@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/nextjs.js CHANGED
@@ -29,6 +29,9 @@ __export(nextjs_exports, {
29
29
  });
30
30
  module.exports = __toCommonJS(nextjs_exports);
31
31
 
32
+ // src/serve/index.ts
33
+ var import_qstash12 = require("@upstash/qstash");
34
+
32
35
  // src/client/utils.ts
33
36
  var import_qstash2 = require("@upstash/qstash");
34
37
 
@@ -217,6 +220,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
217
220
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
218
221
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
219
222
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
223
+ var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
224
+ var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
225
+ var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
226
+ var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
227
+ var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
228
+ var RETRIES_HEADER = "Upstash-Retries";
229
+ var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
230
+ var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
220
231
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
221
232
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
222
233
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -234,102 +245,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
234
245
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
235
246
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
236
247
 
237
- // src/context/auto-executor.ts
238
- var import_qstash5 = require("@upstash/qstash");
239
-
240
- // src/qstash/headers.ts
241
- var import_qstash4 = require("@upstash/qstash");
242
-
243
- // src/utils.ts
244
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
245
- var NANOID_LENGTH = 21;
246
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
247
- function validateLabel(label) {
248
- if (label === void 0) return;
249
- const labels = Array.isArray(label) ? label : [label];
250
- if (labels.length === 0) {
251
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
252
- }
253
- for (const value of labels) {
254
- if (!RESOURCE_NAME_PATTERN.test(value)) {
255
- throw new WorkflowNonRetryableError(
256
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
257
- );
258
- }
259
- }
260
- }
261
- function serializeLabel(label) {
262
- return Array.isArray(label) ? label.join(",") : label;
263
- }
264
- function validateFlowControl(flowControl) {
265
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
266
- throw new WorkflowNonRetryableError(
267
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
268
- );
269
- }
270
- }
271
- function getRandomInt() {
272
- return Math.floor(Math.random() * NANOID_CHARS.length);
273
- }
274
- function nanoid(length = NANOID_LENGTH) {
275
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
276
- }
277
- function getWorkflowRunId(id) {
278
- return `wfr_${id ?? nanoid()}`;
279
- }
280
- function decodeBase64(base64) {
281
- const binString = atob(base64);
282
- try {
283
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
284
- return new TextDecoder().decode(intArray);
285
- } catch (error) {
286
- console.warn(
287
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
288
- );
289
- return binString;
290
- }
291
- }
292
- function getUserIdFromToken(qstashClient) {
293
- try {
294
- const token = qstashClient.token;
295
- const decodedToken = decodeBase64(token);
296
- const tokenPayload = JSON.parse(decodedToken);
297
- const userId = tokenPayload.UserID;
298
- if (!userId) {
299
- throw new WorkflowError("QStash token payload does not contain userId");
300
- }
301
- return userId;
302
- } catch (error) {
303
- throw new WorkflowError(
304
- `Failed to decode QStash token while running create webhook step: ${error.message}`
305
- );
306
- }
307
- }
308
- function getQStashUrl(qstashClient) {
309
- try {
310
- const requester = qstashClient.http;
311
- const baseUrl = requester.baseUrl;
312
- if (!baseUrl) {
313
- throw new WorkflowError("QStash client does not have a baseUrl");
314
- }
315
- return baseUrl;
316
- } catch (error) {
317
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
318
- }
319
- }
320
- function getEventId() {
321
- return `evt_${nanoid(15)}`;
322
- }
323
- function stringifyBody(body) {
324
- if (body === void 0) {
325
- return void 0;
326
- }
327
- if (typeof body === "string") {
328
- return body;
329
- }
330
- return JSON.stringify(body);
331
- }
332
-
333
248
  // node_modules/neverthrow/dist/index.es.js
334
249
  var defaultErrorConfig = {
335
250
  withStackTrace: false
@@ -745,6 +660,102 @@ var Err = class {
745
660
  };
746
661
  var fromThrowable = Result.fromThrowable;
747
662
 
663
+ // src/context/auto-executor.ts
664
+ var import_qstash5 = require("@upstash/qstash");
665
+
666
+ // src/qstash/headers.ts
667
+ var import_qstash4 = require("@upstash/qstash");
668
+
669
+ // src/utils.ts
670
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
671
+ var NANOID_LENGTH = 21;
672
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
673
+ function validateLabel(label) {
674
+ if (label === void 0) return;
675
+ const labels = Array.isArray(label) ? label : [label];
676
+ if (labels.length === 0) {
677
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
678
+ }
679
+ for (const value of labels) {
680
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
681
+ throw new WorkflowNonRetryableError(
682
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
683
+ );
684
+ }
685
+ }
686
+ }
687
+ function serializeLabel(label) {
688
+ return Array.isArray(label) ? label.join(",") : label;
689
+ }
690
+ function validateFlowControl(flowControl) {
691
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
692
+ throw new WorkflowNonRetryableError(
693
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
694
+ );
695
+ }
696
+ }
697
+ function getRandomInt() {
698
+ return Math.floor(Math.random() * NANOID_CHARS.length);
699
+ }
700
+ function nanoid(length = NANOID_LENGTH) {
701
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
702
+ }
703
+ function getWorkflowRunId(id) {
704
+ return `wfr_${id ?? nanoid()}`;
705
+ }
706
+ function decodeBase64(base64) {
707
+ const binString = atob(base64);
708
+ try {
709
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
710
+ return new TextDecoder().decode(intArray);
711
+ } catch (error) {
712
+ console.warn(
713
+ `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
714
+ );
715
+ return binString;
716
+ }
717
+ }
718
+ function getUserIdFromToken(qstashClient) {
719
+ try {
720
+ const token = qstashClient.token;
721
+ const decodedToken = decodeBase64(token);
722
+ const tokenPayload = JSON.parse(decodedToken);
723
+ const userId = tokenPayload.UserID;
724
+ if (!userId) {
725
+ throw new WorkflowError("QStash token payload does not contain userId");
726
+ }
727
+ return userId;
728
+ } catch (error) {
729
+ throw new WorkflowError(
730
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
731
+ );
732
+ }
733
+ }
734
+ function getQStashUrl(qstashClient) {
735
+ try {
736
+ const requester = qstashClient.http;
737
+ const baseUrl = requester.baseUrl;
738
+ if (!baseUrl) {
739
+ throw new WorkflowError("QStash client does not have a baseUrl");
740
+ }
741
+ return baseUrl;
742
+ } catch (error) {
743
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
744
+ }
745
+ }
746
+ function getEventId() {
747
+ return `evt_${nanoid(15)}`;
748
+ }
749
+ function stringifyBody(body) {
750
+ if (body === void 0) {
751
+ return void 0;
752
+ }
753
+ if (typeof body === "string") {
754
+ return body;
755
+ }
756
+ return JSON.stringify(body);
757
+ }
758
+
748
759
  // src/types.ts
749
760
  var StepTypes = [
750
761
  "Initial",
@@ -850,6 +861,10 @@ var triggerFirstInvocation = async (params) => {
850
861
  return err(error_);
851
862
  }
852
863
  };
864
+ var flushPendingStep = async (workflowContext) => {
865
+ const { executor } = workflowContext;
866
+ return await executor.submitPendingStep();
867
+ };
853
868
  var triggerRouteFunction = async ({
854
869
  onCleanup,
855
870
  onStep,
@@ -1057,8 +1072,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1057
1072
  var BaseLazyStep = class _BaseLazyStep {
1058
1073
  stepName;
1059
1074
  context;
1060
- constructor(context, stepName) {
1075
+ /**
1076
+ * step-level settings (flow control, retries etc.) which override the
1077
+ * settings the workflow run was triggered with, for this step only.
1078
+ */
1079
+ stepSettings;
1080
+ /**
1081
+ * whether this step's result can be submitted after the route function
1082
+ * has moved on, instead of right away.
1083
+ *
1084
+ * Enabled for steps whose `getResultStep` produces the final `out` —
1085
+ * which is not the same as "needs no server round trip":
1086
+ * `LazyCreateWebhookStep` builds its result in `getBody` and returns
1087
+ * `out: undefined` from `getResultStep`, so deferring it would hand
1088
+ * `undefined` to the route function.
1089
+ *
1090
+ * Deferring lets the route function continue and reveal the next step,
1091
+ * so that step's settings can ride on this step's submission instead
1092
+ * of needing a step config request of their own.
1093
+ */
1094
+ supportsDeferredSubmission = false;
1095
+ constructor(context, stepName, stepSettings) {
1061
1096
  this.context = context;
1097
+ this.stepSettings = stepSettings;
1062
1098
  if (!stepName) {
1063
1099
  throw new WorkflowError(
1064
1100
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1122,7 +1158,13 @@ var BaseLazyStep = class _BaseLazyStep {
1122
1158
  step.out = JSON.stringify(step.out);
1123
1159
  return JSON.stringify(step);
1124
1160
  }
1125
- getHeaders({ context, telemetry, invokeCount, step }) {
1161
+ getHeaders({
1162
+ context,
1163
+ telemetry,
1164
+ invokeCount,
1165
+ step,
1166
+ stepSettings
1167
+ }) {
1126
1168
  return getHeaders({
1127
1169
  initHeaderValue: "false",
1128
1170
  workflowConfig: {
@@ -1135,7 +1177,8 @@ var BaseLazyStep = class _BaseLazyStep {
1135
1177
  stepInfo: {
1136
1178
  step,
1137
1179
  lazyStep: this
1138
- }
1180
+ },
1181
+ stepSettings
1139
1182
  });
1140
1183
  }
1141
1184
  async submitStep({ context, body, headers }) {
@@ -1153,8 +1196,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1153
1196
  stepFunction;
1154
1197
  stepType = "Run";
1155
1198
  allowUndefinedOut = true;
1156
- constructor(context, stepName, stepFunction) {
1157
- super(context, stepName);
1199
+ supportsDeferredSubmission = true;
1200
+ constructor(context, stepName, stepFunction, stepSettings) {
1201
+ super(context, stepName, stepSettings);
1158
1202
  this.stepFunction = stepFunction;
1159
1203
  }
1160
1204
  getPlanStep(concurrent, targetStep) {
@@ -1184,6 +1228,7 @@ var LazySleepStep = class extends BaseLazyStep {
1184
1228
  sleep;
1185
1229
  stepType = "SleepFor";
1186
1230
  allowUndefinedOut = true;
1231
+ supportsDeferredSubmission = true;
1187
1232
  constructor(context, stepName, sleep) {
1188
1233
  super(context, stepName);
1189
1234
  this.sleep = sleep;
@@ -1223,6 +1268,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1223
1268
  sleepUntil;
1224
1269
  stepType = "SleepUntil";
1225
1270
  allowUndefinedOut = true;
1271
+ supportsDeferredSubmission = true;
1226
1272
  constructor(context, stepName, sleepUntil) {
1227
1273
  super(context, stepName);
1228
1274
  this.sleepUntil = sleepUntil;
@@ -1715,6 +1761,7 @@ var WorkflowHeaders = class {
1715
1761
  invokeCount;
1716
1762
  initHeaderValue;
1717
1763
  stepInfo;
1764
+ stepSettings;
1718
1765
  headers;
1719
1766
  /**
1720
1767
  * @param params workflow header parameters
@@ -1724,17 +1771,20 @@ var WorkflowHeaders = class {
1724
1771
  workflowConfig,
1725
1772
  invokeCount,
1726
1773
  initHeaderValue,
1727
- stepInfo
1774
+ stepInfo,
1775
+ stepSettings
1728
1776
  }) {
1729
1777
  this.userHeaders = userHeaders;
1730
1778
  this.workflowConfig = workflowConfig;
1731
1779
  this.invokeCount = invokeCount;
1732
1780
  this.initHeaderValue = initHeaderValue;
1733
1781
  this.stepInfo = stepInfo;
1782
+ this.stepSettings = stepSettings;
1734
1783
  this.headers = {
1735
1784
  rawHeaders: {},
1736
1785
  workflowHeaders: {},
1737
- failureHeaders: {}
1786
+ failureHeaders: {},
1787
+ stepSettingsHeaders: {}
1738
1788
  };
1739
1789
  }
1740
1790
  getHeaders() {
@@ -1745,6 +1795,7 @@ var WorkflowHeaders = class {
1745
1795
  this.addUserHeaders();
1746
1796
  this.addInvokeCount();
1747
1797
  this.addFailureUrl();
1798
+ this.addStepSettings();
1748
1799
  const contentType = this.addContentType();
1749
1800
  return this.prefixHeaders(contentType);
1750
1801
  }
@@ -1754,7 +1805,7 @@ var WorkflowHeaders = class {
1754
1805
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1755
1806
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1756
1807
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1757
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1808
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
1758
1809
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1759
1810
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1760
1811
  };
@@ -1832,7 +1883,7 @@ var WorkflowHeaders = class {
1832
1883
  this.headers.failureHeaders["Workflow-Init"] = "false";
1833
1884
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1834
1885
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1835
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1886
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
1836
1887
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1837
1888
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1838
1889
  }
@@ -1840,6 +1891,42 @@ var WorkflowHeaders = class {
1840
1891
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
1841
1892
  }
1842
1893
  }
1894
+ /**
1895
+ * Applies the step-level settings of a step to the message.
1896
+ *
1897
+ * These headers configure the message itself, so that QStash uses them
1898
+ * instead of the settings the run was triggered with. The delivery of
1899
+ * the message is what executes the step, which is how a step's
1900
+ * settings reach it.
1901
+ *
1902
+ * When any setting is present the feature set is extended with
1903
+ * `WF_StepConfig`, which is what tells QStash to keep them. QStash
1904
+ * reports that back on the delivery as `Upstash-Workflow-Step-Config`.
1905
+ */
1906
+ addStepSettings() {
1907
+ if (!this.stepSettings) {
1908
+ return;
1909
+ }
1910
+ const headers = {};
1911
+ if (this.stepSettings.flowControl) {
1912
+ const { flowControlKey, flowControlValue } = prepareFlowControl(
1913
+ this.stepSettings.flowControl
1914
+ );
1915
+ headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
1916
+ headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
1917
+ }
1918
+ if (this.stepSettings.retries !== void 0) {
1919
+ headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
1920
+ }
1921
+ if (this.stepSettings.retryDelay) {
1922
+ headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
1923
+ }
1924
+ if (Object.keys(headers).length === 0) {
1925
+ return;
1926
+ }
1927
+ headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
1928
+ this.headers.stepSettingsHeaders = headers;
1929
+ }
1843
1930
  addContentType() {
1844
1931
  if (this.workflowConfig.useJSONContent) {
1845
1932
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -1853,14 +1940,17 @@ var WorkflowHeaders = class {
1853
1940
  return contentType;
1854
1941
  }
1855
1942
  prefixHeaders(contentType) {
1856
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
1943
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
1857
1944
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
1858
1945
  return {
1859
1946
  headers: {
1860
1947
  ...rawHeaders,
1861
1948
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
1862
1949
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
1863
- ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
1950
+ ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
1951
+ // last, so they override the run's settings above. Never
1952
+ // prefixed: they configure this message, not its callback.
1953
+ ...stepSettingsHeaders
1864
1954
  },
1865
1955
  contentType
1866
1956
  };
@@ -1911,7 +2001,7 @@ var submitParallelSteps = async ({
1911
2001
  info: `Submitting ${planSteps.length} parallel steps.`
1912
2002
  });
1913
2003
  const result = await context.qstashClient.batch(
1914
- planSteps.map((planStep) => {
2004
+ planSteps.map((planStep, index) => {
1915
2005
  const { headers } = getHeaders({
1916
2006
  initHeaderValue: "false",
1917
2007
  workflowConfig: {
@@ -1919,7 +2009,8 @@ var submitParallelSteps = async ({
1919
2009
  workflowUrl: context.url,
1920
2010
  telemetry
1921
2011
  },
1922
- invokeCount
2012
+ invokeCount,
2013
+ stepSettings: steps[index].stepSettings
1923
2014
  });
1924
2015
  return {
1925
2016
  headers,
@@ -1938,31 +2029,38 @@ var submitParallelSteps = async ({
1938
2029
  }
1939
2030
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
1940
2031
  };
1941
- var submitSingleStep = async ({
1942
- context,
2032
+ var executeStep = async ({
1943
2033
  lazyStep,
1944
2034
  stepId,
1945
- invokeCount,
1946
2035
  concurrency,
1947
- telemetry,
1948
- dispatchDebug,
1949
2036
  dispatchLifecycle
1950
2037
  }) => {
1951
2038
  await dispatchLifecycle("beforeExecution", {
1952
2039
  stepName: lazyStep.stepName
1953
2040
  });
1954
- let resultStep;
1955
2041
  try {
1956
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2042
+ return await lazyStep.getResultStep(concurrency, stepId);
1957
2043
  } catch (error) {
1958
2044
  attachStepNameToError(error, lazyStep.stepName);
1959
2045
  throw error;
1960
2046
  }
2047
+ };
2048
+ var submitStepResult = async ({
2049
+ context,
2050
+ lazyStep,
2051
+ resultStep,
2052
+ invokeCount,
2053
+ concurrency,
2054
+ telemetry,
2055
+ dispatchDebug,
2056
+ nextStepSettings
2057
+ }) => {
1961
2058
  const { headers } = lazyStep.getHeaders({
1962
2059
  context,
1963
2060
  step: resultStep,
1964
2061
  invokeCount,
1965
- telemetry
2062
+ telemetry,
2063
+ stepSettings: nextStepSettings
1966
2064
  });
1967
2065
  const body = lazyStep.getBody({
1968
2066
  context,
@@ -1987,6 +2085,138 @@ var submitSingleStep = async ({
1987
2085
  }
1988
2086
  return resultStep;
1989
2087
  };
2088
+ var publishStepConfigRequest = async ({
2089
+ context,
2090
+ lazyStep,
2091
+ targetStep,
2092
+ invokeCount,
2093
+ telemetry,
2094
+ dispatchDebug
2095
+ }) => {
2096
+ const { headers } = getHeaders({
2097
+ initHeaderValue: "false",
2098
+ workflowConfig: {
2099
+ workflowRunId: context.workflowRunId,
2100
+ workflowUrl: context.url,
2101
+ telemetry
2102
+ },
2103
+ invokeCount,
2104
+ stepSettings: lazyStep.stepSettings
2105
+ });
2106
+ await dispatchDebug("onInfo", {
2107
+ info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
2108
+ });
2109
+ const result = await context.qstashClient.publishJSON({
2110
+ headers: {
2111
+ ...headers,
2112
+ "Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
2113
+ },
2114
+ method: "POST",
2115
+ body: { targetStep, invokeCount },
2116
+ url: context.url,
2117
+ contentBasedDeduplication: true
2118
+ });
2119
+ if (result?.deduplicated) {
2120
+ await dispatchDebug("onWarning", {
2121
+ 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.`
2122
+ });
2123
+ } else if (result?.messageId) {
2124
+ await dispatchDebug("onInfo", {
2125
+ info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
2126
+ });
2127
+ }
2128
+ };
2129
+
2130
+ // src/qstash/step-config.ts
2131
+ var DEFAULT_PARALLELISM = 0;
2132
+ var DEFAULT_RATE = 0;
2133
+ var DEFAULT_PERIOD_SECONDS = 1;
2134
+ var SECONDS_PER_UNIT = {
2135
+ s: 1,
2136
+ m: 60,
2137
+ h: 60 * 60,
2138
+ d: 24 * 60 * 60
2139
+ };
2140
+ var durationToSeconds = (duration) => {
2141
+ if (typeof duration === "number") {
2142
+ return Number.isFinite(duration) ? duration : void 0;
2143
+ }
2144
+ const match = /^(\d+)([smhd])$/.exec(duration.trim());
2145
+ if (match) {
2146
+ return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
2147
+ }
2148
+ const seconds = Number(duration.trim());
2149
+ return Number.isFinite(seconds) ? seconds : void 0;
2150
+ };
2151
+ var normalizeFlowControl = (flowControl) => {
2152
+ const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
2153
+ return {
2154
+ key: flowControl.key,
2155
+ parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
2156
+ rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
2157
+ period: period ?? DEFAULT_PERIOD_SECONDS
2158
+ };
2159
+ };
2160
+ var parseFlowControlHeaders = (key, value) => {
2161
+ if (!key) {
2162
+ return void 0;
2163
+ }
2164
+ const flowControl = {
2165
+ key,
2166
+ parallelism: DEFAULT_PARALLELISM,
2167
+ rate: DEFAULT_RATE,
2168
+ period: DEFAULT_PERIOD_SECONDS
2169
+ };
2170
+ for (const entry of (value ?? "").split(",")) {
2171
+ const [name, rawValue] = entry.split("=").map((part) => part.trim());
2172
+ if (!name || rawValue === void 0) {
2173
+ continue;
2174
+ }
2175
+ switch (name) {
2176
+ case "parallelism": {
2177
+ flowControl.parallelism = Number(rawValue);
2178
+ break;
2179
+ }
2180
+ case "rate": {
2181
+ flowControl.rate = Number(rawValue);
2182
+ break;
2183
+ }
2184
+ case "period": {
2185
+ flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
2186
+ break;
2187
+ }
2188
+ }
2189
+ }
2190
+ return flowControl;
2191
+ };
2192
+ var getEffectiveConfig = (headers) => {
2193
+ const retriesHeader = headers.get(RETRIES_HEADER);
2194
+ return {
2195
+ flowControl: parseFlowControlHeaders(
2196
+ headers.get(FLOW_CONTROL_KEY_HEADER),
2197
+ headers.get(FLOW_CONTROL_VALUE_HEADER)
2198
+ ),
2199
+ retries: retriesHeader === null ? void 0 : Number(retriesHeader),
2200
+ retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
2201
+ hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
2202
+ };
2203
+ };
2204
+ var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
2205
+ if (stepSettings.flowControl) {
2206
+ const wanted = normalizeFlowControl(stepSettings.flowControl);
2207
+ const applied = effectiveConfig.flowControl;
2208
+ if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
2209
+ return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
2210
+ }
2211
+ }
2212
+ if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
2213
+ return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
2214
+ }
2215
+ if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
2216
+ return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
2217
+ }
2218
+ return void 0;
2219
+ };
1990
2220
 
1991
2221
  // src/context/auto-executor.ts
1992
2222
  var AutoExecutor = class _AutoExecutor {
@@ -2003,6 +2233,25 @@ var AutoExecutor = class _AutoExecutor {
2003
2233
  stepCount = 0;
2004
2234
  planStepCount = 0;
2005
2235
  executingStep = false;
2236
+ /**
2237
+ * an executed step whose result hasn't been submitted to QStash yet.
2238
+ *
2239
+ * Set when a step which can produce its result in-process executes. The
2240
+ * submission waits so that the route function can continue and reveal
2241
+ * what comes next: if that is a single step with step-level settings,
2242
+ * the settings ride on this submission and no step config request is
2243
+ * needed, since the delivery of the submission is what executes it.
2244
+ *
2245
+ * Flushed when the next step is reached (see `addStep`) or when the
2246
+ * route function ends (see `flushPendingStep` in `serve`).
2247
+ */
2248
+ pendingStep;
2249
+ /**
2250
+ * configuration QStash applied to the delivery being handled. A step
2251
+ * with step-level settings is settled by comparing its settings against
2252
+ * this.
2253
+ */
2254
+ effectiveConfig;
2006
2255
  /**
2007
2256
  * @param context workflow context
2008
2257
  * @param steps list of steps
@@ -2010,14 +2259,17 @@ var AutoExecutor = class _AutoExecutor {
2010
2259
  * @param dispatchLifecycle lifecycle event dispatcher
2011
2260
  * @param telemetry optional telemetry information
2012
2261
  * @param invokeCount optional invoke count
2262
+ * @param effectiveConfig configuration QStash applied to the delivery
2263
+ * being handled
2013
2264
  */
2014
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
2265
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount, effectiveConfig) {
2015
2266
  this.context = context;
2016
2267
  this.steps = steps;
2017
2268
  this.dispatchDebug = dispatchDebug;
2018
2269
  this.dispatchLifecycle = dispatchLifecycle;
2019
2270
  this.telemetry = telemetry;
2020
2271
  this.invokeCount = invokeCount ?? 0;
2272
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2021
2273
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2022
2274
  }
2023
2275
  /**
@@ -2052,6 +2304,15 @@ var AutoExecutor = class _AutoExecutor {
2052
2304
  lazyStepList.push(stepInfo);
2053
2305
  const index = this.indexInCurrentList++;
2054
2306
  const requestComplete = this.deferExecution().then(async () => {
2307
+ const submitted = await this.submitPendingStep(
2308
+ lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
2309
+ );
2310
+ if (submitted.isErr()) {
2311
+ throw submitted.error;
2312
+ }
2313
+ if (submitted.value.result === "submitted-step") {
2314
+ throw submitted.value.abort;
2315
+ }
2055
2316
  if (!this.promises.has(lazyStepList)) {
2056
2317
  const promise2 = this.getExecutionPromise(lazyStepList);
2057
2318
  this.promises.set(lazyStepList, promise2);
@@ -2105,18 +2366,100 @@ var AutoExecutor = class _AutoExecutor {
2105
2366
  }
2106
2367
  return parsedOut;
2107
2368
  }
2108
- const resultStep = await submitSingleStep({
2109
- context: this.context,
2369
+ const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
2370
+ if (mismatch) {
2371
+ if (!this.effectiveConfig.hasStepConfig) {
2372
+ await publishStepConfigRequest({
2373
+ context: this.context,
2374
+ lazyStep,
2375
+ targetStep: this.stepCount,
2376
+ invokeCount: this.invokeCount,
2377
+ telemetry: this.telemetry,
2378
+ dispatchDebug: this.dispatchDebug
2379
+ });
2380
+ throw new WorkflowAbort(lazyStep.stepName);
2381
+ } else {
2382
+ await this.dispatchDebug("onWarning", {
2383
+ 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.`
2384
+ });
2385
+ }
2386
+ }
2387
+ const resultStep = await executeStep({
2110
2388
  lazyStep,
2111
2389
  stepId: this.stepCount,
2390
+ concurrency: NO_CONCURRENCY,
2391
+ dispatchLifecycle: this.dispatchLifecycle
2392
+ });
2393
+ if (lazyStep.supportsDeferredSubmission) {
2394
+ this.pendingStep = { status: "held", lazyStep, resultStep };
2395
+ return lazyStep.parseOut({
2396
+ ...resultStep,
2397
+ out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
2398
+ });
2399
+ }
2400
+ await submitStepResult({
2401
+ context: this.context,
2402
+ lazyStep,
2403
+ resultStep,
2112
2404
  invokeCount: this.invokeCount,
2113
- concurrency: 1,
2405
+ concurrency: NO_CONCURRENCY,
2114
2406
  telemetry: this.telemetry,
2115
- dispatchDebug: this.dispatchDebug,
2116
- dispatchLifecycle: this.dispatchLifecycle
2407
+ dispatchDebug: this.dispatchDebug
2117
2408
  });
2118
2409
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2119
2410
  }
2411
+ /**
2412
+ * Submits the result of the step being held, if there is one, and
2413
+ * returns the abort the caller has to throw to end the invocation.
2414
+ *
2415
+ * Returns undefined when no step is being held, which is the caller's
2416
+ * signal that there is nothing to end. Throws instead of returning
2417
+ * when the submission itself fails, so that error reaches the caller
2418
+ * rather than an abort claiming the step was submitted.
2419
+ *
2420
+ * Callers which arrive while a submission is already under way (the
2421
+ * steps of a parallel group, added together) wait for that one and get
2422
+ * the same abort, rather than submitting a second time.
2423
+ *
2424
+ * The next step's settings are attached whenever it has any, without
2425
+ * checking them against the current delivery: the executor only knows
2426
+ * the configuration of the delivery in hand, which inside a
2427
+ * delivery is the *previous* step's, so such a check would be wrong
2428
+ * exactly when two steps with settings follow each other.
2429
+ *
2430
+ * @param nextStepSettings step-level settings of the next step
2431
+ * @returns `submitted-step` with the abort which ends this invocation,
2432
+ * or `no-pending-step` when nothing was held and there is nothing to
2433
+ * end
2434
+ */
2435
+ async submitPendingStep(nextStepSettings) {
2436
+ if (!this.pendingStep) {
2437
+ return ok({ result: "no-pending-step" });
2438
+ }
2439
+ try {
2440
+ if (this.pendingStep.status === "submitting") {
2441
+ return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
2442
+ }
2443
+ const { lazyStep, resultStep } = this.pendingStep;
2444
+ const submitted = (async () => {
2445
+ await submitStepResult({
2446
+ context: this.context,
2447
+ lazyStep,
2448
+ resultStep,
2449
+ invokeCount: this.invokeCount,
2450
+ concurrency: NO_CONCURRENCY,
2451
+ telemetry: this.telemetry,
2452
+ dispatchDebug: this.dispatchDebug,
2453
+ nextStepSettings
2454
+ });
2455
+ return new WorkflowAbort(lazyStep.stepName, resultStep);
2456
+ })();
2457
+ this.pendingStep = { status: "submitting", submitted };
2458
+ return ok({ result: "submitted-step", abort: await submitted });
2459
+ } catch (error) {
2460
+ return err(error);
2461
+ }
2462
+ }
2120
2463
  /**
2121
2464
  * Runs steps in parallel.
2122
2465
  *
@@ -2165,15 +2508,20 @@ var AutoExecutor = class _AutoExecutor {
2165
2508
  validateStep(parallelSteps[stepIndex], planStep);
2166
2509
  try {
2167
2510
  const parallelStep = parallelSteps[stepIndex];
2168
- const resultStep = await submitSingleStep({
2169
- context: this.context,
2511
+ const resultStep = await executeStep({
2170
2512
  lazyStep: parallelStep,
2171
2513
  stepId: planStep.targetStep,
2514
+ concurrency: parallelSteps.length,
2515
+ dispatchLifecycle: this.dispatchLifecycle
2516
+ });
2517
+ await submitStepResult({
2518
+ context: this.context,
2519
+ lazyStep: parallelStep,
2520
+ resultStep,
2172
2521
  invokeCount: this.invokeCount,
2173
2522
  concurrency: parallelSteps.length,
2174
2523
  telemetry: this.telemetry,
2175
- dispatchDebug: this.dispatchDebug,
2176
- dispatchLifecycle: this.dispatchLifecycle
2524
+ dispatchDebug: this.dispatchDebug
2177
2525
  });
2178
2526
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2179
2527
  } catch (error) {
@@ -2789,6 +3137,46 @@ var WorkflowContext = class {
2789
3137
  * `1` on the first retry, `2` on the second, and so on.
2790
3138
  */
2791
3139
  retried;
3140
+ /**
3141
+ * Configuration QStash applied to the request being handled.
3142
+ *
3143
+ * This is the configuration of *this delivery*, not of the run: it is
3144
+ * the configuration the run was triggered with, except inside the
3145
+ * delivery which executes a step that has step-level settings, where
3146
+ * it is that step's settings. The fields below expose it; the executor
3147
+ * gets it directly rather than reaching through the context.
3148
+ */
3149
+ effectiveConfig;
3150
+ /**
3151
+ * Flow control QStash applied to the request being handled, if any.
3152
+ *
3153
+ * This is the flow control of the request in hand, which is the one the
3154
+ * run was triggered with except inside a step carrying its own.
3155
+ */
3156
+ get flowControl() {
3157
+ return this.effectiveConfig.flowControl;
3158
+ }
3159
+ /**
3160
+ * Retry limit QStash applied to the request being handled, if it
3161
+ * reported one.
3162
+ *
3163
+ * Not to be confused with {@link retried}, which is how many retries
3164
+ * have already happened.
3165
+ *
3166
+ * @see {@link flowControl} for what "the request being handled" means
3167
+ */
3168
+ get retries() {
3169
+ return this.effectiveConfig.retries;
3170
+ }
3171
+ /**
3172
+ * Retry delay expression QStash applied to the request being handled,
3173
+ * if any.
3174
+ *
3175
+ * @see {@link flowControl} for what "the request being handled" means
3176
+ */
3177
+ get retryDelay() {
3178
+ return this.effectiveConfig.retryDelay;
3179
+ }
2792
3180
  constructor({
2793
3181
  qstashClient,
2794
3182
  workflowRunId,
@@ -2802,7 +3190,8 @@ var WorkflowContext = class {
2802
3190
  invokeCount,
2803
3191
  label,
2804
3192
  retried,
2805
- middlewareManager
3193
+ middlewareManager,
3194
+ effectiveConfig
2806
3195
  }) {
2807
3196
  this.qstashClient = qstashClient;
2808
3197
  this.workflowRunId = workflowRunId;
@@ -2814,6 +3203,7 @@ var WorkflowContext = class {
2814
3203
  this.env = env ?? {};
2815
3204
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2816
3205
  this.retried = retried ?? 0;
3206
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2817
3207
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2818
3208
  middlewareManagerInstance.assignContext(this);
2819
3209
  this.executor = new AutoExecutor(
@@ -2822,7 +3212,8 @@ var WorkflowContext = class {
2822
3212
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2823
3213
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2824
3214
  telemetry,
2825
- invokeCount
3215
+ invokeCount,
3216
+ this.effectiveConfig
2826
3217
  );
2827
3218
  }
2828
3219
  /**
@@ -2848,13 +3239,35 @@ var WorkflowContext = class {
2848
3239
  * ])
2849
3240
  * ```
2850
3241
  *
3242
+ * Step-level settings can be passed as a third argument, overriding
3243
+ * the settings the workflow run was triggered with for this step only:
3244
+ *
3245
+ * ```typescript
3246
+ * const result = await context.run(
3247
+ * "step 1",
3248
+ * () => {
3249
+ * return "result"
3250
+ * },
3251
+ * {
3252
+ * flowControl: { key: "custom-key", parallelism: 3 },
3253
+ * retries: 5,
3254
+ * }
3255
+ * )
3256
+ * ```
3257
+ *
2851
3258
  * @param stepName name of the step
2852
3259
  * @param stepFunction step function to be executed
3260
+ * @param stepSettings step-level settings for this step
2853
3261
  * @returns result of the step function
2854
3262
  */
2855
- async run(stepName, stepFunction) {
3263
+ async run(stepName, stepFunction, stepSettings) {
3264
+ if (stepSettings) {
3265
+ validateFlowControl(stepSettings.flowControl);
3266
+ }
2856
3267
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2857
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3268
+ return await this.addStep(
3269
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3270
+ );
2858
3271
  }
2859
3272
  /**
2860
3273
  * Stops the execution for the duration provided.
@@ -3077,7 +3490,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3077
3490
  *
3078
3491
  * @param routeFunction
3079
3492
  */
3080
- static async tryAuthentication(routeFunction, context) {
3493
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3081
3494
  const disabledContext = new _DisabledWorkflowContext({
3082
3495
  qstashClient: new import_qstash10.Client({
3083
3496
  baseUrl: "disabled-client",
@@ -3092,7 +3505,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3092
3505
  initialPayload: context.requestPayload,
3093
3506
  env: context.env,
3094
3507
  label: context.labels,
3095
- retried: context.retried
3508
+ retried: context.retried,
3509
+ // the route function runs for real here until it reaches a step, so
3510
+ // it observes the same configuration as it will on the real context
3511
+ effectiveConfig
3096
3512
  });
3097
3513
  try {
3098
3514
  await routeFunction(disabledContext);
@@ -3309,6 +3725,7 @@ var handleFailure = async ({
3309
3725
  }
3310
3726
  const userHeaders = recreateUserHeaders(request.headers);
3311
3727
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3728
+ const effectiveConfig = getEffectiveConfig(request.headers);
3312
3729
  const workflowContext = new WorkflowContext({
3313
3730
  qstashClient,
3314
3731
  workflowRunId,
@@ -3322,11 +3739,13 @@ var handleFailure = async ({
3322
3739
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3323
3740
  retried,
3324
3741
  workflowRunCreatedAt: workflowCreatedAt,
3325
- middlewareManager: void 0
3742
+ middlewareManager: void 0,
3743
+ effectiveConfig
3326
3744
  });
3327
3745
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3328
3746
  routeFunction,
3329
- workflowContext
3747
+ workflowContext,
3748
+ effectiveConfig
3330
3749
  );
3331
3750
  if (authCheck.isErr()) {
3332
3751
  await dispatchDebug?.("onError", {
@@ -3418,7 +3837,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3418
3837
  const client = new import_qstash11.Client({
3419
3838
  ...clientOptions,
3420
3839
  baseUrl: clientEnv.QSTASH_URL,
3421
- token: clientEnv.QSTASH_TOKEN
3840
+ token: clientEnv.QSTASH_TOKEN,
3841
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3842
+ // the client at the local dev server too. Without this the QStash Client
3843
+ // re-derives dev mode from its own process.env, which is blind to the
3844
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3845
+ // An explicit devMode in the user's client config still wins.
3846
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3422
3847
  });
3423
3848
  const receiver = getReceiver(environment, receiverConfig, region);
3424
3849
  return { client, receiver };
@@ -3468,7 +3893,9 @@ var getQStashHandlers = ({
3468
3893
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3469
3894
  ...qstashClientOption,
3470
3895
  baseUrl: environment.QSTASH_URL,
3471
- token: environment.QSTASH_TOKEN
3896
+ token: environment.QSTASH_TOKEN,
3897
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
3898
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3472
3899
  }),
3473
3900
  receiver: getReceiver(environment, receiverConfig)
3474
3901
  }
@@ -3808,12 +4235,14 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3808
4235
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3809
4236
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3810
4237
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4238
+ const effectiveConfig = getEffectiveConfig(request.headers);
3811
4239
  const workflowContext = new WorkflowContext({
3812
4240
  qstashClient: regionalClient,
3813
4241
  workflowRunId,
3814
4242
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
3815
4243
  headers: recreateUserHeaders(request.headers),
3816
4244
  steps,
4245
+ effectiveConfig,
3817
4246
  url: workflowUrl,
3818
4247
  env,
3819
4248
  telemetry,
@@ -3825,7 +4254,8 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3825
4254
  });
3826
4255
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3827
4256
  routeFunction,
3828
- workflowContext
4257
+ workflowContext,
4258
+ effectiveConfig
3829
4259
  );
3830
4260
  if (authCheck.isErr()) {
3831
4261
  throw authCheck.error;
@@ -3862,7 +4292,23 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3862
4292
  if (steps.length === 1) {
3863
4293
  await middlewareManager.dispatchLifecycle("runStarted", {});
3864
4294
  }
3865
- return await routeFunction(workflowContext);
4295
+ let outcome;
4296
+ try {
4297
+ outcome = { ran: true, result: await routeFunction(workflowContext) };
4298
+ } catch (error) {
4299
+ outcome = { ran: false, error };
4300
+ }
4301
+ const submitted = await flushPendingStep(workflowContext);
4302
+ if (submitted.isErr()) {
4303
+ throw submitted.error;
4304
+ }
4305
+ if (submitted.value.result === "submitted-step") {
4306
+ throw submitted.value.abort;
4307
+ }
4308
+ if (!outcome.ran) {
4309
+ throw outcome.error;
4310
+ }
4311
+ return outcome.result;
3866
4312
  },
3867
4313
  onCleanup: async (result2) => {
3868
4314
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -3943,6 +4389,7 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3943
4389
  };
3944
4390
 
3945
4391
  // platforms/nextjs.ts
4392
+ var import_qstash13 = require("@upstash/qstash");
3946
4393
  var appTelemetry = {
3947
4394
  sdk: SDK_TELEMETRY,
3948
4395
  framework: "nextjs",
@@ -3954,6 +4401,7 @@ var pagesTelemetry = {
3954
4401
  runtime: process.versions.bun ? `bun@${process.versions.bun}/node@${process.version}` : `node@${process.version}`
3955
4402
  };
3956
4403
  var serve = (routeFunction, options) => {
4404
+ void (0, import_qstash13.startDevServer)();
3957
4405
  const { handler: serveHandler } = serveBase(
3958
4406
  routeFunction,
3959
4407
  appTelemetry,
@@ -3986,6 +4434,7 @@ var serveMany = (workflows, options) => {
3986
4434
  };
3987
4435
  };
3988
4436
  var servePagesRouter = (routeFunction, options) => {
4437
+ void (0, import_qstash13.startDevServer)();
3989
4438
  const { handler: serveHandler } = serveBase(routeFunction, pagesTelemetry, options, void 0);
3990
4439
  const handler = async (request_, res) => {
3991
4440
  if (request_.method?.toUpperCase() !== "POST") {