@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.
Files changed (49) hide show
  1. package/README.md +51 -1
  2. package/dist/chunk-74VEEZBV.mjs +619 -0
  3. package/dist/chunk-74VEEZBV.mjs.map +1 -0
  4. package/dist/chunk-DU2NFHRR.mjs +103 -0
  5. package/dist/chunk-DU2NFHRR.mjs.map +1 -0
  6. package/dist/{chunk-SHFOGORM.mjs → chunk-DU3RHKT5.mjs} +4 -4
  7. package/dist/{chunk-SHFOGORM.mjs.map → chunk-DU3RHKT5.mjs.map} +1 -1
  8. package/dist/{chunk-HYXFHEDJ.mjs → chunk-IUVV52HO.mjs} +22 -7
  9. package/dist/chunk-IUVV52HO.mjs.map +1 -0
  10. package/dist/{chunk-BJX4YNA5.mjs → chunk-KEYZ5EZT.mjs} +26 -9
  11. package/dist/chunk-KEYZ5EZT.mjs.map +1 -0
  12. package/dist/{chunk-LPM4MM45.mjs → chunk-SBDMF4NQ.mjs} +3 -2
  13. package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
  14. package/dist/{chunk-3SSQKRRO.mjs → chunk-ZVN356JZ.mjs} +4 -4
  15. package/dist/{chunk-3SSQKRRO.mjs.map → chunk-ZVN356JZ.mjs.map} +1 -1
  16. package/dist/geometry-2d.js +250 -218
  17. package/dist/geometry-2d.js.map +1 -1
  18. package/dist/geometry-2d.mjs +2 -2
  19. package/dist/geometry-3d.d.mts +1 -1
  20. package/dist/geometry-3d.d.ts +1 -1
  21. package/dist/geometry-3d.js +3276 -1201
  22. package/dist/geometry-3d.js.map +1 -1
  23. package/dist/geometry-3d.mjs +3 -2
  24. package/dist/graph-2d.js +360 -66
  25. package/dist/graph-2d.js.map +1 -1
  26. package/dist/graph-2d.mjs +2 -2
  27. package/dist/{host-2QGKMGCT.mjs → host-LZH2FZ2N.mjs} +3 -3
  28. package/dist/{host-2QGKMGCT.mjs.map → host-LZH2FZ2N.mjs.map} +1 -1
  29. package/dist/host-PIIDSMVE.mjs +3187 -0
  30. package/dist/host-PIIDSMVE.mjs.map +1 -0
  31. package/dist/{host-T2W6R6SO.mjs → host-VDNAJMLC.mjs} +221 -216
  32. package/dist/host-VDNAJMLC.mjs.map +1 -0
  33. package/dist/index.d.mts +6 -5
  34. package/dist/index.d.ts +6 -5
  35. package/dist/index.js +4365 -1821
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +246 -102
  38. package/dist/index.mjs.map +1 -1
  39. package/package.json +6 -6
  40. package/dist/chunk-BJX4YNA5.mjs.map +0 -1
  41. package/dist/chunk-DJTBZEAR.mjs +0 -25
  42. package/dist/chunk-DJTBZEAR.mjs.map +0 -1
  43. package/dist/chunk-HM7RIXJE.mjs +0 -331
  44. package/dist/chunk-HM7RIXJE.mjs.map +0 -1
  45. package/dist/chunk-HYXFHEDJ.mjs.map +0 -1
  46. package/dist/chunk-LPM4MM45.mjs.map +0 -1
  47. package/dist/host-T2W6R6SO.mjs.map +0 -1
  48. package/dist/host-XUFON6CQ.mjs +0 -1422
  49. 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-SHFOGORM.mjs';
4
- export { geometryStamp } from './chunk-SHFOGORM.mjs';
5
- import { geometry3dStamp } from './chunk-HYXFHEDJ.mjs';
6
- export { geometry3dStamp } from './chunk-HYXFHEDJ.mjs';
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-3SSQKRRO.mjs';
10
- export { graph2dStamp } from './chunk-3SSQKRRO.mjs';
11
- export { isGraph2DCustomData } from './chunk-HM7RIXJE.mjs';
12
- export { isGeometryCustomData } from './chunk-BJX4YNA5.mjs';
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-DJTBZEAR.mjs';
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 = STABLE_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 schedule = () => {
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
- const root = document.querySelector(".excalidraw") ?? document.body;
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) cancelAnimationFrame(rafId);
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(key));
467
+ const raw = window.localStorage.getItem(fullKey(validKey));
392
468
  if (!raw) return null;
393
- try {
394
- const parsed = JSON.parse(raw);
395
- if (!parsed || typeof parsed !== "object") return null;
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(key));
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(key), JSON.stringify(record));
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(storageKey));
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(storageKey));
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
- const mod = await import('@excalidraw/excalidraw');
713
- const latestScene = latestSceneRef.current ?? { elements, appState };
714
- const liveElements = latestScene.elements.filter((e) => !e.isDeleted);
715
- const liveAppState = pickSyncableAppState(latestScene.appState);
716
- const elementHash = mod.hashElementsVersion(liveElements);
717
- const sceneHash = `${elementHash}:${JSON.stringify(liveAppState)}`;
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
- const pending = pendingFilesRef.current;
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
- const currentElements = api?.getSceneElements?.() ?? elements;
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((files) => {
773
- if (cancelled) return;
774
- const entries = Object.entries(files);
775
- if (entries.length === 0) return;
776
- try {
777
- api.addFiles(
778
- entries.map(([id, f]) => ({
779
- id,
780
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
781
- dataURL: f.dataURL,
782
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
783
- mimeType: f.mimeType,
784
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
785
- created: f.created ?? Date.now()
786
- }))
787
- );
788
- entries.forEach(([id]) => knownFileIdsRef.current.add(id));
789
- } catch (err) {
790
- console.warn("[whiteboard] addFiles t\u1EEB IDB th\u1EA5t b\u1EA1i:", err);
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, stamps);
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(run, 400);
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, stamps]);
948
+ }, [api, persistedInitial]);
817
949
  useEffect(
818
950
  () => () => {
819
- if (sceneThrottleRef.current) clearTimeout(sceneThrottleRef.current);
820
- if (fileThrottleRef.current) clearTimeout(fileThrottleRef.current);
821
- if (pruneThrottleRef.current) clearTimeout(pruneThrottleRef.current);
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
  );