Skip to content
docsv0.1.0

FileNonceStore

File-based {@see NonceStoreInterface}: a single JSON file mapping nonce => expiry, guarded by `flock()` across the whole read-prune-check-write cycle. Why `flock()` and not an atomic rename: consuming a nonce is a read-modify-write (read the current set, prune expired entries, check membership, write the updated set back). An atomic rename only makes the final file swap atomic — it does nothing to stop two concurrent callers from both reading the pre-write set and independently deciding the same nonce is still fresh, which is exactly the race this store exists to close. Holding `LOCK_EX` for the full read-modify-write cycle serializes it correctly. For a single small JSON file with the lab's request volume this is the smallest correct primitive; it would need revisiting (e.g. sharding, a real datastore) under real concurrency. Pruning happens inline on every {@see consume()} call rather than via a separate sweep process: entries whose `$expiresAt` is at or before the current time are dropped before the incoming nonce is checked and added, so the store cannot grow unbounded and never needs a cron job.

FileNonceStore::__construct()

public function __construct(string $path, ?Closure $clock = null):

Parameters

Parameters of __construct()
NameTypeDescription
$pathstringPath to the JSON file backing this store; its directory is created on first use if missing.
$clock(\Closure(): int | null)Injectable clock for pruning tests; defaults to `time()`.

FileNonceStore::consume()

public function consume(string $nonce, int $expiresAt): bool

Atomically checks whether `$nonce` has already been consumed and, if not, records it (to expire at `$expiresAt`) in the same locked critical section — returns `true` the first time a nonce is seen, `false` on every subsequent replay. Expired entries are pruned inline on every call, replayed or not.

Parameters

Parameters of consume()
NameTypeDescription
$noncestring
$expiresAtint