@upstash/workflow 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/express.js CHANGED
@@ -51,6 +51,9 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
51
51
  var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
52
52
  var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
53
53
 
54
+ // src/serve/index.ts
55
+ var import_qstash12 = require("@upstash/qstash");
56
+
54
57
  // src/client/utils.ts
55
58
  var import_qstash2 = require("@upstash/qstash");
56
59
 
@@ -781,6 +784,10 @@ var triggerFirstInvocation = async (params) => {
781
784
  workflowConfig: {
782
785
  workflowRunId: workflowContext.workflowRunId,
783
786
  workflowUrl: workflowContext.url,
787
+ // the run doesn't exist yet, so the trigger time is sent when the creation time
788
+ // is not known. serve and client.trigger set it on the context already, so a
789
+ // retry of this request carries the same value and is deduplicated.
790
+ workflowRunCreatedAt: workflowContext.workflowRunCreatedAt || Date.now(),
784
791
  failureUrl,
785
792
  retries,
786
793
  retryDelay,
@@ -884,7 +891,10 @@ var triggerWorkflowDelete = async (workflowContext, result, cancel = false, disp
884
891
  path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
885
892
  method: "DELETE",
886
893
  parseResponseAsJson: false,
887
- body: JSON.stringify(result)
894
+ body: JSON.stringify(result),
895
+ // with the creation time, QStash deletes the run only if it is the same run.
896
+ // a late delete of a previous run can't delete a new run with the same id.
897
+ headers: workflowContext.workflowRunCreatedAt ? { [WORKFLOW_CREATED_AT_HEADER]: workflowContext.workflowRunCreatedAt.toString() } : void 0
888
898
  });
889
899
  await dispatchDebug?.("onInfo", {
890
900
  info: `Workflow run ${workflowContext.workflowRunId} deleted from QStash successfully.`
@@ -961,6 +971,7 @@ ${atob(callbackMessage.body ?? "")}`
961
971
  const concurrentString = request.headers.get("Upstash-Workflow-Concurrent");
962
972
  const contentType = request.headers.get("Upstash-Workflow-ContentType");
963
973
  const invokeCount = request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER);
974
+ const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
964
975
  if (!(workflowRunId && stepIdString && stepName && StepTypes.includes(stepType) && concurrentString && contentType)) {
965
976
  throw new Error(
966
977
  `Missing info in callback message source header: ${JSON.stringify({
@@ -978,6 +989,7 @@ ${atob(callbackMessage.body ?? "")}`
978
989
  workflowConfig: {
979
990
  workflowRunId,
980
991
  workflowUrl,
992
+ workflowRunCreatedAt: Number(workflowRunCreatedAt ?? "0"),
981
993
  telemetry: telemetry2
982
994
  },
983
995
  invokeCount: Number(invokeCount)
@@ -1125,6 +1137,7 @@ var BaseLazyStep = class _BaseLazyStep {
1125
1137
  workflowConfig: {
1126
1138
  workflowRunId: context.workflowRunId,
1127
1139
  workflowUrl: context.url,
1140
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1128
1141
  useJSONContent: false,
1129
1142
  telemetry: telemetry2
1130
1143
  },
@@ -1452,6 +1465,8 @@ var LazyWaitEventStep = class extends BaseLazyStep {
1452
1465
  ) : {},
1453
1466
  // note: using WORKFLOW_ID_HEADER doesn't work, because Runid -> RunId:
1454
1467
  "Upstash-Workflow-Runid": [context.workflowRunId],
1468
+ // same for WORKFLOW_CREATED_AT_HEADER, because Createdat -> CreatedAt:
1469
+ ...context.workflowRunCreatedAt ? { "Upstash-Workflow-Createdat": [context.workflowRunCreatedAt.toString()] } : {},
1455
1470
  [WORKFLOW_INIT_HEADER]: ["false"],
1456
1471
  [WORKFLOW_URL_HEADER]: [context.url],
1457
1472
  "Upstash-Workflow-CallType": ["step"]
@@ -1554,6 +1569,7 @@ var LazyInvokeStep = class extends BaseLazyStep {
1554
1569
  workflowConfig: {
1555
1570
  workflowRunId: context.workflowRunId,
1556
1571
  workflowUrl: context.url,
1572
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1557
1573
  telemetry: telemetry2,
1558
1574
  useJSONContent: false
1559
1575
  },
@@ -1563,6 +1579,9 @@ var LazyInvokeStep = class extends BaseLazyStep {
1563
1579
  invokerHeaders[key] = value;
1564
1580
  });
1565
1581
  invokerHeaders["Upstash-Workflow-Runid"] = context.workflowRunId;
1582
+ if (context.workflowRunCreatedAt) {
1583
+ invokerHeaders["Upstash-Workflow-Createdat"] = context.workflowRunCreatedAt.toString();
1584
+ }
1566
1585
  const request = {
1567
1586
  body: stringifyBody(this.params.body),
1568
1587
  headers: Object.fromEntries(
@@ -1591,6 +1610,9 @@ var LazyInvokeStep = class extends BaseLazyStep {
1591
1610
  workflowConfig: {
1592
1611
  workflowRunId: getWorkflowRunId(workflowRunId),
1593
1612
  workflowUrl: newUrl,
1613
+ // the invoked run doesn't exist yet, so the creation time of the invoker run is sent.
1614
+ // it separates the invocations made by different runs of the invoker.
1615
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1594
1616
  retries,
1595
1617
  retryDelay,
1596
1618
  telemetry: telemetry2,
@@ -1755,6 +1777,9 @@ var WorkflowHeaders = class {
1755
1777
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1756
1778
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1757
1779
  };
1780
+ if (this.workflowConfig.workflowRunCreatedAt) {
1781
+ this.headers.rawHeaders[WORKFLOW_CREATED_AT_HEADER] = this.workflowConfig.workflowRunCreatedAt.toString();
1782
+ }
1758
1783
  if (this.stepInfo?.lazyStep.stepType !== "Call") {
1759
1784
  this.headers.rawHeaders[`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`] = WORKFLOW_PROTOCOL_VERSION;
1760
1785
  }
@@ -1914,6 +1939,7 @@ var submitParallelSteps = async ({
1914
1939
  workflowConfig: {
1915
1940
  workflowRunId: context.workflowRunId,
1916
1941
  workflowUrl: context.url,
1942
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1917
1943
  telemetry: telemetry2
1918
1944
  },
1919
1945
  invokeCount
@@ -3415,7 +3441,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3415
3441
  const client = new import_qstash11.Client({
3416
3442
  ...clientOptions,
3417
3443
  baseUrl: clientEnv.QSTASH_URL,
3418
- token: clientEnv.QSTASH_TOKEN
3444
+ token: clientEnv.QSTASH_TOKEN,
3445
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3446
+ // the client at the local dev server too. Without this the QStash Client
3447
+ // re-derives dev mode from its own process.env, which is blind to the
3448
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3449
+ // An explicit devMode in the user's client config still wins.
3450
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3419
3451
  });
3420
3452
  const receiver = getReceiver(environment, receiverConfig, region);
3421
3453
  return { client, receiver };
@@ -3465,7 +3497,9 @@ var getQStashHandlers = ({
3465
3497
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3466
3498
  ...qstashClientOption,
3467
3499
  baseUrl: environment.QSTASH_URL,
3468
- token: environment.QSTASH_TOKEN
3500
+ token: environment.QSTASH_TOKEN,
3501
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
3502
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3469
3503
  }),
3470
3504
  receiver: getReceiver(environment, receiverConfig)
3471
3505
  }
@@ -3804,7 +3838,8 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3804
3838
  const invokeCount = Number(request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER) ?? "0");
3805
3839
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3806
3840
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3807
- const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
3841
+ const workflowRunCreatedAtHeader = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
3842
+ const workflowRunCreatedAt = workflowRunCreatedAtHeader ? Number(workflowRunCreatedAtHeader) : isFirstInvocation ? Date.now() : 0;
3808
3843
  const workflowContext = new WorkflowContext({
3809
3844
  qstashClient: regionalClient,
3810
3845
  workflowRunId,
@@ -3817,7 +3852,7 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3817
3852
  invokeCount,
3818
3853
  label,
3819
3854
  retried,
3820
- workflowRunCreatedAt: Number(workflowRunCreatedAt),
3855
+ workflowRunCreatedAt,
3821
3856
  middlewareManager
3822
3857
  });
3823
3858
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
@@ -3941,6 +3976,7 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3941
3976
 
3942
3977
  // platforms/express.ts
3943
3978
  var import_express = require("express");
3979
+ var import_qstash13 = require("@upstash/qstash");
3944
3980
  var isEmptyRequest = (req) => {
3945
3981
  return req.headers["content-type"] === "application/json" && req.headers["content-length"] === "0";
3946
3982
  };
@@ -3991,6 +4027,7 @@ function createExpressHandler(params) {
3991
4027
  };
3992
4028
  }
3993
4029
  function serve(routeFunction, options) {
4030
+ void (0, import_qstash13.startDevServer)();
3994
4031
  const router = (0, import_express.Router)();
3995
4032
  const handler = createExpressHandler([routeFunction, options]);
3996
4033
  router.all(/(.*)/, handler);
@@ -4006,6 +4043,7 @@ var createWorkflow = (...params) => {
4006
4043
  };
4007
4044
  };
4008
4045
  var serveMany = (workflows, options) => {
4046
+ void (0, import_qstash13.startDevServer)();
4009
4047
  const router = (0, import_express.Router)();
4010
4048
  const { handler } = serveManyBase({
4011
4049
  workflows,
package/express.mjs CHANGED
@@ -2,12 +2,13 @@ import {
2
2
  SDK_TELEMETRY,
3
3
  serveBase,
4
4
  serveManyBase
5
- } from "./chunk-WEQGCISK.mjs";
5
+ } from "./chunk-DRHEHWMS.mjs";
6
6
 
7
7
  // platforms/express.ts
8
8
  import {
9
9
  Router
10
10
  } from "express";
11
+ import { startDevServer } from "@upstash/qstash";
11
12
  var isEmptyRequest = (req) => {
12
13
  return req.headers["content-type"] === "application/json" && req.headers["content-length"] === "0";
13
14
  };
@@ -58,6 +59,7 @@ function createExpressHandler(params) {
58
59
  };
59
60
  }
60
61
  function serve(routeFunction, options) {
62
+ void startDevServer();
61
63
  const router = Router();
62
64
  const handler = createExpressHandler([routeFunction, options]);
63
65
  router.all(/(.*)/, handler);
@@ -73,6 +75,7 @@ var createWorkflow = (...params) => {
73
75
  };
74
76
  };
75
77
  var serveMany = (workflows, options) => {
78
+ void startDevServer();
76
79
  const router = Router();
77
80
  const { handler } = serveManyBase({
78
81
  workflows,
package/h3.js CHANGED
@@ -335,6 +335,9 @@ async function _callHandler(event, handler, hooks) {
335
335
  var H3Headers = globalThis.Headers;
336
336
  var H3Response = globalThis.Response;
337
337
 
338
+ // src/serve/index.ts
339
+ var import_qstash12 = require("@upstash/qstash");
340
+
338
341
  // src/client/utils.ts
339
342
  var import_qstash2 = require("@upstash/qstash");
340
343
 
@@ -1090,6 +1093,10 @@ var triggerFirstInvocation = async (params) => {
1090
1093
  workflowConfig: {
1091
1094
  workflowRunId: workflowContext.workflowRunId,
1092
1095
  workflowUrl: workflowContext.url,
1096
+ // the run doesn't exist yet, so the trigger time is sent when the creation time
1097
+ // is not known. serve and client.trigger set it on the context already, so a
1098
+ // retry of this request carries the same value and is deduplicated.
1099
+ workflowRunCreatedAt: workflowContext.workflowRunCreatedAt || Date.now(),
1093
1100
  failureUrl,
1094
1101
  retries,
1095
1102
  retryDelay,
@@ -1193,7 +1200,10 @@ var triggerWorkflowDelete = async (workflowContext, result, cancel = false, disp
1193
1200
  path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
1194
1201
  method: "DELETE",
1195
1202
  parseResponseAsJson: false,
1196
- body: JSON.stringify(result)
1203
+ body: JSON.stringify(result),
1204
+ // with the creation time, QStash deletes the run only if it is the same run.
1205
+ // a late delete of a previous run can't delete a new run with the same id.
1206
+ headers: workflowContext.workflowRunCreatedAt ? { [WORKFLOW_CREATED_AT_HEADER]: workflowContext.workflowRunCreatedAt.toString() } : void 0
1197
1207
  });
1198
1208
  await dispatchDebug?.("onInfo", {
1199
1209
  info: `Workflow run ${workflowContext.workflowRunId} deleted from QStash successfully.`
@@ -1270,6 +1280,7 @@ ${atob(callbackMessage.body ?? "")}`
1270
1280
  const concurrentString = request.headers.get("Upstash-Workflow-Concurrent");
1271
1281
  const contentType = request.headers.get("Upstash-Workflow-ContentType");
1272
1282
  const invokeCount = request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER);
1283
+ const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
1273
1284
  if (!(workflowRunId && stepIdString && stepName && StepTypes.includes(stepType) && concurrentString && contentType)) {
1274
1285
  throw new Error(
1275
1286
  `Missing info in callback message source header: ${JSON.stringify({
@@ -1287,6 +1298,7 @@ ${atob(callbackMessage.body ?? "")}`
1287
1298
  workflowConfig: {
1288
1299
  workflowRunId,
1289
1300
  workflowUrl,
1301
+ workflowRunCreatedAt: Number(workflowRunCreatedAt ?? "0"),
1290
1302
  telemetry: telemetry2
1291
1303
  },
1292
1304
  invokeCount: Number(invokeCount)
@@ -1434,6 +1446,7 @@ var BaseLazyStep = class _BaseLazyStep {
1434
1446
  workflowConfig: {
1435
1447
  workflowRunId: context.workflowRunId,
1436
1448
  workflowUrl: context.url,
1449
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1437
1450
  useJSONContent: false,
1438
1451
  telemetry: telemetry2
1439
1452
  },
@@ -1761,6 +1774,8 @@ var LazyWaitEventStep = class extends BaseLazyStep {
1761
1774
  ) : {},
1762
1775
  // note: using WORKFLOW_ID_HEADER doesn't work, because Runid -> RunId:
1763
1776
  "Upstash-Workflow-Runid": [context.workflowRunId],
1777
+ // same for WORKFLOW_CREATED_AT_HEADER, because Createdat -> CreatedAt:
1778
+ ...context.workflowRunCreatedAt ? { "Upstash-Workflow-Createdat": [context.workflowRunCreatedAt.toString()] } : {},
1764
1779
  [WORKFLOW_INIT_HEADER]: ["false"],
1765
1780
  [WORKFLOW_URL_HEADER]: [context.url],
1766
1781
  "Upstash-Workflow-CallType": ["step"]
@@ -1863,6 +1878,7 @@ var LazyInvokeStep = class extends BaseLazyStep {
1863
1878
  workflowConfig: {
1864
1879
  workflowRunId: context.workflowRunId,
1865
1880
  workflowUrl: context.url,
1881
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1866
1882
  telemetry: telemetry2,
1867
1883
  useJSONContent: false
1868
1884
  },
@@ -1872,6 +1888,9 @@ var LazyInvokeStep = class extends BaseLazyStep {
1872
1888
  invokerHeaders[key] = value;
1873
1889
  });
1874
1890
  invokerHeaders["Upstash-Workflow-Runid"] = context.workflowRunId;
1891
+ if (context.workflowRunCreatedAt) {
1892
+ invokerHeaders["Upstash-Workflow-Createdat"] = context.workflowRunCreatedAt.toString();
1893
+ }
1875
1894
  const request = {
1876
1895
  body: stringifyBody(this.params.body),
1877
1896
  headers: Object.fromEntries(
@@ -1900,6 +1919,9 @@ var LazyInvokeStep = class extends BaseLazyStep {
1900
1919
  workflowConfig: {
1901
1920
  workflowRunId: getWorkflowRunId(workflowRunId),
1902
1921
  workflowUrl: newUrl,
1922
+ // the invoked run doesn't exist yet, so the creation time of the invoker run is sent.
1923
+ // it separates the invocations made by different runs of the invoker.
1924
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1903
1925
  retries,
1904
1926
  retryDelay,
1905
1927
  telemetry: telemetry2,
@@ -2064,6 +2086,9 @@ var WorkflowHeaders = class {
2064
2086
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
2065
2087
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
2066
2088
  };
2089
+ if (this.workflowConfig.workflowRunCreatedAt) {
2090
+ this.headers.rawHeaders[WORKFLOW_CREATED_AT_HEADER] = this.workflowConfig.workflowRunCreatedAt.toString();
2091
+ }
2067
2092
  if (this.stepInfo?.lazyStep.stepType !== "Call") {
2068
2093
  this.headers.rawHeaders[`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`] = WORKFLOW_PROTOCOL_VERSION;
2069
2094
  }
@@ -2223,6 +2248,7 @@ var submitParallelSteps = async ({
2223
2248
  workflowConfig: {
2224
2249
  workflowRunId: context.workflowRunId,
2225
2250
  workflowUrl: context.url,
2251
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
2226
2252
  telemetry: telemetry2
2227
2253
  },
2228
2254
  invokeCount
@@ -3724,7 +3750,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3724
3750
  const client = new import_qstash11.Client({
3725
3751
  ...clientOptions,
3726
3752
  baseUrl: clientEnv.QSTASH_URL,
3727
- token: clientEnv.QSTASH_TOKEN
3753
+ token: clientEnv.QSTASH_TOKEN,
3754
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3755
+ // the client at the local dev server too. Without this the QStash Client
3756
+ // re-derives dev mode from its own process.env, which is blind to the
3757
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3758
+ // An explicit devMode in the user's client config still wins.
3759
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3728
3760
  });
3729
3761
  const receiver = getReceiver(environment, receiverConfig, region);
3730
3762
  return { client, receiver };
@@ -3774,7 +3806,9 @@ var getQStashHandlers = ({
3774
3806
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3775
3807
  ...qstashClientOption,
3776
3808
  baseUrl: environment.QSTASH_URL,
3777
- token: environment.QSTASH_TOKEN
3809
+ token: environment.QSTASH_TOKEN,
3810
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
3811
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3778
3812
  }),
3779
3813
  receiver: getReceiver(environment, receiverConfig)
3780
3814
  }
@@ -4113,7 +4147,8 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4113
4147
  const invokeCount = Number(request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER) ?? "0");
4114
4148
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
4115
4149
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
4116
- const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4150
+ const workflowRunCreatedAtHeader = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
4151
+ const workflowRunCreatedAt = workflowRunCreatedAtHeader ? Number(workflowRunCreatedAtHeader) : isFirstInvocation ? Date.now() : 0;
4117
4152
  const workflowContext = new WorkflowContext({
4118
4153
  qstashClient: regionalClient,
4119
4154
  workflowRunId,
@@ -4126,7 +4161,7 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4126
4161
  invokeCount,
4127
4162
  label,
4128
4163
  retried,
4129
- workflowRunCreatedAt: Number(workflowRunCreatedAt),
4164
+ workflowRunCreatedAt,
4130
4165
  middlewareManager
4131
4166
  });
4132
4167
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
@@ -4249,6 +4284,7 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
4249
4284
  };
4250
4285
 
4251
4286
  // platforms/h3.ts
4287
+ var import_qstash13 = require("@upstash/qstash");
4252
4288
  function transformHeaders(headers) {
4253
4289
  const formattedHeaders = Object.entries(headers).map(([key, value]) => [
4254
4290
  key,
@@ -4269,6 +4305,7 @@ var telemetry = {
4269
4305
  runtime: process.versions.bun ? `bun@${process.versions.bun}/node@${process.version}` : `node@${process.version}`
4270
4306
  };
4271
4307
  var serve = (routeFunction, options) => {
4308
+ void (0, import_qstash13.startDevServer)();
4272
4309
  const handler = defineEventHandler(async (event) => {
4273
4310
  const method = event.node.req.method;
4274
4311
  if (method?.toUpperCase() !== "POST") {
package/h3.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  SDK_TELEMETRY,
3
3
  serveBase,
4
4
  serveManyBase
5
- } from "./chunk-WEQGCISK.mjs";
5
+ } from "./chunk-DRHEHWMS.mjs";
6
6
 
7
7
  // node_modules/defu/dist/defu.mjs
8
8
  function isPlainObject(value) {
@@ -314,6 +314,7 @@ var H3Headers = globalThis.Headers;
314
314
  var H3Response = globalThis.Response;
315
315
 
316
316
  // platforms/h3.ts
317
+ import { startDevServer } from "@upstash/qstash";
317
318
  function transformHeaders(headers) {
318
319
  const formattedHeaders = Object.entries(headers).map(([key, value]) => [
319
320
  key,
@@ -334,6 +335,7 @@ var telemetry = {
334
335
  runtime: process.versions.bun ? `bun@${process.versions.bun}/node@${process.version}` : `node@${process.version}`
335
336
  };
336
337
  var serve = (routeFunction, options) => {
338
+ void startDevServer();
337
339
  const handler = defineEventHandler(async (event) => {
338
340
  const method = event.node.req.method;
339
341
  if (method?.toUpperCase() !== "POST") {
package/hono.js CHANGED
@@ -26,6 +26,9 @@ __export(hono_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(hono_exports);
28
28
 
29
+ // src/serve/index.ts
30
+ var import_qstash12 = require("@upstash/qstash");
31
+
29
32
  // src/client/utils.ts
30
33
  var import_qstash2 = require("@upstash/qstash");
31
34
 
@@ -781,6 +784,10 @@ var triggerFirstInvocation = async (params) => {
781
784
  workflowConfig: {
782
785
  workflowRunId: workflowContext.workflowRunId,
783
786
  workflowUrl: workflowContext.url,
787
+ // the run doesn't exist yet, so the trigger time is sent when the creation time
788
+ // is not known. serve and client.trigger set it on the context already, so a
789
+ // retry of this request carries the same value and is deduplicated.
790
+ workflowRunCreatedAt: workflowContext.workflowRunCreatedAt || Date.now(),
784
791
  failureUrl,
785
792
  retries,
786
793
  retryDelay,
@@ -884,7 +891,10 @@ var triggerWorkflowDelete = async (workflowContext, result, cancel = false, disp
884
891
  path: ["v2", "workflows", "runs", `${workflowContext.workflowRunId}?cancel=${cancel}`],
885
892
  method: "DELETE",
886
893
  parseResponseAsJson: false,
887
- body: JSON.stringify(result)
894
+ body: JSON.stringify(result),
895
+ // with the creation time, QStash deletes the run only if it is the same run.
896
+ // a late delete of a previous run can't delete a new run with the same id.
897
+ headers: workflowContext.workflowRunCreatedAt ? { [WORKFLOW_CREATED_AT_HEADER]: workflowContext.workflowRunCreatedAt.toString() } : void 0
888
898
  });
889
899
  await dispatchDebug?.("onInfo", {
890
900
  info: `Workflow run ${workflowContext.workflowRunId} deleted from QStash successfully.`
@@ -961,6 +971,7 @@ ${atob(callbackMessage.body ?? "")}`
961
971
  const concurrentString = request.headers.get("Upstash-Workflow-Concurrent");
962
972
  const contentType = request.headers.get("Upstash-Workflow-ContentType");
963
973
  const invokeCount = request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER);
974
+ const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
964
975
  if (!(workflowRunId && stepIdString && stepName && StepTypes.includes(stepType) && concurrentString && contentType)) {
965
976
  throw new Error(
966
977
  `Missing info in callback message source header: ${JSON.stringify({
@@ -978,6 +989,7 @@ ${atob(callbackMessage.body ?? "")}`
978
989
  workflowConfig: {
979
990
  workflowRunId,
980
991
  workflowUrl,
992
+ workflowRunCreatedAt: Number(workflowRunCreatedAt ?? "0"),
981
993
  telemetry: telemetry2
982
994
  },
983
995
  invokeCount: Number(invokeCount)
@@ -1125,6 +1137,7 @@ var BaseLazyStep = class _BaseLazyStep {
1125
1137
  workflowConfig: {
1126
1138
  workflowRunId: context.workflowRunId,
1127
1139
  workflowUrl: context.url,
1140
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1128
1141
  useJSONContent: false,
1129
1142
  telemetry: telemetry2
1130
1143
  },
@@ -1452,6 +1465,8 @@ var LazyWaitEventStep = class extends BaseLazyStep {
1452
1465
  ) : {},
1453
1466
  // note: using WORKFLOW_ID_HEADER doesn't work, because Runid -> RunId:
1454
1467
  "Upstash-Workflow-Runid": [context.workflowRunId],
1468
+ // same for WORKFLOW_CREATED_AT_HEADER, because Createdat -> CreatedAt:
1469
+ ...context.workflowRunCreatedAt ? { "Upstash-Workflow-Createdat": [context.workflowRunCreatedAt.toString()] } : {},
1455
1470
  [WORKFLOW_INIT_HEADER]: ["false"],
1456
1471
  [WORKFLOW_URL_HEADER]: [context.url],
1457
1472
  "Upstash-Workflow-CallType": ["step"]
@@ -1554,6 +1569,7 @@ var LazyInvokeStep = class extends BaseLazyStep {
1554
1569
  workflowConfig: {
1555
1570
  workflowRunId: context.workflowRunId,
1556
1571
  workflowUrl: context.url,
1572
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1557
1573
  telemetry: telemetry2,
1558
1574
  useJSONContent: false
1559
1575
  },
@@ -1563,6 +1579,9 @@ var LazyInvokeStep = class extends BaseLazyStep {
1563
1579
  invokerHeaders[key] = value;
1564
1580
  });
1565
1581
  invokerHeaders["Upstash-Workflow-Runid"] = context.workflowRunId;
1582
+ if (context.workflowRunCreatedAt) {
1583
+ invokerHeaders["Upstash-Workflow-Createdat"] = context.workflowRunCreatedAt.toString();
1584
+ }
1566
1585
  const request = {
1567
1586
  body: stringifyBody(this.params.body),
1568
1587
  headers: Object.fromEntries(
@@ -1591,6 +1610,9 @@ var LazyInvokeStep = class extends BaseLazyStep {
1591
1610
  workflowConfig: {
1592
1611
  workflowRunId: getWorkflowRunId(workflowRunId),
1593
1612
  workflowUrl: newUrl,
1613
+ // the invoked run doesn't exist yet, so the creation time of the invoker run is sent.
1614
+ // it separates the invocations made by different runs of the invoker.
1615
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1594
1616
  retries,
1595
1617
  retryDelay,
1596
1618
  telemetry: telemetry2,
@@ -1755,6 +1777,9 @@ var WorkflowHeaders = class {
1755
1777
  [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
1756
1778
  ...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
1757
1779
  };
1780
+ if (this.workflowConfig.workflowRunCreatedAt) {
1781
+ this.headers.rawHeaders[WORKFLOW_CREATED_AT_HEADER] = this.workflowConfig.workflowRunCreatedAt.toString();
1782
+ }
1758
1783
  if (this.stepInfo?.lazyStep.stepType !== "Call") {
1759
1784
  this.headers.rawHeaders[`Upstash-Forward-${WORKFLOW_PROTOCOL_VERSION_HEADER}`] = WORKFLOW_PROTOCOL_VERSION;
1760
1785
  }
@@ -1914,6 +1939,7 @@ var submitParallelSteps = async ({
1914
1939
  workflowConfig: {
1915
1940
  workflowRunId: context.workflowRunId,
1916
1941
  workflowUrl: context.url,
1942
+ workflowRunCreatedAt: context.workflowRunCreatedAt,
1917
1943
  telemetry: telemetry2
1918
1944
  },
1919
1945
  invokeCount
@@ -3415,7 +3441,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
3415
3441
  const client = new import_qstash11.Client({
3416
3442
  ...clientOptions,
3417
3443
  baseUrl: clientEnv.QSTASH_URL,
3418
- token: clientEnv.QSTASH_TOKEN
3444
+ token: clientEnv.QSTASH_TOKEN,
3445
+ // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
3446
+ // the client at the local dev server too. Without this the QStash Client
3447
+ // re-derives dev mode from its own process.env, which is blind to the
3448
+ // Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
3449
+ // An explicit devMode in the user's client config still wins.
3450
+ devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
3419
3451
  });
3420
3452
  const receiver = getReceiver(environment, receiverConfig, region);
3421
3453
  return { client, receiver };
@@ -3465,7 +3497,9 @@ var getQStashHandlers = ({
3465
3497
  client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
3466
3498
  ...qstashClientOption,
3467
3499
  baseUrl: environment.QSTASH_URL,
3468
- token: environment.QSTASH_TOKEN
3500
+ token: environment.QSTASH_TOKEN,
3501
+ // Mirror the receiver's dev-mode decision (see createRegionalHandler).
3502
+ devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
3469
3503
  }),
3470
3504
  receiver: getReceiver(environment, receiverConfig)
3471
3505
  }
@@ -3804,7 +3838,8 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3804
3838
  const invokeCount = Number(request.headers.get(WORKFLOW_INVOKE_COUNT_HEADER) ?? "0");
3805
3839
  const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
3806
3840
  const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
3807
- const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
3841
+ const workflowRunCreatedAtHeader = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
3842
+ const workflowRunCreatedAt = workflowRunCreatedAtHeader ? Number(workflowRunCreatedAtHeader) : isFirstInvocation ? Date.now() : 0;
3808
3843
  const workflowContext = new WorkflowContext({
3809
3844
  qstashClient: regionalClient,
3810
3845
  workflowRunId,
@@ -3817,7 +3852,7 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3817
3852
  invokeCount,
3818
3853
  label,
3819
3854
  retried,
3820
- workflowRunCreatedAt: Number(workflowRunCreatedAt),
3855
+ workflowRunCreatedAt,
3821
3856
  middlewareManager
3822
3857
  });
3823
3858
  const authCheck = await DisabledWorkflowContext.tryAuthentication(
@@ -3940,11 +3975,13 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
3940
3975
  };
3941
3976
 
3942
3977
  // platforms/hono.ts
3978
+ var import_qstash13 = require("@upstash/qstash");
3943
3979
  var telemetry = {
3944
3980
  sdk: SDK_TELEMETRY,
3945
3981
  framework: "hono"
3946
3982
  };
3947
3983
  var serve = (routeFunction, options) => {
3984
+ void (0, import_qstash13.startDevServer)();
3948
3985
  const handler = async (context) => {
3949
3986
  const environment = context.env ? context.env : typeof process === "undefined" ? {} : process.env;
3950
3987
  const request = context.req.raw;
package/hono.mjs CHANGED
@@ -2,14 +2,16 @@ import {
2
2
  SDK_TELEMETRY,
3
3
  serveBase,
4
4
  serveManyBase
5
- } from "./chunk-WEQGCISK.mjs";
5
+ } from "./chunk-DRHEHWMS.mjs";
6
6
 
7
7
  // platforms/hono.ts
8
+ import { startDevServer } from "@upstash/qstash";
8
9
  var telemetry = {
9
10
  sdk: SDK_TELEMETRY,
10
11
  framework: "hono"
11
12
  };
12
13
  var serve = (routeFunction, options) => {
14
+ void startDevServer();
13
15
  const handler = async (context) => {
14
16
  const environment = context.env ? context.env : typeof process === "undefined" ? {} : process.env;
15
17
  const request = context.req.raw;
package/index.d.mts CHANGED
@@ -209,6 +209,11 @@ type StepLogGroup = {
209
209
  */
210
210
  type: "next";
211
211
  };
212
+ /**
213
+ * State of the failure function (`failureUrl`/`failureFunction`) call of a
214
+ * failed workflow run.
215
+ */
216
+ type FailureFunctionState = "CALLBACK_INPROGRESS" | "CALLBACK_SUCCESS" | "CALLBACK_FAIL" | "CALLBACK_CANCELED";
212
217
  type FailureFunctionLog = {
213
218
  /**
214
219
  * messageId of the message published for handling the failure
@@ -221,7 +226,7 @@ type FailureFunctionLog = {
221
226
  /**
222
227
  * State of the message published for failure
223
228
  */
224
- state: "CALLBACK_INPROGRESS" | "CALLBACK_SUCCESS" | "CALLBACK_FAIL" | "CALLBACK_CANCELED";
229
+ state: FailureFunctionState;
225
230
  /**
226
231
  * Headers received from the step which caused the workflow to fail
227
232
  */
@@ -570,7 +575,11 @@ type WorkflowFilterFields = {
570
575
  workflowUrl?: string | string[];
571
576
  workflowRunId?: string;
572
577
  workflowCreatedAt?: number;
573
- failureFunctionState?: string;
578
+ /**
579
+ * Filter by the state of the failure function call. Supports multiple values:
580
+ * pass an array to match runs in any of the given states (OR semantics).
581
+ */
582
+ failureFunctionState?: FailureFunctionState | FailureFunctionState[];
574
583
  };
575
584
  type WorkflowLogsFilterFields = {
576
585
  state?: WorkflowRunLog["workflowState"];
@@ -680,7 +689,7 @@ type ResumeRestartOptions = {
680
689
  retries?: number;
681
690
  };
682
691
  type FailureCallbackInfo = {
683
- state?: "CALLBACK_FAIL" | "CALLBACK_SUCCESS" | "CALLBACK_INPROGRESS" | "CALLBACK_CANCELED";
692
+ state?: FailureFunctionState;
684
693
  responseStatus?: number;
685
694
  responseBody?: string;
686
695
  responseHeaders?: Record<string, string[]>;
@@ -834,6 +843,15 @@ declare class DLQ {
834
843
  retryFailureFunction({ dlqId }: Pick<DLQResumeRestartOptions<string>, "dlqId">): Promise<DLQResumeRestartResponse & {
835
844
  error?: string;
836
845
  }>;
846
+ /**
847
+ * Cancel the in-progress failure function (failureUrl/failureFunction) call
848
+ * of a workflow run.
849
+ *
850
+ * Fails if the run has no failure function call in progress.
851
+ *
852
+ * @param dlqId - The ID of the DLQ message whose failure function call will be canceled
853
+ */
854
+ cancelFailureFunction({ dlqId }: Pick<DLQResumeRestartOptions<string>, "dlqId">): Promise<void>;
837
855
  /**
838
856
  * Delete DLQ messages.
839
857
  *
@@ -1071,4 +1089,4 @@ declare class Client {
1071
1089
  get dlq(): DLQ;
1072
1090
  }
1073
1091
 
1074
- export { Client, type DLQResumeRestartOptions, type DLQResumeRestartResponse, NotifyResponse, RawStep, RouteFunction, type StepError, type StepLog, StepType, Telemetry, type TriggerOptions, Waiter, WorkflowClient, WorkflowMiddleware, WorkflowReceiver, type WorkflowRunLog, type WorkflowRunLogs, WorkflowServeOptions, loggingMiddleware, serve };
1092
+ export { Client, type DLQResumeRestartOptions, type DLQResumeRestartResponse, type FailureFunctionState, NotifyResponse, RawStep, RouteFunction, type StepError, type StepLog, StepType, Telemetry, type TriggerOptions, Waiter, WorkflowClient, WorkflowMiddleware, WorkflowReceiver, type WorkflowRunLog, type WorkflowRunLogs, WorkflowServeOptions, loggingMiddleware, serve };