windmill-client 1.805.0 → 1.806.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/client.d.ts CHANGED
@@ -499,6 +499,32 @@ type Flatten<T> = {
499
499
  * overloaded one keeps only its last signature. Neither survives JSON anyway.
500
500
  */
501
501
  export type JsonifiedFn<T extends (...args: any[]) => Promise<any>> = (...args: Parameters<T>) => Promise<Jsonified<Awaited<ReturnType<T>>>>;
502
+ /** Re-dispatch policy for a failed task.
503
+ *
504
+ * Every attempt is a step of its own (`fetch`, `fetch#2`, `fetch#3`), and the
505
+ * wait between two of them is a durable sleep, so a retrying task holds no
506
+ * worker while it backs off.
507
+ *
508
+ * A workflow sleeps once per round, so tasks backing off in the same fan-out
509
+ * wait one after another rather than together: the delay before a fan-out
510
+ * retries is the sum of every backoff pending in it, not the longest one, and
511
+ * it grows with both the width of the fan-out and `attempts`. Retries with no
512
+ * `delay` all go out in a single round.
513
+ */
514
+ export interface TaskRetry {
515
+ /** Attempts after the first failure: `2` runs the task at most 3 times.
516
+ * A whole number from 0 to 100; anything else is rejected where the policy
517
+ * is written. */
518
+ attempts: number;
519
+ /** Seconds to wait before the first retry. Default 0, retry immediately.
520
+ * Sub-second delays are dropped — a durable sleep resolves to the second. */
521
+ delay?: number;
522
+ /** Applied to the delay after each attempt: 1 (the default) keeps it
523
+ * constant, 2 doubles it. */
524
+ multiplier?: number;
525
+ /** Ceiling for the delay in seconds, for a `multiplier` above 1. */
526
+ max_delay?: number;
527
+ }
502
528
  export interface TaskOptions {
503
529
  timeout?: number;
504
530
  tag?: string;
@@ -507,6 +533,7 @@ export interface TaskOptions {
507
533
  concurrency_limit?: number;
508
534
  concurrency_key?: string;
509
535
  concurrency_time_window_s?: number;
536
+ retry?: TaskRetry;
510
537
  }
511
538
  export declare let _workflowCtx: WorkflowCtx | null;
512
539
  export declare function setWorkflowCtx(ctx: WorkflowCtx | null): void;
@@ -529,6 +556,11 @@ export declare class WorkflowCtx {
529
556
  * into a `complete` — the parent would then record the caught branch's value as
530
557
  * a successful step. Boxed: the thrown value may be any falsy value. */
531
558
  private _pendingStepFailure;
559
+ /** Failed tasks whose rejection nothing has consumed, by step key. An unawaited
560
+ * task is still dispatched and still fails, but nothing drives the rejecting
561
+ * thenable it returned. The first `.then()` on that thenable drops the entry,
562
+ * so what remains is only what the body never looked at. */
563
+ private _unobservedTaskFailures;
532
564
  /** When set, the task matching this key executes its inner function directly */
533
565
  _executingKey: string | null;
534
566
  /** Serializes fast-path POSTs across concurrent step() calls within one
@@ -548,6 +580,15 @@ export declare class WorkflowCtx {
548
580
  * fixed by the workflow body, so replays reproduce the same keys. */
549
581
  _allocKey(name: string): string;
550
582
  _nextStep(name: string, script: string, args?: Record<string, any>, dispatch_type?: string, options?: TaskOptions): PromiseLike<any>;
583
+ /** Wait out the backoff between two attempts of a retried task, as a durable
584
+ * sleep, and return once there is nothing to wait for — no delay configured,
585
+ * or the sleep already in the checkpoint.
586
+ *
587
+ * Raises where it stands, the way `_sleep` does, rather than handing back a
588
+ * thenable: a task call the body never awaits is still dispatched (the runner
589
+ * flushes `pending`), so a backoff that only fired when awaited would drop
590
+ * the retry and let the round report the workflow complete. */
591
+ private _retryBackoff;
551
592
  /** Return and clear any pending (unawaited) steps. */
552
593
  _flushPending(): Array<{
553
594
  name: string;
@@ -587,6 +628,9 @@ export declare class WorkflowCtx {
587
628
  _takePendingStepFailure(): {
588
629
  error: unknown;
589
630
  } | null;
631
+ /** Report the task failures the body never looked at, and forget them. Which
632
+ * rounds may call this is the runner's constraint, stated where it is enforced. */
633
+ _warnUnobservedTaskFailures(): void;
590
634
  }
591
635
  export declare function sleep(seconds: number): Promise<void>;
592
636
  /**
@@ -604,9 +648,11 @@ export declare function step<T>(name: string, fn: () => T | Promise<T>): Promise
604
648
  * @example
605
649
  * const extract_data = task(async (url: string) => { ... });
606
650
  * const run_external = task("f/external_script", async (x: number) => { ... });
651
+ * const call_api = task(fetchOrders, { retry: { attempts: 3, delay: 30, multiplier: 2 } });
607
652
  *
608
653
  * Inside a `workflow()`, calling a task dispatches it as a step.
609
- * Outside a workflow, the function body executes directly.
654
+ * Outside a workflow, the function body executes directly and
655
+ * {@link TaskOptions} — retry included — does not apply.
610
656
  *
611
657
  * A task runs as its own job, so its result is always encoded as JSON and
612
658
  * decoded back before the caller sees it: a `Date` comes back as a string, a
package/dist/client.mjs CHANGED
@@ -1008,6 +1008,35 @@ function checkpointableResult(value) {
1008
1008
  function jsonRoundTrip(value) {
1009
1009
  return JSON.parse(encodeCheckpointPayload({ value: checkpointableResult(value) })).value;
1010
1010
  }
1011
+ /** The worker deserializes a sleep into a `u32` of seconds and fails the whole
1012
+ * job on anything wider, so a delay a multiplier has run away with has to be
1013
+ * capped here rather than sent. */
1014
+ const MAX_SLEEP_SECONDS = 4294967295;
1015
+ /** Every attempt claims its keys before the first one is dispatched, so an
1016
+ * unbounded `attempts` is a workflow that hangs allocating rather than a very
1017
+ * patient one. */
1018
+ const MAX_RETRY_ATTEMPTS = 100;
1019
+ /** Rejected where the policy is written, so a workflow fails at its first line
1020
+ * rather than mid-run on a replay. */
1021
+ function assertUsableRetry(retry) {
1022
+ if (retry === void 0) return;
1023
+ const { attempts } = retry;
1024
+ if (!Number.isInteger(attempts) || attempts < 0 || attempts > MAX_RETRY_ATTEMPTS) throw new Error(`retry.attempts must be a whole number between 0 and ${MAX_RETRY_ATTEMPTS}, got ${attempts}`);
1025
+ }
1026
+ /** How many retries the policy asks for, defended against a value that reached
1027
+ * `_nextStep` without going through `assertUsableRetry`. */
1028
+ function retryAttempts(retry) {
1029
+ const attempts = Math.trunc(retry?.attempts ?? 0) || 0;
1030
+ return Math.min(Math.max(attempts, 0), MAX_RETRY_ATTEMPTS);
1031
+ }
1032
+ /** Seconds to wait before retry number `attempt` (0 is the first retry). */
1033
+ function retryDelaySeconds(retry, attempt) {
1034
+ const base = retry.delay ?? 0;
1035
+ if (!(base > 0)) return 0;
1036
+ const grown = base * Math.pow(retry.multiplier ?? 1, attempt);
1037
+ const seconds = Math.floor(Math.min(retry.max_delay ?? grown, grown, MAX_SLEEP_SECONDS));
1038
+ return seconds > 0 ? seconds : 0;
1039
+ }
1011
1040
  /** A step key travels as one path segment when its URLs are minted, so it must be
1012
1041
  * non-empty and free of `/` and dot segments — otherwise `waitForApproval` would
1013
1042
  * accept a key `getApprovalUrls` can never address. */
@@ -1039,6 +1068,11 @@ var WorkflowCtx = class {
1039
1068
  * into a `complete` — the parent would then record the caught branch's value as
1040
1069
  * a successful step. Boxed: the thrown value may be any falsy value. */
1041
1070
  _pendingStepFailure = null;
1071
+ /** Failed tasks whose rejection nothing has consumed, by step key. An unawaited
1072
+ * task is still dispatched and still fails, but nothing drives the rejecting
1073
+ * thenable it returned. The first `.then()` on that thenable drops the entry,
1074
+ * so what remains is only what the body never looked at. */
1075
+ _unobservedTaskFailures = new Map();
1042
1076
  /** When set, the task matching this key executes its inner function directly */
1043
1077
  _executingKey;
1044
1078
  /** Serializes fast-path POSTs across concurrent step() calls within one
@@ -1072,52 +1106,90 @@ var WorkflowCtx = class {
1072
1106
  }
1073
1107
  _nextStep(name, script, args = {}, dispatch_type = "inline", options) {
1074
1108
  this._rethrowSwallowed();
1075
- const key = this._allocKey(name || script || "step");
1076
- if (key in this.completed) {
1077
- const value = this.completed[key];
1078
- if (value && typeof value === "object" && value.__wmill_error) {
1079
- const err = taskErrorFromMarker(value, `Task '${name}' failed`);
1080
- return { then: (_resolve, reject) => {
1081
- if (reject) reject(err);
1082
- else throw err;
1083
- } };
1109
+ const stepName = name || script || "step";
1110
+ const maxRetries = retryAttempts(options?.retry);
1111
+ const baseKey = this._allocKey(stepName);
1112
+ const attemptKeys = [baseKey];
1113
+ const backoffKeys = [];
1114
+ for (let i = 0; i < maxRetries; i++) {
1115
+ backoffKeys.push(this._allocKey(`${baseKey}#retry${i + 2}`));
1116
+ attemptKeys.push(this._allocKey(`${baseKey}#${i + 2}`));
1117
+ }
1118
+ for (let attempt = 0;; attempt++) {
1119
+ const key = attemptKeys[attempt];
1120
+ if (key in this.completed) {
1121
+ const value = this.completed[key];
1122
+ if (value && typeof value === "object" && value.__wmill_error) {
1123
+ if (attempt < maxRetries) {
1124
+ this._retryBackoff(backoffKeys[attempt], baseKey, options.retry, attempt);
1125
+ continue;
1126
+ }
1127
+ const err = taskErrorFromMarker(value, `Task '${name}' failed`);
1128
+ this._unobservedTaskFailures.set(baseKey, err);
1129
+ return { then: (_resolve, reject) => {
1130
+ this._unobservedTaskFailures.delete(baseKey);
1131
+ if (reject) reject(err);
1132
+ else throw err;
1133
+ } };
1134
+ }
1135
+ return { then: (resolve) => resolve(value) };
1084
1136
  }
1085
- return { then: (resolve) => resolve(value) };
1137
+ if (this._executingKey === key) return {
1138
+ then: (resolve) => resolve(null),
1139
+ _execute_directly: true
1140
+ };
1141
+ if (this._executingKey !== null) return { then: () => new Promise(() => {}) };
1142
+ const stepInfo = {
1143
+ name: name || key,
1144
+ script: script || key,
1145
+ args,
1146
+ key,
1147
+ dispatch_type
1148
+ };
1149
+ if (options) {
1150
+ if (options.timeout !== void 0) stepInfo.timeout = options.timeout;
1151
+ if (options.tag !== void 0) stepInfo.tag = options.tag;
1152
+ if (options.cache_ttl !== void 0) stepInfo.cache_ttl = options.cache_ttl;
1153
+ if (options.priority !== void 0) stepInfo.priority = options.priority;
1154
+ if (options.concurrency_limit !== void 0) stepInfo.concurrent_limit = options.concurrency_limit;
1155
+ if (options.concurrency_key !== void 0) stepInfo.concurrency_key = options.concurrency_key;
1156
+ if (options.concurrency_time_window_s !== void 0) stepInfo.concurrency_time_window_s = options.concurrency_time_window_s;
1157
+ }
1158
+ this.pending.push(stepInfo);
1159
+ return { then: () => {
1160
+ if (this._suspended) return new Promise(() => {});
1161
+ this._suspended = true;
1162
+ const steps = [...this.pending];
1163
+ this.pending = [];
1164
+ const names = steps.map((s) => s.name).join(", ");
1165
+ console.log(`\n--- WAC: ${names} ---`);
1166
+ this._raiseSuspend({
1167
+ mode: steps.length > 1 ? "parallel" : "sequential",
1168
+ steps
1169
+ });
1170
+ } };
1086
1171
  }
1087
- if (this._executingKey === key) return {
1088
- then: (resolve) => resolve(null),
1089
- _execute_directly: true
1090
- };
1091
- if (this._executingKey !== null) return { then: () => new Promise(() => {}) };
1092
- const stepInfo = {
1093
- name: name || key,
1094
- script: script || key,
1095
- args,
1172
+ }
1173
+ /** Wait out the backoff between two attempts of a retried task, as a durable
1174
+ * sleep, and return once there is nothing to wait for — no delay configured,
1175
+ * or the sleep already in the checkpoint.
1176
+ *
1177
+ * Raises where it stands, the way `_sleep` does, rather than handing back a
1178
+ * thenable: a task call the body never awaits is still dispatched (the runner
1179
+ * flushes `pending`), so a backoff that only fired when awaited would drop
1180
+ * the retry and let the round report the workflow complete. */
1181
+ _retryBackoff(key, baseKey, retry, attempt) {
1182
+ const seconds = retryDelaySeconds(retry, attempt);
1183
+ if (seconds < 1) return;
1184
+ if (key in this.completed) return;
1185
+ if (this._executingKey !== null) return;
1186
+ console.log(`\n--- WAC: sleep(${key}, ${seconds}s) before retrying ${baseKey} ---`);
1187
+ this._raiseSuspend({
1188
+ mode: "sleep",
1096
1189
  key,
1097
- dispatch_type
1098
- };
1099
- if (options) {
1100
- if (options.timeout !== void 0) stepInfo.timeout = options.timeout;
1101
- if (options.tag !== void 0) stepInfo.tag = options.tag;
1102
- if (options.cache_ttl !== void 0) stepInfo.cache_ttl = options.cache_ttl;
1103
- if (options.priority !== void 0) stepInfo.priority = options.priority;
1104
- if (options.concurrency_limit !== void 0) stepInfo.concurrent_limit = options.concurrency_limit;
1105
- if (options.concurrency_key !== void 0) stepInfo.concurrency_key = options.concurrency_key;
1106
- if (options.concurrency_time_window_s !== void 0) stepInfo.concurrency_time_window_s = options.concurrency_time_window_s;
1107
- }
1108
- this.pending.push(stepInfo);
1109
- return { then: () => {
1110
- if (this._suspended) return new Promise(() => {});
1111
- this._suspended = true;
1112
- const steps = [...this.pending];
1113
- this.pending = [];
1114
- const names = steps.map((s) => s.name).join(", ");
1115
- console.log(`\n--- WAC: ${names} ---`);
1116
- this._raiseSuspend({
1117
- mode: steps.length > 1 ? "parallel" : "sequential",
1118
- steps
1119
- });
1120
- } };
1190
+ seconds,
1191
+ steps: []
1192
+ });
1121
1193
  }
1122
1194
  /** Return and clear any pending (unawaited) steps. */
1123
1195
  _flushPending() {
@@ -1284,6 +1356,13 @@ var WorkflowCtx = class {
1284
1356
  this._pendingStepFailure = null;
1285
1357
  return f;
1286
1358
  }
1359
+ /** Report the task failures the body never looked at, and forget them. Which
1360
+ * rounds may call this is the runner's constraint, stated where it is enforced. */
1361
+ _warnUnobservedTaskFailures() {
1362
+ if (this._executingKey !== null) return;
1363
+ for (const [key, err] of this._unobservedTaskFailures) console.log(`\n--- WAC: task '${key}' failed but was never awaited, so the workflow result does not reflect it: ${err.message} ---`);
1364
+ this._unobservedTaskFailures.clear();
1365
+ }
1287
1366
  };
1288
1367
  async function sleep(seconds) {
1289
1368
  const ctx = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");
@@ -1309,9 +1388,11 @@ async function step(name, fn) {
1309
1388
  * @example
1310
1389
  * const extract_data = task(async (url: string) => { ... });
1311
1390
  * const run_external = task("f/external_script", async (x: number) => { ... });
1391
+ * const call_api = task(fetchOrders, { retry: { attempts: 3, delay: 30, multiplier: 2 } });
1312
1392
  *
1313
1393
  * Inside a `workflow()`, calling a task dispatches it as a step.
1314
- * Outside a workflow, the function body executes directly.
1394
+ * Outside a workflow, the function body executes directly and
1395
+ * {@link TaskOptions} — retry included — does not apply.
1315
1396
  *
1316
1397
  * A task runs as its own job, so its result is always encoded as JSON and
1317
1398
  * decoded back before the caller sees it: a `Date` comes back as a string, a
@@ -1329,6 +1410,7 @@ function task(fnOrPath, maybeFnOrOptions, maybeOptions) {
1329
1410
  fn = fnOrPath;
1330
1411
  taskOptions = maybeFnOrOptions;
1331
1412
  }
1413
+ assertUsableRetry(taskOptions?.retry);
1332
1414
  const taskName = fn.name || taskPath || "";
1333
1415
  const wrapper = function(...args) {
1334
1416
  const ctx = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");
@@ -1387,6 +1469,7 @@ function task(fnOrPath, maybeFnOrOptions, maybeOptions) {
1387
1469
  * // inside workflow: await extract({ url: "https://..." })
1388
1470
  */
1389
1471
  function taskScript(path, options) {
1472
+ assertUsableRetry(options?.retry);
1390
1473
  const name = path.split("/").pop() || path;
1391
1474
  const wrapper = function(...args) {
1392
1475
  const ctx = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");
@@ -1412,6 +1495,7 @@ function taskScript(path, options) {
1412
1495
  * // inside workflow: await pipeline({ input: data })
1413
1496
  */
1414
1497
  function taskFlow(path, options) {
1498
+ assertUsableRetry(options?.retry);
1415
1499
  const name = path.split("/").pop() || path;
1416
1500
  const wrapper = function(...args) {
1417
1501
  const ctx = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");
@@ -29,7 +29,7 @@ const OpenAPI = {
29
29
  PASSWORD: void 0,
30
30
  TOKEN: getEnv("WM_TOKEN"),
31
31
  USERNAME: void 0,
32
- VERSION: "1.805.0",
32
+ VERSION: "1.806.0",
33
33
  WITH_CREDENTIALS: !getEnv("WM_RAW_APP"),
34
34
  interceptors: {
35
35
  request: new Interceptors(),
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI';
4
4
  export * from './services.gen';
5
5
  export * from './types.gen';
6
6
  export type { DenoS3LightClientSettings } from "./s3Types";
7
- export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, setProgress, getProgress, getState, getIdToken, denoS3LightClientSettings, cancelJob, loadS3FileStream, loadS3File, writeS3File, deleteS3File, signS3Objects, signS3Object, getPresignedS3PublicUrls, getPresignedS3PublicUrl, task, taskScript, taskFlow, workflow, step, sleep, parallel, waitForApproval, getApprovalUrls, type TaskOptions, type Jsonified, type JsonifiedFn, WorkflowCtx, _workflowCtx, setWorkflowCtx, StepSuspend, runScript, runScriptAsync, runScriptByPath, runScriptByHash, runScriptByPathAsync, runScriptByHashAsync, runFlow, runFlowAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState, usernameToEmail, requestInteractiveSlackApproval, type Sql, requestInteractiveTeamsApproval, appendToResultStream, streamResult, datatable, ducklake, upsertPartition, appendPartition, type DucklakeMaterializeOptions, type SqlStatement, type DatatableSqlTemplateFunction, type SqlTemplateFunction, type S3Object, type S3ObjectRecord, type S3ObjectURI, commitKafkaOffsets } from "./client";
7
+ export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, setProgress, getProgress, getState, getIdToken, denoS3LightClientSettings, cancelJob, loadS3FileStream, loadS3File, writeS3File, deleteS3File, signS3Objects, signS3Object, getPresignedS3PublicUrls, getPresignedS3PublicUrl, task, taskScript, taskFlow, workflow, step, sleep, parallel, waitForApproval, getApprovalUrls, type TaskOptions, type TaskRetry, type Jsonified, type JsonifiedFn, WorkflowCtx, _workflowCtx, setWorkflowCtx, StepSuspend, runScript, runScriptAsync, runScriptByPath, runScriptByHash, runScriptByPathAsync, runScriptByHashAsync, runFlow, runFlowAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState, usernameToEmail, requestInteractiveSlackApproval, type Sql, requestInteractiveTeamsApproval, appendToResultStream, streamResult, datatable, ducklake, upsertPartition, appendPartition, type DucklakeMaterializeOptions, type SqlStatement, type DatatableSqlTemplateFunction, type SqlTemplateFunction, type S3Object, type S3ObjectRecord, type S3ObjectURI, commitKafkaOffsets } from "./client";
8
8
  import { setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, setProgress, getProgress, getState, getIdToken, denoS3LightClientSettings, cancelJob, loadS3FileStream, loadS3File, writeS3File, deleteS3File, signS3Objects, signS3Object, getPresignedS3PublicUrls, getPresignedS3PublicUrl, task, taskScript, taskFlow, workflow, step, sleep, parallel, waitForApproval, getApprovalUrls, WorkflowCtx, setWorkflowCtx, StepSuspend, runScript, runScriptAsync, runScriptByPath, runScriptByHash, runScriptByPathAsync, runScriptByHashAsync, runFlow, runFlowAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState, usernameToEmail, requestInteractiveSlackApproval, requestInteractiveTeamsApproval, appendToResultStream, streamResult, datatable, ducklake, upsertPartition, appendPartition, getWorkspace, getStatePath, getInternalState, setInternalState, getResumeEndpoints, getResult, getResultMaybe, resolveDefaultResource, databaseUrlFromResource, base64ToUint8Array, uint8ArrayToBase64, parseS3Object, commitKafkaOffsets } from "./client";
9
9
  import { AdminService, AuditService, FlowService, GranularAclService, GroupService, JobService, ResourceService, VariableService, ScriptService, ScheduleService, SettingsService, UserService, WorkspaceService, TeamsService } from "./services.gen";
10
10
  declare const wmill: {