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.
@@ -269,6 +269,12 @@ function parseRealtimeServerMessage(value) {
269
269
  }
270
270
  return void 0;
271
271
  }
272
+ var CLIENT_ONLY_IMAGE_KEYS = /* @__PURE__ */ new Set([
273
+ "imageBlobId",
274
+ "imageThumbnailBlobId",
275
+ "imageRasterHref",
276
+ "imageThumbnailHref"
277
+ ]);
272
278
  function getSceneItemId(item) {
273
279
  const id = item.id;
274
280
  return typeof id === "string" ? id : null;
@@ -285,6 +291,43 @@ function hasMissingLocalItems(localItems, incomingItems) {
285
291
  }
286
292
  return false;
287
293
  }
294
+ function getComparableRealtimeItem(item) {
295
+ const comparable = { ...item };
296
+ if (item.toolKind === "image") {
297
+ for (const key of CLIENT_ONLY_IMAGE_KEYS) {
298
+ delete comparable[key];
299
+ }
300
+ comparable.childrenSvg = "";
301
+ }
302
+ return comparable;
303
+ }
304
+ function serializeComparableRealtimeItem(item) {
305
+ return JSON.stringify(getComparableRealtimeItem(item));
306
+ }
307
+ function hasChangedLocalItems(localItems, incomingItems) {
308
+ const incomingItemsById = /* @__PURE__ */ new Map();
309
+ for (const incomingItem of incomingItems) {
310
+ const id = getSceneItemId(incomingItem);
311
+ if (id) incomingItemsById.set(id, incomingItem);
312
+ }
313
+ for (const localItem of localItems) {
314
+ const id = getSceneItemId(localItem);
315
+ const incomingItem = id ? incomingItemsById.get(id) : null;
316
+ if (!incomingItem) continue;
317
+ if (serializeComparableRealtimeItem(localItem) !== serializeComparableRealtimeItem(incomingItem)) {
318
+ return true;
319
+ }
320
+ }
321
+ return false;
322
+ }
323
+ function shouldPreserveLocalRealtimeItems({
324
+ localItems,
325
+ incomingItems,
326
+ hasPendingLocalChanges
327
+ }) {
328
+ if (!hasPendingLocalChanges) return false;
329
+ return hasMissingLocalItems(localItems, incomingItems) || hasChangedLocalItems(localItems, incomingItems);
330
+ }
288
331
  function useRealtimeCanvasDocument(options) {
289
332
  const {
290
333
  session,
@@ -297,6 +340,8 @@ function useRealtimeCanvasDocument(options) {
297
340
  const [loading, setLoading] = react.useState(false);
298
341
  const lastAppliedRevisionRef = react.useRef(null);
299
342
  const inFlightRevisionRef = react.useRef(null);
343
+ const latestItemsRef = react.useRef(items);
344
+ const hasLocalChangeInFlightRef = react.useRef(false);
300
345
  const hasEverPropagatedItemsRef = react.useRef(false);
301
346
  const realtimeEnabled = enabled && session != null;
302
347
  const documentRevision = session?.document?.revision ?? null;
@@ -319,6 +364,7 @@ function useRealtimeCanvasDocument(options) {
319
364
  onItemsChange?.(normalizeItems ? normalizeItems(nextItems) : nextItems);
320
365
  return;
321
366
  }
367
+ hasLocalChangeInFlightRef.current = true;
322
368
  const normalizedItems = normalizeItems ? normalizeItems(nextItems) : nextItems;
323
369
  onItemsChange?.(normalizedItems);
324
370
  session?.remoteAdapter.send?.(normalizedItems);
@@ -326,10 +372,16 @@ function useRealtimeCanvasDocument(options) {
326
372
  [enabled, normalizeItems, onItemsChange, session]
327
373
  );
328
374
  react.useEffect(() => {
375
+ latestItemsRef.current = items;
329
376
  if (items.length > 0) {
330
377
  hasEverPropagatedItemsRef.current = true;
331
378
  }
332
- }, [items.length]);
379
+ }, [items]);
380
+ react.useEffect(() => {
381
+ if (!hasLocalOfflineDraft && !hasPendingDocumentSync) {
382
+ hasLocalChangeInFlightRef.current = false;
383
+ }
384
+ }, [hasLocalOfflineDraft, hasPendingDocumentSync]);
333
385
  react.useEffect(() => {
334
386
  if (!realtimeEnabled || !onItemsChange || !session?.document) return;
335
387
  if (documentUpdatedByClientId === connectionClientId) return;
@@ -343,17 +395,22 @@ function useRealtimeCanvasDocument(options) {
343
395
  if (cancelled) return;
344
396
  if (inFlightRevisionRef.current !== documentRevision) return;
345
397
  lastAppliedRevisionRef.current = documentRevision;
346
- const hasLocalItems = items.length > 0;
347
- const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync;
398
+ const localItems = latestItemsRef.current;
399
+ const hasLocalItems = localItems.length > 0;
400
+ const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
348
401
  if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
349
402
  if (hasLocalItems) {
350
- const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
403
+ const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
351
404
  session.remoteAdapter.send?.(normalizedLocalItems);
352
405
  }
353
406
  return;
354
407
  }
355
- if (hasLocalItems && hasMissingLocalItems(items, resolvedItems)) {
356
- const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
408
+ if (shouldPreserveLocalRealtimeItems({
409
+ localItems,
410
+ incomingItems: resolvedItems,
411
+ hasPendingLocalChanges
412
+ })) {
413
+ const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
357
414
  session.remoteAdapter.send?.(normalizedLocalItems);
358
415
  return;
359
416
  }
@@ -379,7 +436,6 @@ function useRealtimeCanvasDocument(options) {
379
436
  documentUpdatedByClientId,
380
437
  hasLocalOfflineDraft,
381
438
  hasPendingDocumentSync,
382
- items,
383
439
  normalizeItems,
384
440
  onItemsChange,
385
441
  realtimeEnabled,
@@ -542,7 +598,7 @@ function useRealtimePeerFollow(options) {
542
598
  }, [followedPeerId, onFollowEnd, sessionPeers, viewportRef]);
543
599
  }
544
600
  var ITEMS_KEY = "items";
545
- var CLIENT_ONLY_IMAGE_KEYS = /* @__PURE__ */ new Set([
601
+ var CLIENT_ONLY_IMAGE_KEYS2 = /* @__PURE__ */ new Set([
546
602
  "imageBlobId",
547
603
  "imageThumbnailBlobId",
548
604
  "imageRasterHref",
@@ -635,7 +691,7 @@ function updateYMapInPlace(yMap, next) {
635
691
  function normalizeRealtimeItem(item) {
636
692
  if (item.toolKind !== "image") return item;
637
693
  const normalized = Object.fromEntries(
638
- Object.entries(item).filter(([key]) => !CLIENT_ONLY_IMAGE_KEYS.has(key))
694
+ Object.entries(item).filter(([key]) => !CLIENT_ONLY_IMAGE_KEYS2.has(key))
639
695
  );
640
696
  normalized.childrenSvg = "";
641
697
  return normalized;
@@ -788,6 +844,7 @@ var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
788
844
  var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
789
845
  var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
790
846
  var DRAFT_PERSIST_DEBOUNCE_MS = 420;
847
+ var CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS = 1e3;
791
848
  function requestRuntimeIdleCallback(callback, timeout) {
792
849
  const runtime = globalThis;
793
850
  if (typeof runtime.requestIdleCallback === "function") {
@@ -799,7 +856,7 @@ function cancelRuntimeIdleCallback(handle) {
799
856
  if (handle == null) return;
800
857
  const runtime = globalThis;
801
858
  if (typeof runtime.cancelIdleCallback === "function") {
802
- runtime.cancelIdleCallback(Number(handle));
859
+ runtime.cancelIdleCallback(handle);
803
860
  return;
804
861
  }
805
862
  globalThis.clearTimeout(handle);
@@ -920,6 +977,9 @@ function sameSerializedItems(left, right) {
920
977
  function nowMs() {
921
978
  return Date.now();
922
979
  }
980
+ function shouldUpdateRealtimeConnectionMessageState(input) {
981
+ return input.lastStateUpdateAt == null || input.receivedAt - input.lastStateUpdateAt >= CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS;
982
+ }
923
983
  function hasDurableDocumentPersistence(snapshot) {
924
984
  return snapshot.persistedRevision == null || snapshot.persistedRevision >= snapshot.revision;
925
985
  }
@@ -1083,6 +1143,7 @@ function useRealtimeSession(options) {
1083
1143
  const connectionStateRef = react.useRef(
1084
1144
  enabled ? "connecting" : "offline"
1085
1145
  );
1146
+ const lastConnectionMessageStateUpdateAtRef = react.useRef(null);
1086
1147
  const localDraftRef = react.useRef(null);
1087
1148
  const conflictRef = react.useRef(null);
1088
1149
  const onErrorRef = react.useRef(onError);
@@ -1648,6 +1709,7 @@ function useRealtimeSession(options) {
1648
1709
  react.useEffect(() => {
1649
1710
  if (!enabled) {
1650
1711
  manualDisconnectRef.current = true;
1712
+ lastConnectionMessageStateUpdateAtRef.current = null;
1651
1713
  clearReconnectTimer();
1652
1714
  clearHeartbeatTimer();
1653
1715
  clearConnectTimeout();
@@ -1704,6 +1766,7 @@ function useRealtimeSession(options) {
1704
1766
  return;
1705
1767
  }
1706
1768
  let disposed = false;
1769
+ lastConnectionMessageStateUpdateAtRef.current = null;
1707
1770
  updateConnectionRef.current((prev) => ({
1708
1771
  ...prev,
1709
1772
  state: retryCountRef.current > 0 ? "reconnecting" : "connecting",
@@ -1757,10 +1820,17 @@ function useRealtimeSession(options) {
1757
1820
  }
1758
1821
  const parsed = parseRealtimeServerMessage(payload);
1759
1822
  if (!parsed) return;
1760
- updateConnectionRef.current((prev) => ({
1761
- ...prev,
1762
- lastMessageAt: nowMs()
1763
- }));
1823
+ const receivedAt = nowMs();
1824
+ if (shouldUpdateRealtimeConnectionMessageState({
1825
+ lastStateUpdateAt: lastConnectionMessageStateUpdateAtRef.current,
1826
+ receivedAt
1827
+ })) {
1828
+ lastConnectionMessageStateUpdateAtRef.current = receivedAt;
1829
+ updateConnectionRef.current((prev) => ({
1830
+ ...prev,
1831
+ lastMessageAt: receivedAt
1832
+ }));
1833
+ }
1764
1834
  if (parsed.type === "session:welcome") {
1765
1835
  retryCountRef.current = 0;
1766
1836
  updateConnectionRef.current((prev) => ({
@@ -1769,7 +1839,7 @@ function useRealtimeSession(options) {
1769
1839
  connected: true,
1770
1840
  clientId: parsed.clientId,
1771
1841
  retryCount: 0,
1772
- lastConnectedAt: nowMs(),
1842
+ lastConnectedAt: receivedAt,
1773
1843
  lastError: null
1774
1844
  }));
1775
1845
  applyPeersRef.current(parsed.peers);