@playbasis-ai/qwikcard-sdk 2.3.14 → 2.3.15
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/CHANGELOG.md +6 -0
- package/dist/PlaybasisProvider.d.ts +3 -1
- package/dist/PlaybasisProvider.d.ts.map +1 -1
- package/dist/PlaybasisProvider.js +8 -2
- package/dist/QwikCardApp.d.ts +2 -0
- package/dist/QwikCardApp.d.ts.map +1 -1
- package/dist/api/client.d.ts +4 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +65 -38
- package/package.json +1 -1
- package/src/PlaybasisProvider.tsx +13 -2
- package/src/QwikCardApp.tsx +2 -0
- package/src/api/client.ts +70 -39
package/CHANGELOG.md
CHANGED
|
@@ -67,6 +67,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
67
67
|
|
|
68
68
|
- Fallback badge image mapping for Qwik slugs when API omits image URLs.
|
|
69
69
|
|
|
70
|
+
## [2.3.15] - 2026-01-30
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- Lightweight request retries/timeouts with opt-in configuration for smoother client UX.
|
|
75
|
+
|
|
70
76
|
## [2.3.11] - 2026-01-29
|
|
71
77
|
|
|
72
78
|
### Changed
|
|
@@ -12,8 +12,10 @@ interface PlaybasisProviderProps {
|
|
|
12
12
|
tenantId?: string;
|
|
13
13
|
playerId?: string;
|
|
14
14
|
baseUrl?: string;
|
|
15
|
+
requestTimeoutMs?: number;
|
|
16
|
+
requestRetries?: number;
|
|
15
17
|
}
|
|
16
|
-
export declare function PlaybasisProvider({ children, apiKey, tenantId, playerId, baseUrl, }: PlaybasisProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare function PlaybasisProvider({ children, apiKey, tenantId, playerId, baseUrl, requestTimeoutMs, requestRetries, }: PlaybasisProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
17
19
|
export declare function usePlaybasis(): PlaybasisContextValue;
|
|
18
20
|
export default PlaybasisProvider;
|
|
19
21
|
//# sourceMappingURL=PlaybasisProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlaybasisProvider.d.ts","sourceRoot":"","sources":["../src/PlaybasisProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAO/C,UAAU,qBAAqB;IAC7B,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"PlaybasisProvider.d.ts","sourceRoot":"","sources":["../src/PlaybasisProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAO/C,UAAU,qBAAqB;IAC7B,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAiBD,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,gBAAgB,EAChB,cAAc,GACf,EAAE,sBAAsB,2CA+BxB;AAMD,wBAAgB,YAAY,IAAI,qBAAqB,CAMpD;AAED,eAAe,iBAAiB,CAAC"}
|
|
@@ -17,10 +17,16 @@ const resolveBaseUrl = (tenantId, baseUrl) => {
|
|
|
17
17
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
18
|
// Provider Component
|
|
19
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
-
export function PlaybasisProvider({ children, apiKey, tenantId, playerId, baseUrl, }) {
|
|
20
|
+
export function PlaybasisProvider({ children, apiKey, tenantId, playerId, baseUrl, requestTimeoutMs, requestRetries, }) {
|
|
21
21
|
const resolvedTenantId = tenantId ?? (baseUrl && baseUrl.includes('production') ? 'qwik-prod' : 'qwikcard');
|
|
22
22
|
const resolvedBaseUrl = resolveBaseUrl(resolvedTenantId, baseUrl);
|
|
23
|
-
const client = useMemo(() => new PlaybasisClient({
|
|
23
|
+
const client = useMemo(() => new PlaybasisClient({
|
|
24
|
+
apiKey,
|
|
25
|
+
tenantId: resolvedTenantId,
|
|
26
|
+
baseUrl: resolvedBaseUrl,
|
|
27
|
+
timeoutMs: requestTimeoutMs,
|
|
28
|
+
retries: requestRetries,
|
|
29
|
+
}), [apiKey, resolvedTenantId, resolvedBaseUrl, requestTimeoutMs, requestRetries]);
|
|
24
30
|
const value = useMemo(() => ({
|
|
25
31
|
client,
|
|
26
32
|
tenantId: resolvedTenantId,
|
package/dist/QwikCardApp.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface QwikCardAppProps {
|
|
|
4
4
|
playerId: string;
|
|
5
5
|
baseUrl?: string;
|
|
6
6
|
leaderboardId?: string;
|
|
7
|
+
requestTimeoutMs?: number;
|
|
8
|
+
requestRetries?: number;
|
|
7
9
|
}
|
|
8
10
|
export declare function QwikCardApp(props: QwikCardAppProps): import("react/jsx-runtime").JSX.Element;
|
|
9
11
|
//# sourceMappingURL=QwikCardApp.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QwikCardApp.d.ts","sourceRoot":"","sources":["../src/QwikCardApp.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"QwikCardApp.d.ts","sourceRoot":"","sources":["../src/QwikCardApp.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAwRD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,2CAMlD"}
|
package/dist/api/client.d.ts
CHANGED
|
@@ -4,12 +4,16 @@ interface ClientConfig {
|
|
|
4
4
|
tenantId: string;
|
|
5
5
|
baseUrl?: string;
|
|
6
6
|
fetchImpl?: typeof fetch;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
retries?: number;
|
|
7
9
|
}
|
|
8
10
|
export declare class PlaybasisClient {
|
|
9
11
|
private readonly baseUrl;
|
|
10
12
|
private readonly fetchImpl;
|
|
11
13
|
private readonly apiKey;
|
|
12
14
|
private readonly tenantId;
|
|
15
|
+
private readonly timeoutMs;
|
|
16
|
+
private readonly retries;
|
|
13
17
|
constructor(config: ClientConfig);
|
|
14
18
|
/**
|
|
15
19
|
* Generate a unique ID for idempotency keys.
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,gBAAgB,EAChB,MAAM,EACN,UAAU,EACV,YAAY,EACZ,KAAK,EACL,gBAAgB,EAChB,MAAM,EACN,eAAe,EAChB,MAAM,UAAU,CAAC;AAMlB,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
|
package/dist/api/client.js
CHANGED
|
@@ -8,6 +8,8 @@ export class PlaybasisClient {
|
|
|
8
8
|
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
9
9
|
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
10
10
|
this.apiKey = config.apiKey;
|
|
11
|
+
this.timeoutMs = config.timeoutMs ?? 10000;
|
|
12
|
+
this.retries = config.retries ?? 2;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Generate a unique ID for idempotency keys.
|
|
@@ -22,49 +24,74 @@ export class PlaybasisClient {
|
|
|
22
24
|
}
|
|
23
25
|
async request(method, path, options) {
|
|
24
26
|
const url = `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
39
|
-
};
|
|
40
|
-
const response = await this.fetchImpl(url, init);
|
|
41
|
-
const text = await response.text();
|
|
42
|
-
// Safely parse JSON - server may return non-JSON (e.g., HTML error page)
|
|
43
|
-
let json;
|
|
44
|
-
if (text) {
|
|
27
|
+
const attemptRequest = async () => {
|
|
28
|
+
const headers = {
|
|
29
|
+
'Ocp-Apim-Subscription-Key': this.apiKey,
|
|
30
|
+
'X-Tenant-ID': this.tenantId,
|
|
31
|
+
...options?.headers,
|
|
32
|
+
};
|
|
33
|
+
if (options?.body !== undefined) {
|
|
34
|
+
headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
|
|
35
|
+
}
|
|
36
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
37
|
+
const timeoutId = this.timeoutMs
|
|
38
|
+
? setTimeout(() => controller?.abort(), this.timeoutMs)
|
|
39
|
+
: null;
|
|
45
40
|
try {
|
|
46
|
-
|
|
41
|
+
const init = {
|
|
42
|
+
method,
|
|
43
|
+
headers,
|
|
44
|
+
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
45
|
+
signal: controller?.signal,
|
|
46
|
+
};
|
|
47
|
+
const response = await this.fetchImpl(url, init);
|
|
48
|
+
const text = await response.text();
|
|
49
|
+
let json;
|
|
50
|
+
if (text) {
|
|
51
|
+
try {
|
|
52
|
+
json = JSON.parse(text);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
json = undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const getErrorMessage = (value) => {
|
|
59
|
+
if (!value || typeof value !== 'object')
|
|
60
|
+
return undefined;
|
|
61
|
+
if (!('message' in value))
|
|
62
|
+
return undefined;
|
|
63
|
+
const messageValue = value.message;
|
|
64
|
+
return typeof messageValue === 'string' ? messageValue : undefined;
|
|
65
|
+
};
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorMessage = getErrorMessage(json) ||
|
|
68
|
+
(text && !json ? text.slice(0, 200) : null) ||
|
|
69
|
+
`Request failed: ${response.status} ${response.statusText}`;
|
|
70
|
+
const error = new Error(String(errorMessage));
|
|
71
|
+
error.status = response.status;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return json;
|
|
47
75
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
finally {
|
|
77
|
+
if (timeoutId)
|
|
78
|
+
clearTimeout(timeoutId);
|
|
51
79
|
}
|
|
52
|
-
}
|
|
53
|
-
const getErrorMessage = (value) => {
|
|
54
|
-
if (!value || typeof value !== 'object')
|
|
55
|
-
return undefined;
|
|
56
|
-
if (!('message' in value))
|
|
57
|
-
return undefined;
|
|
58
|
-
const messageValue = value.message;
|
|
59
|
-
return typeof messageValue === 'string' ? messageValue : undefined;
|
|
60
80
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
for (let attempt = 0; attempt <= this.retries; attempt += 1) {
|
|
82
|
+
try {
|
|
83
|
+
return await attemptRequest();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const status = error.status;
|
|
87
|
+
const shouldRetry = attempt < this.retries &&
|
|
88
|
+
(status === undefined || status === 429 || (status >= 500 && status <= 599));
|
|
89
|
+
if (!shouldRetry)
|
|
90
|
+
throw error;
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1)));
|
|
92
|
+
}
|
|
66
93
|
}
|
|
67
|
-
|
|
94
|
+
throw new Error('Request failed after retries');
|
|
68
95
|
}
|
|
69
96
|
// ─────────────────────────────────────────────────────────────────────────
|
|
70
97
|
// Players
|
package/package.json
CHANGED
|
@@ -27,6 +27,8 @@ interface PlaybasisProviderProps {
|
|
|
27
27
|
tenantId?: string;
|
|
28
28
|
playerId?: string;
|
|
29
29
|
baseUrl?: string;
|
|
30
|
+
requestTimeoutMs?: number;
|
|
31
|
+
requestRetries?: number;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const QWIK_BASE_URLS = {
|
|
@@ -50,13 +52,22 @@ export function PlaybasisProvider({
|
|
|
50
52
|
tenantId,
|
|
51
53
|
playerId,
|
|
52
54
|
baseUrl,
|
|
55
|
+
requestTimeoutMs,
|
|
56
|
+
requestRetries,
|
|
53
57
|
}: PlaybasisProviderProps) {
|
|
54
58
|
const resolvedTenantId =
|
|
55
59
|
tenantId ?? (baseUrl && baseUrl.includes('production') ? 'qwik-prod' : 'qwikcard');
|
|
56
60
|
const resolvedBaseUrl = resolveBaseUrl(resolvedTenantId, baseUrl);
|
|
57
61
|
const client = useMemo(
|
|
58
|
-
() =>
|
|
59
|
-
|
|
62
|
+
() =>
|
|
63
|
+
new PlaybasisClient({
|
|
64
|
+
apiKey,
|
|
65
|
+
tenantId: resolvedTenantId,
|
|
66
|
+
baseUrl: resolvedBaseUrl,
|
|
67
|
+
timeoutMs: requestTimeoutMs,
|
|
68
|
+
retries: requestRetries,
|
|
69
|
+
}),
|
|
70
|
+
[apiKey, resolvedTenantId, resolvedBaseUrl, requestTimeoutMs, requestRetries],
|
|
60
71
|
);
|
|
61
72
|
|
|
62
73
|
const value = useMemo(
|
package/src/QwikCardApp.tsx
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -19,6 +19,8 @@ interface ClientConfig {
|
|
|
19
19
|
tenantId: string;
|
|
20
20
|
baseUrl?: string;
|
|
21
21
|
fetchImpl?: typeof fetch;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
retries?: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const DEFAULT_BASE_URL = 'https://apim-pb-staging.azure-api.net/playbasis/v1';
|
|
@@ -32,12 +34,16 @@ export class PlaybasisClient {
|
|
|
32
34
|
private readonly fetchImpl: typeof fetch;
|
|
33
35
|
private readonly apiKey: string;
|
|
34
36
|
private readonly tenantId: string;
|
|
37
|
+
private readonly timeoutMs: number;
|
|
38
|
+
private readonly retries: number;
|
|
35
39
|
|
|
36
40
|
constructor(config: ClientConfig) {
|
|
37
41
|
this.tenantId = config.tenantId;
|
|
38
42
|
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
39
43
|
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
40
44
|
this.apiKey = config.apiKey;
|
|
45
|
+
this.timeoutMs = config.timeoutMs ?? 10000;
|
|
46
|
+
this.retries = config.retries ?? 2;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
/**
|
|
@@ -59,54 +65,79 @@ export class PlaybasisClient {
|
|
|
59
65
|
): Promise<T> {
|
|
60
66
|
const url = `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Set Content-Type before assigning to init to avoid mutation after reference
|
|
70
|
-
if (options?.body !== undefined) {
|
|
71
|
-
headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
|
|
72
|
-
}
|
|
68
|
+
const attemptRequest = async (): Promise<T> => {
|
|
69
|
+
const headers: Record<string, string> = {
|
|
70
|
+
'Ocp-Apim-Subscription-Key': this.apiKey,
|
|
71
|
+
'X-Tenant-ID': this.tenantId,
|
|
72
|
+
...options?.headers,
|
|
73
|
+
};
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
78
|
-
};
|
|
75
|
+
if (options?.body !== undefined) {
|
|
76
|
+
headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
|
|
77
|
+
}
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
80
|
+
const timeoutId = this.timeoutMs
|
|
81
|
+
? setTimeout(() => controller?.abort(), this.timeoutMs)
|
|
82
|
+
: null;
|
|
82
83
|
|
|
83
|
-
// Safely parse JSON - server may return non-JSON (e.g., HTML error page)
|
|
84
|
-
let json: unknown;
|
|
85
|
-
if (text) {
|
|
86
84
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
const init: RequestInit = {
|
|
86
|
+
method,
|
|
87
|
+
headers,
|
|
88
|
+
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
89
|
+
signal: controller?.signal,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const response = await this.fetchImpl(url, init);
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
|
|
95
|
+
let json: unknown;
|
|
96
|
+
if (text) {
|
|
97
|
+
try {
|
|
98
|
+
json = JSON.parse(text);
|
|
99
|
+
} catch {
|
|
100
|
+
json = undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const getErrorMessage = (value: unknown): string | undefined => {
|
|
105
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
106
|
+
if (!('message' in value)) return undefined;
|
|
107
|
+
const messageValue = (value as { message?: unknown }).message;
|
|
108
|
+
return typeof messageValue === 'string' ? messageValue : undefined;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const errorMessage =
|
|
113
|
+
getErrorMessage(json) ||
|
|
114
|
+
(text && !json ? text.slice(0, 200) : null) ||
|
|
115
|
+
`Request failed: ${response.status} ${response.statusText}`;
|
|
116
|
+
const error = new Error(String(errorMessage)) as Error & { status?: number };
|
|
117
|
+
error.status = response.status;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return json as T;
|
|
122
|
+
} finally {
|
|
123
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
91
124
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const getErrorMessage = (value: unknown): string | undefined => {
|
|
95
|
-
if (!value || typeof value !== 'object') return undefined;
|
|
96
|
-
if (!('message' in value)) return undefined;
|
|
97
|
-
const messageValue = (value as { message?: unknown }).message;
|
|
98
|
-
return typeof messageValue === 'string' ? messageValue : undefined;
|
|
99
125
|
};
|
|
100
126
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
for (let attempt = 0; attempt <= this.retries; attempt += 1) {
|
|
128
|
+
try {
|
|
129
|
+
return await attemptRequest();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const status = (error as { status?: number }).status;
|
|
132
|
+
const shouldRetry =
|
|
133
|
+
attempt < this.retries &&
|
|
134
|
+
(status === undefined || status === 429 || (status >= 500 && status <= 599));
|
|
135
|
+
if (!shouldRetry) throw error;
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1)));
|
|
137
|
+
}
|
|
107
138
|
}
|
|
108
139
|
|
|
109
|
-
|
|
140
|
+
throw new Error('Request failed after retries');
|
|
110
141
|
}
|
|
111
142
|
|
|
112
143
|
// ─────────────────────────────────────────────────────────────────────────
|