Skip to content

Documentation / @etherfold/core / Indexer

Class: Indexer<ABI, ProcessResultType> ​

Defined in: packages/core/src/container.ts:665

AN INDEXER: several generations, one canonical pointer, one answer set.

Built through openIndexer. See the module JSDoc above for what it adds over a single IndexerGeneration and what it deliberately leaves to later work.

Type Parameters ​

ABI ​

ABI extends Abi

ProcessResultType ​

ProcessResultType = void

Constructors ​

Constructor ​

new Indexer<ABI, ProcessResultType>(options): Indexer<ABI, ProcessResultType>

Defined in: packages/core/src/container.ts:756

Parameters ​

options ​

IndexerOptions<ABI, ProcessResultType>

Returns ​

Indexer<ABI, ProcessResultType>

Properties ​

config ​

protected config: ProvidedIndexerConfig<ABI>

Defined in: packages/core/src/container.ts:706


createGeneration ​

protected readonly createGeneration: (provider, processor, source, config, processorIdentity) => IndexerGeneration<ABI, ProcessResultType>

Defined in: packages/core/src/container.ts:707

Parameters ​

provider ​

EIP1193ProviderWithoutEvents

processor ​

EventProcessor<ABI, ProcessResultType>

source ​

IndexingSource<ABI>

config ​

ProvidedIndexerConfig<ABI>

processorIdentity ​

string

The identity this generation was REGISTERED under, RESOLVED: whatever the spec's arrival supplied, or the processor's own declared hash where it supplied nothing. Handed over so the engine cannot name its fold anything other than what the registry already recorded.

Returns ​

IndexerGeneration<ABI, ProcessResultType>


current ​

protected current: HeldEntry<ABI, ProcessResultType> | undefined

Defined in: packages/core/src/container.ts:751

WHICH generation reads resolve to, and it moves ONLY inside notifyState.

It is not the same thing as the registry's canonical pointer, and the difference IS the read unit of work: the registry records the decision the moment it is taken, and this follows it at the next notification, so no read ever observes the pointer moving under it without being told.


deferredDrops ​

protected readonly deferredDrops: object[] = []

Defined in: packages/core/src/container.ts:732

The drops the immediate policy DEFERRED, and the cursor each one waits for.

immediate promotes a generation that has caught up to nothing, so dropping the previous one at that moment would discard a complete state for an empty one with no fallback. The two are resolved by ORDER rather than by an interlock: retention simply continues until the successor reaches the cursor the previous generation had AT THE PROMOTION.

at ​

at: number

successor ​

successor: HeldEntry<ABI, ProcessResultType>

superseded ​

superseded: HeldEntry<ABI, ProcessResultType>


handle ​

protected handle: { value: ProcessResultType; } | undefined

Defined in: packages/core/src/container.ts:754

The indirect handle, created once so its identity is stable.


held ​

protected readonly held: HeldEntry<ABI, ProcessResultType>[] = []

Defined in: packages/core/src/container.ts:741


onLastSyncUpdated ​

onLastSyncUpdated: ((lastSync) => void) | undefined

Defined in: packages/core/src/container.ts:676


onLoad ​

onLoad: ((state) => Promise<void>) | undefined

Defined in: packages/core/src/container.ts:669


onPromoted ​

onPromoted: ((promoted, superseded) => void) | undefined

Defined in: packages/core/src/container.ts:688

THE POINTER MOVED. Fired for every move, whether the policy took it or a caller asked for it.

It fires BEFORE the state notification that applies the move on the read path, so a consumer that keeps anything DERIVED from the canonical generation -- a cursor, a progress figure, a checkTxInclusion window -- can drop it before the notification tells everybody to re-read. A consumer told the other way round would answer one notification's worth of questions about the new generation from the retired one's cursor.


onStateUpdated ​

onStateUpdated: ((state) => void) | undefined

Defined in: packages/core/src/container.ts:675

The NOTIFICATION. It publishes the INDIRECT handle rather than the value the canonical generation produced, so a subscriber that keeps what it is handed keeps something that follows the pointer.


opened ​

protected opened: boolean = false

Defined in: packages/core/src/container.ts:721

Whether open has finished, so add knows a SUCCESSOR from the BOOT SET.

The generations an indexer is opened with are the set it holds; which of them is canonical is the registry's durable answer, and the policy has no business second-guessing it at open -- under immediate it would otherwise promote the last spec in the list, and under on-catch-up it would undo a revert recorded in a previous session. A generation ADDED to a running indexer is a successor, and that is what the policy is about.


promotionConfig ​

protected readonly promotionConfig: UsedPromotionConfig

Defined in: packages/core/src/container.ts:709

The promotion policy this indexer runs under, with nothing left to decide.


provider ​

protected provider: EIP1193ProviderWithoutEvents

Defined in: packages/core/src/container.ts:704


registry ​

protected readonly registry: GenerationRegistry

Defined in: packages/core/src/container.ts:703


source ​

protected source: IndexingSource<ABI>

Defined in: packages/core/src/container.ts:705


stateMoved ​

protected readonly stateMoved: StateMovedPublisher

Defined in: packages/core/src/container.ts:701

THE SIGNAL, and the token it carries: what this container tells the sides that are READING (ADR-0083).

Held rather than reimplemented, because the receiving container publishes the SAME signal and one notification model is the claim being made. Everything about subscribing, containing a throwing handler and rotating the token is StateMovedPublisher's; what is here is the half only a container knows -- WHICH generation applied the block and whether it is the one that answers reads.


streamDigest ​

protected streamDigest: string

Defined in: packages/core/src/container.ts:739

The stream every generation built from here folds. Recomputed by a reconfigure.

Accessors ​

canonical ​

Get Signature ​

get canonical(): HeldGeneration<ABI, ProcessResultType>

Defined in: packages/core/src/container.ts:1157

The generation that answers reads right now.

Returns ​

HeldGeneration<ABI, ProcessResultType>


defaultFromBlock ​

Get Signature ​

get defaultFromBlock(): number

Defined in: packages/core/src/container.ts:1269

Returns ​

number


expectedFromBlock ​

Get Signature ​

get expectedFromBlock(): number

Defined in: packages/core/src/container.ts:1277

Returns ​

number


finalityDepth ​

Get Signature ​

get finalityDepth(): number

Defined in: packages/core/src/container.ts:1273

Returns ​

number


generations ​

Get Signature ​

get generations(): readonly HeldGeneration<ABI, ProcessResultType>[]

Defined in: packages/core/src/container.ts:1152

Every generation this container holds, in the order it built them.

Returns ​

readonly HeldGeneration<ABI, ProcessResultType>[]


promotion ​

Get Signature ​

get promotion(): UsedPromotionConfig

Defined in: packages/core/src/container.ts:1168

The promotion policy this indexer runs under, resolved.

REPORTED so a caller (or a test) can see WHICH value is in force rather than re-deriving the default: the whole point of having one default everywhere is lost if each runtime keeps its own copy of what it is.

Returns ​

UsedPromotionConfig


state ​

Get Signature ​

get state(): ProcessResultType

Defined in: packages/core/src/container.ts:1186

THE READ HANDLE, and it is INDIRECT.

A consumer may keep it: it answers from whichever generation is canonical when the read is made, so a promotion cannot leave a held reference reading a retired generation's state (story 6). It is the same object every time, because a handle that changed identity on every publication would defeat exactly the callers who keep one.

Where the state is a VALUE rather than a handle (a plain object a processor hands back, or nothing at all) there is no indirection to give: the value is returned as it is, resolved per call, and the notification is what tells a caller to read again.

Returns ​

ProcessResultType

Methods ​

add() ​

add(spec): Promise<HeldGeneration<ABI, ProcessResultType>>

Defined in: packages/core/src/container.ts:883

Build a generation and register it BESIDE the ones already held.

It does not become canonical (unless nothing is, which is the registry's rule for the first one), and it DOES advance -- but how it advances is decided here and is not a choice anybody gets to make.

The rule: a SHARED stream makes a FOLLOWER, and nothing else does ​

A generation whose stream digest matches one already held is handed a READ-ONLY VIEW of the keeper (readOnlyStream) and advances with followMore: it fetches NOTHING, writes NOTHING, re-folds the stored stream from the start and then follows it as the indexing generation appends. A generation on a stream nobody here holds keeps the keeper itself and advances with indexMore, which is an ordinary indexer at a different address.

There is no flag, because a flag would be wrong in both positions. "Follow a stream nobody writes" never advances; "fetch a stream somebody else writes" is a second writer, and it also makes this generation's state a function of its own fetch rather than of the stream, which is what would break the exact revert (IndexerGeneration.followMore). ADR-0044 records the rule and the options weighed against it.

Which generation FETCHES a stream: the oldest one this container HOLDS ​

Registration order among the folds PRESENT, and not the canonical pointer, so the fetching is stable: moving the pointer is one small record write and must not silently hand the append duty to a different engine mid-flight. The normal case makes the two the same thing anyway, since the first generation registered is the one the registry makes canonical. See holdGeneration for the derivation and for why the candidates are the held folds (ADR-0088).

There is exactly ONE moment at which that answer changes under a held fold, and it is not a pointer move on its own: a promotion that DROPS the writer hands the stream to the generation the derivation names next, which is safe only because that generation has provably reached the writer's cursor (dropSuperseded, ADR-0090). Everywhere else the duty is exactly as stable as this paragraph says.

The registry is written BEFORE the engine exists, which is the order its own documentation asks for: a stream subtree no registered generation claims is what the sweep collects, so nothing may write a stream ahead of its registration.

Parameters ​

spec ​

AnyGenerationSpec<ABI, ProcessResultType>

Returns ​

Promise<HeldGeneration<ABI, ProcessResultType>>


applyAtNotification() ​

protected applyAtNotification(entry): void

Defined in: packages/core/src/container.ts:2221

The one place the read path's pointer moves, and it moves WITH a notification.

Parameters ​

entry ​

HeldEntry<ABI, ProcessResultType>

Returns ​

void


applyPolicyTo() ​

protected applyPolicyTo(entry): Promise<void>

Defined in: packages/core/src/container.ts:1456

What the policy does about a generation that has just been ADDED beside the live one.

Nothing at all during open: see opened. And nothing for the canonical generation itself, which is not a successor to anything.

Parameters ​

entry ​

HeldEntry<ABI, ProcessResultType>

Returns ​

Promise<void>


arrangeDrop() ​

protected arrangeDrop(superseded, successor, wasPromotion): Promise<void>

Defined in: packages/core/src/container.ts:1637

DROP-ON-PROMOTION, and the one case it is resolved by ORDER rather than by an interlock.

Under on-catch-up and manual a promotion means the successor DEMONSTRATED something, so the generation left behind can go at that moment. Under immediate it demonstrated nothing -- it is canonical having caught up to NOTHING -- so dropping the previous one would discard a complete state for an empty one, with no fallback when the new fold throws on its first event. There is no interlock and no refusal: retention simply CONTINUES until the successor reaches the cursor the previous generation had at the promotion, and the drop happens then.

A MOVE THAT IS NOT A PROMOTION DROPS NOTHING. A promotion is a move onto what the successor slot names; a move onto any other registered generation is a going-back move and is treated as one. Dropping what such a move left behind would delete the very thing the developer might move forwards to again.

Parameters ​

superseded ​

HeldEntry<ABI, ProcessResultType>

successor ​

HeldEntry<ABI, ProcessResultType>

wasPromotion ​

boolean

Returns ​

Promise<void>


coherenceNow() ​

coherenceNow(): string

Defined in: packages/core/src/container.ts:1147

THE COHERENCE TOKEN IN FORCE RIGHT NOW, read without waiting for a notification.

The same read as ReceivingIndexer.coherenceNow, present here so the two containers keep differing in exactly ONE place (which fold is canonical) rather than in two. A transport that must tell a client AT CONNECT whether what it holds may be stale asks this; the browser transports do not, because a reader that attaches part way through READS the store it shares (ADR-0083).

OPAQUE and COMPARED, never parsed, exactly as on a notification. It rotates nothing and publishes nothing.

Returns ​

string


digestOf() ​

protected digestOf(source, config): string

Defined in: packages/core/src/container.ts:1437

Parameters ​

source ​

IndexingSource<ABI>

config ​

ProvidedIndexerConfig<ABI>

Returns ​

string


disableProcessing() ​

disableProcessing(): void

Defined in: packages/core/src/container.ts:1348

Stop EVERY generation, because every generation is what advances.

Returns ​

void


dropReplaced() ​

protected dropReplaced(record, arriving, namedBySuccessor): Promise<boolean>

Defined in: packages/core/src/container.ts:2029

Drop ONE replaced successor: its registry row, its state store, and every trace of it in this container.

Deleting a generation is already a drop of its state (dropState, injected by whoever named the storage), so nothing new is invented here: what is new is deciding WHEN, without being asked.

The STREAM is NOT reaped with it (ADR-0087). It used to be, wherever this was the last generation folding it, which made a second save in a tab delete the history the first save had fetched -- the one place this codebase deleted an expensive thing to reclaim a cheap one. What a registration displaces is a FOLD.

The REGISTRY GOES FIRST, which is the opposite order from dropSuperseded and deliberately so: there the drop is the last act of a promotion that has already happened, while here a registration is about to be decided on the result, so a failure must leave the container exactly as it was rather than holding a generation it has stopped driving.

A FAILED DROP DOES NOT STOP THE REPLACEMENT, and the slot is what makes that safe: the arriving generation takes successor regardless, so what failed to go is left named by no slot -- which is the definition of collectable, and the next registration tries again. Refusing the registration instead would make a stuck deletion an outage for the tab that is trying to move forward, and a tab is what a user is looking at.

Parameters ​

record ​

GenerationRecord

arriving ​

GenerationId

namedBySuccessor ​

boolean

Whether the successor slot NAMED it, which is the one thing the two cases this path now covers differ by -- and therefore what the line an operator reads has to say. The other case is a generation no slot names that no fold here exists for (ADR-0090, point 3), and reporting that one as a replaced successor would give it a cause it does not have.

Returns ​

Promise<boolean>


dropSuperseded() ​

protected dropSuperseded(superseded, successor): Promise<void>

Defined in: packages/core/src/container.ts:1704

Drop a superseded generation: its registry row and its state store, and NOT its stream, which outlives every fold over it and is deleted only where a caller ASKS (ADR-0087). The stream is what the chain fetches bought and the state is derived from it, so what the drop takes is the recomputable half.

THE STREAM CHANGES HANDS IN THE SAME ACT (ADR-0090, point 2) ​

The common save is a handler edit, so both generations sit on ONE stream and the successor was constructed as a FOLLOWER of the incumbent -- which made this drop unreachable, because dropping a stream's writer would leave its follower folding a stream nothing appends to (ADR-0044). That follower relationship is an artifact of CONSTRUCTION ORDER and not a fact about the two generations: the same successor, in a tab reloaded one second later, is built alone and IS the fetcher (ADR-0088). So rather than weaken the guard, the promotion does what the reload already does -- the promoted generation STOPS FOLLOWING and takes the stream (takeOverStream), in the same act as the drop.

Why THIS moment and no other. The hand-over is safe exactly because the promoted generation is provably AT the writer's position: under on-catch-up the promotion IS the event "the successor reached the incumbent's cursor", and under immediate the deferral above has already waited for that same condition before this runs. There is no gap for an append to be lost in. This is deliberately NOT a fetcher that can change at any time under a held fold: the initial derivation is taken ONCE and its completeness is load-bearing (holdGeneration), and ADR-0090 rejects continuous recomputation explicitly.

THE DECLINE IS NARROWED, NOT DELETED ​

What the hand-over removes is the CASE the guard was declining, and the guard stays for every case it does not cover. The question is asked as ADR-0088's -- who fetches a stream among the folds this container HOLDS -- so it is one rule at both sites rather than two dialects:

  • the superseded generation does not fetch this stream, so dropping it strands nobody and no hand-over is needed;
  • it fetches, and NOTHING this container holds is left on that stream, so again there is nothing to strand and nothing to hand over (the cross-stream save);
  • it fetches, and the fold the derivation names NEXT is the promoted generation: the hand-over makes reality match that derivation, and the drop proceeds;
  • it fetches, and somebody ELSE is next. The promoted generation has not been following that stream and is at no position on it, so there is nothing to hand over, and the next fetcher is a fold frozen as a follower at construction. Dropping the writer would leave a fold on a stream nothing appends to, so the drop is DECLINED and said out loud, exactly as it always was.

Parameters ​

superseded ​

HeldEntry<ABI, ProcessResultType>

successor ​

HeldEntry<ABI, ProcessResultType>

Returns ​

Promise<void>


feed() ​

feed(eventStream, lastSyncFetched): Promise<LastSync<ABI>>

Defined in: packages/core/src/container.ts:1339

Parameters ​

eventStream ​

LogEvent<ABI>[]

lastSyncFetched ​

LastSync<ABI>

Returns ​

Promise<LastSync<ABI>>


fetcherAmongHeld() ​

protected fetcherAmongHeld(stream, excluding?): HeldEntry<ABI, ProcessResultType> | undefined

Defined in: packages/core/src/container.ts:1773

WHICH FOLD THIS CONTAINER HOLDS FETCHES THIS STREAM, as an entry.

fetcherOf over the records of the folds PRESENT, which is ADR-0088's rule and the same question holdGeneration asks when it derives follows and wouldStrandAFollower asks when it declines a drop. One question, one set, one home: what differs between the three sites is only WHEN it is asked.

excluding is how the drop asks it about the moment AFTER: who fetches this stream once that generation is gone. It is a HYPOTHETICAL and nothing is removed by asking.

Parameters ​

stream ​

string

excluding? ​

HeldEntry<ABI, ProcessResultType>

Returns ​

HeldEntry<ABI, ProcessResultType> | undefined


handOverTheStream() ​

protected handOverTheStream(taking, from): void

Defined in: packages/core/src/container.ts:1803

HAND THE STREAM OVER to the generation a promotion just moved the pointer to (ADR-0090, point 2).

Two halves and they must not come apart: the ENTRY stops following, which is what makes the container advance it with indexMore rather than followMore, and the ENGINE is handed the keeper itself in place of the read-only view it was constructed with, which is what makes its appends land. Marked without the second half the generation would fetch and write nowhere; given the second half without the first it would never fetch at all.

A container with NO keeper has no stream for anyone to hold, so there is only the first half to do: follows still decides which verb advances this fold.

The CALLER is dropSuperseded and only it, because what makes this safe is the pair of facts only a promotion has: the generation losing the stream is going in the same act, and this one is provably at its cursor.

Parameters ​

taking ​

HeldEntry<ABI, ProcessResultType>

from ​

HeldEntry<ABI, ProcessResultType>

Returns ​

void


heldHere() ​

protected heldHere(record): boolean

Defined in: packages/core/src/container.ts:1928

Whether this container holds a fold for that record, which is the question follows and a drop both ask.

Parameters ​

record ​

GenerationId

Returns ​

boolean


heldOf() ​

protected heldOf(entry): HeldGeneration<ABI, ProcessResultType>

Defined in: packages/core/src/container.ts:2103

What the container hands OUT for a generation it holds.

Parameters ​

entry ​

HeldEntry<ABI, ProcessResultType>

Returns ​

HeldGeneration<ABI, ProcessResultType>


holdGeneration() ​

protected holdGeneration(__namedParameters, willHold): Promise<HeldGeneration<ABI, ProcessResultType>>

Defined in: packages/core/src/container.ts:957

PHASE TWO: build the ENGINE for a registered generation and hold it, knowing every fold this container will hold.

willHold is that set, as records. It is the caller's to supply because only the caller knows when it is complete: add adds one to what is already held, open gathers all of its specs first.

Parameters ​

__namedParameters ​

RegisteredGeneration<ABI, ProcessResultType>

willHold ​

readonly GenerationRecord[]

Returns ​

Promise<HeldGeneration<ABI, ProcessResultType>>


indexMore() ​

indexMore(): Promise<LastSync<ABI>>

Defined in: packages/core/src/container.ts:1320

Advance EVERY generation one step, each by the verb its stream decides, and answer with the canonical one's cursor.

The order is the order they were built, which is what makes a follower's step meaningful: a stream's writer is always ahead of it in that list, so by the time a follower reads the stream this cycle's batch is already in it. A follower that ran first would simply see nothing new and catch up on the next tick, so the order is a latency decision rather than a correctness one -- but a driver that loops to the tip would otherwise leave every follower one cycle short at the end.

The canonical generation is resolved BEFORE the loop, so a promotion applied mid-cycle cannot make this return a cursor from a generation that was not the one being driven.

Returns ​

Promise<LastSync<ABI>>


load() ​

load(): Promise<LastSync<ABI>>

Defined in: packages/core/src/container.ts:1291

Load EVERY generation, and answer with the canonical one's cursor.

All of them, because a generation that has not loaded has no state and no cursor, so it could not advance afterwards -- and for a FOLLOWER the load IS the re-fold of the stored stream from the start. In HELD ORDER, which puts a stream's writer before anything following it, so a follower's first re-fold sees whatever the writer's load recovered. load is idempotent per generation (_load.once()), so naming the canonical one twice costs nothing.

Returns ​

Promise<LastSync<ABI>>


movePointerTo() ​

protected movePointerTo(entry): Promise<GenerationRecord>

Defined in: packages/core/src/container.ts:1521

Move the pointer to a generation this container holds: the registry first, the read path at the notification, then what happens to the one left behind.

Parameters ​

entry ​

HeldEntry<ABI, ProcessResultType>

Returns ​

Promise<GenerationRecord>


notifyState() ​

protected notifyState(): void

Defined in: packages/core/src/container.ts:2301

Returns ​

void


onStateMoved() ​

onStateMoved(handler): StateMovedDetach

Defined in: packages/core/src/container.ts:1131

BE TOLD THE STATE MOVED, block by block. Returns the detach.

One notification per block the CANONICAL fold applies, naming that block, the generation that answered, the entity names that block touched and a coherence token to compare -- and one per REORG, naming the fork point that fold reverted to, with a token that has ROTATED (StateMoved). It is a SIGNAL and not a delivery of data: it says what moved so a reader re-reads through the surface it already has -- state here, a port in a tab, a feed or a query surface across a network.

ts
const detach = indexer.onStateMoved((moved) => {
  if (moved.coherence !== held) {held = moved.coherence; return invalidateEverything();}
  if (moved.kind === 'applied') for (const entity of moved.entities) invalidate(entity);
});

Best-effort, with nothing held per subscriber: see StateMovedPublisher. A reader that MISSES a retraction is repaired by the next notification it does receive, because that one carries the rotated token and the first line above invalidates everything.

Parameters ​

handler ​

StateMovedHandler

Returns ​

StateMovedDetach


open() ​

open(specs): Promise<void>

Defined in: packages/core/src/container.ts:808

Build and register the generations, then resolve the canonical pointer onto one of them.

TWO PHASES, because which generation FETCHES is a fact about the whole set ​

Every spec is REGISTERED first, and only then is an ENGINE built for any of them. The split is ADR-0088's and it is not a tidy-up: follows is derived from the oldest generation this container HOLDS on the stream, and add freezes that answer into the engine's config at construction (readOnlyStream), so a derivation taken while the set is still half-built answers a different question for each spec and gives a different result per spec ORDER. Measured, with the same two folds listed edited-first: TWO generations decide they fetch, the same range is asked of the node twice, and one block's log is stored twice (docs/spikes/the-reloaded-tab-stall-is-measured-on-the-configuration-a-tab-actually-has/).

The identity cannot be read any earlier than phase one, which is why the phase boundary is HERE and not before the factories: a fold arriving as a MODULE has no bytes to hash, so GenerationSpec.processorIdentity is filled in from inside createProcessor and a generation genuinely cannot be named before it is built (ADR-0086).

AND NO SPEC MAY DISPLACE ANOTHER SPEC OF THE SAME OPEN ​

Phase one registers into successor, which holds AT MOST ONE (ADR-0084), so two distinct specs beside the canonical generation would have the second displace the first mid-open, and phase two would then build an engine for a generation whose record was gone. Such an open is REFUSED before any of its own specs is deleted (OpenedSpecsDisplaceOneAnotherError): every fold this container holds is one the registry names.

Parameters ​

specs ​

readonly AnyGenerationSpec<ABI, ProcessResultType>[]

Returns ​

Promise<void>


pause() ​

pause(id): void

Defined in: packages/core/src/container.ts:1248

PAUSE ONE GENERATION: it stops indexing without being deleted, by CAPPING and DRAINING.

The mechanism is entirely the engine's (IndexerGeneration.pause), and there is deliberately none of it here: this names WHICH generation and refuses the two ids that have no answer. A paused generation goes on being driven by indexMore -- that is what the drain IS -- and goes on answering reads if it is the canonical one; what it stops doing is moving forward.

IN MEMORY, and not recorded in the registry: the registry holds what a generation IS, and a pause is what one is DOING. So a reload comes back running, which costs one drain to re-pause and never costs correctness.

SYNCHRONOUS, unlike promote, precisely because nothing durable is written.

Parameters ​

id ​

GenerationId

Returns ​

void


promote() ​

promote(id): Promise<GenerationRecord>

Defined in: packages/core/src/container.ts:1228

MOVE THE CANONICAL POINTER, and apply the move AT A NOTIFICATION.

The read unit of work is the interval between notifications ​

The pointer is recorded in the registry first (that is the durable decision: forwards it is promotion, backwards it is revert), and the READ PATH follows it inside notifyState and nowhere else. So the only moment a reader can observe a different generation is a moment it was told about, and every read between two notifications answers from ONE generation.

That boundary already existed -- createIndexerState publishes through subscribable stores and the core's state callback, and an app already treats a notification as "the world moved, re-read" -- which is why there is no scope API here, no transaction handle and no timer. Inventing one would add a second thing to get right for a guarantee the existing one already gives.

The residual, stated rather than discovered: a caller reading OUTSIDE any subscription (a one-off read in an event handler) gets per-CALL resolution, so two such reads either side of a promotion can straddle it. That is tolerable and bounded: each read is answered by a generation that was canonical when it was made, and neither read is stale.

And the COHERENCE TOKEN rotates with it ​

A reader that is not in this heap holds a cache rather than a handle, and nothing about the pointer reaches it -- so every notification after this one carries a token it has never seen, which is its instruction to invalidate EVERYTHING and re-read. See movePointerTo for where that sits and why.

Parameters ​

id ​

GenerationId

Returns ​

Promise<GenerationRecord>


publishDiscard() ​

protected publishDiscard(entry, outcome, publishedBefore): void

Defined in: packages/core/src/container.ts:2161

DROP WHAT THE DISCARD DESTROYED, AND SAY SO.

The three reconfiguring verbs all end in one of two places -- the fold survived, or it is gone and being rebuilt -- and onStateUpdated fires when a state is ADOPTED or PRODUCED, so it fires for neither. A subscriber holding the state the fold just lost is therefore told by nothing, and on the reconfigure this exists for (a contract redeployed behind its proxy, which has emitted nothing yet) the next publication never comes at all: the old contract's numbers stay on screen for the rest of the session.

So the container publishes the discard, which is a NOTIFICATION and not a new state: what goes out is the same indirect handle every publication carries, now resolving to the fold that has processed nothing. createIndexerState did this for its own state store until the-old-indexer-shape-is-deleted; it is here because the container is what knows a verb discarded, and because every other consumer of one (a server, a CLI, a test) was never told at all.

A DISCARD DOES NOT ALWAYS LEAVE NOTHING, which is the whole reason for the publishedBefore guard. When the STREAM survives -- which a processor swap always leaves it, since the stream verdict is about the source and the config and not the processor -- the load inside the verb REPLAYS the cached events and publishes the rebuilt state before the verb returns. That publication is the truth; dropping the handle and re-announcing an empty fold on top of it would report a correct rebuild to every subscriber as an empty state, with the cursor already past the blocks, so nothing would arrive later to correct it.

Parameters ​

entry ​

HeldEntry<ABI, ProcessResultType>

outcome ​

ReconfigureOutcome

publishedBefore ​

number

Returns ​

void


publishFoldReport() ​

protected publishFoldReport(entry, report): void

Defined in: packages/core/src/container.ts:2280

ASSEMBLE and PUBLISH one thing the fold did, and ONLY for the CANONICAL fold.

A non-canonical generation re-folds a whole stored stream to catch up, so publishing per block there would fire thousands of notifications naming past blocks while nothing a reader can see has moved. The filter is the one the per-generation callbacks in add already apply -- entry === this.current -- rather than a second rule that can disagree with it.

It fires AS THE BLOCK LANDS, which is inside the process() call that applied it and therefore BEFORE the onStateUpdated that follows the batch. That order is deliberate and it is safe in the direction that matters: the block is already durable when the fold reports it (a block and its cursor are ONE atomic unit behind the storage seam, ADR-0027), so a reader that re-reads the instant it is told sees that block's effects. The alternative -- holding the reports back until the batch's state notification -- would buffer, which is the one thing the producer must not do, and would collapse a batch's blocks into one moment for no reader's benefit.

A RETRACTION goes out the same way and through the same filter, which is the half worth stating: the filter covers the TOKEN as well as the notification, so a follower re-folding a stored stream's reorg rotates nothing. Rotating there would have every reader of the canonical fold throw its cache away because a second generation caught up.

Parameters ​

entry ​

HeldEntry<ABI, ProcessResultType>

report ​

FoldReport

Returns ​

void


reenableProcessing() ​

reenableProcessing(): void

Defined in: packages/core/src/container.ts:1354

Returns ​

void


registerGeneration() ​

protected registerGeneration(spec, openedAlongside?): Promise<RegisteredGeneration<ABI, ProcessResultType>>

Defined in: packages/core/src/container.ts:903

PHASE ONE: build this generation and put its RECORD in the registry, with no engine and nothing held.

Split out of add so open can register every spec before deciding anything about any of them: what a record ANSWERS -- whether this fold fetches its stream or follows it -- is a question about the whole set, and a record is the cheapest complete statement of a set member (it carries the createdAt the ranking uses). Nothing here reads this.held, and nothing here may: at this point it is half-built by construction.

Parameters ​

spec ​

AnyGenerationSpec<ABI, ProcessResultType>

openedAlongside? ​

readonly GenerationRecord[] = []

The records open has registered for its EARLIER specs, which this registration may not displace. Empty for add, whose arrival replaces a pending successor as it always did.

Returns ​

Promise<RegisteredGeneration<ABI, ProcessResultType>>


relayFoldReports() ​

protected relayFoldReports(entry, processor): void

Defined in: packages/core/src/container.ts:2237

RELAY: hand this generation's fold the reporter it names what it did to.

The entity set is produced where the mutations are, and the fork point where the removed markers are read and revertTo is called -- both one package down -- and this container assembles the signal from them plus what only a container holds (ADR-0083). A fold that implements nothing here reports nothing and therefore publishes nothing, which is the honest coarse answer rather than a fabricated one: core cannot know which blocks such a fold applied, what they touched, or what it took back.

Parameters ​

entry ​

HeldEntry<ABI, ProcessResultType>

processor ​

EventProcessor<ABI, ProcessResultType>

Returns ​

void


replaceTheSuccessor() ​

protected replaceTheSuccessor(arriving, registered, slots, arrivingStream, openedAlongside?): Promise<void>

Defined in: packages/core/src/container.ts:1884

MAKE ROOM IN THE successor SLOT, because the registration about to happen is what takes it.

The container knew ONE kind of supersession and it is a PROMOTION: the incumbent is RETAINED, and on the receiving runtime it is retained BY a slot, since the pointer must be able to move back to it (dropSuperseded, above). Here it is retained by nothing at all -- no move assigns a predecessor on this runtime (ADR-0089) -- so what protects it from this path is only the strand rule below, where it applies. This is the other half, and it is the half a browser tab lives in. A successor that is still catching up and that a NEWER one has just replaced is dead work in every case and a WALL in this one: it keeps its registry row, keeps its state store and keeps being advanced by every cycle, so a developer saving twice reaches maxGenerations -- two here -- almost at once, and the only remedy was deleting a generation by hand.

The PREDICATE is the whole safety argument, and it is now a ROW ​

"Not canonical right now" is NOT the test, and the reason is the receiving twin's -- one rule, one home: a predecessor kept for a revert is not canonical right now either, and dropping it would silently destroy the way back. Nor is it "has never been canonical", which is the question the rows cannot answer and which everCanonical approximated IN MEMORY, from what one tab had seen since the page loaded -- so a RELOAD answered "nothing" and dropped nothing, which is exactly the shape that reloads most. The test is now the SLOT, and displacedBySuccessor is where it is written down, ONCE, for both containers.

"THE SAME ROLE" MEANS THE SLOT, REGARDLESS OF STREAM ​

There is ONE successor slot, so the cross-stream question is answered structurally rather than by fiat: a newer successor replaces the pending one wherever either sits, because there is only one place for a pending successor to be. That is also what frees a STREAM slot -- maxStreams counts the distinct streams among registered generations, and a tab that reconfigures its SOURCE meets that bound first.

AND A GENERATION NO FOLD HERE EXISTS FOR GOES TOO, which is this runtime's ​

answer and not the shared rule's (ADR-0090, point 3)

unheldIsCollectable is TRUE here and FALSE on the receiving twin, and that one word is where the two runtimes differ. A row this container holds no fold for can never answer a read and can never fetch on THIS runtime: after a page reload the previous processor's code is not in the bundle that loaded, which is the same fact ADR-0089 removed the predecessor assignment for. Nothing else here will ever collect it either -- reclaim belongs to the container that HAS an operator (ADR-0084) -- so it survived every session and the developer's next save met maxGenerations with a wall no reload could clear.

It is collected AT A REGISTRATION and at no other moment: nothing here fires on a timer and nothing sweeps at open, so the deletion is a consequence of an act the developer just performed rather than the authorless one ADR-0084 refused. A tab that reloads and sits there collects nothing, because the fold it arrives with is the one canonical names and the shared rule displaces nothing for it.

What happens at a PROMOTION is arrangeDrop's and dropSuperseded's (ADR-0090, points 1 and 2): there the superseded generation goes and its stream CHANGES HANDS, because the promoted fold has provably reached its cursor. Nothing of that belongs here -- a registration demonstrates nothing -- so this path still drops only what it may drop safely, and the caps are untouched by either.

Parameters ​

arriving ​

GenerationId

registered ​

readonly GenerationRecord[]

slots ​

SlottedGenerations

arrivingStream ​

string

openedAlongside? ​

readonly GenerationRecord[] = []

Returns ​

Promise<void>


replay() ​

replay(eventStream, lastSyncStored): Promise<LastSync<ABI>>

Defined in: packages/core/src/container.ts:1343

Parameters ​

eventStream ​

LogEvent<ABI>[]

lastSyncStored ​

LastSync<ABI>

Returns ​

Promise<LastSync<ABI>>


require() ​

protected require(id): HeldEntry<ABI, ProcessResultType>

Defined in: packages/core/src/container.ts:2126

The held generation this id names, or the refusal that says nothing here can answer for it.

Parameters ​

id ​

GenerationId

Returns ​

HeldEntry<ABI, ProcessResultType>


requireCurrent() ​

protected requireCurrent(): HeldEntry<ABI, ProcessResultType>

Defined in: packages/core/src/container.ts:2198

Returns ​

HeldEntry<ABI, ProcessResultType>


reset() ​

reset(): Promise<ReconfigureOutcome>

Defined in: packages/core/src/container.ts:1360

Returns ​

Promise<ReconfigureOutcome>


resolveCanonical() ​

protected resolveCanonical(): Promise<void>

Defined in: packages/core/src/container.ts:2180

Point the read path at the generation the registry already calls canonical.

Returns ​

Promise<void>


resolveState() ​

protected resolveState(): ProcessResultType

Defined in: packages/core/src/container.ts:2209

What the canonical generation answers with: what it last published, or its handle.

Returns ​

ProcessResultType


resume() ​

resume(id): void

Defined in: packages/core/src/container.ts:1257

RESUME one generation: remove the cap. The next round asks the head again.

Parameters ​

id ​

GenerationId

Returns ​

void


settlePromotion() ​

protected settlePromotion(): Promise<void>

Defined in: packages/core/src/container.ts:1493

THE TRIGGER, and the deferred drops that hang off it.

A candidate is promoted the moment its cursor reaches the cursor the CANONICAL generation has -- the one it must not fall behind, since promoting a successor that is behind the incumbent is the state going backwards that story 14 is about. One move per cycle: a second candidate is still a candidate on the next one, and promoting twice inside one advance would publish a generation nobody ever read from.

Returns ​

Promise<void>


stopDriving() ​

protected stopDriving(record): void

Defined in: packages/core/src/container.ts:2087

STOP DRIVING a generation whose record has gone: out of the held list, out of any deferred drop, and off the reporter.

One function rather than the same lines wherever a generation is deleted, because the last is the one that is easy to forget and the worst to omit: a dropped fold that went on REPORTING would be a channel into a publisher nothing can reach it through any more.

A generation this container holds no engine for is the ordinary case after a reload (a tab holds only the fold it was built with), and then there is simply nothing to stop driving.

Parameters ​

record ​

GenerationId

Returns ​

void


stopRelayingFoldReports() ​

protected stopRelayingFoldReports(processor, keeping): void

Defined in: packages/core/src/container.ts:2245

Detach a fold this container no longer drives, unless it is the one it kept.

Parameters ​

processor ​

EventProcessor<ABI, ProcessResultType>

keeping ​

EventProcessor<ABI, ProcessResultType>

Returns ​

void


updateIndexer() ​

updateIndexer(update): Promise<ReconfigureOutcome>

Defined in: packages/core/src/container.ts:1368

Parameters ​

update ​
provider? ​

EIP1193ProviderWithoutEvents

source? ​

IndexingSource<ABI>

streamConfig? ​

ProvidedStreamConfig

Returns ​

Promise<ReconfigureOutcome>


updateProcessor() ​

updateProcessor(newProcessor, options): Promise<ReconfigureOutcome>

Defined in: packages/core/src/container.ts:1404

Swap the canonical generation's processor IN PLACE, exactly as before.

Under the generation model a processor change is a NEW GENERATION over the same stream, built beside the live one and promoted when it is ready. That is the promotion policy's landable (the-promotion-policy-moves-the-canonical-pointer), and reaching for it here would be the outage-shaped in-place discard wearing a container. So this batch keeps today's behaviour and today's cost, and only keeps the container HONEST about it: the held entry now names the processor that is actually folding, so the read handle cannot answer from the fold that was replaced. The registry record still names the fold this generation was REGISTERED with, which is the drift the policy task closes by creating a generation instead of mutating one.

Parameters ​

newProcessor ​

EventProcessor<ABI, ProcessResultType>

options ​
force? ​

boolean

processorIdentity ​

string

Returns ​

Promise<ReconfigureOutcome>


wouldStrandAFollower() ​

protected wouldStrandAFollower(record, registered, arrivingStream): boolean

Defined in: packages/core/src/container.ts:1987

Whether dropping this generation would leave a stream being folded by something with nothing appending to it.

dropSuperseded's rule, applied one moment earlier and with one more follower in view. Which generation WRITES a stream is the oldest SURVIVING one registered on it (ADR-0044), so dropping a writer another held generation follows leaves that one folding a stream nothing appends to. The generation about to be ADDED counts as such a follower, because it is about to be one: a generation on a stream this container already holds FOLLOWS it, and dropping its writer here would leave it, too, on a stream nothing appends to. What the drop does NOT do is take the stored stream with it -- no drop reaps one any more (ADR-0087) -- so the hazard here is the missing appender and only that.

It takes RECORDS in and answers about FOLDS, and that pairing is the point. It has to take records, because the caller hands it rows: the record it is asked about may be one this container holds no fold for at all, which is the reload case and the one ADR-0090's point 3 collects. What it answers with is the derivation over the folds this container HOLDS (below), so such a record is never named as a fetcher and strands nobody -- which is exactly what makes it collectable. The justification this paragraph used to give was the generation "the slot names" being unheld; that is stale twice over, because no slot names a superseded generation on this runtime at all (ADR-0089) and an unheld row is what the narrowing below FILTERS OUT rather than a reason to widen.

WHO FETCHES IS ASKED OF THE FOLDS THIS CONTAINER HOLDS (ADR-0088) ​

fetcherOf and ADR-0044's rule are unchanged; what narrowed is the SET it is asked about, and this is the SECOND of the two sites that ask it. The first is the follows derivation in holdGeneration, narrowed when ADR-0088 landed, and leaving this one over every REGISTERED record made the two answer differently about the same stream: after a reload the oldest record on it is the superseded generation NO FOLD EXISTS FOR, so the held fold correctly derived that IT fetches while this question still named the dead row as the fetcher -- and declined to drop it to protect a follower that does not exist.

The IN-SESSION case is unchanged by the narrowing and must be: there the superseded generation IS held and IS the oldest fold present, so it is still named here and the drop is still DECLINED. What the narrowing removes is only the answer about a generation this process has no fold for, which is the same claim ADR-0088 removed from the derivation: a generation absent from this process cannot be fetching for it.

The FOLLOWER half of the question was always asked of the held set (below), so this makes one question about one set rather than half of each.

AND IT IS NOT NARROWED BY THE HAND-OVER (ADR-0090) ​

A PROMOTION may now hand a stream to the generation it promoted and drop the writer (dropSuperseded), because that generation has provably reached the writer's cursor. A REGISTRATION has no such thing: a fold arriving beside the live one is at no position at all, and nothing about it demonstrates anything. So this question keeps its full force, and its decline is the one that still stops a save deleting the generation that is fetching for it.

Parameters ​

record ​

GenerationRecord

registered ​

readonly GenerationRecord[]

arrivingStream ​

string

Returns ​

boolean