58 lines
4.2 KiB
TypeScript
58 lines
4.2 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路径会展示错误。 */
|
||
// 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(); } };
|
||
}
|