@pluv/client 0.38.14 → 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 +68 -0
- package/dist/index.d.mts +132 -115
- package/dist/index.d.ts +132 -115
- package/dist/index.js +97 -69
- package/dist/index.mjs +97 -69
- 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 +133 -76
- 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
|
|
|
@@ -1016,25 +1048,33 @@ export class PluvRoom<
|
|
|
1016
1048
|
});
|
|
1017
1049
|
}
|
|
1018
1050
|
|
|
1019
|
-
/**
|
|
1020
|
-
* TODO
|
|
1021
|
-
* @description Determine if ws was closed with a code for closing without retries.
|
|
1022
|
-
* Otherwise, attempt to retry again with incremental backoff
|
|
1023
|
-
* @author leedavidcs
|
|
1024
|
-
* @date August 19, 2022
|
|
1025
|
-
*/
|
|
1026
1051
|
private async _onClose(event: CloseEvent): Promise<void> {
|
|
1027
1052
|
this._logDebug("WebSocket closed");
|
|
1028
1053
|
!!event.reason && this._logDebug(event.reason);
|
|
1029
1054
|
|
|
1030
|
-
|
|
1031
|
-
|
|
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);
|
|
1069
|
+
|
|
1070
|
+
this._closeWs();
|
|
1032
1071
|
|
|
1033
|
-
this.
|
|
1072
|
+
if (!shouldRetry) this._clearTimeout(this._timeouts.reconnect);
|
|
1073
|
+
else this._pollReconnect();
|
|
1034
1074
|
|
|
1035
1075
|
this._updateState((oldState) => {
|
|
1036
1076
|
oldState.authorization.token = null;
|
|
1037
|
-
oldState.connection.state = ConnectionState.Closed;
|
|
1077
|
+
oldState.connection.state = shouldRetry ? ConnectionState.Unavailable : ConnectionState.Closed;
|
|
1038
1078
|
oldState.webSocket = null;
|
|
1039
1079
|
|
|
1040
1080
|
return oldState;
|
|
@@ -1044,7 +1084,6 @@ export class PluvRoom<
|
|
|
1044
1084
|
/**
|
|
1045
1085
|
* TODO
|
|
1046
1086
|
* @description Handle websocket errors in a meaningful way, should they occur
|
|
1047
|
-
* @author leedavidcs
|
|
1048
1087
|
* @date August 19, 2022
|
|
1049
1088
|
*/
|
|
1050
1089
|
private _onError(): void {}
|
|
@@ -1115,10 +1154,18 @@ export class PluvRoom<
|
|
|
1115
1154
|
this._intervals.heartbeat = setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS) as unknown as number;
|
|
1116
1155
|
}
|
|
1117
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
|
+
|
|
1118
1167
|
private async _onNavigatorOnline(): Promise<void> {
|
|
1119
|
-
if (this._state.connection.state !== ConnectionState.Unavailable)
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1168
|
+
if (this._state.connection.state !== ConnectionState.Unavailable) return;
|
|
1122
1169
|
|
|
1123
1170
|
await this._reconnect();
|
|
1124
1171
|
}
|
|
@@ -1145,7 +1192,6 @@ export class PluvRoom<
|
|
|
1145
1192
|
/**
|
|
1146
1193
|
* !HACK
|
|
1147
1194
|
* @description We'll only handle stringified JSONs for now
|
|
1148
|
-
* @author leedavidcs
|
|
1149
1195
|
* @date August 9, 2022
|
|
1150
1196
|
*/
|
|
1151
1197
|
if (typeof message.data !== "string") return null;
|
|
@@ -1163,6 +1209,18 @@ export class PluvRoom<
|
|
|
1163
1209
|
}
|
|
1164
1210
|
}
|
|
1165
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
|
+
|
|
1166
1224
|
private async _reconnect(): Promise<void> {
|
|
1167
1225
|
if (!this._endpoints.wsEndpoint) {
|
|
1168
1226
|
throw new Error("Must provide an wsEndpoint.");
|
|
@@ -1172,15 +1230,10 @@ export class PluvRoom<
|
|
|
1172
1230
|
|
|
1173
1231
|
this._closeWs();
|
|
1174
1232
|
|
|
1175
|
-
this.
|
|
1176
|
-
|
|
1177
|
-
oldState.connection.state = ConnectionState.Closed;
|
|
1178
|
-
oldState.webSocket = null;
|
|
1179
|
-
|
|
1180
|
-
return oldState;
|
|
1181
|
-
});
|
|
1233
|
+
const metadata = this._lastMetadata;
|
|
1234
|
+
const params = (typeof metadata === "undefined" ? [] : [{ metadata }]) as RoomConnectParams<TMetadata>;
|
|
1182
1235
|
|
|
1183
|
-
await this.connect();
|
|
1236
|
+
await this.connect(...params);
|
|
1184
1237
|
}
|
|
1185
1238
|
|
|
1186
1239
|
private _sendMessage(data: string): void;
|
|
@@ -1215,6 +1268,10 @@ export class PluvRoom<
|
|
|
1215
1268
|
webSocket.send(message);
|
|
1216
1269
|
}
|
|
1217
1270
|
|
|
1271
|
+
private _setMetadata(metadata: TMetadata): TMetadata {
|
|
1272
|
+
return this.metadata ? this.metadata.parse(metadata) : metadata;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1218
1275
|
private _updateState(updater: (oldState: WebSocketState<TIO>) => WebSocketState<TIO>): WebSocketState<TIO> {
|
|
1219
1276
|
let authorization: AuthorizationState<TIO>;
|
|
1220
1277
|
|
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