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.
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");
@@ -2120,6 +2147,38 @@ function normalizeRealtimeItem(item) {
2120
2147
  normalized.childrenSvg = "";
2121
2148
  return normalized;
2122
2149
  }
2150
+ function reorderYItemsToMatchLocalItems(yItems, items) {
2151
+ const currentItems = readVectorItems(yItems);
2152
+ const currentItemsById = /* @__PURE__ */ new Map();
2153
+ for (const currentItem of currentItems) {
2154
+ const id = getItemId(currentItem);
2155
+ if (id) currentItemsById.set(id, currentItem);
2156
+ }
2157
+ const orderedItems = [];
2158
+ const orderedIds = /* @__PURE__ */ new Set();
2159
+ for (const item of items) {
2160
+ const id = getItemId(item);
2161
+ if (!id) continue;
2162
+ const currentItem = currentItemsById.get(id);
2163
+ if (!currentItem) continue;
2164
+ orderedItems.push(currentItem);
2165
+ orderedIds.add(id);
2166
+ }
2167
+ for (const currentItem of currentItems) {
2168
+ const id = getItemId(currentItem);
2169
+ if (!id || !orderedIds.has(id)) orderedItems.push(currentItem);
2170
+ }
2171
+ const alreadyOrdered = currentItems.length === orderedItems.length && currentItems.every((currentItem, index) => {
2172
+ const nextItem = orderedItems[index];
2173
+ return nextItem ? getItemId(currentItem) === getItemId(nextItem) : false;
2174
+ });
2175
+ if (alreadyOrdered) return;
2176
+ if (yItems.length > 0) yItems.delete(0, yItems.length);
2177
+ yItems.insert(
2178
+ 0,
2179
+ orderedItems.map((item) => vectorItemToYMap(item))
2180
+ );
2181
+ }
2123
2182
  function applyLocalItemsToYDoc(board, options) {
2124
2183
  const { items, origin } = options;
2125
2184
  const addedIds = [];
@@ -2136,9 +2195,11 @@ function applyLocalItemsToYDoc(board, options) {
2136
2195
  const toDelete = [];
2137
2196
  for (const [id, entry] of currentIndex) {
2138
2197
  if (nextIds.has(id)) continue;
2139
- const serverSeenAt = board.serverItemSeenAt.get(id);
2140
- if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
2141
- 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
+ }
2142
2203
  }
2143
2204
  toDelete.push({ id, index: entry.index });
2144
2205
  }
@@ -2146,6 +2207,7 @@ function applyLocalItemsToYDoc(board, options) {
2146
2207
  for (const { id, index } of toDelete) {
2147
2208
  board.yItems.delete(index, 1);
2148
2209
  board.serverItemSeenAt.delete(id);
2210
+ board.locallyDeletedItemIds.set(id, now);
2149
2211
  removedIds.push(id);
2150
2212
  }
2151
2213
  const refreshedIndex = indexYItemsById(board.yItems);
@@ -2154,6 +2216,9 @@ function applyLocalItemsToYDoc(board, options) {
2154
2216
  if (!item) continue;
2155
2217
  const id = getItemId(item);
2156
2218
  if (!id) continue;
2219
+ board.consumerSeenItemIds.add(id);
2220
+ if (board.serverDeletedItemIds.has(id)) continue;
2221
+ board.locallyDeletedItemIds.delete(id);
2157
2222
  const existing = refreshedIndex.get(id);
2158
2223
  if (existing) {
2159
2224
  updateYMapInPlace(existing.yMap, item);
@@ -2163,16 +2228,38 @@ function applyLocalItemsToYDoc(board, options) {
2163
2228
  board.yItems.push([yMap]);
2164
2229
  addedIds.push(id);
2165
2230
  }
2231
+ reorderYItemsToMatchLocalItems(board.yItems, items);
2166
2232
  }, origin);
2167
2233
  return { addedIds, removedIds };
2168
2234
  }
2169
2235
  function applyServerSnapshotToYDoc(board, options) {
2170
- const { items: snapshotItems, origin } = options;
2236
+ const { items: snapshotItems, deletedItemIds, origin } = options;
2171
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
+ }
2172
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
+ }
2173
2256
  for (const item of snapshotItems) {
2174
2257
  const id = getItemId(item);
2175
2258
  if (!id) continue;
2259
+ if (board.serverDeletedItemIds.has(id)) continue;
2260
+ if (isLocalDeleteTombstoneActive(board, id, now)) {
2261
+ continue;
2262
+ }
2176
2263
  const existing = indexYItemsById(board.yItems).get(id);
2177
2264
  if (existing) {
2178
2265
  const currentSerialized = serializeItem(existing.yMap);
@@ -2203,7 +2290,7 @@ function applyServerSnapshotToYDoc(board, options) {
2203
2290
  }
2204
2291
  }
2205
2292
  function replaceYDocWithSnapshot(board, options) {
2206
- const { items: snapshotItems, origin } = options;
2293
+ const { items: snapshotItems, deletedItemIds, origin } = options;
2207
2294
  const now = Date.now();
2208
2295
  board.doc.transact(() => {
2209
2296
  if (board.yItems.length > 0) {
@@ -2216,6 +2303,12 @@ function replaceYDocWithSnapshot(board, options) {
2216
2303
  board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
2217
2304
  board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
2218
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
+ }
2219
2312
  for (const item of snapshotItems) {
2220
2313
  const id = getItemId(item);
2221
2314
  if (id) {
@@ -2578,7 +2671,7 @@ function useRealtimeSession(options) {
2578
2671
  const applyDocument = react.useCallback(
2579
2672
  (snapshot, options2) => {
2580
2673
  const currentSnapshot = latestDocumentRef.current;
2581
- 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)) {
2582
2675
  return;
2583
2676
  }
2584
2677
  const board = boardRef.current;
@@ -2597,11 +2690,13 @@ function useRealtimeSession(options) {
2597
2690
  if (options2?.replace) {
2598
2691
  replaceYDocWithSnapshot(board, {
2599
2692
  items: snapshot.items,
2693
+ deletedItemIds: snapshot.deletedItemIds,
2600
2694
  origin: ORIGIN_REMOTE
2601
2695
  });
2602
2696
  } else {
2603
2697
  applyServerSnapshotToYDoc(board, {
2604
2698
  items: snapshot.items,
2699
+ deletedItemIds: snapshot.deletedItemIds,
2605
2700
  origin: ORIGIN_REMOTE
2606
2701
  });
2607
2702
  }
@@ -2609,7 +2704,11 @@ function useRealtimeSession(options) {
2609
2704
  const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
2610
2705
  const mergedSnapshot = {
2611
2706
  ...snapshot,
2612
- 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)) } : {}
2613
2712
  };
2614
2713
  currentRevisionRef.current = snapshot.revision;
2615
2714
  latestDocumentRef.current = mergedSnapshot;
@@ -3734,6 +3833,7 @@ function useRealtimeCanvasDocument(options) {
3734
3833
  const realtimeEnabled = enabled && session != null;
3735
3834
  const documentRevision = session?.document?.revision ?? null;
3736
3835
  const documentItems = session?.document?.items;
3836
+ const documentDeletedItemIds = session?.document?.deletedItemIds;
3737
3837
  const documentUpdatedByClientId = session?.document?.updatedByClientId ?? null;
3738
3838
  const connectionClientId = session?.connection.clientId ?? null;
3739
3839
  const hasLocalOfflineDraft = session?.hasLocalOfflineDraft ?? false;
@@ -3786,15 +3886,22 @@ function useRealtimeCanvasDocument(options) {
3786
3886
  if (cancelled) return;
3787
3887
  if (inFlightRevisionRef.current !== documentRevision) return;
3788
3888
  lastAppliedRevisionRef.current = documentRevision;
3789
- 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;
3790
3895
  const hasLocalItems = localItems.length > 0;
3791
3896
  const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
3792
3897
  if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
3793
3898
  if (hasLocalItems) {
3794
3899
  const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
3795
3900
  session.remoteAdapter.send?.(normalizedLocalItems);
3901
+ return;
3796
3902
  }
3797
- return;
3903
+ const emptinessExplainedByDeletes = rawLocalItems.length > 0 && localItems.length === 0;
3904
+ if (!emptinessExplainedByDeletes) return;
3798
3905
  }
3799
3906
  if (shouldPreserveLocalRealtimeItems({
3800
3907
  localItems,
@@ -3822,6 +3929,7 @@ function useRealtimeCanvasDocument(options) {
3822
3929
  }, [
3823
3930
  applyIncomingItems,
3824
3931
  connectionClientId,
3932
+ documentDeletedItemIds,
3825
3933
  documentItems,
3826
3934
  documentRevision,
3827
3935
  documentUpdatedByClientId,