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