Repository navigation
Closed
Labels
Description
Overview
Three narrow write-path gaps in SharedNodeStore / SimplePersistenceCoordinator, found while reviewing #3033. None loses data in a common flow, and the first recovers on its own, but all three belong to the same class: per-node write sequencing.
1. Create/typed flush can wait on a move that waits on it (5 s stall)
- Cause:
sendPendingTypedFields()awaits a pending move before sending. It runs:- after each create path (generic closure, setNode closure, batch closure);
- inside the typed write's own closure.
- Create path: when a node's create is still running,
indentNodefalls through to a MOVE that callsflushNodeSaves([nodeId]), which waits for that same create. With typed fields staged, the create waits on the move and the move waits on the create. Only the 5 s timeout influshAndWaitForNodesbreaks the cycle. - Typed path: the same shape occurs when two quick Tab presses are followed by a checkbox click.
- Existing precedent: the generic closure's own pending-move wait (before its update RPC) already has this pattern.
- Direction: in the create paths, hand the staged fields to a new coordinator write (
persist(nodeId, () => sendPendingTypedFields(nodeId), { mode: 'immediate' })) instead of flushing inline. That write queues behind the create, and the move captured the create's promise, not this one, so the cycle can't form. Apply the same review to the generic closure's wait.
2. A queued generic write's non-content fields can be replaced
The coordinator keeps one queued write per node, and the latest write wins regardless of mode. Staged typed fields survive this, but a generic write's capturedNonContentFields do not. The failure: typing, then a custom: property change via a shortcut, then more typing, all within one in-flight RPC — the property change is never sent to the server. Latest-wins is only safe for writes that carry full state.
- Direction: stage generic non-content fields the way typed fields are staged, or merge them when a queued entry is replaced.
3. OCC during a typed flush inside another write, while the node is focused
- Double toast, lost write: hydration is skipped, so the local version isn't refreshed. The generic write that ran the flush then conflicts again, raising a second version-mismatch toast, and its own change is lost.
- Unsettled callbacks: the OCC handler's
pendingTypedFields.delete(nodeId)drops any newly staged fields and their callbacks without firing either. - Direction: have
sendPendingTypedFields()report OCC so the calling write can skip its own RPC, and fireonPersistErrorfor any staged callbacks it drops.
Acceptance Criteria
- No create or typed write waits on a move that waits on it; covered by a test with a pending create plus an indent plus a staged typed field
- A generic non-content change queued behind an in-flight write survives a later queued write for the same node
- An OCC during a flush inside another write raises one notification, and every dropped callback is settled
Related
- Type person and project nodes like task (closes #2807) #3033 (typed person/project nodes; introduced the typed flush)
- An immediate write cancels a node's pending debounced content write #3036 (debounced-write promotion; fixed in Type person and project nodes like task (closes #2807) #3033)
Activity
Address re-review 2: coordinator promotion tests; tighten two nits
Type person and project nodes like task (closes #2807) (#3033)
malibio commentedon Sep 25, 2026
Frontend baseline: 269 files / 5313 tests passed
Sequence per-node writes: no move wait cycle, keep queued field chang…