@giveitsmaller/sdk 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/builder.d.ts CHANGED
@@ -253,10 +253,26 @@ export interface RunOptions {
253
253
  readonly useSSE?: boolean;
254
254
  /** Override the poll interval used by the fallback (ms). */
255
255
  readonly pollIntervalMs?: number;
256
+ /**
257
+ * Best-effort probe-before-create for a VIDEO upload that went multipart:
258
+ * after upload, before createWorkflow, wait for the server's probe to land
259
+ * so it admits the parallel video split. Default `true`; set `false` to
260
+ * skip the wait entirely. Never-bounce — a give-up just proceeds to create.
261
+ */
262
+ readonly probeBeforeCreate?: boolean;
263
+ /** Overall timeout (ms) for the probe-before-create wait. */
264
+ readonly probeTimeoutMs?: number;
256
265
  }
257
266
  export interface SubmitOptions {
258
267
  /** Webhook URL — wired to `WorkflowCreateRequest.callback_url`. */
259
268
  readonly webhook: string;
269
+ /**
270
+ * Best-effort probe-before-create for a VIDEO upload that went multipart.
271
+ * Default `true`; set `false` to skip the wait. See {@link RunOptions}.
272
+ */
273
+ readonly probeBeforeCreate?: boolean;
274
+ /** Overall timeout (ms) for the probe-before-create wait. */
275
+ readonly probeTimeoutMs?: number;
260
276
  }
261
277
  /**
262
278
  * Captures the (op-type, input, options) tuple for one ergonomic-layer
@@ -405,6 +421,17 @@ export declare function _projectResult(status: WorkflowStatusResponse, jobDownlo
405
421
  }[], appliedOptions: Record<string, unknown>, resolvedOptionsOverride?: ResolvedOptions): Result;
406
422
  /** @internal — exported for reuse by `merge.ts` (T3) and future builders. */
407
423
  export declare function _checkAborted(signal: AbortSignal | undefined): void;
424
+ /**
425
+ * Cap a best-effort probe-before-create timeout to the remaining `maxWait`
426
+ * budget so the probe wait can never push createWorkflow past the caller's
427
+ * deadline. Under a deadline an UNSET `probeTimeoutMs` becomes the remaining
428
+ * budget (never the 30s waitForProbe default); a set value is clamped to the
429
+ * remaining budget. With no deadline (the `submit()` fire-and-forget path),
430
+ * `probeTimeoutMs` passes through unchanged.
431
+ *
432
+ * @internal — exported for reuse by `file-first.ts` + `merge.ts`.
433
+ */
434
+ export declare function _cappedProbeTimeoutMs(probeTimeoutMs: number | undefined, deadline: number | undefined): number | undefined;
408
435
  /**
409
436
  * Parse a `maxWait` argument: number = milliseconds; string with suffix
410
437
  * `ms` / `s` / `m` / `h`. Throws if the string is malformed.
package/dist/builder.js CHANGED
@@ -275,6 +275,24 @@ export class OperationBuilder {
275
275
  if (Date.now() >= deadline) {
276
276
  throw new GislTimeoutError(`Upload completed but maxWait elapsed before workflow could be created`);
277
277
  }
278
+ // Best-effort probe-before-create for a multipart video upload (never-bounce).
279
+ // Capped to the remaining maxWait budget so a slow probe cannot push
280
+ // createWorkflow past the caller's deadline.
281
+ await this.client.maybeWaitForVideoProbe(uploadResp.fileId, {
282
+ enabled: options.probeBeforeCreate ?? true,
283
+ isVideo: _detectCompressMedia(this.input) === 'video',
284
+ sizeBytes: uploadResp.sizeBytes,
285
+ timeoutMs: _cappedProbeTimeoutMs(options.probeTimeoutMs, deadline),
286
+ signal,
287
+ });
288
+ // A cancel arriving during the FINAL successful probe request must not still
289
+ // create the workflow (maybeWaitForVideoProbe returns landed without a final
290
+ // abort re-check), so check here BEFORE createWorkflow.
291
+ _checkAborted(signal);
292
+ // RE-CHECK the deadline AFTER the probe wait (it consumes time).
293
+ if (Date.now() >= deadline) {
294
+ throw new GislTimeoutError('Probe wait completed but maxWait elapsed before workflow could be created');
295
+ }
278
296
  // 2. Build + create the workflow.
279
297
  const job = {
280
298
  id: 'op',
@@ -320,6 +338,13 @@ export class OperationBuilder {
320
338
  // call before the upload — same fail-early contract as run().
321
339
  const resolved = this._resolve();
322
340
  const uploadResp = await this.client.uploadFile(this.input);
341
+ // Best-effort probe-before-create for a multipart video upload (never-bounce).
342
+ await this.client.maybeWaitForVideoProbe(uploadResp.fileId, {
343
+ enabled: options.probeBeforeCreate ?? true,
344
+ isVideo: _detectCompressMedia(this.input) === 'video',
345
+ sizeBytes: uploadResp.sizeBytes,
346
+ timeoutMs: options.probeTimeoutMs,
347
+ });
323
348
  const job = {
324
349
  id: 'op',
325
350
  source: uploadSource(uploadResp.fileId),
@@ -759,6 +784,23 @@ export function _checkAborted(signal) {
759
784
  throw new DOMException('Aborted', 'AbortError');
760
785
  }
761
786
  }
787
+ /**
788
+ * Cap a best-effort probe-before-create timeout to the remaining `maxWait`
789
+ * budget so the probe wait can never push createWorkflow past the caller's
790
+ * deadline. Under a deadline an UNSET `probeTimeoutMs` becomes the remaining
791
+ * budget (never the 30s waitForProbe default); a set value is clamped to the
792
+ * remaining budget. With no deadline (the `submit()` fire-and-forget path),
793
+ * `probeTimeoutMs` passes through unchanged.
794
+ *
795
+ * @internal — exported for reuse by `file-first.ts` + `merge.ts`.
796
+ */
797
+ export function _cappedProbeTimeoutMs(probeTimeoutMs, deadline) {
798
+ if (deadline === undefined) {
799
+ return probeTimeoutMs;
800
+ }
801
+ const remaining = Math.max(0, deadline - Date.now());
802
+ return probeTimeoutMs !== undefined ? Math.min(probeTimeoutMs, remaining) : remaining;
803
+ }
762
804
  async function sleep(ms, signal) {
763
805
  return await new Promise((resolve, reject) => {
764
806
  const t = setTimeout(() => {
package/dist/client.d.ts CHANGED
@@ -352,6 +352,21 @@ export declare class GislClient {
352
352
  * the wait returns.
353
353
  */
354
354
  waitForProbe(fileId: string, options?: ProbeWaitOptions): Promise<ProbeWaitResult>;
355
+ /**
356
+ * Best-effort probe-before-create for a VIDEO upload that went multipart.
357
+ * No-op unless enabled AND isVideo AND the upload exceeded the multipart
358
+ * threshold (i.e. it was a multipart upload — small single-shot videos skip
359
+ * the wait). Delegates to {@link waitForProbe} (never-bounce): a give-up just
360
+ * returns; genuine failures / caller abort propagate. The caller passes
361
+ * `isVideo` so the low-level client never imports ergonomic media detection.
362
+ */
363
+ maybeWaitForVideoProbe(fileId: string, opts: {
364
+ enabled: boolean;
365
+ isVideo: boolean;
366
+ sizeBytes?: number;
367
+ timeoutMs?: number;
368
+ signal?: AbortSignal;
369
+ }): Promise<void>;
355
370
  /**
356
371
  * Probe N uploaded files in parallel and partition the results by
357
372
  * outcome. Returns `{ ok, rejected, errors }` so the caller can
package/dist/client.js CHANGED
@@ -2136,6 +2136,21 @@ export class GislClient {
2136
2136
  return { landed: false, reason: 'timeout' };
2137
2137
  }
2138
2138
  }
2139
+ /**
2140
+ * Best-effort probe-before-create for a VIDEO upload that went multipart.
2141
+ * No-op unless enabled AND isVideo AND the upload exceeded the multipart
2142
+ * threshold (i.e. it was a multipart upload — small single-shot videos skip
2143
+ * the wait). Delegates to {@link waitForProbe} (never-bounce): a give-up just
2144
+ * returns; genuine failures / caller abort propagate. The caller passes
2145
+ * `isVideo` so the low-level client never imports ergonomic media detection.
2146
+ */
2147
+ async maybeWaitForVideoProbe(fileId, opts) {
2148
+ if (!opts.enabled || !opts.isVideo)
2149
+ return;
2150
+ if (opts.sizeBytes === undefined || opts.sizeBytes <= this.multipartThreshold)
2151
+ return;
2152
+ await this.waitForProbe(fileId, { timeoutMs: opts.timeoutMs, signal: opts.signal });
2153
+ }
2139
2154
  /**
2140
2155
  * Probe N uploaded files in parallel and partition the results by
2141
2156
  * outcome. Returns `{ ok, rejected, errors }` so the caller can
@@ -1,8 +1,14 @@
1
1
  import type { ResolvedOptions } from '../builder.js';
2
2
  import type { OptimizeFor } from '../generated/sdk_spec/enums.js';
3
3
  import { type PresetDefaults, type PresetMedia, type PresetOp } from './presets/index.js';
4
- /** Bumped on any change to a `*PresetOptions.shippedDefaultsFor(...)` cell value. */
5
- export declare const PRESET_VERSION = "1.0";
4
+ /**
5
+ * Bumped on any change to a `*PresetOptions.shippedDefaultsFor(...)` cell value.
6
+ * Must track the contracts `sdk-spec/version.yaml` `presetVersion` (mirrored in
7
+ * the generated `sdk_spec/version.ts`). 1.0 → 1.2 on the contracts v2.71.0
8
+ * (video_compress `audioBitrate` dropped) + v2.73.0 (image Size/Balanced
9
+ * `outputFormat` Smallest/Auto → Original — VcPeRWdD facade self-422 guard) cuts.
10
+ */
11
+ export declare const PRESET_VERSION = "1.2";
6
12
  /**
7
13
  * Inputs to {@link resolveCompressOptions}. `media` selects which leaf
8
14
  * DTO drives sdkDefault + clientDefault lookups + invalid-combo
@@ -37,8 +37,14 @@
37
37
  import { sha256Hex } from '../sha256.js';
38
38
  import { GislConfigError } from '../errors.js';
39
39
  import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions, DocumentPdfCompressPresetOptions, DocumentOfficeCompressPresetOptions, DocumentOdfCompressPresetOptions, DocumentEpubCompressPresetOptions, } from './presets/index.js';
40
- /** Bumped on any change to a `*PresetOptions.shippedDefaultsFor(...)` cell value. */
41
- export const PRESET_VERSION = '1.0';
40
+ /**
41
+ * Bumped on any change to a `*PresetOptions.shippedDefaultsFor(...)` cell value.
42
+ * Must track the contracts `sdk-spec/version.yaml` `presetVersion` (mirrored in
43
+ * the generated `sdk_spec/version.ts`). 1.0 → 1.2 on the contracts v2.71.0
44
+ * (video_compress `audioBitrate` dropped) + v2.73.0 (image Size/Balanced
45
+ * `outputFormat` Smallest/Auto → Original — VcPeRWdD facade self-422 guard) cuts.
46
+ */
47
+ export const PRESET_VERSION = '1.2';
42
48
  // ---------------------------------------------------------------------------
43
49
  // Wire-field alias map (declarative — NOT generic toSnakeCase).
44
50
  // ---------------------------------------------------------------------------
@@ -396,6 +396,8 @@ export declare class Recipe {
396
396
  onProgress?: (event: ProgressEvent) => void;
397
397
  signal?: AbortSignal;
398
398
  pollIntervalMs?: number;
399
+ probeBeforeCreate?: boolean;
400
+ probeTimeoutMs?: number;
399
401
  }): Promise<RunResult>;
400
402
  /**
401
403
  * Fire-and-forget the recipe: upload the input (when required), create the
@@ -409,8 +411,14 @@ export declare class Recipe {
409
411
  * sent. Mirrors the PHP `Recipe.submit()`.
410
412
  *
411
413
  * @param webhook Absolute callback URL the server POSTs lifecycle events to.
414
+ * @param options Opt-out (`probeBeforeCreate: false`) / tune (`probeTimeoutMs`)
415
+ * the best-effort video probe-before-create. Kept as a 2nd optional param so
416
+ * the existing positional `webhook` arg stays backward compatible.
412
417
  */
413
- submit(webhook?: string): Promise<Handle>;
418
+ submit(webhook?: string, options?: {
419
+ probeBeforeCreate?: boolean;
420
+ probeTimeoutMs?: number;
421
+ }): Promise<Handle>;
414
422
  /**
415
423
  * Resolve the upload id (verbatim for a pre-uploaded id; uploading a path /
416
424
  * blob otherwise, emitting `{phase:'upload'}` progress), check the post-upload
@@ -530,6 +538,8 @@ export declare class FilesRecipe {
530
538
  onProgress?: (event: ProgressEvent) => void;
531
539
  signal?: AbortSignal;
532
540
  pollIntervalMs?: number;
541
+ probeBeforeCreate?: boolean;
542
+ probeTimeoutMs?: number;
533
543
  }): Promise<RunResult>;
534
544
  /**
535
545
  * Fire-and-forget the fan-out: upload every input, create ONE multi-job
@@ -546,8 +556,13 @@ export declare class FilesRecipe {
546
556
  * Mirrors the single-file {@link Recipe.submit}.
547
557
  *
548
558
  * @param webhook Absolute callback URL the server POSTs lifecycle events to.
559
+ * @param options Opt-out / tune the best-effort video probe-before-create
560
+ * (2nd optional param so the positional `webhook` arg stays compatible).
549
561
  */
550
- submit(webhook?: string): Promise<Handle>;
562
+ submit(webhook?: string, options?: {
563
+ probeBeforeCreate?: boolean;
564
+ probeTimeoutMs?: number;
565
+ }): Promise<Handle>;
551
566
  /**
552
567
  * Upload every input (verbatim for a pre-uploaded id; uploading a path /
553
568
  * blob otherwise, emitting `{phase:'upload'}` progress) then create ONE
@@ -633,13 +648,22 @@ export declare class MergedRecipe {
633
648
  onProgress?: (event: ProgressEvent) => void;
634
649
  signal?: AbortSignal;
635
650
  pollIntervalMs?: number;
651
+ probeBeforeCreate?: boolean;
652
+ probeTimeoutMs?: number;
636
653
  }): Promise<RunResult>;
637
654
  /**
638
655
  * Fire-and-forget: upload + create the merge workflow (wiring `webhook` into
639
656
  * `callback_url` when given), return a client-bound {@link Handle}. Does NOT
640
657
  * wait for terminal status. Mirrors {@link Recipe.submit}.
658
+ *
659
+ * @param webhook Absolute callback URL the server POSTs lifecycle events to.
660
+ * @param options Opt-out / tune the best-effort video probe-before-create
661
+ * (2nd optional param so the positional `webhook` arg stays compatible).
641
662
  */
642
- submit(webhook?: string): Promise<Handle>;
663
+ submit(webhook?: string, options?: {
664
+ probeBeforeCreate?: boolean;
665
+ probeTimeoutMs?: number;
666
+ }): Promise<Handle>;
643
667
  /**
644
668
  * Upload every input (verbatim for a pre-uploaded id; uploading a path / blob
645
669
  * otherwise, emitting `{phase:'upload'}` progress) then create ONE merge
@@ -723,13 +747,22 @@ export declare class ArchivedRecipe {
723
747
  onProgress?: (event: ProgressEvent) => void;
724
748
  signal?: AbortSignal;
725
749
  pollIntervalMs?: number;
750
+ probeBeforeCreate?: boolean;
751
+ probeTimeoutMs?: number;
726
752
  }): Promise<RunResult>;
727
753
  /**
728
754
  * Fire-and-forget: upload + create the archive workflow (wiring `webhook` into
729
755
  * `callback_url` when given), return a client-bound {@link Handle}. Mirrors
730
756
  * {@link MergedRecipe.submit}.
757
+ *
758
+ * @param webhook Absolute callback URL the server POSTs lifecycle events to.
759
+ * @param options Opt-out / tune the best-effort video probe-before-create
760
+ * (2nd optional param so the positional `webhook` arg stays compatible).
731
761
  */
732
- submit(webhook?: string): Promise<Handle>;
762
+ submit(webhook?: string, options?: {
763
+ probeBeforeCreate?: boolean;
764
+ probeTimeoutMs?: number;
765
+ }): Promise<Handle>;
733
766
  private _uploadAllAndCreate;
734
767
  /**
735
768
  * Reject an invalid bundle BEFORE any upload fires — the archive schema allows
@@ -10,7 +10,7 @@
10
10
  * Mirrors `packages/php/src/FileFirst/*`.
11
11
  */
12
12
  import { GislConfigError, GislNetworkError, GislNoSuchKeyError, GislSinkError, GislTimeoutError, SseEndedWithoutTerminal } from './errors.js';
13
- import { _detectCompressMedia, _detectAudioLossless, _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, _checkAborted, } from './builder.js';
13
+ import { _detectCompressMedia, _detectAudioLossless, _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, _checkAborted, _cappedProbeTimeoutMs, } from './builder.js';
14
14
  import { LazyHttpDownloader } from './lazy-downloader.js';
15
15
  import { resolveCompressOptions, } from './ergonomic/preset_resolver.js';
16
16
  import { OptimizeFor } from './generated/sdk_spec/enums.js';
@@ -529,7 +529,7 @@ export class Recipe {
529
529
  const deadline = Date.now() + _parseMaxWait(options.maxWait ?? 300_000);
530
530
  // 1+2. Upload (when required) + create the workflow. Shared with submit()
531
531
  // (which passes a webhook → callback_url). run() passes no webhook.
532
- const created = await this._uploadAndCreate(undefined, deadline, onProgress, signal);
532
+ const created = await this._uploadAndCreate(undefined, deadline, onProgress, signal, options.probeBeforeCreate, options.probeTimeoutMs);
533
533
  // 3. Wait to terminal status — SSE first, poll on a genuine SSE error.
534
534
  // Caller-aborted + deadline-elapsed errors MUST propagate (not transient).
535
535
  let finalStatus;
@@ -587,8 +587,11 @@ export class Recipe {
587
587
  * sent. Mirrors the PHP `Recipe.submit()`.
588
588
  *
589
589
  * @param webhook Absolute callback URL the server POSTs lifecycle events to.
590
+ * @param options Opt-out (`probeBeforeCreate: false`) / tune (`probeTimeoutMs`)
591
+ * the best-effort video probe-before-create. Kept as a 2nd optional param so
592
+ * the existing positional `webhook` arg stays backward compatible.
590
593
  */
591
- async submit(webhook) {
594
+ async submit(webhook, options) {
592
595
  if (this.client === undefined) {
593
596
  throw new GislConfigError('Recipe.submit() requires a client; build the recipe via gisl().file(...) rather than constructing Recipe directly.', { reason: 'no_client' });
594
597
  }
@@ -597,7 +600,7 @@ export class Recipe {
597
600
  // own request timeout, not an arbitrary submit-side cap. Pass `undefined`
598
601
  // so the post-upload deadline check is skipped: a 300s cap here would throw
599
602
  // on a slow-but-successful big upload before createWorkflow (codex).
600
- const created = await this._uploadAndCreate(webhook, undefined);
603
+ const created = await this._uploadAndCreate(webhook, undefined, undefined, undefined, options?.probeBeforeCreate, options?.probeTimeoutMs);
601
604
  return new Handle(created.workflowId, created.webhookSecret != null ? created.webhookSecret : undefined, this.client, this.recipeKey ?? null);
602
605
  }
603
606
  /**
@@ -610,10 +613,13 @@ export class Recipe {
610
613
  * The post-upload deadline check carries a prior codex fix (9a117f04eb59): a
611
614
  * slow upload must not proceed to createWorkflow past the deadline.
612
615
  */
613
- async _uploadAndCreate(webhook, deadline, onProgress, signal) {
616
+ async _uploadAndCreate(webhook, deadline, onProgress, signal, probeBeforeCreate, probeTimeoutMs) {
614
617
  // 1. Resolve the upload id. A pre-uploaded id skips the upload entirely;
615
618
  // a path / blob is uploaded now, emitting {phase:'upload'} progress.
616
619
  let fileId;
620
+ // A pre-uploaded id carries no local mime/size, so the video probe-gate is
621
+ // skipped for it (no `up` to read sizeBytes from).
622
+ let uploadSizeBytes;
617
623
  if (this.input.kind === 'uploadId') {
618
624
  fileId = this.input.fileId;
619
625
  }
@@ -630,6 +636,7 @@ export class Recipe {
630
636
  : {}),
631
637
  });
632
638
  fileId = up.fileId;
639
+ uploadSizeBytes = up.sizeBytes;
633
640
  }
634
641
  _checkAborted(signal);
635
642
  // run() passes a whole-run deadline (the codex 9a117f04eb59 fix: a slow
@@ -638,6 +645,35 @@ export class Recipe {
638
645
  if (deadline !== undefined && Date.now() >= deadline) {
639
646
  throw new GislTimeoutError('Upload completed but maxWait elapsed before workflow could be created');
640
647
  }
648
+ // Best-effort probe-before-create: for a VIDEO upload that went multipart,
649
+ // let the server see the codec + duration before createWorkflow so it
650
+ // admits the ~3× parallel split. Never-bounce — a give-up just proceeds.
651
+ // The probe wait is CAPPED to the remaining maxWait budget so a slow probe
652
+ // cannot push createWorkflow past the caller's deadline (an unset
653
+ // probeTimeoutMs under a deadline becomes the remaining budget, never the
654
+ // 30s default).
655
+ // Skip a pre-uploaded (`uploadId`) input entirely — no local mime/size to
656
+ // gate on (mirrors the multi-input seams, which omit uploadId inputs from
657
+ // their probe targets).
658
+ if (this.input.kind !== 'uploadId') {
659
+ await this.client.maybeWaitForVideoProbe(fileId, {
660
+ enabled: probeBeforeCreate ?? true,
661
+ isVideo: this.compressMediaHint() === 'video',
662
+ sizeBytes: uploadSizeBytes,
663
+ timeoutMs: _cappedProbeTimeoutMs(probeTimeoutMs, deadline),
664
+ signal,
665
+ });
666
+ }
667
+ // A cancel arriving during the FINAL successful probe request must not still
668
+ // create the workflow (maybeWaitForVideoProbe returns landed without a final
669
+ // abort re-check), so check here BEFORE createWorkflow.
670
+ _checkAborted(signal);
671
+ // RE-CHECK the deadline AFTER the probe wait: the wait itself consumes time,
672
+ // so a workflow must not be created past maxWait even when the wait was
673
+ // capped (mirrors the post-upload check above).
674
+ if (deadline !== undefined && Date.now() >= deadline) {
675
+ throw new GislTimeoutError('Probe wait completed but maxWait elapsed before workflow could be created');
676
+ }
641
677
  // 2. Create the workflow from the lowered payload (callback_url built into
642
678
  // the payload at construction when a webhook is given).
643
679
  const payload = this.toWorkflowPayload(fileId, webhook);
@@ -884,7 +920,7 @@ export class FilesRecipe {
884
920
  const deadline = Date.now() + _parseMaxWait(options.maxWait ?? 300_000);
885
921
  // 1+2. Upload EVERY input + create ONE multi-job workflow. Shared with
886
922
  // submit() (which passes a webhook → callback_url and no deadline).
887
- const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal);
923
+ const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal, options.probeBeforeCreate, options.probeTimeoutMs);
888
924
  // 3. Wait to terminal status — SSE first, poll on a genuine SSE error.
889
925
  // `partially_failed` is a normal terminal state here (the helper treats it
890
926
  // as terminal); only caller-aborted / deadline / API errors propagate.
@@ -941,12 +977,14 @@ export class FilesRecipe {
941
977
  * Mirrors the single-file {@link Recipe.submit}.
942
978
  *
943
979
  * @param webhook Absolute callback URL the server POSTs lifecycle events to.
980
+ * @param options Opt-out / tune the best-effort video probe-before-create
981
+ * (2nd optional param so the positional `webhook` arg stays compatible).
944
982
  */
945
- async submit(webhook) {
983
+ async submit(webhook, options) {
946
984
  if (this.client === undefined) {
947
985
  throw new GislConfigError('FilesRecipe.submit() requires a client; build the fan-out via gisl().files(...) rather than constructing FilesRecipe directly.', { reason: 'no_client' });
948
986
  }
949
- const created = await this._uploadAllAndCreate(webhook, undefined);
987
+ const created = await this._uploadAllAndCreate(webhook, undefined, undefined, undefined, options?.probeBeforeCreate, options?.probeTimeoutMs);
950
988
  return new Handle(created.workflowId, created.webhookSecret != null ? created.webhookSecret : undefined, this.client, null);
951
989
  }
952
990
  /**
@@ -961,8 +999,11 @@ export class FilesRecipe {
961
999
  * createWorkflow past maxWait); `submit()` passes `undefined`, so the
962
1000
  * deadline checks are skipped.
963
1001
  */
964
- async _uploadAllAndCreate(webhook, deadline, onProgress, signal) {
1002
+ async _uploadAllAndCreate(webhook, deadline, onProgress, signal, probeBeforeCreate, probeTimeoutMs) {
965
1003
  const fileIds = [];
1004
+ // Track each freshly-uploaded input's probe-gate inputs (a pre-uploaded id
1005
+ // carries no local mime/size, so it is excluded — never probed).
1006
+ const probeTargets = [];
966
1007
  for (const input of this.inputs) {
967
1008
  // Fail fast between uploads — a deadline that elapses mid-batch should
968
1009
  // not force every remaining input to upload before throwing.
@@ -986,12 +1027,39 @@ export class FilesRecipe {
986
1027
  : {}),
987
1028
  });
988
1029
  fileIds.push(up.fileId);
1030
+ probeTargets.push({
1031
+ fileId: up.fileId,
1032
+ isVideo: _detectCompressMedia(source) === 'video',
1033
+ sizeBytes: up.sizeBytes,
1034
+ });
989
1035
  }
990
1036
  }
991
1037
  _checkAborted(signal);
992
1038
  if (deadline !== undefined && Date.now() >= deadline) {
993
1039
  throw new GislTimeoutError('Uploads completed but maxWait elapsed before workflow could be created');
994
1040
  }
1041
+ // Best-effort probe-before-create for the multipart-video inputs. Run the
1042
+ // waits CONCURRENTLY (Promise.all): each is bounded by the SAME capped
1043
+ // timeout, so the aggregate wall-clock stays ~timeout rather than N×timeout.
1044
+ // The cap is the remaining maxWait budget so the waits cannot push
1045
+ // createWorkflow past the caller's deadline. Never-bounce, so a give-up
1046
+ // just proceeds.
1047
+ const cappedProbeTimeoutMs = _cappedProbeTimeoutMs(probeTimeoutMs, deadline);
1048
+ await Promise.all(probeTargets.map((t) => this.client.maybeWaitForVideoProbe(t.fileId, {
1049
+ enabled: probeBeforeCreate ?? true,
1050
+ isVideo: t.isVideo,
1051
+ sizeBytes: t.sizeBytes,
1052
+ timeoutMs: cappedProbeTimeoutMs,
1053
+ signal,
1054
+ })));
1055
+ // A cancel arriving during a FINAL successful probe request must not still
1056
+ // create the workflow (the probe waits return landed without a final abort
1057
+ // re-check), so check here BEFORE createWorkflow.
1058
+ _checkAborted(signal);
1059
+ // RE-CHECK the deadline AFTER the probe waits (they consume time).
1060
+ if (deadline !== undefined && Date.now() >= deadline) {
1061
+ throw new GislTimeoutError('Probe wait completed but maxWait elapsed before workflow could be created');
1062
+ }
995
1063
  const created = await this.client.createWorkflow(this.toWorkflowPayload(fileIds, webhook));
996
1064
  _checkAborted(signal);
997
1065
  return created;
@@ -1129,7 +1197,7 @@ export class MergedRecipe {
1129
1197
  throw new GislConfigError('MergedRecipe.run() requires a client; build the merge via gisl().files(...).merge(...) rather than constructing MergedRecipe directly.', { reason: 'no_client' });
1130
1198
  }
1131
1199
  const deadline = Date.now() + _parseMaxWait(options.maxWait ?? 300_000);
1132
- const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal);
1200
+ const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal, options.probeBeforeCreate, options.probeTimeoutMs);
1133
1201
  let finalStatus;
1134
1202
  try {
1135
1203
  finalStatus = await _consumeSseToTerminal(this.client, {
@@ -1170,12 +1238,16 @@ export class MergedRecipe {
1170
1238
  * Fire-and-forget: upload + create the merge workflow (wiring `webhook` into
1171
1239
  * `callback_url` when given), return a client-bound {@link Handle}. Does NOT
1172
1240
  * wait for terminal status. Mirrors {@link Recipe.submit}.
1241
+ *
1242
+ * @param webhook Absolute callback URL the server POSTs lifecycle events to.
1243
+ * @param options Opt-out / tune the best-effort video probe-before-create
1244
+ * (2nd optional param so the positional `webhook` arg stays compatible).
1173
1245
  */
1174
- async submit(webhook) {
1246
+ async submit(webhook, options) {
1175
1247
  if (this.client === undefined) {
1176
1248
  throw new GislConfigError('MergedRecipe.submit() requires a client; build the merge via gisl().files(...).merge(...) rather than constructing MergedRecipe directly.', { reason: 'no_client' });
1177
1249
  }
1178
- const created = await this._uploadAllAndCreate(webhook, undefined);
1250
+ const created = await this._uploadAllAndCreate(webhook, undefined, undefined, undefined, options?.probeBeforeCreate, options?.probeTimeoutMs);
1179
1251
  return new Handle(created.workflowId, created.webhookSecret != null ? created.webhookSecret : undefined, this.client, null);
1180
1252
  }
1181
1253
  // ---------------------------------------------------------------------------
@@ -1185,9 +1257,13 @@ export class MergedRecipe {
1185
1257
  * workflow. Rejects fewer than 2 inputs BEFORE any upload fires. Shared first
1186
1258
  * half of {@link run} + {@link submit}.
1187
1259
  */
1188
- async _uploadAllAndCreate(webhook, deadline, onProgress, signal) {
1260
+ async _uploadAllAndCreate(webhook, deadline, onProgress, signal, probeBeforeCreate, probeTimeoutMs) {
1189
1261
  this.validatePreUpload();
1190
1262
  const fileIds = [];
1263
+ // Per-input video detection: merge's inferMediaKind decides the OUTPUT
1264
+ // media, not each input's, so detect per input via _detectCompressMedia.
1265
+ // A pre-uploaded id carries no local mime/size, so it is never probed.
1266
+ const probeTargets = [];
1191
1267
  for (const input of this.inputs) {
1192
1268
  _checkAborted(signal);
1193
1269
  if (deadline !== undefined && Date.now() >= deadline) {
@@ -1209,12 +1285,36 @@ export class MergedRecipe {
1209
1285
  : {}),
1210
1286
  });
1211
1287
  fileIds.push(up.fileId);
1288
+ probeTargets.push({
1289
+ fileId: up.fileId,
1290
+ isVideo: _detectCompressMedia(source) === 'video',
1291
+ sizeBytes: up.sizeBytes,
1292
+ });
1212
1293
  }
1213
1294
  }
1214
1295
  _checkAborted(signal);
1215
1296
  if (deadline !== undefined && Date.now() >= deadline) {
1216
1297
  throw new GislTimeoutError('Uploads completed but maxWait elapsed before the merge workflow could be created');
1217
1298
  }
1299
+ // Best-effort, concurrent probe-before-create for the multipart-video
1300
+ // inputs (never-bounce; each capped to the remaining maxWait budget so the
1301
+ // waits cannot push createWorkflow past the caller's deadline).
1302
+ const cappedProbeTimeoutMs = _cappedProbeTimeoutMs(probeTimeoutMs, deadline);
1303
+ await Promise.all(probeTargets.map((t) => this.client.maybeWaitForVideoProbe(t.fileId, {
1304
+ enabled: probeBeforeCreate ?? true,
1305
+ isVideo: t.isVideo,
1306
+ sizeBytes: t.sizeBytes,
1307
+ timeoutMs: cappedProbeTimeoutMs,
1308
+ signal,
1309
+ })));
1310
+ // A cancel arriving during a FINAL successful probe request must not still
1311
+ // create the workflow (the probe waits return landed without a final abort
1312
+ // re-check), so check here BEFORE createWorkflow.
1313
+ _checkAborted(signal);
1314
+ // RE-CHECK the deadline AFTER the probe waits (they consume time).
1315
+ if (deadline !== undefined && Date.now() >= deadline) {
1316
+ throw new GislTimeoutError('Probe wait completed but maxWait elapsed before the merge workflow could be created');
1317
+ }
1218
1318
  const created = await this.client.createWorkflow(this.toWorkflowPayload(fileIds, webhook));
1219
1319
  _checkAborted(signal);
1220
1320
  return created;
@@ -1369,7 +1469,7 @@ export class ArchivedRecipe {
1369
1469
  throw new GislConfigError('ArchivedRecipe.run() requires a client; build the bundle via gisl().files(...).archive(...) rather than constructing ArchivedRecipe directly.', { reason: 'no_client' });
1370
1470
  }
1371
1471
  const deadline = Date.now() + _parseMaxWait(options.maxWait ?? 300_000);
1372
- const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal);
1472
+ const created = await this._uploadAllAndCreate(undefined, deadline, onProgress, signal, options.probeBeforeCreate, options.probeTimeoutMs);
1373
1473
  let finalStatus;
1374
1474
  try {
1375
1475
  finalStatus = await _consumeSseToTerminal(this.client, {
@@ -1409,18 +1509,26 @@ export class ArchivedRecipe {
1409
1509
  * Fire-and-forget: upload + create the archive workflow (wiring `webhook` into
1410
1510
  * `callback_url` when given), return a client-bound {@link Handle}. Mirrors
1411
1511
  * {@link MergedRecipe.submit}.
1512
+ *
1513
+ * @param webhook Absolute callback URL the server POSTs lifecycle events to.
1514
+ * @param options Opt-out / tune the best-effort video probe-before-create
1515
+ * (2nd optional param so the positional `webhook` arg stays compatible).
1412
1516
  */
1413
- async submit(webhook) {
1517
+ async submit(webhook, options) {
1414
1518
  if (this.client === undefined) {
1415
1519
  throw new GislConfigError('ArchivedRecipe.submit() requires a client; build the bundle via gisl().files(...).archive(...) rather than constructing ArchivedRecipe directly.', { reason: 'no_client' });
1416
1520
  }
1417
- const created = await this._uploadAllAndCreate(webhook, undefined);
1521
+ const created = await this._uploadAllAndCreate(webhook, undefined, undefined, undefined, options?.probeBeforeCreate, options?.probeTimeoutMs);
1418
1522
  return new Handle(created.workflowId, created.webhookSecret != null ? created.webhookSecret : undefined, this.client, null);
1419
1523
  }
1420
1524
  // ---------------------------------------------------------------------------
1421
- async _uploadAllAndCreate(webhook, deadline, onProgress, signal) {
1525
+ async _uploadAllAndCreate(webhook, deadline, onProgress, signal, probeBeforeCreate, probeTimeoutMs) {
1422
1526
  this.validatePreUpload();
1423
1527
  const fileIds = [];
1528
+ // Archive is media-agnostic (no inference) — detect per input via
1529
+ // _detectCompressMedia so only video uploads are probed. A pre-uploaded id
1530
+ // carries no local mime/size, so it is never probed.
1531
+ const probeTargets = [];
1424
1532
  for (const input of this.inputs) {
1425
1533
  _checkAborted(signal);
1426
1534
  if (deadline !== undefined && Date.now() >= deadline) {
@@ -1442,12 +1550,36 @@ export class ArchivedRecipe {
1442
1550
  : {}),
1443
1551
  });
1444
1552
  fileIds.push(up.fileId);
1553
+ probeTargets.push({
1554
+ fileId: up.fileId,
1555
+ isVideo: _detectCompressMedia(source) === 'video',
1556
+ sizeBytes: up.sizeBytes,
1557
+ });
1445
1558
  }
1446
1559
  }
1447
1560
  _checkAborted(signal);
1448
1561
  if (deadline !== undefined && Date.now() >= deadline) {
1449
1562
  throw new GislTimeoutError('Uploads completed but maxWait elapsed before the archive workflow could be created');
1450
1563
  }
1564
+ // Best-effort, concurrent probe-before-create for the multipart-video
1565
+ // inputs (never-bounce; each capped to the remaining maxWait budget so the
1566
+ // waits cannot push createWorkflow past the caller's deadline).
1567
+ const cappedProbeTimeoutMs = _cappedProbeTimeoutMs(probeTimeoutMs, deadline);
1568
+ await Promise.all(probeTargets.map((t) => this.client.maybeWaitForVideoProbe(t.fileId, {
1569
+ enabled: probeBeforeCreate ?? true,
1570
+ isVideo: t.isVideo,
1571
+ sizeBytes: t.sizeBytes,
1572
+ timeoutMs: cappedProbeTimeoutMs,
1573
+ signal,
1574
+ })));
1575
+ // A cancel arriving during a FINAL successful probe request must not still
1576
+ // create the workflow (the probe waits return landed without a final abort
1577
+ // re-check), so check here BEFORE createWorkflow.
1578
+ _checkAborted(signal);
1579
+ // RE-CHECK the deadline AFTER the probe waits (they consume time).
1580
+ if (deadline !== undefined && Date.now() >= deadline) {
1581
+ throw new GislTimeoutError('Probe wait completed but maxWait elapsed before the archive workflow could be created');
1582
+ }
1451
1583
  const created = await this.client.createWorkflow(this.toWorkflowPayload(fileIds, webhook));
1452
1584
  _checkAborted(signal);
1453
1585
  return created;
@@ -9,7 +9,7 @@ export const PRESETS = Object.freeze({
9
9
  "metadata": "All",
10
10
  "iccProfile": "Strip",
11
11
  "progressive": true,
12
- "outputFormat": "Smallest",
12
+ "outputFormat": "Original",
13
13
  }),
14
14
  Balanced: Object.freeze({
15
15
  "mode": "Auto",
@@ -17,7 +17,7 @@ export const PRESETS = Object.freeze({
17
17
  "metadata": "Sensitive",
18
18
  "iccProfile": "Preserve",
19
19
  "progressive": true,
20
- "outputFormat": "Auto",
20
+ "outputFormat": "Original",
21
21
  }),
22
22
  Quality: Object.freeze({
23
23
  "mode": "Lossless",
@@ -48,17 +48,14 @@ export const PRESETS = Object.freeze({
48
48
  Size: Object.freeze({
49
49
  "crf": 30,
50
50
  "preset": "Slow",
51
- "audioBitrate": "_96",
52
51
  }),
53
52
  Balanced: Object.freeze({
54
53
  "crf": 23,
55
54
  "preset": "Medium",
56
- "audioBitrate": "_128",
57
55
  }),
58
56
  Quality: Object.freeze({
59
57
  "crf": 18,
60
58
  "preset": "Slow",
61
- "audioBitrate": "_192",
62
59
  }),
63
60
  }),
64
61
  "document_pdf_compress": Object.freeze({
@@ -1,3 +1,3 @@
1
- export declare const SDK_SPEC_VERSION: "1.5.0";
2
- export declare const PRESET_VERSION: "1.0";
3
- export declare const PRESET_CONFIG_HASH: "sha256:3791bd2d0cd474c5029707f6e50480bf49be33899f7fc5c1a4e77edb136f6e95";
1
+ export declare const SDK_SPEC_VERSION: "1.7.0";
2
+ export declare const PRESET_VERSION: "1.2";
3
+ export declare const PRESET_CONFIG_HASH: "sha256:ef4d66a81920cec353633d788061a03f7c8891d622908c1bfca326679725e72b";
@@ -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.5.0";
5
- export const PRESET_VERSION = "1.0";
6
- export const PRESET_CONFIG_HASH = "sha256:3791bd2d0cd474c5029707f6e50480bf49be33899f7fc5c1a4e77edb136f6e95";
4
+ export const SDK_SPEC_VERSION = "1.7.0";
5
+ export const PRESET_VERSION = "1.2";
6
+ export const PRESET_CONFIG_HASH = "sha256:ef4d66a81920cec353633d788061a03f7c8891d622908c1bfca326679725e72b";
package/dist/merge.d.ts CHANGED
@@ -137,6 +137,15 @@ export declare class MergeBuilder {
137
137
  private planSequence;
138
138
  private inferMediaKind;
139
139
  private uploadUniqueAssets;
140
+ /**
141
+ * Best-effort, concurrent probe-before-create for the multipart-video
142
+ * inputs (never-bounce; each bounded by the SAME capped timeout, so the
143
+ * aggregate wall-clock stays ~timeout rather than N×timeout). When `deadline`
144
+ * is set (the `run()` path) the timeout is capped to the remaining maxWait
145
+ * budget so the waits cannot push createWorkflow past the caller's deadline;
146
+ * `submit()` passes `undefined` (fire-and-forget, no cap).
147
+ */
148
+ private waitForVideoProbes;
140
149
  private buildPayload;
141
150
  private opOptionsForResolved;
142
151
  private awaitTerminal;
package/dist/merge.js CHANGED
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { uploadSource, jobOutputSource } from './types.js';
29
29
  import { GislConfigError, GislNetworkError, GislPerInputOptionsNotSupportedError, GislTimeoutError, GislUndeclaredAssetError, GislUnusedAssetError, SseEndedWithoutTerminal, } from './errors.js';
30
- import { _checkAborted, _consumeSseToTerminal, _parseMaxWait, _pollToTerminal, _projectResult, } from './builder.js';
30
+ import { _cappedProbeTimeoutMs, _checkAborted, _consumeSseToTerminal, _detectCompressMedia, _parseMaxWait, _pollToTerminal, _projectResult, } from './builder.js';
31
31
  import { Handle } from './handle.js';
32
32
  /**
33
33
  * Construct a path-asset. Bare-string arguments to `merge(...)` are
@@ -91,15 +91,28 @@ export class MergeBuilder {
91
91
  const plan = this.planSequence();
92
92
  // 2. Upload each unique asset exactly ONCE. Pass the deadline so the
93
93
  // upload loop can abort mid-batch on a slow connection.
94
+ const probeTargets = [];
94
95
  const uploadedByAssetId = await this.uploadUniqueAssets(plan.uniqueAssets, {
95
96
  signal,
96
97
  onProgress,
97
98
  deadline,
99
+ probeTargets,
98
100
  });
99
101
  _checkAborted(signal);
100
102
  if (Date.now() >= deadline) {
101
103
  throw new GislTimeoutError(`Upload(s) completed but maxWait elapsed before merge workflow could be created`);
102
104
  }
105
+ // Best-effort probe-before-create for the multipart-video inputs (capped to
106
+ // the remaining maxWait budget).
107
+ await this.waitForVideoProbes(probeTargets, options.probeBeforeCreate, options.probeTimeoutMs, signal, deadline);
108
+ // A cancel arriving during a FINAL successful probe request must not still
109
+ // create the workflow (the probe waits return landed without a final abort
110
+ // re-check), so check here BEFORE createWorkflow.
111
+ _checkAborted(signal);
112
+ // RE-CHECK the deadline AFTER the probe waits (they consume time).
113
+ if (Date.now() >= deadline) {
114
+ throw new GislTimeoutError(`Probe wait completed but maxWait elapsed before merge workflow could be created`);
115
+ }
103
116
  // 3. Build the merge JobDefinitionPayload (multi-input).
104
117
  const payload = this.buildPayload(plan, uploadedByAssetId);
105
118
  const created = await this.client.createWorkflow(payload);
@@ -135,7 +148,11 @@ export class MergeBuilder {
135
148
  }
136
149
  async submit(options) {
137
150
  const plan = this.planSequence();
138
- const uploadedByAssetId = await this.uploadUniqueAssets(plan.uniqueAssets, {});
151
+ const probeTargets = [];
152
+ const uploadedByAssetId = await this.uploadUniqueAssets(plan.uniqueAssets, { probeTargets });
153
+ // Best-effort probe-before-create for the multipart-video inputs.
154
+ // Fire-and-forget — no deadline, so no cap (mirrors Recipe::submit()).
155
+ await this.waitForVideoProbes(probeTargets, options.probeBeforeCreate, options.probeTimeoutMs, undefined, undefined);
139
156
  const payload = this.buildPayload(plan, uploadedByAssetId);
140
157
  payload.callback_url = options.webhook;
141
158
  const created = await this.client.createWorkflow(payload);
@@ -281,9 +298,32 @@ export class MergeBuilder {
281
298
  }
282
299
  const resp = await this.client.uploadFile(a.path, uploadOpts);
283
300
  uploaded.set(id, resp.fileId);
301
+ opts.probeTargets?.push({
302
+ fileId: resp.fileId,
303
+ isVideo: _detectCompressMedia(a.path) === 'video',
304
+ sizeBytes: resp.sizeBytes,
305
+ });
284
306
  }
285
307
  return uploaded;
286
308
  }
309
+ /**
310
+ * Best-effort, concurrent probe-before-create for the multipart-video
311
+ * inputs (never-bounce; each bounded by the SAME capped timeout, so the
312
+ * aggregate wall-clock stays ~timeout rather than N×timeout). When `deadline`
313
+ * is set (the `run()` path) the timeout is capped to the remaining maxWait
314
+ * budget so the waits cannot push createWorkflow past the caller's deadline;
315
+ * `submit()` passes `undefined` (fire-and-forget, no cap).
316
+ */
317
+ async waitForVideoProbes(probeTargets, probeBeforeCreate, probeTimeoutMs, signal, deadline) {
318
+ const cappedProbeTimeoutMs = _cappedProbeTimeoutMs(probeTimeoutMs, deadline);
319
+ await Promise.all(probeTargets.map((t) => this.client.maybeWaitForVideoProbe(t.fileId, {
320
+ enabled: probeBeforeCreate ?? true,
321
+ isVideo: t.isVideo,
322
+ sizeBytes: t.sizeBytes,
323
+ timeoutMs: cappedProbeTimeoutMs,
324
+ signal,
325
+ })));
326
+ }
287
327
  buildPayload(plan, uploadedByAssetId) {
288
328
  // p0SuJEeK — the API rejects upload-direct multi-input
289
329
  // (`MultiInputSource` excludes the `upload` leaf: "use type=job_output").
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giveitsmaller/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Node.js SDK for the GISL (Give It Smaller) file compression and processing API",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "node": ">=18"
32
32
  },
33
33
  "dependencies": {
34
- "@giveitsmaller/contracts": "^0.18.0"
34
+ "@giveitsmaller/contracts": "^0.19.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22",