@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/solidjs.js CHANGED
@@ -24,6 +24,9 @@ __export(solidjs_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(solidjs_exports);
26
26
 
27
+ // src/serve/index.ts
28
+ var import_qstash12 = require("@upstash/qstash");
29
+
27
30
  // src/client/utils.ts
28
31
  var import_qstash2 = require("@upstash/qstash");
29
32
 
@@ -212,6 +215,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
212
215
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
213
216
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
214
217
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
218
+ var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
219
+ var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
220
+ var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
221
+ var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
222
+ var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
223
+ var RETRIES_HEADER = "Upstash-Retries";
224
+ var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
225
+ var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
215
226
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
216
227
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
217
228
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -229,102 +240,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
229
240
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
230
241
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
231
242
 
232
- // src/context/auto-executor.ts
233
- var import_qstash5 = require("@upstash/qstash");
234
-
235
- // src/qstash/headers.ts
236
- var import_qstash4 = require("@upstash/qstash");
237
-
238
- // src/utils.ts
239
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
240
- var NANOID_LENGTH = 21;
241
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
242
- function validateLabel(label) {
243
- if (label === void 0) return;
244
- const labels = Array.isArray(label) ? label : [label];
245
- if (labels.length === 0) {
246
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
247
- }
248
- for (const value of labels) {
249
- if (!RESOURCE_NAME_PATTERN.test(value)) {
250
- throw new WorkflowNonRetryableError(
251
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
252
- );
253
- }
254
- }
255
- }
256
- function serializeLabel(label) {
257
- return Array.isArray(label) ? label.join(",") : label;
258
- }
259
- function validateFlowControl(flowControl) {
260
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
261
- throw new WorkflowNonRetryableError(
262
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
263
- );
264
- }
265
- }
266
- function getRandomInt() {
267
- return Math.floor(Math.random() * NANOID_CHARS.length);
268
- }
269
- function nanoid(length = NANOID_LENGTH) {
270
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
271
- }
272
- function getWorkflowRunId(id) {
273
- return `wfr_${id ?? nanoid()}`;
274
- }
275
- function decodeBase64(base64) {
276
- const binString = atob(base64);
277
- try {
278
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
279
- return new TextDecoder().decode(intArray);
280
- } catch (error) {
281
- console.warn(
282
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
283
- );
284
- return binString;
285
- }
286
- }
287
- function getUserIdFromToken(qstashClient) {
288
- try {
289
- const token = qstashClient.token;
290
- const decodedToken = decodeBase64(token);
291
- const tokenPayload = JSON.parse(decodedToken);
292
- const userId = tokenPayload.UserID;
293
- if (!userId) {
294
- throw new WorkflowError("QStash token payload does not contain userId");
295
- }
296
- return userId;
297
- } catch (error) {
298
- throw new WorkflowError(
299
- `Failed to decode QStash token while running create webhook step: ${error.message}`
300
- );
301
- }
302
- }
303
- function getQStashUrl(qstashClient) {
304
- try {
305
- const requester = qstashClient.http;
306
- const baseUrl = requester.baseUrl;
307
- if (!baseUrl) {
308
- throw new WorkflowError("QStash client does not have a baseUrl");
309
- }
310
- return baseUrl;
311
- } catch (error) {
312
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
313
- }
314
- }
315
- function getEventId() {
316
- return `evt_${nanoid(15)}`;
317
- }
318
- function stringifyBody(body) {
319
- if (body === void 0) {
320
- return void 0;
321
- }
322
- if (typeof body === "string") {
323
- return body;
324
- }
325
- return JSON.stringify(body);
326
- }
327
-
328
243
  // node_modules/neverthrow/dist/index.es.js
329
244
  var defaultErrorConfig = {
330
245
  withStackTrace: false
@@ -740,6 +655,102 @@ var Err = class {
740
655
  };
741
656
  var fromThrowable = Result.fromThrowable;
742
657
 
658
+ // src/context/auto-executor.ts
659
+ var import_qstash5 = require("@upstash/qstash");
660
+
661
+ // src/qstash/headers.ts
662
+ var import_qstash4 = require("@upstash/qstash");
663
+
664
+ // src/utils.ts
665
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
666
+ var NANOID_LENGTH = 21;
667
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
668
+ function validateLabel(label) {
669
+ if (label === void 0) return;
670
+ const labels = Array.isArray(label) ? label : [label];
671
+ if (labels.length === 0) {
672
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
673
+ }
674
+ for (const value of labels) {
675
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
676
+ throw new WorkflowNonRetryableError(
677
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
678
+ );
679
+ }
680
+ }
681
+ }
682
+ function serializeLabel(label) {
683
+ return Array.isArray(label) ? label.join(",") : label;
684
+ }
685
+ function validateFlowControl(flowControl) {
686
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
687
+ throw new WorkflowNonRetryableError(
688
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
689
+ );
690
+ }
691
+ }
692
+ function getRandomInt() {
693
+ return Math.floor(Math.random() * NANOID_CHARS.length);
694
+ }
695
+ function nanoid(length = NANOID_LENGTH) {
696
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
697
+ }
698
+ function getWorkflowRunId(id) {
699
+ return `wfr_${id ?? nanoid()}`;
700
+ }
701
+ function decodeBase64(base64) {
702
+ const binString = atob(base64);
703
+ try {
704
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
705
+ return new TextDecoder().decode(intArray);
706
+ } catch (error) {
707
+ console.warn(
708
+ `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
709
+ );
710
+ return binString;
711
+ }
712
+ }
713
+ function getUserIdFromToken(qstashClient) {
714
+ try {
715
+ const token = qstashClient.token;
716
+ const decodedToken = decodeBase64(token);
717
+ const tokenPayload = JSON.parse(decodedToken);
718
+ const userId = tokenPayload.UserID;
719
+ if (!userId) {
720
+ throw new WorkflowError("QStash token payload does not contain userId");
721
+ }
722
+ return userId;
723
+ } catch (error) {
724
+ throw new WorkflowError(
725
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
726
+ );
727
+ }
728
+ }
729
+ function getQStashUrl(qstashClient) {
730
+ try {
731
+ const requester = qstashClient.http;
732
+ const baseUrl = requester.baseUrl;
733
+ if (!baseUrl) {
734
+ throw new WorkflowError("QStash client does not have a baseUrl");
735
+ }
736
+ return baseUrl;
737
+ } catch (error) {
738
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
739
+ }
740
+ }
741
+ function getEventId() {
742
+ return `evt_${nanoid(15)}`;
743
+ }
744
+ function stringifyBody(body) {
745
+ if (body === void 0) {
746
+ return void 0;
747
+ }
748
+ if (typeof body === "string") {
749
+ return body;
750
+ }
751
+ return JSON.stringify(body);
752
+ }
753
+
743
754
  // src/types.ts
744
755
  var StepTypes = [
745
756
  "Initial",
@@ -845,6 +856,10 @@ var triggerFirstInvocation = async (params) => {
845
856
  return err(error_);
846
857
  }
847
858
  };
859
+ var flushPendingStep = async (workflowContext) => {
860
+ const { executor } = workflowContext;
861
+ return await executor.submitPendingStep();
862
+ };
848
863
  var triggerRouteFunction = async ({
849
864
  onCleanup,
850
865
  onStep,
@@ -1052,8 +1067,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1052
1067
  var BaseLazyStep = class _BaseLazyStep {
1053
1068
  stepName;
1054
1069
  context;
1055
- constructor(context, stepName) {
1070
+ /**
1071
+ * step-level settings (flow control, retries etc.) which override the
1072
+ * settings the workflow run was triggered with, for this step only.
1073
+ */
1074
+ stepSettings;
1075
+ /**
1076
+ * whether this step's result can be submitted after the route function
1077
+ * has moved on, instead of right away.
1078
+ *
1079
+ * Enabled for steps whose `getResultStep` produces the final `out` —
1080
+ * which is not the same as "needs no server round trip":
1081
+ * `LazyCreateWebhookStep` builds its result in `getBody` and returns
1082
+ * `out: undefined` from `getResultStep`, so deferring it would hand
1083
+ * `undefined` to the route function.
1084
+ *
1085
+ * Deferring lets the route function continue and reveal the next step,
1086
+ * so that step's settings can ride on this step's submission instead
1087
+ * of needing a step config request of their own.
1088
+ */
1089
+ supportsDeferredSubmission = false;
1090
+ constructor(context, stepName, stepSettings) {
1056
1091
  this.context = context;
1092
+ this.stepSettings = stepSettings;
1057
1093
  if (!stepName) {
1058
1094
  throw new WorkflowError(
1059
1095
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1117,7 +1153,13 @@ var BaseLazyStep = class _BaseLazyStep {
1117
1153
  step.out = JSON.stringify(step.out);
1118
1154
  return JSON.stringify(step);
1119
1155
  }
1120
- getHeaders({ context, telemetry, invokeCount, step }) {
1156
+ getHeaders({
1157
+ context,
1158
+ telemetry,
1159
+ invokeCount,
1160
+ step,
1161
+ stepSettings
1162
+ }) {
1121
1163
  return getHeaders({
1122
1164
  initHeaderValue: "false",
1123
1165
  workflowConfig: {
@@ -1130,7 +1172,8 @@ var BaseLazyStep = class _BaseLazyStep {
1130
1172
  stepInfo: {
1131
1173
  step,
1132
1174
  lazyStep: this
1133
- }
1175
+ },
1176
+ stepSettings
1134
1177
  });
1135
1178
  }
1136
1179
  async submitStep({ context, body, headers }) {
@@ -1148,8 +1191,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1148
1191
  stepFunction;
1149
1192
  stepType = "Run";
1150
1193
  allowUndefinedOut = true;
1151
- constructor(context, stepName, stepFunction) {
1152
- super(context, stepName);
1194
+ supportsDeferredSubmission = true;
1195
+ constructor(context, stepName, stepFunction, stepSettings) {
1196
+ super(context, stepName, stepSettings);
1153
1197
  this.stepFunction = stepFunction;
1154
1198
  }
1155
1199
  getPlanStep(concurrent, targetStep) {
@@ -1179,6 +1223,7 @@ var LazySleepStep = class extends BaseLazyStep {
1179
1223
  sleep;
1180
1224
  stepType = "SleepFor";
1181
1225
  allowUndefinedOut = true;
1226
+ supportsDeferredSubmission = true;
1182
1227
  constructor(context, stepName, sleep) {
1183
1228
  super(context, stepName);
1184
1229
  this.sleep = sleep;
@@ -1218,6 +1263,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1218
1263
  sleepUntil;
1219
1264
  stepType = "SleepUntil";
1220
1265
  allowUndefinedOut = true;
1266
+ supportsDeferredSubmission = true;
1221
1267
  constructor(context, stepName, sleepUntil) {
1222
1268
  super(context, stepName);
1223
1269
  this.sleepUntil = sleepUntil;
@@ -1710,6 +1756,7 @@ var WorkflowHeaders = class {
1710
1756
  invokeCount;
1711
1757
  initHeaderValue;
1712
1758
  stepInfo;
1759
+ stepSettings;
1713
1760
  headers;
1714
1761
  /**
1715
1762
  * @param params workflow header parameters
@@ -1719,17 +1766,20 @@ var WorkflowHeaders = class {
1719
1766
  workflowConfig,
1720
1767
  invokeCount,
1721
1768
  initHeaderValue,
1722
- stepInfo
1769
+ stepInfo,
1770
+ stepSettings
1723
1771
  }) {
1724
1772
  this.userHeaders = userHeaders;
1725
1773
  this.workflowConfig = workflowConfig;
1726
1774
  this.invokeCount = invokeCount;
1727
1775
  this.initHeaderValue = initHeaderValue;
1728
1776
  this.stepInfo = stepInfo;
1777
+ this.stepSettings = stepSettings;
1729
1778
  this.headers = {
1730
1779
  rawHeaders: {},
1731
1780
  workflowHeaders: {},
1732
- failureHeaders: {}
1781
+ failureHeaders: {},
1782
+ stepSettingsHeaders: {}
1733
1783
  };
1734
1784
  }
1735
1785
  getHeaders() {
@@ -1740,6 +1790,7 @@ var WorkflowHeaders = class {
1740
1790
  this.addUserHeaders();
1741
1791
  this.addInvokeCount();
1742
1792
  this.addFailureUrl();
1793
+ this.addStepSettings();
1743
1794
  const contentType = this.addContentType();
1744
1795
  return this.prefixHeaders(contentType);
1745
1796
  }
@@ -1749,7 +1800,7 @@ var WorkflowHeaders = class {
1749
1800
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1750
1801
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1751
1802
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1752
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1803
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
1753
1804
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1754
1805
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1755
1806
  };
@@ -1827,7 +1878,7 @@ var WorkflowHeaders = class {
1827
1878
  this.headers.failureHeaders["Workflow-Init"] = "false";
1828
1879
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1829
1880
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1830
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1881
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
1831
1882
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1832
1883
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1833
1884
  }
@@ -1835,6 +1886,42 @@ var WorkflowHeaders = class {
1835
1886
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
1836
1887
  }
1837
1888
  }
1889
+ /**
1890
+ * Applies the step-level settings of a step to the message.
1891
+ *
1892
+ * These headers configure the message itself, so that QStash uses them
1893
+ * instead of the settings the run was triggered with. The delivery of
1894
+ * the message is what executes the step, which is how a step's
1895
+ * settings reach it.
1896
+ *
1897
+ * When any setting is present the feature set is extended with
1898
+ * `WF_StepConfig`, which is what tells QStash to keep them. QStash
1899
+ * reports that back on the delivery as `Upstash-Workflow-Step-Config`.
1900
+ */
1901
+ addStepSettings() {
1902
+ if (!this.stepSettings) {
1903
+ return;
1904
+ }
1905
+ const headers = {};
1906
+ if (this.stepSettings.flowControl) {
1907
+ const { flowControlKey, flowControlValue } = prepareFlowControl(
1908
+ this.stepSettings.flowControl
1909
+ );
1910
+ headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
1911
+ headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
1912
+ }
1913
+ if (this.stepSettings.retries !== void 0) {
1914
+ headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
1915
+ }
1916
+ if (this.stepSettings.retryDelay) {
1917
+ headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
1918
+ }
1919
+ if (Object.keys(headers).length === 0) {
1920
+ return;
1921
+ }
1922
+ headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
1923
+ this.headers.stepSettingsHeaders = headers;
1924
+ }
1838
1925
  addContentType() {
1839
1926
  if (this.workflowConfig.useJSONContent) {
1840
1927
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -1848,14 +1935,17 @@ var WorkflowHeaders = class {
1848
1935
  return contentType;
1849
1936
  }
1850
1937
  prefixHeaders(contentType) {
1851
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
1938
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
1852
1939
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
1853
1940
  return {
1854
1941
  headers: {
1855
1942
  ...rawHeaders,
1856
1943
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
1857
1944
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
1858
- ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
1945
+ ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
1946
+ // last, so they override the run's settings above. Never
1947
+ // prefixed: they configure this message, not its callback.
1948
+ ...stepSettingsHeaders
1859
1949
  },
1860
1950
  contentType
1861
1951
  };
@@ -1906,7 +1996,7 @@ var submitParallelSteps = async ({
1906
1996
  info: `Submitting ${planSteps.length} parallel steps.`
1907
1997
  });
1908
1998
  const result = await context.qstashClient.batch(
1909
- planSteps.map((planStep) => {
1999
+ planSteps.map((planStep, index) => {
1910
2000
  const { headers } = getHeaders({
1911
2001
  initHeaderValue: "false",
1912
2002
  workflowConfig: {
@@ -1914,7 +2004,8 @@ var submitParallelSteps = async ({
1914
2004
  workflowUrl: context.url,
1915
2005
  telemetry
1916
2006
  },
1917
- invokeCount
2007
+ invokeCount,
2008
+ stepSettings: steps[index].stepSettings
1918
2009
  });
1919
2010
  return {
1920
2011
  headers,
@@ -1933,31 +2024,38 @@ var submitParallelSteps = async ({
1933
2024
  }
1934
2025
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
1935
2026
  };
1936
- var submitSingleStep = async ({
1937
- context,
2027
+ var executeStep = async ({
1938
2028
  lazyStep,
1939
2029
  stepId,
1940
- invokeCount,
1941
2030
  concurrency,
1942
- telemetry,
1943
- dispatchDebug,
1944
2031
  dispatchLifecycle
1945
2032
  }) => {
1946
2033
  await dispatchLifecycle("beforeExecution", {
1947
2034
  stepName: lazyStep.stepName
1948
2035
  });
1949
- let resultStep;
1950
2036
  try {
1951
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2037
+ return await lazyStep.getResultStep(concurrency, stepId);
1952
2038
  } catch (error) {
1953
2039
  attachStepNameToError(error, lazyStep.stepName);
1954
2040
  throw error;
1955
2041
  }
2042
+ };
2043
+ var submitStepResult = async ({
2044
+ context,
2045
+ lazyStep,
2046
+ resultStep,
2047
+ invokeCount,
2048
+ concurrency,
2049
+ telemetry,
2050
+ dispatchDebug,
2051
+ nextStepSettings
2052
+ }) => {
1956
2053
  const { headers } = lazyStep.getHeaders({
1957
2054
  context,
1958
2055
  step: resultStep,
1959
2056
  invokeCount,
1960
- telemetry
2057
+ telemetry,
2058
+ stepSettings: nextStepSettings
1961
2059
  });
1962
2060
  const body = lazyStep.getBody({
1963
2061
  context,
@@ -1982,6 +2080,138 @@ var submitSingleStep = async ({
1982
2080
  }
1983
2081
  return resultStep;
1984
2082
  };
2083
+ var publishStepConfigRequest = async ({
2084
+ context,
2085
+ lazyStep,
2086
+ targetStep,
2087
+ invokeCount,
2088
+ telemetry,
2089
+ dispatchDebug
2090
+ }) => {
2091
+ const { headers } = getHeaders({
2092
+ initHeaderValue: "false",
2093
+ workflowConfig: {
2094
+ workflowRunId: context.workflowRunId,
2095
+ workflowUrl: context.url,
2096
+ telemetry
2097
+ },
2098
+ invokeCount,
2099
+ stepSettings: lazyStep.stepSettings
2100
+ });
2101
+ await dispatchDebug("onInfo", {
2102
+ info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
2103
+ });
2104
+ const result = await context.qstashClient.publishJSON({
2105
+ headers: {
2106
+ ...headers,
2107
+ "Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
2108
+ },
2109
+ method: "POST",
2110
+ body: { targetStep, invokeCount },
2111
+ url: context.url,
2112
+ contentBasedDeduplication: true
2113
+ });
2114
+ if (result?.deduplicated) {
2115
+ await dispatchDebug("onWarning", {
2116
+ 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.`
2117
+ });
2118
+ } else if (result?.messageId) {
2119
+ await dispatchDebug("onInfo", {
2120
+ info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
2121
+ });
2122
+ }
2123
+ };
2124
+
2125
+ // src/qstash/step-config.ts
2126
+ var DEFAULT_PARALLELISM = 0;
2127
+ var DEFAULT_RATE = 0;
2128
+ var DEFAULT_PERIOD_SECONDS = 1;
2129
+ var SECONDS_PER_UNIT = {
2130
+ s: 1,
2131
+ m: 60,
2132
+ h: 60 * 60,
2133
+ d: 24 * 60 * 60
2134
+ };
2135
+ var durationToSeconds = (duration) => {
2136
+ if (typeof duration === "number") {
2137
+ return Number.isFinite(duration) ? duration : void 0;
2138
+ }
2139
+ const match = /^(\d+)([smhd])$/.exec(duration.trim());
2140
+ if (match) {
2141
+ return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
2142
+ }
2143
+ const seconds = Number(duration.trim());
2144
+ return Number.isFinite(seconds) ? seconds : void 0;
2145
+ };
2146
+ var normalizeFlowControl = (flowControl) => {
2147
+ const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
2148
+ return {
2149
+ key: flowControl.key,
2150
+ parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
2151
+ rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
2152
+ period: period ?? DEFAULT_PERIOD_SECONDS
2153
+ };
2154
+ };
2155
+ var parseFlowControlHeaders = (key, value) => {
2156
+ if (!key) {
2157
+ return void 0;
2158
+ }
2159
+ const flowControl = {
2160
+ key,
2161
+ parallelism: DEFAULT_PARALLELISM,
2162
+ rate: DEFAULT_RATE,
2163
+ period: DEFAULT_PERIOD_SECONDS
2164
+ };
2165
+ for (const entry of (value ?? "").split(",")) {
2166
+ const [name, rawValue] = entry.split("=").map((part) => part.trim());
2167
+ if (!name || rawValue === void 0) {
2168
+ continue;
2169
+ }
2170
+ switch (name) {
2171
+ case "parallelism": {
2172
+ flowControl.parallelism = Number(rawValue);
2173
+ break;
2174
+ }
2175
+ case "rate": {
2176
+ flowControl.rate = Number(rawValue);
2177
+ break;
2178
+ }
2179
+ case "period": {
2180
+ flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
2181
+ break;
2182
+ }
2183
+ }
2184
+ }
2185
+ return flowControl;
2186
+ };
2187
+ var getEffectiveConfig = (headers) => {
2188
+ const retriesHeader = headers.get(RETRIES_HEADER);
2189
+ return {
2190
+ flowControl: parseFlowControlHeaders(
2191
+ headers.get(FLOW_CONTROL_KEY_HEADER),
2192
+ headers.get(FLOW_CONTROL_VALUE_HEADER)
2193
+ ),
2194
+ retries: retriesHeader === null ? void 0 : Number(retriesHeader),
2195
+ retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
2196
+ hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
2197
+ };
2198
+ };
2199
+ var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
2200
+ if (stepSettings.flowControl) {
2201
+ const wanted = normalizeFlowControl(stepSettings.flowControl);
2202
+ const applied = effectiveConfig.flowControl;
2203
+ if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
2204
+ return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
2205
+ }
2206
+ }
2207
+ if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
2208
+ return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
2209
+ }
2210
+ if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
2211
+ return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
2212
+ }
2213
+ return void 0;
2214
+ };
1985
2215
 
1986
2216
  // src/context/auto-executor.ts
1987
2217
  var AutoExecutor = class _AutoExecutor {
@@ -1998,6 +2228,25 @@ var AutoExecutor = class _AutoExecutor {
1998
2228
  stepCount = 0;
1999
2229
  planStepCount = 0;
2000
2230
  executingStep = false;
2231
+ /**
2232
+ * an executed step whose result hasn't been submitted to QStash yet.
2233
+ *
2234
+ * Set when a step which can produce its result in-process executes. The
2235
+ * submission waits so that the route function can continue and reveal
2236
+ * what comes next: if that is a single step with step-level settings,
2237
+ * the settings ride on this submission and no step config request is
2238
+ * needed, since the delivery of the submission is what executes it.
2239
+ *
2240
+ * Flushed when the next step is reached (see `addStep`) or when the
2241
+ * route function ends (see `flushPendingStep` in `serve`).
2242
+ */
2243
+ pendingStep;
2244
+ /**
2245
+ * configuration QStash applied to the delivery being handled. A step
2246
+ * with step-level settings is settled by comparing its settings against
2247
+ * this.
2248
+ */
2249
+ effectiveConfig;
2001
2250
  /**
2002
2251
  * @param context workflow context
2003
2252
  * @param steps list of steps
@@ -2005,14 +2254,17 @@ var AutoExecutor = class _AutoExecutor {
2005
2254
  * @param dispatchLifecycle lifecycle event dispatcher
2006
2255
  * @param telemetry optional telemetry information
2007
2256
  * @param invokeCount optional invoke count
2257
+ * @param effectiveConfig configuration QStash applied to the delivery
2258
+ * being handled
2008
2259
  */
2009
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
2260
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount, effectiveConfig) {
2010
2261
  this.context = context;
2011
2262
  this.steps = steps;
2012
2263
  this.dispatchDebug = dispatchDebug;
2013
2264
  this.dispatchLifecycle = dispatchLifecycle;
2014
2265
  this.telemetry = telemetry;
2015
2266
  this.invokeCount = invokeCount ?? 0;
2267
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2016
2268
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2017
2269
  }
2018
2270
  /**
@@ -2047,6 +2299,15 @@ var AutoExecutor = class _AutoExecutor {
2047
2299
  lazyStepList.push(stepInfo);
2048
2300
  const index = this.indexInCurrentList++;
2049
2301
  const requestComplete = this.deferExecution().then(async () => {
2302
+ const submitted = await this.submitPendingStep(
2303
+ lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
2304
+ );
2305
+ if (submitted.isErr()) {
2306
+ throw submitted.error;
2307
+ }
2308
+ if (submitted.value.result === "submitted-step") {
2309
+ throw submitted.value.abort;
2310
+ }
2050
2311
  if (!this.promises.has(lazyStepList)) {
2051
2312
  const promise2 = this.getExecutionPromise(lazyStepList);
2052
2313
  this.promises.set(lazyStepList, promise2);
@@ -2100,18 +2361,100 @@ var AutoExecutor = class _AutoExecutor {
2100
2361
  }
2101
2362
  return parsedOut;
2102
2363
  }
2103
- const resultStep = await submitSingleStep({
2104
- context: this.context,
2364
+ const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
2365
+ if (mismatch) {
2366
+ if (!this.effectiveConfig.hasStepConfig) {
2367
+ await publishStepConfigRequest({
2368
+ context: this.context,
2369
+ lazyStep,
2370
+ targetStep: this.stepCount,
2371
+ invokeCount: this.invokeCount,
2372
+ telemetry: this.telemetry,
2373
+ dispatchDebug: this.dispatchDebug
2374
+ });
2375
+ throw new WorkflowAbort(lazyStep.stepName);
2376
+ } else {
2377
+ await this.dispatchDebug("onWarning", {
2378
+ 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.`
2379
+ });
2380
+ }
2381
+ }
2382
+ const resultStep = await executeStep({
2105
2383
  lazyStep,
2106
2384
  stepId: this.stepCount,
2385
+ concurrency: NO_CONCURRENCY,
2386
+ dispatchLifecycle: this.dispatchLifecycle
2387
+ });
2388
+ if (lazyStep.supportsDeferredSubmission) {
2389
+ this.pendingStep = { status: "held", lazyStep, resultStep };
2390
+ return lazyStep.parseOut({
2391
+ ...resultStep,
2392
+ out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
2393
+ });
2394
+ }
2395
+ await submitStepResult({
2396
+ context: this.context,
2397
+ lazyStep,
2398
+ resultStep,
2107
2399
  invokeCount: this.invokeCount,
2108
- concurrency: 1,
2400
+ concurrency: NO_CONCURRENCY,
2109
2401
  telemetry: this.telemetry,
2110
- dispatchDebug: this.dispatchDebug,
2111
- dispatchLifecycle: this.dispatchLifecycle
2402
+ dispatchDebug: this.dispatchDebug
2112
2403
  });
2113
2404
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2114
2405
  }
2406
+ /**
2407
+ * Submits the result of the step being held, if there is one, and
2408
+ * returns the abort the caller has to throw to end the invocation.
2409
+ *
2410
+ * Returns undefined when no step is being held, which is the caller's
2411
+ * signal that there is nothing to end. Throws instead of returning
2412
+ * when the submission itself fails, so that error reaches the caller
2413
+ * rather than an abort claiming the step was submitted.
2414
+ *
2415
+ * Callers which arrive while a submission is already under way (the
2416
+ * steps of a parallel group, added together) wait for that one and get
2417
+ * the same abort, rather than submitting a second time.
2418
+ *
2419
+ * The next step's settings are attached whenever it has any, without
2420
+ * checking them against the current delivery: the executor only knows
2421
+ * the configuration of the delivery in hand, which inside a
2422
+ * delivery is the *previous* step's, so such a check would be wrong
2423
+ * exactly when two steps with settings follow each other.
2424
+ *
2425
+ * @param nextStepSettings step-level settings of the next step
2426
+ * @returns `submitted-step` with the abort which ends this invocation,
2427
+ * or `no-pending-step` when nothing was held and there is nothing to
2428
+ * end
2429
+ */
2430
+ async submitPendingStep(nextStepSettings) {
2431
+ if (!this.pendingStep) {
2432
+ return ok({ result: "no-pending-step" });
2433
+ }
2434
+ try {
2435
+ if (this.pendingStep.status === "submitting") {
2436
+ return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
2437
+ }
2438
+ const { lazyStep, resultStep } = this.pendingStep;
2439
+ const submitted = (async () => {
2440
+ await submitStepResult({
2441
+ context: this.context,
2442
+ lazyStep,
2443
+ resultStep,
2444
+ invokeCount: this.invokeCount,
2445
+ concurrency: NO_CONCURRENCY,
2446
+ telemetry: this.telemetry,
2447
+ dispatchDebug: this.dispatchDebug,
2448
+ nextStepSettings
2449
+ });
2450
+ return new WorkflowAbort(lazyStep.stepName, resultStep);
2451
+ })();
2452
+ this.pendingStep = { status: "submitting", submitted };
2453
+ return ok({ result: "submitted-step", abort: await submitted });
2454
+ } catch (error) {
2455
+ return err(error);
2456
+ }
2457
+ }
2115
2458
  /**
2116
2459
  * Runs steps in parallel.
2117
2460
  *
@@ -2160,15 +2503,20 @@ var AutoExecutor = class _AutoExecutor {
2160
2503
  validateStep(parallelSteps[stepIndex], planStep);
2161
2504
  try {
2162
2505
  const parallelStep = parallelSteps[stepIndex];
2163
- const resultStep = await submitSingleStep({
2164
- context: this.context,
2506
+ const resultStep = await executeStep({
2165
2507
  lazyStep: parallelStep,
2166
2508
  stepId: planStep.targetStep,
2509
+ concurrency: parallelSteps.length,
2510
+ dispatchLifecycle: this.dispatchLifecycle
2511
+ });
2512
+ await submitStepResult({
2513
+ context: this.context,
2514
+ lazyStep: parallelStep,
2515
+ resultStep,
2167
2516
  invokeCount: this.invokeCount,
2168
2517
  concurrency: parallelSteps.length,
2169
2518
  telemetry: this.telemetry,
2170
- dispatchDebug: this.dispatchDebug,
2171
- dispatchLifecycle: this.dispatchLifecycle
2519
+ dispatchDebug: this.dispatchDebug
2172
2520
  });
2173
2521
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2174
2522
  } catch (error) {
@@ -2717,6 +3065,46 @@ var WorkflowContext = class {
2717
3065
  * `1` on the first retry, `2` on the second, and so on.
2718
3066
  */
2719
3067
  retried;
3068
+ /**
3069
+ * Configuration QStash applied to the request being handled.
3070
+ *
3071
+ * This is the configuration of *this delivery*, not of the run: it is
3072
+ * the configuration the run was triggered with, except inside the
3073
+ * delivery which executes a step that has step-level settings, where
3074
+ * it is that step's settings. The fields below expose it; the executor
3075
+ * gets it directly rather than reaching through the context.
3076
+ */
3077
+ effectiveConfig;
3078
+ /**
3079
+ * Flow control QStash applied to the request being handled, if any.
3080
+ *
3081
+ * This is the flow control of the request in hand, which is the one the
3082
+ * run was triggered with except inside a step carrying its own.
3083
+ */
3084
+ get flowControl() {
3085
+ return this.effectiveConfig.flowControl;
3086
+ }
3087
+ /**
3088
+ * Retry limit QStash applied to the request being handled, if it
3089
+ * reported one.
3090
+ *
3091
+ * Not to be confused with {@link retried}, which is how many retries
3092
+ * have already happened.
3093
+ *
3094
+ * @see {@link flowControl} for what "the request being handled" means
3095
+ */
3096
+ get retries() {
3097
+ return this.effectiveConfig.retries;
3098
+ }
3099
+ /**
3100
+ * Retry delay expression QStash applied to the request being handled,
3101
+ * if any.
3102
+ *
3103
+ * @see {@link flowControl} for what "the request being handled" means
3104
+ */
3105
+ get retryDelay() {
3106
+ return this.effectiveConfig.retryDelay;
3107
+ }
2720
3108
  constructor({
2721
3109
  qstashClient,
2722
3110
  workflowRunId,
@@ -2730,7 +3118,8 @@ var WorkflowContext = class {
2730
3118
  invokeCount,
2731
3119
  label,
2732
3120
  retried,
2733
- middlewareManager
3121
+ middlewareManager,
3122
+ effectiveConfig
2734
3123
  }) {
2735
3124
  this.qstashClient = qstashClient;
2736
3125
  this.workflowRunId = workflowRunId;
@@ -2742,6 +3131,7 @@ var WorkflowContext = class {
2742
3131
  this.env = env ?? {};
2743
3132
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2744
3133
  this.retried = retried ?? 0;
3134
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2745
3135
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2746
3136
  middlewareManagerInstance.assignContext(this);
2747
3137
  this.executor = new AutoExecutor(
@@ -2750,7 +3140,8 @@ var WorkflowContext = class {
2750
3140
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2751
3141
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2752
3142
  telemetry,
2753
- invokeCount
3143
+ invokeCount,
3144
+ this.effectiveConfig
2754
3145
  );
2755
3146
  }
2756
3147
  /**
@@ -2776,13 +3167,35 @@ var WorkflowContext = class {
2776
3167
  * ])
2777
3168
  * ```
2778
3169
  *
3170
+ * Step-level settings can be passed as a third argument, overriding
3171
+ * the settings the workflow run was triggered with for this step only:
3172
+ *
3173
+ * ```typescript
3174
+ * const result = await context.run(
3175
+ * "step 1",
3176
+ * () => {
3177
+ * return "result"
3178
+ * },
3179
+ * {
3180
+ * flowControl: { key: "custom-key", parallelism: 3 },
3181
+ * retries: 5,
3182
+ * }
3183
+ * )
3184
+ * ```
3185
+ *
2779
3186
  * @param stepName name of the step
2780
3187
  * @param stepFunction step function to be executed
3188
+ * @param stepSettings step-level settings for this step
2781
3189
  * @returns result of the step function
2782
3190
  */
2783
- async run(stepName, stepFunction) {
3191
+ async run(stepName, stepFunction, stepSettings) {
3192
+ if (stepSettings) {
3193
+ validateFlowControl(stepSettings.flowControl);
3194
+ }
2784
3195
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2785
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3196
+ return await this.addStep(
3197
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3198
+ );
2786
3199
  }
2787
3200
  /**
2788
3201
  * Stops the execution for the duration provided.
@@ -3005,7 +3418,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3005
3418
  *
3006
3419
  * @param routeFunction
3007
3420
  */
3008
- static async tryAuthentication(routeFunction, context) {
3421
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3009
3422
  const disabledContext = new _DisabledWorkflowContext({
3010
3423
  qstashClient: new import_qstash10.Client({
3011
3424
  baseUrl: "disabled-client",
@@ -3020,7 +3433,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3020
3433
  initialPayload: context.requestPayload,
3021
3434
  env: context.env,
3022
3435
  label: context.labels,
3023
- retried: context.retried
3436
+ retried: context.retried,
3437
+ // the route function runs for real here until it reaches a step, so
3438
+ // it observes the same configuration as it will on the real context
3439
+ effectiveConfig
3024
3440
  });
3025
3441
  try {
3026
3442
  await routeFunction(disabledContext);
@@ -3237,6 +3653,7 @@ var handleFailure = async ({
3237
3653
  }
3238
3654
  const userHeaders = recreateUserHeaders(request.headers);
3239
3655
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3656
+ const effectiveConfig = getEffectiveConfig(request.headers);
3240
3657
  const workflowContext = new WorkflowContext({
3241
3658
  qstashClient,
3242
3659
  workflowRunId,
@@ -3250,11 +3667,13 @@ var handleFailure = async ({
3250
3667
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3251
3668
  retried,
3252
3669
  workflowRunCreatedAt: workflowCreatedAt,
3253
- middlewareManager: void 0
3670
+ middlewareManager: void 0,
3671
+ effectiveConfig
3254
3672
  });
3255
3673
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3256
3674
  routeFunction,
3257
- workflowContext
3675
+ workflowContext,
3676
+ effectiveConfig
3258
3677
  );
3259
3678
  if (authCheck.isErr()) {
3260
3679
  await dispatchDebug?.("onError", {
@@ -3346,7 +3765,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3346
3765
  const client = new import_qstash11.Client({
3347
3766
  ...clientOptions,
3348
3767
  baseUrl: clientEnv.QSTASH_URL,
3349
- token: clientEnv.QSTASH_TOKEN
3768
+ token: clientEnv.QSTASH_TOKEN,
3769
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3770
+ // the client at the local dev server too. Without this the QStash Client
3771
+ // re-derives dev mode from its own process.env, which is blind to the
3772
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3773
+ // An explicit devMode in the user's client config still wins.
3774
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3350
3775
  });
3351
3776
  const receiver = getReceiver(environment, receiverConfig, region);
3352
3777
  return { client, receiver };
@@ -3396,7 +3821,9 @@ var getQStashHandlers = ({
3396
3821
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3397
3822
  ...qstashClientOption,
3398
3823
  baseUrl: environment.QSTASH_URL,
3399
- token: environment.QSTASH_TOKEN
3824
+ token: environment.QSTASH_TOKEN,
3825
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
3826
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3400
3827
  }),
3401
3828
  receiver: getReceiver(environment, receiverConfig)
3402
3829
  }
@@ -3736,12 +4163,14 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3736
4163
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3737
4164
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3738
4165
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4166
+ const effectiveConfig = getEffectiveConfig(request.headers);
3739
4167
  const workflowContext = new WorkflowContext({
3740
4168
  qstashClient: regionalClient,
3741
4169
  workflowRunId,
3742
4170
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
3743
4171
  headers: recreateUserHeaders(request.headers),
3744
4172
  steps,
4173
+ effectiveConfig,
3745
4174
  url: workflowUrl,
3746
4175
  env,
3747
4176
  telemetry,
@@ -3753,7 +4182,8 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3753
4182
  });
3754
4183
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3755
4184
  routeFunction,
3756
- workflowContext
4185
+ workflowContext,
4186
+ effectiveConfig
3757
4187
  );
3758
4188
  if (authCheck.isErr()) {
3759
4189
  throw authCheck.error;
@@ -3790,7 +4220,23 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3790
4220
  if (steps.length === 1) {
3791
4221
  await middlewareManager.dispatchLifecycle("runStarted", {});
3792
4222
  }
3793
- return await routeFunction(workflowContext);
4223
+ let outcome;
4224
+ try {
4225
+ outcome = { ran: true, result: await routeFunction(workflowContext) };
4226
+ } catch (error) {
4227
+ outcome = { ran: false, error };
4228
+ }
4229
+ const submitted = await flushPendingStep(workflowContext);
4230
+ if (submitted.isErr()) {
4231
+ throw submitted.error;
4232
+ }
4233
+ if (submitted.value.result === "submitted-step") {
4234
+ throw submitted.value.abort;
4235
+ }
4236
+ if (!outcome.ran) {
4237
+ throw outcome.error;
4238
+ }
4239
+ return outcome.result;
3794
4240
  },
3795
4241
  onCleanup: async (result2) => {
3796
4242
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -3871,7 +4317,9 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3871
4317
  };
3872
4318
 
3873
4319
  // platforms/solidjs.ts
4320
+ var import_qstash13 = require("@upstash/qstash");
3874
4321
  var serve = (routeFunction, options) => {
4322
+ void (0, import_qstash13.startDevServer)();
3875
4323
  const telemetry = {
3876
4324
  sdk: SDK_TELEMETRY,
3877
4325
  framework: "solidjs",