54 lines
3.7 KiB
TypeScript
54 lines
3.7 KiB
TypeScript
/**
|
|
* 旧版单画布的自动保存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<SyncState>({ 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(); } };
|
|
}
|