@wvdsh/sdk-js 1.3.10 → 1.3.12

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];
@@ -385,14 +392,25 @@ declare class FileSystemManager extends WavedashManager {
385
392
  */
386
393
  deleteRemoteFile(filePath: string): Promise<string>;
387
394
  /**
388
- * Downloads a remote file to a local location
395
+ * Downloads a remote file to a local location.
396
+ * Throws on failure; the error message is the server's HTTP status (e.g. "404 (Not Found)")
397
+ * or a network-level description if the server didn't respond. See also: {@link remoteFileExists}
389
398
  * @param filePath - The path of the remote file to download
390
399
  * @returns The path of the local file that the remote file was downloaded to
391
400
  */
392
401
  downloadRemoteFile(filePath: string): Promise<string>;
393
402
  /**
394
- * Lists each file in a remote directory, including its subdirectories
395
- * Returns only file paths, no directory paths
403
+ * Checks whether a remote file exists by issuing a HEAD request.
404
+ * Does NOT throw for the "file does not exist" case — returns false.
405
+ * Throws only for real errors (network failure, auth failure, server error).
406
+ * @param filePath - The path of the remote file to check
407
+ * @returns true if the remote file exists, false otherwise
408
+ */
409
+ remoteFileExists(filePath: string): Promise<boolean>;
410
+ /**
411
+ * Lists each file in a remote directory, including its subdirectories.
412
+ * Returns only file paths, no directory paths.
413
+ * An empty or non-existent directory returns an empty array — not an error.
396
414
  * @param path - The path of the remote directory to list
397
415
  * @returns A list of metadata for each file in the remote directory
398
416
  */
@@ -401,7 +419,7 @@ declare class FileSystemManager extends WavedashManager {
401
419
  writeLocalFile(filePath: string, data: Uint8Array): Promise<boolean>;
402
420
  readLocalFile(filePath: string): Promise<Uint8Array | null>;
403
421
  upload(presignedUploadUrl: string, filePath: string): Promise<boolean>;
404
- download(url: string, filePath: string): Promise<boolean>;
422
+ download(url: string, filePath: string): Promise<void>;
405
423
  private getRemoteStorageOrigin;
406
424
  private getRemoteStorageUrl;
407
425
  private uploadFromIndexedDb;
@@ -418,9 +436,10 @@ declare class FileSystemManager extends WavedashManager {
418
436
  declare class UGCManager extends WavedashManager {
419
437
  constructor(sdk: WavedashSDK);
420
438
  createUGCItem(ugcType: UGCType, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<GenericId<"userGeneratedContent">>;
421
- 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">>;
422
440
  deleteUGCItem(ugcId: GenericId<"userGeneratedContent">): Promise<GenericId<"userGeneratedContent">>;
423
441
  downloadUGCItem(ugcId: GenericId<"userGeneratedContent">, filePath: string): Promise<GenericId<"userGeneratedContent">>;
442
+ listUGCItems(args?: ListUGCItemsArgs): Promise<PaginatedUGCItems>;
424
443
  }
425
444
 
426
445
  /**
@@ -623,7 +642,7 @@ declare class HeartbeatManager extends WavedashManager {
623
642
  * @param data - Data to send to the backend
624
643
  * @returns true if the presence was updated successfully
625
644
  */
626
- updateUserPresence(data?: Record<string, unknown>): Promise<boolean>;
645
+ updateUserPresence(data?: Record<string, string | number | boolean | null>): Promise<boolean>;
627
646
  /**
628
647
  * Tests the connection to the backend
629
648
  */
@@ -837,7 +856,6 @@ declare class WavedashSDK extends EventTarget {
837
856
  };
838
857
  UGCVisibility: {
839
858
  readonly PUBLIC: 0;
840
- readonly FRIENDS_ONLY: 1;
841
859
  readonly PRIVATE: 2;
842
860
  };
843
861
  AvatarSize: {
@@ -987,21 +1005,19 @@ declare class WavedashSDK extends EventTarget {
987
1005
  createUGCItem(ugcType: UGCType, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
988
1006
  /**
989
1007
  * Updates a UGC item and uploads the file to the server if a filePath is provided
990
- * TODO: GD Script cannot call with optional arguments, convert this to accept a single dictionary of updates
991
- * @param ugcId
992
- * @param title
993
- * @param description
994
- * @param visibility
995
- * @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.
1008
+ * @param ugcId - The ID of the UGC item to update
1009
+ * @param updates - Object containing the fields to update. May also be passed
1010
+ * as a JSON string by engine bridges (Godot) that can't marshal a dict.
996
1011
  * @returns ugcId
997
1012
  */
998
- updateUGCItem(ugcId: GenericId<"userGeneratedContent">, title?: string, description?: string, visibility?: UGCVisibility, filePath?: string): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
1013
+ updateUGCItem(ugcId: GenericId<"userGeneratedContent">, updates?: UpdateUGCItemArgs): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
999
1014
  /**
1000
1015
  * Delete a UGC item: removes the row, the R2 object, and frees up the
1001
1016
  * user's storage quota by the size of the deleted upload.
1002
1017
  */
1003
1018
  deleteUGCItem(ugcId: GenericId<"userGeneratedContent">): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
1004
1019
  downloadUGCItem(ugcId: GenericId<"userGeneratedContent">, filePath: string): Promise<WavedashResponse<GenericId<"userGeneratedContent">>>;
1020
+ listUGCItems(args?: ListUGCItemsArgs): Promise<WavedashResponse<PaginatedUGCItems>>;
1005
1021
  /**
1006
1022
  * Deletes a remote file from storage
1007
1023
  * @param filePath - The path of the remote file to delete
@@ -1009,12 +1025,19 @@ declare class WavedashSDK extends EventTarget {
1009
1025
  */
1010
1026
  deleteRemoteFile(filePath: string): Promise<WavedashResponse<string>>;
1011
1027
  /**
1012
- * Downloads a remote file to a local location
1028
+ * Downloads a remote file to a local location.
1029
+ * Returns success=false (with the server status in `message`) if the file
1030
+ * doesn't exist or any other error occurs. See also: {@link remoteFileExists}
1013
1031
  * @param filePath - The path of the remote file to download
1014
- * @param downloadTo - Optionally provide a path to download the file to, defaults to the same path as the remote file
1015
1032
  * @returns The path of the local file that the remote file was downloaded to
1016
1033
  */
1017
1034
  downloadRemoteFile(filePath: string): Promise<WavedashResponse<string>>;
1035
+ /**
1036
+ * Checks whether a remote file exists. Sends a lightweight HEAD request to check for existence.
1037
+ * @param filePath - The path of the remote file to check
1038
+ * @returns true if the remote file exists, false if it does not.
1039
+ */
1040
+ remoteFileExists(filePath: string): Promise<WavedashResponse<boolean>>;
1018
1041
  /**
1019
1042
  * Uploads a local file to remote storage
1020
1043
  * @param filePath - The path of the local file to upload
@@ -1140,11 +1163,10 @@ declare class WavedashSDK extends EventTarget {
1140
1163
  getLobbyInviteLink(copyToClipboard?: boolean): Promise<WavedashResponse<string>>;
1141
1164
  /**
1142
1165
  * Updates rich user presence so friends can see what the player is doing in game
1143
- * TODO: data param should be more strongly typed
1144
1166
  * @param data Game data to send to the backend
1145
1167
  * @returns true if the presence was updated successfully
1146
1168
  */
1147
- updateUserPresence(data?: Record<string, unknown>): Promise<WavedashResponse<boolean>>;
1169
+ updateUserPresence(data?: Record<string, string | number | boolean | null>): Promise<WavedashResponse<boolean>>;
1148
1170
  private isGodot;
1149
1171
  private formatResponse;
1150
1172
  private ensureInit;
@@ -1191,4 +1213,4 @@ declare global {
1191
1213
 
1192
1214
  declare function setupWavedashSDK(): WavedashSDK;
1193
1215
 
1194
- 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 };
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 };
package/dist/index.js CHANGED
@@ -270,18 +270,14 @@ var _LobbyManager = class _LobbyManager extends WavedashManager {
270
270
  }
271
271
  getLobbyUsers(lobbyId) {
272
272
  if (this.lobbyId !== lobbyId) {
273
- logger.error(
274
- "Must be a member of the lobby to access user list"
275
- );
273
+ logger.error("Must be a member of the lobby to access user list");
276
274
  return [];
277
275
  }
278
276
  return this.lobbyUsers;
279
277
  }
280
278
  getHostId(lobbyId) {
281
279
  if (this.lobbyId !== lobbyId) {
282
- logger.error(
283
- "Must be a member of the lobby to access the host ID"
284
- );
280
+ logger.error("Must be a member of the lobby to access the host ID");
285
281
  return null;
286
282
  }
287
283
  return this.lobbyHostId;
@@ -467,9 +463,7 @@ var _LobbyManager = class _LobbyManager extends WavedashManager {
467
463
  handleLobbyKicked(reason = LobbyKickedReason.KICKED) {
468
464
  const lobbyId = this.lobbyId;
469
465
  if (!lobbyId) return;
470
- logger.warn(
471
- `User was removed from lobby: ${lobbyId} (reason: ${reason})`
472
- );
466
+ logger.warn(`User was removed from lobby: ${lobbyId} (reason: ${reason})`);
473
467
  this.cleanupLobbyState();
474
468
  this.sdk.iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE.LOBBY_LEFT, {
475
469
  lobbyId
@@ -576,9 +570,7 @@ var _LobbyManager = class _LobbyManager extends WavedashManager {
576
570
  try {
577
571
  if (newUsers.length <= 1) {
578
572
  this.sdk.p2pManager.disconnectP2P();
579
- logger.debug(
580
- "Only one user in lobby, P2P connections disconnected"
581
- );
573
+ logger.debug("Only one user in lobby, P2P connections disconnected");
582
574
  return;
583
575
  }
584
576
  const wavedashUsers = newUsers.map((lobbyUser) => ({
@@ -745,21 +737,41 @@ var FileSystemManager = class extends WavedashManager {
745
737
  return filePath;
746
738
  }
747
739
  /**
748
- * Downloads a remote file to a local location
740
+ * Downloads a remote file to a local location.
741
+ * Throws on failure; the error message is the server's HTTP status (e.g. "404 (Not Found)")
742
+ * or a network-level description if the server didn't respond. See also: {@link remoteFileExists}
749
743
  * @param filePath - The path of the remote file to download
750
744
  * @returns The path of the local file that the remote file was downloaded to
751
745
  */
752
746
  async downloadRemoteFile(filePath) {
753
747
  const url = this.getRemoteStorageUrl(filePath);
754
- const success = await this.download(url, filePath);
755
- if (!success) {
756
- throw new Error(`Failed to download file: ${filePath}`);
757
- }
748
+ await this.download(url, filePath);
758
749
  return filePath;
759
750
  }
760
751
  /**
761
- * Lists each file in a remote directory, including its subdirectories
762
- * Returns only file paths, no directory paths
752
+ * Checks whether a remote file exists by issuing a HEAD request.
753
+ * Does NOT throw for the "file does not exist" case — returns false.
754
+ * Throws only for real errors (network failure, auth failure, server error).
755
+ * @param filePath - The path of the remote file to check
756
+ * @returns true if the remote file exists, false otherwise
757
+ */
758
+ async remoteFileExists(filePath) {
759
+ const url = this.getRemoteStorageUrl(filePath);
760
+ const jwt = await this.sdk.ensureGameplayJwt();
761
+ const response = await fetch(url, {
762
+ method: "HEAD",
763
+ headers: {
764
+ Authorization: `Bearer ${jwt}`
765
+ }
766
+ });
767
+ if (response.status === 404) return false;
768
+ if (response.ok) return true;
769
+ throw new Error(`${response.status} (${response.statusText})`);
770
+ }
771
+ /**
772
+ * Lists each file in a remote directory, including its subdirectories.
773
+ * Returns only file paths, no directory paths.
774
+ * An empty or non-existent directory returns an empty array — not an error.
763
775
  * @param path - The path of the remote directory to list
764
776
  * @returns A list of metadata for each file in the remote directory
765
777
  */
@@ -772,6 +784,7 @@ var FileSystemManager = class extends WavedashManager {
772
784
  Authorization: `Bearer ${jwt}`
773
785
  }
774
786
  });
787
+ if (response.status === 404) return [];
775
788
  if (!response.ok) {
776
789
  throw new Error(`${response.status} (${response.statusText})`);
777
790
  }
@@ -785,8 +798,14 @@ var FileSystemManager = class extends WavedashManager {
785
798
  const files = await this.listRemoteDirectory(path);
786
799
  const downloadPromises = files.map(async (file) => {
787
800
  const url = this.getRemoteStorageUrl(file.key);
788
- const success = await this.download(url, file.key);
789
- return { fileName: file.name, success };
801
+ try {
802
+ await this.download(url, file.key);
803
+ return { fileName: file.name, success: true };
804
+ } catch (error) {
805
+ const message = error instanceof Error ? error.message : String(error);
806
+ logger.error(`Failed to download ${file.name}: ${message}`);
807
+ return { fileName: file.name, success: false };
808
+ }
790
809
  });
791
810
  const downloadResults = await Promise.all(downloadPromises);
792
811
  const failedDownloads = downloadResults.filter((result) => !result.success);
@@ -849,12 +868,15 @@ var FileSystemManager = class extends WavedashManager {
849
868
  }
850
869
  return success;
851
870
  }
852
- // Helper to download a file from a URL and save locally
871
+ // Helper to download a file from a URL and save locally.
872
+ // Throws on any failure with a message containing the HTTP status (e.g.
873
+ // "404 (Not Found)") for server-side failures, or a network/FS error
874
+ // description otherwise. Callers should let the error propagate; the
875
+ // public apiCall wrapper surfaces the message in WavedashResponse.message.
853
876
  async download(url, filePath) {
854
877
  logger.debug(`Downloading ${filePath} from: ${url}`);
855
878
  if (this.sdk.engineInstance && !this.sdk.engineInstance.FS) {
856
- logger.error("Engine instance is missing the Emscripten FS API");
857
- return false;
879
+ throw new Error("Engine instance is missing the Emscripten FS API");
858
880
  }
859
881
  const jwt = await this.sdk.ensureGameplayJwt();
860
882
  const response = await fetch(url, {
@@ -864,36 +886,34 @@ var FileSystemManager = class extends WavedashManager {
864
886
  }
865
887
  });
866
888
  if (!response.ok) {
867
- logger.error(
868
- `Failed to download remote file: ${response.status} (${response.statusText})`
869
- );
870
- return false;
889
+ throw new Error(`${response.status} (${response.statusText})`);
871
890
  }
872
891
  const blob = await response.blob();
873
892
  const arrayBuffer = await blob.arrayBuffer();
874
893
  const dataArray = new Uint8Array(arrayBuffer);
875
- try {
876
- if (this.sdk.engineInstance) {
877
- const dirPath = filePath.substring(0, filePath.lastIndexOf("/"));
878
- if (dirPath) {
879
- try {
880
- this.sdk.engineInstance.FS.mkdirTree(dirPath);
881
- } catch (_error) {
882
- }
894
+ if (this.sdk.engineInstance) {
895
+ const dirPath = filePath.substring(0, filePath.lastIndexOf("/"));
896
+ if (dirPath) {
897
+ try {
898
+ this.sdk.engineInstance.FS.mkdirTree(dirPath);
899
+ } catch (_error) {
883
900
  }
901
+ }
902
+ try {
884
903
  this.sdk.engineInstance.FS.writeFile(filePath, dataArray);
885
- } else {
886
- const success = await this.writeLocalFile(filePath, dataArray);
887
- if (!success) return false;
904
+ } catch (error) {
905
+ const msg = error instanceof Error ? error.message : String(error);
906
+ throw new Error(`Failed to save file ${filePath} to engine FS: ${msg}`);
907
+ }
908
+ } else {
909
+ const success = await this.writeLocalFile(filePath, dataArray);
910
+ if (!success) {
911
+ throw new Error(
912
+ `Failed to save file ${filePath} to local IndexedDB storage`
913
+ );
888
914
  }
889
- logger.debug(`Successfully saved to: ${filePath}`);
890
- return true;
891
- } catch (error) {
892
- logger.error(
893
- `Failed to save file ${filePath}: ${error instanceof Error ? error.message : String(error)}`
894
- );
895
- return false;
896
915
  }
916
+ logger.debug(`Successfully saved to: ${filePath}`);
897
917
  }
898
918
  // ================
899
919
  // Private Methods
@@ -1013,7 +1033,8 @@ var UGCManager = class extends WavedashManager {
1013
1033
  }
1014
1034
  return ugcId;
1015
1035
  }
1016
- async updateUGCItem(ugcId, title, description, visibility, filePath) {
1036
+ async updateUGCItem(ugcId, updates = {}) {
1037
+ const { title, description, visibility, filePath } = updates;
1017
1038
  const { uploadUrl } = await this.sdk.convexClient.mutation(
1018
1039
  api3.sdk.userGeneratedContent.updateUGCItem,
1019
1040
  {
@@ -1051,15 +1072,26 @@ var UGCManager = class extends WavedashManager {
1051
1072
  api3.sdk.userGeneratedContent.getUGCItemDownloadUrl,
1052
1073
  { ugcId }
1053
1074
  );
1054
- const success = await this.sdk.fileSystemManager.download(
1055
- downloadUrl,
1056
- filePath
1057
- );
1058
- if (!success) {
1059
- throw new Error(`Failed to download UGC item: ${ugcId}`);
1075
+ try {
1076
+ await this.sdk.fileSystemManager.download(downloadUrl, filePath);
1077
+ } catch (error) {
1078
+ const msg = error instanceof Error ? error.message : String(error);
1079
+ throw new Error(`Failed to download UGC item ${ugcId}: ${msg}`);
1060
1080
  }
1061
1081
  return ugcId;
1062
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
+ }
1063
1095
  };
1064
1096
 
1065
1097
  // src/services/leaderboards.ts
@@ -1303,9 +1335,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1303
1335
  return this.updateP2PConnection(members);
1304
1336
  }
1305
1337
  if (this.initializationInProgress && this.initializationLobbyId === lobbyId) {
1306
- logger.debug(
1307
- "P2P initialization already in progress, waiting..."
1308
- );
1338
+ logger.debug("P2P initialization already in progress, waiting...");
1309
1339
  await this.initializationInProgress;
1310
1340
  if (this.currentConnection) {
1311
1341
  return this.updateP2PConnection(members);
@@ -1386,9 +1416,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1386
1416
  existingPeer.username = member.username;
1387
1417
  }
1388
1418
  } else {
1389
- logger.debug(
1390
- `Adding new peer: ${member.username} (${member.id})`
1391
- );
1419
+ logger.debug(`Adding new peer: ${member.username} (${member.id})`);
1392
1420
  this.currentConnection.peers[member.id] = {
1393
1421
  userId: member.id,
1394
1422
  username: member.username
@@ -1421,9 +1449,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1421
1449
  return this.createOfferToPeer(userId);
1422
1450
  });
1423
1451
  await Promise.all(offerPromises);
1424
- logger.debug(
1425
- `Initiated ${offerPromises.length} offers to new peers`
1426
- );
1452
+ logger.debug(`Initiated ${offerPromises.length} offers to new peers`);
1427
1453
  }
1428
1454
  }
1429
1455
  for (const userId of Object.keys(
@@ -1512,10 +1538,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1512
1538
  this.pendingProcessedMessageIds.delete(messageId);
1513
1539
  }
1514
1540
  } catch (error) {
1515
- logger.error(
1516
- "Failed to mark signaling messages as processed:",
1517
- error
1518
- );
1541
+ logger.error("Failed to mark signaling messages as processed:", error);
1519
1542
  for (const messageId of newMessageIds) {
1520
1543
  this.pendingProcessedMessageIds.delete(messageId);
1521
1544
  }
@@ -1569,9 +1592,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1569
1592
  await this.flushPendingIceCandidates(remoteUserId, pc);
1570
1593
  const answer = await pc.createAnswer();
1571
1594
  await pc.setLocalDescription(answer);
1572
- logger.debug(
1573
- ` Answer created, waiting for ondatachannel events...`
1574
- );
1595
+ logger.debug(` Answer created, waiting for ondatachannel events...`);
1575
1596
  const answerData = {
1576
1597
  type: answer.type,
1577
1598
  sdp: answer.sdp
@@ -1603,10 +1624,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1603
1624
  break;
1604
1625
  }
1605
1626
  default:
1606
- logger.warn(
1607
- "Unknown signaling message type:",
1608
- message.messageType
1609
- );
1627
+ logger.warn("Unknown signaling message type:", message.messageType);
1610
1628
  }
1611
1629
  }
1612
1630
  /**
@@ -1648,9 +1666,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1648
1666
  const peersToInitiate = Object.keys(connection.peers).filter((userId) => currentUserId < userId);
1649
1667
  if (peersToInitiate.length > 0) {
1650
1668
  const offerPromises = peersToInitiate.map((userId) => {
1651
- logger.debug(
1652
- `Initiating offer to peer ${userId} (lower userId rule)`
1653
- );
1669
+ logger.debug(`Initiating offer to peer ${userId} (lower userId rule)`);
1654
1670
  return this.createOfferToPeer(userId);
1655
1671
  });
1656
1672
  await Promise.all(offerPromises);
@@ -1691,9 +1707,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1691
1707
  async createPeerConnection(remoteUserId, connection, shouldCreateChannels = false) {
1692
1708
  const iceServers = await this.getIceServers();
1693
1709
  if (!iceServers) {
1694
- logger.error(
1695
- `No ICE servers available for peer ${remoteUserId}`
1696
- );
1710
+ logger.error(`No ICE servers available for peer ${remoteUserId}`);
1697
1711
  this.sdk.gameEventManager.notifyGame(
1698
1712
  WavedashEvents.P2P_CONNECTION_FAILED,
1699
1713
  {
@@ -1919,9 +1933,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1919
1933
  }
1920
1934
  setupDataChannelHandlers(channel, remoteUserId, type) {
1921
1935
  channel.onopen = () => {
1922
- logger.debug(
1923
- `${type} data channel opened with peer ${remoteUserId}`
1924
- );
1936
+ logger.debug(`${type} data channel opened with peer ${remoteUserId}`);
1925
1937
  if (this.isPeerReady(remoteUserId) && !this.establishedPeers.has(remoteUserId)) {
1926
1938
  this.establishedPeers.add(remoteUserId);
1927
1939
  const peer = this.currentConnection?.peers[remoteUserId];
@@ -1940,10 +1952,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1940
1952
  this.enqueueMessage(event.data, remoteUserId);
1941
1953
  };
1942
1954
  channel.onerror = (error) => {
1943
- logger.error(
1944
- `Data channel error with peer ${remoteUserId}:`,
1945
- error
1946
- );
1955
+ logger.error(`Data channel error with peer ${remoteUserId}:`, error);
1947
1956
  const peer = this.currentConnection?.peers[remoteUserId];
1948
1957
  if (peer) {
1949
1958
  this.sdk.gameEventManager.notifyGame(
@@ -1957,9 +1966,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1957
1966
  }
1958
1967
  };
1959
1968
  channel.onclose = () => {
1960
- logger.debug(
1961
- `${type} data channel closed with peer ${remoteUserId}`
1962
- );
1969
+ logger.debug(`${type} data channel closed with peer ${remoteUserId}`);
1963
1970
  this.establishedPeers.delete(remoteUserId);
1964
1971
  this.reconnectingPeers.delete(remoteUserId);
1965
1972
  const peer = this.currentConnection?.peers[remoteUserId];
@@ -1988,9 +1995,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
1988
1995
  return false;
1989
1996
  }
1990
1997
  if (!payload) {
1991
- logger.error(
1992
- `P2P send called with missing payload, dropping message.`
1993
- );
1998
+ logger.error(`P2P send called with missing payload, dropping message.`);
1994
1999
  this.reportPacketDrop(appChannel, "SEND", "INVALID_PAYLOAD_SIZE");
1995
2000
  return false;
1996
2001
  }
@@ -2031,10 +2036,7 @@ var _P2PManager = class _P2PManager extends WavedashManager {
2031
2036
  try {
2032
2037
  channel.send(messageData);
2033
2038
  } catch (error) {
2034
- logger.error(
2035
- `P2P broadcast to peer ${peerUserId} failed:`,
2036
- error
2037
- );
2039
+ logger.error(`P2P broadcast to peer ${peerUserId} failed:`, error);
2038
2040
  }
2039
2041
  });
2040
2042
  } else {
@@ -2517,11 +2519,10 @@ var StatsManager = class extends WavedashManager {
2517
2519
  // storeNow=true (and storeStats()) call .flush() to fire the pending invocation
2518
2520
  // immediately. The in-flight gate in persist() covers mutations that outlast
2519
2521
  // the throttle window, which would otherwise overlap and cause OCC conflicts.
2520
- this.throttledPersist = throttle2(
2521
- () => this.persist(),
2522
- STORE_THROTTLE_MS,
2523
- { leading: false, trailing: true }
2524
- );
2522
+ this.throttledPersist = throttle2(() => this.persist(), STORE_THROTTLE_MS, {
2523
+ leading: false,
2524
+ trailing: true
2525
+ });
2525
2526
  this.subscribe();
2526
2527
  this.requestStats().catch((error) => {
2527
2528
  logger.error("Initial stats fetch failed:", error);
@@ -2557,10 +2558,7 @@ var StatsManager = class extends WavedashManager {
2557
2558
  this.knownAchievementIds = new Set(ids);
2558
2559
  },
2559
2560
  (error) => {
2560
- logger.error(
2561
- "Achievement identifiers subscription error:",
2562
- error
2563
- );
2561
+ logger.error("Achievement identifiers subscription error:", error);
2564
2562
  }
2565
2563
  ),
2566
2564
  this.sdk.convexClient.onUpdate(
@@ -2834,9 +2832,7 @@ var HeartbeatManager = class extends WavedashManager {
2834
2832
  );
2835
2833
  } else if (!this.isConnected && wasConnected) {
2836
2834
  this.disconnectedAt = Date.now();
2837
- logger.warn(
2838
- "Backend disconnected - attempting to reconnect..."
2839
- );
2835
+ logger.warn("Backend disconnected - attempting to reconnect...");
2840
2836
  this.sdk.gameEventManager.notifyGame(
2841
2837
  WavedashEvents.BACKEND_RECONNECTING,
2842
2838
  connection
@@ -3118,7 +3114,7 @@ var FriendsManager = class extends WavedashManager {
3118
3114
  /**
3119
3115
  * Replace the leaderboard-page cache with the users from a single page of
3120
3116
  * leaderboard entries.
3121
- *
3117
+ *
3122
3118
  * In general devs should just use the username and userAvatarUrl returned
3123
3119
  * from `listLeaderboardEntries` directly, but we cache the latest leaderboard
3124
3120
  * page here just so getUserAvatarUrl and getUsername still work for the current page.
@@ -3379,11 +3375,29 @@ function vUnion(...variants) {
3379
3375
  throw new Error(`${path}: no variant matched, got ${describeValue(value)}`);
3380
3376
  };
3381
3377
  }
3378
+ function vObject(shape) {
3379
+ return (value, path) => {
3380
+ const obj = vRecord(value, path);
3381
+ for (const key of Object.keys(obj)) {
3382
+ if (!(key in shape)) {
3383
+ throw new Error(`${path}: unrecognized property "${key}"`);
3384
+ }
3385
+ }
3386
+ for (const key of Object.keys(shape)) {
3387
+ shape[key](obj[key], `${path}.${key}`);
3388
+ }
3389
+ return obj;
3390
+ };
3391
+ }
3382
3392
  function validateArgs(methodName, specs, values) {
3393
+ const shape = {};
3394
+ const obj = {};
3383
3395
  for (let i = 0; i < specs.length; i++) {
3384
3396
  const [argName, validator] = specs[i];
3385
- validator(values[i], `${methodName}.${argName}`);
3397
+ shape[argName] = validator;
3398
+ obj[argName] = values[i];
3386
3399
  }
3400
+ vObject(shape)(obj, methodName);
3387
3401
  }
3388
3402
  function describeValue(value) {
3389
3403
  if (value === void 0) return "undefined";
@@ -3515,9 +3529,7 @@ var WavedashSDK = class extends EventTarget {
3515
3529
  }
3516
3530
  this.config = config ?? {};
3517
3531
  this._initialized = true;
3518
- logger.setLogLevel(
3519
- this.config.debug ? LOG_LEVEL.DEBUG : LOG_LEVEL.WARN
3520
- );
3532
+ logger.setLogLevel(this.config.debug ? LOG_LEVEL.DEBUG : LOG_LEVEL.WARN);
3521
3533
  this.p2pManager.init(this.config.p2p);
3522
3534
  logger.debug("Initialized with config:", this.config);
3523
3535
  if (!this.config.deferEvents) {
@@ -3836,30 +3848,45 @@ var WavedashSDK = class extends EventTarget {
3836
3848
  }
3837
3849
  /**
3838
3850
  * Updates a UGC item and uploads the file to the server if a filePath is provided
3839
- * TODO: GD Script cannot call with optional arguments, convert this to accept a single dictionary of updates
3840
- * @param ugcId
3841
- * @param title
3842
- * @param description
3843
- * @param visibility
3844
- * @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.
3851
+ * @param ugcId - The ID of the UGC item to update
3852
+ * @param updates - Object containing the fields to update. May also be passed
3853
+ * as a JSON string by engine bridges (Godot) that can't marshal a dict.
3845
3854
  * @returns ugcId
3846
3855
  */
3847
- async updateUGCItem(ugcId, title, description, visibility, filePath) {
3856
+ async updateUGCItem(ugcId, updates = {}) {
3857
+ if (typeof updates === "string") {
3858
+ const raw = updates;
3859
+ try {
3860
+ updates = JSON.parse(raw);
3861
+ } catch (error) {
3862
+ const message = `updateUGCItem: invalid JSON: ${raw}`;
3863
+ logger.error(message, error);
3864
+ return this.formatResponse({
3865
+ success: false,
3866
+ data: null,
3867
+ message
3868
+ });
3869
+ }
3870
+ }
3848
3871
  return this.apiCall(
3849
3872
  this.ugcManager,
3850
3873
  "updateUGCItem",
3851
3874
  [
3852
3875
  ["ugcId", vId("userGeneratedContent")],
3853
- ["title", vOptional(vString)],
3854
- ["description", vOptional(vString)],
3855
- ["visibility", vOptional(vEnum(UGC_VISIBILITY, "UGCVisibility"))],
3856
- ["filePath", vOptional(vString)]
3876
+ [
3877
+ "updates",
3878
+ vOptional(
3879
+ vObject({
3880
+ title: vOptional(vString),
3881
+ description: vOptional(vString),
3882
+ visibility: vOptional(vEnum(UGC_VISIBILITY, "UGCVisibility")),
3883
+ filePath: vOptional(vString)
3884
+ })
3885
+ )
3886
+ ]
3857
3887
  ],
3858
3888
  ugcId,
3859
- title,
3860
- description,
3861
- visibility,
3862
- filePath
3889
+ updates
3863
3890
  );
3864
3891
  }
3865
3892
  /**
@@ -3886,6 +3913,47 @@ var WavedashSDK = class extends EventTarget {
3886
3913
  filePath
3887
3914
  );
3888
3915
  }
3916
+ async listUGCItems(args = {}) {
3917
+ if (typeof args === "string") {
3918
+ const raw = args;
3919
+ try {
3920
+ args = JSON.parse(raw);
3921
+ } catch (error) {
3922
+ const message = `listUGCItems: invalid JSON: ${raw}`;
3923
+ logger.error(message, error);
3924
+ return this.formatResponse({
3925
+ success: false,
3926
+ data: null,
3927
+ message
3928
+ });
3929
+ }
3930
+ }
3931
+ return this.apiCall(
3932
+ this.ugcManager,
3933
+ "listUGCItems",
3934
+ [
3935
+ [
3936
+ "args",
3937
+ vOptional((value, path) => {
3938
+ const obj = vObject({
3939
+ createdBy: vOptional(vId("users")),
3940
+ ugcType: vOptional(vEnum(UGC_TYPE, "UGCType")),
3941
+ titleSearch: vOptional(vString),
3942
+ numItems: vOptional(vNumber),
3943
+ continueCursor: vOptional(vString)
3944
+ })(value, path);
3945
+ if (obj.continueCursor !== void 0 && (obj.createdBy !== void 0 || obj.ugcType !== void 0 || obj.titleSearch !== void 0 || obj.numItems !== void 0)) {
3946
+ throw new Error(
3947
+ `${path}: continueCursor should be the only argument if present`
3948
+ );
3949
+ }
3950
+ return obj;
3951
+ })
3952
+ ]
3953
+ ],
3954
+ args
3955
+ );
3956
+ }
3889
3957
  // ================================
3890
3958
  // Save state / Remote File Storage
3891
3959
  // ================================
@@ -3903,9 +3971,10 @@ var WavedashSDK = class extends EventTarget {
3903
3971
  );
3904
3972
  }
3905
3973
  /**
3906
- * Downloads a remote file to a local location
3974
+ * Downloads a remote file to a local location.
3975
+ * Returns success=false (with the server status in `message`) if the file
3976
+ * doesn't exist or any other error occurs. See also: {@link remoteFileExists}
3907
3977
  * @param filePath - The path of the remote file to download
3908
- * @param downloadTo - Optionally provide a path to download the file to, defaults to the same path as the remote file
3909
3978
  * @returns The path of the local file that the remote file was downloaded to
3910
3979
  */
3911
3980
  async downloadRemoteFile(filePath) {
@@ -3916,6 +3985,19 @@ var WavedashSDK = class extends EventTarget {
3916
3985
  filePath
3917
3986
  );
3918
3987
  }
3988
+ /**
3989
+ * Checks whether a remote file exists. Sends a lightweight HEAD request to check for existence.
3990
+ * @param filePath - The path of the remote file to check
3991
+ * @returns true if the remote file exists, false if it does not.
3992
+ */
3993
+ async remoteFileExists(filePath) {
3994
+ return this.apiCall(
3995
+ this.fileSystemManager,
3996
+ "remoteFileExists",
3997
+ [["filePath", vString]],
3998
+ filePath
3999
+ );
4000
+ }
3919
4001
  /**
3920
4002
  * Uploads a local file to remote storage
3921
4003
  * @param filePath - The path of the local file to upload
@@ -4287,7 +4369,6 @@ var WavedashSDK = class extends EventTarget {
4287
4369
  // ==============================
4288
4370
  /**
4289
4371
  * Updates rich user presence so friends can see what the player is doing in game
4290
- * TODO: data param should be more strongly typed
4291
4372
  * @param data Game data to send to the backend
4292
4373
  * @returns true if the presence was updated successfully
4293
4374
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wvdsh/sdk-js",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
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.16",
52
+ "@wvdsh/api": "^0.1.27",
53
53
  "convex": "^1.38.0",
54
54
  "lodash.throttle": "^4.1.1"
55
55
  }