@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/express.js CHANGED
@@ -34,6 +34,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
34
34
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
35
35
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
36
36
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
37
+ var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
38
+ var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
39
+ var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
40
+ var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
41
+ var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
42
+ var RETRIES_HEADER = "Upstash-Retries";
43
+ var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
44
+ var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
37
45
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
38
46
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
39
47
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -51,6 +59,9 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
51
59
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
52
60
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
53
61
 
62
+ // src/serve/index.ts
63
+ var import_qstash12 = require("@upstash/qstash");
64
+
54
65
  // src/client/utils.ts
55
66
  var import_qstash2 = require("@upstash/qstash");
56
67
 
@@ -231,102 +242,6 @@ var getSteps = async (requester, workflowRunId, messageId, dispatchDebug) => {
231
242
  }
232
243
  };
233
244
 
234
- // src/context/auto-executor.ts
235
- var import_qstash5 = require("@upstash/qstash");
236
-
237
- // src/qstash/headers.ts
238
- var import_qstash4 = require("@upstash/qstash");
239
-
240
- // src/utils.ts
241
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
242
- var NANOID_LENGTH = 21;
243
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
244
- function validateLabel(label) {
245
- if (label === void 0) return;
246
- const labels = Array.isArray(label) ? label : [label];
247
- if (labels.length === 0) {
248
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
249
- }
250
- for (const value of labels) {
251
- if (!RESOURCE_NAME_PATTERN.test(value)) {
252
- throw new WorkflowNonRetryableError(
253
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
254
- );
255
- }
256
- }
257
- }
258
- function serializeLabel(label) {
259
- return Array.isArray(label) ? label.join(",") : label;
260
- }
261
- function validateFlowControl(flowControl) {
262
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
263
- throw new WorkflowNonRetryableError(
264
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
265
- );
266
- }
267
- }
268
- function getRandomInt() {
269
- return Math.floor(Math.random() * NANOID_CHARS.length);
270
- }
271
- function nanoid(length = NANOID_LENGTH) {
272
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
273
- }
274
- function getWorkflowRunId(id) {
275
- return `wfr_${id ?? nanoid()}`;
276
- }
277
- function decodeBase64(base64) {
278
- const binString = atob(base64);
279
- try {
280
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
281
- return new TextDecoder().decode(intArray);
282
- } catch (error) {
283
- console.warn(
284
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
285
- );
286
- return binString;
287
- }
288
- }
289
- function getUserIdFromToken(qstashClient) {
290
- try {
291
- const token = qstashClient.token;
292
- const decodedToken = decodeBase64(token);
293
- const tokenPayload = JSON.parse(decodedToken);
294
- const userId = tokenPayload.UserID;
295
- if (!userId) {
296
- throw new WorkflowError("QStash token payload does not contain userId");
297
- }
298
- return userId;
299
- } catch (error) {
300
- throw new WorkflowError(
301
- `Failed to decode QStash token while running create webhook step: ${error.message}`
302
- );
303
- }
304
- }
305
- function getQStashUrl(qstashClient) {
306
- try {
307
- const requester = qstashClient.http;
308
- const baseUrl = requester.baseUrl;
309
- if (!baseUrl) {
310
- throw new WorkflowError("QStash client does not have a baseUrl");
311
- }
312
- return baseUrl;
313
- } catch (error) {
314
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
315
- }
316
- }
317
- function getEventId() {
318
- return `evt_${nanoid(15)}`;
319
- }
320
- function stringifyBody(body) {
321
- if (body === void 0) {
322
- return void 0;
323
- }
324
- if (typeof body === "string") {
325
- return body;
326
- }
327
- return JSON.stringify(body);
328
- }
329
-
330
245
  // node_modules/neverthrow/dist/index.es.js
331
246
  var defaultErrorConfig = {
332
247
  withStackTrace: false
@@ -742,6 +657,102 @@ var Err = class {
742
657
  };
743
658
  var fromThrowable = Result.fromThrowable;
744
659
 
660
+ // src/context/auto-executor.ts
661
+ var import_qstash5 = require("@upstash/qstash");
662
+
663
+ // src/qstash/headers.ts
664
+ var import_qstash4 = require("@upstash/qstash");
665
+
666
+ // src/utils.ts
667
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
668
+ var NANOID_LENGTH = 21;
669
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
670
+ function validateLabel(label) {
671
+ if (label === void 0) return;
672
+ const labels = Array.isArray(label) ? label : [label];
673
+ if (labels.length === 0) {
674
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
675
+ }
676
+ for (const value of labels) {
677
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
678
+ throw new WorkflowNonRetryableError(
679
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
680
+ );
681
+ }
682
+ }
683
+ }
684
+ function serializeLabel(label) {
685
+ return Array.isArray(label) ? label.join(",") : label;
686
+ }
687
+ function validateFlowControl(flowControl) {
688
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
689
+ throw new WorkflowNonRetryableError(
690
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
691
+ );
692
+ }
693
+ }
694
+ function getRandomInt() {
695
+ return Math.floor(Math.random() * NANOID_CHARS.length);
696
+ }
697
+ function nanoid(length = NANOID_LENGTH) {
698
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
699
+ }
700
+ function getWorkflowRunId(id) {
701
+ return `wfr_${id ?? nanoid()}`;
702
+ }
703
+ function decodeBase64(base64) {
704
+ const binString = atob(base64);
705
+ try {
706
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
707
+ return new TextDecoder().decode(intArray);
708
+ } catch (error) {
709
+ console.warn(
710
+ `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
711
+ );
712
+ return binString;
713
+ }
714
+ }
715
+ function getUserIdFromToken(qstashClient) {
716
+ try {
717
+ const token = qstashClient.token;
718
+ const decodedToken = decodeBase64(token);
719
+ const tokenPayload = JSON.parse(decodedToken);
720
+ const userId = tokenPayload.UserID;
721
+ if (!userId) {
722
+ throw new WorkflowError("QStash token payload does not contain userId");
723
+ }
724
+ return userId;
725
+ } catch (error) {
726
+ throw new WorkflowError(
727
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
728
+ );
729
+ }
730
+ }
731
+ function getQStashUrl(qstashClient) {
732
+ try {
733
+ const requester = qstashClient.http;
734
+ const baseUrl = requester.baseUrl;
735
+ if (!baseUrl) {
736
+ throw new WorkflowError("QStash client does not have a baseUrl");
737
+ }
738
+ return baseUrl;
739
+ } catch (error) {
740
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
741
+ }
742
+ }
743
+ function getEventId() {
744
+ return `evt_${nanoid(15)}`;
745
+ }
746
+ function stringifyBody(body) {
747
+ if (body === void 0) {
748
+ return void 0;
749
+ }
750
+ if (typeof body === "string") {
751
+ return body;
752
+ }
753
+ return JSON.stringify(body);
754
+ }
755
+
745
756
  // src/types.ts
746
757
  var StepTypes = [
747
758
  "Initial",
@@ -847,6 +858,10 @@ var triggerFirstInvocation = async (params) => {
847
858
  return err(error_);
848
859
  }
849
860
  };
861
+ var flushPendingStep = async (workflowContext) => {
862
+ const { executor } = workflowContext;
863
+ return await executor.submitPendingStep();
864
+ };
850
865
  var triggerRouteFunction = async ({
851
866
  onCleanup,
852
867
  onStep,
@@ -1054,8 +1069,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1054
1069
  var BaseLazyStep = class _BaseLazyStep {
1055
1070
  stepName;
1056
1071
  context;
1057
- constructor(context, stepName) {
1072
+ /**
1073
+ * step-level settings (flow control, retries etc.) which override the
1074
+ * settings the workflow run was triggered with, for this step only.
1075
+ */
1076
+ stepSettings;
1077
+ /**
1078
+ * whether this step's result can be submitted after the route function
1079
+ * has moved on, instead of right away.
1080
+ *
1081
+ * Enabled for steps whose `getResultStep` produces the final `out` —
1082
+ * which is not the same as "needs no server round trip":
1083
+ * `LazyCreateWebhookStep` builds its result in `getBody` and returns
1084
+ * `out: undefined` from `getResultStep`, so deferring it would hand
1085
+ * `undefined` to the route function.
1086
+ *
1087
+ * Deferring lets the route function continue and reveal the next step,
1088
+ * so that step's settings can ride on this step's submission instead
1089
+ * of needing a step config request of their own.
1090
+ */
1091
+ supportsDeferredSubmission = false;
1092
+ constructor(context, stepName, stepSettings) {
1058
1093
  this.context = context;
1094
+ this.stepSettings = stepSettings;
1059
1095
  if (!stepName) {
1060
1096
  throw new WorkflowError(
1061
1097
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1119,7 +1155,13 @@ var BaseLazyStep = class _BaseLazyStep {
1119
1155
  step.out = JSON.stringify(step.out);
1120
1156
  return JSON.stringify(step);
1121
1157
  }
1122
- getHeaders({ context, telemetry: telemetry2, invokeCount, step }) {
1158
+ getHeaders({
1159
+ context,
1160
+ telemetry: telemetry2,
1161
+ invokeCount,
1162
+ step,
1163
+ stepSettings
1164
+ }) {
1123
1165
  return getHeaders({
1124
1166
  initHeaderValue: "false",
1125
1167
  workflowConfig: {
@@ -1132,7 +1174,8 @@ var BaseLazyStep = class _BaseLazyStep {
1132
1174
  stepInfo: {
1133
1175
  step,
1134
1176
  lazyStep: this
1135
- }
1177
+ },
1178
+ stepSettings
1136
1179
  });
1137
1180
  }
1138
1181
  async submitStep({ context, body, headers }) {
@@ -1150,8 +1193,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1150
1193
  stepFunction;
1151
1194
  stepType = "Run";
1152
1195
  allowUndefinedOut = true;
1153
- constructor(context, stepName, stepFunction) {
1154
- super(context, stepName);
1196
+ supportsDeferredSubmission = true;
1197
+ constructor(context, stepName, stepFunction, stepSettings) {
1198
+ super(context, stepName, stepSettings);
1155
1199
  this.stepFunction = stepFunction;
1156
1200
  }
1157
1201
  getPlanStep(concurrent, targetStep) {
@@ -1181,6 +1225,7 @@ var LazySleepStep = class extends BaseLazyStep {
1181
1225
  sleep;
1182
1226
  stepType = "SleepFor";
1183
1227
  allowUndefinedOut = true;
1228
+ supportsDeferredSubmission = true;
1184
1229
  constructor(context, stepName, sleep) {
1185
1230
  super(context, stepName);
1186
1231
  this.sleep = sleep;
@@ -1220,6 +1265,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1220
1265
  sleepUntil;
1221
1266
  stepType = "SleepUntil";
1222
1267
  allowUndefinedOut = true;
1268
+ supportsDeferredSubmission = true;
1223
1269
  constructor(context, stepName, sleepUntil) {
1224
1270
  super(context, stepName);
1225
1271
  this.sleepUntil = sleepUntil;
@@ -1712,6 +1758,7 @@ var WorkflowHeaders = class {
1712
1758
  invokeCount;
1713
1759
  initHeaderValue;
1714
1760
  stepInfo;
1761
+ stepSettings;
1715
1762
  headers;
1716
1763
  /**
1717
1764
  * @param params workflow header parameters
@@ -1721,17 +1768,20 @@ var WorkflowHeaders = class {
1721
1768
  workflowConfig,
1722
1769
  invokeCount,
1723
1770
  initHeaderValue,
1724
- stepInfo
1771
+ stepInfo,
1772
+ stepSettings
1725
1773
  }) {
1726
1774
  this.userHeaders = userHeaders;
1727
1775
  this.workflowConfig = workflowConfig;
1728
1776
  this.invokeCount = invokeCount;
1729
1777
  this.initHeaderValue = initHeaderValue;
1730
1778
  this.stepInfo = stepInfo;
1779
+ this.stepSettings = stepSettings;
1731
1780
  this.headers = {
1732
1781
  rawHeaders: {},
1733
1782
  workflowHeaders: {},
1734
- failureHeaders: {}
1783
+ failureHeaders: {},
1784
+ stepSettingsHeaders: {}
1735
1785
  };
1736
1786
  }
1737
1787
  getHeaders() {
@@ -1742,6 +1792,7 @@ var WorkflowHeaders = class {
1742
1792
  this.addUserHeaders();
1743
1793
  this.addInvokeCount();
1744
1794
  this.addFailureUrl();
1795
+ this.addStepSettings();
1745
1796
  const contentType = this.addContentType();
1746
1797
  return this.prefixHeaders(contentType);
1747
1798
  }
@@ -1751,7 +1802,7 @@ var WorkflowHeaders = class {
1751
1802
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1752
1803
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1753
1804
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1754
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1805
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
1755
1806
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1756
1807
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1757
1808
  };
@@ -1829,7 +1880,7 @@ var WorkflowHeaders = class {
1829
1880
  this.headers.failureHeaders["Workflow-Init"] = "false";
1830
1881
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1831
1882
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1832
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1883
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
1833
1884
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1834
1885
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1835
1886
  }
@@ -1837,6 +1888,42 @@ var WorkflowHeaders = class {
1837
1888
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
1838
1889
  }
1839
1890
  }
1891
+ /**
1892
+ * Applies the step-level settings of a step to the message.
1893
+ *
1894
+ * These headers configure the message itself, so that QStash uses them
1895
+ * instead of the settings the run was triggered with. The delivery of
1896
+ * the message is what executes the step, which is how a step's
1897
+ * settings reach it.
1898
+ *
1899
+ * When any setting is present the feature set is extended with
1900
+ * `WF_StepConfig`, which is what tells QStash to keep them. QStash
1901
+ * reports that back on the delivery as `Upstash-Workflow-Step-Config`.
1902
+ */
1903
+ addStepSettings() {
1904
+ if (!this.stepSettings) {
1905
+ return;
1906
+ }
1907
+ const headers = {};
1908
+ if (this.stepSettings.flowControl) {
1909
+ const { flowControlKey, flowControlValue } = prepareFlowControl(
1910
+ this.stepSettings.flowControl
1911
+ );
1912
+ headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
1913
+ headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
1914
+ }
1915
+ if (this.stepSettings.retries !== void 0) {
1916
+ headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
1917
+ }
1918
+ if (this.stepSettings.retryDelay) {
1919
+ headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
1920
+ }
1921
+ if (Object.keys(headers).length === 0) {
1922
+ return;
1923
+ }
1924
+ headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
1925
+ this.headers.stepSettingsHeaders = headers;
1926
+ }
1840
1927
  addContentType() {
1841
1928
  if (this.workflowConfig.useJSONContent) {
1842
1929
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -1850,14 +1937,17 @@ var WorkflowHeaders = class {
1850
1937
  return contentType;
1851
1938
  }
1852
1939
  prefixHeaders(contentType) {
1853
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
1940
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
1854
1941
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
1855
1942
  return {
1856
1943
  headers: {
1857
1944
  ...rawHeaders,
1858
1945
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
1859
1946
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
1860
- ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
1947
+ ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
1948
+ // last, so they override the run's settings above. Never
1949
+ // prefixed: they configure this message, not its callback.
1950
+ ...stepSettingsHeaders
1861
1951
  },
1862
1952
  contentType
1863
1953
  };
@@ -1908,7 +1998,7 @@ var submitParallelSteps = async ({
1908
1998
  info: `Submitting ${planSteps.length} parallel steps.`
1909
1999
  });
1910
2000
  const result = await context.qstashClient.batch(
1911
- planSteps.map((planStep) => {
2001
+ planSteps.map((planStep, index) => {
1912
2002
  const { headers } = getHeaders({
1913
2003
  initHeaderValue: "false",
1914
2004
  workflowConfig: {
@@ -1916,7 +2006,8 @@ var submitParallelSteps = async ({
1916
2006
  workflowUrl: context.url,
1917
2007
  telemetry: telemetry2
1918
2008
  },
1919
- invokeCount
2009
+ invokeCount,
2010
+ stepSettings: steps[index].stepSettings
1920
2011
  });
1921
2012
  return {
1922
2013
  headers,
@@ -1935,31 +2026,38 @@ var submitParallelSteps = async ({
1935
2026
  }
1936
2027
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
1937
2028
  };
1938
- var submitSingleStep = async ({
1939
- context,
2029
+ var executeStep = async ({
1940
2030
  lazyStep,
1941
2031
  stepId,
1942
- invokeCount,
1943
2032
  concurrency,
1944
- telemetry: telemetry2,
1945
- dispatchDebug,
1946
2033
  dispatchLifecycle
1947
2034
  }) => {
1948
2035
  await dispatchLifecycle("beforeExecution", {
1949
2036
  stepName: lazyStep.stepName
1950
2037
  });
1951
- let resultStep;
1952
2038
  try {
1953
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2039
+ return await lazyStep.getResultStep(concurrency, stepId);
1954
2040
  } catch (error) {
1955
2041
  attachStepNameToError(error, lazyStep.stepName);
1956
2042
  throw error;
1957
2043
  }
2044
+ };
2045
+ var submitStepResult = async ({
2046
+ context,
2047
+ lazyStep,
2048
+ resultStep,
2049
+ invokeCount,
2050
+ concurrency,
2051
+ telemetry: telemetry2,
2052
+ dispatchDebug,
2053
+ nextStepSettings
2054
+ }) => {
1958
2055
  const { headers } = lazyStep.getHeaders({
1959
2056
  context,
1960
2057
  step: resultStep,
1961
2058
  invokeCount,
1962
- telemetry: telemetry2
2059
+ telemetry: telemetry2,
2060
+ stepSettings: nextStepSettings
1963
2061
  });
1964
2062
  const body = lazyStep.getBody({
1965
2063
  context,
@@ -1984,6 +2082,138 @@ var submitSingleStep = async ({
1984
2082
  }
1985
2083
  return resultStep;
1986
2084
  };
2085
+ var publishStepConfigRequest = async ({
2086
+ context,
2087
+ lazyStep,
2088
+ targetStep,
2089
+ invokeCount,
2090
+ telemetry: telemetry2,
2091
+ dispatchDebug
2092
+ }) => {
2093
+ const { headers } = getHeaders({
2094
+ initHeaderValue: "false",
2095
+ workflowConfig: {
2096
+ workflowRunId: context.workflowRunId,
2097
+ workflowUrl: context.url,
2098
+ telemetry: telemetry2
2099
+ },
2100
+ invokeCount,
2101
+ stepSettings: lazyStep.stepSettings
2102
+ });
2103
+ await dispatchDebug("onInfo", {
2104
+ info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
2105
+ });
2106
+ const result = await context.qstashClient.publishJSON({
2107
+ headers: {
2108
+ ...headers,
2109
+ "Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
2110
+ },
2111
+ method: "POST",
2112
+ body: { targetStep, invokeCount },
2113
+ url: context.url,
2114
+ contentBasedDeduplication: true
2115
+ });
2116
+ if (result?.deduplicated) {
2117
+ await dispatchDebug("onWarning", {
2118
+ 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.`
2119
+ });
2120
+ } else if (result?.messageId) {
2121
+ await dispatchDebug("onInfo", {
2122
+ info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
2123
+ });
2124
+ }
2125
+ };
2126
+
2127
+ // src/qstash/step-config.ts
2128
+ var DEFAULT_PARALLELISM = 0;
2129
+ var DEFAULT_RATE = 0;
2130
+ var DEFAULT_PERIOD_SECONDS = 1;
2131
+ var SECONDS_PER_UNIT = {
2132
+ s: 1,
2133
+ m: 60,
2134
+ h: 60 * 60,
2135
+ d: 24 * 60 * 60
2136
+ };
2137
+ var durationToSeconds = (duration) => {
2138
+ if (typeof duration === "number") {
2139
+ return Number.isFinite(duration) ? duration : void 0;
2140
+ }
2141
+ const match = /^(\d+)([smhd])$/.exec(duration.trim());
2142
+ if (match) {
2143
+ return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
2144
+ }
2145
+ const seconds = Number(duration.trim());
2146
+ return Number.isFinite(seconds) ? seconds : void 0;
2147
+ };
2148
+ var normalizeFlowControl = (flowControl) => {
2149
+ const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
2150
+ return {
2151
+ key: flowControl.key,
2152
+ parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
2153
+ rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
2154
+ period: period ?? DEFAULT_PERIOD_SECONDS
2155
+ };
2156
+ };
2157
+ var parseFlowControlHeaders = (key, value) => {
2158
+ if (!key) {
2159
+ return void 0;
2160
+ }
2161
+ const flowControl = {
2162
+ key,
2163
+ parallelism: DEFAULT_PARALLELISM,
2164
+ rate: DEFAULT_RATE,
2165
+ period: DEFAULT_PERIOD_SECONDS
2166
+ };
2167
+ for (const entry of (value ?? "").split(",")) {
2168
+ const [name, rawValue] = entry.split("=").map((part) => part.trim());
2169
+ if (!name || rawValue === void 0) {
2170
+ continue;
2171
+ }
2172
+ switch (name) {
2173
+ case "parallelism": {
2174
+ flowControl.parallelism = Number(rawValue);
2175
+ break;
2176
+ }
2177
+ case "rate": {
2178
+ flowControl.rate = Number(rawValue);
2179
+ break;
2180
+ }
2181
+ case "period": {
2182
+ flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
2183
+ break;
2184
+ }
2185
+ }
2186
+ }
2187
+ return flowControl;
2188
+ };
2189
+ var getEffectiveConfig = (headers) => {
2190
+ const retriesHeader = headers.get(RETRIES_HEADER);
2191
+ return {
2192
+ flowControl: parseFlowControlHeaders(
2193
+ headers.get(FLOW_CONTROL_KEY_HEADER),
2194
+ headers.get(FLOW_CONTROL_VALUE_HEADER)
2195
+ ),
2196
+ retries: retriesHeader === null ? void 0 : Number(retriesHeader),
2197
+ retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
2198
+ hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
2199
+ };
2200
+ };
2201
+ var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
2202
+ if (stepSettings.flowControl) {
2203
+ const wanted = normalizeFlowControl(stepSettings.flowControl);
2204
+ const applied = effectiveConfig.flowControl;
2205
+ if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
2206
+ return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
2207
+ }
2208
+ }
2209
+ if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
2210
+ return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
2211
+ }
2212
+ if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
2213
+ return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
2214
+ }
2215
+ return void 0;
2216
+ };
1987
2217
 
1988
2218
  // src/context/auto-executor.ts
1989
2219
  var AutoExecutor = class _AutoExecutor {
@@ -2000,6 +2230,25 @@ var AutoExecutor = class _AutoExecutor {
2000
2230
  stepCount = 0;
2001
2231
  planStepCount = 0;
2002
2232
  executingStep = false;
2233
+ /**
2234
+ * an executed step whose result hasn't been submitted to QStash yet.
2235
+ *
2236
+ * Set when a step which can produce its result in-process executes. The
2237
+ * submission waits so that the route function can continue and reveal
2238
+ * what comes next: if that is a single step with step-level settings,
2239
+ * the settings ride on this submission and no step config request is
2240
+ * needed, since the delivery of the submission is what executes it.
2241
+ *
2242
+ * Flushed when the next step is reached (see `addStep`) or when the
2243
+ * route function ends (see `flushPendingStep` in `serve`).
2244
+ */
2245
+ pendingStep;
2246
+ /**
2247
+ * configuration QStash applied to the delivery being handled. A step
2248
+ * with step-level settings is settled by comparing its settings against
2249
+ * this.
2250
+ */
2251
+ effectiveConfig;
2003
2252
  /**
2004
2253
  * @param context workflow context
2005
2254
  * @param steps list of steps
@@ -2007,14 +2256,17 @@ var AutoExecutor = class _AutoExecutor {
2007
2256
  * @param dispatchLifecycle lifecycle event dispatcher
2008
2257
  * @param telemetry optional telemetry information
2009
2258
  * @param invokeCount optional invoke count
2259
+ * @param effectiveConfig configuration QStash applied to the delivery
2260
+ * being handled
2010
2261
  */
2011
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount) {
2262
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount, effectiveConfig) {
2012
2263
  this.context = context;
2013
2264
  this.steps = steps;
2014
2265
  this.dispatchDebug = dispatchDebug;
2015
2266
  this.dispatchLifecycle = dispatchLifecycle;
2016
2267
  this.telemetry = telemetry2;
2017
2268
  this.invokeCount = invokeCount ?? 0;
2269
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2018
2270
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2019
2271
  }
2020
2272
  /**
@@ -2049,6 +2301,15 @@ var AutoExecutor = class _AutoExecutor {
2049
2301
  lazyStepList.push(stepInfo);
2050
2302
  const index = this.indexInCurrentList++;
2051
2303
  const requestComplete = this.deferExecution().then(async () => {
2304
+ const submitted = await this.submitPendingStep(
2305
+ lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
2306
+ );
2307
+ if (submitted.isErr()) {
2308
+ throw submitted.error;
2309
+ }
2310
+ if (submitted.value.result === "submitted-step") {
2311
+ throw submitted.value.abort;
2312
+ }
2052
2313
  if (!this.promises.has(lazyStepList)) {
2053
2314
  const promise2 = this.getExecutionPromise(lazyStepList);
2054
2315
  this.promises.set(lazyStepList, promise2);
@@ -2102,18 +2363,100 @@ var AutoExecutor = class _AutoExecutor {
2102
2363
  }
2103
2364
  return parsedOut;
2104
2365
  }
2105
- const resultStep = await submitSingleStep({
2106
- context: this.context,
2366
+ const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
2367
+ if (mismatch) {
2368
+ if (!this.effectiveConfig.hasStepConfig) {
2369
+ await publishStepConfigRequest({
2370
+ context: this.context,
2371
+ lazyStep,
2372
+ targetStep: this.stepCount,
2373
+ invokeCount: this.invokeCount,
2374
+ telemetry: this.telemetry,
2375
+ dispatchDebug: this.dispatchDebug
2376
+ });
2377
+ throw new WorkflowAbort(lazyStep.stepName);
2378
+ } else {
2379
+ await this.dispatchDebug("onWarning", {
2380
+ 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.`
2381
+ });
2382
+ }
2383
+ }
2384
+ const resultStep = await executeStep({
2107
2385
  lazyStep,
2108
2386
  stepId: this.stepCount,
2387
+ concurrency: NO_CONCURRENCY,
2388
+ dispatchLifecycle: this.dispatchLifecycle
2389
+ });
2390
+ if (lazyStep.supportsDeferredSubmission) {
2391
+ this.pendingStep = { status: "held", lazyStep, resultStep };
2392
+ return lazyStep.parseOut({
2393
+ ...resultStep,
2394
+ out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
2395
+ });
2396
+ }
2397
+ await submitStepResult({
2398
+ context: this.context,
2399
+ lazyStep,
2400
+ resultStep,
2109
2401
  invokeCount: this.invokeCount,
2110
- concurrency: 1,
2402
+ concurrency: NO_CONCURRENCY,
2111
2403
  telemetry: this.telemetry,
2112
- dispatchDebug: this.dispatchDebug,
2113
- dispatchLifecycle: this.dispatchLifecycle
2404
+ dispatchDebug: this.dispatchDebug
2114
2405
  });
2115
2406
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2116
2407
  }
2408
+ /**
2409
+ * Submits the result of the step being held, if there is one, and
2410
+ * returns the abort the caller has to throw to end the invocation.
2411
+ *
2412
+ * Returns undefined when no step is being held, which is the caller's
2413
+ * signal that there is nothing to end. Throws instead of returning
2414
+ * when the submission itself fails, so that error reaches the caller
2415
+ * rather than an abort claiming the step was submitted.
2416
+ *
2417
+ * Callers which arrive while a submission is already under way (the
2418
+ * steps of a parallel group, added together) wait for that one and get
2419
+ * the same abort, rather than submitting a second time.
2420
+ *
2421
+ * The next step's settings are attached whenever it has any, without
2422
+ * checking them against the current delivery: the executor only knows
2423
+ * the configuration of the delivery in hand, which inside a
2424
+ * delivery is the *previous* step's, so such a check would be wrong
2425
+ * exactly when two steps with settings follow each other.
2426
+ *
2427
+ * @param nextStepSettings step-level settings of the next step
2428
+ * @returns `submitted-step` with the abort which ends this invocation,
2429
+ * or `no-pending-step` when nothing was held and there is nothing to
2430
+ * end
2431
+ */
2432
+ async submitPendingStep(nextStepSettings) {
2433
+ if (!this.pendingStep) {
2434
+ return ok({ result: "no-pending-step" });
2435
+ }
2436
+ try {
2437
+ if (this.pendingStep.status === "submitting") {
2438
+ return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
2439
+ }
2440
+ const { lazyStep, resultStep } = this.pendingStep;
2441
+ const submitted = (async () => {
2442
+ await submitStepResult({
2443
+ context: this.context,
2444
+ lazyStep,
2445
+ resultStep,
2446
+ invokeCount: this.invokeCount,
2447
+ concurrency: NO_CONCURRENCY,
2448
+ telemetry: this.telemetry,
2449
+ dispatchDebug: this.dispatchDebug,
2450
+ nextStepSettings
2451
+ });
2452
+ return new WorkflowAbort(lazyStep.stepName, resultStep);
2453
+ })();
2454
+ this.pendingStep = { status: "submitting", submitted };
2455
+ return ok({ result: "submitted-step", abort: await submitted });
2456
+ } catch (error) {
2457
+ return err(error);
2458
+ }
2459
+ }
2117
2460
  /**
2118
2461
  * Runs steps in parallel.
2119
2462
  *
@@ -2162,15 +2505,20 @@ var AutoExecutor = class _AutoExecutor {
2162
2505
  validateStep(parallelSteps[stepIndex], planStep);
2163
2506
  try {
2164
2507
  const parallelStep = parallelSteps[stepIndex];
2165
- const resultStep = await submitSingleStep({
2166
- context: this.context,
2508
+ const resultStep = await executeStep({
2167
2509
  lazyStep: parallelStep,
2168
2510
  stepId: planStep.targetStep,
2511
+ concurrency: parallelSteps.length,
2512
+ dispatchLifecycle: this.dispatchLifecycle
2513
+ });
2514
+ await submitStepResult({
2515
+ context: this.context,
2516
+ lazyStep: parallelStep,
2517
+ resultStep,
2169
2518
  invokeCount: this.invokeCount,
2170
2519
  concurrency: parallelSteps.length,
2171
2520
  telemetry: this.telemetry,
2172
- dispatchDebug: this.dispatchDebug,
2173
- dispatchLifecycle: this.dispatchLifecycle
2521
+ dispatchDebug: this.dispatchDebug
2174
2522
  });
2175
2523
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2176
2524
  } catch (error) {
@@ -2786,6 +3134,46 @@ var WorkflowContext = class {
2786
3134
  * `1` on the first retry, `2` on the second, and so on.
2787
3135
  */
2788
3136
  retried;
3137
+ /**
3138
+ * Configuration QStash applied to the request being handled.
3139
+ *
3140
+ * This is the configuration of *this delivery*, not of the run: it is
3141
+ * the configuration the run was triggered with, except inside the
3142
+ * delivery which executes a step that has step-level settings, where
3143
+ * it is that step's settings. The fields below expose it; the executor
3144
+ * gets it directly rather than reaching through the context.
3145
+ */
3146
+ effectiveConfig;
3147
+ /**
3148
+ * Flow control QStash applied to the request being handled, if any.
3149
+ *
3150
+ * This is the flow control of the request in hand, which is the one the
3151
+ * run was triggered with except inside a step carrying its own.
3152
+ */
3153
+ get flowControl() {
3154
+ return this.effectiveConfig.flowControl;
3155
+ }
3156
+ /**
3157
+ * Retry limit QStash applied to the request being handled, if it
3158
+ * reported one.
3159
+ *
3160
+ * Not to be confused with {@link retried}, which is how many retries
3161
+ * have already happened.
3162
+ *
3163
+ * @see {@link flowControl} for what "the request being handled" means
3164
+ */
3165
+ get retries() {
3166
+ return this.effectiveConfig.retries;
3167
+ }
3168
+ /**
3169
+ * Retry delay expression QStash applied to the request being handled,
3170
+ * if any.
3171
+ *
3172
+ * @see {@link flowControl} for what "the request being handled" means
3173
+ */
3174
+ get retryDelay() {
3175
+ return this.effectiveConfig.retryDelay;
3176
+ }
2789
3177
  constructor({
2790
3178
  qstashClient,
2791
3179
  workflowRunId,
@@ -2799,7 +3187,8 @@ var WorkflowContext = class {
2799
3187
  invokeCount,
2800
3188
  label,
2801
3189
  retried,
2802
- middlewareManager
3190
+ middlewareManager,
3191
+ effectiveConfig
2803
3192
  }) {
2804
3193
  this.qstashClient = qstashClient;
2805
3194
  this.workflowRunId = workflowRunId;
@@ -2811,6 +3200,7 @@ var WorkflowContext = class {
2811
3200
  this.env = env ?? {};
2812
3201
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2813
3202
  this.retried = retried ?? 0;
3203
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2814
3204
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2815
3205
  middlewareManagerInstance.assignContext(this);
2816
3206
  this.executor = new AutoExecutor(
@@ -2819,7 +3209,8 @@ var WorkflowContext = class {
2819
3209
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2820
3210
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2821
3211
  telemetry2,
2822
- invokeCount
3212
+ invokeCount,
3213
+ this.effectiveConfig
2823
3214
  );
2824
3215
  }
2825
3216
  /**
@@ -2845,13 +3236,35 @@ var WorkflowContext = class {
2845
3236
  * ])
2846
3237
  * ```
2847
3238
  *
3239
+ * Step-level settings can be passed as a third argument, overriding
3240
+ * the settings the workflow run was triggered with for this step only:
3241
+ *
3242
+ * ```typescript
3243
+ * const result = await context.run(
3244
+ * "step 1",
3245
+ * () => {
3246
+ * return "result"
3247
+ * },
3248
+ * {
3249
+ * flowControl: { key: "custom-key", parallelism: 3 },
3250
+ * retries: 5,
3251
+ * }
3252
+ * )
3253
+ * ```
3254
+ *
2848
3255
  * @param stepName name of the step
2849
3256
  * @param stepFunction step function to be executed
3257
+ * @param stepSettings step-level settings for this step
2850
3258
  * @returns result of the step function
2851
3259
  */
2852
- async run(stepName, stepFunction) {
3260
+ async run(stepName, stepFunction, stepSettings) {
3261
+ if (stepSettings) {
3262
+ validateFlowControl(stepSettings.flowControl);
3263
+ }
2853
3264
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2854
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3265
+ return await this.addStep(
3266
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3267
+ );
2855
3268
  }
2856
3269
  /**
2857
3270
  * Stops the execution for the duration provided.
@@ -3074,7 +3487,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3074
3487
  *
3075
3488
  * @param routeFunction
3076
3489
  */
3077
- static async tryAuthentication(routeFunction, context) {
3490
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3078
3491
  const disabledContext = new _DisabledWorkflowContext({
3079
3492
  qstashClient: new import_qstash10.Client({
3080
3493
  baseUrl: "disabled-client",
@@ -3089,7 +3502,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3089
3502
  initialPayload: context.requestPayload,
3090
3503
  env: context.env,
3091
3504
  label: context.labels,
3092
- retried: context.retried
3505
+ retried: context.retried,
3506
+ // the route function runs for real here until it reaches a step, so
3507
+ // it observes the same configuration as it will on the real context
3508
+ effectiveConfig
3093
3509
  });
3094
3510
  try {
3095
3511
  await routeFunction(disabledContext);
@@ -3306,6 +3722,7 @@ var handleFailure = async ({
3306
3722
  }
3307
3723
  const userHeaders = recreateUserHeaders(request.headers);
3308
3724
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3725
+ const effectiveConfig = getEffectiveConfig(request.headers);
3309
3726
  const workflowContext = new WorkflowContext({
3310
3727
  qstashClient,
3311
3728
  workflowRunId,
@@ -3319,11 +3736,13 @@ var handleFailure = async ({
3319
3736
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3320
3737
  retried,
3321
3738
  workflowRunCreatedAt: workflowCreatedAt,
3322
- middlewareManager: void 0
3739
+ middlewareManager: void 0,
3740
+ effectiveConfig
3323
3741
  });
3324
3742
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3325
3743
  routeFunction,
3326
- workflowContext
3744
+ workflowContext,
3745
+ effectiveConfig
3327
3746
  );
3328
3747
  if (authCheck.isErr()) {
3329
3748
  await dispatchDebug?.("onError", {
@@ -3415,7 +3834,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3415
3834
  const client = new import_qstash11.Client({
3416
3835
  ...clientOptions,
3417
3836
  baseUrl: clientEnv.QSTASH_URL,
3418
- token: clientEnv.QSTASH_TOKEN
3837
+ token: clientEnv.QSTASH_TOKEN,
3838
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3839
+ // the client at the local dev server too. Without this the QStash Client
3840
+ // re-derives dev mode from its own process.env, which is blind to the
3841
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3842
+ // An explicit devMode in the user's client config still wins.
3843
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3419
3844
  });
3420
3845
  const receiver = getReceiver(environment, receiverConfig, region);
3421
3846
  return { client, receiver };
@@ -3465,7 +3890,9 @@ var getQStashHandlers = ({
3465
3890
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3466
3891
  ...qstashClientOption,
3467
3892
  baseUrl: environment.QSTASH_URL,
3468
- token: environment.QSTASH_TOKEN
3893
+ token: environment.QSTASH_TOKEN,
3894
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
3895
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3469
3896
  }),
3470
3897
  receiver: getReceiver(environment, receiverConfig)
3471
3898
  }
@@ -3805,12 +4232,14 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3805
4232
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3806
4233
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3807
4234
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4235
+ const effectiveConfig = getEffectiveConfig(request.headers);
3808
4236
  const workflowContext = new WorkflowContext({
3809
4237
  qstashClient: regionalClient,
3810
4238
  workflowRunId,
3811
4239
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
3812
4240
  headers: recreateUserHeaders(request.headers),
3813
4241
  steps,
4242
+ effectiveConfig,
3814
4243
  url: workflowUrl,
3815
4244
  env,
3816
4245
  telemetry: telemetry2,
@@ -3822,7 +4251,8 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3822
4251
  });
3823
4252
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3824
4253
  routeFunction,
3825
- workflowContext
4254
+ workflowContext,
4255
+ effectiveConfig
3826
4256
  );
3827
4257
  if (authCheck.isErr()) {
3828
4258
  throw authCheck.error;
@@ -3859,7 +4289,23 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3859
4289
  if (steps.length === 1) {
3860
4290
  await middlewareManager.dispatchLifecycle("runStarted", {});
3861
4291
  }
3862
- return await routeFunction(workflowContext);
4292
+ let outcome;
4293
+ try {
4294
+ outcome = { ran: true, result: await routeFunction(workflowContext) };
4295
+ } catch (error) {
4296
+ outcome = { ran: false, error };
4297
+ }
4298
+ const submitted = await flushPendingStep(workflowContext);
4299
+ if (submitted.isErr()) {
4300
+ throw submitted.error;
4301
+ }
4302
+ if (submitted.value.result === "submitted-step") {
4303
+ throw submitted.value.abort;
4304
+ }
4305
+ if (!outcome.ran) {
4306
+ throw outcome.error;
4307
+ }
4308
+ return outcome.result;
3863
4309
  },
3864
4310
  onCleanup: async (result2) => {
3865
4311
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -3941,6 +4387,7 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3941
4387
 
3942
4388
  // platforms/express.ts
3943
4389
  var import_express = require("express");
4390
+ var import_qstash13 = require("@upstash/qstash");
3944
4391
  var isEmptyRequest = (req) => {
3945
4392
  return req.headers["content-type"] === "application/json" && req.headers["content-length"] === "0";
3946
4393
  };
@@ -3991,6 +4438,7 @@ function createExpressHandler(params) {
3991
4438
  };
3992
4439
  }
3993
4440
  function serve(routeFunction, options) {
4441
+ void (0, import_qstash13.startDevServer)();
3994
4442
  const router = (0, import_express.Router)();
3995
4443
  const handler = createExpressHandler([routeFunction, options]);
3996
4444
  router.all(/(.*)/, handler);
@@ -4006,6 +4454,7 @@ var createWorkflow = (...params) => {
4006
4454
  };
4007
4455
  };
4008
4456
  var serveMany = (workflows, options) => {
4457
+ void (0, import_qstash13.startDevServer)();
4009
4458
  const router = (0, import_express.Router)();
4010
4459
  const { handler } = serveManyBase({
4011
4460
  workflows,