← stitchkitDocumentation

ADR 0183 — A watched value may cross as a difference

Decision

stitchkit.watch.value is a discriminated union on kind:

  • full — the whole value, as before.
  • delta — a structural difference (stitchkit/live’s WatchDelta) against base, a revision the subscriber is known to hold.
  • unchanged — the answer the subscriber already has is still the answer.

Every frame carries fingerprint, the order-independent digest (argumentsDigest({ value })) of the value the receiver holds after applying it. stitchkit.watch.open may carry have: { revision, fingerprint }.

The hub chooses the shape per subscriber, from the revision it last delivered to that subscriber without the delivery throwing. It keeps superseded values per key under a byte ceiling — deltaMemoryBytes, default 256 KiB, and 0 disables differences entirely. A difference is sent only when its encoded length is strictly shorter than the value’s.

The client rebuilds the value, checks the result against fingerprint, and on any failure — a base it does not hold, a difference that will not apply, a fingerprint that disagrees — resynchronises that key alone: it drops the value, publishes resync-required, closes and re-opens.

Why

A watched read publishes the whole answer on every change. Measured by a consumer: a ~75 KB list answer in which two timestamps move, republished every fifteen seconds, is a megabyte per subscriber per minute to carry a hundred bytes of news. After a socket drop the same subscriber is sent every value again, including the ones that did not change while it was away.

Alternatives rejected

JSON Patch / RFC 7386 merge patch. Merge patch cannot remove an array element or distinguish null from “delete”, and a watched read’s value is arbitrary application JSON where both are ordinary. Both formats cost one operation per element for an array that merely slid by one — the shape every paged and tailing list has, and the case this exists for.

Acknowledging each frame. A baseline confirmed by the subscriber would be exact, and would put a round trip in front of every frame. The socket is ordered and reliable while it is up; when it is not, the subscriber re-declares what it holds in open. Delivery that throws does not advance the baseline.

A revision alone as the resume token. Revisions restart at zero when a key is released and re-acquired, so a bare revision lets a value from a previous life of the source pass for the current one. The fingerprint decides; the revision only says where to look.

Tolerant reassembly. A difference applied to the wrong base produces a plausible value that nobody holds. applyWatchDelta throws instead, and the client resynchronises.

Limits

Resuming needs the key to still exist on the hub. With holdMs: 0 and no other subscriber, the last detach releases the source and a reconnecting page pays the whole value once; an application that wants cheap reconnections sets holdMs past its reconnect delay. Differences are per process — two hub processes behind a balancer keep separate baselines, as they already keep separate reads.

The frame shape changed, so both ends must come from the same major: a client older than this release reads a delta frame as a value of undefined.