From 549c0926a1ff56d4af0f6ec43166b5205f87f8b4 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Sun, 31 May 2026 09:10:08 +0200 Subject: [PATCH] pushDrafts --- frontend/src/lib/userDraftDbSyncer.svelte.ts | 69 ++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/frontend/src/lib/userDraftDbSyncer.svelte.ts b/frontend/src/lib/userDraftDbSyncer.svelte.ts index 2d3043c07d..549305b369 100644 --- a/frontend/src/lib/userDraftDbSyncer.svelte.ts +++ b/frontend/src/lib/userDraftDbSyncer.svelte.ts @@ -92,6 +92,75 @@ export async function syncDrafts(opts: SyncOptions): Promise() + +/** + * Merge two `SyncOptions` into one. Drafts are keyed by `(itemKind, path)` + * with later wins, so a sequence like push([X₁]), push([X₂, Y₂]), + * push([Y₃]) ends up syncing [X₂, Y₃] — keys only in `prev` survive even + * when `next` doesn't repeat them. Callbacks fall back to `prev` when + * `next` doesn't provide one, so a caller that doesn't pass callbacks + * never silently disarms an earlier caller that did. + */ +function mergeSyncOptions(prev: SyncOptions, next: SyncOptions): SyncOptions { + const merged = new Map() + for (const d of prev.drafts) merged.set(`${d.itemKind}|${d.path}`, d) + for (const d of next.drafts) merged.set(`${d.itemKind}|${d.path}`, d) + return { + workspace: next.workspace, + drafts: [...merged.values()], + onMissedDrafts: next.onMissedDrafts ?? prev.onMissedDrafts, + onDraftsRejected: next.onDraftsRejected ?? prev.onDraftsRejected + } +} + +/** + * Enqueue a push. At most one `syncDrafts` is in flight per workspace; any + * pushes that arrive during a flight are merged via `mergeSyncOptions` and + * sent as a single follow-up request when the in-flight call resolves. + * + * If `syncDrafts` throws while newer work is already queued, the error is + * dropped — the next request supersedes it. Otherwise the error propagates + * out of the leader's `pushDrafts` call. + */ +async function pushDrafts(opts: SyncOptions): Promise { + let state = workspaceStates.get(opts.workspace) + if (!state) { + state = { isFlushing: false, pendingPushReq: undefined } + workspaceStates.set(opts.workspace, state) + } + state.pendingPushReq = + state.pendingPushReq === undefined + ? (opts as SyncOptions) + : mergeSyncOptions(state.pendingPushReq, opts as SyncOptions) + if (state.isFlushing) return + state.isFlushing = true + try { + while (state.pendingPushReq !== undefined) { + const next = state.pendingPushReq + state.pendingPushReq = undefined + try { + await syncDrafts(next) + } catch (e) { + if (state.pendingPushReq === undefined) throw e + // Else: newer pushes arrived during the failed sync — drop + // the error and let the loop send the merged follow-up. + } + } + } finally { + state.isFlushing = false + } +} + export const UserDraftDbSyncer = { getLastSync, + pushDrafts }