canvu-react 0.4.33 → 0.4.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chatbot.d.cts +2 -1
- package/dist/chatbot.d.ts +2 -1
- package/dist/native.cjs +239 -23
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +10 -1
- package/dist/native.d.ts +10 -1
- package/dist/native.js +240 -24
- package/dist/native.js.map +1 -1
- package/dist/react.d.cts +3 -2
- package/dist/react.d.ts +3 -2
- package/dist/realtime.cjs +18 -3
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.d.cts +3 -2
- package/dist/realtime.d.ts +3 -2
- package/dist/realtime.js +18 -3
- package/dist/realtime.js.map +1 -1
- package/dist/realtimeNative.cjs +2124 -0
- package/dist/realtimeNative.cjs.map +1 -0
- package/dist/realtimeNative.d.cts +255 -0
- package/dist/realtimeNative.d.ts +255 -0
- package/dist/realtimeNative.js +2097 -0
- package/dist/realtimeNative.js.map +1 -0
- package/dist/types-B82WiQQh.d.ts +69 -0
- package/dist/types-BQUbxMgz.d.cts +69 -0
- package/dist/{types-UZYYwK-v.d.ts → types-DeDm865m.d.ts} +2 -67
- package/dist/{types-BS-YG8Hx.d.cts → types-NBYvslB-.d.cts} +2 -67
- package/package.json +7 -1
|
@@ -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
|