From 43858cd524d88996b27bc71874be243972dd688d Mon Sep 17 00:00:00 2001 From: "Khokon M." <50947615+khokonm@users.noreply.github.com> Date: Wed, 24 Jun 2026 20:18:48 +0600 Subject: [PATCH] fix(sync): replay pending transfer requests so approval prompt survives reconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New-device join approvals were delivered fire-and-forget via sendToDevice and lost whenever the approver had no live WebSocket at that instant (e.g. a mobile PWA whose socket drops when backgrounded). The join handler never re-delivered them, so the approval prompt never appeared — 'not even when they return'. getPendingTransfers() existed for exactly this but was dead code. Wire it into the join handler: when an established device (re)connects, replay any still-pending transfer requests in its room whose requester is currently online, so it sees the approval prompt. New (needsTransfer) devices are skipped since they have no notes to send. --- server/src/index.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/server/src/index.ts b/server/src/index.ts index c4ba47f..600c5a7 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -60,6 +60,7 @@ import { truncateDeliveredOps, createTransfer, getTransfer, + getPendingTransfers, approveTransfer, updateTransferStatus, updateTransferResumeToken, @@ -687,6 +688,35 @@ function handleMessage(client: Client, msg: Record, ip: string) })) } } + + // Replay any still-pending transfer requests so an approver that was + // offline (or backgrounded — mobile PWAs drop their socket whenever the + // app loses focus) sees the approval prompt as soon as it (re)connects. + // Previously `transfer-requested` was a fire-and-forget `sendToDevice` + // that was silently lost if the approver had no live socket at that + // instant, and nothing ever re-delivered it — so the prompt never showed, + // "not even when they returned". An established device that joins is a + // candidate source/approver; a brand-new device (needsTransfer) has no + // notes to send, so it is never asked to approve. + if (!isNewDevice) { + const pending = getPendingTransfers(roomId) + for (const t of pending) { + if (t.status !== 'pending') continue // already approved/in-flight + if (t.requesterId === deviceId) continue // don't ask the requester to approve itself + // Only prompt when the requester is still connected to receive the data. + const requesterOnline = Array.from(room).some( + c => c.deviceId === t.requesterId && c.ws.readyState === WebSocket.OPEN, + ) + if (!requesterOnline) continue + const requester = getDevice(roomId, t.requesterId) + client.ws.send(JSON.stringify({ + type: 'transfer-requested', + transferId: t.id, + requesterId: t.requesterId, + requesterName: requester?.deviceName || 'A device', + })) + } + } return }