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.
package/dist/realtime.cjs CHANGED
@@ -526,12 +526,16 @@ function parseDocumentSnapshot(value) {
526
526
  const updatedAt = getNumber(value.updatedAt);
527
527
  const items = parseItems(value.items);
528
528
  if (revision == null || updatedAt == null || items == null) return void 0;
529
+ const deletedItemIds = Array.isArray(value.deletedItemIds) ? value.deletedItemIds.filter(
530
+ (entry) => typeof entry === "string"
531
+ ) : void 0;
529
532
  return {
530
533
  revision,
531
534
  updatedAt,
532
535
  items,
533
536
  ...getString(value.updatedByClientId) ? { updatedByClientId: getString(value.updatedByClientId) } : {},
534
- ...getNumber(value.persistedRevision) != null ? { persistedRevision: getNumber(value.persistedRevision) } : {}
537
+ ...getNumber(value.persistedRevision) != null ? { persistedRevision: getNumber(value.persistedRevision) } : {},
538
+ ...deletedItemIds ? { deletedItemIds } : {}
535
539
  };
536
540
  }
537
541
  function parseRealtimeSessionPeer(value) {
@@ -2029,6 +2033,16 @@ var CLIENT_ONLY_IMAGE_KEYS = /* @__PURE__ */ new Set([
2029
2033
  "imageThumbnailHref"
2030
2034
  ]);
2031
2035
  var SERVER_ADD_RACE_WINDOW_MS = 2e3;
2036
+ var LOCAL_DELETE_TOMBSTONE_TTL_MS = 1e4;
2037
+ function isLocalDeleteTombstoneActive(board, id, now) {
2038
+ const deletedAt = board.locallyDeletedItemIds.get(id);
2039
+ if (deletedAt == null) return false;
2040
+ if (now - deletedAt >= LOCAL_DELETE_TOMBSTONE_TTL_MS) {
2041
+ board.locallyDeletedItemIds.delete(id);
2042
+ return false;
2043
+ }
2044
+ return true;
2045
+ }
2032
2046
  function createYjsBoardDoc() {
2033
2047
  const doc = new Y__namespace.Doc();
2034
2048
  const yItems = doc.getArray(ITEMS_KEY);
@@ -2037,9 +2051,22 @@ function createYjsBoardDoc() {
2037
2051
  yItems,
2038
2052
  lastServerConfirmedIds: /* @__PURE__ */ new Set(),
2039
2053
  lastServerConfirmedItemSerializations: /* @__PURE__ */ new Map(),
2040
- serverItemSeenAt: /* @__PURE__ */ new Map()
2054
+ serverItemSeenAt: /* @__PURE__ */ new Map(),
2055
+ serverDeletedItemIds: /* @__PURE__ */ new Set(),
2056
+ locallyDeletedItemIds: /* @__PURE__ */ new Map(),
2057
+ consumerSeenItemIds: /* @__PURE__ */ new Set()
2041
2058
  };
2042
2059
  }
2060
+ function getRealtimeDeletedItemIds(board) {
2061
+ const now = Date.now();
2062
+ const result = new Set(board.serverDeletedItemIds);
2063
+ for (const id of Array.from(board.locallyDeletedItemIds.keys())) {
2064
+ if (isLocalDeleteTombstoneActive(board, id, now)) {
2065
+ result.add(id);
2066
+ }
2067
+ }
2068
+ return result;
2069
+ }
2043
2070
  function getItemId(item) {
2044
2071
  if (item instanceof Y__namespace.Map) {
2045
2072
  const id2 = item.get("id");
@@ -2168,9 +2195,11 @@ function applyLocalItemsToYDoc(board, options) {
2168
2195
  const toDelete = [];
2169
2196
  for (const [id, entry] of currentIndex) {
2170
2197
  if (nextIds.has(id)) continue;
2171
- const serverSeenAt = board.serverItemSeenAt.get(id);
2172
- if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
2173
- continue;
2198
+ if (!board.consumerSeenItemIds.has(id)) {
2199
+ const serverSeenAt = board.serverItemSeenAt.get(id);
2200
+ if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
2201
+ continue;
2202
+ }
2174
2203
  }
2175
2204
  toDelete.push({ id, index: entry.index });
2176
2205
  }
@@ -2178,6 +2207,7 @@ function applyLocalItemsToYDoc(board, options) {
2178
2207
  for (const { id, index } of toDelete) {
2179
2208
  board.yItems.delete(index, 1);
2180
2209
  board.serverItemSeenAt.delete(id);
2210
+ board.locallyDeletedItemIds.set(id, now);
2181
2211
  removedIds.push(id);
2182
2212
  }
2183
2213
  const refreshedIndex = indexYItemsById(board.yItems);
@@ -2186,6 +2216,9 @@ function applyLocalItemsToYDoc(board, options) {
2186
2216
  if (!item) continue;
2187
2217
  const id = getItemId(item);
2188
2218
  if (!id) continue;
2219
+ board.consumerSeenItemIds.add(id);
2220
+ if (board.serverDeletedItemIds.has(id)) continue;
2221
+ board.locallyDeletedItemIds.delete(id);
2189
2222
  const existing = refreshedIndex.get(id);
2190
2223
  if (existing) {
2191
2224
  updateYMapInPlace(existing.yMap, item);
@@ -2200,12 +2233,33 @@ function applyLocalItemsToYDoc(board, options) {
2200
2233
  return { addedIds, removedIds };
2201
2234
  }
2202
2235
  function applyServerSnapshotToYDoc(board, options) {
2203
- const { items: snapshotItems, origin } = options;
2236
+ const { items: snapshotItems, deletedItemIds, origin } = options;
2204
2237
  const now = Date.now();
2238
+ const snapshotItemIds = /* @__PURE__ */ new Set();
2239
+ for (const item of snapshotItems) {
2240
+ const id = getItemId(item);
2241
+ if (id) snapshotItemIds.add(id);
2242
+ }
2205
2243
  board.doc.transact(() => {
2244
+ if (deletedItemIds) {
2245
+ for (const id of deletedItemIds) {
2246
+ if (snapshotItemIds.has(id)) continue;
2247
+ board.serverDeletedItemIds.add(id);
2248
+ board.locallyDeletedItemIds.delete(id);
2249
+ board.serverItemSeenAt.delete(id);
2250
+ const existing = indexYItemsById(board.yItems).get(id);
2251
+ if (existing) {
2252
+ board.yItems.delete(existing.index, 1);
2253
+ }
2254
+ }
2255
+ }
2206
2256
  for (const item of snapshotItems) {
2207
2257
  const id = getItemId(item);
2208
2258
  if (!id) continue;
2259
+ if (board.serverDeletedItemIds.has(id)) continue;
2260
+ if (isLocalDeleteTombstoneActive(board, id, now)) {
2261
+ continue;
2262
+ }
2209
2263
  const existing = indexYItemsById(board.yItems).get(id);
2210
2264
  if (existing) {
2211
2265
  const currentSerialized = serializeItem(existing.yMap);
@@ -2236,7 +2290,7 @@ function applyServerSnapshotToYDoc(board, options) {
2236
2290
  }
2237
2291
  }
2238
2292
  function replaceYDocWithSnapshot(board, options) {
2239
- const { items: snapshotItems, origin } = options;
2293
+ const { items: snapshotItems, deletedItemIds, origin } = options;
2240
2294
  const now = Date.now();
2241
2295
  board.doc.transact(() => {
2242
2296
  if (board.yItems.length > 0) {
@@ -2249,6 +2303,12 @@ function replaceYDocWithSnapshot(board, options) {
2249
2303
  board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
2250
2304
  board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
2251
2305
  board.serverItemSeenAt = /* @__PURE__ */ new Map();
2306
+ board.locallyDeletedItemIds = /* @__PURE__ */ new Map();
2307
+ if (deletedItemIds) {
2308
+ for (const id of deletedItemIds) {
2309
+ board.serverDeletedItemIds.add(id);
2310
+ }
2311
+ }
2252
2312
  for (const item of snapshotItems) {
2253
2313
  const id = getItemId(item);
2254
2314
  if (id) {
@@ -2611,7 +2671,7 @@ function useRealtimeSession(options) {
2611
2671
  const applyDocument = react.useCallback(
2612
2672
  (snapshot, options2) => {
2613
2673
  const currentSnapshot = latestDocumentRef.current;
2614
- if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && currentSnapshot.persistedRevision === snapshot.persistedRevision && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
2674
+ 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)) {
2615
2675
  return;
2616
2676
  }
2617
2677
  const board = boardRef.current;
@@ -2630,11 +2690,13 @@ function useRealtimeSession(options) {
2630
2690
  if (options2?.replace) {
2631
2691
  replaceYDocWithSnapshot(board, {
2632
2692
  items: snapshot.items,
2693
+ deletedItemIds: snapshot.deletedItemIds,
2633
2694
  origin: ORIGIN_REMOTE
2634
2695
  });
2635
2696
  } else {
2636
2697
  applyServerSnapshotToYDoc(board, {
2637
2698
  items: snapshot.items,
2699
+ deletedItemIds: snapshot.deletedItemIds,
2638
2700
  origin: ORIGIN_REMOTE
2639
2701
  });
2640
2702
  }
@@ -2642,7 +2704,11 @@ function useRealtimeSession(options) {
2642
2704
  const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
2643
2705
  const mergedSnapshot = {
2644
2706
  ...snapshot,
2645
- items: mergedItems
2707
+ items: mergedItems,
2708
+ // Expose the accumulated tombstones (server-confirmed + pending
2709
+ // local deletes) so document consumers can prune stale local
2710
+ // views instead of re-sending deleted items.
2711
+ ...board ? { deletedItemIds: Array.from(getRealtimeDeletedItemIds(board)) } : {}
2646
2712
  };
2647
2713
  currentRevisionRef.current = snapshot.revision;
2648
2714
  latestDocumentRef.current = mergedSnapshot;
@@ -3767,6 +3833,7 @@ function useRealtimeCanvasDocument(options) {
3767
3833
  const realtimeEnabled = enabled && session != null;
3768
3834
  const documentRevision = session?.document?.revision ?? null;
3769
3835
  const documentItems = session?.document?.items;
3836
+ const documentDeletedItemIds = session?.document?.deletedItemIds;
3770
3837
  const documentUpdatedByClientId = session?.document?.updatedByClientId ?? null;
3771
3838
  const connectionClientId = session?.connection.clientId ?? null;
3772
3839
  const hasLocalOfflineDraft = session?.hasLocalOfflineDraft ?? false;
@@ -3819,15 +3886,22 @@ function useRealtimeCanvasDocument(options) {
3819
3886
  if (cancelled) return;
3820
3887
  if (inFlightRevisionRef.current !== documentRevision) return;
3821
3888
  lastAppliedRevisionRef.current = documentRevision;
3822
- const localItems = latestItemsRef.current;
3889
+ const rawLocalItems = latestItemsRef.current;
3890
+ const deletedIds = new Set(documentDeletedItemIds ?? []);
3891
+ const localItems = deletedIds.size > 0 ? rawLocalItems.filter((item) => {
3892
+ const id = getSceneItemId(item);
3893
+ return !id || !deletedIds.has(id);
3894
+ }) : rawLocalItems;
3823
3895
  const hasLocalItems = localItems.length > 0;
3824
3896
  const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
3825
3897
  if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
3826
3898
  if (hasLocalItems) {
3827
3899
  const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
3828
3900
  session.remoteAdapter.send?.(normalizedLocalItems);
3901
+ return;
3829
3902
  }
3830
- return;
3903
+ const emptinessExplainedByDeletes = rawLocalItems.length > 0 && localItems.length === 0;
3904
+ if (!emptinessExplainedByDeletes) return;
3831
3905
  }
3832
3906
  if (shouldPreserveLocalRealtimeItems({
3833
3907
  localItems,
@@ -3855,6 +3929,7 @@ function useRealtimeCanvasDocument(options) {
3855
3929
  }, [
3856
3930
  applyIncomingItems,
3857
3931
  connectionClientId,
3932
+ documentDeletedItemIds,
3858
3933
  documentItems,
3859
3934
  documentRevision,
3860
3935
  documentUpdatedByClientId,