@wvdsh/sdk-js 1.3.3 → 1.3.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.
package/dist/index.d.ts CHANGED
@@ -245,14 +245,28 @@ interface P2PConfig {
245
245
  maxIncomingMessages?: number;
246
246
  }
247
247
 
248
+ /**
249
+ * Base class for SDK managers. Provides the shared `sdk` reference and a
250
+ * default no-op `destroy()` so the SDK can safely iterate every manager
251
+ * during teardown without each one having to define an empty stub.
252
+ *
253
+ * Override `destroy()` in any manager that owns ongoing state — Convex
254
+ * subscriptions, intervals, peer connections, monkey-patched globals, etc.
255
+ * — to make sure that state is released when the SDK is torn down.
256
+ */
257
+ declare abstract class WavedashManager {
258
+ protected sdk: WavedashSDK;
259
+ constructor(sdk: WavedashSDK);
260
+ destroy(): void;
261
+ }
262
+
248
263
  /**
249
264
  * Lobby service
250
265
  *
251
266
  * Implements each of the lobby methods of the Wavedash SDK
252
267
  */
253
268
 
254
- declare class LobbyManager {
255
- private sdk;
269
+ declare class LobbyManager extends WavedashManager {
256
270
  private unsubscribeLobbyMessages;
257
271
  private unsubscribeLobbyUsers;
258
272
  private unsubscribeLobbyData;
@@ -342,8 +356,7 @@ declare class LobbyManager {
342
356
  * TODO: Extend this to game-level assets as well.
343
357
  */
344
358
 
345
- declare class FileSystemManager {
346
- private sdk;
359
+ declare class FileSystemManager extends WavedashManager {
347
360
  private remoteStorageOrigin;
348
361
  constructor(sdk: WavedashSDK);
349
362
  /**
@@ -399,8 +412,7 @@ declare class FileSystemManager {
399
412
  * Implements each of the user generated content methods of the Wavedash SDK
400
413
  */
401
414
 
402
- declare class UGCManager {
403
- private sdk;
415
+ declare class UGCManager extends WavedashManager {
404
416
  constructor(sdk: WavedashSDK);
405
417
  createUGCItem(ugcType: UGCType, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<GenericId<"userGeneratedContent">>;
406
418
  updateUGCItem(ugcId: GenericId<"userGeneratedContent">, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<GenericId<"userGeneratedContent">>;
@@ -413,8 +425,7 @@ declare class UGCManager {
413
425
  * Implements each of the leaderboard methods of the Wavedash SDK
414
426
  */
415
427
 
416
- declare class LeaderboardManager {
417
- private sdk;
428
+ declare class LeaderboardManager extends WavedashManager {
418
429
  private leaderboardCache;
419
430
  constructor(sdk: WavedashSDK);
420
431
  getLeaderboard(name: string): Promise<Leaderboard>;
@@ -433,8 +444,7 @@ declare class LeaderboardManager {
433
444
  * Handles WebRTC peer-to-peer connections for lobbies
434
445
  */
435
446
 
436
- declare class P2PManager {
437
- private sdk;
447
+ declare class P2PManager extends WavedashManager {
438
448
  private config;
439
449
  private currentConnection;
440
450
  private peerConnections;
@@ -479,6 +489,7 @@ declare class P2PManager {
479
489
  private textDecoder;
480
490
  private initialized;
481
491
  constructor(sdk: WavedashSDK);
492
+ destroy(): void;
482
493
  private ensureInitialized;
483
494
  init(config?: Partial<P2PConfig>): void;
484
495
  initializeP2PForCurrentLobby(lobbyId: GenericId<"lobbies">, members: SDKUser[]): Promise<P2PConnection>;
@@ -548,8 +559,7 @@ type StatEntry = {
548
559
  identifier: string;
549
560
  value: number;
550
561
  };
551
- declare class StatsManager {
552
- private sdk;
562
+ declare class StatsManager extends WavedashManager {
553
563
  private stats;
554
564
  private unlockedAchievements;
555
565
  private dirtyStats;
@@ -558,6 +568,7 @@ declare class StatsManager {
558
568
  private knownAchievementIds;
559
569
  private loaded;
560
570
  private subscriptions;
571
+ private periodicPersistInterval;
561
572
  constructor(sdk: WavedashSDK);
562
573
  destroy(): void;
563
574
  private isReady;
@@ -585,8 +596,7 @@ declare class StatsManager {
585
596
  * Lets the game update userPresence in the backend
586
597
  */
587
598
 
588
- declare class HeartbeatManager {
589
- private sdk;
599
+ declare class HeartbeatManager extends WavedashManager {
590
600
  private deviceFingerprint;
591
601
  private deviceFingerprintReady;
592
602
  private testConnectionInterval;
@@ -622,8 +632,7 @@ declare class HeartbeatManager {
622
632
  isCurrentlyConnected(): boolean;
623
633
  }
624
634
 
625
- declare class GameEventManager {
626
- private sdk;
635
+ declare class GameEventManager extends WavedashManager {
627
636
  private eventQueue;
628
637
  constructor(sdk: WavedashSDK);
629
638
  notifyGame(event: WavedashEvent, payload: string | number | object): void;
@@ -650,10 +659,9 @@ declare class GameEventManager {
650
659
  * calls route through us. The iframe isn't granted the fullscreen feature
651
660
  * policy anymore, so without these shims those calls would silently reject.
652
661
  */
653
- declare class FullscreenManager {
662
+ declare class FullscreenManager extends WavedashManager {
654
663
  private _isFullscreen;
655
664
  private listeners;
656
- private sdk;
657
665
  constructor(sdk: WavedashSDK);
658
666
  isFullscreen(): boolean;
659
667
  /**
@@ -680,8 +688,7 @@ declare class FullscreenManager {
680
688
  * - `takeFocus()` is also called after load completes so the game starts
681
689
  * with keyboard focus without the player clicking first.
682
690
  */
683
- declare class OverlayManager {
684
- private sdk;
691
+ declare class OverlayManager extends WavedashManager {
685
692
  constructor(sdk: WavedashSDK);
686
693
  toggleOverlay(): void;
687
694
  takeFocus(): void;
@@ -694,8 +701,7 @@ declare class OverlayManager {
694
701
  * Implements friend-related methods for the Wavedash SDK
695
702
  */
696
703
 
697
- declare class FriendsManager {
698
- private sdk;
704
+ declare class FriendsManager extends WavedashManager {
699
705
  private userCache;
700
706
  constructor(sdk: WavedashSDK);
701
707
  /**
@@ -780,8 +786,7 @@ declare class WavedashSDK extends EventTarget {
780
786
  private _eventsReady;
781
787
  get eventsReady(): boolean;
782
788
  private launchParams;
783
- private sessionEndSent;
784
- private convexHttpUrl;
789
+ private destroyed;
785
790
  private gameFinishedLoading;
786
791
  Events: {
787
792
  readonly LOBBY_MESSAGE: "LobbyMessage";
@@ -869,6 +874,7 @@ declare class WavedashSDK extends EventTarget {
869
874
  p2pManager: P2PManager;
870
875
  fullscreenManager: FullscreenManager;
871
876
  overlayManager: OverlayManager;
877
+ private managers;
872
878
  private gameplayJwt;
873
879
  private gameplayJwtPromise;
874
880
  private setupWarningTimeout;
@@ -1154,12 +1160,12 @@ declare class WavedashSDK extends EventTarget {
1154
1160
  */
1155
1161
  ensureGameplayJwt(): Promise<string>;
1156
1162
  /**
1157
- * Set up listeners that flush the end-of-session request when the iframe
1158
- * is going away. We listen for three signals:
1159
- * - `beforeunload` / `pagehide` on our own window: covers tab close, hard
1160
- * reload, and top-level navigation of the parent.
1161
- * - `END_SESSION` postMessage from the parent: covers parent SPA navigation
1163
+ * 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.
1162
1167
  */
1168
+ private destroy;
1163
1169
  private setupSessionEndListeners;
1164
1170
  }
1165
1171
  declare global {
package/dist/index.js CHANGED
@@ -83,8 +83,20 @@ var WavedashEvents = {
83
83
 
84
84
  // src/services/lobby.ts
85
85
  import { api, IFRAME_MESSAGE_TYPE } from "@wvdsh/api";
86
- var LobbyManager = class {
86
+
87
+ // src/services/manager.ts
88
+ var WavedashManager = class {
87
89
  constructor(sdk) {
90
+ this.sdk = sdk;
91
+ }
92
+ destroy() {
93
+ }
94
+ };
95
+
96
+ // src/services/lobby.ts
97
+ var LobbyManager = class extends WavedashManager {
98
+ constructor(sdk) {
99
+ super(sdk);
88
100
  // Track current lobby state
89
101
  this.unsubscribeLobbyMessages = null;
90
102
  this.unsubscribeLobbyUsers = null;
@@ -182,7 +194,6 @@ var LobbyManager = class {
182
194
  invites.map((invite) => invite.notificationId)
183
195
  );
184
196
  };
185
- this.sdk = sdk;
186
197
  this.unsubscribeLobbyInvites = this.sdk.convexClient.onUpdate(
187
198
  api.sdk.gameLobby.getLobbyInvites,
188
199
  {},
@@ -611,9 +622,9 @@ function toBlobFromIndexedDBValue(value) {
611
622
  import { api as api2 } from "@wvdsh/api";
612
623
  var REMOTE_STORAGE_FOLDER = "userfs";
613
624
  var WAVEDASH_PERSISTENT_DATA_PATH = "/idbfs/wavedash";
614
- var FileSystemManager = class {
625
+ var FileSystemManager = class extends WavedashManager {
615
626
  constructor(sdk) {
616
- this.sdk = sdk;
627
+ super(sdk);
617
628
  }
618
629
  /**
619
630
  * Converts a local filesystem path into a full R2 object key.
@@ -907,9 +918,9 @@ var FileSystemManager = class {
907
918
 
908
919
  // src/services/ugc.ts
909
920
  import { api as api3 } from "@wvdsh/api";
910
- var UGCManager = class {
921
+ var UGCManager = class extends WavedashManager {
911
922
  constructor(sdk) {
912
- this.sdk = sdk;
923
+ super(sdk);
913
924
  }
914
925
  async createUGCItem(ugcType, title, description, visibility, filePath) {
915
926
  const { ugcId, uploadUrl } = await this.sdk.convexClient.mutation(
@@ -989,11 +1000,11 @@ var UGCManager = class {
989
1000
 
990
1001
  // src/services/leaderboards.ts
991
1002
  import { api as api4 } from "@wvdsh/api";
992
- var LeaderboardManager = class {
1003
+ var LeaderboardManager = class extends WavedashManager {
993
1004
  constructor(sdk) {
1005
+ super(sdk);
994
1006
  // Cache leaderboards to return totalEntries synchronously without a network call
995
1007
  this.leaderboardCache = /* @__PURE__ */ new Map();
996
- this.sdk = sdk;
997
1008
  }
998
1009
  async getLeaderboard(name) {
999
1010
  const leaderboard = await this.sdk.convexClient.query(
@@ -1086,8 +1097,9 @@ var DEFAULT_P2P_CONFIG = {
1086
1097
  messageSize: 2048,
1087
1098
  maxIncomingMessages: 1024
1088
1099
  };
1089
- var _P2PManager = class _P2PManager {
1100
+ var _P2PManager = class _P2PManager extends WavedashManager {
1090
1101
  constructor(sdk) {
1102
+ super(sdk);
1091
1103
  this.currentConnection = null;
1092
1104
  // WebRTC connection state
1093
1105
  this.peerConnections = /* @__PURE__ */ new Map();
@@ -1171,9 +1183,11 @@ var _P2PManager = class _P2PManager {
1171
1183
  this.textEncoder = new TextEncoder();
1172
1184
  this.textDecoder = new TextDecoder();
1173
1185
  this.initialized = false;
1174
- this.sdk = sdk;
1175
1186
  this.config = { ...DEFAULT_P2P_CONFIG };
1176
1187
  }
1188
+ destroy() {
1189
+ this.disconnectP2P();
1190
+ }
1177
1191
  ensureInitialized() {
1178
1192
  if (!this.initialized) {
1179
1193
  this.init();
@@ -2403,8 +2417,10 @@ var P2PManager = _P2PManager;
2403
2417
  import { api as api6 } from "@wvdsh/api";
2404
2418
  import debounce2 from "lodash.debounce";
2405
2419
  var STORE_DEBOUNCE_MS = 1e3;
2406
- var StatsManager = class {
2420
+ var PERIODIC_PERSIST_MS = 1e4;
2421
+ var StatsManager = class extends WavedashManager {
2407
2422
  constructor(sdk) {
2423
+ super(sdk);
2408
2424
  // Current user values
2409
2425
  this.stats = /* @__PURE__ */ new Map();
2410
2426
  this.unlockedAchievements = /* @__PURE__ */ new Set();
@@ -2419,6 +2435,8 @@ var StatsManager = class {
2419
2435
  this.loaded = { stats: false, achievements: false };
2420
2436
  // Subscription cleanup
2421
2437
  this.subscriptions = [];
2438
+ // Background flush timer — see PERIODIC_PERSIST_MS
2439
+ this.periodicPersistInterval = null;
2422
2440
  // ================
2423
2441
  // Store / Persist
2424
2442
  // ================
@@ -2429,14 +2447,20 @@ var StatsManager = class {
2429
2447
  leading: true,
2430
2448
  trailing: true
2431
2449
  });
2432
- this.sdk = sdk;
2433
2450
  this.subscribe();
2434
2451
  this.requestStats().catch((error) => {
2435
2452
  this.sdk.logger.error("Initial stats fetch failed:", error);
2436
2453
  });
2454
+ this.periodicPersistInterval = setInterval(() => {
2455
+ void this.persist();
2456
+ }, PERIODIC_PERSIST_MS);
2437
2457
  }
2438
2458
  destroy() {
2439
2459
  this.debouncedPersist.cancel();
2460
+ if (this.periodicPersistInterval !== null) {
2461
+ clearInterval(this.periodicPersistInterval);
2462
+ this.periodicPersistInterval = null;
2463
+ }
2440
2464
  for (const unsub of this.subscriptions) unsub();
2441
2465
  this.subscriptions = [];
2442
2466
  }
@@ -2590,8 +2614,9 @@ import {
2590
2614
  HEARTBEAT,
2591
2615
  IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE2
2592
2616
  } from "@wvdsh/api";
2593
- var HeartbeatManager = class {
2617
+ var HeartbeatManager = class extends WavedashManager {
2594
2618
  constructor(sdk) {
2619
+ super(sdk);
2595
2620
  this.deviceFingerprint = void 0;
2596
2621
  this.testConnectionInterval = null;
2597
2622
  this.heartbeatInterval = null;
@@ -2610,7 +2635,6 @@ var HeartbeatManager = class {
2610
2635
  this.stop();
2611
2636
  }
2612
2637
  };
2613
- this.sdk = sdk;
2614
2638
  this.isConnected = this.sdk.convexClient.client.connectionState().isWebSocketConnected;
2615
2639
  document.addEventListener("visibilitychange", this.handleVisibilityChange);
2616
2640
  this.deviceFingerprintReady = this.sdk.iframeMessenger.requestFromParent(IFRAME_MESSAGE_TYPE2.GET_DEVICE_FINGERPRINT).then((fingerprint) => {
@@ -2754,10 +2778,10 @@ var HeartbeatManager = class {
2754
2778
  };
2755
2779
 
2756
2780
  // src/services/gameEvents.ts
2757
- var GameEventManager = class {
2781
+ var GameEventManager = class extends WavedashManager {
2758
2782
  constructor(sdk) {
2783
+ super(sdk);
2759
2784
  this.eventQueue = [];
2760
- this.sdk = sdk;
2761
2785
  }
2762
2786
  // ==============================
2763
2787
  // JS -> Game Event Broadcasting
@@ -2797,11 +2821,11 @@ var GameEventManager = class {
2797
2821
 
2798
2822
  // src/services/fullscreen.ts
2799
2823
  import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE3 } from "@wvdsh/api";
2800
- var FullscreenManager = class {
2824
+ var FullscreenManager = class extends WavedashManager {
2801
2825
  constructor(sdk) {
2826
+ super(sdk);
2802
2827
  this._isFullscreen = false;
2803
2828
  this.listeners = /* @__PURE__ */ new Set();
2804
- this.sdk = sdk;
2805
2829
  this.sdk.iframeMessenger.addEventListener(
2806
2830
  IFRAME_MESSAGE_TYPE3.FULLSCREEN_CHANGED,
2807
2831
  (data) => {
@@ -2886,15 +2910,15 @@ var FullscreenManager = class {
2886
2910
 
2887
2911
  // src/services/overlay.ts
2888
2912
  import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE4 } from "@wvdsh/api";
2889
- var OverlayManager = class {
2913
+ var OverlayManager = class extends WavedashManager {
2890
2914
  constructor(sdk) {
2915
+ super(sdk);
2891
2916
  this.handleKeyDown = (event) => {
2892
2917
  if (event.key === "Tab" && event.shiftKey) {
2893
2918
  event.preventDefault();
2894
2919
  this.toggleOverlay();
2895
2920
  }
2896
2921
  };
2897
- this.sdk = sdk;
2898
2922
  this.sdk.iframeMessenger.addEventListener(
2899
2923
  IFRAME_MESSAGE_TYPE4.TAKE_FOCUS,
2900
2924
  () => this.takeFocus()
@@ -2938,10 +2962,10 @@ function getCdnImageUrl(r2Key, host, options) {
2938
2962
  }
2939
2963
 
2940
2964
  // src/services/friends.ts
2941
- var FriendsManager = class {
2965
+ var FriendsManager = class extends WavedashManager {
2942
2966
  constructor(sdk) {
2967
+ super(sdk);
2943
2968
  this.userCache = /* @__PURE__ */ new Map();
2944
- this.sdk = sdk;
2945
2969
  }
2946
2970
  /**
2947
2971
  * Cache users from any source (friends, lobby users)
@@ -3240,7 +3264,7 @@ var WavedashSDK = class extends EventTarget {
3240
3264
  super();
3241
3265
  this._initialized = false;
3242
3266
  this._eventsReady = false;
3243
- this.sessionEndSent = false;
3267
+ this.destroyed = false;
3244
3268
  this.gameFinishedLoading = false;
3245
3269
  // Expose constants for easy access `Wavedash.LobbyVisibility.PUBLIC` etc.
3246
3270
  this.Events = WavedashEvents;
@@ -3272,7 +3296,6 @@ var WavedashSDK = class extends EventTarget {
3272
3296
  this.convexClient.setAuth(
3273
3297
  ({ forceRefreshToken }) => this.getAuthToken(forceRefreshToken)
3274
3298
  );
3275
- this.convexHttpUrl = sdkConfig.convexHttpUrl;
3276
3299
  this.wavedashUser = sdkConfig.wavedashUser;
3277
3300
  this.iframeMessenger = iframeMessenger;
3278
3301
  this.ugcHost = sdkConfig.ugcHost;
@@ -3289,6 +3312,19 @@ var WavedashSDK = class extends EventTarget {
3289
3312
  this.gameEventManager = new GameEventManager(this);
3290
3313
  this.fullscreenManager = new FullscreenManager(this);
3291
3314
  this.overlayManager = new OverlayManager(this);
3315
+ this.managers = [
3316
+ this.p2pManager,
3317
+ this.lobbyManager,
3318
+ this.statsManager,
3319
+ this.heartbeatManager,
3320
+ this.fileSystemManager,
3321
+ this.ugcManager,
3322
+ this.leaderboardManager,
3323
+ this.friendsManager,
3324
+ this.gameEventManager,
3325
+ this.fullscreenManager,
3326
+ this.overlayManager
3327
+ ];
3292
3328
  this.friendsManager.cacheUsers([
3293
3329
  {
3294
3330
  userId: this.wavedashUser.id,
@@ -4195,6 +4231,9 @@ var WavedashSDK = class extends EventTarget {
4195
4231
  throw new Error(`Failed to refresh gameplay token: ${response.status}`);
4196
4232
  }
4197
4233
  this.gameplayJwt = await response.text();
4234
+ iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE5.GAMEPLAY_JWT_READY, {
4235
+ gameplayJwt: this.gameplayJwt
4236
+ });
4198
4237
  return this.gameplayJwt;
4199
4238
  })().finally(() => {
4200
4239
  if (this.gameplayJwtPromise === promise) {
@@ -4213,47 +4252,22 @@ var WavedashSDK = class extends EventTarget {
4213
4252
  return this.getAuthToken();
4214
4253
  }
4215
4254
  /**
4216
- * Set up listeners that flush the end-of-session request when the iframe
4217
- * is going away. We listen for three signals:
4218
- * - `beforeunload` / `pagehide` on our own window: covers tab close, hard
4219
- * reload, and top-level navigation of the parent.
4220
- * - `END_SESSION` postMessage from the parent: covers parent SPA navigation
4255
+ * 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.
4221
4259
  */
4260
+ destroy() {
4261
+ if (this.destroyed) return;
4262
+ this.destroyed = true;
4263
+ for (const manager of this.managers) {
4264
+ manager.destroy();
4265
+ }
4266
+ }
4222
4267
  setupSessionEndListeners() {
4223
- const endSessionEndpoint = `${this.convexHttpUrl}/gameplay/end-session`;
4224
- const endGameplaySession = () => {
4225
- if (this.sessionEndSent) return;
4226
- if (!this.gameplayJwt) return;
4227
- this.sessionEndSent = true;
4228
- const pendingData = this.statsManager.getPendingData();
4229
- this.lobbyManager.destroy();
4230
- this.heartbeatManager.destroy();
4231
- this.statsManager.destroy();
4232
- const body = {
4233
- gameplayJwt: this.gameplayJwt
4234
- };
4235
- if (pendingData?.stats?.length) {
4236
- body.stats = pendingData.stats;
4237
- }
4238
- if (pendingData?.achievements?.length) {
4239
- body.achievements = pendingData.achievements;
4240
- }
4241
- const payload = JSON.stringify(body);
4242
- const beaconSent = navigator?.sendBeacon?.(endSessionEndpoint, payload);
4243
- if (!beaconSent) {
4244
- fetch(endSessionEndpoint, {
4245
- method: "POST",
4246
- body: payload,
4247
- keepalive: true
4248
- }).catch(() => {
4249
- });
4250
- }
4251
- };
4252
- window.addEventListener("beforeunload", endGameplaySession);
4253
- window.addEventListener("pagehide", endGameplaySession);
4254
4268
  iframeMessenger.addEventListener(
4255
4269
  IFRAME_MESSAGE_TYPE5.END_SESSION,
4256
- endGameplaySession
4270
+ () => this.destroy()
4257
4271
  );
4258
4272
  }
4259
4273
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wvdsh/sdk-js",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "type": "module",
5
5
  "description": "Wavedash JavaScript SDK",
6
6
  "main": "./dist/client.js",
@@ -49,7 +49,7 @@
49
49
  "typescript-eslint": "^8.52.0"
50
50
  },
51
51
  "dependencies": {
52
- "@wvdsh/api": "^0.1.10",
52
+ "@wvdsh/api": "^0.1.11",
53
53
  "convex": "^1.34.0",
54
54
  "lodash.debounce": "^4.0.8"
55
55
  }