@wvdsh/sdk-js 1.3.11 → 1.3.13

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, 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, RemoteFileMetadata, StatsStoredPayload, UGCType, UGCVisibility, 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, 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
@@ -1,7 +1,7 @@
1
1
  import { ConvexClient } from 'convex/browser';
2
2
  import { GenericId } from 'convex/values';
3
3
  export { GenericId as Id } from 'convex/values';
4
- import { FunctionReturnType } from 'convex/server';
4
+ import { FunctionReturnType, FunctionArgs } from 'convex/server';
5
5
  import { LOBBY_VISIBILITY, api, UGC_TYPE, UGC_VISIBILITY, LEADERBOARD_SORT_ORDER, LEADERBOARD_DISPLAY_TYPE, GAME_ENGINE, SDKUser, IFrameEventPayloadMap, IFRAME_MESSAGE_TYPE, SDKConfig, GameLaunchParams } from '@wvdsh/api';
6
6
  export { GameLaunchParams } from '@wvdsh/api';
7
7
 
@@ -71,6 +71,13 @@ type LeaderboardSortOrder = (typeof LEADERBOARD_SORT_ORDER)[keyof typeof LEADERB
71
71
  type LeaderboardDisplayType = (typeof LEADERBOARD_DISPLAY_TYPE)[keyof typeof LEADERBOARD_DISPLAY_TYPE];
72
72
  type UGCType = (typeof UGC_TYPE)[keyof typeof UGC_TYPE];
73
73
  type UGCVisibility = (typeof UGC_VISIBILITY)[keyof typeof UGC_VISIBILITY];
74
+ type UpdateUGCItemArgs = Omit<FunctionArgs<typeof api.sdk.userGeneratedContent.updateUGCItem>, "ugcId" | "createPresignedUploadUrl"> & {
75
+ filePath?: string;
76
+ };
77
+ type UGCItem = FunctionReturnType<typeof api.sdk.userGeneratedContent.listUGCItems>["page"][0];
78
+ type PaginatedUGCItems = FunctionReturnType<typeof api.sdk.userGeneratedContent.listUGCItems>;
79
+ type RawListUGCItemsArgs = FunctionArgs<typeof api.sdk.userGeneratedContent.listUGCItems>;
80
+ type ListUGCItemsArgs = Omit<RawListUGCItemsArgs, "filters"> & NonNullable<RawListUGCItemsArgs["filters"]>;
74
81
  type LobbyUser = FunctionReturnType<typeof api.sdk.gameLobby.lobbyUsers>[0];
75
82
  type LobbyMessage = FunctionReturnType<typeof api.sdk.gameLobby.lobbyMessages>[0];
76
83
  type Lobby = FunctionReturnType<typeof api.sdk.gameLobby.listAvailable>[0];
@@ -429,9 +436,10 @@ declare class FileSystemManager extends WavedashManager {
429
436
  declare class UGCManager extends WavedashManager {
430
437
  constructor(sdk: WavedashSDK);
431
438
  createUGCItem(ugcType: UGCType, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<GenericId<"userGeneratedContent">>;
432
- updateUGCItem(ugcId: GenericId<"userGeneratedContent">, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<GenericId<"userGeneratedContent">>;
439
+ updateUGCItem(ugcId: GenericId<"userGeneratedContent">, updates?: UpdateUGCItemArgs): Promise<GenericId<"userGeneratedContent">>;
433
440
  deleteUGCItem(ugcId: GenericId<"userGeneratedContent">): Promise<GenericId<"userGeneratedContent">>;
434
441
  downloadUGCItem(ugcId: GenericId<"userGeneratedContent">, filePath: string): Promise<GenericId<"userGeneratedContent">>;
442
+ listUGCItems(args?: ListUGCItemsArgs): Promise<PaginatedUGCItems>;
435
443
  }
436
444
 
437
445
  /**
@@ -619,6 +627,7 @@ declare class HeartbeatManager extends WavedashManager {
619
627
  private isFirstTick;
620
628
  private readonly TEST_CONNECTION_INTERVAL_MS;
621
629
  private readonly DISCONNECTED_TIMEOUT_MS;
630
+ private cachedPresenceData;
622
631
  constructor(sdk: WavedashSDK);
623
632
  /** Start heartbeat and connection-check intervals */
624
633
  start(): void;
@@ -630,11 +639,11 @@ declare class HeartbeatManager extends WavedashManager {
630
639
  private tickHeartbeat;
631
640
  private sendHeartbeat;
632
641
  /**
633
- * Updates user presence in the backend
642
+ * Updates user presence in the backend.
634
643
  * @param data - Data to send to the backend
635
644
  * @returns true if the presence was updated successfully
636
645
  */
637
- updateUserPresence(data?: Record<string, unknown>): Promise<boolean>;
646
+ updateUserPresence(data: Record<string, string | number | boolean | null>): Promise<boolean>;
638
647
  /**
639
648
  * Tests the connection to the backend
640
649
  */
@@ -705,6 +714,47 @@ declare class OverlayManager extends WavedashManager {
705
714
  private handleKeyDown;
706
715
  }
707
716
 
717
+ /**
718
+ * AudioManager
719
+ *
720
+ * Mutes & unmutes the game in response to MUTE_CHANGED iframe messages, without
721
+ * the game needing to know anything about it.
722
+ *
723
+ * Web Audio: subclass `AudioContext` so `ctx.destination` resolves to a master
724
+ * GainNode that we control. The master gain wires to the real native destination,
725
+ * so `node.connect(ctx.destination)` and any other game code is unaffected.
726
+ *
727
+ * HTML Media (`<audio>`/`<video>`): override `HTMLMediaElement.prototype.muted`
728
+ * to record the game's intended state, but write `true` to the underlying element
729
+ * whenever the SDK is muted. Tracked elements come from three sources:
730
+ * 1. Pre-existing DOM media (`querySelectorAll`)
731
+ * 2. `new Audio()` constructor shim (covers detached SFX)
732
+ * 3. MutationObserver for any media added to the DOM later (covers innerHTML,
733
+ * framework rendering, createElement + append, etc.)
734
+ */
735
+ declare class AudioManager extends WavedashManager {
736
+ private _isMuted;
737
+ private contexts;
738
+ private elements;
739
+ private intendedMuted;
740
+ private originalAudioContext;
741
+ private originalWebKitAudioContext;
742
+ private originalAudio;
743
+ private originalMutedDescriptor;
744
+ private mutationObserver;
745
+ constructor(sdk: WavedashSDK);
746
+ isMuted(): boolean;
747
+ private handleMute;
748
+ /**
749
+ * Track a media element and (if SDK is currently muted) silence it.
750
+ * Idempotent — safe to call multiple times for the same element.
751
+ */
752
+ private trackElement;
753
+ private installShims;
754
+ private shimAudioContextClass;
755
+ destroy(): void;
756
+ }
757
+
708
758
  /**
709
759
  * Friends service
710
760
  *
@@ -848,7 +898,6 @@ declare class WavedashSDK extends EventTarget {
848
898
  };
849
899
  UGCVisibility: {
850
900
  readonly PUBLIC: 0;
851
- readonly FRIENDS_ONLY: 1;
852
901
  readonly PRIVATE: 2;
853
902
  };
854
903
  AvatarSize: {
@@ -891,6 +940,7 @@ declare class WavedashSDK extends EventTarget {
891
940
  p2pManager: P2PManager;
892
941
  fullscreenManager: FullscreenManager;
893
942
  overlayManager: OverlayManager;
943
+ audioManager: AudioManager;
894
944
  private managers;
895
945
  private gameplayJwt;
896
946
  private gameplayJwtPromise;
@@ -998,21 +1048,19 @@ declare class WavedashSDK extends EventTarget {
998
1048
  createUGCItem(ugcType: UGCType, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
999
1049
  /**
1000
1050
  * Updates a UGC item and uploads the file to the server if a filePath is provided
1001
- * TODO: GD Script cannot call with optional arguments, convert this to accept a single dictionary of updates
1002
- * @param ugcId
1003
- * @param title
1004
- * @param description
1005
- * @param visibility
1006
- * @param filePath - optional IndexedDB key file path to upload to the server. If not provided, the UGC item will be updated but no file will be uploaded.
1051
+ * @param ugcId - The ID of the UGC item to update
1052
+ * @param updates - Object containing the fields to update. May also be passed
1053
+ * as a JSON string by engine bridges (Godot) that can't marshal a dict.
1007
1054
  * @returns ugcId
1008
1055
  */
1009
- updateUGCItem(ugcId: GenericId<"userGeneratedContent">, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
1056
+ updateUGCItem(ugcId: GenericId<"userGeneratedContent">, updates?: UpdateUGCItemArgs): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
1010
1057
  /**
1011
1058
  * Delete a UGC item: removes the row, the R2 object, and frees up the
1012
1059
  * user's storage quota by the size of the deleted upload.
1013
1060
  */
1014
1061
  deleteUGCItem(ugcId: GenericId<"userGeneratedContent">): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
1015
1062
  downloadUGCItem(ugcId: GenericId<"userGeneratedContent">, filePath: string): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
1063
+ listUGCItems(args?: ListUGCItemsArgs): Promise<WavedashResponse<PaginatedUGCItems>>;
1016
1064
  /**
1017
1065
  * Deletes a remote file from storage
1018
1066
  * @param filePath - The path of the remote file to delete
@@ -1157,12 +1205,16 @@ declare class WavedashSDK extends EventTarget {
1157
1205
  inviteUserToLobby(lobbyId: GenericId<"lobbies">, userId: GenericId<"users">): Promise<WavedashResponse<boolean>>;
1158
1206
  getLobbyInviteLink(copyToClipboard?: boolean): Promise<WavedashResponse<string>>;
1159
1207
  /**
1160
- * Updates rich user presence so friends can see what the player is doing in game
1161
- * TODO: data param should be more strongly typed
1162
- * @param data Game data to send to the backend
1208
+ * Updates rich user presence so friends can see what the player is doing in game.
1209
+ * Supported keys:
1210
+ * `status` — one-line activity shown as the primary line (e.g. "Traveling in a group")
1211
+ * `details` — secondary context shown beneath the status (e.g. current zone or mode)
1212
+ *
1213
+ * Pass an empty dictionary to clear all presence fields.
1214
+ * @param data Presence fields to update.
1163
1215
  * @returns true if the presence was updated successfully
1164
1216
  */
1165
- updateUserPresence(data?: Record<string, unknown>): Promise<WavedashResponse<boolean>>;
1217
+ updateUserPresence(data: Record<string, string | number | boolean | null>): Promise<WavedashResponse<boolean>>;
1166
1218
  private isGodot;
1167
1219
  private formatResponse;
1168
1220
  private ensureInit;
@@ -1209,4 +1261,4 @@ declare global {
1209
1261
 
1210
1262
  declare function setupWavedashSDK(): WavedashSDK;
1211
1263
 
1212
- export { type BackendConnectionPayload, type EngineInstance, type Friend, type FullscreenChangedPayload, type Leaderboard, type LeaderboardDisplayType, type LeaderboardEntries, type LeaderboardSortOrder, 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 RemoteFileMetadata, type StatsStoredPayload, type UGCType, type UGCVisibility, type UpsertedLeaderboardEntry, type WavedashConfig, type WavedashEvent, type WavedashEventMap, type WavedashResponse, WavedashSDK, setupWavedashSDK };
1264
+ 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 };
package/dist/index.js CHANGED
@@ -1033,7 +1033,8 @@ var UGCManager = class extends WavedashManager {
1033
1033
  }
1034
1034
  return ugcId;
1035
1035
  }
1036
- async updateUGCItem(ugcId, title, description, visibility, filePath) {
1036
+ async updateUGCItem(ugcId, updates = {}) {
1037
+ const { title, description, visibility, filePath } = updates;
1037
1038
  const { uploadUrl } = await this.sdk.convexClient.mutation(
1038
1039
  api3.sdk.userGeneratedContent.updateUGCItem,
1039
1040
  {
@@ -1079,6 +1080,18 @@ var UGCManager = class extends WavedashManager {
1079
1080
  }
1080
1081
  return ugcId;
1081
1082
  }
1083
+ async listUGCItems(args = {}) {
1084
+ const { createdBy, ugcType, titleSearch, numItems, continueCursor } = args;
1085
+ const filters = createdBy !== void 0 || ugcType !== void 0 || titleSearch !== void 0 ? { createdBy, ugcType, titleSearch } : void 0;
1086
+ return await this.sdk.convexClient.query(
1087
+ api3.sdk.userGeneratedContent.listUGCItems,
1088
+ {
1089
+ filters,
1090
+ numItems,
1091
+ continueCursor
1092
+ }
1093
+ );
1094
+ }
1082
1095
  };
1083
1096
 
1084
1097
  // src/services/leaderboards.ts
@@ -2696,6 +2709,7 @@ var HeartbeatManager = class extends WavedashManager {
2696
2709
  this.isFirstTick = true;
2697
2710
  this.TEST_CONNECTION_INTERVAL_MS = 1e3;
2698
2711
  this.DISCONNECTED_TIMEOUT_MS = 9e4;
2712
+ this.cachedPresenceData = {};
2699
2713
  this.handleVisibilityChange = () => {
2700
2714
  if (document.visibilityState === "visible") {
2701
2715
  this.start();
@@ -2765,7 +2779,7 @@ var HeartbeatManager = class extends WavedashManager {
2765
2779
  this.heartbeatInFlight = true;
2766
2780
  this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
2767
2781
  ...reestablish ? {
2768
- data: { forceUpdate: true },
2782
+ data: this.cachedPresenceData,
2769
2783
  deviceFingerprint: this.deviceFingerprint
2770
2784
  } : {}
2771
2785
  }).then((accepted) => {
@@ -2779,15 +2793,15 @@ var HeartbeatManager = class extends WavedashManager {
2779
2793
  });
2780
2794
  }
2781
2795
  /**
2782
- * Updates user presence in the backend
2796
+ * Updates user presence in the backend.
2783
2797
  * @param data - Data to send to the backend
2784
2798
  * @returns true if the presence was updated successfully
2785
2799
  */
2786
2800
  async updateUserPresence(data) {
2787
2801
  try {
2788
- const dataToSend = data ?? { forceUpdate: true };
2802
+ this.cachedPresenceData = data;
2789
2803
  await this.sdk.convexClient.mutation(api7.sdk.presence.heartbeat, {
2790
- data: dataToSend,
2804
+ data,
2791
2805
  deviceFingerprint: this.deviceFingerprint
2792
2806
  });
2793
2807
  return true;
@@ -3015,6 +3029,211 @@ var OverlayManager = class extends WavedashManager {
3015
3029
  }
3016
3030
  };
3017
3031
 
3032
+ // src/services/audio.ts
3033
+ import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE5 } from "@wvdsh/api";
3034
+ var WeakRefSet = class {
3035
+ constructor() {
3036
+ this.set = /* @__PURE__ */ new Set();
3037
+ }
3038
+ add(value) {
3039
+ for (const ref of this.set) {
3040
+ if (ref.deref() === value) return;
3041
+ }
3042
+ this.set.add(new WeakRef(value));
3043
+ }
3044
+ forEach(callback) {
3045
+ for (const ref of this.set) {
3046
+ const v = ref.deref();
3047
+ if (v === void 0) this.set.delete(ref);
3048
+ else callback(v);
3049
+ }
3050
+ }
3051
+ clear() {
3052
+ this.set.clear();
3053
+ }
3054
+ };
3055
+ var AudioManager = class extends WavedashManager {
3056
+ constructor(sdk) {
3057
+ super(sdk);
3058
+ this._isMuted = false;
3059
+ // Web Audio contexts and their master gain nodes
3060
+ this.contexts = /* @__PURE__ */ new Map();
3061
+ // HTML media elements we know about + their game-intended muted state
3062
+ this.elements = new WeakRefSet();
3063
+ this.intendedMuted = /* @__PURE__ */ new WeakMap();
3064
+ // Originals (restored on destroy)
3065
+ this.originalAudioContext = null;
3066
+ this.originalWebKitAudioContext = null;
3067
+ this.originalAudio = null;
3068
+ this.originalMutedDescriptor = null;
3069
+ this.mutationObserver = null;
3070
+ this.handleMute = (data) => {
3071
+ if (this._isMuted === data.isMuted) return;
3072
+ this._isMuted = data.isMuted;
3073
+ logger.debug(`[AudioManager] muted=${this._isMuted}`);
3074
+ const target = this._isMuted ? 0 : 1;
3075
+ this.contexts.forEach((gain, ctx) => {
3076
+ const now = ctx.currentTime;
3077
+ gain.gain.cancelScheduledValues(now);
3078
+ gain.gain.setValueAtTime(gain.gain.value, now);
3079
+ gain.gain.linearRampToValueAtTime(target, now + 0.05);
3080
+ });
3081
+ const setMutedNative = this.originalMutedDescriptor?.set;
3082
+ if (setMutedNative) {
3083
+ this.elements.forEach((el) => {
3084
+ const intended = this.intendedMuted.get(el) ?? false;
3085
+ setMutedNative.call(el, this._isMuted ? true : intended);
3086
+ });
3087
+ }
3088
+ };
3089
+ this.installShims();
3090
+ this.sdk.iframeMessenger.addEventListener(
3091
+ IFRAME_MESSAGE_TYPE5.MUTE_CHANGED,
3092
+ this.handleMute
3093
+ );
3094
+ }
3095
+ isMuted() {
3096
+ return this._isMuted;
3097
+ }
3098
+ /**
3099
+ * Track a media element and (if SDK is currently muted) silence it.
3100
+ * Idempotent — safe to call multiple times for the same element.
3101
+ */
3102
+ trackElement(el) {
3103
+ if (this.intendedMuted.has(el)) return;
3104
+ const getMuted = this.originalMutedDescriptor?.get;
3105
+ const setMuted = this.originalMutedDescriptor?.set;
3106
+ const current = getMuted ? getMuted.call(el) : el.muted;
3107
+ this.intendedMuted.set(el, current);
3108
+ this.elements.add(el);
3109
+ if (this._isMuted && !current && setMuted) {
3110
+ setMuted.call(el, true);
3111
+ }
3112
+ }
3113
+ installShims() {
3114
+ if (typeof window === "undefined") return;
3115
+ if (window.AudioContext) {
3116
+ this.originalAudioContext = window.AudioContext;
3117
+ window.AudioContext = this.shimAudioContextClass(window.AudioContext);
3118
+ }
3119
+ const win = window;
3120
+ if (win.webkitAudioContext) {
3121
+ this.originalWebKitAudioContext = win.webkitAudioContext;
3122
+ win.webkitAudioContext = this.shimAudioContextClass(win.webkitAudioContext);
3123
+ }
3124
+ if (window.Audio) {
3125
+ const OriginalAudio = window.Audio;
3126
+ this.originalAudio = OriginalAudio;
3127
+ ((manager) => {
3128
+ const Shimmed = function(src) {
3129
+ const audio = new OriginalAudio(src);
3130
+ manager.trackElement(audio);
3131
+ return audio;
3132
+ };
3133
+ Shimmed.prototype = OriginalAudio.prototype;
3134
+ window.Audio = Shimmed;
3135
+ })(this);
3136
+ }
3137
+ if (typeof document !== "undefined") {
3138
+ document.querySelectorAll("audio, video").forEach((el) => {
3139
+ this.trackElement(el);
3140
+ });
3141
+ this.mutationObserver = new MutationObserver((mutations) => {
3142
+ for (const m of mutations) {
3143
+ m.addedNodes.forEach((node) => {
3144
+ if (node instanceof HTMLMediaElement) {
3145
+ this.trackElement(node);
3146
+ } else if (node instanceof HTMLElement) {
3147
+ node.querySelectorAll("audio, video").forEach((el) => {
3148
+ this.trackElement(el);
3149
+ });
3150
+ }
3151
+ });
3152
+ }
3153
+ });
3154
+ this.mutationObserver.observe(document.documentElement, {
3155
+ childList: true,
3156
+ subtree: true
3157
+ });
3158
+ }
3159
+ this.originalMutedDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, "muted") ?? null;
3160
+ const original = this.originalMutedDescriptor;
3161
+ if (original?.get && original?.set) {
3162
+ ((manager) => {
3163
+ Object.defineProperty(HTMLMediaElement.prototype, "muted", {
3164
+ configurable: true,
3165
+ get() {
3166
+ const intended = manager.intendedMuted.get(this);
3167
+ return intended !== void 0 ? intended : original.get.call(this);
3168
+ },
3169
+ set(value) {
3170
+ manager.intendedMuted.set(this, value);
3171
+ manager.elements.add(this);
3172
+ original.set.call(this, manager._isMuted ? true : value);
3173
+ }
3174
+ });
3175
+ })(this);
3176
+ }
3177
+ }
3178
+ shimAudioContextClass(Original) {
3179
+ return /* @__PURE__ */ ((manager) => class extends Original {
3180
+ constructor(opts) {
3181
+ super(opts);
3182
+ const masterGain = this.createGain();
3183
+ masterGain.connect(this.destination);
3184
+ masterGain.gain.setValueAtTime(
3185
+ manager._isMuted ? 0 : 1,
3186
+ this.currentTime
3187
+ );
3188
+ Object.defineProperty(this, "destination", {
3189
+ configurable: true,
3190
+ get() {
3191
+ return masterGain;
3192
+ }
3193
+ });
3194
+ manager.contexts.set(this, masterGain);
3195
+ }
3196
+ close() {
3197
+ manager.contexts.delete(this);
3198
+ return super.close();
3199
+ }
3200
+ })(this);
3201
+ }
3202
+ destroy() {
3203
+ this.sdk.iframeMessenger.removeEventListener(
3204
+ IFRAME_MESSAGE_TYPE5.MUTE_CHANGED,
3205
+ this.handleMute
3206
+ );
3207
+ if (this.mutationObserver) {
3208
+ this.mutationObserver.disconnect();
3209
+ this.mutationObserver = null;
3210
+ }
3211
+ if (typeof window !== "undefined") {
3212
+ if (this.originalAudioContext) {
3213
+ window.AudioContext = this.originalAudioContext;
3214
+ }
3215
+ const win = window;
3216
+ if (this.originalWebKitAudioContext && win.webkitAudioContext) {
3217
+ win.webkitAudioContext = this.originalWebKitAudioContext;
3218
+ }
3219
+ if (this.originalAudio) {
3220
+ window.Audio = this.originalAudio;
3221
+ }
3222
+ }
3223
+ if (this.originalMutedDescriptor) {
3224
+ Object.defineProperty(
3225
+ HTMLMediaElement.prototype,
3226
+ "muted",
3227
+ this.originalMutedDescriptor
3228
+ );
3229
+ }
3230
+ this.contexts.clear();
3231
+ this.elements.clear();
3232
+ this.intendedMuted = /* @__PURE__ */ new WeakMap();
3233
+ super.destroy();
3234
+ }
3235
+ };
3236
+
3018
3237
  // src/services/friends.ts
3019
3238
  import { api as api8 } from "@wvdsh/api";
3020
3239
 
@@ -3276,7 +3495,7 @@ var SwMessenger = class {
3276
3495
  };
3277
3496
 
3278
3497
  // src/index.ts
3279
- import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE5, UrlParams } from "@wvdsh/api";
3498
+ import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE6, UrlParams } from "@wvdsh/api";
3280
3499
 
3281
3500
  // src/utils/validation.ts
3282
3501
  var CONVEX_ID_REGEX = /^[0-9a-z]{31,37}$/;
@@ -3320,6 +3539,13 @@ var vRecord = (value, path) => {
3320
3539
  `${path}: expected plain object, got ${describeValue(value)}`
3321
3540
  );
3322
3541
  }
3542
+ for (const [key, val] of Object.entries(value)) {
3543
+ if (typeof val === "object" && val !== null) {
3544
+ throw new Error(
3545
+ `${path}: expected flat record with no nested objects, but key "${key}" contains ${describeValue(val)}`
3546
+ );
3547
+ }
3548
+ }
3323
3549
  return value;
3324
3550
  };
3325
3551
  function vId(tableName) {
@@ -3362,11 +3588,34 @@ function vUnion(...variants) {
3362
3588
  throw new Error(`${path}: no variant matched, got ${describeValue(value)}`);
3363
3589
  };
3364
3590
  }
3591
+ function vObject(shape) {
3592
+ return (value, path) => {
3593
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3594
+ throw new Error(
3595
+ `${path}: expected plain object, got ${describeValue(value)}`
3596
+ );
3597
+ }
3598
+ const obj = value;
3599
+ for (const key of Object.keys(obj)) {
3600
+ if (!(key in shape)) {
3601
+ throw new Error(`${path}: unrecognized property "${key}"`);
3602
+ }
3603
+ }
3604
+ for (const key of Object.keys(shape)) {
3605
+ shape[key](obj[key], `${path}.${key}`);
3606
+ }
3607
+ return obj;
3608
+ };
3609
+ }
3365
3610
  function validateArgs(methodName, specs, values) {
3611
+ const shape = {};
3612
+ const obj = {};
3366
3613
  for (let i = 0; i < specs.length; i++) {
3367
3614
  const [argName, validator] = specs[i];
3368
- validator(values[i], `${methodName}.${argName}`);
3615
+ shape[argName] = validator;
3616
+ obj[argName] = values[i];
3369
3617
  }
3618
+ vObject(shape)(obj, methodName);
3370
3619
  }
3371
3620
  function describeValue(value) {
3372
3621
  if (value === void 0) return "undefined";
@@ -3437,6 +3686,7 @@ var WavedashSDK = class extends EventTarget {
3437
3686
  this.gameEventManager = new GameEventManager(this);
3438
3687
  this.fullscreenManager = new FullscreenManager(this);
3439
3688
  this.overlayManager = new OverlayManager(this);
3689
+ this.audioManager = new AudioManager(this);
3440
3690
  this.managers = [
3441
3691
  this.p2pManager,
3442
3692
  this.lobbyManager,
@@ -3448,7 +3698,8 @@ var WavedashSDK = class extends EventTarget {
3448
3698
  this.friendsManager,
3449
3699
  this.gameEventManager,
3450
3700
  this.fullscreenManager,
3451
- this.overlayManager
3701
+ this.overlayManager,
3702
+ this.audioManager
3452
3703
  ];
3453
3704
  this.friendsManager.cacheUsers([
3454
3705
  {
@@ -3580,7 +3831,7 @@ var WavedashSDK = class extends EventTarget {
3580
3831
  [progress]
3581
3832
  );
3582
3833
  this.clearSetupWarning();
3583
- iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE5.PROGRESS_UPDATE, {
3834
+ iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE6.PROGRESS_UPDATE, {
3584
3835
  progress
3585
3836
  });
3586
3837
  }
@@ -3589,7 +3840,7 @@ var WavedashSDK = class extends EventTarget {
3589
3840
  if (this.gameFinishedLoading) return;
3590
3841
  this.gameFinishedLoading = true;
3591
3842
  this.heartbeatManager.start();
3592
- iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE5.LOADING_COMPLETE, {});
3843
+ iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE6.LOADING_COMPLETE, {});
3593
3844
  this.overlayManager.takeFocus();
3594
3845
  }
3595
3846
  get gameLoaded() {
@@ -3817,30 +4068,45 @@ var WavedashSDK = class extends EventTarget {
3817
4068
  }
3818
4069
  /**
3819
4070
  * Updates a UGC item and uploads the file to the server if a filePath is provided
3820
- * TODO: GD Script cannot call with optional arguments, convert this to accept a single dictionary of updates
3821
- * @param ugcId
3822
- * @param title
3823
- * @param description
3824
- * @param visibility
3825
- * @param filePath - optional IndexedDB key file path to upload to the server. If not provided, the UGC item will be updated but no file will be uploaded.
4071
+ * @param ugcId - The ID of the UGC item to update
4072
+ * @param updates - Object containing the fields to update. May also be passed
4073
+ * as a JSON string by engine bridges (Godot) that can't marshal a dict.
3826
4074
  * @returns ugcId
3827
4075
  */
3828
- async updateUGCItem(ugcId, title, description, visibility, filePath) {
4076
+ async updateUGCItem(ugcId, updates = {}) {
4077
+ if (typeof updates === "string") {
4078
+ const raw = updates;
4079
+ try {
4080
+ updates = JSON.parse(raw);
4081
+ } catch (error) {
4082
+ const message = `updateUGCItem: invalid JSON: ${raw}`;
4083
+ logger.error(message, error);
4084
+ return this.formatResponse({
4085
+ success: false,
4086
+ data: null,
4087
+ message
4088
+ });
4089
+ }
4090
+ }
3829
4091
  return this.apiCall(
3830
4092
  this.ugcManager,
3831
4093
  "updateUGCItem",
3832
4094
  [
3833
4095
  ["ugcId", vId("userGeneratedContent")],
3834
- ["title", vOptional(vString)],
3835
- ["description", vOptional(vString)],
3836
- ["visibility", vOptional(vEnum(UGC_VISIBILITY, "UGCVisibility"))],
3837
- ["filePath", vOptional(vString)]
4096
+ [
4097
+ "updates",
4098
+ vOptional(
4099
+ vObject({
4100
+ title: vOptional(vString),
4101
+ description: vOptional(vString),
4102
+ visibility: vOptional(vEnum(UGC_VISIBILITY, "UGCVisibility")),
4103
+ filePath: vOptional(vString)
4104
+ })
4105
+ )
4106
+ ]
3838
4107
  ],
3839
4108
  ugcId,
3840
- title,
3841
- description,
3842
- visibility,
3843
- filePath
4109
+ updates
3844
4110
  );
3845
4111
  }
3846
4112
  /**
@@ -3867,6 +4133,47 @@ var WavedashSDK = class extends EventTarget {
3867
4133
  filePath
3868
4134
  );
3869
4135
  }
4136
+ async listUGCItems(args = {}) {
4137
+ if (typeof args === "string") {
4138
+ const raw = args;
4139
+ try {
4140
+ args = JSON.parse(raw);
4141
+ } catch (error) {
4142
+ const message = `listUGCItems: invalid JSON: ${raw}`;
4143
+ logger.error(message, error);
4144
+ return this.formatResponse({
4145
+ success: false,
4146
+ data: null,
4147
+ message
4148
+ });
4149
+ }
4150
+ }
4151
+ return this.apiCall(
4152
+ this.ugcManager,
4153
+ "listUGCItems",
4154
+ [
4155
+ [
4156
+ "args",
4157
+ vOptional((value, path) => {
4158
+ const obj = vObject({
4159
+ createdBy: vOptional(vId("users")),
4160
+ ugcType: vOptional(vEnum(UGC_TYPE, "UGCType")),
4161
+ titleSearch: vOptional(vString),
4162
+ numItems: vOptional(vNumber),
4163
+ continueCursor: vOptional(vString)
4164
+ })(value, path);
4165
+ if (obj.continueCursor !== void 0 && (obj.createdBy !== void 0 || obj.ugcType !== void 0 || obj.titleSearch !== void 0 || obj.numItems !== void 0)) {
4166
+ throw new Error(
4167
+ `${path}: continueCursor should be the only argument if present`
4168
+ );
4169
+ }
4170
+ return obj;
4171
+ })
4172
+ ]
4173
+ ],
4174
+ args
4175
+ );
4176
+ }
3870
4177
  // ================================
3871
4178
  // Save state / Remote File Storage
3872
4179
  // ================================
@@ -4281,16 +4588,34 @@ var WavedashSDK = class extends EventTarget {
4281
4588
  // User Presence
4282
4589
  // ==============================
4283
4590
  /**
4284
- * Updates rich user presence so friends can see what the player is doing in game
4285
- * TODO: data param should be more strongly typed
4286
- * @param data Game data to send to the backend
4591
+ * Updates rich user presence so friends can see what the player is doing in game.
4592
+ * Supported keys:
4593
+ * `status` — one-line activity shown as the primary line (e.g. "Traveling in a group")
4594
+ * `details` — secondary context shown beneath the status (e.g. current zone or mode)
4595
+ *
4596
+ * Pass an empty dictionary to clear all presence fields.
4597
+ * @param data Presence fields to update.
4287
4598
  * @returns true if the presence was updated successfully
4288
4599
  */
4289
4600
  async updateUserPresence(data) {
4601
+ if (typeof data === "string") {
4602
+ const raw = data;
4603
+ try {
4604
+ data = JSON.parse(raw);
4605
+ } catch (error) {
4606
+ const message = `updateUserPresence: invalid JSON: ${raw}`;
4607
+ logger.error(message, error);
4608
+ return this.formatResponse({
4609
+ success: false,
4610
+ data: null,
4611
+ message
4612
+ });
4613
+ }
4614
+ }
4290
4615
  return this.apiCall(
4291
4616
  this.heartbeatManager,
4292
4617
  "updateUserPresence",
4293
- [["data", vOptional(vRecord)]],
4618
+ [["data", vRecord]],
4294
4619
  data
4295
4620
  );
4296
4621
  }
@@ -4381,7 +4706,7 @@ var WavedashSDK = class extends EventTarget {
4381
4706
  throw new Error(`Failed to refresh gameplay token: ${response.status}`);
4382
4707
  }
4383
4708
  this.gameplayJwt = await response.text();
4384
- iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE5.GAMEPLAY_JWT_READY, {
4709
+ iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE6.GAMEPLAY_JWT_READY, {
4385
4710
  gameplayJwt: this.gameplayJwt
4386
4711
  });
4387
4712
  this.swMessenger.postToServiceWorker({
@@ -4417,7 +4742,7 @@ var WavedashSDK = class extends EventTarget {
4417
4742
  }
4418
4743
  setupSessionEndListeners() {
4419
4744
  iframeMessenger.addEventListener(
4420
- IFRAME_MESSAGE_TYPE5.END_SESSION,
4745
+ IFRAME_MESSAGE_TYPE6.END_SESSION,
4421
4746
  () => this.destroy()
4422
4747
  );
4423
4748
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wvdsh/sdk-js",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
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.16",
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
  }