@wvdsh/sdk-js 1.3.5 → 1.3.7

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.
package/dist/index.d.ts CHANGED
@@ -82,6 +82,7 @@ type LeaderboardEntries = FunctionReturnType<typeof api.sdk.leaderboards.listEnt
82
82
  type UpsertedLeaderboardEntry = FunctionReturnType<typeof api.sdk.leaderboards.upsertLeaderboardEntry>["entry"] & {
83
83
  userId: GenericId<"users">;
84
84
  username: string;
85
+ userAvatarUrl?: string;
85
86
  };
86
87
  type WavedashEvent = (typeof WavedashEvents)[keyof typeof WavedashEvents];
87
88
  interface WavedashConfig {
@@ -278,6 +279,8 @@ declare class LobbyManager extends WavedashManager {
278
279
  private recentMessageIds;
279
280
  private maybeBeingDeletedLobbyIds;
280
281
  private resetMaybeBeingDeletedLobbyIdTimeouts;
282
+ private static readonly METADATA_UPDATE_THROTTLE_MS;
283
+ private inFlightMetadataUpdate;
281
284
  private cachedLobbies;
282
285
  private unsubscribeLobbyInvites;
283
286
  private seenInviteIds;
@@ -295,7 +298,6 @@ declare class LobbyManager extends WavedashManager {
295
298
  getHostId(lobbyId: GenericId<"lobbies">): GenericId<"users"> | null;
296
299
  getLobbyData(lobbyId: GenericId<"lobbies">, key: string): string | number | null;
297
300
  deleteLobbyData(lobbyId: GenericId<"lobbies">, key: string): boolean;
298
- private debouncedMetadataUpdate;
299
301
  setLobbyData(lobbyId: GenericId<"lobbies">, key: string, value: string | number | null): boolean;
300
302
  getLobbyMaxPlayers(lobbyId: GenericId<"lobbies">): number;
301
303
  getNumLobbyUsers(lobbyId: GenericId<"lobbies">): number;
@@ -333,7 +335,8 @@ declare class LobbyManager extends WavedashManager {
333
335
  * Called during session end to ensure no lingering listeners.
334
336
  */
335
337
  destroy(): void;
336
- private processPendingLobbyDataUpdates;
338
+ private throttledSetMetadata;
339
+ private setMetadata;
337
340
  /**
338
341
  * Process user updates and emit individual user events
339
342
  * @param newUsers - The updated list of lobby users
@@ -556,10 +559,6 @@ declare class P2PManager extends WavedashManager {
556
559
  private decodeBinaryMessage;
557
560
  }
558
561
 
559
- type StatEntry = {
560
- identifier: string;
561
- value: number;
562
- };
563
562
  declare class StatsManager extends WavedashManager {
564
563
  private stats;
565
564
  private unlockedAchievements;
@@ -569,24 +568,23 @@ declare class StatsManager extends WavedashManager {
569
568
  private knownAchievementIds;
570
569
  private loaded;
571
570
  private subscriptions;
572
- private periodicPersistInterval;
571
+ private inFlightPersist;
572
+ private flushRequested;
573
573
  constructor(sdk: WavedashSDK);
574
574
  destroy(): void;
575
575
  private isReady;
576
576
  private subscribe;
577
577
  requestStats(): Promise<boolean>;
578
- private debouncedPersist;
578
+ private throttledPersist;
579
579
  storeStats(): boolean;
580
+ private requestPersistFlush;
580
581
  private persist;
581
582
  getStat(identifier: string): number;
582
583
  setStat(identifier: string, value: number, storeNow?: boolean): boolean;
583
584
  getAchievement(identifier: string): boolean;
584
585
  setAchievement(identifier: string, storeNow?: boolean): boolean;
585
586
  /** @destructive - Returns the pending stats and achievements and resets the dirty collections */
586
- getPendingData(): {
587
- stats: StatEntry[];
588
- achievements: string[];
589
- } | null;
587
+ private getPendingData;
590
588
  }
591
589
 
592
590
  /**
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { ConvexClient } from "convex/browser";
3
3
 
4
4
  // src/services/lobby.ts
5
- import debounce from "lodash.debounce";
5
+ import throttle from "lodash.throttle";
6
6
 
7
7
  // src/constants.ts
8
8
  import {
@@ -94,7 +94,7 @@ var WavedashManager = class {
94
94
  };
95
95
 
96
96
  // src/services/lobby.ts
97
- var LobbyManager = class extends WavedashManager {
97
+ var _LobbyManager = class _LobbyManager extends WavedashManager {
98
98
  constructor(sdk) {
99
99
  super(sdk);
100
100
  // Track current lobby state
@@ -109,6 +109,7 @@ var LobbyManager = class extends WavedashManager {
109
109
  this.recentMessageIds = [];
110
110
  this.maybeBeingDeletedLobbyIds = /* @__PURE__ */ new Set();
111
111
  this.resetMaybeBeingDeletedLobbyIdTimeouts = /* @__PURE__ */ new Map();
112
+ this.inFlightMetadataUpdate = null;
112
113
  // Cache results of queries for a list of lobbies
113
114
  // We'll cache metadata and num users for each lobby and return that info synchronously when requested by the game
114
115
  this.cachedLobbies = {};
@@ -117,9 +118,13 @@ var LobbyManager = class extends WavedashManager {
117
118
  this.seenInviteIds = /* @__PURE__ */ new Set();
118
119
  // Queue for serializing P2P connection updates to prevent race conditions
119
120
  this.p2pUpdateQueue = Promise.resolve();
120
- this.debouncedMetadataUpdate = debounce(
121
- () => this.processPendingLobbyDataUpdates(),
122
- 50
121
+ // leading: false so the first call doesn't fire synchronously inside setLobbyData
122
+ // (which is called from a tight loop); trailing: true to flush coalesced updates
123
+ // at the end of the window.
124
+ this.throttledSetMetadata = throttle(
125
+ () => this.setMetadata(),
126
+ _LobbyManager.METADATA_UPDATE_THROTTLE_MS,
127
+ { leading: false, trailing: true }
123
128
  );
124
129
  /**
125
130
  * Process user updates and emit individual user events
@@ -266,7 +271,7 @@ var LobbyManager = class extends WavedashManager {
266
271
  this.lobbyMetadata[key] = value;
267
272
  }
268
273
  this.pendingMetadataUpdates[key] = value;
269
- this.debouncedMetadataUpdate();
274
+ this.throttledSetMetadata();
270
275
  return true;
271
276
  }
272
277
  getLobbyMaxPlayers(lobbyId) {
@@ -443,7 +448,7 @@ var LobbyManager = class extends WavedashManager {
443
448
  cleanupLobbyState() {
444
449
  const currentLobbyId = this.lobbyId;
445
450
  this.lobbyId = null;
446
- this.debouncedMetadataUpdate.cancel();
451
+ this.throttledSetMetadata.cancel();
447
452
  this.pendingMetadataUpdates = {};
448
453
  if (this.unsubscribeLobbyMessages) {
449
454
  this.unsubscribeLobbyMessages();
@@ -501,14 +506,22 @@ var LobbyManager = class extends WavedashManager {
501
506
  this.seenInviteIds.clear();
502
507
  this.cachedLobbies = {};
503
508
  }
504
- processPendingLobbyDataUpdates() {
509
+ setMetadata() {
510
+ if (this.inFlightMetadataUpdate !== null) return;
511
+ if (this.lobbyId === null) return;
512
+ if (Object.keys(this.pendingMetadataUpdates).length === 0) return;
505
513
  const updates = this.pendingMetadataUpdates;
506
514
  this.pendingMetadataUpdates = {};
507
- this.sdk.convexClient.mutation(api.sdk.gameLobby.setLobbyMetadata, {
515
+ this.inFlightMetadataUpdate = this.sdk.convexClient.mutation(api.sdk.gameLobby.setLobbyMetadata, {
508
516
  lobbyId: this.lobbyId,
509
517
  updates
510
518
  }).catch((error) => {
511
519
  this.sdk.logger.error("Error updating lobby metadata:", error);
520
+ }).finally(() => {
521
+ this.inFlightMetadataUpdate = null;
522
+ if (Object.keys(this.pendingMetadataUpdates).length > 0) {
523
+ this.throttledSetMetadata();
524
+ }
512
525
  });
513
526
  }
514
527
  // ================
@@ -547,6 +560,10 @@ var LobbyManager = class extends WavedashManager {
547
560
  }
548
561
  }
549
562
  };
563
+ // Throttle (not debounce) batches rapid setLobbyData calls; the in-flight
564
+ // gate in setMetadata prevents OCC self-conflicts.
565
+ _LobbyManager.METADATA_UPDATE_THROTTLE_MS = 150;
566
+ var LobbyManager = _LobbyManager;
550
567
 
551
568
  // src/utils/indexedDB.ts
552
569
  var LOCAL_STORAGE_DB_NAME = "/userfs";
@@ -1081,7 +1098,8 @@ var LeaderboardManager = class extends WavedashManager {
1081
1098
  return {
1082
1099
  ...result.entry,
1083
1100
  userId: this.sdk.wavedashUser.id,
1084
- username: this.sdk.wavedashUser.username
1101
+ username: this.sdk.wavedashUser.username,
1102
+ userAvatarUrl: this.sdk.wavedashUser.avatarUrl
1085
1103
  };
1086
1104
  }
1087
1105
  // ================
@@ -2424,9 +2442,8 @@ var P2PManager = _P2PManager;
2424
2442
 
2425
2443
  // src/services/stats.ts
2426
2444
  import { api as api6 } from "@wvdsh/api";
2427
- import debounce2 from "lodash.debounce";
2428
- var STORE_DEBOUNCE_MS = 1e3;
2429
- var PERIODIC_PERSIST_MS = 1e4;
2445
+ import throttle2 from "lodash.throttle";
2446
+ var STORE_THROTTLE_MS = 1e3;
2430
2447
  var StatsManager = class extends WavedashManager {
2431
2448
  constructor(sdk) {
2432
2449
  super(sdk);
@@ -2444,32 +2461,33 @@ var StatsManager = class extends WavedashManager {
2444
2461
  this.loaded = { stats: false, achievements: false };
2445
2462
  // Subscription cleanup
2446
2463
  this.subscriptions = [];
2447
- // Background flush timer see PERIODIC_PERSIST_MS
2448
- this.periodicPersistInterval = null;
2464
+ // Single in-flight persist mutation; prevents OCC self-conflicts.
2465
+ this.inFlightPersist = null;
2466
+ // Set when a storeNow flush hits the in-flight gate; persist's .finally()
2467
+ // checks this and fires immediately on the next cycle instead of waiting
2468
+ // out the throttle window. (lodash treats the gated flush as a successful
2469
+ // invocation, so without this flag a contended storeNow waits ~THROTTLE_MS.)
2470
+ this.flushRequested = false;
2449
2471
  // ================
2450
2472
  // Store / Persist
2451
2473
  // ================
2452
- // Debounced persist used by storeNow in setters to batch rapid calls.
2453
- // Leading+trailing: first call fires immediately, subsequent calls within
2454
- // the window are batched into one trailing call.
2455
- this.debouncedPersist = debounce2(() => this.persist(), STORE_DEBOUNCE_MS, {
2456
- leading: true,
2457
- trailing: true
2458
- });
2474
+ // leading: false so a single setStat doesn't fire synchronously inside the
2475
+ // setter; trailing: true to flush coalesced edits at the end of the window.
2476
+ // storeNow=true (and storeStats()) call .flush() to fire the pending invocation
2477
+ // immediately. The in-flight gate in persist() covers mutations that outlast
2478
+ // the throttle window, which would otherwise overlap and cause OCC conflicts.
2479
+ this.throttledPersist = throttle2(
2480
+ () => this.persist(),
2481
+ STORE_THROTTLE_MS,
2482
+ { leading: false, trailing: true }
2483
+ );
2459
2484
  this.subscribe();
2460
2485
  this.requestStats().catch((error) => {
2461
2486
  this.sdk.logger.error("Initial stats fetch failed:", error);
2462
2487
  });
2463
- this.periodicPersistInterval = setInterval(() => {
2464
- void this.persist();
2465
- }, PERIODIC_PERSIST_MS);
2466
2488
  }
2467
2489
  destroy() {
2468
- this.debouncedPersist.cancel();
2469
- if (this.periodicPersistInterval !== null) {
2470
- clearInterval(this.periodicPersistInterval);
2471
- this.periodicPersistInterval = null;
2472
- }
2490
+ this.throttledPersist.cancel();
2473
2491
  for (const unsub of this.subscriptions) unsub();
2474
2492
  this.subscriptions = [];
2475
2493
  }
@@ -2534,35 +2552,51 @@ var StatsManager = class extends WavedashManager {
2534
2552
  }
2535
2553
  storeStats() {
2536
2554
  if (!this.isReady()) return false;
2537
- this.debouncedPersist.cancel();
2538
- this.persist();
2555
+ this.throttledPersist();
2556
+ this.requestPersistFlush();
2539
2557
  return true;
2540
2558
  }
2541
- async persist() {
2559
+ // Force-fire the throttled persist now. If a mutation is already in flight,
2560
+ // the gate will swallow the flush(), so we also flag flushRequested so the
2561
+ // next .finally() flushes again instead of waiting a full throttle window.
2562
+ requestPersistFlush() {
2563
+ if (this.inFlightPersist !== null) this.flushRequested = true;
2564
+ this.throttledPersist.flush();
2565
+ }
2566
+ persist() {
2567
+ if (this.inFlightPersist !== null) return;
2568
+ if (this.dirtyStats.size === 0 && this.dirtyAchievements.size === 0) return;
2542
2569
  const pending = this.getPendingData();
2543
2570
  if (!pending) return;
2544
- try {
2545
- await Promise.all([
2546
- pending.stats.length > 0 ? this.sdk.convexClient.mutation(
2547
- api6.sdk.gameAchievements.setUserGameStats,
2548
- { stats: pending.stats }
2549
- ) : Promise.resolve(),
2550
- pending.achievements.length > 0 ? this.sdk.convexClient.mutation(
2551
- api6.sdk.gameAchievements.setUserGameAchievements,
2552
- { achievements: pending.achievements }
2553
- ) : Promise.resolve()
2554
- ]);
2571
+ this.inFlightPersist = Promise.all([
2572
+ pending.stats.length > 0 ? this.sdk.convexClient.mutation(
2573
+ api6.sdk.gameAchievements.setUserGameStats,
2574
+ { stats: pending.stats }
2575
+ ) : Promise.resolve(),
2576
+ pending.achievements.length > 0 ? this.sdk.convexClient.mutation(
2577
+ api6.sdk.gameAchievements.setUserGameAchievements,
2578
+ { achievements: pending.achievements }
2579
+ ) : Promise.resolve()
2580
+ ]).then(() => {
2555
2581
  this.sdk.gameEventManager.notifyGame(WavedashEvents.STATS_STORED, {
2556
2582
  success: true
2557
2583
  });
2558
- } catch (error) {
2584
+ }).catch((error) => {
2559
2585
  const message = error instanceof Error ? error.message : `Error storing stats: ${error}`;
2560
2586
  this.sdk.logger.error(message);
2561
2587
  this.sdk.gameEventManager.notifyGame(WavedashEvents.STATS_STORED, {
2562
2588
  success: false,
2563
2589
  message
2564
2590
  });
2565
- }
2591
+ }).finally(() => {
2592
+ this.inFlightPersist = null;
2593
+ const shouldFlushNow = this.flushRequested;
2594
+ this.flushRequested = false;
2595
+ if (this.dirtyStats.size > 0 || this.dirtyAchievements.size > 0) {
2596
+ this.throttledPersist();
2597
+ if (shouldFlushNow) this.throttledPersist.flush();
2598
+ }
2599
+ });
2566
2600
  }
2567
2601
  // ================
2568
2602
  // Stats
@@ -2576,8 +2610,9 @@ var StatsManager = class extends WavedashManager {
2576
2610
  if (this.stats.get(identifier) !== value) {
2577
2611
  this.stats.set(identifier, value);
2578
2612
  this.dirtyStats.add(identifier);
2613
+ this.throttledPersist();
2579
2614
  }
2580
- if (storeNow) this.debouncedPersist();
2615
+ if (storeNow) this.requestPersistFlush();
2581
2616
  return true;
2582
2617
  }
2583
2618
  // ================
@@ -2594,13 +2629,11 @@ var StatsManager = class extends WavedashManager {
2594
2629
  if (!this.unlockedAchievements.has(identifier)) {
2595
2630
  this.unlockedAchievements.add(identifier);
2596
2631
  this.dirtyAchievements.add(identifier);
2632
+ this.throttledPersist();
2597
2633
  }
2598
- if (storeNow) this.debouncedPersist();
2634
+ if (storeNow) this.requestPersistFlush();
2599
2635
  return true;
2600
2636
  }
2601
- // ================
2602
- // Session End
2603
- // ================
2604
2637
  /** @destructive - Returns the pending stats and achievements and resets the dirty collections */
2605
2638
  getPendingData() {
2606
2639
  if (this.dirtyStats.size === 0 && this.dirtyAchievements.size === 0) {
@@ -2705,8 +2738,10 @@ var HeartbeatManager = class extends WavedashManager {
2705
2738
  if (!reestablish && this.heartbeatInFlight) return;
2706
2739
  this.heartbeatInFlight = true;
2707
2740
  this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
2708
- ...reestablish ? { data: { forceUpdate: true } } : {},
2709
- deviceFingerprint: this.deviceFingerprint
2741
+ ...reestablish ? {
2742
+ data: { forceUpdate: true },
2743
+ deviceFingerprint: this.deviceFingerprint
2744
+ } : {}
2710
2745
  }).then((accepted) => {
2711
2746
  if (accepted) {
2712
2747
  this.lastHeartbeatTime = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wvdsh/sdk-js",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "type": "module",
5
5
  "description": "Wavedash JavaScript SDK",
6
6
  "main": "./dist/client.js",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@eslint/js": "^9.39.2",
43
- "@types/lodash.debounce": "^4.0.9",
43
+ "@types/lodash.throttle": "^4.1.9",
44
44
  "eslint": "^9.39.2",
45
45
  "eslint-config-prettier": "^10.1.8",
46
46
  "prettier": "^3.7.4",
@@ -49,8 +49,8 @@
49
49
  "typescript-eslint": "^8.52.0"
50
50
  },
51
51
  "dependencies": {
52
- "@wvdsh/api": "^0.1.14",
52
+ "@wvdsh/api": "^0.1.16",
53
53
  "convex": "^1.34.0",
54
- "lodash.debounce": "^4.0.8"
54
+ "lodash.throttle": "^4.1.1"
55
55
  }
56
56
  }