@pluv/io 0.42.0 → 0.44.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.42.0 build /home/runner/work/pluv/pluv/packages/io
2
+ > @pluv/io@0.44.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.42.0 version /home/runner/work/pluv/pluv/packages/io
6
+ > @pluv/io@0.44.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.97 KB
16
- ESM ⚡️ Build success in 129ms
17
- CJS dist/index.js 52.15 KB
18
- CJS ⚡️ Build success in 157ms
15
+ CJS dist/index.js 52.90 KB
16
+ CJS ⚡️ Build success in 142ms
17
+ ESM dist/index.mjs 50.73 KB
18
+ ESM ⚡️ Build success in 143ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 3067ms
21
- DTS dist/index.d.mts 31.53 KB
22
- DTS dist/index.d.ts 31.53 KB
20
+ DTS ⚡️ Build success in 3191ms
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,49 @@
1
1
  # @pluv/io
2
2
 
3
+ ## 0.44.0
4
+
5
+ ### Patch Changes
6
+
7
+ - @pluv/crdt@0.44.0
8
+ - @pluv/types@0.44.0
9
+
10
+ ## 0.43.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 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`.
15
+
16
+ ```ts
17
+ createIO({
18
+ limits: {
19
+ storageMaxSize: null,
20
+ },
21
+ });
22
+ ```
23
+
24
+ - 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`.
25
+
26
+ ```ts
27
+ // defaults
28
+ createIO(
29
+ platformCloudflare({
30
+ limits: {
31
+ presenceMaxSize: 512,
32
+ storageMaxSize: 31_457_280,
33
+ userIdMaxLength: 128,
34
+ userMaxSize: 512,
35
+ },
36
+ }),
37
+ );
38
+ ```
39
+
40
+ ### Patch Changes
41
+
42
+ - e60a677: Fix 1 and 2 character long room names not being supported.
43
+ - 244a973: Fixed not applying user size limits when calling `PluvIO.authorize` and not allowing it to be overwritten for `platformPluv`.
44
+ - @pluv/crdt@0.43.0
45
+ - @pluv/types@0.43.0
46
+
3
47
  ## 0.42.0
4
48
 
5
49
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -195,6 +195,24 @@ type ResolvedPluvIOAuthorize<TPlatform extends AbstractPlatform<any, any, any, a
195
195
  secret?: string;
196
196
  }, Exclude<"secret", InferPlatformAuthorizeProperties<TPlatform>>>;
197
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
+ }
198
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>> = {
199
217
  onRoomDeleted: (event: IORoomListenerEvent<TContext>) => void;
200
218
  onRoomMessage: (event: IORoomMessageEvent<TPlatform, TAuthorize, TContext, TEvents>) => void;
@@ -419,6 +437,8 @@ type PluvServerConfig<TPlatform extends AbstractPlatform<any, any> = AbstractPla
419
437
  };
420
438
  debug?: boolean;
421
439
  getInitialStorage?: GetInitialStorageFn<TContext>;
440
+ limits: PluvIOLimits;
441
+ io: PluvIO<TPlatform, TAuthorize, TContext>;
422
442
  platform: TPlatform;
423
443
  router?: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
424
444
  };
@@ -434,6 +454,8 @@ declare class PluvServer<TPlatform extends AbstractPlatform<any, any> = Abstract
434
454
  private readonly _crdt;
435
455
  private readonly _debug;
436
456
  private readonly _getInitialStorage;
457
+ private readonly _io;
458
+ private readonly _limits;
437
459
  private readonly _platform;
438
460
  private readonly _router;
439
461
  get fetch(): (...args: any[]) => Promise<any>;
@@ -452,7 +474,6 @@ declare class PluvServer<TPlatform extends AbstractPlatform<any, any> = Abstract
452
474
  constructor(options: PluvServerConfig<TPlatform, TAuthorize, TContext, TEvents>);
453
475
  createRoom(room: string, ...options: CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>): IORoom<TPlatform, TAuthorize, TContext, TEvents>;
454
476
  createToken(params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>, TPlatform>): Promise<string>;
455
- private _getIOAuthorize;
456
477
  private _getListeners;
457
478
  private _logDebug;
458
479
  }
@@ -462,6 +483,7 @@ type PluvIOConfig<TPlatform extends AbstractPlatform<any>, TAuthorize extends IO
462
483
  context?: PluvContext<TPlatform, TContext>;
463
484
  crdt?: CrdtLibraryType;
464
485
  debug?: boolean;
486
+ limits?: PluvIOLimits;
465
487
  platform: TPlatform;
466
488
  };
467
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> & {
@@ -476,6 +498,7 @@ declare class PluvIO<TPlatform extends AbstractPlatform<any> = AbstractPlatform<
476
498
  private readonly _context;
477
499
  private readonly _crdt;
478
500
  private readonly _debug;
501
+ private readonly _limits;
479
502
  private readonly _platform;
480
503
  get procedure(): PluvProcedure<TPlatform, TAuthorize, TContext, {}, {}>;
481
504
  constructor(options: PluvIOConfig<TPlatform, TAuthorize, TContext>);
@@ -491,8 +514,9 @@ type CreateIOParams<TPlatform extends AbstractPlatform<any> = AbstractPlatform<a
491
514
  context?: PluvContext<TPlatform, TContext>;
492
515
  crdt?: CrdtLibraryType;
493
516
  debug?: boolean;
517
+ limits?: PluvIOLimits;
494
518
  platform: TPlatform;
495
519
  };
496
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>;
497
521
 
498
- 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
@@ -195,6 +195,24 @@ type ResolvedPluvIOAuthorize<TPlatform extends AbstractPlatform<any, any, any, a
195
195
  secret?: string;
196
196
  }, Exclude<"secret", InferPlatformAuthorizeProperties<TPlatform>>>;
197
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
+ }
198
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>> = {
199
217
  onRoomDeleted: (event: IORoomListenerEvent<TContext>) => void;
200
218
  onRoomMessage: (event: IORoomMessageEvent<TPlatform, TAuthorize, TContext, TEvents>) => void;
@@ -419,6 +437,8 @@ type PluvServerConfig<TPlatform extends AbstractPlatform<any, any> = AbstractPla
419
437
  };
420
438
  debug?: boolean;
421
439
  getInitialStorage?: GetInitialStorageFn<TContext>;
440
+ limits: PluvIOLimits;
441
+ io: PluvIO<TPlatform, TAuthorize, TContext>;
422
442
  platform: TPlatform;
423
443
  router?: PluvRouter<TPlatform, TAuthorize, TContext, TEvents>;
424
444
  };
@@ -434,6 +454,8 @@ declare class PluvServer<TPlatform extends AbstractPlatform<any, any> = Abstract
434
454
  private readonly _crdt;
435
455
  private readonly _debug;
436
456
  private readonly _getInitialStorage;
457
+ private readonly _io;
458
+ private readonly _limits;
437
459
  private readonly _platform;
438
460
  private readonly _router;
439
461
  get fetch(): (...args: any[]) => Promise<any>;
@@ -452,7 +474,6 @@ declare class PluvServer<TPlatform extends AbstractPlatform<any, any> = Abstract
452
474
  constructor(options: PluvServerConfig<TPlatform, TAuthorize, TContext, TEvents>);
453
475
  createRoom(room: string, ...options: CreateRoomOptions<TPlatform, TAuthorize, TContext, TEvents>): IORoom<TPlatform, TAuthorize, TContext, TEvents>;
454
476
  createToken(params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>, TPlatform>): Promise<string>;
455
- private _getIOAuthorize;
456
477
  private _getListeners;
457
478
  private _logDebug;
458
479
  }
@@ -462,6 +483,7 @@ type PluvIOConfig<TPlatform extends AbstractPlatform<any>, TAuthorize extends IO
462
483
  context?: PluvContext<TPlatform, TContext>;
463
484
  crdt?: CrdtLibraryType;
464
485
  debug?: boolean;
486
+ limits?: PluvIOLimits;
465
487
  platform: TPlatform;
466
488
  };
467
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> & {
@@ -476,6 +498,7 @@ declare class PluvIO<TPlatform extends AbstractPlatform<any> = AbstractPlatform<
476
498
  private readonly _context;
477
499
  private readonly _crdt;
478
500
  private readonly _debug;
501
+ private readonly _limits;
479
502
  private readonly _platform;
480
503
  get procedure(): PluvProcedure<TPlatform, TAuthorize, TContext, {}, {}>;
481
504
  constructor(options: PluvIOConfig<TPlatform, TAuthorize, TContext>);
@@ -491,8 +514,9 @@ type CreateIOParams<TPlatform extends AbstractPlatform<any> = AbstractPlatform<a
491
514
  context?: PluvContext<TPlatform, TContext>;
492
515
  crdt?: CrdtLibraryType;
493
516
  debug?: boolean;
517
+ limits?: PluvIOLimits;
494
518
  platform: TPlatform;
495
519
  };
496
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>;
497
521
 
498
- 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
@@ -399,6 +399,8 @@ var import_kleur = __toESM(require("kleur"));
399
399
  // src/constants.ts
400
400
  var GARBAGE_COLLECT_INTERVAL_MS = 6e4;
401
401
  var MAX_PRESENCE_SIZE_BYTES = 512;
402
+ var MAX_STORAGE_SIZE_BYTES = 31457280;
403
+ var MAX_USER_ID_LENGTH = 128;
402
404
  var MAX_USER_SIZE_BYTES = 512;
403
405
  var PING_TIMEOUT_MS = 3e4;
404
406
 
@@ -1139,7 +1141,7 @@ var pickBy = (obj, by) => {
1139
1141
  };
1140
1142
 
1141
1143
  // src/version.generated.ts
1142
- var version = "0.42.0";
1144
+ var version = "0.44.0";
1143
1145
 
1144
1146
  // src/PluvServer.ts
1145
1147
  var PluvServer = class {
@@ -1190,18 +1192,22 @@ var PluvServer = class {
1190
1192
  update
1191
1193
  ];
1192
1194
  if (updates.some((_update) => !!_update)) {
1193
- const encodedState = doc.batchApplyEncodedState(updates).getEncodedState();
1194
- yield this._platform.persistence.setStorageState(room, encodedState);
1195
+ const encodedState2 = doc.batchApplyEncodedState({ updates }).getEncodedState();
1196
+ yield this._platform.persistence.setStorageState(room, encodedState2);
1195
1197
  this._getListeners().onStorageUpdated({
1196
1198
  context,
1197
- encodedState,
1199
+ encodedState: encodedState2,
1198
1200
  room,
1199
1201
  user: session == null ? void 0 : session.user,
1200
1202
  webSocket: session == null ? void 0 : session.webSocket.webSocket
1201
1203
  });
1202
1204
  }
1203
- const state = (_b = yield this._platform.persistence.getStorageState(room)) != null ? _b : doc.getEncodedState();
1204
- 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 } };
1205
1211
  })),
1206
1212
  $ping: this._procedure.self((data, { session }) => {
1207
1213
  if (!session) return {};
@@ -1221,7 +1227,7 @@ var PluvServer = class {
1221
1227
  const cleanedPatch = pickBy(presence != null ? presence : {}, (value) => typeof value !== "undefined");
1222
1228
  const updated = Object.assign(/* @__PURE__ */ Object.create(null), session.presence, cleanedPatch);
1223
1229
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
1224
- if (bytes > MAX_PRESENCE_SIZE_BYTES) {
1230
+ if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
1225
1231
  throw new Error(
1226
1232
  `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1227
1233
  );
@@ -1241,6 +1247,10 @@ var PluvServer = class {
1241
1247
  if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
1242
1248
  const updated = update === null ? doc : doc.applyEncodedState({ update });
1243
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
+ }
1244
1254
  this._platform.persistence.setStorageState(room, encodedState).then(() => {
1245
1255
  this._getListeners().onStorageUpdated({
1246
1256
  context,
@@ -1259,6 +1269,8 @@ var PluvServer = class {
1259
1269
  crdt = import_crdt2.noop,
1260
1270
  debug = false,
1261
1271
  getInitialStorage,
1272
+ io,
1273
+ limits,
1262
1274
  platform,
1263
1275
  router = new PluvRouter({})
1264
1276
  } = options;
@@ -1266,6 +1278,8 @@ var PluvServer = class {
1266
1278
  platform.validateConfig(options);
1267
1279
  this._crdt = crdt;
1268
1280
  this._debug = debug;
1281
+ this._io = io;
1282
+ this._limits = limits;
1269
1283
  this._platform = platform;
1270
1284
  this._router = router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter;
1271
1285
  if (authorize2) this._authorize = authorize2;
@@ -1307,7 +1321,7 @@ var PluvServer = class {
1307
1321
  if (this._platform._config.handleMode !== "io") {
1308
1322
  throw new Error(`\`createRoom\` is unsupported for \`${this._platform._name}\``);
1309
1323
  }
1310
- 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");
1311
1325
  const roomContext = platformRoomContext;
1312
1326
  const context = typeof this._context === "function" ? this._context(roomContext) : this._context;
1313
1327
  const newRoom = new IORoom(room, __spreadProps(__spreadValues({}, !!_meta ? { _meta } : {}), {
@@ -1341,29 +1355,9 @@ var PluvServer = class {
1341
1355
  }
1342
1356
  createToken(params) {
1343
1357
  return __async(this, null, function* () {
1344
- var _a;
1345
- if (!this._authorize) {
1346
- throw new Error("IO does not specify authorize during initialization.");
1347
- }
1348
- const ioAuthorize = this._getIOAuthorize(params);
1349
- const user = params.user;
1350
- const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
1351
- const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
1352
- if (bytes > MAX_USER_SIZE_BYTES) {
1353
- throw new Error(
1354
- `createToken called with large payload. User must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1355
- );
1356
- }
1357
- if (this._platform._createToken) return yield this._platform._createToken(params);
1358
- const secret = (_a = ioAuthorize == null ? void 0 : ioAuthorize.secret) != null ? _a : null;
1359
- if (!secret) throw new Error("`authorize` was specified without a valid secret");
1360
- return yield authorize({ platform: this._platform, secret }).encode(params);
1358
+ return yield this._io.createToken(params);
1361
1359
  });
1362
1360
  }
1363
- _getIOAuthorize(options) {
1364
- if (typeof this._authorize === "function") return this._authorize(options);
1365
- return this._authorize;
1366
- }
1367
1361
  _getListeners() {
1368
1362
  return this._listeners;
1369
1363
  }
@@ -1378,9 +1372,15 @@ var PluvIO = class {
1378
1372
  this.version = version;
1379
1373
  this._authorize = null;
1380
1374
  this._context = {};
1381
- 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;
1382
1376
  this._crdt = crdt;
1383
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);
1384
1384
  this._platform = platform;
1385
1385
  if (authorize2) this._authorize = authorize2;
1386
1386
  if (context) this._context = context;
@@ -1393,6 +1393,20 @@ var PluvIO = class {
1393
1393
  var _a;
1394
1394
  if (!this._authorize) throw new Error("IO does not specify authorize during initialization.");
1395
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);
1396
1410
  const secret = (_a = ioAuthorize == null ? void 0 : ioAuthorize.secret) != null ? _a : null;
1397
1411
  if (!secret) throw new Error("`authorize` was specified without a valid secret");
1398
1412
  return yield authorize({ platform: this._platform, secret }).encode(params);
@@ -1415,6 +1429,8 @@ var PluvIO = class {
1415
1429
  context: this._context,
1416
1430
  crdt: this._crdt,
1417
1431
  debug: this._debug,
1432
+ io: this,
1433
+ limits: this._limits,
1418
1434
  platform: this._platform
1419
1435
  }));
1420
1436
  }
@@ -1428,12 +1444,13 @@ var PluvIO = class {
1428
1444
 
1429
1445
  // src/createIO.ts
1430
1446
  var createIO = (params) => {
1431
- const { authorize: authorize2, context, crdt, debug, platform } = params;
1447
+ const { authorize: authorize2, context, crdt, debug, limits, platform } = params;
1432
1448
  return new PluvIO({
1433
1449
  authorize: authorize2,
1434
1450
  context,
1435
1451
  crdt,
1436
1452
  debug,
1453
+ limits,
1437
1454
  platform
1438
1455
  });
1439
1456
  };
package/dist/index.mjs CHANGED
@@ -357,6 +357,8 @@ import colors from "kleur";
357
357
  // src/constants.ts
358
358
  var GARBAGE_COLLECT_INTERVAL_MS = 6e4;
359
359
  var MAX_PRESENCE_SIZE_BYTES = 512;
360
+ var MAX_STORAGE_SIZE_BYTES = 31457280;
361
+ var MAX_USER_ID_LENGTH = 128;
360
362
  var MAX_USER_SIZE_BYTES = 512;
361
363
  var PING_TIMEOUT_MS = 3e4;
362
364
 
@@ -1097,7 +1099,7 @@ var pickBy = (obj, by) => {
1097
1099
  };
1098
1100
 
1099
1101
  // src/version.generated.ts
1100
- var version = "0.42.0";
1102
+ var version = "0.44.0";
1101
1103
 
1102
1104
  // src/PluvServer.ts
1103
1105
  var PluvServer = class {
@@ -1148,18 +1150,22 @@ var PluvServer = class {
1148
1150
  update
1149
1151
  ];
1150
1152
  if (updates.some((_update) => !!_update)) {
1151
- const encodedState = doc.batchApplyEncodedState(updates).getEncodedState();
1152
- yield this._platform.persistence.setStorageState(room, encodedState);
1153
+ const encodedState2 = doc.batchApplyEncodedState({ updates }).getEncodedState();
1154
+ yield this._platform.persistence.setStorageState(room, encodedState2);
1153
1155
  this._getListeners().onStorageUpdated({
1154
1156
  context,
1155
- encodedState,
1157
+ encodedState: encodedState2,
1156
1158
  room,
1157
1159
  user: session == null ? void 0 : session.user,
1158
1160
  webSocket: session == null ? void 0 : session.webSocket.webSocket
1159
1161
  });
1160
1162
  }
1161
- const state = (_b = yield this._platform.persistence.getStorageState(room)) != null ? _b : doc.getEncodedState();
1162
- 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 } };
1163
1169
  })),
1164
1170
  $ping: this._procedure.self((data, { session }) => {
1165
1171
  if (!session) return {};
@@ -1179,7 +1185,7 @@ var PluvServer = class {
1179
1185
  const cleanedPatch = pickBy(presence != null ? presence : {}, (value) => typeof value !== "undefined");
1180
1186
  const updated = Object.assign(/* @__PURE__ */ Object.create(null), session.presence, cleanedPatch);
1181
1187
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
1182
- if (bytes > MAX_PRESENCE_SIZE_BYTES) {
1188
+ if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
1183
1189
  throw new Error(
1184
1190
  `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1185
1191
  );
@@ -1199,6 +1205,10 @@ var PluvServer = class {
1199
1205
  if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
1200
1206
  const updated = update === null ? doc : doc.applyEncodedState({ update });
1201
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
+ }
1202
1212
  this._platform.persistence.setStorageState(room, encodedState).then(() => {
1203
1213
  this._getListeners().onStorageUpdated({
1204
1214
  context,
@@ -1217,6 +1227,8 @@ var PluvServer = class {
1217
1227
  crdt = noop2,
1218
1228
  debug = false,
1219
1229
  getInitialStorage,
1230
+ io,
1231
+ limits,
1220
1232
  platform,
1221
1233
  router = new PluvRouter({})
1222
1234
  } = options;
@@ -1224,6 +1236,8 @@ var PluvServer = class {
1224
1236
  platform.validateConfig(options);
1225
1237
  this._crdt = crdt;
1226
1238
  this._debug = debug;
1239
+ this._io = io;
1240
+ this._limits = limits;
1227
1241
  this._platform = platform;
1228
1242
  this._router = router ? PluvRouter.merge(this._baseRouter, router) : this._baseRouter;
1229
1243
  if (authorize2) this._authorize = authorize2;
@@ -1265,7 +1279,7 @@ var PluvServer = class {
1265
1279
  if (this._platform._config.handleMode !== "io") {
1266
1280
  throw new Error(`\`createRoom\` is unsupported for \`${this._platform._name}\``);
1267
1281
  }
1268
- 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");
1269
1283
  const roomContext = platformRoomContext;
1270
1284
  const context = typeof this._context === "function" ? this._context(roomContext) : this._context;
1271
1285
  const newRoom = new IORoom(room, __spreadProps(__spreadValues({}, !!_meta ? { _meta } : {}), {
@@ -1299,29 +1313,9 @@ var PluvServer = class {
1299
1313
  }
1300
1314
  createToken(params) {
1301
1315
  return __async(this, null, function* () {
1302
- var _a;
1303
- if (!this._authorize) {
1304
- throw new Error("IO does not specify authorize during initialization.");
1305
- }
1306
- const ioAuthorize = this._getIOAuthorize(params);
1307
- const user = params.user;
1308
- const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
1309
- const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
1310
- if (bytes > MAX_USER_SIZE_BYTES) {
1311
- throw new Error(
1312
- `createToken called with large payload. User must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
1313
- );
1314
- }
1315
- if (this._platform._createToken) return yield this._platform._createToken(params);
1316
- const secret = (_a = ioAuthorize == null ? void 0 : ioAuthorize.secret) != null ? _a : null;
1317
- if (!secret) throw new Error("`authorize` was specified without a valid secret");
1318
- return yield authorize({ platform: this._platform, secret }).encode(params);
1316
+ return yield this._io.createToken(params);
1319
1317
  });
1320
1318
  }
1321
- _getIOAuthorize(options) {
1322
- if (typeof this._authorize === "function") return this._authorize(options);
1323
- return this._authorize;
1324
- }
1325
1319
  _getListeners() {
1326
1320
  return this._listeners;
1327
1321
  }
@@ -1336,9 +1330,15 @@ var PluvIO = class {
1336
1330
  this.version = version;
1337
1331
  this._authorize = null;
1338
1332
  this._context = {};
1339
- const { authorize: authorize2, context, crdt = noop3, debug = false, platform } = options;
1333
+ const { authorize: authorize2, context, crdt = noop3, debug = false, limits, platform } = options;
1340
1334
  this._crdt = crdt;
1341
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);
1342
1342
  this._platform = platform;
1343
1343
  if (authorize2) this._authorize = authorize2;
1344
1344
  if (context) this._context = context;
@@ -1351,6 +1351,20 @@ var PluvIO = class {
1351
1351
  var _a;
1352
1352
  if (!this._authorize) throw new Error("IO does not specify authorize during initialization.");
1353
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);
1354
1368
  const secret = (_a = ioAuthorize == null ? void 0 : ioAuthorize.secret) != null ? _a : null;
1355
1369
  if (!secret) throw new Error("`authorize` was specified without a valid secret");
1356
1370
  return yield authorize({ platform: this._platform, secret }).encode(params);
@@ -1373,6 +1387,8 @@ var PluvIO = class {
1373
1387
  context: this._context,
1374
1388
  crdt: this._crdt,
1375
1389
  debug: this._debug,
1390
+ io: this,
1391
+ limits: this._limits,
1376
1392
  platform: this._platform
1377
1393
  }));
1378
1394
  }
@@ -1386,12 +1402,13 @@ var PluvIO = class {
1386
1402
 
1387
1403
  // src/createIO.ts
1388
1404
  var createIO = (params) => {
1389
- const { authorize: authorize2, context, crdt, debug, platform } = params;
1405
+ const { authorize: authorize2, context, crdt, debug, limits, platform } = params;
1390
1406
  return new PluvIO({
1391
1407
  authorize: authorize2,
1392
1408
  context,
1393
1409
  crdt,
1394
1410
  debug,
1411
+ limits,
1395
1412
  platform
1396
1413
  });
1397
1414
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/io",
3
- "version": "0.42.0",
3
+ "version": "0.44.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.42.0",
25
- "@pluv/types": "^0.42.0"
24
+ "@pluv/crdt": "^0.44.0",
25
+ "@pluv/types": "^0.44.0"
26
26
  },
27
27
  "devDependencies": {
28
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.42.0",
33
- "eslint-config-pluv": "^0.42.0"
32
+ "@pluv/tsconfig": "^0.44.0",
33
+ "eslint-config-pluv": "^0.44.0"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "pnpm run version && tsup src/index.ts --format esm,cjs --dts",
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
  };
@@ -132,7 +134,7 @@ export class PluvServer<
132
134
  ];
133
135
 
134
136
  if (updates.some((_update) => !!_update)) {
135
- const encodedState = doc.batchApplyEncodedState(updates).getEncodedState();
137
+ const encodedState = doc.batchApplyEncodedState({ updates }).getEncodedState();
136
138
 
137
139
  await this._platform.persistence.setStorageState(room, encodedState);
138
140
 
@@ -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.42.0'
2
+ export const version = '0.44.0'