import { writeFileSync, existsSync } from "fs"; import { join } from "path"; import * as Y from "yjs"; import { getDataDir, docPath, sanitizeName } from "./utils.js"; import { isHfStorageEnabled, getDatasetId, setUserToken, pullPublishedAssets, schedulePush, recordLocalSave, recordLocalSaveError, } from "./hf-storage.js"; const DEFAULT_DOC_NAME = "default"; const SAVE_DEBOUNCE_MS = 2000; const saveTimers = new Map>(); const lastSaveTimestamp = new Map(); /** @internal - exported for testing */ export function debouncedSave(documentName: string, ydoc: Y.Doc) { const existing = saveTimers.get(documentName); if (existing) clearTimeout(existing); saveTimers.set(documentName, setTimeout(() => { saveTimers.delete(documentName); try { const state = Y.encodeStateAsUpdate(ydoc); const buf = Buffer.from(state); writeFileSync(docPath(documentName), buf); lastSaveTimestamp.set(documentName, Date.now()); recordLocalSave(documentName); console.log(`[persist] saved "${documentName}": ${buf.length} bytes`); if (isHfStorageEnabled()) { schedulePush(documentName, buf); } } catch (err) { // A failed local save is the most dangerous silent failure // because the WS layer already ack'd the edit to the client, // who therefore sees "Saved" while nothing is on disk. Push // the error into the status tracker so the SyncIndicator can // flip to "Local save failed" within a few seconds. recordLocalSaveError(documentName, err); console.error(`[persist] failed to save "${documentName}":`, (err as Error).message); } }, SAVE_DEBOUNCE_MS)); } /** Reset debounce state between tests */ export function resetSaveTimers() { for (const t of saveTimers.values()) clearTimeout(t); saveTimers.clear(); lastSaveTimestamp.clear(); } let _publishedRestored = false; let _restoreInProgress: Promise | null = null; export async function ensurePublishedRestored(token?: string): Promise { const DATA_DIR = getDataDir(); if (_publishedRestored) return; if (!getDatasetId()) return; const publishedPath = join(DATA_DIR, "published", sanitizeName(DEFAULT_DOC_NAME), "index.html"); if (existsSync(publishedPath)) { _publishedRestored = true; return; } if (_restoreInProgress) { await _restoreInProgress; return; } if (token) setUserToken(token); _restoreInProgress = (async () => { try { const found = await pullPublishedAssets(DEFAULT_DOC_NAME, DATA_DIR); if (found) { _publishedRestored = true; console.log("[server] restored published article from HF dataset"); } } catch (err) { console.warn("[server] failed to restore published:", (err as Error).message); } finally { _restoreInProgress = null; } })(); await _restoreInProgress; } /** Reset restore state between tests */ export function resetPublishedRestored() { _publishedRestored = false; _restoreInProgress = null; }