@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/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
 
@@ -140,8 +143,15 @@ function isInstanceOf(v, ctor) {
140
143
  }
141
144
 
142
145
  // src/client/utils.ts
146
+ var assertNonEmptyId = (id, label = "id") => {
147
+ if (!id) {
148
+ throw new import_qstash2.QstashError(`${label} cannot be empty`);
149
+ }
150
+ };
143
151
  var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
144
- const path = workflowRunId ? ["v2", "notify", workflowRunId, eventId] : ["v2", "notify", eventId];
152
+ assertNonEmptyId(eventId, "Event id");
153
+ if (workflowRunId !== void 0) assertNonEmptyId(workflowRunId, "Workflow run id");
154
+ const path = workflowRunId === void 0 ? ["v2", "notify", eventId] : ["v2", "notify", workflowRunId, eventId];
145
155
  const result = await requester.request({
146
156
  path,
147
157
  method: "POST",
@@ -205,6 +215,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
205
215
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
206
216
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
207
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";
208
226
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
209
227
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
210
228
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -222,102 +240,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
222
240
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
223
241
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
224
242
 
225
- // src/context/auto-executor.ts
226
- var import_qstash5 = require("@upstash/qstash");
227
-
228
- // src/qstash/headers.ts
229
- var import_qstash4 = require("@upstash/qstash");
230
-
231
- // src/utils.ts
232
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
233
- var NANOID_LENGTH = 21;
234
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
235
- function validateLabel(label) {
236
- if (label === void 0) return;
237
- const labels = Array.isArray(label) ? label : [label];
238
- if (labels.length === 0) {
239
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
240
- }
241
- for (const value of labels) {
242
- if (!RESOURCE_NAME_PATTERN.test(value)) {
243
- throw new WorkflowNonRetryableError(
244
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
245
- );
246
- }
247
- }
248
- }
249
- function serializeLabel(label) {
250
- return Array.isArray(label) ? label.join(",") : label;
251
- }
252
- function validateFlowControl(flowControl) {
253
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
254
- throw new WorkflowNonRetryableError(
255
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
256
- );
257
- }
258
- }
259
- function getRandomInt() {
260
- return Math.floor(Math.random() * NANOID_CHARS.length);
261
- }
262
- function nanoid(length = NANOID_LENGTH) {
263
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
264
- }
265
- function getWorkflowRunId(id) {
266
- return `wfr_${id ?? nanoid()}`;
267
- }
268
- function decodeBase64(base64) {
269
- const binString = atob(base64);
270
- try {
271
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
272
- return new TextDecoder().decode(intArray);
273
- } catch (error) {
274
- console.warn(
275
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
276
- );
277
- return binString;
278
- }
279
- }
280
- function getUserIdFromToken(qstashClient) {
281
- try {
282
- const token = qstashClient.token;
283
- const decodedToken = decodeBase64(token);
284
- const tokenPayload = JSON.parse(decodedToken);
285
- const userId = tokenPayload.UserID;
286
- if (!userId) {
287
- throw new WorkflowError("QStash token payload does not contain userId");
288
- }
289
- return userId;
290
- } catch (error) {
291
- throw new WorkflowError(
292
- `Failed to decode QStash token while running create webhook step: ${error.message}`
293
- );
294
- }
295
- }
296
- function getQStashUrl(qstashClient) {
297
- try {
298
- const requester = qstashClient.http;
299
- const baseUrl = requester.baseUrl;
300
- if (!baseUrl) {
301
- throw new WorkflowError("QStash client does not have a baseUrl");
302
- }
303
- return baseUrl;
304
- } catch (error) {
305
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
306
- }
307
- }
308
- function getEventId() {
309
- return `evt_${nanoid(15)}`;
310
- }
311
- function stringifyBody(body) {
312
- if (body === void 0) {
313
- return void 0;
314
- }
315
- if (typeof body === "string") {
316
- return body;
317
- }
318
- return JSON.stringify(body);
319
- }
320
-
321
243
  // node_modules/neverthrow/dist/index.es.js
322
244
  var defaultErrorConfig = {
323
245
  withStackTrace: false
@@ -733,6 +655,102 @@ var Err = class {
733
655
  };
734
656
  var fromThrowable = Result.fromThrowable;
735
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
+
736
754
  // src/types.ts
737
755
  var StepTypes = [
738
756
  "Initial",
@@ -838,6 +856,10 @@ var triggerFirstInvocation = async (params) => {
838
856
  return err(error_);
839
857
  }
840
858
  };
859
+ var flushPendingStep = async (workflowContext) => {
860
+ const { executor } = workflowContext;
861
+ return await executor.submitPendingStep();
862
+ };
841
863
  var triggerRouteFunction = async ({
842
864
  onCleanup,
843
865
  onStep,
@@ -1045,8 +1067,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1045
1067
  var BaseLazyStep = class _BaseLazyStep {
1046
1068
  stepName;
1047
1069
  context;
1048
- 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) {
1049
1091
  this.context = context;
1092
+ this.stepSettings = stepSettings;
1050
1093
  if (!stepName) {
1051
1094
  throw new WorkflowError(
1052
1095
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1110,7 +1153,13 @@ var BaseLazyStep = class _BaseLazyStep {
1110
1153
  step.out = JSON.stringify(step.out);
1111
1154
  return JSON.stringify(step);
1112
1155
  }
1113
- getHeaders({ context, telemetry, invokeCount, step }) {
1156
+ getHeaders({
1157
+ context,
1158
+ telemetry,
1159
+ invokeCount,
1160
+ step,
1161
+ stepSettings
1162
+ }) {
1114
1163
  return getHeaders({
1115
1164
  initHeaderValue: "false",
1116
1165
  workflowConfig: {
@@ -1123,7 +1172,8 @@ var BaseLazyStep = class _BaseLazyStep {
1123
1172
  stepInfo: {
1124
1173
  step,
1125
1174
  lazyStep: this
1126
- }
1175
+ },
1176
+ stepSettings
1127
1177
  });
1128
1178
  }
1129
1179
  async submitStep({ context, body, headers }) {
@@ -1141,8 +1191,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1141
1191
  stepFunction;
1142
1192
  stepType = "Run";
1143
1193
  allowUndefinedOut = true;
1144
- constructor(context, stepName, stepFunction) {
1145
- super(context, stepName);
1194
+ supportsDeferredSubmission = true;
1195
+ constructor(context, stepName, stepFunction, stepSettings) {
1196
+ super(context, stepName, stepSettings);
1146
1197
  this.stepFunction = stepFunction;
1147
1198
  }
1148
1199
  getPlanStep(concurrent, targetStep) {
@@ -1172,6 +1223,7 @@ var LazySleepStep = class extends BaseLazyStep {
1172
1223
  sleep;
1173
1224
  stepType = "SleepFor";
1174
1225
  allowUndefinedOut = true;
1226
+ supportsDeferredSubmission = true;
1175
1227
  constructor(context, stepName, sleep) {
1176
1228
  super(context, stepName);
1177
1229
  this.sleep = sleep;
@@ -1211,6 +1263,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1211
1263
  sleepUntil;
1212
1264
  stepType = "SleepUntil";
1213
1265
  allowUndefinedOut = true;
1266
+ supportsDeferredSubmission = true;
1214
1267
  constructor(context, stepName, sleepUntil) {
1215
1268
  super(context, stepName);
1216
1269
  this.sleepUntil = sleepUntil;
@@ -1703,6 +1756,7 @@ var WorkflowHeaders = class {
1703
1756
  invokeCount;
1704
1757
  initHeaderValue;
1705
1758
  stepInfo;
1759
+ stepSettings;
1706
1760
  headers;
1707
1761
  /**
1708
1762
  * @param params workflow header parameters
@@ -1712,17 +1766,20 @@ var WorkflowHeaders = class {
1712
1766
  workflowConfig,
1713
1767
  invokeCount,
1714
1768
  initHeaderValue,
1715
- stepInfo
1769
+ stepInfo,
1770
+ stepSettings
1716
1771
  }) {
1717
1772
  this.userHeaders = userHeaders;
1718
1773
  this.workflowConfig = workflowConfig;
1719
1774
  this.invokeCount = invokeCount;
1720
1775
  this.initHeaderValue = initHeaderValue;
1721
1776
  this.stepInfo = stepInfo;
1777
+ this.stepSettings = stepSettings;
1722
1778
  this.headers = {
1723
1779
  rawHeaders: {},
1724
1780
  workflowHeaders: {},
1725
- failureHeaders: {}
1781
+ failureHeaders: {},
1782
+ stepSettingsHeaders: {}
1726
1783
  };
1727
1784
  }
1728
1785
  getHeaders() {
@@ -1733,6 +1790,7 @@ var WorkflowHeaders = class {
1733
1790
  this.addUserHeaders();
1734
1791
  this.addInvokeCount();
1735
1792
  this.addFailureUrl();
1793
+ this.addStepSettings();
1736
1794
  const contentType = this.addContentType();
1737
1795
  return this.prefixHeaders(contentType);
1738
1796
  }
@@ -1742,7 +1800,7 @@ var WorkflowHeaders = class {
1742
1800
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1743
1801
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1744
1802
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1745
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1803
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
1746
1804
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1747
1805
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1748
1806
  };
@@ -1820,7 +1878,7 @@ var WorkflowHeaders = class {
1820
1878
  this.headers.failureHeaders["Workflow-Init"] = "false";
1821
1879
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1822
1880
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1823
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1881
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
1824
1882
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1825
1883
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1826
1884
  }
@@ -1828,6 +1886,42 @@ var WorkflowHeaders = class {
1828
1886
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
1829
1887
  }
1830
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
+ }
1831
1925
  addContentType() {
1832
1926
  if (this.workflowConfig.useJSONContent) {
1833
1927
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -1841,14 +1935,17 @@ var WorkflowHeaders = class {
1841
1935
  return contentType;
1842
1936
  }
1843
1937
  prefixHeaders(contentType) {
1844
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
1938
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
1845
1939
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
1846
1940
  return {
1847
1941
  headers: {
1848
1942
  ...rawHeaders,
1849
1943
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
1850
1944
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
1851
- ...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
1852
1949
  },
1853
1950
  contentType
1854
1951
  };
@@ -1899,7 +1996,7 @@ var submitParallelSteps = async ({
1899
1996
  info: `Submitting ${planSteps.length} parallel steps.`
1900
1997
  });
1901
1998
  const result = await context.qstashClient.batch(
1902
- planSteps.map((planStep) => {
1999
+ planSteps.map((planStep, index) => {
1903
2000
  const { headers } = getHeaders({
1904
2001
  initHeaderValue: "false",
1905
2002
  workflowConfig: {
@@ -1907,7 +2004,8 @@ var submitParallelSteps = async ({
1907
2004
  workflowUrl: context.url,
1908
2005
  telemetry
1909
2006
  },
1910
- invokeCount
2007
+ invokeCount,
2008
+ stepSettings: steps[index].stepSettings
1911
2009
  });
1912
2010
  return {
1913
2011
  headers,
@@ -1926,31 +2024,38 @@ var submitParallelSteps = async ({
1926
2024
  }
1927
2025
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
1928
2026
  };
1929
- var submitSingleStep = async ({
1930
- context,
2027
+ var executeStep = async ({
1931
2028
  lazyStep,
1932
2029
  stepId,
1933
- invokeCount,
1934
2030
  concurrency,
1935
- telemetry,
1936
- dispatchDebug,
1937
2031
  dispatchLifecycle
1938
2032
  }) => {
1939
2033
  await dispatchLifecycle("beforeExecution", {
1940
2034
  stepName: lazyStep.stepName
1941
2035
  });
1942
- let resultStep;
1943
2036
  try {
1944
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2037
+ return await lazyStep.getResultStep(concurrency, stepId);
1945
2038
  } catch (error) {
1946
2039
  attachStepNameToError(error, lazyStep.stepName);
1947
2040
  throw error;
1948
2041
  }
2042
+ };
2043
+ var submitStepResult = async ({
2044
+ context,
2045
+ lazyStep,
2046
+ resultStep,
2047
+ invokeCount,
2048
+ concurrency,
2049
+ telemetry,
2050
+ dispatchDebug,
2051
+ nextStepSettings
2052
+ }) => {
1949
2053
  const { headers } = lazyStep.getHeaders({
1950
2054
  context,
1951
2055
  step: resultStep,
1952
2056
  invokeCount,
1953
- telemetry
2057
+ telemetry,
2058
+ stepSettings: nextStepSettings
1954
2059
  });
1955
2060
  const body = lazyStep.getBody({
1956
2061
  context,
@@ -1975,6 +2080,138 @@ var submitSingleStep = async ({
1975
2080
  }
1976
2081
  return resultStep;
1977
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
+ };
1978
2215
 
1979
2216
  // src/context/auto-executor.ts
1980
2217
  var AutoExecutor = class _AutoExecutor {
@@ -1991,6 +2228,25 @@ var AutoExecutor = class _AutoExecutor {
1991
2228
  stepCount = 0;
1992
2229
  planStepCount = 0;
1993
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;
1994
2250
  /**
1995
2251
  * @param context workflow context
1996
2252
  * @param steps list of steps
@@ -1998,14 +2254,17 @@ var AutoExecutor = class _AutoExecutor {
1998
2254
  * @param dispatchLifecycle lifecycle event dispatcher
1999
2255
  * @param telemetry optional telemetry information
2000
2256
  * @param invokeCount optional invoke count
2257
+ * @param effectiveConfig configuration QStash applied to the delivery
2258
+ * being handled
2001
2259
  */
2002
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
2260
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount, effectiveConfig) {
2003
2261
  this.context = context;
2004
2262
  this.steps = steps;
2005
2263
  this.dispatchDebug = dispatchDebug;
2006
2264
  this.dispatchLifecycle = dispatchLifecycle;
2007
2265
  this.telemetry = telemetry;
2008
2266
  this.invokeCount = invokeCount ?? 0;
2267
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2009
2268
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2010
2269
  }
2011
2270
  /**
@@ -2040,6 +2299,15 @@ var AutoExecutor = class _AutoExecutor {
2040
2299
  lazyStepList.push(stepInfo);
2041
2300
  const index = this.indexInCurrentList++;
2042
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
+ }
2043
2311
  if (!this.promises.has(lazyStepList)) {
2044
2312
  const promise2 = this.getExecutionPromise(lazyStepList);
2045
2313
  this.promises.set(lazyStepList, promise2);
@@ -2093,18 +2361,100 @@ var AutoExecutor = class _AutoExecutor {
2093
2361
  }
2094
2362
  return parsedOut;
2095
2363
  }
2096
- const resultStep = await submitSingleStep({
2097
- 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({
2098
2383
  lazyStep,
2099
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,
2100
2399
  invokeCount: this.invokeCount,
2101
- concurrency: 1,
2400
+ concurrency: NO_CONCURRENCY,
2102
2401
  telemetry: this.telemetry,
2103
- dispatchDebug: this.dispatchDebug,
2104
- dispatchLifecycle: this.dispatchLifecycle
2402
+ dispatchDebug: this.dispatchDebug
2105
2403
  });
2106
2404
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2107
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
+ }
2108
2458
  /**
2109
2459
  * Runs steps in parallel.
2110
2460
  *
@@ -2153,15 +2503,20 @@ var AutoExecutor = class _AutoExecutor {
2153
2503
  validateStep(parallelSteps[stepIndex], planStep);
2154
2504
  try {
2155
2505
  const parallelStep = parallelSteps[stepIndex];
2156
- const resultStep = await submitSingleStep({
2157
- context: this.context,
2506
+ const resultStep = await executeStep({
2158
2507
  lazyStep: parallelStep,
2159
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,
2160
2516
  invokeCount: this.invokeCount,
2161
2517
  concurrency: parallelSteps.length,
2162
2518
  telemetry: this.telemetry,
2163
- dispatchDebug: this.dispatchDebug,
2164
- dispatchLifecycle: this.dispatchLifecycle
2519
+ dispatchDebug: this.dispatchDebug
2165
2520
  });
2166
2521
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2167
2522
  } catch (error) {
@@ -2710,6 +3065,46 @@ var WorkflowContext = class {
2710
3065
  * `1` on the first retry, `2` on the second, and so on.
2711
3066
  */
2712
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
+ }
2713
3108
  constructor({
2714
3109
  qstashClient,
2715
3110
  workflowRunId,
@@ -2723,7 +3118,8 @@ var WorkflowContext = class {
2723
3118
  invokeCount,
2724
3119
  label,
2725
3120
  retried,
2726
- middlewareManager
3121
+ middlewareManager,
3122
+ effectiveConfig
2727
3123
  }) {
2728
3124
  this.qstashClient = qstashClient;
2729
3125
  this.workflowRunId = workflowRunId;
@@ -2735,6 +3131,7 @@ var WorkflowContext = class {
2735
3131
  this.env = env ?? {};
2736
3132
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2737
3133
  this.retried = retried ?? 0;
3134
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2738
3135
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2739
3136
  middlewareManagerInstance.assignContext(this);
2740
3137
  this.executor = new AutoExecutor(
@@ -2743,7 +3140,8 @@ var WorkflowContext = class {
2743
3140
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2744
3141
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2745
3142
  telemetry,
2746
- invokeCount
3143
+ invokeCount,
3144
+ this.effectiveConfig
2747
3145
  );
2748
3146
  }
2749
3147
  /**
@@ -2769,13 +3167,35 @@ var WorkflowContext = class {
2769
3167
  * ])
2770
3168
  * ```
2771
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
+ *
2772
3186
  * @param stepName name of the step
2773
3187
  * @param stepFunction step function to be executed
3188
+ * @param stepSettings step-level settings for this step
2774
3189
  * @returns result of the step function
2775
3190
  */
2776
- async run(stepName, stepFunction) {
3191
+ async run(stepName, stepFunction, stepSettings) {
3192
+ if (stepSettings) {
3193
+ validateFlowControl(stepSettings.flowControl);
3194
+ }
2777
3195
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2778
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3196
+ return await this.addStep(
3197
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3198
+ );
2779
3199
  }
2780
3200
  /**
2781
3201
  * Stops the execution for the duration provided.
@@ -2998,7 +3418,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
2998
3418
  *
2999
3419
  * @param routeFunction
3000
3420
  */
3001
- static async tryAuthentication(routeFunction, context) {
3421
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3002
3422
  const disabledContext = new _DisabledWorkflowContext({
3003
3423
  qstashClient: new import_qstash10.Client({
3004
3424
  baseUrl: "disabled-client",
@@ -3013,7 +3433,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3013
3433
  initialPayload: context.requestPayload,
3014
3434
  env: context.env,
3015
3435
  label: context.labels,
3016
- 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
3017
3440
  });
3018
3441
  try {
3019
3442
  await routeFunction(disabledContext);
@@ -3230,6 +3653,7 @@ var handleFailure = async ({
3230
3653
  }
3231
3654
  const userHeaders = recreateUserHeaders(request.headers);
3232
3655
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3656
+ const effectiveConfig = getEffectiveConfig(request.headers);
3233
3657
  const workflowContext = new WorkflowContext({
3234
3658
  qstashClient,
3235
3659
  workflowRunId,
@@ -3243,11 +3667,13 @@ var handleFailure = async ({
3243
3667
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3244
3668
  retried,
3245
3669
  workflowRunCreatedAt: workflowCreatedAt,
3246
- middlewareManager: void 0
3670
+ middlewareManager: void 0,
3671
+ effectiveConfig
3247
3672
  });
3248
3673
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3249
3674
  routeFunction,
3250
- workflowContext
3675
+ workflowContext,
3676
+ effectiveConfig
3251
3677
  );
3252
3678
  if (authCheck.isErr()) {
3253
3679
  await dispatchDebug?.("onError", {
@@ -3339,7 +3765,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3339
3765
  const client = new import_qstash11.Client({
3340
3766
  ...clientOptions,
3341
3767
  baseUrl: clientEnv.QSTASH_URL,
3342
- 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)
3343
3775
  });
3344
3776
  const receiver = getReceiver(environment, receiverConfig, region);
3345
3777
  return { client, receiver };
@@ -3389,7 +3821,9 @@ var getQStashHandlers = ({
3389
3821
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3390
3822
  ...qstashClientOption,
3391
3823
  baseUrl: environment.QSTASH_URL,
3392
- 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)
3393
3827
  }),
3394
3828
  receiver: getReceiver(environment, receiverConfig)
3395
3829
  }
@@ -3729,12 +4163,14 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3729
4163
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3730
4164
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3731
4165
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4166
+ const effectiveConfig = getEffectiveConfig(request.headers);
3732
4167
  const workflowContext = new WorkflowContext({
3733
4168
  qstashClient: regionalClient,
3734
4169
  workflowRunId,
3735
4170
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
3736
4171
  headers: recreateUserHeaders(request.headers),
3737
4172
  steps,
4173
+ effectiveConfig,
3738
4174
  url: workflowUrl,
3739
4175
  env,
3740
4176
  telemetry,
@@ -3746,7 +4182,8 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3746
4182
  });
3747
4183
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3748
4184
  routeFunction,
3749
- workflowContext
4185
+ workflowContext,
4186
+ effectiveConfig
3750
4187
  );
3751
4188
  if (authCheck.isErr()) {
3752
4189
  throw authCheck.error;
@@ -3783,7 +4220,23 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3783
4220
  if (steps.length === 1) {
3784
4221
  await middlewareManager.dispatchLifecycle("runStarted", {});
3785
4222
  }
3786
- 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;
3787
4240
  },
3788
4241
  onCleanup: async (result2) => {
3789
4242
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -3864,7 +4317,9 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3864
4317
  };
3865
4318
 
3866
4319
  // platforms/solidjs.ts
4320
+ var import_qstash13 = require("@upstash/qstash");
3867
4321
  var serve = (routeFunction, options) => {
4322
+ void (0, import_qstash13.startDevServer)();
3868
4323
  const telemetry = {
3869
4324
  sdk: SDK_TELEMETRY,
3870
4325
  framework: "solidjs",