/** * 旧版单画布的自动保存Hook:短延迟备份本地草稿,较长延迟把最新快照交给CanvasSync。 * 它使用旧快照writer,不是新版冻结请求ledger;本地recovery记录也不是可靠保存文件字节的资源仓库。 */ "use client"; import { useEffect, useRef, useState } from "react"; import { api } from "@/lib/api"; import { canvasFingerprint, canvasPath, CanvasSync, type CanvasSnapshot, type SyncState } from "@/lib/canvas-sync"; import { decodeCanvas, type CanvasDocument } from "@/lib/canvas"; /** snapshot存在才建立远端writer,无snapshot仍可本地备份;writer在挂载时初始化一次,调用方应按画布重挂载。 * key区分草稿作用域,paused阻断这里的新备份/入队,不在此取消已交给writer的请求。 */ export function useCanvasPersistence(document: CanvasDocument, workspace: string, board: string, key: string, snapshot: CanvasSnapshot | undefined, paused: boolean) { const [sync, setSync] = useState({ status: "saved", version: snapshot?.version || 1 }); // alive限制writer通知更新已卸载组件;latest和pausedRef让异步回调读取当前文档/暂停值。 const alive = useRef(false); const latest = useRef(document); latest.current = document; const pausedRef = useRef(paused); pausedRef.current = paused; // 同一挂载复用writer;状态变化时刷新sync并补本地备份,不随每次文档渲染新建同步器。 const [writer] = useState(() => snapshot ? new CanvasSync(api, canvasPath(workspace, board), snapshot, (next) => { if (alive.current) { setSync(next); backup(); } }) : null); /** 保存规范化document、writer当前版本及内容指纹dirty;回调路径吞掉存储异常,定时effect路径会展示错误。 */ function backup() { if (pausedRef.current) return; try { const current = latest.current; localStorage.setItem(key + ":recovery", JSON.stringify({ document: decodeCanvas(JSON.stringify(current)), baseVersion: writer?.version || 1, dirty: writer ? canvasFingerprint(current) !== writer.saved : false })); } catch { /* Storage failure is surfaced by the effect below. */ } } // 文档变化重置250ms本地/800ms远端计时器,形成防抖;暂停不建立计时器,清理只取消尚未触发的任务。 useEffect(() => { if (paused) return; const local = setTimeout(() => { try { localStorage.setItem(key + ":recovery", JSON.stringify({ document: decodeCanvas(JSON.stringify(latest.current)), baseVersion: writer?.version || 1, dirty: writer ? canvasFingerprint(latest.current) !== writer.saved : false })); } catch { setSync({ status: "error", version: writer?.version || 1, error: "local_backup_failed" }); } }, 250); const remote = writer ? setTimeout(() => void writer.enqueue(latest.current), 800) : null; return () => { clearTimeout(local); if (remote) clearTimeout(remote); }; }, [document, writer, key, paused]); // pagehide与卸载只执行同步本地backup,不保证远端请求在离页前完成;卸载后不再接收writer的UI通知。 useEffect(() => { alive.current = true; /** 页面隐藏/离开时尽量保存当前内存文档,暂停状态由backup内部再次检查。 */ const flush = () => backup(); window.addEventListener("pagehide", flush); return () => { window.removeEventListener("pagehide", flush); backup(); alive.current = false; }; // Writer and key are stable for this board's mounted editor. // eslint-disable-next-line react-hooks/exhaustive-deps }, [writer, key]); // retry先补本地备份再委托writer重试;没有远端writer时返回undefined,不伪造同步成功。 return { ...sync, retry: () => { backup(); return writer?.retry(); } }; }