@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.
@@ -7,26 +7,36 @@ var WorkflowError = class extends QstashError {
7
7
  }
8
8
  };
9
9
  var WorkflowAbort = class extends Error {
10
- stepInfo;
11
10
  stepName;
11
+ stepInfo;
12
12
  /**
13
- * whether workflow is to be canceled on abort
14
- */
15
- cancelWorkflow;
16
- /**
17
- *
18
13
  * @param stepName name of the aborting step
19
14
  * @param stepInfo step information
20
- * @param cancelWorkflow
21
15
  */
22
- constructor(stepName, stepInfo, cancelWorkflow = false) {
16
+ constructor(stepName, stepInfo) {
23
17
  super(
24
18
  `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}'.`
25
19
  );
26
20
  this.name = "WorkflowAbort";
27
21
  this.stepName = stepName;
28
22
  this.stepInfo = stepInfo;
29
- this.cancelWorkflow = cancelWorkflow;
23
+ }
24
+ };
25
+ var WorkflowAuthError = class extends WorkflowAbort {
26
+ /**
27
+ * @param stepName name of the step found during authorization
28
+ */
29
+ constructor(stepName) {
30
+ super(stepName);
31
+ this.name = "WorkflowAuthError";
32
+ this.message = `This is an Upstash Workflow error thrown during authorization check. Found step '${stepName}' during dry-run.`;
33
+ }
34
+ };
35
+ var WorkflowCancelAbort = class extends WorkflowAbort {
36
+ constructor() {
37
+ super("cancel");
38
+ this.name = "WorkflowCancelAbort";
39
+ this.message = "Workflow has been canceled by user via context.cancel().";
30
40
  }
31
41
  };
32
42
  var WorkflowNonRetryableError = class extends WorkflowAbort {
@@ -34,22 +44,22 @@ var WorkflowNonRetryableError = class extends WorkflowAbort {
34
44
  * @param message error message to be displayed
35
45
  */
36
46
  constructor(message) {
37
- super("fail", void 0, false);
47
+ super("non-retryable-error");
38
48
  this.name = "WorkflowNonRetryableError";
39
- if (message) this.message = message;
49
+ this.message = message ?? "Workflow failed with non-retryable error.";
40
50
  }
41
51
  };
42
52
  var WorkflowRetryAfterError = class extends WorkflowAbort {
43
53
  retryAfter;
44
54
  /**
45
- * @param retryAfter time in seconds after which the workflow should be retried
46
55
  * @param message error message to be displayed
56
+ * @param retryAfter time in seconds after which the workflow should be retried
47
57
  */
48
58
  constructor(message, retryAfter) {
49
- super("retry", void 0, false);
59
+ super("retry-after-error");
50
60
  this.name = "WorkflowRetryAfterError";
61
+ this.message = message;
51
62
  this.retryAfter = retryAfter;
52
- if (message) this.message = message;
53
63
  }
54
64
  };
55
65
  var formatWorkflowError = (error) => {
@@ -89,15 +99,18 @@ var WORKFLOW_ID_HEADER = "Upstash-Workflow-RunId";
89
99
  var WORKFLOW_INIT_HEADER = "Upstash-Workflow-Init";
90
100
  var WORKFLOW_URL_HEADER = "Upstash-Workflow-Url";
91
101
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
102
+ var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
92
103
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
93
104
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
94
105
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
106
+ var WORKFLOW_UNKOWN_SDK_VERSION_HEADER = "Upstash-Workflow-Unknown-Sdk";
107
+ var WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER = "upstash-workflow-trigger-by-sdk";
95
108
  var WORKFLOW_PROTOCOL_VERSION = "1";
96
109
  var WORKFLOW_PROTOCOL_VERSION_HEADER = "Upstash-Workflow-Sdk-Version";
97
110
  var DEFAULT_CONTENT_TYPE = "application/json";
98
111
  var NO_CONCURRENCY = 1;
99
112
  var DEFAULT_RETRIES = 3;
100
- var VERSION = "v0.3.0-rc";
113
+ var VERSION = "v1.0.0";
101
114
  var SDK_TELEMETRY = `@upstash/workflow@${VERSION}`;
102
115
  var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
103
116
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
@@ -112,7 +125,9 @@ var StepTypes = [
112
125
  "Call",
113
126
  "Wait",
114
127
  "Notify",
115
- "Invoke"
128
+ "Invoke",
129
+ "CreateWebhook",
130
+ "WaitForWebhook"
116
131
  ];
117
132
 
118
133
  // src/serve/serve-many.ts
@@ -221,15 +236,21 @@ var makeCancelRequest = async (requester, workflowRunId) => {
221
236
  });
222
237
  return true;
223
238
  };
224
- var getSteps = async (requester, workflowRunId, messageId, debug) => {
239
+ var getSteps = async (requester, workflowRunId, messageId, dispatchDebug) => {
225
240
  try {
226
241
  const steps = await requester.request({
227
242
  path: ["v2", "workflows", "runs", workflowRunId],
228
243
  parseResponseAsJson: true
229
244
  });
245
+ if (steps.length === 1) {
246
+ return {
247
+ steps,
248
+ workflowRunEnded: false
249
+ };
250
+ }
230
251
  if (!messageId) {
231
- await debug?.log("INFO", "ENDPOINT_START", {
232
- message: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
252
+ await dispatchDebug?.("onInfo", {
253
+ info: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
233
254
  });
234
255
  return { steps, workflowRunEnded: false };
235
256
  } else {
@@ -238,16 +259,15 @@ var getSteps = async (requester, workflowRunId, messageId, debug) => {
238
259
  return { steps: [], workflowRunEnded: false };
239
260
  }
240
261
  const filteredSteps = steps.slice(0, index + 1);
241
- await debug?.log("INFO", "ENDPOINT_START", {
242
- message: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
262
+ await dispatchDebug?.("onInfo", {
263
+ info: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
243
264
  });
244
265
  return { steps: filteredSteps, workflowRunEnded: false };
245
266
  }
246
267
  } catch (error) {
247
268
  if (isInstanceOf(error, QstashError2) && error.status === 404) {
248
- await debug?.log("WARN", "ENDPOINT_START", {
249
- message: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed.",
250
- error
269
+ await dispatchDebug?.("onWarning", {
270
+ warning: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed."
251
271
  });
252
272
  return { steps: void 0, workflowRunEnded: true };
253
273
  } else {
@@ -262,8 +282,8 @@ var NANOID_LENGTH = 21;
262
282
  function getRandomInt() {
263
283
  return Math.floor(Math.random() * NANOID_CHARS.length);
264
284
  }
265
- function nanoid() {
266
- return Array.from({ length: NANOID_LENGTH }).map(() => NANOID_CHARS[getRandomInt()]).join("");
285
+ function nanoid(length = NANOID_LENGTH) {
286
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
267
287
  }
268
288
  function getWorkflowRunId(id) {
269
289
  return `wfr_${id ?? nanoid()}`;
@@ -280,6 +300,46 @@ function decodeBase64(base64) {
280
300
  return binString;
281
301
  }
282
302
  }
303
+ function getUserIdFromToken(qstashClient) {
304
+ try {
305
+ const token = qstashClient.token;
306
+ const decodedToken = decodeBase64(token);
307
+ const tokenPayload = JSON.parse(decodedToken);
308
+ const userId = tokenPayload.UserID;
309
+ if (!userId) {
310
+ throw new WorkflowError("QStash token payload does not contain userId");
311
+ }
312
+ return userId;
313
+ } catch (error) {
314
+ throw new WorkflowError(
315
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
316
+ );
317
+ }
318
+ }
319
+ function getQStashUrl(qstashClient) {
320
+ try {
321
+ const requester = qstashClient.http;
322
+ const baseUrl = requester.baseUrl;
323
+ if (!baseUrl) {
324
+ throw new WorkflowError("QStash client does not have a baseUrl");
325
+ }
326
+ return baseUrl;
327
+ } catch (error) {
328
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
329
+ }
330
+ }
331
+ function getEventId() {
332
+ return `evt_${nanoid(15)}`;
333
+ }
334
+ function stringifyBody(body) {
335
+ if (body === void 0) {
336
+ return void 0;
337
+ }
338
+ if (typeof body === "string") {
339
+ return body;
340
+ }
341
+ return JSON.stringify(body);
342
+ }
283
343
 
284
344
  // node_modules/neverthrow/dist/index.es.js
285
345
  var defaultErrorConfig = {
@@ -709,23 +769,26 @@ var triggerFirstInvocation = async (params) => {
709
769
  invokeCount,
710
770
  delay,
711
771
  notBefore,
712
- keepTriggerConfig
772
+ failureUrl,
773
+ retries,
774
+ retryDelay,
775
+ flowControl,
776
+ unknownSdk
713
777
  }) => {
714
778
  const { headers } = getHeaders({
715
779
  initHeaderValue: "true",
716
780
  workflowConfig: {
717
781
  workflowRunId: workflowContext.workflowRunId,
718
782
  workflowUrl: workflowContext.url,
719
- failureUrl: workflowContext.failureUrl,
720
- retries: workflowContext.retries,
721
- retryDelay: workflowContext.retryDelay,
783
+ failureUrl,
784
+ retries,
785
+ retryDelay,
722
786
  telemetry,
723
- flowControl: workflowContext.flowControl,
787
+ flowControl,
724
788
  useJSONContent: useJSONContent ?? false
725
789
  },
726
790
  invokeCount: invokeCount ?? 0,
727
- userHeaders: workflowContext.headers,
728
- keepTriggerConfig
791
+ userHeaders: workflowContext.headers
729
792
  });
730
793
  if (workflowContext.headers.get("content-type")) {
731
794
  headers["content-type"] = workflowContext.headers.get("content-type");
@@ -733,6 +796,9 @@ var triggerFirstInvocation = async (params) => {
733
796
  if (useJSONContent) {
734
797
  headers["content-type"] = "application/json";
735
798
  }
799
+ if (unknownSdk) {
800
+ headers[WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER] = "true";
801
+ }
736
802
  if (workflowContext.label) {
737
803
  headers[WORKFLOW_LABEL_HEADER] = workflowContext.label;
738
804
  }
@@ -753,21 +819,15 @@ var triggerFirstInvocation = async (params) => {
753
819
  for (let i = 0; i < results.length; i++) {
754
820
  const result = results[i];
755
821
  const invocationParams = firstInvocationParams[i];
822
+ invocationParams.middlewareManager?.assignContext(invocationParams.workflowContext);
756
823
  if (result.deduplicated) {
757
- await invocationParams.debug?.log("WARN", "SUBMIT_FIRST_INVOCATION", {
758
- message: `Workflow run ${invocationParams.workflowContext.workflowRunId} already exists. A new one isn't created.`,
759
- headers: invocationBatch[i].headers,
760
- requestPayload: invocationParams.workflowContext.requestPayload,
761
- url: invocationParams.workflowContext.url,
762
- messageId: result.messageId
824
+ await invocationParams.middlewareManager?.dispatchDebug("onWarning", {
825
+ warning: `Workflow run ${invocationParams.workflowContext.workflowRunId} already exists. A new one isn't created.`
763
826
  });
764
827
  invocationStatuses.push("workflow-run-already-exists");
765
828
  } else {
766
- await invocationParams.debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
767
- headers: invocationBatch[i].headers,
768
- requestPayload: invocationParams.workflowContext.requestPayload,
769
- url: invocationParams.workflowContext.url,
770
- messageId: result.messageId
829
+ await invocationParams.middlewareManager?.dispatchDebug("onInfo", {
830
+ info: `Workflow run started successfully with URL ${invocationParams.workflowContext.url}.`
771
831
  });
772
832
  invocationStatuses.push("success");
773
833
  }
@@ -789,7 +849,7 @@ var triggerRouteFunction = async ({
789
849
  onCleanup,
790
850
  onStep,
791
851
  onCancel,
792
- debug
852
+ middlewareManager
793
853
  }) => {
794
854
  try {
795
855
  const result = await onStep();
@@ -798,27 +858,25 @@ var triggerRouteFunction = async ({
798
858
  } catch (error) {
799
859
  const error_ = error;
800
860
  if (isInstanceOf(error, QstashError3) && error.status === 400) {
801
- await debug?.log("WARN", "RESPONSE_WORKFLOW", {
802
- message: `tried to append to a cancelled workflow. exiting without publishing.`,
803
- name: error.name,
804
- errorMessage: error.message
861
+ await middlewareManager?.dispatchDebug("onWarning", {
862
+ warning: `Tried to append to a cancelled workflow. Exiting without publishing. Error: ${error.message}`
805
863
  });
806
864
  return ok("workflow-was-finished");
807
865
  } else if (isInstanceOf(error_, WorkflowNonRetryableError) || isInstanceOf(error_, WorkflowRetryAfterError)) {
808
866
  return ok(error_);
809
- } else if (!isInstanceOf(error_, WorkflowAbort)) {
810
- return err(error_);
811
- } else if (error_.cancelWorkflow) {
867
+ } else if (isInstanceOf(error_, WorkflowCancelAbort)) {
812
868
  await onCancel();
813
869
  return ok("workflow-finished");
814
- } else {
870
+ } else if (isInstanceOf(error_, WorkflowAbort)) {
815
871
  return ok("step-finished");
872
+ } else {
873
+ return err(error_);
816
874
  }
817
875
  }
818
876
  };
819
- var triggerWorkflowDelete = async (workflowContext, result, debug, cancel = false) => {
820
- await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
821
- deletedWorkflowRunId: workflowContext.workflowRunId
877
+ var triggerWorkflowDelete = async (workflowContext, result, cancel = false, dispatchDebug) => {
878
+ await dispatchDebug?.("onInfo", {
879
+ info: `Deleting workflow run ${workflowContext.workflowRunId} from QStash` + (cancel ? " with cancel=true." : ".")
822
880
  });
823
881
  await workflowContext.qstashClient.http.request({
824
882
  path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
@@ -826,18 +884,16 @@ var triggerWorkflowDelete = async (workflowContext, result, debug, cancel = fals
826
884
  parseResponseAsJson: false,
827
885
  body: JSON.stringify(result)
828
886
  });
829
- await debug?.log(
830
- "SUBMIT",
831
- "SUBMIT_CLEANUP",
832
- `workflow run ${workflowContext.workflowRunId} deleted.`
833
- );
887
+ await dispatchDebug?.("onInfo", {
888
+ info: `Workflow run ${workflowContext.workflowRunId} deleted from QStash successfully.`
889
+ });
834
890
  };
835
891
  var recreateUserHeaders = (headers) => {
836
892
  const filteredHeaders = new Headers();
837
893
  const pairs = headers.entries();
838
894
  for (const [header, value] of pairs) {
839
895
  const headerLowerCase = header.toLowerCase();
840
- const isUserHeader = !headerLowerCase.startsWith("upstash-workflow-") && // https://vercel.com/docs/edge-network/headers/request-headers#x-vercel-id
896
+ const isUserHeader = headerLowerCase !== "upstash-region" && !headerLowerCase.startsWith("upstash-workflow-") && // https://vercel.com/docs/edge-network/headers/request-headers#x-vercel-id
841
897
  !headerLowerCase.startsWith("x-vercel-") && !headerLowerCase.startsWith("x-forwarded-") && // https://blog.cloudflare.com/preventing-request-loops-using-cdn-loop/
842
898
  headerLowerCase !== "cf-connecting-ip" && headerLowerCase !== "cdn-loop" && headerLowerCase !== "cf-ew-via" && headerLowerCase !== "cf-ray" && // For Render https://render.com
843
899
  headerLowerCase !== "render-proxy-ttl" || headerLowerCase === WORKFLOW_LABEL_HEADER.toLocaleLowerCase();
@@ -852,12 +908,8 @@ var handleThirdPartyCallResult = async ({
852
908
  requestPayload,
853
909
  client,
854
910
  workflowUrl,
855
- failureUrl,
856
- retries,
857
- retryDelay,
858
911
  telemetry,
859
- flowControl,
860
- debug
912
+ middlewareManager
861
913
  }) => {
862
914
  try {
863
915
  if (request.headers.get("Upstash-Workflow-Callback")) {
@@ -874,7 +926,7 @@ var handleThirdPartyCallResult = async ({
874
926
  client.http,
875
927
  workflowRunId2,
876
928
  messageId,
877
- debug
929
+ middlewareManager?.dispatchDebug.bind(middlewareManager)
878
930
  );
879
931
  if (workflowRunEnded) {
880
932
  return ok("workflow-ended");
@@ -888,9 +940,8 @@ var handleThirdPartyCallResult = async ({
888
940
  }
889
941
  const callbackMessage = JSON.parse(callbackPayload);
890
942
  if (!(callbackMessage.status >= 200 && callbackMessage.status < 300) && callbackMessage.maxRetries && callbackMessage.retried !== callbackMessage.maxRetries) {
891
- await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
892
- status: callbackMessage.status,
893
- body: atob(callbackMessage.body ?? "")
943
+ await middlewareManager?.dispatchDebug("onWarning", {
944
+ warning: `Third party call returned status ${callbackMessage.status}. Retrying (${callbackMessage.retried} out of ${callbackMessage.maxRetries}).`
894
945
  });
895
946
  console.warn(
896
947
  `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (retried ${callbackMessage.retried ?? 0} out of ${callbackMessage.maxRetries} times). Error Message:
@@ -923,11 +974,7 @@ ${atob(callbackMessage.body ?? "")}`
923
974
  workflowConfig: {
924
975
  workflowRunId,
925
976
  workflowUrl,
926
- failureUrl,
927
- retries,
928
- retryDelay,
929
- telemetry,
930
- flowControl
977
+ telemetry
931
978
  },
932
979
  userHeaders,
933
980
  invokeCount: Number(invokeCount)
@@ -944,19 +991,17 @@ ${atob(callbackMessage.body ?? "")}`
944
991
  out: JSON.stringify(callResponse),
945
992
  concurrent: Number(concurrentString)
946
993
  };
947
- await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
948
- step: callResultStep,
949
- headers: requestHeaders,
950
- url: workflowUrl
994
+ await middlewareManager?.dispatchDebug("onInfo", {
995
+ info: `Submitting third party call result, step ${stepName} (${stepIdString}).`
951
996
  });
952
- const result = await client.publishJSON({
997
+ await client.publishJSON({
953
998
  headers: requestHeaders,
954
999
  method: "POST",
955
1000
  body: callResultStep,
956
1001
  url: workflowUrl
957
1002
  });
958
- await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
959
- messageId: result.messageId
1003
+ await middlewareManager?.dispatchDebug("onInfo", {
1004
+ info: `Third party call result submitted successfully, step ${stepName} (${stepIdString}).`
960
1005
  });
961
1006
  return ok("is-call-return");
962
1007
  } else {
@@ -1005,15 +1050,17 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1005
1050
  // src/context/steps.ts
1006
1051
  var BaseLazyStep = class _BaseLazyStep {
1007
1052
  stepName;
1008
- constructor(stepName) {
1053
+ context;
1054
+ constructor(context, stepName) {
1055
+ this.context = context;
1009
1056
  if (!stepName) {
1010
1057
  throw new WorkflowError(
1011
1058
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
1012
1059
  );
1013
1060
  }
1014
1061
  if (typeof stepName !== "string") {
1015
- console.warn(
1016
- "Workflow Warning: A workflow step name must be a string. In a future release, this will throw an error."
1062
+ throw new WorkflowError(
1063
+ `A workflow step name must be a string. Received "${stepName}" (${typeof stepName}).`
1017
1064
  );
1018
1065
  }
1019
1066
  this.stepName = stepName;
@@ -1023,13 +1070,14 @@ var BaseLazyStep = class _BaseLazyStep {
1023
1070
  *
1024
1071
  * will be called when returning the steps to the context from auto executor
1025
1072
  *
1026
- * @param out field of the step
1073
+ * @param step step
1027
1074
  * @returns parsed out field
1028
1075
  */
1029
- parseOut(out) {
1076
+ parseOut(step) {
1077
+ const out = step.out;
1030
1078
  if (out === void 0) {
1031
1079
  if (this.allowUndefinedOut) {
1032
- return void 0;
1080
+ return this.handleUndefinedOut(step);
1033
1081
  } else {
1034
1082
  throw new WorkflowError(
1035
1083
  `Error while parsing output of ${this.stepType} step. Expected a string, but got: undefined`
@@ -1037,27 +1085,26 @@ var BaseLazyStep = class _BaseLazyStep {
1037
1085
  }
1038
1086
  }
1039
1087
  if (typeof out === "object") {
1040
- if (this.stepType !== "Wait") {
1041
- console.warn(
1042
- `Error while parsing ${this.stepType} step output. Expected a string, but got object. Please reach out to Upstash Support.`
1043
- );
1044
- return out;
1045
- }
1046
- return {
1047
- ...out,
1048
- eventData: _BaseLazyStep.tryParsing(out.eventData)
1049
- };
1088
+ console.warn(
1089
+ `Error while parsing ${this.stepType} step output. Expected a string, but got object. Please reach out to Upstash Support.`
1090
+ );
1091
+ return out;
1050
1092
  }
1051
1093
  if (typeof out !== "string") {
1052
1094
  throw new WorkflowError(
1053
1095
  `Error while parsing output of ${this.stepType} step. Expected a string or undefined, but got: ${typeof out}`
1054
1096
  );
1055
1097
  }
1056
- return this.safeParseOut(out);
1098
+ return this.safeParseOut(out, step);
1057
1099
  }
1058
- safeParseOut(out) {
1100
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1101
+ safeParseOut(out, step) {
1059
1102
  return _BaseLazyStep.tryParsing(out);
1060
1103
  }
1104
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1105
+ handleUndefinedOut(step) {
1106
+ return void 0;
1107
+ }
1061
1108
  static tryParsing(stepOut) {
1062
1109
  try {
1063
1110
  return JSON.parse(stepOut);
@@ -1075,12 +1122,8 @@ var BaseLazyStep = class _BaseLazyStep {
1075
1122
  workflowConfig: {
1076
1123
  workflowRunId: context.workflowRunId,
1077
1124
  workflowUrl: context.url,
1078
- failureUrl: context.failureUrl,
1079
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1080
- retryDelay: context.retryDelay,
1081
1125
  useJSONContent: false,
1082
- telemetry,
1083
- flowControl: context.flowControl
1126
+ telemetry
1084
1127
  },
1085
1128
  userHeaders: context.headers,
1086
1129
  invokeCount,
@@ -1096,9 +1139,6 @@ var BaseLazyStep = class _BaseLazyStep {
1096
1139
  body,
1097
1140
  headers,
1098
1141
  method: "POST",
1099
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1100
- retryDelay: context.retryDelay,
1101
- flowControl: context.flowControl,
1102
1142
  url: context.url
1103
1143
  }
1104
1144
  ]);
@@ -1108,8 +1148,8 @@ var LazyFunctionStep = class extends BaseLazyStep {
1108
1148
  stepFunction;
1109
1149
  stepType = "Run";
1110
1150
  allowUndefinedOut = true;
1111
- constructor(stepName, stepFunction) {
1112
- super(stepName);
1151
+ constructor(context, stepName, stepFunction) {
1152
+ super(context, stepName);
1113
1153
  this.stepFunction = stepFunction;
1114
1154
  }
1115
1155
  getPlanStep(concurrent, targetStep) {
@@ -1139,8 +1179,8 @@ var LazySleepStep = class extends BaseLazyStep {
1139
1179
  sleep;
1140
1180
  stepType = "SleepFor";
1141
1181
  allowUndefinedOut = true;
1142
- constructor(stepName, sleep) {
1143
- super(stepName);
1182
+ constructor(context, stepName, sleep) {
1183
+ super(context, stepName);
1144
1184
  this.sleep = sleep;
1145
1185
  }
1146
1186
  getPlanStep(concurrent, targetStep) {
@@ -1169,9 +1209,6 @@ var LazySleepStep = class extends BaseLazyStep {
1169
1209
  headers,
1170
1210
  method: "POST",
1171
1211
  url: context.url,
1172
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1173
- retryDelay: context.retryDelay,
1174
- flowControl: context.flowControl,
1175
1212
  delay: isParallel ? void 0 : this.sleep
1176
1213
  }
1177
1214
  ]);
@@ -1181,8 +1218,8 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1181
1218
  sleepUntil;
1182
1219
  stepType = "SleepUntil";
1183
1220
  allowUndefinedOut = true;
1184
- constructor(stepName, sleepUntil) {
1185
- super(stepName);
1221
+ constructor(context, stepName, sleepUntil) {
1222
+ super(context, stepName);
1186
1223
  this.sleepUntil = sleepUntil;
1187
1224
  }
1188
1225
  getPlanStep(concurrent, targetStep) {
@@ -1214,9 +1251,6 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1214
1251
  headers,
1215
1252
  method: "POST",
1216
1253
  url: context.url,
1217
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1218
- retryDelay: context.retryDelay,
1219
- flowControl: context.flowControl,
1220
1254
  notBefore: isParallel ? void 0 : this.sleepUntil
1221
1255
  }
1222
1256
  ]);
@@ -1231,20 +1265,18 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1231
1265
  retryDelay;
1232
1266
  timeout;
1233
1267
  flowControl;
1234
- stringifyBody;
1235
1268
  stepType = "Call";
1236
1269
  allowUndefinedOut = false;
1237
- constructor(stepName, url, method, body, headers, retries, retryDelay, timeout, flowControl, stringifyBody) {
1238
- super(stepName);
1239
- this.url = url;
1240
- this.method = method;
1241
- this.body = body;
1242
- this.headers = headers;
1243
- this.retries = retries;
1244
- this.retryDelay = retryDelay;
1245
- this.timeout = timeout;
1246
- this.flowControl = flowControl;
1247
- this.stringifyBody = stringifyBody;
1270
+ constructor(params) {
1271
+ super(params.context, params.stepName);
1272
+ this.url = params.url;
1273
+ this.method = params.method ?? "GET";
1274
+ this.body = params.body;
1275
+ this.headers = params.headers ?? {};
1276
+ this.retries = params.retries ?? 0;
1277
+ this.retryDelay = params.retryDelay;
1278
+ this.timeout = params.timeout;
1279
+ this.flowControl = params.flowControl;
1248
1280
  }
1249
1281
  getPlanStep(concurrent, targetStep) {
1250
1282
  return {
@@ -1341,7 +1373,7 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1341
1373
  "Upstash-Callback-Workflow-CallType": "fromCallback",
1342
1374
  "Upstash-Callback-Workflow-Init": "false",
1343
1375
  "Upstash-Callback-Workflow-Url": context.url,
1344
- "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody,WF_DetectTrigger",
1376
+ "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1345
1377
  "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
1346
1378
  "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
1347
1379
  "Upstash-Callback-Forward-Upstash-Workflow-StepName": this.stepName,
@@ -1354,22 +1386,10 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1354
1386
  };
1355
1387
  }
1356
1388
  async submitStep({ context, headers }) {
1357
- let callBody;
1358
- if (this.stringifyBody) {
1359
- callBody = JSON.stringify(this.body);
1360
- } else {
1361
- if (typeof this.body === "string") {
1362
- callBody = this.body;
1363
- } else {
1364
- throw new WorkflowError(
1365
- "When stringifyBody is false, body must be a string. Please check the body type of your call step."
1366
- );
1367
- }
1368
- }
1369
1389
  return await context.qstashClient.batch([
1370
1390
  {
1371
1391
  headers,
1372
- body: callBody,
1392
+ body: this.body,
1373
1393
  method: this.method,
1374
1394
  url: this.url,
1375
1395
  retries: DEFAULT_RETRIES === this.retries ? void 0 : this.retries,
@@ -1379,13 +1399,12 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1379
1399
  ]);
1380
1400
  }
1381
1401
  };
1382
- var LazyWaitForEventStep = class extends BaseLazyStep {
1402
+ var LazyWaitEventStep = class extends BaseLazyStep {
1383
1403
  eventId;
1384
1404
  timeout;
1385
- stepType = "Wait";
1386
1405
  allowUndefinedOut = false;
1387
- constructor(stepName, eventId, timeout) {
1388
- super(stepName);
1406
+ constructor(context, stepName, eventId, timeout) {
1407
+ super(context, stepName);
1389
1408
  this.eventId = eventId;
1390
1409
  this.timeout = timeout;
1391
1410
  }
@@ -1410,13 +1429,6 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1410
1429
  concurrent
1411
1430
  });
1412
1431
  }
1413
- safeParseOut(out) {
1414
- const result = JSON.parse(out);
1415
- return {
1416
- ...result,
1417
- eventData: BaseLazyStep.tryParsing(result.eventData)
1418
- };
1419
- }
1420
1432
  getHeaders({ context, telemetry, invokeCount, step }) {
1421
1433
  const headers = super.getHeaders({ context, telemetry, invokeCount, step });
1422
1434
  headers.headers["Upstash-Workflow-CallType"] = "step";
@@ -1450,7 +1462,7 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1450
1462
  timeoutHeaders,
1451
1463
  step: {
1452
1464
  stepId: step.stepId,
1453
- stepType: "Wait",
1465
+ stepType: this.stepType,
1454
1466
  stepName: step.stepName,
1455
1467
  concurrent: step.concurrent,
1456
1468
  targetStep: step.targetStep
@@ -1471,8 +1483,8 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1471
1483
  };
1472
1484
  var LazyNotifyStep = class extends LazyFunctionStep {
1473
1485
  stepType = "Notify";
1474
- constructor(stepName, eventId, eventData, requester) {
1475
- super(stepName, async () => {
1486
+ constructor(context, stepName, eventId, eventData, requester) {
1487
+ super(context, stepName, async () => {
1476
1488
  const notifyResponse = await makeNotifyRequest(requester, eventId, eventData);
1477
1489
  return {
1478
1490
  eventId,
@@ -1497,28 +1509,10 @@ var LazyInvokeStep = class extends BaseLazyStep {
1497
1509
  * workflow id of the invoked workflow
1498
1510
  */
1499
1511
  workflowId;
1500
- constructor(stepName, {
1501
- workflow,
1502
- body,
1503
- headers = {},
1504
- workflowRunId,
1505
- retries,
1506
- retryDelay,
1507
- flowControl,
1508
- stringifyBody = true
1509
- }) {
1510
- super(stepName);
1511
- this.params = {
1512
- workflow,
1513
- body,
1514
- headers,
1515
- workflowRunId: getWorkflowRunId(workflowRunId),
1516
- retries,
1517
- retryDelay,
1518
- flowControl,
1519
- stringifyBody
1520
- };
1521
- const { workflowId } = workflow;
1512
+ constructor(context, stepName, params) {
1513
+ super(context, stepName);
1514
+ this.params = params;
1515
+ const { workflowId } = params.workflow;
1522
1516
  if (!workflowId) {
1523
1517
  throw new WorkflowError("You can only invoke workflow which has a workflowId");
1524
1518
  }
@@ -1558,31 +1552,18 @@ var LazyInvokeStep = class extends BaseLazyStep {
1558
1552
  workflowConfig: {
1559
1553
  workflowRunId: context.workflowRunId,
1560
1554
  workflowUrl: context.url,
1561
- failureUrl: context.failureUrl,
1562
- retries: context.retries,
1563
- retryDelay: context.retryDelay,
1564
1555
  telemetry,
1565
- flowControl: context.flowControl,
1566
1556
  useJSONContent: false
1567
1557
  },
1568
1558
  userHeaders: context.headers,
1569
1559
  invokeCount
1570
1560
  });
1561
+ context.qstashClient.http.headers?.forEach((value, key) => {
1562
+ invokerHeaders[key] = value;
1563
+ });
1571
1564
  invokerHeaders["Upstash-Workflow-Runid"] = context.workflowRunId;
1572
- let invokeBody;
1573
- if (this.params.stringifyBody) {
1574
- invokeBody = JSON.stringify(this.params.body);
1575
- } else {
1576
- if (typeof this.params.body === "string") {
1577
- invokeBody = this.params.body;
1578
- } else {
1579
- throw new WorkflowError(
1580
- "When stringifyBody is false, body must be a string. Please check the body type of your invoke step."
1581
- );
1582
- }
1583
- }
1584
1565
  const request = {
1585
- body: invokeBody,
1566
+ body: stringifyBody(this.params.body),
1586
1567
  headers: Object.fromEntries(
1587
1568
  Object.entries(invokerHeaders).map((pairs) => [pairs[0], [pairs[1]]])
1588
1569
  ),
@@ -1593,34 +1574,19 @@ var LazyInvokeStep = class extends BaseLazyStep {
1593
1574
  return JSON.stringify(request);
1594
1575
  }
1595
1576
  getHeaders({ context, telemetry, invokeCount }) {
1596
- const {
1597
- workflow,
1598
- headers = {},
1599
- workflowRunId = getWorkflowRunId(),
1600
- retries,
1601
- retryDelay,
1602
- flowControl
1603
- } = this.params;
1577
+ const { workflow, headers = {}, workflowRunId, retries, retryDelay, flowControl } = this.params;
1604
1578
  const newUrl = context.url.replace(/[^/]+$/, this.workflowId);
1605
- const {
1606
- retries: workflowRetries,
1607
- retryDelay: workflowRetryDelay,
1608
- failureFunction,
1609
- failureUrl,
1610
- useJSONContent,
1611
- flowControl: workflowFlowControl
1612
- } = workflow.options;
1613
1579
  const { headers: triggerHeaders, contentType } = getHeaders({
1614
1580
  initHeaderValue: "true",
1615
1581
  workflowConfig: {
1616
- workflowRunId,
1582
+ workflowRunId: getWorkflowRunId(workflowRunId),
1617
1583
  workflowUrl: newUrl,
1618
- retries: retries ?? workflowRetries,
1619
- retryDelay: retryDelay ?? workflowRetryDelay,
1584
+ retries,
1585
+ retryDelay,
1620
1586
  telemetry,
1621
- failureUrl: failureFunction ? newUrl : failureUrl,
1622
- flowControl: flowControl ?? workflowFlowControl,
1623
- useJSONContent: useJSONContent ?? false
1587
+ failureUrl: newUrl,
1588
+ flowControl,
1589
+ useJSONContent: workflow.useJSONContent ?? false
1624
1590
  },
1625
1591
  invokeCount: invokeCount + 1,
1626
1592
  userHeaders: new Headers(headers)
@@ -1639,6 +1605,88 @@ var LazyInvokeStep = class extends BaseLazyStep {
1639
1605
  return [result];
1640
1606
  }
1641
1607
  };
1608
+ var LazyCreateWebhookStep = class extends BaseLazyStep {
1609
+ stepType = "CreateWebhook";
1610
+ allowUndefinedOut = false;
1611
+ getPlanStep(concurrent, targetStep) {
1612
+ return {
1613
+ stepId: 0,
1614
+ stepName: this.stepName,
1615
+ stepType: this.stepType,
1616
+ concurrent,
1617
+ targetStep
1618
+ };
1619
+ }
1620
+ async getResultStep(concurrent, stepId) {
1621
+ return {
1622
+ stepId,
1623
+ stepName: this.stepName,
1624
+ stepType: this.stepType,
1625
+ out: void 0,
1626
+ concurrent
1627
+ };
1628
+ }
1629
+ getBody({ step, context }) {
1630
+ const userId = getUserIdFromToken(context.qstashClient);
1631
+ const workflowRunId = context.workflowRunId;
1632
+ const eventId = getEventId();
1633
+ const qstashUrl = getQStashUrl(this.context.qstashClient);
1634
+ return JSON.stringify({
1635
+ ...step,
1636
+ out: JSON.stringify({
1637
+ webhookUrl: `${qstashUrl}/v2/workflows/hooks/${userId}/${workflowRunId}/${eventId}`,
1638
+ eventId
1639
+ })
1640
+ });
1641
+ }
1642
+ };
1643
+ var LazyWaitForWebhookStep = class extends LazyWaitEventStep {
1644
+ stepType = "WaitForWebhook";
1645
+ allowUndefinedOut = true;
1646
+ constructor(context, stepName, webhook, timeout) {
1647
+ super(context, stepName, webhook.eventId, timeout);
1648
+ }
1649
+ safeParseOut(out) {
1650
+ const eventData = decodeBase64(out);
1651
+ const parsedEventData = BaseLazyStep.tryParsing(eventData);
1652
+ const body = parsedEventData.body;
1653
+ const parsedBody = typeof body === "string" ? decodeBase64(body) : void 0;
1654
+ const request = new Request(
1655
+ `${parsedEventData.proto}://${parsedEventData.host}${parsedEventData.url}`,
1656
+ {
1657
+ method: parsedEventData.method,
1658
+ headers: parsedEventData.header,
1659
+ body: parsedBody
1660
+ }
1661
+ );
1662
+ return {
1663
+ request,
1664
+ timeout: false
1665
+ };
1666
+ }
1667
+ handleUndefinedOut() {
1668
+ return {
1669
+ timeout: true,
1670
+ request: void 0
1671
+ };
1672
+ }
1673
+ };
1674
+ var LazyWaitForEventStep = class extends LazyWaitEventStep {
1675
+ stepType = "Wait";
1676
+ allowUndefinedOut = true;
1677
+ parseWaitForEventOut(out, waitTimeout) {
1678
+ return {
1679
+ eventData: out ? BaseLazyStep.tryParsing(decodeBase64(out)) : void 0,
1680
+ timeout: waitTimeout ?? false
1681
+ };
1682
+ }
1683
+ safeParseOut(out, step) {
1684
+ return this.parseWaitForEventOut(out, step.waitTimeout);
1685
+ }
1686
+ handleUndefinedOut(step) {
1687
+ return this.parseWaitForEventOut(void 0, step.waitTimeout);
1688
+ }
1689
+ };
1642
1690
 
1643
1691
  // src/qstash/headers.ts
1644
1692
  var WorkflowHeaders = class {
@@ -1648,14 +1696,15 @@ var WorkflowHeaders = class {
1648
1696
  initHeaderValue;
1649
1697
  stepInfo;
1650
1698
  headers;
1651
- keepTriggerConfig;
1699
+ /**
1700
+ * @param params workflow header parameters
1701
+ */
1652
1702
  constructor({
1653
1703
  userHeaders,
1654
1704
  workflowConfig,
1655
1705
  invokeCount,
1656
1706
  initHeaderValue,
1657
- stepInfo,
1658
- keepTriggerConfig
1707
+ stepInfo
1659
1708
  }) {
1660
1709
  this.userHeaders = userHeaders;
1661
1710
  this.workflowConfig = workflowConfig;
@@ -1667,7 +1716,6 @@ var WorkflowHeaders = class {
1667
1716
  workflowHeaders: {},
1668
1717
  failureHeaders: {}
1669
1718
  };
1670
- this.keepTriggerConfig = keepTriggerConfig;
1671
1719
  }
1672
1720
  getHeaders() {
1673
1721
  this.addBaseHeaders();
@@ -1686,7 +1734,7 @@ var WorkflowHeaders = class {
1686
1734
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1687
1735
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1688
1736
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1689
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger" + (this.keepTriggerConfig ? ",WF_TriggerOnConfig" : ""),
1737
+ [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1690
1738
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1691
1739
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1692
1740
  };
@@ -1756,12 +1804,12 @@ var WorkflowHeaders = class {
1756
1804
  }
1757
1805
  this.headers.workflowHeaders["Failure-Callback"] = this.workflowConfig.failureUrl;
1758
1806
  this.headers.failureHeaders[`Forward-${WORKFLOW_FAILURE_HEADER}`] = "true";
1759
- this.headers.failureHeaders[`Forward-Upstash-Workflow-Failure-Callback`] = "true";
1807
+ this.headers.failureHeaders[`Forward-${WORKFLOW_FAILURE_CALLBACK_HEADER}`] = "true";
1760
1808
  this.headers.failureHeaders["Workflow-Runid"] = this.workflowConfig.workflowRunId;
1761
1809
  this.headers.failureHeaders["Workflow-Init"] = "false";
1762
1810
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1763
1811
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1764
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger";
1812
+ this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1765
1813
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1766
1814
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1767
1815
  }
@@ -1831,14 +1879,13 @@ var submitParallelSteps = async ({
1831
1879
  initialStepCount,
1832
1880
  invokeCount,
1833
1881
  telemetry,
1834
- debug
1882
+ dispatchDebug
1835
1883
  }) => {
1836
1884
  const planSteps = steps.map(
1837
1885
  (step, index) => step.getPlanStep(steps.length, initialStepCount + index)
1838
1886
  );
1839
- await debug?.log("SUBMIT", "SUBMIT_STEP", {
1840
- length: planSteps.length,
1841
- steps: planSteps
1887
+ await dispatchDebug("onInfo", {
1888
+ info: `Submitting ${planSteps.length} parallel steps.`
1842
1889
  });
1843
1890
  const result = await context.qstashClient.batch(
1844
1891
  planSteps.map((planStep) => {
@@ -1847,10 +1894,6 @@ var submitParallelSteps = async ({
1847
1894
  workflowConfig: {
1848
1895
  workflowRunId: context.workflowRunId,
1849
1896
  workflowUrl: context.url,
1850
- failureUrl: context.failureUrl,
1851
- retries: context.retries,
1852
- retryDelay: context.retryDelay,
1853
- flowControl: context.flowControl,
1854
1897
  telemetry
1855
1898
  },
1856
1899
  userHeaders: context.headers,
@@ -1866,13 +1909,11 @@ var submitParallelSteps = async ({
1866
1909
  };
1867
1910
  })
1868
1911
  );
1869
- await debug?.log("INFO", "SUBMIT_STEP", {
1870
- messageIds: result.map((message) => {
1871
- return {
1872
- message: message.messageId
1873
- };
1874
- })
1875
- });
1912
+ if (result && result.length > 0) {
1913
+ await dispatchDebug("onInfo", {
1914
+ info: `Submitted ${planSteps.length} parallel steps. messageIds: ${result.filter((r) => r).map((r) => r.messageId).join(", ")}.`
1915
+ });
1916
+ }
1876
1917
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
1877
1918
  };
1878
1919
  var submitSingleStep = async ({
@@ -1882,14 +1923,13 @@ var submitSingleStep = async ({
1882
1923
  invokeCount,
1883
1924
  concurrency,
1884
1925
  telemetry,
1885
- debug
1926
+ dispatchDebug,
1927
+ dispatchLifecycle
1886
1928
  }) => {
1887
- const resultStep = await lazyStep.getResultStep(concurrency, stepId);
1888
- await debug?.log("INFO", "RUN_SINGLE", {
1889
- fromRequest: false,
1890
- step: resultStep,
1891
- stepCount: stepId
1929
+ await dispatchLifecycle("beforeExecution", {
1930
+ stepName: lazyStep.stepName
1892
1931
  });
1932
+ const resultStep = await lazyStep.getResultStep(concurrency, stepId);
1893
1933
  const { headers } = lazyStep.getHeaders({
1894
1934
  context,
1895
1935
  step: resultStep,
@@ -1903,10 +1943,6 @@ var submitSingleStep = async ({
1903
1943
  invokeCount,
1904
1944
  telemetry
1905
1945
  });
1906
- await debug?.log("SUBMIT", "SUBMIT_STEP", {
1907
- length: 1,
1908
- steps: [resultStep]
1909
- });
1910
1946
  const submitResult = await lazyStep.submitStep({
1911
1947
  context,
1912
1948
  body,
@@ -1916,13 +1952,11 @@ var submitSingleStep = async ({
1916
1952
  step: resultStep,
1917
1953
  telemetry
1918
1954
  });
1919
- await debug?.log("INFO", "SUBMIT_STEP", {
1920
- messageIds: submitResult.map((message) => {
1921
- return {
1922
- message: message.messageId
1923
- };
1924
- })
1925
- });
1955
+ if (submitResult && submitResult[0]) {
1956
+ await dispatchDebug("onInfo", {
1957
+ info: `Submitted step "${resultStep.stepName}" with messageId: ${submitResult[0].messageId}.`
1958
+ });
1959
+ }
1926
1960
  return resultStep;
1927
1961
  };
1928
1962
 
@@ -1931,21 +1965,31 @@ var AutoExecutor = class _AutoExecutor {
1931
1965
  context;
1932
1966
  promises = /* @__PURE__ */ new WeakMap();
1933
1967
  activeLazyStepList;
1934
- debug;
1935
1968
  nonPlanStepCount;
1936
1969
  steps;
1937
1970
  indexInCurrentList = 0;
1938
1971
  invokeCount;
1939
1972
  telemetry;
1973
+ dispatchDebug;
1974
+ dispatchLifecycle;
1940
1975
  stepCount = 0;
1941
1976
  planStepCount = 0;
1942
1977
  executingStep = false;
1943
- constructor(context, steps, telemetry, invokeCount, debug) {
1978
+ /**
1979
+ * @param context workflow context
1980
+ * @param steps list of steps
1981
+ * @param dispatchDebug debug event dispatcher
1982
+ * @param dispatchLifecycle lifecycle event dispatcher
1983
+ * @param telemetry optional telemetry information
1984
+ * @param invokeCount optional invoke count
1985
+ */
1986
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
1944
1987
  this.context = context;
1945
1988
  this.steps = steps;
1989
+ this.dispatchDebug = dispatchDebug;
1990
+ this.dispatchLifecycle = dispatchLifecycle;
1946
1991
  this.telemetry = telemetry;
1947
1992
  this.invokeCount = invokeCount ?? 0;
1948
- this.debug = debug;
1949
1993
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
1950
1994
  }
1951
1995
  /**
@@ -2013,7 +2057,7 @@ var AutoExecutor = class _AutoExecutor {
2013
2057
  /**
2014
2058
  * Executes a step:
2015
2059
  * - If the step result is available in the steps, returns the result
2016
- * - If the result is not avaiable, runs the function
2060
+ * - If the result is not available, runs the function
2017
2061
  * - Sends the result to QStash
2018
2062
  *
2019
2063
  * @param lazyStep lazy step to execute
@@ -2023,12 +2067,15 @@ var AutoExecutor = class _AutoExecutor {
2023
2067
  if (this.stepCount < this.nonPlanStepCount) {
2024
2068
  const step = this.steps[this.stepCount + this.planStepCount];
2025
2069
  validateStep(lazyStep, step);
2026
- await this.debug?.log("INFO", "RUN_SINGLE", {
2027
- fromRequest: true,
2028
- step,
2029
- stepCount: this.stepCount
2030
- });
2031
- return lazyStep.parseOut(step.out);
2070
+ const parsedOut = lazyStep.parseOut(step);
2071
+ const isLastMemoized = this.stepCount + 1 === this.nonPlanStepCount && this.steps.at(-1).stepId !== 0;
2072
+ if (isLastMemoized) {
2073
+ await this.dispatchLifecycle("afterExecution", {
2074
+ stepName: lazyStep.stepName,
2075
+ result: parsedOut
2076
+ });
2077
+ }
2078
+ return parsedOut;
2032
2079
  }
2033
2080
  const resultStep = await submitSingleStep({
2034
2081
  context: this.context,
@@ -2037,15 +2084,15 @@ var AutoExecutor = class _AutoExecutor {
2037
2084
  invokeCount: this.invokeCount,
2038
2085
  concurrency: 1,
2039
2086
  telemetry: this.telemetry,
2040
- debug: this.debug
2087
+ dispatchDebug: this.dispatchDebug,
2088
+ dispatchLifecycle: this.dispatchLifecycle
2041
2089
  });
2042
2090
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2043
2091
  }
2044
2092
  /**
2045
2093
  * Runs steps in parallel.
2046
2094
  *
2047
- * @param stepName parallel step name
2048
- * @param stepFunctions list of async functions to run in parallel
2095
+ * @param parallelSteps list of lazy steps to run in parallel
2049
2096
  * @returns results of the functions run in parallel
2050
2097
  */
2051
2098
  async runParallel(parallelSteps) {
@@ -2058,12 +2105,14 @@ var AutoExecutor = class _AutoExecutor {
2058
2105
  `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
2059
2106
  );
2060
2107
  }
2061
- await this.debug?.log("INFO", "RUN_PARALLEL", {
2062
- parallelCallState,
2063
- initialStepCount,
2064
- plannedParallelStepCount,
2065
- stepCount: this.stepCount,
2066
- planStepCount: this.planStepCount
2108
+ await this.dispatchDebug("onInfo", {
2109
+ info: `Executing parallel steps with: ` + JSON.stringify({
2110
+ parallelCallState,
2111
+ initialStepCount,
2112
+ plannedParallelStepCount,
2113
+ stepCount: this.stepCount,
2114
+ planStepCount: this.planStepCount
2115
+ })
2067
2116
  });
2068
2117
  switch (parallelCallState) {
2069
2118
  case "first": {
@@ -2073,7 +2122,7 @@ var AutoExecutor = class _AutoExecutor {
2073
2122
  initialStepCount,
2074
2123
  invokeCount: this.invokeCount,
2075
2124
  telemetry: this.telemetry,
2076
- debug: this.debug
2125
+ dispatchDebug: this.dispatchDebug
2077
2126
  });
2078
2127
  break;
2079
2128
  }
@@ -2095,7 +2144,8 @@ var AutoExecutor = class _AutoExecutor {
2095
2144
  invokeCount: this.invokeCount,
2096
2145
  concurrency: parallelSteps.length,
2097
2146
  telemetry: this.telemetry,
2098
- debug: this.debug
2147
+ dispatchDebug: this.dispatchDebug,
2148
+ dispatchLifecycle: this.dispatchLifecycle
2099
2149
  });
2100
2150
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2101
2151
  } catch (error) {
@@ -2109,13 +2159,34 @@ var AutoExecutor = class _AutoExecutor {
2109
2159
  break;
2110
2160
  }
2111
2161
  case "discard": {
2162
+ const resultStep = this.steps.at(-1);
2163
+ const lazyStep = parallelSteps.find(
2164
+ (planStep, index) => resultStep.stepId - index === initialStepCount
2165
+ );
2166
+ if (lazyStep) {
2167
+ await this.dispatchLifecycle("afterExecution", {
2168
+ stepName: lazyStep.stepName,
2169
+ result: lazyStep.parseOut(resultStep)
2170
+ });
2171
+ }
2112
2172
  throw new WorkflowAbort("discarded parallel");
2113
2173
  }
2114
2174
  case "last": {
2115
2175
  const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
2116
2176
  validateParallelSteps(parallelSteps, parallelResultSteps);
2177
+ const isLastMemoized = this.stepCount + 1 === this.nonPlanStepCount && this.steps.at(-1).stepId !== 0;
2178
+ if (isLastMemoized) {
2179
+ const resultStep = this.steps.at(-1);
2180
+ const lazyStep = parallelSteps.find(
2181
+ (planStep, index) => resultStep.stepId - index === initialStepCount
2182
+ );
2183
+ await this.dispatchLifecycle("afterExecution", {
2184
+ stepName: lazyStep.stepName,
2185
+ result: lazyStep.parseOut(resultStep)
2186
+ });
2187
+ }
2117
2188
  return parallelResultSteps.map(
2118
- (step, index) => parallelSteps[index].parseOut(step.out)
2189
+ (step, index) => parallelSteps[index].parseOut(step)
2119
2190
  );
2120
2191
  }
2121
2192
  }
@@ -2171,7 +2242,6 @@ var AutoExecutor = class _AutoExecutor {
2171
2242
  * @param index index of the current step
2172
2243
  * @returns result[index] if lazyStepList > 1, otherwise result
2173
2244
  */
2174
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
2175
2245
  static getResult(lazyStepList, result, index) {
2176
2246
  if (lazyStepList.length === 1) {
2177
2247
  return result;
@@ -2258,15 +2328,18 @@ var getProviderInfo = (api) => {
2258
2328
  // src/context/api/base.ts
2259
2329
  var BaseWorkflowApi = class {
2260
2330
  context;
2331
+ /**
2332
+ * @param context workflow context
2333
+ */
2261
2334
  constructor({ context }) {
2262
2335
  this.context = context;
2263
2336
  }
2264
2337
  /**
2265
2338
  * context.call which uses a QStash API
2266
2339
  *
2267
- * @param stepName
2268
- * @param settings
2269
- * @returns
2340
+ * @param stepName name of the step
2341
+ * @param settings call settings including api configuration
2342
+ * @returns call response
2270
2343
  */
2271
2344
  async callApi(stepName, settings) {
2272
2345
  const { url, appendHeaders, method } = getProviderInfo(settings.api);
@@ -2274,7 +2347,7 @@ var BaseWorkflowApi = class {
2274
2347
  return await this.context.call(stepName, {
2275
2348
  url,
2276
2349
  method: userMethod ?? method,
2277
- body,
2350
+ body: typeof body === "string" ? body : JSON.stringify(body),
2278
2351
  headers: {
2279
2352
  ...appendHeaders,
2280
2353
  ...headers
@@ -2351,6 +2424,129 @@ var WorkflowApi = class extends BaseWorkflowApi {
2351
2424
  }
2352
2425
  };
2353
2426
 
2427
+ // src/middleware/default-callbacks.ts
2428
+ var onErrorWithConsole = async ({ workflowRunId, error }) => {
2429
+ console.error(` [Upstash Workflow]: Error in workflow run ${workflowRunId}: ` + error);
2430
+ };
2431
+ var onWarningWithConsole = async ({ workflowRunId, warning }) => {
2432
+ console.warn(` [Upstash Workflow]: Warning in workflow run ${workflowRunId}: ` + warning);
2433
+ };
2434
+ var onInfoWithConsole = async ({
2435
+ workflowRunId,
2436
+ info
2437
+ }) => {
2438
+ console.info(` [Upstash Workflow]: Info in workflow run ${workflowRunId}: ` + info);
2439
+ };
2440
+
2441
+ // src/middleware/manager.ts
2442
+ var MiddlewareManager = class {
2443
+ middlewares;
2444
+ workflowRunId;
2445
+ context;
2446
+ /**
2447
+ * @param middlewares list of workflow middlewares
2448
+ */
2449
+ constructor(middlewares = []) {
2450
+ this.middlewares = middlewares;
2451
+ }
2452
+ /**
2453
+ * Assign workflow run ID - will be passed to debug events
2454
+ *
2455
+ * @param workflowRunId workflow run id to assign
2456
+ */
2457
+ assignWorkflowRunId(workflowRunId) {
2458
+ this.workflowRunId = workflowRunId;
2459
+ }
2460
+ /**
2461
+ * Assign context - required for lifecycle events
2462
+ *
2463
+ * also assigns workflowRunId from context
2464
+ *
2465
+ * @param context workflow context to assign
2466
+ */
2467
+ assignContext(context) {
2468
+ this.context = context;
2469
+ this.workflowRunId = context.workflowRunId;
2470
+ }
2471
+ /**
2472
+ * Internal method to execute middlewares with common error handling logic
2473
+ *
2474
+ * @param event event name to dispatch
2475
+ * @param params event parameters
2476
+ */
2477
+ async executeMiddlewares(event, params) {
2478
+ await Promise.all(this.middlewares.map((m) => m.ensureInit()));
2479
+ await Promise.all(
2480
+ this.middlewares.map(async (middleware) => {
2481
+ const callback = middleware.getCallback(event);
2482
+ if (callback) {
2483
+ try {
2484
+ await callback(params);
2485
+ } catch (error) {
2486
+ try {
2487
+ const onErrorCallback = middleware.getCallback("onError") ?? onErrorWithConsole;
2488
+ await onErrorCallback({
2489
+ workflowRunId: this.workflowRunId,
2490
+ error
2491
+ });
2492
+ } catch (onErrorError) {
2493
+ console.error(
2494
+ `Failed while executing "onError" of middleware "${middleware.name}", falling back to logging the error to console. Error: ${onErrorError}`
2495
+ );
2496
+ onErrorWithConsole({
2497
+ workflowRunId: this.workflowRunId,
2498
+ error
2499
+ });
2500
+ }
2501
+ }
2502
+ }
2503
+ })
2504
+ );
2505
+ if (event === "onError") {
2506
+ onErrorWithConsole({
2507
+ workflowRunId: this.workflowRunId,
2508
+ ...params
2509
+ });
2510
+ } else if (event === "onWarning") {
2511
+ onWarningWithConsole({
2512
+ workflowRunId: this.workflowRunId,
2513
+ ...params
2514
+ });
2515
+ }
2516
+ }
2517
+ /**
2518
+ * Dispatch a debug event (onError, onWarning, onInfo)
2519
+ *
2520
+ * @param event debug event name
2521
+ * @param params event parameters
2522
+ */
2523
+ async dispatchDebug(event, params) {
2524
+ const paramsWithRunId = {
2525
+ ...params,
2526
+ workflowRunId: this.workflowRunId
2527
+ };
2528
+ await this.executeMiddlewares(event, paramsWithRunId);
2529
+ }
2530
+ /**
2531
+ * Dispatch a lifecycle event (beforeExecution, afterExecution, runStarted, runCompleted)
2532
+ *
2533
+ * @param event lifecycle event name
2534
+ * @param params event parameters
2535
+ */
2536
+ async dispatchLifecycle(event, params) {
2537
+ if (!this.context) {
2538
+ throw new WorkflowError(
2539
+ `Something went wrong while calling middlewares. Lifecycle event "${event}" was called before assignContext.`
2540
+ );
2541
+ }
2542
+ const paramsWithContext = {
2543
+ ...params,
2544
+ context: this.context
2545
+ };
2546
+ await this.executeMiddlewares(event, paramsWithContext);
2547
+ }
2548
+ };
2549
+
2354
2550
  // src/context/context.ts
2355
2551
  var WorkflowContext = class {
2356
2552
  executor;
@@ -2396,32 +2592,13 @@ var WorkflowContext = class {
2396
2592
  */
2397
2593
  url;
2398
2594
  /**
2399
- * URL to call in case of workflow failure with QStash failure callback
2400
- *
2401
- * https://upstash.com/docs/qstash/features/callbacks#what-is-a-failure-callback
2595
+ * Payload of the request which started the workflow.
2402
2596
  *
2403
- * Can be overwritten by passing a `failureUrl` parameter in `serve`:
2597
+ * To specify its type, you can define `serve` as follows:
2404
2598
  *
2405
2599
  * ```ts
2406
- * export const POST = serve(
2407
- * async (context) => {
2408
- * ...
2409
- * },
2410
- * {
2411
- * failureUrl: "new-url-value"
2412
- * }
2413
- * )
2414
- * ```
2415
- */
2416
- failureUrl;
2417
- /**
2418
- * Payload of the request which started the workflow.
2419
- *
2420
- * To specify its type, you can define `serve` as follows:
2421
- *
2422
- * ```ts
2423
- * // set requestPayload type to MyPayload:
2424
- * export const POST = serve<MyPayload>(
2600
+ * // set requestPayload type to MyPayload:
2601
+ * export const POST = serve<MyPayload>(
2425
2602
  * async (context) => {
2426
2603
  * ...
2427
2604
  * }
@@ -2469,46 +2646,6 @@ var WorkflowContext = class {
2469
2646
  * Default value is set to `process.env`.
2470
2647
  */
2471
2648
  env;
2472
- /**
2473
- * Number of retries
2474
- */
2475
- retries;
2476
- /**
2477
- * Delay between retries.
2478
- *
2479
- * By default, the `retryDelay` is exponential backoff.
2480
- * More details can be found in: https://upstash.com/docs/qstash/features/retry.
2481
- *
2482
- * The `retryDelay` option allows you to customize the delay (in milliseconds) between retry attempts when message delivery fails.
2483
- *
2484
- * You can use mathematical expressions and the following built-in functions to calculate the delay dynamically.
2485
- * The special variable `retried` represents the current retry attempt count (starting from 0).
2486
- *
2487
- * Supported functions:
2488
- * - `pow`
2489
- * - `sqrt`
2490
- * - `abs`
2491
- * - `exp`
2492
- * - `floor`
2493
- * - `ceil`
2494
- * - `round`
2495
- * - `min`
2496
- * - `max`
2497
- *
2498
- * Examples of valid `retryDelay` values:
2499
- * ```ts
2500
- * 1000 // 1 second
2501
- * 1000 * (1 + retried) // 1 second multiplied by the current retry attempt
2502
- * pow(2, retried) // 2 to the power of the current retry attempt
2503
- * max(10, pow(2, retried)) // The greater of 10 or 2^retried
2504
- * ```
2505
- */
2506
- retryDelay;
2507
- /**
2508
- * Settings for controlling the number of active requests
2509
- * and number of requests per second with the same key.
2510
- */
2511
- flowControl;
2512
2649
  /**
2513
2650
  * Label to apply to the workflow run.
2514
2651
  *
@@ -2531,30 +2668,31 @@ var WorkflowContext = class {
2531
2668
  headers,
2532
2669
  steps,
2533
2670
  url,
2534
- failureUrl,
2535
- debug,
2536
2671
  initialPayload,
2537
2672
  env,
2538
- retries,
2539
- retryDelay,
2540
2673
  telemetry,
2541
2674
  invokeCount,
2542
- flowControl,
2543
- label
2675
+ label,
2676
+ middlewareManager
2544
2677
  }) {
2545
2678
  this.qstashClient = qstashClient;
2546
2679
  this.workflowRunId = workflowRunId;
2547
2680
  this.steps = steps;
2548
2681
  this.url = url;
2549
- this.failureUrl = failureUrl;
2550
2682
  this.headers = headers;
2551
2683
  this.requestPayload = initialPayload;
2552
2684
  this.env = env ?? {};
2553
- this.retries = retries ?? DEFAULT_RETRIES;
2554
- this.retryDelay = retryDelay;
2555
- this.flowControl = flowControl;
2556
2685
  this.label = label;
2557
- this.executor = new AutoExecutor(this, this.steps, telemetry, invokeCount, debug);
2686
+ const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2687
+ middlewareManagerInstance.assignContext(this);
2688
+ this.executor = new AutoExecutor(
2689
+ this,
2690
+ this.steps,
2691
+ middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2692
+ middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2693
+ telemetry,
2694
+ invokeCount
2695
+ );
2558
2696
  }
2559
2697
  /**
2560
2698
  * Executes a workflow step
@@ -2585,7 +2723,7 @@ var WorkflowContext = class {
2585
2723
  */
2586
2724
  async run(stepName, stepFunction) {
2587
2725
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2588
- return await this.addStep(new LazyFunctionStep(stepName, wrappedStepFunction));
2726
+ return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
2589
2727
  }
2590
2728
  /**
2591
2729
  * Stops the execution for the duration provided.
@@ -2599,7 +2737,7 @@ var WorkflowContext = class {
2599
2737
  * @returns undefined
2600
2738
  */
2601
2739
  async sleep(stepName, duration) {
2602
- await this.addStep(new LazySleepStep(stepName, duration));
2740
+ await this.addStep(new LazySleepStep(this, stepName, duration));
2603
2741
  }
2604
2742
  /**
2605
2743
  * Stops the execution until the date time provided.
@@ -2621,48 +2759,38 @@ var WorkflowContext = class {
2621
2759
  datetime = typeof datetime === "string" ? new Date(datetime) : datetime;
2622
2760
  time = Math.round(datetime.getTime() / 1e3);
2623
2761
  }
2624
- await this.addStep(new LazySleepUntilStep(stepName, time));
2762
+ await this.addStep(new LazySleepUntilStep(this, stepName, time));
2625
2763
  }
2626
2764
  async call(stepName, settings) {
2627
2765
  let callStep;
2628
2766
  if ("workflow" in settings) {
2629
2767
  const url = getNewUrlFromWorkflowId(this.url, settings.workflow.workflowId);
2630
- callStep = new LazyCallStep(
2768
+ const stringBody = typeof settings.body === "string" ? settings.body : settings.body === void 0 ? void 0 : JSON.stringify(settings.body);
2769
+ callStep = new LazyCallStep({
2770
+ context: this,
2631
2771
  stepName,
2632
2772
  url,
2633
- "POST",
2634
- settings.body,
2635
- settings.headers || {},
2636
- settings.retries || 0,
2637
- settings.retryDelay,
2638
- settings.timeout,
2639
- settings.flowControl ?? settings.workflow.options.flowControl,
2640
- settings.stringifyBody ?? true
2641
- );
2773
+ method: "POST",
2774
+ body: stringBody,
2775
+ headers: settings.headers || {},
2776
+ retries: settings.retries || 0,
2777
+ retryDelay: settings.retryDelay,
2778
+ timeout: settings.timeout,
2779
+ flowControl: settings.flowControl
2780
+ });
2642
2781
  } else {
2643
- const {
2644
- url,
2645
- method = "GET",
2646
- body,
2647
- headers = {},
2648
- retries = 0,
2649
- retryDelay,
2650
- timeout,
2651
- flowControl,
2652
- stringifyBody = true
2653
- } = settings;
2654
- callStep = new LazyCallStep(
2782
+ callStep = new LazyCallStep({
2783
+ context: this,
2655
2784
  stepName,
2656
- url,
2657
- method,
2658
- body,
2659
- headers,
2660
- retries,
2661
- retryDelay,
2662
- timeout,
2663
- flowControl,
2664
- stringifyBody
2665
- );
2785
+ url: settings.url,
2786
+ method: settings.method ?? "GET",
2787
+ body: settings.body,
2788
+ headers: settings.headers ?? {},
2789
+ retries: settings.retries ?? 0,
2790
+ retryDelay: settings.retryDelay,
2791
+ timeout: settings.timeout,
2792
+ flowControl: settings.flowControl
2793
+ });
2666
2794
  }
2667
2795
  return await this.addStep(callStep);
2668
2796
  }
@@ -2703,7 +2831,9 @@ var WorkflowContext = class {
2703
2831
  async waitForEvent(stepName, eventId, options = {}) {
2704
2832
  const { timeout = "7d" } = options;
2705
2833
  const timeoutStr = typeof timeout === "string" ? timeout : `${timeout}s`;
2706
- return await this.addStep(new LazyWaitForEventStep(stepName, eventId, timeoutStr));
2834
+ return await this.addStep(
2835
+ new LazyWaitForEventStep(this, stepName, eventId, timeoutStr)
2836
+ );
2707
2837
  }
2708
2838
  /**
2709
2839
  * Notify workflow runs waiting for an event
@@ -2728,20 +2858,28 @@ var WorkflowContext = class {
2728
2858
  */
2729
2859
  async notify(stepName, eventId, eventData) {
2730
2860
  return await this.addStep(
2731
- new LazyNotifyStep(stepName, eventId, eventData, this.qstashClient.http)
2861
+ new LazyNotifyStep(this, stepName, eventId, eventData, this.qstashClient.http)
2732
2862
  );
2733
2863
  }
2734
2864
  async invoke(stepName, settings) {
2735
- return await this.addStep(new LazyInvokeStep(stepName, settings));
2865
+ return await this.addStep(
2866
+ new LazyInvokeStep(this, stepName, settings)
2867
+ );
2868
+ }
2869
+ async createWebhook(stepName) {
2870
+ return await this.addStep(new LazyCreateWebhookStep(this, stepName));
2871
+ }
2872
+ async waitForWebhook(stepName, webhook, timeout) {
2873
+ return await this.addStep(new LazyWaitForWebhookStep(this, stepName, webhook, timeout));
2736
2874
  }
2737
2875
  /**
2738
2876
  * Cancel the current workflow run
2739
2877
  *
2740
- * Will throw WorkflowAbort to stop workflow execution.
2878
+ * Will throw WorkflowCancelAbort to stop workflow execution.
2741
2879
  * Shouldn't be inside try/catch.
2742
2880
  */
2743
2881
  async cancel() {
2744
- throw new WorkflowAbort("cancel", void 0, true);
2882
+ throw new WorkflowCancelAbort();
2745
2883
  }
2746
2884
  /**
2747
2885
  * Adds steps to the executor. Needed so that it can be overwritten in
@@ -2757,74 +2895,99 @@ var WorkflowContext = class {
2757
2895
  }
2758
2896
  };
2759
2897
 
2760
- // src/logger.ts
2761
- var LOG_LEVELS = ["DEBUG", "INFO", "SUBMIT", "WARN", "ERROR"];
2762
- var WorkflowLogger = class _WorkflowLogger {
2763
- logs = [];
2764
- options;
2765
- workflowRunId = void 0;
2766
- constructor(options) {
2767
- this.options = options;
2768
- }
2769
- async log(level, eventType, details) {
2770
- if (this.shouldLog(level)) {
2771
- const timestamp = Date.now();
2772
- const logEntry = {
2773
- timestamp,
2774
- workflowRunId: this.workflowRunId ?? "",
2775
- logLevel: level,
2776
- eventType,
2777
- details
2778
- };
2779
- this.logs.push(logEntry);
2780
- if (this.options.logOutput === "console") {
2781
- this.writeToConsole(logEntry);
2782
- }
2783
- await new Promise((resolve) => setTimeout(resolve, 100));
2898
+ // src/middleware/middleware.ts
2899
+ var WorkflowMiddleware = class {
2900
+ name;
2901
+ initCallbacks;
2902
+ /**
2903
+ * Callback functions
2904
+ *
2905
+ * Initially set to undefined, will be populated after init is called
2906
+ */
2907
+ middlewareCallbacks = void 0;
2908
+ constructor(parameters) {
2909
+ this.name = parameters.name;
2910
+ if ("init" in parameters) {
2911
+ this.initCallbacks = parameters.init;
2912
+ } else {
2913
+ this.middlewareCallbacks = parameters.callbacks;
2784
2914
  }
2785
2915
  }
2786
- setWorkflowRunId(workflowRunId) {
2787
- this.workflowRunId = workflowRunId;
2788
- }
2789
- writeToConsole(logEntry) {
2790
- const JSON_SPACING = 2;
2791
- const logMethod = logEntry.logLevel === "ERROR" ? console.error : logEntry.logLevel === "WARN" ? console.warn : console.log;
2792
- logMethod(JSON.stringify(logEntry, void 0, JSON_SPACING));
2793
- }
2794
- shouldLog(level) {
2795
- return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
2796
- }
2797
- static getLogger(verbose) {
2798
- if (typeof verbose === "object") {
2799
- return verbose;
2800
- } else {
2801
- return verbose ? new _WorkflowLogger({
2802
- logLevel: "INFO",
2803
- logOutput: "console"
2804
- }) : void 0;
2916
+ async ensureInit() {
2917
+ if (!this.middlewareCallbacks) {
2918
+ if (!this.initCallbacks) {
2919
+ throw new WorkflowError(`Middleware "${this.name}" has no callbacks or init defined.`);
2920
+ }
2921
+ this.middlewareCallbacks = await this.initCallbacks();
2805
2922
  }
2806
2923
  }
2924
+ /**
2925
+ * Gets a callback function by name.
2926
+ *
2927
+ * @param callback name of the callback to retrieve
2928
+ */
2929
+ getCallback(callback) {
2930
+ return this.middlewareCallbacks?.[callback];
2931
+ }
2807
2932
  };
2808
2933
 
2934
+ // src/middleware/logging.ts
2935
+ var loggingMiddleware = new WorkflowMiddleware({
2936
+ name: "logging",
2937
+ callbacks: {
2938
+ afterExecution(params) {
2939
+ const { context, ...rest } = params;
2940
+ console.log(" [Upstash Workflow]: Step executed:", {
2941
+ workflowRunId: context.workflowRunId,
2942
+ ...rest
2943
+ });
2944
+ },
2945
+ beforeExecution(params) {
2946
+ const { context, ...rest } = params;
2947
+ console.log(" [Upstash Workflow]: Step execution started:", {
2948
+ workflowRunId: context.workflowRunId,
2949
+ ...rest
2950
+ });
2951
+ },
2952
+ runStarted(params) {
2953
+ const { context, ...rest } = params;
2954
+ console.log(" [Upstash Workflow]: Workflow run started:", {
2955
+ workflowRunId: context.workflowRunId,
2956
+ ...rest
2957
+ });
2958
+ },
2959
+ runCompleted(params) {
2960
+ const { context, ...rest } = params;
2961
+ console.log(" [Upstash Workflow]: Workflow run completed:", {
2962
+ workflowRunId: context.workflowRunId,
2963
+ ...rest
2964
+ });
2965
+ },
2966
+ onError: onErrorWithConsole,
2967
+ onWarning: onWarningWithConsole,
2968
+ onInfo: onInfoWithConsole
2969
+ }
2970
+ });
2971
+
2809
2972
  // src/serve/authorization.ts
2810
2973
  import { Client as Client2 } from "@upstash/qstash";
2811
2974
  var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
2812
2975
  static disabledMessage = "disabled-qstash-worklfow-run";
2813
2976
  disabled = true;
2814
2977
  /**
2815
- * overwrite the WorkflowContext.addStep method to always raise WorkflowAbort
2978
+ * overwrite the WorkflowContext.addStep method to always raise WorkflowAuthError
2816
2979
  * error in order to stop the execution whenever we encounter a step.
2817
2980
  *
2818
2981
  * @param _step
2819
2982
  */
2820
2983
  async addStep(_step) {
2821
- throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
2984
+ throw new WorkflowAuthError(_DisabledWorkflowContext.disabledMessage);
2822
2985
  }
2823
2986
  /**
2824
- * overwrite cancel method to throw WorkflowAbort with the disabledMessage
2987
+ * overwrite cancel method to throw WorkflowAuthError with the disabledMessage
2825
2988
  */
2826
2989
  async cancel() {
2827
- throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
2990
+ throw new WorkflowAuthError(_DisabledWorkflowContext.disabledMessage);
2828
2991
  }
2829
2992
  /**
2830
2993
  * copies the passed context to create a DisabledWorkflowContext. Then, runs the
@@ -2847,18 +3010,14 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
2847
3010
  headers: context.headers,
2848
3011
  steps: [],
2849
3012
  url: context.url,
2850
- failureUrl: context.failureUrl,
2851
3013
  initialPayload: context.requestPayload,
2852
3014
  env: context.env,
2853
- retries: context.retries,
2854
- retryDelay: context.retryDelay,
2855
- flowControl: context.flowControl,
2856
3015
  label: context.label
2857
3016
  });
2858
3017
  try {
2859
3018
  await routeFunction(disabledContext);
2860
3019
  } catch (error) {
2861
- if (isInstanceOf(error, WorkflowAbort) && error.stepName === this.disabledMessage || isInstanceOf(error, WorkflowNonRetryableError) || isInstanceOf(error, WorkflowRetryAfterError)) {
3020
+ if (isInstanceOf(error, WorkflowAuthError) && error.stepName === this.disabledMessage || isInstanceOf(error, WorkflowNonRetryableError) || isInstanceOf(error, WorkflowRetryAfterError)) {
2862
3021
  return ok("step-found");
2863
3022
  }
2864
3023
  console.warn(
@@ -2891,13 +3050,6 @@ var processRawSteps = (rawSteps) => {
2891
3050
  const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
2892
3051
  const otherSteps = stepsToDecode.map((rawStep) => {
2893
3052
  const step = JSON.parse(decodeBase64(rawStep.body));
2894
- if (step.waitEventId) {
2895
- const newOut = {
2896
- eventData: step.out ? decodeBase64(step.out) : void 0,
2897
- timeout: step.waitTimeout ?? false
2898
- };
2899
- step.out = newOut;
2900
- }
2901
3053
  return step;
2902
3054
  });
2903
3055
  const steps = [initialStep, ...otherSteps];
@@ -2925,7 +3077,7 @@ var deduplicateSteps = (steps) => {
2925
3077
  }
2926
3078
  return deduplicatedSteps;
2927
3079
  };
2928
- var checkIfLastOneIsDuplicate = async (steps, debug) => {
3080
+ var checkIfLastOneIsDuplicate = async (steps, dispatchDebug) => {
2929
3081
  if (steps.length < 2) {
2930
3082
  return false;
2931
3083
  }
@@ -2936,14 +3088,41 @@ var checkIfLastOneIsDuplicate = async (steps, debug) => {
2936
3088
  const step = steps[index];
2937
3089
  if (step.stepId === lastStepId && step.targetStep === lastTargetStepId) {
2938
3090
  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.`;
2939
- await debug?.log("WARN", "RESPONSE_DEFAULT", message);
2940
- console.warn(message);
3091
+ await dispatchDebug?.("onWarning", {
3092
+ warning: message
3093
+ });
2941
3094
  return true;
2942
3095
  }
2943
3096
  }
2944
3097
  return false;
2945
3098
  };
2946
3099
  var validateRequest = (request) => {
3100
+ if (request.headers.get(WORKFLOW_UNKOWN_SDK_VERSION_HEADER)) {
3101
+ const workflowRunId2 = request.headers.get(WORKFLOW_ID_HEADER);
3102
+ if (!workflowRunId2) {
3103
+ throw new WorkflowError(
3104
+ "Couldn't get workflow id from header when handling unknown sdk request"
3105
+ );
3106
+ }
3107
+ return {
3108
+ unknownSdk: true,
3109
+ isFirstInvocation: true,
3110
+ workflowRunId: workflowRunId2
3111
+ };
3112
+ }
3113
+ if (request.headers.get(WORKFLOW_FAILURE_CALLBACK_HEADER)) {
3114
+ const workflowRunId2 = request.headers.get(WORKFLOW_ID_HEADER);
3115
+ if (!workflowRunId2) {
3116
+ throw new WorkflowError(
3117
+ "Couldn't get workflow id from header when handling failure callback request"
3118
+ );
3119
+ }
3120
+ return {
3121
+ unknownSdk: false,
3122
+ isFirstInvocation: true,
3123
+ workflowRunId: workflowRunId2
3124
+ };
3125
+ }
2947
3126
  const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
2948
3127
  const isFirstInvocation = !versionHeader;
2949
3128
  if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
@@ -2957,11 +3136,20 @@ var validateRequest = (request) => {
2957
3136
  }
2958
3137
  return {
2959
3138
  isFirstInvocation,
2960
- workflowRunId
3139
+ workflowRunId,
3140
+ unknownSdk: false
2961
3141
  };
2962
3142
  };
2963
- var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requester, messageId, debug) => {
2964
- if (isFirstInvocation) {
3143
+ var parseRequest = async ({
3144
+ requestPayload,
3145
+ isFirstInvocation,
3146
+ unknownSdk,
3147
+ workflowRunId,
3148
+ requester,
3149
+ messageId,
3150
+ dispatchDebug
3151
+ }) => {
3152
+ if (isFirstInvocation && !unknownSdk) {
2965
3153
  return {
2966
3154
  rawInitialPayload: requestPayload ?? "",
2967
3155
  steps: [],
@@ -2971,16 +3159,14 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
2971
3159
  } else {
2972
3160
  let rawSteps;
2973
3161
  if (!requestPayload) {
2974
- await debug?.log(
2975
- "INFO",
2976
- "ENDPOINT_START",
2977
- "request payload is empty, steps will be fetched from QStash."
2978
- );
3162
+ await dispatchDebug?.("onInfo", {
3163
+ info: "request payload is empty, steps will be fetched from QStash."
3164
+ });
2979
3165
  const { steps: fetchedSteps, workflowRunEnded } = await getSteps(
2980
3166
  requester,
2981
3167
  workflowRunId,
2982
3168
  messageId,
2983
- debug
3169
+ dispatchDebug
2984
3170
  );
2985
3171
  if (workflowRunEnded) {
2986
3172
  return {
@@ -2995,7 +3181,7 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
2995
3181
  rawSteps = JSON.parse(requestPayload);
2996
3182
  }
2997
3183
  const { rawInitialPayload, steps } = processRawSteps(rawSteps);
2998
- const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
3184
+ const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, dispatchDebug);
2999
3185
  const deduplicatedSteps = deduplicateSteps(steps);
3000
3186
  return {
3001
3187
  rawInitialPayload,
@@ -3005,16 +3191,21 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
3005
3191
  };
3006
3192
  }
3007
3193
  };
3008
- var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, routeFunction, failureFunction, env, retries, retryDelay, flowControl, debug) => {
3009
- if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
3194
+ var handleFailure = async ({
3195
+ request,
3196
+ requestPayload,
3197
+ qstashClient,
3198
+ initialPayloadParser,
3199
+ routeFunction,
3200
+ failureFunction,
3201
+ env,
3202
+ dispatchDebug
3203
+ }) => {
3204
+ if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true" && !request.headers.get(WORKFLOW_FAILURE_CALLBACK_HEADER)) {
3010
3205
  return ok({ result: "not-failure-callback" });
3011
3206
  }
3012
3207
  if (!failureFunction) {
3013
- return err(
3014
- new WorkflowError(
3015
- "Workflow endpoint is called to handle a failure, but a failureFunction is not provided in serve options. Either provide a failureUrl or a failureFunction."
3016
- )
3017
- );
3208
+ return ok({ result: "failure-function-undefined" });
3018
3209
  }
3019
3210
  try {
3020
3211
  const { status, header, body, url, sourceBody, workflowRunId } = JSON.parse(requestPayload);
@@ -3042,23 +3233,21 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
3042
3233
  headers: userHeaders,
3043
3234
  steps: [],
3044
3235
  url,
3045
- failureUrl: url,
3046
- debug,
3047
3236
  env,
3048
- retries,
3049
- retryDelay,
3050
- flowControl,
3051
3237
  telemetry: void 0,
3052
3238
  // not going to make requests in authentication check
3053
- label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0
3239
+ label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3240
+ middlewareManager: void 0
3054
3241
  });
3055
3242
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3056
3243
  routeFunction,
3057
3244
  workflowContext
3058
3245
  );
3059
3246
  if (authCheck.isErr()) {
3060
- await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
3061
- throw authCheck.error;
3247
+ await dispatchDebug?.("onError", {
3248
+ error: authCheck.error
3249
+ });
3250
+ return err(authCheck.error);
3062
3251
  } else if (authCheck.value === "run-ended") {
3063
3252
  return err(new WorkflowError("Not authorized to run the failure function."));
3064
3253
  }
@@ -3069,74 +3258,235 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
3069
3258
  failHeaders: header,
3070
3259
  failStack
3071
3260
  });
3072
- return ok({ result: "is-failure-callback", response: failureResponse });
3261
+ return ok({ result: "failure-function-executed", response: failureResponse });
3073
3262
  } catch (error) {
3074
3263
  return err(error);
3075
3264
  }
3076
3265
  };
3077
3266
 
3078
- // src/serve/options.ts
3079
- import { Receiver } from "@upstash/qstash";
3080
- import { Client as Client3 } from "@upstash/qstash";
3081
- var processOptions = (options) => {
3082
- const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
3083
- const receiverEnvironmentVariablesSet = Boolean(
3084
- environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
3267
+ // src/serve/multi-region/handlers.ts
3268
+ import { Client as Client3, Receiver } from "@upstash/qstash";
3269
+
3270
+ // src/serve/multi-region/utils.ts
3271
+ var VALID_REGIONS = ["EU_CENTRAL_1", "US_EAST_1"];
3272
+ var getRegionFromEnvironment = (environment) => {
3273
+ const region = environment.QSTASH_REGION;
3274
+ return normalizeRegionHeader(region);
3275
+ };
3276
+ function readEnvironmentVariables(environmentVariables, environment, region) {
3277
+ const result = {};
3278
+ for (const variable of environmentVariables) {
3279
+ const key = region ? `${region}_${variable}` : variable;
3280
+ result[variable] = environment[key];
3281
+ }
3282
+ return result;
3283
+ }
3284
+ function readClientEnvironmentVariables(environment, region) {
3285
+ return readEnvironmentVariables(["QSTASH_URL", "QSTASH_TOKEN"], environment, region);
3286
+ }
3287
+ function readReceiverEnvironmentVariables(environment, region) {
3288
+ return readEnvironmentVariables(
3289
+ ["QSTASH_CURRENT_SIGNING_KEY", "QSTASH_NEXT_SIGNING_KEY"],
3290
+ environment,
3291
+ region
3085
3292
  );
3086
- return {
3087
- qstashClient: options?.qstashClient ?? new Client3({
3088
- baseUrl: environment.QSTASH_URL,
3089
- token: environment.QSTASH_TOKEN
3090
- }),
3091
- onStepFinish: (workflowRunId, _finishCondition, detailedFinishCondition) => {
3092
- if (detailedFinishCondition?.condition === "auth-fail") {
3093
- console.error(AUTH_FAIL_MESSAGE);
3094
- return new Response(
3095
- JSON.stringify({
3096
- message: AUTH_FAIL_MESSAGE,
3097
- workflowRunId
3098
- }),
3099
- {
3100
- status: 400,
3101
- headers: {
3102
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3103
- }
3104
- }
3105
- );
3106
- } else if (detailedFinishCondition?.condition === "non-retryable-error") {
3107
- return new Response(JSON.stringify(formatWorkflowError(detailedFinishCondition.result)), {
3108
- headers: {
3109
- "Upstash-NonRetryable-Error": "true",
3110
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3111
- },
3112
- status: 489
3113
- });
3114
- } else if (detailedFinishCondition?.condition === "retry-after-error") {
3115
- return new Response(JSON.stringify(formatWorkflowError(detailedFinishCondition.result)), {
3116
- headers: {
3117
- "Retry-After": detailedFinishCondition.result.retryAfter.toString(),
3118
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3119
- },
3120
- status: 429
3121
- });
3122
- } else if (detailedFinishCondition?.condition === "failure-callback") {
3123
- return new Response(
3124
- JSON.stringify({ result: detailedFinishCondition.result ?? void 0 }),
3125
- {
3126
- status: 200,
3127
- headers: {
3128
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3129
- }
3130
- }
3293
+ }
3294
+ function normalizeRegionHeader(region) {
3295
+ if (!region) {
3296
+ return void 0;
3297
+ }
3298
+ region = region.replaceAll("-", "_").toUpperCase();
3299
+ if (VALID_REGIONS.includes(region)) {
3300
+ return region;
3301
+ }
3302
+ console.warn(
3303
+ `[Upstash Workflow] Invalid UPSTASH-REGION header value: "${region}". Expected one of: ${VALID_REGIONS.join(
3304
+ ", "
3305
+ )}.`
3306
+ );
3307
+ return void 0;
3308
+ }
3309
+
3310
+ // src/serve/multi-region/handlers.ts
3311
+ var getHandlersForRequest = (qstashHandlers, regionHeader, isFirstInvocation) => {
3312
+ if (qstashHandlers.mode === "single-region") {
3313
+ return qstashHandlers.handlers;
3314
+ }
3315
+ let targetRegion;
3316
+ if (isFirstInvocation) {
3317
+ targetRegion = qstashHandlers.defaultRegion;
3318
+ } else {
3319
+ const normalizedRegion = regionHeader ? normalizeRegionHeader(regionHeader) : void 0;
3320
+ targetRegion = normalizedRegion ?? qstashHandlers.defaultRegion;
3321
+ }
3322
+ const handler = qstashHandlers.handlers[targetRegion];
3323
+ if (!handler) {
3324
+ console.warn(
3325
+ `[Upstash Workflow] No handler found for region "${targetRegion}". Falling back to default region.`
3326
+ );
3327
+ return qstashHandlers.handlers[qstashHandlers.defaultRegion];
3328
+ }
3329
+ return handler;
3330
+ };
3331
+ var createRegionalHandler = (environment, receiverConfig, region, clientOptions) => {
3332
+ const clientEnv = readClientEnvironmentVariables(environment, region);
3333
+ const client = new Client3({
3334
+ ...clientOptions,
3335
+ baseUrl: clientEnv.QSTASH_URL,
3336
+ token: clientEnv.QSTASH_TOKEN
3337
+ });
3338
+ const receiver = getReceiver(environment, receiverConfig, region);
3339
+ return { client, receiver };
3340
+ };
3341
+ var shouldUseMultiRegionMode = (environment, qstashClientOption) => {
3342
+ const hasRegionEnv = Boolean(getRegionFromEnvironment(environment));
3343
+ if (hasRegionEnv && (!qstashClientOption || !("http" in qstashClientOption))) {
3344
+ return {
3345
+ isMultiRegion: true,
3346
+ defaultRegion: getRegionFromEnvironment(environment),
3347
+ clientOptions: qstashClientOption
3348
+ };
3349
+ } else {
3350
+ return { isMultiRegion: false };
3351
+ }
3352
+ };
3353
+ var getQStashHandlers = ({
3354
+ environment,
3355
+ qstashClientOption,
3356
+ receiverConfig
3357
+ }) => {
3358
+ const multiRegion = shouldUseMultiRegionMode(environment, qstashClientOption);
3359
+ if (multiRegion.isMultiRegion) {
3360
+ const regions = ["US_EAST_1", "EU_CENTRAL_1"];
3361
+ const handlers = {};
3362
+ for (const region of regions) {
3363
+ try {
3364
+ handlers[region] = createRegionalHandler(
3365
+ environment,
3366
+ receiverConfig,
3367
+ region,
3368
+ multiRegion.clientOptions
3131
3369
  );
3370
+ } catch (error) {
3371
+ console.warn(`[Upstash Workflow] Failed to create handler for region ${region}:`, error);
3132
3372
  }
3133
- return new Response(JSON.stringify({ workflowRunId }), {
3134
- status: 200,
3135
- headers: {
3136
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3137
- }
3138
- });
3139
- },
3373
+ }
3374
+ return {
3375
+ mode: "multi-region",
3376
+ handlers,
3377
+ defaultRegion: multiRegion.defaultRegion
3378
+ };
3379
+ } else {
3380
+ return {
3381
+ mode: "single-region",
3382
+ handlers: {
3383
+ client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new Client3({
3384
+ ...qstashClientOption,
3385
+ baseUrl: environment.QSTASH_URL,
3386
+ token: environment.QSTASH_TOKEN
3387
+ }),
3388
+ receiver: getReceiver(environment, receiverConfig)
3389
+ }
3390
+ };
3391
+ }
3392
+ };
3393
+ var getReceiver = (environment, receiverConfig, region) => {
3394
+ if (typeof receiverConfig === "string") {
3395
+ if (receiverConfig === "set-to-undefined") {
3396
+ return void 0;
3397
+ }
3398
+ const receiverEnv = readReceiverEnvironmentVariables(environment, region);
3399
+ return receiverEnv.QSTASH_CURRENT_SIGNING_KEY && receiverEnv.QSTASH_NEXT_SIGNING_KEY ? new Receiver({
3400
+ currentSigningKey: receiverEnv.QSTASH_CURRENT_SIGNING_KEY,
3401
+ nextSigningKey: receiverEnv.QSTASH_NEXT_SIGNING_KEY
3402
+ }) : void 0;
3403
+ } else {
3404
+ return receiverConfig;
3405
+ }
3406
+ };
3407
+ var getQStashHandlerOptions = (...params) => {
3408
+ const handlers = getQStashHandlers(...params);
3409
+ return {
3410
+ qstashHandlers: handlers,
3411
+ defaultReceiver: handlers.mode === "single-region" ? handlers.handlers.receiver : handlers.handlers[handlers.defaultRegion].receiver,
3412
+ defaultClient: handlers.mode === "single-region" ? handlers.handlers.client : handlers.handlers[handlers.defaultRegion].client
3413
+ };
3414
+ };
3415
+
3416
+ // src/serve/options.ts
3417
+ var createResponseData = (workflowRunId, detailedFinishCondition) => {
3418
+ const baseHeaders = {
3419
+ [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
3420
+ "Upstash-workflow-sdk": VERSION
3421
+ };
3422
+ if (detailedFinishCondition?.condition === "auth-fail") {
3423
+ return {
3424
+ text: JSON.stringify({
3425
+ message: AUTH_FAIL_MESSAGE,
3426
+ workflowRunId
3427
+ }),
3428
+ status: 400,
3429
+ headers: baseHeaders
3430
+ };
3431
+ } else if (detailedFinishCondition?.condition === "non-retryable-error") {
3432
+ return {
3433
+ text: JSON.stringify(formatWorkflowError(detailedFinishCondition.result)),
3434
+ status: 489,
3435
+ headers: {
3436
+ ...baseHeaders,
3437
+ "Upstash-NonRetryable-Error": "true"
3438
+ }
3439
+ };
3440
+ } else if (detailedFinishCondition?.condition === "retry-after-error") {
3441
+ return {
3442
+ text: JSON.stringify(formatWorkflowError(detailedFinishCondition.result)),
3443
+ status: 429,
3444
+ headers: {
3445
+ ...baseHeaders,
3446
+ "Retry-After": detailedFinishCondition.result.retryAfter.toString()
3447
+ }
3448
+ };
3449
+ } else if (detailedFinishCondition?.condition === "failure-callback-executed") {
3450
+ return {
3451
+ text: JSON.stringify({ result: detailedFinishCondition.result ?? void 0 }),
3452
+ status: 200,
3453
+ headers: baseHeaders
3454
+ };
3455
+ } else if (detailedFinishCondition?.condition === "failure-callback-undefined") {
3456
+ return {
3457
+ text: JSON.stringify({
3458
+ workflowRunId,
3459
+ finishCondition: detailedFinishCondition.condition
3460
+ }),
3461
+ status: 200,
3462
+ headers: {
3463
+ ...baseHeaders,
3464
+ "Upstash-Workflow-Failure-Callback-Notfound": "true"
3465
+ }
3466
+ };
3467
+ }
3468
+ return {
3469
+ text: JSON.stringify({
3470
+ workflowRunId,
3471
+ finishCondition: detailedFinishCondition.condition
3472
+ }),
3473
+ status: 200,
3474
+ headers: baseHeaders
3475
+ };
3476
+ };
3477
+ var processOptions = (options, internalOptions) => {
3478
+ const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
3479
+ const {
3480
+ qstashHandlers,
3481
+ defaultClient: qstashClient,
3482
+ defaultReceiver: receiver
3483
+ } = getQStashHandlerOptions({
3484
+ environment,
3485
+ qstashClientOption: options?.qstashClient,
3486
+ receiverConfig: options && "receiver" in options ? options.receiver ? options.receiver : "set-to-undefined" : "not-set"
3487
+ });
3488
+ return {
3489
+ qstashClient,
3140
3490
  initialPayloadParser: (initialRequest) => {
3141
3491
  if (!initialRequest) {
3142
3492
  return void 0;
@@ -3151,35 +3501,38 @@ var processOptions = (options) => {
3151
3501
  throw error;
3152
3502
  }
3153
3503
  },
3154
- receiver: receiverEnvironmentVariablesSet ? new Receiver({
3155
- currentSigningKey: environment.QSTASH_CURRENT_SIGNING_KEY,
3156
- nextSigningKey: environment.QSTASH_NEXT_SIGNING_KEY
3157
- }) : void 0,
3504
+ receiver,
3158
3505
  baseUrl: environment.UPSTASH_WORKFLOW_URL,
3159
3506
  env: environment,
3160
- retries: DEFAULT_RETRIES,
3161
- useJSONContent: false,
3162
3507
  disableTelemetry: false,
3163
- onError: console.error,
3164
- ...options
3508
+ ...options,
3509
+ // merge middlewares
3510
+ middlewares: [options?.middlewares ?? [], options?.verbose ? [loggingMiddleware] : []].flat(),
3511
+ internal: {
3512
+ generateResponse: internalOptions?.generateResponse ?? ((responseData) => {
3513
+ return new Response(responseData.text, {
3514
+ status: responseData.status,
3515
+ headers: responseData.headers
3516
+ });
3517
+ }),
3518
+ useJSONContent: internalOptions?.useJSONContent ?? false,
3519
+ qstashHandlers
3520
+ }
3165
3521
  };
3166
3522
  };
3167
- var determineUrls = async (request, url, baseUrl, failureFunction, failureUrl, debug) => {
3523
+ var determineUrls = async (request, url, baseUrl, dispatchDebug) => {
3168
3524
  const initialWorkflowUrl = url ?? request.url;
3169
3525
  const workflowUrl = baseUrl ? initialWorkflowUrl.replace(/^(https?:\/\/[^/]+)(\/.*)?$/, (_, matchedBaseUrl, path) => {
3170
3526
  return baseUrl + (path || "");
3171
3527
  }) : initialWorkflowUrl;
3172
3528
  if (workflowUrl !== initialWorkflowUrl) {
3173
- await debug?.log("WARN", "ENDPOINT_START", {
3174
- warning: `Upstash Workflow: replacing the base of the url with "${baseUrl}" and using it as workflow endpoint.`,
3175
- originalURL: initialWorkflowUrl,
3176
- updatedURL: workflowUrl
3529
+ await dispatchDebug("onInfo", {
3530
+ info: `The workflow URL's base URL has been replaced with the provided baseUrl. Original URL: ${initialWorkflowUrl}, New URL: ${workflowUrl}`
3177
3531
  });
3178
3532
  }
3179
- const workflowFailureUrl = failureFunction ? workflowUrl : failureUrl;
3180
3533
  if (workflowUrl.includes("localhost")) {
3181
- await debug?.log("WARN", "ENDPOINT_START", {
3182
- 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}`
3534
+ await dispatchDebug("onInfo", {
3535
+ 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}`
3183
3536
  });
3184
3537
  }
3185
3538
  if (!(workflowUrl.startsWith("http://") || workflowUrl.startsWith("https://"))) {
@@ -3188,205 +3541,224 @@ var determineUrls = async (request, url, baseUrl, failureFunction, failureUrl, d
3188
3541
  );
3189
3542
  }
3190
3543
  return {
3191
- workflowUrl,
3192
- workflowFailureUrl
3544
+ workflowUrl
3193
3545
  };
3194
3546
  };
3195
3547
  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`;
3196
3548
 
3197
3549
  // src/serve/index.ts
3198
- var serveBase = (routeFunction, telemetry, options) => {
3550
+ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3199
3551
  const {
3200
- qstashClient,
3201
- onStepFinish,
3202
3552
  initialPayloadParser,
3203
3553
  url,
3204
- verbose,
3205
- receiver,
3206
- failureUrl,
3207
3554
  failureFunction,
3208
3555
  baseUrl,
3209
3556
  env,
3210
- retries,
3211
- retryDelay,
3212
- useJSONContent,
3213
3557
  disableTelemetry,
3214
- flowControl,
3215
- onError
3216
- } = processOptions(options);
3558
+ middlewares,
3559
+ internal
3560
+ } = processOptions(options, internalOptions);
3217
3561
  telemetry = disableTelemetry ? void 0 : telemetry;
3218
- const debug = WorkflowLogger.getLogger(verbose);
3219
- const handler = async (request) => {
3220
- await debug?.log("INFO", "ENDPOINT_START");
3221
- const { workflowUrl, workflowFailureUrl } = await determineUrls(
3562
+ const { generateResponse: responseGenerator, useJSONContent } = internal;
3563
+ const handler = async (request, middlewareManager) => {
3564
+ await middlewareManager.dispatchDebug("onInfo", {
3565
+ info: `Received request for workflow execution.`
3566
+ });
3567
+ const { workflowUrl } = await determineUrls(
3222
3568
  request,
3223
3569
  url,
3224
3570
  baseUrl,
3225
- failureFunction,
3226
- failureUrl,
3227
- debug
3571
+ middlewareManager.dispatchDebug.bind(middlewareManager)
3572
+ );
3573
+ const { isFirstInvocation, workflowRunId, unknownSdk } = validateRequest(request);
3574
+ const regionHeader = request.headers.get("upstash-region");
3575
+ const { client: regionalClient, receiver: regionalReceiver } = getHandlersForRequest(
3576
+ internal.qstashHandlers,
3577
+ regionHeader,
3578
+ isFirstInvocation
3228
3579
  );
3229
3580
  const requestPayload = await getPayload(request) ?? "";
3230
- await verifyRequest(requestPayload, request.headers.get("upstash-signature"), receiver);
3231
- const { isFirstInvocation, workflowRunId } = validateRequest(request);
3232
- debug?.setWorkflowRunId(workflowRunId);
3233
- const { rawInitialPayload, steps, isLastDuplicate, workflowRunEnded } = await parseRequest(
3581
+ await verifyRequest(requestPayload, request.headers.get("upstash-signature"), regionalReceiver);
3582
+ middlewareManager.assignWorkflowRunId(workflowRunId);
3583
+ await middlewareManager.dispatchDebug("onInfo", {
3584
+ info: `Run id identified. isFirstInvocation: ${isFirstInvocation}, unknownSdk: ${unknownSdk}`
3585
+ });
3586
+ const { rawInitialPayload, steps, isLastDuplicate, workflowRunEnded } = await parseRequest({
3234
3587
  requestPayload,
3235
3588
  isFirstInvocation,
3589
+ unknownSdk,
3236
3590
  workflowRunId,
3237
- qstashClient.http,
3238
- request.headers.get("upstash-message-id"),
3239
- debug
3240
- );
3591
+ requester: regionalClient.http,
3592
+ messageId: request.headers.get("upstash-message-id"),
3593
+ dispatchDebug: middlewareManager.dispatchDebug.bind(middlewareManager)
3594
+ });
3241
3595
  if (workflowRunEnded) {
3242
- return onStepFinish(workflowRunId, "workflow-already-ended", {
3243
- condition: "workflow-already-ended"
3244
- });
3596
+ return responseGenerator(
3597
+ createResponseData(workflowRunId, {
3598
+ condition: "workflow-already-ended"
3599
+ })
3600
+ );
3245
3601
  }
3246
3602
  if (isLastDuplicate) {
3247
- return onStepFinish(workflowRunId, "duplicate-step", {
3248
- condition: "duplicate-step"
3249
- });
3603
+ return responseGenerator(
3604
+ createResponseData(workflowRunId, {
3605
+ condition: "duplicate-step"
3606
+ })
3607
+ );
3250
3608
  }
3251
- const failureCheck = await handleFailure(
3609
+ const failureCheck = await handleFailure({
3252
3610
  request,
3253
3611
  requestPayload,
3254
- qstashClient,
3612
+ qstashClient: regionalClient,
3255
3613
  initialPayloadParser,
3256
3614
  routeFunction,
3257
3615
  failureFunction,
3258
3616
  env,
3259
- retries,
3260
- retryDelay,
3261
- flowControl,
3262
- debug
3263
- );
3617
+ dispatchDebug: middlewareManager.dispatchDebug.bind(middlewareManager)
3618
+ });
3264
3619
  if (failureCheck.isErr()) {
3265
3620
  throw failureCheck.error;
3266
- } else if (failureCheck.value.result === "is-failure-callback") {
3267
- await debug?.log("WARN", "RESPONSE_DEFAULT", "failureFunction executed");
3268
- return onStepFinish(workflowRunId, "failure-callback", {
3269
- condition: "failure-callback",
3270
- result: failureCheck.value.response
3621
+ } else if (failureCheck.value.result === "failure-function-executed") {
3622
+ await middlewareManager.dispatchDebug("onInfo", {
3623
+ info: `Handled failure callback.`
3624
+ });
3625
+ return responseGenerator(
3626
+ createResponseData(workflowRunId, {
3627
+ condition: "failure-callback-executed",
3628
+ result: failureCheck.value.response
3629
+ })
3630
+ );
3631
+ } else if (failureCheck.value.result === "failure-function-undefined") {
3632
+ await middlewareManager.dispatchDebug("onInfo", {
3633
+ info: `Failure callback invoked but no failure function defined.`
3271
3634
  });
3635
+ return responseGenerator(
3636
+ createResponseData(workflowRunId, {
3637
+ condition: "failure-callback-undefined"
3638
+ })
3639
+ );
3272
3640
  }
3273
3641
  const invokeCount = Number(request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER) ?? "0");
3274
3642
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3275
3643
  const workflowContext = new WorkflowContext({
3276
- qstashClient,
3644
+ qstashClient: regionalClient,
3277
3645
  workflowRunId,
3278
3646
  initialPayload: initialPayloadParser(rawInitialPayload),
3279
3647
  headers: recreateUserHeaders(request.headers),
3280
3648
  steps,
3281
3649
  url: workflowUrl,
3282
- failureUrl: workflowFailureUrl,
3283
- debug,
3284
3650
  env,
3285
- retries,
3286
- retryDelay,
3287
3651
  telemetry,
3288
3652
  invokeCount,
3289
- flowControl,
3290
- label
3653
+ label,
3654
+ middlewareManager
3291
3655
  });
3292
3656
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3293
3657
  routeFunction,
3294
3658
  workflowContext
3295
3659
  );
3296
3660
  if (authCheck.isErr()) {
3297
- await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
3298
3661
  throw authCheck.error;
3299
3662
  } else if (authCheck.value === "run-ended") {
3300
- await debug?.log("ERROR", "ERROR", { error: AUTH_FAIL_MESSAGE });
3301
- return onStepFinish(
3302
- isFirstInvocation ? "no-workflow-id" : workflowContext.workflowRunId,
3303
- "auth-fail",
3304
- { condition: "auth-fail" }
3663
+ await middlewareManager.dispatchDebug("onError", {
3664
+ error: new Error(AUTH_FAIL_MESSAGE)
3665
+ });
3666
+ return responseGenerator(
3667
+ createResponseData(isFirstInvocation ? "no-workflow-id" : workflowContext.workflowRunId, {
3668
+ condition: "auth-fail"
3669
+ })
3305
3670
  );
3306
3671
  }
3307
3672
  const callReturnCheck = await handleThirdPartyCallResult({
3308
3673
  request,
3309
3674
  requestPayload: rawInitialPayload,
3310
- client: qstashClient,
3675
+ client: regionalClient,
3311
3676
  workflowUrl,
3312
- failureUrl: workflowFailureUrl,
3313
- retries,
3314
- retryDelay,
3315
- flowControl,
3316
3677
  telemetry,
3317
- debug
3678
+ middlewareManager
3318
3679
  });
3319
3680
  if (callReturnCheck.isErr()) {
3320
- await debug?.log("ERROR", "SUBMIT_THIRD_PARTY_RESULT", {
3321
- error: callReturnCheck.error.message
3322
- });
3323
3681
  throw callReturnCheck.error;
3324
3682
  } else if (callReturnCheck.value === "continue-workflow") {
3325
3683
  const result = isFirstInvocation ? await triggerFirstInvocation({
3326
3684
  workflowContext,
3327
3685
  useJSONContent,
3328
3686
  telemetry,
3329
- debug,
3330
- invokeCount
3687
+ invokeCount,
3688
+ middlewareManager,
3689
+ unknownSdk
3331
3690
  }) : await triggerRouteFunction({
3332
- onStep: async () => routeFunction(workflowContext),
3691
+ onStep: async () => {
3692
+ if (steps.length === 1) {
3693
+ await middlewareManager.dispatchLifecycle("runStarted", {});
3694
+ }
3695
+ return await routeFunction(workflowContext);
3696
+ },
3333
3697
  onCleanup: async (result2) => {
3334
- await triggerWorkflowDelete(workflowContext, result2, debug);
3698
+ await middlewareManager.dispatchLifecycle("runCompleted", {
3699
+ result: result2
3700
+ });
3701
+ await triggerWorkflowDelete(
3702
+ workflowContext,
3703
+ result2,
3704
+ false,
3705
+ middlewareManager.dispatchDebug.bind(middlewareManager)
3706
+ );
3335
3707
  },
3336
3708
  onCancel: async () => {
3337
3709
  await makeCancelRequest(workflowContext.qstashClient.http, workflowRunId);
3338
3710
  },
3339
- debug
3711
+ middlewareManager
3340
3712
  });
3341
3713
  if (result.isOk() && isInstanceOf(result.value, WorkflowNonRetryableError)) {
3342
- return onStepFinish(workflowRunId, result.value, {
3343
- condition: "non-retryable-error",
3344
- result: result.value
3345
- });
3714
+ return responseGenerator(
3715
+ createResponseData(workflowRunId, {
3716
+ condition: "non-retryable-error",
3717
+ result: result.value
3718
+ })
3719
+ );
3346
3720
  }
3347
3721
  if (result.isOk() && isInstanceOf(result.value, WorkflowRetryAfterError)) {
3348
- return onStepFinish(workflowRunId, result.value, {
3349
- condition: "retry-after-error",
3350
- result: result.value
3351
- });
3722
+ return responseGenerator(
3723
+ createResponseData(workflowRunId, {
3724
+ condition: "retry-after-error",
3725
+ result: result.value
3726
+ })
3727
+ );
3352
3728
  }
3353
3729
  if (result.isErr()) {
3354
- await debug?.log("ERROR", "ERROR", { error: result.error.message });
3355
3730
  throw result.error;
3356
3731
  }
3357
- await debug?.log("INFO", "RESPONSE_WORKFLOW");
3358
- return onStepFinish(workflowContext.workflowRunId, "success", {
3359
- condition: "success"
3732
+ await middlewareManager.dispatchDebug("onInfo", {
3733
+ info: `Workflow endpoint execution completed successfully.`
3360
3734
  });
3735
+ return responseGenerator(
3736
+ createResponseData(workflowContext.workflowRunId, {
3737
+ condition: "success"
3738
+ })
3739
+ );
3361
3740
  } else if (callReturnCheck.value === "workflow-ended") {
3362
- return onStepFinish(workflowContext.workflowRunId, "workflow-already-ended", {
3363
- condition: "workflow-already-ended"
3364
- });
3741
+ return responseGenerator(
3742
+ createResponseData(workflowContext.workflowRunId, {
3743
+ condition: "workflow-already-ended"
3744
+ })
3745
+ );
3365
3746
  }
3366
- await debug?.log("INFO", "RESPONSE_DEFAULT");
3367
- return onStepFinish("no-workflow-id", "fromCallback", {
3368
- condition: "fromCallback"
3369
- });
3747
+ return responseGenerator(
3748
+ createResponseData(workflowContext.workflowRunId, {
3749
+ condition: "fromCallback"
3750
+ })
3751
+ );
3370
3752
  };
3371
3753
  const safeHandler = async (request) => {
3754
+ const middlewareManager = new MiddlewareManager(middlewares);
3372
3755
  try {
3373
- return await handler(request);
3756
+ return await handler(request, middlewareManager);
3374
3757
  } catch (error) {
3375
3758
  const formattedError = formatWorkflowError(error);
3376
- try {
3377
- onError?.(error);
3378
- } catch (onErrorError) {
3379
- const formattedOnErrorError = formatWorkflowError(onErrorError);
3380
- const errorMessage = `Error while running onError callback: '${formattedOnErrorError.message}'.
3381
- Original error: '${formattedError.message}'`;
3382
- console.error(errorMessage);
3383
- return new Response(JSON.stringify({ error: errorMessage }), {
3384
- status: 500,
3385
- headers: {
3386
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3387
- }
3388
- });
3389
- }
3759
+ await middlewareManager.dispatchDebug("onError", {
3760
+ error: isInstanceOf(error, Error) ? error : new Error(formattedError.message)
3761
+ });
3390
3762
  return new Response(JSON.stringify(formattedError), {
3391
3763
  status: 500,
3392
3764
  headers: {
@@ -3423,7 +3795,8 @@ export {
3423
3795
  prepareFlowControl,
3424
3796
  serveManyBase,
3425
3797
  WorkflowContext,
3426
- WorkflowLogger,
3798
+ WorkflowMiddleware,
3799
+ loggingMiddleware,
3427
3800
  serveBase,
3428
3801
  serve
3429
3802
  };