@giveitsmaller/sdk 0.8.0 → 0.10.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/README.md +8 -0
- package/dist/_audit.js +5 -1
- package/dist/builder.d.ts +1 -8
- package/dist/builder.js +72 -18
- package/dist/client.d.ts +38 -2
- package/dist/client.js +131 -7
- package/dist/credentials.js +4 -2
- package/dist/ergonomic/preset_resolver.js +4 -5
- package/dist/ergonomic/presets/image_compress.d.ts +1 -9
- package/dist/ergonomic/presets/image_compress.js +6 -25
- package/dist/ergonomic/presets/index.d.ts +1 -1
- package/dist/ergonomic/presets/index.js +1 -1
- package/dist/errors.d.ts +75 -1
- package/dist/errors.js +73 -0
- package/dist/file-first.d.ts +456 -4
- package/dist/file-first.js +1042 -83
- package/dist/generated/sdk_spec/enums.d.ts +0 -11
- package/dist/generated/sdk_spec/enums.js +0 -7
- package/dist/generated/sdk_spec/errors.d.ts +1 -1
- package/dist/generated/sdk_spec/errors.js +26 -0
- package/dist/generated/sdk_spec/presets.js +0 -3
- package/dist/generated/sdk_spec/version.d.ts +2 -2
- package/dist/generated/sdk_spec/version.js +2 -2
- package/dist/gisl.d.ts +22 -1
- package/dist/gisl.js +31 -1
- package/dist/handle.d.ts +153 -0
- package/dist/handle.js +273 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +14 -0
- package/dist/index.core.d.ts +35 -0
- package/dist/index.core.js +102 -0
- package/dist/index.d.ts +1 -30
- package/dist/index.js +9 -73
- package/dist/lazy-downloader.d.ts +19 -0
- package/dist/lazy-downloader.js +19 -0
- package/dist/merge.d.ts +13 -1
- package/dist/merge.js +186 -55
- package/dist/node-fs.browser.d.ts +17 -0
- package/dist/node-fs.browser.js +7 -0
- package/dist/node-fs.d.ts +14 -0
- package/dist/node-fs.js +14 -0
- package/dist/sha256.d.ts +20 -0
- package/dist/sha256.js +108 -0
- package/dist/types.d.ts +54 -2
- package/dist/types.js +2 -0
- package/package.json +15 -2
|
@@ -20,12 +20,6 @@ export declare const ImageFormat: {
|
|
|
20
20
|
readonly Avif: "avif";
|
|
21
21
|
};
|
|
22
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
23
|
export declare const ImageMetadataPolicy: {
|
|
30
24
|
readonly All: "all";
|
|
31
25
|
readonly None: "none";
|
|
@@ -122,11 +116,6 @@ export declare const ERGONOMIC_ENUMS: {
|
|
|
122
116
|
readonly Webp: "webp";
|
|
123
117
|
readonly Avif: "avif";
|
|
124
118
|
};
|
|
125
|
-
readonly ImageFit: {
|
|
126
|
-
readonly Max: "max";
|
|
127
|
-
readonly Crop: "crop";
|
|
128
|
-
readonly Scale: "scale";
|
|
129
|
-
};
|
|
130
119
|
readonly ImageMetadataPolicy: {
|
|
131
120
|
readonly All: "all";
|
|
132
121
|
readonly None: "none";
|
|
@@ -23,12 +23,6 @@ export const ImageFormat = {
|
|
|
23
23
|
Webp: "webp",
|
|
24
24
|
Avif: "avif",
|
|
25
25
|
};
|
|
26
|
-
// Image resize mode. Only applies when width or height is set (depends_on width/height in compress.yaml).
|
|
27
|
-
export const ImageFit = {
|
|
28
|
-
Max: "max",
|
|
29
|
-
Crop: "crop",
|
|
30
|
-
Scale: "scale",
|
|
31
|
-
};
|
|
32
26
|
// Image metadata handling. Counter-intuitive wire naming:
|
|
33
27
|
// All = strip everything (smallest file)
|
|
34
28
|
// None = keep all EXIF/IPTC/XMP
|
|
@@ -113,7 +107,6 @@ export const ERGONOMIC_ENUMS = {
|
|
|
113
107
|
OptimizeFor,
|
|
114
108
|
ImageMode,
|
|
115
109
|
ImageFormat,
|
|
116
|
-
ImageFit,
|
|
117
110
|
ImageMetadataPolicy,
|
|
118
111
|
IccProfilePolicy,
|
|
119
112
|
VideoCodec,
|
|
@@ -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" | "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";
|
|
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" | "validation_error" | "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" | "upload_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,19 @@ export const ERROR_CODES = Object.freeze({
|
|
|
150
150
|
"details": "array",
|
|
151
151
|
}),
|
|
152
152
|
}),
|
|
153
|
+
"validation_error": Object.freeze({
|
|
154
|
+
code: "validation_error",
|
|
155
|
+
category: "validation",
|
|
156
|
+
source: "error_type",
|
|
157
|
+
status: "wired",
|
|
158
|
+
httpStatus: 422,
|
|
159
|
+
retryable: false,
|
|
160
|
+
sdkClass: "GislValidationError",
|
|
161
|
+
description: "422 — `error_type` discriminator on `ValidationErrorEnvelope` (ADR-0018/0019); distinct from `validation_failed` (the `error` code), same `GislValidationError`. Carries `details[]`.",
|
|
162
|
+
metadataSchema: Object.freeze({
|
|
163
|
+
"details": "array",
|
|
164
|
+
}),
|
|
165
|
+
}),
|
|
153
166
|
"cyclic_workflow_edges": Object.freeze({
|
|
154
167
|
code: "cyclic_workflow_edges",
|
|
155
168
|
category: "validation",
|
|
@@ -277,6 +290,17 @@ export const ERROR_CODES = Object.freeze({
|
|
|
277
290
|
"uploadId": "string",
|
|
278
291
|
}),
|
|
279
292
|
}),
|
|
293
|
+
"upload_not_found": Object.freeze({
|
|
294
|
+
code: "upload_not_found",
|
|
295
|
+
category: "api",
|
|
296
|
+
source: "ErrorEnvelope.error",
|
|
297
|
+
status: "wired",
|
|
298
|
+
httpStatus: 404,
|
|
299
|
+
retryable: false,
|
|
300
|
+
sdkClass: "GislApiError",
|
|
301
|
+
description: "404 on POST /api/workflows — a referenced upload was not found, OR exists but is owned by a different identity (deliberate BOLA/IDOR existence-mask: reported as not-found, never 403, so the response does not reveal another user's upload exists). message_key upload.not_found. Wire token UPLOAD_NOT_FOUND keyed on the `error` field. Per ADR-0016 amendment.",
|
|
302
|
+
metadataSchema: Object.freeze({}),
|
|
303
|
+
}),
|
|
280
304
|
"workflow_expired": Object.freeze({
|
|
281
305
|
code: "workflow_expired",
|
|
282
306
|
category: "api",
|
|
@@ -477,6 +501,7 @@ export const ERROR_CATEGORIES = Object.freeze({
|
|
|
477
501
|
"feature_tier_restricted",
|
|
478
502
|
"tier_restriction",
|
|
479
503
|
"multipart_session_not_found",
|
|
504
|
+
"upload_not_found",
|
|
480
505
|
"workflow_expired",
|
|
481
506
|
"balance_exhausted",
|
|
482
507
|
"feature_not_available",
|
|
@@ -504,6 +529,7 @@ export const ERROR_CATEGORIES = Object.freeze({
|
|
|
504
529
|
"multipart_part_invalid",
|
|
505
530
|
"multipart_part_count_exceeded",
|
|
506
531
|
"validation_failed",
|
|
532
|
+
"validation_error",
|
|
507
533
|
"cyclic_workflow_edges",
|
|
508
534
|
"workflow_edge_references_unknown_job",
|
|
509
535
|
"reserved_job_id_pattern",
|
|
@@ -8,7 +8,6 @@ export const PRESETS = Object.freeze({
|
|
|
8
8
|
"quality": 65,
|
|
9
9
|
"metadata": "All",
|
|
10
10
|
"iccProfile": "Strip",
|
|
11
|
-
"autoOrient": true,
|
|
12
11
|
"progressive": true,
|
|
13
12
|
"outputFormat": "Smallest",
|
|
14
13
|
}),
|
|
@@ -17,7 +16,6 @@ export const PRESETS = Object.freeze({
|
|
|
17
16
|
"quality": 80,
|
|
18
17
|
"metadata": "Sensitive",
|
|
19
18
|
"iccProfile": "Preserve",
|
|
20
|
-
"autoOrient": true,
|
|
21
19
|
"progressive": true,
|
|
22
20
|
"outputFormat": "Auto",
|
|
23
21
|
}),
|
|
@@ -25,7 +23,6 @@ export const PRESETS = Object.freeze({
|
|
|
25
23
|
"mode": "Lossless",
|
|
26
24
|
"metadata": "None",
|
|
27
25
|
"iccProfile": "Preserve",
|
|
28
|
-
"autoOrient": true,
|
|
29
26
|
"progressive": true,
|
|
30
27
|
"outputFormat": "Original",
|
|
31
28
|
}),
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const SDK_SPEC_VERSION: "1.
|
|
1
|
+
export declare const SDK_SPEC_VERSION: "1.5.0";
|
|
2
2
|
export declare const PRESET_VERSION: "1.0";
|
|
3
|
-
export declare const PRESET_CONFIG_HASH: "sha256:
|
|
3
|
+
export declare const PRESET_CONFIG_HASH: "sha256:dc48f4d31f4037cc07fb0686519122c03a55b03a41f05fbfc2af54e54950a0d2";
|
|
@@ -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.5.0";
|
|
5
5
|
export const PRESET_VERSION = "1.0";
|
|
6
|
-
export const PRESET_CONFIG_HASH = "sha256:
|
|
6
|
+
export const PRESET_CONFIG_HASH = "sha256:dc48f4d31f4037cc07fb0686519122c03a55b03a41f05fbfc2af54e54950a0d2";
|
package/dist/gisl.d.ts
CHANGED
|
@@ -23,7 +23,8 @@ 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
|
+
import { Recipe, FilesRecipe, type FileInput } from './file-first.js';
|
|
27
|
+
import { Handle } from './handle.js';
|
|
27
28
|
/**
|
|
28
29
|
* Operations that may be invoked on a `gisl.anonymous()` client without
|
|
29
30
|
* raising `GislFeatureRequiresAuthError`. Empty until the free-tier launch
|
|
@@ -77,6 +78,26 @@ export type ErgonomicClient = GislClient & {
|
|
|
77
78
|
* Execution (`run()`) lands in FF2b.
|
|
78
79
|
*/
|
|
79
80
|
file(input: string | Blob | FileInput, key?: string): Recipe;
|
|
81
|
+
/**
|
|
82
|
+
* Homogeneous fan-out entry point (FF3a). Apply ONE recipe (op chain) to
|
|
83
|
+
* MANY input files in ONE workflow. Each element is a filesystem path
|
|
84
|
+
* (string), an in-memory {@link FileInput} via `fileInput.*`, or a Blob/File.
|
|
85
|
+
* Returns an immutable {@link FilesRecipe} you call the same ops on
|
|
86
|
+
* (`.compress()` / `.convert()` / `.thumbnail()` / `.textWatermark()`); the
|
|
87
|
+
* chain applies to every input. `run()` returns a partitioned
|
|
88
|
+
* {@link RunResult} keyed by each input's 0-based index — one bad input does
|
|
89
|
+
* not sink the rest. `submit(webhook?)` is the fire-and-forget arm: it
|
|
90
|
+
* returns a {@link Handle} whose `wait()`/`result()` partition per input.
|
|
91
|
+
*/
|
|
92
|
+
files(inputs: ReadonlyArray<string | Blob | FileInput>): FilesRecipe;
|
|
93
|
+
/**
|
|
94
|
+
* Reattach to a previously-created workflow (FF5a). Returns a client-bound
|
|
95
|
+
* {@link Handle} you can `.status()` / `.wait()` / `.result()`. The handle
|
|
96
|
+
* carries no `webhookSecret` and no recipe key, so the {@link RunResult}
|
|
97
|
+
* from `wait()`/`result()` is keyless (`succeeded[].key === null`) — address
|
|
98
|
+
* outputs positionally or via the sinks.
|
|
99
|
+
*/
|
|
100
|
+
workflow(id: string): Handle;
|
|
80
101
|
compress(input: string | Blob, options?: Record<string, unknown>): OperationBuilder;
|
|
81
102
|
convert(input: string | Blob, options?: Record<string, unknown>): OperationBuilder;
|
|
82
103
|
thumbnail(input: string | Blob, options?: Record<string, unknown>): OperationBuilder;
|
package/dist/gisl.js
CHANGED
|
@@ -22,7 +22,8 @@ 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
|
+
import { Recipe, FilesRecipe, fileInput } from './file-first.js';
|
|
26
|
+
import { Handle } from './handle.js';
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Anonymous-capable operation allowlist (internal)
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
@@ -88,6 +89,35 @@ function wrapErgonomic(client, presetDefaults, scopedPresetDefaults) {
|
|
|
88
89
|
return new Recipe(resolved, key, [], presetDefaults, scopedPresetDefaults, target);
|
|
89
90
|
};
|
|
90
91
|
}
|
|
92
|
+
if (prop === 'files') {
|
|
93
|
+
// Homogeneous fan-out entry point (FF3a) — apply ONE recipe (op chain)
|
|
94
|
+
// to MANY input files in ONE workflow. Each element is coerced the same
|
|
95
|
+
// way `file()` coerces its single input: a bare string is a filesystem
|
|
96
|
+
// path, a Blob/File an in-memory input, a `FileInput` passed through.
|
|
97
|
+
// The fan-out's RunResult partitions per input by 0-based index.
|
|
98
|
+
return (inputs) => {
|
|
99
|
+
if (inputs.length === 0) {
|
|
100
|
+
// A zero-input fan-out is a caller error — "one failing input
|
|
101
|
+
// doesn't sink the rest" is meaningless with no inputs, and it
|
|
102
|
+
// would otherwise create an empty-jobs workflow the API 422s.
|
|
103
|
+
throw new GislConfigError('files() requires at least one input file.', {
|
|
104
|
+
reason: 'no_inputs',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const resolved = inputs.map((input) => typeof input === 'string'
|
|
108
|
+
? fileInput.path(input)
|
|
109
|
+
: input instanceof Blob
|
|
110
|
+
? fileInput.blob(input)
|
|
111
|
+
: input);
|
|
112
|
+
return new FilesRecipe(resolved, [], presetDefaults, scopedPresetDefaults, target);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (prop === 'workflow') {
|
|
116
|
+
// Reattach to a previously-created workflow (FF5a). Returns a
|
|
117
|
+
// client-bound Handle with no webhookSecret and no recipe key —
|
|
118
|
+
// its RunResult is therefore keyless (succeeded[].key === null).
|
|
119
|
+
return (id) => new Handle(id, undefined, target);
|
|
120
|
+
}
|
|
91
121
|
if (prop === 'compress' || prop === 'convert' || prop === 'thumbnail') {
|
|
92
122
|
return (input, options = {}) => {
|
|
93
123
|
// T4b — pass client-scope presetDefaults into the builder so
|
package/dist/handle.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-first {@link Handle} + {@link StatusSnapshot} value objects (FF5a).
|
|
3
|
+
*
|
|
4
|
+
* A `Handle` is the lightweight return of a fire-and-forget submit
|
|
5
|
+
* (`OperationBuilder.submit()` / `MergeBuilder.submit()`) AND the value
|
|
6
|
+
* `client.workflow(id)` hands back to reattach to a previously-created
|
|
7
|
+
* workflow. When a `Handle` carries a bound client it exposes three
|
|
8
|
+
* accessors:
|
|
9
|
+
*
|
|
10
|
+
* - `status()` — one non-blocking status fetch, projected to a
|
|
11
|
+
* {@link StatusSnapshot}.
|
|
12
|
+
* - `wait(maxWait, onProgress?)` — the ONLY blocking path: await terminal
|
|
13
|
+
* (SSE with poll fallback), then fetch downloads + project to a
|
|
14
|
+
* {@link RunResult}.
|
|
15
|
+
* - `result()` — non-blocking: fetch status once; if terminal, fetch
|
|
16
|
+
* downloads + project to a {@link RunResult}; if NOT terminal, throw
|
|
17
|
+
* {@link GislResultNotReadyError}. Never waits/polls.
|
|
18
|
+
*
|
|
19
|
+
* A `Handle` built WITHOUT a client (the operation-first/merge `submit()`
|
|
20
|
+
* path) keeps its data fields + `toJSON()` byte-identical to the prior
|
|
21
|
+
* `{ workflowId, webhookSecret }` interface; its accessors throw
|
|
22
|
+
* {@link GislConfigError} (reason `no_client`).
|
|
23
|
+
*
|
|
24
|
+
* Module placement: this lives in its OWN module (not `builder.ts` or
|
|
25
|
+
* `file-first.ts`) to keep the ESM import graph acyclic at module-load time.
|
|
26
|
+
* It imports the await-primitives from `builder.ts` and the
|
|
27
|
+
* {@link RunResult} + {@link projectDownloadsToRunResult} projection from
|
|
28
|
+
* `file-first.ts`; `builder.ts`/`merge.ts` import `Handle` back for
|
|
29
|
+
* construction inside their `submit()` methods. That back-edge is
|
|
30
|
+
* DEFERRED-USAGE-ONLY (construction happens at call time, not module-load),
|
|
31
|
+
* which ESM resolves cleanly.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors the PHP `Gisl\Sdk\Ergonomic\Handle` + `Gisl\Sdk\Ergonomic\StatusSnapshot`.
|
|
34
|
+
*/
|
|
35
|
+
import type { GislClient } from './client.js';
|
|
36
|
+
import { type ProgressEvent } from './builder.js';
|
|
37
|
+
import { RunResult } from './file-first.js';
|
|
38
|
+
/**
|
|
39
|
+
* A non-blocking snapshot of a workflow's lifecycle state, returned by
|
|
40
|
+
* {@link Handle.status}. `state` is the RAW wire `WorkflowStatus` value,
|
|
41
|
+
* verbatim (`pending` | `in_progress` | `completed` | `failed` |
|
|
42
|
+
* `partially_failed` | `paused_insufficient_credits` | `cancelled` |
|
|
43
|
+
* `expired`). There is NO `phase` field — phase is an SSE-only concept; the
|
|
44
|
+
* status response carries no phase.
|
|
45
|
+
*
|
|
46
|
+
* Mirrors the PHP `Gisl\Sdk\Ergonomic\StatusSnapshot`.
|
|
47
|
+
*/
|
|
48
|
+
export declare class StatusSnapshot {
|
|
49
|
+
readonly workflowId: string;
|
|
50
|
+
readonly state: string;
|
|
51
|
+
constructor(workflowId: string, state: string);
|
|
52
|
+
/**
|
|
53
|
+
* True when {@link state} is one of the terminal states (`completed`,
|
|
54
|
+
* `failed`, `partially_failed`, `cancelled`, `expired`,
|
|
55
|
+
* `paused_insufficient_credits`); false for `pending` / `in_progress`.
|
|
56
|
+
*/
|
|
57
|
+
isTerminal(): boolean;
|
|
58
|
+
/** Plain-object projection. Mirrors the PHP `toArray()`. */
|
|
59
|
+
toJSON(): {
|
|
60
|
+
workflowId: string;
|
|
61
|
+
state: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Handle to a created workflow. Carries `workflowId` + an optional
|
|
66
|
+
* `webhookSecret` (the data the operation-first/merge `submit()` returns)
|
|
67
|
+
* and, when reattached or built by the file-first run path, an optional
|
|
68
|
+
* bound {@link GislClient}.
|
|
69
|
+
*
|
|
70
|
+
* The bound client is OPTIONAL (mirrors how {@link RunResult} binds its
|
|
71
|
+
* {@link Downloader}): the data fields + {@link toJSON} stay byte-identical
|
|
72
|
+
* whether or not a client is present, so the operation-first/merge `submit()`
|
|
73
|
+
* back-compat fixture (`{ workflowId, webhookSecret }` via `toJSON()`) holds.
|
|
74
|
+
* When the client is absent, {@link status}/{@link wait}/{@link result} throw
|
|
75
|
+
* {@link GislConfigError} (reason `no_client`).
|
|
76
|
+
*
|
|
77
|
+
* A handle built via `client.workflow(id)` has NO recipe key, so its
|
|
78
|
+
* {@link RunResult} is keyless (`succeeded[].key === null`) — address its
|
|
79
|
+
* outputs positionally / via the sinks rather than `byKey()`.
|
|
80
|
+
*
|
|
81
|
+
* Mirrors the PHP `Gisl\Sdk\Ergonomic\Handle`.
|
|
82
|
+
*/
|
|
83
|
+
export declare class Handle {
|
|
84
|
+
#private;
|
|
85
|
+
readonly workflowId: string;
|
|
86
|
+
readonly webhookSecret?: string | undefined;
|
|
87
|
+
constructor(workflowId: string, webhookSecret?: string | undefined, client?: GislClient, key?: string | null);
|
|
88
|
+
/**
|
|
89
|
+
* Fetch the workflow's current status once (non-blocking) and project it to
|
|
90
|
+
* a {@link StatusSnapshot}.
|
|
91
|
+
* @throws {GislConfigError} reason `no_client` when no client is bound.
|
|
92
|
+
*/
|
|
93
|
+
status(): Promise<StatusSnapshot>;
|
|
94
|
+
/**
|
|
95
|
+
* Block until the workflow reaches a terminal state (SSE with poll
|
|
96
|
+
* fallback), then fetch its downloads and project to a {@link RunResult}.
|
|
97
|
+
* This is the ONLY blocking accessor on a `Handle`.
|
|
98
|
+
*
|
|
99
|
+
* @param maxWait Wall-clock deadline for the wait + downloads (string suffix
|
|
100
|
+
* `'2h'`/`'30m'`/`'120s'` or a number of milliseconds). Defaults to 300s,
|
|
101
|
+
* matching `Recipe.run()` / the PHP `Handle::wait()` default.
|
|
102
|
+
* @throws {GislConfigError} reason `no_client` when no client is bound.
|
|
103
|
+
* @throws {GislTimeoutError} when `maxWait` elapses before terminal.
|
|
104
|
+
*/
|
|
105
|
+
wait(maxWait?: string | number, onProgress?: (event: ProgressEvent) => void): Promise<RunResult>;
|
|
106
|
+
/**
|
|
107
|
+
* Non-blocking result accessor. Fetches the workflow status once: if the
|
|
108
|
+
* workflow is terminal, fetches its downloads and projects to a
|
|
109
|
+
* {@link RunResult}; if it is NOT terminal, throws
|
|
110
|
+
* {@link GislResultNotReadyError}. Never waits or polls — use {@link wait}
|
|
111
|
+
* to block.
|
|
112
|
+
*
|
|
113
|
+
* @throws {GislConfigError} reason `no_client` when no client is bound.
|
|
114
|
+
* @throws {GislResultNotReadyError} when the workflow is not yet terminal.
|
|
115
|
+
*/
|
|
116
|
+
result(): Promise<RunResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Project a terminal status + its per-job downloads into a {@link RunResult},
|
|
119
|
+
* choosing the producer DATA-DRIVEN off the wire (not a construction-time
|
|
120
|
+
* marker, so a fan-out reattached via `client.workflow(id)` — which carries
|
|
121
|
+
* no marker — still partitions per job):
|
|
122
|
+
*
|
|
123
|
+
* - A `files([...])` fan-out (every job ref is `file-{i}`, see
|
|
124
|
+
* {@link isFanoutStatus}) → {@link projectMultiJobToRunResult} with an
|
|
125
|
+
* empty `keyByRef`, so each input's key is recovered from its `file-{i}`
|
|
126
|
+
* ref (`"0"`, `"1"`, …). A submitted/reattached fan-out carries no
|
|
127
|
+
* caller-supplied keys — keyed fan-out is a separate concern.
|
|
128
|
+
* - Anything else (the single-file {@link Recipe} path) →
|
|
129
|
+
* {@link projectDownloadsToRunResult} keyed by this handle's `#key`
|
|
130
|
+
* (the recipe key from a file-first `submit()`, or `null` on reattach).
|
|
131
|
+
*/
|
|
132
|
+
private project;
|
|
133
|
+
/**
|
|
134
|
+
* Plain-object projection. Field order (`workflowId`, then `webhookSecret`
|
|
135
|
+
* when present) and the omit-when-undefined behaviour match the PHP
|
|
136
|
+
* `toArray()` so JSON-string parity holds with the prior `Handle` shape.
|
|
137
|
+
* The bound client is NEVER serialised.
|
|
138
|
+
*/
|
|
139
|
+
toJSON(): {
|
|
140
|
+
workflowId: string;
|
|
141
|
+
webhookSecret?: string;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Back-compat alias for {@link toJSON} — the prior operation-first/merge
|
|
145
|
+
* `submit()` fixture asserts a plain `{ workflowId, webhookSecret }` shape.
|
|
146
|
+
*/
|
|
147
|
+
toArray(): {
|
|
148
|
+
workflowId: string;
|
|
149
|
+
webhookSecret?: string;
|
|
150
|
+
};
|
|
151
|
+
private makeDownloader;
|
|
152
|
+
private requireClient;
|
|
153
|
+
}
|
package/dist/handle.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-first {@link Handle} + {@link StatusSnapshot} value objects (FF5a).
|
|
3
|
+
*
|
|
4
|
+
* A `Handle` is the lightweight return of a fire-and-forget submit
|
|
5
|
+
* (`OperationBuilder.submit()` / `MergeBuilder.submit()`) AND the value
|
|
6
|
+
* `client.workflow(id)` hands back to reattach to a previously-created
|
|
7
|
+
* workflow. When a `Handle` carries a bound client it exposes three
|
|
8
|
+
* accessors:
|
|
9
|
+
*
|
|
10
|
+
* - `status()` — one non-blocking status fetch, projected to a
|
|
11
|
+
* {@link StatusSnapshot}.
|
|
12
|
+
* - `wait(maxWait, onProgress?)` — the ONLY blocking path: await terminal
|
|
13
|
+
* (SSE with poll fallback), then fetch downloads + project to a
|
|
14
|
+
* {@link RunResult}.
|
|
15
|
+
* - `result()` — non-blocking: fetch status once; if terminal, fetch
|
|
16
|
+
* downloads + project to a {@link RunResult}; if NOT terminal, throw
|
|
17
|
+
* {@link GislResultNotReadyError}. Never waits/polls.
|
|
18
|
+
*
|
|
19
|
+
* A `Handle` built WITHOUT a client (the operation-first/merge `submit()`
|
|
20
|
+
* path) keeps its data fields + `toJSON()` byte-identical to the prior
|
|
21
|
+
* `{ workflowId, webhookSecret }` interface; its accessors throw
|
|
22
|
+
* {@link GislConfigError} (reason `no_client`).
|
|
23
|
+
*
|
|
24
|
+
* Module placement: this lives in its OWN module (not `builder.ts` or
|
|
25
|
+
* `file-first.ts`) to keep the ESM import graph acyclic at module-load time.
|
|
26
|
+
* It imports the await-primitives from `builder.ts` and the
|
|
27
|
+
* {@link RunResult} + {@link projectDownloadsToRunResult} projection from
|
|
28
|
+
* `file-first.ts`; `builder.ts`/`merge.ts` import `Handle` back for
|
|
29
|
+
* construction inside their `submit()` methods. That back-edge is
|
|
30
|
+
* DEFERRED-USAGE-ONLY (construction happens at call time, not module-load),
|
|
31
|
+
* which ESM resolves cleanly.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors the PHP `Gisl\Sdk\Ergonomic\Handle` + `Gisl\Sdk\Ergonomic\StatusSnapshot`.
|
|
34
|
+
*/
|
|
35
|
+
import { GislConfigError, GislNetworkError, GislResultNotReadyError, GislTimeoutError, SseEndedWithoutTerminal, } from './errors.js';
|
|
36
|
+
import { _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, } from './builder.js';
|
|
37
|
+
import { projectDownloadsToRunResult, projectMultiJobToRunResult, isFanoutStatus, isMergeStatus, isArchiveStatus, } from './file-first.js';
|
|
38
|
+
import { LazyHttpDownloader } from './lazy-downloader.js';
|
|
39
|
+
/**
|
|
40
|
+
* The terminal workflow states. A status response in any of these states
|
|
41
|
+
* will not change without caller action. Mirrors the `TERMINAL_STATUSES`
|
|
42
|
+
* sets in `client.ts` / `WorkflowConstants` (PHP) — `paused_insufficient_credits`
|
|
43
|
+
* is treated as terminal because the workflow only resumes on caller action.
|
|
44
|
+
*/
|
|
45
|
+
const TERMINAL_STATES = new Set([
|
|
46
|
+
'completed',
|
|
47
|
+
'failed',
|
|
48
|
+
'partially_failed',
|
|
49
|
+
'cancelled',
|
|
50
|
+
'expired',
|
|
51
|
+
'paused_insufficient_credits',
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* A non-blocking snapshot of a workflow's lifecycle state, returned by
|
|
55
|
+
* {@link Handle.status}. `state` is the RAW wire `WorkflowStatus` value,
|
|
56
|
+
* verbatim (`pending` | `in_progress` | `completed` | `failed` |
|
|
57
|
+
* `partially_failed` | `paused_insufficient_credits` | `cancelled` |
|
|
58
|
+
* `expired`). There is NO `phase` field — phase is an SSE-only concept; the
|
|
59
|
+
* status response carries no phase.
|
|
60
|
+
*
|
|
61
|
+
* Mirrors the PHP `Gisl\Sdk\Ergonomic\StatusSnapshot`.
|
|
62
|
+
*/
|
|
63
|
+
export class StatusSnapshot {
|
|
64
|
+
workflowId;
|
|
65
|
+
state;
|
|
66
|
+
constructor(workflowId, state) {
|
|
67
|
+
this.workflowId = workflowId;
|
|
68
|
+
this.state = state;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* True when {@link state} is one of the terminal states (`completed`,
|
|
72
|
+
* `failed`, `partially_failed`, `cancelled`, `expired`,
|
|
73
|
+
* `paused_insufficient_credits`); false for `pending` / `in_progress`.
|
|
74
|
+
*/
|
|
75
|
+
isTerminal() {
|
|
76
|
+
return TERMINAL_STATES.has(this.state);
|
|
77
|
+
}
|
|
78
|
+
/** Plain-object projection. Mirrors the PHP `toArray()`. */
|
|
79
|
+
toJSON() {
|
|
80
|
+
return { workflowId: this.workflowId, state: this.state };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Handle to a created workflow. Carries `workflowId` + an optional
|
|
85
|
+
* `webhookSecret` (the data the operation-first/merge `submit()` returns)
|
|
86
|
+
* and, when reattached or built by the file-first run path, an optional
|
|
87
|
+
* bound {@link GislClient}.
|
|
88
|
+
*
|
|
89
|
+
* The bound client is OPTIONAL (mirrors how {@link RunResult} binds its
|
|
90
|
+
* {@link Downloader}): the data fields + {@link toJSON} stay byte-identical
|
|
91
|
+
* whether or not a client is present, so the operation-first/merge `submit()`
|
|
92
|
+
* back-compat fixture (`{ workflowId, webhookSecret }` via `toJSON()`) holds.
|
|
93
|
+
* When the client is absent, {@link status}/{@link wait}/{@link result} throw
|
|
94
|
+
* {@link GislConfigError} (reason `no_client`).
|
|
95
|
+
*
|
|
96
|
+
* A handle built via `client.workflow(id)` has NO recipe key, so its
|
|
97
|
+
* {@link RunResult} is keyless (`succeeded[].key === null`) — address its
|
|
98
|
+
* outputs positionally / via the sinks rather than `byKey()`.
|
|
99
|
+
*
|
|
100
|
+
* Mirrors the PHP `Gisl\Sdk\Ergonomic\Handle`.
|
|
101
|
+
*/
|
|
102
|
+
export class Handle {
|
|
103
|
+
workflowId;
|
|
104
|
+
webhookSecret;
|
|
105
|
+
// True ES private (`#`), NOT a TS `private` modifier: a `private` constructor
|
|
106
|
+
// parameter property is an enumerable own field, so spreading/logging/Object
|
|
107
|
+
// .assign-ing a bound handle would leak the GislClient (incl. auth headers).
|
|
108
|
+
// `#client` is non-enumerable and inaccessible outside the class (codex high).
|
|
109
|
+
#client;
|
|
110
|
+
// The recipe's result-addressing key, threaded from a file-first `submit()`
|
|
111
|
+
// (`Recipe.submit()`) so the `RunResult` from `wait()`/`result()` is keyed
|
|
112
|
+
// (`succeeded[].key === recipeKey`). A reattached handle
|
|
113
|
+
// (`client.workflow(id)`) passes no key → null → keyless RunResult. It is
|
|
114
|
+
// ES-private (`#`) — like `#client` — NOT just kept out of `toJSON()`: the
|
|
115
|
+
// parity ReturnSerialiser enumerates a Handle's OWN ENUMERABLE properties
|
|
116
|
+
// (it does not call `toJSON()`), so a plain `readonly key` leaked into the
|
|
117
|
+
// operation-first/merge `submit()` back-compat shape ({workflowId,
|
|
118
|
+
// webhookSecret}). `#key` is non-enumerable, so that shape stays byte-identical.
|
|
119
|
+
#key;
|
|
120
|
+
constructor(workflowId, webhookSecret, client, key = null) {
|
|
121
|
+
this.workflowId = workflowId;
|
|
122
|
+
this.webhookSecret = webhookSecret;
|
|
123
|
+
this.#client = client;
|
|
124
|
+
this.#key = key;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Fetch the workflow's current status once (non-blocking) and project it to
|
|
128
|
+
* a {@link StatusSnapshot}.
|
|
129
|
+
* @throws {GislConfigError} reason `no_client` when no client is bound.
|
|
130
|
+
*/
|
|
131
|
+
async status() {
|
|
132
|
+
const client = this.requireClient();
|
|
133
|
+
const status = await client.getWorkflowStatus(this.workflowId);
|
|
134
|
+
return new StatusSnapshot(this.workflowId, status.status);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Block until the workflow reaches a terminal state (SSE with poll
|
|
138
|
+
* fallback), then fetch its downloads and project to a {@link RunResult}.
|
|
139
|
+
* This is the ONLY blocking accessor on a `Handle`.
|
|
140
|
+
*
|
|
141
|
+
* @param maxWait Wall-clock deadline for the wait + downloads (string suffix
|
|
142
|
+
* `'2h'`/`'30m'`/`'120s'` or a number of milliseconds). Defaults to 300s,
|
|
143
|
+
* matching `Recipe.run()` / the PHP `Handle::wait()` default.
|
|
144
|
+
* @throws {GislConfigError} reason `no_client` when no client is bound.
|
|
145
|
+
* @throws {GislTimeoutError} when `maxWait` elapses before terminal.
|
|
146
|
+
*/
|
|
147
|
+
async wait(maxWait = 300_000, onProgress) {
|
|
148
|
+
const client = this.requireClient();
|
|
149
|
+
const deadline = Date.now() + _parseMaxWait(maxWait);
|
|
150
|
+
let finalStatus;
|
|
151
|
+
try {
|
|
152
|
+
finalStatus = await _consumeSseToTerminal(client, {
|
|
153
|
+
workflowId: this.workflowId,
|
|
154
|
+
deadline,
|
|
155
|
+
signal: undefined,
|
|
156
|
+
onProgress,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// TDqmkWpX: mirror Recipe.run() — poll-fallback ONLY on a clean SSE
|
|
161
|
+
// stream-end (SseEndedWithoutTerminal) or a typed transport error
|
|
162
|
+
// (GislNetworkError). Everything else (timeout, abort, API, an onProgress
|
|
163
|
+
// callback throw, anything unexpected) MUST propagate — re-issuing the same
|
|
164
|
+
// doomed request via poll would mask the real failure.
|
|
165
|
+
if (!(err instanceof SseEndedWithoutTerminal || err instanceof GislNetworkError)) {
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
finalStatus = await _pollToTerminal(client, {
|
|
169
|
+
workflowId: this.workflowId,
|
|
170
|
+
deadline,
|
|
171
|
+
signal: undefined,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (Date.now() >= deadline) {
|
|
175
|
+
throw new GislTimeoutError(`Workflow ${this.workflowId} reached terminal status but maxWait elapsed before downloads could be fetched`);
|
|
176
|
+
}
|
|
177
|
+
const downloads = await client.getWorkflowDownloads(this.workflowId);
|
|
178
|
+
// TDqmkWpX: re-check AFTER the downloads fetch so a slow getWorkflowDownloads
|
|
179
|
+
// cannot return a success past the advertised maxWait.
|
|
180
|
+
if (Date.now() >= deadline) {
|
|
181
|
+
throw new GislTimeoutError(`Workflow ${this.workflowId} downloads fetch completed after maxWait elapsed`);
|
|
182
|
+
}
|
|
183
|
+
return this.project(finalStatus, downloads.downloads);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Non-blocking result accessor. Fetches the workflow status once: if the
|
|
187
|
+
* workflow is terminal, fetches its downloads and projects to a
|
|
188
|
+
* {@link RunResult}; if it is NOT terminal, throws
|
|
189
|
+
* {@link GislResultNotReadyError}. Never waits or polls — use {@link wait}
|
|
190
|
+
* to block.
|
|
191
|
+
*
|
|
192
|
+
* @throws {GislConfigError} reason `no_client` when no client is bound.
|
|
193
|
+
* @throws {GislResultNotReadyError} when the workflow is not yet terminal.
|
|
194
|
+
*/
|
|
195
|
+
async result() {
|
|
196
|
+
const client = this.requireClient();
|
|
197
|
+
const status = await client.getWorkflowStatus(this.workflowId);
|
|
198
|
+
if (!TERMINAL_STATES.has(status.status)) {
|
|
199
|
+
throw new GislResultNotReadyError(this.workflowId, status.status);
|
|
200
|
+
}
|
|
201
|
+
const downloads = await client.getWorkflowDownloads(this.workflowId);
|
|
202
|
+
return this.project(status, downloads.downloads);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Project a terminal status + its per-job downloads into a {@link RunResult},
|
|
206
|
+
* choosing the producer DATA-DRIVEN off the wire (not a construction-time
|
|
207
|
+
* marker, so a fan-out reattached via `client.workflow(id)` — which carries
|
|
208
|
+
* no marker — still partitions per job):
|
|
209
|
+
*
|
|
210
|
+
* - A `files([...])` fan-out (every job ref is `file-{i}`, see
|
|
211
|
+
* {@link isFanoutStatus}) → {@link projectMultiJobToRunResult} with an
|
|
212
|
+
* empty `keyByRef`, so each input's key is recovered from its `file-{i}`
|
|
213
|
+
* ref (`"0"`, `"1"`, …). A submitted/reattached fan-out carries no
|
|
214
|
+
* caller-supplied keys — keyed fan-out is a separate concern.
|
|
215
|
+
* - Anything else (the single-file {@link Recipe} path) →
|
|
216
|
+
* {@link projectDownloadsToRunResult} keyed by this handle's `#key`
|
|
217
|
+
* (the recipe key from a file-first `submit()`, or `null` on reattach).
|
|
218
|
+
*/
|
|
219
|
+
project(finalStatus, jobDownloads) {
|
|
220
|
+
const downloader = this.makeDownloader();
|
|
221
|
+
if (isFanoutStatus(finalStatus)) {
|
|
222
|
+
return projectMultiJobToRunResult(this.workflowId, finalStatus, jobDownloads, new Map(), downloader);
|
|
223
|
+
}
|
|
224
|
+
// A fluent `files([...]).merge(...)` combine — project ONLY the merged
|
|
225
|
+
// output, filtering the `src_*` passthrough plumbing (which re-exposes the
|
|
226
|
+
// raw inputs). Matches MergedRecipe.run()'s `ref === 'merge'` filter so a
|
|
227
|
+
// submitted/reattached merge handle never surfaces the input artifacts
|
|
228
|
+
// alongside the combined output (codex c1).
|
|
229
|
+
if (isMergeStatus(finalStatus)) {
|
|
230
|
+
const mergeDownloads = jobDownloads.filter((d) => d.ref === 'merge');
|
|
231
|
+
return projectDownloadsToRunResult(this.workflowId, finalStatus, mergeDownloads, null, downloader);
|
|
232
|
+
}
|
|
233
|
+
// A fluent `files([...]).archive(...)` bundle — project ONLY the archive
|
|
234
|
+
// output, filtering the `src_*` passthrough plumbing (mirror of the merge
|
|
235
|
+
// branch for archive).
|
|
236
|
+
if (isArchiveStatus(finalStatus)) {
|
|
237
|
+
const archiveDownloads = jobDownloads.filter((d) => d.ref === 'archive');
|
|
238
|
+
return projectDownloadsToRunResult(this.workflowId, finalStatus, archiveDownloads, null, downloader);
|
|
239
|
+
}
|
|
240
|
+
return projectDownloadsToRunResult(this.workflowId, finalStatus, jobDownloads, this.#key, downloader);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Plain-object projection. Field order (`workflowId`, then `webhookSecret`
|
|
244
|
+
* when present) and the omit-when-undefined behaviour match the PHP
|
|
245
|
+
* `toArray()` so JSON-string parity holds with the prior `Handle` shape.
|
|
246
|
+
* The bound client is NEVER serialised.
|
|
247
|
+
*/
|
|
248
|
+
toJSON() {
|
|
249
|
+
return this.webhookSecret === undefined
|
|
250
|
+
? { workflowId: this.workflowId }
|
|
251
|
+
: { workflowId: this.workflowId, webhookSecret: this.webhookSecret };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Back-compat alias for {@link toJSON} — the prior operation-first/merge
|
|
255
|
+
* `submit()` fixture asserts a plain `{ workflowId, webhookSecret }` shape.
|
|
256
|
+
*/
|
|
257
|
+
toArray() {
|
|
258
|
+
return this.toJSON();
|
|
259
|
+
}
|
|
260
|
+
makeDownloader() {
|
|
261
|
+
// Download URLs from getWorkflowDownloads are pre-signed and require no SDK
|
|
262
|
+
// auth, so the downloader issues a plain unauthenticated fetch. Lazy so the
|
|
263
|
+
// node:fs-importing HttpDownloader stays out of the browser static graph.
|
|
264
|
+
return new LazyHttpDownloader();
|
|
265
|
+
}
|
|
266
|
+
requireClient() {
|
|
267
|
+
if (this.#client === undefined) {
|
|
268
|
+
throw new GislConfigError('This handle has no client bound, so it cannot query the workflow. ' +
|
|
269
|
+
'Use recipe.run() to execute and get a RunResult directly, or reattach via client.workflow(id).', { reason: 'no_client' });
|
|
270
|
+
}
|
|
271
|
+
return this.#client;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.core.js';
|