Skip to content

refactor(sync): extract submitLocalBatch as the single local intake - #259

Merged
winlp4ever merged 2 commits into
mainfrom
refactor/sync-submit-local-batch
Aug 28, 2026
Merged

refactor(sync): extract submitLocalBatch as the single local intake#259
winlp4ever merged 2 commits into
mainfrom
refactor/sync-submit-local-batch

Conversation

@winlp4ever

Copy link
Copy Markdown
Contributor

What

First, behavior-neutral step of S7 (headless sync-correct emit). Extracts the single local-batch intake that the headless producer (S7b) will call, without changing any current behavior.

Why

Local batches reach the sync coordinator two ways today:

  • store-commit pathattachSync.sendBatch registers the batch in the pending rebase set and schedules a pump (correct);
  • direct oplog writegetBoardPersistenceRef().record(...) in the deep-cascade delete (subtree.ts) lands in the oplog but never joins pending, never triggers a pump, and never gets a serverSeq on ack. On a synced board that batch ships opportunistically and is never rebased against concurrent remote ops — the "bypasses the outbox -> desyncs" hazard.

S7b needs one correct place for an off-scene / cross-layer producer to enter. This PR creates it.

How

  • board-sync.ts: extract submitLocalBatch(batch) = register in the pending rebase set + schedulePump(). attachSync.sendBatch now routes through it (identical behavior). Exposed on BoardSyncHandle.
  • board-sync-ref.ts (new): module singleton mirroring board-persistence-ref, so code outside the harness can reach submitLocalBatch. null on a purely local board (no relay) — callers fall back to the persistence ref, which is sync-correct there.
  • use-board-sync-v2.ts: publish the handle via setBoardSyncRef on mount, clear on detach (before handle.detach()).

No producer is rewired onto the headless path in this PR — the store path is untouched, and the subtree.ts bypass is left as-is (fixed in S7b, where the durable-only rebase handling and ack/serverSeq stamping land with their own tests).

Test plan

  • New test: a batch that was never applied to the live store is recorded to the oplog and entered via handle.submitLocalBatch(...); it ships as one relay message and the peer converges — proving the exposed seam takes the outbox/rebase path, not a bypass.
  • All 29 existing board-sync behaviors pass unchanged.
  • npm run check-all passes.

Follow-ups (rest of S7)

  • S7b — route subtree.ts (and the future headless BoardMutator) through submitLocalBatch into a durable-only rebase set (off-layer batches must be excluded from the in-scene undo/replay loop, since their inverse would re-add nodes into the wrong layer's store), with ack/serverSeq stamping.
  • S7c — ADR-SYNC-001 update documenting "one intake, two producers, one seq authority" and the scene-vs-durable-only rebase split.

Local batches enter the sync coordinator through two paths today — the
store-commit path (attachSync.sendBatch → rebase set + pump) and a direct
oplog write (getBoardPersistenceRef().record) that skips the rebase set and
send trigger, desyncing synced boards.

Extract `submitLocalBatch(batch)` as the one sync-correct intake (track for
rebase + trigger a pump), route the store producer through it, and publish it
via `getBoardSyncRef()` so a headless/off-scene producer can enter the same
path instead of writing the oplog directly. Behavior-neutral: the store path
is unchanged; the ref + seam are additive groundwork for the headless
producer (S7b).
… injection

A headless / off-scene batch's ops are not in the loaded store, so adding it
to the rebase set would make applyRemote replay those ops into the current
scene on the next remote op. Gate rebase tracking behind `scene` (default
true for store commits); `scene: false` batches are pumped but not rebased.
Strengthen the seam test to drive a remote op after a headless submit and
assert the off-scene node is never injected into the submitter's store.
@winlp4ever

Copy link
Copy Markdown
Contributor Author

Review response (code-review high)

Addressed the seam-contract findings in a7fb33e:

  • Rebase-injection (finding feat(*): first commit #1): submitLocalBatch now takes scene (default true). Store commits stay rebase-tracked; a headless / off-scene batch enters with scene: false — pumped but kept out of the rebase set, so applyRemote never replays its off-layer ops into the loaded scene.
  • Weak test (finding feat(*): setup semantic versioning #2): the seam test now seeds an in-scene edit, submits a headless batch with scene: false, and drives a concurrent remote op afterward — asserting the off-scene node reaches the peer but is never injected into the submitter's store (before or after the rebase).
  • Unused seam (finding ci(github): publish github releases from workflow #3): the subtree.ts migration stays in S7b (its own behavior change + tests), but the headless (scene: false) branch is now directly exercised by the test, so it is not dead code.

Bonus — this de-risks S7b. Since off-scene batches never enter pending, they never hit applyBatch, so the cross-layer inverse-safety concern (undoing a node.remove re-adding a node into the wrong layer) disappears entirely. S7b becomes: record + submitLocalBatch(batch, { scene: false }) — no durable-only rebase set needed.

@winlp4ever
winlp4ever merged commit aeb27e8 into main Aug 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant