@upstash/workflow 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/h3.js CHANGED
@@ -338,22 +338,33 @@ var H3Response = globalThis.Response;
338
338
 
339
339
  // src/error.ts
340
340
  var import_qstash = require("@upstash/qstash");
341
- var QStashWorkflowError = class extends import_qstash.QstashError {
341
+ var WorkflowError = class extends import_qstash.QstashError {
342
342
  constructor(message) {
343
343
  super(message);
344
- this.name = "QStashWorkflowError";
344
+ this.name = "WorkflowError";
345
345
  }
346
346
  };
347
- var QStashWorkflowAbort = class extends Error {
347
+ var WorkflowAbort = class extends Error {
348
348
  stepInfo;
349
349
  stepName;
350
- 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) {
351
361
  super(
352
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}'.`
353
363
  );
354
- this.name = "QStashWorkflowAbort";
364
+ this.name = "WorkflowAbort";
355
365
  this.stepName = stepName;
356
366
  this.stepInfo = stepInfo;
367
+ this.cancelWorkflow = cancelWorkflow;
357
368
  }
358
369
  };
359
370
  var formatWorkflowError = (error) => {
@@ -375,6 +386,44 @@ var makeNotifyRequest = async (requester, eventId, eventData) => {
375
386
  });
376
387
  return result;
377
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
+ };
378
427
 
379
428
  // src/context/steps.ts
380
429
  var BaseLazyStep = class {
@@ -989,6 +1038,7 @@ var StepTypes = [
989
1038
  ];
990
1039
 
991
1040
  // src/workflow-requests.ts
1041
+ var import_qstash2 = require("@upstash/qstash");
992
1042
  var triggerFirstInvocation = async (workflowContext, retries, debug) => {
993
1043
  const { headers } = getHeaders(
994
1044
  "true",
@@ -999,20 +1049,32 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
999
1049
  workflowContext.failureUrl,
1000
1050
  retries
1001
1051
  );
1002
- await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
1003
- headers,
1004
- requestPayload: workflowContext.requestPayload,
1005
- url: workflowContext.url
1006
- });
1007
1052
  try {
1008
1053
  const body = typeof workflowContext.requestPayload === "string" ? workflowContext.requestPayload : JSON.stringify(workflowContext.requestPayload);
1009
- await workflowContext.qstashClient.publish({
1054
+ const result = await workflowContext.qstashClient.publish({
1010
1055
  headers,
1011
1056
  method: "POST",
1012
1057
  body,
1013
1058
  url: workflowContext.url
1014
1059
  });
1015
- 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
+ }
1016
1078
  } catch (error) {
1017
1079
  const error_ = error;
1018
1080
  return err(error_);
@@ -1020,7 +1082,9 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
1020
1082
  };
1021
1083
  var triggerRouteFunction = async ({
1022
1084
  onCleanup,
1023
- onStep
1085
+ onStep,
1086
+ onCancel,
1087
+ debug
1024
1088
  }) => {
1025
1089
  try {
1026
1090
  await onStep();
@@ -1028,19 +1092,50 @@ var triggerRouteFunction = async ({
1028
1092
  return ok("workflow-finished");
1029
1093
  } catch (error) {
1030
1094
  const error_ = error;
1031
- 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
+ }
1032
1110
  }
1033
1111
  };
1034
1112
  var triggerWorkflowDelete = async (workflowContext, debug, cancel = false) => {
1035
1113
  await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
1036
1114
  deletedWorkflowRunId: workflowContext.workflowRunId
1037
1115
  });
1038
- const result = await workflowContext.qstashClient.http.request({
1039
- path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
1040
- method: "DELETE",
1041
- parseResponseAsJson: false
1042
- });
1043
- 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
+ }
1044
1139
  };
1045
1140
  var recreateUserHeaders = (headers) => {
1046
1141
  const filteredHeaders = new Headers();
@@ -1056,15 +1151,32 @@ var recreateUserHeaders = (headers) => {
1056
1151
  var handleThirdPartyCallResult = async (request, requestPayload, client, workflowUrl, failureUrl, retries, debug) => {
1057
1152
  try {
1058
1153
  if (request.headers.get("Upstash-Workflow-Callback")) {
1059
- 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);
1060
1172
  if (!(callbackMessage.status >= 200 && callbackMessage.status < 300) && callbackMessage.maxRetries && callbackMessage.retried !== callbackMessage.maxRetries) {
1061
1173
  await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
1062
1174
  status: callbackMessage.status,
1063
- body: atob(callbackMessage.body)
1175
+ body: atob(callbackMessage.body ?? "")
1064
1176
  });
1065
1177
  console.warn(
1066
1178
  `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (retried ${callbackMessage.retried ?? 0} out of ${callbackMessage.maxRetries} times). Error Message:
1067
- ${atob(callbackMessage.body)}`
1179
+ ${atob(callbackMessage.body ?? "")}`
1068
1180
  );
1069
1181
  return ok("call-will-retry");
1070
1182
  }
@@ -1098,7 +1210,7 @@ ${atob(callbackMessage.body)}`
1098
1210
  );
1099
1211
  const callResponse = {
1100
1212
  status: callbackMessage.status,
1101
- body: atob(callbackMessage.body),
1213
+ body: atob(callbackMessage.body ?? ""),
1102
1214
  header: callbackMessage.header
1103
1215
  };
1104
1216
  const callResultStep = {
@@ -1129,9 +1241,7 @@ ${atob(callbackMessage.body)}`
1129
1241
  } catch (error) {
1130
1242
  const isCallReturn = request.headers.get("Upstash-Workflow-Callback");
1131
1243
  return err(
1132
- new QStashWorkflowError(
1133
- `Error when handling call return (isCallReturn=${isCallReturn}): ${error}`
1134
- )
1244
+ new WorkflowError(`Error when handling call return (isCallReturn=${isCallReturn}): ${error}`)
1135
1245
  );
1136
1246
  }
1137
1247
  };
@@ -1139,7 +1249,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
1139
1249
  const baseHeaders = {
1140
1250
  [WORKFLOW_INIT_HEADER]: initHeaderValue,
1141
1251
  [WORKFLOW_ID_HEADER]: workflowRunId,
1142
- [WORKFLOW_URL_HEADER]: workflowUrl
1252
+ [WORKFLOW_URL_HEADER]: workflowUrl,
1253
+ [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody"
1143
1254
  };
1144
1255
  if (!step?.callUrl) {
1145
1256
  baseHeaders[`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`] = WORKFLOW_PROTOCOL_VERSION;
@@ -1152,8 +1263,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
1152
1263
  }
1153
1264
  if (step?.callUrl) {
1154
1265
  baseHeaders["Upstash-Retries"] = callRetries?.toString() ?? "0";
1155
- baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete";
1156
- if (retries) {
1266
+ baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete,InitialBody";
1267
+ if (retries !== void 0) {
1157
1268
  baseHeaders["Upstash-Callback-Retries"] = retries.toString();
1158
1269
  baseHeaders["Upstash-Failure-Callback-Retries"] = retries.toString();
1159
1270
  }
@@ -1188,6 +1299,7 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
1188
1299
  "Upstash-Callback-Workflow-CallType": "fromCallback",
1189
1300
  "Upstash-Callback-Workflow-Init": "false",
1190
1301
  "Upstash-Callback-Workflow-Url": workflowUrl,
1302
+ "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody",
1191
1303
  "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
1192
1304
  "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
1193
1305
  "Upstash-Callback-Forward-Upstash-Workflow-StepName": step.stepName,
@@ -1236,7 +1348,7 @@ var verifyRequest = async (body, signature, verifier) => {
1236
1348
  throw new Error("Signature in `Upstash-Signature` header is not valid");
1237
1349
  }
1238
1350
  } catch (error) {
1239
- throw new QStashWorkflowError(
1351
+ throw new WorkflowError(
1240
1352
  `Failed to verify that the Workflow request comes from QStash: ${error}
1241
1353
 
1242
1354
  If signature is missing, trigger the workflow endpoint by publishing your request to QStash instead of calling it directly.
@@ -1276,14 +1388,14 @@ var AutoExecutor = class _AutoExecutor {
1276
1388
  *
1277
1389
  * If a function is already executing (this.executingStep), this
1278
1390
  * means that there is a nested step which is not allowed. In this
1279
- * case, addStep throws QStashWorkflowError.
1391
+ * case, addStep throws WorkflowError.
1280
1392
  *
1281
1393
  * @param stepInfo step plan to add
1282
1394
  * @returns result of the step function
1283
1395
  */
1284
1396
  async addStep(stepInfo) {
1285
1397
  if (this.executingStep) {
1286
- throw new QStashWorkflowError(
1398
+ throw new WorkflowError(
1287
1399
  `A step can not be run inside another step. Tried to run '${stepInfo.stepName}' inside '${this.executingStep}'`
1288
1400
  );
1289
1401
  }
@@ -1368,7 +1480,7 @@ var AutoExecutor = class _AutoExecutor {
1368
1480
  const sortedSteps = sortSteps(this.steps);
1369
1481
  const plannedParallelStepCount = sortedSteps[initialStepCount + this.planStepCount]?.concurrent;
1370
1482
  if (parallelCallState !== "first" && plannedParallelStepCount !== parallelSteps.length) {
1371
- throw new QStashWorkflowError(
1483
+ throw new WorkflowError(
1372
1484
  `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
1373
1485
  );
1374
1486
  }
@@ -1390,7 +1502,7 @@ var AutoExecutor = class _AutoExecutor {
1390
1502
  case "partial": {
1391
1503
  const planStep = this.steps.at(-1);
1392
1504
  if (!planStep || planStep.targetStep === void 0) {
1393
- throw new QStashWorkflowError(
1505
+ throw new WorkflowError(
1394
1506
  `There must be a last step and it should have targetStep larger than 0.Received: ${JSON.stringify(planStep)}`
1395
1507
  );
1396
1508
  }
@@ -1404,17 +1516,17 @@ var AutoExecutor = class _AutoExecutor {
1404
1516
  );
1405
1517
  await this.submitStepsToQStash([resultStep], [parallelStep]);
1406
1518
  } catch (error) {
1407
- if (error instanceof QStashWorkflowAbort) {
1519
+ if (error instanceof WorkflowAbort) {
1408
1520
  throw error;
1409
1521
  }
1410
- throw new QStashWorkflowError(
1522
+ throw new WorkflowError(
1411
1523
  `Error submitting steps to QStash in partial parallel step execution: ${error}`
1412
1524
  );
1413
1525
  }
1414
1526
  break;
1415
1527
  }
1416
1528
  case "discard": {
1417
- throw new QStashWorkflowAbort("discarded parallel");
1529
+ throw new WorkflowAbort("discarded parallel");
1418
1530
  }
1419
1531
  case "last": {
1420
1532
  const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
@@ -1465,7 +1577,7 @@ var AutoExecutor = class _AutoExecutor {
1465
1577
  */
1466
1578
  async submitStepsToQStash(steps, lazySteps) {
1467
1579
  if (steps.length === 0) {
1468
- throw new QStashWorkflowError(
1580
+ throw new WorkflowError(
1469
1581
  `Unable to submit steps to QStash. Provided list is empty. Current step: ${this.stepCount}`
1470
1582
  );
1471
1583
  }
@@ -1505,7 +1617,7 @@ var AutoExecutor = class _AutoExecutor {
1505
1617
  method: "POST",
1506
1618
  parseResponseAsJson: false
1507
1619
  });
1508
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
1620
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
1509
1621
  }
1510
1622
  const result = await this.context.qstashClient.batchJSON(
1511
1623
  steps.map((singleStep, index) => {
@@ -1557,7 +1669,7 @@ var AutoExecutor = class _AutoExecutor {
1557
1669
  };
1558
1670
  })
1559
1671
  });
1560
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
1672
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
1561
1673
  }
1562
1674
  /**
1563
1675
  * Get the promise by executing the lazt steps list. If there is a single
@@ -1582,7 +1694,7 @@ var AutoExecutor = class _AutoExecutor {
1582
1694
  } else if (Array.isArray(result) && lazyStepList.length === result.length && index < lazyStepList.length) {
1583
1695
  return result[index];
1584
1696
  } else {
1585
- throw new QStashWorkflowError(
1697
+ throw new WorkflowError(
1586
1698
  `Unexpected parallel call result while executing step ${index}: '${result}'. Expected ${lazyStepList.length} many items`
1587
1699
  );
1588
1700
  }
@@ -1594,12 +1706,12 @@ var AutoExecutor = class _AutoExecutor {
1594
1706
  };
1595
1707
  var validateStep = (lazyStep, stepFromRequest) => {
1596
1708
  if (lazyStep.stepName !== stepFromRequest.stepName) {
1597
- throw new QStashWorkflowError(
1709
+ throw new WorkflowError(
1598
1710
  `Incompatible step name. Expected '${lazyStep.stepName}', got '${stepFromRequest.stepName}' from the request`
1599
1711
  );
1600
1712
  }
1601
1713
  if (lazyStep.stepType !== stepFromRequest.stepType) {
1602
- throw new QStashWorkflowError(
1714
+ throw new WorkflowError(
1603
1715
  `Incompatible step type. Expected '${lazyStep.stepType}', got '${stepFromRequest.stepType}' from the request`
1604
1716
  );
1605
1717
  }
@@ -1610,12 +1722,12 @@ var validateParallelSteps = (lazySteps, stepsFromRequest) => {
1610
1722
  validateStep(lazySteps[index], stepFromRequest);
1611
1723
  }
1612
1724
  } catch (error) {
1613
- if (error instanceof QStashWorkflowError) {
1725
+ if (error instanceof WorkflowError) {
1614
1726
  const lazyStepNames = lazySteps.map((lazyStep) => lazyStep.stepName);
1615
1727
  const lazyStepTypes = lazySteps.map((lazyStep) => lazyStep.stepType);
1616
1728
  const requestStepNames = stepsFromRequest.map((step) => step.stepName);
1617
1729
  const requestStepTypes = stepsFromRequest.map((step) => step.stepType);
1618
- throw new QStashWorkflowError(
1730
+ throw new WorkflowError(
1619
1731
  `Incompatible steps detected in parallel execution: ${error.message}
1620
1732
  > Step Names from the request: ${JSON.stringify(requestStepNames)}
1621
1733
  Step Types from the request: ${JSON.stringify(requestStepTypes)}
@@ -1728,10 +1840,6 @@ var WorkflowContext = class {
1728
1840
  * headers of the initial request
1729
1841
  */
1730
1842
  headers;
1731
- /**
1732
- * initial payload as a raw string
1733
- */
1734
- rawInitialPayload;
1735
1843
  /**
1736
1844
  * Map of environment variables and their values.
1737
1845
  *
@@ -1766,7 +1874,6 @@ var WorkflowContext = class {
1766
1874
  failureUrl,
1767
1875
  debug,
1768
1876
  initialPayload,
1769
- rawInitialPayload,
1770
1877
  env,
1771
1878
  retries
1772
1879
  }) {
@@ -1777,7 +1884,6 @@ var WorkflowContext = class {
1777
1884
  this.failureUrl = failureUrl;
1778
1885
  this.headers = headers;
1779
1886
  this.requestPayload = initialPayload;
1780
- this.rawInitialPayload = rawInitialPayload ?? JSON.stringify(this.requestPayload);
1781
1887
  this.env = env ?? {};
1782
1888
  this.retries = retries ?? DEFAULT_RETRIES;
1783
1889
  this.executor = new AutoExecutor(this, this.steps, debug);
@@ -1798,7 +1904,7 @@ var WorkflowContext = class {
1798
1904
  * const [result1, result2] = await Promise.all([
1799
1905
  * context.run("step 1", () => {
1800
1906
  * return "result1"
1801
- * })
1907
+ * }),
1802
1908
  * context.run("step 2", async () => {
1803
1909
  * return await fetchResults()
1804
1910
  * })
@@ -1816,6 +1922,10 @@ var WorkflowContext = class {
1816
1922
  /**
1817
1923
  * Stops the execution for the duration provided.
1818
1924
  *
1925
+ * ```typescript
1926
+ * await context.sleep('sleep1', 3) // wait for three seconds
1927
+ * ```
1928
+ *
1819
1929
  * @param stepName
1820
1930
  * @param duration sleep duration in seconds
1821
1931
  * @returns undefined
@@ -1826,6 +1936,10 @@ var WorkflowContext = class {
1826
1936
  /**
1827
1937
  * Stops the execution until the date time provided.
1828
1938
  *
1939
+ * ```typescript
1940
+ * await context.sleepUntil('sleep1', Date.now() / 1000 + 3) // wait for three seconds
1941
+ * ```
1942
+ *
1829
1943
  * @param stepName
1830
1944
  * @param datetime time to sleep until. Can be provided as a number (in unix seconds),
1831
1945
  * as a Date object or a string (passed to `new Date(datetimeString)`)
@@ -1849,7 +1963,7 @@ var WorkflowContext = class {
1849
1963
  * const { status, body } = await context.call<string>(
1850
1964
  * "post call step",
1851
1965
  * {
1852
- * url: `https://www.some-endpoint.com/api`,
1966
+ * url: "https://www.some-endpoint.com/api",
1853
1967
  * method: "POST",
1854
1968
  * body: "my-payload"
1855
1969
  * }
@@ -1903,45 +2017,43 @@ var WorkflowContext = class {
1903
2017
  }
1904
2018
  }
1905
2019
  /**
1906
- * Makes the workflow run wait until a notify request is sent or until the
1907
- * timeout ends
2020
+ * Pauses workflow execution until a specific event occurs or a timeout is reached.
1908
2021
  *
1909
- * ```ts
1910
- * const { eventData, timeout } = await context.waitForEvent(
1911
- * "wait for event step",
1912
- * "my-event-id",
1913
- * 100 // timeout after 100 seconds
1914
- * );
1915
- * ```
2022
+ *```ts
2023
+ * const result = await workflow.waitForEvent("payment-confirmed", {
2024
+ * timeout: "5m"
2025
+ * });
2026
+ *```
1916
2027
  *
1917
- * To notify a waiting workflow run, you can use the notify method:
2028
+ * To notify a waiting workflow:
1918
2029
  *
1919
2030
  * ```ts
1920
2031
  * import { Client } from "@upstash/workflow";
1921
2032
  *
1922
- * const client = new Client({ token: });
2033
+ * const client = new Client({ token: "<QSTASH_TOKEN>" });
1923
2034
  *
1924
2035
  * await client.notify({
1925
- * eventId: "my-event-id",
1926
- * eventData: "eventData"
2036
+ * eventId: "payment.confirmed",
2037
+ * data: {
2038
+ * amount: 99.99,
2039
+ * currency: "USD"
2040
+ * }
1927
2041
  * })
1928
2042
  * ```
1929
2043
  *
2044
+ * Alternatively, you can use the `context.notify` method.
2045
+ *
1930
2046
  * @param stepName
1931
- * @param eventId event id to wake up the waiting workflow run
1932
- * @param timeout timeout duration in seconds
1933
- * @returns wait response as `{ timeout: boolean, eventData: unknown }`.
1934
- * timeout is true if the wait times out, if notified it is false. eventData
1935
- * 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.
1936
2052
  */
1937
- async waitForEvent(stepName, eventId, timeout) {
1938
- const result = await this.addStep(
1939
- new LazyWaitForEventStep(
1940
- stepName,
1941
- eventId,
1942
- typeof timeout === "string" ? timeout : `${timeout}s`
1943
- )
1944
- );
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));
1945
2057
  try {
1946
2058
  return {
1947
2059
  ...result,
@@ -1951,6 +2063,27 @@ var WorkflowContext = class {
1951
2063
  return result;
1952
2064
  }
1953
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
+ */
1954
2087
  async notify(stepName, eventId, eventData) {
1955
2088
  const result = await this.addStep(
1956
2089
  new LazyNotifyStep(stepName, eventId, eventData, this.qstashClient.http)
@@ -1964,6 +2097,15 @@ var WorkflowContext = class {
1964
2097
  return result;
1965
2098
  }
1966
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
+ }
1967
2109
  /**
1968
2110
  * Adds steps to the executor. Needed so that it can be overwritten in
1969
2111
  * DisabledWorkflowContext.
@@ -2004,7 +2146,8 @@ var WorkflowLogger = class _WorkflowLogger {
2004
2146
  }
2005
2147
  writeToConsole(logEntry) {
2006
2148
  const JSON_SPACING = 2;
2007
- 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));
2008
2151
  }
2009
2152
  shouldLog(level) {
2010
2153
  return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
@@ -2046,6 +2189,63 @@ function decodeBase64(base64) {
2046
2189
  }
2047
2190
  }
2048
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
+
2049
2249
  // src/workflow-parser.ts
2050
2250
  var getPayload = async (request) => {
2051
2251
  try {
@@ -2054,8 +2254,8 @@ var getPayload = async (request) => {
2054
2254
  return;
2055
2255
  }
2056
2256
  };
2057
- var parsePayload = async (rawPayload, debug) => {
2058
- const [encodedInitialPayload, ...encodedSteps] = JSON.parse(rawPayload);
2257
+ var processRawSteps = (rawSteps) => {
2258
+ const [encodedInitialPayload, ...encodedSteps] = rawSteps;
2059
2259
  const rawInitialPayload = decodeBase64(encodedInitialPayload.body);
2060
2260
  const initialStep = {
2061
2261
  stepId: 0,
@@ -2065,27 +2265,21 @@ var parsePayload = async (rawPayload, debug) => {
2065
2265
  concurrent: NO_CONCURRENCY
2066
2266
  };
2067
2267
  const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
2068
- const otherSteps = await Promise.all(
2069
- stepsToDecode.map(async (rawStep) => {
2070
- const step = JSON.parse(decodeBase64(rawStep.body));
2071
- try {
2072
- step.out = JSON.parse(step.out);
2073
- } catch {
2074
- await debug?.log("WARN", "ENDPOINT_START", {
2075
- message: "failed while parsing out field of step",
2076
- step
2077
- });
2078
- }
2079
- if (step.waitEventId) {
2080
- const newOut = {
2081
- eventData: step.out ? decodeBase64(step.out) : void 0,
2082
- timeout: step.waitTimeout ?? false
2083
- };
2084
- step.out = newOut;
2085
- }
2086
- return step;
2087
- })
2088
- );
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
+ });
2089
2283
  const steps = [initialStep, ...otherSteps];
2090
2284
  return {
2091
2285
  rawInitialPayload,
@@ -2133,20 +2327,20 @@ var validateRequest = (request) => {
2133
2327
  const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
2134
2328
  const isFirstInvocation = !versionHeader;
2135
2329
  if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
2136
- throw new QStashWorkflowError(
2330
+ throw new WorkflowError(
2137
2331
  `Incompatible workflow sdk protocol version. Expected ${WORKFLOW_PROTOCOL_VERSION}, got ${versionHeader} from the request.`
2138
2332
  );
2139
2333
  }
2140
2334
  const workflowRunId = isFirstInvocation ? getWorkflowRunId() : request.headers.get(WORKFLOW_ID_HEADER) ?? "";
2141
2335
  if (workflowRunId.length === 0) {
2142
- throw new QStashWorkflowError("Couldn't get workflow id from header");
2336
+ throw new WorkflowError("Couldn't get workflow id from header");
2143
2337
  }
2144
2338
  return {
2145
2339
  isFirstInvocation,
2146
2340
  workflowRunId
2147
2341
  };
2148
2342
  };
2149
- var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
2343
+ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requester, messageId, debug) => {
2150
2344
  if (isFirstInvocation) {
2151
2345
  return {
2152
2346
  rawInitialPayload: requestPayload ?? "",
@@ -2154,10 +2348,18 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
2154
2348
  isLastDuplicate: false
2155
2349
  };
2156
2350
  } else {
2351
+ let rawSteps;
2157
2352
  if (!requestPayload) {
2158
- 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);
2159
2361
  }
2160
- const { rawInitialPayload, steps } = await parsePayload(requestPayload, debug);
2362
+ const { rawInitialPayload, steps } = processRawSteps(rawSteps);
2161
2363
  const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
2162
2364
  const deduplicatedSteps = deduplicateSteps(steps);
2163
2365
  return {
@@ -2167,13 +2369,13 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
2167
2369
  };
2168
2370
  }
2169
2371
  };
2170
- var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, failureFunction, debug) => {
2372
+ var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, routeFunction, failureFunction, debug) => {
2171
2373
  if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
2172
2374
  return ok("not-failure-callback");
2173
2375
  }
2174
2376
  if (!failureFunction) {
2175
2377
  return err(
2176
- new QStashWorkflowError(
2378
+ new WorkflowError(
2177
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."
2178
2380
  )
2179
2381
  );
@@ -2184,92 +2386,48 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
2184
2386
  );
2185
2387
  const decodedBody = body ? decodeBase64(body) : "{}";
2186
2388
  const errorPayload = JSON.parse(decodedBody);
2187
- const {
2188
- rawInitialPayload,
2189
- steps,
2190
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2191
- isLastDuplicate: _isLastDuplicate
2192
- } = await parseRequest(decodeBase64(sourceBody), false, debug);
2193
2389
  const workflowContext = new WorkflowContext({
2194
2390
  qstashClient,
2195
2391
  workflowRunId,
2196
- initialPayload: initialPayloadParser(rawInitialPayload),
2197
- rawInitialPayload,
2392
+ initialPayload: initialPayloadParser(decodeBase64(sourceBody)),
2198
2393
  headers: recreateUserHeaders(new Headers(sourceHeader)),
2199
- steps,
2394
+ steps: [],
2200
2395
  url,
2201
2396
  failureUrl: url,
2202
2397
  debug
2203
2398
  });
2204
- 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
+ });
2205
2415
  } catch (error) {
2206
2416
  return err(error);
2207
2417
  }
2208
2418
  return ok("is-failure-callback");
2209
2419
  };
2210
2420
 
2211
- // src/serve/authorization.ts
2212
- var import_qstash2 = require("@upstash/qstash");
2213
- var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
2214
- static disabledMessage = "disabled-qstash-worklfow-run";
2215
- /**
2216
- * overwrite the WorkflowContext.addStep method to always raise QStashWorkflowAbort
2217
- * error in order to stop the execution whenever we encounter a step.
2218
- *
2219
- * @param _step
2220
- */
2221
- async addStep(_step) {
2222
- throw new QStashWorkflowAbort(_DisabledWorkflowContext.disabledMessage);
2223
- }
2224
- /**
2225
- * copies the passed context to create a DisabledWorkflowContext. Then, runs the
2226
- * route function with the new context.
2227
- *
2228
- * - returns "run-ended" if there are no steps found or
2229
- * if the auth failed and user called `return`
2230
- * - returns "step-found" if DisabledWorkflowContext.addStep is called.
2231
- * - if there is another error, returns the error.
2232
- *
2233
- * @param routeFunction
2234
- */
2235
- static async tryAuthentication(routeFunction, context) {
2236
- const disabledContext = new _DisabledWorkflowContext({
2237
- qstashClient: new import_qstash2.Client({
2238
- baseUrl: "disabled-client",
2239
- token: "disabled-client"
2240
- }),
2241
- workflowRunId: context.workflowRunId,
2242
- headers: context.headers,
2243
- steps: [],
2244
- url: context.url,
2245
- failureUrl: context.failureUrl,
2246
- initialPayload: context.requestPayload,
2247
- rawInitialPayload: context.rawInitialPayload,
2248
- env: context.env,
2249
- retries: context.retries
2250
- });
2251
- try {
2252
- await routeFunction(disabledContext);
2253
- } catch (error) {
2254
- if (error instanceof QStashWorkflowAbort && error.stepName === this.disabledMessage) {
2255
- return ok("step-found");
2256
- }
2257
- return err(error);
2258
- }
2259
- return ok("run-ended");
2260
- }
2261
- };
2262
-
2263
2421
  // src/serve/options.ts
2264
- var import_qstash3 = require("@upstash/qstash");
2265
2422
  var import_qstash4 = require("@upstash/qstash");
2423
+ var import_qstash5 = require("@upstash/qstash");
2266
2424
  var processOptions = (options) => {
2267
2425
  const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
2268
2426
  const receiverEnvironmentVariablesSet = Boolean(
2269
2427
  environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
2270
2428
  );
2271
2429
  return {
2272
- qstashClient: new import_qstash4.Client({
2430
+ qstashClient: new import_qstash5.Client({
2273
2431
  baseUrl: environment.QSTASH_URL,
2274
2432
  token: environment.QSTASH_TOKEN
2275
2433
  }),
@@ -2290,7 +2448,7 @@ var processOptions = (options) => {
2290
2448
  throw error;
2291
2449
  }
2292
2450
  },
2293
- receiver: receiverEnvironmentVariablesSet ? new import_qstash3.Receiver({
2451
+ receiver: receiverEnvironmentVariablesSet ? new import_qstash4.Receiver({
2294
2452
  currentSigningKey: environment.QSTASH_CURRENT_SIGNING_KEY,
2295
2453
  nextSigningKey: environment.QSTASH_NEXT_SIGNING_KEY
2296
2454
  }) : void 0,
@@ -2352,6 +2510,9 @@ var serve = (routeFunction, options) => {
2352
2510
  const { rawInitialPayload, steps, isLastDuplicate } = await parseRequest(
2353
2511
  requestPayload,
2354
2512
  isFirstInvocation,
2513
+ workflowRunId,
2514
+ qstashClient.http,
2515
+ request.headers.get("upstash-message-id"),
2355
2516
  debug
2356
2517
  );
2357
2518
  if (isLastDuplicate) {
@@ -2362,6 +2523,7 @@ var serve = (routeFunction, options) => {
2362
2523
  requestPayload,
2363
2524
  qstashClient,
2364
2525
  initialPayloadParser,
2526
+ routeFunction,
2365
2527
  failureFunction
2366
2528
  );
2367
2529
  if (failureCheck.isErr()) {
@@ -2374,7 +2536,6 @@ var serve = (routeFunction, options) => {
2374
2536
  qstashClient,
2375
2537
  workflowRunId,
2376
2538
  initialPayload: initialPayloadParser(rawInitialPayload),
2377
- rawInitialPayload,
2378
2539
  headers: recreateUserHeaders(request.headers),
2379
2540
  steps,
2380
2541
  url: workflowUrl,
@@ -2412,7 +2573,11 @@ var serve = (routeFunction, options) => {
2412
2573
  onStep: async () => routeFunction(workflowContext),
2413
2574
  onCleanup: async () => {
2414
2575
  await triggerWorkflowDelete(workflowContext, debug);
2415
- }
2576
+ },
2577
+ onCancel: async () => {
2578
+ await makeCancelRequest(workflowContext.qstashClient.http, workflowRunId);
2579
+ },
2580
+ debug
2416
2581
  });
2417
2582
  if (result.isErr()) {
2418
2583
  await debug?.log("ERROR", "ERROR", { error: result.error.message });
@@ -2438,7 +2603,7 @@ var serve = (routeFunction, options) => {
2438
2603
  };
2439
2604
 
2440
2605
  // src/client/index.ts
2441
- var import_qstash5 = require("@upstash/qstash");
2606
+ var import_qstash6 = require("@upstash/qstash");
2442
2607
 
2443
2608
  // platforms/h3.ts
2444
2609
  function transformHeaders(headers) {