@vxil/sdk 0.14.1 → 0.16.0
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 +329 -79
- package/dist/index.js +144 -18
- package/dist/retry.d.ts +33 -0
- package/dist/retry.js +57 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** The 429 error codes `retryOnCapacity` treats as "at a capacity cap — try the
|
|
2
|
+
* SAME request again shortly". Declared here with an explicit type (not a
|
|
3
|
+
* re-export) so the served single-file index.d.ts never imports ./retry. */
|
|
4
|
+
export declare const CAPACITY_ERROR_CODES: ReadonlySet<string>;
|
|
1
5
|
export type ApiVersion = 'v1';
|
|
2
6
|
/** A per-feature API-version override key: the URL namespace that leads a path
|
|
3
7
|
* (`/v1/<namespace>/…`). Matches the segment `versionedPath` rewrites, so an
|
|
@@ -361,7 +365,9 @@ export interface JobRun {
|
|
|
361
365
|
queued_at: string;
|
|
362
366
|
started_at?: string | null;
|
|
363
367
|
completed_at: string | null;
|
|
364
|
-
/** generation runs (POST /v1/jobs/generation) only: the provider-side status
|
|
368
|
+
/** generation runs (POST /v1/jobs/generation) only: the provider-side status
|
|
369
|
+
* (pending | processing | completed | failed); null for a plain run. On the
|
|
370
|
+
* list (`vx.jobs.runs`) and the single-run read. */
|
|
365
371
|
generation_status?: string | null;
|
|
366
372
|
generation_json?: Record<string, unknown> | null;
|
|
367
373
|
/** the single-run read (`vx.jobs.run` / `waitForRun`) only: your enqueue
|
|
@@ -400,6 +406,88 @@ export interface JobRun {
|
|
|
400
406
|
* strands the row; this says what was dropped. Never flips the run; not cleared by a later
|
|
401
407
|
* successful mirror (compare `generation_status` / `at`). */
|
|
402
408
|
mirror_error?: JobRunMirrorError | null;
|
|
409
|
+
/** the single-run read only: the enqueue `concurrency_key` this run holds
|
|
410
|
+
* while running or waiting; null for an unkeyed run */
|
|
411
|
+
concurrency_key?: string | null;
|
|
412
|
+
/** the single-run read only: how many runs sharing the key may hold it at
|
|
413
|
+
* once; null for an unkeyed run */
|
|
414
|
+
concurrency_limit?: number | null;
|
|
415
|
+
/** the fan-in batch the run joined (enqueue `batch_id`); null otherwise */
|
|
416
|
+
batch_id?: string | null;
|
|
417
|
+
/** a plain run enqueued with `ttl_seconds`: its START deadline (it is
|
|
418
|
+
* dead-lettered `Expired` if not started by then); null otherwise */
|
|
419
|
+
expires_at?: string | null;
|
|
420
|
+
/** the single-run read only: the debounce key of a debounced run */
|
|
421
|
+
debounce_key?: string | null;
|
|
422
|
+
}
|
|
423
|
+
/** The plain-run enqueue input shared by `enqueue` and `enqueueBatch` items. */
|
|
424
|
+
export interface JobEnqueueInput {
|
|
425
|
+
job_name: string;
|
|
426
|
+
target_url: string;
|
|
427
|
+
payload?: Record<string, unknown>;
|
|
428
|
+
idempotency_key?: string;
|
|
429
|
+
max_attempts?: number;
|
|
430
|
+
deliver_after?: string;
|
|
431
|
+
delay_seconds?: number;
|
|
432
|
+
/** 60..2678400: dead-letter the run (`Expired`) if not started this long after it is due */
|
|
433
|
+
ttl_seconds?: number;
|
|
434
|
+
/** per-key concurrency: at most `concurrency_limit` runs sharing this key hold it at once */
|
|
435
|
+
concurrency_key?: string;
|
|
436
|
+
/** 1..100, default 1; requires concurrency_key */
|
|
437
|
+
concurrency_limit?: number;
|
|
438
|
+
}
|
|
439
|
+
/** The answer of `vx.jobs.enqueue`. */
|
|
440
|
+
export interface JobEnqueueResult {
|
|
441
|
+
run_id: string;
|
|
442
|
+
state: string;
|
|
443
|
+
deduplicated?: boolean;
|
|
444
|
+
/** a debounced enqueue that pushed the open run (same run_id) */
|
|
445
|
+
debounced?: boolean;
|
|
446
|
+
deliver_after?: string;
|
|
447
|
+
callback_url?: string;
|
|
448
|
+
/** the START deadline when `ttl_seconds` was given */
|
|
449
|
+
expires_at?: string;
|
|
450
|
+
batch_id?: string;
|
|
451
|
+
/** true when the run's first attempt is already in flight (it started on
|
|
452
|
+
* the enqueue request, no queue in front of it); absent when it was queued */
|
|
453
|
+
direct?: boolean;
|
|
454
|
+
}
|
|
455
|
+
/** `vx.jobs.runs` / `runsPage` filters. */
|
|
456
|
+
export interface JobRunsQuery {
|
|
457
|
+
job_name?: string;
|
|
458
|
+
state?: JobRun['state'] | string;
|
|
459
|
+
ids?: string[];
|
|
460
|
+
batch_id?: string;
|
|
461
|
+
/** ISO: runs created at or after */
|
|
462
|
+
since?: string;
|
|
463
|
+
/** ISO: runs created before */
|
|
464
|
+
until?: string;
|
|
465
|
+
limit?: number;
|
|
466
|
+
}
|
|
467
|
+
/** One page of `vx.jobs.runsPage`. */
|
|
468
|
+
export interface JobRunsPage {
|
|
469
|
+
runs: JobRun[];
|
|
470
|
+
/** pass back as `cursor` for the next older page; null on the last page */
|
|
471
|
+
next_cursor: string | null;
|
|
472
|
+
}
|
|
473
|
+
/** `GET /v1/jobs/batches/{batch_id}` — a fan-in batch. */
|
|
474
|
+
export interface JobBatch {
|
|
475
|
+
batch_id: string;
|
|
476
|
+
/** 'completed' once every run is terminal (the event fired, or fires within a minute) */
|
|
477
|
+
state: 'open' | 'completed';
|
|
478
|
+
total: number;
|
|
479
|
+
/** the declared size (`batch_total`), or null */
|
|
480
|
+
expected_total: number | null;
|
|
481
|
+
open: number;
|
|
482
|
+
succeeded: number;
|
|
483
|
+
dead_lettered: number;
|
|
484
|
+
cancelled: number;
|
|
485
|
+
/** live count of the batch's RETAINED runs by state (runs past retention drop out; the counters above do not) */
|
|
486
|
+
runs_by_state: Partial<Record<JobRun['state'], number>>;
|
|
487
|
+
created_at: string;
|
|
488
|
+
completed_at: string | null;
|
|
489
|
+
/** when `job.batch.completed` was written */
|
|
490
|
+
event_emitted_at: string | null;
|
|
403
491
|
}
|
|
404
492
|
/** A refused status-mirror write (`JobRun.mirror_error`). */
|
|
405
493
|
export interface JobRunMirrorError {
|
|
@@ -508,8 +596,11 @@ export interface JobScheduleHeld {
|
|
|
508
596
|
export interface JobSchedule {
|
|
509
597
|
schedule_id: string;
|
|
510
598
|
job_name: string;
|
|
511
|
-
/** the
|
|
599
|
+
/** the cron expression under the request field's name — every schedule
|
|
600
|
+
* answer (create, update, list) carries it, equal to `cron_expr`; null for
|
|
601
|
+
* a one-shot `run_at` schedule. (Before 2026-10-03 only `cron_expr`.) */
|
|
512
602
|
cron?: string | null;
|
|
603
|
+
/** the same value under the stored column's name */
|
|
513
604
|
cron_expr?: string | null;
|
|
514
605
|
run_at?: string | null;
|
|
515
606
|
next_run_at: string | null;
|
|
@@ -524,7 +615,41 @@ export interface JobSchedule {
|
|
|
524
615
|
last_skipped_at?: string | null;
|
|
525
616
|
/** non-null while the schedule is HELD (list only) */
|
|
526
617
|
held?: JobScheduleHeld | null;
|
|
618
|
+
/** the IANA zone the cron is read in (default `'UTC'`); DST-correct */
|
|
619
|
+
timezone?: string;
|
|
620
|
+
/** your upsert key — a create naming a live schedule's external_id updates
|
|
621
|
+
* that schedule instead of adding one */
|
|
622
|
+
external_id?: string | null;
|
|
623
|
+
/** each fired run's attempt budget; null = the project's
|
|
624
|
+
* `jobs.retry.defaultMaxAttempts` */
|
|
625
|
+
max_attempts?: number | null;
|
|
626
|
+
/** on a create: true = a new schedule (201); false = the external_id
|
|
627
|
+
* matched a live schedule, which was updated in place (200) */
|
|
628
|
+
created?: boolean;
|
|
629
|
+
}
|
|
630
|
+
/** The fields of a schedule create (`vx.jobs.schedules.create`). */
|
|
631
|
+
export interface JobScheduleInput {
|
|
632
|
+
job_name: string;
|
|
633
|
+
target_url: string;
|
|
634
|
+
payload?: Record<string, unknown>;
|
|
635
|
+
/** 5-field cron, read in `timezone` — exactly one of cron / run_at */
|
|
636
|
+
cron?: string;
|
|
637
|
+
/** a future ISO timestamp (a one-shot) */
|
|
638
|
+
run_at?: string;
|
|
639
|
+
overlap?: 'allow' | 'skip';
|
|
640
|
+
/** an IANA zone, e.g. `'Asia/Amman'`, `'Europe/London'` (default `'UTC'`) */
|
|
641
|
+
timezone?: string;
|
|
642
|
+
/** ≤ 200 printable ASCII, no spaces — makes the create an upsert */
|
|
643
|
+
external_id?: string;
|
|
644
|
+
/** 1..20 */
|
|
645
|
+
max_attempts?: number;
|
|
527
646
|
}
|
|
647
|
+
/** The fields of a schedule PATCH (`vx.jobs.schedules.update`): any create
|
|
648
|
+
* field; `external_id` / `max_attempts` accept null (clear). */
|
|
649
|
+
export type JobSchedulePatch = Partial<Omit<JobScheduleInput, 'external_id' | 'max_attempts'>> & {
|
|
650
|
+
external_id?: string | null;
|
|
651
|
+
max_attempts?: number | null;
|
|
652
|
+
};
|
|
528
653
|
export interface AuthSession {
|
|
529
654
|
token: string;
|
|
530
655
|
refresh_token: string;
|
|
@@ -740,6 +865,47 @@ export interface FileObject {
|
|
|
740
865
|
/** The object's public URL when it is published to the public asset host
|
|
741
866
|
* (`files.publish`), else null. */
|
|
742
867
|
public_url?: string | null;
|
|
868
|
+
/** When the object expires (null = never). An expired object is no longer
|
|
869
|
+
* listed and answers 404, even before the sweep deletes it. */
|
|
870
|
+
expires_at?: string | null;
|
|
871
|
+
}
|
|
872
|
+
/** `files.createUploadUrl` answer. */
|
|
873
|
+
export interface FileUploadUrl {
|
|
874
|
+
object_id: string;
|
|
875
|
+
upload_url: string;
|
|
876
|
+
upload_method: string;
|
|
877
|
+
/** Seconds the UPLOAD URL stays valid (`uploadUrlTtl`, default 900). */
|
|
878
|
+
expires_in: number;
|
|
879
|
+
/** When the OBJECT expires (null = never). */
|
|
880
|
+
expires_at: string | null;
|
|
881
|
+
}
|
|
882
|
+
/** `files.usage` answer. */
|
|
883
|
+
export interface FilesUsage {
|
|
884
|
+
usage: {
|
|
885
|
+
object_count: number;
|
|
886
|
+
total_bytes: number;
|
|
887
|
+
};
|
|
888
|
+
/** `maxTotalBytes` = the storage quota enforced now (see `storage_plan`). */
|
|
889
|
+
quotas: {
|
|
890
|
+
maxObjectCount?: number;
|
|
891
|
+
maxObjectBytes?: number;
|
|
892
|
+
maxTotalBytes?: number;
|
|
893
|
+
};
|
|
894
|
+
available: Record<string, number | null>;
|
|
895
|
+
/** Your plan's storage ceiling, and whether `quotas.maxTotalBytes` is that
|
|
896
|
+
* ceiling (`plan`) or your own lower value (`config`). */
|
|
897
|
+
storage_plan?: {
|
|
898
|
+
tier: string | null;
|
|
899
|
+
max_total_bytes: number;
|
|
900
|
+
source: 'plan' | 'config';
|
|
901
|
+
};
|
|
902
|
+
public_assets?: {
|
|
903
|
+
enabled: boolean;
|
|
904
|
+
published_objects: number;
|
|
905
|
+
published_bytes: number;
|
|
906
|
+
max_published_bytes: number;
|
|
907
|
+
bytes_remaining: number;
|
|
908
|
+
};
|
|
743
909
|
}
|
|
744
910
|
/** A published object (guide ch. 6, files, "Public asset delivery"): a stable,
|
|
745
911
|
* content-addressed URL on vxil's public asset host, served with
|
|
@@ -1042,13 +1208,23 @@ export interface JobGenerationSettledEventPayload {
|
|
|
1042
1208
|
/** `completed` on the healthy terminal, `failed` on the broken one */
|
|
1043
1209
|
status: 'completed' | 'failed';
|
|
1044
1210
|
/** the run's SETTLED error class (`provider_error`, `PollExhausted`,
|
|
1045
|
-
* `GenerationExpired`, `
|
|
1211
|
+
* `GenerationExpired`, `RetryableHttp`, `ReserveInsufficient`,
|
|
1212
|
+
* `PaymentsUnavailable`, `Cancelled`, …); always null on `completed`.
|
|
1213
|
+
* BRANCH ON THIS in a settle handler: `ReserveInsufficient` (the caller got
|
|
1214
|
+
* a 402), `PaymentsUnavailable` (the run was refused before it started —
|
|
1215
|
+
* usually the caller got a 503; a caller handed this run by a duplicate
|
|
1216
|
+
* request that raced it (202 `deduplicated`) learns it HERE and re-sends
|
|
1217
|
+
* the same request — a NEW run follows) and `Cancelled` are not render
|
|
1218
|
+
* failures. */
|
|
1046
1219
|
error_class: string | null;
|
|
1047
1220
|
/** the provider hint alone (`Upstream said: gemini 400: …`); null on
|
|
1048
1221
|
* `completed` and whenever the failure carried none */
|
|
1049
1222
|
error_hint: string | null;
|
|
1050
|
-
/** the failure-event vocabulary every `*.failed` carries (guide 12)
|
|
1051
|
-
|
|
1223
|
+
/** the failure-event vocabulary every `*.failed` carries (guide 12):
|
|
1224
|
+
* `completed` → info; `failed` → error, except a refusal at admission
|
|
1225
|
+
* (`ReserveInsufficient`, `PaymentsUnavailable`) → warn and a cancel
|
|
1226
|
+
* (`Cancelled`) → info, so neither pages through the immediate digest */
|
|
1227
|
+
level: 'info' | 'warn' | 'error';
|
|
1052
1228
|
state: 'ok' | 'broken';
|
|
1053
1229
|
}
|
|
1054
1230
|
/** The union a handler subscribed to `job.generation.` receives as `data`:
|
|
@@ -1069,9 +1245,25 @@ export interface JobRunEventPayload {
|
|
|
1069
1245
|
* `queue_backstop` (the queue's own retries ran out), `callback_failed`
|
|
1070
1246
|
* (the run's signed callback reported `status: 'failed'`) or
|
|
1071
1247
|
* `callback_timeout` (the handler handed the run off with a 202 and no
|
|
1072
|
-
* callback completed it within its lifetime)
|
|
1248
|
+
* callback completed it within its lifetime) or `expired` (a run enqueued
|
|
1249
|
+
* with `ttl_seconds` that had not started by its deadline — never
|
|
1250
|
+
* delivered; `last_error_class: 'Expired'`). Absent when the run
|
|
1073
1251
|
* exhausted its attempts normally. */
|
|
1074
|
-
reason?: 'reaped' | 'queue_backstop' | 'callback_failed' | 'callback_timeout';
|
|
1252
|
+
reason?: 'reaped' | 'queue_backstop' | 'callback_failed' | 'callback_timeout' | 'expired';
|
|
1253
|
+
}
|
|
1254
|
+
/** The `data` of `job.batch.completed`: EVERY run enqueued with this
|
|
1255
|
+
* `batch_id` reached a terminal state (and, with a declared `batch_total`,
|
|
1256
|
+
* that many runs joined). Emitted exactly ONCE per batch — a run replayed
|
|
1257
|
+
* later is a new run outside the batch. The fan-in signal: subscribe a
|
|
1258
|
+
* function to `job.batch.` and aggregate there; vxil runs no next step. */
|
|
1259
|
+
export interface JobBatchCompletedEventPayload {
|
|
1260
|
+
batch_id: string;
|
|
1261
|
+
/** runs that joined the batch */
|
|
1262
|
+
total: number;
|
|
1263
|
+
succeeded: number;
|
|
1264
|
+
/** ended `dead` (retries exhausted, reaped, expired, callback failed …) */
|
|
1265
|
+
dead_lettered: number;
|
|
1266
|
+
cancelled: number;
|
|
1075
1267
|
}
|
|
1076
1268
|
/** The provider-reported environment of the money (`production` | `sandbox`). */
|
|
1077
1269
|
export type PaymentsEventEnvironment = 'production' | 'sandbox';
|
|
@@ -1318,6 +1510,7 @@ export interface VxilEventPayloads {
|
|
|
1318
1510
|
'job.generation.failed': JobGenerationSettledEventPayload;
|
|
1319
1511
|
'job.succeeded': JobRunEventPayload;
|
|
1320
1512
|
'job.dead_lettered': JobRunEventPayload;
|
|
1513
|
+
'job.batch.completed': JobBatchCompletedEventPayload;
|
|
1321
1514
|
'payments.charge.succeeded': PaymentsChargeEventPayload;
|
|
1322
1515
|
'payments.charge.completed': PaymentsChargeEventPayload;
|
|
1323
1516
|
'payments.charge.refunded': PaymentsChargeRefundedEventPayload;
|
|
@@ -2952,44 +3145,72 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
2952
3145
|
* delivery): an external worker POSTs it to complete / fail the run, report
|
|
2953
3146
|
* progress, or wake a `wait`. A handler that answers 202 HANDS the run off
|
|
2954
3147
|
* — it waits for that callback (dead-lettered as `CallbackTimeout` when the
|
|
2955
|
-
* lifetime passes). See `postRunCallback`.
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3148
|
+
* lifetime passes). See `postRunCallback`.
|
|
3149
|
+
*
|
|
3150
|
+
* `concurrency_key` (≤ 200 chars) + `concurrency_limit` (1..100, default
|
|
3151
|
+
* 1): at most that many runs of this job_name sharing the key hold it at
|
|
3152
|
+
* once — e.g. the end user's id for "one import per user". A run holds its
|
|
3153
|
+
* key from its first start until it finishes (running, waiting, and
|
|
3154
|
+
* between retries). The rest stay queued and start oldest first (never
|
|
3155
|
+
* rejected).
|
|
3156
|
+
*
|
|
3157
|
+
* Queue controls (2026-10-03):
|
|
3158
|
+
* - `ttl_seconds` (60 s..31 d): a run not STARTED within this long of
|
|
3159
|
+
* being due is dead-lettered (`last_error_class: 'Expired'`,
|
|
3160
|
+
* `job.dead_lettered` with `reason: 'expired'`) and never delivered.
|
|
3161
|
+
* - `debounce: { key, delay_seconds (1..86400), max_delay_seconds? }`: the
|
|
3162
|
+
* first enqueue for (job_name, key) makes a run due in delay_seconds;
|
|
3163
|
+
* each later one while it has not started REPLACES its payload and pushes
|
|
3164
|
+
* it to now + delay_seconds (never past first enqueue +
|
|
3165
|
+
* max_delay_seconds — default 10 × delay_seconds, clamped to 1..30 d)
|
|
3166
|
+
* and answers the SAME run_id with `debounced: true`.
|
|
3167
|
+
* Not combinable with idempotency_key / deliver_after / delay_seconds /
|
|
3168
|
+
* callback / batch_id.
|
|
3169
|
+
* - `batch_id` (+ optional `batch_total`): the run joins a fan-in batch —
|
|
3170
|
+
* when every run of the batch is terminal, ONE `job.batch.completed`
|
|
3171
|
+
* event fires (see `batch()`). A completed batch takes no new runs (409
|
|
3172
|
+
* `batch_closed`). */
|
|
3173
|
+
enqueue: (input: JobEnqueueInput & {
|
|
2964
3174
|
callback?: boolean | {
|
|
2965
3175
|
ttl_seconds?: number;
|
|
2966
3176
|
};
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3177
|
+
concurrency_key?: string;
|
|
3178
|
+
concurrency_limit?: number;
|
|
3179
|
+
debounce?: {
|
|
3180
|
+
key: string;
|
|
3181
|
+
delay_seconds: number;
|
|
3182
|
+
max_delay_seconds?: number;
|
|
3183
|
+
};
|
|
3184
|
+
batch_id?: string;
|
|
3185
|
+
batch_total?: number;
|
|
3186
|
+
}) => Promise<JobEnqueueResult>;
|
|
2974
3187
|
/** Atomic multi-enqueue (≤100 items; any invalid item rejects the whole
|
|
2975
|
-
* batch). Each item = the enqueue input, incl. per-item idempotency_key
|
|
2976
|
-
* and
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3188
|
+
* batch). Each item = the enqueue input, incl. per-item idempotency_key,
|
|
3189
|
+
* deliver_after/delay_seconds, ttl_seconds and concurrency_key/concurrency_limit. Results align with the
|
|
3190
|
+
* input order.
|
|
3191
|
+
*
|
|
3192
|
+
* FAN-IN: `opts.batch_id` puts every item in one batch; when every run of
|
|
3193
|
+
* the batch is terminal, ONE `job.batch.completed` event fires ({ batch_id,
|
|
3194
|
+
* total, succeeded, dead_lettered, cancelled }) — subscribe a function to
|
|
3195
|
+
* `job.batch.` to aggregate. A batch may span several calls: pass the same
|
|
3196
|
+
* batch_id and declare `batch_total` (the full size) so it cannot complete
|
|
3197
|
+
* between calls. */
|
|
3198
|
+
enqueueBatch: (items: JobEnqueueInput[], opts?: {
|
|
3199
|
+
batch_id?: string;
|
|
3200
|
+
batch_total?: number;
|
|
3201
|
+
}) => Promise<{
|
|
2986
3202
|
runs: Array<{
|
|
2987
3203
|
run_id: string;
|
|
2988
3204
|
state: string;
|
|
2989
3205
|
deduplicated?: boolean;
|
|
2990
3206
|
}>;
|
|
2991
3207
|
count: number;
|
|
3208
|
+
batch_id?: string;
|
|
2992
3209
|
}>;
|
|
3210
|
+
/** A fan-in batch: its exact counters (`total`, `open`, `succeeded`,
|
|
3211
|
+
* `dead_lettered`, `cancelled`), `state` ('open' | 'completed') and the
|
|
3212
|
+
* live count of its retained runs by state. 404 for an unknown batch. */
|
|
3213
|
+
batch: (batchId: string) => Promise<JobBatch>;
|
|
2993
3214
|
/** Enqueue a long-running EXTERNAL generation run (guide ch. 6, jobs): Vxil calls
|
|
2994
3215
|
* the provider (BYO key), tracks completion via poll/webhook, mirrors a typed
|
|
2995
3216
|
* generation_status onto a tenant record, enforces a built-in timeout, and
|
|
@@ -3006,7 +3227,10 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3006
3227
|
* FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
|
|
3007
3228
|
* the enqueue (insufficient balance; the run ends with job.generation.failed
|
|
3008
3229
|
* `ReserveInsufficient` and the 402 names its `run_id`); a runaway over the
|
|
3009
|
-
* per-tenant outstanding-holds ceiling → 429 (Retry-After 5).
|
|
3230
|
+
* per-tenant outstanding-holds ceiling → 429 (Retry-After 5). Payments
|
|
3231
|
+
* unreachable at the hold → 503 `payments_unavailable` (Retry-After 15;
|
|
3232
|
+
* nothing started, nothing held — re-send the same request), unless
|
|
3233
|
+
* `reserve_credits.on_unavailable: 'proceed'`.
|
|
3010
3234
|
*
|
|
3011
3235
|
* Generic completion options for queue-style providers (fal.ai and alike —
|
|
3012
3236
|
* no vendor adapter): `completion.callback.query_param` puts the signed
|
|
@@ -3019,7 +3243,14 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3019
3243
|
* A mirror write the record refuses never fails the run: a body refusal
|
|
3020
3244
|
* (e.g. a callback key the collection does not declare) is retried with
|
|
3021
3245
|
* only the status column, so the row still reaches `completed`; the run then reports `mirror_error`
|
|
3022
|
-
* (`JobRun.mirror_error` on `vx.jobs.run(run_id)`).
|
|
3246
|
+
* (`JobRun.mirror_error` on `vx.jobs.run(run_id)`).
|
|
3247
|
+
*
|
|
3248
|
+
* `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
|
|
3249
|
+
* (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
|
|
3250
|
+
* `queue_full`) and a 503 `payments_unavailable`, honouring Retry-After
|
|
3251
|
+
* with jitter, with the SAME
|
|
3252
|
+
* idempotency_key (one is generated when the input has none), until
|
|
3253
|
+
* maxWaitMs has passed — then the last 429 is thrown. */
|
|
3023
3254
|
generation: (input: {
|
|
3024
3255
|
job_name: string;
|
|
3025
3256
|
provider: {
|
|
@@ -3073,22 +3304,41 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3073
3304
|
amount: number;
|
|
3074
3305
|
user_id: string;
|
|
3075
3306
|
reason?: string;
|
|
3307
|
+
/** payments unreachable when the hold is placed (a 5xx other than
|
|
3308
|
+
* 501, a 429, no answer within 5 s; payments switched off is NOT
|
|
3309
|
+
* unreachable — that run starts without a hold): `'fail'` (default)
|
|
3310
|
+
* → 503 `payments_unavailable` + Retry-After, nothing started or
|
|
3311
|
+
* held — re-send the SAME request; `'proceed'` → start WITHOUT a
|
|
3312
|
+
* hold (a free run while payments is down) */
|
|
3313
|
+
on_unavailable?: "fail" | "proceed";
|
|
3076
3314
|
};
|
|
3077
3315
|
payload?: Record<string, unknown>;
|
|
3078
3316
|
idempotency_key?: string;
|
|
3317
|
+
/** START attempts = provider calls (1..20, default jobs.retry
|
|
3318
|
+
* .defaultMaxAttempts 5); a 408 / 429 / 5xx / network error retries
|
|
3319
|
+
* after min(2^n × 15, 600) s — or the provider's Retry-After on a
|
|
3320
|
+
* 429 / 503 (1 s..10 min, never past the deadline) */
|
|
3079
3321
|
max_attempts?: number;
|
|
3322
|
+
}, opts?: {
|
|
3323
|
+
retryOnCapacity?: {
|
|
3324
|
+
maxWaitMs: number;
|
|
3325
|
+
};
|
|
3080
3326
|
}) => Promise<{
|
|
3081
3327
|
run_id: string;
|
|
3082
3328
|
generation_status: string;
|
|
3083
3329
|
state?: string;
|
|
3084
3330
|
deduplicated?: boolean;
|
|
3085
3331
|
}>;
|
|
3086
|
-
runs
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3332
|
+
/** The newest runs (≤ `limit`, default 50, max 100). Filters: job_name,
|
|
3333
|
+
* state, batch_id, ids, since/until (creation time, ISO). For more than
|
|
3334
|
+
* one page use `runsPage` (it returns the `next_cursor`). */
|
|
3335
|
+
runs: (q?: JobRunsQuery) => Promise<JobRun[]>;
|
|
3336
|
+
/** One page of runs, newest first, plus `next_cursor` (null on the last
|
|
3337
|
+
* page): pass it back as `cursor` for the next older page. The keyset
|
|
3338
|
+
* keeps pages stable while new runs arrive. */
|
|
3339
|
+
runsPage: (q?: JobRunsQuery & {
|
|
3340
|
+
cursor?: string | null;
|
|
3341
|
+
}) => Promise<JobRunsPage>;
|
|
3092
3342
|
run: (runId: string) => Promise<JobRun>;
|
|
3093
3343
|
/** "Wait for this run": `GET /v1/jobs/runs/{run_id}?wait=<seconds>` holds
|
|
3094
3344
|
* the request platform-side (re-reading the run on one bounded connection,
|
|
@@ -3110,6 +3360,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3110
3360
|
/** The live queue: depth by state, the oldest waiting run's age (the
|
|
3111
3361
|
* number to alarm on), in-flight runs per lane, dead letters in 24 h. */
|
|
3112
3362
|
queue: () => Promise<JobsQueue>;
|
|
3363
|
+
/** Cancel a run that has not started its current attempt (`queued`,
|
|
3364
|
+
* `delayed`, `retrying`) or a plain run handed off to its signed callback
|
|
3365
|
+
* (`waiting` after its handler answered 202 — the callback URL is
|
|
3366
|
+
* consumed). A `running` run (a delivery in flight, or a generation
|
|
3367
|
+
* waiting on its provider), a run waiting on an event, or a terminal run
|
|
3368
|
+
* answers `409 not_cancellable`. A generation's credit hold is released. */
|
|
3113
3369
|
cancel: (runId: string) => Promise<{
|
|
3114
3370
|
run_id: string;
|
|
3115
3371
|
state: string;
|
|
@@ -3142,17 +3398,20 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3142
3398
|
/** Secret for verifying X-Vxil-Jobs-Signature on your callback endpoints. */
|
|
3143
3399
|
signingSecret: () => Promise<string>;
|
|
3144
3400
|
schedules: {
|
|
3145
|
-
/** Recurring (5-field cron
|
|
3401
|
+
/** Recurring (5-field cron) or one-shot (run_at). Exactly one of cron/run_at.
|
|
3402
|
+
* `timezone` (IANA, default UTC) is the zone the cron is read in, DST-
|
|
3403
|
+
* correct. `external_id` makes it an UPSERT: a live schedule with the
|
|
3404
|
+
* same key is updated in place (`created: false`) — one schedule per
|
|
3405
|
+
* key, safe to call on every sign-in / settings save (an identical
|
|
3406
|
+
* repeat writes nothing). Server mode only: in end-user mode a create
|
|
3407
|
+
* with `external_id` is `403 server_only` (the key is tenant-wide).
|
|
3146
3408
|
* `overlap: 'skip'` (cron only): no new run while the previous one is
|
|
3147
3409
|
* still open — a missed window is never caught up either way. */
|
|
3148
|
-
create: (input:
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
run_at?: string;
|
|
3154
|
-
overlap?: "allow" | "skip";
|
|
3155
|
-
}) => Promise<JobSchedule>;
|
|
3410
|
+
create: (input: JobScheduleInput) => Promise<JobSchedule>;
|
|
3411
|
+
/** Change a live schedule in place (its id, state and counters kept). A
|
|
3412
|
+
* cron / run_at / timezone change recomputes the next fire. Server mode
|
|
3413
|
+
* only (`403 server_only` in end-user mode). */
|
|
3414
|
+
update: (scheduleId: string, patch: JobSchedulePatch) => Promise<JobSchedule>;
|
|
3156
3415
|
list: () => Promise<JobSchedule[]>;
|
|
3157
3416
|
delete: (scheduleId: string) => Promise<void>;
|
|
3158
3417
|
/** Stop an active schedule firing (the row + next_run_at survive). */
|
|
@@ -4805,21 +5064,26 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
4805
5064
|
}) => Promise<WebSocket>;
|
|
4806
5065
|
};
|
|
4807
5066
|
readonly files: {
|
|
4808
|
-
/** Mint a presigned PUT; upload the bytes yourself, then call complete().
|
|
5067
|
+
/** Mint a presigned PUT; upload the bytes yourself, then call complete().
|
|
5068
|
+
* `expiresInSeconds` (60 s up to 100 years) makes the OBJECT expire — it is deleted that
|
|
5069
|
+
* long after the mint, whether or not `files.ttl` is enabled; `null` =
|
|
5070
|
+
* never. The answer's `expires_in` is the UPLOAD URL's life in seconds;
|
|
5071
|
+
* `expires_at` is when the object expires (null = never). `user_id` is
|
|
5072
|
+
* required with a server key and omitted in end-user mode. */
|
|
4809
5073
|
createUploadUrl: (input: {
|
|
4810
|
-
user_id
|
|
5074
|
+
user_id?: string;
|
|
4811
5075
|
filename: string;
|
|
4812
5076
|
content_type: string;
|
|
4813
5077
|
size_bytes: number;
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
expires_at: string;
|
|
4819
|
-
}>;
|
|
5078
|
+
expiresInSeconds?: number | null;
|
|
5079
|
+
}) => Promise<FileUploadUrl>;
|
|
5080
|
+
/** Confirm the PUT (bytes verified, object → available). Answers the
|
|
5081
|
+
* object's `expires_at` (null = never). */
|
|
4820
5082
|
complete: (objectId: string) => Promise<{
|
|
4821
5083
|
object_id: string;
|
|
4822
5084
|
status: string;
|
|
5085
|
+
checksum_sha256?: string | null;
|
|
5086
|
+
expires_at?: string | null;
|
|
4823
5087
|
}>;
|
|
4824
5088
|
downloadUrl: (objectId: string) => Promise<string>;
|
|
4825
5089
|
/** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
|
|
@@ -4852,27 +5116,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
4852
5116
|
/** Soft delete; bytes are hard-deleted 30 days later. */
|
|
4853
5117
|
delete: (objectId: string) => Promise<void>;
|
|
4854
5118
|
/** Aggregate storage usage vs quotas (the FilesManager Storage panel).
|
|
5119
|
+
* `quotas.maxTotalBytes` is the quota ENFORCED now — the plan's ceiling
|
|
5120
|
+
* unless you set a lower `quotas.maxTotalBytes` (`storage_plan.source`
|
|
5121
|
+
* says which). An object past its expiry no longer counts.
|
|
4855
5122
|
* `public_assets` = published copies vs the plan's published-bytes ceiling
|
|
4856
5123
|
* (identical bytes count once). */
|
|
4857
|
-
usage: () => Promise<
|
|
4858
|
-
usage: {
|
|
4859
|
-
object_count: number;
|
|
4860
|
-
total_bytes: number;
|
|
4861
|
-
};
|
|
4862
|
-
quotas: {
|
|
4863
|
-
maxObjectCount?: number;
|
|
4864
|
-
maxObjectBytes?: number;
|
|
4865
|
-
maxTotalBytes?: number;
|
|
4866
|
-
};
|
|
4867
|
-
available: Record<string, number | null>;
|
|
4868
|
-
public_assets?: {
|
|
4869
|
-
enabled: boolean;
|
|
4870
|
-
published_objects: number;
|
|
4871
|
-
published_bytes: number;
|
|
4872
|
-
max_published_bytes: number;
|
|
4873
|
-
bytes_remaining: number;
|
|
4874
|
-
};
|
|
4875
|
-
}>;
|
|
5124
|
+
usage: () => Promise<FilesUsage>;
|
|
4876
5125
|
/** SERVER-ONLY (403 server_only in end-user mode). Publish an available
|
|
4877
5126
|
* object to vxil's public asset host: a stable, content-addressed URL
|
|
4878
5127
|
* (`https://cdn.vxil.app/<tenant>/<sha256>.<ext>`) served from a global
|
|
@@ -4932,7 +5181,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
4932
5181
|
error?: string | null;
|
|
4933
5182
|
}>;
|
|
4934
5183
|
/** Set / extend / clear an object's TTL. `expiresInSeconds: null` clears it
|
|
4935
|
-
* (else a future auto-delete after the given seconds;
|
|
5184
|
+
* (else a future auto-delete after the given seconds; 60 s up to 100
|
|
5185
|
+
* years). An object already past its expiry is a 404 — never revived. */
|
|
4936
5186
|
setTtl: (objectId: string, expiresInSeconds: number | null) => Promise<{
|
|
4937
5187
|
object_id: string;
|
|
4938
5188
|
expires_at: string | null;
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* widens this union. Every path the SDK builds is major-versioned; the default
|
|
9
9
|
* is the compile-time constant `'v1'` (never a floating `latest` alias). */
|
|
10
10
|
import { qs } from './qs.js';
|
|
11
|
-
import { createTransport, parseRetryAfter } from './retry.js';
|
|
11
|
+
import { createTransport, parseRetryAfter, retryOnCapacity, CAPACITY_ERROR_CODES as CAPACITY_CODES } from './retry.js';
|
|
12
|
+
/** The 429 error codes `retryOnCapacity` treats as "at a capacity cap — try the
|
|
13
|
+
* SAME request again shortly". Declared here with an explicit type (not a
|
|
14
|
+
* re-export) so the served single-file index.d.ts never imports ./retry. */
|
|
15
|
+
export const CAPACITY_ERROR_CODES = CAPACITY_CODES;
|
|
12
16
|
/** Rewrite a built `/v1/<ns>/…` path onto the version configured for its
|
|
13
17
|
* namespace: the per-namespace override wins, else the global default. PURE and
|
|
14
18
|
* exported so it is unit-testable with a hypothetical future major (the runtime
|
|
@@ -349,9 +353,20 @@ export class Vxil {
|
|
|
349
353
|
body: JSON.stringify(payload ?? {}),
|
|
350
354
|
});
|
|
351
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>).
|
|
352
360
|
let e = {};
|
|
353
361
|
try {
|
|
354
|
-
|
|
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
|
+
}
|
|
355
370
|
}
|
|
356
371
|
catch { /* non-JSON error body */ }
|
|
357
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')));
|
|
@@ -688,12 +703,52 @@ export class Vxil {
|
|
|
688
703
|
* delivery): an external worker POSTs it to complete / fail the run, report
|
|
689
704
|
* progress, or wake a `wait`. A handler that answers 202 HANDS the run off
|
|
690
705
|
* — it waits for that callback (dead-lettered as `CallbackTimeout` when the
|
|
691
|
-
* lifetime passes). See `postRunCallback`.
|
|
706
|
+
* lifetime passes). See `postRunCallback`.
|
|
707
|
+
*
|
|
708
|
+
* `concurrency_key` (≤ 200 chars) + `concurrency_limit` (1..100, default
|
|
709
|
+
* 1): at most that many runs of this job_name sharing the key hold it at
|
|
710
|
+
* once — e.g. the end user's id for "one import per user". A run holds its
|
|
711
|
+
* key from its first start until it finishes (running, waiting, and
|
|
712
|
+
* between retries). The rest stay queued and start oldest first (never
|
|
713
|
+
* rejected).
|
|
714
|
+
*
|
|
715
|
+
* Queue controls (2026-10-03):
|
|
716
|
+
* - `ttl_seconds` (60 s..31 d): a run not STARTED within this long of
|
|
717
|
+
* being due is dead-lettered (`last_error_class: 'Expired'`,
|
|
718
|
+
* `job.dead_lettered` with `reason: 'expired'`) and never delivered.
|
|
719
|
+
* - `debounce: { key, delay_seconds (1..86400), max_delay_seconds? }`: the
|
|
720
|
+
* first enqueue for (job_name, key) makes a run due in delay_seconds;
|
|
721
|
+
* each later one while it has not started REPLACES its payload and pushes
|
|
722
|
+
* it to now + delay_seconds (never past first enqueue +
|
|
723
|
+
* max_delay_seconds — default 10 × delay_seconds, clamped to 1..30 d)
|
|
724
|
+
* and answers the SAME run_id with `debounced: true`.
|
|
725
|
+
* Not combinable with idempotency_key / deliver_after / delay_seconds /
|
|
726
|
+
* callback / batch_id.
|
|
727
|
+
* - `batch_id` (+ optional `batch_total`): the run joins a fan-in batch —
|
|
728
|
+
* when every run of the batch is terminal, ONE `job.batch.completed`
|
|
729
|
+
* event fires (see `batch()`). A completed batch takes no new runs (409
|
|
730
|
+
* `batch_closed`). */
|
|
692
731
|
enqueue: async (input) => (await this.call('POST', '/v1/jobs/enqueue', input)).data,
|
|
693
732
|
/** Atomic multi-enqueue (≤100 items; any invalid item rejects the whole
|
|
694
|
-
* batch). Each item = the enqueue input, incl. per-item idempotency_key
|
|
695
|
-
* and
|
|
696
|
-
|
|
733
|
+
* batch). Each item = the enqueue input, incl. per-item idempotency_key,
|
|
734
|
+
* deliver_after/delay_seconds, ttl_seconds and concurrency_key/concurrency_limit. Results align with the
|
|
735
|
+
* input order.
|
|
736
|
+
*
|
|
737
|
+
* FAN-IN: `opts.batch_id` puts every item in one batch; when every run of
|
|
738
|
+
* the batch is terminal, ONE `job.batch.completed` event fires ({ batch_id,
|
|
739
|
+
* total, succeeded, dead_lettered, cancelled }) — subscribe a function to
|
|
740
|
+
* `job.batch.` to aggregate. A batch may span several calls: pass the same
|
|
741
|
+
* batch_id and declare `batch_total` (the full size) so it cannot complete
|
|
742
|
+
* between calls. */
|
|
743
|
+
enqueueBatch: async (items, opts) => (await this.call('POST', '/v1/jobs/enqueue-batch', {
|
|
744
|
+
jobs: items,
|
|
745
|
+
...(opts?.batch_id !== undefined ? { batch_id: opts.batch_id } : {}),
|
|
746
|
+
...(opts?.batch_total !== undefined ? { batch_total: opts.batch_total } : {}),
|
|
747
|
+
})).data,
|
|
748
|
+
/** A fan-in batch: its exact counters (`total`, `open`, `succeeded`,
|
|
749
|
+
* `dead_lettered`, `cancelled`), `state` ('open' | 'completed') and the
|
|
750
|
+
* live count of its retained runs by state. 404 for an unknown batch. */
|
|
751
|
+
batch: async (batchId) => (await this.call('GET', `/v1/jobs/batches/${encodeURIComponent(batchId)}`)).data,
|
|
697
752
|
/** Enqueue a long-running EXTERNAL generation run (guide ch. 6, jobs): Vxil calls
|
|
698
753
|
* the provider (BYO key), tracks completion via poll/webhook, mirrors a typed
|
|
699
754
|
* generation_status onto a tenant record, enforces a built-in timeout, and
|
|
@@ -710,7 +765,10 @@ export class Vxil {
|
|
|
710
765
|
* FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
|
|
711
766
|
* the enqueue (insufficient balance; the run ends with job.generation.failed
|
|
712
767
|
* `ReserveInsufficient` and the 402 names its `run_id`); a runaway over the
|
|
713
|
-
* 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'`.
|
|
714
772
|
*
|
|
715
773
|
* Generic completion options for queue-style providers (fal.ai and alike —
|
|
716
774
|
* no vendor adapter): `completion.callback.query_param` puts the signed
|
|
@@ -723,17 +781,38 @@ export class Vxil {
|
|
|
723
781
|
* A mirror write the record refuses never fails the run: a body refusal
|
|
724
782
|
* (e.g. a callback key the collection does not declare) is retried with
|
|
725
783
|
* only the status column, so the row still reaches `completed`; the run then reports `mirror_error`
|
|
726
|
-
* (`JobRun.mirror_error` on `vx.jobs.run(run_id)`).
|
|
727
|
-
|
|
784
|
+
* (`JobRun.mirror_error` on `vx.jobs.run(run_id)`).
|
|
785
|
+
*
|
|
786
|
+
* `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
|
|
787
|
+
* (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
|
|
788
|
+
* `queue_full`) and a 503 `payments_unavailable`, honouring Retry-After
|
|
789
|
+
* with jitter, with the SAME
|
|
790
|
+
* idempotency_key (one is generated when the input has none), until
|
|
791
|
+
* maxWaitMs has passed — then the last 429 is thrown. */
|
|
792
|
+
generation: async (input, opts) => {
|
|
793
|
+
if (!opts?.retryOnCapacity) {
|
|
794
|
+
return (await this.call('POST', '/v1/jobs/generation', input)).data;
|
|
795
|
+
}
|
|
796
|
+
// the SAME request every time: a retry that races an accepted earlier
|
|
797
|
+
// one is deduplicated server-side by this key
|
|
798
|
+
const body = input.idempotency_key ? input : { ...input, idempotency_key: randomIdempotencyKey() };
|
|
799
|
+
return retryOnCapacity(async () => (await this.call('POST', '/v1/jobs/generation', body)).data, { maxWaitMs: opts.retryOnCapacity.maxWaitMs });
|
|
800
|
+
},
|
|
801
|
+
/** The newest runs (≤ `limit`, default 50, max 100). Filters: job_name,
|
|
802
|
+
* state, batch_id, ids, since/until (creation time, ISO). For more than
|
|
803
|
+
* one page use `runsPage` (it returns the `next_cursor`). */
|
|
728
804
|
runs: async (q) => {
|
|
729
|
-
const s =
|
|
730
|
-
job_name: q?.job_name || undefined,
|
|
731
|
-
state: q?.state || undefined,
|
|
732
|
-
ids: q?.ids,
|
|
733
|
-
limit: q?.limit || undefined,
|
|
734
|
-
});
|
|
805
|
+
const s = jobRunsQs(q);
|
|
735
806
|
return (await this.call('GET', `/v1/jobs/runs${s}`)).data.runs;
|
|
736
807
|
},
|
|
808
|
+
/** One page of runs, newest first, plus `next_cursor` (null on the last
|
|
809
|
+
* page): pass it back as `cursor` for the next older page. The keyset
|
|
810
|
+
* keeps pages stable while new runs arrive. */
|
|
811
|
+
runsPage: async (q) => {
|
|
812
|
+
const s = jobRunsQs(q);
|
|
813
|
+
const d = (await this.call('GET', `/v1/jobs/runs${s}`)).data;
|
|
814
|
+
return { runs: d.runs, next_cursor: d.next_cursor ?? null };
|
|
815
|
+
},
|
|
737
816
|
run: async (runId) => (await this.call('GET', `/v1/jobs/runs/${encodeURIComponent(runId)}`)).data,
|
|
738
817
|
/** "Wait for this run": `GET /v1/jobs/runs/{run_id}?wait=<seconds>` holds
|
|
739
818
|
* the request platform-side (re-reading the run on one bounded connection,
|
|
@@ -753,6 +832,12 @@ export class Vxil {
|
|
|
753
832
|
/** The live queue: depth by state, the oldest waiting run's age (the
|
|
754
833
|
* number to alarm on), in-flight runs per lane, dead letters in 24 h. */
|
|
755
834
|
queue: async () => (await this.call('GET', '/v1/jobs/queue')).data,
|
|
835
|
+
/** Cancel a run that has not started its current attempt (`queued`,
|
|
836
|
+
* `delayed`, `retrying`) or a plain run handed off to its signed callback
|
|
837
|
+
* (`waiting` after its handler answered 202 — the callback URL is
|
|
838
|
+
* consumed). A `running` run (a delivery in flight, or a generation
|
|
839
|
+
* waiting on its provider), a run waiting on an event, or a terminal run
|
|
840
|
+
* answers `409 not_cancellable`. A generation's credit hold is released. */
|
|
756
841
|
cancel: async (runId) => (await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/cancel`)).data,
|
|
757
842
|
/** Clone a terminal run into a fresh queued run. A generation run answers
|
|
758
843
|
* `409 not_replayable` — submit the generation again instead. */
|
|
@@ -770,10 +855,20 @@ export class Vxil {
|
|
|
770
855
|
/** Secret for verifying X-Vxil-Jobs-Signature on your callback endpoints. */
|
|
771
856
|
signingSecret: async () => (await this.call('GET', '/v1/jobs/signing-secret')).data.signing_secret,
|
|
772
857
|
schedules: {
|
|
773
|
-
/** Recurring (5-field cron
|
|
858
|
+
/** Recurring (5-field cron) or one-shot (run_at). Exactly one of cron/run_at.
|
|
859
|
+
* `timezone` (IANA, default UTC) is the zone the cron is read in, DST-
|
|
860
|
+
* correct. `external_id` makes it an UPSERT: a live schedule with the
|
|
861
|
+
* same key is updated in place (`created: false`) — one schedule per
|
|
862
|
+
* key, safe to call on every sign-in / settings save (an identical
|
|
863
|
+
* repeat writes nothing). Server mode only: in end-user mode a create
|
|
864
|
+
* with `external_id` is `403 server_only` (the key is tenant-wide).
|
|
774
865
|
* `overlap: 'skip'` (cron only): no new run while the previous one is
|
|
775
866
|
* still open — a missed window is never caught up either way. */
|
|
776
867
|
create: async (input) => (await this.call('POST', '/v1/jobs/schedules', input)).data,
|
|
868
|
+
/** Change a live schedule in place (its id, state and counters kept). A
|
|
869
|
+
* cron / run_at / timezone change recomputes the next fire. Server mode
|
|
870
|
+
* only (`403 server_only` in end-user mode). */
|
|
871
|
+
update: async (scheduleId, patch) => (await this.call('PATCH', `/v1/jobs/schedules/${encodeURIComponent(scheduleId)}`, patch)).data,
|
|
777
872
|
list: async () => (await this.call('GET', '/v1/jobs/schedules')).data.schedules,
|
|
778
873
|
delete: async (scheduleId) => {
|
|
779
874
|
await this.call('DELETE', `/v1/jobs/schedules/${encodeURIComponent(scheduleId)}`);
|
|
@@ -1752,8 +1847,15 @@ export class Vxil {
|
|
|
1752
1847
|
},
|
|
1753
1848
|
};
|
|
1754
1849
|
files = {
|
|
1755
|
-
/** 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. */
|
|
1756
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). */
|
|
1757
1859
|
complete: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/complete`)).data,
|
|
1758
1860
|
downloadUrl: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/download-url`)).data.download_url,
|
|
1759
1861
|
/** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
|
|
@@ -1777,6 +1879,9 @@ export class Vxil {
|
|
|
1777
1879
|
await this.call('DELETE', `/v1/files/${encodeURIComponent(objectId)}`);
|
|
1778
1880
|
},
|
|
1779
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.
|
|
1780
1885
|
* `public_assets` = published copies vs the plan's published-bytes ceiling
|
|
1781
1886
|
* (identical bytes count once). */
|
|
1782
1887
|
usage: async () => (await this.call('GET', '/v1/files/usage')).data,
|
|
@@ -1814,7 +1919,8 @@ export class Vxil {
|
|
|
1814
1919
|
/** Fetch the cached extraction (status: not_extracted|pending|available|failed). */
|
|
1815
1920
|
getText: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/text`)).data,
|
|
1816
1921
|
/** Set / extend / clear an object's TTL. `expiresInSeconds: null` clears it
|
|
1817
|
-
* (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. */
|
|
1818
1924
|
setTtl: async (objectId, expiresInSeconds) => (await this.call('PUT', `/v1/files/${encodeURIComponent(objectId)}/ttl`, { expiresInSeconds })).data,
|
|
1819
1925
|
/** SERVER-ONLY (403 server_only in end-user mode). Account merge: move
|
|
1820
1926
|
* every object `from_user_id` owns (every status, tombstones included) and
|
|
@@ -2350,3 +2456,23 @@ export class Vxil {
|
|
|
2350
2456
|
// Failure reporting for tenant functions — the Sentry-envelope forwarder
|
|
2351
2457
|
// (guide ch. 8). Zero dependencies; see reporting.ts.
|
|
2352
2458
|
export { withReporting, report, buildEnvelope, parseDsn, exceptionEvent, reportServerErrors, REPORT_TIMEOUT_MS, } from './reporting.js';
|
|
2459
|
+
/** The query string of `vx.jobs.runs` / `runsPage`. */
|
|
2460
|
+
function jobRunsQs(q) {
|
|
2461
|
+
return qs({
|
|
2462
|
+
job_name: q?.job_name || undefined,
|
|
2463
|
+
state: q?.state || undefined,
|
|
2464
|
+
ids: q?.ids,
|
|
2465
|
+
batch_id: q?.batch_id || undefined,
|
|
2466
|
+
since: q?.since || undefined,
|
|
2467
|
+
until: q?.until || undefined,
|
|
2468
|
+
cursor: q?.cursor || undefined,
|
|
2469
|
+
limit: q?.limit || undefined,
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
/** A fresh idempotency key (crypto.randomUUID where the runtime has it). */
|
|
2473
|
+
function randomIdempotencyKey() {
|
|
2474
|
+
const c = globalThis.crypto;
|
|
2475
|
+
if (c?.randomUUID)
|
|
2476
|
+
return `sdk-${c.randomUUID()}`;
|
|
2477
|
+
return `sdk-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
2478
|
+
}
|
package/dist/retry.d.ts
CHANGED
|
@@ -45,3 +45,36 @@ export interface Transport {
|
|
|
45
45
|
* `retry` / `timeoutMs` / `hooks` set this is one `fetch` + `text()` — the
|
|
46
46
|
* pre-seam behaviour, byte for byte. */
|
|
47
47
|
export declare function createTransport(opts: TransportOptions): Transport;
|
|
48
|
+
/** The 429 answers that mean "the project is at a capacity cap right now —
|
|
49
|
+
* try the SAME request again shortly" (the generation lane's in-flight cap
|
|
50
|
+
* and outstanding-holds ceiling, and the queue admission cap). Any other 429
|
|
51
|
+
* (a rate limit, a plan quota) is not a capacity answer and is rethrown. */
|
|
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;
|
|
61
|
+
/** `retryOnCapacity` options. */
|
|
62
|
+
export interface RetryOnCapacityOptions {
|
|
63
|
+
/** Give up (rethrow the last 429) once waiting again would pass this many
|
|
64
|
+
* milliseconds since the first attempt. */
|
|
65
|
+
maxWaitMs: number;
|
|
66
|
+
sleep?: (ms: number) => Promise<void>;
|
|
67
|
+
random?: () => number;
|
|
68
|
+
now?: () => number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`) or
|
|
72
|
+
* a 503 `payments_unavailable` (`UNAVAILABLE_ERROR_CODES`):
|
|
73
|
+
* wait the server's `Retry-After` (else 5 s) plus up to 20 % jitter (≤ 1 s,
|
|
74
|
+
* so many clients told the same second do not all return on it), and never
|
|
75
|
+
* past `maxWaitMs` since the first attempt — then the last 429 is rethrown.
|
|
76
|
+
* The caller makes `attempt` repeat the SAME request (same idempotency key),
|
|
77
|
+
* so a retry that races an earlier accepted one is deduplicated server-side.
|
|
78
|
+
* Duck-typed on `{ status, code, retryAfter }` (VxilError's fields).
|
|
79
|
+
*/
|
|
80
|
+
export declare function retryOnCapacity<T>(attempt: () => Promise<T>, opts: RetryOnCapacityOptions): Promise<T>;
|
package/dist/retry.js
CHANGED
|
@@ -154,3 +154,60 @@ export function createTransport(opts) {
|
|
|
154
154
|
},
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
+
// ─── capacity retry (`vx.jobs.generation(…, { retryOnCapacity })`) ────────────
|
|
158
|
+
/** The 429 answers that mean "the project is at a capacity cap right now —
|
|
159
|
+
* try the SAME request again shortly" (the generation lane's in-flight cap
|
|
160
|
+
* and outstanding-holds ceiling, and the queue admission cap). Any other 429
|
|
161
|
+
* (a rate limit, a plan quota) is not a capacity answer and is rethrown. */
|
|
162
|
+
export const CAPACITY_ERROR_CODES = new Set([
|
|
163
|
+
'generation_concurrency_exceeded', 'reserve_holds_exceeded', 'queue_full',
|
|
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
|
+
}
|
|
179
|
+
/** Fallback wait (ms) when a capacity 429 carries no Retry-After. */
|
|
180
|
+
const CAPACITY_DEFAULT_WAIT_MS = 5_000;
|
|
181
|
+
/**
|
|
182
|
+
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`) or
|
|
183
|
+
* a 503 `payments_unavailable` (`UNAVAILABLE_ERROR_CODES`):
|
|
184
|
+
* wait the server's `Retry-After` (else 5 s) plus up to 20 % jitter (≤ 1 s,
|
|
185
|
+
* so many clients told the same second do not all return on it), and never
|
|
186
|
+
* past `maxWaitMs` since the first attempt — then the last 429 is rethrown.
|
|
187
|
+
* The caller makes `attempt` repeat the SAME request (same idempotency key),
|
|
188
|
+
* so a retry that races an earlier accepted one is deduplicated server-side.
|
|
189
|
+
* Duck-typed on `{ status, code, retryAfter }` (VxilError's fields).
|
|
190
|
+
*/
|
|
191
|
+
export async function retryOnCapacity(attempt, opts) {
|
|
192
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
193
|
+
const random = opts.random ?? Math.random;
|
|
194
|
+
const now = opts.now ?? Date.now;
|
|
195
|
+
const started = now();
|
|
196
|
+
for (;;) {
|
|
197
|
+
try {
|
|
198
|
+
return await attempt();
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
const err = e;
|
|
202
|
+
if (!err || !isCapacityRetryable(err))
|
|
203
|
+
throw e;
|
|
204
|
+
const baseMs = typeof err.retryAfter === 'number' && err.retryAfter >= 0
|
|
205
|
+
? Math.round(err.retryAfter * 1000)
|
|
206
|
+
: CAPACITY_DEFAULT_WAIT_MS;
|
|
207
|
+
const waitMs = baseMs + Math.round(Math.min(1_000, baseMs * 0.2) * random());
|
|
208
|
+
if (now() - started + waitMs > opts.maxWaitMs)
|
|
209
|
+
throw e;
|
|
210
|
+
await sleep(waitMs);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
package/package.json
CHANGED