@pluv/client 0.25.4 → 0.27.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/MockedRoom.ts CHANGED
@@ -8,18 +8,23 @@ import type { EventNotifierSubscriptionCallback } from "./EventNotifier";
8
8
  import { EventNotifier } from "./EventNotifier";
9
9
  import type { OtherNotifierSubscriptionCallback } from "./OtherNotifier";
10
10
  import { OtherNotifier } from "./OtherNotifier";
11
+ import { PluvProcedure } from "./PluvProcedure";
12
+ import type { MergeEvents, PluvRouterEventConfig } from "./PluvRouter";
13
+ import { PluvRouter } from "./PluvRouter";
11
14
  import type { StateNotifierSubjects, SubscriptionCallback } from "./StateNotifier";
12
15
  import { StateNotifier } from "./StateNotifier";
13
16
  import type { UsersManagerConfig } from "./UsersManager";
14
17
  import { UsersManager } from "./UsersManager";
18
+ import { ConnectionState } from "./enums";
15
19
  import type {
20
+ EventResolver,
21
+ EventResolverContext,
16
22
  InternalSubscriptions,
17
23
  UpdateMyPresenceAction,
18
24
  UserInfo,
19
25
  WebSocketConnection,
20
26
  WebSocketState,
21
27
  } from "./types";
22
- import { ConnectionState } from "./types";
23
28
 
24
29
  export type MockedRoomEvents<TIO extends IOLike> = Partial<{
25
30
  [P in keyof InferIOInput<TIO>]: (data: Id<InferIOInput<TIO>[P]>) => Partial<InferIOOutput<TIO>>;
@@ -29,19 +34,25 @@ export type MockedRoomConfig<
29
34
  TIO extends IOLike,
30
35
  TPresence extends JsonObject = {},
31
36
  TStorage extends Record<string, CrdtType<any, any>> = {},
32
- > = { events?: MockedRoomEvents<TIO> } & Omit<CrdtManagerOptions<TStorage>, "encodedState"> &
37
+ TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
38
+ > = {
39
+ events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
40
+ router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
41
+ } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> &
33
42
  UsersManagerConfig<TPresence>;
34
43
 
35
44
  export class MockedRoom<
36
45
  TIO extends IOLike,
37
46
  TPresence extends JsonObject = {},
38
47
  TStorage extends Record<string, CrdtType<any, any>> = {},
48
+ TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
39
49
  > extends AbstractRoom<TIO, TPresence, TStorage> {
40
50
  private _crdtManager: CrdtManager<TStorage>;
41
51
  private _crdtNotifier = new CrdtNotifier<TStorage>();
42
- private _eventNotifier = new EventNotifier<TIO>();
43
- private _events?: MockedRoomEvents<TIO>;
52
+ private _eventNotifier = new EventNotifier<MergeEvents<TEvents, TIO>>();
53
+ private _events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
44
54
  private _otherNotifier = new OtherNotifier<TIO>();
55
+ private _router: PluvRouter<TIO, TPresence, TStorage, TEvents>;
45
56
  private _state: WebSocketState<TIO> = {
46
57
  authorization: {
47
58
  token: null,
@@ -60,13 +71,14 @@ export class MockedRoom<
60
71
  };
61
72
  private _usersManager: UsersManager<TIO, TPresence>;
62
73
 
63
- constructor(room: string, options: MockedRoomConfig<TIO, TPresence, TStorage>) {
64
- const { events, initialPresence, initialStorage, presence } = options;
74
+ constructor(room: string, options: MockedRoomConfig<TIO, TPresence, TStorage, TEvents>) {
75
+ const { events, initialPresence, initialStorage, presence, router } = options;
65
76
 
66
77
  super(room);
67
78
 
68
79
  this._events = events;
69
80
 
81
+ this._router = router ?? (new PluvRouter({}) as PluvRouter<TIO, TPresence, TStorage, TEvents>);
70
82
  this._usersManager = new UsersManager<TIO, TPresence>({
71
83
  initialPresence,
72
84
  presence,
@@ -83,24 +95,64 @@ export class MockedRoom<
83
95
  return true;
84
96
  }
85
97
 
86
- public broadcast<TEvent extends keyof InferIOInput<TIO>>(event: TEvent, data: Id<InferIOInput<TIO>[TEvent]>): void {
87
- if (!this._events) return;
88
-
89
- const resolver = this._events[event];
90
-
91
- if (!resolver) return;
92
-
93
- const result = resolver(data);
94
-
95
- Object.keys(result).forEach((type) => {
96
- const _type = type.toString() as keyof Partial<InferIOOutput<TIO>>;
97
- const data = result[_type] as any;
98
-
99
- if (!data) return;
98
+ public broadcast = new Proxy(
99
+ async <TEvent extends keyof InferIOInput<MergeEvents<TEvents, TIO>>>(
100
+ event: TEvent,
101
+ data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[TEvent]>,
102
+ ): Promise<void> => {
103
+ if (!this._state.webSocket) return;
104
+ if (this._state.connection.state !== ConnectionState.Open) return;
105
+
106
+ const type = event.toString();
107
+
108
+ const procedure = this._router._defs.events[type] as PluvProcedure<
109
+ TIO,
110
+ any,
111
+ any,
112
+ TPresence,
113
+ TStorage
114
+ > | null;
115
+
116
+ if (!procedure?.config.broadcast) {
117
+ this._simulateEvent(type as TEvent, data);
118
+
119
+ return;
120
+ }
121
+
122
+ const myself = this._usersManager.myself;
123
+
124
+ if (!myself) return;
125
+
126
+ const parsed = procedure.config.input ? procedure.config.input.parse(data) : data;
127
+ const context: EventResolverContext<TIO, TPresence, TStorage> = {
128
+ doc: this._crdtManager.doc,
129
+ others: this._usersManager.getOthers(),
130
+ room: this.id,
131
+ user: myself,
132
+ };
133
+
134
+ const output = await (procedure.config.broadcast as EventResolver<TIO, any, any, TPresence, TStorage>)(
135
+ parsed,
136
+ context,
137
+ );
100
138
 
101
- this._eventNotifier.subject(_type).next(data);
102
- });
103
- }
139
+ Object.entries(output).forEach(([_type, _data]) => {
140
+ this._simulateEvent(_type as TEvent, _data as any);
141
+ });
142
+ },
143
+ {
144
+ get(fn, prop) {
145
+ return async (data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[any]>): Promise<void> => {
146
+ return await fn(prop, data);
147
+ };
148
+ },
149
+ },
150
+ ) as (<TEvent extends keyof InferIOInput<MergeEvents<TEvents, TIO>>>(
151
+ event: TEvent,
152
+ data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[TEvent]>,
153
+ ) => Promise<void>) & {
154
+ [event in keyof InferIOInput<TIO>]: (data: Id<InferIOInput<TIO>[event]>) => Promise<void>;
155
+ };
104
156
 
105
157
  public canRedo = (): boolean => {
106
158
  return !!this._crdtManager.doc.canRedo();
@@ -110,11 +162,24 @@ export class MockedRoom<
110
162
  return !!this._crdtManager.doc.canUndo();
111
163
  };
112
164
 
113
- public event = <TEvent extends keyof InferIOOutput<TIO>>(
165
+ public event = new Proxy(
166
+ <TEvent extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
167
+ event: TEvent,
168
+ callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, any>,
169
+ ): (() => void) => this._eventNotifier.subscribe(event, callback),
170
+ {
171
+ get(fn, prop) {
172
+ return (callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, any>): (() => void) =>
173
+ fn(prop as any, callback);
174
+ },
175
+ },
176
+ ) as (<TEvent extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
114
177
  event: TEvent,
115
- callback: EventNotifierSubscriptionCallback<TIO, TEvent>,
116
- ): (() => void) => {
117
- return this._eventNotifier.subscribe(event, callback);
178
+ callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, TEvent>,
179
+ ) => () => void) & {
180
+ [event in keyof InferIOOutput<MergeEvents<TEvents, TIO>>]: (
181
+ callback: EventNotifierSubscriptionCallback<MergeEvents<TEvents, TIO>, any>,
182
+ ) => () => void;
118
183
  };
119
184
 
120
185
  public getConnection = (): WebSocketConnection => {
@@ -260,4 +325,27 @@ export class MockedRoom<
260
325
 
261
326
  this._subscriptions.observeCrdt = unsubscribe;
262
327
  }
328
+
329
+ private _simulateEvent<TEvent extends keyof InferIOInput<MergeEvents<TEvents, TIO>>>(
330
+ event: TEvent,
331
+ data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[TEvent]>,
332
+ ) {
333
+ if (!this._events) return;
334
+
335
+ const type = event.toString();
336
+ const resolver = this._events[type as TEvent];
337
+
338
+ if (!resolver) return;
339
+
340
+ const result = resolver(data);
341
+
342
+ Object.keys(result).forEach((type) => {
343
+ const _type = type.toString() as keyof Partial<InferIOOutput<TIO>>;
344
+ const data = result[_type] as any;
345
+
346
+ if (!data) return;
347
+
348
+ this._eventNotifier.subject(_type).next(data);
349
+ });
350
+ }
263
351
  }
package/src/PluvClient.ts CHANGED
@@ -1,44 +1,92 @@
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> = RoomEndpoints<TIO> & {
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> {
11
- private _authEndpoint: AuthEndpoint | undefined;
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
+ > {
48
+ private _authEndpoint: AuthEndpoint<TMetadata> | undefined;
12
49
  private _debug: boolean;
13
- private _wsEndpoint: WsEndpoint | undefined;
50
+ private _initialStorage?: AbstractCrdtDocFactory<TStorage>;
51
+ private _presence?: InputZodLike<TPresence>;
52
+ private _wsEndpoint: WsEndpoint<TMetadata> | undefined;
14
53
 
15
- private _rooms = new Map<string, PluvRoom<TIO, any, any>>();
54
+ private _rooms = new Map<string, PluvRoom<TIO, TMetadata, TPresence, TStorage, any>>();
55
+
56
+ public get procedure(): PluvProcedure<TIO, {}, {}, TPresence, TStorage> {
57
+ return new PluvProcedure<TIO, {}, {}, TPresence, TStorage>();
58
+ }
16
59
 
17
- constructor(options: PluvClientOptions<TIO>) {
18
- const { authEndpoint, debug = false, wsEndpoint } = options;
60
+ constructor(options: PluvClientOptions<TIO, TPresence, TStorage, TMetadata>) {
61
+ const { authEndpoint, debug = false, initialStorage, presence, wsEndpoint } = options;
19
62
 
20
- this._authEndpoint = authEndpoint as AuthEndpoint;
63
+ this._authEndpoint = authEndpoint as AuthEndpoint<TMetadata>;
21
64
  this._debug = debug;
65
+ this._initialStorage = initialStorage;
66
+ this._presence = presence;
22
67
  this._wsEndpoint = wsEndpoint;
23
68
  }
24
69
 
25
- public createRoom = <TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}>(
70
+ public createRoom = <TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}>(
26
71
  room: string,
27
- options: PluvRoomOptions<TIO, TPresence, TStorage>,
28
- ): PluvRoom<TIO, TPresence, TStorage> => {
29
- const oldRoom = this.getRoom<TPresence, TStorage>(room);
72
+ options: CreateRoomOptions<TIO, TPresence, TStorage, TMetadata, TEvents>,
73
+ ): PluvRoom<TIO, TMetadata, TPresence, TStorage> => {
74
+ const oldRoom = this.getRoom(room);
30
75
 
31
76
  if (oldRoom) return oldRoom;
32
77
 
33
- const newRoom = new PluvRoom<TIO, TPresence, TStorage>(room, {
78
+ const newRoom = new PluvRoom<TIO, TMetadata, TPresence, TStorage, TEvents>(room, {
34
79
  addons: options.addons,
35
80
  authEndpoint: this._authEndpoint,
36
81
  debug: options.debug,
37
82
  initialPresence: options.initialPresence,
38
- initialStorage: options.initialStorage,
83
+ initialStorage: this._initialStorage,
84
+ metadata: options.metadata,
39
85
  onAuthorizationFail: options.onAuthorizationFail,
86
+ presence: this._presence,
87
+ router: options.router,
40
88
  wsEndpoint: this._wsEndpoint,
41
- } as RoomConfig<TIO, TPresence, TStorage>);
89
+ } as RoomConfig<TIO, TMetadata, TPresence, TStorage, TEvents>);
42
90
 
43
91
  this._rooms.set(room, newRoom);
44
92
 
@@ -47,12 +95,12 @@ export class PluvClient<TIO extends IOLike = IOLike> {
47
95
  return newRoom;
48
96
  };
49
97
 
50
- public enter = async (room: string | PluvRoom<TIO, any, any>): Promise<PluvRoom<TIO, JsonObject, any>> => {
98
+ public enter = async (
99
+ room: string | PluvRoom<TIO, any, any, any>,
100
+ ): Promise<PluvRoom<TIO, TMetadata, TPresence, TStorage, any>> => {
51
101
  const toEnter = typeof room === "string" ? this.getRoom(room) : room;
52
102
 
53
- if (!toEnter) {
54
- throw new Error(`Could not find room: ${room}.`);
55
- }
103
+ if (!toEnter) throw new Error(`Could not find room: ${room}.`);
56
104
 
57
105
  this._rooms.set(toEnter.id, toEnter);
58
106
 
@@ -63,19 +111,17 @@ export class PluvClient<TIO extends IOLike = IOLike> {
63
111
  return toEnter;
64
112
  };
65
113
 
66
- public getRoom = <TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}>(
67
- room: string,
68
- ): PluvRoom<TIO, TPresence, TStorage> | null => {
69
- const found = this._rooms.get(room) as PluvRoom<TIO, TPresence, TStorage> | undefined;
114
+ public getRoom = (room: string): PluvRoom<TIO, TMetadata, TPresence, TStorage, any> | null => {
115
+ const found = this._rooms.get(room) as PluvRoom<TIO, TMetadata, TPresence, TStorage, any> | undefined;
70
116
 
71
117
  return found ?? null;
72
118
  };
73
119
 
74
- public getRooms = (): readonly PluvRoom<TIO, JsonObject, any>[] => {
120
+ public getRooms = (): readonly PluvRoom<TIO, TMetadata, TPresence, TStorage, any>[] => {
75
121
  return Array.from(this._rooms.values());
76
122
  };
77
123
 
78
- public leave = async (room: string | PluvRoom<TIO, any, any>): Promise<void> => {
124
+ public leave = async (room: string | PluvRoom<TIO, any, any, any>): Promise<void> => {
79
125
  const toLeave = typeof room === "string" ? this.getRoom(room) : room;
80
126
 
81
127
  if (!toLeave) return;
@@ -87,6 +133,12 @@ export class PluvClient<TIO extends IOLike = IOLike> {
87
133
  this._logDebug(`Left and deleted room: ${room}`);
88
134
  };
89
135
 
136
+ public router<TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}>(
137
+ events: TEvents,
138
+ ): PluvRouter<TIO, TPresence, TStorage, TEvents> {
139
+ return new PluvRouter<TIO, TPresence, TStorage, TEvents>(events);
140
+ }
141
+
90
142
  private _logDebug(...data: any[]): void {
91
143
  // eslint-disable-next-line turbo/no-undeclared-env-vars
92
144
  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
+ }