Skip to content

Documentation / @etherfold/processor-entities / WritableStateStore

Interface: WritableStateStore ​

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

A store this caller has CLAIMED: the reads, the mutations, and the token that says the claim was taken rather than assumed.

The token is what makes this type unforgeable. Without it a WritableStateStore would be structurally identical to a StateStoreBackend, so any store would satisfy it and "you may write because you claimed" would be a comment rather than a compile error. With it, the only way to obtain one is openForWriting, which claims. That is the same structural move ADR-0044 makes for streams: a follower is HANDED a read-only view rather than asked to behave.

It is deliberately NOT the token in the storage. The value a backend compares inside its transactions stays private to that backend, because a token a caller can read is a token a caller can hand back, and ADR-0075 rejected exactly that shape: a guard whose provenance rests on caller discipline is a read-then-write that merely looks atomic. This one names the claim at the seam and is compared with nothing.

Extends ​

Properties ​

capabilities ​

readonly capabilities: StateStoreCapabilities

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

What this store keeps and what it can answer. Readable before migrate.

Inherited from ​

StateStoreBackend.capabilities


declarations ​

readonly declarations: ReadonlyMap<string, NormalizedEntity>

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

The declared entities, after validation.

Inherited from ​

StateStoreBackend.declarations


token ​

readonly token: string

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

Names THIS claim. Opaque, compared with nothing; see the note above.

Methods ​

applyBlock() ​

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

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

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>

Inherited from ​

StateStoreBackend.applyBlock


applyBlocks()? ​

optional applyBlocks(updates): Promise<void>

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

Apply several blocks, packing as many as fit into each round trip.

OPTIONAL because it is ONE backend's (@etherfold/state-store-sqlite, where backfill is bound by round trips rather than by SQLite work), and it is declared here at all only because this handle WRAPS the store: a caller that used to reach the method on the concrete class would otherwise lose it behind the seam. Feature-detect it (store.applyBlocks?.(...)); a backend without it is not deficient, it simply has nothing to pack.

Parameters ​

updates ​

readonly BlockUpdate[]

Returns ​

Promise<void>


clearCursor() ​

clearCursor(key): Promise<void>

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

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>

Inherited from ​

StateStoreBackend.clearCursor


clearSeamRecord() ​

clearSeamRecord(key): Promise<void>

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

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>

Inherited from ​

StateStoreBackend.clearSeamRecord


getAsOf() ​

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

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

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>

Inherited from ​

StateStoreBackend.getAsOf


getCurrent() ​

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

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

One entity as it stands at the tip.

Type Parameters ​

T ​

T = Record<string, unknown>

Parameters ​

entity ​

string

id ​

EntityId

Returns ​

Promise<T | undefined>

Inherited from ​

StateStoreBackend.getCurrent


listAsOf() ​

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

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

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>>

Inherited from ​

StateStoreBackend.listAsOf


listCurrent() ​

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

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

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>>

Inherited from ​

StateStoreBackend.listCurrent


migrate() ​

migrate(): Promise<void>

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

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>

Inherited from ​

StateStoreBackend.migrate


prune() ​

prune(options?): Promise<PruneReport>

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

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>

Inherited from ​

StateStoreBackend.prune


readCursor() ​

readCursor(key): Promise<string | undefined>

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

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>

Inherited from ​

StateStoreBackend.readCursor


readRetentionEnforcement() ​

readRetentionEnforcement(): Promise<RetentionEnforcement>

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

Whether this store's retention is actually being ENFORCED against its storage: no floor to enforce, a floor and never pruned, or a floor and pruned to a block.

The second half of retention only happens if a host schedules it (prune, below), so a host that rolled its own loop and never does gets the refusals of a bounded store and the footprint of an unbounded one. This is what makes that state discoverable instead of silent.

It is ASYNCHRONOUS and separate from capabilities because the answer is DURABLE: a store pruned before the process died must not come back saying never, so it lives in storage, and the capability getter is synchronous and readable before the storage is even open. See enforcement.ts.

It is a read, so it never claims the writer token and never writes -- which is why it is on THIS side of the split and prune is not.

Returns ​

Promise<RetentionEnforcement>

Inherited from ​

StateStoreBackend.readRetentionEnforcement


readSeamRecord() ​

readSeamRecord(key): Promise<string | undefined>

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

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>

Inherited from ​

StateStoreBackend.readSeamRecord


revertTo() ​

revertTo(keepUpTo): Promise<void>

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

Roll the state back to keepUpTo, dropping everything above it.

Afterwards the store IS the state as of keepUpTo: versions opened above the fork are gone and versions the dead branch closed are live again, so a counter that a reorged block incremented goes back DOWN. That is the canonical bug this design exists to make impossible.

Parameters ​

keepUpTo ​

number

Returns ​

Promise<void>

Inherited from ​

StateStoreBackend.revertTo


writeCursor() ​

writeCursor(key, value): Promise<void>

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

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>

Inherited from ​

StateStoreBackend.writeCursor


writeSeamRecord() ​

writeSeamRecord(key, value): Promise<void>

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

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>

Inherited from ​

StateStoreBackend.writeSeamRecord