docs(canvas): explain legacy persistence and upload cleanup
This commit is contained in:
@@ -21,10 +21,12 @@ export function useCanvasPersistence(document: CanvasDocument, workspace: string
|
||||
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. */ }
|
||||
}
|
||||
@@ -35,6 +37,7 @@ export function useCanvasPersistence(document: CanvasDocument, workspace: string
|
||||
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]);
|
||||
@@ -48,6 +51,7 @@ export function useCanvasPersistence(document: CanvasDocument, workspace: string
|
||||
// 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(); } };
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@ export function useImageUploads(workspace: string, commit: (draft: ImageUploadDr
|
||||
if (job.status === "uploading") return;
|
||||
job.status = "uploading"; job.error = undefined; job.controller = new AbortController(); changed();
|
||||
try {
|
||||
// Promise.all 不会因 ready 拒绝自动中止上传;失败 UI 可先出现,而底层请求仍可能完成。
|
||||
const [asset] = await Promise.all([uploadCanvasImage(api, workspace, job.file, job.file.name, job.controller.signal, job.key), job.ready || Promise.resolve()]);
|
||||
// 结果返回后核对挂载、Map内仍是同一个job且未取消,旧任务不能写到后来同ID的新任务。
|
||||
if (!mounted.current || pending.current.get(job.node.id) !== job || job.controller.signal.aborted) return;
|
||||
// commit 是同步调用,不等待网络 ACK;抛错不会自动撤销它此前已发布的本地修改。
|
||||
job.asset = asset; onCommit.current(job, asset); job.status = "uploaded"; changed();
|
||||
} catch (error) { if (mounted.current && pending.current.get(job.node.id) === job && !job.controller.signal.aborted) { job.status = "failed"; job.error = errorText(error); changed(); } }
|
||||
}
|
||||
@@ -42,11 +44,13 @@ export function useImageUploads(workspace: string, commit: (draft: ImageUploadDr
|
||||
let value: ImageUploadDraft;
|
||||
try { value = draft(localURL); } catch (error) { URL.revokeObjectURL(localURL); throw error; }
|
||||
const job: ImageUploadJob = { ...value, file, localURL, key: crypto.randomUUID(), status: "failed", controller: new AbortController() };
|
||||
// 节点 ID 必须由草稿工厂保证唯一;同 ID set 会替换旧任务,但此处不会 abort/revoke 被替换的任务。
|
||||
pending.current.set(job.node.id, job); changed();
|
||||
// Browser decoding runs locally, concurrently with the original-byte upload.
|
||||
// 纯图片并行解码自然尺寸,限制初始长宽不超过480(不主动放大),再分别设置至少24px的显示尺寸。
|
||||
if (job.node.data.kind === "plain-image") {
|
||||
job.ready = new Promise<void>((resolve,reject) => {
|
||||
// 这个 Image 解码没有接入 controller 的取消,也没有显式移除 onload/onerror;回调以 Map 身份拒绝已移除任务。
|
||||
const image = new window.Image(); image.onload = () => {
|
||||
if (pending.current.get(job.node.id) !== job) { resolve(); return; }
|
||||
// 分别钳制到最小24可能改变极端长宽比;这里只初始化节点和style尺寸,不重编码上传文件。
|
||||
@@ -60,6 +64,7 @@ export function useImageUploads(workspace: string, commit: (draft: ImageUploadDr
|
||||
void run(job);
|
||||
return job;
|
||||
}
|
||||
// 每次重试仍调用上传接口,不直接采用 job.asset;幂等依赖相同 key,ready 失败也不会自动回滚服务端资源。
|
||||
// hasPending包含失败但不包含uploaded,active只看uploading;loaded仅在正式图片加载后移除已上传预览。
|
||||
// retry复用job及幂等键;若ready曾拒绝,重试不会在这里重建尺寸解码Promise。
|
||||
return { jobs, hasPending:()=>[...pending.current.values()].some(job=>job.status!=="uploaded"), active: jobs.some((job) => job.status === "uploading"), start, retry: (id: string) => { const job = pending.current.get(id); if (job) void run(job); }, remove, loaded: (id: string) => { if (pending.current.get(id)?.status === "uploaded") remove(id); } };
|
||||
|
||||
Reference in New Issue
Block a user