@wvdsh/sdk-js 1.3.5 → 1.3.6

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
@@ -278,6 +278,8 @@ declare class LobbyManager extends WavedashManager {
278
278
  private recentMessageIds;
279
279
  private maybeBeingDeletedLobbyIds;
280
280
  private resetMaybeBeingDeletedLobbyIdTimeouts;
281
+ private static readonly METADATA_UPDATE_THROTTLE_MS;
282
+ private inFlightMetadataUpdate;
281
283
  private cachedLobbies;
282
284
  private unsubscribeLobbyInvites;
283
285
  private seenInviteIds;
@@ -295,7 +297,6 @@ declare class LobbyManager extends WavedashManager {
295
297
  getHostId(lobbyId: GenericId<"lobbies">): GenericId<"users"> | null;
296
298
  getLobbyData(lobbyId: GenericId<"lobbies">, key: string): string | number | null;
297
299
  deleteLobbyData(lobbyId: GenericId<"lobbies">, key: string): boolean;
298
- private debouncedMetadataUpdate;
299
300
  setLobbyData(lobbyId: GenericId<"lobbies">, key: string, value: string | number | null): boolean;
300
301
  getLobbyMaxPlayers(lobbyId: GenericId<"lobbies">): number;
301
302
  getNumLobbyUsers(lobbyId: GenericId<"lobbies">): number;
@@ -333,7 +334,8 @@ declare class LobbyManager extends WavedashManager {
333
334
  * Called during session end to ensure no lingering listeners.
334
335
  */
335
336
  destroy(): void;
336
- private processPendingLobbyDataUpdates;
337
+ private throttledSetMetadata;
338
+ private setMetadata;
337
339
  /**
338
340
  * Process user updates and emit individual user events
339
341
  * @param newUsers - The updated list of lobby users
@@ -556,10 +558,6 @@ declare class P2PManager extends WavedashManager {
556
558
  private decodeBinaryMessage;
557
559
  }
558
560
 
559
- type StatEntry = {
560
- identifier: string;
561
- value: number;
562
- };
563
561
  declare class StatsManager extends WavedashManager {
564
562
  private stats;
565
563
  private unlockedAchievements;
@@ -569,24 +567,23 @@ declare class StatsManager extends WavedashManager {
569
567
  private knownAchievementIds;
570
568
  private loaded;
571
569
  private subscriptions;
572
- private periodicPersistInterval;
570
+ private inFlightPersist;
571
+ private flushRequested;
573
572
  constructor(sdk: WavedashSDK);
574
573
  destroy(): void;
575
574
  private isReady;
576
575
  private subscribe;
577
576
  requestStats(): Promise<boolean>;
578
- private debouncedPersist;
577
+ private throttledPersist;
579
578
  storeStats(): boolean;
579
+ private requestPersistFlush;
580
580
  private persist;
581
581
  getStat(identifier: string): number;
582
582
  setStat(identifier: string, value: number, storeNow?: boolean): boolean;
583
583
  getAchievement(identifier: string): boolean;
584
584
  setAchievement(identifier: string, storeNow?: boolean): boolean;
585
585
  /** @destructive - Returns the pending stats and achievements and resets the dirty collections */
586
- getPendingData(): {
587
- stats: StatEntry[];
588
- achievements: string[];
589
- } | null;
586
+ private getPendingData;
590
587
  }
591
588
 
592
589
  /**
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";
@@ -2424,9 +2441,8 @@ var P2PManager = _P2PManager;
2424
2441
 
2425
2442
  // src/services/stats.ts
2426
2443
  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;
2444
+ import throttle2 from "lodash.throttle";
2445
+ var STORE_THROTTLE_MS = 1e3;
2430
2446
  var StatsManager = class extends WavedashManager {
2431
2447
  constructor(sdk) {
2432
2448
  super(sdk);
@@ -2444,32 +2460,33 @@ var StatsManager = class extends WavedashManager {
2444
2460
  this.loaded = { stats: false, achievements: false };
2445
2461
  // Subscription cleanup
2446
2462
  this.subscriptions = [];
2447
- // Background flush timer see PERIODIC_PERSIST_MS
2448
- this.periodicPersistInterval = null;
2463
+ // Single in-flight persist mutation; prevents OCC self-conflicts.
2464
+ this.inFlightPersist = null;
2465
+ // Set when a storeNow flush hits the in-flight gate; persist's .finally()
2466
+ // checks this and fires immediately on the next cycle instead of waiting
2467
+ // out the throttle window. (lodash treats the gated flush as a successful
2468
+ // invocation, so without this flag a contended storeNow waits ~THROTTLE_MS.)
2469
+ this.flushRequested = false;
2449
2470
  // ================
2450
2471
  // Store / Persist
2451
2472
  // ================
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
- });
2473
+ // leading: false so a single setStat doesn't fire synchronously inside the
2474
+ // setter; trailing: true to flush coalesced edits at the end of the window.
2475
+ // storeNow=true (and storeStats()) call .flush() to fire the pending invocation
2476
+ // immediately. The in-flight gate in persist() covers mutations that outlast
2477
+ // the throttle window, which would otherwise overlap and cause OCC conflicts.
2478
+ this.throttledPersist = throttle2(
2479
+ () => this.persist(),
2480
+ STORE_THROTTLE_MS,
2481
+ { leading: false, trailing: true }
2482
+ );
2459
2483
  this.subscribe();
2460
2484
  this.requestStats().catch((error) => {
2461
2485
  this.sdk.logger.error("Initial stats fetch failed:", error);
2462
2486
  });
2463
- this.periodicPersistInterval = setInterval(() => {
2464
- void this.persist();
2465
- }, PERIODIC_PERSIST_MS);
2466
2487
  }
2467
2488
  destroy() {
2468
- this.debouncedPersist.cancel();
2469
- if (this.periodicPersistInterval !== null) {
2470
- clearInterval(this.periodicPersistInterval);
2471
- this.periodicPersistInterval = null;
2472
- }
2489
+ this.throttledPersist.cancel();
2473
2490
  for (const unsub of this.subscriptions) unsub();
2474
2491
  this.subscriptions = [];
2475
2492
  }
@@ -2534,35 +2551,51 @@ var StatsManager = class extends WavedashManager {
2534
2551
  }
2535
2552
  storeStats() {
2536
2553
  if (!this.isReady()) return false;
2537
- this.debouncedPersist.cancel();
2538
- this.persist();
2554
+ this.throttledPersist();
2555
+ this.requestPersistFlush();
2539
2556
  return true;
2540
2557
  }
2541
- async persist() {
2558
+ // Force-fire the throttled persist now. If a mutation is already in flight,
2559
+ // the gate will swallow the flush(), so we also flag flushRequested so the
2560
+ // next .finally() flushes again instead of waiting a full throttle window.
2561
+ requestPersistFlush() {
2562
+ if (this.inFlightPersist !== null) this.flushRequested = true;
2563
+ this.throttledPersist.flush();
2564
+ }
2565
+ persist() {
2566
+ if (this.inFlightPersist !== null) return;
2567
+ if (this.dirtyStats.size === 0 && this.dirtyAchievements.size === 0) return;
2542
2568
  const pending = this.getPendingData();
2543
2569
  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
- ]);
2570
+ this.inFlightPersist = Promise.all([
2571
+ pending.stats.length > 0 ? this.sdk.convexClient.mutation(
2572
+ api6.sdk.gameAchievements.setUserGameStats,
2573
+ { stats: pending.stats }
2574
+ ) : Promise.resolve(),
2575
+ pending.achievements.length > 0 ? this.sdk.convexClient.mutation(
2576
+ api6.sdk.gameAchievements.setUserGameAchievements,
2577
+ { achievements: pending.achievements }
2578
+ ) : Promise.resolve()
2579
+ ]).then(() => {
2555
2580
  this.sdk.gameEventManager.notifyGame(WavedashEvents.STATS_STORED, {
2556
2581
  success: true
2557
2582
  });
2558
- } catch (error) {
2583
+ }).catch((error) => {
2559
2584
  const message = error instanceof Error ? error.message : `Error storing stats: ${error}`;
2560
2585
  this.sdk.logger.error(message);
2561
2586
  this.sdk.gameEventManager.notifyGame(WavedashEvents.STATS_STORED, {
2562
2587
  success: false,
2563
2588
  message
2564
2589
  });
2565
- }
2590
+ }).finally(() => {
2591
+ this.inFlightPersist = null;
2592
+ const shouldFlushNow = this.flushRequested;
2593
+ this.flushRequested = false;
2594
+ if (this.dirtyStats.size > 0 || this.dirtyAchievements.size > 0) {
2595
+ this.throttledPersist();
2596
+ if (shouldFlushNow) this.throttledPersist.flush();
2597
+ }
2598
+ });
2566
2599
  }
2567
2600
  // ================
2568
2601
  // Stats
@@ -2576,8 +2609,9 @@ var StatsManager = class extends WavedashManager {
2576
2609
  if (this.stats.get(identifier) !== value) {
2577
2610
  this.stats.set(identifier, value);
2578
2611
  this.dirtyStats.add(identifier);
2612
+ this.throttledPersist();
2579
2613
  }
2580
- if (storeNow) this.debouncedPersist();
2614
+ if (storeNow) this.requestPersistFlush();
2581
2615
  return true;
2582
2616
  }
2583
2617
  // ================
@@ -2594,13 +2628,11 @@ var StatsManager = class extends WavedashManager {
2594
2628
  if (!this.unlockedAchievements.has(identifier)) {
2595
2629
  this.unlockedAchievements.add(identifier);
2596
2630
  this.dirtyAchievements.add(identifier);
2631
+ this.throttledPersist();
2597
2632
  }
2598
- if (storeNow) this.debouncedPersist();
2633
+ if (storeNow) this.requestPersistFlush();
2599
2634
  return true;
2600
2635
  }
2601
- // ================
2602
- // Session End
2603
- // ================
2604
2636
  /** @destructive - Returns the pending stats and achievements and resets the dirty collections */
2605
2637
  getPendingData() {
2606
2638
  if (this.dirtyStats.size === 0 && this.dirtyAchievements.size === 0) {
@@ -2705,8 +2737,10 @@ var HeartbeatManager = class extends WavedashManager {
2705
2737
  if (!reestablish && this.heartbeatInFlight) return;
2706
2738
  this.heartbeatInFlight = true;
2707
2739
  this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
2708
- ...reestablish ? { data: { forceUpdate: true } } : {},
2709
- deviceFingerprint: this.deviceFingerprint
2740
+ ...reestablish ? {
2741
+ data: { forceUpdate: true },
2742
+ deviceFingerprint: this.deviceFingerprint
2743
+ } : {}
2710
2744
  }).then((accepted) => {
2711
2745
  if (accepted) {
2712
2746
  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.6",
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",
@@ -51,6 +51,6 @@
51
51
  "dependencies": {
52
52
  "@wvdsh/api": "^0.1.14",
53
53
  "convex": "^1.34.0",
54
- "lodash.debounce": "^4.0.8"
54
+ "lodash.throttle": "^4.1.1"
55
55
  }
56
56
  }