@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/h3.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/h3.ts
@@ -348,22 +338,33 @@ var H3Response = globalThis.Response;
348
338
 
349
339
  // src/error.ts
350
340
  var import_qstash = require("@upstash/qstash");
351
- var QStashWorkflowError = class extends import_qstash.QstashError {
341
+ var WorkflowError = class extends import_qstash.QstashError {
352
342
  constructor(message) {
353
343
  super(message);
354
- this.name = "QStashWorkflowError";
344
+ this.name = "WorkflowError";
355
345
  }
356
346
  };
357
- var QStashWorkflowAbort = class extends Error {
347
+ var WorkflowAbort = class extends Error {
358
348
  stepInfo;
359
349
  stepName;
360
- constructor(stepName, stepInfo) {
350
+ /**
351
+ * whether workflow is to be canceled on abort
352
+ */
353
+ cancelWorkflow;
354
+ /**
355
+ *
356
+ * @param stepName name of the aborting step
357
+ * @param stepInfo step information
358
+ * @param cancelWorkflow
359
+ */
360
+ constructor(stepName, stepInfo, cancelWorkflow = false) {
361
361
  super(
362
362
  `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}'.`
363
363
  );
364
- this.name = "QStashWorkflowAbort";
364
+ this.name = "WorkflowAbort";
365
365
  this.stepName = stepName;
366
366
  this.stepInfo = stepInfo;
367
+ this.cancelWorkflow = cancelWorkflow;
367
368
  }
368
369
  };
369
370
  var formatWorkflowError = (error) => {
@@ -385,6 +386,44 @@ var makeNotifyRequest = async (requester, eventId, eventData) => {
385
386
  });
386
387
  return result;
387
388
  };
389
+ var makeCancelRequest = async (requester, workflowRunId) => {
390
+ await requester.request({
391
+ path: ["v2", "workflows", "runs", `${workflowRunId}?cancel=true`],
392
+ method: "DELETE",
393
+ parseResponseAsJson: false
394
+ });
395
+ return true;
396
+ };
397
+ var getSteps = async (requester, workflowRunId, messageId, debug) => {
398
+ try {
399
+ const steps = await requester.request({
400
+ path: ["v2", "workflows", "runs", workflowRunId],
401
+ parseResponseAsJson: true
402
+ });
403
+ if (!messageId) {
404
+ await debug?.log("INFO", "ENDPOINT_START", {
405
+ message: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
406
+ });
407
+ return steps;
408
+ } else {
409
+ const index = steps.findIndex((item) => item.messageId === messageId);
410
+ if (index === -1) {
411
+ return [];
412
+ }
413
+ const filteredSteps = steps.slice(0, index + 1);
414
+ await debug?.log("INFO", "ENDPOINT_START", {
415
+ message: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
416
+ });
417
+ return filteredSteps;
418
+ }
419
+ } catch (error) {
420
+ await debug?.log("ERROR", "ERROR", {
421
+ message: "failed while fetching steps.",
422
+ error
423
+ });
424
+ throw new WorkflowError(`Failed while pulling steps. ${error}`);
425
+ }
426
+ };
388
427
 
389
428
  // src/context/steps.ts
390
429
  var BaseLazyStep = class {
@@ -999,6 +1038,7 @@ var StepTypes = [
999
1038
  ];
1000
1039
 
1001
1040
  // src/workflow-requests.ts
1041
+ var import_qstash2 = require("@upstash/qstash");
1002
1042
  var triggerFirstInvocation = async (workflowContext, retries, debug) => {
1003
1043
  const { headers } = getHeaders(
1004
1044
  "true",
@@ -1009,20 +1049,32 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
1009
1049
  workflowContext.failureUrl,
1010
1050
  retries
1011
1051
  );
1012
- await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
1013
- headers,
1014
- requestPayload: workflowContext.requestPayload,
1015
- url: workflowContext.url
1016
- });
1017
1052
  try {
1018
1053
  const body = typeof workflowContext.requestPayload === "string" ? workflowContext.requestPayload : JSON.stringify(workflowContext.requestPayload);
1019
- await workflowContext.qstashClient.publish({
1054
+ const result = await workflowContext.qstashClient.publish({
1020
1055
  headers,
1021
1056
  method: "POST",
1022
1057
  body,
1023
1058
  url: workflowContext.url
1024
1059
  });
1025
- return ok("success");
1060
+ if (result.deduplicated) {
1061
+ await debug?.log("WARN", "SUBMIT_FIRST_INVOCATION", {
1062
+ message: `Workflow run ${workflowContext.workflowRunId} already exists. A new one isn't created.`,
1063
+ headers,
1064
+ requestPayload: workflowContext.requestPayload,
1065
+ url: workflowContext.url,
1066
+ messageId: result.messageId
1067
+ });
1068
+ return ok("workflow-run-already-exists");
1069
+ } else {
1070
+ await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
1071
+ headers,
1072
+ requestPayload: workflowContext.requestPayload,
1073
+ url: workflowContext.url,
1074
+ messageId: result.messageId
1075
+ });
1076
+ return ok("success");
1077
+ }
1026
1078
  } catch (error) {
1027
1079
  const error_ = error;
1028
1080
  return err(error_);
@@ -1030,7 +1082,9 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
1030
1082
  };
1031
1083
  var triggerRouteFunction = async ({
1032
1084
  onCleanup,
1033
- onStep
1085
+ onStep,
1086
+ onCancel,
1087
+ debug
1034
1088
  }) => {
1035
1089
  try {
1036
1090
  await onStep();
@@ -1038,19 +1092,50 @@ var triggerRouteFunction = async ({
1038
1092
  return ok("workflow-finished");
1039
1093
  } catch (error) {
1040
1094
  const error_ = error;
1041
- return error_ instanceof QStashWorkflowAbort ? ok("step-finished") : err(error_);
1095
+ if (error instanceof import_qstash2.QstashError && error.status === 400) {
1096
+ await debug?.log("WARN", "RESPONSE_WORKFLOW", {
1097
+ message: `tried to append to a cancelled workflow. exiting without publishing.`,
1098
+ name: error.name,
1099
+ errorMessage: error.message
1100
+ });
1101
+ return ok("workflow-was-finished");
1102
+ } else if (!(error_ instanceof WorkflowAbort)) {
1103
+ return err(error_);
1104
+ } else if (error_.cancelWorkflow) {
1105
+ await onCancel();
1106
+ return ok("workflow-finished");
1107
+ } else {
1108
+ return ok("step-finished");
1109
+ }
1042
1110
  }
1043
1111
  };
1044
1112
  var triggerWorkflowDelete = async (workflowContext, debug, cancel = false) => {
1045
1113
  await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
1046
1114
  deletedWorkflowRunId: workflowContext.workflowRunId
1047
1115
  });
1048
- const result = await workflowContext.qstashClient.http.request({
1049
- path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
1050
- method: "DELETE",
1051
- parseResponseAsJson: false
1052
- });
1053
- await debug?.log("SUBMIT", "SUBMIT_CLEANUP", result);
1116
+ try {
1117
+ await workflowContext.qstashClient.http.request({
1118
+ path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
1119
+ method: "DELETE",
1120
+ parseResponseAsJson: false
1121
+ });
1122
+ await debug?.log(
1123
+ "SUBMIT",
1124
+ "SUBMIT_CLEANUP",
1125
+ `workflow run ${workflowContext.workflowRunId} deleted.`
1126
+ );
1127
+ return { deleted: true };
1128
+ } catch (error) {
1129
+ if (error instanceof import_qstash2.QstashError && error.status === 404) {
1130
+ await debug?.log("WARN", "SUBMIT_CLEANUP", {
1131
+ message: `Failed to remove workflow run ${workflowContext.workflowRunId} as it doesn't exist.`,
1132
+ name: error.name,
1133
+ errorMessage: error.message
1134
+ });
1135
+ return { deleted: false };
1136
+ }
1137
+ throw error;
1138
+ }
1054
1139
  };
1055
1140
  var recreateUserHeaders = (headers) => {
1056
1141
  const filteredHeaders = new Headers();
@@ -1066,15 +1151,32 @@ var recreateUserHeaders = (headers) => {
1066
1151
  var handleThirdPartyCallResult = async (request, requestPayload, client, workflowUrl, failureUrl, retries, debug) => {
1067
1152
  try {
1068
1153
  if (request.headers.get("Upstash-Workflow-Callback")) {
1069
- const callbackMessage = JSON.parse(requestPayload);
1154
+ let callbackPayload;
1155
+ if (requestPayload) {
1156
+ callbackPayload = requestPayload;
1157
+ } else {
1158
+ const workflowRunId2 = request.headers.get("upstash-workflow-runid");
1159
+ const messageId = request.headers.get("upstash-message-id");
1160
+ if (!workflowRunId2)
1161
+ throw new WorkflowError("workflow run id missing in context.call lazy fetch.");
1162
+ if (!messageId) throw new WorkflowError("message id missing in context.call lazy fetch.");
1163
+ const steps = await getSteps(client.http, workflowRunId2, messageId, debug);
1164
+ const failingStep = steps.find((step) => step.messageId === messageId);
1165
+ if (!failingStep)
1166
+ throw new WorkflowError(
1167
+ "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.`)
1168
+ );
1169
+ callbackPayload = atob(failingStep.body);
1170
+ }
1171
+ const callbackMessage = JSON.parse(callbackPayload);
1070
1172
  if (!(callbackMessage.status >= 200 && callbackMessage.status < 300) && callbackMessage.maxRetries && callbackMessage.retried !== callbackMessage.maxRetries) {
1071
1173
  await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
1072
1174
  status: callbackMessage.status,
1073
- body: atob(callbackMessage.body)
1175
+ body: atob(callbackMessage.body ?? "")
1074
1176
  });
1075
1177
  console.warn(
1076
1178
  `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (retried ${callbackMessage.retried ?? 0} out of ${callbackMessage.maxRetries} times). Error Message:
1077
- ${atob(callbackMessage.body)}`
1179
+ ${atob(callbackMessage.body ?? "")}`
1078
1180
  );
1079
1181
  return ok("call-will-retry");
1080
1182
  }
@@ -1108,7 +1210,7 @@ ${atob(callbackMessage.body)}`
1108
1210
  );
1109
1211
  const callResponse = {
1110
1212
  status: callbackMessage.status,
1111
- body: atob(callbackMessage.body),
1213
+ body: atob(callbackMessage.body ?? ""),
1112
1214
  header: callbackMessage.header
1113
1215
  };
1114
1216
  const callResultStep = {
@@ -1139,9 +1241,7 @@ ${atob(callbackMessage.body)}`
1139
1241
  } catch (error) {
1140
1242
  const isCallReturn = request.headers.get("Upstash-Workflow-Callback");
1141
1243
  return err(
1142
- new QStashWorkflowError(
1143
- `Error when handling call return (isCallReturn=${isCallReturn}): ${error}`
1144
- )
1244
+ new WorkflowError(`Error when handling call return (isCallReturn=${isCallReturn}): ${error}`)
1145
1245
  );
1146
1246
  }
1147
1247
  };
@@ -1149,7 +1249,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
1149
1249
  const baseHeaders = {
1150
1250
  [WORKFLOW_INIT_HEADER]: initHeaderValue,
1151
1251
  [WORKFLOW_ID_HEADER]: workflowRunId,
1152
- [WORKFLOW_URL_HEADER]: workflowUrl
1252
+ [WORKFLOW_URL_HEADER]: workflowUrl,
1253
+ [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody"
1153
1254
  };
1154
1255
  if (!step?.callUrl) {
1155
1256
  baseHeaders[`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`] = WORKFLOW_PROTOCOL_VERSION;
@@ -1162,8 +1263,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
1162
1263
  }
1163
1264
  if (step?.callUrl) {
1164
1265
  baseHeaders["Upstash-Retries"] = callRetries?.toString() ?? "0";
1165
- baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete";
1166
- if (retries) {
1266
+ baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete,InitialBody";
1267
+ if (retries !== void 0) {
1167
1268
  baseHeaders["Upstash-Callback-Retries"] = retries.toString();
1168
1269
  baseHeaders["Upstash-Failure-Callback-Retries"] = retries.toString();
1169
1270
  }
@@ -1198,6 +1299,7 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
1198
1299
  "Upstash-Callback-Workflow-CallType": "fromCallback",
1199
1300
  "Upstash-Callback-Workflow-Init": "false",
1200
1301
  "Upstash-Callback-Workflow-Url": workflowUrl,
1302
+ "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody",
1201
1303
  "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
1202
1304
  "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
1203
1305
  "Upstash-Callback-Forward-Upstash-Workflow-StepName": step.stepName,
@@ -1246,7 +1348,7 @@ var verifyRequest = async (body, signature, verifier) => {
1246
1348
  throw new Error("Signature in `Upstash-Signature` header is not valid");
1247
1349
  }
1248
1350
  } catch (error) {
1249
- throw new QStashWorkflowError(
1351
+ throw new WorkflowError(
1250
1352
  `Failed to verify that the Workflow request comes from QStash: ${error}
1251
1353
 
1252
1354
  If signature is missing, trigger the workflow endpoint by publishing your request to QStash instead of calling it directly.
@@ -1286,14 +1388,14 @@ var AutoExecutor = class _AutoExecutor {
1286
1388
  *
1287
1389
  * If a function is already executing (this.executingStep), this
1288
1390
  * means that there is a nested step which is not allowed. In this
1289
- * case, addStep throws QStashWorkflowError.
1391
+ * case, addStep throws WorkflowError.
1290
1392
  *
1291
1393
  * @param stepInfo step plan to add
1292
1394
  * @returns result of the step function
1293
1395
  */
1294
1396
  async addStep(stepInfo) {
1295
1397
  if (this.executingStep) {
1296
- throw new QStashWorkflowError(
1398
+ throw new WorkflowError(
1297
1399
  `A step can not be run inside another step. Tried to run '${stepInfo.stepName}' inside '${this.executingStep}'`
1298
1400
  );
1299
1401
  }
@@ -1378,7 +1480,7 @@ var AutoExecutor = class _AutoExecutor {
1378
1480
  const sortedSteps = sortSteps(this.steps);
1379
1481
  const plannedParallelStepCount = sortedSteps[initialStepCount + this.planStepCount]?.concurrent;
1380
1482
  if (parallelCallState !== "first" && plannedParallelStepCount !== parallelSteps.length) {
1381
- throw new QStashWorkflowError(
1483
+ throw new WorkflowError(
1382
1484
  `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
1383
1485
  );
1384
1486
  }
@@ -1400,7 +1502,7 @@ var AutoExecutor = class _AutoExecutor {
1400
1502
  case "partial": {
1401
1503
  const planStep = this.steps.at(-1);
1402
1504
  if (!planStep || planStep.targetStep === void 0) {
1403
- throw new QStashWorkflowError(
1505
+ throw new WorkflowError(
1404
1506
  `There must be a last step and it should have targetStep larger than 0.Received: ${JSON.stringify(planStep)}`
1405
1507
  );
1406
1508
  }
@@ -1414,17 +1516,17 @@ var AutoExecutor = class _AutoExecutor {
1414
1516
  );
1415
1517
  await this.submitStepsToQStash([resultStep], [parallelStep]);
1416
1518
  } catch (error) {
1417
- if (error instanceof QStashWorkflowAbort) {
1519
+ if (error instanceof WorkflowAbort) {
1418
1520
  throw error;
1419
1521
  }
1420
- throw new QStashWorkflowError(
1522
+ throw new WorkflowError(
1421
1523
  `Error submitting steps to QStash in partial parallel step execution: ${error}`
1422
1524
  );
1423
1525
  }
1424
1526
  break;
1425
1527
  }
1426
1528
  case "discard": {
1427
- throw new QStashWorkflowAbort("discarded parallel");
1529
+ throw new WorkflowAbort("discarded parallel");
1428
1530
  }
1429
1531
  case "last": {
1430
1532
  const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
@@ -1475,7 +1577,7 @@ var AutoExecutor = class _AutoExecutor {
1475
1577
  */
1476
1578
  async submitStepsToQStash(steps, lazySteps) {
1477
1579
  if (steps.length === 0) {
1478
- throw new QStashWorkflowError(
1580
+ throw new WorkflowError(
1479
1581
  `Unable to submit steps to QStash. Provided list is empty. Current step: ${this.stepCount}`
1480
1582
  );
1481
1583
  }
@@ -1515,7 +1617,7 @@ var AutoExecutor = class _AutoExecutor {
1515
1617
  method: "POST",
1516
1618
  parseResponseAsJson: false
1517
1619
  });
1518
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
1620
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
1519
1621
  }
1520
1622
  const result = await this.context.qstashClient.batchJSON(
1521
1623
  steps.map((singleStep, index) => {
@@ -1567,7 +1669,7 @@ var AutoExecutor = class _AutoExecutor {
1567
1669
  };
1568
1670
  })
1569
1671
  });
1570
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
1672
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
1571
1673
  }
1572
1674
  /**
1573
1675
  * Get the promise by executing the lazt steps list. If there is a single
@@ -1592,7 +1694,7 @@ var AutoExecutor = class _AutoExecutor {
1592
1694
  } else if (Array.isArray(result) && lazyStepList.length === result.length && index < lazyStepList.length) {
1593
1695
  return result[index];
1594
1696
  } else {
1595
- throw new QStashWorkflowError(
1697
+ throw new WorkflowError(
1596
1698
  `Unexpected parallel call result while executing step ${index}: '${result}'. Expected ${lazyStepList.length} many items`
1597
1699
  );
1598
1700
  }
@@ -1604,12 +1706,12 @@ var AutoExecutor = class _AutoExecutor {
1604
1706
  };
1605
1707
  var validateStep = (lazyStep, stepFromRequest) => {
1606
1708
  if (lazyStep.stepName !== stepFromRequest.stepName) {
1607
- throw new QStashWorkflowError(
1709
+ throw new WorkflowError(
1608
1710
  `Incompatible step name. Expected '${lazyStep.stepName}', got '${stepFromRequest.stepName}' from the request`
1609
1711
  );
1610
1712
  }
1611
1713
  if (lazyStep.stepType !== stepFromRequest.stepType) {
1612
- throw new QStashWorkflowError(
1714
+ throw new WorkflowError(
1613
1715
  `Incompatible step type. Expected '${lazyStep.stepType}', got '${stepFromRequest.stepType}' from the request`
1614
1716
  );
1615
1717
  }
@@ -1620,12 +1722,12 @@ var validateParallelSteps = (lazySteps, stepsFromRequest) => {
1620
1722
  validateStep(lazySteps[index], stepFromRequest);
1621
1723
  }
1622
1724
  } catch (error) {
1623
- if (error instanceof QStashWorkflowError) {
1725
+ if (error instanceof WorkflowError) {
1624
1726
  const lazyStepNames = lazySteps.map((lazyStep) => lazyStep.stepName);
1625
1727
  const lazyStepTypes = lazySteps.map((lazyStep) => lazyStep.stepType);
1626
1728
  const requestStepNames = stepsFromRequest.map((step) => step.stepName);
1627
1729
  const requestStepTypes = stepsFromRequest.map((step) => step.stepType);
1628
- throw new QStashWorkflowError(
1730
+ throw new WorkflowError(
1629
1731
  `Incompatible steps detected in parallel execution: ${error.message}
1630
1732
  > Step Names from the request: ${JSON.stringify(requestStepNames)}
1631
1733
  Step Types from the request: ${JSON.stringify(requestStepTypes)}
@@ -1738,10 +1840,6 @@ var WorkflowContext = class {
1738
1840
  * headers of the initial request
1739
1841
  */
1740
1842
  headers;
1741
- /**
1742
- * initial payload as a raw string
1743
- */
1744
- rawInitialPayload;
1745
1843
  /**
1746
1844
  * Map of environment variables and their values.
1747
1845
  *
@@ -1776,7 +1874,6 @@ var WorkflowContext = class {
1776
1874
  failureUrl,
1777
1875
  debug,
1778
1876
  initialPayload,
1779
- rawInitialPayload,
1780
1877
  env,
1781
1878
  retries
1782
1879
  }) {
@@ -1787,7 +1884,6 @@ var WorkflowContext = class {
1787
1884
  this.failureUrl = failureUrl;
1788
1885
  this.headers = headers;
1789
1886
  this.requestPayload = initialPayload;
1790
- this.rawInitialPayload = rawInitialPayload ?? JSON.stringify(this.requestPayload);
1791
1887
  this.env = env ?? {};
1792
1888
  this.retries = retries ?? DEFAULT_RETRIES;
1793
1889
  this.executor = new AutoExecutor(this, this.steps, debug);
@@ -1808,7 +1904,7 @@ var WorkflowContext = class {
1808
1904
  * const [result1, result2] = await Promise.all([
1809
1905
  * context.run("step 1", () => {
1810
1906
  * return "result1"
1811
- * })
1907
+ * }),
1812
1908
  * context.run("step 2", async () => {
1813
1909
  * return await fetchResults()
1814
1910
  * })
@@ -1826,6 +1922,10 @@ var WorkflowContext = class {
1826
1922
  /**
1827
1923
  * Stops the execution for the duration provided.
1828
1924
  *
1925
+ * ```typescript
1926
+ * await context.sleep('sleep1', 3) // wait for three seconds
1927
+ * ```
1928
+ *
1829
1929
  * @param stepName
1830
1930
  * @param duration sleep duration in seconds
1831
1931
  * @returns undefined
@@ -1836,6 +1936,10 @@ var WorkflowContext = class {
1836
1936
  /**
1837
1937
  * Stops the execution until the date time provided.
1838
1938
  *
1939
+ * ```typescript
1940
+ * await context.sleepUntil('sleep1', Date.now() / 1000 + 3) // wait for three seconds
1941
+ * ```
1942
+ *
1839
1943
  * @param stepName
1840
1944
  * @param datetime time to sleep until. Can be provided as a number (in unix seconds),
1841
1945
  * as a Date object or a string (passed to `new Date(datetimeString)`)
@@ -1859,7 +1963,7 @@ var WorkflowContext = class {
1859
1963
  * const { status, body } = await context.call<string>(
1860
1964
  * "post call step",
1861
1965
  * {
1862
- * url: `https://www.some-endpoint.com/api`,
1966
+ * url: "https://www.some-endpoint.com/api",
1863
1967
  * method: "POST",
1864
1968
  * body: "my-payload"
1865
1969
  * }
@@ -1913,45 +2017,43 @@ var WorkflowContext = class {
1913
2017
  }
1914
2018
  }
1915
2019
  /**
1916
- * Makes the workflow run wait until a notify request is sent or until the
1917
- * timeout ends
2020
+ * Pauses workflow execution until a specific event occurs or a timeout is reached.
1918
2021
  *
1919
- * ```ts
1920
- * const { eventData, timeout } = await context.waitForEvent(
1921
- * "wait for event step",
1922
- * "my-event-id",
1923
- * 100 // timeout after 100 seconds
1924
- * );
1925
- * ```
2022
+ *```ts
2023
+ * const result = await workflow.waitForEvent("payment-confirmed", {
2024
+ * timeout: "5m"
2025
+ * });
2026
+ *```
1926
2027
  *
1927
- * To notify a waiting workflow run, you can use the notify method:
2028
+ * To notify a waiting workflow:
1928
2029
  *
1929
2030
  * ```ts
1930
2031
  * import { Client } from "@upstash/workflow";
1931
2032
  *
1932
- * const client = new Client({ token: });
2033
+ * const client = new Client({ token: "<QSTASH_TOKEN>" });
1933
2034
  *
1934
2035
  * await client.notify({
1935
- * eventId: "my-event-id",
1936
- * eventData: "eventData"
2036
+ * eventId: "payment.confirmed",
2037
+ * data: {
2038
+ * amount: 99.99,
2039
+ * currency: "USD"
2040
+ * }
1937
2041
  * })
1938
2042
  * ```
1939
2043
  *
2044
+ * Alternatively, you can use the `context.notify` method.
2045
+ *
1940
2046
  * @param stepName
1941
- * @param eventId event id to wake up the waiting workflow run
1942
- * @param timeout timeout duration in seconds
1943
- * @returns wait response as `{ timeout: boolean, eventData: unknown }`.
1944
- * timeout is true if the wait times out, if notified it is false. eventData
1945
- * is the value passed to `client.notify`.
2047
+ * @param eventId - Unique identifier for the event to wait for
2048
+ * @param options - Configuration options.
2049
+ * @returns `{ timeout: boolean, eventData: unknown }`.
2050
+ * The `timeout` property specifies if the workflow has timed out. The `eventData`
2051
+ * is the data passed when notifying this workflow of an event.
1946
2052
  */
1947
- async waitForEvent(stepName, eventId, timeout) {
1948
- const result = await this.addStep(
1949
- new LazyWaitForEventStep(
1950
- stepName,
1951
- eventId,
1952
- typeof timeout === "string" ? timeout : `${timeout}s`
1953
- )
1954
- );
2053
+ async waitForEvent(stepName, eventId, options = {}) {
2054
+ const { timeout = "7d" } = options;
2055
+ const timeoutStr = typeof timeout === "string" ? timeout : `${timeout}s`;
2056
+ const result = await this.addStep(new LazyWaitForEventStep(stepName, eventId, timeoutStr));
1955
2057
  try {
1956
2058
  return {
1957
2059
  ...result,
@@ -1961,6 +2063,27 @@ var WorkflowContext = class {
1961
2063
  return result;
1962
2064
  }
1963
2065
  }
2066
+ /**
2067
+ * Notify workflow runs waiting for an event
2068
+ *
2069
+ * ```ts
2070
+ * const { eventId, eventData, notifyResponse } = await context.notify(
2071
+ * "notify step", "event-id", "event-data"
2072
+ * );
2073
+ * ```
2074
+ *
2075
+ * Upon `context.notify`, the workflow runs waiting for the given eventId (context.waitForEvent)
2076
+ * will receive the given event data and resume execution.
2077
+ *
2078
+ * The response includes the same eventId and eventData. Additionally, there is
2079
+ * a notifyResponse field which contains a list of `Waiter` objects, each corresponding
2080
+ * to a notified workflow run.
2081
+ *
2082
+ * @param stepName
2083
+ * @param eventId event id to notify
2084
+ * @param eventData event data to notify with
2085
+ * @returns notify response which has event id, event data and list of waiters which were notified
2086
+ */
1964
2087
  async notify(stepName, eventId, eventData) {
1965
2088
  const result = await this.addStep(
1966
2089
  new LazyNotifyStep(stepName, eventId, eventData, this.qstashClient.http)
@@ -1974,6 +2097,15 @@ var WorkflowContext = class {
1974
2097
  return result;
1975
2098
  }
1976
2099
  }
2100
+ /**
2101
+ * Cancel the current workflow run
2102
+ *
2103
+ * Will throw WorkflowAbort to stop workflow execution.
2104
+ * Shouldn't be inside try/catch.
2105
+ */
2106
+ async cancel() {
2107
+ throw new WorkflowAbort("cancel", void 0, true);
2108
+ }
1977
2109
  /**
1978
2110
  * Adds steps to the executor. Needed so that it can be overwritten in
1979
2111
  * DisabledWorkflowContext.
@@ -2014,7 +2146,8 @@ var WorkflowLogger = class _WorkflowLogger {
2014
2146
  }
2015
2147
  writeToConsole(logEntry) {
2016
2148
  const JSON_SPACING = 2;
2017
- console.log(JSON.stringify(logEntry, void 0, JSON_SPACING));
2149
+ const logMethod = logEntry.logLevel === "ERROR" ? console.error : logEntry.logLevel === "WARN" ? console.warn : console.log;
2150
+ logMethod(JSON.stringify(logEntry, void 0, JSON_SPACING));
2018
2151
  }
2019
2152
  shouldLog(level) {
2020
2153
  return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
@@ -2032,11 +2165,13 @@ var WorkflowLogger = class _WorkflowLogger {
2032
2165
  };
2033
2166
 
2034
2167
  // src/utils.ts
2035
- var import_node_crypto = __toESM(require("crypto"));
2036
2168
  var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
2037
2169
  var NANOID_LENGTH = 21;
2170
+ function getRandomInt() {
2171
+ return Math.floor(Math.random() * NANOID_CHARS.length);
2172
+ }
2038
2173
  function nanoid() {
2039
- return [...import_node_crypto.default.getRandomValues(new Uint8Array(NANOID_LENGTH))].map((x) => NANOID_CHARS[x % NANOID_CHARS.length]).join("");
2174
+ return Array.from({ length: NANOID_LENGTH }).map(() => NANOID_CHARS[getRandomInt()]).join("");
2040
2175
  }
2041
2176
  function getWorkflowRunId(id) {
2042
2177
  return `wfr_${id ?? nanoid()}`;
@@ -2054,6 +2189,63 @@ function decodeBase64(base64) {
2054
2189
  }
2055
2190
  }
2056
2191
 
2192
+ // src/serve/authorization.ts
2193
+ var import_qstash3 = require("@upstash/qstash");
2194
+ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
2195
+ static disabledMessage = "disabled-qstash-worklfow-run";
2196
+ /**
2197
+ * overwrite the WorkflowContext.addStep method to always raise WorkflowAbort
2198
+ * error in order to stop the execution whenever we encounter a step.
2199
+ *
2200
+ * @param _step
2201
+ */
2202
+ async addStep(_step) {
2203
+ throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
2204
+ }
2205
+ /**
2206
+ * overwrite cancel method to do nothing
2207
+ */
2208
+ async cancel() {
2209
+ return;
2210
+ }
2211
+ /**
2212
+ * copies the passed context to create a DisabledWorkflowContext. Then, runs the
2213
+ * route function with the new context.
2214
+ *
2215
+ * - returns "run-ended" if there are no steps found or
2216
+ * if the auth failed and user called `return`
2217
+ * - returns "step-found" if DisabledWorkflowContext.addStep is called.
2218
+ * - if there is another error, returns the error.
2219
+ *
2220
+ * @param routeFunction
2221
+ */
2222
+ static async tryAuthentication(routeFunction, context) {
2223
+ const disabledContext = new _DisabledWorkflowContext({
2224
+ qstashClient: new import_qstash3.Client({
2225
+ baseUrl: "disabled-client",
2226
+ token: "disabled-client"
2227
+ }),
2228
+ workflowRunId: context.workflowRunId,
2229
+ headers: context.headers,
2230
+ steps: [],
2231
+ url: context.url,
2232
+ failureUrl: context.failureUrl,
2233
+ initialPayload: context.requestPayload,
2234
+ env: context.env,
2235
+ retries: context.retries
2236
+ });
2237
+ try {
2238
+ await routeFunction(disabledContext);
2239
+ } catch (error) {
2240
+ if (error instanceof WorkflowAbort && error.stepName === this.disabledMessage) {
2241
+ return ok("step-found");
2242
+ }
2243
+ return err(error);
2244
+ }
2245
+ return ok("run-ended");
2246
+ }
2247
+ };
2248
+
2057
2249
  // src/workflow-parser.ts
2058
2250
  var getPayload = async (request) => {
2059
2251
  try {
@@ -2062,8 +2254,8 @@ var getPayload = async (request) => {
2062
2254
  return;
2063
2255
  }
2064
2256
  };
2065
- var parsePayload = async (rawPayload, debug) => {
2066
- const [encodedInitialPayload, ...encodedSteps] = JSON.parse(rawPayload);
2257
+ var processRawSteps = (rawSteps) => {
2258
+ const [encodedInitialPayload, ...encodedSteps] = rawSteps;
2067
2259
  const rawInitialPayload = decodeBase64(encodedInitialPayload.body);
2068
2260
  const initialStep = {
2069
2261
  stepId: 0,
@@ -2073,27 +2265,21 @@ var parsePayload = async (rawPayload, debug) => {
2073
2265
  concurrent: NO_CONCURRENCY
2074
2266
  };
2075
2267
  const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
2076
- const otherSteps = await Promise.all(
2077
- stepsToDecode.map(async (rawStep) => {
2078
- const step = JSON.parse(decodeBase64(rawStep.body));
2079
- try {
2080
- step.out = JSON.parse(step.out);
2081
- } catch {
2082
- await debug?.log("WARN", "ENDPOINT_START", {
2083
- message: "failed while parsing out field of step",
2084
- step
2085
- });
2086
- }
2087
- if (step.waitEventId) {
2088
- const newOut = {
2089
- eventData: step.out ? decodeBase64(step.out) : void 0,
2090
- timeout: step.waitTimeout ?? false
2091
- };
2092
- step.out = newOut;
2093
- }
2094
- return step;
2095
- })
2096
- );
2268
+ const otherSteps = stepsToDecode.map((rawStep) => {
2269
+ const step = JSON.parse(decodeBase64(rawStep.body));
2270
+ try {
2271
+ step.out = JSON.parse(step.out);
2272
+ } catch {
2273
+ }
2274
+ if (step.waitEventId) {
2275
+ const newOut = {
2276
+ eventData: step.out ? decodeBase64(step.out) : void 0,
2277
+ timeout: step.waitTimeout ?? false
2278
+ };
2279
+ step.out = newOut;
2280
+ }
2281
+ return step;
2282
+ });
2097
2283
  const steps = [initialStep, ...otherSteps];
2098
2284
  return {
2099
2285
  rawInitialPayload,
@@ -2141,20 +2327,20 @@ var validateRequest = (request) => {
2141
2327
  const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
2142
2328
  const isFirstInvocation = !versionHeader;
2143
2329
  if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
2144
- throw new QStashWorkflowError(
2330
+ throw new WorkflowError(
2145
2331
  `Incompatible workflow sdk protocol version. Expected ${WORKFLOW_PROTOCOL_VERSION}, got ${versionHeader} from the request.`
2146
2332
  );
2147
2333
  }
2148
2334
  const workflowRunId = isFirstInvocation ? getWorkflowRunId() : request.headers.get(WORKFLOW_ID_HEADER) ?? "";
2149
2335
  if (workflowRunId.length === 0) {
2150
- throw new QStashWorkflowError("Couldn't get workflow id from header");
2336
+ throw new WorkflowError("Couldn't get workflow id from header");
2151
2337
  }
2152
2338
  return {
2153
2339
  isFirstInvocation,
2154
2340
  workflowRunId
2155
2341
  };
2156
2342
  };
2157
- var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
2343
+ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requester, messageId, debug) => {
2158
2344
  if (isFirstInvocation) {
2159
2345
  return {
2160
2346
  rawInitialPayload: requestPayload ?? "",
@@ -2162,10 +2348,18 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
2162
2348
  isLastDuplicate: false
2163
2349
  };
2164
2350
  } else {
2351
+ let rawSteps;
2165
2352
  if (!requestPayload) {
2166
- throw new QStashWorkflowError("Only first call can have an empty body");
2353
+ await debug?.log(
2354
+ "INFO",
2355
+ "ENDPOINT_START",
2356
+ "request payload is empty, steps will be fetched from QStash."
2357
+ );
2358
+ rawSteps = await getSteps(requester, workflowRunId, messageId, debug);
2359
+ } else {
2360
+ rawSteps = JSON.parse(requestPayload);
2167
2361
  }
2168
- const { rawInitialPayload, steps } = await parsePayload(requestPayload, debug);
2362
+ const { rawInitialPayload, steps } = processRawSteps(rawSteps);
2169
2363
  const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
2170
2364
  const deduplicatedSteps = deduplicateSteps(steps);
2171
2365
  return {
@@ -2175,13 +2369,13 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
2175
2369
  };
2176
2370
  }
2177
2371
  };
2178
- var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, failureFunction, debug) => {
2372
+ var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, routeFunction, failureFunction, debug) => {
2179
2373
  if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
2180
2374
  return ok("not-failure-callback");
2181
2375
  }
2182
2376
  if (!failureFunction) {
2183
2377
  return err(
2184
- new QStashWorkflowError(
2378
+ new WorkflowError(
2185
2379
  "Workflow endpoint is called to handle a failure, but a failureFunction is not provided in serve options. Either provide a failureUrl or a failureFunction."
2186
2380
  )
2187
2381
  );
@@ -2192,92 +2386,48 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
2192
2386
  );
2193
2387
  const decodedBody = body ? decodeBase64(body) : "{}";
2194
2388
  const errorPayload = JSON.parse(decodedBody);
2195
- const {
2196
- rawInitialPayload,
2197
- steps,
2198
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2199
- isLastDuplicate: _isLastDuplicate
2200
- } = await parseRequest(decodeBase64(sourceBody), false, debug);
2201
2389
  const workflowContext = new WorkflowContext({
2202
2390
  qstashClient,
2203
2391
  workflowRunId,
2204
- initialPayload: initialPayloadParser(rawInitialPayload),
2205
- rawInitialPayload,
2392
+ initialPayload: initialPayloadParser(decodeBase64(sourceBody)),
2206
2393
  headers: recreateUserHeaders(new Headers(sourceHeader)),
2207
- steps,
2394
+ steps: [],
2208
2395
  url,
2209
2396
  failureUrl: url,
2210
2397
  debug
2211
2398
  });
2212
- await failureFunction(workflowContext, status, errorPayload.message, header);
2399
+ const authCheck = await DisabledWorkflowContext.tryAuthentication(
2400
+ routeFunction,
2401
+ workflowContext
2402
+ );
2403
+ if (authCheck.isErr()) {
2404
+ await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
2405
+ throw authCheck.error;
2406
+ } else if (authCheck.value === "run-ended") {
2407
+ return err(new WorkflowError("Not authorized to run the failure function."));
2408
+ }
2409
+ await failureFunction({
2410
+ context: workflowContext,
2411
+ failStatus: status,
2412
+ failResponse: errorPayload.message,
2413
+ failHeaders: header
2414
+ });
2213
2415
  } catch (error) {
2214
2416
  return err(error);
2215
2417
  }
2216
2418
  return ok("is-failure-callback");
2217
2419
  };
2218
2420
 
2219
- // src/serve/authorization.ts
2220
- var import_qstash2 = require("@upstash/qstash");
2221
- var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
2222
- static disabledMessage = "disabled-qstash-worklfow-run";
2223
- /**
2224
- * overwrite the WorkflowContext.addStep method to always raise QStashWorkflowAbort
2225
- * error in order to stop the execution whenever we encounter a step.
2226
- *
2227
- * @param _step
2228
- */
2229
- async addStep(_step) {
2230
- throw new QStashWorkflowAbort(_DisabledWorkflowContext.disabledMessage);
2231
- }
2232
- /**
2233
- * copies the passed context to create a DisabledWorkflowContext. Then, runs the
2234
- * route function with the new context.
2235
- *
2236
- * - returns "run-ended" if there are no steps found or
2237
- * if the auth failed and user called `return`
2238
- * - returns "step-found" if DisabledWorkflowContext.addStep is called.
2239
- * - if there is another error, returns the error.
2240
- *
2241
- * @param routeFunction
2242
- */
2243
- static async tryAuthentication(routeFunction, context) {
2244
- const disabledContext = new _DisabledWorkflowContext({
2245
- qstashClient: new import_qstash2.Client({
2246
- baseUrl: "disabled-client",
2247
- token: "disabled-client"
2248
- }),
2249
- workflowRunId: context.workflowRunId,
2250
- headers: context.headers,
2251
- steps: [],
2252
- url: context.url,
2253
- failureUrl: context.failureUrl,
2254
- initialPayload: context.requestPayload,
2255
- rawInitialPayload: context.rawInitialPayload,
2256
- env: context.env,
2257
- retries: context.retries
2258
- });
2259
- try {
2260
- await routeFunction(disabledContext);
2261
- } catch (error) {
2262
- if (error instanceof QStashWorkflowAbort && error.stepName === this.disabledMessage) {
2263
- return ok("step-found");
2264
- }
2265
- return err(error);
2266
- }
2267
- return ok("run-ended");
2268
- }
2269
- };
2270
-
2271
2421
  // src/serve/options.ts
2272
- var import_qstash3 = require("@upstash/qstash");
2273
2422
  var import_qstash4 = require("@upstash/qstash");
2423
+ var import_qstash5 = require("@upstash/qstash");
2274
2424
  var processOptions = (options) => {
2275
2425
  const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
2276
2426
  const receiverEnvironmentVariablesSet = Boolean(
2277
2427
  environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
2278
2428
  );
2279
2429
  return {
2280
- qstashClient: new import_qstash4.Client({
2430
+ qstashClient: new import_qstash5.Client({
2281
2431
  baseUrl: environment.QSTASH_URL,
2282
2432
  token: environment.QSTASH_TOKEN
2283
2433
  }),
@@ -2298,7 +2448,7 @@ var processOptions = (options) => {
2298
2448
  throw error;
2299
2449
  }
2300
2450
  },
2301
- receiver: receiverEnvironmentVariablesSet ? new import_qstash3.Receiver({
2451
+ receiver: receiverEnvironmentVariablesSet ? new import_qstash4.Receiver({
2302
2452
  currentSigningKey: environment.QSTASH_CURRENT_SIGNING_KEY,
2303
2453
  nextSigningKey: environment.QSTASH_NEXT_SIGNING_KEY
2304
2454
  }) : void 0,
@@ -2360,6 +2510,9 @@ var serve = (routeFunction, options) => {
2360
2510
  const { rawInitialPayload, steps, isLastDuplicate } = await parseRequest(
2361
2511
  requestPayload,
2362
2512
  isFirstInvocation,
2513
+ workflowRunId,
2514
+ qstashClient.http,
2515
+ request.headers.get("upstash-message-id"),
2363
2516
  debug
2364
2517
  );
2365
2518
  if (isLastDuplicate) {
@@ -2370,6 +2523,7 @@ var serve = (routeFunction, options) => {
2370
2523
  requestPayload,
2371
2524
  qstashClient,
2372
2525
  initialPayloadParser,
2526
+ routeFunction,
2373
2527
  failureFunction
2374
2528
  );
2375
2529
  if (failureCheck.isErr()) {
@@ -2382,7 +2536,6 @@ var serve = (routeFunction, options) => {
2382
2536
  qstashClient,
2383
2537
  workflowRunId,
2384
2538
  initialPayload: initialPayloadParser(rawInitialPayload),
2385
- rawInitialPayload,
2386
2539
  headers: recreateUserHeaders(request.headers),
2387
2540
  steps,
2388
2541
  url: workflowUrl,
@@ -2420,7 +2573,11 @@ var serve = (routeFunction, options) => {
2420
2573
  onStep: async () => routeFunction(workflowContext),
2421
2574
  onCleanup: async () => {
2422
2575
  await triggerWorkflowDelete(workflowContext, debug);
2423
- }
2576
+ },
2577
+ onCancel: async () => {
2578
+ await makeCancelRequest(workflowContext.qstashClient.http, workflowRunId);
2579
+ },
2580
+ debug
2424
2581
  });
2425
2582
  if (result.isErr()) {
2426
2583
  await debug?.log("ERROR", "ERROR", { error: result.error.message });
@@ -2446,7 +2603,7 @@ var serve = (routeFunction, options) => {
2446
2603
  };
2447
2604
 
2448
2605
  // src/client/index.ts
2449
- var import_qstash5 = require("@upstash/qstash");
2606
+ var import_qstash6 = require("@upstash/qstash");
2450
2607
 
2451
2608
  // platforms/h3.ts
2452
2609
  function transformHeaders(headers) {