@upstash/workflow 0.1.4 → 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
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // platforms/nextjs.ts
@@ -37,22 +27,33 @@ module.exports = __toCommonJS(nextjs_exports);
37
27
 
38
28
  // src/error.ts
39
29
  var import_qstash = require("@upstash/qstash");
40
- var QStashWorkflowError = class extends import_qstash.QstashError {
30
+ var WorkflowError = class extends import_qstash.QstashError {
41
31
  constructor(message) {
42
32
  super(message);
43
- this.name = "QStashWorkflowError";
33
+ this.name = "WorkflowError";
44
34
  }
45
35
  };
46
- var QStashWorkflowAbort = class extends Error {
36
+ var WorkflowAbort = class extends Error {
47
37
  stepInfo;
48
38
  stepName;
49
- 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) {
50
50
  super(
51
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}'.`
52
52
  );
53
- this.name = "QStashWorkflowAbort";
53
+ this.name = "WorkflowAbort";
54
54
  this.stepName = stepName;
55
55
  this.stepInfo = stepInfo;
56
+ this.cancelWorkflow = cancelWorkflow;
56
57
  }
57
58
  };
58
59
  var formatWorkflowError = (error) => {
@@ -74,6 +75,44 @@ var makeNotifyRequest = async (requester, eventId, eventData) => {
74
75
  });
75
76
  return result;
76
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
+ };
77
116
 
78
117
  // src/context/steps.ts
79
118
  var BaseLazyStep = class {
@@ -688,6 +727,7 @@ var StepTypes = [
688
727
  ];
689
728
 
690
729
  // src/workflow-requests.ts
730
+ var import_qstash2 = require("@upstash/qstash");
691
731
  var triggerFirstInvocation = async (workflowContext, retries, debug) => {
692
732
  const { headers } = getHeaders(
693
733
  "true",
@@ -698,20 +738,32 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
698
738
  workflowContext.failureUrl,
699
739
  retries
700
740
  );
701
- await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
702
- headers,
703
- requestPayload: workflowContext.requestPayload,
704
- url: workflowContext.url
705
- });
706
741
  try {
707
742
  const body = typeof workflowContext.requestPayload === "string" ? workflowContext.requestPayload : JSON.stringify(workflowContext.requestPayload);
708
- await workflowContext.qstashClient.publish({
743
+ const result = await workflowContext.qstashClient.publish({
709
744
  headers,
710
745
  method: "POST",
711
746
  body,
712
747
  url: workflowContext.url
713
748
  });
714
- 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
+ }
715
767
  } catch (error) {
716
768
  const error_ = error;
717
769
  return err(error_);
@@ -719,7 +771,9 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
719
771
  };
720
772
  var triggerRouteFunction = async ({
721
773
  onCleanup,
722
- onStep
774
+ onStep,
775
+ onCancel,
776
+ debug
723
777
  }) => {
724
778
  try {
725
779
  await onStep();
@@ -727,19 +781,50 @@ var triggerRouteFunction = async ({
727
781
  return ok("workflow-finished");
728
782
  } catch (error) {
729
783
  const error_ = error;
730
- 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
+ }
731
799
  }
732
800
  };
733
801
  var triggerWorkflowDelete = async (workflowContext, debug, cancel = false) => {
734
802
  await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
735
803
  deletedWorkflowRunId: workflowContext.workflowRunId
736
804
  });
737
- const result = await workflowContext.qstashClient.http.request({
738
- path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
739
- method: "DELETE",
740
- parseResponseAsJson: false
741
- });
742
- 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
+ }
743
828
  };
744
829
  var recreateUserHeaders = (headers) => {
745
830
  const filteredHeaders = new Headers();
@@ -755,15 +840,32 @@ var recreateUserHeaders = (headers) => {
755
840
  var handleThirdPartyCallResult = async (request, requestPayload, client, workflowUrl, failureUrl, retries, debug) => {
756
841
  try {
757
842
  if (request.headers.get("Upstash-Workflow-Callback")) {
758
- 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);
759
861
  if (!(callbackMessage.status >= 200 && callbackMessage.status < 300) && callbackMessage.maxRetries && callbackMessage.retried !== callbackMessage.maxRetries) {
760
862
  await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
761
863
  status: callbackMessage.status,
762
- body: atob(callbackMessage.body)
864
+ body: atob(callbackMessage.body ?? "")
763
865
  });
764
866
  console.warn(
765
867
  `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (retried ${callbackMessage.retried ?? 0} out of ${callbackMessage.maxRetries} times). Error Message:
766
- ${atob(callbackMessage.body)}`
868
+ ${atob(callbackMessage.body ?? "")}`
767
869
  );
768
870
  return ok("call-will-retry");
769
871
  }
@@ -797,7 +899,7 @@ ${atob(callbackMessage.body)}`
797
899
  );
798
900
  const callResponse = {
799
901
  status: callbackMessage.status,
800
- body: atob(callbackMessage.body),
902
+ body: atob(callbackMessage.body ?? ""),
801
903
  header: callbackMessage.header
802
904
  };
803
905
  const callResultStep = {
@@ -828,9 +930,7 @@ ${atob(callbackMessage.body)}`
828
930
  } catch (error) {
829
931
  const isCallReturn = request.headers.get("Upstash-Workflow-Callback");
830
932
  return err(
831
- new QStashWorkflowError(
832
- `Error when handling call return (isCallReturn=${isCallReturn}): ${error}`
833
- )
933
+ new WorkflowError(`Error when handling call return (isCallReturn=${isCallReturn}): ${error}`)
834
934
  );
835
935
  }
836
936
  };
@@ -838,7 +938,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
838
938
  const baseHeaders = {
839
939
  [WORKFLOW_INIT_HEADER]: initHeaderValue,
840
940
  [WORKFLOW_ID_HEADER]: workflowRunId,
841
- [WORKFLOW_URL_HEADER]: workflowUrl
941
+ [WORKFLOW_URL_HEADER]: workflowUrl,
942
+ [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody"
842
943
  };
843
944
  if (!step?.callUrl) {
844
945
  baseHeaders[`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`] = WORKFLOW_PROTOCOL_VERSION;
@@ -851,8 +952,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
851
952
  }
852
953
  if (step?.callUrl) {
853
954
  baseHeaders["Upstash-Retries"] = callRetries?.toString() ?? "0";
854
- baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete";
855
- if (retries) {
955
+ baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete,InitialBody";
956
+ if (retries !== void 0) {
856
957
  baseHeaders["Upstash-Callback-Retries"] = retries.toString();
857
958
  baseHeaders["Upstash-Failure-Callback-Retries"] = retries.toString();
858
959
  }
@@ -887,6 +988,7 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
887
988
  "Upstash-Callback-Workflow-CallType": "fromCallback",
888
989
  "Upstash-Callback-Workflow-Init": "false",
889
990
  "Upstash-Callback-Workflow-Url": workflowUrl,
991
+ "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody",
890
992
  "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
891
993
  "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
892
994
  "Upstash-Callback-Forward-Upstash-Workflow-StepName": step.stepName,
@@ -935,7 +1037,7 @@ var verifyRequest = async (body, signature, verifier) => {
935
1037
  throw new Error("Signature in `Upstash-Signature` header is not valid");
936
1038
  }
937
1039
  } catch (error) {
938
- throw new QStashWorkflowError(
1040
+ throw new WorkflowError(
939
1041
  `Failed to verify that the Workflow request comes from QStash: ${error}
940
1042
 
941
1043
  If signature is missing, trigger the workflow endpoint by publishing your request to QStash instead of calling it directly.
@@ -975,14 +1077,14 @@ var AutoExecutor = class _AutoExecutor {
975
1077
  *
976
1078
  * If a function is already executing (this.executingStep), this
977
1079
  * means that there is a nested step which is not allowed. In this
978
- * case, addStep throws QStashWorkflowError.
1080
+ * case, addStep throws WorkflowError.
979
1081
  *
980
1082
  * @param stepInfo step plan to add
981
1083
  * @returns result of the step function
982
1084
  */
983
1085
  async addStep(stepInfo) {
984
1086
  if (this.executingStep) {
985
- throw new QStashWorkflowError(
1087
+ throw new WorkflowError(
986
1088
  `A step can not be run inside another step. Tried to run '${stepInfo.stepName}' inside '${this.executingStep}'`
987
1089
  );
988
1090
  }
@@ -1067,7 +1169,7 @@ var AutoExecutor = class _AutoExecutor {
1067
1169
  const sortedSteps = sortSteps(this.steps);
1068
1170
  const plannedParallelStepCount = sortedSteps[initialStepCount + this.planStepCount]?.concurrent;
1069
1171
  if (parallelCallState !== "first" && plannedParallelStepCount !== parallelSteps.length) {
1070
- throw new QStashWorkflowError(
1172
+ throw new WorkflowError(
1071
1173
  `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
1072
1174
  );
1073
1175
  }
@@ -1089,7 +1191,7 @@ var AutoExecutor = class _AutoExecutor {
1089
1191
  case "partial": {
1090
1192
  const planStep = this.steps.at(-1);
1091
1193
  if (!planStep || planStep.targetStep === void 0) {
1092
- throw new QStashWorkflowError(
1194
+ throw new WorkflowError(
1093
1195
  `There must be a last step and it should have targetStep larger than 0.Received: ${JSON.stringify(planStep)}`
1094
1196
  );
1095
1197
  }
@@ -1103,17 +1205,17 @@ var AutoExecutor = class _AutoExecutor {
1103
1205
  );
1104
1206
  await this.submitStepsToQStash([resultStep], [parallelStep]);
1105
1207
  } catch (error) {
1106
- if (error instanceof QStashWorkflowAbort) {
1208
+ if (error instanceof WorkflowAbort) {
1107
1209
  throw error;
1108
1210
  }
1109
- throw new QStashWorkflowError(
1211
+ throw new WorkflowError(
1110
1212
  `Error submitting steps to QStash in partial parallel step execution: ${error}`
1111
1213
  );
1112
1214
  }
1113
1215
  break;
1114
1216
  }
1115
1217
  case "discard": {
1116
- throw new QStashWorkflowAbort("discarded parallel");
1218
+ throw new WorkflowAbort("discarded parallel");
1117
1219
  }
1118
1220
  case "last": {
1119
1221
  const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
@@ -1164,7 +1266,7 @@ var AutoExecutor = class _AutoExecutor {
1164
1266
  */
1165
1267
  async submitStepsToQStash(steps, lazySteps) {
1166
1268
  if (steps.length === 0) {
1167
- throw new QStashWorkflowError(
1269
+ throw new WorkflowError(
1168
1270
  `Unable to submit steps to QStash. Provided list is empty. Current step: ${this.stepCount}`
1169
1271
  );
1170
1272
  }
@@ -1204,7 +1306,7 @@ var AutoExecutor = class _AutoExecutor {
1204
1306
  method: "POST",
1205
1307
  parseResponseAsJson: false
1206
1308
  });
1207
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
1309
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
1208
1310
  }
1209
1311
  const result = await this.context.qstashClient.batchJSON(
1210
1312
  steps.map((singleStep, index) => {
@@ -1256,7 +1358,7 @@ var AutoExecutor = class _AutoExecutor {
1256
1358
  };
1257
1359
  })
1258
1360
  });
1259
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
1361
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
1260
1362
  }
1261
1363
  /**
1262
1364
  * Get the promise by executing the lazt steps list. If there is a single
@@ -1281,7 +1383,7 @@ var AutoExecutor = class _AutoExecutor {
1281
1383
  } else if (Array.isArray(result) && lazyStepList.length === result.length && index < lazyStepList.length) {
1282
1384
  return result[index];
1283
1385
  } else {
1284
- throw new QStashWorkflowError(
1386
+ throw new WorkflowError(
1285
1387
  `Unexpected parallel call result while executing step ${index}: '${result}'. Expected ${lazyStepList.length} many items`
1286
1388
  );
1287
1389
  }
@@ -1293,12 +1395,12 @@ var AutoExecutor = class _AutoExecutor {
1293
1395
  };
1294
1396
  var validateStep = (lazyStep, stepFromRequest) => {
1295
1397
  if (lazyStep.stepName !== stepFromRequest.stepName) {
1296
- throw new QStashWorkflowError(
1398
+ throw new WorkflowError(
1297
1399
  `Incompatible step name. Expected '${lazyStep.stepName}', got '${stepFromRequest.stepName}' from the request`
1298
1400
  );
1299
1401
  }
1300
1402
  if (lazyStep.stepType !== stepFromRequest.stepType) {
1301
- throw new QStashWorkflowError(
1403
+ throw new WorkflowError(
1302
1404
  `Incompatible step type. Expected '${lazyStep.stepType}', got '${stepFromRequest.stepType}' from the request`
1303
1405
  );
1304
1406
  }
@@ -1309,12 +1411,12 @@ var validateParallelSteps = (lazySteps, stepsFromRequest) => {
1309
1411
  validateStep(lazySteps[index], stepFromRequest);
1310
1412
  }
1311
1413
  } catch (error) {
1312
- if (error instanceof QStashWorkflowError) {
1414
+ if (error instanceof WorkflowError) {
1313
1415
  const lazyStepNames = lazySteps.map((lazyStep) => lazyStep.stepName);
1314
1416
  const lazyStepTypes = lazySteps.map((lazyStep) => lazyStep.stepType);
1315
1417
  const requestStepNames = stepsFromRequest.map((step) => step.stepName);
1316
1418
  const requestStepTypes = stepsFromRequest.map((step) => step.stepType);
1317
- throw new QStashWorkflowError(
1419
+ throw new WorkflowError(
1318
1420
  `Incompatible steps detected in parallel execution: ${error.message}
1319
1421
  > Step Names from the request: ${JSON.stringify(requestStepNames)}
1320
1422
  Step Types from the request: ${JSON.stringify(requestStepTypes)}
@@ -1427,10 +1529,6 @@ var WorkflowContext = class {
1427
1529
  * headers of the initial request
1428
1530
  */
1429
1531
  headers;
1430
- /**
1431
- * initial payload as a raw string
1432
- */
1433
- rawInitialPayload;
1434
1532
  /**
1435
1533
  * Map of environment variables and their values.
1436
1534
  *
@@ -1465,7 +1563,6 @@ var WorkflowContext = class {
1465
1563
  failureUrl,
1466
1564
  debug,
1467
1565
  initialPayload,
1468
- rawInitialPayload,
1469
1566
  env,
1470
1567
  retries
1471
1568
  }) {
@@ -1476,7 +1573,6 @@ var WorkflowContext = class {
1476
1573
  this.failureUrl = failureUrl;
1477
1574
  this.headers = headers;
1478
1575
  this.requestPayload = initialPayload;
1479
- this.rawInitialPayload = rawInitialPayload ?? JSON.stringify(this.requestPayload);
1480
1576
  this.env = env ?? {};
1481
1577
  this.retries = retries ?? DEFAULT_RETRIES;
1482
1578
  this.executor = new AutoExecutor(this, this.steps, debug);
@@ -1497,7 +1593,7 @@ var WorkflowContext = class {
1497
1593
  * const [result1, result2] = await Promise.all([
1498
1594
  * context.run("step 1", () => {
1499
1595
  * return "result1"
1500
- * })
1596
+ * }),
1501
1597
  * context.run("step 2", async () => {
1502
1598
  * return await fetchResults()
1503
1599
  * })
@@ -1515,6 +1611,10 @@ var WorkflowContext = class {
1515
1611
  /**
1516
1612
  * Stops the execution for the duration provided.
1517
1613
  *
1614
+ * ```typescript
1615
+ * await context.sleep('sleep1', 3) // wait for three seconds
1616
+ * ```
1617
+ *
1518
1618
  * @param stepName
1519
1619
  * @param duration sleep duration in seconds
1520
1620
  * @returns undefined
@@ -1525,6 +1625,10 @@ var WorkflowContext = class {
1525
1625
  /**
1526
1626
  * Stops the execution until the date time provided.
1527
1627
  *
1628
+ * ```typescript
1629
+ * await context.sleepUntil('sleep1', Date.now() / 1000 + 3) // wait for three seconds
1630
+ * ```
1631
+ *
1528
1632
  * @param stepName
1529
1633
  * @param datetime time to sleep until. Can be provided as a number (in unix seconds),
1530
1634
  * as a Date object or a string (passed to `new Date(datetimeString)`)
@@ -1548,7 +1652,7 @@ var WorkflowContext = class {
1548
1652
  * const { status, body } = await context.call<string>(
1549
1653
  * "post call step",
1550
1654
  * {
1551
- * url: `https://www.some-endpoint.com/api`,
1655
+ * url: "https://www.some-endpoint.com/api",
1552
1656
  * method: "POST",
1553
1657
  * body: "my-payload"
1554
1658
  * }
@@ -1602,45 +1706,43 @@ var WorkflowContext = class {
1602
1706
  }
1603
1707
  }
1604
1708
  /**
1605
- * Makes the workflow run wait until a notify request is sent or until the
1606
- * timeout ends
1709
+ * Pauses workflow execution until a specific event occurs or a timeout is reached.
1607
1710
  *
1608
- * ```ts
1609
- * const { eventData, timeout } = await context.waitForEvent(
1610
- * "wait for event step",
1611
- * "my-event-id",
1612
- * 100 // timeout after 100 seconds
1613
- * );
1614
- * ```
1711
+ *```ts
1712
+ * const result = await workflow.waitForEvent("payment-confirmed", {
1713
+ * timeout: "5m"
1714
+ * });
1715
+ *```
1615
1716
  *
1616
- * To notify a waiting workflow run, you can use the notify method:
1717
+ * To notify a waiting workflow:
1617
1718
  *
1618
1719
  * ```ts
1619
1720
  * import { Client } from "@upstash/workflow";
1620
1721
  *
1621
- * const client = new Client({ token: });
1722
+ * const client = new Client({ token: "<QSTASH_TOKEN>" });
1622
1723
  *
1623
1724
  * await client.notify({
1624
- * eventId: "my-event-id",
1625
- * eventData: "eventData"
1725
+ * eventId: "payment.confirmed",
1726
+ * data: {
1727
+ * amount: 99.99,
1728
+ * currency: "USD"
1729
+ * }
1626
1730
  * })
1627
1731
  * ```
1628
1732
  *
1733
+ * Alternatively, you can use the `context.notify` method.
1734
+ *
1629
1735
  * @param stepName
1630
- * @param eventId event id to wake up the waiting workflow run
1631
- * @param timeout timeout duration in seconds
1632
- * @returns wait response as `{ timeout: boolean, eventData: unknown }`.
1633
- * timeout is true if the wait times out, if notified it is false. eventData
1634
- * 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.
1635
1741
  */
1636
- async waitForEvent(stepName, eventId, timeout) {
1637
- const result = await this.addStep(
1638
- new LazyWaitForEventStep(
1639
- stepName,
1640
- eventId,
1641
- typeof timeout === "string" ? timeout : `${timeout}s`
1642
- )
1643
- );
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));
1644
1746
  try {
1645
1747
  return {
1646
1748
  ...result,
@@ -1650,6 +1752,27 @@ var WorkflowContext = class {
1650
1752
  return result;
1651
1753
  }
1652
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
+ */
1653
1776
  async notify(stepName, eventId, eventData) {
1654
1777
  const result = await this.addStep(
1655
1778
  new LazyNotifyStep(stepName, eventId, eventData, this.qstashClient.http)
@@ -1663,6 +1786,15 @@ var WorkflowContext = class {
1663
1786
  return result;
1664
1787
  }
1665
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
+ }
1666
1798
  /**
1667
1799
  * Adds steps to the executor. Needed so that it can be overwritten in
1668
1800
  * DisabledWorkflowContext.
@@ -1703,7 +1835,8 @@ var WorkflowLogger = class _WorkflowLogger {
1703
1835
  }
1704
1836
  writeToConsole(logEntry) {
1705
1837
  const JSON_SPACING = 2;
1706
- 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));
1707
1840
  }
1708
1841
  shouldLog(level) {
1709
1842
  return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
@@ -1721,11 +1854,13 @@ var WorkflowLogger = class _WorkflowLogger {
1721
1854
  };
1722
1855
 
1723
1856
  // src/utils.ts
1724
- var import_node_crypto = __toESM(require("crypto"));
1725
1857
  var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
1726
1858
  var NANOID_LENGTH = 21;
1859
+ function getRandomInt() {
1860
+ return Math.floor(Math.random() * NANOID_CHARS.length);
1861
+ }
1727
1862
  function nanoid() {
1728
- return [...import_node_crypto.default.getRandomValues(new Uint8Array(NANOID_LENGTH))].map((x) => NANOID_CHARS[x % NANOID_CHARS.length]).join("");
1863
+ return Array.from({ length: NANOID_LENGTH }).map(() => NANOID_CHARS[getRandomInt()]).join("");
1729
1864
  }
1730
1865
  function getWorkflowRunId(id) {
1731
1866
  return `wfr_${id ?? nanoid()}`;
@@ -1743,6 +1878,63 @@ function decodeBase64(base64) {
1743
1878
  }
1744
1879
  }
1745
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
+
1746
1938
  // src/workflow-parser.ts
1747
1939
  var getPayload = async (request) => {
1748
1940
  try {
@@ -1751,8 +1943,8 @@ var getPayload = async (request) => {
1751
1943
  return;
1752
1944
  }
1753
1945
  };
1754
- var parsePayload = async (rawPayload, debug) => {
1755
- const [encodedInitialPayload, ...encodedSteps] = JSON.parse(rawPayload);
1946
+ var processRawSteps = (rawSteps) => {
1947
+ const [encodedInitialPayload, ...encodedSteps] = rawSteps;
1756
1948
  const rawInitialPayload = decodeBase64(encodedInitialPayload.body);
1757
1949
  const initialStep = {
1758
1950
  stepId: 0,
@@ -1762,27 +1954,21 @@ var parsePayload = async (rawPayload, debug) => {
1762
1954
  concurrent: NO_CONCURRENCY
1763
1955
  };
1764
1956
  const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
1765
- const otherSteps = await Promise.all(
1766
- stepsToDecode.map(async (rawStep) => {
1767
- const step = JSON.parse(decodeBase64(rawStep.body));
1768
- try {
1769
- step.out = JSON.parse(step.out);
1770
- } catch {
1771
- await debug?.log("WARN", "ENDPOINT_START", {
1772
- message: "failed while parsing out field of step",
1773
- step
1774
- });
1775
- }
1776
- if (step.waitEventId) {
1777
- const newOut = {
1778
- eventData: step.out ? decodeBase64(step.out) : void 0,
1779
- timeout: step.waitTimeout ?? false
1780
- };
1781
- step.out = newOut;
1782
- }
1783
- return step;
1784
- })
1785
- );
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
+ });
1786
1972
  const steps = [initialStep, ...otherSteps];
1787
1973
  return {
1788
1974
  rawInitialPayload,
@@ -1830,20 +2016,20 @@ var validateRequest = (request) => {
1830
2016
  const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
1831
2017
  const isFirstInvocation = !versionHeader;
1832
2018
  if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
1833
- throw new QStashWorkflowError(
2019
+ throw new WorkflowError(
1834
2020
  `Incompatible workflow sdk protocol version. Expected ${WORKFLOW_PROTOCOL_VERSION}, got ${versionHeader} from the request.`
1835
2021
  );
1836
2022
  }
1837
2023
  const workflowRunId = isFirstInvocation ? getWorkflowRunId() : request.headers.get(WORKFLOW_ID_HEADER) ?? "";
1838
2024
  if (workflowRunId.length === 0) {
1839
- throw new QStashWorkflowError("Couldn't get workflow id from header");
2025
+ throw new WorkflowError("Couldn't get workflow id from header");
1840
2026
  }
1841
2027
  return {
1842
2028
  isFirstInvocation,
1843
2029
  workflowRunId
1844
2030
  };
1845
2031
  };
1846
- var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
2032
+ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requester, messageId, debug) => {
1847
2033
  if (isFirstInvocation) {
1848
2034
  return {
1849
2035
  rawInitialPayload: requestPayload ?? "",
@@ -1851,10 +2037,18 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
1851
2037
  isLastDuplicate: false
1852
2038
  };
1853
2039
  } else {
2040
+ let rawSteps;
1854
2041
  if (!requestPayload) {
1855
- 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);
1856
2050
  }
1857
- const { rawInitialPayload, steps } = await parsePayload(requestPayload, debug);
2051
+ const { rawInitialPayload, steps } = processRawSteps(rawSteps);
1858
2052
  const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
1859
2053
  const deduplicatedSteps = deduplicateSteps(steps);
1860
2054
  return {
@@ -1864,13 +2058,13 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
1864
2058
  };
1865
2059
  }
1866
2060
  };
1867
- var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, failureFunction, debug) => {
2061
+ var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, routeFunction, failureFunction, debug) => {
1868
2062
  if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
1869
2063
  return ok("not-failure-callback");
1870
2064
  }
1871
2065
  if (!failureFunction) {
1872
2066
  return err(
1873
- new QStashWorkflowError(
2067
+ new WorkflowError(
1874
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."
1875
2069
  )
1876
2070
  );
@@ -1881,92 +2075,48 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
1881
2075
  );
1882
2076
  const decodedBody = body ? decodeBase64(body) : "{}";
1883
2077
  const errorPayload = JSON.parse(decodedBody);
1884
- const {
1885
- rawInitialPayload,
1886
- steps,
1887
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1888
- isLastDuplicate: _isLastDuplicate
1889
- } = await parseRequest(decodeBase64(sourceBody), false, debug);
1890
2078
  const workflowContext = new WorkflowContext({
1891
2079
  qstashClient,
1892
2080
  workflowRunId,
1893
- initialPayload: initialPayloadParser(rawInitialPayload),
1894
- rawInitialPayload,
2081
+ initialPayload: initialPayloadParser(decodeBase64(sourceBody)),
1895
2082
  headers: recreateUserHeaders(new Headers(sourceHeader)),
1896
- steps,
2083
+ steps: [],
1897
2084
  url,
1898
2085
  failureUrl: url,
1899
2086
  debug
1900
2087
  });
1901
- 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
+ });
1902
2104
  } catch (error) {
1903
2105
  return err(error);
1904
2106
  }
1905
2107
  return ok("is-failure-callback");
1906
2108
  };
1907
2109
 
1908
- // src/serve/authorization.ts
1909
- var import_qstash2 = require("@upstash/qstash");
1910
- var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
1911
- static disabledMessage = "disabled-qstash-worklfow-run";
1912
- /**
1913
- * overwrite the WorkflowContext.addStep method to always raise QStashWorkflowAbort
1914
- * error in order to stop the execution whenever we encounter a step.
1915
- *
1916
- * @param _step
1917
- */
1918
- async addStep(_step) {
1919
- throw new QStashWorkflowAbort(_DisabledWorkflowContext.disabledMessage);
1920
- }
1921
- /**
1922
- * copies the passed context to create a DisabledWorkflowContext. Then, runs the
1923
- * route function with the new context.
1924
- *
1925
- * - returns "run-ended" if there are no steps found or
1926
- * if the auth failed and user called `return`
1927
- * - returns "step-found" if DisabledWorkflowContext.addStep is called.
1928
- * - if there is another error, returns the error.
1929
- *
1930
- * @param routeFunction
1931
- */
1932
- static async tryAuthentication(routeFunction, context) {
1933
- const disabledContext = new _DisabledWorkflowContext({
1934
- qstashClient: new import_qstash2.Client({
1935
- baseUrl: "disabled-client",
1936
- token: "disabled-client"
1937
- }),
1938
- workflowRunId: context.workflowRunId,
1939
- headers: context.headers,
1940
- steps: [],
1941
- url: context.url,
1942
- failureUrl: context.failureUrl,
1943
- initialPayload: context.requestPayload,
1944
- rawInitialPayload: context.rawInitialPayload,
1945
- env: context.env,
1946
- retries: context.retries
1947
- });
1948
- try {
1949
- await routeFunction(disabledContext);
1950
- } catch (error) {
1951
- if (error instanceof QStashWorkflowAbort && error.stepName === this.disabledMessage) {
1952
- return ok("step-found");
1953
- }
1954
- return err(error);
1955
- }
1956
- return ok("run-ended");
1957
- }
1958
- };
1959
-
1960
2110
  // src/serve/options.ts
1961
- var import_qstash3 = require("@upstash/qstash");
1962
2111
  var import_qstash4 = require("@upstash/qstash");
2112
+ var import_qstash5 = require("@upstash/qstash");
1963
2113
  var processOptions = (options) => {
1964
2114
  const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
1965
2115
  const receiverEnvironmentVariablesSet = Boolean(
1966
2116
  environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
1967
2117
  );
1968
2118
  return {
1969
- qstashClient: new import_qstash4.Client({
2119
+ qstashClient: new import_qstash5.Client({
1970
2120
  baseUrl: environment.QSTASH_URL,
1971
2121
  token: environment.QSTASH_TOKEN
1972
2122
  }),
@@ -1987,7 +2137,7 @@ var processOptions = (options) => {
1987
2137
  throw error;
1988
2138
  }
1989
2139
  },
1990
- receiver: receiverEnvironmentVariablesSet ? new import_qstash3.Receiver({
2140
+ receiver: receiverEnvironmentVariablesSet ? new import_qstash4.Receiver({
1991
2141
  currentSigningKey: environment.QSTASH_CURRENT_SIGNING_KEY,
1992
2142
  nextSigningKey: environment.QSTASH_NEXT_SIGNING_KEY
1993
2143
  }) : void 0,
@@ -2049,6 +2199,9 @@ var serve = (routeFunction, options) => {
2049
2199
  const { rawInitialPayload, steps, isLastDuplicate } = await parseRequest(
2050
2200
  requestPayload,
2051
2201
  isFirstInvocation,
2202
+ workflowRunId,
2203
+ qstashClient.http,
2204
+ request.headers.get("upstash-message-id"),
2052
2205
  debug
2053
2206
  );
2054
2207
  if (isLastDuplicate) {
@@ -2059,6 +2212,7 @@ var serve = (routeFunction, options) => {
2059
2212
  requestPayload,
2060
2213
  qstashClient,
2061
2214
  initialPayloadParser,
2215
+ routeFunction,
2062
2216
  failureFunction
2063
2217
  );
2064
2218
  if (failureCheck.isErr()) {
@@ -2071,7 +2225,6 @@ var serve = (routeFunction, options) => {
2071
2225
  qstashClient,
2072
2226
  workflowRunId,
2073
2227
  initialPayload: initialPayloadParser(rawInitialPayload),
2074
- rawInitialPayload,
2075
2228
  headers: recreateUserHeaders(request.headers),
2076
2229
  steps,
2077
2230
  url: workflowUrl,
@@ -2109,7 +2262,11 @@ var serve = (routeFunction, options) => {
2109
2262
  onStep: async () => routeFunction(workflowContext),
2110
2263
  onCleanup: async () => {
2111
2264
  await triggerWorkflowDelete(workflowContext, debug);
2112
- }
2265
+ },
2266
+ onCancel: async () => {
2267
+ await makeCancelRequest(workflowContext.qstashClient.http, workflowRunId);
2268
+ },
2269
+ debug
2113
2270
  });
2114
2271
  if (result.isErr()) {
2115
2272
  await debug?.log("ERROR", "ERROR", { error: result.error.message });
@@ -2135,7 +2292,7 @@ var serve = (routeFunction, options) => {
2135
2292
  };
2136
2293
 
2137
2294
  // src/client/index.ts
2138
- var import_qstash5 = require("@upstash/qstash");
2295
+ var import_qstash6 = require("@upstash/qstash");
2139
2296
 
2140
2297
  // platforms/nextjs.ts
2141
2298
  var serve2 = (routeFunction, options) => {