@giveitsmaller/sdk 0.20.0 → 0.22.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/LICENSE +202 -0
- package/README.md +55 -14
- package/dist/_audit.js +2 -4
- package/dist/builder.d.ts +4 -4
- package/dist/builder.js +47 -22
- package/dist/client.d.ts +9 -6
- package/dist/client.js +90 -31
- package/dist/ergonomic/image_output_routes.d.ts +97 -0
- package/dist/ergonomic/image_output_routes.js +227 -25
- package/dist/ergonomic/option_types.d.ts +11 -2
- package/dist/ergonomic/preset_resolver.d.ts +24 -2
- package/dist/ergonomic/preset_resolver.js +100 -10
- package/dist/ergonomic/presets/image_compress.js +16 -4
- package/dist/ergonomic/presets/index.d.ts +9 -8
- package/dist/ergonomic/presets/index.js +1 -9
- package/dist/ergonomic/presets/video_compress.d.ts +14 -0
- package/dist/errors.d.ts +101 -2
- package/dist/errors.js +108 -1
- package/dist/file-first.d.ts +81 -13
- package/dist/file-first.js +329 -69
- package/dist/generated/sdk_spec/enums.d.ts +0 -26
- package/dist/generated/sdk_spec/enums.js +0 -16
- package/dist/generated/sdk_spec/errors.d.ts +1 -1
- package/dist/generated/sdk_spec/errors.js +159 -1
- package/dist/generated/sdk_spec/presets.js +0 -14
- package/dist/generated/sdk_spec/version.d.ts +2 -2
- package/dist/generated/sdk_spec/version.js +2 -2
- package/dist/gisl.d.ts +21 -2
- package/dist/handle.d.ts +6 -1
- package/dist/handle.js +42 -13
- package/dist/index.core.d.ts +4 -4
- package/dist/index.core.js +6 -3
- package/dist/merge.d.ts +23 -0
- package/dist/merge.js +2 -2
- package/dist/sse.js +49 -1
- package/dist/types.d.ts +11 -3
- package/dist/types.js +1 -0
- package/package.json +3 -3
- package/dist/ergonomic/presets/document_pdf_compress.d.ts +0 -12
- package/dist/ergonomic/presets/document_pdf_compress.js +0 -33
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResolvedOptions } from '../builder.js';
|
|
2
2
|
import type { OptimizeFor } from '../generated/sdk_spec/enums.js';
|
|
3
|
-
import { type PresetDefaults, type PresetMedia, type PresetOp } from './presets/index.js';
|
|
3
|
+
import { type PresetDefaults, type PresetMedia, type DetectedMedia, type PresetOp } from './presets/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* The preset matrix version emitted on every resolve. Re-exported from the
|
|
6
6
|
* GENERATED `sdk_spec/version.ts` (source of truth: contracts
|
|
@@ -17,7 +17,7 @@ export declare const PRESET_VERSION: "1.6";
|
|
|
17
17
|
* extend the union.
|
|
18
18
|
*/
|
|
19
19
|
export interface ResolveCompressOptionsInput {
|
|
20
|
-
readonly media:
|
|
20
|
+
readonly media: DetectedMedia;
|
|
21
21
|
readonly op: PresetOp;
|
|
22
22
|
/** Defaults registered via `gisl.create({ presetDefaults: ... })`. */
|
|
23
23
|
readonly presetDefaults?: PresetDefaults;
|
|
@@ -76,6 +76,28 @@ export interface ResolveCompressOptionsOutput {
|
|
|
76
76
|
*/
|
|
77
77
|
export declare function _parseTargetSize(value: unknown): number;
|
|
78
78
|
export declare const KNOWN_WIRE_FIELDS: Readonly<Record<PresetMedia, ReadonlySet<string>>>;
|
|
79
|
+
/**
|
|
80
|
+
* Compress options that are `availability: planned` per mime-group, mirroring the
|
|
81
|
+
* shipped `availability/availability.json`
|
|
82
|
+
* `operations.compress.mime_groups.<group>.options.<opt>.availability`.
|
|
83
|
+
*
|
|
84
|
+
* Kept as a hand table (NOT a runtime read of the ~238KB availability sidecar) for
|
|
85
|
+
* the same reasons as {@link IMAGE_OUTPUT_ROUTES}: the gate stays browser-safe, and
|
|
86
|
+
* — decisively — it has NO dependency on which `@giveitsmaller/contracts` version a
|
|
87
|
+
* consumer resolved. A generated-metadata read would FAIL OPEN on an older published
|
|
88
|
+
* contracts (the rtkzl9gr failure mode), and fail-open is the wrong direction for a
|
|
89
|
+
* gate whose entire job is to fail closed.
|
|
90
|
+
*
|
|
91
|
+
* PINNED to `availability.json` by `tests/unit/preset-planned-conformance.test.ts`,
|
|
92
|
+
* which fails closed in BOTH directions — a contract regen that marks a new option
|
|
93
|
+
* `planned`, or unmarks one, breaks the build rather than the caller. Mirrored by PHP
|
|
94
|
+
* `PresetResolver::PLANNED_COMPRESS_OPTIONS`.
|
|
95
|
+
*
|
|
96
|
+
* `video.speed` is listed for a faithful projection even though no shipped preset
|
|
97
|
+
* cell emits it; the conformance test pins the whole projection, not just the keys
|
|
98
|
+
* we happen to use today.
|
|
99
|
+
*/
|
|
100
|
+
export declare const PLANNED_COMPRESS_OPTIONS: Readonly<Record<PresetMedia, ReadonlySet<string>>>;
|
|
79
101
|
/**
|
|
80
102
|
* Resolve the wire payload + introspection projection for a compress
|
|
81
103
|
* operation call. Throws {@link GislConfigError} before any network
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
import { sha256Hex } from '../sha256.js';
|
|
38
38
|
import { GislConfigError } from '../errors.js';
|
|
39
39
|
import { PRESET_VERSION as GENERATED_PRESET_VERSION } from '../generated/sdk_spec/version.js';
|
|
40
|
-
import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions,
|
|
40
|
+
import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions, DocumentOfficeCompressPresetOptions, DocumentOdfCompressPresetOptions, DocumentEpubCompressPresetOptions, definedFieldsOf, } from './presets/index.js';
|
|
41
41
|
/**
|
|
42
42
|
* The preset matrix version emitted on every resolve. Re-exported from the
|
|
43
43
|
* GENERATED `sdk_spec/version.ts` (source of truth: contracts
|
|
@@ -175,8 +175,6 @@ function sdkDefaultRecord(media, op, optimize) {
|
|
|
175
175
|
return { ...AudioCompressPresetOptions.shippedDefaultsFor(optimize) };
|
|
176
176
|
case 'video':
|
|
177
177
|
return { ...VideoCompressPresetOptions.shippedDefaultsFor(optimize) };
|
|
178
|
-
case 'document_pdf':
|
|
179
|
-
return { ...DocumentPdfCompressPresetOptions.shippedDefaultsFor(optimize) };
|
|
180
178
|
case 'document_office':
|
|
181
179
|
return { ...DocumentOfficeCompressPresetOptions.shippedDefaultsFor(optimize) };
|
|
182
180
|
case 'document_odf':
|
|
@@ -213,9 +211,6 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
|
|
|
213
211
|
case 'video':
|
|
214
212
|
cell = defaults.cellFor('video', 'compress', optimize);
|
|
215
213
|
break;
|
|
216
|
-
case 'document_pdf':
|
|
217
|
-
cell = defaults.cellFor('document_pdf', 'compress', optimize);
|
|
218
|
-
break;
|
|
219
214
|
case 'document_office':
|
|
220
215
|
cell = defaults.cellFor('document_office', 'compress', optimize);
|
|
221
216
|
break;
|
|
@@ -252,11 +247,24 @@ const MEDIA_FIELDS = Object.freeze({
|
|
|
252
247
|
image: new Set(['quality', 'metadata', 'outputFormat']),
|
|
253
248
|
audio: new Set(['bitrate', 'channels', 'sampleRate', 'normalize']),
|
|
254
249
|
video: new Set(['codec', 'targetSize', 'crf', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audioCodec', 'audioBitrate']),
|
|
255
|
-
document_pdf: new Set(['profile', 'grayscale']),
|
|
256
250
|
document_office: new Set(['stripMacros', 'stripHiddenData', 'stripUnusedFonts']),
|
|
257
251
|
document_odf: new Set(['stripMetadata', 'stripUnusedStyles']),
|
|
258
252
|
document_epub: new Set(['fontSubsetting', 'stripUnusedCss']),
|
|
259
253
|
});
|
|
254
|
+
const OUT = (key) => ({ verb: 'output', key });
|
|
255
|
+
const CROSS_VERB_OVERRIDES = Object.freeze({
|
|
256
|
+
image: Object.freeze({
|
|
257
|
+
width: OUT('width'), height: OUT('height'), fit: OUT('fit'),
|
|
258
|
+
autoOrient: OUT('auto_orient'), colorProfile: OUT('color_profile'),
|
|
259
|
+
progressive: OUT('progressive'), lossless: OUT('lossless'),
|
|
260
|
+
qualityPreset: OUT('quality_preset'), encodingMode: OUT('encoding_mode'),
|
|
261
|
+
targetSizeBytes: OUT('target_size_bytes'), chromaSubsampling: OUT('chroma_subsampling'),
|
|
262
|
+
optimizationLevel: OUT('optimization_level'), avifSpeed: OUT('avif_speed'),
|
|
263
|
+
}),
|
|
264
|
+
// convert() takes the target format positionally, not as an option.
|
|
265
|
+
audio: Object.freeze({ outputFormat: { verb: 'convert', key: null } }),
|
|
266
|
+
video: Object.freeze({ outputFormat: { verb: 'convert', key: null } }),
|
|
267
|
+
});
|
|
260
268
|
function detectMismatchedOverrides(media, overrides) {
|
|
261
269
|
const expected = MEDIA_FIELDS[media];
|
|
262
270
|
const keys = Object.keys(overrides);
|
|
@@ -270,6 +278,37 @@ function detectMismatchedOverrides(media, overrides) {
|
|
|
270
278
|
const unknownFields = keys.filter((k) => !expected.has(k));
|
|
271
279
|
if (unknownFields.length === 0)
|
|
272
280
|
return;
|
|
281
|
+
// Options that are real for THIS media but belong to another verb are
|
|
282
|
+
// answered with that verb, before the other-media guess below — otherwise a
|
|
283
|
+
// key both this media and another one recognises (image `width` vs video
|
|
284
|
+
// `width`) gets blamed on the wrong media.
|
|
285
|
+
// Classified PER FIELD, not all-or-nothing: `{ width, codec }` on an image
|
|
286
|
+
// must still tell the caller that `width` is a legal resize on output(),
|
|
287
|
+
// rather than reverting to "you passed video options" for the pair.
|
|
288
|
+
const crossVerb = CROSS_VERB_OVERRIDES[media];
|
|
289
|
+
const crossVerbFields = crossVerb === undefined ? [] : unknownFields.filter((k) => crossVerb[k] !== undefined);
|
|
290
|
+
if (crossVerbFields.length > 0 && crossVerb !== undefined) {
|
|
291
|
+
const strays = unknownFields.filter((k) => crossVerb[k] === undefined);
|
|
292
|
+
const byVerb = new Map();
|
|
293
|
+
for (const k of crossVerbFields) {
|
|
294
|
+
const target = crossVerb[k];
|
|
295
|
+
const list = byVerb.get(target.verb) ?? [];
|
|
296
|
+
list.push(target.key ?? k);
|
|
297
|
+
byVerb.set(target.verb, list);
|
|
298
|
+
}
|
|
299
|
+
const clauses = [...byVerb.entries()].map(([verb, keys]) => verb === 'convert'
|
|
300
|
+
? `${keys.join(', ')} is the format argument of convert()`
|
|
301
|
+
: `${keys.join(', ')} ${keys.length === 1 ? 'is an option' : 'are options'} on ${verb}()`);
|
|
302
|
+
const outputKeys = byVerb.get('output');
|
|
303
|
+
throw new GislConfigError(`presetOverrides for '${media}' contained ${crossVerbFields.join(', ')}, which ${crossVerbFields.length === 1 ? 'does' : 'do'} not belong on the compress preset surface: ${clauses.join('; ')}.` +
|
|
304
|
+
(strays.length > 0 ? ` Also unrecognised for '${media}': ${strays.join(', ')}.` : ''), {
|
|
305
|
+
reason: 'type_mismatch',
|
|
306
|
+
conflictingFields: unknownFields,
|
|
307
|
+
suggestion: outputKeys !== undefined
|
|
308
|
+
? `Move ${outputKeys.join(', ')} to output(): .output(format, { ${outputKeys[0]}: … }).`
|
|
309
|
+
: 'Use convert(format) to change the output format.',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
273
312
|
// Look up which OTHER media owns every unknown field — if a single
|
|
274
313
|
// OTHER media's field set covers them all, that's a type_mismatch.
|
|
275
314
|
for (const otherMedia of Object.keys(MEDIA_FIELDS)) {
|
|
@@ -278,9 +317,9 @@ function detectMismatchedOverrides(media, overrides) {
|
|
|
278
317
|
const otherSet = MEDIA_FIELDS[otherMedia];
|
|
279
318
|
if (unknownFields.every((k) => otherSet.has(k))) {
|
|
280
319
|
// PascalCase every underscore-separated segment so multi-segment
|
|
281
|
-
// media (`
|
|
320
|
+
// media (`document_office` → `DocumentOffice…`) emit the actual exported
|
|
282
321
|
// class name (code-review MEDIUM: previously emitted
|
|
283
|
-
// `
|
|
322
|
+
// `Documentoffice…` which doesn't resolve in user code).
|
|
284
323
|
const className = otherMedia
|
|
285
324
|
.split('_')
|
|
286
325
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
@@ -321,11 +360,39 @@ export const KNOWN_WIRE_FIELDS = Object.freeze({
|
|
|
321
360
|
image: new Set(['quality', 'metadata', 'output_format']),
|
|
322
361
|
audio: new Set(['bitrate', 'channels', 'sample_rate', 'normalize', 'trim_start', 'trim_end']),
|
|
323
362
|
video: new Set(['codec', 'encoding_mode', 'crf', 'target_size_bytes', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audio_codec', 'audio_bitrate', 'trim_start', 'trim_end']),
|
|
324
|
-
document_pdf: new Set(['profile', 'grayscale']),
|
|
325
363
|
document_office: new Set(['strip_macros', 'strip_hidden_data', 'strip_unused_fonts']),
|
|
326
364
|
document_odf: new Set(['strip_metadata', 'strip_unused_styles']),
|
|
327
365
|
document_epub: new Set(['font_subsetting', 'strip_unused_css']),
|
|
328
366
|
});
|
|
367
|
+
/**
|
|
368
|
+
* Compress options that are `availability: planned` per mime-group, mirroring the
|
|
369
|
+
* shipped `availability/availability.json`
|
|
370
|
+
* `operations.compress.mime_groups.<group>.options.<opt>.availability`.
|
|
371
|
+
*
|
|
372
|
+
* Kept as a hand table (NOT a runtime read of the ~238KB availability sidecar) for
|
|
373
|
+
* the same reasons as {@link IMAGE_OUTPUT_ROUTES}: the gate stays browser-safe, and
|
|
374
|
+
* — decisively — it has NO dependency on which `@giveitsmaller/contracts` version a
|
|
375
|
+
* consumer resolved. A generated-metadata read would FAIL OPEN on an older published
|
|
376
|
+
* contracts (the rtkzl9gr failure mode), and fail-open is the wrong direction for a
|
|
377
|
+
* gate whose entire job is to fail closed.
|
|
378
|
+
*
|
|
379
|
+
* PINNED to `availability.json` by `tests/unit/preset-planned-conformance.test.ts`,
|
|
380
|
+
* which fails closed in BOTH directions — a contract regen that marks a new option
|
|
381
|
+
* `planned`, or unmarks one, breaks the build rather than the caller. Mirrored by PHP
|
|
382
|
+
* `PresetResolver::PLANNED_COMPRESS_OPTIONS`.
|
|
383
|
+
*
|
|
384
|
+
* `video.speed` is listed for a faithful projection even though no shipped preset
|
|
385
|
+
* cell emits it; the conformance test pins the whole projection, not just the keys
|
|
386
|
+
* we happen to use today.
|
|
387
|
+
*/
|
|
388
|
+
export const PLANNED_COMPRESS_OPTIONS = Object.freeze({
|
|
389
|
+
image: new Set([]),
|
|
390
|
+
audio: new Set([]),
|
|
391
|
+
video: new Set(['speed']),
|
|
392
|
+
document_office: new Set(['strip_hidden_data', 'strip_macros', 'strip_unused_fonts']),
|
|
393
|
+
document_odf: new Set(['strip_metadata', 'strip_unused_styles']),
|
|
394
|
+
document_epub: new Set(['font_subsetting', 'strip_unused_css']),
|
|
395
|
+
});
|
|
329
396
|
function validateMerged(media, merged, explicitKeys, winners) {
|
|
330
397
|
// Unknown-field defence-in-depth: every key must belong to the
|
|
331
398
|
// media's wire surface OR be one of the resolver-derived wire keys
|
|
@@ -444,6 +511,9 @@ function computePresetConfigHash(clientDefault, scopedDefault, callPresetOverrid
|
|
|
444
511
|
* `optimize` unset ⇒ layer 1 contributes nothing; `resolvedOptions.preset = null`.
|
|
445
512
|
*/
|
|
446
513
|
export function resolveCompressOptions(input) {
|
|
514
|
+
if (input.media === 'document_pdf') {
|
|
515
|
+
throw new GislConfigError('PDF compression was removed at contracts v2.166.0; convert() / transform() still accept PDF.', { reason: 'unsupported_media' });
|
|
516
|
+
}
|
|
447
517
|
const { media, op, presetDefaults, scopedPresetDefaults, presetOverrides, optimize, explicitOptions, audioLossless } = input;
|
|
448
518
|
if (op !== 'compress') {
|
|
449
519
|
throw new GislConfigError(`Preset resolution is only wired for compress operations today; got op='${op}'.`, { reason: 'unsupported_op' });
|
|
@@ -531,6 +601,26 @@ export function resolveCompressOptions(input) {
|
|
|
531
601
|
delete acc.merged.bitrate;
|
|
532
602
|
acc.winners.delete('bitrate');
|
|
533
603
|
}
|
|
604
|
+
// A shipped preset must never put an `availability: planned` option on the wire.
|
|
605
|
+
// The API rejects a planned option when the KEY IS PRESENT and ignores it when
|
|
606
|
+
// absent (`CreateWorkflowCommandHandler::recordPlannedFromMap` — a materialized
|
|
607
|
+
// default deliberately does NOT trigger it), so a value WE synthesized becomes a
|
|
608
|
+
// 422 `feature_not_available` at create that the caller never asked for. That is
|
|
609
|
+
// what broke every document compress with an `optimizeFor` in 0.21.0 (5Eksm9s7).
|
|
610
|
+
//
|
|
611
|
+
// Drop ONLY the sdkDefault-sourced ones. A planned option from any other layer is
|
|
612
|
+
// a caller choice — `clientDefault` and `scopedDefault` are user-registered via
|
|
613
|
+
// `gisl.create({ presetDefaults })` / `client.withPresetDefaults(...)`, not baked
|
|
614
|
+
// in by us — and is left in place for the server to refuse honestly. Same rule and
|
|
615
|
+
// same reason as the lossless-bitrate drop above: never silently swallow a key the
|
|
616
|
+
// caller chose. Do NOT "simplify" either of these into an always-drop; a silent
|
|
617
|
+
// no-op on an explicit request is worse than an honest 422.
|
|
618
|
+
for (const key of PLANNED_COMPRESS_OPTIONS[media]) {
|
|
619
|
+
if (acc.winners.get(key) === 'sdkDefault') {
|
|
620
|
+
delete acc.merged[key];
|
|
621
|
+
acc.winners.delete(key);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
534
624
|
// 7. Validate the merged payload (post-merge — catches cross-layer
|
|
535
625
|
// disagreements). May throw GislConfigError with resolvedSnapshot.
|
|
536
626
|
const explicitWireKeys = new Set();
|
|
@@ -12,10 +12,22 @@
|
|
|
12
12
|
// `mode` + `iccProfile` were REMOVED — the worker is lossy-only and always
|
|
13
13
|
// strips metadata, so advertising a lossless mode or ICC-profile policy was
|
|
14
14
|
// an over-claim. `progressive` is still a per-JPEG wire option but is no
|
|
15
|
-
// longer carried in the preset cell.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// longer carried in the preset cell.
|
|
16
|
+
//
|
|
17
|
+
// `width`/`height`/`fit`/`autoOrient` were removed earlier on the grounds that
|
|
18
|
+
// "the image-compress worker never resized". CAREFUL — that is still true of
|
|
19
|
+
// the Rust optimiser crate and NOT true end-to-end. Since contract v2.97.0
|
|
20
|
+
// ("resize lives inside Output") the API canonicalises an image compress
|
|
21
|
+
// carrying width/height/fit into a `convert` op, and convert IS the resize
|
|
22
|
+
// engine, so such a request returns a genuinely resized file. Their absence
|
|
23
|
+
// here is therefore a SURFACE choice, not a capability limit: resize is
|
|
24
|
+
// expressed via `output()` (see `OutputOptions`), which the compress
|
|
25
|
+
// conformance gate records as CROSS_VERB_ROUTING and self-verifies. Reading
|
|
26
|
+
// this comment as "unsupported" is what produced cySAEZHR. Whether compress()
|
|
27
|
+
// should ALSO carry them is an open ergonomic-expansion decision, not a bug.
|
|
28
|
+
//
|
|
29
|
+
// Per-call knobs are deliberately excluded — they belong on the per-call
|
|
30
|
+
// argument shape.
|
|
19
31
|
import { shippedDefaultsFor as f3ShippedDefaultsFor } from '../../generated/sdk_spec/presets.js';
|
|
20
32
|
import { translateEnum } from './_translate.js';
|
|
21
33
|
export class ImageCompressPresetOptions {
|
|
@@ -2,27 +2,31 @@ import { OptimizeFor } from '../../generated/sdk_spec/enums.js';
|
|
|
2
2
|
import { ImageCompressPresetOptions, type ImageCompressPresetOptionsInput } from './image_compress.js';
|
|
3
3
|
import { AudioCompressPresetOptions, type AudioCompressPresetOptionsInput } from './audio_compress.js';
|
|
4
4
|
import { VideoCompressPresetOptions, type VideoCompressPresetOptionsInput } from './video_compress.js';
|
|
5
|
-
import { DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptionsInput } from './document_pdf_compress.js';
|
|
6
5
|
import { DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput } from './document_office_compress.js';
|
|
7
6
|
import { DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput } from './document_odf_compress.js';
|
|
8
7
|
import { DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput } from './document_epub_compress.js';
|
|
9
8
|
export { ImageCompressPresetOptions, type ImageCompressPresetOptionsInput, } from './image_compress.js';
|
|
10
9
|
export { AudioCompressPresetOptions, type AudioCompressPresetOptionsInput, } from './audio_compress.js';
|
|
11
10
|
export { VideoCompressPresetOptions, type VideoCompressPresetOptionsInput, } from './video_compress.js';
|
|
12
|
-
export { DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptionsInput, } from './document_pdf_compress.js';
|
|
13
11
|
export { DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput, } from './document_office_compress.js';
|
|
14
12
|
export { DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput, } from './document_odf_compress.js';
|
|
15
13
|
export { DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput, } from './document_epub_compress.js';
|
|
16
|
-
export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate,
|
|
14
|
+
export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, } from '../../generated/sdk_spec/enums.js';
|
|
17
15
|
/** Supported media×op pairs for preset cells in T4a. Compress-only. */
|
|
18
|
-
export type PresetMedia = 'image' | 'audio' | 'video' | '
|
|
16
|
+
export type PresetMedia = 'image' | 'audio' | 'video' | 'document_office' | 'document_odf' | 'document_epub';
|
|
17
|
+
/**
|
|
18
|
+
* Media the file-first detector can identify — the compressible
|
|
19
|
+
* `PresetMedia` set PLUS `document_pdf`, which is detectable (and a valid
|
|
20
|
+
* watermark-reject / convert / transform base) but NOT compressible.
|
|
21
|
+
*/
|
|
22
|
+
export type DetectedMedia = PresetMedia | 'document_pdf';
|
|
19
23
|
export type PresetOp = 'compress';
|
|
20
24
|
/**
|
|
21
25
|
* Union of leaf-DTO types the resolver will see from `cellFor()`.
|
|
22
26
|
* Discriminated by which `media` the caller passes — the type system
|
|
23
27
|
* narrows the return automatically via the overload set below.
|
|
24
28
|
*/
|
|
25
|
-
export type AnyPresetOptions = ImageCompressPresetOptions | AudioCompressPresetOptions | VideoCompressPresetOptions |
|
|
29
|
+
export type AnyPresetOptions = ImageCompressPresetOptions | AudioCompressPresetOptions | VideoCompressPresetOptions | DocumentOfficeCompressPresetOptions | DocumentOdfCompressPresetOptions | DocumentEpubCompressPresetOptions;
|
|
26
30
|
/**
|
|
27
31
|
* Per-cell field-merge: parent fields ⊕ child fields where defined.
|
|
28
32
|
* Re-construct the leaf DTO via the matching `<LeafClass>.from(merged)`
|
|
@@ -73,8 +77,6 @@ export declare class PresetDefaults {
|
|
|
73
77
|
audioCompress(level: OptimizeFor, input?: AudioCompressPresetOptionsInput): PresetDefaults;
|
|
74
78
|
/** Register a (level, delta) on the video-compress cell. Immutable. */
|
|
75
79
|
videoCompress(level: OptimizeFor, input?: VideoCompressPresetOptionsInput): PresetDefaults;
|
|
76
|
-
/** Register a (level, delta) on the document-pdf-compress cell. Immutable. */
|
|
77
|
-
pdfCompress(level: OptimizeFor, input?: DocumentPdfCompressPresetOptionsInput): PresetDefaults;
|
|
78
80
|
/** Register a (level, delta) on the document-office-compress cell. Immutable. */
|
|
79
81
|
officeCompress(level: OptimizeFor, input?: DocumentOfficeCompressPresetOptionsInput): PresetDefaults;
|
|
80
82
|
/** Register a (level, delta) on the document-odf-compress cell. Immutable. */
|
|
@@ -84,7 +86,6 @@ export declare class PresetDefaults {
|
|
|
84
86
|
/** @internal */ cellFor(media: 'image', op: 'compress', level: OptimizeFor): ImageCompressPresetOptions | undefined;
|
|
85
87
|
/** @internal */ cellFor(media: 'audio', op: 'compress', level: OptimizeFor): AudioCompressPresetOptions | undefined;
|
|
86
88
|
/** @internal */ cellFor(media: 'video', op: 'compress', level: OptimizeFor): VideoCompressPresetOptions | undefined;
|
|
87
|
-
/** @internal */ cellFor(media: 'document_pdf', op: 'compress', level: OptimizeFor): DocumentPdfCompressPresetOptions | undefined;
|
|
88
89
|
/** @internal */ cellFor(media: 'document_office', op: 'compress', level: OptimizeFor): DocumentOfficeCompressPresetOptions | undefined;
|
|
89
90
|
/** @internal */ cellFor(media: 'document_odf', op: 'compress', level: OptimizeFor): DocumentOdfCompressPresetOptions | undefined;
|
|
90
91
|
/** @internal */ cellFor(media: 'document_epub', op: 'compress', level: OptimizeFor): DocumentEpubCompressPresetOptions | undefined;
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
import { ImageCompressPresetOptions, } from './image_compress.js';
|
|
29
29
|
import { AudioCompressPresetOptions, } from './audio_compress.js';
|
|
30
30
|
import { VideoCompressPresetOptions, } from './video_compress.js';
|
|
31
|
-
import { DocumentPdfCompressPresetOptions, } from './document_pdf_compress.js';
|
|
32
31
|
import { DocumentOfficeCompressPresetOptions, } from './document_office_compress.js';
|
|
33
32
|
import { DocumentOdfCompressPresetOptions, } from './document_odf_compress.js';
|
|
34
33
|
import { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js';
|
|
@@ -36,12 +35,11 @@ import { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js'
|
|
|
36
35
|
export { ImageCompressPresetOptions, } from './image_compress.js';
|
|
37
36
|
export { AudioCompressPresetOptions, } from './audio_compress.js';
|
|
38
37
|
export { VideoCompressPresetOptions, } from './video_compress.js';
|
|
39
|
-
export { DocumentPdfCompressPresetOptions, } from './document_pdf_compress.js';
|
|
40
38
|
export { DocumentOfficeCompressPresetOptions, } from './document_office_compress.js';
|
|
41
39
|
export { DocumentOdfCompressPresetOptions, } from './document_odf_compress.js';
|
|
42
40
|
export { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js';
|
|
43
41
|
// Re-export ergonomic enums for callers (single canonical path).
|
|
44
|
-
export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate,
|
|
42
|
+
export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, } from '../../generated/sdk_spec/enums.js';
|
|
45
43
|
function cellKeyOf(media, op) {
|
|
46
44
|
return `${media}_${op}`;
|
|
47
45
|
}
|
|
@@ -89,8 +87,6 @@ function mergePresetOptions(cellKey, parentOpts, childOpts) {
|
|
|
89
87
|
return AudioCompressPresetOptions.from(mergedFields);
|
|
90
88
|
case 'video_compress':
|
|
91
89
|
return VideoCompressPresetOptions.from(mergedFields);
|
|
92
|
-
case 'document_pdf_compress':
|
|
93
|
-
return DocumentPdfCompressPresetOptions.from(mergedFields);
|
|
94
90
|
case 'document_office_compress':
|
|
95
91
|
return DocumentOfficeCompressPresetOptions.from(mergedFields);
|
|
96
92
|
case 'document_odf_compress':
|
|
@@ -178,10 +174,6 @@ export class PresetDefaults {
|
|
|
178
174
|
videoCompress(level, input = {}) {
|
|
179
175
|
return new PresetDefaults(withCellEntry(this.cells, 'video_compress', level, VideoCompressPresetOptions.from(input)));
|
|
180
176
|
}
|
|
181
|
-
/** Register a (level, delta) on the document-pdf-compress cell. Immutable. */
|
|
182
|
-
pdfCompress(level, input = {}) {
|
|
183
|
-
return new PresetDefaults(withCellEntry(this.cells, 'document_pdf_compress', level, DocumentPdfCompressPresetOptions.from(input)));
|
|
184
|
-
}
|
|
185
177
|
/** Register a (level, delta) on the document-office-compress cell. Immutable. */
|
|
186
178
|
officeCompress(level, input = {}) {
|
|
187
179
|
return new PresetDefaults(withCellEntry(this.cells, 'document_office_compress', level, DocumentOfficeCompressPresetOptions.from(input)));
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { VideoCodec, VideoPreset, VideoFit, AudioCodec, AudioBitrate, OptimizeFor } from '../../generated/sdk_spec/enums.js';
|
|
2
2
|
export interface VideoCompressPresetOptionsInput {
|
|
3
3
|
readonly codec?: VideoCodec;
|
|
4
|
+
/**
|
|
5
|
+
* Target output size (`'50MB'`-style string — BINARY units, 1 KB = 1024 — or a byte
|
|
6
|
+
* count). Derived by the resolver into `target_size_bytes` + `encoding_mode:
|
|
7
|
+
* 'target_size'`.
|
|
8
|
+
*
|
|
9
|
+
* **NOT AVAILABLE FOR LONG INPUTS.** A compress whose input duration routes to the
|
|
10
|
+
* long-form path rejects it (`reject_long_form_target_size`): that path is
|
|
11
|
+
* single-pass-CRF by construction and two-pass target-size is unbuilt. The request
|
|
12
|
+
* fails during execution, and the SDK cannot warn earlier — routing is decided
|
|
13
|
+
* server-side at create-plan time, so there is nothing here to check it against.
|
|
14
|
+
* Short-form compresses honour it normally. Tracked by `zJN6XIi5`, blocked on a
|
|
15
|
+
* contract that can express per-execution-path availability. The same limit applies
|
|
16
|
+
* to {@link MergeOptions.targetSize}.
|
|
17
|
+
*/
|
|
4
18
|
readonly targetSize?: string | number;
|
|
5
19
|
readonly crf?: number;
|
|
6
20
|
readonly preset?: VideoPreset;
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthErrorResponse, AuthRejectionEnvelope, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponse, FeatureNotAvailableResponse, FeatureTierRestrictedResponse, ProbePendingResponse, TierRestrictionResponse, UploadDurationExceedsTierResponse, UploadSizeExceedsTierResponse, WorkflowExpiredResponse } from '@giveitsmaller/contracts/openapi';
|
|
1
|
+
import type { AuthErrorResponse, AuthRejectionEnvelope, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponse, FeatureNotAvailableResponse, FeatureTierRestrictedResponse, LongFormConcurrencyLimitResponse, ProbePendingResponse, TierRestrictionResponse, UploadDurationExceedsTierResponse, UploadSizeExceedsTierResponse, WorkflowExpiredResponse } from '@giveitsmaller/contracts/openapi';
|
|
2
2
|
import type { ErrorCategory } from './generated/sdk_spec/errors.js';
|
|
3
3
|
import type { RateLimitSnapshot } from './retry-metadata.js';
|
|
4
4
|
export declare class GislError extends Error {
|
|
@@ -134,6 +134,49 @@ export declare class GislBalanceExhaustedError extends GislApiError {
|
|
|
134
134
|
readonly payload: BalanceExhaustedResponse;
|
|
135
135
|
constructor(statusCode: number, errorMessage: string, payload: BalanceExhaustedResponse, path?: string, extra?: Omit<GislApiErrorOptions, 'payload'>);
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* `429` on `POST /api/workflows` when the caller already holds the maximum
|
|
139
|
+
* number of concurrent in-flight long-form (Fargate) workflows their tier
|
|
140
|
+
* permits (Pro 2 / Max 5; Enterprise uncapped). DISTINCT from an infrastructure
|
|
141
|
+
* rate-limit `429`: it carries the machine code `LONG_FORM_CONCURRENCY_LIMIT_EXCEEDED`
|
|
142
|
+
* and a `links.upgrade` deep link, and has **no `Retry-After`** — the limit clears
|
|
143
|
+
* when an in-flight long-form workflow finishes, not on a timer. A generic infra
|
|
144
|
+
* rate-limit `429` (no matching code) surfaces as the base {@link GislApiError}
|
|
145
|
+
* instead, where {@link GislApiError.retryAfterSeconds} applies.
|
|
146
|
+
*
|
|
147
|
+
* Dispatched on the `error` CODE, not `error_type` (the envelope carries none).
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* try {
|
|
151
|
+
* await client.createWorkflow({ jobs });
|
|
152
|
+
* } catch (e) {
|
|
153
|
+
* if (e instanceof GislLongFormConcurrencyError) {
|
|
154
|
+
* showUpgradeCta(e.upgradeUrl); // wait on completion or upgrade — do NOT back off
|
|
155
|
+
* }
|
|
156
|
+
* throw e;
|
|
157
|
+
* }
|
|
158
|
+
*/
|
|
159
|
+
export declare class GislLongFormConcurrencyError extends GislApiError {
|
|
160
|
+
readonly payload: LongFormConcurrencyLimitResponse;
|
|
161
|
+
constructor(statusCode: number, errorMessage: string, payload: LongFormConcurrencyLimitResponse, path?: string, extra?: Omit<GislApiErrorOptions, 'payload'>);
|
|
162
|
+
/**
|
|
163
|
+
* ALWAYS `false`, overriding the base 429-implies-retryable heuristic
|
|
164
|
+
* (UO1xYecu). This 429 is not a rate limit: it carries no `Retry-After` and
|
|
165
|
+
* clears only when an in-flight long-form workflow finishes, so a back-off
|
|
166
|
+
* retries into a wall that no amount of waiting-then-retrying opens. The base
|
|
167
|
+
* accessor reported `true` purely from the status, contradicting this class's
|
|
168
|
+
* own documented handling ("wait on completion or upgrade — do NOT back off")
|
|
169
|
+
* and instructing the one recovery that cannot work.
|
|
170
|
+
*
|
|
171
|
+
* Overridden per-class rather than via a code table because this is the only
|
|
172
|
+
* such code today; the general fix — an explicit taxonomy verdict outranking
|
|
173
|
+
* the status heuristic — arrives with the `error-taxonomy.yaml` `retryable`
|
|
174
|
+
* enum (contracts `plwcAqBr`), tracked on UO1xYecu.
|
|
175
|
+
*/
|
|
176
|
+
get retryable(): boolean;
|
|
177
|
+
/** The pricing / upgrade deep link (`links.upgrade`), or `undefined` when absent. */
|
|
178
|
+
get upgradeUrl(): string | undefined;
|
|
179
|
+
}
|
|
137
180
|
export declare class GislTierRestrictedError extends GislApiError {
|
|
138
181
|
readonly payload: TierRestrictionResponse;
|
|
139
182
|
constructor(statusCode: number, errorMessage: string, payload: TierRestrictionResponse, path?: string, extra?: Omit<GislApiErrorOptions, 'payload'>);
|
|
@@ -411,7 +454,63 @@ export declare class GislBundleAlreadyArchivedError extends GislConfigError {
|
|
|
411
454
|
constructor();
|
|
412
455
|
}
|
|
413
456
|
export declare class GislTimeoutError extends GislError {
|
|
414
|
-
|
|
457
|
+
/**
|
|
458
|
+
* The workflow this timeout is scoped to, when the SDK knows it. Set on a
|
|
459
|
+
* timed-out `run()` / `wait()` / poll / download once the workflow has been
|
|
460
|
+
* created: a timeout does NOT mean the work failed — the server keeps
|
|
461
|
+
* processing, so poll `client.getWorkflowStatus(workflowId)` /
|
|
462
|
+
* `getWorkflowDownloads(workflowId)` to recover a result that completed after
|
|
463
|
+
* the deadline, instead of re-running (a re-run re-uploads and, for
|
|
464
|
+
* authenticated callers, settles a SECOND charge for the same deliverable).
|
|
465
|
+
*
|
|
466
|
+
* `undefined` when the SDK has no id to offer. That is NOT a guarantee that
|
|
467
|
+
* nothing was created or charged: it covers both the safe case (an upload /
|
|
468
|
+
* probe timeout before any workflow existed) AND the AMBIGUOUS case (the
|
|
469
|
+
* `POST /api/workflows` request itself timed out — the server may have
|
|
470
|
+
* created and charged the workflow before its response was lost). Treat an
|
|
471
|
+
* absent id as "cannot auto-recover", not "clean slate": reconcile (e.g. list
|
|
472
|
+
* recent workflows) before re-running rather than assuming nothing happened.
|
|
473
|
+
*/
|
|
474
|
+
readonly workflowId?: string;
|
|
475
|
+
constructor(message: string, workflowId?: string);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* A `mapEach` fan-out timed out mid-batch — the deadline elapsed either while a
|
|
479
|
+
* child was still running (the common case) or cleanly between child runs. The
|
|
480
|
+
* parent and some children have ALREADY completed, so re-running the whole batch
|
|
481
|
+
* re-does finished work. This carries their ids so the caller can poll them (via
|
|
482
|
+
* `client.getWorkflowStatus` / `getWorkflowDownloads`) to recover the finished
|
|
483
|
+
* work and re-run ONLY the children that were never created.
|
|
484
|
+
*
|
|
485
|
+
* Subclasses {@link GislTimeoutError}, so an existing
|
|
486
|
+
* `catch (e) { if (e instanceof GislTimeoutError) … }` still catches it. The
|
|
487
|
+
* inherited `workflowId` carries the IN-FLIGHT child — the one that was running
|
|
488
|
+
* when the deadline elapsed (a child's own timeout, the common path) — or stays
|
|
489
|
+
* `undefined` when the deadline elapsed cleanly BETWEEN children (no in-flight
|
|
490
|
+
* child). To recover, poll `workflowId` (if set) + {@link parentWorkflowId} +
|
|
491
|
+
* {@link completedWorkflowIds}, then re-run only the children that never started.
|
|
492
|
+
*
|
|
493
|
+
* NOTE on double-charge: the server-side create-dedupe (DSxwCetg) is what
|
|
494
|
+
* prevents a byte-identical child re-create from settling a SECOND charge within
|
|
495
|
+
* the dedup window; this error's job is efficient RECOVERY (skip the completed
|
|
496
|
+
* work) + defense-in-depth, not the sole charge guard.
|
|
497
|
+
*/
|
|
498
|
+
export declare class GislFanOutTimeoutError extends GislTimeoutError {
|
|
499
|
+
/** The child workflows that completed before the deadline elapsed. */
|
|
500
|
+
readonly completedWorkflowIds: readonly string[];
|
|
501
|
+
/** The parent workflow, which ran to completion before the fan-out began. */
|
|
502
|
+
readonly parentWorkflowId?: string;
|
|
503
|
+
constructor(message: string, opts: {
|
|
504
|
+
completedWorkflowIds: readonly string[];
|
|
505
|
+
parentWorkflowId?: string;
|
|
506
|
+
/**
|
|
507
|
+
* The in-flight child that timed out mid-run (its own deadline elapsed);
|
|
508
|
+
* `undefined` for a clean between-children timeout with no child running.
|
|
509
|
+
*/
|
|
510
|
+
workflowId?: string;
|
|
511
|
+
/** The underlying child {@link GislTimeoutError}, preserved for chaining. */
|
|
512
|
+
cause?: unknown;
|
|
513
|
+
});
|
|
415
514
|
}
|
|
416
515
|
/**
|
|
417
516
|
* Transport-level failure: the underlying `fetch` (or other transport) could
|
package/dist/errors.js
CHANGED
|
@@ -145,6 +145,55 @@ export class GislBalanceExhaustedError extends GislApiError {
|
|
|
145
145
|
this.name = 'GislBalanceExhaustedError';
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* `429` on `POST /api/workflows` when the caller already holds the maximum
|
|
150
|
+
* number of concurrent in-flight long-form (Fargate) workflows their tier
|
|
151
|
+
* permits (Pro 2 / Max 5; Enterprise uncapped). DISTINCT from an infrastructure
|
|
152
|
+
* rate-limit `429`: it carries the machine code `LONG_FORM_CONCURRENCY_LIMIT_EXCEEDED`
|
|
153
|
+
* and a `links.upgrade` deep link, and has **no `Retry-After`** — the limit clears
|
|
154
|
+
* when an in-flight long-form workflow finishes, not on a timer. A generic infra
|
|
155
|
+
* rate-limit `429` (no matching code) surfaces as the base {@link GislApiError}
|
|
156
|
+
* instead, where {@link GislApiError.retryAfterSeconds} applies.
|
|
157
|
+
*
|
|
158
|
+
* Dispatched on the `error` CODE, not `error_type` (the envelope carries none).
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* try {
|
|
162
|
+
* await client.createWorkflow({ jobs });
|
|
163
|
+
* } catch (e) {
|
|
164
|
+
* if (e instanceof GislLongFormConcurrencyError) {
|
|
165
|
+
* showUpgradeCta(e.upgradeUrl); // wait on completion or upgrade — do NOT back off
|
|
166
|
+
* }
|
|
167
|
+
* throw e;
|
|
168
|
+
* }
|
|
169
|
+
*/
|
|
170
|
+
export class GislLongFormConcurrencyError extends GislApiError {
|
|
171
|
+
constructor(statusCode, errorMessage, payload, path, extra) {
|
|
172
|
+
super(statusCode, errorMessage, path, undefined, buildOptionsWithPayload(payload, extra));
|
|
173
|
+
this.name = 'GislLongFormConcurrencyError';
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* ALWAYS `false`, overriding the base 429-implies-retryable heuristic
|
|
177
|
+
* (UO1xYecu). This 429 is not a rate limit: it carries no `Retry-After` and
|
|
178
|
+
* clears only when an in-flight long-form workflow finishes, so a back-off
|
|
179
|
+
* retries into a wall that no amount of waiting-then-retrying opens. The base
|
|
180
|
+
* accessor reported `true` purely from the status, contradicting this class's
|
|
181
|
+
* own documented handling ("wait on completion or upgrade — do NOT back off")
|
|
182
|
+
* and instructing the one recovery that cannot work.
|
|
183
|
+
*
|
|
184
|
+
* Overridden per-class rather than via a code table because this is the only
|
|
185
|
+
* such code today; the general fix — an explicit taxonomy verdict outranking
|
|
186
|
+
* the status heuristic — arrives with the `error-taxonomy.yaml` `retryable`
|
|
187
|
+
* enum (contracts `plwcAqBr`), tracked on UO1xYecu.
|
|
188
|
+
*/
|
|
189
|
+
get retryable() {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
/** The pricing / upgrade deep link (`links.upgrade`), or `undefined` when absent. */
|
|
193
|
+
get upgradeUrl() {
|
|
194
|
+
return this.payload.links?.upgrade;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
148
197
|
export class GislTierRestrictedError extends GislApiError {
|
|
149
198
|
constructor(statusCode, errorMessage, payload, path, extra) {
|
|
150
199
|
super(statusCode, errorMessage, path, undefined, buildOptionsWithPayload(payload, extra));
|
|
@@ -447,9 +496,67 @@ export class GislBundleAlreadyArchivedError extends GislConfigError {
|
|
|
447
496
|
}
|
|
448
497
|
}
|
|
449
498
|
export class GislTimeoutError extends GislError {
|
|
450
|
-
|
|
499
|
+
/**
|
|
500
|
+
* The workflow this timeout is scoped to, when the SDK knows it. Set on a
|
|
501
|
+
* timed-out `run()` / `wait()` / poll / download once the workflow has been
|
|
502
|
+
* created: a timeout does NOT mean the work failed — the server keeps
|
|
503
|
+
* processing, so poll `client.getWorkflowStatus(workflowId)` /
|
|
504
|
+
* `getWorkflowDownloads(workflowId)` to recover a result that completed after
|
|
505
|
+
* the deadline, instead of re-running (a re-run re-uploads and, for
|
|
506
|
+
* authenticated callers, settles a SECOND charge for the same deliverable).
|
|
507
|
+
*
|
|
508
|
+
* `undefined` when the SDK has no id to offer. That is NOT a guarantee that
|
|
509
|
+
* nothing was created or charged: it covers both the safe case (an upload /
|
|
510
|
+
* probe timeout before any workflow existed) AND the AMBIGUOUS case (the
|
|
511
|
+
* `POST /api/workflows` request itself timed out — the server may have
|
|
512
|
+
* created and charged the workflow before its response was lost). Treat an
|
|
513
|
+
* absent id as "cannot auto-recover", not "clean slate": reconcile (e.g. list
|
|
514
|
+
* recent workflows) before re-running rather than assuming nothing happened.
|
|
515
|
+
*/
|
|
516
|
+
workflowId;
|
|
517
|
+
constructor(message, workflowId) {
|
|
451
518
|
super(message);
|
|
452
519
|
this.name = 'GislTimeoutError';
|
|
520
|
+
// Normalise an empty id to "absent" — an empty string is not a usable
|
|
521
|
+
// recovery handle (some throw sites derive the id as `… ?? ''`).
|
|
522
|
+
this.workflowId = workflowId === '' ? undefined : workflowId;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* A `mapEach` fan-out timed out mid-batch — the deadline elapsed either while a
|
|
527
|
+
* child was still running (the common case) or cleanly between child runs. The
|
|
528
|
+
* parent and some children have ALREADY completed, so re-running the whole batch
|
|
529
|
+
* re-does finished work. This carries their ids so the caller can poll them (via
|
|
530
|
+
* `client.getWorkflowStatus` / `getWorkflowDownloads`) to recover the finished
|
|
531
|
+
* work and re-run ONLY the children that were never created.
|
|
532
|
+
*
|
|
533
|
+
* Subclasses {@link GislTimeoutError}, so an existing
|
|
534
|
+
* `catch (e) { if (e instanceof GislTimeoutError) … }` still catches it. The
|
|
535
|
+
* inherited `workflowId` carries the IN-FLIGHT child — the one that was running
|
|
536
|
+
* when the deadline elapsed (a child's own timeout, the common path) — or stays
|
|
537
|
+
* `undefined` when the deadline elapsed cleanly BETWEEN children (no in-flight
|
|
538
|
+
* child). To recover, poll `workflowId` (if set) + {@link parentWorkflowId} +
|
|
539
|
+
* {@link completedWorkflowIds}, then re-run only the children that never started.
|
|
540
|
+
*
|
|
541
|
+
* NOTE on double-charge: the server-side create-dedupe (DSxwCetg) is what
|
|
542
|
+
* prevents a byte-identical child re-create from settling a SECOND charge within
|
|
543
|
+
* the dedup window; this error's job is efficient RECOVERY (skip the completed
|
|
544
|
+
* work) + defense-in-depth, not the sole charge guard.
|
|
545
|
+
*/
|
|
546
|
+
export class GislFanOutTimeoutError extends GislTimeoutError {
|
|
547
|
+
/** The child workflows that completed before the deadline elapsed. */
|
|
548
|
+
completedWorkflowIds;
|
|
549
|
+
/** The parent workflow, which ran to completion before the fan-out began. */
|
|
550
|
+
parentWorkflowId;
|
|
551
|
+
constructor(message, opts) {
|
|
552
|
+
// The inherited workflowId is the in-flight child (or undefined between children).
|
|
553
|
+
super(message, opts.workflowId);
|
|
554
|
+
this.name = 'GislFanOutTimeoutError';
|
|
555
|
+
this.completedWorkflowIds = [...opts.completedWorkflowIds];
|
|
556
|
+
this.parentWorkflowId = opts.parentWorkflowId === '' ? undefined : opts.parentWorkflowId;
|
|
557
|
+
if (opts.cause !== undefined) {
|
|
558
|
+
this.cause = opts.cause;
|
|
559
|
+
}
|
|
453
560
|
}
|
|
454
561
|
}
|
|
455
562
|
/**
|