@wvdsh/sdk-js 1.3.14 → 1.3.16

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
@@ -91,6 +91,8 @@ type UpsertedLeaderboardEntry = FunctionReturnType<typeof api.sdk.leaderboards.u
91
91
  userId: GenericId<"users">;
92
92
  username: string;
93
93
  userAvatarUrl?: string;
94
+ submittedScore: number;
95
+ submittedRank: number;
94
96
  };
95
97
  type WavedashEvent = (typeof WavedashEvents)[keyof typeof WavedashEvents];
96
98
  interface WavedashConfig {
@@ -625,36 +627,53 @@ declare class HeartbeatManager extends WavedashManager {
625
627
  private deviceFingerprintReady;
626
628
  private testConnectionInterval;
627
629
  private heartbeatInterval;
630
+ private gamepadPollInterval;
631
+ private inactivityTimeout;
628
632
  private isConnected;
629
633
  private sentDisconnectedEvent;
630
634
  private disconnectedAt;
631
635
  private lastHeartbeatTime;
636
+ private lastInputResetAt;
632
637
  private heartbeatInFlight;
633
638
  private isFirstTick;
634
639
  private readonly TEST_CONNECTION_INTERVAL_MS;
635
640
  private readonly DISCONNECTED_TIMEOUT_MS;
641
+ private readonly INACTIVITY_TIMEOUT_MS;
642
+ private readonly INPUT_THROTTLE_MS;
643
+ private readonly GAMEPAD_POLL_INTERVAL_MS;
644
+ private readonly GAMEPAD_AXIS_DEADZONE;
636
645
  private cachedPresenceData;
637
646
  constructor(sdk: WavedashSDK);
638
- /** Start heartbeat and connection-check intervals */
647
+ /**
648
+ * Start (or refresh) the heartbeat. Idempotent: if intervals are already
649
+ * running this just reschedules the inactivity timer. No-op if the game
650
+ * hasn't loaded yet or the tab is hidden.
651
+ */
639
652
  start(): void;
640
- /** Stop heartbeat and connection-check intervals */
653
+ /** Stop the heartbeat and clear the inactivity timer. Idempotent. */
641
654
  stop(): void;
642
- /** Full teardown — stops intervals and removes all listeners */
643
- destroy(): void;
644
- private handleVisibilityChange;
645
- private tickHeartbeat;
646
- private sendHeartbeat;
647
655
  /**
648
656
  * Updates user presence in the backend.
649
657
  * @param data - Data to send to the backend
650
658
  * @returns true if the presence was updated successfully
651
659
  */
652
660
  updateUserPresence(data: Record<string, string | number | boolean | null>): Promise<boolean>;
661
+ isCurrentlyConnected(): boolean;
662
+ /** Full teardown — stops intervals and removes all listeners */
663
+ destroy(): void;
664
+ private tickHeartbeat;
665
+ private sendHeartbeat;
666
+ private handleVisibilityChange;
667
+ private handleUserInput;
668
+ /**
669
+ * Polls connected gamepads; any pressed button or out-of-deadzone axis
670
+ * counts as user activity and (re)starts the heartbeat.
671
+ */
672
+ private pollGamepads;
653
673
  /**
654
674
  * Tests the connection to the backend
655
675
  */
656
676
  private testConnection;
657
- isCurrentlyConnected(): boolean;
658
677
  }
659
678
 
660
679
  declare class GameEventManager extends WavedashManager {
@@ -750,6 +769,20 @@ declare class AudioManager extends WavedashManager {
750
769
  private mutationObserver;
751
770
  constructor(sdk: WavedashSDK);
752
771
  isMuted(): boolean;
772
+ /**
773
+ * Ask the host to mute (true) or unmute (false). Resolves to `true` if the
774
+ * host applied the change, `false` otherwise — notably, the host rejects an
775
+ * unmute when the user muted the game from the Wavedash UI, so games can't
776
+ * override an explicit user mute. The resulting state arrives via the usual
777
+ * MUTE_CHANGED broadcast, so `isMuted()` updates independently of this result.
778
+ */
779
+ requestMute(muted: boolean): Promise<boolean>;
780
+ /**
781
+ * Toggle mute. Like `requestMute`, the host may reject the unmute half of a
782
+ * toggle if the user muted from the Wavedash UI. Resolves to `true` if the
783
+ * host applied the change.
784
+ */
785
+ toggleMute(): Promise<boolean>;
753
786
  private handleMute;
754
787
  /**
755
788
  * Track a media element and (if SDK is currently muted) silence it.
@@ -1004,6 +1037,22 @@ declare class WavedashSDK extends EventTarget {
1004
1037
  * a user gesture handler when entering fullscreen.
1005
1038
  */
1006
1039
  toggleFullscreen(): Promise<boolean>;
1040
+ /**
1041
+ * Whether the game is currently muted. Mirrored from the Wavedash host page,
1042
+ * which owns the mute control so its UI button and the game stay in sync.
1043
+ */
1044
+ isMuted(): boolean;
1045
+ /**
1046
+ * Ask the host to mute (true) or unmute (false). Resolves to `true` if the
1047
+ * change was applied, `false` if it was rejected — the host won't let the
1048
+ * game unmute when the user has muted from the Wavedash UI.
1049
+ */
1050
+ requestMute(muted: boolean): Promise<boolean>;
1051
+ /**
1052
+ * Toggle mute. Resolves to `true` if the change was applied, `false` if it
1053
+ * was rejected (e.g. trying to unmute over an explicit user mute).
1054
+ */
1055
+ toggleMute(): Promise<boolean>;
1007
1056
  getUser(): SDKUser;
1008
1057
  /**
1009
1058
  * Get a username. Returns the logged in user's username if no ID is passed.
package/dist/index.js CHANGED
@@ -1172,7 +1172,12 @@ var LeaderboardManager = class extends WavedashManager {
1172
1172
  this.updateCachedTotalEntries(leaderboardId, result.totalEntries);
1173
1173
  }
1174
1174
  return {
1175
+ // Where your current leaderboard standing ranks
1175
1176
  ...result.entry,
1177
+ // Where the submission itself ranks
1178
+ submittedScore: result.submission.score,
1179
+ submittedRank: result.submission.globalRank,
1180
+ // User info
1176
1181
  userId: this.sdk.wavedashUser.id,
1177
1182
  username: this.sdk.wavedashUser.username,
1178
1183
  userAvatarUrl: this.sdk.wavedashUser.avatarUrl
@@ -2698,20 +2703,31 @@ import {
2698
2703
  HEARTBEAT,
2699
2704
  IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE2
2700
2705
  } from "@wvdsh/api";
2706
+ var INPUT_LISTENER_OPTS = {
2707
+ passive: true,
2708
+ capture: true
2709
+ };
2701
2710
  var HeartbeatManager = class extends WavedashManager {
2702
2711
  constructor(sdk) {
2703
2712
  super(sdk);
2704
2713
  this.deviceFingerprint = void 0;
2705
2714
  this.testConnectionInterval = null;
2706
2715
  this.heartbeatInterval = null;
2716
+ this.gamepadPollInterval = null;
2717
+ this.inactivityTimeout = null;
2707
2718
  this.isConnected = false;
2708
2719
  this.sentDisconnectedEvent = false;
2709
2720
  this.disconnectedAt = null;
2710
2721
  this.lastHeartbeatTime = 0;
2722
+ this.lastInputResetAt = 0;
2711
2723
  this.heartbeatInFlight = false;
2712
2724
  this.isFirstTick = true;
2713
2725
  this.TEST_CONNECTION_INTERVAL_MS = 1e3;
2714
2726
  this.DISCONNECTED_TIMEOUT_MS = 9e4;
2727
+ this.INACTIVITY_TIMEOUT_MS = 30 * 60 * 1e3;
2728
+ this.INPUT_THROTTLE_MS = 1e3;
2729
+ this.GAMEPAD_POLL_INTERVAL_MS = 1e3;
2730
+ this.GAMEPAD_AXIS_DEADZONE = 0.2;
2715
2731
  this.cachedPresenceData = {};
2716
2732
  this.handleVisibilityChange = () => {
2717
2733
  if (document.visibilityState === "visible") {
@@ -2720,19 +2736,53 @@ var HeartbeatManager = class extends WavedashManager {
2720
2736
  this.stop();
2721
2737
  }
2722
2738
  };
2739
+ this.handleUserInput = () => {
2740
+ const now = Date.now();
2741
+ if (now - this.lastInputResetAt < this.INPUT_THROTTLE_MS) return;
2742
+ this.lastInputResetAt = now;
2743
+ this.start();
2744
+ };
2723
2745
  this.isConnected = this.sdk.convexClient.client.connectionState().isWebSocketConnected;
2724
2746
  document.addEventListener("visibilitychange", this.handleVisibilityChange);
2747
+ window.addEventListener(
2748
+ "keydown",
2749
+ this.handleUserInput,
2750
+ INPUT_LISTENER_OPTS
2751
+ );
2752
+ window.addEventListener(
2753
+ "pointerdown",
2754
+ this.handleUserInput,
2755
+ INPUT_LISTENER_OPTS
2756
+ );
2757
+ window.addEventListener(
2758
+ "pointermove",
2759
+ this.handleUserInput,
2760
+ INPUT_LISTENER_OPTS
2761
+ );
2762
+ window.addEventListener("wheel", this.handleUserInput, INPUT_LISTENER_OPTS);
2763
+ this.gamepadPollInterval = setInterval(() => {
2764
+ this.pollGamepads();
2765
+ }, this.GAMEPAD_POLL_INTERVAL_MS);
2725
2766
  this.deviceFingerprintReady = this.sdk.iframeMessenger.requestFromParent(IFRAME_MESSAGE_TYPE2.GET_DEVICE_FINGERPRINT).then((fingerprint) => {
2726
2767
  this.deviceFingerprint = fingerprint;
2727
2768
  }).catch(() => {
2728
2769
  });
2729
2770
  }
2730
- /** Start heartbeat and connection-check intervals */
2771
+ /**
2772
+ * Start (or refresh) the heartbeat. Idempotent: if intervals are already
2773
+ * running this just reschedules the inactivity timer. No-op if the game
2774
+ * hasn't loaded yet or the tab is hidden.
2775
+ */
2731
2776
  start() {
2732
- if (!this.sdk.gameLoaded) {
2733
- return;
2734
- }
2735
- this.stop();
2777
+ if (!this.sdk.gameLoaded) return;
2778
+ if (document.visibilityState !== "visible") return;
2779
+ if (this.inactivityTimeout !== null) {
2780
+ clearTimeout(this.inactivityTimeout);
2781
+ }
2782
+ this.inactivityTimeout = setTimeout(() => {
2783
+ this.stop();
2784
+ }, this.INACTIVITY_TIMEOUT_MS);
2785
+ if (this.heartbeatInterval !== null) return;
2736
2786
  if (this.isFirstTick) {
2737
2787
  void this.deviceFingerprintReady.then(() => {
2738
2788
  if (!this.sdk.gameLoaded || !this.isFirstTick) return;
@@ -2748,8 +2798,12 @@ var HeartbeatManager = class extends WavedashManager {
2748
2798
  this.testConnection();
2749
2799
  }, this.TEST_CONNECTION_INTERVAL_MS);
2750
2800
  }
2751
- /** Stop heartbeat and connection-check intervals */
2801
+ /** Stop the heartbeat and clear the inactivity timer. Idempotent. */
2752
2802
  stop() {
2803
+ if (this.inactivityTimeout !== null) {
2804
+ clearTimeout(this.inactivityTimeout);
2805
+ this.inactivityTimeout = null;
2806
+ }
2753
2807
  if (this.heartbeatInterval !== null) {
2754
2808
  clearInterval(this.heartbeatInterval);
2755
2809
  this.heartbeatInterval = null;
@@ -2759,14 +2813,62 @@ var HeartbeatManager = class extends WavedashManager {
2759
2813
  this.testConnectionInterval = null;
2760
2814
  }
2761
2815
  }
2816
+ /**
2817
+ * Updates user presence in the backend.
2818
+ * @param data - Data to send to the backend
2819
+ * @returns true if the presence was updated successfully
2820
+ */
2821
+ async updateUserPresence(data) {
2822
+ try {
2823
+ this.cachedPresenceData = data;
2824
+ await this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
2825
+ data,
2826
+ deviceFingerprint: this.deviceFingerprint
2827
+ });
2828
+ return true;
2829
+ } catch (error) {
2830
+ logger.error(`Error updating presence: ${error}`);
2831
+ return false;
2832
+ }
2833
+ }
2834
+ isCurrentlyConnected() {
2835
+ return this.isConnected;
2836
+ }
2762
2837
  /** Full teardown — stops intervals and removes all listeners */
2763
2838
  destroy() {
2764
2839
  this.stop();
2840
+ if (this.gamepadPollInterval !== null) {
2841
+ clearInterval(this.gamepadPollInterval);
2842
+ this.gamepadPollInterval = null;
2843
+ }
2765
2844
  document.removeEventListener(
2766
2845
  "visibilitychange",
2767
2846
  this.handleVisibilityChange
2768
2847
  );
2848
+ window.removeEventListener(
2849
+ "keydown",
2850
+ this.handleUserInput,
2851
+ INPUT_LISTENER_OPTS
2852
+ );
2853
+ window.removeEventListener(
2854
+ "pointerdown",
2855
+ this.handleUserInput,
2856
+ INPUT_LISTENER_OPTS
2857
+ );
2858
+ window.removeEventListener(
2859
+ "pointermove",
2860
+ this.handleUserInput,
2861
+ INPUT_LISTENER_OPTS
2862
+ );
2863
+ window.removeEventListener(
2864
+ "wheel",
2865
+ this.handleUserInput,
2866
+ INPUT_LISTENER_OPTS
2867
+ );
2769
2868
  }
2869
+ // =================
2870
+ // Private functions
2871
+ // =================
2770
2872
  tickHeartbeat() {
2771
2873
  const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeatTime;
2772
2874
  const needsReestablish = this.isFirstTick || timeSinceLastHeartbeat >= HEARTBEAT.CLIENT_REESTABLISH_THRESHOLD_MS;
@@ -2796,21 +2898,22 @@ var HeartbeatManager = class extends WavedashManager {
2796
2898
  });
2797
2899
  }
2798
2900
  /**
2799
- * Updates user presence in the backend.
2800
- * @param data - Data to send to the backend
2801
- * @returns true if the presence was updated successfully
2901
+ * Polls connected gamepads; any pressed button or out-of-deadzone axis
2902
+ * counts as user activity and (re)starts the heartbeat.
2802
2903
  */
2803
- async updateUserPresence(data) {
2804
- try {
2805
- this.cachedPresenceData = data;
2806
- await this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
2807
- data,
2808
- deviceFingerprint: this.deviceFingerprint
2809
- });
2810
- return true;
2811
- } catch (error) {
2812
- logger.error(`Error updating presence: ${error}`);
2813
- return false;
2904
+ pollGamepads() {
2905
+ if (typeof navigator === "undefined" || !navigator.getGamepads) return;
2906
+ const pads = navigator.getGamepads();
2907
+ for (const pad of pads) {
2908
+ if (!pad) continue;
2909
+ if (pad.buttons.some((b) => b.pressed)) {
2910
+ this.start();
2911
+ return;
2912
+ }
2913
+ if (pad.axes.some((a) => Math.abs(a) > this.GAMEPAD_AXIS_DEADZONE)) {
2914
+ this.start();
2915
+ return;
2916
+ }
2814
2917
  }
2815
2918
  }
2816
2919
  /**
@@ -2857,9 +2960,6 @@ var HeartbeatManager = class extends WavedashManager {
2857
2960
  logger.error("Error testing connection:", error);
2858
2961
  }
2859
2962
  }
2860
- isCurrentlyConnected() {
2861
- return this.isConnected;
2862
- }
2863
2963
  };
2864
2964
 
2865
2965
  // src/services/gameEvents.ts
@@ -3101,6 +3201,31 @@ var AudioManager = class extends WavedashManager {
3101
3201
  isMuted() {
3102
3202
  return this._isMuted;
3103
3203
  }
3204
+ /**
3205
+ * Ask the host to mute (true) or unmute (false). Resolves to `true` if the
3206
+ * host applied the change, `false` otherwise — notably, the host rejects an
3207
+ * unmute when the user muted the game from the Wavedash UI, so games can't
3208
+ * override an explicit user mute. The resulting state arrives via the usual
3209
+ * MUTE_CHANGED broadcast, so `isMuted()` updates independently of this result.
3210
+ */
3211
+ async requestMute(muted) {
3212
+ const response = await this.sdk.iframeMessenger.requestFromParent(
3213
+ IFRAME_MESSAGE_TYPE5.SET_MUTE,
3214
+ { muted }
3215
+ );
3216
+ return response.success;
3217
+ }
3218
+ /**
3219
+ * Toggle mute. Like `requestMute`, the host may reject the unmute half of a
3220
+ * toggle if the user muted from the Wavedash UI. Resolves to `true` if the
3221
+ * host applied the change.
3222
+ */
3223
+ async toggleMute() {
3224
+ const response = await this.sdk.iframeMessenger.requestFromParent(
3225
+ IFRAME_MESSAGE_TYPE5.TOGGLE_MUTE
3226
+ );
3227
+ return response.success;
3228
+ }
3104
3229
  /**
3105
3230
  * Track a media element and (if SDK is currently muted) silence it.
3106
3231
  * Idempotent — safe to call multiple times for the same element.
@@ -3673,11 +3798,11 @@ var WavedashSDK = class extends EventTarget {
3673
3798
  expectAuth: true
3674
3799
  });
3675
3800
  this.gameCloudId = sdkConfig.gameCloudId;
3801
+ this.iframeMessenger = iframeMessenger;
3676
3802
  this.convexClient.setAuth(
3677
3803
  ({ forceRefreshToken }) => this.getAuthToken(forceRefreshToken)
3678
3804
  );
3679
3805
  this.wavedashUser = sdkConfig.wavedashUser;
3680
- this.iframeMessenger = iframeMessenger;
3681
3806
  this.ugcHost = sdkConfig.ugcHost;
3682
3807
  this.uploadsHost = sdkConfig.uploadsHost;
3683
3808
  this.swMessenger = new SwMessenger();
@@ -3881,6 +4006,31 @@ var WavedashSDK = class extends EventTarget {
3881
4006
  async toggleFullscreen() {
3882
4007
  return this.fullscreenManager.toggleFullscreen();
3883
4008
  }
4009
+ // =====
4010
+ // Audio
4011
+ // =====
4012
+ /**
4013
+ * Whether the game is currently muted. Mirrored from the Wavedash host page,
4014
+ * which owns the mute control so its UI button and the game stay in sync.
4015
+ */
4016
+ isMuted() {
4017
+ return this.audioManager.isMuted();
4018
+ }
4019
+ /**
4020
+ * Ask the host to mute (true) or unmute (false). Resolves to `true` if the
4021
+ * change was applied, `false` if it was rejected — the host won't let the
4022
+ * game unmute when the user has muted from the Wavedash UI.
4023
+ */
4024
+ async requestMute(muted) {
4025
+ return this.audioManager.requestMute(muted);
4026
+ }
4027
+ /**
4028
+ * Toggle mute. Resolves to `true` if the change was applied, `false` if it
4029
+ * was rejected (e.g. trying to unmute over an explicit user mute).
4030
+ */
4031
+ async toggleMute() {
4032
+ return this.audioManager.toggleMute();
4033
+ }
3884
4034
  // ============
3885
4035
  // User methods
3886
4036
  // ============
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wvdsh/sdk-js",
3
- "version": "1.3.14",
3
+ "version": "1.3.16",
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.28",
52
+ "@wvdsh/api": "^0.1.32",
53
53
  "convex": "^1.39.1",
54
54
  "lodash.throttle": "^4.1.1"
55
55
  }