Documentation / @etherfold/core / StreamBuilder
Class: StreamBuilder<ABI, ProcessResultType> ​
Defined in: packages/core/src/streamBuilder.ts:278
The STREAM-BUILDER of ADR-0003, and the receiving half of the wire contract of ADR-0004.
It takes contiguous ranges of raw logs from a stateless log-fetcher, derives every retraction itself, drives an EventProcessor, and is authoritative about where the next range must start. It makes NO chain calls, which is the whole reason it is a separate object from IndexerGeneration: that class opens load() with eth_chainId, so the half of a split deployment that hosts the processor cannot use it (recorded in indexer-load-needs-a-chain-so-the-server-half-cannot-call-it).
Why the cursor is read on every call and never cached ​
The intended host is serverless: several isolates may serve the same database, and an instance can be created and destroyed between two batches. An in-memory cursor would be one instance's private opinion of a value the DATABASE owns, and two instances would disagree the moment either of them moved it. So expectedFromBlock() and receive() both read the persisted cursor through processor.load() first. That costs a read per batch and buys the property the design is built on: the cursor a sender is told is the cursor the state actually has.
The three identities, and who checks which ​
sourceandconfigare asserted by the SENDER, on every batch, and checked here against what this receiver indexes (WireContextMismatchError).processorcannot be asserted by the sender at all, so it is checked against the PERSISTED cursor instead: a cursor written by another processor version, or for another source, is state that means something else, and it is not resumed on top of. WHAT HAPPENS TO IT depends on whether there is a container above this receiver: with one, the running fold is a GENERATION beside the stored one and nothing is discarded; without one, the state is discarded (processor.clear()), which is the holedocs/reviews/todo-triage.mdfound in every persistence layer, closed the only way a receiver holding a single store can close it. SeecurrentLastSyncandreceivingContainer.ts.
What it deliberately does not do ​
It does not batch a large stream into several process() calls the way IndexerGeneration.feed does. A batch is one HTTP request and the sender chose its size; splitting it here would add a second place where a partially-applied range is possible, and the processor already applies block by block atomically.
The two things it hands OUT, and the one it must not lose ​
It knows no database, so both durable facts a fold concludes leave through a PORT the store's owner supplies: the reorg COUNT (recordReorg, ADR-0050) and the emission STREAM (appendEmissions, ADR-0052). Neither is a fact about the transport, so neither may live on an entrance: a combined process folds through createDirectIngestion and reaches no route, and putting either write at both entrances would double it on the shape that both concludes and receives.
They are ORDERED DIFFERENTLY on purpose, and that is the only asymmetry between them. The count is taken AFTER the batch was applied and its failure is swallowed; the stream is written BEFORE, and its failure refuses the batch. See noteReorg and receive.
Type Parameters ​
ABI ​
ABI extends Abi
ProcessResultType ​
ProcessResultType = unknown
Implements ​
Constructors ​
Constructor ​
new StreamBuilder<
ABI,ProcessResultType>(processor,source,config):StreamBuilder<ABI,ProcessResultType>
Defined in: packages/core/src/streamBuilder.ts:322
Parameters ​
processor ​
EventProcessor<ABI, ProcessResultType>
source ​
IndexingSource<ABI>
config ​
StreamBuilderOptions<ABI>
Returns ​
StreamBuilder<ABI, ProcessResultType>
Properties ​
context ​
readonlycontext:WireContext
Defined in: packages/core/src/streamBuilder.ts:284
The {source, config} a sender must assert to be talking to this receiver.
Implementation of ​
LogIngestion.context
defaultFromBlock ​
readonlydefaultFromBlock:number
Defined in: packages/core/src/streamBuilder.ts:280
The earliest block this source can have anything to say about.
streamConfig ​
readonlystreamConfig:UsedStreamConfig
Defined in: packages/core/src/streamBuilder.ts:282
The resolved stream config, which is what config in the context hashes.
streamDigest ​
readonlystreamDigest:string
Defined in: packages/core/src/streamBuilder.ts:286
WHICH stream this folds, as everything that stores its emissions keys them. See LogIngestion.
Implementation of ​
LogIngestion.streamDigest
Accessors ​
generation ​
Get Signature ​
get generation():
GenerationId
Defined in: packages/core/src/streamBuilder.ts:296
WHICH generation this is: the stream it folds, and the fold itself.
A GETTER rather than a field only because the stream half is one: the fold half is the identity this receiver's ARRIVAL derived (ADR-0086), which is a constant, since the config a bundle was built with is IN the bundle. See LogIngestion.
Returns ​
Implementation of ​
LogIngestion.generation
Methods ​
expectedFromBlock() ​
expectedFromBlock():
Promise<number>
Defined in: packages/core/src/streamBuilder.ts:365
Where the next batch must start.
It is NOT lastToBlock + 1: it reaches back to latestBlock - finality so the unconfirmed window is re-delivered every round, which is the only way a reorg is ever detected. A sender that computed this itself would be holding the state that makes it stateless.
Reading this can WRITE, and WHICH write depends on whether a container is attached (see currentLastSync). WITHOUT one, a persisted cursor belonging to another source, config or processor version is cleared here rather than merely ignored: the alternative is answering with a number derived from state this server is about to wipe, so the read and the following write would disagree. Nothing is lost that was not already invalid, and the same reconciliation happens on load() in the single-process shape. WITH one, the write is a REGISTRY write instead -- the fold this receiver runs is resolved-or-created as a generation -- and no state is discarded at all.
Returns ​
Promise<number>
Implementation of ​
LogIngestion.expectedFromBlock
receive() ​
receive(
batch):Promise<IngestionResult<ABI>>
Defined in: packages/core/src/streamBuilder.ts:378
Apply one batch, or refuse it having applied nothing.
The order of the three refusals is deliberate. Identity first, because a batch for another source must never be told to resume from a block number that means nothing to it. Then the envelope, which is cheap and local. Then the cursor, which is generateStreamToAppend's own check and therefore the engine's, not a second one that could disagree with it.
Parameters ​
batch ​
WireBatch<ABI>
Returns ​
Promise<IngestionResult<ABI>>
Implementation of ​
LogIngestion.receive