@q9labsai/chalk-whiteboard 4.1.15 → 4.1.17

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 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,20 @@
1
1
  // src/collab/engine.ts
2
- import { CaptureUpdateAction as CaptureUpdateAction2, hashElementsVersion, reconcileElements, restoreElements } from "@excalidraw/excalidraw";
2
+ import { CaptureUpdateAction as CaptureUpdateAction2, reconcileElements, restoreElements } from "@excalidraw/excalidraw";
3
+
4
+ // src/collab/camera.ts
5
+ function getWhiteboardCamera(appState) {
6
+ return {
7
+ scrollX: appState.scrollX,
8
+ scrollY: appState.scrollY,
9
+ zoom: appState.zoom
10
+ };
11
+ }
12
+ function withLocalWhiteboardCamera(sharedAppState, localAppState) {
13
+ return {
14
+ viewBackgroundColor: sharedAppState?.viewBackgroundColor ?? localAppState.viewBackgroundColor,
15
+ ...getWhiteboardCamera(localAppState)
16
+ };
17
+ }
3
18
 
4
19
  // src/collab/files.ts
5
20
  import { CaptureUpdateAction, MIME_TYPES, newElementWith } from "@excalidraw/excalidraw";
@@ -233,6 +248,15 @@ function requireOption(value, name) {
233
248
  return value;
234
249
  }
235
250
 
251
+ // src/collab/order.ts
252
+ function compareCodeUnits(left, right) {
253
+ if (left === right) return 0;
254
+ return left < right ? -1 : 1;
255
+ }
256
+ function compareElementOrder(left, right) {
257
+ return compareCodeUnits(left.index ?? "", right.index ?? "") || compareCodeUnits(left.id, right.id);
258
+ }
259
+
236
260
  // src/collab/presence.ts
237
261
  var CURSOR_COLORS = [
238
262
  { stroke: "#FF5D5D", background: "rgba(255, 93, 93, 0.2)" },
@@ -315,6 +339,8 @@ var filterSyncableElements = (elements) => elements.filter((el) => {
315
339
  });
316
340
 
317
341
  // src/collab/wire.ts
342
+ var ELEMENT_ENCODED_BYTES_LIMIT = 16384 - 64;
343
+ var encoder = new TextEncoder();
318
344
  var wireElementKeys = ["id", "type", "version", "version_nonce", "index", "is_deleted", "payload"];
319
345
  var wireElementValidators = [
320
346
  (value) => isNonEmptyString(value.id),
@@ -336,6 +362,47 @@ function toWireElement(element) {
336
362
  payload: toJsonRecord(element)
337
363
  };
338
364
  }
365
+ function toFittedWireElement(element) {
366
+ const wire = toWireElement(element);
367
+ if (fits(wire)) return wire;
368
+ const payload = fitPoints(element, wire);
369
+ if (!payload) throw new Error("Whiteboard element exceeds the whiteboard-v1 element size");
370
+ return { ...wire, payload };
371
+ }
372
+ function fitPoints(element, wire) {
373
+ if (!("points" in element) || isElbowArrow(element)) return void 0;
374
+ const points = element.points.map((point) => point.map(roundCoordinate));
375
+ const pressures = element.type === "freedraw" ? element.pressures.map(roundCoordinate) : void 0;
376
+ const sample = (stride) => ({ ...wire.payload, points: sampleValues(points, stride), ...pressures ? { pressures: sampleValues(pressures, stride) } : {} });
377
+ const lastStride = Math.max(1, points.length - 1);
378
+ if (!fits({ ...wire, payload: sample(lastStride) })) return void 0;
379
+ for (let stride = 1; stride < lastStride; stride++) {
380
+ const reduced = sample(stride);
381
+ if (fits({ ...wire, payload: reduced })) return reduced;
382
+ }
383
+ return sample(lastStride);
384
+ }
385
+ function isElbowArrow(element) {
386
+ return element.type === "arrow" && "elbowed" in element && element.elbowed;
387
+ }
388
+ var exponentWithoutFraction = /(^|[^\d.])\d[eE][+-]?\d/g;
389
+ function fits(element) {
390
+ const json = JSON.stringify(element);
391
+ return encoder.encode(json).byteLength + 2 * (json.match(exponentWithoutFraction)?.length ?? 0) <= ELEMENT_ENCODED_BYTES_LIMIT;
392
+ }
393
+ function roundCoordinate(value) {
394
+ return Number(value.toFixed(2));
395
+ }
396
+ function sampleValues(values, stride) {
397
+ const sampled = [];
398
+ for (let index = 0; index < values.length - 1; index += stride) {
399
+ const value = values[index];
400
+ if (value !== void 0) sampled.push(value);
401
+ }
402
+ const last = values.at(-1);
403
+ if (last !== void 0) sampled.push(last);
404
+ return sampled;
405
+ }
339
406
  function fromWireElement(element) {
340
407
  if (!isWireElement(element)) {
341
408
  throw new Error("invalid whiteboard-v1 element");
@@ -404,12 +471,16 @@ function hasExactKeys(value, keys) {
404
471
  }
405
472
 
406
473
  // src/collab/engine.ts
407
- var FULL_SYNC_INTERVAL_MS = 2e4;
474
+ var SUBMISSION_RETRY_DELAY_MS = 1e3;
475
+ var MAX_SUBMISSION_RETRY_DELAY_MS = 16e3;
408
476
  var CHANGE_DEBOUNCE_MS = 150;
477
+ var CHANGE_MAX_WAIT_MS = 1e3;
409
478
  var CURSOR_THROTTLE_MS = 16;
410
479
  var CURSOR_STALE_MS = 1e4;
411
480
  var asArray = (value) => Array.isArray(value) ? value : [];
412
481
  var toReconcileRemoteElements = (elements) => elements;
482
+ var isNewerVersion = (element, previous) => element.version > previous.version || element.version === previous.version && element.versionNonce < previous.versionNonce;
483
+ var editingElementIds = (appState) => [appState.newElement?.id, appState.resizingElement?.id, appState.editingTextElement?.id];
413
484
  var ExcalidrawCollabEngine = class {
414
485
  constructor(opts) {
415
486
  this.opts = opts;
@@ -437,13 +508,16 @@ var ExcalidrawCollabEngine = class {
437
508
  sceneId = null;
438
509
  sceneGeneration = null;
439
510
  canDraw = true;
440
- lastBroadcastedOrReceivedElementsHash = 0;
441
511
  broadcastedElementVersions = /* @__PURE__ */ new Map();
512
+ rejectedElementVersions = /* @__PURE__ */ new Map();
513
+ deferredRemoteElements = /* @__PURE__ */ new Map();
442
514
  submissionInFlight = false;
443
515
  dirtyDuringSubmission = false;
444
516
  disposed = false;
445
517
  changeDebounce = null;
446
- fullSyncTimer = null;
518
+ changeDebounceDeadline = null;
519
+ submissionRetryTimer = null;
520
+ submissionRetries = 0;
447
521
  filesSync;
448
522
  presence;
449
523
  unsubPointerUp;
@@ -457,7 +531,9 @@ var ExcalidrawCollabEngine = class {
457
531
  this.sceneId = commit.sceneId;
458
532
  this.sceneGeneration = commit.sceneGeneration ?? null;
459
533
  this.broadcastedElementVersions.clear();
460
- this.lastBroadcastedOrReceivedElementsHash = 0;
534
+ this.rejectedElementVersions.clear();
535
+ this.deferredRemoteElements.clear();
536
+ this.resetSubmissionRetries();
461
537
  this.opts.excalidrawAPI.updateScene({
462
538
  elements: [],
463
539
  captureUpdate: CaptureUpdateAction2.NEVER
@@ -466,14 +542,14 @@ var ExcalidrawCollabEngine = class {
466
542
  }
467
543
  dispose() {
468
544
  if (this.disposed) return;
469
- const hasPendingChange = this.changeDebounce !== null || this.dirtyDuringSubmission;
545
+ const hasPendingChange = this.changeDebounce !== null || this.dirtyDuringSubmission || this.submissionRetryTimer !== null;
470
546
  this.disposed = true;
471
- if (this.changeDebounce) clearTimeout(this.changeDebounce);
472
- this.changeDebounce = null;
547
+ this.clearChangeDebounce();
473
548
  if (hasPendingChange) this.flushOnDispose();
474
- if (this.fullSyncTimer) clearTimeout(this.fullSyncTimer);
475
- this.fullSyncTimer = null;
549
+ if (this.submissionRetryTimer) clearTimeout(this.submissionRetryTimer);
550
+ this.submissionRetryTimer = null;
476
551
  this.dirtyDuringSubmission = false;
552
+ this.deferredRemoteElements.clear();
477
553
  this.filesSync.dispose();
478
554
  this.presence.dispose();
479
555
  this.unsubscribe();
@@ -481,14 +557,22 @@ var ExcalidrawCollabEngine = class {
481
557
  }
482
558
  handleChange(_elements, _appState, files) {
483
559
  if (this.disposed) return;
560
+ this.applyDeferredRemoteElements();
484
561
  const elementsAll = this.opts.excalidrawAPI.getSceneElementsIncludingDeleted();
485
562
  this.filesSync.handleLocalScene(elementsAll, files);
486
563
  if (!this.canDraw) return;
487
- if (this.changeDebounce) clearTimeout(this.changeDebounce);
488
- this.changeDebounce = setTimeout(() => {
489
- this.changeDebounce = null;
490
- this.flushNow();
491
- }, CHANGE_DEBOUNCE_MS);
564
+ const now = Date.now();
565
+ const deadline = this.changeDebounceDeadline ?? now + CHANGE_MAX_WAIT_MS;
566
+ const wait = Math.min(CHANGE_DEBOUNCE_MS, deadline - now);
567
+ this.clearChangeDebounce();
568
+ this.changeDebounceDeadline = deadline;
569
+ this.changeDebounce = setTimeout(
570
+ () => {
571
+ this.clearChangeDebounce();
572
+ this.flushNow(wait < CHANGE_DEBOUNCE_MS);
573
+ },
574
+ Math.max(0, wait)
575
+ );
492
576
  }
493
577
  handlePointerUpdate(payload) {
494
578
  this.presence.handlePointerUpdate(payload);
@@ -546,49 +630,66 @@ var ExcalidrawCollabEngine = class {
546
630
  isSnapshot: true
547
631
  });
548
632
  }
549
- flushNow() {
550
- if (this.disposed) return;
633
+ flushNow(skipEditing = false) {
634
+ if (this.disposed || this.submissionRetryTimer !== null) return;
551
635
  const context = this.submissionContext();
552
636
  if (!context) return;
553
- const elementsHash = hashElementsVersion(context.elements);
554
- if (elementsHash === this.lastBroadcastedOrReceivedElementsHash) return;
637
+ const editing = skipEditing ? editingElementIds(this.opts.excalidrawAPI.getAppState()) : [];
555
638
  const syncableAll = filterSyncableElements(context.elements);
556
639
  const delta = [];
557
640
  for (const el of syncableAll) {
558
- const prev = this.broadcastedElementVersions.get(el.id) ?? 0;
559
- if (!prev || el.version > prev) delta.push(el);
641
+ if (this.isUnsubmitted(el) && !editing.includes(el.id)) delta.push(el);
560
642
  }
561
643
  if (delta.length === 0) return;
562
- this.submit(context.sceneId, context.sceneGeneration ?? null, false, delta, elementsHash);
644
+ this.submit(context.sceneId, context.sceneGeneration ?? null, delta);
563
645
  }
564
646
  flushOnDispose() {
565
647
  const context = this.submissionContext({ allowDisposed: true, allowInFlight: true });
566
648
  if (!context) return;
567
- const elementsHash = hashElementsVersion(context.elements);
568
- if (elementsHash === this.lastBroadcastedOrReceivedElementsHash) return;
569
649
  const syncableAll = filterSyncableElements(context.elements);
570
650
  const delta = [];
571
651
  for (const element of syncableAll) {
572
- const previousVersion = this.broadcastedElementVersions.get(element.id) ?? 0;
573
- if (!previousVersion || element.version > previousVersion) delta.push(element);
652
+ if (this.isUnsubmitted(element)) delta.push(element);
574
653
  }
575
654
  if (delta.length === 0) return;
576
655
  this.submitFinalUpdate(context.sceneId, context.sceneGeneration ?? null, delta);
577
656
  }
578
- scheduleFullSync() {
579
- if (this.disposed || this.fullSyncTimer) return;
580
- this.fullSyncTimer = setTimeout(() => {
581
- this.fullSyncTimer = null;
582
- if (this.disposed) return;
583
- this.sendFullSync();
584
- }, FULL_SYNC_INTERVAL_MS);
657
+ isUnsubmitted(element) {
658
+ const rejected = this.rejectedElementVersions.get(element.id);
659
+ if (rejected?.version === element.version && rejected.versionNonce === element.versionNonce) return false;
660
+ const previous = this.broadcastedElementVersions.get(element.id);
661
+ return !previous || isNewerVersion(element, previous);
662
+ }
663
+ applyDeferredRemoteElements() {
664
+ if (!this.sceneId || this.deferredRemoteElements.size === 0) return;
665
+ const editing = editingElementIds(this.opts.excalidrawAPI.getAppState());
666
+ const elements = [];
667
+ for (const [id, element] of this.deferredRemoteElements) {
668
+ if (editing.includes(id)) continue;
669
+ this.deferredRemoteElements.delete(id);
670
+ elements.push(element);
671
+ }
672
+ if (elements.length === 0) return;
673
+ this.applyRemoteElements({ sceneId: this.sceneId, ...this.sceneGeneration ? { sceneGeneration: this.sceneGeneration } : {}, syncAll: false, remoteElements: elements, isSnapshot: false });
585
674
  }
586
- sendFullSync() {
587
- if (this.disposed) return;
588
- const context = this.submissionContext();
589
- if (!context) return;
590
- const syncableAll = filterSyncableElements(context.elements);
591
- this.submit(context.sceneId, context.sceneGeneration ?? null, true, syncableAll, hashElementsVersion(context.elements));
675
+ clearChangeDebounce() {
676
+ if (this.changeDebounce !== null) clearTimeout(this.changeDebounce);
677
+ this.changeDebounce = null;
678
+ this.changeDebounceDeadline = null;
679
+ }
680
+ resetSubmissionRetries() {
681
+ if (this.submissionRetryTimer !== null) clearTimeout(this.submissionRetryTimer);
682
+ this.submissionRetryTimer = null;
683
+ this.submissionRetries = 0;
684
+ }
685
+ scheduleSubmissionRetry() {
686
+ if (this.disposed || this.submissionRetryTimer !== null) return;
687
+ const delay = Math.min(SUBMISSION_RETRY_DELAY_MS * 2 ** this.submissionRetries, MAX_SUBMISSION_RETRY_DELAY_MS);
688
+ this.submissionRetries += 1;
689
+ this.submissionRetryTimer = setTimeout(() => {
690
+ this.submissionRetryTimer = null;
691
+ this.flushNow();
692
+ }, delay);
592
693
  }
593
694
  submissionContext(options = {}) {
594
695
  if (this.disposed && !options.allowDisposed) return null;
@@ -607,41 +708,54 @@ var ExcalidrawCollabEngine = class {
607
708
  elements: this.opts.excalidrawAPI.getSceneElementsIncludingDeleted()
608
709
  };
609
710
  }
610
- submit(sceneId, sceneGeneration, syncAll, elements, elementsHash) {
711
+ submit(sceneId, sceneGeneration, elements) {
712
+ const submittedElements = this.toSubmissionElements(elements);
713
+ if (submittedElements.length === 0) return;
611
714
  this.submissionInFlight = true;
612
715
  this.dirtyDuringSubmission = false;
613
- void this.opts.submitUpdate({
614
- sceneId,
615
- ...sceneGeneration ? { sceneGeneration } : {},
616
- syncAll,
617
- elements: elements.map(toWireElement)
618
- }).then((commit) => {
716
+ let acknowledged = false;
717
+ void Promise.resolve().then(
718
+ () => this.opts.submitUpdate({
719
+ sceneId,
720
+ ...sceneGeneration ? { sceneGeneration } : {},
721
+ syncAll: false,
722
+ elements: submittedElements
723
+ })
724
+ ).then((commit) => {
619
725
  if (this.disposed) return;
620
726
  if (commit.sceneId !== sceneId || this.sceneId !== sceneId || this.sceneGeneration !== sceneGeneration) {
621
727
  this.requestSnapshot();
622
728
  return;
623
729
  }
624
- for (const element of elements) {
625
- this.broadcastedElementVersions.set(element.id, element.version);
730
+ for (const element of submittedElements) {
731
+ const previous = this.broadcastedElementVersions.get(element.id);
732
+ const submittedVersion = { version: element.version, versionNonce: element.version_nonce };
733
+ if (!previous || isNewerVersion(submittedVersion, previous)) this.broadcastedElementVersions.set(element.id, submittedVersion);
626
734
  }
627
- this.lastBroadcastedOrReceivedElementsHash = elementsHash;
628
- this.scheduleFullSync();
629
- }).catch(this.opts.onSubmissionError).finally(() => {
735
+ this.submissionRetries = 0;
736
+ acknowledged = true;
737
+ }).catch((cause) => {
738
+ if (this.sceneId === sceneId && this.sceneGeneration === sceneGeneration) this.scheduleSubmissionRetry();
739
+ this.opts.onSubmissionError?.(cause);
740
+ }).finally(() => {
630
741
  if (this.disposed) return;
631
742
  this.submissionInFlight = false;
632
743
  const shouldFlush = this.dirtyDuringSubmission;
633
744
  this.dirtyDuringSubmission = false;
634
- if (!this.disposed && shouldFlush) this.flushNow();
745
+ if (shouldFlush) this.flushNow();
746
+ else if (acknowledged && this.changeDebounce === null) this.flushNow(true);
635
747
  });
636
748
  }
637
749
  submitFinalUpdate(sceneId, sceneGeneration, elements) {
750
+ const submittedElements = this.toSubmissionElements(elements);
751
+ if (submittedElements.length === 0) return;
638
752
  let submission;
639
753
  try {
640
754
  submission = this.opts.submitUpdate({
641
755
  sceneId,
642
756
  ...sceneGeneration ? { sceneGeneration } : {},
643
757
  syncAll: false,
644
- elements: elements.map(toWireElement)
758
+ elements: submittedElements
645
759
  });
646
760
  } catch (cause) {
647
761
  this.opts.onSubmissionError?.(cause);
@@ -649,44 +763,79 @@ var ExcalidrawCollabEngine = class {
649
763
  }
650
764
  void submission.catch((cause) => this.opts.onSubmissionError?.(cause));
651
765
  }
766
+ toSubmissionElements(elements) {
767
+ const submitted = [];
768
+ for (const element of elements) {
769
+ try {
770
+ submitted.push(toFittedWireElement(element));
771
+ this.rejectedElementVersions.delete(element.id);
772
+ } catch (cause) {
773
+ const reported = this.rejectedElementVersions.has(element.id);
774
+ this.rejectedElementVersions.set(element.id, { version: element.version, versionNonce: element.versionNonce });
775
+ 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 }));
776
+ }
777
+ }
778
+ return submitted;
779
+ }
652
780
  applyRemoteElements(args) {
653
781
  const remoteSceneId = args.sceneId;
782
+ const replacingScene = this.sceneId !== null && remoteSceneId !== this.sceneId;
654
783
  if (!this.sceneId) {
655
784
  this.sceneId = remoteSceneId;
656
785
  } else if (args.isSnapshot) {
657
786
  if (remoteSceneId !== this.sceneId) {
658
787
  this.sceneId = remoteSceneId;
659
788
  this.broadcastedElementVersions.clear();
660
- this.lastBroadcastedOrReceivedElementsHash = 0;
661
789
  }
662
790
  } else if (remoteSceneId !== this.sceneId) {
663
791
  if (args.syncAll && asArray(args.remoteElements).length === 0) {
664
792
  this.sceneId = remoteSceneId;
665
793
  this.broadcastedElementVersions.clear();
666
- this.lastBroadcastedOrReceivedElementsHash = 0;
667
794
  } else {
668
795
  this.requestSnapshot();
669
796
  return;
670
797
  }
671
798
  }
799
+ if (replacingScene) {
800
+ this.rejectedElementVersions.clear();
801
+ this.clearChangeDebounce();
802
+ this.dirtyDuringSubmission = false;
803
+ this.sceneGeneration = null;
804
+ this.deferredRemoteElements.clear();
805
+ this.resetSubmissionRetries();
806
+ }
672
807
  if (args.sceneGeneration) this.sceneGeneration = args.sceneGeneration;
673
808
  const excalidrawAPI = this.opts.excalidrawAPI;
674
- const local = excalidrawAPI.getSceneElementsIncludingDeleted();
675
- const remoteElements = args.remoteElements;
676
- const restoredRemote = restoreElements(remoteElements, local);
809
+ if (replacingScene) excalidrawAPI.history.clear();
810
+ const local = replacingScene ? [] : excalidrawAPI.getSceneElementsIncludingDeleted();
811
+ const remoteElements = [...args.remoteElements].sort(compareElementOrder);
812
+ const restoredRemote = restoreElements(remoteElements, null);
677
813
  const remoteForReconcile = toReconcileRemoteElements(restoredRemote);
678
- const reconciled = reconcileElements(local, remoteForReconcile, excalidrawAPI.getAppState());
679
- this.lastBroadcastedOrReceivedElementsHash = hashElementsVersion(reconciled);
680
- this.broadcastedElementVersions.clear();
681
- for (const el of reconciled) {
682
- this.broadcastedElementVersions.set(el.id, el.version);
814
+ const appState = excalidrawAPI.getAppState();
815
+ const editing = editingElementIds(appState);
816
+ const reconciled = reconcileElements(local, remoteForReconcile, appState);
817
+ if (args.isSnapshot) {
818
+ this.broadcastedElementVersions.clear();
819
+ this.resetSubmissionRetries();
820
+ }
821
+ const remoteById = new Map(restoredRemote.map((element) => [element.id, element]));
822
+ for (const element of reconciled) {
823
+ const remote = remoteById.get(element.id);
824
+ if (remote?.version === element.version && remote.versionNonce === element.versionNonce) {
825
+ this.broadcastedElementVersions.set(element.id, { version: element.version, versionNonce: element.versionNonce });
826
+ } else if (remote && editing.includes(element.id) && isNewerVersion(remote, element)) {
827
+ const previous = this.deferredRemoteElements.get(element.id);
828
+ if (!previous || isNewerVersion(remote, previous)) this.deferredRemoteElements.set(element.id, remote);
829
+ }
683
830
  }
831
+ const hasUnsubmittedElements = reconciled.some((element) => this.isUnsubmitted(element));
684
832
  excalidrawAPI.updateScene({
685
833
  elements: reconciled,
686
- appState: args.appState,
834
+ appState: withLocalWhiteboardCamera(args.appState, excalidrawAPI.getAppState()),
687
835
  captureUpdate: CaptureUpdateAction2.NEVER
688
836
  });
689
837
  this.filesSync.handleRemoteScene(reconciled);
838
+ if (args.isSnapshot && hasUnsubmittedElements) queueMicrotask(() => this.flushNow());
690
839
  }
691
840
  requestSnapshot() {
692
841
  if (this.disposed) return;
@@ -698,6 +847,7 @@ var ExcalidrawCollabEngine = class {
698
847
  };
699
848
 
700
849
  export {
850
+ compareElementOrder,
701
851
  toWireElement,
702
852
  fromWireElement,
703
853
  isWireElement,
@@ -1,3 +1,7 @@
1
+ import {
2
+ compareElementOrder
3
+ } from "./chunk-6Q65ADBK.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
@@ -0,0 +1,8 @@
1
+ import type { AppState } from "./types.js";
2
+ export type WhiteboardCamera = Pick<AppState, "scrollX" | "scrollY" | "zoom">;
3
+ export type SharedWhiteboardAppState = {
4
+ readonly viewBackgroundColor?: string;
5
+ };
6
+ export type WhiteboardViewportAppState = Pick<AppState, "scrollX" | "scrollY" | "zoom" | "viewBackgroundColor">;
7
+ export declare function getWhiteboardCamera(appState: WhiteboardCamera): WhiteboardCamera;
8
+ export declare function withLocalWhiteboardCamera(sharedAppState: SharedWhiteboardAppState | undefined, localAppState: WhiteboardViewportAppState): WhiteboardViewportAppState;
@@ -1,3 +1,4 @@
1
+ import { type SharedWhiteboardAppState } from "./camera.js";
1
2
  import type { WhiteboardFileSyncState, WhiteboardFileTransferOptions } from "./files.js";
2
3
  import type { AppState, BinaryFiles, ExcalidrawImperativeAPI, OrderedExcalidrawElement } from "./types.js";
3
4
  import { type WhiteboardCommit, type WhiteboardWireElement } from "./wire.js";
@@ -7,9 +8,7 @@ export type WhiteboardCollaborationEvent = {
7
8
  readonly revision: string;
8
9
  readonly sceneGeneration?: string;
9
10
  readonly elements: readonly WhiteboardWireElement[];
10
- readonly appState?: {
11
- readonly viewBackgroundColor?: string;
12
- };
11
+ readonly appState?: SharedWhiteboardAppState;
13
12
  } | {
14
13
  readonly type: "update";
15
14
  readonly sceneId: string;
@@ -52,13 +51,16 @@ export declare class ExcalidrawCollabEngine {
52
51
  private sceneId;
53
52
  private sceneGeneration;
54
53
  private canDraw;
55
- private lastBroadcastedOrReceivedElementsHash;
56
54
  private broadcastedElementVersions;
55
+ private rejectedElementVersions;
56
+ private deferredRemoteElements;
57
57
  private submissionInFlight;
58
58
  private dirtyDuringSubmission;
59
59
  private disposed;
60
60
  private changeDebounce;
61
- private fullSyncTimer;
61
+ private changeDebounceDeadline;
62
+ private submissionRetryTimer;
63
+ private submissionRetries;
62
64
  private readonly filesSync;
63
65
  private readonly presence;
64
66
  private readonly unsubPointerUp;
@@ -92,15 +94,19 @@ export declare class ExcalidrawCollabEngine {
92
94
  sceneId: string;
93
95
  sceneGeneration?: string;
94
96
  elements: readonly WhiteboardWireElement[];
95
- appState?: AppState;
97
+ appState?: SharedWhiteboardAppState;
96
98
  }): void;
97
99
  private flushNow;
98
100
  private flushOnDispose;
99
- private scheduleFullSync;
100
- private sendFullSync;
101
+ private isUnsubmitted;
102
+ private applyDeferredRemoteElements;
103
+ private clearChangeDebounce;
104
+ private resetSubmissionRetries;
105
+ private scheduleSubmissionRetry;
101
106
  private submissionContext;
102
107
  private submit;
103
108
  private submitFinalUpdate;
109
+ private toSubmissionElements;
104
110
  private applyRemoteElements;
105
111
  private requestSnapshot;
106
112
  }
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  mergeWhiteboardElements
3
- } from "../chunk-4XG2ABSP.js";
3
+ } from "../chunk-EPSMA2DY.js";
4
4
  import {
5
5
  ExcalidrawCollabEngine,
6
6
  fromWireElement,
7
7
  isWireElement,
8
8
  toWireElement
9
- } from "../chunk-GCF4VS3K.js";
9
+ } from "../chunk-6Q65ADBK.js";
10
10
  export {
11
11
  ExcalidrawCollabEngine,
12
12
  fromWireElement,
@@ -0,0 +1,6 @@
1
+ type OrderedElement = {
2
+ readonly index: string | null;
3
+ readonly id: string;
4
+ };
5
+ export declare function compareElementOrder(left: OrderedElement, right: OrderedElement): number;
6
+ export {};
@@ -1,4 +1,5 @@
1
1
  import type { OrderedExcalidrawElement } from "./types.js";
2
+ export declare const ELEMENT_ENCODED_BYTES_LIMIT: number;
2
3
  export type WhiteboardJsonValue = null | boolean | number | string | readonly WhiteboardJsonValue[] | {
3
4
  readonly [key: string]: WhiteboardJsonValue;
4
5
  };
@@ -25,5 +26,10 @@ export interface WhiteboardUploadInstructions {
25
26
  readonly expiresAt: string;
26
27
  }
27
28
  export declare function toWireElement(element: OrderedExcalidrawElement): WhiteboardWireElement;
29
+ /**
30
+ * The element as the engine submits it. It equals `toWireElement` while the element fits the size that Sync stores.
31
+ * A larger element gets its points rounded and, when that is not enough, thinned. Throws when no copy fits.
32
+ */
33
+ export declare function toFittedWireElement(element: OrderedExcalidrawElement): WhiteboardWireElement;
28
34
  export declare function fromWireElement(element: WhiteboardWireElement): OrderedExcalidrawElement;
29
35
  export declare function isWireElement(value: unknown): value is WhiteboardWireElement;
@@ -1216,8 +1216,8 @@
1216
1216
  },
1217
1217
  {
1218
1218
  "path": "renderer.js",
1219
- "bytes": 8499347,
1220
- "sha256": "8fb16da140966885912fd55616156b73e24dafc94e7dd0996e169d202714281a"
1219
+ "bytes": 8504846,
1220
+ "sha256": "20644a73c951091c36858684c83bd579c6894f986bf461cf21289f0ccef4ea59"
1221
1221
  },
1222
1222
  {
1223
1223
  "path": "THIRD_PARTY_NOTICES.json",