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.
@@ -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");
@@ -2095,6 +2122,38 @@ function normalizeRealtimeItem(item) {
2095
2122
  normalized.childrenSvg = "";
2096
2123
  return normalized;
2097
2124
  }
2125
+ function reorderYItemsToMatchLocalItems(yItems, items) {
2126
+ const currentItems = readVectorItems(yItems);
2127
+ const currentItemsById = /* @__PURE__ */ new Map();
2128
+ for (const currentItem of currentItems) {
2129
+ const id = getItemId(currentItem);
2130
+ if (id) currentItemsById.set(id, currentItem);
2131
+ }
2132
+ const orderedItems = [];
2133
+ const orderedIds = /* @__PURE__ */ new Set();
2134
+ for (const item of items) {
2135
+ const id = getItemId(item);
2136
+ if (!id) continue;
2137
+ const currentItem = currentItemsById.get(id);
2138
+ if (!currentItem) continue;
2139
+ orderedItems.push(currentItem);
2140
+ orderedIds.add(id);
2141
+ }
2142
+ for (const currentItem of currentItems) {
2143
+ const id = getItemId(currentItem);
2144
+ if (!id || !orderedIds.has(id)) orderedItems.push(currentItem);
2145
+ }
2146
+ const alreadyOrdered = currentItems.length === orderedItems.length && currentItems.every((currentItem, index) => {
2147
+ const nextItem = orderedItems[index];
2148
+ return nextItem ? getItemId(currentItem) === getItemId(nextItem) : false;
2149
+ });
2150
+ if (alreadyOrdered) return;
2151
+ if (yItems.length > 0) yItems.delete(0, yItems.length);
2152
+ yItems.insert(
2153
+ 0,
2154
+ orderedItems.map((item) => vectorItemToYMap(item))
2155
+ );
2156
+ }
2098
2157
  function applyLocalItemsToYDoc(board, options) {
2099
2158
  const { items, origin } = options;
2100
2159
  const addedIds = [];
@@ -2111,9 +2170,11 @@ function applyLocalItemsToYDoc(board, options) {
2111
2170
  const toDelete = [];
2112
2171
  for (const [id, entry] of currentIndex) {
2113
2172
  if (nextIds.has(id)) continue;
2114
- const serverSeenAt = board.serverItemSeenAt.get(id);
2115
- if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
2116
- 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
+ }
2117
2178
  }
2118
2179
  toDelete.push({ id, index: entry.index });
2119
2180
  }
@@ -2121,6 +2182,7 @@ function applyLocalItemsToYDoc(board, options) {
2121
2182
  for (const { id, index } of toDelete) {
2122
2183
  board.yItems.delete(index, 1);
2123
2184
  board.serverItemSeenAt.delete(id);
2185
+ board.locallyDeletedItemIds.set(id, now);
2124
2186
  removedIds.push(id);
2125
2187
  }
2126
2188
  const refreshedIndex = indexYItemsById(board.yItems);
@@ -2129,6 +2191,9 @@ function applyLocalItemsToYDoc(board, options) {
2129
2191
  if (!item) continue;
2130
2192
  const id = getItemId(item);
2131
2193
  if (!id) continue;
2194
+ board.consumerSeenItemIds.add(id);
2195
+ if (board.serverDeletedItemIds.has(id)) continue;
2196
+ board.locallyDeletedItemIds.delete(id);
2132
2197
  const existing = refreshedIndex.get(id);
2133
2198
  if (existing) {
2134
2199
  updateYMapInPlace(existing.yMap, item);
@@ -2138,16 +2203,38 @@ function applyLocalItemsToYDoc(board, options) {
2138
2203
  board.yItems.push([yMap]);
2139
2204
  addedIds.push(id);
2140
2205
  }
2206
+ reorderYItemsToMatchLocalItems(board.yItems, items);
2141
2207
  }, origin);
2142
2208
  return { addedIds, removedIds };
2143
2209
  }
2144
2210
  function applyServerSnapshotToYDoc(board, options) {
2145
- const { items: snapshotItems, origin } = options;
2211
+ const { items: snapshotItems, deletedItemIds, origin } = options;
2146
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
+ }
2147
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
+ }
2148
2231
  for (const item of snapshotItems) {
2149
2232
  const id = getItemId(item);
2150
2233
  if (!id) continue;
2234
+ if (board.serverDeletedItemIds.has(id)) continue;
2235
+ if (isLocalDeleteTombstoneActive(board, id, now)) {
2236
+ continue;
2237
+ }
2151
2238
  const existing = indexYItemsById(board.yItems).get(id);
2152
2239
  if (existing) {
2153
2240
  const currentSerialized = serializeItem(existing.yMap);
@@ -2178,7 +2265,7 @@ function applyServerSnapshotToYDoc(board, options) {
2178
2265
  }
2179
2266
  }
2180
2267
  function replaceYDocWithSnapshot(board, options) {
2181
- const { items: snapshotItems, origin } = options;
2268
+ const { items: snapshotItems, deletedItemIds, origin } = options;
2182
2269
  const now = Date.now();
2183
2270
  board.doc.transact(() => {
2184
2271
  if (board.yItems.length > 0) {
@@ -2191,6 +2278,12 @@ function replaceYDocWithSnapshot(board, options) {
2191
2278
  board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
2192
2279
  board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
2193
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
+ }
2194
2287
  for (const item of snapshotItems) {
2195
2288
  const id = getItemId(item);
2196
2289
  if (id) {
@@ -2553,7 +2646,7 @@ function useRealtimeSession(options) {
2553
2646
  const applyDocument = useCallback(
2554
2647
  (snapshot, options2) => {
2555
2648
  const currentSnapshot = latestDocumentRef.current;
2556
- 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)) {
2557
2650
  return;
2558
2651
  }
2559
2652
  const board = boardRef.current;
@@ -2572,11 +2665,13 @@ function useRealtimeSession(options) {
2572
2665
  if (options2?.replace) {
2573
2666
  replaceYDocWithSnapshot(board, {
2574
2667
  items: snapshot.items,
2668
+ deletedItemIds: snapshot.deletedItemIds,
2575
2669
  origin: ORIGIN_REMOTE
2576
2670
  });
2577
2671
  } else {
2578
2672
  applyServerSnapshotToYDoc(board, {
2579
2673
  items: snapshot.items,
2674
+ deletedItemIds: snapshot.deletedItemIds,
2580
2675
  origin: ORIGIN_REMOTE
2581
2676
  });
2582
2677
  }
@@ -2584,7 +2679,11 @@ function useRealtimeSession(options) {
2584
2679
  const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
2585
2680
  const mergedSnapshot = {
2586
2681
  ...snapshot,
2587
- 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)) } : {}
2588
2687
  };
2589
2688
  currentRevisionRef.current = snapshot.revision;
2590
2689
  latestDocumentRef.current = mergedSnapshot;
@@ -3709,6 +3808,7 @@ function useRealtimeCanvasDocument(options) {
3709
3808
  const realtimeEnabled = enabled && session != null;
3710
3809
  const documentRevision = session?.document?.revision ?? null;
3711
3810
  const documentItems = session?.document?.items;
3811
+ const documentDeletedItemIds = session?.document?.deletedItemIds;
3712
3812
  const documentUpdatedByClientId = session?.document?.updatedByClientId ?? null;
3713
3813
  const connectionClientId = session?.connection.clientId ?? null;
3714
3814
  const hasLocalOfflineDraft = session?.hasLocalOfflineDraft ?? false;
@@ -3761,15 +3861,22 @@ function useRealtimeCanvasDocument(options) {
3761
3861
  if (cancelled) return;
3762
3862
  if (inFlightRevisionRef.current !== documentRevision) return;
3763
3863
  lastAppliedRevisionRef.current = documentRevision;
3764
- 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;
3765
3870
  const hasLocalItems = localItems.length > 0;
3766
3871
  const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
3767
3872
  if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
3768
3873
  if (hasLocalItems) {
3769
3874
  const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
3770
3875
  session.remoteAdapter.send?.(normalizedLocalItems);
3876
+ return;
3771
3877
  }
3772
- return;
3878
+ const emptinessExplainedByDeletes = rawLocalItems.length > 0 && localItems.length === 0;
3879
+ if (!emptinessExplainedByDeletes) return;
3773
3880
  }
3774
3881
  if (shouldPreserveLocalRealtimeItems({
3775
3882
  localItems,
@@ -3797,6 +3904,7 @@ function useRealtimeCanvasDocument(options) {
3797
3904
  }, [
3798
3905
  applyIncomingItems,
3799
3906
  connectionClientId,
3907
+ documentDeletedItemIds,
3800
3908
  documentItems,
3801
3909
  documentRevision,
3802
3910
  documentUpdatedByClientId,