diff --git a/frontend/src/lib/userDraftDbSyncer.svelte.ts b/frontend/src/lib/userDraftDbSyncer.svelte.ts index 2bbc084612..2d3043c07d 100644 --- a/frontend/src/lib/userDraftDbSyncer.svelte.ts +++ b/frontend/src/lib/userDraftDbSyncer.svelte.ts @@ -4,18 +4,12 @@ * which doubles as "what the server has for me that I haven't seen yet" and * "push these drafts up, rejecting any whose server copy moved forward * since my last sync". - * - * The syncer is a single shared module-level service (not per-handle) so - * that pushes from any UserDraft.save call across the app coalesce into one - * batched sync request. */ import type { UserDraftItemKind } from './userDraft.svelte' import { DraftService, type SyncDraftsResponse } from './gen' import { useLocalStorageValue } from './svelte5Utils.svelte' const LAST_SYNC_KEY = 'userdraft/lastSync' -const DEBOUNCE_MS = 2_000 -const MAX_DEBOUNCE_MS = 10_000 export type MissedDraft = SyncDraftsResponse['missed_drafts'][number] export type RejectedDraft = Extract @@ -48,21 +42,6 @@ export type SyncOptions = { onDraftsRejected?: RejectedDraftsCallback } -type QueueKey = string -function queueKey(workspace: string, kind: UserDraftItemKind, path: string): QueueKey { - return `${workspace}|${kind}|${path}` -} - -type QueuedEntry = { - workspace: string - itemKind: UserDraftItemKind - path: string - value: unknown - force: boolean - onMissedDrafts?: MissedDraftCallback - onDraftsRejected?: RejectedDraftsCallback -} - // Setter-only callers can use `useLocalStorageValue` at module scope by // disabling the nested-mutation `$effect`. The lastSync slot is a flat // string updated exclusively via `cell.val = ...`, so the effect is @@ -82,63 +61,9 @@ function bumpLastSync(serverTimestamp: string): void { } } -const queue = new Map() -let debounceTimer: ReturnType | undefined -let maxDebounceTimer: ReturnType | undefined - -function clearTimers(): void { - if (debounceTimer !== undefined) { - clearTimeout(debounceTimer) - debounceTimer = undefined - } - if (maxDebounceTimer !== undefined) { - clearTimeout(maxDebounceTimer) - maxDebounceTimer = undefined - } -} - -async function flushQueue(): Promise { - clearTimers() - if (queue.size === 0) return - - const entries = Array.from(queue.values()) - queue.clear() - - // One request per workspace. The server scopes every row by the - // session's authed email, so we don't track user identity on the - // client side — a tab can only be logged in as one user at a time. - const groups = new Map() - for (const entry of entries) { - const list = groups.get(entry.workspace) - if (list) list.push(entry) - else groups.set(entry.workspace, [entry]) - } - - for (const [workspace, group] of groups.entries()) { - await runSync(workspace, group) - } -} - -async function runSync(workspace: string, group: QueuedEntry[]): Promise { - const onMissedDrafts = group.find((e) => e.onMissedDrafts)?.onMissedDrafts - const onDraftsRejected = group.find((e) => e.onDraftsRejected)?.onDraftsRejected - const drafts: PendingDraft[] = group.map(({ itemKind, path, value, force }) => ({ - itemKind, - path, - value, - force - })) - await syncDrafts({ - workspace, - drafts, - onMissedDrafts, - onDraftsRejected - }) -} - /** - * Immediate sync. Bypasses the queue. Caller is responsible for handling - * the missed/rejected lists. + * Immediate sync. Caller is responsible for handling the missed/rejected + * lists via the callbacks. */ export async function syncDrafts(opts: SyncOptions): Promise { const lastSync = getLastSync() @@ -169,54 +94,4 @@ export async function syncDrafts(opts: SyncOptions): Promise(opts: SyncOptions): void { - const ws = opts.workspace - for (const d of opts.drafts) { - queue.set(queueKey(ws, d.itemKind, d.path), { - workspace: ws, - itemKind: d.itemKind, - path: d.path, - value: d.value, - force: d.force ?? false, - onMissedDrafts: opts.onMissedDrafts, - onDraftsRejected: opts.onDraftsRejected - }) - } - - if (debounceTimer !== undefined) clearTimeout(debounceTimer) - debounceTimer = setTimeout(() => { - void flushQueue() - }, DEBOUNCE_MS) - - if (maxDebounceTimer === undefined) { - maxDebounceTimer = setTimeout(() => { - void flushQueue() - }, MAX_DEBOUNCE_MS) - } - }, - - /** - * Force a synchronous flush of any queued pushes. Useful for tests and - * for "logout" / "navigate away" hooks that want to guarantee delivery. - */ - async flush(): Promise { - await flushQueue() - } }