@pluv/client 5.0.1 → 5.0.3

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 DELETED
@@ -1,71 +0,0 @@
1
- import type { AbstractCrdtDocFactory } from "@pluv/crdt";
2
- import type { IOLike, IORouterLike, JsonObject } from "@pluv/types";
3
- import type { PluvProcedure } from "./PluvProcedure";
4
-
5
- export type PluvRouterEventConfig<
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
-
11
- export type MergedRouter<
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
- > = TRouters extends [
18
- infer IHead extends PluvRouter<TIO, TPresence, TCrdt, any>,
19
- ...infer ITail extends PluvRouter<TIO, TPresence, TCrdt, any>[],
20
- ]
21
- ? MergedRouter<ITail, TIO, TPresence, TCrdt, TRoot & IHead["_defs"]["events"]>
22
- : PluvRouter<TIO, TPresence, TCrdt, TRoot>;
23
-
24
- export class PluvRouter<
25
- TIO extends IOLike,
26
- TPresence extends Record<string, any>,
27
- TCrdt extends AbstractCrdtDocFactory<any, any>,
28
- TEvents extends PluvRouterEventConfig<TIO, TPresence, TCrdt> = {},
29
- > implements IORouterLike<TEvents> {
30
- readonly _defs: { events: TEvents } = { events: {} as TEvents };
31
-
32
- constructor(events: TEvents) {
33
- const invalidName = Object.keys(events).find((name) => !this._isValidEventName(name));
34
-
35
- if (typeof invalidName === "string") {
36
- throw new Error(
37
- `Invalid event name. Event names must be formatted as valid JavaScript variable names: "${invalidName}"`,
38
- );
39
- }
40
-
41
- this._defs = { events };
42
- }
43
-
44
- public static merge<
45
- TRouters extends PluvRouter<TIO, TPresence, TCrdt, any>[],
46
- TIO extends IOLike,
47
- TPresence extends Record<string, any>,
48
- TCrdt extends AbstractCrdtDocFactory<any, any>,
49
- >(...routers: TRouters): MergedRouter<TRouters, TIO, TPresence, TCrdt, {}> {
50
- const events = Object.assign(
51
- Object.create(null),
52
- ...routers.map((router) => router._defs.events),
53
- );
54
-
55
- if (Object.keys(events).some((key) => key.startsWith("$"))) {
56
- throw new Error('Procedures may not start with "$"');
57
- }
58
-
59
- return new PluvRouter<any, any, any, any>(events) as MergedRouter<
60
- TRouters,
61
- TIO,
62
- TPresence,
63
- TCrdt,
64
- {}
65
- >;
66
- }
67
-
68
- private _isValidEventName(name: string): boolean {
69
- return /^[a-z_$][a-z0-9_$]*$/gi.test(name);
70
- }
71
- }
@@ -1,41 +0,0 @@
1
- import type {
2
- Id,
3
- IOLike,
4
- JsonObject,
5
- StateNotifierSubjects,
6
- SubscriptionCallback,
7
- UserInfo,
8
- WebSocketState,
9
- } from "@pluv/types";
10
- import type { Subject } from "wonka";
11
- import { makeSubject, subscribe } from "wonka";
12
-
13
- type InferSubjectValue<
14
- TIO extends IOLike,
15
- TPresence extends Record<string, any>,
16
- TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
17
- > =
18
- StateNotifierSubjects<TIO, TPresence>[TSubject] extends Subject<infer IValue>
19
- ? Id<IValue>
20
- : never;
21
-
22
- export class StateNotifier<TIO extends IOLike, TPresence extends Record<string, any> = {}> {
23
- public subjects: StateNotifierSubjects<TIO, TPresence> = {
24
- connection: makeSubject<Id<WebSocketState<TIO>>>(),
25
- "my-presence": makeSubject<TPresence>(),
26
- myself: makeSubject<Readonly<Id<UserInfo<TIO, TPresence>>> | null>(),
27
- others: makeSubject<readonly Id<UserInfo<TIO, TPresence>>[]>(),
28
- "storage-loaded": makeSubject<true>(),
29
- };
30
-
31
- public subscribe<TSubject extends keyof StateNotifierSubjects<TIO, TPresence>>(
32
- name: TSubject,
33
- callback: SubscriptionCallback<TIO, TPresence, TSubject>,
34
- ): () => void {
35
- const source = this.subjects[name]?.source as any;
36
-
37
- if (!source) throw new Error(`Could not subscribe to source: ${name}`);
38
-
39
- return subscribe(callback)(source).unsubscribe;
40
- }
41
- }
@@ -1,31 +0,0 @@
1
- import { AbstractStorageStore } from "./AbstractStorageStore";
2
-
3
- export class StorageStore extends AbstractStorageStore {
4
- public addUpdate() {
5
- return Promise.resolve();
6
- }
7
-
8
- public applyUpdates() {
9
- return Promise.resolve();
10
- }
11
-
12
- public destroy() {
13
- return Promise.resolve();
14
- }
15
-
16
- public flatten() {
17
- return Promise.resolve();
18
- }
19
-
20
- public getShouldFlatten() {
21
- return Promise.resolve(false);
22
- }
23
-
24
- public getUpdates() {
25
- return Promise.resolve([]);
26
- }
27
-
28
- public initialize() {
29
- return Promise.resolve();
30
- }
31
- }
@@ -1,341 +0,0 @@
1
- import type { InputZodLike, IOLike, JsonObject, OptionalProps, UserInfo } from "@pluv/types";
2
- import { PLUV_PRESENCE_META_KEY } from "./constants";
3
- import type { PluvClientLimits } from "./types";
4
- import { pickBy } from "./utils";
5
-
6
- export type Presence = Record<string, unknown>;
7
-
8
- export type UsersManagerConfig<TPresence extends Record<string, any> = {}> = {
9
- initialPresence?: TPresence;
10
- limits: PluvClientLimits;
11
- presence?: InputZodLike<TPresence>;
12
- };
13
-
14
- export type AddConnectionResult<TIO extends IOLike, TPresence extends Record<string, any> = {}> = {
15
- clientId: string;
16
- data: UserInfo<TIO, TPresence>;
17
- isMyself: boolean;
18
- remaining: number;
19
- };
20
-
21
- export type DeleteConnectionResult<
22
- TIO extends IOLike,
23
- TPresence extends Record<string, any> = {},
24
- > = {
25
- clientId: string;
26
- data: UserInfo<TIO, TPresence>;
27
- remaining: number;
28
- };
29
-
30
- export class UsersManager<TIO extends IOLike, TPresence extends Record<string, any> = {}> {
31
- public readonly initialPresence: TPresence;
32
-
33
- private readonly _idMap = {
34
- fromConnectionId: new Map<[connectionId: string][0], [clientId: string][0]>(),
35
- fromClientId: new Map<[clientId: string][0], Set<[connectionId: string][0]>>(),
36
- };
37
- private _limits: PluvClientLimits;
38
- /**
39
- * @description This presence can be updated while the user is not
40
- * connected.
41
- */
42
- private _myPresence: TPresence;
43
- /**
44
- * @description This is only set when the user is connected.
45
- */
46
- private _myself: UserInfo<TIO, TPresence> | null = null;
47
- private _others = new Map<[clientId: string][0], UserInfo<TIO, TPresence>>();
48
- private _presence: InputZodLike<TPresence> | null = null;
49
-
50
- constructor(config: UsersManagerConfig<TPresence>) {
51
- const { initialPresence, limits, presence = null } = config;
52
-
53
- this.initialPresence = initialPresence as TPresence;
54
-
55
- this._limits = limits;
56
- this._myPresence = initialPresence as TPresence;
57
- this._presence = presence;
58
- }
59
-
60
- public get myPresence(): TPresence {
61
- return this._myPresence;
62
- }
63
-
64
- public get myself(): UserInfo<TIO, TPresence> | null {
65
- return this._myself;
66
- }
67
-
68
- public addConnection(
69
- userInfo: OptionalProps<UserInfo<TIO, TPresence>, "presence">,
70
- ): AddConnectionResult<TIO, TPresence> {
71
- /**
72
- * @description Ensure that the presence is complete, so that it does not fail schema
73
- * validation
74
- * @date April 20, 2025
75
- */
76
- const cleaned = pickBy(userInfo.presence ?? {}, (value) => typeof value !== "undefined");
77
- const presence = { ...this.initialPresence, ...cleaned };
78
- const data: UserInfo<TIO, TPresence> = { ...userInfo, presence };
79
-
80
- const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
81
- const clientId = this.getClientId(data);
82
- const remaining = this._setConnectionId(userInfo.connectionId, clientId).size;
83
-
84
- if (myClientId === clientId) {
85
- const result: AddConnectionResult<TIO, TPresence> = {
86
- clientId,
87
- data,
88
- isMyself: true,
89
- remaining,
90
- };
91
-
92
- return result;
93
- }
94
-
95
- const other = this._others.get(clientId);
96
-
97
- /**
98
- * !HACK
99
- * @description Don't override the other user, so that we ensure the user's presence isn't
100
- * overwritten by whatever incoming connection.
101
- * @date April 16, 2025
102
- */
103
- if (!other) this._others.set(clientId, data);
104
-
105
- const result: AddConnectionResult<TIO, TPresence> = {
106
- clientId,
107
- data,
108
- isMyself: false,
109
- remaining,
110
- };
111
-
112
- return result;
113
- }
114
-
115
- public clearConnections(): void {
116
- this.removeMyself();
117
- this._others.clear();
118
- this._idMap.fromClientId.clear();
119
- this._idMap.fromConnectionId.clear();
120
- }
121
-
122
- public deleteConnection(connectionId: string): DeleteConnectionResult<TIO, TPresence> | null {
123
- const clientId = this.getClientId(connectionId);
124
-
125
- this._deleteConnectionId(connectionId);
126
-
127
- // We expect that a clientId should always exist here, but we'll just check this anyways
128
- if (!clientId) return null;
129
-
130
- const userInfo = this._others.get(clientId) ?? null;
131
-
132
- // We also expect this should always exist here, but we'll just check this anyways
133
- if (!userInfo) return null;
134
-
135
- const remaining = this._getConnectionIds(clientId)?.size ?? 0;
136
- const result: DeleteConnectionResult<TIO, TPresence> = {
137
- clientId,
138
- remaining,
139
- data: userInfo,
140
- };
141
-
142
- /**
143
- * @description A user may have multiple websocket connections open. If one connection is
144
- * dropped, the user may still be connected via another websocket (e.g. on anther browser
145
- * tab). In this case, we just want to remove the connection mapping for the connection
146
- * that is dropped, but keep the remaining connections and the user.
147
- * @date April 16, 2025
148
- */
149
- if (!remaining) this._others.delete(clientId);
150
-
151
- return result;
152
- }
153
-
154
- /**
155
- * @description The client id is either the user's id (if authorized) or the connection id
156
- * otherwise. This is so that, despite having multiple connections, the user will only have one
157
- * presence to all other users.
158
- * @date April 16, 2025
159
- */
160
- public getClientId(connectionId: string): string | null;
161
- public getClientId(userInfo: UserInfo<TIO, TPresence>): string;
162
- public getClientId(input: string | UserInfo<TIO, TPresence>): string | null {
163
- if (typeof input !== "string") {
164
- const userInfo = input;
165
- const connectionId = userInfo.connectionId;
166
- const userId = userInfo.user?.id ?? null;
167
-
168
- return userId ?? connectionId;
169
- }
170
-
171
- const connectionId = input;
172
-
173
- if (this._myself?.connectionId === connectionId) {
174
- return this.getClientId(this._myself);
175
- }
176
-
177
- const clientId = this._idMap.fromConnectionId.get(connectionId) ?? null;
178
-
179
- return clientId;
180
- }
181
-
182
- public getOther(connectionId: string): UserInfo<TIO, TPresence> | null {
183
- const clientId = this.getClientId(connectionId);
184
-
185
- if (!clientId) return null;
186
-
187
- return this._others.get(clientId) ?? null;
188
- }
189
-
190
- public getOthers(): readonly UserInfo<TIO, TPresence>[] {
191
- return Array.from(this._others.values());
192
- }
193
-
194
- public patchPresence(connectionId: string, patch: Partial<TPresence>): TPresence | null {
195
- const clientId = this.getClientId(connectionId);
196
- const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
197
-
198
- if (!clientId) return null;
199
- if (myClientId === clientId) return this.updateMyPresence(patch);
200
-
201
- const other = this._others.get(clientId);
202
-
203
- if (!other) return null;
204
-
205
- const cleanedPatch = pickBy(patch ?? {}, (value) => typeof value !== "undefined");
206
- const cleanedPresence = pickBy(
207
- other.presence ?? {},
208
- (value) => typeof value !== "undefined",
209
- );
210
- const presence = {
211
- ...this.initialPresence,
212
- ...cleanedPresence,
213
- ...cleanedPatch,
214
- } as TPresence;
215
- const validated = this._presence ? this._presence.parse(presence) : presence;
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
-
227
- this._others.set(clientId, { ...other, presence: validated });
228
-
229
- return validated;
230
- }
231
-
232
- public removeMyself(): void {
233
- if (!this._myself) return;
234
-
235
- const connectionId = this._myself.connectionId;
236
-
237
- this._deleteConnectionId(connectionId);
238
- this._myself = null;
239
- }
240
-
241
- public setMyself(userInfo: OptionalProps<UserInfo<TIO, TPresence>, "presence">): void {
242
- const connectionId = userInfo.connectionId;
243
- const presence = userInfo.presence ?? this.initialPresence;
244
-
245
- this._myself = { ...userInfo, presence };
246
- this._myPresence = presence;
247
-
248
- const clientId = this.getClientId(this._myself);
249
-
250
- this._setConnectionId(connectionId, clientId);
251
- }
252
-
253
- public setPresence(connectionId: string, presence: TPresence): void {
254
- const clientId = this.getClientId(connectionId);
255
- const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
256
-
257
- if (!clientId) return;
258
-
259
- if (myClientId === clientId) {
260
- if (!this._myself) return;
261
-
262
- this._myself.presence = presence;
263
- this._myPresence = presence;
264
-
265
- return;
266
- }
267
-
268
- const other = this._others.get(clientId);
269
-
270
- if (!other) return;
271
-
272
- this._others.set(clientId, { ...other, presence });
273
- }
274
-
275
- /**
276
- * @description This method need not care about being connected. This is because a user should
277
- * be able to view and update their own presence without being online
278
- * @date April 20, 2025
279
- */
280
- public updateMyPresence(patch: Partial<TPresence>): TPresence {
281
- const cleanedPatch = pickBy(patch, (value) => typeof value !== "undefined");
282
- const cleanedPresence = pickBy(this._myPresence, (value) => typeof value !== "undefined");
283
- const updated = {
284
- ...this.initialPresence,
285
- ...cleanedPresence,
286
- ...cleanedPatch,
287
- } as TPresence;
288
- const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
289
-
290
- if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
291
- throw new Error(
292
- `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`,
293
- );
294
- }
295
-
296
- this._myPresence = updated;
297
-
298
- if (!this._myself) return updated;
299
-
300
- this._myself.presence = updated;
301
-
302
- return updated;
303
- }
304
-
305
- private _deleteConnectionId(connectionId: string): void {
306
- const clientId = this.getClientId(connectionId);
307
-
308
- this._idMap.fromConnectionId.delete(connectionId);
309
-
310
- if (!clientId) return;
311
-
312
- const set = this._idMap.fromClientId.get(clientId) ?? null;
313
-
314
- if (!set) return;
315
-
316
- set.delete(connectionId);
317
-
318
- if (!!set.size) return;
319
-
320
- this._idMap.fromClientId.delete(clientId);
321
- }
322
-
323
- private _getConnectionIds(clientId: string): Set<[connectionId: string][0]> | null {
324
- const set = this._idMap.fromClientId.get(clientId);
325
-
326
- return set ?? null;
327
- }
328
-
329
- private _setConnectionId(
330
- connectionId: string,
331
- clientId: string,
332
- ): Set<[connectionId: string][0]> {
333
- const set = this._idMap.fromClientId.get(clientId) ?? new Set<string>();
334
- const updated = set.add(connectionId);
335
-
336
- this._idMap.fromConnectionId.set(connectionId, clientId);
337
- this._idMap.fromClientId.set(clientId, updated);
338
-
339
- return updated;
340
- }
341
- }
@@ -1,66 +0,0 @@
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 DELETED
@@ -1,2 +0,0 @@
1
- export const MAX_PRESENCE_SIZE_BYTES = 512;
2
- export const PLUV_PRESENCE_META_KEY = "__pluv";
@@ -1,14 +0,0 @@
1
- import type { InferIOCrdtKind, IOLike, JsonObject } from "@pluv/types";
2
- import type { PluvClientOptions } from "./PluvClient";
3
- import { PluvClient } from "./PluvClient";
4
-
5
- export const createClient = <
6
- TIO extends IOLike<any, any, any>,
7
- TPresence extends Record<string, any> = {},
8
- TCrdt extends InferIOCrdtKind<TIO> = InferIOCrdtKind<TIO>,
9
- TMetadata extends JsonObject = {},
10
- >(
11
- options: PluvClientOptions<TIO, TPresence, TCrdt, TMetadata>,
12
- ): PluvClient<TIO, TPresence, TCrdt, TMetadata> => {
13
- return new PluvClient<TIO, TPresence, TCrdt, TMetadata>(options);
14
- };
package/src/index.ts DELETED
@@ -1,36 +0,0 @@
1
- export type { CrdtType, InferCrdtJson } from "@pluv/crdt";
2
- export { ConnectionState } from "@pluv/types";
3
- export type {
4
- BaseUser,
5
- EventMessage,
6
- EventNotifierSubscriptionCallback,
7
- EventRecord,
8
- IOEventMessage,
9
- MergeEvents,
10
- RoomLike,
11
- UserInfo,
12
- WebSocketConnection,
13
- WebSocketState,
14
- } from "@pluv/types";
15
- export { AbstractStorageStore } from "./AbstractStorageStore";
16
- export { createClient } from "./createClient";
17
- export { infer } from "./infer";
18
- export type { InferCallback } from "./infer";
19
- export { MockedRoom } from "./MockedRoom";
20
- export type { MockedRoomConfig, MockedRoomEvents } from "./MockedRoom";
21
- export { PluvClient } from "./PluvClient";
22
- export type { CreateRoomOptions, EnterRoomParams, PluvClientOptions } from "./PluvClient";
23
- export type { PluvProcedureConfig } from "./PluvProcedure";
24
- export { PluvRoom } from "./PluvRoom";
25
- export type {
26
- PluvRoomAddon,
27
- PluvRoomAddonInput,
28
- PluvRoomAddonResult,
29
- PluvRoomDebug,
30
- RoomConnectParams,
31
- RoomEndpoints,
32
- } from "./PluvRoom";
33
- export type { PluvRouter, PluvRouterEventConfig } from "./PluvRouter";
34
- export { register } from "./register";
35
- export type { RegisterParams } from "./register";
36
- export type { InferMetadata, PublicKey, PublicKeyParams } from "./types";
package/src/infer.ts DELETED
@@ -1,12 +0,0 @@
1
- import type { InferIOAuthorize, InferIOCrdt, InferIOEvents, IOLike } from "@pluv/types";
2
- import type { identity } from "./utils";
3
-
4
- export type InferCallback<TIO extends IOLike<any, any, any>> = (i: typeof identity) => {
5
- io: (io: TIO) => TIO;
6
- };
7
-
8
- export const infer = <TIO extends IOLike<any, any, any>>(
9
- callback: InferCallback<TIO>,
10
- ): InferCallback<IOLike<InferIOAuthorize<TIO>, InferIOCrdt<TIO>, InferIOEvents<TIO>>> => {
11
- return callback as any;
12
- };
package/src/register.ts DELETED
@@ -1,20 +0,0 @@
1
- import type { JsonObject } from "@pluv/types";
2
- import type { AuthEndpoint, WsEndpoint } from "./PluvRoom";
3
-
4
- export interface RegisterParams<TMetadata extends JsonObject> {
5
- authEndpoint: AuthEndpoint<TMetadata>;
6
- publicKey: string;
7
- }
8
-
9
- export const register = <TMetadata extends JsonObject>(params: RegisterParams<TMetadata>) => {
10
- const { authEndpoint, publicKey } = params;
11
-
12
- const wsEndpoint: WsEndpoint<TMetadata> = ({ room }) =>
13
- `wss://connect.pluv.io/api/room?room=${room}`;
14
-
15
- return {
16
- authEndpoint,
17
- publicKey,
18
- wsEndpoint,
19
- };
20
- };