@upstash/workflow 1.3.3 → 1.4.0-rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/astro.d.mts +3 -2
  2. package/astro.d.ts +3 -2
  3. package/astro.js +587 -139
  4. package/astro.mjs +3 -1
  5. package/{chunk-WEQGCISK.mjs → chunk-UH6O5IJY.mjs} +749 -301
  6. package/cloudflare.d.mts +3 -2
  7. package/cloudflare.d.ts +3 -2
  8. package/cloudflare.js +587 -139
  9. package/cloudflare.mjs +3 -1
  10. package/express.d.mts +3 -2
  11. package/express.d.ts +3 -2
  12. package/express.js +588 -139
  13. package/express.mjs +4 -1
  14. package/h3.d.mts +3 -2
  15. package/h3.d.ts +3 -2
  16. package/h3.js +587 -139
  17. package/h3.mjs +3 -1
  18. package/hono.d.mts +3 -2
  19. package/hono.d.ts +3 -2
  20. package/hono.js +587 -139
  21. package/hono.mjs +3 -1
  22. package/index.d.mts +25 -6
  23. package/index.d.ts +25 -6
  24. package/index.js +610 -141
  25. package/index.mjs +26 -3
  26. package/nextjs.d.mts +3 -2
  27. package/nextjs.d.ts +3 -2
  28. package/nextjs.js +588 -139
  29. package/nextjs.mjs +4 -1
  30. package/package.json +1 -1
  31. package/react-router.d.mts +3 -2
  32. package/react-router.d.ts +3 -2
  33. package/react-router.js +587 -139
  34. package/react-router.mjs +3 -1
  35. package/{serve-many-iJF1IUXk.d.ts → serve-many-BCtZg31b.d.ts} +1 -1
  36. package/{serve-many-CG3BFvO3.d.mts → serve-many-CbIV7145.d.mts} +1 -1
  37. package/solidjs.d.mts +2 -1
  38. package/solidjs.d.ts +2 -1
  39. package/solidjs.js +587 -139
  40. package/solidjs.mjs +3 -1
  41. package/svelte.d.mts +3 -2
  42. package/svelte.d.ts +3 -2
  43. package/svelte.js +587 -139
  44. package/svelte.mjs +3 -1
  45. package/tanstack.d.mts +3 -2
  46. package/tanstack.d.ts +3 -2
  47. package/tanstack.js +587 -139
  48. package/tanstack.mjs +3 -1
  49. package/{types-CekOpKvz.d.ts → types-ffLPVG5_.d.mts} +248 -8
  50. package/{types-CekOpKvz.d.mts → types-ffLPVG5_.d.ts} +248 -8
package/h3.js CHANGED
@@ -335,6 +335,9 @@ async function _callHandler(event, handler, hooks) {
335
335
  var H3Headers = globalThis.Headers;
336
336
  var H3Response = globalThis.Response;
337
337
 
338
+ // src/serve/index.ts
339
+ var import_qstash12 = require("@upstash/qstash");
340
+
338
341
  // src/client/utils.ts
339
342
  var import_qstash2 = require("@upstash/qstash");
340
343
 
@@ -523,6 +526,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
523
526
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
524
527
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
525
528
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
529
+ var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
530
+ var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
531
+ var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
532
+ var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
533
+ var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
534
+ var RETRIES_HEADER = "Upstash-Retries";
535
+ var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
536
+ var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
526
537
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
527
538
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
528
539
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -540,102 +551,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
540
551
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
541
552
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
542
553
 
543
- // src/context/auto-executor.ts
544
- var import_qstash5 = require("@upstash/qstash");
545
-
546
- // src/qstash/headers.ts
547
- var import_qstash4 = require("@upstash/qstash");
548
-
549
- // src/utils.ts
550
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
551
- var NANOID_LENGTH = 21;
552
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
553
- function validateLabel(label) {
554
- if (label === void 0) return;
555
- const labels = Array.isArray(label) ? label : [label];
556
- if (labels.length === 0) {
557
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
558
- }
559
- for (const value of labels) {
560
- if (!RESOURCE_NAME_PATTERN.test(value)) {
561
- throw new WorkflowNonRetryableError(
562
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
563
- );
564
- }
565
- }
566
- }
567
- function serializeLabel(label) {
568
- return Array.isArray(label) ? label.join(",") : label;
569
- }
570
- function validateFlowControl(flowControl) {
571
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
572
- throw new WorkflowNonRetryableError(
573
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
574
- );
575
- }
576
- }
577
- function getRandomInt() {
578
- return Math.floor(Math.random() * NANOID_CHARS.length);
579
- }
580
- function nanoid(length = NANOID_LENGTH) {
581
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
582
- }
583
- function getWorkflowRunId(id) {
584
- return `wfr_${id ?? nanoid()}`;
585
- }
586
- function decodeBase64(base64) {
587
- const binString = atob(base64);
588
- try {
589
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
590
- return new TextDecoder().decode(intArray);
591
- } catch (error) {
592
- console.warn(
593
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
594
- );
595
- return binString;
596
- }
597
- }
598
- function getUserIdFromToken(qstashClient) {
599
- try {
600
- const token = qstashClient.token;
601
- const decodedToken = decodeBase64(token);
602
- const tokenPayload = JSON.parse(decodedToken);
603
- const userId = tokenPayload.UserID;
604
- if (!userId) {
605
- throw new WorkflowError("QStash token payload does not contain userId");
606
- }
607
- return userId;
608
- } catch (error) {
609
- throw new WorkflowError(
610
- `Failed to decode QStash token while running create webhook step: ${error.message}`
611
- );
612
- }
613
- }
614
- function getQStashUrl(qstashClient) {
615
- try {
616
- const requester = qstashClient.http;
617
- const baseUrl = requester.baseUrl;
618
- if (!baseUrl) {
619
- throw new WorkflowError("QStash client does not have a baseUrl");
620
- }
621
- return baseUrl;
622
- } catch (error) {
623
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
624
- }
625
- }
626
- function getEventId() {
627
- return `evt_${nanoid(15)}`;
628
- }
629
- function stringifyBody(body) {
630
- if (body === void 0) {
631
- return void 0;
632
- }
633
- if (typeof body === "string") {
634
- return body;
635
- }
636
- return JSON.stringify(body);
637
- }
638
-
639
554
  // node_modules/neverthrow/dist/index.es.js
640
555
  var defaultErrorConfig = {
641
556
  withStackTrace: false
@@ -1051,6 +966,102 @@ var Err = class {
1051
966
  };
1052
967
  var fromThrowable = Result.fromThrowable;
1053
968
 
969
+ // src/context/auto-executor.ts
970
+ var import_qstash5 = require("@upstash/qstash");
971
+
972
+ // src/qstash/headers.ts
973
+ var import_qstash4 = require("@upstash/qstash");
974
+
975
+ // src/utils.ts
976
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
977
+ var NANOID_LENGTH = 21;
978
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
979
+ function validateLabel(label) {
980
+ if (label === void 0) return;
981
+ const labels = Array.isArray(label) ? label : [label];
982
+ if (labels.length === 0) {
983
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
984
+ }
985
+ for (const value of labels) {
986
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
987
+ throw new WorkflowNonRetryableError(
988
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
989
+ );
990
+ }
991
+ }
992
+ }
993
+ function serializeLabel(label) {
994
+ return Array.isArray(label) ? label.join(",") : label;
995
+ }
996
+ function validateFlowControl(flowControl) {
997
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
998
+ throw new WorkflowNonRetryableError(
999
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
1000
+ );
1001
+ }
1002
+ }
1003
+ function getRandomInt() {
1004
+ return Math.floor(Math.random() * NANOID_CHARS.length);
1005
+ }
1006
+ function nanoid(length = NANOID_LENGTH) {
1007
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
1008
+ }
1009
+ function getWorkflowRunId(id) {
1010
+ return `wfr_${id ?? nanoid()}`;
1011
+ }
1012
+ function decodeBase64(base64) {
1013
+ const binString = atob(base64);
1014
+ try {
1015
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
1016
+ return new TextDecoder().decode(intArray);
1017
+ } catch (error) {
1018
+ console.warn(
1019
+ `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
1020
+ );
1021
+ return binString;
1022
+ }
1023
+ }
1024
+ function getUserIdFromToken(qstashClient) {
1025
+ try {
1026
+ const token = qstashClient.token;
1027
+ const decodedToken = decodeBase64(token);
1028
+ const tokenPayload = JSON.parse(decodedToken);
1029
+ const userId = tokenPayload.UserID;
1030
+ if (!userId) {
1031
+ throw new WorkflowError("QStash token payload does not contain userId");
1032
+ }
1033
+ return userId;
1034
+ } catch (error) {
1035
+ throw new WorkflowError(
1036
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
1037
+ );
1038
+ }
1039
+ }
1040
+ function getQStashUrl(qstashClient) {
1041
+ try {
1042
+ const requester = qstashClient.http;
1043
+ const baseUrl = requester.baseUrl;
1044
+ if (!baseUrl) {
1045
+ throw new WorkflowError("QStash client does not have a baseUrl");
1046
+ }
1047
+ return baseUrl;
1048
+ } catch (error) {
1049
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
1050
+ }
1051
+ }
1052
+ function getEventId() {
1053
+ return `evt_${nanoid(15)}`;
1054
+ }
1055
+ function stringifyBody(body) {
1056
+ if (body === void 0) {
1057
+ return void 0;
1058
+ }
1059
+ if (typeof body === "string") {
1060
+ return body;
1061
+ }
1062
+ return JSON.stringify(body);
1063
+ }
1064
+
1054
1065
  // src/types.ts
1055
1066
  var StepTypes = [
1056
1067
  "Initial",
@@ -1156,6 +1167,10 @@ var triggerFirstInvocation = async (params) => {
1156
1167
  return err(error_);
1157
1168
  }
1158
1169
  };
1170
+ var flushPendingStep = async (workflowContext) => {
1171
+ const { executor } = workflowContext;
1172
+ return await executor.submitPendingStep();
1173
+ };
1159
1174
  var triggerRouteFunction = async ({
1160
1175
  onCleanup,
1161
1176
  onStep,
@@ -1363,8 +1378,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1363
1378
  var BaseLazyStep = class _BaseLazyStep {
1364
1379
  stepName;
1365
1380
  context;
1366
- constructor(context, stepName) {
1381
+ /**
1382
+ * step-level settings (flow control, retries etc.) which override the
1383
+ * settings the workflow run was triggered with, for this step only.
1384
+ */
1385
+ stepSettings;
1386
+ /**
1387
+ * whether this step's result can be submitted after the route function
1388
+ * has moved on, instead of right away.
1389
+ *
1390
+ * Enabled for steps whose `getResultStep` produces the final `out` —
1391
+ * which is not the same as "needs no server round trip":
1392
+ * `LazyCreateWebhookStep` builds its result in `getBody` and returns
1393
+ * `out: undefined` from `getResultStep`, so deferring it would hand
1394
+ * `undefined` to the route function.
1395
+ *
1396
+ * Deferring lets the route function continue and reveal the next step,
1397
+ * so that step's settings can ride on this step's submission instead
1398
+ * of needing a step config request of their own.
1399
+ */
1400
+ supportsDeferredSubmission = false;
1401
+ constructor(context, stepName, stepSettings) {
1367
1402
  this.context = context;
1403
+ this.stepSettings = stepSettings;
1368
1404
  if (!stepName) {
1369
1405
  throw new WorkflowError(
1370
1406
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1428,7 +1464,13 @@ var BaseLazyStep = class _BaseLazyStep {
1428
1464
  step.out = JSON.stringify(step.out);
1429
1465
  return JSON.stringify(step);
1430
1466
  }
1431
- getHeaders({ context, telemetry: telemetry2, invokeCount, step }) {
1467
+ getHeaders({
1468
+ context,
1469
+ telemetry: telemetry2,
1470
+ invokeCount,
1471
+ step,
1472
+ stepSettings
1473
+ }) {
1432
1474
  return getHeaders({
1433
1475
  initHeaderValue: "false",
1434
1476
  workflowConfig: {
@@ -1441,7 +1483,8 @@ var BaseLazyStep = class _BaseLazyStep {
1441
1483
  stepInfo: {
1442
1484
  step,
1443
1485
  lazyStep: this
1444
- }
1486
+ },
1487
+ stepSettings
1445
1488
  });
1446
1489
  }
1447
1490
  async submitStep({ context, body, headers }) {
@@ -1459,8 +1502,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1459
1502
  stepFunction;
1460
1503
  stepType = "Run";
1461
1504
  allowUndefinedOut = true;
1462
- constructor(context, stepName, stepFunction) {
1463
- super(context, stepName);
1505
+ supportsDeferredSubmission = true;
1506
+ constructor(context, stepName, stepFunction, stepSettings) {
1507
+ super(context, stepName, stepSettings);
1464
1508
  this.stepFunction = stepFunction;
1465
1509
  }
1466
1510
  getPlanStep(concurrent, targetStep) {
@@ -1490,6 +1534,7 @@ var LazySleepStep = class extends BaseLazyStep {
1490
1534
  sleep;
1491
1535
  stepType = "SleepFor";
1492
1536
  allowUndefinedOut = true;
1537
+ supportsDeferredSubmission = true;
1493
1538
  constructor(context, stepName, sleep) {
1494
1539
  super(context, stepName);
1495
1540
  this.sleep = sleep;
@@ -1529,6 +1574,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1529
1574
  sleepUntil;
1530
1575
  stepType = "SleepUntil";
1531
1576
  allowUndefinedOut = true;
1577
+ supportsDeferredSubmission = true;
1532
1578
  constructor(context, stepName, sleepUntil) {
1533
1579
  super(context, stepName);
1534
1580
  this.sleepUntil = sleepUntil;
@@ -2021,6 +2067,7 @@ var WorkflowHeaders = class {
2021
2067
  invokeCount;
2022
2068
  initHeaderValue;
2023
2069
  stepInfo;
2070
+ stepSettings;
2024
2071
  headers;
2025
2072
  /**
2026
2073
  * @param params workflow header parameters
@@ -2030,17 +2077,20 @@ var WorkflowHeaders = class {
2030
2077
  workflowConfig,
2031
2078
  invokeCount,
2032
2079
  initHeaderValue,
2033
- stepInfo
2080
+ stepInfo,
2081
+ stepSettings
2034
2082
  }) {
2035
2083
  this.userHeaders = userHeaders;
2036
2084
  this.workflowConfig = workflowConfig;
2037
2085
  this.invokeCount = invokeCount;
2038
2086
  this.initHeaderValue = initHeaderValue;
2039
2087
  this.stepInfo = stepInfo;
2088
+ this.stepSettings = stepSettings;
2040
2089
  this.headers = {
2041
2090
  rawHeaders: {},
2042
2091
  workflowHeaders: {},
2043
- failureHeaders: {}
2092
+ failureHeaders: {},
2093
+ stepSettingsHeaders: {}
2044
2094
  };
2045
2095
  }
2046
2096
  getHeaders() {
@@ -2051,6 +2101,7 @@ var WorkflowHeaders = class {
2051
2101
  this.addUserHeaders();
2052
2102
  this.addInvokeCount();
2053
2103
  this.addFailureUrl();
2104
+ this.addStepSettings();
2054
2105
  const contentType = this.addContentType();
2055
2106
  return this.prefixHeaders(contentType);
2056
2107
  }
@@ -2060,7 +2111,7 @@ var WorkflowHeaders = class {
2060
2111
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
2061
2112
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
2062
2113
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
2063
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
2114
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
2064
2115
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
2065
2116
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
2066
2117
  };
@@ -2138,7 +2189,7 @@ var WorkflowHeaders = class {
2138
2189
  this.headers.failureHeaders["Workflow-Init"] = "false";
2139
2190
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
2140
2191
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
2141
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
2192
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
2142
2193
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
2143
2194
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
2144
2195
  }
@@ -2146,6 +2197,42 @@ var WorkflowHeaders = class {
2146
2197
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
2147
2198
  }
2148
2199
  }
2200
+ /**
2201
+ * Applies the step-level settings of a step to the message.
2202
+ *
2203
+ * These headers configure the message itself, so that QStash uses them
2204
+ * instead of the settings the run was triggered with. The delivery of
2205
+ * the message is what executes the step, which is how a step's
2206
+ * settings reach it.
2207
+ *
2208
+ * When any setting is present the feature set is extended with
2209
+ * `WF_StepConfig`, which is what tells QStash to keep them. QStash
2210
+ * reports that back on the delivery as `Upstash-Workflow-Step-Config`.
2211
+ */
2212
+ addStepSettings() {
2213
+ if (!this.stepSettings) {
2214
+ return;
2215
+ }
2216
+ const headers = {};
2217
+ if (this.stepSettings.flowControl) {
2218
+ const { flowControlKey, flowControlValue } = prepareFlowControl(
2219
+ this.stepSettings.flowControl
2220
+ );
2221
+ headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
2222
+ headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
2223
+ }
2224
+ if (this.stepSettings.retries !== void 0) {
2225
+ headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
2226
+ }
2227
+ if (this.stepSettings.retryDelay) {
2228
+ headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
2229
+ }
2230
+ if (Object.keys(headers).length === 0) {
2231
+ return;
2232
+ }
2233
+ headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
2234
+ this.headers.stepSettingsHeaders = headers;
2235
+ }
2149
2236
  addContentType() {
2150
2237
  if (this.workflowConfig.useJSONContent) {
2151
2238
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -2159,14 +2246,17 @@ var WorkflowHeaders = class {
2159
2246
  return contentType;
2160
2247
  }
2161
2248
  prefixHeaders(contentType) {
2162
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
2249
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
2163
2250
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
2164
2251
  return {
2165
2252
  headers: {
2166
2253
  ...rawHeaders,
2167
2254
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
2168
2255
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
2169
- ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
2256
+ ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
2257
+ // last, so they override the run's settings above. Never
2258
+ // prefixed: they configure this message, not its callback.
2259
+ ...stepSettingsHeaders
2170
2260
  },
2171
2261
  contentType
2172
2262
  };
@@ -2217,7 +2307,7 @@ var submitParallelSteps = async ({
2217
2307
  info: `Submitting ${planSteps.length} parallel steps.`
2218
2308
  });
2219
2309
  const result = await context.qstashClient.batch(
2220
- planSteps.map((planStep) => {
2310
+ planSteps.map((planStep, index) => {
2221
2311
  const { headers } = getHeaders({
2222
2312
  initHeaderValue: "false",
2223
2313
  workflowConfig: {
@@ -2225,7 +2315,8 @@ var submitParallelSteps = async ({
2225
2315
  workflowUrl: context.url,
2226
2316
  telemetry: telemetry2
2227
2317
  },
2228
- invokeCount
2318
+ invokeCount,
2319
+ stepSettings: steps[index].stepSettings
2229
2320
  });
2230
2321
  return {
2231
2322
  headers,
@@ -2244,31 +2335,38 @@ var submitParallelSteps = async ({
2244
2335
  }
2245
2336
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
2246
2337
  };
2247
- var submitSingleStep = async ({
2248
- context,
2338
+ var executeStep = async ({
2249
2339
  lazyStep,
2250
2340
  stepId,
2251
- invokeCount,
2252
2341
  concurrency,
2253
- telemetry: telemetry2,
2254
- dispatchDebug,
2255
2342
  dispatchLifecycle
2256
2343
  }) => {
2257
2344
  await dispatchLifecycle("beforeExecution", {
2258
2345
  stepName: lazyStep.stepName
2259
2346
  });
2260
- let resultStep;
2261
2347
  try {
2262
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2348
+ return await lazyStep.getResultStep(concurrency, stepId);
2263
2349
  } catch (error) {
2264
2350
  attachStepNameToError(error, lazyStep.stepName);
2265
2351
  throw error;
2266
2352
  }
2353
+ };
2354
+ var submitStepResult = async ({
2355
+ context,
2356
+ lazyStep,
2357
+ resultStep,
2358
+ invokeCount,
2359
+ concurrency,
2360
+ telemetry: telemetry2,
2361
+ dispatchDebug,
2362
+ nextStepSettings
2363
+ }) => {
2267
2364
  const { headers } = lazyStep.getHeaders({
2268
2365
  context,
2269
2366
  step: resultStep,
2270
2367
  invokeCount,
2271
- telemetry: telemetry2
2368
+ telemetry: telemetry2,
2369
+ stepSettings: nextStepSettings
2272
2370
  });
2273
2371
  const body = lazyStep.getBody({
2274
2372
  context,
@@ -2293,6 +2391,138 @@ var submitSingleStep = async ({
2293
2391
  }
2294
2392
  return resultStep;
2295
2393
  };
2394
+ var publishStepConfigRequest = async ({
2395
+ context,
2396
+ lazyStep,
2397
+ targetStep,
2398
+ invokeCount,
2399
+ telemetry: telemetry2,
2400
+ dispatchDebug
2401
+ }) => {
2402
+ const { headers } = getHeaders({
2403
+ initHeaderValue: "false",
2404
+ workflowConfig: {
2405
+ workflowRunId: context.workflowRunId,
2406
+ workflowUrl: context.url,
2407
+ telemetry: telemetry2
2408
+ },
2409
+ invokeCount,
2410
+ stepSettings: lazyStep.stepSettings
2411
+ });
2412
+ await dispatchDebug("onInfo", {
2413
+ info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
2414
+ });
2415
+ const result = await context.qstashClient.publishJSON({
2416
+ headers: {
2417
+ ...headers,
2418
+ "Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
2419
+ },
2420
+ method: "POST",
2421
+ body: { targetStep, invokeCount },
2422
+ url: context.url,
2423
+ contentBasedDeduplication: true
2424
+ });
2425
+ if (result?.deduplicated) {
2426
+ await dispatchDebug("onWarning", {
2427
+ 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.`
2428
+ });
2429
+ } else if (result?.messageId) {
2430
+ await dispatchDebug("onInfo", {
2431
+ info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
2432
+ });
2433
+ }
2434
+ };
2435
+
2436
+ // src/qstash/step-config.ts
2437
+ var DEFAULT_PARALLELISM = 0;
2438
+ var DEFAULT_RATE = 0;
2439
+ var DEFAULT_PERIOD_SECONDS = 1;
2440
+ var SECONDS_PER_UNIT = {
2441
+ s: 1,
2442
+ m: 60,
2443
+ h: 60 * 60,
2444
+ d: 24 * 60 * 60
2445
+ };
2446
+ var durationToSeconds = (duration) => {
2447
+ if (typeof duration === "number") {
2448
+ return Number.isFinite(duration) ? duration : void 0;
2449
+ }
2450
+ const match = /^(\d+)([smhd])$/.exec(duration.trim());
2451
+ if (match) {
2452
+ return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
2453
+ }
2454
+ const seconds = Number(duration.trim());
2455
+ return Number.isFinite(seconds) ? seconds : void 0;
2456
+ };
2457
+ var normalizeFlowControl = (flowControl) => {
2458
+ const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
2459
+ return {
2460
+ key: flowControl.key,
2461
+ parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
2462
+ rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
2463
+ period: period ?? DEFAULT_PERIOD_SECONDS
2464
+ };
2465
+ };
2466
+ var parseFlowControlHeaders = (key, value) => {
2467
+ if (!key) {
2468
+ return void 0;
2469
+ }
2470
+ const flowControl = {
2471
+ key,
2472
+ parallelism: DEFAULT_PARALLELISM,
2473
+ rate: DEFAULT_RATE,
2474
+ period: DEFAULT_PERIOD_SECONDS
2475
+ };
2476
+ for (const entry of (value ?? "").split(",")) {
2477
+ const [name, rawValue] = entry.split("=").map((part) => part.trim());
2478
+ if (!name || rawValue === void 0) {
2479
+ continue;
2480
+ }
2481
+ switch (name) {
2482
+ case "parallelism": {
2483
+ flowControl.parallelism = Number(rawValue);
2484
+ break;
2485
+ }
2486
+ case "rate": {
2487
+ flowControl.rate = Number(rawValue);
2488
+ break;
2489
+ }
2490
+ case "period": {
2491
+ flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
2492
+ break;
2493
+ }
2494
+ }
2495
+ }
2496
+ return flowControl;
2497
+ };
2498
+ var getEffectiveConfig = (headers) => {
2499
+ const retriesHeader = headers.get(RETRIES_HEADER);
2500
+ return {
2501
+ flowControl: parseFlowControlHeaders(
2502
+ headers.get(FLOW_CONTROL_KEY_HEADER),
2503
+ headers.get(FLOW_CONTROL_VALUE_HEADER)
2504
+ ),
2505
+ retries: retriesHeader === null ? void 0 : Number(retriesHeader),
2506
+ retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
2507
+ hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
2508
+ };
2509
+ };
2510
+ var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
2511
+ if (stepSettings.flowControl) {
2512
+ const wanted = normalizeFlowControl(stepSettings.flowControl);
2513
+ const applied = effectiveConfig.flowControl;
2514
+ if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
2515
+ return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
2516
+ }
2517
+ }
2518
+ if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
2519
+ return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
2520
+ }
2521
+ if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
2522
+ return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
2523
+ }
2524
+ return void 0;
2525
+ };
2296
2526
 
2297
2527
  // src/context/auto-executor.ts
2298
2528
  var AutoExecutor = class _AutoExecutor {
@@ -2309,6 +2539,25 @@ var AutoExecutor = class _AutoExecutor {
2309
2539
  stepCount = 0;
2310
2540
  planStepCount = 0;
2311
2541
  executingStep = false;
2542
+ /**
2543
+ * an executed step whose result hasn't been submitted to QStash yet.
2544
+ *
2545
+ * Set when a step which can produce its result in-process executes. The
2546
+ * submission waits so that the route function can continue and reveal
2547
+ * what comes next: if that is a single step with step-level settings,
2548
+ * the settings ride on this submission and no step config request is
2549
+ * needed, since the delivery of the submission is what executes it.
2550
+ *
2551
+ * Flushed when the next step is reached (see `addStep`) or when the
2552
+ * route function ends (see `flushPendingStep` in `serve`).
2553
+ */
2554
+ pendingStep;
2555
+ /**
2556
+ * configuration QStash applied to the delivery being handled. A step
2557
+ * with step-level settings is settled by comparing its settings against
2558
+ * this.
2559
+ */
2560
+ effectiveConfig;
2312
2561
  /**
2313
2562
  * @param context workflow context
2314
2563
  * @param steps list of steps
@@ -2316,14 +2565,17 @@ var AutoExecutor = class _AutoExecutor {
2316
2565
  * @param dispatchLifecycle lifecycle event dispatcher
2317
2566
  * @param telemetry optional telemetry information
2318
2567
  * @param invokeCount optional invoke count
2568
+ * @param effectiveConfig configuration QStash applied to the delivery
2569
+ * being handled
2319
2570
  */
2320
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount) {
2571
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount, effectiveConfig) {
2321
2572
  this.context = context;
2322
2573
  this.steps = steps;
2323
2574
  this.dispatchDebug = dispatchDebug;
2324
2575
  this.dispatchLifecycle = dispatchLifecycle;
2325
2576
  this.telemetry = telemetry2;
2326
2577
  this.invokeCount = invokeCount ?? 0;
2578
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2327
2579
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2328
2580
  }
2329
2581
  /**
@@ -2358,6 +2610,15 @@ var AutoExecutor = class _AutoExecutor {
2358
2610
  lazyStepList.push(stepInfo);
2359
2611
  const index = this.indexInCurrentList++;
2360
2612
  const requestComplete = this.deferExecution().then(async () => {
2613
+ const submitted = await this.submitPendingStep(
2614
+ lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
2615
+ );
2616
+ if (submitted.isErr()) {
2617
+ throw submitted.error;
2618
+ }
2619
+ if (submitted.value.result === "submitted-step") {
2620
+ throw submitted.value.abort;
2621
+ }
2361
2622
  if (!this.promises.has(lazyStepList)) {
2362
2623
  const promise2 = this.getExecutionPromise(lazyStepList);
2363
2624
  this.promises.set(lazyStepList, promise2);
@@ -2411,18 +2672,100 @@ var AutoExecutor = class _AutoExecutor {
2411
2672
  }
2412
2673
  return parsedOut;
2413
2674
  }
2414
- const resultStep = await submitSingleStep({
2415
- context: this.context,
2675
+ const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
2676
+ if (mismatch) {
2677
+ if (!this.effectiveConfig.hasStepConfig) {
2678
+ await publishStepConfigRequest({
2679
+ context: this.context,
2680
+ lazyStep,
2681
+ targetStep: this.stepCount,
2682
+ invokeCount: this.invokeCount,
2683
+ telemetry: this.telemetry,
2684
+ dispatchDebug: this.dispatchDebug
2685
+ });
2686
+ throw new WorkflowAbort(lazyStep.stepName);
2687
+ } else {
2688
+ await this.dispatchDebug("onWarning", {
2689
+ 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.`
2690
+ });
2691
+ }
2692
+ }
2693
+ const resultStep = await executeStep({
2416
2694
  lazyStep,
2417
2695
  stepId: this.stepCount,
2696
+ concurrency: NO_CONCURRENCY,
2697
+ dispatchLifecycle: this.dispatchLifecycle
2698
+ });
2699
+ if (lazyStep.supportsDeferredSubmission) {
2700
+ this.pendingStep = { status: "held", lazyStep, resultStep };
2701
+ return lazyStep.parseOut({
2702
+ ...resultStep,
2703
+ out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
2704
+ });
2705
+ }
2706
+ await submitStepResult({
2707
+ context: this.context,
2708
+ lazyStep,
2709
+ resultStep,
2418
2710
  invokeCount: this.invokeCount,
2419
- concurrency: 1,
2711
+ concurrency: NO_CONCURRENCY,
2420
2712
  telemetry: this.telemetry,
2421
- dispatchDebug: this.dispatchDebug,
2422
- dispatchLifecycle: this.dispatchLifecycle
2713
+ dispatchDebug: this.dispatchDebug
2423
2714
  });
2424
2715
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2425
2716
  }
2717
+ /**
2718
+ * Submits the result of the step being held, if there is one, and
2719
+ * returns the abort the caller has to throw to end the invocation.
2720
+ *
2721
+ * Returns undefined when no step is being held, which is the caller's
2722
+ * signal that there is nothing to end. Throws instead of returning
2723
+ * when the submission itself fails, so that error reaches the caller
2724
+ * rather than an abort claiming the step was submitted.
2725
+ *
2726
+ * Callers which arrive while a submission is already under way (the
2727
+ * steps of a parallel group, added together) wait for that one and get
2728
+ * the same abort, rather than submitting a second time.
2729
+ *
2730
+ * The next step's settings are attached whenever it has any, without
2731
+ * checking them against the current delivery: the executor only knows
2732
+ * the configuration of the delivery in hand, which inside a
2733
+ * delivery is the *previous* step's, so such a check would be wrong
2734
+ * exactly when two steps with settings follow each other.
2735
+ *
2736
+ * @param nextStepSettings step-level settings of the next step
2737
+ * @returns `submitted-step` with the abort which ends this invocation,
2738
+ * or `no-pending-step` when nothing was held and there is nothing to
2739
+ * end
2740
+ */
2741
+ async submitPendingStep(nextStepSettings) {
2742
+ if (!this.pendingStep) {
2743
+ return ok({ result: "no-pending-step" });
2744
+ }
2745
+ try {
2746
+ if (this.pendingStep.status === "submitting") {
2747
+ return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
2748
+ }
2749
+ const { lazyStep, resultStep } = this.pendingStep;
2750
+ const submitted = (async () => {
2751
+ await submitStepResult({
2752
+ context: this.context,
2753
+ lazyStep,
2754
+ resultStep,
2755
+ invokeCount: this.invokeCount,
2756
+ concurrency: NO_CONCURRENCY,
2757
+ telemetry: this.telemetry,
2758
+ dispatchDebug: this.dispatchDebug,
2759
+ nextStepSettings
2760
+ });
2761
+ return new WorkflowAbort(lazyStep.stepName, resultStep);
2762
+ })();
2763
+ this.pendingStep = { status: "submitting", submitted };
2764
+ return ok({ result: "submitted-step", abort: await submitted });
2765
+ } catch (error) {
2766
+ return err(error);
2767
+ }
2768
+ }
2426
2769
  /**
2427
2770
  * Runs steps in parallel.
2428
2771
  *
@@ -2471,15 +2814,20 @@ var AutoExecutor = class _AutoExecutor {
2471
2814
  validateStep(parallelSteps[stepIndex], planStep);
2472
2815
  try {
2473
2816
  const parallelStep = parallelSteps[stepIndex];
2474
- const resultStep = await submitSingleStep({
2475
- context: this.context,
2817
+ const resultStep = await executeStep({
2476
2818
  lazyStep: parallelStep,
2477
2819
  stepId: planStep.targetStep,
2820
+ concurrency: parallelSteps.length,
2821
+ dispatchLifecycle: this.dispatchLifecycle
2822
+ });
2823
+ await submitStepResult({
2824
+ context: this.context,
2825
+ lazyStep: parallelStep,
2826
+ resultStep,
2478
2827
  invokeCount: this.invokeCount,
2479
2828
  concurrency: parallelSteps.length,
2480
2829
  telemetry: this.telemetry,
2481
- dispatchDebug: this.dispatchDebug,
2482
- dispatchLifecycle: this.dispatchLifecycle
2830
+ dispatchDebug: this.dispatchDebug
2483
2831
  });
2484
2832
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2485
2833
  } catch (error) {
@@ -3095,6 +3443,46 @@ var WorkflowContext = class {
3095
3443
  * `1` on the first retry, `2` on the second, and so on.
3096
3444
  */
3097
3445
  retried;
3446
+ /**
3447
+ * Configuration QStash applied to the request being handled.
3448
+ *
3449
+ * This is the configuration of *this delivery*, not of the run: it is
3450
+ * the configuration the run was triggered with, except inside the
3451
+ * delivery which executes a step that has step-level settings, where
3452
+ * it is that step's settings. The fields below expose it; the executor
3453
+ * gets it directly rather than reaching through the context.
3454
+ */
3455
+ effectiveConfig;
3456
+ /**
3457
+ * Flow control QStash applied to the request being handled, if any.
3458
+ *
3459
+ * This is the flow control of the request in hand, which is the one the
3460
+ * run was triggered with except inside a step carrying its own.
3461
+ */
3462
+ get flowControl() {
3463
+ return this.effectiveConfig.flowControl;
3464
+ }
3465
+ /**
3466
+ * Retry limit QStash applied to the request being handled, if it
3467
+ * reported one.
3468
+ *
3469
+ * Not to be confused with {@link retried}, which is how many retries
3470
+ * have already happened.
3471
+ *
3472
+ * @see {@link flowControl} for what "the request being handled" means
3473
+ */
3474
+ get retries() {
3475
+ return this.effectiveConfig.retries;
3476
+ }
3477
+ /**
3478
+ * Retry delay expression QStash applied to the request being handled,
3479
+ * if any.
3480
+ *
3481
+ * @see {@link flowControl} for what "the request being handled" means
3482
+ */
3483
+ get retryDelay() {
3484
+ return this.effectiveConfig.retryDelay;
3485
+ }
3098
3486
  constructor({
3099
3487
  qstashClient,
3100
3488
  workflowRunId,
@@ -3108,7 +3496,8 @@ var WorkflowContext = class {
3108
3496
  invokeCount,
3109
3497
  label,
3110
3498
  retried,
3111
- middlewareManager
3499
+ middlewareManager,
3500
+ effectiveConfig
3112
3501
  }) {
3113
3502
  this.qstashClient = qstashClient;
3114
3503
  this.workflowRunId = workflowRunId;
@@ -3120,6 +3509,7 @@ var WorkflowContext = class {
3120
3509
  this.env = env ?? {};
3121
3510
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
3122
3511
  this.retried = retried ?? 0;
3512
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
3123
3513
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
3124
3514
  middlewareManagerInstance.assignContext(this);
3125
3515
  this.executor = new AutoExecutor(
@@ -3128,7 +3518,8 @@ var WorkflowContext = class {
3128
3518
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
3129
3519
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
3130
3520
  telemetry2,
3131
- invokeCount
3521
+ invokeCount,
3522
+ this.effectiveConfig
3132
3523
  );
3133
3524
  }
3134
3525
  /**
@@ -3154,13 +3545,35 @@ var WorkflowContext = class {
3154
3545
  * ])
3155
3546
  * ```
3156
3547
  *
3548
+ * Step-level settings can be passed as a third argument, overriding
3549
+ * the settings the workflow run was triggered with for this step only:
3550
+ *
3551
+ * ```typescript
3552
+ * const result = await context.run(
3553
+ * "step 1",
3554
+ * () => {
3555
+ * return "result"
3556
+ * },
3557
+ * {
3558
+ * flowControl: { key: "custom-key", parallelism: 3 },
3559
+ * retries: 5,
3560
+ * }
3561
+ * )
3562
+ * ```
3563
+ *
3157
3564
  * @param stepName name of the step
3158
3565
  * @param stepFunction step function to be executed
3566
+ * @param stepSettings step-level settings for this step
3159
3567
  * @returns result of the step function
3160
3568
  */
3161
- async run(stepName, stepFunction) {
3569
+ async run(stepName, stepFunction, stepSettings) {
3570
+ if (stepSettings) {
3571
+ validateFlowControl(stepSettings.flowControl);
3572
+ }
3162
3573
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
3163
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3574
+ return await this.addStep(
3575
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3576
+ );
3164
3577
  }
3165
3578
  /**
3166
3579
  * Stops the execution for the duration provided.
@@ -3383,7 +3796,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3383
3796
  *
3384
3797
  * @param routeFunction
3385
3798
  */
3386
- static async tryAuthentication(routeFunction, context) {
3799
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3387
3800
  const disabledContext = new _DisabledWorkflowContext({
3388
3801
  qstashClient: new import_qstash10.Client({
3389
3802
  baseUrl: "disabled-client",
@@ -3398,7 +3811,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3398
3811
  initialPayload: context.requestPayload,
3399
3812
  env: context.env,
3400
3813
  label: context.labels,
3401
- retried: context.retried
3814
+ retried: context.retried,
3815
+ // the route function runs for real here until it reaches a step, so
3816
+ // it observes the same configuration as it will on the real context
3817
+ effectiveConfig
3402
3818
  });
3403
3819
  try {
3404
3820
  await routeFunction(disabledContext);
@@ -3615,6 +4031,7 @@ var handleFailure = async ({
3615
4031
  }
3616
4032
  const userHeaders = recreateUserHeaders(request.headers);
3617
4033
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
4034
+ const effectiveConfig = getEffectiveConfig(request.headers);
3618
4035
  const workflowContext = new WorkflowContext({
3619
4036
  qstashClient,
3620
4037
  workflowRunId,
@@ -3628,11 +4045,13 @@ var handleFailure = async ({
3628
4045
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3629
4046
  retried,
3630
4047
  workflowRunCreatedAt: workflowCreatedAt,
3631
- middlewareManager: void 0
4048
+ middlewareManager: void 0,
4049
+ effectiveConfig
3632
4050
  });
3633
4051
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3634
4052
  routeFunction,
3635
- workflowContext
4053
+ workflowContext,
4054
+ effectiveConfig
3636
4055
  );
3637
4056
  if (authCheck.isErr()) {
3638
4057
  await dispatchDebug?.("onError", {
@@ -3724,7 +4143,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3724
4143
  const client = new import_qstash11.Client({
3725
4144
  ...clientOptions,
3726
4145
  baseUrl: clientEnv.QSTASH_URL,
3727
- token: clientEnv.QSTASH_TOKEN
4146
+ token: clientEnv.QSTASH_TOKEN,
4147
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
4148
+ // the client at the local dev server too. Without this the QStash Client
4149
+ // re-derives dev mode from its own process.env, which is blind to the
4150
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
4151
+ // An explicit devMode in the user's client config still wins.
4152
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3728
4153
  });
3729
4154
  const receiver = getReceiver(environment, receiverConfig, region);
3730
4155
  return { client, receiver };
@@ -3774,7 +4199,9 @@ var getQStashHandlers = ({
3774
4199
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3775
4200
  ...qstashClientOption,
3776
4201
  baseUrl: environment.QSTASH_URL,
3777
- token: environment.QSTASH_TOKEN
4202
+ token: environment.QSTASH_TOKEN,
4203
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
4204
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3778
4205
  }),
3779
4206
  receiver: getReceiver(environment, receiverConfig)
3780
4207
  }
@@ -4114,12 +4541,14 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4114
4541
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
4115
4542
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
4116
4543
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4544
+ const effectiveConfig = getEffectiveConfig(request.headers);
4117
4545
  const workflowContext = new WorkflowContext({
4118
4546
  qstashClient: regionalClient,
4119
4547
  workflowRunId,
4120
4548
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
4121
4549
  headers: recreateUserHeaders(request.headers),
4122
4550
  steps,
4551
+ effectiveConfig,
4123
4552
  url: workflowUrl,
4124
4553
  env,
4125
4554
  telemetry: telemetry2,
@@ -4131,7 +4560,8 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4131
4560
  });
4132
4561
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
4133
4562
  routeFunction,
4134
- workflowContext
4563
+ workflowContext,
4564
+ effectiveConfig
4135
4565
  );
4136
4566
  if (authCheck.isErr()) {
4137
4567
  throw authCheck.error;
@@ -4168,7 +4598,23 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4168
4598
  if (steps.length === 1) {
4169
4599
  await middlewareManager.dispatchLifecycle("runStarted", {});
4170
4600
  }
4171
- return await routeFunction(workflowContext);
4601
+ let outcome;
4602
+ try {
4603
+ outcome = { ran: true, result: await routeFunction(workflowContext) };
4604
+ } catch (error) {
4605
+ outcome = { ran: false, error };
4606
+ }
4607
+ const submitted = await flushPendingStep(workflowContext);
4608
+ if (submitted.isErr()) {
4609
+ throw submitted.error;
4610
+ }
4611
+ if (submitted.value.result === "submitted-step") {
4612
+ throw submitted.value.abort;
4613
+ }
4614
+ if (!outcome.ran) {
4615
+ throw outcome.error;
4616
+ }
4617
+ return outcome.result;
4172
4618
  },
4173
4619
  onCleanup: async (result2) => {
4174
4620
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -4249,6 +4695,7 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4249
4695
  };
4250
4696
 
4251
4697
  // platforms/h3.ts
4698
+ var import_qstash13 = require("@upstash/qstash");
4252
4699
  function transformHeaders(headers) {
4253
4700
  const formattedHeaders = Object.entries(headers).map(([key, value]) => [
4254
4701
  key,
@@ -4269,6 +4716,7 @@ var telemetry = {
4269
4716
  runtime: process.versions.bun ? `bun@${process.versions.bun}/node@${process.version}` : `node@${process.version}`
4270
4717
  };
4271
4718
  var serve = (routeFunction, options) => {
4719
+ void (0, import_qstash13.startDevServer)();
4272
4720
  const handler = defineEventHandler(async (event) => {
4273
4721
  const method = event.node.req.method;
4274
4722
  if (method?.toUpperCase() !== "POST") {