canvu-react 0.4.79 → 0.4.81

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.
@@ -104,12 +104,16 @@ function parseDocumentSnapshot(value) {
104
104
  const updatedAt = getNumber(value.updatedAt);
105
105
  const items = parseItems(value.items);
106
106
  if (revision == null || updatedAt == null || items == null) return void 0;
107
+ const deletedItemIds = Array.isArray(value.deletedItemIds) ? value.deletedItemIds.filter(
108
+ (entry) => typeof entry === "string"
109
+ ) : void 0;
107
110
  return {
108
111
  revision,
109
112
  updatedAt,
110
113
  items,
111
114
  ...getString(value.updatedByClientId) ? { updatedByClientId: getString(value.updatedByClientId) } : {},
112
- ...getNumber(value.persistedRevision) != null ? { persistedRevision: getNumber(value.persistedRevision) } : {}
115
+ ...getNumber(value.persistedRevision) != null ? { persistedRevision: getNumber(value.persistedRevision) } : {},
116
+ ...deletedItemIds ? { deletedItemIds } : {}
113
117
  };
114
118
  }
115
119
  function parseRealtimeSessionPeer(value) {
@@ -346,6 +350,7 @@ function useRealtimeCanvasDocument(options) {
346
350
  const realtimeEnabled = enabled && session != null;
347
351
  const documentRevision = session?.document?.revision ?? null;
348
352
  const documentItems = session?.document?.items;
353
+ const documentDeletedItemIds = session?.document?.deletedItemIds;
349
354
  const documentUpdatedByClientId = session?.document?.updatedByClientId ?? null;
350
355
  const connectionClientId = session?.connection.clientId ?? null;
351
356
  const hasLocalOfflineDraft = session?.hasLocalOfflineDraft ?? false;
@@ -395,15 +400,22 @@ function useRealtimeCanvasDocument(options) {
395
400
  if (cancelled) return;
396
401
  if (inFlightRevisionRef.current !== documentRevision) return;
397
402
  lastAppliedRevisionRef.current = documentRevision;
398
- const localItems = latestItemsRef.current;
403
+ const rawLocalItems = latestItemsRef.current;
404
+ const deletedIds = new Set(documentDeletedItemIds ?? []);
405
+ const localItems = deletedIds.size > 0 ? rawLocalItems.filter((item) => {
406
+ const id = getSceneItemId(item);
407
+ return !id || !deletedIds.has(id);
408
+ }) : rawLocalItems;
399
409
  const hasLocalItems = localItems.length > 0;
400
410
  const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
401
411
  if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
402
412
  if (hasLocalItems) {
403
413
  const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
404
414
  session.remoteAdapter.send?.(normalizedLocalItems);
415
+ return;
405
416
  }
406
- return;
417
+ const emptinessExplainedByDeletes = rawLocalItems.length > 0 && localItems.length === 0;
418
+ if (!emptinessExplainedByDeletes) return;
407
419
  }
408
420
  if (shouldPreserveLocalRealtimeItems({
409
421
  localItems,
@@ -431,6 +443,7 @@ function useRealtimeCanvasDocument(options) {
431
443
  }, [
432
444
  applyIncomingItems,
433
445
  connectionClientId,
446
+ documentDeletedItemIds,
434
447
  documentItems,
435
448
  documentRevision,
436
449
  documentUpdatedByClientId,
@@ -607,6 +620,16 @@ var CLIENT_ONLY_IMAGE_KEYS2 = /* @__PURE__ */ new Set([
607
620
  "imageThumbnailHref"
608
621
  ]);
609
622
  var SERVER_ADD_RACE_WINDOW_MS = 2e3;
623
+ var LOCAL_DELETE_TOMBSTONE_TTL_MS = 1e4;
624
+ function isLocalDeleteTombstoneActive(board, id, now) {
625
+ const deletedAt = board.locallyDeletedItemIds.get(id);
626
+ if (deletedAt == null) return false;
627
+ if (now - deletedAt >= LOCAL_DELETE_TOMBSTONE_TTL_MS) {
628
+ board.locallyDeletedItemIds.delete(id);
629
+ return false;
630
+ }
631
+ return true;
632
+ }
610
633
  function createYjsBoardDoc() {
611
634
  const doc = new Y__namespace.Doc();
612
635
  const yItems = doc.getArray(ITEMS_KEY);
@@ -615,9 +638,22 @@ function createYjsBoardDoc() {
615
638
  yItems,
616
639
  lastServerConfirmedIds: /* @__PURE__ */ new Set(),
617
640
  lastServerConfirmedItemSerializations: /* @__PURE__ */ new Map(),
618
- serverItemSeenAt: /* @__PURE__ */ new Map()
641
+ serverItemSeenAt: /* @__PURE__ */ new Map(),
642
+ serverDeletedItemIds: /* @__PURE__ */ new Set(),
643
+ locallyDeletedItemIds: /* @__PURE__ */ new Map(),
644
+ consumerSeenItemIds: /* @__PURE__ */ new Set()
619
645
  };
620
646
  }
647
+ function getRealtimeDeletedItemIds(board) {
648
+ const now = Date.now();
649
+ const result = new Set(board.serverDeletedItemIds);
650
+ for (const id of Array.from(board.locallyDeletedItemIds.keys())) {
651
+ if (isLocalDeleteTombstoneActive(board, id, now)) {
652
+ result.add(id);
653
+ }
654
+ }
655
+ return result;
656
+ }
621
657
  function getItemId(item) {
622
658
  if (item instanceof Y__namespace.Map) {
623
659
  const id2 = item.get("id");
@@ -698,6 +734,38 @@ function normalizeRealtimeItem(item) {
698
734
  normalized.childrenSvg = "";
699
735
  return normalized;
700
736
  }
737
+ function reorderYItemsToMatchLocalItems(yItems, items) {
738
+ const currentItems = readVectorItems(yItems);
739
+ const currentItemsById = /* @__PURE__ */ new Map();
740
+ for (const currentItem of currentItems) {
741
+ const id = getItemId(currentItem);
742
+ if (id) currentItemsById.set(id, currentItem);
743
+ }
744
+ const orderedItems = [];
745
+ const orderedIds = /* @__PURE__ */ new Set();
746
+ for (const item of items) {
747
+ const id = getItemId(item);
748
+ if (!id) continue;
749
+ const currentItem = currentItemsById.get(id);
750
+ if (!currentItem) continue;
751
+ orderedItems.push(currentItem);
752
+ orderedIds.add(id);
753
+ }
754
+ for (const currentItem of currentItems) {
755
+ const id = getItemId(currentItem);
756
+ if (!id || !orderedIds.has(id)) orderedItems.push(currentItem);
757
+ }
758
+ const alreadyOrdered = currentItems.length === orderedItems.length && currentItems.every((currentItem, index) => {
759
+ const nextItem = orderedItems[index];
760
+ return nextItem ? getItemId(currentItem) === getItemId(nextItem) : false;
761
+ });
762
+ if (alreadyOrdered) return;
763
+ if (yItems.length > 0) yItems.delete(0, yItems.length);
764
+ yItems.insert(
765
+ 0,
766
+ orderedItems.map((item) => vectorItemToYMap(item))
767
+ );
768
+ }
701
769
  function applyLocalItemsToYDoc(board, options) {
702
770
  const { items, origin } = options;
703
771
  const addedIds = [];
@@ -714,9 +782,11 @@ function applyLocalItemsToYDoc(board, options) {
714
782
  const toDelete = [];
715
783
  for (const [id, entry] of currentIndex) {
716
784
  if (nextIds.has(id)) continue;
717
- const serverSeenAt = board.serverItemSeenAt.get(id);
718
- if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
719
- continue;
785
+ if (!board.consumerSeenItemIds.has(id)) {
786
+ const serverSeenAt = board.serverItemSeenAt.get(id);
787
+ if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
788
+ continue;
789
+ }
720
790
  }
721
791
  toDelete.push({ id, index: entry.index });
722
792
  }
@@ -724,6 +794,7 @@ function applyLocalItemsToYDoc(board, options) {
724
794
  for (const { id, index } of toDelete) {
725
795
  board.yItems.delete(index, 1);
726
796
  board.serverItemSeenAt.delete(id);
797
+ board.locallyDeletedItemIds.set(id, now);
727
798
  removedIds.push(id);
728
799
  }
729
800
  const refreshedIndex = indexYItemsById(board.yItems);
@@ -732,6 +803,9 @@ function applyLocalItemsToYDoc(board, options) {
732
803
  if (!item) continue;
733
804
  const id = getItemId(item);
734
805
  if (!id) continue;
806
+ board.consumerSeenItemIds.add(id);
807
+ if (board.serverDeletedItemIds.has(id)) continue;
808
+ board.locallyDeletedItemIds.delete(id);
735
809
  const existing = refreshedIndex.get(id);
736
810
  if (existing) {
737
811
  updateYMapInPlace(existing.yMap, item);
@@ -741,16 +815,38 @@ function applyLocalItemsToYDoc(board, options) {
741
815
  board.yItems.push([yMap]);
742
816
  addedIds.push(id);
743
817
  }
818
+ reorderYItemsToMatchLocalItems(board.yItems, items);
744
819
  }, origin);
745
820
  return { addedIds, removedIds };
746
821
  }
747
822
  function applyServerSnapshotToYDoc(board, options) {
748
- const { items: snapshotItems, origin } = options;
823
+ const { items: snapshotItems, deletedItemIds, origin } = options;
749
824
  const now = Date.now();
825
+ const snapshotItemIds = /* @__PURE__ */ new Set();
826
+ for (const item of snapshotItems) {
827
+ const id = getItemId(item);
828
+ if (id) snapshotItemIds.add(id);
829
+ }
750
830
  board.doc.transact(() => {
831
+ if (deletedItemIds) {
832
+ for (const id of deletedItemIds) {
833
+ if (snapshotItemIds.has(id)) continue;
834
+ board.serverDeletedItemIds.add(id);
835
+ board.locallyDeletedItemIds.delete(id);
836
+ board.serverItemSeenAt.delete(id);
837
+ const existing = indexYItemsById(board.yItems).get(id);
838
+ if (existing) {
839
+ board.yItems.delete(existing.index, 1);
840
+ }
841
+ }
842
+ }
751
843
  for (const item of snapshotItems) {
752
844
  const id = getItemId(item);
753
845
  if (!id) continue;
846
+ if (board.serverDeletedItemIds.has(id)) continue;
847
+ if (isLocalDeleteTombstoneActive(board, id, now)) {
848
+ continue;
849
+ }
754
850
  const existing = indexYItemsById(board.yItems).get(id);
755
851
  if (existing) {
756
852
  const currentSerialized = serializeItem(existing.yMap);
@@ -781,7 +877,7 @@ function applyServerSnapshotToYDoc(board, options) {
781
877
  }
782
878
  }
783
879
  function replaceYDocWithSnapshot(board, options) {
784
- const { items: snapshotItems, origin } = options;
880
+ const { items: snapshotItems, deletedItemIds, origin } = options;
785
881
  const now = Date.now();
786
882
  board.doc.transact(() => {
787
883
  if (board.yItems.length > 0) {
@@ -794,6 +890,12 @@ function replaceYDocWithSnapshot(board, options) {
794
890
  board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
795
891
  board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
796
892
  board.serverItemSeenAt = /* @__PURE__ */ new Map();
893
+ board.locallyDeletedItemIds = /* @__PURE__ */ new Map();
894
+ if (deletedItemIds) {
895
+ for (const id of deletedItemIds) {
896
+ board.serverDeletedItemIds.add(id);
897
+ }
898
+ }
797
899
  for (const item of snapshotItems) {
798
900
  const id = getItemId(item);
799
901
  if (id) {
@@ -1240,7 +1342,7 @@ function useRealtimeSession(options) {
1240
1342
  const applyDocument = react.useCallback(
1241
1343
  (snapshot, options2) => {
1242
1344
  const currentSnapshot = latestDocumentRef.current;
1243
- if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && currentSnapshot.persistedRevision === snapshot.persistedRevision && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
1345
+ if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && currentSnapshot.persistedRevision === snapshot.persistedRevision && (currentSnapshot.deletedItemIds?.length ?? 0) === (snapshot.deletedItemIds?.length ?? 0) && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
1244
1346
  return;
1245
1347
  }
1246
1348
  const board = boardRef.current;
@@ -1259,11 +1361,13 @@ function useRealtimeSession(options) {
1259
1361
  if (options2?.replace) {
1260
1362
  replaceYDocWithSnapshot(board, {
1261
1363
  items: snapshot.items,
1364
+ deletedItemIds: snapshot.deletedItemIds,
1262
1365
  origin: ORIGIN_REMOTE
1263
1366
  });
1264
1367
  } else {
1265
1368
  applyServerSnapshotToYDoc(board, {
1266
1369
  items: snapshot.items,
1370
+ deletedItemIds: snapshot.deletedItemIds,
1267
1371
  origin: ORIGIN_REMOTE
1268
1372
  });
1269
1373
  }
@@ -1271,7 +1375,11 @@ function useRealtimeSession(options) {
1271
1375
  const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
1272
1376
  const mergedSnapshot = {
1273
1377
  ...snapshot,
1274
- items: mergedItems
1378
+ items: mergedItems,
1379
+ // Expose the accumulated tombstones (server-confirmed + pending
1380
+ // local deletes) so document consumers can prune stale local
1381
+ // views instead of re-sending deleted items.
1382
+ ...board ? { deletedItemIds: Array.from(getRealtimeDeletedItemIds(board)) } : {}
1275
1383
  };
1276
1384
  currentRevisionRef.current = snapshot.revision;
1277
1385
  latestDocumentRef.current = mergedSnapshot;