- Complete bidirectional sync fix: eliminate notification loop caused by stale-focus guard, then stabilize with stress testing
- WASM build target (
wasm-pack build --target web) - TypeScript SDK build via
npm run buildinsdk/ - All Rust crates must compile (
cargo check --workspace) - Instrumentation logs must clearly identify each pipeline stage with unique prefixes
- Bug A — Per-tab OPFS storage path fix: Changed
crates/vaultsync-wasm/src/client.rslines 58 and 119 fromformat!("{}_db", namespace)toformat!("{}_{}_db", namespace, replica_id). Each tab now has its ownindex.json. Cross-tab OPFS lost-update race eliminated. - Bug B — Focus guard notification loop: Identified and fixed the root cause of one-way replication in
sdk/examples/notes/src/components/NoteEditor.tsx. Thedocument.activeElementfocus guard at line 60 blocked external merges when the textarea merely had focus (even with no active typing), causing the follower to auto-save stale state back to the leader via BC commands, creating an infinite loop. - Fix applied: Replaced binary
document.activeElementcheck with edit-timestamp cooldown. AddedlastEditRef.currenttracking per field ({ title: number, body: number }) andFOCUS_COOLDOWN_MS = 2000. A field is "actively editing" only if it has focus AND the user typed within 2 seconds. Stale focus does not block merge. Added[FocusGuard] console.warnlogs. - BC command instrumentation: Added
[BC] FOLLOW_INSERT_BEGIN/[BC] FOLLOW_UPDATE_BEGINlogs on the follower side (payload_len + first 500 chars of payload) and[BC] LEADER_INSERT_BEGIN/[BC] LEADER_UPDATE_BEGINlogs on the leader side (field_count, keys, payload_len + first 300 chars of payload) - Build verification: All Rust crates pass
cargo check --workspace, WASM binary builds, all 5 SDK packages build clean - Phase 4 — Replay batching: Aggregates stale push skips into single
debug!line withswap(0)counter - Phase 5 — Upload worker instrumentation: WAKE/OFFLINE/RECONNECTED/BATCH markers with stage prefixes
- vaultsync-cli fix: Added missing
FireSource::LocalWriteargument - vaultsync-napi fix:
*constraw pointers →usizefor#[napi]Send bound - Bidirectional sync confirmed: Both leader→follower and follower→leader work reliably. Logs show clean
PUSH_SEND → PUSH_ACK → AFTER_MARK_SYNCED pending=0 → BATCH_DONEon every upload. Reconciler firessubscription.fireonce per mutation. Document content is identical on both tabs across multiple alternating edits.
- Stress test suite: 6 scenarios to run before removing instrumentation: (1) 3 tabs simultaneous edits, (2) leader closes while follower editing (leader election), (3) offline → edit → reconnect, (4) refresh one tab during continuous edits, (5) create/delete multiple notes concurrently, (6) 15-minute soak with continuous alternating edits.
- (none)
- Focus guard was the root cause of one-way replication: The
NoteEditor.tsxdocument.activeElementcheck at line 60 prevented the follower from merging external edits when its textarea had focus (even stale, no-active-typing focus). This caused the auto-save to fire with stale local state, sending a BC command to the leader that overwrote the correct data with stale data. The subsequent push then re-triggered the cycle. - Edit-timestamp cooldown replaces binary focus check: Instead of checking
document.activeElement?.id !== 'note-body-textarea', the fix checks if the field has focus AND the user has typed withinFOCUS_COOLDOWN_MS(2 seconds). This protects active editing while allowing external merges when the focus is stale. - The fix is a circuit breaker, not a semantic version guard: The fix uses string equality (via
lastEditReftimestamps + content hashing) rather than CRDT document revision IDs. A more robust solution would tracklastAppliedRemoteRevisionto prevent re-saving data that arrived from remote.
- Stress test: Run 3+ tab multi-scenario stress test suite before removing instrumentation
- Demote instrumentation logs: Move
[opfs_flush],[BC fire_subscription],[FocusGuard], upload pipeline markers frominfo!/console.warntotrace!/console.debugafter stress tests pass - Tag release: Once stable, tag a release and update AGENTS.md with final status
- Bug A (OPFS race) — FIXED: Cross-tab OPFS race eliminated by per-tab storage paths. Upload pipeline health confirmed by consistent
PUSH_ACK sequences=[N],AFTER_MARK_SYNCED pending=0,BATCH_DONE success=1. - Bug B (Focus guard loop) — FIXED: One-way replication caused by
document.activeElementcheck blocking external merges on the follower tab. Fixed with edit-timestamp cooldown. Both directions now confirmed working. - Bidirectional sync is stable: Logs show clean single-pipeline flow:
upload → PUSH_SEND → PUSH_ACK → AFTER_MARK_SYNCED pending=0 → BATCH_DONE → reconciler → subscription.fire → React callback → setData. No repeating notification cycles, no pending uploads. - The focus guard was the biggest remaining bug after OPFS fix: We traced the issue through Rust/WASM/OPFS/CRDT/BC/WebSockets/React/browser event loop, and the root cause was a single line in a React component that blocked external merge when the textarea had focus.
- Instrumentation should be kept until stress tests pass, then cleaned up for release.
sdk/examples/notes/src/components/NoteEditor.tsx: Fix applied. Replaceddocument.activeElementfocus guard with edit-timestamp cooldown usinglastEditRef.current.title/lastEditRef.current.bodyandFOCUS_COOLDOWN_MS = 2000. Added[FocusGuard] console.warnlogs.crates/vaultsync-wasm/src/client.rs: Bug A fix (per-tab OPFS paths). BC command instrumentation (follower[BC] FOLLOW_INSERT_BEGIN, leader[BC] LEADER_INSERT_BEGIN).crates/vaultsync-core/src/sync/upload.rs: Upload pipeline instrumentation (BATCH_START, PUSH_SEND, PUSH_ACK, AFTER_MARK_SYNCED, BATCH_DONE) — all confirmed healthycrates/vaultsync-core/src/sync/download.rs: Download worker wake path, cursor advancementcrates/vaultsync-core/src/sync/reconciler.rs: Reconciler ENTER/EXIT logs — confirmed workingsdk/packages/react/src/useQuery.ts,useVaultSyncOne.ts: React hooks with notification chainsdk/examples/notes/src/components/NoteEditor.tsx: Focus guard fix with edit-timestamp cooldown