Skip to content

fix(sync): pump deep-layer cascade deletes through the sync intake - #260

Merged
winlp4ever merged 1 commit into
mainfrom
fix/sync-headless-subtree-delete
Aug 28, 2026
Merged

fix(sync): pump deep-layer cascade deletes through the sync intake#260
winlp4ever merged 1 commit into
mainfrom
fix/sync-headless-subtree-delete

Conversation

@winlp4ever

Copy link
Copy Markdown
Contributor

What

S7b of the sync spine work. Migrates the one real oplog bypass onto the submitLocalBatch seam from #259, so a synced board ships deep-layer cascade deletes correctly.

The bug

Deleting a folder whose children live in an unloaded layer runs a two-part cascade (subtree.ts): the loaded layer is removed through the store, and the deeper descendants are swept from the whole-board oplog. That deep sweep recorded its removals straight to the oplog via getBoardPersistenceRef().record(...), bypassing the sync coordinator entirely.

On a synced board the consequence is: the batch lands in the oplog but never triggers a pump, so it ships only opportunistically on the next unrelated edit, and is never serverSeq-stamped promptly. Deep-layer deletes could sit unsent (until a reload / next edit), and a peer wouldn't see the folder's descendants removed.

The fix

After recording, enter the sync-correct intake:

const batch = makeBatch(store, origin, ops)
persistence.record(batch)
await persistence.flush()
getBoardSyncRef()?.submitLocalBatch(batch, { scene: false })
  • scene: false because these ops target unloaded layers — they must NOT join the in-scene rebase set (replaying them would inject off-layer nodes into the current scene; the refactor(sync): extract submitLocalBatch as the single local intake #259 review established this).
  • On a local board getBoardSyncRef() is null, so nothing changes — the oplog record alone is sync-correct there (no outbox to desync).

No new rebase machinery is needed: because off-scene batches never enter pending, they never reach applyBatch, so the cross-layer inverse-safety concern from the original plan doesn't arise.

Test plan

  • New test: on a synced board (sync ref installed), a folder delete with a deeper child routes the sweep through submitLocalBatch exactly once, with scene: false, carrying the deeper node's removal.
  • All existing subtree tests pass unchanged — the local-board path (no sync ref) is a no-op, so the deep-sweep behavior is untouched there.
  • npm run check-all passes.

Follow-up

  • S7c — ADR-SYNC-001 update documenting "one intake, two producers, one seq authority" and the scene-vs-headless rebase split.

The deep-descendant sweep (deleting a folder whose children live in an
unloaded layer) recorded the removals straight to the oplog via the
persistence ref, bypassing the sync coordinator. On a synced board that batch
never triggered a pump — it shipped only opportunistically on the next
unrelated edit and was never serverSeq-stamped promptly.

Route it through getBoardSyncRef().submitLocalBatch(batch, { scene: false })
after recording: scene:false because the ops target unloaded layers and must
not enter the in-scene rebase set. Local boards have no sync ref, so the oplog
record alone stays sync-correct there (no outbox to desync).
@winlp4ever
winlp4ever merged commit 10e2b7c 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