@pluv/io 0.3.9 → 0.4.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/src/IORoom.ts CHANGED
@@ -15,16 +15,20 @@ import type {
15
15
  JsonObject,
16
16
  Maybe,
17
17
  MaybePromise,
18
+ Spread,
18
19
  } from "@pluv/types";
19
20
  import colors from "kleur";
20
21
  import type { AbstractPlatform, InferWebSocketType } from "./AbstractPlatform";
21
22
  import { authorize } from "./authorize";
22
23
  import type {
24
+ EventResolver,
23
25
  EventResolverContext,
26
+ EventResolverObject,
24
27
  InferEventConfig,
25
28
  SendMessageOptions,
26
29
  WebSocketSession,
27
30
  } from "./types";
31
+ import { AbstractMessageEvent } from "./AbstractWebSocket";
28
32
 
29
33
  const PING_TIMEOUT_MS = 30_000;
30
34
 
@@ -46,12 +50,20 @@ export type IORoomConfig<
46
50
  TAuthorize extends IOAuthorize<any, any> = BaseIOAuthorize,
47
51
  TContext extends JsonObject = {},
48
52
  TInput extends EventRecord<string, any> = {},
49
- TOutput extends EventRecord<string, any> = {}
53
+ TOutputBroadcast extends EventRecord<string, any> = {},
54
+ TOutputSelf extends EventRecord<string, any> = {},
55
+ TOutputSync extends EventRecord<string, any> = {}
50
56
  > = Partial<IORoomListeners> & {
51
57
  authorize?: TAuthorize;
52
58
  context: TContext;
53
59
  debug: boolean;
54
- events: InferEventConfig<TContext, TInput, TOutput>;
60
+ events: InferEventConfig<
61
+ TContext,
62
+ TInput,
63
+ TOutputBroadcast,
64
+ TOutputSelf,
65
+ TOutputSync
66
+ >;
55
67
  initialStorage?: () => MaybePromise<Maybe<string>>;
56
68
  platform: TPlatform;
57
69
  };
@@ -70,8 +82,11 @@ export class IORoom<
70
82
  TAuthorize extends IOAuthorize<any, any> = BaseIOAuthorize,
71
83
  TContext extends JsonObject = {},
72
84
  TInput extends EventRecord<string, any> = {},
73
- TOutput extends EventRecord<string, any> = {}
74
- > implements IOLike<TAuthorize, TInput, TOutput>
85
+ TOutputBroadcast extends EventRecord<string, any> = {},
86
+ TOutputSelf extends EventRecord<string, any> = {},
87
+ TOutputSync extends EventRecord<string, any> = {}
88
+ > implements
89
+ IOLike<TAuthorize, TInput, TOutputBroadcast, TOutputSelf, TOutputSync>
75
90
  {
76
91
  private readonly _context: TContext;
77
92
  private readonly _debug: boolean;
@@ -84,13 +99,27 @@ export class IORoom<
84
99
  private _uninitialize: (() => Promise<void>) | null = null;
85
100
 
86
101
  readonly _authorize: TAuthorize | null = null;
87
- readonly _events: InferEventConfig<TContext, TInput, TOutput>;
102
+ readonly _events: InferEventConfig<
103
+ TContext,
104
+ TInput,
105
+ TOutputBroadcast,
106
+ TOutputSelf,
107
+ TOutputSync
108
+ >;
88
109
 
89
110
  readonly id: string;
90
111
 
91
112
  constructor(
92
113
  id: string,
93
- config: IORoomConfig<TPlatform, TAuthorize, TContext, TInput, TOutput>
114
+ config: IORoomConfig<
115
+ TPlatform,
116
+ TAuthorize,
117
+ TContext,
118
+ TInput,
119
+ TOutputBroadcast,
120
+ TOutputSelf,
121
+ TOutputSync
122
+ >
94
123
  ) {
95
124
  const {
96
125
  authorize,
@@ -210,120 +239,14 @@ export class IORoom<
210
239
  user ?? {}
211
240
  );
212
241
 
213
- this._emitRegistered(session);
214
- this._emitStorageReceived(session);
215
- this._emitUserJoined(session);
216
-
217
- const context: EventResolverContext<TContext> = {
218
- context: this._context,
219
- doc: this._doc,
220
- room: this.id,
221
- session,
222
- sessions: this._sessions,
223
- };
224
-
225
- pluvWs.addEventListener("message", (event) => {
226
- if (!this._uninitialize) return;
227
-
228
- if (session.quit) {
229
- session.webSocket.close(1011, "WebSocket broken.");
230
-
231
- return;
232
- }
233
-
234
- const message = this._parseMessage(event);
235
-
236
- if (!message) return;
237
-
238
- const eventConfig = this._getEventConfig(message);
239
-
240
- if (!eventConfig) return;
241
-
242
- this._resolveMessage(message, context).then((output) => {
243
- if (!output) return;
244
-
245
- const eventType = eventConfig.options?.type ?? "broadcast";
246
-
247
- switch (eventType) {
248
- case "self": {
249
- Object.entries(output).forEach(([type, data]) => {
250
- this._sendSelfMessage({ data, type }, session);
251
- });
252
-
253
- return;
254
- }
255
- case "sync": {
256
- this._platform.pubSub.publish(this.id, {
257
- connectionId: session.id,
258
- options: eventConfig.options,
259
- room: this.id,
260
- user: session.user,
261
- ...message,
262
- });
263
-
264
- Object.entries(output).forEach(([type, data]) => {
265
- this._sendSelfMessage({ data, type }, session);
266
- });
267
-
268
- return;
269
- }
270
- case "broadcast":
271
- default: {
272
- Object.entries(output).forEach(([type, data]: any) => {
273
- this._broadcast({
274
- message: { data, type },
275
- senderId: session.id,
276
- });
277
- });
278
- }
279
- }
280
- });
281
- });
282
-
283
- const closeHandler = () => {
284
- if (!this._uninitialize) return;
285
-
286
- session.quit = true;
287
-
288
- this._logDebug(
289
- `${colors.blue(
290
- `(Unregistering connection for room ${this.id}:`
291
- )} ${sessionId}`
292
- );
293
- this._sessions.delete(session.id);
294
-
295
- this._platform.persistance
296
- .deleteUser(this.id, session.id)
297
- .finally(() => {
298
- this._broadcast({
299
- message: {
300
- type: "$EXIT",
301
- data: { sessionId: session.id },
302
- },
303
- senderId: session.id,
304
- });
305
-
306
- uninitializeWs();
242
+ const onClose = this._onClose(session, uninitializeWs).bind(this);
243
+ const onMessage = this._onMessage(session).bind(this);
307
244
 
308
- const size = this.getSize();
309
-
310
- this._logDebug(
311
- `${colors.blue(
312
- `Unregistered connection for room ${this.id}:`
313
- )} ${sessionId}`
314
- );
315
- this._logDebug(
316
- `${colors.blue(`Room ${this.id} size:`)} ${size}`
317
- );
245
+ pluvWs.addEventListener("close", onClose);
246
+ pluvWs.addEventListener("error", onClose);
247
+ pluvWs.addEventListener("message", onMessage);
318
248
 
319
- if (size) return;
320
-
321
- this._uninitialize?.();
322
- });
323
- };
324
-
325
- pluvWs.addEventListener("close", closeHandler);
326
- pluvWs.addEventListener("error", closeHandler);
249
+ this._emitRegistered(session);
327
250
 
328
251
  this._logDebug(
329
252
  `${colors.blue(
@@ -384,42 +307,6 @@ export class IORoom<
384
307
  );
385
308
  }
386
309
 
387
- private _emitStorageReceived(session: WebSocketSession): void {
388
- this._platform.persistance
389
- .getStorageState(this.id)
390
- .then(async (_state) => {
391
- const state =
392
- _state ??
393
- (await this._initialStorage?.()) ??
394
- this._doc.encodeStateAsUpdate();
395
-
396
- this._sendSelfMessage(
397
- {
398
- type: "$STORAGE_RECEIVED",
399
- data: { state },
400
- },
401
- session
402
- );
403
- });
404
- }
405
-
406
- private _emitUserJoined(session: WebSocketSession): void {
407
- const user = session.user as any;
408
-
409
- if (!user) return;
410
-
411
- this._broadcast({
412
- message: {
413
- type: "$USER_JOINED",
414
- data: {
415
- connectionId: session.id,
416
- user,
417
- },
418
- },
419
- senderId: session.id,
420
- });
421
- }
422
-
423
310
  private async _getAuthorizedUser(
424
311
  token: string | undefined
425
312
  ): Promise<InferIOAuthorizeUser<InferIOAuthorize<this>> | null> {
@@ -461,10 +348,66 @@ export class IORoom<
461
348
 
462
349
  private _getEventConfig(
463
350
  message: EventMessage<string, any>
464
- ): InferEventConfig<TContext, TInput, TOutput>[keyof TInput] | null {
351
+ ):
352
+ | InferEventConfig<
353
+ TContext,
354
+ TInput,
355
+ TOutputBroadcast,
356
+ TOutputSelf,
357
+ TOutputSync
358
+ >[keyof TInput]
359
+ | null {
465
360
  return this._events[message.type as keyof TInput] ?? null;
466
361
  }
467
362
 
363
+ private _getEventInputs(
364
+ message: EventMessage<string, any>
365
+ ): TInput[string] {
366
+ const eventConfig = this._getEventConfig(message);
367
+
368
+ if (!eventConfig) return message.data;
369
+
370
+ /**
371
+ * !HACK
372
+ * @description If the config doesn't specify validation, we
373
+ * assume no input is to be expected, but pass through all values
374
+ * regardless. Leave this up to the implementer of the event.
375
+ * @author leedavidcs
376
+ * @date September 25, 2022
377
+ */
378
+ return eventConfig.input
379
+ ? (eventConfig.input.parse(message.data) as TInput[string])
380
+ : message.data;
381
+ }
382
+
383
+ private _getEventResolverObject(
384
+ message: EventMessage<string, any>
385
+ ): EventResolverObject<
386
+ TContext,
387
+ TInput[string],
388
+ TOutputBroadcast,
389
+ TOutputSelf,
390
+ TOutputSync
391
+ > {
392
+ const eventConfig = this._getEventConfig(message);
393
+
394
+ if (!eventConfig) return {};
395
+
396
+ const resolver:
397
+ | EventResolver<TContext, TInput[string], TOutputBroadcast>
398
+ | EventResolverObject<
399
+ TContext,
400
+ TInput[string],
401
+ TOutputBroadcast,
402
+ TOutputSelf,
403
+ TOutputSync
404
+ > = eventConfig.resolver;
405
+
406
+ return typeof resolver === "function"
407
+ ? { broadcast: resolver }
408
+ : { ...resolver };
409
+ }
410
+
468
411
  private async _initialize(): Promise<void> {
469
412
  this._logDebug(`${colors.blue(`Initializing room ${this.id}:`)}`);
470
413
 
@@ -504,6 +447,130 @@ export class IORoom<
504
447
  this._debug && console.log(...data);
505
448
  }
506
449
 
450
+ private _onClose(
451
+ session: WebSocketSession,
452
+ callback?: () => void
453
+ ): () => void {
454
+ return (): void => {
455
+ if (!this._uninitialize) return;
456
+
457
+ session.quit = true;
458
+
459
+ this._logDebug(
460
+ `${colors.blue(
461
+ `(Unregistering connection for room ${this.id}:`
462
+ )} ${session.id}`
463
+ );
464
+ this._sessions.delete(session.id);
465
+
466
+ this._platform.persistance
467
+ .deleteUser(this.id, session.id)
468
+ .finally(() => {
469
+ this._broadcast({
470
+ message: {
471
+ type: "$EXIT",
472
+ data: { sessionId: session.id },
473
+ },
474
+ senderId: session.id,
475
+ });
476
+
477
+ callback?.();
478
+
479
+ const size = this.getSize();
480
+
481
+ this._logDebug(
482
+ `${colors.blue(
483
+ `Unregistered connection for room ${this.id}:`
484
+ )} ${session.id}`
485
+ );
486
+ this._logDebug(
487
+ `${colors.blue(`Room ${this.id} size:`)} ${size}`
488
+ );
489
+
490
+ if (size) return;
491
+
492
+ this._uninitialize?.();
493
+ });
494
+ };
495
+ }
496
+
497
+ private _onMessage(
498
+ session: WebSocketSession
499
+ ): (event: AbstractMessageEvent) => void {
500
+ return (event: AbstractMessageEvent): void => {
501
+ const context: EventResolverContext<TContext> = {
502
+ context: this._context,
503
+ doc: this._doc,
504
+ room: this.id,
505
+ session,
506
+ sessions: this._sessions,
507
+ };
508
+
509
+ if (!this._uninitialize) return;
510
+
511
+ if (session.quit) {
512
+ session.webSocket.close(1011, "WebSocket broken.");
513
+
514
+ return;
515
+ }
516
+
517
+ const message = this._parseMessage(event);
518
+
519
+ if (!message) return;
520
+
521
+ const resolver = this._getEventResolverObject(message);
522
+
523
+ let inputs: TInput[string];
524
+
525
+ try {
526
+ inputs = this._getEventInputs(message);
527
+ } catch (error) {
528
+ session?.webSocket.handleError({
529
+ error,
530
+ message: "Invalid input",
531
+ session,
532
+ });
533
+
534
+ return;
535
+ }
536
+
537
+ Promise.all([
538
+ resolver.broadcast?.(inputs, context),
539
+ resolver.self?.(inputs, context),
540
+ resolver.sync?.(inputs, context),
541
+ ]).then(([broadcast, self, sync]) => {
542
+ if (broadcast) {
543
+ Object.entries(broadcast).forEach(([type, data]: any) => {
544
+ this._broadcast({
545
+ message: { data, type },
546
+ senderId: session.id,
547
+ });
548
+ });
549
+ }
550
+
551
+ if (self) {
552
+ Object.entries(self).forEach(([type, data]) => {
553
+ this._sendSelfMessage({ data, type }, session);
554
+ });
555
+ }
556
+
557
+ if (sync) {
558
+ this._platform.pubSub.publish(this.id, {
559
+ connectionId: session.id,
560
+ options: { type: "sync" },
561
+ room: this.id,
562
+ user: session.user,
563
+ ...message,
564
+ });
565
+
566
+ Object.entries(sync).forEach(([type, data]) => {
567
+ this._sendSelfMessage({ data, type }, session);
568
+ });
569
+ }
570
+ });
571
+ };
572
+ }
573
+
507
574
  private _parseMessage(message: {
508
575
  data: string | ArrayBuffer;
509
576
  }): EventMessage<string, any> | null {
@@ -520,47 +587,6 @@ export class IORoom<
520
587
  }
521
588
  }
522
589
 
523
- private async _resolveMessage(
524
- message: EventMessage<string, any>,
525
- context: EventResolverContext<TContext>
526
- ): Promise<TOutput | null> {
527
- const { session } = context;
528
-
529
- const eventConfig = this._getEventConfig(message);
530
-
531
- if (!eventConfig) return null;
532
-
533
- let input: TInput[string];
534
-
535
- /**
536
- * !HACK
537
- * @description If the config doesn't specify validation, we
538
- * assume no input is to be expected, but pass through all values
539
- * regardless. Leave this up to the implementer of the event.
540
- * @author leedavidcs
541
- * @date September 25, 2022
542
- */
543
- try {
544
- input = eventConfig.input
545
- ? (eventConfig.input.parse(message.data) as TInput[string])
546
- : message.data;
547
- } catch (error) {
548
- session?.webSocket.handleError({
549
- error,
550
- message: "Invalid input",
551
- session,
552
- });
553
-
554
- return null;
555
- }
556
-
557
- const output = await eventConfig.resolver(input, context);
558
-
559
- if (!output) return null;
560
-
561
- return output;
562
- }
563
-
564
590
  private _sendMessage(
565
591
  message: EventMessage<string, any>,
566
592
  sender: SendMessageSender | null,
@@ -638,13 +664,27 @@ export class IORoom<
638
664
 
639
665
  if (!senderId) return;
640
666
 
641
- this._resolveMessage(message, {
667
+ const context: EventResolverContext<TContext> = {
642
668
  context: this._context,
643
669
  doc: this._doc,
644
670
  room: this.id,
645
671
  session: null,
646
672
  sessions: this._sessions,
647
- }).then((output) => {
673
+ };
674
+
675
+ const resolver = this._getEventResolverObject(message).sync;
676
+
677
+ if (!resolver) return;
678
+
679
+ let inputs: TInput[string];
680
+
681
+ try {
682
+ inputs = this._getEventInputs(message);
683
+ } catch {
684
+ return;
685
+ }
686
+
687
+ Promise.resolve(resolver(inputs, context)).then((output) => {
648
688
  if (!output) return;
649
689
 
650
690
  Object.keys(output).forEach((type: string) => {