@giveitsmaller/sdk 0.21.0 → 0.25.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 +84 -0
- package/dist/builder.js +47 -11
- package/dist/client.d.ts +7 -0
- package/dist/client.js +83 -2
- package/dist/credentials.d.ts +74 -0
- package/dist/credentials.js +123 -0
- package/dist/ergonomic/image_output_routes.d.ts +67 -0
- package/dist/ergonomic/image_output_routes.js +146 -15
- package/dist/ergonomic/option_types.d.ts +11 -2
- package/dist/ergonomic/preset_resolver.d.ts +22 -0
- package/dist/ergonomic/preset_resolver.js +94 -0
- package/dist/ergonomic/presets/image_compress.js +16 -4
- package/dist/ergonomic/presets/video_compress.d.ts +20 -0
- package/dist/errors.d.ts +183 -7
- package/dist/errors.js +203 -7
- package/dist/file-first.js +63 -8
- package/dist/generated/sdk_spec/errors.d.ts +1 -1
- package/dist/generated/sdk_spec/errors.js +176 -1
- package/dist/gisl.d.ts +1 -1
- package/dist/gisl.js +14 -2
- package/dist/handle.js +12 -2
- package/dist/http-downloader.js +25 -7
- package/dist/index.core.d.ts +2 -2
- package/dist/index.core.js +19 -3
- package/dist/merge.d.ts +29 -0
- package/dist/merge.js +12 -2
- package/dist/sse.d.ts +23 -1
- package/dist/sse.js +76 -3
- package/dist/types.d.ts +18 -0
- package/package.json +10 -2
package/dist/handle.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
*
|
|
33
33
|
* Mirrors the PHP `Gisl\Sdk\Ergonomic\Handle` + `Gisl\Sdk\Ergonomic\StatusSnapshot`.
|
|
34
34
|
*/
|
|
35
|
-
import { GislConfigError, GislNetworkError, GislResultNotReadyError, GislTimeoutError, SseEndedWithoutTerminal, } from './errors.js';
|
|
35
|
+
import { GislConfigError, GislNetworkError, GislResultNotReadyError, GislTimeoutError, GislStreamHostNotDeclaredError, SseEndedWithoutTerminal, } from './errors.js';
|
|
36
36
|
import { _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, } from './builder.js';
|
|
37
37
|
import { projectDownloadsToRunResult, projectMultiJobToRunResult, isFanoutStatus, isMergeStatus, isArchiveStatus, isWatermarkStatus, isSoleOpChainStatus, soleOpChainDeliverableRef, _POST_STEP_JOB_REF, } from './file-first.js';
|
|
38
38
|
import { LazyHttpDownloader } from './lazy-downloader.js';
|
|
@@ -172,7 +172,17 @@ export class Handle {
|
|
|
172
172
|
// (GislNetworkError). Everything else (timeout, abort, API, an onProgress
|
|
173
173
|
// callback throw, anything unexpected) MUST propagate — re-issuing the same
|
|
174
174
|
// doomed request via poll would mask the real failure.
|
|
175
|
-
if (!(err instanceof SseEndedWithoutTerminal ||
|
|
175
|
+
if (!(err instanceof SseEndedWithoutTerminal ||
|
|
176
|
+
err instanceof GislNetworkError ||
|
|
177
|
+
// VUozk5Bc: no stream host is DECLARED for this configuration (a
|
|
178
|
+
// configuration nothing declares; both named environments resolve as of
|
|
179
|
+
// contracts v2.195.0). That is not a failure to recover from,
|
|
180
|
+
// it is SSE being unavailable here, and polling is a working
|
|
181
|
+
// transport. Failing hard instead would strand every caller on a host
|
|
182
|
+
// nobody has declared yet. A DIRECT `streamEvents` caller still gets
|
|
183
|
+
// the hard error — they asked for the stream specifically; a `run()`
|
|
184
|
+
// caller asked for a result.
|
|
185
|
+
err instanceof GislStreamHostNotDeclaredError)) {
|
|
176
186
|
throw err;
|
|
177
187
|
}
|
|
178
188
|
finalStatus = await _pollToTerminal(client, {
|
package/dist/http-downloader.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createWriteStream } from 'node:fs';
|
|
8
8
|
import { Readable } from 'node:stream';
|
|
9
9
|
import { pipeline } from 'node:stream/promises';
|
|
10
|
-
import {
|
|
10
|
+
import { GislDownloadHttpError, GislRequestNotSentError, GislSinkError, GislTransportError, } from './errors.js';
|
|
11
11
|
/**
|
|
12
12
|
* Streams a (typically pre-signed) URL to a local path without buffering the
|
|
13
13
|
* whole body in memory. Pre-signed download URLs require no SDK auth, so this
|
|
@@ -21,22 +21,40 @@ export class HttpDownloader {
|
|
|
21
21
|
// on the RunResult sink side. Parity-critical: the FF1 sink contract tells
|
|
22
22
|
// callers to narrow with instanceof, so the source-read error type must
|
|
23
23
|
// match across languages.
|
|
24
|
+
// codex f46340e1d58a: a malformed URL fails DETERMINISTICALLY, so it must
|
|
25
|
+
// not land in the always-retryable bucket with DNS and TLS. `fetch` rejects
|
|
26
|
+
// both with an indistinguishable TypeError, so the only way to tell them
|
|
27
|
+
// apart is to check BEFORE the call — which also gives
|
|
28
|
+
// GislRequestNotSentError a real throw site in TypeScript rather than
|
|
29
|
+
// leaving it declared-but-dormant.
|
|
30
|
+
try {
|
|
31
|
+
new URL(url);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new GislRequestNotSentError(`Download source is not a valid URL: ${url}`);
|
|
35
|
+
}
|
|
24
36
|
let res;
|
|
25
37
|
try {
|
|
26
38
|
res = await fetch(url);
|
|
27
39
|
}
|
|
28
40
|
catch (cause) {
|
|
29
41
|
// A rejected fetch (DNS, TCP, TLS, mid-flight disconnect) must surface as
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// (codex review medium).
|
|
33
|
-
|
|
42
|
+
// a typed error — not the raw TypeError — so callers can narrow every
|
|
43
|
+
// download-source failure with `instanceof GislNetworkError`
|
|
44
|
+
// (codex review medium). t2qCrjdr: TRANSPORT specifically, so the
|
|
45
|
+
// retry advice is `true` here and status-derived below.
|
|
46
|
+
throw new GislTransportError(`Failed to fetch download source: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
34
47
|
}
|
|
35
48
|
if (!res.ok) {
|
|
36
|
-
|
|
49
|
+
// t2qCrjdr: the server was REACHED and refused. Carries the status so a
|
|
50
|
+
// consumer telling a permanent 404 from a transient 503 never has to
|
|
51
|
+
// parse the message.
|
|
52
|
+
throw new GislDownloadHttpError(`Download failed with status ${res.status}`, res.status);
|
|
37
53
|
}
|
|
38
54
|
if (res.body === null) {
|
|
39
|
-
|
|
55
|
+
// 2xx with nothing in it — the server did not refuse, it under-delivered.
|
|
56
|
+
// Transport rather than HTTP, and retrying is the right advice.
|
|
57
|
+
throw new GislTransportError('Download response had no body');
|
|
40
58
|
}
|
|
41
59
|
// `fetch`'s WHATWG ReadableStream and Node's `stream/web` ReadableStream
|
|
42
60
|
// are structurally the same at runtime but typed in two different lib
|
package/dist/index.core.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { parseSseStream } from './sse.js';
|
|
|
3
3
|
export type { CreditsUsageOptions, ListWorkflowsOptions, GetSchemaOptions, GetSchemaResult, CapabilitiesSnapshot, PreflightClipError, PreflightClipsResult, ProbeWaitOptions, ProbeWaitResult, GislClientConfig, GislSseEvent, GislSseParseFailure, UploadOptions, WaitOptions, WorkflowCreatePayload, OperationDef, WorkflowSourcePayload, MultiInputSourcePayload, UploadSourcePayload, JobOutputSourcePayload, ExternalImportSourcePayload, ConnectionSourcePayload, JobInputV2Payload, JobDefinitionPayload, ExternalDestinationPayload, DeliveryPayload, DeliveryModePayload, DeliveryBundleFormatPayload, DeliverySelectionPayload, DeliverySelectionTypePayload, DeliveryOutputRefPayload, WorkflowProcessingPayload, ProcessingClassHintPayload, MultipartCheckpointState, _Sdk3HandCodedUploadedPart, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignedPart, _Sdk3HandCodedPresignPartsResult, _Sdk3HandCodedKeepaliveResult, } from './types.js';
|
|
4
4
|
export { uploadSource, jobOutputSource, externalImportSource, connectionSource, } from './types.js';
|
|
5
5
|
export type { GislConfigErrorMetadata } from './errors.js';
|
|
6
|
-
export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislLongFormConcurrencyError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError, GislAbortError, GislNetworkError, GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError, GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError, GislChainCardinalityMismatchError, GislBundleAlreadyArchivedError, GislNoSuchKeyError, GislSinkError, GislItemFailedError, GislResultNotReadyError, } from './errors.js';
|
|
6
|
+
export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislLongFormConcurrencyError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError, GislFanOutTimeoutError, GislAbortError, GislNetworkError, GislTransportError, GislDownloadHttpError, GislRequestNotSentError, GislConfigError, GislMissingCredentialsError, GislStreamHostNotDeclaredError, GislFeatureRequiresAuthError, GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError, GislChainCardinalityMismatchError, GislBundleAlreadyArchivedError, GislNoSuchKeyError, GislSinkError, GislItemFailedError, GislResultNotReadyError, } from './errors.js';
|
|
7
7
|
export type { GislApiErrorOptions, GislUploadCapKind } from './errors.js';
|
|
8
8
|
export type { ErrorCategory } from './generated/sdk_spec/errors.js';
|
|
9
9
|
export { RunResult } from './file-first.js';
|
|
@@ -33,7 +33,7 @@ export type { SseOperationProgressData, SseOperationCompletedData, SseOperationF
|
|
|
33
33
|
export type { MultiOutputCompletion, PageIndexed, PositionIndexed, Unindexed, } from '@giveitsmaller/contracts/asyncapi';
|
|
34
34
|
import type { MultiOutputCompletion as _MultiOutputCompletion } from '@giveitsmaller/contracts/asyncapi';
|
|
35
35
|
export type OperationResultOutputEntry = _MultiOutputCompletion['outputs'][number];
|
|
36
|
-
export type { CompressImageOptions, CompressImageJpegOptions, CompressImagePngOptions, CompressImageAvifOptions, CompressVideoOptions, CompressAudioOptions, CompressDocumentOfficeOptions, CompressDocumentOdfOptions, CompressDocumentEpubOptions, ThumbnailImageOptions, ThumbnailVideoOptions,
|
|
36
|
+
export type { CompressImageOptions, CompressImageJpegOptions, CompressImagePngOptions, CompressImageAvifOptions, CompressVideoOptions, CompressAudioOptions, CompressDocumentOfficeOptions, CompressDocumentOdfOptions, CompressDocumentEpubOptions, ThumbnailImageOptions, ThumbnailVideoOptions, ThumbnailDocumentOfficeOptions, ThumbnailDocumentPdfOptions, ThumbnailDocumentEpubOptions, TransformImageOptions, TransformImageGifOptions, TransformVideoOptions, TransformDocumentPdfOptions, ConvertImageOptions, ConvertVideoOptions, ConvertAudioOptions, ConvertDocumentPdfOptions, MergeImageOptions, MergeVideoOptions, MergeVideoPerInputOptions, MergeAudioOptions, MergeAudioPerInputOptions, ArchiveOptions, ImageWatermarkImageOptions, ImageWatermarkImageGifOptions, TextWatermarkImageOptions, CustomLumaVideoOptions, AudioOverlayAudioOptions, AudioOverlayVideoOptions, AudioWatermarkAudioOptions, AudioWatermarkVideoOptions, AudioToVideoAudioOptions, VideoWatermarkVideoOptions, VideoTextWatermarkVideoOptions, SplitImageGifOptions, SplitDocumentPdfOptions, SplitAudioOptions, SplitVideoOptions, } from '@giveitsmaller/contracts/operations';
|
|
37
37
|
export { ImageWatermarkImageAnchor, ImageWatermarkImageGifAnchor, TextWatermarkImageAnchor, TextWatermarkImageFontFamily, TextWatermarkImageWatermarkMode, AudioOverlayAudioMode, AudioOverlayVideoMode, AudioOverlayVideoNoAudioTrackBehaviour, AudioWatermarkAudioMethod, AudioWatermarkAudioRobustness, AudioWatermarkAudioDensity, AudioWatermarkVideoMethod, AudioWatermarkVideoRobustness, AudioWatermarkVideoDensity, AudioToVideoAudioOutputResolution, AudioToVideoAudioImageFit, AudioToVideoAudioOutputFormat, VideoWatermarkVideoAnchor, VideoTextWatermarkVideoFontFamily, VideoTextWatermarkVideoWatermarkMode, VideoTextWatermarkVideoAnchor, SplitImageGifOutputFormat, SplitDocumentPdfMode, SplitAudioMode, SplitAudioPrecision, SplitVideoMode, SplitVideoPrecision, } from '@giveitsmaller/contracts/operations';
|
|
38
38
|
export { archiveMetadata, audioOverlayMetadata, audioWatermarkMetadata, compressMetadata, convertMetadata, customLumaMetadata, imageWatermarkMetadata, mergeMetadata, textWatermarkMetadata, thumbnailMetadata, audioToVideoMetadata, videoWatermarkMetadata, videoTextWatermarkMetadata, splitMetadata, transformMetadata, } from '@giveitsmaller/contracts/operations';
|
|
39
39
|
export type { ConvertOptions, ThumbnailOptions, TransformOptions, TextWatermarkOptions, WatermarkOptions, WatermarkOverlay, WatermarkAnchor, } from './ergonomic/option_types.js';
|
package/dist/index.core.js
CHANGED
|
@@ -11,12 +11,28 @@ export { uploadSource, jobOutputSource, externalImportSource, connectionSource,
|
|
|
11
11
|
// Errors
|
|
12
12
|
export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislLongFormConcurrencyError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError,
|
|
13
13
|
// SDK-3 (Wb6ebOMM) — typed errors for the 3 resume-support endpoints.
|
|
14
|
-
GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError,
|
|
15
|
-
//
|
|
14
|
+
GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError,
|
|
15
|
+
// 4G4FaA9X — mapEach fan-out timed out mid-batch; carries the completed
|
|
16
|
+
// child ids + parent id so the caller can recover without a whole-batch re-run.
|
|
17
|
+
GislFanOutTimeoutError, GislAbortError,
|
|
18
|
+
// FF2b / tywwynmN — off-envelope failure base (mirrors PHP GislNetworkError);
|
|
16
19
|
// raised by the file-first HttpDownloader when an output URL cannot be read.
|
|
20
|
+
// t2qCrjdr: NEVER THROWN DIRECTLY any more — it is the hierarchy node the two
|
|
21
|
+
// subclasses below share, kept so existing `instanceof GislNetworkError`
|
|
22
|
+
// narrowing (including the SSE poll-fallback) is unchanged.
|
|
17
23
|
GislNetworkError,
|
|
24
|
+
// t2qCrjdr — the split. One `retryable` could not be honest for both a DNS
|
|
25
|
+
// failure and a 404, so each case is now its own class with its own answer.
|
|
26
|
+
GislTransportError, GislDownloadHttpError,
|
|
27
|
+
// The request never left the client — never retryable. TS detects only what
|
|
28
|
+
// it can see BEFORE the call (an unparseable URL); PHP also classifies
|
|
29
|
+
// PSR-18's RequestExceptionInterface, which `fetch` gives no equivalent of.
|
|
30
|
+
GislRequestNotSentError,
|
|
18
31
|
// T1 / wVU4xHx3 — local config-error tree (pre-I/O; sibling of GislApiError).
|
|
19
|
-
GislConfigError, GislMissingCredentialsError,
|
|
32
|
+
GislConfigError, GislMissingCredentialsError,
|
|
33
|
+
// VUozk5Bc — `streamEvents` on a client with no DECLARED stream host. The
|
|
34
|
+
// SDK refuses to derive `stream.*` from `api.*`; `run()` polls instead.
|
|
35
|
+
GislStreamHostNotDeclaredError, GislFeatureRequiresAuthError,
|
|
20
36
|
// T3 / cuecCmb5 — merge-compose local validation errors.
|
|
21
37
|
GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError,
|
|
22
38
|
// T6 / aDR1jnyZ — chain-cardinality validation (dormant until chain
|
package/dist/merge.d.ts
CHANGED
|
@@ -103,6 +103,35 @@ export interface MergeOptions {
|
|
|
103
103
|
readonly preset?: string;
|
|
104
104
|
/** Video output dimensions `WxH` (e.g. `"1920x1080"`); omit to inherit from inputs. Video merge only. */
|
|
105
105
|
readonly targetResolution?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Target output size (bytes, or a `'50MB'`-style string). Lowered to wire
|
|
108
|
+
* `target_size_bytes`, and the SDK also sets `encoding_mode: 'target_size'` alongside
|
|
109
|
+
* it. Video merge only.
|
|
110
|
+
*
|
|
111
|
+
* **UNITS ARE DECIMAL HERE (1 KB = 1000), UNLIKE `compress`.** `compress`'s
|
|
112
|
+
* `targetSize` parses the same strings as BINARY (1 KB = 1024), so `'50MB'` means
|
|
113
|
+
* 50,000,000 bytes on a merge and 52,428,800 bytes on a compress. That divergence is
|
|
114
|
+
* NOT deliberate — it contradicts the pinned convention that every human-readable
|
|
115
|
+
* size string in this SDK is binary — and it has a sharp edge: the contract floor for
|
|
116
|
+
* `target_size_bytes` is 1 MiB (1,048,576), so `'1MB'` here resolves to 1,000,000 and
|
|
117
|
+
* is rejected as below the minimum. Prefer an explicit byte count until this is
|
|
118
|
+
* reconciled. Tracked by `YOCz0i74`; changing it moves bytes for existing callers, so
|
|
119
|
+
* it is a deliberate decision rather than a silent correction.
|
|
120
|
+
*
|
|
121
|
+
* **NOT AVAILABLE FOR LONG INPUTS.** Merges whose summed input duration routes to
|
|
122
|
+
* the long-form Fargate path reject both keys — that path is single-pass-CRF by
|
|
123
|
+
* construction and two-pass target-size is unbuilt. The request fails during
|
|
124
|
+
* execution, and the SDK cannot warn earlier: the routing decision is made
|
|
125
|
+
* server-side at create-plan time, so there is nothing here to check it against.
|
|
126
|
+
* Short-form merges honour it normally.
|
|
127
|
+
*
|
|
128
|
+
* The contract CAN now express this — `per_class_availability` scopes an option to
|
|
129
|
+
* a processing class, vendored at v2.195.0 and pinned by
|
|
130
|
+
* `tests/unit/per-class-availability-conformance.test.ts`. That buys an honest 422
|
|
131
|
+
* from the API at CREATE rather than a job dying mid-execution; it does NOT become
|
|
132
|
+
* a client-side gate, because routing is still decided server-side and a duration
|
|
133
|
+
* heuristic here would be wrong at the boundary. Tracked by `zJN6XIi5`.
|
|
134
|
+
*/
|
|
106
135
|
readonly targetSize?: string | number;
|
|
107
136
|
readonly transitionDuration?: number;
|
|
108
137
|
readonly fps?: number;
|
package/dist/merge.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* assets both fail fast so the caller saves bandwidth on typo'd composes.
|
|
27
27
|
*/
|
|
28
28
|
import { uploadSource, jobOutputSource } from './types.js';
|
|
29
|
-
import { GislConfigError, GislNetworkError, GislPerInputOptionsNotSupportedError, GislTimeoutError, GislUndeclaredAssetError, GislUnusedAssetError, SseEndedWithoutTerminal, } from './errors.js';
|
|
29
|
+
import { GislConfigError, GislNetworkError, GislPerInputOptionsNotSupportedError, GislTimeoutError, GislUndeclaredAssetError, GislUnusedAssetError, GislStreamHostNotDeclaredError, SseEndedWithoutTerminal, } from './errors.js';
|
|
30
30
|
import { _cappedProbeTimeoutMs, _checkAborted, _consumeSseToTerminal, _detectCompressMedia, _parseMaxWait, _pollToTerminal, _projectResult, } from './builder.js';
|
|
31
31
|
import { Handle } from './handle.js';
|
|
32
32
|
/**
|
|
@@ -445,7 +445,17 @@ export class MergeBuilder {
|
|
|
445
445
|
// TDqmkWpX: poll-fallback ONLY on a clean SSE stream-end or a typed
|
|
446
446
|
// transport error; rethrow everything else (timeout, abort, API, an
|
|
447
447
|
// onProgress callback throw, anything unexpected) so it isn't masked.
|
|
448
|
-
if (!(err instanceof SseEndedWithoutTerminal ||
|
|
448
|
+
if (!(err instanceof SseEndedWithoutTerminal ||
|
|
449
|
+
err instanceof GislNetworkError ||
|
|
450
|
+
// VUozk5Bc: no stream host is DECLARED for this configuration (a
|
|
451
|
+
// configuration nothing declares; both named environments resolve as of
|
|
452
|
+
// contracts v2.195.0). That is not a failure to recover from,
|
|
453
|
+
// it is SSE being unavailable here, and polling is a working
|
|
454
|
+
// transport. Failing hard instead would strand every caller on a host
|
|
455
|
+
// nobody has declared yet. A DIRECT `streamEvents` caller still gets
|
|
456
|
+
// the hard error — they asked for the stream specifically; a `run()`
|
|
457
|
+
// caller asked for a result.
|
|
458
|
+
err instanceof GislStreamHostNotDeclaredError)) {
|
|
449
459
|
throw err;
|
|
450
460
|
}
|
|
451
461
|
}
|
package/dist/sse.d.ts
CHANGED
|
@@ -6,7 +6,29 @@ import type { GislSseEvent, GislSseParseFailure } from './types.js';
|
|
|
6
6
|
* - Chunk boundary buffering (events split across chunks)
|
|
7
7
|
* - Multi-line `data:` fields (concatenated with newlines)
|
|
8
8
|
* - Comment lines (`:` prefix) used as keep-alives
|
|
9
|
-
* - `retry:`
|
|
9
|
+
* - `id:` and `retry:` fields — IGNORED, and neither is surfaced on
|
|
10
|
+
* `GislSseEvent`
|
|
11
|
+
*
|
|
12
|
+
* 🔴 THIS SDK DOES NOT RECONNECT. It opens ONE stream and yields frames until
|
|
13
|
+
* the server ends it, the caller breaks, or the signal aborts. There is no
|
|
14
|
+
* retry loop, no backoff, and **no `Last-Event-ID` resumption** — so a dropped
|
|
15
|
+
* connection loses every event published while it was down, and the server
|
|
16
|
+
* cannot replay them.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ AN EARLIER VERSION OF THIS LINE READ "ignored, SDK manages its own
|
|
19
|
+
* reconnection", WHICH IS FALSE AND SAYS THE OPPOSITE OF THE TRUTH. A reader
|
|
20
|
+
* meeting it concluded retries were handled here. The poll-fallback in `run()`
|
|
21
|
+
* is a DIFFERENT TRANSPORT — it abandons the stream and polls
|
|
22
|
+
* `getWorkflowStatus` — not a reconnection, and it exists only on the
|
|
23
|
+
* ergonomic path. A direct `streamEvents` caller gets no recovery of any kind.
|
|
24
|
+
*
|
|
25
|
+
* ⇒ If you need to survive a drop, wrap this in your own loop AND reconcile
|
|
26
|
+
* the terminal state via `getWorkflowStatus` afterwards, because the gap is
|
|
27
|
+
* unrecoverable from the stream alone. See `docs/typescript/sse.md`.
|
|
28
|
+
*
|
|
29
|
+
* PHP and Python have carried this disclaimer since B2.2; TypeScript is the
|
|
30
|
+
* reference implementation both mirror and was the only one asserting the
|
|
31
|
+
* opposite (hub audit, 2026-08-29).
|
|
10
32
|
*
|
|
11
33
|
* `opts.signal` (optional): when it aborts, the underlying body reader is
|
|
12
34
|
* cancelled. This is the ONLY way to promptly stop a stream parked on a
|
package/dist/sse.js
CHANGED
|
@@ -5,7 +5,29 @@
|
|
|
5
5
|
* - Chunk boundary buffering (events split across chunks)
|
|
6
6
|
* - Multi-line `data:` fields (concatenated with newlines)
|
|
7
7
|
* - Comment lines (`:` prefix) used as keep-alives
|
|
8
|
-
* - `retry:`
|
|
8
|
+
* - `id:` and `retry:` fields — IGNORED, and neither is surfaced on
|
|
9
|
+
* `GislSseEvent`
|
|
10
|
+
*
|
|
11
|
+
* 🔴 THIS SDK DOES NOT RECONNECT. It opens ONE stream and yields frames until
|
|
12
|
+
* the server ends it, the caller breaks, or the signal aborts. There is no
|
|
13
|
+
* retry loop, no backoff, and **no `Last-Event-ID` resumption** — so a dropped
|
|
14
|
+
* connection loses every event published while it was down, and the server
|
|
15
|
+
* cannot replay them.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ AN EARLIER VERSION OF THIS LINE READ "ignored, SDK manages its own
|
|
18
|
+
* reconnection", WHICH IS FALSE AND SAYS THE OPPOSITE OF THE TRUTH. A reader
|
|
19
|
+
* meeting it concluded retries were handled here. The poll-fallback in `run()`
|
|
20
|
+
* is a DIFFERENT TRANSPORT — it abandons the stream and polls
|
|
21
|
+
* `getWorkflowStatus` — not a reconnection, and it exists only on the
|
|
22
|
+
* ergonomic path. A direct `streamEvents` caller gets no recovery of any kind.
|
|
23
|
+
*
|
|
24
|
+
* ⇒ If you need to survive a drop, wrap this in your own loop AND reconcile
|
|
25
|
+
* the terminal state via `getWorkflowStatus` afterwards, because the gap is
|
|
26
|
+
* unrecoverable from the stream alone. See `docs/typescript/sse.md`.
|
|
27
|
+
*
|
|
28
|
+
* PHP and Python have carried this disclaimer since B2.2; TypeScript is the
|
|
29
|
+
* reference implementation both mirror and was the only one asserting the
|
|
30
|
+
* opposite (hub audit, 2026-08-29).
|
|
9
31
|
*
|
|
10
32
|
* `opts.signal` (optional): when it aborts, the underlying body reader is
|
|
11
33
|
* cancelled. This is the ONLY way to promptly stop a stream parked on a
|
|
@@ -105,7 +127,55 @@ export async function* parseSseStream(response, opts = {}) {
|
|
|
105
127
|
continue;
|
|
106
128
|
}
|
|
107
129
|
if (line.startsWith(':')) {
|
|
108
|
-
// Comment line (keep-alive), skip
|
|
130
|
+
// Comment line (keep-alive), skip.
|
|
131
|
+
//
|
|
132
|
+
// ⚠️ DO NOT "FIX" THIS INTO SURFACING COMMENT FRAMES WITHOUT READING
|
|
133
|
+
// THIS. Dropping them is CORRECT per the SSE spec — a comment carries
|
|
134
|
+
// no event — but it is also LOAD-BEARING FOR A CONSUMER, and that
|
|
135
|
+
// consumer is in another repo.
|
|
136
|
+
//
|
|
137
|
+
// The frontend does NOT close its SSE reader on the terminal event:
|
|
138
|
+
// `workflow_completed` patches status and breaks WITHOUT aborting.
|
|
139
|
+
// What actually closes the stream is an IDLE WATCHDOG at 20s. That
|
|
140
|
+
// watchdog only fires because heartbeats are never yielded here, so
|
|
141
|
+
// they never re-arm it — roughly 20s after the last real event the
|
|
142
|
+
// reader closes and the PHP worker is released.
|
|
143
|
+
//
|
|
144
|
+
// HEARTBEAT ~16s vs IDLE TIMEOUT 20s: a FOUR-SECOND MARGIN that holds
|
|
145
|
+
// only while the heartbeats are invisible. Surfacing comment frames —
|
|
146
|
+
// a completely reasonable change, since EventSource semantics treat
|
|
147
|
+
// keep-alive comments as liveness signals that legitimately reset
|
|
148
|
+
// timeouts — would CONTINUOUSLY re-arm the watchdog. The stream would
|
|
149
|
+
// never go idle, and every completed job left on screen would hold a
|
|
150
|
+
// PHP worker for the full 570s. That is worker exhaustion from
|
|
151
|
+
// ordinary users leaving tabs open.
|
|
152
|
+
//
|
|
153
|
+
// A ONE-LINE CHANGE HERE SILENTLY CONVERTS THE FRONTEND FROM BOUNDED
|
|
154
|
+
// TO UNBOUNDED, and nothing on either side would flag it.
|
|
155
|
+
//
|
|
156
|
+
// ⚠️ THIS NOTE IS PERMANENT. DO NOT DELETE IT WHEN THE FRONTEND
|
|
157
|
+
// ADDS AN EXPLICIT CLOSE-ON-TERMINAL. That fix removes the TERMINAL
|
|
158
|
+
// case and does NOT remove the dependency, because closing finished
|
|
159
|
+
// streams was never the watchdog's job — that was a side effect
|
|
160
|
+
// nobody knew about until 2026-08-12.
|
|
161
|
+
//
|
|
162
|
+
// THE WATCHDOG'S ACTUAL JOB IS DETECTING A STREAM THAT HAS GONE
|
|
163
|
+
// SILENT WHILE THE WORKFLOW IS STILL RUNNING, and resuming the
|
|
164
|
+
// fallback poll. That case survives every fix in flight, and it is
|
|
165
|
+
// not exotic: a long operation between progress events produces it
|
|
166
|
+
// exactly — heartbeats flowing, no data frames, job still running.
|
|
167
|
+
//
|
|
168
|
+
// If this parser surfaced comment frames, heartbeats at ~16s would
|
|
169
|
+
// re-arm the 20s watchdog FOREVER and STALL DETECTION WOULD NEVER
|
|
170
|
+
// FIRE AT ALL, on a stream that is genuinely stuck. The UI would go
|
|
171
|
+
// on believing a dead job is fine. That is worse than the worker
|
|
172
|
+
// leak: a leak is bounded by 570s, a missed stall is not bounded at
|
|
173
|
+
// all.
|
|
174
|
+
//
|
|
175
|
+
// The PHP SDK does the same thing at GislClient.php (`$line[0] === ':'`),
|
|
176
|
+
// verified 2026-08-12 — the two languages agree on this axis, and
|
|
177
|
+
// they must stay agreed: this property governs whether a stuck
|
|
178
|
+
// stream is EVER detected, so a divergence here is not cosmetic.
|
|
109
179
|
continue;
|
|
110
180
|
}
|
|
111
181
|
const colonIndex = line.indexOf(':');
|
|
@@ -124,7 +194,10 @@ export async function* parseSseStream(response, opts = {}) {
|
|
|
124
194
|
dataLines.push(fieldValue);
|
|
125
195
|
break;
|
|
126
196
|
case 'retry':
|
|
127
|
-
// Ignored —
|
|
197
|
+
// Ignored — and NOT because something else honours it. This SDK
|
|
198
|
+
// never reconnects, so a server-suggested retry interval has no
|
|
199
|
+
// consumer here. (Was "SDK manages its own polling/reconnection",
|
|
200
|
+
// which claimed a behaviour that does not exist.)
|
|
128
201
|
break;
|
|
129
202
|
}
|
|
130
203
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,24 @@ import type { OperationType, OperationsSchemaResponse, OperationCapability, Outp
|
|
|
2
2
|
import type { JobInputV2RoleEnum, NotifyConfig } from '@giveitsmaller/contracts/openapi';
|
|
3
3
|
export interface GislClientConfig {
|
|
4
4
|
baseUrl: string;
|
|
5
|
+
/**
|
|
6
|
+
* Host for the **SSE event stream** (`streamEvents`). The stream is served
|
|
7
|
+
* from a SECOND public entry point, separate from `baseUrl`: the API host
|
|
8
|
+
* fronts an integration with no response-streaming mode.
|
|
9
|
+
*
|
|
10
|
+
* Setting this moves the stream and **nothing else** — uploads,
|
|
11
|
+
* workflow-create and downloads keep using `baseUrl`. That is the reason it
|
|
12
|
+
* exists as its own field rather than being expressed by overriding
|
|
13
|
+
* `baseUrl`, which moves every call.
|
|
14
|
+
*
|
|
15
|
+
* When omitted, `gisl.create()` resolves it from the `environment` against
|
|
16
|
+
* the contract-declared stream hosts. It is **never derived from `baseUrl`**
|
|
17
|
+
* — if nothing declares a stream host for your configuration, `streamEvents`
|
|
18
|
+
* throws `GislStreamHostNotDeclaredError` rather than silently reusing the
|
|
19
|
+
* API host, and `run()` falls back to polling. See
|
|
20
|
+
* `ENVIRONMENT_STREAM_ENDPOINTS`.
|
|
21
|
+
*/
|
|
22
|
+
streamBaseUrl?: string;
|
|
5
23
|
apiKey?: string;
|
|
6
24
|
headers?: Record<string, string>;
|
|
7
25
|
timeout?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giveitsmaller/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Node.js SDK for the GISL (Give It Smaller) file compression and processing API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@giveitsmaller/contracts": "^0.
|
|
34
|
+
"@giveitsmaller/contracts": "^0.69.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22",
|
|
@@ -47,5 +47,13 @@
|
|
|
47
47
|
"test": "vitest run",
|
|
48
48
|
"test:parity": "vitest run tests/parity",
|
|
49
49
|
"parity:update": "UPDATE_PARITY_FIXTURES=1 vitest run tests/parity"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://docs.giveitsmaller.com",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/giveitsmaller/typescript-sdk.git"
|
|
55
|
+
},
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/giveitsmaller/typescript-sdk/issues"
|
|
50
58
|
}
|
|
51
59
|
}
|