canvu-react 0.3.39 → 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/{asset-hydration-DowNdaOJ.d.cts → asset-hydration-B7yMDQE-.d.cts} +2 -2
- package/dist/{asset-hydration-DdFLdlqX.d.ts → asset-hydration-CbwQVAwh.d.ts} +2 -2
- package/dist/{camera-Di5R_Rwl.d.cts → camera-CVVG7z56.d.cts} +1 -1
- package/dist/{camera-AoTwBSoE.d.ts → camera-CoRYN_IV.d.ts} +1 -1
- package/dist/chatbot.d.cts +4 -4
- package/dist/chatbot.d.ts +4 -4
- package/dist/index.cjs +59 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +59 -15
- package/dist/index.js.map +1 -1
- package/dist/native.cjs +57 -14
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +2 -2
- package/dist/native.d.ts +2 -2
- package/dist/native.js +57 -14
- package/dist/native.js.map +1 -1
- package/dist/react.cjs +699 -255
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +10 -10
- package/dist/react.d.ts +10 -10
- package/dist/react.js +699 -255
- package/dist/react.js.map +1 -1
- package/dist/realtime.cjs +498 -129
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.d.cts +6 -6
- package/dist/realtime.d.ts +6 -6
- package/dist/realtime.js +479 -129
- package/dist/realtime.js.map +1 -1
- package/dist/{shape-builders-Dedcl6tw.d.cts → shape-builders-BAWu-PxX.d.cts} +7 -3
- package/dist/{shape-builders-C7bxJBGR.d.ts → shape-builders-ClKv9tz9.d.ts} +7 -3
- package/dist/tldraw.cjs +56 -14
- package/dist/tldraw.cjs.map +1 -1
- package/dist/tldraw.d.cts +1 -1
- package/dist/tldraw.d.ts +1 -1
- package/dist/tldraw.js +56 -14
- package/dist/tldraw.js.map +1 -1
- package/dist/{types-DUW61Tjy.d.cts → types-BC9Xgfu6.d.cts} +11 -6
- package/dist/{types-Bnq2HtHQ.d.cts → types-BCCvY6ie.d.cts} +2 -0
- package/dist/{types-Bnq2HtHQ.d.ts → types-BCCvY6ie.d.ts} +2 -0
- package/dist/{types-B2Na677H.d.cts → types-BUPc2Zgw.d.cts} +1 -1
- package/dist/{types-zmUah-vP.d.ts → types-CYtq9Pr9.d.ts} +1 -1
- package/dist/{types-BBb8KoyW.d.ts → types-DlSVGX0w.d.ts} +11 -6
- 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) {
|
|
@@ -149,6 +150,30 @@ function perfectFreehandOptions(toolKind, style, strokeComplete, pressureAware =
|
|
|
149
150
|
simulatePressure: true
|
|
150
151
|
};
|
|
151
152
|
}
|
|
153
|
+
function dashArrayForDrawStroke(strokeWidth) {
|
|
154
|
+
const dash = Math.max(strokeWidth * 1.8, 4);
|
|
155
|
+
const gap = Math.max(strokeWidth * 1.4, 3);
|
|
156
|
+
return `${dash} ${gap}`;
|
|
157
|
+
}
|
|
158
|
+
function buildSmoothedCenterlinePath(points) {
|
|
159
|
+
if (points.length < 2) return null;
|
|
160
|
+
const first = points[0];
|
|
161
|
+
if (!first) return null;
|
|
162
|
+
let d = `M ${first.x} ${first.y}`;
|
|
163
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
164
|
+
const a = points[i];
|
|
165
|
+
const b = points[i + 1];
|
|
166
|
+
if (!a || !b) continue;
|
|
167
|
+
const midX = (a.x + b.x) / 2;
|
|
168
|
+
const midY = (a.y + b.y) / 2;
|
|
169
|
+
d += ` Q ${a.x} ${a.y} ${midX} ${midY}`;
|
|
170
|
+
}
|
|
171
|
+
const last = points[points.length - 1];
|
|
172
|
+
if (last) {
|
|
173
|
+
d += ` L ${last.x} ${last.y}`;
|
|
174
|
+
}
|
|
175
|
+
return d;
|
|
176
|
+
}
|
|
152
177
|
function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeComplete = true) {
|
|
153
178
|
if (pathPointsLocal.length === 0) return null;
|
|
154
179
|
if (pathPointsLocal.length === 1) {
|
|
@@ -163,6 +188,18 @@ function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeCompl
|
|
|
163
188
|
fillOpacity: style.strokeOpacity
|
|
164
189
|
};
|
|
165
190
|
}
|
|
191
|
+
if (style.strokeDash === "dashed" && (toolKind === "draw" || toolKind === "pencil")) {
|
|
192
|
+
const d2 = buildSmoothedCenterlinePath(pathPointsLocal);
|
|
193
|
+
if (!d2) return null;
|
|
194
|
+
return {
|
|
195
|
+
kind: "strokePath",
|
|
196
|
+
d: d2,
|
|
197
|
+
stroke: style.stroke,
|
|
198
|
+
strokeWidth: style.strokeWidth,
|
|
199
|
+
strokeOpacity: style.strokeOpacity,
|
|
200
|
+
strokeDasharray: dashArrayForDrawStroke(style.strokeWidth)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
166
203
|
const hasPressure = pathPointsLocal.some(
|
|
167
204
|
(p) => p.pressure != null && Number.isFinite(p.pressure)
|
|
168
205
|
);
|
|
@@ -1950,8 +1987,241 @@ function useRealtimePeerFollow(options) {
|
|
|
1950
1987
|
lastAppliedCameraKeyRef.current = nextCameraKey;
|
|
1951
1988
|
}, [followedPeerId, onFollowEnd, sessionPeers, viewportRef]);
|
|
1952
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
|
+
}
|
|
1953
2220
|
|
|
1954
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");
|
|
1955
2225
|
var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
|
|
1956
2226
|
var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
|
|
1957
2227
|
var DRAFT_PERSIST_DEBOUNCE_MS = 420;
|
|
@@ -2038,7 +2308,16 @@ function draftStorageKey(roomId) {
|
|
|
2038
2308
|
function isRealtimeOfflineDraft(value) {
|
|
2039
2309
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2040
2310
|
const record = value;
|
|
2041
|
-
|
|
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;
|
|
2042
2321
|
}
|
|
2043
2322
|
function readRealtimeOfflineDraft(roomId) {
|
|
2044
2323
|
if (typeof window === "undefined" || !roomId) return null;
|
|
@@ -2076,14 +2355,6 @@ function removeRealtimeOfflineDraft(roomId) {
|
|
|
2076
2355
|
} catch {
|
|
2077
2356
|
}
|
|
2078
2357
|
}
|
|
2079
|
-
function buildDraftSnapshot(draft, clientId) {
|
|
2080
|
-
return {
|
|
2081
|
-
revision: draft.baseRevision,
|
|
2082
|
-
items: draft.items,
|
|
2083
|
-
updatedAt: draft.updatedAt,
|
|
2084
|
-
updatedByClientId: clientId
|
|
2085
|
-
};
|
|
2086
|
-
}
|
|
2087
2358
|
function getViewportCameraSnapshot(viewport) {
|
|
2088
2359
|
if (!viewport) return null;
|
|
2089
2360
|
const camera = viewport.getCamera();
|
|
@@ -2134,8 +2405,13 @@ function useRealtimeSession(options) {
|
|
|
2134
2405
|
const retryCountRef = useRef(0);
|
|
2135
2406
|
const currentRevisionRef = useRef(0);
|
|
2136
2407
|
const outboundInFlightRef = useRef(null);
|
|
2137
|
-
const
|
|
2408
|
+
const queuedDirtyRef = useRef(false);
|
|
2409
|
+
const pendingLocalItemsRef = useRef(null);
|
|
2138
2410
|
const subscriberRefs = useRef(/* @__PURE__ */ new Set());
|
|
2411
|
+
const boardRef = useRef(null);
|
|
2412
|
+
if (boardRef.current == null) {
|
|
2413
|
+
boardRef.current = createYjsBoardDoc();
|
|
2414
|
+
}
|
|
2139
2415
|
const lastCursorRef = useRef(null);
|
|
2140
2416
|
const lastMarkupStrokeRef = useRef(null);
|
|
2141
2417
|
const lastCameraRef = useRef(
|
|
@@ -2230,11 +2506,30 @@ function useRealtimeSession(options) {
|
|
|
2230
2506
|
if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
|
|
2231
2507
|
return;
|
|
2232
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
|
+
};
|
|
2233
2528
|
currentRevisionRef.current = snapshot.revision;
|
|
2234
|
-
latestDocumentRef.current =
|
|
2235
|
-
setDocument(
|
|
2529
|
+
latestDocumentRef.current = mergedSnapshot;
|
|
2530
|
+
setDocument(mergedSnapshot);
|
|
2236
2531
|
if (!options2?.suppressSubscriberNotify) {
|
|
2237
|
-
notifySubscribers(
|
|
2532
|
+
notifySubscribers(mergedItems);
|
|
2238
2533
|
}
|
|
2239
2534
|
},
|
|
2240
2535
|
[notifySubscribers]
|
|
@@ -2344,13 +2639,23 @@ function useRealtimeSession(options) {
|
|
|
2344
2639
|
);
|
|
2345
2640
|
const flushQueuedDocument = useCallback(() => {
|
|
2346
2641
|
clearDocumentFlushSchedule();
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
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
|
+
}
|
|
2350
2654
|
const baseRevision = currentRevisionRef.current;
|
|
2351
|
-
const
|
|
2655
|
+
const mergedItems = readVectorItems(board.yItems);
|
|
2656
|
+
const preparedItems = prepareRealtimeItems(mergedItems);
|
|
2352
2657
|
if (!preparedItems) return;
|
|
2353
|
-
|
|
2658
|
+
queuedDirtyRef.current = false;
|
|
2354
2659
|
outboundInFlightRef.current = {
|
|
2355
2660
|
baseRevision,
|
|
2356
2661
|
items: preparedItems.items,
|
|
@@ -2363,8 +2668,20 @@ function useRealtimeSession(options) {
|
|
|
2363
2668
|
baseRevision,
|
|
2364
2669
|
items: preparedItems.items
|
|
2365
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
|
+
}
|
|
2366
2683
|
if (!didSend) {
|
|
2367
|
-
|
|
2684
|
+
queuedDirtyRef.current = true;
|
|
2368
2685
|
outboundInFlightRef.current = null;
|
|
2369
2686
|
setHasPendingDocumentSync(true);
|
|
2370
2687
|
}
|
|
@@ -2379,66 +2696,103 @@ function useRealtimeSession(options) {
|
|
|
2379
2696
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2380
2697
|
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2381
2698
|
}, [clearDocumentFlushSchedule, flushQueuedDocument]);
|
|
2382
|
-
const queueDocumentSend = useCallback(
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
});
|
|
2390
|
-
scheduleDraftPersistence();
|
|
2391
|
-
queuedItemsRef.current = items;
|
|
2392
|
-
setHasPendingDocumentSync(true);
|
|
2393
|
-
if (conflictRef.current) return;
|
|
2394
|
-
scheduleDocumentFlushRef.current();
|
|
2395
|
-
},
|
|
2396
|
-
[roomId, scheduleDraftPersistence, setLocalDraft]
|
|
2397
|
-
);
|
|
2699
|
+
const queueDocumentSend = useCallback((items) => {
|
|
2700
|
+
pendingLocalItemsRef.current = items;
|
|
2701
|
+
queuedDirtyRef.current = true;
|
|
2702
|
+
setHasPendingDocumentSync(true);
|
|
2703
|
+
setHasLocalOfflineDraft(true);
|
|
2704
|
+
scheduleDocumentFlushRef.current();
|
|
2705
|
+
}, []);
|
|
2398
2706
|
const applyDraftSnapshot = useCallback(
|
|
2399
2707
|
(draft, options2) => {
|
|
2400
|
-
|
|
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
|
+
}
|
|
2401
2756
|
},
|
|
2402
|
-
[
|
|
2757
|
+
[notifySubscribers]
|
|
2403
2758
|
);
|
|
2404
2759
|
const resolveAuthoritativeDocument = useCallback(
|
|
2405
2760
|
(serverDocument, options2) => {
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
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) {
|
|
2409
2768
|
setHasPendingDocumentSync(false);
|
|
2410
|
-
|
|
2769
|
+
clearLocalDraftRef.current();
|
|
2411
2770
|
return false;
|
|
2412
2771
|
}
|
|
2413
|
-
|
|
2772
|
+
const pendingIds = getLocallyPendingItemIds(board);
|
|
2773
|
+
if (pendingIds.size === 0) {
|
|
2414
2774
|
clearLocalDraftRef.current();
|
|
2415
2775
|
setHasPendingDocumentSync(false);
|
|
2416
|
-
setConflictState(null);
|
|
2417
|
-
applyDocument(serverDocument, options2);
|
|
2418
|
-
return true;
|
|
2419
|
-
}
|
|
2420
|
-
if (serverDocument.revision === localDraft.baseRevision) {
|
|
2421
|
-
setConflictState(null);
|
|
2422
|
-
applyDraftSnapshot(localDraft, {
|
|
2423
|
-
suppressSubscriberNotify: options2?.suppressSubscriberNotify ?? sameSerializedItems(latestDocumentRef.current?.items, localDraft.items)
|
|
2424
|
-
});
|
|
2425
2776
|
outboundInFlightRef.current = null;
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
scheduleDocumentFlush();
|
|
2429
|
-
return true;
|
|
2777
|
+
queuedDirtyRef.current = false;
|
|
2778
|
+
return false;
|
|
2430
2779
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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)
|
|
2438
2788
|
});
|
|
2789
|
+
outboundInFlightRef.current = null;
|
|
2790
|
+
queuedDirtyRef.current = true;
|
|
2791
|
+
setHasPendingDocumentSync(true);
|
|
2792
|
+
scheduleDocumentFlush();
|
|
2439
2793
|
return true;
|
|
2440
2794
|
},
|
|
2441
|
-
[applyDocument,
|
|
2795
|
+
[applyDocument, roomId, scheduleDocumentFlush, setLocalDraft]
|
|
2442
2796
|
);
|
|
2443
2797
|
const sendPresenceUpdate = useCallback(() => {
|
|
2444
2798
|
sendRaw({
|
|
@@ -2477,54 +2831,8 @@ function useRealtimeSession(options) {
|
|
|
2477
2831
|
reconnect,
|
|
2478
2832
|
updateConnection
|
|
2479
2833
|
]);
|
|
2480
|
-
const resolveConflict = useCallback(
|
|
2481
|
-
|
|
2482
|
-
const activeConflict = conflictRef.current;
|
|
2483
|
-
if (!activeConflict) return;
|
|
2484
|
-
if (action === "use-server") {
|
|
2485
|
-
clearLocalDraft();
|
|
2486
|
-
queuedItemsRef.current = null;
|
|
2487
|
-
outboundInFlightRef.current = null;
|
|
2488
|
-
setConflictState(null);
|
|
2489
|
-
applyDocument({
|
|
2490
|
-
revision: activeConflict.serverRevision,
|
|
2491
|
-
items: activeConflict.serverItems,
|
|
2492
|
-
updatedAt: nowMs()
|
|
2493
|
-
});
|
|
2494
|
-
return;
|
|
2495
|
-
}
|
|
2496
|
-
const nextDraft = {
|
|
2497
|
-
roomId,
|
|
2498
|
-
baseRevision: activeConflict.serverRevision,
|
|
2499
|
-
items: activeConflict.localItems,
|
|
2500
|
-
updatedAt: nowMs()
|
|
2501
|
-
};
|
|
2502
|
-
setLocalDraft(nextDraft);
|
|
2503
|
-
scheduleDraftPersistence();
|
|
2504
|
-
setConflictState(null);
|
|
2505
|
-
applyDraftSnapshot(nextDraft, {
|
|
2506
|
-
suppressSubscriberNotify: sameSerializedItems(
|
|
2507
|
-
latestDocumentRef.current?.items,
|
|
2508
|
-
nextDraft.items
|
|
2509
|
-
)
|
|
2510
|
-
});
|
|
2511
|
-
currentRevisionRef.current = activeConflict.serverRevision;
|
|
2512
|
-
queuedItemsRef.current = nextDraft.items;
|
|
2513
|
-
outboundInFlightRef.current = null;
|
|
2514
|
-
setHasPendingDocumentSync(true);
|
|
2515
|
-
scheduleDocumentFlush();
|
|
2516
|
-
},
|
|
2517
|
-
[
|
|
2518
|
-
applyDocument,
|
|
2519
|
-
applyDraftSnapshot,
|
|
2520
|
-
clearLocalDraft,
|
|
2521
|
-
roomId,
|
|
2522
|
-
scheduleDocumentFlush,
|
|
2523
|
-
scheduleDraftPersistence,
|
|
2524
|
-
setConflictState,
|
|
2525
|
-
setLocalDraft
|
|
2526
|
-
]
|
|
2527
|
-
);
|
|
2834
|
+
const resolveConflict = useCallback((_action) => {
|
|
2835
|
+
}, []);
|
|
2528
2836
|
const setConflictStateRef = useRef(setConflictState);
|
|
2529
2837
|
setConflictStateRef.current = setConflictState;
|
|
2530
2838
|
const updateConnectionRef = useRef(updateConnection);
|
|
@@ -2545,7 +2853,15 @@ function useRealtimeSession(options) {
|
|
|
2545
2853
|
scheduleReconnectRef.current = scheduleReconnect;
|
|
2546
2854
|
const sendRawRef = useRef(sendRaw);
|
|
2547
2855
|
sendRawRef.current = sendRaw;
|
|
2856
|
+
const setLocalDraftRef = useRef(setLocalDraft);
|
|
2857
|
+
setLocalDraftRef.current = setLocalDraft;
|
|
2858
|
+
const scheduleDraftPersistenceRef = useRef(scheduleDraftPersistence);
|
|
2859
|
+
scheduleDraftPersistenceRef.current = scheduleDraftPersistence;
|
|
2548
2860
|
useEffect(() => {
|
|
2861
|
+
if (boardRef.current) {
|
|
2862
|
+
boardRef.current.doc.destroy();
|
|
2863
|
+
}
|
|
2864
|
+
boardRef.current = createYjsBoardDoc();
|
|
2549
2865
|
if (!roomId) {
|
|
2550
2866
|
clearDocumentFlushSchedule();
|
|
2551
2867
|
clearDraftPersistSchedule();
|
|
@@ -2553,7 +2869,8 @@ function useRealtimeSession(options) {
|
|
|
2553
2869
|
setHasLocalOfflineDraft(false);
|
|
2554
2870
|
setHasPendingDocumentSync(false);
|
|
2555
2871
|
setConflictState(null);
|
|
2556
|
-
|
|
2872
|
+
queuedDirtyRef.current = false;
|
|
2873
|
+
pendingLocalItemsRef.current = null;
|
|
2557
2874
|
outboundInFlightRef.current = null;
|
|
2558
2875
|
latestDocumentRef.current = null;
|
|
2559
2876
|
setDocument(null);
|
|
@@ -2564,7 +2881,8 @@ function useRealtimeSession(options) {
|
|
|
2564
2881
|
setLocalDraft(localDraft);
|
|
2565
2882
|
setHasPendingDocumentSync(localDraft != null);
|
|
2566
2883
|
setConflictState(null);
|
|
2567
|
-
|
|
2884
|
+
queuedDirtyRef.current = localDraft != null;
|
|
2885
|
+
pendingLocalItemsRef.current = null;
|
|
2568
2886
|
outboundInFlightRef.current = null;
|
|
2569
2887
|
if (localDraft) {
|
|
2570
2888
|
applyDraftSnapshot(localDraft, {
|
|
@@ -2595,7 +2913,7 @@ function useRealtimeSession(options) {
|
|
|
2595
2913
|
clearDocumentFlushSchedule();
|
|
2596
2914
|
wsRef.current?.close();
|
|
2597
2915
|
wsRef.current = null;
|
|
2598
|
-
|
|
2916
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
2599
2917
|
outboundInFlightRef.current = null;
|
|
2600
2918
|
setHasPendingDocumentSync(localDraftRef.current != null);
|
|
2601
2919
|
collapsePeersToSelfRef.current("offline");
|
|
@@ -2710,7 +3028,7 @@ function useRealtimeSession(options) {
|
|
|
2710
3028
|
}
|
|
2711
3029
|
);
|
|
2712
3030
|
if (!handledByDraft) {
|
|
2713
|
-
|
|
3031
|
+
queuedDirtyRef.current = false;
|
|
2714
3032
|
outboundInFlightRef.current = null;
|
|
2715
3033
|
setHasPendingDocumentSync(false);
|
|
2716
3034
|
}
|
|
@@ -2761,8 +3079,6 @@ function useRealtimeSession(options) {
|
|
|
2761
3079
|
const isSelfAck = parsed.document.updatedByClientId === selfClientId;
|
|
2762
3080
|
if (!isSelfAck) {
|
|
2763
3081
|
outboundInFlightRef.current = null;
|
|
2764
|
-
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2765
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2766
3082
|
resolveAuthoritativeDocumentRef.current(
|
|
2767
3083
|
sanitizeRealtimeSnapshot(parsed.document)
|
|
2768
3084
|
);
|
|
@@ -2773,24 +3089,33 @@ function useRealtimeSession(options) {
|
|
|
2773
3089
|
applyDocumentRef.current(sanitizeRealtimeSnapshot(parsed.document), {
|
|
2774
3090
|
suppressSubscriberNotify: shouldSuppress
|
|
2775
3091
|
});
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
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;
|
|
2784
3109
|
clearLocalDraftRef.current();
|
|
3110
|
+
setHasPendingDocumentSync(false);
|
|
2785
3111
|
}
|
|
2786
|
-
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2787
3112
|
setConflictStateRef.current(null);
|
|
2788
3113
|
return;
|
|
2789
3114
|
}
|
|
2790
3115
|
if (parsed.type === "document:resync-required") {
|
|
2791
3116
|
outboundInFlightRef.current = null;
|
|
2792
|
-
|
|
2793
|
-
setHasPendingDocumentSync(
|
|
3117
|
+
queuedDirtyRef.current = localDraftRef.current != null;
|
|
3118
|
+
setHasPendingDocumentSync(queuedDirtyRef.current);
|
|
2794
3119
|
updateConnectionRef.current((prev) => ({
|
|
2795
3120
|
...prev,
|
|
2796
3121
|
lastError: parsed.reason
|
|
@@ -2865,14 +3190,39 @@ function useRealtimeSession(options) {
|
|
|
2865
3190
|
() => () => {
|
|
2866
3191
|
clearDocumentFlushSchedule();
|
|
2867
3192
|
clearDraftPersistSchedule();
|
|
3193
|
+
if (boardRef.current) {
|
|
3194
|
+
boardRef.current.doc.destroy();
|
|
3195
|
+
boardRef.current = null;
|
|
3196
|
+
}
|
|
2868
3197
|
},
|
|
2869
3198
|
[clearDocumentFlushSchedule, clearDraftPersistSchedule]
|
|
2870
3199
|
);
|
|
2871
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
|
+
}
|
|
2872
3222
|
persistLocalDraft();
|
|
2873
3223
|
if (!connection.connected) return;
|
|
2874
3224
|
flushQueuedDocument();
|
|
2875
|
-
}, [connection.connected, flushQueuedDocument, persistLocalDraft]);
|
|
3225
|
+
}, [connection.connected, flushQueuedDocument, persistLocalDraft, roomId]);
|
|
2876
3226
|
const remoteAdapter = useMemo(
|
|
2877
3227
|
() => ({
|
|
2878
3228
|
subscribe(onItems) {
|