@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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +29 -24
- package/dist/index.js +195 -173
- package/dist/index.mjs +195 -173
- package/package.json +9 -9
- package/src/IORoom.ts +238 -198
- package/src/PluvIO.ts +168 -53
- package/src/types.ts +38 -7
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
|
-
|
|
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<
|
|
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
|
-
|
|
74
|
-
|
|
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<
|
|
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<
|
|
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.
|
|
214
|
-
this.
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
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
|
-
}
|
|
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) => {
|