Fix canvas transfer timeouts and move images out of snapshots
This commit is contained in:
+28
-8
@@ -11,8 +11,25 @@ export function canvasPayload(document: CanvasDocument) {
|
||||
const clean = decodeCanvas(JSON.stringify(document));
|
||||
return { ...clean, nodes: clean.nodes.map(({ id, type, position, width, height, data }) => ({ id, type, position, ...(width !== undefined ? { width, height } : {}), data })) };
|
||||
}
|
||||
export function canvasFingerprint(document: CanvasDocument) {
|
||||
return JSON.stringify(canvasPayload(document), (_key, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value);
|
||||
export function canvasFingerprint(document: CanvasDocument, includeViewport = true) {
|
||||
const { viewport, ...content } = canvasPayload(document);
|
||||
return JSON.stringify(includeViewport ? { ...content, viewport } : content, (_key, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value);
|
||||
}
|
||||
export function canvasChanges(before: CanvasDocument, after: CanvasDocument) {
|
||||
const a = canvasPayload(before), b = canvasPayload(after);
|
||||
const changed = (left: unknown, right: unknown) => JSON.stringify(left) !== JSON.stringify(right);
|
||||
return {
|
||||
viewport: changed(a.viewport, b.viewport),
|
||||
nodes: b.nodes.flatMap((node) => {
|
||||
const prior = a.nodes.find((item) => item.id === node.id);
|
||||
if (!prior) return [{ id: node.id, fields: ["added"] }];
|
||||
const fields = ["position", "width", "height"].filter((key) => changed(prior[key as keyof typeof prior], node[key as keyof typeof node]));
|
||||
for (const key of new Set([...Object.keys(prior.data), ...Object.keys(node.data)])) if (changed((prior.data as Record<string, unknown>)[key], (node.data as Record<string, unknown>)[key])) fields.push("data." + key);
|
||||
return fields.length ? [{ id: node.id, fields }] : [];
|
||||
}),
|
||||
removed_nodes: a.nodes.filter((node) => !b.nodes.some((item) => item.id === node.id)).map((node) => node.id),
|
||||
edges: changed(a.edges, b.edges),
|
||||
};
|
||||
}
|
||||
export function decodeSnapshot(value: CanvasSnapshot): CanvasSnapshot {
|
||||
if (!Number.isSafeInteger(value?.version) || value.version < 1) throw new Error("invalid_canvas_snapshot");
|
||||
@@ -23,6 +40,7 @@ export function decodeSnapshot(value: CanvasSnapshot): CanvasSnapshot {
|
||||
export class CanvasSync {
|
||||
version: number;
|
||||
saved: string;
|
||||
private savedContent: string;
|
||||
error = "";
|
||||
document: CanvasDocument;
|
||||
private pending: CanvasDocument | null = null;
|
||||
@@ -32,9 +50,10 @@ export class CanvasSync {
|
||||
private notify: (state: SyncState) => void;
|
||||
constructor(request: CanvasRequest, path: string, initial: CanvasSnapshot, notify: (state: SyncState) => void) {
|
||||
this.request = request; this.path = path; this.version = initial.version;
|
||||
this.saved = canvasFingerprint(initial.document); this.notify = notify;
|
||||
this.saved = canvasFingerprint(initial.document); this.savedContent = canvasFingerprint(initial.document, false); this.notify = notify;
|
||||
this.document = initial.document;
|
||||
}
|
||||
needsSave(document: CanvasDocument) { return canvasFingerprint(document, false) !== this.savedContent; }
|
||||
enqueue(document: CanvasDocument): Promise<void> {
|
||||
this.pending = document;
|
||||
if (this.error) return Promise.resolve();
|
||||
@@ -46,7 +65,7 @@ export class CanvasSync {
|
||||
adopt(snapshot: CanvasSnapshot) {
|
||||
if (this.running) throw new Error("canvas_busy");
|
||||
this.pending = null; this.error = ""; this.version = snapshot.version; this.document = snapshot.document;
|
||||
this.saved = canvasFingerprint(snapshot.document); this.notify({ status: "saved", version: this.version });
|
||||
this.saved = canvasFingerprint(snapshot.document); this.savedContent = canvasFingerprint(snapshot.document, false); this.notify({ status: "saved", version: this.version });
|
||||
}
|
||||
private async drain() {
|
||||
while (this.pending) {
|
||||
@@ -54,12 +73,13 @@ export class CanvasSync {
|
||||
this.notify({ status: "saving", version: this.version });
|
||||
try {
|
||||
const fingerprint = canvasFingerprint(document);
|
||||
if (fingerprint === this.saved) { this.notify({ status: "saved", version: this.version }); continue; }
|
||||
if (!this.needsSave(document)) { this.notify({ status: "saved", version: this.version }); continue; }
|
||||
if (process.env.NODE_ENV === "development") console.info("[canvas-dirty]", JSON.stringify({ resource: this.path, ...canvasChanges(this.document, document) }));
|
||||
const body = JSON.stringify({ document: canvasPayload(document) });
|
||||
if (new TextEncoder().encode(body).length > 5 * 1024 * 1024) throw new Error("canvas_too_large");
|
||||
const result = await this.request<{ version: number }>(this.path, { method: "PATCH", headers: { Accept: CANVAS_ACCEPT, "If-Match": `"${this.version}"` }, body });
|
||||
if (!Number.isSafeInteger(result?.version) || result.version !== this.version + 1) throw new Error("invalid_canvas_snapshot");
|
||||
this.version = result.version; this.saved = fingerprint;
|
||||
this.version = result.version; this.saved = fingerprint; this.savedContent = canvasFingerprint(document, false);
|
||||
this.document = document;
|
||||
this.notify({ status: "saved", version: this.version });
|
||||
} catch (error) {
|
||||
@@ -77,13 +97,13 @@ export function readLocalRecovery(storage: Pick<Storage, "getItem">, key: string
|
||||
if (record) {
|
||||
const value = JSON.parse(record);
|
||||
const doc = decodeCanvas(JSON.stringify(value.document));
|
||||
if (!snapshot || (value.dirty && canvasFingerprint(doc) !== canvasFingerprint(snapshot.document))) return doc;
|
||||
if (!snapshot || (value.dirty && canvasFingerprint(doc, false) !== canvasFingerprint(snapshot.document, false))) return doc;
|
||||
return null;
|
||||
}
|
||||
const legacy = storage.getItem(key);
|
||||
if (!legacy) return null;
|
||||
const doc = decodeCanvas(legacy);
|
||||
return !snapshot || canvasFingerprint(doc) !== canvasFingerprint(snapshot.document) ? doc : null;
|
||||
return !snapshot || canvasFingerprint(doc, false) !== canvasFingerprint(snapshot.document, false) ? doc : null;
|
||||
}
|
||||
export function canRestoreCanvasDraft(draft: CanvasDocument, remote: CanvasDocument) {
|
||||
if (draft.schema_version < remote.schema_version) return false;
|
||||
|
||||
Reference in New Issue
Block a user