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