Skip to content

Documentation / @etherfold/browser / BrowserGenerationSpec

Type Alias: BrowserGenerationSpec<ABI, ProcessResultType, ProcessorConfig> ​

BrowserGenerationSpec<ABI, ProcessResultType, ProcessorConfig> = object

Defined in: packages/browser/src/IndexerState.ts:359

THE SHAPE of this hook: the factories that BUILD a generation, rather than one already-built processor over one already-built store.

An indexer holds any number of generations -- a stream plus a fold over it -- and one of them is canonical and answers every read. So it cannot be handed a constructed processor and a constructed store: each generation folds into its OWN state, and the container has to be able to build the next one.

ts
const indexer = createIndexerState({
  // the signal bounds the CLAIM, so a storage that never answers is a refusal
  // an app can render rather than a wait with no end (see `createState`)
  createState: async (context, {signal}) =>
    openForWriting(await createBrowserStateStore(myProcessor.entities), {signal}),
  createProcessor: (store) => fromEntityProcessor(myProcessor)(store),
});

The order is createState then createProcessor, and it is the order a generation's IDENTITY forces: the stream half is known from the source and the stream config, and the FOLD half is processorIdentity where the arrival derived one and the processor's own declared hash where it did not, so the processor has to exist before the generation can be named -- which means the state cannot be keyed on the finished name. The factories are per generation instead, so the caller's own closure is what distinguishes this generation's store from the next one's.

There used to be a second accepted shape -- createIndexerState(fromEntityProcessor(p)(store)), one already-built processor over one already-built store, which meant exactly one generation. It is DELETED: an indexer that holds generations cannot be handed one, so keeping it would have been a second call shape that could never reach what the first one is for.

Type Parameters ​

ABI ​

ABI extends Abi

ProcessResultType ​

ProcessResultType

ProcessorConfig ​

ProcessorConfig = undefined

Properties ​

createProcessor ​

createProcessor: (state, context, bundle?) => EntityEventProcessorLike<ABI, ProcessResultType, ProcessorConfig> | Promise<EntityEventProcessorLike<ABI, ProcessResultType, ProcessorConfig>>

Defined in: packages/browser/src/IndexerState.ts:462

The fold, over that state. The FACTORY, not its result: processorIdentity NAMES the generation.

The third argument is the PUBLISHED BUNDLE this generation runs, present exactly when the spec names a processorBundle: its processor is the authoring object the bundle's own bytes made, which the factory wraps in the entity runtime over state the way it would wrap an imported one (new EntityEventProcessor(state, bundle.processor)). A module arrival is handed undefined and ignores it.

Parameters ​

state ​

WritableStateStore

context ​

GenerationContext

bundle? ​

InstantiatedProcessorBundle

Returns ​

EntityEventProcessorLike<ABI, ProcessResultType, ProcessorConfig> | Promise<EntityEventProcessorLike<ABI, ProcessResultType, ProcessorConfig>>


createState ​

createState: (context, patience, bundle?, published?) => WritableStateStore | Promise<WritableStateStore>

Defined in: packages/browser/src/IndexerState.ts:399

Where THIS generation's state lives, CLAIMED. Called once, before its processor.

It hands back a WritableStateStore, which is what openForWriting returns and the only way to obtain one: folding is writing, so the factory that builds a generation's store is where the claim is taken (ADR-0077). A reader never comes through here, which is the whole point of the narrowing -- a tab that only renders holds the store as a StateStore and cannot mutate it.

The claim is per STORE INSTANCE and openForWriting is idempotent over one, so the shipped createState: () => store shape (one instance for every generation) takes ONE claim and every generation writes through it.

FORWARD THE SIGNAL TO openForWriting ​

ts
createState: async (context, {signal}) =>
  openForWriting(await createBrowserStateStore(processor.entities), {signal}),

Taking a claim is one round trip to the storage, and a storage that never answers makes it hang for ever -- which is not hypothetical: on WebKit a database can be left permanently unable to run any transaction, and an unbounded claim there is an application stuck in waiting with nothing to render (work/notes/findings/webkit-does-not-abort-a-terminated-workers-indexeddb-transaction.md). The signal is this indexer's patience for the CLAIM and nothing else.

Give it to the CLAIM, not to everything this factory does. It is sized for a single transaction (claimWithinSeconds, ten by default), so wrapping a snapshot download or a long migration in it would refuse a healthy deployment on a slow connection. Those have their own timeouts to state.

Forwarding is a convention and not a guarantee, because a factory is the application's code: the signal is handed over, and an implementation that ignores it goes back to waiting for ever. That is the price of the factory being the app's, and it is why this is documented at every call site rather than enforced at one.

Parameters ​

context ​

GenerationContext

patience ​

ClaimPatience

bundle? ​

InstantiatedProcessorBundle

published? ​

PublicationSnapshot

Returns ​

WritableStateStore | Promise<WritableStateStore>


openState? ​

optional openState?: (context, bundle?) => ReaderState<ProcessResultType> | Promise<ReaderState<ProcessResultType>>

Defined in: packages/browser/src/IndexerState.ts:448

THE READER FACTORY: where a tab that does NOT index reads this state from (ADR-0097, D2). Optional, and it is half of the tab election's opt-in: the election runs only when the host is also given a tabElection name.

It hands back the SAME storage createState would open, opened for READING (openForReading), plus the read handle over it -- no claim, no processor, no fetch. Derive it and createState from ONE store constructor, so the two can never name different databases (stateFactoriesFrom, @etherfold/processor-entities):

ts
...stateFactoriesFrom({
  open: (context, entities) => createBrowserStateStore(entities, {databaseName: `app-${context.stream}`}),
  entities: processor.entities,
}),

Written by hand it is the same constructor with openForReading: openForReading(await createBrowserStateStore(processor.entities, {databaseName: app-${context.stream}})) and {store, state: new EntityStateView(store)}.

A tab that does not hold the election's lock is built from this at once: its reads answer the shared store the leader writes, it follows the leader's state-moved signal and progress, and it queues for the lock. When it wins, it becomes the writer through createState exactly as a fresh start does (ADR-0078). The bundle is the published bundle where the spec runs one, as createState is handed it.

Parameters ​

context ​

GenerationContext

bundle? ​

InstantiatedProcessorBundle

Returns ​

ReaderState<ProcessResultType> | Promise<ReaderState<ProcessResultType>>


processorBundle? ​

optional processorBundle?: ProcessorBundleSource

Defined in: packages/browser/src/IndexerState.ts:521

THE BUNDLE ARRIVAL: run the processor bundle a build PUBLISHED, fetched from url, and name this generation by the SHA-256 of those bytes (ADR-0095).

The host fetches the bytes, hashes them exactly as etherfold build does, refuses them if they are not self-contained, and instantiates the processor FROM THOSE BYTES -- inside the worker, where the host is one -- before it builds this generation's state. The instantiated bundle is handed to createProcessor as its third argument, and its identity is what the generation is registered under, so a snapshot published for that bundle is one this tab can use.

A refusal (ProcessorBundleRefusedError, with the refusal's reason) stops the host before anything is claimed or folded: init rejects with it on the main thread, and a worker host reports it as phase: 'refused' with the failure a tab reads across the port. A strict Content-Security-Policy is one of those refusals, forbidden-by-policy, and names the policy.

It EXCLUDES processorIdentity: the identity of a bundle is derived from the bytes that run, never supplied beside them, and a spec naming both is refused as a wiring mistake. Development keeps the MODULE arrival (hot updates, the module identity); this is what a deployed tab runs.


processorIdentity? ​

optional processorIdentity?: string

Defined in: packages/browser/src/IndexerState.ts:498

THE IDENTITY THIS GENERATION WAS HANDED, where the ARRIVAL derived one: the fold half of the generation this spec registers.

ADR-0086: an author cannot STATE a processor's identity, so it comes from what the processor IS. A tab running a published BUNDLE is named by the SHA-256 of those octets, which processorBundle derives from the bytes it runs (use that rather than supplying a hash here, which ADR-0095 refuses: a hash handed in beside a module names bytes the tab does not run), and an edited handler is a different generation whether or not anybody remembered to say so. Nothing here or below looks INSIDE the value -- it is COMPARED and RENDERED, and nothing in the tree parses one -- so neither this hook nor the container cares which arrival derived it.

It is handed straight to GenerationSpec.processorIdentity (@etherfold/core) and is ALSO what this hook keys its own per-generation store record on, so the name the registry files and the name the scheduled prune looks a store up by cannot be two different values.

ABSENT says only that no arrival named this fold before it was built: a tab handed a MODULE derives one from the module's handler sources the moment createProcessor has returned (moduleProcessorIdentity), and a module that cannot be named that way is refused rather than left nameless.

It is PER GENERATION, which is why it sits here beside the factories rather than among the options: two generations given one identity would be ONE registry record, ONE state namespace and one fold of a stream two specs asked to fold separately.


registry? ​

optional registry?: GenerationRegistry

Defined in: packages/browser/src/IndexerState.ts:533

Which generations this indexer holds and which one is canonical.

Defaults to a MEMORY registry under BROWSER_GENERATION_CAPS, because this hook knows no indexer NAME and a durable registry is addressed under one -- inventing a name here would fork the discriminator the stream address already carries. A tab that holds one generation re-registers it on every boot and loses nothing by that; an app that keeps a superseded generation to move the pointer BACK to wants a durable one and passes openGenerationRegistryOnIndexedDB(name, {dropState}).