@dudousxd/nestjs-durable 0.47.1 → 0.48.1

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/dist/index.js CHANGED
@@ -125,6 +125,7 @@ function Workflow(options) {
125
125
  name: options.name,
126
126
  version: options.version ?? "1",
127
127
  deadLetterWorkflow: options.deadLetterWorkflow,
128
+ onError: options.onError,
128
129
  tags: options.tags,
129
130
  singleton: options.singleton,
130
131
  concurrency: options.concurrency,
@@ -202,6 +203,26 @@ function isDeadLetterHandler(method) {
202
203
  return Reflect.getMetadata(DEAD_LETTER_METADATA, method) === true;
203
204
  }
204
205
  __name(isDeadLetterHandler, "isDeadLetterHandler");
206
+ var ON_ERROR_METADATA = Symbol("nestjs-durable:on-error");
207
+ function OnError(match) {
208
+ const code = match === void 0 || typeof match === "string" ? match : match.code;
209
+ if (match !== void 0 && typeof code !== "string") {
210
+ throw new Error('@OnError(ErrorClass): the class has no static `code`. Declare `static readonly code = "..."` on it, or pass the code string.');
211
+ }
212
+ return (_target, _propertyKey, descriptor) => {
213
+ const method = descriptor.value;
214
+ const codes = Reflect.getMetadata(ON_ERROR_METADATA, method) ?? [];
215
+ Reflect.defineMetadata(ON_ERROR_METADATA, [
216
+ ...codes,
217
+ code
218
+ ], method);
219
+ };
220
+ }
221
+ __name(OnError, "OnError");
222
+ function onErrorCodesOf(method) {
223
+ return Reflect.getMetadata(ON_ERROR_METADATA, method);
224
+ }
225
+ __name(onErrorCodesOf, "onErrorCodesOf");
205
226
  var ON_EVENT_METADATA = Symbol("nestjs-durable:on-event");
206
227
  function OnDurableEvent(...events) {
207
228
  return (target) => {
@@ -404,6 +425,66 @@ function scanSteps(discovery, scanner, register) {
404
425
  }
405
426
  __name(scanSteps, "scanSteps");
406
427
 
428
+ // src/on-error-handlers.ts
429
+ import { workflowName as workflowName2 } from "@dudousxd/nestjs-durable-core";
430
+ function onErrorHandlersOf(meta, instance, scanner) {
431
+ const handlers = [];
432
+ const routes = [];
433
+ for (const methodName of scanner.getAllMethodNames(Object.getPrototypeOf(instance))) {
434
+ const method = instance[methodName];
435
+ const codes = typeof method === "function" ? onErrorCodesOf(method) : void 0;
436
+ if (!codes) continue;
437
+ const name = `${meta.name}.onError.${methodName}`;
438
+ handlers.push([
439
+ name,
440
+ (ctx, input) => method.call(instance, ctx, input)
441
+ ]);
442
+ for (const code of codes) routes.push({
443
+ code,
444
+ workflow: name
445
+ });
446
+ }
447
+ for (const r of meta.onError ?? []) {
448
+ routes.push({
449
+ code: r.code === void 0 || typeof r.code === "string" ? r.code : r.code.code,
450
+ workflow: workflowName2(r.workflow)
451
+ });
452
+ }
453
+ return {
454
+ handlers,
455
+ routes
456
+ };
457
+ }
458
+ __name(onErrorHandlersOf, "onErrorHandlersOf");
459
+
460
+ // src/runtime-registration.ts
461
+ function registerOnRuntime(runtime, scanner, meta, instance, origin) {
462
+ const { handlers, routes } = onErrorHandlersOf(meta, instance, scanner);
463
+ const announced = {
464
+ version: meta.version,
465
+ ...origin !== void 0 ? {
466
+ origin
467
+ } : {}
468
+ };
469
+ runtime.registerWorkflow(meta.name, (ctx, input) => instance.run(ctx, input), {
470
+ ...announced,
471
+ ...meta.requires !== void 0 ? {
472
+ requires: meta.requires
473
+ } : {},
474
+ // A catch-all goes on the wire without a `code` key, not with an undefined one.
475
+ ...routes.length > 0 ? {
476
+ onError: routes.map(({ code, workflow }) => code === void 0 ? {
477
+ workflow
478
+ } : {
479
+ code,
480
+ workflow
481
+ })
482
+ } : {}
483
+ });
484
+ for (const [name, fn] of handlers) runtime.registerWorkflow(name, fn, announced);
485
+ }
486
+ __name(registerOnRuntime, "registerOnRuntime");
487
+
407
488
  // src/durable-worker.module.ts
408
489
  function _ts_decorate2(decorators, target, key, desc) {
409
490
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -443,34 +524,29 @@ var ThinWorkflowRegistrar = class {
443
524
  __name(this, "ThinWorkflowRegistrar");
444
525
  }
445
526
  discovery;
527
+ metadataScanner;
446
528
  runtime;
447
529
  options;
448
- constructor(discovery, runtime, options) {
530
+ constructor(discovery, metadataScanner, runtime, options) {
449
531
  this.discovery = discovery;
532
+ this.metadataScanner = metadataScanner;
450
533
  this.runtime = runtime;
451
534
  this.options = options;
452
535
  }
453
536
  onModuleInit() {
454
537
  const runtime = this.runtime;
455
538
  if (runtime === null || !isPureThinWorker(this.options)) return;
456
- scanWorkflows(this.discovery, (meta, instance, origin) => runtime.registerWorkflow(meta.name, (ctx, input) => instance.run(ctx, input), {
457
- version: meta.version,
458
- ...meta.requires !== void 0 ? {
459
- requires: meta.requires
460
- } : {},
461
- ...origin !== void 0 ? {
462
- origin
463
- } : {}
464
- }));
539
+ scanWorkflows(this.discovery, (meta, instance, origin) => registerOnRuntime(runtime, this.metadataScanner, meta, instance, origin));
465
540
  }
466
541
  };
467
542
  ThinWorkflowRegistrar = _ts_decorate2([
468
543
  Injectable2(),
469
- _ts_param(1, Inject(DURABLE_WORKER_RUNTIME)),
470
- _ts_param(2, Inject(DURABLE_OPTIONS_CANONICAL)),
544
+ _ts_param(2, Inject(DURABLE_WORKER_RUNTIME)),
545
+ _ts_param(3, Inject(DURABLE_OPTIONS_CANONICAL)),
471
546
  _ts_metadata2("design:type", Function),
472
547
  _ts_metadata2("design:paramtypes", [
473
548
  typeof DiscoveryService === "undefined" ? Object : DiscoveryService,
549
+ typeof MetadataScanner === "undefined" ? Object : MetadataScanner,
474
550
  Object,
475
551
  typeof DurableModuleOptions === "undefined" ? Object : DurableModuleOptions
476
552
  ])
@@ -526,6 +602,7 @@ var ThinWorkerBootstrap = class {
526
602
  const options = this.options;
527
603
  const runtime = this.runtime;
528
604
  if (runtime === null || !isPureThinWorker(options) || options.connection === void 0) return;
605
+ if (options.consumers === false) return;
529
606
  const handle = await this.runRedisWorker({
530
607
  runtime,
531
608
  connection: options.connection,
@@ -897,21 +974,14 @@ var InAppWorkerBootstrap = class {
897
974
  onModuleInit() {
898
975
  const runtime = this.runtime;
899
976
  if (runtime === null || !isCoLocatedWorker(this.options)) return;
900
- scanWorkflows(this.discovery, (meta, instance, origin) => runtime.registerWorkflow(meta.name, (ctx, input) => instance.run(ctx, input), {
901
- version: meta.version,
902
- ...meta.requires !== void 0 ? {
903
- requires: meta.requires
904
- } : {},
905
- ...origin !== void 0 ? {
906
- origin
907
- } : {}
908
- }));
977
+ scanWorkflows(this.discovery, (meta, instance, origin) => registerOnRuntime(runtime, this.metadataScanner, meta, instance, origin));
909
978
  scanSteps(this.discovery, this.metadataScanner, (meta, handler) => runtime.registerStep(meta.name, handler));
910
979
  }
911
980
  async onApplicationBootstrap() {
912
981
  const options = this.options;
913
982
  const runtime = this.runtime;
914
983
  if (runtime === null || !isCoLocatedWorker(options) || options.connection === void 0) return;
984
+ if (options.consumers === false) return;
915
985
  const handle = await this.runRedisWorker({
916
986
  runtime,
917
987
  connection: options.connection,
@@ -995,7 +1065,212 @@ function inAppWorkerProviders() {
995
1065
  __name(inAppWorkerProviders, "inAppWorkerProviders");
996
1066
 
997
1067
  // src/proxy-run-gateway.ts
1068
+ import { SUMMARY_CHILD_LIMIT, childIdsOf, pageFan, summarizeTimeline } from "@dudousxd/nestjs-durable-core";
998
1069
  import { Injectable as Injectable6 } from "@nestjs/common";
1070
+
1071
+ // src/run-request-responder.ts
1072
+ var UNSUPPORTED_RUN_REQUEST = "unsupported-request";
1073
+ var KNOWN_KINDS = /* @__PURE__ */ new Set([
1074
+ "getRunDetail",
1075
+ "getFanMembers",
1076
+ "listRuns",
1077
+ "runFacets",
1078
+ "runValueFacets",
1079
+ "workerHealth",
1080
+ "cancel",
1081
+ "retry",
1082
+ "continue",
1083
+ "retryWithInput",
1084
+ "redispatch",
1085
+ "waitingFor"
1086
+ ]);
1087
+ var RunRequestResponder = class {
1088
+ static {
1089
+ __name(this, "RunRequestResponder");
1090
+ }
1091
+ transport;
1092
+ gateway;
1093
+ constructor(transport, gateway) {
1094
+ this.transport = transport;
1095
+ this.gateway = gateway;
1096
+ }
1097
+ /** Register the consumer on the transport. Each request is answered independently; a handler
1098
+ * failure never throws back into the transport (errors are captured into an error reply). */
1099
+ start() {
1100
+ this.transport.onRunRequest(async (msg) => {
1101
+ let reply;
1102
+ try {
1103
+ reply = await this.handle(msg);
1104
+ } catch (err) {
1105
+ reply = {
1106
+ requestId: msg.requestId,
1107
+ result: {
1108
+ ok: false,
1109
+ error: {
1110
+ message: err instanceof Error ? err.message : String(err)
1111
+ }
1112
+ }
1113
+ };
1114
+ }
1115
+ await this.transport.publishRunReply(reply);
1116
+ });
1117
+ }
1118
+ async handle(msg) {
1119
+ const { body } = msg;
1120
+ if (!KNOWN_KINDS.has(body.kind)) {
1121
+ return {
1122
+ requestId: msg.requestId,
1123
+ result: {
1124
+ ok: false,
1125
+ error: {
1126
+ message: `the control plane does not support the ${String(body.kind)} run request`,
1127
+ code: UNSUPPORTED_RUN_REQUEST
1128
+ }
1129
+ }
1130
+ };
1131
+ }
1132
+ if (body.kind === "listRuns") {
1133
+ const data = await this.gateway.listRuns({
1134
+ ...body.query,
1135
+ namespace: msg.tenant
1136
+ });
1137
+ return {
1138
+ requestId: msg.requestId,
1139
+ result: {
1140
+ ok: true,
1141
+ data
1142
+ }
1143
+ };
1144
+ }
1145
+ if (body.kind === "runFacets") {
1146
+ const data = await this.gateway.runFacets({
1147
+ ...body.query,
1148
+ namespace: msg.tenant
1149
+ });
1150
+ return {
1151
+ requestId: msg.requestId,
1152
+ result: {
1153
+ ok: true,
1154
+ data
1155
+ }
1156
+ };
1157
+ }
1158
+ if (body.kind === "runValueFacets") {
1159
+ const data = await this.gateway.runValueFacets(body.axis, {
1160
+ ...body.query,
1161
+ namespace: msg.tenant
1162
+ }, body.opts);
1163
+ return {
1164
+ requestId: msg.requestId,
1165
+ result: {
1166
+ ok: true,
1167
+ data
1168
+ }
1169
+ };
1170
+ }
1171
+ if (body.kind === "workerHealth") {
1172
+ const all = await this.gateway.workerHealth();
1173
+ const data = all.filter((h) => h.group.endsWith(`@${msg.tenant}`));
1174
+ return {
1175
+ requestId: msg.requestId,
1176
+ result: {
1177
+ ok: true,
1178
+ data
1179
+ }
1180
+ };
1181
+ }
1182
+ if (body.kind === "waitingFor") {
1183
+ const all = await this.gateway.waitingFor(body.runIds);
1184
+ const owned = await Promise.all(Object.entries(all).map(async ([runId, waiting]) => {
1185
+ const owner = this.gateway.getRun ? await this.gateway.getRun(runId) : (await this.gateway.getRunDetail(runId))?.run;
1186
+ return owner && owner.namespace === msg.tenant ? [
1187
+ runId,
1188
+ waiting
1189
+ ] : void 0;
1190
+ }));
1191
+ const data = {};
1192
+ for (const entry of owned) {
1193
+ if (entry) data[entry[0]] = entry[1];
1194
+ }
1195
+ return {
1196
+ requestId: msg.requestId,
1197
+ result: {
1198
+ ok: true,
1199
+ data
1200
+ }
1201
+ };
1202
+ }
1203
+ const detail = body.kind === "getRunDetail" || !this.gateway.getRun ? await this.gateway.getRunDetail(body.runId, body.kind === "getRunDetail" ? body.options : void 0) : void 0;
1204
+ const run = detail !== void 0 ? detail?.run ?? null : await this.gateway.getRun?.(body.runId);
1205
+ if (run && run.namespace !== msg.tenant) {
1206
+ return {
1207
+ requestId: msg.requestId,
1208
+ result: {
1209
+ ok: false,
1210
+ error: {
1211
+ message: "run belongs to another tenant",
1212
+ code: "cross-tenant"
1213
+ }
1214
+ }
1215
+ };
1216
+ }
1217
+ if (body.kind === "getRunDetail") {
1218
+ return {
1219
+ requestId: msg.requestId,
1220
+ result: {
1221
+ ok: true,
1222
+ data: detail
1223
+ }
1224
+ };
1225
+ }
1226
+ if (body.kind === "getFanMembers") {
1227
+ const data = run ? await this.gateway.getFanMembers?.(body.runId, body.group, body.query) ?? null : null;
1228
+ return {
1229
+ requestId: msg.requestId,
1230
+ result: {
1231
+ ok: true,
1232
+ data
1233
+ }
1234
+ };
1235
+ }
1236
+ try {
1237
+ const data = await this.callVerb(body);
1238
+ return {
1239
+ requestId: msg.requestId,
1240
+ result: {
1241
+ ok: true,
1242
+ data
1243
+ }
1244
+ };
1245
+ } catch (err) {
1246
+ return {
1247
+ requestId: msg.requestId,
1248
+ result: {
1249
+ ok: false,
1250
+ error: {
1251
+ message: err instanceof Error ? err.message : String(err)
1252
+ }
1253
+ }
1254
+ };
1255
+ }
1256
+ }
1257
+ callVerb(body) {
1258
+ switch (body.kind) {
1259
+ case "cancel":
1260
+ return this.gateway.cancel(body.runId, body.opts);
1261
+ case "retry":
1262
+ return this.gateway.retry(body.runId);
1263
+ case "continue":
1264
+ return this.gateway.continue(body.runId);
1265
+ case "retryWithInput":
1266
+ return this.gateway.retryWithInput(body.runId, body.input);
1267
+ case "redispatch":
1268
+ return this.gateway.redispatchPending(body.runId);
1269
+ }
1270
+ }
1271
+ };
1272
+
1273
+ // src/proxy-run-gateway.ts
999
1274
  function _ts_decorate6(decorators, target, key, desc) {
1000
1275
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1001
1276
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -1016,6 +1291,15 @@ function _ts_metadata6(metadataKey, metadataValue) {
1016
1291
  }
1017
1292
  }
1018
1293
  __name(_ts_metadata6, "_ts_metadata");
1294
+ var UnsupportedRunRequestError = class UnsupportedRunRequestError2 extends Error {
1295
+ static {
1296
+ __name(this, "UnsupportedRunRequestError");
1297
+ }
1298
+ };
1299
+ function isUnsupported(err) {
1300
+ return err instanceof UnsupportedRunRequestError;
1301
+ }
1302
+ __name(isUnsupported, "isUnsupported");
1019
1303
  var ProxyRunGateway = class {
1020
1304
  static {
1021
1305
  __name(this, "ProxyRunGateway");
@@ -1023,11 +1307,18 @@ var ProxyRunGateway = class {
1023
1307
  transport;
1024
1308
  tenant;
1025
1309
  timeoutMs;
1310
+ capabilityTtlMs;
1026
1311
  pending = /* @__PURE__ */ new Map();
1027
- constructor(transport, tenant, timeoutMs = 1e4) {
1312
+ /** Verb → when to ask the control plane for it again, after it answered that it does not know it.
1313
+ * During a rolling upgrade the control plane is several pods of mixed versions behind one queue,
1314
+ * and a reply does not say which pod sent it, so the whole control plane is assumed old until the
1315
+ * entry expires. */
1316
+ unsupportedUntil = /* @__PURE__ */ new Map();
1317
+ constructor(transport, tenant, timeoutMs = 1e4, capabilityTtlMs = 6e4) {
1028
1318
  this.transport = transport;
1029
1319
  this.tenant = tenant;
1030
1320
  this.timeoutMs = timeoutMs;
1321
+ this.capabilityTtlMs = capabilityTtlMs;
1031
1322
  this.transport.onRunReply?.((reply) => this.handleReply(reply));
1032
1323
  }
1033
1324
  handleReply(reply) {
@@ -1037,6 +1328,8 @@ var ProxyRunGateway = class {
1037
1328
  this.pending.delete(reply.requestId);
1038
1329
  if (reply.result.ok) {
1039
1330
  pending.resolve(reply.result.data);
1331
+ } else if (reply.result.error.code === UNSUPPORTED_RUN_REQUEST) {
1332
+ pending.reject(new UnsupportedRunRequestError(reply.result.error.message));
1040
1333
  } else {
1041
1334
  pending.reject(new Error(reply.result.error.message));
1042
1335
  }
@@ -1072,20 +1365,59 @@ var ProxyRunGateway = class {
1072
1365
  tenant: this.tenant
1073
1366
  };
1074
1367
  }
1075
- getRunDetail(runId, options) {
1076
- return this.request({
1368
+ /**
1369
+ * A control plane that predates {@link RunDetailOptions} ignores them and sends the full detail;
1370
+ * a summarized detail always carries `fans`, so its absence means the summary is made here.
1371
+ */
1372
+ async getRunDetail(runId, options) {
1373
+ const detail = await this.request(options === void 0 ? {
1374
+ kind: "getRunDetail",
1375
+ runId
1376
+ } : {
1077
1377
  kind: "getRunDetail",
1078
1378
  runId,
1079
1379
  options
1080
1380
  });
1381
+ const over = options?.summarizeFansOver;
1382
+ if (over === void 0 || !detail || detail.fans !== void 0) return detail;
1383
+ const { timeline, fans } = summarizeTimeline(detail.timeline, over);
1384
+ const ids = childIdsOf(detail.timeline);
1385
+ return {
1386
+ ...detail,
1387
+ timeline,
1388
+ children: ids.slice(0, SUMMARY_CHILD_LIMIT),
1389
+ fans,
1390
+ childCount: ids.length
1391
+ };
1081
1392
  }
1082
- getFanMembers(runId, group, query) {
1083
- return this.request({
1084
- kind: "getFanMembers",
1085
- runId,
1086
- group,
1087
- query
1393
+ /** Paged from the full detail when the control plane is too old to page a fan itself. */
1394
+ async getFanMembers(runId, group, query) {
1395
+ if (!this.knownUnsupported("getFanMembers")) {
1396
+ try {
1397
+ const page = await this.request({
1398
+ kind: "getFanMembers",
1399
+ runId,
1400
+ group,
1401
+ query
1402
+ });
1403
+ if (page !== void 0) return page;
1404
+ } catch (err) {
1405
+ if (!isUnsupported(err)) throw err;
1406
+ }
1407
+ this.unsupportedUntil.set("getFanMembers", Date.now() + this.capabilityTtlMs);
1408
+ }
1409
+ const detail = await this.request({
1410
+ kind: "getRunDetail",
1411
+ runId
1088
1412
  });
1413
+ return detail ? pageFan(detail.timeline, group, query) : null;
1414
+ }
1415
+ knownUnsupported(kind) {
1416
+ const until = this.unsupportedUntil.get(kind);
1417
+ if (until === void 0) return false;
1418
+ if (Date.now() < until) return true;
1419
+ this.unsupportedUntil.delete(kind);
1420
+ return false;
1089
1421
  }
1090
1422
  /** The control plane's `StoreRunGateway` resolves each run's `waiting` descriptor; the reply carries
1091
1423
  * it through as plain JSON, so a tenant's list rows name the wait too. */
@@ -1175,6 +1507,7 @@ ProxyRunGateway = _ts_decorate6([
1175
1507
  _ts_metadata6("design:paramtypes", [
1176
1508
  typeof Transport === "undefined" ? Object : Transport,
1177
1509
  String,
1510
+ void 0,
1178
1511
  void 0
1179
1512
  ])
1180
1513
  ], ProxyRunGateway);
@@ -1372,170 +1705,8 @@ RetentionPoller = _ts_decorate7([
1372
1705
  ])
1373
1706
  ], RetentionPoller);
1374
1707
 
1375
- // src/run-request-responder.ts
1376
- var RunRequestResponder = class {
1377
- static {
1378
- __name(this, "RunRequestResponder");
1379
- }
1380
- transport;
1381
- gateway;
1382
- constructor(transport, gateway) {
1383
- this.transport = transport;
1384
- this.gateway = gateway;
1385
- }
1386
- /** Register the consumer on the transport. Each request is answered independently; a handler
1387
- * failure never throws back into the transport (errors are captured into an error reply). */
1388
- start() {
1389
- this.transport.onRunRequest(async (msg) => {
1390
- const reply = await this.handle(msg);
1391
- await this.transport.publishRunReply(reply);
1392
- });
1393
- }
1394
- async handle(msg) {
1395
- const { body } = msg;
1396
- if (body.kind === "listRuns") {
1397
- const data = await this.gateway.listRuns({
1398
- ...body.query,
1399
- namespace: msg.tenant
1400
- });
1401
- return {
1402
- requestId: msg.requestId,
1403
- result: {
1404
- ok: true,
1405
- data
1406
- }
1407
- };
1408
- }
1409
- if (body.kind === "runFacets") {
1410
- const data = await this.gateway.runFacets({
1411
- ...body.query,
1412
- namespace: msg.tenant
1413
- });
1414
- return {
1415
- requestId: msg.requestId,
1416
- result: {
1417
- ok: true,
1418
- data
1419
- }
1420
- };
1421
- }
1422
- if (body.kind === "runValueFacets") {
1423
- const data = await this.gateway.runValueFacets(body.axis, {
1424
- ...body.query,
1425
- namespace: msg.tenant
1426
- }, body.opts);
1427
- return {
1428
- requestId: msg.requestId,
1429
- result: {
1430
- ok: true,
1431
- data
1432
- }
1433
- };
1434
- }
1435
- if (body.kind === "workerHealth") {
1436
- const all = await this.gateway.workerHealth();
1437
- const data = all.filter((h) => h.group.endsWith(`@${msg.tenant}`));
1438
- return {
1439
- requestId: msg.requestId,
1440
- result: {
1441
- ok: true,
1442
- data
1443
- }
1444
- };
1445
- }
1446
- if (body.kind === "waitingFor") {
1447
- const all = await this.gateway.waitingFor(body.runIds);
1448
- const owned = await Promise.all(Object.entries(all).map(async ([runId, waiting]) => {
1449
- const owner = this.gateway.getRun ? await this.gateway.getRun(runId) : (await this.gateway.getRunDetail(runId))?.run;
1450
- return owner && owner.namespace === msg.tenant ? [
1451
- runId,
1452
- waiting
1453
- ] : void 0;
1454
- }));
1455
- const data = {};
1456
- for (const entry of owned) {
1457
- if (entry) data[entry[0]] = entry[1];
1458
- }
1459
- return {
1460
- requestId: msg.requestId,
1461
- result: {
1462
- ok: true,
1463
- data
1464
- }
1465
- };
1466
- }
1467
- const detail = body.kind === "getRunDetail" || !this.gateway.getRun ? await this.gateway.getRunDetail(body.runId, body.kind === "getRunDetail" ? body.options : void 0) : void 0;
1468
- const run = detail !== void 0 ? detail?.run ?? null : await this.gateway.getRun?.(body.runId);
1469
- if (run && run.namespace !== msg.tenant) {
1470
- return {
1471
- requestId: msg.requestId,
1472
- result: {
1473
- ok: false,
1474
- error: {
1475
- message: "run belongs to another tenant",
1476
- code: "cross-tenant"
1477
- }
1478
- }
1479
- };
1480
- }
1481
- if (body.kind === "getRunDetail") {
1482
- return {
1483
- requestId: msg.requestId,
1484
- result: {
1485
- ok: true,
1486
- data: detail
1487
- }
1488
- };
1489
- }
1490
- if (body.kind === "getFanMembers") {
1491
- const data = run ? await this.gateway.getFanMembers?.(body.runId, body.group, body.query) ?? null : null;
1492
- return {
1493
- requestId: msg.requestId,
1494
- result: {
1495
- ok: true,
1496
- data
1497
- }
1498
- };
1499
- }
1500
- try {
1501
- const data = await this.callVerb(body);
1502
- return {
1503
- requestId: msg.requestId,
1504
- result: {
1505
- ok: true,
1506
- data
1507
- }
1508
- };
1509
- } catch (err) {
1510
- return {
1511
- requestId: msg.requestId,
1512
- result: {
1513
- ok: false,
1514
- error: {
1515
- message: err instanceof Error ? err.message : String(err)
1516
- }
1517
- }
1518
- };
1519
- }
1520
- }
1521
- callVerb(body) {
1522
- switch (body.kind) {
1523
- case "cancel":
1524
- return this.gateway.cancel(body.runId, body.opts);
1525
- case "retry":
1526
- return this.gateway.retry(body.runId);
1527
- case "continue":
1528
- return this.gateway.continue(body.runId);
1529
- case "retryWithInput":
1530
- return this.gateway.retryWithInput(body.runId, body.input);
1531
- case "redispatch":
1532
- return this.gateway.redispatchPending(body.runId);
1533
- }
1534
- }
1535
- };
1536
-
1537
1708
  // src/store-run-gateway.ts
1538
- import { STATE_STORE_CANONICAL as STATE_STORE_CANONICAL2, SUMMARY_CHILD_LIMIT, WorkflowEngine as WorkflowEngine2, childIdsOf, indexWaitersByRun, listSignalWaitersOfRuns, mergeRunFacetRows, pageFan, resolveRunWaiting, summarizeTimeline } from "@dudousxd/nestjs-durable-core";
1709
+ import { STATE_STORE_CANONICAL as STATE_STORE_CANONICAL2, SUMMARY_CHILD_LIMIT as SUMMARY_CHILD_LIMIT2, WorkflowEngine as WorkflowEngine2, aggregatesCheckpoints, childIdsOf as childIdsOf2, etagMatches, fanPageEtag, indexWaitersByRun, listSignalWaitersOfRuns, mergeRunFacetRows, pageFan as pageFan2, pageFanFromStore, resolveRunWaiting, runDetailEtag, summarizeRunFromStore, summarizeTimeline as summarizeTimeline2 } from "@dudousxd/nestjs-durable-core";
1539
1710
  import { Inject as Inject5, Injectable as Injectable8 } from "@nestjs/common";
1540
1711
  function _ts_decorate8(decorators, target, key, desc) {
1541
1712
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -1593,13 +1764,31 @@ var StoreRunGateway = class {
1593
1764
  children
1594
1765
  };
1595
1766
  }
1767
+ if (aggregatesCheckpoints(this.store)) {
1768
+ const stats = await this.store.checkpointStats(runId);
1769
+ const etag = runDetailEtag(run, stats, over);
1770
+ if (etagMatches(options?.ifNoneMatch, etag)) {
1771
+ return {
1772
+ run,
1773
+ timeline: [],
1774
+ children: [],
1775
+ notModified: true,
1776
+ etag
1777
+ };
1778
+ }
1779
+ return {
1780
+ run,
1781
+ ...await summarizeRunFromStore(this.store, runId, over, stats),
1782
+ etag
1783
+ };
1784
+ }
1596
1785
  const full = await this.store.listCheckpoints(runId);
1597
- const { timeline, fans } = summarizeTimeline(full, over);
1598
- const ids = childIdsOf(full);
1786
+ const { timeline, fans } = summarizeTimeline2(full, over);
1787
+ const ids = childIdsOf2(full);
1599
1788
  return {
1600
1789
  run,
1601
1790
  timeline,
1602
- children: ids.slice(0, SUMMARY_CHILD_LIMIT),
1791
+ children: ids.slice(0, SUMMARY_CHILD_LIMIT2),
1603
1792
  fans,
1604
1793
  childCount: ids.length
1605
1794
  };
@@ -1609,7 +1798,25 @@ var StoreRunGateway = class {
1609
1798
  }
1610
1799
  async getFanMembers(runId, group, query) {
1611
1800
  if (!await this.store.getRun(runId)) return null;
1612
- return pageFan(await this.store.listCheckpoints(runId), group, query);
1801
+ if (aggregatesCheckpoints(this.store)) {
1802
+ const stats = await this.store.checkpointStats(runId, {
1803
+ group
1804
+ });
1805
+ const etag = fanPageEtag(group, stats, query);
1806
+ if (etagMatches(query.ifNoneMatch, etag)) {
1807
+ return {
1808
+ items: [],
1809
+ total: 0,
1810
+ notModified: true,
1811
+ etag
1812
+ };
1813
+ }
1814
+ return {
1815
+ ...await pageFanFromStore(this.store, runId, group, query, stats),
1816
+ etag
1817
+ };
1818
+ }
1819
+ return pageFan2(await this.store.listCheckpoints(runId), group, query);
1613
1820
  }
1614
1821
  async listRuns(query) {
1615
1822
  const runs = await this.store.listRuns(query);
@@ -1900,7 +2107,7 @@ var CONTEXT_ACCESSOR = Symbol.for("@dudousxd/nestjs-context:accessor");
1900
2107
  var RUN_GATEWAY = RunGateway;
1901
2108
 
1902
2109
  // src/workflow.registrar.ts
1903
- import { DURABLE_OPTIONS_CANONICAL as DURABLE_OPTIONS_CANONICAL6, RemoteWorkflowExecutor, STATE_STORE_CANONICAL as STATE_STORE_CANONICAL3, WorkflowEngine as WorkflowEngine4, bindWorkflowClass, parseDuration as parseDuration2, sanitizeQueueToken, tenantGroup, workflowName as workflowName2 } from "@dudousxd/nestjs-durable-core";
2110
+ import { DURABLE_OPTIONS_CANONICAL as DURABLE_OPTIONS_CANONICAL6, RemoteWorkflowExecutor, STATE_STORE_CANONICAL as STATE_STORE_CANONICAL3, WorkflowEngine as WorkflowEngine4, bindWorkflowClass, parseDuration as parseDuration2, sanitizeQueueToken, tenantGroup, workflowName as workflowName3 } from "@dudousxd/nestjs-durable-core";
1904
2111
  import { Inject as Inject7, Injectable as Injectable10 } from "@nestjs/common";
1905
2112
  import { DiscoveryService as DiscoveryService4, MetadataScanner as MetadataScanner4 } from "@nestjs/core";
1906
2113
 
@@ -1970,6 +2177,7 @@ function _ts_param7(paramIndex, decorator) {
1970
2177
  };
1971
2178
  }
1972
2179
  __name(_ts_param7, "_ts_param");
2180
+ var UNSERVED_ROUTES_RECHECK_MS = 3e4;
1973
2181
  var WorkflowRegistrar = class {
1974
2182
  static {
1975
2183
  __name(this, "WorkflowRegistrar");
@@ -2040,6 +2248,12 @@ var WorkflowRegistrar = class {
2040
2248
  windowMs: parseDuration2(meta.batch.within)
2041
2249
  } : void 0;
2042
2250
  const workflowCtor = workflow.constructor;
2251
+ const onError = onErrorHandlersOf(meta, workflow, this.metadataScanner);
2252
+ for (const [name, fn] of onError.handlers) {
2253
+ this.engine.register(name, meta.version, fn, {
2254
+ origin
2255
+ });
2256
+ }
2043
2257
  this.engine.register(meta.name, meta.version, (ctx, input) => workflow.run(ctx, input), {
2044
2258
  // Stamped onto every run this workflow starts (see `WorkflowRun.origin`). Derived from the
2045
2259
  // class's declaring file, so a lib gets attributed without opting in; `undefined` when it
@@ -2054,6 +2268,7 @@ var WorkflowRegistrar = class {
2054
2268
  searchAttributesSchema: meta.searchAttributes,
2055
2269
  onEvent: getOnEvents(meta, workflowCtor),
2056
2270
  eventBatch,
2271
+ onError: onError.routes,
2057
2272
  // Uniform dispatch (opt-in): when an in-app worker is configured, register the body GROUP-SERVED
2058
2273
  // so the engine dispatches its turns over the transport instead of running it inline; the
2059
2274
  // co-located worker consumer (Task 5: one queue PER REGISTERED NAME) replays the same body.
@@ -2083,10 +2298,11 @@ var WorkflowRegistrar = class {
2083
2298
  });
2084
2299
  deadLetterByWorkflow.set(meta.name, dlqName);
2085
2300
  } else if (meta.deadLetterWorkflow) {
2086
- deadLetterByWorkflow.set(meta.name, workflowName2(meta.deadLetterWorkflow));
2301
+ deadLetterByWorkflow.set(meta.name, workflowName3(meta.deadLetterWorkflow));
2087
2302
  }
2088
2303
  });
2089
2304
  this.reportUnattributed(unattributed);
2305
+ void this.reportUnservedOnErrorRoutes();
2090
2306
  this.installDeadLetterRouting(deadLetterByWorkflow);
2091
2307
  }
2092
2308
  /**
@@ -2099,6 +2315,20 @@ var WorkflowRegistrar = class {
2099
2315
  if (names.length === 0) return;
2100
2316
  console.warn(`[nestjs-durable] could not derive an origin (declaring package) for ${names.length} workflow(s): ${names.join(", ")}. Their runs carry no origin and will not match an origin filter.`);
2101
2317
  }
2318
+ /**
2319
+ * Says, once after boot, which onError routes name a handler workflow nothing can run, so a typo is
2320
+ * caught before the first failure goes nowhere. Routes unserved at boot are checked again after
2321
+ * {@link UNSERVED_ROUTES_RECHECK_MS}, so a handler whose workers start after this process is not
2322
+ * reported.
2323
+ */
2324
+ async reportUnservedOnErrorRoutes(recheckMs = UNSERVED_ROUTES_RECHECK_MS) {
2325
+ if ((await this.engine.unservedOnErrorRoutes().catch(() => [])).length === 0) return;
2326
+ await new Promise((resolve) => setTimeout(resolve, recheckMs).unref());
2327
+ const unserved = await this.engine.unservedOnErrorRoutes().catch(() => []);
2328
+ if (unserved.length === 0) return;
2329
+ const list = unserved.map(({ workflow, route }) => `${workflow} \u2192 ${route.workflow} (code ${route.code ?? "*"})`).join(", ");
2330
+ console.warn(`[nestjs-durable] ${unserved.length} onError route(s) name a handler workflow that is not registered here and has no live worker: ${list}.`);
2331
+ }
2102
2332
  /** Returns the instance's `@DeadLetter()` method bound to the instance, or undefined if none. */
2103
2333
  findDeadLetterHandler(instance) {
2104
2334
  const prototype = Object.getPrototypeOf(instance);
@@ -2116,7 +2346,7 @@ var WorkflowRegistrar = class {
2116
2346
  * a `dlq:<runId>` id, so re-recovery never double-dispatches.
2117
2347
  */
2118
2348
  installDeadLetterRouting(byWorkflow) {
2119
- const fallback = this.options.deadLetterWorkflow ? workflowName2(this.options.deadLetterWorkflow) : void 0;
2349
+ const fallback = this.options.deadLetterWorkflow ? workflowName3(this.options.deadLetterWorkflow) : void 0;
2120
2350
  if (byWorkflow.size === 0 && !fallback) return;
2121
2351
  this.engine.onDead((run) => {
2122
2352
  const target = byWorkflow.get(run.workflow) ?? fallback;
@@ -2308,6 +2538,15 @@ function scopedStore(store, options) {
2308
2538
  });
2309
2539
  }
2310
2540
  __name(scopedStore, "scopedStore");
2541
+ function deferTransportConsumers(transport) {
2542
+ const deferrable = transport;
2543
+ if (typeof deferrable.deferConsumers === "function") {
2544
+ deferrable.deferConsumers();
2545
+ return;
2546
+ }
2547
+ console.warn(`[nestjs-durable] consumers: false \u2014 ${transport.constructor?.name ?? "this transport"} cannot defer its consumers and will keep consuming in this process`);
2548
+ }
2549
+ __name(deferTransportConsumers, "deferTransportConsumers");
2311
2550
  function assertValidRole(options) {
2312
2551
  const hasStore = options.store !== void 0;
2313
2552
  const hasConnection = options.connection !== void 0;
@@ -2534,6 +2773,14 @@ var DurableModule = class _DurableModule {
2534
2773
  const context = options.context ?? (accessor ? () => carrierFromAccessor(accessor) : void 0);
2535
2774
  const runtime = accessor ? await resolveContextRuntime() : void 0;
2536
2775
  const rehydrate = runtime && ((carrier, fn) => carrier && Object.keys(carrier).length > 0 ? runtime.deserialize(carrier, fn) : fn());
2776
+ if (options.consumers === false) {
2777
+ for (const t of [
2778
+ transport,
2779
+ ...(options.transports ?? []).map((n) => n.transport)
2780
+ ]) {
2781
+ if (t) deferTransportConsumers(t);
2782
+ }
2783
+ }
2537
2784
  const engine = new WorkflowEngine6({
2538
2785
  store,
2539
2786
  transport: transport ?? void 0,
@@ -2651,9 +2898,11 @@ export {
2651
2898
  IN_APP_WORKER_RUNNERS,
2652
2899
  IN_APP_WORKER_RUNTIME,
2653
2900
  InAppWorkerBootstrap,
2901
+ ON_ERROR_METADATA,
2654
2902
  ON_EVENT_METADATA,
2655
2903
  On,
2656
2904
  OnDurableEvent,
2905
+ OnError,
2657
2906
  OnEvent,
2658
2907
  ProxyRunGateway,
2659
2908
  RUN_GATEWAY,
@@ -2668,6 +2917,7 @@ export {
2668
2917
  ThinStepRegistrar,
2669
2918
  ThinWorkerBootstrap,
2670
2919
  ThinWorkflowRegistrar,
2920
+ UNSUPPORTED_RUN_REQUEST,
2671
2921
  WORKFLOW_METADATA,
2672
2922
  Workflow,
2673
2923
  WorkflowEngine7 as WorkflowEngine,
@@ -2683,6 +2933,7 @@ export {
2683
2933
  isDrivingOperator,
2684
2934
  isOperatorRole,
2685
2935
  isStepInterceptor,
2936
+ onErrorCodesOf,
2686
2937
  readSearchAttributes2 as readSearchAttributes,
2687
2938
  thinWorkerProviders,
2688
2939
  unavailableRunGateway