@wvdsh/sdk-js 1.3.4 → 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
@@ -416,6 +418,7 @@ declare class UGCManager extends WavedashManager {
416
418
  constructor(sdk: WavedashSDK);
417
419
  createUGCItem(ugcType: UGCType, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<GenericId<"userGeneratedContent">>;
418
420
  updateUGCItem(ugcId: GenericId<"userGeneratedContent">, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<GenericId<"userGeneratedContent">>;
421
+ deleteUGCItem(ugcId: GenericId<"userGeneratedContent">): Promise<GenericId<"userGeneratedContent">>;
419
422
  downloadUGCItem(ugcId: GenericId<"userGeneratedContent">, filePath: string): Promise<GenericId<"userGeneratedContent">>;
420
423
  }
421
424
 
@@ -555,10 +558,6 @@ declare class P2PManager extends WavedashManager {
555
558
  private decodeBinaryMessage;
556
559
  }
557
560
 
558
- type StatEntry = {
559
- identifier: string;
560
- value: number;
561
- };
562
561
  declare class StatsManager extends WavedashManager {
563
562
  private stats;
564
563
  private unlockedAchievements;
@@ -568,24 +567,23 @@ declare class StatsManager extends WavedashManager {
568
567
  private knownAchievementIds;
569
568
  private loaded;
570
569
  private subscriptions;
571
- private periodicPersistInterval;
570
+ private inFlightPersist;
571
+ private flushRequested;
572
572
  constructor(sdk: WavedashSDK);
573
573
  destroy(): void;
574
574
  private isReady;
575
575
  private subscribe;
576
576
  requestStats(): Promise<boolean>;
577
- private debouncedPersist;
577
+ private throttledPersist;
578
578
  storeStats(): boolean;
579
+ private requestPersistFlush;
579
580
  private persist;
580
581
  getStat(identifier: string): number;
581
582
  setStat(identifier: string, value: number, storeNow?: boolean): boolean;
582
583
  getAchievement(identifier: string): boolean;
583
584
  setAchievement(identifier: string, storeNow?: boolean): boolean;
584
585
  /** @destructive - Returns the pending stats and achievements and resets the dirty collections */
585
- getPendingData(): {
586
- stats: StatEntry[];
587
- achievements: string[];
588
- } | null;
586
+ private getPendingData;
589
587
  }
590
588
 
591
589
  /**
@@ -990,6 +988,11 @@ declare class WavedashSDK extends EventTarget {
990
988
  * @returns ugcId
991
989
  */
992
990
  updateUGCItem(ugcId: GenericId<"userGeneratedContent">, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
991
+ /**
992
+ * Delete a UGC item: removes the row, the R2 object, and frees up the
993
+ * user's storage quota by the size of the deleted upload.
994
+ */
995
+ deleteUGCItem(ugcId: GenericId<"userGeneratedContent">): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
993
996
  downloadUGCItem(ugcId: GenericId<"userGeneratedContent">, filePath: string): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
994
997
  /**
995
998
  * Deletes a remote file from storage
@@ -1161,9 +1164,6 @@ declare class WavedashSDK extends EventTarget {
1161
1164
  ensureGameplayJwt(): Promise<string>;
1162
1165
  /**
1163
1166
  * Tear down every manager. Called on the parent's `END_SESSION` signal
1164
- * (committed leaves only — see GameRunnerComponent.svelte). Idempotent.
1165
- * Each manager's `destroy()` defaults to a no-op; managers with ongoing
1166
- * state (subscriptions, intervals, peer connections) override it.
1167
1167
  */
1168
1168
  private destroy;
1169
1169
  private setupSessionEndListeners;
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";
@@ -674,9 +691,19 @@ var FileSystemManager = class extends WavedashManager {
674
691
  * @returns The path of the remote file that was deleted
675
692
  */
676
693
  async deleteRemoteFile(filePath) {
677
- await this.sdk.convexClient.action(api2.sdk.remoteFileStorage.deleteFile, {
678
- path: this.toRemoteKey(filePath)
694
+ const url = this.getRemoteStorageUrl(filePath);
695
+ const jwt = await this.sdk.ensureGameplayJwt();
696
+ const response = await fetch(url, {
697
+ method: "DELETE",
698
+ headers: {
699
+ Authorization: `Bearer ${jwt}`
700
+ }
679
701
  });
702
+ if (!response.ok) {
703
+ const msg = `Failed to delete remote file ${filePath}: ${response.status} (${response.statusText})`;
704
+ this.sdk.logger.error(msg);
705
+ throw new Error(msg);
706
+ }
680
707
  return filePath;
681
708
  }
682
709
  /**
@@ -942,10 +969,6 @@ var UGCManager = class extends WavedashManager {
942
969
  uploadUrl,
943
970
  filePath
944
971
  );
945
- await this.sdk.convexClient.mutation(
946
- api3.sdk.userGeneratedContent.finishUGCUpload,
947
- { success, ugcId }
948
- );
949
972
  if (!success) {
950
973
  throw new Error(`Failed to upload UGC item: ${filePath}`);
951
974
  }
@@ -972,16 +995,19 @@ var UGCManager = class extends WavedashManager {
972
995
  uploadUrl,
973
996
  filePath
974
997
  );
975
- await this.sdk.convexClient.mutation(
976
- api3.sdk.userGeneratedContent.finishUGCUpload,
977
- { success, ugcId }
978
- );
979
998
  if (!success) {
980
999
  throw new Error(`Failed to upload UGC item: ${filePath}`);
981
1000
  }
982
1001
  }
983
1002
  return ugcId;
984
1003
  }
1004
+ async deleteUGCItem(ugcId) {
1005
+ await this.sdk.convexClient.mutation(
1006
+ api3.sdk.userGeneratedContent.deleteUGCItem,
1007
+ { ugcId }
1008
+ );
1009
+ return ugcId;
1010
+ }
985
1011
  async downloadUGCItem(ugcId, filePath) {
986
1012
  const downloadUrl = await this.sdk.convexClient.query(
987
1013
  api3.sdk.userGeneratedContent.getUGCItemDownloadUrl,
@@ -2415,9 +2441,8 @@ var P2PManager = _P2PManager;
2415
2441
 
2416
2442
  // src/services/stats.ts
2417
2443
  import { api as api6 } from "@wvdsh/api";
2418
- import debounce2 from "lodash.debounce";
2419
- var STORE_DEBOUNCE_MS = 1e3;
2420
- var PERIODIC_PERSIST_MS = 1e4;
2444
+ import throttle2 from "lodash.throttle";
2445
+ var STORE_THROTTLE_MS = 1e3;
2421
2446
  var StatsManager = class extends WavedashManager {
2422
2447
  constructor(sdk) {
2423
2448
  super(sdk);
@@ -2435,32 +2460,33 @@ var StatsManager = class extends WavedashManager {
2435
2460
  this.loaded = { stats: false, achievements: false };
2436
2461
  // Subscription cleanup
2437
2462
  this.subscriptions = [];
2438
- // Background flush timer see PERIODIC_PERSIST_MS
2439
- 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;
2440
2470
  // ================
2441
2471
  // Store / Persist
2442
2472
  // ================
2443
- // Debounced persist used by storeNow in setters to batch rapid calls.
2444
- // Leading+trailing: first call fires immediately, subsequent calls within
2445
- // the window are batched into one trailing call.
2446
- this.debouncedPersist = debounce2(() => this.persist(), STORE_DEBOUNCE_MS, {
2447
- leading: true,
2448
- trailing: true
2449
- });
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
+ );
2450
2483
  this.subscribe();
2451
2484
  this.requestStats().catch((error) => {
2452
2485
  this.sdk.logger.error("Initial stats fetch failed:", error);
2453
2486
  });
2454
- this.periodicPersistInterval = setInterval(() => {
2455
- void this.persist();
2456
- }, PERIODIC_PERSIST_MS);
2457
2487
  }
2458
2488
  destroy() {
2459
- this.debouncedPersist.cancel();
2460
- if (this.periodicPersistInterval !== null) {
2461
- clearInterval(this.periodicPersistInterval);
2462
- this.periodicPersistInterval = null;
2463
- }
2489
+ this.throttledPersist.cancel();
2464
2490
  for (const unsub of this.subscriptions) unsub();
2465
2491
  this.subscriptions = [];
2466
2492
  }
@@ -2525,35 +2551,51 @@ var StatsManager = class extends WavedashManager {
2525
2551
  }
2526
2552
  storeStats() {
2527
2553
  if (!this.isReady()) return false;
2528
- this.debouncedPersist.cancel();
2529
- this.persist();
2554
+ this.throttledPersist();
2555
+ this.requestPersistFlush();
2530
2556
  return true;
2531
2557
  }
2532
- 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;
2533
2568
  const pending = this.getPendingData();
2534
2569
  if (!pending) return;
2535
- try {
2536
- await Promise.all([
2537
- pending.stats.length > 0 ? this.sdk.convexClient.mutation(
2538
- api6.sdk.gameAchievements.setUserGameStats,
2539
- { stats: pending.stats }
2540
- ) : Promise.resolve(),
2541
- pending.achievements.length > 0 ? this.sdk.convexClient.mutation(
2542
- api6.sdk.gameAchievements.setUserGameAchievements,
2543
- { achievements: pending.achievements }
2544
- ) : Promise.resolve()
2545
- ]);
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(() => {
2546
2580
  this.sdk.gameEventManager.notifyGame(WavedashEvents.STATS_STORED, {
2547
2581
  success: true
2548
2582
  });
2549
- } catch (error) {
2583
+ }).catch((error) => {
2550
2584
  const message = error instanceof Error ? error.message : `Error storing stats: ${error}`;
2551
2585
  this.sdk.logger.error(message);
2552
2586
  this.sdk.gameEventManager.notifyGame(WavedashEvents.STATS_STORED, {
2553
2587
  success: false,
2554
2588
  message
2555
2589
  });
2556
- }
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
+ });
2557
2599
  }
2558
2600
  // ================
2559
2601
  // Stats
@@ -2567,8 +2609,9 @@ var StatsManager = class extends WavedashManager {
2567
2609
  if (this.stats.get(identifier) !== value) {
2568
2610
  this.stats.set(identifier, value);
2569
2611
  this.dirtyStats.add(identifier);
2612
+ this.throttledPersist();
2570
2613
  }
2571
- if (storeNow) this.debouncedPersist();
2614
+ if (storeNow) this.requestPersistFlush();
2572
2615
  return true;
2573
2616
  }
2574
2617
  // ================
@@ -2585,13 +2628,11 @@ var StatsManager = class extends WavedashManager {
2585
2628
  if (!this.unlockedAchievements.has(identifier)) {
2586
2629
  this.unlockedAchievements.add(identifier);
2587
2630
  this.dirtyAchievements.add(identifier);
2631
+ this.throttledPersist();
2588
2632
  }
2589
- if (storeNow) this.debouncedPersist();
2633
+ if (storeNow) this.requestPersistFlush();
2590
2634
  return true;
2591
2635
  }
2592
- // ================
2593
- // Session End
2594
- // ================
2595
2636
  /** @destructive - Returns the pending stats and achievements and resets the dirty collections */
2596
2637
  getPendingData() {
2597
2638
  if (this.dirtyStats.size === 0 && this.dirtyAchievements.size === 0) {
@@ -2696,8 +2737,10 @@ var HeartbeatManager = class extends WavedashManager {
2696
2737
  if (!reestablish && this.heartbeatInFlight) return;
2697
2738
  this.heartbeatInFlight = true;
2698
2739
  this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
2699
- ...reestablish ? { data: { forceUpdate: true } } : {},
2700
- deviceFingerprint: this.deviceFingerprint
2740
+ ...reestablish ? {
2741
+ data: { forceUpdate: true },
2742
+ deviceFingerprint: this.deviceFingerprint
2743
+ } : {}
2701
2744
  }).then((accepted) => {
2702
2745
  if (accepted) {
2703
2746
  this.lastHeartbeatTime = Date.now();
@@ -3719,6 +3762,18 @@ var WavedashSDK = class extends EventTarget {
3719
3762
  filePath
3720
3763
  );
3721
3764
  }
3765
+ /**
3766
+ * Delete a UGC item: removes the row, the R2 object, and frees up the
3767
+ * user's storage quota by the size of the deleted upload.
3768
+ */
3769
+ async deleteUGCItem(ugcId) {
3770
+ return this.apiCall(
3771
+ this.ugcManager,
3772
+ "deleteUGCItem",
3773
+ [["ugcId", vId("userGeneratedContent")]],
3774
+ ugcId
3775
+ );
3776
+ }
3722
3777
  async downloadUGCItem(ugcId, filePath) {
3723
3778
  return this.apiCall(
3724
3779
  this.ugcManager,
@@ -4253,9 +4308,6 @@ var WavedashSDK = class extends EventTarget {
4253
4308
  }
4254
4309
  /**
4255
4310
  * Tear down every manager. Called on the parent's `END_SESSION` signal
4256
- * (committed leaves only — see GameRunnerComponent.svelte). Idempotent.
4257
- * Each manager's `destroy()` defaults to a no-op; managers with ongoing
4258
- * state (subscriptions, intervals, peer connections) override it.
4259
4311
  */
4260
4312
  destroy() {
4261
4313
  if (this.destroyed) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wvdsh/sdk-js",
3
- "version": "1.3.4",
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",
@@ -49,8 +49,8 @@
49
49
  "typescript-eslint": "^8.52.0"
50
50
  },
51
51
  "dependencies": {
52
- "@wvdsh/api": "^0.1.11",
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
  }