@pluv/client 0.41.2 → 0.41.4

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.2 build /home/runner/work/pluv/pluv/packages/client
2
+ > @pluv/client@0.41.4 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 54.44 KB
12
- ESM ⚡️ Build success in 116ms
13
- CJS dist/index.js 55.76 KB
14
- CJS ⚡️ Build success in 116ms
11
+ ESM dist/index.mjs 55.36 KB
12
+ ESM ⚡️ Build success in 130ms
13
+ CJS dist/index.js 56.67 KB
14
+ CJS ⚡️ Build success in 132ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 3794ms
16
+ DTS ⚡️ Build success in 3238ms
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,20 @@
1
1
  # @pluv/client
2
2
 
3
+ ## 0.41.4
4
+
5
+ ### Patch Changes
6
+
7
+ - @pluv/crdt@0.41.4
8
+ - @pluv/types@0.41.4
9
+
10
+ ## 0.41.3
11
+
12
+ ### Patch Changes
13
+
14
+ - 49051c0: Fix presence keys becoming lost, causing presence objects to be partial and failing schema validation.
15
+ - @pluv/crdt@0.41.3
16
+ - @pluv/types@0.41.3
17
+
3
18
  ## 0.41.2
4
19
 
5
20
  ### 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) {
@@ -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;
@@ -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;
@@ -989,7 +1008,7 @@ var PluvRoom = class extends AbstractRoom {
989
1008
  } else {
990
1009
  const added = this._usersManager.addConnection({
991
1010
  connectionId,
992
- presence: __spreadValues(__spreadValues({}, this._usersManager.initialPresence), data.presence),
1011
+ presence: data.presence,
993
1012
  user: message.user
994
1013
  });
995
1014
  const other = this._usersManager.getOther(connectionId);
@@ -1006,7 +1025,7 @@ var PluvRoom = class extends AbstractRoom {
1006
1025
  const { presence, user } = data.others[connectionId];
1007
1026
  const result = this._usersManager.addConnection({
1008
1027
  connectionId,
1009
- presence: presence != null ? presence : void 0,
1028
+ presence,
1010
1029
  user
1011
1030
  });
1012
1031
  if (!!presence) this._usersManager.patchPresence(connectionId, presence);
@@ -1269,13 +1288,15 @@ var PluvRoom = class extends AbstractRoom {
1269
1288
  if (typeof document === "undefined") return;
1270
1289
  if (document.visibilityState !== "visible") return;
1271
1290
  switch (this._state.connection.state) {
1272
- case "Open" /* Open */:
1291
+ case "Open" /* Open */: {
1273
1292
  this._clearInterval(this._intervals.heartbeat);
1274
1293
  setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1275
1294
  return;
1276
- case "Unavailable" /* Unavailable */:
1295
+ }
1296
+ case "Unavailable" /* Unavailable */: {
1277
1297
  yield this._reconnect();
1278
1298
  return;
1299
+ }
1279
1300
  default:
1280
1301
  }
1281
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) {
@@ -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;
@@ -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;
@@ -958,7 +977,7 @@ var PluvRoom = class extends AbstractRoom {
958
977
  } else {
959
978
  const added = this._usersManager.addConnection({
960
979
  connectionId,
961
- presence: __spreadValues(__spreadValues({}, this._usersManager.initialPresence), data.presence),
980
+ presence: data.presence,
962
981
  user: message.user
963
982
  });
964
983
  const other = this._usersManager.getOther(connectionId);
@@ -975,7 +994,7 @@ var PluvRoom = class extends AbstractRoom {
975
994
  const { presence, user } = data.others[connectionId];
976
995
  const result = this._usersManager.addConnection({
977
996
  connectionId,
978
- presence: presence != null ? presence : void 0,
997
+ presence,
979
998
  user
980
999
  });
981
1000
  if (!!presence) this._usersManager.patchPresence(connectionId, presence);
@@ -1238,13 +1257,15 @@ var PluvRoom = class extends AbstractRoom {
1238
1257
  if (typeof document === "undefined") return;
1239
1258
  if (document.visibilityState !== "visible") return;
1240
1259
  switch (this._state.connection.state) {
1241
- case "Open" /* Open */:
1260
+ case "Open" /* Open */: {
1242
1261
  this._clearInterval(this._intervals.heartbeat);
1243
1262
  setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1244
1263
  return;
1245
- case "Unavailable" /* Unavailable */:
1264
+ }
1265
+ case "Unavailable" /* Unavailable */: {
1246
1266
  yield this._reconnect();
1247
1267
  return;
1268
+ }
1248
1269
  default:
1249
1270
  }
1250
1271
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/client",
3
- "version": "0.41.2",
3
+ "version": "0.41.4",
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/types": "^0.41.2",
22
- "@pluv/crdt": "^0.41.2"
21
+ "@pluv/crdt": "^0.41.4",
22
+ "@pluv/types": "^0.41.4"
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.2",
29
- "eslint-config-pluv": "^0.41.2"
28
+ "@pluv/tsconfig": "^0.41.4",
29
+ "eslint-config-pluv": "^0.41.4"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup src/index.ts --format esm,cjs --dts",
package/src/PluvRoom.ts CHANGED
@@ -866,7 +866,7 @@ export class PluvRoom<
866
866
  */
867
867
  const added = this._usersManager.addConnection({
868
868
  connectionId,
869
- presence: { ...this._usersManager.initialPresence, ...data.presence } as TPresence,
869
+ presence: data.presence as TPresence,
870
870
  user: message.user as Id<InferIOAuthorizeUser<InferIOAuthorize<TIO>>>,
871
871
  });
872
872
  const other = this._usersManager.getOther(connectionId);
@@ -891,7 +891,7 @@ export class PluvRoom<
891
891
 
892
892
  const result = this._usersManager.addConnection({
893
893
  connectionId,
894
- presence: (presence ?? undefined) as TPresence,
894
+ presence: presence as TPresence,
895
895
  user,
896
896
  });
897
897
 
@@ -1236,15 +1236,17 @@ export class PluvRoom<
1236
1236
  if (document.visibilityState !== "visible") return;
1237
1237
 
1238
1238
  switch (this._state.connection.state) {
1239
- case ConnectionState.Open:
1239
+ case ConnectionState.Open: {
1240
1240
  this._clearInterval(this._intervals.heartbeat);
1241
1241
  setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1242
1242
 
1243
1243
  return;
1244
- case ConnectionState.Unavailable:
1244
+ }
1245
+ case ConnectionState.Unavailable: {
1245
1246
  await this._reconnect();
1246
1247
 
1247
1248
  return;
1249
+ }
1248
1250
  default:
1249
1251
  }
1250
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
 
@@ -61,10 +62,14 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
61
62
  public addConnection(
62
63
  userInfo: OptionalProps<UserInfo<TIO, TPresence>, "presence">,
63
64
  ): AddConnectionResult<TIO, TPresence> {
64
- const data: UserInfo<TIO, TPresence> = {
65
- ...userInfo,
66
- presence: userInfo.presence ?? this.initialPresence,
67
- };
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 };
68
73
 
69
74
  const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
70
75
  const clientId = this.getClientId(data);
@@ -187,7 +192,9 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
187
192
 
188
193
  if (!other) return null;
189
194
 
190
- 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;
191
198
  const validated = this._presence ? this._presence.parse(presence) : presence;
192
199
 
193
200
  this._others.set(clientId, { ...other, presence: validated });
@@ -239,10 +246,14 @@ export class UsersManager<TIO extends IOLike, TPresence extends JsonObject = {}>
239
246
  }
240
247
 
241
248
  /**
242
- * @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
243
252
  */
244
253
  public updateMyPresence(patch: Partial<TPresence>): TPresence {
245
- 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;
246
257
  const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
247
258
 
248
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
+ };