@upstash/workflow 0.3.0-rc → 1.0.0

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/index.js CHANGED
@@ -25,9 +25,10 @@ __export(src_exports, {
25
25
  WorkflowAbort: () => WorkflowAbort,
26
26
  WorkflowContext: () => WorkflowContext,
27
27
  WorkflowError: () => WorkflowError,
28
- WorkflowLogger: () => WorkflowLogger,
28
+ WorkflowMiddleware: () => WorkflowMiddleware,
29
29
  WorkflowNonRetryableError: () => WorkflowNonRetryableError,
30
30
  WorkflowRetryAfterError: () => WorkflowRetryAfterError,
31
+ loggingMiddleware: () => loggingMiddleware,
31
32
  serve: () => serve
32
33
  });
33
34
  module.exports = __toCommonJS(src_exports);
@@ -44,26 +45,36 @@ var WorkflowError = class extends import_qstash.QstashError {
44
45
  }
45
46
  };
46
47
  var WorkflowAbort = class extends Error {
47
- stepInfo;
48
48
  stepName;
49
+ stepInfo;
49
50
  /**
50
- * whether workflow is to be canceled on abort
51
- */
52
- cancelWorkflow;
53
- /**
54
- *
55
51
  * @param stepName name of the aborting step
56
52
  * @param stepInfo step information
57
- * @param cancelWorkflow
58
53
  */
59
- constructor(stepName, stepInfo, cancelWorkflow = false) {
54
+ constructor(stepName, stepInfo) {
60
55
  super(
61
56
  `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}'.`
62
57
  );
63
58
  this.name = "WorkflowAbort";
64
59
  this.stepName = stepName;
65
60
  this.stepInfo = stepInfo;
66
- this.cancelWorkflow = cancelWorkflow;
61
+ }
62
+ };
63
+ var WorkflowAuthError = class extends WorkflowAbort {
64
+ /**
65
+ * @param stepName name of the step found during authorization
66
+ */
67
+ constructor(stepName) {
68
+ super(stepName);
69
+ this.name = "WorkflowAuthError";
70
+ this.message = `This is an Upstash Workflow error thrown during authorization check. Found step '${stepName}' during dry-run.`;
71
+ }
72
+ };
73
+ var WorkflowCancelAbort = class extends WorkflowAbort {
74
+ constructor() {
75
+ super("cancel");
76
+ this.name = "WorkflowCancelAbort";
77
+ this.message = "Workflow has been canceled by user via context.cancel().";
67
78
  }
68
79
  };
69
80
  var WorkflowNonRetryableError = class extends WorkflowAbort {
@@ -71,22 +82,22 @@ var WorkflowNonRetryableError = class extends WorkflowAbort {
71
82
  * @param message error message to be displayed
72
83
  */
73
84
  constructor(message) {
74
- super("fail", void 0, false);
85
+ super("non-retryable-error");
75
86
  this.name = "WorkflowNonRetryableError";
76
- if (message) this.message = message;
87
+ this.message = message ?? "Workflow failed with non-retryable error.";
77
88
  }
78
89
  };
79
90
  var WorkflowRetryAfterError = class extends WorkflowAbort {
80
91
  retryAfter;
81
92
  /**
82
- * @param retryAfter time in seconds after which the workflow should be retried
83
93
  * @param message error message to be displayed
94
+ * @param retryAfter time in seconds after which the workflow should be retried
84
95
  */
85
96
  constructor(message, retryAfter) {
86
- super("retry", void 0, false);
97
+ super("retry-after-error");
87
98
  this.name = "WorkflowRetryAfterError";
99
+ this.message = message;
88
100
  this.retryAfter = retryAfter;
89
- if (message) this.message = message;
90
101
  }
91
102
  };
92
103
  var formatWorkflowError = (error) => {
@@ -145,15 +156,21 @@ var makeCancelRequest = async (requester, workflowRunId) => {
145
156
  });
146
157
  return true;
147
158
  };
148
- var getSteps = async (requester, workflowRunId, messageId, debug) => {
159
+ var getSteps = async (requester, workflowRunId, messageId, dispatchDebug) => {
149
160
  try {
150
161
  const steps = await requester.request({
151
162
  path: ["v2", "workflows", "runs", workflowRunId],
152
163
  parseResponseAsJson: true
153
164
  });
165
+ if (steps.length === 1) {
166
+ return {
167
+ steps,
168
+ workflowRunEnded: false
169
+ };
170
+ }
154
171
  if (!messageId) {
155
- await debug?.log("INFO", "ENDPOINT_START", {
156
- message: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
172
+ await dispatchDebug?.("onInfo", {
173
+ info: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
157
174
  });
158
175
  return { steps, workflowRunEnded: false };
159
176
  } else {
@@ -162,16 +179,15 @@ var getSteps = async (requester, workflowRunId, messageId, debug) => {
162
179
  return { steps: [], workflowRunEnded: false };
163
180
  }
164
181
  const filteredSteps = steps.slice(0, index + 1);
165
- await debug?.log("INFO", "ENDPOINT_START", {
166
- message: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
182
+ await dispatchDebug?.("onInfo", {
183
+ info: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
167
184
  });
168
185
  return { steps: filteredSteps, workflowRunEnded: false };
169
186
  }
170
187
  } catch (error) {
171
188
  if (isInstanceOf(error, import_qstash2.QstashError) && error.status === 404) {
172
- await debug?.log("WARN", "ENDPOINT_START", {
173
- message: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed.",
174
- error
189
+ await dispatchDebug?.("onWarning", {
190
+ warning: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed."
175
191
  });
176
192
  return { steps: void 0, workflowRunEnded: true };
177
193
  } else {
@@ -185,15 +201,18 @@ var WORKFLOW_ID_HEADER = "Upstash-Workflow-RunId";
185
201
  var WORKFLOW_INIT_HEADER = "Upstash-Workflow-Init";
186
202
  var WORKFLOW_URL_HEADER = "Upstash-Workflow-Url";
187
203
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
204
+ var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
188
205
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
189
206
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
190
207
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
208
+ var WORKFLOW_UNKOWN_SDK_VERSION_HEADER = "Upstash-Workflow-Unknown-Sdk";
209
+ var WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER = "upstash-workflow-trigger-by-sdk";
191
210
  var WORKFLOW_PROTOCOL_VERSION = "1";
192
211
  var WORKFLOW_PROTOCOL_VERSION_HEADER = "Upstash-Workflow-Sdk-Version";
193
212
  var DEFAULT_CONTENT_TYPE = "application/json";
194
213
  var NO_CONCURRENCY = 1;
195
214
  var DEFAULT_RETRIES = 3;
196
- var VERSION = "v0.3.0-rc";
215
+ var VERSION = "v1.0.0";
197
216
  var SDK_TELEMETRY = `@upstash/workflow@${VERSION}`;
198
217
  var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
199
218
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
@@ -211,8 +230,8 @@ var NANOID_LENGTH = 21;
211
230
  function getRandomInt() {
212
231
  return Math.floor(Math.random() * NANOID_CHARS.length);
213
232
  }
214
- function nanoid() {
215
- return Array.from({ length: NANOID_LENGTH }).map(() => NANOID_CHARS[getRandomInt()]).join("");
233
+ function nanoid(length = NANOID_LENGTH) {
234
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
216
235
  }
217
236
  function getWorkflowRunId(id) {
218
237
  return `wfr_${id ?? nanoid()}`;
@@ -229,6 +248,46 @@ function decodeBase64(base64) {
229
248
  return binString;
230
249
  }
231
250
  }
251
+ function getUserIdFromToken(qstashClient) {
252
+ try {
253
+ const token = qstashClient.token;
254
+ const decodedToken = decodeBase64(token);
255
+ const tokenPayload = JSON.parse(decodedToken);
256
+ const userId = tokenPayload.UserID;
257
+ if (!userId) {
258
+ throw new WorkflowError("QStash token payload does not contain userId");
259
+ }
260
+ return userId;
261
+ } catch (error) {
262
+ throw new WorkflowError(
263
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
264
+ );
265
+ }
266
+ }
267
+ function getQStashUrl(qstashClient) {
268
+ try {
269
+ const requester = qstashClient.http;
270
+ const baseUrl = requester.baseUrl;
271
+ if (!baseUrl) {
272
+ throw new WorkflowError("QStash client does not have a baseUrl");
273
+ }
274
+ return baseUrl;
275
+ } catch (error) {
276
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
277
+ }
278
+ }
279
+ function getEventId() {
280
+ return `evt_${nanoid(15)}`;
281
+ }
282
+ function stringifyBody(body) {
283
+ if (body === void 0) {
284
+ return void 0;
285
+ }
286
+ if (typeof body === "string") {
287
+ return body;
288
+ }
289
+ return JSON.stringify(body);
290
+ }
232
291
 
233
292
  // node_modules/neverthrow/dist/index.es.js
234
293
  var defaultErrorConfig = {
@@ -654,7 +713,9 @@ var StepTypes = [
654
713
  "Call",
655
714
  "Wait",
656
715
  "Notify",
657
- "Invoke"
716
+ "Invoke",
717
+ "CreateWebhook",
718
+ "WaitForWebhook"
658
719
  ];
659
720
 
660
721
  // src/workflow-requests.ts
@@ -670,23 +731,26 @@ var triggerFirstInvocation = async (params) => {
670
731
  invokeCount,
671
732
  delay,
672
733
  notBefore,
673
- keepTriggerConfig
734
+ failureUrl,
735
+ retries,
736
+ retryDelay,
737
+ flowControl,
738
+ unknownSdk
674
739
  }) => {
675
740
  const { headers } = getHeaders({
676
741
  initHeaderValue: "true",
677
742
  workflowConfig: {
678
743
  workflowRunId: workflowContext.workflowRunId,
679
744
  workflowUrl: workflowContext.url,
680
- failureUrl: workflowContext.failureUrl,
681
- retries: workflowContext.retries,
682
- retryDelay: workflowContext.retryDelay,
745
+ failureUrl,
746
+ retries,
747
+ retryDelay,
683
748
  telemetry,
684
- flowControl: workflowContext.flowControl,
749
+ flowControl,
685
750
  useJSONContent: useJSONContent ?? false
686
751
  },
687
752
  invokeCount: invokeCount ?? 0,
688
- userHeaders: workflowContext.headers,
689
- keepTriggerConfig
753
+ userHeaders: workflowContext.headers
690
754
  });
691
755
  if (workflowContext.headers.get("content-type")) {
692
756
  headers["content-type"] = workflowContext.headers.get("content-type");
@@ -694,6 +758,9 @@ var triggerFirstInvocation = async (params) => {
694
758
  if (useJSONContent) {
695
759
  headers["content-type"] = "application/json";
696
760
  }
761
+ if (unknownSdk) {
762
+ headers[WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER] = "true";
763
+ }
697
764
  if (workflowContext.label) {
698
765
  headers[WORKFLOW_LABEL_HEADER] = workflowContext.label;
699
766
  }
@@ -714,21 +781,15 @@ var triggerFirstInvocation = async (params) => {
714
781
  for (let i = 0; i < results.length; i++) {
715
782
  const result = results[i];
716
783
  const invocationParams = firstInvocationParams[i];
784
+ invocationParams.middlewareManager?.assignContext(invocationParams.workflowContext);
717
785
  if (result.deduplicated) {
718
- await invocationParams.debug?.log("WARN", "SUBMIT_FIRST_INVOCATION", {
719
- message: `Workflow run ${invocationParams.workflowContext.workflowRunId} already exists. A new one isn't created.`,
720
- headers: invocationBatch[i].headers,
721
- requestPayload: invocationParams.workflowContext.requestPayload,
722
- url: invocationParams.workflowContext.url,
723
- messageId: result.messageId
786
+ await invocationParams.middlewareManager?.dispatchDebug("onWarning", {
787
+ warning: `Workflow run ${invocationParams.workflowContext.workflowRunId} already exists. A new one isn't created.`
724
788
  });
725
789
  invocationStatuses.push("workflow-run-already-exists");
726
790
  } else {
727
- await invocationParams.debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
728
- headers: invocationBatch[i].headers,
729
- requestPayload: invocationParams.workflowContext.requestPayload,
730
- url: invocationParams.workflowContext.url,
731
- messageId: result.messageId
791
+ await invocationParams.middlewareManager?.dispatchDebug("onInfo", {
792
+ info: `Workflow run started successfully with URL ${invocationParams.workflowContext.url}.`
732
793
  });
733
794
  invocationStatuses.push("success");
734
795
  }
@@ -750,7 +811,7 @@ var triggerRouteFunction = async ({
750
811
  onCleanup,
751
812
  onStep,
752
813
  onCancel,
753
- debug
814
+ middlewareManager
754
815
  }) => {
755
816
  try {
756
817
  const result = await onStep();
@@ -759,27 +820,25 @@ var triggerRouteFunction = async ({
759
820
  } catch (error) {
760
821
  const error_ = error;
761
822
  if (isInstanceOf(error, import_qstash3.QstashError) && error.status === 400) {
762
- await debug?.log("WARN", "RESPONSE_WORKFLOW", {
763
- message: `tried to append to a cancelled workflow. exiting without publishing.`,
764
- name: error.name,
765
- errorMessage: error.message
823
+ await middlewareManager?.dispatchDebug("onWarning", {
824
+ warning: `Tried to append to a cancelled workflow. Exiting without publishing. Error: ${error.message}`
766
825
  });
767
826
  return ok("workflow-was-finished");
768
827
  } else if (isInstanceOf(error_, WorkflowNonRetryableError) || isInstanceOf(error_, WorkflowRetryAfterError)) {
769
828
  return ok(error_);
770
- } else if (!isInstanceOf(error_, WorkflowAbort)) {
771
- return err(error_);
772
- } else if (error_.cancelWorkflow) {
829
+ } else if (isInstanceOf(error_, WorkflowCancelAbort)) {
773
830
  await onCancel();
774
831
  return ok("workflow-finished");
775
- } else {
832
+ } else if (isInstanceOf(error_, WorkflowAbort)) {
776
833
  return ok("step-finished");
834
+ } else {
835
+ return err(error_);
777
836
  }
778
837
  }
779
838
  };
780
- var triggerWorkflowDelete = async (workflowContext, result, debug, cancel = false) => {
781
- await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
782
- deletedWorkflowRunId: workflowContext.workflowRunId
839
+ var triggerWorkflowDelete = async (workflowContext, result, cancel = false, dispatchDebug) => {
840
+ await dispatchDebug?.("onInfo", {
841
+ info: `Deleting workflow run ${workflowContext.workflowRunId} from QStash` + (cancel ? " with cancel=true." : ".")
783
842
  });
784
843
  await workflowContext.qstashClient.http.request({
785
844
  path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
@@ -787,11 +846,9 @@ var triggerWorkflowDelete = async (workflowContext, result, debug, cancel = fals
787
846
  parseResponseAsJson: false,
788
847
  body: JSON.stringify(result)
789
848
  });
790
- await debug?.log(
791
- "SUBMIT",
792
- "SUBMIT_CLEANUP",
793
- `workflow run ${workflowContext.workflowRunId} deleted.`
794
- );
849
+ await dispatchDebug?.("onInfo", {
850
+ info: `Workflow run ${workflowContext.workflowRunId} deleted from QStash successfully.`
851
+ });
795
852
  };
796
853
  var recreateUserHeaders = (headers) => {
797
854
  const filteredHeaders = new Headers();
@@ -813,12 +870,8 @@ var handleThirdPartyCallResult = async ({
813
870
  requestPayload,
814
871
  client,
815
872
  workflowUrl,
816
- failureUrl,
817
- retries,
818
- retryDelay,
819
873
  telemetry,
820
- flowControl,
821
- debug
874
+ middlewareManager
822
875
  }) => {
823
876
  try {
824
877
  if (request.headers.get("Upstash-Workflow-Callback")) {
@@ -835,7 +888,7 @@ var handleThirdPartyCallResult = async ({
835
888
  client.http,
836
889
  workflowRunId2,
837
890
  messageId,
838
- debug
891
+ middlewareManager?.dispatchDebug.bind(middlewareManager)
839
892
  );
840
893
  if (workflowRunEnded) {
841
894
  return ok("workflow-ended");
@@ -849,9 +902,8 @@ var handleThirdPartyCallResult = async ({
849
902
  }
850
903
  const callbackMessage = JSON.parse(callbackPayload);
851
904
  if (!(callbackMessage.status >= 200 && callbackMessage.status < 300) && callbackMessage.maxRetries && callbackMessage.retried !== callbackMessage.maxRetries) {
852
- await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
853
- status: callbackMessage.status,
854
- body: atob(callbackMessage.body ?? "")
905
+ await middlewareManager?.dispatchDebug("onWarning", {
906
+ warning: `Third party call returned status ${callbackMessage.status}. Retrying (${callbackMessage.retried} out of ${callbackMessage.maxRetries}).`
855
907
  });
856
908
  console.warn(
857
909
  `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (retried ${callbackMessage.retried ?? 0} out of ${callbackMessage.maxRetries} times). Error Message:
@@ -884,11 +936,7 @@ ${atob(callbackMessage.body ?? "")}`
884
936
  workflowConfig: {
885
937
  workflowRunId,
886
938
  workflowUrl,
887
- failureUrl,
888
- retries,
889
- retryDelay,
890
- telemetry,
891
- flowControl
939
+ telemetry
892
940
  },
893
941
  userHeaders,
894
942
  invokeCount: Number(invokeCount)
@@ -905,19 +953,17 @@ ${atob(callbackMessage.body ?? "")}`
905
953
  out: JSON.stringify(callResponse),
906
954
  concurrent: Number(concurrentString)
907
955
  };
908
- await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
909
- step: callResultStep,
910
- headers: requestHeaders,
911
- url: workflowUrl
956
+ await middlewareManager?.dispatchDebug("onInfo", {
957
+ info: `Submitting third party call result, step ${stepName} (${stepIdString}).`
912
958
  });
913
- const result = await client.publishJSON({
959
+ await client.publishJSON({
914
960
  headers: requestHeaders,
915
961
  method: "POST",
916
962
  body: callResultStep,
917
963
  url: workflowUrl
918
964
  });
919
- await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
920
- messageId: result.messageId
965
+ await middlewareManager?.dispatchDebug("onInfo", {
966
+ info: `Third party call result submitted successfully, step ${stepName} (${stepIdString}).`
921
967
  });
922
968
  return ok("is-call-return");
923
969
  } else {
@@ -966,15 +1012,17 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
966
1012
  // src/context/steps.ts
967
1013
  var BaseLazyStep = class _BaseLazyStep {
968
1014
  stepName;
969
- constructor(stepName) {
1015
+ context;
1016
+ constructor(context, stepName) {
1017
+ this.context = context;
970
1018
  if (!stepName) {
971
1019
  throw new WorkflowError(
972
1020
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
973
1021
  );
974
1022
  }
975
1023
  if (typeof stepName !== "string") {
976
- console.warn(
977
- "Workflow Warning: A workflow step name must be a string. In a future release, this will throw an error."
1024
+ throw new WorkflowError(
1025
+ `A workflow step name must be a string. Received "${stepName}" (${typeof stepName}).`
978
1026
  );
979
1027
  }
980
1028
  this.stepName = stepName;
@@ -984,13 +1032,14 @@ var BaseLazyStep = class _BaseLazyStep {
984
1032
  *
985
1033
  * will be called when returning the steps to the context from auto executor
986
1034
  *
987
- * @param out field of the step
1035
+ * @param step step
988
1036
  * @returns parsed out field
989
1037
  */
990
- parseOut(out) {
1038
+ parseOut(step) {
1039
+ const out = step.out;
991
1040
  if (out === void 0) {
992
1041
  if (this.allowUndefinedOut) {
993
- return void 0;
1042
+ return this.handleUndefinedOut(step);
994
1043
  } else {
995
1044
  throw new WorkflowError(
996
1045
  `Error while parsing output of ${this.stepType} step. Expected a string, but got: undefined`
@@ -998,27 +1047,26 @@ var BaseLazyStep = class _BaseLazyStep {
998
1047
  }
999
1048
  }
1000
1049
  if (typeof out === "object") {
1001
- if (this.stepType !== "Wait") {
1002
- console.warn(
1003
- `Error while parsing ${this.stepType} step output. Expected a string, but got object. Please reach out to Upstash Support.`
1004
- );
1005
- return out;
1006
- }
1007
- return {
1008
- ...out,
1009
- eventData: _BaseLazyStep.tryParsing(out.eventData)
1010
- };
1050
+ console.warn(
1051
+ `Error while parsing ${this.stepType} step output. Expected a string, but got object. Please reach out to Upstash Support.`
1052
+ );
1053
+ return out;
1011
1054
  }
1012
1055
  if (typeof out !== "string") {
1013
1056
  throw new WorkflowError(
1014
1057
  `Error while parsing output of ${this.stepType} step. Expected a string or undefined, but got: ${typeof out}`
1015
1058
  );
1016
1059
  }
1017
- return this.safeParseOut(out);
1060
+ return this.safeParseOut(out, step);
1018
1061
  }
1019
- safeParseOut(out) {
1062
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1063
+ safeParseOut(out, step) {
1020
1064
  return _BaseLazyStep.tryParsing(out);
1021
1065
  }
1066
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1067
+ handleUndefinedOut(step) {
1068
+ return void 0;
1069
+ }
1022
1070
  static tryParsing(stepOut) {
1023
1071
  try {
1024
1072
  return JSON.parse(stepOut);
@@ -1036,12 +1084,8 @@ var BaseLazyStep = class _BaseLazyStep {
1036
1084
  workflowConfig: {
1037
1085
  workflowRunId: context.workflowRunId,
1038
1086
  workflowUrl: context.url,
1039
- failureUrl: context.failureUrl,
1040
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1041
- retryDelay: context.retryDelay,
1042
1087
  useJSONContent: false,
1043
- telemetry,
1044
- flowControl: context.flowControl
1088
+ telemetry
1045
1089
  },
1046
1090
  userHeaders: context.headers,
1047
1091
  invokeCount,
@@ -1057,9 +1101,6 @@ var BaseLazyStep = class _BaseLazyStep {
1057
1101
  body,
1058
1102
  headers,
1059
1103
  method: "POST",
1060
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1061
- retryDelay: context.retryDelay,
1062
- flowControl: context.flowControl,
1063
1104
  url: context.url
1064
1105
  }
1065
1106
  ]);
@@ -1069,8 +1110,8 @@ var LazyFunctionStep = class extends BaseLazyStep {
1069
1110
  stepFunction;
1070
1111
  stepType = "Run";
1071
1112
  allowUndefinedOut = true;
1072
- constructor(stepName, stepFunction) {
1073
- super(stepName);
1113
+ constructor(context, stepName, stepFunction) {
1114
+ super(context, stepName);
1074
1115
  this.stepFunction = stepFunction;
1075
1116
  }
1076
1117
  getPlanStep(concurrent, targetStep) {
@@ -1100,8 +1141,8 @@ var LazySleepStep = class extends BaseLazyStep {
1100
1141
  sleep;
1101
1142
  stepType = "SleepFor";
1102
1143
  allowUndefinedOut = true;
1103
- constructor(stepName, sleep) {
1104
- super(stepName);
1144
+ constructor(context, stepName, sleep) {
1145
+ super(context, stepName);
1105
1146
  this.sleep = sleep;
1106
1147
  }
1107
1148
  getPlanStep(concurrent, targetStep) {
@@ -1130,9 +1171,6 @@ var LazySleepStep = class extends BaseLazyStep {
1130
1171
  headers,
1131
1172
  method: "POST",
1132
1173
  url: context.url,
1133
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1134
- retryDelay: context.retryDelay,
1135
- flowControl: context.flowControl,
1136
1174
  delay: isParallel ? void 0 : this.sleep
1137
1175
  }
1138
1176
  ]);
@@ -1142,8 +1180,8 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1142
1180
  sleepUntil;
1143
1181
  stepType = "SleepUntil";
1144
1182
  allowUndefinedOut = true;
1145
- constructor(stepName, sleepUntil) {
1146
- super(stepName);
1183
+ constructor(context, stepName, sleepUntil) {
1184
+ super(context, stepName);
1147
1185
  this.sleepUntil = sleepUntil;
1148
1186
  }
1149
1187
  getPlanStep(concurrent, targetStep) {
@@ -1175,9 +1213,6 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1175
1213
  headers,
1176
1214
  method: "POST",
1177
1215
  url: context.url,
1178
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1179
- retryDelay: context.retryDelay,
1180
- flowControl: context.flowControl,
1181
1216
  notBefore: isParallel ? void 0 : this.sleepUntil
1182
1217
  }
1183
1218
  ]);
@@ -1192,20 +1227,18 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1192
1227
  retryDelay;
1193
1228
  timeout;
1194
1229
  flowControl;
1195
- stringifyBody;
1196
1230
  stepType = "Call";
1197
1231
  allowUndefinedOut = false;
1198
- constructor(stepName, url, method, body, headers, retries, retryDelay, timeout, flowControl, stringifyBody) {
1199
- super(stepName);
1200
- this.url = url;
1201
- this.method = method;
1202
- this.body = body;
1203
- this.headers = headers;
1204
- this.retries = retries;
1205
- this.retryDelay = retryDelay;
1206
- this.timeout = timeout;
1207
- this.flowControl = flowControl;
1208
- this.stringifyBody = stringifyBody;
1232
+ constructor(params) {
1233
+ super(params.context, params.stepName);
1234
+ this.url = params.url;
1235
+ this.method = params.method ?? "GET";
1236
+ this.body = params.body;
1237
+ this.headers = params.headers ?? {};
1238
+ this.retries = params.retries ?? 0;
1239
+ this.retryDelay = params.retryDelay;
1240
+ this.timeout = params.timeout;
1241
+ this.flowControl = params.flowControl;
1209
1242
  }
1210
1243
  getPlanStep(concurrent, targetStep) {
1211
1244
  return {
@@ -1302,7 +1335,7 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1302
1335
  "Upstash-Callback-Workflow-CallType": "fromCallback",
1303
1336
  "Upstash-Callback-Workflow-Init": "false",
1304
1337
  "Upstash-Callback-Workflow-Url": context.url,
1305
- "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody,WF_DetectTrigger",
1338
+ "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1306
1339
  "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
1307
1340
  "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
1308
1341
  "Upstash-Callback-Forward-Upstash-Workflow-StepName": this.stepName,
@@ -1315,22 +1348,10 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1315
1348
  };
1316
1349
  }
1317
1350
  async submitStep({ context, headers }) {
1318
- let callBody;
1319
- if (this.stringifyBody) {
1320
- callBody = JSON.stringify(this.body);
1321
- } else {
1322
- if (typeof this.body === "string") {
1323
- callBody = this.body;
1324
- } else {
1325
- throw new WorkflowError(
1326
- "When stringifyBody is false, body must be a string. Please check the body type of your call step."
1327
- );
1328
- }
1329
- }
1330
1351
  return await context.qstashClient.batch([
1331
1352
  {
1332
1353
  headers,
1333
- body: callBody,
1354
+ body: this.body,
1334
1355
  method: this.method,
1335
1356
  url: this.url,
1336
1357
  retries: DEFAULT_RETRIES === this.retries ? void 0 : this.retries,
@@ -1340,13 +1361,12 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1340
1361
  ]);
1341
1362
  }
1342
1363
  };
1343
- var LazyWaitForEventStep = class extends BaseLazyStep {
1364
+ var LazyWaitEventStep = class extends BaseLazyStep {
1344
1365
  eventId;
1345
1366
  timeout;
1346
- stepType = "Wait";
1347
1367
  allowUndefinedOut = false;
1348
- constructor(stepName, eventId, timeout) {
1349
- super(stepName);
1368
+ constructor(context, stepName, eventId, timeout) {
1369
+ super(context, stepName);
1350
1370
  this.eventId = eventId;
1351
1371
  this.timeout = timeout;
1352
1372
  }
@@ -1371,13 +1391,6 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1371
1391
  concurrent
1372
1392
  });
1373
1393
  }
1374
- safeParseOut(out) {
1375
- const result = JSON.parse(out);
1376
- return {
1377
- ...result,
1378
- eventData: BaseLazyStep.tryParsing(result.eventData)
1379
- };
1380
- }
1381
1394
  getHeaders({ context, telemetry, invokeCount, step }) {
1382
1395
  const headers = super.getHeaders({ context, telemetry, invokeCount, step });
1383
1396
  headers.headers["Upstash-Workflow-CallType"] = "step";
@@ -1411,7 +1424,7 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1411
1424
  timeoutHeaders,
1412
1425
  step: {
1413
1426
  stepId: step.stepId,
1414
- stepType: "Wait",
1427
+ stepType: this.stepType,
1415
1428
  stepName: step.stepName,
1416
1429
  concurrent: step.concurrent,
1417
1430
  targetStep: step.targetStep
@@ -1432,8 +1445,8 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1432
1445
  };
1433
1446
  var LazyNotifyStep = class extends LazyFunctionStep {
1434
1447
  stepType = "Notify";
1435
- constructor(stepName, eventId, eventData, requester) {
1436
- super(stepName, async () => {
1448
+ constructor(context, stepName, eventId, eventData, requester) {
1449
+ super(context, stepName, async () => {
1437
1450
  const notifyResponse = await makeNotifyRequest(requester, eventId, eventData);
1438
1451
  return {
1439
1452
  eventId,
@@ -1458,28 +1471,10 @@ var LazyInvokeStep = class extends BaseLazyStep {
1458
1471
  * workflow id of the invoked workflow
1459
1472
  */
1460
1473
  workflowId;
1461
- constructor(stepName, {
1462
- workflow,
1463
- body,
1464
- headers = {},
1465
- workflowRunId,
1466
- retries,
1467
- retryDelay,
1468
- flowControl,
1469
- stringifyBody = true
1470
- }) {
1471
- super(stepName);
1472
- this.params = {
1473
- workflow,
1474
- body,
1475
- headers,
1476
- workflowRunId: getWorkflowRunId(workflowRunId),
1477
- retries,
1478
- retryDelay,
1479
- flowControl,
1480
- stringifyBody
1481
- };
1482
- const { workflowId } = workflow;
1474
+ constructor(context, stepName, params) {
1475
+ super(context, stepName);
1476
+ this.params = params;
1477
+ const { workflowId } = params.workflow;
1483
1478
  if (!workflowId) {
1484
1479
  throw new WorkflowError("You can only invoke workflow which has a workflowId");
1485
1480
  }
@@ -1519,31 +1514,18 @@ var LazyInvokeStep = class extends BaseLazyStep {
1519
1514
  workflowConfig: {
1520
1515
  workflowRunId: context.workflowRunId,
1521
1516
  workflowUrl: context.url,
1522
- failureUrl: context.failureUrl,
1523
- retries: context.retries,
1524
- retryDelay: context.retryDelay,
1525
1517
  telemetry,
1526
- flowControl: context.flowControl,
1527
1518
  useJSONContent: false
1528
1519
  },
1529
1520
  userHeaders: context.headers,
1530
1521
  invokeCount
1531
1522
  });
1523
+ context.qstashClient.http.headers?.forEach((value, key) => {
1524
+ invokerHeaders[key] = value;
1525
+ });
1532
1526
  invokerHeaders["Upstash-Workflow-Runid"] = context.workflowRunId;
1533
- let invokeBody;
1534
- if (this.params.stringifyBody) {
1535
- invokeBody = JSON.stringify(this.params.body);
1536
- } else {
1537
- if (typeof this.params.body === "string") {
1538
- invokeBody = this.params.body;
1539
- } else {
1540
- throw new WorkflowError(
1541
- "When stringifyBody is false, body must be a string. Please check the body type of your invoke step."
1542
- );
1543
- }
1544
- }
1545
1527
  const request = {
1546
- body: invokeBody,
1528
+ body: stringifyBody(this.params.body),
1547
1529
  headers: Object.fromEntries(
1548
1530
  Object.entries(invokerHeaders).map((pairs) => [pairs[0], [pairs[1]]])
1549
1531
  ),
@@ -1554,34 +1536,19 @@ var LazyInvokeStep = class extends BaseLazyStep {
1554
1536
  return JSON.stringify(request);
1555
1537
  }
1556
1538
  getHeaders({ context, telemetry, invokeCount }) {
1557
- const {
1558
- workflow,
1559
- headers = {},
1560
- workflowRunId = getWorkflowRunId(),
1561
- retries,
1562
- retryDelay,
1563
- flowControl
1564
- } = this.params;
1539
+ const { workflow, headers = {}, workflowRunId, retries, retryDelay, flowControl } = this.params;
1565
1540
  const newUrl = context.url.replace(/[^/]+$/, this.workflowId);
1566
- const {
1567
- retries: workflowRetries,
1568
- retryDelay: workflowRetryDelay,
1569
- failureFunction,
1570
- failureUrl,
1571
- useJSONContent,
1572
- flowControl: workflowFlowControl
1573
- } = workflow.options;
1574
1541
  const { headers: triggerHeaders, contentType } = getHeaders({
1575
1542
  initHeaderValue: "true",
1576
1543
  workflowConfig: {
1577
- workflowRunId,
1544
+ workflowRunId: getWorkflowRunId(workflowRunId),
1578
1545
  workflowUrl: newUrl,
1579
- retries: retries ?? workflowRetries,
1580
- retryDelay: retryDelay ?? workflowRetryDelay,
1546
+ retries,
1547
+ retryDelay,
1581
1548
  telemetry,
1582
- failureUrl: failureFunction ? newUrl : failureUrl,
1583
- flowControl: flowControl ?? workflowFlowControl,
1584
- useJSONContent: useJSONContent ?? false
1549
+ failureUrl: newUrl,
1550
+ flowControl,
1551
+ useJSONContent: workflow.useJSONContent ?? false
1585
1552
  },
1586
1553
  invokeCount: invokeCount + 1,
1587
1554
  userHeaders: new Headers(headers)
@@ -1600,6 +1567,88 @@ var LazyInvokeStep = class extends BaseLazyStep {
1600
1567
  return [result];
1601
1568
  }
1602
1569
  };
1570
+ var LazyCreateWebhookStep = class extends BaseLazyStep {
1571
+ stepType = "CreateWebhook";
1572
+ allowUndefinedOut = false;
1573
+ getPlanStep(concurrent, targetStep) {
1574
+ return {
1575
+ stepId: 0,
1576
+ stepName: this.stepName,
1577
+ stepType: this.stepType,
1578
+ concurrent,
1579
+ targetStep
1580
+ };
1581
+ }
1582
+ async getResultStep(concurrent, stepId) {
1583
+ return {
1584
+ stepId,
1585
+ stepName: this.stepName,
1586
+ stepType: this.stepType,
1587
+ out: void 0,
1588
+ concurrent
1589
+ };
1590
+ }
1591
+ getBody({ step, context }) {
1592
+ const userId = getUserIdFromToken(context.qstashClient);
1593
+ const workflowRunId = context.workflowRunId;
1594
+ const eventId = getEventId();
1595
+ const qstashUrl = getQStashUrl(this.context.qstashClient);
1596
+ return JSON.stringify({
1597
+ ...step,
1598
+ out: JSON.stringify({
1599
+ webhookUrl: `${qstashUrl}/v2/workflows/hooks/${userId}/${workflowRunId}/${eventId}`,
1600
+ eventId
1601
+ })
1602
+ });
1603
+ }
1604
+ };
1605
+ var LazyWaitForWebhookStep = class extends LazyWaitEventStep {
1606
+ stepType = "WaitForWebhook";
1607
+ allowUndefinedOut = true;
1608
+ constructor(context, stepName, webhook, timeout) {
1609
+ super(context, stepName, webhook.eventId, timeout);
1610
+ }
1611
+ safeParseOut(out) {
1612
+ const eventData = decodeBase64(out);
1613
+ const parsedEventData = BaseLazyStep.tryParsing(eventData);
1614
+ const body = parsedEventData.body;
1615
+ const parsedBody = typeof body === "string" ? decodeBase64(body) : void 0;
1616
+ const request = new Request(
1617
+ `${parsedEventData.proto}://${parsedEventData.host}${parsedEventData.url}`,
1618
+ {
1619
+ method: parsedEventData.method,
1620
+ headers: parsedEventData.header,
1621
+ body: parsedBody
1622
+ }
1623
+ );
1624
+ return {
1625
+ request,
1626
+ timeout: false
1627
+ };
1628
+ }
1629
+ handleUndefinedOut() {
1630
+ return {
1631
+ timeout: true,
1632
+ request: void 0
1633
+ };
1634
+ }
1635
+ };
1636
+ var LazyWaitForEventStep = class extends LazyWaitEventStep {
1637
+ stepType = "Wait";
1638
+ allowUndefinedOut = true;
1639
+ parseWaitForEventOut(out, waitTimeout) {
1640
+ return {
1641
+ eventData: out ? BaseLazyStep.tryParsing(decodeBase64(out)) : void 0,
1642
+ timeout: waitTimeout ?? false
1643
+ };
1644
+ }
1645
+ safeParseOut(out, step) {
1646
+ return this.parseWaitForEventOut(out, step.waitTimeout);
1647
+ }
1648
+ handleUndefinedOut(step) {
1649
+ return this.parseWaitForEventOut(void 0, step.waitTimeout);
1650
+ }
1651
+ };
1603
1652
 
1604
1653
  // src/qstash/headers.ts
1605
1654
  var WorkflowHeaders = class {
@@ -1609,14 +1658,15 @@ var WorkflowHeaders = class {
1609
1658
  initHeaderValue;
1610
1659
  stepInfo;
1611
1660
  headers;
1612
- keepTriggerConfig;
1661
+ /**
1662
+ * @param params workflow header parameters
1663
+ */
1613
1664
  constructor({
1614
1665
  userHeaders,
1615
1666
  workflowConfig,
1616
1667
  invokeCount,
1617
1668
  initHeaderValue,
1618
- stepInfo,
1619
- keepTriggerConfig
1669
+ stepInfo
1620
1670
  }) {
1621
1671
  this.userHeaders = userHeaders;
1622
1672
  this.workflowConfig = workflowConfig;
@@ -1628,7 +1678,6 @@ var WorkflowHeaders = class {
1628
1678
  workflowHeaders: {},
1629
1679
  failureHeaders: {}
1630
1680
  };
1631
- this.keepTriggerConfig = keepTriggerConfig;
1632
1681
  }
1633
1682
  getHeaders() {
1634
1683
  this.addBaseHeaders();
@@ -1647,7 +1696,7 @@ var WorkflowHeaders = class {
1647
1696
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1648
1697
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1649
1698
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1650
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger" + (this.keepTriggerConfig ? ",WF_TriggerOnConfig" : ""),
1699
+ [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1651
1700
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1652
1701
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1653
1702
  };
@@ -1717,12 +1766,12 @@ var WorkflowHeaders = class {
1717
1766
  }
1718
1767
  this.headers.workflowHeaders["Failure-Callback"] = this.workflowConfig.failureUrl;
1719
1768
  this.headers.failureHeaders[`Forward-${WORKFLOW_FAILURE_HEADER}`] = "true";
1720
- this.headers.failureHeaders[`Forward-Upstash-Workflow-Failure-Callback`] = "true";
1769
+ this.headers.failureHeaders[`Forward-${WORKFLOW_FAILURE_CALLBACK_HEADER}`] = "true";
1721
1770
  this.headers.failureHeaders["Workflow-Runid"] = this.workflowConfig.workflowRunId;
1722
1771
  this.headers.failureHeaders["Workflow-Init"] = "false";
1723
1772
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
1724
1773
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
1725
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger";
1774
+ this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
1726
1775
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
1727
1776
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
1728
1777
  }
@@ -1792,14 +1841,13 @@ var submitParallelSteps = async ({
1792
1841
  initialStepCount,
1793
1842
  invokeCount,
1794
1843
  telemetry,
1795
- debug
1844
+ dispatchDebug
1796
1845
  }) => {
1797
1846
  const planSteps = steps.map(
1798
1847
  (step, index) => step.getPlanStep(steps.length, initialStepCount + index)
1799
1848
  );
1800
- await debug?.log("SUBMIT", "SUBMIT_STEP", {
1801
- length: planSteps.length,
1802
- steps: planSteps
1849
+ await dispatchDebug("onInfo", {
1850
+ info: `Submitting ${planSteps.length} parallel steps.`
1803
1851
  });
1804
1852
  const result = await context.qstashClient.batch(
1805
1853
  planSteps.map((planStep) => {
@@ -1808,10 +1856,6 @@ var submitParallelSteps = async ({
1808
1856
  workflowConfig: {
1809
1857
  workflowRunId: context.workflowRunId,
1810
1858
  workflowUrl: context.url,
1811
- failureUrl: context.failureUrl,
1812
- retries: context.retries,
1813
- retryDelay: context.retryDelay,
1814
- flowControl: context.flowControl,
1815
1859
  telemetry
1816
1860
  },
1817
1861
  userHeaders: context.headers,
@@ -1827,13 +1871,11 @@ var submitParallelSteps = async ({
1827
1871
  };
1828
1872
  })
1829
1873
  );
1830
- await debug?.log("INFO", "SUBMIT_STEP", {
1831
- messageIds: result.map((message) => {
1832
- return {
1833
- message: message.messageId
1834
- };
1835
- })
1836
- });
1874
+ if (result && result.length > 0) {
1875
+ await dispatchDebug("onInfo", {
1876
+ info: `Submitted ${planSteps.length} parallel steps. messageIds: ${result.filter((r) => r).map((r) => r.messageId).join(", ")}.`
1877
+ });
1878
+ }
1837
1879
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
1838
1880
  };
1839
1881
  var submitSingleStep = async ({
@@ -1843,14 +1885,13 @@ var submitSingleStep = async ({
1843
1885
  invokeCount,
1844
1886
  concurrency,
1845
1887
  telemetry,
1846
- debug
1888
+ dispatchDebug,
1889
+ dispatchLifecycle
1847
1890
  }) => {
1848
- const resultStep = await lazyStep.getResultStep(concurrency, stepId);
1849
- await debug?.log("INFO", "RUN_SINGLE", {
1850
- fromRequest: false,
1851
- step: resultStep,
1852
- stepCount: stepId
1891
+ await dispatchLifecycle("beforeExecution", {
1892
+ stepName: lazyStep.stepName
1853
1893
  });
1894
+ const resultStep = await lazyStep.getResultStep(concurrency, stepId);
1854
1895
  const { headers } = lazyStep.getHeaders({
1855
1896
  context,
1856
1897
  step: resultStep,
@@ -1864,10 +1905,6 @@ var submitSingleStep = async ({
1864
1905
  invokeCount,
1865
1906
  telemetry
1866
1907
  });
1867
- await debug?.log("SUBMIT", "SUBMIT_STEP", {
1868
- length: 1,
1869
- steps: [resultStep]
1870
- });
1871
1908
  const submitResult = await lazyStep.submitStep({
1872
1909
  context,
1873
1910
  body,
@@ -1877,13 +1914,11 @@ var submitSingleStep = async ({
1877
1914
  step: resultStep,
1878
1915
  telemetry
1879
1916
  });
1880
- await debug?.log("INFO", "SUBMIT_STEP", {
1881
- messageIds: submitResult.map((message) => {
1882
- return {
1883
- message: message.messageId
1884
- };
1885
- })
1886
- });
1917
+ if (submitResult && submitResult[0]) {
1918
+ await dispatchDebug("onInfo", {
1919
+ info: `Submitted step "${resultStep.stepName}" with messageId: ${submitResult[0].messageId}.`
1920
+ });
1921
+ }
1887
1922
  return resultStep;
1888
1923
  };
1889
1924
 
@@ -1892,21 +1927,31 @@ var AutoExecutor = class _AutoExecutor {
1892
1927
  context;
1893
1928
  promises = /* @__PURE__ */ new WeakMap();
1894
1929
  activeLazyStepList;
1895
- debug;
1896
1930
  nonPlanStepCount;
1897
1931
  steps;
1898
1932
  indexInCurrentList = 0;
1899
1933
  invokeCount;
1900
1934
  telemetry;
1935
+ dispatchDebug;
1936
+ dispatchLifecycle;
1901
1937
  stepCount = 0;
1902
1938
  planStepCount = 0;
1903
1939
  executingStep = false;
1904
- constructor(context, steps, telemetry, invokeCount, debug) {
1940
+ /**
1941
+ * @param context workflow context
1942
+ * @param steps list of steps
1943
+ * @param dispatchDebug debug event dispatcher
1944
+ * @param dispatchLifecycle lifecycle event dispatcher
1945
+ * @param telemetry optional telemetry information
1946
+ * @param invokeCount optional invoke count
1947
+ */
1948
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
1905
1949
  this.context = context;
1906
1950
  this.steps = steps;
1951
+ this.dispatchDebug = dispatchDebug;
1952
+ this.dispatchLifecycle = dispatchLifecycle;
1907
1953
  this.telemetry = telemetry;
1908
1954
  this.invokeCount = invokeCount ?? 0;
1909
- this.debug = debug;
1910
1955
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
1911
1956
  }
1912
1957
  /**
@@ -1974,7 +2019,7 @@ var AutoExecutor = class _AutoExecutor {
1974
2019
  /**
1975
2020
  * Executes a step:
1976
2021
  * - If the step result is available in the steps, returns the result
1977
- * - If the result is not avaiable, runs the function
2022
+ * - If the result is not available, runs the function
1978
2023
  * - Sends the result to QStash
1979
2024
  *
1980
2025
  * @param lazyStep lazy step to execute
@@ -1984,12 +2029,15 @@ var AutoExecutor = class _AutoExecutor {
1984
2029
  if (this.stepCount < this.nonPlanStepCount) {
1985
2030
  const step = this.steps[this.stepCount + this.planStepCount];
1986
2031
  validateStep(lazyStep, step);
1987
- await this.debug?.log("INFO", "RUN_SINGLE", {
1988
- fromRequest: true,
1989
- step,
1990
- stepCount: this.stepCount
1991
- });
1992
- return lazyStep.parseOut(step.out);
2032
+ const parsedOut = lazyStep.parseOut(step);
2033
+ const isLastMemoized = this.stepCount + 1 === this.nonPlanStepCount && this.steps.at(-1).stepId !== 0;
2034
+ if (isLastMemoized) {
2035
+ await this.dispatchLifecycle("afterExecution", {
2036
+ stepName: lazyStep.stepName,
2037
+ result: parsedOut
2038
+ });
2039
+ }
2040
+ return parsedOut;
1993
2041
  }
1994
2042
  const resultStep = await submitSingleStep({
1995
2043
  context: this.context,
@@ -1998,15 +2046,15 @@ var AutoExecutor = class _AutoExecutor {
1998
2046
  invokeCount: this.invokeCount,
1999
2047
  concurrency: 1,
2000
2048
  telemetry: this.telemetry,
2001
- debug: this.debug
2049
+ dispatchDebug: this.dispatchDebug,
2050
+ dispatchLifecycle: this.dispatchLifecycle
2002
2051
  });
2003
2052
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2004
2053
  }
2005
2054
  /**
2006
2055
  * Runs steps in parallel.
2007
2056
  *
2008
- * @param stepName parallel step name
2009
- * @param stepFunctions list of async functions to run in parallel
2057
+ * @param parallelSteps list of lazy steps to run in parallel
2010
2058
  * @returns results of the functions run in parallel
2011
2059
  */
2012
2060
  async runParallel(parallelSteps) {
@@ -2019,12 +2067,14 @@ var AutoExecutor = class _AutoExecutor {
2019
2067
  `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
2020
2068
  );
2021
2069
  }
2022
- await this.debug?.log("INFO", "RUN_PARALLEL", {
2023
- parallelCallState,
2024
- initialStepCount,
2025
- plannedParallelStepCount,
2026
- stepCount: this.stepCount,
2027
- planStepCount: this.planStepCount
2070
+ await this.dispatchDebug("onInfo", {
2071
+ info: `Executing parallel steps with: ` + JSON.stringify({
2072
+ parallelCallState,
2073
+ initialStepCount,
2074
+ plannedParallelStepCount,
2075
+ stepCount: this.stepCount,
2076
+ planStepCount: this.planStepCount
2077
+ })
2028
2078
  });
2029
2079
  switch (parallelCallState) {
2030
2080
  case "first": {
@@ -2034,7 +2084,7 @@ var AutoExecutor = class _AutoExecutor {
2034
2084
  initialStepCount,
2035
2085
  invokeCount: this.invokeCount,
2036
2086
  telemetry: this.telemetry,
2037
- debug: this.debug
2087
+ dispatchDebug: this.dispatchDebug
2038
2088
  });
2039
2089
  break;
2040
2090
  }
@@ -2056,7 +2106,8 @@ var AutoExecutor = class _AutoExecutor {
2056
2106
  invokeCount: this.invokeCount,
2057
2107
  concurrency: parallelSteps.length,
2058
2108
  telemetry: this.telemetry,
2059
- debug: this.debug
2109
+ dispatchDebug: this.dispatchDebug,
2110
+ dispatchLifecycle: this.dispatchLifecycle
2060
2111
  });
2061
2112
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2062
2113
  } catch (error) {
@@ -2070,13 +2121,34 @@ var AutoExecutor = class _AutoExecutor {
2070
2121
  break;
2071
2122
  }
2072
2123
  case "discard": {
2124
+ const resultStep = this.steps.at(-1);
2125
+ const lazyStep = parallelSteps.find(
2126
+ (planStep, index) => resultStep.stepId - index === initialStepCount
2127
+ );
2128
+ if (lazyStep) {
2129
+ await this.dispatchLifecycle("afterExecution", {
2130
+ stepName: lazyStep.stepName,
2131
+ result: lazyStep.parseOut(resultStep)
2132
+ });
2133
+ }
2073
2134
  throw new WorkflowAbort("discarded parallel");
2074
2135
  }
2075
2136
  case "last": {
2076
2137
  const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
2077
2138
  validateParallelSteps(parallelSteps, parallelResultSteps);
2139
+ const isLastMemoized = this.stepCount + 1 === this.nonPlanStepCount && this.steps.at(-1).stepId !== 0;
2140
+ if (isLastMemoized) {
2141
+ const resultStep = this.steps.at(-1);
2142
+ const lazyStep = parallelSteps.find(
2143
+ (planStep, index) => resultStep.stepId - index === initialStepCount
2144
+ );
2145
+ await this.dispatchLifecycle("afterExecution", {
2146
+ stepName: lazyStep.stepName,
2147
+ result: lazyStep.parseOut(resultStep)
2148
+ });
2149
+ }
2078
2150
  return parallelResultSteps.map(
2079
- (step, index) => parallelSteps[index].parseOut(step.out)
2151
+ (step, index) => parallelSteps[index].parseOut(step)
2080
2152
  );
2081
2153
  }
2082
2154
  }
@@ -2132,7 +2204,6 @@ var AutoExecutor = class _AutoExecutor {
2132
2204
  * @param index index of the current step
2133
2205
  * @returns result[index] if lazyStepList > 1, otherwise result
2134
2206
  */
2135
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
2136
2207
  static getResult(lazyStepList, result, index) {
2137
2208
  if (lazyStepList.length === 1) {
2138
2209
  return result;
@@ -2219,15 +2290,18 @@ var getProviderInfo = (api) => {
2219
2290
  // src/context/api/base.ts
2220
2291
  var BaseWorkflowApi = class {
2221
2292
  context;
2293
+ /**
2294
+ * @param context workflow context
2295
+ */
2222
2296
  constructor({ context }) {
2223
2297
  this.context = context;
2224
2298
  }
2225
2299
  /**
2226
2300
  * context.call which uses a QStash API
2227
2301
  *
2228
- * @param stepName
2229
- * @param settings
2230
- * @returns
2302
+ * @param stepName name of the step
2303
+ * @param settings call settings including api configuration
2304
+ * @returns call response
2231
2305
  */
2232
2306
  async callApi(stepName, settings) {
2233
2307
  const { url, appendHeaders, method } = getProviderInfo(settings.api);
@@ -2235,7 +2309,7 @@ var BaseWorkflowApi = class {
2235
2309
  return await this.context.call(stepName, {
2236
2310
  url,
2237
2311
  method: userMethod ?? method,
2238
- body,
2312
+ body: typeof body === "string" ? body : JSON.stringify(body),
2239
2313
  headers: {
2240
2314
  ...appendHeaders,
2241
2315
  ...headers
@@ -2320,6 +2394,129 @@ var getNewUrlFromWorkflowId = (url, workflowId) => {
2320
2394
  return url.replace(/[^/]+$/, workflowId);
2321
2395
  };
2322
2396
 
2397
+ // src/middleware/default-callbacks.ts
2398
+ var onErrorWithConsole = async ({ workflowRunId, error }) => {
2399
+ console.error(` [Upstash Workflow]: Error in workflow run ${workflowRunId}: ` + error);
2400
+ };
2401
+ var onWarningWithConsole = async ({ workflowRunId, warning }) => {
2402
+ console.warn(` [Upstash Workflow]: Warning in workflow run ${workflowRunId}: ` + warning);
2403
+ };
2404
+ var onInfoWithConsole = async ({
2405
+ workflowRunId,
2406
+ info
2407
+ }) => {
2408
+ console.info(` [Upstash Workflow]: Info in workflow run ${workflowRunId}: ` + info);
2409
+ };
2410
+
2411
+ // src/middleware/manager.ts
2412
+ var MiddlewareManager = class {
2413
+ middlewares;
2414
+ workflowRunId;
2415
+ context;
2416
+ /**
2417
+ * @param middlewares list of workflow middlewares
2418
+ */
2419
+ constructor(middlewares = []) {
2420
+ this.middlewares = middlewares;
2421
+ }
2422
+ /**
2423
+ * Assign workflow run ID - will be passed to debug events
2424
+ *
2425
+ * @param workflowRunId workflow run id to assign
2426
+ */
2427
+ assignWorkflowRunId(workflowRunId) {
2428
+ this.workflowRunId = workflowRunId;
2429
+ }
2430
+ /**
2431
+ * Assign context - required for lifecycle events
2432
+ *
2433
+ * also assigns workflowRunId from context
2434
+ *
2435
+ * @param context workflow context to assign
2436
+ */
2437
+ assignContext(context) {
2438
+ this.context = context;
2439
+ this.workflowRunId = context.workflowRunId;
2440
+ }
2441
+ /**
2442
+ * Internal method to execute middlewares with common error handling logic
2443
+ *
2444
+ * @param event event name to dispatch
2445
+ * @param params event parameters
2446
+ */
2447
+ async executeMiddlewares(event, params) {
2448
+ await Promise.all(this.middlewares.map((m) => m.ensureInit()));
2449
+ await Promise.all(
2450
+ this.middlewares.map(async (middleware) => {
2451
+ const callback = middleware.getCallback(event);
2452
+ if (callback) {
2453
+ try {
2454
+ await callback(params);
2455
+ } catch (error) {
2456
+ try {
2457
+ const onErrorCallback = middleware.getCallback("onError") ?? onErrorWithConsole;
2458
+ await onErrorCallback({
2459
+ workflowRunId: this.workflowRunId,
2460
+ error
2461
+ });
2462
+ } catch (onErrorError) {
2463
+ console.error(
2464
+ `Failed while executing "onError" of middleware "${middleware.name}", falling back to logging the error to console. Error: ${onErrorError}`
2465
+ );
2466
+ onErrorWithConsole({
2467
+ workflowRunId: this.workflowRunId,
2468
+ error
2469
+ });
2470
+ }
2471
+ }
2472
+ }
2473
+ })
2474
+ );
2475
+ if (event === "onError") {
2476
+ onErrorWithConsole({
2477
+ workflowRunId: this.workflowRunId,
2478
+ ...params
2479
+ });
2480
+ } else if (event === "onWarning") {
2481
+ onWarningWithConsole({
2482
+ workflowRunId: this.workflowRunId,
2483
+ ...params
2484
+ });
2485
+ }
2486
+ }
2487
+ /**
2488
+ * Dispatch a debug event (onError, onWarning, onInfo)
2489
+ *
2490
+ * @param event debug event name
2491
+ * @param params event parameters
2492
+ */
2493
+ async dispatchDebug(event, params) {
2494
+ const paramsWithRunId = {
2495
+ ...params,
2496
+ workflowRunId: this.workflowRunId
2497
+ };
2498
+ await this.executeMiddlewares(event, paramsWithRunId);
2499
+ }
2500
+ /**
2501
+ * Dispatch a lifecycle event (beforeExecution, afterExecution, runStarted, runCompleted)
2502
+ *
2503
+ * @param event lifecycle event name
2504
+ * @param params event parameters
2505
+ */
2506
+ async dispatchLifecycle(event, params) {
2507
+ if (!this.context) {
2508
+ throw new WorkflowError(
2509
+ `Something went wrong while calling middlewares. Lifecycle event "${event}" was called before assignContext.`
2510
+ );
2511
+ }
2512
+ const paramsWithContext = {
2513
+ ...params,
2514
+ context: this.context
2515
+ };
2516
+ await this.executeMiddlewares(event, paramsWithContext);
2517
+ }
2518
+ };
2519
+
2323
2520
  // src/context/context.ts
2324
2521
  var WorkflowContext = class {
2325
2522
  executor;
@@ -2364,25 +2561,6 @@ var WorkflowContext = class {
2364
2561
  * ```
2365
2562
  */
2366
2563
  url;
2367
- /**
2368
- * URL to call in case of workflow failure with QStash failure callback
2369
- *
2370
- * https://upstash.com/docs/qstash/features/callbacks#what-is-a-failure-callback
2371
- *
2372
- * Can be overwritten by passing a `failureUrl` parameter in `serve`:
2373
- *
2374
- * ```ts
2375
- * export const POST = serve(
2376
- * async (context) => {
2377
- * ...
2378
- * },
2379
- * {
2380
- * failureUrl: "new-url-value"
2381
- * }
2382
- * )
2383
- * ```
2384
- */
2385
- failureUrl;
2386
2564
  /**
2387
2565
  * Payload of the request which started the workflow.
2388
2566
  *
@@ -2438,46 +2616,6 @@ var WorkflowContext = class {
2438
2616
  * Default value is set to `process.env`.
2439
2617
  */
2440
2618
  env;
2441
- /**
2442
- * Number of retries
2443
- */
2444
- retries;
2445
- /**
2446
- * Delay between retries.
2447
- *
2448
- * By default, the `retryDelay` is exponential backoff.
2449
- * More details can be found in: https://upstash.com/docs/qstash/features/retry.
2450
- *
2451
- * The `retryDelay` option allows you to customize the delay (in milliseconds) between retry attempts when message delivery fails.
2452
- *
2453
- * You can use mathematical expressions and the following built-in functions to calculate the delay dynamically.
2454
- * The special variable `retried` represents the current retry attempt count (starting from 0).
2455
- *
2456
- * Supported functions:
2457
- * - `pow`
2458
- * - `sqrt`
2459
- * - `abs`
2460
- * - `exp`
2461
- * - `floor`
2462
- * - `ceil`
2463
- * - `round`
2464
- * - `min`
2465
- * - `max`
2466
- *
2467
- * Examples of valid `retryDelay` values:
2468
- * ```ts
2469
- * 1000 // 1 second
2470
- * 1000 * (1 + retried) // 1 second multiplied by the current retry attempt
2471
- * pow(2, retried) // 2 to the power of the current retry attempt
2472
- * max(10, pow(2, retried)) // The greater of 10 or 2^retried
2473
- * ```
2474
- */
2475
- retryDelay;
2476
- /**
2477
- * Settings for controlling the number of active requests
2478
- * and number of requests per second with the same key.
2479
- */
2480
- flowControl;
2481
2619
  /**
2482
2620
  * Label to apply to the workflow run.
2483
2621
  *
@@ -2500,30 +2638,31 @@ var WorkflowContext = class {
2500
2638
  headers,
2501
2639
  steps,
2502
2640
  url,
2503
- failureUrl,
2504
- debug,
2505
2641
  initialPayload,
2506
2642
  env,
2507
- retries,
2508
- retryDelay,
2509
2643
  telemetry,
2510
2644
  invokeCount,
2511
- flowControl,
2512
- label
2645
+ label,
2646
+ middlewareManager
2513
2647
  }) {
2514
2648
  this.qstashClient = qstashClient;
2515
2649
  this.workflowRunId = workflowRunId;
2516
2650
  this.steps = steps;
2517
2651
  this.url = url;
2518
- this.failureUrl = failureUrl;
2519
2652
  this.headers = headers;
2520
2653
  this.requestPayload = initialPayload;
2521
2654
  this.env = env ?? {};
2522
- this.retries = retries ?? DEFAULT_RETRIES;
2523
- this.retryDelay = retryDelay;
2524
- this.flowControl = flowControl;
2525
2655
  this.label = label;
2526
- this.executor = new AutoExecutor(this, this.steps, telemetry, invokeCount, debug);
2656
+ const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
2657
+ middlewareManagerInstance.assignContext(this);
2658
+ this.executor = new AutoExecutor(
2659
+ this,
2660
+ this.steps,
2661
+ middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
2662
+ middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
2663
+ telemetry,
2664
+ invokeCount
2665
+ );
2527
2666
  }
2528
2667
  /**
2529
2668
  * Executes a workflow step
@@ -2554,7 +2693,7 @@ var WorkflowContext = class {
2554
2693
  */
2555
2694
  async run(stepName, stepFunction) {
2556
2695
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2557
- return await this.addStep(new LazyFunctionStep(stepName, wrappedStepFunction));
2696
+ return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
2558
2697
  }
2559
2698
  /**
2560
2699
  * Stops the execution for the duration provided.
@@ -2568,7 +2707,7 @@ var WorkflowContext = class {
2568
2707
  * @returns undefined
2569
2708
  */
2570
2709
  async sleep(stepName, duration) {
2571
- await this.addStep(new LazySleepStep(stepName, duration));
2710
+ await this.addStep(new LazySleepStep(this, stepName, duration));
2572
2711
  }
2573
2712
  /**
2574
2713
  * Stops the execution until the date time provided.
@@ -2590,48 +2729,38 @@ var WorkflowContext = class {
2590
2729
  datetime = typeof datetime === "string" ? new Date(datetime) : datetime;
2591
2730
  time = Math.round(datetime.getTime() / 1e3);
2592
2731
  }
2593
- await this.addStep(new LazySleepUntilStep(stepName, time));
2732
+ await this.addStep(new LazySleepUntilStep(this, stepName, time));
2594
2733
  }
2595
2734
  async call(stepName, settings) {
2596
2735
  let callStep;
2597
2736
  if ("workflow" in settings) {
2598
2737
  const url = getNewUrlFromWorkflowId(this.url, settings.workflow.workflowId);
2599
- callStep = new LazyCallStep(
2738
+ const stringBody = typeof settings.body === "string" ? settings.body : settings.body === void 0 ? void 0 : JSON.stringify(settings.body);
2739
+ callStep = new LazyCallStep({
2740
+ context: this,
2600
2741
  stepName,
2601
2742
  url,
2602
- "POST",
2603
- settings.body,
2604
- settings.headers || {},
2605
- settings.retries || 0,
2606
- settings.retryDelay,
2607
- settings.timeout,
2608
- settings.flowControl ?? settings.workflow.options.flowControl,
2609
- settings.stringifyBody ?? true
2610
- );
2743
+ method: "POST",
2744
+ body: stringBody,
2745
+ headers: settings.headers || {},
2746
+ retries: settings.retries || 0,
2747
+ retryDelay: settings.retryDelay,
2748
+ timeout: settings.timeout,
2749
+ flowControl: settings.flowControl
2750
+ });
2611
2751
  } else {
2612
- const {
2613
- url,
2614
- method = "GET",
2615
- body,
2616
- headers = {},
2617
- retries = 0,
2618
- retryDelay,
2619
- timeout,
2620
- flowControl,
2621
- stringifyBody = true
2622
- } = settings;
2623
- callStep = new LazyCallStep(
2752
+ callStep = new LazyCallStep({
2753
+ context: this,
2624
2754
  stepName,
2625
- url,
2626
- method,
2627
- body,
2628
- headers,
2629
- retries,
2630
- retryDelay,
2631
- timeout,
2632
- flowControl,
2633
- stringifyBody
2634
- );
2755
+ url: settings.url,
2756
+ method: settings.method ?? "GET",
2757
+ body: settings.body,
2758
+ headers: settings.headers ?? {},
2759
+ retries: settings.retries ?? 0,
2760
+ retryDelay: settings.retryDelay,
2761
+ timeout: settings.timeout,
2762
+ flowControl: settings.flowControl
2763
+ });
2635
2764
  }
2636
2765
  return await this.addStep(callStep);
2637
2766
  }
@@ -2672,7 +2801,9 @@ var WorkflowContext = class {
2672
2801
  async waitForEvent(stepName, eventId, options = {}) {
2673
2802
  const { timeout = "7d" } = options;
2674
2803
  const timeoutStr = typeof timeout === "string" ? timeout : `${timeout}s`;
2675
- return await this.addStep(new LazyWaitForEventStep(stepName, eventId, timeoutStr));
2804
+ return await this.addStep(
2805
+ new LazyWaitForEventStep(this, stepName, eventId, timeoutStr)
2806
+ );
2676
2807
  }
2677
2808
  /**
2678
2809
  * Notify workflow runs waiting for an event
@@ -2697,20 +2828,28 @@ var WorkflowContext = class {
2697
2828
  */
2698
2829
  async notify(stepName, eventId, eventData) {
2699
2830
  return await this.addStep(
2700
- new LazyNotifyStep(stepName, eventId, eventData, this.qstashClient.http)
2831
+ new LazyNotifyStep(this, stepName, eventId, eventData, this.qstashClient.http)
2701
2832
  );
2702
2833
  }
2703
2834
  async invoke(stepName, settings) {
2704
- return await this.addStep(new LazyInvokeStep(stepName, settings));
2835
+ return await this.addStep(
2836
+ new LazyInvokeStep(this, stepName, settings)
2837
+ );
2838
+ }
2839
+ async createWebhook(stepName) {
2840
+ return await this.addStep(new LazyCreateWebhookStep(this, stepName));
2841
+ }
2842
+ async waitForWebhook(stepName, webhook, timeout) {
2843
+ return await this.addStep(new LazyWaitForWebhookStep(this, stepName, webhook, timeout));
2705
2844
  }
2706
2845
  /**
2707
2846
  * Cancel the current workflow run
2708
2847
  *
2709
- * Will throw WorkflowAbort to stop workflow execution.
2848
+ * Will throw WorkflowCancelAbort to stop workflow execution.
2710
2849
  * Shouldn't be inside try/catch.
2711
2850
  */
2712
2851
  async cancel() {
2713
- throw new WorkflowAbort("cancel", void 0, true);
2852
+ throw new WorkflowCancelAbort();
2714
2853
  }
2715
2854
  /**
2716
2855
  * Adds steps to the executor. Needed so that it can be overwritten in
@@ -2726,74 +2865,25 @@ var WorkflowContext = class {
2726
2865
  }
2727
2866
  };
2728
2867
 
2729
- // src/logger.ts
2730
- var LOG_LEVELS = ["DEBUG", "INFO", "SUBMIT", "WARN", "ERROR"];
2731
- var WorkflowLogger = class _WorkflowLogger {
2732
- logs = [];
2733
- options;
2734
- workflowRunId = void 0;
2735
- constructor(options) {
2736
- this.options = options;
2737
- }
2738
- async log(level, eventType, details) {
2739
- if (this.shouldLog(level)) {
2740
- const timestamp = Date.now();
2741
- const logEntry = {
2742
- timestamp,
2743
- workflowRunId: this.workflowRunId ?? "",
2744
- logLevel: level,
2745
- eventType,
2746
- details
2747
- };
2748
- this.logs.push(logEntry);
2749
- if (this.options.logOutput === "console") {
2750
- this.writeToConsole(logEntry);
2751
- }
2752
- await new Promise((resolve) => setTimeout(resolve, 100));
2753
- }
2754
- }
2755
- setWorkflowRunId(workflowRunId) {
2756
- this.workflowRunId = workflowRunId;
2757
- }
2758
- writeToConsole(logEntry) {
2759
- const JSON_SPACING = 2;
2760
- const logMethod = logEntry.logLevel === "ERROR" ? console.error : logEntry.logLevel === "WARN" ? console.warn : console.log;
2761
- logMethod(JSON.stringify(logEntry, void 0, JSON_SPACING));
2762
- }
2763
- shouldLog(level) {
2764
- return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
2765
- }
2766
- static getLogger(verbose) {
2767
- if (typeof verbose === "object") {
2768
- return verbose;
2769
- } else {
2770
- return verbose ? new _WorkflowLogger({
2771
- logLevel: "INFO",
2772
- logOutput: "console"
2773
- }) : void 0;
2774
- }
2775
- }
2776
- };
2777
-
2778
2868
  // src/serve/authorization.ts
2779
2869
  var import_qstash9 = require("@upstash/qstash");
2780
2870
  var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
2781
2871
  static disabledMessage = "disabled-qstash-worklfow-run";
2782
2872
  disabled = true;
2783
2873
  /**
2784
- * overwrite the WorkflowContext.addStep method to always raise WorkflowAbort
2874
+ * overwrite the WorkflowContext.addStep method to always raise WorkflowAuthError
2785
2875
  * error in order to stop the execution whenever we encounter a step.
2786
2876
  *
2787
2877
  * @param _step
2788
2878
  */
2789
2879
  async addStep(_step) {
2790
- throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
2880
+ throw new WorkflowAuthError(_DisabledWorkflowContext.disabledMessage);
2791
2881
  }
2792
2882
  /**
2793
- * overwrite cancel method to throw WorkflowAbort with the disabledMessage
2883
+ * overwrite cancel method to throw WorkflowAuthError with the disabledMessage
2794
2884
  */
2795
2885
  async cancel() {
2796
- throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
2886
+ throw new WorkflowAuthError(_DisabledWorkflowContext.disabledMessage);
2797
2887
  }
2798
2888
  /**
2799
2889
  * copies the passed context to create a DisabledWorkflowContext. Then, runs the
@@ -2816,18 +2906,14 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
2816
2906
  headers: context.headers,
2817
2907
  steps: [],
2818
2908
  url: context.url,
2819
- failureUrl: context.failureUrl,
2820
2909
  initialPayload: context.requestPayload,
2821
2910
  env: context.env,
2822
- retries: context.retries,
2823
- retryDelay: context.retryDelay,
2824
- flowControl: context.flowControl,
2825
2911
  label: context.label
2826
2912
  });
2827
2913
  try {
2828
2914
  await routeFunction(disabledContext);
2829
2915
  } catch (error) {
2830
- if (isInstanceOf(error, WorkflowAbort) && error.stepName === this.disabledMessage || isInstanceOf(error, WorkflowNonRetryableError) || isInstanceOf(error, WorkflowRetryAfterError)) {
2916
+ if (isInstanceOf(error, WorkflowAuthError) && error.stepName === this.disabledMessage || isInstanceOf(error, WorkflowNonRetryableError) || isInstanceOf(error, WorkflowRetryAfterError)) {
2831
2917
  return ok("step-found");
2832
2918
  }
2833
2919
  console.warn(
@@ -2860,13 +2946,6 @@ var processRawSteps = (rawSteps) => {
2860
2946
  const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
2861
2947
  const otherSteps = stepsToDecode.map((rawStep) => {
2862
2948
  const step = JSON.parse(decodeBase64(rawStep.body));
2863
- if (step.waitEventId) {
2864
- const newOut = {
2865
- eventData: step.out ? decodeBase64(step.out) : void 0,
2866
- timeout: step.waitTimeout ?? false
2867
- };
2868
- step.out = newOut;
2869
- }
2870
2949
  return step;
2871
2950
  });
2872
2951
  const steps = [initialStep, ...otherSteps];
@@ -2894,7 +2973,7 @@ var deduplicateSteps = (steps) => {
2894
2973
  }
2895
2974
  return deduplicatedSteps;
2896
2975
  };
2897
- var checkIfLastOneIsDuplicate = async (steps, debug) => {
2976
+ var checkIfLastOneIsDuplicate = async (steps, dispatchDebug) => {
2898
2977
  if (steps.length < 2) {
2899
2978
  return false;
2900
2979
  }
@@ -2905,14 +2984,41 @@ var checkIfLastOneIsDuplicate = async (steps, debug) => {
2905
2984
  const step = steps[index];
2906
2985
  if (step.stepId === lastStepId && step.targetStep === lastTargetStepId) {
2907
2986
  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.`;
2908
- await debug?.log("WARN", "RESPONSE_DEFAULT", message);
2909
- console.warn(message);
2987
+ await dispatchDebug?.("onWarning", {
2988
+ warning: message
2989
+ });
2910
2990
  return true;
2911
2991
  }
2912
2992
  }
2913
2993
  return false;
2914
2994
  };
2915
2995
  var validateRequest = (request) => {
2996
+ if (request.headers.get(WORKFLOW_UNKOWN_SDK_VERSION_HEADER)) {
2997
+ const workflowRunId2 = request.headers.get(WORKFLOW_ID_HEADER);
2998
+ if (!workflowRunId2) {
2999
+ throw new WorkflowError(
3000
+ "Couldn't get workflow id from header when handling unknown sdk request"
3001
+ );
3002
+ }
3003
+ return {
3004
+ unknownSdk: true,
3005
+ isFirstInvocation: true,
3006
+ workflowRunId: workflowRunId2
3007
+ };
3008
+ }
3009
+ if (request.headers.get(WORKFLOW_FAILURE_CALLBACK_HEADER)) {
3010
+ const workflowRunId2 = request.headers.get(WORKFLOW_ID_HEADER);
3011
+ if (!workflowRunId2) {
3012
+ throw new WorkflowError(
3013
+ "Couldn't get workflow id from header when handling failure callback request"
3014
+ );
3015
+ }
3016
+ return {
3017
+ unknownSdk: false,
3018
+ isFirstInvocation: true,
3019
+ workflowRunId: workflowRunId2
3020
+ };
3021
+ }
2916
3022
  const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
2917
3023
  const isFirstInvocation = !versionHeader;
2918
3024
  if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
@@ -2926,11 +3032,20 @@ var validateRequest = (request) => {
2926
3032
  }
2927
3033
  return {
2928
3034
  isFirstInvocation,
2929
- workflowRunId
3035
+ workflowRunId,
3036
+ unknownSdk: false
2930
3037
  };
2931
3038
  };
2932
- var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requester, messageId, debug) => {
2933
- if (isFirstInvocation) {
3039
+ var parseRequest = async ({
3040
+ requestPayload,
3041
+ isFirstInvocation,
3042
+ unknownSdk,
3043
+ workflowRunId,
3044
+ requester,
3045
+ messageId,
3046
+ dispatchDebug
3047
+ }) => {
3048
+ if (isFirstInvocation && !unknownSdk) {
2934
3049
  return {
2935
3050
  rawInitialPayload: requestPayload ?? "",
2936
3051
  steps: [],
@@ -2940,16 +3055,14 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
2940
3055
  } else {
2941
3056
  let rawSteps;
2942
3057
  if (!requestPayload) {
2943
- await debug?.log(
2944
- "INFO",
2945
- "ENDPOINT_START",
2946
- "request payload is empty, steps will be fetched from QStash."
2947
- );
3058
+ await dispatchDebug?.("onInfo", {
3059
+ info: "request payload is empty, steps will be fetched from QStash."
3060
+ });
2948
3061
  const { steps: fetchedSteps, workflowRunEnded } = await getSteps(
2949
3062
  requester,
2950
3063
  workflowRunId,
2951
3064
  messageId,
2952
- debug
3065
+ dispatchDebug
2953
3066
  );
2954
3067
  if (workflowRunEnded) {
2955
3068
  return {
@@ -2964,7 +3077,7 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
2964
3077
  rawSteps = JSON.parse(requestPayload);
2965
3078
  }
2966
3079
  const { rawInitialPayload, steps } = processRawSteps(rawSteps);
2967
- const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
3080
+ const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, dispatchDebug);
2968
3081
  const deduplicatedSteps = deduplicateSteps(steps);
2969
3082
  return {
2970
3083
  rawInitialPayload,
@@ -2974,16 +3087,21 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
2974
3087
  };
2975
3088
  }
2976
3089
  };
2977
- var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, routeFunction, failureFunction, env, retries, retryDelay, flowControl, debug) => {
2978
- if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
3090
+ var handleFailure = async ({
3091
+ request,
3092
+ requestPayload,
3093
+ qstashClient,
3094
+ initialPayloadParser,
3095
+ routeFunction,
3096
+ failureFunction,
3097
+ env,
3098
+ dispatchDebug
3099
+ }) => {
3100
+ if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true" && !request.headers.get(WORKFLOW_FAILURE_CALLBACK_HEADER)) {
2979
3101
  return ok({ result: "not-failure-callback" });
2980
3102
  }
2981
3103
  if (!failureFunction) {
2982
- return err(
2983
- new WorkflowError(
2984
- "Workflow endpoint is called to handle a failure, but a failureFunction is not provided in serve options. Either provide a failureUrl or a failureFunction."
2985
- )
2986
- );
3104
+ return ok({ result: "failure-function-undefined" });
2987
3105
  }
2988
3106
  try {
2989
3107
  const { status, header, body, url, sourceBody, workflowRunId } = JSON.parse(requestPayload);
@@ -3011,23 +3129,21 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
3011
3129
  headers: userHeaders,
3012
3130
  steps: [],
3013
3131
  url,
3014
- failureUrl: url,
3015
- debug,
3016
3132
  env,
3017
- retries,
3018
- retryDelay,
3019
- flowControl,
3020
3133
  telemetry: void 0,
3021
3134
  // not going to make requests in authentication check
3022
- label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0
3135
+ label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3136
+ middlewareManager: void 0
3023
3137
  });
3024
3138
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3025
3139
  routeFunction,
3026
3140
  workflowContext
3027
3141
  );
3028
3142
  if (authCheck.isErr()) {
3029
- await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
3030
- throw authCheck.error;
3143
+ await dispatchDebug?.("onError", {
3144
+ error: authCheck.error
3145
+ });
3146
+ return err(authCheck.error);
3031
3147
  } else if (authCheck.value === "run-ended") {
3032
3148
  return err(new WorkflowError("Not authorized to run the failure function."));
3033
3149
  }
@@ -3038,7 +3154,7 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
3038
3154
  failHeaders: header,
3039
3155
  failStack
3040
3156
  });
3041
- return ok({ result: "is-failure-callback", response: failureResponse });
3157
+ return ok({ result: "failure-function-executed", response: failureResponse });
3042
3158
  } catch (error) {
3043
3159
  return err(error);
3044
3160
  }
@@ -3047,7 +3163,143 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
3047
3163
  // src/serve/options.ts
3048
3164
  var import_qstash10 = require("@upstash/qstash");
3049
3165
  var import_qstash11 = require("@upstash/qstash");
3050
- var processOptions = (options) => {
3166
+
3167
+ // src/middleware/middleware.ts
3168
+ var WorkflowMiddleware = class {
3169
+ name;
3170
+ initCallbacks;
3171
+ /**
3172
+ * Callback functions
3173
+ *
3174
+ * Initially set to undefined, will be populated after init is called
3175
+ */
3176
+ middlewareCallbacks = void 0;
3177
+ constructor(parameters) {
3178
+ this.name = parameters.name;
3179
+ if ("init" in parameters) {
3180
+ this.initCallbacks = parameters.init;
3181
+ } else {
3182
+ this.middlewareCallbacks = parameters.callbacks;
3183
+ }
3184
+ }
3185
+ async ensureInit() {
3186
+ if (!this.middlewareCallbacks) {
3187
+ if (!this.initCallbacks) {
3188
+ throw new WorkflowError(`Middleware "${this.name}" has no callbacks or init defined.`);
3189
+ }
3190
+ this.middlewareCallbacks = await this.initCallbacks();
3191
+ }
3192
+ }
3193
+ /**
3194
+ * Gets a callback function by name.
3195
+ *
3196
+ * @param callback name of the callback to retrieve
3197
+ */
3198
+ getCallback(callback) {
3199
+ return this.middlewareCallbacks?.[callback];
3200
+ }
3201
+ };
3202
+
3203
+ // src/middleware/logging.ts
3204
+ var loggingMiddleware = new WorkflowMiddleware({
3205
+ name: "logging",
3206
+ callbacks: {
3207
+ afterExecution(params) {
3208
+ const { context, ...rest } = params;
3209
+ console.log(" [Upstash Workflow]: Step executed:", {
3210
+ workflowRunId: context.workflowRunId,
3211
+ ...rest
3212
+ });
3213
+ },
3214
+ beforeExecution(params) {
3215
+ const { context, ...rest } = params;
3216
+ console.log(" [Upstash Workflow]: Step execution started:", {
3217
+ workflowRunId: context.workflowRunId,
3218
+ ...rest
3219
+ });
3220
+ },
3221
+ runStarted(params) {
3222
+ const { context, ...rest } = params;
3223
+ console.log(" [Upstash Workflow]: Workflow run started:", {
3224
+ workflowRunId: context.workflowRunId,
3225
+ ...rest
3226
+ });
3227
+ },
3228
+ runCompleted(params) {
3229
+ const { context, ...rest } = params;
3230
+ console.log(" [Upstash Workflow]: Workflow run completed:", {
3231
+ workflowRunId: context.workflowRunId,
3232
+ ...rest
3233
+ });
3234
+ },
3235
+ onError: onErrorWithConsole,
3236
+ onWarning: onWarningWithConsole,
3237
+ onInfo: onInfoWithConsole
3238
+ }
3239
+ });
3240
+
3241
+ // src/serve/options.ts
3242
+ var createResponseData = (workflowRunId, detailedFinishCondition) => {
3243
+ const baseHeaders = {
3244
+ [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
3245
+ "Upstash-workflow-sdk": VERSION
3246
+ };
3247
+ if (detailedFinishCondition?.condition === "auth-fail") {
3248
+ return {
3249
+ text: JSON.stringify({
3250
+ message: AUTH_FAIL_MESSAGE,
3251
+ workflowRunId
3252
+ }),
3253
+ status: 400,
3254
+ headers: baseHeaders
3255
+ };
3256
+ } else if (detailedFinishCondition?.condition === "non-retryable-error") {
3257
+ return {
3258
+ text: JSON.stringify(formatWorkflowError(detailedFinishCondition.result)),
3259
+ status: 489,
3260
+ headers: {
3261
+ ...baseHeaders,
3262
+ "Upstash-NonRetryable-Error": "true"
3263
+ }
3264
+ };
3265
+ } else if (detailedFinishCondition?.condition === "retry-after-error") {
3266
+ return {
3267
+ text: JSON.stringify(formatWorkflowError(detailedFinishCondition.result)),
3268
+ status: 429,
3269
+ headers: {
3270
+ ...baseHeaders,
3271
+ "Retry-After": detailedFinishCondition.result.retryAfter.toString()
3272
+ }
3273
+ };
3274
+ } else if (detailedFinishCondition?.condition === "failure-callback-executed") {
3275
+ return {
3276
+ text: JSON.stringify({ result: detailedFinishCondition.result ?? void 0 }),
3277
+ status: 200,
3278
+ headers: baseHeaders
3279
+ };
3280
+ } else if (detailedFinishCondition?.condition === "failure-callback-undefined") {
3281
+ return {
3282
+ text: JSON.stringify({
3283
+ workflowRunId,
3284
+ finishCondition: detailedFinishCondition.condition
3285
+ }),
3286
+ status: 200,
3287
+ headers: {
3288
+ ...baseHeaders,
3289
+ "Upstash-Workflow-Failure-Callback-Notfound": "true"
3290
+ }
3291
+ };
3292
+ }
3293
+ return {
3294
+ text: JSON.stringify({
3295
+ workflowRunId,
3296
+ finishCondition: detailedFinishCondition.condition
3297
+ }),
3298
+ status: 200,
3299
+ headers: baseHeaders
3300
+ };
3301
+ };
3302
+ var processOptions = (options, internalOptions) => {
3051
3303
  const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
3052
3304
  const receiverEnvironmentVariablesSet = Boolean(
3053
3305
  environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
@@ -3057,55 +3309,6 @@ var processOptions = (options) => {
3057
3309
  baseUrl: environment.QSTASH_URL,
3058
3310
  token: environment.QSTASH_TOKEN
3059
3311
  }),
3060
- onStepFinish: (workflowRunId, _finishCondition, detailedFinishCondition) => {
3061
- if (detailedFinishCondition?.condition === "auth-fail") {
3062
- console.error(AUTH_FAIL_MESSAGE);
3063
- return new Response(
3064
- JSON.stringify({
3065
- message: AUTH_FAIL_MESSAGE,
3066
- workflowRunId
3067
- }),
3068
- {
3069
- status: 400,
3070
- headers: {
3071
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3072
- }
3073
- }
3074
- );
3075
- } else if (detailedFinishCondition?.condition === "non-retryable-error") {
3076
- return new Response(JSON.stringify(formatWorkflowError(detailedFinishCondition.result)), {
3077
- headers: {
3078
- "Upstash-NonRetryable-Error": "true",
3079
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3080
- },
3081
- status: 489
3082
- });
3083
- } else if (detailedFinishCondition?.condition === "retry-after-error") {
3084
- return new Response(JSON.stringify(formatWorkflowError(detailedFinishCondition.result)), {
3085
- headers: {
3086
- "Retry-After": detailedFinishCondition.result.retryAfter.toString(),
3087
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3088
- },
3089
- status: 429
3090
- });
3091
- } else if (detailedFinishCondition?.condition === "failure-callback") {
3092
- return new Response(
3093
- JSON.stringify({ result: detailedFinishCondition.result ?? void 0 }),
3094
- {
3095
- status: 200,
3096
- headers: {
3097
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3098
- }
3099
- }
3100
- );
3101
- }
3102
- return new Response(JSON.stringify({ workflowRunId }), {
3103
- status: 200,
3104
- headers: {
3105
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3106
- }
3107
- });
3108
- },
3109
3312
  initialPayloadParser: (initialRequest) => {
3110
3313
  if (!initialRequest) {
3111
3314
  return void 0;
@@ -3126,29 +3329,34 @@ var processOptions = (options) => {
3126
3329
  }) : void 0,
3127
3330
  baseUrl: environment.UPSTASH_WORKFLOW_URL,
3128
3331
  env: environment,
3129
- retries: DEFAULT_RETRIES,
3130
- useJSONContent: false,
3131
3332
  disableTelemetry: false,
3132
- onError: console.error,
3133
- ...options
3333
+ ...options,
3334
+ // merge middlewares
3335
+ middlewares: [options?.middlewares ?? [], options?.verbose ? [loggingMiddleware] : []].flat(),
3336
+ internal: {
3337
+ generateResponse: internalOptions?.generateResponse ?? ((responseData) => {
3338
+ return new Response(responseData.text, {
3339
+ status: responseData.status,
3340
+ headers: responseData.headers
3341
+ });
3342
+ }),
3343
+ useJSONContent: internalOptions?.useJSONContent ?? false
3344
+ }
3134
3345
  };
3135
3346
  };
3136
- var determineUrls = async (request, url, baseUrl, failureFunction, failureUrl, debug) => {
3347
+ var determineUrls = async (request, url, baseUrl, dispatchDebug) => {
3137
3348
  const initialWorkflowUrl = url ?? request.url;
3138
3349
  const workflowUrl = baseUrl ? initialWorkflowUrl.replace(/^(https?:\/\/[^/]+)(\/.*)?$/, (_, matchedBaseUrl, path) => {
3139
3350
  return baseUrl + (path || "");
3140
3351
  }) : initialWorkflowUrl;
3141
3352
  if (workflowUrl !== initialWorkflowUrl) {
3142
- await debug?.log("WARN", "ENDPOINT_START", {
3143
- warning: `Upstash Workflow: replacing the base of the url with "${baseUrl}" and using it as workflow endpoint.`,
3144
- originalURL: initialWorkflowUrl,
3145
- updatedURL: workflowUrl
3353
+ await dispatchDebug("onInfo", {
3354
+ info: `The workflow URL's base URL has been replaced with the provided baseUrl. Original URL: ${initialWorkflowUrl}, New URL: ${workflowUrl}`
3146
3355
  });
3147
3356
  }
3148
- const workflowFailureUrl = failureFunction ? workflowUrl : failureUrl;
3149
3357
  if (workflowUrl.includes("localhost")) {
3150
- await debug?.log("WARN", "ENDPOINT_START", {
3151
- 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}`
3358
+ await dispatchDebug("onInfo", {
3359
+ 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}`
3152
3360
  });
3153
3361
  }
3154
3362
  if (!(workflowUrl.startsWith("http://") || workflowUrl.startsWith("https://"))) {
@@ -3157,67 +3365,68 @@ var determineUrls = async (request, url, baseUrl, failureFunction, failureUrl, d
3157
3365
  );
3158
3366
  }
3159
3367
  return {
3160
- workflowUrl,
3161
- workflowFailureUrl
3368
+ workflowUrl
3162
3369
  };
3163
3370
  };
3164
3371
  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`;
3165
3372
 
3166
3373
  // src/serve/index.ts
3167
- var serveBase = (routeFunction, telemetry, options) => {
3374
+ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
3168
3375
  const {
3169
3376
  qstashClient,
3170
- onStepFinish,
3171
3377
  initialPayloadParser,
3172
3378
  url,
3173
- verbose,
3174
3379
  receiver,
3175
- failureUrl,
3176
3380
  failureFunction,
3177
3381
  baseUrl,
3178
3382
  env,
3179
- retries,
3180
- retryDelay,
3181
- useJSONContent,
3182
3383
  disableTelemetry,
3183
- flowControl,
3184
- onError
3185
- } = processOptions(options);
3384
+ middlewares,
3385
+ internal
3386
+ } = processOptions(options, internalOptions);
3186
3387
  telemetry = disableTelemetry ? void 0 : telemetry;
3187
- const debug = WorkflowLogger.getLogger(verbose);
3188
- const handler = async (request) => {
3189
- await debug?.log("INFO", "ENDPOINT_START");
3190
- const { workflowUrl, workflowFailureUrl } = await determineUrls(
3388
+ const { generateResponse: responseGenerator, useJSONContent } = internal;
3389
+ const handler = async (request, middlewareManager) => {
3390
+ await middlewareManager.dispatchDebug("onInfo", {
3391
+ info: `Received request for workflow execution.`
3392
+ });
3393
+ const { workflowUrl } = await determineUrls(
3191
3394
  request,
3192
3395
  url,
3193
3396
  baseUrl,
3194
- failureFunction,
3195
- failureUrl,
3196
- debug
3397
+ middlewareManager.dispatchDebug.bind(middlewareManager)
3197
3398
  );
3198
3399
  const requestPayload = await getPayload(request) ?? "";
3199
3400
  await verifyRequest(requestPayload, request.headers.get("upstash-signature"), receiver);
3200
- const { isFirstInvocation, workflowRunId } = validateRequest(request);
3201
- debug?.setWorkflowRunId(workflowRunId);
3202
- const { rawInitialPayload, steps, isLastDuplicate, workflowRunEnded } = await parseRequest(
3401
+ const { isFirstInvocation, workflowRunId, unknownSdk } = validateRequest(request);
3402
+ middlewareManager.assignWorkflowRunId(workflowRunId);
3403
+ await middlewareManager.dispatchDebug("onInfo", {
3404
+ info: `Run id identified. isFirstInvocation: ${isFirstInvocation}, unknownSdk: ${unknownSdk}`
3405
+ });
3406
+ const { rawInitialPayload, steps, isLastDuplicate, workflowRunEnded } = await parseRequest({
3203
3407
  requestPayload,
3204
3408
  isFirstInvocation,
3409
+ unknownSdk,
3205
3410
  workflowRunId,
3206
- qstashClient.http,
3207
- request.headers.get("upstash-message-id"),
3208
- debug
3209
- );
3411
+ requester: qstashClient.http,
3412
+ messageId: request.headers.get("upstash-message-id"),
3413
+ dispatchDebug: middlewareManager.dispatchDebug.bind(middlewareManager)
3414
+ });
3210
3415
  if (workflowRunEnded) {
3211
- return onStepFinish(workflowRunId, "workflow-already-ended", {
3212
- condition: "workflow-already-ended"
3213
- });
3416
+ return responseGenerator(
3417
+ createResponseData(workflowRunId, {
3418
+ condition: "workflow-already-ended"
3419
+ })
3420
+ );
3214
3421
  }
3215
3422
  if (isLastDuplicate) {
3216
- return onStepFinish(workflowRunId, "duplicate-step", {
3217
- condition: "duplicate-step"
3218
- });
3423
+ return responseGenerator(
3424
+ createResponseData(workflowRunId, {
3425
+ condition: "duplicate-step"
3426
+ })
3427
+ );
3219
3428
  }
3220
- const failureCheck = await handleFailure(
3429
+ const failureCheck = await handleFailure({
3221
3430
  request,
3222
3431
  requestPayload,
3223
3432
  qstashClient,
@@ -3225,19 +3434,29 @@ var serveBase = (routeFunction, telemetry, options) => {
3225
3434
  routeFunction,
3226
3435
  failureFunction,
3227
3436
  env,
3228
- retries,
3229
- retryDelay,
3230
- flowControl,
3231
- debug
3232
- );
3437
+ dispatchDebug: middlewareManager.dispatchDebug.bind(middlewareManager)
3438
+ });
3233
3439
  if (failureCheck.isErr()) {
3234
3440
  throw failureCheck.error;
3235
- } else if (failureCheck.value.result === "is-failure-callback") {
3236
- await debug?.log("WARN", "RESPONSE_DEFAULT", "failureFunction executed");
3237
- return onStepFinish(workflowRunId, "failure-callback", {
3238
- condition: "failure-callback",
3239
- result: failureCheck.value.response
3441
+ } else if (failureCheck.value.result === "failure-function-executed") {
3442
+ await middlewareManager.dispatchDebug("onInfo", {
3443
+ info: `Handled failure callback.`
3444
+ });
3445
+ return responseGenerator(
3446
+ createResponseData(workflowRunId, {
3447
+ condition: "failure-callback-executed",
3448
+ result: failureCheck.value.response
3449
+ })
3450
+ );
3451
+ } else if (failureCheck.value.result === "failure-function-undefined") {
3452
+ await middlewareManager.dispatchDebug("onInfo", {
3453
+ info: `Failure callback invoked but no failure function defined.`
3240
3454
  });
3455
+ return responseGenerator(
3456
+ createResponseData(workflowRunId, {
3457
+ condition: "failure-callback-undefined"
3458
+ })
3459
+ );
3241
3460
  }
3242
3461
  const invokeCount = Number(request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER) ?? "0");
3243
3462
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
@@ -3248,29 +3467,26 @@ var serveBase = (routeFunction, telemetry, options) => {
3248
3467
  headers: recreateUserHeaders(request.headers),
3249
3468
  steps,
3250
3469
  url: workflowUrl,
3251
- failureUrl: workflowFailureUrl,
3252
- debug,
3253
3470
  env,
3254
- retries,
3255
- retryDelay,
3256
3471
  telemetry,
3257
3472
  invokeCount,
3258
- flowControl,
3259
- label
3473
+ label,
3474
+ middlewareManager
3260
3475
  });
3261
3476
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3262
3477
  routeFunction,
3263
3478
  workflowContext
3264
3479
  );
3265
3480
  if (authCheck.isErr()) {
3266
- await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
3267
3481
  throw authCheck.error;
3268
3482
  } else if (authCheck.value === "run-ended") {
3269
- await debug?.log("ERROR", "ERROR", { error: AUTH_FAIL_MESSAGE });
3270
- return onStepFinish(
3271
- isFirstInvocation ? "no-workflow-id" : workflowContext.workflowRunId,
3272
- "auth-fail",
3273
- { condition: "auth-fail" }
3483
+ await middlewareManager.dispatchDebug("onError", {
3484
+ error: new Error(AUTH_FAIL_MESSAGE)
3485
+ });
3486
+ return responseGenerator(
3487
+ createResponseData(isFirstInvocation ? "no-workflow-id" : workflowContext.workflowRunId, {
3488
+ condition: "auth-fail"
3489
+ })
3274
3490
  );
3275
3491
  }
3276
3492
  const callReturnCheck = await handleThirdPartyCallResult({
@@ -3278,84 +3494,91 @@ var serveBase = (routeFunction, telemetry, options) => {
3278
3494
  requestPayload: rawInitialPayload,
3279
3495
  client: qstashClient,
3280
3496
  workflowUrl,
3281
- failureUrl: workflowFailureUrl,
3282
- retries,
3283
- retryDelay,
3284
- flowControl,
3285
3497
  telemetry,
3286
- debug
3498
+ middlewareManager
3287
3499
  });
3288
3500
  if (callReturnCheck.isErr()) {
3289
- await debug?.log("ERROR", "SUBMIT_THIRD_PARTY_RESULT", {
3290
- error: callReturnCheck.error.message
3291
- });
3292
3501
  throw callReturnCheck.error;
3293
3502
  } else if (callReturnCheck.value === "continue-workflow") {
3294
3503
  const result = isFirstInvocation ? await triggerFirstInvocation({
3295
3504
  workflowContext,
3296
3505
  useJSONContent,
3297
3506
  telemetry,
3298
- debug,
3299
- invokeCount
3507
+ invokeCount,
3508
+ middlewareManager,
3509
+ unknownSdk
3300
3510
  }) : await triggerRouteFunction({
3301
- onStep: async () => routeFunction(workflowContext),
3511
+ onStep: async () => {
3512
+ if (steps.length === 1) {
3513
+ await middlewareManager.dispatchLifecycle("runStarted", {});
3514
+ }
3515
+ return await routeFunction(workflowContext);
3516
+ },
3302
3517
  onCleanup: async (result2) => {
3303
- await triggerWorkflowDelete(workflowContext, result2, debug);
3518
+ await middlewareManager.dispatchLifecycle("runCompleted", {
3519
+ result: result2
3520
+ });
3521
+ await triggerWorkflowDelete(
3522
+ workflowContext,
3523
+ result2,
3524
+ false,
3525
+ middlewareManager.dispatchDebug.bind(middlewareManager)
3526
+ );
3304
3527
  },
3305
3528
  onCancel: async () => {
3306
3529
  await makeCancelRequest(workflowContext.qstashClient.http, workflowRunId);
3307
3530
  },
3308
- debug
3531
+ middlewareManager
3309
3532
  });
3310
3533
  if (result.isOk() && isInstanceOf(result.value, WorkflowNonRetryableError)) {
3311
- return onStepFinish(workflowRunId, result.value, {
3312
- condition: "non-retryable-error",
3313
- result: result.value
3314
- });
3534
+ return responseGenerator(
3535
+ createResponseData(workflowRunId, {
3536
+ condition: "non-retryable-error",
3537
+ result: result.value
3538
+ })
3539
+ );
3315
3540
  }
3316
3541
  if (result.isOk() && isInstanceOf(result.value, WorkflowRetryAfterError)) {
3317
- return onStepFinish(workflowRunId, result.value, {
3318
- condition: "retry-after-error",
3319
- result: result.value
3320
- });
3542
+ return responseGenerator(
3543
+ createResponseData(workflowRunId, {
3544
+ condition: "retry-after-error",
3545
+ result: result.value
3546
+ })
3547
+ );
3321
3548
  }
3322
3549
  if (result.isErr()) {
3323
- await debug?.log("ERROR", "ERROR", { error: result.error.message });
3324
3550
  throw result.error;
3325
3551
  }
3326
- await debug?.log("INFO", "RESPONSE_WORKFLOW");
3327
- return onStepFinish(workflowContext.workflowRunId, "success", {
3328
- condition: "success"
3552
+ await middlewareManager.dispatchDebug("onInfo", {
3553
+ info: `Workflow endpoint execution completed successfully.`
3329
3554
  });
3555
+ return responseGenerator(
3556
+ createResponseData(workflowContext.workflowRunId, {
3557
+ condition: "success"
3558
+ })
3559
+ );
3330
3560
  } else if (callReturnCheck.value === "workflow-ended") {
3331
- return onStepFinish(workflowContext.workflowRunId, "workflow-already-ended", {
3332
- condition: "workflow-already-ended"
3333
- });
3561
+ return responseGenerator(
3562
+ createResponseData(workflowContext.workflowRunId, {
3563
+ condition: "workflow-already-ended"
3564
+ })
3565
+ );
3334
3566
  }
3335
- await debug?.log("INFO", "RESPONSE_DEFAULT");
3336
- return onStepFinish("no-workflow-id", "fromCallback", {
3337
- condition: "fromCallback"
3338
- });
3567
+ return responseGenerator(
3568
+ createResponseData(workflowContext.workflowRunId, {
3569
+ condition: "fromCallback"
3570
+ })
3571
+ );
3339
3572
  };
3340
3573
  const safeHandler = async (request) => {
3574
+ const middlewareManager = new MiddlewareManager(middlewares);
3341
3575
  try {
3342
- return await handler(request);
3576
+ return await handler(request, middlewareManager);
3343
3577
  } catch (error) {
3344
3578
  const formattedError = formatWorkflowError(error);
3345
- try {
3346
- onError?.(error);
3347
- } catch (onErrorError) {
3348
- const formattedOnErrorError = formatWorkflowError(onErrorError);
3349
- const errorMessage = `Error while running onError callback: '${formattedOnErrorError.message}'.
3350
- Original error: '${formattedError.message}'`;
3351
- console.error(errorMessage);
3352
- return new Response(JSON.stringify({ error: errorMessage }), {
3353
- status: 500,
3354
- headers: {
3355
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3356
- }
3357
- });
3358
- }
3579
+ await middlewareManager.dispatchDebug("onError", {
3580
+ error: isInstanceOf(error, Error) ? error : new Error(formattedError.message)
3581
+ });
3359
3582
  return new Response(JSON.stringify(formattedError), {
3360
3583
  status: 500,
3361
3584
  headers: {
@@ -3388,15 +3611,19 @@ var DLQ = class _DLQ {
3388
3611
  /**
3389
3612
  * list the items in the DLQ
3390
3613
  *
3391
- * @param cursor - Optional cursor for pagination.
3392
- * @param count - Optional number of items to return.
3393
- * @param filter - Optional filter options to apply to the DLQ items.
3614
+ * @param parameters - Optional parameters object
3615
+ * @param parameters.cursor - Optional cursor for pagination
3616
+ * @param parameters.count - Optional number of items to return
3617
+ * @param parameters.filter - Optional filter options to apply to the DLQ items.
3394
3618
  * The available filter options are:
3395
3619
  * - `fromDate`: Filter items which entered the DLQ after this date.
3396
3620
  * - `toDate`: Filter items which entered the DLQ before this date.
3397
3621
  * - `url`: Filter items by the URL they were sent to.
3398
3622
  * - `responseStatus`: Filter items by the response status code.
3399
- * @returns
3623
+ * - `workflowRunId`: Filter items by workflow run ID.
3624
+ * - `workflowCreatedAt`: Filter items by workflow creation time.
3625
+ * - `failureFunctionState`: Filter items by failure callback state.
3626
+ * - `label`: Filter items by label.
3400
3627
  */
3401
3628
  async list(parameters) {
3402
3629
  const { cursor, count, filter } = parameters || {};
@@ -3439,8 +3666,8 @@ var DLQ = class _DLQ {
3439
3666
  * Retry the failure callback of a workflow run whose failureUrl/failureFunction
3440
3667
  * request has failed.
3441
3668
  *
3442
- * @param dlqId - The ID of the DLQ message to retry.
3443
- * @returns
3669
+ * @param dlqId - The ID of the DLQ message to retry
3670
+ * @returns response with workflow run information
3444
3671
  */
3445
3672
  async retryFailureFunction({ dlqId }) {
3446
3673
  const response = await this.client.http.request({
@@ -3449,6 +3676,11 @@ var DLQ = class _DLQ {
3449
3676
  });
3450
3677
  return response;
3451
3678
  }
3679
+ /**
3680
+ * Handles DLQ options and prepares headers and query parameters.
3681
+ *
3682
+ * @param options - DLQ resume/restart options
3683
+ */
3452
3684
  static handleDLQOptions(options) {
3453
3685
  const { dlqId, flowControl, retries } = options;
3454
3686
  const headers = {};
@@ -3465,6 +3697,11 @@ var DLQ = class _DLQ {
3465
3697
  headers
3466
3698
  };
3467
3699
  }
3700
+ /**
3701
+ * Converts DLQ ID(s) to query parameter string.
3702
+ *
3703
+ * @param dlqId - Single DLQ ID or array of DLQ IDs
3704
+ */
3468
3705
  static getDlqIdQueryParameter(dlqId) {
3469
3706
  const dlqIds = Array.isArray(dlqId) ? dlqId : [dlqId];
3470
3707
  const paramsArray = dlqIds.map((id) => ["dlqIds", id]);
@@ -3603,7 +3840,7 @@ var Client4 = class {
3603
3840
  const isBatchInput = Array.isArray(params);
3604
3841
  const options = isBatchInput ? params : [params];
3605
3842
  const invocations = options.map((option) => {
3606
- const failureUrl = option.useFailureFunction ? option.url : option.failureUrl;
3843
+ const failureUrl = option.failureUrl ?? option.url;
3607
3844
  const finalWorkflowRunId = getWorkflowRunId(option.workflowRunId);
3608
3845
  const context = new WorkflowContext({
3609
3846
  qstashClient: this.client,
@@ -3615,11 +3852,7 @@ var Client4 = class {
3615
3852
  steps: [],
3616
3853
  url: option.url,
3617
3854
  workflowRunId: finalWorkflowRunId,
3618
- retries: option.retries,
3619
- retryDelay: option.retryDelay,
3620
3855
  telemetry: option.disableTelemetry ? void 0 : { sdk: SDK_TELEMETRY },
3621
- flowControl: option.flowControl,
3622
- failureUrl,
3623
3856
  label: option.label
3624
3857
  });
3625
3858
  return {
@@ -3627,7 +3860,10 @@ var Client4 = class {
3627
3860
  telemetry: option.disableTelemetry ? void 0 : { sdk: SDK_TELEMETRY },
3628
3861
  delay: option.delay,
3629
3862
  notBefore: option.notBefore,
3630
- keepTriggerConfig: option.keepTriggerConfig
3863
+ failureUrl,
3864
+ retries: option.retries,
3865
+ retryDelay: option.retryDelay,
3866
+ flowControl: option.flowControl
3631
3867
  };
3632
3868
  });
3633
3869
  const result = await triggerFirstInvocation(invocations);
@@ -3707,8 +3943,9 @@ var Client4 = class {
3707
3943
  WorkflowAbort,
3708
3944
  WorkflowContext,
3709
3945
  WorkflowError,
3710
- WorkflowLogger,
3946
+ WorkflowMiddleware,
3711
3947
  WorkflowNonRetryableError,
3712
3948
  WorkflowRetryAfterError,
3949
+ loggingMiddleware,
3713
3950
  serve
3714
3951
  });