The write pipeline
This page explains what happens between a call to set() and the bytes in storage: the steps of the write pipeline, how
debounceMs delays a write and when a delayed write is flushed, and how a failed write is reported. It is for readers
who need the exact behavior, for example to decide whether to debounce a value that changes on every keystroke, or to
understand why isPersistent() changed. Reading is covered in The read pipeline.
The pipeline is in src/core/state.ts (set, write, store, remove, flush and flushOnHide). The text it writes
is described in Stored format.
The steps of set()
Section titled “The steps of set()”set(next) runs these steps in order. A call on the server throws, because there is no storage to write.
- Resolve the value. The first
set()in a page loads the shared record so that an updater function receives the stored value.nextis the new value, or the result ofnext(previous). A value ofundefinedis the same asreset(). A value that isObject.is-equal to the current one does nothing, so setting a new object with the same contents writes, and setting the same object does not. - Update memory and notify. The record takes the value, is marked as unsaved, and every subscriber is notified synchronously. Components re-render before anything is written.
- Cancel the pending write. A write that was waiting for its debounce is replaced by this one.
- Write now, or later. With
debounceMsof0, the write runs at once. With a larger value, it is scheduled; see Debounce and flush. - Write. The
consentpredicate is evaluated, and storage availability is checked. If consent is withheld or storage cannot be used, nothing is written, andisPersistent()becomesfalse. Otherwise the value is stored. - Store. The value is serialized with JSON or the
serializer, wrapped in an envelope whenversionis set or a Web Storageexpiresis set, and written. A cookie is written todocument.cookieand then read back to confirm it was accepted. - Report the outcome. A successful write clears the unsaved mark and sets
isPersistent()totrue; a failed one sets it tofalse. Subscribers are notified whenisPersistent()changed. - Announce a cookie write. After a successful cookie write, a message is posted on the
BroadcastChannelso other tabs re-read. Web Storage writes need no message, because the browser firesstorageevents in the other tabs.
Write failures
Section titled “Write failures”A write that fails never throws. It is reported to onError as write-failed, the value stays in memory, and
isPersistent() is false until a later write succeeds. A write fails when:
setItemthrows, for example because the storage is full (QuotaExceededError);- a cookie does not read back from
document.cookie, because the browser blocked it or the cookie is over its size limit; - the value cannot be serialized, for example a
serializer.serializethat throws, or a valueJSON.stringifyrejects.
The error carries a cause when something threw; a cookie that does not read back has no underlying error. A set()
that finds storage unavailable reports unavailable when the record loads, as described in
The read pipeline.
reset()
Section titled “reset()”reset() loads the record if needed, cancels a pending write, sets the value to the default, clears the unsaved mark,
notifies subscribers and removes the stored item. A cookie is removed under every name an older version may have written
it, using the definition’s path, domain, secure and sameSite, and a message is posted on the
BroadcastChannel. A Web Storage item is removed with removeItem. Removal does not evaluate the consent predicate:
a visitor who withdraws consent can have the stored item deleted, while the predicate returns false. It notifies
subscribers even when the value was already the default.
Debounce and flush
Section titled “Debounce and flush”With debounceMs greater than 0, step 4 does not write. It stores the write as the record’s pendingWrite and starts a
timer for debounceMs milliseconds. A newer set() cancels both and starts again, so a value that changes on every
keystroke is written once, debounceMs after the last change.
While a write is pending:
- memory and subscribers already hold the new value;
isPersistent()keeps its previous value, because only the write changes it;- the
consentpredicate is evaluated when the write runs, not whenset()was called; - changes made by other tabs are ignored for this record, so they cannot overwrite the pending value;
reset()discards the write.
The first debounced set() of a page installs two listeners, once per page:
| Listener | Fires when | Does |
|---|---|---|
pagehide on window |
The page is navigated away from. | Writes every pending write of every record at once. |
visibilitychange on document |
The tab’s visibility state changes. | Does the same when the state is hidden. |
Flushing writes each pending value immediately, through the same write step as any other write. There is no
beforeunload or unload listener, so using persistent-state does not make a page ineligible for the browser’s
back/forward cache.
What is not flushed
A pending write is lost if the page is destroyed without a pagehide or visibilitychange to hidden, for example when
the browser process is killed. For values that must not be lost, leave debounceMs at 0.