@pluv/client 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,5 +1,5 @@
1
1
 
2
- > @pluv/client@0.42.0 build /home/runner/work/pluv/pluv/packages/client
2
+ > @pluv/client@0.44.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.91 KB
11
+ ESM dist/index.mjs 56.45 KB
12
12
  ESM ⚡️ Build success in 112ms
13
- CJS dist/index.js 57.22 KB
14
- CJS ⚡️ Build success in 118ms
13
+ CJS dist/index.js 57.76 KB
14
+ CJS ⚡️ Build success in 112ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 3444ms
17
- DTS dist/index.d.mts 24.61 KB
18
- DTS dist/index.d.ts 24.61 KB
16
+ DTS ⚡️ Build success in 3446ms
17
+ DTS dist/index.d.mts 25.36 KB
18
+ DTS dist/index.d.ts 25.36 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @pluv/client
2
2
 
3
+ ## 0.44.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c9fcea5: **BREAKING** The `ConnectionState` enum is now a `const` to avoid several pitfalls of using TypeScript enums.
8
+
9
+ ### Patch Changes
10
+
11
+ - fba9354: Fix potential edge case where event handlers may be called redundantly.
12
+ - bc2d684: Fix `PluvRoom` not properly reconnecting when connections are unavailable.
13
+ - @pluv/crdt@0.44.0
14
+ - @pluv/types@0.44.0
15
+
16
+ ## 0.43.0
17
+
18
+ ### Patch Changes
19
+
20
+ - 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.
21
+
22
+ ```ts
23
+ // defaults
24
+ createClient({
25
+ limits: {
26
+ presenceMaxSize: 512,
27
+ },
28
+ });
29
+ ```
30
+
31
+ - @pluv/crdt@0.43.0
32
+ - @pluv/types@0.43.0
33
+
3
34
  ## 0.42.0
4
35
 
5
36
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -6,13 +6,14 @@ import { Subject } from 'wonka';
6
6
 
7
7
  type EventNotifierSubscriptionCallback<TIO extends IOLike<any, any>, TEvent extends keyof InferIOOutput<TIO>> = (value: Id<IOEventMessage<TIO, TEvent>>) => void;
8
8
 
9
- declare enum ConnectionState {
10
- Closed = "Closed",
11
- Connecting = "Connecting",
12
- Open = "Open",
13
- Unavailable = "Unavailable",
14
- Untouched = "Untouched"
15
- }
9
+ declare const ConnectionState: {
10
+ readonly Closed: "Closed";
11
+ readonly Connecting: "Connecting";
12
+ readonly Open: "Open";
13
+ readonly Unavailable: "Unavailable";
14
+ readonly Untouched: "Untouched";
15
+ };
16
+ type ConnectionState = (typeof ConnectionState)[keyof typeof ConnectionState];
16
17
 
17
18
  declare const identity: <T extends unknown>(x: T) => T;
18
19
 
@@ -84,6 +85,7 @@ type SubscriptionCallback<TIO extends IOLike, TPresence extends JsonObject, TSub
84
85
 
85
86
  type UsersManagerConfig<TPresence extends JsonObject = {}> = {
86
87
  initialPresence?: TPresence;
88
+ limits: PluvClientLimits;
87
89
  presence?: InputZodLike<TPresence>;
88
90
  };
89
91
 
@@ -130,6 +132,7 @@ type ReconnectTimeoutMs = number | ((params: ReconnectTimeoutMsParams) => number
130
132
  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
133
  addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
132
134
  debug?: boolean | PluvRoomDebug<TIO>;
135
+ limits: PluvClientLimits;
133
136
  onAuthorizationFail?: (error: Error) => void;
134
137
  metadata?: InputZodLike<TMetadata>;
135
138
  publicKey?: PublicKey<TMetadata>;
@@ -144,6 +147,7 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
144
147
  private readonly _debug;
145
148
  private readonly _eventNotifier;
146
149
  private readonly _intervals;
150
+ private readonly _limits;
147
151
  private readonly _listeners;
148
152
  private readonly _otherNotifier;
149
153
  private readonly _publicKey;
@@ -235,6 +239,11 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
235
239
  type PluvClientOptions<TIO extends IOLike<any, any>, TPresence extends JsonObject, TStorage extends Record<string, CrdtType<any, any>>, TMetadata extends JsonObject> = RoomEndpoints<TIO, TMetadata> & {
236
240
  debug?: boolean;
237
241
  initialStorage?: AbstractCrdtDocFactory<TStorage>;
242
+ /**
243
+ * @description Configurable limits defined for client-side validation. You should only set
244
+ * this if you control the server and have changed the limits there.
245
+ */
246
+ limits?: PluvClientLimits;
238
247
  metadata?: InputZodLike<TMetadata>;
239
248
  presence?: InputZodLike<TPresence>;
240
249
  publicKey?: PublicKey<TMetadata>;
@@ -255,6 +264,7 @@ declare class PluvClient<TIO extends IOLike<any, any>, TPresence extends JsonObj
255
264
  private readonly _authEndpoint;
256
265
  private readonly _debug;
257
266
  private readonly _initialStorage?;
267
+ private readonly _limits;
258
268
  private readonly _presence?;
259
269
  private readonly _publicKey;
260
270
  private readonly _rooms;
@@ -291,6 +301,12 @@ type MergeEventRecords<TEventRecords extends EventRecord<string, any>[], TRoot e
291
301
  ] ? MergeEventRecords<ITail, Omit<TRoot, keyof IHead> & {
292
302
  [P in keyof IHead]: TRoot extends Record<P, any> ? TRoot[P] | IHead[P] : IHead[P];
293
303
  }> : Id<TRoot>;
304
+ interface PluvClientLimits {
305
+ /**
306
+ * @description Maximum size of presence object in bytes
307
+ */
308
+ presenceMaxSize?: number | null;
309
+ }
294
310
  interface PublicKeyParams<TMetadata extends JsonObject> {
295
311
  metadata: TMetadata;
296
312
  }
@@ -368,19 +384,21 @@ type MockedRoomEvents<TIO extends IOLike> = Partial<{
368
384
  }>;
369
385
  type MockedRoomConfig<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}, TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}> = {
370
386
  events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
387
+ limits?: PluvClientLimits;
371
388
  router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
372
- } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> & UsersManagerConfig<TPresence>;
389
+ } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> & Omit<UsersManagerConfig<TPresence>, "limits">;
373
390
  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> {
374
- private _crdtManager;
375
- private _crdtNotifier;
376
- private _eventNotifier;
377
- private _events?;
378
- private _otherNotifier;
379
- private _router;
391
+ private readonly _crdtManager;
392
+ private readonly _crdtNotifier;
393
+ private readonly _eventNotifier;
394
+ private readonly _events?;
395
+ private readonly _limits;
396
+ private readonly _otherNotifier;
397
+ private readonly _router;
380
398
  private _state;
381
- private _stateNotifier;
382
- private _subscriptions;
383
- private _usersManager;
399
+ private readonly _stateNotifier;
400
+ private readonly _subscriptions;
401
+ private readonly _usersManager;
384
402
  constructor(room: string, options: MockedRoomConfig<TIO, TPresence, TStorage, TEvents>);
385
403
  get storageLoaded(): boolean;
386
404
  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
@@ -6,13 +6,14 @@ import { Subject } from 'wonka';
6
6
 
7
7
  type EventNotifierSubscriptionCallback<TIO extends IOLike<any, any>, TEvent extends keyof InferIOOutput<TIO>> = (value: Id<IOEventMessage<TIO, TEvent>>) => void;
8
8
 
9
- declare enum ConnectionState {
10
- Closed = "Closed",
11
- Connecting = "Connecting",
12
- Open = "Open",
13
- Unavailable = "Unavailable",
14
- Untouched = "Untouched"
15
- }
9
+ declare const ConnectionState: {
10
+ readonly Closed: "Closed";
11
+ readonly Connecting: "Connecting";
12
+ readonly Open: "Open";
13
+ readonly Unavailable: "Unavailable";
14
+ readonly Untouched: "Untouched";
15
+ };
16
+ type ConnectionState = (typeof ConnectionState)[keyof typeof ConnectionState];
16
17
 
17
18
  declare const identity: <T extends unknown>(x: T) => T;
18
19
 
@@ -84,6 +85,7 @@ type SubscriptionCallback<TIO extends IOLike, TPresence extends JsonObject, TSub
84
85
 
85
86
  type UsersManagerConfig<TPresence extends JsonObject = {}> = {
86
87
  initialPresence?: TPresence;
88
+ limits: PluvClientLimits;
87
89
  presence?: InputZodLike<TPresence>;
88
90
  };
89
91
 
@@ -130,6 +132,7 @@ type ReconnectTimeoutMs = number | ((params: ReconnectTimeoutMsParams) => number
130
132
  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
133
  addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
132
134
  debug?: boolean | PluvRoomDebug<TIO>;
135
+ limits: PluvClientLimits;
133
136
  onAuthorizationFail?: (error: Error) => void;
134
137
  metadata?: InputZodLike<TMetadata>;
135
138
  publicKey?: PublicKey<TMetadata>;
@@ -144,6 +147,7 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
144
147
  private readonly _debug;
145
148
  private readonly _eventNotifier;
146
149
  private readonly _intervals;
150
+ private readonly _limits;
147
151
  private readonly _listeners;
148
152
  private readonly _otherNotifier;
149
153
  private readonly _publicKey;
@@ -235,6 +239,11 @@ declare class PluvRoom<TIO extends IOLike<any, any>, TMetadata extends JsonObjec
235
239
  type PluvClientOptions<TIO extends IOLike<any, any>, TPresence extends JsonObject, TStorage extends Record<string, CrdtType<any, any>>, TMetadata extends JsonObject> = RoomEndpoints<TIO, TMetadata> & {
236
240
  debug?: boolean;
237
241
  initialStorage?: AbstractCrdtDocFactory<TStorage>;
242
+ /**
243
+ * @description Configurable limits defined for client-side validation. You should only set
244
+ * this if you control the server and have changed the limits there.
245
+ */
246
+ limits?: PluvClientLimits;
238
247
  metadata?: InputZodLike<TMetadata>;
239
248
  presence?: InputZodLike<TPresence>;
240
249
  publicKey?: PublicKey<TMetadata>;
@@ -255,6 +264,7 @@ declare class PluvClient<TIO extends IOLike<any, any>, TPresence extends JsonObj
255
264
  private readonly _authEndpoint;
256
265
  private readonly _debug;
257
266
  private readonly _initialStorage?;
267
+ private readonly _limits;
258
268
  private readonly _presence?;
259
269
  private readonly _publicKey;
260
270
  private readonly _rooms;
@@ -291,6 +301,12 @@ type MergeEventRecords<TEventRecords extends EventRecord<string, any>[], TRoot e
291
301
  ] ? MergeEventRecords<ITail, Omit<TRoot, keyof IHead> & {
292
302
  [P in keyof IHead]: TRoot extends Record<P, any> ? TRoot[P] | IHead[P] : IHead[P];
293
303
  }> : Id<TRoot>;
304
+ interface PluvClientLimits {
305
+ /**
306
+ * @description Maximum size of presence object in bytes
307
+ */
308
+ presenceMaxSize?: number | null;
309
+ }
294
310
  interface PublicKeyParams<TMetadata extends JsonObject> {
295
311
  metadata: TMetadata;
296
312
  }
@@ -368,19 +384,21 @@ type MockedRoomEvents<TIO extends IOLike> = Partial<{
368
384
  }>;
369
385
  type MockedRoomConfig<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}, TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {}> = {
370
386
  events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
387
+ limits?: PluvClientLimits;
371
388
  router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
372
- } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> & UsersManagerConfig<TPresence>;
389
+ } & Pick<CrdtManagerOptions<TStorage>, "initialStorage"> & Omit<UsersManagerConfig<TPresence>, "limits">;
373
390
  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> {
374
- private _crdtManager;
375
- private _crdtNotifier;
376
- private _eventNotifier;
377
- private _events?;
378
- private _otherNotifier;
379
- private _router;
391
+ private readonly _crdtManager;
392
+ private readonly _crdtNotifier;
393
+ private readonly _eventNotifier;
394
+ private readonly _events?;
395
+ private readonly _limits;
396
+ private readonly _otherNotifier;
397
+ private readonly _router;
380
398
  private _state;
381
- private _stateNotifier;
382
- private _subscriptions;
383
- private _usersManager;
399
+ private readonly _stateNotifier;
400
+ private readonly _subscriptions;
401
+ private readonly _usersManager;
384
402
  constructor(room: string, options: MockedRoomConfig<TIO, TPresence, TStorage, TEvents>);
385
403
  get storageLoaded(): boolean;
386
404
  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 = {}) {
@@ -169,7 +172,7 @@ var CrdtManager = class {
169
172
  }
170
173
  _applyDocUpdates(doc, updates, origin) {
171
174
  doc.transact(() => {
172
- doc.batchApplyEncodedState(updates.map((update) => ({ origin, update })));
175
+ doc.batchApplyEncodedState({ origin, updates });
173
176
  }, origin);
174
177
  return doc;
175
178
  }
@@ -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
  );
@@ -534,14 +535,13 @@ var UsersManager = class {
534
535
  };
535
536
 
536
537
  // src/enums.ts
537
- var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
538
- ConnectionState2["Closed"] = "Closed";
539
- ConnectionState2["Connecting"] = "Connecting";
540
- ConnectionState2["Open"] = "Open";
541
- ConnectionState2["Unavailable"] = "Unavailable";
542
- ConnectionState2["Untouched"] = "Untouched";
543
- return ConnectionState2;
544
- })(ConnectionState || {});
538
+ var ConnectionState = {
539
+ Closed: "Closed",
540
+ Connecting: "Connecting",
541
+ Open: "Open",
542
+ Unavailable: "Unavailable",
543
+ Untouched: "Untouched"
544
+ };
545
545
 
546
546
  // src/PluvRoom.ts
547
547
  var ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1e3;
@@ -563,6 +563,7 @@ var PluvRoom = class extends AbstractRoom {
563
563
  debug = false,
564
564
  initialPresence,
565
565
  initialStorage,
566
+ limits,
566
567
  metadata,
567
568
  onAuthorizationFail,
568
569
  presence,
@@ -597,7 +598,7 @@ var PluvRoom = class extends AbstractRoom {
597
598
  attempts: 0,
598
599
  count: 0,
599
600
  id: null,
600
- state: "Untouched" /* Untouched */
601
+ state: ConnectionState.Untouched
601
602
  },
602
603
  webSocket: null
603
604
  };
@@ -606,7 +607,7 @@ var PluvRoom = class extends AbstractRoom {
606
607
  this.broadcast = new Proxy(
607
608
  (event, data) => __async(this, null, function* () {
608
609
  if (!this._state.webSocket) return;
609
- if (this._state.connection.state !== "Open" /* Open */) return;
610
+ if (this._state.connection.state !== ConnectionState.Open) return;
610
611
  const type = event.toString();
611
612
  const procedure = this._router._defs.events[type];
612
613
  if (!(procedure == null ? void 0 : procedure.config.broadcast)) {
@@ -738,6 +739,7 @@ var PluvRoom = class extends AbstractRoom {
738
739
  this.metadata = metadata;
739
740
  this._debug = debug;
740
741
  this._endpoints = { authEndpoint, wsEndpoint };
742
+ this._limits = limits;
741
743
  this._reconnectTimeoutMs = reconnectTimeoutMs;
742
744
  this._storageStore = storage;
743
745
  if (!!publicKey) this._publicKey = publicKey;
@@ -747,7 +749,7 @@ var PluvRoom = class extends AbstractRoom {
747
749
  }
748
750
  };
749
751
  this._router = router != null ? router : new PluvRouter({});
750
- this._usersManager = new UsersManager({ initialPresence, presence });
752
+ this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
751
753
  this._crdtManager = new CrdtManager({ initialStorage });
752
754
  }
753
755
  get storageLoaded() {
@@ -762,13 +764,13 @@ var PluvRoom = class extends AbstractRoom {
762
764
  const params = (_a = args[0]) != null ? _a : {};
763
765
  const metadata = params.metadata;
764
766
  this._setMetadata(metadata);
765
- const canConnect = ["Closed" /* Closed */, "Untouched" /* Untouched */].some(
767
+ const canConnect = [ConnectionState.Closed, ConnectionState.Unavailable, ConnectionState.Untouched].some(
766
768
  (state) => this._state.connection.state === state
767
769
  );
768
770
  if (!canConnect) return;
769
771
  this._clearTimeout(this._timeouts.reconnect);
770
772
  this._updateState((oldState) => {
771
- oldState.connection.state = "Connecting" /* Connecting */;
773
+ oldState.connection.state = ConnectionState.Connecting;
772
774
  return oldState;
773
775
  });
774
776
  yield this._storageStore.initialize();
@@ -806,7 +808,7 @@ var PluvRoom = class extends AbstractRoom {
806
808
  disconnect() {
807
809
  return __async(this, null, function* () {
808
810
  if (!this._state.webSocket) return;
809
- const canDisconnect = ["Connecting" /* Connecting */, "Open" /* Open */].some(
811
+ const canDisconnect = [ConnectionState.Connecting, ConnectionState.Open].some(
810
812
  (state) => this._state.connection.state === state
811
813
  );
812
814
  if (!canDisconnect) return;
@@ -815,7 +817,7 @@ var PluvRoom = class extends AbstractRoom {
815
817
  this._clearTimeout(this._timeouts.reconnect);
816
818
  this._updateState((oldState) => {
817
819
  oldState.authorization.token = null;
818
- oldState.connection.state = "Closed" /* Closed */;
820
+ oldState.connection.state = ConnectionState.Closed;
819
821
  oldState.webSocket = null;
820
822
  return oldState;
821
823
  });
@@ -854,7 +856,7 @@ var PluvRoom = class extends AbstractRoom {
854
856
  _attachWindowListeners() {
855
857
  if (typeof window === "undefined") return;
856
858
  if (typeof document === "undefined") return;
857
- this._windowListeners = {
859
+ this._windowListeners = this._windowListeners || {
858
860
  onNavigatorOffline: this._onNavigatorOffline.bind(this),
859
861
  onNavigatorOnline: this._onNavigatorOnline.bind(this),
860
862
  onVisibilityChange: this._onVisibilityChange.bind(this)
@@ -865,7 +867,7 @@ var PluvRoom = class extends AbstractRoom {
865
867
  }
866
868
  _attachWsListeners() {
867
869
  if (!this._state.webSocket) return;
868
- this._wsListeners = {
870
+ this._wsListeners = this._wsListeners || {
869
871
  onClose: this._onClose.bind(this),
870
872
  onError: this._onError.bind(this),
871
873
  onMessage: this._onMessage.bind(this),
@@ -1185,7 +1187,7 @@ var PluvRoom = class extends AbstractRoom {
1185
1187
  const authError = error instanceof Error ? error : new Error("Authorization failed");
1186
1188
  this._listeners.onAuthorizationFail(authError);
1187
1189
  this._updateState((oldState) => {
1188
- oldState.connection.state = "Unavailable" /* Unavailable */;
1190
+ oldState.connection.state = ConnectionState.Unavailable;
1189
1191
  return oldState;
1190
1192
  });
1191
1193
  }
@@ -1209,7 +1211,15 @@ var PluvRoom = class extends AbstractRoom {
1209
1211
  ].some((okToRetryCode) => event.code === okToRetryCode);
1210
1212
  this._updateState((oldState) => {
1211
1213
  oldState.authorization.token = null;
1212
- oldState.connection.state = shouldRetry ? "Unavailable" /* Unavailable */ : "Closed" /* Closed */;
1214
+ oldState.connection.state = shouldRetry ? ConnectionState.Unavailable : (
1215
+ /**
1216
+ * !HACK
1217
+ * @description Assume these are not recoverable. We're just going to mark this
1218
+ * as Closed to avoid reconnect attempts.
1219
+ * @date April 28, 2025
1220
+ */
1221
+ ConnectionState.Closed
1222
+ );
1213
1223
  oldState.webSocket = null;
1214
1224
  return oldState;
1215
1225
  });
@@ -1277,10 +1287,10 @@ var PluvRoom = class extends AbstractRoom {
1277
1287
  }
1278
1288
  }
1279
1289
  _onOpen() {
1280
- if (this._state.connection.state !== "Connecting" /* Connecting */) return;
1290
+ if (this._state.connection.state !== ConnectionState.Connecting) return;
1281
1291
  this._logDebug("WebSocket connected");
1282
1292
  this._updateState((oldState) => {
1283
- oldState.connection.state = "Open" /* Open */;
1293
+ oldState.connection.state = ConnectionState.Open;
1284
1294
  return oldState;
1285
1295
  });
1286
1296
  this._clearInterval(this._intervals.heartbeat);
@@ -1288,16 +1298,16 @@ var PluvRoom = class extends AbstractRoom {
1288
1298
  }
1289
1299
  _onNavigatorOffline() {
1290
1300
  return __async(this, null, function* () {
1291
- if (this._state.connection.state === "Closed" /* Closed */) return;
1301
+ if (this._state.connection.state === ConnectionState.Closed) return;
1292
1302
  this._updateState((oldState) => {
1293
- oldState.connection.state = "Unavailable" /* Unavailable */;
1303
+ oldState.connection.state = ConnectionState.Unavailable;
1294
1304
  return oldState;
1295
1305
  });
1296
1306
  });
1297
1307
  }
1298
1308
  _onNavigatorOnline() {
1299
1309
  return __async(this, null, function* () {
1300
- if (this._state.connection.state !== "Unavailable" /* Unavailable */) return;
1310
+ if (this._state.connection.state !== ConnectionState.Unavailable) return;
1301
1311
  yield this._reconnect();
1302
1312
  });
1303
1313
  }
@@ -1306,12 +1316,12 @@ var PluvRoom = class extends AbstractRoom {
1306
1316
  if (typeof document === "undefined") return;
1307
1317
  if (document.visibilityState !== "visible") return;
1308
1318
  switch (this._state.connection.state) {
1309
- case "Open" /* Open */: {
1319
+ case ConnectionState.Open: {
1310
1320
  this._clearInterval(this._intervals.heartbeat);
1311
1321
  setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1312
1322
  return;
1313
1323
  }
1314
- case "Unavailable" /* Unavailable */: {
1324
+ case ConnectionState.Unavailable: {
1315
1325
  yield this._reconnect();
1316
1326
  return;
1317
1327
  }
@@ -1339,8 +1349,8 @@ var PluvRoom = class extends AbstractRoom {
1339
1349
  }
1340
1350
  _reconnect() {
1341
1351
  return __async(this, null, function* () {
1342
- if (!this._state.webSocket) return;
1343
1352
  this._closeWs();
1353
+ if (this._state.connection.state === ConnectionState.Closed) return;
1344
1354
  const metadata = this._lastMetadata;
1345
1355
  const params = typeof metadata === "undefined" ? [] : [{ metadata }];
1346
1356
  yield this.connect(...params);
@@ -1402,6 +1412,7 @@ var PluvClient = class {
1402
1412
  debug: options.debug,
1403
1413
  initialPresence: options.initialPresence,
1404
1414
  initialStorage: this._initialStorage,
1415
+ limits: this._limits,
1405
1416
  metadata: this.metadata,
1406
1417
  onAuthorizationFail: options.onAuthorizationFail,
1407
1418
  presence: this._presence,
@@ -1436,11 +1447,23 @@ var PluvClient = class {
1436
1447
  this._rooms.delete(toLeave.id);
1437
1448
  this._logDebug(`Left and deleted room: ${room}`);
1438
1449
  });
1439
- const { authEndpoint, debug = false, initialStorage, metadata, presence, publicKey, wsEndpoint } = options;
1450
+ const {
1451
+ authEndpoint,
1452
+ debug = false,
1453
+ initialStorage,
1454
+ limits,
1455
+ metadata,
1456
+ presence,
1457
+ publicKey,
1458
+ wsEndpoint
1459
+ } = options;
1440
1460
  this.metadata = metadata;
1441
1461
  this._authEndpoint = authEndpoint;
1442
1462
  this._debug = debug;
1443
1463
  this._initialStorage = initialStorage;
1464
+ this._limits = __spreadValues({
1465
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1466
+ }, limits);
1444
1467
  this._presence = presence;
1445
1468
  this._wsEndpoint = wsEndpoint;
1446
1469
  if (!!publicKey) this._publicKey = publicKey;
@@ -1478,7 +1501,7 @@ var infer = (callback) => callback;
1478
1501
  // src/MockedRoom.ts
1479
1502
  var MockedRoom = class extends AbstractRoom {
1480
1503
  constructor(room, options) {
1481
- const { events, initialPresence, initialStorage, presence, router } = options;
1504
+ const { events, initialPresence, initialStorage, limits, presence, router } = options;
1482
1505
  super(room);
1483
1506
  this._crdtNotifier = new CrdtNotifier();
1484
1507
  this._eventNotifier = new EventNotifier();
@@ -1492,7 +1515,7 @@ var MockedRoom = class extends AbstractRoom {
1492
1515
  attempts: 0,
1493
1516
  count: 1,
1494
1517
  id: null,
1495
- state: "Untouched" /* Untouched */
1518
+ state: ConnectionState.Untouched
1496
1519
  },
1497
1520
  webSocket: null
1498
1521
  };
@@ -1503,7 +1526,7 @@ var MockedRoom = class extends AbstractRoom {
1503
1526
  this.broadcast = new Proxy(
1504
1527
  (event, data) => __async(this, null, function* () {
1505
1528
  if (!this._state.webSocket) return;
1506
- if (this._state.connection.state !== "Open" /* Open */) return;
1529
+ if (this._state.connection.state !== ConnectionState.Open) return;
1507
1530
  const type = event.toString();
1508
1531
  const procedure = this._router._defs.events[type];
1509
1532
  if (!(procedure == null ? void 0 : procedure.config.broadcast)) {
@@ -1609,11 +1632,11 @@ var MockedRoom = class extends AbstractRoom {
1609
1632
  if (!!myself) this._stateNotifier.subjects["myself"].next(myself);
1610
1633
  };
1611
1634
  this._events = events;
1635
+ this._limits = __spreadValues({
1636
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1637
+ }, limits);
1612
1638
  this._router = router != null ? router : new PluvRouter({});
1613
- this._usersManager = new UsersManager({
1614
- initialPresence,
1615
- presence
1616
- });
1639
+ this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
1617
1640
  this._crdtManager = new CrdtManager({
1618
1641
  initialStorage
1619
1642
  });
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 = {}) {
@@ -138,7 +141,7 @@ var CrdtManager = class {
138
141
  }
139
142
  _applyDocUpdates(doc, updates, origin) {
140
143
  doc.transact(() => {
141
- doc.batchApplyEncodedState(updates.map((update) => ({ origin, update })));
144
+ doc.batchApplyEncodedState({ origin, updates });
142
145
  }, origin);
143
146
  return doc;
144
147
  }
@@ -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
  );
@@ -503,14 +504,13 @@ var UsersManager = class {
503
504
  };
504
505
 
505
506
  // src/enums.ts
506
- var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
507
- ConnectionState2["Closed"] = "Closed";
508
- ConnectionState2["Connecting"] = "Connecting";
509
- ConnectionState2["Open"] = "Open";
510
- ConnectionState2["Unavailable"] = "Unavailable";
511
- ConnectionState2["Untouched"] = "Untouched";
512
- return ConnectionState2;
513
- })(ConnectionState || {});
507
+ var ConnectionState = {
508
+ Closed: "Closed",
509
+ Connecting: "Connecting",
510
+ Open: "Open",
511
+ Unavailable: "Unavailable",
512
+ Untouched: "Untouched"
513
+ };
514
514
 
515
515
  // src/PluvRoom.ts
516
516
  var ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1e3;
@@ -532,6 +532,7 @@ var PluvRoom = class extends AbstractRoom {
532
532
  debug = false,
533
533
  initialPresence,
534
534
  initialStorage,
535
+ limits,
535
536
  metadata,
536
537
  onAuthorizationFail,
537
538
  presence,
@@ -566,7 +567,7 @@ var PluvRoom = class extends AbstractRoom {
566
567
  attempts: 0,
567
568
  count: 0,
568
569
  id: null,
569
- state: "Untouched" /* Untouched */
570
+ state: ConnectionState.Untouched
570
571
  },
571
572
  webSocket: null
572
573
  };
@@ -575,7 +576,7 @@ var PluvRoom = class extends AbstractRoom {
575
576
  this.broadcast = new Proxy(
576
577
  (event, data) => __async(this, null, function* () {
577
578
  if (!this._state.webSocket) return;
578
- if (this._state.connection.state !== "Open" /* Open */) return;
579
+ if (this._state.connection.state !== ConnectionState.Open) return;
579
580
  const type = event.toString();
580
581
  const procedure = this._router._defs.events[type];
581
582
  if (!(procedure == null ? void 0 : procedure.config.broadcast)) {
@@ -707,6 +708,7 @@ var PluvRoom = class extends AbstractRoom {
707
708
  this.metadata = metadata;
708
709
  this._debug = debug;
709
710
  this._endpoints = { authEndpoint, wsEndpoint };
711
+ this._limits = limits;
710
712
  this._reconnectTimeoutMs = reconnectTimeoutMs;
711
713
  this._storageStore = storage;
712
714
  if (!!publicKey) this._publicKey = publicKey;
@@ -716,7 +718,7 @@ var PluvRoom = class extends AbstractRoom {
716
718
  }
717
719
  };
718
720
  this._router = router != null ? router : new PluvRouter({});
719
- this._usersManager = new UsersManager({ initialPresence, presence });
721
+ this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
720
722
  this._crdtManager = new CrdtManager({ initialStorage });
721
723
  }
722
724
  get storageLoaded() {
@@ -731,13 +733,13 @@ var PluvRoom = class extends AbstractRoom {
731
733
  const params = (_a = args[0]) != null ? _a : {};
732
734
  const metadata = params.metadata;
733
735
  this._setMetadata(metadata);
734
- const canConnect = ["Closed" /* Closed */, "Untouched" /* Untouched */].some(
736
+ const canConnect = [ConnectionState.Closed, ConnectionState.Unavailable, ConnectionState.Untouched].some(
735
737
  (state) => this._state.connection.state === state
736
738
  );
737
739
  if (!canConnect) return;
738
740
  this._clearTimeout(this._timeouts.reconnect);
739
741
  this._updateState((oldState) => {
740
- oldState.connection.state = "Connecting" /* Connecting */;
742
+ oldState.connection.state = ConnectionState.Connecting;
741
743
  return oldState;
742
744
  });
743
745
  yield this._storageStore.initialize();
@@ -775,7 +777,7 @@ var PluvRoom = class extends AbstractRoom {
775
777
  disconnect() {
776
778
  return __async(this, null, function* () {
777
779
  if (!this._state.webSocket) return;
778
- const canDisconnect = ["Connecting" /* Connecting */, "Open" /* Open */].some(
780
+ const canDisconnect = [ConnectionState.Connecting, ConnectionState.Open].some(
779
781
  (state) => this._state.connection.state === state
780
782
  );
781
783
  if (!canDisconnect) return;
@@ -784,7 +786,7 @@ var PluvRoom = class extends AbstractRoom {
784
786
  this._clearTimeout(this._timeouts.reconnect);
785
787
  this._updateState((oldState) => {
786
788
  oldState.authorization.token = null;
787
- oldState.connection.state = "Closed" /* Closed */;
789
+ oldState.connection.state = ConnectionState.Closed;
788
790
  oldState.webSocket = null;
789
791
  return oldState;
790
792
  });
@@ -823,7 +825,7 @@ var PluvRoom = class extends AbstractRoom {
823
825
  _attachWindowListeners() {
824
826
  if (typeof window === "undefined") return;
825
827
  if (typeof document === "undefined") return;
826
- this._windowListeners = {
828
+ this._windowListeners = this._windowListeners || {
827
829
  onNavigatorOffline: this._onNavigatorOffline.bind(this),
828
830
  onNavigatorOnline: this._onNavigatorOnline.bind(this),
829
831
  onVisibilityChange: this._onVisibilityChange.bind(this)
@@ -834,7 +836,7 @@ var PluvRoom = class extends AbstractRoom {
834
836
  }
835
837
  _attachWsListeners() {
836
838
  if (!this._state.webSocket) return;
837
- this._wsListeners = {
839
+ this._wsListeners = this._wsListeners || {
838
840
  onClose: this._onClose.bind(this),
839
841
  onError: this._onError.bind(this),
840
842
  onMessage: this._onMessage.bind(this),
@@ -1154,7 +1156,7 @@ var PluvRoom = class extends AbstractRoom {
1154
1156
  const authError = error instanceof Error ? error : new Error("Authorization failed");
1155
1157
  this._listeners.onAuthorizationFail(authError);
1156
1158
  this._updateState((oldState) => {
1157
- oldState.connection.state = "Unavailable" /* Unavailable */;
1159
+ oldState.connection.state = ConnectionState.Unavailable;
1158
1160
  return oldState;
1159
1161
  });
1160
1162
  }
@@ -1178,7 +1180,15 @@ var PluvRoom = class extends AbstractRoom {
1178
1180
  ].some((okToRetryCode) => event.code === okToRetryCode);
1179
1181
  this._updateState((oldState) => {
1180
1182
  oldState.authorization.token = null;
1181
- oldState.connection.state = shouldRetry ? "Unavailable" /* Unavailable */ : "Closed" /* Closed */;
1183
+ oldState.connection.state = shouldRetry ? ConnectionState.Unavailable : (
1184
+ /**
1185
+ * !HACK
1186
+ * @description Assume these are not recoverable. We're just going to mark this
1187
+ * as Closed to avoid reconnect attempts.
1188
+ * @date April 28, 2025
1189
+ */
1190
+ ConnectionState.Closed
1191
+ );
1182
1192
  oldState.webSocket = null;
1183
1193
  return oldState;
1184
1194
  });
@@ -1246,10 +1256,10 @@ var PluvRoom = class extends AbstractRoom {
1246
1256
  }
1247
1257
  }
1248
1258
  _onOpen() {
1249
- if (this._state.connection.state !== "Connecting" /* Connecting */) return;
1259
+ if (this._state.connection.state !== ConnectionState.Connecting) return;
1250
1260
  this._logDebug("WebSocket connected");
1251
1261
  this._updateState((oldState) => {
1252
- oldState.connection.state = "Open" /* Open */;
1262
+ oldState.connection.state = ConnectionState.Open;
1253
1263
  return oldState;
1254
1264
  });
1255
1265
  this._clearInterval(this._intervals.heartbeat);
@@ -1257,16 +1267,16 @@ var PluvRoom = class extends AbstractRoom {
1257
1267
  }
1258
1268
  _onNavigatorOffline() {
1259
1269
  return __async(this, null, function* () {
1260
- if (this._state.connection.state === "Closed" /* Closed */) return;
1270
+ if (this._state.connection.state === ConnectionState.Closed) return;
1261
1271
  this._updateState((oldState) => {
1262
- oldState.connection.state = "Unavailable" /* Unavailable */;
1272
+ oldState.connection.state = ConnectionState.Unavailable;
1263
1273
  return oldState;
1264
1274
  });
1265
1275
  });
1266
1276
  }
1267
1277
  _onNavigatorOnline() {
1268
1278
  return __async(this, null, function* () {
1269
- if (this._state.connection.state !== "Unavailable" /* Unavailable */) return;
1279
+ if (this._state.connection.state !== ConnectionState.Unavailable) return;
1270
1280
  yield this._reconnect();
1271
1281
  });
1272
1282
  }
@@ -1275,12 +1285,12 @@ var PluvRoom = class extends AbstractRoom {
1275
1285
  if (typeof document === "undefined") return;
1276
1286
  if (document.visibilityState !== "visible") return;
1277
1287
  switch (this._state.connection.state) {
1278
- case "Open" /* Open */: {
1288
+ case ConnectionState.Open: {
1279
1289
  this._clearInterval(this._intervals.heartbeat);
1280
1290
  setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1281
1291
  return;
1282
1292
  }
1283
- case "Unavailable" /* Unavailable */: {
1293
+ case ConnectionState.Unavailable: {
1284
1294
  yield this._reconnect();
1285
1295
  return;
1286
1296
  }
@@ -1308,8 +1318,8 @@ var PluvRoom = class extends AbstractRoom {
1308
1318
  }
1309
1319
  _reconnect() {
1310
1320
  return __async(this, null, function* () {
1311
- if (!this._state.webSocket) return;
1312
1321
  this._closeWs();
1322
+ if (this._state.connection.state === ConnectionState.Closed) return;
1313
1323
  const metadata = this._lastMetadata;
1314
1324
  const params = typeof metadata === "undefined" ? [] : [{ metadata }];
1315
1325
  yield this.connect(...params);
@@ -1371,6 +1381,7 @@ var PluvClient = class {
1371
1381
  debug: options.debug,
1372
1382
  initialPresence: options.initialPresence,
1373
1383
  initialStorage: this._initialStorage,
1384
+ limits: this._limits,
1374
1385
  metadata: this.metadata,
1375
1386
  onAuthorizationFail: options.onAuthorizationFail,
1376
1387
  presence: this._presence,
@@ -1405,11 +1416,23 @@ var PluvClient = class {
1405
1416
  this._rooms.delete(toLeave.id);
1406
1417
  this._logDebug(`Left and deleted room: ${room}`);
1407
1418
  });
1408
- const { authEndpoint, debug = false, initialStorage, metadata, presence, publicKey, wsEndpoint } = options;
1419
+ const {
1420
+ authEndpoint,
1421
+ debug = false,
1422
+ initialStorage,
1423
+ limits,
1424
+ metadata,
1425
+ presence,
1426
+ publicKey,
1427
+ wsEndpoint
1428
+ } = options;
1409
1429
  this.metadata = metadata;
1410
1430
  this._authEndpoint = authEndpoint;
1411
1431
  this._debug = debug;
1412
1432
  this._initialStorage = initialStorage;
1433
+ this._limits = __spreadValues({
1434
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1435
+ }, limits);
1413
1436
  this._presence = presence;
1414
1437
  this._wsEndpoint = wsEndpoint;
1415
1438
  if (!!publicKey) this._publicKey = publicKey;
@@ -1447,7 +1470,7 @@ var infer = (callback) => callback;
1447
1470
  // src/MockedRoom.ts
1448
1471
  var MockedRoom = class extends AbstractRoom {
1449
1472
  constructor(room, options) {
1450
- const { events, initialPresence, initialStorage, presence, router } = options;
1473
+ const { events, initialPresence, initialStorage, limits, presence, router } = options;
1451
1474
  super(room);
1452
1475
  this._crdtNotifier = new CrdtNotifier();
1453
1476
  this._eventNotifier = new EventNotifier();
@@ -1461,7 +1484,7 @@ var MockedRoom = class extends AbstractRoom {
1461
1484
  attempts: 0,
1462
1485
  count: 1,
1463
1486
  id: null,
1464
- state: "Untouched" /* Untouched */
1487
+ state: ConnectionState.Untouched
1465
1488
  },
1466
1489
  webSocket: null
1467
1490
  };
@@ -1472,7 +1495,7 @@ var MockedRoom = class extends AbstractRoom {
1472
1495
  this.broadcast = new Proxy(
1473
1496
  (event, data) => __async(this, null, function* () {
1474
1497
  if (!this._state.webSocket) return;
1475
- if (this._state.connection.state !== "Open" /* Open */) return;
1498
+ if (this._state.connection.state !== ConnectionState.Open) return;
1476
1499
  const type = event.toString();
1477
1500
  const procedure = this._router._defs.events[type];
1478
1501
  if (!(procedure == null ? void 0 : procedure.config.broadcast)) {
@@ -1578,11 +1601,11 @@ var MockedRoom = class extends AbstractRoom {
1578
1601
  if (!!myself) this._stateNotifier.subjects["myself"].next(myself);
1579
1602
  };
1580
1603
  this._events = events;
1604
+ this._limits = __spreadValues({
1605
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1606
+ }, limits);
1581
1607
  this._router = router != null ? router : new PluvRouter({});
1582
- this._usersManager = new UsersManager({
1583
- initialPresence,
1584
- presence
1585
- });
1608
+ this._usersManager = new UsersManager({ initialPresence, limits: this._limits, presence });
1586
1609
  this._crdtManager = new CrdtManager({
1587
1610
  initialStorage
1588
1611
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/client",
3
- "version": "0.42.0",
3
+ "version": "0.44.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.42.0",
22
- "@pluv/types": "^0.42.0"
21
+ "@pluv/crdt": "^0.44.0",
22
+ "@pluv/types": "^0.44.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "eslint": "^9.25.1",
26
26
  "tsup": "^8.4.0",
27
27
  "typescript": "^5.8.3",
28
- "@pluv/tsconfig": "^0.42.0",
29
- "eslint-config-pluv": "^0.42.0"
28
+ "@pluv/tsconfig": "^0.44.0",
29
+ "eslint-config-pluv": "^0.44.0"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup src/index.ts --format esm,cjs --dts",
@@ -93,7 +93,7 @@ export class CrdtManager<TStorage extends Record<string, CrdtType<any, any>>> {
93
93
  origin?: string,
94
94
  ): AbstractCrdtDoc<TStorage> {
95
95
  doc.transact(() => {
96
- doc.batchApplyEncodedState(updates.map((update) => ({ origin, update })));
96
+ doc.batchApplyEncodedState({ origin, updates });
97
97
  }, origin);
98
98
 
99
99
  return doc;
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
 
@@ -358,7 +363,7 @@ export class PluvRoom<
358
363
 
359
364
  this._setMetadata(metadata);
360
365
 
361
- const canConnect = [ConnectionState.Closed, ConnectionState.Untouched].some(
366
+ const canConnect = [ConnectionState.Closed, ConnectionState.Unavailable, ConnectionState.Untouched].some(
362
367
  (state) => this._state.connection.state === state,
363
368
  );
364
369
 
@@ -610,7 +615,7 @@ export class PluvRoom<
610
615
  if (typeof window === "undefined") return;
611
616
  if (typeof document === "undefined") return;
612
617
 
613
- this._windowListeners = {
618
+ this._windowListeners = this._windowListeners || {
614
619
  onNavigatorOffline: this._onNavigatorOffline.bind(this),
615
620
  onNavigatorOnline: this._onNavigatorOnline.bind(this),
616
621
  onVisibilityChange: this._onVisibilityChange.bind(this),
@@ -624,7 +629,7 @@ export class PluvRoom<
624
629
  private _attachWsListeners(): void {
625
630
  if (!this._state.webSocket) return;
626
631
 
627
- this._wsListeners = {
632
+ this._wsListeners = this._wsListeners || {
628
633
  onClose: this._onClose.bind(this),
629
634
  onError: this._onError.bind(this),
630
635
  onMessage: this._onMessage.bind(this),
@@ -1147,7 +1152,15 @@ export class PluvRoom<
1147
1152
 
1148
1153
  this._updateState((oldState) => {
1149
1154
  oldState.authorization.token = null;
1150
- oldState.connection.state = shouldRetry ? ConnectionState.Unavailable : ConnectionState.Closed;
1155
+ oldState.connection.state = shouldRetry
1156
+ ? ConnectionState.Unavailable
1157
+ : /**
1158
+ * !HACK
1159
+ * @description Assume these are not recoverable. We're just going to mark this
1160
+ * as Closed to avoid reconnect attempts.
1161
+ * @date April 28, 2025
1162
+ */
1163
+ ConnectionState.Closed;
1151
1164
  oldState.webSocket = null;
1152
1165
 
1153
1166
  return oldState;
@@ -1309,10 +1322,16 @@ export class PluvRoom<
1309
1322
  }
1310
1323
 
1311
1324
  private async _reconnect(): Promise<void> {
1312
- if (!this._state.webSocket) return;
1313
-
1314
1325
  this._closeWs();
1315
1326
 
1327
+ /**
1328
+ * @description User has manually closed this connection. We're not going to retry, since
1329
+ * the user probably intends for this to stay closed. They will need to invoke connect
1330
+ * themselves.
1331
+ * @date April 28, 2025
1332
+ */
1333
+ if (this._state.connection.state === ConnectionState.Closed) return;
1334
+
1316
1335
  const metadata = this._lastMetadata;
1317
1336
  const params = (typeof metadata === "undefined" ? [] : [{ metadata }]) as RoomConnectParams<TMetadata>;
1318
1337
 
@@ -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/enums.ts CHANGED
@@ -1,7 +1,9 @@
1
- export enum ConnectionState {
2
- Closed = "Closed",
3
- Connecting = "Connecting",
4
- Open = "Open",
5
- Unavailable = "Unavailable",
6
- Untouched = "Untouched",
7
- }
1
+ export const ConnectionState = {
2
+ Closed: "Closed",
3
+ Connecting: "Connecting",
4
+ Open: "Open",
5
+ Unavailable: "Unavailable",
6
+ Untouched: "Untouched",
7
+ } as const;
8
+
9
+ export type ConnectionState = (typeof ConnectionState)[keyof typeof ConnectionState];
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
  }