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.
Files changed (45) hide show
  1. package/dist/{asset-hydration-DowNdaOJ.d.cts → asset-hydration-B7yMDQE-.d.cts} +2 -2
  2. package/dist/{asset-hydration-DdFLdlqX.d.ts → asset-hydration-CbwQVAwh.d.ts} +2 -2
  3. package/dist/{camera-Di5R_Rwl.d.cts → camera-CVVG7z56.d.cts} +1 -1
  4. package/dist/{camera-AoTwBSoE.d.ts → camera-CoRYN_IV.d.ts} +1 -1
  5. package/dist/chatbot.d.cts +4 -4
  6. package/dist/chatbot.d.ts +4 -4
  7. package/dist/index.cjs +59 -15
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +6 -6
  10. package/dist/index.d.ts +6 -6
  11. package/dist/index.js +59 -15
  12. package/dist/index.js.map +1 -1
  13. package/dist/native.cjs +57 -14
  14. package/dist/native.cjs.map +1 -1
  15. package/dist/native.d.cts +2 -2
  16. package/dist/native.d.ts +2 -2
  17. package/dist/native.js +57 -14
  18. package/dist/native.js.map +1 -1
  19. package/dist/react.cjs +699 -255
  20. package/dist/react.cjs.map +1 -1
  21. package/dist/react.d.cts +10 -10
  22. package/dist/react.d.ts +10 -10
  23. package/dist/react.js +699 -255
  24. package/dist/react.js.map +1 -1
  25. package/dist/realtime.cjs +498 -129
  26. package/dist/realtime.cjs.map +1 -1
  27. package/dist/realtime.d.cts +6 -6
  28. package/dist/realtime.d.ts +6 -6
  29. package/dist/realtime.js +479 -129
  30. package/dist/realtime.js.map +1 -1
  31. package/dist/{shape-builders-Dedcl6tw.d.cts → shape-builders-BAWu-PxX.d.cts} +7 -3
  32. package/dist/{shape-builders-C7bxJBGR.d.ts → shape-builders-ClKv9tz9.d.ts} +7 -3
  33. package/dist/tldraw.cjs +56 -14
  34. package/dist/tldraw.cjs.map +1 -1
  35. package/dist/tldraw.d.cts +1 -1
  36. package/dist/tldraw.d.ts +1 -1
  37. package/dist/tldraw.js +56 -14
  38. package/dist/tldraw.js.map +1 -1
  39. package/dist/{types-DUW61Tjy.d.cts → types-BC9Xgfu6.d.cts} +11 -6
  40. package/dist/{types-Bnq2HtHQ.d.cts → types-BCCvY6ie.d.cts} +2 -0
  41. package/dist/{types-Bnq2HtHQ.d.ts → types-BCCvY6ie.d.ts} +2 -0
  42. package/dist/{types-B2Na677H.d.cts → types-BUPc2Zgw.d.cts} +1 -1
  43. package/dist/{types-zmUah-vP.d.ts → types-CYtq9Pr9.d.ts} +1 -1
  44. package/dist/{types-BBb8KoyW.d.ts → types-DlSVGX0w.d.ts} +11 -6
  45. 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) {
@@ -155,6 +175,30 @@ function perfectFreehandOptions(toolKind, style, strokeComplete, pressureAware =
155
175
  simulatePressure: true
156
176
  };
157
177
  }
178
+ function dashArrayForDrawStroke(strokeWidth) {
179
+ const dash = Math.max(strokeWidth * 1.8, 4);
180
+ const gap = Math.max(strokeWidth * 1.4, 3);
181
+ return `${dash} ${gap}`;
182
+ }
183
+ function buildSmoothedCenterlinePath(points) {
184
+ if (points.length < 2) return null;
185
+ const first = points[0];
186
+ if (!first) return null;
187
+ let d = `M ${first.x} ${first.y}`;
188
+ for (let i = 1; i < points.length - 1; i++) {
189
+ const a = points[i];
190
+ const b = points[i + 1];
191
+ if (!a || !b) continue;
192
+ const midX = (a.x + b.x) / 2;
193
+ const midY = (a.y + b.y) / 2;
194
+ d += ` Q ${a.x} ${a.y} ${midX} ${midY}`;
195
+ }
196
+ const last = points[points.length - 1];
197
+ if (last) {
198
+ d += ` L ${last.x} ${last.y}`;
199
+ }
200
+ return d;
201
+ }
158
202
  function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeComplete = true) {
159
203
  if (pathPointsLocal.length === 0) return null;
160
204
  if (pathPointsLocal.length === 1) {
@@ -169,6 +213,18 @@ function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeCompl
169
213
  fillOpacity: style.strokeOpacity
170
214
  };
171
215
  }
216
+ if (style.strokeDash === "dashed" && (toolKind === "draw" || toolKind === "pencil")) {
217
+ const d2 = buildSmoothedCenterlinePath(pathPointsLocal);
218
+ if (!d2) return null;
219
+ return {
220
+ kind: "strokePath",
221
+ d: d2,
222
+ stroke: style.stroke,
223
+ strokeWidth: style.strokeWidth,
224
+ strokeOpacity: style.strokeOpacity,
225
+ strokeDasharray: dashArrayForDrawStroke(style.strokeWidth)
226
+ };
227
+ }
172
228
  const hasPressure = pathPointsLocal.some(
173
229
  (p) => p.pressure != null && Number.isFinite(p.pressure)
174
230
  );
@@ -1956,8 +2012,241 @@ function useRealtimePeerFollow(options) {
1956
2012
  lastAppliedCameraKeyRef.current = nextCameraKey;
1957
2013
  }, [followedPeerId, onFollowEnd, sessionPeers, viewportRef]);
1958
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
+ }
1959
2245
 
1960
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");
1961
2250
  var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
1962
2251
  var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
1963
2252
  var DRAFT_PERSIST_DEBOUNCE_MS = 420;
@@ -2044,7 +2333,16 @@ function draftStorageKey(roomId) {
2044
2333
  function isRealtimeOfflineDraft(value) {
2045
2334
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2046
2335
  const record = value;
2047
- return typeof record.roomId === "string" && typeof record.baseRevision === "number" && typeof record.updatedAt === "number" && Array.isArray(record.items);
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;
2048
2346
  }
2049
2347
  function readRealtimeOfflineDraft(roomId) {
2050
2348
  if (typeof window === "undefined" || !roomId) return null;
@@ -2082,14 +2380,6 @@ function removeRealtimeOfflineDraft(roomId) {
2082
2380
  } catch {
2083
2381
  }
2084
2382
  }
2085
- function buildDraftSnapshot(draft, clientId) {
2086
- return {
2087
- revision: draft.baseRevision,
2088
- items: draft.items,
2089
- updatedAt: draft.updatedAt,
2090
- updatedByClientId: clientId
2091
- };
2092
- }
2093
2383
  function getViewportCameraSnapshot(viewport) {
2094
2384
  if (!viewport) return null;
2095
2385
  const camera = viewport.getCamera();
@@ -2140,8 +2430,13 @@ function useRealtimeSession(options) {
2140
2430
  const retryCountRef = react.useRef(0);
2141
2431
  const currentRevisionRef = react.useRef(0);
2142
2432
  const outboundInFlightRef = react.useRef(null);
2143
- const queuedItemsRef = react.useRef(null);
2433
+ const queuedDirtyRef = react.useRef(false);
2434
+ const pendingLocalItemsRef = react.useRef(null);
2144
2435
  const subscriberRefs = react.useRef(/* @__PURE__ */ new Set());
2436
+ const boardRef = react.useRef(null);
2437
+ if (boardRef.current == null) {
2438
+ boardRef.current = createYjsBoardDoc();
2439
+ }
2145
2440
  const lastCursorRef = react.useRef(null);
2146
2441
  const lastMarkupStrokeRef = react.useRef(null);
2147
2442
  const lastCameraRef = react.useRef(
@@ -2236,11 +2531,30 @@ function useRealtimeSession(options) {
2236
2531
  if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
2237
2532
  return;
2238
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
+ };
2239
2553
  currentRevisionRef.current = snapshot.revision;
2240
- latestDocumentRef.current = snapshot;
2241
- setDocument(snapshot);
2554
+ latestDocumentRef.current = mergedSnapshot;
2555
+ setDocument(mergedSnapshot);
2242
2556
  if (!options2?.suppressSubscriberNotify) {
2243
- notifySubscribers(snapshot.items);
2557
+ notifySubscribers(mergedItems);
2244
2558
  }
2245
2559
  },
2246
2560
  [notifySubscribers]
@@ -2350,13 +2664,23 @@ function useRealtimeSession(options) {
2350
2664
  );
2351
2665
  const flushQueuedDocument = react.useCallback(() => {
2352
2666
  clearDocumentFlushSchedule();
2353
- if (conflictRef.current) return;
2354
- const next = queuedItemsRef.current;
2355
- if (!next || outboundInFlightRef.current) return;
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
+ }
2356
2679
  const baseRevision = currentRevisionRef.current;
2357
- const preparedItems = prepareRealtimeItems(next);
2680
+ const mergedItems = readVectorItems(board.yItems);
2681
+ const preparedItems = prepareRealtimeItems(mergedItems);
2358
2682
  if (!preparedItems) return;
2359
- queuedItemsRef.current = null;
2683
+ queuedDirtyRef.current = false;
2360
2684
  outboundInFlightRef.current = {
2361
2685
  baseRevision,
2362
2686
  items: preparedItems.items,
@@ -2369,8 +2693,20 @@ function useRealtimeSession(options) {
2369
2693
  baseRevision,
2370
2694
  items: preparedItems.items
2371
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
+ }
2372
2708
  if (!didSend) {
2373
- queuedItemsRef.current = next;
2709
+ queuedDirtyRef.current = true;
2374
2710
  outboundInFlightRef.current = null;
2375
2711
  setHasPendingDocumentSync(true);
2376
2712
  }
@@ -2385,66 +2721,103 @@ function useRealtimeSession(options) {
2385
2721
  }, DOCUMENT_FLUSH_DEBOUNCE_MS);
2386
2722
  }, DOCUMENT_FLUSH_DEBOUNCE_MS);
2387
2723
  }, [clearDocumentFlushSchedule, flushQueuedDocument]);
2388
- const queueDocumentSend = react.useCallback(
2389
- (items) => {
2390
- setLocalDraft({
2391
- roomId,
2392
- baseRevision: currentRevisionRef.current,
2393
- items,
2394
- updatedAt: nowMs()
2395
- });
2396
- scheduleDraftPersistence();
2397
- queuedItemsRef.current = items;
2398
- setHasPendingDocumentSync(true);
2399
- if (conflictRef.current) return;
2400
- scheduleDocumentFlushRef.current();
2401
- },
2402
- [roomId, scheduleDraftPersistence, setLocalDraft]
2403
- );
2724
+ const queueDocumentSend = react.useCallback((items) => {
2725
+ pendingLocalItemsRef.current = items;
2726
+ queuedDirtyRef.current = true;
2727
+ setHasPendingDocumentSync(true);
2728
+ setHasLocalOfflineDraft(true);
2729
+ scheduleDocumentFlushRef.current();
2730
+ }, []);
2404
2731
  const applyDraftSnapshot = react.useCallback(
2405
2732
  (draft, options2) => {
2406
- applyDocument(buildDraftSnapshot(draft, clientIdRef.current), options2);
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
+ }
2407
2781
  },
2408
- [applyDocument]
2782
+ [notifySubscribers]
2409
2783
  );
2410
2784
  const resolveAuthoritativeDocument = react.useCallback(
2411
2785
  (serverDocument, options2) => {
2412
- const localDraft = localDraftRef.current;
2413
- if (!localDraft) {
2414
- setConflictState(null);
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) {
2415
2793
  setHasPendingDocumentSync(false);
2416
- applyDocument(serverDocument, options2);
2794
+ clearLocalDraftRef.current();
2417
2795
  return false;
2418
2796
  }
2419
- if (sameSerializedItems(localDraft.items, serverDocument.items)) {
2797
+ const pendingIds = getLocallyPendingItemIds(board);
2798
+ if (pendingIds.size === 0) {
2420
2799
  clearLocalDraftRef.current();
2421
2800
  setHasPendingDocumentSync(false);
2422
- setConflictState(null);
2423
- applyDocument(serverDocument, options2);
2424
- return true;
2425
- }
2426
- if (serverDocument.revision === localDraft.baseRevision) {
2427
- setConflictState(null);
2428
- applyDraftSnapshot(localDraft, {
2429
- suppressSubscriberNotify: options2?.suppressSubscriberNotify ?? sameSerializedItems(latestDocumentRef.current?.items, localDraft.items)
2430
- });
2431
2801
  outboundInFlightRef.current = null;
2432
- queuedItemsRef.current = localDraft.items;
2433
- setHasPendingDocumentSync(true);
2434
- scheduleDocumentFlush();
2435
- return true;
2802
+ queuedDirtyRef.current = false;
2803
+ return false;
2436
2804
  }
2437
- setConflictState({
2438
- serverRevision: serverDocument.revision,
2439
- serverItems: serverDocument.items,
2440
- localItems: localDraft.items
2441
- });
2442
- applyDraftSnapshot(localDraft, {
2443
- suppressSubscriberNotify: options2?.suppressSubscriberNotify ?? sameSerializedItems(latestDocumentRef.current?.items, localDraft.items)
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)
2444
2813
  });
2814
+ outboundInFlightRef.current = null;
2815
+ queuedDirtyRef.current = true;
2816
+ setHasPendingDocumentSync(true);
2817
+ scheduleDocumentFlush();
2445
2818
  return true;
2446
2819
  },
2447
- [applyDocument, applyDraftSnapshot, scheduleDocumentFlush, setConflictState]
2820
+ [applyDocument, roomId, scheduleDocumentFlush, setLocalDraft]
2448
2821
  );
2449
2822
  const sendPresenceUpdate = react.useCallback(() => {
2450
2823
  sendRaw({
@@ -2483,54 +2856,8 @@ function useRealtimeSession(options) {
2483
2856
  reconnect,
2484
2857
  updateConnection
2485
2858
  ]);
2486
- const resolveConflict = react.useCallback(
2487
- (action) => {
2488
- const activeConflict = conflictRef.current;
2489
- if (!activeConflict) return;
2490
- if (action === "use-server") {
2491
- clearLocalDraft();
2492
- queuedItemsRef.current = null;
2493
- outboundInFlightRef.current = null;
2494
- setConflictState(null);
2495
- applyDocument({
2496
- revision: activeConflict.serverRevision,
2497
- items: activeConflict.serverItems,
2498
- updatedAt: nowMs()
2499
- });
2500
- return;
2501
- }
2502
- const nextDraft = {
2503
- roomId,
2504
- baseRevision: activeConflict.serverRevision,
2505
- items: activeConflict.localItems,
2506
- updatedAt: nowMs()
2507
- };
2508
- setLocalDraft(nextDraft);
2509
- scheduleDraftPersistence();
2510
- setConflictState(null);
2511
- applyDraftSnapshot(nextDraft, {
2512
- suppressSubscriberNotify: sameSerializedItems(
2513
- latestDocumentRef.current?.items,
2514
- nextDraft.items
2515
- )
2516
- });
2517
- currentRevisionRef.current = activeConflict.serverRevision;
2518
- queuedItemsRef.current = nextDraft.items;
2519
- outboundInFlightRef.current = null;
2520
- setHasPendingDocumentSync(true);
2521
- scheduleDocumentFlush();
2522
- },
2523
- [
2524
- applyDocument,
2525
- applyDraftSnapshot,
2526
- clearLocalDraft,
2527
- roomId,
2528
- scheduleDocumentFlush,
2529
- scheduleDraftPersistence,
2530
- setConflictState,
2531
- setLocalDraft
2532
- ]
2533
- );
2859
+ const resolveConflict = react.useCallback((_action) => {
2860
+ }, []);
2534
2861
  const setConflictStateRef = react.useRef(setConflictState);
2535
2862
  setConflictStateRef.current = setConflictState;
2536
2863
  const updateConnectionRef = react.useRef(updateConnection);
@@ -2551,7 +2878,15 @@ function useRealtimeSession(options) {
2551
2878
  scheduleReconnectRef.current = scheduleReconnect;
2552
2879
  const sendRawRef = react.useRef(sendRaw);
2553
2880
  sendRawRef.current = sendRaw;
2881
+ const setLocalDraftRef = react.useRef(setLocalDraft);
2882
+ setLocalDraftRef.current = setLocalDraft;
2883
+ const scheduleDraftPersistenceRef = react.useRef(scheduleDraftPersistence);
2884
+ scheduleDraftPersistenceRef.current = scheduleDraftPersistence;
2554
2885
  react.useEffect(() => {
2886
+ if (boardRef.current) {
2887
+ boardRef.current.doc.destroy();
2888
+ }
2889
+ boardRef.current = createYjsBoardDoc();
2555
2890
  if (!roomId) {
2556
2891
  clearDocumentFlushSchedule();
2557
2892
  clearDraftPersistSchedule();
@@ -2559,7 +2894,8 @@ function useRealtimeSession(options) {
2559
2894
  setHasLocalOfflineDraft(false);
2560
2895
  setHasPendingDocumentSync(false);
2561
2896
  setConflictState(null);
2562
- queuedItemsRef.current = null;
2897
+ queuedDirtyRef.current = false;
2898
+ pendingLocalItemsRef.current = null;
2563
2899
  outboundInFlightRef.current = null;
2564
2900
  latestDocumentRef.current = null;
2565
2901
  setDocument(null);
@@ -2570,7 +2906,8 @@ function useRealtimeSession(options) {
2570
2906
  setLocalDraft(localDraft);
2571
2907
  setHasPendingDocumentSync(localDraft != null);
2572
2908
  setConflictState(null);
2573
- queuedItemsRef.current = localDraft?.items ?? null;
2909
+ queuedDirtyRef.current = localDraft != null;
2910
+ pendingLocalItemsRef.current = null;
2574
2911
  outboundInFlightRef.current = null;
2575
2912
  if (localDraft) {
2576
2913
  applyDraftSnapshot(localDraft, {
@@ -2601,7 +2938,7 @@ function useRealtimeSession(options) {
2601
2938
  clearDocumentFlushSchedule();
2602
2939
  wsRef.current?.close();
2603
2940
  wsRef.current = null;
2604
- queuedItemsRef.current = localDraftRef.current?.items ?? null;
2941
+ queuedDirtyRef.current = localDraftRef.current != null;
2605
2942
  outboundInFlightRef.current = null;
2606
2943
  setHasPendingDocumentSync(localDraftRef.current != null);
2607
2944
  collapsePeersToSelfRef.current("offline");
@@ -2716,7 +3053,7 @@ function useRealtimeSession(options) {
2716
3053
  }
2717
3054
  );
2718
3055
  if (!handledByDraft) {
2719
- queuedItemsRef.current = null;
3056
+ queuedDirtyRef.current = false;
2720
3057
  outboundInFlightRef.current = null;
2721
3058
  setHasPendingDocumentSync(false);
2722
3059
  }
@@ -2767,8 +3104,6 @@ function useRealtimeSession(options) {
2767
3104
  const isSelfAck = parsed.document.updatedByClientId === selfClientId;
2768
3105
  if (!isSelfAck) {
2769
3106
  outboundInFlightRef.current = null;
2770
- queuedItemsRef.current = localDraftRef.current?.items ?? null;
2771
- setHasPendingDocumentSync(queuedItemsRef.current != null);
2772
3107
  resolveAuthoritativeDocumentRef.current(
2773
3108
  sanitizeRealtimeSnapshot(parsed.document)
2774
3109
  );
@@ -2779,24 +3114,33 @@ function useRealtimeSession(options) {
2779
3114
  applyDocumentRef.current(sanitizeRealtimeSnapshot(parsed.document), {
2780
3115
  suppressSubscriberNotify: shouldSuppress
2781
3116
  });
2782
- if (queuedItemsRef.current) {
2783
- if (sameSerializedItems(queuedItemsRef.current, parsed.document.items)) {
2784
- queuedItemsRef.current = null;
2785
- } else {
2786
- scheduleDocumentFlushRef.current();
2787
- }
2788
- }
2789
- if (!queuedItemsRef.current) {
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;
2790
3134
  clearLocalDraftRef.current();
3135
+ setHasPendingDocumentSync(false);
2791
3136
  }
2792
- setHasPendingDocumentSync(queuedItemsRef.current != null);
2793
3137
  setConflictStateRef.current(null);
2794
3138
  return;
2795
3139
  }
2796
3140
  if (parsed.type === "document:resync-required") {
2797
3141
  outboundInFlightRef.current = null;
2798
- queuedItemsRef.current = localDraftRef.current?.items ?? null;
2799
- setHasPendingDocumentSync(queuedItemsRef.current != null);
3142
+ queuedDirtyRef.current = localDraftRef.current != null;
3143
+ setHasPendingDocumentSync(queuedDirtyRef.current);
2800
3144
  updateConnectionRef.current((prev) => ({
2801
3145
  ...prev,
2802
3146
  lastError: parsed.reason
@@ -2871,14 +3215,39 @@ function useRealtimeSession(options) {
2871
3215
  () => () => {
2872
3216
  clearDocumentFlushSchedule();
2873
3217
  clearDraftPersistSchedule();
3218
+ if (boardRef.current) {
3219
+ boardRef.current.doc.destroy();
3220
+ boardRef.current = null;
3221
+ }
2874
3222
  },
2875
3223
  [clearDocumentFlushSchedule, clearDraftPersistSchedule]
2876
3224
  );
2877
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
+ }
2878
3247
  persistLocalDraft();
2879
3248
  if (!connection.connected) return;
2880
3249
  flushQueuedDocument();
2881
- }, [connection.connected, flushQueuedDocument, persistLocalDraft]);
3250
+ }, [connection.connected, flushQueuedDocument, persistLocalDraft, roomId]);
2882
3251
  const remoteAdapter = react.useMemo(
2883
3252
  () => ({
2884
3253
  subscribe(onItems) {