@q9labsai/chalk-whiteboard 4.1.16 → 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,5 @@
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
3
 
4
4
  // src/collab/camera.ts
5
5
  function getWhiteboardCamera(appState) {
@@ -248,6 +248,15 @@ function requireOption(value, name) {
248
248
  return value;
249
249
  }
250
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
+
251
260
  // src/collab/presence.ts
252
261
  var CURSOR_COLORS = [
253
262
  { stroke: "#FF5D5D", background: "rgba(255, 93, 93, 0.2)" },
@@ -330,6 +339,8 @@ var filterSyncableElements = (elements) => elements.filter((el) => {
330
339
  });
331
340
 
332
341
  // src/collab/wire.ts
342
+ var ELEMENT_ENCODED_BYTES_LIMIT = 16384 - 64;
343
+ var encoder = new TextEncoder();
333
344
  var wireElementKeys = ["id", "type", "version", "version_nonce", "index", "is_deleted", "payload"];
334
345
  var wireElementValidators = [
335
346
  (value) => isNonEmptyString(value.id),
@@ -351,6 +362,47 @@ function toWireElement(element) {
351
362
  payload: toJsonRecord(element)
352
363
  };
353
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
+ }
354
406
  function fromWireElement(element) {
355
407
  if (!isWireElement(element)) {
356
408
  throw new Error("invalid whiteboard-v1 element");
@@ -419,12 +471,16 @@ function hasExactKeys(value, keys) {
419
471
  }
420
472
 
421
473
  // src/collab/engine.ts
422
- var FULL_SYNC_INTERVAL_MS = 2e4;
474
+ var SUBMISSION_RETRY_DELAY_MS = 1e3;
475
+ var MAX_SUBMISSION_RETRY_DELAY_MS = 16e3;
423
476
  var CHANGE_DEBOUNCE_MS = 150;
477
+ var CHANGE_MAX_WAIT_MS = 1e3;
424
478
  var CURSOR_THROTTLE_MS = 16;
425
479
  var CURSOR_STALE_MS = 1e4;
426
480
  var asArray = (value) => Array.isArray(value) ? value : [];
427
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];
428
484
  var ExcalidrawCollabEngine = class {
429
485
  constructor(opts) {
430
486
  this.opts = opts;
@@ -452,13 +508,16 @@ var ExcalidrawCollabEngine = class {
452
508
  sceneId = null;
453
509
  sceneGeneration = null;
454
510
  canDraw = true;
455
- lastBroadcastedOrReceivedElementsHash = 0;
456
511
  broadcastedElementVersions = /* @__PURE__ */ new Map();
512
+ rejectedElementVersions = /* @__PURE__ */ new Map();
513
+ deferredRemoteElements = /* @__PURE__ */ new Map();
457
514
  submissionInFlight = false;
458
515
  dirtyDuringSubmission = false;
459
516
  disposed = false;
460
517
  changeDebounce = null;
461
- fullSyncTimer = null;
518
+ changeDebounceDeadline = null;
519
+ submissionRetryTimer = null;
520
+ submissionRetries = 0;
462
521
  filesSync;
463
522
  presence;
464
523
  unsubPointerUp;
@@ -472,7 +531,9 @@ var ExcalidrawCollabEngine = class {
472
531
  this.sceneId = commit.sceneId;
473
532
  this.sceneGeneration = commit.sceneGeneration ?? null;
474
533
  this.broadcastedElementVersions.clear();
475
- this.lastBroadcastedOrReceivedElementsHash = 0;
534
+ this.rejectedElementVersions.clear();
535
+ this.deferredRemoteElements.clear();
536
+ this.resetSubmissionRetries();
476
537
  this.opts.excalidrawAPI.updateScene({
477
538
  elements: [],
478
539
  captureUpdate: CaptureUpdateAction2.NEVER
@@ -481,14 +542,14 @@ var ExcalidrawCollabEngine = class {
481
542
  }
482
543
  dispose() {
483
544
  if (this.disposed) return;
484
- const hasPendingChange = this.changeDebounce !== null || this.dirtyDuringSubmission;
545
+ const hasPendingChange = this.changeDebounce !== null || this.dirtyDuringSubmission || this.submissionRetryTimer !== null;
485
546
  this.disposed = true;
486
- if (this.changeDebounce) clearTimeout(this.changeDebounce);
487
- this.changeDebounce = null;
547
+ this.clearChangeDebounce();
488
548
  if (hasPendingChange) this.flushOnDispose();
489
- if (this.fullSyncTimer) clearTimeout(this.fullSyncTimer);
490
- this.fullSyncTimer = null;
549
+ if (this.submissionRetryTimer) clearTimeout(this.submissionRetryTimer);
550
+ this.submissionRetryTimer = null;
491
551
  this.dirtyDuringSubmission = false;
552
+ this.deferredRemoteElements.clear();
492
553
  this.filesSync.dispose();
493
554
  this.presence.dispose();
494
555
  this.unsubscribe();
@@ -496,14 +557,22 @@ var ExcalidrawCollabEngine = class {
496
557
  }
497
558
  handleChange(_elements, _appState, files) {
498
559
  if (this.disposed) return;
560
+ this.applyDeferredRemoteElements();
499
561
  const elementsAll = this.opts.excalidrawAPI.getSceneElementsIncludingDeleted();
500
562
  this.filesSync.handleLocalScene(elementsAll, files);
501
563
  if (!this.canDraw) return;
502
- if (this.changeDebounce) clearTimeout(this.changeDebounce);
503
- this.changeDebounce = setTimeout(() => {
504
- this.changeDebounce = null;
505
- this.flushNow();
506
- }, 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
+ );
507
576
  }
508
577
  handlePointerUpdate(payload) {
509
578
  this.presence.handlePointerUpdate(payload);
@@ -561,49 +630,66 @@ var ExcalidrawCollabEngine = class {
561
630
  isSnapshot: true
562
631
  });
563
632
  }
564
- flushNow() {
565
- if (this.disposed) return;
633
+ flushNow(skipEditing = false) {
634
+ if (this.disposed || this.submissionRetryTimer !== null) return;
566
635
  const context = this.submissionContext();
567
636
  if (!context) return;
568
- const elementsHash = hashElementsVersion(context.elements);
569
- if (elementsHash === this.lastBroadcastedOrReceivedElementsHash) return;
637
+ const editing = skipEditing ? editingElementIds(this.opts.excalidrawAPI.getAppState()) : [];
570
638
  const syncableAll = filterSyncableElements(context.elements);
571
639
  const delta = [];
572
640
  for (const el of syncableAll) {
573
- const prev = this.broadcastedElementVersions.get(el.id) ?? 0;
574
- if (!prev || el.version > prev) delta.push(el);
641
+ if (this.isUnsubmitted(el) && !editing.includes(el.id)) delta.push(el);
575
642
  }
576
643
  if (delta.length === 0) return;
577
- this.submit(context.sceneId, context.sceneGeneration ?? null, false, delta, elementsHash);
644
+ this.submit(context.sceneId, context.sceneGeneration ?? null, delta);
578
645
  }
579
646
  flushOnDispose() {
580
647
  const context = this.submissionContext({ allowDisposed: true, allowInFlight: true });
581
648
  if (!context) return;
582
- const elementsHash = hashElementsVersion(context.elements);
583
- if (elementsHash === this.lastBroadcastedOrReceivedElementsHash) return;
584
649
  const syncableAll = filterSyncableElements(context.elements);
585
650
  const delta = [];
586
651
  for (const element of syncableAll) {
587
- const previousVersion = this.broadcastedElementVersions.get(element.id) ?? 0;
588
- if (!previousVersion || element.version > previousVersion) delta.push(element);
652
+ if (this.isUnsubmitted(element)) delta.push(element);
589
653
  }
590
654
  if (delta.length === 0) return;
591
655
  this.submitFinalUpdate(context.sceneId, context.sceneGeneration ?? null, delta);
592
656
  }
593
- scheduleFullSync() {
594
- if (this.disposed || this.fullSyncTimer) return;
595
- this.fullSyncTimer = setTimeout(() => {
596
- this.fullSyncTimer = null;
597
- if (this.disposed) return;
598
- this.sendFullSync();
599
- }, 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 });
600
674
  }
601
- sendFullSync() {
602
- if (this.disposed) return;
603
- const context = this.submissionContext();
604
- if (!context) return;
605
- const syncableAll = filterSyncableElements(context.elements);
606
- 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);
607
693
  }
608
694
  submissionContext(options = {}) {
609
695
  if (this.disposed && !options.allowDisposed) return null;
@@ -622,41 +708,54 @@ var ExcalidrawCollabEngine = class {
622
708
  elements: this.opts.excalidrawAPI.getSceneElementsIncludingDeleted()
623
709
  };
624
710
  }
625
- submit(sceneId, sceneGeneration, syncAll, elements, elementsHash) {
711
+ submit(sceneId, sceneGeneration, elements) {
712
+ const submittedElements = this.toSubmissionElements(elements);
713
+ if (submittedElements.length === 0) return;
626
714
  this.submissionInFlight = true;
627
715
  this.dirtyDuringSubmission = false;
628
- void this.opts.submitUpdate({
629
- sceneId,
630
- ...sceneGeneration ? { sceneGeneration } : {},
631
- syncAll,
632
- elements: elements.map(toWireElement)
633
- }).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) => {
634
725
  if (this.disposed) return;
635
726
  if (commit.sceneId !== sceneId || this.sceneId !== sceneId || this.sceneGeneration !== sceneGeneration) {
636
727
  this.requestSnapshot();
637
728
  return;
638
729
  }
639
- for (const element of elements) {
640
- 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);
641
734
  }
642
- this.lastBroadcastedOrReceivedElementsHash = elementsHash;
643
- this.scheduleFullSync();
644
- }).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(() => {
645
741
  if (this.disposed) return;
646
742
  this.submissionInFlight = false;
647
743
  const shouldFlush = this.dirtyDuringSubmission;
648
744
  this.dirtyDuringSubmission = false;
649
- if (!this.disposed && shouldFlush) this.flushNow();
745
+ if (shouldFlush) this.flushNow();
746
+ else if (acknowledged && this.changeDebounce === null) this.flushNow(true);
650
747
  });
651
748
  }
652
749
  submitFinalUpdate(sceneId, sceneGeneration, elements) {
750
+ const submittedElements = this.toSubmissionElements(elements);
751
+ if (submittedElements.length === 0) return;
653
752
  let submission;
654
753
  try {
655
754
  submission = this.opts.submitUpdate({
656
755
  sceneId,
657
756
  ...sceneGeneration ? { sceneGeneration } : {},
658
757
  syncAll: false,
659
- elements: elements.map(toWireElement)
758
+ elements: submittedElements
660
759
  });
661
760
  } catch (cause) {
662
761
  this.opts.onSubmissionError?.(cause);
@@ -664,6 +763,20 @@ var ExcalidrawCollabEngine = class {
664
763
  }
665
764
  void submission.catch((cause) => this.opts.onSubmissionError?.(cause));
666
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
+ }
667
780
  applyRemoteElements(args) {
668
781
  const remoteSceneId = args.sceneId;
669
782
  const replacingScene = this.sceneId !== null && remoteSceneId !== this.sceneId;
@@ -673,43 +786,56 @@ var ExcalidrawCollabEngine = class {
673
786
  if (remoteSceneId !== this.sceneId) {
674
787
  this.sceneId = remoteSceneId;
675
788
  this.broadcastedElementVersions.clear();
676
- this.lastBroadcastedOrReceivedElementsHash = 0;
677
789
  }
678
790
  } else if (remoteSceneId !== this.sceneId) {
679
791
  if (args.syncAll && asArray(args.remoteElements).length === 0) {
680
792
  this.sceneId = remoteSceneId;
681
793
  this.broadcastedElementVersions.clear();
682
- this.lastBroadcastedOrReceivedElementsHash = 0;
683
794
  } else {
684
795
  this.requestSnapshot();
685
796
  return;
686
797
  }
687
798
  }
688
799
  if (replacingScene) {
689
- if (this.changeDebounce !== null) clearTimeout(this.changeDebounce);
690
- this.changeDebounce = null;
800
+ this.rejectedElementVersions.clear();
801
+ this.clearChangeDebounce();
691
802
  this.dirtyDuringSubmission = false;
692
803
  this.sceneGeneration = null;
804
+ this.deferredRemoteElements.clear();
805
+ this.resetSubmissionRetries();
693
806
  }
694
807
  if (args.sceneGeneration) this.sceneGeneration = args.sceneGeneration;
695
808
  const excalidrawAPI = this.opts.excalidrawAPI;
696
809
  if (replacingScene) excalidrawAPI.history.clear();
697
810
  const local = replacingScene ? [] : excalidrawAPI.getSceneElementsIncludingDeleted();
698
- const remoteElements = args.remoteElements;
699
- const restoredRemote = restoreElements(remoteElements, local);
811
+ const remoteElements = [...args.remoteElements].sort(compareElementOrder);
812
+ const restoredRemote = restoreElements(remoteElements, null);
700
813
  const remoteForReconcile = toReconcileRemoteElements(restoredRemote);
701
- const reconciled = reconcileElements(local, remoteForReconcile, excalidrawAPI.getAppState());
702
- this.lastBroadcastedOrReceivedElementsHash = hashElementsVersion(reconciled);
703
- this.broadcastedElementVersions.clear();
704
- for (const el of reconciled) {
705
- 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
+ }
706
830
  }
831
+ const hasUnsubmittedElements = reconciled.some((element) => this.isUnsubmitted(element));
707
832
  excalidrawAPI.updateScene({
708
833
  elements: reconciled,
709
834
  appState: withLocalWhiteboardCamera(args.appState, excalidrawAPI.getAppState()),
710
835
  captureUpdate: CaptureUpdateAction2.NEVER
711
836
  });
712
837
  this.filesSync.handleRemoteScene(reconciled);
838
+ if (args.isSnapshot && hasUnsubmittedElements) queueMicrotask(() => this.flushNow());
713
839
  }
714
840
  requestSnapshot() {
715
841
  if (this.disposed) return;
@@ -721,6 +847,7 @@ var ExcalidrawCollabEngine = class {
721
847
  };
722
848
 
723
849
  export {
850
+ compareElementOrder,
724
851
  toWireElement,
725
852
  fromWireElement,
726
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
@@ -51,13 +51,16 @@ export declare class ExcalidrawCollabEngine {
51
51
  private sceneId;
52
52
  private sceneGeneration;
53
53
  private canDraw;
54
- private lastBroadcastedOrReceivedElementsHash;
55
54
  private broadcastedElementVersions;
55
+ private rejectedElementVersions;
56
+ private deferredRemoteElements;
56
57
  private submissionInFlight;
57
58
  private dirtyDuringSubmission;
58
59
  private disposed;
59
60
  private changeDebounce;
60
- private fullSyncTimer;
61
+ private changeDebounceDeadline;
62
+ private submissionRetryTimer;
63
+ private submissionRetries;
61
64
  private readonly filesSync;
62
65
  private readonly presence;
63
66
  private readonly unsubPointerUp;
@@ -95,11 +98,15 @@ export declare class ExcalidrawCollabEngine {
95
98
  }): void;
96
99
  private flushNow;
97
100
  private flushOnDispose;
98
- private scheduleFullSync;
99
- private sendFullSync;
101
+ private isUnsubmitted;
102
+ private applyDeferredRemoteElements;
103
+ private clearChangeDebounce;
104
+ private resetSubmissionRetries;
105
+ private scheduleSubmissionRetry;
100
106
  private submissionContext;
101
107
  private submit;
102
108
  private submitFinalUpdate;
109
+ private toSubmissionElements;
103
110
  private applyRemoteElements;
104
111
  private requestSnapshot;
105
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-F4B4ANIZ.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": 8501481,
1220
- "sha256": "0c4c6df489516442d98fb69d33d1129d762ba3404a2eaf6f3e7fd7ea391cb6d9"
1219
+ "bytes": 8504846,
1220
+ "sha256": "20644a73c951091c36858684c83bd579c6894f986bf461cf21289f0ccef4ea59"
1221
1221
  },
1222
1222
  {
1223
1223
  "path": "THIRD_PARTY_NOTICES.json",