@pluv/client 2.0.2 → 2.2.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/PluvRouter.ts CHANGED
@@ -1,31 +1,31 @@
1
- import type { CrdtType } from "@pluv/crdt";
1
+ import type { AbstractCrdtDocFactory } from "@pluv/crdt";
2
2
  import type { IOLike, IORouterLike, JsonObject } from "@pluv/types";
3
3
  import type { PluvProcedure } from "./PluvProcedure";
4
4
 
5
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"> };
6
+ TIO extends IOLike,
7
+ TPresence extends Record<string, any>,
8
+ TCrdt extends AbstractCrdtDocFactory<any, any>,
9
+ > = { [P: string]: Pick<PluvProcedure<TIO, any, any, TPresence, TCrdt, "">, "config"> };
10
10
 
11
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"] = {},
12
+ TRouters extends PluvRouter<TIO, TPresence, TCrdt, any>[],
13
+ TIO extends IOLike,
14
+ TPresence extends Record<string, any>,
15
+ TCrdt extends AbstractCrdtDocFactory<any, any>,
16
+ TRoot extends TRouters[0]["_defs"]["events"],
17
17
  > = TRouters extends [
18
- infer IHead extends PluvRouter<TIO, TPresence, TStorage, any>,
19
- ...infer ITail extends PluvRouter<TIO, TPresence, TStorage, any>[],
18
+ infer IHead extends PluvRouter<TIO, TPresence, TCrdt, any>,
19
+ ...infer ITail extends PluvRouter<TIO, TPresence, TCrdt, any>[],
20
20
  ]
21
- ? MergedRouter<ITail, TIO, TPresence, TStorage, TRoot & IHead["_defs"]["events"]>
22
- : PluvRouter<TIO, TPresence, TStorage, TRoot>;
21
+ ? MergedRouter<ITail, TIO, TPresence, TCrdt, TRoot & IHead["_defs"]["events"]>
22
+ : PluvRouter<TIO, TPresence, TCrdt, TRoot>;
23
23
 
24
24
  export class PluvRouter<
25
- TIO extends IOLike = IOLike,
26
- TPresence extends JsonObject = {},
27
- TStorage extends Record<string, CrdtType<any, any>> = {},
28
- TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
25
+ TIO extends IOLike,
26
+ TPresence extends Record<string, any>,
27
+ TCrdt extends AbstractCrdtDocFactory<any, any>,
28
+ TEvents extends PluvRouterEventConfig<TIO, TPresence, TCrdt> = {},
29
29
  > implements IORouterLike<TEvents>
30
30
  {
31
31
  readonly _defs: { events: TEvents } = { events: {} as TEvents };
@@ -42,9 +42,12 @@ export class PluvRouter<
42
42
  this._defs = { events };
43
43
  }
44
44
 
45
- public static merge<TRouters extends PluvRouter<any, any, any, any>[]>(
46
- ...routers: TRouters
47
- ): MergedRouter<TRouters> {
45
+ public static merge<
46
+ TRouters extends PluvRouter<TIO, TPresence, TCrdt, any>[],
47
+ TIO extends IOLike,
48
+ TPresence extends Record<string, any>,
49
+ TCrdt extends AbstractCrdtDocFactory<any, any>,
50
+ >(...routers: TRouters): MergedRouter<TRouters, TIO, TPresence, TCrdt, {}> {
48
51
  const events = Object.assign(
49
52
  Object.create(null),
50
53
  ...routers.map((router) => router._defs.events),
@@ -54,7 +57,13 @@ export class PluvRouter<
54
57
  throw new Error('Procedures may not start with "$"');
55
58
  }
56
59
 
57
- return new PluvRouter<any, any, any, any>(events) as MergedRouter<TRouters>;
60
+ return new PluvRouter<any, any, any, any>(events) as MergedRouter<
61
+ TRouters,
62
+ TIO,
63
+ TPresence,
64
+ TCrdt,
65
+ {}
66
+ >;
58
67
  }
59
68
 
60
69
  private _isValidEventName(name: string): boolean {
@@ -1,36 +1,30 @@
1
- import type { Id, IOLike, JsonObject, UserInfo, WebSocketState } from "@pluv/types";
1
+ import type {
2
+ Id,
3
+ IOLike,
4
+ JsonObject,
5
+ StateNotifierSubjects,
6
+ SubscriptionCallback,
7
+ UserInfo,
8
+ WebSocketState,
9
+ } from "@pluv/types";
2
10
  import type { Subject } from "wonka";
3
11
  import { makeSubject, subscribe } from "wonka";
4
12
 
5
- export interface StateNotifierSubjects<TIO extends IOLike, TPresence extends JsonObject> {
6
- connection: Subject<Id<WebSocketState<TIO>>>;
7
- "my-presence": Subject<TPresence | null>;
8
- myself: Subject<Readonly<Id<UserInfo<TIO>>> | null>;
9
- others: Subject<readonly Id<UserInfo<TIO>>[]>;
10
- "storage-loaded": Subject<boolean>;
11
- }
12
-
13
13
  type InferSubjectValue<
14
14
  TIO extends IOLike,
15
- TPresence extends JsonObject,
15
+ TPresence extends Record<string, any>,
16
16
  TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
17
17
  > =
18
18
  StateNotifierSubjects<TIO, TPresence>[TSubject] extends Subject<infer IValue>
19
19
  ? Id<IValue>
20
20
  : never;
21
21
 
22
- export type SubscriptionCallback<
23
- TIO extends IOLike,
24
- TPresence extends JsonObject,
25
- TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
26
- > = (value: InferSubjectValue<TIO, TPresence, TSubject>) => void;
27
-
28
- export class StateNotifier<TIO extends IOLike, TPresence extends JsonObject = {}> {
22
+ export class StateNotifier<TIO extends IOLike, TPresence extends Record<string, any> = {}> {
29
23
  public subjects: StateNotifierSubjects<TIO, TPresence> = {
30
24
  connection: makeSubject<Id<WebSocketState<TIO>>>(),
31
25
  "my-presence": makeSubject<TPresence>(),
32
- myself: makeSubject<Readonly<Id<UserInfo<TIO>>> | null>(),
33
- others: makeSubject<readonly Id<UserInfo<TIO>>[]>(),
26
+ myself: makeSubject<Readonly<Id<UserInfo<TIO, TPresence>>> | null>(),
27
+ others: makeSubject<readonly Id<UserInfo<TIO, TPresence>>[]>(),
34
28
  "storage-loaded": makeSubject<true>(),
35
29
  };
36
30
 
@@ -1,29 +1,33 @@
1
1
  import type { InputZodLike, IOLike, JsonObject, OptionalProps, UserInfo } from "@pluv/types";
2
+ import { PLUV_PRESENCE_META_KEY } from "./constants";
2
3
  import type { PluvClientLimits } from "./types";
3
4
  import { pickBy } from "./utils";
4
5
 
5
6
  export type Presence = Record<string, unknown>;
6
7
 
7
- export type UsersManagerConfig<TPresence extends JsonObject = {}> = {
8
+ export type UsersManagerConfig<TPresence extends Record<string, any> = {}> = {
8
9
  initialPresence?: TPresence;
9
10
  limits: PluvClientLimits;
10
11
  presence?: InputZodLike<TPresence>;
11
12
  };
12
13
 
13
- export type AddConnectionResult<TIO extends IOLike, TPresence extends JsonObject = {}> = {
14
+ export type AddConnectionResult<TIO extends IOLike, TPresence extends Record<string, any> = {}> = {
14
15
  clientId: string;
15
16
  data: UserInfo<TIO, TPresence>;
16
17
  isMyself: boolean;
17
18
  remaining: number;
18
19
  };
19
20
 
20
- export type DeleteConnectionResult<TIO extends IOLike, TPresence extends JsonObject = {}> = {
21
+ export type DeleteConnectionResult<
22
+ TIO extends IOLike,
23
+ TPresence extends Record<string, any> = {},
24
+ > = {
21
25
  clientId: string;
22
26
  data: UserInfo<TIO, TPresence>;
23
27
  remaining: number;
24
28
  };
25
29
 
26
- export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}> {
30
+ export class UsersManager<TIO extends IOLike, TPresence extends Record<string, any> = {}> {
27
31
  public readonly initialPresence: TPresence;
28
32
 
29
33
  private readonly _idMap = {
@@ -210,6 +214,16 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
210
214
  } as TPresence;
211
215
  const validated = this._presence ? this._presence.parse(presence) : presence;
212
216
 
217
+ /**
218
+ * !HACK
219
+ * @description We're patching internal metadata back into the presence field so it doesn't
220
+ * get stripped from validation
221
+ * @date May 13, 2025
222
+ */
223
+ if (!!presence[PLUV_PRESENCE_META_KEY]) {
224
+ (validated as any)[PLUV_PRESENCE_META_KEY] = presence[PLUV_PRESENCE_META_KEY];
225
+ }
226
+
213
227
  this._others.set(clientId, { ...other, presence: validated });
214
228
 
215
229
  return validated;
@@ -0,0 +1,66 @@
1
+ import type {
2
+ Id,
3
+ IOLike,
4
+ JsonObject,
5
+ OtherSubscriptionCallback,
6
+ OthersSubscriptionCallback,
7
+ OthersSubscriptionEvent,
8
+ UserInfo,
9
+ } from "@pluv/types";
10
+ import type { Subject } from "wonka";
11
+ import { makeSubject, subscribe, TypeOfSource } from "wonka";
12
+
13
+ export class UsersNotifier<TIO extends IOLike, TPresence extends Record<string, any> = {}> {
14
+ public readonly others = makeSubject<{
15
+ others: readonly Id<UserInfo<TIO, TPresence>>[];
16
+ event: OthersSubscriptionEvent<TIO, TPresence>;
17
+ }>();
18
+
19
+ private _otherSubjects = new Map<
20
+ [clientId: string][0],
21
+ Subject<Id<UserInfo<TIO, TPresence>> | null>
22
+ >();
23
+
24
+ public clear(): void {
25
+ this._otherSubjects.forEach((subject) => {
26
+ subject.next(null);
27
+ });
28
+
29
+ this._otherSubjects.clear();
30
+ }
31
+
32
+ public delete(clientId: string): void {
33
+ this.other(clientId).next(null);
34
+ this._otherSubjects.delete(clientId);
35
+ }
36
+
37
+ public other(clientId: string): Subject<Id<UserInfo<TIO, TPresence>> | null> {
38
+ const subject = this._otherSubjects.get(clientId);
39
+
40
+ if (subject) return subject;
41
+
42
+ const newSubject = makeSubject<Id<UserInfo<TIO, TPresence>>>();
43
+
44
+ this._otherSubjects.set(clientId, newSubject);
45
+
46
+ return newSubject;
47
+ }
48
+
49
+ public subscribeOther(
50
+ clientId: string,
51
+ callback: OtherSubscriptionCallback<TIO, TPresence>,
52
+ ): () => void {
53
+ const source = this.other(clientId).source;
54
+ const subscription = subscribe(callback)(source);
55
+
56
+ return subscription.unsubscribe;
57
+ }
58
+
59
+ public subscribeOthers(callback: OthersSubscriptionCallback<TIO, TPresence>): () => void {
60
+ const subscription = subscribe<TypeOfSource<typeof this.others.source>>(
61
+ ({ others, event }) => callback(others, event),
62
+ )(this.others.source);
63
+
64
+ return subscription.unsubscribe;
65
+ }
66
+ }
package/src/constants.ts CHANGED
@@ -1 +1,2 @@
1
1
  export const MAX_PRESENCE_SIZE_BYTES = 512;
2
+ export const PLUV_PRESENCE_META_KEY = "__pluv";
@@ -5,8 +5,8 @@ import { PluvClient } from "./PluvClient";
5
5
 
6
6
  export const createClient = <
7
7
  TIO extends IOLike<any, any>,
8
- TPresence extends JsonObject = {},
9
- TCrdt extends AbstractCrdtDocFactory<any> = NoopCrdtDocFactory,
8
+ TPresence extends Record<string, any> = {},
9
+ TCrdt extends AbstractCrdtDocFactory<any, any> = NoopCrdtDocFactory,
10
10
  TMetadata extends JsonObject = {},
11
11
  >(
12
12
  options: PluvClientOptions<TIO, TPresence, TCrdt, TMetadata>,
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export type {
9
9
  MergeEvents,
10
10
  RoomLike,
11
11
  UserInfo,
12
+ WebSocketConnection,
12
13
  WebSocketState,
13
14
  } from "@pluv/types";
14
15
  export { AbstractStorageStore } from "./AbstractStorageStore";
@@ -32,4 +33,4 @@ export type {
32
33
  export type { PluvRouter, PluvRouterEventConfig } from "./PluvRouter";
33
34
  export { register } from "./register";
34
35
  export type { RegisterParams } from "./register";
35
- export type { InferMetadata, PublicKey, PublicKeyParams, WebSocketConnection } from "./types";
36
+ export type { InferMetadata, PublicKey, PublicKeyParams } from "./types";
package/src/types.ts CHANGED
@@ -1,6 +1,4 @@
1
- import type { CrdtType } from "@pluv/crdt";
2
1
  import type {
3
- ConnectionState,
4
2
  CrdtDocLike,
5
3
  EventRecord,
6
4
  Id,
@@ -19,22 +17,22 @@ export interface AuthorizationState<TIO extends IOLike> {
19
17
  }
20
18
 
21
19
  export type EventResolver<
22
- TIO extends IOLike = IOLike,
23
- TInput extends JsonObject = {},
24
- TOutput extends EventRecord<string, any> = {},
25
- TPresence extends JsonObject = {},
26
- TStorage extends Record<string, CrdtType<any, any>> = {},
20
+ TIO extends IOLike,
21
+ TInput extends JsonObject,
22
+ TOutput extends EventRecord<string, any>,
23
+ TPresence extends Record<string, any>,
24
+ TDocLike extends CrdtDocLike<any, any>,
27
25
  > = (
28
26
  data: TInput,
29
- context: EventResolverContext<TIO, TPresence, TStorage>,
27
+ context: EventResolverContext<TIO, TPresence, TDocLike>,
30
28
  ) => MaybePromise<TOutput | void>;
31
29
 
32
30
  export interface EventResolverContext<
33
- TIO extends IOLike = IOLike,
34
- TPresence extends JsonObject = {},
35
- TStorage extends Record<string, CrdtType<any, any>> = {},
31
+ TIO extends IOLike,
32
+ TPresence extends Record<string, any>,
33
+ TDocLike extends CrdtDocLike<any, any>,
36
34
  > {
37
- doc: CrdtDocLike<TStorage>;
35
+ doc: TDocLike;
38
36
  others: readonly UserInfo<TIO, TPresence>[];
39
37
  room: string;
40
38
  user: UserInfo<TIO, TPresence>;
@@ -76,25 +74,6 @@ export type PublicKey<TMetadata extends JsonObject> =
76
74
  | string
77
75
  | ((params: PublicKeyParams<TMetadata>) => string);
78
76
 
79
- export interface WebSocketConnection {
80
- /**
81
- * @description How many times a connection attempt was made. This will increment upon each
82
- * unsuccessful attempt, and will reset upon a successful connection.
83
- * @date April 13, 2025
84
- */
85
- attempts: number;
86
- /**
87
- * @description How many times the user has connected to the room. This can be more than 1 in
88
- * the case of reconnects. This is tracked because on the very first connection (i.e. when
89
- * count is 0) we don't want to send the storage state as an update to the room and overwrite
90
- * the upstream state.
91
- * @date April 13, 2025
92
- */
93
- count: number;
94
- id: string | null;
95
- state: ConnectionState;
96
- }
97
-
98
77
  export type WithMetadata<TMetadata extends JsonObject = {}> = keyof TMetadata extends never
99
78
  ? { metadata?: undefined }
100
79
  : { metadata: TMetadata };
@@ -1,50 +0,0 @@
1
- import type {
2
- Id,
3
- IOLike,
4
- JsonObject,
5
- OtherNotifierSubscriptionCallback,
6
- UserInfo,
7
- } from "@pluv/types";
8
- import type { Subject } from "wonka";
9
- import { makeSubject, subscribe } from "wonka";
10
-
11
- export class OtherNotifier<TIO extends IOLike, TPresence extends JsonObject = {}> {
12
- private _subjects = new Map<
13
- [clientId: string][0],
14
- Subject<Id<UserInfo<TIO, TPresence>> | null>
15
- >();
16
-
17
- public clear(): void {
18
- this._subjects.forEach((subject) => {
19
- subject.next(null);
20
- });
21
-
22
- this._subjects.clear();
23
- }
24
-
25
- public delete(clientId: string): void {
26
- this.subject(clientId).next(null);
27
- this._subjects.delete(clientId);
28
- }
29
-
30
- public subject(clientId: string): Subject<Id<UserInfo<TIO, TPresence>> | null> {
31
- const subject = this._subjects.get(clientId);
32
-
33
- if (subject) return subject;
34
-
35
- const newSubject = makeSubject<Id<UserInfo<TIO, TPresence>>>();
36
-
37
- this._subjects.set(clientId, newSubject);
38
-
39
- return newSubject;
40
- }
41
-
42
- public subscribe(
43
- clientId: string,
44
- callback: OtherNotifierSubscriptionCallback<TIO, TPresence>,
45
- ): () => void {
46
- const source = this.subject(clientId).source;
47
-
48
- return subscribe(callback)(source).unsubscribe;
49
- }
50
- }