@upstash/workflow 1.3.2 → 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 +595 -140
  4. package/astro.mjs +3 -1
  5. package/{chunk-ICDNN4UW.mjs → chunk-UH6O5IJY.mjs} +751 -281
  6. package/cloudflare.d.mts +3 -2
  7. package/cloudflare.d.ts +3 -2
  8. package/cloudflare.js +595 -140
  9. package/cloudflare.mjs +3 -1
  10. package/express.d.mts +3 -2
  11. package/express.d.ts +3 -2
  12. package/express.js +596 -140
  13. package/express.mjs +4 -1
  14. package/h3.d.mts +3 -2
  15. package/h3.d.ts +3 -2
  16. package/h3.js +595 -140
  17. package/h3.mjs +3 -1
  18. package/hono.d.mts +3 -2
  19. package/hono.d.ts +3 -2
  20. package/hono.js +595 -140
  21. package/hono.mjs +3 -1
  22. package/index.d.mts +64 -17
  23. package/index.d.ts +64 -17
  24. package/index.js +652 -150
  25. package/index.mjs +49 -11
  26. package/nextjs.d.mts +3 -2
  27. package/nextjs.d.ts +3 -2
  28. package/nextjs.js +596 -140
  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 +595 -140
  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 +595 -140
  40. package/solidjs.mjs +3 -1
  41. package/svelte.d.mts +3 -2
  42. package/svelte.d.ts +3 -2
  43. package/svelte.js +595 -140
  44. package/svelte.mjs +3 -1
  45. package/tanstack.d.mts +3 -2
  46. package/tanstack.d.ts +3 -2
  47. package/tanstack.js +595 -140
  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
 
@@ -145,8 +148,15 @@ function isInstanceOf(v, ctor) {
145
148
  }
146
149
 
147
150
  // src/client/utils.ts
151
+ var assertNonEmptyId = (id, label = "id") => {
152
+ if (!id) {
153
+ throw new import_qstash2.QstashError(`${label} cannot be empty`);
154
+ }
155
+ };
148
156
  var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
149
- const path = workflowRunId ? ["v2", "notify", workflowRunId, eventId] : ["v2", "notify", eventId];
157
+ assertNonEmptyId(eventId, "Event id");
158
+ if (workflowRunId !== void 0) assertNonEmptyId(workflowRunId, "Workflow run id");
159
+ const path = workflowRunId === void 0 ? ["v2", "notify", eventId] : ["v2", "notify", workflowRunId, eventId];
150
160
  const result = await requester.request({
151
161
  path,
152
162
  method: "POST",
@@ -210,6 +220,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
210
220
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
211
221
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
212
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";
213
231
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
214
232
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
215
233
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -227,102 +245,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
227
245
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
228
246
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
229
247
 
230
- // src/context/auto-executor.ts
231
- var import_qstash5 = require("@upstash/qstash");
232
-
233
- // src/qstash/headers.ts
234
- var import_qstash4 = require("@upstash/qstash");
235
-
236
- // src/utils.ts
237
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
238
- var NANOID_LENGTH = 21;
239
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
240
- function validateLabel(label) {
241
- if (label === void 0) return;
242
- const labels = Array.isArray(label) ? label : [label];
243
- if (labels.length === 0) {
244
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
245
- }
246
- for (const value of labels) {
247
- if (!RESOURCE_NAME_PATTERN.test(value)) {
248
- throw new WorkflowNonRetryableError(
249
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
250
- );
251
- }
252
- }
253
- }
254
- function serializeLabel(label) {
255
- return Array.isArray(label) ? label.join(",") : label;
256
- }
257
- function validateFlowControl(flowControl) {
258
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
259
- throw new WorkflowNonRetryableError(
260
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
261
- );
262
- }
263
- }
264
- function getRandomInt() {
265
- return Math.floor(Math.random() * NANOID_CHARS.length);
266
- }
267
- function nanoid(length = NANOID_LENGTH) {
268
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
269
- }
270
- function getWorkflowRunId(id) {
271
- return `wfr_${id ?? nanoid()}`;
272
- }
273
- function decodeBase64(base64) {
274
- const binString = atob(base64);
275
- try {
276
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
277
- return new TextDecoder().decode(intArray);
278
- } catch (error) {
279
- console.warn(
280
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
281
- );
282
- return binString;
283
- }
284
- }
285
- function getUserIdFromToken(qstashClient) {
286
- try {
287
- const token = qstashClient.token;
288
- const decodedToken = decodeBase64(token);
289
- const tokenPayload = JSON.parse(decodedToken);
290
- const userId = tokenPayload.UserID;
291
- if (!userId) {
292
- throw new WorkflowError("QStash token payload does not contain userId");
293
- }
294
- return userId;
295
- } catch (error) {
296
- throw new WorkflowError(
297
- `Failed to decode QStash token while running create webhook step: ${error.message}`
298
- );
299
- }
300
- }
301
- function getQStashUrl(qstashClient) {
302
- try {
303
- const requester = qstashClient.http;
304
- const baseUrl = requester.baseUrl;
305
- if (!baseUrl) {
306
- throw new WorkflowError("QStash client does not have a baseUrl");
307
- }
308
- return baseUrl;
309
- } catch (error) {
310
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
311
- }
312
- }
313
- function getEventId() {
314
- return `evt_${nanoid(15)}`;
315
- }
316
- function stringifyBody(body) {
317
- if (body === void 0) {
318
- return void 0;
319
- }
320
- if (typeof body === "string") {
321
- return body;
322
- }
323
- return JSON.stringify(body);
324
- }
325
-
326
248
  // node_modules/neverthrow/dist/index.es.js
327
249
  var defaultErrorConfig = {
328
250
  withStackTrace: false
@@ -738,6 +660,102 @@ var Err = class {
738
660
  };
739
661
  var fromThrowable = Result.fromThrowable;
740
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
+
741
759
  // src/types.ts
742
760
  var StepTypes = [
743
761
  "Initial",
@@ -843,6 +861,10 @@ var triggerFirstInvocation = async (params) => {
843
861
  return err(error_);
844
862
  }
845
863
  };
864
+ var flushPendingStep = async (workflowContext) => {
865
+ const { executor } = workflowContext;
866
+ return await executor.submitPendingStep();
867
+ };
846
868
  var triggerRouteFunction = async ({
847
869
  onCleanup,
848
870
  onStep,
@@ -1050,8 +1072,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1050
1072
  var BaseLazyStep = class _BaseLazyStep {
1051
1073
  stepName;
1052
1074
  context;
1053
- 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) {
1054
1096
  this.context = context;
1097
+ this.stepSettings = stepSettings;
1055
1098
  if (!stepName) {
1056
1099
  throw new WorkflowError(
1057
1100
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1115,7 +1158,13 @@ var BaseLazyStep = class _BaseLazyStep {
1115
1158
  step.out = JSON.stringify(step.out);
1116
1159
  return JSON.stringify(step);
1117
1160
  }
1118
- getHeaders({ context, telemetry, invokeCount, step }) {
1161
+ getHeaders({
1162
+ context,
1163
+ telemetry,
1164
+ invokeCount,
1165
+ step,
1166
+ stepSettings
1167
+ }) {
1119
1168
  return getHeaders({
1120
1169
  initHeaderValue: "false",
1121
1170
  workflowConfig: {
@@ -1128,7 +1177,8 @@ var BaseLazyStep = class _BaseLazyStep {
1128
1177
  stepInfo: {
1129
1178
  step,
1130
1179
  lazyStep: this
1131
- }
1180
+ },
1181
+ stepSettings
1132
1182
  });
1133
1183
  }
1134
1184
  async submitStep({ context, body, headers }) {
@@ -1146,8 +1196,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1146
1196
  stepFunction;
1147
1197
  stepType = "Run";
1148
1198
  allowUndefinedOut = true;
1149
- constructor(context, stepName, stepFunction) {
1150
- super(context, stepName);
1199
+ supportsDeferredSubmission = true;
1200
+ constructor(context, stepName, stepFunction, stepSettings) {
1201
+ super(context, stepName, stepSettings);
1151
1202
  this.stepFunction = stepFunction;
1152
1203
  }
1153
1204
  getPlanStep(concurrent, targetStep) {
@@ -1177,6 +1228,7 @@ var LazySleepStep = class extends BaseLazyStep {
1177
1228
  sleep;
1178
1229
  stepType = "SleepFor";
1179
1230
  allowUndefinedOut = true;
1231
+ supportsDeferredSubmission = true;
1180
1232
  constructor(context, stepName, sleep) {
1181
1233
  super(context, stepName);
1182
1234
  this.sleep = sleep;
@@ -1216,6 +1268,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1216
1268
  sleepUntil;
1217
1269
  stepType = "SleepUntil";
1218
1270
  allowUndefinedOut = true;
1271
+ supportsDeferredSubmission = true;
1219
1272
  constructor(context, stepName, sleepUntil) {
1220
1273
  super(context, stepName);
1221
1274
  this.sleepUntil = sleepUntil;
@@ -1708,6 +1761,7 @@ var WorkflowHeaders = class {
1708
1761
  invokeCount;
1709
1762
  initHeaderValue;
1710
1763
  stepInfo;
1764
+ stepSettings;
1711
1765
  headers;
1712
1766
  /**
1713
1767
  * @param params workflow header parameters
@@ -1717,17 +1771,20 @@ var WorkflowHeaders = class {
1717
1771
  workflowConfig,
1718
1772
  invokeCount,
1719
1773
  initHeaderValue,
1720
- stepInfo
1774
+ stepInfo,
1775
+ stepSettings
1721
1776
  }) {
1722
1777
  this.userHeaders = userHeaders;
1723
1778
  this.workflowConfig = workflowConfig;
1724
1779
  this.invokeCount = invokeCount;
1725
1780
  this.initHeaderValue = initHeaderValue;
1726
1781
  this.stepInfo = stepInfo;
1782
+ this.stepSettings = stepSettings;
1727
1783
  this.headers = {
1728
1784
  rawHeaders: {},
1729
1785
  workflowHeaders: {},
1730
- failureHeaders: {}
1786
+ failureHeaders: {},
1787
+ stepSettingsHeaders: {}
1731
1788
  };
1732
1789
  }
1733
1790
  getHeaders() {
@@ -1738,6 +1795,7 @@ var WorkflowHeaders = class {
1738
1795
  this.addUserHeaders();
1739
1796
  this.addInvokeCount();
1740
1797
  this.addFailureUrl();
1798
+ this.addStepSettings();
1741
1799
  const contentType = this.addContentType();
1742
1800
  return this.prefixHeaders(contentType);
1743
1801
  }
@@ -1747,7 +1805,7 @@ var WorkflowHeaders = class {
1747
1805
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1748
1806
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1749
1807
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1750
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1808
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
1751
1809
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1752
1810
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1753
1811
  };
@@ -1825,7 +1883,7 @@ var WorkflowHeaders = class {
1825
1883
  this.headers.failureHeaders["Workflow-Init"] = "false";
1826
1884
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1827
1885
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1828
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1886
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
1829
1887
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1830
1888
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1831
1889
  }
@@ -1833,6 +1891,42 @@ var WorkflowHeaders = class {
1833
1891
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
1834
1892
  }
1835
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
+ }
1836
1930
  addContentType() {
1837
1931
  if (this.workflowConfig.useJSONContent) {
1838
1932
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -1846,14 +1940,17 @@ var WorkflowHeaders = class {
1846
1940
  return contentType;
1847
1941
  }
1848
1942
  prefixHeaders(contentType) {
1849
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
1943
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
1850
1944
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
1851
1945
  return {
1852
1946
  headers: {
1853
1947
  ...rawHeaders,
1854
1948
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
1855
1949
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
1856
- ...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
1857
1954
  },
1858
1955
  contentType
1859
1956
  };
@@ -1904,7 +2001,7 @@ var submitParallelSteps = async ({
1904
2001
  info: `Submitting ${planSteps.length} parallel steps.`
1905
2002
  });
1906
2003
  const result = await context.qstashClient.batch(
1907
- planSteps.map((planStep) => {
2004
+ planSteps.map((planStep, index) => {
1908
2005
  const { headers } = getHeaders({
1909
2006
  initHeaderValue: "false",
1910
2007
  workflowConfig: {
@@ -1912,7 +2009,8 @@ var submitParallelSteps = async ({
1912
2009
  workflowUrl: context.url,
1913
2010
  telemetry
1914
2011
  },
1915
- invokeCount
2012
+ invokeCount,
2013
+ stepSettings: steps[index].stepSettings
1916
2014
  });
1917
2015
  return {
1918
2016
  headers,
@@ -1931,31 +2029,38 @@ var submitParallelSteps = async ({
1931
2029
  }
1932
2030
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
1933
2031
  };
1934
- var submitSingleStep = async ({
1935
- context,
2032
+ var executeStep = async ({
1936
2033
  lazyStep,
1937
2034
  stepId,
1938
- invokeCount,
1939
2035
  concurrency,
1940
- telemetry,
1941
- dispatchDebug,
1942
2036
  dispatchLifecycle
1943
2037
  }) => {
1944
2038
  await dispatchLifecycle("beforeExecution", {
1945
2039
  stepName: lazyStep.stepName
1946
2040
  });
1947
- let resultStep;
1948
2041
  try {
1949
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2042
+ return await lazyStep.getResultStep(concurrency, stepId);
1950
2043
  } catch (error) {
1951
2044
  attachStepNameToError(error, lazyStep.stepName);
1952
2045
  throw error;
1953
2046
  }
2047
+ };
2048
+ var submitStepResult = async ({
2049
+ context,
2050
+ lazyStep,
2051
+ resultStep,
2052
+ invokeCount,
2053
+ concurrency,
2054
+ telemetry,
2055
+ dispatchDebug,
2056
+ nextStepSettings
2057
+ }) => {
1954
2058
  const { headers } = lazyStep.getHeaders({
1955
2059
  context,
1956
2060
  step: resultStep,
1957
2061
  invokeCount,
1958
- telemetry
2062
+ telemetry,
2063
+ stepSettings: nextStepSettings
1959
2064
  });
1960
2065
  const body = lazyStep.getBody({
1961
2066
  context,
@@ -1980,6 +2085,138 @@ var submitSingleStep = async ({
1980
2085
  }
1981
2086
  return resultStep;
1982
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
+ };
1983
2220
 
1984
2221
  // src/context/auto-executor.ts
1985
2222
  var AutoExecutor = class _AutoExecutor {
@@ -1996,6 +2233,25 @@ var AutoExecutor = class _AutoExecutor {
1996
2233
  stepCount = 0;
1997
2234
  planStepCount = 0;
1998
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;
1999
2255
  /**
2000
2256
  * @param context workflow context
2001
2257
  * @param steps list of steps
@@ -2003,14 +2259,17 @@ var AutoExecutor = class _AutoExecutor {
2003
2259
  * @param dispatchLifecycle lifecycle event dispatcher
2004
2260
  * @param telemetry optional telemetry information
2005
2261
  * @param invokeCount optional invoke count
2262
+ * @param effectiveConfig configuration QStash applied to the delivery
2263
+ * being handled
2006
2264
  */
2007
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
2265
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount, effectiveConfig) {
2008
2266
  this.context = context;
2009
2267
  this.steps = steps;
2010
2268
  this.dispatchDebug = dispatchDebug;
2011
2269
  this.dispatchLifecycle = dispatchLifecycle;
2012
2270
  this.telemetry = telemetry;
2013
2271
  this.invokeCount = invokeCount ?? 0;
2272
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2014
2273
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2015
2274
  }
2016
2275
  /**
@@ -2045,6 +2304,15 @@ var AutoExecutor = class _AutoExecutor {
2045
2304
  lazyStepList.push(stepInfo);
2046
2305
  const index = this.indexInCurrentList++;
2047
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
+ }
2048
2316
  if (!this.promises.has(lazyStepList)) {
2049
2317
  const promise2 = this.getExecutionPromise(lazyStepList);
2050
2318
  this.promises.set(lazyStepList, promise2);
@@ -2098,18 +2366,100 @@ var AutoExecutor = class _AutoExecutor {
2098
2366
  }
2099
2367
  return parsedOut;
2100
2368
  }
2101
- const resultStep = await submitSingleStep({
2102
- 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({
2103
2388
  lazyStep,
2104
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,
2105
2404
  invokeCount: this.invokeCount,
2106
- concurrency: 1,
2405
+ concurrency: NO_CONCURRENCY,
2107
2406
  telemetry: this.telemetry,
2108
- dispatchDebug: this.dispatchDebug,
2109
- dispatchLifecycle: this.dispatchLifecycle
2407
+ dispatchDebug: this.dispatchDebug
2110
2408
  });
2111
2409
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2112
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
+ }
2113
2463
  /**
2114
2464
  * Runs steps in parallel.
2115
2465
  *
@@ -2158,15 +2508,20 @@ var AutoExecutor = class _AutoExecutor {
2158
2508
  validateStep(parallelSteps[stepIndex], planStep);
2159
2509
  try {
2160
2510
  const parallelStep = parallelSteps[stepIndex];
2161
- const resultStep = await submitSingleStep({
2162
- context: this.context,
2511
+ const resultStep = await executeStep({
2163
2512
  lazyStep: parallelStep,
2164
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,
2165
2521
  invokeCount: this.invokeCount,
2166
2522
  concurrency: parallelSteps.length,
2167
2523
  telemetry: this.telemetry,
2168
- dispatchDebug: this.dispatchDebug,
2169
- dispatchLifecycle: this.dispatchLifecycle
2524
+ dispatchDebug: this.dispatchDebug
2170
2525
  });
2171
2526
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2172
2527
  } catch (error) {
@@ -2782,6 +3137,46 @@ var WorkflowContext = class {
2782
3137
  * `1` on the first retry, `2` on the second, and so on.
2783
3138
  */
2784
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
+ }
2785
3180
  constructor({
2786
3181
  qstashClient,
2787
3182
  workflowRunId,
@@ -2795,7 +3190,8 @@ var WorkflowContext = class {
2795
3190
  invokeCount,
2796
3191
  label,
2797
3192
  retried,
2798
- middlewareManager
3193
+ middlewareManager,
3194
+ effectiveConfig
2799
3195
  }) {
2800
3196
  this.qstashClient = qstashClient;
2801
3197
  this.workflowRunId = workflowRunId;
@@ -2807,6 +3203,7 @@ var WorkflowContext = class {
2807
3203
  this.env = env ?? {};
2808
3204
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2809
3205
  this.retried = retried ?? 0;
3206
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2810
3207
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2811
3208
  middlewareManagerInstance.assignContext(this);
2812
3209
  this.executor = new AutoExecutor(
@@ -2815,7 +3212,8 @@ var WorkflowContext = class {
2815
3212
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2816
3213
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2817
3214
  telemetry,
2818
- invokeCount
3215
+ invokeCount,
3216
+ this.effectiveConfig
2819
3217
  );
2820
3218
  }
2821
3219
  /**
@@ -2841,13 +3239,35 @@ var WorkflowContext = class {
2841
3239
  * ])
2842
3240
  * ```
2843
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
+ *
2844
3258
  * @param stepName name of the step
2845
3259
  * @param stepFunction step function to be executed
3260
+ * @param stepSettings step-level settings for this step
2846
3261
  * @returns result of the step function
2847
3262
  */
2848
- async run(stepName, stepFunction) {
3263
+ async run(stepName, stepFunction, stepSettings) {
3264
+ if (stepSettings) {
3265
+ validateFlowControl(stepSettings.flowControl);
3266
+ }
2849
3267
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2850
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3268
+ return await this.addStep(
3269
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3270
+ );
2851
3271
  }
2852
3272
  /**
2853
3273
  * Stops the execution for the duration provided.
@@ -3070,7 +3490,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3070
3490
  *
3071
3491
  * @param routeFunction
3072
3492
  */
3073
- static async tryAuthentication(routeFunction, context) {
3493
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3074
3494
  const disabledContext = new _DisabledWorkflowContext({
3075
3495
  qstashClient: new import_qstash10.Client({
3076
3496
  baseUrl: "disabled-client",
@@ -3085,7 +3505,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3085
3505
  initialPayload: context.requestPayload,
3086
3506
  env: context.env,
3087
3507
  label: context.labels,
3088
- 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
3089
3512
  });
3090
3513
  try {
3091
3514
  await routeFunction(disabledContext);
@@ -3302,6 +3725,7 @@ var handleFailure = async ({
3302
3725
  }
3303
3726
  const userHeaders = recreateUserHeaders(request.headers);
3304
3727
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3728
+ const effectiveConfig = getEffectiveConfig(request.headers);
3305
3729
  const workflowContext = new WorkflowContext({
3306
3730
  qstashClient,
3307
3731
  workflowRunId,
@@ -3315,11 +3739,13 @@ var handleFailure = async ({
3315
3739
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3316
3740
  retried,
3317
3741
  workflowRunCreatedAt: workflowCreatedAt,
3318
- middlewareManager: void 0
3742
+ middlewareManager: void 0,
3743
+ effectiveConfig
3319
3744
  });
3320
3745
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3321
3746
  routeFunction,
3322
- workflowContext
3747
+ workflowContext,
3748
+ effectiveConfig
3323
3749
  );
3324
3750
  if (authCheck.isErr()) {
3325
3751
  await dispatchDebug?.("onError", {
@@ -3411,7 +3837,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3411
3837
  const client = new import_qstash11.Client({
3412
3838
  ...clientOptions,
3413
3839
  baseUrl: clientEnv.QSTASH_URL,
3414
- 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)
3415
3847
  });
3416
3848
  const receiver = getReceiver(environment, receiverConfig, region);
3417
3849
  return { client, receiver };
@@ -3461,7 +3893,9 @@ var getQStashHandlers = ({
3461
3893
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3462
3894
  ...qstashClientOption,
3463
3895
  baseUrl: environment.QSTASH_URL,
3464
- 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)
3465
3899
  }),
3466
3900
  receiver: getReceiver(environment, receiverConfig)
3467
3901
  }
@@ -3801,12 +4235,14 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3801
4235
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3802
4236
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3803
4237
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4238
+ const effectiveConfig = getEffectiveConfig(request.headers);
3804
4239
  const workflowContext = new WorkflowContext({
3805
4240
  qstashClient: regionalClient,
3806
4241
  workflowRunId,
3807
4242
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
3808
4243
  headers: recreateUserHeaders(request.headers),
3809
4244
  steps,
4245
+ effectiveConfig,
3810
4246
  url: workflowUrl,
3811
4247
  env,
3812
4248
  telemetry,
@@ -3818,7 +4254,8 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3818
4254
  });
3819
4255
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3820
4256
  routeFunction,
3821
- workflowContext
4257
+ workflowContext,
4258
+ effectiveConfig
3822
4259
  );
3823
4260
  if (authCheck.isErr()) {
3824
4261
  throw authCheck.error;
@@ -3855,7 +4292,23 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3855
4292
  if (steps.length === 1) {
3856
4293
  await middlewareManager.dispatchLifecycle("runStarted", {});
3857
4294
  }
3858
- 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;
3859
4312
  },
3860
4313
  onCleanup: async (result2) => {
3861
4314
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -3936,6 +4389,7 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3936
4389
  };
3937
4390
 
3938
4391
  // platforms/nextjs.ts
4392
+ var import_qstash13 = require("@upstash/qstash");
3939
4393
  var appTelemetry = {
3940
4394
  sdk: SDK_TELEMETRY,
3941
4395
  framework: "nextjs",
@@ -3947,6 +4401,7 @@ var pagesTelemetry = {
3947
4401
  runtime: process.versions.bun ? `bun@${process.versions.bun}/node@${process.version}` : `node@${process.version}`
3948
4402
  };
3949
4403
  var serve = (routeFunction, options) => {
4404
+ void (0, import_qstash13.startDevServer)();
3950
4405
  const { handler: serveHandler } = serveBase(
3951
4406
  routeFunction,
3952
4407
  appTelemetry,
@@ -3979,6 +4434,7 @@ var serveMany = (workflows, options) => {
3979
4434
  };
3980
4435
  };
3981
4436
  var servePagesRouter = (routeFunction, options) => {
4437
+ void (0, import_qstash13.startDevServer)();
3982
4438
  const { handler: serveHandler } = serveBase(routeFunction, pagesTelemetry, options, void 0);
3983
4439
  const handler = async (request_, res) => {
3984
4440
  if (request_.method?.toUpperCase() !== "POST") {