canvu-react 0.4.80 → 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");
@@ -746,9 +782,11 @@ function applyLocalItemsToYDoc(board, options) {
746
782
  const toDelete = [];
747
783
  for (const [id, entry] of currentIndex) {
748
784
  if (nextIds.has(id)) continue;
749
- const serverSeenAt = board.serverItemSeenAt.get(id);
750
- if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
751
- 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
+ }
752
790
  }
753
791
  toDelete.push({ id, index: entry.index });
754
792
  }
@@ -756,6 +794,7 @@ function applyLocalItemsToYDoc(board, options) {
756
794
  for (const { id, index } of toDelete) {
757
795
  board.yItems.delete(index, 1);
758
796
  board.serverItemSeenAt.delete(id);
797
+ board.locallyDeletedItemIds.set(id, now);
759
798
  removedIds.push(id);
760
799
  }
761
800
  const refreshedIndex = indexYItemsById(board.yItems);
@@ -764,6 +803,9 @@ function applyLocalItemsToYDoc(board, options) {
764
803
  if (!item) continue;
765
804
  const id = getItemId(item);
766
805
  if (!id) continue;
806
+ board.consumerSeenItemIds.add(id);
807
+ if (board.serverDeletedItemIds.has(id)) continue;
808
+ board.locallyDeletedItemIds.delete(id);
767
809
  const existing = refreshedIndex.get(id);
768
810
  if (existing) {
769
811
  updateYMapInPlace(existing.yMap, item);
@@ -778,12 +820,33 @@ function applyLocalItemsToYDoc(board, options) {
778
820
  return { addedIds, removedIds };
779
821
  }
780
822
  function applyServerSnapshotToYDoc(board, options) {
781
- const { items: snapshotItems, origin } = options;
823
+ const { items: snapshotItems, deletedItemIds, origin } = options;
782
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
+ }
783
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
+ }
784
843
  for (const item of snapshotItems) {
785
844
  const id = getItemId(item);
786
845
  if (!id) continue;
846
+ if (board.serverDeletedItemIds.has(id)) continue;
847
+ if (isLocalDeleteTombstoneActive(board, id, now)) {
848
+ continue;
849
+ }
787
850
  const existing = indexYItemsById(board.yItems).get(id);
788
851
  if (existing) {
789
852
  const currentSerialized = serializeItem(existing.yMap);
@@ -814,7 +877,7 @@ function applyServerSnapshotToYDoc(board, options) {
814
877
  }
815
878
  }
816
879
  function replaceYDocWithSnapshot(board, options) {
817
- const { items: snapshotItems, origin } = options;
880
+ const { items: snapshotItems, deletedItemIds, origin } = options;
818
881
  const now = Date.now();
819
882
  board.doc.transact(() => {
820
883
  if (board.yItems.length > 0) {
@@ -827,6 +890,12 @@ function replaceYDocWithSnapshot(board, options) {
827
890
  board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
828
891
  board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
829
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
+ }
830
899
  for (const item of snapshotItems) {
831
900
  const id = getItemId(item);
832
901
  if (id) {
@@ -1273,7 +1342,7 @@ function useRealtimeSession(options) {
1273
1342
  const applyDocument = react.useCallback(
1274
1343
  (snapshot, options2) => {
1275
1344
  const currentSnapshot = latestDocumentRef.current;
1276
- 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)) {
1277
1346
  return;
1278
1347
  }
1279
1348
  const board = boardRef.current;
@@ -1292,11 +1361,13 @@ function useRealtimeSession(options) {
1292
1361
  if (options2?.replace) {
1293
1362
  replaceYDocWithSnapshot(board, {
1294
1363
  items: snapshot.items,
1364
+ deletedItemIds: snapshot.deletedItemIds,
1295
1365
  origin: ORIGIN_REMOTE
1296
1366
  });
1297
1367
  } else {
1298
1368
  applyServerSnapshotToYDoc(board, {
1299
1369
  items: snapshot.items,
1370
+ deletedItemIds: snapshot.deletedItemIds,
1300
1371
  origin: ORIGIN_REMOTE
1301
1372
  });
1302
1373
  }
@@ -1304,7 +1375,11 @@ function useRealtimeSession(options) {
1304
1375
  const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
1305
1376
  const mergedSnapshot = {
1306
1377
  ...snapshot,
1307
- 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)) } : {}
1308
1383
  };
1309
1384
  currentRevisionRef.current = snapshot.revision;
1310
1385
  latestDocumentRef.current = mergedSnapshot;