Skip to content

fix(sync): classify transfer-need by explicit join intent + auto-request source so approval prompt appears#11

Merged
khokonm merged 1 commit into
mainfrom
fix/sync-bootstrap-intent-and-auto-request
Jun 25, 2026
Merged

fix(sync): classify transfer-need by explicit join intent + auto-request source so approval prompt appears#11
khokonm merged 1 commit into
mainfrom
fix/sync-bootstrap-intent-and-auto-request

Conversation

@khokonm

@khokonm khokonm commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Symptom (after #10)

The new device now connects, but immediately shows "Connected" + the device list and never asks to sync, so the old device gets no approve/deny prompt. On refresh the new device briefly shows "Connecting…" again.

Root cause — "needs a transfer?" was inferred, not declared

Whether a joining device should pull the existing notes was being guessed from proxies that don't actually mean "this device is a member of the chain":

  • fix(sync): fix stuck-on-Connecting (wiped sync code) + device-role classification #10 used hasData (does the device hold any local notes). But a fresh device usually does have a note (a seed/empty note, or something typed before joining), so hasData was true → the server marked it already-established → it never requested a transfer → the old device never got a prompt. That's the "immediately Connected" behavior.
  • The earlier cursor === 0 proxy was wrong in the opposite direction (the origin has cursor 0).

The only reliable signal is explicit intent: did this device generate the code (it's the origin/source) or enter a code to join (it must bootstrap from an existing device)? That's a per-chain fact the device knows and must remember.

Fix — persisted per-chain intent + auto-request

  1. Client declares its role. New notes-sync-initialized-room localStorage key records the chain (roomId) this device has bootstrapped. A device is "initialized" when it generated the code or finished receiving a transfer. join now sends needsTransfer = !initializedForThisRoom instead of hasData.
    • enableSync(..., { initialized: true }) on generate; { initialized: false } on entering a code.
    • On transfer completion, the device marks itself initialized → it won't ask again and reconnects as a full member (fixes the "Connecting… again after refresh / asks forever" loop).
  2. Server trusts the declaration. isNewDevice = clientNeedsTransfer && otherDevices.length > 0 (replacing the hasData/cursor guess). This drives the welcome needsTransfer, the catch-up-ops gate, and the fix(sync): show device-approval prompt after reconnect (replay pending transfers) #8 approval-replay gate.
  3. Auto-request the source. When a device still needs its initial copy and an existing device is online, it now automatically sends the transfer request (on welcome and when a device comes online), so the approve/deny prompt appears on the existing device without the user manually picking a source. The "Choose a source device" picker remains as an override for multi-device setups. Self-guarded so it never fires while a transfer is pending/in progress.

Files changed

  • src/utils/sync.ts — per-chain initialized flag + helpers; buildJoinPayload sends needsTransfer; welcome uses client-authoritative needsTransfer; maybeAutoRequestTransfer() on welcome + device-joined; mark initialized on transfer completion; enableSync intent param; clear flag on disable.
  • src/components/SettingsPanel.tsx — pass { initialized: true } on generate, { initialized: false } on join.
  • server/src/index.ts — classify role from msg.needsTransfer; protocol doc comment.

Where the prompt appears

On the device that generated the code / holds the notes: a full-screen "Allow sync to ?" modal (Deny/Approve) and a "Transfer Request" card under Settings → Sync. With auto-request, you no longer need to tap a source on the new device — just keep both apps open.

Verification

  • Traced: enter code → enableSync({initialized:false}) → join needsTransfer:true → server isNewDevice:true → welcome → auto-request → old device shows approval → approve → chunks → new device marks initialized → subsequent joins are needsTransfer:false ("Connected").
  • Not run locally: tsc/build/lint — sandbox has no installed toolchain for either workspace and installs are blocked (registry 403). Changes use only existing in-scope symbols; requestTransferFromDevice is a hoisted function declaration referenced by maybeAutoRequestTransfer.

Manual test plan

  1. Device A: enable sync → Generate. Status should be Connected (A is the source).
  2. Device B (fresh): enter the code with both apps open. Within a moment, A shows the approve/deny prompt automatically (no tapping on B).
  3. Approve on A → B receives notes and shows Connected.
  4. Refresh B → it reconnects to Connected (does not ask to sync again, no stuck "Connecting…").
  5. Deny on A → B shows the picker so you can retry; pick A again → prompt reappears on A.
  6. Multi-device: with a 3rd device, the "Choose a source device" picker still lets you select which device to copy from.

…st source

After #10 the new device connected but immediately showed 'Connected' and never
asked to sync, so the old device got no approval prompt. Cause: 'needsTransfer'
was inferred from hasData (note count), and a fresh device usually has a seed
note -> it was wrongly classified as already-established.

Replace the guess with explicit, persisted per-chain intent: a device is
'initialized' for a room when it generated the code or finished receiving a
transfer (new notes-sync-initialized-room key). The client now declares
needsTransfer on join; the server trusts it (isNewDevice = needsTransfer &&
others>0). On completing a transfer the device marks itself initialized, so it
stops asking and reconnects as a full member (fixes the connecting-again loop).

Also auto-request a transfer from an online device on welcome/device-joined, so
the approve/deny prompt reaches the existing device without manually picking a
source; the picker stays as an override.
@khokonm khokonm merged commit 24bb347 into main Jun 25, 2026
3 checks passed
@khokonm khokonm deleted the fix/sync-bootstrap-intent-and-auto-request branch June 25, 2026 09:23
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