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.
@@ -47,6 +47,11 @@ type RealtimeDocumentSnapshot = {
47
47
  readonly updatedAt: number;
48
48
  readonly updatedByClientId?: string;
49
49
  readonly persistedRevision?: number;
50
+ /**
51
+ * Tombstones for items deleted in the room. Sent by servers that support
52
+ * explicit delete propagation; absent on older servers.
53
+ */
54
+ readonly deletedItemIds?: readonly string[];
50
55
  };
51
56
  type RealtimeConnectionInfo = {
52
57
  readonly state: RealtimeConnectionState;
@@ -47,6 +47,11 @@ type RealtimeDocumentSnapshot = {
47
47
  readonly updatedAt: number;
48
48
  readonly updatedByClientId?: string;
49
49
  readonly persistedRevision?: number;
50
+ /**
51
+ * Tombstones for items deleted in the room. Sent by servers that support
52
+ * explicit delete propagation; absent on older servers.
53
+ */
54
+ readonly deletedItemIds?: readonly string[];
50
55
  };
51
56
  type RealtimeConnectionInfo = {
52
57
  readonly state: RealtimeConnectionState;
package/dist/realtime.js CHANGED
@@ -501,12 +501,16 @@ function parseDocumentSnapshot(value) {
501
501
  const updatedAt = getNumber(value.updatedAt);
502
502
  const items = parseItems(value.items);
503
503
  if (revision == null || updatedAt == null || items == null) return void 0;
504
+ const deletedItemIds = Array.isArray(value.deletedItemIds) ? value.deletedItemIds.filter(
505
+ (entry) => typeof entry === "string"
506
+ ) : void 0;
504
507
  return {
505
508
  revision,
506
509
  updatedAt,
507
510
  items,
508
511
  ...getString(value.updatedByClientId) ? { updatedByClientId: getString(value.updatedByClientId) } : {},
509
- ...getNumber(value.persistedRevision) != null ? { persistedRevision: getNumber(value.persistedRevision) } : {}
512
+ ...getNumber(value.persistedRevision) != null ? { persistedRevision: getNumber(value.persistedRevision) } : {},
513
+ ...deletedItemIds ? { deletedItemIds } : {}
510
514
  };
511
515
  }
512
516
  function parseRealtimeSessionPeer(value) {
@@ -2004,6 +2008,16 @@ var CLIENT_ONLY_IMAGE_KEYS = /* @__PURE__ */ new Set([
2004
2008
  "imageThumbnailHref"
2005
2009
  ]);
2006
2010
  var SERVER_ADD_RACE_WINDOW_MS = 2e3;
2011
+ var LOCAL_DELETE_TOMBSTONE_TTL_MS = 1e4;
2012
+ function isLocalDeleteTombstoneActive(board, id, now) {
2013
+ const deletedAt = board.locallyDeletedItemIds.get(id);
2014
+ if (deletedAt == null) return false;
2015
+ if (now - deletedAt >= LOCAL_DELETE_TOMBSTONE_TTL_MS) {
2016
+ board.locallyDeletedItemIds.delete(id);
2017
+ return false;
2018
+ }
2019
+ return true;
2020
+ }
2007
2021
  function createYjsBoardDoc() {
2008
2022
  const doc = new Y.Doc();
2009
2023
  const yItems = doc.getArray(ITEMS_KEY);
@@ -2012,9 +2026,22 @@ function createYjsBoardDoc() {
2012
2026
  yItems,
2013
2027
  lastServerConfirmedIds: /* @__PURE__ */ new Set(),
2014
2028
  lastServerConfirmedItemSerializations: /* @__PURE__ */ new Map(),
2015
- serverItemSeenAt: /* @__PURE__ */ new Map()
2029
+ serverItemSeenAt: /* @__PURE__ */ new Map(),
2030
+ serverDeletedItemIds: /* @__PURE__ */ new Set(),
2031
+ locallyDeletedItemIds: /* @__PURE__ */ new Map(),
2032
+ consumerSeenItemIds: /* @__PURE__ */ new Set()
2016
2033
  };
2017
2034
  }
2035
+ function getRealtimeDeletedItemIds(board) {
2036
+ const now = Date.now();
2037
+ const result = new Set(board.serverDeletedItemIds);
2038
+ for (const id of Array.from(board.locallyDeletedItemIds.keys())) {
2039
+ if (isLocalDeleteTombstoneActive(board, id, now)) {
2040
+ result.add(id);
2041
+ }
2042
+ }
2043
+ return result;
2044
+ }
2018
2045
  function getItemId(item) {
2019
2046
  if (item instanceof Y.Map) {
2020
2047
  const id2 = item.get("id");
@@ -2143,9 +2170,11 @@ function applyLocalItemsToYDoc(board, options) {
2143
2170
  const toDelete = [];
2144
2171
  for (const [id, entry] of currentIndex) {
2145
2172
  if (nextIds.has(id)) continue;
2146
- const serverSeenAt = board.serverItemSeenAt.get(id);
2147
- if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
2148
- continue;
2173
+ if (!board.consumerSeenItemIds.has(id)) {
2174
+ const serverSeenAt = board.serverItemSeenAt.get(id);
2175
+ if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
2176
+ continue;
2177
+ }
2149
2178
  }
2150
2179
  toDelete.push({ id, index: entry.index });
2151
2180
  }
@@ -2153,6 +2182,7 @@ function applyLocalItemsToYDoc(board, options) {
2153
2182
  for (const { id, index } of toDelete) {
2154
2183
  board.yItems.delete(index, 1);
2155
2184
  board.serverItemSeenAt.delete(id);
2185
+ board.locallyDeletedItemIds.set(id, now);
2156
2186
  removedIds.push(id);
2157
2187
  }
2158
2188
  const refreshedIndex = indexYItemsById(board.yItems);
@@ -2161,6 +2191,9 @@ function applyLocalItemsToYDoc(board, options) {
2161
2191
  if (!item) continue;
2162
2192
  const id = getItemId(item);
2163
2193
  if (!id) continue;
2194
+ board.consumerSeenItemIds.add(id);
2195
+ if (board.serverDeletedItemIds.has(id)) continue;
2196
+ board.locallyDeletedItemIds.delete(id);
2164
2197
  const existing = refreshedIndex.get(id);
2165
2198
  if (existing) {
2166
2199
  updateYMapInPlace(existing.yMap, item);
@@ -2175,12 +2208,33 @@ function applyLocalItemsToYDoc(board, options) {
2175
2208
  return { addedIds, removedIds };
2176
2209
  }
2177
2210
  function applyServerSnapshotToYDoc(board, options) {
2178
- const { items: snapshotItems, origin } = options;
2211
+ const { items: snapshotItems, deletedItemIds, origin } = options;
2179
2212
  const now = Date.now();
2213
+ const snapshotItemIds = /* @__PURE__ */ new Set();
2214
+ for (const item of snapshotItems) {
2215
+ const id = getItemId(item);
2216
+ if (id) snapshotItemIds.add(id);
2217
+ }
2180
2218
  board.doc.transact(() => {
2219
+ if (deletedItemIds) {
2220
+ for (const id of deletedItemIds) {
2221
+ if (snapshotItemIds.has(id)) continue;
2222
+ board.serverDeletedItemIds.add(id);
2223
+ board.locallyDeletedItemIds.delete(id);
2224
+ board.serverItemSeenAt.delete(id);
2225
+ const existing = indexYItemsById(board.yItems).get(id);
2226
+ if (existing) {
2227
+ board.yItems.delete(existing.index, 1);
2228
+ }
2229
+ }
2230
+ }
2181
2231
  for (const item of snapshotItems) {
2182
2232
  const id = getItemId(item);
2183
2233
  if (!id) continue;
2234
+ if (board.serverDeletedItemIds.has(id)) continue;
2235
+ if (isLocalDeleteTombstoneActive(board, id, now)) {
2236
+ continue;
2237
+ }
2184
2238
  const existing = indexYItemsById(board.yItems).get(id);
2185
2239
  if (existing) {
2186
2240
  const currentSerialized = serializeItem(existing.yMap);
@@ -2211,7 +2265,7 @@ function applyServerSnapshotToYDoc(board, options) {
2211
2265
  }
2212
2266
  }
2213
2267
  function replaceYDocWithSnapshot(board, options) {
2214
- const { items: snapshotItems, origin } = options;
2268
+ const { items: snapshotItems, deletedItemIds, origin } = options;
2215
2269
  const now = Date.now();
2216
2270
  board.doc.transact(() => {
2217
2271
  if (board.yItems.length > 0) {
@@ -2224,6 +2278,12 @@ function replaceYDocWithSnapshot(board, options) {
2224
2278
  board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
2225
2279
  board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
2226
2280
  board.serverItemSeenAt = /* @__PURE__ */ new Map();
2281
+ board.locallyDeletedItemIds = /* @__PURE__ */ new Map();
2282
+ if (deletedItemIds) {
2283
+ for (const id of deletedItemIds) {
2284
+ board.serverDeletedItemIds.add(id);
2285
+ }
2286
+ }
2227
2287
  for (const item of snapshotItems) {
2228
2288
  const id = getItemId(item);
2229
2289
  if (id) {
@@ -2586,7 +2646,7 @@ function useRealtimeSession(options) {
2586
2646
  const applyDocument = useCallback(
2587
2647
  (snapshot, options2) => {
2588
2648
  const currentSnapshot = latestDocumentRef.current;
2589
- if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && currentSnapshot.persistedRevision === snapshot.persistedRevision && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
2649
+ 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)) {
2590
2650
  return;
2591
2651
  }
2592
2652
  const board = boardRef.current;
@@ -2605,11 +2665,13 @@ function useRealtimeSession(options) {
2605
2665
  if (options2?.replace) {
2606
2666
  replaceYDocWithSnapshot(board, {
2607
2667
  items: snapshot.items,
2668
+ deletedItemIds: snapshot.deletedItemIds,
2608
2669
  origin: ORIGIN_REMOTE
2609
2670
  });
2610
2671
  } else {
2611
2672
  applyServerSnapshotToYDoc(board, {
2612
2673
  items: snapshot.items,
2674
+ deletedItemIds: snapshot.deletedItemIds,
2613
2675
  origin: ORIGIN_REMOTE
2614
2676
  });
2615
2677
  }
@@ -2617,7 +2679,11 @@ function useRealtimeSession(options) {
2617
2679
  const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
2618
2680
  const mergedSnapshot = {
2619
2681
  ...snapshot,
2620
- items: mergedItems
2682
+ items: mergedItems,
2683
+ // Expose the accumulated tombstones (server-confirmed + pending
2684
+ // local deletes) so document consumers can prune stale local
2685
+ // views instead of re-sending deleted items.
2686
+ ...board ? { deletedItemIds: Array.from(getRealtimeDeletedItemIds(board)) } : {}
2621
2687
  };
2622
2688
  currentRevisionRef.current = snapshot.revision;
2623
2689
  latestDocumentRef.current = mergedSnapshot;
@@ -3742,6 +3808,7 @@ function useRealtimeCanvasDocument(options) {
3742
3808
  const realtimeEnabled = enabled && session != null;
3743
3809
  const documentRevision = session?.document?.revision ?? null;
3744
3810
  const documentItems = session?.document?.items;
3811
+ const documentDeletedItemIds = session?.document?.deletedItemIds;
3745
3812
  const documentUpdatedByClientId = session?.document?.updatedByClientId ?? null;
3746
3813
  const connectionClientId = session?.connection.clientId ?? null;
3747
3814
  const hasLocalOfflineDraft = session?.hasLocalOfflineDraft ?? false;
@@ -3794,15 +3861,22 @@ function useRealtimeCanvasDocument(options) {
3794
3861
  if (cancelled) return;
3795
3862
  if (inFlightRevisionRef.current !== documentRevision) return;
3796
3863
  lastAppliedRevisionRef.current = documentRevision;
3797
- const localItems = latestItemsRef.current;
3864
+ const rawLocalItems = latestItemsRef.current;
3865
+ const deletedIds = new Set(documentDeletedItemIds ?? []);
3866
+ const localItems = deletedIds.size > 0 ? rawLocalItems.filter((item) => {
3867
+ const id = getSceneItemId(item);
3868
+ return !id || !deletedIds.has(id);
3869
+ }) : rawLocalItems;
3798
3870
  const hasLocalItems = localItems.length > 0;
3799
3871
  const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
3800
3872
  if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
3801
3873
  if (hasLocalItems) {
3802
3874
  const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
3803
3875
  session.remoteAdapter.send?.(normalizedLocalItems);
3876
+ return;
3804
3877
  }
3805
- return;
3878
+ const emptinessExplainedByDeletes = rawLocalItems.length > 0 && localItems.length === 0;
3879
+ if (!emptinessExplainedByDeletes) return;
3806
3880
  }
3807
3881
  if (shouldPreserveLocalRealtimeItems({
3808
3882
  localItems,
@@ -3830,6 +3904,7 @@ function useRealtimeCanvasDocument(options) {
3830
3904
  }, [
3831
3905
  applyIncomingItems,
3832
3906
  connectionClientId,
3907
+ documentDeletedItemIds,
3833
3908
  documentItems,
3834
3909
  documentRevision,
3835
3910
  documentUpdatedByClientId,