Skip to content

Documentation / @etherfold/processor-entities / SnapshotAwareStateStore

Interface: SnapshotAwareStateStore ​

Defined in: packages/state-store/dist/snapshot.d.ts:114

The store handle a deployment that MAY start from a snapshot uses -- on every boot, not only on the one that installs it.

It is a thin decorator over any StateStoreBackend, and it does exactly three things: it installs a snapshot as one unit, it remembers (durably) which block the contents came from, and it makes every read and every revert respect that floor. A store that was never bootstrapped is a pass-through, reporting whatever the store underneath reports.

ts
const store = await openSnapshotAware(await createBrowserStateStore(processor.entities));
// the identity this deployment's ARRIVAL handed its fold (ADR-0086), never a
// value the processor was asked for: it is compared for equality and never parsed
await store.bootstrap(snapshot, {processor: processorIdentity});

A host that fetches its snapshot from published mirrors, and that wants "only if this store has never synced" decided for it, uses openAndBootstrap in @etherfold/processor-entities instead: knowing how far the local store has got means reading lastToBlock out of a cursor, and the cursor is an opaque string here on purpose (ADR-0027).

The tip it measures a window from ​

A window is a distance from the tip, and the seam has no verb that reports one, so this handle tracks the highest block it has seen: the snapshot's when it opens, and each applied block after that. The FLOOR -- the number a caller acts on, and the one a read is refused at -- is exact either way, because it is the snapshot's block and not a distance. What is approximate is the width REPORTED between a reload and the first block applied in that session: a store that indexed a thousand blocks past its snapshot, was reloaded, and has not yet applied a block reports a narrower window than it can actually answer. That is the safe direction (it claims LESS than it holds, never more) and it corrects itself on the first applyBlock.

Implements ​

Properties ​

accessor? ​

readonly optional accessor?: (options?) => unknown

Defined in: packages/state-store/dist/snapshot.d.ts:138

THE QUERY LAYER'S TWO READS (ADR-0099), present only when the store underneath has them, detected as ClaimedStateStore detects them (typeof ... === 'function'), so a store without them still gets the query handler's clear refusal rather than a failure at call time. Neither is part of the seam, and both are typed loosely for the reason the claimed handle's are: the accessor's type lives in @etherfold/accessor, which depends on this package.

accessor keeps THIS handle's as-of rule, which is the whole reason it is not a plain forward: every accessor read that carries a block (at) is checked against the NARROWED claim first, with the same assertReadable getAsOf and listAsOf use, so a block below the snapshot's floor is refused with BlockNotRetainedError and never answered from rows that have no history below it (ADR-0095, ADR-0028). A tip read (no at) goes straight through, and a store that was never bootstrapped is a pass-through, as its other reads are. See floored.

tip is the store underneath's, unchanged.

Parameters ​

options? ​

undefined

Returns ​

unknown


blockAt? ​

readonly optional blockAt?: (number) => Promise<BlockPointer | undefined>

Defined in: packages/state-store/dist/snapshot.d.ts:145

The query layer's block reads and revert sequence (query-reads.ts), the store underneath's, unchanged. A hash below the floor needs no refusal here: an install records no block below it, so none resolves.

The block recorded at a height, or undefined when none is.

Parameters ​

number ​

number

Returns ​

Promise<BlockPointer | undefined>


blockOf? ​

readonly optional blockOf?: (hash) => Promise<BlockPointer | undefined>

Defined in: packages/state-store/dist/snapshot.d.ts:146

The recorded block a HASH names, or undefined when none is. The hash is normalised as the store normalises it on write (normalizeBlockHash, ADR-0015), so an echoed-back upper-case hash still resolves.

Parameters ​

hash ​

string

Returns ​

Promise<BlockPointer | undefined>


revertSequence? ​

readonly optional revertSequence?: () => Promise<number>

Defined in: packages/state-store/dist/snapshot.d.ts:147

How many times this store has reverted: a monotonic count, persisted, and incremented in the SAME transaction as every revertTo. 0 before the first.

Returns ​

Promise<number>


tip? ​

readonly optional tip?: () => Promise<number | undefined>

Defined in: packages/state-store/dist/snapshot.d.ts:139

The highest recorded block's number, or undefined before the first.

Returns ​

Promise<number | undefined>

Accessors ​

capabilities ​

Get Signature ​

get capabilities(): StateStoreCapabilities

Defined in: packages/state-store/dist/snapshot.d.ts:175

What the store underneath claims, narrowed by the history this one never received.

Three cases, and the two that pass through are as important as the one that does not:

  • Not bootstrapped: the inner report, untouched. There is no floor to impose and imposing one would refuse reads the store can answer.
  • revert-only, or no as-of reads at all: also untouched. A store that answers no historical read refuses everywhere already, which is strictly stronger than a floor.
  • Otherwise: a window from the snapshot's block to the tip, intersected with whatever the deployment configured. Both are floors on the same answer, so the report is the tighter one -- and because both are expressed as a distance behind the tip, the intersection is expressible in the same vocabulary rather than needing a new retention kind.

Any extra fields a backend puts on its report (durability on the patch store, for instance) are preserved: this narrows a claim, it does not replace one.

Returns ​

StateStoreCapabilities

Implementation of ​

StateStoreBackend.capabilities


declarations ​

Get Signature ​

get declarations(): ReadonlyMap<string, NormalizedEntity>

Defined in: packages/state-store/dist/snapshot.d.ts:152

The declared entities, after validation.

Returns ​

ReadonlyMap<string, NormalizedEntity>

Implementation of ​

StateStoreBackend.declarations


snapshotOrigin ​

Get Signature ​

get snapshotOrigin(): number | undefined

Defined in: packages/state-store/dist/snapshot.d.ts:151

The block this store's contents came from, or undefined if it computed them itself.

Returns ​

number | undefined

Methods ​

applyBlock() ​

applyBlock(block, mutations?, cursor?): Promise<void>

Defined in: packages/state-store/dist/snapshot.d.ts:259

Apply one block: the block itself plus every mutation, as ONE atomic unit.

Which blocks get recorded is the CALLER's judgement: every block handed over is recorded, including one that carried no mutation, and nothing else is. A block that carries a log of ours which changes nothing is still a block a consumer can legitimately pin.

The height must be ABOVE the recorded tip, which is wider than refusing a duplicate and is the invariant a single writer maintains anyway: a caller reverts to the fork BEFORE it applies the branch that replaces it (see applyEventStream in @etherfold/processor-entities), so every apply lands above what the store holds. An offer at or below the tip is therefore a writer working from a position the store has passed -- a backgrounded tab resuming on a stale cursor, a second instance -- and taking it would open a version underneath the live one rather than after it. The tip is read inside the same atomic unit as the write, so a revert lowering it and an apply above it cannot interleave with another writer. An EMPTY store has no tip and admits whatever height its caller starts at, which is what a fresh index, a rebuild resuming mid-chain and a bootstrap installing a snapshot all need.

cursor joins that unit. It is how a processor's "I have got this far" stops being a separate round trip: the block and the cursor that describes it move together or neither moves, so a crash can never leave state ahead of the cursor (a wedge, since the replay re-applies a block the store holds) nor a cursor ahead of the state (silent loss, since the replay skips a block nothing applied). A backend with no transaction to join must still not write the cursor when the block fails. See cursor.ts.

Parameters ​

block ​

BlockPointer

mutations? ​

readonly Mutation[]

cursor? ​

CursorWrite

Returns ​

Promise<void>

Implementation of ​

StateStoreBackend.applyBlock


bootstrap() ​

bootstrap(snapshot, options?): Promise<void>

Defined in: packages/state-store/dist/snapshot.d.ts:256

Install a snapshot document: check its head, record where its contents came from, then replay its blocks through applyBlock, the cursor riding the LAST.

Streaming, one block at a time ​

The document is inflated and parsed as it arrives (readSnapshot), and each block is applied the moment the next one's opening line proves it complete, so the install holds at most ONE block's mutations and never the document. A block is one applyBlock, which is one atomic unit on every backend, so a snapshot without history (none, the floor at the cut) holds its live rows once, while they are written: that is intended (ADR-0095), and it is what keeps installing a replay rather than a new verb on every backend.

Everything checkable before a write is checked before one: the format and the processor from the head, and the entity declarations and the floor block from the first block, which is read in full before the marker goes down. A document that goes wrong LATER (a download cut short) leaves the marker and whatever blocks had landed; for a none snapshot that is the marker over an empty store, the recoverable case below, and with history it is the marker over the floor and some later blocks, which the next install replaces (below).

The order, which is the interesting part ​

The origin marker is written BEFORE the rows, and it is a separate write because the seam carries exactly one cursor slot per applyBlock and the sync cursor has it. So there is a window, and the ordering decides which way a crash inside it falls:

  • Marker first (this): a crash leaves a floor recorded over an EMPTY store. The store then claims less history than it has (it will index from the start block and refuse as-of reads below the floor anyway), which is over-cautious rather than wrong, and running the bootstrap again clears it because applyBlock never happened.
  • Marker last: a crash leaves rows and a cursor with NO floor, and the store answers historical reads about blocks it has nothing for. That is the exact failure this module exists to prevent.

Same reasoning as the cursor being written last in a store with no transaction to join: where atomicity is unavailable, the ORDER has to be the safe one.

Whatever the store held is replaced, never built on ​

An install REPLACES the store: once the new document's head and floor have been checked (so a refused document still changes nothing), the store is WIPED (revertTo(-1), which drops the rows, the recorded blocks and any origin marker) and the document's cursor key is cleared, and only then is the marker written. It is unconditional, because this handle has no verb that says whether the store holds anything, and a wipe of an empty store is a no-op. Three stores reach it holding state, and each is wrong to build on:

  • An interrupted install. A snapshot that carries history is SEVERAL applyBlocks, so a download cut short part-way leaves the marker, the floor and some later blocks, and no cursor (it rides the last block). That is not a complete install and is not treated as one (openAndBootstrap finds no cursor and bootstraps again), and replaying a document over it would offer blocks the store already holds, which every backend refuses.
  • A complete earlier install a caller chose to replace with a more advanced one: laying a floor's live rows over it would keep every row the newer snapshot no longer has.
  • A store that computed its own state and is behind the snapshot (bootstrapFromSnapshot installs whenever the local cursor is below the best candidate). The floor carries only LIVE rows, so a row the chain deleted between this store's tip and the floor would survive as a stale row nobody reports, and a floor at or below the store's own tip would be refused by applyBlock. A tab that is behind starts from the snapshot rather than catching up from its cursor, because the gap after a long absence is the historical eth_getLogs range a public node may refuse, which is what a snapshot exists to avoid.

The cursor is cleared BEFORE the rows, for the reason the marker goes down first: a revert leaves cursors alone (how far the caller got is not entity state), so without this a replaced store's old cursor would sit over a partly installed snapshot, and a download cut short would leave a store that claims to have synced to a block its rows are not the state of. Cleared, an interrupted install is cursor-less whatever the store held before, which is the case the next boot already bootstraps again.

Parameters ​

snapshot ​

SnapshotReader | SnapshotDocument

options? ​
processor? ​

string

Returns ​

Promise<void>


clearCursor() ​

clearCursor(key): Promise<void>

Defined in: packages/state-store/dist/snapshot.d.ts:262

Forget a cursor. A no-op where none was written.

Paired with wiping the state it points at, never on its own: a cursor without its state would have a caller resume into an empty store.

It is a MUTATION even when it deletes nothing, and a backend that claims singleWriter must claim on it anyway -- the same obligation clearSeamRecord carries, which is the one openForWriting actually uses.

Parameters ​

key ​

string

Returns ​

Promise<void>

Implementation of ​

StateStoreBackend.clearCursor


clearSeamRecord() ​

clearSeamRecord(key): Promise<void>

Defined in: packages/state-store/dist/snapshot.d.ts:265

Forget one of the seam's records. A no-op where none was written.

It is a MUTATION even when it deletes nothing, and a backend that claims singleWriter must claim on it anyway. That is what openForWriting rests on: clearing a record nothing ever wrote is the one verb that is a guaranteed no-op on every backend, so it is how a claim is taken without touching a byte -- and now without touching the caller's namespace either.

Parameters ​

key ​

"snapshotOrigin" | "retentionEnforcement" | "writerClaim" | "revertSequence"

Returns ​

Promise<void>

Implementation of ​

StateStoreBackend.clearSeamRecord


getAsOf() ​

getAsOf<T>(entity, id, at): Promise<T | undefined>

Defined in: packages/state-store/dist/snapshot.d.ts:281

One entity as of a block NUMBER.

undefined means the entity was absent at that block. A store that cannot answer historical reads at all reports asOf: false and refuses rather than answering from the tip.

Type Parameters ​

T ​

T = Record<string, unknown>

Parameters ​

entity ​

string

id ​

EntityId

at ​

number

Returns ​

Promise<T | undefined>

Implementation of ​

StateStoreBackend.getAsOf


getCurrent() ​

getCurrent<T>(entity, id): Promise<T | undefined>

Defined in: packages/state-store/dist/snapshot.d.ts:279

One entity as it stands at the tip.

Type Parameters ​

T ​

T = Record<string, unknown>

Parameters ​

entity ​

string

id ​

EntityId

Returns ​

Promise<T | undefined>

Implementation of ​

StateStoreBackend.getCurrent


listAsOf() ​

listAsOf<T>(entity, prefix, at, limit): Promise<Listing<T>>

Defined in: packages/state-store/dist/snapshot.d.ts:282

The same listing as of a block NUMBER: the children that were live then.

Refused, not answered from the tip, by a store whose retention does not cover that block -- the same contract as getAsOf, for the same reason.

Type Parameters ​

T ​

T = Record<string, unknown>

Parameters ​

entity ​

string

prefix ​

EntityIdPrefix

at ​

number

limit ​

number

Returns ​

Promise<Listing<T>>

Implementation of ​

StateStoreBackend.listAsOf


listCurrent() ​

listCurrent<T>(entity, prefix, limit): Promise<Listing<T>>

Defined in: packages/state-store/dist/snapshot.d.ts:280

The rows whose declared id starts with prefix, at the tip, in ascending id order, at most limit of them. The id order is UTF-8 byte order, each column in turn (compareIds, ADR-0021), on every backend.

This is the derived collection a one-to-many is read through, and the REQUIRED limit is the whole reason it can be asked of any backend: the operation is a key-prefix range with a bound, which is an indexed range scan on every substrate. truncated says whether more matched, because a set that exactly fills the limit is otherwise indistinguishable from a cut-off one.

Type Parameters ​

T ​

T = Record<string, unknown>

Parameters ​

entity ​

string

prefix ​

EntityIdPrefix

limit ​

number

Returns ​

Promise<Listing<T>>

Implementation of ​

StateStoreBackend.listCurrent


migrate() ​

migrate(): Promise<void>

Defined in: packages/state-store/dist/snapshot.d.ts:176

Bring the storage to the declared shape. Idempotent, so it is safe on every boot.

It is NOT a mutating path in the writer-token sense and must never claim: opening a store is not writing to it, and every tab of one app opens.

Returns ​

Promise<void>

Implementation of ​

StateStoreBackend.migrate


prune() ​

prune(options?): Promise<PruneReport>

Defined in: packages/state-store/dist/snapshot.d.ts:266

Delete the versions the declared retention no longer covers, and report what went.

It is an EXPLICIT call, and that placement is the decision. The window bounds what a read may ask about at all times, whether or not this ever runs; what this adds is the other half, bounding the BYTES. It is deliberately not a side effect of applyBlock, because a prune costs real time (1.1 s at 62,553 versions, measured in work/notes/findings/sqlite-in-the-browser.md) and a block carries a median of 7 mutations, so folding it in would stall whichever block happened to cross a threshold by a second for work that block did not ask for. Which block pays, and how often, is the host's scheduling decision, and a store is the wrong place to invent one. An amortised policy is maxVersions on a schedule; a background policy is this call on a timer. Both are built ON this verb rather than instead of it.

The caller is the writer: a store has one, and pruning between blocks is safe exactly where applying a block is.

Pruning a store that has no floor to prune at (unbounded, or revert-only with no declared finality depth) is a NO-OP and never an error: "keep everything" is a legitimate answer to "drop what is unreachable", and a host that prunes on a timer must not have to ask what it is holding first.

The LIVE version of an entity is never dropped, however old it is. A row written once at block 12,082,307 and never touched again is still the current state, and deleting by age alone destroys it (see retentionFloor).

Parameters ​

options? ​

PruneOptions

Returns ​

Promise<PruneReport>

Implementation of ​

StateStoreBackend.prune


readCursor() ​

readCursor(key): Promise<string | undefined>

Defined in: packages/state-store/dist/snapshot.d.ts:260

The cursor stored under key, or undefined if nothing was ever written there.

Opaque: whatever string the caller last wrote, byte for byte.

Parameters ​

key ​

string

Returns ​

Promise<string | undefined>

Implementation of ​

StateStoreBackend.readCursor


readRetentionEnforcement() ​

readRetentionEnforcement(): Promise<RetentionEnforcement>

Defined in: packages/state-store/dist/snapshot.d.ts:278

Delegated whole, and NOT narrowed the way capabilities is.

The two questions are about different things, which is why the same handle answers them differently. capabilities is narrowed because a bootstrapped store must not CLAIM history it never received. This one is about whether versions are physically dropped, and the snapshot floor drops nothing: there is nothing below it to delete, because the snapshot IS the oldest state this store has. So an unbounded store bootstrapped from a snapshot reports a window here and no-floor there, and both are true of it.

Returns ​

Promise<RetentionEnforcement>

Implementation of ​

StateStoreBackend.readRetentionEnforcement


readSeamRecord() ​

readSeamRecord(key): Promise<string | undefined>

Defined in: packages/state-store/dist/snapshot.d.ts:263

The seam's own record under key, or undefined if none was written.

Opaque in the same way a cursor is -- whatever string was last written, byte for byte -- and kept somewhere a CALLER cannot address, which is the whole point of the port (records.ts). A store that kept these beside the cursors would be handing the seam's memory to whoever picked the same name.

A READ, so it never claims: openSnapshotAware runs on every boot, including in a tab that only renders.

Parameters ​

key ​

"snapshotOrigin" | "retentionEnforcement" | "writerClaim" | "revertSequence"

Returns ​

Promise<string | undefined>

Implementation of ​

StateStoreBackend.readSeamRecord


revertTo() ​

revertTo(keepUpTo): Promise<void>

Defined in: packages/state-store/dist/snapshot.d.ts:292

Roll back, unless that would reach under the snapshot.

A WIPE (keepUpTo < 0, which is what EntityEventProcessor.reset() calls) is not a reorg and is not refused: it drops the rows, so there is no snapshot-derived state left to be honest about, and the floor goes with them. The store then reports what it was configured to keep, which is true of it again the moment it is empty.

Parameters ​

keepUpTo ​

number

Returns ​

Promise<void>

Implementation of ​

StateStoreBackend.revertTo


writeCursor() ​

writeCursor(key, value): Promise<void>

Defined in: packages/state-store/dist/snapshot.d.ts:261

Move a cursor on its own, with no block.

Needed because progress is not only blocks. A processor that scanned a range carrying none of its logs has advanced and written nothing, and it must be able to say so or it re-scans that range on every restart forever. It is also how a BOOTSTRAP installs a cursor that belongs to rows it did not compute.

It is NOT the way to record progress that a block DID cause: pass the cursor to applyBlock instead, which is the whole point of it being here.

Parameters ​

key ​

string

value ​

string

Returns ​

Promise<void>

Implementation of ​

StateStoreBackend.writeCursor


writeSeamRecord() ​

writeSeamRecord(key, value): Promise<void>

Defined in: packages/state-store/dist/snapshot.d.ts:264

Write one of the seam's records.

A MUTATION, guarded exactly as writeCursor is on a backend that enforces a single writer: the snapshot origin is a claim about what the storage holds, so a writer that has lost the store must not be able to move it.

Parameters ​

key ​

"snapshotOrigin" | "retentionEnforcement" | "writerClaim" | "revertSequence"

value ​

string

Returns ​

Promise<void>

Implementation of ​

StateStoreBackend.writeSeamRecord