@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/express.js CHANGED
@@ -15316,9 +15316,19 @@ var require_object_inspect = __commonJS({
15316
15316
  var utilInspect = require_util_inspect();
15317
15317
  var inspectCustom = utilInspect.custom;
15318
15318
  var inspectSymbol = isSymbol(inspectCustom) ? inspectCustom : null;
15319
+ var quotes = {
15320
+ __proto__: null,
15321
+ "double": '"',
15322
+ single: "'"
15323
+ };
15324
+ var quoteREs = {
15325
+ __proto__: null,
15326
+ "double": /(["\\])/g,
15327
+ single: /(['\\])/g
15328
+ };
15319
15329
  module2.exports = function inspect_(obj, options, depth, seen) {
15320
15330
  var opts = options || {};
15321
- if (has(opts, "quoteStyle") && (opts.quoteStyle !== "single" && opts.quoteStyle !== "double")) {
15331
+ if (has(opts, "quoteStyle") && !has(quotes, opts.quoteStyle)) {
15322
15332
  throw new TypeError('option "quoteStyle" must be "single" or "double"');
15323
15333
  }
15324
15334
  if (has(opts, "maxStringLength") && (typeof opts.maxStringLength === "number" ? opts.maxStringLength < 0 && opts.maxStringLength !== Infinity : opts.maxStringLength !== null)) {
@@ -15499,7 +15509,8 @@ var require_object_inspect = __commonJS({
15499
15509
  return String(obj);
15500
15510
  };
15501
15511
  function wrapQuotes(s, defaultStyle, opts) {
15502
- var quoteChar = (opts.quoteStyle || defaultStyle) === "double" ? '"' : "'";
15512
+ var style = opts.quoteStyle || defaultStyle;
15513
+ var quoteChar = quotes[style];
15503
15514
  return quoteChar + s + quoteChar;
15504
15515
  }
15505
15516
  function quote(s) {
@@ -15674,7 +15685,9 @@ var require_object_inspect = __commonJS({
15674
15685
  var trailer = "... " + remaining + " more character" + (remaining > 1 ? "s" : "");
15675
15686
  return inspectString($slice.call(str, 0, opts.maxStringLength), opts) + trailer;
15676
15687
  }
15677
- var s = $replace.call($replace.call(str, /(['\\])/g, "\\$1"), /[\x00-\x1f]/g, lowbyte);
15688
+ var quoteRE = quoteREs[opts.quoteStyle || "single"];
15689
+ quoteRE.lastIndex = 0;
15690
+ var s = $replace.call($replace.call(str, quoteRE, "\\$1"), /[\x00-\x1f]/g, lowbyte);
15678
15691
  return wrapQuotes(s, "single", opts);
15679
15692
  }
15680
15693
  function lowbyte(c) {
@@ -23549,22 +23562,33 @@ module.exports = __toCommonJS(express_exports);
23549
23562
 
23550
23563
  // src/error.ts
23551
23564
  var import_qstash = require("@upstash/qstash");
23552
- var QStashWorkflowError = class extends import_qstash.QstashError {
23565
+ var WorkflowError = class extends import_qstash.QstashError {
23553
23566
  constructor(message) {
23554
23567
  super(message);
23555
- this.name = "QStashWorkflowError";
23568
+ this.name = "WorkflowError";
23556
23569
  }
23557
23570
  };
23558
- var QStashWorkflowAbort = class extends Error {
23571
+ var WorkflowAbort = class extends Error {
23559
23572
  stepInfo;
23560
23573
  stepName;
23561
- constructor(stepName, stepInfo) {
23574
+ /**
23575
+ * whether workflow is to be canceled on abort
23576
+ */
23577
+ cancelWorkflow;
23578
+ /**
23579
+ *
23580
+ * @param stepName name of the aborting step
23581
+ * @param stepInfo step information
23582
+ * @param cancelWorkflow
23583
+ */
23584
+ constructor(stepName, stepInfo, cancelWorkflow = false) {
23562
23585
  super(
23563
23586
  `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}'.`
23564
23587
  );
23565
- this.name = "QStashWorkflowAbort";
23588
+ this.name = "WorkflowAbort";
23566
23589
  this.stepName = stepName;
23567
23590
  this.stepInfo = stepInfo;
23591
+ this.cancelWorkflow = cancelWorkflow;
23568
23592
  }
23569
23593
  };
23570
23594
  var formatWorkflowError = (error) => {
@@ -23586,6 +23610,44 @@ var makeNotifyRequest = async (requester, eventId, eventData) => {
23586
23610
  });
23587
23611
  return result;
23588
23612
  };
23613
+ var makeCancelRequest = async (requester, workflowRunId) => {
23614
+ await requester.request({
23615
+ path: ["v2", "workflows", "runs", `${workflowRunId}?cancel=true`],
23616
+ method: "DELETE",
23617
+ parseResponseAsJson: false
23618
+ });
23619
+ return true;
23620
+ };
23621
+ var getSteps = async (requester, workflowRunId, messageId, debug) => {
23622
+ try {
23623
+ const steps = await requester.request({
23624
+ path: ["v2", "workflows", "runs", workflowRunId],
23625
+ parseResponseAsJson: true
23626
+ });
23627
+ if (!messageId) {
23628
+ await debug?.log("INFO", "ENDPOINT_START", {
23629
+ message: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
23630
+ });
23631
+ return steps;
23632
+ } else {
23633
+ const index = steps.findIndex((item) => item.messageId === messageId);
23634
+ if (index === -1) {
23635
+ return [];
23636
+ }
23637
+ const filteredSteps = steps.slice(0, index + 1);
23638
+ await debug?.log("INFO", "ENDPOINT_START", {
23639
+ message: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
23640
+ });
23641
+ return filteredSteps;
23642
+ }
23643
+ } catch (error) {
23644
+ await debug?.log("ERROR", "ERROR", {
23645
+ message: "failed while fetching steps.",
23646
+ error
23647
+ });
23648
+ throw new WorkflowError(`Failed while pulling steps. ${error}`);
23649
+ }
23650
+ };
23589
23651
 
23590
23652
  // src/context/steps.ts
23591
23653
  var BaseLazyStep = class {
@@ -24200,6 +24262,7 @@ var StepTypes = [
24200
24262
  ];
24201
24263
 
24202
24264
  // src/workflow-requests.ts
24265
+ var import_qstash2 = require("@upstash/qstash");
24203
24266
  var triggerFirstInvocation = async (workflowContext, retries, debug) => {
24204
24267
  const { headers } = getHeaders(
24205
24268
  "true",
@@ -24210,20 +24273,32 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
24210
24273
  workflowContext.failureUrl,
24211
24274
  retries
24212
24275
  );
24213
- await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
24214
- headers,
24215
- requestPayload: workflowContext.requestPayload,
24216
- url: workflowContext.url
24217
- });
24218
24276
  try {
24219
24277
  const body = typeof workflowContext.requestPayload === "string" ? workflowContext.requestPayload : JSON.stringify(workflowContext.requestPayload);
24220
- await workflowContext.qstashClient.publish({
24278
+ const result = await workflowContext.qstashClient.publish({
24221
24279
  headers,
24222
24280
  method: "POST",
24223
24281
  body,
24224
24282
  url: workflowContext.url
24225
24283
  });
24226
- return ok("success");
24284
+ if (result.deduplicated) {
24285
+ await debug?.log("WARN", "SUBMIT_FIRST_INVOCATION", {
24286
+ message: `Workflow run ${workflowContext.workflowRunId} already exists. A new one isn't created.`,
24287
+ headers,
24288
+ requestPayload: workflowContext.requestPayload,
24289
+ url: workflowContext.url,
24290
+ messageId: result.messageId
24291
+ });
24292
+ return ok("workflow-run-already-exists");
24293
+ } else {
24294
+ await debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
24295
+ headers,
24296
+ requestPayload: workflowContext.requestPayload,
24297
+ url: workflowContext.url,
24298
+ messageId: result.messageId
24299
+ });
24300
+ return ok("success");
24301
+ }
24227
24302
  } catch (error) {
24228
24303
  const error_ = error;
24229
24304
  return err(error_);
@@ -24231,7 +24306,9 @@ var triggerFirstInvocation = async (workflowContext, retries, debug) => {
24231
24306
  };
24232
24307
  var triggerRouteFunction = async ({
24233
24308
  onCleanup,
24234
- onStep
24309
+ onStep,
24310
+ onCancel,
24311
+ debug
24235
24312
  }) => {
24236
24313
  try {
24237
24314
  await onStep();
@@ -24239,19 +24316,50 @@ var triggerRouteFunction = async ({
24239
24316
  return ok("workflow-finished");
24240
24317
  } catch (error) {
24241
24318
  const error_ = error;
24242
- return error_ instanceof QStashWorkflowAbort ? ok("step-finished") : err(error_);
24319
+ if (error instanceof import_qstash2.QstashError && error.status === 400) {
24320
+ await debug?.log("WARN", "RESPONSE_WORKFLOW", {
24321
+ message: `tried to append to a cancelled workflow. exiting without publishing.`,
24322
+ name: error.name,
24323
+ errorMessage: error.message
24324
+ });
24325
+ return ok("workflow-was-finished");
24326
+ } else if (!(error_ instanceof WorkflowAbort)) {
24327
+ return err(error_);
24328
+ } else if (error_.cancelWorkflow) {
24329
+ await onCancel();
24330
+ return ok("workflow-finished");
24331
+ } else {
24332
+ return ok("step-finished");
24333
+ }
24243
24334
  }
24244
24335
  };
24245
24336
  var triggerWorkflowDelete = async (workflowContext, debug, cancel = false) => {
24246
24337
  await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
24247
24338
  deletedWorkflowRunId: workflowContext.workflowRunId
24248
24339
  });
24249
- const result = await workflowContext.qstashClient.http.request({
24250
- path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
24251
- method: "DELETE",
24252
- parseResponseAsJson: false
24253
- });
24254
- await debug?.log("SUBMIT", "SUBMIT_CLEANUP", result);
24340
+ try {
24341
+ await workflowContext.qstashClient.http.request({
24342
+ path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
24343
+ method: "DELETE",
24344
+ parseResponseAsJson: false
24345
+ });
24346
+ await debug?.log(
24347
+ "SUBMIT",
24348
+ "SUBMIT_CLEANUP",
24349
+ `workflow run ${workflowContext.workflowRunId} deleted.`
24350
+ );
24351
+ return { deleted: true };
24352
+ } catch (error) {
24353
+ if (error instanceof import_qstash2.QstashError && error.status === 404) {
24354
+ await debug?.log("WARN", "SUBMIT_CLEANUP", {
24355
+ message: `Failed to remove workflow run ${workflowContext.workflowRunId} as it doesn't exist.`,
24356
+ name: error.name,
24357
+ errorMessage: error.message
24358
+ });
24359
+ return { deleted: false };
24360
+ }
24361
+ throw error;
24362
+ }
24255
24363
  };
24256
24364
  var recreateUserHeaders = (headers) => {
24257
24365
  const filteredHeaders = new Headers();
@@ -24267,15 +24375,32 @@ var recreateUserHeaders = (headers) => {
24267
24375
  var handleThirdPartyCallResult = async (request, requestPayload, client, workflowUrl, failureUrl, retries, debug) => {
24268
24376
  try {
24269
24377
  if (request.headers.get("Upstash-Workflow-Callback")) {
24270
- const callbackMessage = JSON.parse(requestPayload);
24378
+ let callbackPayload;
24379
+ if (requestPayload) {
24380
+ callbackPayload = requestPayload;
24381
+ } else {
24382
+ const workflowRunId2 = request.headers.get("upstash-workflow-runid");
24383
+ const messageId = request.headers.get("upstash-message-id");
24384
+ if (!workflowRunId2)
24385
+ throw new WorkflowError("workflow run id missing in context.call lazy fetch.");
24386
+ if (!messageId) throw new WorkflowError("message id missing in context.call lazy fetch.");
24387
+ const steps = await getSteps(client.http, workflowRunId2, messageId, debug);
24388
+ const failingStep = steps.find((step) => step.messageId === messageId);
24389
+ if (!failingStep)
24390
+ throw new WorkflowError(
24391
+ "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.`)
24392
+ );
24393
+ callbackPayload = atob(failingStep.body);
24394
+ }
24395
+ const callbackMessage = JSON.parse(callbackPayload);
24271
24396
  if (!(callbackMessage.status >= 200 && callbackMessage.status < 300) && callbackMessage.maxRetries && callbackMessage.retried !== callbackMessage.maxRetries) {
24272
24397
  await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
24273
24398
  status: callbackMessage.status,
24274
- body: atob(callbackMessage.body)
24399
+ body: atob(callbackMessage.body ?? "")
24275
24400
  });
24276
24401
  console.warn(
24277
24402
  `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (retried ${callbackMessage.retried ?? 0} out of ${callbackMessage.maxRetries} times). Error Message:
24278
- ${atob(callbackMessage.body)}`
24403
+ ${atob(callbackMessage.body ?? "")}`
24279
24404
  );
24280
24405
  return ok("call-will-retry");
24281
24406
  }
@@ -24309,7 +24434,7 @@ ${atob(callbackMessage.body)}`
24309
24434
  );
24310
24435
  const callResponse = {
24311
24436
  status: callbackMessage.status,
24312
- body: atob(callbackMessage.body),
24437
+ body: atob(callbackMessage.body ?? ""),
24313
24438
  header: callbackMessage.header
24314
24439
  };
24315
24440
  const callResultStep = {
@@ -24340,9 +24465,7 @@ ${atob(callbackMessage.body)}`
24340
24465
  } catch (error) {
24341
24466
  const isCallReturn = request.headers.get("Upstash-Workflow-Callback");
24342
24467
  return err(
24343
- new QStashWorkflowError(
24344
- `Error when handling call return (isCallReturn=${isCallReturn}): ${error}`
24345
- )
24468
+ new WorkflowError(`Error when handling call return (isCallReturn=${isCallReturn}): ${error}`)
24346
24469
  );
24347
24470
  }
24348
24471
  };
@@ -24350,7 +24473,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
24350
24473
  const baseHeaders = {
24351
24474
  [WORKFLOW_INIT_HEADER]: initHeaderValue,
24352
24475
  [WORKFLOW_ID_HEADER]: workflowRunId,
24353
- [WORKFLOW_URL_HEADER]: workflowUrl
24476
+ [WORKFLOW_URL_HEADER]: workflowUrl,
24477
+ [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody"
24354
24478
  };
24355
24479
  if (!step?.callUrl) {
24356
24480
  baseHeaders[`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`] = WORKFLOW_PROTOCOL_VERSION;
@@ -24363,8 +24487,8 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
24363
24487
  }
24364
24488
  if (step?.callUrl) {
24365
24489
  baseHeaders["Upstash-Retries"] = callRetries?.toString() ?? "0";
24366
- baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete";
24367
- if (retries) {
24490
+ baseHeaders[WORKFLOW_FEATURE_HEADER] = "WF_NoDelete,InitialBody";
24491
+ if (retries !== void 0) {
24368
24492
  baseHeaders["Upstash-Callback-Retries"] = retries.toString();
24369
24493
  baseHeaders["Upstash-Failure-Callback-Retries"] = retries.toString();
24370
24494
  }
@@ -24399,6 +24523,7 @@ var getHeaders = (initHeaderValue, workflowRunId, workflowUrl, userHeaders, step
24399
24523
  "Upstash-Callback-Workflow-CallType": "fromCallback",
24400
24524
  "Upstash-Callback-Workflow-Init": "false",
24401
24525
  "Upstash-Callback-Workflow-Url": workflowUrl,
24526
+ "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody",
24402
24527
  "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
24403
24528
  "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
24404
24529
  "Upstash-Callback-Forward-Upstash-Workflow-StepName": step.stepName,
@@ -24447,7 +24572,7 @@ var verifyRequest = async (body, signature, verifier) => {
24447
24572
  throw new Error("Signature in `Upstash-Signature` header is not valid");
24448
24573
  }
24449
24574
  } catch (error) {
24450
- throw new QStashWorkflowError(
24575
+ throw new WorkflowError(
24451
24576
  `Failed to verify that the Workflow request comes from QStash: ${error}
24452
24577
 
24453
24578
  If signature is missing, trigger the workflow endpoint by publishing your request to QStash instead of calling it directly.
@@ -24487,14 +24612,14 @@ var AutoExecutor = class _AutoExecutor {
24487
24612
  *
24488
24613
  * If a function is already executing (this.executingStep), this
24489
24614
  * means that there is a nested step which is not allowed. In this
24490
- * case, addStep throws QStashWorkflowError.
24615
+ * case, addStep throws WorkflowError.
24491
24616
  *
24492
24617
  * @param stepInfo step plan to add
24493
24618
  * @returns result of the step function
24494
24619
  */
24495
24620
  async addStep(stepInfo) {
24496
24621
  if (this.executingStep) {
24497
- throw new QStashWorkflowError(
24622
+ throw new WorkflowError(
24498
24623
  `A step can not be run inside another step. Tried to run '${stepInfo.stepName}' inside '${this.executingStep}'`
24499
24624
  );
24500
24625
  }
@@ -24579,7 +24704,7 @@ var AutoExecutor = class _AutoExecutor {
24579
24704
  const sortedSteps = sortSteps(this.steps);
24580
24705
  const plannedParallelStepCount = sortedSteps[initialStepCount + this.planStepCount]?.concurrent;
24581
24706
  if (parallelCallState !== "first" && plannedParallelStepCount !== parallelSteps.length) {
24582
- throw new QStashWorkflowError(
24707
+ throw new WorkflowError(
24583
24708
  `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
24584
24709
  );
24585
24710
  }
@@ -24601,7 +24726,7 @@ var AutoExecutor = class _AutoExecutor {
24601
24726
  case "partial": {
24602
24727
  const planStep = this.steps.at(-1);
24603
24728
  if (!planStep || planStep.targetStep === void 0) {
24604
- throw new QStashWorkflowError(
24729
+ throw new WorkflowError(
24605
24730
  `There must be a last step and it should have targetStep larger than 0.Received: ${JSON.stringify(planStep)}`
24606
24731
  );
24607
24732
  }
@@ -24615,17 +24740,17 @@ var AutoExecutor = class _AutoExecutor {
24615
24740
  );
24616
24741
  await this.submitStepsToQStash([resultStep], [parallelStep]);
24617
24742
  } catch (error) {
24618
- if (error instanceof QStashWorkflowAbort) {
24743
+ if (error instanceof WorkflowAbort) {
24619
24744
  throw error;
24620
24745
  }
24621
- throw new QStashWorkflowError(
24746
+ throw new WorkflowError(
24622
24747
  `Error submitting steps to QStash in partial parallel step execution: ${error}`
24623
24748
  );
24624
24749
  }
24625
24750
  break;
24626
24751
  }
24627
24752
  case "discard": {
24628
- throw new QStashWorkflowAbort("discarded parallel");
24753
+ throw new WorkflowAbort("discarded parallel");
24629
24754
  }
24630
24755
  case "last": {
24631
24756
  const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
@@ -24676,7 +24801,7 @@ var AutoExecutor = class _AutoExecutor {
24676
24801
  */
24677
24802
  async submitStepsToQStash(steps, lazySteps) {
24678
24803
  if (steps.length === 0) {
24679
- throw new QStashWorkflowError(
24804
+ throw new WorkflowError(
24680
24805
  `Unable to submit steps to QStash. Provided list is empty. Current step: ${this.stepCount}`
24681
24806
  );
24682
24807
  }
@@ -24716,7 +24841,7 @@ var AutoExecutor = class _AutoExecutor {
24716
24841
  method: "POST",
24717
24842
  parseResponseAsJson: false
24718
24843
  });
24719
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
24844
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
24720
24845
  }
24721
24846
  const result = await this.context.qstashClient.batchJSON(
24722
24847
  steps.map((singleStep, index) => {
@@ -24768,7 +24893,7 @@ var AutoExecutor = class _AutoExecutor {
24768
24893
  };
24769
24894
  })
24770
24895
  });
24771
- throw new QStashWorkflowAbort(steps[0].stepName, steps[0]);
24896
+ throw new WorkflowAbort(steps[0].stepName, steps[0]);
24772
24897
  }
24773
24898
  /**
24774
24899
  * Get the promise by executing the lazt steps list. If there is a single
@@ -24793,7 +24918,7 @@ var AutoExecutor = class _AutoExecutor {
24793
24918
  } else if (Array.isArray(result) && lazyStepList.length === result.length && index < lazyStepList.length) {
24794
24919
  return result[index];
24795
24920
  } else {
24796
- throw new QStashWorkflowError(
24921
+ throw new WorkflowError(
24797
24922
  `Unexpected parallel call result while executing step ${index}: '${result}'. Expected ${lazyStepList.length} many items`
24798
24923
  );
24799
24924
  }
@@ -24805,12 +24930,12 @@ var AutoExecutor = class _AutoExecutor {
24805
24930
  };
24806
24931
  var validateStep = (lazyStep, stepFromRequest) => {
24807
24932
  if (lazyStep.stepName !== stepFromRequest.stepName) {
24808
- throw new QStashWorkflowError(
24933
+ throw new WorkflowError(
24809
24934
  `Incompatible step name. Expected '${lazyStep.stepName}', got '${stepFromRequest.stepName}' from the request`
24810
24935
  );
24811
24936
  }
24812
24937
  if (lazyStep.stepType !== stepFromRequest.stepType) {
24813
- throw new QStashWorkflowError(
24938
+ throw new WorkflowError(
24814
24939
  `Incompatible step type. Expected '${lazyStep.stepType}', got '${stepFromRequest.stepType}' from the request`
24815
24940
  );
24816
24941
  }
@@ -24821,12 +24946,12 @@ var validateParallelSteps = (lazySteps, stepsFromRequest) => {
24821
24946
  validateStep(lazySteps[index], stepFromRequest);
24822
24947
  }
24823
24948
  } catch (error) {
24824
- if (error instanceof QStashWorkflowError) {
24949
+ if (error instanceof WorkflowError) {
24825
24950
  const lazyStepNames = lazySteps.map((lazyStep) => lazyStep.stepName);
24826
24951
  const lazyStepTypes = lazySteps.map((lazyStep) => lazyStep.stepType);
24827
24952
  const requestStepNames = stepsFromRequest.map((step) => step.stepName);
24828
24953
  const requestStepTypes = stepsFromRequest.map((step) => step.stepType);
24829
- throw new QStashWorkflowError(
24954
+ throw new WorkflowError(
24830
24955
  `Incompatible steps detected in parallel execution: ${error.message}
24831
24956
  > Step Names from the request: ${JSON.stringify(requestStepNames)}
24832
24957
  Step Types from the request: ${JSON.stringify(requestStepTypes)}
@@ -24939,10 +25064,6 @@ var WorkflowContext = class {
24939
25064
  * headers of the initial request
24940
25065
  */
24941
25066
  headers;
24942
- /**
24943
- * initial payload as a raw string
24944
- */
24945
- rawInitialPayload;
24946
25067
  /**
24947
25068
  * Map of environment variables and their values.
24948
25069
  *
@@ -24977,7 +25098,6 @@ var WorkflowContext = class {
24977
25098
  failureUrl,
24978
25099
  debug,
24979
25100
  initialPayload,
24980
- rawInitialPayload,
24981
25101
  env,
24982
25102
  retries
24983
25103
  }) {
@@ -24988,7 +25108,6 @@ var WorkflowContext = class {
24988
25108
  this.failureUrl = failureUrl;
24989
25109
  this.headers = headers;
24990
25110
  this.requestPayload = initialPayload;
24991
- this.rawInitialPayload = rawInitialPayload ?? JSON.stringify(this.requestPayload);
24992
25111
  this.env = env ?? {};
24993
25112
  this.retries = retries ?? DEFAULT_RETRIES;
24994
25113
  this.executor = new AutoExecutor(this, this.steps, debug);
@@ -25009,7 +25128,7 @@ var WorkflowContext = class {
25009
25128
  * const [result1, result2] = await Promise.all([
25010
25129
  * context.run("step 1", () => {
25011
25130
  * return "result1"
25012
- * })
25131
+ * }),
25013
25132
  * context.run("step 2", async () => {
25014
25133
  * return await fetchResults()
25015
25134
  * })
@@ -25027,6 +25146,10 @@ var WorkflowContext = class {
25027
25146
  /**
25028
25147
  * Stops the execution for the duration provided.
25029
25148
  *
25149
+ * ```typescript
25150
+ * await context.sleep('sleep1', 3) // wait for three seconds
25151
+ * ```
25152
+ *
25030
25153
  * @param stepName
25031
25154
  * @param duration sleep duration in seconds
25032
25155
  * @returns undefined
@@ -25037,6 +25160,10 @@ var WorkflowContext = class {
25037
25160
  /**
25038
25161
  * Stops the execution until the date time provided.
25039
25162
  *
25163
+ * ```typescript
25164
+ * await context.sleepUntil('sleep1', Date.now() / 1000 + 3) // wait for three seconds
25165
+ * ```
25166
+ *
25040
25167
  * @param stepName
25041
25168
  * @param datetime time to sleep until. Can be provided as a number (in unix seconds),
25042
25169
  * as a Date object or a string (passed to `new Date(datetimeString)`)
@@ -25060,7 +25187,7 @@ var WorkflowContext = class {
25060
25187
  * const { status, body } = await context.call<string>(
25061
25188
  * "post call step",
25062
25189
  * {
25063
- * url: `https://www.some-endpoint.com/api`,
25190
+ * url: "https://www.some-endpoint.com/api",
25064
25191
  * method: "POST",
25065
25192
  * body: "my-payload"
25066
25193
  * }
@@ -25114,45 +25241,43 @@ var WorkflowContext = class {
25114
25241
  }
25115
25242
  }
25116
25243
  /**
25117
- * Makes the workflow run wait until a notify request is sent or until the
25118
- * timeout ends
25244
+ * Pauses workflow execution until a specific event occurs or a timeout is reached.
25119
25245
  *
25120
- * ```ts
25121
- * const { eventData, timeout } = await context.waitForEvent(
25122
- * "wait for event step",
25123
- * "my-event-id",
25124
- * 100 // timeout after 100 seconds
25125
- * );
25126
- * ```
25246
+ *```ts
25247
+ * const result = await workflow.waitForEvent("payment-confirmed", {
25248
+ * timeout: "5m"
25249
+ * });
25250
+ *```
25127
25251
  *
25128
- * To notify a waiting workflow run, you can use the notify method:
25252
+ * To notify a waiting workflow:
25129
25253
  *
25130
25254
  * ```ts
25131
25255
  * import { Client } from "@upstash/workflow";
25132
25256
  *
25133
- * const client = new Client({ token: });
25257
+ * const client = new Client({ token: "<QSTASH_TOKEN>" });
25134
25258
  *
25135
25259
  * await client.notify({
25136
- * eventId: "my-event-id",
25137
- * eventData: "eventData"
25260
+ * eventId: "payment.confirmed",
25261
+ * data: {
25262
+ * amount: 99.99,
25263
+ * currency: "USD"
25264
+ * }
25138
25265
  * })
25139
25266
  * ```
25140
25267
  *
25268
+ * Alternatively, you can use the `context.notify` method.
25269
+ *
25141
25270
  * @param stepName
25142
- * @param eventId event id to wake up the waiting workflow run
25143
- * @param timeout timeout duration in seconds
25144
- * @returns wait response as `{ timeout: boolean, eventData: unknown }`.
25145
- * timeout is true if the wait times out, if notified it is false. eventData
25146
- * is the value passed to `client.notify`.
25271
+ * @param eventId - Unique identifier for the event to wait for
25272
+ * @param options - Configuration options.
25273
+ * @returns `{ timeout: boolean, eventData: unknown }`.
25274
+ * The `timeout` property specifies if the workflow has timed out. The `eventData`
25275
+ * is the data passed when notifying this workflow of an event.
25147
25276
  */
25148
- async waitForEvent(stepName, eventId, timeout) {
25149
- const result = await this.addStep(
25150
- new LazyWaitForEventStep(
25151
- stepName,
25152
- eventId,
25153
- typeof timeout === "string" ? timeout : `${timeout}s`
25154
- )
25155
- );
25277
+ async waitForEvent(stepName, eventId, options = {}) {
25278
+ const { timeout = "7d" } = options;
25279
+ const timeoutStr = typeof timeout === "string" ? timeout : `${timeout}s`;
25280
+ const result = await this.addStep(new LazyWaitForEventStep(stepName, eventId, timeoutStr));
25156
25281
  try {
25157
25282
  return {
25158
25283
  ...result,
@@ -25162,6 +25287,27 @@ var WorkflowContext = class {
25162
25287
  return result;
25163
25288
  }
25164
25289
  }
25290
+ /**
25291
+ * Notify workflow runs waiting for an event
25292
+ *
25293
+ * ```ts
25294
+ * const { eventId, eventData, notifyResponse } = await context.notify(
25295
+ * "notify step", "event-id", "event-data"
25296
+ * );
25297
+ * ```
25298
+ *
25299
+ * Upon `context.notify`, the workflow runs waiting for the given eventId (context.waitForEvent)
25300
+ * will receive the given event data and resume execution.
25301
+ *
25302
+ * The response includes the same eventId and eventData. Additionally, there is
25303
+ * a notifyResponse field which contains a list of `Waiter` objects, each corresponding
25304
+ * to a notified workflow run.
25305
+ *
25306
+ * @param stepName
25307
+ * @param eventId event id to notify
25308
+ * @param eventData event data to notify with
25309
+ * @returns notify response which has event id, event data and list of waiters which were notified
25310
+ */
25165
25311
  async notify(stepName, eventId, eventData) {
25166
25312
  const result = await this.addStep(
25167
25313
  new LazyNotifyStep(stepName, eventId, eventData, this.qstashClient.http)
@@ -25175,6 +25321,15 @@ var WorkflowContext = class {
25175
25321
  return result;
25176
25322
  }
25177
25323
  }
25324
+ /**
25325
+ * Cancel the current workflow run
25326
+ *
25327
+ * Will throw WorkflowAbort to stop workflow execution.
25328
+ * Shouldn't be inside try/catch.
25329
+ */
25330
+ async cancel() {
25331
+ throw new WorkflowAbort("cancel", void 0, true);
25332
+ }
25178
25333
  /**
25179
25334
  * Adds steps to the executor. Needed so that it can be overwritten in
25180
25335
  * DisabledWorkflowContext.
@@ -25215,7 +25370,8 @@ var WorkflowLogger = class _WorkflowLogger {
25215
25370
  }
25216
25371
  writeToConsole(logEntry) {
25217
25372
  const JSON_SPACING = 2;
25218
- console.log(JSON.stringify(logEntry, void 0, JSON_SPACING));
25373
+ const logMethod = logEntry.logLevel === "ERROR" ? console.error : logEntry.logLevel === "WARN" ? console.warn : console.log;
25374
+ logMethod(JSON.stringify(logEntry, void 0, JSON_SPACING));
25219
25375
  }
25220
25376
  shouldLog(level) {
25221
25377
  return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
@@ -25257,6 +25413,63 @@ function decodeBase64(base64) {
25257
25413
  }
25258
25414
  }
25259
25415
 
25416
+ // src/serve/authorization.ts
25417
+ var import_qstash3 = require("@upstash/qstash");
25418
+ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
25419
+ static disabledMessage = "disabled-qstash-worklfow-run";
25420
+ /**
25421
+ * overwrite the WorkflowContext.addStep method to always raise WorkflowAbort
25422
+ * error in order to stop the execution whenever we encounter a step.
25423
+ *
25424
+ * @param _step
25425
+ */
25426
+ async addStep(_step) {
25427
+ throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
25428
+ }
25429
+ /**
25430
+ * overwrite cancel method to do nothing
25431
+ */
25432
+ async cancel() {
25433
+ return;
25434
+ }
25435
+ /**
25436
+ * copies the passed context to create a DisabledWorkflowContext. Then, runs the
25437
+ * route function with the new context.
25438
+ *
25439
+ * - returns "run-ended" if there are no steps found or
25440
+ * if the auth failed and user called `return`
25441
+ * - returns "step-found" if DisabledWorkflowContext.addStep is called.
25442
+ * - if there is another error, returns the error.
25443
+ *
25444
+ * @param routeFunction
25445
+ */
25446
+ static async tryAuthentication(routeFunction, context) {
25447
+ const disabledContext = new _DisabledWorkflowContext({
25448
+ qstashClient: new import_qstash3.Client({
25449
+ baseUrl: "disabled-client",
25450
+ token: "disabled-client"
25451
+ }),
25452
+ workflowRunId: context.workflowRunId,
25453
+ headers: context.headers,
25454
+ steps: [],
25455
+ url: context.url,
25456
+ failureUrl: context.failureUrl,
25457
+ initialPayload: context.requestPayload,
25458
+ env: context.env,
25459
+ retries: context.retries
25460
+ });
25461
+ try {
25462
+ await routeFunction(disabledContext);
25463
+ } catch (error) {
25464
+ if (error instanceof WorkflowAbort && error.stepName === this.disabledMessage) {
25465
+ return ok("step-found");
25466
+ }
25467
+ return err(error);
25468
+ }
25469
+ return ok("run-ended");
25470
+ }
25471
+ };
25472
+
25260
25473
  // src/workflow-parser.ts
25261
25474
  var getPayload = async (request) => {
25262
25475
  try {
@@ -25265,8 +25478,8 @@ var getPayload = async (request) => {
25265
25478
  return;
25266
25479
  }
25267
25480
  };
25268
- var parsePayload = async (rawPayload, debug) => {
25269
- const [encodedInitialPayload, ...encodedSteps] = JSON.parse(rawPayload);
25481
+ var processRawSteps = (rawSteps) => {
25482
+ const [encodedInitialPayload, ...encodedSteps] = rawSteps;
25270
25483
  const rawInitialPayload = decodeBase64(encodedInitialPayload.body);
25271
25484
  const initialStep = {
25272
25485
  stepId: 0,
@@ -25276,27 +25489,21 @@ var parsePayload = async (rawPayload, debug) => {
25276
25489
  concurrent: NO_CONCURRENCY
25277
25490
  };
25278
25491
  const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
25279
- const otherSteps = await Promise.all(
25280
- stepsToDecode.map(async (rawStep) => {
25281
- const step = JSON.parse(decodeBase64(rawStep.body));
25282
- try {
25283
- step.out = JSON.parse(step.out);
25284
- } catch {
25285
- await debug?.log("WARN", "ENDPOINT_START", {
25286
- message: "failed while parsing out field of step",
25287
- step
25288
- });
25289
- }
25290
- if (step.waitEventId) {
25291
- const newOut = {
25292
- eventData: step.out ? decodeBase64(step.out) : void 0,
25293
- timeout: step.waitTimeout ?? false
25294
- };
25295
- step.out = newOut;
25296
- }
25297
- return step;
25298
- })
25299
- );
25492
+ const otherSteps = stepsToDecode.map((rawStep) => {
25493
+ const step = JSON.parse(decodeBase64(rawStep.body));
25494
+ try {
25495
+ step.out = JSON.parse(step.out);
25496
+ } catch {
25497
+ }
25498
+ if (step.waitEventId) {
25499
+ const newOut = {
25500
+ eventData: step.out ? decodeBase64(step.out) : void 0,
25501
+ timeout: step.waitTimeout ?? false
25502
+ };
25503
+ step.out = newOut;
25504
+ }
25505
+ return step;
25506
+ });
25300
25507
  const steps = [initialStep, ...otherSteps];
25301
25508
  return {
25302
25509
  rawInitialPayload,
@@ -25344,20 +25551,20 @@ var validateRequest = (request) => {
25344
25551
  const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
25345
25552
  const isFirstInvocation = !versionHeader;
25346
25553
  if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
25347
- throw new QStashWorkflowError(
25554
+ throw new WorkflowError(
25348
25555
  `Incompatible workflow sdk protocol version. Expected ${WORKFLOW_PROTOCOL_VERSION}, got ${versionHeader} from the request.`
25349
25556
  );
25350
25557
  }
25351
25558
  const workflowRunId = isFirstInvocation ? getWorkflowRunId() : request.headers.get(WORKFLOW_ID_HEADER) ?? "";
25352
25559
  if (workflowRunId.length === 0) {
25353
- throw new QStashWorkflowError("Couldn't get workflow id from header");
25560
+ throw new WorkflowError("Couldn't get workflow id from header");
25354
25561
  }
25355
25562
  return {
25356
25563
  isFirstInvocation,
25357
25564
  workflowRunId
25358
25565
  };
25359
25566
  };
25360
- var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
25567
+ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requester, messageId, debug) => {
25361
25568
  if (isFirstInvocation) {
25362
25569
  return {
25363
25570
  rawInitialPayload: requestPayload ?? "",
@@ -25365,10 +25572,18 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
25365
25572
  isLastDuplicate: false
25366
25573
  };
25367
25574
  } else {
25575
+ let rawSteps;
25368
25576
  if (!requestPayload) {
25369
- throw new QStashWorkflowError("Only first call can have an empty body");
25577
+ await debug?.log(
25578
+ "INFO",
25579
+ "ENDPOINT_START",
25580
+ "request payload is empty, steps will be fetched from QStash."
25581
+ );
25582
+ rawSteps = await getSteps(requester, workflowRunId, messageId, debug);
25583
+ } else {
25584
+ rawSteps = JSON.parse(requestPayload);
25370
25585
  }
25371
- const { rawInitialPayload, steps } = await parsePayload(requestPayload, debug);
25586
+ const { rawInitialPayload, steps } = processRawSteps(rawSteps);
25372
25587
  const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
25373
25588
  const deduplicatedSteps = deduplicateSteps(steps);
25374
25589
  return {
@@ -25378,13 +25593,13 @@ var parseRequest = async (requestPayload, isFirstInvocation, debug) => {
25378
25593
  };
25379
25594
  }
25380
25595
  };
25381
- var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, failureFunction, debug) => {
25596
+ var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, routeFunction, failureFunction, debug) => {
25382
25597
  if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
25383
25598
  return ok("not-failure-callback");
25384
25599
  }
25385
25600
  if (!failureFunction) {
25386
25601
  return err(
25387
- new QStashWorkflowError(
25602
+ new WorkflowError(
25388
25603
  "Workflow endpoint is called to handle a failure, but a failureFunction is not provided in serve options. Either provide a failureUrl or a failureFunction."
25389
25604
  )
25390
25605
  );
@@ -25395,92 +25610,48 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
25395
25610
  );
25396
25611
  const decodedBody = body ? decodeBase64(body) : "{}";
25397
25612
  const errorPayload = JSON.parse(decodedBody);
25398
- const {
25399
- rawInitialPayload,
25400
- steps,
25401
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
25402
- isLastDuplicate: _isLastDuplicate
25403
- } = await parseRequest(decodeBase64(sourceBody), false, debug);
25404
25613
  const workflowContext = new WorkflowContext({
25405
25614
  qstashClient,
25406
25615
  workflowRunId,
25407
- initialPayload: initialPayloadParser(rawInitialPayload),
25408
- rawInitialPayload,
25616
+ initialPayload: initialPayloadParser(decodeBase64(sourceBody)),
25409
25617
  headers: recreateUserHeaders(new Headers(sourceHeader)),
25410
- steps,
25618
+ steps: [],
25411
25619
  url,
25412
25620
  failureUrl: url,
25413
25621
  debug
25414
25622
  });
25415
- await failureFunction(workflowContext, status, errorPayload.message, header);
25623
+ const authCheck = await DisabledWorkflowContext.tryAuthentication(
25624
+ routeFunction,
25625
+ workflowContext
25626
+ );
25627
+ if (authCheck.isErr()) {
25628
+ await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
25629
+ throw authCheck.error;
25630
+ } else if (authCheck.value === "run-ended") {
25631
+ return err(new WorkflowError("Not authorized to run the failure function."));
25632
+ }
25633
+ await failureFunction({
25634
+ context: workflowContext,
25635
+ failStatus: status,
25636
+ failResponse: errorPayload.message,
25637
+ failHeaders: header
25638
+ });
25416
25639
  } catch (error) {
25417
25640
  return err(error);
25418
25641
  }
25419
25642
  return ok("is-failure-callback");
25420
25643
  };
25421
25644
 
25422
- // src/serve/authorization.ts
25423
- var import_qstash2 = require("@upstash/qstash");
25424
- var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
25425
- static disabledMessage = "disabled-qstash-worklfow-run";
25426
- /**
25427
- * overwrite the WorkflowContext.addStep method to always raise QStashWorkflowAbort
25428
- * error in order to stop the execution whenever we encounter a step.
25429
- *
25430
- * @param _step
25431
- */
25432
- async addStep(_step) {
25433
- throw new QStashWorkflowAbort(_DisabledWorkflowContext.disabledMessage);
25434
- }
25435
- /**
25436
- * copies the passed context to create a DisabledWorkflowContext. Then, runs the
25437
- * route function with the new context.
25438
- *
25439
- * - returns "run-ended" if there are no steps found or
25440
- * if the auth failed and user called `return`
25441
- * - returns "step-found" if DisabledWorkflowContext.addStep is called.
25442
- * - if there is another error, returns the error.
25443
- *
25444
- * @param routeFunction
25445
- */
25446
- static async tryAuthentication(routeFunction, context) {
25447
- const disabledContext = new _DisabledWorkflowContext({
25448
- qstashClient: new import_qstash2.Client({
25449
- baseUrl: "disabled-client",
25450
- token: "disabled-client"
25451
- }),
25452
- workflowRunId: context.workflowRunId,
25453
- headers: context.headers,
25454
- steps: [],
25455
- url: context.url,
25456
- failureUrl: context.failureUrl,
25457
- initialPayload: context.requestPayload,
25458
- rawInitialPayload: context.rawInitialPayload,
25459
- env: context.env,
25460
- retries: context.retries
25461
- });
25462
- try {
25463
- await routeFunction(disabledContext);
25464
- } catch (error) {
25465
- if (error instanceof QStashWorkflowAbort && error.stepName === this.disabledMessage) {
25466
- return ok("step-found");
25467
- }
25468
- return err(error);
25469
- }
25470
- return ok("run-ended");
25471
- }
25472
- };
25473
-
25474
25645
  // src/serve/options.ts
25475
- var import_qstash3 = require("@upstash/qstash");
25476
25646
  var import_qstash4 = require("@upstash/qstash");
25647
+ var import_qstash5 = require("@upstash/qstash");
25477
25648
  var processOptions = (options) => {
25478
25649
  const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
25479
25650
  const receiverEnvironmentVariablesSet = Boolean(
25480
25651
  environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
25481
25652
  );
25482
25653
  return {
25483
- qstashClient: new import_qstash4.Client({
25654
+ qstashClient: new import_qstash5.Client({
25484
25655
  baseUrl: environment.QSTASH_URL,
25485
25656
  token: environment.QSTASH_TOKEN
25486
25657
  }),
@@ -25501,7 +25672,7 @@ var processOptions = (options) => {
25501
25672
  throw error;
25502
25673
  }
25503
25674
  },
25504
- receiver: receiverEnvironmentVariablesSet ? new import_qstash3.Receiver({
25675
+ receiver: receiverEnvironmentVariablesSet ? new import_qstash4.Receiver({
25505
25676
  currentSigningKey: environment.QSTASH_CURRENT_SIGNING_KEY,
25506
25677
  nextSigningKey: environment.QSTASH_NEXT_SIGNING_KEY
25507
25678
  }) : void 0,
@@ -25563,6 +25734,9 @@ var serve = (routeFunction, options) => {
25563
25734
  const { rawInitialPayload, steps, isLastDuplicate } = await parseRequest(
25564
25735
  requestPayload,
25565
25736
  isFirstInvocation,
25737
+ workflowRunId,
25738
+ qstashClient.http,
25739
+ request.headers.get("upstash-message-id"),
25566
25740
  debug
25567
25741
  );
25568
25742
  if (isLastDuplicate) {
@@ -25573,6 +25747,7 @@ var serve = (routeFunction, options) => {
25573
25747
  requestPayload,
25574
25748
  qstashClient,
25575
25749
  initialPayloadParser,
25750
+ routeFunction,
25576
25751
  failureFunction
25577
25752
  );
25578
25753
  if (failureCheck.isErr()) {
@@ -25585,7 +25760,6 @@ var serve = (routeFunction, options) => {
25585
25760
  qstashClient,
25586
25761
  workflowRunId,
25587
25762
  initialPayload: initialPayloadParser(rawInitialPayload),
25588
- rawInitialPayload,
25589
25763
  headers: recreateUserHeaders(request.headers),
25590
25764
  steps,
25591
25765
  url: workflowUrl,
@@ -25623,7 +25797,11 @@ var serve = (routeFunction, options) => {
25623
25797
  onStep: async () => routeFunction(workflowContext),
25624
25798
  onCleanup: async () => {
25625
25799
  await triggerWorkflowDelete(workflowContext, debug);
25626
- }
25800
+ },
25801
+ onCancel: async () => {
25802
+ await makeCancelRequest(workflowContext.qstashClient.http, workflowRunId);
25803
+ },
25804
+ debug
25627
25805
  });
25628
25806
  if (result.isErr()) {
25629
25807
  await debug?.log("ERROR", "ERROR", { error: result.error.message });
@@ -25649,7 +25827,7 @@ var serve = (routeFunction, options) => {
25649
25827
  };
25650
25828
 
25651
25829
  // src/client/index.ts
25652
- var import_qstash5 = require("@upstash/qstash");
25830
+ var import_qstash6 = require("@upstash/qstash");
25653
25831
 
25654
25832
  // platforms/express.ts
25655
25833
  var import_express = __toESM(require_express2());