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.cjs
CHANGED
|
@@ -4,10 +4,30 @@ var lucideReact = require('lucide-react');
|
|
|
4
4
|
var getStroke = require('perfect-freehand');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
var react = require('react');
|
|
7
|
+
var Y = require('yjs');
|
|
7
8
|
|
|
8
9
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
10
|
|
|
11
|
+
function _interopNamespace(e) {
|
|
12
|
+
if (e && e.__esModule) return e;
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
var getStroke__default = /*#__PURE__*/_interopDefault(getStroke);
|
|
30
|
+
var Y__namespace = /*#__PURE__*/_interopNamespace(Y);
|
|
11
31
|
|
|
12
32
|
// src/react/presence/map-placement-preview.ts
|
|
13
33
|
function remoteMarkupStrokeFromPlacementPreview(preview) {
|
|
@@ -1992,8 +2012,215 @@ function useRealtimePeerFollow(options) {
|
|
|
1992
2012
|
lastAppliedCameraKeyRef.current = nextCameraKey;
|
|
1993
2013
|
}, [followedPeerId, onFollowEnd, sessionPeers, viewportRef]);
|
|
1994
2014
|
}
|
|
2015
|
+
var ITEMS_KEY = "items";
|
|
2016
|
+
var SERVER_ADD_RACE_WINDOW_MS = 2e3;
|
|
2017
|
+
function createYjsBoardDoc() {
|
|
2018
|
+
const doc = new Y__namespace.Doc();
|
|
2019
|
+
const yItems = doc.getArray(ITEMS_KEY);
|
|
2020
|
+
return {
|
|
2021
|
+
doc,
|
|
2022
|
+
yItems,
|
|
2023
|
+
lastServerConfirmedIds: /* @__PURE__ */ new Set(),
|
|
2024
|
+
serverItemSeenAt: /* @__PURE__ */ new Map()
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function getItemId(item) {
|
|
2028
|
+
if (item instanceof Y__namespace.Map) {
|
|
2029
|
+
const id2 = item.get("id");
|
|
2030
|
+
return typeof id2 === "string" ? id2 : null;
|
|
2031
|
+
}
|
|
2032
|
+
const id = item.id;
|
|
2033
|
+
return typeof id === "string" ? id : null;
|
|
2034
|
+
}
|
|
2035
|
+
function vectorItemToYMap(item) {
|
|
2036
|
+
const yMap = new Y__namespace.Map();
|
|
2037
|
+
for (const [key, value] of Object.entries(item)) {
|
|
2038
|
+
if (value === void 0) continue;
|
|
2039
|
+
yMap.set(key, value);
|
|
2040
|
+
}
|
|
2041
|
+
return yMap;
|
|
2042
|
+
}
|
|
2043
|
+
function yMapToVectorItem(yMap) {
|
|
2044
|
+
const obj = {};
|
|
2045
|
+
for (const [key, value] of yMap.entries()) {
|
|
2046
|
+
obj[key] = value;
|
|
2047
|
+
}
|
|
2048
|
+
return obj;
|
|
2049
|
+
}
|
|
2050
|
+
function readVectorItems(yItems) {
|
|
2051
|
+
const result = [];
|
|
2052
|
+
for (let i = 0; i < yItems.length; i++) {
|
|
2053
|
+
const yMap = yItems.get(i);
|
|
2054
|
+
if (yMap) result.push(yMapToVectorItem(yMap));
|
|
2055
|
+
}
|
|
2056
|
+
return result;
|
|
2057
|
+
}
|
|
2058
|
+
function indexYItemsById(yItems) {
|
|
2059
|
+
const result = /* @__PURE__ */ new Map();
|
|
2060
|
+
for (let i = 0; i < yItems.length; i++) {
|
|
2061
|
+
const yMap = yItems.get(i);
|
|
2062
|
+
if (!yMap) continue;
|
|
2063
|
+
const id = getItemId(yMap);
|
|
2064
|
+
if (!id) continue;
|
|
2065
|
+
result.set(id, { yMap, index: i });
|
|
2066
|
+
}
|
|
2067
|
+
return result;
|
|
2068
|
+
}
|
|
2069
|
+
function valuesEqual(left, right) {
|
|
2070
|
+
if (left === right) return true;
|
|
2071
|
+
if (typeof left !== typeof right) return false;
|
|
2072
|
+
if (left === null || right === null) return false;
|
|
2073
|
+
if (typeof left !== "object") return false;
|
|
2074
|
+
try {
|
|
2075
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
2076
|
+
} catch {
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function updateYMapInPlace(yMap, next) {
|
|
2081
|
+
const nextKeys = /* @__PURE__ */ new Set();
|
|
2082
|
+
for (const [key, value] of Object.entries(next)) {
|
|
2083
|
+
if (value === void 0) continue;
|
|
2084
|
+
nextKeys.add(key);
|
|
2085
|
+
const current = yMap.get(key);
|
|
2086
|
+
if (!valuesEqual(current, value)) {
|
|
2087
|
+
yMap.set(key, value);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
for (const key of Array.from(yMap.keys())) {
|
|
2091
|
+
if (!nextKeys.has(key)) yMap.delete(key);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
function applyLocalItemsToYDoc(board, options) {
|
|
2095
|
+
const { items, origin } = options;
|
|
2096
|
+
const addedIds = [];
|
|
2097
|
+
const removedIds = [];
|
|
2098
|
+
const now = Date.now();
|
|
2099
|
+
board.doc.transact(() => {
|
|
2100
|
+
const currentIndex = indexYItemsById(board.yItems);
|
|
2101
|
+
const nextIds = /* @__PURE__ */ new Set();
|
|
2102
|
+
for (const item of items) {
|
|
2103
|
+
const id = getItemId(item);
|
|
2104
|
+
if (!id) continue;
|
|
2105
|
+
nextIds.add(id);
|
|
2106
|
+
}
|
|
2107
|
+
const toDelete = [];
|
|
2108
|
+
for (const [id, entry] of currentIndex) {
|
|
2109
|
+
if (nextIds.has(id)) continue;
|
|
2110
|
+
const serverSeenAt = board.serverItemSeenAt.get(id);
|
|
2111
|
+
if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
toDelete.push({ id, index: entry.index });
|
|
2115
|
+
}
|
|
2116
|
+
toDelete.sort((a, b) => b.index - a.index);
|
|
2117
|
+
for (const { id, index } of toDelete) {
|
|
2118
|
+
board.yItems.delete(index, 1);
|
|
2119
|
+
board.serverItemSeenAt.delete(id);
|
|
2120
|
+
removedIds.push(id);
|
|
2121
|
+
}
|
|
2122
|
+
const refreshedIndex = indexYItemsById(board.yItems);
|
|
2123
|
+
for (let nextOrder = 0; nextOrder < items.length; nextOrder++) {
|
|
2124
|
+
const item = items[nextOrder];
|
|
2125
|
+
if (!item) continue;
|
|
2126
|
+
const id = getItemId(item);
|
|
2127
|
+
if (!id) continue;
|
|
2128
|
+
const existing = refreshedIndex.get(id);
|
|
2129
|
+
if (existing) {
|
|
2130
|
+
updateYMapInPlace(existing.yMap, item);
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
const yMap = vectorItemToYMap(item);
|
|
2134
|
+
board.yItems.push([yMap]);
|
|
2135
|
+
addedIds.push(id);
|
|
2136
|
+
}
|
|
2137
|
+
}, origin);
|
|
2138
|
+
return { addedIds, removedIds };
|
|
2139
|
+
}
|
|
2140
|
+
function applyServerSnapshotToYDoc(board, options) {
|
|
2141
|
+
const { items: snapshotItems, origin } = options;
|
|
2142
|
+
const now = Date.now();
|
|
2143
|
+
board.doc.transact(() => {
|
|
2144
|
+
for (const item of snapshotItems) {
|
|
2145
|
+
const id = getItemId(item);
|
|
2146
|
+
if (!id) continue;
|
|
2147
|
+
const existing = indexYItemsById(board.yItems).get(id);
|
|
2148
|
+
if (existing) {
|
|
2149
|
+
updateYMapInPlace(existing.yMap, item);
|
|
2150
|
+
board.serverItemSeenAt.set(id, now);
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
const yMap = vectorItemToYMap(item);
|
|
2154
|
+
board.yItems.push([yMap]);
|
|
2155
|
+
board.serverItemSeenAt.set(id, now);
|
|
2156
|
+
}
|
|
2157
|
+
}, origin);
|
|
2158
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2159
|
+
for (const item of snapshotItems) {
|
|
2160
|
+
const id = getItemId(item);
|
|
2161
|
+
if (id) board.lastServerConfirmedIds.add(id);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
function replaceYDocWithSnapshot(board, options) {
|
|
2165
|
+
const { items: snapshotItems, origin } = options;
|
|
2166
|
+
const now = Date.now();
|
|
2167
|
+
board.doc.transact(() => {
|
|
2168
|
+
if (board.yItems.length > 0) {
|
|
2169
|
+
board.yItems.delete(0, board.yItems.length);
|
|
2170
|
+
}
|
|
2171
|
+
for (const item of snapshotItems) {
|
|
2172
|
+
board.yItems.push([vectorItemToYMap(item)]);
|
|
2173
|
+
}
|
|
2174
|
+
}, origin);
|
|
2175
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2176
|
+
board.serverItemSeenAt = /* @__PURE__ */ new Map();
|
|
2177
|
+
for (const item of snapshotItems) {
|
|
2178
|
+
const id = getItemId(item);
|
|
2179
|
+
if (id) {
|
|
2180
|
+
board.lastServerConfirmedIds.add(id);
|
|
2181
|
+
board.serverItemSeenAt.set(id, now);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function getLocallyPendingItemIds(board) {
|
|
2186
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2187
|
+
for (let i = 0; i < board.yItems.length; i++) {
|
|
2188
|
+
const yMap = board.yItems.get(i);
|
|
2189
|
+
if (!yMap) continue;
|
|
2190
|
+
const id = getItemId(yMap);
|
|
2191
|
+
if (!id) continue;
|
|
2192
|
+
if (!board.lastServerConfirmedIds.has(id)) {
|
|
2193
|
+
pending.add(id);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return pending;
|
|
2197
|
+
}
|
|
2198
|
+
function encodeYDocState(board) {
|
|
2199
|
+
const update = Y__namespace.encodeStateAsUpdate(board.doc);
|
|
2200
|
+
let binary = "";
|
|
2201
|
+
for (let i = 0; i < update.length; i++) {
|
|
2202
|
+
binary += String.fromCharCode(update[i]);
|
|
2203
|
+
}
|
|
2204
|
+
return btoa(binary);
|
|
2205
|
+
}
|
|
2206
|
+
function decodeYDocState(board, encoded) {
|
|
2207
|
+
try {
|
|
2208
|
+
const binary = atob(encoded);
|
|
2209
|
+
const update = new Uint8Array(binary.length);
|
|
2210
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2211
|
+
update[i] = binary.charCodeAt(i);
|
|
2212
|
+
}
|
|
2213
|
+
Y__namespace.applyUpdate(board.doc, update);
|
|
2214
|
+
return true;
|
|
2215
|
+
} catch {
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
1995
2219
|
|
|
1996
2220
|
// src/react/plugins/realtime/use-realtime-session.ts
|
|
2221
|
+
var ORIGIN_LOCAL = /* @__PURE__ */ Symbol("canvu/realtime/local");
|
|
2222
|
+
var ORIGIN_REMOTE = /* @__PURE__ */ Symbol("canvu/realtime/remote");
|
|
2223
|
+
var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
|
|
1997
2224
|
var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
|
|
1998
2225
|
var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
|
|
1999
2226
|
var DRAFT_PERSIST_DEBOUNCE_MS = 420;
|
|
@@ -2080,7 +2307,16 @@ function draftStorageKey(roomId) {
|
|
|
2080
2307
|
function isRealtimeOfflineDraft(value) {
|
|
2081
2308
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2082
2309
|
const record = value;
|
|
2083
|
-
|
|
2310
|
+
if (typeof record.roomId !== "string" || typeof record.baseRevision !== "number" || typeof record.updatedAt !== "number" || !Array.isArray(record.items)) {
|
|
2311
|
+
return false;
|
|
2312
|
+
}
|
|
2313
|
+
if (record.yDocState != null && typeof record.yDocState !== "string") {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
2316
|
+
if (record.pendingIds != null && !Array.isArray(record.pendingIds)) {
|
|
2317
|
+
return false;
|
|
2318
|
+
}
|
|
2319
|
+
return true;
|
|
2084
2320
|
}
|
|
2085
2321
|
function readRealtimeOfflineDraft(roomId) {
|
|
2086
2322
|
if (typeof window === "undefined" || !roomId) return null;
|
|
@@ -2118,14 +2354,6 @@ function removeRealtimeOfflineDraft(roomId) {
|
|
|
2118
2354
|
} catch {
|
|
2119
2355
|
}
|
|
2120
2356
|
}
|
|
2121
|
-
function buildDraftSnapshot(draft, clientId) {
|
|
2122
|
-
return {
|
|
2123
|
-
revision: draft.baseRevision,
|
|
2124
|
-
items: draft.items,
|
|
2125
|
-
updatedAt: draft.updatedAt,
|
|
2126
|
-
updatedByClientId: clientId
|
|
2127
|
-
};
|
|
2128
|
-
}
|
|
2129
2357
|
function getViewportCameraSnapshot(viewport) {
|
|
2130
2358
|
if (!viewport) return null;
|
|
2131
2359
|
const camera = viewport.getCamera();
|
|
@@ -2176,8 +2404,13 @@ function useRealtimeSession(options) {
|
|
|
2176
2404
|
const retryCountRef = react.useRef(0);
|
|
2177
2405
|
const currentRevisionRef = react.useRef(0);
|
|
2178
2406
|
const outboundInFlightRef = react.useRef(null);
|
|
2179
|
-
const
|
|
2407
|
+
const queuedDirtyRef = react.useRef(false);
|
|
2408
|
+
const pendingLocalItemsRef = react.useRef(null);
|
|
2180
2409
|
const subscriberRefs = react.useRef(/* @__PURE__ */ new Set());
|
|
2410
|
+
const boardRef = react.useRef(null);
|
|
2411
|
+
if (boardRef.current == null) {
|
|
2412
|
+
boardRef.current = createYjsBoardDoc();
|
|
2413
|
+
}
|
|
2181
2414
|
const lastCursorRef = react.useRef(null);
|
|
2182
2415
|
const lastMarkupStrokeRef = react.useRef(null);
|
|
2183
2416
|
const lastCameraRef = react.useRef(
|
|
@@ -2272,11 +2505,30 @@ function useRealtimeSession(options) {
|
|
|
2272
2505
|
if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
|
|
2273
2506
|
return;
|
|
2274
2507
|
}
|
|
2508
|
+
const board = boardRef.current;
|
|
2509
|
+
if (board) {
|
|
2510
|
+
if (options2?.replace) {
|
|
2511
|
+
replaceYDocWithSnapshot(board, {
|
|
2512
|
+
items: snapshot.items,
|
|
2513
|
+
origin: ORIGIN_REMOTE
|
|
2514
|
+
});
|
|
2515
|
+
} else {
|
|
2516
|
+
applyServerSnapshotToYDoc(board, {
|
|
2517
|
+
items: snapshot.items,
|
|
2518
|
+
origin: ORIGIN_REMOTE
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
|
|
2523
|
+
const mergedSnapshot = {
|
|
2524
|
+
...snapshot,
|
|
2525
|
+
items: mergedItems
|
|
2526
|
+
};
|
|
2275
2527
|
currentRevisionRef.current = snapshot.revision;
|
|
2276
|
-
latestDocumentRef.current =
|
|
2277
|
-
setDocument(
|
|
2528
|
+
latestDocumentRef.current = mergedSnapshot;
|
|
2529
|
+
setDocument(mergedSnapshot);
|
|
2278
2530
|
if (!options2?.suppressSubscriberNotify) {
|
|
2279
|
-
notifySubscribers(
|
|
2531
|
+
notifySubscribers(mergedItems);
|
|
2280
2532
|
}
|
|
2281
2533
|
},
|
|
2282
2534
|
[notifySubscribers]
|
|
@@ -2386,13 +2638,23 @@ function useRealtimeSession(options) {
|
|
|
2386
2638
|
);
|
|
2387
2639
|
const flushQueuedDocument = react.useCallback(() => {
|
|
2388
2640
|
clearDocumentFlushSchedule();
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
if (!
|
|
2641
|
+
const board = boardRef.current;
|
|
2642
|
+
if (!board) return;
|
|
2643
|
+
if (!queuedDirtyRef.current && pendingLocalItemsRef.current == null) return;
|
|
2644
|
+
if (outboundInFlightRef.current) return;
|
|
2645
|
+
const pendingLocal = pendingLocalItemsRef.current;
|
|
2646
|
+
if (pendingLocal) {
|
|
2647
|
+
pendingLocalItemsRef.current = null;
|
|
2648
|
+
applyLocalItemsToYDoc(board, {
|
|
2649
|
+
items: pendingLocal,
|
|
2650
|
+
origin: ORIGIN_LOCAL
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2392
2653
|
const baseRevision = currentRevisionRef.current;
|
|
2393
|
-
const
|
|
2654
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
2655
|
+
const preparedItems = prepareRealtimeItems(mergedItems);
|
|
2394
2656
|
if (!preparedItems) return;
|
|
2395
|
-
|
|
2657
|
+
queuedDirtyRef.current = false;
|
|
2396
2658
|
outboundInFlightRef.current = {
|
|
2397
2659
|
baseRevision,
|
|
2398
2660
|
items: preparedItems.items,
|
|
@@ -2405,8 +2667,20 @@ function useRealtimeSession(options) {
|
|
|
2405
2667
|
baseRevision,
|
|
2406
2668
|
items: preparedItems.items
|
|
2407
2669
|
});
|
|
2670
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
2671
|
+
if (pendingIds.size > 0) {
|
|
2672
|
+
setLocalDraftRef.current({
|
|
2673
|
+
roomId,
|
|
2674
|
+
baseRevision,
|
|
2675
|
+
items: mergedItems,
|
|
2676
|
+
updatedAt: nowMs(),
|
|
2677
|
+
yDocState: encodeYDocState(board),
|
|
2678
|
+
pendingIds: Array.from(pendingIds)
|
|
2679
|
+
});
|
|
2680
|
+
scheduleDraftPersistenceRef.current?.();
|
|
2681
|
+
}
|
|
2408
2682
|
if (!didSend) {
|
|
2409
|
-
|
|
2683
|
+
queuedDirtyRef.current = true;
|
|
2410
2684
|
outboundInFlightRef.current = null;
|
|
2411
2685
|
setHasPendingDocumentSync(true);
|
|
2412
2686
|
}
|
|
@@ -2421,66 +2695,103 @@ function useRealtimeSession(options) {
|
|
|
2421
2695
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2422
2696
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2423
2697
|
}, [clearDocumentFlushSchedule, flushQueuedDocument]);
|
|
2424
|
-
const queueDocumentSend = react.useCallback(
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
});
|
|
2432
|
-
scheduleDraftPersistence();
|
|
2433
|
-
queuedItemsRef.current = items;
|
|
2434
|
-
setHasPendingDocumentSync(true);
|
|
2435
|
-
if (conflictRef.current) return;
|
|
2436
|
-
scheduleDocumentFlushRef.current();
|
|
2437
|
-
},
|
|
2438
|
-
[roomId, scheduleDraftPersistence, setLocalDraft]
|
|
2439
|
-
);
|
|
2698
|
+
const queueDocumentSend = react.useCallback((items) => {
|
|
2699
|
+
pendingLocalItemsRef.current = items;
|
|
2700
|
+
queuedDirtyRef.current = true;
|
|
2701
|
+
setHasPendingDocumentSync(true);
|
|
2702
|
+
setHasLocalOfflineDraft(true);
|
|
2703
|
+
scheduleDocumentFlushRef.current();
|
|
2704
|
+
}, []);
|
|
2440
2705
|
const applyDraftSnapshot = react.useCallback(
|
|
2441
2706
|
(draft, options2) => {
|
|
2442
|
-
|
|
2707
|
+
const board = boardRef.current;
|
|
2708
|
+
if (board) {
|
|
2709
|
+
let restoredFromBinary = false;
|
|
2710
|
+
if (draft.yDocState) {
|
|
2711
|
+
if (board.yItems.length > 0) {
|
|
2712
|
+
board.doc.transact(() => {
|
|
2713
|
+
board.yItems.delete(0, board.yItems.length);
|
|
2714
|
+
}, ORIGIN_BOOTSTRAP);
|
|
2715
|
+
}
|
|
2716
|
+
restoredFromBinary = decodeYDocState(board, draft.yDocState);
|
|
2717
|
+
if (restoredFromBinary) {
|
|
2718
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
2719
|
+
for (let i = 0; i < board.yItems.length; i++) {
|
|
2720
|
+
const yMap = board.yItems.get(i);
|
|
2721
|
+
if (!yMap) continue;
|
|
2722
|
+
const id = yMap.get("id");
|
|
2723
|
+
if (typeof id === "string") allIds.add(id);
|
|
2724
|
+
}
|
|
2725
|
+
const pendingIds = new Set(draft.pendingIds ?? []);
|
|
2726
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2727
|
+
for (const id of allIds) {
|
|
2728
|
+
if (!pendingIds.has(id)) {
|
|
2729
|
+
board.lastServerConfirmedIds.add(id);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
if (!restoredFromBinary) {
|
|
2735
|
+
replaceYDocWithSnapshot(board, {
|
|
2736
|
+
items: draft.items,
|
|
2737
|
+
origin: ORIGIN_BOOTSTRAP
|
|
2738
|
+
});
|
|
2739
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
const items = board ? readVectorItems(board.yItems) : draft.items;
|
|
2743
|
+
const snapshot = {
|
|
2744
|
+
revision: draft.baseRevision,
|
|
2745
|
+
items,
|
|
2746
|
+
updatedAt: draft.updatedAt,
|
|
2747
|
+
updatedByClientId: clientIdRef.current
|
|
2748
|
+
};
|
|
2749
|
+
currentRevisionRef.current = snapshot.revision;
|
|
2750
|
+
latestDocumentRef.current = snapshot;
|
|
2751
|
+
setDocument(snapshot);
|
|
2752
|
+
if (!options2?.suppressSubscriberNotify) {
|
|
2753
|
+
notifySubscribers(items);
|
|
2754
|
+
}
|
|
2443
2755
|
},
|
|
2444
|
-
[
|
|
2756
|
+
[notifySubscribers]
|
|
2445
2757
|
);
|
|
2446
2758
|
const resolveAuthoritativeDocument = react.useCallback(
|
|
2447
2759
|
(serverDocument, options2) => {
|
|
2448
|
-
const
|
|
2449
|
-
|
|
2450
|
-
|
|
2760
|
+
const board = boardRef.current;
|
|
2761
|
+
const hadLocalContent = localDraftRef.current != null || board != null && board.yItems.length > 0;
|
|
2762
|
+
applyDocument(serverDocument, {
|
|
2763
|
+
...options2,
|
|
2764
|
+
replace: !hadLocalContent
|
|
2765
|
+
});
|
|
2766
|
+
if (!board) {
|
|
2451
2767
|
setHasPendingDocumentSync(false);
|
|
2452
|
-
|
|
2768
|
+
clearLocalDraftRef.current();
|
|
2453
2769
|
return false;
|
|
2454
2770
|
}
|
|
2455
|
-
|
|
2771
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
2772
|
+
if (pendingIds.size === 0) {
|
|
2456
2773
|
clearLocalDraftRef.current();
|
|
2457
2774
|
setHasPendingDocumentSync(false);
|
|
2458
|
-
setConflictState(null);
|
|
2459
|
-
applyDocument(serverDocument, options2);
|
|
2460
|
-
return true;
|
|
2461
|
-
}
|
|
2462
|
-
if (serverDocument.revision === localDraft.baseRevision) {
|
|
2463
|
-
setConflictState(null);
|
|
2464
|
-
applyDraftSnapshot(localDraft, {
|
|
2465
|
-
suppressSubscriberNotify: options2?.suppressSubscriberNotify ?? sameSerializedItems(latestDocumentRef.current?.items, localDraft.items)
|
|
2466
|
-
});
|
|
2467
2775
|
outboundInFlightRef.current = null;
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
scheduleDocumentFlush();
|
|
2471
|
-
return true;
|
|
2776
|
+
queuedDirtyRef.current = false;
|
|
2777
|
+
return false;
|
|
2472
2778
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2779
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
2780
|
+
setLocalDraft({
|
|
2781
|
+
roomId,
|
|
2782
|
+
baseRevision: serverDocument.revision,
|
|
2783
|
+
items: mergedItems,
|
|
2784
|
+
updatedAt: nowMs(),
|
|
2785
|
+
yDocState: encodeYDocState(board),
|
|
2786
|
+
pendingIds: Array.from(pendingIds)
|
|
2480
2787
|
});
|
|
2788
|
+
outboundInFlightRef.current = null;
|
|
2789
|
+
queuedDirtyRef.current = true;
|
|
2790
|
+
setHasPendingDocumentSync(true);
|
|
2791
|
+
scheduleDocumentFlush();
|
|
2481
2792
|
return true;
|
|
2482
2793
|
},
|
|
2483
|
-
[applyDocument,
|
|
2794
|
+
[applyDocument, roomId, scheduleDocumentFlush, setLocalDraft]
|
|
2484
2795
|
);
|
|
2485
2796
|
const sendPresenceUpdate = react.useCallback(() => {
|
|
2486
2797
|
sendRaw({
|
|
@@ -2519,54 +2830,8 @@ function useRealtimeSession(options) {
|
|
|
2519
2830
|
reconnect,
|
|
2520
2831
|
updateConnection
|
|
2521
2832
|
]);
|
|
2522
|
-
const resolveConflict = react.useCallback(
|
|
2523
|
-
|
|
2524
|
-
const activeConflict = conflictRef.current;
|
|
2525
|
-
if (!activeConflict) return;
|
|
2526
|
-
if (action === "use-server") {
|
|
2527
|
-
clearLocalDraft();
|
|
2528
|
-
queuedItemsRef.current = null;
|
|
2529
|
-
outboundInFlightRef.current = null;
|
|
2530
|
-
setConflictState(null);
|
|
2531
|
-
applyDocument({
|
|
2532
|
-
revision: activeConflict.serverRevision,
|
|
2533
|
-
items: activeConflict.serverItems,
|
|
2534
|
-
updatedAt: nowMs()
|
|
2535
|
-
});
|
|
2536
|
-
return;
|
|
2537
|
-
}
|
|
2538
|
-
const nextDraft = {
|
|
2539
|
-
roomId,
|
|
2540
|
-
baseRevision: activeConflict.serverRevision,
|
|
2541
|
-
items: activeConflict.localItems,
|
|
2542
|
-
updatedAt: nowMs()
|
|
2543
|
-
};
|
|
2544
|
-
setLocalDraft(nextDraft);
|
|
2545
|
-
scheduleDraftPersistence();
|
|
2546
|
-
setConflictState(null);
|
|
2547
|
-
applyDraftSnapshot(nextDraft, {
|
|
2548
|
-
suppressSubscriberNotify: sameSerializedItems(
|
|
2549
|
-
latestDocumentRef.current?.items,
|
|
2550
|
-
nextDraft.items
|
|
2551
|
-
)
|
|
2552
|
-
});
|
|
2553
|
-
currentRevisionRef.current = activeConflict.serverRevision;
|
|
2554
|
-
queuedItemsRef.current = nextDraft.items;
|
|
2555
|
-
outboundInFlightRef.current = null;
|
|
2556
|
-
setHasPendingDocumentSync(true);
|
|
2557
|
-
scheduleDocumentFlush();
|
|
2558
|
-
},
|
|
2559
|
-
[
|
|
2560
|
-
applyDocument,
|
|
2561
|
-
applyDraftSnapshot,
|
|
2562
|
-
clearLocalDraft,
|
|
2563
|
-
roomId,
|
|
2564
|
-
scheduleDocumentFlush,
|
|
2565
|
-
scheduleDraftPersistence,
|
|
2566
|
-
setConflictState,
|
|
2567
|
-
setLocalDraft
|
|
2568
|
-
]
|
|
2569
|
-
);
|
|
2833
|
+
const resolveConflict = react.useCallback((_action) => {
|
|
2834
|
+
}, []);
|
|
2570
2835
|
const setConflictStateRef = react.useRef(setConflictState);
|
|
2571
2836
|
setConflictStateRef.current = setConflictState;
|
|
2572
2837
|
const updateConnectionRef = react.useRef(updateConnection);
|
|
@@ -2587,7 +2852,15 @@ function useRealtimeSession(options) {
|
|
|
2587
2852
|
scheduleReconnectRef.current = scheduleReconnect;
|
|
2588
2853
|
const sendRawRef = react.useRef(sendRaw);
|
|
2589
2854
|
sendRawRef.current = sendRaw;
|
|
2855
|
+
const setLocalDraftRef = react.useRef(setLocalDraft);
|
|
2856
|
+
setLocalDraftRef.current = setLocalDraft;
|
|
2857
|
+
const scheduleDraftPersistenceRef = react.useRef(scheduleDraftPersistence);
|
|
2858
|
+
scheduleDraftPersistenceRef.current = scheduleDraftPersistence;
|
|
2590
2859
|
react.useEffect(() => {
|
|
2860
|
+
if (boardRef.current) {
|
|
2861
|
+
boardRef.current.doc.destroy();
|
|
2862
|
+
}
|
|
2863
|
+
boardRef.current = createYjsBoardDoc();
|
|
2591
2864
|
if (!roomId) {
|
|
2592
2865
|
clearDocumentFlushSchedule();
|
|
2593
2866
|
clearDraftPersistSchedule();
|
|
@@ -2595,7 +2868,8 @@ function useRealtimeSession(options) {
|
|
|
2595
2868
|
setHasLocalOfflineDraft(false);
|
|
2596
2869
|
setHasPendingDocumentSync(false);
|
|
2597
2870
|
setConflictState(null);
|
|
2598
|
-
|
|
2871
|
+
queuedDirtyRef.current = false;
|
|
2872
|
+
pendingLocalItemsRef.current = null;
|
|
2599
2873
|
outboundInFlightRef.current = null;
|
|
2600
2874
|
latestDocumentRef.current = null;
|
|
2601
2875
|
setDocument(null);
|
|
@@ -2606,7 +2880,8 @@ function useRealtimeSession(options) {
|
|
|
2606
2880
|
setLocalDraft(localDraft);
|
|
2607
2881
|
setHasPendingDocumentSync(localDraft != null);
|
|
2608
2882
|
setConflictState(null);
|
|
2609
|
-
|
|
2883
|
+
queuedDirtyRef.current = localDraft != null;
|
|
2884
|
+
pendingLocalItemsRef.current = null;
|
|
2610
2885
|
outboundInFlightRef.current = null;
|
|
2611
2886
|
if (localDraft) {
|
|
2612
2887
|
applyDraftSnapshot(localDraft, {
|
|
@@ -2637,7 +2912,7 @@ function useRealtimeSession(options) {
|
|
|
2637
2912
|
clearDocumentFlushSchedule();
|
|
2638
2913
|
wsRef.current?.close();
|
|
2639
2914
|
wsRef.current = null;
|
|
2640
|
-
|
|
2915
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
2641
2916
|
outboundInFlightRef.current = null;
|
|
2642
2917
|
setHasPendingDocumentSync(localDraftRef.current != null);
|
|
2643
2918
|
collapsePeersToSelfRef.current("offline");
|
|
@@ -2752,7 +3027,7 @@ function useRealtimeSession(options) {
|
|
|
2752
3027
|
}
|
|
2753
3028
|
);
|
|
2754
3029
|
if (!handledByDraft) {
|
|
2755
|
-
|
|
3030
|
+
queuedDirtyRef.current = false;
|
|
2756
3031
|
outboundInFlightRef.current = null;
|
|
2757
3032
|
setHasPendingDocumentSync(false);
|
|
2758
3033
|
}
|
|
@@ -2803,8 +3078,6 @@ function useRealtimeSession(options) {
|
|
|
2803
3078
|
const isSelfAck = parsed.document.updatedByClientId === selfClientId;
|
|
2804
3079
|
if (!isSelfAck) {
|
|
2805
3080
|
outboundInFlightRef.current = null;
|
|
2806
|
-
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2807
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2808
3081
|
resolveAuthoritativeDocumentRef.current(
|
|
2809
3082
|
sanitizeRealtimeSnapshot(parsed.document)
|
|
2810
3083
|
);
|
|
@@ -2815,24 +3088,33 @@ function useRealtimeSession(options) {
|
|
|
2815
3088
|
applyDocumentRef.current(sanitizeRealtimeSnapshot(parsed.document), {
|
|
2816
3089
|
suppressSubscriberNotify: shouldSuppress
|
|
2817
3090
|
});
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3091
|
+
const board = boardRef.current;
|
|
3092
|
+
const stillPending = board ? getLocallyPendingItemIds(board).size > 0 : false;
|
|
3093
|
+
if (stillPending) {
|
|
3094
|
+
const mergedItems = board ? readVectorItems(board.yItems) : [];
|
|
3095
|
+
setLocalDraftRef.current({
|
|
3096
|
+
roomId,
|
|
3097
|
+
baseRevision: currentRevisionRef.current,
|
|
3098
|
+
items: mergedItems,
|
|
3099
|
+
updatedAt: nowMs(),
|
|
3100
|
+
yDocState: board ? encodeYDocState(board) : void 0,
|
|
3101
|
+
pendingIds: board ? Array.from(getLocallyPendingItemIds(board)) : void 0
|
|
3102
|
+
});
|
|
3103
|
+
queuedDirtyRef.current = true;
|
|
3104
|
+
setHasPendingDocumentSync(true);
|
|
3105
|
+
scheduleDocumentFlushRef.current();
|
|
3106
|
+
} else {
|
|
3107
|
+
queuedDirtyRef.current = false;
|
|
2826
3108
|
clearLocalDraftRef.current();
|
|
3109
|
+
setHasPendingDocumentSync(false);
|
|
2827
3110
|
}
|
|
2828
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2829
3111
|
setConflictStateRef.current(null);
|
|
2830
3112
|
return;
|
|
2831
3113
|
}
|
|
2832
3114
|
if (parsed.type === "document:resync-required") {
|
|
2833
3115
|
outboundInFlightRef.current = null;
|
|
2834
|
-
|
|
2835
|
-
setHasPendingDocumentSync(
|
|
3116
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
3117
|
+
setHasPendingDocumentSync(queuedDirtyRef.current);
|
|
2836
3118
|
updateConnectionRef.current((prev) => ({
|
|
2837
3119
|
...prev,
|
|
2838
3120
|
lastError: parsed.reason
|
|
@@ -2907,14 +3189,39 @@ function useRealtimeSession(options) {
|
|
|
2907
3189
|
() => () => {
|
|
2908
3190
|
clearDocumentFlushSchedule();
|
|
2909
3191
|
clearDraftPersistSchedule();
|
|
3192
|
+
if (boardRef.current) {
|
|
3193
|
+
boardRef.current.doc.destroy();
|
|
3194
|
+
boardRef.current = null;
|
|
3195
|
+
}
|
|
2910
3196
|
},
|
|
2911
3197
|
[clearDocumentFlushSchedule, clearDraftPersistSchedule]
|
|
2912
3198
|
);
|
|
2913
3199
|
const flushDocumentSync = react.useCallback(async () => {
|
|
3200
|
+
const board = boardRef.current;
|
|
3201
|
+
const pendingLocal = pendingLocalItemsRef.current;
|
|
3202
|
+
if (board && pendingLocal) {
|
|
3203
|
+
pendingLocalItemsRef.current = null;
|
|
3204
|
+
applyLocalItemsToYDoc(board, {
|
|
3205
|
+
items: pendingLocal,
|
|
3206
|
+
origin: ORIGIN_LOCAL
|
|
3207
|
+
});
|
|
3208
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
3209
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
3210
|
+
if (pendingIds.size > 0) {
|
|
3211
|
+
setLocalDraftRef.current({
|
|
3212
|
+
roomId,
|
|
3213
|
+
baseRevision: currentRevisionRef.current,
|
|
3214
|
+
items: mergedItems,
|
|
3215
|
+
updatedAt: nowMs(),
|
|
3216
|
+
yDocState: encodeYDocState(board),
|
|
3217
|
+
pendingIds: Array.from(pendingIds)
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
2914
3221
|
persistLocalDraft();
|
|
2915
3222
|
if (!connection.connected) return;
|
|
2916
3223
|
flushQueuedDocument();
|
|
2917
|
-
}, [connection.connected, flushQueuedDocument, persistLocalDraft]);
|
|
3224
|
+
}, [connection.connected, flushQueuedDocument, persistLocalDraft, roomId]);
|
|
2918
3225
|
const remoteAdapter = react.useMemo(
|
|
2919
3226
|
() => ({
|
|
2920
3227
|
subscribe(onItems) {
|