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.
@@ -247,6 +247,12 @@ function parseRealtimeServerMessage(value) {
247
247
  }
248
248
  return void 0;
249
249
  }
250
+ var CLIENT_ONLY_IMAGE_KEYS = /* @__PURE__ */ new Set([
251
+ "imageBlobId",
252
+ "imageThumbnailBlobId",
253
+ "imageRasterHref",
254
+ "imageThumbnailHref"
255
+ ]);
250
256
  function getSceneItemId(item) {
251
257
  const id = item.id;
252
258
  return typeof id === "string" ? id : null;
@@ -263,6 +269,43 @@ function hasMissingLocalItems(localItems, incomingItems) {
263
269
  }
264
270
  return false;
265
271
  }
272
+ function getComparableRealtimeItem(item) {
273
+ const comparable = { ...item };
274
+ if (item.toolKind === "image") {
275
+ for (const key of CLIENT_ONLY_IMAGE_KEYS) {
276
+ delete comparable[key];
277
+ }
278
+ comparable.childrenSvg = "";
279
+ }
280
+ return comparable;
281
+ }
282
+ function serializeComparableRealtimeItem(item) {
283
+ return JSON.stringify(getComparableRealtimeItem(item));
284
+ }
285
+ function hasChangedLocalItems(localItems, incomingItems) {
286
+ const incomingItemsById = /* @__PURE__ */ new Map();
287
+ for (const incomingItem of incomingItems) {
288
+ const id = getSceneItemId(incomingItem);
289
+ if (id) incomingItemsById.set(id, incomingItem);
290
+ }
291
+ for (const localItem of localItems) {
292
+ const id = getSceneItemId(localItem);
293
+ const incomingItem = id ? incomingItemsById.get(id) : null;
294
+ if (!incomingItem) continue;
295
+ if (serializeComparableRealtimeItem(localItem) !== serializeComparableRealtimeItem(incomingItem)) {
296
+ return true;
297
+ }
298
+ }
299
+ return false;
300
+ }
301
+ function shouldPreserveLocalRealtimeItems({
302
+ localItems,
303
+ incomingItems,
304
+ hasPendingLocalChanges
305
+ }) {
306
+ if (!hasPendingLocalChanges) return false;
307
+ return hasMissingLocalItems(localItems, incomingItems) || hasChangedLocalItems(localItems, incomingItems);
308
+ }
266
309
  function useRealtimeCanvasDocument(options) {
267
310
  const {
268
311
  session,
@@ -275,6 +318,8 @@ function useRealtimeCanvasDocument(options) {
275
318
  const [loading, setLoading] = useState(false);
276
319
  const lastAppliedRevisionRef = useRef(null);
277
320
  const inFlightRevisionRef = useRef(null);
321
+ const latestItemsRef = useRef(items);
322
+ const hasLocalChangeInFlightRef = useRef(false);
278
323
  const hasEverPropagatedItemsRef = useRef(false);
279
324
  const realtimeEnabled = enabled && session != null;
280
325
  const documentRevision = session?.document?.revision ?? null;
@@ -297,6 +342,7 @@ function useRealtimeCanvasDocument(options) {
297
342
  onItemsChange?.(normalizeItems ? normalizeItems(nextItems) : nextItems);
298
343
  return;
299
344
  }
345
+ hasLocalChangeInFlightRef.current = true;
300
346
  const normalizedItems = normalizeItems ? normalizeItems(nextItems) : nextItems;
301
347
  onItemsChange?.(normalizedItems);
302
348
  session?.remoteAdapter.send?.(normalizedItems);
@@ -304,10 +350,16 @@ function useRealtimeCanvasDocument(options) {
304
350
  [enabled, normalizeItems, onItemsChange, session]
305
351
  );
306
352
  useEffect(() => {
353
+ latestItemsRef.current = items;
307
354
  if (items.length > 0) {
308
355
  hasEverPropagatedItemsRef.current = true;
309
356
  }
310
- }, [items.length]);
357
+ }, [items]);
358
+ useEffect(() => {
359
+ if (!hasLocalOfflineDraft && !hasPendingDocumentSync) {
360
+ hasLocalChangeInFlightRef.current = false;
361
+ }
362
+ }, [hasLocalOfflineDraft, hasPendingDocumentSync]);
311
363
  useEffect(() => {
312
364
  if (!realtimeEnabled || !onItemsChange || !session?.document) return;
313
365
  if (documentUpdatedByClientId === connectionClientId) return;
@@ -321,17 +373,22 @@ function useRealtimeCanvasDocument(options) {
321
373
  if (cancelled) return;
322
374
  if (inFlightRevisionRef.current !== documentRevision) return;
323
375
  lastAppliedRevisionRef.current = documentRevision;
324
- const hasLocalItems = items.length > 0;
325
- const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync;
376
+ const localItems = latestItemsRef.current;
377
+ const hasLocalItems = localItems.length > 0;
378
+ const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
326
379
  if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
327
380
  if (hasLocalItems) {
328
- const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
381
+ const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
329
382
  session.remoteAdapter.send?.(normalizedLocalItems);
330
383
  }
331
384
  return;
332
385
  }
333
- if (hasLocalItems && hasMissingLocalItems(items, resolvedItems)) {
334
- const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
386
+ if (shouldPreserveLocalRealtimeItems({
387
+ localItems,
388
+ incomingItems: resolvedItems,
389
+ hasPendingLocalChanges
390
+ })) {
391
+ const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
335
392
  session.remoteAdapter.send?.(normalizedLocalItems);
336
393
  return;
337
394
  }
@@ -357,7 +414,6 @@ function useRealtimeCanvasDocument(options) {
357
414
  documentUpdatedByClientId,
358
415
  hasLocalOfflineDraft,
359
416
  hasPendingDocumentSync,
360
- items,
361
417
  normalizeItems,
362
418
  onItemsChange,
363
419
  realtimeEnabled,
@@ -520,7 +576,7 @@ function useRealtimePeerFollow(options) {
520
576
  }, [followedPeerId, onFollowEnd, sessionPeers, viewportRef]);
521
577
  }
522
578
  var ITEMS_KEY = "items";
523
- var CLIENT_ONLY_IMAGE_KEYS = /* @__PURE__ */ new Set([
579
+ var CLIENT_ONLY_IMAGE_KEYS2 = /* @__PURE__ */ new Set([
524
580
  "imageBlobId",
525
581
  "imageThumbnailBlobId",
526
582
  "imageRasterHref",
@@ -613,7 +669,7 @@ function updateYMapInPlace(yMap, next) {
613
669
  function normalizeRealtimeItem(item) {
614
670
  if (item.toolKind !== "image") return item;
615
671
  const normalized = Object.fromEntries(
616
- Object.entries(item).filter(([key]) => !CLIENT_ONLY_IMAGE_KEYS.has(key))
672
+ Object.entries(item).filter(([key]) => !CLIENT_ONLY_IMAGE_KEYS2.has(key))
617
673
  );
618
674
  normalized.childrenSvg = "";
619
675
  return normalized;
@@ -766,6 +822,7 @@ var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
766
822
  var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
767
823
  var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
768
824
  var DRAFT_PERSIST_DEBOUNCE_MS = 420;
825
+ var CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS = 1e3;
769
826
  function requestRuntimeIdleCallback(callback, timeout) {
770
827
  const runtime = globalThis;
771
828
  if (typeof runtime.requestIdleCallback === "function") {
@@ -777,7 +834,7 @@ function cancelRuntimeIdleCallback(handle) {
777
834
  if (handle == null) return;
778
835
  const runtime = globalThis;
779
836
  if (typeof runtime.cancelIdleCallback === "function") {
780
- runtime.cancelIdleCallback(Number(handle));
837
+ runtime.cancelIdleCallback(handle);
781
838
  return;
782
839
  }
783
840
  globalThis.clearTimeout(handle);
@@ -898,6 +955,9 @@ function sameSerializedItems(left, right) {
898
955
  function nowMs() {
899
956
  return Date.now();
900
957
  }
958
+ function shouldUpdateRealtimeConnectionMessageState(input) {
959
+ return input.lastStateUpdateAt == null || input.receivedAt - input.lastStateUpdateAt >= CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS;
960
+ }
901
961
  function hasDurableDocumentPersistence(snapshot) {
902
962
  return snapshot.persistedRevision == null || snapshot.persistedRevision >= snapshot.revision;
903
963
  }
@@ -1061,6 +1121,7 @@ function useRealtimeSession(options) {
1061
1121
  const connectionStateRef = useRef(
1062
1122
  enabled ? "connecting" : "offline"
1063
1123
  );
1124
+ const lastConnectionMessageStateUpdateAtRef = useRef(null);
1064
1125
  const localDraftRef = useRef(null);
1065
1126
  const conflictRef = useRef(null);
1066
1127
  const onErrorRef = useRef(onError);
@@ -1626,6 +1687,7 @@ function useRealtimeSession(options) {
1626
1687
  useEffect(() => {
1627
1688
  if (!enabled) {
1628
1689
  manualDisconnectRef.current = true;
1690
+ lastConnectionMessageStateUpdateAtRef.current = null;
1629
1691
  clearReconnectTimer();
1630
1692
  clearHeartbeatTimer();
1631
1693
  clearConnectTimeout();
@@ -1682,6 +1744,7 @@ function useRealtimeSession(options) {
1682
1744
  return;
1683
1745
  }
1684
1746
  let disposed = false;
1747
+ lastConnectionMessageStateUpdateAtRef.current = null;
1685
1748
  updateConnectionRef.current((prev) => ({
1686
1749
  ...prev,
1687
1750
  state: retryCountRef.current > 0 ? "reconnecting" : "connecting",
@@ -1735,10 +1798,17 @@ function useRealtimeSession(options) {
1735
1798
  }
1736
1799
  const parsed = parseRealtimeServerMessage(payload);
1737
1800
  if (!parsed) return;
1738
- updateConnectionRef.current((prev) => ({
1739
- ...prev,
1740
- lastMessageAt: nowMs()
1741
- }));
1801
+ const receivedAt = nowMs();
1802
+ if (shouldUpdateRealtimeConnectionMessageState({
1803
+ lastStateUpdateAt: lastConnectionMessageStateUpdateAtRef.current,
1804
+ receivedAt
1805
+ })) {
1806
+ lastConnectionMessageStateUpdateAtRef.current = receivedAt;
1807
+ updateConnectionRef.current((prev) => ({
1808
+ ...prev,
1809
+ lastMessageAt: receivedAt
1810
+ }));
1811
+ }
1742
1812
  if (parsed.type === "session:welcome") {
1743
1813
  retryCountRef.current = 0;
1744
1814
  updateConnectionRef.current((prev) => ({
@@ -1747,7 +1817,7 @@ function useRealtimeSession(options) {
1747
1817
  connected: true,
1748
1818
  clientId: parsed.clientId,
1749
1819
  retryCount: 0,
1750
- lastConnectedAt: nowMs(),
1820
+ lastConnectedAt: receivedAt,
1751
1821
  lastError: null
1752
1822
  }));
1753
1823
  applyPeersRef.current(parsed.peers);