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 ​
readonlycapabilities: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 ​
readonlydeclarations:ReadonlyMap<string,NormalizedEntity>
Defined in: packages/state-store/dist/store.d.ts:45
The declared entities, after validation.
Inherited from ​
StateStoreBackend.declarations
token ​
readonlytoken: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 ​
mutations? ​
readonly Mutation[]
cursor? ​
Returns ​
Promise<void>
Inherited from ​
applyBlocks()? ​
optionalapplyBlocks(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 ​
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 ​
at ​
number
Returns ​
Promise<T | undefined>
Inherited from ​
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 ​
Returns ​
Promise<T | undefined>
Inherited from ​
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 ​
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 ​
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 ​
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? ​
Returns ​
Promise<PruneReport>
Inherited from ​
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 ​
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 ​
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 ​
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>