@xom11/whiteboard 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/dist/chunk-74VEEZBV.mjs +619 -0
- package/dist/chunk-74VEEZBV.mjs.map +1 -0
- package/dist/chunk-DU2NFHRR.mjs +103 -0
- package/dist/chunk-DU2NFHRR.mjs.map +1 -0
- package/dist/{chunk-SHFOGORM.mjs → chunk-DU3RHKT5.mjs} +4 -4
- package/dist/{chunk-SHFOGORM.mjs.map → chunk-DU3RHKT5.mjs.map} +1 -1
- package/dist/{chunk-HYXFHEDJ.mjs → chunk-IUVV52HO.mjs} +22 -7
- package/dist/chunk-IUVV52HO.mjs.map +1 -0
- package/dist/{chunk-BJX4YNA5.mjs → chunk-KEYZ5EZT.mjs} +26 -9
- package/dist/chunk-KEYZ5EZT.mjs.map +1 -0
- package/dist/{chunk-LPM4MM45.mjs → chunk-SBDMF4NQ.mjs} +3 -2
- package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
- package/dist/{chunk-3SSQKRRO.mjs → chunk-ZVN356JZ.mjs} +4 -4
- package/dist/{chunk-3SSQKRRO.mjs.map → chunk-ZVN356JZ.mjs.map} +1 -1
- package/dist/geometry-2d.js +250 -218
- package/dist/geometry-2d.js.map +1 -1
- package/dist/geometry-2d.mjs +2 -2
- package/dist/geometry-3d.d.mts +1 -1
- package/dist/geometry-3d.d.ts +1 -1
- package/dist/geometry-3d.js +3276 -1201
- package/dist/geometry-3d.js.map +1 -1
- package/dist/geometry-3d.mjs +3 -2
- package/dist/graph-2d.js +360 -66
- package/dist/graph-2d.js.map +1 -1
- package/dist/graph-2d.mjs +2 -2
- package/dist/{host-2QGKMGCT.mjs → host-LZH2FZ2N.mjs} +3 -3
- package/dist/{host-2QGKMGCT.mjs.map → host-LZH2FZ2N.mjs.map} +1 -1
- package/dist/host-PIIDSMVE.mjs +3187 -0
- package/dist/host-PIIDSMVE.mjs.map +1 -0
- package/dist/{host-T2W6R6SO.mjs → host-VDNAJMLC.mjs} +221 -216
- package/dist/host-VDNAJMLC.mjs.map +1 -0
- package/dist/index.d.mts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +4365 -1821
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +246 -102
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-BJX4YNA5.mjs.map +0 -1
- package/dist/chunk-DJTBZEAR.mjs +0 -25
- package/dist/chunk-DJTBZEAR.mjs.map +0 -1
- package/dist/chunk-HM7RIXJE.mjs +0 -331
- package/dist/chunk-HM7RIXJE.mjs.map +0 -1
- package/dist/chunk-HYXFHEDJ.mjs.map +0 -1
- package/dist/chunk-LPM4MM45.mjs.map +0 -1
- package/dist/host-T2W6R6SO.mjs.map +0 -1
- package/dist/host-XUFON6CQ.mjs +0 -1422
- package/dist/host-XUFON6CQ.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import './index.css';
|
|
3
|
-
import { geometryStamp } from './chunk-
|
|
4
|
-
export { geometryStamp } from './chunk-
|
|
5
|
-
import { geometry3dStamp } from './chunk-
|
|
6
|
-
export { geometry3dStamp } from './chunk-
|
|
3
|
+
import { geometryStamp } from './chunk-DU3RHKT5.mjs';
|
|
4
|
+
export { geometryStamp } from './chunk-DU3RHKT5.mjs';
|
|
5
|
+
import { geometry3dStamp } from './chunk-IUVV52HO.mjs';
|
|
6
|
+
export { geometry3dStamp } from './chunk-IUVV52HO.mjs';
|
|
7
7
|
import { latexStamp } from './chunk-7P7SQFOW.mjs';
|
|
8
8
|
export { latexStamp } from './chunk-7P7SQFOW.mjs';
|
|
9
|
-
import { graph2dStamp } from './chunk-
|
|
10
|
-
export { graph2dStamp } from './chunk-
|
|
11
|
-
export { isGraph2DCustomData } from './chunk-
|
|
12
|
-
export { isGeometryCustomData } from './chunk-
|
|
9
|
+
import { graph2dStamp } from './chunk-ZVN356JZ.mjs';
|
|
10
|
+
export { graph2dStamp } from './chunk-ZVN356JZ.mjs';
|
|
11
|
+
export { isGraph2DCustomData } from './chunk-74VEEZBV.mjs';
|
|
12
|
+
export { isGeometryCustomData } from './chunk-KEYZ5EZT.mjs';
|
|
13
13
|
export { isLatexCustomData } from './chunk-X5R72SSJ.mjs';
|
|
14
|
-
export { isGeometry3DCustomData } from './chunk-
|
|
14
|
+
export { isGeometry3DCustomData } from './chunk-DU2NFHRR.mjs';
|
|
15
15
|
import './chunk-HTBLO5JO.mjs';
|
|
16
16
|
import './chunk-C6SCVOMC.mjs';
|
|
17
17
|
import './chunk-BJTO5JO5.mjs';
|
|
@@ -45,7 +45,7 @@ var ALL_STAMPS = Object.freeze([
|
|
|
45
45
|
...STABLE_STAMPS,
|
|
46
46
|
...EXPERIMENTAL_STAMPS
|
|
47
47
|
]);
|
|
48
|
-
var DEFAULT_STAMPS =
|
|
48
|
+
var DEFAULT_STAMPS = ALL_STAMPS;
|
|
49
49
|
function findStampForCustomData(data, stamps = DEFAULT_STAMPS) {
|
|
50
50
|
for (const s of stamps) {
|
|
51
51
|
if (s.matchesCustomData(data)) return s;
|
|
@@ -77,6 +77,7 @@ function ToolbarInjector({
|
|
|
77
77
|
let cancelled = false;
|
|
78
78
|
let observer = null;
|
|
79
79
|
let rafId = null;
|
|
80
|
+
let observedRoot = null;
|
|
80
81
|
const apply = (next) => {
|
|
81
82
|
if (cancelled || menuMountRef.current === next) return;
|
|
82
83
|
menuMountRef.current = next;
|
|
@@ -102,21 +103,38 @@ function ToolbarInjector({
|
|
|
102
103
|
}
|
|
103
104
|
apply(wrapper);
|
|
104
105
|
};
|
|
105
|
-
const
|
|
106
|
+
const attachObserver = () => {
|
|
107
|
+
if (cancelled) return;
|
|
108
|
+
const excalidraw = document.querySelector(".excalidraw");
|
|
109
|
+
const nextRoot = excalidraw ?? document.body;
|
|
110
|
+
if (observedRoot === nextRoot) return;
|
|
111
|
+
observer?.disconnect();
|
|
112
|
+
observedRoot = nextRoot;
|
|
113
|
+
observer = new MutationObserver(onMutation);
|
|
114
|
+
observer.observe(nextRoot, { childList: true, subtree: true });
|
|
115
|
+
};
|
|
116
|
+
const onMutation = () => {
|
|
106
117
|
if (rafId != null) return;
|
|
107
118
|
rafId = requestAnimationFrame(() => {
|
|
108
119
|
rafId = null;
|
|
120
|
+
if (cancelled) return;
|
|
121
|
+
if (observedRoot !== document.querySelector(".excalidraw")) {
|
|
122
|
+
attachObserver();
|
|
123
|
+
}
|
|
109
124
|
findMenu();
|
|
110
125
|
});
|
|
111
126
|
};
|
|
112
127
|
findMenu();
|
|
113
|
-
|
|
114
|
-
observer = new MutationObserver(schedule);
|
|
115
|
-
observer.observe(root, { childList: true, subtree: true });
|
|
128
|
+
attachObserver();
|
|
116
129
|
return () => {
|
|
117
130
|
cancelled = true;
|
|
118
|
-
if (rafId != null)
|
|
131
|
+
if (rafId != null) {
|
|
132
|
+
cancelAnimationFrame(rafId);
|
|
133
|
+
rafId = null;
|
|
134
|
+
}
|
|
119
135
|
observer?.disconnect();
|
|
136
|
+
observer = null;
|
|
137
|
+
observedRoot = null;
|
|
120
138
|
document.getElementById(MENU_WRAPPER_ID)?.remove();
|
|
121
139
|
};
|
|
122
140
|
}, [enabled]);
|
|
@@ -380,6 +398,63 @@ async function restoreMissingStampFiles(api, elements, stamps = DEFAULT_STAMPS)
|
|
|
380
398
|
}
|
|
381
399
|
}
|
|
382
400
|
|
|
401
|
+
// src/core/persistence/validation.ts
|
|
402
|
+
var STORAGE_KEY_RE = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
403
|
+
function validateStorageKey(key) {
|
|
404
|
+
if (typeof key !== "string" || !STORAGE_KEY_RE.test(key)) {
|
|
405
|
+
const sample = key === void 0 ? "undefined" : String(key).slice(0, 32);
|
|
406
|
+
throw new Error(
|
|
407
|
+
`[whiteboard] Invalid storageKey: must match ${STORAGE_KEY_RE} (got: ${sample})`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return key;
|
|
411
|
+
}
|
|
412
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
413
|
+
function sanitizingReviver(_key, value) {
|
|
414
|
+
if (DANGEROUS_KEYS.has(_key)) return void 0;
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
var MAX_NESTED_DEPTH = 64;
|
|
418
|
+
function depthExceeds(v, max, depth = 0) {
|
|
419
|
+
if (depth > max) return true;
|
|
420
|
+
if (v === null || typeof v !== "object") return false;
|
|
421
|
+
const children = Array.isArray(v) ? v : Object.values(v);
|
|
422
|
+
for (const child of children) {
|
|
423
|
+
if (depthExceeds(child, max, depth + 1)) return true;
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "elements", "appState", "savedAt"]);
|
|
428
|
+
function isPlainObject(v) {
|
|
429
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
430
|
+
}
|
|
431
|
+
function safeParseScene(raw) {
|
|
432
|
+
let parsed;
|
|
433
|
+
try {
|
|
434
|
+
parsed = JSON.parse(raw, sanitizingReviver);
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
if (!isPlainObject(parsed)) return null;
|
|
439
|
+
if (depthExceeds(parsed, MAX_NESTED_DEPTH)) return null;
|
|
440
|
+
const safe = {};
|
|
441
|
+
for (const k of Object.keys(parsed)) {
|
|
442
|
+
if (ALLOWED_TOP_LEVEL_KEYS.has(k)) safe[k] = parsed[k];
|
|
443
|
+
}
|
|
444
|
+
if (!Array.isArray(safe.elements)) return null;
|
|
445
|
+
for (const el of safe.elements) {
|
|
446
|
+
if (!isPlainObject(el)) return null;
|
|
447
|
+
if (typeof el.id !== "string" || typeof el.type !== "string") return null;
|
|
448
|
+
}
|
|
449
|
+
const appState = isPlainObject(safe.appState) ? safe.appState : {};
|
|
450
|
+
return {
|
|
451
|
+
version: safe.version,
|
|
452
|
+
elements: safe.elements,
|
|
453
|
+
appState,
|
|
454
|
+
savedAt: safe.savedAt
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
383
458
|
// src/core/persistence/sceneStore.ts
|
|
384
459
|
var PREFIX = "whiteboard:scene:";
|
|
385
460
|
var SCHEMA_VERSION = 1;
|
|
@@ -387,35 +462,34 @@ function fullKey(key) {
|
|
|
387
462
|
return PREFIX + key;
|
|
388
463
|
}
|
|
389
464
|
function readScene(key) {
|
|
465
|
+
const validKey = validateStorageKey(key);
|
|
390
466
|
if (typeof window === "undefined") return null;
|
|
391
|
-
const raw = window.localStorage.getItem(fullKey(
|
|
467
|
+
const raw = window.localStorage.getItem(fullKey(validKey));
|
|
392
468
|
if (!raw) return null;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (parsed.version !== SCHEMA_VERSION) {
|
|
397
|
-
console.warn(
|
|
398
|
-
`[whiteboard] scene version ${parsed.version} kh\xF4ng kh\u1EDBp ${SCHEMA_VERSION}, b\u1ECF qua.`
|
|
399
|
-
);
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
if (!Array.isArray(parsed.elements)) return null;
|
|
403
|
-
return {
|
|
404
|
-
version: SCHEMA_VERSION,
|
|
405
|
-
elements: parsed.elements,
|
|
406
|
-
appState: parsed.appState ?? {},
|
|
407
|
-
savedAt: typeof parsed.savedAt === "number" ? parsed.savedAt : Date.now()
|
|
408
|
-
};
|
|
409
|
-
} catch (err) {
|
|
410
|
-
console.warn("[whiteboard] scene parse error, clear:", err);
|
|
469
|
+
const parsed = safeParseScene(raw);
|
|
470
|
+
if (!parsed) {
|
|
471
|
+
console.warn("[whiteboard] scene parse/validation failed, clear:", validKey);
|
|
411
472
|
try {
|
|
412
|
-
window.localStorage.removeItem(fullKey(
|
|
473
|
+
window.localStorage.removeItem(fullKey(validKey));
|
|
413
474
|
} catch {
|
|
414
475
|
}
|
|
415
476
|
return null;
|
|
416
477
|
}
|
|
478
|
+
if (parsed.version !== SCHEMA_VERSION) {
|
|
479
|
+
console.warn(
|
|
480
|
+
`[whiteboard] scene version ${parsed.version} kh\xF4ng kh\u1EDBp ${SCHEMA_VERSION}, b\u1ECF qua.`
|
|
481
|
+
);
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
version: SCHEMA_VERSION,
|
|
486
|
+
elements: parsed.elements,
|
|
487
|
+
appState: parsed.appState,
|
|
488
|
+
savedAt: typeof parsed.savedAt === "number" ? parsed.savedAt : Date.now()
|
|
489
|
+
};
|
|
417
490
|
}
|
|
418
491
|
function writeScene(key, payload) {
|
|
492
|
+
const validKey = validateStorageKey(key);
|
|
419
493
|
if (typeof window === "undefined") return;
|
|
420
494
|
const record = {
|
|
421
495
|
version: SCHEMA_VERSION,
|
|
@@ -424,7 +498,7 @@ function writeScene(key, payload) {
|
|
|
424
498
|
savedAt: Date.now()
|
|
425
499
|
};
|
|
426
500
|
try {
|
|
427
|
-
window.localStorage.setItem(fullKey(
|
|
501
|
+
window.localStorage.setItem(fullKey(validKey), JSON.stringify(record));
|
|
428
502
|
} catch (err) {
|
|
429
503
|
console.warn("[whiteboard] scene write failed:", err);
|
|
430
504
|
}
|
|
@@ -493,12 +567,13 @@ async function withStore(mode, fn, fallback) {
|
|
|
493
567
|
});
|
|
494
568
|
}
|
|
495
569
|
async function readFiles(storageKey) {
|
|
570
|
+
const validKey = validateStorageKey(storageKey);
|
|
496
571
|
try {
|
|
497
572
|
return await withStore(
|
|
498
573
|
"readonly",
|
|
499
574
|
(store, setResult, fail) => {
|
|
500
575
|
const out = {};
|
|
501
|
-
const req = store.index("storageKey").openCursor(IDBKeyRange.only(
|
|
576
|
+
const req = store.index("storageKey").openCursor(IDBKeyRange.only(validKey));
|
|
502
577
|
req.onsuccess = () => {
|
|
503
578
|
const cursor = req.result;
|
|
504
579
|
if (!cursor) {
|
|
@@ -523,6 +598,7 @@ async function readFiles(storageKey) {
|
|
|
523
598
|
}
|
|
524
599
|
}
|
|
525
600
|
async function writeFiles(storageKey, files) {
|
|
601
|
+
const validKey = validateStorageKey(storageKey);
|
|
526
602
|
const entries = Object.entries(files);
|
|
527
603
|
if (entries.length === 0) return;
|
|
528
604
|
try {
|
|
@@ -545,7 +621,7 @@ async function writeFiles(storageKey, files) {
|
|
|
545
621
|
}
|
|
546
622
|
const rec = {
|
|
547
623
|
id,
|
|
548
|
-
storageKey,
|
|
624
|
+
storageKey: validKey,
|
|
549
625
|
dataURL: ff.dataURL,
|
|
550
626
|
mimeType: ff.mimeType,
|
|
551
627
|
created: ff.created ?? now,
|
|
@@ -566,11 +642,12 @@ async function writeFiles(storageKey, files) {
|
|
|
566
642
|
}
|
|
567
643
|
}
|
|
568
644
|
async function pruneFiles(storageKey, keepIds) {
|
|
645
|
+
const validKey = validateStorageKey(storageKey);
|
|
569
646
|
try {
|
|
570
647
|
await withStore(
|
|
571
648
|
"readwrite",
|
|
572
649
|
(store, setResult, fail) => {
|
|
573
|
-
const req = store.index("storageKey").openCursor(IDBKeyRange.only(
|
|
650
|
+
const req = store.index("storageKey").openCursor(IDBKeyRange.only(validKey));
|
|
574
651
|
req.onsuccess = () => {
|
|
575
652
|
const cursor = req.result;
|
|
576
653
|
if (!cursor) {
|
|
@@ -619,9 +696,18 @@ function Whiteboard({
|
|
|
619
696
|
const pruneThrottleRef = useRef(null);
|
|
620
697
|
const latestSceneRef = useRef(null);
|
|
621
698
|
const pendingFilesRef = useRef({});
|
|
699
|
+
const hashElementsVersionRef = useRef(null);
|
|
700
|
+
const stampsRef = useRef(stamps);
|
|
701
|
+
stampsRef.current = stamps;
|
|
622
702
|
const persistEnabled = typeof storageKey === "string" && storageKey.length > 0;
|
|
623
703
|
const persistKeyRef = useRef(storageKey);
|
|
624
704
|
persistKeyRef.current = storageKey;
|
|
705
|
+
const onSceneChangeRef = useRef(onSceneChange);
|
|
706
|
+
onSceneChangeRef.current = onSceneChange;
|
|
707
|
+
const onFilesChangeRef = useRef(onFilesChange);
|
|
708
|
+
onFilesChangeRef.current = onFilesChange;
|
|
709
|
+
const persistEnabledRef = useRef(persistEnabled);
|
|
710
|
+
persistEnabledRef.current = persistEnabled;
|
|
625
711
|
const persistedInitial = useMemo(
|
|
626
712
|
() => persistEnabled ? readScene(storageKey) : null,
|
|
627
713
|
[persistEnabled, storageKey]
|
|
@@ -709,21 +795,14 @@ function Whiteboard({
|
|
|
709
795
|
if (!sceneThrottleRef.current) {
|
|
710
796
|
sceneThrottleRef.current = setTimeout(async () => {
|
|
711
797
|
sceneThrottleRef.current = null;
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (sceneHash === lastSceneHashRef.current) return;
|
|
719
|
-
lastSceneHashRef.current = sceneHash;
|
|
720
|
-
onSceneChange?.({ elements: liveElements, appState: liveAppState });
|
|
721
|
-
if (persistEnabled) {
|
|
722
|
-
writeScene(storageKey, {
|
|
723
|
-
elements: liveElements,
|
|
724
|
-
appState: liveAppState
|
|
725
|
-
});
|
|
798
|
+
try {
|
|
799
|
+
const mod = await import('@excalidraw/excalidraw');
|
|
800
|
+
hashElementsVersionRef.current = mod.hashElementsVersion;
|
|
801
|
+
} catch (err) {
|
|
802
|
+
console.warn("[whiteboard] import excalidraw \u0111\u1EC3 flush scene th\u1EA5t b\u1EA1i:", err);
|
|
803
|
+
return;
|
|
726
804
|
}
|
|
805
|
+
flushSceneRef.current();
|
|
727
806
|
}, SYNC_THROTTLE_MS);
|
|
728
807
|
}
|
|
729
808
|
if (persistEnabled && newIds.length > 0) {
|
|
@@ -733,63 +812,112 @@ function Whiteboard({
|
|
|
733
812
|
if (!fileThrottleRef.current) {
|
|
734
813
|
fileThrottleRef.current = setTimeout(() => {
|
|
735
814
|
fileThrottleRef.current = null;
|
|
736
|
-
|
|
737
|
-
pendingFilesRef.current = {};
|
|
738
|
-
const currentElements = api?.getSceneElements?.() ?? elements;
|
|
739
|
-
const stampIds = /* @__PURE__ */ new Set();
|
|
740
|
-
for (const el of currentElements) {
|
|
741
|
-
const fid = el.fileId;
|
|
742
|
-
if (fid && isStampElement(el)) stampIds.add(fid);
|
|
743
|
-
}
|
|
744
|
-
const raster = {};
|
|
745
|
-
for (const [id, f] of Object.entries(pending)) {
|
|
746
|
-
if (!stampIds.has(id)) raster[id] = f;
|
|
747
|
-
}
|
|
748
|
-
if (Object.keys(raster).length > 0) {
|
|
749
|
-
void writeFiles(persistKeyRef.current, raster);
|
|
750
|
-
}
|
|
815
|
+
flushFilesRef.current();
|
|
751
816
|
}, 1e3);
|
|
752
817
|
}
|
|
753
818
|
}
|
|
754
819
|
if (persistEnabled && !pruneThrottleRef.current) {
|
|
755
820
|
pruneThrottleRef.current = setTimeout(() => {
|
|
756
821
|
pruneThrottleRef.current = null;
|
|
757
|
-
|
|
758
|
-
const keep = /* @__PURE__ */ new Set();
|
|
759
|
-
for (const el of currentElements) {
|
|
760
|
-
const fid = el.fileId;
|
|
761
|
-
if (fid && !isStampElement(el)) keep.add(fid);
|
|
762
|
-
}
|
|
763
|
-
void pruneFiles(persistKeyRef.current, keep);
|
|
822
|
+
flushPruneRef.current();
|
|
764
823
|
}, 2e3);
|
|
765
824
|
}
|
|
766
825
|
},
|
|
767
826
|
[readOnly, api, onSceneChange, onFilesChange, persistEnabled, storageKey, stamps, openStamp]
|
|
768
827
|
);
|
|
828
|
+
const flushSceneRef = useRef(() => void 0);
|
|
829
|
+
flushSceneRef.current = () => {
|
|
830
|
+
try {
|
|
831
|
+
const latestScene = latestSceneRef.current;
|
|
832
|
+
if (!latestScene) return;
|
|
833
|
+
const liveElements = latestScene.elements.filter((e) => !e.isDeleted);
|
|
834
|
+
const liveAppState = pickSyncableAppState(latestScene.appState);
|
|
835
|
+
const hashFn = hashElementsVersionRef.current;
|
|
836
|
+
const elementHash = hashFn ? hashFn(liveElements) : liveElements.map((e) => e.id).join("|");
|
|
837
|
+
const sceneHash = `${elementHash}:${JSON.stringify(liveAppState)}`;
|
|
838
|
+
if (sceneHash === lastSceneHashRef.current) return;
|
|
839
|
+
lastSceneHashRef.current = sceneHash;
|
|
840
|
+
onSceneChangeRef.current?.({ elements: liveElements, appState: liveAppState });
|
|
841
|
+
if (persistEnabledRef.current) {
|
|
842
|
+
writeScene(persistKeyRef.current, {
|
|
843
|
+
elements: liveElements,
|
|
844
|
+
appState: liveAppState
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
} catch (err) {
|
|
848
|
+
console.warn("[whiteboard] flushScene th\u1EA5t b\u1EA1i:", err);
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const flushFilesRef = useRef(() => void 0);
|
|
852
|
+
flushFilesRef.current = () => {
|
|
853
|
+
try {
|
|
854
|
+
const pending = pendingFilesRef.current;
|
|
855
|
+
pendingFilesRef.current = {};
|
|
856
|
+
if (Object.keys(pending).length === 0) return;
|
|
857
|
+
const currentElements = apiRef.current?.getSceneElements?.() ?? latestSceneRef.current?.elements ?? [];
|
|
858
|
+
const stampIds = /* @__PURE__ */ new Set();
|
|
859
|
+
for (const el of currentElements) {
|
|
860
|
+
const fid = el.fileId;
|
|
861
|
+
if (fid && isStampElement(el)) stampIds.add(fid);
|
|
862
|
+
}
|
|
863
|
+
const raster = {};
|
|
864
|
+
for (const [id, f] of Object.entries(pending)) {
|
|
865
|
+
if (!stampIds.has(id)) raster[id] = f;
|
|
866
|
+
}
|
|
867
|
+
if (Object.keys(raster).length > 0) {
|
|
868
|
+
void writeFiles(persistKeyRef.current, raster);
|
|
869
|
+
}
|
|
870
|
+
} catch (err) {
|
|
871
|
+
console.warn("[whiteboard] flushFiles th\u1EA5t b\u1EA1i:", err);
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
const flushPruneRef = useRef(() => void 0);
|
|
875
|
+
flushPruneRef.current = () => {
|
|
876
|
+
try {
|
|
877
|
+
const currentElements = apiRef.current?.getSceneElements?.() ?? latestSceneRef.current?.elements ?? [];
|
|
878
|
+
const keep = /* @__PURE__ */ new Set();
|
|
879
|
+
for (const el of currentElements) {
|
|
880
|
+
const fid = el.fileId;
|
|
881
|
+
if (fid && !isStampElement(el)) keep.add(fid);
|
|
882
|
+
}
|
|
883
|
+
void pruneFiles(persistKeyRef.current, keep);
|
|
884
|
+
} catch (err) {
|
|
885
|
+
console.warn("[whiteboard] flushPrune th\u1EA5t b\u1EA1i:", err);
|
|
886
|
+
}
|
|
887
|
+
};
|
|
769
888
|
useEffect(() => {
|
|
770
889
|
if (!api || !persistEnabled) return;
|
|
771
890
|
let cancelled = false;
|
|
772
|
-
void readFiles(storageKey).then(
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
891
|
+
void readFiles(storageKey).then(
|
|
892
|
+
(files) => {
|
|
893
|
+
if (cancelled) return;
|
|
894
|
+
const entries = Object.entries(files);
|
|
895
|
+
if (entries.length === 0) return;
|
|
896
|
+
if (cancelled) return;
|
|
897
|
+
try {
|
|
898
|
+
api.addFiles(
|
|
899
|
+
entries.map(([id, f]) => ({
|
|
900
|
+
id,
|
|
901
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
902
|
+
dataURL: f.dataURL,
|
|
903
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
904
|
+
mimeType: f.mimeType,
|
|
905
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
906
|
+
created: f.created ?? Date.now()
|
|
907
|
+
}))
|
|
908
|
+
);
|
|
909
|
+
if (cancelled) return;
|
|
910
|
+
entries.forEach(([id]) => knownFileIdsRef.current.add(id));
|
|
911
|
+
} catch (err) {
|
|
912
|
+
if (cancelled) return;
|
|
913
|
+
console.warn("[whiteboard] addFiles t\u1EEB IDB th\u1EA5t b\u1EA1i:", err);
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
(err) => {
|
|
917
|
+
if (cancelled) return;
|
|
918
|
+
console.warn("[whiteboard] readFiles th\u1EA5t b\u1EA1i:", err);
|
|
791
919
|
}
|
|
792
|
-
|
|
920
|
+
);
|
|
793
921
|
return () => {
|
|
794
922
|
cancelled = true;
|
|
795
923
|
};
|
|
@@ -798,27 +926,43 @@ function Whiteboard({
|
|
|
798
926
|
if (!api) return;
|
|
799
927
|
let cancelled = false;
|
|
800
928
|
const run = async () => {
|
|
929
|
+
if (cancelled) return;
|
|
801
930
|
try {
|
|
802
931
|
const elements = api.getSceneElements();
|
|
803
932
|
if (!elements || elements.length === 0) return;
|
|
804
933
|
if (cancelled) return;
|
|
805
|
-
await restoreMissingStampFiles(api, elements,
|
|
934
|
+
await restoreMissingStampFiles(api, elements, stampsRef.current);
|
|
806
935
|
} catch (err) {
|
|
936
|
+
if (cancelled) return;
|
|
807
937
|
console.warn("Math stamp restore pass failed:", err);
|
|
808
938
|
}
|
|
809
939
|
};
|
|
810
|
-
run();
|
|
811
|
-
const t = setTimeout(
|
|
940
|
+
void run();
|
|
941
|
+
const t = setTimeout(() => {
|
|
942
|
+
void run();
|
|
943
|
+
}, 400);
|
|
812
944
|
return () => {
|
|
813
945
|
cancelled = true;
|
|
814
946
|
clearTimeout(t);
|
|
815
947
|
};
|
|
816
|
-
}, [api, persistedInitial
|
|
948
|
+
}, [api, persistedInitial]);
|
|
817
949
|
useEffect(
|
|
818
950
|
() => () => {
|
|
819
|
-
if (sceneThrottleRef.current)
|
|
820
|
-
|
|
821
|
-
|
|
951
|
+
if (sceneThrottleRef.current) {
|
|
952
|
+
clearTimeout(sceneThrottleRef.current);
|
|
953
|
+
sceneThrottleRef.current = null;
|
|
954
|
+
flushSceneRef.current();
|
|
955
|
+
}
|
|
956
|
+
if (fileThrottleRef.current) {
|
|
957
|
+
clearTimeout(fileThrottleRef.current);
|
|
958
|
+
fileThrottleRef.current = null;
|
|
959
|
+
flushFilesRef.current();
|
|
960
|
+
}
|
|
961
|
+
if (pruneThrottleRef.current) {
|
|
962
|
+
clearTimeout(pruneThrottleRef.current);
|
|
963
|
+
pruneThrottleRef.current = null;
|
|
964
|
+
flushPruneRef.current();
|
|
965
|
+
}
|
|
822
966
|
},
|
|
823
967
|
[]
|
|
824
968
|
);
|