Files
Endoleap/src/components/canvas/use-canvas-persistence.ts
T

58 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 旧版单画布的自动保存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路径会展示错误。 */
// writer 通知捕获创建时的 backup/key;latest 与 pausedRef 保持当前值,key/workspace/board 改变须重建 Hook 所在视图。
function backup() {
if (pausedRef.current) return;
try {
const current = latest.current;
// dirty 使用包含 viewport 的指纹,而 writer.needsSave 忽略 viewport;两者用途不同,不可互换解释。
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);
// 此处只入队,不 await/捕获 running 的拒绝;CanvasSync 的通知与回调需遵守它自身的异常约定。
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]);
// 同步 localStorage 写入只保证调用返回时 API 未抛错;没有回读、版本日志或刷盘确认。
// retry先补本地备份再委托writer重试;没有远端writer时返回undefined,不伪造同步成功。
return { ...sync, retry: () => { backup(); return writer?.retry(); } };
}