d14b4d0b8ef7
fix: productionSyncRunner returns {ok:false,error} on vault locked instead of throwing; SyncRunResult becomes discriminated union (v3.6.102)
- time
- 2026-05-22 22:05 UTC · 5 months ago
- machine
- koh2
- parent
- 1969735e961d
↓ peach-browser-d14b4d0b.tar.gzatlas at this save
files changed4 files
src/background/chirp-sync-runner.test.ts+8 −4src/background/chirp-sync-runner.ts+6 −6src/background/sync-trigger.test.ts+6 −6src/background/sync-trigger.ts+7 −4src/background/chirp-sync-runner.test.ts+8 −4
@@ -314,8 +314,11 @@314314 peerEntriesB64: { "id-1": "AAAA", "id-2": "BBBB", "id-3": "CCCC" },315315 });316316 const result = await runPromise;317- expect(result.entriesReceived).toBe(3);318- expect(result.entriesSent).toBe(0);317+ expect(result.ok).toBe(true);318+ if (result.ok) {319+ expect(result.entriesReceived).toBe(3);320+ expect(result.entriesSent).toBe(0);321+ }319322 });320323 321324 it("surfaces entriesSent from the adapter's encrypted-entry response", async () => {@@ -339,7 +342,8 @@339342 const runPromise = runWithDeps(makeDevice(), deps);340343 await runPeer(peer, { wantFromUsIds: ["a", "b"] });341344 const result = await runPromise;342- expect(result.entriesSent).toBe(2);345+ expect(result.ok).toBe(true);346+ if (result.ok) expect(result.entriesSent).toBe(2);343347 });344348 345349 it("translates a vault_id_mismatch coordinator outcome into an error throw", async () => {@@ -394,7 +398,7 @@394398 longTermPublicKeyHex: "a".repeat(64),395399 pairedAt: 1700000000000,396400 });397- trigger.setSyncRunner(async () => ({ entriesReceived: 1, entriesSent: 0 }));401+ trigger.setSyncRunner(async () => ({ ok: true, entriesReceived: 1, entriesSent: 0 }));398402 await trigger.triggerSyncIfReachable({ reason: "manual" });399403 expect(typeof local.peach_last_sync_at).toBe("number");400404 expect(local.peach_last_sync_at).toBeGreaterThan(0);src/background/chirp-sync-runner.ts+6 −6
@@ -19,7 +19,7 @@1919 // into the throw-or-return contract the dispatcher expects.2020 2121 import { runChirpSyncInitiator, type ChirpSyncVaultAdapter } from "./chirp-sync-coordinator";22-import { createChirpSyncVaultAdapter, VaultLockedError } from "./chirp-sync-vault-adapter";22+import { createChirpSyncVaultAdapter } from "./chirp-sync-vault-adapter";2323 import {2424 getLastKnownIp,2525 setLastKnownIp,@@ -81,7 +81,7 @@8181 device: PairedDevice,8282 deps: SyncRunnerDeps,8383 ): Promise<SyncRunResult> {84- if (!isUnlocked()) throw new VaultLockedError();84+ if (!isUnlocked()) return { ok: false, error: "VAULT_LOCKED" };8585 if (!device.longTermPublicKeyHex) {8686 throw new Error("paired_device_missing_pubkey");8787 }@@ -89,7 +89,7 @@8989 const port = device.wsPort ?? DEFAULT_PORT;9090 9191 const masterKeyHex = getDerivedKeyHex();92- if (!masterKeyHex) throw new VaultLockedError();92+ if (!masterKeyHex) return { ok: false, error: "VAULT_LOCKED" };9393 const masterKey = hexToBytesLocal(masterKeyHex);9494 const ourDeviceId = await getOrCreateDeviceId();9595 const ourKeypair = deriveSyncIdentityKeypair(masterKey, ourDeviceId);@@ -104,6 +104,7 @@104104 const outcome = await runChirpSyncInitiator(session, adapter);105105 if (outcome.status === "completed") {106106 return {107+ ok: true,107108 entriesReceived: outcome.entriesReceived,108109 entriesSent: outcome.entriesSent,109110 };@@ -116,9 +117,8 @@116117 }117118 throw new Error(outcome.error ?? "sync_errored");118119 } finally {119- // The coordinator already closes the session inside its own `finally`;120- // calling close again is harmless because `ChirpSyncSession.close()` is121- // idempotent (ws.close() is a no-op once closed).120+ // The coordinator does NOT close the session (it returns and leaves the121+ // session open). This runner is the sole owner of session lifetime.122122 await session.close().catch(() => undefined);123123 }124124 }src/background/sync-trigger.test.ts+6 −6
@@ -71,7 +71,7 @@7171 7272 it("does not call the runner when no mobile hosts are paired", async () => {7373 await upsertPairedDevice(EXT);74- const runner = vi.fn(async () => ({ entriesReceived: 0, entriesSent: 0 }));74+ const runner = vi.fn(async () => ({ ok: true as const, entriesReceived: 0, entriesSent: 0 }));7575 setSyncRunner(runner);7676 await triggerSyncIfReachable({ reason: "manual" });7777 expect(runner).not.toHaveBeenCalled();@@ -84,7 +84,7 @@8484 const seen: string[] = [];8585 setSyncRunner(async (device) => {8686 seen.push(device.deviceId);87- return { entriesReceived: 1, entriesSent: 0 };87+ return { ok: true, entriesReceived: 1, entriesSent: 0 };8888 });8989 await triggerSyncIfReachable({ reason: "manual" });9090 expect(seen.sort()).toEqual([ANDROID.deviceId, IOS.deviceId]);@@ -93,7 +93,7 @@9393 it("marks lastSyncedAt on each host that completes successfully", async () => {9494 await upsertPairedDevice(ANDROID);9595 await upsertPairedDevice(IOS);96- setSyncRunner(async () => ({ entriesReceived: 0, entriesSent: 0 }));96+ setSyncRunner(async () => ({ ok: true, entriesReceived: 0, entriesSent: 0 }));9797 await triggerSyncIfReachable({ reason: "manual" });9898 const a = await findPairedDeviceById(ANDROID.deviceId);9999 const b = await findPairedDeviceById(IOS.deviceId);@@ -105,7 +105,7 @@105105 await upsertPairedDevice(ANDROID);106106 const runner = vi.fn(async () => {107107 await new Promise<void>((r) => setTimeout(r, 50));108- return { entriesReceived: 0, entriesSent: 0 };108+ return { ok: true as const, entriesReceived: 0, entriesSent: 0 };109109 });110110 setSyncRunner(runner);111111 const a = triggerSyncIfReachable({ reason: "vault_change" });@@ -118,7 +118,7 @@118118 await upsertPairedDevice(ANDROID);119119 const runner = vi.fn(async () => {120120 await new Promise<void>((r) => setTimeout(r, 30));121- return { entriesReceived: 0, entriesSent: 0 };121+ return { ok: true as const, entriesReceived: 0, entriesSent: 0 };122122 });123123 setSyncRunner(runner);124124 const a = triggerSyncIfReachable({ reason: "vault_change" });@@ -140,7 +140,7 @@140140 it("returns to idle state on success with aggregated entry counts", async () => {141141 await upsertPairedDevice(ANDROID);142142 await upsertPairedDevice(IOS);143- setSyncRunner(async () => ({ entriesReceived: 3, entriesSent: 2 }));143+ setSyncRunner(async () => ({ ok: true, entriesReceived: 3, entriesSent: 2 }));144144 await triggerSyncIfReachable({ reason: "manual" });145145 const s = getSyncState();146146 expect(s.status).toBe("idle");src/background/sync-trigger.ts+7 −4
@@ -32,10 +32,9 @@3232 error?: string;3333 }3434 35-export interface SyncRunResult {36- entriesReceived: number;37- entriesSent: number;38-}35+export type SyncRunResult =36+ | { ok: true; entriesReceived: number; entriesSent: number }37+ | { ok: false; error: string };3938 4039 export type RunSyncWithDevice = (device: PairedDevice) => Promise<SyncRunResult>;4140 @@ -109,6 +108,10 @@109108 for (const host of hosts) {110109 try {111110 const result = await runner(host);111+ if (!result.ok) {112+ if (firstError === undefined) firstError = result.error;113+ continue;114+ }112115 entriesReceived += result.entriesReceived;113116 entriesSent += result.entriesSent;114117 await markLastSyncedAt(host.deviceId, Date.now());