@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/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
 
@@ -451,8 +454,15 @@ function isInstanceOf(v, ctor) {
451
454
  }
452
455
 
453
456
  // src/client/utils.ts
457
+ var assertNonEmptyId = (id, label = "id") => {
458
+ if (!id) {
459
+ throw new import_qstash2.QstashError(`${label} cannot be empty`);
460
+ }
461
+ };
454
462
  var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
455
- const path = workflowRunId ? ["v2", "notify", workflowRunId, eventId] : ["v2", "notify", eventId];
463
+ assertNonEmptyId(eventId, "Event id");
464
+ if (workflowRunId !== void 0) assertNonEmptyId(workflowRunId, "Workflow run id");
465
+ const path = workflowRunId === void 0 ? ["v2", "notify", eventId] : ["v2", "notify", workflowRunId, eventId];
456
466
  const result = await requester.request({
457
467
  path,
458
468
  method: "POST",
@@ -516,6 +526,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
516
526
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
517
527
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
518
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";
519
537
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
520
538
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
521
539
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -533,102 +551,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
533
551
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
534
552
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
535
553
 
536
- // src/context/auto-executor.ts
537
- var import_qstash5 = require("@upstash/qstash");
538
-
539
- // src/qstash/headers.ts
540
- var import_qstash4 = require("@upstash/qstash");
541
-
542
- // src/utils.ts
543
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
544
- var NANOID_LENGTH = 21;
545
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
546
- function validateLabel(label) {
547
- if (label === void 0) return;
548
- const labels = Array.isArray(label) ? label : [label];
549
- if (labels.length === 0) {
550
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
551
- }
552
- for (const value of labels) {
553
- if (!RESOURCE_NAME_PATTERN.test(value)) {
554
- throw new WorkflowNonRetryableError(
555
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
556
- );
557
- }
558
- }
559
- }
560
- function serializeLabel(label) {
561
- return Array.isArray(label) ? label.join(",") : label;
562
- }
563
- function validateFlowControl(flowControl) {
564
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
565
- throw new WorkflowNonRetryableError(
566
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
567
- );
568
- }
569
- }
570
- function getRandomInt() {
571
- return Math.floor(Math.random() * NANOID_CHARS.length);
572
- }
573
- function nanoid(length = NANOID_LENGTH) {
574
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
575
- }
576
- function getWorkflowRunId(id) {
577
- return `wfr_${id ?? nanoid()}`;
578
- }
579
- function decodeBase64(base64) {
580
- const binString = atob(base64);
581
- try {
582
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
583
- return new TextDecoder().decode(intArray);
584
- } catch (error) {
585
- console.warn(
586
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
587
- );
588
- return binString;
589
- }
590
- }
591
- function getUserIdFromToken(qstashClient) {
592
- try {
593
- const token = qstashClient.token;
594
- const decodedToken = decodeBase64(token);
595
- const tokenPayload = JSON.parse(decodedToken);
596
- const userId = tokenPayload.UserID;
597
- if (!userId) {
598
- throw new WorkflowError("QStash token payload does not contain userId");
599
- }
600
- return userId;
601
- } catch (error) {
602
- throw new WorkflowError(
603
- `Failed to decode QStash token while running create webhook step: ${error.message}`
604
- );
605
- }
606
- }
607
- function getQStashUrl(qstashClient) {
608
- try {
609
- const requester = qstashClient.http;
610
- const baseUrl = requester.baseUrl;
611
- if (!baseUrl) {
612
- throw new WorkflowError("QStash client does not have a baseUrl");
613
- }
614
- return baseUrl;
615
- } catch (error) {
616
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
617
- }
618
- }
619
- function getEventId() {
620
- return `evt_${nanoid(15)}`;
621
- }
622
- function stringifyBody(body) {
623
- if (body === void 0) {
624
- return void 0;
625
- }
626
- if (typeof body === "string") {
627
- return body;
628
- }
629
- return JSON.stringify(body);
630
- }
631
-
632
554
  // node_modules/neverthrow/dist/index.es.js
633
555
  var defaultErrorConfig = {
634
556
  withStackTrace: false
@@ -1044,6 +966,102 @@ var Err = class {
1044
966
  };
1045
967
  var fromThrowable = Result.fromThrowable;
1046
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
+
1047
1065
  // src/types.ts
1048
1066
  var StepTypes = [
1049
1067
  "Initial",
@@ -1149,6 +1167,10 @@ var triggerFirstInvocation = async (params) => {
1149
1167
  return err(error_);
1150
1168
  }
1151
1169
  };
1170
+ var flushPendingStep = async (workflowContext) => {
1171
+ const { executor } = workflowContext;
1172
+ return await executor.submitPendingStep();
1173
+ };
1152
1174
  var triggerRouteFunction = async ({
1153
1175
  onCleanup,
1154
1176
  onStep,
@@ -1356,8 +1378,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1356
1378
  var BaseLazyStep = class _BaseLazyStep {
1357
1379
  stepName;
1358
1380
  context;
1359
- 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) {
1360
1402
  this.context = context;
1403
+ this.stepSettings = stepSettings;
1361
1404
  if (!stepName) {
1362
1405
  throw new WorkflowError(
1363
1406
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1421,7 +1464,13 @@ var BaseLazyStep = class _BaseLazyStep {
1421
1464
  step.out = JSON.stringify(step.out);
1422
1465
  return JSON.stringify(step);
1423
1466
  }
1424
- getHeaders({ context, telemetry: telemetry2, invokeCount, step }) {
1467
+ getHeaders({
1468
+ context,
1469
+ telemetry: telemetry2,
1470
+ invokeCount,
1471
+ step,
1472
+ stepSettings
1473
+ }) {
1425
1474
  return getHeaders({
1426
1475
  initHeaderValue: "false",
1427
1476
  workflowConfig: {
@@ -1434,7 +1483,8 @@ var BaseLazyStep = class _BaseLazyStep {
1434
1483
  stepInfo: {
1435
1484
  step,
1436
1485
  lazyStep: this
1437
- }
1486
+ },
1487
+ stepSettings
1438
1488
  });
1439
1489
  }
1440
1490
  async submitStep({ context, body, headers }) {
@@ -1452,8 +1502,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1452
1502
  stepFunction;
1453
1503
  stepType = "Run";
1454
1504
  allowUndefinedOut = true;
1455
- constructor(context, stepName, stepFunction) {
1456
- super(context, stepName);
1505
+ supportsDeferredSubmission = true;
1506
+ constructor(context, stepName, stepFunction, stepSettings) {
1507
+ super(context, stepName, stepSettings);
1457
1508
  this.stepFunction = stepFunction;
1458
1509
  }
1459
1510
  getPlanStep(concurrent, targetStep) {
@@ -1483,6 +1534,7 @@ var LazySleepStep = class extends BaseLazyStep {
1483
1534
  sleep;
1484
1535
  stepType = "SleepFor";
1485
1536
  allowUndefinedOut = true;
1537
+ supportsDeferredSubmission = true;
1486
1538
  constructor(context, stepName, sleep) {
1487
1539
  super(context, stepName);
1488
1540
  this.sleep = sleep;
@@ -1522,6 +1574,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1522
1574
  sleepUntil;
1523
1575
  stepType = "SleepUntil";
1524
1576
  allowUndefinedOut = true;
1577
+ supportsDeferredSubmission = true;
1525
1578
  constructor(context, stepName, sleepUntil) {
1526
1579
  super(context, stepName);
1527
1580
  this.sleepUntil = sleepUntil;
@@ -2014,6 +2067,7 @@ var WorkflowHeaders = class {
2014
2067
  invokeCount;
2015
2068
  initHeaderValue;
2016
2069
  stepInfo;
2070
+ stepSettings;
2017
2071
  headers;
2018
2072
  /**
2019
2073
  * @param params workflow header parameters
@@ -2023,17 +2077,20 @@ var WorkflowHeaders = class {
2023
2077
  workflowConfig,
2024
2078
  invokeCount,
2025
2079
  initHeaderValue,
2026
- stepInfo
2080
+ stepInfo,
2081
+ stepSettings
2027
2082
  }) {
2028
2083
  this.userHeaders = userHeaders;
2029
2084
  this.workflowConfig = workflowConfig;
2030
2085
  this.invokeCount = invokeCount;
2031
2086
  this.initHeaderValue = initHeaderValue;
2032
2087
  this.stepInfo = stepInfo;
2088
+ this.stepSettings = stepSettings;
2033
2089
  this.headers = {
2034
2090
  rawHeaders: {},
2035
2091
  workflowHeaders: {},
2036
- failureHeaders: {}
2092
+ failureHeaders: {},
2093
+ stepSettingsHeaders: {}
2037
2094
  };
2038
2095
  }
2039
2096
  getHeaders() {
@@ -2044,6 +2101,7 @@ var WorkflowHeaders = class {
2044
2101
  this.addUserHeaders();
2045
2102
  this.addInvokeCount();
2046
2103
  this.addFailureUrl();
2104
+ this.addStepSettings();
2047
2105
  const contentType = this.addContentType();
2048
2106
  return this.prefixHeaders(contentType);
2049
2107
  }
@@ -2053,7 +2111,7 @@ var WorkflowHeaders = class {
2053
2111
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
2054
2112
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
2055
2113
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
2056
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
2114
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
2057
2115
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
2058
2116
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
2059
2117
  };
@@ -2131,7 +2189,7 @@ var WorkflowHeaders = class {
2131
2189
  this.headers.failureHeaders["Workflow-Init"] = "false";
2132
2190
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
2133
2191
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
2134
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
2192
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
2135
2193
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
2136
2194
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
2137
2195
  }
@@ -2139,6 +2197,42 @@ var WorkflowHeaders = class {
2139
2197
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
2140
2198
  }
2141
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
+ }
2142
2236
  addContentType() {
2143
2237
  if (this.workflowConfig.useJSONContent) {
2144
2238
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -2152,14 +2246,17 @@ var WorkflowHeaders = class {
2152
2246
  return contentType;
2153
2247
  }
2154
2248
  prefixHeaders(contentType) {
2155
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
2249
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
2156
2250
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
2157
2251
  return {
2158
2252
  headers: {
2159
2253
  ...rawHeaders,
2160
2254
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
2161
2255
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
2162
- ...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
2163
2260
  },
2164
2261
  contentType
2165
2262
  };
@@ -2210,7 +2307,7 @@ var submitParallelSteps = async ({
2210
2307
  info: `Submitting ${planSteps.length} parallel steps.`
2211
2308
  });
2212
2309
  const result = await context.qstashClient.batch(
2213
- planSteps.map((planStep) => {
2310
+ planSteps.map((planStep, index) => {
2214
2311
  const { headers } = getHeaders({
2215
2312
  initHeaderValue: "false",
2216
2313
  workflowConfig: {
@@ -2218,7 +2315,8 @@ var submitParallelSteps = async ({
2218
2315
  workflowUrl: context.url,
2219
2316
  telemetry: telemetry2
2220
2317
  },
2221
- invokeCount
2318
+ invokeCount,
2319
+ stepSettings: steps[index].stepSettings
2222
2320
  });
2223
2321
  return {
2224
2322
  headers,
@@ -2237,31 +2335,38 @@ var submitParallelSteps = async ({
2237
2335
  }
2238
2336
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
2239
2337
  };
2240
- var submitSingleStep = async ({
2241
- context,
2338
+ var executeStep = async ({
2242
2339
  lazyStep,
2243
2340
  stepId,
2244
- invokeCount,
2245
2341
  concurrency,
2246
- telemetry: telemetry2,
2247
- dispatchDebug,
2248
2342
  dispatchLifecycle
2249
2343
  }) => {
2250
2344
  await dispatchLifecycle("beforeExecution", {
2251
2345
  stepName: lazyStep.stepName
2252
2346
  });
2253
- let resultStep;
2254
2347
  try {
2255
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2348
+ return await lazyStep.getResultStep(concurrency, stepId);
2256
2349
  } catch (error) {
2257
2350
  attachStepNameToError(error, lazyStep.stepName);
2258
2351
  throw error;
2259
2352
  }
2353
+ };
2354
+ var submitStepResult = async ({
2355
+ context,
2356
+ lazyStep,
2357
+ resultStep,
2358
+ invokeCount,
2359
+ concurrency,
2360
+ telemetry: telemetry2,
2361
+ dispatchDebug,
2362
+ nextStepSettings
2363
+ }) => {
2260
2364
  const { headers } = lazyStep.getHeaders({
2261
2365
  context,
2262
2366
  step: resultStep,
2263
2367
  invokeCount,
2264
- telemetry: telemetry2
2368
+ telemetry: telemetry2,
2369
+ stepSettings: nextStepSettings
2265
2370
  });
2266
2371
  const body = lazyStep.getBody({
2267
2372
  context,
@@ -2286,6 +2391,138 @@ var submitSingleStep = async ({
2286
2391
  }
2287
2392
  return resultStep;
2288
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
+ };
2289
2526
 
2290
2527
  // src/context/auto-executor.ts
2291
2528
  var AutoExecutor = class _AutoExecutor {
@@ -2302,6 +2539,25 @@ var AutoExecutor = class _AutoExecutor {
2302
2539
  stepCount = 0;
2303
2540
  planStepCount = 0;
2304
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;
2305
2561
  /**
2306
2562
  * @param context workflow context
2307
2563
  * @param steps list of steps
@@ -2309,14 +2565,17 @@ var AutoExecutor = class _AutoExecutor {
2309
2565
  * @param dispatchLifecycle lifecycle event dispatcher
2310
2566
  * @param telemetry optional telemetry information
2311
2567
  * @param invokeCount optional invoke count
2568
+ * @param effectiveConfig configuration QStash applied to the delivery
2569
+ * being handled
2312
2570
  */
2313
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount) {
2571
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount, effectiveConfig) {
2314
2572
  this.context = context;
2315
2573
  this.steps = steps;
2316
2574
  this.dispatchDebug = dispatchDebug;
2317
2575
  this.dispatchLifecycle = dispatchLifecycle;
2318
2576
  this.telemetry = telemetry2;
2319
2577
  this.invokeCount = invokeCount ?? 0;
2578
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2320
2579
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2321
2580
  }
2322
2581
  /**
@@ -2351,6 +2610,15 @@ var AutoExecutor = class _AutoExecutor {
2351
2610
  lazyStepList.push(stepInfo);
2352
2611
  const index = this.indexInCurrentList++;
2353
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
+ }
2354
2622
  if (!this.promises.has(lazyStepList)) {
2355
2623
  const promise2 = this.getExecutionPromise(lazyStepList);
2356
2624
  this.promises.set(lazyStepList, promise2);
@@ -2404,18 +2672,100 @@ var AutoExecutor = class _AutoExecutor {
2404
2672
  }
2405
2673
  return parsedOut;
2406
2674
  }
2407
- const resultStep = await submitSingleStep({
2408
- 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({
2409
2694
  lazyStep,
2410
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,
2411
2710
  invokeCount: this.invokeCount,
2412
- concurrency: 1,
2711
+ concurrency: NO_CONCURRENCY,
2413
2712
  telemetry: this.telemetry,
2414
- dispatchDebug: this.dispatchDebug,
2415
- dispatchLifecycle: this.dispatchLifecycle
2713
+ dispatchDebug: this.dispatchDebug
2416
2714
  });
2417
2715
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2418
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
+ }
2419
2769
  /**
2420
2770
  * Runs steps in parallel.
2421
2771
  *
@@ -2464,15 +2814,20 @@ var AutoExecutor = class _AutoExecutor {
2464
2814
  validateStep(parallelSteps[stepIndex], planStep);
2465
2815
  try {
2466
2816
  const parallelStep = parallelSteps[stepIndex];
2467
- const resultStep = await submitSingleStep({
2468
- context: this.context,
2817
+ const resultStep = await executeStep({
2469
2818
  lazyStep: parallelStep,
2470
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,
2471
2827
  invokeCount: this.invokeCount,
2472
2828
  concurrency: parallelSteps.length,
2473
2829
  telemetry: this.telemetry,
2474
- dispatchDebug: this.dispatchDebug,
2475
- dispatchLifecycle: this.dispatchLifecycle
2830
+ dispatchDebug: this.dispatchDebug
2476
2831
  });
2477
2832
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2478
2833
  } catch (error) {
@@ -3088,6 +3443,46 @@ var WorkflowContext = class {
3088
3443
  * `1` on the first retry, `2` on the second, and so on.
3089
3444
  */
3090
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
+ }
3091
3486
  constructor({
3092
3487
  qstashClient,
3093
3488
  workflowRunId,
@@ -3101,7 +3496,8 @@ var WorkflowContext = class {
3101
3496
  invokeCount,
3102
3497
  label,
3103
3498
  retried,
3104
- middlewareManager
3499
+ middlewareManager,
3500
+ effectiveConfig
3105
3501
  }) {
3106
3502
  this.qstashClient = qstashClient;
3107
3503
  this.workflowRunId = workflowRunId;
@@ -3113,6 +3509,7 @@ var WorkflowContext = class {
3113
3509
  this.env = env ?? {};
3114
3510
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
3115
3511
  this.retried = retried ?? 0;
3512
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
3116
3513
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
3117
3514
  middlewareManagerInstance.assignContext(this);
3118
3515
  this.executor = new AutoExecutor(
@@ -3121,7 +3518,8 @@ var WorkflowContext = class {
3121
3518
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
3122
3519
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
3123
3520
  telemetry2,
3124
- invokeCount
3521
+ invokeCount,
3522
+ this.effectiveConfig
3125
3523
  );
3126
3524
  }
3127
3525
  /**
@@ -3147,13 +3545,35 @@ var WorkflowContext = class {
3147
3545
  * ])
3148
3546
  * ```
3149
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
+ *
3150
3564
  * @param stepName name of the step
3151
3565
  * @param stepFunction step function to be executed
3566
+ * @param stepSettings step-level settings for this step
3152
3567
  * @returns result of the step function
3153
3568
  */
3154
- async run(stepName, stepFunction) {
3569
+ async run(stepName, stepFunction, stepSettings) {
3570
+ if (stepSettings) {
3571
+ validateFlowControl(stepSettings.flowControl);
3572
+ }
3155
3573
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
3156
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3574
+ return await this.addStep(
3575
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3576
+ );
3157
3577
  }
3158
3578
  /**
3159
3579
  * Stops the execution for the duration provided.
@@ -3376,7 +3796,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3376
3796
  *
3377
3797
  * @param routeFunction
3378
3798
  */
3379
- static async tryAuthentication(routeFunction, context) {
3799
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3380
3800
  const disabledContext = new _DisabledWorkflowContext({
3381
3801
  qstashClient: new import_qstash10.Client({
3382
3802
  baseUrl: "disabled-client",
@@ -3391,7 +3811,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3391
3811
  initialPayload: context.requestPayload,
3392
3812
  env: context.env,
3393
3813
  label: context.labels,
3394
- 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
3395
3818
  });
3396
3819
  try {
3397
3820
  await routeFunction(disabledContext);
@@ -3608,6 +4031,7 @@ var handleFailure = async ({
3608
4031
  }
3609
4032
  const userHeaders = recreateUserHeaders(request.headers);
3610
4033
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
4034
+ const effectiveConfig = getEffectiveConfig(request.headers);
3611
4035
  const workflowContext = new WorkflowContext({
3612
4036
  qstashClient,
3613
4037
  workflowRunId,
@@ -3621,11 +4045,13 @@ var handleFailure = async ({
3621
4045
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3622
4046
  retried,
3623
4047
  workflowRunCreatedAt: workflowCreatedAt,
3624
- middlewareManager: void 0
4048
+ middlewareManager: void 0,
4049
+ effectiveConfig
3625
4050
  });
3626
4051
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3627
4052
  routeFunction,
3628
- workflowContext
4053
+ workflowContext,
4054
+ effectiveConfig
3629
4055
  );
3630
4056
  if (authCheck.isErr()) {
3631
4057
  await dispatchDebug?.("onError", {
@@ -3717,7 +4143,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3717
4143
  const client = new import_qstash11.Client({
3718
4144
  ...clientOptions,
3719
4145
  baseUrl: clientEnv.QSTASH_URL,
3720
- 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)
3721
4153
  });
3722
4154
  const receiver = getReceiver(environment, receiverConfig, region);
3723
4155
  return { client, receiver };
@@ -3767,7 +4199,9 @@ var getQStashHandlers = ({
3767
4199
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3768
4200
  ...qstashClientOption,
3769
4201
  baseUrl: environment.QSTASH_URL,
3770
- 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)
3771
4205
  }),
3772
4206
  receiver: getReceiver(environment, receiverConfig)
3773
4207
  }
@@ -4107,12 +4541,14 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4107
4541
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
4108
4542
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
4109
4543
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4544
+ const effectiveConfig = getEffectiveConfig(request.headers);
4110
4545
  const workflowContext = new WorkflowContext({
4111
4546
  qstashClient: regionalClient,
4112
4547
  workflowRunId,
4113
4548
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
4114
4549
  headers: recreateUserHeaders(request.headers),
4115
4550
  steps,
4551
+ effectiveConfig,
4116
4552
  url: workflowUrl,
4117
4553
  env,
4118
4554
  telemetry: telemetry2,
@@ -4124,7 +4560,8 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4124
4560
  });
4125
4561
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
4126
4562
  routeFunction,
4127
- workflowContext
4563
+ workflowContext,
4564
+ effectiveConfig
4128
4565
  );
4129
4566
  if (authCheck.isErr()) {
4130
4567
  throw authCheck.error;
@@ -4161,7 +4598,23 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4161
4598
  if (steps.length === 1) {
4162
4599
  await middlewareManager.dispatchLifecycle("runStarted", {});
4163
4600
  }
4164
- 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;
4165
4618
  },
4166
4619
  onCleanup: async (result2) => {
4167
4620
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -4242,6 +4695,7 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4242
4695
  };
4243
4696
 
4244
4697
  // platforms/h3.ts
4698
+ var import_qstash13 = require("@upstash/qstash");
4245
4699
  function transformHeaders(headers) {
4246
4700
  const formattedHeaders = Object.entries(headers).map(([key, value]) => [
4247
4701
  key,
@@ -4262,6 +4716,7 @@ var telemetry = {
4262
4716
  runtime: process.versions.bun ? `bun@${process.versions.bun}/node@${process.version}` : `node@${process.version}`
4263
4717
  };
4264
4718
  var serve = (routeFunction, options) => {
4719
+ void (0, import_qstash13.startDevServer)();
4265
4720
  const handler = defineEventHandler(async (event) => {
4266
4721
  const method = event.node.req.method;
4267
4722
  if (method?.toUpperCase() !== "POST") {