@giveitsmaller/sdk 0.7.0 → 0.8.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 +11 -0
- package/dist/client.js +14 -4
- package/dist/errors.d.ts +49 -0
- package/dist/errors.js +55 -0
- package/dist/file-first.d.ts +284 -0
- package/dist/file-first.js +445 -0
- package/dist/generated/sdk_spec/errors.d.ts +1 -1
- package/dist/generated/sdk_spec/errors.js +50 -0
- package/dist/generated/sdk_spec/version.d.ts +1 -1
- package/dist/generated/sdk_spec/version.js +1 -1
- package/dist/gisl.d.ts +10 -0
- package/dist/gisl.js +17 -0
- package/dist/http-downloader.d.ts +9 -0
- package/dist/http-downloader.js +55 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -1
- package/package.json +3 -3
package/dist/_audit.js
CHANGED
|
@@ -121,4 +121,15 @@ export function _runAudit() {
|
|
|
121
121
|
accept();
|
|
122
122
|
accept();
|
|
123
123
|
accept();
|
|
124
|
+
// FF1 / 3BIxEnfR — file-first result surface + sink errors.
|
|
125
|
+
accept();
|
|
126
|
+
accept();
|
|
127
|
+
accept();
|
|
128
|
+
accept();
|
|
129
|
+
accept();
|
|
130
|
+
accept();
|
|
131
|
+
accept();
|
|
132
|
+
accept();
|
|
133
|
+
accept();
|
|
134
|
+
accept();
|
|
124
135
|
}
|
package/dist/client.js
CHANGED
|
@@ -399,10 +399,19 @@ export class GislClient {
|
|
|
399
399
|
locale: json.locale,
|
|
400
400
|
messageParams: json.message_params,
|
|
401
401
|
};
|
|
402
|
+
// Human-readable text comes from `message` (the I26 localised field).
|
|
403
|
+
// `error` is the stable, never-localised SCREAMING_SNAKE machine code —
|
|
404
|
+
// NOT display text. Surfacing `error` as the thrown error's `.message`
|
|
405
|
+
// regressed consumers that render the human string (x9Lbf6uy). Fall back
|
|
406
|
+
// to `error` when `message` is absent (deployed contract guarantees
|
|
407
|
+
// `message` on conforming error envelopes). Machine dispatch keys off
|
|
408
|
+
// `error_type` (below), unchanged.
|
|
409
|
+
const status = response.status;
|
|
410
|
+
const errorMessage = json.message ?? json.error ?? 'Unknown error';
|
|
402
411
|
// Validation-details branch first — preserve existing shape so callers
|
|
403
412
|
// matching on `instanceof GislValidationError` keep working.
|
|
404
413
|
if (isValidationDetails(json.details)) {
|
|
405
|
-
throw new GislValidationError(response.status,
|
|
414
|
+
throw new GislValidationError(response.status, errorMessage, json.details, path, i18n);
|
|
406
415
|
}
|
|
407
416
|
// Dispatch by (status, error_type) onto the structured envelope shapes
|
|
408
417
|
// emitted by the v2 contracts. Each branch builds the typed payload via
|
|
@@ -416,8 +425,6 @@ export class GislClient {
|
|
|
416
425
|
// fall through to the base `GislApiError` rather than handing the
|
|
417
426
|
// caller silently-corrupted typed metadata.
|
|
418
427
|
const errorType = json.error_type;
|
|
419
|
-
const status = response.status;
|
|
420
|
-
const errorMessage = json.error ?? 'Unknown error';
|
|
421
428
|
// Build the typed payload via FromJSON, then validate that all
|
|
422
429
|
// required typed fields are well-formed. FromJSON does not throw on
|
|
423
430
|
// missing required fields — for example `workflow_expired` without
|
|
@@ -1740,7 +1747,10 @@ export class GislClient {
|
|
|
1740
1747
|
let errorMessage = 'Unknown error';
|
|
1741
1748
|
try {
|
|
1742
1749
|
const errJson = (await response.json());
|
|
1743
|
-
|
|
1750
|
+
// Prefer the human `message`; `error` is the machine code (x9Lbf6uy).
|
|
1751
|
+
if (errJson.message)
|
|
1752
|
+
errorMessage = errJson.message;
|
|
1753
|
+
else if (errJson.error)
|
|
1744
1754
|
errorMessage = errJson.error;
|
|
1745
1755
|
}
|
|
1746
1756
|
catch {
|
package/dist/errors.d.ts
CHANGED
|
@@ -300,6 +300,18 @@ export declare class GislChainCardinalityMismatchError extends GislConfigError {
|
|
|
300
300
|
export declare class GislTimeoutError extends GislError {
|
|
301
301
|
constructor(message: string);
|
|
302
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Transport-level failure: the underlying `fetch` (or other transport) could
|
|
305
|
+
* not produce a usable response — DNS, TCP, TLS, a mid-stream disconnect, or a
|
|
306
|
+
* non-ok status / empty body when fetching a result download. Mirrors the PHP
|
|
307
|
+
* `Gisl\Sdk\Errors\GislNetworkError`. Subclasses `GislError` (not
|
|
308
|
+
* `GislApiError`) because it carries no contract error envelope. The concrete
|
|
309
|
+
* file-first {@link Downloader} raises this when the output URL cannot be read
|
|
310
|
+
* (a destination-WRITE failure is `GislSinkError` reason `write_failed`).
|
|
311
|
+
*/
|
|
312
|
+
export declare class GislNetworkError extends GislError {
|
|
313
|
+
constructor(message: string);
|
|
314
|
+
}
|
|
303
315
|
export declare class GislAbortError extends GislError {
|
|
304
316
|
constructor(message: string);
|
|
305
317
|
}
|
|
@@ -326,3 +338,40 @@ export declare class GislMultipartPartCountError extends GislError {
|
|
|
326
338
|
readonly maxParts: number;
|
|
327
339
|
constructor(message: string, requiredParts: number, maxParts: number);
|
|
328
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Thrown by the file-first `RunResult.byKey()` (FF1) when no result entry
|
|
343
|
+
* matches the requested key. A keyless run (no `key:` supplied to `file()`)
|
|
344
|
+
* is addressable positionally only — `byKey()` always throws.
|
|
345
|
+
*
|
|
346
|
+
* Mirrors the PHP `Gisl\Sdk\Errors\GislNoSuchKeyError`.
|
|
347
|
+
*/
|
|
348
|
+
export declare class GislNoSuchKeyError extends GislError {
|
|
349
|
+
constructor(message: string);
|
|
350
|
+
}
|
|
351
|
+
/** Machine-readable cause carried by {@link GislSinkError}. */
|
|
352
|
+
export type GislSinkErrorReason = 'not_single_output' | 'downloader_unavailable' | 'partial_failure' | 'duplicate_filename' | 'invalid_directory' | 'write_failed';
|
|
353
|
+
/**
|
|
354
|
+
* Thrown by the file-first `RunResult` sinks (`toFile()` / `downloadTo()`,
|
|
355
|
+
* FF1) when they cannot deliver. The machine-readable `reason` discriminates
|
|
356
|
+
* the three cases, mirroring the `reason`-bag convention on
|
|
357
|
+
* {@link GislConfigError}:
|
|
358
|
+
*
|
|
359
|
+
* - `not_single_output` — `toFile()` requires exactly one output but the
|
|
360
|
+
* run produced zero or more than one.
|
|
361
|
+
* - `downloader_unavailable` — the `RunResult` has no downloader bound (e.g. a
|
|
362
|
+
* browser / no-I/O context).
|
|
363
|
+
* - `partial_failure` — `downloadTo({ failOnPartial: true })` and the
|
|
364
|
+
* run had at least one failed input.
|
|
365
|
+
* - `duplicate_filename` — two outputs share a destination filename in one
|
|
366
|
+
* `downloadTo(dir)`, which would silently overwrite.
|
|
367
|
+
* - `write_failed` — a concrete {@link Downloader} could not open or
|
|
368
|
+
* stream to the destination path.
|
|
369
|
+
*
|
|
370
|
+
* Mirrors the PHP `Gisl\Sdk\Errors\GislSinkError`.
|
|
371
|
+
*/
|
|
372
|
+
export declare class GislSinkError extends GislError {
|
|
373
|
+
readonly reason: GislSinkErrorReason;
|
|
374
|
+
constructor(message: string, options: {
|
|
375
|
+
readonly reason: GislSinkErrorReason;
|
|
376
|
+
});
|
|
377
|
+
}
|
package/dist/errors.js
CHANGED
|
@@ -320,6 +320,21 @@ export class GislTimeoutError extends GislError {
|
|
|
320
320
|
this.name = 'GislTimeoutError';
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Transport-level failure: the underlying `fetch` (or other transport) could
|
|
325
|
+
* not produce a usable response — DNS, TCP, TLS, a mid-stream disconnect, or a
|
|
326
|
+
* non-ok status / empty body when fetching a result download. Mirrors the PHP
|
|
327
|
+
* `Gisl\Sdk\Errors\GislNetworkError`. Subclasses `GislError` (not
|
|
328
|
+
* `GislApiError`) because it carries no contract error envelope. The concrete
|
|
329
|
+
* file-first {@link Downloader} raises this when the output URL cannot be read
|
|
330
|
+
* (a destination-WRITE failure is `GislSinkError` reason `write_failed`).
|
|
331
|
+
*/
|
|
332
|
+
export class GislNetworkError extends GislError {
|
|
333
|
+
constructor(message) {
|
|
334
|
+
super(message);
|
|
335
|
+
this.name = 'GislNetworkError';
|
|
336
|
+
}
|
|
337
|
+
}
|
|
323
338
|
export class GislAbortError extends GislError {
|
|
324
339
|
constructor(message) {
|
|
325
340
|
super(message);
|
|
@@ -359,3 +374,43 @@ export class GislMultipartPartCountError extends GislError {
|
|
|
359
374
|
this.maxParts = maxParts;
|
|
360
375
|
}
|
|
361
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Thrown by the file-first `RunResult.byKey()` (FF1) when no result entry
|
|
379
|
+
* matches the requested key. A keyless run (no `key:` supplied to `file()`)
|
|
380
|
+
* is addressable positionally only — `byKey()` always throws.
|
|
381
|
+
*
|
|
382
|
+
* Mirrors the PHP `Gisl\Sdk\Errors\GislNoSuchKeyError`.
|
|
383
|
+
*/
|
|
384
|
+
export class GislNoSuchKeyError extends GislError {
|
|
385
|
+
constructor(message) {
|
|
386
|
+
super(message);
|
|
387
|
+
this.name = 'GislNoSuchKeyError';
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Thrown by the file-first `RunResult` sinks (`toFile()` / `downloadTo()`,
|
|
392
|
+
* FF1) when they cannot deliver. The machine-readable `reason` discriminates
|
|
393
|
+
* the three cases, mirroring the `reason`-bag convention on
|
|
394
|
+
* {@link GislConfigError}:
|
|
395
|
+
*
|
|
396
|
+
* - `not_single_output` — `toFile()` requires exactly one output but the
|
|
397
|
+
* run produced zero or more than one.
|
|
398
|
+
* - `downloader_unavailable` — the `RunResult` has no downloader bound (e.g. a
|
|
399
|
+
* browser / no-I/O context).
|
|
400
|
+
* - `partial_failure` — `downloadTo({ failOnPartial: true })` and the
|
|
401
|
+
* run had at least one failed input.
|
|
402
|
+
* - `duplicate_filename` — two outputs share a destination filename in one
|
|
403
|
+
* `downloadTo(dir)`, which would silently overwrite.
|
|
404
|
+
* - `write_failed` — a concrete {@link Downloader} could not open or
|
|
405
|
+
* stream to the destination path.
|
|
406
|
+
*
|
|
407
|
+
* Mirrors the PHP `Gisl\Sdk\Errors\GislSinkError`.
|
|
408
|
+
*/
|
|
409
|
+
export class GislSinkError extends GislError {
|
|
410
|
+
reason;
|
|
411
|
+
constructor(message, options) {
|
|
412
|
+
super(message);
|
|
413
|
+
this.name = 'GislSinkError';
|
|
414
|
+
this.reason = options.reason;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-first result surface — the value the file-first layer's `run()` /
|
|
3
|
+
* `Handle.wait()` / `Handle.result()` return (producers land in FF2b/FF5).
|
|
4
|
+
*
|
|
5
|
+
* Coexists with the operation-first `Result`/`Artifact` (in `builder.ts`)
|
|
6
|
+
* until the operation-first layer is removed (FF6). The file-first shape is
|
|
7
|
+
* flatter and adds an always-present per-input partition (`succeeded` /
|
|
8
|
+
* `failed`) so one bad input in a multi-input run doesn't sink the rest.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors `packages/php/src/FileFirst/*`.
|
|
11
|
+
*/
|
|
12
|
+
import { type ProgressEvent } from './builder.js';
|
|
13
|
+
import type { GislClient } from './client.js';
|
|
14
|
+
import { OptimizeFor } from './generated/sdk_spec/enums.js';
|
|
15
|
+
import type { PresetDefaults } from './ergonomic/presets/index.js';
|
|
16
|
+
import type { WorkflowCreatePayload } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Streams a single output URL to a local path. The seam between the
|
|
19
|
+
* file-first {@link RunResult} sinks and the SDK's HTTP/auth layer.
|
|
20
|
+
*
|
|
21
|
+
* FF1 defines ONLY this type — the concrete implementation (fetch +
|
|
22
|
+
* filesystem streamer) is wired by the producer tickets (`run()`/`submit()`,
|
|
23
|
+
* FF2b/FF5), which construct a `RunResult` with a real downloader bound to
|
|
24
|
+
* the client's auth context. Unit tests inject a small stub. A `RunResult`
|
|
25
|
+
* built WITHOUT a downloader (e.g. in a browser, or any no-I/O context)
|
|
26
|
+
* throws {@link GislSinkError} from its sinks rather than reaching for a
|
|
27
|
+
* global client.
|
|
28
|
+
*
|
|
29
|
+
* Mirrors the PHP `Downloader` interface.
|
|
30
|
+
*
|
|
31
|
+
* STREAMING CONTRACT: implementations MUST stream the URL body to
|
|
32
|
+
* `destPath` — they MUST NOT buffer the whole output in memory. The
|
|
33
|
+
* `Promise<void>` return exists precisely so no buffered-bytes value can
|
|
34
|
+
* leak into the calling convention.
|
|
35
|
+
*/
|
|
36
|
+
export interface Downloader {
|
|
37
|
+
/**
|
|
38
|
+
* Stream the body at `url` to the local filesystem path `destPath`.
|
|
39
|
+
* Implementations create/overwrite `destPath`. Failures reject.
|
|
40
|
+
*/
|
|
41
|
+
downloadTo(url: string, destPath: string): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A single deliverable output of a file-first run — the file-first layer's
|
|
45
|
+
* flat output type. Leaner than the operation-first `Artifact`: just the
|
|
46
|
+
* four fields a caller needs to identify + fetch an output.
|
|
47
|
+
*
|
|
48
|
+
* Mirrors the PHP `OutputFile`.
|
|
49
|
+
*/
|
|
50
|
+
export interface OutputFile {
|
|
51
|
+
readonly url: string;
|
|
52
|
+
readonly filename: string;
|
|
53
|
+
readonly sizeBytes: number;
|
|
54
|
+
readonly operation: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* One succeeded entry in {@link RunResult.succeeded}: a single input's
|
|
58
|
+
* outputs, addressable by the `key:` the caller gave that file (null when
|
|
59
|
+
* no key was supplied). Mirrors the PHP `ItemResult`.
|
|
60
|
+
*/
|
|
61
|
+
export interface ItemResult {
|
|
62
|
+
readonly key: string | null;
|
|
63
|
+
readonly outputs: readonly OutputFile[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One failed entry in {@link RunResult.failed}: an input that did not
|
|
67
|
+
* produce a deliverable, paired with the cause. One bad input does not sink
|
|
68
|
+
* the rest of a multi-input run. `error` is `unknown` (mirroring the PHP
|
|
69
|
+
* `\Throwable`) so the caller narrows with `instanceof`. Mirrors the PHP
|
|
70
|
+
* `ItemFailure`.
|
|
71
|
+
*/
|
|
72
|
+
export interface ItemFailure {
|
|
73
|
+
readonly key: string | null;
|
|
74
|
+
readonly error: unknown;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Return value of {@link RunResult.downloadTo} — the local paths written,
|
|
78
|
+
* in the SAME order as {@link RunResult.artifacts}. Mirrors the PHP
|
|
79
|
+
* `Manifest`.
|
|
80
|
+
*/
|
|
81
|
+
export interface Manifest {
|
|
82
|
+
readonly paths: readonly string[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Result of a file-first run. Coexists with the operation-first `Result`
|
|
86
|
+
* (in `builder.ts`) until FF6.
|
|
87
|
+
*
|
|
88
|
+
* Mirrors the PHP `RunResult` class. A class (not a bare interface) because
|
|
89
|
+
* it carries the `byKey()`/`toFile()`/`downloadTo()` behaviour; the data
|
|
90
|
+
* fields stay public + readonly so `toArray()` round-trips.
|
|
91
|
+
*
|
|
92
|
+
* Field notes:
|
|
93
|
+
* - `url`: single-output sugar — the lone artifact's URL when exactly one
|
|
94
|
+
* output exists, else undefined.
|
|
95
|
+
* - `ok`: true iff `failed` is empty. (A boolean — the partition lists are
|
|
96
|
+
* `succeeded`/`failed`; resolves the design doc's `ok` bool-vs-list
|
|
97
|
+
* contradiction.)
|
|
98
|
+
* - `state`: lifecycle state (`completed` | `failed` | ...). Named `state`,
|
|
99
|
+
* NOT `status`, matching the file-first `StatusSnapshot.state`.
|
|
100
|
+
* - sinks fetch via the injected {@link Downloader}; a result with no
|
|
101
|
+
* downloader throws {@link GislSinkError} (reason `downloader_unavailable`).
|
|
102
|
+
*/
|
|
103
|
+
export declare class RunResult {
|
|
104
|
+
readonly workflowId: string;
|
|
105
|
+
readonly state: string;
|
|
106
|
+
readonly artifacts: readonly OutputFile[];
|
|
107
|
+
readonly succeeded: readonly ItemResult[];
|
|
108
|
+
readonly failed: readonly ItemFailure[];
|
|
109
|
+
private readonly downloader?;
|
|
110
|
+
/** Single-output sugar: the lone artifact's URL, or undefined for 0 / >1. */
|
|
111
|
+
readonly url?: string;
|
|
112
|
+
/** True iff {@link failed} is empty. */
|
|
113
|
+
readonly ok: boolean;
|
|
114
|
+
constructor(workflowId: string, state: string, artifacts: readonly OutputFile[], succeeded: readonly ItemResult[], failed: readonly ItemFailure[], downloader?: Downloader | undefined);
|
|
115
|
+
/**
|
|
116
|
+
* Address a succeeded input by the `key:` given to `file()`. Duplicate keys
|
|
117
|
+
* are not valid input — the producer enforces key uniqueness (a later
|
|
118
|
+
* ticket); the first match is returned.
|
|
119
|
+
* @throws {GislNoSuchKeyError} when no succeeded entry has that key (a
|
|
120
|
+
* keyless run always throws — it is positionally addressable only).
|
|
121
|
+
*/
|
|
122
|
+
byKey(key: string): ItemResult;
|
|
123
|
+
/**
|
|
124
|
+
* Write the single output to `path`. Requires EXACTLY ONE artifact.
|
|
125
|
+
* @throws {GislSinkError} reason `not_single_output` for 0/>1 outputs;
|
|
126
|
+
* reason `downloader_unavailable` when no downloader is bound.
|
|
127
|
+
*/
|
|
128
|
+
toFile(path: string): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Download every output into `dir` (filename per output), in output order.
|
|
131
|
+
* Returns the {@link Manifest} of local paths written.
|
|
132
|
+
* @throws {GislSinkError} reason `partial_failure` when `failOnPartial` and
|
|
133
|
+
* the run had failed inputs; reason `downloader_unavailable` when no
|
|
134
|
+
* downloader is bound.
|
|
135
|
+
*/
|
|
136
|
+
downloadTo(dir: string, options?: {
|
|
137
|
+
failOnPartial?: boolean;
|
|
138
|
+
}): Promise<Manifest>;
|
|
139
|
+
/**
|
|
140
|
+
* Plain-object projection. Field ORDER (workflowId, state, ok, url?,
|
|
141
|
+
* artifacts, succeeded, failed) is fixed to match the PHP `toArray()`
|
|
142
|
+
* reference so JSON-string parity holds (FF1 shape assertion + FF2b harness
|
|
143
|
+
* fixture). `url` is omitted entirely when undefined — `JSON.stringify`
|
|
144
|
+
* then produces the identical shape to PHP's omit-when-null `toArray()`.
|
|
145
|
+
*/
|
|
146
|
+
toJSON(): {
|
|
147
|
+
workflowId: string;
|
|
148
|
+
state: string;
|
|
149
|
+
ok: boolean;
|
|
150
|
+
url?: string;
|
|
151
|
+
artifacts: readonly OutputFile[];
|
|
152
|
+
succeeded: readonly {
|
|
153
|
+
key: string | null;
|
|
154
|
+
outputs: readonly OutputFile[];
|
|
155
|
+
}[];
|
|
156
|
+
failed: readonly {
|
|
157
|
+
key: string | null;
|
|
158
|
+
error: string;
|
|
159
|
+
}[];
|
|
160
|
+
};
|
|
161
|
+
private requireDownloader;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The primary file a {@link Recipe} operates on — the "subject" of the
|
|
165
|
+
* file-first surface. A discriminated union over the ways a caller names an
|
|
166
|
+
* input:
|
|
167
|
+
*
|
|
168
|
+
* - `path` — a local filesystem path (Node; the common case).
|
|
169
|
+
* - `blob` — an in-memory `Blob`/`File` (browser, or Node 18+).
|
|
170
|
+
* - `uploadId` — a previously-uploaded `file_id` (reuse across recipes).
|
|
171
|
+
*
|
|
172
|
+
* FF2a does NO upload, so only the `path` + `uploadId` arms are exercised
|
|
173
|
+
* end-to-end here; the `blob` arm is DEFINED and type-checked but its upload
|
|
174
|
+
* is wired by FF2b (`run()`). Mirrors the PHP `FileInput` value object.
|
|
175
|
+
*/
|
|
176
|
+
export type FileInput = {
|
|
177
|
+
readonly kind: 'path';
|
|
178
|
+
readonly path: string;
|
|
179
|
+
} | {
|
|
180
|
+
readonly kind: 'blob';
|
|
181
|
+
readonly blob: Blob;
|
|
182
|
+
} | {
|
|
183
|
+
readonly kind: 'uploadId';
|
|
184
|
+
readonly fileId: string;
|
|
185
|
+
};
|
|
186
|
+
/** Named constructors for {@link FileInput} — mirror the PHP static factories. */
|
|
187
|
+
export declare const fileInput: {
|
|
188
|
+
readonly path: (path: string) => FileInput;
|
|
189
|
+
readonly blob: (blob: Blob) => FileInput;
|
|
190
|
+
readonly uploadId: (fileId: string) => FileInput;
|
|
191
|
+
};
|
|
192
|
+
/** One step in a {@link Recipe}'s chain — an op kind + captured ergonomic args. */
|
|
193
|
+
interface RecipeStep {
|
|
194
|
+
readonly opType: 'compress' | 'convert' | 'thumbnail' | 'text_watermark';
|
|
195
|
+
readonly options: Readonly<Record<string, unknown>>;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The file-first builder value. `client.file(path)` returns a `Recipe`;
|
|
199
|
+
* single-input operations called on it (`compress`, `convert`, `thumbnail`,
|
|
200
|
+
* `textWatermark`) chain SEQUENTIALLY — each op feeds the next, and the chain
|
|
201
|
+
* lowers to ONE workflow job with an ordered `operations[]` (per ADR-0004:
|
|
202
|
+
* operations execute sequentially, each consuming the previous output). A
|
|
203
|
+
* chain yields the TERMINAL output only; intermediates are consumed (surfaced
|
|
204
|
+
* by FF2b's `run()`/{@link RunResult}).
|
|
205
|
+
*
|
|
206
|
+
* **Immutable / clone-on-write.** Every op returns a NEW `Recipe` carrying the
|
|
207
|
+
* appended step — `this` is never mutated. A Recipe is therefore a reusable
|
|
208
|
+
* value: branching the same base recipe two different ways cannot let one
|
|
209
|
+
* branch observe the other's steps (the aliasing trap mutable builders fall
|
|
210
|
+
* into).
|
|
211
|
+
*
|
|
212
|
+
* FF2a is network-free: there is NO `run()` here (that is FF2b). The lowering
|
|
213
|
+
* seam {@link toWorkflowPayload} takes the resolved upload id as a parameter
|
|
214
|
+
* so it stays pure — FF2b's `run()` calls the SAME method after uploading, and
|
|
215
|
+
* the parity harness calls it with a fixed id to assert the lowered shape.
|
|
216
|
+
*
|
|
217
|
+
* Mirrors the PHP `Recipe`.
|
|
218
|
+
*/
|
|
219
|
+
export declare class Recipe {
|
|
220
|
+
private readonly input;
|
|
221
|
+
private readonly recipeKey;
|
|
222
|
+
private readonly steps;
|
|
223
|
+
private readonly presetDefaults?;
|
|
224
|
+
private readonly scopedPresetDefaults?;
|
|
225
|
+
private readonly client?;
|
|
226
|
+
constructor(input: FileInput, recipeKey?: string | undefined, steps?: readonly RecipeStep[], presetDefaults?: PresetDefaults | undefined, scopedPresetDefaults?: PresetDefaults | undefined, client?: GislClient | undefined);
|
|
227
|
+
/**
|
|
228
|
+
* Reduce file size. `optimize` selects a per-media preset (resolved to
|
|
229
|
+
* concrete wire fields at lower-time, exactly as `client.compress()` does).
|
|
230
|
+
*/
|
|
231
|
+
compress(optimize?: OptimizeFor): Recipe;
|
|
232
|
+
/** Change format. `format` is lowered verbatim to the `format` wire option. */
|
|
233
|
+
convert(format: string): Recipe;
|
|
234
|
+
/**
|
|
235
|
+
* Generate a preview. Width and/or height in pixels; an omitted dimension is
|
|
236
|
+
* dropped from the wire options (not sent as `undefined`).
|
|
237
|
+
*/
|
|
238
|
+
thumbnail(options?: {
|
|
239
|
+
width?: number;
|
|
240
|
+
height?: number;
|
|
241
|
+
}): Recipe;
|
|
242
|
+
/**
|
|
243
|
+
* Apply a text watermark. Single-input (the text is an option, not a
|
|
244
|
+
* secondary file) — lowers to the `text_watermark` op with a `text` option.
|
|
245
|
+
*/
|
|
246
|
+
textWatermark(text: string): Recipe;
|
|
247
|
+
/**
|
|
248
|
+
* Lower this recipe to a workflow-create payload against a resolved upload
|
|
249
|
+
* id. Single-input chain → ONE job, `source: upload(fileId)`, ordered
|
|
250
|
+
* `operations[]`; the job `id` is omitted (a single job referenced by
|
|
251
|
+
* nothing — the server auto-assigns `job_N`).
|
|
252
|
+
*
|
|
253
|
+
* @internal Consumed by FF2b's `run()` (after a real upload) and by the
|
|
254
|
+
* cross-language parity harness (with a fixed id). Not part of the
|
|
255
|
+
* caller-facing fluent surface.
|
|
256
|
+
*/
|
|
257
|
+
toWorkflowPayload(fileId: string): WorkflowCreatePayload;
|
|
258
|
+
/** The result-addressing key passed to `file()`, or undefined. */
|
|
259
|
+
key(): string | undefined;
|
|
260
|
+
/** The number of operations chained so far (introspection / tests). */
|
|
261
|
+
get stepCount(): number;
|
|
262
|
+
/**
|
|
263
|
+
* Execute the recipe end-to-end: upload the input (when required), create
|
|
264
|
+
* the workflow, await a terminal state (SSE with poll fallback), then
|
|
265
|
+
* resolve the produced downloads into a flat {@link RunResult}. Throws
|
|
266
|
+
* {@link GislTimeoutError} if `maxWait` elapses before terminal status.
|
|
267
|
+
*
|
|
268
|
+
* Mirrors the operation-first `OperationBuilder.run` (in `builder.ts`).
|
|
269
|
+
* Requires a client bound at construction time — `gisl().file(...)` wires
|
|
270
|
+
* it; a directly-constructed `Recipe` (e.g. in a lowering-only test) has no
|
|
271
|
+
* client and throws {@link GislConfigError}.
|
|
272
|
+
*/
|
|
273
|
+
run(options?: {
|
|
274
|
+
maxWait?: string | number;
|
|
275
|
+
onProgress?: (event: ProgressEvent) => void;
|
|
276
|
+
signal?: AbortSignal;
|
|
277
|
+
pollIntervalMs?: number;
|
|
278
|
+
}): Promise<RunResult>;
|
|
279
|
+
private withStep;
|
|
280
|
+
private lowerStep;
|
|
281
|
+
private lowerCompressOptions;
|
|
282
|
+
private compressMediaHint;
|
|
283
|
+
}
|
|
284
|
+
export {};
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-first result surface — the value the file-first layer's `run()` /
|
|
3
|
+
* `Handle.wait()` / `Handle.result()` return (producers land in FF2b/FF5).
|
|
4
|
+
*
|
|
5
|
+
* Coexists with the operation-first `Result`/`Artifact` (in `builder.ts`)
|
|
6
|
+
* until the operation-first layer is removed (FF6). The file-first shape is
|
|
7
|
+
* flatter and adds an always-present per-input partition (`succeeded` /
|
|
8
|
+
* `failed`) so one bad input in a multi-input run doesn't sink the rest.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors `packages/php/src/FileFirst/*`.
|
|
11
|
+
*/
|
|
12
|
+
import { GislApiError, GislConfigError, GislNoSuchKeyError, GislSinkError, GislTimeoutError } from './errors.js';
|
|
13
|
+
import { _detectCompressMedia, _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, _checkAborted, } from './builder.js';
|
|
14
|
+
import { HttpDownloader } from './http-downloader.js';
|
|
15
|
+
import { resolveCompressOptions, } from './ergonomic/preset_resolver.js';
|
|
16
|
+
import { OptimizeFor } from './generated/sdk_spec/enums.js';
|
|
17
|
+
import { uploadSource } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Result of a file-first run. Coexists with the operation-first `Result`
|
|
20
|
+
* (in `builder.ts`) until FF6.
|
|
21
|
+
*
|
|
22
|
+
* Mirrors the PHP `RunResult` class. A class (not a bare interface) because
|
|
23
|
+
* it carries the `byKey()`/`toFile()`/`downloadTo()` behaviour; the data
|
|
24
|
+
* fields stay public + readonly so `toArray()` round-trips.
|
|
25
|
+
*
|
|
26
|
+
* Field notes:
|
|
27
|
+
* - `url`: single-output sugar — the lone artifact's URL when exactly one
|
|
28
|
+
* output exists, else undefined.
|
|
29
|
+
* - `ok`: true iff `failed` is empty. (A boolean — the partition lists are
|
|
30
|
+
* `succeeded`/`failed`; resolves the design doc's `ok` bool-vs-list
|
|
31
|
+
* contradiction.)
|
|
32
|
+
* - `state`: lifecycle state (`completed` | `failed` | ...). Named `state`,
|
|
33
|
+
* NOT `status`, matching the file-first `StatusSnapshot.state`.
|
|
34
|
+
* - sinks fetch via the injected {@link Downloader}; a result with no
|
|
35
|
+
* downloader throws {@link GislSinkError} (reason `downloader_unavailable`).
|
|
36
|
+
*/
|
|
37
|
+
export class RunResult {
|
|
38
|
+
workflowId;
|
|
39
|
+
state;
|
|
40
|
+
artifacts;
|
|
41
|
+
succeeded;
|
|
42
|
+
failed;
|
|
43
|
+
downloader;
|
|
44
|
+
/** Single-output sugar: the lone artifact's URL, or undefined for 0 / >1. */
|
|
45
|
+
url;
|
|
46
|
+
/** True iff {@link failed} is empty. */
|
|
47
|
+
ok;
|
|
48
|
+
constructor(workflowId, state, artifacts, succeeded, failed, downloader) {
|
|
49
|
+
this.workflowId = workflowId;
|
|
50
|
+
this.state = state;
|
|
51
|
+
this.artifacts = artifacts;
|
|
52
|
+
this.succeeded = succeeded;
|
|
53
|
+
this.failed = failed;
|
|
54
|
+
this.downloader = downloader;
|
|
55
|
+
this.url = artifacts.length === 1 ? artifacts[0].url : undefined;
|
|
56
|
+
this.ok = failed.length === 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Address a succeeded input by the `key:` given to `file()`. Duplicate keys
|
|
60
|
+
* are not valid input — the producer enforces key uniqueness (a later
|
|
61
|
+
* ticket); the first match is returned.
|
|
62
|
+
* @throws {GislNoSuchKeyError} when no succeeded entry has that key (a
|
|
63
|
+
* keyless run always throws — it is positionally addressable only).
|
|
64
|
+
*/
|
|
65
|
+
byKey(key) {
|
|
66
|
+
const item = this.succeeded.find((i) => i.key === key);
|
|
67
|
+
if (item === undefined) {
|
|
68
|
+
throw new GislNoSuchKeyError(`No result for key '${key}'.`);
|
|
69
|
+
}
|
|
70
|
+
return item;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Write the single output to `path`. Requires EXACTLY ONE artifact.
|
|
74
|
+
* @throws {GislSinkError} reason `not_single_output` for 0/>1 outputs;
|
|
75
|
+
* reason `downloader_unavailable` when no downloader is bound.
|
|
76
|
+
*/
|
|
77
|
+
async toFile(path) {
|
|
78
|
+
if (this.artifacts.length !== 1) {
|
|
79
|
+
throw new GislSinkError(`toFile() requires exactly one output; this run produced ${this.artifacts.length}. ` +
|
|
80
|
+
'Use downloadTo() for multi-output runs.', { reason: 'not_single_output' });
|
|
81
|
+
}
|
|
82
|
+
await this.requireDownloader().downloadTo(this.artifacts[0].url, path);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Download every output into `dir` (filename per output), in output order.
|
|
86
|
+
* Returns the {@link Manifest} of local paths written.
|
|
87
|
+
* @throws {GislSinkError} reason `partial_failure` when `failOnPartial` and
|
|
88
|
+
* the run had failed inputs; reason `downloader_unavailable` when no
|
|
89
|
+
* downloader is bound.
|
|
90
|
+
*/
|
|
91
|
+
async downloadTo(dir, options) {
|
|
92
|
+
if (options?.failOnPartial && this.failed.length > 0) {
|
|
93
|
+
throw new GislSinkError(`downloadTo({ failOnPartial: true }) but the run had ${this.failed.length} failed input(s).`, { reason: 'partial_failure' });
|
|
94
|
+
}
|
|
95
|
+
if (dir === '') {
|
|
96
|
+
throw new GislSinkError("downloadTo(): the directory argument is empty. Pass a target directory (use '.' for the current directory).", { reason: 'invalid_directory' });
|
|
97
|
+
}
|
|
98
|
+
const downloader = this.requireDownloader();
|
|
99
|
+
const sep = dir.endsWith('/') || dir.endsWith('\\') ? '' : '/';
|
|
100
|
+
// Resolve destinations first so a basename collision fails loudly BEFORE any
|
|
101
|
+
// byte is written — silently overwriting an earlier output is data loss.
|
|
102
|
+
const names = this.artifacts.map(
|
|
103
|
+
// Strip any directory component from a server-supplied filename so a value
|
|
104
|
+
// like "../x" or "a/b" cannot escape `dir` (mirrors PHP basename()).
|
|
105
|
+
(a) => a.filename.split(/[/\\]/).pop() ?? a.filename);
|
|
106
|
+
// Collision key is case-folded: many destination filesystems (macOS, NTFS)
|
|
107
|
+
// are case-insensitive, so `a.jpg` and `A.jpg` would target the same file.
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
for (const name of names) {
|
|
110
|
+
const key = name.toLowerCase();
|
|
111
|
+
if (seen.has(key)) {
|
|
112
|
+
throw new GislSinkError(`downloadTo(): two outputs resolve to the same filename '${name}' in '${dir}' ` +
|
|
113
|
+
'(case-insensitively). Download them to separate directories.', { reason: 'duplicate_filename' });
|
|
114
|
+
}
|
|
115
|
+
seen.add(key);
|
|
116
|
+
}
|
|
117
|
+
const paths = [];
|
|
118
|
+
for (let i = 0; i < this.artifacts.length; i++) {
|
|
119
|
+
const dest = `${dir}${sep}${names[i]}`;
|
|
120
|
+
await downloader.downloadTo(this.artifacts[i].url, dest);
|
|
121
|
+
paths.push(dest);
|
|
122
|
+
}
|
|
123
|
+
return { paths };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Plain-object projection. Field ORDER (workflowId, state, ok, url?,
|
|
127
|
+
* artifacts, succeeded, failed) is fixed to match the PHP `toArray()`
|
|
128
|
+
* reference so JSON-string parity holds (FF1 shape assertion + FF2b harness
|
|
129
|
+
* fixture). `url` is omitted entirely when undefined — `JSON.stringify`
|
|
130
|
+
* then produces the identical shape to PHP's omit-when-null `toArray()`.
|
|
131
|
+
*/
|
|
132
|
+
toJSON() {
|
|
133
|
+
// Re-project each OutputFile to exactly its four fields so structurally
|
|
134
|
+
// compatible inputs carrying extra properties can't leak into the JSON.
|
|
135
|
+
const file = (o) => ({
|
|
136
|
+
url: o.url,
|
|
137
|
+
filename: o.filename,
|
|
138
|
+
sizeBytes: o.sizeBytes,
|
|
139
|
+
operation: o.operation,
|
|
140
|
+
});
|
|
141
|
+
const rest = {
|
|
142
|
+
artifacts: this.artifacts.map(file),
|
|
143
|
+
succeeded: this.succeeded.map((i) => ({ key: i.key, outputs: i.outputs.map(file) })),
|
|
144
|
+
failed: this.failed.map((f) => ({
|
|
145
|
+
key: f.key,
|
|
146
|
+
error: f.error instanceof Error ? f.error.message : String(f.error),
|
|
147
|
+
})),
|
|
148
|
+
};
|
|
149
|
+
const head = { workflowId: this.workflowId, state: this.state, ok: this.ok };
|
|
150
|
+
// Insert `url` BETWEEN ok and artifacts when present, matching the PHP
|
|
151
|
+
// toArray() field order (workflowId, state, ok, url?, artifacts, ...) so
|
|
152
|
+
// JSON-string parity holds. Omitted entirely when undefined (PHP omits
|
|
153
|
+
// null), so `JSON.stringify` produces the identical shape.
|
|
154
|
+
return this.url === undefined
|
|
155
|
+
? { ...head, ...rest }
|
|
156
|
+
: { ...head, url: this.url, ...rest };
|
|
157
|
+
}
|
|
158
|
+
requireDownloader() {
|
|
159
|
+
if (this.downloader === undefined) {
|
|
160
|
+
throw new GislSinkError('This result has no downloader bound, so its outputs cannot be written to disk here ' +
|
|
161
|
+
'(e.g. a browser / no-I/O context). Fetch each output from its URL instead.', { reason: 'downloader_unavailable' });
|
|
162
|
+
}
|
|
163
|
+
return this.downloader;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Named constructors for {@link FileInput} — mirror the PHP static factories. */
|
|
167
|
+
export const fileInput = {
|
|
168
|
+
path(path) {
|
|
169
|
+
return { kind: 'path', path };
|
|
170
|
+
},
|
|
171
|
+
blob(blob) {
|
|
172
|
+
return { kind: 'blob', blob };
|
|
173
|
+
},
|
|
174
|
+
uploadId(fileId) {
|
|
175
|
+
return { kind: 'uploadId', fileId };
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* The file-first builder value. `client.file(path)` returns a `Recipe`;
|
|
180
|
+
* single-input operations called on it (`compress`, `convert`, `thumbnail`,
|
|
181
|
+
* `textWatermark`) chain SEQUENTIALLY — each op feeds the next, and the chain
|
|
182
|
+
* lowers to ONE workflow job with an ordered `operations[]` (per ADR-0004:
|
|
183
|
+
* operations execute sequentially, each consuming the previous output). A
|
|
184
|
+
* chain yields the TERMINAL output only; intermediates are consumed (surfaced
|
|
185
|
+
* by FF2b's `run()`/{@link RunResult}).
|
|
186
|
+
*
|
|
187
|
+
* **Immutable / clone-on-write.** Every op returns a NEW `Recipe` carrying the
|
|
188
|
+
* appended step — `this` is never mutated. A Recipe is therefore a reusable
|
|
189
|
+
* value: branching the same base recipe two different ways cannot let one
|
|
190
|
+
* branch observe the other's steps (the aliasing trap mutable builders fall
|
|
191
|
+
* into).
|
|
192
|
+
*
|
|
193
|
+
* FF2a is network-free: there is NO `run()` here (that is FF2b). The lowering
|
|
194
|
+
* seam {@link toWorkflowPayload} takes the resolved upload id as a parameter
|
|
195
|
+
* so it stays pure — FF2b's `run()` calls the SAME method after uploading, and
|
|
196
|
+
* the parity harness calls it with a fixed id to assert the lowered shape.
|
|
197
|
+
*
|
|
198
|
+
* Mirrors the PHP `Recipe`.
|
|
199
|
+
*/
|
|
200
|
+
export class Recipe {
|
|
201
|
+
input;
|
|
202
|
+
recipeKey;
|
|
203
|
+
steps;
|
|
204
|
+
presetDefaults;
|
|
205
|
+
scopedPresetDefaults;
|
|
206
|
+
client;
|
|
207
|
+
constructor(input, recipeKey = undefined, steps = [], presetDefaults, scopedPresetDefaults, client) {
|
|
208
|
+
this.input = input;
|
|
209
|
+
this.recipeKey = recipeKey;
|
|
210
|
+
this.steps = steps;
|
|
211
|
+
this.presetDefaults = presetDefaults;
|
|
212
|
+
this.scopedPresetDefaults = scopedPresetDefaults;
|
|
213
|
+
this.client = client;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Reduce file size. `optimize` selects a per-media preset (resolved to
|
|
217
|
+
* concrete wire fields at lower-time, exactly as `client.compress()` does).
|
|
218
|
+
*/
|
|
219
|
+
compress(optimize) {
|
|
220
|
+
if (optimize !== undefined && !Object.values(OptimizeFor).includes(optimize)) {
|
|
221
|
+
const allowed = Object.values(OptimizeFor).join(', ');
|
|
222
|
+
throw new GislConfigError(`compress 'optimize' must be one of ${allowed}; got '${String(optimize)}'.`, { reason: 'invalid_optimize', conflictingFields: ['optimize'] });
|
|
223
|
+
}
|
|
224
|
+
return this.withStep({ opType: 'compress', options: optimize === undefined ? {} : { optimize } });
|
|
225
|
+
}
|
|
226
|
+
/** Change format. `format` is lowered verbatim to the `format` wire option. */
|
|
227
|
+
convert(format) {
|
|
228
|
+
return this.withStep({ opType: 'convert', options: { format } });
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Generate a preview. Width and/or height in pixels; an omitted dimension is
|
|
232
|
+
* dropped from the wire options (not sent as `undefined`).
|
|
233
|
+
*/
|
|
234
|
+
thumbnail(options = {}) {
|
|
235
|
+
const wire = {};
|
|
236
|
+
if (options.width !== undefined)
|
|
237
|
+
wire.width = options.width;
|
|
238
|
+
if (options.height !== undefined)
|
|
239
|
+
wire.height = options.height;
|
|
240
|
+
return this.withStep({ opType: 'thumbnail', options: wire });
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Apply a text watermark. Single-input (the text is an option, not a
|
|
244
|
+
* secondary file) — lowers to the `text_watermark` op with a `text` option.
|
|
245
|
+
*/
|
|
246
|
+
textWatermark(text) {
|
|
247
|
+
return this.withStep({ opType: 'text_watermark', options: { text } });
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Lower this recipe to a workflow-create payload against a resolved upload
|
|
251
|
+
* id. Single-input chain → ONE job, `source: upload(fileId)`, ordered
|
|
252
|
+
* `operations[]`; the job `id` is omitted (a single job referenced by
|
|
253
|
+
* nothing — the server auto-assigns `job_N`).
|
|
254
|
+
*
|
|
255
|
+
* @internal Consumed by FF2b's `run()` (after a real upload) and by the
|
|
256
|
+
* cross-language parity harness (with a fixed id). Not part of the
|
|
257
|
+
* caller-facing fluent surface.
|
|
258
|
+
*/
|
|
259
|
+
toWorkflowPayload(fileId) {
|
|
260
|
+
const operations = this.steps.map((step) => this.lowerStep(step));
|
|
261
|
+
// Key order (source, operations) matches the PHP `toWire()` so the
|
|
262
|
+
// JSON-string serialisation is byte-identical across languages.
|
|
263
|
+
const job = { source: uploadSource(fileId), operations };
|
|
264
|
+
return { jobs: [job] };
|
|
265
|
+
}
|
|
266
|
+
/** The result-addressing key passed to `file()`, or undefined. */
|
|
267
|
+
key() {
|
|
268
|
+
return this.recipeKey;
|
|
269
|
+
}
|
|
270
|
+
/** The number of operations chained so far (introspection / tests). */
|
|
271
|
+
get stepCount() {
|
|
272
|
+
return this.steps.length;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Execute the recipe end-to-end: upload the input (when required), create
|
|
276
|
+
* the workflow, await a terminal state (SSE with poll fallback), then
|
|
277
|
+
* resolve the produced downloads into a flat {@link RunResult}. Throws
|
|
278
|
+
* {@link GislTimeoutError} if `maxWait` elapses before terminal status.
|
|
279
|
+
*
|
|
280
|
+
* Mirrors the operation-first `OperationBuilder.run` (in `builder.ts`).
|
|
281
|
+
* Requires a client bound at construction time — `gisl().file(...)` wires
|
|
282
|
+
* it; a directly-constructed `Recipe` (e.g. in a lowering-only test) has no
|
|
283
|
+
* client and throws {@link GislConfigError}.
|
|
284
|
+
*/
|
|
285
|
+
async run(options = {}) {
|
|
286
|
+
const signal = options.signal;
|
|
287
|
+
const onProgress = options.onProgress;
|
|
288
|
+
if (this.client === undefined) {
|
|
289
|
+
throw new GislConfigError('Recipe.run() requires a client; build the recipe via gisl().file(...) rather than constructing Recipe directly.', { reason: 'no_client' });
|
|
290
|
+
}
|
|
291
|
+
const deadline = Date.now() + _parseMaxWait(options.maxWait ?? 300_000);
|
|
292
|
+
// 1. Resolve the upload id. A pre-uploaded id skips the upload entirely;
|
|
293
|
+
// a path / blob is uploaded now, emitting {phase:'upload'} progress.
|
|
294
|
+
let fileId;
|
|
295
|
+
if (this.input.kind === 'uploadId') {
|
|
296
|
+
fileId = this.input.fileId;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const source = this.input.kind === 'path' ? this.input.path : this.input.blob;
|
|
300
|
+
const up = await this.client.uploadFile(source, {
|
|
301
|
+
signal,
|
|
302
|
+
...(onProgress !== undefined
|
|
303
|
+
? {
|
|
304
|
+
onProgress: (uploadedBytes, totalBytes) => {
|
|
305
|
+
onProgress({ phase: 'upload', uploadedBytes, totalBytes });
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
: {}),
|
|
309
|
+
});
|
|
310
|
+
fileId = up.fileId;
|
|
311
|
+
}
|
|
312
|
+
_checkAborted(signal);
|
|
313
|
+
if (Date.now() >= deadline) {
|
|
314
|
+
throw new GislTimeoutError('Upload completed but maxWait elapsed before workflow could be created');
|
|
315
|
+
}
|
|
316
|
+
// 2. Create the workflow from the lowered payload.
|
|
317
|
+
const payload = this.toWorkflowPayload(fileId);
|
|
318
|
+
const created = await this.client.createWorkflow(payload);
|
|
319
|
+
_checkAborted(signal);
|
|
320
|
+
// 3. Wait to terminal status — SSE first, poll on a genuine SSE error.
|
|
321
|
+
// Caller-aborted + deadline-elapsed errors MUST propagate (not transient).
|
|
322
|
+
let finalStatus;
|
|
323
|
+
try {
|
|
324
|
+
finalStatus = await _consumeSseToTerminal(this.client, {
|
|
325
|
+
workflowId: created.workflowId,
|
|
326
|
+
deadline,
|
|
327
|
+
signal,
|
|
328
|
+
onProgress,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
// Only genuine SSE transport / clean-stream-end failures fall through to
|
|
333
|
+
// poll. Caller-deadline, abort, and API errors (a 401/402/etc. from
|
|
334
|
+
// /events, or an onProgress callback throw surfacing as GislApiError)
|
|
335
|
+
// MUST propagate — re-issuing the same doomed request via poll would mask
|
|
336
|
+
// them. Mirrors the PHP BuilderInternals::awaitTerminal sealed-marker
|
|
337
|
+
// discipline (codex review medium).
|
|
338
|
+
if (err instanceof GislTimeoutError)
|
|
339
|
+
throw err;
|
|
340
|
+
if (err instanceof DOMException && err.name === 'AbortError')
|
|
341
|
+
throw err;
|
|
342
|
+
if (err instanceof GislApiError)
|
|
343
|
+
throw err;
|
|
344
|
+
finalStatus = await _pollToTerminal(this.client, {
|
|
345
|
+
workflowId: created.workflowId,
|
|
346
|
+
deadline,
|
|
347
|
+
signal,
|
|
348
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// 4. Fetch downloads. The maxWait deadline covers upload + create + wait +
|
|
352
|
+
// downloads, so check before issuing the request (mirrors builder.ts).
|
|
353
|
+
if (Date.now() >= deadline) {
|
|
354
|
+
throw new GislTimeoutError(`Workflow ${created.workflowId} reached terminal status but maxWait elapsed before downloads could be fetched`);
|
|
355
|
+
}
|
|
356
|
+
const downloads = await this.client.getWorkflowDownloads(created.workflowId);
|
|
357
|
+
// Flatten to the lean OutputFile[] (the four file-first fields only).
|
|
358
|
+
const artifacts = [];
|
|
359
|
+
for (const job of downloads.downloads) {
|
|
360
|
+
for (const f of job.files) {
|
|
361
|
+
artifacts.push({
|
|
362
|
+
url: f.downloadUrl,
|
|
363
|
+
filename: f.filename,
|
|
364
|
+
sizeBytes: f.sizeBytes,
|
|
365
|
+
operation: f.operation,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Partition the single input into succeeded / failed by terminal state.
|
|
370
|
+
// Success is ONLY `completed`: every other terminal state — `failed`,
|
|
371
|
+
// `partially_failed`, `cancelled`, `expired`,
|
|
372
|
+
// `paused_insufficient_credits` — is a non-success and partitions into
|
|
373
|
+
// `failed` so a caller's `ok`/`succeeded` check can never treat a
|
|
374
|
+
// cancelled/expired/paused run as a clean result (codex review high).
|
|
375
|
+
const state = finalStatus.status;
|
|
376
|
+
const key = this.recipeKey ?? null;
|
|
377
|
+
let succeeded;
|
|
378
|
+
let failed;
|
|
379
|
+
if (state === 'completed') {
|
|
380
|
+
succeeded = [{ key, outputs: artifacts }];
|
|
381
|
+
failed = [];
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
const firstError = (finalStatus.jobs ?? [])
|
|
385
|
+
.flatMap((j) => j.operations ?? [])
|
|
386
|
+
.map((op) => op.errorMessage)
|
|
387
|
+
.find((m) => m !== undefined);
|
|
388
|
+
succeeded = [];
|
|
389
|
+
failed = [
|
|
390
|
+
{ key, error: new Error(firstError !== undefined ? `${state}: ${firstError}` : state) },
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
// Download URLs from getWorkflowDownloads are pre-signed and require no SDK
|
|
394
|
+
// auth, so the downloader issues a plain unauthenticated fetch.
|
|
395
|
+
const downloader = new HttpDownloader();
|
|
396
|
+
return new RunResult(created.workflowId, state, artifacts, succeeded, failed, downloader);
|
|
397
|
+
}
|
|
398
|
+
withStep(step) {
|
|
399
|
+
return new Recipe(this.input, this.recipeKey, [...this.steps, step], this.presetDefaults, this.scopedPresetDefaults, this.client);
|
|
400
|
+
}
|
|
401
|
+
lowerStep(step) {
|
|
402
|
+
const options = step.opType === 'compress' ? this.lowerCompressOptions(step.options) : { ...step.options };
|
|
403
|
+
// Empty options omit the `options` wire key entirely, so TS (undefined →
|
|
404
|
+
// absent) and PHP (null → absent) serialise byte-identically.
|
|
405
|
+
return Object.keys(options).length === 0
|
|
406
|
+
? { type: step.opType }
|
|
407
|
+
: { type: step.opType, options };
|
|
408
|
+
}
|
|
409
|
+
lowerCompressOptions(options) {
|
|
410
|
+
const optimize = options.optimize;
|
|
411
|
+
const media = this.compressMediaHint();
|
|
412
|
+
if (media === undefined) {
|
|
413
|
+
// Cannot infer a media class (a Blob without a recognised name, or a
|
|
414
|
+
// bare upload id) → preset resolution is impossible. Fail FAST rather
|
|
415
|
+
// than silently dropping an explicit `optimize`; bare compress() is fine.
|
|
416
|
+
if (optimize !== undefined) {
|
|
417
|
+
throw new GislConfigError(`compress(optimize: ${String(optimize)}) needs a media type to resolve the preset, but the ` +
|
|
418
|
+
'input has no inferable media (a pre-uploaded file id or unnamed Blob carries no extension). ' +
|
|
419
|
+
'Use a path with a file extension, or call compress() without optimize.', { reason: 'media_unknown', conflictingFields: ['optimize'] });
|
|
420
|
+
}
|
|
421
|
+
return {};
|
|
422
|
+
}
|
|
423
|
+
const input = { media, op: 'compress', explicitOptions: {} };
|
|
424
|
+
if (this.presetDefaults !== undefined) {
|
|
425
|
+
input.presetDefaults = this.presetDefaults;
|
|
426
|
+
}
|
|
427
|
+
if (this.scopedPresetDefaults !== undefined) {
|
|
428
|
+
input.scopedPresetDefaults =
|
|
429
|
+
this.scopedPresetDefaults;
|
|
430
|
+
}
|
|
431
|
+
if (optimize !== undefined) {
|
|
432
|
+
input.optimize = optimize;
|
|
433
|
+
}
|
|
434
|
+
return { ...resolveCompressOptions(input).wireOptions };
|
|
435
|
+
}
|
|
436
|
+
compressMediaHint() {
|
|
437
|
+
if (this.input.kind === 'path') {
|
|
438
|
+
return _detectCompressMedia(this.input.path);
|
|
439
|
+
}
|
|
440
|
+
if (this.input.kind === 'blob') {
|
|
441
|
+
return _detectCompressMedia(this.input.blob);
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ErrorCode = "missing_credentials" | "feature_requires_auth" | "undeclared_asset" | "unused_asset" | "per_input_options_not_supported" | "chain_cardinality_mismatch" | "multipart_part_invalid" | "multipart_part_count_exceeded" | "timeout" | "aborted" | "validation_failed" | "auth_failed" | "feature_tier_restricted" | "tier_restriction" | "multipart_session_ownership" | "multipart_session_auth_required" | "multipart_session_not_found" | "workflow_expired" | "balance_exhausted" | "feature_not_available" | "upload_size_exceeds_tier" | "upload_duration_exceeds_tier" | "probe_pending" | "requires_reencode" | "invalid_options" | "invalid_combination" | "missing_dependency" | "unsupported_value" | "type_mismatch" | "upload_failed" | "workflow_failed";
|
|
1
|
+
export type ErrorCode = "missing_credentials" | "feature_requires_auth" | "undeclared_asset" | "unused_asset" | "per_input_options_not_supported" | "chain_cardinality_mismatch" | "multipart_part_invalid" | "multipart_part_count_exceeded" | "timeout" | "aborted" | "validation_failed" | "cyclic_workflow_edges" | "workflow_edge_references_unknown_job" | "reserved_job_id_pattern" | "cyclic_job_output_source_graph" | "auth_failed" | "feature_tier_restricted" | "tier_restriction" | "multipart_session_ownership" | "multipart_session_auth_required" | "multipart_session_not_found" | "workflow_expired" | "balance_exhausted" | "feature_not_available" | "upload_size_exceeds_tier" | "upload_duration_exceeds_tier" | "probe_pending" | "requires_reencode" | "invalid_options" | "invalid_combination" | "missing_dependency" | "unsupported_value" | "type_mismatch" | "upload_failed" | "workflow_failed";
|
|
2
2
|
export type ErrorCategory = 'api' | 'config' | 'network' | 'auth' | 'validation' | 'chain';
|
|
3
3
|
export type ErrorStatus = 'wired' | 'planned';
|
|
4
4
|
export interface ErrorEntry {
|
|
@@ -150,6 +150,52 @@ export const ERROR_CODES = Object.freeze({
|
|
|
150
150
|
"details": "array",
|
|
151
151
|
}),
|
|
152
152
|
}),
|
|
153
|
+
"cyclic_workflow_edges": Object.freeze({
|
|
154
|
+
code: "cyclic_workflow_edges",
|
|
155
|
+
category: "validation",
|
|
156
|
+
source: "ErrorEnvelope.error",
|
|
157
|
+
status: "wired",
|
|
158
|
+
httpStatus: 422,
|
|
159
|
+
retryable: false,
|
|
160
|
+
sdkClass: "GislValidationError",
|
|
161
|
+
description: "422 — cycle/self-edge in the explicit `workflow_edges` DAG. Wire `CYCLIC_WORKFLOW_EDGES` (g8PPkbNu); ValidationErrorEnvelope shape, `details[0].field`=`workflow_edges`.",
|
|
162
|
+
metadataSchema: Object.freeze({
|
|
163
|
+
"details": "array",
|
|
164
|
+
}),
|
|
165
|
+
}),
|
|
166
|
+
"workflow_edge_references_unknown_job": Object.freeze({
|
|
167
|
+
code: "workflow_edge_references_unknown_job",
|
|
168
|
+
category: "validation",
|
|
169
|
+
source: "ErrorEnvelope.error",
|
|
170
|
+
status: "wired",
|
|
171
|
+
httpStatus: 400,
|
|
172
|
+
retryable: false,
|
|
173
|
+
sdkClass: "GislValidationError",
|
|
174
|
+
description: "400 — a `workflow_edges` entry, job-level `source`, or `inputs[].source` references a job not in the request. Wire `WORKFLOW_EDGE_REFERENCES_UNKNOWN_JOB` (g8PPkbNu).",
|
|
175
|
+
metadataSchema: Object.freeze({}),
|
|
176
|
+
}),
|
|
177
|
+
"reserved_job_id_pattern": Object.freeze({
|
|
178
|
+
code: "reserved_job_id_pattern",
|
|
179
|
+
category: "validation",
|
|
180
|
+
source: "ErrorEnvelope.error",
|
|
181
|
+
status: "wired",
|
|
182
|
+
httpStatus: 400,
|
|
183
|
+
retryable: false,
|
|
184
|
+
sdkClass: "GislValidationError",
|
|
185
|
+
description: "400 — user job `id` matches the reserved `^job_\\d+$` pattern as the SOLE failure. Wire `RESERVED_JOB_ID_PATTERN` (g8PPkbNu); mixed violations keep generic `BAD_REQUEST`.",
|
|
186
|
+
metadataSchema: Object.freeze({}),
|
|
187
|
+
}),
|
|
188
|
+
"cyclic_job_output_source_graph": Object.freeze({
|
|
189
|
+
code: "cyclic_job_output_source_graph",
|
|
190
|
+
category: "validation",
|
|
191
|
+
source: "ErrorEnvelope.error",
|
|
192
|
+
status: "wired",
|
|
193
|
+
httpStatus: 400,
|
|
194
|
+
retryable: false,
|
|
195
|
+
sdkClass: "GislValidationError",
|
|
196
|
+
description: "400 — an implicit cycle in the `job_output` source graph, caught during effective-input-MIME resolution on POST /api/workflows. Wire `CYCLIC_JOB_OUTPUT_SOURCE_GRAPH` (g8PPkbNu).",
|
|
197
|
+
metadataSchema: Object.freeze({}),
|
|
198
|
+
}),
|
|
153
199
|
"auth_failed": Object.freeze({
|
|
154
200
|
code: "auth_failed",
|
|
155
201
|
category: "auth",
|
|
@@ -458,6 +504,10 @@ export const ERROR_CATEGORIES = Object.freeze({
|
|
|
458
504
|
"multipart_part_invalid",
|
|
459
505
|
"multipart_part_count_exceeded",
|
|
460
506
|
"validation_failed",
|
|
507
|
+
"cyclic_workflow_edges",
|
|
508
|
+
"workflow_edge_references_unknown_job",
|
|
509
|
+
"reserved_job_id_pattern",
|
|
510
|
+
"cyclic_job_output_source_graph",
|
|
461
511
|
"invalid_options",
|
|
462
512
|
"invalid_combination",
|
|
463
513
|
"missing_dependency",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// CODE GENERATED — DO NOT EDIT.
|
|
2
2
|
// Source: compression_contracts/sdk-spec/ (see sdk-spec/README.md).
|
|
3
3
|
// Regenerate with: scripts/generate.py.
|
|
4
|
-
export const SDK_SPEC_VERSION = "1.
|
|
4
|
+
export const SDK_SPEC_VERSION = "1.2.0";
|
|
5
5
|
export const PRESET_VERSION = "1.0";
|
|
6
6
|
export const PRESET_CONFIG_HASH = "sha256:35aeb0b6b86edd9814ace5b75cfeef8a7b1432ecb8e4d5ee8b38d9187a0b45eb";
|
package/dist/gisl.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { GislClientConfig } from './types.js';
|
|
|
23
23
|
import { OperationBuilder } from './builder.js';
|
|
24
24
|
import { MergeBuilder, type Asset, type MergeOptions } from './merge.js';
|
|
25
25
|
import { PresetDefaults } from './ergonomic/presets/index.js';
|
|
26
|
+
import { Recipe, type FileInput } from './file-first.js';
|
|
26
27
|
/**
|
|
27
28
|
* Operations that may be invoked on a `gisl.anonymous()` client without
|
|
28
29
|
* raising `GislFeatureRequiresAuthError`. Empty until the free-tier launch
|
|
@@ -67,6 +68,15 @@ export declare function create(opts?: GislCreateOptions): Promise<ErgonomicClien
|
|
|
67
68
|
* ergonomic factory's narrower string-only typing).
|
|
68
69
|
*/
|
|
69
70
|
export type ErgonomicClient = GislClient & {
|
|
71
|
+
/**
|
|
72
|
+
* File-first entry point (FF2a). Returns an immutable {@link Recipe} you
|
|
73
|
+
* call operations on (`.compress()` / `.convert()` / `.thumbnail()` /
|
|
74
|
+
* `.textWatermark()`), chaining sequentially. A bare string is a filesystem
|
|
75
|
+
* path; pass a {@link FileInput} (e.g. `fileInput.uploadId(...)`) to reuse a
|
|
76
|
+
* pre-uploaded file. `key` is RESULT-addressing only — never input wiring.
|
|
77
|
+
* Execution (`run()`) lands in FF2b.
|
|
78
|
+
*/
|
|
79
|
+
file(input: string | Blob | FileInput, key?: string): Recipe;
|
|
70
80
|
compress(input: string | Blob, options?: Record<string, unknown>): OperationBuilder;
|
|
71
81
|
convert(input: string | Blob, options?: Record<string, unknown>): OperationBuilder;
|
|
72
82
|
thumbnail(input: string | Blob, options?: Record<string, unknown>): OperationBuilder;
|
package/dist/gisl.js
CHANGED
|
@@ -22,6 +22,7 @@ import { resolveApiKey, resolveEndpoint, } from './credentials.js';
|
|
|
22
22
|
import { OperationBuilder } from './builder.js';
|
|
23
23
|
import { MergeBuilder, asset } from './merge.js';
|
|
24
24
|
import { PresetDefaults } from './ergonomic/presets/index.js';
|
|
25
|
+
import { Recipe, fileInput } from './file-first.js';
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Anonymous-capable operation allowlist (internal)
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
@@ -71,6 +72,22 @@ export async function create(opts = {}) {
|
|
|
71
72
|
function wrapErgonomic(client, presetDefaults, scopedPresetDefaults) {
|
|
72
73
|
return new Proxy(client, {
|
|
73
74
|
get(target, prop, receiver) {
|
|
75
|
+
if (prop === 'file') {
|
|
76
|
+
// File-first entry point — the subject of the file-first surface.
|
|
77
|
+
// A bare string is a filesystem path, a Blob/File an in-memory input;
|
|
78
|
+
// pass a `FileInput` (e.g. `fileInput.uploadId(...)`) to reuse a
|
|
79
|
+
// pre-uploaded file. `key` is RESULT-addressing only. The Proxy's
|
|
80
|
+
// closure forwards the same preset-defaults references the op builders
|
|
81
|
+
// get, so a file-first `compress()` resolves presets identically.
|
|
82
|
+
return (input, key) => {
|
|
83
|
+
const resolved = typeof input === 'string'
|
|
84
|
+
? fileInput.path(input)
|
|
85
|
+
: input instanceof Blob
|
|
86
|
+
? fileInput.blob(input)
|
|
87
|
+
: input;
|
|
88
|
+
return new Recipe(resolved, key, [], presetDefaults, scopedPresetDefaults, target);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
74
91
|
if (prop === 'compress' || prop === 'convert' || prop === 'thumbnail') {
|
|
75
92
|
return (input, options = {}) => {
|
|
76
93
|
// T4b — pass client-scope presetDefaults into the builder so
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Downloader } from './file-first.js';
|
|
2
|
+
/**
|
|
3
|
+
* Streams a (typically pre-signed) URL to a local path without buffering the
|
|
4
|
+
* whole body in memory. Pre-signed download URLs require no SDK auth, so this
|
|
5
|
+
* issues a plain unauthenticated `fetch`.
|
|
6
|
+
*/
|
|
7
|
+
export declare class HttpDownloader implements Downloader {
|
|
8
|
+
downloadTo(url: string, destPath: string): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only streaming {@link Downloader} implementation.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own module so the framework-free `file-first.ts` stays
|
|
5
|
+
* Node-import-free; the Node `fs`/`stream` imports are isolated here.
|
|
6
|
+
*/
|
|
7
|
+
import { createWriteStream } from 'node:fs';
|
|
8
|
+
import { Readable } from 'node:stream';
|
|
9
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
|
+
import { GislNetworkError, GislSinkError } from './errors.js';
|
|
11
|
+
/**
|
|
12
|
+
* Streams a (typically pre-signed) URL to a local path without buffering the
|
|
13
|
+
* whole body in memory. Pre-signed download URLs require no SDK auth, so this
|
|
14
|
+
* issues a plain unauthenticated `fetch`.
|
|
15
|
+
*/
|
|
16
|
+
export class HttpDownloader {
|
|
17
|
+
async downloadTo(url, destPath) {
|
|
18
|
+
// Source-read failures surface as GislNetworkError to match the PHP
|
|
19
|
+
// StreamingDownloader (both raise GislNetworkError when the output URL
|
|
20
|
+
// cannot be fetched); a dest-write failure is GislSinkError(write_failed)
|
|
21
|
+
// on the RunResult sink side. Parity-critical: the FF1 sink contract tells
|
|
22
|
+
// callers to narrow with instanceof, so the source-read error type must
|
|
23
|
+
// match across languages.
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(url);
|
|
27
|
+
}
|
|
28
|
+
catch (cause) {
|
|
29
|
+
// A rejected fetch (DNS, TCP, TLS, mid-flight disconnect) must surface as
|
|
30
|
+
// GislNetworkError too — not the raw TypeError — so callers can narrow
|
|
31
|
+
// every download-source failure with `instanceof GislNetworkError`
|
|
32
|
+
// (codex review medium).
|
|
33
|
+
throw new GislNetworkError(`Failed to fetch download source: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new GislNetworkError(`Download failed with status ${res.status}`);
|
|
37
|
+
}
|
|
38
|
+
if (res.body === null) {
|
|
39
|
+
throw new GislNetworkError('Download response had no body');
|
|
40
|
+
}
|
|
41
|
+
// `fetch`'s WHATWG ReadableStream and Node's `stream/web` ReadableStream
|
|
42
|
+
// are structurally the same at runtime but typed in two different lib
|
|
43
|
+
// declarations; the cast bridges them for `Readable.fromWeb`.
|
|
44
|
+
try {
|
|
45
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Destination-write failures (unwritable dir, disk full, …) surface as
|
|
49
|
+
// GislSinkError(write_failed) to match the PHP StreamingDownloader — a
|
|
50
|
+
// raw Node ENOENT would otherwise leak through the parity-critical sink
|
|
51
|
+
// contract. Source-read failures are handled above as GislNetworkError.
|
|
52
|
+
throw new GislSinkError(`Failed to stream download to destination: ${destPath}`, { reason: 'write_failed' });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,13 @@ export { parseSseStream } from './sse.js';
|
|
|
4
4
|
export type { CreditsUsageOptions, GetSchemaOptions, GetSchemaResult, PreflightClipError, PreflightClipsResult, GislClientConfig, GislSseEvent, UploadOptions, WaitOptions, WorkflowCreatePayload, OperationDef, WorkflowSourcePayload, UploadSourcePayload, JobOutputSourcePayload, ExternalImportSourcePayload, ConnectionSourcePayload, JobInputV2Payload, JobDefinitionPayload, ExternalDestinationPayload, DeliveryPayload, DeliveryModePayload, DeliveryBundleFormatPayload, DeliverySelectionPayload, DeliverySelectionTypePayload, DeliveryOutputRefPayload, WorkflowProcessingPayload, ProcessingClassHintPayload, MultipartCheckpointState, _Sdk3HandCodedUploadedPart, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignedPart, _Sdk3HandCodedPresignPartsResult, _Sdk3HandCodedKeepaliveResult, } from './types.js';
|
|
5
5
|
export { uploadSource, jobOutputSource, externalImportSource, connectionSource, } from './types.js';
|
|
6
6
|
export type { GislConfigErrorMetadata } from './errors.js';
|
|
7
|
-
export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError, GislAbortError, GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError, GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError, GislChainCardinalityMismatchError, } from './errors.js';
|
|
7
|
+
export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError, GislAbortError, GislNetworkError, GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError, GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError, GislChainCardinalityMismatchError, GislNoSuchKeyError, GislSinkError, } from './errors.js';
|
|
8
8
|
export type { GislApiErrorOptions, GislUploadCapKind } from './errors.js';
|
|
9
|
+
export { RunResult } from './file-first.js';
|
|
10
|
+
export type { OutputFile, ItemResult, ItemFailure, Manifest, Downloader, } from './file-first.js';
|
|
11
|
+
export { Recipe, fileInput } from './file-first.js';
|
|
12
|
+
export type { FileInput } from './file-first.js';
|
|
13
|
+
export { HttpDownloader } from './http-downloader.js';
|
|
9
14
|
export { gisl, create } from './gisl.js';
|
|
10
15
|
export type { GislCreateOptions, Environment, ErgonomicClient } from './gisl.js';
|
|
11
16
|
export { presetDefaults, PresetDefaults, type PresetMedia, type PresetOp, type AnyPresetOptions, ImageCompressPresetOptions, type ImageCompressPresetOptionsInput, AudioCompressPresetOptions, type AudioCompressPresetOptionsInput, VideoCompressPresetOptions, type VideoCompressPresetOptionsInput, DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptionsInput, DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput, DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput, DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput, OptimizeFor, ImageMode, ImageFit, ImageMetadataPolicy, IccProfilePolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from './ergonomic/presets/index.js';
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ export { uploadSource, jobOutputSource, externalImportSource, connectionSource,
|
|
|
7
7
|
export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError,
|
|
8
8
|
// SDK-3 (Wb6ebOMM) — typed errors for the 3 resume-support endpoints.
|
|
9
9
|
GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError, GislAbortError,
|
|
10
|
+
// FF2b / tywwynmN — transport-level failure (mirrors PHP GislNetworkError);
|
|
11
|
+
// raised by the file-first HttpDownloader when an output URL cannot be read.
|
|
12
|
+
GislNetworkError,
|
|
10
13
|
// T1 / wVU4xHx3 — local config-error tree (pre-I/O; sibling of GislApiError).
|
|
11
14
|
GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError,
|
|
12
15
|
// T3 / cuecCmb5 — merge-compose local validation errors.
|
|
@@ -14,7 +17,17 @@ GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedE
|
|
|
14
17
|
// T6 / aDR1jnyZ — chain-cardinality validation (dormant until chain
|
|
15
18
|
// methods on OperationBuilder ship; type + audit registration land
|
|
16
19
|
// here so the future chain-method PR is a pure addition).
|
|
17
|
-
GislChainCardinalityMismatchError,
|
|
20
|
+
GislChainCardinalityMismatchError,
|
|
21
|
+
// FF1 / 3BIxEnfR — file-first result sink errors.
|
|
22
|
+
GislNoSuchKeyError, GislSinkError, } from './errors.js';
|
|
23
|
+
// File-first result surface (FF1 / 3BIxEnfR) — coexists with the
|
|
24
|
+
// operation-first `Result`/`Artifact` until FF6 removes the old layer.
|
|
25
|
+
export { RunResult } from './file-first.js';
|
|
26
|
+
// File-first builder (FF2a / MfV0PDok) — `client.file(path).op()...` lowering.
|
|
27
|
+
export { Recipe, fileInput } from './file-first.js';
|
|
28
|
+
// File-first execution (FF2b / MfV0PDok) — Node streaming downloader bound by
|
|
29
|
+
// `Recipe.run()` to write pre-signed output URLs to disk.
|
|
30
|
+
export { HttpDownloader } from './http-downloader.js';
|
|
18
31
|
// Ergonomic-layer entrypoint (T1 / wVU4xHx3) — `gisl.create()` factory +
|
|
19
32
|
// credential-chain types. `gisl.anonymous()` (public export) lands once
|
|
20
33
|
// the anonymous-capable operation allowlist is non-empty (plan §12).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giveitsmaller/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Node.js SDK for the GISL (Give It Smaller) file compression and processing API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"node": ">=18"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@giveitsmaller/contracts": "^0.
|
|
22
|
+
"@giveitsmaller/contracts": "^0.9.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^22",
|
|
26
26
|
"typescript": "^5.7",
|
|
27
|
-
"vitest": "^3.1",
|
|
27
|
+
"vitest": "^3.1 <3.2.5",
|
|
28
28
|
"yaml": "^2.6"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|