@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/index.js CHANGED
@@ -33,6 +33,9 @@ __export(src_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(src_exports);
35
35
 
36
+ // src/serve/index.ts
37
+ var import_qstash12 = require("@upstash/qstash");
38
+
36
39
  // src/client/utils.ts
37
40
  var import_qstash2 = require("@upstash/qstash");
38
41
 
@@ -149,8 +152,20 @@ function isInstanceOf(v, ctor) {
149
152
  }
150
153
 
151
154
  // src/client/utils.ts
155
+ var assertNonEmptyId = (id, label = "id") => {
156
+ if (!id) {
157
+ throw new import_qstash2.QstashError(`${label} cannot be empty`);
158
+ }
159
+ };
160
+ var toNonEmptyIdArray = (request, label = "id") => {
161
+ const ids = typeof request === "string" ? [request] : request;
162
+ for (const id of ids) assertNonEmptyId(id, label);
163
+ return ids;
164
+ };
152
165
  var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
153
- const path = workflowRunId ? ["v2", "notify", workflowRunId, eventId] : ["v2", "notify", eventId];
166
+ assertNonEmptyId(eventId, "Event id");
167
+ if (workflowRunId !== void 0) assertNonEmptyId(workflowRunId, "Workflow run id");
168
+ const path = workflowRunId === void 0 ? ["v2", "notify", eventId] : ["v2", "notify", workflowRunId, eventId];
154
169
  const result = await requester.request({
155
170
  path,
156
171
  method: "POST",
@@ -159,6 +174,7 @@ var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) =>
159
174
  return result;
160
175
  };
161
176
  var makeGetWaitersRequest = async (requester, eventId) => {
177
+ assertNonEmptyId(eventId, "Event id");
162
178
  const result = await requester.request({
163
179
  path: ["v2", "waiters", eventId],
164
180
  method: "GET"
@@ -245,6 +261,13 @@ function buildBulkActionQueryParameters(request, options) {
245
261
  "No filter provided. Use { filter: { ... } } with at least one filter field, or { all: true }."
246
262
  );
247
263
  }
264
+ for (const [field, value] of Object.entries(filter)) {
265
+ if (Array.isArray(value) && value.length === 0) {
266
+ throw new import_qstash2.QstashError(
267
+ `Empty array provided for filter field '${field}'. If you intend to target all records, use { all: true } explicitly.`
268
+ );
269
+ }
270
+ }
248
271
  if (options?.translateWorkflowUrl) {
249
272
  const { workflowUrlStartingWith, workflowUrl, ...rest } = filter;
250
273
  if (workflowUrlStartingWith && workflowUrl) {
@@ -281,6 +304,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
281
304
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
282
305
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
283
306
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
307
+ var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
308
+ var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
309
+ var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
310
+ var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
311
+ var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
312
+ var RETRIES_HEADER = "Upstash-Retries";
313
+ var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
314
+ var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
284
315
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
285
316
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
286
317
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -298,102 +329,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
298
329
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
299
330
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
300
331
 
301
- // src/context/auto-executor.ts
302
- var import_qstash5 = require("@upstash/qstash");
303
-
304
- // src/qstash/headers.ts
305
- var import_qstash4 = require("@upstash/qstash");
306
-
307
- // src/utils.ts
308
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
309
- var NANOID_LENGTH = 21;
310
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
311
- function validateLabel(label) {
312
- if (label === void 0) return;
313
- const labels = Array.isArray(label) ? label : [label];
314
- if (labels.length === 0) {
315
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
316
- }
317
- for (const value of labels) {
318
- if (!RESOURCE_NAME_PATTERN.test(value)) {
319
- throw new WorkflowNonRetryableError(
320
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
321
- );
322
- }
323
- }
324
- }
325
- function serializeLabel(label) {
326
- return Array.isArray(label) ? label.join(",") : label;
327
- }
328
- function validateFlowControl(flowControl) {
329
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
330
- throw new WorkflowNonRetryableError(
331
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
332
- );
333
- }
334
- }
335
- function getRandomInt() {
336
- return Math.floor(Math.random() * NANOID_CHARS.length);
337
- }
338
- function nanoid(length = NANOID_LENGTH) {
339
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
340
- }
341
- function getWorkflowRunId(id) {
342
- return `wfr_${id ?? nanoid()}`;
343
- }
344
- function decodeBase64(base64) {
345
- const binString = atob(base64);
346
- try {
347
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
348
- return new TextDecoder().decode(intArray);
349
- } catch (error) {
350
- console.warn(
351
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
352
- );
353
- return binString;
354
- }
355
- }
356
- function getUserIdFromToken(qstashClient) {
357
- try {
358
- const token = qstashClient.token;
359
- const decodedToken = decodeBase64(token);
360
- const tokenPayload = JSON.parse(decodedToken);
361
- const userId = tokenPayload.UserID;
362
- if (!userId) {
363
- throw new WorkflowError("QStash token payload does not contain userId");
364
- }
365
- return userId;
366
- } catch (error) {
367
- throw new WorkflowError(
368
- `Failed to decode QStash token while running create webhook step: ${error.message}`
369
- );
370
- }
371
- }
372
- function getQStashUrl(qstashClient) {
373
- try {
374
- const requester = qstashClient.http;
375
- const baseUrl = requester.baseUrl;
376
- if (!baseUrl) {
377
- throw new WorkflowError("QStash client does not have a baseUrl");
378
- }
379
- return baseUrl;
380
- } catch (error) {
381
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
382
- }
383
- }
384
- function getEventId() {
385
- return `evt_${nanoid(15)}`;
386
- }
387
- function stringifyBody(body) {
388
- if (body === void 0) {
389
- return void 0;
390
- }
391
- if (typeof body === "string") {
392
- return body;
393
- }
394
- return JSON.stringify(body);
395
- }
396
-
397
332
  // node_modules/neverthrow/dist/index.es.js
398
333
  var defaultErrorConfig = {
399
334
  withStackTrace: false
@@ -809,6 +744,102 @@ var Err = class {
809
744
  };
810
745
  var fromThrowable = Result.fromThrowable;
811
746
 
747
+ // src/context/auto-executor.ts
748
+ var import_qstash5 = require("@upstash/qstash");
749
+
750
+ // src/qstash/headers.ts
751
+ var import_qstash4 = require("@upstash/qstash");
752
+
753
+ // src/utils.ts
754
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
755
+ var NANOID_LENGTH = 21;
756
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
757
+ function validateLabel(label) {
758
+ if (label === void 0) return;
759
+ const labels = Array.isArray(label) ? label : [label];
760
+ if (labels.length === 0) {
761
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
762
+ }
763
+ for (const value of labels) {
764
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
765
+ throw new WorkflowNonRetryableError(
766
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
767
+ );
768
+ }
769
+ }
770
+ }
771
+ function serializeLabel(label) {
772
+ return Array.isArray(label) ? label.join(",") : label;
773
+ }
774
+ function validateFlowControl(flowControl) {
775
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
776
+ throw new WorkflowNonRetryableError(
777
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
778
+ );
779
+ }
780
+ }
781
+ function getRandomInt() {
782
+ return Math.floor(Math.random() * NANOID_CHARS.length);
783
+ }
784
+ function nanoid(length = NANOID_LENGTH) {
785
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
786
+ }
787
+ function getWorkflowRunId(id) {
788
+ return `wfr_${id ?? nanoid()}`;
789
+ }
790
+ function decodeBase64(base64) {
791
+ const binString = atob(base64);
792
+ try {
793
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
794
+ return new TextDecoder().decode(intArray);
795
+ } catch (error) {
796
+ console.warn(
797
+ `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
798
+ );
799
+ return binString;
800
+ }
801
+ }
802
+ function getUserIdFromToken(qstashClient) {
803
+ try {
804
+ const token = qstashClient.token;
805
+ const decodedToken = decodeBase64(token);
806
+ const tokenPayload = JSON.parse(decodedToken);
807
+ const userId = tokenPayload.UserID;
808
+ if (!userId) {
809
+ throw new WorkflowError("QStash token payload does not contain userId");
810
+ }
811
+ return userId;
812
+ } catch (error) {
813
+ throw new WorkflowError(
814
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
815
+ );
816
+ }
817
+ }
818
+ function getQStashUrl(qstashClient) {
819
+ try {
820
+ const requester = qstashClient.http;
821
+ const baseUrl = requester.baseUrl;
822
+ if (!baseUrl) {
823
+ throw new WorkflowError("QStash client does not have a baseUrl");
824
+ }
825
+ return baseUrl;
826
+ } catch (error) {
827
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
828
+ }
829
+ }
830
+ function getEventId() {
831
+ return `evt_${nanoid(15)}`;
832
+ }
833
+ function stringifyBody(body) {
834
+ if (body === void 0) {
835
+ return void 0;
836
+ }
837
+ if (typeof body === "string") {
838
+ return body;
839
+ }
840
+ return JSON.stringify(body);
841
+ }
842
+
812
843
  // src/types.ts
813
844
  var StepTypes = [
814
845
  "Initial",
@@ -914,6 +945,10 @@ var triggerFirstInvocation = async (params) => {
914
945
  return err(error_);
915
946
  }
916
947
  };
948
+ var flushPendingStep = async (workflowContext) => {
949
+ const { executor } = workflowContext;
950
+ return await executor.submitPendingStep();
951
+ };
917
952
  var triggerRouteFunction = async ({
918
953
  onCleanup,
919
954
  onStep,
@@ -1121,8 +1156,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1121
1156
  var BaseLazyStep = class _BaseLazyStep {
1122
1157
  stepName;
1123
1158
  context;
1124
- constructor(context, stepName) {
1159
+ /**
1160
+ * step-level settings (flow control, retries etc.) which override the
1161
+ * settings the workflow run was triggered with, for this step only.
1162
+ */
1163
+ stepSettings;
1164
+ /**
1165
+ * whether this step's result can be submitted after the route function
1166
+ * has moved on, instead of right away.
1167
+ *
1168
+ * Enabled for steps whose `getResultStep` produces the final `out` —
1169
+ * which is not the same as "needs no server round trip":
1170
+ * `LazyCreateWebhookStep` builds its result in `getBody` and returns
1171
+ * `out: undefined` from `getResultStep`, so deferring it would hand
1172
+ * `undefined` to the route function.
1173
+ *
1174
+ * Deferring lets the route function continue and reveal the next step,
1175
+ * so that step's settings can ride on this step's submission instead
1176
+ * of needing a step config request of their own.
1177
+ */
1178
+ supportsDeferredSubmission = false;
1179
+ constructor(context, stepName, stepSettings) {
1125
1180
  this.context = context;
1181
+ this.stepSettings = stepSettings;
1126
1182
  if (!stepName) {
1127
1183
  throw new WorkflowError(
1128
1184
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1186,7 +1242,13 @@ var BaseLazyStep = class _BaseLazyStep {
1186
1242
  step.out = JSON.stringify(step.out);
1187
1243
  return JSON.stringify(step);
1188
1244
  }
1189
- getHeaders({ context, telemetry, invokeCount, step }) {
1245
+ getHeaders({
1246
+ context,
1247
+ telemetry,
1248
+ invokeCount,
1249
+ step,
1250
+ stepSettings
1251
+ }) {
1190
1252
  return getHeaders({
1191
1253
  initHeaderValue: "false",
1192
1254
  workflowConfig: {
@@ -1199,7 +1261,8 @@ var BaseLazyStep = class _BaseLazyStep {
1199
1261
  stepInfo: {
1200
1262
  step,
1201
1263
  lazyStep: this
1202
- }
1264
+ },
1265
+ stepSettings
1203
1266
  });
1204
1267
  }
1205
1268
  async submitStep({ context, body, headers }) {
@@ -1217,8 +1280,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1217
1280
  stepFunction;
1218
1281
  stepType = "Run";
1219
1282
  allowUndefinedOut = true;
1220
- constructor(context, stepName, stepFunction) {
1221
- super(context, stepName);
1283
+ supportsDeferredSubmission = true;
1284
+ constructor(context, stepName, stepFunction, stepSettings) {
1285
+ super(context, stepName, stepSettings);
1222
1286
  this.stepFunction = stepFunction;
1223
1287
  }
1224
1288
  getPlanStep(concurrent, targetStep) {
@@ -1248,6 +1312,7 @@ var LazySleepStep = class extends BaseLazyStep {
1248
1312
  sleep;
1249
1313
  stepType = "SleepFor";
1250
1314
  allowUndefinedOut = true;
1315
+ supportsDeferredSubmission = true;
1251
1316
  constructor(context, stepName, sleep) {
1252
1317
  super(context, stepName);
1253
1318
  this.sleep = sleep;
@@ -1287,6 +1352,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1287
1352
  sleepUntil;
1288
1353
  stepType = "SleepUntil";
1289
1354
  allowUndefinedOut = true;
1355
+ supportsDeferredSubmission = true;
1290
1356
  constructor(context, stepName, sleepUntil) {
1291
1357
  super(context, stepName);
1292
1358
  this.sleepUntil = sleepUntil;
@@ -1779,6 +1845,7 @@ var WorkflowHeaders = class {
1779
1845
  invokeCount;
1780
1846
  initHeaderValue;
1781
1847
  stepInfo;
1848
+ stepSettings;
1782
1849
  headers;
1783
1850
  /**
1784
1851
  * @param params workflow header parameters
@@ -1788,17 +1855,20 @@ var WorkflowHeaders = class {
1788
1855
  workflowConfig,
1789
1856
  invokeCount,
1790
1857
  initHeaderValue,
1791
- stepInfo
1858
+ stepInfo,
1859
+ stepSettings
1792
1860
  }) {
1793
1861
  this.userHeaders = userHeaders;
1794
1862
  this.workflowConfig = workflowConfig;
1795
1863
  this.invokeCount = invokeCount;
1796
1864
  this.initHeaderValue = initHeaderValue;
1797
1865
  this.stepInfo = stepInfo;
1866
+ this.stepSettings = stepSettings;
1798
1867
  this.headers = {
1799
1868
  rawHeaders: {},
1800
1869
  workflowHeaders: {},
1801
- failureHeaders: {}
1870
+ failureHeaders: {},
1871
+ stepSettingsHeaders: {}
1802
1872
  };
1803
1873
  }
1804
1874
  getHeaders() {
@@ -1809,6 +1879,7 @@ var WorkflowHeaders = class {
1809
1879
  this.addUserHeaders();
1810
1880
  this.addInvokeCount();
1811
1881
  this.addFailureUrl();
1882
+ this.addStepSettings();
1812
1883
  const contentType = this.addContentType();
1813
1884
  return this.prefixHeaders(contentType);
1814
1885
  }
@@ -1818,7 +1889,7 @@ var WorkflowHeaders = class {
1818
1889
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1819
1890
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1820
1891
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1821
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1892
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
1822
1893
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1823
1894
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1824
1895
  };
@@ -1896,7 +1967,7 @@ var WorkflowHeaders = class {
1896
1967
  this.headers.failureHeaders["Workflow-Init"] = "false";
1897
1968
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1898
1969
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1899
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1970
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
1900
1971
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1901
1972
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1902
1973
  }
@@ -1904,6 +1975,42 @@ var WorkflowHeaders = class {
1904
1975
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
1905
1976
  }
1906
1977
  }
1978
+ /**
1979
+ * Applies the step-level settings of a step to the message.
1980
+ *
1981
+ * These headers configure the message itself, so that QStash uses them
1982
+ * instead of the settings the run was triggered with. The delivery of
1983
+ * the message is what executes the step, which is how a step's
1984
+ * settings reach it.
1985
+ *
1986
+ * When any setting is present the feature set is extended with
1987
+ * `WF_StepConfig`, which is what tells QStash to keep them. QStash
1988
+ * reports that back on the delivery as `Upstash-Workflow-Step-Config`.
1989
+ */
1990
+ addStepSettings() {
1991
+ if (!this.stepSettings) {
1992
+ return;
1993
+ }
1994
+ const headers = {};
1995
+ if (this.stepSettings.flowControl) {
1996
+ const { flowControlKey, flowControlValue } = prepareFlowControl(
1997
+ this.stepSettings.flowControl
1998
+ );
1999
+ headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
2000
+ headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
2001
+ }
2002
+ if (this.stepSettings.retries !== void 0) {
2003
+ headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
2004
+ }
2005
+ if (this.stepSettings.retryDelay) {
2006
+ headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
2007
+ }
2008
+ if (Object.keys(headers).length === 0) {
2009
+ return;
2010
+ }
2011
+ headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
2012
+ this.headers.stepSettingsHeaders = headers;
2013
+ }
1907
2014
  addContentType() {
1908
2015
  if (this.workflowConfig.useJSONContent) {
1909
2016
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -1917,14 +2024,17 @@ var WorkflowHeaders = class {
1917
2024
  return contentType;
1918
2025
  }
1919
2026
  prefixHeaders(contentType) {
1920
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
2027
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
1921
2028
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
1922
2029
  return {
1923
2030
  headers: {
1924
2031
  ...rawHeaders,
1925
2032
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
1926
2033
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
1927
- ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
2034
+ ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
2035
+ // last, so they override the run's settings above. Never
2036
+ // prefixed: they configure this message, not its callback.
2037
+ ...stepSettingsHeaders
1928
2038
  },
1929
2039
  contentType
1930
2040
  };
@@ -1975,7 +2085,7 @@ var submitParallelSteps = async ({
1975
2085
  info: `Submitting ${planSteps.length} parallel steps.`
1976
2086
  });
1977
2087
  const result = await context.qstashClient.batch(
1978
- planSteps.map((planStep) => {
2088
+ planSteps.map((planStep, index) => {
1979
2089
  const { headers } = getHeaders({
1980
2090
  initHeaderValue: "false",
1981
2091
  workflowConfig: {
@@ -1983,7 +2093,8 @@ var submitParallelSteps = async ({
1983
2093
  workflowUrl: context.url,
1984
2094
  telemetry
1985
2095
  },
1986
- invokeCount
2096
+ invokeCount,
2097
+ stepSettings: steps[index].stepSettings
1987
2098
  });
1988
2099
  return {
1989
2100
  headers,
@@ -2002,31 +2113,38 @@ var submitParallelSteps = async ({
2002
2113
  }
2003
2114
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
2004
2115
  };
2005
- var submitSingleStep = async ({
2006
- context,
2116
+ var executeStep = async ({
2007
2117
  lazyStep,
2008
2118
  stepId,
2009
- invokeCount,
2010
2119
  concurrency,
2011
- telemetry,
2012
- dispatchDebug,
2013
2120
  dispatchLifecycle
2014
2121
  }) => {
2015
2122
  await dispatchLifecycle("beforeExecution", {
2016
2123
  stepName: lazyStep.stepName
2017
2124
  });
2018
- let resultStep;
2019
2125
  try {
2020
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2126
+ return await lazyStep.getResultStep(concurrency, stepId);
2021
2127
  } catch (error) {
2022
2128
  attachStepNameToError(error, lazyStep.stepName);
2023
2129
  throw error;
2024
2130
  }
2131
+ };
2132
+ var submitStepResult = async ({
2133
+ context,
2134
+ lazyStep,
2135
+ resultStep,
2136
+ invokeCount,
2137
+ concurrency,
2138
+ telemetry,
2139
+ dispatchDebug,
2140
+ nextStepSettings
2141
+ }) => {
2025
2142
  const { headers } = lazyStep.getHeaders({
2026
2143
  context,
2027
2144
  step: resultStep,
2028
2145
  invokeCount,
2029
- telemetry
2146
+ telemetry,
2147
+ stepSettings: nextStepSettings
2030
2148
  });
2031
2149
  const body = lazyStep.getBody({
2032
2150
  context,
@@ -2051,6 +2169,138 @@ var submitSingleStep = async ({
2051
2169
  }
2052
2170
  return resultStep;
2053
2171
  };
2172
+ var publishStepConfigRequest = async ({
2173
+ context,
2174
+ lazyStep,
2175
+ targetStep,
2176
+ invokeCount,
2177
+ telemetry,
2178
+ dispatchDebug
2179
+ }) => {
2180
+ const { headers } = getHeaders({
2181
+ initHeaderValue: "false",
2182
+ workflowConfig: {
2183
+ workflowRunId: context.workflowRunId,
2184
+ workflowUrl: context.url,
2185
+ telemetry
2186
+ },
2187
+ invokeCount,
2188
+ stepSettings: lazyStep.stepSettings
2189
+ });
2190
+ await dispatchDebug("onInfo", {
2191
+ info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
2192
+ });
2193
+ const result = await context.qstashClient.publishJSON({
2194
+ headers: {
2195
+ ...headers,
2196
+ "Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
2197
+ },
2198
+ method: "POST",
2199
+ body: { targetStep, invokeCount },
2200
+ url: context.url,
2201
+ contentBasedDeduplication: true
2202
+ });
2203
+ if (result?.deduplicated) {
2204
+ await dispatchDebug("onWarning", {
2205
+ 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.`
2206
+ });
2207
+ } else if (result?.messageId) {
2208
+ await dispatchDebug("onInfo", {
2209
+ info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
2210
+ });
2211
+ }
2212
+ };
2213
+
2214
+ // src/qstash/step-config.ts
2215
+ var DEFAULT_PARALLELISM = 0;
2216
+ var DEFAULT_RATE = 0;
2217
+ var DEFAULT_PERIOD_SECONDS = 1;
2218
+ var SECONDS_PER_UNIT = {
2219
+ s: 1,
2220
+ m: 60,
2221
+ h: 60 * 60,
2222
+ d: 24 * 60 * 60
2223
+ };
2224
+ var durationToSeconds = (duration) => {
2225
+ if (typeof duration === "number") {
2226
+ return Number.isFinite(duration) ? duration : void 0;
2227
+ }
2228
+ const match = /^(\d+)([smhd])$/.exec(duration.trim());
2229
+ if (match) {
2230
+ return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
2231
+ }
2232
+ const seconds = Number(duration.trim());
2233
+ return Number.isFinite(seconds) ? seconds : void 0;
2234
+ };
2235
+ var normalizeFlowControl = (flowControl) => {
2236
+ const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
2237
+ return {
2238
+ key: flowControl.key,
2239
+ parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
2240
+ rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
2241
+ period: period ?? DEFAULT_PERIOD_SECONDS
2242
+ };
2243
+ };
2244
+ var parseFlowControlHeaders = (key, value) => {
2245
+ if (!key) {
2246
+ return void 0;
2247
+ }
2248
+ const flowControl = {
2249
+ key,
2250
+ parallelism: DEFAULT_PARALLELISM,
2251
+ rate: DEFAULT_RATE,
2252
+ period: DEFAULT_PERIOD_SECONDS
2253
+ };
2254
+ for (const entry of (value ?? "").split(",")) {
2255
+ const [name, rawValue] = entry.split("=").map((part) => part.trim());
2256
+ if (!name || rawValue === void 0) {
2257
+ continue;
2258
+ }
2259
+ switch (name) {
2260
+ case "parallelism": {
2261
+ flowControl.parallelism = Number(rawValue);
2262
+ break;
2263
+ }
2264
+ case "rate": {
2265
+ flowControl.rate = Number(rawValue);
2266
+ break;
2267
+ }
2268
+ case "period": {
2269
+ flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
2270
+ break;
2271
+ }
2272
+ }
2273
+ }
2274
+ return flowControl;
2275
+ };
2276
+ var getEffectiveConfig = (headers) => {
2277
+ const retriesHeader = headers.get(RETRIES_HEADER);
2278
+ return {
2279
+ flowControl: parseFlowControlHeaders(
2280
+ headers.get(FLOW_CONTROL_KEY_HEADER),
2281
+ headers.get(FLOW_CONTROL_VALUE_HEADER)
2282
+ ),
2283
+ retries: retriesHeader === null ? void 0 : Number(retriesHeader),
2284
+ retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
2285
+ hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
2286
+ };
2287
+ };
2288
+ var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
2289
+ if (stepSettings.flowControl) {
2290
+ const wanted = normalizeFlowControl(stepSettings.flowControl);
2291
+ const applied = effectiveConfig.flowControl;
2292
+ if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
2293
+ return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
2294
+ }
2295
+ }
2296
+ if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
2297
+ return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
2298
+ }
2299
+ if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
2300
+ return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
2301
+ }
2302
+ return void 0;
2303
+ };
2054
2304
 
2055
2305
  // src/context/auto-executor.ts
2056
2306
  var AutoExecutor = class _AutoExecutor {
@@ -2067,6 +2317,25 @@ var AutoExecutor = class _AutoExecutor {
2067
2317
  stepCount = 0;
2068
2318
  planStepCount = 0;
2069
2319
  executingStep = false;
2320
+ /**
2321
+ * an executed step whose result hasn't been submitted to QStash yet.
2322
+ *
2323
+ * Set when a step which can produce its result in-process executes. The
2324
+ * submission waits so that the route function can continue and reveal
2325
+ * what comes next: if that is a single step with step-level settings,
2326
+ * the settings ride on this submission and no step config request is
2327
+ * needed, since the delivery of the submission is what executes it.
2328
+ *
2329
+ * Flushed when the next step is reached (see `addStep`) or when the
2330
+ * route function ends (see `flushPendingStep` in `serve`).
2331
+ */
2332
+ pendingStep;
2333
+ /**
2334
+ * configuration QStash applied to the delivery being handled. A step
2335
+ * with step-level settings is settled by comparing its settings against
2336
+ * this.
2337
+ */
2338
+ effectiveConfig;
2070
2339
  /**
2071
2340
  * @param context workflow context
2072
2341
  * @param steps list of steps
@@ -2074,14 +2343,17 @@ var AutoExecutor = class _AutoExecutor {
2074
2343
  * @param dispatchLifecycle lifecycle event dispatcher
2075
2344
  * @param telemetry optional telemetry information
2076
2345
  * @param invokeCount optional invoke count
2346
+ * @param effectiveConfig configuration QStash applied to the delivery
2347
+ * being handled
2077
2348
  */
2078
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
2349
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount, effectiveConfig) {
2079
2350
  this.context = context;
2080
2351
  this.steps = steps;
2081
2352
  this.dispatchDebug = dispatchDebug;
2082
2353
  this.dispatchLifecycle = dispatchLifecycle;
2083
2354
  this.telemetry = telemetry;
2084
2355
  this.invokeCount = invokeCount ?? 0;
2356
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2085
2357
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2086
2358
  }
2087
2359
  /**
@@ -2116,6 +2388,15 @@ var AutoExecutor = class _AutoExecutor {
2116
2388
  lazyStepList.push(stepInfo);
2117
2389
  const index = this.indexInCurrentList++;
2118
2390
  const requestComplete = this.deferExecution().then(async () => {
2391
+ const submitted = await this.submitPendingStep(
2392
+ lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
2393
+ );
2394
+ if (submitted.isErr()) {
2395
+ throw submitted.error;
2396
+ }
2397
+ if (submitted.value.result === "submitted-step") {
2398
+ throw submitted.value.abort;
2399
+ }
2119
2400
  if (!this.promises.has(lazyStepList)) {
2120
2401
  const promise2 = this.getExecutionPromise(lazyStepList);
2121
2402
  this.promises.set(lazyStepList, promise2);
@@ -2169,18 +2450,100 @@ var AutoExecutor = class _AutoExecutor {
2169
2450
  }
2170
2451
  return parsedOut;
2171
2452
  }
2172
- const resultStep = await submitSingleStep({
2173
- context: this.context,
2453
+ const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
2454
+ if (mismatch) {
2455
+ if (!this.effectiveConfig.hasStepConfig) {
2456
+ await publishStepConfigRequest({
2457
+ context: this.context,
2458
+ lazyStep,
2459
+ targetStep: this.stepCount,
2460
+ invokeCount: this.invokeCount,
2461
+ telemetry: this.telemetry,
2462
+ dispatchDebug: this.dispatchDebug
2463
+ });
2464
+ throw new WorkflowAbort(lazyStep.stepName);
2465
+ } else {
2466
+ await this.dispatchDebug("onWarning", {
2467
+ 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.`
2468
+ });
2469
+ }
2470
+ }
2471
+ const resultStep = await executeStep({
2174
2472
  lazyStep,
2175
2473
  stepId: this.stepCount,
2474
+ concurrency: NO_CONCURRENCY,
2475
+ dispatchLifecycle: this.dispatchLifecycle
2476
+ });
2477
+ if (lazyStep.supportsDeferredSubmission) {
2478
+ this.pendingStep = { status: "held", lazyStep, resultStep };
2479
+ return lazyStep.parseOut({
2480
+ ...resultStep,
2481
+ out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
2482
+ });
2483
+ }
2484
+ await submitStepResult({
2485
+ context: this.context,
2486
+ lazyStep,
2487
+ resultStep,
2176
2488
  invokeCount: this.invokeCount,
2177
- concurrency: 1,
2489
+ concurrency: NO_CONCURRENCY,
2178
2490
  telemetry: this.telemetry,
2179
- dispatchDebug: this.dispatchDebug,
2180
- dispatchLifecycle: this.dispatchLifecycle
2491
+ dispatchDebug: this.dispatchDebug
2181
2492
  });
2182
2493
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2183
2494
  }
2495
+ /**
2496
+ * Submits the result of the step being held, if there is one, and
2497
+ * returns the abort the caller has to throw to end the invocation.
2498
+ *
2499
+ * Returns undefined when no step is being held, which is the caller's
2500
+ * signal that there is nothing to end. Throws instead of returning
2501
+ * when the submission itself fails, so that error reaches the caller
2502
+ * rather than an abort claiming the step was submitted.
2503
+ *
2504
+ * Callers which arrive while a submission is already under way (the
2505
+ * steps of a parallel group, added together) wait for that one and get
2506
+ * the same abort, rather than submitting a second time.
2507
+ *
2508
+ * The next step's settings are attached whenever it has any, without
2509
+ * checking them against the current delivery: the executor only knows
2510
+ * the configuration of the delivery in hand, which inside a
2511
+ * delivery is the *previous* step's, so such a check would be wrong
2512
+ * exactly when two steps with settings follow each other.
2513
+ *
2514
+ * @param nextStepSettings step-level settings of the next step
2515
+ * @returns `submitted-step` with the abort which ends this invocation,
2516
+ * or `no-pending-step` when nothing was held and there is nothing to
2517
+ * end
2518
+ */
2519
+ async submitPendingStep(nextStepSettings) {
2520
+ if (!this.pendingStep) {
2521
+ return ok({ result: "no-pending-step" });
2522
+ }
2523
+ try {
2524
+ if (this.pendingStep.status === "submitting") {
2525
+ return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
2526
+ }
2527
+ const { lazyStep, resultStep } = this.pendingStep;
2528
+ const submitted = (async () => {
2529
+ await submitStepResult({
2530
+ context: this.context,
2531
+ lazyStep,
2532
+ resultStep,
2533
+ invokeCount: this.invokeCount,
2534
+ concurrency: NO_CONCURRENCY,
2535
+ telemetry: this.telemetry,
2536
+ dispatchDebug: this.dispatchDebug,
2537
+ nextStepSettings
2538
+ });
2539
+ return new WorkflowAbort(lazyStep.stepName, resultStep);
2540
+ })();
2541
+ this.pendingStep = { status: "submitting", submitted };
2542
+ return ok({ result: "submitted-step", abort: await submitted });
2543
+ } catch (error) {
2544
+ return err(error);
2545
+ }
2546
+ }
2184
2547
  /**
2185
2548
  * Runs steps in parallel.
2186
2549
  *
@@ -2229,15 +2592,20 @@ var AutoExecutor = class _AutoExecutor {
2229
2592
  validateStep(parallelSteps[stepIndex], planStep);
2230
2593
  try {
2231
2594
  const parallelStep = parallelSteps[stepIndex];
2232
- const resultStep = await submitSingleStep({
2233
- context: this.context,
2595
+ const resultStep = await executeStep({
2234
2596
  lazyStep: parallelStep,
2235
2597
  stepId: planStep.targetStep,
2598
+ concurrency: parallelSteps.length,
2599
+ dispatchLifecycle: this.dispatchLifecycle
2600
+ });
2601
+ await submitStepResult({
2602
+ context: this.context,
2603
+ lazyStep: parallelStep,
2604
+ resultStep,
2236
2605
  invokeCount: this.invokeCount,
2237
2606
  concurrency: parallelSteps.length,
2238
2607
  telemetry: this.telemetry,
2239
- dispatchDebug: this.dispatchDebug,
2240
- dispatchLifecycle: this.dispatchLifecycle
2608
+ dispatchDebug: this.dispatchDebug
2241
2609
  });
2242
2610
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2243
2611
  } catch (error) {
@@ -2786,6 +3154,46 @@ var WorkflowContext = class {
2786
3154
  * `1` on the first retry, `2` on the second, and so on.
2787
3155
  */
2788
3156
  retried;
3157
+ /**
3158
+ * Configuration QStash applied to the request being handled.
3159
+ *
3160
+ * This is the configuration of *this delivery*, not of the run: it is
3161
+ * the configuration the run was triggered with, except inside the
3162
+ * delivery which executes a step that has step-level settings, where
3163
+ * it is that step's settings. The fields below expose it; the executor
3164
+ * gets it directly rather than reaching through the context.
3165
+ */
3166
+ effectiveConfig;
3167
+ /**
3168
+ * Flow control QStash applied to the request being handled, if any.
3169
+ *
3170
+ * This is the flow control of the request in hand, which is the one the
3171
+ * run was triggered with except inside a step carrying its own.
3172
+ */
3173
+ get flowControl() {
3174
+ return this.effectiveConfig.flowControl;
3175
+ }
3176
+ /**
3177
+ * Retry limit QStash applied to the request being handled, if it
3178
+ * reported one.
3179
+ *
3180
+ * Not to be confused with {@link retried}, which is how many retries
3181
+ * have already happened.
3182
+ *
3183
+ * @see {@link flowControl} for what "the request being handled" means
3184
+ */
3185
+ get retries() {
3186
+ return this.effectiveConfig.retries;
3187
+ }
3188
+ /**
3189
+ * Retry delay expression QStash applied to the request being handled,
3190
+ * if any.
3191
+ *
3192
+ * @see {@link flowControl} for what "the request being handled" means
3193
+ */
3194
+ get retryDelay() {
3195
+ return this.effectiveConfig.retryDelay;
3196
+ }
2789
3197
  constructor({
2790
3198
  qstashClient,
2791
3199
  workflowRunId,
@@ -2799,7 +3207,8 @@ var WorkflowContext = class {
2799
3207
  invokeCount,
2800
3208
  label,
2801
3209
  retried,
2802
- middlewareManager
3210
+ middlewareManager,
3211
+ effectiveConfig
2803
3212
  }) {
2804
3213
  this.qstashClient = qstashClient;
2805
3214
  this.workflowRunId = workflowRunId;
@@ -2811,6 +3220,7 @@ var WorkflowContext = class {
2811
3220
  this.env = env ?? {};
2812
3221
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2813
3222
  this.retried = retried ?? 0;
3223
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2814
3224
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2815
3225
  middlewareManagerInstance.assignContext(this);
2816
3226
  this.executor = new AutoExecutor(
@@ -2819,7 +3229,8 @@ var WorkflowContext = class {
2819
3229
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2820
3230
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2821
3231
  telemetry,
2822
- invokeCount
3232
+ invokeCount,
3233
+ this.effectiveConfig
2823
3234
  );
2824
3235
  }
2825
3236
  /**
@@ -2845,13 +3256,35 @@ var WorkflowContext = class {
2845
3256
  * ])
2846
3257
  * ```
2847
3258
  *
3259
+ * Step-level settings can be passed as a third argument, overriding
3260
+ * the settings the workflow run was triggered with for this step only:
3261
+ *
3262
+ * ```typescript
3263
+ * const result = await context.run(
3264
+ * "step 1",
3265
+ * () => {
3266
+ * return "result"
3267
+ * },
3268
+ * {
3269
+ * flowControl: { key: "custom-key", parallelism: 3 },
3270
+ * retries: 5,
3271
+ * }
3272
+ * )
3273
+ * ```
3274
+ *
2848
3275
  * @param stepName name of the step
2849
3276
  * @param stepFunction step function to be executed
3277
+ * @param stepSettings step-level settings for this step
2850
3278
  * @returns result of the step function
2851
3279
  */
2852
- async run(stepName, stepFunction) {
3280
+ async run(stepName, stepFunction, stepSettings) {
3281
+ if (stepSettings) {
3282
+ validateFlowControl(stepSettings.flowControl);
3283
+ }
2853
3284
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2854
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3285
+ return await this.addStep(
3286
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3287
+ );
2855
3288
  }
2856
3289
  /**
2857
3290
  * Stops the execution for the duration provided.
@@ -3074,7 +3507,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3074
3507
  *
3075
3508
  * @param routeFunction
3076
3509
  */
3077
- static async tryAuthentication(routeFunction, context) {
3510
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3078
3511
  const disabledContext = new _DisabledWorkflowContext({
3079
3512
  qstashClient: new import_qstash10.Client({
3080
3513
  baseUrl: "disabled-client",
@@ -3089,7 +3522,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3089
3522
  initialPayload: context.requestPayload,
3090
3523
  env: context.env,
3091
3524
  label: context.labels,
3092
- retried: context.retried
3525
+ retried: context.retried,
3526
+ // the route function runs for real here until it reaches a step, so
3527
+ // it observes the same configuration as it will on the real context
3528
+ effectiveConfig
3093
3529
  });
3094
3530
  try {
3095
3531
  await routeFunction(disabledContext);
@@ -3306,6 +3742,7 @@ var handleFailure = async ({
3306
3742
  }
3307
3743
  const userHeaders = recreateUserHeaders(request.headers);
3308
3744
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3745
+ const effectiveConfig = getEffectiveConfig(request.headers);
3309
3746
  const workflowContext = new WorkflowContext({
3310
3747
  qstashClient,
3311
3748
  workflowRunId,
@@ -3319,11 +3756,13 @@ var handleFailure = async ({
3319
3756
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3320
3757
  retried,
3321
3758
  workflowRunCreatedAt: workflowCreatedAt,
3322
- middlewareManager: void 0
3759
+ middlewareManager: void 0,
3760
+ effectiveConfig
3323
3761
  });
3324
3762
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3325
3763
  routeFunction,
3326
- workflowContext
3764
+ workflowContext,
3765
+ effectiveConfig
3327
3766
  );
3328
3767
  if (authCheck.isErr()) {
3329
3768
  await dispatchDebug?.("onError", {
@@ -3415,7 +3854,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3415
3854
  const client = new import_qstash11.Client({
3416
3855
  ...clientOptions,
3417
3856
  baseUrl: clientEnv.QSTASH_URL,
3418
- token: clientEnv.QSTASH_TOKEN
3857
+ token: clientEnv.QSTASH_TOKEN,
3858
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3859
+ // the client at the local dev server too. Without this the QStash Client
3860
+ // re-derives dev mode from its own process.env, which is blind to the
3861
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3862
+ // An explicit devMode in the user's client config still wins.
3863
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3419
3864
  });
3420
3865
  const receiver = getReceiver(environment, receiverConfig, region);
3421
3866
  return { client, receiver };
@@ -3465,7 +3910,9 @@ var getQStashHandlers = ({
3465
3910
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3466
3911
  ...qstashClientOption,
3467
3912
  baseUrl: environment.QSTASH_URL,
3468
- token: environment.QSTASH_TOKEN
3913
+ token: environment.QSTASH_TOKEN,
3914
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
3915
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3469
3916
  }),
3470
3917
  receiver: getReceiver(environment, receiverConfig)
3471
3918
  }
@@ -3805,12 +4252,14 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3805
4252
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3806
4253
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3807
4254
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4255
+ const effectiveConfig = getEffectiveConfig(request.headers);
3808
4256
  const workflowContext = new WorkflowContext({
3809
4257
  qstashClient: regionalClient,
3810
4258
  workflowRunId,
3811
4259
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
3812
4260
  headers: recreateUserHeaders(request.headers),
3813
4261
  steps,
4262
+ effectiveConfig,
3814
4263
  url: workflowUrl,
3815
4264
  env,
3816
4265
  telemetry,
@@ -3822,7 +4271,8 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3822
4271
  });
3823
4272
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3824
4273
  routeFunction,
3825
- workflowContext
4274
+ workflowContext,
4275
+ effectiveConfig
3826
4276
  );
3827
4277
  if (authCheck.isErr()) {
3828
4278
  throw authCheck.error;
@@ -3859,7 +4309,23 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3859
4309
  if (steps.length === 1) {
3860
4310
  await middlewareManager.dispatchLifecycle("runStarted", {});
3861
4311
  }
3862
- return await routeFunction(workflowContext);
4312
+ let outcome;
4313
+ try {
4314
+ outcome = { ran: true, result: await routeFunction(workflowContext) };
4315
+ } catch (error) {
4316
+ outcome = { ran: false, error };
4317
+ }
4318
+ const submitted = await flushPendingStep(workflowContext);
4319
+ if (submitted.isErr()) {
4320
+ throw submitted.error;
4321
+ }
4322
+ if (submitted.value.result === "submitted-step") {
4323
+ throw submitted.value.abort;
4324
+ }
4325
+ if (!outcome.ran) {
4326
+ throw outcome.error;
4327
+ }
4328
+ return outcome.result;
3863
4329
  },
3864
4330
  onCleanup: async (result2) => {
3865
4331
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -3939,6 +4405,7 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3939
4405
  return { handler: safeHandler };
3940
4406
  };
3941
4407
  var serve = (routeFunction, options) => {
4408
+ void (0, import_qstash12.startDevServer)();
3942
4409
  return serveBase(
3943
4410
  routeFunction,
3944
4411
  {
@@ -3950,9 +4417,10 @@ var serve = (routeFunction, options) => {
3950
4417
  };
3951
4418
 
3952
4419
  // src/client/index.ts
3953
- var import_qstash12 = require("@upstash/qstash");
4420
+ var import_qstash14 = require("@upstash/qstash");
3954
4421
 
3955
4422
  // src/client/dlq.ts
4423
+ var import_qstash13 = require("@upstash/qstash");
3956
4424
  function buildResumeRestartHeaders(options) {
3957
4425
  const headers = {};
3958
4426
  if (options?.flowControl) {
@@ -4013,8 +4481,11 @@ var DLQ = class {
4013
4481
  });
4014
4482
  return Array.isArray(dlqId) ? workflowRuns : workflowRuns[0];
4015
4483
  }
4016
- if (typeof request === "string") request = [request];
4017
- if (Array.isArray(request) && request.length === 0) return { workflowRuns: [] };
4484
+ if (typeof request === "string" || Array.isArray(request)) {
4485
+ const ids = toNonEmptyIdArray(request, "DLQ id");
4486
+ if (ids.length === 0) return { workflowRuns: [] };
4487
+ request = ids;
4488
+ }
4018
4489
  const filters = Array.isArray(request) ? { dlqIds: request } : request;
4019
4490
  return normalizeCursor(
4020
4491
  await this.client.http.request({
@@ -4037,8 +4508,11 @@ var DLQ = class {
4037
4508
  });
4038
4509
  return Array.isArray(dlqId) ? workflowRuns : workflowRuns[0];
4039
4510
  }
4040
- if (typeof request === "string") request = [request];
4041
- if (Array.isArray(request) && request.length === 0) return { workflowRuns: [] };
4511
+ if (typeof request === "string" || Array.isArray(request)) {
4512
+ const ids = toNonEmptyIdArray(request, "DLQ id");
4513
+ if (ids.length === 0) return { workflowRuns: [] };
4514
+ request = ids;
4515
+ }
4042
4516
  const filters = Array.isArray(request) ? { dlqIds: request } : request;
4043
4517
  return normalizeCursor(
4044
4518
  await this.client.http.request({
@@ -4057,12 +4531,34 @@ var DLQ = class {
4057
4531
  * @returns response with workflow run information
4058
4532
  */
4059
4533
  async retryFailureFunction({ dlqId }) {
4534
+ assertNonEmptyId(dlqId, "DLQ id");
4060
4535
  const response = await this.client.http.request({
4061
4536
  path: ["v2", "workflows", "dlq", "callback", dlqId],
4062
4537
  method: "POST"
4063
4538
  });
4064
4539
  return response;
4065
4540
  }
4541
+ /**
4542
+ * Cancel the in-progress failure function (failureUrl/failureFunction) call
4543
+ * of a workflow run.
4544
+ *
4545
+ * Fails if the run has no failure function call in progress.
4546
+ *
4547
+ * @param dlqId - The ID of the DLQ message whose failure function call will be canceled
4548
+ */
4549
+ async cancelFailureFunction({ dlqId }) {
4550
+ assertNonEmptyId(dlqId, "DLQ id");
4551
+ try {
4552
+ await this.client.http.request({
4553
+ path: ["v2", "workflows", "dlq", "callback", dlqId],
4554
+ method: "DELETE",
4555
+ parseResponseAsJson: false
4556
+ });
4557
+ } catch (error) {
4558
+ if (isInstanceOf(error, import_qstash13.QstashError) && error.status === 302) return;
4559
+ throw error;
4560
+ }
4561
+ }
4066
4562
  /**
4067
4563
  * Delete DLQ messages.
4068
4564
  *
@@ -4084,8 +4580,11 @@ var DLQ = class {
4084
4580
  * ```
4085
4581
  */
4086
4582
  async delete(request) {
4087
- if (typeof request === "string") request = [request];
4088
- if (Array.isArray(request) && request.length === 0) return { deleted: 0 };
4583
+ if (typeof request === "string" || Array.isArray(request)) {
4584
+ const ids = toNonEmptyIdArray(request, "DLQ id");
4585
+ if (ids.length === 0) return { deleted: 0 };
4586
+ request = ids;
4587
+ }
4089
4588
  const filters = Array.isArray(request) ? { dlqIds: request } : request;
4090
4589
  return normalizeCursor(
4091
4590
  await this.client.http.request({
@@ -4101,7 +4600,7 @@ var DLQ = class {
4101
4600
  var Client4 = class {
4102
4601
  client;
4103
4602
  constructor(clientConfig) {
4104
- this.client = new import_qstash12.Client(clientConfig);
4603
+ this.client = new import_qstash14.Client(clientConfig);
4105
4604
  }
4106
4605
  async cancel(request) {
4107
4606
  if (typeof request === "object" && !Array.isArray(request) && ("ids" in request || "urlStartingWith" in request)) {
@@ -4114,8 +4613,11 @@ var Client4 = class {
4114
4613
  return this.cancel({ filter: { workflowUrlStartingWith: legacy.urlStartingWith } });
4115
4614
  }
4116
4615
  }
4117
- if (typeof request === "string") request = [request];
4118
- if (Array.isArray(request) && request.length === 0) return { cancelled: 0 };
4616
+ if (typeof request === "string" || Array.isArray(request)) {
4617
+ const ids = toNonEmptyIdArray(request, "Workflow run id");
4618
+ if (ids.length === 0) return { cancelled: 0 };
4619
+ request = ids;
4620
+ }
4119
4621
  const filters = Array.isArray(request) ? { workflowRunIds: request } : request;
4120
4622
  return await this.client.http.request({
4121
4623
  path: ["v2", "workflows", "runs"],