@wvdsh/sdk-js 1.3.12 → 1.3.14

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/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { WavedashSDK } from './index.js';
2
- export { BackendConnectionPayload, EngineInstance, Friend, FullscreenChangedPayload, Leaderboard, LeaderboardDisplayType, LeaderboardEntries, LeaderboardSortOrder, ListUGCItemsArgs, Lobby, LobbyDataUpdatedPayload, LobbyInvite, LobbyInvitePayload, LobbyJoinResponse, LobbyJoinedPayload, LobbyKickedPayload, LobbyKickedReason, LobbyMessage, LobbyMessagePayload, LobbyUser, LobbyUserChangeType, LobbyUsersUpdatedPayload, LobbyVisibility, P2PConfig, P2PConnection, P2PConnectionEstablishedPayload, P2PConnectionFailedPayload, P2PMessage, P2PPacketDropReason, P2PPacketDroppedPayload, P2PPeer, P2PPeerDisconnectedPayload, P2PPeerReconnectedPayload, P2PPeerReconnectingPayload, PaginatedUGCItems, RemoteFileMetadata, StatsStoredPayload, UGCItem, UGCType, UGCVisibility, UpdateUGCItemArgs, UpsertedLeaderboardEntry, WavedashConfig, WavedashEvent, WavedashEventMap, WavedashResponse } from './index.js';
2
+ export { BackendConnectionPayload, EngineInstance, Friend, FullscreenChangedPayload, Leaderboard, LeaderboardDisplayType, LeaderboardEntries, LeaderboardSortOrder, ListUGCItemsArgs, Lobby, LobbyDataUpdatedPayload, LobbyInvite, LobbyInvitePayload, LobbyJoinResponse, LobbyJoinedPayload, LobbyKickedPayload, LobbyKickedReason, LobbyMessage, LobbyMessagePayload, LobbyUser, LobbyUserChangeType, LobbyUsersUpdatedPayload, LobbyVisibility, MuteChangedPayload, P2PConfig, P2PConnection, P2PConnectionEstablishedPayload, P2PConnectionFailedPayload, P2PMessage, P2PPacketDropReason, P2PPacketDroppedPayload, P2PPeer, P2PPeerDisconnectedPayload, P2PPeerReconnectedPayload, P2PPeerReconnectingPayload, PaginatedUGCItems, RemoteFileMetadata, StatsStoredPayload, UGCItem, UGCType, UGCVisibility, UpdateUGCItemArgs, UpsertedLeaderboardEntry, WavedashConfig, WavedashEvent, WavedashEventMap, WavedashResponse } from './index.js';
3
3
  export { GameLaunchParams } from '@wvdsh/api';
4
4
  export { GenericId as Id } from 'convex/values';
5
5
  import 'convex/browser';
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ declare const WavedashEvents: {
31
31
  readonly BACKEND_DISCONNECTED: "BackendDisconnected";
32
32
  readonly BACKEND_RECONNECTING: "BackendReconnecting";
33
33
  readonly FULLSCREEN_CHANGED: "FullscreenChanged";
34
+ readonly MUTE_CHANGED: "MuteChanged";
34
35
  };
35
36
 
36
37
  /** Reasons why a user was kicked from a lobby */
@@ -214,6 +215,10 @@ interface BackendConnectionPayload {
214
215
  interface FullscreenChangedPayload {
215
216
  isFullscreen: boolean;
216
217
  }
218
+ /** Payload for MuteChanged event - emitted when mute state flips */
219
+ interface MuteChangedPayload {
220
+ isMuted: boolean;
221
+ }
217
222
  type WavedashEventMap = {
218
223
  [WavedashEvents.LOBBY_MESSAGE]: LobbyMessagePayload;
219
224
  [WavedashEvents.LOBBY_JOINED]: LobbyJoinedPayload;
@@ -232,6 +237,7 @@ type WavedashEventMap = {
232
237
  [WavedashEvents.BACKEND_DISCONNECTED]: BackendConnectionPayload;
233
238
  [WavedashEvents.BACKEND_RECONNECTING]: BackendConnectionPayload;
234
239
  [WavedashEvents.FULLSCREEN_CHANGED]: FullscreenChangedPayload;
240
+ [WavedashEvents.MUTE_CHANGED]: MuteChangedPayload;
235
241
  };
236
242
  interface P2PPeer {
237
243
  userId: GenericId<"users">;
@@ -627,6 +633,7 @@ declare class HeartbeatManager extends WavedashManager {
627
633
  private isFirstTick;
628
634
  private readonly TEST_CONNECTION_INTERVAL_MS;
629
635
  private readonly DISCONNECTED_TIMEOUT_MS;
636
+ private cachedPresenceData;
630
637
  constructor(sdk: WavedashSDK);
631
638
  /** Start heartbeat and connection-check intervals */
632
639
  start(): void;
@@ -638,11 +645,11 @@ declare class HeartbeatManager extends WavedashManager {
638
645
  private tickHeartbeat;
639
646
  private sendHeartbeat;
640
647
  /**
641
- * Updates user presence in the backend
648
+ * Updates user presence in the backend.
642
649
  * @param data - Data to send to the backend
643
650
  * @returns true if the presence was updated successfully
644
651
  */
645
- updateUserPresence(data?: Record<string, string | number | boolean | null>): Promise<boolean>;
652
+ updateUserPresence(data: Record<string, string | number | boolean | null>): Promise<boolean>;
646
653
  /**
647
654
  * Tests the connection to the backend
648
655
  */
@@ -713,6 +720,47 @@ declare class OverlayManager extends WavedashManager {
713
720
  private handleKeyDown;
714
721
  }
715
722
 
723
+ /**
724
+ * AudioManager
725
+ *
726
+ * Mutes & unmutes the game in response to MUTE_CHANGED iframe messages, without
727
+ * the game needing to know anything about it.
728
+ *
729
+ * Web Audio: subclass `AudioContext` so `ctx.destination` resolves to a master
730
+ * GainNode that we control. The master gain wires to the real native destination,
731
+ * so `node.connect(ctx.destination)` and any other game code is unaffected.
732
+ *
733
+ * HTML Media (`<audio>`/`<video>`): override `HTMLMediaElement.prototype.muted`
734
+ * to record the game's intended state, but write `true` to the underlying element
735
+ * whenever the SDK is muted. Tracked elements come from three sources:
736
+ * 1. Pre-existing DOM media (`querySelectorAll`)
737
+ * 2. `new Audio()` constructor shim (covers detached SFX)
738
+ * 3. MutationObserver for any media added to the DOM later (covers innerHTML,
739
+ * framework rendering, createElement + append, etc.)
740
+ */
741
+ declare class AudioManager extends WavedashManager {
742
+ private _isMuted;
743
+ private contexts;
744
+ private elements;
745
+ private intendedMuted;
746
+ private originalAudioContext;
747
+ private originalWebKitAudioContext;
748
+ private originalAudio;
749
+ private originalMutedDescriptor;
750
+ private mutationObserver;
751
+ constructor(sdk: WavedashSDK);
752
+ isMuted(): boolean;
753
+ private handleMute;
754
+ /**
755
+ * Track a media element and (if SDK is currently muted) silence it.
756
+ * Idempotent — safe to call multiple times for the same element.
757
+ */
758
+ private trackElement;
759
+ private installShims;
760
+ private shimAudioContextClass;
761
+ destroy(): void;
762
+ }
763
+
716
764
  /**
717
765
  * Friends service
718
766
  *
@@ -831,6 +879,7 @@ declare class WavedashSDK extends EventTarget {
831
879
  readonly BACKEND_DISCONNECTED: "BackendDisconnected";
832
880
  readonly BACKEND_RECONNECTING: "BackendReconnecting";
833
881
  readonly FULLSCREEN_CHANGED: "FullscreenChanged";
882
+ readonly MUTE_CHANGED: "MuteChanged";
834
883
  };
835
884
  LobbyVisibility: {
836
885
  readonly PUBLIC: 0;
@@ -898,6 +947,7 @@ declare class WavedashSDK extends EventTarget {
898
947
  p2pManager: P2PManager;
899
948
  fullscreenManager: FullscreenManager;
900
949
  overlayManager: OverlayManager;
950
+ audioManager: AudioManager;
901
951
  private managers;
902
952
  private gameplayJwt;
903
953
  private gameplayJwtPromise;
@@ -1162,11 +1212,16 @@ declare class WavedashSDK extends EventTarget {
1162
1212
  inviteUserToLobby(lobbyId: GenericId<"lobbies">, userId: GenericId<"users">): Promise<WavedashResponse<boolean>>;
1163
1213
  getLobbyInviteLink(copyToClipboard?: boolean): Promise<WavedashResponse<string>>;
1164
1214
  /**
1165
- * Updates rich user presence so friends can see what the player is doing in game
1166
- * @param data Game data to send to the backend
1215
+ * Updates rich user presence so friends can see what the player is doing in game.
1216
+ * Supported keys:
1217
+ * `status` — one-line activity shown as the primary line (e.g. "Traveling in a group")
1218
+ * `details` — secondary context shown beneath the status (e.g. current zone or mode)
1219
+ *
1220
+ * Pass an empty dictionary to clear all presence fields.
1221
+ * @param data Presence fields to update.
1167
1222
  * @returns true if the presence was updated successfully
1168
1223
  */
1169
- updateUserPresence(data?: Record<string, string | number | boolean | null>): Promise<WavedashResponse<boolean>>;
1224
+ updateUserPresence(data: Record<string, string | number | boolean | null>): Promise<WavedashResponse<boolean>>;
1170
1225
  private isGodot;
1171
1226
  private formatResponse;
1172
1227
  private ensureInit;
@@ -1213,4 +1268,4 @@ declare global {
1213
1268
 
1214
1269
  declare function setupWavedashSDK(): WavedashSDK;
1215
1270
 
1216
- export { type BackendConnectionPayload, type EngineInstance, type Friend, type FullscreenChangedPayload, type Leaderboard, type LeaderboardDisplayType, type LeaderboardEntries, type LeaderboardSortOrder, type ListUGCItemsArgs, type Lobby, type LobbyDataUpdatedPayload, type LobbyInvite, type LobbyInvitePayload, type LobbyJoinResponse, type LobbyJoinedPayload, type LobbyKickedPayload, type LobbyKickedReason, type LobbyMessage, type LobbyMessagePayload, type LobbyUser, type LobbyUserChangeType, type LobbyUsersUpdatedPayload, type LobbyVisibility, type P2PConfig, type P2PConnection, type P2PConnectionEstablishedPayload, type P2PConnectionFailedPayload, type P2PMessage, type P2PPacketDropReason, type P2PPacketDroppedPayload, type P2PPeer, type P2PPeerDisconnectedPayload, type P2PPeerReconnectedPayload, type P2PPeerReconnectingPayload, type PaginatedUGCItems, type RemoteFileMetadata, type StatsStoredPayload, type UGCItem, type UGCType, type UGCVisibility, type UpdateUGCItemArgs, type UpsertedLeaderboardEntry, type WavedashConfig, type WavedashEvent, type WavedashEventMap, type WavedashResponse, WavedashSDK, setupWavedashSDK };
1271
+ export { type BackendConnectionPayload, type EngineInstance, type Friend, type FullscreenChangedPayload, type Leaderboard, type LeaderboardDisplayType, type LeaderboardEntries, type LeaderboardSortOrder, type ListUGCItemsArgs, type Lobby, type LobbyDataUpdatedPayload, type LobbyInvite, type LobbyInvitePayload, type LobbyJoinResponse, type LobbyJoinedPayload, type LobbyKickedPayload, type LobbyKickedReason, type LobbyMessage, type LobbyMessagePayload, type LobbyUser, type LobbyUserChangeType, type LobbyUsersUpdatedPayload, type LobbyVisibility, type MuteChangedPayload, type P2PConfig, type P2PConnection, type P2PConnectionEstablishedPayload, type P2PConnectionFailedPayload, type P2PMessage, type P2PPacketDropReason, type P2PPacketDroppedPayload, type P2PPeer, type P2PPeerDisconnectedPayload, type P2PPeerReconnectedPayload, type P2PPeerReconnectingPayload, type PaginatedUGCItems, type RemoteFileMetadata, type StatsStoredPayload, type UGCItem, type UGCType, type UGCVisibility, type UpdateUGCItemArgs, type UpsertedLeaderboardEntry, type WavedashConfig, type WavedashEvent, type WavedashEventMap, type WavedashResponse, WavedashSDK, setupWavedashSDK };
package/dist/index.js CHANGED
@@ -75,8 +75,11 @@ var WavedashEvents = {
75
75
  BACKEND_RECONNECTING: "BackendReconnecting",
76
76
  // attempting to reconnect to backend
77
77
  // Fullscreen events
78
- FULLSCREEN_CHANGED: "FullscreenChanged"
78
+ FULLSCREEN_CHANGED: "FullscreenChanged",
79
79
  // fullscreen state changed
80
+ // Audio events
81
+ MUTE_CHANGED: "MuteChanged"
82
+ // mute state changed
80
83
  // TODO: Future events to implement
81
84
  // P2P_CONNECTION_REQUESTED: 'P2PConnectionRequested', // for now we always connect all lobby members
82
85
  };
@@ -2709,6 +2712,7 @@ var HeartbeatManager = class extends WavedashManager {
2709
2712
  this.isFirstTick = true;
2710
2713
  this.TEST_CONNECTION_INTERVAL_MS = 1e3;
2711
2714
  this.DISCONNECTED_TIMEOUT_MS = 9e4;
2715
+ this.cachedPresenceData = {};
2712
2716
  this.handleVisibilityChange = () => {
2713
2717
  if (document.visibilityState === "visible") {
2714
2718
  this.start();
@@ -2778,7 +2782,7 @@ var HeartbeatManager = class extends WavedashManager {
2778
2782
  this.heartbeatInFlight = true;
2779
2783
  this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
2780
2784
  ...reestablish ? {
2781
- data: { forceUpdate: true },
2785
+ data: this.cachedPresenceData,
2782
2786
  deviceFingerprint: this.deviceFingerprint
2783
2787
  } : {}
2784
2788
  }).then((accepted) => {
@@ -2792,15 +2796,15 @@ var HeartbeatManager = class extends WavedashManager {
2792
2796
  });
2793
2797
  }
2794
2798
  /**
2795
- * Updates user presence in the backend
2799
+ * Updates user presence in the backend.
2796
2800
  * @param data - Data to send to the backend
2797
2801
  * @returns true if the presence was updated successfully
2798
2802
  */
2799
2803
  async updateUserPresence(data) {
2800
2804
  try {
2801
- const dataToSend = data ?? { forceUpdate: true };
2805
+ this.cachedPresenceData = data;
2802
2806
  await this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
2803
- data: dataToSend,
2807
+ data,
2804
2808
  deviceFingerprint: this.deviceFingerprint
2805
2809
  });
2806
2810
  return true;
@@ -3028,6 +3032,214 @@ var OverlayManager = class extends WavedashManager {
3028
3032
  }
3029
3033
  };
3030
3034
 
3035
+ // src/services/audio.ts
3036
+ import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE5 } from "@wvdsh/api";
3037
+ var WeakRefSet = class {
3038
+ constructor() {
3039
+ this.set = /* @__PURE__ */ new Set();
3040
+ }
3041
+ add(value) {
3042
+ for (const ref of this.set) {
3043
+ if (ref.deref() === value) return;
3044
+ }
3045
+ this.set.add(new WeakRef(value));
3046
+ }
3047
+ forEach(callback) {
3048
+ for (const ref of this.set) {
3049
+ const v = ref.deref();
3050
+ if (v === void 0) this.set.delete(ref);
3051
+ else callback(v);
3052
+ }
3053
+ }
3054
+ clear() {
3055
+ this.set.clear();
3056
+ }
3057
+ };
3058
+ var AudioManager = class extends WavedashManager {
3059
+ constructor(sdk) {
3060
+ super(sdk);
3061
+ this._isMuted = false;
3062
+ // Web Audio contexts and their master gain nodes
3063
+ this.contexts = /* @__PURE__ */ new Map();
3064
+ // HTML media elements we know about + their game-intended muted state
3065
+ this.elements = new WeakRefSet();
3066
+ this.intendedMuted = /* @__PURE__ */ new WeakMap();
3067
+ // Originals (restored on destroy)
3068
+ this.originalAudioContext = null;
3069
+ this.originalWebKitAudioContext = null;
3070
+ this.originalAudio = null;
3071
+ this.originalMutedDescriptor = null;
3072
+ this.mutationObserver = null;
3073
+ this.handleMute = (data) => {
3074
+ if (this._isMuted === data.isMuted) return;
3075
+ this._isMuted = data.isMuted;
3076
+ const target = this._isMuted ? 0 : 1;
3077
+ this.contexts.forEach((gain, ctx) => {
3078
+ const now = ctx.currentTime;
3079
+ gain.gain.cancelScheduledValues(now);
3080
+ gain.gain.setValueAtTime(gain.gain.value, now);
3081
+ gain.gain.linearRampToValueAtTime(target, now + 0.05);
3082
+ });
3083
+ const setMutedNative = this.originalMutedDescriptor?.set;
3084
+ if (setMutedNative) {
3085
+ this.elements.forEach((el) => {
3086
+ const intended = this.intendedMuted.get(el) ?? false;
3087
+ setMutedNative.call(el, this._isMuted ? true : intended);
3088
+ });
3089
+ }
3090
+ this.sdk.gameEventManager.notifyGame(
3091
+ WavedashEvents.MUTE_CHANGED,
3092
+ { isMuted: this._isMuted }
3093
+ );
3094
+ };
3095
+ this.installShims();
3096
+ this.sdk.iframeMessenger.addEventListener(
3097
+ IFRAME_MESSAGE_TYPE5.MUTE_CHANGED,
3098
+ this.handleMute
3099
+ );
3100
+ }
3101
+ isMuted() {
3102
+ return this._isMuted;
3103
+ }
3104
+ /**
3105
+ * Track a media element and (if SDK is currently muted) silence it.
3106
+ * Idempotent — safe to call multiple times for the same element.
3107
+ */
3108
+ trackElement(el) {
3109
+ if (this.intendedMuted.has(el)) return;
3110
+ const getMuted = this.originalMutedDescriptor?.get;
3111
+ const setMuted = this.originalMutedDescriptor?.set;
3112
+ const current = getMuted ? getMuted.call(el) : el.muted;
3113
+ this.intendedMuted.set(el, current);
3114
+ this.elements.add(el);
3115
+ if (this._isMuted && !current && setMuted) {
3116
+ setMuted.call(el, true);
3117
+ }
3118
+ }
3119
+ installShims() {
3120
+ if (typeof window === "undefined") return;
3121
+ if (window.AudioContext) {
3122
+ this.originalAudioContext = window.AudioContext;
3123
+ window.AudioContext = this.shimAudioContextClass(window.AudioContext);
3124
+ }
3125
+ const win = window;
3126
+ if (win.webkitAudioContext) {
3127
+ this.originalWebKitAudioContext = win.webkitAudioContext;
3128
+ win.webkitAudioContext = this.shimAudioContextClass(win.webkitAudioContext);
3129
+ }
3130
+ if (window.Audio) {
3131
+ const OriginalAudio = window.Audio;
3132
+ this.originalAudio = OriginalAudio;
3133
+ ((manager) => {
3134
+ const Shimmed = function(src) {
3135
+ const audio = new OriginalAudio(src);
3136
+ manager.trackElement(audio);
3137
+ return audio;
3138
+ };
3139
+ Shimmed.prototype = OriginalAudio.prototype;
3140
+ window.Audio = Shimmed;
3141
+ })(this);
3142
+ }
3143
+ if (typeof document !== "undefined") {
3144
+ document.querySelectorAll("audio, video").forEach((el) => {
3145
+ this.trackElement(el);
3146
+ });
3147
+ this.mutationObserver = new MutationObserver((mutations) => {
3148
+ for (const m of mutations) {
3149
+ m.addedNodes.forEach((node) => {
3150
+ if (node instanceof HTMLMediaElement) {
3151
+ this.trackElement(node);
3152
+ } else if (node instanceof HTMLElement) {
3153
+ node.querySelectorAll("audio, video").forEach((el) => {
3154
+ this.trackElement(el);
3155
+ });
3156
+ }
3157
+ });
3158
+ }
3159
+ });
3160
+ this.mutationObserver.observe(document.documentElement, {
3161
+ childList: true,
3162
+ subtree: true
3163
+ });
3164
+ }
3165
+ this.originalMutedDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, "muted") ?? null;
3166
+ const original = this.originalMutedDescriptor;
3167
+ if (original?.get && original?.set) {
3168
+ ((manager) => {
3169
+ Object.defineProperty(HTMLMediaElement.prototype, "muted", {
3170
+ configurable: true,
3171
+ get() {
3172
+ const intended = manager.intendedMuted.get(this);
3173
+ return intended !== void 0 ? intended : original.get.call(this);
3174
+ },
3175
+ set(value) {
3176
+ manager.intendedMuted.set(this, value);
3177
+ manager.elements.add(this);
3178
+ original.set.call(this, manager._isMuted ? true : value);
3179
+ }
3180
+ });
3181
+ })(this);
3182
+ }
3183
+ }
3184
+ shimAudioContextClass(Original) {
3185
+ return /* @__PURE__ */ ((manager) => class extends Original {
3186
+ constructor(opts) {
3187
+ super(opts);
3188
+ const masterGain = this.createGain();
3189
+ masterGain.connect(this.destination);
3190
+ masterGain.gain.setValueAtTime(
3191
+ manager._isMuted ? 0 : 1,
3192
+ this.currentTime
3193
+ );
3194
+ Object.defineProperty(this, "destination", {
3195
+ configurable: true,
3196
+ get() {
3197
+ return masterGain;
3198
+ }
3199
+ });
3200
+ manager.contexts.set(this, masterGain);
3201
+ }
3202
+ close() {
3203
+ manager.contexts.delete(this);
3204
+ return super.close();
3205
+ }
3206
+ })(this);
3207
+ }
3208
+ destroy() {
3209
+ this.sdk.iframeMessenger.removeEventListener(
3210
+ IFRAME_MESSAGE_TYPE5.MUTE_CHANGED,
3211
+ this.handleMute
3212
+ );
3213
+ if (this.mutationObserver) {
3214
+ this.mutationObserver.disconnect();
3215
+ this.mutationObserver = null;
3216
+ }
3217
+ if (typeof window !== "undefined") {
3218
+ if (this.originalAudioContext) {
3219
+ window.AudioContext = this.originalAudioContext;
3220
+ }
3221
+ const win = window;
3222
+ if (this.originalWebKitAudioContext && win.webkitAudioContext) {
3223
+ win.webkitAudioContext = this.originalWebKitAudioContext;
3224
+ }
3225
+ if (this.originalAudio) {
3226
+ window.Audio = this.originalAudio;
3227
+ }
3228
+ }
3229
+ if (this.originalMutedDescriptor) {
3230
+ Object.defineProperty(
3231
+ HTMLMediaElement.prototype,
3232
+ "muted",
3233
+ this.originalMutedDescriptor
3234
+ );
3235
+ }
3236
+ this.contexts.clear();
3237
+ this.elements.clear();
3238
+ this.intendedMuted = /* @__PURE__ */ new WeakMap();
3239
+ super.destroy();
3240
+ }
3241
+ };
3242
+
3031
3243
  // src/services/friends.ts
3032
3244
  import { api as api8 } from "@wvdsh/api";
3033
3245
 
@@ -3289,7 +3501,7 @@ var SwMessenger = class {
3289
3501
  };
3290
3502
 
3291
3503
  // src/index.ts
3292
- import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE5, UrlParams } from "@wvdsh/api";
3504
+ import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE6, UrlParams } from "@wvdsh/api";
3293
3505
 
3294
3506
  // src/utils/validation.ts
3295
3507
  var CONVEX_ID_REGEX = /^[0-9a-z]{31,37}$/;
@@ -3333,6 +3545,13 @@ var vRecord = (value, path) => {
3333
3545
  `${path}: expected plain object, got ${describeValue(value)}`
3334
3546
  );
3335
3547
  }
3548
+ for (const [key, val] of Object.entries(value)) {
3549
+ if (typeof val === "object" && val !== null) {
3550
+ throw new Error(
3551
+ `${path}: expected flat record with no nested objects, but key "${key}" contains ${describeValue(val)}`
3552
+ );
3553
+ }
3554
+ }
3336
3555
  return value;
3337
3556
  };
3338
3557
  function vId(tableName) {
@@ -3377,7 +3596,12 @@ function vUnion(...variants) {
3377
3596
  }
3378
3597
  function vObject(shape) {
3379
3598
  return (value, path) => {
3380
- const obj = vRecord(value, path);
3599
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3600
+ throw new Error(
3601
+ `${path}: expected plain object, got ${describeValue(value)}`
3602
+ );
3603
+ }
3604
+ const obj = value;
3381
3605
  for (const key of Object.keys(obj)) {
3382
3606
  if (!(key in shape)) {
3383
3607
  throw new Error(`${path}: unrecognized property "${key}"`);
@@ -3468,6 +3692,7 @@ var WavedashSDK = class extends EventTarget {
3468
3692
  this.gameEventManager = new GameEventManager(this);
3469
3693
  this.fullscreenManager = new FullscreenManager(this);
3470
3694
  this.overlayManager = new OverlayManager(this);
3695
+ this.audioManager = new AudioManager(this);
3471
3696
  this.managers = [
3472
3697
  this.p2pManager,
3473
3698
  this.lobbyManager,
@@ -3479,7 +3704,8 @@ var WavedashSDK = class extends EventTarget {
3479
3704
  this.friendsManager,
3480
3705
  this.gameEventManager,
3481
3706
  this.fullscreenManager,
3482
- this.overlayManager
3707
+ this.overlayManager,
3708
+ this.audioManager
3483
3709
  ];
3484
3710
  this.friendsManager.cacheUsers([
3485
3711
  {
@@ -3611,7 +3837,7 @@ var WavedashSDK = class extends EventTarget {
3611
3837
  [progress]
3612
3838
  );
3613
3839
  this.clearSetupWarning();
3614
- iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE5.PROGRESS_UPDATE, {
3840
+ iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE6.PROGRESS_UPDATE, {
3615
3841
  progress
3616
3842
  });
3617
3843
  }
@@ -3620,7 +3846,7 @@ var WavedashSDK = class extends EventTarget {
3620
3846
  if (this.gameFinishedLoading) return;
3621
3847
  this.gameFinishedLoading = true;
3622
3848
  this.heartbeatManager.start();
3623
- iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE5.LOADING_COMPLETE, {});
3849
+ iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE6.LOADING_COMPLETE, {});
3624
3850
  this.overlayManager.takeFocus();
3625
3851
  }
3626
3852
  get gameLoaded() {
@@ -4368,15 +4594,34 @@ var WavedashSDK = class extends EventTarget {
4368
4594
  // User Presence
4369
4595
  // ==============================
4370
4596
  /**
4371
- * Updates rich user presence so friends can see what the player is doing in game
4372
- * @param data Game data to send to the backend
4597
+ * Updates rich user presence so friends can see what the player is doing in game.
4598
+ * Supported keys:
4599
+ * `status` — one-line activity shown as the primary line (e.g. "Traveling in a group")
4600
+ * `details` — secondary context shown beneath the status (e.g. current zone or mode)
4601
+ *
4602
+ * Pass an empty dictionary to clear all presence fields.
4603
+ * @param data Presence fields to update.
4373
4604
  * @returns true if the presence was updated successfully
4374
4605
  */
4375
4606
  async updateUserPresence(data) {
4607
+ if (typeof data === "string") {
4608
+ const raw = data;
4609
+ try {
4610
+ data = JSON.parse(raw);
4611
+ } catch (error) {
4612
+ const message = `updateUserPresence: invalid JSON: ${raw}`;
4613
+ logger.error(message, error);
4614
+ return this.formatResponse({
4615
+ success: false,
4616
+ data: null,
4617
+ message
4618
+ });
4619
+ }
4620
+ }
4376
4621
  return this.apiCall(
4377
4622
  this.heartbeatManager,
4378
4623
  "updateUserPresence",
4379
- [["data", vOptional(vRecord)]],
4624
+ [["data", vRecord]],
4380
4625
  data
4381
4626
  );
4382
4627
  }
@@ -4467,7 +4712,7 @@ var WavedashSDK = class extends EventTarget {
4467
4712
  throw new Error(`Failed to refresh gameplay token: ${response.status}`);
4468
4713
  }
4469
4714
  this.gameplayJwt = await response.text();
4470
- iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE5.GAMEPLAY_JWT_READY, {
4715
+ iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE6.GAMEPLAY_JWT_READY, {
4471
4716
  gameplayJwt: this.gameplayJwt
4472
4717
  });
4473
4718
  this.swMessenger.postToServiceWorker({
@@ -4503,7 +4748,7 @@ var WavedashSDK = class extends EventTarget {
4503
4748
  }
4504
4749
  setupSessionEndListeners() {
4505
4750
  iframeMessenger.addEventListener(
4506
- IFRAME_MESSAGE_TYPE5.END_SESSION,
4751
+ IFRAME_MESSAGE_TYPE6.END_SESSION,
4507
4752
  () => this.destroy()
4508
4753
  );
4509
4754
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wvdsh/sdk-js",
3
- "version": "1.3.12",
3
+ "version": "1.3.14",
4
4
  "type": "module",
5
5
  "description": "Wavedash JavaScript SDK",
6
6
  "main": "./dist/client.js",
@@ -49,8 +49,8 @@
49
49
  "typescript-eslint": "^8.52.0"
50
50
  },
51
51
  "dependencies": {
52
- "@wvdsh/api": "^0.1.27",
53
- "convex": "^1.38.0",
52
+ "@wvdsh/api": "^0.1.28",
53
+ "convex": "^1.39.1",
54
54
  "lodash.throttle": "^4.1.1"
55
55
  }
56
56
  }