@pluv/client 2.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +141 -0
- package/dist/index.d.mts +74 -79
- package/dist/index.d.ts +74 -79
- package/dist/index.js +267 -68
- package/dist/index.mjs +269 -70
- package/package.json +5 -5
- package/src/CrdtManager.ts +9 -13
- package/src/CrdtNotifier.ts +2 -1
- package/src/ListenerManager.ts +21 -0
- package/src/MockedRoom.ts +118 -48
- package/src/PluvClient.ts +8 -8
- package/src/PluvProcedure.ts +27 -25
- package/src/PluvRoom.ts +235 -82
- package/src/PluvRouter.ts +31 -22
- package/src/StateNotifier.ts +13 -19
- package/src/UsersManager.ts +18 -4
- package/src/UsersNotifier.ts +66 -0
- package/src/constants.ts +1 -0
- package/src/createClient.ts +2 -2
- package/src/index.ts +2 -1
- package/src/types.ts +10 -31
- package/src/OtherNotifier.ts +0 -50
package/src/PluvRoom.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AbstractCrdtDocFactory,
|
|
3
|
-
CrdtType,
|
|
4
3
|
InferCrdtJson,
|
|
4
|
+
InferDoc,
|
|
5
|
+
InferDocLike,
|
|
5
6
|
InferStorage,
|
|
6
7
|
NoopCrdtDocFactory,
|
|
7
8
|
} from "@pluv/crdt";
|
|
8
9
|
import type {
|
|
9
10
|
BaseIOEventRecord,
|
|
10
11
|
BaseUser,
|
|
12
|
+
BroadcastProxy,
|
|
11
13
|
CrdtDocLike,
|
|
12
14
|
EventMessage,
|
|
13
15
|
EventNotifierSubscriptionCallback,
|
|
16
|
+
EventProxy,
|
|
14
17
|
IOEventMessage,
|
|
15
18
|
IOLike,
|
|
16
19
|
Id,
|
|
@@ -22,23 +25,31 @@ import type {
|
|
|
22
25
|
JsonObject,
|
|
23
26
|
MergeEvents,
|
|
24
27
|
OptionalProps,
|
|
25
|
-
|
|
28
|
+
OtherSubscriptionCallback,
|
|
29
|
+
OthersSubscriptionCallback,
|
|
30
|
+
RoomEventListenerMap,
|
|
26
31
|
RoomLike,
|
|
32
|
+
StateNotifierSubjects,
|
|
33
|
+
StorageProxy,
|
|
34
|
+
StorageRootSubscriptionCallback,
|
|
35
|
+
StorageSubscriptionCallback,
|
|
36
|
+
SubscribeProxy,
|
|
37
|
+
SubscriptionCallback,
|
|
27
38
|
UpdateMyPresenceAction,
|
|
28
39
|
UserInfo,
|
|
40
|
+
WebSocketConnection,
|
|
29
41
|
WebSocketState,
|
|
30
42
|
} from "@pluv/types";
|
|
31
|
-
import { ConnectionState } from "@pluv/types";
|
|
43
|
+
import { ConnectionState, StorageState } from "@pluv/types";
|
|
32
44
|
import type { AbstractStorageStore } from "./AbstractStorageStore";
|
|
33
45
|
import type { CrdtManagerOptions } from "./CrdtManager";
|
|
34
46
|
import { CrdtManager } from "./CrdtManager";
|
|
35
47
|
import { CrdtNotifier } from "./CrdtNotifier";
|
|
36
48
|
import { EventNotifier } from "./EventNotifier";
|
|
37
|
-
import {
|
|
49
|
+
import { ListenerManager } from "./ListenerManager";
|
|
38
50
|
import { PluvProcedure } from "./PluvProcedure";
|
|
39
51
|
import type { PluvRouterEventConfig } from "./PluvRouter";
|
|
40
52
|
import { PluvRouter } from "./PluvRouter";
|
|
41
|
-
import type { StateNotifierSubjects, SubscriptionCallback } from "./StateNotifier";
|
|
42
53
|
import { StateNotifier } from "./StateNotifier";
|
|
43
54
|
import { StorageStore } from "./StorageStore";
|
|
44
55
|
import type {
|
|
@@ -48,11 +59,11 @@ import type {
|
|
|
48
59
|
InternalSubscriptions,
|
|
49
60
|
PluvClientLimits,
|
|
50
61
|
PublicKey,
|
|
51
|
-
WebSocketConnection,
|
|
52
62
|
WithMetadata,
|
|
53
63
|
} from "./types";
|
|
54
64
|
import type { UsersManagerConfig } from "./UsersManager";
|
|
55
65
|
import { UsersManager } from "./UsersManager";
|
|
66
|
+
import { UsersNotifier } from "./UsersNotifier";
|
|
56
67
|
import { debounce } from "./utils";
|
|
57
68
|
|
|
58
69
|
const ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1_000;
|
|
@@ -78,8 +89,8 @@ declare global {
|
|
|
78
89
|
export const DEFAULT_PLUV_CLIENT_ADDON = <
|
|
79
90
|
TIO extends IOLike,
|
|
80
91
|
TMetadata extends JsonObject,
|
|
81
|
-
TPresence extends
|
|
82
|
-
TCrdt extends AbstractCrdtDocFactory<any>,
|
|
92
|
+
TPresence extends Record<string, any>,
|
|
93
|
+
TCrdt extends AbstractCrdtDocFactory<any, any>,
|
|
83
94
|
>(
|
|
84
95
|
input: PluvRoomAddonInput<TIO, TMetadata, TPresence, TCrdt>,
|
|
85
96
|
): PluvRoomAddonResult => ({
|
|
@@ -95,9 +106,9 @@ interface WindowListeners {
|
|
|
95
106
|
|
|
96
107
|
interface WebSocketListeners {
|
|
97
108
|
onClose: (event: CloseEvent) => void;
|
|
98
|
-
onError: () => void;
|
|
109
|
+
onError: (event: Event) => void;
|
|
99
110
|
onMessage: (message: MessageEvent<string>) => void;
|
|
100
|
-
onOpen: () => void;
|
|
111
|
+
onOpen: (event: Event) => void;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
interface IntervalIds {
|
|
@@ -136,15 +147,15 @@ interface InternalListeners {
|
|
|
136
147
|
export type PluvRoomAddon<
|
|
137
148
|
TIO extends IOLike,
|
|
138
149
|
TMetadata extends JsonObject,
|
|
139
|
-
TPresence extends
|
|
140
|
-
TCrdt extends AbstractCrdtDocFactory<any>,
|
|
150
|
+
TPresence extends Record<string, any>,
|
|
151
|
+
TCrdt extends AbstractCrdtDocFactory<any, any>,
|
|
141
152
|
> = (input: PluvRoomAddonInput<TIO, TMetadata, TPresence, TCrdt>) => Partial<PluvRoomAddonResult>;
|
|
142
153
|
|
|
143
154
|
export interface PluvRoomAddonInput<
|
|
144
155
|
TIO extends IOLike,
|
|
145
156
|
TMetadata extends JsonObject,
|
|
146
|
-
TPresence extends
|
|
147
|
-
TCrdt extends AbstractCrdtDocFactory<any>,
|
|
157
|
+
TPresence extends Record<string, any>,
|
|
158
|
+
TCrdt extends AbstractCrdtDocFactory<any, any>,
|
|
148
159
|
> {
|
|
149
160
|
room: PluvRoom<TIO, TMetadata, TPresence, TCrdt>;
|
|
150
161
|
}
|
|
@@ -174,8 +185,8 @@ export type ReconnectTimeoutMs = number | ((params: ReconnectTimeoutMsParams) =>
|
|
|
174
185
|
export type RoomConfig<
|
|
175
186
|
TIO extends IOLike,
|
|
176
187
|
TMetadata extends JsonObject,
|
|
177
|
-
TPresence extends
|
|
178
|
-
TCrdt extends AbstractCrdtDocFactory<any>,
|
|
188
|
+
TPresence extends Record<string, any>,
|
|
189
|
+
TCrdt extends AbstractCrdtDocFactory<any, any>,
|
|
179
190
|
TEvents extends PluvRouterEventConfig<TIO, TPresence, InferStorage<TCrdt>>,
|
|
180
191
|
> = Id<
|
|
181
192
|
{
|
|
@@ -195,10 +206,10 @@ export type RoomConfig<
|
|
|
195
206
|
export class PluvRoom<
|
|
196
207
|
TIO extends IOLike<any, any>,
|
|
197
208
|
TMetadata extends JsonObject = {},
|
|
198
|
-
TPresence extends
|
|
199
|
-
TCrdt extends AbstractCrdtDocFactory<any> = NoopCrdtDocFactory,
|
|
209
|
+
TPresence extends Record<string, any> = {},
|
|
210
|
+
TCrdt extends AbstractCrdtDocFactory<any, any> = NoopCrdtDocFactory,
|
|
200
211
|
TEvents extends PluvRouterEventConfig<TIO, TPresence, InferStorage<TCrdt>> = {},
|
|
201
|
-
> implements RoomLike<TIO, TPresence, InferStorage<TCrdt>>
|
|
212
|
+
> implements RoomLike<TIO, InferDoc<TCrdt>, TPresence, InferStorage<TCrdt>>
|
|
202
213
|
{
|
|
203
214
|
readonly _endpoints: RoomEndpoints<TIO, TMetadata>;
|
|
204
215
|
|
|
@@ -213,8 +224,8 @@ export class PluvRoom<
|
|
|
213
224
|
heartbeat: null,
|
|
214
225
|
};
|
|
215
226
|
private readonly _limits: PluvClientLimits;
|
|
227
|
+
private readonly _listenerManager = new ListenerManager();
|
|
216
228
|
private readonly _listeners: InternalListeners;
|
|
217
|
-
private readonly _otherNotifier = new OtherNotifier<TIO, TPresence>();
|
|
218
229
|
private readonly _publicKey: PublicKey<TMetadata> | null = null;
|
|
219
230
|
private readonly _reconnectTimeoutMs: ReconnectTimeoutMs;
|
|
220
231
|
private readonly _router: PluvRouter<TIO, TPresence, InferStorage<TCrdt>, TEvents>;
|
|
@@ -228,6 +239,7 @@ export class PluvRoom<
|
|
|
228
239
|
reconnect: null,
|
|
229
240
|
};
|
|
230
241
|
private readonly _usersManager: UsersManager<TIO, TPresence>;
|
|
242
|
+
private readonly _usersNotifier = new UsersNotifier<TIO, TPresence>();
|
|
231
243
|
|
|
232
244
|
private _lastMetadata: TMetadata | null = null;
|
|
233
245
|
private _state: WebSocketState<TIO> = {
|
|
@@ -237,10 +249,12 @@ export class PluvRoom<
|
|
|
237
249
|
},
|
|
238
250
|
connection: {
|
|
239
251
|
attempts: 0,
|
|
240
|
-
count: 0,
|
|
241
252
|
id: null,
|
|
242
253
|
state: ConnectionState.Untouched,
|
|
243
254
|
},
|
|
255
|
+
storage: {
|
|
256
|
+
state: StorageState.Unavailable,
|
|
257
|
+
},
|
|
244
258
|
webSocket: null,
|
|
245
259
|
};
|
|
246
260
|
private _windowListeners: WindowListeners | null = null;
|
|
@@ -297,14 +311,24 @@ export class PluvRoom<
|
|
|
297
311
|
this._crdtManager = new CrdtManager<TCrdt>({ initialStorage });
|
|
298
312
|
}
|
|
299
313
|
|
|
314
|
+
/**
|
|
315
|
+
* @deprecated To be removed in v3. Use `getStorageLoaded` instead.
|
|
316
|
+
*/
|
|
300
317
|
public get storageLoaded(): boolean {
|
|
301
|
-
return this.
|
|
318
|
+
return this.getStorageLoaded();
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
public get webSocket(): WebSocket | null {
|
|
305
322
|
return this._state.webSocket;
|
|
306
323
|
}
|
|
307
324
|
|
|
325
|
+
public addEventListener<TKind extends keyof RoomEventListenerMap>(
|
|
326
|
+
kind: TKind,
|
|
327
|
+
handler: RoomEventListenerMap[TKind],
|
|
328
|
+
): () => void {
|
|
329
|
+
return this._listenerManager.subscribe(kind, handler as any);
|
|
330
|
+
}
|
|
331
|
+
|
|
308
332
|
public broadcast = new Proxy(
|
|
309
333
|
async <TEvent extends keyof InferIOInput<MergeEvents<TEvents, TIO>>>(
|
|
310
334
|
event: TEvent,
|
|
@@ -320,7 +344,8 @@ export class PluvRoom<
|
|
|
320
344
|
any,
|
|
321
345
|
any,
|
|
322
346
|
TPresence,
|
|
323
|
-
|
|
347
|
+
TCrdt,
|
|
348
|
+
""
|
|
324
349
|
> | null;
|
|
325
350
|
|
|
326
351
|
if (!procedure?.config.broadcast) {
|
|
@@ -334,7 +359,7 @@ export class PluvRoom<
|
|
|
334
359
|
if (!myself) return;
|
|
335
360
|
|
|
336
361
|
const parsed = procedure.config.input ? procedure.config.input.parse(data) : data;
|
|
337
|
-
const context: EventResolverContext<TIO, TPresence,
|
|
362
|
+
const context: EventResolverContext<TIO, TPresence, InferDocLike<TCrdt>> = {
|
|
338
363
|
doc: this._crdtManager.doc,
|
|
339
364
|
others: this._usersManager.getOthers(),
|
|
340
365
|
room: this.id,
|
|
@@ -364,14 +389,7 @@ export class PluvRoom<
|
|
|
364
389
|
};
|
|
365
390
|
},
|
|
366
391
|
},
|
|
367
|
-
) as
|
|
368
|
-
event: TEvent,
|
|
369
|
-
data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[TEvent]>,
|
|
370
|
-
) => Promise<void>) & {
|
|
371
|
-
[PEvent in keyof InferIOInput<MergeEvents<TEvents, TIO>>]: (
|
|
372
|
-
data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[PEvent]>,
|
|
373
|
-
) => Promise<void>;
|
|
374
|
-
};
|
|
392
|
+
) as BroadcastProxy<TIO, TEvents>;
|
|
375
393
|
|
|
376
394
|
public canRedo = (): boolean => {
|
|
377
395
|
return !!this._crdtManager.doc.canRedo();
|
|
@@ -436,6 +454,7 @@ export class PluvRoom<
|
|
|
436
454
|
this._updateState((oldState) => {
|
|
437
455
|
oldState.connection.attempts = 0;
|
|
438
456
|
oldState.authorization.token = authToken;
|
|
457
|
+
oldState.storage.state = StorageState.Loading;
|
|
439
458
|
oldState.webSocket = webSocket;
|
|
440
459
|
|
|
441
460
|
return oldState;
|
|
@@ -460,6 +479,10 @@ export class PluvRoom<
|
|
|
460
479
|
this._updateState((oldState) => {
|
|
461
480
|
oldState.authorization.token = null;
|
|
462
481
|
oldState.connection.state = ConnectionState.Closed;
|
|
482
|
+
oldState.storage.state =
|
|
483
|
+
oldState.storage.state === StorageState.Synced
|
|
484
|
+
? StorageState.Offline
|
|
485
|
+
: StorageState.Unavailable;
|
|
463
486
|
oldState.webSocket = null;
|
|
464
487
|
|
|
465
488
|
return oldState;
|
|
@@ -470,6 +493,9 @@ export class PluvRoom<
|
|
|
470
493
|
this._stateNotifier.subjects["my-presence"].next(null);
|
|
471
494
|
}
|
|
472
495
|
|
|
496
|
+
/**
|
|
497
|
+
* @deprecated To be removed in v3. Use PluvRoom.subscribe.event instead
|
|
498
|
+
*/
|
|
473
499
|
public event = new Proxy(
|
|
474
500
|
<TEvent extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
|
|
475
501
|
event: TEvent,
|
|
@@ -482,21 +508,14 @@ export class PluvRoom<
|
|
|
482
508
|
): (() => void) => fn(prop as any, callback);
|
|
483
509
|
},
|
|
484
510
|
},
|
|
485
|
-
) as
|
|
486
|
-
event: TEvent,
|
|
487
|
-
callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, TEvent>,
|
|
488
|
-
) => () => void) & {
|
|
489
|
-
[PEvent in keyof InferIOOutput<MergeEvents<TEvents, TIO>>]: (
|
|
490
|
-
callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, PEvent>,
|
|
491
|
-
) => () => void;
|
|
492
|
-
};
|
|
511
|
+
) as EventProxy<TIO, TEvents>;
|
|
493
512
|
|
|
494
513
|
public getConnection = (): WebSocketConnection => {
|
|
495
514
|
// Create a read-only clone of the connection state
|
|
496
515
|
return Object.freeze(JSON.parse(JSON.stringify(this._state.connection)));
|
|
497
516
|
};
|
|
498
517
|
|
|
499
|
-
public getDoc = (): CrdtDocLike<InferStorage<TCrdt>> => {
|
|
518
|
+
public getDoc = (): CrdtDocLike<InferDoc<TCrdt>, InferStorage<TCrdt>> => {
|
|
500
519
|
return this._crdtManager.doc;
|
|
501
520
|
};
|
|
502
521
|
|
|
@@ -532,7 +551,6 @@ export class PluvRoom<
|
|
|
532
551
|
): InferCrdtJson<InferStorage<TCrdt>[TKey]> | null;
|
|
533
552
|
public getStorageJson<TKey extends keyof InferStorage<TCrdt>>(type?: TKey) {
|
|
534
553
|
if (this._state.connection.id === null) return null;
|
|
535
|
-
|
|
536
554
|
if (typeof type === "undefined") return this._crdtManager.doc.toJson();
|
|
537
555
|
|
|
538
556
|
const sharedType = this._crdtManager.get(type);
|
|
@@ -542,42 +560,111 @@ export class PluvRoom<
|
|
|
542
560
|
return this._crdtManager.doc.toJson(type);
|
|
543
561
|
}
|
|
544
562
|
|
|
563
|
+
public getStorageLoaded = (): boolean => {
|
|
564
|
+
return (
|
|
565
|
+
this._state.storage.state === StorageState.Offline ||
|
|
566
|
+
this._state.storage.state === StorageState.Synced
|
|
567
|
+
);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @deprecated To be removed in v3. Use PluvRoom.subscribe.other instead
|
|
572
|
+
*/
|
|
545
573
|
public other = (
|
|
546
574
|
connectionId: string,
|
|
547
|
-
callback:
|
|
575
|
+
callback: OtherSubscriptionCallback<TIO, TPresence>,
|
|
548
576
|
): (() => void) => {
|
|
549
577
|
const clientId = this._usersManager.getClientId(connectionId);
|
|
550
578
|
|
|
551
579
|
if (!clientId) return () => undefined;
|
|
552
580
|
|
|
553
|
-
return this.
|
|
581
|
+
return this._usersNotifier.subscribeOther(clientId, callback);
|
|
554
582
|
};
|
|
555
583
|
|
|
556
584
|
public redo = (): void => {
|
|
557
585
|
this._crdtManager.doc.redo();
|
|
558
586
|
};
|
|
559
587
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
588
|
+
/**
|
|
589
|
+
* @deprecated To be removed in v3. Use PluvRoom.subscribe.storage instead
|
|
590
|
+
*/
|
|
591
|
+
public storage = new Proxy(
|
|
592
|
+
<TKey extends keyof InferStorage<TCrdt>>(
|
|
593
|
+
key: TKey,
|
|
594
|
+
callback: StorageSubscriptionCallback<InferStorage<TCrdt>, TKey>,
|
|
595
|
+
): (() => void) => this._crdtNotifier.subscribe(key, callback),
|
|
596
|
+
{
|
|
597
|
+
get: (fn, prop) => {
|
|
598
|
+
type _Storage = InferStorage<TCrdt>;
|
|
599
|
+
|
|
600
|
+
if (!!prop) {
|
|
601
|
+
return (callback: StorageRootSubscriptionCallback<_Storage>) => {
|
|
602
|
+
return this._crdtNotifier.subcribeRoot(callback);
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return (callback: StorageSubscriptionCallback<_Storage, keyof _Storage>) => {
|
|
607
|
+
return fn(prop as keyof InferStorage<TCrdt>, callback);
|
|
608
|
+
};
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
) as StorageProxy<InferStorage<TCrdt>>;
|
|
566
612
|
|
|
613
|
+
/**
|
|
614
|
+
* @deprecated To be removed in v3. Use PluvRoom.subscribe.storage instead
|
|
615
|
+
* @description Subscribes to the root storage object (serialized)
|
|
616
|
+
*/
|
|
567
617
|
public storageRoot = (
|
|
568
|
-
|
|
618
|
+
callback: (value: {
|
|
569
619
|
[P in keyof InferStorage<TCrdt>]: InferCrdtJson<InferStorage<TCrdt>[P]>;
|
|
570
620
|
}) => void,
|
|
571
621
|
): (() => void) => {
|
|
572
|
-
return this._crdtNotifier.subcribeRoot(
|
|
622
|
+
return this._crdtNotifier.subcribeRoot(callback);
|
|
573
623
|
};
|
|
574
624
|
|
|
575
|
-
public subscribe =
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
625
|
+
public subscribe = new Proxy(
|
|
626
|
+
<TSubject extends keyof StateNotifierSubjects<TIO, TPresence>>(
|
|
627
|
+
name: TSubject,
|
|
628
|
+
callback: SubscriptionCallback<TIO, TPresence, TSubject>,
|
|
629
|
+
): (() => void) => this._stateNotifier.subscribe(name, callback),
|
|
630
|
+
{
|
|
631
|
+
get: (fn, prop) => {
|
|
632
|
+
if (prop === "connection") {
|
|
633
|
+
return (callback: SubscriptionCallback<TIO, TPresence, "connection">) => {
|
|
634
|
+
return fn("connection", callback);
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (prop === "myPresence") {
|
|
639
|
+
return (callback: SubscriptionCallback<TIO, TPresence, "my-presence">) => {
|
|
640
|
+
return fn("my-presence", callback);
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (prop === "myself") {
|
|
645
|
+
return (callback: SubscriptionCallback<TIO, TPresence, "myself">) => {
|
|
646
|
+
return fn("myself", callback);
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (prop === "others") {
|
|
651
|
+
return (callback: OthersSubscriptionCallback<TIO, TPresence>) => {
|
|
652
|
+
return this._usersNotifier.subscribeOthers(callback);
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (prop === "storageLoaded") {
|
|
657
|
+
return (callback: SubscriptionCallback<TIO, TPresence, "storage-loaded">) => {
|
|
658
|
+
return fn("storage-loaded", callback);
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (prop === "event") return this.event;
|
|
663
|
+
if (prop === "other") return this.other;
|
|
664
|
+
if (prop === "storage") return this.storage;
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
) as SubscribeProxy<TIO, TPresence, InferStorage<TCrdt>, TEvents>;
|
|
581
668
|
|
|
582
669
|
public transact = (fn: (storage: InferStorage<TCrdt>) => void, origin?: string): void => {
|
|
583
670
|
const _origin = origin ?? this._state.connection.id;
|
|
@@ -679,9 +766,10 @@ export class PluvRoom<
|
|
|
679
766
|
this._detachWindowListeners();
|
|
680
767
|
this._usersManager.removeMyself();
|
|
681
768
|
this._usersManager.clearConnections();
|
|
682
|
-
this.
|
|
769
|
+
this._usersNotifier.clear();
|
|
683
770
|
this._stateNotifier.subjects.myself.next(null);
|
|
684
771
|
this._stateNotifier.subjects.others.next([]);
|
|
772
|
+
this._usersNotifier.others.next({ others: [], event: { kind: "clear" } });
|
|
685
773
|
this._state.webSocket?.close();
|
|
686
774
|
|
|
687
775
|
this._detachWsListeners();
|
|
@@ -841,13 +929,26 @@ export class PluvRoom<
|
|
|
841
929
|
|
|
842
930
|
this._stateNotifier.subjects.others.next(others);
|
|
843
931
|
|
|
932
|
+
// Should not reach here
|
|
933
|
+
if (!deleted) {
|
|
934
|
+
console.warn("Could not identify exited connection");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const { data: user, remaining } = deleted;
|
|
939
|
+
|
|
940
|
+
this._usersNotifier.others.next({
|
|
941
|
+
others,
|
|
942
|
+
event: { kind: "leave", user },
|
|
943
|
+
});
|
|
944
|
+
|
|
844
945
|
/**
|
|
845
|
-
* @description A single user can have multiple connections. So we're checking that
|
|
846
|
-
* isn't a remaining connection before choosing to delete that user for other
|
|
847
|
-
* participants.
|
|
946
|
+
* @description A single user can have multiple connections. So we're checking that
|
|
947
|
+
* there isn't a remaining connection before choosing to delete that user for other
|
|
948
|
+
* connected participants.
|
|
848
949
|
* @date April 16, 2025
|
|
849
950
|
*/
|
|
850
|
-
if (!
|
|
951
|
+
if (!remaining && !!clientId) this._usersNotifier.delete(clientId);
|
|
851
952
|
}
|
|
852
953
|
|
|
853
954
|
private _handlePresenceUpdatedMessage(message: IOEventMessage<TIO>): void {
|
|
@@ -886,27 +987,41 @@ export class PluvRoom<
|
|
|
886
987
|
|
|
887
988
|
if (!!clientId) {
|
|
888
989
|
const other = this._usersManager.getOther(connectionId);
|
|
990
|
+
const others = this._usersManager.getOthers();
|
|
889
991
|
|
|
890
|
-
this.
|
|
891
|
-
|
|
892
|
-
/**
|
|
893
|
-
* !HACK
|
|
894
|
-
* @description User could not be found. Add the connection to keep others up-to-date
|
|
895
|
-
* @date April 19, 2025
|
|
896
|
-
*/
|
|
897
|
-
const added = this._usersManager.addConnection({
|
|
898
|
-
connectionId,
|
|
899
|
-
presence: data.presence as TPresence,
|
|
900
|
-
user: message.user as Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>>,
|
|
901
|
-
});
|
|
902
|
-
const other = this._usersManager.getOther(connectionId);
|
|
992
|
+
this._usersNotifier.other(clientId).next(other);
|
|
993
|
+
this._stateNotifier.subjects.others.next(others);
|
|
903
994
|
|
|
904
|
-
|
|
995
|
+
if (!!other) {
|
|
996
|
+
this._usersNotifier.others.next({
|
|
997
|
+
others,
|
|
998
|
+
event: { kind: "update", user: other },
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
return;
|
|
905
1003
|
}
|
|
906
1004
|
|
|
1005
|
+
/**
|
|
1006
|
+
* !HACK
|
|
1007
|
+
* @description User could not be found. Add the connection to keep others up-to-date
|
|
1008
|
+
* @date April 19, 2025
|
|
1009
|
+
*/
|
|
1010
|
+
const added = this._usersManager.addConnection({
|
|
1011
|
+
connectionId,
|
|
1012
|
+
presence: data.presence as TPresence,
|
|
1013
|
+
user: message.user as Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>>,
|
|
1014
|
+
});
|
|
1015
|
+
const other = this._usersManager.getOther(connectionId);
|
|
907
1016
|
const others = this._usersManager.getOthers();
|
|
908
1017
|
|
|
1018
|
+
this._usersNotifier.other(added.clientId).next(other);
|
|
909
1019
|
this._stateNotifier.subjects.others.next(others);
|
|
1020
|
+
|
|
1021
|
+
this._usersNotifier.others.next({
|
|
1022
|
+
others,
|
|
1023
|
+
event: { kind: "update", user: added.data },
|
|
1024
|
+
});
|
|
910
1025
|
}
|
|
911
1026
|
|
|
912
1027
|
private _handleReceiveOthers(message: IOEventMessage<TIO>): void {
|
|
@@ -931,12 +1046,16 @@ export class PluvRoom<
|
|
|
931
1046
|
const clientId = result.clientId;
|
|
932
1047
|
const other = this._usersManager.getOther(connectionId);
|
|
933
1048
|
|
|
934
|
-
this.
|
|
1049
|
+
this._usersNotifier.other(clientId).next(other);
|
|
935
1050
|
});
|
|
936
1051
|
|
|
937
1052
|
const others = this._usersManager.getOthers();
|
|
938
1053
|
|
|
939
1054
|
this._stateNotifier.subjects.others.next(others);
|
|
1055
|
+
this._usersNotifier.others.next({
|
|
1056
|
+
others,
|
|
1057
|
+
event: { kind: "sync", users: others },
|
|
1058
|
+
});
|
|
940
1059
|
}
|
|
941
1060
|
|
|
942
1061
|
private async _handleRegisteredMessage(message: IOEventMessage<TIO>): Promise<void> {
|
|
@@ -951,7 +1070,6 @@ export class PluvRoom<
|
|
|
951
1070
|
const state = data.state;
|
|
952
1071
|
|
|
953
1072
|
this._updateState((oldState) => {
|
|
954
|
-
oldState.connection.count += 1;
|
|
955
1073
|
oldState.connection.id = connectionId;
|
|
956
1074
|
oldState.authorization.user = user ?? null;
|
|
957
1075
|
|
|
@@ -1024,6 +1142,11 @@ export class PluvRoom<
|
|
|
1024
1142
|
|
|
1025
1143
|
this._emitSharedTypes();
|
|
1026
1144
|
this._observeCrdt();
|
|
1145
|
+
this._updateState((oldState) => {
|
|
1146
|
+
oldState.storage.state = StorageState.Synced;
|
|
1147
|
+
|
|
1148
|
+
return oldState;
|
|
1149
|
+
});
|
|
1027
1150
|
this._stateNotifier.subjects["storage-loaded"].next(true);
|
|
1028
1151
|
|
|
1029
1152
|
this._sendMessage({
|
|
@@ -1083,6 +1206,13 @@ export class PluvRoom<
|
|
|
1083
1206
|
|
|
1084
1207
|
quitters.forEach((quitter) => {
|
|
1085
1208
|
this._usersManager.deleteConnection(quitter.connectionId);
|
|
1209
|
+
|
|
1210
|
+
const remaining = this._usersManager.getOthers();
|
|
1211
|
+
|
|
1212
|
+
this._usersNotifier.others.next({
|
|
1213
|
+
others: remaining,
|
|
1214
|
+
event: { kind: "leave", user: quitter },
|
|
1215
|
+
});
|
|
1086
1216
|
});
|
|
1087
1217
|
|
|
1088
1218
|
const remaining = this._usersManager.getOthers();
|
|
@@ -1115,8 +1245,13 @@ export class PluvRoom<
|
|
|
1115
1245
|
const other = this._usersManager.getOther(connectionId);
|
|
1116
1246
|
const others = this._usersManager.getOthers();
|
|
1117
1247
|
|
|
1118
|
-
this.
|
|
1248
|
+
this._usersNotifier.other(added.clientId).next(other);
|
|
1119
1249
|
this._stateNotifier.subjects.others.next(others);
|
|
1250
|
+
|
|
1251
|
+
this._usersNotifier.others.next({
|
|
1252
|
+
others,
|
|
1253
|
+
event: { kind: "enter", user: added.data },
|
|
1254
|
+
});
|
|
1120
1255
|
}
|
|
1121
1256
|
|
|
1122
1257
|
private _heartbeat(): void {
|
|
@@ -1175,12 +1310,15 @@ export class PluvRoom<
|
|
|
1175
1310
|
|
|
1176
1311
|
this._updateState((oldState) => {
|
|
1177
1312
|
oldState.connection.state = ConnectionState.Unavailable;
|
|
1313
|
+
oldState.storage.state = StorageState.Unavailable;
|
|
1178
1314
|
|
|
1179
1315
|
return oldState;
|
|
1180
1316
|
});
|
|
1181
1317
|
}
|
|
1182
1318
|
|
|
1183
1319
|
private async _onClose(event: CloseEvent): Promise<void> {
|
|
1320
|
+
this._listenerManager.subjects.close.next(event);
|
|
1321
|
+
|
|
1184
1322
|
this._logDebug("WebSocket closed");
|
|
1185
1323
|
if (!!event.reason) this._logDebug(event.reason);
|
|
1186
1324
|
|
|
@@ -1210,6 +1348,10 @@ export class PluvRoom<
|
|
|
1210
1348
|
* @date April 28, 2025
|
|
1211
1349
|
*/
|
|
1212
1350
|
ConnectionState.Closed;
|
|
1351
|
+
oldState.storage.state =
|
|
1352
|
+
oldState.storage.state === StorageState.Synced
|
|
1353
|
+
? StorageState.Offline
|
|
1354
|
+
: StorageState.Unavailable;
|
|
1213
1355
|
oldState.webSocket = null;
|
|
1214
1356
|
|
|
1215
1357
|
return oldState;
|
|
@@ -1229,9 +1371,13 @@ export class PluvRoom<
|
|
|
1229
1371
|
* @description Handle websocket errors in a meaningful way, should they occur
|
|
1230
1372
|
* @date August 19, 2022
|
|
1231
1373
|
*/
|
|
1232
|
-
private _onError(): void {
|
|
1374
|
+
private _onError(event: Event): void {
|
|
1375
|
+
this._listenerManager.subjects.error.next(event);
|
|
1376
|
+
}
|
|
1233
1377
|
|
|
1234
1378
|
private _onMessage(event: MessageEvent<string>): void {
|
|
1379
|
+
this._listenerManager.subjects.message.next(event);
|
|
1380
|
+
|
|
1235
1381
|
const message = this._parseMessage(event);
|
|
1236
1382
|
|
|
1237
1383
|
if (!message) return;
|
|
@@ -1288,7 +1434,9 @@ export class PluvRoom<
|
|
|
1288
1434
|
}
|
|
1289
1435
|
}
|
|
1290
1436
|
|
|
1291
|
-
private _onOpen(): void {
|
|
1437
|
+
private _onOpen(event: Event): void {
|
|
1438
|
+
this._listenerManager.subjects.open.next(event);
|
|
1439
|
+
|
|
1292
1440
|
if (this._state.connection.state !== ConnectionState.Connecting) return;
|
|
1293
1441
|
|
|
1294
1442
|
this._logDebug("WebSocket connected");
|
|
@@ -1310,6 +1458,10 @@ export class PluvRoom<
|
|
|
1310
1458
|
|
|
1311
1459
|
this._updateState((oldState) => {
|
|
1312
1460
|
oldState.connection.state = ConnectionState.Unavailable;
|
|
1461
|
+
oldState.storage.state =
|
|
1462
|
+
oldState.storage.state === StorageState.Synced
|
|
1463
|
+
? StorageState.Offline
|
|
1464
|
+
: StorageState.Unavailable;
|
|
1313
1465
|
|
|
1314
1466
|
return oldState;
|
|
1315
1467
|
});
|
|
@@ -1460,6 +1612,7 @@ export class PluvRoom<
|
|
|
1460
1612
|
...this._state,
|
|
1461
1613
|
authorization,
|
|
1462
1614
|
connection: JSON.parse(JSON.stringify(this._state.connection)),
|
|
1615
|
+
storage: JSON.parse(JSON.stringify(this._state.storage)),
|
|
1463
1616
|
webSocket: this._state.webSocket,
|
|
1464
1617
|
};
|
|
1465
1618
|
|