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 +119 -11
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.d.cts +5 -0
- package/dist/realtime.d.ts +5 -0
- package/dist/realtime.js +119 -11
- package/dist/realtime.js.map +1 -1
- package/dist/realtimeNative.cjs +119 -11
- package/dist/realtimeNative.cjs.map +1 -1
- package/dist/realtimeNative.d.cts +5 -0
- package/dist/realtimeNative.d.ts +5 -0
- package/dist/realtimeNative.js +119 -11
- package/dist/realtimeNative.js.map +1 -1
- package/package.json +1 -1
|
@@ -19,6 +19,11 @@ type RealtimeDocumentSnapshot = {
|
|
|
19
19
|
readonly updatedAt: number;
|
|
20
20
|
readonly updatedByClientId?: string;
|
|
21
21
|
readonly persistedRevision?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Tombstones for items deleted in the room. Sent by servers that support
|
|
24
|
+
* explicit delete propagation; absent on older servers.
|
|
25
|
+
*/
|
|
26
|
+
readonly deletedItemIds?: readonly string[];
|
|
22
27
|
};
|
|
23
28
|
type RealtimeConnectionInfo = {
|
|
24
29
|
readonly state: RealtimeConnectionState;
|
package/dist/realtimeNative.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ type RealtimeDocumentSnapshot = {
|
|
|
19
19
|
readonly updatedAt: number;
|
|
20
20
|
readonly updatedByClientId?: string;
|
|
21
21
|
readonly persistedRevision?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Tombstones for items deleted in the room. Sent by servers that support
|
|
24
|
+
* explicit delete propagation; absent on older servers.
|
|
25
|
+
*/
|
|
26
|
+
readonly deletedItemIds?: readonly string[];
|
|
22
27
|
};
|
|
23
28
|
type RealtimeConnectionInfo = {
|
|
24
29
|
readonly state: RealtimeConnectionState;
|
package/dist/realtimeNative.js
CHANGED
|
@@ -82,12 +82,16 @@ function parseDocumentSnapshot(value) {
|
|
|
82
82
|
const updatedAt = getNumber(value.updatedAt);
|
|
83
83
|
const items = parseItems(value.items);
|
|
84
84
|
if (revision == null || updatedAt == null || items == null) return void 0;
|
|
85
|
+
const deletedItemIds = Array.isArray(value.deletedItemIds) ? value.deletedItemIds.filter(
|
|
86
|
+
(entry) => typeof entry === "string"
|
|
87
|
+
) : void 0;
|
|
85
88
|
return {
|
|
86
89
|
revision,
|
|
87
90
|
updatedAt,
|
|
88
91
|
items,
|
|
89
92
|
...getString(value.updatedByClientId) ? { updatedByClientId: getString(value.updatedByClientId) } : {},
|
|
90
|
-
...getNumber(value.persistedRevision) != null ? { persistedRevision: getNumber(value.persistedRevision) } : {}
|
|
93
|
+
...getNumber(value.persistedRevision) != null ? { persistedRevision: getNumber(value.persistedRevision) } : {},
|
|
94
|
+
...deletedItemIds ? { deletedItemIds } : {}
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
97
|
function parseRealtimeSessionPeer(value) {
|
|
@@ -324,6 +328,7 @@ function useRealtimeCanvasDocument(options) {
|
|
|
324
328
|
const realtimeEnabled = enabled && session != null;
|
|
325
329
|
const documentRevision = session?.document?.revision ?? null;
|
|
326
330
|
const documentItems = session?.document?.items;
|
|
331
|
+
const documentDeletedItemIds = session?.document?.deletedItemIds;
|
|
327
332
|
const documentUpdatedByClientId = session?.document?.updatedByClientId ?? null;
|
|
328
333
|
const connectionClientId = session?.connection.clientId ?? null;
|
|
329
334
|
const hasLocalOfflineDraft = session?.hasLocalOfflineDraft ?? false;
|
|
@@ -373,15 +378,22 @@ function useRealtimeCanvasDocument(options) {
|
|
|
373
378
|
if (cancelled) return;
|
|
374
379
|
if (inFlightRevisionRef.current !== documentRevision) return;
|
|
375
380
|
lastAppliedRevisionRef.current = documentRevision;
|
|
376
|
-
const
|
|
381
|
+
const rawLocalItems = latestItemsRef.current;
|
|
382
|
+
const deletedIds = new Set(documentDeletedItemIds ?? []);
|
|
383
|
+
const localItems = deletedIds.size > 0 ? rawLocalItems.filter((item) => {
|
|
384
|
+
const id = getSceneItemId(item);
|
|
385
|
+
return !id || !deletedIds.has(id);
|
|
386
|
+
}) : rawLocalItems;
|
|
377
387
|
const hasLocalItems = localItems.length > 0;
|
|
378
388
|
const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
|
|
379
389
|
if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
|
|
380
390
|
if (hasLocalItems) {
|
|
381
391
|
const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
|
|
382
392
|
session.remoteAdapter.send?.(normalizedLocalItems);
|
|
393
|
+
return;
|
|
383
394
|
}
|
|
384
|
-
|
|
395
|
+
const emptinessExplainedByDeletes = rawLocalItems.length > 0 && localItems.length === 0;
|
|
396
|
+
if (!emptinessExplainedByDeletes) return;
|
|
385
397
|
}
|
|
386
398
|
if (shouldPreserveLocalRealtimeItems({
|
|
387
399
|
localItems,
|
|
@@ -409,6 +421,7 @@ function useRealtimeCanvasDocument(options) {
|
|
|
409
421
|
}, [
|
|
410
422
|
applyIncomingItems,
|
|
411
423
|
connectionClientId,
|
|
424
|
+
documentDeletedItemIds,
|
|
412
425
|
documentItems,
|
|
413
426
|
documentRevision,
|
|
414
427
|
documentUpdatedByClientId,
|
|
@@ -585,6 +598,16 @@ var CLIENT_ONLY_IMAGE_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
585
598
|
"imageThumbnailHref"
|
|
586
599
|
]);
|
|
587
600
|
var SERVER_ADD_RACE_WINDOW_MS = 2e3;
|
|
601
|
+
var LOCAL_DELETE_TOMBSTONE_TTL_MS = 1e4;
|
|
602
|
+
function isLocalDeleteTombstoneActive(board, id, now) {
|
|
603
|
+
const deletedAt = board.locallyDeletedItemIds.get(id);
|
|
604
|
+
if (deletedAt == null) return false;
|
|
605
|
+
if (now - deletedAt >= LOCAL_DELETE_TOMBSTONE_TTL_MS) {
|
|
606
|
+
board.locallyDeletedItemIds.delete(id);
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
588
611
|
function createYjsBoardDoc() {
|
|
589
612
|
const doc = new Y.Doc();
|
|
590
613
|
const yItems = doc.getArray(ITEMS_KEY);
|
|
@@ -593,9 +616,22 @@ function createYjsBoardDoc() {
|
|
|
593
616
|
yItems,
|
|
594
617
|
lastServerConfirmedIds: /* @__PURE__ */ new Set(),
|
|
595
618
|
lastServerConfirmedItemSerializations: /* @__PURE__ */ new Map(),
|
|
596
|
-
serverItemSeenAt: /* @__PURE__ */ new Map()
|
|
619
|
+
serverItemSeenAt: /* @__PURE__ */ new Map(),
|
|
620
|
+
serverDeletedItemIds: /* @__PURE__ */ new Set(),
|
|
621
|
+
locallyDeletedItemIds: /* @__PURE__ */ new Map(),
|
|
622
|
+
consumerSeenItemIds: /* @__PURE__ */ new Set()
|
|
597
623
|
};
|
|
598
624
|
}
|
|
625
|
+
function getRealtimeDeletedItemIds(board) {
|
|
626
|
+
const now = Date.now();
|
|
627
|
+
const result = new Set(board.serverDeletedItemIds);
|
|
628
|
+
for (const id of Array.from(board.locallyDeletedItemIds.keys())) {
|
|
629
|
+
if (isLocalDeleteTombstoneActive(board, id, now)) {
|
|
630
|
+
result.add(id);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
599
635
|
function getItemId(item) {
|
|
600
636
|
if (item instanceof Y.Map) {
|
|
601
637
|
const id2 = item.get("id");
|
|
@@ -676,6 +712,38 @@ function normalizeRealtimeItem(item) {
|
|
|
676
712
|
normalized.childrenSvg = "";
|
|
677
713
|
return normalized;
|
|
678
714
|
}
|
|
715
|
+
function reorderYItemsToMatchLocalItems(yItems, items) {
|
|
716
|
+
const currentItems = readVectorItems(yItems);
|
|
717
|
+
const currentItemsById = /* @__PURE__ */ new Map();
|
|
718
|
+
for (const currentItem of currentItems) {
|
|
719
|
+
const id = getItemId(currentItem);
|
|
720
|
+
if (id) currentItemsById.set(id, currentItem);
|
|
721
|
+
}
|
|
722
|
+
const orderedItems = [];
|
|
723
|
+
const orderedIds = /* @__PURE__ */ new Set();
|
|
724
|
+
for (const item of items) {
|
|
725
|
+
const id = getItemId(item);
|
|
726
|
+
if (!id) continue;
|
|
727
|
+
const currentItem = currentItemsById.get(id);
|
|
728
|
+
if (!currentItem) continue;
|
|
729
|
+
orderedItems.push(currentItem);
|
|
730
|
+
orderedIds.add(id);
|
|
731
|
+
}
|
|
732
|
+
for (const currentItem of currentItems) {
|
|
733
|
+
const id = getItemId(currentItem);
|
|
734
|
+
if (!id || !orderedIds.has(id)) orderedItems.push(currentItem);
|
|
735
|
+
}
|
|
736
|
+
const alreadyOrdered = currentItems.length === orderedItems.length && currentItems.every((currentItem, index) => {
|
|
737
|
+
const nextItem = orderedItems[index];
|
|
738
|
+
return nextItem ? getItemId(currentItem) === getItemId(nextItem) : false;
|
|
739
|
+
});
|
|
740
|
+
if (alreadyOrdered) return;
|
|
741
|
+
if (yItems.length > 0) yItems.delete(0, yItems.length);
|
|
742
|
+
yItems.insert(
|
|
743
|
+
0,
|
|
744
|
+
orderedItems.map((item) => vectorItemToYMap(item))
|
|
745
|
+
);
|
|
746
|
+
}
|
|
679
747
|
function applyLocalItemsToYDoc(board, options) {
|
|
680
748
|
const { items, origin } = options;
|
|
681
749
|
const addedIds = [];
|
|
@@ -692,9 +760,11 @@ function applyLocalItemsToYDoc(board, options) {
|
|
|
692
760
|
const toDelete = [];
|
|
693
761
|
for (const [id, entry] of currentIndex) {
|
|
694
762
|
if (nextIds.has(id)) continue;
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
763
|
+
if (!board.consumerSeenItemIds.has(id)) {
|
|
764
|
+
const serverSeenAt = board.serverItemSeenAt.get(id);
|
|
765
|
+
if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
698
768
|
}
|
|
699
769
|
toDelete.push({ id, index: entry.index });
|
|
700
770
|
}
|
|
@@ -702,6 +772,7 @@ function applyLocalItemsToYDoc(board, options) {
|
|
|
702
772
|
for (const { id, index } of toDelete) {
|
|
703
773
|
board.yItems.delete(index, 1);
|
|
704
774
|
board.serverItemSeenAt.delete(id);
|
|
775
|
+
board.locallyDeletedItemIds.set(id, now);
|
|
705
776
|
removedIds.push(id);
|
|
706
777
|
}
|
|
707
778
|
const refreshedIndex = indexYItemsById(board.yItems);
|
|
@@ -710,6 +781,9 @@ function applyLocalItemsToYDoc(board, options) {
|
|
|
710
781
|
if (!item) continue;
|
|
711
782
|
const id = getItemId(item);
|
|
712
783
|
if (!id) continue;
|
|
784
|
+
board.consumerSeenItemIds.add(id);
|
|
785
|
+
if (board.serverDeletedItemIds.has(id)) continue;
|
|
786
|
+
board.locallyDeletedItemIds.delete(id);
|
|
713
787
|
const existing = refreshedIndex.get(id);
|
|
714
788
|
if (existing) {
|
|
715
789
|
updateYMapInPlace(existing.yMap, item);
|
|
@@ -719,16 +793,38 @@ function applyLocalItemsToYDoc(board, options) {
|
|
|
719
793
|
board.yItems.push([yMap]);
|
|
720
794
|
addedIds.push(id);
|
|
721
795
|
}
|
|
796
|
+
reorderYItemsToMatchLocalItems(board.yItems, items);
|
|
722
797
|
}, origin);
|
|
723
798
|
return { addedIds, removedIds };
|
|
724
799
|
}
|
|
725
800
|
function applyServerSnapshotToYDoc(board, options) {
|
|
726
|
-
const { items: snapshotItems, origin } = options;
|
|
801
|
+
const { items: snapshotItems, deletedItemIds, origin } = options;
|
|
727
802
|
const now = Date.now();
|
|
803
|
+
const snapshotItemIds = /* @__PURE__ */ new Set();
|
|
804
|
+
for (const item of snapshotItems) {
|
|
805
|
+
const id = getItemId(item);
|
|
806
|
+
if (id) snapshotItemIds.add(id);
|
|
807
|
+
}
|
|
728
808
|
board.doc.transact(() => {
|
|
809
|
+
if (deletedItemIds) {
|
|
810
|
+
for (const id of deletedItemIds) {
|
|
811
|
+
if (snapshotItemIds.has(id)) continue;
|
|
812
|
+
board.serverDeletedItemIds.add(id);
|
|
813
|
+
board.locallyDeletedItemIds.delete(id);
|
|
814
|
+
board.serverItemSeenAt.delete(id);
|
|
815
|
+
const existing = indexYItemsById(board.yItems).get(id);
|
|
816
|
+
if (existing) {
|
|
817
|
+
board.yItems.delete(existing.index, 1);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
729
821
|
for (const item of snapshotItems) {
|
|
730
822
|
const id = getItemId(item);
|
|
731
823
|
if (!id) continue;
|
|
824
|
+
if (board.serverDeletedItemIds.has(id)) continue;
|
|
825
|
+
if (isLocalDeleteTombstoneActive(board, id, now)) {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
732
828
|
const existing = indexYItemsById(board.yItems).get(id);
|
|
733
829
|
if (existing) {
|
|
734
830
|
const currentSerialized = serializeItem(existing.yMap);
|
|
@@ -759,7 +855,7 @@ function applyServerSnapshotToYDoc(board, options) {
|
|
|
759
855
|
}
|
|
760
856
|
}
|
|
761
857
|
function replaceYDocWithSnapshot(board, options) {
|
|
762
|
-
const { items: snapshotItems, origin } = options;
|
|
858
|
+
const { items: snapshotItems, deletedItemIds, origin } = options;
|
|
763
859
|
const now = Date.now();
|
|
764
860
|
board.doc.transact(() => {
|
|
765
861
|
if (board.yItems.length > 0) {
|
|
@@ -772,6 +868,12 @@ function replaceYDocWithSnapshot(board, options) {
|
|
|
772
868
|
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
773
869
|
board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
|
|
774
870
|
board.serverItemSeenAt = /* @__PURE__ */ new Map();
|
|
871
|
+
board.locallyDeletedItemIds = /* @__PURE__ */ new Map();
|
|
872
|
+
if (deletedItemIds) {
|
|
873
|
+
for (const id of deletedItemIds) {
|
|
874
|
+
board.serverDeletedItemIds.add(id);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
775
877
|
for (const item of snapshotItems) {
|
|
776
878
|
const id = getItemId(item);
|
|
777
879
|
if (id) {
|
|
@@ -1218,7 +1320,7 @@ function useRealtimeSession(options) {
|
|
|
1218
1320
|
const applyDocument = useCallback(
|
|
1219
1321
|
(snapshot, options2) => {
|
|
1220
1322
|
const currentSnapshot = latestDocumentRef.current;
|
|
1221
|
-
if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && currentSnapshot.persistedRevision === snapshot.persistedRevision && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
|
|
1323
|
+
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)) {
|
|
1222
1324
|
return;
|
|
1223
1325
|
}
|
|
1224
1326
|
const board = boardRef.current;
|
|
@@ -1237,11 +1339,13 @@ function useRealtimeSession(options) {
|
|
|
1237
1339
|
if (options2?.replace) {
|
|
1238
1340
|
replaceYDocWithSnapshot(board, {
|
|
1239
1341
|
items: snapshot.items,
|
|
1342
|
+
deletedItemIds: snapshot.deletedItemIds,
|
|
1240
1343
|
origin: ORIGIN_REMOTE
|
|
1241
1344
|
});
|
|
1242
1345
|
} else {
|
|
1243
1346
|
applyServerSnapshotToYDoc(board, {
|
|
1244
1347
|
items: snapshot.items,
|
|
1348
|
+
deletedItemIds: snapshot.deletedItemIds,
|
|
1245
1349
|
origin: ORIGIN_REMOTE
|
|
1246
1350
|
});
|
|
1247
1351
|
}
|
|
@@ -1249,7 +1353,11 @@ function useRealtimeSession(options) {
|
|
|
1249
1353
|
const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
|
|
1250
1354
|
const mergedSnapshot = {
|
|
1251
1355
|
...snapshot,
|
|
1252
|
-
items: mergedItems
|
|
1356
|
+
items: mergedItems,
|
|
1357
|
+
// Expose the accumulated tombstones (server-confirmed + pending
|
|
1358
|
+
// local deletes) so document consumers can prune stale local
|
|
1359
|
+
// views instead of re-sending deleted items.
|
|
1360
|
+
...board ? { deletedItemIds: Array.from(getRealtimeDeletedItemIds(board)) } : {}
|
|
1253
1361
|
};
|
|
1254
1362
|
currentRevisionRef.current = snapshot.revision;
|
|
1255
1363
|
latestDocumentRef.current = mergedSnapshot;
|