/** * Draft Save Worker * * 在 Web Worker 中执行草稿序列化和指纹计算,避免阻塞主线程。 * 主线程通过 postMessage 传入 canvas 快照,Worker 返回序列化后的 payload。 * * 放置在 public/ 下以从同源加载,避免 assetPrefix (CDN) 导致的跨域 Worker 加载失败。 */ let lastFingerprint = ""; /** Canonical source: src/views/canvas/workers/draftSaveUtils.ts — keep in sync */ function djb2(str) { let hash = 5381; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0; } return hash >>> 0; } /** * 计算草稿指纹:O(n log n) 排序保证一致性 */ function computeFingerprint(nodes, edges, viewport) { const nodeFingerprints = nodes .map( (n) => `${n.id}:${n.position?.x ?? 0},${n.position?.y ?? 0}:${typeof n.data === "object" ? djb2(JSON.stringify(n.data)) : 0}`, ) .sort() .join("|"); const edgeFingerprints = edges .map((e) => `${e.id}:${e.source}-${e.target}`) .sort() .join("|"); const vp = `V${Math.round(viewport.position.x)},${Math.round(viewport.position.y)},${viewport.zoom.toFixed(2)}`; return `N${nodes.length}E${edges.length}:${vp}:${nodeFingerprints}::${edgeFingerprints}`; } /** * 清理 blob:/data: URL,避免序列化无效引用 */ function sanitizeDraftJson(draftJson) { try { const data = JSON.parse(draftJson); return JSON.stringify(data, (_key, value) => { if ( typeof value === "string" && (value.startsWith("blob:") || value.startsWith("data:")) ) { return ""; } return value; }); } catch { return draftJson; } } self.onmessage = (e) => { try { const { nodes, edges, viewport, projectUuid, syncVersion } = e.data; const fingerprint = computeFingerprint(nodes, edges, viewport); if (fingerprint === lastFingerprint) { self.postMessage({ type: "result", fingerprint, payload: null }); return; } lastFingerprint = fingerprint; const draftData = { nodes, edges, savedAt: Date.now(), version: syncVersion, }; const draftJson = sanitizeDraftJson(JSON.stringify(draftData)); self.postMessage({ type: "result", fingerprint, payload: { projectUuid, draftJson, viewportX: viewport.position.x.toString(), viewportY: viewport.position.y.toString(), viewportZoom: viewport.zoom.toString(), }, }); } catch (err) { self.postMessage({ type: "error", message: err instanceof Error ? err.message : String(err), }); } };