@pluv/client 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,5 +1,5 @@
1
1
 
2
- > @pluv/client@0.41.7 build /home/runner/work/pluv/pluv/packages/client
2
+ > @pluv/client@0.43.0 build /home/runner/work/pluv/pluv/packages/client
3
3
  > tsup src/index.ts --format esm,cjs --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -8,11 +8,11 @@
8
8
  CLI Target: es6
9
9
  ESM Build start
10
10
  CJS Build start
11
- ESM dist/index.mjs 55.12 KB
12
- ESM ⚡️ Build success in 110ms
13
- CJS dist/index.js 56.44 KB
14
- CJS ⚡️ Build success in 116ms
11
+ CJS dist/index.js 57.65 KB
12
+ CJS ⚡️ Build success in 115ms
13
+ ESM dist/index.mjs 56.33 KB
14
+ ESM ⚡️ Build success in 116ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 3198ms
17
- DTS dist/index.d.mts 24.58 KB
18
- DTS dist/index.d.ts 24.58 KB
16
+ DTS ⚡️ Build success in 3550ms
17
+ DTS dist/index.d.mts 25.24 KB
18
+ DTS dist/index.d.ts 25.24 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @pluv/client
2
2
 
3
+ ## 0.43.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 65d9172: Added configurable limits for client-side validation. These should only be updated if you control the server and the server limits have been changed.
8
+
9
+ ```ts
10
+ // defaults
11
+ createClient({
12
+ limits: {
13
+ presenceMaxSize: 512,
14
+ },
15
+ });
16
+ ```
17
+
18
+ - @pluv/crdt@0.43.0
19
+ - @pluv/types@0.43.0
20
+
21
+ ## 0.42.0
22
+
23
+ ### Patch Changes
24
+
25
+ - 7ae56bf: Periodically sync connection ids with the server to clear stale connections.
26
+ - @pluv/crdt@0.42.0
27
+ - @pluv/types@0.42.0
28
+
3
29
  ## 0.41.7
4
30
 
5
31
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -84,6 +84,7 @@ type SubscriptionCallback<TIO extends IOLike, TPresence extends JsonObject, TSub
84
84
 
85
85
  type UsersManagerConfig<TPresence extends JsonObject = {}> = {
86
86
  initialPresence?: TPresence;
87
+ limits: PluvClientLimits;
87
88
  presence?: InputZodLike<TPresence>;
88
89
  };
89
90
 
@@ -130,6 +131,7 @@ type ReconnectTimeoutMs = number | ((params: ReconnectTimeoutMsParams) => number
130
131
  type RoomConfig<TIO extends IOLike, TMetadata extends JsonObject = {}, TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}, TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}> = Id<{
131
132
  addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
132
133
  debug?: boolean | PluvRoomDebug<TIO>;
134
+ limits: PluvClientLimits;
133
135
  onAuthorizationFail?: (error: Error) => void;
134
136
  metadata?: InputZodLike<TMetadata>;
135
137
  publicKey?: PublicKey<TMetadata>;
@@ -144,6 +146,7 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
144
146
  private readonly _debug;
145
147
  private readonly _eventNotifier;
146
148
  private readonly _intervals;
149
+ private readonly _limits;
147
150
  private readonly _listeners;
148
151
  private readonly _otherNotifier;
149
152
  private readonly _publicKey;
@@ -206,6 +209,7 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
206
209
  private _handleRegisteredMessage;
207
210
  private _handleStorageReceivedMessage;
208
211
  private _handleStorageUpdatedMessage;
212
+ private _handleSyncStateReceived;
209
213
  private _handleUserJoinedMessage;
210
214
  private _heartbeat;
211
215
  private _logDebug;
@@ -234,6 +238,11 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
234
238
  type PluvClientOptions<TIO extends IOLike<any, any>, TPresence extends JsonObject, TStorage extends Record<string, CrdtType<any, any>>, TMetadata extends JsonObject> = RoomEndpoints<TIO, TMetadata> & {
235
239
  debug?: boolean;
236
240
  initialStorage?: AbstractCrdtDocFactory<TStorage>;
241
+ /**
242
+ * @description Configurable limits defined for client-side validation. You should only set
243
+ * this if you control the server and have changed the limits there.
244
+ */
245
+ limits?: PluvClientLimits;
237
246
  metadata?: InputZodLike<TMetadata>;
238
247
  presence?: InputZodLike<TPresence>;
239
248
  publicKey?: PublicKey<TMetadata>;
@@ -254,6 +263,7 @@ declare class PluvClient<TIO extends IOLike<any, any>, TPresence extends JsonObj
254
263
  private readonly _authEndpoint;
255
264
  private readonly _debug;
256
265
  private readonly _initialStorage?;
266
+ private readonly _limits;
257
267
  private readonly _presence?;
258
268
  private readonly _publicKey;
259
269
  private readonly _rooms;
@@ -290,6 +300,12 @@ type MergeEventRecords<TEventRecords extends EventRecord<string, any>[], TRoot e
290
300
  ] ? MergeEventRecords<ITail, Omit<TRoot, keyof IHead> & {
291
301
  [P in keyof IHead]: TRoot extends Record<P, any> ? TRoot[P] | IHead[P] : IHead[P];
292
302
  }> : Id<TRoot>;
303
+ interface PluvClientLimits {
304
+ /**
305
+ * @description Maximum size of presence object in bytes
306
+ */
307
+ presenceMaxSize?: number | null;
308
+ }
293
309
  interface PublicKeyParams<TMetadata extends JsonObject> {
294
310
  metadata: TMetadata;
295
311
  }
@@ -367,19 +383,21 @@ type MockedRoomEvents<TIO extends IOLike> = Partial<{
367
383
  }>;
368
384
  type MockedRoomConfig<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}, TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}> = {
369
385
  events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
386
+ limits?: PluvClientLimits;
370
387
  router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
371
- } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> & UsersManagerConfig<TPresence>;
388
+ } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> & Omit<UsersManagerConfig<TPresence>, "limits">;
372
389
  declare class MockedRoom<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}, TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}> extends AbstractRoom<TIO, TPresence, TStorage> {
373
- private _crdtManager;
374
- private _crdtNotifier;
375
- private _eventNotifier;
376
- private _events?;
377
- private _otherNotifier;
378
- private _router;
390
+ private readonly _crdtManager;
391
+ private readonly _crdtNotifier;
392
+ private readonly _eventNotifier;
393
+ private readonly _events?;
394
+ private readonly _limits;
395
+ private readonly _otherNotifier;
396
+ private readonly _router;
379
397
  private _state;
380
- private _stateNotifier;
381
- private _subscriptions;
382
- private _usersManager;
398
+ private readonly _stateNotifier;
399
+ private readonly _subscriptions;
400
+ private readonly _usersManager;
383
401
  constructor(room: string, options: MockedRoomConfig<TIO, TPresence, TStorage, TEvents>);
384
402
  get storageLoaded(): boolean;
385
403
  broadcast: (<TEvent extends keyof InferIOInput<MergeEvents<TEvents, TIO>>>(event: TEvent, data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[TEvent]>) => Promise<void>) & { [event in keyof InferIOInput<TIO>]: (data: Id<InferIOInput<TIO>[event]>) => Promise<void>; };
package/dist/index.d.ts CHANGED
@@ -84,6 +84,7 @@ type SubscriptionCallback<TIO extends IOLike, TPresence extends JsonObject, TSub
84
84
 
85
85
  type UsersManagerConfig<TPresence extends JsonObject = {}> = {
86
86
  initialPresence?: TPresence;
87
+ limits: PluvClientLimits;
87
88
  presence?: InputZodLike<TPresence>;
88
89
  };
89
90
 
@@ -130,6 +131,7 @@ type ReconnectTimeoutMs = number | ((params: ReconnectTimeoutMsParams) => number
130
131
  type RoomConfig<TIO extends IOLike, TMetadata extends JsonObject = {}, TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}, TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}> = Id<{
131
132
  addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
132
133
  debug?: boolean | PluvRoomDebug<TIO>;
134
+ limits: PluvClientLimits;
133
135
  onAuthorizationFail?: (error: Error) => void;
134
136
  metadata?: InputZodLike<TMetadata>;
135
137
  publicKey?: PublicKey<TMetadata>;
@@ -144,6 +146,7 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
144
146
  private readonly _debug;
145
147
  private readonly _eventNotifier;
146
148
  private readonly _intervals;
149
+ private readonly _limits;
147
150
  private readonly _listeners;
148
151
  private readonly _otherNotifier;
149
152
  private readonly _publicKey;
@@ -206,6 +209,7 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
206
209
  private _handleRegisteredMessage;
207
210
  private _handleStorageReceivedMessage;
208
211
  private _handleStorageUpdatedMessage;
212
+ private _handleSyncStateReceived;
209
213
  private _handleUserJoinedMessage;
210
214
  private _heartbeat;
211
215
  private _logDebug;
@@ -234,6 +238,11 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
234
238
  type PluvClientOptions<TIO extends IOLike<any, any>, TPresence extends JsonObject, TStorage extends Record<string, CrdtType<any, any>>, TMetadata extends JsonObject> = RoomEndpoints<TIO, TMetadata> & {
235
239
  debug?: boolean;
236
240
  initialStorage?: AbstractCrdtDocFactory<TStorage>;
241
+ /**
242
+ * @description Configurable limits defined for client-side validation. You should only set
243
+ * this if you control the server and have changed the limits there.
244
+ */
245
+ limits?: PluvClientLimits;
237
246
  metadata?: InputZodLike<TMetadata>;
238
247
  presence?: InputZodLike<TPresence>;
239
248
  publicKey?: PublicKey<TMetadata>;
@@ -254,6 +263,7 @@ declare class PluvClient<TIO extends IOLike<any, any>, TPresence extends JsonObj
254
263
  private readonly _authEndpoint;
255
264
  private readonly _debug;
256
265
  private readonly _initialStorage?;
266
+ private readonly _limits;
257
267
  private readonly _presence?;
258
268
  private readonly _publicKey;
259
269
  private readonly _rooms;
@@ -290,6 +300,12 @@ type MergeEventRecords<TEventRecords extends EventRecord<string, any>[], TRoot e
290
300
  ] ? MergeEventRecords<ITail, Omit<TRoot, keyof IHead> & {
291
301
  [P in keyof IHead]: TRoot extends Record<P, any> ? TRoot[P] | IHead[P] : IHead[P];
292
302
  }> : Id<TRoot>;
303
+ interface PluvClientLimits {
304
+ /**
305
+ * @description Maximum size of presence object in bytes
306
+ */
307
+ presenceMaxSize?: number | null;
308
+ }
293
309
  interface PublicKeyParams<TMetadata extends JsonObject> {
294
310
  metadata: TMetadata;
295
311
  }
@@ -367,19 +383,21 @@ type MockedRoomEvents<TIO extends IOLike> = Partial<{
367
383
  }>;
368
384
  type MockedRoomConfig<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}, TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}> = {
369
385
  events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
386
+ limits?: PluvClientLimits;
370
387
  router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
371
- } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> & UsersManagerConfig<TPresence>;
388
+ } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> & Omit<UsersManagerConfig<TPresence>, "limits">;
372
389
  declare class MockedRoom<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}, TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}> extends AbstractRoom<TIO, TPresence, TStorage> {
373
- private _crdtManager;
374
- private _crdtNotifier;
375
- private _eventNotifier;
376
- private _events?;
377
- private _otherNotifier;
378
- private _router;
390
+ private readonly _crdtManager;
391
+ private readonly _crdtNotifier;
392
+ private readonly _eventNotifier;
393
+ private readonly _events?;
394
+ private readonly _limits;
395
+ private readonly _otherNotifier;
396
+ private readonly _router;
379
397
  private _state;
380
- private _stateNotifier;
381
- private _subscriptions;
382
- private _usersManager;
398
+ private readonly _stateNotifier;
399
+ private readonly _subscriptions;
400
+ private readonly _usersManager;
383
401
  constructor(room: string, options: MockedRoomConfig<TIO, TPresence, TStorage, TEvents>);
384
402
  get storageLoaded(): boolean;
385
403
  broadcast: (<TEvent extends keyof InferIOInput<MergeEvents<TEvents, TIO>>>(event: TEvent, data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[TEvent]>) => Promise<void>) & { [event in keyof InferIOInput<TIO>]: (data: Id<InferIOInput<TIO>[event]>) => Promise<void>; };
package/dist/index.js CHANGED
@@ -83,6 +83,9 @@ var AbstractStorageStore = class {
83
83
  }
84
84
  };
85
85
 
86
+ // src/constants.ts
87
+ var MAX_PRESENCE_SIZE_BYTES = 512;
88
+
86
89
  // src/PluvProcedure.ts
87
90
  var PluvProcedure = class _PluvProcedure {
88
91
  constructor(config = {}) {
@@ -317,9 +320,6 @@ var StorageStore = class extends AbstractStorageStore {
317
320
  }
318
321
  };
319
322
 
320
- // src/constants.ts
321
- var MAX_PRESENCE_SIZE_BYTES = 512;
322
-
323
323
  // src/utils/debounce.ts
324
324
  var debounce = (fn, options) => {
325
325
  const { wait, immediate = false } = options != null ? options : {};
@@ -364,8 +364,9 @@ var UsersManager = class {
364
364
  this._myself = null;
365
365
  this._others = /* @__PURE__ */ new Map();
366
366
  this._presence = null;
367
- const { initialPresence, presence = null } = config;
367
+ const { initialPresence, limits, presence = null } = config;
368
368
  this.initialPresence = initialPresence;
369
+ this._limits = limits;
369
370
  this._myPresence = initialPresence;
370
371
  this._presence = presence;
371
372
  }
@@ -498,7 +499,7 @@ var UsersManager = class {
498
499
  const cleanedPresence = pickBy(this._myPresence, (value) => typeof value !== "undefined");
499
500
  const updated = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
500
501
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
501
- if (bytes > MAX_PRESENCE_SIZE_BYTES) {
502
+ if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
502
503
  throw new Error(
503
504
  `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
504
505
  );
@@ -563,6 +564,7 @@ var PluvRoom = class extends AbstractRoom {
563
564
  debug = false,
564
565
  initialPresence,
565
566
  initialStorage,
567
+ limits,
566
568
  metadata,
567
569
  onAuthorizationFail,
568
570
  presence,
@@ -738,6 +740,7 @@ var PluvRoom = class extends AbstractRoom {
738
740
  this.metadata = metadata;
739
741
  this._debug = debug;
740
742
  this._endpoints = { authEndpoint, wsEndpoint };
743
+ this._limits = limits;
741
744
  this._reconnectTimeoutMs = reconnectTimeoutMs;
742
745
  this._storageStore = storage;
743
746
  if (!!publicKey) this._publicKey = publicKey;
@@ -747,7 +750,7 @@ var PluvRoom = class extends AbstractRoom {
747
750
  }
748
751
  };
749
752
  this._router = router != null ? router : new PluvRouter({});
750
- this._usersManager = new UsersManager({ initialPresence, presence });
753
+ this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
751
754
  this._crdtManager = new CrdtManager({ initialStorage });
752
755
  }
753
756
  get storageLoaded() {
@@ -1113,6 +1116,21 @@ var PluvRoom = class extends AbstractRoom {
1113
1116
  );
1114
1117
  this._crdtNotifier.rootSubject.next(storageRoot);
1115
1118
  }
1119
+ _handleSyncStateReceived(message) {
1120
+ var _a;
1121
+ const { connectionId } = message;
1122
+ if (!connectionId) return;
1123
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1124
+ if (!this._usersManager.myself) return;
1125
+ const data = message.data;
1126
+ const active = new Set((_a = data == null ? void 0 : data.connectionIds) != null ? _a : []);
1127
+ const quitters = this._usersManager.getOthers().filter((other) => !active.has(other.connectionId));
1128
+ quitters.forEach((quitter) => {
1129
+ this._usersManager.deleteConnection(quitter.connectionId);
1130
+ });
1131
+ const remaining = this._usersManager.getOthers();
1132
+ this._stateNotifier.subjects.others.next(remaining);
1133
+ }
1116
1134
  _handleUserJoinedMessage(message) {
1117
1135
  const { connectionId } = message;
1118
1136
  if (!connectionId) return;
@@ -1250,6 +1268,10 @@ var PluvRoom = class extends AbstractRoom {
1250
1268
  this._handleStorageUpdatedMessage(message);
1251
1269
  return;
1252
1270
  }
1271
+ case "$syncStateReceived": {
1272
+ this._handleSyncStateReceived(message);
1273
+ return;
1274
+ }
1253
1275
  case "$userJoined": {
1254
1276
  this._handleUserJoinedMessage(message);
1255
1277
  return;
@@ -1383,6 +1405,7 @@ var PluvClient = class {
1383
1405
  debug: options.debug,
1384
1406
  initialPresence: options.initialPresence,
1385
1407
  initialStorage: this._initialStorage,
1408
+ limits: this._limits,
1386
1409
  metadata: this.metadata,
1387
1410
  onAuthorizationFail: options.onAuthorizationFail,
1388
1411
  presence: this._presence,
@@ -1417,11 +1440,23 @@ var PluvClient = class {
1417
1440
  this._rooms.delete(toLeave.id);
1418
1441
  this._logDebug(`Left and deleted room: ${room}`);
1419
1442
  });
1420
- const { authEndpoint, debug = false, initialStorage, metadata, presence, publicKey, wsEndpoint } = options;
1443
+ const {
1444
+ authEndpoint,
1445
+ debug = false,
1446
+ initialStorage,
1447
+ limits,
1448
+ metadata,
1449
+ presence,
1450
+ publicKey,
1451
+ wsEndpoint
1452
+ } = options;
1421
1453
  this.metadata = metadata;
1422
1454
  this._authEndpoint = authEndpoint;
1423
1455
  this._debug = debug;
1424
1456
  this._initialStorage = initialStorage;
1457
+ this._limits = __spreadValues({
1458
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1459
+ }, limits);
1425
1460
  this._presence = presence;
1426
1461
  this._wsEndpoint = wsEndpoint;
1427
1462
  if (!!publicKey) this._publicKey = publicKey;
@@ -1459,7 +1494,7 @@ var infer = (callback) => callback;
1459
1494
  // src/MockedRoom.ts
1460
1495
  var MockedRoom = class extends AbstractRoom {
1461
1496
  constructor(room, options) {
1462
- const { events, initialPresence, initialStorage, presence, router } = options;
1497
+ const { events, initialPresence, initialStorage, limits, presence, router } = options;
1463
1498
  super(room);
1464
1499
  this._crdtNotifier = new CrdtNotifier();
1465
1500
  this._eventNotifier = new EventNotifier();
@@ -1590,11 +1625,11 @@ var MockedRoom = class extends AbstractRoom {
1590
1625
  if (!!myself) this._stateNotifier.subjects["myself"].next(myself);
1591
1626
  };
1592
1627
  this._events = events;
1628
+ this._limits = __spreadValues({
1629
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1630
+ }, limits);
1593
1631
  this._router = router != null ? router : new PluvRouter({});
1594
- this._usersManager = new UsersManager({
1595
- initialPresence,
1596
- presence
1597
- });
1632
+ this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
1598
1633
  this._crdtManager = new CrdtManager({
1599
1634
  initialStorage
1600
1635
  });
package/dist/index.mjs CHANGED
@@ -52,6 +52,9 @@ var AbstractStorageStore = class {
52
52
  }
53
53
  };
54
54
 
55
+ // src/constants.ts
56
+ var MAX_PRESENCE_SIZE_BYTES = 512;
57
+
55
58
  // src/PluvProcedure.ts
56
59
  var PluvProcedure = class _PluvProcedure {
57
60
  constructor(config = {}) {
@@ -286,9 +289,6 @@ var StorageStore = class extends AbstractStorageStore {
286
289
  }
287
290
  };
288
291
 
289
- // src/constants.ts
290
- var MAX_PRESENCE_SIZE_BYTES = 512;
291
-
292
292
  // src/utils/debounce.ts
293
293
  var debounce = (fn, options) => {
294
294
  const { wait, immediate = false } = options != null ? options : {};
@@ -333,8 +333,9 @@ var UsersManager = class {
333
333
  this._myself = null;
334
334
  this._others = /* @__PURE__ */ new Map();
335
335
  this._presence = null;
336
- const { initialPresence, presence = null } = config;
336
+ const { initialPresence, limits, presence = null } = config;
337
337
  this.initialPresence = initialPresence;
338
+ this._limits = limits;
338
339
  this._myPresence = initialPresence;
339
340
  this._presence = presence;
340
341
  }
@@ -467,7 +468,7 @@ var UsersManager = class {
467
468
  const cleanedPresence = pickBy(this._myPresence, (value) => typeof value !== "undefined");
468
469
  const updated = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
469
470
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
470
- if (bytes > MAX_PRESENCE_SIZE_BYTES) {
471
+ if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
471
472
  throw new Error(
472
473
  `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
473
474
  );
@@ -532,6 +533,7 @@ var PluvRoom = class extends AbstractRoom {
532
533
  debug = false,
533
534
  initialPresence,
534
535
  initialStorage,
536
+ limits,
535
537
  metadata,
536
538
  onAuthorizationFail,
537
539
  presence,
@@ -707,6 +709,7 @@ var PluvRoom = class extends AbstractRoom {
707
709
  this.metadata = metadata;
708
710
  this._debug = debug;
709
711
  this._endpoints = { authEndpoint, wsEndpoint };
712
+ this._limits = limits;
710
713
  this._reconnectTimeoutMs = reconnectTimeoutMs;
711
714
  this._storageStore = storage;
712
715
  if (!!publicKey) this._publicKey = publicKey;
@@ -716,7 +719,7 @@ var PluvRoom = class extends AbstractRoom {
716
719
  }
717
720
  };
718
721
  this._router = router != null ? router : new PluvRouter({});
719
- this._usersManager = new UsersManager({ initialPresence, presence });
722
+ this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
720
723
  this._crdtManager = new CrdtManager({ initialStorage });
721
724
  }
722
725
  get storageLoaded() {
@@ -1082,6 +1085,21 @@ var PluvRoom = class extends AbstractRoom {
1082
1085
  );
1083
1086
  this._crdtNotifier.rootSubject.next(storageRoot);
1084
1087
  }
1088
+ _handleSyncStateReceived(message) {
1089
+ var _a;
1090
+ const { connectionId } = message;
1091
+ if (!connectionId) return;
1092
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1093
+ if (!this._usersManager.myself) return;
1094
+ const data = message.data;
1095
+ const active = new Set((_a = data == null ? void 0 : data.connectionIds) != null ? _a : []);
1096
+ const quitters = this._usersManager.getOthers().filter((other) => !active.has(other.connectionId));
1097
+ quitters.forEach((quitter) => {
1098
+ this._usersManager.deleteConnection(quitter.connectionId);
1099
+ });
1100
+ const remaining = this._usersManager.getOthers();
1101
+ this._stateNotifier.subjects.others.next(remaining);
1102
+ }
1085
1103
  _handleUserJoinedMessage(message) {
1086
1104
  const { connectionId } = message;
1087
1105
  if (!connectionId) return;
@@ -1219,6 +1237,10 @@ var PluvRoom = class extends AbstractRoom {
1219
1237
  this._handleStorageUpdatedMessage(message);
1220
1238
  return;
1221
1239
  }
1240
+ case "$syncStateReceived": {
1241
+ this._handleSyncStateReceived(message);
1242
+ return;
1243
+ }
1222
1244
  case "$userJoined": {
1223
1245
  this._handleUserJoinedMessage(message);
1224
1246
  return;
@@ -1352,6 +1374,7 @@ var PluvClient = class {
1352
1374
  debug: options.debug,
1353
1375
  initialPresence: options.initialPresence,
1354
1376
  initialStorage: this._initialStorage,
1377
+ limits: this._limits,
1355
1378
  metadata: this.metadata,
1356
1379
  onAuthorizationFail: options.onAuthorizationFail,
1357
1380
  presence: this._presence,
@@ -1386,11 +1409,23 @@ var PluvClient = class {
1386
1409
  this._rooms.delete(toLeave.id);
1387
1410
  this._logDebug(`Left and deleted room: ${room}`);
1388
1411
  });
1389
- const { authEndpoint, debug = false, initialStorage, metadata, presence, publicKey, wsEndpoint } = options;
1412
+ const {
1413
+ authEndpoint,
1414
+ debug = false,
1415
+ initialStorage,
1416
+ limits,
1417
+ metadata,
1418
+ presence,
1419
+ publicKey,
1420
+ wsEndpoint
1421
+ } = options;
1390
1422
  this.metadata = metadata;
1391
1423
  this._authEndpoint = authEndpoint;
1392
1424
  this._debug = debug;
1393
1425
  this._initialStorage = initialStorage;
1426
+ this._limits = __spreadValues({
1427
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1428
+ }, limits);
1394
1429
  this._presence = presence;
1395
1430
  this._wsEndpoint = wsEndpoint;
1396
1431
  if (!!publicKey) this._publicKey = publicKey;
@@ -1428,7 +1463,7 @@ var infer = (callback) => callback;
1428
1463
  // src/MockedRoom.ts
1429
1464
  var MockedRoom = class extends AbstractRoom {
1430
1465
  constructor(room, options) {
1431
- const { events, initialPresence, initialStorage, presence, router } = options;
1466
+ const { events, initialPresence, initialStorage, limits, presence, router } = options;
1432
1467
  super(room);
1433
1468
  this._crdtNotifier = new CrdtNotifier();
1434
1469
  this._eventNotifier = new EventNotifier();
@@ -1559,11 +1594,11 @@ var MockedRoom = class extends AbstractRoom {
1559
1594
  if (!!myself) this._stateNotifier.subjects["myself"].next(myself);
1560
1595
  };
1561
1596
  this._events = events;
1597
+ this._limits = __spreadValues({
1598
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1599
+ }, limits);
1562
1600
  this._router = router != null ? router : new PluvRouter({});
1563
- this._usersManager = new UsersManager({
1564
- initialPresence,
1565
- presence
1566
- });
1601
+ this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
1567
1602
  this._crdtManager = new CrdtManager({
1568
1603
  initialStorage
1569
1604
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/client",
3
- "version": "0.41.7",
3
+ "version": "0.43.0",
4
4
  "description": "@pluv/io frontend client",
5
5
  "author": "leedavidcs",
6
6
  "license": "MIT",
@@ -18,15 +18,15 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "wonka": "^6.3.5",
21
- "@pluv/crdt": "^0.41.7",
22
- "@pluv/types": "^0.41.7"
21
+ "@pluv/crdt": "^0.43.0",
22
+ "@pluv/types": "^0.43.0"
23
23
  },
24
24
  "devDependencies": {
25
- "eslint": "^9.25.0",
25
+ "eslint": "^9.25.1",
26
26
  "tsup": "^8.4.0",
27
27
  "typescript": "^5.8.3",
28
- "@pluv/tsconfig": "^0.41.7",
29
- "eslint-config-pluv": "^0.41.7"
28
+ "@pluv/tsconfig": "^0.43.0",
29
+ "eslint-config-pluv": "^0.43.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup src/index.ts --format esm,cjs --dts",
package/src/MockedRoom.ts CHANGED
@@ -20,11 +20,13 @@ import type {
20
20
  EventResolver,
21
21
  EventResolverContext,
22
22
  InternalSubscriptions,
23
+ PluvClientLimits,
23
24
  UpdateMyPresenceAction,
24
25
  UserInfo,
25
26
  WebSocketConnection,
26
27
  WebSocketState,
27
28
  } from "./types";
29
+ import { MAX_PRESENCE_SIZE_BYTES } from "./constants";
28
30
 
29
31
  export type MockedRoomEvents<TIO extends IOLike> = Partial<{
30
32
  [P in keyof InferIOInput<TIO>]: (data: Id<InferIOInput<TIO>[P]>) => Partial<InferIOOutput<TIO>>;
@@ -37,9 +39,10 @@ export type MockedRoomConfig<
37
39
  TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
38
40
  > = {
39
41
  events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
42
+ limits?: PluvClientLimits;
40
43
  router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
41
44
  } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> &
42
- UsersManagerConfig<TPresence>;
45
+ Omit<UsersManagerConfig<TPresence>, "limits">;
43
46
 
44
47
  export class MockedRoom<
45
48
  TIO extends IOLike,
@@ -47,12 +50,13 @@ export class MockedRoom<
47
50
  TStorage extends Record<string, CrdtType<any, any>> = {},
48
51
  TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
49
52
  > extends AbstractRoom<TIO, TPresence, TStorage> {
50
- private _crdtManager: CrdtManager<TStorage>;
51
- private _crdtNotifier = new CrdtNotifier<TStorage>();
52
- private _eventNotifier = new EventNotifier<MergeEvents<TEvents, TIO>>();
53
- private _events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
54
- private _otherNotifier = new OtherNotifier<TIO, TPresence>();
55
- private _router: PluvRouter<TIO, TPresence, TStorage, TEvents>;
53
+ private readonly _crdtManager: CrdtManager<TStorage>;
54
+ private readonly _crdtNotifier = new CrdtNotifier<TStorage>();
55
+ private readonly _eventNotifier = new EventNotifier<MergeEvents<TEvents, TIO>>();
56
+ private readonly _events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
57
+ private readonly _limits: PluvClientLimits;
58
+ private readonly _otherNotifier = new OtherNotifier<TIO, TPresence>();
59
+ private readonly _router: PluvRouter<TIO, TPresence, TStorage, TEvents>;
56
60
  private _state: WebSocketState<TIO> = {
57
61
  authorization: {
58
62
  token: null,
@@ -66,24 +70,24 @@ export class MockedRoom<
66
70
  },
67
71
  webSocket: null,
68
72
  };
69
- private _stateNotifier = new StateNotifier<TIO, TPresence>();
70
- private _subscriptions: InternalSubscriptions = {
73
+ private readonly _stateNotifier = new StateNotifier<TIO, TPresence>();
74
+ private readonly _subscriptions: InternalSubscriptions = {
71
75
  observeCrdt: null,
72
76
  };
73
- private _usersManager: UsersManager<TIO, TPresence>;
77
+ private readonly _usersManager: UsersManager<TIO, TPresence>;
74
78
 
75
79
  constructor(room: string, options: MockedRoomConfig<TIO, TPresence, TStorage, TEvents>) {
76
- const { events, initialPresence, initialStorage, presence, router } = options;
80
+ const { events, initialPresence, initialStorage, limits, presence, router } = options;
77
81
 
78
82
  super(room);
79
83
 
80
84
  this._events = events;
81
-
85
+ this._limits = {
86
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES,
87
+ ...limits,
88
+ };
82
89
  this._router = router ?? (new PluvRouter({}) as PluvRouter<TIO, TPresence, TStorage, TEvents>);
83
- this._usersManager = new UsersManager<TIO, TPresence>({
84
- initialPresence,
85
- presence,
86
- });
90
+ this._usersManager = new UsersManager<TIO, TPresence>({ initialPresence, limits: this._limits, presence });
87
91
 
88
92
  this._crdtManager = new CrdtManager<TStorage>({
89
93
  initialStorage,
package/src/PluvClient.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AbstractCrdtDocFactory, CrdtType } from "@pluv/crdt";
2
2
  import type { InputZodLike, IOLike, JsonObject } from "@pluv/types";
3
+ import { MAX_PRESENCE_SIZE_BYTES } from "./constants";
3
4
  import type { InferCallback } from "./infer";
4
5
  import { PluvProcedure } from "./PluvProcedure";
5
6
  import type {
@@ -14,7 +15,7 @@ import type {
14
15
  import { PluvRoom } from "./PluvRoom";
15
16
  import type { PluvRouterEventConfig } from "./PluvRouter";
16
17
  import { PluvRouter } from "./PluvRouter";
17
- import type { PublicKey, WithMetadata } from "./types";
18
+ import type { PluvClientLimits, PublicKey, WithMetadata } from "./types";
18
19
 
19
20
  export type PluvClientOptions<
20
21
  TIO extends IOLike<any, any>,
@@ -24,6 +25,11 @@ export type PluvClientOptions<
24
25
  > = RoomEndpoints<TIO, TMetadata> & {
25
26
  debug?: boolean;
26
27
  initialStorage?: AbstractCrdtDocFactory<TStorage>;
28
+ /**
29
+ * @description Configurable limits defined for client-side validation. You should only set
30
+ * this if you control the server and have changed the limits there.
31
+ */
32
+ limits?: PluvClientLimits;
27
33
  metadata?: InputZodLike<TMetadata>;
28
34
  presence?: InputZodLike<TPresence>;
29
35
  publicKey?: PublicKey<TMetadata>;
@@ -61,6 +67,7 @@ export class PluvClient<
61
67
  private readonly _authEndpoint: AuthEndpoint<TMetadata> | undefined;
62
68
  private readonly _debug: boolean;
63
69
  private readonly _initialStorage?: AbstractCrdtDocFactory<TStorage>;
70
+ private readonly _limits: PluvClientLimits;
64
71
  private readonly _presence?: InputZodLike<TPresence>;
65
72
  private readonly _publicKey: PublicKey<TMetadata> | null = null;
66
73
  private readonly _rooms = new Map<string, PluvRoom<TIO, TMetadata, TPresence, TStorage, any>>();
@@ -77,13 +84,26 @@ export class PluvClient<
77
84
  }
78
85
 
79
86
  constructor(options: PluvClientOptions<TIO, TPresence, TStorage, TMetadata>) {
80
- const { authEndpoint, debug = false, initialStorage, metadata, presence, publicKey, wsEndpoint } = options;
87
+ const {
88
+ authEndpoint,
89
+ debug = false,
90
+ initialStorage,
91
+ limits,
92
+ metadata,
93
+ presence,
94
+ publicKey,
95
+ wsEndpoint,
96
+ } = options;
81
97
 
82
98
  this.metadata = metadata;
83
99
 
84
100
  this._authEndpoint = authEndpoint as AuthEndpoint<TMetadata>;
85
101
  this._debug = debug;
86
102
  this._initialStorage = initialStorage;
103
+ this._limits = {
104
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES,
105
+ ...limits,
106
+ };
87
107
  this._presence = presence;
88
108
  this._wsEndpoint = wsEndpoint;
89
109
 
@@ -104,6 +124,7 @@ export class PluvClient<
104
124
  debug: options.debug,
105
125
  initialPresence: options.initialPresence,
106
126
  initialStorage: this._initialStorage,
127
+ limits: this._limits,
107
128
  metadata: this.metadata,
108
129
  onAuthorizationFail: options.onAuthorizationFail,
109
130
  presence: this._presence,
package/src/PluvRoom.ts CHANGED
@@ -37,6 +37,7 @@ import type {
37
37
  EventResolver,
38
38
  EventResolverContext,
39
39
  InternalSubscriptions,
40
+ PluvClientLimits,
40
41
  PublicKey,
41
42
  UpdateMyPresenceAction,
42
43
  UserInfo,
@@ -169,6 +170,7 @@ export type RoomConfig<
169
170
  {
170
171
  addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
171
172
  debug?: boolean | PluvRoomDebug<TIO>;
173
+ limits: PluvClientLimits;
172
174
  onAuthorizationFail?: (error: Error) => void;
173
175
  metadata?: InputZodLike<TMetadata>;
174
176
  publicKey?: PublicKey<TMetadata>;
@@ -197,6 +199,7 @@ export class PluvRoom<
197
199
  private readonly _intervals: IntervalIds = {
198
200
  heartbeat: null,
199
201
  };
202
+ private readonly _limits: PluvClientLimits;
200
203
  private readonly _listeners: InternalListeners;
201
204
  private readonly _otherNotifier = new OtherNotifier<TIO, TPresence>();
202
205
  private readonly _publicKey: PublicKey<TMetadata> | null = null;
@@ -237,6 +240,7 @@ export class PluvRoom<
237
240
  debug = false,
238
241
  initialPresence,
239
242
  initialStorage,
243
+ limits,
240
244
  metadata,
241
245
  onAuthorizationFail,
242
246
  presence,
@@ -259,6 +263,7 @@ export class PluvRoom<
259
263
 
260
264
  this._debug = debug;
261
265
  this._endpoints = { authEndpoint, wsEndpoint } as RoomEndpoints<TIO, TMetadata>;
266
+ this._limits = limits;
262
267
  this._reconnectTimeoutMs = reconnectTimeoutMs;
263
268
  this._storageStore = storage;
264
269
 
@@ -271,7 +276,7 @@ export class PluvRoom<
271
276
  };
272
277
 
273
278
  this._router = router ?? (new PluvRouter({}) as PluvRouter<TIO, TPresence, TStorage, TEvents>);
274
- this._usersManager = new UsersManager<TIO, TPresence>({ initialPresence, presence });
279
+ this._usersManager = new UsersManager<TIO, TPresence>({ initialPresence, limits: this._limits, presence });
275
280
  this._crdtManager = new CrdtManager<TStorage>({ initialStorage });
276
281
  }
277
282
 
@@ -1017,6 +1022,28 @@ export class PluvRoom<
1017
1022
  this._crdtNotifier.rootSubject.next(storageRoot);
1018
1023
  }
1019
1024
 
1025
+ private _handleSyncStateReceived(message: IOEventMessage<TIO>): void {
1026
+ const { connectionId } = message;
1027
+
1028
+ if (!connectionId) return;
1029
+ // Should not reach here
1030
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1031
+ if (!this._usersManager.myself) return;
1032
+
1033
+ const data = message.data as BaseIOEventRecord<InferIOAuthorize<TIO>>["$syncStateReceived"];
1034
+ const active = new Set(data?.connectionIds ?? []);
1035
+
1036
+ const quitters = this._usersManager.getOthers().filter((other) => !active.has(other.connectionId));
1037
+
1038
+ quitters.forEach((quitter) => {
1039
+ this._usersManager.deleteConnection(quitter.connectionId);
1040
+ });
1041
+
1042
+ const remaining = this._usersManager.getOthers();
1043
+
1044
+ this._stateNotifier.subjects.others.next(remaining);
1045
+ }
1046
+
1020
1047
  private _handleUserJoinedMessage(message: IOEventMessage<TIO>): void {
1021
1048
  const { connectionId } = message;
1022
1049
 
@@ -1190,6 +1217,10 @@ export class PluvRoom<
1190
1217
  this._handleStorageUpdatedMessage(message);
1191
1218
  return;
1192
1219
  }
1220
+ case "$syncStateReceived": {
1221
+ this._handleSyncStateReceived(message);
1222
+ return;
1223
+ }
1193
1224
  case "$userJoined": {
1194
1225
  this._handleUserJoinedMessage(message);
1195
1226
  return;
@@ -1,12 +1,12 @@
1
1
  import type { InputZodLike, IOLike, JsonObject, OptionalProps } from "@pluv/types";
2
- import { MAX_PRESENCE_SIZE_BYTES } from "./constants";
3
- import type { UserInfo } from "./types";
2
+ import type { PluvClientLimits, UserInfo } from "./types";
4
3
  import { pickBy } from "./utils";
5
4
 
6
5
  export type Presence = Record<string, unknown>;
7
6
 
8
7
  export type UsersManagerConfig<TPresence extends JsonObject = {}> = {
9
8
  initialPresence?: TPresence;
9
+ limits: PluvClientLimits;
10
10
  presence?: InputZodLike<TPresence>;
11
11
  };
12
12
 
@@ -24,12 +24,13 @@ export type DeleteConnectionResult<TIO extends IOLike, TPresence extends JsonObj
24
24
  };
25
25
 
26
26
  export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}> {
27
- private _idMap = {
27
+ public readonly initialPresence: TPresence;
28
+
29
+ private readonly _idMap = {
28
30
  fromConnectionId: new Map<[connectionId: string][0], [clientId: string][0]>(),
29
31
  fromClientId: new Map<[clientId: string][0], Set<[connectionId: string][0]>>(),
30
32
  };
31
-
32
- public readonly initialPresence: TPresence;
33
+ private _limits: PluvClientLimits;
33
34
  /**
34
35
  * @description This presence can be updated while the user is not
35
36
  * connected.
@@ -43,10 +44,11 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
43
44
  private _presence: InputZodLike<TPresence> | null = null;
44
45
 
45
46
  constructor(config: UsersManagerConfig<TPresence>) {
46
- const { initialPresence, presence = null } = config;
47
+ const { initialPresence, limits, presence = null } = config;
47
48
 
48
49
  this.initialPresence = initialPresence as TPresence;
49
50
 
51
+ this._limits = limits;
50
52
  this._myPresence = initialPresence as TPresence;
51
53
  this._presence = presence;
52
54
  }
@@ -256,7 +258,7 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
256
258
  const updated = { ...this.initialPresence, ...cleanedPresence, ...cleanedPatch } as TPresence;
257
259
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
258
260
 
259
- if (bytes > MAX_PRESENCE_SIZE_BYTES) {
261
+ if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
260
262
  throw new Error(
261
263
  `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`,
262
264
  );
package/src/types.ts CHANGED
@@ -57,6 +57,13 @@ export type MergeEventRecords<
57
57
  >
58
58
  : Id<TRoot>;
59
59
 
60
+ export interface PluvClientLimits {
61
+ /**
62
+ * @description Maximum size of presence object in bytes
63
+ */
64
+ presenceMaxSize?: number | null;
65
+ }
66
+
60
67
  export interface PublicKeyParams<TMetadata extends JsonObject> {
61
68
  metadata: TMetadata;
62
69
  }