@pluv/io 0.22.0 → 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,187 +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
- const prevState = session.webSocket.state;
131
-
132
- session.webSocket.state = {
133
- ...prevState,
134
- timers: {
135
- ...prevState.timers,
136
- ping: currentTime,
137
- },
138
- };
139
-
140
- return { $PONG: {} };
141
- }),
142
- $UPDATE_PRESENCE: this.procedure.broadcast((data, { session }) => {
143
- const presence = (data as any)?.presence;
144
-
145
- if (!session) return {};
146
-
147
- const updated = Object.assign(Object.create(null), session.presence, presence);
148
-
149
- session.webSocket.presence = updated;
150
-
151
- return { $PRESENCE_UPDATED: { presence: updated } };
152
- }),
153
- $UPDATE_STORAGE: this.procedure.broadcast((data, { context, doc, room }) => {
154
- const origin = (data as any)?.origin as Maybe<string>;
155
- const update: string | null = (data as any)?.update ?? null;
156
-
157
- if (origin === "$INITIALIZED" && Object.keys(doc.toJson()).length) {
158
- return {};
159
- }
160
-
161
- const updated = update === null ? doc : doc.applyEncodedState({ update });
162
- const encodedState = updated.getEncodedState();
163
-
164
- this._platform.persistance.setStorageState(room, encodedState).then(() => {
165
- this._listeners.onStorageUpdated({
166
- ...context,
167
- encodedState,
168
- room,
169
- });
170
- });
171
-
172
- return { $STORAGE_UPDATED: { state: encodedState } };
173
- }),
174
- });
175
-
176
- readonly _authorize: TAuthorize | null = null;
177
- readonly _context: TContext = {} as TContext;
178
- readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any> };
179
- readonly _debug: boolean;
180
- readonly _getInitialStorage: GetInitialStorageFn<TPlatform> | null = null;
181
- readonly _listeners: PluvIOListeners<TPlatform>;
182
- readonly _platform: TPlatform;
183
- readonly _version: string = __PLUV_VERSION as any;
184
-
185
- public get _events() {
186
- return this._router._events;
187
- }
40
+ > {
41
+ public readonly version: string = __PLUV_VERSION as any;
42
+ public readonly _authorize: TAuthorize | null = null;
188
43
 
189
- public get _registrationMode(): WebSocketRegistrationMode {
190
- return this._platform._registrationMode;
191
- }
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;
192
48
 
193
49
  public get procedure(): PluvProcedure<TPlatform, TAuthorize, TContext, {}, {}> {
194
50
  return new PluvProcedure();
195
51
  }
196
52
 
197
53
  constructor(options: PluvIOConfig<TPlatform, TAuthorize, TContext>) {
198
- const {
199
- authorize,
200
- context,
201
- crdt = noop,
202
- debug = false,
203
- getInitialStorage,
204
- onRoomDeleted,
205
- onStorageUpdated,
206
- platform,
207
- } = options;
54
+ const { authorize, context, crdt = noop, debug = false, platform } = options;
208
55
 
209
56
  this._crdt = crdt;
210
57
  this._debug = debug;
@@ -212,17 +59,9 @@ export class PluvIO<
212
59
 
213
60
  if (authorize) this._authorize = authorize;
214
61
  if (context) this._context = context;
215
- if (getInitialStorage) this._getInitialStorage = getInitialStorage;
216
-
217
- this._listeners = {
218
- onRoomDeleted: (event) => onRoomDeleted?.(event),
219
- onStorageUpdated: (event) => onStorageUpdated?.(event),
220
- };
221
62
  }
222
63
 
223
- public async createToken(
224
- params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>, TPlatform>,
225
- ): Promise<string> {
64
+ public async createToken(params: JWTEncodeParams<InferIOAuthorizeUser<TAuthorize>, TPlatform>): Promise<string> {
226
65
  if (!this._authorize) throw new Error("IO does not specify authorize during initialization.");
227
66
 
228
67
  const ioAuthorize =
@@ -251,21 +90,13 @@ export class PluvIO<
251
90
  public server<TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>>(
252
91
  config: ServerConfig<TPlatform, TAuthorize, TContext, TEvents> = {},
253
92
  ): PluvServer<TPlatform, TAuthorize, TContext, TEvents> {
254
- const router = (config.router ? this.mergeRouters(this._router, config.router) : this._router) as PluvRouter<
255
- TPlatform,
256
- TAuthorize,
257
- TContext,
258
- TEvents
259
- >;
260
-
261
93
  return new PluvServer<TPlatform, TAuthorize, TContext, TEvents>({
262
- ...this._listeners,
94
+ ...config,
263
95
  authorize: this._authorize ?? undefined,
264
96
  context: this._context,
265
97
  crdt: this._crdt,
266
98
  debug: this._debug,
267
99
  platform: this._platform,
268
- router,
269
100
  });
270
101
  }
271
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>[] = [],