@upstash/workflow 1.3.3 → 1.4.0-rc

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.
Files changed (50) hide show
  1. package/astro.d.mts +3 -2
  2. package/astro.d.ts +3 -2
  3. package/astro.js +587 -139
  4. package/astro.mjs +3 -1
  5. package/{chunk-WEQGCISK.mjs → chunk-UH6O5IJY.mjs} +749 -301
  6. package/cloudflare.d.mts +3 -2
  7. package/cloudflare.d.ts +3 -2
  8. package/cloudflare.js +587 -139
  9. package/cloudflare.mjs +3 -1
  10. package/express.d.mts +3 -2
  11. package/express.d.ts +3 -2
  12. package/express.js +588 -139
  13. package/express.mjs +4 -1
  14. package/h3.d.mts +3 -2
  15. package/h3.d.ts +3 -2
  16. package/h3.js +587 -139
  17. package/h3.mjs +3 -1
  18. package/hono.d.mts +3 -2
  19. package/hono.d.ts +3 -2
  20. package/hono.js +587 -139
  21. package/hono.mjs +3 -1
  22. package/index.d.mts +25 -6
  23. package/index.d.ts +25 -6
  24. package/index.js +610 -141
  25. package/index.mjs +26 -3
  26. package/nextjs.d.mts +3 -2
  27. package/nextjs.d.ts +3 -2
  28. package/nextjs.js +588 -139
  29. package/nextjs.mjs +4 -1
  30. package/package.json +1 -1
  31. package/react-router.d.mts +3 -2
  32. package/react-router.d.ts +3 -2
  33. package/react-router.js +587 -139
  34. package/react-router.mjs +3 -1
  35. package/{serve-many-iJF1IUXk.d.ts → serve-many-BCtZg31b.d.ts} +1 -1
  36. package/{serve-many-CG3BFvO3.d.mts → serve-many-CbIV7145.d.mts} +1 -1
  37. package/solidjs.d.mts +2 -1
  38. package/solidjs.d.ts +2 -1
  39. package/solidjs.js +587 -139
  40. package/solidjs.mjs +3 -1
  41. package/svelte.d.mts +3 -2
  42. package/svelte.d.ts +3 -2
  43. package/svelte.js +587 -139
  44. package/svelte.mjs +3 -1
  45. package/tanstack.d.mts +3 -2
  46. package/tanstack.d.ts +3 -2
  47. package/tanstack.js +587 -139
  48. package/tanstack.mjs +3 -1
  49. package/{types-CekOpKvz.d.ts → types-ffLPVG5_.d.mts} +248 -8
  50. package/{types-CekOpKvz.d.mts → types-ffLPVG5_.d.ts} +248 -8
package/index.js CHANGED
@@ -33,6 +33,9 @@ __export(src_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(src_exports);
35
35
 
36
+ // src/serve/index.ts
37
+ var import_qstash12 = require("@upstash/qstash");
38
+
36
39
  // src/client/utils.ts
37
40
  var import_qstash2 = require("@upstash/qstash");
38
41
 
@@ -301,6 +304,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
301
304
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
302
305
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
303
306
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
307
+ var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
308
+ var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
309
+ var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
310
+ var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
311
+ var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
312
+ var RETRIES_HEADER = "Upstash-Retries";
313
+ var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
314
+ var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
304
315
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
305
316
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
306
317
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -318,102 +329,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
318
329
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
319
330
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
320
331
 
321
- // src/context/auto-executor.ts
322
- var import_qstash5 = require("@upstash/qstash");
323
-
324
- // src/qstash/headers.ts
325
- var import_qstash4 = require("@upstash/qstash");
326
-
327
- // src/utils.ts
328
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
329
- var NANOID_LENGTH = 21;
330
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
331
- function validateLabel(label) {
332
- if (label === void 0) return;
333
- const labels = Array.isArray(label) ? label : [label];
334
- if (labels.length === 0) {
335
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
336
- }
337
- for (const value of labels) {
338
- if (!RESOURCE_NAME_PATTERN.test(value)) {
339
- throw new WorkflowNonRetryableError(
340
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
341
- );
342
- }
343
- }
344
- }
345
- function serializeLabel(label) {
346
- return Array.isArray(label) ? label.join(",") : label;
347
- }
348
- function validateFlowControl(flowControl) {
349
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
350
- throw new WorkflowNonRetryableError(
351
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
352
- );
353
- }
354
- }
355
- function getRandomInt() {
356
- return Math.floor(Math.random() * NANOID_CHARS.length);
357
- }
358
- function nanoid(length = NANOID_LENGTH) {
359
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
360
- }
361
- function getWorkflowRunId(id) {
362
- return `wfr_${id ?? nanoid()}`;
363
- }
364
- function decodeBase64(base64) {
365
- const binString = atob(base64);
366
- try {
367
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
368
- return new TextDecoder().decode(intArray);
369
- } catch (error) {
370
- console.warn(
371
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
372
- );
373
- return binString;
374
- }
375
- }
376
- function getUserIdFromToken(qstashClient) {
377
- try {
378
- const token = qstashClient.token;
379
- const decodedToken = decodeBase64(token);
380
- const tokenPayload = JSON.parse(decodedToken);
381
- const userId = tokenPayload.UserID;
382
- if (!userId) {
383
- throw new WorkflowError("QStash token payload does not contain userId");
384
- }
385
- return userId;
386
- } catch (error) {
387
- throw new WorkflowError(
388
- `Failed to decode QStash token while running create webhook step: ${error.message}`
389
- );
390
- }
391
- }
392
- function getQStashUrl(qstashClient) {
393
- try {
394
- const requester = qstashClient.http;
395
- const baseUrl = requester.baseUrl;
396
- if (!baseUrl) {
397
- throw new WorkflowError("QStash client does not have a baseUrl");
398
- }
399
- return baseUrl;
400
- } catch (error) {
401
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
402
- }
403
- }
404
- function getEventId() {
405
- return `evt_${nanoid(15)}`;
406
- }
407
- function stringifyBody(body) {
408
- if (body === void 0) {
409
- return void 0;
410
- }
411
- if (typeof body === "string") {
412
- return body;
413
- }
414
- return JSON.stringify(body);
415
- }
416
-
417
332
  // node_modules/neverthrow/dist/index.es.js
418
333
  var defaultErrorConfig = {
419
334
  withStackTrace: false
@@ -829,6 +744,102 @@ var Err = class {
829
744
  };
830
745
  var fromThrowable = Result.fromThrowable;
831
746
 
747
+ // src/context/auto-executor.ts
748
+ var import_qstash5 = require("@upstash/qstash");
749
+
750
+ // src/qstash/headers.ts
751
+ var import_qstash4 = require("@upstash/qstash");
752
+
753
+ // src/utils.ts
754
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
755
+ var NANOID_LENGTH = 21;
756
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
757
+ function validateLabel(label) {
758
+ if (label === void 0) return;
759
+ const labels = Array.isArray(label) ? label : [label];
760
+ if (labels.length === 0) {
761
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
762
+ }
763
+ for (const value of labels) {
764
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
765
+ throw new WorkflowNonRetryableError(
766
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
767
+ );
768
+ }
769
+ }
770
+ }
771
+ function serializeLabel(label) {
772
+ return Array.isArray(label) ? label.join(",") : label;
773
+ }
774
+ function validateFlowControl(flowControl) {
775
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
776
+ throw new WorkflowNonRetryableError(
777
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
778
+ );
779
+ }
780
+ }
781
+ function getRandomInt() {
782
+ return Math.floor(Math.random() * NANOID_CHARS.length);
783
+ }
784
+ function nanoid(length = NANOID_LENGTH) {
785
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
786
+ }
787
+ function getWorkflowRunId(id) {
788
+ return `wfr_${id ?? nanoid()}`;
789
+ }
790
+ function decodeBase64(base64) {
791
+ const binString = atob(base64);
792
+ try {
793
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
794
+ return new TextDecoder().decode(intArray);
795
+ } catch (error) {
796
+ console.warn(
797
+ `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
798
+ );
799
+ return binString;
800
+ }
801
+ }
802
+ function getUserIdFromToken(qstashClient) {
803
+ try {
804
+ const token = qstashClient.token;
805
+ const decodedToken = decodeBase64(token);
806
+ const tokenPayload = JSON.parse(decodedToken);
807
+ const userId = tokenPayload.UserID;
808
+ if (!userId) {
809
+ throw new WorkflowError("QStash token payload does not contain userId");
810
+ }
811
+ return userId;
812
+ } catch (error) {
813
+ throw new WorkflowError(
814
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
815
+ );
816
+ }
817
+ }
818
+ function getQStashUrl(qstashClient) {
819
+ try {
820
+ const requester = qstashClient.http;
821
+ const baseUrl = requester.baseUrl;
822
+ if (!baseUrl) {
823
+ throw new WorkflowError("QStash client does not have a baseUrl");
824
+ }
825
+ return baseUrl;
826
+ } catch (error) {
827
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
828
+ }
829
+ }
830
+ function getEventId() {
831
+ return `evt_${nanoid(15)}`;
832
+ }
833
+ function stringifyBody(body) {
834
+ if (body === void 0) {
835
+ return void 0;
836
+ }
837
+ if (typeof body === "string") {
838
+ return body;
839
+ }
840
+ return JSON.stringify(body);
841
+ }
842
+
832
843
  // src/types.ts
833
844
  var StepTypes = [
834
845
  "Initial",
@@ -934,6 +945,10 @@ var triggerFirstInvocation = async (params) => {
934
945
  return err(error_);
935
946
  }
936
947
  };
948
+ var flushPendingStep = async (workflowContext) => {
949
+ const { executor } = workflowContext;
950
+ return await executor.submitPendingStep();
951
+ };
937
952
  var triggerRouteFunction = async ({
938
953
  onCleanup,
939
954
  onStep,
@@ -1141,8 +1156,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1141
1156
  var BaseLazyStep = class _BaseLazyStep {
1142
1157
  stepName;
1143
1158
  context;
1144
- constructor(context, stepName) {
1159
+ /**
1160
+ * step-level settings (flow control, retries etc.) which override the
1161
+ * settings the workflow run was triggered with, for this step only.
1162
+ */
1163
+ stepSettings;
1164
+ /**
1165
+ * whether this step's result can be submitted after the route function
1166
+ * has moved on, instead of right away.
1167
+ *
1168
+ * Enabled for steps whose `getResultStep` produces the final `out` —
1169
+ * which is not the same as "needs no server round trip":
1170
+ * `LazyCreateWebhookStep` builds its result in `getBody` and returns
1171
+ * `out: undefined` from `getResultStep`, so deferring it would hand
1172
+ * `undefined` to the route function.
1173
+ *
1174
+ * Deferring lets the route function continue and reveal the next step,
1175
+ * so that step's settings can ride on this step's submission instead
1176
+ * of needing a step config request of their own.
1177
+ */
1178
+ supportsDeferredSubmission = false;
1179
+ constructor(context, stepName, stepSettings) {
1145
1180
  this.context = context;
1181
+ this.stepSettings = stepSettings;
1146
1182
  if (!stepName) {
1147
1183
  throw new WorkflowError(
1148
1184
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1206,7 +1242,13 @@ var BaseLazyStep = class _BaseLazyStep {
1206
1242
  step.out = JSON.stringify(step.out);
1207
1243
  return JSON.stringify(step);
1208
1244
  }
1209
- getHeaders({ context, telemetry, invokeCount, step }) {
1245
+ getHeaders({
1246
+ context,
1247
+ telemetry,
1248
+ invokeCount,
1249
+ step,
1250
+ stepSettings
1251
+ }) {
1210
1252
  return getHeaders({
1211
1253
  initHeaderValue: "false",
1212
1254
  workflowConfig: {
@@ -1219,7 +1261,8 @@ var BaseLazyStep = class _BaseLazyStep {
1219
1261
  stepInfo: {
1220
1262
  step,
1221
1263
  lazyStep: this
1222
- }
1264
+ },
1265
+ stepSettings
1223
1266
  });
1224
1267
  }
1225
1268
  async submitStep({ context, body, headers }) {
@@ -1237,8 +1280,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1237
1280
  stepFunction;
1238
1281
  stepType = "Run";
1239
1282
  allowUndefinedOut = true;
1240
- constructor(context, stepName, stepFunction) {
1241
- super(context, stepName);
1283
+ supportsDeferredSubmission = true;
1284
+ constructor(context, stepName, stepFunction, stepSettings) {
1285
+ super(context, stepName, stepSettings);
1242
1286
  this.stepFunction = stepFunction;
1243
1287
  }
1244
1288
  getPlanStep(concurrent, targetStep) {
@@ -1268,6 +1312,7 @@ var LazySleepStep = class extends BaseLazyStep {
1268
1312
  sleep;
1269
1313
  stepType = "SleepFor";
1270
1314
  allowUndefinedOut = true;
1315
+ supportsDeferredSubmission = true;
1271
1316
  constructor(context, stepName, sleep) {
1272
1317
  super(context, stepName);
1273
1318
  this.sleep = sleep;
@@ -1307,6 +1352,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1307
1352
  sleepUntil;
1308
1353
  stepType = "SleepUntil";
1309
1354
  allowUndefinedOut = true;
1355
+ supportsDeferredSubmission = true;
1310
1356
  constructor(context, stepName, sleepUntil) {
1311
1357
  super(context, stepName);
1312
1358
  this.sleepUntil = sleepUntil;
@@ -1799,6 +1845,7 @@ var WorkflowHeaders = class {
1799
1845
  invokeCount;
1800
1846
  initHeaderValue;
1801
1847
  stepInfo;
1848
+ stepSettings;
1802
1849
  headers;
1803
1850
  /**
1804
1851
  * @param params workflow header parameters
@@ -1808,17 +1855,20 @@ var WorkflowHeaders = class {
1808
1855
  workflowConfig,
1809
1856
  invokeCount,
1810
1857
  initHeaderValue,
1811
- stepInfo
1858
+ stepInfo,
1859
+ stepSettings
1812
1860
  }) {
1813
1861
  this.userHeaders = userHeaders;
1814
1862
  this.workflowConfig = workflowConfig;
1815
1863
  this.invokeCount = invokeCount;
1816
1864
  this.initHeaderValue = initHeaderValue;
1817
1865
  this.stepInfo = stepInfo;
1866
+ this.stepSettings = stepSettings;
1818
1867
  this.headers = {
1819
1868
  rawHeaders: {},
1820
1869
  workflowHeaders: {},
1821
- failureHeaders: {}
1870
+ failureHeaders: {},
1871
+ stepSettingsHeaders: {}
1822
1872
  };
1823
1873
  }
1824
1874
  getHeaders() {
@@ -1829,6 +1879,7 @@ var WorkflowHeaders = class {
1829
1879
  this.addUserHeaders();
1830
1880
  this.addInvokeCount();
1831
1881
  this.addFailureUrl();
1882
+ this.addStepSettings();
1832
1883
  const contentType = this.addContentType();
1833
1884
  return this.prefixHeaders(contentType);
1834
1885
  }
@@ -1838,7 +1889,7 @@ var WorkflowHeaders = class {
1838
1889
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1839
1890
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1840
1891
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1841
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1892
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
1842
1893
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1843
1894
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1844
1895
  };
@@ -1916,7 +1967,7 @@ var WorkflowHeaders = class {
1916
1967
  this.headers.failureHeaders["Workflow-Init"] = "false";
1917
1968
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1918
1969
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1919
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1970
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
1920
1971
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1921
1972
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1922
1973
  }
@@ -1924,6 +1975,42 @@ var WorkflowHeaders = class {
1924
1975
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
1925
1976
  }
1926
1977
  }
1978
+ /**
1979
+ * Applies the step-level settings of a step to the message.
1980
+ *
1981
+ * These headers configure the message itself, so that QStash uses them
1982
+ * instead of the settings the run was triggered with. The delivery of
1983
+ * the message is what executes the step, which is how a step's
1984
+ * settings reach it.
1985
+ *
1986
+ * When any setting is present the feature set is extended with
1987
+ * `WF_StepConfig`, which is what tells QStash to keep them. QStash
1988
+ * reports that back on the delivery as `Upstash-Workflow-Step-Config`.
1989
+ */
1990
+ addStepSettings() {
1991
+ if (!this.stepSettings) {
1992
+ return;
1993
+ }
1994
+ const headers = {};
1995
+ if (this.stepSettings.flowControl) {
1996
+ const { flowControlKey, flowControlValue } = prepareFlowControl(
1997
+ this.stepSettings.flowControl
1998
+ );
1999
+ headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
2000
+ headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
2001
+ }
2002
+ if (this.stepSettings.retries !== void 0) {
2003
+ headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
2004
+ }
2005
+ if (this.stepSettings.retryDelay) {
2006
+ headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
2007
+ }
2008
+ if (Object.keys(headers).length === 0) {
2009
+ return;
2010
+ }
2011
+ headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
2012
+ this.headers.stepSettingsHeaders = headers;
2013
+ }
1927
2014
  addContentType() {
1928
2015
  if (this.workflowConfig.useJSONContent) {
1929
2016
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -1937,14 +2024,17 @@ var WorkflowHeaders = class {
1937
2024
  return contentType;
1938
2025
  }
1939
2026
  prefixHeaders(contentType) {
1940
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
2027
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
1941
2028
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
1942
2029
  return {
1943
2030
  headers: {
1944
2031
  ...rawHeaders,
1945
2032
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
1946
2033
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
1947
- ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
2034
+ ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
2035
+ // last, so they override the run's settings above. Never
2036
+ // prefixed: they configure this message, not its callback.
2037
+ ...stepSettingsHeaders
1948
2038
  },
1949
2039
  contentType
1950
2040
  };
@@ -1995,7 +2085,7 @@ var submitParallelSteps = async ({
1995
2085
  info: `Submitting ${planSteps.length} parallel steps.`
1996
2086
  });
1997
2087
  const result = await context.qstashClient.batch(
1998
- planSteps.map((planStep) => {
2088
+ planSteps.map((planStep, index) => {
1999
2089
  const { headers } = getHeaders({
2000
2090
  initHeaderValue: "false",
2001
2091
  workflowConfig: {
@@ -2003,7 +2093,8 @@ var submitParallelSteps = async ({
2003
2093
  workflowUrl: context.url,
2004
2094
  telemetry
2005
2095
  },
2006
- invokeCount
2096
+ invokeCount,
2097
+ stepSettings: steps[index].stepSettings
2007
2098
  });
2008
2099
  return {
2009
2100
  headers,
@@ -2022,31 +2113,38 @@ var submitParallelSteps = async ({
2022
2113
  }
2023
2114
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
2024
2115
  };
2025
- var submitSingleStep = async ({
2026
- context,
2116
+ var executeStep = async ({
2027
2117
  lazyStep,
2028
2118
  stepId,
2029
- invokeCount,
2030
2119
  concurrency,
2031
- telemetry,
2032
- dispatchDebug,
2033
2120
  dispatchLifecycle
2034
2121
  }) => {
2035
2122
  await dispatchLifecycle("beforeExecution", {
2036
2123
  stepName: lazyStep.stepName
2037
2124
  });
2038
- let resultStep;
2039
2125
  try {
2040
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2126
+ return await lazyStep.getResultStep(concurrency, stepId);
2041
2127
  } catch (error) {
2042
2128
  attachStepNameToError(error, lazyStep.stepName);
2043
2129
  throw error;
2044
2130
  }
2131
+ };
2132
+ var submitStepResult = async ({
2133
+ context,
2134
+ lazyStep,
2135
+ resultStep,
2136
+ invokeCount,
2137
+ concurrency,
2138
+ telemetry,
2139
+ dispatchDebug,
2140
+ nextStepSettings
2141
+ }) => {
2045
2142
  const { headers } = lazyStep.getHeaders({
2046
2143
  context,
2047
2144
  step: resultStep,
2048
2145
  invokeCount,
2049
- telemetry
2146
+ telemetry,
2147
+ stepSettings: nextStepSettings
2050
2148
  });
2051
2149
  const body = lazyStep.getBody({
2052
2150
  context,
@@ -2071,6 +2169,138 @@ var submitSingleStep = async ({
2071
2169
  }
2072
2170
  return resultStep;
2073
2171
  };
2172
+ var publishStepConfigRequest = async ({
2173
+ context,
2174
+ lazyStep,
2175
+ targetStep,
2176
+ invokeCount,
2177
+ telemetry,
2178
+ dispatchDebug
2179
+ }) => {
2180
+ const { headers } = getHeaders({
2181
+ initHeaderValue: "false",
2182
+ workflowConfig: {
2183
+ workflowRunId: context.workflowRunId,
2184
+ workflowUrl: context.url,
2185
+ telemetry
2186
+ },
2187
+ invokeCount,
2188
+ stepSettings: lazyStep.stepSettings
2189
+ });
2190
+ await dispatchDebug("onInfo", {
2191
+ info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
2192
+ });
2193
+ const result = await context.qstashClient.publishJSON({
2194
+ headers: {
2195
+ ...headers,
2196
+ "Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
2197
+ },
2198
+ method: "POST",
2199
+ body: { targetStep, invokeCount },
2200
+ url: context.url,
2201
+ contentBasedDeduplication: true
2202
+ });
2203
+ if (result?.deduplicated) {
2204
+ await dispatchDebug("onWarning", {
2205
+ warning: `A step config request for step "${lazyStep.stepName}" (${targetStep}) was already published and this one was deduplicated. Expected when the delivery which published it is being retried; otherwise the run stops here.`
2206
+ });
2207
+ } else if (result?.messageId) {
2208
+ await dispatchDebug("onInfo", {
2209
+ info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
2210
+ });
2211
+ }
2212
+ };
2213
+
2214
+ // src/qstash/step-config.ts
2215
+ var DEFAULT_PARALLELISM = 0;
2216
+ var DEFAULT_RATE = 0;
2217
+ var DEFAULT_PERIOD_SECONDS = 1;
2218
+ var SECONDS_PER_UNIT = {
2219
+ s: 1,
2220
+ m: 60,
2221
+ h: 60 * 60,
2222
+ d: 24 * 60 * 60
2223
+ };
2224
+ var durationToSeconds = (duration) => {
2225
+ if (typeof duration === "number") {
2226
+ return Number.isFinite(duration) ? duration : void 0;
2227
+ }
2228
+ const match = /^(\d+)([smhd])$/.exec(duration.trim());
2229
+ if (match) {
2230
+ return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
2231
+ }
2232
+ const seconds = Number(duration.trim());
2233
+ return Number.isFinite(seconds) ? seconds : void 0;
2234
+ };
2235
+ var normalizeFlowControl = (flowControl) => {
2236
+ const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
2237
+ return {
2238
+ key: flowControl.key,
2239
+ parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
2240
+ rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
2241
+ period: period ?? DEFAULT_PERIOD_SECONDS
2242
+ };
2243
+ };
2244
+ var parseFlowControlHeaders = (key, value) => {
2245
+ if (!key) {
2246
+ return void 0;
2247
+ }
2248
+ const flowControl = {
2249
+ key,
2250
+ parallelism: DEFAULT_PARALLELISM,
2251
+ rate: DEFAULT_RATE,
2252
+ period: DEFAULT_PERIOD_SECONDS
2253
+ };
2254
+ for (const entry of (value ?? "").split(",")) {
2255
+ const [name, rawValue] = entry.split("=").map((part) => part.trim());
2256
+ if (!name || rawValue === void 0) {
2257
+ continue;
2258
+ }
2259
+ switch (name) {
2260
+ case "parallelism": {
2261
+ flowControl.parallelism = Number(rawValue);
2262
+ break;
2263
+ }
2264
+ case "rate": {
2265
+ flowControl.rate = Number(rawValue);
2266
+ break;
2267
+ }
2268
+ case "period": {
2269
+ flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
2270
+ break;
2271
+ }
2272
+ }
2273
+ }
2274
+ return flowControl;
2275
+ };
2276
+ var getEffectiveConfig = (headers) => {
2277
+ const retriesHeader = headers.get(RETRIES_HEADER);
2278
+ return {
2279
+ flowControl: parseFlowControlHeaders(
2280
+ headers.get(FLOW_CONTROL_KEY_HEADER),
2281
+ headers.get(FLOW_CONTROL_VALUE_HEADER)
2282
+ ),
2283
+ retries: retriesHeader === null ? void 0 : Number(retriesHeader),
2284
+ retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
2285
+ hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
2286
+ };
2287
+ };
2288
+ var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
2289
+ if (stepSettings.flowControl) {
2290
+ const wanted = normalizeFlowControl(stepSettings.flowControl);
2291
+ const applied = effectiveConfig.flowControl;
2292
+ if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
2293
+ return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
2294
+ }
2295
+ }
2296
+ if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
2297
+ return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
2298
+ }
2299
+ if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
2300
+ return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
2301
+ }
2302
+ return void 0;
2303
+ };
2074
2304
 
2075
2305
  // src/context/auto-executor.ts
2076
2306
  var AutoExecutor = class _AutoExecutor {
@@ -2087,6 +2317,25 @@ var AutoExecutor = class _AutoExecutor {
2087
2317
  stepCount = 0;
2088
2318
  planStepCount = 0;
2089
2319
  executingStep = false;
2320
+ /**
2321
+ * an executed step whose result hasn't been submitted to QStash yet.
2322
+ *
2323
+ * Set when a step which can produce its result in-process executes. The
2324
+ * submission waits so that the route function can continue and reveal
2325
+ * what comes next: if that is a single step with step-level settings,
2326
+ * the settings ride on this submission and no step config request is
2327
+ * needed, since the delivery of the submission is what executes it.
2328
+ *
2329
+ * Flushed when the next step is reached (see `addStep`) or when the
2330
+ * route function ends (see `flushPendingStep` in `serve`).
2331
+ */
2332
+ pendingStep;
2333
+ /**
2334
+ * configuration QStash applied to the delivery being handled. A step
2335
+ * with step-level settings is settled by comparing its settings against
2336
+ * this.
2337
+ */
2338
+ effectiveConfig;
2090
2339
  /**
2091
2340
  * @param context workflow context
2092
2341
  * @param steps list of steps
@@ -2094,14 +2343,17 @@ var AutoExecutor = class _AutoExecutor {
2094
2343
  * @param dispatchLifecycle lifecycle event dispatcher
2095
2344
  * @param telemetry optional telemetry information
2096
2345
  * @param invokeCount optional invoke count
2346
+ * @param effectiveConfig configuration QStash applied to the delivery
2347
+ * being handled
2097
2348
  */
2098
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
2349
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount, effectiveConfig) {
2099
2350
  this.context = context;
2100
2351
  this.steps = steps;
2101
2352
  this.dispatchDebug = dispatchDebug;
2102
2353
  this.dispatchLifecycle = dispatchLifecycle;
2103
2354
  this.telemetry = telemetry;
2104
2355
  this.invokeCount = invokeCount ?? 0;
2356
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2105
2357
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2106
2358
  }
2107
2359
  /**
@@ -2136,6 +2388,15 @@ var AutoExecutor = class _AutoExecutor {
2136
2388
  lazyStepList.push(stepInfo);
2137
2389
  const index = this.indexInCurrentList++;
2138
2390
  const requestComplete = this.deferExecution().then(async () => {
2391
+ const submitted = await this.submitPendingStep(
2392
+ lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
2393
+ );
2394
+ if (submitted.isErr()) {
2395
+ throw submitted.error;
2396
+ }
2397
+ if (submitted.value.result === "submitted-step") {
2398
+ throw submitted.value.abort;
2399
+ }
2139
2400
  if (!this.promises.has(lazyStepList)) {
2140
2401
  const promise2 = this.getExecutionPromise(lazyStepList);
2141
2402
  this.promises.set(lazyStepList, promise2);
@@ -2189,18 +2450,100 @@ var AutoExecutor = class _AutoExecutor {
2189
2450
  }
2190
2451
  return parsedOut;
2191
2452
  }
2192
- const resultStep = await submitSingleStep({
2193
- context: this.context,
2453
+ const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
2454
+ if (mismatch) {
2455
+ if (!this.effectiveConfig.hasStepConfig) {
2456
+ await publishStepConfigRequest({
2457
+ context: this.context,
2458
+ lazyStep,
2459
+ targetStep: this.stepCount,
2460
+ invokeCount: this.invokeCount,
2461
+ telemetry: this.telemetry,
2462
+ dispatchDebug: this.dispatchDebug
2463
+ });
2464
+ throw new WorkflowAbort(lazyStep.stepName);
2465
+ } else {
2466
+ await this.dispatchDebug("onWarning", {
2467
+ warning: `The step-level settings of step '${lazyStep.stepName}' were published but are not recognized on the delivery which carries them (${mismatch}). Executing the step with the settings of the delivery instead. This is a bug in @upstash/workflow, please report it.`
2468
+ });
2469
+ }
2470
+ }
2471
+ const resultStep = await executeStep({
2194
2472
  lazyStep,
2195
2473
  stepId: this.stepCount,
2474
+ concurrency: NO_CONCURRENCY,
2475
+ dispatchLifecycle: this.dispatchLifecycle
2476
+ });
2477
+ if (lazyStep.supportsDeferredSubmission) {
2478
+ this.pendingStep = { status: "held", lazyStep, resultStep };
2479
+ return lazyStep.parseOut({
2480
+ ...resultStep,
2481
+ out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
2482
+ });
2483
+ }
2484
+ await submitStepResult({
2485
+ context: this.context,
2486
+ lazyStep,
2487
+ resultStep,
2196
2488
  invokeCount: this.invokeCount,
2197
- concurrency: 1,
2489
+ concurrency: NO_CONCURRENCY,
2198
2490
  telemetry: this.telemetry,
2199
- dispatchDebug: this.dispatchDebug,
2200
- dispatchLifecycle: this.dispatchLifecycle
2491
+ dispatchDebug: this.dispatchDebug
2201
2492
  });
2202
2493
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2203
2494
  }
2495
+ /**
2496
+ * Submits the result of the step being held, if there is one, and
2497
+ * returns the abort the caller has to throw to end the invocation.
2498
+ *
2499
+ * Returns undefined when no step is being held, which is the caller's
2500
+ * signal that there is nothing to end. Throws instead of returning
2501
+ * when the submission itself fails, so that error reaches the caller
2502
+ * rather than an abort claiming the step was submitted.
2503
+ *
2504
+ * Callers which arrive while a submission is already under way (the
2505
+ * steps of a parallel group, added together) wait for that one and get
2506
+ * the same abort, rather than submitting a second time.
2507
+ *
2508
+ * The next step's settings are attached whenever it has any, without
2509
+ * checking them against the current delivery: the executor only knows
2510
+ * the configuration of the delivery in hand, which inside a
2511
+ * delivery is the *previous* step's, so such a check would be wrong
2512
+ * exactly when two steps with settings follow each other.
2513
+ *
2514
+ * @param nextStepSettings step-level settings of the next step
2515
+ * @returns `submitted-step` with the abort which ends this invocation,
2516
+ * or `no-pending-step` when nothing was held and there is nothing to
2517
+ * end
2518
+ */
2519
+ async submitPendingStep(nextStepSettings) {
2520
+ if (!this.pendingStep) {
2521
+ return ok({ result: "no-pending-step" });
2522
+ }
2523
+ try {
2524
+ if (this.pendingStep.status === "submitting") {
2525
+ return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
2526
+ }
2527
+ const { lazyStep, resultStep } = this.pendingStep;
2528
+ const submitted = (async () => {
2529
+ await submitStepResult({
2530
+ context: this.context,
2531
+ lazyStep,
2532
+ resultStep,
2533
+ invokeCount: this.invokeCount,
2534
+ concurrency: NO_CONCURRENCY,
2535
+ telemetry: this.telemetry,
2536
+ dispatchDebug: this.dispatchDebug,
2537
+ nextStepSettings
2538
+ });
2539
+ return new WorkflowAbort(lazyStep.stepName, resultStep);
2540
+ })();
2541
+ this.pendingStep = { status: "submitting", submitted };
2542
+ return ok({ result: "submitted-step", abort: await submitted });
2543
+ } catch (error) {
2544
+ return err(error);
2545
+ }
2546
+ }
2204
2547
  /**
2205
2548
  * Runs steps in parallel.
2206
2549
  *
@@ -2249,15 +2592,20 @@ var AutoExecutor = class _AutoExecutor {
2249
2592
  validateStep(parallelSteps[stepIndex], planStep);
2250
2593
  try {
2251
2594
  const parallelStep = parallelSteps[stepIndex];
2252
- const resultStep = await submitSingleStep({
2253
- context: this.context,
2595
+ const resultStep = await executeStep({
2254
2596
  lazyStep: parallelStep,
2255
2597
  stepId: planStep.targetStep,
2598
+ concurrency: parallelSteps.length,
2599
+ dispatchLifecycle: this.dispatchLifecycle
2600
+ });
2601
+ await submitStepResult({
2602
+ context: this.context,
2603
+ lazyStep: parallelStep,
2604
+ resultStep,
2256
2605
  invokeCount: this.invokeCount,
2257
2606
  concurrency: parallelSteps.length,
2258
2607
  telemetry: this.telemetry,
2259
- dispatchDebug: this.dispatchDebug,
2260
- dispatchLifecycle: this.dispatchLifecycle
2608
+ dispatchDebug: this.dispatchDebug
2261
2609
  });
2262
2610
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2263
2611
  } catch (error) {
@@ -2806,6 +3154,46 @@ var WorkflowContext = class {
2806
3154
  * `1` on the first retry, `2` on the second, and so on.
2807
3155
  */
2808
3156
  retried;
3157
+ /**
3158
+ * Configuration QStash applied to the request being handled.
3159
+ *
3160
+ * This is the configuration of *this delivery*, not of the run: it is
3161
+ * the configuration the run was triggered with, except inside the
3162
+ * delivery which executes a step that has step-level settings, where
3163
+ * it is that step's settings. The fields below expose it; the executor
3164
+ * gets it directly rather than reaching through the context.
3165
+ */
3166
+ effectiveConfig;
3167
+ /**
3168
+ * Flow control QStash applied to the request being handled, if any.
3169
+ *
3170
+ * This is the flow control of the request in hand, which is the one the
3171
+ * run was triggered with except inside a step carrying its own.
3172
+ */
3173
+ get flowControl() {
3174
+ return this.effectiveConfig.flowControl;
3175
+ }
3176
+ /**
3177
+ * Retry limit QStash applied to the request being handled, if it
3178
+ * reported one.
3179
+ *
3180
+ * Not to be confused with {@link retried}, which is how many retries
3181
+ * have already happened.
3182
+ *
3183
+ * @see {@link flowControl} for what "the request being handled" means
3184
+ */
3185
+ get retries() {
3186
+ return this.effectiveConfig.retries;
3187
+ }
3188
+ /**
3189
+ * Retry delay expression QStash applied to the request being handled,
3190
+ * if any.
3191
+ *
3192
+ * @see {@link flowControl} for what "the request being handled" means
3193
+ */
3194
+ get retryDelay() {
3195
+ return this.effectiveConfig.retryDelay;
3196
+ }
2809
3197
  constructor({
2810
3198
  qstashClient,
2811
3199
  workflowRunId,
@@ -2819,7 +3207,8 @@ var WorkflowContext = class {
2819
3207
  invokeCount,
2820
3208
  label,
2821
3209
  retried,
2822
- middlewareManager
3210
+ middlewareManager,
3211
+ effectiveConfig
2823
3212
  }) {
2824
3213
  this.qstashClient = qstashClient;
2825
3214
  this.workflowRunId = workflowRunId;
@@ -2831,6 +3220,7 @@ var WorkflowContext = class {
2831
3220
  this.env = env ?? {};
2832
3221
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2833
3222
  this.retried = retried ?? 0;
3223
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2834
3224
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2835
3225
  middlewareManagerInstance.assignContext(this);
2836
3226
  this.executor = new AutoExecutor(
@@ -2839,7 +3229,8 @@ var WorkflowContext = class {
2839
3229
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2840
3230
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2841
3231
  telemetry,
2842
- invokeCount
3232
+ invokeCount,
3233
+ this.effectiveConfig
2843
3234
  );
2844
3235
  }
2845
3236
  /**
@@ -2865,13 +3256,35 @@ var WorkflowContext = class {
2865
3256
  * ])
2866
3257
  * ```
2867
3258
  *
3259
+ * Step-level settings can be passed as a third argument, overriding
3260
+ * the settings the workflow run was triggered with for this step only:
3261
+ *
3262
+ * ```typescript
3263
+ * const result = await context.run(
3264
+ * "step 1",
3265
+ * () => {
3266
+ * return "result"
3267
+ * },
3268
+ * {
3269
+ * flowControl: { key: "custom-key", parallelism: 3 },
3270
+ * retries: 5,
3271
+ * }
3272
+ * )
3273
+ * ```
3274
+ *
2868
3275
  * @param stepName name of the step
2869
3276
  * @param stepFunction step function to be executed
3277
+ * @param stepSettings step-level settings for this step
2870
3278
  * @returns result of the step function
2871
3279
  */
2872
- async run(stepName, stepFunction) {
3280
+ async run(stepName, stepFunction, stepSettings) {
3281
+ if (stepSettings) {
3282
+ validateFlowControl(stepSettings.flowControl);
3283
+ }
2873
3284
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2874
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3285
+ return await this.addStep(
3286
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3287
+ );
2875
3288
  }
2876
3289
  /**
2877
3290
  * Stops the execution for the duration provided.
@@ -3094,7 +3507,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3094
3507
  *
3095
3508
  * @param routeFunction
3096
3509
  */
3097
- static async tryAuthentication(routeFunction, context) {
3510
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3098
3511
  const disabledContext = new _DisabledWorkflowContext({
3099
3512
  qstashClient: new import_qstash10.Client({
3100
3513
  baseUrl: "disabled-client",
@@ -3109,7 +3522,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3109
3522
  initialPayload: context.requestPayload,
3110
3523
  env: context.env,
3111
3524
  label: context.labels,
3112
- retried: context.retried
3525
+ retried: context.retried,
3526
+ // the route function runs for real here until it reaches a step, so
3527
+ // it observes the same configuration as it will on the real context
3528
+ effectiveConfig
3113
3529
  });
3114
3530
  try {
3115
3531
  await routeFunction(disabledContext);
@@ -3326,6 +3742,7 @@ var handleFailure = async ({
3326
3742
  }
3327
3743
  const userHeaders = recreateUserHeaders(request.headers);
3328
3744
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3745
+ const effectiveConfig = getEffectiveConfig(request.headers);
3329
3746
  const workflowContext = new WorkflowContext({
3330
3747
  qstashClient,
3331
3748
  workflowRunId,
@@ -3339,11 +3756,13 @@ var handleFailure = async ({
3339
3756
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3340
3757
  retried,
3341
3758
  workflowRunCreatedAt: workflowCreatedAt,
3342
- middlewareManager: void 0
3759
+ middlewareManager: void 0,
3760
+ effectiveConfig
3343
3761
  });
3344
3762
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3345
3763
  routeFunction,
3346
- workflowContext
3764
+ workflowContext,
3765
+ effectiveConfig
3347
3766
  );
3348
3767
  if (authCheck.isErr()) {
3349
3768
  await dispatchDebug?.("onError", {
@@ -3435,7 +3854,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3435
3854
  const client = new import_qstash11.Client({
3436
3855
  ...clientOptions,
3437
3856
  baseUrl: clientEnv.QSTASH_URL,
3438
- token: clientEnv.QSTASH_TOKEN
3857
+ token: clientEnv.QSTASH_TOKEN,
3858
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3859
+ // the client at the local dev server too. Without this the QStash Client
3860
+ // re-derives dev mode from its own process.env, which is blind to the
3861
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3862
+ // An explicit devMode in the user's client config still wins.
3863
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3439
3864
  });
3440
3865
  const receiver = getReceiver(environment, receiverConfig, region);
3441
3866
  return { client, receiver };
@@ -3485,7 +3910,9 @@ var getQStashHandlers = ({
3485
3910
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3486
3911
  ...qstashClientOption,
3487
3912
  baseUrl: environment.QSTASH_URL,
3488
- token: environment.QSTASH_TOKEN
3913
+ token: environment.QSTASH_TOKEN,
3914
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
3915
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3489
3916
  }),
3490
3917
  receiver: getReceiver(environment, receiverConfig)
3491
3918
  }
@@ -3825,12 +4252,14 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3825
4252
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3826
4253
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3827
4254
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4255
+ const effectiveConfig = getEffectiveConfig(request.headers);
3828
4256
  const workflowContext = new WorkflowContext({
3829
4257
  qstashClient: regionalClient,
3830
4258
  workflowRunId,
3831
4259
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
3832
4260
  headers: recreateUserHeaders(request.headers),
3833
4261
  steps,
4262
+ effectiveConfig,
3834
4263
  url: workflowUrl,
3835
4264
  env,
3836
4265
  telemetry,
@@ -3842,7 +4271,8 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3842
4271
  });
3843
4272
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3844
4273
  routeFunction,
3845
- workflowContext
4274
+ workflowContext,
4275
+ effectiveConfig
3846
4276
  );
3847
4277
  if (authCheck.isErr()) {
3848
4278
  throw authCheck.error;
@@ -3879,7 +4309,23 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3879
4309
  if (steps.length === 1) {
3880
4310
  await middlewareManager.dispatchLifecycle("runStarted", {});
3881
4311
  }
3882
- return await routeFunction(workflowContext);
4312
+ let outcome;
4313
+ try {
4314
+ outcome = { ran: true, result: await routeFunction(workflowContext) };
4315
+ } catch (error) {
4316
+ outcome = { ran: false, error };
4317
+ }
4318
+ const submitted = await flushPendingStep(workflowContext);
4319
+ if (submitted.isErr()) {
4320
+ throw submitted.error;
4321
+ }
4322
+ if (submitted.value.result === "submitted-step") {
4323
+ throw submitted.value.abort;
4324
+ }
4325
+ if (!outcome.ran) {
4326
+ throw outcome.error;
4327
+ }
4328
+ return outcome.result;
3883
4329
  },
3884
4330
  onCleanup: async (result2) => {
3885
4331
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -3959,6 +4405,7 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3959
4405
  return { handler: safeHandler };
3960
4406
  };
3961
4407
  var serve = (routeFunction, options) => {
4408
+ void (0, import_qstash12.startDevServer)();
3962
4409
  return serveBase(
3963
4410
  routeFunction,
3964
4411
  {
@@ -3970,9 +4417,10 @@ var serve = (routeFunction, options) => {
3970
4417
  };
3971
4418
 
3972
4419
  // src/client/index.ts
3973
- var import_qstash12 = require("@upstash/qstash");
4420
+ var import_qstash14 = require("@upstash/qstash");
3974
4421
 
3975
4422
  // src/client/dlq.ts
4423
+ var import_qstash13 = require("@upstash/qstash");
3976
4424
  function buildResumeRestartHeaders(options) {
3977
4425
  const headers = {};
3978
4426
  if (options?.flowControl) {
@@ -4090,6 +4538,27 @@ var DLQ = class {
4090
4538
  });
4091
4539
  return response;
4092
4540
  }
4541
+ /**
4542
+ * Cancel the in-progress failure function (failureUrl/failureFunction) call
4543
+ * of a workflow run.
4544
+ *
4545
+ * Fails if the run has no failure function call in progress.
4546
+ *
4547
+ * @param dlqId - The ID of the DLQ message whose failure function call will be canceled
4548
+ */
4549
+ async cancelFailureFunction({ dlqId }) {
4550
+ assertNonEmptyId(dlqId, "DLQ id");
4551
+ try {
4552
+ await this.client.http.request({
4553
+ path: ["v2", "workflows", "dlq", "callback", dlqId],
4554
+ method: "DELETE",
4555
+ parseResponseAsJson: false
4556
+ });
4557
+ } catch (error) {
4558
+ if (isInstanceOf(error, import_qstash13.QstashError) && error.status === 302) return;
4559
+ throw error;
4560
+ }
4561
+ }
4093
4562
  /**
4094
4563
  * Delete DLQ messages.
4095
4564
  *
@@ -4131,7 +4600,7 @@ var DLQ = class {
4131
4600
  var Client4 = class {
4132
4601
  client;
4133
4602
  constructor(clientConfig) {
4134
- this.client = new import_qstash12.Client(clientConfig);
4603
+ this.client = new import_qstash14.Client(clientConfig);
4135
4604
  }
4136
4605
  async cancel(request) {
4137
4606
  if (typeof request === "object" && !Array.isArray(request) && ("ids" in request || "urlStartingWith" in request)) {