canvu-react 0.4.43 → 0.4.44
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 +82 -12
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.js +82 -12
- package/dist/realtime.js.map +1 -1
- package/dist/realtimeNative.cjs +85 -15
- package/dist/realtimeNative.cjs.map +1 -1
- package/dist/realtimeNative.js +85 -15
- package/dist/realtimeNative.js.map +1 -1
- package/package.json +1 -1
package/dist/realtimeNative.cjs
CHANGED
|
@@ -269,6 +269,12 @@ function parseRealtimeServerMessage(value) {
|
|
|
269
269
|
}
|
|
270
270
|
return void 0;
|
|
271
271
|
}
|
|
272
|
+
var CLIENT_ONLY_IMAGE_KEYS = /* @__PURE__ */ new Set([
|
|
273
|
+
"imageBlobId",
|
|
274
|
+
"imageThumbnailBlobId",
|
|
275
|
+
"imageRasterHref",
|
|
276
|
+
"imageThumbnailHref"
|
|
277
|
+
]);
|
|
272
278
|
function getSceneItemId(item) {
|
|
273
279
|
const id = item.id;
|
|
274
280
|
return typeof id === "string" ? id : null;
|
|
@@ -285,6 +291,43 @@ function hasMissingLocalItems(localItems, incomingItems) {
|
|
|
285
291
|
}
|
|
286
292
|
return false;
|
|
287
293
|
}
|
|
294
|
+
function getComparableRealtimeItem(item) {
|
|
295
|
+
const comparable = { ...item };
|
|
296
|
+
if (item.toolKind === "image") {
|
|
297
|
+
for (const key of CLIENT_ONLY_IMAGE_KEYS) {
|
|
298
|
+
delete comparable[key];
|
|
299
|
+
}
|
|
300
|
+
comparable.childrenSvg = "";
|
|
301
|
+
}
|
|
302
|
+
return comparable;
|
|
303
|
+
}
|
|
304
|
+
function serializeComparableRealtimeItem(item) {
|
|
305
|
+
return JSON.stringify(getComparableRealtimeItem(item));
|
|
306
|
+
}
|
|
307
|
+
function hasChangedLocalItems(localItems, incomingItems) {
|
|
308
|
+
const incomingItemsById = /* @__PURE__ */ new Map();
|
|
309
|
+
for (const incomingItem of incomingItems) {
|
|
310
|
+
const id = getSceneItemId(incomingItem);
|
|
311
|
+
if (id) incomingItemsById.set(id, incomingItem);
|
|
312
|
+
}
|
|
313
|
+
for (const localItem of localItems) {
|
|
314
|
+
const id = getSceneItemId(localItem);
|
|
315
|
+
const incomingItem = id ? incomingItemsById.get(id) : null;
|
|
316
|
+
if (!incomingItem) continue;
|
|
317
|
+
if (serializeComparableRealtimeItem(localItem) !== serializeComparableRealtimeItem(incomingItem)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
function shouldPreserveLocalRealtimeItems({
|
|
324
|
+
localItems,
|
|
325
|
+
incomingItems,
|
|
326
|
+
hasPendingLocalChanges
|
|
327
|
+
}) {
|
|
328
|
+
if (!hasPendingLocalChanges) return false;
|
|
329
|
+
return hasMissingLocalItems(localItems, incomingItems) || hasChangedLocalItems(localItems, incomingItems);
|
|
330
|
+
}
|
|
288
331
|
function useRealtimeCanvasDocument(options) {
|
|
289
332
|
const {
|
|
290
333
|
session,
|
|
@@ -297,6 +340,8 @@ function useRealtimeCanvasDocument(options) {
|
|
|
297
340
|
const [loading, setLoading] = react.useState(false);
|
|
298
341
|
const lastAppliedRevisionRef = react.useRef(null);
|
|
299
342
|
const inFlightRevisionRef = react.useRef(null);
|
|
343
|
+
const latestItemsRef = react.useRef(items);
|
|
344
|
+
const hasLocalChangeInFlightRef = react.useRef(false);
|
|
300
345
|
const hasEverPropagatedItemsRef = react.useRef(false);
|
|
301
346
|
const realtimeEnabled = enabled && session != null;
|
|
302
347
|
const documentRevision = session?.document?.revision ?? null;
|
|
@@ -319,6 +364,7 @@ function useRealtimeCanvasDocument(options) {
|
|
|
319
364
|
onItemsChange?.(normalizeItems ? normalizeItems(nextItems) : nextItems);
|
|
320
365
|
return;
|
|
321
366
|
}
|
|
367
|
+
hasLocalChangeInFlightRef.current = true;
|
|
322
368
|
const normalizedItems = normalizeItems ? normalizeItems(nextItems) : nextItems;
|
|
323
369
|
onItemsChange?.(normalizedItems);
|
|
324
370
|
session?.remoteAdapter.send?.(normalizedItems);
|
|
@@ -326,10 +372,16 @@ function useRealtimeCanvasDocument(options) {
|
|
|
326
372
|
[enabled, normalizeItems, onItemsChange, session]
|
|
327
373
|
);
|
|
328
374
|
react.useEffect(() => {
|
|
375
|
+
latestItemsRef.current = items;
|
|
329
376
|
if (items.length > 0) {
|
|
330
377
|
hasEverPropagatedItemsRef.current = true;
|
|
331
378
|
}
|
|
332
|
-
}, [items
|
|
379
|
+
}, [items]);
|
|
380
|
+
react.useEffect(() => {
|
|
381
|
+
if (!hasLocalOfflineDraft && !hasPendingDocumentSync) {
|
|
382
|
+
hasLocalChangeInFlightRef.current = false;
|
|
383
|
+
}
|
|
384
|
+
}, [hasLocalOfflineDraft, hasPendingDocumentSync]);
|
|
333
385
|
react.useEffect(() => {
|
|
334
386
|
if (!realtimeEnabled || !onItemsChange || !session?.document) return;
|
|
335
387
|
if (documentUpdatedByClientId === connectionClientId) return;
|
|
@@ -343,17 +395,22 @@ function useRealtimeCanvasDocument(options) {
|
|
|
343
395
|
if (cancelled) return;
|
|
344
396
|
if (inFlightRevisionRef.current !== documentRevision) return;
|
|
345
397
|
lastAppliedRevisionRef.current = documentRevision;
|
|
346
|
-
const
|
|
347
|
-
const
|
|
398
|
+
const localItems = latestItemsRef.current;
|
|
399
|
+
const hasLocalItems = localItems.length > 0;
|
|
400
|
+
const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync || hasLocalChangeInFlightRef.current;
|
|
348
401
|
if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
|
|
349
402
|
if (hasLocalItems) {
|
|
350
|
-
const normalizedLocalItems = normalizeItems ? normalizeItems(
|
|
403
|
+
const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
|
|
351
404
|
session.remoteAdapter.send?.(normalizedLocalItems);
|
|
352
405
|
}
|
|
353
406
|
return;
|
|
354
407
|
}
|
|
355
|
-
if (
|
|
356
|
-
|
|
408
|
+
if (shouldPreserveLocalRealtimeItems({
|
|
409
|
+
localItems,
|
|
410
|
+
incomingItems: resolvedItems,
|
|
411
|
+
hasPendingLocalChanges
|
|
412
|
+
})) {
|
|
413
|
+
const normalizedLocalItems = normalizeItems ? normalizeItems(localItems) : [...localItems];
|
|
357
414
|
session.remoteAdapter.send?.(normalizedLocalItems);
|
|
358
415
|
return;
|
|
359
416
|
}
|
|
@@ -379,7 +436,6 @@ function useRealtimeCanvasDocument(options) {
|
|
|
379
436
|
documentUpdatedByClientId,
|
|
380
437
|
hasLocalOfflineDraft,
|
|
381
438
|
hasPendingDocumentSync,
|
|
382
|
-
items,
|
|
383
439
|
normalizeItems,
|
|
384
440
|
onItemsChange,
|
|
385
441
|
realtimeEnabled,
|
|
@@ -542,7 +598,7 @@ function useRealtimePeerFollow(options) {
|
|
|
542
598
|
}, [followedPeerId, onFollowEnd, sessionPeers, viewportRef]);
|
|
543
599
|
}
|
|
544
600
|
var ITEMS_KEY = "items";
|
|
545
|
-
var
|
|
601
|
+
var CLIENT_ONLY_IMAGE_KEYS2 = /* @__PURE__ */ new Set([
|
|
546
602
|
"imageBlobId",
|
|
547
603
|
"imageThumbnailBlobId",
|
|
548
604
|
"imageRasterHref",
|
|
@@ -635,7 +691,7 @@ function updateYMapInPlace(yMap, next) {
|
|
|
635
691
|
function normalizeRealtimeItem(item) {
|
|
636
692
|
if (item.toolKind !== "image") return item;
|
|
637
693
|
const normalized = Object.fromEntries(
|
|
638
|
-
Object.entries(item).filter(([key]) => !
|
|
694
|
+
Object.entries(item).filter(([key]) => !CLIENT_ONLY_IMAGE_KEYS2.has(key))
|
|
639
695
|
);
|
|
640
696
|
normalized.childrenSvg = "";
|
|
641
697
|
return normalized;
|
|
@@ -788,6 +844,7 @@ var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
|
|
|
788
844
|
var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
|
|
789
845
|
var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
|
|
790
846
|
var DRAFT_PERSIST_DEBOUNCE_MS = 420;
|
|
847
|
+
var CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS = 1e3;
|
|
791
848
|
function requestRuntimeIdleCallback(callback, timeout) {
|
|
792
849
|
const runtime = globalThis;
|
|
793
850
|
if (typeof runtime.requestIdleCallback === "function") {
|
|
@@ -799,7 +856,7 @@ function cancelRuntimeIdleCallback(handle) {
|
|
|
799
856
|
if (handle == null) return;
|
|
800
857
|
const runtime = globalThis;
|
|
801
858
|
if (typeof runtime.cancelIdleCallback === "function") {
|
|
802
|
-
runtime.cancelIdleCallback(
|
|
859
|
+
runtime.cancelIdleCallback(handle);
|
|
803
860
|
return;
|
|
804
861
|
}
|
|
805
862
|
globalThis.clearTimeout(handle);
|
|
@@ -920,6 +977,9 @@ function sameSerializedItems(left, right) {
|
|
|
920
977
|
function nowMs() {
|
|
921
978
|
return Date.now();
|
|
922
979
|
}
|
|
980
|
+
function shouldUpdateRealtimeConnectionMessageState(input) {
|
|
981
|
+
return input.lastStateUpdateAt == null || input.receivedAt - input.lastStateUpdateAt >= CONNECTION_MESSAGE_STATE_UPDATE_INTERVAL_MS;
|
|
982
|
+
}
|
|
923
983
|
function hasDurableDocumentPersistence(snapshot) {
|
|
924
984
|
return snapshot.persistedRevision == null || snapshot.persistedRevision >= snapshot.revision;
|
|
925
985
|
}
|
|
@@ -1083,6 +1143,7 @@ function useRealtimeSession(options) {
|
|
|
1083
1143
|
const connectionStateRef = react.useRef(
|
|
1084
1144
|
enabled ? "connecting" : "offline"
|
|
1085
1145
|
);
|
|
1146
|
+
const lastConnectionMessageStateUpdateAtRef = react.useRef(null);
|
|
1086
1147
|
const localDraftRef = react.useRef(null);
|
|
1087
1148
|
const conflictRef = react.useRef(null);
|
|
1088
1149
|
const onErrorRef = react.useRef(onError);
|
|
@@ -1648,6 +1709,7 @@ function useRealtimeSession(options) {
|
|
|
1648
1709
|
react.useEffect(() => {
|
|
1649
1710
|
if (!enabled) {
|
|
1650
1711
|
manualDisconnectRef.current = true;
|
|
1712
|
+
lastConnectionMessageStateUpdateAtRef.current = null;
|
|
1651
1713
|
clearReconnectTimer();
|
|
1652
1714
|
clearHeartbeatTimer();
|
|
1653
1715
|
clearConnectTimeout();
|
|
@@ -1704,6 +1766,7 @@ function useRealtimeSession(options) {
|
|
|
1704
1766
|
return;
|
|
1705
1767
|
}
|
|
1706
1768
|
let disposed = false;
|
|
1769
|
+
lastConnectionMessageStateUpdateAtRef.current = null;
|
|
1707
1770
|
updateConnectionRef.current((prev) => ({
|
|
1708
1771
|
...prev,
|
|
1709
1772
|
state: retryCountRef.current > 0 ? "reconnecting" : "connecting",
|
|
@@ -1757,10 +1820,17 @@ function useRealtimeSession(options) {
|
|
|
1757
1820
|
}
|
|
1758
1821
|
const parsed = parseRealtimeServerMessage(payload);
|
|
1759
1822
|
if (!parsed) return;
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1823
|
+
const receivedAt = nowMs();
|
|
1824
|
+
if (shouldUpdateRealtimeConnectionMessageState({
|
|
1825
|
+
lastStateUpdateAt: lastConnectionMessageStateUpdateAtRef.current,
|
|
1826
|
+
receivedAt
|
|
1827
|
+
})) {
|
|
1828
|
+
lastConnectionMessageStateUpdateAtRef.current = receivedAt;
|
|
1829
|
+
updateConnectionRef.current((prev) => ({
|
|
1830
|
+
...prev,
|
|
1831
|
+
lastMessageAt: receivedAt
|
|
1832
|
+
}));
|
|
1833
|
+
}
|
|
1764
1834
|
if (parsed.type === "session:welcome") {
|
|
1765
1835
|
retryCountRef.current = 0;
|
|
1766
1836
|
updateConnectionRef.current((prev) => ({
|
|
@@ -1769,7 +1839,7 @@ function useRealtimeSession(options) {
|
|
|
1769
1839
|
connected: true,
|
|
1770
1840
|
clientId: parsed.clientId,
|
|
1771
1841
|
retryCount: 0,
|
|
1772
|
-
lastConnectedAt:
|
|
1842
|
+
lastConnectedAt: receivedAt,
|
|
1773
1843
|
lastError: null
|
|
1774
1844
|
}));
|
|
1775
1845
|
applyPeersRef.current(parsed.peers);
|