@pluv/io 1.0.0 → 1.0.2

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/PluvServer.ts CHANGED
@@ -1,8 +1,19 @@
1
1
  import type { AbstractCrdtDocFactory } from "@pluv/crdt";
2
2
  import { noop } from "@pluv/crdt";
3
- import type { IOLike, Id, InferIOAuthorize, InferIOAuthorizeUser, JsonObject, Maybe } from "@pluv/types";
3
+ import type {
4
+ IOLike,
5
+ Id,
6
+ InferIOAuthorize,
7
+ InferIOAuthorizeUser,
8
+ JsonObject,
9
+ Maybe,
10
+ } from "@pluv/types";
4
11
  import colors from "kleur";
5
- import type { AbstractPlatform, InferInitContextType, InferRoomContextType } from "./AbstractPlatform";
12
+ import type {
13
+ AbstractPlatform,
14
+ InferInitContextType,
15
+ InferRoomContextType,
16
+ } from "./AbstractPlatform";
6
17
  import type { IORoomListeners } from "./IORoom";
7
18
  import { IORoom } from "./IORoom";
8
19
  import type { PluvIO } from "./PluvIO";
@@ -29,7 +40,11 @@ export type InferIORoom<TServer extends PluvServer<any, any, any, any>> =
29
40
 
30
41
  export type PluvServerConfig<
31
42
  TPlatform extends AbstractPlatform<any, any> = AbstractPlatform<any, any>,
32
- TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null = any,
43
+ TAuthorize extends PluvIOAuthorize<
44
+ TPlatform,
45
+ any,
46
+ InferInitContextType<TPlatform>
47
+ > | null = any,
33
48
  TContext extends Record<string, any> = {},
34
49
  TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
35
50
  > = Partial<PluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>> & {
@@ -60,11 +75,20 @@ export type CreateRoomOptions<
60
75
  TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>,
61
76
  > = keyof InferRoomContextType<TPlatform> extends never
62
77
  ? [BaseCreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>] | []
63
- : [Id<BaseCreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents> & InferRoomContextType<TPlatform>>];
78
+ : [
79
+ Id<
80
+ BaseCreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents> &
81
+ InferRoomContextType<TPlatform>
82
+ >,
83
+ ];
64
84
 
65
85
  export class PluvServer<
66
86
  TPlatform extends AbstractPlatform<any, any> = AbstractPlatform<any, any>,
67
- TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null = any,
87
+ TAuthorize extends PluvIOAuthorize<
88
+ TPlatform,
89
+ any,
90
+ InferInitContextType<TPlatform>
91
+ > | null = any,
68
92
  TContext extends Record<string, any> = {},
69
93
  TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {},
70
94
  > implements IOLike<TAuthorize, TEvents>
@@ -126,17 +150,56 @@ export class PluvServer<
126
150
  })
127
151
  .self(async (data, { context, doc, room, session }) => {
128
152
  const oldState = await this._platform.persistence.getStorageState(room);
153
+ /**
154
+ * !HACK
155
+ * @description This is the frontend's initialStorage. We only want to
156
+ * apply this if the server's storage is empty (i.e. no initial storage
157
+ * has been applied)
158
+ */
129
159
  const update = (data as any)?.update as Maybe<string>;
130
160
 
131
- const updates: readonly Maybe<string>[] = [
132
- oldState ? null : await this._getInitialStorage?.({ context, room }),
133
- update,
134
- ];
161
+ if (!oldState) {
162
+ const loadedState = await this._getInitialStorage?.({ context, room });
163
+
164
+ if (!!loadedState) {
165
+ doc.applyEncodedState({ update: loadedState }).getEncodedState();
166
+ }
167
+ }
168
+
169
+ /**
170
+ * @description Storage was already initialized. Don't overwrite the current
171
+ * storage state with the incoming initial storage. Return what the current state
172
+ * is without changes.
173
+ * @date May 7, 2025
174
+ */
175
+ if (!doc.isEmpty()) {
176
+ const encodedState = doc.getEncodedState();
135
177
 
136
- if (updates.some((_update) => !!_update)) {
137
- const encodedState = doc.batchApplyEncodedState({ updates }).getEncodedState();
178
+ return { $storageReceived: { changeKind: "unchanged", state: encodedState } };
179
+ }
138
180
 
139
- await this._platform.persistence.setStorageState(room, encodedState);
181
+ /**
182
+ * @description Storage was never initialized, and there is an incoming
183
+ * initialStorage from the client. Apply the initialStorage and persist the state
184
+ * as an update.
185
+ * @date May 7, 2025
186
+ */
187
+ if (!!update) {
188
+ const encodedState = doc.applyEncodedState({ update }).getEncodedState();
189
+ const storageSize = new TextEncoder().encode(encodedState).length;
190
+
191
+ if (
192
+ !!this._limits.storageMaxSize &&
193
+ storageSize > this._limits.storageMaxSize
194
+ ) {
195
+ throw new Error("Storage has exceeded the size limit");
196
+ }
197
+
198
+ await this._platform.persistence
199
+ .setStorageState(room, encodedState)
200
+ .catch((error) => {
201
+ this._logDebug(error);
202
+ });
140
203
 
141
204
  this._getListeners().onStorageUpdated({
142
205
  context,
@@ -145,16 +208,13 @@ export class PluvServer<
145
208
  user: session?.user,
146
209
  webSocket: session?.webSocket.webSocket,
147
210
  });
148
- }
149
-
150
- const encodedState = (await this._platform.persistence.getStorageState(room)) ?? doc.getEncodedState();
151
- const storageSize = new TextEncoder().encode(encodedState).length;
152
211
 
153
- if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
154
- throw new Error("Storage has exceeded the size limit");
212
+ return { $storageReceived: { changeKind: "initialized", state: encodedState } };
155
213
  }
156
214
 
157
- return { $storageReceived: { state: encodedState } };
215
+ const encodedState = doc.getEncodedState();
216
+
217
+ return { $storageReceived: { changeKind: "empty", state: encodedState } };
158
218
  }),
159
219
  $ping: this._procedure.self((data, { session }) => {
160
220
  if (!session) return {};
@@ -222,7 +282,10 @@ export class PluvServer<
222
282
  return { $storageUpdated: { state: encodedState } };
223
283
  }),
224
284
  });
225
- private readonly _context: PluvContext<TPlatform, TContext> = {} as PluvContext<TPlatform, TContext>;
285
+ private readonly _context: PluvContext<TPlatform, TContext> = {} as PluvContext<
286
+ TPlatform,
287
+ TContext
288
+ >;
226
289
  private readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any> };
227
290
  private readonly _debug: boolean;
228
291
  private readonly _getInitialStorage: GetInitialStorageFn<TContext> | null = null;
@@ -233,7 +296,8 @@ export class PluvServer<
233
296
 
234
297
  public get fetch(): (...args: any[]) => Promise<any> {
235
298
  return ((...args: any[]): Promise<any> => {
236
- if (!this._platform._fetch) throw new Error(`\`${this._platform._name}\` does not support \`fetch\``);
299
+ if (!this._platform._fetch)
300
+ throw new Error(`\`${this._platform._name}\` does not support \`fetch\``);
237
301
 
238
302
  return this._platform._fetch(...args);
239
303
  }) as NonNullable<typeof this._platform._fetch>;
@@ -275,8 +339,13 @@ export class PluvServer<
275
339
  router = new PluvRouter<TPlatform, TAuthorize, TContext, TEvents>({} as TEvents),
276
340
  } = options;
277
341
 
278
- const { onRoomDeleted, onRoomMessage, onStorageUpdated, onUserConnected, onUserDisconnected } =
279
- options as Partial<BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>>;
342
+ const {
343
+ onRoomDeleted,
344
+ onRoomMessage,
345
+ onStorageUpdated,
346
+ onUserConnected,
347
+ onUserDisconnected,
348
+ } = options as Partial<BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>>;
280
349
 
281
350
  platform.validateConfig(options);
282
351
 
@@ -285,12 +354,9 @@ export class PluvServer<
285
354
  this._io = io;
286
355
  this._limits = limits;
287
356
  this._platform = platform;
288
- this._router = (router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter) as PluvRouter<
289
- TPlatform,
290
- TAuthorize,
291
- TContext,
292
- TEvents
293
- >;
357
+ this._router = (
358
+ router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter
359
+ ) as PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
294
360
 
295
361
  if (authorize) this._authorize = authorize;
296
362
  if (context) this._context = context;
@@ -309,21 +375,19 @@ export class PluvServer<
309
375
  room: string,
310
376
  ...options: CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>
311
377
  ): IORoom<TPlatform, TAuthorize, TContext, TEvents> {
312
- const { _meta, debug, onDestroy, onMessage, ...platformRoomContext } = (options[0] ?? {}) as CreateRoomOptions<
313
- TPlatform,
314
- TAuthorize,
315
- TContext,
316
- TEvents
317
- >[0] & { _meta?: any };
378
+ const { _meta, debug, onDestroy, onMessage, ...platformRoomContext } = (options[0] ??
379
+ {}) as CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>[0] & { _meta?: any };
318
380
 
319
381
  if (this._platform._config.handleMode !== "io") {
320
382
  throw new Error(`\`createRoom\` is unsupported for \`${this._platform._name}\``);
321
383
  }
322
384
 
323
- if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room)) throw new Error("Unsupported room name");
385
+ if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room))
386
+ throw new Error("Unsupported room name");
324
387
 
325
388
  const roomContext = platformRoomContext as InferRoomContextType<TPlatform>;
326
- const context: TContext = typeof this._context === "function" ? this._context(roomContext) : this._context;
389
+ const context: TContext =
390
+ typeof this._context === "function" ? this._context(roomContext) : this._context;
327
391
 
328
392
  const newRoom = new IORoom<TPlatform, TAuthorize, TContext, TEvents>(room, {
329
393
  ...(!!_meta ? { _meta } : {}),
package/src/PubSub.ts CHANGED
@@ -20,7 +20,10 @@ export class PubSub extends AbstractPubSub {
20
20
  Promise.resolve(subject.next(payload));
21
21
  }
22
22
 
23
- public subscribe(roomName: string, onMessage: (message: IOPubSubEventMessage<any>) => void): Promise<number> {
23
+ public subscribe(
24
+ roomName: string,
25
+ onMessage: (message: IOPubSubEventMessage<any>) => void,
26
+ ): Promise<number> {
24
27
  const id = this._currentSubscriptionId++;
25
28
  const refs = this._subscriptionsRefs.get(roomName) ?? [];
26
29
 
package/src/authorize.ts CHANGED
@@ -15,7 +15,10 @@ export interface JWT<TUser extends BaseUser> {
15
15
  user: TUser;
16
16
  }
17
17
 
18
- export type JWTEncodeParams<TUser extends BaseUser | null, TPlatform extends AbstractPlatform<any, any>> = {
18
+ export type JWTEncodeParams<
19
+ TUser extends BaseUser | null,
20
+ TPlatform extends AbstractPlatform<any, any>,
21
+ > = {
19
22
  maxAge?: number;
20
23
  room: string;
21
24
  user: TUser extends BaseUser ? TUser : "Error: Cannot create token without authorization!";
package/src/createIO.ts CHANGED
@@ -22,10 +22,18 @@ export const createIO = <
22
22
  TUser extends BaseUser | null = null,
23
23
  >(
24
24
  params: CreateIOParams<TPlatform, TContext, TUser>,
25
- ): PluvIO<TPlatform, PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>, TContext> => {
25
+ ): PluvIO<
26
+ TPlatform,
27
+ PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>,
28
+ TContext
29
+ > => {
26
30
  const { authorize, context, crdt, debug, limits, platform } = params;
27
31
 
28
- return new PluvIO<TPlatform, PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>, TContext>({
32
+ return new PluvIO<
33
+ TPlatform,
34
+ PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>,
35
+ TContext
36
+ >({
29
37
  authorize,
30
38
  context,
31
39
  crdt,
package/src/types.ts CHANGED
@@ -57,7 +57,9 @@ export interface EventResolverContext<
57
57
  garbageCollect: () => Promise<void>;
58
58
  presence: JsonObject | null;
59
59
  room: string;
60
- session: TKind extends "sync" ? WebSocketSession<TAuthorize> | null : WebSocketSession<TAuthorize>;
60
+ session: TKind extends "sync"
61
+ ? WebSocketSession<TAuthorize> | null
62
+ : WebSocketSession<TAuthorize>;
61
63
  sessions: readonly WebSocketSession<TAuthorize>[];
62
64
  time: number;
63
65
  }
@@ -79,11 +81,12 @@ export interface WebSocketSerializedState {
79
81
  timers: WebSocketSessionTimers;
80
82
  }
81
83
 
82
- export type WebSocketSession<TAuthorize extends IOAuthorize<any, any> | null = null> = WebSocketSerializedState & {
83
- id: string;
84
- user: InferIOAuthorizeUser<TAuthorize>;
85
- webSocket: AbstractWebSocket;
86
- };
84
+ export type WebSocketSession<TAuthorize extends IOAuthorize<any, any> | null = null> =
85
+ WebSocketSerializedState & {
86
+ id: string;
87
+ user: InferIOAuthorizeUser<TAuthorize>;
88
+ webSocket: AbstractWebSocket;
89
+ };
87
90
 
88
91
  export type MergeEventRecords<
89
92
  TEventRecords extends EventRecord<string, any>[],
@@ -143,7 +146,9 @@ export type PluvIOAuthorize<
143
146
  TContext extends Record<string, unknown> = {},
144
147
  > =
145
148
  | (TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null)
146
- | ((context: TContext) => TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null);
149
+ | ((
150
+ context: TContext,
151
+ ) => TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null);
147
152
 
148
153
  export interface PluvIOLimits {
149
154
  /**
@@ -184,7 +189,10 @@ export type PluvIOListeners<
184
189
  TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>,
185
190
  > = UndefinedProps<
186
191
  BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>,
187
- Exclude<keyof BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>, InferPlatformListeners<TPlatform>>
192
+ Exclude<
193
+ keyof BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>,
194
+ InferPlatformListeners<TPlatform>
195
+ >
188
196
  >;
189
197
 
190
198
  export type PluvIORouter<
@@ -200,14 +208,20 @@ export type PluvIORouter<
200
208
  export type InferPlatformConfig<TPlatform extends AbstractPlatform<any, any, any, any>> =
201
209
  TPlatform extends AbstractPlatform<any, any, any, infer IConfig> ? IConfig : never;
202
210
 
203
- export type InferPlatformAuthorizeProperties<TPlatform extends AbstractPlatform<any, any, any, any>> = keyof {
204
- [P in keyof PlatformConfig["authorize"] as InferPlatformConfig<TPlatform>["authorize"][P] extends true | undefined
211
+ export type InferPlatformAuthorizeProperties<
212
+ TPlatform extends AbstractPlatform<any, any, any, any>,
213
+ > = keyof {
214
+ [P in keyof PlatformConfig["authorize"] as InferPlatformConfig<TPlatform>["authorize"][P] extends
215
+ | true
216
+ | undefined
205
217
  ? P
206
218
  : never]: true;
207
219
  };
208
220
 
209
221
  export type InferPlatformListeners<TPlatform extends AbstractPlatform<any, any, any, any>> = keyof {
210
- [P in keyof PlatformConfig["listeners"] as InferPlatformConfig<TPlatform>["listeners"][P] extends true | undefined
222
+ [P in keyof PlatformConfig["listeners"] as InferPlatformConfig<TPlatform>["listeners"][P] extends
223
+ | true
224
+ | undefined
211
225
  ? P
212
226
  : never]: true;
213
227
  };
@@ -1,4 +1,7 @@
1
- export const pickBy = <T extends Record<string, any>>(obj: T, by: (value: T[keyof T]) => boolean): Partial<T> => {
1
+ export const pickBy = <T extends Record<string, any>>(
2
+ obj: T,
3
+ by: (value: T[keyof T]) => boolean,
4
+ ): Partial<T> => {
2
5
  const result: Partial<T> = {};
3
6
  const keys: (keyof T)[] = Object.keys(obj);
4
7
 
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '1.0.0'
2
+ export const version = '1.0.2'