@upstash/workflow 0.1.5 → 0.2.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/astro.js CHANGED
@@ -26,22 +26,33 @@ module.exports = __toCommonJS(astro_exports);
26
26
 
27
27
  // src/error.ts
28
28
  var import_qstash = require("@upstash/qstash");
29
- var QStashWorkflowError = class extends import_qstash.QstashError {
29
+ var WorkflowError = class extends import_qstash.QstashError {
30
30
  constructor(message) {
31
31
  super(message);
32
- this.name = "QStashWorkflowError";
32
+ this.name = "WorkflowError";
33
33
  }
34
34
  };
35
- var QStashWorkflowAbort = class extends Error {
35
+ var WorkflowAbort = class extends Error {
36
36
  stepInfo;
37
37
  stepName;
38
- constructor(stepName, stepInfo) {
38
+ /**
39
+ * whether workflow is to be canceled on abort
40
+ */
41
+ cancelWorkflow;
42
+ /**
43
+ *
44
+ * @param stepName name of the aborting step
45
+ * @param stepInfo step information
46
+ * @param cancelWorkflow
47
+ */
48
+ constructor(stepName, stepInfo, cancelWorkflow = false) {
39
49
  super(
40
50
  `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}'.`
41
51
  );
42
- this.name = "QStashWorkflowAbort";
52
+ this.name = "WorkflowAbort";
43
53
  this.stepName = stepName;
44
54
  this.stepInfo = stepInfo;
55
+ this.cancelWorkflow = cancelWorkflow;
45
56
  }
46
57
  };
47
58
  var formatWorkflowError = (error) => {
@@ -63,6 +74,44 @@ var makeNotifyRequest = async (requester, eventId, eventData) => {
63
74
  });
64
75
  return result;
65
76
  };
77
+ var makeCancelRequest = async (requester, workflowRunId) => {
78
+ await requester.request({
79
+ path: ["v2", "workflows", "runs", `${workflowRunId}?cancel=true`],
80
+ method: "DELETE",
81
+ parseResponseAsJson: false
82
+ });
83
+ return true;
84
+ };
85
+ var getSteps = async (requester, workflowRunId, messageId, debug) => {
86
+ try {
87
+ const steps = await requester.request({
88
+ path: ["v2", "workflows", "runs", workflowRunId],
89
+ parseResponseAsJson: true
90
+ });
91
+ if (!messageId) {
92
+ await debug?.log("INFO", "ENDPOINT_START", {
93
+ message: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
94
+ });
95
+ return steps;
96
+ } else {
97
+ const index = steps.findIndex((item) => item.messageId === messageId);
98
+ if (index === -1) {
99
+ return [];
100
+ }
101
+ const filteredSteps = steps.slice(0, index + 1);
102
+ await debug?.log("INFO", "ENDPOINT_START", {
103
+ message: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
104
+ });
105
+ return filteredSteps;
106
+ }
107
+ } catch (error) {
108
+ await debug?.log("ERROR", "ERROR", {
109
+ message: "failed while fetching steps.",
110
+ error
111
+ });
112
+ throw new WorkflowError(`Failed while pulling steps. ${error}`);
113
+ }
114
+ };
66
115
 
67
116
  // src/context/steps.ts
68
117
  var BaseLazyStep = class {
@@ -677,6 +726,7 @@ var StepTypes = [
677
726
  ];
678
727
 
679
728
  // src/workflow-requests.ts
729
+ var import_qstash2 = require("@upstash/qstash");
680
730
  var triggerFirstInvocation = async (workflowContext, retries, debug) => {
681
731
  const { headers } = getHeaders(
682
732
  "true",
@@ -687,20 +737,32 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
687
737
  workflowContext.failureUrl,
688
738
  retries
689
739
  );
690
- await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
691
- headers,
692
- requestPayload: workflowContext.requestPayload,
693
- url: workflowContext.url
694
- });
695
740
  try {
696
741
  const body = typeof workflowContext.requestPayload === "string" ? workflowContext.requestPayload : JSON.stringify(workflowContext.requestPayload);
697
- await workflowContext.qstashClient.publish({
742
+ const result = await workflowContext.qstashClient.publish({
698
743
  headers,
699
744
  method: "POST",
700
745
  body,
701
746
  url: workflowContext.url
702
747
  });
703
- return ok("success");
748
+ if (result.deduplicated) {
749
+ await debug?.log("WARN", "SUBMIT_FIRST_INVOCATION", {
750
+ message: `Workflow run ${workflowContext.workflowRunId} already exists. A new one isn't created.`,
751
+ headers,
752
+ requestPayload: workflowContext.requestPayload,
753
+ url: workflowContext.url,
754
+ messageId: result.messageId
755
+ });
756
+ return ok("workflow-run-already-exists");
757
+ } else {
758
+ await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
759
+ headers,
760
+ requestPayload: workflowContext.requestPayload,
761
+ url: workflowContext.url,
762
+ messageId: result.messageId
763
+ });
764
+ return ok("success");
765
+ }
704
766
  } catch (error) {
705
767
  const error_ = error;
706
768
  return err(error_);
@@ -708,7 +770,9 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
708
770
  };
709
771
  var triggerRouteFunction = async ({
710
772
  onCleanup,
711
- onStep
773
+ onStep,
774
+ onCancel,
775
+ debug
712
776
  }) => {
713
777
  try {
714
778
  await onStep();
@@ -716,19 +780,50 @@ var triggerRouteFunction = async ({
716
780
  return ok("workflow-finished");
717
781
  } catch (error) {
718
782
  const error_ = error;
719
- return error_ instanceof QStashWorkflowAbort ? ok("step-finished") : err(error_);
783
+ if (error instanceof import_qstash2.QstashError && error.status === 400) {
784
+ await debug?.log("WARN", "RESPONSE_WORKFLOW", {
785
+ message: `tried to append to a cancelled workflow. exiting without publishing.`,
786
+ name: error.name,
787
+ errorMessage: error.message
788
+ });
789
+ return ok("workflow-was-finished");
790
+ } else if (!(error_ instanceof WorkflowAbort)) {
791
+ return err(error_);
792
+ } else if (error_.cancelWorkflow) {
793
+ await onCancel();
794
+ return ok("workflow-finished");
795
+ } else {
796
+ return ok("step-finished");
797
+ }
720
798
  }
721
799
  };
722
800
  var triggerWorkflowDelete = async (workflowContext, debug, cancel = false) => {
723
801
  await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
724
802
  deletedWorkflowRunId: workflowContext.workflowRunId
725
803
  });
726
- const result = await workflowContext.qstashClient.http.request({
727
- path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
728
- method: "DELETE",
729
- parseResponseAsJson: false
730
- });
731
- await debug?.log("SUBMIT", "SUBMIT_CLEANUP", result);
804
+ try {
805
+ await workflowContext.qstashClient.http.request({
806
+ path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
807
+ method: "DELETE",
808
+ parseResponseAsJson: false
809
+ });
810
+ await debug?.log(
811
+ "SUBMIT",
812
+ "SUBMIT_CLEANUP",
813
+ `workflow run ${workflowContext.workflowRunId} deleted.`
814
+ );
815
+ return { deleted: true };
816
+ } catch (error) {
817
+ if (error instanceof import_qstash2.QstashError && error.status === 404) {
818
+ await debug?.log("WARN", "SUBMIT_CLEANUP", {
819
+ message: `Failed to remove workflow run ${workflowContext.workflowRunId} as it doesn't exist.`,
820
+ name: error.name,
821
+ errorMessage: error.message
822
+ });
823
+ return { deleted: false };
824
+ }
825
+ throw error;
826
+ }
732
827
  };
733
828
  var recreateUserHeaders = (headers) => {
734
829
  const filteredHeaders = new Headers();
@@ -744,15 +839,32 @@ var recreateUserHeaders = (headers) => {
744
839
  var handleThirdPartyCallResult = async (request, requestPayload, client, workflowUrl, failureUrl, retries, debug) => {
745
840
  try {
746
841
  if (request.headers.get("Upstash-Workflow-Callback")) {
747
- const callbackMessage = JSON.parse(requestPayload);
842
+ let callbackPayload;
843
+ if (requestPayload) {
844
+ callbackPayload = requestPayload;
845
+ } else {
846
+ const workflowRunId2 = request.headers.get("upstash-workflow-runid");
847
+ const messageId = request.headers.get("upstash-message-id");
848
+ if (!workflowRunId2)
849
+ throw new WorkflowError("workflow run id missing in context.call lazy fetch.");
850
+ if (!messageId) throw new WorkflowError("message id missing in context.call lazy fetch.");
851
+ const steps = await getSteps(client.http, workflowRunId2, messageId, debug);
852
+ const failingStep = steps.find((step) => step.messageId === messageId);
853
+ if (!failingStep)
854
+ throw new WorkflowError(
855
+ "Failed to submit the context.call." + (steps.length === 0 ? "No steps found." : `No step was found with matching messageId ${messageId} out of ${steps.length} steps.`)
856
+ );
857
+ callbackPayload = atob(failingStep.body);
858
+ }
859
+ const callbackMessage = JSON.parse(callbackPayload);
748
860
  if (!(callbackMessage.status >= 200 && callbackMessage.status < 300) && callbackMessage.maxRetries && callbackMessage.retried !== callbackMessage.maxRetries) {
749
861
  await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
750
862
  status: callbackMessage.status,
751
- body: atob(callbackMessage.body)
863
+ body: atob(callbackMessage.body ?? "")
752
864
  });
753
865
  console.warn(
754
866
  `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (retried ${callbackMessage.retried ?? 0} out of ${callbackMessage.maxRetries} times). Error Message:
755
- ${atob(callbackMessage.body)}`
867
+ ${atob(callbackMessage.body ?? "")}`
756
868
  );
757
869
  return ok("call-will-retry");
758
870
  }
@@ -786,7 +898,7 @@ ${atob(callbackMessage.body)}`
786
898
  );
787
899
  const callResponse = {
788
900
  status: callbackMessage.status,
789
- body: atob(callbackMessage.body),
901
+ body: atob(callbackMessage.body ?? ""),
790
902
  header: callbackMessage.header
791
903
  };
792
904
  const callResultStep = {
@@ -817,9 +929,7 @@ ${atob(callbackMessage.body)}`
817
929
  } catch (error) {
818
930
  const isCallReturn = request.headers.get("Upstash-Workflow-Callback");
819
931
  return err(
820
- new QStashWorkflowError(
821
- `Error when handling call return (isCallReturn=${isCallReturn}): ${error}`
822
- )
932
+ new WorkflowError(`Error when handling call return (isCallReturn=${isCallReturn}): ${error}`)
823
933
  );
824
934
  }
825
935
  };
@@ -827,7 +937,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
827
937
  const baseHeaders = {
828
938
  [WORKFLOW_INIT_HEADER]: initHeaderValue,
829
939
  [WORKFLOW_ID_HEADER]: workflowRunId,
830
- [WORKFLOW_URL_HEADER]: workflowUrl
940
+ [WORKFLOW_URL_HEADER]: workflowUrl,
941
+ [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody"
831
942
  };
832
943
  if (!step?.callUrl) {
833
944
  baseHeaders[`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`] = WORKFLOW_PROTOCOL_VERSION;
@@ -840,8 +951,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
840
951
  }
841
952
  if (step?.callUrl) {
842
953
  baseHeaders["Upstash-Retries"] = callRetries?.toString() ?? "0";
843
- baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete";
844
- if (retries) {
954
+ baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete,InitialBody";
955
+ if (retries !== void 0) {
845
956
  baseHeaders["Upstash-Callback-Retries"] = retries.toString();
846
957
  baseHeaders["Upstash-Failure-Callback-Retries"] = retries.toString();
847
958
  }
@@ -876,6 +987,7 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
876
987
  "Upstash-Callback-Workflow-CallType": "fromCallback",
877
988
  "Upstash-Callback-Workflow-Init": "false",
878
989
  "Upstash-Callback-Workflow-Url": workflowUrl,
990
+ "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody",
879
991
  "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
880
992
  "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
881
993
  "Upstash-Callback-Forward-Upstash-Workflow-StepName": step.stepName,
@@ -924,7 +1036,7 @@ var verifyRequest = async (body, signature, verifier) => {
924
1036
  throw new Error("Signature in `Upstash-Signature` header is not valid");
925
1037
  }
926
1038
  } catch (error) {
927
- throw new QStashWorkflowError(
1039
+ throw new WorkflowError(
928
1040
  `Failed to verify that the Workflow request comes from QStash: ${error}
929
1041
 
930
1042
  If signature is missing, trigger the workflow endpoint by publishing your request to QStash instead of calling it directly.
@@ -964,14 +1076,14 @@ var AutoExecutor = class _AutoExecutor {
964
1076
  *
965
1077
  * If a function is already executing (this.executingStep), this
966
1078
  * means that there is a nested step which is not allowed. In this
967
- * case, addStep throws QStashWorkflowError.
1079
+ * case, addStep throws WorkflowError.
968
1080
  *
969
1081
  * @param stepInfo step plan to add
970
1082
  * @returns result of the step function
971
1083
  */
972
1084
  async addStep(stepInfo) {
973
1085
  if (this.executingStep) {
974
- throw new QStashWorkflowError(
1086
+ throw new WorkflowError(
975
1087
  `A step can not be run inside another step. Tried to run '${stepInfo.stepName}' inside '${this.executingStep}'`
976
1088
  );
977
1089
  }
@@ -1056,7 +1168,7 @@ var AutoExecutor = class _AutoExecutor {
1056
1168
  const sortedSteps = sortSteps(this.steps);
1057
1169
  const plannedParallelStepCount = sortedSteps[initialStepCount + this.planStepCount]?.concurrent;
1058
1170
  if (parallelCallState !== "first" && plannedParallelStepCount !== parallelSteps.length) {
1059
- throw new QStashWorkflowError(
1171
+ throw new WorkflowError(
1060
1172
  `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
1061
1173
  );
1062
1174
  }
@@ -1078,7 +1190,7 @@ var AutoExecutor = class _AutoExecutor {
1078
1190
  case "partial": {
1079
1191
  const planStep = this.steps.at(-1);
1080
1192
  if (!planStep || planStep.targetStep === void 0) {
1081
- throw new QStashWorkflowError(
1193
+ throw new WorkflowError(
1082
1194
  `There must be a last step and it should have targetStep larger than 0.Received: ${JSON.stringify(planStep)}`
1083
1195
  );
1084
1196
  }
@@ -1092,17 +1204,17 @@ var AutoExecutor = class _AutoExecutor {
1092
1204
  );
1093
1205
  await this.submitStepsToQStash([resultStep], [parallelStep]);
1094
1206
  } catch (error) {
1095
- if (error instanceof QStashWorkflowAbort) {
1207
+ if (error instanceof WorkflowAbort) {
1096
1208
  throw error;
1097
1209
  }
1098
- throw new QStashWorkflowError(
1210
+ throw new WorkflowError(
1099
1211
  `Error submitting steps to QStash in partial parallel step execution: ${error}`
1100
1212
  );
1101
1213
  }
1102
1214
  break;
1103
1215
  }
1104
1216
  case "discard": {
1105
- throw new QStashWorkflowAbort("discarded parallel");
1217
+ throw new WorkflowAbort("discarded parallel");
1106
1218
  }
1107
1219
  case "last": {
1108
1220
  const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
@@ -1153,7 +1265,7 @@ var AutoExecutor = class _AutoExecutor {
1153
1265
  */
1154
1266
  async submitStepsToQStash(steps, lazySteps) {
1155
1267
  if (steps.length === 0) {
1156
- throw new QStashWorkflowError(
1268
+ throw new WorkflowError(
1157
1269
  `Unable to submit steps to QStash. Provided list is empty. Current step: ${this.stepCount}`
1158
1270
  );
1159
1271
  }
@@ -1193,7 +1305,7 @@ var AutoExecutor = class _AutoExecutor {
1193
1305
  method: "POST",
1194
1306
  parseResponseAsJson: false
1195
1307
  });
1196
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
1308
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
1197
1309
  }
1198
1310
  const result = await this.context.qstashClient.batchJSON(
1199
1311
  steps.map((singleStep, index) => {
@@ -1245,7 +1357,7 @@ var AutoExecutor = class _AutoExecutor {
1245
1357
  };
1246
1358
  })
1247
1359
  });
1248
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
1360
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
1249
1361
  }
1250
1362
  /**
1251
1363
  * Get the promise by executing the lazt steps list. If there is a single
@@ -1270,7 +1382,7 @@ var AutoExecutor = class _AutoExecutor {
1270
1382
  } else if (Array.isArray(result) && lazyStepList.length === result.length && index < lazyStepList.length) {
1271
1383
  return result[index];
1272
1384
  } else {
1273
- throw new QStashWorkflowError(
1385
+ throw new WorkflowError(
1274
1386
  `Unexpected parallel call result while executing step ${index}: '${result}'. Expected ${lazyStepList.length} many items`
1275
1387
  );
1276
1388
  }
@@ -1282,12 +1394,12 @@ var AutoExecutor = class _AutoExecutor {
1282
1394
  };
1283
1395
  var validateStep = (lazyStep, stepFromRequest) => {
1284
1396
  if (lazyStep.stepName !== stepFromRequest.stepName) {
1285
- throw new QStashWorkflowError(
1397
+ throw new WorkflowError(
1286
1398
  `Incompatible step name. Expected '${lazyStep.stepName}', got '${stepFromRequest.stepName}' from the request`
1287
1399
  );
1288
1400
  }
1289
1401
  if (lazyStep.stepType !== stepFromRequest.stepType) {
1290
- throw new QStashWorkflowError(
1402
+ throw new WorkflowError(
1291
1403
  `Incompatible step type. Expected '${lazyStep.stepType}', got '${stepFromRequest.stepType}' from the request`
1292
1404
  );
1293
1405
  }
@@ -1298,12 +1410,12 @@ var validateParallelSteps = (lazySteps, stepsFromRequest) => {
1298
1410
  validateStep(lazySteps[index], stepFromRequest);
1299
1411
  }
1300
1412
  } catch (error) {
1301
- if (error instanceof QStashWorkflowError) {
1413
+ if (error instanceof WorkflowError) {
1302
1414
  const lazyStepNames = lazySteps.map((lazyStep) => lazyStep.stepName);
1303
1415
  const lazyStepTypes = lazySteps.map((lazyStep) => lazyStep.stepType);
1304
1416
  const requestStepNames = stepsFromRequest.map((step) => step.stepName);
1305
1417
  const requestStepTypes = stepsFromRequest.map((step) => step.stepType);
1306
- throw new QStashWorkflowError(
1418
+ throw new WorkflowError(
1307
1419
  `Incompatible steps detected in parallel execution: ${error.message}
1308
1420
  > Step Names from the request: ${JSON.stringify(requestStepNames)}
1309
1421
  Step Types from the request: ${JSON.stringify(requestStepTypes)}
@@ -1416,10 +1528,6 @@ var WorkflowContext = class {
1416
1528
  * headers of the initial request
1417
1529
  */
1418
1530
  headers;
1419
- /**
1420
- * initial payload as a raw string
1421
- */
1422
- rawInitialPayload;
1423
1531
  /**
1424
1532
  * Map of environment variables and their values.
1425
1533
  *
@@ -1454,7 +1562,6 @@ var WorkflowContext = class {
1454
1562
  failureUrl,
1455
1563
  debug,
1456
1564
  initialPayload,
1457
- rawInitialPayload,
1458
1565
  env,
1459
1566
  retries
1460
1567
  }) {
@@ -1465,7 +1572,6 @@ var WorkflowContext = class {
1465
1572
  this.failureUrl = failureUrl;
1466
1573
  this.headers = headers;
1467
1574
  this.requestPayload = initialPayload;
1468
- this.rawInitialPayload = rawInitialPayload ?? JSON.stringify(this.requestPayload);
1469
1575
  this.env = env ?? {};
1470
1576
  this.retries = retries ?? DEFAULT_RETRIES;
1471
1577
  this.executor = new AutoExecutor(this, this.steps, debug);
@@ -1486,7 +1592,7 @@ var WorkflowContext = class {
1486
1592
  * const [result1, result2] = await Promise.all([
1487
1593
  * context.run("step 1", () => {
1488
1594
  * return "result1"
1489
- * })
1595
+ * }),
1490
1596
  * context.run("step 2", async () => {
1491
1597
  * return await fetchResults()
1492
1598
  * })
@@ -1504,6 +1610,10 @@ var WorkflowContext = class {
1504
1610
  /**
1505
1611
  * Stops the execution for the duration provided.
1506
1612
  *
1613
+ * ```typescript
1614
+ * await context.sleep('sleep1', 3) // wait for three seconds
1615
+ * ```
1616
+ *
1507
1617
  * @param stepName
1508
1618
  * @param duration sleep duration in seconds
1509
1619
  * @returns undefined
@@ -1514,6 +1624,10 @@ var WorkflowContext = class {
1514
1624
  /**
1515
1625
  * Stops the execution until the date time provided.
1516
1626
  *
1627
+ * ```typescript
1628
+ * await context.sleepUntil('sleep1', Date.now() / 1000 + 3) // wait for three seconds
1629
+ * ```
1630
+ *
1517
1631
  * @param stepName
1518
1632
  * @param datetime time to sleep until. Can be provided as a number (in unix seconds),
1519
1633
  * as a Date object or a string (passed to `new Date(datetimeString)`)
@@ -1537,7 +1651,7 @@ var WorkflowContext = class {
1537
1651
  * const { status, body } = await context.call<string>(
1538
1652
  * "post call step",
1539
1653
  * {
1540
- * url: `https://www.some-endpoint.com/api`,
1654
+ * url: "https://www.some-endpoint.com/api",
1541
1655
  * method: "POST",
1542
1656
  * body: "my-payload"
1543
1657
  * }
@@ -1591,45 +1705,43 @@ var WorkflowContext = class {
1591
1705
  }
1592
1706
  }
1593
1707
  /**
1594
- * Makes the workflow run wait until a notify request is sent or until the
1595
- * timeout ends
1708
+ * Pauses workflow execution until a specific event occurs or a timeout is reached.
1596
1709
  *
1597
- * ```ts
1598
- * const { eventData, timeout } = await context.waitForEvent(
1599
- * "wait for event step",
1600
- * "my-event-id",
1601
- * 100 // timeout after 100 seconds
1602
- * );
1603
- * ```
1710
+ *```ts
1711
+ * const result = await workflow.waitForEvent("payment-confirmed", {
1712
+ * timeout: "5m"
1713
+ * });
1714
+ *```
1604
1715
  *
1605
- * To notify a waiting workflow run, you can use the notify method:
1716
+ * To notify a waiting workflow:
1606
1717
  *
1607
1718
  * ```ts
1608
1719
  * import { Client } from "@upstash/workflow";
1609
1720
  *
1610
- * const client = new Client({ token: });
1721
+ * const client = new Client({ token: "<QSTASH_TOKEN>" });
1611
1722
  *
1612
1723
  * await client.notify({
1613
- * eventId: "my-event-id",
1614
- * eventData: "eventData"
1724
+ * eventId: "payment.confirmed",
1725
+ * data: {
1726
+ * amount: 99.99,
1727
+ * currency: "USD"
1728
+ * }
1615
1729
  * })
1616
1730
  * ```
1617
1731
  *
1732
+ * Alternatively, you can use the `context.notify` method.
1733
+ *
1618
1734
  * @param stepName
1619
- * @param eventId event id to wake up the waiting workflow run
1620
- * @param timeout timeout duration in seconds
1621
- * @returns wait response as `{ timeout: boolean, eventData: unknown }`.
1622
- * timeout is true if the wait times out, if notified it is false. eventData
1623
- * is the value passed to `client.notify`.
1735
+ * @param eventId - Unique identifier for the event to wait for
1736
+ * @param options - Configuration options.
1737
+ * @returns `{ timeout: boolean, eventData: unknown }`.
1738
+ * The `timeout` property specifies if the workflow has timed out. The `eventData`
1739
+ * is the data passed when notifying this workflow of an event.
1624
1740
  */
1625
- async waitForEvent(stepName, eventId, timeout) {
1626
- const result = await this.addStep(
1627
- new LazyWaitForEventStep(
1628
- stepName,
1629
- eventId,
1630
- typeof timeout === "string" ? timeout : `${timeout}s`
1631
- )
1632
- );
1741
+ async waitForEvent(stepName, eventId, options = {}) {
1742
+ const { timeout = "7d" } = options;
1743
+ const timeoutStr = typeof timeout === "string" ? timeout : `${timeout}s`;
1744
+ const result = await this.addStep(new LazyWaitForEventStep(stepName, eventId, timeoutStr));
1633
1745
  try {
1634
1746
  return {
1635
1747
  ...result,
@@ -1639,6 +1751,27 @@ var WorkflowContext = class {
1639
1751
  return result;
1640
1752
  }
1641
1753
  }
1754
+ /**
1755
+ * Notify workflow runs waiting for an event
1756
+ *
1757
+ * ```ts
1758
+ * const { eventId, eventData, notifyResponse } = await context.notify(
1759
+ * "notify step", "event-id", "event-data"
1760
+ * );
1761
+ * ```
1762
+ *
1763
+ * Upon `context.notify`, the workflow runs waiting for the given eventId (context.waitForEvent)
1764
+ * will receive the given event data and resume execution.
1765
+ *
1766
+ * The response includes the same eventId and eventData. Additionally, there is
1767
+ * a notifyResponse field which contains a list of `Waiter` objects, each corresponding
1768
+ * to a notified workflow run.
1769
+ *
1770
+ * @param stepName
1771
+ * @param eventId event id to notify
1772
+ * @param eventData event data to notify with
1773
+ * @returns notify response which has event id, event data and list of waiters which were notified
1774
+ */
1642
1775
  async notify(stepName, eventId, eventData) {
1643
1776
  const result = await this.addStep(
1644
1777
  new LazyNotifyStep(stepName, eventId, eventData, this.qstashClient.http)
@@ -1652,6 +1785,15 @@ var WorkflowContext = class {
1652
1785
  return result;
1653
1786
  }
1654
1787
  }
1788
+ /**
1789
+ * Cancel the current workflow run
1790
+ *
1791
+ * Will throw WorkflowAbort to stop workflow execution.
1792
+ * Shouldn't be inside try/catch.
1793
+ */
1794
+ async cancel() {
1795
+ throw new WorkflowAbort("cancel", void 0, true);
1796
+ }
1655
1797
  /**
1656
1798
  * Adds steps to the executor. Needed so that it can be overwritten in
1657
1799
  * DisabledWorkflowContext.
@@ -1692,7 +1834,8 @@ var WorkflowLogger = class _WorkflowLogger {
1692
1834
  }
1693
1835
  writeToConsole(logEntry) {
1694
1836
  const JSON_SPACING = 2;
1695
- console.log(JSON.stringify(logEntry, void 0, JSON_SPACING));
1837
+ const logMethod = logEntry.logLevel === "ERROR" ? console.error : logEntry.logLevel === "WARN" ? console.warn : console.log;
1838
+ logMethod(JSON.stringify(logEntry, void 0, JSON_SPACING));
1696
1839
  }
1697
1840
  shouldLog(level) {
1698
1841
  return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
@@ -1734,6 +1877,63 @@ function decodeBase64(base64) {
1734
1877
  }
1735
1878
  }
1736
1879
 
1880
+ // src/serve/authorization.ts
1881
+ var import_qstash3 = require("@upstash/qstash");
1882
+ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
1883
+ static disabledMessage = "disabled-qstash-worklfow-run";
1884
+ /**
1885
+ * overwrite the WorkflowContext.addStep method to always raise WorkflowAbort
1886
+ * error in order to stop the execution whenever we encounter a step.
1887
+ *
1888
+ * @param _step
1889
+ */
1890
+ async addStep(_step) {
1891
+ throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
1892
+ }
1893
+ /**
1894
+ * overwrite cancel method to do nothing
1895
+ */
1896
+ async cancel() {
1897
+ return;
1898
+ }
1899
+ /**
1900
+ * copies the passed context to create a DisabledWorkflowContext. Then, runs the
1901
+ * route function with the new context.
1902
+ *
1903
+ * - returns "run-ended" if there are no steps found or
1904
+ * if the auth failed and user called `return`
1905
+ * - returns "step-found" if DisabledWorkflowContext.addStep is called.
1906
+ * - if there is another error, returns the error.
1907
+ *
1908
+ * @param routeFunction
1909
+ */
1910
+ static async tryAuthentication(routeFunction, context) {
1911
+ const disabledContext = new _DisabledWorkflowContext({
1912
+ qstashClient: new import_qstash3.Client({
1913
+ baseUrl: "disabled-client",
1914
+ token: "disabled-client"
1915
+ }),
1916
+ workflowRunId: context.workflowRunId,
1917
+ headers: context.headers,
1918
+ steps: [],
1919
+ url: context.url,
1920
+ failureUrl: context.failureUrl,
1921
+ initialPayload: context.requestPayload,
1922
+ env: context.env,
1923
+ retries: context.retries
1924
+ });
1925
+ try {
1926
+ await routeFunction(disabledContext);
1927
+ } catch (error) {
1928
+ if (error instanceof WorkflowAbort && error.stepName === this.disabledMessage) {
1929
+ return ok("step-found");
1930
+ }
1931
+ return err(error);
1932
+ }
1933
+ return ok("run-ended");
1934
+ }
1935
+ };
1936
+
1737
1937
  // src/workflow-parser.ts
1738
1938
  var getPayload = async (request) => {
1739
1939
  try {
@@ -1742,8 +1942,8 @@ var getPayload = async (request) => {
1742
1942
  return;
1743
1943
  }
1744
1944
  };
1745
- var parsePayload = async (rawPayload, debug) => {
1746
- const [encodedInitialPayload, ...encodedSteps] = JSON.parse(rawPayload);
1945
+ var processRawSteps = (rawSteps) => {
1946
+ const [encodedInitialPayload, ...encodedSteps] = rawSteps;
1747
1947
  const rawInitialPayload = decodeBase64(encodedInitialPayload.body);
1748
1948
  const initialStep = {
1749
1949
  stepId: 0,
@@ -1753,27 +1953,21 @@ var parsePayload = async (rawPayload, debug) => {
1753
1953
  concurrent: NO_CONCURRENCY
1754
1954
  };
1755
1955
  const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
1756
- const otherSteps = await Promise.all(
1757
- stepsToDecode.map(async (rawStep) => {
1758
- const step = JSON.parse(decodeBase64(rawStep.body));
1759
- try {
1760
- step.out = JSON.parse(step.out);
1761
- } catch {
1762
- await debug?.log("WARN", "ENDPOINT_START", {
1763
- message: "failed while parsing out field of step",
1764
- step
1765
- });
1766
- }
1767
- if (step.waitEventId) {
1768
- const newOut = {
1769
- eventData: step.out ? decodeBase64(step.out) : void 0,
1770
- timeout: step.waitTimeout ?? false
1771
- };
1772
- step.out = newOut;
1773
- }
1774
- return step;
1775
- })
1776
- );
1956
+ const otherSteps = stepsToDecode.map((rawStep) => {
1957
+ const step = JSON.parse(decodeBase64(rawStep.body));
1958
+ try {
1959
+ step.out = JSON.parse(step.out);
1960
+ } catch {
1961
+ }
1962
+ if (step.waitEventId) {
1963
+ const newOut = {
1964
+ eventData: step.out ? decodeBase64(step.out) : void 0,
1965
+ timeout: step.waitTimeout ?? false
1966
+ };
1967
+ step.out = newOut;
1968
+ }
1969
+ return step;
1970
+ });
1777
1971
  const steps = [initialStep, ...otherSteps];
1778
1972
  return {
1779
1973
  rawInitialPayload,
@@ -1821,20 +2015,20 @@ var validateRequest = (request) => {
1821
2015
  const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
1822
2016
  const isFirstInvocation = !versionHeader;
1823
2017
  if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
1824
- throw new QStashWorkflowError(
2018
+ throw new WorkflowError(
1825
2019
  `Incompatible workflow sdk protocol version. Expected ${WORKFLOW_PROTOCOL_VERSION}, got ${versionHeader} from the request.`
1826
2020
  );
1827
2021
  }
1828
2022
  const workflowRunId = isFirstInvocation ? getWorkflowRunId() : request.headers.get(WORKFLOW_ID_HEADER) ?? "";
1829
2023
  if (workflowRunId.length === 0) {
1830
- throw new QStashWorkflowError("Couldn't get workflow id from header");
2024
+ throw new WorkflowError("Couldn't get workflow id from header");
1831
2025
  }
1832
2026
  return {
1833
2027
  isFirstInvocation,
1834
2028
  workflowRunId
1835
2029
  };
1836
2030
  };
1837
- var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
2031
+ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requester, messageId, debug) => {
1838
2032
  if (isFirstInvocation) {
1839
2033
  return {
1840
2034
  rawInitialPayload: requestPayload ?? "",
@@ -1842,10 +2036,18 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
1842
2036
  isLastDuplicate: false
1843
2037
  };
1844
2038
  } else {
2039
+ let rawSteps;
1845
2040
  if (!requestPayload) {
1846
- throw new QStashWorkflowError("Only first call can have an empty body");
2041
+ await debug?.log(
2042
+ "INFO",
2043
+ "ENDPOINT_START",
2044
+ "request payload is empty, steps will be fetched from QStash."
2045
+ );
2046
+ rawSteps = await getSteps(requester, workflowRunId, messageId, debug);
2047
+ } else {
2048
+ rawSteps = JSON.parse(requestPayload);
1847
2049
  }
1848
- const { rawInitialPayload, steps } = await parsePayload(requestPayload, debug);
2050
+ const { rawInitialPayload, steps } = processRawSteps(rawSteps);
1849
2051
  const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
1850
2052
  const deduplicatedSteps = deduplicateSteps(steps);
1851
2053
  return {
@@ -1855,13 +2057,13 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
1855
2057
  };
1856
2058
  }
1857
2059
  };
1858
- var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, failureFunction, debug) => {
2060
+ var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, routeFunction, failureFunction, debug) => {
1859
2061
  if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
1860
2062
  return ok("not-failure-callback");
1861
2063
  }
1862
2064
  if (!failureFunction) {
1863
2065
  return err(
1864
- new QStashWorkflowError(
2066
+ new WorkflowError(
1865
2067
  "Workflow endpoint is called to handle a failure, but a failureFunction is not provided in serve options. Either provide a failureUrl or a failureFunction."
1866
2068
  )
1867
2069
  );
@@ -1872,92 +2074,48 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
1872
2074
  );
1873
2075
  const decodedBody = body ? decodeBase64(body) : "{}";
1874
2076
  const errorPayload = JSON.parse(decodedBody);
1875
- const {
1876
- rawInitialPayload,
1877
- steps,
1878
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1879
- isLastDuplicate: _isLastDuplicate
1880
- } = await parseRequest(decodeBase64(sourceBody), false, debug);
1881
2077
  const workflowContext = new WorkflowContext({
1882
2078
  qstashClient,
1883
2079
  workflowRunId,
1884
- initialPayload: initialPayloadParser(rawInitialPayload),
1885
- rawInitialPayload,
2080
+ initialPayload: initialPayloadParser(decodeBase64(sourceBody)),
1886
2081
  headers: recreateUserHeaders(new Headers(sourceHeader)),
1887
- steps,
2082
+ steps: [],
1888
2083
  url,
1889
2084
  failureUrl: url,
1890
2085
  debug
1891
2086
  });
1892
- await failureFunction(workflowContext, status, errorPayload.message, header);
2087
+ const authCheck = await DisabledWorkflowContext.tryAuthentication(
2088
+ routeFunction,
2089
+ workflowContext
2090
+ );
2091
+ if (authCheck.isErr()) {
2092
+ await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
2093
+ throw authCheck.error;
2094
+ } else if (authCheck.value === "run-ended") {
2095
+ return err(new WorkflowError("Not authorized to run the failure function."));
2096
+ }
2097
+ await failureFunction({
2098
+ context: workflowContext,
2099
+ failStatus: status,
2100
+ failResponse: errorPayload.message,
2101
+ failHeaders: header
2102
+ });
1893
2103
  } catch (error) {
1894
2104
  return err(error);
1895
2105
  }
1896
2106
  return ok("is-failure-callback");
1897
2107
  };
1898
2108
 
1899
- // src/serve/authorization.ts
1900
- var import_qstash2 = require("@upstash/qstash");
1901
- var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
1902
- static disabledMessage = "disabled-qstash-worklfow-run";
1903
- /**
1904
- * overwrite the WorkflowContext.addStep method to always raise QStashWorkflowAbort
1905
- * error in order to stop the execution whenever we encounter a step.
1906
- *
1907
- * @param _step
1908
- */
1909
- async addStep(_step) {
1910
- throw new QStashWorkflowAbort(_DisabledWorkflowContext.disabledMessage);
1911
- }
1912
- /**
1913
- * copies the passed context to create a DisabledWorkflowContext. Then, runs the
1914
- * route function with the new context.
1915
- *
1916
- * - returns "run-ended" if there are no steps found or
1917
- * if the auth failed and user called `return`
1918
- * - returns "step-found" if DisabledWorkflowContext.addStep is called.
1919
- * - if there is another error, returns the error.
1920
- *
1921
- * @param routeFunction
1922
- */
1923
- static async tryAuthentication(routeFunction, context) {
1924
- const disabledContext = new _DisabledWorkflowContext({
1925
- qstashClient: new import_qstash2.Client({
1926
- baseUrl: "disabled-client",
1927
- token: "disabled-client"
1928
- }),
1929
- workflowRunId: context.workflowRunId,
1930
- headers: context.headers,
1931
- steps: [],
1932
- url: context.url,
1933
- failureUrl: context.failureUrl,
1934
- initialPayload: context.requestPayload,
1935
- rawInitialPayload: context.rawInitialPayload,
1936
- env: context.env,
1937
- retries: context.retries
1938
- });
1939
- try {
1940
- await routeFunction(disabledContext);
1941
- } catch (error) {
1942
- if (error instanceof QStashWorkflowAbort && error.stepName === this.disabledMessage) {
1943
- return ok("step-found");
1944
- }
1945
- return err(error);
1946
- }
1947
- return ok("run-ended");
1948
- }
1949
- };
1950
-
1951
2109
  // src/serve/options.ts
1952
- var import_qstash3 = require("@upstash/qstash");
1953
2110
  var import_qstash4 = require("@upstash/qstash");
2111
+ var import_qstash5 = require("@upstash/qstash");
1954
2112
  var processOptions = (options) => {
1955
2113
  const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
1956
2114
  const receiverEnvironmentVariablesSet = Boolean(
1957
2115
  environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
1958
2116
  );
1959
2117
  return {
1960
- qstashClient: new import_qstash4.Client({
2118
+ qstashClient: new import_qstash5.Client({
1961
2119
  baseUrl: environment.QSTASH_URL,
1962
2120
  token: environment.QSTASH_TOKEN
1963
2121
  }),
@@ -1978,7 +2136,7 @@ var processOptions = (options) => {
1978
2136
  throw error;
1979
2137
  }
1980
2138
  },
1981
- receiver: receiverEnvironmentVariablesSet ? new import_qstash3.Receiver({
2139
+ receiver: receiverEnvironmentVariablesSet ? new import_qstash4.Receiver({
1982
2140
  currentSigningKey: environment.QSTASH_CURRENT_SIGNING_KEY,
1983
2141
  nextSigningKey: environment.QSTASH_NEXT_SIGNING_KEY
1984
2142
  }) : void 0,
@@ -2040,6 +2198,9 @@ var serve = (routeFunction, options) => {
2040
2198
  const { rawInitialPayload, steps, isLastDuplicate } = await parseRequest(
2041
2199
  requestPayload,
2042
2200
  isFirstInvocation,
2201
+ workflowRunId,
2202
+ qstashClient.http,
2203
+ request.headers.get("upstash-message-id"),
2043
2204
  debug
2044
2205
  );
2045
2206
  if (isLastDuplicate) {
@@ -2050,6 +2211,7 @@ var serve = (routeFunction, options) => {
2050
2211
  requestPayload,
2051
2212
  qstashClient,
2052
2213
  initialPayloadParser,
2214
+ routeFunction,
2053
2215
  failureFunction
2054
2216
  );
2055
2217
  if (failureCheck.isErr()) {
@@ -2062,7 +2224,6 @@ var serve = (routeFunction, options) => {
2062
2224
  qstashClient,
2063
2225
  workflowRunId,
2064
2226
  initialPayload: initialPayloadParser(rawInitialPayload),
2065
- rawInitialPayload,
2066
2227
  headers: recreateUserHeaders(request.headers),
2067
2228
  steps,
2068
2229
  url: workflowUrl,
@@ -2100,7 +2261,11 @@ var serve = (routeFunction, options) => {
2100
2261
  onStep: async () => routeFunction(workflowContext),
2101
2262
  onCleanup: async () => {
2102
2263
  await triggerWorkflowDelete(workflowContext, debug);
2103
- }
2264
+ },
2265
+ onCancel: async () => {
2266
+ await makeCancelRequest(workflowContext.qstashClient.http, workflowRunId);
2267
+ },
2268
+ debug
2104
2269
  });
2105
2270
  if (result.isErr()) {
2106
2271
  await debug?.log("ERROR", "ERROR", { error: result.error.message });
@@ -2126,7 +2291,7 @@ var serve = (routeFunction, options) => {
2126
2291
  };
2127
2292
 
2128
2293
  // src/client/index.ts
2129
- var import_qstash5 = require("@upstash/qstash");
2294
+ var import_qstash6 = require("@upstash/qstash");
2130
2295
 
2131
2296
  // platforms/astro.ts
2132
2297
  function serve2(routeFunction, options) {