@playcademy/sdk 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +19 -1
- package/dist/index.js +173 -43
- package/dist/internal.d.ts +183 -152
- package/dist/internal.js +175 -43
- package/dist/server.js +12 -0
- package/dist/types.d.ts +6 -3
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,21 @@ import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
|
|
|
7
7
|
declare class PlaycademyError extends Error {
|
|
8
8
|
constructor(message: string);
|
|
9
9
|
}
|
|
10
|
+
type CorsProbeResult = 'reachable' | 'unreachable' | 'skipped';
|
|
11
|
+
interface ManifestErrorDetails {
|
|
12
|
+
manifestUrl: string;
|
|
13
|
+
manifestHost: string;
|
|
14
|
+
deploymentUrl: string;
|
|
15
|
+
fetchOutcome: 'network_error' | 'bad_status' | 'invalid_body';
|
|
16
|
+
retryCount: number;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
fetchErrorMessage?: string;
|
|
19
|
+
corsProbe?: CorsProbeResult;
|
|
20
|
+
status?: number;
|
|
21
|
+
contentType?: string;
|
|
22
|
+
cfRay?: string;
|
|
23
|
+
redirected?: boolean;
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* Error codes returned by the API.
|
|
12
27
|
* These map to specific error types and HTTP status codes.
|
|
@@ -855,6 +870,7 @@ declare abstract class PlaycademyBaseClient {
|
|
|
855
870
|
};
|
|
856
871
|
protected initPayload?: InitPayload;
|
|
857
872
|
protected connectionManager?: ConnectionManager;
|
|
873
|
+
protected launchId?: string;
|
|
858
874
|
/**
|
|
859
875
|
* Internal session manager for automatic session lifecycle.
|
|
860
876
|
* @private
|
|
@@ -883,6 +899,7 @@ declare abstract class PlaycademyBaseClient {
|
|
|
883
899
|
* Sets the authentication token for API requests.
|
|
884
900
|
*/
|
|
885
901
|
setToken(token: string | null, tokenType?: TokenType): void;
|
|
902
|
+
setLaunchId(launchId: string | null | undefined): void;
|
|
886
903
|
/**
|
|
887
904
|
* Gets the current token type.
|
|
888
905
|
*/
|
|
@@ -1308,6 +1325,7 @@ interface ClientConfig {
|
|
|
1308
1325
|
token?: string;
|
|
1309
1326
|
tokenType?: TokenType;
|
|
1310
1327
|
gameId?: string;
|
|
1328
|
+
launchId?: string;
|
|
1311
1329
|
autoStartSession?: boolean;
|
|
1312
1330
|
onDisconnect?: DisconnectHandler;
|
|
1313
1331
|
enableConnectionMonitoring?: boolean;
|
|
@@ -2301,4 +2319,4 @@ declare class PlaycademyMessaging {
|
|
|
2301
2319
|
declare const messaging: PlaycademyMessaging;
|
|
2302
2320
|
|
|
2303
2321
|
export { ApiError, ConnectionManager, ConnectionMonitor, MessageEvents, PlaycademyClient, PlaycademyError, extractApiErrorInfo, messaging };
|
|
2304
|
-
export type { ApiErrorCode, ApiErrorInfo, ConnectionMonitorConfig, ConnectionState, ConnectionStatePayload, DevUploadEvent, DevUploadHooks, DisconnectContext, DisconnectHandler, DisplayAlertPayload, ErrorResponseBody };
|
|
2322
|
+
export type { ApiErrorCode, ApiErrorInfo, ConnectionMonitorConfig, ConnectionState, ConnectionStatePayload, DevUploadEvent, DevUploadHooks, DisconnectContext, DisconnectHandler, DisplayAlertPayload, ErrorResponseBody, ManifestErrorDetails };
|
package/dist/index.js
CHANGED
|
@@ -407,6 +407,18 @@ class PlaycademyError extends Error {
|
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
class ManifestError extends PlaycademyError {
|
|
411
|
+
kind;
|
|
412
|
+
details;
|
|
413
|
+
constructor(message, kind, details) {
|
|
414
|
+
super(message);
|
|
415
|
+
this.name = "ManifestError";
|
|
416
|
+
this.kind = kind;
|
|
417
|
+
this.details = details;
|
|
418
|
+
Object.setPrototypeOf(this, ManifestError.prototype);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
410
422
|
class ApiError extends Error {
|
|
411
423
|
code;
|
|
412
424
|
details;
|
|
@@ -1915,7 +1927,158 @@ class ConnectionManager {
|
|
|
1915
1927
|
}
|
|
1916
1928
|
}
|
|
1917
1929
|
}
|
|
1918
|
-
// src/core/
|
|
1930
|
+
// src/core/transport/retry.ts
|
|
1931
|
+
var RETRY_DELAYS_MS = [500, 1500];
|
|
1932
|
+
function wait(ms) {
|
|
1933
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1934
|
+
}
|
|
1935
|
+
var retryRuntime = { wait };
|
|
1936
|
+
function isRetryableStatus(status) {
|
|
1937
|
+
return status === 429 || status >= 500;
|
|
1938
|
+
}
|
|
1939
|
+
async function fetchWithRetry(url, init2) {
|
|
1940
|
+
const startedAt = Date.now();
|
|
1941
|
+
for (let attempt = 0;attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
1942
|
+
const retryDelayMs = RETRY_DELAYS_MS[attempt];
|
|
1943
|
+
const canRetry = init2.method === "GET" && retryDelayMs !== undefined;
|
|
1944
|
+
try {
|
|
1945
|
+
const response = await fetch(url, init2);
|
|
1946
|
+
if (canRetry && isRetryableStatus(response.status)) {
|
|
1947
|
+
await retryRuntime.wait(retryDelayMs);
|
|
1948
|
+
} else {
|
|
1949
|
+
return { response, retryCount: attempt, durationMs: Date.now() - startedAt };
|
|
1950
|
+
}
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
if (canRetry && error instanceof TypeError) {
|
|
1953
|
+
await retryRuntime.wait(retryDelayMs);
|
|
1954
|
+
} else {
|
|
1955
|
+
return { error, retryCount: attempt, durationMs: Date.now() - startedAt };
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return {
|
|
1960
|
+
error: new PlaycademyError("Request failed after exhausting retries"),
|
|
1961
|
+
retryCount: RETRY_DELAYS_MS.length,
|
|
1962
|
+
durationMs: Date.now() - startedAt
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function unwrapFetchResult(result) {
|
|
1966
|
+
if (result.error) {
|
|
1967
|
+
throw result.error;
|
|
1968
|
+
}
|
|
1969
|
+
if (!result.response) {
|
|
1970
|
+
throw new PlaycademyError("Request failed after exhausting retries");
|
|
1971
|
+
}
|
|
1972
|
+
return result.response;
|
|
1973
|
+
}
|
|
1974
|
+
// src/core/transport/manifest.ts
|
|
1975
|
+
var CORS_PROBE_TIMEOUT_MS = 3000;
|
|
1976
|
+
async function probeCorsReachability(url) {
|
|
1977
|
+
if (typeof globalThis.AbortController === "undefined") {
|
|
1978
|
+
return "skipped";
|
|
1979
|
+
}
|
|
1980
|
+
const controller = new AbortController;
|
|
1981
|
+
const timer = setTimeout(() => controller.abort(), CORS_PROBE_TIMEOUT_MS);
|
|
1982
|
+
try {
|
|
1983
|
+
await fetch(url, { mode: "no-cors", signal: controller.signal });
|
|
1984
|
+
return "reachable";
|
|
1985
|
+
} catch {
|
|
1986
|
+
return "unreachable";
|
|
1987
|
+
} finally {
|
|
1988
|
+
clearTimeout(timer);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
var MAX_FETCH_ERROR_MESSAGE_LENGTH = 512;
|
|
1992
|
+
function getFetchErrorMessage(error) {
|
|
1993
|
+
let raw;
|
|
1994
|
+
if (error instanceof Error) {
|
|
1995
|
+
raw = error.message;
|
|
1996
|
+
} else if (typeof error === "string") {
|
|
1997
|
+
raw = error;
|
|
1998
|
+
}
|
|
1999
|
+
if (!raw) {
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
2003
|
+
if (!normalized) {
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
return normalized.slice(0, MAX_FETCH_ERROR_MESSAGE_LENGTH);
|
|
2007
|
+
}
|
|
2008
|
+
async function fetchManifest(deploymentUrl) {
|
|
2009
|
+
const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
|
|
2010
|
+
const result = await fetchWithRetry(manifestUrl, { method: "GET" });
|
|
2011
|
+
let manifestHost;
|
|
2012
|
+
try {
|
|
2013
|
+
manifestHost = new URL(manifestUrl).host;
|
|
2014
|
+
} catch {
|
|
2015
|
+
manifestHost = manifestUrl;
|
|
2016
|
+
}
|
|
2017
|
+
const base = {
|
|
2018
|
+
manifestUrl,
|
|
2019
|
+
manifestHost,
|
|
2020
|
+
deploymentUrl,
|
|
2021
|
+
retryCount: result.retryCount,
|
|
2022
|
+
durationMs: result.durationMs
|
|
2023
|
+
};
|
|
2024
|
+
if (result.error || !result.response) {
|
|
2025
|
+
log.error(`[Playcademy SDK] Error fetching manifest from ${manifestUrl}:`, {
|
|
2026
|
+
error: result.error
|
|
2027
|
+
});
|
|
2028
|
+
throw new ManifestError("Failed to load game manifest", "temporary", {
|
|
2029
|
+
...base,
|
|
2030
|
+
fetchOutcome: "network_error",
|
|
2031
|
+
...result.error ? { fetchErrorMessage: getFetchErrorMessage(result.error) } : {}
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
const response = result.response;
|
|
2035
|
+
if (!response.ok) {
|
|
2036
|
+
log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
|
|
2037
|
+
throw new ManifestError(`Failed to fetch manifest: ${response.status} ${response.statusText}`, isRetryableStatus(response.status) ? "temporary" : "permanent", {
|
|
2038
|
+
...base,
|
|
2039
|
+
fetchOutcome: "bad_status",
|
|
2040
|
+
status: response.status,
|
|
2041
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
2042
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
2043
|
+
redirected: response.redirected
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
try {
|
|
2047
|
+
return await response.json();
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
log.error(`[Playcademy SDK] Error parsing manifest from ${manifestUrl}:`, {
|
|
2050
|
+
error
|
|
2051
|
+
});
|
|
2052
|
+
throw new ManifestError("Failed to parse game manifest", "permanent", {
|
|
2053
|
+
...base,
|
|
2054
|
+
fetchOutcome: "invalid_body",
|
|
2055
|
+
status: response.status,
|
|
2056
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
2057
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
2058
|
+
redirected: response.redirected
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
// src/core/transport/request.ts
|
|
2063
|
+
function prepareRequestBody(body, headers) {
|
|
2064
|
+
if (body instanceof FormData) {
|
|
2065
|
+
return body;
|
|
2066
|
+
}
|
|
2067
|
+
if (body instanceof ArrayBuffer || body instanceof Blob || ArrayBuffer.isView(body)) {
|
|
2068
|
+
if (!headers["Content-Type"]) {
|
|
2069
|
+
headers["Content-Type"] = "application/octet-stream";
|
|
2070
|
+
}
|
|
2071
|
+
return body;
|
|
2072
|
+
}
|
|
2073
|
+
if (body !== undefined && body !== null) {
|
|
2074
|
+
if (headers["Content-Type"]) {
|
|
2075
|
+
return typeof body === "string" ? body : JSON.stringify(body);
|
|
2076
|
+
}
|
|
2077
|
+
headers["Content-Type"] = "application/json";
|
|
2078
|
+
return JSON.stringify(body);
|
|
2079
|
+
}
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
1919
2082
|
function checkDevWarnings(data) {
|
|
1920
2083
|
if (!data || typeof data !== "object") {
|
|
1921
2084
|
return;
|
|
@@ -1943,25 +2106,6 @@ function checkDevWarnings(data) {
|
|
|
1943
2106
|
}
|
|
1944
2107
|
}
|
|
1945
2108
|
}
|
|
1946
|
-
function prepareRequestBody(body, headers) {
|
|
1947
|
-
if (body instanceof FormData) {
|
|
1948
|
-
return body;
|
|
1949
|
-
}
|
|
1950
|
-
if (body instanceof ArrayBuffer || body instanceof Blob || ArrayBuffer.isView(body)) {
|
|
1951
|
-
if (!headers["Content-Type"]) {
|
|
1952
|
-
headers["Content-Type"] = "application/octet-stream";
|
|
1953
|
-
}
|
|
1954
|
-
return body;
|
|
1955
|
-
}
|
|
1956
|
-
if (body !== undefined && body !== null) {
|
|
1957
|
-
if (headers["Content-Type"]) {
|
|
1958
|
-
return typeof body === "string" ? body : JSON.stringify(body);
|
|
1959
|
-
}
|
|
1960
|
-
headers["Content-Type"] = "application/json";
|
|
1961
|
-
return JSON.stringify(body);
|
|
1962
|
-
}
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
2109
|
async function request({
|
|
1966
2110
|
path,
|
|
1967
2111
|
baseUrl,
|
|
@@ -1973,12 +2117,12 @@ async function request({
|
|
|
1973
2117
|
const url = baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : `/${path}`);
|
|
1974
2118
|
const headers = { ...extraHeaders };
|
|
1975
2119
|
const payload = prepareRequestBody(body, headers);
|
|
1976
|
-
const res = await
|
|
2120
|
+
const res = unwrapFetchResult(await fetchWithRetry(url, {
|
|
1977
2121
|
method,
|
|
1978
2122
|
headers,
|
|
1979
2123
|
body: payload,
|
|
1980
2124
|
credentials: "omit"
|
|
1981
|
-
});
|
|
2125
|
+
}));
|
|
1982
2126
|
if (raw) {
|
|
1983
2127
|
return res;
|
|
1984
2128
|
}
|
|
@@ -2008,26 +2152,6 @@ async function request({
|
|
|
2008
2152
|
const rawText = await res.text().catch(() => "");
|
|
2009
2153
|
return rawText && rawText.length > 0 ? rawText : undefined;
|
|
2010
2154
|
}
|
|
2011
|
-
async function fetchManifest(deploymentUrl) {
|
|
2012
|
-
const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
|
|
2013
|
-
try {
|
|
2014
|
-
const response = await fetch(manifestUrl);
|
|
2015
|
-
if (!response.ok) {
|
|
2016
|
-
log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
|
|
2017
|
-
throw new PlaycademyError(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
2018
|
-
}
|
|
2019
|
-
return await response.json();
|
|
2020
|
-
} catch (error) {
|
|
2021
|
-
if (error instanceof PlaycademyError) {
|
|
2022
|
-
throw error;
|
|
2023
|
-
}
|
|
2024
|
-
log.error(`[Playcademy SDK] Error fetching or parsing manifest from ${manifestUrl}:`, {
|
|
2025
|
-
error
|
|
2026
|
-
});
|
|
2027
|
-
throw new PlaycademyError("Failed to load or parse game manifest");
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
2155
|
// src/clients/base.ts
|
|
2032
2156
|
class PlaycademyBaseClient {
|
|
2033
2157
|
baseUrl;
|
|
@@ -2040,6 +2164,7 @@ class PlaycademyBaseClient {
|
|
|
2040
2164
|
authContext;
|
|
2041
2165
|
initPayload;
|
|
2042
2166
|
connectionManager;
|
|
2167
|
+
launchId;
|
|
2043
2168
|
_sessionManager = {
|
|
2044
2169
|
startSession: async (gameId) => this.request(`/games/${gameId}/sessions`, "POST"),
|
|
2045
2170
|
endSession: async (sessionId, gameId) => this.request(`/games/${gameId}/sessions/${sessionId}`, "DELETE")
|
|
@@ -2048,6 +2173,7 @@ class PlaycademyBaseClient {
|
|
|
2048
2173
|
this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
|
|
2049
2174
|
this.gameUrl = config?.gameUrl;
|
|
2050
2175
|
this.gameId = config?.gameId;
|
|
2176
|
+
this.launchId = config?.launchId ?? undefined;
|
|
2051
2177
|
this.config = config || {};
|
|
2052
2178
|
this.authStrategy = createAuthStrategy(config?.token ?? null, config?.tokenType);
|
|
2053
2179
|
this._detectAuthContext();
|
|
@@ -2075,6 +2201,9 @@ class PlaycademyBaseClient {
|
|
|
2075
2201
|
this.authStrategy = createAuthStrategy(token, tokenType);
|
|
2076
2202
|
this.emit("authChange", { token });
|
|
2077
2203
|
}
|
|
2204
|
+
setLaunchId(launchId) {
|
|
2205
|
+
this.launchId = launchId ?? undefined;
|
|
2206
|
+
}
|
|
2078
2207
|
getTokenType() {
|
|
2079
2208
|
return this.authStrategy.getType();
|
|
2080
2209
|
}
|
|
@@ -2117,7 +2246,8 @@ class PlaycademyBaseClient {
|
|
|
2117
2246
|
async request(path, method, options) {
|
|
2118
2247
|
const effectiveHeaders = {
|
|
2119
2248
|
...options?.headers,
|
|
2120
|
-
...this.authStrategy.getHeaders()
|
|
2249
|
+
...this.authStrategy.getHeaders(),
|
|
2250
|
+
...this.launchId ? { "x-playcademy-launch-id": this.launchId } : {}
|
|
2121
2251
|
};
|
|
2122
2252
|
try {
|
|
2123
2253
|
const result = await request({
|
package/dist/internal.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { z } from 'zod';
|
|
|
5
5
|
import { SchemaInfo } from '@playcademy/cloudflare';
|
|
6
6
|
import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
|
|
7
7
|
|
|
8
|
+
/** Permitted HTTP verbs */
|
|
9
|
+
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Game Types
|
|
10
13
|
*
|
|
@@ -82,8 +85,181 @@ interface DomainValidationRecords {
|
|
|
82
85
|
}[];
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Base error class for Cademy SDK specific errors.
|
|
90
|
+
*/
|
|
91
|
+
declare class PlaycademyError extends Error {
|
|
92
|
+
constructor(message: string);
|
|
93
|
+
}
|
|
94
|
+
type ManifestErrorKind = 'temporary' | 'permanent';
|
|
95
|
+
type CorsProbeResult = 'reachable' | 'unreachable' | 'skipped';
|
|
96
|
+
interface ManifestErrorDetails {
|
|
97
|
+
manifestUrl: string;
|
|
98
|
+
manifestHost: string;
|
|
99
|
+
deploymentUrl: string;
|
|
100
|
+
fetchOutcome: 'network_error' | 'bad_status' | 'invalid_body';
|
|
101
|
+
retryCount: number;
|
|
102
|
+
durationMs: number;
|
|
103
|
+
fetchErrorMessage?: string;
|
|
104
|
+
corsProbe?: CorsProbeResult;
|
|
105
|
+
status?: number;
|
|
106
|
+
contentType?: string;
|
|
107
|
+
cfRay?: string;
|
|
108
|
+
redirected?: boolean;
|
|
109
|
+
}
|
|
110
|
+
declare class ManifestError extends PlaycademyError {
|
|
111
|
+
readonly kind: ManifestErrorKind;
|
|
112
|
+
readonly details: ManifestErrorDetails | undefined;
|
|
113
|
+
constructor(message: string, kind: ManifestErrorKind, details?: ManifestErrorDetails);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Error codes returned by the API.
|
|
117
|
+
* These map to specific error types and HTTP status codes.
|
|
118
|
+
*/
|
|
119
|
+
type ApiErrorCode = 'BAD_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'ACCESS_DENIED' | 'NOT_FOUND' | 'METHOD_NOT_ALLOWED' | 'CONFLICT' | 'ALREADY_EXISTS' | 'GONE' | 'PRECONDITION_FAILED' | 'PAYLOAD_TOO_LARGE' | 'VALIDATION_FAILED' | 'TOO_MANY_REQUESTS' | 'RATE_LIMITED' | 'EXPIRED' | 'INTERNAL' | 'INTERNAL_ERROR' | 'NOT_IMPLEMENTED' | 'SERVICE_UNAVAILABLE' | 'TIMEOUT' | string;
|
|
120
|
+
/**
|
|
121
|
+
* Structure of error response bodies returned by API endpoints.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```json
|
|
125
|
+
* {
|
|
126
|
+
* "error": {
|
|
127
|
+
* "code": "NOT_FOUND",
|
|
128
|
+
* "message": "Item not found",
|
|
129
|
+
* "details": { "identifier": "abc123" }
|
|
130
|
+
* }
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
interface ErrorResponseBody {
|
|
135
|
+
error?: {
|
|
136
|
+
code?: string;
|
|
137
|
+
message?: string;
|
|
138
|
+
details?: unknown;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* API error thrown when a request fails.
|
|
143
|
+
*
|
|
144
|
+
* Contains structured error information from the API response:
|
|
145
|
+
* - `status` - HTTP status code (e.g., 404)
|
|
146
|
+
* - `code` - API error code (e.g., "NOT_FOUND")
|
|
147
|
+
* - `message` - Human-readable error message
|
|
148
|
+
* - `details` - Optional additional error context
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* try {
|
|
153
|
+
* await client.games.get('nonexistent')
|
|
154
|
+
* } catch (error) {
|
|
155
|
+
* if (error instanceof ApiError) {
|
|
156
|
+
* console.log(error.status) // 404
|
|
157
|
+
* console.log(error.code) // "NOT_FOUND"
|
|
158
|
+
* console.log(error.message) // "Game not found"
|
|
159
|
+
* console.log(error.details) // { identifier: "nonexistent" }
|
|
160
|
+
* }
|
|
161
|
+
* }
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
declare class ApiError extends Error {
|
|
165
|
+
/**
|
|
166
|
+
* API error code (e.g., "NOT_FOUND", "VALIDATION_FAILED").
|
|
167
|
+
* Use this for programmatic error handling.
|
|
168
|
+
*/
|
|
169
|
+
readonly code: ApiErrorCode;
|
|
170
|
+
/**
|
|
171
|
+
* Additional error context from the API.
|
|
172
|
+
* Structure varies by error type (e.g., validation errors include field details).
|
|
173
|
+
*/
|
|
174
|
+
readonly details: unknown;
|
|
175
|
+
/**
|
|
176
|
+
* Raw response body for debugging.
|
|
177
|
+
* @internal
|
|
178
|
+
*/
|
|
179
|
+
readonly rawBody: unknown;
|
|
180
|
+
readonly status: number;
|
|
181
|
+
constructor(
|
|
182
|
+
/** HTTP status code */
|
|
183
|
+
status: number,
|
|
184
|
+
/** API error code */
|
|
185
|
+
code: ApiErrorCode,
|
|
186
|
+
/** Human-readable error message */
|
|
187
|
+
message: string,
|
|
188
|
+
/** Additional error context */
|
|
189
|
+
details?: unknown,
|
|
190
|
+
/** Raw response body */
|
|
191
|
+
rawBody?: unknown);
|
|
192
|
+
/**
|
|
193
|
+
* Create an ApiError from an HTTP response.
|
|
194
|
+
* Parses the structured error response from the API.
|
|
195
|
+
*
|
|
196
|
+
* @internal
|
|
197
|
+
*/
|
|
198
|
+
static fromResponse(status: number, statusText: string, body: unknown): ApiError;
|
|
199
|
+
/**
|
|
200
|
+
* Check if this is a specific error type.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* if (error.is('NOT_FOUND')) {
|
|
205
|
+
* // Handle not found
|
|
206
|
+
* } else if (error.is('VALIDATION_FAILED')) {
|
|
207
|
+
* // Handle validation error
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
is(code: ApiErrorCode): boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Check if this is a client error (4xx).
|
|
214
|
+
*/
|
|
215
|
+
isClientError(): boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Check if this is a server error (5xx).
|
|
218
|
+
*/
|
|
219
|
+
isServerError(): boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Check if this error is retryable.
|
|
222
|
+
* Server errors and rate limits are typically retryable.
|
|
223
|
+
*/
|
|
224
|
+
isRetryable(): boolean;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Extracted error information for display purposes.
|
|
228
|
+
*/
|
|
229
|
+
interface ApiErrorInfo {
|
|
230
|
+
/** HTTP status code */
|
|
231
|
+
status: number;
|
|
232
|
+
/** API error code */
|
|
233
|
+
code: ApiErrorCode;
|
|
234
|
+
/** Human-readable error message */
|
|
235
|
+
message: string;
|
|
236
|
+
/** Additional error context */
|
|
237
|
+
details?: unknown;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Extract useful error information from an API error.
|
|
241
|
+
* Useful for displaying errors to users in a friendly way.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* try {
|
|
246
|
+
* await client.shop.purchase(itemId)
|
|
247
|
+
* } catch (error) {
|
|
248
|
+
* const info = extractApiErrorInfo(error)
|
|
249
|
+
* if (info) {
|
|
250
|
+
* showToast(`Error: ${info.message}`)
|
|
251
|
+
* }
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
declare function extractApiErrorInfo(error: unknown): ApiErrorInfo | null;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Fire-and-forget reachability check using `no-cors` mode.
|
|
259
|
+
* Exported for use by the app telemetry layer — not awaited inside
|
|
260
|
+
* fetchManifest so it doesn't block the error screen.
|
|
261
|
+
*/
|
|
262
|
+
declare function probeCorsReachability(url: string): Promise<CorsProbeResult>;
|
|
87
263
|
|
|
88
264
|
/**
|
|
89
265
|
* User Types
|
|
@@ -935,154 +1111,6 @@ interface SpriteConfigWithDimensions {
|
|
|
935
1111
|
animations: Record<string, SpriteAnimationFrame>;
|
|
936
1112
|
}
|
|
937
1113
|
|
|
938
|
-
/**
|
|
939
|
-
* Base error class for Cademy SDK specific errors.
|
|
940
|
-
*/
|
|
941
|
-
declare class PlaycademyError extends Error {
|
|
942
|
-
constructor(message: string);
|
|
943
|
-
}
|
|
944
|
-
/**
|
|
945
|
-
* Error codes returned by the API.
|
|
946
|
-
* These map to specific error types and HTTP status codes.
|
|
947
|
-
*/
|
|
948
|
-
type ApiErrorCode = 'BAD_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'ACCESS_DENIED' | 'NOT_FOUND' | 'METHOD_NOT_ALLOWED' | 'CONFLICT' | 'ALREADY_EXISTS' | 'GONE' | 'PRECONDITION_FAILED' | 'PAYLOAD_TOO_LARGE' | 'VALIDATION_FAILED' | 'TOO_MANY_REQUESTS' | 'RATE_LIMITED' | 'EXPIRED' | 'INTERNAL' | 'INTERNAL_ERROR' | 'NOT_IMPLEMENTED' | 'SERVICE_UNAVAILABLE' | 'TIMEOUT' | string;
|
|
949
|
-
/**
|
|
950
|
-
* Structure of error response bodies returned by API endpoints.
|
|
951
|
-
*
|
|
952
|
-
* @example
|
|
953
|
-
* ```json
|
|
954
|
-
* {
|
|
955
|
-
* "error": {
|
|
956
|
-
* "code": "NOT_FOUND",
|
|
957
|
-
* "message": "Item not found",
|
|
958
|
-
* "details": { "identifier": "abc123" }
|
|
959
|
-
* }
|
|
960
|
-
* }
|
|
961
|
-
* ```
|
|
962
|
-
*/
|
|
963
|
-
interface ErrorResponseBody {
|
|
964
|
-
error?: {
|
|
965
|
-
code?: string;
|
|
966
|
-
message?: string;
|
|
967
|
-
details?: unknown;
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* API error thrown when a request fails.
|
|
972
|
-
*
|
|
973
|
-
* Contains structured error information from the API response:
|
|
974
|
-
* - `status` - HTTP status code (e.g., 404)
|
|
975
|
-
* - `code` - API error code (e.g., "NOT_FOUND")
|
|
976
|
-
* - `message` - Human-readable error message
|
|
977
|
-
* - `details` - Optional additional error context
|
|
978
|
-
*
|
|
979
|
-
* @example
|
|
980
|
-
* ```typescript
|
|
981
|
-
* try {
|
|
982
|
-
* await client.games.get('nonexistent')
|
|
983
|
-
* } catch (error) {
|
|
984
|
-
* if (error instanceof ApiError) {
|
|
985
|
-
* console.log(error.status) // 404
|
|
986
|
-
* console.log(error.code) // "NOT_FOUND"
|
|
987
|
-
* console.log(error.message) // "Game not found"
|
|
988
|
-
* console.log(error.details) // { identifier: "nonexistent" }
|
|
989
|
-
* }
|
|
990
|
-
* }
|
|
991
|
-
* ```
|
|
992
|
-
*/
|
|
993
|
-
declare class ApiError extends Error {
|
|
994
|
-
/**
|
|
995
|
-
* API error code (e.g., "NOT_FOUND", "VALIDATION_FAILED").
|
|
996
|
-
* Use this for programmatic error handling.
|
|
997
|
-
*/
|
|
998
|
-
readonly code: ApiErrorCode;
|
|
999
|
-
/**
|
|
1000
|
-
* Additional error context from the API.
|
|
1001
|
-
* Structure varies by error type (e.g., validation errors include field details).
|
|
1002
|
-
*/
|
|
1003
|
-
readonly details: unknown;
|
|
1004
|
-
/**
|
|
1005
|
-
* Raw response body for debugging.
|
|
1006
|
-
* @internal
|
|
1007
|
-
*/
|
|
1008
|
-
readonly rawBody: unknown;
|
|
1009
|
-
readonly status: number;
|
|
1010
|
-
constructor(
|
|
1011
|
-
/** HTTP status code */
|
|
1012
|
-
status: number,
|
|
1013
|
-
/** API error code */
|
|
1014
|
-
code: ApiErrorCode,
|
|
1015
|
-
/** Human-readable error message */
|
|
1016
|
-
message: string,
|
|
1017
|
-
/** Additional error context */
|
|
1018
|
-
details?: unknown,
|
|
1019
|
-
/** Raw response body */
|
|
1020
|
-
rawBody?: unknown);
|
|
1021
|
-
/**
|
|
1022
|
-
* Create an ApiError from an HTTP response.
|
|
1023
|
-
* Parses the structured error response from the API.
|
|
1024
|
-
*
|
|
1025
|
-
* @internal
|
|
1026
|
-
*/
|
|
1027
|
-
static fromResponse(status: number, statusText: string, body: unknown): ApiError;
|
|
1028
|
-
/**
|
|
1029
|
-
* Check if this is a specific error type.
|
|
1030
|
-
*
|
|
1031
|
-
* @example
|
|
1032
|
-
* ```typescript
|
|
1033
|
-
* if (error.is('NOT_FOUND')) {
|
|
1034
|
-
* // Handle not found
|
|
1035
|
-
* } else if (error.is('VALIDATION_FAILED')) {
|
|
1036
|
-
* // Handle validation error
|
|
1037
|
-
* }
|
|
1038
|
-
* ```
|
|
1039
|
-
*/
|
|
1040
|
-
is(code: ApiErrorCode): boolean;
|
|
1041
|
-
/**
|
|
1042
|
-
* Check if this is a client error (4xx).
|
|
1043
|
-
*/
|
|
1044
|
-
isClientError(): boolean;
|
|
1045
|
-
/**
|
|
1046
|
-
* Check if this is a server error (5xx).
|
|
1047
|
-
*/
|
|
1048
|
-
isServerError(): boolean;
|
|
1049
|
-
/**
|
|
1050
|
-
* Check if this error is retryable.
|
|
1051
|
-
* Server errors and rate limits are typically retryable.
|
|
1052
|
-
*/
|
|
1053
|
-
isRetryable(): boolean;
|
|
1054
|
-
}
|
|
1055
|
-
/**
|
|
1056
|
-
* Extracted error information for display purposes.
|
|
1057
|
-
*/
|
|
1058
|
-
interface ApiErrorInfo {
|
|
1059
|
-
/** HTTP status code */
|
|
1060
|
-
status: number;
|
|
1061
|
-
/** API error code */
|
|
1062
|
-
code: ApiErrorCode;
|
|
1063
|
-
/** Human-readable error message */
|
|
1064
|
-
message: string;
|
|
1065
|
-
/** Additional error context */
|
|
1066
|
-
details?: unknown;
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* Extract useful error information from an API error.
|
|
1070
|
-
* Useful for displaying errors to users in a friendly way.
|
|
1071
|
-
*
|
|
1072
|
-
* @example
|
|
1073
|
-
* ```typescript
|
|
1074
|
-
* try {
|
|
1075
|
-
* await client.shop.purchase(itemId)
|
|
1076
|
-
* } catch (error) {
|
|
1077
|
-
* const info = extractApiErrorInfo(error)
|
|
1078
|
-
* if (info) {
|
|
1079
|
-
* showToast(`Error: ${info.message}`)
|
|
1080
|
-
* }
|
|
1081
|
-
* }
|
|
1082
|
-
* ```
|
|
1083
|
-
*/
|
|
1084
|
-
declare function extractApiErrorInfo(error: unknown): ApiErrorInfo | null;
|
|
1085
|
-
|
|
1086
1114
|
/**
|
|
1087
1115
|
* Connection monitoring types
|
|
1088
1116
|
*
|
|
@@ -5629,6 +5657,7 @@ declare abstract class PlaycademyBaseClient {
|
|
|
5629
5657
|
};
|
|
5630
5658
|
protected initPayload?: InitPayload;
|
|
5631
5659
|
protected connectionManager?: ConnectionManager;
|
|
5660
|
+
protected launchId?: string;
|
|
5632
5661
|
/**
|
|
5633
5662
|
* Internal session manager for automatic session lifecycle.
|
|
5634
5663
|
* @private
|
|
@@ -5657,6 +5686,7 @@ declare abstract class PlaycademyBaseClient {
|
|
|
5657
5686
|
* Sets the authentication token for API requests.
|
|
5658
5687
|
*/
|
|
5659
5688
|
setToken(token: string | null, tokenType?: TokenType): void;
|
|
5689
|
+
setLaunchId(launchId: string | null | undefined): void;
|
|
5660
5690
|
/**
|
|
5661
5691
|
* Gets the current token type.
|
|
5662
5692
|
*/
|
|
@@ -5954,6 +5984,7 @@ interface ClientConfig {
|
|
|
5954
5984
|
token?: string;
|
|
5955
5985
|
tokenType?: TokenType;
|
|
5956
5986
|
gameId?: string;
|
|
5987
|
+
launchId?: string;
|
|
5957
5988
|
autoStartSession?: boolean;
|
|
5958
5989
|
onDisconnect?: DisconnectHandler;
|
|
5959
5990
|
enableConnectionMonitoring?: boolean;
|
|
@@ -7629,5 +7660,5 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
|
|
|
7629
7660
|
};
|
|
7630
7661
|
}
|
|
7631
7662
|
|
|
7632
|
-
export { AchievementCompletionType, ApiError, ConnectionManager, ConnectionMonitor, MessageEvents, NotificationStatus, NotificationType, PlaycademyInternalClient as PlaycademyClient, PlaycademyError, PlaycademyInternalClient, extractApiErrorInfo, messaging };
|
|
7633
|
-
export type { AchievementCurrent, AchievementHistoryEntry, AchievementProgressResponse, AchievementScopeType, AchievementWithStatus, ApiErrorCode, ApiErrorInfo, AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, AuthenticatedUser, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, CharacterComponentRow as CharacterComponent, CharacterComponentType, CharacterComponentWithSpriteUrl, CharacterComponentsOptions, ClientConfig, ClientEvents, ConnectionMonitorConfig, ConnectionState, ConnectionStatePayload, CourseXp, CreateCharacterData, CreateMapObjectData, CurrencyRow as Currency, DevUploadEvent, DevUploadHooks, DeveloperStatusEnumType, DeveloperStatusResponse, DeveloperStatusValue, DisconnectContext, DisconnectHandler, DisplayAlertPayload, ErrorResponseBody, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameLeaderboardEntry, MapRow as GameMap, GamePlatform, GameRow as GameRecord, GameSessionRow as GameSession, GameTimebackIntegration, GameTokenResponse, GameType, GameUser, GetXpOptions, HostedGame, InitPayload, InsertCurrencyInput, InsertItemInput, InsertShopListingInput, InteractionType, InventoryItemRow as InventoryItem, InventoryItemWithItem, InventoryMutationResponse, ItemRow as Item, ItemType, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LeaderboardEntry, LeaderboardOptions, LeaderboardTimeframe, LevelConfigRow as LevelConfig, LevelProgressResponse, LevelUpCheckResult, LoginResponse, ManifestV1, MapData, MapElementRow as MapElement, MapElementMetadata, MapElementWithGame, MapObjectRow as MapObject, MapObjectWithItem, NotificationRow as Notification, NotificationStats, PlaceableItemMetadata, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyServerClientConfig, PlaycademyServerClientState, PlayerCharacterRow as PlayerCharacter, PlayerCharacterAccessoryRow as PlayerCharacterAccessory, PlayerCurrency, PlayerInventoryItem, PlayerProfile, PlayerSessionPayload, PopulateStudentResponse, RealtimeTokenResponse, ScoreSubmission, ShopCurrency, ShopDisplayItem, ShopListingRow as ShopListing, ShopViewResponse, SpriteAnimationFrame, SpriteConfigWithDimensions, SpriteTemplateRow as SpriteTemplate, SpriteTemplateData, StartSessionResponse, TelemetryPayload, TimebackEnrollment, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserXp, TodayXpResponse, TokenRefreshPayload, TokenType, TotalXpResponse, UpdateCharacterData, UpdateCurrencyInput, UpdateItemInput, UpdateShopListingInput, UpsertGameMetadataInput, UserRow as User, UserEnrollment, UserInfo, UserLevelRow as UserLevel, UserLevelWithConfig, UserOrganization, UserRank, UserRankResponse, UserRoleEnumType, UserScore, UserTimebackData, XPAddResult, XpHistoryResponse, XpResponse, XpSummaryResponse };
|
|
7663
|
+
export { AchievementCompletionType, ApiError, ConnectionManager, ConnectionMonitor, ManifestError, MessageEvents, NotificationStatus, NotificationType, PlaycademyInternalClient as PlaycademyClient, PlaycademyError, PlaycademyInternalClient, extractApiErrorInfo, messaging, probeCorsReachability };
|
|
7664
|
+
export type { AchievementCurrent, AchievementHistoryEntry, AchievementProgressResponse, AchievementScopeType, AchievementWithStatus, ApiErrorCode, ApiErrorInfo, AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, AuthenticatedUser, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, CharacterComponentRow as CharacterComponent, CharacterComponentType, CharacterComponentWithSpriteUrl, CharacterComponentsOptions, ClientConfig, ClientEvents, ConnectionMonitorConfig, ConnectionState, ConnectionStatePayload, CorsProbeResult, CourseXp, CreateCharacterData, CreateMapObjectData, CurrencyRow as Currency, DevUploadEvent, DevUploadHooks, DeveloperStatusEnumType, DeveloperStatusResponse, DeveloperStatusValue, DisconnectContext, DisconnectHandler, DisplayAlertPayload, ErrorResponseBody, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameLeaderboardEntry, MapRow as GameMap, GamePlatform, GameRow as GameRecord, GameSessionRow as GameSession, GameTimebackIntegration, GameTokenResponse, GameType, GameUser, GetXpOptions, HostedGame, InitPayload, InsertCurrencyInput, InsertItemInput, InsertShopListingInput, InteractionType, InventoryItemRow as InventoryItem, InventoryItemWithItem, InventoryMutationResponse, ItemRow as Item, ItemType, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LeaderboardEntry, LeaderboardOptions, LeaderboardTimeframe, LevelConfigRow as LevelConfig, LevelProgressResponse, LevelUpCheckResult, LoginResponse, ManifestErrorDetails, ManifestV1, MapData, MapElementRow as MapElement, MapElementMetadata, MapElementWithGame, MapObjectRow as MapObject, MapObjectWithItem, NotificationRow as Notification, NotificationStats, PlaceableItemMetadata, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyServerClientConfig, PlaycademyServerClientState, PlayerCharacterRow as PlayerCharacter, PlayerCharacterAccessoryRow as PlayerCharacterAccessory, PlayerCurrency, PlayerInventoryItem, PlayerProfile, PlayerSessionPayload, PopulateStudentResponse, RealtimeTokenResponse, ScoreSubmission, ShopCurrency, ShopDisplayItem, ShopListingRow as ShopListing, ShopViewResponse, SpriteAnimationFrame, SpriteConfigWithDimensions, SpriteTemplateRow as SpriteTemplate, SpriteTemplateData, StartSessionResponse, TelemetryPayload, TimebackEnrollment, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserXp, TodayXpResponse, TokenRefreshPayload, TokenType, TotalXpResponse, UpdateCharacterData, UpdateCurrencyInput, UpdateItemInput, UpdateShopListingInput, UpsertGameMetadataInput, UserRow as User, UserEnrollment, UserInfo, UserLevelRow as UserLevel, UserLevelWithConfig, UserOrganization, UserRank, UserRankResponse, UserRoleEnumType, UserScore, UserTimebackData, XPAddResult, XpHistoryResponse, XpResponse, XpSummaryResponse };
|
package/dist/internal.js
CHANGED
|
@@ -407,6 +407,18 @@ class PlaycademyError extends Error {
|
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
class ManifestError extends PlaycademyError {
|
|
411
|
+
kind;
|
|
412
|
+
details;
|
|
413
|
+
constructor(message, kind, details) {
|
|
414
|
+
super(message);
|
|
415
|
+
this.name = "ManifestError";
|
|
416
|
+
this.kind = kind;
|
|
417
|
+
this.details = details;
|
|
418
|
+
Object.setPrototypeOf(this, ManifestError.prototype);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
410
422
|
class ApiError extends Error {
|
|
411
423
|
code;
|
|
412
424
|
details;
|
|
@@ -2026,7 +2038,158 @@ function createDevNamespace(client) {
|
|
|
2026
2038
|
}
|
|
2027
2039
|
};
|
|
2028
2040
|
}
|
|
2029
|
-
// src/core/
|
|
2041
|
+
// src/core/transport/retry.ts
|
|
2042
|
+
var RETRY_DELAYS_MS = [500, 1500];
|
|
2043
|
+
function wait(ms) {
|
|
2044
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2045
|
+
}
|
|
2046
|
+
var retryRuntime = { wait };
|
|
2047
|
+
function isRetryableStatus(status) {
|
|
2048
|
+
return status === 429 || status >= 500;
|
|
2049
|
+
}
|
|
2050
|
+
async function fetchWithRetry(url, init2) {
|
|
2051
|
+
const startedAt = Date.now();
|
|
2052
|
+
for (let attempt = 0;attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
2053
|
+
const retryDelayMs = RETRY_DELAYS_MS[attempt];
|
|
2054
|
+
const canRetry = init2.method === "GET" && retryDelayMs !== undefined;
|
|
2055
|
+
try {
|
|
2056
|
+
const response = await fetch(url, init2);
|
|
2057
|
+
if (canRetry && isRetryableStatus(response.status)) {
|
|
2058
|
+
await retryRuntime.wait(retryDelayMs);
|
|
2059
|
+
} else {
|
|
2060
|
+
return { response, retryCount: attempt, durationMs: Date.now() - startedAt };
|
|
2061
|
+
}
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
if (canRetry && error instanceof TypeError) {
|
|
2064
|
+
await retryRuntime.wait(retryDelayMs);
|
|
2065
|
+
} else {
|
|
2066
|
+
return { error, retryCount: attempt, durationMs: Date.now() - startedAt };
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return {
|
|
2071
|
+
error: new PlaycademyError("Request failed after exhausting retries"),
|
|
2072
|
+
retryCount: RETRY_DELAYS_MS.length,
|
|
2073
|
+
durationMs: Date.now() - startedAt
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
function unwrapFetchResult(result) {
|
|
2077
|
+
if (result.error) {
|
|
2078
|
+
throw result.error;
|
|
2079
|
+
}
|
|
2080
|
+
if (!result.response) {
|
|
2081
|
+
throw new PlaycademyError("Request failed after exhausting retries");
|
|
2082
|
+
}
|
|
2083
|
+
return result.response;
|
|
2084
|
+
}
|
|
2085
|
+
// src/core/transport/manifest.ts
|
|
2086
|
+
var CORS_PROBE_TIMEOUT_MS = 3000;
|
|
2087
|
+
async function probeCorsReachability(url) {
|
|
2088
|
+
if (typeof globalThis.AbortController === "undefined") {
|
|
2089
|
+
return "skipped";
|
|
2090
|
+
}
|
|
2091
|
+
const controller = new AbortController;
|
|
2092
|
+
const timer = setTimeout(() => controller.abort(), CORS_PROBE_TIMEOUT_MS);
|
|
2093
|
+
try {
|
|
2094
|
+
await fetch(url, { mode: "no-cors", signal: controller.signal });
|
|
2095
|
+
return "reachable";
|
|
2096
|
+
} catch {
|
|
2097
|
+
return "unreachable";
|
|
2098
|
+
} finally {
|
|
2099
|
+
clearTimeout(timer);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
var MAX_FETCH_ERROR_MESSAGE_LENGTH = 512;
|
|
2103
|
+
function getFetchErrorMessage(error) {
|
|
2104
|
+
let raw;
|
|
2105
|
+
if (error instanceof Error) {
|
|
2106
|
+
raw = error.message;
|
|
2107
|
+
} else if (typeof error === "string") {
|
|
2108
|
+
raw = error;
|
|
2109
|
+
}
|
|
2110
|
+
if (!raw) {
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
2114
|
+
if (!normalized) {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
return normalized.slice(0, MAX_FETCH_ERROR_MESSAGE_LENGTH);
|
|
2118
|
+
}
|
|
2119
|
+
async function fetchManifest(deploymentUrl) {
|
|
2120
|
+
const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
|
|
2121
|
+
const result = await fetchWithRetry(manifestUrl, { method: "GET" });
|
|
2122
|
+
let manifestHost;
|
|
2123
|
+
try {
|
|
2124
|
+
manifestHost = new URL(manifestUrl).host;
|
|
2125
|
+
} catch {
|
|
2126
|
+
manifestHost = manifestUrl;
|
|
2127
|
+
}
|
|
2128
|
+
const base = {
|
|
2129
|
+
manifestUrl,
|
|
2130
|
+
manifestHost,
|
|
2131
|
+
deploymentUrl,
|
|
2132
|
+
retryCount: result.retryCount,
|
|
2133
|
+
durationMs: result.durationMs
|
|
2134
|
+
};
|
|
2135
|
+
if (result.error || !result.response) {
|
|
2136
|
+
log.error(`[Playcademy SDK] Error fetching manifest from ${manifestUrl}:`, {
|
|
2137
|
+
error: result.error
|
|
2138
|
+
});
|
|
2139
|
+
throw new ManifestError("Failed to load game manifest", "temporary", {
|
|
2140
|
+
...base,
|
|
2141
|
+
fetchOutcome: "network_error",
|
|
2142
|
+
...result.error ? { fetchErrorMessage: getFetchErrorMessage(result.error) } : {}
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
const response = result.response;
|
|
2146
|
+
if (!response.ok) {
|
|
2147
|
+
log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
|
|
2148
|
+
throw new ManifestError(`Failed to fetch manifest: ${response.status} ${response.statusText}`, isRetryableStatus(response.status) ? "temporary" : "permanent", {
|
|
2149
|
+
...base,
|
|
2150
|
+
fetchOutcome: "bad_status",
|
|
2151
|
+
status: response.status,
|
|
2152
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
2153
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
2154
|
+
redirected: response.redirected
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
try {
|
|
2158
|
+
return await response.json();
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
log.error(`[Playcademy SDK] Error parsing manifest from ${manifestUrl}:`, {
|
|
2161
|
+
error
|
|
2162
|
+
});
|
|
2163
|
+
throw new ManifestError("Failed to parse game manifest", "permanent", {
|
|
2164
|
+
...base,
|
|
2165
|
+
fetchOutcome: "invalid_body",
|
|
2166
|
+
status: response.status,
|
|
2167
|
+
contentType: response.headers.get("content-type") ?? undefined,
|
|
2168
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
2169
|
+
redirected: response.redirected
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
// src/core/transport/request.ts
|
|
2174
|
+
function prepareRequestBody(body, headers) {
|
|
2175
|
+
if (body instanceof FormData) {
|
|
2176
|
+
return body;
|
|
2177
|
+
}
|
|
2178
|
+
if (body instanceof ArrayBuffer || body instanceof Blob || ArrayBuffer.isView(body)) {
|
|
2179
|
+
if (!headers["Content-Type"]) {
|
|
2180
|
+
headers["Content-Type"] = "application/octet-stream";
|
|
2181
|
+
}
|
|
2182
|
+
return body;
|
|
2183
|
+
}
|
|
2184
|
+
if (body !== undefined && body !== null) {
|
|
2185
|
+
if (headers["Content-Type"]) {
|
|
2186
|
+
return typeof body === "string" ? body : JSON.stringify(body);
|
|
2187
|
+
}
|
|
2188
|
+
headers["Content-Type"] = "application/json";
|
|
2189
|
+
return JSON.stringify(body);
|
|
2190
|
+
}
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2030
2193
|
function checkDevWarnings(data) {
|
|
2031
2194
|
if (!data || typeof data !== "object") {
|
|
2032
2195
|
return;
|
|
@@ -2054,25 +2217,6 @@ function checkDevWarnings(data) {
|
|
|
2054
2217
|
}
|
|
2055
2218
|
}
|
|
2056
2219
|
}
|
|
2057
|
-
function prepareRequestBody(body, headers) {
|
|
2058
|
-
if (body instanceof FormData) {
|
|
2059
|
-
return body;
|
|
2060
|
-
}
|
|
2061
|
-
if (body instanceof ArrayBuffer || body instanceof Blob || ArrayBuffer.isView(body)) {
|
|
2062
|
-
if (!headers["Content-Type"]) {
|
|
2063
|
-
headers["Content-Type"] = "application/octet-stream";
|
|
2064
|
-
}
|
|
2065
|
-
return body;
|
|
2066
|
-
}
|
|
2067
|
-
if (body !== undefined && body !== null) {
|
|
2068
|
-
if (headers["Content-Type"]) {
|
|
2069
|
-
return typeof body === "string" ? body : JSON.stringify(body);
|
|
2070
|
-
}
|
|
2071
|
-
headers["Content-Type"] = "application/json";
|
|
2072
|
-
return JSON.stringify(body);
|
|
2073
|
-
}
|
|
2074
|
-
return;
|
|
2075
|
-
}
|
|
2076
2220
|
async function request({
|
|
2077
2221
|
path,
|
|
2078
2222
|
baseUrl,
|
|
@@ -2084,12 +2228,12 @@ async function request({
|
|
|
2084
2228
|
const url = baseUrl.replace(/\/$/, "") + (path.startsWith("/") ? path : `/${path}`);
|
|
2085
2229
|
const headers = { ...extraHeaders };
|
|
2086
2230
|
const payload = prepareRequestBody(body, headers);
|
|
2087
|
-
const res = await
|
|
2231
|
+
const res = unwrapFetchResult(await fetchWithRetry(url, {
|
|
2088
2232
|
method,
|
|
2089
2233
|
headers,
|
|
2090
2234
|
body: payload,
|
|
2091
2235
|
credentials: "omit"
|
|
2092
|
-
});
|
|
2236
|
+
}));
|
|
2093
2237
|
if (raw) {
|
|
2094
2238
|
return res;
|
|
2095
2239
|
}
|
|
@@ -2119,26 +2263,6 @@ async function request({
|
|
|
2119
2263
|
const rawText = await res.text().catch(() => "");
|
|
2120
2264
|
return rawText && rawText.length > 0 ? rawText : undefined;
|
|
2121
2265
|
}
|
|
2122
|
-
async function fetchManifest(deploymentUrl) {
|
|
2123
|
-
const manifestUrl = `${deploymentUrl.replace(/\/$/, "")}/playcademy.manifest.json`;
|
|
2124
|
-
try {
|
|
2125
|
-
const response = await fetch(manifestUrl);
|
|
2126
|
-
if (!response.ok) {
|
|
2127
|
-
log.error(`[fetchManifest] Failed to fetch manifest from ${manifestUrl}. Status: ${response.status}`);
|
|
2128
|
-
throw new PlaycademyError(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
2129
|
-
}
|
|
2130
|
-
return await response.json();
|
|
2131
|
-
} catch (error) {
|
|
2132
|
-
if (error instanceof PlaycademyError) {
|
|
2133
|
-
throw error;
|
|
2134
|
-
}
|
|
2135
|
-
log.error(`[Playcademy SDK] Error fetching or parsing manifest from ${manifestUrl}:`, {
|
|
2136
|
-
error
|
|
2137
|
-
});
|
|
2138
|
-
throw new PlaycademyError("Failed to load or parse game manifest");
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
2266
|
// src/namespaces/platform/games.ts
|
|
2143
2267
|
function createGamesNamespace(client) {
|
|
2144
2268
|
const gamesListCache = createTTLCache({
|
|
@@ -2956,6 +3080,7 @@ class PlaycademyBaseClient {
|
|
|
2956
3080
|
authContext;
|
|
2957
3081
|
initPayload;
|
|
2958
3082
|
connectionManager;
|
|
3083
|
+
launchId;
|
|
2959
3084
|
_sessionManager = {
|
|
2960
3085
|
startSession: async (gameId) => this.request(`/games/${gameId}/sessions`, "POST"),
|
|
2961
3086
|
endSession: async (sessionId, gameId) => this.request(`/games/${gameId}/sessions/${sessionId}`, "DELETE")
|
|
@@ -2964,6 +3089,7 @@ class PlaycademyBaseClient {
|
|
|
2964
3089
|
this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
|
|
2965
3090
|
this.gameUrl = config?.gameUrl;
|
|
2966
3091
|
this.gameId = config?.gameId;
|
|
3092
|
+
this.launchId = config?.launchId ?? undefined;
|
|
2967
3093
|
this.config = config || {};
|
|
2968
3094
|
this.authStrategy = createAuthStrategy(config?.token ?? null, config?.tokenType);
|
|
2969
3095
|
this._detectAuthContext();
|
|
@@ -2991,6 +3117,9 @@ class PlaycademyBaseClient {
|
|
|
2991
3117
|
this.authStrategy = createAuthStrategy(token, tokenType);
|
|
2992
3118
|
this.emit("authChange", { token });
|
|
2993
3119
|
}
|
|
3120
|
+
setLaunchId(launchId) {
|
|
3121
|
+
this.launchId = launchId ?? undefined;
|
|
3122
|
+
}
|
|
2994
3123
|
getTokenType() {
|
|
2995
3124
|
return this.authStrategy.getType();
|
|
2996
3125
|
}
|
|
@@ -3033,7 +3162,8 @@ class PlaycademyBaseClient {
|
|
|
3033
3162
|
async request(path, method, options) {
|
|
3034
3163
|
const effectiveHeaders = {
|
|
3035
3164
|
...options?.headers,
|
|
3036
|
-
...this.authStrategy.getHeaders()
|
|
3165
|
+
...this.authStrategy.getHeaders(),
|
|
3166
|
+
...this.launchId ? { "x-playcademy-launch-id": this.launchId } : {}
|
|
3037
3167
|
};
|
|
3038
3168
|
try {
|
|
3039
3169
|
const result = await request({
|
|
@@ -3154,12 +3284,14 @@ class PlaycademyInternalClient extends PlaycademyBaseClient {
|
|
|
3154
3284
|
static identity = identity;
|
|
3155
3285
|
}
|
|
3156
3286
|
export {
|
|
3287
|
+
probeCorsReachability,
|
|
3157
3288
|
messaging,
|
|
3158
3289
|
extractApiErrorInfo,
|
|
3159
3290
|
PlaycademyInternalClient,
|
|
3160
3291
|
PlaycademyError,
|
|
3161
3292
|
PlaycademyInternalClient as PlaycademyClient,
|
|
3162
3293
|
MessageEvents,
|
|
3294
|
+
ManifestError,
|
|
3163
3295
|
ConnectionMonitor,
|
|
3164
3296
|
ConnectionManager,
|
|
3165
3297
|
ApiError
|
package/dist/server.js
CHANGED
|
@@ -81,6 +81,18 @@ class PlaycademyError extends Error {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
class ManifestError extends PlaycademyError {
|
|
85
|
+
kind;
|
|
86
|
+
details;
|
|
87
|
+
constructor(message, kind, details) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = "ManifestError";
|
|
90
|
+
this.kind = kind;
|
|
91
|
+
this.details = details;
|
|
92
|
+
Object.setPrototypeOf(this, ManifestError.prototype);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
class ApiError extends Error {
|
|
85
97
|
code;
|
|
86
98
|
details;
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ declare function parseOAuthState(state: string): {
|
|
|
19
19
|
data?: Record<string, string>;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
/** Permitted HTTP verbs */
|
|
23
|
+
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
24
|
+
|
|
22
25
|
/**
|
|
23
26
|
* Game Types
|
|
24
27
|
*
|
|
@@ -49,9 +52,6 @@ interface DomainValidationRecords {
|
|
|
49
52
|
}[];
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
/** Permitted HTTP verbs */
|
|
53
|
-
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
54
|
-
|
|
55
55
|
/**
|
|
56
56
|
* User Types
|
|
57
57
|
*
|
|
@@ -4735,6 +4735,7 @@ declare abstract class PlaycademyBaseClient {
|
|
|
4735
4735
|
};
|
|
4736
4736
|
protected initPayload?: InitPayload;
|
|
4737
4737
|
protected connectionManager?: ConnectionManager;
|
|
4738
|
+
protected launchId?: string;
|
|
4738
4739
|
/**
|
|
4739
4740
|
* Internal session manager for automatic session lifecycle.
|
|
4740
4741
|
* @private
|
|
@@ -4763,6 +4764,7 @@ declare abstract class PlaycademyBaseClient {
|
|
|
4763
4764
|
* Sets the authentication token for API requests.
|
|
4764
4765
|
*/
|
|
4765
4766
|
setToken(token: string | null, tokenType?: TokenType): void;
|
|
4767
|
+
setLaunchId(launchId: string | null | undefined): void;
|
|
4766
4768
|
/**
|
|
4767
4769
|
* Gets the current token type.
|
|
4768
4770
|
*/
|
|
@@ -5188,6 +5190,7 @@ interface ClientConfig {
|
|
|
5188
5190
|
token?: string;
|
|
5189
5191
|
tokenType?: TokenType;
|
|
5190
5192
|
gameId?: string;
|
|
5193
|
+
launchId?: string;
|
|
5191
5194
|
autoStartSession?: boolean;
|
|
5192
5195
|
onDisconnect?: DisconnectHandler;
|
|
5193
5196
|
enableConnectionMonitoring?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcademy/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "bun build.js",
|
|
38
|
+
"docs": "typedoc",
|
|
38
39
|
"pub": "bun publish.ts",
|
|
39
40
|
"test": "bun test",
|
|
40
|
-
"test:watch": "bun test --watch"
|
|
41
|
-
"docs": "typedoc"
|
|
41
|
+
"test:watch": "bun test --watch"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@inquirer/prompts": "^7.8.6",
|