@pluv/client 0.41.1 → 0.41.3

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.1 build /home/runner/work/pluv/pluv/packages/client
2
+ > @pluv/client@0.41.3 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
- CJS dist/index.js 55.51 KB
12
- CJS ⚡️ Build success in 131ms
13
- ESM dist/index.mjs 54.19 KB
14
- ESM ⚡️ Build success in 132ms
11
+ CJS dist/index.js 56.67 KB
12
+ CJS ⚡️ Build success in 110ms
13
+ ESM dist/index.mjs 55.36 KB
14
+ ESM ⚡️ Build success in 111ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 3291ms
16
+ DTS ⚡️ Build success in 3194ms
17
17
  DTS dist/index.d.mts 24.58 KB
18
18
  DTS dist/index.d.ts 24.58 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @pluv/client
2
2
 
3
+ ## 0.41.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 49051c0: Fix presence keys becoming lost, causing presence objects to be partial and failing schema validation.
8
+ - @pluv/crdt@0.41.3
9
+ - @pluv/types@0.41.3
10
+
11
+ ## 0.41.2
12
+
13
+ ### Patch Changes
14
+
15
+ - 37956d3: When presence is received for a user that is not currently being tracked, `PluvRoom` now automatically updates `others` to include the connection of the received presence.
16
+ - @pluv/crdt@0.41.2
17
+ - @pluv/types@0.41.2
18
+
3
19
  ## 0.41.1
4
20
 
5
21
  ### Patch Changes
package/dist/index.js CHANGED
@@ -320,6 +320,37 @@ var StorageStore = class extends AbstractStorageStore {
320
320
  // src/constants.ts
321
321
  var MAX_PRESENCE_SIZE_BYTES = 512;
322
322
 
323
+ // src/utils/debounce.ts
324
+ var debounce = (fn, options) => {
325
+ const { wait, immediate = false } = options != null ? options : {};
326
+ let prevResult;
327
+ let timeout = null;
328
+ return (...args) => {
329
+ if (typeof timeout === "number") clearTimeout(timeout);
330
+ timeout = setTimeout(() => {
331
+ timeout = null;
332
+ if (!immediate) prevResult = fn(args);
333
+ }, wait);
334
+ if (immediate && !timeout) {
335
+ fn(args);
336
+ return;
337
+ }
338
+ return prevResult;
339
+ };
340
+ };
341
+
342
+ // src/utils/pickBy.ts
343
+ var pickBy = (obj, by) => {
344
+ const result = {};
345
+ const keys = Object.keys(obj);
346
+ for (let i = 0; i < keys.length; ++i) {
347
+ const key = keys[i];
348
+ const value = obj[key];
349
+ if (by(value)) result[key] = value;
350
+ }
351
+ return result;
352
+ };
353
+
323
354
  // src/UsersManager.ts
324
355
  var UsersManager = class {
325
356
  constructor(config) {
@@ -334,7 +365,7 @@ var UsersManager = class {
334
365
  this._others = /* @__PURE__ */ new Map();
335
366
  this._presence = null;
336
367
  const { initialPresence, presence = null } = config;
337
- this._initialPresence = initialPresence;
368
+ this.initialPresence = initialPresence;
338
369
  this._myPresence = initialPresence;
339
370
  this._presence = presence;
340
371
  }
@@ -346,9 +377,9 @@ var UsersManager = class {
346
377
  }
347
378
  addConnection(userInfo) {
348
379
  var _a;
349
- const data = __spreadProps(__spreadValues({}, userInfo), {
350
- presence: (_a = userInfo.presence) != null ? _a : this._initialPresence
351
- });
380
+ const cleaned = pickBy((_a = userInfo.presence) != null ? _a : {}, (value) => typeof value !== "undefined");
381
+ const presence = __spreadValues(__spreadValues({}, this.initialPresence), cleaned);
382
+ const data = __spreadProps(__spreadValues({}, userInfo), { presence });
352
383
  const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
353
384
  const clientId = this.getClientId(data);
354
385
  const remaining = this._setConnectionId(userInfo.connectionId, clientId).size;
@@ -414,13 +445,16 @@ var UsersManager = class {
414
445
  return Array.from(this._others.values());
415
446
  }
416
447
  patchPresence(connectionId, patch) {
448
+ var _a;
417
449
  const clientId = this.getClientId(connectionId);
418
450
  const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
419
451
  if (!clientId) return null;
420
452
  if (myClientId === clientId) return this.updateMyPresence(patch);
421
453
  const other = this._others.get(clientId);
422
454
  if (!other) return null;
423
- const presence = __spreadValues(__spreadValues({}, other.presence), patch);
455
+ const cleanedPatch = pickBy(patch != null ? patch : {}, (value) => typeof value !== "undefined");
456
+ const cleanedPresence = pickBy((_a = other.presence) != null ? _a : {}, (value) => typeof value !== "undefined");
457
+ const presence = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
424
458
  const validated = this._presence ? this._presence.parse(presence) : presence;
425
459
  this._others.set(clientId, __spreadProps(__spreadValues({}, other), { presence: validated }));
426
460
  return validated;
@@ -434,7 +468,7 @@ var UsersManager = class {
434
468
  setMyself(userInfo) {
435
469
  var _a;
436
470
  const connectionId = userInfo.connectionId;
437
- const presence = (_a = userInfo.presence) != null ? _a : this._initialPresence;
471
+ const presence = (_a = userInfo.presence) != null ? _a : this.initialPresence;
438
472
  this._myself = __spreadProps(__spreadValues({}, userInfo), { presence });
439
473
  this._myPresence = presence;
440
474
  const clientId = this.getClientId(this._myself);
@@ -455,10 +489,14 @@ var UsersManager = class {
455
489
  this._others.set(clientId, __spreadProps(__spreadValues({}, other), { presence }));
456
490
  }
457
491
  /**
458
- * @description This method need not care about being connected.
492
+ * @description This method need not care about being connected. This is because a user should
493
+ * be able to view and update their own presence without being online
494
+ * @date April 20, 2025
459
495
  */
460
496
  updateMyPresence(patch) {
461
- const updated = __spreadValues(__spreadValues({}, this._myPresence), patch);
497
+ const cleanedPatch = pickBy(patch, (value) => typeof value !== "undefined");
498
+ const cleanedPresence = pickBy(this._myPresence, (value) => typeof value !== "undefined");
499
+ const updated = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
462
500
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
463
501
  if (bytes > MAX_PRESENCE_SIZE_BYTES) {
464
502
  throw new Error(
@@ -505,25 +543,6 @@ var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
505
543
  return ConnectionState2;
506
544
  })(ConnectionState || {});
507
545
 
508
- // src/utils/debounce.ts
509
- var debounce = (fn, options) => {
510
- const { wait, immediate = false } = options != null ? options : {};
511
- let prevResult;
512
- let timeout = null;
513
- return (...args) => {
514
- if (typeof timeout === "number") clearTimeout(timeout);
515
- timeout = setTimeout(() => {
516
- timeout = null;
517
- if (!immediate) prevResult = fn(args);
518
- }, wait);
519
- if (immediate && !timeout) {
520
- fn(args);
521
- return;
522
- }
523
- return prevResult;
524
- };
525
- };
526
-
527
546
  // src/PluvRoom.ts
528
547
  var ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1e3;
529
548
  var HEARTBEAT_INTERVAL_MS = 1e4;
@@ -983,9 +1002,19 @@ var PluvRoom = class extends AbstractRoom {
983
1002
  this._stateNotifier.subjects.myself.next(this._usersManager.myself);
984
1003
  return;
985
1004
  }
986
- const other = this._usersManager.getOther(connectionId);
1005
+ if (!!clientId) {
1006
+ const other = this._usersManager.getOther(connectionId);
1007
+ this._otherNotifier.subject(clientId).next(other);
1008
+ } else {
1009
+ const added = this._usersManager.addConnection({
1010
+ connectionId,
1011
+ presence: data.presence,
1012
+ user: message.user
1013
+ });
1014
+ const other = this._usersManager.getOther(connectionId);
1015
+ this._otherNotifier.subject(added.clientId).next(other);
1016
+ }
987
1017
  const others = this._usersManager.getOthers();
988
- if (!!clientId) this._otherNotifier.subject(clientId).next(other);
989
1018
  this._stateNotifier.subjects.others.next(others);
990
1019
  }
991
1020
  _handleReceiveOthers(message) {
@@ -996,7 +1025,7 @@ var PluvRoom = class extends AbstractRoom {
996
1025
  const { presence, user } = data.others[connectionId];
997
1026
  const result = this._usersManager.addConnection({
998
1027
  connectionId,
999
- presence: presence != null ? presence : void 0,
1028
+ presence,
1000
1029
  user
1001
1030
  });
1002
1031
  if (!!presence) this._usersManager.patchPresence(connectionId, presence);
@@ -1086,7 +1115,6 @@ var PluvRoom = class extends AbstractRoom {
1086
1115
  this._crdtNotifier.rootSubject.next(storageRoot);
1087
1116
  }
1088
1117
  _handleUserJoinedMessage(message) {
1089
- var _a;
1090
1118
  const { connectionId } = message;
1091
1119
  if (!connectionId) return;
1092
1120
  if (!this._state.webSocket) throw new Error("Could not find WebSocket");
@@ -1097,15 +1125,14 @@ var PluvRoom = class extends AbstractRoom {
1097
1125
  this._sendMessage({ type: "$getOthers", data: {} });
1098
1126
  return;
1099
1127
  }
1100
- const result = this._usersManager.addConnection({
1128
+ const added = this._usersManager.addConnection({
1101
1129
  connectionId,
1102
1130
  presence: data.presence,
1103
1131
  user: data.user
1104
1132
  });
1105
- const clientId = (_a = result == null ? void 0 : result.clientId) != null ? _a : null;
1106
1133
  const other = this._usersManager.getOther(connectionId);
1107
1134
  const others = this._usersManager.getOthers();
1108
- if (!!clientId) this._otherNotifier.subject(clientId).next(other);
1135
+ this._otherNotifier.subject(added.clientId).next(other);
1109
1136
  this._stateNotifier.subjects.others.next(others);
1110
1137
  }
1111
1138
  _heartbeat() {
@@ -1261,13 +1288,15 @@ var PluvRoom = class extends AbstractRoom {
1261
1288
  if (typeof document === "undefined") return;
1262
1289
  if (document.visibilityState !== "visible") return;
1263
1290
  switch (this._state.connection.state) {
1264
- case "Open" /* Open */:
1291
+ case "Open" /* Open */: {
1265
1292
  this._clearInterval(this._intervals.heartbeat);
1266
1293
  setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1267
1294
  return;
1268
- case "Unavailable" /* Unavailable */:
1295
+ }
1296
+ case "Unavailable" /* Unavailable */: {
1269
1297
  yield this._reconnect();
1270
1298
  return;
1299
+ }
1271
1300
  default:
1272
1301
  }
1273
1302
  });
package/dist/index.mjs CHANGED
@@ -289,6 +289,37 @@ var StorageStore = class extends AbstractStorageStore {
289
289
  // src/constants.ts
290
290
  var MAX_PRESENCE_SIZE_BYTES = 512;
291
291
 
292
+ // src/utils/debounce.ts
293
+ var debounce = (fn, options) => {
294
+ const { wait, immediate = false } = options != null ? options : {};
295
+ let prevResult;
296
+ let timeout = null;
297
+ return (...args) => {
298
+ if (typeof timeout === "number") clearTimeout(timeout);
299
+ timeout = setTimeout(() => {
300
+ timeout = null;
301
+ if (!immediate) prevResult = fn(args);
302
+ }, wait);
303
+ if (immediate && !timeout) {
304
+ fn(args);
305
+ return;
306
+ }
307
+ return prevResult;
308
+ };
309
+ };
310
+
311
+ // src/utils/pickBy.ts
312
+ var pickBy = (obj, by) => {
313
+ const result = {};
314
+ const keys = Object.keys(obj);
315
+ for (let i = 0; i < keys.length; ++i) {
316
+ const key = keys[i];
317
+ const value = obj[key];
318
+ if (by(value)) result[key] = value;
319
+ }
320
+ return result;
321
+ };
322
+
292
323
  // src/UsersManager.ts
293
324
  var UsersManager = class {
294
325
  constructor(config) {
@@ -303,7 +334,7 @@ var UsersManager = class {
303
334
  this._others = /* @__PURE__ */ new Map();
304
335
  this._presence = null;
305
336
  const { initialPresence, presence = null } = config;
306
- this._initialPresence = initialPresence;
337
+ this.initialPresence = initialPresence;
307
338
  this._myPresence = initialPresence;
308
339
  this._presence = presence;
309
340
  }
@@ -315,9 +346,9 @@ var UsersManager = class {
315
346
  }
316
347
  addConnection(userInfo) {
317
348
  var _a;
318
- const data = __spreadProps(__spreadValues({}, userInfo), {
319
- presence: (_a = userInfo.presence) != null ? _a : this._initialPresence
320
- });
349
+ const cleaned = pickBy((_a = userInfo.presence) != null ? _a : {}, (value) => typeof value !== "undefined");
350
+ const presence = __spreadValues(__spreadValues({}, this.initialPresence), cleaned);
351
+ const data = __spreadProps(__spreadValues({}, userInfo), { presence });
321
352
  const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
322
353
  const clientId = this.getClientId(data);
323
354
  const remaining = this._setConnectionId(userInfo.connectionId, clientId).size;
@@ -383,13 +414,16 @@ var UsersManager = class {
383
414
  return Array.from(this._others.values());
384
415
  }
385
416
  patchPresence(connectionId, patch) {
417
+ var _a;
386
418
  const clientId = this.getClientId(connectionId);
387
419
  const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
388
420
  if (!clientId) return null;
389
421
  if (myClientId === clientId) return this.updateMyPresence(patch);
390
422
  const other = this._others.get(clientId);
391
423
  if (!other) return null;
392
- const presence = __spreadValues(__spreadValues({}, other.presence), patch);
424
+ const cleanedPatch = pickBy(patch != null ? patch : {}, (value) => typeof value !== "undefined");
425
+ const cleanedPresence = pickBy((_a = other.presence) != null ? _a : {}, (value) => typeof value !== "undefined");
426
+ const presence = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
393
427
  const validated = this._presence ? this._presence.parse(presence) : presence;
394
428
  this._others.set(clientId, __spreadProps(__spreadValues({}, other), { presence: validated }));
395
429
  return validated;
@@ -403,7 +437,7 @@ var UsersManager = class {
403
437
  setMyself(userInfo) {
404
438
  var _a;
405
439
  const connectionId = userInfo.connectionId;
406
- const presence = (_a = userInfo.presence) != null ? _a : this._initialPresence;
440
+ const presence = (_a = userInfo.presence) != null ? _a : this.initialPresence;
407
441
  this._myself = __spreadProps(__spreadValues({}, userInfo), { presence });
408
442
  this._myPresence = presence;
409
443
  const clientId = this.getClientId(this._myself);
@@ -424,10 +458,14 @@ var UsersManager = class {
424
458
  this._others.set(clientId, __spreadProps(__spreadValues({}, other), { presence }));
425
459
  }
426
460
  /**
427
- * @description This method need not care about being connected.
461
+ * @description This method need not care about being connected. This is because a user should
462
+ * be able to view and update their own presence without being online
463
+ * @date April 20, 2025
428
464
  */
429
465
  updateMyPresence(patch) {
430
- const updated = __spreadValues(__spreadValues({}, this._myPresence), patch);
466
+ const cleanedPatch = pickBy(patch, (value) => typeof value !== "undefined");
467
+ const cleanedPresence = pickBy(this._myPresence, (value) => typeof value !== "undefined");
468
+ const updated = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
431
469
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
432
470
  if (bytes > MAX_PRESENCE_SIZE_BYTES) {
433
471
  throw new Error(
@@ -474,25 +512,6 @@ var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
474
512
  return ConnectionState2;
475
513
  })(ConnectionState || {});
476
514
 
477
- // src/utils/debounce.ts
478
- var debounce = (fn, options) => {
479
- const { wait, immediate = false } = options != null ? options : {};
480
- let prevResult;
481
- let timeout = null;
482
- return (...args) => {
483
- if (typeof timeout === "number") clearTimeout(timeout);
484
- timeout = setTimeout(() => {
485
- timeout = null;
486
- if (!immediate) prevResult = fn(args);
487
- }, wait);
488
- if (immediate && !timeout) {
489
- fn(args);
490
- return;
491
- }
492
- return prevResult;
493
- };
494
- };
495
-
496
515
  // src/PluvRoom.ts
497
516
  var ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1e3;
498
517
  var HEARTBEAT_INTERVAL_MS = 1e4;
@@ -952,9 +971,19 @@ var PluvRoom = class extends AbstractRoom {
952
971
  this._stateNotifier.subjects.myself.next(this._usersManager.myself);
953
972
  return;
954
973
  }
955
- const other = this._usersManager.getOther(connectionId);
974
+ if (!!clientId) {
975
+ const other = this._usersManager.getOther(connectionId);
976
+ this._otherNotifier.subject(clientId).next(other);
977
+ } else {
978
+ const added = this._usersManager.addConnection({
979
+ connectionId,
980
+ presence: data.presence,
981
+ user: message.user
982
+ });
983
+ const other = this._usersManager.getOther(connectionId);
984
+ this._otherNotifier.subject(added.clientId).next(other);
985
+ }
956
986
  const others = this._usersManager.getOthers();
957
- if (!!clientId) this._otherNotifier.subject(clientId).next(other);
958
987
  this._stateNotifier.subjects.others.next(others);
959
988
  }
960
989
  _handleReceiveOthers(message) {
@@ -965,7 +994,7 @@ var PluvRoom = class extends AbstractRoom {
965
994
  const { presence, user } = data.others[connectionId];
966
995
  const result = this._usersManager.addConnection({
967
996
  connectionId,
968
- presence: presence != null ? presence : void 0,
997
+ presence,
969
998
  user
970
999
  });
971
1000
  if (!!presence) this._usersManager.patchPresence(connectionId, presence);
@@ -1055,7 +1084,6 @@ var PluvRoom = class extends AbstractRoom {
1055
1084
  this._crdtNotifier.rootSubject.next(storageRoot);
1056
1085
  }
1057
1086
  _handleUserJoinedMessage(message) {
1058
- var _a;
1059
1087
  const { connectionId } = message;
1060
1088
  if (!connectionId) return;
1061
1089
  if (!this._state.webSocket) throw new Error("Could not find WebSocket");
@@ -1066,15 +1094,14 @@ var PluvRoom = class extends AbstractRoom {
1066
1094
  this._sendMessage({ type: "$getOthers", data: {} });
1067
1095
  return;
1068
1096
  }
1069
- const result = this._usersManager.addConnection({
1097
+ const added = this._usersManager.addConnection({
1070
1098
  connectionId,
1071
1099
  presence: data.presence,
1072
1100
  user: data.user
1073
1101
  });
1074
- const clientId = (_a = result == null ? void 0 : result.clientId) != null ? _a : null;
1075
1102
  const other = this._usersManager.getOther(connectionId);
1076
1103
  const others = this._usersManager.getOthers();
1077
- if (!!clientId) this._otherNotifier.subject(clientId).next(other);
1104
+ this._otherNotifier.subject(added.clientId).next(other);
1078
1105
  this._stateNotifier.subjects.others.next(others);
1079
1106
  }
1080
1107
  _heartbeat() {
@@ -1230,13 +1257,15 @@ var PluvRoom = class extends AbstractRoom {
1230
1257
  if (typeof document === "undefined") return;
1231
1258
  if (document.visibilityState !== "visible") return;
1232
1259
  switch (this._state.connection.state) {
1233
- case "Open" /* Open */:
1260
+ case "Open" /* Open */: {
1234
1261
  this._clearInterval(this._intervals.heartbeat);
1235
1262
  setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1236
1263
  return;
1237
- case "Unavailable" /* Unavailable */:
1264
+ }
1265
+ case "Unavailable" /* Unavailable */: {
1238
1266
  yield this._reconnect();
1239
1267
  return;
1268
+ }
1240
1269
  default:
1241
1270
  }
1242
1271
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/client",
3
- "version": "0.41.1",
3
+ "version": "0.41.3",
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.1",
22
- "@pluv/types": "^0.41.1"
21
+ "@pluv/crdt": "^0.41.3",
22
+ "@pluv/types": "^0.41.3"
23
23
  },
24
24
  "devDependencies": {
25
25
  "eslint": "^9.25.0",
26
26
  "tsup": "^8.4.0",
27
27
  "typescript": "^5.8.3",
28
- "@pluv/tsconfig": "^0.41.1",
29
- "eslint-config-pluv": "^0.41.1"
28
+ "@pluv/tsconfig": "^0.41.3",
29
+ "eslint-config-pluv": "^0.41.3"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup src/index.ts --format esm,cjs --dts",
package/src/PluvRoom.ts CHANGED
@@ -77,9 +77,6 @@ export const DEFAULT_PLUV_CLIENT_ADDON = <
77
77
  storage: new StorageStore(input.room.id),
78
78
  });
79
79
 
80
- interface CloseWsOptions {
81
- connectionState?: ConnectionState;
82
- }
83
80
  interface WindowListeners {
84
81
  onNavigatorOffline: () => void;
85
82
  onNavigatorOnline: () => void;
@@ -857,10 +854,28 @@ export class PluvRoom<
857
854
  return;
858
855
  }
859
856
 
860
- const other = this._usersManager.getOther(connectionId);
857
+ if (!!clientId) {
858
+ const other = this._usersManager.getOther(connectionId);
859
+
860
+ this._otherNotifier.subject(clientId).next(other);
861
+ } else {
862
+ /**
863
+ * !HACK
864
+ * @description User could not be found. Add the connection to keep others up-to-date
865
+ * @date April 19, 2025
866
+ */
867
+ const added = this._usersManager.addConnection({
868
+ connectionId,
869
+ presence: data.presence as TPresence,
870
+ user: message.user as Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>>,
871
+ });
872
+ const other = this._usersManager.getOther(connectionId);
873
+
874
+ this._otherNotifier.subject(added.clientId).next(other);
875
+ }
876
+
861
877
  const others = this._usersManager.getOthers();
862
878
 
863
- if (!!clientId) this._otherNotifier.subject(clientId).next(other);
864
879
  this._stateNotifier.subjects.others.next(others);
865
880
  }
866
881
 
@@ -876,7 +891,7 @@ export class PluvRoom<
876
891
 
877
892
  const result = this._usersManager.addConnection({
878
893
  connectionId,
879
- presence: (presence ?? undefined) as TPresence,
894
+ presence: presence as TPresence,
880
895
  user,
881
896
  });
882
897
 
@@ -1020,17 +1035,16 @@ export class PluvRoom<
1020
1035
  return;
1021
1036
  }
1022
1037
 
1023
- const result = this._usersManager.addConnection({
1038
+ const added = this._usersManager.addConnection({
1024
1039
  connectionId,
1025
1040
  presence: data.presence as TPresence,
1026
1041
  user: data.user,
1027
1042
  });
1028
1043
 
1029
- const clientId = result?.clientId ?? null;
1030
1044
  const other = this._usersManager.getOther(connectionId);
1031
1045
  const others = this._usersManager.getOthers();
1032
1046
 
1033
- if (!!clientId) this._otherNotifier.subject(clientId).next(other);
1047
+ this._otherNotifier.subject(added.clientId).next(other);
1034
1048
  this._stateNotifier.subjects.others.next(others);
1035
1049
  }
1036
1050
 
@@ -1222,15 +1236,17 @@ export class PluvRoom<
1222
1236
  if (document.visibilityState !== "visible") return;
1223
1237
 
1224
1238
  switch (this._state.connection.state) {
1225
- case ConnectionState.Open:
1239
+ case ConnectionState.Open: {
1226
1240
  this._clearInterval(this._intervals.heartbeat);
1227
1241
  setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1228
1242
 
1229
1243
  return;
1230
- case ConnectionState.Unavailable:
1244
+ }
1245
+ case ConnectionState.Unavailable: {
1231
1246
  await this._reconnect();
1232
1247
 
1233
1248
  return;
1249
+ }
1234
1250
  default:
1235
1251
  }
1236
1252
  }
@@ -1,6 +1,7 @@
1
1
  import type { InputZodLike, IOLike, JsonObject, OptionalProps } from "@pluv/types";
2
2
  import { MAX_PRESENCE_SIZE_BYTES } from "./constants";
3
3
  import type { UserInfo } from "./types";
4
+ import { pickBy } from "./utils";
4
5
 
5
6
  export type Presence = Record<string, unknown>;
6
7
 
@@ -28,7 +29,7 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
28
29
  fromClientId: new Map<[clientId: string][0], Set<[connectionId: string][0]>>(),
29
30
  };
30
31
 
31
- private _initialPresence: TPresence;
32
+ public readonly initialPresence: TPresence;
32
33
  /**
33
34
  * @description This presence can be updated while the user is not
34
35
  * connected.
@@ -44,7 +45,8 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
44
45
  constructor(config: UsersManagerConfig<TPresence>) {
45
46
  const { initialPresence, presence = null } = config;
46
47
 
47
- this._initialPresence = initialPresence as TPresence;
48
+ this.initialPresence = initialPresence as TPresence;
49
+
48
50
  this._myPresence = initialPresence as TPresence;
49
51
  this._presence = presence;
50
52
  }
@@ -60,10 +62,14 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
60
62
  public addConnection(
61
63
  userInfo: OptionalProps<UserInfo<TIO, TPresence>, "presence">,
62
64
  ): AddConnectionResult<TIO, TPresence> {
63
- const data: UserInfo<TIO, TPresence> = {
64
- ...userInfo,
65
- presence: userInfo.presence ?? this._initialPresence,
66
- };
65
+ /**
66
+ * @description Ensure that the presence is complete, so that it does not fail schema
67
+ * validation
68
+ * @date April 20, 2025
69
+ */
70
+ const cleaned = pickBy(userInfo.presence ?? {}, (value) => typeof value !== "undefined");
71
+ const presence = { ...this.initialPresence, ...cleaned };
72
+ const data: UserInfo<TIO, TPresence> = { ...userInfo, presence };
67
73
 
68
74
  const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
69
75
  const clientId = this.getClientId(data);
@@ -186,7 +192,9 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
186
192
 
187
193
  if (!other) return null;
188
194
 
189
- const presence = { ...other.presence, ...patch } as TPresence;
195
+ const cleanedPatch = pickBy(patch ?? {}, (value) => typeof value !== "undefined");
196
+ const cleanedPresence = pickBy(other.presence ?? {}, (value) => typeof value !== "undefined");
197
+ const presence = { ...this.initialPresence, ...cleanedPresence, ...cleanedPatch } as TPresence;
190
198
  const validated = this._presence ? this._presence.parse(presence) : presence;
191
199
 
192
200
  this._others.set(clientId, { ...other, presence: validated });
@@ -205,7 +213,7 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
205
213
 
206
214
  public setMyself(userInfo: OptionalProps<UserInfo<TIO, TPresence>, "presence">): void {
207
215
  const connectionId = userInfo.connectionId;
208
- const presence = userInfo.presence ?? this._initialPresence;
216
+ const presence = userInfo.presence ?? this.initialPresence;
209
217
 
210
218
  this._myself = { ...userInfo, presence };
211
219
  this._myPresence = presence;
@@ -238,10 +246,14 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
238
246
  }
239
247
 
240
248
  /**
241
- * @description This method need not care about being connected.
249
+ * @description This method need not care about being connected. This is because a user should
250
+ * be able to view and update their own presence without being online
251
+ * @date April 20, 2025
242
252
  */
243
253
  public updateMyPresence(patch: Partial<TPresence>): TPresence {
244
- const updated = { ...this._myPresence, ...patch };
254
+ const cleanedPatch = pickBy(patch, (value) => typeof value !== "undefined");
255
+ const cleanedPresence = pickBy(this._myPresence, (value) => typeof value !== "undefined");
256
+ const updated = { ...this.initialPresence, ...cleanedPresence, ...cleanedPatch } as TPresence;
245
257
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
246
258
 
247
259
  if (bytes > MAX_PRESENCE_SIZE_BYTES) {
@@ -1,2 +1,3 @@
1
1
  export { debounce } from "./debounce";
2
2
  export { identity } from "./identity";
3
+ export { pickBy } from "./pickBy";
@@ -0,0 +1,13 @@
1
+ export const pickBy = <T extends Record<string, any>>(obj: T, by: (value: T[keyof T]) => boolean): Partial<T> => {
2
+ const result: Partial<T> = {};
3
+ const keys: (keyof T)[] = Object.keys(obj);
4
+
5
+ for (let i = 0; i < keys.length; ++i) {
6
+ const key = keys[i];
7
+ const value = obj[key];
8
+
9
+ if (by(value)) result[key] = value;
10
+ }
11
+
12
+ return result;
13
+ };