@pluv/io 0.20.0 → 0.21.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/src/IORoom.ts CHANGED
@@ -18,16 +18,21 @@ import type {
18
18
  import colors from "kleur";
19
19
  import type {
20
20
  AbstractPlatform,
21
- InferPlatformEventContextType,
22
- InferPlatformRoomContextType,
21
+ InferPlatformWebSocketSource,
23
22
  InferPlatformWebSocketType,
23
+ InferRoomContextType,
24
24
  } from "./AbstractPlatform";
25
- import { AbstractMessageEvent } from "./AbstractWebSocket";
25
+ import { AbstractCloseEvent, AbstractErrorEvent, AbstractMessageEvent, AbstractWebSocket } from "./AbstractWebSocket";
26
26
  import type { PluvRouter, PluvRouterEventConfig } from "./PluvRouter";
27
27
  import { authorize } from "./authorize";
28
- import type { EventResolverContext, IORoomListenerEvent, SendMessageOptions, WebSocketSession } from "./types";
29
-
30
- const PING_TIMEOUT_MS = 30_000;
28
+ import { PING_TIMEOUT_MS } from "./constants";
29
+ import type {
30
+ EventResolverContext,
31
+ IORoomListenerEvent,
32
+ SendMessageOptions,
33
+ WebSocketSession,
34
+ WebSocketType,
35
+ } from "./types";
31
36
 
32
37
  type BroadcastMessage<TIO extends IORoom<any, any, any, any>> =
33
38
  | InferEventMessage<InferIOInput<TIO>>
@@ -51,12 +56,12 @@ export type BroadcastProxy<TIO extends IORoom<any, any, any, any>> = (<TEvent ex
51
56
 
52
57
  export type IORoomConfig<
53
58
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
54
- TAuthorize extends IOAuthorize<any, any, InferPlatformRoomContextType<TPlatform>> = BaseIOAuthorize,
59
+ TAuthorize extends IOAuthorize<any, any, InferRoomContextType<TPlatform>> = BaseIOAuthorize,
55
60
  TContext extends JsonObject = {},
56
61
  TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
57
62
  > = Partial<IORoomListeners<TPlatform>> & {
58
63
  authorize?: TAuthorize;
59
- context: TContext & InferPlatformRoomContextType<TPlatform>;
64
+ context: TContext & InferRoomContextType<TPlatform>;
60
65
  crdt?: { doc: (value: any) => AbstractCrdtDocFactory<any> };
61
66
  debug: boolean;
62
67
  platform: TPlatform;
@@ -64,30 +69,29 @@ export type IORoomConfig<
64
69
  };
65
70
 
66
71
  interface SendMessageSender {
67
- id: string | null;
72
+ sessionId: string | null;
68
73
  user: JsonObject | null;
69
74
  }
70
75
 
71
- export type WebsocketRegisterOptions<TPlatform extends AbstractPlatform> = {
76
+ export interface WebsocketRegisterOptions {
72
77
  token?: string | null;
73
- } & InferPlatformRoomContextType<TPlatform> &
74
- InferPlatformEventContextType<TPlatform>;
78
+ }
75
79
 
76
80
  export class IORoom<
77
81
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
78
- TAuthorize extends IOAuthorize<any, any, InferPlatformRoomContextType<TPlatform>> = BaseIOAuthorize,
82
+ TAuthorize extends IOAuthorize<any, any, InferRoomContextType<TPlatform>> = BaseIOAuthorize,
79
83
  TContext extends Record<string, any> = {},
80
84
  TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
81
85
  > implements IOLike<TAuthorize, TEvents>
82
86
  {
83
- private readonly _context: TContext & InferPlatformRoomContextType<TPlatform>;
87
+ private readonly _context: TContext & InferRoomContextType<TPlatform>;
84
88
  private readonly _debug: boolean;
85
89
  private readonly _docFactory: AbstractCrdtDocFactory<any>;
86
90
  private readonly _platform: TPlatform;
87
91
 
88
92
  private _doc: AbstractCrdtDoc<any>;
89
93
  private _listeners: IORoomListeners<TPlatform>;
90
- private _sessions = new Map<string, WebSocketSession<TAuthorize>>();
94
+ private _sessions = new Map<string, AbstractWebSocket>();
91
95
  private _uninitialize: (() => Promise<void>) | null = null;
92
96
 
93
97
  readonly _authorize: TAuthorize | null = null;
@@ -124,7 +128,7 @@ export class IORoom<
124
128
  this._docFactory = crdt.doc(() => ({}));
125
129
  this._doc = this._docFactory.getEmpty();
126
130
  this._router = router;
127
- this._platform = platform;
131
+ this._platform = platform.initialize({ context });
128
132
 
129
133
  this.id = id;
130
134
 
@@ -133,6 +137,20 @@ export class IORoom<
133
137
  };
134
138
 
135
139
  if (authorize) this._authorize = authorize;
140
+
141
+ const webSockets = this._platform.getWebSockets() as readonly InferPlatformWebSocketSource<TPlatform>[];
142
+
143
+ if (!webSockets.length) return;
144
+
145
+ webSockets.forEach((webSocket) => {
146
+ const deserialized = this._platform.getSerializedState(webSocket);
147
+ const sessionId = this._platform.getSessionId(webSocket);
148
+
149
+ if (!deserialized) return;
150
+ if (typeof sessionId !== "string") return;
151
+
152
+ this._sessions.set(sessionId, this._platform.convertWebSocket(webSocket, { room: this.id }));
153
+ });
136
154
  }
137
155
 
138
156
  public getSize(): number {
@@ -144,147 +162,177 @@ export class IORoom<
144
162
  * @author leedavidcs
145
163
  * @date December 21, 2022
146
164
  */
147
- return Array.from(this._sessions.values()).reduce((count, session) => {
148
- if (session.quit) return count;
165
+ return Array.from(this._sessions.values()).reduce((count, pluvWs) => {
166
+ if (pluvWs.state.quit) return count;
167
+
168
+ const pingTime = this._platform.getLastPing(pluvWs) ?? pluvWs.state.timers.ping;
149
169
 
150
- return currentTime - session.timers.ping > PING_TIMEOUT_MS ? count : count + 1;
170
+ return currentTime - pingTime > PING_TIMEOUT_MS ? count : count + 1;
151
171
  }, 0);
152
172
  }
153
173
 
174
+ public onClose(webSocket: WebSocketType<TPlatform>): (event: AbstractCloseEvent) => void {
175
+ this._ensureDetached();
176
+
177
+ const wsSession = this._getAbstractWs(webSocket);
178
+
179
+ if (!wsSession) return (): void => undefined;
180
+
181
+ return this._onClose(wsSession);
182
+ }
183
+
184
+ public onError(webSocket: WebSocketType<TPlatform>): (event: AbstractErrorEvent) => void {
185
+ this._ensureDetached();
186
+
187
+ const wsSession = this._getAbstractWs(webSocket);
188
+
189
+ if (!wsSession) return (): void => undefined;
190
+
191
+ return this._onClose(wsSession);
192
+ }
193
+
194
+ public onMessage(webSocket: WebSocketType<TPlatform>): (event: AbstractMessageEvent) => void {
195
+ this._ensureDetached();
196
+
197
+ const wsSession = this._getAbstractWs(webSocket);
198
+
199
+ if (!wsSession) return (): void => undefined;
200
+
201
+ return this._onMessage(wsSession);
202
+ }
203
+
154
204
  public async register(
155
- webSocket: InferPlatformWebSocketType<TPlatform>,
156
- options: WebsocketRegisterOptions<TPlatform>,
205
+ webSocket: InferPlatformWebSocketSource<TPlatform>,
206
+ options: WebsocketRegisterOptions = {},
157
207
  ): Promise<void> {
158
- const { token, ..._platformEventContext } = options;
159
- const platformEventContext = _platformEventContext as InferPlatformRoomContextType<TPlatform> &
160
- InferPlatformEventContextType<TPlatform>;
208
+ const { token } = options;
161
209
 
162
210
  if (!this._uninitialize) await this._initialize();
163
211
 
164
- const user = await this._getAuthorizedUser(token, options);
212
+ const user = await this._getAuthorizedUser(token);
213
+ const ioAuthorize = this._getIOAuthorize();
214
+ const isUnauthorized = !!ioAuthorize?.required && !user;
165
215
 
166
- const sessionId = this._platform.randomUUID();
167
- const pluvWs = this._platform.convertWebSocket(webSocket, {
168
- room: this.id,
169
- sessionId,
170
- userId: user?.id ?? null,
171
- });
216
+ // There is an attempt for multiple of the same identities. Probably malicious.
217
+ const isTokenInUse: boolean =
218
+ !!user &&
219
+ (await this._getSessions().then((sessions) => sessions.some((session) => session.user?.id === user.id)));
172
220
 
173
- this._logDebug(`${colors.blue(`Registering connection for room ${this.id}:`)} ${sessionId}`);
221
+ const pluvWs = await this._platform.convertWebSocket(webSocket, { room: this.id });
174
222
 
175
- const uninitializeWs = await pluvWs.initialize();
176
- const ioAuthorize = this._getIOAuthorize(options);
223
+ this._logDebug(`${colors.blue(`Registering connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
177
224
 
178
- const isUnauthorized = !!ioAuthorize?.required && !user;
179
- // There is an attempt for multiple of the same identities. Probably malicious.
180
- const isTokenInUse =
181
- !!user && Array.from(this._sessions.values()).some((session) => session.user?.id === user.id);
225
+ await this._platform.acceptWebSocket(pluvWs);
182
226
 
183
227
  if (isUnauthorized || isTokenInUse) {
184
- this._logDebug(`${colors.blue("Authorization failed for connection:")} ${sessionId}`);
228
+ this._logDebug(`${colors.blue("Authorization failed for connection:")} ${pluvWs.sessionId}`);
185
229
 
186
- pluvWs.handleError({ error: new Error("Not authorized") });
230
+ pluvWs.handleError({ error: new Error("Not authorized"), room: this.id });
187
231
  pluvWs.close(3000, "WebSocket unauthorized.");
188
232
 
189
233
  return;
190
234
  }
191
235
 
192
- const currentTime = new Date().getTime();
193
- const session: WebSocketSession<TAuthorize> = {
194
- id: sessionId,
195
- presence: null,
196
- quit: false,
197
- room: this.id,
198
- timers: {
199
- ping: currentTime,
200
- },
201
- webSocket: pluvWs,
202
- user,
203
- };
204
-
205
- this._sessions.set(session.id, session);
236
+ this._sessions.set(pluvWs.sessionId, pluvWs);
206
237
 
207
- await this._platform.persistance.addUser(this.id, sessionId, user ?? {});
238
+ await this._platform.persistance.addUser(this.id, pluvWs.sessionId, user ?? {});
208
239
 
209
- const onClose = this._onClose(session, uninitializeWs).bind(this);
210
- const onMessage = this._onMessage(session, platformEventContext).bind(this);
240
+ if (this._platform._registrationMode === "attached") {
241
+ const onClose = this._onClose(pluvWs).bind(this);
242
+ const onMessage = this._onMessage(pluvWs).bind(this);
211
243
 
212
- pluvWs.addEventListener("close", onClose);
213
- pluvWs.addEventListener("error", onClose);
214
- pluvWs.addEventListener("message", onMessage);
244
+ pluvWs.addEventListener("close", onClose);
245
+ pluvWs.addEventListener("error", onClose);
246
+ pluvWs.addEventListener("message", onMessage);
247
+ }
215
248
 
216
- this._emitRegistered(session);
249
+ await this._emitRegistered(pluvWs);
217
250
 
218
- this._logDebug(`${colors.blue(`Registered connection for room ${this.id}:`)} ${sessionId}`);
251
+ this._logDebug(`${colors.blue(`Registered connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
219
252
 
220
253
  const size = this.getSize();
221
254
 
222
255
  this._logDebug(`${colors.blue(`Room ${this.id} size:`)} ${size}`);
223
256
  }
224
257
 
225
- private _broadcast(params: BroadcastParams<this>): void {
258
+ private async _broadcast(params: BroadcastParams<this>): Promise<void> {
226
259
  const { message, senderId } = params;
227
260
 
228
- const currentTime = new Date().getTime();
229
- const sender = senderId ? this._sessions.get(senderId) : null;
230
-
231
- const quitters = Array.from(this._sessions.values()).filter(
232
- (session) => session.quit || currentTime - session.timers.ping > PING_TIMEOUT_MS,
233
- );
234
-
235
- quitters.forEach((session) => {
236
- this._sessions.delete(session.id);
237
- });
238
-
239
- this._emitQuitters(quitters);
261
+ const sender = senderId ? (this._sessions.get(senderId) ?? null) : null;
262
+ const session = (await sender?.getSession()) ?? null;
263
+ const user = session?.user ?? null;
240
264
 
241
265
  this._platform.pubSub.publish(this.id, {
242
266
  connectionId: senderId ?? null,
243
267
  room: this.id,
244
- user: sender?.user ?? null,
268
+ user,
245
269
  ...message,
246
270
  });
247
271
  }
248
272
 
249
- private _emitQuitters(quitters: readonly WebSocketSession<TAuthorize>[]): void {
250
- quitters.forEach((quitter) => {
251
- this._broadcast({
273
+ private async _emitQuitters(): Promise<void> {
274
+ const currentTime = new Date().getTime();
275
+ const quitters = Array.from(this._sessions.values()).filter(
276
+ (pluvWs) => pluvWs.state.quit || currentTime - pluvWs.state.timers.ping > PING_TIMEOUT_MS,
277
+ );
278
+
279
+ quitters.forEach((pluvWs) => {
280
+ this._sessions.delete(pluvWs.sessionId);
281
+ });
282
+
283
+ const promises = quitters.map(async (quitter) => {
284
+ await this._broadcast({
252
285
  message: {
253
286
  type: "$EXIT",
254
- data: { sessionId: quitter.id },
287
+ data: { sessionId: quitter.sessionId },
255
288
  },
256
- senderId: quitter.id,
289
+ senderId: quitter.sessionId,
257
290
  });
258
291
  });
292
+
293
+ await Promise.all(promises);
259
294
  }
260
295
 
261
- private _emitRegistered(session: WebSocketSession<TAuthorize>): void {
262
- this._sendSelfMessage(
263
- {
264
- type: "$REGISTERED",
265
- data: { sessionId: session.id },
266
- },
267
- session,
268
- );
296
+ private async _emitRegistered(pluvWs: AbstractWebSocket): Promise<void> {
297
+ const session = await pluvWs.getSession();
298
+ const sessionId = session.id;
299
+ const user = session.user;
300
+
301
+ await this._sendSelfMessage({ type: "$REGISTERED", data: { sessionId } }, { sessionId, user });
269
302
  }
270
303
 
271
- private _getIOAuthorize(context: InferPlatformRoomContextType<TPlatform>) {
272
- if (typeof this._authorize === "function") {
273
- return this._authorize(context);
304
+ private _ensureDetached(): void {
305
+ if (this._platform._registrationMode === "detached") return;
306
+
307
+ throw new Error("Platform must use detached mode");
308
+ }
309
+
310
+ private _getAbstractWs(webSocket: WebSocketType<TPlatform>): AbstractWebSocket | null {
311
+ if ((webSocket as unknown as any) instanceof AbstractWebSocket) return webSocket;
312
+
313
+ const sessionId = this._platform.getSessionId(webSocket);
314
+
315
+ if (typeof sessionId === "string") {
316
+ const session = this._sessions.get(sessionId) ?? null;
317
+
318
+ if (session) return session;
274
319
  }
275
320
 
276
- return this._authorize as {
277
- required: any;
278
- secret: string;
279
- user: InputZodLike<any>;
280
- } | null;
321
+ const sessions = Array.from(this._sessions.values());
322
+
323
+ return sessions.find((pluvWs) => pluvWs.webSocket === webSocket) ?? null;
281
324
  }
282
325
 
283
- private async _getAuthorizedUser(
284
- token: Maybe<string>,
285
- context: InferPlatformRoomContextType<TPlatform>,
286
- ): Promise<InferIOAuthorizeUser<InferIOAuthorize<this>>> {
287
- const ioAuthorize = this._getIOAuthorize(context);
326
+ private async _getSession(webSocket: WebSocketType<TPlatform>): Promise<WebSocketSession<TAuthorize>> {
327
+ const pluvWs = this._getAbstractWs(webSocket);
328
+
329
+ if (!pluvWs) throw new Error("Session could not be found");
330
+
331
+ return await pluvWs.getSession<TAuthorize>();
332
+ }
333
+
334
+ private async _getAuthorizedUser(token: Maybe<string>): Promise<InferIOAuthorizeUser<InferIOAuthorize<this>>> {
335
+ const ioAuthorize = this._getIOAuthorize();
288
336
 
289
337
  if (!ioAuthorize) return null as InferIOAuthorizeUser<InferIOAuthorize<this>>;
290
338
  if (!token) return null as InferIOAuthorizeUser<InferIOAuthorize<this>>;
@@ -318,6 +366,18 @@ export class IORoom<
318
366
  }
319
367
  }
320
368
 
369
+ private _getIOAuthorize() {
370
+ if (typeof this._authorize === "function") {
371
+ return this._authorize(this._context);
372
+ }
373
+
374
+ return this._authorize as {
375
+ required: any;
376
+ secret: string;
377
+ user: InputZodLike<any>;
378
+ } | null;
379
+ }
380
+
321
381
  private _getProcedure(
322
382
  message: EventMessage<string, any>,
323
383
  ): (typeof this._router)["_events"][keyof (typeof this._router)["_events"]] | null {
@@ -332,6 +392,12 @@ export class IORoom<
332
392
  return procedure.config.input ? procedure.config.input.parse(message.data) : message.data;
333
393
  }
334
394
 
395
+ private async _getSessions() {
396
+ const promises = Array.from(this._sessions.values()).map(async (pluvWs) => await pluvWs.getSession());
397
+
398
+ return await Promise.all(promises);
399
+ }
400
+
335
401
  private async _initialize(): Promise<void> {
336
402
  this._logDebug(`${colors.blue(`Initializing room ${this.id}:`)}`);
337
403
 
@@ -341,14 +407,15 @@ export class IORoom<
341
407
  this._uninitialize = null;
342
408
  }
343
409
 
344
- const pubSubId = await this._platform.pubSub.subscribe(this.id, ({ options = {}, ...message }) => {
345
- const session = {
346
- id: message.connectionId,
347
- user: message.user,
348
- };
410
+ const pubSubId = await this._platform.pubSub.subscribe(
411
+ this.id,
412
+ async ({ options = {}, ...message }): Promise<void> => {
413
+ const sessionId = message.connectionId;
414
+ const user = message.user;
349
415
 
350
- this._sendMessage(message, session, options);
351
- });
416
+ await this._sendMessage(message, { sessionId, user }, options);
417
+ },
418
+ );
352
419
 
353
420
  this._uninitialize = async () => {
354
421
  this._platform.pubSub.unsubscribe(pubSubId);
@@ -372,29 +439,27 @@ export class IORoom<
372
439
  this._debug && console.log(...data);
373
440
  }
374
441
 
375
- private _onClose(session: WebSocketSession<TAuthorize>, callback?: () => void): () => void {
442
+ private _onClose(webSocket: AbstractWebSocket): () => void {
376
443
  return (): void => {
377
444
  if (!this._uninitialize) return;
378
445
 
379
- session.quit = true;
446
+ webSocket.state.quit = true;
380
447
 
381
- this._logDebug(`${colors.blue(`(Unregistering connection for room ${this.id}:`)} ${session.id}`);
382
- this._sessions.delete(session.id);
448
+ this._logDebug(`${colors.blue(`Unregistering connection for room ${this.id}:`)} ${webSocket.sessionId}`);
449
+ this._sessions.delete(webSocket.sessionId);
383
450
 
384
- this._platform.persistance.deleteUser(this.id, session.id).finally(() => {
385
- this._broadcast({
451
+ this._platform.persistance.deleteUser(this.id, webSocket.sessionId).finally(async () => {
452
+ await this._broadcast({
386
453
  message: {
387
454
  type: "$EXIT",
388
- data: { sessionId: session.id },
455
+ data: { sessionId: webSocket.sessionId },
389
456
  },
390
- senderId: session.id,
457
+ senderId: webSocket.sessionId,
391
458
  });
392
459
 
393
- callback?.();
394
-
395
460
  const size = this.getSize();
396
461
 
397
- this._logDebug(`${colors.blue(`Unregistered connection for room ${this.id}:`)} ${session.id}`);
462
+ this._logDebug(`${colors.blue(`Unregistered connection for room ${this.id}:`)} ${webSocket.sessionId}`);
398
463
  this._logDebug(`${colors.blue(`Room ${this.id} size:`)} ${size}`);
399
464
 
400
465
  if (size) return;
@@ -404,23 +469,29 @@ export class IORoom<
404
469
  };
405
470
  }
406
471
 
407
- private _onMessage(
408
- session: WebSocketSession<TAuthorize>,
409
- platformEventContext: InferPlatformEventContextType<TPlatform>,
410
- ): (event: AbstractMessageEvent) => void {
411
- return (event: AbstractMessageEvent): void => {
472
+ private _onMessage(webSocket: AbstractWebSocket): (event: AbstractMessageEvent) => void {
473
+ return async (event: AbstractMessageEvent): Promise<void> => {
474
+ const pluvWs = this._getAbstractWs(webSocket as WebSocketType<TPlatform>);
475
+
476
+ if (!pluvWs) throw new Error("Could not get session");
477
+
478
+ const [session, sessions] = await Promise.all([
479
+ this._getSession(pluvWs as WebSocketType<TPlatform>),
480
+ this._getSessions(),
481
+ ]);
482
+
412
483
  const baseContext: EventResolverContext<TPlatform, TAuthorize, TContext> = {
413
484
  context: this._context,
414
485
  doc: this._doc,
415
486
  room: this.id,
416
487
  session,
417
- sessions: this._sessions,
488
+ sessions,
418
489
  };
419
490
 
420
491
  if (!this._uninitialize) return;
421
492
 
422
- if (session.quit) {
423
- session.webSocket.close(1011, "WebSocket broken.");
493
+ if (pluvWs.state.quit) {
494
+ pluvWs.close(1011, "WebSocket broken.");
424
495
 
425
496
  return;
426
497
  }
@@ -438,9 +509,10 @@ export class IORoom<
438
509
  try {
439
510
  inputs = this._getProcedureInputs(message);
440
511
  } catch (error) {
441
- session?.webSocket.handleError({
512
+ pluvWs.handleError({
442
513
  error,
443
514
  message: "Invalid input",
515
+ room: this.id,
444
516
  session,
445
517
  });
446
518
 
@@ -450,30 +522,32 @@ export class IORoom<
450
522
  const extendedContext: EventResolverContext<
451
523
  TPlatform,
452
524
  TAuthorize,
453
- TContext & InferPlatformRoomContextType<TPlatform> & InferPlatformEventContextType<TPlatform>
454
- > = {
455
- ...baseContext,
456
- context: { ...this._context, ...platformEventContext },
457
- };
525
+ TContext & InferRoomContextType<TPlatform>
526
+ > = { ...baseContext, context: { ...this._context } };
458
527
 
459
528
  Promise.all([
460
529
  procedure.config.broadcast?.(inputs, extendedContext),
461
530
  procedure.config.self?.(inputs, extendedContext),
462
531
  procedure.config.sync?.(inputs, baseContext),
463
- ]).then(([broadcast, self, sync]) => {
532
+ ]).then(async ([broadcast, self, sync]) => {
533
+ const sessionId = session.id;
534
+ const user = session.user;
535
+
464
536
  if (broadcast) {
465
537
  Object.entries(broadcast).forEach(([type, data]: any) => {
466
538
  this._broadcast({
467
539
  message: { data, type },
468
- senderId: session.id,
540
+ senderId: sessionId,
469
541
  });
470
542
  });
471
543
  }
472
544
 
473
545
  if (self) {
474
- Object.entries(self).forEach(([type, data]) => {
475
- this._sendSelfMessage({ data, type }, session);
476
- });
546
+ await Promise.all(
547
+ Object.entries(self).map(async ([type, data]) => {
548
+ await this._sendSelfMessage({ data, type }, { sessionId, user });
549
+ }),
550
+ );
477
551
  }
478
552
 
479
553
  if (sync) {
@@ -481,13 +555,15 @@ export class IORoom<
481
555
  connectionId: session.id,
482
556
  options: { type: "sync" },
483
557
  room: this.id,
484
- user: session.user,
558
+ user,
485
559
  ...message,
486
560
  });
487
561
 
488
- Object.entries(sync).forEach(([type, data]) => {
489
- this._sendSelfMessage({ data, type }, session);
490
- });
562
+ await Promise.all(
563
+ Object.entries(sync).map(async ([type, data]) => {
564
+ await this._sendSelfMessage({ data, type }, { sessionId, user });
565
+ }),
566
+ );
491
567
  }
492
568
  });
493
569
  };
@@ -507,19 +583,19 @@ export class IORoom<
507
583
  }
508
584
  }
509
585
 
510
- private _sendMessage(
586
+ private async _sendMessage(
511
587
  message: EventMessage<string, any>,
512
588
  sender: SendMessageSender | null,
513
589
  options: SendMessageOptions = {},
514
- ): void {
590
+ ): Promise<void> {
515
591
  switch (options.type) {
516
592
  case "self": {
517
- this._sendSelfMessage(message, sender);
593
+ await this._sendSelfMessage(message, sender);
518
594
 
519
595
  return;
520
596
  }
521
597
  case "sync": {
522
- this._sendSyncMessage(message, sender);
598
+ await this._sendSyncMessage(message, sender);
523
599
 
524
600
  return;
525
601
  }
@@ -527,63 +603,73 @@ export class IORoom<
527
603
  default: {
528
604
  const sessionIds = options.sessionIds;
529
605
 
530
- this._sendBroadcastMessage(message, sender, sessionIds);
606
+ await this._sendBroadcastMessage(message, sender, sessionIds);
531
607
  }
532
608
  }
533
609
  }
534
610
 
535
- private _sendBroadcastMessage(
611
+ private async _sendBroadcastMessage(
536
612
  message: EventMessage<string, any>,
537
613
  sender: SendMessageSender | null,
538
614
  sessionIds?: readonly string[],
539
- ): void {
540
- const senderId = sender?.id ?? null;
615
+ ): Promise<void> {
616
+ const { data, type } = message;
617
+ const connectionId = sender?.sessionId ?? null;
618
+ const room = this.id;
541
619
 
542
- const sessions =
620
+ const webSockets =
543
621
  sessionIds?.reduce((dict, id) => {
544
- const _session = this._sessions.get(id);
622
+ const pluvWs = this._sessions.get(id);
545
623
 
546
- return _session ? dict.set(id, _session) : dict;
547
- }, new Map<string, WebSocketSession<TAuthorize>>()) ?? this._sessions;
624
+ return pluvWs ? dict.set(id, pluvWs) : dict;
625
+ }, new Map<string, AbstractWebSocket>()) ?? this._sessions;
548
626
 
549
- Array.from(sessions.values()).forEach((_session) => {
550
- _session.webSocket.sendMessage({
551
- connectionId: senderId,
552
- room: this.id,
553
- user: _session.user,
554
- ...message,
555
- });
627
+ const promises = Array.from(webSockets.values()).map(async (pluvWs) => {
628
+ const session = await pluvWs.getSession();
629
+ const user = session.user ?? null;
630
+
631
+ pluvWs.sendMessage({ connectionId, data, room, type, user });
556
632
  });
633
+
634
+ await Promise.all(promises);
557
635
  }
558
636
 
559
- private _sendSelfMessage(message: EventMessage<string, any>, sender: SendMessageSender | null): void {
560
- const senderId = sender?.id;
637
+ private async _sendSelfMessage(
638
+ message: EventMessage<string, any>,
639
+ sender: SendMessageSender | null,
640
+ ): Promise<void> {
641
+ const senderId = sender?.sessionId ?? null;
642
+
643
+ if (typeof senderId !== "string") return;
561
644
 
562
- if (!senderId) return;
645
+ const pluvWs = this._sessions.get(senderId);
563
646
 
564
- const session = this._sessions.get(senderId);
647
+ if (!pluvWs) return;
648
+ if (message.type === "$PONG") await this._emitQuitters();
565
649
 
566
- if (!session) return;
650
+ const session = await pluvWs.getSession();
651
+ const user = session.user ?? null;
567
652
 
568
- session.webSocket.sendMessage({
569
- connectionId: sender.id,
653
+ pluvWs.sendMessage({
654
+ connectionId: senderId,
570
655
  room: this.id,
571
- user: sender.user,
656
+ user,
572
657
  ...message,
573
658
  });
574
659
  }
575
660
 
576
- private _sendSyncMessage(message: EventMessage<string, any>, sender: SendMessageSender | null): void {
577
- const senderId = sender?.id;
578
-
579
- if (!senderId) return;
661
+ private async _sendSyncMessage(
662
+ message: EventMessage<string, any>,
663
+ sender: SendMessageSender | null,
664
+ ): Promise<void> {
665
+ if (!sender) return;
580
666
 
581
667
  const context: EventResolverContext<TPlatform, TAuthorize, TContext> = {
582
668
  context: this._context,
583
669
  doc: this._doc,
584
670
  room: this.id,
585
671
  session: null,
586
- sessions: this._sessions,
672
+ sessions: await this._getSessions(),
587
673
  };
588
674
 
589
675
  const resolver = this._getProcedure(message)?.config.sync;
@@ -605,7 +691,7 @@ export class IORoom<
605
691
  const data = output[type] ?? {};
606
692
 
607
693
  this._platform.pubSub.publish(this.id, {
608
- connectionId: senderId,
694
+ connectionId: sender.sessionId,
609
695
  data,
610
696
  options: { type: "self" },
611
697
  room: this.id,