@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
@@ -118,6 +118,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
118
118
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
119
119
  var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
120
120
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
121
+ var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
122
+ var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
123
+ var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
124
+ var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
125
+ var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
126
+ var RETRIES_HEADER = "Upstash-Retries";
127
+ var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
128
+ var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
121
129
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
122
130
  var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
123
131
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
@@ -224,228 +232,6 @@ var getNewUrlFromWorkflowId = (url, workflowId) => {
224
232
  return url.replace(/[^/]+$/, workflowId);
225
233
  };
226
234
 
227
- // src/context/auto-executor.ts
228
- import { QstashError as QstashError5 } from "@upstash/qstash";
229
-
230
- // src/qstash/headers.ts
231
- import { QstashError as QstashError4 } from "@upstash/qstash";
232
-
233
- // src/client/utils.ts
234
- import { QstashError as QstashError2 } from "@upstash/qstash";
235
- var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
236
- const path = workflowRunId ? ["v2", "notify", workflowRunId, eventId] : ["v2", "notify", eventId];
237
- const result = await requester.request({
238
- path,
239
- method: "POST",
240
- body: typeof eventData === "string" ? eventData : JSON.stringify(eventData)
241
- });
242
- return result;
243
- };
244
- var makeGetWaitersRequest = async (requester, eventId) => {
245
- const result = await requester.request({
246
- path: ["v2", "waiters", eventId],
247
- method: "GET"
248
- });
249
- return result;
250
- };
251
- var makeCancelRequest = async (requester, workflowRunId) => {
252
- await requester.request({
253
- path: ["v2", "workflows", "runs", `${workflowRunId}?cancel=true`],
254
- method: "DELETE",
255
- parseResponseAsJson: false
256
- });
257
- return true;
258
- };
259
- var getSteps = async (requester, workflowRunId, messageId, dispatchDebug) => {
260
- try {
261
- const steps = await requester.request({
262
- path: ["v2", "workflows", "runs", workflowRunId],
263
- parseResponseAsJson: true
264
- });
265
- if (steps.length === 1) {
266
- return {
267
- steps,
268
- workflowRunEnded: false
269
- };
270
- }
271
- if (!messageId) {
272
- await dispatchDebug?.("onInfo", {
273
- info: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
274
- });
275
- return { steps, workflowRunEnded: false };
276
- } else {
277
- const index = steps.findIndex((item) => item.messageId === messageId);
278
- if (index === -1) {
279
- return { steps: [], workflowRunEnded: false };
280
- }
281
- const filteredSteps = steps.slice(0, index + 1);
282
- await dispatchDebug?.("onInfo", {
283
- info: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
284
- });
285
- return { steps: filteredSteps, workflowRunEnded: false };
286
- }
287
- } catch (error) {
288
- if (isInstanceOf(error, QstashError2) && error.status === 404) {
289
- await dispatchDebug?.("onWarning", {
290
- warning: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed."
291
- });
292
- return { steps: void 0, workflowRunEnded: true };
293
- } else {
294
- throw error;
295
- }
296
- }
297
- };
298
- function normalizeCursor(response) {
299
- const cursor = response.cursor;
300
- return { ...response, cursor: cursor || void 0 };
301
- }
302
- var DEFAULT_BULK_COUNT = 100;
303
- function buildBulkActionQueryParameters(request, options) {
304
- const cursor = "cursor" in request ? request.cursor : void 0;
305
- if ("all" in request) {
306
- return { count: request.count ?? DEFAULT_BULK_COUNT, cursor };
307
- }
308
- if ("dlqIds" in request) {
309
- const ids = request.dlqIds;
310
- if (Array.isArray(ids) && ids.length === 0) {
311
- throw new QstashError2(
312
- "Empty dlqIds array provided. If you intend to target all DLQ messages, use { all: true } explicitly."
313
- );
314
- }
315
- return { dlqIds: ids, cursor };
316
- }
317
- if ("workflowRunIds" in request && request.workflowRunIds) {
318
- if (request.workflowRunIds.length === 0) {
319
- throw new QstashError2(
320
- "Empty workflowRunIds array provided. If you intend to target all workflow runs, use { all: true } explicitly."
321
- );
322
- }
323
- return { workflowRunIds: request.workflowRunIds };
324
- }
325
- const filter = request.filter;
326
- if (!filter) {
327
- throw new QstashError2(
328
- "No filter provided. Use { filter: { ... } } with at least one filter field, or { all: true }."
329
- );
330
- }
331
- if (options?.translateWorkflowUrl) {
332
- const { workflowUrlStartingWith, workflowUrl, ...rest } = filter;
333
- if (workflowUrlStartingWith && workflowUrl) {
334
- throw new QstashError2(
335
- "workflowUrl and workflowUrlStartingWith are mutually exclusive. Use workflowUrl for exact match or workflowUrlStartingWith for prefix match."
336
- );
337
- }
338
- const urlParams = {};
339
- if (workflowUrlStartingWith) {
340
- urlParams.workflowUrl = workflowUrlStartingWith;
341
- } else if (workflowUrl) {
342
- urlParams.workflowUrl = workflowUrl;
343
- urlParams.workflowUrlExactMatch = true;
344
- }
345
- return {
346
- ...rest,
347
- ...urlParams,
348
- count: request.count ?? DEFAULT_BULK_COUNT,
349
- cursor
350
- };
351
- }
352
- return {
353
- ...filter,
354
- count: request.count ?? DEFAULT_BULK_COUNT,
355
- cursor
356
- };
357
- }
358
-
359
- // src/utils.ts
360
- var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
361
- var NANOID_LENGTH = 21;
362
- var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
363
- function validateLabel(label) {
364
- if (label === void 0) return;
365
- const labels = Array.isArray(label) ? label : [label];
366
- if (labels.length === 0) {
367
- throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
368
- }
369
- for (const value of labels) {
370
- if (!RESOURCE_NAME_PATTERN.test(value)) {
371
- throw new WorkflowNonRetryableError(
372
- `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
373
- );
374
- }
375
- }
376
- }
377
- function serializeLabel(label) {
378
- return Array.isArray(label) ? label.join(",") : label;
379
- }
380
- function validateFlowControl(flowControl) {
381
- if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
382
- throw new WorkflowNonRetryableError(
383
- `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
384
- );
385
- }
386
- }
387
- function getRandomInt() {
388
- return Math.floor(Math.random() * NANOID_CHARS.length);
389
- }
390
- function nanoid(length = NANOID_LENGTH) {
391
- return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
392
- }
393
- function getWorkflowRunId(id) {
394
- return `wfr_${id ?? nanoid()}`;
395
- }
396
- function decodeBase64(base64) {
397
- const binString = atob(base64);
398
- try {
399
- const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
400
- return new TextDecoder().decode(intArray);
401
- } catch (error) {
402
- console.warn(
403
- `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
404
- );
405
- return binString;
406
- }
407
- }
408
- function getUserIdFromToken(qstashClient) {
409
- try {
410
- const token = qstashClient.token;
411
- const decodedToken = decodeBase64(token);
412
- const tokenPayload = JSON.parse(decodedToken);
413
- const userId = tokenPayload.UserID;
414
- if (!userId) {
415
- throw new WorkflowError("QStash token payload does not contain userId");
416
- }
417
- return userId;
418
- } catch (error) {
419
- throw new WorkflowError(
420
- `Failed to decode QStash token while running create webhook step: ${error.message}`
421
- );
422
- }
423
- }
424
- function getQStashUrl(qstashClient) {
425
- try {
426
- const requester = qstashClient.http;
427
- const baseUrl = requester.baseUrl;
428
- if (!baseUrl) {
429
- throw new WorkflowError("QStash client does not have a baseUrl");
430
- }
431
- return baseUrl;
432
- } catch (error) {
433
- throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
434
- }
435
- }
436
- function getEventId() {
437
- return `evt_${nanoid(15)}`;
438
- }
439
- function stringifyBody(body) {
440
- if (body === void 0) {
441
- return void 0;
442
- }
443
- if (typeof body === "string") {
444
- return body;
445
- }
446
- return JSON.stringify(body);
447
- }
448
-
449
235
  // node_modules/neverthrow/dist/index.es.js
450
236
  var defaultErrorConfig = {
451
237
  withStackTrace: false
@@ -839,27 +625,269 @@ var Err = class {
839
625
  asyncMap(_f) {
840
626
  return errAsync(this.error);
841
627
  }
842
- unwrapOr(v) {
843
- return v;
628
+ unwrapOr(v) {
629
+ return v;
630
+ }
631
+ match(_ok, err2) {
632
+ return err2(this.error);
633
+ }
634
+ safeUnwrap() {
635
+ const error = this.error;
636
+ return (function* () {
637
+ yield err(error);
638
+ throw new Error("Do not use this generator out of `safeTry`");
639
+ })();
640
+ }
641
+ _unsafeUnwrap(config) {
642
+ throw createNeverThrowError("Called `_unsafeUnwrap` on an Err", this, config);
643
+ }
644
+ _unsafeUnwrapErr(_) {
645
+ return this.error;
646
+ }
647
+ };
648
+ var fromThrowable = Result.fromThrowable;
649
+
650
+ // src/context/auto-executor.ts
651
+ import { QstashError as QstashError5 } from "@upstash/qstash";
652
+
653
+ // src/qstash/headers.ts
654
+ import { QstashError as QstashError4 } from "@upstash/qstash";
655
+
656
+ // src/client/utils.ts
657
+ import { QstashError as QstashError2 } from "@upstash/qstash";
658
+ var assertNonEmptyId = (id, label = "id") => {
659
+ if (!id) {
660
+ throw new QstashError2(`${label} cannot be empty`);
661
+ }
662
+ };
663
+ var toNonEmptyIdArray = (request, label = "id") => {
664
+ const ids = typeof request === "string" ? [request] : request;
665
+ for (const id of ids) assertNonEmptyId(id, label);
666
+ return ids;
667
+ };
668
+ var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
669
+ assertNonEmptyId(eventId, "Event id");
670
+ if (workflowRunId !== void 0) assertNonEmptyId(workflowRunId, "Workflow run id");
671
+ const path = workflowRunId === void 0 ? ["v2", "notify", eventId] : ["v2", "notify", workflowRunId, eventId];
672
+ const result = await requester.request({
673
+ path,
674
+ method: "POST",
675
+ body: typeof eventData === "string" ? eventData : JSON.stringify(eventData)
676
+ });
677
+ return result;
678
+ };
679
+ var makeGetWaitersRequest = async (requester, eventId) => {
680
+ assertNonEmptyId(eventId, "Event id");
681
+ const result = await requester.request({
682
+ path: ["v2", "waiters", eventId],
683
+ method: "GET"
684
+ });
685
+ return result;
686
+ };
687
+ var makeCancelRequest = async (requester, workflowRunId) => {
688
+ await requester.request({
689
+ path: ["v2", "workflows", "runs", `${workflowRunId}?cancel=true`],
690
+ method: "DELETE",
691
+ parseResponseAsJson: false
692
+ });
693
+ return true;
694
+ };
695
+ var getSteps = async (requester, workflowRunId, messageId, dispatchDebug) => {
696
+ try {
697
+ const steps = await requester.request({
698
+ path: ["v2", "workflows", "runs", workflowRunId],
699
+ parseResponseAsJson: true
700
+ });
701
+ if (steps.length === 1) {
702
+ return {
703
+ steps,
704
+ workflowRunEnded: false
705
+ };
706
+ }
707
+ if (!messageId) {
708
+ await dispatchDebug?.("onInfo", {
709
+ info: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
710
+ });
711
+ return { steps, workflowRunEnded: false };
712
+ } else {
713
+ const index = steps.findIndex((item) => item.messageId === messageId);
714
+ if (index === -1) {
715
+ return { steps: [], workflowRunEnded: false };
716
+ }
717
+ const filteredSteps = steps.slice(0, index + 1);
718
+ await dispatchDebug?.("onInfo", {
719
+ info: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
720
+ });
721
+ return { steps: filteredSteps, workflowRunEnded: false };
722
+ }
723
+ } catch (error) {
724
+ if (isInstanceOf(error, QstashError2) && error.status === 404) {
725
+ await dispatchDebug?.("onWarning", {
726
+ warning: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed."
727
+ });
728
+ return { steps: void 0, workflowRunEnded: true };
729
+ } else {
730
+ throw error;
731
+ }
732
+ }
733
+ };
734
+ function normalizeCursor(response) {
735
+ const cursor = response.cursor;
736
+ return { ...response, cursor: cursor || void 0 };
737
+ }
738
+ var DEFAULT_BULK_COUNT = 100;
739
+ function buildBulkActionQueryParameters(request, options) {
740
+ const cursor = "cursor" in request ? request.cursor : void 0;
741
+ if ("all" in request) {
742
+ return { count: request.count ?? DEFAULT_BULK_COUNT, cursor };
743
+ }
744
+ if ("dlqIds" in request) {
745
+ const ids = request.dlqIds;
746
+ if (Array.isArray(ids) && ids.length === 0) {
747
+ throw new QstashError2(
748
+ "Empty dlqIds array provided. If you intend to target all DLQ messages, use { all: true } explicitly."
749
+ );
750
+ }
751
+ return { dlqIds: ids, cursor };
752
+ }
753
+ if ("workflowRunIds" in request && request.workflowRunIds) {
754
+ if (request.workflowRunIds.length === 0) {
755
+ throw new QstashError2(
756
+ "Empty workflowRunIds array provided. If you intend to target all workflow runs, use { all: true } explicitly."
757
+ );
758
+ }
759
+ return { workflowRunIds: request.workflowRunIds };
760
+ }
761
+ const filter = request.filter;
762
+ if (!filter) {
763
+ throw new QstashError2(
764
+ "No filter provided. Use { filter: { ... } } with at least one filter field, or { all: true }."
765
+ );
766
+ }
767
+ for (const [field, value] of Object.entries(filter)) {
768
+ if (Array.isArray(value) && value.length === 0) {
769
+ throw new QstashError2(
770
+ `Empty array provided for filter field '${field}'. If you intend to target all records, use { all: true } explicitly.`
771
+ );
772
+ }
773
+ }
774
+ if (options?.translateWorkflowUrl) {
775
+ const { workflowUrlStartingWith, workflowUrl, ...rest } = filter;
776
+ if (workflowUrlStartingWith && workflowUrl) {
777
+ throw new QstashError2(
778
+ "workflowUrl and workflowUrlStartingWith are mutually exclusive. Use workflowUrl for exact match or workflowUrlStartingWith for prefix match."
779
+ );
780
+ }
781
+ const urlParams = {};
782
+ if (workflowUrlStartingWith) {
783
+ urlParams.workflowUrl = workflowUrlStartingWith;
784
+ } else if (workflowUrl) {
785
+ urlParams.workflowUrl = workflowUrl;
786
+ urlParams.workflowUrlExactMatch = true;
787
+ }
788
+ return {
789
+ ...rest,
790
+ ...urlParams,
791
+ count: request.count ?? DEFAULT_BULK_COUNT,
792
+ cursor
793
+ };
794
+ }
795
+ return {
796
+ ...filter,
797
+ count: request.count ?? DEFAULT_BULK_COUNT,
798
+ cursor
799
+ };
800
+ }
801
+
802
+ // src/utils.ts
803
+ var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
804
+ var NANOID_LENGTH = 21;
805
+ var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
806
+ function validateLabel(label) {
807
+ if (label === void 0) return;
808
+ const labels = Array.isArray(label) ? label : [label];
809
+ if (labels.length === 0) {
810
+ throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
811
+ }
812
+ for (const value of labels) {
813
+ if (!RESOURCE_NAME_PATTERN.test(value)) {
814
+ throw new WorkflowNonRetryableError(
815
+ `Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
816
+ );
817
+ }
818
+ }
819
+ }
820
+ function serializeLabel(label) {
821
+ return Array.isArray(label) ? label.join(",") : label;
822
+ }
823
+ function validateFlowControl(flowControl) {
824
+ if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
825
+ throw new WorkflowNonRetryableError(
826
+ `Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
827
+ );
828
+ }
829
+ }
830
+ function getRandomInt() {
831
+ return Math.floor(Math.random() * NANOID_CHARS.length);
832
+ }
833
+ function nanoid(length = NANOID_LENGTH) {
834
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
835
+ }
836
+ function getWorkflowRunId(id) {
837
+ return `wfr_${id ?? nanoid()}`;
838
+ }
839
+ function decodeBase64(base64) {
840
+ const binString = atob(base64);
841
+ try {
842
+ const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
843
+ return new TextDecoder().decode(intArray);
844
+ } catch (error) {
845
+ console.warn(
846
+ `Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
847
+ );
848
+ return binString;
844
849
  }
845
- match(_ok, err2) {
846
- return err2(this.error);
850
+ }
851
+ function getUserIdFromToken(qstashClient) {
852
+ try {
853
+ const token = qstashClient.token;
854
+ const decodedToken = decodeBase64(token);
855
+ const tokenPayload = JSON.parse(decodedToken);
856
+ const userId = tokenPayload.UserID;
857
+ if (!userId) {
858
+ throw new WorkflowError("QStash token payload does not contain userId");
859
+ }
860
+ return userId;
861
+ } catch (error) {
862
+ throw new WorkflowError(
863
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
864
+ );
847
865
  }
848
- safeUnwrap() {
849
- const error = this.error;
850
- return (function* () {
851
- yield err(error);
852
- throw new Error("Do not use this generator out of `safeTry`");
853
- })();
866
+ }
867
+ function getQStashUrl(qstashClient) {
868
+ try {
869
+ const requester = qstashClient.http;
870
+ const baseUrl = requester.baseUrl;
871
+ if (!baseUrl) {
872
+ throw new WorkflowError("QStash client does not have a baseUrl");
873
+ }
874
+ return baseUrl;
875
+ } catch (error) {
876
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
854
877
  }
855
- _unsafeUnwrap(config) {
856
- throw createNeverThrowError("Called `_unsafeUnwrap` on an Err", this, config);
878
+ }
879
+ function getEventId() {
880
+ return `evt_${nanoid(15)}`;
881
+ }
882
+ function stringifyBody(body) {
883
+ if (body === void 0) {
884
+ return void 0;
857
885
  }
858
- _unsafeUnwrapErr(_) {
859
- return this.error;
886
+ if (typeof body === "string") {
887
+ return body;
860
888
  }
861
- };
862
- var fromThrowable = Result.fromThrowable;
889
+ return JSON.stringify(body);
890
+ }
863
891
 
864
892
  // src/workflow-requests.ts
865
893
  import { QstashError as QstashError3 } from "@upstash/qstash";
@@ -952,6 +980,10 @@ var triggerFirstInvocation = async (params) => {
952
980
  return err(error_);
953
981
  }
954
982
  };
983
+ var flushPendingStep = async (workflowContext) => {
984
+ const { executor } = workflowContext;
985
+ return await executor.submitPendingStep();
986
+ };
955
987
  var triggerRouteFunction = async ({
956
988
  onCleanup,
957
989
  onStep,
@@ -1159,8 +1191,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1159
1191
  var BaseLazyStep = class _BaseLazyStep {
1160
1192
  stepName;
1161
1193
  context;
1162
- constructor(context, stepName) {
1194
+ /**
1195
+ * step-level settings (flow control, retries etc.) which override the
1196
+ * settings the workflow run was triggered with, for this step only.
1197
+ */
1198
+ stepSettings;
1199
+ /**
1200
+ * whether this step's result can be submitted after the route function
1201
+ * has moved on, instead of right away.
1202
+ *
1203
+ * Enabled for steps whose `getResultStep` produces the final `out` —
1204
+ * which is not the same as "needs no server round trip":
1205
+ * `LazyCreateWebhookStep` builds its result in `getBody` and returns
1206
+ * `out: undefined` from `getResultStep`, so deferring it would hand
1207
+ * `undefined` to the route function.
1208
+ *
1209
+ * Deferring lets the route function continue and reveal the next step,
1210
+ * so that step's settings can ride on this step's submission instead
1211
+ * of needing a step config request of their own.
1212
+ */
1213
+ supportsDeferredSubmission = false;
1214
+ constructor(context, stepName, stepSettings) {
1163
1215
  this.context = context;
1216
+ this.stepSettings = stepSettings;
1164
1217
  if (!stepName) {
1165
1218
  throw new WorkflowError(
1166
1219
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
@@ -1224,7 +1277,13 @@ var BaseLazyStep = class _BaseLazyStep {
1224
1277
  step.out = JSON.stringify(step.out);
1225
1278
  return JSON.stringify(step);
1226
1279
  }
1227
- getHeaders({ context, telemetry, invokeCount, step }) {
1280
+ getHeaders({
1281
+ context,
1282
+ telemetry,
1283
+ invokeCount,
1284
+ step,
1285
+ stepSettings
1286
+ }) {
1228
1287
  return getHeaders({
1229
1288
  initHeaderValue: "false",
1230
1289
  workflowConfig: {
@@ -1237,7 +1296,8 @@ var BaseLazyStep = class _BaseLazyStep {
1237
1296
  stepInfo: {
1238
1297
  step,
1239
1298
  lazyStep: this
1240
- }
1299
+ },
1300
+ stepSettings
1241
1301
  });
1242
1302
  }
1243
1303
  async submitStep({ context, body, headers }) {
@@ -1255,8 +1315,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
1255
1315
  stepFunction;
1256
1316
  stepType = "Run";
1257
1317
  allowUndefinedOut = true;
1258
- constructor(context, stepName, stepFunction) {
1259
- super(context, stepName);
1318
+ supportsDeferredSubmission = true;
1319
+ constructor(context, stepName, stepFunction, stepSettings) {
1320
+ super(context, stepName, stepSettings);
1260
1321
  this.stepFunction = stepFunction;
1261
1322
  }
1262
1323
  getPlanStep(concurrent, targetStep) {
@@ -1286,6 +1347,7 @@ var LazySleepStep = class extends BaseLazyStep {
1286
1347
  sleep;
1287
1348
  stepType = "SleepFor";
1288
1349
  allowUndefinedOut = true;
1350
+ supportsDeferredSubmission = true;
1289
1351
  constructor(context, stepName, sleep) {
1290
1352
  super(context, stepName);
1291
1353
  this.sleep = sleep;
@@ -1325,6 +1387,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1325
1387
  sleepUntil;
1326
1388
  stepType = "SleepUntil";
1327
1389
  allowUndefinedOut = true;
1390
+ supportsDeferredSubmission = true;
1328
1391
  constructor(context, stepName, sleepUntil) {
1329
1392
  super(context, stepName);
1330
1393
  this.sleepUntil = sleepUntil;
@@ -1817,6 +1880,7 @@ var WorkflowHeaders = class {
1817
1880
  invokeCount;
1818
1881
  initHeaderValue;
1819
1882
  stepInfo;
1883
+ stepSettings;
1820
1884
  headers;
1821
1885
  /**
1822
1886
  * @param params workflow header parameters
@@ -1826,17 +1890,20 @@ var WorkflowHeaders = class {
1826
1890
  workflowConfig,
1827
1891
  invokeCount,
1828
1892
  initHeaderValue,
1829
- stepInfo
1893
+ stepInfo,
1894
+ stepSettings
1830
1895
  }) {
1831
1896
  this.userHeaders = userHeaders;
1832
1897
  this.workflowConfig = workflowConfig;
1833
1898
  this.invokeCount = invokeCount;
1834
1899
  this.initHeaderValue = initHeaderValue;
1835
1900
  this.stepInfo = stepInfo;
1901
+ this.stepSettings = stepSettings;
1836
1902
  this.headers = {
1837
1903
  rawHeaders: {},
1838
1904
  workflowHeaders: {},
1839
- failureHeaders: {}
1905
+ failureHeaders: {},
1906
+ stepSettingsHeaders: {}
1840
1907
  };
1841
1908
  }
1842
1909
  getHeaders() {
@@ -1847,6 +1914,7 @@ var WorkflowHeaders = class {
1847
1914
  this.addUserHeaders();
1848
1915
  this.addInvokeCount();
1849
1916
  this.addFailureUrl();
1917
+ this.addStepSettings();
1850
1918
  const contentType = this.addContentType();
1851
1919
  return this.prefixHeaders(contentType);
1852
1920
  }
@@ -1856,7 +1924,7 @@ var WorkflowHeaders = class {
1856
1924
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1857
1925
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1858
1926
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1859
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1927
+ [WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
1860
1928
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1861
1929
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1862
1930
  };
@@ -1934,7 +2002,7 @@ var WorkflowHeaders = class {
1934
2002
  this.headers.failureHeaders["Workflow-Init"] = "false";
1935
2003
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1936
2004
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1937
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
2005
+ this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
1938
2006
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1939
2007
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1940
2008
  }
@@ -1942,6 +2010,42 @@ var WorkflowHeaders = class {
1942
2010
  this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
1943
2011
  }
1944
2012
  }
2013
+ /**
2014
+ * Applies the step-level settings of a step to the message.
2015
+ *
2016
+ * These headers configure the message itself, so that QStash uses them
2017
+ * instead of the settings the run was triggered with. The delivery of
2018
+ * the message is what executes the step, which is how a step's
2019
+ * settings reach it.
2020
+ *
2021
+ * When any setting is present the feature set is extended with
2022
+ * `WF_StepConfig`, which is what tells QStash to keep them. QStash
2023
+ * reports that back on the delivery as `Upstash-Workflow-Step-Config`.
2024
+ */
2025
+ addStepSettings() {
2026
+ if (!this.stepSettings) {
2027
+ return;
2028
+ }
2029
+ const headers = {};
2030
+ if (this.stepSettings.flowControl) {
2031
+ const { flowControlKey, flowControlValue } = prepareFlowControl(
2032
+ this.stepSettings.flowControl
2033
+ );
2034
+ headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
2035
+ headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
2036
+ }
2037
+ if (this.stepSettings.retries !== void 0) {
2038
+ headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
2039
+ }
2040
+ if (this.stepSettings.retryDelay) {
2041
+ headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
2042
+ }
2043
+ if (Object.keys(headers).length === 0) {
2044
+ return;
2045
+ }
2046
+ headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
2047
+ this.headers.stepSettingsHeaders = headers;
2048
+ }
1945
2049
  addContentType() {
1946
2050
  if (this.workflowConfig.useJSONContent) {
1947
2051
  this.headers.rawHeaders["content-type"] = "application/json";
@@ -1955,14 +2059,17 @@ var WorkflowHeaders = class {
1955
2059
  return contentType;
1956
2060
  }
1957
2061
  prefixHeaders(contentType) {
1958
- const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
2062
+ const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
1959
2063
  const isCall = this.stepInfo?.lazyStep.stepType === "Call";
1960
2064
  return {
1961
2065
  headers: {
1962
2066
  ...rawHeaders,
1963
2067
  ...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
1964
2068
  ...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
1965
- ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
2069
+ ...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
2070
+ // last, so they override the run's settings above. Never
2071
+ // prefixed: they configure this message, not its callback.
2072
+ ...stepSettingsHeaders
1966
2073
  },
1967
2074
  contentType
1968
2075
  };
@@ -2013,7 +2120,7 @@ var submitParallelSteps = async ({
2013
2120
  info: `Submitting ${planSteps.length} parallel steps.`
2014
2121
  });
2015
2122
  const result = await context.qstashClient.batch(
2016
- planSteps.map((planStep) => {
2123
+ planSteps.map((planStep, index) => {
2017
2124
  const { headers } = getHeaders({
2018
2125
  initHeaderValue: "false",
2019
2126
  workflowConfig: {
@@ -2021,7 +2128,8 @@ var submitParallelSteps = async ({
2021
2128
  workflowUrl: context.url,
2022
2129
  telemetry
2023
2130
  },
2024
- invokeCount
2131
+ invokeCount,
2132
+ stepSettings: steps[index].stepSettings
2025
2133
  });
2026
2134
  return {
2027
2135
  headers,
@@ -2040,31 +2148,38 @@ var submitParallelSteps = async ({
2040
2148
  }
2041
2149
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
2042
2150
  };
2043
- var submitSingleStep = async ({
2044
- context,
2151
+ var executeStep = async ({
2045
2152
  lazyStep,
2046
2153
  stepId,
2047
- invokeCount,
2048
2154
  concurrency,
2049
- telemetry,
2050
- dispatchDebug,
2051
2155
  dispatchLifecycle
2052
2156
  }) => {
2053
2157
  await dispatchLifecycle("beforeExecution", {
2054
2158
  stepName: lazyStep.stepName
2055
2159
  });
2056
- let resultStep;
2057
2160
  try {
2058
- resultStep = await lazyStep.getResultStep(concurrency, stepId);
2161
+ return await lazyStep.getResultStep(concurrency, stepId);
2059
2162
  } catch (error) {
2060
2163
  attachStepNameToError(error, lazyStep.stepName);
2061
2164
  throw error;
2062
2165
  }
2166
+ };
2167
+ var submitStepResult = async ({
2168
+ context,
2169
+ lazyStep,
2170
+ resultStep,
2171
+ invokeCount,
2172
+ concurrency,
2173
+ telemetry,
2174
+ dispatchDebug,
2175
+ nextStepSettings
2176
+ }) => {
2063
2177
  const { headers } = lazyStep.getHeaders({
2064
2178
  context,
2065
2179
  step: resultStep,
2066
2180
  invokeCount,
2067
- telemetry
2181
+ telemetry,
2182
+ stepSettings: nextStepSettings
2068
2183
  });
2069
2184
  const body = lazyStep.getBody({
2070
2185
  context,
@@ -2089,6 +2204,138 @@ var submitSingleStep = async ({
2089
2204
  }
2090
2205
  return resultStep;
2091
2206
  };
2207
+ var publishStepConfigRequest = async ({
2208
+ context,
2209
+ lazyStep,
2210
+ targetStep,
2211
+ invokeCount,
2212
+ telemetry,
2213
+ dispatchDebug
2214
+ }) => {
2215
+ const { headers } = getHeaders({
2216
+ initHeaderValue: "false",
2217
+ workflowConfig: {
2218
+ workflowRunId: context.workflowRunId,
2219
+ workflowUrl: context.url,
2220
+ telemetry
2221
+ },
2222
+ invokeCount,
2223
+ stepSettings: lazyStep.stepSettings
2224
+ });
2225
+ await dispatchDebug("onInfo", {
2226
+ info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
2227
+ });
2228
+ const result = await context.qstashClient.publishJSON({
2229
+ headers: {
2230
+ ...headers,
2231
+ "Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
2232
+ },
2233
+ method: "POST",
2234
+ body: { targetStep, invokeCount },
2235
+ url: context.url,
2236
+ contentBasedDeduplication: true
2237
+ });
2238
+ if (result?.deduplicated) {
2239
+ await dispatchDebug("onWarning", {
2240
+ 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.`
2241
+ });
2242
+ } else if (result?.messageId) {
2243
+ await dispatchDebug("onInfo", {
2244
+ info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
2245
+ });
2246
+ }
2247
+ };
2248
+
2249
+ // src/qstash/step-config.ts
2250
+ var DEFAULT_PARALLELISM = 0;
2251
+ var DEFAULT_RATE = 0;
2252
+ var DEFAULT_PERIOD_SECONDS = 1;
2253
+ var SECONDS_PER_UNIT = {
2254
+ s: 1,
2255
+ m: 60,
2256
+ h: 60 * 60,
2257
+ d: 24 * 60 * 60
2258
+ };
2259
+ var durationToSeconds = (duration) => {
2260
+ if (typeof duration === "number") {
2261
+ return Number.isFinite(duration) ? duration : void 0;
2262
+ }
2263
+ const match = /^(\d+)([smhd])$/.exec(duration.trim());
2264
+ if (match) {
2265
+ return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
2266
+ }
2267
+ const seconds = Number(duration.trim());
2268
+ return Number.isFinite(seconds) ? seconds : void 0;
2269
+ };
2270
+ var normalizeFlowControl = (flowControl) => {
2271
+ const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
2272
+ return {
2273
+ key: flowControl.key,
2274
+ parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
2275
+ rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
2276
+ period: period ?? DEFAULT_PERIOD_SECONDS
2277
+ };
2278
+ };
2279
+ var parseFlowControlHeaders = (key, value) => {
2280
+ if (!key) {
2281
+ return void 0;
2282
+ }
2283
+ const flowControl = {
2284
+ key,
2285
+ parallelism: DEFAULT_PARALLELISM,
2286
+ rate: DEFAULT_RATE,
2287
+ period: DEFAULT_PERIOD_SECONDS
2288
+ };
2289
+ for (const entry of (value ?? "").split(",")) {
2290
+ const [name, rawValue] = entry.split("=").map((part) => part.trim());
2291
+ if (!name || rawValue === void 0) {
2292
+ continue;
2293
+ }
2294
+ switch (name) {
2295
+ case "parallelism": {
2296
+ flowControl.parallelism = Number(rawValue);
2297
+ break;
2298
+ }
2299
+ case "rate": {
2300
+ flowControl.rate = Number(rawValue);
2301
+ break;
2302
+ }
2303
+ case "period": {
2304
+ flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
2305
+ break;
2306
+ }
2307
+ }
2308
+ }
2309
+ return flowControl;
2310
+ };
2311
+ var getEffectiveConfig = (headers) => {
2312
+ const retriesHeader = headers.get(RETRIES_HEADER);
2313
+ return {
2314
+ flowControl: parseFlowControlHeaders(
2315
+ headers.get(FLOW_CONTROL_KEY_HEADER),
2316
+ headers.get(FLOW_CONTROL_VALUE_HEADER)
2317
+ ),
2318
+ retries: retriesHeader === null ? void 0 : Number(retriesHeader),
2319
+ retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
2320
+ hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
2321
+ };
2322
+ };
2323
+ var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
2324
+ if (stepSettings.flowControl) {
2325
+ const wanted = normalizeFlowControl(stepSettings.flowControl);
2326
+ const applied = effectiveConfig.flowControl;
2327
+ if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
2328
+ return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
2329
+ }
2330
+ }
2331
+ if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
2332
+ return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
2333
+ }
2334
+ if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
2335
+ return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
2336
+ }
2337
+ return void 0;
2338
+ };
2092
2339
 
2093
2340
  // src/context/auto-executor.ts
2094
2341
  var AutoExecutor = class _AutoExecutor {
@@ -2105,6 +2352,25 @@ var AutoExecutor = class _AutoExecutor {
2105
2352
  stepCount = 0;
2106
2353
  planStepCount = 0;
2107
2354
  executingStep = false;
2355
+ /**
2356
+ * an executed step whose result hasn't been submitted to QStash yet.
2357
+ *
2358
+ * Set when a step which can produce its result in-process executes. The
2359
+ * submission waits so that the route function can continue and reveal
2360
+ * what comes next: if that is a single step with step-level settings,
2361
+ * the settings ride on this submission and no step config request is
2362
+ * needed, since the delivery of the submission is what executes it.
2363
+ *
2364
+ * Flushed when the next step is reached (see `addStep`) or when the
2365
+ * route function ends (see `flushPendingStep` in `serve`).
2366
+ */
2367
+ pendingStep;
2368
+ /**
2369
+ * configuration QStash applied to the delivery being handled. A step
2370
+ * with step-level settings is settled by comparing its settings against
2371
+ * this.
2372
+ */
2373
+ effectiveConfig;
2108
2374
  /**
2109
2375
  * @param context workflow context
2110
2376
  * @param steps list of steps
@@ -2112,14 +2378,17 @@ var AutoExecutor = class _AutoExecutor {
2112
2378
  * @param dispatchLifecycle lifecycle event dispatcher
2113
2379
  * @param telemetry optional telemetry information
2114
2380
  * @param invokeCount optional invoke count
2381
+ * @param effectiveConfig configuration QStash applied to the delivery
2382
+ * being handled
2115
2383
  */
2116
- constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
2384
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount, effectiveConfig) {
2117
2385
  this.context = context;
2118
2386
  this.steps = steps;
2119
2387
  this.dispatchDebug = dispatchDebug;
2120
2388
  this.dispatchLifecycle = dispatchLifecycle;
2121
2389
  this.telemetry = telemetry;
2122
2390
  this.invokeCount = invokeCount ?? 0;
2391
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2123
2392
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2124
2393
  }
2125
2394
  /**
@@ -2154,6 +2423,15 @@ var AutoExecutor = class _AutoExecutor {
2154
2423
  lazyStepList.push(stepInfo);
2155
2424
  const index = this.indexInCurrentList++;
2156
2425
  const requestComplete = this.deferExecution().then(async () => {
2426
+ const submitted = await this.submitPendingStep(
2427
+ lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
2428
+ );
2429
+ if (submitted.isErr()) {
2430
+ throw submitted.error;
2431
+ }
2432
+ if (submitted.value.result === "submitted-step") {
2433
+ throw submitted.value.abort;
2434
+ }
2157
2435
  if (!this.promises.has(lazyStepList)) {
2158
2436
  const promise2 = this.getExecutionPromise(lazyStepList);
2159
2437
  this.promises.set(lazyStepList, promise2);
@@ -2207,18 +2485,100 @@ var AutoExecutor = class _AutoExecutor {
2207
2485
  }
2208
2486
  return parsedOut;
2209
2487
  }
2210
- const resultStep = await submitSingleStep({
2211
- context: this.context,
2488
+ const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
2489
+ if (mismatch) {
2490
+ if (!this.effectiveConfig.hasStepConfig) {
2491
+ await publishStepConfigRequest({
2492
+ context: this.context,
2493
+ lazyStep,
2494
+ targetStep: this.stepCount,
2495
+ invokeCount: this.invokeCount,
2496
+ telemetry: this.telemetry,
2497
+ dispatchDebug: this.dispatchDebug
2498
+ });
2499
+ throw new WorkflowAbort(lazyStep.stepName);
2500
+ } else {
2501
+ await this.dispatchDebug("onWarning", {
2502
+ 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.`
2503
+ });
2504
+ }
2505
+ }
2506
+ const resultStep = await executeStep({
2212
2507
  lazyStep,
2213
2508
  stepId: this.stepCount,
2509
+ concurrency: NO_CONCURRENCY,
2510
+ dispatchLifecycle: this.dispatchLifecycle
2511
+ });
2512
+ if (lazyStep.supportsDeferredSubmission) {
2513
+ this.pendingStep = { status: "held", lazyStep, resultStep };
2514
+ return lazyStep.parseOut({
2515
+ ...resultStep,
2516
+ out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
2517
+ });
2518
+ }
2519
+ await submitStepResult({
2520
+ context: this.context,
2521
+ lazyStep,
2522
+ resultStep,
2214
2523
  invokeCount: this.invokeCount,
2215
- concurrency: 1,
2524
+ concurrency: NO_CONCURRENCY,
2216
2525
  telemetry: this.telemetry,
2217
- dispatchDebug: this.dispatchDebug,
2218
- dispatchLifecycle: this.dispatchLifecycle
2526
+ dispatchDebug: this.dispatchDebug
2219
2527
  });
2220
2528
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2221
2529
  }
2530
+ /**
2531
+ * Submits the result of the step being held, if there is one, and
2532
+ * returns the abort the caller has to throw to end the invocation.
2533
+ *
2534
+ * Returns undefined when no step is being held, which is the caller's
2535
+ * signal that there is nothing to end. Throws instead of returning
2536
+ * when the submission itself fails, so that error reaches the caller
2537
+ * rather than an abort claiming the step was submitted.
2538
+ *
2539
+ * Callers which arrive while a submission is already under way (the
2540
+ * steps of a parallel group, added together) wait for that one and get
2541
+ * the same abort, rather than submitting a second time.
2542
+ *
2543
+ * The next step's settings are attached whenever it has any, without
2544
+ * checking them against the current delivery: the executor only knows
2545
+ * the configuration of the delivery in hand, which inside a
2546
+ * delivery is the *previous* step's, so such a check would be wrong
2547
+ * exactly when two steps with settings follow each other.
2548
+ *
2549
+ * @param nextStepSettings step-level settings of the next step
2550
+ * @returns `submitted-step` with the abort which ends this invocation,
2551
+ * or `no-pending-step` when nothing was held and there is nothing to
2552
+ * end
2553
+ */
2554
+ async submitPendingStep(nextStepSettings) {
2555
+ if (!this.pendingStep) {
2556
+ return ok({ result: "no-pending-step" });
2557
+ }
2558
+ try {
2559
+ if (this.pendingStep.status === "submitting") {
2560
+ return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
2561
+ }
2562
+ const { lazyStep, resultStep } = this.pendingStep;
2563
+ const submitted = (async () => {
2564
+ await submitStepResult({
2565
+ context: this.context,
2566
+ lazyStep,
2567
+ resultStep,
2568
+ invokeCount: this.invokeCount,
2569
+ concurrency: NO_CONCURRENCY,
2570
+ telemetry: this.telemetry,
2571
+ dispatchDebug: this.dispatchDebug,
2572
+ nextStepSettings
2573
+ });
2574
+ return new WorkflowAbort(lazyStep.stepName, resultStep);
2575
+ })();
2576
+ this.pendingStep = { status: "submitting", submitted };
2577
+ return ok({ result: "submitted-step", abort: await submitted });
2578
+ } catch (error) {
2579
+ return err(error);
2580
+ }
2581
+ }
2222
2582
  /**
2223
2583
  * Runs steps in parallel.
2224
2584
  *
@@ -2267,15 +2627,20 @@ var AutoExecutor = class _AutoExecutor {
2267
2627
  validateStep(parallelSteps[stepIndex], planStep);
2268
2628
  try {
2269
2629
  const parallelStep = parallelSteps[stepIndex];
2270
- const resultStep = await submitSingleStep({
2271
- context: this.context,
2630
+ const resultStep = await executeStep({
2272
2631
  lazyStep: parallelStep,
2273
2632
  stepId: planStep.targetStep,
2633
+ concurrency: parallelSteps.length,
2634
+ dispatchLifecycle: this.dispatchLifecycle
2635
+ });
2636
+ await submitStepResult({
2637
+ context: this.context,
2638
+ lazyStep: parallelStep,
2639
+ resultStep,
2274
2640
  invokeCount: this.invokeCount,
2275
2641
  concurrency: parallelSteps.length,
2276
2642
  telemetry: this.telemetry,
2277
- dispatchDebug: this.dispatchDebug,
2278
- dispatchLifecycle: this.dispatchLifecycle
2643
+ dispatchDebug: this.dispatchDebug
2279
2644
  });
2280
2645
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2281
2646
  } catch (error) {
@@ -2816,6 +3181,46 @@ var WorkflowContext = class {
2816
3181
  * `1` on the first retry, `2` on the second, and so on.
2817
3182
  */
2818
3183
  retried;
3184
+ /**
3185
+ * Configuration QStash applied to the request being handled.
3186
+ *
3187
+ * This is the configuration of *this delivery*, not of the run: it is
3188
+ * the configuration the run was triggered with, except inside the
3189
+ * delivery which executes a step that has step-level settings, where
3190
+ * it is that step's settings. The fields below expose it; the executor
3191
+ * gets it directly rather than reaching through the context.
3192
+ */
3193
+ effectiveConfig;
3194
+ /**
3195
+ * Flow control QStash applied to the request being handled, if any.
3196
+ *
3197
+ * This is the flow control of the request in hand, which is the one the
3198
+ * run was triggered with except inside a step carrying its own.
3199
+ */
3200
+ get flowControl() {
3201
+ return this.effectiveConfig.flowControl;
3202
+ }
3203
+ /**
3204
+ * Retry limit QStash applied to the request being handled, if it
3205
+ * reported one.
3206
+ *
3207
+ * Not to be confused with {@link retried}, which is how many retries
3208
+ * have already happened.
3209
+ *
3210
+ * @see {@link flowControl} for what "the request being handled" means
3211
+ */
3212
+ get retries() {
3213
+ return this.effectiveConfig.retries;
3214
+ }
3215
+ /**
3216
+ * Retry delay expression QStash applied to the request being handled,
3217
+ * if any.
3218
+ *
3219
+ * @see {@link flowControl} for what "the request being handled" means
3220
+ */
3221
+ get retryDelay() {
3222
+ return this.effectiveConfig.retryDelay;
3223
+ }
2819
3224
  constructor({
2820
3225
  qstashClient,
2821
3226
  workflowRunId,
@@ -2829,7 +3234,8 @@ var WorkflowContext = class {
2829
3234
  invokeCount,
2830
3235
  label,
2831
3236
  retried,
2832
- middlewareManager
3237
+ middlewareManager,
3238
+ effectiveConfig
2833
3239
  }) {
2834
3240
  this.qstashClient = qstashClient;
2835
3241
  this.workflowRunId = workflowRunId;
@@ -2841,6 +3247,7 @@ var WorkflowContext = class {
2841
3247
  this.env = env ?? {};
2842
3248
  this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
2843
3249
  this.retried = retried ?? 0;
3250
+ this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
2844
3251
  const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2845
3252
  middlewareManagerInstance.assignContext(this);
2846
3253
  this.executor = new AutoExecutor(
@@ -2849,7 +3256,8 @@ var WorkflowContext = class {
2849
3256
  middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2850
3257
  middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2851
3258
  telemetry,
2852
- invokeCount
3259
+ invokeCount,
3260
+ this.effectiveConfig
2853
3261
  );
2854
3262
  }
2855
3263
  /**
@@ -2875,13 +3283,35 @@ var WorkflowContext = class {
2875
3283
  * ])
2876
3284
  * ```
2877
3285
  *
3286
+ * Step-level settings can be passed as a third argument, overriding
3287
+ * the settings the workflow run was triggered with for this step only:
3288
+ *
3289
+ * ```typescript
3290
+ * const result = await context.run(
3291
+ * "step 1",
3292
+ * () => {
3293
+ * return "result"
3294
+ * },
3295
+ * {
3296
+ * flowControl: { key: "custom-key", parallelism: 3 },
3297
+ * retries: 5,
3298
+ * }
3299
+ * )
3300
+ * ```
3301
+ *
2878
3302
  * @param stepName name of the step
2879
3303
  * @param stepFunction step function to be executed
3304
+ * @param stepSettings step-level settings for this step
2880
3305
  * @returns result of the step function
2881
3306
  */
2882
- async run(stepName, stepFunction) {
3307
+ async run(stepName, stepFunction, stepSettings) {
3308
+ if (stepSettings) {
3309
+ validateFlowControl(stepSettings.flowControl);
3310
+ }
2883
3311
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2884
- return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
3312
+ return await this.addStep(
3313
+ new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
3314
+ );
2885
3315
  }
2886
3316
  /**
2887
3317
  * Stops the execution for the duration provided.
@@ -3147,6 +3577,9 @@ var loggingMiddleware = new WorkflowMiddleware({
3147
3577
  }
3148
3578
  });
3149
3579
 
3580
+ // src/serve/index.ts
3581
+ import { startDevServer } from "@upstash/qstash";
3582
+
3150
3583
  // src/serve/authorization.ts
3151
3584
  import { Client as Client2 } from "@upstash/qstash";
3152
3585
  var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
@@ -3178,7 +3611,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3178
3611
  *
3179
3612
  * @param routeFunction
3180
3613
  */
3181
- static async tryAuthentication(routeFunction, context) {
3614
+ static async tryAuthentication(routeFunction, context, effectiveConfig) {
3182
3615
  const disabledContext = new _DisabledWorkflowContext({
3183
3616
  qstashClient: new Client2({
3184
3617
  baseUrl: "disabled-client",
@@ -3193,7 +3626,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3193
3626
  initialPayload: context.requestPayload,
3194
3627
  env: context.env,
3195
3628
  label: context.labels,
3196
- retried: context.retried
3629
+ retried: context.retried,
3630
+ // the route function runs for real here until it reaches a step, so
3631
+ // it observes the same configuration as it will on the real context
3632
+ effectiveConfig
3197
3633
  });
3198
3634
  try {
3199
3635
  await routeFunction(disabledContext);
@@ -3410,6 +3846,7 @@ var handleFailure = async ({
3410
3846
  }
3411
3847
  const userHeaders = recreateUserHeaders(request.headers);
3412
3848
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3849
+ const effectiveConfig = getEffectiveConfig(request.headers);
3413
3850
  const workflowContext = new WorkflowContext({
3414
3851
  qstashClient,
3415
3852
  workflowRunId,
@@ -3423,11 +3860,13 @@ var handleFailure = async ({
3423
3860
  label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3424
3861
  retried,
3425
3862
  workflowRunCreatedAt: workflowCreatedAt,
3426
- middlewareManager: void 0
3863
+ middlewareManager: void 0,
3864
+ effectiveConfig
3427
3865
  });
3428
3866
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3429
3867
  routeFunction,
3430
- workflowContext
3868
+ workflowContext,
3869
+ effectiveConfig
3431
3870
  );
3432
3871
  if (authCheck.isErr()) {
3433
3872
  await dispatchDebug?.("onError", {
@@ -3519,7 +3958,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3519
3958
  const client = new Client3({
3520
3959
  ...clientOptions,
3521
3960
  baseUrl: clientEnv.QSTASH_URL,
3522
- token: clientEnv.QSTASH_TOKEN
3961
+ token: clientEnv.QSTASH_TOKEN,
3962
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3963
+ // the client at the local dev server too. Without this the QStash Client
3964
+ // re-derives dev mode from its own process.env, which is blind to the
3965
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3966
+ // An explicit devMode in the user's client config still wins.
3967
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3523
3968
  });
3524
3969
  const receiver = getReceiver(environment, receiverConfig, region);
3525
3970
  return { client, receiver };
@@ -3569,7 +4014,9 @@ var getQStashHandlers = ({
3569
4014
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new Client3({
3570
4015
  ...qstashClientOption,
3571
4016
  baseUrl: environment.QSTASH_URL,
3572
- token: environment.QSTASH_TOKEN
4017
+ token: environment.QSTASH_TOKEN,
4018
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
4019
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3573
4020
  }),
3574
4021
  receiver: getReceiver(environment, receiverConfig)
3575
4022
  }
@@ -3835,12 +4282,14 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3835
4282
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3836
4283
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3837
4284
  const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4285
+ const effectiveConfig = getEffectiveConfig(request.headers);
3838
4286
  const workflowContext = new WorkflowContext({
3839
4287
  qstashClient: regionalClient,
3840
4288
  workflowRunId,
3841
4289
  initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
3842
4290
  headers: recreateUserHeaders(request.headers),
3843
4291
  steps,
4292
+ effectiveConfig,
3844
4293
  url: workflowUrl,
3845
4294
  env,
3846
4295
  telemetry,
@@ -3852,7 +4301,8 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3852
4301
  });
3853
4302
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3854
4303
  routeFunction,
3855
- workflowContext
4304
+ workflowContext,
4305
+ effectiveConfig
3856
4306
  );
3857
4307
  if (authCheck.isErr()) {
3858
4308
  throw authCheck.error;
@@ -3889,7 +4339,23 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3889
4339
  if (steps.length === 1) {
3890
4340
  await middlewareManager.dispatchLifecycle("runStarted", {});
3891
4341
  }
3892
- return await routeFunction(workflowContext);
4342
+ let outcome;
4343
+ try {
4344
+ outcome = { ran: true, result: await routeFunction(workflowContext) };
4345
+ } catch (error) {
4346
+ outcome = { ran: false, error };
4347
+ }
4348
+ const submitted = await flushPendingStep(workflowContext);
4349
+ if (submitted.isErr()) {
4350
+ throw submitted.error;
4351
+ }
4352
+ if (submitted.value.result === "submitted-step") {
4353
+ throw submitted.value.abort;
4354
+ }
4355
+ if (!outcome.ran) {
4356
+ throw outcome.error;
4357
+ }
4358
+ return outcome.result;
3893
4359
  },
3894
4360
  onCleanup: async (result2) => {
3895
4361
  await middlewareManager.dispatchLifecycle("runCompleted", {
@@ -3969,6 +4435,7 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3969
4435
  return { handler: safeHandler };
3970
4436
  };
3971
4437
  var serve = (routeFunction, options) => {
4438
+ void startDevServer();
3972
4439
  return serveBase(
3973
4440
  routeFunction,
3974
4441
  {
@@ -3984,6 +4451,9 @@ export {
3984
4451
  WorkflowAbort,
3985
4452
  WorkflowNonRetryableError,
3986
4453
  WorkflowRetryAfterError,
4454
+ isInstanceOf,
4455
+ assertNonEmptyId,
4456
+ toNonEmptyIdArray,
3987
4457
  makeNotifyRequest,
3988
4458
  makeGetWaitersRequest,
3989
4459
  normalizeCursor,