canvu-react 0.3.40 → 0.4.1
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 +436 -129
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.js +417 -129
- package/dist/realtime.js.map +1 -1
- package/package.json +3 -2
package/dist/realtime.js
CHANGED
|
@@ -2,6 +2,7 @@ import { MousePointer2, MessageSquare, Sparkles, Hand, Square, Circle, Minus, Ar
|
|
|
2
2
|
import getStroke from 'perfect-freehand';
|
|
3
3
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
import { createContext, useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect, useContext } from 'react';
|
|
5
|
+
import * as Y from 'yjs';
|
|
5
6
|
|
|
6
7
|
// src/react/presence/map-placement-preview.ts
|
|
7
8
|
function remoteMarkupStrokeFromPlacementPreview(preview) {
|
|
@@ -1986,8 +1987,215 @@ function useRealtimePeerFollow(options) {
|
|
|
1986
1987
|
lastAppliedCameraKeyRef.current = nextCameraKey;
|
|
1987
1988
|
}, [followedPeerId, onFollowEnd, sessionPeers, viewportRef]);
|
|
1988
1989
|
}
|
|
1990
|
+
var ITEMS_KEY = "items";
|
|
1991
|
+
var SERVER_ADD_RACE_WINDOW_MS = 2e3;
|
|
1992
|
+
function createYjsBoardDoc() {
|
|
1993
|
+
const doc = new Y.Doc();
|
|
1994
|
+
const yItems = doc.getArray(ITEMS_KEY);
|
|
1995
|
+
return {
|
|
1996
|
+
doc,
|
|
1997
|
+
yItems,
|
|
1998
|
+
lastServerConfirmedIds: /* @__PURE__ */ new Set(),
|
|
1999
|
+
serverItemSeenAt: /* @__PURE__ */ new Map()
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
function getItemId(item) {
|
|
2003
|
+
if (item instanceof Y.Map) {
|
|
2004
|
+
const id2 = item.get("id");
|
|
2005
|
+
return typeof id2 === "string" ? id2 : null;
|
|
2006
|
+
}
|
|
2007
|
+
const id = item.id;
|
|
2008
|
+
return typeof id === "string" ? id : null;
|
|
2009
|
+
}
|
|
2010
|
+
function vectorItemToYMap(item) {
|
|
2011
|
+
const yMap = new Y.Map();
|
|
2012
|
+
for (const [key, value] of Object.entries(item)) {
|
|
2013
|
+
if (value === void 0) continue;
|
|
2014
|
+
yMap.set(key, value);
|
|
2015
|
+
}
|
|
2016
|
+
return yMap;
|
|
2017
|
+
}
|
|
2018
|
+
function yMapToVectorItem(yMap) {
|
|
2019
|
+
const obj = {};
|
|
2020
|
+
for (const [key, value] of yMap.entries()) {
|
|
2021
|
+
obj[key] = value;
|
|
2022
|
+
}
|
|
2023
|
+
return obj;
|
|
2024
|
+
}
|
|
2025
|
+
function readVectorItems(yItems) {
|
|
2026
|
+
const result = [];
|
|
2027
|
+
for (let i = 0; i < yItems.length; i++) {
|
|
2028
|
+
const yMap = yItems.get(i);
|
|
2029
|
+
if (yMap) result.push(yMapToVectorItem(yMap));
|
|
2030
|
+
}
|
|
2031
|
+
return result;
|
|
2032
|
+
}
|
|
2033
|
+
function indexYItemsById(yItems) {
|
|
2034
|
+
const result = /* @__PURE__ */ new Map();
|
|
2035
|
+
for (let i = 0; i < yItems.length; i++) {
|
|
2036
|
+
const yMap = yItems.get(i);
|
|
2037
|
+
if (!yMap) continue;
|
|
2038
|
+
const id = getItemId(yMap);
|
|
2039
|
+
if (!id) continue;
|
|
2040
|
+
result.set(id, { yMap, index: i });
|
|
2041
|
+
}
|
|
2042
|
+
return result;
|
|
2043
|
+
}
|
|
2044
|
+
function valuesEqual(left, right) {
|
|
2045
|
+
if (left === right) return true;
|
|
2046
|
+
if (typeof left !== typeof right) return false;
|
|
2047
|
+
if (left === null || right === null) return false;
|
|
2048
|
+
if (typeof left !== "object") return false;
|
|
2049
|
+
try {
|
|
2050
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
2051
|
+
} catch {
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function updateYMapInPlace(yMap, next) {
|
|
2056
|
+
const nextKeys = /* @__PURE__ */ new Set();
|
|
2057
|
+
for (const [key, value] of Object.entries(next)) {
|
|
2058
|
+
if (value === void 0) continue;
|
|
2059
|
+
nextKeys.add(key);
|
|
2060
|
+
const current = yMap.get(key);
|
|
2061
|
+
if (!valuesEqual(current, value)) {
|
|
2062
|
+
yMap.set(key, value);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
for (const key of Array.from(yMap.keys())) {
|
|
2066
|
+
if (!nextKeys.has(key)) yMap.delete(key);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
function applyLocalItemsToYDoc(board, options) {
|
|
2070
|
+
const { items, origin } = options;
|
|
2071
|
+
const addedIds = [];
|
|
2072
|
+
const removedIds = [];
|
|
2073
|
+
const now = Date.now();
|
|
2074
|
+
board.doc.transact(() => {
|
|
2075
|
+
const currentIndex = indexYItemsById(board.yItems);
|
|
2076
|
+
const nextIds = /* @__PURE__ */ new Set();
|
|
2077
|
+
for (const item of items) {
|
|
2078
|
+
const id = getItemId(item);
|
|
2079
|
+
if (!id) continue;
|
|
2080
|
+
nextIds.add(id);
|
|
2081
|
+
}
|
|
2082
|
+
const toDelete = [];
|
|
2083
|
+
for (const [id, entry] of currentIndex) {
|
|
2084
|
+
if (nextIds.has(id)) continue;
|
|
2085
|
+
const serverSeenAt = board.serverItemSeenAt.get(id);
|
|
2086
|
+
if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
toDelete.push({ id, index: entry.index });
|
|
2090
|
+
}
|
|
2091
|
+
toDelete.sort((a, b) => b.index - a.index);
|
|
2092
|
+
for (const { id, index } of toDelete) {
|
|
2093
|
+
board.yItems.delete(index, 1);
|
|
2094
|
+
board.serverItemSeenAt.delete(id);
|
|
2095
|
+
removedIds.push(id);
|
|
2096
|
+
}
|
|
2097
|
+
const refreshedIndex = indexYItemsById(board.yItems);
|
|
2098
|
+
for (let nextOrder = 0; nextOrder < items.length; nextOrder++) {
|
|
2099
|
+
const item = items[nextOrder];
|
|
2100
|
+
if (!item) continue;
|
|
2101
|
+
const id = getItemId(item);
|
|
2102
|
+
if (!id) continue;
|
|
2103
|
+
const existing = refreshedIndex.get(id);
|
|
2104
|
+
if (existing) {
|
|
2105
|
+
updateYMapInPlace(existing.yMap, item);
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
const yMap = vectorItemToYMap(item);
|
|
2109
|
+
board.yItems.push([yMap]);
|
|
2110
|
+
addedIds.push(id);
|
|
2111
|
+
}
|
|
2112
|
+
}, origin);
|
|
2113
|
+
return { addedIds, removedIds };
|
|
2114
|
+
}
|
|
2115
|
+
function applyServerSnapshotToYDoc(board, options) {
|
|
2116
|
+
const { items: snapshotItems, origin } = options;
|
|
2117
|
+
const now = Date.now();
|
|
2118
|
+
board.doc.transact(() => {
|
|
2119
|
+
for (const item of snapshotItems) {
|
|
2120
|
+
const id = getItemId(item);
|
|
2121
|
+
if (!id) continue;
|
|
2122
|
+
const existing = indexYItemsById(board.yItems).get(id);
|
|
2123
|
+
if (existing) {
|
|
2124
|
+
updateYMapInPlace(existing.yMap, item);
|
|
2125
|
+
board.serverItemSeenAt.set(id, now);
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
const yMap = vectorItemToYMap(item);
|
|
2129
|
+
board.yItems.push([yMap]);
|
|
2130
|
+
board.serverItemSeenAt.set(id, now);
|
|
2131
|
+
}
|
|
2132
|
+
}, origin);
|
|
2133
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2134
|
+
for (const item of snapshotItems) {
|
|
2135
|
+
const id = getItemId(item);
|
|
2136
|
+
if (id) board.lastServerConfirmedIds.add(id);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
function replaceYDocWithSnapshot(board, options) {
|
|
2140
|
+
const { items: snapshotItems, origin } = options;
|
|
2141
|
+
const now = Date.now();
|
|
2142
|
+
board.doc.transact(() => {
|
|
2143
|
+
if (board.yItems.length > 0) {
|
|
2144
|
+
board.yItems.delete(0, board.yItems.length);
|
|
2145
|
+
}
|
|
2146
|
+
for (const item of snapshotItems) {
|
|
2147
|
+
board.yItems.push([vectorItemToYMap(item)]);
|
|
2148
|
+
}
|
|
2149
|
+
}, origin);
|
|
2150
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2151
|
+
board.serverItemSeenAt = /* @__PURE__ */ new Map();
|
|
2152
|
+
for (const item of snapshotItems) {
|
|
2153
|
+
const id = getItemId(item);
|
|
2154
|
+
if (id) {
|
|
2155
|
+
board.lastServerConfirmedIds.add(id);
|
|
2156
|
+
board.serverItemSeenAt.set(id, now);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
function getLocallyPendingItemIds(board) {
|
|
2161
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2162
|
+
for (let i = 0; i < board.yItems.length; i++) {
|
|
2163
|
+
const yMap = board.yItems.get(i);
|
|
2164
|
+
if (!yMap) continue;
|
|
2165
|
+
const id = getItemId(yMap);
|
|
2166
|
+
if (!id) continue;
|
|
2167
|
+
if (!board.lastServerConfirmedIds.has(id)) {
|
|
2168
|
+
pending.add(id);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return pending;
|
|
2172
|
+
}
|
|
2173
|
+
function encodeYDocState(board) {
|
|
2174
|
+
const update = Y.encodeStateAsUpdate(board.doc);
|
|
2175
|
+
let binary = "";
|
|
2176
|
+
for (let i = 0; i < update.length; i++) {
|
|
2177
|
+
binary += String.fromCharCode(update[i]);
|
|
2178
|
+
}
|
|
2179
|
+
return btoa(binary);
|
|
2180
|
+
}
|
|
2181
|
+
function decodeYDocState(board, encoded) {
|
|
2182
|
+
try {
|
|
2183
|
+
const binary = atob(encoded);
|
|
2184
|
+
const update = new Uint8Array(binary.length);
|
|
2185
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2186
|
+
update[i] = binary.charCodeAt(i);
|
|
2187
|
+
}
|
|
2188
|
+
Y.applyUpdate(board.doc, update);
|
|
2189
|
+
return true;
|
|
2190
|
+
} catch {
|
|
2191
|
+
return false;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
1989
2194
|
|
|
1990
2195
|
// src/react/plugins/realtime/use-realtime-session.ts
|
|
2196
|
+
var ORIGIN_LOCAL = /* @__PURE__ */ Symbol("canvu/realtime/local");
|
|
2197
|
+
var ORIGIN_REMOTE = /* @__PURE__ */ Symbol("canvu/realtime/remote");
|
|
2198
|
+
var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
|
|
1991
2199
|
var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
|
|
1992
2200
|
var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
|
|
1993
2201
|
var DRAFT_PERSIST_DEBOUNCE_MS = 420;
|
|
@@ -2074,7 +2282,16 @@ function draftStorageKey(roomId) {
|
|
|
2074
2282
|
function isRealtimeOfflineDraft(value) {
|
|
2075
2283
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2076
2284
|
const record = value;
|
|
2077
|
-
|
|
2285
|
+
if (typeof record.roomId !== "string" || typeof record.baseRevision !== "number" || typeof record.updatedAt !== "number" || !Array.isArray(record.items)) {
|
|
2286
|
+
return false;
|
|
2287
|
+
}
|
|
2288
|
+
if (record.yDocState != null && typeof record.yDocState !== "string") {
|
|
2289
|
+
return false;
|
|
2290
|
+
}
|
|
2291
|
+
if (record.pendingIds != null && !Array.isArray(record.pendingIds)) {
|
|
2292
|
+
return false;
|
|
2293
|
+
}
|
|
2294
|
+
return true;
|
|
2078
2295
|
}
|
|
2079
2296
|
function readRealtimeOfflineDraft(roomId) {
|
|
2080
2297
|
if (typeof window === "undefined" || !roomId) return null;
|
|
@@ -2112,14 +2329,6 @@ function removeRealtimeOfflineDraft(roomId) {
|
|
|
2112
2329
|
} catch {
|
|
2113
2330
|
}
|
|
2114
2331
|
}
|
|
2115
|
-
function buildDraftSnapshot(draft, clientId) {
|
|
2116
|
-
return {
|
|
2117
|
-
revision: draft.baseRevision,
|
|
2118
|
-
items: draft.items,
|
|
2119
|
-
updatedAt: draft.updatedAt,
|
|
2120
|
-
updatedByClientId: clientId
|
|
2121
|
-
};
|
|
2122
|
-
}
|
|
2123
2332
|
function getViewportCameraSnapshot(viewport) {
|
|
2124
2333
|
if (!viewport) return null;
|
|
2125
2334
|
const camera = viewport.getCamera();
|
|
@@ -2170,8 +2379,13 @@ function useRealtimeSession(options) {
|
|
|
2170
2379
|
const retryCountRef = useRef(0);
|
|
2171
2380
|
const currentRevisionRef = useRef(0);
|
|
2172
2381
|
const outboundInFlightRef = useRef(null);
|
|
2173
|
-
const
|
|
2382
|
+
const queuedDirtyRef = useRef(false);
|
|
2383
|
+
const pendingLocalItemsRef = useRef(null);
|
|
2174
2384
|
const subscriberRefs = useRef(/* @__PURE__ */ new Set());
|
|
2385
|
+
const boardRef = useRef(null);
|
|
2386
|
+
if (boardRef.current == null) {
|
|
2387
|
+
boardRef.current = createYjsBoardDoc();
|
|
2388
|
+
}
|
|
2175
2389
|
const lastCursorRef = useRef(null);
|
|
2176
2390
|
const lastMarkupStrokeRef = useRef(null);
|
|
2177
2391
|
const lastCameraRef = useRef(
|
|
@@ -2266,11 +2480,30 @@ function useRealtimeSession(options) {
|
|
|
2266
2480
|
if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
|
|
2267
2481
|
return;
|
|
2268
2482
|
}
|
|
2483
|
+
const board = boardRef.current;
|
|
2484
|
+
if (board) {
|
|
2485
|
+
if (options2?.replace) {
|
|
2486
|
+
replaceYDocWithSnapshot(board, {
|
|
2487
|
+
items: snapshot.items,
|
|
2488
|
+
origin: ORIGIN_REMOTE
|
|
2489
|
+
});
|
|
2490
|
+
} else {
|
|
2491
|
+
applyServerSnapshotToYDoc(board, {
|
|
2492
|
+
items: snapshot.items,
|
|
2493
|
+
origin: ORIGIN_REMOTE
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
|
|
2498
|
+
const mergedSnapshot = {
|
|
2499
|
+
...snapshot,
|
|
2500
|
+
items: mergedItems
|
|
2501
|
+
};
|
|
2269
2502
|
currentRevisionRef.current = snapshot.revision;
|
|
2270
|
-
latestDocumentRef.current =
|
|
2271
|
-
setDocument(
|
|
2503
|
+
latestDocumentRef.current = mergedSnapshot;
|
|
2504
|
+
setDocument(mergedSnapshot);
|
|
2272
2505
|
if (!options2?.suppressSubscriberNotify) {
|
|
2273
|
-
notifySubscribers(
|
|
2506
|
+
notifySubscribers(mergedItems);
|
|
2274
2507
|
}
|
|
2275
2508
|
},
|
|
2276
2509
|
[notifySubscribers]
|
|
@@ -2380,13 +2613,23 @@ function useRealtimeSession(options) {
|
|
|
2380
2613
|
);
|
|
2381
2614
|
const flushQueuedDocument = useCallback(() => {
|
|
2382
2615
|
clearDocumentFlushSchedule();
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
if (!
|
|
2616
|
+
const board = boardRef.current;
|
|
2617
|
+
if (!board) return;
|
|
2618
|
+
if (!queuedDirtyRef.current && pendingLocalItemsRef.current == null) return;
|
|
2619
|
+
if (outboundInFlightRef.current) return;
|
|
2620
|
+
const pendingLocal = pendingLocalItemsRef.current;
|
|
2621
|
+
if (pendingLocal) {
|
|
2622
|
+
pendingLocalItemsRef.current = null;
|
|
2623
|
+
applyLocalItemsToYDoc(board, {
|
|
2624
|
+
items: pendingLocal,
|
|
2625
|
+
origin: ORIGIN_LOCAL
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2386
2628
|
const baseRevision = currentRevisionRef.current;
|
|
2387
|
-
const
|
|
2629
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
2630
|
+
const preparedItems = prepareRealtimeItems(mergedItems);
|
|
2388
2631
|
if (!preparedItems) return;
|
|
2389
|
-
|
|
2632
|
+
queuedDirtyRef.current = false;
|
|
2390
2633
|
outboundInFlightRef.current = {
|
|
2391
2634
|
baseRevision,
|
|
2392
2635
|
items: preparedItems.items,
|
|
@@ -2399,8 +2642,20 @@ function useRealtimeSession(options) {
|
|
|
2399
2642
|
baseRevision,
|
|
2400
2643
|
items: preparedItems.items
|
|
2401
2644
|
});
|
|
2645
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
2646
|
+
if (pendingIds.size > 0) {
|
|
2647
|
+
setLocalDraftRef.current({
|
|
2648
|
+
roomId,
|
|
2649
|
+
baseRevision,
|
|
2650
|
+
items: mergedItems,
|
|
2651
|
+
updatedAt: nowMs(),
|
|
2652
|
+
yDocState: encodeYDocState(board),
|
|
2653
|
+
pendingIds: Array.from(pendingIds)
|
|
2654
|
+
});
|
|
2655
|
+
scheduleDraftPersistenceRef.current?.();
|
|
2656
|
+
}
|
|
2402
2657
|
if (!didSend) {
|
|
2403
|
-
|
|
2658
|
+
queuedDirtyRef.current = true;
|
|
2404
2659
|
outboundInFlightRef.current = null;
|
|
2405
2660
|
setHasPendingDocumentSync(true);
|
|
2406
2661
|
}
|
|
@@ -2415,66 +2670,103 @@ function useRealtimeSession(options) {
|
|
|
2415
2670
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2416
2671
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2417
2672
|
}, [clearDocumentFlushSchedule, flushQueuedDocument]);
|
|
2418
|
-
const queueDocumentSend = useCallback(
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
});
|
|
2426
|
-
scheduleDraftPersistence();
|
|
2427
|
-
queuedItemsRef.current = items;
|
|
2428
|
-
setHasPendingDocumentSync(true);
|
|
2429
|
-
if (conflictRef.current) return;
|
|
2430
|
-
scheduleDocumentFlushRef.current();
|
|
2431
|
-
},
|
|
2432
|
-
[roomId, scheduleDraftPersistence, setLocalDraft]
|
|
2433
|
-
);
|
|
2673
|
+
const queueDocumentSend = useCallback((items) => {
|
|
2674
|
+
pendingLocalItemsRef.current = items;
|
|
2675
|
+
queuedDirtyRef.current = true;
|
|
2676
|
+
setHasPendingDocumentSync(true);
|
|
2677
|
+
setHasLocalOfflineDraft(true);
|
|
2678
|
+
scheduleDocumentFlushRef.current();
|
|
2679
|
+
}, []);
|
|
2434
2680
|
const applyDraftSnapshot = useCallback(
|
|
2435
2681
|
(draft, options2) => {
|
|
2436
|
-
|
|
2682
|
+
const board = boardRef.current;
|
|
2683
|
+
if (board) {
|
|
2684
|
+
let restoredFromBinary = false;
|
|
2685
|
+
if (draft.yDocState) {
|
|
2686
|
+
if (board.yItems.length > 0) {
|
|
2687
|
+
board.doc.transact(() => {
|
|
2688
|
+
board.yItems.delete(0, board.yItems.length);
|
|
2689
|
+
}, ORIGIN_BOOTSTRAP);
|
|
2690
|
+
}
|
|
2691
|
+
restoredFromBinary = decodeYDocState(board, draft.yDocState);
|
|
2692
|
+
if (restoredFromBinary) {
|
|
2693
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
2694
|
+
for (let i = 0; i < board.yItems.length; i++) {
|
|
2695
|
+
const yMap = board.yItems.get(i);
|
|
2696
|
+
if (!yMap) continue;
|
|
2697
|
+
const id = yMap.get("id");
|
|
2698
|
+
if (typeof id === "string") allIds.add(id);
|
|
2699
|
+
}
|
|
2700
|
+
const pendingIds = new Set(draft.pendingIds ?? []);
|
|
2701
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2702
|
+
for (const id of allIds) {
|
|
2703
|
+
if (!pendingIds.has(id)) {
|
|
2704
|
+
board.lastServerConfirmedIds.add(id);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
if (!restoredFromBinary) {
|
|
2710
|
+
replaceYDocWithSnapshot(board, {
|
|
2711
|
+
items: draft.items,
|
|
2712
|
+
origin: ORIGIN_BOOTSTRAP
|
|
2713
|
+
});
|
|
2714
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
const items = board ? readVectorItems(board.yItems) : draft.items;
|
|
2718
|
+
const snapshot = {
|
|
2719
|
+
revision: draft.baseRevision,
|
|
2720
|
+
items,
|
|
2721
|
+
updatedAt: draft.updatedAt,
|
|
2722
|
+
updatedByClientId: clientIdRef.current
|
|
2723
|
+
};
|
|
2724
|
+
currentRevisionRef.current = snapshot.revision;
|
|
2725
|
+
latestDocumentRef.current = snapshot;
|
|
2726
|
+
setDocument(snapshot);
|
|
2727
|
+
if (!options2?.suppressSubscriberNotify) {
|
|
2728
|
+
notifySubscribers(items);
|
|
2729
|
+
}
|
|
2437
2730
|
},
|
|
2438
|
-
[
|
|
2731
|
+
[notifySubscribers]
|
|
2439
2732
|
);
|
|
2440
2733
|
const resolveAuthoritativeDocument = useCallback(
|
|
2441
2734
|
(serverDocument, options2) => {
|
|
2442
|
-
const
|
|
2443
|
-
|
|
2444
|
-
|
|
2735
|
+
const board = boardRef.current;
|
|
2736
|
+
const hadLocalContent = localDraftRef.current != null || board != null && board.yItems.length > 0;
|
|
2737
|
+
applyDocument(serverDocument, {
|
|
2738
|
+
...options2,
|
|
2739
|
+
replace: !hadLocalContent
|
|
2740
|
+
});
|
|
2741
|
+
if (!board) {
|
|
2445
2742
|
setHasPendingDocumentSync(false);
|
|
2446
|
-
|
|
2743
|
+
clearLocalDraftRef.current();
|
|
2447
2744
|
return false;
|
|
2448
2745
|
}
|
|
2449
|
-
|
|
2746
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
2747
|
+
if (pendingIds.size === 0) {
|
|
2450
2748
|
clearLocalDraftRef.current();
|
|
2451
2749
|
setHasPendingDocumentSync(false);
|
|
2452
|
-
setConflictState(null);
|
|
2453
|
-
applyDocument(serverDocument, options2);
|
|
2454
|
-
return true;
|
|
2455
|
-
}
|
|
2456
|
-
if (serverDocument.revision === localDraft.baseRevision) {
|
|
2457
|
-
setConflictState(null);
|
|
2458
|
-
applyDraftSnapshot(localDraft, {
|
|
2459
|
-
suppressSubscriberNotify: options2?.suppressSubscriberNotify ?? sameSerializedItems(latestDocumentRef.current?.items, localDraft.items)
|
|
2460
|
-
});
|
|
2461
2750
|
outboundInFlightRef.current = null;
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
scheduleDocumentFlush();
|
|
2465
|
-
return true;
|
|
2751
|
+
queuedDirtyRef.current = false;
|
|
2752
|
+
return false;
|
|
2466
2753
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2754
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
2755
|
+
setLocalDraft({
|
|
2756
|
+
roomId,
|
|
2757
|
+
baseRevision: serverDocument.revision,
|
|
2758
|
+
items: mergedItems,
|
|
2759
|
+
updatedAt: nowMs(),
|
|
2760
|
+
yDocState: encodeYDocState(board),
|
|
2761
|
+
pendingIds: Array.from(pendingIds)
|
|
2474
2762
|
});
|
|
2763
|
+
outboundInFlightRef.current = null;
|
|
2764
|
+
queuedDirtyRef.current = true;
|
|
2765
|
+
setHasPendingDocumentSync(true);
|
|
2766
|
+
scheduleDocumentFlush();
|
|
2475
2767
|
return true;
|
|
2476
2768
|
},
|
|
2477
|
-
[applyDocument,
|
|
2769
|
+
[applyDocument, roomId, scheduleDocumentFlush, setLocalDraft]
|
|
2478
2770
|
);
|
|
2479
2771
|
const sendPresenceUpdate = useCallback(() => {
|
|
2480
2772
|
sendRaw({
|
|
@@ -2513,54 +2805,8 @@ function useRealtimeSession(options) {
|
|
|
2513
2805
|
reconnect,
|
|
2514
2806
|
updateConnection
|
|
2515
2807
|
]);
|
|
2516
|
-
const resolveConflict = useCallback(
|
|
2517
|
-
|
|
2518
|
-
const activeConflict = conflictRef.current;
|
|
2519
|
-
if (!activeConflict) return;
|
|
2520
|
-
if (action === "use-server") {
|
|
2521
|
-
clearLocalDraft();
|
|
2522
|
-
queuedItemsRef.current = null;
|
|
2523
|
-
outboundInFlightRef.current = null;
|
|
2524
|
-
setConflictState(null);
|
|
2525
|
-
applyDocument({
|
|
2526
|
-
revision: activeConflict.serverRevision,
|
|
2527
|
-
items: activeConflict.serverItems,
|
|
2528
|
-
updatedAt: nowMs()
|
|
2529
|
-
});
|
|
2530
|
-
return;
|
|
2531
|
-
}
|
|
2532
|
-
const nextDraft = {
|
|
2533
|
-
roomId,
|
|
2534
|
-
baseRevision: activeConflict.serverRevision,
|
|
2535
|
-
items: activeConflict.localItems,
|
|
2536
|
-
updatedAt: nowMs()
|
|
2537
|
-
};
|
|
2538
|
-
setLocalDraft(nextDraft);
|
|
2539
|
-
scheduleDraftPersistence();
|
|
2540
|
-
setConflictState(null);
|
|
2541
|
-
applyDraftSnapshot(nextDraft, {
|
|
2542
|
-
suppressSubscriberNotify: sameSerializedItems(
|
|
2543
|
-
latestDocumentRef.current?.items,
|
|
2544
|
-
nextDraft.items
|
|
2545
|
-
)
|
|
2546
|
-
});
|
|
2547
|
-
currentRevisionRef.current = activeConflict.serverRevision;
|
|
2548
|
-
queuedItemsRef.current = nextDraft.items;
|
|
2549
|
-
outboundInFlightRef.current = null;
|
|
2550
|
-
setHasPendingDocumentSync(true);
|
|
2551
|
-
scheduleDocumentFlush();
|
|
2552
|
-
},
|
|
2553
|
-
[
|
|
2554
|
-
applyDocument,
|
|
2555
|
-
applyDraftSnapshot,
|
|
2556
|
-
clearLocalDraft,
|
|
2557
|
-
roomId,
|
|
2558
|
-
scheduleDocumentFlush,
|
|
2559
|
-
scheduleDraftPersistence,
|
|
2560
|
-
setConflictState,
|
|
2561
|
-
setLocalDraft
|
|
2562
|
-
]
|
|
2563
|
-
);
|
|
2808
|
+
const resolveConflict = useCallback((_action) => {
|
|
2809
|
+
}, []);
|
|
2564
2810
|
const setConflictStateRef = useRef(setConflictState);
|
|
2565
2811
|
setConflictStateRef.current = setConflictState;
|
|
2566
2812
|
const updateConnectionRef = useRef(updateConnection);
|
|
@@ -2581,7 +2827,15 @@ function useRealtimeSession(options) {
|
|
|
2581
2827
|
scheduleReconnectRef.current = scheduleReconnect;
|
|
2582
2828
|
const sendRawRef = useRef(sendRaw);
|
|
2583
2829
|
sendRawRef.current = sendRaw;
|
|
2830
|
+
const setLocalDraftRef = useRef(setLocalDraft);
|
|
2831
|
+
setLocalDraftRef.current = setLocalDraft;
|
|
2832
|
+
const scheduleDraftPersistenceRef = useRef(scheduleDraftPersistence);
|
|
2833
|
+
scheduleDraftPersistenceRef.current = scheduleDraftPersistence;
|
|
2584
2834
|
useEffect(() => {
|
|
2835
|
+
if (boardRef.current) {
|
|
2836
|
+
boardRef.current.doc.destroy();
|
|
2837
|
+
}
|
|
2838
|
+
boardRef.current = createYjsBoardDoc();
|
|
2585
2839
|
if (!roomId) {
|
|
2586
2840
|
clearDocumentFlushSchedule();
|
|
2587
2841
|
clearDraftPersistSchedule();
|
|
@@ -2589,7 +2843,8 @@ function useRealtimeSession(options) {
|
|
|
2589
2843
|
setHasLocalOfflineDraft(false);
|
|
2590
2844
|
setHasPendingDocumentSync(false);
|
|
2591
2845
|
setConflictState(null);
|
|
2592
|
-
|
|
2846
|
+
queuedDirtyRef.current = false;
|
|
2847
|
+
pendingLocalItemsRef.current = null;
|
|
2593
2848
|
outboundInFlightRef.current = null;
|
|
2594
2849
|
latestDocumentRef.current = null;
|
|
2595
2850
|
setDocument(null);
|
|
@@ -2600,7 +2855,8 @@ function useRealtimeSession(options) {
|
|
|
2600
2855
|
setLocalDraft(localDraft);
|
|
2601
2856
|
setHasPendingDocumentSync(localDraft != null);
|
|
2602
2857
|
setConflictState(null);
|
|
2603
|
-
|
|
2858
|
+
queuedDirtyRef.current = localDraft != null;
|
|
2859
|
+
pendingLocalItemsRef.current = null;
|
|
2604
2860
|
outboundInFlightRef.current = null;
|
|
2605
2861
|
if (localDraft) {
|
|
2606
2862
|
applyDraftSnapshot(localDraft, {
|
|
@@ -2631,7 +2887,7 @@ function useRealtimeSession(options) {
|
|
|
2631
2887
|
clearDocumentFlushSchedule();
|
|
2632
2888
|
wsRef.current?.close();
|
|
2633
2889
|
wsRef.current = null;
|
|
2634
|
-
|
|
2890
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
2635
2891
|
outboundInFlightRef.current = null;
|
|
2636
2892
|
setHasPendingDocumentSync(localDraftRef.current != null);
|
|
2637
2893
|
collapsePeersToSelfRef.current("offline");
|
|
@@ -2746,7 +3002,7 @@ function useRealtimeSession(options) {
|
|
|
2746
3002
|
}
|
|
2747
3003
|
);
|
|
2748
3004
|
if (!handledByDraft) {
|
|
2749
|
-
|
|
3005
|
+
queuedDirtyRef.current = false;
|
|
2750
3006
|
outboundInFlightRef.current = null;
|
|
2751
3007
|
setHasPendingDocumentSync(false);
|
|
2752
3008
|
}
|
|
@@ -2797,8 +3053,6 @@ function useRealtimeSession(options) {
|
|
|
2797
3053
|
const isSelfAck = parsed.document.updatedByClientId === selfClientId;
|
|
2798
3054
|
if (!isSelfAck) {
|
|
2799
3055
|
outboundInFlightRef.current = null;
|
|
2800
|
-
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2801
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2802
3056
|
resolveAuthoritativeDocumentRef.current(
|
|
2803
3057
|
sanitizeRealtimeSnapshot(parsed.document)
|
|
2804
3058
|
);
|
|
@@ -2809,24 +3063,33 @@ function useRealtimeSession(options) {
|
|
|
2809
3063
|
applyDocumentRef.current(sanitizeRealtimeSnapshot(parsed.document), {
|
|
2810
3064
|
suppressSubscriberNotify: shouldSuppress
|
|
2811
3065
|
});
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3066
|
+
const board = boardRef.current;
|
|
3067
|
+
const stillPending = board ? getLocallyPendingItemIds(board).size > 0 : false;
|
|
3068
|
+
if (stillPending) {
|
|
3069
|
+
const mergedItems = board ? readVectorItems(board.yItems) : [];
|
|
3070
|
+
setLocalDraftRef.current({
|
|
3071
|
+
roomId,
|
|
3072
|
+
baseRevision: currentRevisionRef.current,
|
|
3073
|
+
items: mergedItems,
|
|
3074
|
+
updatedAt: nowMs(),
|
|
3075
|
+
yDocState: board ? encodeYDocState(board) : void 0,
|
|
3076
|
+
pendingIds: board ? Array.from(getLocallyPendingItemIds(board)) : void 0
|
|
3077
|
+
});
|
|
3078
|
+
queuedDirtyRef.current = true;
|
|
3079
|
+
setHasPendingDocumentSync(true);
|
|
3080
|
+
scheduleDocumentFlushRef.current();
|
|
3081
|
+
} else {
|
|
3082
|
+
queuedDirtyRef.current = false;
|
|
2820
3083
|
clearLocalDraftRef.current();
|
|
3084
|
+
setHasPendingDocumentSync(false);
|
|
2821
3085
|
}
|
|
2822
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2823
3086
|
setConflictStateRef.current(null);
|
|
2824
3087
|
return;
|
|
2825
3088
|
}
|
|
2826
3089
|
if (parsed.type === "document:resync-required") {
|
|
2827
3090
|
outboundInFlightRef.current = null;
|
|
2828
|
-
|
|
2829
|
-
setHasPendingDocumentSync(
|
|
3091
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
3092
|
+
setHasPendingDocumentSync(queuedDirtyRef.current);
|
|
2830
3093
|
updateConnectionRef.current((prev) => ({
|
|
2831
3094
|
...prev,
|
|
2832
3095
|
lastError: parsed.reason
|
|
@@ -2901,14 +3164,39 @@ function useRealtimeSession(options) {
|
|
|
2901
3164
|
() => () => {
|
|
2902
3165
|
clearDocumentFlushSchedule();
|
|
2903
3166
|
clearDraftPersistSchedule();
|
|
3167
|
+
if (boardRef.current) {
|
|
3168
|
+
boardRef.current.doc.destroy();
|
|
3169
|
+
boardRef.current = null;
|
|
3170
|
+
}
|
|
2904
3171
|
},
|
|
2905
3172
|
[clearDocumentFlushSchedule, clearDraftPersistSchedule]
|
|
2906
3173
|
);
|
|
2907
3174
|
const flushDocumentSync = useCallback(async () => {
|
|
3175
|
+
const board = boardRef.current;
|
|
3176
|
+
const pendingLocal = pendingLocalItemsRef.current;
|
|
3177
|
+
if (board && pendingLocal) {
|
|
3178
|
+
pendingLocalItemsRef.current = null;
|
|
3179
|
+
applyLocalItemsToYDoc(board, {
|
|
3180
|
+
items: pendingLocal,
|
|
3181
|
+
origin: ORIGIN_LOCAL
|
|
3182
|
+
});
|
|
3183
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
3184
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
3185
|
+
if (pendingIds.size > 0) {
|
|
3186
|
+
setLocalDraftRef.current({
|
|
3187
|
+
roomId,
|
|
3188
|
+
baseRevision: currentRevisionRef.current,
|
|
3189
|
+
items: mergedItems,
|
|
3190
|
+
updatedAt: nowMs(),
|
|
3191
|
+
yDocState: encodeYDocState(board),
|
|
3192
|
+
pendingIds: Array.from(pendingIds)
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
2908
3196
|
persistLocalDraft();
|
|
2909
3197
|
if (!connection.connected) return;
|
|
2910
3198
|
flushQueuedDocument();
|
|
2911
|
-
}, [connection.connected, flushQueuedDocument, persistLocalDraft]);
|
|
3199
|
+
}, [connection.connected, flushQueuedDocument, persistLocalDraft, roomId]);
|
|
2912
3200
|
const remoteAdapter = useMemo(
|
|
2913
3201
|
() => ({
|
|
2914
3202
|
subscribe(onItems) {
|