Skip to content

Documentation / @etherfold/core / StateMovedPublisher

Class: StateMovedPublisher ​

Defined in: packages/core/src/stateMoved.ts:278

THE PRODUCER'S HALF, as ONE assembly both containers hold.

There are two things in this system that apply blocks -- the chain-facing Indexer and the chain-free ReceivingIndexer -- and ADR-0083 says both publish THE SAME signal FROM THE SAME ASSEMBLY, because one notification model is the claim being made and two producers that drift is how that claim dies. So the subscription, the containment and the token live here, and a container supplies only what a container knows: which block, which generation, and whether the fold that applied it is the one that answers reads.

It holds NOTHING per subscriber ​

Nothing is buffered, nothing is retried, nothing is remembered about who is listening: one handler reference each, and that is the whole of it. That is what stops a SharedWorker's memory growing with the number of open tabs, and it is the property every transport downstream depends on, so it belongs here rather than in each of them. A client that missed a notification is repaired by the next one plus the token, which is the trade ADR-0083 makes explicitly.

Constructors ​

Constructor ​

new StateMovedPublisher(): StateMovedPublisher

Returns ​

StateMovedPublisher

Accessors ​

subscriberCount ​

Get Signature ​

get subscriberCount(): number

Defined in: packages/core/src/stateMoved.ts:305

How many handlers are attached. Reported so a test can see that nothing else is kept.

Returns ​

number


token ​

Get Signature ​

get token(): string

Defined in: packages/core/src/stateMoved.ts:310

The token every notification is currently carrying.

Returns ​

string

Methods ​

publish() ​

publish(moved): void

Defined in: packages/core/src/stateMoved.ts:349

PUBLISH one applied block, stamped with the token in force and with its CASE.

Both stamps are made HERE rather than supplied by the caller, and for one reason: a producer must not be able to publish a token it has not rotated, nor an append wearing a retraction's tag. publishRetraction and rotateForPointerMove beside this are the only other ways out, and both rotate.

Parameters ​

moved ​

Omit<StateApplied, "coherence" | "kind">

Returns ​

void


publishRetraction() ​

publishRetraction(retraction): void

Defined in: packages/core/src/stateMoved.ts:369

PUBLISH a retraction, ROTATING the token as part of publishing it.

The rotation is not the caller's to remember. "A missed notification is repaired by the next one" is true of an APPEND and false of a RETRACTION -- after a reorg the stale entities are the ABANDONED branch's, and those are generally not in the changed-set of whatever block arrives next -- so a retraction published under the OLD token would leave a reader that missed it invalidating narrowly and keeping dead-branch rows indefinitely. Putting the two in one call is what makes that unexpressible, here, in the one assembly both containers publish from (ADR-0083).

The retraction therefore carries the NEW token: a reader that RECEIVED it invalidates everything and is then holding exactly the token the appends after it carry, so it invalidates narrowly again from the next block on.

Parameters ​

retraction ​

Omit<StateRetracted, "coherence" | "kind">

Returns ​

void


rotate() ​

rotate(reason): string

Defined in: packages/core/src/stateMoved.ts:335

ROTATE THE TOKEN: something happened that may have made ANYTHING a reader holds wrong.

ONE LINE AT THE POINT WHERE THE REASON OCCURS, which is the whole shape of it: a RETRACTION rotates it because the stale entities are the abandoned branch's and no changed-set names them (publishRetraction below does it, so that a retraction which forgot to rotate is unexpressible), and a POINTER MOVE rotates it because a different fold now answers, which from a cache's point of view is indistinguishable from "everything you hold may be wrong" (rotateForPointerMove below does it, for the same reason). One comparison and one code path in a reader rather than three.

It rotates whether or not anybody is listening, because the token is a fact about THIS PRODUCER's history and not about a delivery: a reader that attaches afterwards, or a transport that reconnects, must still be told that what it holds from before is suspect.

reason is LOGGED and never published: the token names nothing, because a reader that could read a reason out of it would be parsing it.

Parameters ​

reason ​

string

Returns ​

string


rotateForPointerMove() ​

rotateForPointerMove(reason): (moved) => void

Defined in: packages/core/src/stateMoved.ts:395

ROTATE for a POINTER MOVE now, and answer the call that ANNOUNCES it.

Two steps rather than one, because the two halves belong at two different points of a move, and getting either order wrong is visible to a reader:

  • the ROTATION belongs FIRST, before anything tells a reader to re-read (a container's pointer-moved callback, its state notification), so that no question about the new generation is answered under the retired one's token;
  • the ANNOUNCEMENT belongs LAST, once the read path has followed the pointer, so that a reader re-reading the instant it is told is answered by the generation the announcement names rather than by the one it replaced.

The announcer carries the token rotated HERE, so a pointer move published under a token it did not rotate is unexpressible, exactly as a retraction's is. It is called once; calling it again announces the same move again, which is a caller's bug and not one this guards (it holds nothing to guard it with).

A move that is NOT announced (a caller that rotated and then threw) leaves the token rotated, which is the safe direction: the next notification carries it.

Parameters ​

reason ​

string

Returns ​

(moved) => void


subscribe() ​

subscribe(handler): StateMovedDetach

Defined in: packages/core/src/stateMoved.ts:297

BE TOLD THE STATE MOVED. Returns the detach.

ts
const detach = indexer.onStateMoved(({entities, coherence}) => {
  if (coherence !== held) return queryClient.invalidateQueries();
  for (const entity of entities) queryClient.invalidateQueries({queryKey: [entity]});
});

Best-effort: a handler is called as the block lands and is never called again for one it missed.

Parameters ​

handler ​

StateMovedHandler

Returns ​

StateMovedDetach