canvu-react 0.3.40 → 0.4.0
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 +462 -129
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.js +443 -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,241 @@ 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
|
+
const snapshotIds = /* @__PURE__ */ new Set();
|
|
2120
|
+
const snapshotById = /* @__PURE__ */ new Map();
|
|
2121
|
+
for (const item of snapshotItems) {
|
|
2122
|
+
const id = getItemId(item);
|
|
2123
|
+
if (!id) continue;
|
|
2124
|
+
snapshotIds.add(id);
|
|
2125
|
+
snapshotById.set(id, item);
|
|
2126
|
+
}
|
|
2127
|
+
const currentIndex = indexYItemsById(board.yItems);
|
|
2128
|
+
const toDeleteIds = [];
|
|
2129
|
+
for (const [id] of currentIndex) {
|
|
2130
|
+
if (snapshotIds.has(id)) continue;
|
|
2131
|
+
if (board.lastServerConfirmedIds.has(id)) {
|
|
2132
|
+
toDeleteIds.push(id);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (toDeleteIds.length > 0) {
|
|
2136
|
+
const refreshed = indexYItemsById(board.yItems);
|
|
2137
|
+
const sortedIndices = toDeleteIds.map((id) => ({ id, index: refreshed.get(id)?.index })).filter(
|
|
2138
|
+
(entry) => entry.index != null
|
|
2139
|
+
).sort((a, b) => b.index - a.index);
|
|
2140
|
+
for (const { id, index } of sortedIndices) {
|
|
2141
|
+
board.yItems.delete(index, 1);
|
|
2142
|
+
board.serverItemSeenAt.delete(id);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
for (const item of snapshotItems) {
|
|
2146
|
+
const id = getItemId(item);
|
|
2147
|
+
if (!id) continue;
|
|
2148
|
+
const existing = indexYItemsById(board.yItems).get(id);
|
|
2149
|
+
if (existing) {
|
|
2150
|
+
updateYMapInPlace(existing.yMap, item);
|
|
2151
|
+
board.serverItemSeenAt.set(id, now);
|
|
2152
|
+
continue;
|
|
2153
|
+
}
|
|
2154
|
+
const yMap = vectorItemToYMap(item);
|
|
2155
|
+
board.yItems.push([yMap]);
|
|
2156
|
+
board.serverItemSeenAt.set(id, now);
|
|
2157
|
+
}
|
|
2158
|
+
}, origin);
|
|
2159
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2160
|
+
for (const item of snapshotItems) {
|
|
2161
|
+
const id = getItemId(item);
|
|
2162
|
+
if (id) board.lastServerConfirmedIds.add(id);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
function replaceYDocWithSnapshot(board, options) {
|
|
2166
|
+
const { items: snapshotItems, origin } = options;
|
|
2167
|
+
const now = Date.now();
|
|
2168
|
+
board.doc.transact(() => {
|
|
2169
|
+
if (board.yItems.length > 0) {
|
|
2170
|
+
board.yItems.delete(0, board.yItems.length);
|
|
2171
|
+
}
|
|
2172
|
+
for (const item of snapshotItems) {
|
|
2173
|
+
board.yItems.push([vectorItemToYMap(item)]);
|
|
2174
|
+
}
|
|
2175
|
+
}, origin);
|
|
2176
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2177
|
+
board.serverItemSeenAt = /* @__PURE__ */ new Map();
|
|
2178
|
+
for (const item of snapshotItems) {
|
|
2179
|
+
const id = getItemId(item);
|
|
2180
|
+
if (id) {
|
|
2181
|
+
board.lastServerConfirmedIds.add(id);
|
|
2182
|
+
board.serverItemSeenAt.set(id, now);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function getLocallyPendingItemIds(board) {
|
|
2187
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2188
|
+
for (let i = 0; i < board.yItems.length; i++) {
|
|
2189
|
+
const yMap = board.yItems.get(i);
|
|
2190
|
+
if (!yMap) continue;
|
|
2191
|
+
const id = getItemId(yMap);
|
|
2192
|
+
if (!id) continue;
|
|
2193
|
+
if (!board.lastServerConfirmedIds.has(id)) {
|
|
2194
|
+
pending.add(id);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return pending;
|
|
2198
|
+
}
|
|
2199
|
+
function encodeYDocState(board) {
|
|
2200
|
+
const update = Y.encodeStateAsUpdate(board.doc);
|
|
2201
|
+
let binary = "";
|
|
2202
|
+
for (let i = 0; i < update.length; i++) {
|
|
2203
|
+
binary += String.fromCharCode(update[i]);
|
|
2204
|
+
}
|
|
2205
|
+
return btoa(binary);
|
|
2206
|
+
}
|
|
2207
|
+
function decodeYDocState(board, encoded) {
|
|
2208
|
+
try {
|
|
2209
|
+
const binary = atob(encoded);
|
|
2210
|
+
const update = new Uint8Array(binary.length);
|
|
2211
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2212
|
+
update[i] = binary.charCodeAt(i);
|
|
2213
|
+
}
|
|
2214
|
+
Y.applyUpdate(board.doc, update);
|
|
2215
|
+
return true;
|
|
2216
|
+
} catch {
|
|
2217
|
+
return false;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
1989
2220
|
|
|
1990
2221
|
// src/react/plugins/realtime/use-realtime-session.ts
|
|
2222
|
+
var ORIGIN_LOCAL = /* @__PURE__ */ Symbol("canvu/realtime/local");
|
|
2223
|
+
var ORIGIN_REMOTE = /* @__PURE__ */ Symbol("canvu/realtime/remote");
|
|
2224
|
+
var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
|
|
1991
2225
|
var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
|
|
1992
2226
|
var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
|
|
1993
2227
|
var DRAFT_PERSIST_DEBOUNCE_MS = 420;
|
|
@@ -2074,7 +2308,16 @@ function draftStorageKey(roomId) {
|
|
|
2074
2308
|
function isRealtimeOfflineDraft(value) {
|
|
2075
2309
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2076
2310
|
const record = value;
|
|
2077
|
-
|
|
2311
|
+
if (typeof record.roomId !== "string" || typeof record.baseRevision !== "number" || typeof record.updatedAt !== "number" || !Array.isArray(record.items)) {
|
|
2312
|
+
return false;
|
|
2313
|
+
}
|
|
2314
|
+
if (record.yDocState != null && typeof record.yDocState !== "string") {
|
|
2315
|
+
return false;
|
|
2316
|
+
}
|
|
2317
|
+
if (record.pendingIds != null && !Array.isArray(record.pendingIds)) {
|
|
2318
|
+
return false;
|
|
2319
|
+
}
|
|
2320
|
+
return true;
|
|
2078
2321
|
}
|
|
2079
2322
|
function readRealtimeOfflineDraft(roomId) {
|
|
2080
2323
|
if (typeof window === "undefined" || !roomId) return null;
|
|
@@ -2112,14 +2355,6 @@ function removeRealtimeOfflineDraft(roomId) {
|
|
|
2112
2355
|
} catch {
|
|
2113
2356
|
}
|
|
2114
2357
|
}
|
|
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
2358
|
function getViewportCameraSnapshot(viewport) {
|
|
2124
2359
|
if (!viewport) return null;
|
|
2125
2360
|
const camera = viewport.getCamera();
|
|
@@ -2170,8 +2405,13 @@ function useRealtimeSession(options) {
|
|
|
2170
2405
|
const retryCountRef = useRef(0);
|
|
2171
2406
|
const currentRevisionRef = useRef(0);
|
|
2172
2407
|
const outboundInFlightRef = useRef(null);
|
|
2173
|
-
const
|
|
2408
|
+
const queuedDirtyRef = useRef(false);
|
|
2409
|
+
const pendingLocalItemsRef = useRef(null);
|
|
2174
2410
|
const subscriberRefs = useRef(/* @__PURE__ */ new Set());
|
|
2411
|
+
const boardRef = useRef(null);
|
|
2412
|
+
if (boardRef.current == null) {
|
|
2413
|
+
boardRef.current = createYjsBoardDoc();
|
|
2414
|
+
}
|
|
2175
2415
|
const lastCursorRef = useRef(null);
|
|
2176
2416
|
const lastMarkupStrokeRef = useRef(null);
|
|
2177
2417
|
const lastCameraRef = useRef(
|
|
@@ -2266,11 +2506,30 @@ function useRealtimeSession(options) {
|
|
|
2266
2506
|
if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
|
|
2267
2507
|
return;
|
|
2268
2508
|
}
|
|
2509
|
+
const board = boardRef.current;
|
|
2510
|
+
if (board) {
|
|
2511
|
+
if (options2?.replace) {
|
|
2512
|
+
replaceYDocWithSnapshot(board, {
|
|
2513
|
+
items: snapshot.items,
|
|
2514
|
+
origin: ORIGIN_REMOTE
|
|
2515
|
+
});
|
|
2516
|
+
} else {
|
|
2517
|
+
applyServerSnapshotToYDoc(board, {
|
|
2518
|
+
items: snapshot.items,
|
|
2519
|
+
origin: ORIGIN_REMOTE
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
|
|
2524
|
+
const mergedSnapshot = {
|
|
2525
|
+
...snapshot,
|
|
2526
|
+
items: mergedItems
|
|
2527
|
+
};
|
|
2269
2528
|
currentRevisionRef.current = snapshot.revision;
|
|
2270
|
-
latestDocumentRef.current =
|
|
2271
|
-
setDocument(
|
|
2529
|
+
latestDocumentRef.current = mergedSnapshot;
|
|
2530
|
+
setDocument(mergedSnapshot);
|
|
2272
2531
|
if (!options2?.suppressSubscriberNotify) {
|
|
2273
|
-
notifySubscribers(
|
|
2532
|
+
notifySubscribers(mergedItems);
|
|
2274
2533
|
}
|
|
2275
2534
|
},
|
|
2276
2535
|
[notifySubscribers]
|
|
@@ -2380,13 +2639,23 @@ function useRealtimeSession(options) {
|
|
|
2380
2639
|
);
|
|
2381
2640
|
const flushQueuedDocument = useCallback(() => {
|
|
2382
2641
|
clearDocumentFlushSchedule();
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
if (!
|
|
2642
|
+
const board = boardRef.current;
|
|
2643
|
+
if (!board) return;
|
|
2644
|
+
if (!queuedDirtyRef.current && pendingLocalItemsRef.current == null) return;
|
|
2645
|
+
if (outboundInFlightRef.current) return;
|
|
2646
|
+
const pendingLocal = pendingLocalItemsRef.current;
|
|
2647
|
+
if (pendingLocal) {
|
|
2648
|
+
pendingLocalItemsRef.current = null;
|
|
2649
|
+
applyLocalItemsToYDoc(board, {
|
|
2650
|
+
items: pendingLocal,
|
|
2651
|
+
origin: ORIGIN_LOCAL
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2386
2654
|
const baseRevision = currentRevisionRef.current;
|
|
2387
|
-
const
|
|
2655
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
2656
|
+
const preparedItems = prepareRealtimeItems(mergedItems);
|
|
2388
2657
|
if (!preparedItems) return;
|
|
2389
|
-
|
|
2658
|
+
queuedDirtyRef.current = false;
|
|
2390
2659
|
outboundInFlightRef.current = {
|
|
2391
2660
|
baseRevision,
|
|
2392
2661
|
items: preparedItems.items,
|
|
@@ -2399,8 +2668,20 @@ function useRealtimeSession(options) {
|
|
|
2399
2668
|
baseRevision,
|
|
2400
2669
|
items: preparedItems.items
|
|
2401
2670
|
});
|
|
2671
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
2672
|
+
if (pendingIds.size > 0) {
|
|
2673
|
+
setLocalDraftRef.current({
|
|
2674
|
+
roomId,
|
|
2675
|
+
baseRevision,
|
|
2676
|
+
items: mergedItems,
|
|
2677
|
+
updatedAt: nowMs(),
|
|
2678
|
+
yDocState: encodeYDocState(board),
|
|
2679
|
+
pendingIds: Array.from(pendingIds)
|
|
2680
|
+
});
|
|
2681
|
+
scheduleDraftPersistenceRef.current?.();
|
|
2682
|
+
}
|
|
2402
2683
|
if (!didSend) {
|
|
2403
|
-
|
|
2684
|
+
queuedDirtyRef.current = true;
|
|
2404
2685
|
outboundInFlightRef.current = null;
|
|
2405
2686
|
setHasPendingDocumentSync(true);
|
|
2406
2687
|
}
|
|
@@ -2415,66 +2696,103 @@ function useRealtimeSession(options) {
|
|
|
2415
2696
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2416
2697
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2417
2698
|
}, [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
|
-
);
|
|
2699
|
+
const queueDocumentSend = useCallback((items) => {
|
|
2700
|
+
pendingLocalItemsRef.current = items;
|
|
2701
|
+
queuedDirtyRef.current = true;
|
|
2702
|
+
setHasPendingDocumentSync(true);
|
|
2703
|
+
setHasLocalOfflineDraft(true);
|
|
2704
|
+
scheduleDocumentFlushRef.current();
|
|
2705
|
+
}, []);
|
|
2434
2706
|
const applyDraftSnapshot = useCallback(
|
|
2435
2707
|
(draft, options2) => {
|
|
2436
|
-
|
|
2708
|
+
const board = boardRef.current;
|
|
2709
|
+
if (board) {
|
|
2710
|
+
let restoredFromBinary = false;
|
|
2711
|
+
if (draft.yDocState) {
|
|
2712
|
+
if (board.yItems.length > 0) {
|
|
2713
|
+
board.doc.transact(() => {
|
|
2714
|
+
board.yItems.delete(0, board.yItems.length);
|
|
2715
|
+
}, ORIGIN_BOOTSTRAP);
|
|
2716
|
+
}
|
|
2717
|
+
restoredFromBinary = decodeYDocState(board, draft.yDocState);
|
|
2718
|
+
if (restoredFromBinary) {
|
|
2719
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
2720
|
+
for (let i = 0; i < board.yItems.length; i++) {
|
|
2721
|
+
const yMap = board.yItems.get(i);
|
|
2722
|
+
if (!yMap) continue;
|
|
2723
|
+
const id = yMap.get("id");
|
|
2724
|
+
if (typeof id === "string") allIds.add(id);
|
|
2725
|
+
}
|
|
2726
|
+
const pendingIds = new Set(draft.pendingIds ?? []);
|
|
2727
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2728
|
+
for (const id of allIds) {
|
|
2729
|
+
if (!pendingIds.has(id)) {
|
|
2730
|
+
board.lastServerConfirmedIds.add(id);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
if (!restoredFromBinary) {
|
|
2736
|
+
replaceYDocWithSnapshot(board, {
|
|
2737
|
+
items: draft.items,
|
|
2738
|
+
origin: ORIGIN_BOOTSTRAP
|
|
2739
|
+
});
|
|
2740
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
const items = board ? readVectorItems(board.yItems) : draft.items;
|
|
2744
|
+
const snapshot = {
|
|
2745
|
+
revision: draft.baseRevision,
|
|
2746
|
+
items,
|
|
2747
|
+
updatedAt: draft.updatedAt,
|
|
2748
|
+
updatedByClientId: clientIdRef.current
|
|
2749
|
+
};
|
|
2750
|
+
currentRevisionRef.current = snapshot.revision;
|
|
2751
|
+
latestDocumentRef.current = snapshot;
|
|
2752
|
+
setDocument(snapshot);
|
|
2753
|
+
if (!options2?.suppressSubscriberNotify) {
|
|
2754
|
+
notifySubscribers(items);
|
|
2755
|
+
}
|
|
2437
2756
|
},
|
|
2438
|
-
[
|
|
2757
|
+
[notifySubscribers]
|
|
2439
2758
|
);
|
|
2440
2759
|
const resolveAuthoritativeDocument = useCallback(
|
|
2441
2760
|
(serverDocument, options2) => {
|
|
2442
|
-
const
|
|
2443
|
-
|
|
2444
|
-
|
|
2761
|
+
const board = boardRef.current;
|
|
2762
|
+
const hadLocalContent = localDraftRef.current != null || board != null && board.yItems.length > 0;
|
|
2763
|
+
applyDocument(serverDocument, {
|
|
2764
|
+
...options2,
|
|
2765
|
+
replace: !hadLocalContent
|
|
2766
|
+
});
|
|
2767
|
+
if (!board) {
|
|
2445
2768
|
setHasPendingDocumentSync(false);
|
|
2446
|
-
|
|
2769
|
+
clearLocalDraftRef.current();
|
|
2447
2770
|
return false;
|
|
2448
2771
|
}
|
|
2449
|
-
|
|
2772
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
2773
|
+
if (pendingIds.size === 0) {
|
|
2450
2774
|
clearLocalDraftRef.current();
|
|
2451
2775
|
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
2776
|
outboundInFlightRef.current = null;
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
scheduleDocumentFlush();
|
|
2465
|
-
return true;
|
|
2777
|
+
queuedDirtyRef.current = false;
|
|
2778
|
+
return false;
|
|
2466
2779
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2780
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
2781
|
+
setLocalDraft({
|
|
2782
|
+
roomId,
|
|
2783
|
+
baseRevision: serverDocument.revision,
|
|
2784
|
+
items: mergedItems,
|
|
2785
|
+
updatedAt: nowMs(),
|
|
2786
|
+
yDocState: encodeYDocState(board),
|
|
2787
|
+
pendingIds: Array.from(pendingIds)
|
|
2474
2788
|
});
|
|
2789
|
+
outboundInFlightRef.current = null;
|
|
2790
|
+
queuedDirtyRef.current = true;
|
|
2791
|
+
setHasPendingDocumentSync(true);
|
|
2792
|
+
scheduleDocumentFlush();
|
|
2475
2793
|
return true;
|
|
2476
2794
|
},
|
|
2477
|
-
[applyDocument,
|
|
2795
|
+
[applyDocument, roomId, scheduleDocumentFlush, setLocalDraft]
|
|
2478
2796
|
);
|
|
2479
2797
|
const sendPresenceUpdate = useCallback(() => {
|
|
2480
2798
|
sendRaw({
|
|
@@ -2513,54 +2831,8 @@ function useRealtimeSession(options) {
|
|
|
2513
2831
|
reconnect,
|
|
2514
2832
|
updateConnection
|
|
2515
2833
|
]);
|
|
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
|
-
);
|
|
2834
|
+
const resolveConflict = useCallback((_action) => {
|
|
2835
|
+
}, []);
|
|
2564
2836
|
const setConflictStateRef = useRef(setConflictState);
|
|
2565
2837
|
setConflictStateRef.current = setConflictState;
|
|
2566
2838
|
const updateConnectionRef = useRef(updateConnection);
|
|
@@ -2581,7 +2853,15 @@ function useRealtimeSession(options) {
|
|
|
2581
2853
|
scheduleReconnectRef.current = scheduleReconnect;
|
|
2582
2854
|
const sendRawRef = useRef(sendRaw);
|
|
2583
2855
|
sendRawRef.current = sendRaw;
|
|
2856
|
+
const setLocalDraftRef = useRef(setLocalDraft);
|
|
2857
|
+
setLocalDraftRef.current = setLocalDraft;
|
|
2858
|
+
const scheduleDraftPersistenceRef = useRef(scheduleDraftPersistence);
|
|
2859
|
+
scheduleDraftPersistenceRef.current = scheduleDraftPersistence;
|
|
2584
2860
|
useEffect(() => {
|
|
2861
|
+
if (boardRef.current) {
|
|
2862
|
+
boardRef.current.doc.destroy();
|
|
2863
|
+
}
|
|
2864
|
+
boardRef.current = createYjsBoardDoc();
|
|
2585
2865
|
if (!roomId) {
|
|
2586
2866
|
clearDocumentFlushSchedule();
|
|
2587
2867
|
clearDraftPersistSchedule();
|
|
@@ -2589,7 +2869,8 @@ function useRealtimeSession(options) {
|
|
|
2589
2869
|
setHasLocalOfflineDraft(false);
|
|
2590
2870
|
setHasPendingDocumentSync(false);
|
|
2591
2871
|
setConflictState(null);
|
|
2592
|
-
|
|
2872
|
+
queuedDirtyRef.current = false;
|
|
2873
|
+
pendingLocalItemsRef.current = null;
|
|
2593
2874
|
outboundInFlightRef.current = null;
|
|
2594
2875
|
latestDocumentRef.current = null;
|
|
2595
2876
|
setDocument(null);
|
|
@@ -2600,7 +2881,8 @@ function useRealtimeSession(options) {
|
|
|
2600
2881
|
setLocalDraft(localDraft);
|
|
2601
2882
|
setHasPendingDocumentSync(localDraft != null);
|
|
2602
2883
|
setConflictState(null);
|
|
2603
|
-
|
|
2884
|
+
queuedDirtyRef.current = localDraft != null;
|
|
2885
|
+
pendingLocalItemsRef.current = null;
|
|
2604
2886
|
outboundInFlightRef.current = null;
|
|
2605
2887
|
if (localDraft) {
|
|
2606
2888
|
applyDraftSnapshot(localDraft, {
|
|
@@ -2631,7 +2913,7 @@ function useRealtimeSession(options) {
|
|
|
2631
2913
|
clearDocumentFlushSchedule();
|
|
2632
2914
|
wsRef.current?.close();
|
|
2633
2915
|
wsRef.current = null;
|
|
2634
|
-
|
|
2916
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
2635
2917
|
outboundInFlightRef.current = null;
|
|
2636
2918
|
setHasPendingDocumentSync(localDraftRef.current != null);
|
|
2637
2919
|
collapsePeersToSelfRef.current("offline");
|
|
@@ -2746,7 +3028,7 @@ function useRealtimeSession(options) {
|
|
|
2746
3028
|
}
|
|
2747
3029
|
);
|
|
2748
3030
|
if (!handledByDraft) {
|
|
2749
|
-
|
|
3031
|
+
queuedDirtyRef.current = false;
|
|
2750
3032
|
outboundInFlightRef.current = null;
|
|
2751
3033
|
setHasPendingDocumentSync(false);
|
|
2752
3034
|
}
|
|
@@ -2797,8 +3079,6 @@ function useRealtimeSession(options) {
|
|
|
2797
3079
|
const isSelfAck = parsed.document.updatedByClientId === selfClientId;
|
|
2798
3080
|
if (!isSelfAck) {
|
|
2799
3081
|
outboundInFlightRef.current = null;
|
|
2800
|
-
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2801
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2802
3082
|
resolveAuthoritativeDocumentRef.current(
|
|
2803
3083
|
sanitizeRealtimeSnapshot(parsed.document)
|
|
2804
3084
|
);
|
|
@@ -2809,24 +3089,33 @@ function useRealtimeSession(options) {
|
|
|
2809
3089
|
applyDocumentRef.current(sanitizeRealtimeSnapshot(parsed.document), {
|
|
2810
3090
|
suppressSubscriberNotify: shouldSuppress
|
|
2811
3091
|
});
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3092
|
+
const board = boardRef.current;
|
|
3093
|
+
const stillPending = board ? getLocallyPendingItemIds(board).size > 0 : false;
|
|
3094
|
+
if (stillPending) {
|
|
3095
|
+
const mergedItems = board ? readVectorItems(board.yItems) : [];
|
|
3096
|
+
setLocalDraftRef.current({
|
|
3097
|
+
roomId,
|
|
3098
|
+
baseRevision: currentRevisionRef.current,
|
|
3099
|
+
items: mergedItems,
|
|
3100
|
+
updatedAt: nowMs(),
|
|
3101
|
+
yDocState: board ? encodeYDocState(board) : void 0,
|
|
3102
|
+
pendingIds: board ? Array.from(getLocallyPendingItemIds(board)) : void 0
|
|
3103
|
+
});
|
|
3104
|
+
queuedDirtyRef.current = true;
|
|
3105
|
+
setHasPendingDocumentSync(true);
|
|
3106
|
+
scheduleDocumentFlushRef.current();
|
|
3107
|
+
} else {
|
|
3108
|
+
queuedDirtyRef.current = false;
|
|
2820
3109
|
clearLocalDraftRef.current();
|
|
3110
|
+
setHasPendingDocumentSync(false);
|
|
2821
3111
|
}
|
|
2822
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2823
3112
|
setConflictStateRef.current(null);
|
|
2824
3113
|
return;
|
|
2825
3114
|
}
|
|
2826
3115
|
if (parsed.type === "document:resync-required") {
|
|
2827
3116
|
outboundInFlightRef.current = null;
|
|
2828
|
-
|
|
2829
|
-
setHasPendingDocumentSync(
|
|
3117
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
3118
|
+
setHasPendingDocumentSync(queuedDirtyRef.current);
|
|
2830
3119
|
updateConnectionRef.current((prev) => ({
|
|
2831
3120
|
...prev,
|
|
2832
3121
|
lastError: parsed.reason
|
|
@@ -2901,14 +3190,39 @@ function useRealtimeSession(options) {
|
|
|
2901
3190
|
() => () => {
|
|
2902
3191
|
clearDocumentFlushSchedule();
|
|
2903
3192
|
clearDraftPersistSchedule();
|
|
3193
|
+
if (boardRef.current) {
|
|
3194
|
+
boardRef.current.doc.destroy();
|
|
3195
|
+
boardRef.current = null;
|
|
3196
|
+
}
|
|
2904
3197
|
},
|
|
2905
3198
|
[clearDocumentFlushSchedule, clearDraftPersistSchedule]
|
|
2906
3199
|
);
|
|
2907
3200
|
const flushDocumentSync = useCallback(async () => {
|
|
3201
|
+
const board = boardRef.current;
|
|
3202
|
+
const pendingLocal = pendingLocalItemsRef.current;
|
|
3203
|
+
if (board && pendingLocal) {
|
|
3204
|
+
pendingLocalItemsRef.current = null;
|
|
3205
|
+
applyLocalItemsToYDoc(board, {
|
|
3206
|
+
items: pendingLocal,
|
|
3207
|
+
origin: ORIGIN_LOCAL
|
|
3208
|
+
});
|
|
3209
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
3210
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
3211
|
+
if (pendingIds.size > 0) {
|
|
3212
|
+
setLocalDraftRef.current({
|
|
3213
|
+
roomId,
|
|
3214
|
+
baseRevision: currentRevisionRef.current,
|
|
3215
|
+
items: mergedItems,
|
|
3216
|
+
updatedAt: nowMs(),
|
|
3217
|
+
yDocState: encodeYDocState(board),
|
|
3218
|
+
pendingIds: Array.from(pendingIds)
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
2908
3222
|
persistLocalDraft();
|
|
2909
3223
|
if (!connection.connected) return;
|
|
2910
3224
|
flushQueuedDocument();
|
|
2911
|
-
}, [connection.connected, flushQueuedDocument, persistLocalDraft]);
|
|
3225
|
+
}, [connection.connected, flushQueuedDocument, persistLocalDraft, roomId]);
|
|
2912
3226
|
const remoteAdapter = useMemo(
|
|
2913
3227
|
() => ({
|
|
2914
3228
|
subscribe(onItems) {
|