@vxil/sdk 0.15.0 → 0.16.1
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 +94 -35
- package/dist/index.js +31 -5
- package/dist/retry.d.ts +10 -1
- package/dist/retry.js +17 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -414,8 +414,10 @@ export interface JobRun {
|
|
|
414
414
|
concurrency_limit?: number | null;
|
|
415
415
|
/** the fan-in batch the run joined (enqueue `batch_id`); null otherwise */
|
|
416
416
|
batch_id?: string | null;
|
|
417
|
-
/**
|
|
418
|
-
* dead-lettered `Expired` if not started by then)
|
|
417
|
+
/** the run's deadline. A plain run enqueued with `ttl_seconds`: its START
|
|
418
|
+
* deadline (dead-lettered `Expired` if not started by then). A generation
|
|
419
|
+
* run: its completion deadline (`timeout.after_ms`; past it the run ends
|
|
420
|
+
* `GenerationExpired` and any credit hold is released). Null otherwise. */
|
|
419
421
|
expires_at?: string | null;
|
|
420
422
|
/** the single-run read only: the debounce key of a debounced run */
|
|
421
423
|
debounce_key?: string | null;
|
|
@@ -865,6 +867,47 @@ export interface FileObject {
|
|
|
865
867
|
/** The object's public URL when it is published to the public asset host
|
|
866
868
|
* (`files.publish`), else null. */
|
|
867
869
|
public_url?: string | null;
|
|
870
|
+
/** When the object expires (null = never). An expired object is no longer
|
|
871
|
+
* listed and answers 404, even before the sweep deletes it. */
|
|
872
|
+
expires_at?: string | null;
|
|
873
|
+
}
|
|
874
|
+
/** `files.createUploadUrl` answer. */
|
|
875
|
+
export interface FileUploadUrl {
|
|
876
|
+
object_id: string;
|
|
877
|
+
upload_url: string;
|
|
878
|
+
upload_method: string;
|
|
879
|
+
/** Seconds the UPLOAD URL stays valid (`uploadUrlTtl`, default 900). */
|
|
880
|
+
expires_in: number;
|
|
881
|
+
/** When the OBJECT expires (null = never). */
|
|
882
|
+
expires_at: string | null;
|
|
883
|
+
}
|
|
884
|
+
/** `files.usage` answer. */
|
|
885
|
+
export interface FilesUsage {
|
|
886
|
+
usage: {
|
|
887
|
+
object_count: number;
|
|
888
|
+
total_bytes: number;
|
|
889
|
+
};
|
|
890
|
+
/** `maxTotalBytes` = the storage quota enforced now (see `storage_plan`). */
|
|
891
|
+
quotas: {
|
|
892
|
+
maxObjectCount?: number;
|
|
893
|
+
maxObjectBytes?: number;
|
|
894
|
+
maxTotalBytes?: number;
|
|
895
|
+
};
|
|
896
|
+
available: Record<string, number | null>;
|
|
897
|
+
/** Your plan's storage ceiling, and whether `quotas.maxTotalBytes` is that
|
|
898
|
+
* ceiling (`plan`) or your own lower value (`config`). */
|
|
899
|
+
storage_plan?: {
|
|
900
|
+
tier: string | null;
|
|
901
|
+
max_total_bytes: number;
|
|
902
|
+
source: 'plan' | 'config';
|
|
903
|
+
};
|
|
904
|
+
public_assets?: {
|
|
905
|
+
enabled: boolean;
|
|
906
|
+
published_objects: number;
|
|
907
|
+
published_bytes: number;
|
|
908
|
+
max_published_bytes: number;
|
|
909
|
+
bytes_remaining: number;
|
|
910
|
+
};
|
|
868
911
|
}
|
|
869
912
|
/** A published object (guide ch. 6, files, "Public asset delivery"): a stable,
|
|
870
913
|
* content-addressed URL on vxil's public asset host, served with
|
|
@@ -1167,13 +1210,23 @@ export interface JobGenerationSettledEventPayload {
|
|
|
1167
1210
|
/** `completed` on the healthy terminal, `failed` on the broken one */
|
|
1168
1211
|
status: 'completed' | 'failed';
|
|
1169
1212
|
/** the run's SETTLED error class (`provider_error`, `PollExhausted`,
|
|
1170
|
-
* `GenerationExpired`, `
|
|
1213
|
+
* `GenerationExpired`, `RetryableHttp`, `ReserveInsufficient`,
|
|
1214
|
+
* `PaymentsUnavailable`, `Cancelled`, …); always null on `completed`.
|
|
1215
|
+
* BRANCH ON THIS in a settle handler: `ReserveInsufficient` (the caller got
|
|
1216
|
+
* a 402), `PaymentsUnavailable` (the run was refused before it started —
|
|
1217
|
+
* usually the caller got a 503; a caller handed this run by a duplicate
|
|
1218
|
+
* request that raced it (202 `deduplicated`) learns it HERE and re-sends
|
|
1219
|
+
* the same request — a NEW run follows) and `Cancelled` are not render
|
|
1220
|
+
* failures. */
|
|
1171
1221
|
error_class: string | null;
|
|
1172
1222
|
/** the provider hint alone (`Upstream said: gemini 400: …`); null on
|
|
1173
1223
|
* `completed` and whenever the failure carried none */
|
|
1174
1224
|
error_hint: string | null;
|
|
1175
|
-
/** the failure-event vocabulary every `*.failed` carries (guide 12)
|
|
1176
|
-
|
|
1225
|
+
/** the failure-event vocabulary every `*.failed` carries (guide 12):
|
|
1226
|
+
* `completed` → info; `failed` → error, except a refusal at admission
|
|
1227
|
+
* (`ReserveInsufficient`, `PaymentsUnavailable`) → warn and a cancel
|
|
1228
|
+
* (`Cancelled`) → info, so neither pages through the immediate digest */
|
|
1229
|
+
level: 'info' | 'warn' | 'error';
|
|
1177
1230
|
state: 'ok' | 'broken';
|
|
1178
1231
|
}
|
|
1179
1232
|
/** The union a handler subscribed to `job.generation.` receives as `data`:
|
|
@@ -3176,7 +3229,10 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3176
3229
|
* FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
|
|
3177
3230
|
* the enqueue (insufficient balance; the run ends with job.generation.failed
|
|
3178
3231
|
* `ReserveInsufficient` and the 402 names its `run_id`); a runaway over the
|
|
3179
|
-
* per-tenant outstanding-holds ceiling → 429 (Retry-After 5).
|
|
3232
|
+
* per-tenant outstanding-holds ceiling → 429 (Retry-After 5). Payments
|
|
3233
|
+
* unreachable at the hold → 503 `payments_unavailable` (Retry-After 15;
|
|
3234
|
+
* nothing started, nothing held — re-send the same request), unless
|
|
3235
|
+
* `reserve_credits.on_unavailable: 'proceed'`.
|
|
3180
3236
|
*
|
|
3181
3237
|
* Generic completion options for queue-style providers (fal.ai and alike —
|
|
3182
3238
|
* no vendor adapter): `completion.callback.query_param` puts the signed
|
|
@@ -3193,7 +3249,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3193
3249
|
*
|
|
3194
3250
|
* `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
|
|
3195
3251
|
* (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
|
|
3196
|
-
* `queue_full`) honouring Retry-After
|
|
3252
|
+
* `queue_full`) and a 503 `payments_unavailable`, honouring Retry-After
|
|
3253
|
+
* with jitter, with the SAME
|
|
3197
3254
|
* idempotency_key (one is generated when the input has none), until
|
|
3198
3255
|
* maxWaitMs has passed — then the last 429 is thrown. */
|
|
3199
3256
|
generation: (input: {
|
|
@@ -3249,9 +3306,20 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3249
3306
|
amount: number;
|
|
3250
3307
|
user_id: string;
|
|
3251
3308
|
reason?: string;
|
|
3309
|
+
/** payments unreachable when the hold is placed (a 5xx other than
|
|
3310
|
+
* 501, a 429, no answer within 5 s; payments switched off is NOT
|
|
3311
|
+
* unreachable — that run starts without a hold): `'fail'` (default)
|
|
3312
|
+
* → 503 `payments_unavailable` + Retry-After, nothing started or
|
|
3313
|
+
* held — re-send the SAME request; `'proceed'` → start WITHOUT a
|
|
3314
|
+
* hold (a free run while payments is down) */
|
|
3315
|
+
on_unavailable?: "fail" | "proceed";
|
|
3252
3316
|
};
|
|
3253
3317
|
payload?: Record<string, unknown>;
|
|
3254
3318
|
idempotency_key?: string;
|
|
3319
|
+
/** START attempts = provider calls (1..20, default jobs.retry
|
|
3320
|
+
* .defaultMaxAttempts 5); a 408 / 429 / 5xx / network error retries
|
|
3321
|
+
* after min(2^n × 15, 600) s — or the provider's Retry-After on a
|
|
3322
|
+
* 429 / 503 (1 s..10 min, never past the deadline) */
|
|
3255
3323
|
max_attempts?: number;
|
|
3256
3324
|
}, opts?: {
|
|
3257
3325
|
retryOnCapacity?: {
|
|
@@ -4998,21 +5066,26 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
4998
5066
|
}) => Promise<WebSocket>;
|
|
4999
5067
|
};
|
|
5000
5068
|
readonly files: {
|
|
5001
|
-
/** Mint a presigned PUT; upload the bytes yourself, then call complete().
|
|
5069
|
+
/** Mint a presigned PUT; upload the bytes yourself, then call complete().
|
|
5070
|
+
* `expiresInSeconds` (60 s up to 100 years) makes the OBJECT expire — it is deleted that
|
|
5071
|
+
* long after the mint, whether or not `files.ttl` is enabled; `null` =
|
|
5072
|
+
* never. The answer's `expires_in` is the UPLOAD URL's life in seconds;
|
|
5073
|
+
* `expires_at` is when the object expires (null = never). `user_id` is
|
|
5074
|
+
* required with a server key and omitted in end-user mode. */
|
|
5002
5075
|
createUploadUrl: (input: {
|
|
5003
|
-
user_id
|
|
5076
|
+
user_id?: string;
|
|
5004
5077
|
filename: string;
|
|
5005
5078
|
content_type: string;
|
|
5006
5079
|
size_bytes: number;
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
expires_at: string;
|
|
5012
|
-
}>;
|
|
5080
|
+
expiresInSeconds?: number | null;
|
|
5081
|
+
}) => Promise<FileUploadUrl>;
|
|
5082
|
+
/** Confirm the PUT (bytes verified, object → available). Answers the
|
|
5083
|
+
* object's `expires_at` (null = never). */
|
|
5013
5084
|
complete: (objectId: string) => Promise<{
|
|
5014
5085
|
object_id: string;
|
|
5015
5086
|
status: string;
|
|
5087
|
+
checksum_sha256?: string | null;
|
|
5088
|
+
expires_at?: string | null;
|
|
5016
5089
|
}>;
|
|
5017
5090
|
downloadUrl: (objectId: string) => Promise<string>;
|
|
5018
5091
|
/** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
|
|
@@ -5045,27 +5118,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
5045
5118
|
/** Soft delete; bytes are hard-deleted 30 days later. */
|
|
5046
5119
|
delete: (objectId: string) => Promise<void>;
|
|
5047
5120
|
/** Aggregate storage usage vs quotas (the FilesManager Storage panel).
|
|
5121
|
+
* `quotas.maxTotalBytes` is the quota ENFORCED now — the plan's ceiling
|
|
5122
|
+
* unless you set a lower `quotas.maxTotalBytes` (`storage_plan.source`
|
|
5123
|
+
* says which). An object past its expiry no longer counts.
|
|
5048
5124
|
* `public_assets` = published copies vs the plan's published-bytes ceiling
|
|
5049
5125
|
* (identical bytes count once). */
|
|
5050
|
-
usage: () => Promise<
|
|
5051
|
-
usage: {
|
|
5052
|
-
object_count: number;
|
|
5053
|
-
total_bytes: number;
|
|
5054
|
-
};
|
|
5055
|
-
quotas: {
|
|
5056
|
-
maxObjectCount?: number;
|
|
5057
|
-
maxObjectBytes?: number;
|
|
5058
|
-
maxTotalBytes?: number;
|
|
5059
|
-
};
|
|
5060
|
-
available: Record<string, number | null>;
|
|
5061
|
-
public_assets?: {
|
|
5062
|
-
enabled: boolean;
|
|
5063
|
-
published_objects: number;
|
|
5064
|
-
published_bytes: number;
|
|
5065
|
-
max_published_bytes: number;
|
|
5066
|
-
bytes_remaining: number;
|
|
5067
|
-
};
|
|
5068
|
-
}>;
|
|
5126
|
+
usage: () => Promise<FilesUsage>;
|
|
5069
5127
|
/** SERVER-ONLY (403 server_only in end-user mode). Publish an available
|
|
5070
5128
|
* object to vxil's public asset host: a stable, content-addressed URL
|
|
5071
5129
|
* (`https://cdn.vxil.app/<tenant>/<sha256>.<ext>`) served from a global
|
|
@@ -5125,7 +5183,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
5125
5183
|
error?: string | null;
|
|
5126
5184
|
}>;
|
|
5127
5185
|
/** Set / extend / clear an object's TTL. `expiresInSeconds: null` clears it
|
|
5128
|
-
* (else a future auto-delete after the given seconds;
|
|
5186
|
+
* (else a future auto-delete after the given seconds; 60 s up to 100
|
|
5187
|
+
* years). An object already past its expiry is a 404 — never revived. */
|
|
5129
5188
|
setTtl: (objectId: string, expiresInSeconds: number | null) => Promise<{
|
|
5130
5189
|
object_id: string;
|
|
5131
5190
|
expires_at: string | null;
|
package/dist/index.js
CHANGED
|
@@ -353,9 +353,20 @@ export class Vxil {
|
|
|
353
353
|
body: JSON.stringify(payload ?? {}),
|
|
354
354
|
});
|
|
355
355
|
if (!res.ok) {
|
|
356
|
+
// A function answers its OWN error body: the platform envelope
|
|
357
|
+
// `{ error: { code, message } }`, or a bare string code
|
|
358
|
+
// `{ error: 'role_required', message? }` (the blueprints' shape) —
|
|
359
|
+
// both keep their code (a string used to collapse to http_<status>).
|
|
356
360
|
let e = {};
|
|
357
361
|
try {
|
|
358
|
-
|
|
362
|
+
const b = JSON.parse(text);
|
|
363
|
+
if (typeof b.error === 'string') {
|
|
364
|
+
if (b.error)
|
|
365
|
+
e = { code: b.error, message: typeof b.message === 'string' && b.message ? b.message : b.error };
|
|
366
|
+
}
|
|
367
|
+
else if (b.error && typeof b.error === 'object') {
|
|
368
|
+
e = b.error;
|
|
369
|
+
}
|
|
359
370
|
}
|
|
360
371
|
catch { /* non-JSON error body */ }
|
|
361
372
|
throw new VxilError(res.status, e.code ?? `http_${res.status}`, e.message ?? text.slice(0, 200), undefined, undefined, undefined, parseRetryAfter(res.headers.get('retry-after')));
|
|
@@ -754,7 +765,10 @@ export class Vxil {
|
|
|
754
765
|
* FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
|
|
755
766
|
* the enqueue (insufficient balance; the run ends with job.generation.failed
|
|
756
767
|
* `ReserveInsufficient` and the 402 names its `run_id`); a runaway over the
|
|
757
|
-
* per-tenant outstanding-holds ceiling → 429 (Retry-After 5).
|
|
768
|
+
* per-tenant outstanding-holds ceiling → 429 (Retry-After 5). Payments
|
|
769
|
+
* unreachable at the hold → 503 `payments_unavailable` (Retry-After 15;
|
|
770
|
+
* nothing started, nothing held — re-send the same request), unless
|
|
771
|
+
* `reserve_credits.on_unavailable: 'proceed'`.
|
|
758
772
|
*
|
|
759
773
|
* Generic completion options for queue-style providers (fal.ai and alike —
|
|
760
774
|
* no vendor adapter): `completion.callback.query_param` puts the signed
|
|
@@ -771,7 +785,8 @@ export class Vxil {
|
|
|
771
785
|
*
|
|
772
786
|
* `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
|
|
773
787
|
* (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
|
|
774
|
-
* `queue_full`) honouring Retry-After
|
|
788
|
+
* `queue_full`) and a 503 `payments_unavailable`, honouring Retry-After
|
|
789
|
+
* with jitter, with the SAME
|
|
775
790
|
* idempotency_key (one is generated when the input has none), until
|
|
776
791
|
* maxWaitMs has passed — then the last 429 is thrown. */
|
|
777
792
|
generation: async (input, opts) => {
|
|
@@ -1832,8 +1847,15 @@ export class Vxil {
|
|
|
1832
1847
|
},
|
|
1833
1848
|
};
|
|
1834
1849
|
files = {
|
|
1835
|
-
/** Mint a presigned PUT; upload the bytes yourself, then call complete().
|
|
1850
|
+
/** Mint a presigned PUT; upload the bytes yourself, then call complete().
|
|
1851
|
+
* `expiresInSeconds` (60 s up to 100 years) makes the OBJECT expire — it is deleted that
|
|
1852
|
+
* long after the mint, whether or not `files.ttl` is enabled; `null` =
|
|
1853
|
+
* never. The answer's `expires_in` is the UPLOAD URL's life in seconds;
|
|
1854
|
+
* `expires_at` is when the object expires (null = never). `user_id` is
|
|
1855
|
+
* required with a server key and omitted in end-user mode. */
|
|
1836
1856
|
createUploadUrl: async (input) => (await this.call('POST', '/v1/files/upload-url', input)).data,
|
|
1857
|
+
/** Confirm the PUT (bytes verified, object → available). Answers the
|
|
1858
|
+
* object's `expires_at` (null = never). */
|
|
1837
1859
|
complete: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/complete`)).data,
|
|
1838
1860
|
downloadUrl: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/download-url`)).data.download_url,
|
|
1839
1861
|
/** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
|
|
@@ -1857,6 +1879,9 @@ export class Vxil {
|
|
|
1857
1879
|
await this.call('DELETE', `/v1/files/${encodeURIComponent(objectId)}`);
|
|
1858
1880
|
},
|
|
1859
1881
|
/** Aggregate storage usage vs quotas (the FilesManager Storage panel).
|
|
1882
|
+
* `quotas.maxTotalBytes` is the quota ENFORCED now — the plan's ceiling
|
|
1883
|
+
* unless you set a lower `quotas.maxTotalBytes` (`storage_plan.source`
|
|
1884
|
+
* says which). An object past its expiry no longer counts.
|
|
1860
1885
|
* `public_assets` = published copies vs the plan's published-bytes ceiling
|
|
1861
1886
|
* (identical bytes count once). */
|
|
1862
1887
|
usage: async () => (await this.call('GET', '/v1/files/usage')).data,
|
|
@@ -1894,7 +1919,8 @@ export class Vxil {
|
|
|
1894
1919
|
/** Fetch the cached extraction (status: not_extracted|pending|available|failed). */
|
|
1895
1920
|
getText: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/text`)).data,
|
|
1896
1921
|
/** Set / extend / clear an object's TTL. `expiresInSeconds: null` clears it
|
|
1897
|
-
* (else a future auto-delete after the given seconds;
|
|
1922
|
+
* (else a future auto-delete after the given seconds; 60 s up to 100
|
|
1923
|
+
* years). An object already past its expiry is a 404 — never revived. */
|
|
1898
1924
|
setTtl: async (objectId, expiresInSeconds) => (await this.call('PUT', `/v1/files/${encodeURIComponent(objectId)}/ttl`, { expiresInSeconds })).data,
|
|
1899
1925
|
/** SERVER-ONLY (403 server_only in end-user mode). Account merge: move
|
|
1900
1926
|
* every object `from_user_id` owns (every status, tombstones included) and
|
package/dist/retry.d.ts
CHANGED
|
@@ -50,6 +50,14 @@ export declare function createTransport(opts: TransportOptions): Transport;
|
|
|
50
50
|
* and outstanding-holds ceiling, and the queue admission cap). Any other 429
|
|
51
51
|
* (a rate limit, a plan quota) is not a capacity answer and is rethrown. */
|
|
52
52
|
export declare const CAPACITY_ERROR_CODES: ReadonlySet<string>;
|
|
53
|
+
/** The 503 answers retried the same way (M1, 2026-10-04): the generation
|
|
54
|
+
* lane could not reach payments to place its credit hold — nothing was
|
|
55
|
+
* started and nothing held, and the SAME request re-sent after Retry-After
|
|
56
|
+
* starts the run (the refused run released its idempotency key). */
|
|
57
|
+
export declare const UNAVAILABLE_ERROR_CODES: ReadonlySet<string>;
|
|
58
|
+
/** Is this thrown error one `retryOnCapacity` waits out? Duck-typed on
|
|
59
|
+
* `{ status, code }` (VxilError's fields). */
|
|
60
|
+
export declare function isCapacityRetryable(e: unknown): boolean;
|
|
53
61
|
/** `retryOnCapacity` options. */
|
|
54
62
|
export interface RetryOnCapacityOptions {
|
|
55
63
|
/** Give up (rethrow the last 429) once waiting again would pass this many
|
|
@@ -60,7 +68,8 @@ export interface RetryOnCapacityOptions {
|
|
|
60
68
|
now?: () => number;
|
|
61
69
|
}
|
|
62
70
|
/**
|
|
63
|
-
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`)
|
|
71
|
+
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`) or
|
|
72
|
+
* a 503 `payments_unavailable` (`UNAVAILABLE_ERROR_CODES`):
|
|
64
73
|
* wait the server's `Retry-After` (else 5 s) plus up to 20 % jitter (≤ 1 s,
|
|
65
74
|
* so many clients told the same second do not all return on it), and never
|
|
66
75
|
* past `maxWaitMs` since the first attempt — then the last 429 is rethrown.
|
package/dist/retry.js
CHANGED
|
@@ -162,10 +162,25 @@ export function createTransport(opts) {
|
|
|
162
162
|
export const CAPACITY_ERROR_CODES = new Set([
|
|
163
163
|
'generation_concurrency_exceeded', 'reserve_holds_exceeded', 'queue_full',
|
|
164
164
|
]);
|
|
165
|
+
/** The 503 answers retried the same way (M1, 2026-10-04): the generation
|
|
166
|
+
* lane could not reach payments to place its credit hold — nothing was
|
|
167
|
+
* started and nothing held, and the SAME request re-sent after Retry-After
|
|
168
|
+
* starts the run (the refused run released its idempotency key). */
|
|
169
|
+
export const UNAVAILABLE_ERROR_CODES = new Set(['payments_unavailable']);
|
|
170
|
+
/** Is this thrown error one `retryOnCapacity` waits out? Duck-typed on
|
|
171
|
+
* `{ status, code }` (VxilError's fields). */
|
|
172
|
+
export function isCapacityRetryable(e) {
|
|
173
|
+
const err = e;
|
|
174
|
+
if (!err || typeof err.code !== 'string')
|
|
175
|
+
return false;
|
|
176
|
+
return (err.status === 429 && CAPACITY_ERROR_CODES.has(err.code))
|
|
177
|
+
|| (err.status === 503 && UNAVAILABLE_ERROR_CODES.has(err.code));
|
|
178
|
+
}
|
|
165
179
|
/** Fallback wait (ms) when a capacity 429 carries no Retry-After. */
|
|
166
180
|
const CAPACITY_DEFAULT_WAIT_MS = 5_000;
|
|
167
181
|
/**
|
|
168
|
-
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`)
|
|
182
|
+
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`) or
|
|
183
|
+
* a 503 `payments_unavailable` (`UNAVAILABLE_ERROR_CODES`):
|
|
169
184
|
* wait the server's `Retry-After` (else 5 s) plus up to 20 % jitter (≤ 1 s,
|
|
170
185
|
* so many clients told the same second do not all return on it), and never
|
|
171
186
|
* past `maxWaitMs` since the first attempt — then the last 429 is rethrown.
|
|
@@ -184,7 +199,7 @@ export async function retryOnCapacity(attempt, opts) {
|
|
|
184
199
|
}
|
|
185
200
|
catch (e) {
|
|
186
201
|
const err = e;
|
|
187
|
-
if (!err ||
|
|
202
|
+
if (!err || !isCapacityRetryable(err))
|
|
188
203
|
throw e;
|
|
189
204
|
const baseMs = typeof err.retryAfter === 'number' && err.retryAfter >= 0
|
|
190
205
|
? Math.round(err.retryAfter * 1000)
|
package/package.json
CHANGED