Documentation / @etherfold/browser / IndexerPort
Type Alias: IndexerPort ​
IndexerPort =
object
Defined in: packages/browser/src/host/port.ts:110
THE PORT: the typed boundary a tab holds onto a host that is not its own thread.
Every verb here is a CASE on the envelope, except onProgress and onStateMoved, which are the two things that travel the other way: PUSHES the host sends unprompted (ADR-0082 for the first, ADR-0083 for the second, which answers a different question at a different cadence and is deliberately not merged into it). What is not here is as much the point as what is: a tab holds no store, no container and no processor, so there is nothing on this type that could mutate the state the host is folding into. The writer/reader split (ADR-0077, ADR-0079) reaches across the boundary as a fact of the TYPE rather than as a rule anybody has to remember -- the host opened the store for WRITING, and what a tab can name is this.
What a tab reads WITH is reads below: the store's four reads, proxied, with nothing beside them that could write. A tab may also open the same store for READING itself (openForReading), which the same-origin IndexedDB default makes possible -- the port is what makes the surface work for a store a tab cannot open, and what keeps an app from having to know which of the two it is in.
Properties ​
host ​
readonlyhost:HostingShape
Defined in: packages/browser/src/host/port.ts:112
WHICH hosting shape this port leads to, as the access that built it named it.
reads ​
readonlyreads:PortStateReads
Defined in: packages/browser/src/host/port.ts:320
THE STORE'S FOUR READS, served by the host from the store its canonical generation folds into.
Untyped here, by entity NAME, exactly as EntityStateView is on this thread: what an app should hold is the TYPED surface generated from its own declarations over these, which is createPortReadSurface(port, entities).
There is nothing beside them that could mutate, and that is a fact of this type rather than a rule to remember: the host opened the store for WRITING and what crosses is four reads (ADR-0077, ADR-0082).
Methods ​
checkTxInclusion() ​
checkTxInclusion(
queries):Promise<Record<string,TxInclusionVerdict>>
Defined in: packages/browser/src/host/port.ts:307
DOES THE STATE THIS APP IS ABOUT TO RENDER ALREADY ACCOUNT FOR THESE TRANSACTIONS?
The reconciliation an app needs before it lays an OPTIMISTIC update over indexed state: applied on top of a state that already contains it, a non-idempotent update (a counter, a balance, an append) is counted twice. See checkTxInclusion in @etherfold/core for what the verdicts mean, why the caller's own receipt cannot answer this, and what it cannot tell you.
const verdicts = await indexer.checkTxInclusion(pending.map(({hash, block}) => ({txHash: hash, minedAtBlock: block})));
for (const {hash} of pending) {
if (verdicts[hash].status === 'included') overlay.drop(hash); // the fold has it: stop predicting it
}THE WHOLE PENDING SET IN ONE CALL, because that is how an app holding one uses it: one round trip, and one verdict per hash keyed as it was asked for.
The verdict crosses WHOLE -- a STATUS and the BASIS for it -- and reading the status alone is the mistake to avoid. unknown has two causes (nothing is synced yet; the fold is so far behind the tip that its window says nothing about the region asked about), and both mean KEEP the optimistic update, where an honest absent means the fold has looked and not found it. minedAtBlock is per query and is what a caller holding a RECEIPT passes to close the sparse window's two limits, through the below-window branch.
A SNAPSHOT and not a subscription: it is answered from where the fold is at the moment of the call, so an app watching a transaction ASKS AGAIN -- when onProgress says the fold moved, which is exactly when the answer can have changed.
Parameters ​
queries ​
readonly TxInclusionQuery[]
Returns ​
Promise<Record<string, TxInclusionVerdict>>
close() ​
close():
void
Defined in: packages/browser/src/host/port.ts:341
Stop holding the host.
Every call still in flight REJECTS: a hung promise is the worst available outcome, because a stalled app and a slow app look identical from outside (ADR-0082). What close does to the host itself is the hosting shape's business -- a dedicated worker belongs to the tab that made it and is terminated; a SharedWorker serving other tabs is not.
Returns ​
void
generations() ​
generations():
Promise<readonlyHostGeneration[]>
Defined in: packages/browser/src/host/port.ts:264
EVERY GENERATION THE HOST HOLDS, with the one answering reads marked and each one's progress.
Asked rather than pushed, because a generation list moves when somebody RECONFIGURES or a pointer moves and not while a fold advances -- which is also why a tab that has just reconfigured is the one that asks.
Returns ​
Promise<readonly HostGeneration[]>
onHostDeath() ​
onHostDeath(
listener): () =>void
Defined in: packages/browser/src/host/port.ts:210
BE TOLD THAT THE HOST DIED. Returns the detach.
Browsers evict workers, so this is an expected event with a defined outcome rather than a failure nobody handled (ADR-0082): the app is TOLD, every call in flight rejects with an IndexerHostDiedError, the port starts another host, and the fold resumes from the cursor the store already holds (ADR-0027). Nothing is lost but the questions that were in the air.
indexer.onHostDeath(({attempt, restarting}) => {
banner.textContent = restarting ? 'the indexer restarted' : `the indexer keeps failing (${attempt} times)`;
});AN EVENT rather than something to infer, because silence is the worst available outcome: a stalled app and a slow app look identical from outside, and that is where "is it broken?" reports come from. Nothing polls for it -- the port watches the host (IndexerPortOptions.watch) and tells whoever asked.
It is NOT HostProgress.failure, and the difference decides what an app should do: a failure is a host that is alive and whose DRIVER stopped on something waiting cannot fix, so it still answers reads. This is the host being gone.
Parameters ​
listener ​
(death) => void
Returns ​
() => void
onProgress() ​
onProgress(
listener): () =>void
Defined in: packages/browser/src/host/port.ts:147
BE TOLD where the fold has got to, whenever it MOVES. Returns the detach.
This is the channel ADR-0082 decides on: status is PUSHED, and an app builds whatever reactive wrapper its framework wants over this signal -- createProgressReadable(port) is the one this package ships for the common case. Nothing here polls, and nothing on a timer moves it: the host posts when a batch has been APPLIED or the phase changed, and posts nothing when the report would repeat the last one.
const stop = indexer.onProgress(({phase, blocksBehindTip}) => {
banner.textContent = phase === 'at-tip' ? 'live' : `syncing, ${blocksBehindTip} blocks behind`;
});The listener is called with WHERE THE FOLD IS NOW as soon as the host answers, so a tab that attached half way through a fold renders the truth without waiting for the next batch to land.
A SUBSCRIPTION and not a slot: several listeners may hold it at once, and each releases its own -- unlike the container's onLastSyncUpdated and its neighbours, which are single assignable callbacks on an object only the host can reach. When the LAST one lets go, the host is told to stop posting, so an unsubscribed tab stops receiving pushes rather than merely ignoring them.
Parameters ​
listener ​
(progress) => void
Returns ​
() => void
onStateMoved() ​
onStateMoved(
listener): () =>void
Defined in: packages/browser/src/host/port.ts:185
BE TOLD THE STATE MOVED, so this tab RE-READS at the right moment instead of polling on an interval it invented. Returns the detach.
The state-moved signal ADR-0083 decides, delivered across the port unchanged: one notification per block the host's canonical fold APPLIED, and one per RETRACTION, carrying what a reader needs to decide what to throw away. The same value crosses every transport -- this port, a BroadcastChannel from an indexing tab, a server's stream -- so an app that later points at a remote indexer keeps this handler.
A reader's whole rule is two lines:
let held: string | undefined;
const stop = indexer.onStateMoved((moved) => {
if (moved.coherence !== held) {held = moved.coherence; return queryClient.invalidateQueries();}
if (moved.kind === 'applied') for (const entity of moved.entities) queryClient.invalidateQueries({queryKey: [entity]});
});It is NOT onProgress, and the difference is which question each answers: how far the fold has got is a STATE a progress bar renders, and this is WHAT MOVED so a cache can invalidate narrowly. A host resting at the tip pushes neither, because nothing moved.
A tab that attaches part way through is told NOTHING until the next block ​
Unlike onProgress, whose listener is called with where the fold is as soon as the host answers, this one is silent until the fold moves again: a notification is an EVENT about a moment, so there is nothing current to be handed, and replaying the last one would say a block just landed when it landed some time ago. What a freshly attached tab does instead is READ, which is what it was going to do with the notification anyway. Delivery is best-effort and the producer holds nothing per client (ADR-0083), so a missed notification is repaired by the next one plus the coherence token.
Parameters ​
listener ​
(moved) => void
Returns ​
() => void
progress() ​
progress():
Promise<HostProgress>
Defined in: packages/browser/src/host/port.ts:120
How far the host's fold has got.
Answering at all is proof the host is ALIVE, which is why nothing here says so separately. A host that has stopped reports why in failure instead of leaving a tab to infer it from a number that stopped moving.
Returns ​
Promise<HostProgress>
promotion() ​
promotion():
Promise<UsedPromotionConfig>
Defined in: packages/browser/src/host/port.ts:273
THE PROMOTION POLICY IN FORCE, as the host's container resolved it.
Nothing is defaulted on this side. There is one default everywhere (on-catch-up) and it lives with the type it belongs to, so a value invented here would be a second answer to "which policy is this app running under" (CONTEXT.md, canonical pointer).
Returns ​
Promise<UsedPromotionConfig>
query() ​
query(
request):Promise<unknown>
Defined in: packages/browser/src/host/port.ts:331
ONE QUERY, answered by the query handler the host's entry injected (ADR-0099).
Opaque here, because this package carries the case and never the query language: what an app holds is an EXECUTOR over this, workerExecutor(port) from @etherfold/graphql/worker, which sends a GraphQL request and normalises a closed port (IndexerPortClosedError) or a dead host (IndexerHostDiedError) to the one transport-failure shape. A host whose entry passed no handler refuses it, naming the missing handler.
Parameters ​
request ​
unknown
Returns ​
Promise<unknown>
reconfigure() ​
reconfigure(
update):Promise<HostReconfigure>
Defined in: packages/browser/src/host/port.ts:255
RECONFIGURE THE SOURCE, and be told what that produced.
A reconfigure is not an outage: the new generation folds BESIDE the live one, which goes on answering every read until the promotion policy moves the canonical pointer (promotion() reports which policy is in force). Nothing is discarded, and a source whose hashable shape did not move resolves to the generation that is already running rather than rebuilding anything (HostReconfigure.added).
const {generation, added} = await indexer.reconfigure({source: nextSource});
if (added && !generation.follows) banner.textContent = 'refetching this contract\u2019s history';The SOURCE is the only half of a generation that can cross: the fold is code, so changing it is a new worker bundle rather than a message (ADR-0082). A REFUSAL -- the generation caps, which is the one this call meets -- rejects it carrying its own name and its own fields, so an app branches on the refusal rather than reading its sentence. What the new generation then meets while it FOLDS (a node on another chain, a refused write) reaches the tab where every other driver failure does, on progress.
Parameters ​
update ​
source ​
Returns ​
Promise<HostReconfigure>
startIndexing() ​
startIndexing():
Promise<HostProgress>
Defined in: packages/browser/src/host/port.ts:219
START INDEXING, and answer where the fold is now.
What a settings screen turns back on. Asking a host that is already indexing is an ANSWER rather than a refusal, because this names a STATE and not an edge: two components that each ask once leave the host in the state they both asked for.
Returns ​
Promise<HostProgress>
stopIndexing() ​
stopIndexing():
Promise<HostProgress>
Defined in: packages/browser/src/host/port.ts:231
STOP INDEXING, and answer where the fold stopped.
What a backgrounded tab or a settings screen calls so an app stops burning a user's rate limit. It RESOLVES WHEN THE CYCLE IN FLIGHT HAS LANDED, so a caller that has been answered knows no further chain request will be made and that the cursor is where a completed cycle would have left it -- a stopped indexer resumes without re-indexing and without skipping.
Stopping a host that is not indexing answers where it is and changes nothing.
Returns ​
Promise<HostProgress>