Documentation / @etherfold/browser / BrowserStateStoreFactory
Type Alias: BrowserStateStoreFactory ​
BrowserStateStoreFactory = (
declarations) =>StateStoreBackend|Promise<StateStoreBackend>
Defined in: packages/browser/src/storage/state-store/BrowserStateStore.ts:115
Build the StateStore a browser deployment keeps its entity state in.
This is where an application developer CHOOSES a storage backend for the browser, and the choice is a line of configuration rather than a change to any processor: the processor is entity declarations plus on<EventName> handlers over a MutationContext, and it names no backend at all.
// the default: versioned rows in IndexedDB (ADR-0024). The CLAIM is what turns
// a store into one a generation may fold into (ADR-0077), and it is taken here
// because this factory is the one place that knows this tab means to INDEX.
const createState = async (context, {signal}) =>
openForWriting(await createBrowserStateStore(processor.entities), {signal});
// the same app on the light store instead: nothing else changes
const createState = async (context, {signal}) =>
openForWriting(
await createBrowserStateStore(processor.entities, {
backend: (entities) => new PatchStateStore(entities, {retention: 'revert-only', finalityDepth: 64}),
}),
);
// ...and either one indexes through the hook, with the processor untouched.
// It is the FACTORY the hook takes, not the store: each GENERATION folds into
// its own state, so the hook is what calls this, once per generation.
const indexer = createIndexerState({
createState,
createProcessor: (store) => fromEntityProcessor(processor)(store),
});This function does NOT claim, and that is deliberate. A tab that only RENDERS opens the same store (it is the same origin and the same database), so claiming here would have every reading tab take the store from the tab that is indexing. Building a store and becoming its writer are two acts, and openForWriting is the second one.
Does a RELOAD keep it? Ask the store, at startup ​
This is the question that is specific to a browser, and the answer is a property of the backend rather than of this function. A server restarts into the database it left; a tab is CLOSED, and what it finds when it opens again decides whether it resumes or indexes the chain a second time.
The sync cursor lives BEHIND this seam (ADR-0027), written in the same transaction as the block it describes, so it survives exactly as far as the store does. On the IndexedDB default that is across reloads: the tab picks up from its cursor. @etherfold/state-store-patch is memory-only by design (ADR-0023) and reports durability: 'memory-only' in its capabilities, so a reload there starts over -- correctly, and knowably in advance rather than from an empty tab:
if ((store.capabilities as {durability?: string}).durability === 'memory-only') {
// this app will re-index on every reload; decide whether that is acceptable
}durability is not at the seam because every other backend answers it by existing on disk; it is on the report of the one backend for which the answer is interesting. See PatchStateStoreCapabilities.
Starting from a published snapshot instead of replaying the chain ​
A tab does not have to index from the start block. @etherfold/processor-entities can install state another indexer already computed -- several published locations, the most advanced one wins, local state kept when local is already ahead, an unreachable mirror skipped:
const {store} = await openAndBootstrap(
await createBrowserStateStore(processor.entities),
['https://mirror-a.example/state.json', 'https://mirror-b.example/state.json'],
// WHICH FOLD is about to index: the identity this app's arrival derived and
// handed the hook (`BrowserGenerationSpec.processorIdentity`, ADR-0086). A
// snapshot computed by a different fold is not a candidate at all, which is
// the rule this value is compared for -- nothing parses it.
{processor: processorIdentity, finalityDepth: 64},
);Open it through openSnapshotAware on EVERY boot, not only the one that installs a snapshot (openAndBootstrap above does it for you). A snapshot carries no history below the block it was taken at, so a bootstrapped store must report a floor there and refuse as-of reads below it; the floor is persisted, and a handle constructed without reading it back would go on claiming the history of a store that has been indexing since genesis. See ADR-0028.
Why IndexedDB is the default ​
Measured rather than preferred, on the real workload, and recorded as a CONDITION in ADR-0024: what would have to be true for wasm SQLite to win, and what would overturn the answer. The short of it is that IndexedDB beat wasm SQLite on writes by 1.6x to 6.9x and on reads by 4x to 14x on every engine that can run both, that WebKit cannot run the SQLite route at all, and that three of four tabs fail AT OPEN on both SQLite VFSs.
What it replaced, and what that cost ​
There used to be a second way to write a browser deployment's state to IndexedDB: keepStateOnIndexedDB, a KeepState keeper that serialised the WHOLE state object of a JSObjectEventProcessor on every save. It was the fastest writer at today's sizes (2.0 ms/block on Chromium) precisely because it kept no history, could not answer an as-of read and could not revert. It is deleted with the free-form processor path (ADR-0037). This builds a store behind the seam: the state IS versioned rows, so a write costs what CHANGED, a reload reads only what it asks for, a reorg is a revert, and history is readable to the declared retention.
Parameters ​
declarations ​
readonly EntityDeclaration[]
Returns ​
StateStoreBackend | Promise<StateStoreBackend>