@edge-base/web 0.2.6 → 0.2.8

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,675 @@
1
+ import { EdgeBaseError, createSubscription } from '@edge-base/core';
2
+ const ROOM_COLLAB_MEMBER_STATE_KEY = '__collab';
3
+ const ROOM_COLLAB_MEMBER_META_KEY = '__collab_meta';
4
+ const ROOM_COLLAB_CONTROL_EVENT = 'collab.control';
5
+ const ROOM_COLLAB_UPDATE_EVENT = 'collab.update';
6
+ const ROOM_COLLAB_SYNC_REQUEST_EVENT = 'collab.sync_request';
7
+ const ROOM_COLLAB_SYNC_RESPONSE_EVENT = 'collab.sync_response';
8
+ const DEFAULT_SYNC_TIMEOUT_MS = 1500;
9
+ const ROOM_COLLAB_SYNC_SOURCE_SERVER = 'server_durable';
10
+ const ROOM_COLLAB_SYNC_SOURCE_PEER = 'peer_live';
11
+ let yjsRuntimePromise = null;
12
+ function isRecord(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+ function cloneRecord(value) {
16
+ if (typeof structuredClone === 'function') {
17
+ return structuredClone(value);
18
+ }
19
+ return JSON.parse(JSON.stringify(value ?? {}));
20
+ }
21
+ function scopedCollabKey(format, key) {
22
+ return `${format}:${key}`;
23
+ }
24
+ function encodeBytesToBase64(bytes) {
25
+ let binary = '';
26
+ for (const byte of bytes) {
27
+ binary += String.fromCharCode(byte);
28
+ }
29
+ if (typeof btoa === 'function') {
30
+ return btoa(binary);
31
+ }
32
+ const globalBuffer = globalThis.Buffer;
33
+ if (globalBuffer) {
34
+ return globalBuffer.from(binary, 'binary').toString('base64');
35
+ }
36
+ throw new EdgeBaseError(0, 'Base64 encoding is unavailable in this runtime.');
37
+ }
38
+ function decodeBase64ToBytes(value) {
39
+ let binary = '';
40
+ if (typeof atob === 'function') {
41
+ binary = atob(value);
42
+ }
43
+ else {
44
+ const globalBuffer = globalThis.Buffer;
45
+ if (!globalBuffer) {
46
+ throw new EdgeBaseError(0, 'Base64 decoding is unavailable in this runtime.');
47
+ }
48
+ binary = globalBuffer.from(value, 'base64').toString('binary');
49
+ }
50
+ const bytes = new Uint8Array(binary.length);
51
+ for (let index = 0; index < binary.length; index += 1) {
52
+ bytes[index] = binary.charCodeAt(index);
53
+ }
54
+ return bytes;
55
+ }
56
+ function extractAwarenessRoot(memberState) {
57
+ const root = memberState[ROOM_COLLAB_MEMBER_STATE_KEY];
58
+ return isRecord(root) ? root : {};
59
+ }
60
+ function extractMetaRoot(memberState) {
61
+ const root = memberState[ROOM_COLLAB_MEMBER_META_KEY];
62
+ return isRecord(root) ? root : {};
63
+ }
64
+ function extractScopedAwarenessState(memberState, collabKey) {
65
+ const awarenessRoot = extractAwarenessRoot(memberState);
66
+ const scopedState = awarenessRoot[collabKey];
67
+ if (!isRecord(scopedState)) {
68
+ return null;
69
+ }
70
+ return cloneRecord(scopedState);
71
+ }
72
+ function isMode(value) {
73
+ return value === 'editable' || value === 'read_only';
74
+ }
75
+ function extractScopedMeta(memberState, collabKey) {
76
+ const metaRoot = extractMetaRoot(memberState);
77
+ const scopedMeta = metaRoot[collabKey];
78
+ if (!isRecord(scopedMeta)) {
79
+ return null;
80
+ }
81
+ return {
82
+ mode: isMode(scopedMeta.mode) ? scopedMeta.mode : undefined,
83
+ capabilityFingerprint: typeof scopedMeta.capabilityFingerprint === 'string'
84
+ || scopedMeta.capabilityFingerprint === null
85
+ ? scopedMeta.capabilityFingerprint
86
+ : undefined,
87
+ };
88
+ }
89
+ function mapConnectionStateToCollabStatus(state, hasSyncPending) {
90
+ switch (state) {
91
+ case 'connecting':
92
+ return 'connecting';
93
+ case 'reconnecting':
94
+ return 'reconnecting';
95
+ case 'connected':
96
+ return hasSyncPending ? 'syncing' : 'ready';
97
+ case 'auth_lost':
98
+ case 'kicked':
99
+ return 'degraded';
100
+ case 'idle':
101
+ return 'idle';
102
+ case 'disconnected':
103
+ return 'degraded';
104
+ default:
105
+ return 'idle';
106
+ }
107
+ }
108
+ async function loadYjsRuntime() {
109
+ if (!yjsRuntimePromise) {
110
+ yjsRuntimePromise = import('yjs')
111
+ .then((module) => ({
112
+ applyUpdate: module.applyUpdate,
113
+ encodeStateAsUpdate: module.encodeStateAsUpdate,
114
+ }))
115
+ .catch((error) => {
116
+ yjsRuntimePromise = null;
117
+ const message = error instanceof Error ? error.message : 'Unknown Yjs import error';
118
+ throw new EdgeBaseError(0, `room.collab({ format: 'yjs', ... }) requires the 'yjs' package in the application runtime. ${message}`);
119
+ });
120
+ }
121
+ return yjsRuntimePromise;
122
+ }
123
+ export class RoomCollabClient {
124
+ room;
125
+ options;
126
+ collabKey;
127
+ remoteOriginToken = Symbol('edgebase.room.collab.remote');
128
+ subscriptions = [];
129
+ statusHandlers = [];
130
+ modeHandlers = [];
131
+ capabilityFingerprintHandlers = [];
132
+ awarenessHandlers = [];
133
+ reconnectHandlers = [];
134
+ recoveryFailureHandlers = [];
135
+ status = 'idle';
136
+ mode;
137
+ capabilityFingerprint = null;
138
+ doc = null;
139
+ docUpdateHandler = null;
140
+ pendingSyncRequestId = null;
141
+ syncTimeout = null;
142
+ pendingSyncPromise = null;
143
+ pendingSyncResolve = null;
144
+ pendingSyncReject = null;
145
+ localAwarenessOverride = null;
146
+ serverSyncEnabled = false;
147
+ awareness = {
148
+ setLocalState: (state) => this.setLocalAwarenessState(state),
149
+ clearLocalState: () => this.clearLocalAwarenessState(),
150
+ onChange: (handler) => {
151
+ this.awarenessHandlers.push(handler);
152
+ return createSubscription(() => {
153
+ const index = this.awarenessHandlers.indexOf(handler);
154
+ if (index >= 0)
155
+ this.awarenessHandlers.splice(index, 1);
156
+ });
157
+ },
158
+ getPeers: () => this.getPeers(),
159
+ getSelf: () => this.getSelf(),
160
+ };
161
+ constructor(room, options) {
162
+ this.room = room;
163
+ this.options = {
164
+ format: options.format,
165
+ key: options.key,
166
+ initialMode: options.initialMode ?? options.mode ?? 'editable',
167
+ mode: options.mode ?? options.initialMode ?? 'editable',
168
+ syncTimeoutMs: options.syncTimeoutMs ?? DEFAULT_SYNC_TIMEOUT_MS,
169
+ };
170
+ this.collabKey = scopedCollabKey(this.options.format, this.options.key);
171
+ this.mode = this.options.initialMode;
172
+ this.status = mapConnectionStateToCollabStatus(this.room.getConnectionState(), false);
173
+ this.subscriptions.push(this.room.session.onReconnect((info) => {
174
+ for (const handler of this.reconnectHandlers) {
175
+ handler(info);
176
+ }
177
+ }));
178
+ this.subscriptions.push(this.room.session.onConnectionStateChange((state) => {
179
+ this.setStatus(mapConnectionStateToCollabStatus(state, this.pendingSyncRequestId !== null));
180
+ this.refreshCapabilityState();
181
+ if ((state === 'auth_lost' || state === 'kicked') && this.pendingSyncReject) {
182
+ this.rejectPendingSync(new EdgeBaseError(401, 'Room collab lost authorization while syncing.'));
183
+ }
184
+ if (state === 'connected' && this.doc) {
185
+ void this.sync();
186
+ }
187
+ }));
188
+ this.subscriptions.push(this.room.session.onRecoveryFailure((info) => {
189
+ for (const handler of this.recoveryFailureHandlers) {
190
+ handler(info);
191
+ }
192
+ }));
193
+ this.subscriptions.push(this.room.members.onSync(() => {
194
+ this.refreshCapabilityState();
195
+ this.emitAwarenessChange();
196
+ }));
197
+ this.subscriptions.push(this.room.members.onJoin(() => {
198
+ this.refreshCapabilityState();
199
+ this.emitAwarenessChange();
200
+ }));
201
+ this.subscriptions.push(this.room.members.onLeave(() => {
202
+ this.refreshCapabilityState();
203
+ this.emitAwarenessChange();
204
+ }));
205
+ this.subscriptions.push(this.room.members.onStateChange(() => {
206
+ this.refreshCapabilityState();
207
+ this.emitAwarenessChange();
208
+ }));
209
+ this.subscriptions.push(this.room.signals.on(ROOM_COLLAB_CONTROL_EVENT, (payload, meta) => {
210
+ this.handleControlSignal(payload, meta);
211
+ }));
212
+ this.subscriptions.push(this.room.signals.on(ROOM_COLLAB_UPDATE_EVENT, (payload) => {
213
+ void this.handleIncomingUpdate(payload);
214
+ }));
215
+ this.subscriptions.push(this.room.signals.on(ROOM_COLLAB_SYNC_REQUEST_EVENT, (payload, meta) => {
216
+ void this.handleSyncRequest(payload, meta);
217
+ }));
218
+ this.subscriptions.push(this.room.signals.on(ROOM_COLLAB_SYNC_RESPONSE_EVENT, (payload) => {
219
+ void this.handleSyncResponse(payload);
220
+ }));
221
+ }
222
+ getStatus() {
223
+ return this.status;
224
+ }
225
+ getMode() {
226
+ return this.mode;
227
+ }
228
+ getCapabilityFingerprint() {
229
+ return this.capabilityFingerprint;
230
+ }
231
+ onStatusChange(handler) {
232
+ this.statusHandlers.push(handler);
233
+ return createSubscription(() => {
234
+ const index = this.statusHandlers.indexOf(handler);
235
+ if (index >= 0)
236
+ this.statusHandlers.splice(index, 1);
237
+ });
238
+ }
239
+ onModeChange(handler) {
240
+ this.modeHandlers.push(handler);
241
+ return createSubscription(() => {
242
+ const index = this.modeHandlers.indexOf(handler);
243
+ if (index >= 0)
244
+ this.modeHandlers.splice(index, 1);
245
+ });
246
+ }
247
+ onCapabilityFingerprintChange(handler) {
248
+ this.capabilityFingerprintHandlers.push(handler);
249
+ return createSubscription(() => {
250
+ const index = this.capabilityFingerprintHandlers.indexOf(handler);
251
+ if (index >= 0)
252
+ this.capabilityFingerprintHandlers.splice(index, 1);
253
+ });
254
+ }
255
+ onReconnect(handler) {
256
+ this.reconnectHandlers.push(handler);
257
+ return createSubscription(() => {
258
+ const index = this.reconnectHandlers.indexOf(handler);
259
+ if (index >= 0)
260
+ this.reconnectHandlers.splice(index, 1);
261
+ });
262
+ }
263
+ onRecoveryFailure(handler) {
264
+ this.recoveryFailureHandlers.push(handler);
265
+ return createSubscription(() => {
266
+ const index = this.recoveryFailureHandlers.indexOf(handler);
267
+ if (index >= 0)
268
+ this.recoveryFailureHandlers.splice(index, 1);
269
+ });
270
+ }
271
+ async join() {
272
+ this.setStatus('connecting');
273
+ await this.room.join();
274
+ await this.waitForRoomConnection();
275
+ this.refreshCapabilityState();
276
+ if (this.doc) {
277
+ await this.sync();
278
+ return;
279
+ }
280
+ this.setStatus('ready');
281
+ }
282
+ bind(doc) {
283
+ if (this.doc === doc) {
284
+ if (this.room.getConnectionState() === 'connected') {
285
+ void this.sync();
286
+ }
287
+ return;
288
+ }
289
+ this.unbind();
290
+ this.doc = doc;
291
+ this.docUpdateHandler = (update, origin) => {
292
+ if (origin === this.remoteOriginToken || this.mode === 'read_only') {
293
+ return;
294
+ }
295
+ if (this.room.getConnectionState() !== 'connected') {
296
+ return;
297
+ }
298
+ void this.room.signals
299
+ .send(ROOM_COLLAB_UPDATE_EVENT, {
300
+ format: this.options.format,
301
+ key: this.options.key,
302
+ update: encodeBytesToBase64(update),
303
+ })
304
+ .catch(() => {
305
+ this.setStatus('degraded');
306
+ });
307
+ };
308
+ doc.on('update', this.docUpdateHandler);
309
+ if (this.room.getConnectionState() === 'connected') {
310
+ void this.room.members.awaitCurrent(1000).then((localMember) => {
311
+ if (localMember) {
312
+ this.refreshCapabilityState();
313
+ this.emitAwarenessChange();
314
+ }
315
+ });
316
+ void this.requestSyncIfNeeded();
317
+ }
318
+ }
319
+ unbind() {
320
+ this.resolvePendingSync();
321
+ if (this.doc && this.docUpdateHandler) {
322
+ this.doc.off('update', this.docUpdateHandler);
323
+ }
324
+ this.doc = null;
325
+ this.docUpdateHandler = null;
326
+ if (this.room.getConnectionState() === 'connected') {
327
+ this.setStatus('ready');
328
+ }
329
+ else {
330
+ this.setStatus(mapConnectionStateToCollabStatus(this.room.getConnectionState(), false));
331
+ }
332
+ }
333
+ async sync() {
334
+ if (!this.doc) {
335
+ if (this.room.getConnectionState() === 'connected') {
336
+ this.setStatus('ready');
337
+ }
338
+ return;
339
+ }
340
+ const connectionState = this.room.getConnectionState();
341
+ if (connectionState !== 'connected') {
342
+ await this.waitForRoomConnection();
343
+ }
344
+ await this.requestSyncIfNeeded();
345
+ }
346
+ async leave() {
347
+ this.unbind();
348
+ this.unsubscribeAll();
349
+ this.room.leave();
350
+ this.setStatus('idle');
351
+ }
352
+ async destroy() {
353
+ await this.leave();
354
+ }
355
+ async waitForRoomConnection() {
356
+ const currentState = this.room.getConnectionState();
357
+ if (currentState === 'connected') {
358
+ return;
359
+ }
360
+ if (currentState === 'auth_lost' || currentState === 'kicked') {
361
+ throw new EdgeBaseError(401, 'Room collab could not join because the room session is unavailable.');
362
+ }
363
+ await new Promise((resolve, reject) => {
364
+ const subscription = this.room.session.onConnectionStateChange((state) => {
365
+ if (state === 'connected') {
366
+ subscription.unsubscribe();
367
+ resolve();
368
+ return;
369
+ }
370
+ if (state === 'auth_lost' || state === 'kicked') {
371
+ subscription.unsubscribe();
372
+ reject(new EdgeBaseError(401, 'Room collab lost authorization while connecting.'));
373
+ }
374
+ });
375
+ });
376
+ }
377
+ async setLocalAwarenessState(state) {
378
+ const nextState = cloneRecord(state);
379
+ const currentMemberState = this.room.members.current()?.state ?? {};
380
+ const awarenessRoot = extractAwarenessRoot(currentMemberState);
381
+ await this.room.members.setState({
382
+ [ROOM_COLLAB_MEMBER_STATE_KEY]: {
383
+ ...awarenessRoot,
384
+ [this.collabKey]: nextState,
385
+ },
386
+ });
387
+ this.localAwarenessOverride = nextState;
388
+ this.emitAwarenessChange();
389
+ }
390
+ async clearLocalAwarenessState() {
391
+ const currentMemberState = this.room.members.current()?.state ?? {};
392
+ const awarenessRoot = extractAwarenessRoot(currentMemberState);
393
+ if (!(this.collabKey in awarenessRoot)) {
394
+ this.localAwarenessOverride = null;
395
+ this.emitAwarenessChange();
396
+ return;
397
+ }
398
+ const nextAwarenessRoot = { ...awarenessRoot };
399
+ delete nextAwarenessRoot[this.collabKey];
400
+ await this.room.members.setState({
401
+ [ROOM_COLLAB_MEMBER_STATE_KEY]: nextAwarenessRoot,
402
+ });
403
+ this.localAwarenessOverride = null;
404
+ this.emitAwarenessChange();
405
+ }
406
+ getSelf() {
407
+ const current = this.room.members.current();
408
+ if (!current) {
409
+ return null;
410
+ }
411
+ return this.toPeer(current, true);
412
+ }
413
+ getSyncPeerCount() {
414
+ const currentMember = this.room.members.current();
415
+ const currentMemberId = currentMember?.memberId ?? null;
416
+ const currentConnectionId = currentMember?.connectionId ?? null;
417
+ return this.room.members
418
+ .list()
419
+ .filter((member) => {
420
+ if (currentConnectionId && member.connectionId === currentConnectionId) {
421
+ return false;
422
+ }
423
+ if (currentMemberId && member.memberId === currentMemberId) {
424
+ return false;
425
+ }
426
+ return true;
427
+ })
428
+ .length;
429
+ }
430
+ getPeers() {
431
+ const currentMemberId = this.room.members.current()?.memberId ?? null;
432
+ return this.room.members
433
+ .list()
434
+ .filter((member) => member.memberId !== currentMemberId)
435
+ .map((member) => this.toPeer(member, false))
436
+ .filter((peer) => peer !== null);
437
+ }
438
+ toPeer(member, isSelf) {
439
+ const scopedState = isSelf && this.localAwarenessOverride
440
+ ? cloneRecord(this.localAwarenessOverride)
441
+ : extractScopedAwarenessState(member.state, this.collabKey);
442
+ if (!scopedState) {
443
+ return null;
444
+ }
445
+ return {
446
+ memberId: member.memberId,
447
+ userId: member.userId,
448
+ connectionId: member.connectionId,
449
+ role: member.role,
450
+ state: scopedState,
451
+ isSelf,
452
+ };
453
+ }
454
+ emitAwarenessChange() {
455
+ const peers = this.getPeers();
456
+ for (const handler of this.awarenessHandlers) {
457
+ handler(peers.map((peer) => ({
458
+ ...peer,
459
+ state: cloneRecord(peer.state),
460
+ })));
461
+ }
462
+ }
463
+ async requestSyncIfNeeded() {
464
+ if (this.pendingSyncPromise) {
465
+ return this.pendingSyncPromise;
466
+ }
467
+ if (!this.doc) {
468
+ this.setStatus('ready');
469
+ return Promise.resolve();
470
+ }
471
+ const syncPeerCount = this.getSyncPeerCount();
472
+ if (syncPeerCount === 0 && !this.serverSyncEnabled) {
473
+ this.resolvePendingSync();
474
+ this.setStatus('ready');
475
+ return Promise.resolve();
476
+ }
477
+ this.resolvePendingSync();
478
+ const requestId = `collab-sync-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
479
+ this.pendingSyncPromise = new Promise((resolve, reject) => {
480
+ this.pendingSyncResolve = resolve;
481
+ this.pendingSyncReject = reject;
482
+ });
483
+ this.pendingSyncRequestId = requestId;
484
+ this.setStatus('syncing');
485
+ this.syncTimeout = setTimeout(() => {
486
+ if (this.pendingSyncRequestId === requestId) {
487
+ this.resolvePendingSync();
488
+ this.setStatus('ready');
489
+ }
490
+ }, this.options.syncTimeoutMs);
491
+ void this.room.signals
492
+ .send(ROOM_COLLAB_SYNC_REQUEST_EVENT, {
493
+ format: this.options.format,
494
+ key: this.options.key,
495
+ requestId,
496
+ })
497
+ .catch(() => {
498
+ this.rejectPendingSync(new EdgeBaseError(0, 'Room collab sync request failed.'));
499
+ this.setStatus('degraded');
500
+ });
501
+ return this.pendingSyncPromise;
502
+ }
503
+ async handleIncomingUpdate(payload) {
504
+ if (!this.doc) {
505
+ return;
506
+ }
507
+ const parsed = this.parsePayload(payload);
508
+ if (!parsed || typeof parsed.update !== 'string') {
509
+ return;
510
+ }
511
+ try {
512
+ const yjs = await loadYjsRuntime();
513
+ yjs.applyUpdate(this.doc, decodeBase64ToBytes(parsed.update), this.remoteOriginToken);
514
+ }
515
+ catch {
516
+ this.setStatus('degraded');
517
+ }
518
+ }
519
+ handleControlSignal(payload, meta) {
520
+ if (meta.serverSent !== true || !isRecord(payload)) {
521
+ return;
522
+ }
523
+ const typed = payload;
524
+ if (typed.format !== this.options.format || typed.key !== this.options.key) {
525
+ return;
526
+ }
527
+ if (isMode(typed.mode)) {
528
+ this.setMode(typed.mode);
529
+ }
530
+ if (typeof typed.capabilityFingerprint === 'string' || typed.capabilityFingerprint === null) {
531
+ this.setCapabilityFingerprint(typed.capabilityFingerprint);
532
+ }
533
+ if (typeof typed.serverSync === 'boolean') {
534
+ const previous = this.serverSyncEnabled;
535
+ this.serverSyncEnabled = typed.serverSync;
536
+ if (typed.serverSync &&
537
+ !previous &&
538
+ this.doc &&
539
+ this.room.getConnectionState() === 'connected' &&
540
+ this.pendingSyncRequestId === null) {
541
+ void this.requestSyncIfNeeded();
542
+ }
543
+ }
544
+ }
545
+ async handleSyncRequest(payload, meta) {
546
+ if (!this.doc || !meta.memberId) {
547
+ return;
548
+ }
549
+ const parsed = this.parsePayload(payload);
550
+ if (!parsed || typeof parsed.requestId !== 'string') {
551
+ return;
552
+ }
553
+ try {
554
+ const yjs = await loadYjsRuntime();
555
+ const snapshot = yjs.encodeStateAsUpdate(this.doc);
556
+ await this.room.signals.sendTo(meta.memberId, ROOM_COLLAB_SYNC_RESPONSE_EVENT, {
557
+ format: this.options.format,
558
+ key: this.options.key,
559
+ requestId: parsed.requestId,
560
+ update: encodeBytesToBase64(snapshot),
561
+ syncSource: ROOM_COLLAB_SYNC_SOURCE_PEER,
562
+ });
563
+ }
564
+ catch {
565
+ this.setStatus('degraded');
566
+ }
567
+ }
568
+ async handleSyncResponse(payload) {
569
+ if (!this.doc || !this.pendingSyncRequestId) {
570
+ return;
571
+ }
572
+ const parsed = this.parsePayload(payload);
573
+ if (!parsed
574
+ || typeof parsed.requestId !== 'string'
575
+ || parsed.requestId !== this.pendingSyncRequestId
576
+ || typeof parsed.update !== 'string') {
577
+ return;
578
+ }
579
+ try {
580
+ const yjs = await loadYjsRuntime();
581
+ yjs.applyUpdate(this.doc, decodeBase64ToBytes(parsed.update), this.remoteOriginToken);
582
+ const syncSource = parsed.syncSource ?? ROOM_COLLAB_SYNC_SOURCE_PEER;
583
+ if (syncSource === ROOM_COLLAB_SYNC_SOURCE_SERVER && this.getSyncPeerCount() > 0) {
584
+ return;
585
+ }
586
+ this.resolvePendingSync();
587
+ this.setStatus('ready');
588
+ }
589
+ catch (error) {
590
+ const message = error instanceof Error ? error.message : 'Unknown sync apply error';
591
+ this.rejectPendingSync(new EdgeBaseError(0, `Room collab sync response failed to apply. ${message}`));
592
+ this.setStatus('degraded');
593
+ }
594
+ }
595
+ parsePayload(payload) {
596
+ if (!isRecord(payload)) {
597
+ return null;
598
+ }
599
+ const typed = payload;
600
+ if (typed.format !== this.options.format || typed.key !== this.options.key) {
601
+ return null;
602
+ }
603
+ return {
604
+ requestId: typeof typed.requestId === 'string' ? typed.requestId : undefined,
605
+ update: typeof typed.update === 'string' ? typed.update : undefined,
606
+ syncSource: typed.syncSource === ROOM_COLLAB_SYNC_SOURCE_SERVER ||
607
+ typed.syncSource === ROOM_COLLAB_SYNC_SOURCE_PEER
608
+ ? typed.syncSource
609
+ : undefined,
610
+ };
611
+ }
612
+ clearSyncState() {
613
+ this.pendingSyncRequestId = null;
614
+ if (this.syncTimeout) {
615
+ clearTimeout(this.syncTimeout);
616
+ this.syncTimeout = null;
617
+ }
618
+ this.pendingSyncPromise = null;
619
+ this.pendingSyncResolve = null;
620
+ this.pendingSyncReject = null;
621
+ }
622
+ resolvePendingSync() {
623
+ const resolve = this.pendingSyncResolve;
624
+ this.clearSyncState();
625
+ resolve?.();
626
+ }
627
+ rejectPendingSync(error) {
628
+ const reject = this.pendingSyncReject;
629
+ this.clearSyncState();
630
+ reject?.(error);
631
+ }
632
+ unsubscribeAll() {
633
+ while (this.subscriptions.length > 0) {
634
+ const subscription = this.subscriptions.pop();
635
+ subscription?.unsubscribe();
636
+ }
637
+ }
638
+ setStatus(next) {
639
+ if (this.status === next) {
640
+ return;
641
+ }
642
+ this.status = next;
643
+ for (const handler of this.statusHandlers) {
644
+ handler(next);
645
+ }
646
+ }
647
+ setMode(next) {
648
+ if (this.mode === next) {
649
+ return;
650
+ }
651
+ this.mode = next;
652
+ for (const handler of this.modeHandlers) {
653
+ handler(next);
654
+ }
655
+ }
656
+ setCapabilityFingerprint(next) {
657
+ if (this.capabilityFingerprint === next) {
658
+ return;
659
+ }
660
+ this.capabilityFingerprint = next;
661
+ for (const handler of this.capabilityFingerprintHandlers) {
662
+ handler(next);
663
+ }
664
+ }
665
+ refreshCapabilityState() {
666
+ const currentMember = this.room.members.current();
667
+ const scopedMeta = currentMember ? extractScopedMeta(currentMember.state, this.collabKey) : null;
668
+ this.setMode(scopedMeta?.mode ?? this.options.initialMode);
669
+ this.setCapabilityFingerprint(scopedMeta?.capabilityFingerprint ?? null);
670
+ }
671
+ }
672
+ export function createRoomCollab(room, options) {
673
+ return new RoomCollabClient(room, options);
674
+ }
675
+ //# sourceMappingURL=room-collab-core.js.map