@pluv/io 0.40.2 → 0.41.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 +9 -9
- package/CHANGELOG.md +69 -0
- package/dist/index.d.mts +30 -21
- package/dist/index.d.ts +30 -21
- package/dist/index.js +198 -75
- package/dist/index.mjs +198 -75
- package/package.json +7 -7
- package/src/AbstractPlatform.ts +3 -3
- package/src/AbstractWebSocket.ts +3 -20
- package/src/IORoom.ts +248 -65
- package/src/PluvServer.ts +39 -12
- package/src/authorize.ts +4 -4
- package/src/constants.ts +3 -0
- package/src/types.ts +4 -8
- package/src/version.generated.ts +1 -1
- package/tsconfig.json +5 -1
package/src/IORoom.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AbstractCrdtDoc, AbstractCrdtDocFactory } from "@pluv/crdt";
|
|
|
2
2
|
import { noop } from "@pluv/crdt";
|
|
3
3
|
import type {
|
|
4
4
|
BaseIOEventRecord,
|
|
5
|
+
BaseUser,
|
|
5
6
|
EventMessage,
|
|
6
7
|
IOEventMessage,
|
|
7
8
|
IOLike,
|
|
@@ -25,7 +26,7 @@ import type { AbstractCloseEvent, AbstractErrorEvent, AbstractMessageEvent } fro
|
|
|
25
26
|
import { AbstractWebSocket } from "./AbstractWebSocket";
|
|
26
27
|
import type { PluvRouter, PluvRouterEventConfig } from "./PluvRouter";
|
|
27
28
|
import { authorize } from "./authorize";
|
|
28
|
-
import { PING_TIMEOUT_MS } from "./constants";
|
|
29
|
+
import { GARBAGE_COLLECT_INTERVAL_MS, PING_TIMEOUT_MS } from "./constants";
|
|
29
30
|
import type {
|
|
30
31
|
EventResolverContext,
|
|
31
32
|
EventResolverKind,
|
|
@@ -87,6 +88,12 @@ interface SendMessageSender {
|
|
|
87
88
|
user: JsonObject | null;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
interface PatchPresenceParams {
|
|
92
|
+
presence: JsonObject | null;
|
|
93
|
+
sessionId: string;
|
|
94
|
+
timer?: number | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
export type WebSocketRegisterConfig<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>> = {
|
|
91
98
|
token?: string | null;
|
|
92
99
|
} & InferInitContextType<TPlatform>;
|
|
@@ -101,6 +108,7 @@ export class IORoom<
|
|
|
101
108
|
public readonly id: string;
|
|
102
109
|
|
|
103
110
|
private _doc: Promise<AbstractCrdtDoc<any>>;
|
|
111
|
+
private _lastGarbageCollectMs: number = -1 * (GARBAGE_COLLECT_INTERVAL_MS + 1);
|
|
104
112
|
private _uninitialize: Promise<() => Promise<void>> | null = null;
|
|
105
113
|
|
|
106
114
|
private readonly _authorize: TAuthorize = null as TAuthorize;
|
|
@@ -110,7 +118,8 @@ export class IORoom<
|
|
|
110
118
|
private readonly _listeners: IORoomListeners<TPlatform, TAuthorize, TContext, TEvents>;
|
|
111
119
|
private readonly _platform: TPlatform;
|
|
112
120
|
private readonly _router: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
|
|
113
|
-
private readonly _sessions = new Map<string, AbstractWebSocket
|
|
121
|
+
private readonly _sessions = new Map<[sessionId: string][0], AbstractWebSocket<any, TAuthorize>>();
|
|
122
|
+
private readonly _userSessionss = new Map<[userId: string][0], Set<[sessionId: string][0]>>();
|
|
114
123
|
|
|
115
124
|
/**
|
|
116
125
|
* @ignore
|
|
@@ -185,6 +194,11 @@ export class IORoom<
|
|
|
185
194
|
|
|
186
195
|
if (authorize) this._authorize = authorize;
|
|
187
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @description Everything below this line relates to waking up from hibernation.
|
|
199
|
+
* @see https://developers.cloudflare.com/durable-objects/best-practices/websockets/#websocket-hibernation-api
|
|
200
|
+
* @date April 17, 2025
|
|
201
|
+
*/
|
|
188
202
|
const webSockets = this._platform.getWebSockets() as readonly InferPlatformWebSocketSource<TPlatform>[];
|
|
189
203
|
|
|
190
204
|
webSockets.forEach((webSocket) => {
|
|
@@ -197,6 +211,10 @@ export class IORoom<
|
|
|
197
211
|
const pluvWs = this._platform.convertWebSocket(webSocket, { room: this.id });
|
|
198
212
|
|
|
199
213
|
this._sessions.set(sessionId, pluvWs);
|
|
214
|
+
|
|
215
|
+
const userId = pluvWs.session.user?.id;
|
|
216
|
+
|
|
217
|
+
if (!!userId) this._addUserSession(userId, sessionId);
|
|
200
218
|
});
|
|
201
219
|
|
|
202
220
|
const { doc, uninitialize } = this._initialize();
|
|
@@ -231,6 +249,15 @@ export class IORoom<
|
|
|
231
249
|
await this._emitQuitters();
|
|
232
250
|
}
|
|
233
251
|
|
|
252
|
+
/**
|
|
253
|
+
* @description The IORoom will garbage collect occasionally as connections ping/pong the
|
|
254
|
+
* server
|
|
255
|
+
*/
|
|
256
|
+
public async garbageCollect(): Promise<void> {
|
|
257
|
+
this._lastGarbageCollectMs = Date.now();
|
|
258
|
+
this._emitQuitters();
|
|
259
|
+
}
|
|
260
|
+
|
|
234
261
|
public getSize(): number {
|
|
235
262
|
const currentTime = new Date().getTime();
|
|
236
263
|
|
|
@@ -286,42 +313,54 @@ export class IORoom<
|
|
|
286
313
|
): Promise<void> {
|
|
287
314
|
const _options = (options[0] ?? {}) as WebSocketRegisterConfig<TPlatform>;
|
|
288
315
|
const token = _options.token ?? null;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* TODO
|
|
319
|
+
* @description Update the sessionId strategy so that the userId can be used to retrieve
|
|
320
|
+
* the session in O(1) time if authorized.
|
|
321
|
+
* @date April 15, 2025
|
|
322
|
+
*/
|
|
289
323
|
const sessionId = this._platform.getSessionId(webSocket);
|
|
290
324
|
const sessionExists = typeof sessionId === "string" && this._sessions.has(sessionId);
|
|
291
325
|
|
|
292
326
|
if (sessionExists) return;
|
|
327
|
+
|
|
293
328
|
if (!(await this._initialized)) {
|
|
294
329
|
this._initialize();
|
|
295
|
-
|
|
296
330
|
await this._initialized;
|
|
297
331
|
}
|
|
298
332
|
|
|
299
|
-
const user = await this._getAuthorizedUser(token, _options);
|
|
333
|
+
const user: BaseUser | null = await this._getAuthorizedUser(token, _options);
|
|
300
334
|
const ioAuthorize = this._getIOAuthorize(_options);
|
|
301
335
|
const isUnauthorized = !!ioAuthorize && !user;
|
|
302
|
-
|
|
303
|
-
// There is an attempt for multiple of the same identities. Probably malicious.
|
|
304
|
-
const isTokenInUse: boolean =
|
|
305
|
-
!!user &&
|
|
306
|
-
(await this._getSessions().then((sessions) => sessions.some((session) => session.user?.id === user.id)));
|
|
307
|
-
|
|
308
336
|
const pluvWs = this._platform.convertWebSocket(webSocket, { room: this.id });
|
|
309
337
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
await this._platform.acceptWebSocket(pluvWs);
|
|
313
|
-
|
|
314
|
-
if (isUnauthorized || isTokenInUse) {
|
|
315
|
-
this._logDebug(`${colors.blue("Authorization failed for connection:")} ${pluvWs.sessionId}`);
|
|
316
|
-
|
|
338
|
+
if (isUnauthorized) {
|
|
339
|
+
this._logDebug(`${colors.blue("Authorization failed for connection")}`);
|
|
317
340
|
pluvWs.handleError({ error: new Error("Not authorized"), room: this.id });
|
|
318
341
|
pluvWs.close(3000, "WebSocket unauthorized.");
|
|
319
342
|
|
|
320
343
|
return;
|
|
321
344
|
}
|
|
322
345
|
|
|
323
|
-
|
|
346
|
+
if (!!user) {
|
|
347
|
+
const latest = this._getLatestPresence(user.id);
|
|
348
|
+
const prevState = pluvWs.state;
|
|
349
|
+
|
|
350
|
+
pluvWs.user = user;
|
|
351
|
+
|
|
352
|
+
this._platform.setSerializedState(pluvWs, {
|
|
353
|
+
...prevState,
|
|
354
|
+
presence: latest.presence,
|
|
355
|
+
timers: { ...prevState.timers, presence: latest.timer },
|
|
356
|
+
});
|
|
357
|
+
this._addUserSession(user.id, pluvWs.sessionId);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this._logDebug(`${colors.blue(`Registering connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
|
|
324
361
|
|
|
362
|
+
await this._platform.acceptWebSocket(pluvWs);
|
|
363
|
+
this._sessions.set(pluvWs.sessionId, pluvWs);
|
|
325
364
|
await this._platform.persistence.addUser(this.id, pluvWs.sessionId, user ?? {});
|
|
326
365
|
|
|
327
366
|
if (this._platform._config.registrationMode === "attached") {
|
|
@@ -335,18 +374,26 @@ export class IORoom<
|
|
|
335
374
|
|
|
336
375
|
await this._emitRegistered(pluvWs);
|
|
337
376
|
|
|
338
|
-
this._logDebug(`${colors.blue(`Registered connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
|
|
339
|
-
|
|
340
377
|
const size = this.getSize();
|
|
341
378
|
|
|
379
|
+
this._logDebug(`${colors.blue(`Registered connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
|
|
342
380
|
this._logDebug(`${colors.blue(`Room ${this.id} size:`)} ${size}`);
|
|
343
381
|
}
|
|
344
382
|
|
|
383
|
+
private _addUserSession(userId: string, sessionId: string): Set<[sessionId: string][0]> {
|
|
384
|
+
const set = this._userSessionss.get(userId) ?? new Set<string>();
|
|
385
|
+
const updated = set.add(sessionId);
|
|
386
|
+
|
|
387
|
+
this._userSessionss.set(userId, updated);
|
|
388
|
+
|
|
389
|
+
return updated;
|
|
390
|
+
}
|
|
391
|
+
|
|
345
392
|
private async _broadcast(params: BroadcastParams<this>): Promise<void> {
|
|
346
393
|
const { message, senderId } = params;
|
|
347
394
|
|
|
348
395
|
const sender = senderId ? (this._sessions.get(senderId) ?? null) : null;
|
|
349
|
-
const session =
|
|
396
|
+
const session = sender?.session ?? null;
|
|
350
397
|
const user = session?.user ?? null;
|
|
351
398
|
|
|
352
399
|
if (typeof senderId !== "string") return;
|
|
@@ -359,8 +406,8 @@ export class IORoom<
|
|
|
359
406
|
});
|
|
360
407
|
}
|
|
361
408
|
|
|
362
|
-
private async _closeWebSockets(webSockets: readonly AbstractWebSocket[]): Promise<void> {
|
|
363
|
-
const closeWebSocket = async (webSocket: AbstractWebSocket): Promise<void> => {
|
|
409
|
+
private async _closeWebSockets(webSockets: readonly AbstractWebSocket<any, TAuthorize>[]): Promise<void> {
|
|
410
|
+
const closeWebSocket = async (webSocket: AbstractWebSocket<any, TAuthorize>): Promise<void> => {
|
|
364
411
|
webSocket.state = { ...webSocket.state, quit: true };
|
|
365
412
|
|
|
366
413
|
const sessionId = webSocket.sessionId;
|
|
@@ -374,8 +421,13 @@ export class IORoom<
|
|
|
374
421
|
senderId: sessionId,
|
|
375
422
|
});
|
|
376
423
|
|
|
424
|
+
const session = webSocket.session;
|
|
425
|
+
const user = session.user;
|
|
426
|
+
|
|
427
|
+
if (!!user) this._removeUserSession(user.id, sessionId);
|
|
428
|
+
|
|
377
429
|
try {
|
|
378
|
-
const
|
|
430
|
+
const doc = await this._doc;
|
|
379
431
|
const encodedState = doc.getEncodedState();
|
|
380
432
|
|
|
381
433
|
await Promise.resolve(
|
|
@@ -383,7 +435,7 @@ export class IORoom<
|
|
|
383
435
|
context: this._context,
|
|
384
436
|
encodedState,
|
|
385
437
|
room: this.id,
|
|
386
|
-
user
|
|
438
|
+
user,
|
|
387
439
|
}),
|
|
388
440
|
);
|
|
389
441
|
} catch (error) {
|
|
@@ -419,12 +471,23 @@ export class IORoom<
|
|
|
419
471
|
this._closeWebSockets(quitters);
|
|
420
472
|
}
|
|
421
473
|
|
|
422
|
-
private async _emitRegistered(pluvWs: AbstractWebSocket): Promise<void> {
|
|
423
|
-
const session =
|
|
474
|
+
private async _emitRegistered(pluvWs: AbstractWebSocket<any, TAuthorize>): Promise<void> {
|
|
475
|
+
const session = pluvWs.session;
|
|
424
476
|
const sessionId = session.id;
|
|
477
|
+
const presence = session.presence;
|
|
425
478
|
const user = session.user;
|
|
426
479
|
|
|
427
|
-
await this._sendSelfMessage(
|
|
480
|
+
await this._sendSelfMessage(
|
|
481
|
+
{
|
|
482
|
+
type: "$registered",
|
|
483
|
+
data: {
|
|
484
|
+
presence,
|
|
485
|
+
sessionId,
|
|
486
|
+
timers: { presence: session.timers.presence },
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
{ sessionId, user },
|
|
490
|
+
);
|
|
428
491
|
|
|
429
492
|
try {
|
|
430
493
|
const doc = await this._doc;
|
|
@@ -450,7 +513,7 @@ export class IORoom<
|
|
|
450
513
|
throw new Error("Platform must use detached mode");
|
|
451
514
|
}
|
|
452
515
|
|
|
453
|
-
private _getAbstractWs(webSocket: WebSocketType<TPlatform>): AbstractWebSocket | null {
|
|
516
|
+
private _getAbstractWs(webSocket: WebSocketType<TPlatform>): AbstractWebSocket<any, TAuthorize> | null {
|
|
454
517
|
if ((webSocket as unknown as any) instanceof AbstractWebSocket) return webSocket;
|
|
455
518
|
|
|
456
519
|
const sessionId = this._platform.getSessionId(webSocket);
|
|
@@ -469,7 +532,7 @@ export class IORoom<
|
|
|
469
532
|
private async _getAuthorizedUser(
|
|
470
533
|
token: Maybe<string>,
|
|
471
534
|
options: WebSocketRegisterConfig<TPlatform>,
|
|
472
|
-
): Promise<InferIOAuthorizeUser<
|
|
535
|
+
): Promise<InferIOAuthorizeUser<TAuthorize>> {
|
|
473
536
|
const ioAuthorize = this._getIOAuthorize(options);
|
|
474
537
|
|
|
475
538
|
if (!ioAuthorize) return null as InferIOAuthorizeUser<InferIOAuthorize<this>>;
|
|
@@ -521,6 +584,36 @@ export class IORoom<
|
|
|
521
584
|
return this._authorize as ResolvedPluvIOAuthorize<any, any> | null;
|
|
522
585
|
}
|
|
523
586
|
|
|
587
|
+
private _getLatestPresence(userId: string): { timer: number | null; presence: JsonObject | null } {
|
|
588
|
+
if (!this._authorize) return { timer: null, presence: null };
|
|
589
|
+
|
|
590
|
+
const sessionIds = Array.from(this._userSessionss.get(userId)?.values() ?? []);
|
|
591
|
+
|
|
592
|
+
if (!sessionIds.length) return { timer: null, presence: null };
|
|
593
|
+
|
|
594
|
+
return sessionIds.reduce(
|
|
595
|
+
(state, sessionId) => {
|
|
596
|
+
const pluvWs = this._sessions.get(sessionId) ?? null;
|
|
597
|
+
|
|
598
|
+
if (!pluvWs) return state;
|
|
599
|
+
|
|
600
|
+
const session = pluvWs.session;
|
|
601
|
+
const presence = session.presence;
|
|
602
|
+
const timer = session.timers.presence;
|
|
603
|
+
|
|
604
|
+
if (session.user?.id !== userId) return state;
|
|
605
|
+
if (typeof state.timer !== "number") return { presence, timer };
|
|
606
|
+
if (typeof timer !== "number") return state;
|
|
607
|
+
|
|
608
|
+
return timer > state.timer ? { presence, timer } : state;
|
|
609
|
+
},
|
|
610
|
+
{ presence: null, timer: null } as {
|
|
611
|
+
presence: JsonObject | null;
|
|
612
|
+
timer: number | null;
|
|
613
|
+
},
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
524
617
|
private _getProcedure(
|
|
525
618
|
message: EventMessage<string, any>,
|
|
526
619
|
): (typeof this._router)["_defs"]["events"][keyof (typeof this._router)["_defs"]["events"]] | null {
|
|
@@ -535,18 +628,18 @@ export class IORoom<
|
|
|
535
628
|
return procedure.config.input ? procedure.config.input.parse(message.data) : message.data;
|
|
536
629
|
}
|
|
537
630
|
|
|
538
|
-
private
|
|
631
|
+
private _getSession(webSocket: WebSocketType<TPlatform>): WebSocketSession<TAuthorize> {
|
|
539
632
|
const pluvWs = this._getAbstractWs(webSocket);
|
|
540
633
|
|
|
541
634
|
if (!pluvWs) throw new Error("Session could not be found");
|
|
542
635
|
|
|
543
|
-
return
|
|
636
|
+
return pluvWs.session;
|
|
544
637
|
}
|
|
545
638
|
|
|
546
|
-
private
|
|
547
|
-
const
|
|
639
|
+
private _getSessions() {
|
|
640
|
+
const sessions = Array.from(this._sessions.values()).map((pluvWs) => pluvWs.session);
|
|
548
641
|
|
|
549
|
-
return
|
|
642
|
+
return sessions;
|
|
550
643
|
}
|
|
551
644
|
|
|
552
645
|
private _initialize() {
|
|
@@ -624,14 +717,14 @@ export class IORoom<
|
|
|
624
717
|
if (this._debug) console.log(...data);
|
|
625
718
|
}
|
|
626
719
|
|
|
627
|
-
private _onClose(webSocket: AbstractWebSocket): () => Promise<void> {
|
|
720
|
+
private _onClose(webSocket: AbstractWebSocket<any, TAuthorize>): () => Promise<void> {
|
|
628
721
|
return async (): Promise<void> => {
|
|
629
722
|
if (!(await this._initialized)) return;
|
|
630
723
|
await this._closeWebSockets([webSocket]);
|
|
631
724
|
};
|
|
632
725
|
}
|
|
633
726
|
|
|
634
|
-
private _onMessage(webSocket: AbstractWebSocket): (event: AbstractMessageEvent) => Promise<void> {
|
|
727
|
+
private _onMessage(webSocket: AbstractWebSocket<any, TAuthorize>): (event: AbstractMessageEvent) => Promise<void> {
|
|
635
728
|
return async (event: AbstractMessageEvent): Promise<void> => {
|
|
636
729
|
if (!(await this._initialized)) return;
|
|
637
730
|
|
|
@@ -639,18 +732,34 @@ export class IORoom<
|
|
|
639
732
|
|
|
640
733
|
if (!pluvWs) throw new Error("Could not get session");
|
|
641
734
|
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
735
|
+
const session = this._getSession(pluvWs as WebSocketType<TPlatform>);
|
|
736
|
+
const sessions = this._getSessions();
|
|
737
|
+
|
|
738
|
+
const setPresence = (params: PatchPresenceParams): void => {
|
|
739
|
+
this._setPresence.bind(this)(params);
|
|
740
|
+
};
|
|
646
741
|
|
|
647
742
|
const doc = await this._doc;
|
|
648
743
|
const eventContext: EventResolverContext<EventResolverKind, TPlatform, TAuthorize, TContext> = {
|
|
649
744
|
context: this._context,
|
|
650
745
|
doc,
|
|
746
|
+
garbageCollect: async () => {
|
|
747
|
+
await this.garbageCollect();
|
|
748
|
+
},
|
|
749
|
+
get presence() {
|
|
750
|
+
return session.presence as JsonObject | null;
|
|
751
|
+
},
|
|
752
|
+
set presence(presence: JsonObject | null) {
|
|
753
|
+
const sessionId = this.session?.id;
|
|
754
|
+
|
|
755
|
+
if (!sessionId) return;
|
|
756
|
+
|
|
757
|
+
setPresence({ presence, sessionId, timer: this.time });
|
|
758
|
+
},
|
|
651
759
|
room: this.id,
|
|
652
760
|
session,
|
|
653
761
|
sessions,
|
|
762
|
+
time: new Date().getTime(),
|
|
654
763
|
};
|
|
655
764
|
|
|
656
765
|
if (pluvWs.state.quit) {
|
|
@@ -711,24 +820,24 @@ export class IORoom<
|
|
|
711
820
|
const handleBroadcast = async () => {
|
|
712
821
|
if (!broadcast) return;
|
|
713
822
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
);
|
|
823
|
+
const messages = Object.entries(broadcast).map(async ([type, data]: any) => {
|
|
824
|
+
await this._broadcast({
|
|
825
|
+
message: { data, type },
|
|
826
|
+
senderId: sessionId,
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
await Promise.all(messages);
|
|
722
831
|
};
|
|
723
832
|
|
|
724
833
|
const handleSelf = async () => {
|
|
725
834
|
if (!self) return;
|
|
726
835
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
);
|
|
836
|
+
const messages = Object.entries(self).map(async ([type, data]) => {
|
|
837
|
+
await this._sendSelfMessage({ data, type }, { sessionId, user });
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
await Promise.all(messages);
|
|
732
841
|
};
|
|
733
842
|
|
|
734
843
|
const handleSync = async () => {
|
|
@@ -742,11 +851,11 @@ export class IORoom<
|
|
|
742
851
|
...message,
|
|
743
852
|
});
|
|
744
853
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
);
|
|
854
|
+
const messages = Object.entries(sync).map(async ([type, data]) => {
|
|
855
|
+
await this._sendSelfMessage({ data, type }, { sessionId, user });
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
await Promise.all(messages);
|
|
750
859
|
};
|
|
751
860
|
|
|
752
861
|
await Promise.all([handleBroadcast(), handleSelf(), handleSync()]);
|
|
@@ -768,7 +877,58 @@ export class IORoom<
|
|
|
768
877
|
}
|
|
769
878
|
}
|
|
770
879
|
|
|
771
|
-
private
|
|
880
|
+
private _setPresence(params: PatchPresenceParams): void {
|
|
881
|
+
const { presence, sessionId, timer: _timer } = params;
|
|
882
|
+
|
|
883
|
+
const timer = _timer ?? new Date().getTime();
|
|
884
|
+
const pluvWs = this._sessions.get(sessionId) ?? null;
|
|
885
|
+
|
|
886
|
+
if (!pluvWs) return;
|
|
887
|
+
|
|
888
|
+
const wsSession = pluvWs.session;
|
|
889
|
+
const user = wsSession.user as BaseUser | null;
|
|
890
|
+
|
|
891
|
+
const sessionIds = user
|
|
892
|
+
? new Set<string>([...(this._userSessionss.get(user.id) ?? []), sessionId])
|
|
893
|
+
: new Set([sessionId]);
|
|
894
|
+
|
|
895
|
+
sessionIds.forEach((sId) => {
|
|
896
|
+
const pWs = this._sessions.get(sId);
|
|
897
|
+
const session = pWs?.session;
|
|
898
|
+
|
|
899
|
+
if (!session) return;
|
|
900
|
+
|
|
901
|
+
const prevState = session.webSocket.state;
|
|
902
|
+
|
|
903
|
+
this._platform.setSerializedState(session.webSocket, {
|
|
904
|
+
...prevState,
|
|
905
|
+
presence,
|
|
906
|
+
timers: {
|
|
907
|
+
...prevState.timers,
|
|
908
|
+
presence: timer,
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private _removeUserSession(userId: string, sessionId: string): Set<[sessionId: string][0]> | null {
|
|
915
|
+
const set = this._userSessionss.get(userId);
|
|
916
|
+
|
|
917
|
+
if (!set) return null;
|
|
918
|
+
|
|
919
|
+
set.delete(sessionId);
|
|
920
|
+
|
|
921
|
+
if (!!set.size) return set;
|
|
922
|
+
|
|
923
|
+
this._userSessionss.delete(userId);
|
|
924
|
+
|
|
925
|
+
return set;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private async _sendMessage(
|
|
929
|
+
pluvWs: AbstractWebSocket<any, TAuthorize>,
|
|
930
|
+
message: IOEventMessage<any>,
|
|
931
|
+
): Promise<void> {
|
|
772
932
|
if (!(await this._initialized)) return;
|
|
773
933
|
|
|
774
934
|
await Promise.resolve(pluvWs.sendMessage(message));
|
|
@@ -790,10 +950,10 @@ export class IORoom<
|
|
|
790
950
|
const pluvWs = this._sessions.get(id);
|
|
791
951
|
|
|
792
952
|
return pluvWs ? dict.set(id, pluvWs) : dict;
|
|
793
|
-
}, new Map<string, AbstractWebSocket
|
|
953
|
+
}, new Map<string, AbstractWebSocket<any, TAuthorize>>()) ?? this._sessions;
|
|
794
954
|
|
|
795
955
|
const promises = Array.from(webSockets.values()).map(async (pluvWs) => {
|
|
796
|
-
const session =
|
|
956
|
+
const session = pluvWs.session;
|
|
797
957
|
const user = session.user ?? null;
|
|
798
958
|
|
|
799
959
|
this._sendMessage(pluvWs, { connectionId, data, room, type, user });
|
|
@@ -813,9 +973,22 @@ export class IORoom<
|
|
|
813
973
|
const pluvWs = this._sessions.get(senderId);
|
|
814
974
|
|
|
815
975
|
if (!pluvWs) return;
|
|
816
|
-
if (message.type === "$pong") await this._emitQuitters();
|
|
817
976
|
|
|
818
|
-
|
|
977
|
+
/**
|
|
978
|
+
* @description Note that this will not fire for Cloudflare's hibernatable websockets
|
|
979
|
+
* because the server will auto-respond with the $pong event without calling any
|
|
980
|
+
* functions.
|
|
981
|
+
* @date April 18, 2025
|
|
982
|
+
*/
|
|
983
|
+
if (message.type === "$pong") {
|
|
984
|
+
const elapsedMs = Date.now() - this._lastGarbageCollectMs;
|
|
985
|
+
|
|
986
|
+
if (elapsedMs > GARBAGE_COLLECT_INTERVAL_MS) {
|
|
987
|
+
await this.garbageCollect();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const session = pluvWs.session;
|
|
819
992
|
const user = session.user ?? null;
|
|
820
993
|
|
|
821
994
|
this._sendMessage(pluvWs, {
|
|
@@ -840,9 +1013,19 @@ export class IORoom<
|
|
|
840
1013
|
const context: EventResolverContext<"sync", TPlatform, TAuthorize, TContext> = {
|
|
841
1014
|
context: this._context,
|
|
842
1015
|
doc,
|
|
1016
|
+
garbageCollect: async () => {
|
|
1017
|
+
await this.garbageCollect();
|
|
1018
|
+
},
|
|
1019
|
+
get presence() {
|
|
1020
|
+
return null;
|
|
1021
|
+
},
|
|
1022
|
+
set presence(presence: JsonObject | null) {
|
|
1023
|
+
return;
|
|
1024
|
+
},
|
|
843
1025
|
room: this.id,
|
|
844
1026
|
session: null,
|
|
845
|
-
sessions:
|
|
1027
|
+
sessions: this._getSessions(),
|
|
1028
|
+
time: new Date().getTime(),
|
|
846
1029
|
};
|
|
847
1030
|
|
|
848
1031
|
const resolver = this._getProcedure(message)?.config.sync;
|