@upstash/workflow 0.3.0-rc → 0.3.0-rc1

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.
package/nextjs.js CHANGED
@@ -41,26 +41,36 @@ var WorkflowError = class extends import_qstash.QstashError {
41
41
  }
42
42
  };
43
43
  var WorkflowAbort = class extends Error {
44
- stepInfo;
45
44
  stepName;
45
+ stepInfo;
46
46
  /**
47
- * whether workflow is to be canceled on abort
48
- */
49
- cancelWorkflow;
50
- /**
51
- *
52
47
  * @param stepName name of the aborting step
53
48
  * @param stepInfo step information
54
- * @param cancelWorkflow
55
49
  */
56
- constructor(stepName, stepInfo, cancelWorkflow = false) {
50
+ constructor(stepName, stepInfo) {
57
51
  super(
58
52
  `This is an Upstash Workflow error thrown after a step executes. It is expected to be raised. Make sure that you await for each step. Also, if you are using try/catch blocks, you should not wrap context.run/sleep/sleepUntil/call methods with try/catch. Aborting workflow after executing step '${stepName}'.`
59
53
  );
60
54
  this.name = "WorkflowAbort";
61
55
  this.stepName = stepName;
62
56
  this.stepInfo = stepInfo;
63
- this.cancelWorkflow = cancelWorkflow;
57
+ }
58
+ };
59
+ var WorkflowAuthError = class extends WorkflowAbort {
60
+ /**
61
+ * @param stepName name of the step found during authorization
62
+ */
63
+ constructor(stepName) {
64
+ super(stepName);
65
+ this.name = "WorkflowAuthError";
66
+ this.message = `This is an Upstash Workflow error thrown during authorization check. Found step '${stepName}' during dry-run.`;
67
+ }
68
+ };
69
+ var WorkflowCancelAbort = class extends WorkflowAbort {
70
+ constructor() {
71
+ super("cancel");
72
+ this.name = "WorkflowCancelAbort";
73
+ this.message = "Workflow has been canceled by user via context.cancel().";
64
74
  }
65
75
  };
66
76
  var WorkflowNonRetryableError = class extends WorkflowAbort {
@@ -68,22 +78,22 @@ var WorkflowNonRetryableError = class extends WorkflowAbort {
68
78
  * @param message error message to be displayed
69
79
  */
70
80
  constructor(message) {
71
- super("fail", void 0, false);
81
+ super("non-retryable-error");
72
82
  this.name = "WorkflowNonRetryableError";
73
- if (message) this.message = message;
83
+ this.message = message ?? "Workflow failed with non-retryable error.";
74
84
  }
75
85
  };
76
86
  var WorkflowRetryAfterError = class extends WorkflowAbort {
77
87
  retryAfter;
78
88
  /**
79
- * @param retryAfter time in seconds after which the workflow should be retried
80
89
  * @param message error message to be displayed
90
+ * @param retryAfter time in seconds after which the workflow should be retried
81
91
  */
82
92
  constructor(message, retryAfter) {
83
- super("retry", void 0, false);
93
+ super("retry-after-error");
84
94
  this.name = "WorkflowRetryAfterError";
95
+ this.message = message;
85
96
  this.retryAfter = retryAfter;
86
- if (message) this.message = message;
87
97
  }
88
98
  };
89
99
  var formatWorkflowError = (error) => {
@@ -135,15 +145,21 @@ var makeCancelRequest = async (requester, workflowRunId) => {
135
145
  });
136
146
  return true;
137
147
  };
138
- var getSteps = async (requester, workflowRunId, messageId, debug) => {
148
+ var getSteps = async (requester, workflowRunId, messageId, dispatchDebug) => {
139
149
  try {
140
150
  const steps = await requester.request({
141
151
  path: ["v2", "workflows", "runs", workflowRunId],
142
152
  parseResponseAsJson: true
143
153
  });
154
+ if (steps.length === 1) {
155
+ return {
156
+ steps,
157
+ workflowRunEnded: false
158
+ };
159
+ }
144
160
  if (!messageId) {
145
- await debug?.log("INFO", "ENDPOINT_START", {
146
- message: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
161
+ await dispatchDebug?.("onInfo", {
162
+ info: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
147
163
  });
148
164
  return { steps, workflowRunEnded: false };
149
165
  } else {
@@ -152,16 +168,15 @@ var getSteps = async (requester, workflowRunId, messageId, debug) => {
152
168
  return { steps: [], workflowRunEnded: false };
153
169
  }
154
170
  const filteredSteps = steps.slice(0, index + 1);
155
- await debug?.log("INFO", "ENDPOINT_START", {
156
- message: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
171
+ await dispatchDebug?.("onInfo", {
172
+ info: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
157
173
  });
158
174
  return { steps: filteredSteps, workflowRunEnded: false };
159
175
  }
160
176
  } catch (error) {
161
177
  if (isInstanceOf(error, import_qstash2.QstashError) && error.status === 404) {
162
- await debug?.log("WARN", "ENDPOINT_START", {
163
- message: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed.",
164
- error
178
+ await dispatchDebug?.("onWarning", {
179
+ warning: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed."
165
180
  });
166
181
  return { steps: void 0, workflowRunEnded: true };
167
182
  } else {
@@ -175,15 +190,18 @@ var WORKFLOW_ID_HEADER = "Upstash-Workflow-RunId";
175
190
  var WORKFLOW_INIT_HEADER = "Upstash-Workflow-Init";
176
191
  var WORKFLOW_URL_HEADER = "Upstash-Workflow-Url";
177
192
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
193
+ var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
178
194
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
179
195
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
180
196
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
197
+ var WORKFLOW_UNKOWN_SDK_VERSION_HEADER = "Upstash-Workflow-Unknown-Sdk";
198
+ var WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER = "upstash-workflow-trigger-by-sdk";
181
199
  var WORKFLOW_PROTOCOL_VERSION = "1";
182
200
  var WORKFLOW_PROTOCOL_VERSION_HEADER = "Upstash-Workflow-Sdk-Version";
183
201
  var DEFAULT_CONTENT_TYPE = "application/json";
184
202
  var NO_CONCURRENCY = 1;
185
203
  var DEFAULT_RETRIES = 3;
186
- var VERSION = "v0.3.0-rc";
204
+ var VERSION = "v1.0.0";
187
205
  var SDK_TELEMETRY = `@upstash/workflow@${VERSION}`;
188
206
  var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
189
207
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
@@ -201,8 +219,8 @@ var NANOID_LENGTH = 21;
201
219
  function getRandomInt() {
202
220
  return Math.floor(Math.random() * NANOID_CHARS.length);
203
221
  }
204
- function nanoid() {
205
- return Array.from({ length: NANOID_LENGTH }).map(() => NANOID_CHARS[getRandomInt()]).join("");
222
+ function nanoid(length = NANOID_LENGTH) {
223
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
206
224
  }
207
225
  function getWorkflowRunId(id) {
208
226
  return `wfr_${id ?? nanoid()}`;
@@ -219,6 +237,46 @@ function decodeBase64(base64) {
219
237
  return binString;
220
238
  }
221
239
  }
240
+ function getUserIdFromToken(qstashClient) {
241
+ try {
242
+ const token = qstashClient.token;
243
+ const decodedToken = decodeBase64(token);
244
+ const tokenPayload = JSON.parse(decodedToken);
245
+ const userId = tokenPayload.UserID;
246
+ if (!userId) {
247
+ throw new WorkflowError("QStash token payload does not contain userId");
248
+ }
249
+ return userId;
250
+ } catch (error) {
251
+ throw new WorkflowError(
252
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
253
+ );
254
+ }
255
+ }
256
+ function getQStashUrl(qstashClient) {
257
+ try {
258
+ const requester = qstashClient.http;
259
+ const baseUrl = requester.baseUrl;
260
+ if (!baseUrl) {
261
+ throw new WorkflowError("QStash client does not have a baseUrl");
262
+ }
263
+ return baseUrl;
264
+ } catch (error) {
265
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
266
+ }
267
+ }
268
+ function getEventId() {
269
+ return `evt_${nanoid(15)}`;
270
+ }
271
+ function stringifyBody(body) {
272
+ if (body === void 0) {
273
+ return void 0;
274
+ }
275
+ if (typeof body === "string") {
276
+ return body;
277
+ }
278
+ return JSON.stringify(body);
279
+ }
222
280
 
223
281
  // node_modules/neverthrow/dist/index.es.js
224
282
  var defaultErrorConfig = {
@@ -644,7 +702,9 @@ var StepTypes = [
644
702
  "Call",
645
703
  "Wait",
646
704
  "Notify",
647
- "Invoke"
705
+ "Invoke",
706
+ "CreateWebhook",
707
+ "WaitForWebhook"
648
708
  ];
649
709
 
650
710
  // src/workflow-requests.ts
@@ -660,23 +720,26 @@ var triggerFirstInvocation = async (params) => {
660
720
  invokeCount,
661
721
  delay,
662
722
  notBefore,
663
- keepTriggerConfig
723
+ failureUrl,
724
+ retries,
725
+ retryDelay,
726
+ flowControl,
727
+ unknownSdk
664
728
  }) => {
665
729
  const { headers } = getHeaders({
666
730
  initHeaderValue: "true",
667
731
  workflowConfig: {
668
732
  workflowRunId: workflowContext.workflowRunId,
669
733
  workflowUrl: workflowContext.url,
670
- failureUrl: workflowContext.failureUrl,
671
- retries: workflowContext.retries,
672
- retryDelay: workflowContext.retryDelay,
734
+ failureUrl,
735
+ retries,
736
+ retryDelay,
673
737
  telemetry,
674
- flowControl: workflowContext.flowControl,
738
+ flowControl,
675
739
  useJSONContent: useJSONContent ?? false
676
740
  },
677
741
  invokeCount: invokeCount ?? 0,
678
- userHeaders: workflowContext.headers,
679
- keepTriggerConfig
742
+ userHeaders: workflowContext.headers
680
743
  });
681
744
  if (workflowContext.headers.get("content-type")) {
682
745
  headers["content-type"] = workflowContext.headers.get("content-type");
@@ -684,6 +747,9 @@ var triggerFirstInvocation = async (params) => {
684
747
  if (useJSONContent) {
685
748
  headers["content-type"] = "application/json";
686
749
  }
750
+ if (unknownSdk) {
751
+ headers[WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER] = "true";
752
+ }
687
753
  if (workflowContext.label) {
688
754
  headers[WORKFLOW_LABEL_HEADER] = workflowContext.label;
689
755
  }
@@ -704,21 +770,15 @@ var triggerFirstInvocation = async (params) => {
704
770
  for (let i = 0; i < results.length; i++) {
705
771
  const result = results[i];
706
772
  const invocationParams = firstInvocationParams[i];
773
+ invocationParams.middlewareManager?.assignContext(invocationParams.workflowContext);
707
774
  if (result.deduplicated) {
708
- await invocationParams.debug?.log("WARN", "SUBMIT_FIRST_INVOCATION", {
709
- message: `Workflow run ${invocationParams.workflowContext.workflowRunId} already exists. A new one isn't created.`,
710
- headers: invocationBatch[i].headers,
711
- requestPayload: invocationParams.workflowContext.requestPayload,
712
- url: invocationParams.workflowContext.url,
713
- messageId: result.messageId
775
+ await invocationParams.middlewareManager?.dispatchDebug("onWarning", {
776
+ warning: `Workflow run ${invocationParams.workflowContext.workflowRunId} already exists. A new one isn't created.`
714
777
  });
715
778
  invocationStatuses.push("workflow-run-already-exists");
716
779
  } else {
717
- await invocationParams.debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
718
- headers: invocationBatch[i].headers,
719
- requestPayload: invocationParams.workflowContext.requestPayload,
720
- url: invocationParams.workflowContext.url,
721
- messageId: result.messageId
780
+ await invocationParams.middlewareManager?.dispatchDebug("onInfo", {
781
+ info: `Workflow run started successfully with URL ${invocationParams.workflowContext.url}.`
722
782
  });
723
783
  invocationStatuses.push("success");
724
784
  }
@@ -740,7 +800,7 @@ var triggerRouteFunction = async ({
740
800
  onCleanup,
741
801
  onStep,
742
802
  onCancel,
743
- debug
803
+ middlewareManager
744
804
  }) => {
745
805
  try {
746
806
  const result = await onStep();
@@ -749,27 +809,25 @@ var triggerRouteFunction = async ({
749
809
  } catch (error) {
750
810
  const error_ = error;
751
811
  if (isInstanceOf(error, import_qstash3.QstashError) && error.status === 400) {
752
- await debug?.log("WARN", "RESPONSE_WORKFLOW", {
753
- message: `tried to append to a cancelled workflow. exiting without publishing.`,
754
- name: error.name,
755
- errorMessage: error.message
812
+ await middlewareManager?.dispatchDebug("onWarning", {
813
+ warning: `Tried to append to a cancelled workflow. Exiting without publishing. Error: ${error.message}`
756
814
  });
757
815
  return ok("workflow-was-finished");
758
816
  } else if (isInstanceOf(error_, WorkflowNonRetryableError) || isInstanceOf(error_, WorkflowRetryAfterError)) {
759
817
  return ok(error_);
760
- } else if (!isInstanceOf(error_, WorkflowAbort)) {
761
- return err(error_);
762
- } else if (error_.cancelWorkflow) {
818
+ } else if (isInstanceOf(error_, WorkflowCancelAbort)) {
763
819
  await onCancel();
764
820
  return ok("workflow-finished");
765
- } else {
821
+ } else if (isInstanceOf(error_, WorkflowAbort)) {
766
822
  return ok("step-finished");
823
+ } else {
824
+ return err(error_);
767
825
  }
768
826
  }
769
827
  };
770
- var triggerWorkflowDelete = async (workflowContext, result, debug, cancel = false) => {
771
- await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
772
- deletedWorkflowRunId: workflowContext.workflowRunId
828
+ var triggerWorkflowDelete = async (workflowContext, result, cancel = false, dispatchDebug) => {
829
+ await dispatchDebug?.("onInfo", {
830
+ info: `Deleting workflow run ${workflowContext.workflowRunId} from QStash` + (cancel ? " with cancel=true." : ".")
773
831
  });
774
832
  await workflowContext.qstashClient.http.request({
775
833
  path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
@@ -777,18 +835,16 @@ var triggerWorkflowDelete = async (workflowContext, result, debug, cancel = fals
777
835
  parseResponseAsJson: false,
778
836
  body: JSON.stringify(result)
779
837
  });
780
- await debug?.log(
781
- "SUBMIT",
782
- "SUBMIT_CLEANUP",
783
- `workflow run ${workflowContext.workflowRunId} deleted.`
784
- );
838
+ await dispatchDebug?.("onInfo", {
839
+ info: `Workflow run ${workflowContext.workflowRunId} deleted from QStash successfully.`
840
+ });
785
841
  };
786
842
  var recreateUserHeaders = (headers) => {
787
843
  const filteredHeaders = new Headers();
788
844
  const pairs = headers.entries();
789
845
  for (const [header, value] of pairs) {
790
846
  const headerLowerCase = header.toLowerCase();
791
- const isUserHeader = !headerLowerCase.startsWith("upstash-workflow-") && // https://vercel.com/docs/edge-network/headers/request-headers#x-vercel-id
847
+ const isUserHeader = headerLowerCase !== "upstash-region" && !headerLowerCase.startsWith("upstash-workflow-") && // https://vercel.com/docs/edge-network/headers/request-headers#x-vercel-id
792
848
  !headerLowerCase.startsWith("x-vercel-") && !headerLowerCase.startsWith("x-forwarded-") && // https://blog.cloudflare.com/preventing-request-loops-using-cdn-loop/
793
849
  headerLowerCase !== "cf-connecting-ip" && headerLowerCase !== "cdn-loop" && headerLowerCase !== "cf-ew-via" && headerLowerCase !== "cf-ray" && // For Render https://render.com
794
850
  headerLowerCase !== "render-proxy-ttl" || headerLowerCase === WORKFLOW_LABEL_HEADER.toLocaleLowerCase();
@@ -803,12 +859,8 @@ var handleThirdPartyCallResult = async ({
803
859
  requestPayload,
804
860
  client,
805
861
  workflowUrl,
806
- failureUrl,
807
- retries,
808
- retryDelay,
809
862
  telemetry,
810
- flowControl,
811
- debug
863
+ middlewareManager
812
864
  }) => {
813
865
  try {
814
866
  if (request.headers.get("Upstash-Workflow-Callback")) {
@@ -825,7 +877,7 @@ var handleThirdPartyCallResult = async ({
825
877
  client.http,
826
878
  workflowRunId2,
827
879
  messageId,
828
- debug
880
+ middlewareManager?.dispatchDebug.bind(middlewareManager)
829
881
  );
830
882
  if (workflowRunEnded) {
831
883
  return ok("workflow-ended");
@@ -839,9 +891,8 @@ var handleThirdPartyCallResult = async ({
839
891
  }
840
892
  const callbackMessage = JSON.parse(callbackPayload);
841
893
  if (!(callbackMessage.status >= 200 && callbackMessage.status < 300) && callbackMessage.maxRetries && callbackMessage.retried !== callbackMessage.maxRetries) {
842
- await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
843
- status: callbackMessage.status,
844
- body: atob(callbackMessage.body ?? "")
894
+ await middlewareManager?.dispatchDebug("onWarning", {
895
+ warning: `Third party call returned status ${callbackMessage.status}. Retrying (${callbackMessage.retried} out of ${callbackMessage.maxRetries}).`
845
896
  });
846
897
  console.warn(
847
898
  `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (retried ${callbackMessage.retried ?? 0} out of ${callbackMessage.maxRetries} times). Error Message:
@@ -874,11 +925,7 @@ ${atob(callbackMessage.body ?? "")}`
874
925
  workflowConfig: {
875
926
  workflowRunId,
876
927
  workflowUrl,
877
- failureUrl,
878
- retries,
879
- retryDelay,
880
- telemetry,
881
- flowControl
928
+ telemetry
882
929
  },
883
930
  userHeaders,
884
931
  invokeCount: Number(invokeCount)
@@ -895,19 +942,17 @@ ${atob(callbackMessage.body ?? "")}`
895
942
  out: JSON.stringify(callResponse),
896
943
  concurrent: Number(concurrentString)
897
944
  };
898
- await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
899
- step: callResultStep,
900
- headers: requestHeaders,
901
- url: workflowUrl
945
+ await middlewareManager?.dispatchDebug("onInfo", {
946
+ info: `Submitting third party call result, step ${stepName} (${stepIdString}).`
902
947
  });
903
- const result = await client.publishJSON({
948
+ await client.publishJSON({
904
949
  headers: requestHeaders,
905
950
  method: "POST",
906
951
  body: callResultStep,
907
952
  url: workflowUrl
908
953
  });
909
- await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
910
- messageId: result.messageId
954
+ await middlewareManager?.dispatchDebug("onInfo", {
955
+ info: `Third party call result submitted successfully, step ${stepName} (${stepIdString}).`
911
956
  });
912
957
  return ok("is-call-return");
913
958
  } else {
@@ -956,15 +1001,17 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
956
1001
  // src/context/steps.ts
957
1002
  var BaseLazyStep = class _BaseLazyStep {
958
1003
  stepName;
959
- constructor(stepName) {
1004
+ context;
1005
+ constructor(context, stepName) {
1006
+ this.context = context;
960
1007
  if (!stepName) {
961
1008
  throw new WorkflowError(
962
1009
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
963
1010
  );
964
1011
  }
965
1012
  if (typeof stepName !== "string") {
966
- console.warn(
967
- "Workflow Warning: A workflow step name must be a string. In a future release, this will throw an error."
1013
+ throw new WorkflowError(
1014
+ `A workflow step name must be a string. Received "${stepName}" (${typeof stepName}).`
968
1015
  );
969
1016
  }
970
1017
  this.stepName = stepName;
@@ -974,13 +1021,14 @@ var BaseLazyStep = class _BaseLazyStep {
974
1021
  *
975
1022
  * will be called when returning the steps to the context from auto executor
976
1023
  *
977
- * @param out field of the step
1024
+ * @param step step
978
1025
  * @returns parsed out field
979
1026
  */
980
- parseOut(out) {
1027
+ parseOut(step) {
1028
+ const out = step.out;
981
1029
  if (out === void 0) {
982
1030
  if (this.allowUndefinedOut) {
983
- return void 0;
1031
+ return this.handleUndefinedOut(step);
984
1032
  } else {
985
1033
  throw new WorkflowError(
986
1034
  `Error while parsing output of ${this.stepType} step. Expected a string, but got: undefined`
@@ -988,27 +1036,26 @@ var BaseLazyStep = class _BaseLazyStep {
988
1036
  }
989
1037
  }
990
1038
  if (typeof out === "object") {
991
- if (this.stepType !== "Wait") {
992
- console.warn(
993
- `Error while parsing ${this.stepType} step output. Expected a string, but got object. Please reach out to Upstash Support.`
994
- );
995
- return out;
996
- }
997
- return {
998
- ...out,
999
- eventData: _BaseLazyStep.tryParsing(out.eventData)
1000
- };
1039
+ console.warn(
1040
+ `Error while parsing ${this.stepType} step output. Expected a string, but got object. Please reach out to Upstash Support.`
1041
+ );
1042
+ return out;
1001
1043
  }
1002
1044
  if (typeof out !== "string") {
1003
1045
  throw new WorkflowError(
1004
1046
  `Error while parsing output of ${this.stepType} step. Expected a string or undefined, but got: ${typeof out}`
1005
1047
  );
1006
1048
  }
1007
- return this.safeParseOut(out);
1049
+ return this.safeParseOut(out, step);
1008
1050
  }
1009
- safeParseOut(out) {
1051
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1052
+ safeParseOut(out, step) {
1010
1053
  return _BaseLazyStep.tryParsing(out);
1011
1054
  }
1055
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1056
+ handleUndefinedOut(step) {
1057
+ return void 0;
1058
+ }
1012
1059
  static tryParsing(stepOut) {
1013
1060
  try {
1014
1061
  return JSON.parse(stepOut);
@@ -1026,12 +1073,8 @@ var BaseLazyStep = class _BaseLazyStep {
1026
1073
  workflowConfig: {
1027
1074
  workflowRunId: context.workflowRunId,
1028
1075
  workflowUrl: context.url,
1029
- failureUrl: context.failureUrl,
1030
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1031
- retryDelay: context.retryDelay,
1032
1076
  useJSONContent: false,
1033
- telemetry,
1034
- flowControl: context.flowControl
1077
+ telemetry
1035
1078
  },
1036
1079
  userHeaders: context.headers,
1037
1080
  invokeCount,
@@ -1047,9 +1090,6 @@ var BaseLazyStep = class _BaseLazyStep {
1047
1090
  body,
1048
1091
  headers,
1049
1092
  method: "POST",
1050
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1051
- retryDelay: context.retryDelay,
1052
- flowControl: context.flowControl,
1053
1093
  url: context.url
1054
1094
  }
1055
1095
  ]);
@@ -1059,8 +1099,8 @@ var LazyFunctionStep = class extends BaseLazyStep {
1059
1099
  stepFunction;
1060
1100
  stepType = "Run";
1061
1101
  allowUndefinedOut = true;
1062
- constructor(stepName, stepFunction) {
1063
- super(stepName);
1102
+ constructor(context, stepName, stepFunction) {
1103
+ super(context, stepName);
1064
1104
  this.stepFunction = stepFunction;
1065
1105
  }
1066
1106
  getPlanStep(concurrent, targetStep) {
@@ -1090,8 +1130,8 @@ var LazySleepStep = class extends BaseLazyStep {
1090
1130
  sleep;
1091
1131
  stepType = "SleepFor";
1092
1132
  allowUndefinedOut = true;
1093
- constructor(stepName, sleep) {
1094
- super(stepName);
1133
+ constructor(context, stepName, sleep) {
1134
+ super(context, stepName);
1095
1135
  this.sleep = sleep;
1096
1136
  }
1097
1137
  getPlanStep(concurrent, targetStep) {
@@ -1120,9 +1160,6 @@ var LazySleepStep = class extends BaseLazyStep {
1120
1160
  headers,
1121
1161
  method: "POST",
1122
1162
  url: context.url,
1123
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1124
- retryDelay: context.retryDelay,
1125
- flowControl: context.flowControl,
1126
1163
  delay: isParallel ? void 0 : this.sleep
1127
1164
  }
1128
1165
  ]);
@@ -1132,8 +1169,8 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1132
1169
  sleepUntil;
1133
1170
  stepType = "SleepUntil";
1134
1171
  allowUndefinedOut = true;
1135
- constructor(stepName, sleepUntil) {
1136
- super(stepName);
1172
+ constructor(context, stepName, sleepUntil) {
1173
+ super(context, stepName);
1137
1174
  this.sleepUntil = sleepUntil;
1138
1175
  }
1139
1176
  getPlanStep(concurrent, targetStep) {
@@ -1165,9 +1202,6 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1165
1202
  headers,
1166
1203
  method: "POST",
1167
1204
  url: context.url,
1168
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1169
- retryDelay: context.retryDelay,
1170
- flowControl: context.flowControl,
1171
1205
  notBefore: isParallel ? void 0 : this.sleepUntil
1172
1206
  }
1173
1207
  ]);
@@ -1182,20 +1216,18 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1182
1216
  retryDelay;
1183
1217
  timeout;
1184
1218
  flowControl;
1185
- stringifyBody;
1186
1219
  stepType = "Call";
1187
1220
  allowUndefinedOut = false;
1188
- constructor(stepName, url, method, body, headers, retries, retryDelay, timeout, flowControl, stringifyBody) {
1189
- super(stepName);
1190
- this.url = url;
1191
- this.method = method;
1192
- this.body = body;
1193
- this.headers = headers;
1194
- this.retries = retries;
1195
- this.retryDelay = retryDelay;
1196
- this.timeout = timeout;
1197
- this.flowControl = flowControl;
1198
- this.stringifyBody = stringifyBody;
1221
+ constructor(params) {
1222
+ super(params.context, params.stepName);
1223
+ this.url = params.url;
1224
+ this.method = params.method ?? "GET";
1225
+ this.body = params.body;
1226
+ this.headers = params.headers ?? {};
1227
+ this.retries = params.retries ?? 0;
1228
+ this.retryDelay = params.retryDelay;
1229
+ this.timeout = params.timeout;
1230
+ this.flowControl = params.flowControl;
1199
1231
  }
1200
1232
  getPlanStep(concurrent, targetStep) {
1201
1233
  return {
@@ -1292,7 +1324,7 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1292
1324
  "Upstash-Callback-Workflow-CallType": "fromCallback",
1293
1325
  "Upstash-Callback-Workflow-Init": "false",
1294
1326
  "Upstash-Callback-Workflow-Url": context.url,
1295
- "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody,WF_DetectTrigger",
1327
+ "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1296
1328
  "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
1297
1329
  "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
1298
1330
  "Upstash-Callback-Forward-Upstash-Workflow-StepName": this.stepName,
@@ -1305,22 +1337,10 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1305
1337
  };
1306
1338
  }
1307
1339
  async submitStep({ context, headers }) {
1308
- let callBody;
1309
- if (this.stringifyBody) {
1310
- callBody = JSON.stringify(this.body);
1311
- } else {
1312
- if (typeof this.body === "string") {
1313
- callBody = this.body;
1314
- } else {
1315
- throw new WorkflowError(
1316
- "When stringifyBody is false, body must be a string. Please check the body type of your call step."
1317
- );
1318
- }
1319
- }
1320
1340
  return await context.qstashClient.batch([
1321
1341
  {
1322
1342
  headers,
1323
- body: callBody,
1343
+ body: this.body,
1324
1344
  method: this.method,
1325
1345
  url: this.url,
1326
1346
  retries: DEFAULT_RETRIES === this.retries ? void 0 : this.retries,
@@ -1330,13 +1350,12 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1330
1350
  ]);
1331
1351
  }
1332
1352
  };
1333
- var LazyWaitForEventStep = class extends BaseLazyStep {
1353
+ var LazyWaitEventStep = class extends BaseLazyStep {
1334
1354
  eventId;
1335
1355
  timeout;
1336
- stepType = "Wait";
1337
1356
  allowUndefinedOut = false;
1338
- constructor(stepName, eventId, timeout) {
1339
- super(stepName);
1357
+ constructor(context, stepName, eventId, timeout) {
1358
+ super(context, stepName);
1340
1359
  this.eventId = eventId;
1341
1360
  this.timeout = timeout;
1342
1361
  }
@@ -1361,13 +1380,6 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1361
1380
  concurrent
1362
1381
  });
1363
1382
  }
1364
- safeParseOut(out) {
1365
- const result = JSON.parse(out);
1366
- return {
1367
- ...result,
1368
- eventData: BaseLazyStep.tryParsing(result.eventData)
1369
- };
1370
- }
1371
1383
  getHeaders({ context, telemetry, invokeCount, step }) {
1372
1384
  const headers = super.getHeaders({ context, telemetry, invokeCount, step });
1373
1385
  headers.headers["Upstash-Workflow-CallType"] = "step";
@@ -1401,7 +1413,7 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1401
1413
  timeoutHeaders,
1402
1414
  step: {
1403
1415
  stepId: step.stepId,
1404
- stepType: "Wait",
1416
+ stepType: this.stepType,
1405
1417
  stepName: step.stepName,
1406
1418
  concurrent: step.concurrent,
1407
1419
  targetStep: step.targetStep
@@ -1422,8 +1434,8 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1422
1434
  };
1423
1435
  var LazyNotifyStep = class extends LazyFunctionStep {
1424
1436
  stepType = "Notify";
1425
- constructor(stepName, eventId, eventData, requester) {
1426
- super(stepName, async () => {
1437
+ constructor(context, stepName, eventId, eventData, requester) {
1438
+ super(context, stepName, async () => {
1427
1439
  const notifyResponse = await makeNotifyRequest(requester, eventId, eventData);
1428
1440
  return {
1429
1441
  eventId,
@@ -1448,28 +1460,10 @@ var LazyInvokeStep = class extends BaseLazyStep {
1448
1460
  * workflow id of the invoked workflow
1449
1461
  */
1450
1462
  workflowId;
1451
- constructor(stepName, {
1452
- workflow,
1453
- body,
1454
- headers = {},
1455
- workflowRunId,
1456
- retries,
1457
- retryDelay,
1458
- flowControl,
1459
- stringifyBody = true
1460
- }) {
1461
- super(stepName);
1462
- this.params = {
1463
- workflow,
1464
- body,
1465
- headers,
1466
- workflowRunId: getWorkflowRunId(workflowRunId),
1467
- retries,
1468
- retryDelay,
1469
- flowControl,
1470
- stringifyBody
1471
- };
1472
- const { workflowId } = workflow;
1463
+ constructor(context, stepName, params) {
1464
+ super(context, stepName);
1465
+ this.params = params;
1466
+ const { workflowId } = params.workflow;
1473
1467
  if (!workflowId) {
1474
1468
  throw new WorkflowError("You can only invoke workflow which has a workflowId");
1475
1469
  }
@@ -1509,31 +1503,18 @@ var LazyInvokeStep = class extends BaseLazyStep {
1509
1503
  workflowConfig: {
1510
1504
  workflowRunId: context.workflowRunId,
1511
1505
  workflowUrl: context.url,
1512
- failureUrl: context.failureUrl,
1513
- retries: context.retries,
1514
- retryDelay: context.retryDelay,
1515
1506
  telemetry,
1516
- flowControl: context.flowControl,
1517
1507
  useJSONContent: false
1518
1508
  },
1519
1509
  userHeaders: context.headers,
1520
1510
  invokeCount
1521
1511
  });
1512
+ context.qstashClient.http.headers?.forEach((value, key) => {
1513
+ invokerHeaders[key] = value;
1514
+ });
1522
1515
  invokerHeaders["Upstash-Workflow-Runid"] = context.workflowRunId;
1523
- let invokeBody;
1524
- if (this.params.stringifyBody) {
1525
- invokeBody = JSON.stringify(this.params.body);
1526
- } else {
1527
- if (typeof this.params.body === "string") {
1528
- invokeBody = this.params.body;
1529
- } else {
1530
- throw new WorkflowError(
1531
- "When stringifyBody is false, body must be a string. Please check the body type of your invoke step."
1532
- );
1533
- }
1534
- }
1535
1516
  const request = {
1536
- body: invokeBody,
1517
+ body: stringifyBody(this.params.body),
1537
1518
  headers: Object.fromEntries(
1538
1519
  Object.entries(invokerHeaders).map((pairs) => [pairs[0], [pairs[1]]])
1539
1520
  ),
@@ -1544,34 +1525,19 @@ var LazyInvokeStep = class extends BaseLazyStep {
1544
1525
  return JSON.stringify(request);
1545
1526
  }
1546
1527
  getHeaders({ context, telemetry, invokeCount }) {
1547
- const {
1548
- workflow,
1549
- headers = {},
1550
- workflowRunId = getWorkflowRunId(),
1551
- retries,
1552
- retryDelay,
1553
- flowControl
1554
- } = this.params;
1528
+ const { workflow, headers = {}, workflowRunId, retries, retryDelay, flowControl } = this.params;
1555
1529
  const newUrl = context.url.replace(/[^/]+$/, this.workflowId);
1556
- const {
1557
- retries: workflowRetries,
1558
- retryDelay: workflowRetryDelay,
1559
- failureFunction,
1560
- failureUrl,
1561
- useJSONContent,
1562
- flowControl: workflowFlowControl
1563
- } = workflow.options;
1564
1530
  const { headers: triggerHeaders, contentType } = getHeaders({
1565
1531
  initHeaderValue: "true",
1566
1532
  workflowConfig: {
1567
- workflowRunId,
1533
+ workflowRunId: getWorkflowRunId(workflowRunId),
1568
1534
  workflowUrl: newUrl,
1569
- retries: retries ?? workflowRetries,
1570
- retryDelay: retryDelay ?? workflowRetryDelay,
1535
+ retries,
1536
+ retryDelay,
1571
1537
  telemetry,
1572
- failureUrl: failureFunction ? newUrl : failureUrl,
1573
- flowControl: flowControl ?? workflowFlowControl,
1574
- useJSONContent: useJSONContent ?? false
1538
+ failureUrl: newUrl,
1539
+ flowControl,
1540
+ useJSONContent: workflow.useJSONContent ?? false
1575
1541
  },
1576
1542
  invokeCount: invokeCount + 1,
1577
1543
  userHeaders: new Headers(headers)
@@ -1590,6 +1556,88 @@ var LazyInvokeStep = class extends BaseLazyStep {
1590
1556
  return [result];
1591
1557
  }
1592
1558
  };
1559
+ var LazyCreateWebhookStep = class extends BaseLazyStep {
1560
+ stepType = "CreateWebhook";
1561
+ allowUndefinedOut = false;
1562
+ getPlanStep(concurrent, targetStep) {
1563
+ return {
1564
+ stepId: 0,
1565
+ stepName: this.stepName,
1566
+ stepType: this.stepType,
1567
+ concurrent,
1568
+ targetStep
1569
+ };
1570
+ }
1571
+ async getResultStep(concurrent, stepId) {
1572
+ return {
1573
+ stepId,
1574
+ stepName: this.stepName,
1575
+ stepType: this.stepType,
1576
+ out: void 0,
1577
+ concurrent
1578
+ };
1579
+ }
1580
+ getBody({ step, context }) {
1581
+ const userId = getUserIdFromToken(context.qstashClient);
1582
+ const workflowRunId = context.workflowRunId;
1583
+ const eventId = getEventId();
1584
+ const qstashUrl = getQStashUrl(this.context.qstashClient);
1585
+ return JSON.stringify({
1586
+ ...step,
1587
+ out: JSON.stringify({
1588
+ webhookUrl: `${qstashUrl}/v2/workflows/hooks/${userId}/${workflowRunId}/${eventId}`,
1589
+ eventId
1590
+ })
1591
+ });
1592
+ }
1593
+ };
1594
+ var LazyWaitForWebhookStep = class extends LazyWaitEventStep {
1595
+ stepType = "WaitForWebhook";
1596
+ allowUndefinedOut = true;
1597
+ constructor(context, stepName, webhook, timeout) {
1598
+ super(context, stepName, webhook.eventId, timeout);
1599
+ }
1600
+ safeParseOut(out) {
1601
+ const eventData = decodeBase64(out);
1602
+ const parsedEventData = BaseLazyStep.tryParsing(eventData);
1603
+ const body = parsedEventData.body;
1604
+ const parsedBody = typeof body === "string" ? decodeBase64(body) : void 0;
1605
+ const request = new Request(
1606
+ `${parsedEventData.proto}://${parsedEventData.host}${parsedEventData.url}`,
1607
+ {
1608
+ method: parsedEventData.method,
1609
+ headers: parsedEventData.header,
1610
+ body: parsedBody
1611
+ }
1612
+ );
1613
+ return {
1614
+ request,
1615
+ timeout: false
1616
+ };
1617
+ }
1618
+ handleUndefinedOut() {
1619
+ return {
1620
+ timeout: true,
1621
+ request: void 0
1622
+ };
1623
+ }
1624
+ };
1625
+ var LazyWaitForEventStep = class extends LazyWaitEventStep {
1626
+ stepType = "Wait";
1627
+ allowUndefinedOut = true;
1628
+ parseWaitForEventOut(out, waitTimeout) {
1629
+ return {
1630
+ eventData: out ? BaseLazyStep.tryParsing(decodeBase64(out)) : void 0,
1631
+ timeout: waitTimeout ?? false
1632
+ };
1633
+ }
1634
+ safeParseOut(out, step) {
1635
+ return this.parseWaitForEventOut(out, step.waitTimeout);
1636
+ }
1637
+ handleUndefinedOut(step) {
1638
+ return this.parseWaitForEventOut(void 0, step.waitTimeout);
1639
+ }
1640
+ };
1593
1641
 
1594
1642
  // src/qstash/headers.ts
1595
1643
  var WorkflowHeaders = class {
@@ -1599,14 +1647,15 @@ var WorkflowHeaders = class {
1599
1647
  initHeaderValue;
1600
1648
  stepInfo;
1601
1649
  headers;
1602
- keepTriggerConfig;
1650
+ /**
1651
+ * @param params workflow header parameters
1652
+ */
1603
1653
  constructor({
1604
1654
  userHeaders,
1605
1655
  workflowConfig,
1606
1656
  invokeCount,
1607
1657
  initHeaderValue,
1608
- stepInfo,
1609
- keepTriggerConfig
1658
+ stepInfo
1610
1659
  }) {
1611
1660
  this.userHeaders = userHeaders;
1612
1661
  this.workflowConfig = workflowConfig;
@@ -1618,7 +1667,6 @@ var WorkflowHeaders = class {
1618
1667
  workflowHeaders: {},
1619
1668
  failureHeaders: {}
1620
1669
  };
1621
- this.keepTriggerConfig = keepTriggerConfig;
1622
1670
  }
1623
1671
  getHeaders() {
1624
1672
  this.addBaseHeaders();
@@ -1637,7 +1685,7 @@ var WorkflowHeaders = class {
1637
1685
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1638
1686
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1639
1687
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1640
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger" + (this.keepTriggerConfig ? ",WF_TriggerOnConfig" : ""),
1688
+ [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1641
1689
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1642
1690
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1643
1691
  };
@@ -1707,12 +1755,12 @@ var WorkflowHeaders = class {
1707
1755
  }
1708
1756
  this.headers.workflowHeaders["Failure-Callback"] = this.workflowConfig.failureUrl;
1709
1757
  this.headers.failureHeaders[`Forward-${WORKFLOW_FAILURE_HEADER}`] = "true";
1710
- this.headers.failureHeaders[`Forward-Upstash-Workflow-Failure-Callback`] = "true";
1758
+ this.headers.failureHeaders[`Forward-${WORKFLOW_FAILURE_CALLBACK_HEADER}`] = "true";
1711
1759
  this.headers.failureHeaders["Workflow-Runid"] = this.workflowConfig.workflowRunId;
1712
1760
  this.headers.failureHeaders["Workflow-Init"] = "false";
1713
1761
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1714
1762
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1715
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger";
1763
+ this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1716
1764
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1717
1765
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1718
1766
  }
@@ -1782,14 +1830,13 @@ var submitParallelSteps = async ({
1782
1830
  initialStepCount,
1783
1831
  invokeCount,
1784
1832
  telemetry,
1785
- debug
1833
+ dispatchDebug
1786
1834
  }) => {
1787
1835
  const planSteps = steps.map(
1788
1836
  (step, index) => step.getPlanStep(steps.length, initialStepCount + index)
1789
1837
  );
1790
- await debug?.log("SUBMIT", "SUBMIT_STEP", {
1791
- length: planSteps.length,
1792
- steps: planSteps
1838
+ await dispatchDebug("onInfo", {
1839
+ info: `Submitting ${planSteps.length} parallel steps.`
1793
1840
  });
1794
1841
  const result = await context.qstashClient.batch(
1795
1842
  planSteps.map((planStep) => {
@@ -1798,10 +1845,6 @@ var submitParallelSteps = async ({
1798
1845
  workflowConfig: {
1799
1846
  workflowRunId: context.workflowRunId,
1800
1847
  workflowUrl: context.url,
1801
- failureUrl: context.failureUrl,
1802
- retries: context.retries,
1803
- retryDelay: context.retryDelay,
1804
- flowControl: context.flowControl,
1805
1848
  telemetry
1806
1849
  },
1807
1850
  userHeaders: context.headers,
@@ -1817,13 +1860,11 @@ var submitParallelSteps = async ({
1817
1860
  };
1818
1861
  })
1819
1862
  );
1820
- await debug?.log("INFO", "SUBMIT_STEP", {
1821
- messageIds: result.map((message) => {
1822
- return {
1823
- message: message.messageId
1824
- };
1825
- })
1826
- });
1863
+ if (result && result.length > 0) {
1864
+ await dispatchDebug("onInfo", {
1865
+ info: `Submitted ${planSteps.length} parallel steps. messageIds: ${result.filter((r) => r).map((r) => r.messageId).join(", ")}.`
1866
+ });
1867
+ }
1827
1868
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
1828
1869
  };
1829
1870
  var submitSingleStep = async ({
@@ -1833,14 +1874,13 @@ var submitSingleStep = async ({
1833
1874
  invokeCount,
1834
1875
  concurrency,
1835
1876
  telemetry,
1836
- debug
1877
+ dispatchDebug,
1878
+ dispatchLifecycle
1837
1879
  }) => {
1838
- const resultStep = await lazyStep.getResultStep(concurrency, stepId);
1839
- await debug?.log("INFO", "RUN_SINGLE", {
1840
- fromRequest: false,
1841
- step: resultStep,
1842
- stepCount: stepId
1880
+ await dispatchLifecycle("beforeExecution", {
1881
+ stepName: lazyStep.stepName
1843
1882
  });
1883
+ const resultStep = await lazyStep.getResultStep(concurrency, stepId);
1844
1884
  const { headers } = lazyStep.getHeaders({
1845
1885
  context,
1846
1886
  step: resultStep,
@@ -1854,10 +1894,6 @@ var submitSingleStep = async ({
1854
1894
  invokeCount,
1855
1895
  telemetry
1856
1896
  });
1857
- await debug?.log("SUBMIT", "SUBMIT_STEP", {
1858
- length: 1,
1859
- steps: [resultStep]
1860
- });
1861
1897
  const submitResult = await lazyStep.submitStep({
1862
1898
  context,
1863
1899
  body,
@@ -1867,13 +1903,11 @@ var submitSingleStep = async ({
1867
1903
  step: resultStep,
1868
1904
  telemetry
1869
1905
  });
1870
- await debug?.log("INFO", "SUBMIT_STEP", {
1871
- messageIds: submitResult.map((message) => {
1872
- return {
1873
- message: message.messageId
1874
- };
1875
- })
1876
- });
1906
+ if (submitResult && submitResult[0]) {
1907
+ await dispatchDebug("onInfo", {
1908
+ info: `Submitted step "${resultStep.stepName}" with messageId: ${submitResult[0].messageId}.`
1909
+ });
1910
+ }
1877
1911
  return resultStep;
1878
1912
  };
1879
1913
 
@@ -1882,21 +1916,31 @@ var AutoExecutor = class _AutoExecutor {
1882
1916
  context;
1883
1917
  promises = /* @__PURE__ */ new WeakMap();
1884
1918
  activeLazyStepList;
1885
- debug;
1886
1919
  nonPlanStepCount;
1887
1920
  steps;
1888
1921
  indexInCurrentList = 0;
1889
1922
  invokeCount;
1890
1923
  telemetry;
1924
+ dispatchDebug;
1925
+ dispatchLifecycle;
1891
1926
  stepCount = 0;
1892
1927
  planStepCount = 0;
1893
1928
  executingStep = false;
1894
- constructor(context, steps, telemetry, invokeCount, debug) {
1929
+ /**
1930
+ * @param context workflow context
1931
+ * @param steps list of steps
1932
+ * @param dispatchDebug debug event dispatcher
1933
+ * @param dispatchLifecycle lifecycle event dispatcher
1934
+ * @param telemetry optional telemetry information
1935
+ * @param invokeCount optional invoke count
1936
+ */
1937
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
1895
1938
  this.context = context;
1896
1939
  this.steps = steps;
1940
+ this.dispatchDebug = dispatchDebug;
1941
+ this.dispatchLifecycle = dispatchLifecycle;
1897
1942
  this.telemetry = telemetry;
1898
1943
  this.invokeCount = invokeCount ?? 0;
1899
- this.debug = debug;
1900
1944
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
1901
1945
  }
1902
1946
  /**
@@ -1964,7 +2008,7 @@ var AutoExecutor = class _AutoExecutor {
1964
2008
  /**
1965
2009
  * Executes a step:
1966
2010
  * - If the step result is available in the steps, returns the result
1967
- * - If the result is not avaiable, runs the function
2011
+ * - If the result is not available, runs the function
1968
2012
  * - Sends the result to QStash
1969
2013
  *
1970
2014
  * @param lazyStep lazy step to execute
@@ -1974,12 +2018,15 @@ var AutoExecutor = class _AutoExecutor {
1974
2018
  if (this.stepCount < this.nonPlanStepCount) {
1975
2019
  const step = this.steps[this.stepCount + this.planStepCount];
1976
2020
  validateStep(lazyStep, step);
1977
- await this.debug?.log("INFO", "RUN_SINGLE", {
1978
- fromRequest: true,
1979
- step,
1980
- stepCount: this.stepCount
1981
- });
1982
- return lazyStep.parseOut(step.out);
2021
+ const parsedOut = lazyStep.parseOut(step);
2022
+ const isLastMemoized = this.stepCount + 1 === this.nonPlanStepCount && this.steps.at(-1).stepId !== 0;
2023
+ if (isLastMemoized) {
2024
+ await this.dispatchLifecycle("afterExecution", {
2025
+ stepName: lazyStep.stepName,
2026
+ result: parsedOut
2027
+ });
2028
+ }
2029
+ return parsedOut;
1983
2030
  }
1984
2031
  const resultStep = await submitSingleStep({
1985
2032
  context: this.context,
@@ -1988,15 +2035,15 @@ var AutoExecutor = class _AutoExecutor {
1988
2035
  invokeCount: this.invokeCount,
1989
2036
  concurrency: 1,
1990
2037
  telemetry: this.telemetry,
1991
- debug: this.debug
2038
+ dispatchDebug: this.dispatchDebug,
2039
+ dispatchLifecycle: this.dispatchLifecycle
1992
2040
  });
1993
2041
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
1994
2042
  }
1995
2043
  /**
1996
2044
  * Runs steps in parallel.
1997
2045
  *
1998
- * @param stepName parallel step name
1999
- * @param stepFunctions list of async functions to run in parallel
2046
+ * @param parallelSteps list of lazy steps to run in parallel
2000
2047
  * @returns results of the functions run in parallel
2001
2048
  */
2002
2049
  async runParallel(parallelSteps) {
@@ -2009,12 +2056,14 @@ var AutoExecutor = class _AutoExecutor {
2009
2056
  `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
2010
2057
  );
2011
2058
  }
2012
- await this.debug?.log("INFO", "RUN_PARALLEL", {
2013
- parallelCallState,
2014
- initialStepCount,
2015
- plannedParallelStepCount,
2016
- stepCount: this.stepCount,
2017
- planStepCount: this.planStepCount
2059
+ await this.dispatchDebug("onInfo", {
2060
+ info: `Executing parallel steps with: ` + JSON.stringify({
2061
+ parallelCallState,
2062
+ initialStepCount,
2063
+ plannedParallelStepCount,
2064
+ stepCount: this.stepCount,
2065
+ planStepCount: this.planStepCount
2066
+ })
2018
2067
  });
2019
2068
  switch (parallelCallState) {
2020
2069
  case "first": {
@@ -2024,7 +2073,7 @@ var AutoExecutor = class _AutoExecutor {
2024
2073
  initialStepCount,
2025
2074
  invokeCount: this.invokeCount,
2026
2075
  telemetry: this.telemetry,
2027
- debug: this.debug
2076
+ dispatchDebug: this.dispatchDebug
2028
2077
  });
2029
2078
  break;
2030
2079
  }
@@ -2046,7 +2095,8 @@ var AutoExecutor = class _AutoExecutor {
2046
2095
  invokeCount: this.invokeCount,
2047
2096
  concurrency: parallelSteps.length,
2048
2097
  telemetry: this.telemetry,
2049
- debug: this.debug
2098
+ dispatchDebug: this.dispatchDebug,
2099
+ dispatchLifecycle: this.dispatchLifecycle
2050
2100
  });
2051
2101
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2052
2102
  } catch (error) {
@@ -2060,13 +2110,34 @@ var AutoExecutor = class _AutoExecutor {
2060
2110
  break;
2061
2111
  }
2062
2112
  case "discard": {
2113
+ const resultStep = this.steps.at(-1);
2114
+ const lazyStep = parallelSteps.find(
2115
+ (planStep, index) => resultStep.stepId - index === initialStepCount
2116
+ );
2117
+ if (lazyStep) {
2118
+ await this.dispatchLifecycle("afterExecution", {
2119
+ stepName: lazyStep.stepName,
2120
+ result: lazyStep.parseOut(resultStep)
2121
+ });
2122
+ }
2063
2123
  throw new WorkflowAbort("discarded parallel");
2064
2124
  }
2065
2125
  case "last": {
2066
2126
  const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
2067
2127
  validateParallelSteps(parallelSteps, parallelResultSteps);
2128
+ const isLastMemoized = this.stepCount + 1 === this.nonPlanStepCount && this.steps.at(-1).stepId !== 0;
2129
+ if (isLastMemoized) {
2130
+ const resultStep = this.steps.at(-1);
2131
+ const lazyStep = parallelSteps.find(
2132
+ (planStep, index) => resultStep.stepId - index === initialStepCount
2133
+ );
2134
+ await this.dispatchLifecycle("afterExecution", {
2135
+ stepName: lazyStep.stepName,
2136
+ result: lazyStep.parseOut(resultStep)
2137
+ });
2138
+ }
2068
2139
  return parallelResultSteps.map(
2069
- (step, index) => parallelSteps[index].parseOut(step.out)
2140
+ (step, index) => parallelSteps[index].parseOut(step)
2070
2141
  );
2071
2142
  }
2072
2143
  }
@@ -2122,7 +2193,6 @@ var AutoExecutor = class _AutoExecutor {
2122
2193
  * @param index index of the current step
2123
2194
  * @returns result[index] if lazyStepList > 1, otherwise result
2124
2195
  */
2125
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
2126
2196
  static getResult(lazyStepList, result, index) {
2127
2197
  if (lazyStepList.length === 1) {
2128
2198
  return result;
@@ -2209,15 +2279,18 @@ var getProviderInfo = (api) => {
2209
2279
  // src/context/api/base.ts
2210
2280
  var BaseWorkflowApi = class {
2211
2281
  context;
2282
+ /**
2283
+ * @param context workflow context
2284
+ */
2212
2285
  constructor({ context }) {
2213
2286
  this.context = context;
2214
2287
  }
2215
2288
  /**
2216
2289
  * context.call which uses a QStash API
2217
2290
  *
2218
- * @param stepName
2219
- * @param settings
2220
- * @returns
2291
+ * @param stepName name of the step
2292
+ * @param settings call settings including api configuration
2293
+ * @returns call response
2221
2294
  */
2222
2295
  async callApi(stepName, settings) {
2223
2296
  const { url, appendHeaders, method } = getProviderInfo(settings.api);
@@ -2225,7 +2298,7 @@ var BaseWorkflowApi = class {
2225
2298
  return await this.context.call(stepName, {
2226
2299
  url,
2227
2300
  method: userMethod ?? method,
2228
- body,
2301
+ body: typeof body === "string" ? body : JSON.stringify(body),
2229
2302
  headers: {
2230
2303
  ...appendHeaders,
2231
2304
  ...headers
@@ -2377,6 +2450,129 @@ var getNewUrlFromWorkflowId = (url, workflowId) => {
2377
2450
  return url.replace(/[^/]+$/, workflowId);
2378
2451
  };
2379
2452
 
2453
+ // src/middleware/default-callbacks.ts
2454
+ var onErrorWithConsole = async ({ workflowRunId, error }) => {
2455
+ console.error(` [Upstash Workflow]: Error in workflow run ${workflowRunId}: ` + error);
2456
+ };
2457
+ var onWarningWithConsole = async ({ workflowRunId, warning }) => {
2458
+ console.warn(` [Upstash Workflow]: Warning in workflow run ${workflowRunId}: ` + warning);
2459
+ };
2460
+ var onInfoWithConsole = async ({
2461
+ workflowRunId,
2462
+ info
2463
+ }) => {
2464
+ console.info(` [Upstash Workflow]: Info in workflow run ${workflowRunId}: ` + info);
2465
+ };
2466
+
2467
+ // src/middleware/manager.ts
2468
+ var MiddlewareManager = class {
2469
+ middlewares;
2470
+ workflowRunId;
2471
+ context;
2472
+ /**
2473
+ * @param middlewares list of workflow middlewares
2474
+ */
2475
+ constructor(middlewares = []) {
2476
+ this.middlewares = middlewares;
2477
+ }
2478
+ /**
2479
+ * Assign workflow run ID - will be passed to debug events
2480
+ *
2481
+ * @param workflowRunId workflow run id to assign
2482
+ */
2483
+ assignWorkflowRunId(workflowRunId) {
2484
+ this.workflowRunId = workflowRunId;
2485
+ }
2486
+ /**
2487
+ * Assign context - required for lifecycle events
2488
+ *
2489
+ * also assigns workflowRunId from context
2490
+ *
2491
+ * @param context workflow context to assign
2492
+ */
2493
+ assignContext(context) {
2494
+ this.context = context;
2495
+ this.workflowRunId = context.workflowRunId;
2496
+ }
2497
+ /**
2498
+ * Internal method to execute middlewares with common error handling logic
2499
+ *
2500
+ * @param event event name to dispatch
2501
+ * @param params event parameters
2502
+ */
2503
+ async executeMiddlewares(event, params) {
2504
+ await Promise.all(this.middlewares.map((m) => m.ensureInit()));
2505
+ await Promise.all(
2506
+ this.middlewares.map(async (middleware) => {
2507
+ const callback = middleware.getCallback(event);
2508
+ if (callback) {
2509
+ try {
2510
+ await callback(params);
2511
+ } catch (error) {
2512
+ try {
2513
+ const onErrorCallback = middleware.getCallback("onError") ?? onErrorWithConsole;
2514
+ await onErrorCallback({
2515
+ workflowRunId: this.workflowRunId,
2516
+ error
2517
+ });
2518
+ } catch (onErrorError) {
2519
+ console.error(
2520
+ `Failed while executing "onError" of middleware "${middleware.name}", falling back to logging the error to console. Error: ${onErrorError}`
2521
+ );
2522
+ onErrorWithConsole({
2523
+ workflowRunId: this.workflowRunId,
2524
+ error
2525
+ });
2526
+ }
2527
+ }
2528
+ }
2529
+ })
2530
+ );
2531
+ if (event === "onError") {
2532
+ onErrorWithConsole({
2533
+ workflowRunId: this.workflowRunId,
2534
+ ...params
2535
+ });
2536
+ } else if (event === "onWarning") {
2537
+ onWarningWithConsole({
2538
+ workflowRunId: this.workflowRunId,
2539
+ ...params
2540
+ });
2541
+ }
2542
+ }
2543
+ /**
2544
+ * Dispatch a debug event (onError, onWarning, onInfo)
2545
+ *
2546
+ * @param event debug event name
2547
+ * @param params event parameters
2548
+ */
2549
+ async dispatchDebug(event, params) {
2550
+ const paramsWithRunId = {
2551
+ ...params,
2552
+ workflowRunId: this.workflowRunId
2553
+ };
2554
+ await this.executeMiddlewares(event, paramsWithRunId);
2555
+ }
2556
+ /**
2557
+ * Dispatch a lifecycle event (beforeExecution, afterExecution, runStarted, runCompleted)
2558
+ *
2559
+ * @param event lifecycle event name
2560
+ * @param params event parameters
2561
+ */
2562
+ async dispatchLifecycle(event, params) {
2563
+ if (!this.context) {
2564
+ throw new WorkflowError(
2565
+ `Something went wrong while calling middlewares. Lifecycle event "${event}" was called before assignContext.`
2566
+ );
2567
+ }
2568
+ const paramsWithContext = {
2569
+ ...params,
2570
+ context: this.context
2571
+ };
2572
+ await this.executeMiddlewares(event, paramsWithContext);
2573
+ }
2574
+ };
2575
+
2380
2576
  // src/context/context.ts
2381
2577
  var WorkflowContext = class {
2382
2578
  executor;
@@ -2422,32 +2618,13 @@ var WorkflowContext = class {
2422
2618
  */
2423
2619
  url;
2424
2620
  /**
2425
- * URL to call in case of workflow failure with QStash failure callback
2426
- *
2427
- * https://upstash.com/docs/qstash/features/callbacks#what-is-a-failure-callback
2621
+ * Payload of the request which started the workflow.
2428
2622
  *
2429
- * Can be overwritten by passing a `failureUrl` parameter in `serve`:
2623
+ * To specify its type, you can define `serve` as follows:
2430
2624
  *
2431
2625
  * ```ts
2432
- * export const POST = serve(
2433
- * async (context) => {
2434
- * ...
2435
- * },
2436
- * {
2437
- * failureUrl: "new-url-value"
2438
- * }
2439
- * )
2440
- * ```
2441
- */
2442
- failureUrl;
2443
- /**
2444
- * Payload of the request which started the workflow.
2445
- *
2446
- * To specify its type, you can define `serve` as follows:
2447
- *
2448
- * ```ts
2449
- * // set requestPayload type to MyPayload:
2450
- * export const POST = serve<MyPayload>(
2626
+ * // set requestPayload type to MyPayload:
2627
+ * export const POST = serve<MyPayload>(
2451
2628
  * async (context) => {
2452
2629
  * ...
2453
2630
  * }
@@ -2495,46 +2672,6 @@ var WorkflowContext = class {
2495
2672
  * Default value is set to `process.env`.
2496
2673
  */
2497
2674
  env;
2498
- /**
2499
- * Number of retries
2500
- */
2501
- retries;
2502
- /**
2503
- * Delay between retries.
2504
- *
2505
- * By default, the `retryDelay` is exponential backoff.
2506
- * More details can be found in: https://upstash.com/docs/qstash/features/retry.
2507
- *
2508
- * The `retryDelay` option allows you to customize the delay (in milliseconds) between retry attempts when message delivery fails.
2509
- *
2510
- * You can use mathematical expressions and the following built-in functions to calculate the delay dynamically.
2511
- * The special variable `retried` represents the current retry attempt count (starting from 0).
2512
- *
2513
- * Supported functions:
2514
- * - `pow`
2515
- * - `sqrt`
2516
- * - `abs`
2517
- * - `exp`
2518
- * - `floor`
2519
- * - `ceil`
2520
- * - `round`
2521
- * - `min`
2522
- * - `max`
2523
- *
2524
- * Examples of valid `retryDelay` values:
2525
- * ```ts
2526
- * 1000 // 1 second
2527
- * 1000 * (1 + retried) // 1 second multiplied by the current retry attempt
2528
- * pow(2, retried) // 2 to the power of the current retry attempt
2529
- * max(10, pow(2, retried)) // The greater of 10 or 2^retried
2530
- * ```
2531
- */
2532
- retryDelay;
2533
- /**
2534
- * Settings for controlling the number of active requests
2535
- * and number of requests per second with the same key.
2536
- */
2537
- flowControl;
2538
2675
  /**
2539
2676
  * Label to apply to the workflow run.
2540
2677
  *
@@ -2557,30 +2694,31 @@ var WorkflowContext = class {
2557
2694
  headers,
2558
2695
  steps,
2559
2696
  url,
2560
- failureUrl,
2561
- debug,
2562
2697
  initialPayload,
2563
2698
  env,
2564
- retries,
2565
- retryDelay,
2566
2699
  telemetry,
2567
2700
  invokeCount,
2568
- flowControl,
2569
- label
2701
+ label,
2702
+ middlewareManager
2570
2703
  }) {
2571
2704
  this.qstashClient = qstashClient;
2572
2705
  this.workflowRunId = workflowRunId;
2573
2706
  this.steps = steps;
2574
2707
  this.url = url;
2575
- this.failureUrl = failureUrl;
2576
2708
  this.headers = headers;
2577
2709
  this.requestPayload = initialPayload;
2578
2710
  this.env = env ?? {};
2579
- this.retries = retries ?? DEFAULT_RETRIES;
2580
- this.retryDelay = retryDelay;
2581
- this.flowControl = flowControl;
2582
2711
  this.label = label;
2583
- this.executor = new AutoExecutor(this, this.steps, telemetry, invokeCount, debug);
2712
+ const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2713
+ middlewareManagerInstance.assignContext(this);
2714
+ this.executor = new AutoExecutor(
2715
+ this,
2716
+ this.steps,
2717
+ middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2718
+ middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2719
+ telemetry,
2720
+ invokeCount
2721
+ );
2584
2722
  }
2585
2723
  /**
2586
2724
  * Executes a workflow step
@@ -2611,7 +2749,7 @@ var WorkflowContext = class {
2611
2749
  */
2612
2750
  async run(stepName, stepFunction) {
2613
2751
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2614
- return await this.addStep(new LazyFunctionStep(stepName, wrappedStepFunction));
2752
+ return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
2615
2753
  }
2616
2754
  /**
2617
2755
  * Stops the execution for the duration provided.
@@ -2625,7 +2763,7 @@ var WorkflowContext = class {
2625
2763
  * @returns undefined
2626
2764
  */
2627
2765
  async sleep(stepName, duration) {
2628
- await this.addStep(new LazySleepStep(stepName, duration));
2766
+ await this.addStep(new LazySleepStep(this, stepName, duration));
2629
2767
  }
2630
2768
  /**
2631
2769
  * Stops the execution until the date time provided.
@@ -2647,48 +2785,38 @@ var WorkflowContext = class {
2647
2785
  datetime = typeof datetime === "string" ? new Date(datetime) : datetime;
2648
2786
  time = Math.round(datetime.getTime() / 1e3);
2649
2787
  }
2650
- await this.addStep(new LazySleepUntilStep(stepName, time));
2788
+ await this.addStep(new LazySleepUntilStep(this, stepName, time));
2651
2789
  }
2652
2790
  async call(stepName, settings) {
2653
2791
  let callStep;
2654
2792
  if ("workflow" in settings) {
2655
2793
  const url = getNewUrlFromWorkflowId(this.url, settings.workflow.workflowId);
2656
- callStep = new LazyCallStep(
2794
+ const stringBody = typeof settings.body === "string" ? settings.body : settings.body === void 0 ? void 0 : JSON.stringify(settings.body);
2795
+ callStep = new LazyCallStep({
2796
+ context: this,
2657
2797
  stepName,
2658
2798
  url,
2659
- "POST",
2660
- settings.body,
2661
- settings.headers || {},
2662
- settings.retries || 0,
2663
- settings.retryDelay,
2664
- settings.timeout,
2665
- settings.flowControl ?? settings.workflow.options.flowControl,
2666
- settings.stringifyBody ?? true
2667
- );
2799
+ method: "POST",
2800
+ body: stringBody,
2801
+ headers: settings.headers || {},
2802
+ retries: settings.retries || 0,
2803
+ retryDelay: settings.retryDelay,
2804
+ timeout: settings.timeout,
2805
+ flowControl: settings.flowControl
2806
+ });
2668
2807
  } else {
2669
- const {
2670
- url,
2671
- method = "GET",
2672
- body,
2673
- headers = {},
2674
- retries = 0,
2675
- retryDelay,
2676
- timeout,
2677
- flowControl,
2678
- stringifyBody = true
2679
- } = settings;
2680
- callStep = new LazyCallStep(
2808
+ callStep = new LazyCallStep({
2809
+ context: this,
2681
2810
  stepName,
2682
- url,
2683
- method,
2684
- body,
2685
- headers,
2686
- retries,
2687
- retryDelay,
2688
- timeout,
2689
- flowControl,
2690
- stringifyBody
2691
- );
2811
+ url: settings.url,
2812
+ method: settings.method ?? "GET",
2813
+ body: settings.body,
2814
+ headers: settings.headers ?? {},
2815
+ retries: settings.retries ?? 0,
2816
+ retryDelay: settings.retryDelay,
2817
+ timeout: settings.timeout,
2818
+ flowControl: settings.flowControl
2819
+ });
2692
2820
  }
2693
2821
  return await this.addStep(callStep);
2694
2822
  }
@@ -2729,7 +2857,9 @@ var WorkflowContext = class {
2729
2857
  async waitForEvent(stepName, eventId, options = {}) {
2730
2858
  const { timeout = "7d" } = options;
2731
2859
  const timeoutStr = typeof timeout === "string" ? timeout : `${timeout}s`;
2732
- return await this.addStep(new LazyWaitForEventStep(stepName, eventId, timeoutStr));
2860
+ return await this.addStep(
2861
+ new LazyWaitForEventStep(this, stepName, eventId, timeoutStr)
2862
+ );
2733
2863
  }
2734
2864
  /**
2735
2865
  * Notify workflow runs waiting for an event
@@ -2754,20 +2884,28 @@ var WorkflowContext = class {
2754
2884
  */
2755
2885
  async notify(stepName, eventId, eventData) {
2756
2886
  return await this.addStep(
2757
- new LazyNotifyStep(stepName, eventId, eventData, this.qstashClient.http)
2887
+ new LazyNotifyStep(this, stepName, eventId, eventData, this.qstashClient.http)
2758
2888
  );
2759
2889
  }
2760
2890
  async invoke(stepName, settings) {
2761
- return await this.addStep(new LazyInvokeStep(stepName, settings));
2891
+ return await this.addStep(
2892
+ new LazyInvokeStep(this, stepName, settings)
2893
+ );
2894
+ }
2895
+ async createWebhook(stepName) {
2896
+ return await this.addStep(new LazyCreateWebhookStep(this, stepName));
2897
+ }
2898
+ async waitForWebhook(stepName, webhook, timeout) {
2899
+ return await this.addStep(new LazyWaitForWebhookStep(this, stepName, webhook, timeout));
2762
2900
  }
2763
2901
  /**
2764
2902
  * Cancel the current workflow run
2765
2903
  *
2766
- * Will throw WorkflowAbort to stop workflow execution.
2904
+ * Will throw WorkflowCancelAbort to stop workflow execution.
2767
2905
  * Shouldn't be inside try/catch.
2768
2906
  */
2769
2907
  async cancel() {
2770
- throw new WorkflowAbort("cancel", void 0, true);
2908
+ throw new WorkflowCancelAbort();
2771
2909
  }
2772
2910
  /**
2773
2911
  * Adds steps to the executor. Needed so that it can be overwritten in
@@ -2783,74 +2921,25 @@ var WorkflowContext = class {
2783
2921
  }
2784
2922
  };
2785
2923
 
2786
- // src/logger.ts
2787
- var LOG_LEVELS = ["DEBUG", "INFO", "SUBMIT", "WARN", "ERROR"];
2788
- var WorkflowLogger = class _WorkflowLogger {
2789
- logs = [];
2790
- options;
2791
- workflowRunId = void 0;
2792
- constructor(options) {
2793
- this.options = options;
2794
- }
2795
- async log(level, eventType, details) {
2796
- if (this.shouldLog(level)) {
2797
- const timestamp = Date.now();
2798
- const logEntry = {
2799
- timestamp,
2800
- workflowRunId: this.workflowRunId ?? "",
2801
- logLevel: level,
2802
- eventType,
2803
- details
2804
- };
2805
- this.logs.push(logEntry);
2806
- if (this.options.logOutput === "console") {
2807
- this.writeToConsole(logEntry);
2808
- }
2809
- await new Promise((resolve) => setTimeout(resolve, 100));
2810
- }
2811
- }
2812
- setWorkflowRunId(workflowRunId) {
2813
- this.workflowRunId = workflowRunId;
2814
- }
2815
- writeToConsole(logEntry) {
2816
- const JSON_SPACING = 2;
2817
- const logMethod = logEntry.logLevel === "ERROR" ? console.error : logEntry.logLevel === "WARN" ? console.warn : console.log;
2818
- logMethod(JSON.stringify(logEntry, void 0, JSON_SPACING));
2819
- }
2820
- shouldLog(level) {
2821
- return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
2822
- }
2823
- static getLogger(verbose) {
2824
- if (typeof verbose === "object") {
2825
- return verbose;
2826
- } else {
2827
- return verbose ? new _WorkflowLogger({
2828
- logLevel: "INFO",
2829
- logOutput: "console"
2830
- }) : void 0;
2831
- }
2832
- }
2833
- };
2834
-
2835
2924
  // src/serve/authorization.ts
2836
2925
  var import_qstash9 = require("@upstash/qstash");
2837
2926
  var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
2838
2927
  static disabledMessage = "disabled-qstash-worklfow-run";
2839
2928
  disabled = true;
2840
2929
  /**
2841
- * overwrite the WorkflowContext.addStep method to always raise WorkflowAbort
2930
+ * overwrite the WorkflowContext.addStep method to always raise WorkflowAuthError
2842
2931
  * error in order to stop the execution whenever we encounter a step.
2843
2932
  *
2844
2933
  * @param _step
2845
2934
  */
2846
2935
  async addStep(_step) {
2847
- throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
2936
+ throw new WorkflowAuthError(_DisabledWorkflowContext.disabledMessage);
2848
2937
  }
2849
2938
  /**
2850
- * overwrite cancel method to throw WorkflowAbort with the disabledMessage
2939
+ * overwrite cancel method to throw WorkflowAuthError with the disabledMessage
2851
2940
  */
2852
2941
  async cancel() {
2853
- throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
2942
+ throw new WorkflowAuthError(_DisabledWorkflowContext.disabledMessage);
2854
2943
  }
2855
2944
  /**
2856
2945
  * copies the passed context to create a DisabledWorkflowContext. Then, runs the
@@ -2873,18 +2962,14 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
2873
2962
  headers: context.headers,
2874
2963
  steps: [],
2875
2964
  url: context.url,
2876
- failureUrl: context.failureUrl,
2877
2965
  initialPayload: context.requestPayload,
2878
2966
  env: context.env,
2879
- retries: context.retries,
2880
- retryDelay: context.retryDelay,
2881
- flowControl: context.flowControl,
2882
2967
  label: context.label
2883
2968
  });
2884
2969
  try {
2885
2970
  await routeFunction(disabledContext);
2886
2971
  } catch (error) {
2887
- if (isInstanceOf(error, WorkflowAbort) && error.stepName === this.disabledMessage || isInstanceOf(error, WorkflowNonRetryableError) || isInstanceOf(error, WorkflowRetryAfterError)) {
2972
+ if (isInstanceOf(error, WorkflowAuthError) && error.stepName === this.disabledMessage || isInstanceOf(error, WorkflowNonRetryableError) || isInstanceOf(error, WorkflowRetryAfterError)) {
2888
2973
  return ok("step-found");
2889
2974
  }
2890
2975
  console.warn(
@@ -2917,13 +3002,6 @@ var processRawSteps = (rawSteps) => {
2917
3002
  const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
2918
3003
  const otherSteps = stepsToDecode.map((rawStep) => {
2919
3004
  const step = JSON.parse(decodeBase64(rawStep.body));
2920
- if (step.waitEventId) {
2921
- const newOut = {
2922
- eventData: step.out ? decodeBase64(step.out) : void 0,
2923
- timeout: step.waitTimeout ?? false
2924
- };
2925
- step.out = newOut;
2926
- }
2927
3005
  return step;
2928
3006
  });
2929
3007
  const steps = [initialStep, ...otherSteps];
@@ -2951,7 +3029,7 @@ var deduplicateSteps = (steps) => {
2951
3029
  }
2952
3030
  return deduplicatedSteps;
2953
3031
  };
2954
- var checkIfLastOneIsDuplicate = async (steps, debug) => {
3032
+ var checkIfLastOneIsDuplicate = async (steps, dispatchDebug) => {
2955
3033
  if (steps.length < 2) {
2956
3034
  return false;
2957
3035
  }
@@ -2962,14 +3040,41 @@ var checkIfLastOneIsDuplicate = async (steps, debug) => {
2962
3040
  const step = steps[index];
2963
3041
  if (step.stepId === lastStepId && step.targetStep === lastTargetStepId) {
2964
3042
  const message = `Upstash Workflow: The step '${step.stepName}' with id '${step.stepId}' has run twice during workflow execution. Rest of the workflow will continue running as usual.`;
2965
- await debug?.log("WARN", "RESPONSE_DEFAULT", message);
2966
- console.warn(message);
3043
+ await dispatchDebug?.("onWarning", {
3044
+ warning: message
3045
+ });
2967
3046
  return true;
2968
3047
  }
2969
3048
  }
2970
3049
  return false;
2971
3050
  };
2972
3051
  var validateRequest = (request) => {
3052
+ if (request.headers.get(WORKFLOW_UNKOWN_SDK_VERSION_HEADER)) {
3053
+ const workflowRunId2 = request.headers.get(WORKFLOW_ID_HEADER);
3054
+ if (!workflowRunId2) {
3055
+ throw new WorkflowError(
3056
+ "Couldn't get workflow id from header when handling unknown sdk request"
3057
+ );
3058
+ }
3059
+ return {
3060
+ unknownSdk: true,
3061
+ isFirstInvocation: true,
3062
+ workflowRunId: workflowRunId2
3063
+ };
3064
+ }
3065
+ if (request.headers.get(WORKFLOW_FAILURE_CALLBACK_HEADER)) {
3066
+ const workflowRunId2 = request.headers.get(WORKFLOW_ID_HEADER);
3067
+ if (!workflowRunId2) {
3068
+ throw new WorkflowError(
3069
+ "Couldn't get workflow id from header when handling failure callback request"
3070
+ );
3071
+ }
3072
+ return {
3073
+ unknownSdk: false,
3074
+ isFirstInvocation: true,
3075
+ workflowRunId: workflowRunId2
3076
+ };
3077
+ }
2973
3078
  const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
2974
3079
  const isFirstInvocation = !versionHeader;
2975
3080
  if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
@@ -2983,11 +3088,20 @@ var validateRequest = (request) => {
2983
3088
  }
2984
3089
  return {
2985
3090
  isFirstInvocation,
2986
- workflowRunId
3091
+ workflowRunId,
3092
+ unknownSdk: false
2987
3093
  };
2988
3094
  };
2989
- var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requester, messageId, debug) => {
2990
- if (isFirstInvocation) {
3095
+ var parseRequest = async ({
3096
+ requestPayload,
3097
+ isFirstInvocation,
3098
+ unknownSdk,
3099
+ workflowRunId,
3100
+ requester,
3101
+ messageId,
3102
+ dispatchDebug
3103
+ }) => {
3104
+ if (isFirstInvocation && !unknownSdk) {
2991
3105
  return {
2992
3106
  rawInitialPayload: requestPayload ?? "",
2993
3107
  steps: [],
@@ -2997,16 +3111,14 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
2997
3111
  } else {
2998
3112
  let rawSteps;
2999
3113
  if (!requestPayload) {
3000
- await debug?.log(
3001
- "INFO",
3002
- "ENDPOINT_START",
3003
- "request payload is empty, steps will be fetched from QStash."
3004
- );
3114
+ await dispatchDebug?.("onInfo", {
3115
+ info: "request payload is empty, steps will be fetched from QStash."
3116
+ });
3005
3117
  const { steps: fetchedSteps, workflowRunEnded } = await getSteps(
3006
3118
  requester,
3007
3119
  workflowRunId,
3008
3120
  messageId,
3009
- debug
3121
+ dispatchDebug
3010
3122
  );
3011
3123
  if (workflowRunEnded) {
3012
3124
  return {
@@ -3021,7 +3133,7 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
3021
3133
  rawSteps = JSON.parse(requestPayload);
3022
3134
  }
3023
3135
  const { rawInitialPayload, steps } = processRawSteps(rawSteps);
3024
- const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
3136
+ const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, dispatchDebug);
3025
3137
  const deduplicatedSteps = deduplicateSteps(steps);
3026
3138
  return {
3027
3139
  rawInitialPayload,
@@ -3031,16 +3143,21 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
3031
3143
  };
3032
3144
  }
3033
3145
  };
3034
- var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, routeFunction, failureFunction, env, retries, retryDelay, flowControl, debug) => {
3035
- if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
3146
+ var handleFailure = async ({
3147
+ request,
3148
+ requestPayload,
3149
+ qstashClient,
3150
+ initialPayloadParser,
3151
+ routeFunction,
3152
+ failureFunction,
3153
+ env,
3154
+ dispatchDebug
3155
+ }) => {
3156
+ if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true" && !request.headers.get(WORKFLOW_FAILURE_CALLBACK_HEADER)) {
3036
3157
  return ok({ result: "not-failure-callback" });
3037
3158
  }
3038
3159
  if (!failureFunction) {
3039
- return err(
3040
- new WorkflowError(
3041
- "Workflow endpoint is called to handle a failure, but a failureFunction is not provided in serve options. Either provide a failureUrl or a failureFunction."
3042
- )
3043
- );
3160
+ return ok({ result: "failure-function-undefined" });
3044
3161
  }
3045
3162
  try {
3046
3163
  const { status, header, body, url, sourceBody, workflowRunId } = JSON.parse(requestPayload);
@@ -3068,23 +3185,21 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
3068
3185
  headers: userHeaders,
3069
3186
  steps: [],
3070
3187
  url,
3071
- failureUrl: url,
3072
- debug,
3073
3188
  env,
3074
- retries,
3075
- retryDelay,
3076
- flowControl,
3077
3189
  telemetry: void 0,
3078
3190
  // not going to make requests in authentication check
3079
- label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0
3191
+ label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3192
+ middlewareManager: void 0
3080
3193
  });
3081
3194
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3082
3195
  routeFunction,
3083
3196
  workflowContext
3084
3197
  );
3085
3198
  if (authCheck.isErr()) {
3086
- await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
3087
- throw authCheck.error;
3199
+ await dispatchDebug?.("onError", {
3200
+ error: authCheck.error
3201
+ });
3202
+ return err(authCheck.error);
3088
3203
  } else if (authCheck.value === "run-ended") {
3089
3204
  return err(new WorkflowError("Not authorized to run the failure function."));
3090
3205
  }
@@ -3095,74 +3210,309 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
3095
3210
  failHeaders: header,
3096
3211
  failStack
3097
3212
  });
3098
- return ok({ result: "is-failure-callback", response: failureResponse });
3213
+ return ok({ result: "failure-function-executed", response: failureResponse });
3099
3214
  } catch (error) {
3100
3215
  return err(error);
3101
3216
  }
3102
3217
  };
3103
3218
 
3104
- // src/serve/options.ts
3219
+ // src/serve/multi-region/handlers.ts
3105
3220
  var import_qstash10 = require("@upstash/qstash");
3106
- var import_qstash11 = require("@upstash/qstash");
3107
- var processOptions = (options) => {
3108
- const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
3109
- const receiverEnvironmentVariablesSet = Boolean(
3110
- environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
3221
+
3222
+ // src/serve/multi-region/utils.ts
3223
+ var VALID_REGIONS = ["EU_CENTRAL_1", "US_EAST_1"];
3224
+ var getRegionFromEnvironment = (environment) => {
3225
+ const region = environment.QSTASH_REGION;
3226
+ return normalizeRegionHeader(region);
3227
+ };
3228
+ function readEnvironmentVariables(environmentVariables, environment, region) {
3229
+ const result = {};
3230
+ for (const variable of environmentVariables) {
3231
+ const key = region ? `${region}_${variable}` : variable;
3232
+ result[variable] = environment[key];
3233
+ }
3234
+ return result;
3235
+ }
3236
+ function readClientEnvironmentVariables(environment, region) {
3237
+ return readEnvironmentVariables(["QSTASH_URL", "QSTASH_TOKEN"], environment, region);
3238
+ }
3239
+ function readReceiverEnvironmentVariables(environment, region) {
3240
+ return readEnvironmentVariables(
3241
+ ["QSTASH_CURRENT_SIGNING_KEY", "QSTASH_NEXT_SIGNING_KEY"],
3242
+ environment,
3243
+ region
3111
3244
  );
3112
- return {
3113
- qstashClient: options?.qstashClient ?? new import_qstash11.Client({
3114
- baseUrl: environment.QSTASH_URL,
3115
- token: environment.QSTASH_TOKEN
3116
- }),
3117
- onStepFinish: (workflowRunId, _finishCondition, detailedFinishCondition) => {
3118
- if (detailedFinishCondition?.condition === "auth-fail") {
3119
- console.error(AUTH_FAIL_MESSAGE);
3120
- return new Response(
3121
- JSON.stringify({
3122
- message: AUTH_FAIL_MESSAGE,
3123
- workflowRunId
3124
- }),
3125
- {
3126
- status: 400,
3127
- headers: {
3128
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3129
- }
3130
- }
3131
- );
3132
- } else if (detailedFinishCondition?.condition === "non-retryable-error") {
3133
- return new Response(JSON.stringify(formatWorkflowError(detailedFinishCondition.result)), {
3134
- headers: {
3135
- "Upstash-NonRetryable-Error": "true",
3136
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3137
- },
3138
- status: 489
3139
- });
3140
- } else if (detailedFinishCondition?.condition === "retry-after-error") {
3141
- return new Response(JSON.stringify(formatWorkflowError(detailedFinishCondition.result)), {
3142
- headers: {
3143
- "Retry-After": detailedFinishCondition.result.retryAfter.toString(),
3144
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3145
- },
3146
- status: 429
3147
- });
3148
- } else if (detailedFinishCondition?.condition === "failure-callback") {
3149
- return new Response(
3150
- JSON.stringify({ result: detailedFinishCondition.result ?? void 0 }),
3151
- {
3152
- status: 200,
3153
- headers: {
3154
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3155
- }
3156
- }
3245
+ }
3246
+ function normalizeRegionHeader(region) {
3247
+ if (!region) {
3248
+ return void 0;
3249
+ }
3250
+ region = region.replaceAll("-", "_").toUpperCase();
3251
+ if (VALID_REGIONS.includes(region)) {
3252
+ return region;
3253
+ }
3254
+ console.warn(
3255
+ `[Upstash Workflow] Invalid UPSTASH-REGION header value: "${region}". Expected one of: ${VALID_REGIONS.join(
3256
+ ", "
3257
+ )}.`
3258
+ );
3259
+ return void 0;
3260
+ }
3261
+
3262
+ // src/serve/multi-region/handlers.ts
3263
+ var getHandlersForRequest = (qstashHandlers, regionHeader, isFirstInvocation) => {
3264
+ if (qstashHandlers.mode === "single-region") {
3265
+ return qstashHandlers.handlers;
3266
+ }
3267
+ let targetRegion;
3268
+ if (isFirstInvocation) {
3269
+ targetRegion = qstashHandlers.defaultRegion;
3270
+ } else {
3271
+ const normalizedRegion = regionHeader ? normalizeRegionHeader(regionHeader) : void 0;
3272
+ targetRegion = normalizedRegion ?? qstashHandlers.defaultRegion;
3273
+ }
3274
+ const handler = qstashHandlers.handlers[targetRegion];
3275
+ if (!handler) {
3276
+ console.warn(
3277
+ `[Upstash Workflow] No handler found for region "${targetRegion}". Falling back to default region.`
3278
+ );
3279
+ return qstashHandlers.handlers[qstashHandlers.defaultRegion];
3280
+ }
3281
+ return handler;
3282
+ };
3283
+ var createRegionalHandler = (environment, receiverConfig, region, clientOptions) => {
3284
+ const clientEnv = readClientEnvironmentVariables(environment, region);
3285
+ const client = new import_qstash10.Client({
3286
+ ...clientOptions,
3287
+ baseUrl: clientEnv.QSTASH_URL,
3288
+ token: clientEnv.QSTASH_TOKEN
3289
+ });
3290
+ const receiver = getReceiver(environment, receiverConfig, region);
3291
+ return { client, receiver };
3292
+ };
3293
+ var shouldUseMultiRegionMode = (environment, qstashClientOption) => {
3294
+ const hasRegionEnv = Boolean(getRegionFromEnvironment(environment));
3295
+ if (hasRegionEnv && (!qstashClientOption || !("http" in qstashClientOption))) {
3296
+ return {
3297
+ isMultiRegion: true,
3298
+ defaultRegion: getRegionFromEnvironment(environment),
3299
+ clientOptions: qstashClientOption
3300
+ };
3301
+ } else {
3302
+ return { isMultiRegion: false };
3303
+ }
3304
+ };
3305
+ var getQStashHandlers = ({
3306
+ environment,
3307
+ qstashClientOption,
3308
+ receiverConfig
3309
+ }) => {
3310
+ const multiRegion = shouldUseMultiRegionMode(environment, qstashClientOption);
3311
+ if (multiRegion.isMultiRegion) {
3312
+ const regions = ["US_EAST_1", "EU_CENTRAL_1"];
3313
+ const handlers = {};
3314
+ for (const region of regions) {
3315
+ try {
3316
+ handlers[region] = createRegionalHandler(
3317
+ environment,
3318
+ receiverConfig,
3319
+ region,
3320
+ multiRegion.clientOptions
3157
3321
  );
3322
+ } catch (error) {
3323
+ console.warn(`[Upstash Workflow] Failed to create handler for region ${region}:`, error);
3158
3324
  }
3159
- return new Response(JSON.stringify({ workflowRunId }), {
3160
- status: 200,
3161
- headers: {
3162
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3163
- }
3325
+ }
3326
+ return {
3327
+ mode: "multi-region",
3328
+ handlers,
3329
+ defaultRegion: multiRegion.defaultRegion
3330
+ };
3331
+ } else {
3332
+ return {
3333
+ mode: "single-region",
3334
+ handlers: {
3335
+ client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash10.Client({
3336
+ ...qstashClientOption,
3337
+ baseUrl: environment.QSTASH_URL,
3338
+ token: environment.QSTASH_TOKEN
3339
+ }),
3340
+ receiver: getReceiver(environment, receiverConfig)
3341
+ }
3342
+ };
3343
+ }
3344
+ };
3345
+ var getReceiver = (environment, receiverConfig, region) => {
3346
+ if (typeof receiverConfig === "string") {
3347
+ if (receiverConfig === "set-to-undefined") {
3348
+ return void 0;
3349
+ }
3350
+ const receiverEnv = readReceiverEnvironmentVariables(environment, region);
3351
+ return receiverEnv.QSTASH_CURRENT_SIGNING_KEY && receiverEnv.QSTASH_NEXT_SIGNING_KEY ? new import_qstash10.Receiver({
3352
+ currentSigningKey: receiverEnv.QSTASH_CURRENT_SIGNING_KEY,
3353
+ nextSigningKey: receiverEnv.QSTASH_NEXT_SIGNING_KEY
3354
+ }) : void 0;
3355
+ } else {
3356
+ return receiverConfig;
3357
+ }
3358
+ };
3359
+ var getQStashHandlerOptions = (...params) => {
3360
+ const handlers = getQStashHandlers(...params);
3361
+ return {
3362
+ qstashHandlers: handlers,
3363
+ defaultReceiver: handlers.mode === "single-region" ? handlers.handlers.receiver : handlers.handlers[handlers.defaultRegion].receiver,
3364
+ defaultClient: handlers.mode === "single-region" ? handlers.handlers.client : handlers.handlers[handlers.defaultRegion].client
3365
+ };
3366
+ };
3367
+
3368
+ // src/middleware/middleware.ts
3369
+ var WorkflowMiddleware = class {
3370
+ name;
3371
+ initCallbacks;
3372
+ /**
3373
+ * Callback functions
3374
+ *
3375
+ * Initially set to undefined, will be populated after init is called
3376
+ */
3377
+ middlewareCallbacks = void 0;
3378
+ constructor(parameters) {
3379
+ this.name = parameters.name;
3380
+ if ("init" in parameters) {
3381
+ this.initCallbacks = parameters.init;
3382
+ } else {
3383
+ this.middlewareCallbacks = parameters.callbacks;
3384
+ }
3385
+ }
3386
+ async ensureInit() {
3387
+ if (!this.middlewareCallbacks) {
3388
+ if (!this.initCallbacks) {
3389
+ throw new WorkflowError(`Middleware "${this.name}" has no callbacks or init defined.`);
3390
+ }
3391
+ this.middlewareCallbacks = await this.initCallbacks();
3392
+ }
3393
+ }
3394
+ /**
3395
+ * Gets a callback function by name.
3396
+ *
3397
+ * @param callback name of the callback to retrieve
3398
+ */
3399
+ getCallback(callback) {
3400
+ return this.middlewareCallbacks?.[callback];
3401
+ }
3402
+ };
3403
+
3404
+ // src/middleware/logging.ts
3405
+ var loggingMiddleware = new WorkflowMiddleware({
3406
+ name: "logging",
3407
+ callbacks: {
3408
+ afterExecution(params) {
3409
+ const { context, ...rest } = params;
3410
+ console.log(" [Upstash Workflow]: Step executed:", {
3411
+ workflowRunId: context.workflowRunId,
3412
+ ...rest
3413
+ });
3414
+ },
3415
+ beforeExecution(params) {
3416
+ const { context, ...rest } = params;
3417
+ console.log(" [Upstash Workflow]: Step execution started:", {
3418
+ workflowRunId: context.workflowRunId,
3419
+ ...rest
3164
3420
  });
3165
3421
  },
3422
+ runStarted(params) {
3423
+ const { context, ...rest } = params;
3424
+ console.log(" [Upstash Workflow]: Workflow run started:", {
3425
+ workflowRunId: context.workflowRunId,
3426
+ ...rest
3427
+ });
3428
+ },
3429
+ runCompleted(params) {
3430
+ const { context, ...rest } = params;
3431
+ console.log(" [Upstash Workflow]: Workflow run completed:", {
3432
+ workflowRunId: context.workflowRunId,
3433
+ ...rest
3434
+ });
3435
+ },
3436
+ onError: onErrorWithConsole,
3437
+ onWarning: onWarningWithConsole,
3438
+ onInfo: onInfoWithConsole
3439
+ }
3440
+ });
3441
+
3442
+ // src/serve/options.ts
3443
+ var createResponseData = (workflowRunId, detailedFinishCondition) => {
3444
+ const baseHeaders = {
3445
+ [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
3446
+ "Upstash-workflow-sdk": VERSION
3447
+ };
3448
+ if (detailedFinishCondition?.condition === "auth-fail") {
3449
+ return {
3450
+ text: JSON.stringify({
3451
+ message: AUTH_FAIL_MESSAGE,
3452
+ workflowRunId
3453
+ }),
3454
+ status: 400,
3455
+ headers: baseHeaders
3456
+ };
3457
+ } else if (detailedFinishCondition?.condition === "non-retryable-error") {
3458
+ return {
3459
+ text: JSON.stringify(formatWorkflowError(detailedFinishCondition.result)),
3460
+ status: 489,
3461
+ headers: {
3462
+ ...baseHeaders,
3463
+ "Upstash-NonRetryable-Error": "true"
3464
+ }
3465
+ };
3466
+ } else if (detailedFinishCondition?.condition === "retry-after-error") {
3467
+ return {
3468
+ text: JSON.stringify(formatWorkflowError(detailedFinishCondition.result)),
3469
+ status: 429,
3470
+ headers: {
3471
+ ...baseHeaders,
3472
+ "Retry-After": detailedFinishCondition.result.retryAfter.toString()
3473
+ }
3474
+ };
3475
+ } else if (detailedFinishCondition?.condition === "failure-callback-executed") {
3476
+ return {
3477
+ text: JSON.stringify({ result: detailedFinishCondition.result ?? void 0 }),
3478
+ status: 200,
3479
+ headers: baseHeaders
3480
+ };
3481
+ } else if (detailedFinishCondition?.condition === "failure-callback-undefined") {
3482
+ return {
3483
+ text: JSON.stringify({
3484
+ workflowRunId,
3485
+ finishCondition: detailedFinishCondition.condition
3486
+ }),
3487
+ status: 200,
3488
+ headers: {
3489
+ ...baseHeaders,
3490
+ "Upstash-Workflow-Failure-Callback-Notfound": "true"
3491
+ }
3492
+ };
3493
+ }
3494
+ return {
3495
+ text: JSON.stringify({
3496
+ workflowRunId,
3497
+ finishCondition: detailedFinishCondition.condition
3498
+ }),
3499
+ status: 200,
3500
+ headers: baseHeaders
3501
+ };
3502
+ };
3503
+ var processOptions = (options, internalOptions) => {
3504
+ const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
3505
+ const {
3506
+ qstashHandlers,
3507
+ defaultClient: qstashClient,
3508
+ defaultReceiver: receiver
3509
+ } = getQStashHandlerOptions({
3510
+ environment,
3511
+ qstashClientOption: options?.qstashClient,
3512
+ receiverConfig: options && "receiver" in options ? options.receiver ? options.receiver : "set-to-undefined" : "not-set"
3513
+ });
3514
+ return {
3515
+ qstashClient,
3166
3516
  initialPayloadParser: (initialRequest) => {
3167
3517
  if (!initialRequest) {
3168
3518
  return void 0;
@@ -3177,35 +3527,38 @@ var processOptions = (options) => {
3177
3527
  throw error;
3178
3528
  }
3179
3529
  },
3180
- receiver: receiverEnvironmentVariablesSet ? new import_qstash10.Receiver({
3181
- currentSigningKey: environment.QSTASH_CURRENT_SIGNING_KEY,
3182
- nextSigningKey: environment.QSTASH_NEXT_SIGNING_KEY
3183
- }) : void 0,
3530
+ receiver,
3184
3531
  baseUrl: environment.UPSTASH_WORKFLOW_URL,
3185
3532
  env: environment,
3186
- retries: DEFAULT_RETRIES,
3187
- useJSONContent: false,
3188
3533
  disableTelemetry: false,
3189
- onError: console.error,
3190
- ...options
3534
+ ...options,
3535
+ // merge middlewares
3536
+ middlewares: [options?.middlewares ?? [], options?.verbose ? [loggingMiddleware] : []].flat(),
3537
+ internal: {
3538
+ generateResponse: internalOptions?.generateResponse ?? ((responseData) => {
3539
+ return new Response(responseData.text, {
3540
+ status: responseData.status,
3541
+ headers: responseData.headers
3542
+ });
3543
+ }),
3544
+ useJSONContent: internalOptions?.useJSONContent ?? false,
3545
+ qstashHandlers
3546
+ }
3191
3547
  };
3192
3548
  };
3193
- var determineUrls = async (request, url, baseUrl, failureFunction, failureUrl, debug) => {
3549
+ var determineUrls = async (request, url, baseUrl, dispatchDebug) => {
3194
3550
  const initialWorkflowUrl = url ?? request.url;
3195
3551
  const workflowUrl = baseUrl ? initialWorkflowUrl.replace(/^(https?:\/\/[^/]+)(\/.*)?$/, (_, matchedBaseUrl, path) => {
3196
3552
  return baseUrl + (path || "");
3197
3553
  }) : initialWorkflowUrl;
3198
3554
  if (workflowUrl !== initialWorkflowUrl) {
3199
- await debug?.log("WARN", "ENDPOINT_START", {
3200
- warning: `Upstash Workflow: replacing the base of the url with "${baseUrl}" and using it as workflow endpoint.`,
3201
- originalURL: initialWorkflowUrl,
3202
- updatedURL: workflowUrl
3555
+ await dispatchDebug("onInfo", {
3556
+ info: `The workflow URL's base URL has been replaced with the provided baseUrl. Original URL: ${initialWorkflowUrl}, New URL: ${workflowUrl}`
3203
3557
  });
3204
3558
  }
3205
- const workflowFailureUrl = failureFunction ? workflowUrl : failureUrl;
3206
3559
  if (workflowUrl.includes("localhost")) {
3207
- await debug?.log("WARN", "ENDPOINT_START", {
3208
- message: `Workflow URL contains localhost. This can happen in local development, but shouldn't happen in production unless you have a route which contains localhost. Received: ${workflowUrl}`
3560
+ await dispatchDebug("onInfo", {
3561
+ info: `Workflow URL contains localhost. This can happen in local development, but shouldn't happen in production unless you have a route which contains localhost. Received: ${workflowUrl}`
3209
3562
  });
3210
3563
  }
3211
3564
  if (!(workflowUrl.startsWith("http://") || workflowUrl.startsWith("https://"))) {
@@ -3214,205 +3567,224 @@ var determineUrls = async (request, url, baseUrl, failureFunction, failureUrl, d
3214
3567
  );
3215
3568
  }
3216
3569
  return {
3217
- workflowUrl,
3218
- workflowFailureUrl
3570
+ workflowUrl
3219
3571
  };
3220
3572
  };
3221
3573
  var AUTH_FAIL_MESSAGE = `Failed to authenticate Workflow request. If this is unexpected, see the caveat https://upstash.com/docs/workflow/basics/caveats#avoid-non-deterministic-code-outside-context-run`;
3222
3574
 
3223
3575
  // src/serve/index.ts
3224
- var serveBase = (routeFunction, telemetry, options) => {
3576
+ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3225
3577
  const {
3226
- qstashClient,
3227
- onStepFinish,
3228
3578
  initialPayloadParser,
3229
3579
  url,
3230
- verbose,
3231
- receiver,
3232
- failureUrl,
3233
3580
  failureFunction,
3234
3581
  baseUrl,
3235
3582
  env,
3236
- retries,
3237
- retryDelay,
3238
- useJSONContent,
3239
3583
  disableTelemetry,
3240
- flowControl,
3241
- onError
3242
- } = processOptions(options);
3584
+ middlewares,
3585
+ internal
3586
+ } = processOptions(options, internalOptions);
3243
3587
  telemetry = disableTelemetry ? void 0 : telemetry;
3244
- const debug = WorkflowLogger.getLogger(verbose);
3245
- const handler = async (request) => {
3246
- await debug?.log("INFO", "ENDPOINT_START");
3247
- const { workflowUrl, workflowFailureUrl } = await determineUrls(
3588
+ const { generateResponse: responseGenerator, useJSONContent } = internal;
3589
+ const handler = async (request, middlewareManager) => {
3590
+ await middlewareManager.dispatchDebug("onInfo", {
3591
+ info: `Received request for workflow execution.`
3592
+ });
3593
+ const { workflowUrl } = await determineUrls(
3248
3594
  request,
3249
3595
  url,
3250
3596
  baseUrl,
3251
- failureFunction,
3252
- failureUrl,
3253
- debug
3597
+ middlewareManager.dispatchDebug.bind(middlewareManager)
3598
+ );
3599
+ const { isFirstInvocation, workflowRunId, unknownSdk } = validateRequest(request);
3600
+ const regionHeader = request.headers.get("upstash-region");
3601
+ const { client: regionalClient, receiver: regionalReceiver } = getHandlersForRequest(
3602
+ internal.qstashHandlers,
3603
+ regionHeader,
3604
+ isFirstInvocation
3254
3605
  );
3255
3606
  const requestPayload = await getPayload(request) ?? "";
3256
- await verifyRequest(requestPayload, request.headers.get("upstash-signature"), receiver);
3257
- const { isFirstInvocation, workflowRunId } = validateRequest(request);
3258
- debug?.setWorkflowRunId(workflowRunId);
3259
- const { rawInitialPayload, steps, isLastDuplicate, workflowRunEnded } = await parseRequest(
3607
+ await verifyRequest(requestPayload, request.headers.get("upstash-signature"), regionalReceiver);
3608
+ middlewareManager.assignWorkflowRunId(workflowRunId);
3609
+ await middlewareManager.dispatchDebug("onInfo", {
3610
+ info: `Run id identified. isFirstInvocation: ${isFirstInvocation}, unknownSdk: ${unknownSdk}`
3611
+ });
3612
+ const { rawInitialPayload, steps, isLastDuplicate, workflowRunEnded } = await parseRequest({
3260
3613
  requestPayload,
3261
3614
  isFirstInvocation,
3615
+ unknownSdk,
3262
3616
  workflowRunId,
3263
- qstashClient.http,
3264
- request.headers.get("upstash-message-id"),
3265
- debug
3266
- );
3617
+ requester: regionalClient.http,
3618
+ messageId: request.headers.get("upstash-message-id"),
3619
+ dispatchDebug: middlewareManager.dispatchDebug.bind(middlewareManager)
3620
+ });
3267
3621
  if (workflowRunEnded) {
3268
- return onStepFinish(workflowRunId, "workflow-already-ended", {
3269
- condition: "workflow-already-ended"
3270
- });
3622
+ return responseGenerator(
3623
+ createResponseData(workflowRunId, {
3624
+ condition: "workflow-already-ended"
3625
+ })
3626
+ );
3271
3627
  }
3272
3628
  if (isLastDuplicate) {
3273
- return onStepFinish(workflowRunId, "duplicate-step", {
3274
- condition: "duplicate-step"
3275
- });
3629
+ return responseGenerator(
3630
+ createResponseData(workflowRunId, {
3631
+ condition: "duplicate-step"
3632
+ })
3633
+ );
3276
3634
  }
3277
- const failureCheck = await handleFailure(
3635
+ const failureCheck = await handleFailure({
3278
3636
  request,
3279
3637
  requestPayload,
3280
- qstashClient,
3638
+ qstashClient: regionalClient,
3281
3639
  initialPayloadParser,
3282
3640
  routeFunction,
3283
3641
  failureFunction,
3284
3642
  env,
3285
- retries,
3286
- retryDelay,
3287
- flowControl,
3288
- debug
3289
- );
3643
+ dispatchDebug: middlewareManager.dispatchDebug.bind(middlewareManager)
3644
+ });
3290
3645
  if (failureCheck.isErr()) {
3291
3646
  throw failureCheck.error;
3292
- } else if (failureCheck.value.result === "is-failure-callback") {
3293
- await debug?.log("WARN", "RESPONSE_DEFAULT", "failureFunction executed");
3294
- return onStepFinish(workflowRunId, "failure-callback", {
3295
- condition: "failure-callback",
3296
- result: failureCheck.value.response
3647
+ } else if (failureCheck.value.result === "failure-function-executed") {
3648
+ await middlewareManager.dispatchDebug("onInfo", {
3649
+ info: `Handled failure callback.`
3297
3650
  });
3651
+ return responseGenerator(
3652
+ createResponseData(workflowRunId, {
3653
+ condition: "failure-callback-executed",
3654
+ result: failureCheck.value.response
3655
+ })
3656
+ );
3657
+ } else if (failureCheck.value.result === "failure-function-undefined") {
3658
+ await middlewareManager.dispatchDebug("onInfo", {
3659
+ info: `Failure callback invoked but no failure function defined.`
3660
+ });
3661
+ return responseGenerator(
3662
+ createResponseData(workflowRunId, {
3663
+ condition: "failure-callback-undefined"
3664
+ })
3665
+ );
3298
3666
  }
3299
3667
  const invokeCount = Number(request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER) ?? "0");
3300
3668
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3301
3669
  const workflowContext = new WorkflowContext({
3302
- qstashClient,
3670
+ qstashClient: regionalClient,
3303
3671
  workflowRunId,
3304
3672
  initialPayload: initialPayloadParser(rawInitialPayload),
3305
3673
  headers: recreateUserHeaders(request.headers),
3306
3674
  steps,
3307
3675
  url: workflowUrl,
3308
- failureUrl: workflowFailureUrl,
3309
- debug,
3310
3676
  env,
3311
- retries,
3312
- retryDelay,
3313
3677
  telemetry,
3314
3678
  invokeCount,
3315
- flowControl,
3316
- label
3679
+ label,
3680
+ middlewareManager
3317
3681
  });
3318
3682
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3319
3683
  routeFunction,
3320
3684
  workflowContext
3321
3685
  );
3322
3686
  if (authCheck.isErr()) {
3323
- await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
3324
3687
  throw authCheck.error;
3325
3688
  } else if (authCheck.value === "run-ended") {
3326
- await debug?.log("ERROR", "ERROR", { error: AUTH_FAIL_MESSAGE });
3327
- return onStepFinish(
3328
- isFirstInvocation ? "no-workflow-id" : workflowContext.workflowRunId,
3329
- "auth-fail",
3330
- { condition: "auth-fail" }
3689
+ await middlewareManager.dispatchDebug("onError", {
3690
+ error: new Error(AUTH_FAIL_MESSAGE)
3691
+ });
3692
+ return responseGenerator(
3693
+ createResponseData(isFirstInvocation ? "no-workflow-id" : workflowContext.workflowRunId, {
3694
+ condition: "auth-fail"
3695
+ })
3331
3696
  );
3332
3697
  }
3333
3698
  const callReturnCheck = await handleThirdPartyCallResult({
3334
3699
  request,
3335
3700
  requestPayload: rawInitialPayload,
3336
- client: qstashClient,
3701
+ client: regionalClient,
3337
3702
  workflowUrl,
3338
- failureUrl: workflowFailureUrl,
3339
- retries,
3340
- retryDelay,
3341
- flowControl,
3342
3703
  telemetry,
3343
- debug
3704
+ middlewareManager
3344
3705
  });
3345
3706
  if (callReturnCheck.isErr()) {
3346
- await debug?.log("ERROR", "SUBMIT_THIRD_PARTY_RESULT", {
3347
- error: callReturnCheck.error.message
3348
- });
3349
3707
  throw callReturnCheck.error;
3350
3708
  } else if (callReturnCheck.value === "continue-workflow") {
3351
3709
  const result = isFirstInvocation ? await triggerFirstInvocation({
3352
3710
  workflowContext,
3353
3711
  useJSONContent,
3354
3712
  telemetry,
3355
- debug,
3356
- invokeCount
3713
+ invokeCount,
3714
+ middlewareManager,
3715
+ unknownSdk
3357
3716
  }) : await triggerRouteFunction({
3358
- onStep: async () => routeFunction(workflowContext),
3717
+ onStep: async () => {
3718
+ if (steps.length === 1) {
3719
+ await middlewareManager.dispatchLifecycle("runStarted", {});
3720
+ }
3721
+ return await routeFunction(workflowContext);
3722
+ },
3359
3723
  onCleanup: async (result2) => {
3360
- await triggerWorkflowDelete(workflowContext, result2, debug);
3724
+ await middlewareManager.dispatchLifecycle("runCompleted", {
3725
+ result: result2
3726
+ });
3727
+ await triggerWorkflowDelete(
3728
+ workflowContext,
3729
+ result2,
3730
+ false,
3731
+ middlewareManager.dispatchDebug.bind(middlewareManager)
3732
+ );
3361
3733
  },
3362
3734
  onCancel: async () => {
3363
3735
  await makeCancelRequest(workflowContext.qstashClient.http, workflowRunId);
3364
3736
  },
3365
- debug
3737
+ middlewareManager
3366
3738
  });
3367
3739
  if (result.isOk() && isInstanceOf(result.value, WorkflowNonRetryableError)) {
3368
- return onStepFinish(workflowRunId, result.value, {
3369
- condition: "non-retryable-error",
3370
- result: result.value
3371
- });
3740
+ return responseGenerator(
3741
+ createResponseData(workflowRunId, {
3742
+ condition: "non-retryable-error",
3743
+ result: result.value
3744
+ })
3745
+ );
3372
3746
  }
3373
3747
  if (result.isOk() && isInstanceOf(result.value, WorkflowRetryAfterError)) {
3374
- return onStepFinish(workflowRunId, result.value, {
3375
- condition: "retry-after-error",
3376
- result: result.value
3377
- });
3748
+ return responseGenerator(
3749
+ createResponseData(workflowRunId, {
3750
+ condition: "retry-after-error",
3751
+ result: result.value
3752
+ })
3753
+ );
3378
3754
  }
3379
3755
  if (result.isErr()) {
3380
- await debug?.log("ERROR", "ERROR", { error: result.error.message });
3381
3756
  throw result.error;
3382
3757
  }
3383
- await debug?.log("INFO", "RESPONSE_WORKFLOW");
3384
- return onStepFinish(workflowContext.workflowRunId, "success", {
3385
- condition: "success"
3758
+ await middlewareManager.dispatchDebug("onInfo", {
3759
+ info: `Workflow endpoint execution completed successfully.`
3386
3760
  });
3761
+ return responseGenerator(
3762
+ createResponseData(workflowContext.workflowRunId, {
3763
+ condition: "success"
3764
+ })
3765
+ );
3387
3766
  } else if (callReturnCheck.value === "workflow-ended") {
3388
- return onStepFinish(workflowContext.workflowRunId, "workflow-already-ended", {
3389
- condition: "workflow-already-ended"
3390
- });
3767
+ return responseGenerator(
3768
+ createResponseData(workflowContext.workflowRunId, {
3769
+ condition: "workflow-already-ended"
3770
+ })
3771
+ );
3391
3772
  }
3392
- await debug?.log("INFO", "RESPONSE_DEFAULT");
3393
- return onStepFinish("no-workflow-id", "fromCallback", {
3394
- condition: "fromCallback"
3395
- });
3773
+ return responseGenerator(
3774
+ createResponseData(workflowContext.workflowRunId, {
3775
+ condition: "fromCallback"
3776
+ })
3777
+ );
3396
3778
  };
3397
3779
  const safeHandler = async (request) => {
3780
+ const middlewareManager = new MiddlewareManager(middlewares);
3398
3781
  try {
3399
- return await handler(request);
3782
+ return await handler(request, middlewareManager);
3400
3783
  } catch (error) {
3401
3784
  const formattedError = formatWorkflowError(error);
3402
- try {
3403
- onError?.(error);
3404
- } catch (onErrorError) {
3405
- const formattedOnErrorError = formatWorkflowError(onErrorError);
3406
- const errorMessage = `Error while running onError callback: '${formattedOnErrorError.message}'.
3407
- Original error: '${formattedError.message}'`;
3408
- console.error(errorMessage);
3409
- return new Response(JSON.stringify({ error: errorMessage }), {
3410
- status: 500,
3411
- headers: {
3412
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3413
- }
3414
- });
3415
- }
3785
+ await middlewareManager.dispatchDebug("onError", {
3786
+ error: isInstanceOf(error, Error) ? error : new Error(formattedError.message)
3787
+ });
3416
3788
  return new Response(JSON.stringify(formattedError), {
3417
3789
  status: 500,
3418
3790
  headers: {
@@ -3468,7 +3840,7 @@ var serveMany = (workflows, options) => {
3468
3840
  };
3469
3841
  };
3470
3842
  var servePagesRouter = (routeFunction, options) => {
3471
- const { handler: serveHandler } = serveBase(routeFunction, pagesTelemetry, options);
3843
+ const { handler: serveHandler } = serveBase(routeFunction, pagesTelemetry, options, void 0);
3472
3844
  const handler = async (request_, res) => {
3473
3845
  if (request_.method?.toUpperCase() !== "POST") {
3474
3846
  res.status(405).json("Only POST requests are allowed in worklfows");