@pluv/io 0.41.7 → 0.43.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.
@@ -1,9 +1,9 @@
1
1
 
2
- > @pluv/io@0.41.7 build /home/runner/work/pluv/pluv/packages/io
2
+ > @pluv/io@0.43.0 build /home/runner/work/pluv/pluv/packages/io
3
3
  > pnpm run version && tsup src/index.ts --format esm,cjs --dts
4
4
 
5
5
 
6
- > @pluv/io@0.41.7 version /home/runner/work/pluv/pluv/packages/io
6
+ > @pluv/io@0.43.0 version /home/runner/work/pluv/pluv/packages/io
7
7
  > genversion ./src/version.generated.ts --esm
8
8
 
9
9
  CLI Building entry: src/index.ts
@@ -12,11 +12,11 @@
12
12
  CLI Target: es6
13
13
  ESM Build start
14
14
  CJS Build start
15
- ESM dist/index.mjs 49.44 KB
16
- ESM ⚡️ Build success in 113ms
17
- CJS dist/index.js 51.61 KB
18
- CJS ⚡️ Build success in 116ms
15
+ ESM dist/index.mjs 50.72 KB
16
+ ESM ⚡️ Build success in 150ms
17
+ CJS dist/index.js 52.90 KB
18
+ CJS ⚡️ Build success in 151ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 2933ms
21
- DTS dist/index.d.mts 31.42 KB
22
- DTS dist/index.d.ts 31.42 KB
20
+ DTS ⚡️ Build success in 2855ms
21
+ DTS dist/index.d.mts 32.19 KB
22
+ DTS dist/index.d.ts 32.19 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # @pluv/io
2
2
 
3
+ ## 0.43.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0063e80: **BREAKING** Added a configurable limit for the max-size (in bytes) for storage (defaulting to 30MB). To unset this limit, you can set this limit to `0` or `null`.
8
+
9
+ ```ts
10
+ createIO({
11
+ limits: {
12
+ storageMaxSize: null,
13
+ },
14
+ });
15
+ ```
16
+
17
+ - 4b0fbb3: **BREAKING** Added the ability to configure the limits for user object size, presence size, and user id length. User id is now defaulted to be limited to 128 characters. To unset this limit, you can set this limit to `0` or `null`.
18
+
19
+ ```ts
20
+ // defaults
21
+ createIO(
22
+ platformCloudflare({
23
+ limits: {
24
+ presenceMaxSize: 512,
25
+ storageMaxSize: 31_457_280,
26
+ userIdMaxLength: 128,
27
+ userMaxSize: 512,
28
+ },
29
+ }),
30
+ );
31
+ ```
32
+
33
+ ### Patch Changes
34
+
35
+ - e60a677: Fix 1 and 2 character long room names not being supported.
36
+ - 244a973: Fixed not applying user size limits when calling `PluvIO.authorize` and not allowing it to be overwritten for `platformPluv`.
37
+ - @pluv/crdt@0.43.0
38
+ - @pluv/types@0.43.0
39
+
40
+ ## 0.42.0
41
+
42
+ ### Patch Changes
43
+
44
+ - cf1529a: Each time garbage collection is run, a sync-state event will be emitted to connected sessions with info of currently connected users.
45
+ - @pluv/crdt@0.42.0
46
+ - @pluv/types@0.42.0
47
+
3
48
  ## 0.41.7
4
49
 
5
50
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -9,8 +9,9 @@ declare abstract class AbstractPersistence {
9
9
  abstract deleteUsers(room: string): Promise<void>;
10
10
  abstract getStorageState(room: string): Promise<string | null>;
11
11
  abstract getUser(room: string, connectionId: string): Promise<JsonObject | null>;
12
- abstract getUsers(room: string): Promise<readonly (JsonObject | null)[]>;
12
+ abstract getUsers(room: string): Promise<Map<[connectionId: string][0], JsonObject | null>>;
13
13
  abstract getUsersSize(room: string): Promise<number>;
14
+ abstract initialize(roomContext: any): AbstractPersistence;
14
15
  abstract setStorageState(room: string, state: string): Promise<void>;
15
16
  }
16
17
 
@@ -194,6 +195,24 @@ type ResolvedPluvIOAuthorize<TPlatform extends AbstractPlatform<any, any, any, a
194
195
  secret?: string;
195
196
  }, Exclude<"secret", InferPlatformAuthorizeProperties<TPlatform>>>;
196
197
  type PluvIOAuthorize<TPlatform extends AbstractPlatform<any, any, any, any>, TUser extends BaseUser | null = any, TContext extends Record<string, unknown> = {}> = (TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null) | ((context: TContext) => TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null);
198
+ interface PluvIOLimits {
199
+ /**
200
+ * @description Maximum size of presence object in bytes
201
+ */
202
+ presenceMaxSize?: number | null;
203
+ /**
204
+ * @description Maximum size of storage state in bytes
205
+ */
206
+ storageMaxSize?: number | null;
207
+ /**
208
+ * @description Maximum length of user id in characters
209
+ */
210
+ userIdMaxLength?: number | null;
211
+ /**
212
+ * @description Maximum size of user object in bytes
213
+ */
214
+ userMaxSize?: number | null;
215
+ }
197
216
  type BasePluvIOListeners<TPlatform extends AbstractPlatform<any, any, any, any>, TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null, TContext extends Record<string, any>, TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>> = {
198
217
  onRoomDeleted: (event: IORoomListenerEvent<TContext>) => void;
199
218
  onRoomMessage: (event: IORoomMessageEvent<TPlatform, TAuthorize, TContext, TEvents>) => void;
@@ -385,6 +404,7 @@ declare class IORoom<TPlatform extends AbstractPlatform<any> = AbstractPlatform<
385
404
  private _closeWebSockets;
386
405
  private _emitQuitters;
387
406
  private _emitRegistered;
407
+ private _emitSyncState;
388
408
  private _ensureDetached;
389
409
  private _getAbstractWs;
390
410
  private _getAuthorizedUser;
@@ -417,6 +437,8 @@ type PluvServerConfig<TPlatform extends AbstractPlatform<any, any> = AbstractPla
417
437
  };
418
438
  debug?: boolean;
419
439
  getInitialStorage?: GetInitialStorageFn<TContext>;
440
+ limits: PluvIOLimits;
441
+ io: PluvIO<TPlatform, TAuthorize, TContext>;
420
442
  platform: TPlatform;
421
443
  router?: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
422
444
  };
@@ -432,6 +454,8 @@ declare class PluvServer<TPlatform extends AbstractPlatform<any, any> = Abstract
432
454
  private readonly _crdt;
433
455
  private readonly _debug;
434
456
  private readonly _getInitialStorage;
457
+ private readonly _io;
458
+ private readonly _limits;
435
459
  private readonly _platform;
436
460
  private readonly _router;
437
461
  get fetch(): (...args: any[]) => Promise<any>;
@@ -450,7 +474,6 @@ declare class PluvServer<TPlatform extends AbstractPlatform<any, any> = Abstract
450
474
  constructor(options: PluvServerConfig<TPlatform, TAuthorize, TContext, TEvents>);
451
475
  createRoom(room: string, ...options: CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>): IORoom<TPlatform, TAuthorize, TContext, TEvents>;
452
476
  createToken(params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>, TPlatform>): Promise<string>;
453
- private _getIOAuthorize;
454
477
  private _getListeners;
455
478
  private _logDebug;
456
479
  }
@@ -460,6 +483,7 @@ type PluvIOConfig<TPlatform extends AbstractPlatform<any>, TAuthorize extends IO
460
483
  context?: PluvContext<TPlatform, TContext>;
461
484
  crdt?: CrdtLibraryType;
462
485
  debug?: boolean;
486
+ limits?: PluvIOLimits;
463
487
  platform: TPlatform;
464
488
  };
465
489
  type ResolvedServerConfig<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null = any, TContext extends Record<string, any> = {}, TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {}> = Partial<PluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>> & PluvIORouter<TPlatform, TAuthorize, TContext, TEvents> & {
@@ -474,6 +498,7 @@ declare class PluvIO<TPlatform extends AbstractPlatform<any> = AbstractPlatform<
474
498
  private readonly _context;
475
499
  private readonly _crdt;
476
500
  private readonly _debug;
501
+ private readonly _limits;
477
502
  private readonly _platform;
478
503
  get procedure(): PluvProcedure<TPlatform, TAuthorize, TContext, {}, {}>;
479
504
  constructor(options: PluvIOConfig<TPlatform, TAuthorize, TContext>);
@@ -489,8 +514,9 @@ type CreateIOParams<TPlatform extends AbstractPlatform<any> = AbstractPlatform<a
489
514
  context?: PluvContext<TPlatform, TContext>;
490
515
  crdt?: CrdtLibraryType;
491
516
  debug?: boolean;
517
+ limits?: PluvIOLimits;
492
518
  platform: TPlatform;
493
519
  };
494
520
  declare const createIO: <TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TContext extends Record<string, any> = {}, TUser extends BaseUser | null = null>(params: CreateIOParams<TPlatform, TContext, TUser>) => PluvIO<TPlatform, PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>, TContext>;
495
521
 
496
- export { type AbstractCloseEvent, type AbstractErrorEvent, type AbstractEvent, type AbstractEventMap, type AbstractListener, type AbstractMessageEvent, AbstractPersistence, AbstractPlatform, type AbstractPlatformConfig, AbstractPubSub, AbstractWebSocket, type AbstractWebSocketConfig, type AbstractWebSocketHandleErrorParams, type AuthorizeModule, type AuthorizeParams, type ConvertWebSocketConfig, type CrdtLibraryType, type CreateIOParams, type CreateRoomOptions, type GetInitialStorageFn, type HandleMode, type IOPubSubEventMessage, IORoom, type IORoomListenerEvent, type IORoomMessageEvent, type IOStorageUpdatedEvent, type IOUserConnectedEvent, type IOUserDisconnectedEvent, type InferIORoom, type InferInitContextType, type InferPlatformWebSocketType, type InferRoomContextType, type JWT, type JWTEncodeParams, type MergedRouter, type PlatformConfig, type PluvContext, PluvIO, type PluvIOAuthorize, type PluvIOConfig, type PluvIOListeners, PluvProcedure, type PluvProcedureConfig, PluvRouter, type PluvRouterEventConfig, PluvServer, type PluvServerConfig, type ServerConfig, type WebSocketRegistrationMode, type WebSocketSerializedState, type WebSocketSession, authorize, createIO };
522
+ export { type AbstractCloseEvent, type AbstractErrorEvent, type AbstractEvent, type AbstractEventMap, type AbstractListener, type AbstractMessageEvent, AbstractPersistence, AbstractPlatform, type AbstractPlatformConfig, AbstractPubSub, AbstractWebSocket, type AbstractWebSocketConfig, type AbstractWebSocketHandleErrorParams, type AuthorizeModule, type AuthorizeParams, type ConvertWebSocketConfig, type CrdtLibraryType, type CreateIOParams, type CreateRoomOptions, type GetInitialStorageFn, type HandleMode, type IOPubSubEventMessage, IORoom, type IORoomListenerEvent, type IORoomMessageEvent, type IOStorageUpdatedEvent, type IOUserConnectedEvent, type IOUserDisconnectedEvent, type InferIORoom, type InferInitContextType, type InferPlatformWebSocketType, type InferRoomContextType, type JWT, type JWTEncodeParams, type MergedRouter, type PlatformConfig, type PluvContext, PluvIO, type PluvIOAuthorize, type PluvIOConfig, type PluvIOLimits, type PluvIOListeners, PluvProcedure, type PluvProcedureConfig, PluvRouter, type PluvRouterEventConfig, PluvServer, type PluvServerConfig, type ServerConfig, type WebSocketRegistrationMode, type WebSocketSerializedState, type WebSocketSession, authorize, createIO };
package/dist/index.d.ts CHANGED
@@ -9,8 +9,9 @@ declare abstract class AbstractPersistence {
9
9
  abstract deleteUsers(room: string): Promise<void>;
10
10
  abstract getStorageState(room: string): Promise<string | null>;
11
11
  abstract getUser(room: string, connectionId: string): Promise<JsonObject | null>;
12
- abstract getUsers(room: string): Promise<readonly (JsonObject | null)[]>;
12
+ abstract getUsers(room: string): Promise<Map<[connectionId: string][0], JsonObject | null>>;
13
13
  abstract getUsersSize(room: string): Promise<number>;
14
+ abstract initialize(roomContext: any): AbstractPersistence;
14
15
  abstract setStorageState(room: string, state: string): Promise<void>;
15
16
  }
16
17
 
@@ -194,6 +195,24 @@ type ResolvedPluvIOAuthorize<TPlatform extends AbstractPlatform<any, any, any, a
194
195
  secret?: string;
195
196
  }, Exclude<"secret", InferPlatformAuthorizeProperties<TPlatform>>>;
196
197
  type PluvIOAuthorize<TPlatform extends AbstractPlatform<any, any, any, any>, TUser extends BaseUser | null = any, TContext extends Record<string, unknown> = {}> = (TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null) | ((context: TContext) => TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null);
198
+ interface PluvIOLimits {
199
+ /**
200
+ * @description Maximum size of presence object in bytes
201
+ */
202
+ presenceMaxSize?: number | null;
203
+ /**
204
+ * @description Maximum size of storage state in bytes
205
+ */
206
+ storageMaxSize?: number | null;
207
+ /**
208
+ * @description Maximum length of user id in characters
209
+ */
210
+ userIdMaxLength?: number | null;
211
+ /**
212
+ * @description Maximum size of user object in bytes
213
+ */
214
+ userMaxSize?: number | null;
215
+ }
197
216
  type BasePluvIOListeners<TPlatform extends AbstractPlatform<any, any, any, any>, TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null, TContext extends Record<string, any>, TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext>> = {
198
217
  onRoomDeleted: (event: IORoomListenerEvent<TContext>) => void;
199
218
  onRoomMessage: (event: IORoomMessageEvent<TPlatform, TAuthorize, TContext, TEvents>) => void;
@@ -385,6 +404,7 @@ declare class IORoom<TPlatform extends AbstractPlatform<any> = AbstractPlatform<
385
404
  private _closeWebSockets;
386
405
  private _emitQuitters;
387
406
  private _emitRegistered;
407
+ private _emitSyncState;
388
408
  private _ensureDetached;
389
409
  private _getAbstractWs;
390
410
  private _getAuthorizedUser;
@@ -417,6 +437,8 @@ type PluvServerConfig<TPlatform extends AbstractPlatform<any, any> = AbstractPla
417
437
  };
418
438
  debug?: boolean;
419
439
  getInitialStorage?: GetInitialStorageFn<TContext>;
440
+ limits: PluvIOLimits;
441
+ io: PluvIO<TPlatform, TAuthorize, TContext>;
420
442
  platform: TPlatform;
421
443
  router?: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
422
444
  };
@@ -432,6 +454,8 @@ declare class PluvServer<TPlatform extends AbstractPlatform<any, any> = Abstract
432
454
  private readonly _crdt;
433
455
  private readonly _debug;
434
456
  private readonly _getInitialStorage;
457
+ private readonly _io;
458
+ private readonly _limits;
435
459
  private readonly _platform;
436
460
  private readonly _router;
437
461
  get fetch(): (...args: any[]) => Promise<any>;
@@ -450,7 +474,6 @@ declare class PluvServer<TPlatform extends AbstractPlatform<any, any> = Abstract
450
474
  constructor(options: PluvServerConfig<TPlatform, TAuthorize, TContext, TEvents>);
451
475
  createRoom(room: string, ...options: CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>): IORoom<TPlatform, TAuthorize, TContext, TEvents>;
452
476
  createToken(params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>, TPlatform>): Promise<string>;
453
- private _getIOAuthorize;
454
477
  private _getListeners;
455
478
  private _logDebug;
456
479
  }
@@ -460,6 +483,7 @@ type PluvIOConfig<TPlatform extends AbstractPlatform<any>, TAuthorize extends IO
460
483
  context?: PluvContext<TPlatform, TContext>;
461
484
  crdt?: CrdtLibraryType;
462
485
  debug?: boolean;
486
+ limits?: PluvIOLimits;
463
487
  platform: TPlatform;
464
488
  };
465
489
  type ResolvedServerConfig<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null = any, TContext extends Record<string, any> = {}, TEvents extends PluvRouterEventConfig<TPlatform, TAuthorize, TContext> = {}> = Partial<PluvIOListeners<TPlatform, TAuthorize, TContext, TEvents>> & PluvIORouter<TPlatform, TAuthorize, TContext, TEvents> & {
@@ -474,6 +498,7 @@ declare class PluvIO<TPlatform extends AbstractPlatform<any> = AbstractPlatform<
474
498
  private readonly _context;
475
499
  private readonly _crdt;
476
500
  private readonly _debug;
501
+ private readonly _limits;
477
502
  private readonly _platform;
478
503
  get procedure(): PluvProcedure<TPlatform, TAuthorize, TContext, {}, {}>;
479
504
  constructor(options: PluvIOConfig<TPlatform, TAuthorize, TContext>);
@@ -489,8 +514,9 @@ type CreateIOParams<TPlatform extends AbstractPlatform<any> = AbstractPlatform<a
489
514
  context?: PluvContext<TPlatform, TContext>;
490
515
  crdt?: CrdtLibraryType;
491
516
  debug?: boolean;
517
+ limits?: PluvIOLimits;
492
518
  platform: TPlatform;
493
519
  };
494
520
  declare const createIO: <TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TContext extends Record<string, any> = {}, TUser extends BaseUser | null = null>(params: CreateIOParams<TPlatform, TContext, TUser>) => PluvIO<TPlatform, PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>, TContext>;
495
521
 
496
- export { type AbstractCloseEvent, type AbstractErrorEvent, type AbstractEvent, type AbstractEventMap, type AbstractListener, type AbstractMessageEvent, AbstractPersistence, AbstractPlatform, type AbstractPlatformConfig, AbstractPubSub, AbstractWebSocket, type AbstractWebSocketConfig, type AbstractWebSocketHandleErrorParams, type AuthorizeModule, type AuthorizeParams, type ConvertWebSocketConfig, type CrdtLibraryType, type CreateIOParams, type CreateRoomOptions, type GetInitialStorageFn, type HandleMode, type IOPubSubEventMessage, IORoom, type IORoomListenerEvent, type IORoomMessageEvent, type IOStorageUpdatedEvent, type IOUserConnectedEvent, type IOUserDisconnectedEvent, type InferIORoom, type InferInitContextType, type InferPlatformWebSocketType, type InferRoomContextType, type JWT, type JWTEncodeParams, type MergedRouter, type PlatformConfig, type PluvContext, PluvIO, type PluvIOAuthorize, type PluvIOConfig, type PluvIOListeners, PluvProcedure, type PluvProcedureConfig, PluvRouter, type PluvRouterEventConfig, PluvServer, type PluvServerConfig, type ServerConfig, type WebSocketRegistrationMode, type WebSocketSerializedState, type WebSocketSession, authorize, createIO };
522
+ export { type AbstractCloseEvent, type AbstractErrorEvent, type AbstractEvent, type AbstractEventMap, type AbstractListener, type AbstractMessageEvent, AbstractPersistence, AbstractPlatform, type AbstractPlatformConfig, AbstractPubSub, AbstractWebSocket, type AbstractWebSocketConfig, type AbstractWebSocketHandleErrorParams, type AuthorizeModule, type AuthorizeParams, type ConvertWebSocketConfig, type CrdtLibraryType, type CreateIOParams, type CreateRoomOptions, type GetInitialStorageFn, type HandleMode, type IOPubSubEventMessage, IORoom, type IORoomListenerEvent, type IORoomMessageEvent, type IOStorageUpdatedEvent, type IOUserConnectedEvent, type IOUserDisconnectedEvent, type InferIORoom, type InferInitContextType, type InferPlatformWebSocketType, type InferRoomContextType, type JWT, type JWTEncodeParams, type MergedRouter, type PlatformConfig, type PluvContext, PluvIO, type PluvIOAuthorize, type PluvIOConfig, type PluvIOLimits, type PluvIOListeners, PluvProcedure, type PluvProcedureConfig, PluvRouter, type PluvRouterEventConfig, PluvServer, type PluvServerConfig, type ServerConfig, type WebSocketRegistrationMode, type WebSocketSerializedState, type WebSocketSession, authorize, createIO };
package/dist/index.js CHANGED
@@ -145,9 +145,19 @@ var Persistence = class extends AbstractPersistence {
145
145
  return Promise.resolve(user != null ? user : null);
146
146
  }
147
147
  getUsers(room) {
148
- const users = this._users.get(room);
149
- if (!users) return Promise.resolve([]);
150
- return Promise.resolve(Array.from(users.values()));
148
+ return __async(this, null, function* () {
149
+ const users = this._users.get(room);
150
+ if (!users) return /* @__PURE__ */ new Map();
151
+ return users;
152
+ });
153
+ }
154
+ getUsersSize(room) {
155
+ return __async(this, null, function* () {
156
+ return this._users.size;
157
+ });
158
+ }
159
+ initialize(roomContext) {
160
+ return this;
151
161
  }
152
162
  setStorageState(room, state) {
153
163
  this._storages.set(room, state);
@@ -389,6 +399,8 @@ var import_kleur = __toESM(require("kleur"));
389
399
  // src/constants.ts
390
400
  var GARBAGE_COLLECT_INTERVAL_MS = 6e4;
391
401
  var MAX_PRESENCE_SIZE_BYTES = 512;
402
+ var MAX_STORAGE_SIZE_BYTES = 31457280;
403
+ var MAX_USER_ID_LENGTH = 128;
392
404
  var MAX_USER_SIZE_BYTES = 512;
393
405
  var PING_TIMEOUT_MS = 3e4;
394
406
 
@@ -503,6 +515,7 @@ var IORoom = class {
503
515
  return __async(this, null, function* () {
504
516
  this._lastGarbageCollectMs = Date.now();
505
517
  this._emitQuitters();
518
+ this._emitSyncState();
506
519
  });
507
520
  }
508
521
  getSize() {
@@ -695,6 +708,17 @@ var IORoom = class {
695
708
  }
696
709
  });
697
710
  }
711
+ _emitSyncState() {
712
+ return __async(this, null, function* () {
713
+ const connectionIds = yield this._platform.persistence.getUsers(this.id).then((map) => Object.keys(map));
714
+ yield this._broadcast({
715
+ message: {
716
+ type: "$syncStateReceived",
717
+ data: { connectionIds }
718
+ }
719
+ });
720
+ });
721
+ }
698
722
  _ensureDetached() {
699
723
  if (this._platform._config.registrationMode === "detached") return;
700
724
  throw new Error("Platform must use detached mode");
@@ -1117,7 +1141,7 @@ var pickBy = (obj, by) => {
1117
1141
  };
1118
1142
 
1119
1143
  // src/version.generated.ts
1120
- var version = "0.41.7";
1144
+ var version = "0.43.0";
1121
1145
 
1122
1146
  // src/PluvServer.ts
1123
1147
  var PluvServer = class {
@@ -1168,18 +1192,22 @@ var PluvServer = class {
1168
1192
  update
1169
1193
  ];
1170
1194
  if (updates.some((_update) => !!_update)) {
1171
- const encodedState = doc.batchApplyEncodedState(updates).getEncodedState();
1172
- yield this._platform.persistence.setStorageState(room, encodedState);
1195
+ const encodedState2 = doc.batchApplyEncodedState(updates).getEncodedState();
1196
+ yield this._platform.persistence.setStorageState(room, encodedState2);
1173
1197
  this._getListeners().onStorageUpdated({
1174
1198
  context,
1175
- encodedState,
1199
+ encodedState: encodedState2,
1176
1200
  room,
1177
1201
  user: session == null ? void 0 : session.user,
1178
1202
  webSocket: session == null ? void 0 : session.webSocket.webSocket
1179
1203
  });
1180
1204
  }
1181
- const state = (_b = yield this._platform.persistence.getStorageState(room)) != null ? _b : doc.getEncodedState();
1182
- return { $storageReceived: { state } };
1205
+ const encodedState = (_b = yield this._platform.persistence.getStorageState(room)) != null ? _b : doc.getEncodedState();
1206
+ const storageSize = new TextEncoder().encode(encodedState).length;
1207
+ if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
1208
+ throw new Error("Storage has exceeded the size limit");
1209
+ }
1210
+ return { $storageReceived: { state: encodedState } };
1183
1211
  })),
1184
1212
  $ping: this._procedure.self((data, { session }) => {
1185
1213
  if (!session) return {};
@@ -1199,7 +1227,7 @@ var PluvServer = class {
1199
1227
  const cleanedPatch = pickBy(presence != null ? presence : {}, (value) => typeof value !== "undefined");
1200
1228
  const updated = Object.assign(/* @__PURE__ */ Object.create(null), session.presence, cleanedPatch);
1201
1229
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
1202
- if (bytes > MAX_PRESENCE_SIZE_BYTES) {
1230
+ if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
1203
1231
  throw new Error(
1204
1232
  `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1205
1233
  );
@@ -1219,6 +1247,10 @@ var PluvServer = class {
1219
1247
  if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
1220
1248
  const updated = update === null ? doc : doc.applyEncodedState({ update });
1221
1249
  const encodedState = updated.getEncodedState();
1250
+ const storageSize = new TextEncoder().encode(encodedState).length;
1251
+ if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
1252
+ throw new Error("Storage has exceeded the size limit");
1253
+ }
1222
1254
  this._platform.persistence.setStorageState(room, encodedState).then(() => {
1223
1255
  this._getListeners().onStorageUpdated({
1224
1256
  context,
@@ -1237,6 +1269,8 @@ var PluvServer = class {
1237
1269
  crdt = import_crdt2.noop,
1238
1270
  debug = false,
1239
1271
  getInitialStorage,
1272
+ io,
1273
+ limits,
1240
1274
  platform,
1241
1275
  router = new PluvRouter({})
1242
1276
  } = options;
@@ -1244,6 +1278,8 @@ var PluvServer = class {
1244
1278
  platform.validateConfig(options);
1245
1279
  this._crdt = crdt;
1246
1280
  this._debug = debug;
1281
+ this._io = io;
1282
+ this._limits = limits;
1247
1283
  this._platform = platform;
1248
1284
  this._router = router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter;
1249
1285
  if (authorize2) this._authorize = authorize2;
@@ -1285,7 +1321,7 @@ var PluvServer = class {
1285
1321
  if (this._platform._config.handleMode !== "io") {
1286
1322
  throw new Error(`\`createRoom\` is unsupported for \`${this._platform._name}\``);
1287
1323
  }
1288
- if (!/^[a-z0-9]+[a-z0-9\-_]+[a-z0-9]+$/i.test(room)) throw new Error("Unsupported room name");
1324
+ if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room)) throw new Error("Unsupported room name");
1289
1325
  const roomContext = platformRoomContext;
1290
1326
  const context = typeof this._context === "function" ? this._context(roomContext) : this._context;
1291
1327
  const newRoom = new IORoom(room, __spreadProps(__spreadValues({}, !!_meta ? { _meta } : {}), {
@@ -1319,29 +1355,9 @@ var PluvServer = class {
1319
1355
  }
1320
1356
  createToken(params) {
1321
1357
  return __async(this, null, function* () {
1322
- var _a;
1323
- if (!this._authorize) {
1324
- throw new Error("IO does not specify authorize during initialization.");
1325
- }
1326
- const ioAuthorize = this._getIOAuthorize(params);
1327
- const user = params.user;
1328
- const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
1329
- const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
1330
- if (bytes > MAX_USER_SIZE_BYTES) {
1331
- throw new Error(
1332
- `createToken called with large payload. User must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1333
- );
1334
- }
1335
- if (this._platform._createToken) return yield this._platform._createToken(params);
1336
- const secret = (_a = ioAuthorize == null ? void 0 : ioAuthorize.secret) != null ? _a : null;
1337
- if (!secret) throw new Error("`authorize` was specified without a valid secret");
1338
- return yield authorize({ platform: this._platform, secret }).encode(params);
1358
+ return yield this._io.createToken(params);
1339
1359
  });
1340
1360
  }
1341
- _getIOAuthorize(options) {
1342
- if (typeof this._authorize === "function") return this._authorize(options);
1343
- return this._authorize;
1344
- }
1345
1361
  _getListeners() {
1346
1362
  return this._listeners;
1347
1363
  }
@@ -1356,9 +1372,15 @@ var PluvIO = class {
1356
1372
  this.version = version;
1357
1373
  this._authorize = null;
1358
1374
  this._context = {};
1359
- const { authorize: authorize2, context, crdt = import_crdt3.noop, debug = false, platform } = options;
1375
+ const { authorize: authorize2, context, crdt = import_crdt3.noop, debug = false, limits, platform } = options;
1360
1376
  this._crdt = crdt;
1361
1377
  this._debug = debug;
1378
+ this._limits = __spreadValues({
1379
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES,
1380
+ storageMaxSize: MAX_STORAGE_SIZE_BYTES,
1381
+ userIdMaxLength: MAX_USER_ID_LENGTH,
1382
+ userMaxSize: MAX_USER_SIZE_BYTES
1383
+ }, limits);
1362
1384
  this._platform = platform;
1363
1385
  if (authorize2) this._authorize = authorize2;
1364
1386
  if (context) this._context = context;
@@ -1371,6 +1393,20 @@ var PluvIO = class {
1371
1393
  var _a;
1372
1394
  if (!this._authorize) throw new Error("IO does not specify authorize during initialization.");
1373
1395
  const ioAuthorize = this._getIOAuthorize(params);
1396
+ const user = params.user;
1397
+ const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
1398
+ if (!!this._limits.userIdMaxLength && user.id.length > this._limits.userIdMaxLength) {
1399
+ throw new Error(
1400
+ `createToken was called with a long user id. User ID must be at most 128 characters. Current length: ${user.id.length.toLocaleString()}`
1401
+ );
1402
+ }
1403
+ const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
1404
+ if (!!this._limits.userMaxSize && bytes > this._limits.userMaxSize) {
1405
+ throw new Error(
1406
+ `createToken called with large payload. User must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1407
+ );
1408
+ }
1409
+ if (this._platform._createToken) return yield this._platform._createToken(params);
1374
1410
  const secret = (_a = ioAuthorize == null ? void 0 : ioAuthorize.secret) != null ? _a : null;
1375
1411
  if (!secret) throw new Error("`authorize` was specified without a valid secret");
1376
1412
  return yield authorize({ platform: this._platform, secret }).encode(params);
@@ -1393,6 +1429,8 @@ var PluvIO = class {
1393
1429
  context: this._context,
1394
1430
  crdt: this._crdt,
1395
1431
  debug: this._debug,
1432
+ io: this,
1433
+ limits: this._limits,
1396
1434
  platform: this._platform
1397
1435
  }));
1398
1436
  }
@@ -1406,12 +1444,13 @@ var PluvIO = class {
1406
1444
 
1407
1445
  // src/createIO.ts
1408
1446
  var createIO = (params) => {
1409
- const { authorize: authorize2, context, crdt, debug, platform } = params;
1447
+ const { authorize: authorize2, context, crdt, debug, limits, platform } = params;
1410
1448
  return new PluvIO({
1411
1449
  authorize: authorize2,
1412
1450
  context,
1413
1451
  crdt,
1414
1452
  debug,
1453
+ limits,
1415
1454
  platform
1416
1455
  });
1417
1456
  };
package/dist/index.mjs CHANGED
@@ -103,9 +103,19 @@ var Persistence = class extends AbstractPersistence {
103
103
  return Promise.resolve(user != null ? user : null);
104
104
  }
105
105
  getUsers(room) {
106
- const users = this._users.get(room);
107
- if (!users) return Promise.resolve([]);
108
- return Promise.resolve(Array.from(users.values()));
106
+ return __async(this, null, function* () {
107
+ const users = this._users.get(room);
108
+ if (!users) return /* @__PURE__ */ new Map();
109
+ return users;
110
+ });
111
+ }
112
+ getUsersSize(room) {
113
+ return __async(this, null, function* () {
114
+ return this._users.size;
115
+ });
116
+ }
117
+ initialize(roomContext) {
118
+ return this;
109
119
  }
110
120
  setStorageState(room, state) {
111
121
  this._storages.set(room, state);
@@ -347,6 +357,8 @@ import colors from "kleur";
347
357
  // src/constants.ts
348
358
  var GARBAGE_COLLECT_INTERVAL_MS = 6e4;
349
359
  var MAX_PRESENCE_SIZE_BYTES = 512;
360
+ var MAX_STORAGE_SIZE_BYTES = 31457280;
361
+ var MAX_USER_ID_LENGTH = 128;
350
362
  var MAX_USER_SIZE_BYTES = 512;
351
363
  var PING_TIMEOUT_MS = 3e4;
352
364
 
@@ -461,6 +473,7 @@ var IORoom = class {
461
473
  return __async(this, null, function* () {
462
474
  this._lastGarbageCollectMs = Date.now();
463
475
  this._emitQuitters();
476
+ this._emitSyncState();
464
477
  });
465
478
  }
466
479
  getSize() {
@@ -653,6 +666,17 @@ var IORoom = class {
653
666
  }
654
667
  });
655
668
  }
669
+ _emitSyncState() {
670
+ return __async(this, null, function* () {
671
+ const connectionIds = yield this._platform.persistence.getUsers(this.id).then((map) => Object.keys(map));
672
+ yield this._broadcast({
673
+ message: {
674
+ type: "$syncStateReceived",
675
+ data: { connectionIds }
676
+ }
677
+ });
678
+ });
679
+ }
656
680
  _ensureDetached() {
657
681
  if (this._platform._config.registrationMode === "detached") return;
658
682
  throw new Error("Platform must use detached mode");
@@ -1075,7 +1099,7 @@ var pickBy = (obj, by) => {
1075
1099
  };
1076
1100
 
1077
1101
  // src/version.generated.ts
1078
- var version = "0.41.7";
1102
+ var version = "0.43.0";
1079
1103
 
1080
1104
  // src/PluvServer.ts
1081
1105
  var PluvServer = class {
@@ -1126,18 +1150,22 @@ var PluvServer = class {
1126
1150
  update
1127
1151
  ];
1128
1152
  if (updates.some((_update) => !!_update)) {
1129
- const encodedState = doc.batchApplyEncodedState(updates).getEncodedState();
1130
- yield this._platform.persistence.setStorageState(room, encodedState);
1153
+ const encodedState2 = doc.batchApplyEncodedState(updates).getEncodedState();
1154
+ yield this._platform.persistence.setStorageState(room, encodedState2);
1131
1155
  this._getListeners().onStorageUpdated({
1132
1156
  context,
1133
- encodedState,
1157
+ encodedState: encodedState2,
1134
1158
  room,
1135
1159
  user: session == null ? void 0 : session.user,
1136
1160
  webSocket: session == null ? void 0 : session.webSocket.webSocket
1137
1161
  });
1138
1162
  }
1139
- const state = (_b = yield this._platform.persistence.getStorageState(room)) != null ? _b : doc.getEncodedState();
1140
- return { $storageReceived: { state } };
1163
+ const encodedState = (_b = yield this._platform.persistence.getStorageState(room)) != null ? _b : doc.getEncodedState();
1164
+ const storageSize = new TextEncoder().encode(encodedState).length;
1165
+ if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
1166
+ throw new Error("Storage has exceeded the size limit");
1167
+ }
1168
+ return { $storageReceived: { state: encodedState } };
1141
1169
  })),
1142
1170
  $ping: this._procedure.self((data, { session }) => {
1143
1171
  if (!session) return {};
@@ -1157,7 +1185,7 @@ var PluvServer = class {
1157
1185
  const cleanedPatch = pickBy(presence != null ? presence : {}, (value) => typeof value !== "undefined");
1158
1186
  const updated = Object.assign(/* @__PURE__ */ Object.create(null), session.presence, cleanedPatch);
1159
1187
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
1160
- if (bytes > MAX_PRESENCE_SIZE_BYTES) {
1188
+ if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
1161
1189
  throw new Error(
1162
1190
  `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1163
1191
  );
@@ -1177,6 +1205,10 @@ var PluvServer = class {
1177
1205
  if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
1178
1206
  const updated = update === null ? doc : doc.applyEncodedState({ update });
1179
1207
  const encodedState = updated.getEncodedState();
1208
+ const storageSize = new TextEncoder().encode(encodedState).length;
1209
+ if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
1210
+ throw new Error("Storage has exceeded the size limit");
1211
+ }
1180
1212
  this._platform.persistence.setStorageState(room, encodedState).then(() => {
1181
1213
  this._getListeners().onStorageUpdated({
1182
1214
  context,
@@ -1195,6 +1227,8 @@ var PluvServer = class {
1195
1227
  crdt = noop2,
1196
1228
  debug = false,
1197
1229
  getInitialStorage,
1230
+ io,
1231
+ limits,
1198
1232
  platform,
1199
1233
  router = new PluvRouter({})
1200
1234
  } = options;
@@ -1202,6 +1236,8 @@ var PluvServer = class {
1202
1236
  platform.validateConfig(options);
1203
1237
  this._crdt = crdt;
1204
1238
  this._debug = debug;
1239
+ this._io = io;
1240
+ this._limits = limits;
1205
1241
  this._platform = platform;
1206
1242
  this._router = router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter;
1207
1243
  if (authorize2) this._authorize = authorize2;
@@ -1243,7 +1279,7 @@ var PluvServer = class {
1243
1279
  if (this._platform._config.handleMode !== "io") {
1244
1280
  throw new Error(`\`createRoom\` is unsupported for \`${this._platform._name}\``);
1245
1281
  }
1246
- if (!/^[a-z0-9]+[a-z0-9\-_]+[a-z0-9]+$/i.test(room)) throw new Error("Unsupported room name");
1282
+ if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room)) throw new Error("Unsupported room name");
1247
1283
  const roomContext = platformRoomContext;
1248
1284
  const context = typeof this._context === "function" ? this._context(roomContext) : this._context;
1249
1285
  const newRoom = new IORoom(room, __spreadProps(__spreadValues({}, !!_meta ? { _meta } : {}), {
@@ -1277,29 +1313,9 @@ var PluvServer = class {
1277
1313
  }
1278
1314
  createToken(params) {
1279
1315
  return __async(this, null, function* () {
1280
- var _a;
1281
- if (!this._authorize) {
1282
- throw new Error("IO does not specify authorize during initialization.");
1283
- }
1284
- const ioAuthorize = this._getIOAuthorize(params);
1285
- const user = params.user;
1286
- const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
1287
- const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
1288
- if (bytes > MAX_USER_SIZE_BYTES) {
1289
- throw new Error(
1290
- `createToken called with large payload. User must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1291
- );
1292
- }
1293
- if (this._platform._createToken) return yield this._platform._createToken(params);
1294
- const secret = (_a = ioAuthorize == null ? void 0 : ioAuthorize.secret) != null ? _a : null;
1295
- if (!secret) throw new Error("`authorize` was specified without a valid secret");
1296
- return yield authorize({ platform: this._platform, secret }).encode(params);
1316
+ return yield this._io.createToken(params);
1297
1317
  });
1298
1318
  }
1299
- _getIOAuthorize(options) {
1300
- if (typeof this._authorize === "function") return this._authorize(options);
1301
- return this._authorize;
1302
- }
1303
1319
  _getListeners() {
1304
1320
  return this._listeners;
1305
1321
  }
@@ -1314,9 +1330,15 @@ var PluvIO = class {
1314
1330
  this.version = version;
1315
1331
  this._authorize = null;
1316
1332
  this._context = {};
1317
- const { authorize: authorize2, context, crdt = noop3, debug = false, platform } = options;
1333
+ const { authorize: authorize2, context, crdt = noop3, debug = false, limits, platform } = options;
1318
1334
  this._crdt = crdt;
1319
1335
  this._debug = debug;
1336
+ this._limits = __spreadValues({
1337
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES,
1338
+ storageMaxSize: MAX_STORAGE_SIZE_BYTES,
1339
+ userIdMaxLength: MAX_USER_ID_LENGTH,
1340
+ userMaxSize: MAX_USER_SIZE_BYTES
1341
+ }, limits);
1320
1342
  this._platform = platform;
1321
1343
  if (authorize2) this._authorize = authorize2;
1322
1344
  if (context) this._context = context;
@@ -1329,6 +1351,20 @@ var PluvIO = class {
1329
1351
  var _a;
1330
1352
  if (!this._authorize) throw new Error("IO does not specify authorize during initialization.");
1331
1353
  const ioAuthorize = this._getIOAuthorize(params);
1354
+ const user = params.user;
1355
+ const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
1356
+ if (!!this._limits.userIdMaxLength && user.id.length > this._limits.userIdMaxLength) {
1357
+ throw new Error(
1358
+ `createToken was called with a long user id. User ID must be at most 128 characters. Current length: ${user.id.length.toLocaleString()}`
1359
+ );
1360
+ }
1361
+ const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
1362
+ if (!!this._limits.userMaxSize && bytes > this._limits.userMaxSize) {
1363
+ throw new Error(
1364
+ `createToken called with large payload. User must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1365
+ );
1366
+ }
1367
+ if (this._platform._createToken) return yield this._platform._createToken(params);
1332
1368
  const secret = (_a = ioAuthorize == null ? void 0 : ioAuthorize.secret) != null ? _a : null;
1333
1369
  if (!secret) throw new Error("`authorize` was specified without a valid secret");
1334
1370
  return yield authorize({ platform: this._platform, secret }).encode(params);
@@ -1351,6 +1387,8 @@ var PluvIO = class {
1351
1387
  context: this._context,
1352
1388
  crdt: this._crdt,
1353
1389
  debug: this._debug,
1390
+ io: this,
1391
+ limits: this._limits,
1354
1392
  platform: this._platform
1355
1393
  }));
1356
1394
  }
@@ -1364,12 +1402,13 @@ var PluvIO = class {
1364
1402
 
1365
1403
  // src/createIO.ts
1366
1404
  var createIO = (params) => {
1367
- const { authorize: authorize2, context, crdt, debug, platform } = params;
1405
+ const { authorize: authorize2, context, crdt, debug, limits, platform } = params;
1368
1406
  return new PluvIO({
1369
1407
  authorize: authorize2,
1370
1408
  context,
1371
1409
  crdt,
1372
1410
  debug,
1411
+ limits,
1373
1412
  platform
1374
1413
  });
1375
1414
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/io",
3
- "version": "0.41.7",
3
+ "version": "0.43.0",
4
4
  "description": "@pluv/io server-side websocket manager",
5
5
  "author": "leedavidcs",
6
6
  "license": "MIT",
@@ -21,16 +21,16 @@
21
21
  "jose": "^5.10.0",
22
22
  "kleur": "^4.1.5",
23
23
  "wonka": "^6.3.5",
24
- "@pluv/crdt": "^0.41.7",
25
- "@pluv/types": "^0.41.7"
24
+ "@pluv/crdt": "^0.43.0",
25
+ "@pluv/types": "^0.43.0"
26
26
  },
27
27
  "devDependencies": {
28
- "eslint": "^9.25.0",
28
+ "eslint": "^9.25.1",
29
29
  "genversion": "^3.2.0",
30
30
  "tsup": "^8.4.0",
31
31
  "typescript": "^5.8.3",
32
- "@pluv/tsconfig": "^0.41.7",
33
- "eslint-config-pluv": "^0.41.7"
32
+ "@pluv/tsconfig": "^0.43.0",
33
+ "eslint-config-pluv": "^0.43.0"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "pnpm run version && tsup src/index.ts --format esm,cjs --dts",
@@ -13,9 +13,11 @@ export abstract class AbstractPersistence {
13
13
 
14
14
  public abstract getUser(room: string, connectionId: string): Promise<JsonObject | null>;
15
15
 
16
- public abstract getUsers(room: string): Promise<readonly (JsonObject | null)[]>;
16
+ public abstract getUsers(room: string): Promise<Map<[connectionId: string][0], JsonObject | null>>;
17
17
 
18
18
  public abstract getUsersSize(room: string): Promise<number>;
19
19
 
20
+ public abstract initialize(roomContext: any): AbstractPersistence;
21
+
20
22
  public abstract setStorageState(room: string, state: string): Promise<void>;
21
23
  }
package/src/IORoom.ts CHANGED
@@ -256,6 +256,7 @@ export class IORoom<
256
256
  public async garbageCollect(): Promise<void> {
257
257
  this._lastGarbageCollectMs = Date.now();
258
258
  this._emitQuitters();
259
+ this._emitSyncState();
259
260
  }
260
261
 
261
262
  public getSize(): number {
@@ -507,6 +508,17 @@ export class IORoom<
507
508
  }
508
509
  }
509
510
 
511
+ private async _emitSyncState(): Promise<void> {
512
+ const connectionIds = await this._platform.persistence.getUsers(this.id).then((map) => Object.keys(map));
513
+
514
+ await this._broadcast({
515
+ message: {
516
+ type: "$syncStateReceived",
517
+ data: { connectionIds },
518
+ },
519
+ });
520
+ }
521
+
510
522
  private _ensureDetached(): void {
511
523
  if (this._platform._config.registrationMode === "detached") return;
512
524
 
@@ -57,12 +57,20 @@ export class Persistence extends AbstractPersistence {
57
57
  return Promise.resolve(user ?? null);
58
58
  }
59
59
 
60
- public getUsers(room: string): Promise<readonly (JsonObject | null)[]> {
60
+ public async getUsers(room: string): Promise<Map<string, JsonObject | null>> {
61
61
  const users = this._users.get(room);
62
62
 
63
- if (!users) return Promise.resolve([]);
63
+ if (!users) return new Map();
64
64
 
65
- return Promise.resolve(Array.from(users.values()));
65
+ return users;
66
+ }
67
+
68
+ public async getUsersSize(room: string): Promise<number> {
69
+ return this._users.size;
70
+ }
71
+
72
+ public initialize(roomContext: any): this {
73
+ return this;
66
74
  }
67
75
 
68
76
  public setStorageState(room: string, state: string): Promise<void> {
package/src/PluvIO.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AbstractCrdtDocFactory } from "@pluv/crdt";
2
2
  import { noop } from "@pluv/crdt";
3
- import type { IOAuthorize, InferIOAuthorizeUser } from "@pluv/types";
3
+ import type { BaseUser, IOAuthorize, InferIOAuthorizeUser } from "@pluv/types";
4
4
  import type { AbstractPlatform, InferInitContextType } from "./AbstractPlatform";
5
5
  import type { WebSocketRegisterConfig } from "./IORoom";
6
6
  import { PluvProcedure } from "./PluvProcedure";
@@ -9,11 +9,13 @@ import { PluvRouter } from "./PluvRouter";
9
9
  import { PluvServer, PluvServerConfig } from "./PluvServer";
10
10
  import type { JWTEncodeParams } from "./authorize";
11
11
  import { authorize } from "./authorize";
12
+ import { MAX_PRESENCE_SIZE_BYTES, MAX_STORAGE_SIZE_BYTES, MAX_USER_ID_LENGTH, MAX_USER_SIZE_BYTES } from "./constants";
12
13
  import type {
13
14
  CrdtLibraryType,
14
15
  GetInitialStorageFn,
15
16
  PluvContext,
16
17
  PluvIOAuthorize,
18
+ PluvIOLimits,
17
19
  PluvIOListeners,
18
20
  PluvIORouter,
19
21
  ResolvedPluvIOAuthorize,
@@ -29,6 +31,7 @@ export type PluvIOConfig<
29
31
  context?: PluvContext<TPlatform, TContext>;
30
32
  crdt?: CrdtLibraryType;
31
33
  debug?: boolean;
34
+ limits?: PluvIOLimits;
32
35
  platform: TPlatform;
33
36
  };
34
37
 
@@ -69,6 +72,7 @@ export class PluvIO<
69
72
  private readonly _context: PluvContext<TPlatform, TContext> = {} as PluvContext<TPlatform, TContext>;
70
73
  private readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any> };
71
74
  private readonly _debug: boolean;
75
+ private readonly _limits: PluvIOLimits;
72
76
  private readonly _platform: TPlatform;
73
77
 
74
78
  public get procedure(): PluvProcedure<TPlatform, TAuthorize, TContext, {}, {}> {
@@ -76,10 +80,17 @@ export class PluvIO<
76
80
  }
77
81
 
78
82
  constructor(options: PluvIOConfig<TPlatform, TAuthorize, TContext>) {
79
- const { authorize, context, crdt = noop, debug = false, platform } = options;
83
+ const { authorize, context, crdt = noop, debug = false, limits, platform } = options;
80
84
 
81
85
  this._crdt = crdt;
82
86
  this._debug = debug;
87
+ this._limits = {
88
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES,
89
+ storageMaxSize: MAX_STORAGE_SIZE_BYTES,
90
+ userIdMaxLength: MAX_USER_ID_LENGTH,
91
+ userMaxSize: MAX_USER_SIZE_BYTES,
92
+ ...limits,
93
+ };
83
94
  this._platform = platform;
84
95
 
85
96
  if (authorize) this._authorize = authorize;
@@ -90,6 +101,31 @@ export class PluvIO<
90
101
  if (!this._authorize) throw new Error("IO does not specify authorize during initialization.");
91
102
 
92
103
  const ioAuthorize = this._getIOAuthorize(params);
104
+ const user = params.user as BaseUser;
105
+ const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
106
+
107
+ if (!!this._limits.userIdMaxLength && user.id.length > this._limits.userIdMaxLength) {
108
+ throw new Error(
109
+ `createToken was called with a long user id. User ID must be at most 128 characters. Current length: ${user.id.length.toLocaleString()}`,
110
+ );
111
+ }
112
+
113
+ const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
114
+
115
+ if (!!this._limits.userMaxSize && bytes > this._limits.userMaxSize) {
116
+ throw new Error(
117
+ `createToken called with large payload. User must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`,
118
+ );
119
+ }
120
+
121
+ /**
122
+ * !HACK
123
+ * @description Allow the platform to overwrite this behavior as needed. This is introduced
124
+ * to support platformPluv
125
+ * @date December 15, 2024
126
+ */
127
+ if (this._platform._createToken) return await this._platform._createToken(params as any);
128
+
93
129
  const secret = ioAuthorize?.secret ?? null;
94
130
 
95
131
  if (!secret) throw new Error("`authorize` was specified without a valid secret");
@@ -129,6 +165,8 @@ export class PluvIO<
129
165
  context: this._context,
130
166
  crdt: this._crdt,
131
167
  debug: this._debug,
168
+ io: this as PluvIO<TPlatform, TAuthorize, TContext>,
169
+ limits: this._limits,
132
170
  platform: this._platform,
133
171
  } as PluvServerConfig<TPlatform, TAuthorize, TContext, TEvents>);
134
172
  }
package/src/PluvServer.ts CHANGED
@@ -1,23 +1,23 @@
1
1
  import type { AbstractCrdtDocFactory } from "@pluv/crdt";
2
2
  import { noop } from "@pluv/crdt";
3
- import type { BaseUser, IOLike, Id, InferIOAuthorize, InferIOAuthorizeUser, JsonObject, Maybe } from "@pluv/types";
3
+ import type { IOLike, Id, InferIOAuthorize, InferIOAuthorizeUser, JsonObject, Maybe } from "@pluv/types";
4
4
  import colors from "kleur";
5
5
  import type { AbstractPlatform, InferInitContextType, InferRoomContextType } from "./AbstractPlatform";
6
- import type { IORoomListeners, WebSocketRegisterConfig } from "./IORoom";
6
+ import type { IORoomListeners } from "./IORoom";
7
7
  import { IORoom } from "./IORoom";
8
+ import type { PluvIO } from "./PluvIO";
8
9
  import { PluvProcedure } from "./PluvProcedure";
9
10
  import type { PluvRouterEventConfig } from "./PluvRouter";
10
11
  import { PluvRouter } from "./PluvRouter";
11
12
  import type { JWTEncodeParams } from "./authorize";
12
- import { authorize } from "./authorize";
13
- import { MAX_PRESENCE_SIZE_BYTES, MAX_USER_SIZE_BYTES, PING_TIMEOUT_MS } from "./constants";
13
+ import { PING_TIMEOUT_MS } from "./constants";
14
14
  import type {
15
15
  BasePluvIOListeners,
16
16
  GetInitialStorageFn,
17
17
  PluvContext,
18
18
  PluvIOAuthorize,
19
+ PluvIOLimits,
19
20
  PluvIOListeners,
20
- ResolvedPluvIOAuthorize,
21
21
  } from "./types";
22
22
  import { pickBy } from "./utils";
23
23
  import { __PLUV_VERSION } from "./version";
@@ -38,6 +38,8 @@ export type PluvServerConfig<
38
38
  crdt?: { doc: (value: any) => AbstractCrdtDocFactory<any> };
39
39
  debug?: boolean;
40
40
  getInitialStorage?: GetInitialStorageFn<TContext>;
41
+ limits: PluvIOLimits;
42
+ io: PluvIO<TPlatform, TAuthorize, TContext>;
41
43
  platform: TPlatform;
42
44
  router?: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
43
45
  };
@@ -145,9 +147,14 @@ export class PluvServer<
145
147
  });
146
148
  }
147
149
 
148
- const state = (await this._platform.persistence.getStorageState(room)) ?? doc.getEncodedState();
150
+ const encodedState = (await this._platform.persistence.getStorageState(room)) ?? doc.getEncodedState();
151
+ const storageSize = new TextEncoder().encode(encodedState).length;
149
152
 
150
- return { $storageReceived: { state } };
153
+ if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
154
+ throw new Error("Storage has exceeded the size limit");
155
+ }
156
+
157
+ return { $storageReceived: { state: encodedState } };
151
158
  }),
152
159
  $ping: this._procedure.self((data, { session }) => {
153
160
  if (!session) return {};
@@ -175,7 +182,7 @@ export class PluvServer<
175
182
  const updated = Object.assign(Object.create(null), session.presence, cleanedPatch);
176
183
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
177
184
 
178
- if (bytes > MAX_PRESENCE_SIZE_BYTES) {
185
+ if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
179
186
  throw new Error(
180
187
  `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`,
181
188
  );
@@ -198,6 +205,11 @@ export class PluvServer<
198
205
 
199
206
  const updated = update === null ? doc : doc.applyEncodedState({ update });
200
207
  const encodedState = updated.getEncodedState();
208
+ const storageSize = new TextEncoder().encode(encodedState).length;
209
+
210
+ if (!!this._limits.storageMaxSize && storageSize > this._limits.storageMaxSize) {
211
+ throw new Error("Storage has exceeded the size limit");
212
+ }
201
213
 
202
214
  this._platform.persistence.setStorageState(room, encodedState).then(() => {
203
215
  this._getListeners().onStorageUpdated({
@@ -214,6 +226,8 @@ export class PluvServer<
214
226
  private readonly _crdt: { doc: (value: any) => AbstractCrdtDocFactory<any> };
215
227
  private readonly _debug: boolean;
216
228
  private readonly _getInitialStorage: GetInitialStorageFn<TContext> | null = null;
229
+ private readonly _io: PluvIO<TPlatform, TAuthorize, TContext>;
230
+ private readonly _limits: PluvIOLimits;
217
231
  private readonly _platform: TPlatform;
218
232
  private readonly _router: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
219
233
 
@@ -255,6 +269,8 @@ export class PluvServer<
255
269
  crdt = noop,
256
270
  debug = false,
257
271
  getInitialStorage,
272
+ io,
273
+ limits,
258
274
  platform,
259
275
  router = new PluvRouter<TPlatform, TAuthorize, TContext, TEvents>({} as TEvents),
260
276
  } = options;
@@ -266,6 +282,8 @@ export class PluvServer<
266
282
 
267
283
  this._crdt = crdt;
268
284
  this._debug = debug;
285
+ this._io = io;
286
+ this._limits = limits;
269
287
  this._platform = platform;
270
288
  this._router = (router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter) as PluvRouter<
271
289
  TPlatform,
@@ -302,7 +320,7 @@ export class PluvServer<
302
320
  throw new Error(`\`createRoom\` is unsupported for \`${this._platform._name}\``);
303
321
  }
304
322
 
305
- if (!/^[a-z0-9]+[a-z0-9\-_]+[a-z0-9]+$/i.test(room)) throw new Error("Unsupported room name");
323
+ if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room)) throw new Error("Unsupported room name");
306
324
 
307
325
  const roomContext = platformRoomContext as InferRoomContextType<TPlatform>;
308
326
  const context: TContext = typeof this._context === "function" ? this._context(roomContext) : this._context;
@@ -345,41 +363,7 @@ export class PluvServer<
345
363
  public async createToken(
346
364
  params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>, TPlatform>,
347
365
  ): Promise<string> {
348
- if (!this._authorize) {
349
- throw new Error("IO does not specify authorize during initialization.");
350
- }
351
-
352
- const ioAuthorize = this._getIOAuthorize(params);
353
- const user = params.user as BaseUser;
354
- const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
355
-
356
- const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
357
-
358
- if (bytes > MAX_USER_SIZE_BYTES) {
359
- throw new Error(
360
- `createToken called with large payload. User must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`,
361
- );
362
- }
363
-
364
- /**
365
- * !HACK
366
- * @description Allow the platform to overwrite this behavior as needed. This is introduced
367
- * to support platformPluv
368
- * @date December 15, 2024
369
- */
370
- if (this._platform._createToken) return await this._platform._createToken(params as any);
371
-
372
- const secret = ioAuthorize?.secret ?? null;
373
-
374
- if (!secret) throw new Error("`authorize` was specified without a valid secret");
375
-
376
- return await authorize({ platform: this._platform, secret }).encode(params as JWTEncodeParams<any, TPlatform>);
377
- }
378
-
379
- private _getIOAuthorize(options: WebSocketRegisterConfig<TPlatform>): ResolvedPluvIOAuthorize<any, any> | null {
380
- if (typeof this._authorize === "function") return this._authorize(options);
381
-
382
- return this._authorize as ResolvedPluvIOAuthorize<any, any> | null;
366
+ return await this._io.createToken(params);
383
367
  }
384
368
 
385
369
  private _getListeners(): BasePluvIOListeners<TPlatform, TAuthorize, TContext, TEvents> {
package/src/constants.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export const GARBAGE_COLLECT_INTERVAL_MS = 60_000;
2
2
  export const MAX_PRESENCE_SIZE_BYTES = 512;
3
+ export const MAX_STORAGE_SIZE_BYTES = 31_457_280;
4
+ export const MAX_USER_ID_LENGTH = 128;
3
5
  export const MAX_USER_SIZE_BYTES = 512;
4
6
  export const PING_TIMEOUT_MS = 30_000;
package/src/createIO.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { BaseUser } from "@pluv/types";
2
2
  import type { AbstractPlatform, InferInitContextType } from "./AbstractPlatform";
3
3
  import { PluvIO } from "./PluvIO";
4
- import type { CrdtLibraryType, PluvContext, PluvIOAuthorize } from "./types";
4
+ import type { CrdtLibraryType, PluvContext, PluvIOAuthorize, PluvIOLimits } from "./types";
5
5
 
6
6
  export type CreateIOParams<
7
7
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
@@ -12,6 +12,7 @@ export type CreateIOParams<
12
12
  context?: PluvContext<TPlatform, TContext>;
13
13
  crdt?: CrdtLibraryType;
14
14
  debug?: boolean;
15
+ limits?: PluvIOLimits;
15
16
  platform: TPlatform;
16
17
  };
17
18
 
@@ -22,13 +23,14 @@ export const createIO = <
22
23
  >(
23
24
  params: CreateIOParams<TPlatform, TContext, TUser>,
24
25
  ): PluvIO<TPlatform, PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>, TContext> => {
25
- const { authorize, context, crdt, debug, platform } = params;
26
+ const { authorize, context, crdt, debug, limits, platform } = params;
26
27
 
27
28
  return new PluvIO<TPlatform, PluvIOAuthorize<TPlatform, TUser, InferInitContextType<TPlatform>>, TContext>({
28
29
  authorize,
29
30
  context,
30
31
  crdt,
31
32
  debug,
33
+ limits,
32
34
  platform,
33
35
  });
34
36
  };
package/src/index.ts CHANGED
@@ -46,6 +46,7 @@ export type {
46
46
  PlatformConfig,
47
47
  PluvContext,
48
48
  PluvIOAuthorize,
49
+ PluvIOLimits,
49
50
  PluvIOListeners,
50
51
  WebSocketRegistrationMode,
51
52
  WebSocketSerializedState,
package/src/types.ts CHANGED
@@ -145,6 +145,25 @@ export type PluvIOAuthorize<
145
145
  | (TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null)
146
146
  | ((context: TContext) => TUser extends BaseUser ? ResolvedPluvIOAuthorize<TPlatform, TUser> : null);
147
147
 
148
+ export interface PluvIOLimits {
149
+ /**
150
+ * @description Maximum size of presence object in bytes
151
+ */
152
+ presenceMaxSize?: number | null;
153
+ /**
154
+ * @description Maximum size of storage state in bytes
155
+ */
156
+ storageMaxSize?: number | null;
157
+ /**
158
+ * @description Maximum length of user id in characters
159
+ */
160
+ userIdMaxLength?: number | null;
161
+ /**
162
+ * @description Maximum size of user object in bytes
163
+ */
164
+ userMaxSize?: number | null;
165
+ }
166
+
148
167
  export type BasePluvIOListeners<
149
168
  TPlatform extends AbstractPlatform<any, any, any, any>,
150
169
  TAuthorize extends PluvIOAuthorize<TPlatform, any, InferInitContextType<TPlatform>> | null,
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '0.41.7'
2
+ export const version = '0.43.0'