@pluv/client 0.38.13 → 0.39.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 +76 -0
- package/dist/index.d.mts +132 -115
- package/dist/index.d.ts +132 -115
- package/dist/index.js +101 -76
- package/dist/index.mjs +101 -76
- package/eslint.config.mjs +4 -0
- package/package.json +7 -7
- package/src/MockedRoom.ts +1 -0
- package/src/PluvClient.ts +25 -11
- package/src/PluvRoom.ts +135 -79
- package/src/UsersManager.ts +3 -9
- package/src/index.ts +10 -3
- package/src/types.ts +21 -0
- package/.eslintrc.js +0 -4
package/src/PluvRoom.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
InferIOAuthorizeUser,
|
|
11
11
|
InferIOInput,
|
|
12
12
|
InferIOOutput,
|
|
13
|
+
InputZodLike,
|
|
13
14
|
JsonObject,
|
|
14
15
|
} from "@pluv/types";
|
|
15
16
|
import { AbstractRoom } from "./AbstractRoom";
|
|
@@ -40,13 +41,18 @@ import type {
|
|
|
40
41
|
UserInfo,
|
|
41
42
|
WebSocketConnection,
|
|
42
43
|
WebSocketState,
|
|
44
|
+
WithMetadata,
|
|
43
45
|
} from "./types";
|
|
44
46
|
import { debounce } from "./utils";
|
|
45
47
|
|
|
46
48
|
const ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1_000;
|
|
47
49
|
const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
48
50
|
const PONG_TIMEOUT_MS = 2_000;
|
|
49
|
-
|
|
51
|
+
|
|
52
|
+
const RECONNECT_TIMEOUT_MS = 5_000;
|
|
53
|
+
const MIN_RECONNECT_TIMEOUT_MS = 1_000;
|
|
54
|
+
const MAX_RECONNECT_TIMEOUT_MS = 60_000;
|
|
55
|
+
|
|
50
56
|
const ORIGIN_INITIALIZED = "$initialized";
|
|
51
57
|
const ORIGIN_STORAGE_UPDATED = "$storageUpdated";
|
|
52
58
|
|
|
@@ -69,7 +75,11 @@ export const DEFAULT_PLUV_CLIENT_ADDON = <
|
|
|
69
75
|
storage: new StorageStore(input.room.id),
|
|
70
76
|
});
|
|
71
77
|
|
|
78
|
+
interface CloseWsOptions {
|
|
79
|
+
connectionState?: ConnectionState;
|
|
80
|
+
}
|
|
72
81
|
interface WindowListeners {
|
|
82
|
+
onNavigatorOffline: () => void;
|
|
73
83
|
onNavigatorOnline: () => void;
|
|
74
84
|
onVisibilityChange: () => void;
|
|
75
85
|
}
|
|
@@ -137,6 +147,19 @@ export type PluvRoomDebug<TIO extends IOLike> = Id<{
|
|
|
137
147
|
input: readonly (keyof InferIOInput<TIO>)[];
|
|
138
148
|
}>;
|
|
139
149
|
|
|
150
|
+
type GetPublickKeyParams<TMetadata extends JsonObject = {}> = WithMetadata<TMetadata>;
|
|
151
|
+
type GetAuthEndpointParams<TMetadata extends JsonObject = {}> = WithMetadata<TMetadata>;
|
|
152
|
+
type GetWsEndpointParams<TMetadata extends JsonObject = {}> = WithMetadata<TMetadata>;
|
|
153
|
+
|
|
154
|
+
export type RoomConnectParams<TMetadata extends JsonObject = {}> = keyof TMetadata extends never
|
|
155
|
+
? []
|
|
156
|
+
: [WithMetadata<TMetadata>];
|
|
157
|
+
|
|
158
|
+
export interface ReconnectTimeoutMsParams {
|
|
159
|
+
attempts: number;
|
|
160
|
+
}
|
|
161
|
+
export type ReconnectTimeoutMs = number | ((params: ReconnectTimeoutMsParams) => number);
|
|
162
|
+
|
|
140
163
|
export type RoomConfig<
|
|
141
164
|
TIO extends IOLike,
|
|
142
165
|
TMetadata extends JsonObject = {},
|
|
@@ -148,12 +171,13 @@ export type RoomConfig<
|
|
|
148
171
|
addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
|
|
149
172
|
debug?: boolean | PluvRoomDebug<TIO>;
|
|
150
173
|
onAuthorizationFail?: (error: Error) => void;
|
|
174
|
+
metadata?: InputZodLike<TMetadata>;
|
|
151
175
|
publicKey?: PublicKey<TMetadata>;
|
|
176
|
+
reconnectTimeoutMs?: ReconnectTimeoutMs;
|
|
152
177
|
router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
|
|
153
178
|
} & RoomEndpoints<TIO, TMetadata> &
|
|
154
179
|
Pick<CrdtManagerOptions<TStorage>, "initialStorage"> &
|
|
155
|
-
UsersManagerConfig<TPresence>
|
|
156
|
-
(keyof TMetadata extends never ? { metadata?: undefined } : { metadata: TMetadata })
|
|
180
|
+
UsersManagerConfig<TPresence>
|
|
157
181
|
>;
|
|
158
182
|
|
|
159
183
|
export class PluvRoom<
|
|
@@ -165,6 +189,8 @@ export class PluvRoom<
|
|
|
165
189
|
> extends AbstractRoom<TIO, TPresence, TStorage> {
|
|
166
190
|
readonly _endpoints: RoomEndpoints<TIO, TMetadata>;
|
|
167
191
|
|
|
192
|
+
public readonly metadata?: InputZodLike<TMetadata>;
|
|
193
|
+
|
|
168
194
|
private readonly _crdtManager: CrdtManager<TStorage>;
|
|
169
195
|
private readonly _crdtNotifier = new CrdtNotifier<TStorage>();
|
|
170
196
|
private readonly _debug: boolean | PluvRoomDebug<TIO>;
|
|
@@ -173,9 +199,9 @@ export class PluvRoom<
|
|
|
173
199
|
heartbeat: null,
|
|
174
200
|
};
|
|
175
201
|
private readonly _listeners: InternalListeners;
|
|
176
|
-
private readonly _metadata: TMetadata = {} as TMetadata;
|
|
177
202
|
private readonly _otherNotifier = new OtherNotifier<TIO>();
|
|
178
203
|
private readonly _publicKey: PublicKey<TMetadata> | null = null;
|
|
204
|
+
private readonly _reconnectTimeoutMs: ReconnectTimeoutMs;
|
|
179
205
|
private readonly _router: PluvRouter<TIO, TPresence, TStorage, TEvents>;
|
|
180
206
|
private readonly _stateNotifier = new StateNotifier<TIO, TPresence>();
|
|
181
207
|
private readonly _storageStore: AbstractStorageStore;
|
|
@@ -188,12 +214,14 @@ export class PluvRoom<
|
|
|
188
214
|
};
|
|
189
215
|
private readonly _usersManager: UsersManager<TIO, TPresence>;
|
|
190
216
|
|
|
217
|
+
private _lastMetadata: TMetadata | null = null;
|
|
191
218
|
private _state: WebSocketState<TIO> = {
|
|
192
219
|
authorization: {
|
|
193
220
|
token: null,
|
|
194
221
|
user: null,
|
|
195
222
|
},
|
|
196
223
|
connection: {
|
|
224
|
+
attempts: 0,
|
|
197
225
|
count: 0,
|
|
198
226
|
id: null,
|
|
199
227
|
state: ConnectionState.Untouched,
|
|
@@ -214,6 +242,7 @@ export class PluvRoom<
|
|
|
214
242
|
onAuthorizationFail,
|
|
215
243
|
presence,
|
|
216
244
|
publicKey,
|
|
245
|
+
reconnectTimeoutMs = RECONNECT_TIMEOUT_MS,
|
|
217
246
|
router,
|
|
218
247
|
wsEndpoint,
|
|
219
248
|
} = options;
|
|
@@ -227,21 +256,17 @@ export class PluvRoom<
|
|
|
227
256
|
...addon({ room: this }),
|
|
228
257
|
};
|
|
229
258
|
|
|
259
|
+
this.metadata = metadata;
|
|
260
|
+
|
|
230
261
|
this._debug = debug;
|
|
231
262
|
this._endpoints = { authEndpoint, wsEndpoint } as RoomEndpoints<TIO, TMetadata>;
|
|
263
|
+
this._reconnectTimeoutMs = reconnectTimeoutMs;
|
|
232
264
|
this._storageStore = storage;
|
|
233
265
|
|
|
234
|
-
if (typeof metadata !== "undefined") this._metadata = metadata;
|
|
235
266
|
if (!!publicKey) this._publicKey = publicKey;
|
|
236
267
|
|
|
237
268
|
this._listeners = {
|
|
238
269
|
onAuthorizationFail: (error) => {
|
|
239
|
-
this._clearTimeout(this._timeouts.reconnect);
|
|
240
|
-
this._timeouts.reconnect = setTimeout(
|
|
241
|
-
this._reconnect.bind(this),
|
|
242
|
-
RECONNECT_TIMEOUT_MS,
|
|
243
|
-
) as unknown as number;
|
|
244
|
-
|
|
245
270
|
onAuthorizationFail?.(error);
|
|
246
271
|
},
|
|
247
272
|
};
|
|
@@ -328,10 +353,13 @@ export class PluvRoom<
|
|
|
328
353
|
return !!this._crdtManager.doc.canUndo();
|
|
329
354
|
};
|
|
330
355
|
|
|
331
|
-
public async connect(): Promise<void> {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
356
|
+
public async connect(...args: RoomConnectParams<TMetadata>): Promise<void> {
|
|
357
|
+
const params = (args[0] ?? {}) as WithMetadata<TMetadata>;
|
|
358
|
+
const metadata = params.metadata as TMetadata;
|
|
359
|
+
|
|
360
|
+
this._setMetadata(metadata);
|
|
361
|
+
|
|
362
|
+
if (!this._endpoints.wsEndpoint) throw new Error("Must provide a wsEndpoint.");
|
|
335
363
|
|
|
336
364
|
const canConnect = [ConnectionState.Closed, ConnectionState.Untouched].some(
|
|
337
365
|
(state) => this._state.connection.state === state,
|
|
@@ -340,7 +368,6 @@ export class PluvRoom<
|
|
|
340
368
|
if (!canConnect) return;
|
|
341
369
|
|
|
342
370
|
this._clearTimeout(this._timeouts.reconnect);
|
|
343
|
-
|
|
344
371
|
this._updateState((oldState) => {
|
|
345
372
|
oldState.connection.state = ConnectionState.Connecting;
|
|
346
373
|
|
|
@@ -350,29 +377,36 @@ export class PluvRoom<
|
|
|
350
377
|
await this._storageStore.initialize();
|
|
351
378
|
await this._applyStorageStore();
|
|
352
379
|
|
|
353
|
-
const url = new URL(this._getWsEndpoint(this.id));
|
|
380
|
+
const url = new URL(this._getWsEndpoint(this.id, params));
|
|
354
381
|
|
|
355
382
|
let authToken: string | null = null;
|
|
356
383
|
let webSocket: WebSocket;
|
|
357
384
|
|
|
358
385
|
try {
|
|
359
|
-
const publicKey = this._getPublicKey();
|
|
386
|
+
const publicKey = this._getPublicKey(params);
|
|
360
387
|
|
|
361
|
-
authToken = await this._getAuthorization(this.id);
|
|
388
|
+
authToken = await this._getAuthorization(this.id, params);
|
|
362
389
|
|
|
363
390
|
authToken && url.searchParams.set("token", encodeURIComponent(authToken));
|
|
364
391
|
publicKey && url.searchParams.set("public_key", encodeURIComponent(publicKey));
|
|
365
392
|
|
|
366
393
|
webSocket = new WebSocket(url.toString());
|
|
367
394
|
} catch (err) {
|
|
368
|
-
|
|
395
|
+
this._updateState((oldState) => {
|
|
396
|
+
oldState.connection.attempts += 1;
|
|
369
397
|
|
|
370
|
-
|
|
398
|
+
return oldState;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await this._storageStore.destroy();
|
|
402
|
+
this._pollReconnect();
|
|
403
|
+
await Promise.resolve(this._onAuthorizationFail(err));
|
|
371
404
|
|
|
372
405
|
return;
|
|
373
406
|
}
|
|
374
407
|
|
|
375
408
|
this._updateState((oldState) => {
|
|
409
|
+
oldState.connection.attempts = 0;
|
|
376
410
|
oldState.authorization.token = authToken;
|
|
377
411
|
oldState.webSocket = webSocket;
|
|
378
412
|
|
|
@@ -384,10 +418,7 @@ export class PluvRoom<
|
|
|
384
418
|
}
|
|
385
419
|
|
|
386
420
|
public async disconnect(): Promise<void> {
|
|
387
|
-
if (!this._endpoints.wsEndpoint)
|
|
388
|
-
throw new Error("Must provide an wsEndpoint.");
|
|
389
|
-
}
|
|
390
|
-
|
|
421
|
+
if (!this._endpoints.wsEndpoint) throw new Error("Must provide a wsEndpoint.");
|
|
391
422
|
if (!this._state.webSocket) return;
|
|
392
423
|
|
|
393
424
|
const canDisconnect = [ConnectionState.Connecting, ConnectionState.Open].some(
|
|
@@ -396,14 +427,19 @@ export class PluvRoom<
|
|
|
396
427
|
|
|
397
428
|
if (!canDisconnect) return;
|
|
398
429
|
|
|
430
|
+
this._closeWs();
|
|
399
431
|
this._clearInterval(this._intervals.heartbeat);
|
|
432
|
+
this._clearTimeout(this._timeouts.reconnect);
|
|
433
|
+
this._updateState((oldState) => {
|
|
434
|
+
oldState.authorization.token = null;
|
|
435
|
+
oldState.connection.state = ConnectionState.Closed;
|
|
436
|
+
oldState.webSocket = null;
|
|
400
437
|
|
|
401
|
-
|
|
438
|
+
return oldState;
|
|
439
|
+
});
|
|
402
440
|
|
|
403
441
|
await this._storageStore.destroy();
|
|
404
|
-
|
|
405
442
|
this._crdtManager.destroy();
|
|
406
|
-
|
|
407
443
|
this._stateNotifier.subjects["my-presence"].next(null);
|
|
408
444
|
}
|
|
409
445
|
|
|
@@ -571,10 +607,12 @@ export class PluvRoom<
|
|
|
571
607
|
if (typeof document === "undefined") return;
|
|
572
608
|
|
|
573
609
|
this._windowListeners = {
|
|
610
|
+
onNavigatorOffline: this._onNavigatorOffline.bind(this),
|
|
574
611
|
onNavigatorOnline: this._onNavigatorOnline.bind(this),
|
|
575
612
|
onVisibilityChange: this._onVisibilityChange.bind(this),
|
|
576
613
|
};
|
|
577
614
|
|
|
615
|
+
window.addEventListener("offline", this._windowListeners.onNavigatorOffline);
|
|
578
616
|
window.addEventListener("online", this._windowListeners.onNavigatorOnline);
|
|
579
617
|
document.addEventListener("visibilitychange", this._windowListeners.onVisibilityChange);
|
|
580
618
|
}
|
|
@@ -608,26 +646,17 @@ export class PluvRoom<
|
|
|
608
646
|
}
|
|
609
647
|
|
|
610
648
|
private _closeWs(): void {
|
|
611
|
-
if (!this._state.webSocket) return;
|
|
612
|
-
|
|
613
649
|
this._subscriptions.observeCrdt?.();
|
|
614
650
|
|
|
651
|
+
this._clearInterval(this._intervals.heartbeat);
|
|
615
652
|
this._clearTimeout(this._timeouts.pong);
|
|
616
|
-
this._clearTimeout(this._timeouts.reconnect);
|
|
617
|
-
|
|
618
653
|
this._detachWindowListeners();
|
|
619
|
-
|
|
620
654
|
this._usersManager.removeMyself();
|
|
621
655
|
this._usersManager.removeUsers();
|
|
622
656
|
this._otherNotifier.clear();
|
|
623
657
|
this._stateNotifier.subjects.myself.next(null);
|
|
624
658
|
this._stateNotifier.subjects.others.next([]);
|
|
625
|
-
|
|
626
|
-
const canClose = [WebSocket.CONNECTING, WebSocket.OPEN].some(
|
|
627
|
-
(readyState) => readyState === this._state.webSocket?.readyState,
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
if (canClose) this._state.webSocket.close();
|
|
659
|
+
this._state.webSocket?.close();
|
|
631
660
|
|
|
632
661
|
this._detachWsListeners();
|
|
633
662
|
}
|
|
@@ -700,7 +729,9 @@ export class PluvRoom<
|
|
|
700
729
|
);
|
|
701
730
|
};
|
|
702
731
|
|
|
703
|
-
private _getAuthFetchOptions(room: string): FetchOptions | null {
|
|
732
|
+
private _getAuthFetchOptions(room: string, params: GetAuthEndpointParams<TMetadata>): FetchOptions | null {
|
|
733
|
+
const metadata = params.metadata as TMetadata;
|
|
734
|
+
|
|
704
735
|
if (typeof this._endpoints.authEndpoint === "undefined") return null;
|
|
705
736
|
|
|
706
737
|
if (this._endpoints.authEndpoint === true) {
|
|
@@ -717,23 +748,20 @@ export class PluvRoom<
|
|
|
717
748
|
};
|
|
718
749
|
}
|
|
719
750
|
|
|
720
|
-
const result = this._endpoints.authEndpoint({ metadata
|
|
751
|
+
const result = this._endpoints.authEndpoint({ metadata, room });
|
|
721
752
|
|
|
722
753
|
return typeof result === "string" ? { url: result, options: {} } : result;
|
|
723
754
|
}
|
|
724
755
|
|
|
725
|
-
private async _getAuthorization(room: string): Promise<string | null> {
|
|
726
|
-
const fetchOptions = this._getAuthFetchOptions(room);
|
|
756
|
+
private async _getAuthorization(room: string, params: GetAuthEndpointParams<TMetadata>): Promise<string | null> {
|
|
757
|
+
const fetchOptions = this._getAuthFetchOptions(room, params);
|
|
727
758
|
|
|
728
759
|
if (!fetchOptions) return null;
|
|
729
760
|
|
|
730
761
|
const { url, options } = fetchOptions;
|
|
731
|
-
|
|
732
762
|
const res = await fetch(url, options);
|
|
733
763
|
|
|
734
|
-
if (!res.ok || res.status !== 200)
|
|
735
|
-
throw new Error("Room is unauthorized");
|
|
736
|
-
}
|
|
764
|
+
if (!res.ok || res.status !== 200) throw new Error("Room is unauthorized");
|
|
737
765
|
|
|
738
766
|
try {
|
|
739
767
|
return await res.text().then((text) => text.trim());
|
|
@@ -742,21 +770,25 @@ export class PluvRoom<
|
|
|
742
770
|
}
|
|
743
771
|
}
|
|
744
772
|
|
|
745
|
-
private _getPublicKey(): string | null {
|
|
773
|
+
private _getPublicKey(params: GetPublickKeyParams<TMetadata>): string | null {
|
|
774
|
+
const metadata = params.metadata as TMetadata;
|
|
775
|
+
|
|
746
776
|
if (!this._publicKey) return null;
|
|
747
777
|
if (typeof this._publicKey === "string") return this._publicKey;
|
|
748
778
|
|
|
749
|
-
return this._publicKey({ metadata
|
|
779
|
+
return this._publicKey({ metadata });
|
|
750
780
|
}
|
|
751
781
|
|
|
752
|
-
private _getWsEndpoint(room: string): string {
|
|
782
|
+
private _getWsEndpoint(room: string, params: GetWsEndpointParams<TMetadata>): string {
|
|
783
|
+
const metadata = params.metadata as TMetadata;
|
|
784
|
+
|
|
753
785
|
switch (typeof this._endpoints.wsEndpoint) {
|
|
754
786
|
case "undefined":
|
|
755
|
-
return !!this._getPublicKey() ? `wss://rooms.pluv.io/api/room/${room}` : `/api/pluv/room/${room}`;
|
|
787
|
+
return !!this._getPublicKey(params) ? `wss://rooms.pluv.io/api/room/${room}` : `/api/pluv/room/${room}`;
|
|
756
788
|
case "string":
|
|
757
789
|
return this._endpoints.wsEndpoint;
|
|
758
790
|
default:
|
|
759
|
-
return this._endpoints.wsEndpoint({ metadata
|
|
791
|
+
return this._endpoints.wsEndpoint({ metadata, room });
|
|
760
792
|
}
|
|
761
793
|
}
|
|
762
794
|
|
|
@@ -785,9 +817,6 @@ export class PluvRoom<
|
|
|
785
817
|
|
|
786
818
|
const data = message.data as BaseIOEventRecord<InferIOAuthorize<TIO>>["$presenceUpdated"];
|
|
787
819
|
|
|
788
|
-
this._usersManager.patchPresence(connectionId, data.presence as TPresence);
|
|
789
|
-
|
|
790
|
-
const myPresence = this._usersManager.myPresence;
|
|
791
820
|
const myself = this._usersManager.myself ?? null;
|
|
792
821
|
|
|
793
822
|
/**
|
|
@@ -799,6 +828,8 @@ export class PluvRoom<
|
|
|
799
828
|
*/
|
|
800
829
|
if (myself?.connectionId === connectionId) return;
|
|
801
830
|
|
|
831
|
+
this._usersManager.patchPresence(connectionId, data.presence as TPresence);
|
|
832
|
+
|
|
802
833
|
const other = this._usersManager.getOther(connectionId);
|
|
803
834
|
const others = this._usersManager.getOthers();
|
|
804
835
|
|
|
@@ -1017,25 +1048,33 @@ export class PluvRoom<
|
|
|
1017
1048
|
});
|
|
1018
1049
|
}
|
|
1019
1050
|
|
|
1020
|
-
/**
|
|
1021
|
-
* TODO
|
|
1022
|
-
* @description Determine if ws was closed with a code for closing without retries.
|
|
1023
|
-
* Otherwise, attempt to retry again with incremental backoff
|
|
1024
|
-
* @author leedavidcs
|
|
1025
|
-
* @date August 19, 2022
|
|
1026
|
-
*/
|
|
1027
1051
|
private async _onClose(event: CloseEvent): Promise<void> {
|
|
1028
1052
|
this._logDebug("WebSocket closed");
|
|
1029
1053
|
!!event.reason && this._logDebug(event.reason);
|
|
1030
1054
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1055
|
+
const shouldRetry = [
|
|
1056
|
+
// Going away: Client/server is shutting down or navigating
|
|
1057
|
+
1001,
|
|
1058
|
+
// Abnormal closure: Usually network error
|
|
1059
|
+
1006,
|
|
1060
|
+
// Internal error: may be a larger problem, but try reconnecting for now
|
|
1061
|
+
1011,
|
|
1062
|
+
// Service restart: Server restarted
|
|
1063
|
+
1012,
|
|
1064
|
+
// Try again later: server overloaded
|
|
1065
|
+
1013,
|
|
1066
|
+
// Bad gateway
|
|
1067
|
+
1014,
|
|
1068
|
+
].some((okToRetryCode) => event.code === okToRetryCode);
|
|
1033
1069
|
|
|
1034
|
-
this.
|
|
1070
|
+
this._closeWs();
|
|
1071
|
+
|
|
1072
|
+
if (!shouldRetry) this._clearTimeout(this._timeouts.reconnect);
|
|
1073
|
+
else this._pollReconnect();
|
|
1035
1074
|
|
|
1036
1075
|
this._updateState((oldState) => {
|
|
1037
1076
|
oldState.authorization.token = null;
|
|
1038
|
-
oldState.connection.state = ConnectionState.Closed;
|
|
1077
|
+
oldState.connection.state = shouldRetry ? ConnectionState.Unavailable : ConnectionState.Closed;
|
|
1039
1078
|
oldState.webSocket = null;
|
|
1040
1079
|
|
|
1041
1080
|
return oldState;
|
|
@@ -1045,7 +1084,6 @@ export class PluvRoom<
|
|
|
1045
1084
|
/**
|
|
1046
1085
|
* TODO
|
|
1047
1086
|
* @description Handle websocket errors in a meaningful way, should they occur
|
|
1048
|
-
* @author leedavidcs
|
|
1049
1087
|
* @date August 19, 2022
|
|
1050
1088
|
*/
|
|
1051
1089
|
private _onError(): void {}
|
|
@@ -1116,10 +1154,18 @@ export class PluvRoom<
|
|
|
1116
1154
|
this._intervals.heartbeat = setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS) as unknown as number;
|
|
1117
1155
|
}
|
|
1118
1156
|
|
|
1157
|
+
private async _onNavigatorOffline(): Promise<void> {
|
|
1158
|
+
if (this._state.connection.state === ConnectionState.Closed) return;
|
|
1159
|
+
|
|
1160
|
+
this._updateState((oldState) => {
|
|
1161
|
+
oldState.connection.state = ConnectionState.Unavailable;
|
|
1162
|
+
|
|
1163
|
+
return oldState;
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1119
1167
|
private async _onNavigatorOnline(): Promise<void> {
|
|
1120
|
-
if (this._state.connection.state !== ConnectionState.Unavailable)
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1168
|
+
if (this._state.connection.state !== ConnectionState.Unavailable) return;
|
|
1123
1169
|
|
|
1124
1170
|
await this._reconnect();
|
|
1125
1171
|
}
|
|
@@ -1146,7 +1192,6 @@ export class PluvRoom<
|
|
|
1146
1192
|
/**
|
|
1147
1193
|
* !HACK
|
|
1148
1194
|
* @description We'll only handle stringified JSONs for now
|
|
1149
|
-
* @author leedavidcs
|
|
1150
1195
|
* @date August 9, 2022
|
|
1151
1196
|
*/
|
|
1152
1197
|
if (typeof message.data !== "string") return null;
|
|
@@ -1164,6 +1209,18 @@ export class PluvRoom<
|
|
|
1164
1209
|
}
|
|
1165
1210
|
}
|
|
1166
1211
|
|
|
1212
|
+
private _pollReconnect(): void {
|
|
1213
|
+
this._clearTimeout(this._timeouts.reconnect);
|
|
1214
|
+
|
|
1215
|
+
const timeoutMs =
|
|
1216
|
+
typeof this._reconnectTimeoutMs === "number"
|
|
1217
|
+
? this._reconnectTimeoutMs
|
|
1218
|
+
: this._reconnectTimeoutMs({ attempts: this._state.connection.attempts });
|
|
1219
|
+
const clampedMs = Math.max(Math.min(MIN_RECONNECT_TIMEOUT_MS, timeoutMs), MAX_RECONNECT_TIMEOUT_MS);
|
|
1220
|
+
|
|
1221
|
+
this._timeouts.reconnect = setTimeout(this._reconnect.bind(this), clampedMs) as unknown as number;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1167
1224
|
private async _reconnect(): Promise<void> {
|
|
1168
1225
|
if (!this._endpoints.wsEndpoint) {
|
|
1169
1226
|
throw new Error("Must provide an wsEndpoint.");
|
|
@@ -1173,15 +1230,10 @@ export class PluvRoom<
|
|
|
1173
1230
|
|
|
1174
1231
|
this._closeWs();
|
|
1175
1232
|
|
|
1176
|
-
this.
|
|
1177
|
-
|
|
1178
|
-
oldState.connection.state = ConnectionState.Closed;
|
|
1179
|
-
oldState.webSocket = null;
|
|
1180
|
-
|
|
1181
|
-
return oldState;
|
|
1182
|
-
});
|
|
1233
|
+
const metadata = this._lastMetadata;
|
|
1234
|
+
const params = (typeof metadata === "undefined" ? [] : [{ metadata }]) as RoomConnectParams<TMetadata>;
|
|
1183
1235
|
|
|
1184
|
-
await this.connect();
|
|
1236
|
+
await this.connect(...params);
|
|
1185
1237
|
}
|
|
1186
1238
|
|
|
1187
1239
|
private _sendMessage(data: string): void;
|
|
@@ -1216,6 +1268,10 @@ export class PluvRoom<
|
|
|
1216
1268
|
webSocket.send(message);
|
|
1217
1269
|
}
|
|
1218
1270
|
|
|
1271
|
+
private _setMetadata(metadata: TMetadata): TMetadata {
|
|
1272
|
+
return this.metadata ? this.metadata.parse(metadata) : metadata;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1219
1275
|
private _updateState(updater: (oldState: WebSocketState<TIO>) => WebSocketState<TIO>): WebSocketState<TIO> {
|
|
1220
1276
|
let authorization: AuthorizationState<TIO>;
|
|
1221
1277
|
|
package/src/UsersManager.ts
CHANGED
|
@@ -52,13 +52,7 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
|
|
|
52
52
|
|
|
53
53
|
public patchPresence(connectionId: string, patch: Partial<TPresence>): void {
|
|
54
54
|
if (this._myself?.connectionId === connectionId) {
|
|
55
|
-
|
|
56
|
-
...this._myPresence,
|
|
57
|
-
...patch,
|
|
58
|
-
} as TPresence;
|
|
59
|
-
|
|
60
|
-
this._myself.presence = presence;
|
|
61
|
-
this._myPresence = presence;
|
|
55
|
+
this.updateMyPresence(patch);
|
|
62
56
|
|
|
63
57
|
return;
|
|
64
58
|
}
|
|
@@ -135,8 +129,8 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
|
|
|
135
129
|
/**
|
|
136
130
|
* @description This method need not care about being connected.
|
|
137
131
|
*/
|
|
138
|
-
public updateMyPresence(
|
|
139
|
-
this._myPresence = { ...this._myPresence, ...
|
|
132
|
+
public updateMyPresence(patch: Partial<TPresence>): void {
|
|
133
|
+
this._myPresence = { ...this._myPresence, ...patch };
|
|
140
134
|
|
|
141
135
|
if (!this._myself) return;
|
|
142
136
|
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,18 @@ export type { InferCallback } from "./infer";
|
|
|
9
9
|
export { MockedRoom } from "./MockedRoom";
|
|
10
10
|
export type { MockedRoomConfig, MockedRoomEvents } from "./MockedRoom";
|
|
11
11
|
export { PluvClient } from "./PluvClient";
|
|
12
|
-
export type { CreateRoomOptions, PluvClientOptions } from "./PluvClient";
|
|
12
|
+
export type { CreateRoomOptions, EnterRoomParams, PluvClientOptions } from "./PluvClient";
|
|
13
13
|
export type { PluvProcedureConfig } from "./PluvProcedure";
|
|
14
14
|
export { PluvRoom } from "./PluvRoom";
|
|
15
|
-
export type {
|
|
15
|
+
export type {
|
|
16
|
+
PluvRoomAddon,
|
|
17
|
+
PluvRoomAddonInput,
|
|
18
|
+
PluvRoomAddonResult,
|
|
19
|
+
PluvRoomDebug,
|
|
20
|
+
RoomConnectParams,
|
|
21
|
+
RoomEndpoints,
|
|
22
|
+
} from "./PluvRoom";
|
|
16
23
|
export type { MergeEvents, PluvRouter, PluvRouterEventConfig } from "./PluvRouter";
|
|
17
24
|
export { register } from "./register";
|
|
18
25
|
export type { RegisterParams } from "./register";
|
|
19
|
-
export type { PublicKey, PublicKeyParams, UserInfo, WebSocketConnection, WebSocketState } from "./types";
|
|
26
|
+
export type { InferMetadata, PublicKey, PublicKeyParams, UserInfo, WebSocketConnection, WebSocketState } from "./types";
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
MaybePromise,
|
|
10
10
|
} from "@pluv/types";
|
|
11
11
|
import type { ConnectionState } from "./enums";
|
|
12
|
+
import type { PluvClient } from "./PluvClient";
|
|
12
13
|
|
|
13
14
|
export interface AuthorizationState<TIO extends IOLike> {
|
|
14
15
|
token: string | null;
|
|
@@ -34,6 +35,9 @@ export interface EventResolverContext<
|
|
|
34
35
|
user: UserInfo<TIO, TPresence>;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
export type InferMetadata<TClient extends PluvClient<any, any, any, any>> =
|
|
39
|
+
TClient extends PluvClient<any, any, any, infer IMetadata> ? IMetadata : never;
|
|
40
|
+
|
|
37
41
|
export interface InternalSubscriptions {
|
|
38
42
|
observeCrdt: (() => void) | null;
|
|
39
43
|
}
|
|
@@ -69,6 +73,19 @@ export interface UserInfo<TIO extends IOLike, TPresence extends JsonObject = {}>
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
export interface WebSocketConnection {
|
|
76
|
+
/**
|
|
77
|
+
* @description How many times a connection attempt was made. This will increment upon each
|
|
78
|
+
* unsuccessful attempt, and will reset upon a successful connection.
|
|
79
|
+
* @date April 13, 2025
|
|
80
|
+
*/
|
|
81
|
+
attempts: number;
|
|
82
|
+
/**
|
|
83
|
+
* @description How many times the user has connected to the room. This can be more than 1 in
|
|
84
|
+
* the case of reconnects. This is tracked because on the very first connection (i.e. when
|
|
85
|
+
* count is 0) we don't want to send the storage state as an update to the room and overwrite
|
|
86
|
+
* the upstream state.
|
|
87
|
+
* @date April 13, 2025
|
|
88
|
+
*/
|
|
72
89
|
count: number;
|
|
73
90
|
id: string | null;
|
|
74
91
|
state: ConnectionState;
|
|
@@ -79,3 +96,7 @@ export interface WebSocketState<TIO extends IOLike> {
|
|
|
79
96
|
connection: WebSocketConnection;
|
|
80
97
|
webSocket: WebSocket | null;
|
|
81
98
|
}
|
|
99
|
+
|
|
100
|
+
export type WithMetadata<TMetadata extends JsonObject = {}> = keyof TMetadata extends never
|
|
101
|
+
? { metadata?: undefined }
|
|
102
|
+
: { metadata: TMetadata };
|
package/.eslintrc.js
DELETED