/** * @factory/collaboration — offline persistence wiring (TASK-0092 R5 / * TASK-0095). * * `y-indexeddb` (`IndexeddbPersistence`) is the offline store: it keeps the * `Y.Doc` in IndexedDB under a deterministic name ({@link offlineDocName} — * `factory:{workspaceId}:{pageId}`, stable across tabs so two tabs of the * same page share one offline doc and tab-to-tab offline edits converge) * and loads stored updates into the doc on construction. * * v9 storage semantics (the library's built-in autosave — the task's 1 s * cadence, the architecture doc's win over the brief's 2 s): every update * (local or network) is APPENDED to the `updates` store immediately; when * the store reaches 500 updates the provider merges the doc state into it * after its built-in 1 s debounce (`_storeTimeout` — the private 1 s * autosave the library ships). {@link OfflinePersistence.flush} forces an * immediate merge via the exported `storeState(provider, true)` — the * provider calls it before a hard navigation and on dispose, so no * pending offline edit is ever lost. * * The "offline → online" convergence is the CRDT's, not the store's: on * reconnect the provider sends `sync.request {stateVector}` and the server * answers with only the missing updates (the merge discards duplicates — * realtime-architecture §4.5). * * `createMemoryOfflineFactory` is the Node/test stand-in (no IndexedDB in * Node — recorded; the browser path is the real one). The y-indexeddb * import is side-effect-free to load in Node; only the factory CALL * touches IndexedDB (the browser). */ import { IndexeddbPersistence, storeState } from "y-indexeddb"; import type { OfflinePersistence, OfflinePersistenceFactory } from "./types.js"; /** * The offline doc name: deterministic per (workspace, page) so every tab * of the page shares one IndexedDB doc and offline edits converge * tab-to-tab. (Phase-7 brief: `factory:{workspaceId}:{pageId}`.) */ export function offlineDocName(workspaceId: string, pageId: string): string { return `factory:${workspaceId}:${pageId}`; } /** * The autosave interval passed to the factory contract. The y-indexeddb * v9 merge debounce is a fixed 1 s inside the library (`_storeTimeout`); * this constant documents that contract and is what the task sheet's * "debounced 1 s (architecture doc over the brief's 2 s)" resolves to. */ export const OFFLINE_AUTOSAVE_INTERVAL_MS = 1000; /** * The browser default: y-indexeddb. Construction opens the DB, loads the * stored updates into the doc, and fires `synced` (even when empty) — * `create` awaits `whenSynced` so the provider can compute its * `sync.request` (full vs state-vector) only AFTER local offline state * exists. A 2.5 s guard resolves anyway: a corrupt/locked store must not * block the connect — the full sync covers the gap (recorded). */ export function createIndexeddbOfflineFactory(): OfflinePersistenceFactory { return { async create(docName, doc, _opts) { const provider = new IndexeddbPersistence(docName, doc); await raceTimeout(provider.whenSynced, 2500); return { docName, async flush() { // Force the immediate merge (no 500-update threshold, no debounce). await storeState(provider, true); }, async dispose() { // Flush then close (destroy unbinds the doc's update listener). await storeState(provider, true); await provider.destroy(); }, }; }, }; } /** * A no-op offline persistence for environments without IndexedDB (Node * tests — the provider tests inject this or nothing). The doc name is * honored for logging/consistency; nothing is stored. */ export function createMemoryOfflineFactory(): OfflinePersistenceFactory { return { async create(docName, _doc, _opts) { return { docName, async flush() {}, async dispose() {}, }; }, }; } function raceTimeout(promise: Promise, ms: number): Promise { return new Promise((resolve) => { let settled = false; const timer = setTimeout(() => { if (!settled) { settled = true; resolve(undefined); } }, ms); promise.then( (value) => { if (!settled) { settled = true; clearTimeout(timer); resolve(value); } }, () => { if (!settled) { settled = true; clearTimeout(timer); resolve(undefined); } }, ); }); } /** Re-export for the canvas (the offline store IS y-indexeddb). */ export { IndexeddbPersistence };