@pluv/io 0.21.1 → 0.23.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
@@ -5,9 +5,11 @@ import type {
5
5
  BaseIOEventRecord,
6
6
  EventMessage,
7
7
  IOAuthorize,
8
+ IOEventMessage,
8
9
  IOLike,
9
10
  Id,
10
11
  InferEventMessage,
12
+ InferEventsOutput,
11
13
  InferIOAuthorize,
12
14
  InferIOAuthorizeUser,
13
15
  InferIOInput,
@@ -16,12 +18,7 @@ import type {
16
18
  Maybe,
17
19
  } from "@pluv/types";
18
20
  import colors from "kleur";
19
- import type {
20
- AbstractPlatform,
21
- InferPlatformWebSocketSource,
22
- InferPlatformWebSocketType,
23
- InferRoomContextType,
24
- } from "./AbstractPlatform";
21
+ import type { AbstractPlatform, InferPlatformWebSocketSource, InferRoomContextType } from "./AbstractPlatform";
25
22
  import { AbstractCloseEvent, AbstractErrorEvent, AbstractMessageEvent, AbstractWebSocket } from "./AbstractWebSocket";
26
23
  import type { PluvRouter, PluvRouterEventConfig } from "./PluvRouter";
27
24
  import { authorize } from "./authorize";
@@ -29,7 +26,7 @@ import { PING_TIMEOUT_MS } from "./constants";
29
26
  import type {
30
27
  EventResolverContext,
31
28
  IORoomListenerEvent,
32
- SendMessageOptions,
29
+ IORoomMessageEvent,
33
30
  WebSocketSession,
34
31
  WebSocketType,
35
32
  } from "./types";
@@ -43,8 +40,14 @@ interface BroadcastParams<TIO extends IORoom<any, any, any, any>> {
43
40
  senderId?: string;
44
41
  }
45
42
 
46
- interface IORoomListeners<TPlatform extends AbstractPlatform> {
47
- onDestroy: (event: IORoomListenerEvent<TPlatform>) => void;
43
+ interface IORoomListeners<
44
+ TPlatform extends AbstractPlatform<any>,
45
+ TAuthorize extends IOAuthorize<any, any, InferRoomContextType<TPlatform>>,
46
+ TContext extends JsonObject,
47
+ TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>,
48
+ > {
49
+ onDestroy: (event: IORoomListenerEvent<TPlatform, TAuthorize, TContext, TEvents>) => void;
50
+ onMessage: (event: IORoomMessageEvent<TPlatform, TAuthorize, TContext, TEvents>) => void;
48
51
  }
49
52
 
50
53
  export type BroadcastProxy<TIO extends IORoom<any, any, any, any>> = (<TEvent extends keyof InferIOInput<TIO>>(
@@ -59,7 +62,7 @@ export type IORoomConfig<
59
62
  TAuthorize extends IOAuthorize<any, any, InferRoomContextType<TPlatform>> = BaseIOAuthorize,
60
63
  TContext extends JsonObject = {},
61
64
  TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
62
- > = Partial<IORoomListeners<TPlatform>> & {
65
+ > = Partial<IORoomListeners<TPlatform, TAuthorize, TContext, TEvents>> & {
63
66
  authorize?: TAuthorize;
64
67
  context: TContext & InferRoomContextType<TPlatform>;
65
68
  crdt?: { doc: (value: any) => AbstractCrdtDocFactory<any> };
@@ -84,20 +87,19 @@ export class IORoom<
84
87
  TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
85
88
  > implements IOLike<TAuthorize, TEvents>
86
89
  {
87
- private readonly _context: TContext & InferRoomContextType<TPlatform>;
88
- private readonly _debug: boolean;
89
- private readonly _docFactory: AbstractCrdtDocFactory<any>;
90
- private readonly _platform: TPlatform;
90
+ public readonly id: string;
91
+ public readonly _authorize: TAuthorize | null = null;
91
92
 
92
93
  private _doc: AbstractCrdtDoc<any>;
93
- private _listeners: IORoomListeners<TPlatform>;
94
- private _sessions = new Map<string, AbstractWebSocket>();
95
94
  private _uninitialize: (() => Promise<void>) | null = null;
96
95
 
97
- readonly _authorize: TAuthorize | null = null;
98
- readonly _router: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
99
-
100
- readonly id: string;
96
+ private readonly _context: TContext & InferRoomContextType<TPlatform>;
97
+ private readonly _debug: boolean;
98
+ private readonly _docFactory: AbstractCrdtDocFactory<any>;
99
+ private readonly _listeners: IORoomListeners<TPlatform, TAuthorize, TContext, TEvents>;
100
+ private readonly _platform: TPlatform;
101
+ private readonly _router: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
102
+ private readonly _sessions = new Map<string, AbstractWebSocket>();
101
103
 
102
104
  public get _events() {
103
105
  return this._router._events;
@@ -121,7 +123,7 @@ export class IORoom<
121
123
  }
122
124
 
123
125
  constructor(id: string, config: IORoomConfig<TPlatform, TAuthorize, TContext, TEvents>) {
124
- const { authorize, context, crdt = noop, debug, onDestroy, platform, router } = config;
126
+ const { authorize, context, crdt = noop, debug, onDestroy, onMessage, platform, router } = config;
125
127
 
126
128
  this._context = context;
127
129
  this._debug = debug;
@@ -134,6 +136,7 @@ export class IORoom<
134
136
 
135
137
  this._listeners = {
136
138
  onDestroy: (event) => onDestroy?.(event),
139
+ onMessage: (event) => onMessage?.(event),
137
140
  };
138
141
 
139
142
  if (authorize) this._authorize = authorize;
@@ -410,10 +413,29 @@ export class IORoom<
410
413
  const pubSubId = await this._platform.pubSub.subscribe(
411
414
  this.id,
412
415
  async ({ options = {}, ...message }): Promise<void> => {
413
- const sessionId = message.connectionId;
414
- const user = message.user;
415
-
416
- await this._sendMessage(message, { sessionId, user }, options);
416
+ const sender: SendMessageSender = {
417
+ sessionId: message.connectionId,
418
+ user: message.user,
419
+ };
420
+
421
+ switch (options.type) {
422
+ case "self": {
423
+ await this._sendSelfMessage(message, sender);
424
+
425
+ return;
426
+ }
427
+ case "sync": {
428
+ await this._sendSyncMessage(message, sender);
429
+
430
+ return;
431
+ }
432
+ case "broadcast":
433
+ default: {
434
+ const sessionIds = options.sessionIds;
435
+
436
+ await this._sendBroadcastMessage(message, sender, sessionIds);
437
+ }
438
+ }
417
439
  },
418
440
  );
419
441
 
@@ -426,7 +448,7 @@ export class IORoom<
426
448
  this._doc = this._docFactory.getEmpty();
427
449
 
428
450
  this._listeners.onDestroy({
429
- ...this._context,
451
+ context: this._context,
430
452
  encodedState,
431
453
  room: this.id,
432
454
  });
@@ -583,29 +605,18 @@ export class IORoom<
583
605
  }
584
606
  }
585
607
 
586
- private async _sendMessage(
587
- message: EventMessage<string, any>,
588
- sender: SendMessageSender | null,
589
- options: SendMessageOptions = {},
590
- ): Promise<void> {
591
- switch (options.type) {
592
- case "self": {
593
- await this._sendSelfMessage(message, sender);
594
-
595
- return;
596
- }
597
- case "sync": {
598
- await this._sendSyncMessage(message, sender);
608
+ private async _sendMessage(pluvWs: AbstractWebSocket<any>, message: IOEventMessage<any>): Promise<void> {
609
+ const { data, type } = message;
599
610
 
600
- return;
601
- }
602
- case "broadcast":
603
- default: {
604
- const sessionIds = options.sessionIds;
611
+ this._listeners.onMessage({
612
+ context: this._context,
613
+ encodedState: this._doc.getEncodedState(),
614
+ message: { data, type } as InferEventMessage<InferEventsOutput<TEvents>, keyof InferEventsOutput<TEvents>>,
615
+ room: this.id,
616
+ user: message.user,
617
+ });
605
618
 
606
- await this._sendBroadcastMessage(message, sender, sessionIds);
607
- }
608
- }
619
+ await Promise.resolve(pluvWs.sendMessage(message));
609
620
  }
610
621
 
611
622
  private async _sendBroadcastMessage(
@@ -628,7 +639,7 @@ export class IORoom<
628
639
  const session = await pluvWs.getSession();
629
640
  const user = session.user ?? null;
630
641
 
631
- pluvWs.sendMessage({ connectionId, data, room, type, user });
642
+ this._sendMessage(pluvWs, { connectionId, data, room, type, user });
632
643
  });
633
644
 
634
645
  await Promise.all(promises);
@@ -650,7 +661,7 @@ export class IORoom<
650
661
  const session = await pluvWs.getSession();
651
662
  const user = session.user ?? null;
652
663
 
653
- pluvWs.sendMessage({
664
+ this._sendMessage(pluvWs, {
654
665
  connectionId: senderId,
655
666
  room: this.id,
656
667
  user,
package/src/PluvIO.ts CHANGED
@@ -1,14 +1,6 @@
1
1
  import type { AbstractCrdtDocFactory } from "@pluv/crdt";
2
2
  import { noop } from "@pluv/crdt";
3
- import type {
4
- BaseIOAuthorize,
5
- IOAuthorize,
6
- IOLike,
7
- InferIOAuthorize,
8
- InferIOAuthorizeUser,
9
- JsonObject,
10
- Maybe,
11
- } from "@pluv/types";
3
+ import type { BaseIOAuthorize, IOAuthorize, InferIOAuthorizeUser, JsonObject } from "@pluv/types";
12
4
  import type { AbstractPlatform, InferPlatformContextType, WebSocketRegistrationMode } from "./AbstractPlatform";
13
5
  import { PluvProcedure } from "./PluvProcedure";
14
6
  import type { MergedRouter, PluvRouterEventConfig } from "./PluvRouter";
@@ -16,7 +8,6 @@ import { PluvRouter } from "./PluvRouter";
16
8
  import { PluvServer } from "./PluvServer";
17
9
  import type { JWTEncodeParams } from "./authorize";
18
10
  import { authorize } from "./authorize";
19
- import { PING_TIMEOUT_MS } from "./constants";
20
11
  import type { GetInitialStorageFn, PluvIOListeners } from "./types";
21
12
  import { __PLUV_VERSION } from "./version";
22
13
 
@@ -24,180 +15,43 @@ export type PluvIOConfig<
24
15
  TPlatform extends AbstractPlatform<any>,
25
16
  TAuthorize extends IOAuthorize<any, any, InferPlatformContextType<TPlatform>>,
26
17
  TContext extends JsonObject,
27
- > = Partial<PluvIOListeners<TPlatform>> & {
18
+ > = {
28
19
  authorize?: TAuthorize;
29
20
  context?: TContext;
30
21
  crdt?: { doc: (value: any) => AbstractCrdtDocFactory<any> };
31
22
  debug?: boolean;
32
- getInitialStorage?: GetInitialStorageFn<TPlatform>;
33
23
  platform: TPlatform;
34
24
  };
35
25
 
36
- export interface ServerConfig<
26
+ export type ServerConfig<
37
27
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
38
28
  TAuthorize extends IOAuthorize<any, any, InferPlatformContextType<TPlatform>> = BaseIOAuthorize,
39
29
  TContext extends JsonObject = {},
40
30
  TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
41
- > {
31
+ > = Partial<PluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>> & {
32
+ getInitialStorage?: GetInitialStorageFn<TPlatform>;
42
33
  router?: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
43
- }
34
+ };
44
35
 
45
36
  export class PluvIO<
46
37
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
47
38
  TAuthorize extends IOAuthorize<any, any, InferPlatformContextType<TPlatform>> = BaseIOAuthorize,
48
39
  TContext extends JsonObject = {},
49
- > implements IOLike<TAuthorize, {}>
50
- {
51
- private _router: PluvRouter<TPlatform, TAuthorize, TContext, {}> = new PluvRouter({
52
- $GET_OTHERS: this.procedure.sync((data, { room, session, sessions }) => {
53
- const currentTime = new Date().getTime();
54
-
55
- const others = sessions
56
- .filter((wsSession) => {
57
- if (wsSession.id === session?.id) return false;
58
- if (wsSession.quit) return false;
59
- if (currentTime - wsSession.timers.ping > PING_TIMEOUT_MS) return false;
60
-
61
- return true;
62
- })
63
- .reduce<{
64
- [connectionId: string]: {
65
- connectionId: string;
66
- room: string | null;
67
- user: JsonObject;
68
- };
69
- }>(
70
- (acc, { id, presence, user }) => ({
71
- ...acc,
72
- [id]: {
73
- connectionId: id,
74
- presence,
75
- room,
76
- user,
77
- },
78
- }),
79
- {},
80
- );
81
-
82
- return { $OTHERS_RECEIVED: { others } };
83
- }),
84
- $INITIALIZE_SESSION: this.procedure
85
- .broadcast((data, { session }) => {
86
- const presence = (data as any)?.presence;
87
-
88
- if (!session) return {};
89
-
90
- session.presence = presence;
91
-
92
- return {
93
- $USER_JOINED: {
94
- connectionId: session.id,
95
- user: session.user,
96
- presence,
97
- },
98
- };
99
- })
100
- .self(async (data, { context, doc, room }) => {
101
- const oldState = await this._platform.persistance.getStorageState(room);
102
- const update = (data as any)?.update as Maybe<string>;
103
-
104
- const updates: readonly Maybe<string>[] = [
105
- oldState
106
- ? null
107
- : await this._getInitialStorage?.({
108
- ...context,
109
- room,
110
- }),
111
- update,
112
- ];
113
-
114
- if (updates.some((_update) => !!_update)) {
115
- const encodedState = doc.batchApplyEncodedState(updates).getEncodedState();
116
-
117
- await this._platform.persistance.setStorageState(room, encodedState);
118
-
119
- this._listeners.onStorageUpdated({ ...context, encodedState, room });
120
- }
121
-
122
- const state = (await this._platform.persistance.getStorageState(room)) ?? doc.getEncodedState();
123
-
124
- return { $STORAGE_RECEIVED: { state } };
125
- }),
126
- $PING: this.procedure.self((data, { session }) => {
127
- if (!session) return {};
128
-
129
- const currentTime = new Date().getTime();
130
-
131
- session.timers.ping = currentTime;
132
-
133
- return { $PONG: {} };
134
- }),
135
- $UPDATE_PRESENCE: this.procedure.broadcast((data, { session }) => {
136
- const presence = (data as any)?.presence;
137
-
138
- if (!session) return {};
139
-
140
- const updated = Object.assign(Object.create(null), session.presence, presence);
141
-
142
- session.webSocket.presence = updated;
143
-
144
- return { $PRESENCE_UPDATED: { presence: updated } };
145
- }),
146
- $UPDATE_STORAGE: this.procedure.broadcast((data, { context, doc, room }) => {
147
- const origin = (data as any)?.origin as Maybe<string>;
148
- const update: string | null = (data as any)?.update ?? null;
149
-
150
- if (origin === "$INITIALIZED" && Object.keys(doc.toJson()).length) {
151
- return {};
152
- }
153
-
154
- const updated = update === null ? doc : doc.applyEncodedState({ update });
155
- const encodedState = updated.getEncodedState();
156
-
157
- this._platform.persistance.setStorageState(room, encodedState).then(() => {
158
- this._listeners.onStorageUpdated({
159
- ...context,
160
- encodedState,
161
- room,
162
- });
163
- });
164
-
165
- return { $STORAGE_UPDATED: { state: encodedState } };
166
- }),
167
- });
168
-
169
- readonly _authorize: TAuthorize | null = null;
170
- readonly _context: TContext = {} as TContext;
171
- readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any> };
172
- readonly _debug: boolean;
173
- readonly _getInitialStorage: GetInitialStorageFn<TPlatform> | null = null;
174
- readonly _listeners: PluvIOListeners<TPlatform>;
175
- readonly _platform: TPlatform;
176
- readonly _version: string = __PLUV_VERSION as any;
177
-
178
- public get _events() {
179
- return this._router._events;
180
- }
40
+ > {
41
+ public readonly version: string = __PLUV_VERSION as any;
42
+ public readonly _authorize: TAuthorize | null = null;
181
43
 
182
- public get _registrationMode(): WebSocketRegistrationMode {
183
- return this._platform._registrationMode;
184
- }
44
+ private readonly _context: TContext = {} as TContext;
45
+ private readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any> };
46
+ private readonly _debug: boolean;
47
+ private readonly _platform: TPlatform;
185
48
 
186
49
  public get procedure(): PluvProcedure<TPlatform, TAuthorize, TContext, {}, {}> {
187
50
  return new PluvProcedure();
188
51
  }
189
52
 
190
53
  constructor(options: PluvIOConfig<TPlatform, TAuthorize, TContext>) {
191
- const {
192
- authorize,
193
- context,
194
- crdt = noop,
195
- debug = false,
196
- getInitialStorage,
197
- onRoomDeleted,
198
- onStorageUpdated,
199
- platform,
200
- } = options;
54
+ const { authorize, context, crdt = noop, debug = false, platform } = options;
201
55
 
202
56
  this._crdt = crdt;
203
57
  this._debug = debug;
@@ -205,17 +59,9 @@ export class PluvIO<
205
59
 
206
60
  if (authorize) this._authorize = authorize;
207
61
  if (context) this._context = context;
208
- if (getInitialStorage) this._getInitialStorage = getInitialStorage;
209
-
210
- this._listeners = {
211
- onRoomDeleted: (event) => onRoomDeleted?.(event),
212
- onStorageUpdated: (event) => onStorageUpdated?.(event),
213
- };
214
62
  }
215
63
 
216
- public async createToken(
217
- params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>, TPlatform>,
218
- ): Promise<string> {
64
+ public async createToken(params: JWTEncodeParams<InferIOAuthorizeUser<TAuthorize>, TPlatform>): Promise<string> {
219
65
  if (!this._authorize) throw new Error("IO does not specify authorize during initialization.");
220
66
 
221
67
  const ioAuthorize =
@@ -244,21 +90,13 @@ export class PluvIO<
244
90
  public server<TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>>(
245
91
  config: ServerConfig<TPlatform, TAuthorize, TContext, TEvents> = {},
246
92
  ): PluvServer<TPlatform, TAuthorize, TContext, TEvents> {
247
- const router = (config.router ? this.mergeRouters(this._router, config.router) : this._router) as PluvRouter<
248
- TPlatform,
249
- TAuthorize,
250
- TContext,
251
- TEvents
252
- >;
253
-
254
93
  return new PluvServer<TPlatform, TAuthorize, TContext, TEvents>({
255
- ...this._listeners,
94
+ ...config,
256
95
  authorize: this._authorize ?? undefined,
257
96
  context: this._context,
258
97
  crdt: this._crdt,
259
98
  debug: this._debug,
260
99
  platform: this._platform,
261
- router,
262
100
  });
263
101
  }
264
102
  }
package/src/PluvRouter.ts CHANGED
@@ -6,9 +6,7 @@ export type PluvRouterEventConfig<
6
6
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
7
7
  TAuthorize extends IOAuthorize<any, any, InferRoomContextType<TPlatform>> = BaseIOAuthorize,
8
8
  TContext extends JsonObject = {},
9
- > = {
10
- [P: string]: Pick<PluvProcedure<TPlatform, TAuthorize, TContext, any, any>, "config">;
11
- };
9
+ > = { [P: string]: Pick<PluvProcedure<TPlatform, TAuthorize, TContext, any, any>, "config"> };
12
10
 
13
11
  export type MergedRouter<
14
12
  TRouters extends PluvRouter<TPlatform, TAuthorize, TContext, any>[] = [],