From 5b9736f88e0a2ac7f82561b48479922f476c361a Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 29 Sep 2026 21:18:33 +0100 Subject: [PATCH] RObust client saving --- .../src/persistence/client-metadata-store.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 frontend/sync-client/src/persistence/client-metadata-store.ts diff --git a/frontend/sync-client/src/persistence/client-metadata-store.ts b/frontend/sync-client/src/persistence/client-metadata-store.ts new file mode 100644 index 00000000..43daf227 --- /dev/null +++ b/frontend/sync-client/src/persistence/client-metadata-store.ts @@ -0,0 +1,55 @@ +import type { StoredClient } from "../sync-client"; +import type { StoredDatabase } from "./database"; +import type { MetadataPersistenceProvider } from "./metadata-persistence-provider"; +import { Lock } from "../utils/data-structures/locks"; + +// Serialize partial updates to the shared settings, database and notification store +export class ClientMetadataStore { + private readonly lock = new Lock(); + private reloadBeforeSave = false; + + private constructor( + public stored: StoredClient, + private readonly persistence: MetadataPersistenceProvider + ) { } + + public static async load( + persistence: MetadataPersistenceProvider + ): Promise { + return new ClientMetadataStore( + (await persistence.load()) ?? {}, + persistence + ); + } + + public async save(update: StoredClient): Promise { + await this.lock.withLock(async () => { + if (this.reloadBeforeSave) { + await this.reload(); + } + + const next = { ...this.stored, ...structuredClone(update) }; + try { + await this.persistence.save(next); + this.stored = next; + } catch (error) { + // A failed save may have committed. Reload before another update + // so settings or notifications cannot roll the database back. + this.reloadBeforeSave = true; + throw error; + } + }); + } + + public async reloadDatabase(): Promise { + return this.lock.withLock(async () => { + await this.reload(); + return this.stored.database; + }); + } + + private async reload(): Promise { + this.stored = (await this.persistence.load()) ?? {}; + this.reloadBeforeSave = false; + } +}