Skip to content
D
Documentation

CausalBuffer

reference
2 min readUpdated

Import it from @grafloria/engine.

Holds ops whose entity has not been added yet, and releases them the instant it is.

Sits BETWEEN the transport and the Replica. Nothing it holds has touched the log, the clock or the LWW registry — which is the whole point: an op the replica has never seen is an op that can still be applied later.

ts
class CausalBuffer

Properties

NameTypeDefaultDescription
overflowed0Ops force-released because the buffer was full. Should be 0. Watch it.

Methods

  • constructor( private readonly diagram: DiagramModel, options: CausalBufferOptions = {} )
  • get pendingCount(): number
  • noteLocal(op: Op): void — WE created an entity. Record it — the transport will never tell us about it.

THE FUZZ FOUND THIS ONE, AND IT IS A PERMANENT HANG, NOT A HICCUP.

known was fed from exactly two places: entities on the diagram at construction, and add ops that arrived from PEERS. A node the local user creates during the session is in NEITHER — so as far as the buffer was concerned, an entity this peer invented did not exist.

Now watch a peer edit it. Bob creates node X. Alice learns about it, drags it, and sends set(X, position). It arrives at Bob, whose buffer asks "have I seen an add for X?", answers NO, and holds it — waiting for an add(X) that is never coming, from anyone, ever. It cannot come: the only add(X) in existence is Bob's OWN, it is already in Bob's log, and Alice's anti-entropy correctly concludes that Bob already has it and never echoes it back.

So Bob's node freezes wherever it was when he made it, Alice watches it move, and the two documents differ forever — over the single most ordinary interaction in a collaborative editor: one person moving another person's node. On a healthy transport it never fires (the set cannot overtake an add that was never sent), which is exactly why it took a reordering channel to find it.

No drain is needed. A peer can only set an entity it has learned about, which means our add had already reached it — so nothing can be waiting on an id at the moment we create it. The registration alone closes the hole.

  • admit(incoming: readonly Op[]): CausalSplit — Split an arriving batch into what can be applied now and what must wait — and fold in anything that was already waiting and has just become releasable.
  • pending(): Op[] — Everything we are still holding — for a status panel, and for the tests.

Was this page helpful?

CausalBuffer — Grafloria