@giveitsmaller/sdk 0.26.0 → 0.28.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/_audit.js +81 -3
- package/dist/builder.d.ts +47 -13
- package/dist/builder.js +104 -26
- package/dist/client.d.ts +13 -0
- package/dist/client.js +13 -1
- package/dist/credentials.d.ts +10 -3
- package/dist/credentials.js +34 -5
- package/dist/ergonomic/option_types.d.ts +1 -0
- package/dist/file-first.d.ts +7 -3
- package/dist/file-first.js +34 -11
- package/dist/handle.js +2 -1
- package/dist/index.core.d.ts +1 -0
- package/dist/index.core.js +25 -0
- package/dist/merge.d.ts +2 -2
- package/dist/merge.js +14 -7
- package/dist/types.d.ts +1 -1
- package/package.json +5 -3
package/dist/_audit.js
CHANGED
|
@@ -75,12 +75,90 @@ export function _runAudit() {
|
|
|
75
75
|
// methods EXIST and their signatures/return types match (indexed access errors
|
|
76
76
|
// if a method is missing; the typed LHS errors if the signature drifts). The
|
|
77
77
|
// RHS is a type-only cast (`null as unknown as …`) — no runtime property read.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
//
|
|
79
|
+
// 🔴 THE ASSIGNMENT FORM BELOW WAS WEAKER THAN IT LOOKED, AND IS NOW FIXED.
|
|
80
|
+
// A single assignment tests ONE-WAY assignability, so a drift to `any` — or to any
|
|
81
|
+
// broader callable — stays assignable and PASSES. Assigning both ways does not fix
|
|
82
|
+
// it either: `any` is assignable in both directions. Real equality needs a
|
|
83
|
+
// conditional-type helper, and this repo already had one in
|
|
84
|
+
// `ergonomic/option_types.ts`, used there to pin option key-sets to the contract.
|
|
85
|
+
// ⇒ These three have been decorative since 8yqUXLCS shipped them. Tightened here
|
|
86
|
+
// rather than left one-way beside sixteen correct ones.
|
|
87
|
+
const _creditsSig = true;
|
|
88
|
+
const _creditsUsageSig = true;
|
|
89
|
+
const _limitsSig = true;
|
|
81
90
|
void _creditsSig;
|
|
82
91
|
void _creditsUsageSig;
|
|
83
92
|
void _limitsSig;
|
|
93
|
+
// ── BQXpFV2R — the thirteen unpinned ergonomic symbols ────────────────────────
|
|
94
|
+
//
|
|
95
|
+
// ⚠️ EXISTENCE IS NOW THE SNAPSHOT'S JOB, not this list's.
|
|
96
|
+
// `tests/api-surface.test.ts` computes all 381 exports from source and compares
|
|
97
|
+
// them to a committed file, so a symbol cannot go unpinned because nobody
|
|
98
|
+
// remembered it — which is exactly how these thirteen were missed, alongside the
|
|
99
|
+
// whole `Gisl*Error` tree and `GislClient` itself. What remains here is the part a
|
|
100
|
+
// name-and-kind snapshot CANNOT express: SIGNATURES.
|
|
101
|
+
//
|
|
102
|
+
// ⚠️ COVERAGE BOUNDARY, stated so the gate is not mistaken for complete:
|
|
103
|
+
// signatures are pinned for the nine builders' execution methods and the three
|
|
104
|
+
// ErgonomicClient accessors above. A signature change to `GislClient`, `gisl`,
|
|
105
|
+
// `create` or `parseSseStream` is caught by NOTHING here. That is a deliberate
|
|
106
|
+
// scope line — widening it is ticket `YebCTMuY`.
|
|
107
|
+
accept();
|
|
108
|
+
accept();
|
|
109
|
+
accept();
|
|
110
|
+
accept();
|
|
111
|
+
accept();
|
|
112
|
+
accept();
|
|
113
|
+
accept();
|
|
114
|
+
accept();
|
|
115
|
+
accept();
|
|
116
|
+
accept();
|
|
117
|
+
accept();
|
|
118
|
+
accept();
|
|
119
|
+
accept();
|
|
120
|
+
accept();
|
|
121
|
+
accept();
|
|
122
|
+
accept();
|
|
123
|
+
accept();
|
|
124
|
+
accept();
|
|
125
|
+
accept();
|
|
126
|
+
const _recipeRun = true;
|
|
127
|
+
const _recipeSubmit = true;
|
|
128
|
+
const _filesRun = true;
|
|
129
|
+
const _filesSubmit = true;
|
|
130
|
+
const _mergedRun = true;
|
|
131
|
+
const _mergedSubmit = true;
|
|
132
|
+
const _archivedRun = true;
|
|
133
|
+
const _archivedSubmit = true;
|
|
134
|
+
const _watermarkedRun = true;
|
|
135
|
+
const _watermarkedSubmit = true;
|
|
136
|
+
const _batchRun = true;
|
|
137
|
+
const _opRun = true;
|
|
138
|
+
const _opSubmit = true;
|
|
139
|
+
const _mergeRun = true;
|
|
140
|
+
const _mergeSubmit = true;
|
|
141
|
+
// ⚠️ `Promise<Result>`, NOT `Promise<Result[]>`. A fan-out returns ONE aggregate
|
|
142
|
+
// result carrying `childWorkflowIds`, not an array. I wrote `Result[]` from
|
|
143
|
+
// assumption and this pin failed on its first compile — the gate catching a wrong
|
|
144
|
+
// belief before any mutation test, which is the whole point of writing it out.
|
|
145
|
+
const _mapEachRun = true;
|
|
146
|
+
void _recipeRun;
|
|
147
|
+
void _recipeSubmit;
|
|
148
|
+
void _filesRun;
|
|
149
|
+
void _filesSubmit;
|
|
150
|
+
void _mergedRun;
|
|
151
|
+
void _mergedSubmit;
|
|
152
|
+
void _archivedRun;
|
|
153
|
+
void _archivedSubmit;
|
|
154
|
+
void _watermarkedRun;
|
|
155
|
+
void _watermarkedSubmit;
|
|
156
|
+
void _batchRun;
|
|
157
|
+
void _opRun;
|
|
158
|
+
void _opSubmit;
|
|
159
|
+
void _mergeRun;
|
|
160
|
+
void _mergeSubmit;
|
|
161
|
+
void _mapEachRun;
|
|
84
162
|
accept();
|
|
85
163
|
accept();
|
|
86
164
|
accept();
|
package/dist/builder.d.ts
CHANGED
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
* `SseOperationProgressData`. The `phase` discriminator is SDK-added;
|
|
21
21
|
* `status` values pass through verbatim from `SseOperationProgressDataStatusEnum`.
|
|
22
22
|
* The wire does NOT carry a `phase` field — see karen reality-check 2026-05-23.
|
|
23
|
-
* - `.run()`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* - `.run()` takes an OPTIONAL options bag; `maxWait` defaults to
|
|
24
|
+
* `DEFAULT_POLL_TIMEOUT_MS`, the same constant the poll fallback and every
|
|
25
|
+
* file-first builder use. It was mandatory until 36AZ98FV, on the grounds
|
|
26
|
+
* that inheriting that default would "leak silently" — while the SDK applied
|
|
27
|
+
* it at fourteen sites regardless.
|
|
26
28
|
*/
|
|
27
29
|
import type { GislClient } from './client.js';
|
|
28
30
|
import type { OperationDownload, WorkflowStatusResponse, SseOperationProgressDataStatusEnum } from '@giveitsmaller/contracts/openapi';
|
|
@@ -235,12 +237,17 @@ export type ProgressEvent = UploadProgressEvent | ProcessingProgressEvent;
|
|
|
235
237
|
export interface RunOptions {
|
|
236
238
|
/**
|
|
237
239
|
* Wall-clock deadline for the entire run (upload + create + wait + downloads).
|
|
238
|
-
*
|
|
239
|
-
* `
|
|
240
|
-
*
|
|
241
|
-
* a
|
|
240
|
+
* Optional; defaults to {@link DEFAULT_POLL_TIMEOUT_MS}. Pass `'2h'` / `'30m'` /
|
|
241
|
+
* `'120s'` as a string suffix or a number of milliseconds.
|
|
242
|
+
*
|
|
243
|
+
* ⚠️ THIS WAS MANDATORY, on the stated grounds that a 600s default "would
|
|
244
|
+
* otherwise leak silently" (36AZ98FV). The same tree applied exactly that
|
|
245
|
+
* default at FOURTEEN sites across both SDKs, so the prohibition was refuted
|
|
246
|
+
* by the code it protected — and the file-first spelling of the same task
|
|
247
|
+
* accepted no arguments at all. One shared constant is what makes the rule
|
|
248
|
+
* unnecessary rather than what breaks it.
|
|
242
249
|
*/
|
|
243
|
-
readonly maxWait
|
|
250
|
+
readonly maxWait?: string | number;
|
|
244
251
|
/** Abort signal — terminates upload, SSE, and poll cleanly. */
|
|
245
252
|
readonly signal?: AbortSignal;
|
|
246
253
|
/** Progress callback receiving the SDK-synthesised discriminated union. */
|
|
@@ -265,8 +272,18 @@ export interface RunOptions {
|
|
|
265
272
|
readonly probeTimeoutMs?: number;
|
|
266
273
|
}
|
|
267
274
|
export interface SubmitOptions {
|
|
268
|
-
/**
|
|
269
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Webhook URL — wired to `WorkflowCreateRequest.callback_url`.
|
|
277
|
+
*
|
|
278
|
+
* Optional. `callback_url` is not in `WorkflowCreateRequest`'s required set and
|
|
279
|
+
* is typed `string | null`, so omitting it is contract-valid. It was mandatory
|
|
280
|
+
* here because the returned {@link Handle} carried no client and its
|
|
281
|
+
* `status()`/`wait()`/`result()` threw `no_client` — the webhook was the only
|
|
282
|
+
* channel by which the outcome could be learned. The handle is bound now
|
|
283
|
+
* (36AZ98FV), so omitting the webhook leaves a usable handle rather than a
|
|
284
|
+
* dead end.
|
|
285
|
+
*/
|
|
286
|
+
readonly webhook?: string;
|
|
270
287
|
/**
|
|
271
288
|
* Best-effort probe-before-create for a VIDEO upload that went multipart.
|
|
272
289
|
* Default `true`; set `false` to skip the wait. See {@link RunOptions}.
|
|
@@ -341,14 +358,14 @@ export declare class OperationBuilder {
|
|
|
341
358
|
* fetches downloads, and projects to a flat `Result`. Throws
|
|
342
359
|
* `GislTimeoutError` if `maxWait` elapses before terminal status.
|
|
343
360
|
*/
|
|
344
|
-
run(options
|
|
361
|
+
run(options?: RunOptions): Promise<Result>;
|
|
345
362
|
/**
|
|
346
363
|
* Fire-and-forget: upload the input + create the workflow with a
|
|
347
364
|
* `callback_url` wired to the supplied `webhook`, then return a
|
|
348
365
|
* `Handle` (workflowId + webhookSecret) without waiting. The webhook
|
|
349
366
|
* receives completion + the `webhookSecret` is the verifier seed.
|
|
350
367
|
*/
|
|
351
|
-
submit(options
|
|
368
|
+
submit(options?: SubmitOptions): Promise<Handle>;
|
|
352
369
|
/**
|
|
353
370
|
* Fan-out chain: run this builder to completion, then for each artifact
|
|
354
371
|
* in the resulting `Result`, call `fn(artifactRef)` to construct a
|
|
@@ -391,8 +408,25 @@ export declare class MapEachBuilder {
|
|
|
391
408
|
* run + every child's full run — each child sees the REMAINING budget
|
|
392
409
|
* after the parent and prior children completed. Signal aborts cascade.
|
|
393
410
|
*/
|
|
394
|
-
run(options
|
|
411
|
+
run(options?: RunOptions): Promise<Result>;
|
|
395
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* The clamp itself, exported for an EXACT test (codex d218bd6a0c62).
|
|
415
|
+
*
|
|
416
|
+
* ⚠️ **A behavioural test cannot pin this number, and that is why this seam
|
|
417
|
+
* exists.** Counting requests over a real deadline discriminates 1000 ms from
|
|
418
|
+
* 100 and from 500, but it cannot tell 1000 from 750 — the counts collide inside
|
|
419
|
+
* scheduler jitter. Widening the window to separate them makes the suite slower
|
|
420
|
+
* and the test flakier, in exchange for a weaker claim.
|
|
421
|
+
*
|
|
422
|
+
* ⇒ So the two tests do different jobs and neither is redundant: this one pins
|
|
423
|
+
* the VALUE exactly, and the `run()` test proves the clamp is on the path a
|
|
424
|
+
* caller actually travels. A value test alone would pass while nothing called
|
|
425
|
+
* it; a path test alone would pass at 750 ms.
|
|
426
|
+
*
|
|
427
|
+
* @internal — not re-exported from the package barrel.
|
|
428
|
+
*/
|
|
429
|
+
export declare function _clampPollIntervalMs(requested: number | undefined): number;
|
|
396
430
|
/** @internal — exported for reuse by `merge.ts` (T3) and future builders. */
|
|
397
431
|
export declare function _consumeSseToTerminal(client: GislClient, args: {
|
|
398
432
|
workflowId: string;
|
package/dist/builder.js
CHANGED
|
@@ -20,10 +20,13 @@
|
|
|
20
20
|
* `SseOperationProgressData`. The `phase` discriminator is SDK-added;
|
|
21
21
|
* `status` values pass through verbatim from `SseOperationProgressDataStatusEnum`.
|
|
22
22
|
* The wire does NOT carry a `phase` field — see karen reality-check 2026-05-23.
|
|
23
|
-
* - `.run()`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* - `.run()` takes an OPTIONAL options bag; `maxWait` defaults to
|
|
24
|
+
* `DEFAULT_POLL_TIMEOUT_MS`, the same constant the poll fallback and every
|
|
25
|
+
* file-first builder use. It was mandatory until 36AZ98FV, on the grounds
|
|
26
|
+
* that inheriting that default would "leak silently" — while the SDK applied
|
|
27
|
+
* it at fourteen sites regardless.
|
|
26
28
|
*/
|
|
29
|
+
import { DEFAULT_POLL_TIMEOUT_MS } from './client.js';
|
|
27
30
|
import { SseEventType, SseOperationProgressDataFromJSON, } from '@giveitsmaller/contracts/openapi';
|
|
28
31
|
import { uploadSource } from './types.js';
|
|
29
32
|
import { GislTimeoutError, GislFanOutTimeoutError, GislNetworkError, GislStreamHostNotDeclaredError, GislTransportError, SseEndedWithoutTerminal } from './errors.js';
|
|
@@ -253,8 +256,8 @@ export class OperationBuilder {
|
|
|
253
256
|
* fetches downloads, and projects to a flat `Result`. Throws
|
|
254
257
|
* `GislTimeoutError` if `maxWait` elapses before terminal status.
|
|
255
258
|
*/
|
|
256
|
-
async run(options) {
|
|
257
|
-
const deadline = Date.now() + _parseMaxWait(options.maxWait);
|
|
259
|
+
async run(options = {}) {
|
|
260
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
258
261
|
const signal = options.signal;
|
|
259
262
|
const onProgress = options.onProgress;
|
|
260
263
|
const useSSE = options.useSSE ?? true;
|
|
@@ -333,7 +336,7 @@ export class OperationBuilder {
|
|
|
333
336
|
* `Handle` (workflowId + webhookSecret) without waiting. The webhook
|
|
334
337
|
* receives completion + the `webhookSecret` is the verifier seed.
|
|
335
338
|
*/
|
|
336
|
-
async submit(options) {
|
|
339
|
+
async submit(options = {}) {
|
|
337
340
|
// Resolve presets before any I/O so a GislConfigError fails the
|
|
338
341
|
// call before the upload — same fail-early contract as run().
|
|
339
342
|
const resolved = this._resolve();
|
|
@@ -350,14 +353,22 @@ export class OperationBuilder {
|
|
|
350
353
|
source: uploadSource(uploadResp.fileId),
|
|
351
354
|
operations: [{ type: this.opType, options: resolved.wireOptions }],
|
|
352
355
|
};
|
|
356
|
+
// ⚠️ OMIT THE KEY, do not set it to `undefined`. `JSON.stringify` drops an
|
|
357
|
+
// undefined value so the wire is the same either way — but an own property
|
|
358
|
+
// that exists with no value makes `'callback_url' in payload` TRUE, so any
|
|
359
|
+
// test asserting omission by key passes vacuously (codex 4763eb48189a).
|
|
353
360
|
const payload = {
|
|
354
361
|
jobs: [job],
|
|
355
|
-
callback_url: options.webhook,
|
|
362
|
+
...(options.webhook !== undefined ? { callback_url: options.webhook } : {}),
|
|
356
363
|
};
|
|
357
364
|
const created = await this.client.createWorkflow(payload);
|
|
358
|
-
//
|
|
359
|
-
// throw `no_client
|
|
360
|
-
|
|
365
|
+
// ⚠️ THE CLIENT IS THE POINT (36AZ98FV). Without it the returned Handle's
|
|
366
|
+
// status()/wait()/result() throw `no_client`, which made `webhook` the only
|
|
367
|
+
// channel for this call's outcome and is why it used to be mandatory. The
|
|
368
|
+
// file-first path has always passed it (`file-first.ts`); the operation-first
|
|
369
|
+
// path did not, which is the same "demands what file-first does not" defect
|
|
370
|
+
// this ticket is about, one layer down.
|
|
371
|
+
return new Handle(created.workflowId, created.webhookSecret != null ? created.webhookSecret : undefined, this.client, null);
|
|
361
372
|
}
|
|
362
373
|
/**
|
|
363
374
|
* Fan-out chain: run this builder to completion, then for each artifact
|
|
@@ -438,8 +449,8 @@ export class MapEachBuilder {
|
|
|
438
449
|
* run + every child's full run — each child sees the REMAINING budget
|
|
439
450
|
* after the parent and prior children completed. Signal aborts cascade.
|
|
440
451
|
*/
|
|
441
|
-
async run(options) {
|
|
442
|
-
const deadline = Date.now() + _parseMaxWait(options.maxWait);
|
|
452
|
+
async run(options = {}) {
|
|
453
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
443
454
|
// 1. Run the parent.
|
|
444
455
|
const remainingForParent = Math.max(1, deadline - Date.now());
|
|
445
456
|
const parentResult = await this.parent.run({
|
|
@@ -540,6 +551,82 @@ function aggregateWorkflowStatus(statuses) {
|
|
|
540
551
|
// ---------------------------------------------------------------------------
|
|
541
552
|
// Internals — SSE + poll
|
|
542
553
|
// ---------------------------------------------------------------------------
|
|
554
|
+
/**
|
|
555
|
+
* Poll-fallback interval bounds, in milliseconds.
|
|
556
|
+
*
|
|
557
|
+
* 🔴 **THE FLOOR IS SIZED AGAINST A PUBLISHED RATE LIMIT, NOT AGAINST A
|
|
558
|
+
* BUSY-LOOP.** Its predecessor was 100 ms and its comment said it guarded
|
|
559
|
+
* against values "that would hammer getWorkflowStatus" — which is what you
|
|
560
|
+
* write when you are stopping a `0`/`NaN` spin, not when you have asked what
|
|
561
|
+
* the server allows. Same words, different standard, and nothing recorded
|
|
562
|
+
* which one applied (`r7bpd7MY`).
|
|
563
|
+
*
|
|
564
|
+
* The limit, read from `compression_api` rather than relayed:
|
|
565
|
+
* `Identity/Application/RateLimiting/TieredRateLimiterService.php:37-39`
|
|
566
|
+
* declares the `status_poll` family — guarding `GET /api/workflows/{id}/status`
|
|
567
|
+
* and `/downloads` — as a **sliding window of 60 requests per minute**, scaled
|
|
568
|
+
* at consume time (`:177`, `:188`) by `UserTier::rateLimitMultiplier()`
|
|
569
|
+
* (`Identity/Domain/Enums/UserTier.php:186-194`): Free and Basic ×1, Pro ×5,
|
|
570
|
+
* Max ×15, Enterprise ×20.
|
|
571
|
+
*
|
|
572
|
+
* ⇒ 100 ms is **600 requests/minute**: ten times the Free ceiling, twice Pro's,
|
|
573
|
+
* and inside budget only on Max and Enterprise. 1000 ms is the minimum legal
|
|
574
|
+
* interval on the tightest tier, so it is correct on every tier and needs no
|
|
575
|
+
* tier knowledge in the SDK.
|
|
576
|
+
*
|
|
577
|
+
* ⚠️ **THE WINDOW IS SLIDING, SO IT PUNISHES BURSTS, NOT JUST AVERAGES** — ten
|
|
578
|
+
* polls in the first second genuinely consume ten of the sixty.
|
|
579
|
+
*
|
|
580
|
+
* 📌 **AND THE BUDGET IS SHARED**: keyed per user id when authenticated, per IP
|
|
581
|
+
* when not. N SDK instances under one account spend one allowance between them,
|
|
582
|
+
* which is the argument for the conservative floor even on a paid tier — this
|
|
583
|
+
* process cannot see the other two.
|
|
584
|
+
*
|
|
585
|
+
* A tier-aware floor would need the caller's tier, which neither SDK reads
|
|
586
|
+
* today, and api has carded the effective-limits field as `4cHoxAcm` (Backlog,
|
|
587
|
+
* unscheduled — deliberately, because this flat floor ships without it).
|
|
588
|
+
*
|
|
589
|
+
* ⛔ **WHAT THIS DOES NOT DO, STATED SO NOBODY READS IT AS A GUARANTEE (codex
|
|
590
|
+
* 6b56d75f126d).** This is a PER-CALL floor, not a per-credential budget:
|
|
591
|
+
*
|
|
592
|
+
* - **Two concurrent `run()`s under one credential issue ~120 requests/minute**
|
|
593
|
+
* and blow a 60/minute bucket. Bounding that needs coordination the SDK does
|
|
594
|
+
* not have — a shared limiter across calls, processes and machines.
|
|
595
|
+
* - **`getWorkflowDownloads` draws on the SAME `status_poll` family**, so a run
|
|
596
|
+
* that spends the last token on a status poll can be 429'd on the terminal
|
|
597
|
+
* downloads call it needs to finish.
|
|
598
|
+
*
|
|
599
|
+
* ⇒ This change makes a SINGLE run legal on every tier. It does not make the SDK
|
|
600
|
+
* rate-limit-safe under concurrency, and the honest next step is retry-on-429
|
|
601
|
+
* with `Retry-After`, not a larger number here. Tracked separately.
|
|
602
|
+
*/
|
|
603
|
+
const MIN_POLL_INTERVAL_MS = 1_000;
|
|
604
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
605
|
+
/**
|
|
606
|
+
* The clamp itself, exported for an EXACT test (codex d218bd6a0c62).
|
|
607
|
+
*
|
|
608
|
+
* ⚠️ **A behavioural test cannot pin this number, and that is why this seam
|
|
609
|
+
* exists.** Counting requests over a real deadline discriminates 1000 ms from
|
|
610
|
+
* 100 and from 500, but it cannot tell 1000 from 750 — the counts collide inside
|
|
611
|
+
* scheduler jitter. Widening the window to separate them makes the suite slower
|
|
612
|
+
* and the test flakier, in exchange for a weaker claim.
|
|
613
|
+
*
|
|
614
|
+
* ⇒ So the two tests do different jobs and neither is redundant: this one pins
|
|
615
|
+
* the VALUE exactly, and the `run()` test proves the clamp is on the path a
|
|
616
|
+
* caller actually travels. A value test alone would pass while nothing called
|
|
617
|
+
* it; a path test alone would pass at 750 ms.
|
|
618
|
+
*
|
|
619
|
+
* @internal — not re-exported from the package barrel.
|
|
620
|
+
*/
|
|
621
|
+
export function _clampPollIntervalMs(requested) {
|
|
622
|
+
if (requested === undefined) {
|
|
623
|
+
return DEFAULT_POLL_INTERVAL_MS;
|
|
624
|
+
}
|
|
625
|
+
if (!Number.isFinite(requested) || requested < MIN_POLL_INTERVAL_MS) {
|
|
626
|
+
return MIN_POLL_INTERVAL_MS;
|
|
627
|
+
}
|
|
628
|
+
return requested;
|
|
629
|
+
}
|
|
543
630
|
const TERMINAL_STATUS = new Set([
|
|
544
631
|
'completed',
|
|
545
632
|
'failed',
|
|
@@ -705,20 +792,11 @@ export async function _consumeSseToTerminal(client, args) {
|
|
|
705
792
|
export async function _pollToTerminal(client, args) {
|
|
706
793
|
// Codex r1 medium 89130e3ea75d — guard against 0/negative/NaN/Infinity
|
|
707
794
|
// pollIntervalMs values that would hammer getWorkflowStatus until maxWait.
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
else if (!Number.isFinite(requested) || requested < 100) {
|
|
714
|
-
// Clamp to a safe minimum (100ms) rather than throw — small/zero/NaN
|
|
715
|
-
// were almost certainly a caller mistake, but ergonomic-layer
|
|
716
|
-
// shouldn't crash an otherwise valid run on this.
|
|
717
|
-
intervalMs = 100;
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
intervalMs = requested;
|
|
721
|
-
}
|
|
795
|
+
// Clamp rather than throw — a zero/NaN/tiny value is a caller mistake, and the
|
|
796
|
+
// ergonomic layer should not crash an otherwise valid run over it. ONE call
|
|
797
|
+
// site, so the exact test above and the behavioural test below are talking
|
|
798
|
+
// about the same code.
|
|
799
|
+
const intervalMs = _clampPollIntervalMs(args.pollIntervalMs);
|
|
722
800
|
while (true) {
|
|
723
801
|
_checkAborted(args.signal);
|
|
724
802
|
if (Date.now() >= args.deadline) {
|
package/dist/client.d.ts
CHANGED
|
@@ -2,6 +2,19 @@ import type { AudioWatermarkDecodeRequest, AudioWatermarkDecodeResponse, Externa
|
|
|
2
2
|
import type { CreditsUsageOptions, ListWorkflowsOptions, GetSchemaOptions, GetSchemaResult, GislClientConfig, GislSseEvent, GislSseParseFailure, PreflightClipsResult, ProbeWaitOptions, ProbeWaitResult, ReadCapabilityOptions, UploadOptions, WaitOptions, WorkflowCreatePayload, _Sdk3HandCodedKeepaliveResult, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignPartsResult } from './types.js';
|
|
3
3
|
export declare const MULTIPART_CONCURRENCY_DEFAULT: 4;
|
|
4
4
|
export declare const DEFAULT_MULTIPART_FIRST_CHUNK_SIZE: number;
|
|
5
|
+
/**
|
|
6
|
+
* THE ONE DEADLINE DEFAULT (36AZ98FV). One wall-clock deadline covers the whole
|
|
7
|
+
* operation — upload, create, wait, downloads. Every entry point that takes a
|
|
8
|
+
* `maxWait` defaults to THIS value; a caller who passes one overrides it, and a
|
|
9
|
+
* caller who does not gets the same deadline whichever spelling they used.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ EXPORTED FOR REUSE, NOT FOR CONSUMERS. It is deliberately absent from the
|
|
12
|
+
* package entry points, so it does not reach the public API surface and the
|
|
13
|
+
* committed export snapshots do not move. Before this ticket the number was
|
|
14
|
+
* already named here and in PHP's `WorkflowConstants`, and hard-coded at
|
|
15
|
+
* FOURTEEN defaulting sites anyway — which is the duplication this replaces.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_POLL_TIMEOUT_MS = 600000;
|
|
5
18
|
export interface ValidationDetail {
|
|
6
19
|
message: string;
|
|
7
20
|
field?: string;
|
package/dist/client.js
CHANGED
|
@@ -63,7 +63,19 @@ const S3_MAX_MULTIPART_PARTS = 10_000;
|
|
|
63
63
|
// exists to prevent). codex review (high).
|
|
64
64
|
const RECOMMENDED_CHUNK_SIZE_MAX_BYTES = 104_857_600; // 100 MiB
|
|
65
65
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* THE ONE DEADLINE DEFAULT (36AZ98FV). One wall-clock deadline covers the whole
|
|
68
|
+
* operation — upload, create, wait, downloads. Every entry point that takes a
|
|
69
|
+
* `maxWait` defaults to THIS value; a caller who passes one overrides it, and a
|
|
70
|
+
* caller who does not gets the same deadline whichever spelling they used.
|
|
71
|
+
*
|
|
72
|
+
* ⚠️ EXPORTED FOR REUSE, NOT FOR CONSUMERS. It is deliberately absent from the
|
|
73
|
+
* package entry points, so it does not reach the public API surface and the
|
|
74
|
+
* committed export snapshots do not move. Before this ticket the number was
|
|
75
|
+
* already named here and in PHP's `WorkflowConstants`, and hard-coded at
|
|
76
|
+
* FOURTEEN defaulting sites anyway — which is the duplication this replaces.
|
|
77
|
+
*/
|
|
78
|
+
export const DEFAULT_POLL_TIMEOUT_MS = 600_000; // 10 min
|
|
67
79
|
/**
|
|
68
80
|
* Re-tag a bare per-request transport {@link GislTimeoutError} with the workflow
|
|
69
81
|
* it was scoped to, so a timed-out workflow read (status / downloads) stays
|
package/dist/credentials.d.ts
CHANGED
|
@@ -21,10 +21,10 @@ export declare const GISL_STREAM_BASE_URL_ENV = "GISL_STREAM_BASE_URL";
|
|
|
21
21
|
* Named environments → base URLs. Kept colocated with the resolver so the
|
|
22
22
|
* mapping table doesn't leak into `gisl.ts`.
|
|
23
23
|
*/
|
|
24
|
-
export declare const ENVIRONMENT_ENDPOINTS: {
|
|
24
|
+
export declare const ENVIRONMENT_ENDPOINTS: Readonly<{
|
|
25
25
|
readonly prod: "https://api.giveitsmaller.com";
|
|
26
26
|
readonly staging: "https://api.staging.giveitsmaller.com";
|
|
27
|
-
}
|
|
27
|
+
}>;
|
|
28
28
|
export type Environment = keyof typeof ENVIRONMENT_ENDPOINTS;
|
|
29
29
|
export declare const DEFAULT_ENDPOINT: "https://api.giveitsmaller.com";
|
|
30
30
|
/**
|
|
@@ -61,8 +61,15 @@ export declare const DEFAULT_ENDPOINT: "https://api.giveitsmaller.com";
|
|
|
61
61
|
* `localhost` is intentionally absent too: it is declared in the contract as a
|
|
62
62
|
* development server, but there is no `localhost` *environment* name to key it
|
|
63
63
|
* off. Local callers pass `{streamBaseUrl}` or set `GISL_STREAM_BASE_URL`.
|
|
64
|
+
* ⚠️ **FROZEN, AND PART OF THE PUBLIC SURFACE.** Both tables are exported from the
|
|
65
|
+
* package barrel because the SDK requires callers on the low-level surface to
|
|
66
|
+
* supply `streamBaseUrl` and previously published no way to learn the declared
|
|
67
|
+
* hosts. They are the SAME objects this module's resolvers read, so a consumer
|
|
68
|
+
* mutating one would have repointed the SDK's own resolution — hence
|
|
69
|
+
* `Object.freeze`, which makes that a no-op in sloppy mode and a `TypeError`
|
|
70
|
+
* under `'use strict'` (every ES module) rather than a silent redirection.
|
|
64
71
|
*/
|
|
65
|
-
export declare const ENVIRONMENT_STREAM_ENDPOINTS: Partial<Record<Environment, string
|
|
72
|
+
export declare const ENVIRONMENT_STREAM_ENDPOINTS: Readonly<Partial<Record<Environment, string>>>;
|
|
66
73
|
export interface ResolveCredentialsOptions {
|
|
67
74
|
/** Explicit API key — highest precedence. */
|
|
68
75
|
readonly apiKey?: string;
|
package/dist/credentials.js
CHANGED
|
@@ -25,10 +25,10 @@ export const GISL_STREAM_BASE_URL_ENV = 'GISL_STREAM_BASE_URL';
|
|
|
25
25
|
* Named environments → base URLs. Kept colocated with the resolver so the
|
|
26
26
|
* mapping table doesn't leak into `gisl.ts`.
|
|
27
27
|
*/
|
|
28
|
-
export const ENVIRONMENT_ENDPOINTS = {
|
|
28
|
+
export const ENVIRONMENT_ENDPOINTS = Object.freeze({
|
|
29
29
|
prod: 'https://api.giveitsmaller.com',
|
|
30
30
|
staging: 'https://api.staging.giveitsmaller.com',
|
|
31
|
-
};
|
|
31
|
+
});
|
|
32
32
|
export const DEFAULT_ENDPOINT = ENVIRONMENT_ENDPOINTS.prod;
|
|
33
33
|
/**
|
|
34
34
|
* Named environments → **SSE stream host**. A SECOND host, deliberately
|
|
@@ -64,11 +64,18 @@ export const DEFAULT_ENDPOINT = ENVIRONMENT_ENDPOINTS.prod;
|
|
|
64
64
|
* `localhost` is intentionally absent too: it is declared in the contract as a
|
|
65
65
|
* development server, but there is no `localhost` *environment* name to key it
|
|
66
66
|
* off. Local callers pass `{streamBaseUrl}` or set `GISL_STREAM_BASE_URL`.
|
|
67
|
+
* ⚠️ **FROZEN, AND PART OF THE PUBLIC SURFACE.** Both tables are exported from the
|
|
68
|
+
* package barrel because the SDK requires callers on the low-level surface to
|
|
69
|
+
* supply `streamBaseUrl` and previously published no way to learn the declared
|
|
70
|
+
* hosts. They are the SAME objects this module's resolvers read, so a consumer
|
|
71
|
+
* mutating one would have repointed the SDK's own resolution — hence
|
|
72
|
+
* `Object.freeze`, which makes that a no-op in sloppy mode and a `TypeError`
|
|
73
|
+
* under `'use strict'` (every ES module) rather than a silent redirection.
|
|
67
74
|
*/
|
|
68
|
-
export const ENVIRONMENT_STREAM_ENDPOINTS = {
|
|
75
|
+
export const ENVIRONMENT_STREAM_ENDPOINTS = Object.freeze({
|
|
69
76
|
prod: 'https://stream.giveitsmaller.com',
|
|
70
77
|
staging: 'https://stream.staging.giveitsmaller.com',
|
|
71
|
-
};
|
|
78
|
+
});
|
|
72
79
|
// ---------------------------------------------------------------------------
|
|
73
80
|
// Public resolvers
|
|
74
81
|
// ---------------------------------------------------------------------------
|
|
@@ -241,7 +248,29 @@ function readEnv(name) {
|
|
|
241
248
|
return null;
|
|
242
249
|
}
|
|
243
250
|
const value = process.env[name];
|
|
244
|
-
|
|
251
|
+
// 🔴 A WHITESPACE-ONLY ENV VAR IS UNSET, and the presence check is what says
|
|
252
|
+
// so — the value itself is returned UNTOUCHED.
|
|
253
|
+
//
|
|
254
|
+
// `resolveStreamEndpoint` already trims the `streamBaseUrl` OPTION before its
|
|
255
|
+
// presence check (codex a7f5ec9f0d32) and this path did not, so the two
|
|
256
|
+
// disagreed about the same question: `GISL_STREAM_BASE_URL=' '` was returned
|
|
257
|
+
// verbatim as the stream host — a URL made of spaces — while the equivalent
|
|
258
|
+
// option resolved to the environment's declared host. The same asymmetry let a
|
|
259
|
+
// blank `GISL_BASE_URL` count as "the API host was configured" and suppress
|
|
260
|
+
// the production stream fallback.
|
|
261
|
+
//
|
|
262
|
+
// ⚠️ Presence only. A non-blank value is NOT trimmed here: silently altering a
|
|
263
|
+
// value the caller supplied is how you end up connecting somewhere they did
|
|
264
|
+
// not name. Blank means unset; anything else means what it says.
|
|
265
|
+
//
|
|
266
|
+
// Reported by compression_e2e, 2026-09-15: a whitespace-only value is TRUTHY
|
|
267
|
+
// in JavaScript, so a consumer's `if (url)` guard passes and the SDK then
|
|
268
|
+
// fails downstream — the mismatch only exists because the two sides answer
|
|
269
|
+
// "is this set?" differently.
|
|
270
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return value;
|
|
245
274
|
}
|
|
246
275
|
function defaultProfilePath() {
|
|
247
276
|
const homeDir = readEnv('HOME') ?? readEnv('USERPROFILE');
|
|
@@ -215,6 +215,7 @@ export interface OutputOptions {
|
|
|
215
215
|
/** JPEG/WebP lossless. Honored: same_format jpeg/webp (stable since v2.101.0). */
|
|
216
216
|
lossless?: boolean;
|
|
217
217
|
}
|
|
218
|
+
export type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
|
|
218
219
|
/**
|
|
219
220
|
* The user-supplyable option keys per verb (excludes positional-owned keys).
|
|
220
221
|
* Exported for the wire-key conformance guard, which asserts each tuple ∪ its
|
package/dist/file-first.d.ts
CHANGED
|
@@ -492,8 +492,12 @@ export declare class Recipe {
|
|
|
492
492
|
* Composite an image OVERLAY onto this file (a multi-input op). `overlay` is a
|
|
493
493
|
* secondary file-NODE (a {@link Recipe} — e.g. `client.file('logo.png')`),
|
|
494
494
|
* itself optionally processed first. Routes by THIS file's effective media:
|
|
495
|
-
* image base → `image_watermark` (stable)
|
|
496
|
-
*
|
|
495
|
+
* image base → `image_watermark` (stable).
|
|
496
|
+
* A VIDEO base is REFUSED: `video_watermark` was withdrawn to `planned`
|
|
497
|
+
* by contracts v2.203.0, so this verb throws before any upload rather than
|
|
498
|
+
* building a workflow the server would reject. The routing is unchanged and
|
|
499
|
+
* returns when the operation is re-listed.
|
|
500
|
+
* Audio/document/animated-GIF/unsupported-subtype/undetectable bases
|
|
497
501
|
* throw locally BEFORE any upload (the planned-op gate). `options` carries the
|
|
498
502
|
* wire watermark options (`anchor`, `opacity`, `margin_x`, `margin_y`,
|
|
499
503
|
* `overlay_width`, or `overlays[]` for the multi-overlay stack). Returns a
|
|
@@ -671,7 +675,7 @@ export declare const WATERMARK_CAPABILITY: {
|
|
|
671
675
|
readonly video_watermark: {
|
|
672
676
|
readonly video: {
|
|
673
677
|
readonly mimes: readonly ["video/mp4", "video/webm"];
|
|
674
|
-
readonly availability: "
|
|
678
|
+
readonly availability: "planned";
|
|
675
679
|
};
|
|
676
680
|
};
|
|
677
681
|
};
|
package/dist/file-first.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { GislConfigError, GislItemFailedError, GislNetworkError, GislNoSuchKeyError, GislSinkError, GislStreamHostNotDeclaredError, GislTimeoutError, SseEndedWithoutTerminal } from './errors.js';
|
|
13
13
|
import { _detectCompressMedia, _detectAudioLossless, _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, _checkAborted, _cappedProbeTimeoutMs, } from './builder.js';
|
|
14
|
+
import { DEFAULT_POLL_TIMEOUT_MS } from './client.js';
|
|
14
15
|
import { LazyHttpDownloader } from './lazy-downloader.js';
|
|
15
16
|
import { resolveCompressOptions, } from './ergonomic/preset_resolver.js';
|
|
16
17
|
import { validateVerbOptions, assertThumbnailDimensions } from './ergonomic/option_validation.js';
|
|
@@ -821,8 +822,12 @@ export class Recipe {
|
|
|
821
822
|
* Composite an image OVERLAY onto this file (a multi-input op). `overlay` is a
|
|
822
823
|
* secondary file-NODE (a {@link Recipe} — e.g. `client.file('logo.png')`),
|
|
823
824
|
* itself optionally processed first. Routes by THIS file's effective media:
|
|
824
|
-
* image base → `image_watermark` (stable)
|
|
825
|
-
*
|
|
825
|
+
* image base → `image_watermark` (stable).
|
|
826
|
+
* A VIDEO base is REFUSED: `video_watermark` was withdrawn to `planned`
|
|
827
|
+
* by contracts v2.203.0, so this verb throws before any upload rather than
|
|
828
|
+
* building a workflow the server would reject. The routing is unchanged and
|
|
829
|
+
* returns when the operation is re-listed.
|
|
830
|
+
* Audio/document/animated-GIF/unsupported-subtype/undetectable bases
|
|
826
831
|
* throw locally BEFORE any upload (the planned-op gate). `options` carries the
|
|
827
832
|
* wire watermark options (`anchor`, `opacity`, `margin_x`, `margin_y`,
|
|
828
833
|
* `overlay_width`, or `overlays[]` for the multi-overlay stack). Returns a
|
|
@@ -920,7 +925,7 @@ export class Recipe {
|
|
|
920
925
|
if (this.client === undefined) {
|
|
921
926
|
throw new GislConfigError('Recipe.run() requires a client; build the recipe via gisl().file(...) rather than constructing Recipe directly.', { reason: 'no_client' });
|
|
922
927
|
}
|
|
923
|
-
const deadline = Date.now() + _parseMaxWait(options.maxWait ??
|
|
928
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
924
929
|
// 1+2. Upload (when required) + create the workflow. Shared with submit()
|
|
925
930
|
// (which passes a webhook → callback_url). run() passes no webhook.
|
|
926
931
|
const created = await this._uploadAndCreate(undefined, deadline, onProgress, signal, options.probeBeforeCreate, options.probeTimeoutMs);
|
|
@@ -1372,7 +1377,14 @@ export const WATERMARK_CAPABILITY = {
|
|
|
1372
1377
|
image_bmp: { mimes: ['image/bmp'], availability: 'stable' },
|
|
1373
1378
|
},
|
|
1374
1379
|
video_watermark: {
|
|
1375
|
-
|
|
1380
|
+
// 🔴 WITHDRAWN to `planned` by contracts v2.203.0 — withdrawn 2026-09-15, tag cut 2026-09-16 —
|
|
1381
|
+
// after three measured staging proofs. It was `stable` — a worker EXISTS,
|
|
1382
|
+
// and it cannot serve the ceiling this contract advertised. That is a
|
|
1383
|
+
// different kind of `planned` from `image_gif` below, where nothing is built
|
|
1384
|
+
// yet, and the two have opposite remedies; no contract field separates them
|
|
1385
|
+
// (contracts SYQhXb6R). `_WATERMARK_SHIPPABLE` treats both as not-shippable,
|
|
1386
|
+
// which is right for the gate and worth knowing when rendering a reason.
|
|
1387
|
+
video: { mimes: ['video/mp4', 'video/webm'], availability: 'planned' },
|
|
1376
1388
|
},
|
|
1377
1389
|
};
|
|
1378
1390
|
const _WATERMARK_SHIPPABLE = new Set(['stable', 'beta']);
|
|
@@ -1472,8 +1484,19 @@ function _resolveWatermarkWireOp(base) {
|
|
|
1472
1484
|
if (group.mimes.includes(mime)) {
|
|
1473
1485
|
if (_WATERMARK_SHIPPABLE.has(group.availability))
|
|
1474
1486
|
return wireOp;
|
|
1475
|
-
|
|
1476
|
-
|
|
1487
|
+
// ⚠️ "NOT YET" WAS A PROMISE THE CONTRACT DOES NOT MAKE. This message
|
|
1488
|
+
// used to say the schema "is defined but the server returns
|
|
1489
|
+
// feature_not_available until it ships" — true for a capability nobody
|
|
1490
|
+
// has built, and FALSE for one that was `stable` last week and has been
|
|
1491
|
+
// WITHDRAWN (contracts v2.203.0 did exactly that to `video_watermark`).
|
|
1492
|
+
// Telling a user to wait for something that shipped and was pulled is
|
|
1493
|
+
// worse than telling them nothing. No contract field separates the two
|
|
1494
|
+
// kinds yet (contracts SYQhXb6R), so the wording states only what is
|
|
1495
|
+
// true of both.
|
|
1496
|
+
throw new GislConfigError(`watermark for ${mime} bases is not available (${wireOp} is ` +
|
|
1497
|
+
`'${group.availability}' in the contract this SDK was built against). ` +
|
|
1498
|
+
'Workflow-create would return feature_not_available, so this is refused ' +
|
|
1499
|
+
'before any upload. Check getSchema() for the current server answer.', { reason: 'feature_not_available' });
|
|
1477
1500
|
}
|
|
1478
1501
|
}
|
|
1479
1502
|
}
|
|
@@ -1754,7 +1777,7 @@ export class FilesRecipe {
|
|
|
1754
1777
|
if (this.client === undefined) {
|
|
1755
1778
|
throw new GislConfigError('FilesRecipe.run() requires a client; build the fan-out via gisl().files(...) rather than constructing FilesRecipe directly.', { reason: 'no_client' });
|
|
1756
1779
|
}
|
|
1757
|
-
const deadline = Date.now() + _parseMaxWait(options.maxWait ??
|
|
1780
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
1758
1781
|
// 1+2. Upload EVERY input + create ONE multi-job workflow. Shared with
|
|
1759
1782
|
// submit() (which passes a webhook → callback_url and no deadline).
|
|
1760
1783
|
const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal, options.probeBeforeCreate, options.probeTimeoutMs);
|
|
@@ -1983,7 +2006,7 @@ export class MergedRecipe {
|
|
|
1983
2006
|
if (this.client === undefined) {
|
|
1984
2007
|
throw new GislConfigError('MergedRecipe.run() requires a client; build the merge via gisl().files(...).merge(...) rather than constructing MergedRecipe directly.', { reason: 'no_client' });
|
|
1985
2008
|
}
|
|
1986
|
-
const deadline = Date.now() + _parseMaxWait(options.maxWait ??
|
|
2009
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
1987
2010
|
const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal, options.probeBeforeCreate, options.probeTimeoutMs);
|
|
1988
2011
|
const finalStatus = await _awaitTerminal(this.client, {
|
|
1989
2012
|
workflowId: created.workflowId,
|
|
@@ -2196,7 +2219,7 @@ export class ArchivedRecipe {
|
|
|
2196
2219
|
if (this.client === undefined) {
|
|
2197
2220
|
throw new GislConfigError('ArchivedRecipe.run() requires a client; build the bundle via gisl().files(...).archive(...) rather than constructing ArchivedRecipe directly.', { reason: 'no_client' });
|
|
2198
2221
|
}
|
|
2199
|
-
const deadline = Date.now() + _parseMaxWait(options.maxWait ??
|
|
2222
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
2200
2223
|
const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal, options.probeBeforeCreate, options.probeTimeoutMs);
|
|
2201
2224
|
const finalStatus = await _awaitTerminal(this.client, {
|
|
2202
2225
|
workflowId: created.workflowId,
|
|
@@ -2409,7 +2432,7 @@ export class WatermarkedRecipe {
|
|
|
2409
2432
|
if (this.client === undefined) {
|
|
2410
2433
|
throw new GislConfigError('WatermarkedRecipe.run() requires a client; build the watermark via gisl().file(...).watermark(...) rather than constructing WatermarkedRecipe directly.', { reason: 'no_client' });
|
|
2411
2434
|
}
|
|
2412
|
-
const deadline = Date.now() + _parseMaxWait(options.maxWait ??
|
|
2435
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
2413
2436
|
const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal, options.probeBeforeCreate, options.probeTimeoutMs);
|
|
2414
2437
|
const finalStatus = await _awaitTerminal(this.client, {
|
|
2415
2438
|
workflowId: created.workflowId,
|
|
@@ -2597,7 +2620,7 @@ export class BatchRecipe {
|
|
|
2597
2620
|
// FilesRecipe behavior and is intentionally NOT closed here — a TS
|
|
2598
2621
|
// path-precheck would diverge batch from FilesRecipe.)
|
|
2599
2622
|
this.validatePreUpload();
|
|
2600
|
-
const deadline = Date.now() + _parseMaxWait(options.maxWait ??
|
|
2623
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
2601
2624
|
// 1+2. Upload each entry's input + create ONE multi-job workflow. batch v1
|
|
2602
2625
|
// sends NO webhook (run()-only), so `callback_url` is omitted from the
|
|
2603
2626
|
// payload (the closure receives `callbackUrl` undefined).
|
package/dist/handle.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*
|
|
33
33
|
* Mirrors the PHP `Gisl\Sdk\Ergonomic\Handle` + `Gisl\Sdk\Ergonomic\StatusSnapshot`.
|
|
34
34
|
*/
|
|
35
|
+
import { DEFAULT_POLL_TIMEOUT_MS } from './client.js';
|
|
35
36
|
import { GislConfigError, GislNetworkError, GislResultNotReadyError, GislTimeoutError, GislStreamHostNotDeclaredError, SseEndedWithoutTerminal, } from './errors.js';
|
|
36
37
|
import { _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, } from './builder.js';
|
|
37
38
|
import { projectDownloadsToRunResult, projectMultiJobToRunResult, isFanoutStatus, isMergeStatus, isArchiveStatus, isWatermarkStatus, isSoleOpChainStatus, soleOpChainDeliverableRef, _POST_STEP_JOB_REF, } from './file-first.js';
|
|
@@ -154,7 +155,7 @@ export class Handle {
|
|
|
154
155
|
* @throws {GislConfigError} reason `no_client` when no client is bound.
|
|
155
156
|
* @throws {GislTimeoutError} when `maxWait` elapses before terminal.
|
|
156
157
|
*/
|
|
157
|
-
async wait(maxWait =
|
|
158
|
+
async wait(maxWait = DEFAULT_POLL_TIMEOUT_MS, onProgress) {
|
|
158
159
|
const client = this.requireClient();
|
|
159
160
|
const deadline = Date.now() + _parseMaxWait(maxWait);
|
|
160
161
|
let finalStatus;
|
package/dist/index.core.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type { WatermarkWireOp } from './file-first.js';
|
|
|
19
19
|
export { BatchRecipe } from './file-first.js';
|
|
20
20
|
export { Handle, StatusSnapshot } from './handle.js';
|
|
21
21
|
export { gisl, create } from './gisl.js';
|
|
22
|
+
export { ENVIRONMENT_ENDPOINTS, ENVIRONMENT_STREAM_ENDPOINTS } from './credentials.js';
|
|
22
23
|
export type { GislCreateOptions, Environment, ErgonomicClient, SingleInputOperationType, MultiInputOperationType, } from './gisl.js';
|
|
23
24
|
export { presetDefaults, PresetDefaults, type PresetMedia, type PresetOp, type AnyPresetOptions, ImageCompressPresetOptions, type ImageCompressPresetOptionsInput, AudioCompressPresetOptions, type AudioCompressPresetOptionsInput, VideoCompressPresetOptions, type VideoCompressPresetOptionsInput, DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput, DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput, DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput, OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, } from './ergonomic/presets/index.js';
|
|
24
25
|
export { OperationBuilder, MapEachBuilder } from './builder.js';
|
package/dist/index.core.js
CHANGED
|
@@ -90,6 +90,31 @@ export { Handle, StatusSnapshot } from './handle.js';
|
|
|
90
90
|
// credential-chain types. `gisl.anonymous()` (public export) lands once
|
|
91
91
|
// the anonymous-capable operation allowlist is non-empty (plan §12).
|
|
92
92
|
export { gisl, create } from './gisl.js';
|
|
93
|
+
// 🔑 THE TWO ENDPOINT TABLES, EXPORTED BECAUSE THE SDK DEMANDS A VALUE IT DID
|
|
94
|
+
// NOT PUBLISH (e2e, 2026-09-15).
|
|
95
|
+
//
|
|
96
|
+
// `streamEvents()` fails CLOSED: `resolveStreamEndpoint` returns `null` rather
|
|
97
|
+
// than deriving a stream host from `baseUrl`, because deriving one is what put
|
|
98
|
+
// production on the gateway path. That design is right and is not changing.
|
|
99
|
+
//
|
|
100
|
+
// But its consequence is that a low-level `new GislClient({…})` caller MUST
|
|
101
|
+
// supply `streamBaseUrl` — and until now the table of declared hosts was
|
|
102
|
+
// reachable by no import a consumer could write: the `exports` map admits only
|
|
103
|
+
// `.` and `./browser`, and neither barrel re-exported these. ⇒ Every such
|
|
104
|
+
// consumer hard-codes the hosts. A library that fails closed on a value and
|
|
105
|
+
// does not export the value converts good design into N private copies that
|
|
106
|
+
// drift, and the copies are invisible until one is wrong.
|
|
107
|
+
//
|
|
108
|
+
// ⚠️ A SET OF HOSTS IS NOT ENOUGH, WHICH IS WHY THIS EXPORTS THE OBJECTS
|
|
109
|
+
// RATHER THAN A LIST. e2e's interim guard read our shipped `dist` as text and
|
|
110
|
+
// compared host sets in both directions — it could not have caught staging and
|
|
111
|
+
// prod being SWAPPED, which is the exact mistake that sends a credentialed
|
|
112
|
+
// stream request to the wrong environment. The PAIRING is the thing worth
|
|
113
|
+
// exporting.
|
|
114
|
+
//
|
|
115
|
+
// Frozen at their definition: these are the same objects the resolver reads, so
|
|
116
|
+
// a consumer mutating one would have repointed the SDK's own resolution.
|
|
117
|
+
export { ENVIRONMENT_ENDPOINTS, ENVIRONMENT_STREAM_ENDPOINTS } from './credentials.js';
|
|
93
118
|
// Ergonomic preset defaults (T4a / VhIj4S7T) — typed leaf DTOs + immutable
|
|
94
119
|
// `PresetDefaults` builder + `presetDefaults()` factory + ergonomic enum
|
|
95
120
|
// re-exports. Resolver wiring (T4b) consumes `PresetDefaults.cellFor()`.
|
package/dist/merge.d.ts
CHANGED
|
@@ -168,8 +168,8 @@ export declare class MergeBuilder {
|
|
|
168
168
|
* and deduped on upload (one upload per unique declared asset).
|
|
169
169
|
*/
|
|
170
170
|
sequence(...entries: SequenceEntry[]): this;
|
|
171
|
-
run(options
|
|
172
|
-
submit(options
|
|
171
|
+
run(options?: RunOptions): Promise<Result>;
|
|
172
|
+
submit(options?: SubmitOptions): Promise<Handle>;
|
|
173
173
|
/**
|
|
174
174
|
* Resolve the declared assets + sequence (or fall back to declared order),
|
|
175
175
|
* dedupe by identity, and run the local validators. The returned plan
|
package/dist/merge.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* Local validation runs BEFORE any upload — undeclared refs and unused
|
|
26
26
|
* assets both fail fast so the caller saves bandwidth on typo'd composes.
|
|
27
27
|
*/
|
|
28
|
+
import { DEFAULT_POLL_TIMEOUT_MS } from './client.js';
|
|
28
29
|
import { uploadSource, jobOutputSource } from './types.js';
|
|
29
30
|
import { GislConfigError, GislNetworkError, GislPerInputOptionsNotSupportedError, GislTimeoutError, GislUndeclaredAssetError, GislUnusedAssetError, GislStreamHostNotDeclaredError, SseEndedWithoutTerminal, } from './errors.js';
|
|
30
31
|
import { _cappedProbeTimeoutMs, _checkAborted, _consumeSseToTerminal, _detectCompressMedia, _parseMaxWait, _pollToTerminal, _projectResult, } from './builder.js';
|
|
@@ -82,8 +83,8 @@ export class MergeBuilder {
|
|
|
82
83
|
this.sequenceEntries = entries;
|
|
83
84
|
return this;
|
|
84
85
|
}
|
|
85
|
-
async run(options) {
|
|
86
|
-
const deadline = Date.now() + _parseMaxWait(options.maxWait);
|
|
86
|
+
async run(options = {}) {
|
|
87
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
87
88
|
const signal = options.signal;
|
|
88
89
|
const onProgress = options.onProgress;
|
|
89
90
|
const useSSE = options.useSSE ?? true;
|
|
@@ -146,7 +147,7 @@ export class MergeBuilder {
|
|
|
146
147
|
const mergeDownloads = downloads.downloads.filter((d) => d.ref === 'merge');
|
|
147
148
|
return _projectResult(finalStatus, mergeDownloads, this.opOptionsForResolved(plan.mediaKind));
|
|
148
149
|
}
|
|
149
|
-
async submit(options) {
|
|
150
|
+
async submit(options = {}) {
|
|
150
151
|
const plan = this.planSequence();
|
|
151
152
|
const probeTargets = [];
|
|
152
153
|
const uploadedByAssetId = await this.uploadUniqueAssets(plan.uniqueAssets, { probeTargets });
|
|
@@ -154,11 +155,17 @@ export class MergeBuilder {
|
|
|
154
155
|
// Fire-and-forget — no deadline, so no cap (mirrors Recipe::submit()).
|
|
155
156
|
await this.waitForVideoProbes(probeTargets, options.probeBeforeCreate, options.probeTimeoutMs, undefined, undefined);
|
|
156
157
|
const payload = this.buildPayload(plan, uploadedByAssetId);
|
|
157
|
-
|
|
158
|
+
// Set only when present — an own `callback_url: undefined` would make a
|
|
159
|
+
// key-presence assertion pass vacuously. See builder.ts.
|
|
160
|
+
if (options.webhook !== undefined)
|
|
161
|
+
payload.callback_url = options.webhook;
|
|
158
162
|
const created = await this.client.createWorkflow(payload);
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
163
|
+
// ⚠️ The client is passed so the returned Handle is usable (36AZ98FV). This
|
|
164
|
+
// comment previously read "No client passed → the returned Handle's
|
|
165
|
+
// status()/wait()/result() throw `no_client`; the merge submit reconciles via
|
|
166
|
+
// webhook" — an accurate description of a defect. `webhook` is optional now,
|
|
167
|
+
// so an unbound handle here would leave a submit with no outcome channel at all.
|
|
168
|
+
return new Handle(created.workflowId, created.webhookSecret != null ? created.webhookSecret : undefined, this.client, null);
|
|
162
169
|
}
|
|
163
170
|
// ---------------------------------------------------------------------------
|
|
164
171
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -361,7 +361,7 @@ export interface ReadCapabilityOptions {
|
|
|
361
361
|
export interface WaitOptions {
|
|
362
362
|
/** Poll interval in milliseconds (default: 2000) */
|
|
363
363
|
intervalMs?: number;
|
|
364
|
-
/** Maximum wait time in milliseconds (default:
|
|
364
|
+
/** Maximum wait time in milliseconds (default: `DEFAULT_POLL_TIMEOUT_MS`, 10 min). */
|
|
365
365
|
timeoutMs?: number;
|
|
366
366
|
/** Called after each poll with current status */
|
|
367
367
|
onPoll?: (status: string) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giveitsmaller/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
|
+
"gislContractsSpec": "v2.203.0",
|
|
4
5
|
"description": "Node.js SDK for the GISL (Give It Smaller) file compression and processing API",
|
|
5
6
|
"license": "Apache-2.0",
|
|
6
7
|
"type": "module",
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"node": ">=18"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@giveitsmaller/contracts": "^0.
|
|
35
|
+
"@giveitsmaller/contracts": "^0.72.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/node": "^22",
|
|
@@ -46,7 +47,8 @@
|
|
|
46
47
|
"check": "tsc --noEmit",
|
|
47
48
|
"test": "vitest run",
|
|
48
49
|
"test:parity": "vitest run tests/parity",
|
|
49
|
-
"parity:update": "UPDATE_PARITY_FIXTURES=1 vitest run tests/parity"
|
|
50
|
+
"parity:update": "UPDATE_PARITY_FIXTURES=1 vitest run tests/parity",
|
|
51
|
+
"api:update": "UPDATE_API_SNAPSHOT=1 vitest run tests/api-surface.test.ts"
|
|
50
52
|
},
|
|
51
53
|
"homepage": "https://docs.giveitsmaller.com",
|
|
52
54
|
"repository": {
|