@q9labsai/chalk-whiteboard 4.1.16 → 5.0.0
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/LICENSE +21 -0
- package/dist/{chunk-F4B4ANIZ.js → chunk-6ZV7GBAC.js} +208 -161
- package/dist/{chunk-4XG2ABSP.js → chunk-W4FLPXAX.js} +4 -4
- package/dist/collab/engine.d.ts +14 -5
- package/dist/collab/files.d.ts +2 -26
- package/dist/collab/index.js +2 -2
- package/dist/collab/order.d.ts +6 -0
- package/dist/collab/syncable.d.ts +2 -0
- package/dist/collab/wire.d.ts +6 -0
- package/dist/embedded/chalk-whiteboard/asset-manifest.json +2 -2
- package/dist/embedded/chalk-whiteboard/renderer.js +510 -510
- package/dist/embedded/controller.d.ts +0 -12
- package/dist/embedded/index.js +4 -55
- package/dist/embedded/renderer-bridge.d.ts +0 -7
- package/dist/embedded/renderer.d.ts +0 -1
- package/dist/{engine-5XZHDUXG.js → engine-BIJ57YN3.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/react/WhiteboardCanvas.d.ts +2 -0
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +77 -59
- package/package.json +2 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Q9Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/collab/engine.ts
|
|
2
|
-
import { CaptureUpdateAction as CaptureUpdateAction2,
|
|
2
|
+
import { CaptureUpdateAction as CaptureUpdateAction2, reconcileElements, restoreElements } from "@excalidraw/excalidraw";
|
|
3
3
|
|
|
4
4
|
// src/collab/camera.ts
|
|
5
5
|
function getWhiteboardCamera(appState) {
|
|
@@ -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 =
|
|
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,
|
|
@@ -248,6 +160,15 @@ function requireOption(value, name) {
|
|
|
248
160
|
return value;
|
|
249
161
|
}
|
|
250
162
|
|
|
163
|
+
// src/collab/order.ts
|
|
164
|
+
function compareCodeUnits(left, right) {
|
|
165
|
+
if (left === right) return 0;
|
|
166
|
+
return left < right ? -1 : 1;
|
|
167
|
+
}
|
|
168
|
+
function compareElementOrder(left, right) {
|
|
169
|
+
return compareCodeUnits(left.index ?? "", right.index ?? "") || compareCodeUnits(left.id, right.id);
|
|
170
|
+
}
|
|
171
|
+
|
|
251
172
|
// src/collab/presence.ts
|
|
252
173
|
var CURSOR_COLORS = [
|
|
253
174
|
{ stroke: "#FF5D5D", background: "rgba(255, 93, 93, 0.2)" },
|
|
@@ -328,8 +249,12 @@ var filterSyncableElements = (elements) => elements.filter((el) => {
|
|
|
328
249
|
if (el.isDeleted) return true;
|
|
329
250
|
return !isInvisiblySmallElement(el);
|
|
330
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));
|
|
331
254
|
|
|
332
255
|
// src/collab/wire.ts
|
|
256
|
+
var ELEMENT_ENCODED_BYTES_LIMIT = 16384 - 64;
|
|
257
|
+
var encoder = new TextEncoder();
|
|
333
258
|
var wireElementKeys = ["id", "type", "version", "version_nonce", "index", "is_deleted", "payload"];
|
|
334
259
|
var wireElementValidators = [
|
|
335
260
|
(value) => isNonEmptyString(value.id),
|
|
@@ -351,6 +276,47 @@ function toWireElement(element) {
|
|
|
351
276
|
payload: toJsonRecord(element)
|
|
352
277
|
};
|
|
353
278
|
}
|
|
279
|
+
function toFittedWireElement(element) {
|
|
280
|
+
const wire = toWireElement(element);
|
|
281
|
+
if (fits(wire)) return wire;
|
|
282
|
+
const payload = fitPoints(element, wire);
|
|
283
|
+
if (!payload) throw new Error("Whiteboard element exceeds the whiteboard-v1 element size");
|
|
284
|
+
return { ...wire, payload };
|
|
285
|
+
}
|
|
286
|
+
function fitPoints(element, wire) {
|
|
287
|
+
if (!("points" in element) || isElbowArrow(element)) return void 0;
|
|
288
|
+
const points = element.points.map((point) => point.map(roundCoordinate));
|
|
289
|
+
const pressures = element.type === "freedraw" ? element.pressures.map(roundCoordinate) : void 0;
|
|
290
|
+
const sample = (stride) => ({ ...wire.payload, points: sampleValues(points, stride), ...pressures ? { pressures: sampleValues(pressures, stride) } : {} });
|
|
291
|
+
const lastStride = Math.max(1, points.length - 1);
|
|
292
|
+
if (!fits({ ...wire, payload: sample(lastStride) })) return void 0;
|
|
293
|
+
for (let stride = 1; stride < lastStride; stride++) {
|
|
294
|
+
const reduced = sample(stride);
|
|
295
|
+
if (fits({ ...wire, payload: reduced })) return reduced;
|
|
296
|
+
}
|
|
297
|
+
return sample(lastStride);
|
|
298
|
+
}
|
|
299
|
+
function isElbowArrow(element) {
|
|
300
|
+
return element.type === "arrow" && "elbowed" in element && element.elbowed;
|
|
301
|
+
}
|
|
302
|
+
var exponentWithoutFraction = /(^|[^\d.])\d[eE][+-]?\d/g;
|
|
303
|
+
function fits(element) {
|
|
304
|
+
const json = JSON.stringify(element);
|
|
305
|
+
return encoder.encode(json).byteLength + 2 * (json.match(exponentWithoutFraction)?.length ?? 0) <= ELEMENT_ENCODED_BYTES_LIMIT;
|
|
306
|
+
}
|
|
307
|
+
function roundCoordinate(value) {
|
|
308
|
+
return Number(value.toFixed(2));
|
|
309
|
+
}
|
|
310
|
+
function sampleValues(values, stride) {
|
|
311
|
+
const sampled = [];
|
|
312
|
+
for (let index = 0; index < values.length - 1; index += stride) {
|
|
313
|
+
const value = values[index];
|
|
314
|
+
if (value !== void 0) sampled.push(value);
|
|
315
|
+
}
|
|
316
|
+
const last = values.at(-1);
|
|
317
|
+
if (last !== void 0) sampled.push(last);
|
|
318
|
+
return sampled;
|
|
319
|
+
}
|
|
354
320
|
function fromWireElement(element) {
|
|
355
321
|
if (!isWireElement(element)) {
|
|
356
322
|
throw new Error("invalid whiteboard-v1 element");
|
|
@@ -419,12 +385,16 @@ function hasExactKeys(value, keys) {
|
|
|
419
385
|
}
|
|
420
386
|
|
|
421
387
|
// src/collab/engine.ts
|
|
422
|
-
var
|
|
388
|
+
var SUBMISSION_RETRY_DELAY_MS = 1e3;
|
|
389
|
+
var MAX_SUBMISSION_RETRY_DELAY_MS = 16e3;
|
|
423
390
|
var CHANGE_DEBOUNCE_MS = 150;
|
|
391
|
+
var CHANGE_MAX_WAIT_MS = 1e3;
|
|
424
392
|
var CURSOR_THROTTLE_MS = 16;
|
|
425
393
|
var CURSOR_STALE_MS = 1e4;
|
|
426
394
|
var asArray = (value) => Array.isArray(value) ? value : [];
|
|
427
395
|
var toReconcileRemoteElements = (elements) => elements;
|
|
396
|
+
var isNewerVersion = (element, previous) => element.version > previous.version || element.version === previous.version && element.versionNonce < previous.versionNonce;
|
|
397
|
+
var editingElementIds = (appState) => [appState.newElement?.id, appState.resizingElement?.id, appState.editingTextElement?.id];
|
|
428
398
|
var ExcalidrawCollabEngine = class {
|
|
429
399
|
constructor(opts) {
|
|
430
400
|
this.opts = opts;
|
|
@@ -432,10 +402,7 @@ var ExcalidrawCollabEngine = class {
|
|
|
432
402
|
this.filesSync = new WhiteboardFilesSync({
|
|
433
403
|
excalidrawAPI: opts.excalidrawAPI,
|
|
434
404
|
fileTransfer: opts.fileTransfer,
|
|
435
|
-
initiateUpload: opts.initiateUpload,
|
|
436
|
-
finalizeUpload: opts.finalizeUpload,
|
|
437
405
|
presignDownload: opts.presignDownload,
|
|
438
|
-
uploadThrottleMs: 300,
|
|
439
406
|
downloadThrottleMs: 500,
|
|
440
407
|
onStateChange: opts.onFileSyncStateChange
|
|
441
408
|
});
|
|
@@ -452,13 +419,17 @@ var ExcalidrawCollabEngine = class {
|
|
|
452
419
|
sceneId = null;
|
|
453
420
|
sceneGeneration = null;
|
|
454
421
|
canDraw = true;
|
|
455
|
-
|
|
422
|
+
storedImageFileIds = /* @__PURE__ */ new Set();
|
|
456
423
|
broadcastedElementVersions = /* @__PURE__ */ new Map();
|
|
424
|
+
rejectedElementVersions = /* @__PURE__ */ new Map();
|
|
425
|
+
deferredRemoteElements = /* @__PURE__ */ new Map();
|
|
457
426
|
submissionInFlight = false;
|
|
458
427
|
dirtyDuringSubmission = false;
|
|
459
428
|
disposed = false;
|
|
460
429
|
changeDebounce = null;
|
|
461
|
-
|
|
430
|
+
changeDebounceDeadline = null;
|
|
431
|
+
submissionRetryTimer = null;
|
|
432
|
+
submissionRetries = 0;
|
|
462
433
|
filesSync;
|
|
463
434
|
presence;
|
|
464
435
|
unsubPointerUp;
|
|
@@ -469,10 +440,13 @@ var ExcalidrawCollabEngine = class {
|
|
|
469
440
|
async clear() {
|
|
470
441
|
if (!this.canDraw) throw new Error("whiteboard drawing is not permitted");
|
|
471
442
|
const commit = await this.opts.clear();
|
|
443
|
+
this.storedImageFileIds.clear();
|
|
472
444
|
this.sceneId = commit.sceneId;
|
|
473
445
|
this.sceneGeneration = commit.sceneGeneration ?? null;
|
|
474
446
|
this.broadcastedElementVersions.clear();
|
|
475
|
-
this.
|
|
447
|
+
this.rejectedElementVersions.clear();
|
|
448
|
+
this.deferredRemoteElements.clear();
|
|
449
|
+
this.resetSubmissionRetries();
|
|
476
450
|
this.opts.excalidrawAPI.updateScene({
|
|
477
451
|
elements: [],
|
|
478
452
|
captureUpdate: CaptureUpdateAction2.NEVER
|
|
@@ -481,29 +455,36 @@ var ExcalidrawCollabEngine = class {
|
|
|
481
455
|
}
|
|
482
456
|
dispose() {
|
|
483
457
|
if (this.disposed) return;
|
|
484
|
-
const hasPendingChange = this.changeDebounce !== null || this.dirtyDuringSubmission;
|
|
458
|
+
const hasPendingChange = this.changeDebounce !== null || this.dirtyDuringSubmission || this.submissionRetryTimer !== null;
|
|
485
459
|
this.disposed = true;
|
|
486
|
-
|
|
487
|
-
this.changeDebounce = null;
|
|
460
|
+
this.clearChangeDebounce();
|
|
488
461
|
if (hasPendingChange) this.flushOnDispose();
|
|
489
|
-
if (this.
|
|
490
|
-
this.
|
|
462
|
+
if (this.submissionRetryTimer) clearTimeout(this.submissionRetryTimer);
|
|
463
|
+
this.submissionRetryTimer = null;
|
|
491
464
|
this.dirtyDuringSubmission = false;
|
|
465
|
+
this.deferredRemoteElements.clear();
|
|
492
466
|
this.filesSync.dispose();
|
|
493
467
|
this.presence.dispose();
|
|
494
468
|
this.unsubscribe();
|
|
495
469
|
this.unsubPointerUp?.();
|
|
496
470
|
}
|
|
497
|
-
handleChange(_elements, _appState,
|
|
471
|
+
handleChange(_elements, _appState, _files) {
|
|
498
472
|
if (this.disposed) return;
|
|
499
|
-
|
|
500
|
-
this.
|
|
473
|
+
this.applyDeferredRemoteElements();
|
|
474
|
+
this.stripUnstoredImages();
|
|
501
475
|
if (!this.canDraw) return;
|
|
502
|
-
|
|
503
|
-
this.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
476
|
+
const now = Date.now();
|
|
477
|
+
const deadline = this.changeDebounceDeadline ?? now + CHANGE_MAX_WAIT_MS;
|
|
478
|
+
const wait = Math.min(CHANGE_DEBOUNCE_MS, deadline - now);
|
|
479
|
+
this.clearChangeDebounce();
|
|
480
|
+
this.changeDebounceDeadline = deadline;
|
|
481
|
+
this.changeDebounce = setTimeout(
|
|
482
|
+
() => {
|
|
483
|
+
this.clearChangeDebounce();
|
|
484
|
+
this.flushNow(wait < CHANGE_DEBOUNCE_MS);
|
|
485
|
+
},
|
|
486
|
+
Math.max(0, wait)
|
|
487
|
+
);
|
|
507
488
|
}
|
|
508
489
|
handlePointerUpdate(payload) {
|
|
509
490
|
this.presence.handlePointerUpdate(payload);
|
|
@@ -561,49 +542,72 @@ var ExcalidrawCollabEngine = class {
|
|
|
561
542
|
isSnapshot: true
|
|
562
543
|
});
|
|
563
544
|
}
|
|
564
|
-
|
|
565
|
-
|
|
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
|
+
}
|
|
551
|
+
flushNow(skipEditing = false) {
|
|
552
|
+
if (this.disposed || this.submissionRetryTimer !== null) return;
|
|
566
553
|
const context = this.submissionContext();
|
|
567
554
|
if (!context) return;
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
const syncableAll = filterSyncableElements(context.elements);
|
|
555
|
+
const editing = skipEditing ? editingElementIds(this.opts.excalidrawAPI.getAppState()) : [];
|
|
556
|
+
const syncableAll = filterSyncableElements(retainStoredImages(context.elements, this.storedImageFileIds));
|
|
571
557
|
const delta = [];
|
|
572
558
|
for (const el of syncableAll) {
|
|
573
|
-
|
|
574
|
-
if (!prev || el.version > prev) delta.push(el);
|
|
559
|
+
if (this.isUnsubmitted(el) && !editing.includes(el.id)) delta.push(el);
|
|
575
560
|
}
|
|
576
561
|
if (delta.length === 0) return;
|
|
577
|
-
this.submit(context.sceneId, context.sceneGeneration ?? null,
|
|
562
|
+
this.submit(context.sceneId, context.sceneGeneration ?? null, delta);
|
|
578
563
|
}
|
|
579
564
|
flushOnDispose() {
|
|
580
565
|
const context = this.submissionContext({ allowDisposed: true, allowInFlight: true });
|
|
581
566
|
if (!context) return;
|
|
582
|
-
const
|
|
583
|
-
if (elementsHash === this.lastBroadcastedOrReceivedElementsHash) return;
|
|
584
|
-
const syncableAll = filterSyncableElements(context.elements);
|
|
567
|
+
const syncableAll = filterSyncableElements(retainStoredImages(context.elements, this.storedImageFileIds));
|
|
585
568
|
const delta = [];
|
|
586
569
|
for (const element of syncableAll) {
|
|
587
|
-
|
|
588
|
-
if (!previousVersion || element.version > previousVersion) delta.push(element);
|
|
570
|
+
if (this.isUnsubmitted(element)) delta.push(element);
|
|
589
571
|
}
|
|
590
572
|
if (delta.length === 0) return;
|
|
591
573
|
this.submitFinalUpdate(context.sceneId, context.sceneGeneration ?? null, delta);
|
|
592
574
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
575
|
+
isUnsubmitted(element) {
|
|
576
|
+
const rejected = this.rejectedElementVersions.get(element.id);
|
|
577
|
+
if (rejected?.version === element.version && rejected.versionNonce === element.versionNonce) return false;
|
|
578
|
+
const previous = this.broadcastedElementVersions.get(element.id);
|
|
579
|
+
return !previous || isNewerVersion(element, previous);
|
|
580
|
+
}
|
|
581
|
+
applyDeferredRemoteElements() {
|
|
582
|
+
if (!this.sceneId || this.deferredRemoteElements.size === 0) return;
|
|
583
|
+
const editing = editingElementIds(this.opts.excalidrawAPI.getAppState());
|
|
584
|
+
const elements = [];
|
|
585
|
+
for (const [id, element] of this.deferredRemoteElements) {
|
|
586
|
+
if (editing.includes(id)) continue;
|
|
587
|
+
this.deferredRemoteElements.delete(id);
|
|
588
|
+
elements.push(element);
|
|
589
|
+
}
|
|
590
|
+
if (elements.length === 0) return;
|
|
591
|
+
this.applyRemoteElements({ sceneId: this.sceneId, ...this.sceneGeneration ? { sceneGeneration: this.sceneGeneration } : {}, syncAll: false, remoteElements: elements, isSnapshot: false });
|
|
600
592
|
}
|
|
601
|
-
|
|
602
|
-
if (this.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
593
|
+
clearChangeDebounce() {
|
|
594
|
+
if (this.changeDebounce !== null) clearTimeout(this.changeDebounce);
|
|
595
|
+
this.changeDebounce = null;
|
|
596
|
+
this.changeDebounceDeadline = null;
|
|
597
|
+
}
|
|
598
|
+
resetSubmissionRetries() {
|
|
599
|
+
if (this.submissionRetryTimer !== null) clearTimeout(this.submissionRetryTimer);
|
|
600
|
+
this.submissionRetryTimer = null;
|
|
601
|
+
this.submissionRetries = 0;
|
|
602
|
+
}
|
|
603
|
+
scheduleSubmissionRetry() {
|
|
604
|
+
if (this.disposed || this.submissionRetryTimer !== null) return;
|
|
605
|
+
const delay = Math.min(SUBMISSION_RETRY_DELAY_MS * 2 ** this.submissionRetries, MAX_SUBMISSION_RETRY_DELAY_MS);
|
|
606
|
+
this.submissionRetries += 1;
|
|
607
|
+
this.submissionRetryTimer = setTimeout(() => {
|
|
608
|
+
this.submissionRetryTimer = null;
|
|
609
|
+
this.flushNow();
|
|
610
|
+
}, delay);
|
|
607
611
|
}
|
|
608
612
|
submissionContext(options = {}) {
|
|
609
613
|
if (this.disposed && !options.allowDisposed) return null;
|
|
@@ -622,41 +626,54 @@ var ExcalidrawCollabEngine = class {
|
|
|
622
626
|
elements: this.opts.excalidrawAPI.getSceneElementsIncludingDeleted()
|
|
623
627
|
};
|
|
624
628
|
}
|
|
625
|
-
submit(sceneId, sceneGeneration,
|
|
629
|
+
submit(sceneId, sceneGeneration, elements) {
|
|
630
|
+
const submittedElements = this.toSubmissionElements(elements);
|
|
631
|
+
if (submittedElements.length === 0) return;
|
|
626
632
|
this.submissionInFlight = true;
|
|
627
633
|
this.dirtyDuringSubmission = false;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
+
let acknowledged = false;
|
|
635
|
+
void Promise.resolve().then(
|
|
636
|
+
() => this.opts.submitUpdate({
|
|
637
|
+
sceneId,
|
|
638
|
+
...sceneGeneration ? { sceneGeneration } : {},
|
|
639
|
+
syncAll: false,
|
|
640
|
+
elements: submittedElements
|
|
641
|
+
})
|
|
642
|
+
).then((commit) => {
|
|
634
643
|
if (this.disposed) return;
|
|
635
644
|
if (commit.sceneId !== sceneId || this.sceneId !== sceneId || this.sceneGeneration !== sceneGeneration) {
|
|
636
645
|
this.requestSnapshot();
|
|
637
646
|
return;
|
|
638
647
|
}
|
|
639
|
-
for (const element of
|
|
640
|
-
this.broadcastedElementVersions.
|
|
648
|
+
for (const element of submittedElements) {
|
|
649
|
+
const previous = this.broadcastedElementVersions.get(element.id);
|
|
650
|
+
const submittedVersion = { version: element.version, versionNonce: element.version_nonce };
|
|
651
|
+
if (!previous || isNewerVersion(submittedVersion, previous)) this.broadcastedElementVersions.set(element.id, submittedVersion);
|
|
641
652
|
}
|
|
642
|
-
this.
|
|
643
|
-
|
|
644
|
-
}).catch(
|
|
653
|
+
this.submissionRetries = 0;
|
|
654
|
+
acknowledged = true;
|
|
655
|
+
}).catch((cause) => {
|
|
656
|
+
if (this.sceneId === sceneId && this.sceneGeneration === sceneGeneration) this.scheduleSubmissionRetry();
|
|
657
|
+
this.opts.onSubmissionError?.(cause);
|
|
658
|
+
}).finally(() => {
|
|
645
659
|
if (this.disposed) return;
|
|
646
660
|
this.submissionInFlight = false;
|
|
647
661
|
const shouldFlush = this.dirtyDuringSubmission;
|
|
648
662
|
this.dirtyDuringSubmission = false;
|
|
649
|
-
if (
|
|
663
|
+
if (shouldFlush) this.flushNow();
|
|
664
|
+
else if (acknowledged && this.changeDebounce === null) this.flushNow(true);
|
|
650
665
|
});
|
|
651
666
|
}
|
|
652
667
|
submitFinalUpdate(sceneId, sceneGeneration, elements) {
|
|
668
|
+
const submittedElements = this.toSubmissionElements(elements);
|
|
669
|
+
if (submittedElements.length === 0) return;
|
|
653
670
|
let submission;
|
|
654
671
|
try {
|
|
655
672
|
submission = this.opts.submitUpdate({
|
|
656
673
|
sceneId,
|
|
657
674
|
...sceneGeneration ? { sceneGeneration } : {},
|
|
658
675
|
syncAll: false,
|
|
659
|
-
elements:
|
|
676
|
+
elements: submittedElements
|
|
660
677
|
});
|
|
661
678
|
} catch (cause) {
|
|
662
679
|
this.opts.onSubmissionError?.(cause);
|
|
@@ -664,6 +681,20 @@ var ExcalidrawCollabEngine = class {
|
|
|
664
681
|
}
|
|
665
682
|
void submission.catch((cause) => this.opts.onSubmissionError?.(cause));
|
|
666
683
|
}
|
|
684
|
+
toSubmissionElements(elements) {
|
|
685
|
+
const submitted = [];
|
|
686
|
+
for (const element of elements) {
|
|
687
|
+
try {
|
|
688
|
+
submitted.push(toFittedWireElement(element));
|
|
689
|
+
this.rejectedElementVersions.delete(element.id);
|
|
690
|
+
} catch (cause) {
|
|
691
|
+
const reported = this.rejectedElementVersions.has(element.id);
|
|
692
|
+
this.rejectedElementVersions.set(element.id, { version: element.version, versionNonce: element.versionNonce });
|
|
693
|
+
if (!reported) this.opts.onSubmissionError?.(new Error("One whiteboard item could not be shared. If it is very large, make it smaller or split it.", { cause }));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return submitted;
|
|
697
|
+
}
|
|
667
698
|
applyRemoteElements(args) {
|
|
668
699
|
const remoteSceneId = args.sceneId;
|
|
669
700
|
const replacingScene = this.sceneId !== null && remoteSceneId !== this.sceneId;
|
|
@@ -673,43 +704,58 @@ var ExcalidrawCollabEngine = class {
|
|
|
673
704
|
if (remoteSceneId !== this.sceneId) {
|
|
674
705
|
this.sceneId = remoteSceneId;
|
|
675
706
|
this.broadcastedElementVersions.clear();
|
|
676
|
-
this.lastBroadcastedOrReceivedElementsHash = 0;
|
|
677
707
|
}
|
|
678
708
|
} else if (remoteSceneId !== this.sceneId) {
|
|
679
709
|
if (args.syncAll && asArray(args.remoteElements).length === 0) {
|
|
680
710
|
this.sceneId = remoteSceneId;
|
|
681
711
|
this.broadcastedElementVersions.clear();
|
|
682
|
-
this.lastBroadcastedOrReceivedElementsHash = 0;
|
|
683
712
|
} else {
|
|
684
713
|
this.requestSnapshot();
|
|
685
714
|
return;
|
|
686
715
|
}
|
|
687
716
|
}
|
|
688
717
|
if (replacingScene) {
|
|
689
|
-
|
|
690
|
-
this.
|
|
718
|
+
this.storedImageFileIds.clear();
|
|
719
|
+
this.rejectedElementVersions.clear();
|
|
720
|
+
this.clearChangeDebounce();
|
|
691
721
|
this.dirtyDuringSubmission = false;
|
|
692
722
|
this.sceneGeneration = null;
|
|
723
|
+
this.deferredRemoteElements.clear();
|
|
724
|
+
this.resetSubmissionRetries();
|
|
693
725
|
}
|
|
694
726
|
if (args.sceneGeneration) this.sceneGeneration = args.sceneGeneration;
|
|
695
727
|
const excalidrawAPI = this.opts.excalidrawAPI;
|
|
696
728
|
if (replacingScene) excalidrawAPI.history.clear();
|
|
697
729
|
const local = replacingScene ? [] : excalidrawAPI.getSceneElementsIncludingDeleted();
|
|
698
|
-
const remoteElements = args.remoteElements;
|
|
699
|
-
const restoredRemote = restoreElements(remoteElements,
|
|
730
|
+
const remoteElements = [...args.remoteElements].sort(compareElementOrder);
|
|
731
|
+
const restoredRemote = restoreElements(remoteElements, null);
|
|
732
|
+
this.storedImageFileIds = /* @__PURE__ */ new Set([...this.storedImageFileIds, ...remoteImageFileIds(restoredRemote)]);
|
|
700
733
|
const remoteForReconcile = toReconcileRemoteElements(restoredRemote);
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
this.
|
|
704
|
-
|
|
705
|
-
this.broadcastedElementVersions.
|
|
734
|
+
const appState = excalidrawAPI.getAppState();
|
|
735
|
+
const editing = editingElementIds(appState);
|
|
736
|
+
const reconciled = retainStoredImages(reconcileElements(local, remoteForReconcile, appState), this.storedImageFileIds);
|
|
737
|
+
if (args.isSnapshot) {
|
|
738
|
+
this.broadcastedElementVersions.clear();
|
|
739
|
+
this.resetSubmissionRetries();
|
|
740
|
+
}
|
|
741
|
+
const remoteById = new Map(restoredRemote.map((element) => [element.id, element]));
|
|
742
|
+
for (const element of reconciled) {
|
|
743
|
+
const remote = remoteById.get(element.id);
|
|
744
|
+
if (remote?.version === element.version && remote.versionNonce === element.versionNonce) {
|
|
745
|
+
this.broadcastedElementVersions.set(element.id, { version: element.version, versionNonce: element.versionNonce });
|
|
746
|
+
} else if (remote && editing.includes(element.id) && isNewerVersion(remote, element)) {
|
|
747
|
+
const previous = this.deferredRemoteElements.get(element.id);
|
|
748
|
+
if (!previous || isNewerVersion(remote, previous)) this.deferredRemoteElements.set(element.id, remote);
|
|
749
|
+
}
|
|
706
750
|
}
|
|
751
|
+
const hasUnsubmittedElements = reconciled.some((element) => this.isUnsubmitted(element));
|
|
707
752
|
excalidrawAPI.updateScene({
|
|
708
753
|
elements: reconciled,
|
|
709
754
|
appState: withLocalWhiteboardCamera(args.appState, excalidrawAPI.getAppState()),
|
|
710
755
|
captureUpdate: CaptureUpdateAction2.NEVER
|
|
711
756
|
});
|
|
712
757
|
this.filesSync.handleRemoteScene(reconciled);
|
|
758
|
+
if (args.isSnapshot && hasUnsubmittedElements) queueMicrotask(() => this.flushNow());
|
|
713
759
|
}
|
|
714
760
|
requestSnapshot() {
|
|
715
761
|
if (this.disposed) return;
|
|
@@ -721,6 +767,7 @@ var ExcalidrawCollabEngine = class {
|
|
|
721
767
|
};
|
|
722
768
|
|
|
723
769
|
export {
|
|
770
|
+
compareElementOrder,
|
|
724
771
|
toWireElement,
|
|
725
772
|
fromWireElement,
|
|
726
773
|
isWireElement,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compareElementOrder
|
|
3
|
+
} from "./chunk-6ZV7GBAC.js";
|
|
4
|
+
|
|
1
5
|
// src/collab/reducer.ts
|
|
2
6
|
function mergeWhiteboardElements(current, incoming) {
|
|
3
7
|
const merged = new Map(current.map((element) => [element.id, element]));
|
|
@@ -13,10 +17,6 @@ function wins(candidate, existing) {
|
|
|
13
17
|
if (candidate.version !== existing.version) return candidate.version > existing.version;
|
|
14
18
|
return candidate.version_nonce < existing.version_nonce;
|
|
15
19
|
}
|
|
16
|
-
function compareElementOrder(left, right) {
|
|
17
|
-
const byIndex = left.index.localeCompare(right.index);
|
|
18
|
-
return byIndex === 0 ? left.id.localeCompare(right.id) : byIndex;
|
|
19
|
-
}
|
|
20
20
|
|
|
21
21
|
export {
|
|
22
22
|
mergeWhiteboardElements
|