@pluv/io 0.3.8 → 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,
@@ -110,7 +139,9 @@ export class IORoom<
110
139
  this.id = id;
111
140
 
112
141
  this._listeners = {
113
- onDestroy: (encodedState) => onDestroy?.(encodedState),
142
+ onDestroy: (encodedState) => {
143
+ onDestroy?.(encodedState);
144
+ },
114
145
  };
115
146
 
116
147
  if (authorize) this._authorize = authorize;
@@ -208,120 +239,14 @@ export class IORoom<
208
239
  user ?? {}
209
240
  );
210
241
 
211
- this._emitRegistered(session);
212
- this._emitStorageReceived(session);
213
- this._emitUserJoined(session);
214
-
215
- const context: EventResolverContext<TContext> = {
216
- context: this._context,
217
- doc: this._doc,
218
- room: this.id,
219
- session,
220
- sessions: this._sessions,
221
- };
222
-
223
- pluvWs.addEventListener("message", (event) => {
224
- if (!this._uninitialize) return;
225
-
226
- if (session.quit) {
227
- session.webSocket.close(1011, "WebSocket broken.");
228
-
229
- return;
230
- }
231
-
232
- const message = this._parseMessage(event);
233
-
234
- if (!message) return;
235
-
236
- const eventConfig = this._getEventConfig(message);
237
-
238
- if (!eventConfig) return;
239
-
240
- this._resolveMessage(message, context).then((output) => {
241
- if (!output) return;
242
-
243
- const eventType = eventConfig.options?.type ?? "broadcast";
242
+ const onClose = this._onClose(session, uninitializeWs).bind(this);
243
+ const onMessage = this._onMessage(session).bind(this);
244
244
 
245
- switch (eventType) {
246
- case "self": {
247
- Object.entries(output).forEach(([type, data]) => {
248
- this._sendSelfMessage({ data, type }, session);
249
- });
250
-
251
- return;
252
- }
253
- case "sync": {
254
- this._platform.pubSub.publish(this.id, {
255
- connectionId: session.id,
256
- options: eventConfig.options,
257
- room: this.id,
258
- user: session.user,
259
- ...message,
260
- });
245
+ pluvWs.addEventListener("close", onClose);
246
+ pluvWs.addEventListener("error", onClose);
247
+ pluvWs.addEventListener("message", onMessage);
261
248
 
262
- Object.entries(output).forEach(([type, data]) => {
263
- this._sendSelfMessage({ data, type }, session);
264
- });
265
-
266
- return;
267
- }
268
- case "broadcast":
269
- default: {
270
- Object.entries(output).forEach(([type, data]: any) => {
271
- this._broadcast({
272
- message: { data, type },
273
- senderId: session.id,
274
- });
275
- });
276
- }
277
- }
278
- });
279
- });
280
-
281
- const closeHandler = () => {
282
- if (!this._uninitialize) return;
283
-
284
- session.quit = true;
285
-
286
- this._logDebug(
287
- `${colors.blue(
288
- `(Unregistering connection for room ${this.id}:`
289
- )} ${sessionId}`
290
- );
291
- this._sessions.delete(session.id);
292
-
293
- this._platform.persistance
294
- .deleteUser(this.id, session.id)
295
- .finally(() => {
296
- this._broadcast({
297
- message: {
298
- type: "$EXIT",
299
- data: { sessionId: session.id },
300
- },
301
- senderId: session.id,
302
- });
303
-
304
- uninitializeWs();
305
-
306
- const size = this.getSize();
307
-
308
- this._logDebug(
309
- `${colors.blue(
310
- `Unregistered connection for room ${this.id}:`
311
- )} ${sessionId}`
312
- );
313
- this._logDebug(
314
- `${colors.blue(`Room ${this.id} size:`)} ${size}`
315
- );
316
-
317
- if (size) return;
318
-
319
- this._uninitialize?.();
320
- });
321
- };
322
-
323
- pluvWs.addEventListener("close", closeHandler);
324
- pluvWs.addEventListener("error", closeHandler);
249
+ this._emitRegistered(session);
325
250
 
326
251
  this._logDebug(
327
252
  `${colors.blue(
@@ -382,42 +307,6 @@ export class IORoom<
382
307
  );
383
308
  }
384
309
 
385
- private _emitStorageReceived(session: WebSocketSession): void {
386
- this._platform.persistance
387
- .getStorageState(this.id)
388
- .then(async (_state) => {
389
- const state =
390
- _state ??
391
- (await this._initialStorage?.()) ??
392
- this._doc.encodeStateAsUpdate();
393
-
394
- this._sendSelfMessage(
395
- {
396
- type: "$STORAGE_RECEIVED",
397
- data: { state },
398
- },
399
- session
400
- );
401
- });
402
- }
403
-
404
- private _emitUserJoined(session: WebSocketSession): void {
405
- const user = session.user as any;
406
-
407
- if (!user) return;
408
-
409
- this._broadcast({
410
- message: {
411
- type: "$USER_JOINED",
412
- data: {
413
- connectionId: session.id,
414
- user,
415
- },
416
- },
417
- senderId: session.id,
418
- });
419
- }
420
-
421
310
  private async _getAuthorizedUser(
422
311
  token: string | undefined
423
312
  ): Promise<InferIOAuthorizeUser<InferIOAuthorize<this>> | null> {
@@ -459,10 +348,66 @@ export class IORoom<
459
348
 
460
349
  private _getEventConfig(
461
350
  message: EventMessage<string, any>
462
- ): 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 {
463
360
  return this._events[message.type as keyof TInput] ?? null;
464
361
  }
465
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
+
466
411
  private async _initialize(): Promise<void> {
467
412
  this._logDebug(`${colors.blue(`Initializing room ${this.id}:`)}`);
468
413
 
@@ -493,6 +438,8 @@ export class IORoom<
493
438
  this._doc = doc();
494
439
 
495
440
  this._listeners.onDestroy(encodedState);
441
+
442
+ this._uninitialize = null;
496
443
  };
497
444
  }
498
445
 
@@ -500,6 +447,130 @@ export class IORoom<
500
447
  this._debug && console.log(...data);
501
448
  }
502
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
+
503
574
  private _parseMessage(message: {
504
575
  data: string | ArrayBuffer;
505
576
  }): EventMessage<string, any> | null {
@@ -516,47 +587,6 @@ export class IORoom<
516
587
  }
517
588
  }
518
589
 
519
- private async _resolveMessage(
520
- message: EventMessage<string, any>,
521
- context: EventResolverContext<TContext>
522
- ): Promise<TOutput | null> {
523
- const { session } = context;
524
-
525
- const eventConfig = this._getEventConfig(message);
526
-
527
- if (!eventConfig) return null;
528
-
529
- let input: TInput[string];
530
-
531
- /**
532
- * !HACK
533
- * @description If the config doesn't specify validation, we
534
- * assume no input is to be expected, but pass through all values
535
- * regardless. Leave this up to the implementer of the event.
536
- * @author leedavidcs
537
- * @date September 25, 2022
538
- */
539
- try {
540
- input = eventConfig.input
541
- ? (eventConfig.input.parse(message.data) as TInput[string])
542
- : message.data;
543
- } catch (error) {
544
- session?.webSocket.handleError({
545
- error,
546
- message: "Invalid input",
547
- session,
548
- });
549
-
550
- return null;
551
- }
552
-
553
- const output = await eventConfig.resolver(input, context);
554
-
555
- if (!output) return null;
556
-
557
- return output;
558
- }
559
-
560
590
  private _sendMessage(
561
591
  message: EventMessage<string, any>,
562
592
  sender: SendMessageSender | null,
@@ -634,13 +664,27 @@ export class IORoom<
634
664
 
635
665
  if (!senderId) return;
636
666
 
637
- this._resolveMessage(message, {
667
+ const context: EventResolverContext<TContext> = {
638
668
  context: this._context,
639
669
  doc: this._doc,
640
670
  room: this.id,
641
671
  session: null,
642
672
  sessions: this._sessions,
643
- }).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) => {
644
688
  if (!output) return;
645
689
 
646
690
  Object.keys(output).forEach((type: string) => {