@pluv/client 0.44.2 → 1.0.1
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 +24 -0
- package/dist/index.d.mts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +71 -43
- package/dist/index.mjs +71 -43
- package/package.json +5 -5
- package/src/AbstractRoom.ts +3 -1
- package/src/CrdtManager.ts +35 -23
- package/src/CrdtNotifier.ts +4 -1
- package/src/EventNotifier.ts +11 -5
- package/src/MockedRoom.ts +21 -11
- package/src/OtherNotifier.ts +8 -2
- package/src/PluvClient.ts +3 -1
- package/src/PluvProcedure.ts +6 -2
- package/src/PluvRoom.ts +90 -45
- package/src/PluvRouter.ts +4 -1
- package/src/StateNotifier.ts +4 -1
- package/src/UsersManager.ts +23 -5
- package/src/index.ts +8 -1
- package/src/infer.ts +3 -1
- package/src/register.ts +2 -1
- package/src/types.ts +7 -2
- package/src/utils/pickBy.ts +4 -1
package/src/CrdtManager.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { AbstractCrdtDoc, AbstractCrdtDocFactory, CrdtType } from "@pluv/cr
|
|
|
2
2
|
import { noop } from "@pluv/crdt";
|
|
3
3
|
|
|
4
4
|
export type CrdtManagerOptions<TStorage extends Record<string, CrdtType<any, any>> = {}> = {
|
|
5
|
-
encodedState?: string | Uint8Array | null;
|
|
6
5
|
initialStorage?: AbstractCrdtDocFactory<TStorage>;
|
|
7
6
|
};
|
|
8
7
|
|
|
@@ -19,13 +18,10 @@ export class CrdtManager<TStorage extends Record<string, CrdtType<any, any>>> {
|
|
|
19
18
|
private readonly _docFactory: AbstractCrdtDocFactory<TStorage>;
|
|
20
19
|
|
|
21
20
|
constructor(options: CrdtManagerOptions<TStorage>) {
|
|
22
|
-
const {
|
|
21
|
+
const { initialStorage = noop.doc({}) as AbstractCrdtDocFactory<TStorage> } = options;
|
|
23
22
|
|
|
24
23
|
this._docFactory = initialStorage;
|
|
25
|
-
|
|
26
|
-
this.doc = encodedState
|
|
27
|
-
? initialStorage.getFresh().applyEncodedState({ update: encodedState })
|
|
28
|
-
: initialStorage.getFresh();
|
|
24
|
+
this.doc = initialStorage.getEmpty();
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
/**
|
|
@@ -52,35 +48,51 @@ export class CrdtManager<TStorage extends Record<string, CrdtType<any, any>>> {
|
|
|
52
48
|
return this.doc.get(key);
|
|
53
49
|
}
|
|
54
50
|
|
|
51
|
+
public getInitialState(): string {
|
|
52
|
+
return this._docFactory.getInitialized().getEncodedState();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
55
|
public initialize(params: CrdtManagerInitializeParams): this {
|
|
56
56
|
const { onInitialized, origin, update } = params;
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
/**
|
|
59
|
+
* !HACK
|
|
60
|
+
* @description If the doc was already initialized, we don't want to re-initialize
|
|
61
|
+
* the doc and overwrite what's already there. Skip the initialization and log a
|
|
62
|
+
* warning.
|
|
63
|
+
* @date May 7, 2025
|
|
64
|
+
*/
|
|
62
65
|
if (this.initialized) {
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
console.warn("Attempted to initialize storage multiple times");
|
|
65
67
|
return this;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* !HACK
|
|
72
|
+
* @description If the doc is already populated, somehow we're attempting to overwrite
|
|
73
|
+
* a doc that was presumably initialized but not identified as initialized. Skip the
|
|
74
|
+
* initialization and log a warning.
|
|
75
|
+
* @date May 7, 2025
|
|
76
|
+
*/
|
|
77
|
+
if (!this.doc.isEmpty()) {
|
|
78
|
+
console.warn("Attempted to initialize over a non-empty storage");
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
this.initialized = true;
|
|
82
|
+
const updates = typeof update === "string" ? [update] : update;
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
if (!updates.length) return this;
|
|
77
85
|
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
const initialized = this._docFactory.getInitialized();
|
|
87
|
+
const reference = initialized.get();
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
this.doc = this._applyDocUpdates(
|
|
90
|
+
this._docFactory.getEmpty(),
|
|
91
|
+
updates,
|
|
92
|
+
origin,
|
|
93
|
+
).rebuildStorage(reference);
|
|
83
94
|
this.initialized = true;
|
|
95
|
+
initialized.destroy();
|
|
84
96
|
|
|
85
97
|
onInitialized?.(this.doc.getEncodedState());
|
|
86
98
|
|
package/src/CrdtNotifier.ts
CHANGED
|
@@ -7,7 +7,10 @@ export class CrdtNotifier<TStorage extends Record<string, CrdtType<any, any>>> {
|
|
|
7
7
|
[P in keyof TStorage]: InferCrdtJson<TStorage[P]>;
|
|
8
8
|
}>();
|
|
9
9
|
|
|
10
|
-
public subjects = new Map<
|
|
10
|
+
public subjects = new Map<
|
|
11
|
+
keyof TStorage,
|
|
12
|
+
Subject<InferCrdtJson<TStorage[keyof TStorage]>> | null
|
|
13
|
+
>();
|
|
11
14
|
|
|
12
15
|
public subject<TKey extends keyof TStorage>(key: TKey): Subject<InferCrdtJson<TStorage[TKey]>> {
|
|
13
16
|
const subject = this.subjects.get(key);
|
package/src/EventNotifier.ts
CHANGED
|
@@ -2,14 +2,20 @@ import type { Id, InferIOOutput, IOEventMessage, IOLike } from "@pluv/types";
|
|
|
2
2
|
import type { Subject } from "wonka";
|
|
3
3
|
import { makeSubject, subscribe } from "wonka";
|
|
4
4
|
|
|
5
|
-
export type EventNotifierSubscriptionCallback<
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export type EventNotifierSubscriptionCallback<
|
|
6
|
+
TIO extends IOLike<any, any>,
|
|
7
|
+
TEvent extends keyof InferIOOutput<TIO>,
|
|
8
|
+
> = (value: Id<IOEventMessage<TIO, TEvent>>) => void;
|
|
8
9
|
|
|
9
10
|
export class EventNotifier<TIO extends IOLike> {
|
|
10
|
-
public subjects = new Map<
|
|
11
|
+
public subjects = new Map<
|
|
12
|
+
keyof InferIOOutput<TIO>,
|
|
13
|
+
Subject<Id<IOEventMessage<TIO, keyof InferIOOutput<TIO>>>>
|
|
14
|
+
>();
|
|
11
15
|
|
|
12
|
-
public subject<TEvent extends keyof InferIOOutput<TIO>>(
|
|
16
|
+
public subject<TEvent extends keyof InferIOOutput<TIO>>(
|
|
17
|
+
name: TEvent,
|
|
18
|
+
): Subject<Id<IOEventMessage<TIO, TEvent>>> {
|
|
13
19
|
const subject = this.subjects.get(name);
|
|
14
20
|
|
|
15
21
|
if (subject) return subject;
|
package/src/MockedRoom.ts
CHANGED
|
@@ -86,8 +86,13 @@ export class MockedRoom<
|
|
|
86
86
|
presenceMaxSize: MAX_PRESENCE_SIZE_BYTES,
|
|
87
87
|
...limits,
|
|
88
88
|
};
|
|
89
|
-
this._router =
|
|
90
|
-
|
|
89
|
+
this._router =
|
|
90
|
+
router ?? (new PluvRouter({}) as PluvRouter<TIO, TPresence, TStorage, TEvents>);
|
|
91
|
+
this._usersManager = new UsersManager<TIO, TPresence>({
|
|
92
|
+
initialPresence,
|
|
93
|
+
limits: this._limits,
|
|
94
|
+
presence,
|
|
95
|
+
});
|
|
91
96
|
|
|
92
97
|
this._crdtManager = new CrdtManager<TStorage>({
|
|
93
98
|
initialStorage,
|
|
@@ -136,10 +141,9 @@ export class MockedRoom<
|
|
|
136
141
|
user: myself,
|
|
137
142
|
};
|
|
138
143
|
|
|
139
|
-
const output = await (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
144
|
+
const output = await (
|
|
145
|
+
procedure.config.broadcast as EventResolver<TIO, any, any, TPresence, TStorage>
|
|
146
|
+
)(parsed, context);
|
|
143
147
|
|
|
144
148
|
Object.entries(output).forEach(([_type, _data]) => {
|
|
145
149
|
this._simulateEvent(_type as TEvent, _data as any);
|
|
@@ -147,7 +151,9 @@ export class MockedRoom<
|
|
|
147
151
|
},
|
|
148
152
|
{
|
|
149
153
|
get(fn, prop) {
|
|
150
|
-
return async (
|
|
154
|
+
return async (
|
|
155
|
+
data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[any]>,
|
|
156
|
+
): Promise<void> => {
|
|
151
157
|
return await fn(prop, data);
|
|
152
158
|
};
|
|
153
159
|
},
|
|
@@ -174,8 +180,9 @@ export class MockedRoom<
|
|
|
174
180
|
): (() => void) => this._eventNotifier.subscribe(event, callback),
|
|
175
181
|
{
|
|
176
182
|
get(fn, prop) {
|
|
177
|
-
return (
|
|
178
|
-
|
|
183
|
+
return (
|
|
184
|
+
callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, any>,
|
|
185
|
+
): (() => void) => fn(prop as any, callback);
|
|
179
186
|
},
|
|
180
187
|
},
|
|
181
188
|
) as (<TEvent extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
|
|
@@ -221,7 +228,9 @@ export class MockedRoom<
|
|
|
221
228
|
};
|
|
222
229
|
|
|
223
230
|
public getStorageJson(): InferCrdtJson<TStorage> | null;
|
|
224
|
-
public getStorageJson<TKey extends keyof TStorage>(
|
|
231
|
+
public getStorageJson<TKey extends keyof TStorage>(
|
|
232
|
+
type: TKey,
|
|
233
|
+
): InferCrdtJson<TStorage[TKey]> | null;
|
|
225
234
|
public getStorageJson<TKey extends keyof TStorage>(type?: TKey) {
|
|
226
235
|
if (this._state.connection.id === null) return null;
|
|
227
236
|
|
|
@@ -293,7 +302,8 @@ export class MockedRoom<
|
|
|
293
302
|
};
|
|
294
303
|
|
|
295
304
|
public updateMyPresence = (presence: UpdateMyPresenceAction<TPresence>): void => {
|
|
296
|
-
const newPresence =
|
|
305
|
+
const newPresence =
|
|
306
|
+
typeof presence === "function" ? presence(this.getMyPresence()) : presence;
|
|
297
307
|
|
|
298
308
|
this._usersManager.updateMyPresence(newPresence);
|
|
299
309
|
|
package/src/OtherNotifier.ts
CHANGED
|
@@ -8,7 +8,10 @@ export type OtherNotifierSubscriptionCallback<TIO extends IOLike, TPresence exte
|
|
|
8
8
|
) => void;
|
|
9
9
|
|
|
10
10
|
export class OtherNotifier<TIO extends IOLike, TPresence extends JsonObject = {}> {
|
|
11
|
-
private _subjects = new Map<
|
|
11
|
+
private _subjects = new Map<
|
|
12
|
+
[clientId: string][0],
|
|
13
|
+
Subject<Id<UserInfo<TIO, TPresence>> | null>
|
|
14
|
+
>();
|
|
12
15
|
|
|
13
16
|
public clear(): void {
|
|
14
17
|
this._subjects.forEach((subject) => {
|
|
@@ -35,7 +38,10 @@ export class OtherNotifier<TIO extends IOLike, TPresence extends JsonObject = {}
|
|
|
35
38
|
return newSubject;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
public subscribe(
|
|
41
|
+
public subscribe(
|
|
42
|
+
clientId: string,
|
|
43
|
+
callback: OtherNotifierSubscriptionCallback<TIO, TPresence>,
|
|
44
|
+
): () => void {
|
|
39
45
|
const source = this.subject(clientId).source;
|
|
40
46
|
|
|
41
47
|
return subscribe(callback)(source).unsubscribe;
|
package/src/PluvClient.ts
CHANGED
|
@@ -158,7 +158,9 @@ export class PluvClient<
|
|
|
158
158
|
};
|
|
159
159
|
|
|
160
160
|
public getRoom = (room: string): PluvRoom<TIO, TMetadata, TPresence, TStorage, any> | null => {
|
|
161
|
-
const found = this._rooms.get(room) as
|
|
161
|
+
const found = this._rooms.get(room) as
|
|
162
|
+
| PluvRoom<TIO, TMetadata, TPresence, TStorage, any>
|
|
163
|
+
| undefined;
|
|
162
164
|
|
|
163
165
|
return found ?? null;
|
|
164
166
|
};
|
package/src/PluvProcedure.ts
CHANGED
|
@@ -21,7 +21,8 @@ export class PluvProcedure<
|
|
|
21
21
|
TStorage extends Record<string, CrdtType<any, any>> = {},
|
|
22
22
|
TFilled extends "input" | "broadcast" | "" = "",
|
|
23
23
|
> {
|
|
24
|
-
private _broadcast: EventResolver<TIO, TInput, Partial<TOutput>, TPresence, TStorage> | null =
|
|
24
|
+
private _broadcast: EventResolver<TIO, TInput, Partial<TOutput>, TPresence, TStorage> | null =
|
|
25
|
+
null;
|
|
25
26
|
private _input: InputZodLike<TInput> | null = null;
|
|
26
27
|
|
|
27
28
|
public get config(): ProcedureLike<TInput, TOutput>["config"] {
|
|
@@ -71,7 +72,10 @@ export class PluvProcedure<
|
|
|
71
72
|
|
|
72
73
|
public input<TData extends JsonObject>(
|
|
73
74
|
input: InputZodLike<TData>,
|
|
74
|
-
): Omit<
|
|
75
|
+
): Omit<
|
|
76
|
+
PluvProcedure<TIO, TData, {}, TPresence, TStorage, TFilled | "input">,
|
|
77
|
+
TFilled | "input"
|
|
78
|
+
> {
|
|
75
79
|
return new PluvProcedure<TIO, TData, {}, TPresence, TStorage, TFilled | "input">({ input });
|
|
76
80
|
}
|
|
77
81
|
|
package/src/PluvRoom.ts
CHANGED
|
@@ -109,7 +109,9 @@ export type AuthEndpoint<TMetadata extends JsonObject> =
|
|
|
109
109
|
| string
|
|
110
110
|
| ((params: EndpointParams<TMetadata>) => string | FetchOptions)
|
|
111
111
|
| true;
|
|
112
|
-
export type WsEndpoint<TMetadata extends JsonObject> =
|
|
112
|
+
export type WsEndpoint<TMetadata extends JsonObject> =
|
|
113
|
+
| string
|
|
114
|
+
| ((params: EndpointParams<TMetadata>) => string);
|
|
113
115
|
|
|
114
116
|
type FetchOptions = { url: string; options?: RequestInit };
|
|
115
117
|
|
|
@@ -128,7 +130,9 @@ export type PluvRoomAddon<
|
|
|
128
130
|
TMetadata extends JsonObject = {},
|
|
129
131
|
TPresence extends JsonObject = {},
|
|
130
132
|
TStorage extends Record<string, CrdtType<any, any>> = {},
|
|
131
|
-
> = (
|
|
133
|
+
> = (
|
|
134
|
+
input: PluvRoomAddonInput<TIO, TMetadata, TPresence, TStorage>,
|
|
135
|
+
) => Partial<PluvRoomAddonResult>;
|
|
132
136
|
|
|
133
137
|
export interface PluvRoomAddonInput<
|
|
134
138
|
TIO extends IOLike = IOLike,
|
|
@@ -276,8 +280,13 @@ export class PluvRoom<
|
|
|
276
280
|
},
|
|
277
281
|
};
|
|
278
282
|
|
|
279
|
-
this._router =
|
|
280
|
-
|
|
283
|
+
this._router =
|
|
284
|
+
router ?? (new PluvRouter({}) as PluvRouter<TIO, TPresence, TStorage, TEvents>);
|
|
285
|
+
this._usersManager = new UsersManager<TIO, TPresence>({
|
|
286
|
+
initialPresence,
|
|
287
|
+
limits: this._limits,
|
|
288
|
+
presence,
|
|
289
|
+
});
|
|
281
290
|
this._crdtManager = new CrdtManager<TStorage>({ initialStorage });
|
|
282
291
|
}
|
|
283
292
|
|
|
@@ -325,10 +334,9 @@ export class PluvRoom<
|
|
|
325
334
|
user: myself,
|
|
326
335
|
};
|
|
327
336
|
|
|
328
|
-
const output = await (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
);
|
|
337
|
+
const output = await (
|
|
338
|
+
procedure.config.broadcast as EventResolver<TIO, any, any, TPresence, TStorage>
|
|
339
|
+
)(parsed, context);
|
|
332
340
|
|
|
333
341
|
Object.entries(output).forEach(([_type, _data]) => {
|
|
334
342
|
this._sendMessage({ data: _data, type: _type });
|
|
@@ -336,7 +344,9 @@ export class PluvRoom<
|
|
|
336
344
|
},
|
|
337
345
|
{
|
|
338
346
|
get(fn, prop) {
|
|
339
|
-
return async (
|
|
347
|
+
return async (
|
|
348
|
+
data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[any]>,
|
|
349
|
+
): Promise<void> => {
|
|
340
350
|
return await fn(prop, data);
|
|
341
351
|
};
|
|
342
352
|
},
|
|
@@ -364,9 +374,11 @@ export class PluvRoom<
|
|
|
364
374
|
|
|
365
375
|
this._setMetadata(metadata);
|
|
366
376
|
|
|
367
|
-
const canConnect = [
|
|
368
|
-
|
|
369
|
-
|
|
377
|
+
const canConnect = [
|
|
378
|
+
ConnectionState.Closed,
|
|
379
|
+
ConnectionState.Unavailable,
|
|
380
|
+
ConnectionState.Untouched,
|
|
381
|
+
].some((state) => this._state.connection.state === state);
|
|
370
382
|
|
|
371
383
|
if (!canConnect) return;
|
|
372
384
|
|
|
@@ -377,8 +389,8 @@ export class PluvRoom<
|
|
|
377
389
|
return oldState;
|
|
378
390
|
});
|
|
379
391
|
|
|
380
|
-
await this._storageStore.initialize();
|
|
381
|
-
await this._applyStorageStore();
|
|
392
|
+
// await this._storageStore.initialize();
|
|
393
|
+
// await this._applyStorageStore();
|
|
382
394
|
|
|
383
395
|
const wsEndpoint = this._getWsEndpoint(this.id, params);
|
|
384
396
|
const url = new URL(wsEndpoint);
|
|
@@ -453,8 +465,9 @@ export class PluvRoom<
|
|
|
453
465
|
): (() => void) => this._eventNotifier.subscribe(event, callback),
|
|
454
466
|
{
|
|
455
467
|
get(fn, prop) {
|
|
456
|
-
return (
|
|
457
|
-
|
|
468
|
+
return (
|
|
469
|
+
callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, any>,
|
|
470
|
+
): (() => void) => fn(prop as any, callback);
|
|
458
471
|
},
|
|
459
472
|
},
|
|
460
473
|
) as (<TEvent extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
|
|
@@ -500,7 +513,9 @@ export class PluvRoom<
|
|
|
500
513
|
};
|
|
501
514
|
|
|
502
515
|
public getStorageJson(): InferCrdtJson<TStorage> | null;
|
|
503
|
-
public getStorageJson<TKey extends keyof TStorage>(
|
|
516
|
+
public getStorageJson<TKey extends keyof TStorage>(
|
|
517
|
+
type: TKey,
|
|
518
|
+
): InferCrdtJson<TStorage[TKey]> | null;
|
|
504
519
|
public getStorageJson<TKey extends keyof TStorage>(type?: TKey) {
|
|
505
520
|
if (this._state.connection.id === null) return null;
|
|
506
521
|
|
|
@@ -574,7 +589,8 @@ export class PluvRoom<
|
|
|
574
589
|
};
|
|
575
590
|
|
|
576
591
|
public updateMyPresence = (presence: UpdateMyPresenceAction<TPresence>): void => {
|
|
577
|
-
const newPresence =
|
|
592
|
+
const newPresence =
|
|
593
|
+
typeof presence === "function" ? presence(this.getMyPresence()) : presence;
|
|
578
594
|
|
|
579
595
|
this._usersManager.updateMyPresence(newPresence);
|
|
580
596
|
|
|
@@ -743,7 +759,10 @@ export class PluvRoom<
|
|
|
743
759
|
);
|
|
744
760
|
};
|
|
745
761
|
|
|
746
|
-
private _getAuthFetchOptions(
|
|
762
|
+
private _getAuthFetchOptions(
|
|
763
|
+
room: string,
|
|
764
|
+
params: GetAuthEndpointParams<TMetadata>,
|
|
765
|
+
): FetchOptions | null {
|
|
747
766
|
const metadata = params.metadata as TMetadata;
|
|
748
767
|
|
|
749
768
|
if (typeof this._endpoints.authEndpoint === "undefined") return null;
|
|
@@ -767,7 +786,10 @@ export class PluvRoom<
|
|
|
767
786
|
return typeof result === "string" ? { url: result, options: {} } : result;
|
|
768
787
|
}
|
|
769
788
|
|
|
770
|
-
private async _getAuthorization(
|
|
789
|
+
private async _getAuthorization(
|
|
790
|
+
room: string,
|
|
791
|
+
params: GetAuthEndpointParams<TMetadata>,
|
|
792
|
+
): Promise<string | null> {
|
|
771
793
|
const fetchOptions = this._getAuthFetchOptions(room, params);
|
|
772
794
|
|
|
773
795
|
if (!fetchOptions) return null;
|
|
@@ -798,7 +820,9 @@ export class PluvRoom<
|
|
|
798
820
|
|
|
799
821
|
switch (typeof this._endpoints.wsEndpoint) {
|
|
800
822
|
case "undefined":
|
|
801
|
-
return !!this._getPublicKey(params)
|
|
823
|
+
return !!this._getPublicKey(params)
|
|
824
|
+
? `wss://rooms.pluv.io/api/room/${room}`
|
|
825
|
+
: `/api/pluv/room/${room}`;
|
|
802
826
|
case "string":
|
|
803
827
|
return this._endpoints.wsEndpoint;
|
|
804
828
|
default:
|
|
@@ -926,6 +950,12 @@ export class PluvRoom<
|
|
|
926
950
|
if (!this._state.webSocket) throw new Error("Could not find WebSocket");
|
|
927
951
|
|
|
928
952
|
const data = message.data as BaseIOEventRecord<InferIOAuthorize<TIO>>["$registered"];
|
|
953
|
+
/**
|
|
954
|
+
* TODO
|
|
955
|
+
* @description Maybe we can use this to resolve storage initialization more efficiently
|
|
956
|
+
* @date May 7, 2025
|
|
957
|
+
*/
|
|
958
|
+
const state = data.state;
|
|
929
959
|
|
|
930
960
|
this._updateState((oldState) => {
|
|
931
961
|
oldState.connection.count += 1;
|
|
@@ -948,7 +978,7 @@ export class PluvRoom<
|
|
|
948
978
|
this._stateNotifier.subjects["my-presence"].next(presence);
|
|
949
979
|
this._stateNotifier.subjects.myself.next(myself);
|
|
950
980
|
|
|
951
|
-
const update = this.
|
|
981
|
+
const update = this._crdtManager.getInitialState();
|
|
952
982
|
|
|
953
983
|
this._sendMessage({
|
|
954
984
|
type: "$initializeSession",
|
|
@@ -964,17 +994,17 @@ export class PluvRoom<
|
|
|
964
994
|
if (!this._state.webSocket) throw new Error("Could not find WebSocket");
|
|
965
995
|
|
|
966
996
|
const data = message.data as BaseIOEventRecord<InferIOAuthorize<TIO>>["$storageReceived"];
|
|
997
|
+
const state = data.state;
|
|
967
998
|
|
|
968
999
|
this._crdtManager.initialize({
|
|
969
1000
|
onInitialized: async (update) => {
|
|
970
|
-
this._addToStorageStore(update);
|
|
1001
|
+
await this._addToStorageStore(update);
|
|
971
1002
|
this._emitSharedTypes();
|
|
972
|
-
|
|
973
1003
|
this._observeCrdt();
|
|
974
1004
|
this._stateNotifier.subjects["storage-loaded"].next(true);
|
|
975
1005
|
},
|
|
976
1006
|
origin: ORIGIN_INITIALIZED,
|
|
977
|
-
update:
|
|
1007
|
+
update: state,
|
|
978
1008
|
});
|
|
979
1009
|
|
|
980
1010
|
const encodedState = this._crdtManager.doc.getEncodedState();
|
|
@@ -998,9 +1028,9 @@ export class PluvRoom<
|
|
|
998
1028
|
// Should not reach here
|
|
999
1029
|
if (!this._state.webSocket) throw new Error("Could not find WebSocket");
|
|
1000
1030
|
|
|
1001
|
-
const data = message.data as BaseIOEventRecord<
|
|
1002
|
-
|
|
1003
|
-
|
|
1031
|
+
const data = message.data as BaseIOEventRecord<
|
|
1032
|
+
InferIOAuthorize<TIO>
|
|
1033
|
+
>[typeof ORIGIN_STORAGE_UPDATED];
|
|
1004
1034
|
|
|
1005
1035
|
this._crdtManager.doc.applyEncodedState({
|
|
1006
1036
|
origin: ORIGIN_STORAGE_UPDATED,
|
|
@@ -1011,8 +1041,6 @@ export class PluvRoom<
|
|
|
1011
1041
|
|
|
1012
1042
|
const storageRoot = Object.keys(sharedTypes).reduce(
|
|
1013
1043
|
(acc, prop) => {
|
|
1014
|
-
if (!this._crdtManager) return acc;
|
|
1015
|
-
|
|
1016
1044
|
const serialized = this._crdtManager.doc.toJson(prop);
|
|
1017
1045
|
|
|
1018
1046
|
this._crdtNotifier.subject(prop).next(serialized);
|
|
@@ -1038,7 +1066,9 @@ export class PluvRoom<
|
|
|
1038
1066
|
const data = message.data as BaseIOEventRecord<InferIOAuthorize<TIO>>["$syncStateReceived"];
|
|
1039
1067
|
const active = new Set(data?.connectionIds ?? []);
|
|
1040
1068
|
|
|
1041
|
-
const quitters = this._usersManager
|
|
1069
|
+
const quitters = this._usersManager
|
|
1070
|
+
.getOthers()
|
|
1071
|
+
.filter((other) => !active.has(other.connectionId));
|
|
1042
1072
|
|
|
1043
1073
|
quitters.forEach((quitter) => {
|
|
1044
1074
|
this._usersManager.deleteConnection(quitter.connectionId);
|
|
@@ -1080,7 +1110,10 @@ export class PluvRoom<
|
|
|
1080
1110
|
|
|
1081
1111
|
private _heartbeat(): void {
|
|
1082
1112
|
this._clearTimeout(this._timeouts.pong);
|
|
1083
|
-
this._timeouts.pong = setTimeout(
|
|
1113
|
+
this._timeouts.pong = setTimeout(
|
|
1114
|
+
this._reconnect.bind(this),
|
|
1115
|
+
PONG_TIMEOUT_MS,
|
|
1116
|
+
) as unknown as number;
|
|
1084
1117
|
|
|
1085
1118
|
/**
|
|
1086
1119
|
* !HACK
|
|
@@ -1099,8 +1132,6 @@ export class PluvRoom<
|
|
|
1099
1132
|
private _observeCrdt(): void {
|
|
1100
1133
|
this._subscriptions.observeCrdt?.();
|
|
1101
1134
|
|
|
1102
|
-
if (!this._crdtManager) return;
|
|
1103
|
-
|
|
1104
1135
|
const unsubscribe = this._crdtManager.doc.subscribe((event) => {
|
|
1105
1136
|
const origin = event.origin ?? null;
|
|
1106
1137
|
|
|
@@ -1193,7 +1224,9 @@ export class PluvRoom<
|
|
|
1193
1224
|
if (!message) return;
|
|
1194
1225
|
|
|
1195
1226
|
const shouldLog =
|
|
1196
|
-
typeof this._debug === "boolean"
|
|
1227
|
+
typeof this._debug === "boolean"
|
|
1228
|
+
? this._debug
|
|
1229
|
+
: this._debug.output.find((value) => value === message.type);
|
|
1197
1230
|
|
|
1198
1231
|
if (shouldLog) {
|
|
1199
1232
|
this._logDebug("WebSocket event received: ", message.type, message);
|
|
@@ -1246,7 +1279,6 @@ export class PluvRoom<
|
|
|
1246
1279
|
if (this._state.connection.state !== ConnectionState.Connecting) return;
|
|
1247
1280
|
|
|
1248
1281
|
this._logDebug("WebSocket connected");
|
|
1249
|
-
|
|
1250
1282
|
this._updateState((oldState) => {
|
|
1251
1283
|
oldState.connection.state = ConnectionState.Open;
|
|
1252
1284
|
|
|
@@ -1254,7 +1286,10 @@ export class PluvRoom<
|
|
|
1254
1286
|
});
|
|
1255
1287
|
|
|
1256
1288
|
this._clearInterval(this._intervals.heartbeat);
|
|
1257
|
-
this._intervals.heartbeat = setInterval(
|
|
1289
|
+
this._intervals.heartbeat = setInterval(
|
|
1290
|
+
this._heartbeat.bind(this),
|
|
1291
|
+
HEARTBEAT_INTERVAL_MS,
|
|
1292
|
+
) as unknown as number;
|
|
1258
1293
|
}
|
|
1259
1294
|
|
|
1260
1295
|
private async _onNavigatorOffline(): Promise<void> {
|
|
@@ -1325,9 +1360,15 @@ export class PluvRoom<
|
|
|
1325
1360
|
typeof this._reconnectTimeoutMs === "number"
|
|
1326
1361
|
? this._reconnectTimeoutMs
|
|
1327
1362
|
: this._reconnectTimeoutMs({ attempts: this._state.connection.attempts });
|
|
1328
|
-
const clampedMs = Math.max(
|
|
1363
|
+
const clampedMs = Math.max(
|
|
1364
|
+
Math.min(MIN_RECONNECT_TIMEOUT_MS, timeoutMs),
|
|
1365
|
+
MAX_RECONNECT_TIMEOUT_MS,
|
|
1366
|
+
);
|
|
1329
1367
|
|
|
1330
|
-
this._timeouts.reconnect = setTimeout(
|
|
1368
|
+
this._timeouts.reconnect = setTimeout(
|
|
1369
|
+
this._reconnect.bind(this),
|
|
1370
|
+
clampedMs,
|
|
1371
|
+
) as unknown as number;
|
|
1331
1372
|
}
|
|
1332
1373
|
|
|
1333
1374
|
private async _reconnect(): Promise<void> {
|
|
@@ -1342,13 +1383,17 @@ export class PluvRoom<
|
|
|
1342
1383
|
if (this._state.connection.state === ConnectionState.Closed) return;
|
|
1343
1384
|
|
|
1344
1385
|
const metadata = this._lastMetadata;
|
|
1345
|
-
const params = (
|
|
1386
|
+
const params = (
|
|
1387
|
+
typeof metadata === "undefined" ? [] : [{ metadata }]
|
|
1388
|
+
) as RoomConnectParams<TMetadata>;
|
|
1346
1389
|
|
|
1347
1390
|
await this.connect(...params);
|
|
1348
1391
|
}
|
|
1349
1392
|
|
|
1350
1393
|
private _sendMessage(data: string): void;
|
|
1351
|
-
private _sendMessage<TMessage extends EventMessage<string, any> = EventMessage<string, any>>(
|
|
1394
|
+
private _sendMessage<TMessage extends EventMessage<string, any> = EventMessage<string, any>>(
|
|
1395
|
+
data: TMessage,
|
|
1396
|
+
): void;
|
|
1352
1397
|
private _sendMessage(data: any): void {
|
|
1353
1398
|
const webSocket = this._state.webSocket;
|
|
1354
1399
|
|
|
@@ -1387,7 +1432,9 @@ export class PluvRoom<
|
|
|
1387
1432
|
return parsed;
|
|
1388
1433
|
}
|
|
1389
1434
|
|
|
1390
|
-
private _updateState(
|
|
1435
|
+
private _updateState(
|
|
1436
|
+
updater: (oldState: WebSocketState<TIO>) => WebSocketState<TIO>,
|
|
1437
|
+
): WebSocketState<TIO> {
|
|
1391
1438
|
let authorization: AuthorizationState<TIO>;
|
|
1392
1439
|
|
|
1393
1440
|
try {
|
|
@@ -1404,11 +1451,9 @@ export class PluvRoom<
|
|
|
1404
1451
|
};
|
|
1405
1452
|
|
|
1406
1453
|
const newState = updater(oldState);
|
|
1407
|
-
|
|
1408
|
-
this._logDebug("WebSocket new state: ", newState);
|
|
1409
|
-
|
|
1410
1454
|
this._state = newState;
|
|
1411
1455
|
|
|
1456
|
+
this._logDebug("WebSocket new state: ", newState);
|
|
1412
1457
|
this._stateNotifier.subjects.connection.next(newState);
|
|
1413
1458
|
|
|
1414
1459
|
return newState;
|
package/src/PluvRouter.ts
CHANGED
|
@@ -67,7 +67,10 @@ export class PluvRouter<
|
|
|
67
67
|
public static merge<TRouters extends PluvRouter<any, any, any, any>[]>(
|
|
68
68
|
...routers: TRouters
|
|
69
69
|
): MergedRouter<TRouters> {
|
|
70
|
-
const events = Object.assign(
|
|
70
|
+
const events = Object.assign(
|
|
71
|
+
Object.create(null),
|
|
72
|
+
...routers.map((router) => router._defs.events),
|
|
73
|
+
);
|
|
71
74
|
|
|
72
75
|
if (Object.keys(events).some((key) => key.startsWith("$"))) {
|
|
73
76
|
throw new Error('Procedures may not start with "$"');
|
package/src/StateNotifier.ts
CHANGED
|
@@ -15,7 +15,10 @@ type InferSubjectValue<
|
|
|
15
15
|
TIO extends IOLike,
|
|
16
16
|
TPresence extends JsonObject,
|
|
17
17
|
TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
|
|
18
|
-
> =
|
|
18
|
+
> =
|
|
19
|
+
StateNotifierSubjects<TIO, TPresence>[TSubject] extends Subject<infer IValue>
|
|
20
|
+
? Id<IValue>
|
|
21
|
+
: never;
|
|
19
22
|
|
|
20
23
|
export type SubscriptionCallback<
|
|
21
24
|
TIO extends IOLike,
|