canvu-react 0.4.43 → 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.cjs CHANGED
@@ -2260,6 +2260,7 @@ var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
2260
2260
  var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
2261
2261
  var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
2262
2262
  var DRAFT_PERSIST_DEBOUNCE_MS = 420;
2263
+ var CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS = 1e3;
2263
2264
  function requestWindowIdleCallback(callback, timeout) {
2264
2265
  if (typeof window.requestIdleCallback === "function") {
2265
2266
  return window.requestIdleCallback(callback, { timeout });
@@ -2337,6 +2338,9 @@ function sameSerializedItems(left, right) {
2337
2338
  function nowMs() {
2338
2339
  return Date.now();
2339
2340
  }
2341
+ function shouldUpdateRealtimeConnectionMessageState(input) {
2342
+ return input.lastStateUpdateAt == null || input.receivedAt - input.lastStateUpdateAt >= CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS;
2343
+ }
2340
2344
  function hasDurableDocumentPersistence(snapshot) {
2341
2345
  return snapshot.persistedRevision == null || snapshot.persistedRevision >= snapshot.revision;
2342
2346
  }
@@ -2484,6 +2488,7 @@ function useRealtimeSession(options) {
2484
2488
  const connectionStateRef = react.useRef(
2485
2489
  enabled ? "connecting" : "offline"
2486
2490
  );
2491
+ const lastConnectionMessageStateUpdateAtRef = react.useRef(null);
2487
2492
  const localDraftRef = react.useRef(null);
2488
2493
  const conflictRef = react.useRef(null);
2489
2494
  const onErrorRef = react.useRef(onError);
@@ -3036,6 +3041,7 @@ function useRealtimeSession(options) {
3036
3041
  react.useEffect(() => {
3037
3042
  if (!enabled) {
3038
3043
  manualDisconnectRef.current = true;
3044
+ lastConnectionMessageStateUpdateAtRef.current = null;
3039
3045
  clearReconnectTimer();
3040
3046
  clearHeartbeatTimer();
3041
3047
  clearConnectTimeout();
@@ -3080,6 +3086,7 @@ function useRealtimeSession(options) {
3080
3086
  return;
3081
3087
  }
3082
3088
  let disposed = false;
3089
+ lastConnectionMessageStateUpdateAtRef.current = null;
3083
3090
  updateConnectionRef.current((prev) => ({
3084
3091
  ...prev,
3085
3092
  state: retryCountRef.current > 0 ? "reconnecting" : "connecting",
@@ -3131,10 +3138,17 @@ function useRealtimeSession(options) {
3131
3138
  }
3132
3139
  const parsed = parseRealtimeServerMessage(payload);
3133
3140
  if (!parsed) return;
3134
- updateConnectionRef.current((prev) => ({
3135
- ...prev,
3136
- lastMessageAt: nowMs()
3137
- }));
3141
+ const receivedAt = nowMs();
3142
+ if (shouldUpdateRealtimeConnectionMessageState({
3143
+ lastStateUpdateAt: lastConnectionMessageStateUpdateAtRef.current,
3144
+ receivedAt
3145
+ })) {
3146
+ lastConnectionMessageStateUpdateAtRef.current = receivedAt;
3147
+ updateConnectionRef.current((prev) => ({
3148
+ ...prev,
3149
+ lastMessageAt: receivedAt
3150
+ }));
3151
+ }
3138
3152
  if (parsed.type === "session:welcome") {
3139
3153
  retryCountRef.current = 0;
3140
3154
  updateConnectionRef.current((prev) => ({
@@ -3143,7 +3157,7 @@ function useRealtimeSession(options) {
3143
3157
  connected: true,
3144
3158
  clientId: parsed.clientId,
3145
3159
  retryCount: 0,
3146
- lastConnectedAt: nowMs(),
3160
+ lastConnectedAt: receivedAt,
3147
3161
  lastError: null
3148
3162
  }));
3149
3163
  applyPeersRef.current(parsed.peers);
@@ -3631,6 +3645,12 @@ function realtimeSessionPlugin(options) {
3631
3645
  render: () => /* @__PURE__ */ jsxRuntime.jsx(RealtimeSessionPanel, { ...options })
3632
3646
  };
3633
3647
  }
3648
+ var CLIENT_ONLY_IMAGE_KEYS2 = /* @__PURE__ */ new Set([
3649
+ "imageBlobId",
3650
+ "imageThumbnailBlobId",
3651
+ "imageRasterHref",
3652
+ "imageThumbnailHref"
3653
+ ]);
3634
3654
  function getSceneItemId(item) {
3635
3655
  const id = item.id;
3636
3656
  return typeof id === "string" ? id : null;
@@ -3647,6 +3667,43 @@ function hasMissingLocalItems(localItems, incomingItems) {
3647
3667
  }
3648
3668
  return false;
3649
3669
  }
3670
+ function getComparableRealtimeItem(item) {
3671
+ const comparable = { ...item };
3672
+ if (item.toolKind === "image") {
3673
+ for (const key of CLIENT_ONLY_IMAGE_KEYS2) {
3674
+ delete comparable[key];
3675
+ }
3676
+ comparable.childrenSvg = "";
3677
+ }
3678
+ return comparable;
3679
+ }
3680
+ function serializeComparableRealtimeItem(item) {
3681
+ return JSON.stringify(getComparableRealtimeItem(item));
3682
+ }
3683
+ function hasChangedLocalItems(localItems, incomingItems) {
3684
+ const incomingItemsById = /* @__PURE__ */ new Map();
3685
+ for (const incomingItem of incomingItems) {
3686
+ const id = getSceneItemId(incomingItem);
3687
+ if (id) incomingItemsById.set(id, incomingItem);
3688
+ }
3689
+ for (const localItem of localItems) {
3690
+ const id = getSceneItemId(localItem);
3691
+ const incomingItem = id ? incomingItemsById.get(id) : null;
3692
+ if (!incomingItem) continue;
3693
+ if (serializeComparableRealtimeItem(localItem) !== serializeComparableRealtimeItem(incomingItem)) {
3694
+ return true;
3695
+ }
3696
+ }
3697
+ return false;
3698
+ }
3699
+ function shouldPreserveLocalRealtimeItems({
3700
+ localItems,
3701
+ incomingItems,
3702
+ hasPendingLocalChanges
3703
+ }) {
3704
+ if (!hasPendingLocalChanges) return false;
3705
+ return hasMissingLocalItems(localItems, incomingItems) || hasChangedLocalItems(localItems, incomingItems);
3706
+ }
3650
3707
  function useRealtimeCanvasDocument(options) {
3651
3708
  const {
3652
3709
  session,
@@ -3659,6 +3716,8 @@ function useRealtimeCanvasDocument(options) {
3659
3716
  const [loading, setLoading] = react.useState(false);
3660
3717
  const lastAppliedRevisionRef = react.useRef(null);
3661
3718
  const inFlightRevisionRef = react.useRef(null);
3719
+ const latestItemsRef = react.useRef(items);
3720
+ const hasLocalChangeInFlightRef = react.useRef(false);
3662
3721
  const hasEverPropagatedItemsRef = react.useRef(false);
3663
3722
  const realtimeEnabled = enabled && session != null;
3664
3723
  const documentRevision = session?.document?.revision ?? null;
@@ -3681,6 +3740,7 @@ function useRealtimeCanvasDocument(options) {
3681
3740
  onItemsChange?.(normalizeItems ? normalizeItems(nextItems) : nextItems);
3682
3741
  return;
3683
3742
  }
3743
+ hasLocalChangeInFlightRef.current = true;
3684
3744
  const normalizedItems = normalizeItems ? normalizeItems(nextItems) : nextItems;
3685
3745
  onItemsChange?.(normalizedItems);
3686
3746
  session?.remoteAdapter.send?.(normalizedItems);
@@ -3688,10 +3748,16 @@ function useRealtimeCanvasDocument(options) {
3688
3748
  [enabled, normalizeItems, onItemsChange, session]
3689
3749
  );
3690
3750
  react.useEffect(() => {
3751
+ latestItemsRef.current = items;
3691
3752
  if (items.length > 0) {
3692
3753
  hasEverPropagatedItemsRef.current = true;
3693
3754
  }
3694
- }, [items.length]);
3755
+ }, [items]);
3756
+ react.useEffect(() => {
3757
+ if (!hasLocalOfflineDraft && !hasPendingDocumentSync) {
3758
+ hasLocalChangeInFlightRef.current = false;
3759
+ }
3760
+ }, [hasLocalOfflineDraft, hasPendingDocumentSync]);
3695
3761
  react.useEffect(() => {
3696
3762
  if (!realtimeEnabled || !onItemsChange || !session?.document) return;
3697
3763
  if (documentUpdatedByClientId === connectionClientId) return;
@@ -3705,17 +3771,22 @@ function useRealtimeCanvasDocument(options) {
3705
3771
  if (cancelled) return;
3706
3772
  if (inFlightRevisionRef.current !== documentRevision) return;
3707
3773
  lastAppliedRevisionRef.current = documentRevision;
3708
- const hasLocalItems = items.length > 0;
3709
- const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync;
3774
+ const localItems = latestItemsRef.current;
3775
+ const hasLocalItems = localItems.length > 0;
3776
+ const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
3710
3777
  if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
3711
3778
  if (hasLocalItems) {
3712
- const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
3779
+ const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
3713
3780
  session.remoteAdapter.send?.(normalizedLocalItems);
3714
3781
  }
3715
3782
  return;
3716
3783
  }
3717
- if (hasLocalItems && hasMissingLocalItems(items, resolvedItems)) {
3718
- const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
3784
+ if (shouldPreserveLocalRealtimeItems({
3785
+ localItems,
3786
+ incomingItems: resolvedItems,
3787
+ hasPendingLocalChanges
3788
+ })) {
3789
+ const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
3719
3790
  session.remoteAdapter.send?.(normalizedLocalItems);
3720
3791
  return;
3721
3792
  }
@@ -3741,7 +3812,6 @@ function useRealtimeCanvasDocument(options) {
3741
3812
  documentUpdatedByClientId,
3742
3813
  hasLocalOfflineDraft,
3743
3814
  hasPendingDocumentSync,
3744
- items,
3745
3815
  normalizeItems,
3746
3816
  onItemsChange,
3747
3817
  realtimeEnabled,