@giveitsmaller/sdk 0.6.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 +71 -0
- package/dist/builder.d.ts +406 -0
- package/dist/builder.js +706 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +42 -6
- package/dist/credentials.d.ts +61 -0
- package/dist/credentials.js +200 -0
- package/dist/ergonomic/preset_resolver.d.ts +75 -0
- package/dist/ergonomic/preset_resolver.js +568 -0
- package/dist/ergonomic/presets/_translate.d.ts +11 -0
- package/dist/ergonomic/presets/_translate.js +35 -0
- package/dist/ergonomic/presets/audio_compress.d.ts +16 -0
- package/dist/ergonomic/presets/audio_compress.js +45 -0
- package/dist/ergonomic/presets/document_epub_compress.d.ts +14 -0
- package/dist/ergonomic/presets/document_epub_compress.js +34 -0
- package/dist/ergonomic/presets/document_odf_compress.d.ts +14 -0
- package/dist/ergonomic/presets/document_odf_compress.js +34 -0
- package/dist/ergonomic/presets/document_office_compress.d.ts +16 -0
- package/dist/ergonomic/presets/document_office_compress.js +40 -0
- package/dist/ergonomic/presets/document_pdf_compress.d.ts +14 -0
- package/dist/ergonomic/presets/document_pdf_compress.js +35 -0
- package/dist/ergonomic/presets/image_compress.d.ts +43 -0
- package/dist/ergonomic/presets/image_compress.js +95 -0
- package/dist/ergonomic/presets/index.d.ts +77 -0
- package/dist/ergonomic/presets/index.js +216 -0
- package/dist/ergonomic/presets/video_compress.d.ts +30 -0
- package/dist/ergonomic/presets/video_compress.js +83 -0
- package/dist/errors.d.ts +196 -1
- package/dist/errors.js +216 -0
- package/dist/file-first.d.ts +284 -0
- package/dist/file-first.js +445 -0
- package/dist/generated/sdk_spec/enums.d.ts +195 -0
- package/dist/generated/sdk_spec/enums.js +127 -0
- package/dist/generated/sdk_spec/errors.d.ts +16 -0
- package/dist/generated/sdk_spec/errors.js +523 -0
- package/dist/generated/sdk_spec/index.d.ts +4 -0
- package/dist/generated/sdk_spec/index.js +7 -0
- package/dist/generated/sdk_spec/presets.d.ts +6 -0
- package/dist/generated/sdk_spec/presets.js +157 -0
- package/dist/generated/sdk_spec/version.d.ts +3 -0
- package/dist/generated/sdk_spec/version.js +6 -0
- package/dist/gisl.d.ts +122 -0
- package/dist/gisl.js +283 -0
- package/dist/http-downloader.d.ts +9 -0
- package/dist/http-downloader.js +55 -0
- package/dist/index.d.ts +20 -5
- package/dist/index.js +45 -4
- package/dist/merge.d.ts +142 -0
- package/dist/merge.js +411 -0
- package/dist/types.d.ts +12 -14
- package/dist/types.js +18 -0
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export declare const OptimizeFor: {
|
|
2
|
+
readonly Size: "Size";
|
|
3
|
+
readonly Balanced: "Balanced";
|
|
4
|
+
readonly Quality: "Quality";
|
|
5
|
+
};
|
|
6
|
+
export type OptimizeFor = typeof OptimizeFor[keyof typeof OptimizeFor];
|
|
7
|
+
export declare const ImageMode: {
|
|
8
|
+
readonly Lossy: "lossy";
|
|
9
|
+
readonly Lossless: "lossless";
|
|
10
|
+
readonly Auto: "auto";
|
|
11
|
+
};
|
|
12
|
+
export type ImageMode = typeof ImageMode[keyof typeof ImageMode];
|
|
13
|
+
export declare const ImageFormat: {
|
|
14
|
+
readonly Original: "original";
|
|
15
|
+
readonly Auto: "auto";
|
|
16
|
+
readonly Smallest: "smallest";
|
|
17
|
+
readonly Jpeg: "jpeg";
|
|
18
|
+
readonly Png: "png";
|
|
19
|
+
readonly Webp: "webp";
|
|
20
|
+
readonly Avif: "avif";
|
|
21
|
+
};
|
|
22
|
+
export type ImageFormat = typeof ImageFormat[keyof typeof ImageFormat];
|
|
23
|
+
export declare const ImageFit: {
|
|
24
|
+
readonly Max: "max";
|
|
25
|
+
readonly Crop: "crop";
|
|
26
|
+
readonly Scale: "scale";
|
|
27
|
+
};
|
|
28
|
+
export type ImageFit = typeof ImageFit[keyof typeof ImageFit];
|
|
29
|
+
export declare const ImageMetadataPolicy: {
|
|
30
|
+
readonly All: "all";
|
|
31
|
+
readonly None: "none";
|
|
32
|
+
readonly Copyright: "copyright";
|
|
33
|
+
readonly Sensitive: "sensitive";
|
|
34
|
+
};
|
|
35
|
+
export type ImageMetadataPolicy = typeof ImageMetadataPolicy[keyof typeof ImageMetadataPolicy];
|
|
36
|
+
export declare const IccProfilePolicy: {
|
|
37
|
+
readonly Preserve: "preserve";
|
|
38
|
+
readonly Strip: "strip";
|
|
39
|
+
readonly Srgb: "srgb";
|
|
40
|
+
};
|
|
41
|
+
export type IccProfilePolicy = typeof IccProfilePolicy[keyof typeof IccProfilePolicy];
|
|
42
|
+
export declare const VideoCodec: {
|
|
43
|
+
readonly H264: "h264";
|
|
44
|
+
readonly H265: "h265";
|
|
45
|
+
readonly Vp9: "vp9";
|
|
46
|
+
readonly Av1: "av1";
|
|
47
|
+
};
|
|
48
|
+
export type VideoCodec = typeof VideoCodec[keyof typeof VideoCodec];
|
|
49
|
+
export declare const VideoPreset: {
|
|
50
|
+
readonly Ultrafast: "ultrafast";
|
|
51
|
+
readonly Superfast: "superfast";
|
|
52
|
+
readonly Veryfast: "veryfast";
|
|
53
|
+
readonly Faster: "faster";
|
|
54
|
+
readonly Fast: "fast";
|
|
55
|
+
readonly Medium: "medium";
|
|
56
|
+
readonly Slow: "slow";
|
|
57
|
+
readonly Slower: "slower";
|
|
58
|
+
readonly Veryslow: "veryslow";
|
|
59
|
+
};
|
|
60
|
+
export type VideoPreset = typeof VideoPreset[keyof typeof VideoPreset];
|
|
61
|
+
export declare const VideoFit: {
|
|
62
|
+
readonly Max: "max";
|
|
63
|
+
readonly Crop: "crop";
|
|
64
|
+
readonly Scale: "scale";
|
|
65
|
+
readonly Pad: "pad";
|
|
66
|
+
};
|
|
67
|
+
export type VideoFit = typeof VideoFit[keyof typeof VideoFit];
|
|
68
|
+
export declare const AudioBitrate: {
|
|
69
|
+
readonly _64: 64;
|
|
70
|
+
readonly _96: 96;
|
|
71
|
+
readonly _128: 128;
|
|
72
|
+
readonly _192: 192;
|
|
73
|
+
readonly _256: 256;
|
|
74
|
+
readonly _320: 320;
|
|
75
|
+
};
|
|
76
|
+
export type AudioBitrate = typeof AudioBitrate[keyof typeof AudioBitrate];
|
|
77
|
+
export declare const AudioCodec: {
|
|
78
|
+
readonly Aac: "aac";
|
|
79
|
+
readonly Opus: "opus";
|
|
80
|
+
readonly Vorbis: "vorbis";
|
|
81
|
+
readonly Copy: "copy";
|
|
82
|
+
};
|
|
83
|
+
export type AudioCodec = typeof AudioCodec[keyof typeof AudioCodec];
|
|
84
|
+
export declare const AudioSampleRate: {
|
|
85
|
+
readonly _22050: 22050;
|
|
86
|
+
readonly _44100: 44100;
|
|
87
|
+
readonly _48000: 48000;
|
|
88
|
+
};
|
|
89
|
+
export type AudioSampleRate = typeof AudioSampleRate[keyof typeof AudioSampleRate];
|
|
90
|
+
export declare const PdfProfile: {
|
|
91
|
+
readonly Web: "web";
|
|
92
|
+
readonly Print: "print";
|
|
93
|
+
readonly Archive: "archive";
|
|
94
|
+
readonly Max: "max";
|
|
95
|
+
};
|
|
96
|
+
export type PdfProfile = typeof PdfProfile[keyof typeof PdfProfile];
|
|
97
|
+
export declare const PdfColorspace: {
|
|
98
|
+
readonly Unchanged: "unchanged";
|
|
99
|
+
readonly Rgb: "rgb";
|
|
100
|
+
readonly Cmyk: "cmyk";
|
|
101
|
+
readonly Grayscale: "grayscale";
|
|
102
|
+
};
|
|
103
|
+
export type PdfColorspace = typeof PdfColorspace[keyof typeof PdfColorspace];
|
|
104
|
+
/** Catalog of every ergonomic enum (canonicalName → wire). */
|
|
105
|
+
export declare const ERGONOMIC_ENUMS: {
|
|
106
|
+
readonly OptimizeFor: {
|
|
107
|
+
readonly Size: "Size";
|
|
108
|
+
readonly Balanced: "Balanced";
|
|
109
|
+
readonly Quality: "Quality";
|
|
110
|
+
};
|
|
111
|
+
readonly ImageMode: {
|
|
112
|
+
readonly Lossy: "lossy";
|
|
113
|
+
readonly Lossless: "lossless";
|
|
114
|
+
readonly Auto: "auto";
|
|
115
|
+
};
|
|
116
|
+
readonly ImageFormat: {
|
|
117
|
+
readonly Original: "original";
|
|
118
|
+
readonly Auto: "auto";
|
|
119
|
+
readonly Smallest: "smallest";
|
|
120
|
+
readonly Jpeg: "jpeg";
|
|
121
|
+
readonly Png: "png";
|
|
122
|
+
readonly Webp: "webp";
|
|
123
|
+
readonly Avif: "avif";
|
|
124
|
+
};
|
|
125
|
+
readonly ImageFit: {
|
|
126
|
+
readonly Max: "max";
|
|
127
|
+
readonly Crop: "crop";
|
|
128
|
+
readonly Scale: "scale";
|
|
129
|
+
};
|
|
130
|
+
readonly ImageMetadataPolicy: {
|
|
131
|
+
readonly All: "all";
|
|
132
|
+
readonly None: "none";
|
|
133
|
+
readonly Copyright: "copyright";
|
|
134
|
+
readonly Sensitive: "sensitive";
|
|
135
|
+
};
|
|
136
|
+
readonly IccProfilePolicy: {
|
|
137
|
+
readonly Preserve: "preserve";
|
|
138
|
+
readonly Strip: "strip";
|
|
139
|
+
readonly Srgb: "srgb";
|
|
140
|
+
};
|
|
141
|
+
readonly VideoCodec: {
|
|
142
|
+
readonly H264: "h264";
|
|
143
|
+
readonly H265: "h265";
|
|
144
|
+
readonly Vp9: "vp9";
|
|
145
|
+
readonly Av1: "av1";
|
|
146
|
+
};
|
|
147
|
+
readonly VideoPreset: {
|
|
148
|
+
readonly Ultrafast: "ultrafast";
|
|
149
|
+
readonly Superfast: "superfast";
|
|
150
|
+
readonly Veryfast: "veryfast";
|
|
151
|
+
readonly Faster: "faster";
|
|
152
|
+
readonly Fast: "fast";
|
|
153
|
+
readonly Medium: "medium";
|
|
154
|
+
readonly Slow: "slow";
|
|
155
|
+
readonly Slower: "slower";
|
|
156
|
+
readonly Veryslow: "veryslow";
|
|
157
|
+
};
|
|
158
|
+
readonly VideoFit: {
|
|
159
|
+
readonly Max: "max";
|
|
160
|
+
readonly Crop: "crop";
|
|
161
|
+
readonly Scale: "scale";
|
|
162
|
+
readonly Pad: "pad";
|
|
163
|
+
};
|
|
164
|
+
readonly AudioBitrate: {
|
|
165
|
+
readonly _64: 64;
|
|
166
|
+
readonly _96: 96;
|
|
167
|
+
readonly _128: 128;
|
|
168
|
+
readonly _192: 192;
|
|
169
|
+
readonly _256: 256;
|
|
170
|
+
readonly _320: 320;
|
|
171
|
+
};
|
|
172
|
+
readonly AudioCodec: {
|
|
173
|
+
readonly Aac: "aac";
|
|
174
|
+
readonly Opus: "opus";
|
|
175
|
+
readonly Vorbis: "vorbis";
|
|
176
|
+
readonly Copy: "copy";
|
|
177
|
+
};
|
|
178
|
+
readonly AudioSampleRate: {
|
|
179
|
+
readonly _22050: 22050;
|
|
180
|
+
readonly _44100: 44100;
|
|
181
|
+
readonly _48000: 48000;
|
|
182
|
+
};
|
|
183
|
+
readonly PdfProfile: {
|
|
184
|
+
readonly Web: "web";
|
|
185
|
+
readonly Print: "print";
|
|
186
|
+
readonly Archive: "archive";
|
|
187
|
+
readonly Max: "max";
|
|
188
|
+
};
|
|
189
|
+
readonly PdfColorspace: {
|
|
190
|
+
readonly Unchanged: "unchanged";
|
|
191
|
+
readonly Rgb: "rgb";
|
|
192
|
+
readonly Cmyk: "cmyk";
|
|
193
|
+
readonly Grayscale: "grayscale";
|
|
194
|
+
};
|
|
195
|
+
};
|