From 00e195c1ba2648094fab8a48035cb565c67879cf Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Mon, 1 Jun 2026 20:30:34 +0200 Subject: [PATCH] readLastSyncMap --- frontend/src/lib/userDraftDbSyncer.svelte.ts | 94 +++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/userDraftDbSyncer.svelte.ts b/frontend/src/lib/userDraftDbSyncer.svelte.ts index 33610fef21..7b391fb455 100644 --- a/frontend/src/lib/userDraftDbSyncer.svelte.ts +++ b/frontend/src/lib/userDraftDbSyncer.svelte.ts @@ -1,5 +1,70 @@ import { DraftService, type UserDraftItemKind } from './gen' +/** + * Per-draft last-sync map persisted under a single localStorage key. Lets + * the next `save_draft` (or a downstream UI) know the server's clock at + * our most recent successful sync for `(workspace, itemKind, path)` — + * without doing a network round-trip first. + * + * Shape: `Record`. The map is + * intentionally one storage slot for the whole tab; reading is one + * `localStorage.getItem` + `JSON.parse`, not one per entry. + */ +const DRAFT_LAST_SYNC_KEY = 'userdraft/draftLastSync' + +type DraftLastSyncEntry = { lastSync: string } +type DraftLastSyncMap = Record + +/** Must match `mapKey` in `userDraft.svelte.ts` so the two files agree + * on what identifies a draft. */ +function draftKey(workspace: string, itemKind: UserDraftItemKind, path: string): string { + return `${workspace}/${itemKind}/${path}` +} + +function readLastSyncMap(): DraftLastSyncMap { + try { + const raw = localStorage.getItem(DRAFT_LAST_SYNC_KEY) + if (!raw) return {} + const parsed = JSON.parse(raw) + // Defensive: a corrupt slot (wrong type, array, null) shouldn't + // crash the syncer — reset to an empty map. + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + return parsed as DraftLastSyncMap + } + return {} + } catch (e) { + console.error('UserDraftDbSyncer: draftLastSync read failed', e) + return {} + } +} + +function writeLastSyncMap(map: DraftLastSyncMap): void { + try { + localStorage.setItem(DRAFT_LAST_SYNC_KEY, JSON.stringify(map)) + } catch (e) { + console.error('UserDraftDbSyncer: draftLastSync write failed', e) + } +} + +function setLastSync( + workspace: string, + itemKind: UserDraftItemKind, + path: string, + lastSync: string +): void { + // Read-modify-write is fine: each step is synchronous, so two saves + // for different keys can't interleave their updates within a tab. + const map = readLastSyncMap() + map[draftKey(workspace, itemKind, path)] = { lastSync } + writeLastSyncMap(map) +} + +function clearLastSync(workspace: string, itemKind: UserDraftItemKind, path: string): void { + const map = readLastSyncMap() + delete map[draftKey(workspace, itemKind, path)] + writeLastSyncMap(map) +} + export type UserDraftDbSyncerSaveOpts = { workspace: string itemKind: UserDraftItemKind @@ -9,6 +74,12 @@ export type UserDraftDbSyncerSaveOpts = { value: unknown | null } +export type UserDraftLastSyncQuery = { + workspace: string + itemKind: UserDraftItemKind + path: string +} + /** * Server-side persistence for `UserDraft`. `UserDraft` owns the localStorage * cache; this module forwards each write through to `POST /drafts/save_draft` @@ -21,12 +92,23 @@ export type UserDraftDbSyncerSaveOpts = { * * NOTE: every save currently uses `force: true`, so the server copy is * unconditionally overwritten. This is intentional for the first cut — - * we'll thread `last_sync` through once the client side is settled. + * we'll thread `last_sync` through (using `getLastSync` below) once the + * client side is settled. */ export const UserDraftDbSyncer = { + /** + * Read the recorded last-sync timestamp (ISO string) for a draft. + * Returns `undefined` when we've never successfully synced this + * `(workspace, itemKind, path)` — a fresh save should be treated as + * a first-time push. + */ + getLastSync(opts: UserDraftLastSyncQuery): string | undefined { + return readLastSyncMap()[draftKey(opts.workspace, opts.itemKind, opts.path)]?.lastSync + }, + async save(opts: UserDraftDbSyncerSaveOpts): Promise { try { - await DraftService.saveDraft({ + const resp = await DraftService.saveDraft({ workspace: opts.workspace, kind: opts.itemKind, path: opts.path, @@ -35,6 +117,14 @@ export const UserDraftDbSyncer = { force: true } }) + // On a successful delete, drop the recorded last-sync so the + // next save starts fresh. On an upsert, remember the server's + // timestamp as our baseline for future conflict checks. + if (opts.value === null) { + clearLastSync(opts.workspace, opts.itemKind, opts.path) + } else { + setLastSync(opts.workspace, opts.itemKind, opts.path, resp.current_timestamp) + } } catch (e) { console.error('UserDraftDbSyncer.save failed', e) }