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.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
- return typeof record.roomId === "string" && typeof record.baseRevision === "number" && typeof record.updatedAt === "number" && Array.isArray(record.items);
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 queuedItemsRef = useRef(null);
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 = snapshot;
2235
- setDocument(snapshot);
2529
+ latestDocumentRef.current = mergedSnapshot;
2530
+ setDocument(mergedSnapshot);
2236
2531
  if (!options2?.suppressSubscriberNotify) {
2237
- notifySubscribers(snapshot.items);
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
- if (conflictRef.current) return;
2348
- const next = queuedItemsRef.current;
2349
- if (!next || outboundInFlightRef.current) return;
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 preparedItems = prepareRealtimeItems(next);
2655
+ const mergedItems = readVectorItems(board.yItems);
2656
+ const preparedItems = prepareRealtimeItems(mergedItems);
2352
2657
  if (!preparedItems) return;
2353
- queuedItemsRef.current = null;
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
- queuedItemsRef.current = next;
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
- (items) => {
2384
- setLocalDraft({
2385
- roomId,
2386
- baseRevision: currentRevisionRef.current,
2387
- items,
2388
- updatedAt: nowMs()
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
- applyDocument(buildDraftSnapshot(draft, clientIdRef.current), options2);
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
- [applyDocument]
2757
+ [notifySubscribers]
2403
2758
  );
2404
2759
  const resolveAuthoritativeDocument = useCallback(
2405
2760
  (serverDocument, options2) => {
2406
- const localDraft = localDraftRef.current;
2407
- if (!localDraft) {
2408
- setConflictState(null);
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
- applyDocument(serverDocument, options2);
2769
+ clearLocalDraftRef.current();
2411
2770
  return false;
2412
2771
  }
2413
- if (sameSerializedItems(localDraft.items, serverDocument.items)) {
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
- queuedItemsRef.current = localDraft.items;
2427
- setHasPendingDocumentSync(true);
2428
- scheduleDocumentFlush();
2429
- return true;
2777
+ queuedDirtyRef.current = false;
2778
+ return false;
2430
2779
  }
2431
- setConflictState({
2432
- serverRevision: serverDocument.revision,
2433
- serverItems: serverDocument.items,
2434
- localItems: localDraft.items
2435
- });
2436
- applyDraftSnapshot(localDraft, {
2437
- suppressSubscriberNotify: options2?.suppressSubscriberNotify ?? sameSerializedItems(latestDocumentRef.current?.items, localDraft.items)
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, applyDraftSnapshot, scheduleDocumentFlush, setConflictState]
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
- (action) => {
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
- queuedItemsRef.current = null;
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
- queuedItemsRef.current = localDraft?.items ?? null;
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
- queuedItemsRef.current = localDraftRef.current?.items ?? null;
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
- queuedItemsRef.current = null;
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
- if (queuedItemsRef.current) {
2777
- if (sameSerializedItems(queuedItemsRef.current, parsed.document.items)) {
2778
- queuedItemsRef.current = null;
2779
- } else {
2780
- scheduleDocumentFlushRef.current();
2781
- }
2782
- }
2783
- if (!queuedItemsRef.current) {
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
- queuedItemsRef.current = localDraftRef.current?.items ?? null;
2793
- setHasPendingDocumentSync(queuedItemsRef.current != null);
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) {