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.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,241 @@ 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
|
+
const snapshotIds = /* @__PURE__ */ new Set();
|
|
2145
|
+
const snapshotById = /* @__PURE__ */ new Map();
|
|
2146
|
+
for (const item of snapshotItems) {
|
|
2147
|
+
const id = getItemId(item);
|
|
2148
|
+
if (!id) continue;
|
|
2149
|
+
snapshotIds.add(id);
|
|
2150
|
+
snapshotById.set(id, item);
|
|
2151
|
+
}
|
|
2152
|
+
const currentIndex = indexYItemsById(board.yItems);
|
|
2153
|
+
const toDeleteIds = [];
|
|
2154
|
+
for (const [id] of currentIndex) {
|
|
2155
|
+
if (snapshotIds.has(id)) continue;
|
|
2156
|
+
if (board.lastServerConfirmedIds.has(id)) {
|
|
2157
|
+
toDeleteIds.push(id);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (toDeleteIds.length > 0) {
|
|
2161
|
+
const refreshed = indexYItemsById(board.yItems);
|
|
2162
|
+
const sortedIndices = toDeleteIds.map((id) => ({ id, index: refreshed.get(id)?.index })).filter(
|
|
2163
|
+
(entry) => entry.index != null
|
|
2164
|
+
).sort((a, b) => b.index - a.index);
|
|
2165
|
+
for (const { id, index } of sortedIndices) {
|
|
2166
|
+
board.yItems.delete(index, 1);
|
|
2167
|
+
board.serverItemSeenAt.delete(id);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
for (const item of snapshotItems) {
|
|
2171
|
+
const id = getItemId(item);
|
|
2172
|
+
if (!id) continue;
|
|
2173
|
+
const existing = indexYItemsById(board.yItems).get(id);
|
|
2174
|
+
if (existing) {
|
|
2175
|
+
updateYMapInPlace(existing.yMap, item);
|
|
2176
|
+
board.serverItemSeenAt.set(id, now);
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
const yMap = vectorItemToYMap(item);
|
|
2180
|
+
board.yItems.push([yMap]);
|
|
2181
|
+
board.serverItemSeenAt.set(id, now);
|
|
2182
|
+
}
|
|
2183
|
+
}, origin);
|
|
2184
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2185
|
+
for (const item of snapshotItems) {
|
|
2186
|
+
const id = getItemId(item);
|
|
2187
|
+
if (id) board.lastServerConfirmedIds.add(id);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
function replaceYDocWithSnapshot(board, options) {
|
|
2191
|
+
const { items: snapshotItems, origin } = options;
|
|
2192
|
+
const now = Date.now();
|
|
2193
|
+
board.doc.transact(() => {
|
|
2194
|
+
if (board.yItems.length > 0) {
|
|
2195
|
+
board.yItems.delete(0, board.yItems.length);
|
|
2196
|
+
}
|
|
2197
|
+
for (const item of snapshotItems) {
|
|
2198
|
+
board.yItems.push([vectorItemToYMap(item)]);
|
|
2199
|
+
}
|
|
2200
|
+
}, origin);
|
|
2201
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2202
|
+
board.serverItemSeenAt = /* @__PURE__ */ new Map();
|
|
2203
|
+
for (const item of snapshotItems) {
|
|
2204
|
+
const id = getItemId(item);
|
|
2205
|
+
if (id) {
|
|
2206
|
+
board.lastServerConfirmedIds.add(id);
|
|
2207
|
+
board.serverItemSeenAt.set(id, now);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
function getLocallyPendingItemIds(board) {
|
|
2212
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2213
|
+
for (let i = 0; i < board.yItems.length; i++) {
|
|
2214
|
+
const yMap = board.yItems.get(i);
|
|
2215
|
+
if (!yMap) continue;
|
|
2216
|
+
const id = getItemId(yMap);
|
|
2217
|
+
if (!id) continue;
|
|
2218
|
+
if (!board.lastServerConfirmedIds.has(id)) {
|
|
2219
|
+
pending.add(id);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return pending;
|
|
2223
|
+
}
|
|
2224
|
+
function encodeYDocState(board) {
|
|
2225
|
+
const update = Y__namespace.encodeStateAsUpdate(board.doc);
|
|
2226
|
+
let binary = "";
|
|
2227
|
+
for (let i = 0; i < update.length; i++) {
|
|
2228
|
+
binary += String.fromCharCode(update[i]);
|
|
2229
|
+
}
|
|
2230
|
+
return btoa(binary);
|
|
2231
|
+
}
|
|
2232
|
+
function decodeYDocState(board, encoded) {
|
|
2233
|
+
try {
|
|
2234
|
+
const binary = atob(encoded);
|
|
2235
|
+
const update = new Uint8Array(binary.length);
|
|
2236
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2237
|
+
update[i] = binary.charCodeAt(i);
|
|
2238
|
+
}
|
|
2239
|
+
Y__namespace.applyUpdate(board.doc, update);
|
|
2240
|
+
return true;
|
|
2241
|
+
} catch {
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
1995
2245
|
|
|
1996
2246
|
// src/react/plugins/realtime/use-realtime-session.ts
|
|
2247
|
+
var ORIGIN_LOCAL = /* @__PURE__ */ Symbol("canvu/realtime/local");
|
|
2248
|
+
var ORIGIN_REMOTE = /* @__PURE__ */ Symbol("canvu/realtime/remote");
|
|
2249
|
+
var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
|
|
1997
2250
|
var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
|
|
1998
2251
|
var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
|
|
1999
2252
|
var DRAFT_PERSIST_DEBOUNCE_MS = 420;
|
|
@@ -2080,7 +2333,16 @@ function draftStorageKey(roomId) {
|
|
|
2080
2333
|
function isRealtimeOfflineDraft(value) {
|
|
2081
2334
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2082
2335
|
const record = value;
|
|
2083
|
-
|
|
2336
|
+
if (typeof record.roomId !== "string" || typeof record.baseRevision !== "number" || typeof record.updatedAt !== "number" || !Array.isArray(record.items)) {
|
|
2337
|
+
return false;
|
|
2338
|
+
}
|
|
2339
|
+
if (record.yDocState != null && typeof record.yDocState !== "string") {
|
|
2340
|
+
return false;
|
|
2341
|
+
}
|
|
2342
|
+
if (record.pendingIds != null && !Array.isArray(record.pendingIds)) {
|
|
2343
|
+
return false;
|
|
2344
|
+
}
|
|
2345
|
+
return true;
|
|
2084
2346
|
}
|
|
2085
2347
|
function readRealtimeOfflineDraft(roomId) {
|
|
2086
2348
|
if (typeof window === "undefined" || !roomId) return null;
|
|
@@ -2118,14 +2380,6 @@ function removeRealtimeOfflineDraft(roomId) {
|
|
|
2118
2380
|
} catch {
|
|
2119
2381
|
}
|
|
2120
2382
|
}
|
|
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
2383
|
function getViewportCameraSnapshot(viewport) {
|
|
2130
2384
|
if (!viewport) return null;
|
|
2131
2385
|
const camera = viewport.getCamera();
|
|
@@ -2176,8 +2430,13 @@ function useRealtimeSession(options) {
|
|
|
2176
2430
|
const retryCountRef = react.useRef(0);
|
|
2177
2431
|
const currentRevisionRef = react.useRef(0);
|
|
2178
2432
|
const outboundInFlightRef = react.useRef(null);
|
|
2179
|
-
const
|
|
2433
|
+
const queuedDirtyRef = react.useRef(false);
|
|
2434
|
+
const pendingLocalItemsRef = react.useRef(null);
|
|
2180
2435
|
const subscriberRefs = react.useRef(/* @__PURE__ */ new Set());
|
|
2436
|
+
const boardRef = react.useRef(null);
|
|
2437
|
+
if (boardRef.current == null) {
|
|
2438
|
+
boardRef.current = createYjsBoardDoc();
|
|
2439
|
+
}
|
|
2181
2440
|
const lastCursorRef = react.useRef(null);
|
|
2182
2441
|
const lastMarkupStrokeRef = react.useRef(null);
|
|
2183
2442
|
const lastCameraRef = react.useRef(
|
|
@@ -2272,11 +2531,30 @@ function useRealtimeSession(options) {
|
|
|
2272
2531
|
if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
|
|
2273
2532
|
return;
|
|
2274
2533
|
}
|
|
2534
|
+
const board = boardRef.current;
|
|
2535
|
+
if (board) {
|
|
2536
|
+
if (options2?.replace) {
|
|
2537
|
+
replaceYDocWithSnapshot(board, {
|
|
2538
|
+
items: snapshot.items,
|
|
2539
|
+
origin: ORIGIN_REMOTE
|
|
2540
|
+
});
|
|
2541
|
+
} else {
|
|
2542
|
+
applyServerSnapshotToYDoc(board, {
|
|
2543
|
+
items: snapshot.items,
|
|
2544
|
+
origin: ORIGIN_REMOTE
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
|
|
2549
|
+
const mergedSnapshot = {
|
|
2550
|
+
...snapshot,
|
|
2551
|
+
items: mergedItems
|
|
2552
|
+
};
|
|
2275
2553
|
currentRevisionRef.current = snapshot.revision;
|
|
2276
|
-
latestDocumentRef.current =
|
|
2277
|
-
setDocument(
|
|
2554
|
+
latestDocumentRef.current = mergedSnapshot;
|
|
2555
|
+
setDocument(mergedSnapshot);
|
|
2278
2556
|
if (!options2?.suppressSubscriberNotify) {
|
|
2279
|
-
notifySubscribers(
|
|
2557
|
+
notifySubscribers(mergedItems);
|
|
2280
2558
|
}
|
|
2281
2559
|
},
|
|
2282
2560
|
[notifySubscribers]
|
|
@@ -2386,13 +2664,23 @@ function useRealtimeSession(options) {
|
|
|
2386
2664
|
);
|
|
2387
2665
|
const flushQueuedDocument = react.useCallback(() => {
|
|
2388
2666
|
clearDocumentFlushSchedule();
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
if (!
|
|
2667
|
+
const board = boardRef.current;
|
|
2668
|
+
if (!board) return;
|
|
2669
|
+
if (!queuedDirtyRef.current && pendingLocalItemsRef.current == null) return;
|
|
2670
|
+
if (outboundInFlightRef.current) return;
|
|
2671
|
+
const pendingLocal = pendingLocalItemsRef.current;
|
|
2672
|
+
if (pendingLocal) {
|
|
2673
|
+
pendingLocalItemsRef.current = null;
|
|
2674
|
+
applyLocalItemsToYDoc(board, {
|
|
2675
|
+
items: pendingLocal,
|
|
2676
|
+
origin: ORIGIN_LOCAL
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2392
2679
|
const baseRevision = currentRevisionRef.current;
|
|
2393
|
-
const
|
|
2680
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
2681
|
+
const preparedItems = prepareRealtimeItems(mergedItems);
|
|
2394
2682
|
if (!preparedItems) return;
|
|
2395
|
-
|
|
2683
|
+
queuedDirtyRef.current = false;
|
|
2396
2684
|
outboundInFlightRef.current = {
|
|
2397
2685
|
baseRevision,
|
|
2398
2686
|
items: preparedItems.items,
|
|
@@ -2405,8 +2693,20 @@ function useRealtimeSession(options) {
|
|
|
2405
2693
|
baseRevision,
|
|
2406
2694
|
items: preparedItems.items
|
|
2407
2695
|
});
|
|
2696
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
2697
|
+
if (pendingIds.size > 0) {
|
|
2698
|
+
setLocalDraftRef.current({
|
|
2699
|
+
roomId,
|
|
2700
|
+
baseRevision,
|
|
2701
|
+
items: mergedItems,
|
|
2702
|
+
updatedAt: nowMs(),
|
|
2703
|
+
yDocState: encodeYDocState(board),
|
|
2704
|
+
pendingIds: Array.from(pendingIds)
|
|
2705
|
+
});
|
|
2706
|
+
scheduleDraftPersistenceRef.current?.();
|
|
2707
|
+
}
|
|
2408
2708
|
if (!didSend) {
|
|
2409
|
-
|
|
2709
|
+
queuedDirtyRef.current = true;
|
|
2410
2710
|
outboundInFlightRef.current = null;
|
|
2411
2711
|
setHasPendingDocumentSync(true);
|
|
2412
2712
|
}
|
|
@@ -2421,66 +2721,103 @@ function useRealtimeSession(options) {
|
|
|
2421
2721
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2422
2722
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2423
2723
|
}, [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
|
-
);
|
|
2724
|
+
const queueDocumentSend = react.useCallback((items) => {
|
|
2725
|
+
pendingLocalItemsRef.current = items;
|
|
2726
|
+
queuedDirtyRef.current = true;
|
|
2727
|
+
setHasPendingDocumentSync(true);
|
|
2728
|
+
setHasLocalOfflineDraft(true);
|
|
2729
|
+
scheduleDocumentFlushRef.current();
|
|
2730
|
+
}, []);
|
|
2440
2731
|
const applyDraftSnapshot = react.useCallback(
|
|
2441
2732
|
(draft, options2) => {
|
|
2442
|
-
|
|
2733
|
+
const board = boardRef.current;
|
|
2734
|
+
if (board) {
|
|
2735
|
+
let restoredFromBinary = false;
|
|
2736
|
+
if (draft.yDocState) {
|
|
2737
|
+
if (board.yItems.length > 0) {
|
|
2738
|
+
board.doc.transact(() => {
|
|
2739
|
+
board.yItems.delete(0, board.yItems.length);
|
|
2740
|
+
}, ORIGIN_BOOTSTRAP);
|
|
2741
|
+
}
|
|
2742
|
+
restoredFromBinary = decodeYDocState(board, draft.yDocState);
|
|
2743
|
+
if (restoredFromBinary) {
|
|
2744
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
2745
|
+
for (let i = 0; i < board.yItems.length; i++) {
|
|
2746
|
+
const yMap = board.yItems.get(i);
|
|
2747
|
+
if (!yMap) continue;
|
|
2748
|
+
const id = yMap.get("id");
|
|
2749
|
+
if (typeof id === "string") allIds.add(id);
|
|
2750
|
+
}
|
|
2751
|
+
const pendingIds = new Set(draft.pendingIds ?? []);
|
|
2752
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2753
|
+
for (const id of allIds) {
|
|
2754
|
+
if (!pendingIds.has(id)) {
|
|
2755
|
+
board.lastServerConfirmedIds.add(id);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
if (!restoredFromBinary) {
|
|
2761
|
+
replaceYDocWithSnapshot(board, {
|
|
2762
|
+
items: draft.items,
|
|
2763
|
+
origin: ORIGIN_BOOTSTRAP
|
|
2764
|
+
});
|
|
2765
|
+
board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
const items = board ? readVectorItems(board.yItems) : draft.items;
|
|
2769
|
+
const snapshot = {
|
|
2770
|
+
revision: draft.baseRevision,
|
|
2771
|
+
items,
|
|
2772
|
+
updatedAt: draft.updatedAt,
|
|
2773
|
+
updatedByClientId: clientIdRef.current
|
|
2774
|
+
};
|
|
2775
|
+
currentRevisionRef.current = snapshot.revision;
|
|
2776
|
+
latestDocumentRef.current = snapshot;
|
|
2777
|
+
setDocument(snapshot);
|
|
2778
|
+
if (!options2?.suppressSubscriberNotify) {
|
|
2779
|
+
notifySubscribers(items);
|
|
2780
|
+
}
|
|
2443
2781
|
},
|
|
2444
|
-
[
|
|
2782
|
+
[notifySubscribers]
|
|
2445
2783
|
);
|
|
2446
2784
|
const resolveAuthoritativeDocument = react.useCallback(
|
|
2447
2785
|
(serverDocument, options2) => {
|
|
2448
|
-
const
|
|
2449
|
-
|
|
2450
|
-
|
|
2786
|
+
const board = boardRef.current;
|
|
2787
|
+
const hadLocalContent = localDraftRef.current != null || board != null && board.yItems.length > 0;
|
|
2788
|
+
applyDocument(serverDocument, {
|
|
2789
|
+
...options2,
|
|
2790
|
+
replace: !hadLocalContent
|
|
2791
|
+
});
|
|
2792
|
+
if (!board) {
|
|
2451
2793
|
setHasPendingDocumentSync(false);
|
|
2452
|
-
|
|
2794
|
+
clearLocalDraftRef.current();
|
|
2453
2795
|
return false;
|
|
2454
2796
|
}
|
|
2455
|
-
|
|
2797
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
2798
|
+
if (pendingIds.size === 0) {
|
|
2456
2799
|
clearLocalDraftRef.current();
|
|
2457
2800
|
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
2801
|
outboundInFlightRef.current = null;
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
scheduleDocumentFlush();
|
|
2471
|
-
return true;
|
|
2802
|
+
queuedDirtyRef.current = false;
|
|
2803
|
+
return false;
|
|
2472
2804
|
}
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2805
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
2806
|
+
setLocalDraft({
|
|
2807
|
+
roomId,
|
|
2808
|
+
baseRevision: serverDocument.revision,
|
|
2809
|
+
items: mergedItems,
|
|
2810
|
+
updatedAt: nowMs(),
|
|
2811
|
+
yDocState: encodeYDocState(board),
|
|
2812
|
+
pendingIds: Array.from(pendingIds)
|
|
2480
2813
|
});
|
|
2814
|
+
outboundInFlightRef.current = null;
|
|
2815
|
+
queuedDirtyRef.current = true;
|
|
2816
|
+
setHasPendingDocumentSync(true);
|
|
2817
|
+
scheduleDocumentFlush();
|
|
2481
2818
|
return true;
|
|
2482
2819
|
},
|
|
2483
|
-
[applyDocument,
|
|
2820
|
+
[applyDocument, roomId, scheduleDocumentFlush, setLocalDraft]
|
|
2484
2821
|
);
|
|
2485
2822
|
const sendPresenceUpdate = react.useCallback(() => {
|
|
2486
2823
|
sendRaw({
|
|
@@ -2519,54 +2856,8 @@ function useRealtimeSession(options) {
|
|
|
2519
2856
|
reconnect,
|
|
2520
2857
|
updateConnection
|
|
2521
2858
|
]);
|
|
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
|
-
);
|
|
2859
|
+
const resolveConflict = react.useCallback((_action) => {
|
|
2860
|
+
}, []);
|
|
2570
2861
|
const setConflictStateRef = react.useRef(setConflictState);
|
|
2571
2862
|
setConflictStateRef.current = setConflictState;
|
|
2572
2863
|
const updateConnectionRef = react.useRef(updateConnection);
|
|
@@ -2587,7 +2878,15 @@ function useRealtimeSession(options) {
|
|
|
2587
2878
|
scheduleReconnectRef.current = scheduleReconnect;
|
|
2588
2879
|
const sendRawRef = react.useRef(sendRaw);
|
|
2589
2880
|
sendRawRef.current = sendRaw;
|
|
2881
|
+
const setLocalDraftRef = react.useRef(setLocalDraft);
|
|
2882
|
+
setLocalDraftRef.current = setLocalDraft;
|
|
2883
|
+
const scheduleDraftPersistenceRef = react.useRef(scheduleDraftPersistence);
|
|
2884
|
+
scheduleDraftPersistenceRef.current = scheduleDraftPersistence;
|
|
2590
2885
|
react.useEffect(() => {
|
|
2886
|
+
if (boardRef.current) {
|
|
2887
|
+
boardRef.current.doc.destroy();
|
|
2888
|
+
}
|
|
2889
|
+
boardRef.current = createYjsBoardDoc();
|
|
2591
2890
|
if (!roomId) {
|
|
2592
2891
|
clearDocumentFlushSchedule();
|
|
2593
2892
|
clearDraftPersistSchedule();
|
|
@@ -2595,7 +2894,8 @@ function useRealtimeSession(options) {
|
|
|
2595
2894
|
setHasLocalOfflineDraft(false);
|
|
2596
2895
|
setHasPendingDocumentSync(false);
|
|
2597
2896
|
setConflictState(null);
|
|
2598
|
-
|
|
2897
|
+
queuedDirtyRef.current = false;
|
|
2898
|
+
pendingLocalItemsRef.current = null;
|
|
2599
2899
|
outboundInFlightRef.current = null;
|
|
2600
2900
|
latestDocumentRef.current = null;
|
|
2601
2901
|
setDocument(null);
|
|
@@ -2606,7 +2906,8 @@ function useRealtimeSession(options) {
|
|
|
2606
2906
|
setLocalDraft(localDraft);
|
|
2607
2907
|
setHasPendingDocumentSync(localDraft != null);
|
|
2608
2908
|
setConflictState(null);
|
|
2609
|
-
|
|
2909
|
+
queuedDirtyRef.current = localDraft != null;
|
|
2910
|
+
pendingLocalItemsRef.current = null;
|
|
2610
2911
|
outboundInFlightRef.current = null;
|
|
2611
2912
|
if (localDraft) {
|
|
2612
2913
|
applyDraftSnapshot(localDraft, {
|
|
@@ -2637,7 +2938,7 @@ function useRealtimeSession(options) {
|
|
|
2637
2938
|
clearDocumentFlushSchedule();
|
|
2638
2939
|
wsRef.current?.close();
|
|
2639
2940
|
wsRef.current = null;
|
|
2640
|
-
|
|
2941
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
2641
2942
|
outboundInFlightRef.current = null;
|
|
2642
2943
|
setHasPendingDocumentSync(localDraftRef.current != null);
|
|
2643
2944
|
collapsePeersToSelfRef.current("offline");
|
|
@@ -2752,7 +3053,7 @@ function useRealtimeSession(options) {
|
|
|
2752
3053
|
}
|
|
2753
3054
|
);
|
|
2754
3055
|
if (!handledByDraft) {
|
|
2755
|
-
|
|
3056
|
+
queuedDirtyRef.current = false;
|
|
2756
3057
|
outboundInFlightRef.current = null;
|
|
2757
3058
|
setHasPendingDocumentSync(false);
|
|
2758
3059
|
}
|
|
@@ -2803,8 +3104,6 @@ function useRealtimeSession(options) {
|
|
|
2803
3104
|
const isSelfAck = parsed.document.updatedByClientId === selfClientId;
|
|
2804
3105
|
if (!isSelfAck) {
|
|
2805
3106
|
outboundInFlightRef.current = null;
|
|
2806
|
-
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2807
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2808
3107
|
resolveAuthoritativeDocumentRef.current(
|
|
2809
3108
|
sanitizeRealtimeSnapshot(parsed.document)
|
|
2810
3109
|
);
|
|
@@ -2815,24 +3114,33 @@ function useRealtimeSession(options) {
|
|
|
2815
3114
|
applyDocumentRef.current(sanitizeRealtimeSnapshot(parsed.document), {
|
|
2816
3115
|
suppressSubscriberNotify: shouldSuppress
|
|
2817
3116
|
});
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3117
|
+
const board = boardRef.current;
|
|
3118
|
+
const stillPending = board ? getLocallyPendingItemIds(board).size > 0 : false;
|
|
3119
|
+
if (stillPending) {
|
|
3120
|
+
const mergedItems = board ? readVectorItems(board.yItems) : [];
|
|
3121
|
+
setLocalDraftRef.current({
|
|
3122
|
+
roomId,
|
|
3123
|
+
baseRevision: currentRevisionRef.current,
|
|
3124
|
+
items: mergedItems,
|
|
3125
|
+
updatedAt: nowMs(),
|
|
3126
|
+
yDocState: board ? encodeYDocState(board) : void 0,
|
|
3127
|
+
pendingIds: board ? Array.from(getLocallyPendingItemIds(board)) : void 0
|
|
3128
|
+
});
|
|
3129
|
+
queuedDirtyRef.current = true;
|
|
3130
|
+
setHasPendingDocumentSync(true);
|
|
3131
|
+
scheduleDocumentFlushRef.current();
|
|
3132
|
+
} else {
|
|
3133
|
+
queuedDirtyRef.current = false;
|
|
2826
3134
|
clearLocalDraftRef.current();
|
|
3135
|
+
setHasPendingDocumentSync(false);
|
|
2827
3136
|
}
|
|
2828
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2829
3137
|
setConflictStateRef.current(null);
|
|
2830
3138
|
return;
|
|
2831
3139
|
}
|
|
2832
3140
|
if (parsed.type === "document:resync-required") {
|
|
2833
3141
|
outboundInFlightRef.current = null;
|
|
2834
|
-
|
|
2835
|
-
setHasPendingDocumentSync(
|
|
3142
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
3143
|
+
setHasPendingDocumentSync(queuedDirtyRef.current);
|
|
2836
3144
|
updateConnectionRef.current((prev) => ({
|
|
2837
3145
|
...prev,
|
|
2838
3146
|
lastError: parsed.reason
|
|
@@ -2907,14 +3215,39 @@ function useRealtimeSession(options) {
|
|
|
2907
3215
|
() => () => {
|
|
2908
3216
|
clearDocumentFlushSchedule();
|
|
2909
3217
|
clearDraftPersistSchedule();
|
|
3218
|
+
if (boardRef.current) {
|
|
3219
|
+
boardRef.current.doc.destroy();
|
|
3220
|
+
boardRef.current = null;
|
|
3221
|
+
}
|
|
2910
3222
|
},
|
|
2911
3223
|
[clearDocumentFlushSchedule, clearDraftPersistSchedule]
|
|
2912
3224
|
);
|
|
2913
3225
|
const flushDocumentSync = react.useCallback(async () => {
|
|
3226
|
+
const board = boardRef.current;
|
|
3227
|
+
const pendingLocal = pendingLocalItemsRef.current;
|
|
3228
|
+
if (board && pendingLocal) {
|
|
3229
|
+
pendingLocalItemsRef.current = null;
|
|
3230
|
+
applyLocalItemsToYDoc(board, {
|
|
3231
|
+
items: pendingLocal,
|
|
3232
|
+
origin: ORIGIN_LOCAL
|
|
3233
|
+
});
|
|
3234
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
3235
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
3236
|
+
if (pendingIds.size > 0) {
|
|
3237
|
+
setLocalDraftRef.current({
|
|
3238
|
+
roomId,
|
|
3239
|
+
baseRevision: currentRevisionRef.current,
|
|
3240
|
+
items: mergedItems,
|
|
3241
|
+
updatedAt: nowMs(),
|
|
3242
|
+
yDocState: encodeYDocState(board),
|
|
3243
|
+
pendingIds: Array.from(pendingIds)
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
2914
3247
|
persistLocalDraft();
|
|
2915
3248
|
if (!connection.connected) return;
|
|
2916
3249
|
flushQueuedDocument();
|
|
2917
|
-
}, [connection.connected, flushQueuedDocument, persistLocalDraft]);
|
|
3250
|
+
}, [connection.connected, flushQueuedDocument, persistLocalDraft, roomId]);
|
|
2918
3251
|
const remoteAdapter = react.useMemo(
|
|
2919
3252
|
() => ({
|
|
2920
3253
|
subscribe(onItems) {
|