@upstash/workflow 0.3.0-rc → 0.3.0-rc1

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