@pluv/client 0.26.0 → 0.28.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/PluvClient.ts CHANGED
@@ -1,45 +1,98 @@
1
- import type { CrdtType } from "@pluv/crdt";
2
- import type { IOLike, JsonObject } from "@pluv/types";
3
- import type { AuthEndpoint, PluvRoomOptions, RoomConfig, RoomEndpoints, WsEndpoint } from "./PluvRoom";
1
+ import type { AbstractCrdtDocFactory, CrdtType } from "@pluv/crdt";
2
+ import type { InputZodLike, IOLike, JsonObject } from "@pluv/types";
3
+ import { PluvProcedure } from "./PluvProcedure";
4
+ import type { AuthEndpoint, PluvRoomAddon, PluvRoomDebug, RoomConfig, RoomEndpoints, WsEndpoint } from "./PluvRoom";
4
5
  import { PluvRoom } from "./PluvRoom";
6
+ import type { PluvRouterEventConfig } from "./PluvRouter";
7
+ import { PluvRouter } from "./PluvRouter";
8
+ import type { identity } from "./utils";
5
9
 
6
- export type PluvClientOptions<TIO extends IOLike, TMetadata extends JsonObject> = RoomEndpoints<TIO, TMetadata> & {
10
+ export type InferCallback<TIO extends IOLike> = (i: typeof identity) => {
11
+ io: (io: TIO) => TIO;
12
+ };
13
+
14
+ export type PluvClientOptions<
15
+ TIO extends IOLike,
16
+ TPresence extends JsonObject,
17
+ TStorage extends Record<string, CrdtType<any, any>>,
18
+ TMetadata extends JsonObject,
19
+ > = RoomEndpoints<TIO, TMetadata> & {
7
20
  debug?: boolean;
21
+ infer: InferCallback<TIO>;
22
+ initialStorage?: AbstractCrdtDocFactory<TStorage>;
23
+ metadata?: InputZodLike<TMetadata>;
24
+ presence?: InputZodLike<TPresence>;
8
25
  };
9
26
 
10
- export class PluvClient<TIO extends IOLike = IOLike, TMetadata extends JsonObject = {}> {
27
+ export type CreateRoomOptions<
28
+ TIO extends IOLike,
29
+ TPresence extends JsonObject,
30
+ TStorage extends Record<string, CrdtType<any, any>>,
31
+ TMetadata extends JsonObject,
32
+ TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
33
+ > = {
34
+ addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
35
+ debug?: boolean | PluvRoomDebug<TIO>;
36
+ initialPresence?: TPresence;
37
+ initialStorage?: AbstractCrdtDocFactory<TStorage>;
38
+ onAuthorizationFail?: (error: Error) => void;
39
+ router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
40
+ } & (keyof TMetadata extends never ? { metadata?: undefined } : { metadata: TMetadata });
41
+
42
+ export class PluvClient<
43
+ TIO extends IOLike = IOLike,
44
+ TPresence extends JsonObject = {},
45
+ TStorage extends Record<string, CrdtType<any, any>> = {},
46
+ TMetadata extends JsonObject = {},
47
+ > {
11
48
  private _authEndpoint: AuthEndpoint<TMetadata> | undefined;
12
49
  private _debug: boolean;
50
+ private _initialStorage?: AbstractCrdtDocFactory<TStorage>;
51
+ private _presence?: InputZodLike<TPresence>;
13
52
  private _wsEndpoint: WsEndpoint<TMetadata> | undefined;
14
53
 
15
- private _rooms = new Map<string, PluvRoom<TIO, TMetadata, any, any>>();
54
+ private _rooms = new Map<string, PluvRoom<TIO, TMetadata, TPresence, TStorage, any>>();
16
55
 
17
- constructor(options: PluvClientOptions<TIO, TMetadata>) {
18
- const { authEndpoint, debug = false, wsEndpoint } = options;
56
+ public get _defs() {
57
+ return {
58
+ initialStorage: this._initialStorage,
59
+ };
60
+ }
61
+
62
+ public get procedure(): PluvProcedure<TIO, {}, {}, TPresence, TStorage> {
63
+ return new PluvProcedure<TIO, {}, {}, TPresence, TStorage>();
64
+ }
65
+
66
+ constructor(options: PluvClientOptions<TIO, TPresence, TStorage, TMetadata>) {
67
+ const { authEndpoint, debug = false, initialStorage, presence, wsEndpoint } = options;
19
68
 
20
69
  this._authEndpoint = authEndpoint as AuthEndpoint<TMetadata>;
21
70
  this._debug = debug;
71
+ this._initialStorage = initialStorage;
72
+ this._presence = presence;
22
73
  this._wsEndpoint = wsEndpoint;
23
74
  }
24
75
 
25
- public createRoom = <TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}>(
76
+ public createRoom = <TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}>(
26
77
  room: string,
27
- options: PluvRoomOptions<TIO, TMetadata, TPresence, TStorage>,
78
+ options: CreateRoomOptions<TIO, TPresence, TStorage, TMetadata, TEvents>,
28
79
  ): PluvRoom<TIO, TMetadata, TPresence, TStorage> => {
29
- const oldRoom = this.getRoom<TPresence, TStorage>(room);
80
+ const oldRoom = this.getRoom(room);
30
81
 
31
82
  if (oldRoom) return oldRoom;
32
83
 
33
- const newRoom = new PluvRoom<TIO, TMetadata, TPresence, TStorage>(room, {
84
+ const newRoom = new PluvRoom<TIO, TMetadata, TPresence, TStorage, TEvents>(room, {
34
85
  addons: options.addons,
35
86
  authEndpoint: this._authEndpoint,
36
87
  debug: options.debug,
37
88
  initialPresence: options.initialPresence,
38
- initialStorage: options.initialStorage,
89
+ initialStorage: this._initialStorage,
39
90
  metadata: options.metadata,
40
91
  onAuthorizationFail: options.onAuthorizationFail,
92
+ presence: this._presence,
93
+ router: options.router,
41
94
  wsEndpoint: this._wsEndpoint,
42
- } as RoomConfig<TIO, TMetadata, TPresence, TStorage>);
95
+ } as RoomConfig<TIO, TMetadata, TPresence, TStorage, TEvents>);
43
96
 
44
97
  this._rooms.set(room, newRoom);
45
98
 
@@ -50,7 +103,7 @@ export class PluvClient<TIO extends IOLike = IOLike, TMetadata extends JsonObjec
50
103
 
51
104
  public enter = async (
52
105
  room: string | PluvRoom<TIO, any, any, any>,
53
- ): Promise<PluvRoom<TIO, TMetadata, JsonObject, any>> => {
106
+ ): Promise<PluvRoom<TIO, TMetadata, TPresence, TStorage, any>> => {
54
107
  const toEnter = typeof room === "string" ? this.getRoom(room) : room;
55
108
 
56
109
  if (!toEnter) throw new Error(`Could not find room: ${room}.`);
@@ -64,15 +117,13 @@ export class PluvClient<TIO extends IOLike = IOLike, TMetadata extends JsonObjec
64
117
  return toEnter;
65
118
  };
66
119
 
67
- public getRoom = <TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}>(
68
- room: string,
69
- ): PluvRoom<TIO, TMetadata, TPresence, TStorage> | null => {
70
- const found = this._rooms.get(room) as PluvRoom<TIO, TMetadata, TPresence, TStorage> | undefined;
120
+ public getRoom = (room: string): PluvRoom<TIO, TMetadata, TPresence, TStorage, any> | null => {
121
+ const found = this._rooms.get(room) as PluvRoom<TIO, TMetadata, TPresence, TStorage, any> | undefined;
71
122
 
72
123
  return found ?? null;
73
124
  };
74
125
 
75
- public getRooms = (): readonly PluvRoom<TIO, TMetadata, JsonObject, any>[] => {
126
+ public getRooms = (): readonly PluvRoom<TIO, TMetadata, TPresence, TStorage, any>[] => {
76
127
  return Array.from(this._rooms.values());
77
128
  };
78
129
 
@@ -88,6 +139,12 @@ export class PluvClient<TIO extends IOLike = IOLike, TMetadata extends JsonObjec
88
139
  this._logDebug(`Left and deleted room: ${room}`);
89
140
  };
90
141
 
142
+ public router<TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}>(
143
+ events: TEvents,
144
+ ): PluvRouter<TIO, TPresence, TStorage, TEvents> {
145
+ return new PluvRouter<TIO, TPresence, TStorage, TEvents>(events);
146
+ }
147
+
91
148
  private _logDebug(...data: any[]): void {
92
149
  // eslint-disable-next-line turbo/no-undeclared-env-vars
93
150
  if (process?.env?.NODE_ENV === "production") return;
@@ -0,0 +1,81 @@
1
+ import type { CrdtType } from "@pluv/crdt";
2
+ import type { EventRecord, IOLike, InputZodLike, JsonObject, ProcedureLike } from "@pluv/types";
3
+ import type { EventResolver, MergeEventRecords } from "./types";
4
+
5
+ export interface PluvProcedureConfig<
6
+ TIO extends IOLike = IOLike,
7
+ TInput extends JsonObject = {},
8
+ TOutput extends EventRecord<string, any> = {},
9
+ TPresence extends JsonObject = {},
10
+ TStorage extends Record<string, CrdtType<any, any>> = {},
11
+ > {
12
+ broadcast?: EventResolver<TIO, TInput, TOutput, TPresence, TStorage> | null;
13
+ input?: InputZodLike<TInput>;
14
+ }
15
+
16
+ export class PluvProcedure<
17
+ TIO extends IOLike = IOLike,
18
+ TInput extends JsonObject = {},
19
+ TOutput extends EventRecord<string, any> = {},
20
+ TPresence extends JsonObject = {},
21
+ TStorage extends Record<string, CrdtType<any, any>> = {},
22
+ TFilled extends "input" | "broadcast" | "" = "",
23
+ > {
24
+ private _broadcast: EventResolver<TIO, TInput, Partial<TOutput>, TPresence, TStorage> | null = null;
25
+ private _input: InputZodLike<TInput> | null = null;
26
+
27
+ public get config(): ProcedureLike<TInput, TOutput>["config"] {
28
+ return {
29
+ broadcast: this._broadcast?.bind(this) ?? null,
30
+ input: this._input ?? null,
31
+ resolver: this._resolver(),
32
+ } as ProcedureLike<TInput, TOutput>["config"];
33
+ }
34
+
35
+ constructor(config: PluvProcedureConfig<TIO, TInput, TOutput, TPresence, TStorage> = {}) {
36
+ const { broadcast, input } = config;
37
+
38
+ this._broadcast = broadcast ?? null;
39
+ this._input = input ?? null;
40
+ }
41
+
42
+ public broadcast<TResult extends EventRecord<string, any> = {}>(
43
+ resolver: EventResolver<TIO, TInput, TResult, TPresence, TStorage>,
44
+ ): Omit<
45
+ PluvProcedure<
46
+ TIO,
47
+ TInput,
48
+ MergeEventRecords<[TOutput, TResult]>,
49
+ TPresence,
50
+ TStorage,
51
+ TFilled | "input" | "broadcast"
52
+ >,
53
+ TFilled | "input" | "broadcast"
54
+ > {
55
+ const { broadcast } = this.config;
56
+
57
+ if (!!broadcast) throw new Error("Broadcast was already defined for this procedure");
58
+
59
+ return new PluvProcedure<
60
+ TIO,
61
+ TInput,
62
+ MergeEventRecords<[TOutput, TResult]>,
63
+ TPresence,
64
+ TStorage,
65
+ TFilled | "input" | "broadcast"
66
+ >({
67
+ ...(this.config as any),
68
+ broadcast: resolver as any,
69
+ });
70
+ }
71
+
72
+ public input<TData extends JsonObject>(
73
+ input: InputZodLike<TData>,
74
+ ): Omit<PluvProcedure<TIO, TData, {}, TPresence, TStorage, TFilled | "input">, TFilled | "input"> {
75
+ return new PluvProcedure<TIO, TData, {}, TPresence, TStorage, TFilled | "input">({ input });
76
+ }
77
+
78
+ private _resolver(): EventResolver<TIO, TInput, TOutput, TPresence, TStorage> {
79
+ return (data, context) => this._broadcast?.(data, context) as TOutput;
80
+ }
81
+ }
package/src/PluvRoom.ts CHANGED
@@ -21,20 +21,25 @@ import type { EventNotifierSubscriptionCallback } from "./EventNotifier";
21
21
  import { EventNotifier } from "./EventNotifier";
22
22
  import type { OtherNotifierSubscriptionCallback } from "./OtherNotifier";
23
23
  import { OtherNotifier } from "./OtherNotifier";
24
+ import { PluvProcedure } from "./PluvProcedure";
25
+ import type { MergeEvents, PluvRouterEventConfig } from "./PluvRouter";
26
+ import { PluvRouter } from "./PluvRouter";
24
27
  import type { StateNotifierSubjects, SubscriptionCallback } from "./StateNotifier";
25
28
  import { StateNotifier } from "./StateNotifier";
26
29
  import { StorageStore } from "./StorageStore";
27
30
  import type { UsersManagerConfig } from "./UsersManager";
28
31
  import { UsersManager } from "./UsersManager";
32
+ import { ConnectionState } from "./enums";
29
33
  import type {
30
34
  AuthorizationState,
35
+ EventResolver,
36
+ EventResolverContext,
31
37
  InternalSubscriptions,
32
38
  UpdateMyPresenceAction,
33
39
  UserInfo,
34
40
  WebSocketConnection,
35
41
  WebSocketState,
36
42
  } from "./types";
37
- import { ConnectionState } from "./types";
38
43
  import { debounce } from "./utils";
39
44
 
40
45
  const ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1_000;
@@ -131,44 +136,44 @@ export type PluvRoomDebug<TIO extends IOLike> = Id<{
131
136
  input: readonly (keyof InferIOInput<TIO>)[];
132
137
  }>;
133
138
 
134
- export type PluvRoomOptions<
135
- TIO extends IOLike,
136
- TMetadata extends JsonObject = {},
137
- TPresence extends JsonObject = {},
138
- TStorage extends Record<string, CrdtType<any, any>> = {},
139
- > = {
140
- addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
141
- debug?: boolean | PluvRoomDebug<TIO>;
142
- onAuthorizationFail?: (error: Error) => void;
143
- } & Omit<CrdtManagerOptions<TStorage>, "encodedState"> &
144
- UsersManagerConfig<TPresence> &
145
- (keyof TMetadata extends never ? { metadata?: undefined } : { metadata: TMetadata });
146
-
147
139
  export type RoomConfig<
148
140
  TIO extends IOLike,
149
141
  TMetadata extends JsonObject = {},
150
142
  TPresence extends JsonObject = {},
151
143
  TStorage extends Record<string, CrdtType<any, any>> = {},
152
- > = RoomEndpoints<TIO, TMetadata> & PluvRoomOptions<TIO, TMetadata, TPresence, TStorage>;
144
+ TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
145
+ > = Id<
146
+ {
147
+ addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
148
+ debug?: boolean | PluvRoomDebug<TIO>;
149
+ onAuthorizationFail?: (error: Error) => void;
150
+ router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
151
+ } & RoomEndpoints<TIO, TMetadata> &
152
+ Pick<CrdtManagerOptions<TStorage>, "initialStorage"> &
153
+ UsersManagerConfig<TPresence> &
154
+ (keyof TMetadata extends never ? { metadata?: undefined } : { metadata: TMetadata })
155
+ >;
153
156
 
154
157
  export class PluvRoom<
155
158
  TIO extends IOLike,
156
159
  TMetadata extends JsonObject = {},
157
160
  TPresence extends JsonObject = {},
158
161
  TStorage extends Record<string, CrdtType<any, any>> = {},
162
+ TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
159
163
  > extends AbstractRoom<TIO, TPresence, TStorage> {
160
164
  readonly _endpoints: RoomEndpoints<TIO, TMetadata>;
161
165
 
162
166
  private _crdtManager: CrdtManager<TStorage>;
163
167
  private _crdtNotifier = new CrdtNotifier<TStorage>();
164
168
  private _debug: boolean | PluvRoomDebug<TIO>;
165
- private _eventNotifier = new EventNotifier<TIO>();
169
+ private _eventNotifier = new EventNotifier<MergeEvents<TEvents, TIO>>();
166
170
  private _intervals: IntervalIds = {
167
171
  heartbeat: null,
168
172
  };
169
173
  private _listeners: InternalListeners;
170
174
  private _metadata: TMetadata = {} as TMetadata;
171
175
  private _otherNotifier = new OtherNotifier<TIO>();
176
+ private _router: PluvRouter<TIO, TPresence, TStorage, TEvents>;
172
177
  private _state: WebSocketState<TIO> = {
173
178
  authorization: {
174
179
  token: null,
@@ -194,7 +199,7 @@ export class PluvRoom<
194
199
  private _wsListeners: WebSocketListeners | null = null;
195
200
  private _usersManager: UsersManager<TIO, TPresence>;
196
201
 
197
- constructor(room: string, options: RoomConfig<TIO, TMetadata, TPresence, TStorage>) {
202
+ constructor(room: string, options: RoomConfig<TIO, TMetadata, TPresence, TStorage, TEvents>) {
198
203
  const {
199
204
  addons = [],
200
205
  authEndpoint,
@@ -204,6 +209,7 @@ export class PluvRoom<
204
209
  metadata,
205
210
  onAuthorizationFail,
206
211
  presence,
212
+ router,
207
213
  wsEndpoint,
208
214
  } = options;
209
215
 
@@ -231,6 +237,7 @@ export class PluvRoom<
231
237
  },
232
238
  };
233
239
 
240
+ this._router = router ?? (new PluvRouter({}) as PluvRouter<TIO, TPresence, TStorage, TEvents>);
234
241
  this._usersManager = new UsersManager<TIO, TPresence>({ initialPresence, presence });
235
242
  this._crdtManager = new CrdtManager<TStorage>({ initialStorage });
236
243
  }
@@ -244,23 +251,62 @@ export class PluvRoom<
244
251
  }
245
252
 
246
253
  public broadcast = new Proxy(
247
- <TEvent extends keyof InferIOInput<TIO>>(event: TEvent, data: Id<InferIOInput<TIO>[TEvent]>): void => {
254
+ async <TEvent extends keyof InferIOInput<MergeEvents<TEvents, TIO>>>(
255
+ event: TEvent,
256
+ data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[TEvent]>,
257
+ ): Promise<void> => {
248
258
  if (!this._state.webSocket) return;
249
259
  if (this._state.connection.state !== ConnectionState.Open) return;
250
260
 
251
261
  const type = event.toString();
252
262
 
253
- this._sendMessage({ data, type });
263
+ const procedure = this._router._defs.events[type] as PluvProcedure<
264
+ TIO,
265
+ any,
266
+ any,
267
+ TPresence,
268
+ TStorage
269
+ > | null;
270
+
271
+ if (!procedure?.config.broadcast) {
272
+ this._sendMessage({ data, type });
273
+
274
+ return;
275
+ }
276
+
277
+ const myself = this._usersManager.myself;
278
+
279
+ if (!myself) return;
280
+
281
+ const parsed = procedure.config.input ? procedure.config.input.parse(data) : data;
282
+ const context: EventResolverContext<TIO, TPresence, TStorage> = {
283
+ doc: this._crdtManager.doc,
284
+ others: this._usersManager.getOthers(),
285
+ room: this.id,
286
+ user: myself,
287
+ };
288
+
289
+ const output = await (procedure.config.broadcast as EventResolver<TIO, any, any, TPresence, TStorage>)(
290
+ parsed,
291
+ context,
292
+ );
293
+
294
+ Object.entries(output).forEach(([_type, _data]) => {
295
+ this._sendMessage({ data: _data, type: _type });
296
+ });
254
297
  },
255
298
  {
256
299
  get(fn, prop) {
257
- return (data: Id<InferIOInput<TIO>[any]>): void => {
258
- return fn(prop, data);
300
+ return async (data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[any]>): Promise<void> => {
301
+ return await fn(prop, data);
259
302
  };
260
303
  },
261
304
  },
262
- ) as (<TEvent extends keyof InferIOInput<TIO>>(event: TEvent, data: Id<InferIOInput<TIO>[TEvent]>) => void) & {
263
- [event in keyof InferIOInput<TIO>]: (data: Id<InferIOInput<TIO>[event]>) => void;
305
+ ) as (<TEvent extends keyof InferIOInput<MergeEvents<TEvents, TIO>>>(
306
+ event: TEvent,
307
+ data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[TEvent]>,
308
+ ) => Promise<void>) & {
309
+ [event in keyof InferIOInput<TIO>]: (data: Id<InferIOInput<TIO>[event]>) => Promise<void>;
264
310
  };
265
311
 
266
312
  public canRedo = (): boolean => {
@@ -348,20 +394,23 @@ export class PluvRoom<
348
394
  }
349
395
 
350
396
  public event = new Proxy(
351
- <TEvent extends keyof InferIOOutput<TIO>>(
397
+ <TEvent extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
352
398
  event: TEvent,
353
- callback: EventNotifierSubscriptionCallback<TIO, TEvent>,
399
+ callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, any>,
354
400
  ): (() => void) => this._eventNotifier.subscribe(event, callback),
355
401
  {
356
402
  get(fn, prop) {
357
- return (callback: EventNotifierSubscriptionCallback<TIO, any>): (() => void) => fn(prop, callback);
403
+ return (callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, any>): (() => void) =>
404
+ fn(prop as any, callback);
358
405
  },
359
406
  },
360
- ) as (<TEvent extends keyof InferIOOutput<TIO>>(
407
+ ) as (<TEvent extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
361
408
  event: TEvent,
362
- callback: EventNotifierSubscriptionCallback<TIO, TEvent>,
409
+ callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, TEvent>,
363
410
  ) => () => void) & {
364
- [event in keyof InferIOOutput<TIO>]: (callback: EventNotifierSubscriptionCallback<TIO, event>) => () => void;
411
+ [event in keyof InferIOOutput<MergeEvents<TEvents, TIO>>]: (
412
+ callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, any>,
413
+ ) => () => void;
365
414
  };
366
415
 
367
416
  public getConnection = (): WebSocketConnection => {
@@ -475,7 +524,10 @@ export class PluvRoom<
475
524
  this._stateNotifier.subjects["my-presence"].next(myPresence);
476
525
  !!myself && this._stateNotifier.subjects["myself"].next(myself);
477
526
 
478
- this.broadcast("$UPDATE_PRESENCE" as keyof InferIOInput<TIO>, { presence: newPresence } as any);
527
+ this.broadcast(
528
+ "$UPDATE_PRESENCE" as keyof InferIOInput<MergeEvents<TEvents, TIO>>,
529
+ { presence: newPresence } as any,
530
+ );
479
531
  };
480
532
 
481
533
  private async _addToStorageStore(update: string): Promise<void> {
@@ -0,0 +1,70 @@
1
+ import type { CrdtType } from "@pluv/crdt";
2
+ import type { Id, IOLike, IORouterLike, JsonObject, ProcedureLike } from "@pluv/types";
3
+ import type { PluvProcedure } from "./PluvProcedure";
4
+
5
+ export type PluvRouterEventConfig<
6
+ TIO extends IOLike = IOLike,
7
+ TPresence extends JsonObject = {},
8
+ TStorage extends Record<string, CrdtType<any, any>> = {},
9
+ > = { [P: string]: Pick<PluvProcedure<TIO, any, any, TPresence, TStorage>, "config"> };
10
+
11
+ export type MergedRouter<
12
+ TRouters extends PluvRouter<TIO, TPresence, TStorage, any>[] = [],
13
+ TIO extends IOLike = IOLike,
14
+ TPresence extends JsonObject = {},
15
+ TStorage extends Record<string, CrdtType<any, any>> = {},
16
+ TRoot extends TRouters[0]["_defs"]["events"] = {},
17
+ > = TRouters extends [
18
+ infer IHead extends PluvRouter<TIO, TPresence, TStorage, any>,
19
+ ...infer ITail extends PluvRouter<TIO, TPresence, TStorage, any>[],
20
+ ]
21
+ ? MergedRouter<ITail, TIO, TPresence, TStorage, TRoot & IHead["_defs"]["events"]>
22
+ : PluvRouter<TIO, TPresence, TStorage, TRoot>;
23
+
24
+ export type MergeEvents<
25
+ TClientEvents extends PluvRouterEventConfig<any, any, any>,
26
+ TServerIO extends IOLike<any, any>,
27
+ > =
28
+ TServerIO extends IOLike<infer IAuthorize, infer IServerEvents>
29
+ ? IOLike<
30
+ IAuthorize,
31
+ {
32
+ [P in keyof TClientEvents]: TClientEvents[P] extends ProcedureLike<
33
+ infer IClientInput,
34
+ infer IClientOutput
35
+ >
36
+ ? {
37
+ [K in keyof IClientOutput]: K extends keyof IServerEvents
38
+ ? IServerEvents[K]
39
+ : ProcedureLike<IClientInput, Id<Pick<IClientOutput, K>>>;
40
+ }[keyof IClientOutput]
41
+ : never;
42
+ } & IServerEvents
43
+ >
44
+ : never;
45
+
46
+ export class PluvRouter<
47
+ TIO extends IOLike = IOLike,
48
+ TPresence extends JsonObject = {},
49
+ TStorage extends Record<string, CrdtType<any, any>> = {},
50
+ TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
51
+ > implements IORouterLike<TEvents>
52
+ {
53
+ readonly _defs: { events: TEvents } = { events: {} as TEvents };
54
+
55
+ constructor(events: TEvents) {
56
+ this._defs = { events };
57
+ }
58
+
59
+ public static merge<TRouters extends PluvRouter<any, any, any, any>[]>(
60
+ ...routers: TRouters
61
+ ): MergedRouter<TRouters> {
62
+ const events = Object.assign(Object.create(null), ...routers.map((router) => router._defs.events));
63
+
64
+ if (Object.keys(events).some((key) => key.startsWith("$"))) {
65
+ throw new Error('Procedures may not start with "$"');
66
+ }
67
+
68
+ return new PluvRouter<any, any, any, any>(events) as MergedRouter<TRouters>;
69
+ }
70
+ }
@@ -1,9 +1,15 @@
1
+ import type { CrdtType } from "@pluv/crdt";
1
2
  import type { IOLike, JsonObject } from "@pluv/types";
2
3
  import type { PluvClientOptions } from "./PluvClient";
3
4
  import { PluvClient } from "./PluvClient";
4
5
 
5
- export const createClient = <TIO extends IOLike = IOLike, TMetadata extends JsonObject = {}>(
6
- options: PluvClientOptions<TIO, TMetadata>,
7
- ): PluvClient<TIO, TMetadata> => {
8
- return new PluvClient<TIO, TMetadata>(options);
6
+ export const createClient = <
7
+ TIO extends IOLike = IOLike,
8
+ TPresence extends JsonObject = {},
9
+ TStorage extends Record<string, CrdtType<any, any>> = {},
10
+ TMetadata extends JsonObject = {},
11
+ >(
12
+ options: PluvClientOptions<TIO, TPresence, TStorage, TMetadata>,
13
+ ): PluvClient<TIO, TPresence, TStorage, TMetadata> => {
14
+ return new PluvClient<TIO, TPresence, TStorage, TMetadata>(options);
9
15
  };
package/src/enums.ts ADDED
@@ -0,0 +1,7 @@
1
+ export enum ConnectionState {
2
+ Closed = "Closed",
3
+ Connecting = "Connecting",
4
+ Open = "Open",
5
+ Unavailable = "Unavailable",
6
+ Untouched = "Untouched",
7
+ }
package/src/index.ts CHANGED
@@ -3,17 +3,13 @@ export type { BaseUser, EventMessage, EventRecord, IOEventMessage } from "@pluv/
3
3
  export { AbstractRoom } from "./AbstractRoom";
4
4
  export { AbstractStorageStore } from "./AbstractStorageStore";
5
5
  export { createClient } from "./createClient";
6
+ export { ConnectionState } from "./enums";
6
7
  export { MockedRoom } from "./MockedRoom";
7
8
  export type { MockedRoomConfig, MockedRoomEvents } from "./MockedRoom";
8
9
  export { PluvClient } from "./PluvClient";
9
- export type { PluvClientOptions } from "./PluvClient";
10
+ export type { CreateRoomOptions, PluvClientOptions } from "./PluvClient";
11
+ export type { PluvProcedureConfig } from "./PluvProcedure";
10
12
  export { PluvRoom } from "./PluvRoom";
11
- export type {
12
- PluvRoomAddon,
13
- PluvRoomAddonInput,
14
- PluvRoomAddonResult,
15
- PluvRoomDebug,
16
- PluvRoomOptions,
17
- } from "./PluvRoom";
18
- export { ConnectionState } from "./types";
13
+ export type { PluvRoomAddon, PluvRoomAddonInput, PluvRoomAddonResult, PluvRoomDebug } from "./PluvRoom";
14
+ export type { MergeEvents, PluvRouter, PluvRouterEventConfig } from "./PluvRouter";
19
15
  export type { UserInfo, WebSocketConnection, WebSocketState } from "./types";
package/src/types.ts CHANGED
@@ -1,22 +1,58 @@
1
- import type { Id, InferIOAuthorize, InferIOAuthorizeUser, IOLike, JsonObject } from "@pluv/types";
1
+ import type { AbstractCrdtDoc, CrdtType } from "@pluv/crdt";
2
+ import type {
3
+ EventRecord,
4
+ Id,
5
+ InferIOAuthorize,
6
+ InferIOAuthorizeUser,
7
+ IOLike,
8
+ JsonObject,
9
+ MaybePromise,
10
+ } from "@pluv/types";
11
+ import type { ConnectionState } from "./enums";
2
12
 
3
13
  export interface AuthorizationState<TIO extends IOLike> {
4
14
  token: string | null;
5
15
  user: InferIOAuthorizeUser<InferIOAuthorize<TIO>> | null;
6
16
  }
7
17
 
8
- export enum ConnectionState {
9
- Closed = "Closed",
10
- Connecting = "Connecting",
11
- Open = "Open",
12
- Unavailable = "Unavailable",
13
- Untouched = "Untouched",
18
+ export type EventResolver<
19
+ TIO extends IOLike = IOLike,
20
+ TInput extends JsonObject = {},
21
+ TOutput extends EventRecord<string, any> = {},
22
+ TPresence extends JsonObject = {},
23
+ TStorage extends Record<string, CrdtType<any, any>> = {},
24
+ > = (data: TInput, context: EventResolverContext<TIO, TPresence, TStorage>) => MaybePromise<TOutput | void>;
25
+
26
+ export interface EventResolverContext<
27
+ TIO extends IOLike = IOLike,
28
+ TPresence extends JsonObject = {},
29
+ TStorage extends Record<string, CrdtType<any, any>> = {},
30
+ > {
31
+ doc: AbstractCrdtDoc<TStorage>;
32
+ others: readonly UserInfo<TIO, TPresence>[];
33
+ room: string;
34
+ user: UserInfo<TIO, TPresence>;
14
35
  }
15
36
 
16
37
  export interface InternalSubscriptions {
17
38
  observeCrdt: (() => void) | null;
18
39
  }
19
40
 
41
+ export type MergeEventRecords<
42
+ TEventRecords extends EventRecord<string, any>[],
43
+ TRoot extends EventRecord<string, any> = {},
44
+ > = TEventRecords extends [
45
+ infer IHead extends EventRecord<string, any>,
46
+ ...infer ITail extends EventRecord<string, any>[],
47
+ ]
48
+ ? MergeEventRecords<
49
+ ITail,
50
+ Omit<TRoot, keyof IHead> & {
51
+ [P in keyof IHead]: TRoot extends Record<P, any> ? TRoot[P] | IHead[P] : IHead[P];
52
+ }
53
+ >
54
+ : Id<TRoot>;
55
+
20
56
  export type UpdateMyPresenceAction<TPresence extends JsonObject> =
21
57
  | Partial<TPresence>
22
58
  | ((oldPresence: TPresence | null) => Partial<TPresence>);
@@ -0,0 +1 @@
1
+ export const identity = <T extends unknown>(x: T): T => x;
@@ -1 +1,2 @@
1
1
  export { debounce } from "./debounce";
2
+ export { identity } from "./identity";