canvu-react 0.4.42 → 0.4.44

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/dist/realtime.js CHANGED
@@ -2235,6 +2235,7 @@ var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
2235
2235
  var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
2236
2236
  var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
2237
2237
  var DRAFT_PERSIST_DEBOUNCE_MS = 420;
2238
+ var CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS = 1e3;
2238
2239
  function requestWindowIdleCallback(callback, timeout) {
2239
2240
  if (typeof window.requestIdleCallback === "function") {
2240
2241
  return window.requestIdleCallback(callback, { timeout });
@@ -2312,6 +2313,9 @@ function sameSerializedItems(left, right) {
2312
2313
  function nowMs() {
2313
2314
  return Date.now();
2314
2315
  }
2316
+ function shouldUpdateRealtimeConnectionMessageState(input) {
2317
+ return input.lastStateUpdateAt == null || input.receivedAt - input.lastStateUpdateAt >= CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS;
2318
+ }
2315
2319
  function hasDurableDocumentPersistence(snapshot) {
2316
2320
  return snapshot.persistedRevision == null || snapshot.persistedRevision >= snapshot.revision;
2317
2321
  }
@@ -2459,6 +2463,7 @@ function useRealtimeSession(options) {
2459
2463
  const connectionStateRef = useRef(
2460
2464
  enabled ? "connecting" : "offline"
2461
2465
  );
2466
+ const lastConnectionMessageStateUpdateAtRef = useRef(null);
2462
2467
  const localDraftRef = useRef(null);
2463
2468
  const conflictRef = useRef(null);
2464
2469
  const onErrorRef = useRef(onError);
@@ -3011,6 +3016,7 @@ function useRealtimeSession(options) {
3011
3016
  useEffect(() => {
3012
3017
  if (!enabled) {
3013
3018
  manualDisconnectRef.current = true;
3019
+ lastConnectionMessageStateUpdateAtRef.current = null;
3014
3020
  clearReconnectTimer();
3015
3021
  clearHeartbeatTimer();
3016
3022
  clearConnectTimeout();
@@ -3055,6 +3061,7 @@ function useRealtimeSession(options) {
3055
3061
  return;
3056
3062
  }
3057
3063
  let disposed = false;
3064
+ lastConnectionMessageStateUpdateAtRef.current = null;
3058
3065
  updateConnectionRef.current((prev) => ({
3059
3066
  ...prev,
3060
3067
  state: retryCountRef.current > 0 ? "reconnecting" : "connecting",
@@ -3106,10 +3113,17 @@ function useRealtimeSession(options) {
3106
3113
  }
3107
3114
  const parsed = parseRealtimeServerMessage(payload);
3108
3115
  if (!parsed) return;
3109
- updateConnectionRef.current((prev) => ({
3110
- ...prev,
3111
- lastMessageAt: nowMs()
3112
- }));
3116
+ const receivedAt = nowMs();
3117
+ if (shouldUpdateRealtimeConnectionMessageState({
3118
+ lastStateUpdateAt: lastConnectionMessageStateUpdateAtRef.current,
3119
+ receivedAt
3120
+ })) {
3121
+ lastConnectionMessageStateUpdateAtRef.current = receivedAt;
3122
+ updateConnectionRef.current((prev) => ({
3123
+ ...prev,
3124
+ lastMessageAt: receivedAt
3125
+ }));
3126
+ }
3113
3127
  if (parsed.type === "session:welcome") {
3114
3128
  retryCountRef.current = 0;
3115
3129
  updateConnectionRef.current((prev) => ({
@@ -3118,7 +3132,7 @@ function useRealtimeSession(options) {
3118
3132
  connected: true,
3119
3133
  clientId: parsed.clientId,
3120
3134
  retryCount: 0,
3121
- lastConnectedAt: nowMs(),
3135
+ lastConnectedAt: receivedAt,
3122
3136
  lastError: null
3123
3137
  }));
3124
3138
  applyPeersRef.current(parsed.peers);
@@ -3606,6 +3620,12 @@ function realtimeSessionPlugin(options) {
3606
3620
  render: () => /* @__PURE__ */ jsx(RealtimeSessionPanel, { ...options })
3607
3621
  };
3608
3622
  }
3623
+ var CLIENT_ONLY_IMAGE_KEYS2 = /* @__PURE__ */ new Set([
3624
+ "imageBlobId",
3625
+ "imageThumbnailBlobId",
3626
+ "imageRasterHref",
3627
+ "imageThumbnailHref"
3628
+ ]);
3609
3629
  function getSceneItemId(item) {
3610
3630
  const id = item.id;
3611
3631
  return typeof id === "string" ? id : null;
@@ -3622,6 +3642,43 @@ function hasMissingLocalItems(localItems, incomingItems) {
3622
3642
  }
3623
3643
  return false;
3624
3644
  }
3645
+ function getComparableRealtimeItem(item) {
3646
+ const comparable = { ...item };
3647
+ if (item.toolKind === "image") {
3648
+ for (const key of CLIENT_ONLY_IMAGE_KEYS2) {
3649
+ delete comparable[key];
3650
+ }
3651
+ comparable.childrenSvg = "";
3652
+ }
3653
+ return comparable;
3654
+ }
3655
+ function serializeComparableRealtimeItem(item) {
3656
+ return JSON.stringify(getComparableRealtimeItem(item));
3657
+ }
3658
+ function hasChangedLocalItems(localItems, incomingItems) {
3659
+ const incomingItemsById = /* @__PURE__ */ new Map();
3660
+ for (const incomingItem of incomingItems) {
3661
+ const id = getSceneItemId(incomingItem);
3662
+ if (id) incomingItemsById.set(id, incomingItem);
3663
+ }
3664
+ for (const localItem of localItems) {
3665
+ const id = getSceneItemId(localItem);
3666
+ const incomingItem = id ? incomingItemsById.get(id) : null;
3667
+ if (!incomingItem) continue;
3668
+ if (serializeComparableRealtimeItem(localItem) !== serializeComparableRealtimeItem(incomingItem)) {
3669
+ return true;
3670
+ }
3671
+ }
3672
+ return false;
3673
+ }
3674
+ function shouldPreserveLocalRealtimeItems({
3675
+ localItems,
3676
+ incomingItems,
3677
+ hasPendingLocalChanges
3678
+ }) {
3679
+ if (!hasPendingLocalChanges) return false;
3680
+ return hasMissingLocalItems(localItems, incomingItems) || hasChangedLocalItems(localItems, incomingItems);
3681
+ }
3625
3682
  function useRealtimeCanvasDocument(options) {
3626
3683
  const {
3627
3684
  session,
@@ -3634,6 +3691,8 @@ function useRealtimeCanvasDocument(options) {
3634
3691
  const [loading, setLoading] = useState(false);
3635
3692
  const lastAppliedRevisionRef = useRef(null);
3636
3693
  const inFlightRevisionRef = useRef(null);
3694
+ const latestItemsRef = useRef(items);
3695
+ const hasLocalChangeInFlightRef = useRef(false);
3637
3696
  const hasEverPropagatedItemsRef = useRef(false);
3638
3697
  const realtimeEnabled = enabled && session != null;
3639
3698
  const documentRevision = session?.document?.revision ?? null;
@@ -3656,6 +3715,7 @@ function useRealtimeCanvasDocument(options) {
3656
3715
  onItemsChange?.(normalizeItems ? normalizeItems(nextItems) : nextItems);
3657
3716
  return;
3658
3717
  }
3718
+ hasLocalChangeInFlightRef.current = true;
3659
3719
  const normalizedItems = normalizeItems ? normalizeItems(nextItems) : nextItems;
3660
3720
  onItemsChange?.(normalizedItems);
3661
3721
  session?.remoteAdapter.send?.(normalizedItems);
@@ -3663,10 +3723,16 @@ function useRealtimeCanvasDocument(options) {
3663
3723
  [enabled, normalizeItems, onItemsChange, session]
3664
3724
  );
3665
3725
  useEffect(() => {
3726
+ latestItemsRef.current = items;
3666
3727
  if (items.length > 0) {
3667
3728
  hasEverPropagatedItemsRef.current = true;
3668
3729
  }
3669
- }, [items.length]);
3730
+ }, [items]);
3731
+ useEffect(() => {
3732
+ if (!hasLocalOfflineDraft && !hasPendingDocumentSync) {
3733
+ hasLocalChangeInFlightRef.current = false;
3734
+ }
3735
+ }, [hasLocalOfflineDraft, hasPendingDocumentSync]);
3670
3736
  useEffect(() => {
3671
3737
  if (!realtimeEnabled || !onItemsChange || !session?.document) return;
3672
3738
  if (documentUpdatedByClientId === connectionClientId) return;
@@ -3680,17 +3746,22 @@ function useRealtimeCanvasDocument(options) {
3680
3746
  if (cancelled) return;
3681
3747
  if (inFlightRevisionRef.current !== documentRevision) return;
3682
3748
  lastAppliedRevisionRef.current = documentRevision;
3683
- const hasLocalItems = items.length > 0;
3684
- const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync;
3749
+ const localItems = latestItemsRef.current;
3750
+ const hasLocalItems = localItems.length > 0;
3751
+ const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
3685
3752
  if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
3686
3753
  if (hasLocalItems) {
3687
- const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
3754
+ const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
3688
3755
  session.remoteAdapter.send?.(normalizedLocalItems);
3689
3756
  }
3690
3757
  return;
3691
3758
  }
3692
- if (hasLocalItems && hasMissingLocalItems(items, resolvedItems)) {
3693
- const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
3759
+ if (shouldPreserveLocalRealtimeItems({
3760
+ localItems,
3761
+ incomingItems: resolvedItems,
3762
+ hasPendingLocalChanges
3763
+ })) {
3764
+ const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
3694
3765
  session.remoteAdapter.send?.(normalizedLocalItems);
3695
3766
  return;
3696
3767
  }
@@ -3716,7 +3787,6 @@ function useRealtimeCanvasDocument(options) {
3716
3787
  documentUpdatedByClientId,
3717
3788
  hasLocalOfflineDraft,
3718
3789
  hasPendingDocumentSync,
3719
- items,
3720
3790
  normalizeItems,
3721
3791
  onItemsChange,
3722
3792
  realtimeEnabled,