@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/svelte.js CHANGED
@@ -26,6 +26,9 @@ __export(svelte_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(svelte_exports);
28
28
 
29
+ // src/serve/index.ts
30
+ var import_qstash12 = require("@upstash/qstash");
31
+
29
32
  // src/client/utils.ts
30
33
  var import_qstash2 = require("@upstash/qstash");
31
34
 
@@ -142,8 +145,15 @@ function isInstanceOf(v, ctor) {
142
145
  }
143
146
 
144
147
  // src/client/utils.ts
148
+ var assertNonEmptyId = (id, label = "id") => {
149
+ if (!id) {
150
+ throw new import_qstash2.QstashError(`${label} cannot be empty`);
151
+ }
152
+ };
145
153
  var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
146
- const path = workflowRunId ? ["v2", "notify", workflowRunId, eventId] : ["v2", "notify", eventId];
154
+ assertNonEmptyId(eventId, "Event id");
155
+ if (workflowRunId !== void 0) assertNonEmptyId(workflowRunId, "Workflow run id");
156
+ const path = workflowRunId === void 0 ? ["v2", "notify", eventId] : ["v2", "notify", workflowRunId, eventId];
147
157
  const result = await requester.request({
148
158
  path,
149
159
  method: "POST",
@@ -207,6 +217,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
207
217
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
208
218
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
209
219
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
220
+ var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
221
+ var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
222
+ var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
223
+ var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
224
+ var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
225
+ var RETRIES_HEADER = "Upstash-Retries";
226
+ var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
227
+ var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
210
228
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
211
229
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
212
230
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -224,102 +242,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
224
242
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
225
243
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
226
244
 
227
- // src/context/auto-executor.ts
228
- var import_qstash5 = require("@upstash/qstash");
229
-
230
- // src/qstash/headers.ts
231
- var import_qstash4 = require("@upstash/qstash");
232
-
233
- // src/utils.ts
234
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
235
- var NANOID_LENGTH = 21;
236
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
237
- function validateLabel(label) {
238
- if (label === void 0) return;
239
- const labels = Array.isArray(label) ? label : [label];
240
- if (labels.length === 0) {
241
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
242
- }
243
- for (const value of labels) {
244
- if (!RESOURCE_NAME_PATTERN.test(value)) {
245
- throw new WorkflowNonRetryableError(
246
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
247
- );
248
- }
249
- }
250
- }
251
- function serializeLabel(label) {
252
- return Array.isArray(label) ? label.join(",") : label;
253
- }
254
- function validateFlowControl(flowControl) {
255
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
256
- throw new WorkflowNonRetryableError(
257
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
258
- );
259
- }
260
- }
261
- function getRandomInt() {
262
- return Math.floor(Math.random() * NANOID_CHARS.length);
263
- }
264
- function nanoid(length = NANOID_LENGTH) {
265
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
266
- }
267
- function getWorkflowRunId(id) {
268
- return `wfr_${id ?? nanoid()}`;
269
- }
270
- function decodeBase64(base64) {
271
- const binString = atob(base64);
272
- try {
273
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
274
- return new TextDecoder().decode(intArray);
275
- } catch (error) {
276
- console.warn(
277
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
278
- );
279
- return binString;
280
- }
281
- }
282
- function getUserIdFromToken(qstashClient) {
283
- try {
284
- const token = qstashClient.token;
285
- const decodedToken = decodeBase64(token);
286
- const tokenPayload = JSON.parse(decodedToken);
287
- const userId = tokenPayload.UserID;
288
- if (!userId) {
289
- throw new WorkflowError("QStash token payload does not contain userId");
290
- }
291
- return userId;
292
- } catch (error) {
293
- throw new WorkflowError(
294
- `Failed to decode QStash token while running create webhook step: ${error.message}`
295
- );
296
- }
297
- }
298
- function getQStashUrl(qstashClient) {
299
- try {
300
- const requester = qstashClient.http;
301
- const baseUrl = requester.baseUrl;
302
- if (!baseUrl) {
303
- throw new WorkflowError("QStash client does not have a baseUrl");
304
- }
305
- return baseUrl;
306
- } catch (error) {
307
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
308
- }
309
- }
310
- function getEventId() {
311
- return `evt_${nanoid(15)}`;
312
- }
313
- function stringifyBody(body) {
314
- if (body === void 0) {
315
- return void 0;
316
- }
317
- if (typeof body === "string") {
318
- return body;
319
- }
320
- return JSON.stringify(body);
321
- }
322
-
323
245
  // node_modules/neverthrow/dist/index.es.js
324
246
  var defaultErrorConfig = {
325
247
  withStackTrace: false
@@ -735,6 +657,102 @@ var Err = class {
735
657
  };
736
658
  var fromThrowable = Result.fromThrowable;
737
659
 
660
+ // src/context/auto-executor.ts
661
+ var import_qstash5 = require("@upstash/qstash");
662
+
663
+ // src/qstash/headers.ts
664
+ var import_qstash4 = require("@upstash/qstash");
665
+
666
+ // src/utils.ts
667
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
668
+ var NANOID_LENGTH = 21;
669
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
670
+ function validateLabel(label) {
671
+ if (label === void 0) return;
672
+ const labels = Array.isArray(label) ? label : [label];
673
+ if (labels.length === 0) {
674
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
675
+ }
676
+ for (const value of labels) {
677
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
678
+ throw new WorkflowNonRetryableError(
679
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
680
+ );
681
+ }
682
+ }
683
+ }
684
+ function serializeLabel(label) {
685
+ return Array.isArray(label) ? label.join(",") : label;
686
+ }
687
+ function validateFlowControl(flowControl) {
688
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
689
+ throw new WorkflowNonRetryableError(
690
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
691
+ );
692
+ }
693
+ }
694
+ function getRandomInt() {
695
+ return Math.floor(Math.random() * NANOID_CHARS.length);
696
+ }
697
+ function nanoid(length = NANOID_LENGTH) {
698
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
699
+ }
700
+ function getWorkflowRunId(id) {
701
+ return `wfr_${id ?? nanoid()}`;
702
+ }
703
+ function decodeBase64(base64) {
704
+ const binString = atob(base64);
705
+ try {
706
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
707
+ return new TextDecoder().decode(intArray);
708
+ } catch (error) {
709
+ console.warn(
710
+ `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
711
+ );
712
+ return binString;
713
+ }
714
+ }
715
+ function getUserIdFromToken(qstashClient) {
716
+ try {
717
+ const token = qstashClient.token;
718
+ const decodedToken = decodeBase64(token);
719
+ const tokenPayload = JSON.parse(decodedToken);
720
+ const userId = tokenPayload.UserID;
721
+ if (!userId) {
722
+ throw new WorkflowError("QStash token payload does not contain userId");
723
+ }
724
+ return userId;
725
+ } catch (error) {
726
+ throw new WorkflowError(
727
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
728
+ );
729
+ }
730
+ }
731
+ function getQStashUrl(qstashClient) {
732
+ try {
733
+ const requester = qstashClient.http;
734
+ const baseUrl = requester.baseUrl;
735
+ if (!baseUrl) {
736
+ throw new WorkflowError("QStash client does not have a baseUrl");
737
+ }
738
+ return baseUrl;
739
+ } catch (error) {
740
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
741
+ }
742
+ }
743
+ function getEventId() {
744
+ return `evt_${nanoid(15)}`;
745
+ }
746
+ function stringifyBody(body) {
747
+ if (body === void 0) {
748
+ return void 0;
749
+ }
750
+ if (typeof body === "string") {
751
+ return body;
752
+ }
753
+ return JSON.stringify(body);
754
+ }
755
+
738
756
  // src/types.ts
739
757
  var StepTypes = [
740
758
  "Initial",
@@ -840,6 +858,10 @@ var triggerFirstInvocation = async (params) => {
840
858
  return err(error_);
841
859
  }
842
860
  };
861
+ var flushPendingStep = async (workflowContext) => {
862
+ const { executor } = workflowContext;
863
+ return await executor.submitPendingStep();
864
+ };
843
865
  var triggerRouteFunction = async ({
844
866
  onCleanup,
845
867
  onStep,
@@ -1047,8 +1069,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1047
1069
  var BaseLazyStep = class _BaseLazyStep {
1048
1070
  stepName;
1049
1071
  context;
1050
- constructor(context, stepName) {
1072
+ /**
1073
+ * step-level settings (flow control, retries etc.) which override the
1074
+ * settings the workflow run was triggered with, for this step only.
1075
+ */
1076
+ stepSettings;
1077
+ /**
1078
+ * whether this step's result can be submitted after the route function
1079
+ * has moved on, instead of right away.
1080
+ *
1081
+ * Enabled for steps whose `getResultStep` produces the final `out` —
1082
+ * which is not the same as "needs no server round trip":
1083
+ * `LazyCreateWebhookStep` builds its result in `getBody` and returns
1084
+ * `out: undefined` from `getResultStep`, so deferring it would hand
1085
+ * `undefined` to the route function.
1086
+ *
1087
+ * Deferring lets the route function continue and reveal the next step,
1088
+ * so that step's settings can ride on this step's submission instead
1089
+ * of needing a step config request of their own.
1090
+ */
1091
+ supportsDeferredSubmission = false;
1092
+ constructor(context, stepName, stepSettings) {
1051
1093
  this.context = context;
1094
+ this.stepSettings = stepSettings;
1052
1095
  if (!stepName) {
1053
1096
  throw new WorkflowError(
1054
1097
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1112,7 +1155,13 @@ var BaseLazyStep = class _BaseLazyStep {
1112
1155
  step.out = JSON.stringify(step.out);
1113
1156
  return JSON.stringify(step);
1114
1157
  }
1115
- getHeaders({ context, telemetry: telemetry2, invokeCount, step }) {
1158
+ getHeaders({
1159
+ context,
1160
+ telemetry: telemetry2,
1161
+ invokeCount,
1162
+ step,
1163
+ stepSettings
1164
+ }) {
1116
1165
  return getHeaders({
1117
1166
  initHeaderValue: "false",
1118
1167
  workflowConfig: {
@@ -1125,7 +1174,8 @@ var BaseLazyStep = class _BaseLazyStep {
1125
1174
  stepInfo: {
1126
1175
  step,
1127
1176
  lazyStep: this
1128
- }
1177
+ },
1178
+ stepSettings
1129
1179
  });
1130
1180
  }
1131
1181
  async submitStep({ context, body, headers }) {
@@ -1143,8 +1193,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1143
1193
  stepFunction;
1144
1194
  stepType = "Run";
1145
1195
  allowUndefinedOut = true;
1146
- constructor(context, stepName, stepFunction) {
1147
- super(context, stepName);
1196
+ supportsDeferredSubmission = true;
1197
+ constructor(context, stepName, stepFunction, stepSettings) {
1198
+ super(context, stepName, stepSettings);
1148
1199
  this.stepFunction = stepFunction;
1149
1200
  }
1150
1201
  getPlanStep(concurrent, targetStep) {
@@ -1174,6 +1225,7 @@ var LazySleepStep = class extends BaseLazyStep {
1174
1225
  sleep;
1175
1226
  stepType = "SleepFor";
1176
1227
  allowUndefinedOut = true;
1228
+ supportsDeferredSubmission = true;
1177
1229
  constructor(context, stepName, sleep) {
1178
1230
  super(context, stepName);
1179
1231
  this.sleep = sleep;
@@ -1213,6 +1265,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1213
1265
  sleepUntil;
1214
1266
  stepType = "SleepUntil";
1215
1267
  allowUndefinedOut = true;
1268
+ supportsDeferredSubmission = true;
1216
1269
  constructor(context, stepName, sleepUntil) {
1217
1270
  super(context, stepName);
1218
1271
  this.sleepUntil = sleepUntil;
@@ -1705,6 +1758,7 @@ var WorkflowHeaders = class {
1705
1758
  invokeCount;
1706
1759
  initHeaderValue;
1707
1760
  stepInfo;
1761
+ stepSettings;
1708
1762
  headers;
1709
1763
  /**
1710
1764
  * @param params workflow header parameters
@@ -1714,17 +1768,20 @@ var WorkflowHeaders = class {
1714
1768
  workflowConfig,
1715
1769
  invokeCount,
1716
1770
  initHeaderValue,
1717
- stepInfo
1771
+ stepInfo,
1772
+ stepSettings
1718
1773
  }) {
1719
1774
  this.userHeaders = userHeaders;
1720
1775
  this.workflowConfig = workflowConfig;
1721
1776
  this.invokeCount = invokeCount;
1722
1777
  this.initHeaderValue = initHeaderValue;
1723
1778
  this.stepInfo = stepInfo;
1779
+ this.stepSettings = stepSettings;
1724
1780
  this.headers = {
1725
1781
  rawHeaders: {},
1726
1782
  workflowHeaders: {},
1727
- failureHeaders: {}
1783
+ failureHeaders: {},
1784
+ stepSettingsHeaders: {}
1728
1785
  };
1729
1786
  }
1730
1787
  getHeaders() {
@@ -1735,6 +1792,7 @@ var WorkflowHeaders = class {
1735
1792
  this.addUserHeaders();
1736
1793
  this.addInvokeCount();
1737
1794
  this.addFailureUrl();
1795
+ this.addStepSettings();
1738
1796
  const contentType = this.addContentType();
1739
1797
  return this.prefixHeaders(contentType);
1740
1798
  }
@@ -1744,7 +1802,7 @@ var WorkflowHeaders = class {
1744
1802
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1745
1803
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1746
1804
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1747
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1805
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
1748
1806
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1749
1807
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1750
1808
  };
@@ -1822,7 +1880,7 @@ var WorkflowHeaders = class {
1822
1880
  this.headers.failureHeaders["Workflow-Init"] = "false";
1823
1881
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1824
1882
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1825
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1883
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
1826
1884
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1827
1885
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1828
1886
  }
@@ -1830,6 +1888,42 @@ var WorkflowHeaders = class {
1830
1888
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
1831
1889
  }
1832
1890
  }
1891
+ /**
1892
+ * Applies the step-level settings of a step to the message.
1893
+ *
1894
+ * These headers configure the message itself, so that QStash uses them
1895
+ * instead of the settings the run was triggered with. The delivery of
1896
+ * the message is what executes the step, which is how a step's
1897
+ * settings reach it.
1898
+ *
1899
+ * When any setting is present the feature set is extended with
1900
+ * `WF_StepConfig`, which is what tells QStash to keep them. QStash
1901
+ * reports that back on the delivery as `Upstash-Workflow-Step-Config`.
1902
+ */
1903
+ addStepSettings() {
1904
+ if (!this.stepSettings) {
1905
+ return;
1906
+ }
1907
+ const headers = {};
1908
+ if (this.stepSettings.flowControl) {
1909
+ const { flowControlKey, flowControlValue } = prepareFlowControl(
1910
+ this.stepSettings.flowControl
1911
+ );
1912
+ headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
1913
+ headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
1914
+ }
1915
+ if (this.stepSettings.retries !== void 0) {
1916
+ headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
1917
+ }
1918
+ if (this.stepSettings.retryDelay) {
1919
+ headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
1920
+ }
1921
+ if (Object.keys(headers).length === 0) {
1922
+ return;
1923
+ }
1924
+ headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
1925
+ this.headers.stepSettingsHeaders = headers;
1926
+ }
1833
1927
  addContentType() {
1834
1928
  if (this.workflowConfig.useJSONContent) {
1835
1929
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -1843,14 +1937,17 @@ var WorkflowHeaders = class {
1843
1937
  return contentType;
1844
1938
  }
1845
1939
  prefixHeaders(contentType) {
1846
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
1940
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
1847
1941
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
1848
1942
  return {
1849
1943
  headers: {
1850
1944
  ...rawHeaders,
1851
1945
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
1852
1946
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
1853
- ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
1947
+ ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
1948
+ // last, so they override the run's settings above. Never
1949
+ // prefixed: they configure this message, not its callback.
1950
+ ...stepSettingsHeaders
1854
1951
  },
1855
1952
  contentType
1856
1953
  };
@@ -1901,7 +1998,7 @@ var submitParallelSteps = async ({
1901
1998
  info: `Submitting ${planSteps.length} parallel steps.`
1902
1999
  });
1903
2000
  const result = await context.qstashClient.batch(
1904
- planSteps.map((planStep) => {
2001
+ planSteps.map((planStep, index) => {
1905
2002
  const { headers } = getHeaders({
1906
2003
  initHeaderValue: "false",
1907
2004
  workflowConfig: {
@@ -1909,7 +2006,8 @@ var submitParallelSteps = async ({
1909
2006
  workflowUrl: context.url,
1910
2007
  telemetry: telemetry2
1911
2008
  },
1912
- invokeCount
2009
+ invokeCount,
2010
+ stepSettings: steps[index].stepSettings
1913
2011
  });
1914
2012
  return {
1915
2013
  headers,
@@ -1928,31 +2026,38 @@ var submitParallelSteps = async ({
1928
2026
  }
1929
2027
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
1930
2028
  };
1931
- var submitSingleStep = async ({
1932
- context,
2029
+ var executeStep = async ({
1933
2030
  lazyStep,
1934
2031
  stepId,
1935
- invokeCount,
1936
2032
  concurrency,
1937
- telemetry: telemetry2,
1938
- dispatchDebug,
1939
2033
  dispatchLifecycle
1940
2034
  }) => {
1941
2035
  await dispatchLifecycle("beforeExecution", {
1942
2036
  stepName: lazyStep.stepName
1943
2037
  });
1944
- let resultStep;
1945
2038
  try {
1946
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2039
+ return await lazyStep.getResultStep(concurrency, stepId);
1947
2040
  } catch (error) {
1948
2041
  attachStepNameToError(error, lazyStep.stepName);
1949
2042
  throw error;
1950
2043
  }
2044
+ };
2045
+ var submitStepResult = async ({
2046
+ context,
2047
+ lazyStep,
2048
+ resultStep,
2049
+ invokeCount,
2050
+ concurrency,
2051
+ telemetry: telemetry2,
2052
+ dispatchDebug,
2053
+ nextStepSettings
2054
+ }) => {
1951
2055
  const { headers } = lazyStep.getHeaders({
1952
2056
  context,
1953
2057
  step: resultStep,
1954
2058
  invokeCount,
1955
- telemetry: telemetry2
2059
+ telemetry: telemetry2,
2060
+ stepSettings: nextStepSettings
1956
2061
  });
1957
2062
  const body = lazyStep.getBody({
1958
2063
  context,
@@ -1977,6 +2082,138 @@ var submitSingleStep = async ({
1977
2082
  }
1978
2083
  return resultStep;
1979
2084
  };
2085
+ var publishStepConfigRequest = async ({
2086
+ context,
2087
+ lazyStep,
2088
+ targetStep,
2089
+ invokeCount,
2090
+ telemetry: telemetry2,
2091
+ dispatchDebug
2092
+ }) => {
2093
+ const { headers } = getHeaders({
2094
+ initHeaderValue: "false",
2095
+ workflowConfig: {
2096
+ workflowRunId: context.workflowRunId,
2097
+ workflowUrl: context.url,
2098
+ telemetry: telemetry2
2099
+ },
2100
+ invokeCount,
2101
+ stepSettings: lazyStep.stepSettings
2102
+ });
2103
+ await dispatchDebug("onInfo", {
2104
+ info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
2105
+ });
2106
+ const result = await context.qstashClient.publishJSON({
2107
+ headers: {
2108
+ ...headers,
2109
+ "Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
2110
+ },
2111
+ method: "POST",
2112
+ body: { targetStep, invokeCount },
2113
+ url: context.url,
2114
+ contentBasedDeduplication: true
2115
+ });
2116
+ if (result?.deduplicated) {
2117
+ await dispatchDebug("onWarning", {
2118
+ warning: `A step config request for step "${lazyStep.stepName}" (${targetStep}) was already published and this one was deduplicated. Expected when the delivery which published it is being retried; otherwise the run stops here.`
2119
+ });
2120
+ } else if (result?.messageId) {
2121
+ await dispatchDebug("onInfo", {
2122
+ info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
2123
+ });
2124
+ }
2125
+ };
2126
+
2127
+ // src/qstash/step-config.ts
2128
+ var DEFAULT_PARALLELISM = 0;
2129
+ var DEFAULT_RATE = 0;
2130
+ var DEFAULT_PERIOD_SECONDS = 1;
2131
+ var SECONDS_PER_UNIT = {
2132
+ s: 1,
2133
+ m: 60,
2134
+ h: 60 * 60,
2135
+ d: 24 * 60 * 60
2136
+ };
2137
+ var durationToSeconds = (duration) => {
2138
+ if (typeof duration === "number") {
2139
+ return Number.isFinite(duration) ? duration : void 0;
2140
+ }
2141
+ const match = /^(\d+)([smhd])$/.exec(duration.trim());
2142
+ if (match) {
2143
+ return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
2144
+ }
2145
+ const seconds = Number(duration.trim());
2146
+ return Number.isFinite(seconds) ? seconds : void 0;
2147
+ };
2148
+ var normalizeFlowControl = (flowControl) => {
2149
+ const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
2150
+ return {
2151
+ key: flowControl.key,
2152
+ parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
2153
+ rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
2154
+ period: period ?? DEFAULT_PERIOD_SECONDS
2155
+ };
2156
+ };
2157
+ var parseFlowControlHeaders = (key, value) => {
2158
+ if (!key) {
2159
+ return void 0;
2160
+ }
2161
+ const flowControl = {
2162
+ key,
2163
+ parallelism: DEFAULT_PARALLELISM,
2164
+ rate: DEFAULT_RATE,
2165
+ period: DEFAULT_PERIOD_SECONDS
2166
+ };
2167
+ for (const entry of (value ?? "").split(",")) {
2168
+ const [name, rawValue] = entry.split("=").map((part) => part.trim());
2169
+ if (!name || rawValue === void 0) {
2170
+ continue;
2171
+ }
2172
+ switch (name) {
2173
+ case "parallelism": {
2174
+ flowControl.parallelism = Number(rawValue);
2175
+ break;
2176
+ }
2177
+ case "rate": {
2178
+ flowControl.rate = Number(rawValue);
2179
+ break;
2180
+ }
2181
+ case "period": {
2182
+ flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
2183
+ break;
2184
+ }
2185
+ }
2186
+ }
2187
+ return flowControl;
2188
+ };
2189
+ var getEffectiveConfig = (headers) => {
2190
+ const retriesHeader = headers.get(RETRIES_HEADER);
2191
+ return {
2192
+ flowControl: parseFlowControlHeaders(
2193
+ headers.get(FLOW_CONTROL_KEY_HEADER),
2194
+ headers.get(FLOW_CONTROL_VALUE_HEADER)
2195
+ ),
2196
+ retries: retriesHeader === null ? void 0 : Number(retriesHeader),
2197
+ retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
2198
+ hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
2199
+ };
2200
+ };
2201
+ var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
2202
+ if (stepSettings.flowControl) {
2203
+ const wanted = normalizeFlowControl(stepSettings.flowControl);
2204
+ const applied = effectiveConfig.flowControl;
2205
+ if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
2206
+ return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
2207
+ }
2208
+ }
2209
+ if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
2210
+ return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
2211
+ }
2212
+ if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
2213
+ return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
2214
+ }
2215
+ return void 0;
2216
+ };
1980
2217
 
1981
2218
  // src/context/auto-executor.ts
1982
2219
  var AutoExecutor = class _AutoExecutor {
@@ -1993,6 +2230,25 @@ var AutoExecutor = class _AutoExecutor {
1993
2230
  stepCount = 0;
1994
2231
  planStepCount = 0;
1995
2232
  executingStep = false;
2233
+ /**
2234
+ * an executed step whose result hasn't been submitted to QStash yet.
2235
+ *
2236
+ * Set when a step which can produce its result in-process executes. The
2237
+ * submission waits so that the route function can continue and reveal
2238
+ * what comes next: if that is a single step with step-level settings,
2239
+ * the settings ride on this submission and no step config request is
2240
+ * needed, since the delivery of the submission is what executes it.
2241
+ *
2242
+ * Flushed when the next step is reached (see `addStep`) or when the
2243
+ * route function ends (see `flushPendingStep` in `serve`).
2244
+ */
2245
+ pendingStep;
2246
+ /**
2247
+ * configuration QStash applied to the delivery being handled. A step
2248
+ * with step-level settings is settled by comparing its settings against
2249
+ * this.
2250
+ */
2251
+ effectiveConfig;
1996
2252
  /**
1997
2253
  * @param context workflow context
1998
2254
  * @param steps list of steps
@@ -2000,14 +2256,17 @@ var AutoExecutor = class _AutoExecutor {
2000
2256
  * @param dispatchLifecycle lifecycle event dispatcher
2001
2257
  * @param telemetry optional telemetry information
2002
2258
  * @param invokeCount optional invoke count
2259
+ * @param effectiveConfig configuration QStash applied to the delivery
2260
+ * being handled
2003
2261
  */
2004
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount) {
2262
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount, effectiveConfig) {
2005
2263
  this.context = context;
2006
2264
  this.steps = steps;
2007
2265
  this.dispatchDebug = dispatchDebug;
2008
2266
  this.dispatchLifecycle = dispatchLifecycle;
2009
2267
  this.telemetry = telemetry2;
2010
2268
  this.invokeCount = invokeCount ?? 0;
2269
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2011
2270
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2012
2271
  }
2013
2272
  /**
@@ -2042,6 +2301,15 @@ var AutoExecutor = class _AutoExecutor {
2042
2301
  lazyStepList.push(stepInfo);
2043
2302
  const index = this.indexInCurrentList++;
2044
2303
  const requestComplete = this.deferExecution().then(async () => {
2304
+ const submitted = await this.submitPendingStep(
2305
+ lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
2306
+ );
2307
+ if (submitted.isErr()) {
2308
+ throw submitted.error;
2309
+ }
2310
+ if (submitted.value.result === "submitted-step") {
2311
+ throw submitted.value.abort;
2312
+ }
2045
2313
  if (!this.promises.has(lazyStepList)) {
2046
2314
  const promise2 = this.getExecutionPromise(lazyStepList);
2047
2315
  this.promises.set(lazyStepList, promise2);
@@ -2095,18 +2363,100 @@ var AutoExecutor = class _AutoExecutor {
2095
2363
  }
2096
2364
  return parsedOut;
2097
2365
  }
2098
- const resultStep = await submitSingleStep({
2099
- context: this.context,
2366
+ const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
2367
+ if (mismatch) {
2368
+ if (!this.effectiveConfig.hasStepConfig) {
2369
+ await publishStepConfigRequest({
2370
+ context: this.context,
2371
+ lazyStep,
2372
+ targetStep: this.stepCount,
2373
+ invokeCount: this.invokeCount,
2374
+ telemetry: this.telemetry,
2375
+ dispatchDebug: this.dispatchDebug
2376
+ });
2377
+ throw new WorkflowAbort(lazyStep.stepName);
2378
+ } else {
2379
+ await this.dispatchDebug("onWarning", {
2380
+ warning: `The step-level settings of step '${lazyStep.stepName}' were published but are not recognized on the delivery which carries them (${mismatch}). Executing the step with the settings of the delivery instead. This is a bug in @upstash/workflow, please report it.`
2381
+ });
2382
+ }
2383
+ }
2384
+ const resultStep = await executeStep({
2100
2385
  lazyStep,
2101
2386
  stepId: this.stepCount,
2387
+ concurrency: NO_CONCURRENCY,
2388
+ dispatchLifecycle: this.dispatchLifecycle
2389
+ });
2390
+ if (lazyStep.supportsDeferredSubmission) {
2391
+ this.pendingStep = { status: "held", lazyStep, resultStep };
2392
+ return lazyStep.parseOut({
2393
+ ...resultStep,
2394
+ out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
2395
+ });
2396
+ }
2397
+ await submitStepResult({
2398
+ context: this.context,
2399
+ lazyStep,
2400
+ resultStep,
2102
2401
  invokeCount: this.invokeCount,
2103
- concurrency: 1,
2402
+ concurrency: NO_CONCURRENCY,
2104
2403
  telemetry: this.telemetry,
2105
- dispatchDebug: this.dispatchDebug,
2106
- dispatchLifecycle: this.dispatchLifecycle
2404
+ dispatchDebug: this.dispatchDebug
2107
2405
  });
2108
2406
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2109
2407
  }
2408
+ /**
2409
+ * Submits the result of the step being held, if there is one, and
2410
+ * returns the abort the caller has to throw to end the invocation.
2411
+ *
2412
+ * Returns undefined when no step is being held, which is the caller's
2413
+ * signal that there is nothing to end. Throws instead of returning
2414
+ * when the submission itself fails, so that error reaches the caller
2415
+ * rather than an abort claiming the step was submitted.
2416
+ *
2417
+ * Callers which arrive while a submission is already under way (the
2418
+ * steps of a parallel group, added together) wait for that one and get
2419
+ * the same abort, rather than submitting a second time.
2420
+ *
2421
+ * The next step's settings are attached whenever it has any, without
2422
+ * checking them against the current delivery: the executor only knows
2423
+ * the configuration of the delivery in hand, which inside a
2424
+ * delivery is the *previous* step's, so such a check would be wrong
2425
+ * exactly when two steps with settings follow each other.
2426
+ *
2427
+ * @param nextStepSettings step-level settings of the next step
2428
+ * @returns `submitted-step` with the abort which ends this invocation,
2429
+ * or `no-pending-step` when nothing was held and there is nothing to
2430
+ * end
2431
+ */
2432
+ async submitPendingStep(nextStepSettings) {
2433
+ if (!this.pendingStep) {
2434
+ return ok({ result: "no-pending-step" });
2435
+ }
2436
+ try {
2437
+ if (this.pendingStep.status === "submitting") {
2438
+ return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
2439
+ }
2440
+ const { lazyStep, resultStep } = this.pendingStep;
2441
+ const submitted = (async () => {
2442
+ await submitStepResult({
2443
+ context: this.context,
2444
+ lazyStep,
2445
+ resultStep,
2446
+ invokeCount: this.invokeCount,
2447
+ concurrency: NO_CONCURRENCY,
2448
+ telemetry: this.telemetry,
2449
+ dispatchDebug: this.dispatchDebug,
2450
+ nextStepSettings
2451
+ });
2452
+ return new WorkflowAbort(lazyStep.stepName, resultStep);
2453
+ })();
2454
+ this.pendingStep = { status: "submitting", submitted };
2455
+ return ok({ result: "submitted-step", abort: await submitted });
2456
+ } catch (error) {
2457
+ return err(error);
2458
+ }
2459
+ }
2110
2460
  /**
2111
2461
  * Runs steps in parallel.
2112
2462
  *
@@ -2155,15 +2505,20 @@ var AutoExecutor = class _AutoExecutor {
2155
2505
  validateStep(parallelSteps[stepIndex], planStep);
2156
2506
  try {
2157
2507
  const parallelStep = parallelSteps[stepIndex];
2158
- const resultStep = await submitSingleStep({
2159
- context: this.context,
2508
+ const resultStep = await executeStep({
2160
2509
  lazyStep: parallelStep,
2161
2510
  stepId: planStep.targetStep,
2511
+ concurrency: parallelSteps.length,
2512
+ dispatchLifecycle: this.dispatchLifecycle
2513
+ });
2514
+ await submitStepResult({
2515
+ context: this.context,
2516
+ lazyStep: parallelStep,
2517
+ resultStep,
2162
2518
  invokeCount: this.invokeCount,
2163
2519
  concurrency: parallelSteps.length,
2164
2520
  telemetry: this.telemetry,
2165
- dispatchDebug: this.dispatchDebug,
2166
- dispatchLifecycle: this.dispatchLifecycle
2521
+ dispatchDebug: this.dispatchDebug
2167
2522
  });
2168
2523
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2169
2524
  } catch (error) {
@@ -2779,6 +3134,46 @@ var WorkflowContext = class {
2779
3134
  * `1` on the first retry, `2` on the second, and so on.
2780
3135
  */
2781
3136
  retried;
3137
+ /**
3138
+ * Configuration QStash applied to the request being handled.
3139
+ *
3140
+ * This is the configuration of *this delivery*, not of the run: it is
3141
+ * the configuration the run was triggered with, except inside the
3142
+ * delivery which executes a step that has step-level settings, where
3143
+ * it is that step's settings. The fields below expose it; the executor
3144
+ * gets it directly rather than reaching through the context.
3145
+ */
3146
+ effectiveConfig;
3147
+ /**
3148
+ * Flow control QStash applied to the request being handled, if any.
3149
+ *
3150
+ * This is the flow control of the request in hand, which is the one the
3151
+ * run was triggered with except inside a step carrying its own.
3152
+ */
3153
+ get flowControl() {
3154
+ return this.effectiveConfig.flowControl;
3155
+ }
3156
+ /**
3157
+ * Retry limit QStash applied to the request being handled, if it
3158
+ * reported one.
3159
+ *
3160
+ * Not to be confused with {@link retried}, which is how many retries
3161
+ * have already happened.
3162
+ *
3163
+ * @see {@link flowControl} for what "the request being handled" means
3164
+ */
3165
+ get retries() {
3166
+ return this.effectiveConfig.retries;
3167
+ }
3168
+ /**
3169
+ * Retry delay expression QStash applied to the request being handled,
3170
+ * if any.
3171
+ *
3172
+ * @see {@link flowControl} for what "the request being handled" means
3173
+ */
3174
+ get retryDelay() {
3175
+ return this.effectiveConfig.retryDelay;
3176
+ }
2782
3177
  constructor({
2783
3178
  qstashClient,
2784
3179
  workflowRunId,
@@ -2792,7 +3187,8 @@ var WorkflowContext = class {
2792
3187
  invokeCount,
2793
3188
  label,
2794
3189
  retried,
2795
- middlewareManager
3190
+ middlewareManager,
3191
+ effectiveConfig
2796
3192
  }) {
2797
3193
  this.qstashClient = qstashClient;
2798
3194
  this.workflowRunId = workflowRunId;
@@ -2804,6 +3200,7 @@ var WorkflowContext = class {
2804
3200
  this.env = env ?? {};
2805
3201
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2806
3202
  this.retried = retried ?? 0;
3203
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2807
3204
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2808
3205
  middlewareManagerInstance.assignContext(this);
2809
3206
  this.executor = new AutoExecutor(
@@ -2812,7 +3209,8 @@ var WorkflowContext = class {
2812
3209
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2813
3210
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2814
3211
  telemetry2,
2815
- invokeCount
3212
+ invokeCount,
3213
+ this.effectiveConfig
2816
3214
  );
2817
3215
  }
2818
3216
  /**
@@ -2838,13 +3236,35 @@ var WorkflowContext = class {
2838
3236
  * ])
2839
3237
  * ```
2840
3238
  *
3239
+ * Step-level settings can be passed as a third argument, overriding
3240
+ * the settings the workflow run was triggered with for this step only:
3241
+ *
3242
+ * ```typescript
3243
+ * const result = await context.run(
3244
+ * "step 1",
3245
+ * () => {
3246
+ * return "result"
3247
+ * },
3248
+ * {
3249
+ * flowControl: { key: "custom-key", parallelism: 3 },
3250
+ * retries: 5,
3251
+ * }
3252
+ * )
3253
+ * ```
3254
+ *
2841
3255
  * @param stepName name of the step
2842
3256
  * @param stepFunction step function to be executed
3257
+ * @param stepSettings step-level settings for this step
2843
3258
  * @returns result of the step function
2844
3259
  */
2845
- async run(stepName, stepFunction) {
3260
+ async run(stepName, stepFunction, stepSettings) {
3261
+ if (stepSettings) {
3262
+ validateFlowControl(stepSettings.flowControl);
3263
+ }
2846
3264
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2847
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3265
+ return await this.addStep(
3266
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3267
+ );
2848
3268
  }
2849
3269
  /**
2850
3270
  * Stops the execution for the duration provided.
@@ -3067,7 +3487,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3067
3487
  *
3068
3488
  * @param routeFunction
3069
3489
  */
3070
- static async tryAuthentication(routeFunction, context) {
3490
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3071
3491
  const disabledContext = new _DisabledWorkflowContext({
3072
3492
  qstashClient: new import_qstash10.Client({
3073
3493
  baseUrl: "disabled-client",
@@ -3082,7 +3502,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3082
3502
  initialPayload: context.requestPayload,
3083
3503
  env: context.env,
3084
3504
  label: context.labels,
3085
- retried: context.retried
3505
+ retried: context.retried,
3506
+ // the route function runs for real here until it reaches a step, so
3507
+ // it observes the same configuration as it will on the real context
3508
+ effectiveConfig
3086
3509
  });
3087
3510
  try {
3088
3511
  await routeFunction(disabledContext);
@@ -3299,6 +3722,7 @@ var handleFailure = async ({
3299
3722
  }
3300
3723
  const userHeaders = recreateUserHeaders(request.headers);
3301
3724
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3725
+ const effectiveConfig = getEffectiveConfig(request.headers);
3302
3726
  const workflowContext = new WorkflowContext({
3303
3727
  qstashClient,
3304
3728
  workflowRunId,
@@ -3312,11 +3736,13 @@ var handleFailure = async ({
3312
3736
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3313
3737
  retried,
3314
3738
  workflowRunCreatedAt: workflowCreatedAt,
3315
- middlewareManager: void 0
3739
+ middlewareManager: void 0,
3740
+ effectiveConfig
3316
3741
  });
3317
3742
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3318
3743
  routeFunction,
3319
- workflowContext
3744
+ workflowContext,
3745
+ effectiveConfig
3320
3746
  );
3321
3747
  if (authCheck.isErr()) {
3322
3748
  await dispatchDebug?.("onError", {
@@ -3408,7 +3834,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3408
3834
  const client = new import_qstash11.Client({
3409
3835
  ...clientOptions,
3410
3836
  baseUrl: clientEnv.QSTASH_URL,
3411
- token: clientEnv.QSTASH_TOKEN
3837
+ token: clientEnv.QSTASH_TOKEN,
3838
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3839
+ // the client at the local dev server too. Without this the QStash Client
3840
+ // re-derives dev mode from its own process.env, which is blind to the
3841
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3842
+ // An explicit devMode in the user's client config still wins.
3843
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3412
3844
  });
3413
3845
  const receiver = getReceiver(environment, receiverConfig, region);
3414
3846
  return { client, receiver };
@@ -3458,7 +3890,9 @@ var getQStashHandlers = ({
3458
3890
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3459
3891
  ...qstashClientOption,
3460
3892
  baseUrl: environment.QSTASH_URL,
3461
- token: environment.QSTASH_TOKEN
3893
+ token: environment.QSTASH_TOKEN,
3894
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
3895
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3462
3896
  }),
3463
3897
  receiver: getReceiver(environment, receiverConfig)
3464
3898
  }
@@ -3798,12 +4232,14 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3798
4232
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3799
4233
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3800
4234
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4235
+ const effectiveConfig = getEffectiveConfig(request.headers);
3801
4236
  const workflowContext = new WorkflowContext({
3802
4237
  qstashClient: regionalClient,
3803
4238
  workflowRunId,
3804
4239
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
3805
4240
  headers: recreateUserHeaders(request.headers),
3806
4241
  steps,
4242
+ effectiveConfig,
3807
4243
  url: workflowUrl,
3808
4244
  env,
3809
4245
  telemetry: telemetry2,
@@ -3815,7 +4251,8 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3815
4251
  });
3816
4252
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3817
4253
  routeFunction,
3818
- workflowContext
4254
+ workflowContext,
4255
+ effectiveConfig
3819
4256
  );
3820
4257
  if (authCheck.isErr()) {
3821
4258
  throw authCheck.error;
@@ -3852,7 +4289,23 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3852
4289
  if (steps.length === 1) {
3853
4290
  await middlewareManager.dispatchLifecycle("runStarted", {});
3854
4291
  }
3855
- return await routeFunction(workflowContext);
4292
+ let outcome;
4293
+ try {
4294
+ outcome = { ran: true, result: await routeFunction(workflowContext) };
4295
+ } catch (error) {
4296
+ outcome = { ran: false, error };
4297
+ }
4298
+ const submitted = await flushPendingStep(workflowContext);
4299
+ if (submitted.isErr()) {
4300
+ throw submitted.error;
4301
+ }
4302
+ if (submitted.value.result === "submitted-step") {
4303
+ throw submitted.value.abort;
4304
+ }
4305
+ if (!outcome.ran) {
4306
+ throw outcome.error;
4307
+ }
4308
+ return outcome.result;
3856
4309
  },
3857
4310
  onCleanup: async (result2) => {
3858
4311
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -3933,11 +4386,13 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3933
4386
  };
3934
4387
 
3935
4388
  // platforms/svelte.ts
4389
+ var import_qstash13 = require("@upstash/qstash");
3936
4390
  var telemetry = {
3937
4391
  sdk: SDK_TELEMETRY,
3938
4392
  framework: "svelte"
3939
4393
  };
3940
4394
  var serve = (routeFunction, options) => {
4395
+ void (0, import_qstash13.startDevServer)();
3941
4396
  const handler = async ({ request }) => {
3942
4397
  const { handler: serveHandler } = serveBase(
3943
4398
  routeFunction,