@q9labsai/chalk-whiteboard 4.1.17 → 5.0.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.
@@ -18,23 +18,12 @@ function withLocalWhiteboardCamera(sharedAppState, localAppState) {
18
18
 
19
19
  // src/collab/files.ts
20
20
  import { CaptureUpdateAction, MIME_TYPES, newElementWith } from "@excalidraw/excalidraw";
21
- import { sha256 } from "@noble/hashes/sha2.js";
22
- import { bytesToHex } from "@noble/hashes/utils.js";
23
- var dataURLToBlob = (dataURL) => {
24
- const [header, base64] = dataURL.split(",", 2);
25
- if (!header || !base64) throw new Error("invalid dataURL");
26
- const match = /data:(.+?)(;base64)?$/i.exec(header);
27
- const mimeType = match?.[1] ?? "application/octet-stream";
28
- const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
29
- return new Blob([bytes], { type: mimeType });
30
- };
31
21
  var blobToDataURL = (blob) => new Promise((resolve, reject) => {
32
22
  const reader = new FileReader();
33
23
  reader.onerror = () => reject(new Error("failed to read blob"));
34
24
  reader.onload = () => resolve(String(reader.result));
35
25
  reader.readAsDataURL(blob);
36
26
  });
37
- var sha256Hex = async (blob) => bytesToHex(sha256(new Uint8Array(await blob.arrayBuffer())));
38
27
  var isImageElement = (el) => el.type === "image";
39
28
  var toFileId = (value) => value;
40
29
  var toDataURL = (value) => {
@@ -68,29 +57,11 @@ var WhiteboardFilesSync = class {
68
57
  this.opts = opts;
69
58
  }
70
59
  opts;
71
- uploadTimer = null;
72
60
  downloadTimer = null;
73
- uploadQueue = [];
74
61
  downloadQueue = [];
75
- uploading = /* @__PURE__ */ new Set();
76
62
  downloading = /* @__PURE__ */ new Set();
77
63
  remotePendingUploads = 0;
78
64
  lastErrorAtMs = null;
79
- handleLocalScene(elementsAll, files) {
80
- for (const el of elementsAll) {
81
- if (!isImageElement(el) || el.isDeleted) continue;
82
- if (el.status !== "pending") continue;
83
- if (!el.fileId) continue;
84
- const fileId = String(el.fileId);
85
- if (this.uploading.has(fileId)) continue;
86
- if (this.uploadQueue.includes(fileId)) continue;
87
- const file = files[fileId];
88
- if (!file?.dataURL || !file?.mimeType) continue;
89
- this.uploadQueue.push(fileId);
90
- }
91
- this.emitState();
92
- if (this.uploadQueue.length > 0) this.scheduleUpload();
93
- }
94
65
  handleRemoteScene(elementsAll) {
95
66
  const haveFiles = this.opts.excalidrawAPI.getFiles();
96
67
  let pendingRemoteUploads = 0;
@@ -113,25 +84,14 @@ var WhiteboardFilesSync = class {
113
84
  if (this.downloadQueue.length > 0) this.scheduleDownload();
114
85
  }
115
86
  dispose() {
116
- if (this.uploadTimer) clearTimeout(this.uploadTimer);
117
87
  if (this.downloadTimer) clearTimeout(this.downloadTimer);
118
- this.uploadTimer = null;
119
88
  this.downloadTimer = null;
120
- this.uploadQueue = [];
121
89
  this.downloadQueue = [];
122
- this.uploading.clear();
123
90
  this.downloading.clear();
124
91
  this.remotePendingUploads = 0;
125
92
  this.lastErrorAtMs = null;
126
93
  this.emitState();
127
94
  }
128
- scheduleUpload() {
129
- if (this.uploadTimer) return;
130
- this.uploadTimer = setTimeout(() => {
131
- this.uploadTimer = null;
132
- void this.processNextUpload();
133
- }, this.opts.uploadThrottleMs ?? 300);
134
- }
135
95
  scheduleDownload() {
136
96
  if (this.downloadTimer) return;
137
97
  this.downloadTimer = setTimeout(() => {
@@ -139,50 +99,6 @@ var WhiteboardFilesSync = class {
139
99
  void this.processNextDownload();
140
100
  }, this.opts.downloadThrottleMs ?? 500);
141
101
  }
142
- async processNextUpload() {
143
- const fileId = this.uploadQueue.shift();
144
- if (!fileId) return;
145
- if (this.uploading.has(fileId)) return;
146
- const files = this.opts.excalidrawAPI.getFiles();
147
- const file = files[fileId];
148
- if (!file?.dataURL || !file?.mimeType) {
149
- if (this.uploadQueue.length > 0) this.scheduleUpload();
150
- return;
151
- }
152
- this.uploading.add(fileId);
153
- this.emitState();
154
- try {
155
- const blob = dataURLToBlob(file.dataURL);
156
- const metadata = {
157
- fileId,
158
- mimeType: file.mimeType,
159
- byteLength: blob.size,
160
- sha256: await sha256Hex(blob)
161
- };
162
- if (this.opts.fileTransfer) {
163
- await this.opts.fileTransfer.upload({ ...metadata, dataURL: file.dataURL });
164
- } else {
165
- const initiateUpload = requireOption(this.opts.initiateUpload, "initiateUpload");
166
- const finalizeUpload = requireOption(this.opts.finalizeUpload, "finalizeUpload");
167
- const instructions = await initiateUpload(metadata);
168
- const res = await fetch(instructions.uploadUrl, {
169
- method: instructions.method,
170
- headers: instructions.headers,
171
- body: blob
172
- });
173
- if (!res.ok) throw new Error(`upload failed: ${res.status}`);
174
- await finalizeUpload(instructions.uploadId);
175
- }
176
- updateImageStatus(this.opts.excalidrawAPI, fileId, "saved");
177
- } catch {
178
- this.lastErrorAtMs = Date.now();
179
- updateImageStatus(this.opts.excalidrawAPI, fileId, "error");
180
- } finally {
181
- this.uploading.delete(fileId);
182
- this.emitState();
183
- if (this.uploadQueue.length > 0) this.scheduleUpload();
184
- }
185
- }
186
102
  async processNextDownload() {
187
103
  const fileId = this.downloadQueue.shift();
188
104
  if (!fileId) return;
@@ -227,15 +143,11 @@ var WhiteboardFilesSync = class {
227
143
  }
228
144
  }
229
145
  emitState() {
230
- const uploadQueued = this.uploadQueue.length;
231
- const uploading = this.uploading.size;
232
146
  const downloadQueued = this.downloadQueue.length;
233
147
  const downloading = this.downloading.size;
234
- const phase = uploading > 0 || uploadQueued > 0 ? "uploading" : this.remotePendingUploads > 0 ? "awaiting_remote_upload" : downloading > 0 || downloadQueued > 0 ? "downloading" : this.lastErrorAtMs && Date.now() - this.lastErrorAtMs < 4e3 ? "error" : "idle";
148
+ const phase = this.remotePendingUploads > 0 ? "awaiting_remote_upload" : downloading > 0 || downloadQueued > 0 ? "downloading" : this.lastErrorAtMs && Date.now() - this.lastErrorAtMs < 4e3 ? "error" : "idle";
235
149
  this.opts.onStateChange?.({
236
150
  phase,
237
- uploading,
238
- uploadQueued,
239
151
  remotePendingUploads: this.remotePendingUploads,
240
152
  downloading,
241
153
  downloadQueued,
@@ -337,6 +249,8 @@ var filterSyncableElements = (elements) => elements.filter((el) => {
337
249
  if (el.isDeleted) return true;
338
250
  return !isInvisiblySmallElement(el);
339
251
  });
252
+ var remoteImageFileIds = (elements) => elements.flatMap((element) => element.type === "image" && element.fileId ? [element.fileId] : []);
253
+ var retainStoredImages = (elements, storedFileIds) => elements.filter((element) => element.type !== "image" || element.fileId !== null && storedFileIds.has(element.fileId));
340
254
 
341
255
  // src/collab/wire.ts
342
256
  var ELEMENT_ENCODED_BYTES_LIMIT = 16384 - 64;
@@ -488,10 +402,7 @@ var ExcalidrawCollabEngine = class {
488
402
  this.filesSync = new WhiteboardFilesSync({
489
403
  excalidrawAPI: opts.excalidrawAPI,
490
404
  fileTransfer: opts.fileTransfer,
491
- initiateUpload: opts.initiateUpload,
492
- finalizeUpload: opts.finalizeUpload,
493
405
  presignDownload: opts.presignDownload,
494
- uploadThrottleMs: 300,
495
406
  downloadThrottleMs: 500,
496
407
  onStateChange: opts.onFileSyncStateChange
497
408
  });
@@ -508,6 +419,7 @@ var ExcalidrawCollabEngine = class {
508
419
  sceneId = null;
509
420
  sceneGeneration = null;
510
421
  canDraw = true;
422
+ storedImageFileIds = /* @__PURE__ */ new Set();
511
423
  broadcastedElementVersions = /* @__PURE__ */ new Map();
512
424
  rejectedElementVersions = /* @__PURE__ */ new Map();
513
425
  deferredRemoteElements = /* @__PURE__ */ new Map();
@@ -528,6 +440,7 @@ var ExcalidrawCollabEngine = class {
528
440
  async clear() {
529
441
  if (!this.canDraw) throw new Error("whiteboard drawing is not permitted");
530
442
  const commit = await this.opts.clear();
443
+ this.storedImageFileIds.clear();
531
444
  this.sceneId = commit.sceneId;
532
445
  this.sceneGeneration = commit.sceneGeneration ?? null;
533
446
  this.broadcastedElementVersions.clear();
@@ -555,11 +468,10 @@ var ExcalidrawCollabEngine = class {
555
468
  this.unsubscribe();
556
469
  this.unsubPointerUp?.();
557
470
  }
558
- handleChange(_elements, _appState, files) {
471
+ handleChange(_elements, _appState, _files) {
559
472
  if (this.disposed) return;
560
473
  this.applyDeferredRemoteElements();
561
- const elementsAll = this.opts.excalidrawAPI.getSceneElementsIncludingDeleted();
562
- this.filesSync.handleLocalScene(elementsAll, files);
474
+ this.stripUnstoredImages();
563
475
  if (!this.canDraw) return;
564
476
  const now = Date.now();
565
477
  const deadline = this.changeDebounceDeadline ?? now + CHANGE_MAX_WAIT_MS;
@@ -630,12 +542,18 @@ var ExcalidrawCollabEngine = class {
630
542
  isSnapshot: true
631
543
  });
632
544
  }
545
+ stripUnstoredImages() {
546
+ const elements = this.opts.excalidrawAPI.getSceneElementsIncludingDeleted();
547
+ const retained = retainStoredImages(elements, this.storedImageFileIds);
548
+ if (retained.length === elements.length) return;
549
+ this.opts.excalidrawAPI.updateScene({ elements: retained, captureUpdate: CaptureUpdateAction2.NEVER });
550
+ }
633
551
  flushNow(skipEditing = false) {
634
552
  if (this.disposed || this.submissionRetryTimer !== null) return;
635
553
  const context = this.submissionContext();
636
554
  if (!context) return;
637
555
  const editing = skipEditing ? editingElementIds(this.opts.excalidrawAPI.getAppState()) : [];
638
- const syncableAll = filterSyncableElements(context.elements);
556
+ const syncableAll = filterSyncableElements(retainStoredImages(context.elements, this.storedImageFileIds));
639
557
  const delta = [];
640
558
  for (const el of syncableAll) {
641
559
  if (this.isUnsubmitted(el) && !editing.includes(el.id)) delta.push(el);
@@ -646,7 +564,7 @@ var ExcalidrawCollabEngine = class {
646
564
  flushOnDispose() {
647
565
  const context = this.submissionContext({ allowDisposed: true, allowInFlight: true });
648
566
  if (!context) return;
649
- const syncableAll = filterSyncableElements(context.elements);
567
+ const syncableAll = filterSyncableElements(retainStoredImages(context.elements, this.storedImageFileIds));
650
568
  const delta = [];
651
569
  for (const element of syncableAll) {
652
570
  if (this.isUnsubmitted(element)) delta.push(element);
@@ -797,6 +715,7 @@ var ExcalidrawCollabEngine = class {
797
715
  }
798
716
  }
799
717
  if (replacingScene) {
718
+ this.storedImageFileIds.clear();
800
719
  this.rejectedElementVersions.clear();
801
720
  this.clearChangeDebounce();
802
721
  this.dirtyDuringSubmission = false;
@@ -810,10 +729,11 @@ var ExcalidrawCollabEngine = class {
810
729
  const local = replacingScene ? [] : excalidrawAPI.getSceneElementsIncludingDeleted();
811
730
  const remoteElements = [...args.remoteElements].sort(compareElementOrder);
812
731
  const restoredRemote = restoreElements(remoteElements, null);
732
+ this.storedImageFileIds = /* @__PURE__ */ new Set([...this.storedImageFileIds, ...remoteImageFileIds(restoredRemote)]);
813
733
  const remoteForReconcile = toReconcileRemoteElements(restoredRemote);
814
734
  const appState = excalidrawAPI.getAppState();
815
735
  const editing = editingElementIds(appState);
816
- const reconciled = reconcileElements(local, remoteForReconcile, appState);
736
+ const reconciled = retainStoredImages(reconcileElements(local, remoteForReconcile, appState), this.storedImageFileIds);
817
737
  if (args.isSnapshot) {
818
738
  this.broadcastedElementVersions.clear();
819
739
  this.resetSubmissionRetries();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  compareElementOrder
3
- } from "./chunk-6Q65ADBK.js";
3
+ } from "./chunk-6ZV7GBAC.js";
4
4
 
5
5
  // src/collab/reducer.ts
6
6
  function mergeWhiteboardElements(current, incoming) {
@@ -51,6 +51,7 @@ export declare class ExcalidrawCollabEngine {
51
51
  private sceneId;
52
52
  private sceneGeneration;
53
53
  private canDraw;
54
+ private storedImageFileIds;
54
55
  private broadcastedElementVersions;
55
56
  private rejectedElementVersions;
56
57
  private deferredRemoteElements;
@@ -69,7 +70,7 @@ export declare class ExcalidrawCollabEngine {
69
70
  setCanDraw(next: boolean): void;
70
71
  clear(): Promise<WhiteboardCommit>;
71
72
  dispose(): void;
72
- handleChange(_elements: readonly OrderedExcalidrawElement[], _appState: AppState, files: BinaryFiles): void;
73
+ handleChange(_elements: readonly OrderedExcalidrawElement[], _appState: AppState, _files: BinaryFiles): void;
73
74
  handlePointerUpdate(payload: {
74
75
  pointer: {
75
76
  x: number;
@@ -96,6 +97,7 @@ export declare class ExcalidrawCollabEngine {
96
97
  elements: readonly WhiteboardWireElement[];
97
98
  appState?: SharedWhiteboardAppState;
98
99
  }): void;
100
+ private stripUnstoredImages;
99
101
  private flushNow;
100
102
  private flushOnDispose;
101
103
  private isUnsubmitted;
@@ -1,23 +1,13 @@
1
- import type { BinaryFiles, ExcalidrawImperativeAPI, OrderedExcalidrawElement } from "./types.js";
2
- import type { WhiteboardUploadInstructions } from "./wire.js";
3
- export type WhiteboardFileSyncPhase = "idle" | "uploading" | "awaiting_remote_upload" | "downloading" | "error";
1
+ import type { ExcalidrawImperativeAPI, OrderedExcalidrawElement } from "./types.js";
2
+ export type WhiteboardFileSyncPhase = "idle" | "awaiting_remote_upload" | "downloading" | "error";
4
3
  export interface WhiteboardFileSyncState {
5
4
  phase: WhiteboardFileSyncPhase;
6
- uploading: number;
7
- uploadQueued: number;
8
5
  remotePendingUploads: number;
9
6
  downloading: number;
10
7
  downloadQueued: number;
11
8
  lastErrorAtMs: number | null;
12
9
  }
13
10
  export interface WhiteboardFileTransfer {
14
- readonly upload: (input: {
15
- readonly fileId: string;
16
- readonly mimeType: string;
17
- readonly byteLength: number;
18
- readonly sha256: string;
19
- readonly dataURL: string;
20
- }) => Promise<void>;
21
11
  readonly download: (fileId: string) => Promise<{
22
12
  readonly mimeType: string;
23
13
  readonly dataURL: string;
@@ -25,40 +15,26 @@ export interface WhiteboardFileTransfer {
25
15
  }
26
16
  export interface WhiteboardFileTransferOptions {
27
17
  fileTransfer?: WhiteboardFileTransfer;
28
- initiateUpload?: (input: {
29
- fileId: string;
30
- mimeType: string;
31
- byteLength: number;
32
- sha256: string;
33
- }) => Promise<WhiteboardUploadInstructions>;
34
- finalizeUpload?: (uploadId: string) => Promise<void>;
35
18
  presignDownload?: (fileId: string) => Promise<{
36
19
  downloadUrl: string;
37
20
  }>;
38
21
  }
39
22
  interface WhiteboardFilesSyncOptions extends WhiteboardFileTransferOptions {
40
23
  excalidrawAPI: ExcalidrawImperativeAPI;
41
- uploadThrottleMs?: number;
42
24
  downloadThrottleMs?: number;
43
25
  onStateChange?: (state: WhiteboardFileSyncState) => void;
44
26
  }
45
27
  export declare class WhiteboardFilesSync {
46
28
  private readonly opts;
47
- private uploadTimer;
48
29
  private downloadTimer;
49
- private uploadQueue;
50
30
  private downloadQueue;
51
- private uploading;
52
31
  private downloading;
53
32
  constructor(opts: WhiteboardFilesSyncOptions);
54
33
  private remotePendingUploads;
55
34
  private lastErrorAtMs;
56
- handleLocalScene(elementsAll: readonly OrderedExcalidrawElement[], files: BinaryFiles): void;
57
35
  handleRemoteScene(elementsAll: readonly OrderedExcalidrawElement[]): void;
58
36
  dispose(): void;
59
- private scheduleUpload;
60
37
  private scheduleDownload;
61
- private processNextUpload;
62
38
  private processNextDownload;
63
39
  private emitState;
64
40
  }
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  mergeWhiteboardElements
3
- } from "../chunk-EPSMA2DY.js";
3
+ } from "../chunk-W4FLPXAX.js";
4
4
  import {
5
5
  ExcalidrawCollabEngine,
6
6
  fromWireElement,
7
7
  isWireElement,
8
8
  toWireElement
9
- } from "../chunk-6Q65ADBK.js";
9
+ } from "../chunk-6ZV7GBAC.js";
10
10
  export {
11
11
  ExcalidrawCollabEngine,
12
12
  fromWireElement,
@@ -1,2 +1,4 @@
1
1
  import type { OrderedExcalidrawElement } from "./types.js";
2
2
  export declare const filterSyncableElements: (elements: readonly OrderedExcalidrawElement[]) => OrderedExcalidrawElement[];
3
+ export declare const remoteImageFileIds: (elements: readonly OrderedExcalidrawElement[]) => string[];
4
+ export declare const retainStoredImages: (elements: readonly OrderedExcalidrawElement[], storedFileIds: ReadonlySet<string>) => OrderedExcalidrawElement[];
@@ -1216,8 +1216,8 @@
1216
1216
  },
1217
1217
  {
1218
1218
  "path": "renderer.js",
1219
- "bytes": 8504846,
1220
- "sha256": "20644a73c951091c36858684c83bd579c6894f986bf461cf21289f0ccef4ea59"
1219
+ "bytes": 8497928,
1220
+ "sha256": "7f754073f20cee4b3adceb40731faf75d94849f6ce01a0b2e30fdad10867a5d9"
1221
1221
  },
1222
1222
  {
1223
1223
  "path": "THIRD_PARTY_NOTICES.json",