canvu-react 0.4.33 → 0.4.35

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.
@@ -0,0 +1,2097 @@
1
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
2
+ import * as Y from 'yjs';
3
+
4
+ // src/realtime/protocol.ts
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function getString(value) {
9
+ return typeof value === "string" ? value : void 0;
10
+ }
11
+ function getNumber(value) {
12
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
13
+ }
14
+ function parseCursor(value) {
15
+ if (value === null) return null;
16
+ if (!isRecord(value)) return void 0;
17
+ const x = getNumber(value.x);
18
+ const y = getNumber(value.y);
19
+ if (x == null || y == null) return void 0;
20
+ return { x, y };
21
+ }
22
+ function parseCamera(value) {
23
+ if (value === null) return null;
24
+ if (!isRecord(value)) return void 0;
25
+ const x = getNumber(value.x);
26
+ const y = getNumber(value.y);
27
+ const zoom = getNumber(value.zoom);
28
+ const viewportWidth = getNumber(value.viewportWidth);
29
+ const viewportHeight = getNumber(value.viewportHeight);
30
+ if (x == null || y == null || zoom == null || viewportWidth == null || viewportHeight == null) {
31
+ return void 0;
32
+ }
33
+ return { x, y, zoom, viewportWidth, viewportHeight };
34
+ }
35
+ function parseMarkupStroke(value) {
36
+ if (value === null) return null;
37
+ if (!isRecord(value) || !Array.isArray(value.points)) return void 0;
38
+ const tool = getString(value.tool);
39
+ if (tool !== "draw" && tool !== "pencil" && tool !== "brush" && tool !== "marker" && tool !== "laser") {
40
+ return void 0;
41
+ }
42
+ const points = value.points.map((point) => {
43
+ if (!isRecord(point)) return null;
44
+ const x = getNumber(point.x);
45
+ const y = getNumber(point.y);
46
+ if (x == null || y == null) return null;
47
+ return { x, y };
48
+ }).filter((point) => point != null);
49
+ const strokeWidth = getNumber(value.strokeWidth);
50
+ const stroke = getString(value.stroke);
51
+ const strokeOpacity = getNumber(value.strokeOpacity);
52
+ return {
53
+ points,
54
+ tool,
55
+ ...strokeWidth != null ? { strokeWidth } : {},
56
+ ...stroke ? { stroke } : {},
57
+ ...strokeOpacity != null ? { strokeOpacity } : {}
58
+ };
59
+ }
60
+ function parsePresencePayload(value) {
61
+ if (!isRecord(value)) return void 0;
62
+ const cursor = parseCursor(value.cursor);
63
+ if (cursor === void 0) return void 0;
64
+ const markupStroke = parseMarkupStroke(value.markupStroke);
65
+ if (markupStroke === void 0 && value.markupStroke !== void 0)
66
+ return void 0;
67
+ const camera = parseCamera(value.camera);
68
+ if (camera === void 0 && value.camera !== void 0) return void 0;
69
+ return {
70
+ cursor,
71
+ ...markupStroke !== void 0 ? { markupStroke } : {},
72
+ ...camera !== void 0 ? { camera } : {},
73
+ ...getString(value.activeTool) ? { activeTool: getString(value.activeTool) } : {}
74
+ };
75
+ }
76
+ function parseItems(value) {
77
+ return Array.isArray(value) ? value : void 0;
78
+ }
79
+ function parseDocumentSnapshot(value) {
80
+ if (!isRecord(value)) return void 0;
81
+ const revision = getNumber(value.revision);
82
+ const updatedAt = getNumber(value.updatedAt);
83
+ const items = parseItems(value.items);
84
+ if (revision == null || updatedAt == null || items == null) return void 0;
85
+ return {
86
+ revision,
87
+ updatedAt,
88
+ items,
89
+ ...getString(value.updatedByClientId) ? { updatedByClientId: getString(value.updatedByClientId) } : {},
90
+ ...getNumber(value.persistedRevision) != null ? { persistedRevision: getNumber(value.persistedRevision) } : {}
91
+ };
92
+ }
93
+ function parseRealtimeSessionPeer(value) {
94
+ if (!isRecord(value)) return void 0;
95
+ const clientId = getString(value.clientId);
96
+ const peerId = getString(value.peerId);
97
+ const roomId = getString(value.roomId);
98
+ const joinedAt = getNumber(value.joinedAt);
99
+ const lastSeenAt = getNumber(value.lastSeenAt);
100
+ const cursor = parseCursor(value.cursor);
101
+ if (clientId == null || peerId == null || roomId == null || joinedAt == null || lastSeenAt == null || cursor === void 0) {
102
+ return void 0;
103
+ }
104
+ const markupStroke = parseMarkupStroke(value.markupStroke);
105
+ if (markupStroke === void 0 && value.markupStroke !== void 0)
106
+ return void 0;
107
+ const camera = parseCamera(value.camera);
108
+ if (camera === void 0 && value.camera !== void 0) return void 0;
109
+ const isSelf = value.isSelf === true;
110
+ const connectionState = getString(value.connectionState);
111
+ return {
112
+ id: clientId,
113
+ clientId,
114
+ peerId,
115
+ roomId,
116
+ joinedAt,
117
+ lastSeenAt,
118
+ isSelf,
119
+ cursor,
120
+ ...getString(value.displayName) ? { displayName: getString(value.displayName) } : {},
121
+ ...getString(value.color) ? { color: getString(value.color) } : {},
122
+ ...getString(value.image) ? { image: getString(value.image) } : {},
123
+ ...markupStroke !== void 0 ? { markupStroke } : {},
124
+ ...camera !== void 0 ? { camera } : {},
125
+ ...getString(value.activeTool) ? { activeTool: getString(value.activeTool) } : {},
126
+ ...connectionState ? { connectionState } : {}
127
+ };
128
+ }
129
+ function parsePeers(value) {
130
+ if (!Array.isArray(value)) return void 0;
131
+ const peers = value.map((peer) => parseRealtimeSessionPeer(peer)).filter((peer) => peer != null);
132
+ return peers.length === value.length ? peers : void 0;
133
+ }
134
+ function parseRealtimeClientMessage(value) {
135
+ if (!isRecord(value)) return void 0;
136
+ const type = getString(value.type);
137
+ if (type === "session:join") {
138
+ const roomId = getString(value.roomId);
139
+ const peer = isRecord(value.peer) ? value.peer : void 0;
140
+ const clientId = peer ? getString(peer.clientId) : void 0;
141
+ const peerId = peer ? getString(peer.peerId) : void 0;
142
+ if (!roomId || !peer || !clientId || !peerId) return void 0;
143
+ return {
144
+ type,
145
+ roomId,
146
+ peer: {
147
+ clientId,
148
+ peerId,
149
+ ...getString(peer.displayName) ? { displayName: getString(peer.displayName) } : {},
150
+ ...getString(peer.color) ? { color: getString(peer.color) } : {},
151
+ ...getString(peer.image) ? { image: getString(peer.image) } : {}
152
+ }
153
+ };
154
+ }
155
+ if (type === "session:leave" || type === "session:ping") {
156
+ const roomId = getString(value.roomId);
157
+ const clientId = getString(value.clientId);
158
+ if (!roomId || !clientId) return void 0;
159
+ if (type === "session:leave") {
160
+ return { type, roomId, clientId };
161
+ }
162
+ const sentAt = getNumber(value.sentAt);
163
+ if (sentAt == null) return void 0;
164
+ return { type, roomId, clientId, sentAt };
165
+ }
166
+ if (type === "presence:update") {
167
+ const roomId = getString(value.roomId);
168
+ const clientId = getString(value.clientId);
169
+ const presence = parsePresencePayload(value.presence);
170
+ if (!roomId || !clientId || !presence) return void 0;
171
+ return { type, roomId, clientId, presence };
172
+ }
173
+ if (type === "document:update") {
174
+ const roomId = getString(value.roomId);
175
+ const clientId = getString(value.clientId);
176
+ const baseRevision = getNumber(value.baseRevision);
177
+ const items = parseItems(value.items);
178
+ if (!roomId || !clientId || baseRevision == null || !items) return void 0;
179
+ return { type, roomId, clientId, baseRevision, items };
180
+ }
181
+ return void 0;
182
+ }
183
+ function parseRealtimeServerMessage(value) {
184
+ if (!isRecord(value)) return void 0;
185
+ const type = getString(value.type);
186
+ if (type === "session:welcome") {
187
+ const roomId = getString(value.roomId);
188
+ const clientId = getString(value.clientId);
189
+ const serverTime = getNumber(value.serverTime);
190
+ const document = parseDocumentSnapshot(value.document);
191
+ const peers = parsePeers(value.peers);
192
+ if (!roomId || !clientId || serverTime == null || !document || !peers) {
193
+ return void 0;
194
+ }
195
+ return { type, roomId, clientId, serverTime, document, peers };
196
+ }
197
+ if (type === "session:peer-joined") {
198
+ const roomId = getString(value.roomId);
199
+ const peer = parseRealtimeSessionPeer(value.peer);
200
+ if (!roomId || !peer) return void 0;
201
+ return { type, roomId, peer };
202
+ }
203
+ if (type === "session:peer-left") {
204
+ const roomId = getString(value.roomId);
205
+ const clientId = getString(value.clientId);
206
+ if (!roomId || !clientId) return void 0;
207
+ return { type, roomId, clientId };
208
+ }
209
+ if (type === "session:pong") {
210
+ const roomId = getString(value.roomId);
211
+ const clientId = getString(value.clientId);
212
+ const sentAt = getNumber(value.sentAt);
213
+ const serverTime = getNumber(value.serverTime);
214
+ if (!roomId || !clientId || sentAt == null || serverTime == null) {
215
+ return void 0;
216
+ }
217
+ return { type, roomId, clientId, sentAt, serverTime };
218
+ }
219
+ if (type === "session:error") {
220
+ const code = getString(value.code);
221
+ const message = getString(value.message);
222
+ if (!code || !message) return void 0;
223
+ return {
224
+ type,
225
+ code,
226
+ message,
227
+ ...getString(value.roomId) ? { roomId: getString(value.roomId) } : {}
228
+ };
229
+ }
230
+ if (type === "presence:sync") {
231
+ const roomId = getString(value.roomId);
232
+ const peers = parsePeers(value.peers);
233
+ const serverTime = getNumber(value.serverTime);
234
+ if (!roomId || !peers || serverTime == null) return void 0;
235
+ return { type, roomId, peers, serverTime };
236
+ }
237
+ if (type === "document:sync" || type === "document:resync-required") {
238
+ const roomId = getString(value.roomId);
239
+ const document = parseDocumentSnapshot(value.document);
240
+ if (!roomId || !document) return void 0;
241
+ if (type === "document:sync") {
242
+ return { type, roomId, document };
243
+ }
244
+ const reason = getString(value.reason);
245
+ if (!reason) return void 0;
246
+ return { type, roomId, reason, document };
247
+ }
248
+ return void 0;
249
+ }
250
+ function getSceneItemId(item) {
251
+ const id = item.id;
252
+ return typeof id === "string" ? id : null;
253
+ }
254
+ function hasMissingLocalItems(localItems, incomingItems) {
255
+ const incomingIds = /* @__PURE__ */ new Set();
256
+ for (const item of incomingItems) {
257
+ const id = getSceneItemId(item);
258
+ if (id) incomingIds.add(id);
259
+ }
260
+ for (const item of localItems) {
261
+ const id = getSceneItemId(item);
262
+ if (id && !incomingIds.has(id)) return true;
263
+ }
264
+ return false;
265
+ }
266
+ function useRealtimeCanvasDocument(options) {
267
+ const {
268
+ session,
269
+ items,
270
+ onItemsChange,
271
+ normalizeItems,
272
+ hydrateItems,
273
+ enabled = true
274
+ } = options;
275
+ const [loading, setLoading] = useState(false);
276
+ const lastAppliedRevisionRef = useRef(null);
277
+ const inFlightRevisionRef = useRef(null);
278
+ const hasEverPropagatedItemsRef = useRef(false);
279
+ const realtimeEnabled = enabled && session != null;
280
+ const documentRevision = session?.document?.revision ?? null;
281
+ const documentItems = session?.document?.items;
282
+ const documentUpdatedByClientId = session?.document?.updatedByClientId ?? null;
283
+ const connectionClientId = session?.connection.clientId ?? null;
284
+ const hasLocalOfflineDraft = session?.hasLocalOfflineDraft ?? false;
285
+ const hasPendingDocumentSync = session?.hasPendingDocumentSync ?? false;
286
+ const applyIncomingItems = useCallback(
287
+ async (nextItems) => {
288
+ const normalizedItems = normalizeItems ? normalizeItems(nextItems) : [...nextItems];
289
+ if (!hydrateItems) return normalizedItems;
290
+ return await hydrateItems(normalizedItems);
291
+ },
292
+ [hydrateItems, normalizeItems]
293
+ );
294
+ const handleItemsChange = useCallback(
295
+ (nextItems) => {
296
+ if (!enabled) {
297
+ onItemsChange?.(normalizeItems ? normalizeItems(nextItems) : nextItems);
298
+ return;
299
+ }
300
+ const normalizedItems = normalizeItems ? normalizeItems(nextItems) : nextItems;
301
+ onItemsChange?.(normalizedItems);
302
+ session?.remoteAdapter.send?.(normalizedItems);
303
+ },
304
+ [enabled, normalizeItems, onItemsChange, session]
305
+ );
306
+ useEffect(() => {
307
+ if (items.length > 0) {
308
+ hasEverPropagatedItemsRef.current = true;
309
+ }
310
+ }, [items.length]);
311
+ useEffect(() => {
312
+ if (!realtimeEnabled || !onItemsChange || !session?.document) return;
313
+ if (documentUpdatedByClientId === connectionClientId) return;
314
+ if (documentRevision == null) return;
315
+ if (lastAppliedRevisionRef.current === documentRevision) return;
316
+ if (inFlightRevisionRef.current === documentRevision) return;
317
+ inFlightRevisionRef.current = documentRevision;
318
+ let cancelled = false;
319
+ setLoading(true);
320
+ void applyIncomingItems(documentItems ?? []).then((resolvedItems) => {
321
+ if (cancelled) return;
322
+ if (inFlightRevisionRef.current !== documentRevision) return;
323
+ lastAppliedRevisionRef.current = documentRevision;
324
+ const hasLocalItems = items.length > 0;
325
+ const hasPendingLocalChanges = hasLocalOfflineDraft || hasPendingDocumentSync;
326
+ if (resolvedItems.length === 0 && (hasEverPropagatedItemsRef.current || hasLocalItems || hasPendingLocalChanges)) {
327
+ if (hasLocalItems) {
328
+ const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
329
+ session.remoteAdapter.send?.(normalizedLocalItems);
330
+ }
331
+ return;
332
+ }
333
+ if (hasLocalItems && hasMissingLocalItems(items, resolvedItems)) {
334
+ const normalizedLocalItems = normalizeItems ? normalizeItems(items) : [...items];
335
+ session.remoteAdapter.send?.(normalizedLocalItems);
336
+ return;
337
+ }
338
+ if (resolvedItems.length > 0) {
339
+ hasEverPropagatedItemsRef.current = true;
340
+ }
341
+ onItemsChange(resolvedItems);
342
+ }).finally(() => {
343
+ if (inFlightRevisionRef.current === documentRevision) {
344
+ inFlightRevisionRef.current = null;
345
+ }
346
+ if (cancelled) return;
347
+ setLoading(false);
348
+ });
349
+ return () => {
350
+ cancelled = true;
351
+ };
352
+ }, [
353
+ applyIncomingItems,
354
+ connectionClientId,
355
+ documentItems,
356
+ documentRevision,
357
+ documentUpdatedByClientId,
358
+ hasLocalOfflineDraft,
359
+ hasPendingDocumentSync,
360
+ items,
361
+ normalizeItems,
362
+ onItemsChange,
363
+ realtimeEnabled,
364
+ session?.remoteAdapter,
365
+ session?.document
366
+ ]);
367
+ return useMemo(
368
+ () => ({
369
+ items,
370
+ onItemsChange: onItemsChange ? handleItemsChange : void 0,
371
+ loading,
372
+ saving: session?.hasPendingDocumentSync ?? false,
373
+ hasLocalOfflineDraft: session?.hasLocalOfflineDraft ?? false,
374
+ syncState: session?.syncState ?? "offline",
375
+ conflict: session?.conflict ?? null,
376
+ resolveConflict: session?.resolveConflict ?? (() => {
377
+ }),
378
+ clearLocalDraft: session?.clearLocalDraft ?? (() => {
379
+ }),
380
+ flush: session?.flushDocumentSync ?? (async () => {
381
+ })
382
+ }),
383
+ [handleItemsChange, items, loading, onItemsChange, session]
384
+ );
385
+ }
386
+ var viewportFollowSyncSnapshots = /* @__PURE__ */ new WeakMap();
387
+ function requestRuntimeAnimationFrame(callback) {
388
+ const runtime = globalThis;
389
+ if (typeof runtime.requestAnimationFrame === "function") {
390
+ return runtime.requestAnimationFrame(callback);
391
+ }
392
+ return globalThis.setTimeout(() => callback(Date.now()), 16);
393
+ }
394
+ function cancelRuntimeAnimationFrame(handle) {
395
+ const runtime = globalThis;
396
+ if (typeof runtime.cancelAnimationFrame === "function") {
397
+ runtime.cancelAnimationFrame(handle);
398
+ return;
399
+ }
400
+ globalThis.clearTimeout(handle);
401
+ }
402
+ function getFollowedPeer(sessionPeers, followedPeerId) {
403
+ return sessionPeers.find(
404
+ (peerState) => peerState.peerId === followedPeerId || peerState.id === followedPeerId
405
+ );
406
+ }
407
+ function getCameraKey(peer) {
408
+ if (!peer.camera) return null;
409
+ return [
410
+ peer.peerId,
411
+ peer.camera.x,
412
+ peer.camera.y,
413
+ peer.camera.zoom,
414
+ peer.camera.viewportWidth,
415
+ peer.camera.viewportHeight
416
+ ].join(":");
417
+ }
418
+ function getViewportSizeKey(viewport) {
419
+ if (!viewport) return null;
420
+ const viewportSize = viewport.getViewportSize();
421
+ return [viewportSize.width, viewportSize.height].join(":");
422
+ }
423
+ function getFollowedCameraPosition(viewport, peer) {
424
+ if (!peer.camera) return null;
425
+ const viewportSize = viewport.getViewportSize();
426
+ return {
427
+ x: peer.camera.x + (viewportSize.width - peer.camera.viewportWidth) / 2,
428
+ y: peer.camera.y + (viewportSize.height - peer.camera.viewportHeight) / 2,
429
+ zoom: peer.camera.zoom
430
+ };
431
+ }
432
+ function applyPeerCamera(viewport, peer) {
433
+ const camera = viewport.getCamera();
434
+ const nextCamera = getFollowedCameraPosition(viewport, peer);
435
+ const viewportSize = viewport.getViewportSize();
436
+ if (!camera || !nextCamera) return false;
437
+ if (camera.x === nextCamera.x && camera.y === nextCamera.y && camera.zoom === nextCamera.zoom) {
438
+ return true;
439
+ }
440
+ markViewportFollowSync(viewport, {
441
+ x: nextCamera.x,
442
+ y: nextCamera.y,
443
+ zoom: nextCamera.zoom,
444
+ viewportWidth: viewportSize.width,
445
+ viewportHeight: viewportSize.height
446
+ });
447
+ camera.x = nextCamera.x;
448
+ camera.y = nextCamera.y;
449
+ camera.zoom = nextCamera.zoom;
450
+ viewport.requestRender();
451
+ return true;
452
+ }
453
+ function markViewportFollowSync(viewport, snapshot) {
454
+ viewportFollowSyncSnapshots.set(viewport, snapshot);
455
+ }
456
+ function consumeViewportFollowSync(viewport, snapshot) {
457
+ const currentSnapshot = viewportFollowSyncSnapshots.get(viewport);
458
+ if (!currentSnapshot || currentSnapshot.x !== snapshot.x || currentSnapshot.y !== snapshot.y || currentSnapshot.zoom !== snapshot.zoom || currentSnapshot.viewportWidth !== snapshot.viewportWidth || currentSnapshot.viewportHeight !== snapshot.viewportHeight) {
459
+ return false;
460
+ }
461
+ viewportFollowSyncSnapshots.delete(viewport);
462
+ return true;
463
+ }
464
+ function useRealtimePeerFollow(options) {
465
+ const { viewportRef, sessionPeers, followedPeerId, onFollowEnd } = options;
466
+ const endedPeerIdRef = useRef(null);
467
+ const lastAppliedCameraKeyRef = useRef(null);
468
+ const [, setViewportSizeVersion] = useState(0);
469
+ useEffect(() => {
470
+ if (!followedPeerId) return;
471
+ let animationFrameId = 0;
472
+ let lastViewportSizeKey = getViewportSizeKey(viewportRef.current ?? null);
473
+ const checkViewportSize = () => {
474
+ const nextViewportSizeKey = getViewportSizeKey(viewportRef.current ?? null);
475
+ if (nextViewportSizeKey !== lastViewportSizeKey) {
476
+ lastViewportSizeKey = nextViewportSizeKey;
477
+ setViewportSizeVersion((value) => value + 1);
478
+ }
479
+ animationFrameId = requestRuntimeAnimationFrame(checkViewportSize);
480
+ };
481
+ animationFrameId = requestRuntimeAnimationFrame(checkViewportSize);
482
+ return () => {
483
+ cancelRuntimeAnimationFrame(animationFrameId);
484
+ };
485
+ }, [followedPeerId, viewportRef]);
486
+ useEffect(() => {
487
+ if (!followedPeerId) {
488
+ endedPeerIdRef.current = null;
489
+ lastAppliedCameraKeyRef.current = null;
490
+ return;
491
+ }
492
+ const followedPeer = getFollowedPeer(sessionPeers, followedPeerId);
493
+ if (!followedPeer) {
494
+ lastAppliedCameraKeyRef.current = null;
495
+ if (endedPeerIdRef.current === followedPeerId) {
496
+ return;
497
+ }
498
+ endedPeerIdRef.current = followedPeerId;
499
+ onFollowEnd?.();
500
+ return;
501
+ }
502
+ if (!followedPeer.camera) {
503
+ endedPeerIdRef.current = null;
504
+ lastAppliedCameraKeyRef.current = null;
505
+ return;
506
+ }
507
+ endedPeerIdRef.current = null;
508
+ const viewport = viewportRef.current;
509
+ const nextCameraKey = [
510
+ getCameraKey(followedPeer),
511
+ getViewportSizeKey(viewport ?? null)
512
+ ].join(":");
513
+ if (nextCameraKey && nextCameraKey === lastAppliedCameraKeyRef.current) {
514
+ return;
515
+ }
516
+ if (!viewport || !applyPeerCamera(viewport, followedPeer)) {
517
+ return;
518
+ }
519
+ lastAppliedCameraKeyRef.current = nextCameraKey;
520
+ }, [followedPeerId, onFollowEnd, sessionPeers, viewportRef]);
521
+ }
522
+ var ITEMS_KEY = "items";
523
+ var CLIENT_ONLY_IMAGE_KEYS = /* @__PURE__ */ new Set([
524
+ "imageBlobId",
525
+ "imageThumbnailBlobId",
526
+ "imageRasterHref",
527
+ "imageThumbnailHref"
528
+ ]);
529
+ var SERVER_ADD_RACE_WINDOW_MS = 2e3;
530
+ function createYjsBoardDoc() {
531
+ const doc = new Y.Doc();
532
+ const yItems = doc.getArray(ITEMS_KEY);
533
+ return {
534
+ doc,
535
+ yItems,
536
+ lastServerConfirmedIds: /* @__PURE__ */ new Set(),
537
+ lastServerConfirmedItemSerializations: /* @__PURE__ */ new Map(),
538
+ serverItemSeenAt: /* @__PURE__ */ new Map()
539
+ };
540
+ }
541
+ function getItemId(item) {
542
+ if (item instanceof Y.Map) {
543
+ const id2 = item.get("id");
544
+ return typeof id2 === "string" ? id2 : null;
545
+ }
546
+ const id = item.id;
547
+ return typeof id === "string" ? id : null;
548
+ }
549
+ function vectorItemToYMap(item) {
550
+ const yMap = new Y.Map();
551
+ for (const [key, value] of Object.entries(normalizeRealtimeItem(item))) {
552
+ if (value === void 0) continue;
553
+ yMap.set(key, value);
554
+ }
555
+ return yMap;
556
+ }
557
+ function yMapToVectorItem(yMap) {
558
+ const obj = {};
559
+ for (const [key, value] of yMap.entries()) {
560
+ obj[key] = value;
561
+ }
562
+ return obj;
563
+ }
564
+ function readVectorItems(yItems) {
565
+ const result = [];
566
+ for (let i = 0; i < yItems.length; i++) {
567
+ const yMap = yItems.get(i);
568
+ if (yMap) result.push(yMapToVectorItem(yMap));
569
+ }
570
+ return result;
571
+ }
572
+ function indexYItemsById(yItems) {
573
+ const result = /* @__PURE__ */ new Map();
574
+ for (let i = 0; i < yItems.length; i++) {
575
+ const yMap = yItems.get(i);
576
+ if (!yMap) continue;
577
+ const id = getItemId(yMap);
578
+ if (!id) continue;
579
+ result.set(id, { yMap, index: i });
580
+ }
581
+ return result;
582
+ }
583
+ function valuesEqual(left, right) {
584
+ if (left === right) return true;
585
+ if (typeof left !== typeof right) return false;
586
+ if (left === null || right === null) return false;
587
+ if (typeof left !== "object") return false;
588
+ try {
589
+ return JSON.stringify(left) === JSON.stringify(right);
590
+ } catch {
591
+ return false;
592
+ }
593
+ }
594
+ function serializeItem(value) {
595
+ const item = value instanceof Y.Map ? yMapToVectorItem(value) : value;
596
+ return JSON.stringify(normalizeRealtimeItem(item));
597
+ }
598
+ function updateYMapInPlace(yMap, next) {
599
+ const normalizedNext = normalizeRealtimeItem(next);
600
+ const nextKeys = /* @__PURE__ */ new Set();
601
+ for (const [key, value] of Object.entries(normalizedNext)) {
602
+ if (value === void 0) continue;
603
+ nextKeys.add(key);
604
+ const current = yMap.get(key);
605
+ if (!valuesEqual(current, value)) {
606
+ yMap.set(key, value);
607
+ }
608
+ }
609
+ for (const key of Array.from(yMap.keys())) {
610
+ if (!nextKeys.has(key)) yMap.delete(key);
611
+ }
612
+ }
613
+ function normalizeRealtimeItem(item) {
614
+ if (item.toolKind !== "image") return item;
615
+ const normalized = Object.fromEntries(
616
+ Object.entries(item).filter(([key]) => !CLIENT_ONLY_IMAGE_KEYS.has(key))
617
+ );
618
+ normalized.childrenSvg = "";
619
+ return normalized;
620
+ }
621
+ function applyLocalItemsToYDoc(board, options) {
622
+ const { items, origin } = options;
623
+ const addedIds = [];
624
+ const removedIds = [];
625
+ const now = Date.now();
626
+ board.doc.transact(() => {
627
+ const currentIndex = indexYItemsById(board.yItems);
628
+ const nextIds = /* @__PURE__ */ new Set();
629
+ for (const item of items) {
630
+ const id = getItemId(item);
631
+ if (!id) continue;
632
+ nextIds.add(id);
633
+ }
634
+ const toDelete = [];
635
+ for (const [id, entry] of currentIndex) {
636
+ if (nextIds.has(id)) continue;
637
+ const serverSeenAt = board.serverItemSeenAt.get(id);
638
+ if (serverSeenAt != null && now - serverSeenAt < SERVER_ADD_RACE_WINDOW_MS) {
639
+ continue;
640
+ }
641
+ toDelete.push({ id, index: entry.index });
642
+ }
643
+ toDelete.sort((a, b) => b.index - a.index);
644
+ for (const { id, index } of toDelete) {
645
+ board.yItems.delete(index, 1);
646
+ board.serverItemSeenAt.delete(id);
647
+ removedIds.push(id);
648
+ }
649
+ const refreshedIndex = indexYItemsById(board.yItems);
650
+ for (let nextOrder = 0; nextOrder < items.length; nextOrder++) {
651
+ const item = items[nextOrder];
652
+ if (!item) continue;
653
+ const id = getItemId(item);
654
+ if (!id) continue;
655
+ const existing = refreshedIndex.get(id);
656
+ if (existing) {
657
+ updateYMapInPlace(existing.yMap, item);
658
+ continue;
659
+ }
660
+ const yMap = vectorItemToYMap(item);
661
+ board.yItems.push([yMap]);
662
+ addedIds.push(id);
663
+ }
664
+ }, origin);
665
+ return { addedIds, removedIds };
666
+ }
667
+ function applyServerSnapshotToYDoc(board, options) {
668
+ const { items: snapshotItems, origin } = options;
669
+ const now = Date.now();
670
+ board.doc.transact(() => {
671
+ for (const item of snapshotItems) {
672
+ const id = getItemId(item);
673
+ if (!id) continue;
674
+ const existing = indexYItemsById(board.yItems).get(id);
675
+ if (existing) {
676
+ const currentSerialized = serializeItem(existing.yMap);
677
+ const incomingSerialized = serializeItem(item);
678
+ const confirmedSerialized = board.lastServerConfirmedItemSerializations.get(id);
679
+ const hasPendingLocalChange = confirmedSerialized === void 0 ? currentSerialized !== incomingSerialized : currentSerialized !== confirmedSerialized && currentSerialized !== incomingSerialized;
680
+ if (hasPendingLocalChange) {
681
+ board.serverItemSeenAt.set(id, now);
682
+ continue;
683
+ }
684
+ updateYMapInPlace(existing.yMap, item);
685
+ board.serverItemSeenAt.set(id, now);
686
+ continue;
687
+ }
688
+ const yMap = vectorItemToYMap(item);
689
+ board.yItems.push([yMap]);
690
+ board.serverItemSeenAt.set(id, now);
691
+ }
692
+ }, origin);
693
+ board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
694
+ board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
695
+ for (const item of snapshotItems) {
696
+ const id = getItemId(item);
697
+ if (id) {
698
+ board.lastServerConfirmedIds.add(id);
699
+ board.lastServerConfirmedItemSerializations.set(id, serializeItem(item));
700
+ }
701
+ }
702
+ }
703
+ function replaceYDocWithSnapshot(board, options) {
704
+ const { items: snapshotItems, origin } = options;
705
+ const now = Date.now();
706
+ board.doc.transact(() => {
707
+ if (board.yItems.length > 0) {
708
+ board.yItems.delete(0, board.yItems.length);
709
+ }
710
+ for (const item of snapshotItems) {
711
+ board.yItems.push([vectorItemToYMap(item)]);
712
+ }
713
+ }, origin);
714
+ board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
715
+ board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
716
+ board.serverItemSeenAt = /* @__PURE__ */ new Map();
717
+ for (const item of snapshotItems) {
718
+ const id = getItemId(item);
719
+ if (id) {
720
+ board.lastServerConfirmedIds.add(id);
721
+ board.lastServerConfirmedItemSerializations.set(id, serializeItem(item));
722
+ board.serverItemSeenAt.set(id, now);
723
+ }
724
+ }
725
+ }
726
+ function getLocallyPendingItemIds(board) {
727
+ const pending = /* @__PURE__ */ new Set();
728
+ for (let i = 0; i < board.yItems.length; i++) {
729
+ const yMap = board.yItems.get(i);
730
+ if (!yMap) continue;
731
+ const id = getItemId(yMap);
732
+ if (!id) continue;
733
+ const confirmedSerialized = board.lastServerConfirmedItemSerializations.get(id);
734
+ if (!confirmedSerialized || serializeItem(yMap) !== confirmedSerialized) {
735
+ pending.add(id);
736
+ }
737
+ }
738
+ return pending;
739
+ }
740
+ function encodeYDocState(board) {
741
+ const update = Y.encodeStateAsUpdate(board.doc);
742
+ let binary = "";
743
+ for (let i = 0; i < update.length; i++) {
744
+ binary += String.fromCharCode(update[i]);
745
+ }
746
+ return btoa(binary);
747
+ }
748
+ function decodeYDocState(board, encoded) {
749
+ try {
750
+ const binary = atob(encoded);
751
+ const update = new Uint8Array(binary.length);
752
+ for (let i = 0; i < binary.length; i++) {
753
+ update[i] = binary.charCodeAt(i);
754
+ }
755
+ Y.applyUpdate(board.doc, update);
756
+ return true;
757
+ } catch {
758
+ return false;
759
+ }
760
+ }
761
+
762
+ // src/realtime/use-realtime-session.ts
763
+ var ORIGIN_LOCAL = /* @__PURE__ */ Symbol("canvu/realtime/local");
764
+ var ORIGIN_REMOTE = /* @__PURE__ */ Symbol("canvu/realtime/remote");
765
+ var ORIGIN_BOOTSTRAP = /* @__PURE__ */ Symbol("canvu/realtime/bootstrap");
766
+ var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
767
+ var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
768
+ var DRAFT_PERSIST_DEBOUNCE_MS = 420;
769
+ function requestRuntimeIdleCallback(callback, timeout) {
770
+ const runtime = globalThis;
771
+ if (typeof runtime.requestIdleCallback === "function") {
772
+ return runtime.requestIdleCallback(callback, { timeout });
773
+ }
774
+ return globalThis.setTimeout(callback, timeout);
775
+ }
776
+ function cancelRuntimeIdleCallback(handle) {
777
+ if (handle == null) return;
778
+ const runtime = globalThis;
779
+ if (typeof runtime.cancelIdleCallback === "function") {
780
+ runtime.cancelIdleCallback(Number(handle));
781
+ return;
782
+ }
783
+ globalThis.clearTimeout(handle);
784
+ }
785
+ function createBrowserDraftStorage() {
786
+ const runtime = globalThis;
787
+ const storage = runtime.localStorage;
788
+ if (!storage) return null;
789
+ return {
790
+ getItem: (key) => storage.getItem(key),
791
+ setItem: (key, value) => storage.setItem(key, value),
792
+ removeItem: (key) => storage.removeItem(key)
793
+ };
794
+ }
795
+ function getSocketHandlerProperty(type) {
796
+ if (type === "open") return "onopen";
797
+ if (type === "message") return "onmessage";
798
+ if (type === "error") return "onerror";
799
+ return "onclose";
800
+ }
801
+ function addRealtimeSocketListener(socket, type, handler) {
802
+ if (typeof socket.addEventListener === "function" && typeof socket.removeEventListener === "function") {
803
+ const listener = handler;
804
+ socket.addEventListener(type, listener);
805
+ return () => socket.removeEventListener(type, listener);
806
+ }
807
+ const property = getSocketHandlerProperty(type);
808
+ const handlerSocket = socket;
809
+ if (property === "onopen") {
810
+ const previous2 = handlerSocket.onopen;
811
+ handlerSocket.onopen = (event) => handler(event);
812
+ return () => {
813
+ handlerSocket.onopen = previous2;
814
+ };
815
+ }
816
+ if (property === "onmessage") {
817
+ const previous2 = handlerSocket.onmessage;
818
+ handlerSocket.onmessage = (event) => handler(event);
819
+ return () => {
820
+ handlerSocket.onmessage = previous2;
821
+ };
822
+ }
823
+ if (property === "onerror") {
824
+ const previous2 = handlerSocket.onerror;
825
+ handlerSocket.onerror = (event) => handler(event);
826
+ return () => {
827
+ handlerSocket.onerror = previous2;
828
+ };
829
+ }
830
+ const previous = handlerSocket.onclose;
831
+ handlerSocket.onclose = (event) => handler(event);
832
+ return () => {
833
+ handlerSocket.onclose = previous;
834
+ };
835
+ }
836
+ function createClientId() {
837
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
838
+ return crypto.randomUUID();
839
+ }
840
+ return `client-${Math.random().toString(36).slice(2, 10)}`;
841
+ }
842
+ function normalizeSocketUrl(input) {
843
+ if (!input) return "";
844
+ if (input.startsWith("ws://") || input.startsWith("wss://")) return input;
845
+ if (input.startsWith("http://")) return `ws://${input.slice("http://".length)}`;
846
+ if (input.startsWith("https://")) return `wss://${input.slice("https://".length)}`;
847
+ if (input.startsWith("/")) {
848
+ const location = globalThis.location;
849
+ if (!location) return "";
850
+ const protocol = location.protocol === "https:" ? "wss:" : "ws:";
851
+ return `${protocol}//${location.host}${input}`;
852
+ }
853
+ return input;
854
+ }
855
+ function isValidSocketUrl(input) {
856
+ if (!input || input.includes("<") || input.includes(">")) return false;
857
+ try {
858
+ const parsed = new URL(input);
859
+ return parsed.protocol === "ws:" || parsed.protocol === "wss:";
860
+ } catch {
861
+ return false;
862
+ }
863
+ }
864
+ function sanitizeRealtimeItem(item) {
865
+ if (item.toolKind !== "image") return item;
866
+ return {
867
+ ...item,
868
+ imageBlobId: void 0,
869
+ imageThumbnailBlobId: void 0,
870
+ imageRasterHref: void 0,
871
+ imageThumbnailHref: void 0,
872
+ childrenSvg: ""
873
+ };
874
+ }
875
+ function sanitizeRealtimeItems(items) {
876
+ return items.map(sanitizeRealtimeItem);
877
+ }
878
+ function sanitizeRealtimeSnapshot(snapshot) {
879
+ return {
880
+ ...snapshot,
881
+ items: sanitizeRealtimeItems(snapshot.items)
882
+ };
883
+ }
884
+ function serializeItems(items) {
885
+ try {
886
+ return JSON.stringify(sanitizeRealtimeItems(items));
887
+ } catch {
888
+ return null;
889
+ }
890
+ }
891
+ function sameSerializedItems(left, right) {
892
+ if (left === right) return true;
893
+ if (!left || !right) return false;
894
+ const leftJson = serializeItems(left);
895
+ const rightJson = serializeItems(right);
896
+ return leftJson != null && leftJson === rightJson;
897
+ }
898
+ function nowMs() {
899
+ return Date.now();
900
+ }
901
+ function hasDurableDocumentPersistence(snapshot) {
902
+ return snapshot.persistedRevision == null || snapshot.persistedRevision >= snapshot.revision;
903
+ }
904
+ function draftStorageKey(roomId) {
905
+ return `${DRAFT_STORAGE_PREFIX}${roomId}`;
906
+ }
907
+ function isRealtimeOfflineDraft(value) {
908
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
909
+ const record = value;
910
+ if (typeof record.roomId !== "string" || typeof record.baseRevision !== "number" || typeof record.updatedAt !== "number" || !Array.isArray(record.items)) {
911
+ return false;
912
+ }
913
+ if (record.yDocState != null && typeof record.yDocState !== "string") {
914
+ return false;
915
+ }
916
+ if (record.pendingIds != null && !Array.isArray(record.pendingIds)) {
917
+ return false;
918
+ }
919
+ return true;
920
+ }
921
+ async function readRealtimeOfflineDraft(roomId, storage) {
922
+ if (!storage || !roomId) return null;
923
+ try {
924
+ const raw = await storage.getItem(draftStorageKey(roomId));
925
+ if (!raw) return null;
926
+ const parsed = JSON.parse(raw);
927
+ if (!isRealtimeOfflineDraft(parsed)) return null;
928
+ return {
929
+ ...parsed,
930
+ items: sanitizeRealtimeItems(parsed.items)
931
+ };
932
+ } catch {
933
+ return null;
934
+ }
935
+ }
936
+ async function writeRealtimeOfflineDraft(draft, storage) {
937
+ if (!storage) return;
938
+ if (!draft) return;
939
+ try {
940
+ await storage.setItem(
941
+ draftStorageKey(draft.roomId),
942
+ JSON.stringify({
943
+ ...draft,
944
+ items: sanitizeRealtimeItems(draft.items)
945
+ })
946
+ );
947
+ } catch {
948
+ }
949
+ }
950
+ async function removeRealtimeOfflineDraft(roomId, storage) {
951
+ if (!storage || !roomId) return;
952
+ try {
953
+ await storage.removeItem(draftStorageKey(roomId));
954
+ } catch {
955
+ }
956
+ }
957
+ function getViewportCameraSnapshot(viewport) {
958
+ if (!viewport) return null;
959
+ const camera = viewport.getCamera();
960
+ if (!camera) return null;
961
+ const viewportSize = viewport.getViewportSize();
962
+ return {
963
+ x: camera.x,
964
+ y: camera.y,
965
+ zoom: camera.zoom,
966
+ viewportWidth: viewportSize.width,
967
+ viewportHeight: viewportSize.height
968
+ };
969
+ }
970
+ function prepareRealtimeItems(items) {
971
+ try {
972
+ const sanitizedItems = sanitizeRealtimeItems(items);
973
+ return {
974
+ items: sanitizedItems,
975
+ serialized: JSON.stringify(sanitizedItems)
976
+ };
977
+ } catch {
978
+ return null;
979
+ }
980
+ }
981
+ function getRealtimeItemId(item) {
982
+ const id = item.id;
983
+ return typeof id === "string" ? id : null;
984
+ }
985
+ function serializeRealtimeItem(item) {
986
+ try {
987
+ return JSON.stringify(sanitizeRealtimeItem(item));
988
+ } catch {
989
+ return null;
990
+ }
991
+ }
992
+ function resolvePendingDraftItemIds(board, items) {
993
+ const pendingIds = /* @__PURE__ */ new Set();
994
+ for (const item of items) {
995
+ const id = getRealtimeItemId(item);
996
+ if (!id) continue;
997
+ const confirmedSerialized = board.lastServerConfirmedItemSerializations.get(id);
998
+ const serialized = serializeRealtimeItem(item);
999
+ if (!confirmedSerialized || !serialized || serialized !== confirmedSerialized) {
1000
+ pendingIds.add(id);
1001
+ }
1002
+ }
1003
+ return pendingIds.size > 0 ? Array.from(pendingIds) : void 0;
1004
+ }
1005
+ function remoteMarkupStrokeFromPlacementPreview(preview) {
1006
+ if (!preview || preview.kind !== "stroke" || preview.points.length === 0) {
1007
+ return null;
1008
+ }
1009
+ const style = preview.style;
1010
+ return {
1011
+ points: preview.points,
1012
+ tool: preview.tool,
1013
+ ...style?.strokeWidth != null ? { strokeWidth: style.strokeWidth } : {},
1014
+ ...style?.stroke ? { stroke: style.stroke } : {},
1015
+ ...style?.strokeOpacity != null ? { strokeOpacity: style.strokeOpacity } : {}
1016
+ };
1017
+ }
1018
+ function useRealtimeSession(options) {
1019
+ const {
1020
+ url,
1021
+ roomId,
1022
+ peer,
1023
+ enabled = true,
1024
+ draftStorage = createBrowserDraftStorage(),
1025
+ reconnect = true,
1026
+ heartbeatMs = 1e4,
1027
+ maxReconnectDelayMs = 12e3,
1028
+ initialReconnectDelayMs = 800,
1029
+ connectTimeoutMs = 1e4,
1030
+ onError
1031
+ } = options;
1032
+ const clientIdRef = useRef(peer.clientId ?? createClientId());
1033
+ const wsRef = useRef(null);
1034
+ const reconnectTimerRef = useRef(null);
1035
+ const heartbeatTimerRef = useRef(null);
1036
+ const connectTimeoutRef = useRef(null);
1037
+ const documentFlushTimerRef = useRef(null);
1038
+ const documentFlushIdleRef = useRef(null);
1039
+ const draftPersistTimerRef = useRef(null);
1040
+ const draftPersistIdleRef = useRef(null);
1041
+ const draftStorageRef = useRef(draftStorage);
1042
+ draftStorageRef.current = draftStorage;
1043
+ const manualDisconnectRef = useRef(false);
1044
+ const retryCountRef = useRef(0);
1045
+ const currentRevisionRef = useRef(0);
1046
+ const outboundInFlightRef = useRef(null);
1047
+ const queuedDirtyRef = useRef(false);
1048
+ const pendingLocalItemsRef = useRef(null);
1049
+ const subscriberRefs = useRef(/* @__PURE__ */ new Set());
1050
+ const boardRef = useRef(null);
1051
+ if (boardRef.current == null) {
1052
+ boardRef.current = createYjsBoardDoc();
1053
+ }
1054
+ const lastCursorRef = useRef(null);
1055
+ const lastMarkupStrokeRef = useRef(null);
1056
+ const lastCameraRef = useRef(
1057
+ null
1058
+ );
1059
+ const lastActiveToolRef = useRef(void 0);
1060
+ const latestDocumentRef = useRef(null);
1061
+ const connectionStateRef = useRef(
1062
+ enabled ? "connecting" : "offline"
1063
+ );
1064
+ const localDraftRef = useRef(null);
1065
+ const conflictRef = useRef(null);
1066
+ const onErrorRef = useRef(onError);
1067
+ onErrorRef.current = onError;
1068
+ const [connectSequence, setConnectSequence] = useState(0);
1069
+ const [connection, setConnection] = useState({
1070
+ state: enabled ? "connecting" : "offline",
1071
+ connected: false,
1072
+ roomId,
1073
+ clientId: null,
1074
+ retryCount: 0,
1075
+ lastConnectedAt: null,
1076
+ lastMessageAt: null,
1077
+ lastPongAt: null,
1078
+ lastError: null
1079
+ });
1080
+ const [sessionPeers, setSessionPeers] = useState([]);
1081
+ const [document, setDocument] = useState(null);
1082
+ const [hasLocalOfflineDraft, setHasLocalOfflineDraft] = useState(false);
1083
+ const [hasPendingDocumentSync, setHasPendingDocumentSync] = useState(false);
1084
+ const [conflict, setConflict] = useState(null);
1085
+ connectionStateRef.current = connection.state;
1086
+ const syncState = useMemo(() => {
1087
+ if (conflict != null) return "conflicted";
1088
+ if (connection.connected) return "connected";
1089
+ if (connection.state === "reconnecting" || connection.state === "connecting") {
1090
+ return "reconnecting";
1091
+ }
1092
+ return "offline";
1093
+ }, [conflict, connection.connected, connection.state]);
1094
+ const clearReconnectTimer = useCallback(() => {
1095
+ if (reconnectTimerRef.current != null) {
1096
+ globalThis.clearTimeout(reconnectTimerRef.current);
1097
+ reconnectTimerRef.current = null;
1098
+ }
1099
+ }, []);
1100
+ const clearHeartbeatTimer = useCallback(() => {
1101
+ if (heartbeatTimerRef.current != null) {
1102
+ globalThis.clearInterval(heartbeatTimerRef.current);
1103
+ heartbeatTimerRef.current = null;
1104
+ }
1105
+ }, []);
1106
+ const clearConnectTimeout = useCallback(() => {
1107
+ if (connectTimeoutRef.current != null) {
1108
+ globalThis.clearTimeout(connectTimeoutRef.current);
1109
+ connectTimeoutRef.current = null;
1110
+ }
1111
+ }, []);
1112
+ const clearDocumentFlushSchedule = useCallback(() => {
1113
+ if (documentFlushTimerRef.current != null) {
1114
+ globalThis.clearTimeout(documentFlushTimerRef.current);
1115
+ documentFlushTimerRef.current = null;
1116
+ }
1117
+ cancelRuntimeIdleCallback(documentFlushIdleRef.current);
1118
+ documentFlushIdleRef.current = null;
1119
+ }, []);
1120
+ const clearDraftPersistSchedule = useCallback(() => {
1121
+ if (draftPersistTimerRef.current != null) {
1122
+ globalThis.clearTimeout(draftPersistTimerRef.current);
1123
+ draftPersistTimerRef.current = null;
1124
+ }
1125
+ cancelRuntimeIdleCallback(draftPersistIdleRef.current);
1126
+ draftPersistIdleRef.current = null;
1127
+ }, []);
1128
+ const updateConnection = useCallback(
1129
+ (patch) => {
1130
+ setConnection((prev) => {
1131
+ if (typeof patch === "function") return patch(prev);
1132
+ return { ...prev, ...patch };
1133
+ });
1134
+ },
1135
+ []
1136
+ );
1137
+ const notifySubscribers = useCallback((items) => {
1138
+ for (const subscriber of subscriberRefs.current) {
1139
+ subscriber(items);
1140
+ }
1141
+ }, []);
1142
+ const applyDocument = useCallback(
1143
+ (snapshot, options2) => {
1144
+ const currentSnapshot = latestDocumentRef.current;
1145
+ if (currentSnapshot && currentSnapshot.revision === snapshot.revision && currentSnapshot.updatedByClientId === snapshot.updatedByClientId && currentSnapshot.persistedRevision === snapshot.persistedRevision && sameSerializedItems(currentSnapshot.items, snapshot.items)) {
1146
+ return;
1147
+ }
1148
+ const board = boardRef.current;
1149
+ if (board) {
1150
+ if (!options2?.replace) {
1151
+ const pendingLocal = pendingLocalItemsRef.current;
1152
+ if (pendingLocal) {
1153
+ pendingLocalItemsRef.current = null;
1154
+ applyLocalItemsToYDoc(board, {
1155
+ items: pendingLocal,
1156
+ origin: ORIGIN_LOCAL
1157
+ });
1158
+ queuedDirtyRef.current = true;
1159
+ }
1160
+ }
1161
+ if (options2?.replace) {
1162
+ replaceYDocWithSnapshot(board, {
1163
+ items: snapshot.items,
1164
+ origin: ORIGIN_REMOTE
1165
+ });
1166
+ } else {
1167
+ applyServerSnapshotToYDoc(board, {
1168
+ items: snapshot.items,
1169
+ origin: ORIGIN_REMOTE
1170
+ });
1171
+ }
1172
+ }
1173
+ const mergedItems = board ? readVectorItems(board.yItems) : snapshot.items;
1174
+ const mergedSnapshot = {
1175
+ ...snapshot,
1176
+ items: mergedItems
1177
+ };
1178
+ currentRevisionRef.current = snapshot.revision;
1179
+ latestDocumentRef.current = mergedSnapshot;
1180
+ setDocument(mergedSnapshot);
1181
+ if (!options2?.suppressSubscriberNotify) {
1182
+ notifySubscribers(mergedItems);
1183
+ }
1184
+ },
1185
+ [notifySubscribers]
1186
+ );
1187
+ const setConflictState = useCallback(
1188
+ (nextConflict) => {
1189
+ conflictRef.current = nextConflict;
1190
+ setConflict(nextConflict);
1191
+ },
1192
+ []
1193
+ );
1194
+ const setLocalDraft = useCallback(
1195
+ (nextDraft) => {
1196
+ localDraftRef.current = nextDraft;
1197
+ setHasLocalOfflineDraft(nextDraft != null);
1198
+ if (nextDraft) return;
1199
+ void removeRealtimeOfflineDraft(roomId, draftStorageRef.current);
1200
+ },
1201
+ [roomId]
1202
+ );
1203
+ const persistLocalDraft = useCallback(() => {
1204
+ clearDraftPersistSchedule();
1205
+ if (!localDraftRef.current) {
1206
+ void removeRealtimeOfflineDraft(roomId, draftStorageRef.current);
1207
+ return;
1208
+ }
1209
+ const board = boardRef.current;
1210
+ const draft = localDraftRef.current;
1211
+ const canEncodeYDocState = board && draft.yDocState == null && sameSerializedItems(readVectorItems(board.yItems), draft.items);
1212
+ const draftToWrite = canEncodeYDocState ? { ...draft, yDocState: encodeYDocState(board) } : draft;
1213
+ void writeRealtimeOfflineDraft(draftToWrite, draftStorageRef.current);
1214
+ }, [clearDraftPersistSchedule, roomId]);
1215
+ const scheduleDraftPersistence = useCallback(() => {
1216
+ clearDraftPersistSchedule();
1217
+ draftPersistTimerRef.current = globalThis.setTimeout(() => {
1218
+ draftPersistTimerRef.current = null;
1219
+ draftPersistIdleRef.current = requestRuntimeIdleCallback(() => {
1220
+ draftPersistIdleRef.current = null;
1221
+ persistLocalDraft();
1222
+ }, DRAFT_PERSIST_DEBOUNCE_MS);
1223
+ }, DRAFT_PERSIST_DEBOUNCE_MS);
1224
+ }, [clearDraftPersistSchedule, persistLocalDraft]);
1225
+ const clearLocalDraft = useCallback(() => {
1226
+ setLocalDraft(null);
1227
+ clearDraftPersistSchedule();
1228
+ }, [clearDraftPersistSchedule, setLocalDraft]);
1229
+ const createSelfPeer = useCallback(
1230
+ (state) => ({
1231
+ id: peer.id,
1232
+ clientId: clientIdRef.current,
1233
+ peerId: peer.id,
1234
+ roomId,
1235
+ joinedAt: connection.lastConnectedAt ?? nowMs(),
1236
+ lastSeenAt: nowMs(),
1237
+ cursor: lastCursorRef.current,
1238
+ isSelf: true,
1239
+ connectionState: state,
1240
+ ...peer.displayName ? { displayName: peer.displayName } : {},
1241
+ ...peer.color ? { color: peer.color } : {},
1242
+ ...peer.image ? { image: peer.image } : {},
1243
+ ...lastMarkupStrokeRef.current !== void 0 ? { markupStroke: lastMarkupStrokeRef.current ?? null } : {},
1244
+ ...lastCameraRef.current !== void 0 ? { camera: lastCameraRef.current ?? null } : {},
1245
+ ...lastActiveToolRef.current ? { activeTool: lastActiveToolRef.current } : {}
1246
+ }),
1247
+ [
1248
+ connection.lastConnectedAt,
1249
+ peer.color,
1250
+ peer.displayName,
1251
+ peer.id,
1252
+ peer.image,
1253
+ roomId
1254
+ ]
1255
+ );
1256
+ const collapsePeersToSelf = useCallback(
1257
+ (state) => {
1258
+ setSessionPeers((prev) => {
1259
+ const selfPeer = prev.find(
1260
+ (peerState) => peerState.clientId === clientIdRef.current
1261
+ );
1262
+ return [
1263
+ selfPeer ? { ...selfPeer, isSelf: true, connectionState: state } : createSelfPeer(state)
1264
+ ];
1265
+ });
1266
+ },
1267
+ [createSelfPeer]
1268
+ );
1269
+ const applyPeers = useCallback((peers) => {
1270
+ const selfClientId = clientIdRef.current;
1271
+ setSessionPeers(
1272
+ peers.map(
1273
+ (peerState) => peerState.clientId === selfClientId ? {
1274
+ ...peerState,
1275
+ isSelf: true,
1276
+ connectionState: connectionStateRef.current
1277
+ } : peerState
1278
+ )
1279
+ );
1280
+ }, []);
1281
+ const buildClientMessage = useCallback(
1282
+ (message) => JSON.stringify(message),
1283
+ []
1284
+ );
1285
+ const sendRaw = useCallback(
1286
+ (message) => {
1287
+ const ws = wsRef.current;
1288
+ const WebSocketConstructor = globalThis.WebSocket;
1289
+ if (!ws || !WebSocketConstructor) return false;
1290
+ if (ws.readyState !== WebSocketConstructor.OPEN) return false;
1291
+ ws.send(buildClientMessage(message));
1292
+ return true;
1293
+ },
1294
+ [buildClientMessage]
1295
+ );
1296
+ const flushQueuedDocument = useCallback(() => {
1297
+ clearDocumentFlushSchedule();
1298
+ const board = boardRef.current;
1299
+ if (!board) return;
1300
+ if (!queuedDirtyRef.current && pendingLocalItemsRef.current == null) return;
1301
+ if (outboundInFlightRef.current) return;
1302
+ const pendingLocal = pendingLocalItemsRef.current;
1303
+ if (pendingLocal) {
1304
+ pendingLocalItemsRef.current = null;
1305
+ applyLocalItemsToYDoc(board, {
1306
+ items: pendingLocal,
1307
+ origin: ORIGIN_LOCAL
1308
+ });
1309
+ }
1310
+ const baseRevision = currentRevisionRef.current;
1311
+ const mergedItems = readVectorItems(board.yItems);
1312
+ const preparedItems = prepareRealtimeItems(mergedItems);
1313
+ if (!preparedItems) return;
1314
+ queuedDirtyRef.current = false;
1315
+ outboundInFlightRef.current = {
1316
+ baseRevision,
1317
+ items: preparedItems.items,
1318
+ serialized: preparedItems.serialized
1319
+ };
1320
+ const didSend = sendRawRef.current({
1321
+ type: "document:update",
1322
+ roomId,
1323
+ clientId: clientIdRef.current,
1324
+ baseRevision,
1325
+ items: preparedItems.items
1326
+ });
1327
+ const pendingIds = getLocallyPendingItemIds(board);
1328
+ if (pendingIds.size > 0) {
1329
+ setLocalDraftRef.current({
1330
+ roomId,
1331
+ baseRevision,
1332
+ items: mergedItems,
1333
+ updatedAt: nowMs(),
1334
+ pendingIds: Array.from(pendingIds)
1335
+ });
1336
+ scheduleDraftPersistenceRef.current?.();
1337
+ }
1338
+ if (!didSend) {
1339
+ queuedDirtyRef.current = true;
1340
+ outboundInFlightRef.current = null;
1341
+ setHasPendingDocumentSync(true);
1342
+ }
1343
+ }, [clearDocumentFlushSchedule, roomId]);
1344
+ const scheduleDocumentFlush = useCallback(() => {
1345
+ clearDocumentFlushSchedule();
1346
+ documentFlushTimerRef.current = globalThis.setTimeout(() => {
1347
+ documentFlushTimerRef.current = null;
1348
+ documentFlushIdleRef.current = requestRuntimeIdleCallback(() => {
1349
+ documentFlushIdleRef.current = null;
1350
+ flushQueuedDocument();
1351
+ }, DOCUMENT_FLUSH_DEBOUNCE_MS);
1352
+ }, DOCUMENT_FLUSH_DEBOUNCE_MS);
1353
+ }, [clearDocumentFlushSchedule, flushQueuedDocument]);
1354
+ const queueDocumentSend = useCallback(
1355
+ (items) => {
1356
+ pendingLocalItemsRef.current = items;
1357
+ queuedDirtyRef.current = true;
1358
+ setHasPendingDocumentSync(true);
1359
+ setHasLocalOfflineDraft(true);
1360
+ const board = boardRef.current;
1361
+ const draftItems = sanitizeRealtimeItems(items);
1362
+ setLocalDraftRef.current({
1363
+ roomId,
1364
+ baseRevision: currentRevisionRef.current,
1365
+ items: draftItems,
1366
+ updatedAt: nowMs(),
1367
+ pendingIds: board ? resolvePendingDraftItemIds(board, draftItems) : void 0
1368
+ });
1369
+ scheduleDraftPersistenceRef.current?.();
1370
+ scheduleDocumentFlushRef.current();
1371
+ },
1372
+ [roomId]
1373
+ );
1374
+ const applyDraftSnapshot = useCallback(
1375
+ (draft, options2) => {
1376
+ const board = boardRef.current;
1377
+ if (board) {
1378
+ let restoredFromBinary = false;
1379
+ if (draft.yDocState) {
1380
+ if (board.yItems.length > 0) {
1381
+ board.doc.transact(() => {
1382
+ board.yItems.delete(0, board.yItems.length);
1383
+ }, ORIGIN_BOOTSTRAP);
1384
+ }
1385
+ restoredFromBinary = decodeYDocState(board, draft.yDocState);
1386
+ if (restoredFromBinary) {
1387
+ const allIds = /* @__PURE__ */ new Set();
1388
+ const allItems = readVectorItems(board.yItems);
1389
+ for (let i = 0; i < board.yItems.length; i++) {
1390
+ const yMap = board.yItems.get(i);
1391
+ if (!yMap) continue;
1392
+ const id = yMap.get("id");
1393
+ if (typeof id === "string") allIds.add(id);
1394
+ }
1395
+ const pendingIds = new Set(draft.pendingIds ?? []);
1396
+ board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
1397
+ board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
1398
+ for (const id of allIds) {
1399
+ if (!pendingIds.has(id)) {
1400
+ board.lastServerConfirmedIds.add(id);
1401
+ const item = allItems.find((candidate) => candidate.id === id);
1402
+ if (item) {
1403
+ board.lastServerConfirmedItemSerializations.set(
1404
+ id,
1405
+ JSON.stringify(item)
1406
+ );
1407
+ }
1408
+ }
1409
+ }
1410
+ }
1411
+ }
1412
+ if (!restoredFromBinary) {
1413
+ replaceYDocWithSnapshot(board, {
1414
+ items: draft.items,
1415
+ origin: ORIGIN_BOOTSTRAP
1416
+ });
1417
+ board.lastServerConfirmedIds = /* @__PURE__ */ new Set();
1418
+ board.lastServerConfirmedItemSerializations = /* @__PURE__ */ new Map();
1419
+ }
1420
+ }
1421
+ const items = board ? readVectorItems(board.yItems) : draft.items;
1422
+ const snapshot = {
1423
+ revision: draft.baseRevision,
1424
+ items,
1425
+ updatedAt: draft.updatedAt,
1426
+ updatedByClientId: clientIdRef.current
1427
+ };
1428
+ currentRevisionRef.current = snapshot.revision;
1429
+ latestDocumentRef.current = snapshot;
1430
+ setDocument(snapshot);
1431
+ if (!options2?.suppressSubscriberNotify) {
1432
+ notifySubscribers(items);
1433
+ }
1434
+ },
1435
+ [notifySubscribers]
1436
+ );
1437
+ const resolveAuthoritativeDocument = useCallback(
1438
+ (serverDocument, options2) => {
1439
+ const board = boardRef.current;
1440
+ const hadLocalContent = localDraftRef.current != null || board != null && board.yItems.length > 0;
1441
+ applyDocument(serverDocument, {
1442
+ ...options2,
1443
+ replace: !hadLocalContent
1444
+ });
1445
+ if (!board) {
1446
+ setHasPendingDocumentSync(false);
1447
+ clearLocalDraftRef.current();
1448
+ return false;
1449
+ }
1450
+ const pendingIds = getLocallyPendingItemIds(board);
1451
+ if (pendingIds.size === 0) {
1452
+ outboundInFlightRef.current = null;
1453
+ queuedDirtyRef.current = false;
1454
+ if (hasDurableDocumentPersistence(serverDocument)) {
1455
+ clearLocalDraftRef.current();
1456
+ setHasPendingDocumentSync(false);
1457
+ return false;
1458
+ }
1459
+ const mergedItems2 = readVectorItems(board.yItems);
1460
+ setLocalDraft({
1461
+ roomId,
1462
+ baseRevision: serverDocument.revision,
1463
+ items: mergedItems2,
1464
+ updatedAt: nowMs(),
1465
+ pendingIds: []
1466
+ });
1467
+ setHasPendingDocumentSync(true);
1468
+ return true;
1469
+ }
1470
+ const mergedItems = readVectorItems(board.yItems);
1471
+ setLocalDraft({
1472
+ roomId,
1473
+ baseRevision: serverDocument.revision,
1474
+ items: mergedItems,
1475
+ updatedAt: nowMs(),
1476
+ pendingIds: Array.from(pendingIds)
1477
+ });
1478
+ outboundInFlightRef.current = null;
1479
+ queuedDirtyRef.current = true;
1480
+ setHasPendingDocumentSync(true);
1481
+ scheduleDocumentFlush();
1482
+ return true;
1483
+ },
1484
+ [applyDocument, roomId, scheduleDocumentFlush, setLocalDraft]
1485
+ );
1486
+ const sendPresenceUpdate = useCallback(() => {
1487
+ sendRaw({
1488
+ type: "presence:update",
1489
+ roomId,
1490
+ clientId: clientIdRef.current,
1491
+ presence: {
1492
+ cursor: lastCursorRef.current,
1493
+ markupStroke: lastMarkupStrokeRef.current ?? null,
1494
+ camera: lastCameraRef.current ?? null,
1495
+ ...lastActiveToolRef.current ? { activeTool: lastActiveToolRef.current } : {}
1496
+ }
1497
+ });
1498
+ }, [roomId, sendRaw]);
1499
+ const scheduleReconnect = useCallback(() => {
1500
+ if (!reconnect || manualDisconnectRef.current) return;
1501
+ clearReconnectTimer();
1502
+ retryCountRef.current += 1;
1503
+ const delay = Math.min(
1504
+ maxReconnectDelayMs,
1505
+ initialReconnectDelayMs * 2 ** Math.max(0, retryCountRef.current - 1)
1506
+ );
1507
+ updateConnection((prev) => ({
1508
+ ...prev,
1509
+ state: "reconnecting",
1510
+ connected: false,
1511
+ retryCount: retryCountRef.current
1512
+ }));
1513
+ reconnectTimerRef.current = globalThis.setTimeout(() => {
1514
+ setConnectSequence((value) => value + 1);
1515
+ }, delay);
1516
+ }, [
1517
+ clearReconnectTimer,
1518
+ initialReconnectDelayMs,
1519
+ maxReconnectDelayMs,
1520
+ reconnect,
1521
+ updateConnection
1522
+ ]);
1523
+ const resolveConflict = useCallback((_action) => {
1524
+ }, []);
1525
+ const setConflictStateRef = useRef(setConflictState);
1526
+ setConflictStateRef.current = setConflictState;
1527
+ const updateConnectionRef = useRef(updateConnection);
1528
+ updateConnectionRef.current = updateConnection;
1529
+ const applyDocumentRef = useRef(applyDocument);
1530
+ applyDocumentRef.current = applyDocument;
1531
+ const clearLocalDraftRef = useRef(clearLocalDraft);
1532
+ clearLocalDraftRef.current = clearLocalDraft;
1533
+ const collapsePeersToSelfRef = useRef(collapsePeersToSelf);
1534
+ collapsePeersToSelfRef.current = collapsePeersToSelf;
1535
+ const applyPeersRef = useRef(applyPeers);
1536
+ applyPeersRef.current = applyPeers;
1537
+ const resolveAuthoritativeDocumentRef = useRef(resolveAuthoritativeDocument);
1538
+ resolveAuthoritativeDocumentRef.current = resolveAuthoritativeDocument;
1539
+ const scheduleDocumentFlushRef = useRef(scheduleDocumentFlush);
1540
+ scheduleDocumentFlushRef.current = scheduleDocumentFlush;
1541
+ const scheduleReconnectRef = useRef(scheduleReconnect);
1542
+ scheduleReconnectRef.current = scheduleReconnect;
1543
+ const sendRawRef = useRef(sendRaw);
1544
+ sendRawRef.current = sendRaw;
1545
+ const setLocalDraftRef = useRef(setLocalDraft);
1546
+ setLocalDraftRef.current = setLocalDraft;
1547
+ const scheduleDraftPersistenceRef = useRef(scheduleDraftPersistence);
1548
+ scheduleDraftPersistenceRef.current = scheduleDraftPersistence;
1549
+ const persistLocalDraftRef = useRef(persistLocalDraft);
1550
+ persistLocalDraftRef.current = persistLocalDraft;
1551
+ useEffect(() => {
1552
+ const runtimeWindow = globalThis.window;
1553
+ const runtimeDocument = runtimeWindow?.document;
1554
+ if (!runtimeWindow || !runtimeDocument) return;
1555
+ const persistDraft = () => {
1556
+ persistLocalDraftRef.current();
1557
+ };
1558
+ const handleVisibilityChange = () => {
1559
+ if (runtimeDocument.visibilityState === "hidden") persistDraft();
1560
+ };
1561
+ runtimeWindow.addEventListener("pagehide", persistDraft);
1562
+ runtimeDocument.addEventListener("visibilitychange", handleVisibilityChange);
1563
+ return () => {
1564
+ runtimeWindow.removeEventListener("pagehide", persistDraft);
1565
+ runtimeDocument.removeEventListener(
1566
+ "visibilitychange",
1567
+ handleVisibilityChange
1568
+ );
1569
+ };
1570
+ }, []);
1571
+ useEffect(() => {
1572
+ let cancelled = false;
1573
+ if (boardRef.current) {
1574
+ boardRef.current.doc.destroy();
1575
+ }
1576
+ boardRef.current = createYjsBoardDoc();
1577
+ if (!roomId) {
1578
+ clearDocumentFlushSchedule();
1579
+ clearDraftPersistSchedule();
1580
+ localDraftRef.current = null;
1581
+ setHasLocalOfflineDraft(false);
1582
+ setHasPendingDocumentSync(false);
1583
+ setConflictState(null);
1584
+ queuedDirtyRef.current = false;
1585
+ pendingLocalItemsRef.current = null;
1586
+ outboundInFlightRef.current = null;
1587
+ latestDocumentRef.current = null;
1588
+ setDocument(null);
1589
+ currentRevisionRef.current = 0;
1590
+ return;
1591
+ }
1592
+ setConflictState(null);
1593
+ pendingLocalItemsRef.current = null;
1594
+ outboundInFlightRef.current = null;
1595
+ void readRealtimeOfflineDraft(roomId, draftStorageRef.current).then(
1596
+ (localDraft) => {
1597
+ if (cancelled) return;
1598
+ setLocalDraft(localDraft);
1599
+ setHasPendingDocumentSync(localDraft != null);
1600
+ queuedDirtyRef.current = localDraft != null;
1601
+ if (localDraft) {
1602
+ applyDraftSnapshot(localDraft, {
1603
+ suppressSubscriberNotify: sameSerializedItems(
1604
+ latestDocumentRef.current?.items,
1605
+ localDraft.items
1606
+ )
1607
+ });
1608
+ return;
1609
+ }
1610
+ latestDocumentRef.current = null;
1611
+ setDocument(null);
1612
+ currentRevisionRef.current = 0;
1613
+ }
1614
+ );
1615
+ return () => {
1616
+ cancelled = true;
1617
+ };
1618
+ }, [
1619
+ applyDraftSnapshot,
1620
+ clearDocumentFlushSchedule,
1621
+ clearDraftPersistSchedule,
1622
+ roomId,
1623
+ setConflictState,
1624
+ setLocalDraft
1625
+ ]);
1626
+ useEffect(() => {
1627
+ if (!enabled) {
1628
+ manualDisconnectRef.current = true;
1629
+ clearReconnectTimer();
1630
+ clearHeartbeatTimer();
1631
+ clearConnectTimeout();
1632
+ clearDocumentFlushSchedule();
1633
+ wsRef.current?.close();
1634
+ wsRef.current = null;
1635
+ queuedDirtyRef.current = localDraftRef.current != null;
1636
+ outboundInFlightRef.current = null;
1637
+ setHasPendingDocumentSync(localDraftRef.current != null);
1638
+ collapsePeersToSelfRef.current("offline");
1639
+ updateConnectionRef.current((prev) => ({
1640
+ ...prev,
1641
+ state: "offline",
1642
+ connected: false,
1643
+ roomId,
1644
+ clientId: null
1645
+ }));
1646
+ return;
1647
+ }
1648
+ manualDisconnectRef.current = false;
1649
+ const socketUrl = normalizeSocketUrl(url);
1650
+ if (!socketUrl) {
1651
+ collapsePeersToSelfRef.current("offline");
1652
+ updateConnectionRef.current((prev) => ({
1653
+ ...prev,
1654
+ state: "offline",
1655
+ connected: false,
1656
+ roomId,
1657
+ lastError: null
1658
+ }));
1659
+ return;
1660
+ }
1661
+ if (!isValidSocketUrl(socketUrl)) {
1662
+ updateConnectionRef.current((prev) => ({
1663
+ ...prev,
1664
+ state: "error",
1665
+ connected: false,
1666
+ roomId,
1667
+ lastError: "URL de websocket invalida."
1668
+ }));
1669
+ onErrorRef.current?.("URL de websocket invalida.");
1670
+ return;
1671
+ }
1672
+ const WebSocketConstructor = globalThis.WebSocket;
1673
+ if (!WebSocketConstructor) {
1674
+ updateConnectionRef.current((prev) => ({
1675
+ ...prev,
1676
+ state: "error",
1677
+ connected: false,
1678
+ roomId,
1679
+ lastError: "WebSocket nao esta disponivel neste runtime."
1680
+ }));
1681
+ onErrorRef.current?.("WebSocket nao esta disponivel neste runtime.");
1682
+ return;
1683
+ }
1684
+ let disposed = false;
1685
+ updateConnectionRef.current((prev) => ({
1686
+ ...prev,
1687
+ state: retryCountRef.current > 0 ? "reconnecting" : "connecting",
1688
+ connected: false,
1689
+ roomId,
1690
+ lastError: null
1691
+ }));
1692
+ const socket = new WebSocketConstructor(socketUrl);
1693
+ wsRef.current = socket;
1694
+ clearConnectTimeout();
1695
+ connectTimeoutRef.current = globalThis.setTimeout(() => {
1696
+ if (socket.readyState === WebSocketConstructor.CONNECTING) {
1697
+ socket.close();
1698
+ }
1699
+ }, connectTimeoutMs);
1700
+ const removeSocketListeners = [
1701
+ addRealtimeSocketListener(socket, "open", () => {
1702
+ if (disposed) return;
1703
+ clearConnectTimeout();
1704
+ sendRawRef.current({
1705
+ type: "session:join",
1706
+ roomId,
1707
+ peer: {
1708
+ clientId: clientIdRef.current,
1709
+ peerId: peer.id,
1710
+ ...peer.displayName ? { displayName: peer.displayName } : {},
1711
+ ...peer.color ? { color: peer.color } : {},
1712
+ ...peer.image ? { image: peer.image } : {}
1713
+ }
1714
+ });
1715
+ clearHeartbeatTimer();
1716
+ heartbeatTimerRef.current = globalThis.setInterval(() => {
1717
+ sendRawRef.current({
1718
+ type: "session:ping",
1719
+ roomId,
1720
+ clientId: clientIdRef.current,
1721
+ sentAt: nowMs()
1722
+ });
1723
+ }, heartbeatMs);
1724
+ }),
1725
+ addRealtimeSocketListener(socket, "message", (event) => {
1726
+ if (disposed) return;
1727
+ if (!("data" in event)) return;
1728
+ let payload = event.data;
1729
+ if (typeof payload === "string") {
1730
+ try {
1731
+ payload = JSON.parse(payload);
1732
+ } catch {
1733
+ return;
1734
+ }
1735
+ }
1736
+ const parsed = parseRealtimeServerMessage(payload);
1737
+ if (!parsed) return;
1738
+ updateConnectionRef.current((prev) => ({
1739
+ ...prev,
1740
+ lastMessageAt: nowMs()
1741
+ }));
1742
+ if (parsed.type === "session:welcome") {
1743
+ retryCountRef.current = 0;
1744
+ updateConnectionRef.current((prev) => ({
1745
+ ...prev,
1746
+ state: "connected",
1747
+ connected: true,
1748
+ clientId: parsed.clientId,
1749
+ retryCount: 0,
1750
+ lastConnectedAt: nowMs(),
1751
+ lastError: null
1752
+ }));
1753
+ applyPeersRef.current(parsed.peers);
1754
+ const handledByDraft = resolveAuthoritativeDocumentRef.current(
1755
+ sanitizeRealtimeSnapshot(parsed.document),
1756
+ {
1757
+ suppressSubscriberNotify: localDraftRef.current != null && sameSerializedItems(
1758
+ latestDocumentRef.current?.items,
1759
+ localDraftRef.current.items
1760
+ )
1761
+ }
1762
+ );
1763
+ if (!handledByDraft) {
1764
+ queuedDirtyRef.current = false;
1765
+ outboundInFlightRef.current = null;
1766
+ setHasPendingDocumentSync(false);
1767
+ }
1768
+ scheduleDocumentFlushRef.current();
1769
+ return;
1770
+ }
1771
+ if (parsed.type === "presence:sync") {
1772
+ applyPeersRef.current(parsed.peers);
1773
+ return;
1774
+ }
1775
+ if (parsed.type === "session:peer-joined") {
1776
+ setSessionPeers((prev) => {
1777
+ const next = prev.filter(
1778
+ (peerState) => peerState.clientId !== parsed.peer.clientId
1779
+ );
1780
+ next.push(
1781
+ parsed.peer.clientId === clientIdRef.current ? { ...parsed.peer, isSelf: true } : parsed.peer
1782
+ );
1783
+ return next;
1784
+ });
1785
+ return;
1786
+ }
1787
+ if (parsed.type === "session:peer-left") {
1788
+ setSessionPeers(
1789
+ (prev) => prev.filter((peerState) => peerState.clientId !== parsed.clientId)
1790
+ );
1791
+ return;
1792
+ }
1793
+ if (parsed.type === "session:pong") {
1794
+ updateConnectionRef.current((prev) => ({
1795
+ ...prev,
1796
+ lastPongAt: parsed.serverTime
1797
+ }));
1798
+ return;
1799
+ }
1800
+ if (parsed.type === "session:error") {
1801
+ updateConnectionRef.current((prev) => ({
1802
+ ...prev,
1803
+ state: prev.connected ? prev.state : "error",
1804
+ lastError: parsed.message
1805
+ }));
1806
+ onErrorRef.current?.(parsed.message);
1807
+ return;
1808
+ }
1809
+ if (parsed.type === "document:sync") {
1810
+ const selfClientId = clientIdRef.current;
1811
+ const inFlight = outboundInFlightRef.current;
1812
+ const isSelfAck = parsed.document.updatedByClientId === selfClientId;
1813
+ if (!isSelfAck) {
1814
+ outboundInFlightRef.current = null;
1815
+ resolveAuthoritativeDocumentRef.current(
1816
+ sanitizeRealtimeSnapshot(parsed.document)
1817
+ );
1818
+ return;
1819
+ }
1820
+ const shouldSuppress = inFlight != null && sameSerializedItems(inFlight.items, parsed.document.items);
1821
+ outboundInFlightRef.current = null;
1822
+ applyDocumentRef.current(sanitizeRealtimeSnapshot(parsed.document), {
1823
+ suppressSubscriberNotify: shouldSuppress
1824
+ });
1825
+ const board = boardRef.current;
1826
+ const stillPending = board ? getLocallyPendingItemIds(board).size > 0 : false;
1827
+ if (stillPending) {
1828
+ const mergedItems = board ? readVectorItems(board.yItems) : [];
1829
+ setLocalDraftRef.current({
1830
+ roomId,
1831
+ baseRevision: currentRevisionRef.current,
1832
+ items: mergedItems,
1833
+ updatedAt: nowMs(),
1834
+ pendingIds: board ? Array.from(getLocallyPendingItemIds(board)) : void 0
1835
+ });
1836
+ queuedDirtyRef.current = true;
1837
+ setHasPendingDocumentSync(true);
1838
+ scheduleDocumentFlushRef.current();
1839
+ } else {
1840
+ queuedDirtyRef.current = false;
1841
+ if (hasDurableDocumentPersistence(parsed.document)) {
1842
+ clearLocalDraftRef.current();
1843
+ setHasPendingDocumentSync(false);
1844
+ } else {
1845
+ const mergedItems = board ? readVectorItems(board.yItems) : [];
1846
+ setLocalDraftRef.current({
1847
+ roomId,
1848
+ baseRevision: currentRevisionRef.current,
1849
+ items: mergedItems,
1850
+ updatedAt: nowMs(),
1851
+ pendingIds: []
1852
+ });
1853
+ setHasPendingDocumentSync(true);
1854
+ }
1855
+ }
1856
+ setConflictStateRef.current(null);
1857
+ return;
1858
+ }
1859
+ if (parsed.type === "document:resync-required") {
1860
+ outboundInFlightRef.current = null;
1861
+ queuedDirtyRef.current = localDraftRef.current != null;
1862
+ setHasPendingDocumentSync(queuedDirtyRef.current);
1863
+ updateConnectionRef.current((prev) => ({
1864
+ ...prev,
1865
+ lastError: parsed.reason
1866
+ }));
1867
+ resolveAuthoritativeDocumentRef.current(
1868
+ sanitizeRealtimeSnapshot(parsed.document)
1869
+ );
1870
+ }
1871
+ }),
1872
+ addRealtimeSocketListener(socket, "error", () => {
1873
+ if (disposed) return;
1874
+ updateConnectionRef.current((prev) => ({
1875
+ ...prev,
1876
+ state: prev.connected ? prev.state : "error",
1877
+ lastError: "Falha de conex\xE3o websocket."
1878
+ }));
1879
+ }),
1880
+ addRealtimeSocketListener(socket, "close", () => {
1881
+ if (disposed) return;
1882
+ clearHeartbeatTimer();
1883
+ clearConnectTimeout();
1884
+ wsRef.current = null;
1885
+ collapsePeersToSelfRef.current(
1886
+ manualDisconnectRef.current || !enabled ? "offline" : "reconnecting"
1887
+ );
1888
+ updateConnectionRef.current((prev) => ({
1889
+ ...prev,
1890
+ connected: false,
1891
+ clientId: prev.clientId,
1892
+ state: manualDisconnectRef.current || !enabled ? "offline" : prev.state
1893
+ }));
1894
+ if (!manualDisconnectRef.current && enabled) {
1895
+ scheduleReconnectRef.current();
1896
+ }
1897
+ })
1898
+ ];
1899
+ return () => {
1900
+ disposed = true;
1901
+ for (const removeSocketListener of removeSocketListeners) {
1902
+ removeSocketListener();
1903
+ }
1904
+ clearReconnectTimer();
1905
+ clearHeartbeatTimer();
1906
+ clearConnectTimeout();
1907
+ clearDocumentFlushSchedule();
1908
+ socket.close();
1909
+ };
1910
+ }, [
1911
+ clearConnectTimeout,
1912
+ clearDocumentFlushSchedule,
1913
+ clearHeartbeatTimer,
1914
+ clearReconnectTimer,
1915
+ connectSequence,
1916
+ connectTimeoutMs,
1917
+ enabled,
1918
+ heartbeatMs,
1919
+ peer.color,
1920
+ peer.displayName,
1921
+ peer.id,
1922
+ peer.image,
1923
+ roomId,
1924
+ url
1925
+ ]);
1926
+ useEffect(() => {
1927
+ setSessionPeers(
1928
+ (prev) => prev.map(
1929
+ (peerState) => peerState.clientId === clientIdRef.current ? {
1930
+ ...peerState,
1931
+ isSelf: true,
1932
+ connectionState: connection.state
1933
+ } : peerState
1934
+ )
1935
+ );
1936
+ }, [connection.state]);
1937
+ useEffect(
1938
+ () => () => {
1939
+ clearDocumentFlushSchedule();
1940
+ clearDraftPersistSchedule();
1941
+ if (boardRef.current) {
1942
+ boardRef.current.doc.destroy();
1943
+ boardRef.current = null;
1944
+ }
1945
+ },
1946
+ [clearDocumentFlushSchedule, clearDraftPersistSchedule]
1947
+ );
1948
+ const flushDocumentSync = useCallback(async () => {
1949
+ const board = boardRef.current;
1950
+ const pendingLocal = pendingLocalItemsRef.current;
1951
+ if (board && pendingLocal) {
1952
+ pendingLocalItemsRef.current = null;
1953
+ applyLocalItemsToYDoc(board, {
1954
+ items: pendingLocal,
1955
+ origin: ORIGIN_LOCAL
1956
+ });
1957
+ const mergedItems = readVectorItems(board.yItems);
1958
+ const pendingIds = getLocallyPendingItemIds(board);
1959
+ if (pendingIds.size > 0) {
1960
+ setLocalDraftRef.current({
1961
+ roomId,
1962
+ baseRevision: currentRevisionRef.current,
1963
+ items: mergedItems,
1964
+ updatedAt: nowMs(),
1965
+ pendingIds: Array.from(pendingIds)
1966
+ });
1967
+ }
1968
+ }
1969
+ persistLocalDraft();
1970
+ if (!connection.connected) return;
1971
+ flushQueuedDocument();
1972
+ }, [connection.connected, flushQueuedDocument, persistLocalDraft, roomId]);
1973
+ const remoteAdapter = useMemo(
1974
+ () => ({
1975
+ subscribe(onItems) {
1976
+ subscriberRefs.current.add(onItems);
1977
+ if (latestDocumentRef.current) {
1978
+ onItems(latestDocumentRef.current.items);
1979
+ }
1980
+ return () => {
1981
+ subscriberRefs.current.delete(onItems);
1982
+ };
1983
+ },
1984
+ send(items) {
1985
+ queueDocumentSend(items);
1986
+ },
1987
+ flush() {
1988
+ return flushDocumentSync();
1989
+ }
1990
+ }),
1991
+ [flushDocumentSync, queueDocumentSend]
1992
+ );
1993
+ const disconnect = useCallback(() => {
1994
+ manualDisconnectRef.current = true;
1995
+ clearReconnectTimer();
1996
+ clearHeartbeatTimer();
1997
+ clearConnectTimeout();
1998
+ sendRaw({
1999
+ type: "session:leave",
2000
+ roomId,
2001
+ clientId: clientIdRef.current
2002
+ });
2003
+ wsRef.current?.close();
2004
+ wsRef.current = null;
2005
+ collapsePeersToSelf("offline");
2006
+ updateConnection((prev) => ({
2007
+ ...prev,
2008
+ state: "offline",
2009
+ connected: false
2010
+ }));
2011
+ }, [
2012
+ clearConnectTimeout,
2013
+ clearHeartbeatTimer,
2014
+ clearReconnectTimer,
2015
+ collapsePeersToSelf,
2016
+ roomId,
2017
+ sendRaw,
2018
+ updateConnection
2019
+ ]);
2020
+ const reconnectNow = useCallback(() => {
2021
+ disconnect();
2022
+ manualDisconnectRef.current = false;
2023
+ retryCountRef.current = 0;
2024
+ setConnectSequence((value) => value + 1);
2025
+ }, [disconnect]);
2026
+ const remotePresence = useMemo(
2027
+ () => sessionPeers.filter((peerState) => !peerState.isSelf),
2028
+ [sessionPeers]
2029
+ );
2030
+ const syncViewportPresence = useCallback(
2031
+ (bindingOptions) => {
2032
+ const viewport = bindingOptions?.viewportRef?.current;
2033
+ const cameraSnapshot = getViewportCameraSnapshot(viewport);
2034
+ if (!cameraSnapshot) return false;
2035
+ lastCameraRef.current = cameraSnapshot;
2036
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2037
+ sendPresenceUpdate();
2038
+ return true;
2039
+ },
2040
+ [sendPresenceUpdate]
2041
+ );
2042
+ const bindViewportPresence = useCallback(
2043
+ (bindingOptions) => ({
2044
+ remotePresence,
2045
+ onWorldPointerMove(world) {
2046
+ lastCursorRef.current = world;
2047
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2048
+ sendPresenceUpdate();
2049
+ },
2050
+ onWorldPointerLeave() {
2051
+ lastCursorRef.current = null;
2052
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2053
+ sendPresenceUpdate();
2054
+ },
2055
+ onPlacementPreviewChange(preview) {
2056
+ lastMarkupStrokeRef.current = remoteMarkupStrokeFromPlacementPreview(preview);
2057
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2058
+ sendPresenceUpdate();
2059
+ },
2060
+ onCameraChange() {
2061
+ const viewport = bindingOptions?.viewportRef?.current;
2062
+ const cameraSnapshot = getViewportCameraSnapshot(viewport);
2063
+ if (!cameraSnapshot) return;
2064
+ if (viewport && consumeViewportFollowSync(viewport, cameraSnapshot)) {
2065
+ lastCameraRef.current = cameraSnapshot;
2066
+ return;
2067
+ }
2068
+ lastCameraRef.current = cameraSnapshot;
2069
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2070
+ sendPresenceUpdate();
2071
+ }
2072
+ }),
2073
+ [remotePresence, sendPresenceUpdate]
2074
+ );
2075
+ return {
2076
+ connection,
2077
+ sessionPeers,
2078
+ remotePresence,
2079
+ remoteAdapter,
2080
+ document,
2081
+ hasLocalOfflineDraft,
2082
+ hasPendingDocumentSync,
2083
+ syncState,
2084
+ conflict,
2085
+ bindViewportPresence,
2086
+ syncViewportPresence,
2087
+ resolveConflict,
2088
+ clearLocalDraft,
2089
+ flushDocumentSync,
2090
+ disconnect,
2091
+ reconnectNow
2092
+ };
2093
+ }
2094
+
2095
+ export { createBrowserDraftStorage, parseRealtimeClientMessage, parseRealtimeServerMessage, useRealtimeCanvasDocument, useRealtimePeerFollow, useRealtimeSession };
2096
+ //# sourceMappingURL=realtimeNative.js.map
2097
+ //# sourceMappingURL=realtimeNative.js.map