Skip to content

v0.43.0

@nick-inkeep nick-inkeep tagged this 29 Jul 10:32
* test(ok): pin the __system__ refresh epoch-rotation suppression race

Two RED gates for a pre-existing suppression race in the provider pool.
The token-less __system__ provider re-syncs freely after a server restart
and its refresh writes the rotated epoch into cachedServerInstanceId, which
doubles as the recycle-dedupe latch. Per-doc providers holding frozen stale
claims then bail at that latch and never recycle.

A deterministic interleaving unit test drives the exact ordering through the
pool's public entry points, and an integration falsifier wires the real
__system__ subscriber alongside a real same-port restart and forces the
refresh to reach the pool first. Both fail on the current implementation.

* fix(ok): route an observed epoch rotation through the recycle transition

The provider pool used cachedServerInstanceId as both the live-epoch cache
and the recycle-dedupe latch. Any other writer of that field could disarm
the latch, and one exists: the __system__ provider carries no epoch claim,
so the server never rejects it, and after a restart its refresh reports the
rotated epoch over HTTP while per-doc providers are still retrying tokens
frozen with the dead one. Those providers then found cached != claimed and
bailed, so the recycle never ran and the docs stayed offline for the rest of
the session behind a toast promising their edits would sync.

setExpectedServerInstanceId becomes an observe-epoch entry point. The same
epoch is a real no-op, the first epoch after boot or a recycle is adopted,
and a rotation routes through the same transition handler a rejected claim
takes before the new epoch is adopted. Dedupe now keys on the stale claim
value, which no other writer can touch, with the in-flight recycle promise
collapsing siblings that report different dead epochs. Both dedupe bails
emit a breadcrumb; this was the last silent one on a loss-adjacent path.

__system__ stays token-less on purpose and now says so at the construction
site: it is the only channel by which a live client learns of a rotation,
and it can only be that because it has no claim for the server to reject.

* test(ok): retarget the warm-persistence gate at the epoch it observes

The gate drove attachDeferredPersistence's already-attached guard by
observing a different id, which is now a rotation and legitimately recycles
the entry. Split it: re-observing the live epoch still asserts the exact
persistence identity it always did, and a second case pins what a rotation
does to that entry instead of leaving the behavior unpinned.

Ship the changeset alongside so the stuck-document fix has release notes.

* chore(ok): refresh the ng-anchors catalog for the new test file

The catalog's testFileCount is gated by a strict deep-equal in the
md-conformance test tier; the lighter freshness scripts do not see it.

* fix(ok): keep one dispatch to one whole-pool invalidation

Routing an observed rotation through the recycle made a synchronous caller
able to start one, which one /api/server-info response can now do two of: it
observes the epoch first, then resolves the branch, and the cross-branch
invalidation had no stand-down because nothing could previously reach a
mismatch recycle from the same dispatch. The second pass tore down providers
the first had just re-opened and drove the restart-recovery state machine off
a snapshot that was already replaced. The CC1 push has the same composition.

handleBranchSwitched now stands down while a mismatch recycle owns the pool,
the same way flushOnHide and resyncOnVisible do. That is only safe because
the discard policy is enforced where the buffer is consumed rather than only
at that entry point, so the replay drain now fences on the buffer's own
capture branch: edits authored against a branch the client has left are
discarded, whichever channel triggered the recycle.

Also from review: the dedupe breadcrumbs carry the stale claim so a burst can
be correlated, the authenticationFailed docblock states the claim-keyed
dedupe rather than the old cached-claim story, and the rotation tests assert
the epoch is adopted, not merely cleared.

* test(ok): cover the in-flight collapse and correct two epoch docblocks

The sibling-collapse test was vacuous: its second emission came from a
provider the first recycle had already destroyed, so the listener was gone
and the in-flight latch was never exercised. Rebuilt on two live providers
holding distinct claims, asserting one recycle plus the collapse breadcrumb
and its claim. dispose() now nulls mismatchInFlight like its branch-side
sibling, with a test, since a latched flag would make every later whole-pool
invalidation stand down against a recycle that can never settle.

The cachedServerInstanceId and pendingServerInstanceKnown docblocks still
described unconditional clearing and a resolve-race that only the
auth-rejection arm has; both now distinguish the two arms.

* test(ok): pin both halves of the collapse breadcrumb

Correlating a burst needs the pair, not just the collapsed claim.

GitOrigin-RevId: 1cdd481bff1be0738780b42b7562e12a30fe6700
Assets 2
Loading