Documentation / @etherfold/processor-entities / EntityEventProcessor
Class: EntityEventProcessor<ABI, ProcessorConfig> ​
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:87
The EventProcessor that runs an EntityProcessor against ANY StateStore.
This is the runtime the seam was built for and the one thing that was missing from it. Everything underneath already named no backend -- MutationContext, the entity declarations, runBlockHandlers, applyEventStream -- while the only shipped EventProcessor over the seam built its own SQLite store from a RemoteSQL and kept its cursor in a SQL table. So "one processor, several backends" was true in the test suite and reachable through no deployment. Here the store is INJECTED, and everything this class does to it is at the seam, so the same processor object indexes to SQLite on a server, to IndexedDB in a browser tab, to the light patch store, or to memory in a test, with nothing about the processor changed.
const store = await createBrowserStateStore(myProcessor.entities); // browser
const store = new VersionedStateStore(db, myProcessor.entities); // server
const processor = new EntityEventProcessor(store, myProcessor);Revert is replay-free, and that is ADR-0001's revisit condition arriving ​
The retired in-memory path (a free-form object plus a patch History) undid a reorg with immer reverse-patches, because reverting by replay needs the state as of the fork block and the in-browser path had no way to get it. ADR-0001 records that choice and names the condition that would justify revisiting it: a store that can answer "state as of block N". A versioned StateStore is that store, and revertTo(keepUpTo) is neither replay nor an undo log, because the history IS the storage model. A backend that keeps a patch log instead (@etherfold/state-store-patch) implements the same verb its own way, which is precisely what the seam is for.
Where the cursor lives ​
In the store, as an opaque string, written in the same transaction as the block it describes. That is not filing: a cursor kept anywhere else is a second round trip after the block, and a crash in that window leaves state ahead of the cursor, which no restart clears. See cursor.ts here for what the string means and cursor.ts at the seam for why the store owns it.
What this is NOT ​
Not a second implementation of anything. VersionedStateEventProcessor in @etherfold/processor-sqlite is now a thin wrapper over this class that builds the store from a RemoteSQL and hands back the SQL-tier read handle; revert-then-apply exists once, here, which is the drift this whole seam exists to prevent.
Type Parameters ​
ABI ​
ABI extends Abi
ProcessorConfig ​
ProcessorConfig = undefined
Implements ​
Constructors ​
Constructor ​
new EntityEventProcessor<
ABI,ProcessorConfig>(store,processor,options?):EntityEventProcessor<ABI,ProcessorConfig>
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:105
Parameters ​
store ​
processor ​
EntityProcessor<ABI, ProcessorConfig>
options? ​
EntityEventProcessorOptions = {}
Returns ​
EntityEventProcessor<ABI, ProcessorConfig>
Properties ​
source ​
protectedsource:IndexingSource<ABI> |undefined
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:94
Kept for the context a future rebuild/upgrade path will need; not read on the hot path.
store ​
protectedreadonlystore:WritableStateStore
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:106
Accessors ​
state ​
Get Signature ​
get state():
EntityStateView
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:114
The read handle, also what load and process hand back.
Returns ​
Methods ​
clear() ​
clear():
Promise<void>
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:273
Same as reset.
The retired free-form path distinguished the two because it had two places to forget: reset dropped the object it held, clear also asked its persistence keeper to drop the saved copy. Here there is one place, the store, so collapsing them is the honest implementation rather than a shortcut. The core calls clear on a context mismatch and reset on a rebuild, and both mean the same thing to a processor whose state is never anywhere else.
Returns ​
Promise<void>
Implementation of ​
EventProcessor.clear
configure() ​
configure(
config):void
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:158
Parameters ​
config ​
ProcessorConfig
Returns ​
void
getCodeFingerprint() ​
getCodeFingerprint():
string|undefined
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:154
A DIGEST OF THE AUTHOR'S HANDLER SOURCE; see EventProcessor.getCodeFingerprint.
Taken from the AUTHOR'S object, whose on<Event> functions and handleUnparsedEvent are the logic, and never from this wrapper, whose own methods are identical for every processor ever built this way. It is what names the one arrival with no bytes to hash -- a module a dev server handed a browser tab (moduleProcessorIdentity, @etherfold/browser, ADR-0086) -- and nothing else reads it.
Returns ​
string | undefined
Implementation of ​
EventProcessor.getCodeFingerprint
load() ​
load(
source,streamConfig):Promise<{lastSync:LastSync<ABI>;state:EntityStateView; } |undefined>
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:184
PREPARE THIS FOLD TO ADVANCE, and answer its persisted state and cursor, or undefined when it has folded nothing.
It is handed the source THIS FOLD folds under, and only ever that one. A processor MAY key its cursor by source and answer undefined for a source it never folded under; neither shipped implementation does (their cursor is keyed by the generation's own namespace, ADR-0053), which is exactly why a caller handing the WRONG source would go unnoticed, so the callers are what guarantee it. The callers are the engines that DRIVE one fold, each built with that fold's own source (IndexerGeneration on the chain-facing container, GenerationRebuild on the receiving one, StreamBuilder for a lone receiver), and the one-shot preparations of a fold about to be driven (replayFixtureInto, the CLI's index start-up). No container calls it with a source of its own.
It is not a READ, and no cursor read goes through it. It may write: the entity processors record the finality, run their migrations and check retention here, so it belongs to the advance path, which is where it is called. What a container COMPARES or REPORTS -- the promotion trigger, /status, folding() -- is read without it: the chain-facing container keeps the cursor each engine last reported, and the receiving one reads the persisted cursor by identity (GenerationRegistryPort.readStateCursor), which also answers for a generation this process holds no fold for. Pinned by packages/core/test/aFoldIsLoadedOnlyWithTheSourceItFolds.test.ts, with a double that honours source.
Parameters ​
source ​
IndexingSource<ABI>
streamConfig ​
Returns ​
Promise<{ lastSync: LastSync<ABI>; state: EntityStateView; } | undefined>
Implementation of ​
EventProcessor.load
process() ​
process(
eventStream,lastSync):Promise<EntityStateView>
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:224
Apply a stream, reverting first if any of it is a retraction, and move the cursor WITH the blocks that caused it. Report both, if anybody is listening.
All of it is applyEventStream, at the seam: revert ONCE at the fork point, group by block hash, run the handlers with read-your-writes, apply each block as one atomic unit, and hand each applyBlock the cursor that describes that block. None of it is SQL and none of it is duplicated per backend; the reasoning lives on applyEventStream and on cursor.ts.
There is nothing left here, and that is the point: the cursor used to be the half this shell owned, written as a second round trip after the last block, and that second round trip was a live defect.
Parameters ​
eventStream ​
LogEvent<ABI>[]
lastSync ​
LastSync<ABI>
Returns ​
Promise<EntityStateView>
Implementation of ​
EventProcessor.process
prune() ​
prune(
options?):Promise<PruneReport>
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:139
Enforce the configured retention against the storage: drop the versions it no longer covers.
It lives HERE and not on state because it is a WRITE, and the view is read-only for the reason recorded on it: a UI callback holding a handle that can delete versions is a corruption waiting to happen. The processor is the writer, so scheduling a prune is scheduling one more thing the writer does.
It is a call a deployment makes rather than something process does on its own, and that placement is deliberate: pruning costs time proportional to what it drops (1.1 s at 62,553 versions, measured in work/notes/findings/sqlite-in-the-browser.md), so a prune inside process would stall whichever block crossed a threshold. See StateStore.prune for the whole reasoning and for maxVersions, which is how an amortised policy is expressed. Call it BETWEEN process calls: it is a write, and this processor has one writer.
On the default (unbounded) retention it is a no-op, so a host may schedule it unconditionally.
Parameters ​
options? ​
Returns ​
Promise<PruneReport>
reset() ​
reset():
Promise<void>
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:255
Wipe the state back to empty.
revertTo(-1) is the whole of the state half, through the store's public API: every version has a lower bound at or above 0, so "drop everything opened above -1" is "drop everything", and the same call clears the recorded blocks. The cursor is dropped alongside, because a cursor without the state it points at would have the core resume into an empty store.
Two calls rather than one atomic unit, and here that is fine where it was not for process: reset is idempotent, so a crash between them is cleared by running it again, which is exactly what the caller does.
Returns ​
Promise<void>
Implementation of ​
EventProcessor.reset
setFoldReporter() ​
setFoldReporter(
reporter):void
Defined in: packages/processor-entities/src/EntityEventProcessor.ts:180
REPORT WHAT THIS FOLD DID -- each applied block with the entity names it touched, and each retraction with the fork point it reverted to -- to whoever is driving it.
This is the bottom of the signal channel (ADR-0083). Both halves are known HERE and nowhere above: the Mutation objects that name an entity are collected on this path, and the fork point is derived on this path too, from the removed markers core emitted, by the same line that calls revertTo. The container RELAYS them into a StateMoved with the generation and the coherence token. Nothing about process changes, which is the point -- widening it would have reached every package in the tree to move information that already exists here.
A processor that declares no entities reports an EMPTY set per block rather than failing or fabricating one, and narrow invalidation then degrades to whatever the token says, which is correct if coarse.
Parameters ​
reporter ​
FoldReporter | undefined
Returns ​
void
Implementation of ​
EventProcessor.setFoldReporter