@dudousxd/nestjs-durable 0.47.1 → 0.48.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/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
  ])
@@ -897,15 +973,7 @@ var InAppWorkerBootstrap = class {
897
973
  onModuleInit() {
898
974
  const runtime = this.runtime;
899
975
  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
- }));
976
+ scanWorkflows(this.discovery, (meta, instance, origin) => registerOnRuntime(runtime, this.metadataScanner, meta, instance, origin));
909
977
  scanSteps(this.discovery, this.metadataScanner, (meta, handler) => runtime.registerStep(meta.name, handler));
910
978
  }
911
979
  async onApplicationBootstrap() {
@@ -995,7 +1063,212 @@ function inAppWorkerProviders() {
995
1063
  __name(inAppWorkerProviders, "inAppWorkerProviders");
996
1064
 
997
1065
  // src/proxy-run-gateway.ts
1066
+ import { SUMMARY_CHILD_LIMIT, childIdsOf, pageFan, summarizeTimeline } from "@dudousxd/nestjs-durable-core";
998
1067
  import { Injectable as Injectable6 } from "@nestjs/common";
1068
+
1069
+ // src/run-request-responder.ts
1070
+ var UNSUPPORTED_RUN_REQUEST = "unsupported-request";
1071
+ var KNOWN_KINDS = /* @__PURE__ */ new Set([
1072
+ "getRunDetail",
1073
+ "getFanMembers",
1074
+ "listRuns",
1075
+ "runFacets",
1076
+ "runValueFacets",
1077
+ "workerHealth",
1078
+ "cancel",
1079
+ "retry",
1080
+ "continue",
1081
+ "retryWithInput",
1082
+ "redispatch",
1083
+ "waitingFor"
1084
+ ]);
1085
+ var RunRequestResponder = class {
1086
+ static {
1087
+ __name(this, "RunRequestResponder");
1088
+ }
1089
+ transport;
1090
+ gateway;
1091
+ constructor(transport, gateway) {
1092
+ this.transport = transport;
1093
+ this.gateway = gateway;
1094
+ }
1095
+ /** Register the consumer on the transport. Each request is answered independently; a handler
1096
+ * failure never throws back into the transport (errors are captured into an error reply). */
1097
+ start() {
1098
+ this.transport.onRunRequest(async (msg) => {
1099
+ let reply;
1100
+ try {
1101
+ reply = await this.handle(msg);
1102
+ } catch (err) {
1103
+ reply = {
1104
+ requestId: msg.requestId,
1105
+ result: {
1106
+ ok: false,
1107
+ error: {
1108
+ message: err instanceof Error ? err.message : String(err)
1109
+ }
1110
+ }
1111
+ };
1112
+ }
1113
+ await this.transport.publishRunReply(reply);
1114
+ });
1115
+ }
1116
+ async handle(msg) {
1117
+ const { body } = msg;
1118
+ if (!KNOWN_KINDS.has(body.kind)) {
1119
+ return {
1120
+ requestId: msg.requestId,
1121
+ result: {
1122
+ ok: false,
1123
+ error: {
1124
+ message: `the control plane does not support the ${String(body.kind)} run request`,
1125
+ code: UNSUPPORTED_RUN_REQUEST
1126
+ }
1127
+ }
1128
+ };
1129
+ }
1130
+ if (body.kind === "listRuns") {
1131
+ const data = await this.gateway.listRuns({
1132
+ ...body.query,
1133
+ namespace: msg.tenant
1134
+ });
1135
+ return {
1136
+ requestId: msg.requestId,
1137
+ result: {
1138
+ ok: true,
1139
+ data
1140
+ }
1141
+ };
1142
+ }
1143
+ if (body.kind === "runFacets") {
1144
+ const data = await this.gateway.runFacets({
1145
+ ...body.query,
1146
+ namespace: msg.tenant
1147
+ });
1148
+ return {
1149
+ requestId: msg.requestId,
1150
+ result: {
1151
+ ok: true,
1152
+ data
1153
+ }
1154
+ };
1155
+ }
1156
+ if (body.kind === "runValueFacets") {
1157
+ const data = await this.gateway.runValueFacets(body.axis, {
1158
+ ...body.query,
1159
+ namespace: msg.tenant
1160
+ }, body.opts);
1161
+ return {
1162
+ requestId: msg.requestId,
1163
+ result: {
1164
+ ok: true,
1165
+ data
1166
+ }
1167
+ };
1168
+ }
1169
+ if (body.kind === "workerHealth") {
1170
+ const all = await this.gateway.workerHealth();
1171
+ const data = all.filter((h) => h.group.endsWith(`@${msg.tenant}`));
1172
+ return {
1173
+ requestId: msg.requestId,
1174
+ result: {
1175
+ ok: true,
1176
+ data
1177
+ }
1178
+ };
1179
+ }
1180
+ if (body.kind === "waitingFor") {
1181
+ const all = await this.gateway.waitingFor(body.runIds);
1182
+ const owned = await Promise.all(Object.entries(all).map(async ([runId, waiting]) => {
1183
+ const owner = this.gateway.getRun ? await this.gateway.getRun(runId) : (await this.gateway.getRunDetail(runId))?.run;
1184
+ return owner && owner.namespace === msg.tenant ? [
1185
+ runId,
1186
+ waiting
1187
+ ] : void 0;
1188
+ }));
1189
+ const data = {};
1190
+ for (const entry of owned) {
1191
+ if (entry) data[entry[0]] = entry[1];
1192
+ }
1193
+ return {
1194
+ requestId: msg.requestId,
1195
+ result: {
1196
+ ok: true,
1197
+ data
1198
+ }
1199
+ };
1200
+ }
1201
+ const detail = body.kind === "getRunDetail" || !this.gateway.getRun ? await this.gateway.getRunDetail(body.runId, body.kind === "getRunDetail" ? body.options : void 0) : void 0;
1202
+ const run = detail !== void 0 ? detail?.run ?? null : await this.gateway.getRun?.(body.runId);
1203
+ if (run && run.namespace !== msg.tenant) {
1204
+ return {
1205
+ requestId: msg.requestId,
1206
+ result: {
1207
+ ok: false,
1208
+ error: {
1209
+ message: "run belongs to another tenant",
1210
+ code: "cross-tenant"
1211
+ }
1212
+ }
1213
+ };
1214
+ }
1215
+ if (body.kind === "getRunDetail") {
1216
+ return {
1217
+ requestId: msg.requestId,
1218
+ result: {
1219
+ ok: true,
1220
+ data: detail
1221
+ }
1222
+ };
1223
+ }
1224
+ if (body.kind === "getFanMembers") {
1225
+ const data = run ? await this.gateway.getFanMembers?.(body.runId, body.group, body.query) ?? null : null;
1226
+ return {
1227
+ requestId: msg.requestId,
1228
+ result: {
1229
+ ok: true,
1230
+ data
1231
+ }
1232
+ };
1233
+ }
1234
+ try {
1235
+ const data = await this.callVerb(body);
1236
+ return {
1237
+ requestId: msg.requestId,
1238
+ result: {
1239
+ ok: true,
1240
+ data
1241
+ }
1242
+ };
1243
+ } catch (err) {
1244
+ return {
1245
+ requestId: msg.requestId,
1246
+ result: {
1247
+ ok: false,
1248
+ error: {
1249
+ message: err instanceof Error ? err.message : String(err)
1250
+ }
1251
+ }
1252
+ };
1253
+ }
1254
+ }
1255
+ callVerb(body) {
1256
+ switch (body.kind) {
1257
+ case "cancel":
1258
+ return this.gateway.cancel(body.runId, body.opts);
1259
+ case "retry":
1260
+ return this.gateway.retry(body.runId);
1261
+ case "continue":
1262
+ return this.gateway.continue(body.runId);
1263
+ case "retryWithInput":
1264
+ return this.gateway.retryWithInput(body.runId, body.input);
1265
+ case "redispatch":
1266
+ return this.gateway.redispatchPending(body.runId);
1267
+ }
1268
+ }
1269
+ };
1270
+
1271
+ // src/proxy-run-gateway.ts
999
1272
  function _ts_decorate6(decorators, target, key, desc) {
1000
1273
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1001
1274
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -1016,6 +1289,15 @@ function _ts_metadata6(metadataKey, metadataValue) {
1016
1289
  }
1017
1290
  }
1018
1291
  __name(_ts_metadata6, "_ts_metadata");
1292
+ var UnsupportedRunRequestError = class UnsupportedRunRequestError2 extends Error {
1293
+ static {
1294
+ __name(this, "UnsupportedRunRequestError");
1295
+ }
1296
+ };
1297
+ function isUnsupported(err) {
1298
+ return err instanceof UnsupportedRunRequestError;
1299
+ }
1300
+ __name(isUnsupported, "isUnsupported");
1019
1301
  var ProxyRunGateway = class {
1020
1302
  static {
1021
1303
  __name(this, "ProxyRunGateway");
@@ -1023,11 +1305,18 @@ var ProxyRunGateway = class {
1023
1305
  transport;
1024
1306
  tenant;
1025
1307
  timeoutMs;
1308
+ capabilityTtlMs;
1026
1309
  pending = /* @__PURE__ */ new Map();
1027
- constructor(transport, tenant, timeoutMs = 1e4) {
1310
+ /** Verb → when to ask the control plane for it again, after it answered that it does not know it.
1311
+ * During a rolling upgrade the control plane is several pods of mixed versions behind one queue,
1312
+ * and a reply does not say which pod sent it, so the whole control plane is assumed old until the
1313
+ * entry expires. */
1314
+ unsupportedUntil = /* @__PURE__ */ new Map();
1315
+ constructor(transport, tenant, timeoutMs = 1e4, capabilityTtlMs = 6e4) {
1028
1316
  this.transport = transport;
1029
1317
  this.tenant = tenant;
1030
1318
  this.timeoutMs = timeoutMs;
1319
+ this.capabilityTtlMs = capabilityTtlMs;
1031
1320
  this.transport.onRunReply?.((reply) => this.handleReply(reply));
1032
1321
  }
1033
1322
  handleReply(reply) {
@@ -1037,6 +1326,8 @@ var ProxyRunGateway = class {
1037
1326
  this.pending.delete(reply.requestId);
1038
1327
  if (reply.result.ok) {
1039
1328
  pending.resolve(reply.result.data);
1329
+ } else if (reply.result.error.code === UNSUPPORTED_RUN_REQUEST) {
1330
+ pending.reject(new UnsupportedRunRequestError(reply.result.error.message));
1040
1331
  } else {
1041
1332
  pending.reject(new Error(reply.result.error.message));
1042
1333
  }
@@ -1072,20 +1363,59 @@ var ProxyRunGateway = class {
1072
1363
  tenant: this.tenant
1073
1364
  };
1074
1365
  }
1075
- getRunDetail(runId, options) {
1076
- return this.request({
1366
+ /**
1367
+ * A control plane that predates {@link RunDetailOptions} ignores them and sends the full detail;
1368
+ * a summarized detail always carries `fans`, so its absence means the summary is made here.
1369
+ */
1370
+ async getRunDetail(runId, options) {
1371
+ const detail = await this.request(options === void 0 ? {
1372
+ kind: "getRunDetail",
1373
+ runId
1374
+ } : {
1077
1375
  kind: "getRunDetail",
1078
1376
  runId,
1079
1377
  options
1080
1378
  });
1379
+ const over = options?.summarizeFansOver;
1380
+ if (over === void 0 || !detail || detail.fans !== void 0) return detail;
1381
+ const { timeline, fans } = summarizeTimeline(detail.timeline, over);
1382
+ const ids = childIdsOf(detail.timeline);
1383
+ return {
1384
+ ...detail,
1385
+ timeline,
1386
+ children: ids.slice(0, SUMMARY_CHILD_LIMIT),
1387
+ fans,
1388
+ childCount: ids.length
1389
+ };
1081
1390
  }
1082
- getFanMembers(runId, group, query) {
1083
- return this.request({
1084
- kind: "getFanMembers",
1085
- runId,
1086
- group,
1087
- query
1391
+ /** Paged from the full detail when the control plane is too old to page a fan itself. */
1392
+ async getFanMembers(runId, group, query) {
1393
+ if (!this.knownUnsupported("getFanMembers")) {
1394
+ try {
1395
+ const page = await this.request({
1396
+ kind: "getFanMembers",
1397
+ runId,
1398
+ group,
1399
+ query
1400
+ });
1401
+ if (page !== void 0) return page;
1402
+ } catch (err) {
1403
+ if (!isUnsupported(err)) throw err;
1404
+ }
1405
+ this.unsupportedUntil.set("getFanMembers", Date.now() + this.capabilityTtlMs);
1406
+ }
1407
+ const detail = await this.request({
1408
+ kind: "getRunDetail",
1409
+ runId
1088
1410
  });
1411
+ return detail ? pageFan(detail.timeline, group, query) : null;
1412
+ }
1413
+ knownUnsupported(kind) {
1414
+ const until = this.unsupportedUntil.get(kind);
1415
+ if (until === void 0) return false;
1416
+ if (Date.now() < until) return true;
1417
+ this.unsupportedUntil.delete(kind);
1418
+ return false;
1089
1419
  }
1090
1420
  /** The control plane's `StoreRunGateway` resolves each run's `waiting` descriptor; the reply carries
1091
1421
  * it through as plain JSON, so a tenant's list rows name the wait too. */
@@ -1175,6 +1505,7 @@ ProxyRunGateway = _ts_decorate6([
1175
1505
  _ts_metadata6("design:paramtypes", [
1176
1506
  typeof Transport === "undefined" ? Object : Transport,
1177
1507
  String,
1508
+ void 0,
1178
1509
  void 0
1179
1510
  ])
1180
1511
  ], ProxyRunGateway);
@@ -1372,170 +1703,8 @@ RetentionPoller = _ts_decorate7([
1372
1703
  ])
1373
1704
  ], RetentionPoller);
1374
1705
 
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
1706
  // 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";
1707
+ 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
1708
  import { Inject as Inject5, Injectable as Injectable8 } from "@nestjs/common";
1540
1709
  function _ts_decorate8(decorators, target, key, desc) {
1541
1710
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -1593,13 +1762,31 @@ var StoreRunGateway = class {
1593
1762
  children
1594
1763
  };
1595
1764
  }
1765
+ if (aggregatesCheckpoints(this.store)) {
1766
+ const stats = await this.store.checkpointStats(runId);
1767
+ const etag = runDetailEtag(run, stats, over);
1768
+ if (etagMatches(options?.ifNoneMatch, etag)) {
1769
+ return {
1770
+ run,
1771
+ timeline: [],
1772
+ children: [],
1773
+ notModified: true,
1774
+ etag
1775
+ };
1776
+ }
1777
+ return {
1778
+ run,
1779
+ ...await summarizeRunFromStore(this.store, runId, over, stats),
1780
+ etag
1781
+ };
1782
+ }
1596
1783
  const full = await this.store.listCheckpoints(runId);
1597
- const { timeline, fans } = summarizeTimeline(full, over);
1598
- const ids = childIdsOf(full);
1784
+ const { timeline, fans } = summarizeTimeline2(full, over);
1785
+ const ids = childIdsOf2(full);
1599
1786
  return {
1600
1787
  run,
1601
1788
  timeline,
1602
- children: ids.slice(0, SUMMARY_CHILD_LIMIT),
1789
+ children: ids.slice(0, SUMMARY_CHILD_LIMIT2),
1603
1790
  fans,
1604
1791
  childCount: ids.length
1605
1792
  };
@@ -1609,7 +1796,25 @@ var StoreRunGateway = class {
1609
1796
  }
1610
1797
  async getFanMembers(runId, group, query) {
1611
1798
  if (!await this.store.getRun(runId)) return null;
1612
- return pageFan(await this.store.listCheckpoints(runId), group, query);
1799
+ if (aggregatesCheckpoints(this.store)) {
1800
+ const stats = await this.store.checkpointStats(runId, {
1801
+ group
1802
+ });
1803
+ const etag = fanPageEtag(group, stats, query);
1804
+ if (etagMatches(query.ifNoneMatch, etag)) {
1805
+ return {
1806
+ items: [],
1807
+ total: 0,
1808
+ notModified: true,
1809
+ etag
1810
+ };
1811
+ }
1812
+ return {
1813
+ ...await pageFanFromStore(this.store, runId, group, query, stats),
1814
+ etag
1815
+ };
1816
+ }
1817
+ return pageFan2(await this.store.listCheckpoints(runId), group, query);
1613
1818
  }
1614
1819
  async listRuns(query) {
1615
1820
  const runs = await this.store.listRuns(query);
@@ -1900,7 +2105,7 @@ var CONTEXT_ACCESSOR = Symbol.for("@dudousxd/nestjs-context:accessor");
1900
2105
  var RUN_GATEWAY = RunGateway;
1901
2106
 
1902
2107
  // 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";
2108
+ 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
2109
  import { Inject as Inject7, Injectable as Injectable10 } from "@nestjs/common";
1905
2110
  import { DiscoveryService as DiscoveryService4, MetadataScanner as MetadataScanner4 } from "@nestjs/core";
1906
2111
 
@@ -1970,6 +2175,7 @@ function _ts_param7(paramIndex, decorator) {
1970
2175
  };
1971
2176
  }
1972
2177
  __name(_ts_param7, "_ts_param");
2178
+ var UNSERVED_ROUTES_RECHECK_MS = 3e4;
1973
2179
  var WorkflowRegistrar = class {
1974
2180
  static {
1975
2181
  __name(this, "WorkflowRegistrar");
@@ -2040,6 +2246,12 @@ var WorkflowRegistrar = class {
2040
2246
  windowMs: parseDuration2(meta.batch.within)
2041
2247
  } : void 0;
2042
2248
  const workflowCtor = workflow.constructor;
2249
+ const onError = onErrorHandlersOf(meta, workflow, this.metadataScanner);
2250
+ for (const [name, fn] of onError.handlers) {
2251
+ this.engine.register(name, meta.version, fn, {
2252
+ origin
2253
+ });
2254
+ }
2043
2255
  this.engine.register(meta.name, meta.version, (ctx, input) => workflow.run(ctx, input), {
2044
2256
  // Stamped onto every run this workflow starts (see `WorkflowRun.origin`). Derived from the
2045
2257
  // class's declaring file, so a lib gets attributed without opting in; `undefined` when it
@@ -2054,6 +2266,7 @@ var WorkflowRegistrar = class {
2054
2266
  searchAttributesSchema: meta.searchAttributes,
2055
2267
  onEvent: getOnEvents(meta, workflowCtor),
2056
2268
  eventBatch,
2269
+ onError: onError.routes,
2057
2270
  // Uniform dispatch (opt-in): when an in-app worker is configured, register the body GROUP-SERVED
2058
2271
  // so the engine dispatches its turns over the transport instead of running it inline; the
2059
2272
  // co-located worker consumer (Task 5: one queue PER REGISTERED NAME) replays the same body.
@@ -2083,10 +2296,11 @@ var WorkflowRegistrar = class {
2083
2296
  });
2084
2297
  deadLetterByWorkflow.set(meta.name, dlqName);
2085
2298
  } else if (meta.deadLetterWorkflow) {
2086
- deadLetterByWorkflow.set(meta.name, workflowName2(meta.deadLetterWorkflow));
2299
+ deadLetterByWorkflow.set(meta.name, workflowName3(meta.deadLetterWorkflow));
2087
2300
  }
2088
2301
  });
2089
2302
  this.reportUnattributed(unattributed);
2303
+ void this.reportUnservedOnErrorRoutes();
2090
2304
  this.installDeadLetterRouting(deadLetterByWorkflow);
2091
2305
  }
2092
2306
  /**
@@ -2099,6 +2313,20 @@ var WorkflowRegistrar = class {
2099
2313
  if (names.length === 0) return;
2100
2314
  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
2315
  }
2316
+ /**
2317
+ * Says, once after boot, which onError routes name a handler workflow nothing can run, so a typo is
2318
+ * caught before the first failure goes nowhere. Routes unserved at boot are checked again after
2319
+ * {@link UNSERVED_ROUTES_RECHECK_MS}, so a handler whose workers start after this process is not
2320
+ * reported.
2321
+ */
2322
+ async reportUnservedOnErrorRoutes(recheckMs = UNSERVED_ROUTES_RECHECK_MS) {
2323
+ if ((await this.engine.unservedOnErrorRoutes().catch(() => [])).length === 0) return;
2324
+ await new Promise((resolve) => setTimeout(resolve, recheckMs).unref());
2325
+ const unserved = await this.engine.unservedOnErrorRoutes().catch(() => []);
2326
+ if (unserved.length === 0) return;
2327
+ const list = unserved.map(({ workflow, route }) => `${workflow} \u2192 ${route.workflow} (code ${route.code ?? "*"})`).join(", ");
2328
+ console.warn(`[nestjs-durable] ${unserved.length} onError route(s) name a handler workflow that is not registered here and has no live worker: ${list}.`);
2329
+ }
2102
2330
  /** Returns the instance's `@DeadLetter()` method bound to the instance, or undefined if none. */
2103
2331
  findDeadLetterHandler(instance) {
2104
2332
  const prototype = Object.getPrototypeOf(instance);
@@ -2116,7 +2344,7 @@ var WorkflowRegistrar = class {
2116
2344
  * a `dlq:<runId>` id, so re-recovery never double-dispatches.
2117
2345
  */
2118
2346
  installDeadLetterRouting(byWorkflow) {
2119
- const fallback = this.options.deadLetterWorkflow ? workflowName2(this.options.deadLetterWorkflow) : void 0;
2347
+ const fallback = this.options.deadLetterWorkflow ? workflowName3(this.options.deadLetterWorkflow) : void 0;
2120
2348
  if (byWorkflow.size === 0 && !fallback) return;
2121
2349
  this.engine.onDead((run) => {
2122
2350
  const target = byWorkflow.get(run.workflow) ?? fallback;
@@ -2651,9 +2879,11 @@ export {
2651
2879
  IN_APP_WORKER_RUNNERS,
2652
2880
  IN_APP_WORKER_RUNTIME,
2653
2881
  InAppWorkerBootstrap,
2882
+ ON_ERROR_METADATA,
2654
2883
  ON_EVENT_METADATA,
2655
2884
  On,
2656
2885
  OnDurableEvent,
2886
+ OnError,
2657
2887
  OnEvent,
2658
2888
  ProxyRunGateway,
2659
2889
  RUN_GATEWAY,
@@ -2668,6 +2898,7 @@ export {
2668
2898
  ThinStepRegistrar,
2669
2899
  ThinWorkerBootstrap,
2670
2900
  ThinWorkflowRegistrar,
2901
+ UNSUPPORTED_RUN_REQUEST,
2671
2902
  WORKFLOW_METADATA,
2672
2903
  Workflow,
2673
2904
  WorkflowEngine7 as WorkflowEngine,
@@ -2683,6 +2914,7 @@ export {
2683
2914
  isDrivingOperator,
2684
2915
  isOperatorRole,
2685
2916
  isStepInterceptor,
2917
+ onErrorCodesOf,
2686
2918
  readSearchAttributes2 as readSearchAttributes,
2687
2919
  thinWorkerProviders,
2688
2920
  unavailableRunGateway