import { useSettingsStore } from "@/stores/settings-store"; import type { OpenFile } from "@/hooks/editor-api"; import { serializeDocument } from "@/lib/frontmatter"; import * as tauri from "@/lib/tauri"; const THROTTLE_MS = 1000; /** * The slice of editor-store behavior the save engine needs. The store injects * it via `registerSaveStore` at startup so this `lib/` module never imports * `stores/editor-store` — that back-edge would invert the stores → lib layering * and form an import cycle (the store already imports the save engine). */ export interface SaveStoreAccess { getOpenFile: (path: string) => OpenFile | undefined; markSaved: (path: string, diskContent: string, hasNewerChanges: boolean) => void; setSaveError: (path: string, error: string) => void; } let storeAccess: SaveStoreAccess | null = null; /** Wire the save engine to the editor store. Called once from editor-store at * module init. */ export function registerSaveStore(access: SaveStoreAccess) { storeAccess = access; } function requireStore(): SaveStoreAccess { if (!storeAccess) { throw new Error("[save] used before registerSaveStore() — editor store not wired"); } return storeAccess; } interface SaveController { lastSaveTime: number; timer: ReturnType | null; inFlight: boolean; pending: boolean; } const saveControllers = new Map(); function getSaveController(path: string): SaveController { let controller = saveControllers.get(path); if (!controller) { controller = { lastSaveTime: 0, timer: null, inFlight: false, pending: false, }; saveControllers.set(path, controller); } return controller; } function clearSaveTimer(controller: SaveController) { if (!controller.timer) return; clearTimeout(controller.timer); controller.timer = null; } function cleanupSaveController(path: string, controller: SaveController) { if (controller.inFlight || controller.pending || controller.timer) return; saveControllers.delete(path); } export function scheduleSave(path: string) { const controller = getSaveController(path); controller.pending = true; queueSave(path, controller); } export function isSaveInFlight(path: string) { return saveControllers.get(path)?.inFlight === true; } export function cancelSave(path: string) { const controller = saveControllers.get(path); if (!controller) return; controller.pending = false; clearSaveTimer(controller); cleanupSaveController(path, controller); } function queueSave(path: string, controller: SaveController) { if (controller.inFlight) return; clearSaveTimer(controller); const elapsed = Date.now() - controller.lastSaveTime; if (elapsed >= THROTTLE_MS) { void performSave(path, controller); return; } controller.timer = setTimeout(() => { controller.timer = null; void performSave(path, controller); }, THROTTLE_MS - elapsed); } function applyFileProcessing(content: string): string { const settings = useSettingsStore.getState().settings; let result = content; if (settings["files.trim-trailing-whitespace"]) { result = result .split("\n") .map((line) => line.replace(/\s+$/, "")) .join("\n"); } if (settings["files.insert-final-newline"]) { if (!result.endsWith("\n")) { result += "\n"; } } return result; } function serializeForSave(file: OpenFile) { return applyFileProcessing(serializeDocument(file.frontmatter, file.content)); } async function performSave(path: string, controller = getSaveController(path)) { if (controller.inFlight) return; const store = requireStore(); const file = store.getOpenFile(path); if (!file || !file.isDirty) { controller.pending = false; cleanupSaveController(path, controller); return; } controller.inFlight = true; controller.pending = false; controller.lastSaveTime = Date.now(); const full = serializeForSave(file); let shouldReschedule = false; try { await tauri.writeFile(path, full); const latestFile = store.getOpenFile(path); if (!latestFile) return; shouldReschedule = serializeForSave(latestFile) !== full; store.markSaved(path, full, shouldReschedule); } catch (err) { console.error(`[save] Failed to save ${path}:`, err); const message = err instanceof Error ? err.message : String(err); store.setSaveError(path, message); } finally { controller.inFlight = false; const needsFollowUpSave = controller.pending || shouldReschedule; if (needsFollowUpSave) { queueSave(path, controller); } else { cleanupSaveController(path, controller); } } }