@upstash/workflow 0.3.0-rc → 0.3.0-rc1

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
@@ -347,26 +347,36 @@ var WorkflowError = class extends import_qstash.QstashError {
347
347
  }
348
348
  };
349
349
  var WorkflowAbort = class extends Error {
350
- stepInfo;
351
350
  stepName;
351
+ stepInfo;
352
352
  /**
353
- * whether workflow is to be canceled on abort
354
- */
355
- cancelWorkflow;
356
- /**
357
- *
358
353
  * @param stepName name of the aborting step
359
354
  * @param stepInfo step information
360
- * @param cancelWorkflow
361
355
  */
362
- constructor(stepName, stepInfo, cancelWorkflow = false) {
356
+ constructor(stepName, stepInfo) {
363
357
  super(
364
358
  `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}'.`
365
359
  );
366
360
  this.name = "WorkflowAbort";
367
361
  this.stepName = stepName;
368
362
  this.stepInfo = stepInfo;
369
- this.cancelWorkflow = cancelWorkflow;
363
+ }
364
+ };
365
+ var WorkflowAuthError = class extends WorkflowAbort {
366
+ /**
367
+ * @param stepName name of the step found during authorization
368
+ */
369
+ constructor(stepName) {
370
+ super(stepName);
371
+ this.name = "WorkflowAuthError";
372
+ this.message = `This is an Upstash Workflow error thrown during authorization check. Found step '${stepName}' during dry-run.`;
373
+ }
374
+ };
375
+ var WorkflowCancelAbort = class extends WorkflowAbort {
376
+ constructor() {
377
+ super("cancel");
378
+ this.name = "WorkflowCancelAbort";
379
+ this.message = "Workflow has been canceled by user via context.cancel().";
370
380
  }
371
381
  };
372
382
  var WorkflowNonRetryableError = class extends WorkflowAbort {
@@ -374,22 +384,22 @@ var WorkflowNonRetryableError = class extends WorkflowAbort {
374
384
  * @param message error message to be displayed
375
385
  */
376
386
  constructor(message) {
377
- super("fail", void 0, false);
387
+ super("non-retryable-error");
378
388
  this.name = "WorkflowNonRetryableError";
379
- if (message) this.message = message;
389
+ this.message = message ?? "Workflow failed with non-retryable error.";
380
390
  }
381
391
  };
382
392
  var WorkflowRetryAfterError = class extends WorkflowAbort {
383
393
  retryAfter;
384
394
  /**
385
- * @param retryAfter time in seconds after which the workflow should be retried
386
395
  * @param message error message to be displayed
396
+ * @param retryAfter time in seconds after which the workflow should be retried
387
397
  */
388
398
  constructor(message, retryAfter) {
389
- super("retry", void 0, false);
399
+ super("retry-after-error");
390
400
  this.name = "WorkflowRetryAfterError";
401
+ this.message = message;
391
402
  this.retryAfter = retryAfter;
392
- if (message) this.message = message;
393
403
  }
394
404
  };
395
405
  var formatWorkflowError = (error) => {
@@ -441,15 +451,21 @@ var makeCancelRequest = async (requester, workflowRunId) => {
441
451
  });
442
452
  return true;
443
453
  };
444
- var getSteps = async (requester, workflowRunId, messageId, debug) => {
454
+ var getSteps = async (requester, workflowRunId, messageId, dispatchDebug) => {
445
455
  try {
446
456
  const steps = await requester.request({
447
457
  path: ["v2", "workflows", "runs", workflowRunId],
448
458
  parseResponseAsJson: true
449
459
  });
460
+ if (steps.length === 1) {
461
+ return {
462
+ steps,
463
+ workflowRunEnded: false
464
+ };
465
+ }
450
466
  if (!messageId) {
451
- await debug?.log("INFO", "ENDPOINT_START", {
452
- message: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
467
+ await dispatchDebug?.("onInfo", {
468
+ info: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
453
469
  });
454
470
  return { steps, workflowRunEnded: false };
455
471
  } else {
@@ -458,16 +474,15 @@ var getSteps = async (requester, workflowRunId, messageId, debug) => {
458
474
  return { steps: [], workflowRunEnded: false };
459
475
  }
460
476
  const filteredSteps = steps.slice(0, index + 1);
461
- await debug?.log("INFO", "ENDPOINT_START", {
462
- message: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
477
+ await dispatchDebug?.("onInfo", {
478
+ info: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
463
479
  });
464
480
  return { steps: filteredSteps, workflowRunEnded: false };
465
481
  }
466
482
  } catch (error) {
467
483
  if (isInstanceOf(error, import_qstash2.QstashError) && error.status === 404) {
468
- await debug?.log("WARN", "ENDPOINT_START", {
469
- message: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed.",
470
- error
484
+ await dispatchDebug?.("onWarning", {
485
+ warning: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed."
471
486
  });
472
487
  return { steps: void 0, workflowRunEnded: true };
473
488
  } else {
@@ -481,15 +496,18 @@ var WORKFLOW_ID_HEADER = "Upstash-Workflow-RunId";
481
496
  var WORKFLOW_INIT_HEADER = "Upstash-Workflow-Init";
482
497
  var WORKFLOW_URL_HEADER = "Upstash-Workflow-Url";
483
498
  var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
499
+ var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
484
500
  var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
485
501
  var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
486
502
  var WORKFLOW_LABEL_HEADER = "Upstash-Label";
503
+ var WORKFLOW_UNKOWN_SDK_VERSION_HEADER = "Upstash-Workflow-Unknown-Sdk";
504
+ var WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER = "upstash-workflow-trigger-by-sdk";
487
505
  var WORKFLOW_PROTOCOL_VERSION = "1";
488
506
  var WORKFLOW_PROTOCOL_VERSION_HEADER = "Upstash-Workflow-Sdk-Version";
489
507
  var DEFAULT_CONTENT_TYPE = "application/json";
490
508
  var NO_CONCURRENCY = 1;
491
509
  var DEFAULT_RETRIES = 3;
492
- var VERSION = "v0.3.0-rc";
510
+ var VERSION = "v1.0.0";
493
511
  var SDK_TELEMETRY = `@upstash/workflow@${VERSION}`;
494
512
  var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
495
513
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
@@ -507,8 +525,8 @@ var NANOID_LENGTH = 21;
507
525
  function getRandomInt() {
508
526
  return Math.floor(Math.random() * NANOID_CHARS.length);
509
527
  }
510
- function nanoid() {
511
- return Array.from({ length: NANOID_LENGTH }).map(() => NANOID_CHARS[getRandomInt()]).join("");
528
+ function nanoid(length = NANOID_LENGTH) {
529
+ return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
512
530
  }
513
531
  function getWorkflowRunId(id) {
514
532
  return `wfr_${id ?? nanoid()}`;
@@ -525,6 +543,46 @@ function decodeBase64(base64) {
525
543
  return binString;
526
544
  }
527
545
  }
546
+ function getUserIdFromToken(qstashClient) {
547
+ try {
548
+ const token = qstashClient.token;
549
+ const decodedToken = decodeBase64(token);
550
+ const tokenPayload = JSON.parse(decodedToken);
551
+ const userId = tokenPayload.UserID;
552
+ if (!userId) {
553
+ throw new WorkflowError("QStash token payload does not contain userId");
554
+ }
555
+ return userId;
556
+ } catch (error) {
557
+ throw new WorkflowError(
558
+ `Failed to decode QStash token while running create webhook step: ${error.message}`
559
+ );
560
+ }
561
+ }
562
+ function getQStashUrl(qstashClient) {
563
+ try {
564
+ const requester = qstashClient.http;
565
+ const baseUrl = requester.baseUrl;
566
+ if (!baseUrl) {
567
+ throw new WorkflowError("QStash client does not have a baseUrl");
568
+ }
569
+ return baseUrl;
570
+ } catch (error) {
571
+ throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
572
+ }
573
+ }
574
+ function getEventId() {
575
+ return `evt_${nanoid(15)}`;
576
+ }
577
+ function stringifyBody(body) {
578
+ if (body === void 0) {
579
+ return void 0;
580
+ }
581
+ if (typeof body === "string") {
582
+ return body;
583
+ }
584
+ return JSON.stringify(body);
585
+ }
528
586
 
529
587
  // node_modules/neverthrow/dist/index.es.js
530
588
  var defaultErrorConfig = {
@@ -950,7 +1008,9 @@ var StepTypes = [
950
1008
  "Call",
951
1009
  "Wait",
952
1010
  "Notify",
953
- "Invoke"
1011
+ "Invoke",
1012
+ "CreateWebhook",
1013
+ "WaitForWebhook"
954
1014
  ];
955
1015
 
956
1016
  // src/workflow-requests.ts
@@ -966,23 +1026,26 @@ var triggerFirstInvocation = async (params) => {
966
1026
  invokeCount,
967
1027
  delay,
968
1028
  notBefore,
969
- keepTriggerConfig
1029
+ failureUrl,
1030
+ retries,
1031
+ retryDelay,
1032
+ flowControl,
1033
+ unknownSdk
970
1034
  }) => {
971
1035
  const { headers } = getHeaders({
972
1036
  initHeaderValue: "true",
973
1037
  workflowConfig: {
974
1038
  workflowRunId: workflowContext.workflowRunId,
975
1039
  workflowUrl: workflowContext.url,
976
- failureUrl: workflowContext.failureUrl,
977
- retries: workflowContext.retries,
978
- retryDelay: workflowContext.retryDelay,
1040
+ failureUrl,
1041
+ retries,
1042
+ retryDelay,
979
1043
  telemetry: telemetry2,
980
- flowControl: workflowContext.flowControl,
1044
+ flowControl,
981
1045
  useJSONContent: useJSONContent ?? false
982
1046
  },
983
1047
  invokeCount: invokeCount ?? 0,
984
- userHeaders: workflowContext.headers,
985
- keepTriggerConfig
1048
+ userHeaders: workflowContext.headers
986
1049
  });
987
1050
  if (workflowContext.headers.get("content-type")) {
988
1051
  headers["content-type"] = workflowContext.headers.get("content-type");
@@ -990,6 +1053,9 @@ var triggerFirstInvocation = async (params) => {
990
1053
  if (useJSONContent) {
991
1054
  headers["content-type"] = "application/json";
992
1055
  }
1056
+ if (unknownSdk) {
1057
+ headers[WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER] = "true";
1058
+ }
993
1059
  if (workflowContext.label) {
994
1060
  headers[WORKFLOW_LABEL_HEADER] = workflowContext.label;
995
1061
  }
@@ -1010,21 +1076,15 @@ var triggerFirstInvocation = async (params) => {
1010
1076
  for (let i = 0; i < results.length; i++) {
1011
1077
  const result = results[i];
1012
1078
  const invocationParams = firstInvocationParams[i];
1079
+ invocationParams.middlewareManager?.assignContext(invocationParams.workflowContext);
1013
1080
  if (result.deduplicated) {
1014
- await invocationParams.debug?.log("WARN", "SUBMIT_FIRST_INVOCATION", {
1015
- message: `Workflow run ${invocationParams.workflowContext.workflowRunId} already exists. A new one isn't created.`,
1016
- headers: invocationBatch[i].headers,
1017
- requestPayload: invocationParams.workflowContext.requestPayload,
1018
- url: invocationParams.workflowContext.url,
1019
- messageId: result.messageId
1081
+ await invocationParams.middlewareManager?.dispatchDebug("onWarning", {
1082
+ warning: `Workflow run ${invocationParams.workflowContext.workflowRunId} already exists. A new one isn't created.`
1020
1083
  });
1021
1084
  invocationStatuses.push("workflow-run-already-exists");
1022
1085
  } else {
1023
- await invocationParams.debug?.log("SUBMIT", "SUBMIT_FIRST_INVOCATION", {
1024
- headers: invocationBatch[i].headers,
1025
- requestPayload: invocationParams.workflowContext.requestPayload,
1026
- url: invocationParams.workflowContext.url,
1027
- messageId: result.messageId
1086
+ await invocationParams.middlewareManager?.dispatchDebug("onInfo", {
1087
+ info: `Workflow run started successfully with URL ${invocationParams.workflowContext.url}.`
1028
1088
  });
1029
1089
  invocationStatuses.push("success");
1030
1090
  }
@@ -1046,7 +1106,7 @@ var triggerRouteFunction = async ({
1046
1106
  onCleanup,
1047
1107
  onStep,
1048
1108
  onCancel,
1049
- debug
1109
+ middlewareManager
1050
1110
  }) => {
1051
1111
  try {
1052
1112
  const result = await onStep();
@@ -1055,27 +1115,25 @@ var triggerRouteFunction = async ({
1055
1115
  } catch (error) {
1056
1116
  const error_ = error;
1057
1117
  if (isInstanceOf(error, import_qstash3.QstashError) && error.status === 400) {
1058
- await debug?.log("WARN", "RESPONSE_WORKFLOW", {
1059
- message: `tried to append to a cancelled workflow. exiting without publishing.`,
1060
- name: error.name,
1061
- errorMessage: error.message
1118
+ await middlewareManager?.dispatchDebug("onWarning", {
1119
+ warning: `Tried to append to a cancelled workflow. Exiting without publishing. Error: ${error.message}`
1062
1120
  });
1063
1121
  return ok("workflow-was-finished");
1064
1122
  } else if (isInstanceOf(error_, WorkflowNonRetryableError) || isInstanceOf(error_, WorkflowRetryAfterError)) {
1065
1123
  return ok(error_);
1066
- } else if (!isInstanceOf(error_, WorkflowAbort)) {
1067
- return err(error_);
1068
- } else if (error_.cancelWorkflow) {
1124
+ } else if (isInstanceOf(error_, WorkflowCancelAbort)) {
1069
1125
  await onCancel();
1070
1126
  return ok("workflow-finished");
1071
- } else {
1127
+ } else if (isInstanceOf(error_, WorkflowAbort)) {
1072
1128
  return ok("step-finished");
1129
+ } else {
1130
+ return err(error_);
1073
1131
  }
1074
1132
  }
1075
1133
  };
1076
- var triggerWorkflowDelete = async (workflowContext, result, debug, cancel = false) => {
1077
- await debug?.log("SUBMIT", "SUBMIT_CLEANUP", {
1078
- deletedWorkflowRunId: workflowContext.workflowRunId
1134
+ var triggerWorkflowDelete = async (workflowContext, result, cancel = false, dispatchDebug) => {
1135
+ await dispatchDebug?.("onInfo", {
1136
+ info: `Deleting workflow run ${workflowContext.workflowRunId} from QStash` + (cancel ? " with cancel=true." : ".")
1079
1137
  });
1080
1138
  await workflowContext.qstashClient.http.request({
1081
1139
  path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
@@ -1083,18 +1141,16 @@ var triggerWorkflowDelete = async (workflowContext, result, debug, cancel = fals
1083
1141
  parseResponseAsJson: false,
1084
1142
  body: JSON.stringify(result)
1085
1143
  });
1086
- await debug?.log(
1087
- "SUBMIT",
1088
- "SUBMIT_CLEANUP",
1089
- `workflow run ${workflowContext.workflowRunId} deleted.`
1090
- );
1144
+ await dispatchDebug?.("onInfo", {
1145
+ info: `Workflow run ${workflowContext.workflowRunId} deleted from QStash successfully.`
1146
+ });
1091
1147
  };
1092
1148
  var recreateUserHeaders = (headers) => {
1093
1149
  const filteredHeaders = new Headers();
1094
1150
  const pairs = headers.entries();
1095
1151
  for (const [header, value] of pairs) {
1096
1152
  const headerLowerCase = header.toLowerCase();
1097
- const isUserHeader = !headerLowerCase.startsWith("upstash-workflow-") && // https://vercel.com/docs/edge-network/headers/request-headers#x-vercel-id
1153
+ const isUserHeader = headerLowerCase !== "upstash-region" && !headerLowerCase.startsWith("upstash-workflow-") && // https://vercel.com/docs/edge-network/headers/request-headers#x-vercel-id
1098
1154
  !headerLowerCase.startsWith("x-vercel-") && !headerLowerCase.startsWith("x-forwarded-") && // https://blog.cloudflare.com/preventing-request-loops-using-cdn-loop/
1099
1155
  headerLowerCase !== "cf-connecting-ip" && headerLowerCase !== "cdn-loop" && headerLowerCase !== "cf-ew-via" && headerLowerCase !== "cf-ray" && // For Render https://render.com
1100
1156
  headerLowerCase !== "render-proxy-ttl" || headerLowerCase === WORKFLOW_LABEL_HEADER.toLocaleLowerCase();
@@ -1109,12 +1165,8 @@ var handleThirdPartyCallResult = async ({
1109
1165
  requestPayload,
1110
1166
  client,
1111
1167
  workflowUrl,
1112
- failureUrl,
1113
- retries,
1114
- retryDelay,
1115
1168
  telemetry: telemetry2,
1116
- flowControl,
1117
- debug
1169
+ middlewareManager
1118
1170
  }) => {
1119
1171
  try {
1120
1172
  if (request.headers.get("Upstash-Workflow-Callback")) {
@@ -1131,7 +1183,7 @@ var handleThirdPartyCallResult = async ({
1131
1183
  client.http,
1132
1184
  workflowRunId2,
1133
1185
  messageId,
1134
- debug
1186
+ middlewareManager?.dispatchDebug.bind(middlewareManager)
1135
1187
  );
1136
1188
  if (workflowRunEnded) {
1137
1189
  return ok("workflow-ended");
@@ -1145,9 +1197,8 @@ var handleThirdPartyCallResult = async ({
1145
1197
  }
1146
1198
  const callbackMessage = JSON.parse(callbackPayload);
1147
1199
  if (!(callbackMessage.status >= 200 && callbackMessage.status < 300) && callbackMessage.maxRetries && callbackMessage.retried !== callbackMessage.maxRetries) {
1148
- await debug?.log("WARN", "SUBMIT_THIRD_PARTY_RESULT", {
1149
- status: callbackMessage.status,
1150
- body: atob(callbackMessage.body ?? "")
1200
+ await middlewareManager?.dispatchDebug("onWarning", {
1201
+ warning: `Third party call returned status ${callbackMessage.status}. Retrying (${callbackMessage.retried} out of ${callbackMessage.maxRetries}).`
1151
1202
  });
1152
1203
  console.warn(
1153
1204
  `Workflow Warning: "context.call" failed with status ${callbackMessage.status} and will retry (retried ${callbackMessage.retried ?? 0} out of ${callbackMessage.maxRetries} times). Error Message:
@@ -1180,11 +1231,7 @@ ${atob(callbackMessage.body ?? "")}`
1180
1231
  workflowConfig: {
1181
1232
  workflowRunId,
1182
1233
  workflowUrl,
1183
- failureUrl,
1184
- retries,
1185
- retryDelay,
1186
- telemetry: telemetry2,
1187
- flowControl
1234
+ telemetry: telemetry2
1188
1235
  },
1189
1236
  userHeaders,
1190
1237
  invokeCount: Number(invokeCount)
@@ -1201,19 +1248,17 @@ ${atob(callbackMessage.body ?? "")}`
1201
1248
  out: JSON.stringify(callResponse),
1202
1249
  concurrent: Number(concurrentString)
1203
1250
  };
1204
- await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
1205
- step: callResultStep,
1206
- headers: requestHeaders,
1207
- url: workflowUrl
1251
+ await middlewareManager?.dispatchDebug("onInfo", {
1252
+ info: `Submitting third party call result, step ${stepName} (${stepIdString}).`
1208
1253
  });
1209
- const result = await client.publishJSON({
1254
+ await client.publishJSON({
1210
1255
  headers: requestHeaders,
1211
1256
  method: "POST",
1212
1257
  body: callResultStep,
1213
1258
  url: workflowUrl
1214
1259
  });
1215
- await debug?.log("SUBMIT", "SUBMIT_THIRD_PARTY_RESULT", {
1216
- messageId: result.messageId
1260
+ await middlewareManager?.dispatchDebug("onInfo", {
1261
+ info: `Third party call result submitted successfully, step ${stepName} (${stepIdString}).`
1217
1262
  });
1218
1263
  return ok("is-call-return");
1219
1264
  } else {
@@ -1262,15 +1307,17 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
1262
1307
  // src/context/steps.ts
1263
1308
  var BaseLazyStep = class _BaseLazyStep {
1264
1309
  stepName;
1265
- constructor(stepName) {
1310
+ context;
1311
+ constructor(context, stepName) {
1312
+ this.context = context;
1266
1313
  if (!stepName) {
1267
1314
  throw new WorkflowError(
1268
1315
  "A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
1269
1316
  );
1270
1317
  }
1271
1318
  if (typeof stepName !== "string") {
1272
- console.warn(
1273
- "Workflow Warning: A workflow step name must be a string. In a future release, this will throw an error."
1319
+ throw new WorkflowError(
1320
+ `A workflow step name must be a string. Received "${stepName}" (${typeof stepName}).`
1274
1321
  );
1275
1322
  }
1276
1323
  this.stepName = stepName;
@@ -1280,13 +1327,14 @@ var BaseLazyStep = class _BaseLazyStep {
1280
1327
  *
1281
1328
  * will be called when returning the steps to the context from auto executor
1282
1329
  *
1283
- * @param out field of the step
1330
+ * @param step step
1284
1331
  * @returns parsed out field
1285
1332
  */
1286
- parseOut(out) {
1333
+ parseOut(step) {
1334
+ const out = step.out;
1287
1335
  if (out === void 0) {
1288
1336
  if (this.allowUndefinedOut) {
1289
- return void 0;
1337
+ return this.handleUndefinedOut(step);
1290
1338
  } else {
1291
1339
  throw new WorkflowError(
1292
1340
  `Error while parsing output of ${this.stepType} step. Expected a string, but got: undefined`
@@ -1294,27 +1342,26 @@ var BaseLazyStep = class _BaseLazyStep {
1294
1342
  }
1295
1343
  }
1296
1344
  if (typeof out === "object") {
1297
- if (this.stepType !== "Wait") {
1298
- console.warn(
1299
- `Error while parsing ${this.stepType} step output. Expected a string, but got object. Please reach out to Upstash Support.`
1300
- );
1301
- return out;
1302
- }
1303
- return {
1304
- ...out,
1305
- eventData: _BaseLazyStep.tryParsing(out.eventData)
1306
- };
1345
+ console.warn(
1346
+ `Error while parsing ${this.stepType} step output. Expected a string, but got object. Please reach out to Upstash Support.`
1347
+ );
1348
+ return out;
1307
1349
  }
1308
1350
  if (typeof out !== "string") {
1309
1351
  throw new WorkflowError(
1310
1352
  `Error while parsing output of ${this.stepType} step. Expected a string or undefined, but got: ${typeof out}`
1311
1353
  );
1312
1354
  }
1313
- return this.safeParseOut(out);
1355
+ return this.safeParseOut(out, step);
1314
1356
  }
1315
- safeParseOut(out) {
1357
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1358
+ safeParseOut(out, step) {
1316
1359
  return _BaseLazyStep.tryParsing(out);
1317
1360
  }
1361
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1362
+ handleUndefinedOut(step) {
1363
+ return void 0;
1364
+ }
1318
1365
  static tryParsing(stepOut) {
1319
1366
  try {
1320
1367
  return JSON.parse(stepOut);
@@ -1332,12 +1379,8 @@ var BaseLazyStep = class _BaseLazyStep {
1332
1379
  workflowConfig: {
1333
1380
  workflowRunId: context.workflowRunId,
1334
1381
  workflowUrl: context.url,
1335
- failureUrl: context.failureUrl,
1336
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1337
- retryDelay: context.retryDelay,
1338
1382
  useJSONContent: false,
1339
- telemetry: telemetry2,
1340
- flowControl: context.flowControl
1383
+ telemetry: telemetry2
1341
1384
  },
1342
1385
  userHeaders: context.headers,
1343
1386
  invokeCount,
@@ -1353,9 +1396,6 @@ var BaseLazyStep = class _BaseLazyStep {
1353
1396
  body,
1354
1397
  headers,
1355
1398
  method: "POST",
1356
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1357
- retryDelay: context.retryDelay,
1358
- flowControl: context.flowControl,
1359
1399
  url: context.url
1360
1400
  }
1361
1401
  ]);
@@ -1365,8 +1405,8 @@ var LazyFunctionStep = class extends BaseLazyStep {
1365
1405
  stepFunction;
1366
1406
  stepType = "Run";
1367
1407
  allowUndefinedOut = true;
1368
- constructor(stepName, stepFunction) {
1369
- super(stepName);
1408
+ constructor(context, stepName, stepFunction) {
1409
+ super(context, stepName);
1370
1410
  this.stepFunction = stepFunction;
1371
1411
  }
1372
1412
  getPlanStep(concurrent, targetStep) {
@@ -1396,8 +1436,8 @@ var LazySleepStep = class extends BaseLazyStep {
1396
1436
  sleep;
1397
1437
  stepType = "SleepFor";
1398
1438
  allowUndefinedOut = true;
1399
- constructor(stepName, sleep) {
1400
- super(stepName);
1439
+ constructor(context, stepName, sleep) {
1440
+ super(context, stepName);
1401
1441
  this.sleep = sleep;
1402
1442
  }
1403
1443
  getPlanStep(concurrent, targetStep) {
@@ -1426,9 +1466,6 @@ var LazySleepStep = class extends BaseLazyStep {
1426
1466
  headers,
1427
1467
  method: "POST",
1428
1468
  url: context.url,
1429
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1430
- retryDelay: context.retryDelay,
1431
- flowControl: context.flowControl,
1432
1469
  delay: isParallel ? void 0 : this.sleep
1433
1470
  }
1434
1471
  ]);
@@ -1438,8 +1475,8 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1438
1475
  sleepUntil;
1439
1476
  stepType = "SleepUntil";
1440
1477
  allowUndefinedOut = true;
1441
- constructor(stepName, sleepUntil) {
1442
- super(stepName);
1478
+ constructor(context, stepName, sleepUntil) {
1479
+ super(context, stepName);
1443
1480
  this.sleepUntil = sleepUntil;
1444
1481
  }
1445
1482
  getPlanStep(concurrent, targetStep) {
@@ -1471,9 +1508,6 @@ var LazySleepUntilStep = class extends BaseLazyStep {
1471
1508
  headers,
1472
1509
  method: "POST",
1473
1510
  url: context.url,
1474
- retries: DEFAULT_RETRIES === context.retries ? void 0 : context.retries,
1475
- retryDelay: context.retryDelay,
1476
- flowControl: context.flowControl,
1477
1511
  notBefore: isParallel ? void 0 : this.sleepUntil
1478
1512
  }
1479
1513
  ]);
@@ -1488,20 +1522,18 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1488
1522
  retryDelay;
1489
1523
  timeout;
1490
1524
  flowControl;
1491
- stringifyBody;
1492
1525
  stepType = "Call";
1493
1526
  allowUndefinedOut = false;
1494
- constructor(stepName, url, method, body, headers, retries, retryDelay, timeout, flowControl, stringifyBody) {
1495
- super(stepName);
1496
- this.url = url;
1497
- this.method = method;
1498
- this.body = body;
1499
- this.headers = headers;
1500
- this.retries = retries;
1501
- this.retryDelay = retryDelay;
1502
- this.timeout = timeout;
1503
- this.flowControl = flowControl;
1504
- this.stringifyBody = stringifyBody;
1527
+ constructor(params) {
1528
+ super(params.context, params.stepName);
1529
+ this.url = params.url;
1530
+ this.method = params.method ?? "GET";
1531
+ this.body = params.body;
1532
+ this.headers = params.headers ?? {};
1533
+ this.retries = params.retries ?? 0;
1534
+ this.retryDelay = params.retryDelay;
1535
+ this.timeout = params.timeout;
1536
+ this.flowControl = params.flowControl;
1505
1537
  }
1506
1538
  getPlanStep(concurrent, targetStep) {
1507
1539
  return {
@@ -1598,7 +1630,7 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1598
1630
  "Upstash-Callback-Workflow-CallType": "fromCallback",
1599
1631
  "Upstash-Callback-Workflow-Init": "false",
1600
1632
  "Upstash-Callback-Workflow-Url": context.url,
1601
- "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody,WF_DetectTrigger",
1633
+ "Upstash-Callback-Feature-Set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1602
1634
  "Upstash-Callback-Forward-Upstash-Workflow-Callback": "true",
1603
1635
  "Upstash-Callback-Forward-Upstash-Workflow-StepId": step.stepId.toString(),
1604
1636
  "Upstash-Callback-Forward-Upstash-Workflow-StepName": this.stepName,
@@ -1611,22 +1643,10 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1611
1643
  };
1612
1644
  }
1613
1645
  async submitStep({ context, headers }) {
1614
- let callBody;
1615
- if (this.stringifyBody) {
1616
- callBody = JSON.stringify(this.body);
1617
- } else {
1618
- if (typeof this.body === "string") {
1619
- callBody = this.body;
1620
- } else {
1621
- throw new WorkflowError(
1622
- "When stringifyBody is false, body must be a string. Please check the body type of your call step."
1623
- );
1624
- }
1625
- }
1626
1646
  return await context.qstashClient.batch([
1627
1647
  {
1628
1648
  headers,
1629
- body: callBody,
1649
+ body: this.body,
1630
1650
  method: this.method,
1631
1651
  url: this.url,
1632
1652
  retries: DEFAULT_RETRIES === this.retries ? void 0 : this.retries,
@@ -1636,13 +1656,12 @@ var LazyCallStep = class _LazyCallStep extends BaseLazyStep {
1636
1656
  ]);
1637
1657
  }
1638
1658
  };
1639
- var LazyWaitForEventStep = class extends BaseLazyStep {
1659
+ var LazyWaitEventStep = class extends BaseLazyStep {
1640
1660
  eventId;
1641
1661
  timeout;
1642
- stepType = "Wait";
1643
1662
  allowUndefinedOut = false;
1644
- constructor(stepName, eventId, timeout) {
1645
- super(stepName);
1663
+ constructor(context, stepName, eventId, timeout) {
1664
+ super(context, stepName);
1646
1665
  this.eventId = eventId;
1647
1666
  this.timeout = timeout;
1648
1667
  }
@@ -1667,13 +1686,6 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1667
1686
  concurrent
1668
1687
  });
1669
1688
  }
1670
- safeParseOut(out) {
1671
- const result = JSON.parse(out);
1672
- return {
1673
- ...result,
1674
- eventData: BaseLazyStep.tryParsing(result.eventData)
1675
- };
1676
- }
1677
1689
  getHeaders({ context, telemetry: telemetry2, invokeCount, step }) {
1678
1690
  const headers = super.getHeaders({ context, telemetry: telemetry2, invokeCount, step });
1679
1691
  headers.headers["Upstash-Workflow-CallType"] = "step";
@@ -1707,7 +1719,7 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1707
1719
  timeoutHeaders,
1708
1720
  step: {
1709
1721
  stepId: step.stepId,
1710
- stepType: "Wait",
1722
+ stepType: this.stepType,
1711
1723
  stepName: step.stepName,
1712
1724
  concurrent: step.concurrent,
1713
1725
  targetStep: step.targetStep
@@ -1728,8 +1740,8 @@ var LazyWaitForEventStep = class extends BaseLazyStep {
1728
1740
  };
1729
1741
  var LazyNotifyStep = class extends LazyFunctionStep {
1730
1742
  stepType = "Notify";
1731
- constructor(stepName, eventId, eventData, requester) {
1732
- super(stepName, async () => {
1743
+ constructor(context, stepName, eventId, eventData, requester) {
1744
+ super(context, stepName, async () => {
1733
1745
  const notifyResponse = await makeNotifyRequest(requester, eventId, eventData);
1734
1746
  return {
1735
1747
  eventId,
@@ -1754,28 +1766,10 @@ var LazyInvokeStep = class extends BaseLazyStep {
1754
1766
  * workflow id of the invoked workflow
1755
1767
  */
1756
1768
  workflowId;
1757
- constructor(stepName, {
1758
- workflow,
1759
- body,
1760
- headers = {},
1761
- workflowRunId,
1762
- retries,
1763
- retryDelay,
1764
- flowControl,
1765
- stringifyBody = true
1766
- }) {
1767
- super(stepName);
1768
- this.params = {
1769
- workflow,
1770
- body,
1771
- headers,
1772
- workflowRunId: getWorkflowRunId(workflowRunId),
1773
- retries,
1774
- retryDelay,
1775
- flowControl,
1776
- stringifyBody
1777
- };
1778
- const { workflowId } = workflow;
1769
+ constructor(context, stepName, params) {
1770
+ super(context, stepName);
1771
+ this.params = params;
1772
+ const { workflowId } = params.workflow;
1779
1773
  if (!workflowId) {
1780
1774
  throw new WorkflowError("You can only invoke workflow which has a workflowId");
1781
1775
  }
@@ -1815,31 +1809,18 @@ var LazyInvokeStep = class extends BaseLazyStep {
1815
1809
  workflowConfig: {
1816
1810
  workflowRunId: context.workflowRunId,
1817
1811
  workflowUrl: context.url,
1818
- failureUrl: context.failureUrl,
1819
- retries: context.retries,
1820
- retryDelay: context.retryDelay,
1821
1812
  telemetry: telemetry2,
1822
- flowControl: context.flowControl,
1823
1813
  useJSONContent: false
1824
1814
  },
1825
1815
  userHeaders: context.headers,
1826
1816
  invokeCount
1827
1817
  });
1818
+ context.qstashClient.http.headers?.forEach((value, key) => {
1819
+ invokerHeaders[key] = value;
1820
+ });
1828
1821
  invokerHeaders["Upstash-Workflow-Runid"] = context.workflowRunId;
1829
- let invokeBody;
1830
- if (this.params.stringifyBody) {
1831
- invokeBody = JSON.stringify(this.params.body);
1832
- } else {
1833
- if (typeof this.params.body === "string") {
1834
- invokeBody = this.params.body;
1835
- } else {
1836
- throw new WorkflowError(
1837
- "When stringifyBody is false, body must be a string. Please check the body type of your invoke step."
1838
- );
1839
- }
1840
- }
1841
1822
  const request = {
1842
- body: invokeBody,
1823
+ body: stringifyBody(this.params.body),
1843
1824
  headers: Object.fromEntries(
1844
1825
  Object.entries(invokerHeaders).map((pairs) => [pairs[0], [pairs[1]]])
1845
1826
  ),
@@ -1850,34 +1831,19 @@ var LazyInvokeStep = class extends BaseLazyStep {
1850
1831
  return JSON.stringify(request);
1851
1832
  }
1852
1833
  getHeaders({ context, telemetry: telemetry2, invokeCount }) {
1853
- const {
1854
- workflow,
1855
- headers = {},
1856
- workflowRunId = getWorkflowRunId(),
1857
- retries,
1858
- retryDelay,
1859
- flowControl
1860
- } = this.params;
1834
+ const { workflow, headers = {}, workflowRunId, retries, retryDelay, flowControl } = this.params;
1861
1835
  const newUrl = context.url.replace(/[^/]+$/, this.workflowId);
1862
- const {
1863
- retries: workflowRetries,
1864
- retryDelay: workflowRetryDelay,
1865
- failureFunction,
1866
- failureUrl,
1867
- useJSONContent,
1868
- flowControl: workflowFlowControl
1869
- } = workflow.options;
1870
1836
  const { headers: triggerHeaders, contentType } = getHeaders({
1871
1837
  initHeaderValue: "true",
1872
1838
  workflowConfig: {
1873
- workflowRunId,
1839
+ workflowRunId: getWorkflowRunId(workflowRunId),
1874
1840
  workflowUrl: newUrl,
1875
- retries: retries ?? workflowRetries,
1876
- retryDelay: retryDelay ?? workflowRetryDelay,
1841
+ retries,
1842
+ retryDelay,
1877
1843
  telemetry: telemetry2,
1878
- failureUrl: failureFunction ? newUrl : failureUrl,
1879
- flowControl: flowControl ?? workflowFlowControl,
1880
- useJSONContent: useJSONContent ?? false
1844
+ failureUrl: newUrl,
1845
+ flowControl,
1846
+ useJSONContent: workflow.useJSONContent ?? false
1881
1847
  },
1882
1848
  invokeCount: invokeCount + 1,
1883
1849
  userHeaders: new Headers(headers)
@@ -1896,6 +1862,88 @@ var LazyInvokeStep = class extends BaseLazyStep {
1896
1862
  return [result];
1897
1863
  }
1898
1864
  };
1865
+ var LazyCreateWebhookStep = class extends BaseLazyStep {
1866
+ stepType = "CreateWebhook";
1867
+ allowUndefinedOut = false;
1868
+ getPlanStep(concurrent, targetStep) {
1869
+ return {
1870
+ stepId: 0,
1871
+ stepName: this.stepName,
1872
+ stepType: this.stepType,
1873
+ concurrent,
1874
+ targetStep
1875
+ };
1876
+ }
1877
+ async getResultStep(concurrent, stepId) {
1878
+ return {
1879
+ stepId,
1880
+ stepName: this.stepName,
1881
+ stepType: this.stepType,
1882
+ out: void 0,
1883
+ concurrent
1884
+ };
1885
+ }
1886
+ getBody({ step, context }) {
1887
+ const userId = getUserIdFromToken(context.qstashClient);
1888
+ const workflowRunId = context.workflowRunId;
1889
+ const eventId = getEventId();
1890
+ const qstashUrl = getQStashUrl(this.context.qstashClient);
1891
+ return JSON.stringify({
1892
+ ...step,
1893
+ out: JSON.stringify({
1894
+ webhookUrl: `${qstashUrl}/v2/workflows/hooks/${userId}/${workflowRunId}/${eventId}`,
1895
+ eventId
1896
+ })
1897
+ });
1898
+ }
1899
+ };
1900
+ var LazyWaitForWebhookStep = class extends LazyWaitEventStep {
1901
+ stepType = "WaitForWebhook";
1902
+ allowUndefinedOut = true;
1903
+ constructor(context, stepName, webhook, timeout) {
1904
+ super(context, stepName, webhook.eventId, timeout);
1905
+ }
1906
+ safeParseOut(out) {
1907
+ const eventData = decodeBase64(out);
1908
+ const parsedEventData = BaseLazyStep.tryParsing(eventData);
1909
+ const body = parsedEventData.body;
1910
+ const parsedBody = typeof body === "string" ? decodeBase64(body) : void 0;
1911
+ const request = new Request(
1912
+ `${parsedEventData.proto}://${parsedEventData.host}${parsedEventData.url}`,
1913
+ {
1914
+ method: parsedEventData.method,
1915
+ headers: parsedEventData.header,
1916
+ body: parsedBody
1917
+ }
1918
+ );
1919
+ return {
1920
+ request,
1921
+ timeout: false
1922
+ };
1923
+ }
1924
+ handleUndefinedOut() {
1925
+ return {
1926
+ timeout: true,
1927
+ request: void 0
1928
+ };
1929
+ }
1930
+ };
1931
+ var LazyWaitForEventStep = class extends LazyWaitEventStep {
1932
+ stepType = "Wait";
1933
+ allowUndefinedOut = true;
1934
+ parseWaitForEventOut(out, waitTimeout) {
1935
+ return {
1936
+ eventData: out ? BaseLazyStep.tryParsing(decodeBase64(out)) : void 0,
1937
+ timeout: waitTimeout ?? false
1938
+ };
1939
+ }
1940
+ safeParseOut(out, step) {
1941
+ return this.parseWaitForEventOut(out, step.waitTimeout);
1942
+ }
1943
+ handleUndefinedOut(step) {
1944
+ return this.parseWaitForEventOut(void 0, step.waitTimeout);
1945
+ }
1946
+ };
1899
1947
 
1900
1948
  // src/qstash/headers.ts
1901
1949
  var WorkflowHeaders = class {
@@ -1905,14 +1953,15 @@ var WorkflowHeaders = class {
1905
1953
  initHeaderValue;
1906
1954
  stepInfo;
1907
1955
  headers;
1908
- keepTriggerConfig;
1956
+ /**
1957
+ * @param params workflow header parameters
1958
+ */
1909
1959
  constructor({
1910
1960
  userHeaders,
1911
1961
  workflowConfig,
1912
1962
  invokeCount,
1913
1963
  initHeaderValue,
1914
- stepInfo,
1915
- keepTriggerConfig
1964
+ stepInfo
1916
1965
  }) {
1917
1966
  this.userHeaders = userHeaders;
1918
1967
  this.workflowConfig = workflowConfig;
@@ -1924,7 +1973,6 @@ var WorkflowHeaders = class {
1924
1973
  workflowHeaders: {},
1925
1974
  failureHeaders: {}
1926
1975
  };
1927
- this.keepTriggerConfig = keepTriggerConfig;
1928
1976
  }
1929
1977
  getHeaders() {
1930
1978
  this.addBaseHeaders();
@@ -1943,7 +1991,7 @@ var WorkflowHeaders = class {
1943
1991
  [WORKFLOW_INIT_HEADER]: this.initHeaderValue,
1944
1992
  [WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
1945
1993
  [WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
1946
- [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger" + (this.keepTriggerConfig ? ",WF_TriggerOnConfig" : ""),
1994
+ [WORKFLOW_FEATURE_HEADER]: "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig",
1947
1995
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1948
1996
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1949
1997
  };
@@ -2013,12 +2061,12 @@ var WorkflowHeaders = class {
2013
2061
  }
2014
2062
  this.headers.workflowHeaders["Failure-Callback"] = this.workflowConfig.failureUrl;
2015
2063
  this.headers.failureHeaders[`Forward-${WORKFLOW_FAILURE_HEADER}`] = "true";
2016
- this.headers.failureHeaders[`Forward-Upstash-Workflow-Failure-Callback`] = "true";
2064
+ this.headers.failureHeaders[`Forward-${WORKFLOW_FAILURE_CALLBACK_HEADER}`] = "true";
2017
2065
  this.headers.failureHeaders["Workflow-Runid"] = this.workflowConfig.workflowRunId;
2018
2066
  this.headers.failureHeaders["Workflow-Init"] = "false";
2019
2067
  this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
2020
2068
  this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
2021
- this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger";
2069
+ this.headers.failureHeaders["Feature-Set"] = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
2022
2070
  if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
2023
2071
  this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
2024
2072
  }
@@ -2088,14 +2136,13 @@ var submitParallelSteps = async ({
2088
2136
  initialStepCount,
2089
2137
  invokeCount,
2090
2138
  telemetry: telemetry2,
2091
- debug
2139
+ dispatchDebug
2092
2140
  }) => {
2093
2141
  const planSteps = steps.map(
2094
2142
  (step, index) => step.getPlanStep(steps.length, initialStepCount + index)
2095
2143
  );
2096
- await debug?.log("SUBMIT", "SUBMIT_STEP", {
2097
- length: planSteps.length,
2098
- steps: planSteps
2144
+ await dispatchDebug("onInfo", {
2145
+ info: `Submitting ${planSteps.length} parallel steps.`
2099
2146
  });
2100
2147
  const result = await context.qstashClient.batch(
2101
2148
  planSteps.map((planStep) => {
@@ -2104,10 +2151,6 @@ var submitParallelSteps = async ({
2104
2151
  workflowConfig: {
2105
2152
  workflowRunId: context.workflowRunId,
2106
2153
  workflowUrl: context.url,
2107
- failureUrl: context.failureUrl,
2108
- retries: context.retries,
2109
- retryDelay: context.retryDelay,
2110
- flowControl: context.flowControl,
2111
2154
  telemetry: telemetry2
2112
2155
  },
2113
2156
  userHeaders: context.headers,
@@ -2123,13 +2166,11 @@ var submitParallelSteps = async ({
2123
2166
  };
2124
2167
  })
2125
2168
  );
2126
- await debug?.log("INFO", "SUBMIT_STEP", {
2127
- messageIds: result.map((message) => {
2128
- return {
2129
- message: message.messageId
2130
- };
2131
- })
2132
- });
2169
+ if (result && result.length > 0) {
2170
+ await dispatchDebug("onInfo", {
2171
+ info: `Submitted ${planSteps.length} parallel steps. messageIds: ${result.filter((r) => r).map((r) => r.messageId).join(", ")}.`
2172
+ });
2173
+ }
2133
2174
  throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
2134
2175
  };
2135
2176
  var submitSingleStep = async ({
@@ -2139,14 +2180,13 @@ var submitSingleStep = async ({
2139
2180
  invokeCount,
2140
2181
  concurrency,
2141
2182
  telemetry: telemetry2,
2142
- debug
2183
+ dispatchDebug,
2184
+ dispatchLifecycle
2143
2185
  }) => {
2144
- const resultStep = await lazyStep.getResultStep(concurrency, stepId);
2145
- await debug?.log("INFO", "RUN_SINGLE", {
2146
- fromRequest: false,
2147
- step: resultStep,
2148
- stepCount: stepId
2186
+ await dispatchLifecycle("beforeExecution", {
2187
+ stepName: lazyStep.stepName
2149
2188
  });
2189
+ const resultStep = await lazyStep.getResultStep(concurrency, stepId);
2150
2190
  const { headers } = lazyStep.getHeaders({
2151
2191
  context,
2152
2192
  step: resultStep,
@@ -2160,10 +2200,6 @@ var submitSingleStep = async ({
2160
2200
  invokeCount,
2161
2201
  telemetry: telemetry2
2162
2202
  });
2163
- await debug?.log("SUBMIT", "SUBMIT_STEP", {
2164
- length: 1,
2165
- steps: [resultStep]
2166
- });
2167
2203
  const submitResult = await lazyStep.submitStep({
2168
2204
  context,
2169
2205
  body,
@@ -2173,13 +2209,11 @@ var submitSingleStep = async ({
2173
2209
  step: resultStep,
2174
2210
  telemetry: telemetry2
2175
2211
  });
2176
- await debug?.log("INFO", "SUBMIT_STEP", {
2177
- messageIds: submitResult.map((message) => {
2178
- return {
2179
- message: message.messageId
2180
- };
2181
- })
2182
- });
2212
+ if (submitResult && submitResult[0]) {
2213
+ await dispatchDebug("onInfo", {
2214
+ info: `Submitted step "${resultStep.stepName}" with messageId: ${submitResult[0].messageId}.`
2215
+ });
2216
+ }
2183
2217
  return resultStep;
2184
2218
  };
2185
2219
 
@@ -2188,21 +2222,31 @@ var AutoExecutor = class _AutoExecutor {
2188
2222
  context;
2189
2223
  promises = /* @__PURE__ */ new WeakMap();
2190
2224
  activeLazyStepList;
2191
- debug;
2192
2225
  nonPlanStepCount;
2193
2226
  steps;
2194
2227
  indexInCurrentList = 0;
2195
2228
  invokeCount;
2196
2229
  telemetry;
2230
+ dispatchDebug;
2231
+ dispatchLifecycle;
2197
2232
  stepCount = 0;
2198
2233
  planStepCount = 0;
2199
2234
  executingStep = false;
2200
- constructor(context, steps, telemetry2, invokeCount, debug) {
2235
+ /**
2236
+ * @param context workflow context
2237
+ * @param steps list of steps
2238
+ * @param dispatchDebug debug event dispatcher
2239
+ * @param dispatchLifecycle lifecycle event dispatcher
2240
+ * @param telemetry optional telemetry information
2241
+ * @param invokeCount optional invoke count
2242
+ */
2243
+ constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount) {
2201
2244
  this.context = context;
2202
2245
  this.steps = steps;
2246
+ this.dispatchDebug = dispatchDebug;
2247
+ this.dispatchLifecycle = dispatchLifecycle;
2203
2248
  this.telemetry = telemetry2;
2204
2249
  this.invokeCount = invokeCount ?? 0;
2205
- this.debug = debug;
2206
2250
  this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
2207
2251
  }
2208
2252
  /**
@@ -2270,7 +2314,7 @@ var AutoExecutor = class _AutoExecutor {
2270
2314
  /**
2271
2315
  * Executes a step:
2272
2316
  * - If the step result is available in the steps, returns the result
2273
- * - If the result is not avaiable, runs the function
2317
+ * - If the result is not available, runs the function
2274
2318
  * - Sends the result to QStash
2275
2319
  *
2276
2320
  * @param lazyStep lazy step to execute
@@ -2280,12 +2324,15 @@ var AutoExecutor = class _AutoExecutor {
2280
2324
  if (this.stepCount < this.nonPlanStepCount) {
2281
2325
  const step = this.steps[this.stepCount + this.planStepCount];
2282
2326
  validateStep(lazyStep, step);
2283
- await this.debug?.log("INFO", "RUN_SINGLE", {
2284
- fromRequest: true,
2285
- step,
2286
- stepCount: this.stepCount
2287
- });
2288
- return lazyStep.parseOut(step.out);
2327
+ const parsedOut = lazyStep.parseOut(step);
2328
+ const isLastMemoized = this.stepCount + 1 === this.nonPlanStepCount && this.steps.at(-1).stepId !== 0;
2329
+ if (isLastMemoized) {
2330
+ await this.dispatchLifecycle("afterExecution", {
2331
+ stepName: lazyStep.stepName,
2332
+ result: parsedOut
2333
+ });
2334
+ }
2335
+ return parsedOut;
2289
2336
  }
2290
2337
  const resultStep = await submitSingleStep({
2291
2338
  context: this.context,
@@ -2294,15 +2341,15 @@ var AutoExecutor = class _AutoExecutor {
2294
2341
  invokeCount: this.invokeCount,
2295
2342
  concurrency: 1,
2296
2343
  telemetry: this.telemetry,
2297
- debug: this.debug
2344
+ dispatchDebug: this.dispatchDebug,
2345
+ dispatchLifecycle: this.dispatchLifecycle
2298
2346
  });
2299
2347
  throw new WorkflowAbort(lazyStep.stepName, resultStep);
2300
2348
  }
2301
2349
  /**
2302
2350
  * Runs steps in parallel.
2303
2351
  *
2304
- * @param stepName parallel step name
2305
- * @param stepFunctions list of async functions to run in parallel
2352
+ * @param parallelSteps list of lazy steps to run in parallel
2306
2353
  * @returns results of the functions run in parallel
2307
2354
  */
2308
2355
  async runParallel(parallelSteps) {
@@ -2315,12 +2362,14 @@ var AutoExecutor = class _AutoExecutor {
2315
2362
  `Incompatible number of parallel steps when call state was '${parallelCallState}'. Expected ${parallelSteps.length}, got ${plannedParallelStepCount} from the request.`
2316
2363
  );
2317
2364
  }
2318
- await this.debug?.log("INFO", "RUN_PARALLEL", {
2319
- parallelCallState,
2320
- initialStepCount,
2321
- plannedParallelStepCount,
2322
- stepCount: this.stepCount,
2323
- planStepCount: this.planStepCount
2365
+ await this.dispatchDebug("onInfo", {
2366
+ info: `Executing parallel steps with: ` + JSON.stringify({
2367
+ parallelCallState,
2368
+ initialStepCount,
2369
+ plannedParallelStepCount,
2370
+ stepCount: this.stepCount,
2371
+ planStepCount: this.planStepCount
2372
+ })
2324
2373
  });
2325
2374
  switch (parallelCallState) {
2326
2375
  case "first": {
@@ -2330,7 +2379,7 @@ var AutoExecutor = class _AutoExecutor {
2330
2379
  initialStepCount,
2331
2380
  invokeCount: this.invokeCount,
2332
2381
  telemetry: this.telemetry,
2333
- debug: this.debug
2382
+ dispatchDebug: this.dispatchDebug
2334
2383
  });
2335
2384
  break;
2336
2385
  }
@@ -2352,7 +2401,8 @@ var AutoExecutor = class _AutoExecutor {
2352
2401
  invokeCount: this.invokeCount,
2353
2402
  concurrency: parallelSteps.length,
2354
2403
  telemetry: this.telemetry,
2355
- debug: this.debug
2404
+ dispatchDebug: this.dispatchDebug,
2405
+ dispatchLifecycle: this.dispatchLifecycle
2356
2406
  });
2357
2407
  throw new WorkflowAbort(parallelStep.stepName, resultStep);
2358
2408
  } catch (error) {
@@ -2366,13 +2416,34 @@ var AutoExecutor = class _AutoExecutor {
2366
2416
  break;
2367
2417
  }
2368
2418
  case "discard": {
2419
+ const resultStep = this.steps.at(-1);
2420
+ const lazyStep = parallelSteps.find(
2421
+ (planStep, index) => resultStep.stepId - index === initialStepCount
2422
+ );
2423
+ if (lazyStep) {
2424
+ await this.dispatchLifecycle("afterExecution", {
2425
+ stepName: lazyStep.stepName,
2426
+ result: lazyStep.parseOut(resultStep)
2427
+ });
2428
+ }
2369
2429
  throw new WorkflowAbort("discarded parallel");
2370
2430
  }
2371
2431
  case "last": {
2372
2432
  const parallelResultSteps = sortedSteps.filter((step) => step.stepId >= initialStepCount).slice(0, parallelSteps.length);
2373
2433
  validateParallelSteps(parallelSteps, parallelResultSteps);
2434
+ const isLastMemoized = this.stepCount + 1 === this.nonPlanStepCount && this.steps.at(-1).stepId !== 0;
2435
+ if (isLastMemoized) {
2436
+ const resultStep = this.steps.at(-1);
2437
+ const lazyStep = parallelSteps.find(
2438
+ (planStep, index) => resultStep.stepId - index === initialStepCount
2439
+ );
2440
+ await this.dispatchLifecycle("afterExecution", {
2441
+ stepName: lazyStep.stepName,
2442
+ result: lazyStep.parseOut(resultStep)
2443
+ });
2444
+ }
2374
2445
  return parallelResultSteps.map(
2375
- (step, index) => parallelSteps[index].parseOut(step.out)
2446
+ (step, index) => parallelSteps[index].parseOut(step)
2376
2447
  );
2377
2448
  }
2378
2449
  }
@@ -2428,7 +2499,6 @@ var AutoExecutor = class _AutoExecutor {
2428
2499
  * @param index index of the current step
2429
2500
  * @returns result[index] if lazyStepList > 1, otherwise result
2430
2501
  */
2431
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
2432
2502
  static getResult(lazyStepList, result, index) {
2433
2503
  if (lazyStepList.length === 1) {
2434
2504
  return result;
@@ -2515,15 +2585,18 @@ var getProviderInfo = (api) => {
2515
2585
  // src/context/api/base.ts
2516
2586
  var BaseWorkflowApi = class {
2517
2587
  context;
2588
+ /**
2589
+ * @param context workflow context
2590
+ */
2518
2591
  constructor({ context }) {
2519
2592
  this.context = context;
2520
2593
  }
2521
2594
  /**
2522
2595
  * context.call which uses a QStash API
2523
2596
  *
2524
- * @param stepName
2525
- * @param settings
2526
- * @returns
2597
+ * @param stepName name of the step
2598
+ * @param settings call settings including api configuration
2599
+ * @returns call response
2527
2600
  */
2528
2601
  async callApi(stepName, settings) {
2529
2602
  const { url, appendHeaders, method } = getProviderInfo(settings.api);
@@ -2531,7 +2604,7 @@ var BaseWorkflowApi = class {
2531
2604
  return await this.context.call(stepName, {
2532
2605
  url,
2533
2606
  method: userMethod ?? method,
2534
- body,
2607
+ body: typeof body === "string" ? body : JSON.stringify(body),
2535
2608
  headers: {
2536
2609
  ...appendHeaders,
2537
2610
  ...headers
@@ -2683,6 +2756,129 @@ var getNewUrlFromWorkflowId = (url, workflowId) => {
2683
2756
  return url.replace(/[^/]+$/, workflowId);
2684
2757
  };
2685
2758
 
2759
+ // src/middleware/default-callbacks.ts
2760
+ var onErrorWithConsole = async ({ workflowRunId, error }) => {
2761
+ console.error(` [Upstash Workflow]: Error in workflow run ${workflowRunId}: ` + error);
2762
+ };
2763
+ var onWarningWithConsole = async ({ workflowRunId, warning }) => {
2764
+ console.warn(` [Upstash Workflow]: Warning in workflow run ${workflowRunId}: ` + warning);
2765
+ };
2766
+ var onInfoWithConsole = async ({
2767
+ workflowRunId,
2768
+ info
2769
+ }) => {
2770
+ console.info(` [Upstash Workflow]: Info in workflow run ${workflowRunId}: ` + info);
2771
+ };
2772
+
2773
+ // src/middleware/manager.ts
2774
+ var MiddlewareManager = class {
2775
+ middlewares;
2776
+ workflowRunId;
2777
+ context;
2778
+ /**
2779
+ * @param middlewares list of workflow middlewares
2780
+ */
2781
+ constructor(middlewares = []) {
2782
+ this.middlewares = middlewares;
2783
+ }
2784
+ /**
2785
+ * Assign workflow run ID - will be passed to debug events
2786
+ *
2787
+ * @param workflowRunId workflow run id to assign
2788
+ */
2789
+ assignWorkflowRunId(workflowRunId) {
2790
+ this.workflowRunId = workflowRunId;
2791
+ }
2792
+ /**
2793
+ * Assign context - required for lifecycle events
2794
+ *
2795
+ * also assigns workflowRunId from context
2796
+ *
2797
+ * @param context workflow context to assign
2798
+ */
2799
+ assignContext(context) {
2800
+ this.context = context;
2801
+ this.workflowRunId = context.workflowRunId;
2802
+ }
2803
+ /**
2804
+ * Internal method to execute middlewares with common error handling logic
2805
+ *
2806
+ * @param event event name to dispatch
2807
+ * @param params event parameters
2808
+ */
2809
+ async executeMiddlewares(event, params) {
2810
+ await Promise.all(this.middlewares.map((m) => m.ensureInit()));
2811
+ await Promise.all(
2812
+ this.middlewares.map(async (middleware) => {
2813
+ const callback = middleware.getCallback(event);
2814
+ if (callback) {
2815
+ try {
2816
+ await callback(params);
2817
+ } catch (error) {
2818
+ try {
2819
+ const onErrorCallback = middleware.getCallback("onError") ?? onErrorWithConsole;
2820
+ await onErrorCallback({
2821
+ workflowRunId: this.workflowRunId,
2822
+ error
2823
+ });
2824
+ } catch (onErrorError) {
2825
+ console.error(
2826
+ `Failed while executing "onError" of middleware "${middleware.name}", falling back to logging the error to console. Error: ${onErrorError}`
2827
+ );
2828
+ onErrorWithConsole({
2829
+ workflowRunId: this.workflowRunId,
2830
+ error
2831
+ });
2832
+ }
2833
+ }
2834
+ }
2835
+ })
2836
+ );
2837
+ if (event === "onError") {
2838
+ onErrorWithConsole({
2839
+ workflowRunId: this.workflowRunId,
2840
+ ...params
2841
+ });
2842
+ } else if (event === "onWarning") {
2843
+ onWarningWithConsole({
2844
+ workflowRunId: this.workflowRunId,
2845
+ ...params
2846
+ });
2847
+ }
2848
+ }
2849
+ /**
2850
+ * Dispatch a debug event (onError, onWarning, onInfo)
2851
+ *
2852
+ * @param event debug event name
2853
+ * @param params event parameters
2854
+ */
2855
+ async dispatchDebug(event, params) {
2856
+ const paramsWithRunId = {
2857
+ ...params,
2858
+ workflowRunId: this.workflowRunId
2859
+ };
2860
+ await this.executeMiddlewares(event, paramsWithRunId);
2861
+ }
2862
+ /**
2863
+ * Dispatch a lifecycle event (beforeExecution, afterExecution, runStarted, runCompleted)
2864
+ *
2865
+ * @param event lifecycle event name
2866
+ * @param params event parameters
2867
+ */
2868
+ async dispatchLifecycle(event, params) {
2869
+ if (!this.context) {
2870
+ throw new WorkflowError(
2871
+ `Something went wrong while calling middlewares. Lifecycle event "${event}" was called before assignContext.`
2872
+ );
2873
+ }
2874
+ const paramsWithContext = {
2875
+ ...params,
2876
+ context: this.context
2877
+ };
2878
+ await this.executeMiddlewares(event, paramsWithContext);
2879
+ }
2880
+ };
2881
+
2686
2882
  // src/context/context.ts
2687
2883
  var WorkflowContext = class {
2688
2884
  executor;
@@ -2728,32 +2924,13 @@ var WorkflowContext = class {
2728
2924
  */
2729
2925
  url;
2730
2926
  /**
2731
- * URL to call in case of workflow failure with QStash failure callback
2732
- *
2733
- * https://upstash.com/docs/qstash/features/callbacks#what-is-a-failure-callback
2927
+ * Payload of the request which started the workflow.
2734
2928
  *
2735
- * Can be overwritten by passing a `failureUrl` parameter in `serve`:
2929
+ * To specify its type, you can define `serve` as follows:
2736
2930
  *
2737
2931
  * ```ts
2738
- * export const POST = serve(
2739
- * async (context) => {
2740
- * ...
2741
- * },
2742
- * {
2743
- * failureUrl: "new-url-value"
2744
- * }
2745
- * )
2746
- * ```
2747
- */
2748
- failureUrl;
2749
- /**
2750
- * Payload of the request which started the workflow.
2751
- *
2752
- * To specify its type, you can define `serve` as follows:
2753
- *
2754
- * ```ts
2755
- * // set requestPayload type to MyPayload:
2756
- * export const POST = serve<MyPayload>(
2932
+ * // set requestPayload type to MyPayload:
2933
+ * export const POST = serve<MyPayload>(
2757
2934
  * async (context) => {
2758
2935
  * ...
2759
2936
  * }
@@ -2801,46 +2978,6 @@ var WorkflowContext = class {
2801
2978
  * Default value is set to `process.env`.
2802
2979
  */
2803
2980
  env;
2804
- /**
2805
- * Number of retries
2806
- */
2807
- retries;
2808
- /**
2809
- * Delay between retries.
2810
- *
2811
- * By default, the `retryDelay` is exponential backoff.
2812
- * More details can be found in: https://upstash.com/docs/qstash/features/retry.
2813
- *
2814
- * The `retryDelay` option allows you to customize the delay (in milliseconds) between retry attempts when message delivery fails.
2815
- *
2816
- * You can use mathematical expressions and the following built-in functions to calculate the delay dynamically.
2817
- * The special variable `retried` represents the current retry attempt count (starting from 0).
2818
- *
2819
- * Supported functions:
2820
- * - `pow`
2821
- * - `sqrt`
2822
- * - `abs`
2823
- * - `exp`
2824
- * - `floor`
2825
- * - `ceil`
2826
- * - `round`
2827
- * - `min`
2828
- * - `max`
2829
- *
2830
- * Examples of valid `retryDelay` values:
2831
- * ```ts
2832
- * 1000 // 1 second
2833
- * 1000 * (1 + retried) // 1 second multiplied by the current retry attempt
2834
- * pow(2, retried) // 2 to the power of the current retry attempt
2835
- * max(10, pow(2, retried)) // The greater of 10 or 2^retried
2836
- * ```
2837
- */
2838
- retryDelay;
2839
- /**
2840
- * Settings for controlling the number of active requests
2841
- * and number of requests per second with the same key.
2842
- */
2843
- flowControl;
2844
2981
  /**
2845
2982
  * Label to apply to the workflow run.
2846
2983
  *
@@ -2863,30 +3000,31 @@ var WorkflowContext = class {
2863
3000
  headers,
2864
3001
  steps,
2865
3002
  url,
2866
- failureUrl,
2867
- debug,
2868
3003
  initialPayload,
2869
3004
  env,
2870
- retries,
2871
- retryDelay,
2872
3005
  telemetry: telemetry2,
2873
3006
  invokeCount,
2874
- flowControl,
2875
- label
3007
+ label,
3008
+ middlewareManager
2876
3009
  }) {
2877
3010
  this.qstashClient = qstashClient;
2878
3011
  this.workflowRunId = workflowRunId;
2879
3012
  this.steps = steps;
2880
3013
  this.url = url;
2881
- this.failureUrl = failureUrl;
2882
3014
  this.headers = headers;
2883
3015
  this.requestPayload = initialPayload;
2884
3016
  this.env = env ?? {};
2885
- this.retries = retries ?? DEFAULT_RETRIES;
2886
- this.retryDelay = retryDelay;
2887
- this.flowControl = flowControl;
2888
3017
  this.label = label;
2889
- this.executor = new AutoExecutor(this, this.steps, telemetry2, invokeCount, debug);
3018
+ const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
3019
+ middlewareManagerInstance.assignContext(this);
3020
+ this.executor = new AutoExecutor(
3021
+ this,
3022
+ this.steps,
3023
+ middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
3024
+ middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
3025
+ telemetry2,
3026
+ invokeCount
3027
+ );
2890
3028
  }
2891
3029
  /**
2892
3030
  * Executes a workflow step
@@ -2917,7 +3055,7 @@ var WorkflowContext = class {
2917
3055
  */
2918
3056
  async run(stepName, stepFunction) {
2919
3057
  const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
2920
- return await this.addStep(new LazyFunctionStep(stepName, wrappedStepFunction));
3058
+ return await this.addStep(new LazyFunctionStep(this, stepName, wrappedStepFunction));
2921
3059
  }
2922
3060
  /**
2923
3061
  * Stops the execution for the duration provided.
@@ -2931,7 +3069,7 @@ var WorkflowContext = class {
2931
3069
  * @returns undefined
2932
3070
  */
2933
3071
  async sleep(stepName, duration) {
2934
- await this.addStep(new LazySleepStep(stepName, duration));
3072
+ await this.addStep(new LazySleepStep(this, stepName, duration));
2935
3073
  }
2936
3074
  /**
2937
3075
  * Stops the execution until the date time provided.
@@ -2953,48 +3091,38 @@ var WorkflowContext = class {
2953
3091
  datetime = typeof datetime === "string" ? new Date(datetime) : datetime;
2954
3092
  time = Math.round(datetime.getTime() / 1e3);
2955
3093
  }
2956
- await this.addStep(new LazySleepUntilStep(stepName, time));
3094
+ await this.addStep(new LazySleepUntilStep(this, stepName, time));
2957
3095
  }
2958
3096
  async call(stepName, settings) {
2959
3097
  let callStep;
2960
3098
  if ("workflow" in settings) {
2961
3099
  const url = getNewUrlFromWorkflowId(this.url, settings.workflow.workflowId);
2962
- callStep = new LazyCallStep(
3100
+ const stringBody = typeof settings.body === "string" ? settings.body : settings.body === void 0 ? void 0 : JSON.stringify(settings.body);
3101
+ callStep = new LazyCallStep({
3102
+ context: this,
2963
3103
  stepName,
2964
3104
  url,
2965
- "POST",
2966
- settings.body,
2967
- settings.headers || {},
2968
- settings.retries || 0,
2969
- settings.retryDelay,
2970
- settings.timeout,
2971
- settings.flowControl ?? settings.workflow.options.flowControl,
2972
- settings.stringifyBody ?? true
2973
- );
3105
+ method: "POST",
3106
+ body: stringBody,
3107
+ headers: settings.headers || {},
3108
+ retries: settings.retries || 0,
3109
+ retryDelay: settings.retryDelay,
3110
+ timeout: settings.timeout,
3111
+ flowControl: settings.flowControl
3112
+ });
2974
3113
  } else {
2975
- const {
2976
- url,
2977
- method = "GET",
2978
- body,
2979
- headers = {},
2980
- retries = 0,
2981
- retryDelay,
2982
- timeout,
2983
- flowControl,
2984
- stringifyBody = true
2985
- } = settings;
2986
- callStep = new LazyCallStep(
3114
+ callStep = new LazyCallStep({
3115
+ context: this,
2987
3116
  stepName,
2988
- url,
2989
- method,
2990
- body,
2991
- headers,
2992
- retries,
2993
- retryDelay,
2994
- timeout,
2995
- flowControl,
2996
- stringifyBody
2997
- );
3117
+ url: settings.url,
3118
+ method: settings.method ?? "GET",
3119
+ body: settings.body,
3120
+ headers: settings.headers ?? {},
3121
+ retries: settings.retries ?? 0,
3122
+ retryDelay: settings.retryDelay,
3123
+ timeout: settings.timeout,
3124
+ flowControl: settings.flowControl
3125
+ });
2998
3126
  }
2999
3127
  return await this.addStep(callStep);
3000
3128
  }
@@ -3035,7 +3163,9 @@ var WorkflowContext = class {
3035
3163
  async waitForEvent(stepName, eventId, options = {}) {
3036
3164
  const { timeout = "7d" } = options;
3037
3165
  const timeoutStr = typeof timeout === "string" ? timeout : `${timeout}s`;
3038
- return await this.addStep(new LazyWaitForEventStep(stepName, eventId, timeoutStr));
3166
+ return await this.addStep(
3167
+ new LazyWaitForEventStep(this, stepName, eventId, timeoutStr)
3168
+ );
3039
3169
  }
3040
3170
  /**
3041
3171
  * Notify workflow runs waiting for an event
@@ -3060,20 +3190,28 @@ var WorkflowContext = class {
3060
3190
  */
3061
3191
  async notify(stepName, eventId, eventData) {
3062
3192
  return await this.addStep(
3063
- new LazyNotifyStep(stepName, eventId, eventData, this.qstashClient.http)
3193
+ new LazyNotifyStep(this, stepName, eventId, eventData, this.qstashClient.http)
3064
3194
  );
3065
3195
  }
3066
3196
  async invoke(stepName, settings) {
3067
- return await this.addStep(new LazyInvokeStep(stepName, settings));
3197
+ return await this.addStep(
3198
+ new LazyInvokeStep(this, stepName, settings)
3199
+ );
3200
+ }
3201
+ async createWebhook(stepName) {
3202
+ return await this.addStep(new LazyCreateWebhookStep(this, stepName));
3203
+ }
3204
+ async waitForWebhook(stepName, webhook, timeout) {
3205
+ return await this.addStep(new LazyWaitForWebhookStep(this, stepName, webhook, timeout));
3068
3206
  }
3069
3207
  /**
3070
3208
  * Cancel the current workflow run
3071
3209
  *
3072
- * Will throw WorkflowAbort to stop workflow execution.
3210
+ * Will throw WorkflowCancelAbort to stop workflow execution.
3073
3211
  * Shouldn't be inside try/catch.
3074
3212
  */
3075
3213
  async cancel() {
3076
- throw new WorkflowAbort("cancel", void 0, true);
3214
+ throw new WorkflowCancelAbort();
3077
3215
  }
3078
3216
  /**
3079
3217
  * Adds steps to the executor. Needed so that it can be overwritten in
@@ -3089,74 +3227,25 @@ var WorkflowContext = class {
3089
3227
  }
3090
3228
  };
3091
3229
 
3092
- // src/logger.ts
3093
- var LOG_LEVELS = ["DEBUG", "INFO", "SUBMIT", "WARN", "ERROR"];
3094
- var WorkflowLogger = class _WorkflowLogger {
3095
- logs = [];
3096
- options;
3097
- workflowRunId = void 0;
3098
- constructor(options) {
3099
- this.options = options;
3100
- }
3101
- async log(level, eventType, details) {
3102
- if (this.shouldLog(level)) {
3103
- const timestamp = Date.now();
3104
- const logEntry = {
3105
- timestamp,
3106
- workflowRunId: this.workflowRunId ?? "",
3107
- logLevel: level,
3108
- eventType,
3109
- details
3110
- };
3111
- this.logs.push(logEntry);
3112
- if (this.options.logOutput === "console") {
3113
- this.writeToConsole(logEntry);
3114
- }
3115
- await new Promise((resolve) => setTimeout(resolve, 100));
3116
- }
3117
- }
3118
- setWorkflowRunId(workflowRunId) {
3119
- this.workflowRunId = workflowRunId;
3120
- }
3121
- writeToConsole(logEntry) {
3122
- const JSON_SPACING = 2;
3123
- const logMethod = logEntry.logLevel === "ERROR" ? console.error : logEntry.logLevel === "WARN" ? console.warn : console.log;
3124
- logMethod(JSON.stringify(logEntry, void 0, JSON_SPACING));
3125
- }
3126
- shouldLog(level) {
3127
- return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.options.logLevel);
3128
- }
3129
- static getLogger(verbose) {
3130
- if (typeof verbose === "object") {
3131
- return verbose;
3132
- } else {
3133
- return verbose ? new _WorkflowLogger({
3134
- logLevel: "INFO",
3135
- logOutput: "console"
3136
- }) : void 0;
3137
- }
3138
- }
3139
- };
3140
-
3141
3230
  // src/serve/authorization.ts
3142
3231
  var import_qstash9 = require("@upstash/qstash");
3143
3232
  var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
3144
3233
  static disabledMessage = "disabled-qstash-worklfow-run";
3145
3234
  disabled = true;
3146
3235
  /**
3147
- * overwrite the WorkflowContext.addStep method to always raise WorkflowAbort
3236
+ * overwrite the WorkflowContext.addStep method to always raise WorkflowAuthError
3148
3237
  * error in order to stop the execution whenever we encounter a step.
3149
3238
  *
3150
3239
  * @param _step
3151
3240
  */
3152
3241
  async addStep(_step) {
3153
- throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
3242
+ throw new WorkflowAuthError(_DisabledWorkflowContext.disabledMessage);
3154
3243
  }
3155
3244
  /**
3156
- * overwrite cancel method to throw WorkflowAbort with the disabledMessage
3245
+ * overwrite cancel method to throw WorkflowAuthError with the disabledMessage
3157
3246
  */
3158
3247
  async cancel() {
3159
- throw new WorkflowAbort(_DisabledWorkflowContext.disabledMessage);
3248
+ throw new WorkflowAuthError(_DisabledWorkflowContext.disabledMessage);
3160
3249
  }
3161
3250
  /**
3162
3251
  * copies the passed context to create a DisabledWorkflowContext. Then, runs the
@@ -3179,18 +3268,14 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
3179
3268
  headers: context.headers,
3180
3269
  steps: [],
3181
3270
  url: context.url,
3182
- failureUrl: context.failureUrl,
3183
3271
  initialPayload: context.requestPayload,
3184
3272
  env: context.env,
3185
- retries: context.retries,
3186
- retryDelay: context.retryDelay,
3187
- flowControl: context.flowControl,
3188
3273
  label: context.label
3189
3274
  });
3190
3275
  try {
3191
3276
  await routeFunction(disabledContext);
3192
3277
  } catch (error) {
3193
- if (isInstanceOf(error, WorkflowAbort) && error.stepName === this.disabledMessage || isInstanceOf(error, WorkflowNonRetryableError) || isInstanceOf(error, WorkflowRetryAfterError)) {
3278
+ if (isInstanceOf(error, WorkflowAuthError) && error.stepName === this.disabledMessage || isInstanceOf(error, WorkflowNonRetryableError) || isInstanceOf(error, WorkflowRetryAfterError)) {
3194
3279
  return ok("step-found");
3195
3280
  }
3196
3281
  console.warn(
@@ -3223,13 +3308,6 @@ var processRawSteps = (rawSteps) => {
3223
3308
  const stepsToDecode = encodedSteps.filter((step) => step.callType === "step");
3224
3309
  const otherSteps = stepsToDecode.map((rawStep) => {
3225
3310
  const step = JSON.parse(decodeBase64(rawStep.body));
3226
- if (step.waitEventId) {
3227
- const newOut = {
3228
- eventData: step.out ? decodeBase64(step.out) : void 0,
3229
- timeout: step.waitTimeout ?? false
3230
- };
3231
- step.out = newOut;
3232
- }
3233
3311
  return step;
3234
3312
  });
3235
3313
  const steps = [initialStep, ...otherSteps];
@@ -3257,7 +3335,7 @@ var deduplicateSteps = (steps) => {
3257
3335
  }
3258
3336
  return deduplicatedSteps;
3259
3337
  };
3260
- var checkIfLastOneIsDuplicate = async (steps, debug) => {
3338
+ var checkIfLastOneIsDuplicate = async (steps, dispatchDebug) => {
3261
3339
  if (steps.length < 2) {
3262
3340
  return false;
3263
3341
  }
@@ -3268,14 +3346,41 @@ var checkIfLastOneIsDuplicate = async (steps, debug) => {
3268
3346
  const step = steps[index];
3269
3347
  if (step.stepId === lastStepId && step.targetStep === lastTargetStepId) {
3270
3348
  const message = `Upstash Workflow: The step '${step.stepName}' with id '${step.stepId}' has run twice during workflow execution. Rest of the workflow will continue running as usual.`;
3271
- await debug?.log("WARN", "RESPONSE_DEFAULT", message);
3272
- console.warn(message);
3349
+ await dispatchDebug?.("onWarning", {
3350
+ warning: message
3351
+ });
3273
3352
  return true;
3274
3353
  }
3275
3354
  }
3276
3355
  return false;
3277
3356
  };
3278
3357
  var validateRequest = (request) => {
3358
+ if (request.headers.get(WORKFLOW_UNKOWN_SDK_VERSION_HEADER)) {
3359
+ const workflowRunId2 = request.headers.get(WORKFLOW_ID_HEADER);
3360
+ if (!workflowRunId2) {
3361
+ throw new WorkflowError(
3362
+ "Couldn't get workflow id from header when handling unknown sdk request"
3363
+ );
3364
+ }
3365
+ return {
3366
+ unknownSdk: true,
3367
+ isFirstInvocation: true,
3368
+ workflowRunId: workflowRunId2
3369
+ };
3370
+ }
3371
+ if (request.headers.get(WORKFLOW_FAILURE_CALLBACK_HEADER)) {
3372
+ const workflowRunId2 = request.headers.get(WORKFLOW_ID_HEADER);
3373
+ if (!workflowRunId2) {
3374
+ throw new WorkflowError(
3375
+ "Couldn't get workflow id from header when handling failure callback request"
3376
+ );
3377
+ }
3378
+ return {
3379
+ unknownSdk: false,
3380
+ isFirstInvocation: true,
3381
+ workflowRunId: workflowRunId2
3382
+ };
3383
+ }
3279
3384
  const versionHeader = request.headers.get(WORKFLOW_PROTOCOL_VERSION_HEADER);
3280
3385
  const isFirstInvocation = !versionHeader;
3281
3386
  if (!isFirstInvocation && versionHeader !== WORKFLOW_PROTOCOL_VERSION) {
@@ -3289,11 +3394,20 @@ var validateRequest = (request) => {
3289
3394
  }
3290
3395
  return {
3291
3396
  isFirstInvocation,
3292
- workflowRunId
3397
+ workflowRunId,
3398
+ unknownSdk: false
3293
3399
  };
3294
3400
  };
3295
- var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requester, messageId, debug) => {
3296
- if (isFirstInvocation) {
3401
+ var parseRequest = async ({
3402
+ requestPayload,
3403
+ isFirstInvocation,
3404
+ unknownSdk,
3405
+ workflowRunId,
3406
+ requester,
3407
+ messageId,
3408
+ dispatchDebug
3409
+ }) => {
3410
+ if (isFirstInvocation && !unknownSdk) {
3297
3411
  return {
3298
3412
  rawInitialPayload: requestPayload ?? "",
3299
3413
  steps: [],
@@ -3303,16 +3417,14 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
3303
3417
  } else {
3304
3418
  let rawSteps;
3305
3419
  if (!requestPayload) {
3306
- await debug?.log(
3307
- "INFO",
3308
- "ENDPOINT_START",
3309
- "request payload is empty, steps will be fetched from QStash."
3310
- );
3420
+ await dispatchDebug?.("onInfo", {
3421
+ info: "request payload is empty, steps will be fetched from QStash."
3422
+ });
3311
3423
  const { steps: fetchedSteps, workflowRunEnded } = await getSteps(
3312
3424
  requester,
3313
3425
  workflowRunId,
3314
3426
  messageId,
3315
- debug
3427
+ dispatchDebug
3316
3428
  );
3317
3429
  if (workflowRunEnded) {
3318
3430
  return {
@@ -3327,7 +3439,7 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
3327
3439
  rawSteps = JSON.parse(requestPayload);
3328
3440
  }
3329
3441
  const { rawInitialPayload, steps } = processRawSteps(rawSteps);
3330
- const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, debug);
3442
+ const isLastDuplicate = await checkIfLastOneIsDuplicate(steps, dispatchDebug);
3331
3443
  const deduplicatedSteps = deduplicateSteps(steps);
3332
3444
  return {
3333
3445
  rawInitialPayload,
@@ -3337,16 +3449,21 @@ var parseRequest = async (requestPayload, isFirstInvocation, workflowRunId, requ
3337
3449
  };
3338
3450
  }
3339
3451
  };
3340
- var handleFailure = async (request, requestPayload, qstashClient, initialPayloadParser, routeFunction, failureFunction, env, retries, retryDelay, flowControl, debug) => {
3341
- if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true") {
3452
+ var handleFailure = async ({
3453
+ request,
3454
+ requestPayload,
3455
+ qstashClient,
3456
+ initialPayloadParser,
3457
+ routeFunction,
3458
+ failureFunction,
3459
+ env,
3460
+ dispatchDebug
3461
+ }) => {
3462
+ if (request.headers.get(WORKFLOW_FAILURE_HEADER) !== "true" && !request.headers.get(WORKFLOW_FAILURE_CALLBACK_HEADER)) {
3342
3463
  return ok({ result: "not-failure-callback" });
3343
3464
  }
3344
3465
  if (!failureFunction) {
3345
- return err(
3346
- new WorkflowError(
3347
- "Workflow endpoint is called to handle a failure, but a failureFunction is not provided in serve options. Either provide a failureUrl or a failureFunction."
3348
- )
3349
- );
3466
+ return ok({ result: "failure-function-undefined" });
3350
3467
  }
3351
3468
  try {
3352
3469
  const { status, header, body, url, sourceBody, workflowRunId } = JSON.parse(requestPayload);
@@ -3374,23 +3491,21 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
3374
3491
  headers: userHeaders,
3375
3492
  steps: [],
3376
3493
  url,
3377
- failureUrl: url,
3378
- debug,
3379
3494
  env,
3380
- retries,
3381
- retryDelay,
3382
- flowControl,
3383
3495
  telemetry: void 0,
3384
3496
  // not going to make requests in authentication check
3385
- label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0
3497
+ label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
3498
+ middlewareManager: void 0
3386
3499
  });
3387
3500
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3388
3501
  routeFunction,
3389
3502
  workflowContext
3390
3503
  );
3391
3504
  if (authCheck.isErr()) {
3392
- await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
3393
- throw authCheck.error;
3505
+ await dispatchDebug?.("onError", {
3506
+ error: authCheck.error
3507
+ });
3508
+ return err(authCheck.error);
3394
3509
  } else if (authCheck.value === "run-ended") {
3395
3510
  return err(new WorkflowError("Not authorized to run the failure function."));
3396
3511
  }
@@ -3401,74 +3516,309 @@ var handleFailure = async (request, requestPayload, qstashClient, initialPayload
3401
3516
  failHeaders: header,
3402
3517
  failStack
3403
3518
  });
3404
- return ok({ result: "is-failure-callback", response: failureResponse });
3519
+ return ok({ result: "failure-function-executed", response: failureResponse });
3405
3520
  } catch (error) {
3406
3521
  return err(error);
3407
3522
  }
3408
3523
  };
3409
3524
 
3410
- // src/serve/options.ts
3525
+ // src/serve/multi-region/handlers.ts
3411
3526
  var import_qstash10 = require("@upstash/qstash");
3412
- var import_qstash11 = require("@upstash/qstash");
3413
- var processOptions = (options) => {
3414
- const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
3415
- const receiverEnvironmentVariablesSet = Boolean(
3416
- environment.QSTASH_CURRENT_SIGNING_KEY && environment.QSTASH_NEXT_SIGNING_KEY
3527
+
3528
+ // src/serve/multi-region/utils.ts
3529
+ var VALID_REGIONS = ["EU_CENTRAL_1", "US_EAST_1"];
3530
+ var getRegionFromEnvironment = (environment) => {
3531
+ const region = environment.QSTASH_REGION;
3532
+ return normalizeRegionHeader(region);
3533
+ };
3534
+ function readEnvironmentVariables(environmentVariables, environment, region) {
3535
+ const result = {};
3536
+ for (const variable of environmentVariables) {
3537
+ const key = region ? `${region}_${variable}` : variable;
3538
+ result[variable] = environment[key];
3539
+ }
3540
+ return result;
3541
+ }
3542
+ function readClientEnvironmentVariables(environment, region) {
3543
+ return readEnvironmentVariables(["QSTASH_URL", "QSTASH_TOKEN"], environment, region);
3544
+ }
3545
+ function readReceiverEnvironmentVariables(environment, region) {
3546
+ return readEnvironmentVariables(
3547
+ ["QSTASH_CURRENT_SIGNING_KEY", "QSTASH_NEXT_SIGNING_KEY"],
3548
+ environment,
3549
+ region
3417
3550
  );
3418
- return {
3419
- qstashClient: options?.qstashClient ?? new import_qstash11.Client({
3420
- baseUrl: environment.QSTASH_URL,
3421
- token: environment.QSTASH_TOKEN
3422
- }),
3423
- onStepFinish: (workflowRunId, _finishCondition, detailedFinishCondition) => {
3424
- if (detailedFinishCondition?.condition === "auth-fail") {
3425
- console.error(AUTH_FAIL_MESSAGE);
3426
- return new Response(
3427
- JSON.stringify({
3428
- message: AUTH_FAIL_MESSAGE,
3429
- workflowRunId
3430
- }),
3431
- {
3432
- status: 400,
3433
- headers: {
3434
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3435
- }
3436
- }
3437
- );
3438
- } else if (detailedFinishCondition?.condition === "non-retryable-error") {
3439
- return new Response(JSON.stringify(formatWorkflowError(detailedFinishCondition.result)), {
3440
- headers: {
3441
- "Upstash-NonRetryable-Error": "true",
3442
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3443
- },
3444
- status: 489
3445
- });
3446
- } else if (detailedFinishCondition?.condition === "retry-after-error") {
3447
- return new Response(JSON.stringify(formatWorkflowError(detailedFinishCondition.result)), {
3448
- headers: {
3449
- "Retry-After": detailedFinishCondition.result.retryAfter.toString(),
3450
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3451
- },
3452
- status: 429
3453
- });
3454
- } else if (detailedFinishCondition?.condition === "failure-callback") {
3455
- return new Response(
3456
- JSON.stringify({ result: detailedFinishCondition.result ?? void 0 }),
3457
- {
3458
- status: 200,
3459
- headers: {
3460
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3461
- }
3462
- }
3551
+ }
3552
+ function normalizeRegionHeader(region) {
3553
+ if (!region) {
3554
+ return void 0;
3555
+ }
3556
+ region = region.replaceAll("-", "_").toUpperCase();
3557
+ if (VALID_REGIONS.includes(region)) {
3558
+ return region;
3559
+ }
3560
+ console.warn(
3561
+ `[Upstash Workflow] Invalid UPSTASH-REGION header value: "${region}". Expected one of: ${VALID_REGIONS.join(
3562
+ ", "
3563
+ )}.`
3564
+ );
3565
+ return void 0;
3566
+ }
3567
+
3568
+ // src/serve/multi-region/handlers.ts
3569
+ var getHandlersForRequest = (qstashHandlers, regionHeader, isFirstInvocation) => {
3570
+ if (qstashHandlers.mode === "single-region") {
3571
+ return qstashHandlers.handlers;
3572
+ }
3573
+ let targetRegion;
3574
+ if (isFirstInvocation) {
3575
+ targetRegion = qstashHandlers.defaultRegion;
3576
+ } else {
3577
+ const normalizedRegion = regionHeader ? normalizeRegionHeader(regionHeader) : void 0;
3578
+ targetRegion = normalizedRegion ?? qstashHandlers.defaultRegion;
3579
+ }
3580
+ const handler = qstashHandlers.handlers[targetRegion];
3581
+ if (!handler) {
3582
+ console.warn(
3583
+ `[Upstash Workflow] No handler found for region "${targetRegion}". Falling back to default region.`
3584
+ );
3585
+ return qstashHandlers.handlers[qstashHandlers.defaultRegion];
3586
+ }
3587
+ return handler;
3588
+ };
3589
+ var createRegionalHandler = (environment, receiverConfig, region, clientOptions) => {
3590
+ const clientEnv = readClientEnvironmentVariables(environment, region);
3591
+ const client = new import_qstash10.Client({
3592
+ ...clientOptions,
3593
+ baseUrl: clientEnv.QSTASH_URL,
3594
+ token: clientEnv.QSTASH_TOKEN
3595
+ });
3596
+ const receiver = getReceiver(environment, receiverConfig, region);
3597
+ return { client, receiver };
3598
+ };
3599
+ var shouldUseMultiRegionMode = (environment, qstashClientOption) => {
3600
+ const hasRegionEnv = Boolean(getRegionFromEnvironment(environment));
3601
+ if (hasRegionEnv && (!qstashClientOption || !("http" in qstashClientOption))) {
3602
+ return {
3603
+ isMultiRegion: true,
3604
+ defaultRegion: getRegionFromEnvironment(environment),
3605
+ clientOptions: qstashClientOption
3606
+ };
3607
+ } else {
3608
+ return { isMultiRegion: false };
3609
+ }
3610
+ };
3611
+ var getQStashHandlers = ({
3612
+ environment,
3613
+ qstashClientOption,
3614
+ receiverConfig
3615
+ }) => {
3616
+ const multiRegion = shouldUseMultiRegionMode(environment, qstashClientOption);
3617
+ if (multiRegion.isMultiRegion) {
3618
+ const regions = ["US_EAST_1", "EU_CENTRAL_1"];
3619
+ const handlers = {};
3620
+ for (const region of regions) {
3621
+ try {
3622
+ handlers[region] = createRegionalHandler(
3623
+ environment,
3624
+ receiverConfig,
3625
+ region,
3626
+ multiRegion.clientOptions
3463
3627
  );
3628
+ } catch (error) {
3629
+ console.warn(`[Upstash Workflow] Failed to create handler for region ${region}:`, error);
3464
3630
  }
3465
- return new Response(JSON.stringify({ workflowRunId }), {
3466
- status: 200,
3467
- headers: {
3468
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3469
- }
3631
+ }
3632
+ return {
3633
+ mode: "multi-region",
3634
+ handlers,
3635
+ defaultRegion: multiRegion.defaultRegion
3636
+ };
3637
+ } else {
3638
+ return {
3639
+ mode: "single-region",
3640
+ handlers: {
3641
+ client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash10.Client({
3642
+ ...qstashClientOption,
3643
+ baseUrl: environment.QSTASH_URL,
3644
+ token: environment.QSTASH_TOKEN
3645
+ }),
3646
+ receiver: getReceiver(environment, receiverConfig)
3647
+ }
3648
+ };
3649
+ }
3650
+ };
3651
+ var getReceiver = (environment, receiverConfig, region) => {
3652
+ if (typeof receiverConfig === "string") {
3653
+ if (receiverConfig === "set-to-undefined") {
3654
+ return void 0;
3655
+ }
3656
+ const receiverEnv = readReceiverEnvironmentVariables(environment, region);
3657
+ return receiverEnv.QSTASH_CURRENT_SIGNING_KEY && receiverEnv.QSTASH_NEXT_SIGNING_KEY ? new import_qstash10.Receiver({
3658
+ currentSigningKey: receiverEnv.QSTASH_CURRENT_SIGNING_KEY,
3659
+ nextSigningKey: receiverEnv.QSTASH_NEXT_SIGNING_KEY
3660
+ }) : void 0;
3661
+ } else {
3662
+ return receiverConfig;
3663
+ }
3664
+ };
3665
+ var getQStashHandlerOptions = (...params) => {
3666
+ const handlers = getQStashHandlers(...params);
3667
+ return {
3668
+ qstashHandlers: handlers,
3669
+ defaultReceiver: handlers.mode === "single-region" ? handlers.handlers.receiver : handlers.handlers[handlers.defaultRegion].receiver,
3670
+ defaultClient: handlers.mode === "single-region" ? handlers.handlers.client : handlers.handlers[handlers.defaultRegion].client
3671
+ };
3672
+ };
3673
+
3674
+ // src/middleware/middleware.ts
3675
+ var WorkflowMiddleware = class {
3676
+ name;
3677
+ initCallbacks;
3678
+ /**
3679
+ * Callback functions
3680
+ *
3681
+ * Initially set to undefined, will be populated after init is called
3682
+ */
3683
+ middlewareCallbacks = void 0;
3684
+ constructor(parameters) {
3685
+ this.name = parameters.name;
3686
+ if ("init" in parameters) {
3687
+ this.initCallbacks = parameters.init;
3688
+ } else {
3689
+ this.middlewareCallbacks = parameters.callbacks;
3690
+ }
3691
+ }
3692
+ async ensureInit() {
3693
+ if (!this.middlewareCallbacks) {
3694
+ if (!this.initCallbacks) {
3695
+ throw new WorkflowError(`Middleware "${this.name}" has no callbacks or init defined.`);
3696
+ }
3697
+ this.middlewareCallbacks = await this.initCallbacks();
3698
+ }
3699
+ }
3700
+ /**
3701
+ * Gets a callback function by name.
3702
+ *
3703
+ * @param callback name of the callback to retrieve
3704
+ */
3705
+ getCallback(callback) {
3706
+ return this.middlewareCallbacks?.[callback];
3707
+ }
3708
+ };
3709
+
3710
+ // src/middleware/logging.ts
3711
+ var loggingMiddleware = new WorkflowMiddleware({
3712
+ name: "logging",
3713
+ callbacks: {
3714
+ afterExecution(params) {
3715
+ const { context, ...rest } = params;
3716
+ console.log(" [Upstash Workflow]: Step executed:", {
3717
+ workflowRunId: context.workflowRunId,
3718
+ ...rest
3719
+ });
3720
+ },
3721
+ beforeExecution(params) {
3722
+ const { context, ...rest } = params;
3723
+ console.log(" [Upstash Workflow]: Step execution started:", {
3724
+ workflowRunId: context.workflowRunId,
3725
+ ...rest
3470
3726
  });
3471
3727
  },
3728
+ runStarted(params) {
3729
+ const { context, ...rest } = params;
3730
+ console.log(" [Upstash Workflow]: Workflow run started:", {
3731
+ workflowRunId: context.workflowRunId,
3732
+ ...rest
3733
+ });
3734
+ },
3735
+ runCompleted(params) {
3736
+ const { context, ...rest } = params;
3737
+ console.log(" [Upstash Workflow]: Workflow run completed:", {
3738
+ workflowRunId: context.workflowRunId,
3739
+ ...rest
3740
+ });
3741
+ },
3742
+ onError: onErrorWithConsole,
3743
+ onWarning: onWarningWithConsole,
3744
+ onInfo: onInfoWithConsole
3745
+ }
3746
+ });
3747
+
3748
+ // src/serve/options.ts
3749
+ var createResponseData = (workflowRunId, detailedFinishCondition) => {
3750
+ const baseHeaders = {
3751
+ [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
3752
+ "Upstash-workflow-sdk": VERSION
3753
+ };
3754
+ if (detailedFinishCondition?.condition === "auth-fail") {
3755
+ return {
3756
+ text: JSON.stringify({
3757
+ message: AUTH_FAIL_MESSAGE,
3758
+ workflowRunId
3759
+ }),
3760
+ status: 400,
3761
+ headers: baseHeaders
3762
+ };
3763
+ } else if (detailedFinishCondition?.condition === "non-retryable-error") {
3764
+ return {
3765
+ text: JSON.stringify(formatWorkflowError(detailedFinishCondition.result)),
3766
+ status: 489,
3767
+ headers: {
3768
+ ...baseHeaders,
3769
+ "Upstash-NonRetryable-Error": "true"
3770
+ }
3771
+ };
3772
+ } else if (detailedFinishCondition?.condition === "retry-after-error") {
3773
+ return {
3774
+ text: JSON.stringify(formatWorkflowError(detailedFinishCondition.result)),
3775
+ status: 429,
3776
+ headers: {
3777
+ ...baseHeaders,
3778
+ "Retry-After": detailedFinishCondition.result.retryAfter.toString()
3779
+ }
3780
+ };
3781
+ } else if (detailedFinishCondition?.condition === "failure-callback-executed") {
3782
+ return {
3783
+ text: JSON.stringify({ result: detailedFinishCondition.result ?? void 0 }),
3784
+ status: 200,
3785
+ headers: baseHeaders
3786
+ };
3787
+ } else if (detailedFinishCondition?.condition === "failure-callback-undefined") {
3788
+ return {
3789
+ text: JSON.stringify({
3790
+ workflowRunId,
3791
+ finishCondition: detailedFinishCondition.condition
3792
+ }),
3793
+ status: 200,
3794
+ headers: {
3795
+ ...baseHeaders,
3796
+ "Upstash-Workflow-Failure-Callback-Notfound": "true"
3797
+ }
3798
+ };
3799
+ }
3800
+ return {
3801
+ text: JSON.stringify({
3802
+ workflowRunId,
3803
+ finishCondition: detailedFinishCondition.condition
3804
+ }),
3805
+ status: 200,
3806
+ headers: baseHeaders
3807
+ };
3808
+ };
3809
+ var processOptions = (options, internalOptions) => {
3810
+ const environment = options?.env ?? (typeof process === "undefined" ? {} : process.env);
3811
+ const {
3812
+ qstashHandlers,
3813
+ defaultClient: qstashClient,
3814
+ defaultReceiver: receiver
3815
+ } = getQStashHandlerOptions({
3816
+ environment,
3817
+ qstashClientOption: options?.qstashClient,
3818
+ receiverConfig: options && "receiver" in options ? options.receiver ? options.receiver : "set-to-undefined" : "not-set"
3819
+ });
3820
+ return {
3821
+ qstashClient,
3472
3822
  initialPayloadParser: (initialRequest) => {
3473
3823
  if (!initialRequest) {
3474
3824
  return void 0;
@@ -3483,35 +3833,38 @@ var processOptions = (options) => {
3483
3833
  throw error;
3484
3834
  }
3485
3835
  },
3486
- receiver: receiverEnvironmentVariablesSet ? new import_qstash10.Receiver({
3487
- currentSigningKey: environment.QSTASH_CURRENT_SIGNING_KEY,
3488
- nextSigningKey: environment.QSTASH_NEXT_SIGNING_KEY
3489
- }) : void 0,
3836
+ receiver,
3490
3837
  baseUrl: environment.UPSTASH_WORKFLOW_URL,
3491
3838
  env: environment,
3492
- retries: DEFAULT_RETRIES,
3493
- useJSONContent: false,
3494
3839
  disableTelemetry: false,
3495
- onError: console.error,
3496
- ...options
3840
+ ...options,
3841
+ // merge middlewares
3842
+ middlewares: [options?.middlewares ?? [], options?.verbose ? [loggingMiddleware] : []].flat(),
3843
+ internal: {
3844
+ generateResponse: internalOptions?.generateResponse ?? ((responseData) => {
3845
+ return new Response(responseData.text, {
3846
+ status: responseData.status,
3847
+ headers: responseData.headers
3848
+ });
3849
+ }),
3850
+ useJSONContent: internalOptions?.useJSONContent ?? false,
3851
+ qstashHandlers
3852
+ }
3497
3853
  };
3498
3854
  };
3499
- var determineUrls = async (request, url, baseUrl, failureFunction, failureUrl, debug) => {
3855
+ var determineUrls = async (request, url, baseUrl, dispatchDebug) => {
3500
3856
  const initialWorkflowUrl = url ?? request.url;
3501
3857
  const workflowUrl = baseUrl ? initialWorkflowUrl.replace(/^(https?:\/\/[^/]+)(\/.*)?$/, (_, matchedBaseUrl, path) => {
3502
3858
  return baseUrl + (path || "");
3503
3859
  }) : initialWorkflowUrl;
3504
3860
  if (workflowUrl !== initialWorkflowUrl) {
3505
- await debug?.log("WARN", "ENDPOINT_START", {
3506
- warning: `Upstash Workflow: replacing the base of the url with "${baseUrl}" and using it as workflow endpoint.`,
3507
- originalURL: initialWorkflowUrl,
3508
- updatedURL: workflowUrl
3861
+ await dispatchDebug("onInfo", {
3862
+ info: `The workflow URL's base URL has been replaced with the provided baseUrl. Original URL: ${initialWorkflowUrl}, New URL: ${workflowUrl}`
3509
3863
  });
3510
3864
  }
3511
- const workflowFailureUrl = failureFunction ? workflowUrl : failureUrl;
3512
3865
  if (workflowUrl.includes("localhost")) {
3513
- await debug?.log("WARN", "ENDPOINT_START", {
3514
- message: `Workflow URL contains localhost. This can happen in local development, but shouldn't happen in production unless you have a route which contains localhost. Received: ${workflowUrl}`
3866
+ await dispatchDebug("onInfo", {
3867
+ info: `Workflow URL contains localhost. This can happen in local development, but shouldn't happen in production unless you have a route which contains localhost. Received: ${workflowUrl}`
3515
3868
  });
3516
3869
  }
3517
3870
  if (!(workflowUrl.startsWith("http://") || workflowUrl.startsWith("https://"))) {
@@ -3520,205 +3873,224 @@ var determineUrls = async (request, url, baseUrl, failureFunction, failureUrl, d
3520
3873
  );
3521
3874
  }
3522
3875
  return {
3523
- workflowUrl,
3524
- workflowFailureUrl
3876
+ workflowUrl
3525
3877
  };
3526
3878
  };
3527
3879
  var AUTH_FAIL_MESSAGE = `Failed to authenticate Workflow request. If this is unexpected, see the caveat https://upstash.com/docs/workflow/basics/caveats#avoid-non-deterministic-code-outside-context-run`;
3528
3880
 
3529
3881
  // src/serve/index.ts
3530
- var serveBase = (routeFunction, telemetry2, options) => {
3882
+ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3531
3883
  const {
3532
- qstashClient,
3533
- onStepFinish,
3534
3884
  initialPayloadParser,
3535
3885
  url,
3536
- verbose,
3537
- receiver,
3538
- failureUrl,
3539
3886
  failureFunction,
3540
3887
  baseUrl,
3541
3888
  env,
3542
- retries,
3543
- retryDelay,
3544
- useJSONContent,
3545
3889
  disableTelemetry,
3546
- flowControl,
3547
- onError
3548
- } = processOptions(options);
3890
+ middlewares,
3891
+ internal
3892
+ } = processOptions(options, internalOptions);
3549
3893
  telemetry2 = disableTelemetry ? void 0 : telemetry2;
3550
- const debug = WorkflowLogger.getLogger(verbose);
3551
- const handler = async (request) => {
3552
- await debug?.log("INFO", "ENDPOINT_START");
3553
- const { workflowUrl, workflowFailureUrl } = await determineUrls(
3894
+ const { generateResponse: responseGenerator, useJSONContent } = internal;
3895
+ const handler = async (request, middlewareManager) => {
3896
+ await middlewareManager.dispatchDebug("onInfo", {
3897
+ info: `Received request for workflow execution.`
3898
+ });
3899
+ const { workflowUrl } = await determineUrls(
3554
3900
  request,
3555
3901
  url,
3556
3902
  baseUrl,
3557
- failureFunction,
3558
- failureUrl,
3559
- debug
3903
+ middlewareManager.dispatchDebug.bind(middlewareManager)
3904
+ );
3905
+ const { isFirstInvocation, workflowRunId, unknownSdk } = validateRequest(request);
3906
+ const regionHeader = request.headers.get("upstash-region");
3907
+ const { client: regionalClient, receiver: regionalReceiver } = getHandlersForRequest(
3908
+ internal.qstashHandlers,
3909
+ regionHeader,
3910
+ isFirstInvocation
3560
3911
  );
3561
3912
  const requestPayload = await getPayload(request) ?? "";
3562
- await verifyRequest(requestPayload, request.headers.get("upstash-signature"), receiver);
3563
- const { isFirstInvocation, workflowRunId } = validateRequest(request);
3564
- debug?.setWorkflowRunId(workflowRunId);
3565
- const { rawInitialPayload, steps, isLastDuplicate, workflowRunEnded } = await parseRequest(
3913
+ await verifyRequest(requestPayload, request.headers.get("upstash-signature"), regionalReceiver);
3914
+ middlewareManager.assignWorkflowRunId(workflowRunId);
3915
+ await middlewareManager.dispatchDebug("onInfo", {
3916
+ info: `Run id identified. isFirstInvocation: ${isFirstInvocation}, unknownSdk: ${unknownSdk}`
3917
+ });
3918
+ const { rawInitialPayload, steps, isLastDuplicate, workflowRunEnded } = await parseRequest({
3566
3919
  requestPayload,
3567
3920
  isFirstInvocation,
3921
+ unknownSdk,
3568
3922
  workflowRunId,
3569
- qstashClient.http,
3570
- request.headers.get("upstash-message-id"),
3571
- debug
3572
- );
3923
+ requester: regionalClient.http,
3924
+ messageId: request.headers.get("upstash-message-id"),
3925
+ dispatchDebug: middlewareManager.dispatchDebug.bind(middlewareManager)
3926
+ });
3573
3927
  if (workflowRunEnded) {
3574
- return onStepFinish(workflowRunId, "workflow-already-ended", {
3575
- condition: "workflow-already-ended"
3576
- });
3928
+ return responseGenerator(
3929
+ createResponseData(workflowRunId, {
3930
+ condition: "workflow-already-ended"
3931
+ })
3932
+ );
3577
3933
  }
3578
3934
  if (isLastDuplicate) {
3579
- return onStepFinish(workflowRunId, "duplicate-step", {
3580
- condition: "duplicate-step"
3581
- });
3935
+ return responseGenerator(
3936
+ createResponseData(workflowRunId, {
3937
+ condition: "duplicate-step"
3938
+ })
3939
+ );
3582
3940
  }
3583
- const failureCheck = await handleFailure(
3941
+ const failureCheck = await handleFailure({
3584
3942
  request,
3585
3943
  requestPayload,
3586
- qstashClient,
3944
+ qstashClient: regionalClient,
3587
3945
  initialPayloadParser,
3588
3946
  routeFunction,
3589
3947
  failureFunction,
3590
3948
  env,
3591
- retries,
3592
- retryDelay,
3593
- flowControl,
3594
- debug
3595
- );
3949
+ dispatchDebug: middlewareManager.dispatchDebug.bind(middlewareManager)
3950
+ });
3596
3951
  if (failureCheck.isErr()) {
3597
3952
  throw failureCheck.error;
3598
- } else if (failureCheck.value.result === "is-failure-callback") {
3599
- await debug?.log("WARN", "RESPONSE_DEFAULT", "failureFunction executed");
3600
- return onStepFinish(workflowRunId, "failure-callback", {
3601
- condition: "failure-callback",
3602
- result: failureCheck.value.response
3953
+ } else if (failureCheck.value.result === "failure-function-executed") {
3954
+ await middlewareManager.dispatchDebug("onInfo", {
3955
+ info: `Handled failure callback.`
3603
3956
  });
3957
+ return responseGenerator(
3958
+ createResponseData(workflowRunId, {
3959
+ condition: "failure-callback-executed",
3960
+ result: failureCheck.value.response
3961
+ })
3962
+ );
3963
+ } else if (failureCheck.value.result === "failure-function-undefined") {
3964
+ await middlewareManager.dispatchDebug("onInfo", {
3965
+ info: `Failure callback invoked but no failure function defined.`
3966
+ });
3967
+ return responseGenerator(
3968
+ createResponseData(workflowRunId, {
3969
+ condition: "failure-callback-undefined"
3970
+ })
3971
+ );
3604
3972
  }
3605
3973
  const invokeCount = Number(request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER) ?? "0");
3606
3974
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3607
3975
  const workflowContext = new WorkflowContext({
3608
- qstashClient,
3976
+ qstashClient: regionalClient,
3609
3977
  workflowRunId,
3610
3978
  initialPayload: initialPayloadParser(rawInitialPayload),
3611
3979
  headers: recreateUserHeaders(request.headers),
3612
3980
  steps,
3613
3981
  url: workflowUrl,
3614
- failureUrl: workflowFailureUrl,
3615
- debug,
3616
3982
  env,
3617
- retries,
3618
- retryDelay,
3619
3983
  telemetry: telemetry2,
3620
3984
  invokeCount,
3621
- flowControl,
3622
- label
3985
+ label,
3986
+ middlewareManager
3623
3987
  });
3624
3988
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
3625
3989
  routeFunction,
3626
3990
  workflowContext
3627
3991
  );
3628
3992
  if (authCheck.isErr()) {
3629
- await debug?.log("ERROR", "ERROR", { error: authCheck.error.message });
3630
3993
  throw authCheck.error;
3631
3994
  } else if (authCheck.value === "run-ended") {
3632
- await debug?.log("ERROR", "ERROR", { error: AUTH_FAIL_MESSAGE });
3633
- return onStepFinish(
3634
- isFirstInvocation ? "no-workflow-id" : workflowContext.workflowRunId,
3635
- "auth-fail",
3636
- { condition: "auth-fail" }
3995
+ await middlewareManager.dispatchDebug("onError", {
3996
+ error: new Error(AUTH_FAIL_MESSAGE)
3997
+ });
3998
+ return responseGenerator(
3999
+ createResponseData(isFirstInvocation ? "no-workflow-id" : workflowContext.workflowRunId, {
4000
+ condition: "auth-fail"
4001
+ })
3637
4002
  );
3638
4003
  }
3639
4004
  const callReturnCheck = await handleThirdPartyCallResult({
3640
4005
  request,
3641
4006
  requestPayload: rawInitialPayload,
3642
- client: qstashClient,
4007
+ client: regionalClient,
3643
4008
  workflowUrl,
3644
- failureUrl: workflowFailureUrl,
3645
- retries,
3646
- retryDelay,
3647
- flowControl,
3648
4009
  telemetry: telemetry2,
3649
- debug
4010
+ middlewareManager
3650
4011
  });
3651
4012
  if (callReturnCheck.isErr()) {
3652
- await debug?.log("ERROR", "SUBMIT_THIRD_PARTY_RESULT", {
3653
- error: callReturnCheck.error.message
3654
- });
3655
4013
  throw callReturnCheck.error;
3656
4014
  } else if (callReturnCheck.value === "continue-workflow") {
3657
4015
  const result = isFirstInvocation ? await triggerFirstInvocation({
3658
4016
  workflowContext,
3659
4017
  useJSONContent,
3660
4018
  telemetry: telemetry2,
3661
- debug,
3662
- invokeCount
4019
+ invokeCount,
4020
+ middlewareManager,
4021
+ unknownSdk
3663
4022
  }) : await triggerRouteFunction({
3664
- onStep: async () => routeFunction(workflowContext),
4023
+ onStep: async () => {
4024
+ if (steps.length === 1) {
4025
+ await middlewareManager.dispatchLifecycle("runStarted", {});
4026
+ }
4027
+ return await routeFunction(workflowContext);
4028
+ },
3665
4029
  onCleanup: async (result2) => {
3666
- await triggerWorkflowDelete(workflowContext, result2, debug);
4030
+ await middlewareManager.dispatchLifecycle("runCompleted", {
4031
+ result: result2
4032
+ });
4033
+ await triggerWorkflowDelete(
4034
+ workflowContext,
4035
+ result2,
4036
+ false,
4037
+ middlewareManager.dispatchDebug.bind(middlewareManager)
4038
+ );
3667
4039
  },
3668
4040
  onCancel: async () => {
3669
4041
  await makeCancelRequest(workflowContext.qstashClient.http, workflowRunId);
3670
4042
  },
3671
- debug
4043
+ middlewareManager
3672
4044
  });
3673
4045
  if (result.isOk() && isInstanceOf(result.value, WorkflowNonRetryableError)) {
3674
- return onStepFinish(workflowRunId, result.value, {
3675
- condition: "non-retryable-error",
3676
- result: result.value
3677
- });
4046
+ return responseGenerator(
4047
+ createResponseData(workflowRunId, {
4048
+ condition: "non-retryable-error",
4049
+ result: result.value
4050
+ })
4051
+ );
3678
4052
  }
3679
4053
  if (result.isOk() && isInstanceOf(result.value, WorkflowRetryAfterError)) {
3680
- return onStepFinish(workflowRunId, result.value, {
3681
- condition: "retry-after-error",
3682
- result: result.value
3683
- });
4054
+ return responseGenerator(
4055
+ createResponseData(workflowRunId, {
4056
+ condition: "retry-after-error",
4057
+ result: result.value
4058
+ })
4059
+ );
3684
4060
  }
3685
4061
  if (result.isErr()) {
3686
- await debug?.log("ERROR", "ERROR", { error: result.error.message });
3687
4062
  throw result.error;
3688
4063
  }
3689
- await debug?.log("INFO", "RESPONSE_WORKFLOW");
3690
- return onStepFinish(workflowContext.workflowRunId, "success", {
3691
- condition: "success"
4064
+ await middlewareManager.dispatchDebug("onInfo", {
4065
+ info: `Workflow endpoint execution completed successfully.`
3692
4066
  });
4067
+ return responseGenerator(
4068
+ createResponseData(workflowContext.workflowRunId, {
4069
+ condition: "success"
4070
+ })
4071
+ );
3693
4072
  } else if (callReturnCheck.value === "workflow-ended") {
3694
- return onStepFinish(workflowContext.workflowRunId, "workflow-already-ended", {
3695
- condition: "workflow-already-ended"
3696
- });
4073
+ return responseGenerator(
4074
+ createResponseData(workflowContext.workflowRunId, {
4075
+ condition: "workflow-already-ended"
4076
+ })
4077
+ );
3697
4078
  }
3698
- await debug?.log("INFO", "RESPONSE_DEFAULT");
3699
- return onStepFinish("no-workflow-id", "fromCallback", {
3700
- condition: "fromCallback"
3701
- });
4079
+ return responseGenerator(
4080
+ createResponseData(workflowContext.workflowRunId, {
4081
+ condition: "fromCallback"
4082
+ })
4083
+ );
3702
4084
  };
3703
4085
  const safeHandler = async (request) => {
4086
+ const middlewareManager = new MiddlewareManager(middlewares);
3704
4087
  try {
3705
- return await handler(request);
4088
+ return await handler(request, middlewareManager);
3706
4089
  } catch (error) {
3707
4090
  const formattedError = formatWorkflowError(error);
3708
- try {
3709
- onError?.(error);
3710
- } catch (onErrorError) {
3711
- const formattedOnErrorError = formatWorkflowError(onErrorError);
3712
- const errorMessage = `Error while running onError callback: '${formattedOnErrorError.message}'.
3713
- Original error: '${formattedError.message}'`;
3714
- console.error(errorMessage);
3715
- return new Response(JSON.stringify({ error: errorMessage }), {
3716
- status: 500,
3717
- headers: {
3718
- [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION
3719
- }
3720
- });
3721
- }
4091
+ await middlewareManager.dispatchDebug("onError", {
4092
+ error: isInstanceOf(error, Error) ? error : new Error(formattedError.message)
4093
+ });
3722
4094
  return new Response(JSON.stringify(formattedError), {
3723
4095
  status: 500,
3724
4096
  headers: {
@@ -3765,7 +4137,11 @@ var serve = (routeFunction, options) => {
3765
4137
  body: await readRawBody(event),
3766
4138
  method: "POST"
3767
4139
  });
3768
- const { handler: serveHandler } = serveBase(routeFunction, telemetry, options);
4140
+ const { handler: serveHandler } = serveBase(
4141
+ routeFunction,
4142
+ telemetry,
4143
+ options
4144
+ );
3769
4145
  return await serveHandler(request);
3770
4146
  });
3771
4147
  return { handler };