Spaces:
Sleeping
Sleeping
| 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<string, ReturnType<typeof setTimeout>>(); | |
| const lastSaveTimestamp = new Map<string, number>(); | |
| /** @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<void> | null = null; | |
| export async function ensurePublishedRestored(token?: string): Promise<void> { | |
| 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; | |
| } | |