@pattern-stack/codegen 0.8.1 → 0.9.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
  3. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
  4. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +3 -0
  5. package/dist/runtime/subsystems/bridge/bridge.module.js +930 -275
  6. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  7. package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
  8. package/dist/runtime/subsystems/bridge/index.d.ts +3 -0
  9. package/dist/runtime/subsystems/bridge/index.js +837 -182
  10. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  11. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +3 -1
  12. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +92 -1
  13. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
  14. package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +3 -1
  15. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +99 -0
  16. package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
  17. package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
  18. package/dist/runtime/subsystems/events/event-keyset-cursor.d.ts +32 -0
  19. package/dist/runtime/subsystems/events/event-keyset-cursor.js +38 -0
  20. package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -0
  21. package/dist/runtime/subsystems/events/event-read.protocol.d.ts +94 -0
  22. package/dist/runtime/subsystems/events/event-read.protocol.js +9 -0
  23. package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -0
  24. package/dist/runtime/subsystems/events/events.module.js +177 -3
  25. package/dist/runtime/subsystems/events/events.module.js.map +1 -1
  26. package/dist/runtime/subsystems/events/events.tokens.d.ts +16 -1
  27. package/dist/runtime/subsystems/events/events.tokens.js +2 -0
  28. package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
  29. package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
  30. package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
  31. package/dist/runtime/subsystems/events/index.d.ts +2 -1
  32. package/dist/runtime/subsystems/events/index.js +178 -3
  33. package/dist/runtime/subsystems/events/index.js.map +1 -1
  34. package/dist/runtime/subsystems/index.d.ts +1 -0
  35. package/dist/runtime/subsystems/index.js +1194 -264
  36. package/dist/runtime/subsystems/index.js.map +1 -1
  37. package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +98 -0
  38. package/dist/runtime/subsystems/jobs/bullmq.config.js +143 -0
  39. package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -0
  40. package/dist/runtime/subsystems/jobs/index.d.ts +6 -2
  41. package/dist/runtime/subsystems/jobs/index.js +861 -201
  42. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  43. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +107 -0
  44. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +922 -0
  45. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -0
  46. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +52 -0
  47. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +57 -0
  48. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -0
  49. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +2 -1
  50. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +81 -1
  51. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
  52. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +2 -1
  53. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +81 -0
  54. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
  55. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +74 -1
  56. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +48 -0
  57. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +374 -0
  58. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -0
  59. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +42 -4
  60. package/dist/runtime/subsystems/jobs/job-worker.module.js +832 -178
  61. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  62. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +10 -1
  63. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +519 -20
  64. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  65. package/dist/runtime/subsystems/jobs/pool-config.loader.d.ts +9 -1
  66. package/dist/runtime/subsystems/jobs/pool-config.loader.js +4 -0
  67. package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
  68. package/dist/runtime/subsystems/observability/index.d.ts +4 -3
  69. package/dist/runtime/subsystems/observability/index.js +109 -2
  70. package/dist/runtime/subsystems/observability/index.js.map +1 -1
  71. package/dist/runtime/subsystems/observability/observability.module.js +109 -2
  72. package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
  73. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +63 -2
  74. package/dist/runtime/subsystems/observability/observability.service.d.ts +21 -3
  75. package/dist/runtime/subsystems/observability/observability.service.js +109 -2
  76. package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
  77. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -0
  78. package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -0
  79. package/dist/src/cli/index.js +30 -6
  80. package/dist/src/cli/index.js.map +1 -1
  81. package/package.json +1 -1
  82. package/runtime/subsystems/bridge/bridge.module.ts +5 -0
  83. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +109 -3
  84. package/runtime/subsystems/events/event-bus.memory-backend.ts +103 -1
  85. package/runtime/subsystems/events/event-keyset-cursor.ts +59 -0
  86. package/runtime/subsystems/events/event-read.protocol.ts +97 -0
  87. package/runtime/subsystems/events/events.module.ts +18 -2
  88. package/runtime/subsystems/events/events.tokens.ts +16 -0
  89. package/runtime/subsystems/events/index.ts +7 -0
  90. package/runtime/subsystems/jobs/bullmq.config.ts +125 -0
  91. package/runtime/subsystems/jobs/index.ts +22 -0
  92. package/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts +381 -0
  93. package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +88 -0
  94. package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +59 -1
  95. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +53 -0
  96. package/runtime/subsystems/jobs/job-run-service.protocol.ts +77 -0
  97. package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +311 -0
  98. package/runtime/subsystems/jobs/job-worker.module.ts +124 -10
  99. package/runtime/subsystems/jobs/jobs-domain.module.ts +40 -21
  100. package/runtime/subsystems/jobs/pool-config.loader.ts +11 -0
  101. package/runtime/subsystems/observability/index.ts +8 -0
  102. package/runtime/subsystems/observability/observability.protocol.ts +76 -0
  103. package/runtime/subsystems/observability/observability.service.ts +148 -1
  104. package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -12
  105. package/templates/relationship/new/prompt.js +8 -5
  106. package/templates/subsystem/jobs/worker.ejs.t +30 -7
  107. package/templates/subsystem/sync/sync-audit.schema.ejs.t +12 -16
@@ -14,6 +14,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
14
14
 
15
15
  // runtime/subsystems/events/events.tokens.ts
16
16
  var EVENT_BUS = "EVENT_BUS";
17
+ var EVENT_READ_PORT = "EVENT_READ_PORT";
17
18
  var TYPED_EVENT_BUS = "TYPED_EVENT_BUS";
18
19
  var EVENTS_MULTI_TENANT = "EVENTS_MULTI_TENANT";
19
20
  var REDIS_URL = /* @__PURE__ */ Symbol("REDIS_URL");
@@ -247,7 +248,38 @@ var DRIZZLE = "DRIZZLE";
247
248
 
248
249
  // runtime/subsystems/events/event-bus.drizzle-backend.ts
249
250
  import { Injectable as Injectable2, Inject as Inject2, Logger, Optional } from "@nestjs/common";
250
- import { eq, and, inArray, asc } from "drizzle-orm";
251
+ import { eq, and, inArray, asc, desc, gte, lt, or, sql as sql2 } from "drizzle-orm";
252
+
253
+ // runtime/subsystems/events/event-keyset-cursor.ts
254
+ var DEFAULT_EVENT_LIST_LIMIT = 50;
255
+ var MAX_EVENT_LIST_LIMIT = 200;
256
+ function clampEventLimit(limit) {
257
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
258
+ return DEFAULT_EVENT_LIST_LIMIT;
259
+ }
260
+ const floored = Math.floor(limit);
261
+ if (floored < 1) return 1;
262
+ if (floored > MAX_EVENT_LIST_LIMIT) return MAX_EVENT_LIST_LIMIT;
263
+ return floored;
264
+ }
265
+ function encodeEventCursor(keyset) {
266
+ const tuple = [keyset.occurredAt.toISOString(), keyset.id];
267
+ return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
268
+ }
269
+ function decodeEventCursor(cursor) {
270
+ try {
271
+ const json = Buffer.from(cursor, "base64url").toString("utf8");
272
+ const parsed = JSON.parse(json);
273
+ if (!Array.isArray(parsed) || parsed.length !== 2) return null;
274
+ const [iso, id] = parsed;
275
+ if (typeof iso !== "string" || typeof id !== "string") return null;
276
+ const occurredAt = new Date(iso);
277
+ if (Number.isNaN(occurredAt.getTime())) return null;
278
+ return { occurredAt, id };
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
251
283
 
252
284
  // runtime/subsystems/events/domain-events.schema.ts
253
285
  import {
@@ -352,6 +384,24 @@ function toInsertValues(event) {
352
384
  tenantId
353
385
  };
354
386
  }
387
+ function toEventSummary(r) {
388
+ const metadata = r.metadata ?? void 0;
389
+ const rootRunId = metadata?.["rootRunId"];
390
+ return {
391
+ id: r.id,
392
+ type: r.type,
393
+ aggregateId: r.aggregateId,
394
+ aggregateType: r.aggregateType,
395
+ status: r.status,
396
+ pool: r.pool,
397
+ direction: r.direction,
398
+ tier: r.tier,
399
+ rootRunId: typeof rootRunId === "string" ? rootRunId : null,
400
+ tenantId: r.tenantId,
401
+ occurredAt: r.occurredAt instanceof Date ? r.occurredAt : new Date(r.occurredAt),
402
+ processedAt: r.processedAt == null ? null : r.processedAt instanceof Date ? r.processedAt : new Date(r.processedAt)
403
+ };
404
+ }
355
405
  var DrizzleEventBus = class {
356
406
  constructor(db, opts, bridgeHook = null) {
357
407
  this.db = db;
@@ -417,6 +467,48 @@ var DrizzleEventBus = class {
417
467
  };
418
468
  }
419
469
  // ============================================================================
470
+ // IEventReadPort (OBS-LIST-1)
471
+ // ============================================================================
472
+ async listEvents(query = {}) {
473
+ const limit = clampEventLimit(query.limit);
474
+ const conditions = [];
475
+ if (query.poolId) conditions.push(eq(domainEvents.pool, query.poolId));
476
+ if (query.direction)
477
+ conditions.push(eq(domainEvents.direction, query.direction));
478
+ if (query.since) conditions.push(gte(domainEvents.occurredAt, query.since));
479
+ if (query.rootRunId) {
480
+ conditions.push(
481
+ sql2`${domainEvents.metadata}->>'rootRunId' = ${query.rootRunId}`
482
+ );
483
+ }
484
+ if (query.tenantId !== void 0) {
485
+ conditions.push(
486
+ query.tenantId === null ? sql2`${domainEvents.tenantId} is null` : eq(domainEvents.tenantId, query.tenantId)
487
+ );
488
+ }
489
+ if (query.cursor) {
490
+ const keyset = decodeEventCursor(query.cursor);
491
+ if (keyset) {
492
+ conditions.push(
493
+ or(
494
+ lt(domainEvents.occurredAt, keyset.occurredAt),
495
+ and(
496
+ eq(domainEvents.occurredAt, keyset.occurredAt),
497
+ lt(domainEvents.id, keyset.id)
498
+ )
499
+ )
500
+ );
501
+ }
502
+ }
503
+ const rows = await this.db.select().from(domainEvents).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(desc(domainEvents.occurredAt), desc(domainEvents.id)).limit(limit + 1);
504
+ const hasMore = rows.length > limit;
505
+ const page = hasMore ? rows.slice(0, limit) : rows;
506
+ const items = page.map(toEventSummary);
507
+ const last = page[page.length - 1];
508
+ const nextCursor = hasMore && last ? encodeEventCursor({ occurredAt: last.occurredAt, id: last.id }) : null;
509
+ return { items, nextCursor };
510
+ }
511
+ // ============================================================================
420
512
  // Polling
421
513
  // ============================================================================
422
514
  /**
@@ -541,6 +633,27 @@ DrizzleEventBus = __decorateClass([
541
633
 
542
634
  // runtime/subsystems/events/event-bus.memory-backend.ts
543
635
  import { Inject as Inject3, Injectable as Injectable3, Logger as Logger2, Optional as Optional2 } from "@nestjs/common";
636
+ function toEventSummary2(event) {
637
+ const metadata = event.metadata;
638
+ const str = (key) => {
639
+ const v = metadata?.[key];
640
+ return typeof v === "string" ? v : null;
641
+ };
642
+ return {
643
+ id: event.id,
644
+ type: event.type,
645
+ aggregateId: event.aggregateId,
646
+ aggregateType: event.aggregateType,
647
+ status: "processed",
648
+ pool: str("pool"),
649
+ direction: str("direction"),
650
+ tier: str("tier") ?? "domain",
651
+ rootRunId: str("rootRunId"),
652
+ tenantId: str("tenantId"),
653
+ occurredAt: event.occurredAt,
654
+ processedAt: event.occurredAt
655
+ };
656
+ }
544
657
  var MemoryEventBus = class {
545
658
  logger = new Logger2(MemoryEventBus.name);
546
659
  /** All events published since construction (or last clear). */
@@ -576,6 +689,53 @@ var MemoryEventBus = class {
576
689
  set.delete(h);
577
690
  };
578
691
  }
692
+ // ============================================================================
693
+ // IEventReadPort (OBS-LIST-1)
694
+ // ============================================================================
695
+ async listEvents(query = {}) {
696
+ const limit = clampEventLimit(query.limit);
697
+ const keyset = query.cursor ? decodeEventCursor(query.cursor) : null;
698
+ const str = (e, key) => {
699
+ const v = e.metadata?.[key];
700
+ return typeof v === "string" ? v : null;
701
+ };
702
+ const matched = this.publishedEvents.filter((e) => {
703
+ if (query.poolId && str(e, "pool") !== query.poolId) return false;
704
+ if (query.direction && str(e, "direction") !== query.direction)
705
+ return false;
706
+ if (query.rootRunId && str(e, "rootRunId") !== query.rootRunId)
707
+ return false;
708
+ if (query.since && e.occurredAt.getTime() < query.since.getTime())
709
+ return false;
710
+ if (query.tenantId !== void 0) {
711
+ const t = str(e, "tenantId");
712
+ if (query.tenantId === null) {
713
+ if (t !== null) return false;
714
+ } else if (t !== query.tenantId) {
715
+ return false;
716
+ }
717
+ }
718
+ return true;
719
+ });
720
+ matched.sort((a, b) => {
721
+ const dt = b.occurredAt.getTime() - a.occurredAt.getTime();
722
+ if (dt !== 0) return dt;
723
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
724
+ });
725
+ const seeked = keyset ? matched.filter((e) => {
726
+ const ct = e.occurredAt.getTime();
727
+ const kt = keyset.occurredAt.getTime();
728
+ if (ct < kt) return true;
729
+ if (ct > kt) return false;
730
+ return e.id < keyset.id;
731
+ }) : matched;
732
+ const hasMore = seeked.length > limit;
733
+ const page = hasMore ? seeked.slice(0, limit) : seeked;
734
+ const items = page.map(toEventSummary2);
735
+ const last = page[page.length - 1];
736
+ const nextCursor = hasMore && last ? encodeEventCursor({ occurredAt: last.occurredAt, id: last.id }) : null;
737
+ return { items, nextCursor };
738
+ }
579
739
  /** Remove all published events and subscriptions. Useful in beforeEach. */
580
740
  clear() {
581
741
  this.publishedEvents.length = 0;
@@ -933,10 +1093,20 @@ var EventsModule = class {
933
1093
  REDIS_URL
934
1094
  ]
935
1095
  },
1096
+ {
1097
+ // Read port (OBS-LIST-1). Drizzle + memory backends implement
1098
+ // IEventReadPort on the EVENT_BUS instance; the redis backend
1099
+ // retains no history, so EVENT_READ_PORT resolves to `null` and
1100
+ // optional consumers (the observability combiner) degrade to
1101
+ // empty results.
1102
+ provide: EVENT_READ_PORT,
1103
+ useFactory: (options, bus) => options.backend === "redis" ? null : bus,
1104
+ inject: [EVENTS_MODULE_OPTIONS, EVENT_BUS]
1105
+ },
936
1106
  TypedEventBus,
937
1107
  { provide: TYPED_EVENT_BUS, useExisting: TypedEventBus }
938
1108
  ],
939
- exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
1109
+ exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
940
1110
  };
941
1111
  }
942
1112
  static forRoot(options = { backend: "drizzle" }) {
@@ -964,9 +1134,13 @@ var EventsModule = class {
964
1134
  providers: [
965
1135
  { provide: EVENTS_MODULE_OPTIONS, useValue: options },
966
1136
  provider,
1137
+ // Read port (OBS-LIST-1): drizzle + memory backends implement
1138
+ // IEventReadPort on the same instance as EVENT_BUS. The redis
1139
+ // backend retains no history and does not provide this token.
1140
+ { provide: EVENT_READ_PORT, useExisting: EVENT_BUS },
967
1141
  ...buildTypedBusProviders(multiTenant)
968
1142
  ],
969
- exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
1143
+ exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
970
1144
  };
971
1145
  }
972
1146
  };
@@ -986,7 +1160,7 @@ import {
986
1160
  index as index2,
987
1161
  uniqueIndex
988
1162
  } from "drizzle-orm/pg-core";
989
- import { sql as sql2 } from "drizzle-orm";
1163
+ import { sql as sql3 } from "drizzle-orm";
990
1164
  var jobRunStatusEnum = pgEnum("job_run_status", [
991
1165
  "pending",
992
1166
  "running",
@@ -1091,10 +1265,10 @@ var jobRuns = pgTable2(
1091
1265
  /** listForScope query. */
1092
1266
  idxJobRunScope: index2("idx_job_run_scope").on(t.scopeEntityType, t.scopeEntityId),
1093
1267
  /** Idempotency collapse — partial index. */
1094
- idxJobRunDedupe: index2("idx_job_run_dedupe").on(t.jobType, t.dedupeKey).where(sql2`${t.dedupeKey} IS NOT NULL`),
1268
+ idxJobRunDedupe: index2("idx_job_run_dedupe").on(t.jobType, t.dedupeKey).where(sql3`${t.dedupeKey} IS NOT NULL`),
1095
1269
  /** Collision check — partial index. */
1096
1270
  idxJobRunConcurrency: index2("idx_job_run_concurrency").on(t.concurrencyKey).where(
1097
- sql2`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`
1271
+ sql3`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`
1098
1272
  )
1099
1273
  })
1100
1274
  );
@@ -1151,7 +1325,7 @@ var HandlerRegistry;
1151
1325
  // runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts
1152
1326
  import { randomUUID as randomUUID2 } from "crypto";
1153
1327
  import { Inject as Inject5, Injectable as Injectable5, Logger as Logger4 } from "@nestjs/common";
1154
- import { and as and2, desc, eq as eq2, gt, inArray as inArray2, isNotNull, ne, notInArray, sql as sql3 } from "drizzle-orm";
1328
+ import { and as and2, desc as desc2, eq as eq2, gt, inArray as inArray2, isNotNull, ne, notInArray, sql as sql4 } from "drizzle-orm";
1155
1329
 
1156
1330
  // runtime/subsystems/jobs/jobs-errors.ts
1157
1331
  var JobTypeNotFoundError = class extends Error {
@@ -1292,7 +1466,7 @@ var DrizzleJobOrchestrator = class {
1292
1466
  // status NOT IN ('canceled', 'failed')
1293
1467
  notInStatus(DEDUPE_EXCLUDED_STATUSES)
1294
1468
  )
1295
- ).orderBy(desc(jobRuns.createdAt)).limit(1);
1469
+ ).orderBy(desc2(jobRuns.createdAt)).limit(1);
1296
1470
  if (existing.length > 0) {
1297
1471
  return existing[0];
1298
1472
  }
@@ -1510,24 +1684,24 @@ var DrizzleJobOrchestrator = class {
1510
1684
  }).onConflictDoUpdate({
1511
1685
  target: jobs.type,
1512
1686
  set: {
1513
- pool: sql3`EXCLUDED.pool`,
1514
- scopeEntityType: sql3`EXCLUDED.scope_entity_type`,
1515
- retryPolicy: sql3`EXCLUDED.retry_policy`,
1516
- timeoutMs: sql3`EXCLUDED.timeout_ms`,
1517
- concurrencyKeyTemplate: sql3`EXCLUDED.concurrency_key_template`,
1518
- collisionMode: sql3`EXCLUDED.collision_mode`,
1519
- dedupeKeyTemplate: sql3`EXCLUDED.dedupe_key_template`,
1520
- dedupeWindowMs: sql3`EXCLUDED.dedupe_window_ms`,
1521
- priorityDefault: sql3`EXCLUDED.priority_default`,
1522
- replayFrom: sql3`EXCLUDED.replay_from`,
1523
- version: sql3`${jobs.version} + 1`,
1524
- updatedAt: sql3`now()`
1687
+ pool: sql4`EXCLUDED.pool`,
1688
+ scopeEntityType: sql4`EXCLUDED.scope_entity_type`,
1689
+ retryPolicy: sql4`EXCLUDED.retry_policy`,
1690
+ timeoutMs: sql4`EXCLUDED.timeout_ms`,
1691
+ concurrencyKeyTemplate: sql4`EXCLUDED.concurrency_key_template`,
1692
+ collisionMode: sql4`EXCLUDED.collision_mode`,
1693
+ dedupeKeyTemplate: sql4`EXCLUDED.dedupe_key_template`,
1694
+ dedupeWindowMs: sql4`EXCLUDED.dedupe_window_ms`,
1695
+ priorityDefault: sql4`EXCLUDED.priority_default`,
1696
+ replayFrom: sql4`EXCLUDED.replay_from`,
1697
+ version: sql4`${jobs.version} + 1`,
1698
+ updatedAt: sql4`now()`
1525
1699
  },
1526
1700
  // The hash gate: every field listed in the Q3 resolution appears
1527
1701
  // here. `IS DISTINCT FROM` is the null-safe inequality operator;
1528
1702
  // jsonb cast to text gives stable comparison without invoking a
1529
1703
  // dedicated hash column (avoids a JOB-1 schema migration).
1530
- setWhere: sql3`
1704
+ setWhere: sql4`
1531
1705
  ${jobs.pool} IS DISTINCT FROM EXCLUDED.pool OR
1532
1706
  ${jobs.retryPolicy}::text IS DISTINCT FROM EXCLUDED.retry_policy::text OR
1533
1707
  ${jobs.timeoutMs} IS DISTINCT FROM EXCLUDED.timeout_ms OR
@@ -1558,7 +1732,58 @@ function notInStatus(statuses) {
1558
1732
 
1559
1733
  // runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
1560
1734
  import { Inject as Inject6, Injectable as Injectable6 } from "@nestjs/common";
1561
- import { and as and3, asc as asc2, desc as desc2, eq as eq3, inArray as inArray3, isNull, sql as sql4 } from "drizzle-orm";
1735
+ import { and as and3, asc as asc2, desc as desc3, eq as eq3, gte as gte2, inArray as inArray3, isNull, lt as lt2, or as or2, sql as sql5 } from "drizzle-orm";
1736
+
1737
+ // runtime/subsystems/jobs/job-run-keyset-cursor.ts
1738
+ var DEFAULT_LIST_LIMIT = 50;
1739
+ var MAX_LIST_LIMIT = 200;
1740
+ function clampLimit(limit) {
1741
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
1742
+ return DEFAULT_LIST_LIMIT;
1743
+ }
1744
+ const floored = Math.floor(limit);
1745
+ if (floored < 1) return 1;
1746
+ if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
1747
+ return floored;
1748
+ }
1749
+ function encodeKeysetCursor(keyset) {
1750
+ const tuple = [keyset.createdAt.toISOString(), keyset.id];
1751
+ return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
1752
+ }
1753
+ function decodeKeysetCursor(cursor) {
1754
+ try {
1755
+ const json = Buffer.from(cursor, "base64url").toString("utf8");
1756
+ const parsed = JSON.parse(json);
1757
+ if (!Array.isArray(parsed) || parsed.length !== 2) return null;
1758
+ const [iso, id] = parsed;
1759
+ if (typeof iso !== "string" || typeof id !== "string") return null;
1760
+ const createdAt = new Date(iso);
1761
+ if (Number.isNaN(createdAt.getTime())) return null;
1762
+ return { createdAt, id };
1763
+ } catch {
1764
+ return null;
1765
+ }
1766
+ }
1767
+ function toJobRunSummary(r) {
1768
+ return {
1769
+ runId: r.id,
1770
+ rootRunId: r.rootRunId,
1771
+ jobType: r.jobType,
1772
+ pool: r.pool,
1773
+ status: r.status,
1774
+ scopeEntityType: r.scopeEntityType,
1775
+ scopeEntityId: r.scopeEntityId,
1776
+ tenantId: r.tenantId,
1777
+ attempts: r.attempts,
1778
+ errorMessage: r.error?.message ?? null,
1779
+ runAt: r.runAt,
1780
+ startedAt: r.startedAt,
1781
+ finishedAt: r.finishedAt,
1782
+ createdAt: r.createdAt
1783
+ };
1784
+ }
1785
+
1786
+ // runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
1562
1787
  var NON_TERMINAL_STATUSES = [
1563
1788
  "pending",
1564
1789
  "running",
@@ -1606,12 +1831,12 @@ var DrizzleJobRunService = class {
1606
1831
  case "created_at asc":
1607
1832
  return asc2(jobRuns.createdAt);
1608
1833
  case "run_at desc":
1609
- return desc2(jobRuns.runAt);
1834
+ return desc3(jobRuns.runAt);
1610
1835
  case "run_at asc":
1611
1836
  return asc2(jobRuns.runAt);
1612
1837
  case "created_at desc":
1613
1838
  default:
1614
- return desc2(jobRuns.createdAt);
1839
+ return desc3(jobRuns.createdAt);
1615
1840
  }
1616
1841
  })();
1617
1842
  let q = this.db.select().from(jobRuns).where(and3(...conditions)).orderBy(orderCol).$dynamic();
@@ -1655,7 +1880,7 @@ var DrizzleJobRunService = class {
1655
1880
  const rows = await this.db.select({
1656
1881
  pool: jobRuns.pool,
1657
1882
  status: jobRuns.status,
1658
- count: sql4`count(*)::int`.as("count")
1883
+ count: sql5`count(*)::int`.as("count")
1659
1884
  }).from(jobRuns).where(tenantCond ?? void 0).groupBy(jobRuns.pool, jobRuns.status);
1660
1885
  return rows.map((r) => ({
1661
1886
  pool: r.pool,
@@ -1667,7 +1892,7 @@ var DrizzleJobRunService = class {
1667
1892
  const conditions = [eq3(jobRuns.status, "failed")];
1668
1893
  const tenantCond = this.tenantCondition("listRecentFailed", tenantId);
1669
1894
  if (tenantCond) conditions.push(tenantCond);
1670
- const rows = await this.db.select().from(jobRuns).where(and3(...conditions)).orderBy(desc2(jobRuns.finishedAt), desc2(jobRuns.updatedAt)).limit(limit);
1895
+ const rows = await this.db.select().from(jobRuns).where(and3(...conditions)).orderBy(desc3(jobRuns.finishedAt), desc3(jobRuns.updatedAt)).limit(limit);
1671
1896
  return rows.map((r) => ({
1672
1897
  runId: r.id,
1673
1898
  jobType: r.jobType,
@@ -1681,6 +1906,37 @@ var DrizzleJobRunService = class {
1681
1906
  createdAt: r.createdAt
1682
1907
  }));
1683
1908
  }
1909
+ async listJobRuns(query = {}) {
1910
+ const limit = clampLimit(query.limit);
1911
+ const conditions = [];
1912
+ const tenantCond = this.tenantCondition("listJobRuns", query.tenantId);
1913
+ if (tenantCond) conditions.push(tenantCond);
1914
+ if (query.poolId) conditions.push(eq3(jobRuns.pool, query.poolId));
1915
+ if (query.rootRunId) conditions.push(eq3(jobRuns.rootRunId, query.rootRunId));
1916
+ if (query.status) conditions.push(eq3(jobRuns.status, query.status));
1917
+ if (query.since) conditions.push(gte2(jobRuns.createdAt, query.since));
1918
+ if (query.cursor) {
1919
+ const keyset = decodeKeysetCursor(query.cursor);
1920
+ if (keyset) {
1921
+ conditions.push(
1922
+ or2(
1923
+ lt2(jobRuns.createdAt, keyset.createdAt),
1924
+ and3(
1925
+ eq3(jobRuns.createdAt, keyset.createdAt),
1926
+ lt2(jobRuns.id, keyset.id)
1927
+ )
1928
+ )
1929
+ );
1930
+ }
1931
+ }
1932
+ const rows = await this.db.select().from(jobRuns).where(conditions.length > 0 ? and3(...conditions) : void 0).orderBy(desc3(jobRuns.createdAt), desc3(jobRuns.id)).limit(limit + 1);
1933
+ const hasMore = rows.length > limit;
1934
+ const page = hasMore ? rows.slice(0, limit) : rows;
1935
+ const items = page.map(toJobRunSummary);
1936
+ const last = page[page.length - 1];
1937
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
1938
+ return { items, nextCursor };
1939
+ }
1684
1940
  /**
1685
1941
  * Internal helper used by cascade paths (not on the public protocol).
1686
1942
  * Exposed as a public method on the concrete class so infrastructure
@@ -1729,22 +1985,608 @@ var DrizzleJobStepService = class {
1729
1985
  finishedAt: values.finishedAt,
1730
1986
  attempts: values.attempts
1731
1987
  }
1732
- }).returning();
1733
- return row;
1988
+ }).returning();
1989
+ return row;
1990
+ }
1991
+ async findStep(runId, stepId) {
1992
+ const [row] = await this.db.select().from(jobSteps).where(and4(eq4(jobSteps.jobRunId, runId), eq4(jobSteps.stepId, stepId))).limit(1);
1993
+ return row ?? null;
1994
+ }
1995
+ };
1996
+ DrizzleJobStepService = __decorateClass([
1997
+ Injectable7(),
1998
+ __decorateParam(0, Inject7(DRIZZLE))
1999
+ ], DrizzleJobStepService);
2000
+
2001
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
2002
+ import { createHash } from "crypto";
2003
+ import { Inject as Inject8, Injectable as Injectable8, Logger as Logger5, Optional as Optional3 } from "@nestjs/common";
2004
+ import { eq as eq5 } from "drizzle-orm";
2005
+
2006
+ // runtime/subsystems/jobs/pool-config.loader.ts
2007
+ import { existsSync, readFileSync } from "fs";
2008
+ import { resolve } from "path";
2009
+ import { parse as parseYaml } from "yaml";
2010
+ var FRAMEWORK_POOLS = Object.freeze({
2011
+ events_inbound: Object.freeze({
2012
+ queue: "jobs-events-inbound",
2013
+ concurrency: 20,
2014
+ reserved: true,
2015
+ description: "Inbound events drain (events subsystem outbox)."
2016
+ }),
2017
+ events_change: Object.freeze({
2018
+ queue: "jobs-events-change",
2019
+ concurrency: 30,
2020
+ reserved: true,
2021
+ description: "Change events drain (events subsystem outbox)."
2022
+ }),
2023
+ events_outbound: Object.freeze({
2024
+ queue: "jobs-events-outbound",
2025
+ concurrency: 10,
2026
+ reserved: true,
2027
+ description: "Outbound events drain (events subsystem outbox)."
2028
+ }),
2029
+ interactive: Object.freeze({
2030
+ queue: "jobs-interactive",
2031
+ concurrency: 20,
2032
+ reserved: false,
2033
+ description: "User-facing latency-sensitive jobs."
2034
+ }),
2035
+ batch: Object.freeze({
2036
+ queue: "jobs-batch",
2037
+ concurrency: 5,
2038
+ reserved: false,
2039
+ description: "Default pool for background jobs."
2040
+ })
2041
+ });
2042
+ var RESERVED_POOL_NAMES = new Set(
2043
+ Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
2044
+ );
2045
+ var cache = /* @__PURE__ */ new Map();
2046
+ function loadPoolConfig(configPath) {
2047
+ const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
2048
+ const cached = cache.get(resolved);
2049
+ if (cached) return cached;
2050
+ const merged = /* @__PURE__ */ new Map();
2051
+ for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
2052
+ merged.set(name, { ...def });
2053
+ }
2054
+ if (!existsSync(resolved)) {
2055
+ cache.set(resolved, merged);
2056
+ return merged;
2057
+ }
2058
+ let raw;
2059
+ try {
2060
+ raw = parseYaml(readFileSync(resolved, "utf8"));
2061
+ } catch (err) {
2062
+ throw new Error(
2063
+ `pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
2064
+ );
2065
+ }
2066
+ const userPools = extractUserPools(raw);
2067
+ for (const [name, userDef] of Object.entries(userPools)) {
2068
+ const existing = merged.get(name);
2069
+ if (existing) {
2070
+ const next = {
2071
+ queue: existing.queue,
2072
+ concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
2073
+ reserved: existing.reserved,
2074
+ description: userDef.description ?? existing.description
2075
+ };
2076
+ merged.set(name, next);
2077
+ continue;
2078
+ }
2079
+ if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
2080
+ throw new Error(
2081
+ `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
2082
+ );
2083
+ }
2084
+ if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
2085
+ throw new Error(
2086
+ `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
2087
+ );
2088
+ }
2089
+ if (userDef.reserved === true) {
2090
+ throw new Error(
2091
+ `pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
2092
+ );
2093
+ }
2094
+ merged.set(name, {
2095
+ queue: userDef.queue,
2096
+ concurrency: userDef.concurrency,
2097
+ reserved: false,
2098
+ description: userDef.description
2099
+ });
2100
+ }
2101
+ cache.set(resolved, merged);
2102
+ return merged;
2103
+ }
2104
+ function allNonReservedPoolNames(config) {
2105
+ const out = [];
2106
+ for (const [name, def] of config) {
2107
+ if (!def.reserved) out.push(name);
2108
+ }
2109
+ return out;
2110
+ }
2111
+ function allPoolNames(config) {
2112
+ return [...config.keys()];
2113
+ }
2114
+ function extractUserPools(raw) {
2115
+ if (!raw || typeof raw !== "object") return {};
2116
+ const jobs2 = raw.jobs;
2117
+ if (!jobs2 || typeof jobs2 !== "object") return {};
2118
+ const pools = jobs2.pools;
2119
+ if (!pools || typeof pools !== "object") return {};
2120
+ const out = {};
2121
+ for (const [name, def] of Object.entries(pools)) {
2122
+ if (!def || typeof def !== "object") continue;
2123
+ out[name] = def;
2124
+ }
2125
+ return out;
2126
+ }
2127
+
2128
+ // runtime/subsystems/jobs/bullmq.config.ts
2129
+ var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
2130
+ var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
2131
+ var DEFAULT_REDIS_URL = "redis://localhost:6379";
2132
+ var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
2133
+ function resolveBullMqConfig(ext) {
2134
+ const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
2135
+ const resolved = {
2136
+ connection: { url },
2137
+ queuePrefix: ext?.queue_prefix
2138
+ };
2139
+ if (ext?.bull_board?.enabled) {
2140
+ resolved.bullBoard = {
2141
+ enabled: true,
2142
+ mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
2143
+ };
2144
+ }
2145
+ return resolved;
2146
+ }
2147
+ function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
2148
+ const alias = poolConfig.get(pool)?.queue ?? pool;
2149
+ const prefix = config?.queuePrefix;
2150
+ return prefix ? `${prefix}:${alias}` : alias;
2151
+ }
2152
+
2153
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
2154
+ function sha1JobId(idempotencyKey) {
2155
+ return createHash("sha1").update(idempotencyKey).digest("hex");
2156
+ }
2157
+ var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
2158
+ constructor(db, multiTenant, connection, bullConfig = null) {
2159
+ super(db, multiTenant);
2160
+ this.connection = connection;
2161
+ this.bullConfig = bullConfig;
2162
+ this.bullDb = db;
2163
+ }
2164
+ connection;
2165
+ bullConfig;
2166
+ // TODO(logging-subsystem): swap to ILogger once ADR-028 lands
2167
+ bullLogger = new Logger5(BullMQJobOrchestrator.name);
2168
+ /** Lazily-opened `Queue` handles, one per pool. */
2169
+ queues = /* @__PURE__ */ new Map();
2170
+ /** Single FlowProducer for parent/child hierarchies. Lazily opened. */
2171
+ _flow = null;
2172
+ /**
2173
+ * Cached `bullmq` value constructors, populated by `loadBullMq()` on first
2174
+ * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
2175
+ * a queue). Kept off the import graph so a `drizzle`-only consumer never
2176
+ * resolves the optional `'bullmq'` package.
2177
+ */
2178
+ QueueCtor = null;
2179
+ FlowProducerCtor = null;
2180
+ bullMqLoad = null;
2181
+ /**
2182
+ * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
2183
+ * `private` (can't be redeclared even privately in a subclass), and the
2184
+ * spec forbids touching that file — so the subclass keeps its own handle
2185
+ * under a distinct name (same instance, passed through to `super`) for the
2186
+ * cancel-cascade snapshot + definition/run loads below.
2187
+ */
2188
+ bullDb;
2189
+ /**
2190
+ * Lazily load the optional `bullmq` package and cache its value
2191
+ * constructors. Idempotent (single in-flight promise). Throws a friendly,
2192
+ * actionable error when the consumer selected `backend: 'bullmq'` but did
2193
+ * not install the package — mirrors `createRedisClient` in the redis event
2194
+ * backend. Must be `await`ed before any `queueFor`/`flow` access.
2195
+ */
2196
+ async loadBullMq() {
2197
+ if (this.QueueCtor && this.FlowProducerCtor) return;
2198
+ if (!this.bullMqLoad) {
2199
+ this.bullMqLoad = (async () => {
2200
+ try {
2201
+ const mod = await import("bullmq");
2202
+ this.QueueCtor = mod.Queue;
2203
+ this.FlowProducerCtor = mod.FlowProducer;
2204
+ } catch {
2205
+ throw new Error(
2206
+ 'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
2207
+ );
2208
+ }
2209
+ })();
2210
+ }
2211
+ await this.bullMqLoad;
2212
+ }
2213
+ /**
2214
+ * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
2215
+ * loadBullMq()` first so `QueueCtor` is populated.
2216
+ */
2217
+ queueFor(pool) {
2218
+ if (!this.QueueCtor) {
2219
+ throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
2220
+ }
2221
+ const name = resolvePoolQueueName(pool, this.bullConfig);
2222
+ let q = this.queues.get(name);
2223
+ if (!q) {
2224
+ q = new this.QueueCtor(name, { connection: this.connection });
2225
+ this.queues.set(name, q);
2226
+ }
2227
+ return q;
2228
+ }
2229
+ flow() {
2230
+ if (!this.FlowProducerCtor) {
2231
+ throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
2232
+ }
2233
+ if (!this._flow) {
2234
+ this._flow = new this.FlowProducerCtor({ connection: this.connection });
2235
+ }
2236
+ return this._flow;
2237
+ }
2238
+ // ==========================================================================
2239
+ // start — Postgres insert (super) + BullMQ dispatch
2240
+ // ==========================================================================
2241
+ async start(type, input, opts = {}, tx) {
2242
+ const run = await super.start(type, input, opts, tx);
2243
+ await this.dispatch(run, type);
2244
+ return run;
2245
+ }
2246
+ /**
2247
+ * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
2248
+ * `parentRunId` we attach it to the parent's existing BullMQ job through the
2249
+ * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
2250
+ * its own graph. (The FlowProducer is reserved for whole-tree atomic
2251
+ * submits, exposed as an opt-in extension via `flowProducer()`; runtime
2252
+ * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
2253
+ * correct primitive here.)
2254
+ *
2255
+ * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
2256
+ * present (so the same logical key dedups), else the `job_run.id` UUID
2257
+ * (already colon-free).
2258
+ *
2259
+ * The domain `parentClosePolicy` cascade is still enforced in Postgres by
2260
+ * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
2261
+ * not the authority.
2262
+ */
2263
+ async dispatch(run, type) {
2264
+ await this.loadBullMq();
2265
+ const def = await this.loadDefinition(type);
2266
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
2267
+ const jobOpts = {
2268
+ jobId,
2269
+ ...this.retryOpts(def),
2270
+ ...this.dedupeOpts(run, def)
2271
+ };
2272
+ if (run.parentRunId) {
2273
+ const parentRow = await this.loadRun(run.parentRunId);
2274
+ if (parentRow) {
2275
+ const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
2276
+ jobOpts.parent = {
2277
+ id: parentJobId,
2278
+ queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
2279
+ };
2280
+ }
2281
+ }
2282
+ const payload = { runId: run.id, type, input: run.input };
2283
+ await this.queueFor(run.pool).add(type, payload, jobOpts);
2284
+ }
2285
+ /**
2286
+ * Opt-in extension (spec §Extensions): expose the FlowProducer for
2287
+ * consumers that want to submit a whole parent/child DAG atomically up
2288
+ * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
2289
+ * code using it is not portable to the Drizzle backend. Async because it
2290
+ * lazily loads the optional `bullmq` package on first use.
2291
+ */
2292
+ async flowProducer() {
2293
+ await this.loadBullMq();
2294
+ return this.flow();
2295
+ }
2296
+ retryOpts(def) {
2297
+ const policy = def.retryPolicy;
2298
+ if (!policy) return {};
2299
+ return {
2300
+ attempts: policy.attempts,
2301
+ backoff: {
2302
+ type: policy.backoff === "exponential" ? "exponential" : "fixed",
2303
+ delay: policy.baseMs
2304
+ }
2305
+ };
2306
+ }
2307
+ dedupeOpts(run, def) {
2308
+ if (!run.dedupeKey || !def.dedupeWindowMs) return {};
2309
+ return {
2310
+ deduplication: {
2311
+ id: sha1JobId(run.dedupeKey),
2312
+ ttl: def.dedupeWindowMs
2313
+ }
2314
+ };
2315
+ }
2316
+ // ==========================================================================
2317
+ // cancel — Postgres cascade (super) + remove from queue
2318
+ // ==========================================================================
2319
+ async cancel(runId, opts = {}) {
2320
+ const target = await this.loadRun(runId);
2321
+ await super.cancel(runId, opts);
2322
+ if (!target) return;
2323
+ await this.loadBullMq();
2324
+ await this.removeFromQueue(target);
2325
+ if (opts.cascade === false) return;
2326
+ const descendants = await this.bullDb.select().from(jobRuns).where(eq5(jobRuns.rootRunId, target.rootRunId));
2327
+ for (const child of descendants) {
2328
+ if (child.id === runId) continue;
2329
+ await this.removeFromQueue(child);
2330
+ }
2331
+ }
2332
+ async removeFromQueue(run) {
2333
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
2334
+ try {
2335
+ const job = await this.queueFor(run.pool).getJob(jobId);
2336
+ if (job) await job.remove();
2337
+ } catch (err) {
2338
+ this.bullLogger.warn(
2339
+ `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
2340
+ );
2341
+ }
2342
+ }
2343
+ // ==========================================================================
2344
+ // replay — Postgres reset (super) + re-enqueue
2345
+ // ==========================================================================
2346
+ async replay(runId) {
2347
+ const run = await super.replay(runId);
2348
+ await this.dispatch(run, run.jobType);
2349
+ return run;
2350
+ }
2351
+ // ==========================================================================
2352
+ // Internals
2353
+ // ==========================================================================
2354
+ async loadDefinition(type) {
2355
+ const [def] = await this.bullDb.select().from(jobs).where(eq5(jobs.type, type)).limit(1);
2356
+ if (!def) {
2357
+ throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
2358
+ }
2359
+ return def;
2360
+ }
2361
+ async loadRun(id) {
2362
+ const [row] = await this.bullDb.select().from(jobRuns).where(eq5(jobRuns.id, id)).limit(1);
2363
+ return row ?? null;
2364
+ }
2365
+ /** Close all open queue + flow connections. Called on module destroy. */
2366
+ async closeConnections() {
2367
+ for (const q of this.queues.values()) {
2368
+ await q.close().catch(() => void 0);
2369
+ }
2370
+ this.queues.clear();
2371
+ if (this._flow) {
2372
+ await this._flow.close().catch(() => void 0);
2373
+ this._flow = null;
2374
+ }
2375
+ }
2376
+ };
2377
+ BullMQJobOrchestrator = __decorateClass([
2378
+ Injectable8(),
2379
+ __decorateParam(0, Inject8(DRIZZLE)),
2380
+ __decorateParam(1, Inject8(JOBS_MULTI_TENANT)),
2381
+ __decorateParam(2, Inject8(BULLMQ_CONNECTION)),
2382
+ __decorateParam(3, Optional3()),
2383
+ __decorateParam(3, Inject8(BULLMQ_RESOLVED_CONFIG))
2384
+ ], BullMQJobOrchestrator);
2385
+
2386
+ // runtime/subsystems/jobs/job-worker.bullmq-backend.ts
2387
+ import { Logger as Logger6 } from "@nestjs/common";
2388
+ import { eq as eq6 } from "drizzle-orm";
2389
+ function serialiseError(err, attempt, retryable) {
2390
+ const e = err;
2391
+ return {
2392
+ message: e?.message ?? String(err),
2393
+ stack: e?.stack,
2394
+ retryable,
2395
+ attempt
2396
+ };
2397
+ }
2398
+ var BullMQJobWorker = class _BullMQJobWorker {
2399
+ constructor(db, orchestrator, stepService, options, moduleRef) {
2400
+ this.db = db;
2401
+ this.orchestrator = orchestrator;
2402
+ this.stepService = stepService;
2403
+ this.options = options;
2404
+ this.moduleRef = moduleRef;
2405
+ }
2406
+ db;
2407
+ orchestrator;
2408
+ stepService;
2409
+ options;
2410
+ moduleRef;
2411
+ logger = new Logger6(_BullMQJobWorker.name);
2412
+ worker = null;
2413
+ async onModuleInit() {
2414
+ let WorkerCtor;
2415
+ try {
2416
+ const mod = await import("bullmq");
2417
+ WorkerCtor = mod.Worker;
2418
+ } catch {
2419
+ throw new Error(
2420
+ 'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
2421
+ );
2422
+ }
2423
+ this.worker = new WorkerCtor(
2424
+ this.options.queueName,
2425
+ (job) => this.process(job),
2426
+ {
2427
+ connection: this.options.connection,
2428
+ concurrency: this.options.concurrency
2429
+ }
2430
+ );
2431
+ this.worker.on("failed", (job, err) => {
2432
+ if (!job) return;
2433
+ const attemptsMade = job.attemptsMade;
2434
+ const maxAttempts = job.opts.attempts ?? 1;
2435
+ if (attemptsMade >= maxAttempts) {
2436
+ void this.markFailed(job.data.runId, err, attemptsMade);
2437
+ }
2438
+ });
2439
+ this.logger.log(
2440
+ `BullMQ worker started: pool='${this.options.pool}' queue='${this.options.queueName}' concurrency=${this.options.concurrency}`
2441
+ );
2442
+ }
2443
+ async onModuleDestroy() {
2444
+ if (this.worker) {
2445
+ await this.worker.close();
2446
+ this.worker = null;
2447
+ }
2448
+ }
2449
+ /**
2450
+ * Process one BullMQ job. Returns the handler output (stored by BullMQ as
2451
+ * the job return value AND written to `job_run.output`). Throws on handler
2452
+ * failure so BullMQ applies the retry policy.
2453
+ */
2454
+ async process(job) {
2455
+ const { runId } = job.data;
2456
+ const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
2457
+ if (!row) {
2458
+ this.logger.warn(`process: job_run ${runId} not found; skipping`);
2459
+ return {};
2460
+ }
2461
+ const run = row;
2462
+ if (run.status === "canceled") {
2463
+ return {};
2464
+ }
2465
+ const registryEntry = JOB_HANDLER_REGISTRY.get(run.jobType);
2466
+ if (!registryEntry) {
2467
+ throw new Error(
2468
+ `No handler registered for jobType='${run.jobType}' (run ${run.id})`
2469
+ );
2470
+ }
2471
+ await this.db.update(jobRuns).set({
2472
+ status: "running",
2473
+ claimedAt: /* @__PURE__ */ new Date(),
2474
+ startedAt: /* @__PURE__ */ new Date(),
2475
+ attempts: job.attemptsMade + 1,
2476
+ updatedAt: /* @__PURE__ */ new Date()
2477
+ }).where(eq6(jobRuns.id, run.id));
2478
+ const HandlerClass = registryEntry.handlerClass;
2479
+ const handler = this.moduleRef.get(
2480
+ HandlerClass,
2481
+ { strict: false }
2482
+ );
2483
+ const ctx = {
2484
+ input: run.input,
2485
+ run,
2486
+ step: this.makeStepFn(run),
2487
+ spawnChild: this.makeSpawnFn(run),
2488
+ logger: new Logger6(`JobRun:${run.id}`)
2489
+ };
2490
+ const output = await handler.run(ctx);
2491
+ await this.db.update(jobRuns).set({
2492
+ status: "completed",
2493
+ output: output ?? {},
2494
+ finishedAt: /* @__PURE__ */ new Date(),
2495
+ updatedAt: /* @__PURE__ */ new Date()
2496
+ }).where(eq6(jobRuns.id, run.id));
2497
+ return output ?? {};
2498
+ }
2499
+ async markFailed(runId, err, finalAttempts) {
2500
+ const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
2501
+ if (!row) return;
2502
+ const run = row;
2503
+ await this.db.update(jobRuns).set({
2504
+ status: "failed",
2505
+ attempts: finalAttempts,
2506
+ finishedAt: /* @__PURE__ */ new Date(),
2507
+ error: serialiseError(err, finalAttempts, false),
2508
+ updatedAt: /* @__PURE__ */ new Date()
2509
+ }).where(eq6(jobRuns.id, runId));
2510
+ if (run.parentClosePolicy === "terminate") {
2511
+ try {
2512
+ await this.orchestrator.cancel(run.id, {
2513
+ cascade: true,
2514
+ reason: "parent-failed",
2515
+ tenantId: run.tenantId
2516
+ });
2517
+ } catch (cascadeErr) {
2518
+ this.logger.warn(
2519
+ `cascade on failed run ${run.id}: ${cascadeErr.message}`
2520
+ );
2521
+ }
2522
+ }
2523
+ }
2524
+ // ── ctx.step / ctx.spawnChild (mirror JobWorker) ──────────────────────────
2525
+ makeStepFn(run) {
2526
+ return async (stepId, fn, _opts) => {
2527
+ void _opts;
2528
+ const existing = await this.stepService.findStep(run.id, stepId);
2529
+ if (existing?.status === "completed") {
2530
+ return existing.output;
2531
+ }
2532
+ const nextAttempts = (existing?.attempts ?? 0) + 1;
2533
+ const seq = nextAttempts;
2534
+ await this.stepService.recordStep({
2535
+ jobRunId: run.id,
2536
+ stepId,
2537
+ kind: "task",
2538
+ seq,
2539
+ status: "running",
2540
+ startedAt: /* @__PURE__ */ new Date(),
2541
+ attempts: nextAttempts
2542
+ });
2543
+ try {
2544
+ const output = await fn();
2545
+ await this.stepService.recordStep({
2546
+ jobRunId: run.id,
2547
+ stepId,
2548
+ kind: "task",
2549
+ seq,
2550
+ status: "completed",
2551
+ output,
2552
+ finishedAt: /* @__PURE__ */ new Date(),
2553
+ attempts: nextAttempts
2554
+ });
2555
+ return output;
2556
+ } catch (err) {
2557
+ await this.stepService.recordStep({
2558
+ jobRunId: run.id,
2559
+ stepId,
2560
+ kind: "task",
2561
+ seq,
2562
+ status: "failed",
2563
+ error: serialiseError(err, nextAttempts, false),
2564
+ finishedAt: /* @__PURE__ */ new Date(),
2565
+ attempts: nextAttempts
2566
+ });
2567
+ throw err;
2568
+ }
2569
+ };
1734
2570
  }
1735
- async findStep(runId, stepId) {
1736
- const [row] = await this.db.select().from(jobSteps).where(and4(eq4(jobSteps.jobRunId, runId), eq4(jobSteps.stepId, stepId))).limit(1);
1737
- return row ?? null;
2571
+ makeSpawnFn(run) {
2572
+ return async (type, input, opts) => {
2573
+ return this.orchestrator.start(type, input, {
2574
+ parentRunId: run.id,
2575
+ parentClosePolicy: opts?.closePolicy,
2576
+ runAt: opts?.runAt,
2577
+ priority: opts?.priority,
2578
+ tags: opts?.tags,
2579
+ triggerSource: "parent",
2580
+ triggerRef: run.id,
2581
+ tenantId: run.tenantId
2582
+ });
2583
+ };
1738
2584
  }
1739
2585
  };
1740
- DrizzleJobStepService = __decorateClass([
1741
- Injectable7(),
1742
- __decorateParam(0, Inject7(DRIZZLE))
1743
- ], DrizzleJobStepService);
1744
2586
 
1745
2587
  // runtime/subsystems/jobs/job-worker.ts
1746
- import { Inject as Inject8, Injectable as Injectable8, Logger as Logger5 } from "@nestjs/common";
1747
- import { and as and5, asc as asc3, desc as desc3, eq as eq5, inArray as inArray4, lt, lte, sql as sql5 } from "drizzle-orm";
2588
+ import { Inject as Inject9, Injectable as Injectable9, Logger as Logger7 } from "@nestjs/common";
2589
+ import { and as and5, asc as asc3, desc as desc4, eq as eq7, inArray as inArray4, lt as lt3, lte, sql as sql6 } from "drizzle-orm";
1748
2590
  var JOB_WORKER_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_OPTIONS");
1749
2591
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
1750
2592
  var DEFAULT_STALE_SWEEPER_INTERVAL_MS = 6e4;
@@ -1773,7 +2615,7 @@ function classifyError(err, policy, currentAttempts) {
1773
2615
  if (currentAttempts + 1 >= policy.attempts) return "fail";
1774
2616
  return "retry";
1775
2617
  }
1776
- function serialiseError(err, attempt, retryable) {
2618
+ function serialiseError2(err, attempt, retryable) {
1777
2619
  const e = err;
1778
2620
  return {
1779
2621
  message: e?.message ?? String(err),
@@ -1807,7 +2649,7 @@ var JobWorker = class {
1807
2649
  stepService;
1808
2650
  options;
1809
2651
  moduleRef;
1810
- logger = new Logger5(JobWorker.name);
2652
+ logger = new Logger7(JobWorker.name);
1811
2653
  shuttingDown = false;
1812
2654
  inFlight = /* @__PURE__ */ new Set();
1813
2655
  pollTimer = null;
@@ -1848,7 +2690,7 @@ var JobWorker = class {
1848
2690
  await this.drainInFlight();
1849
2691
  try {
1850
2692
  await this.db.update(jobRuns).set({ status: "pending", claimedAt: null, startedAt: null }).where(
1851
- and5(eq5(jobRuns.status, "running"), eq5(jobRuns.pool, this.options.pool))
2693
+ and5(eq7(jobRuns.status, "running"), eq7(jobRuns.pool, this.options.pool))
1852
2694
  );
1853
2695
  } catch (err) {
1854
2696
  this.logger.error(`shutdown reset failed: ${err.message}`);
@@ -1898,11 +2740,11 @@ var JobWorker = class {
1898
2740
  return this.db.transaction(async (tx) => {
1899
2741
  const candidates = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
1900
2742
  and5(
1901
- eq5(jobRuns.status, "pending"),
1902
- eq5(jobRuns.pool, pool),
2743
+ eq7(jobRuns.status, "pending"),
2744
+ eq7(jobRuns.pool, pool),
1903
2745
  lte(jobRuns.runAt, /* @__PURE__ */ new Date())
1904
2746
  )
1905
- ).orderBy(desc3(jobRuns.priority), asc3(jobRuns.runAt)).limit(1).for("update", { skipLocked: true });
2747
+ ).orderBy(desc4(jobRuns.priority), asc3(jobRuns.runAt)).limit(1).for("update", { skipLocked: true });
1906
2748
  const candidate = candidates[0];
1907
2749
  if (!candidate) return null;
1908
2750
  const [claimed] = await tx.update(jobRuns).set({
@@ -1910,7 +2752,7 @@ var JobWorker = class {
1910
2752
  claimedAt: /* @__PURE__ */ new Date(),
1911
2753
  startedAt: /* @__PURE__ */ new Date(),
1912
2754
  updatedAt: /* @__PURE__ */ new Date()
1913
- }).where(eq5(jobRuns.id, candidate.id)).returning();
2755
+ }).where(eq7(jobRuns.id, candidate.id)).returning();
1914
2756
  return claimed ?? null;
1915
2757
  });
1916
2758
  }
@@ -1928,7 +2770,7 @@ var JobWorker = class {
1928
2770
  await this.db.transaction(async (tx) => {
1929
2771
  const threshold = new Date(Date.now() - this.staleThresholdMs);
1930
2772
  const stale = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
1931
- and5(eq5(jobRuns.status, "running"), lt(jobRuns.claimedAt, threshold))
2773
+ and5(eq7(jobRuns.status, "running"), lt3(jobRuns.claimedAt, threshold))
1932
2774
  ).for("update", { skipLocked: true });
1933
2775
  if (stale.length === 0) return;
1934
2776
  const ids = stale.map((r) => r.id);
@@ -1961,8 +2803,8 @@ var JobWorker = class {
1961
2803
  if (claimed.concurrencyKey) {
1962
2804
  const inflight = await this.db.select({ id: jobRuns.id }).from(jobRuns).where(
1963
2805
  and5(
1964
- eq5(jobRuns.concurrencyKey, claimed.concurrencyKey),
1965
- eq5(jobRuns.status, "running")
2806
+ eq7(jobRuns.concurrencyKey, claimed.concurrencyKey),
2807
+ eq7(jobRuns.status, "running")
1966
2808
  )
1967
2809
  );
1968
2810
  const other = inflight.find((r) => r.id !== claimed.id);
@@ -1972,7 +2814,7 @@ var JobWorker = class {
1972
2814
  claimedAt: null,
1973
2815
  startedAt: null,
1974
2816
  updatedAt: /* @__PURE__ */ new Date()
1975
- }).where(eq5(jobRuns.id, claimed.id));
2817
+ }).where(eq7(jobRuns.id, claimed.id));
1976
2818
  return;
1977
2819
  }
1978
2820
  }
@@ -1987,7 +2829,7 @@ var JobWorker = class {
1987
2829
  run: claimed,
1988
2830
  step: this.makeStepFn(claimed),
1989
2831
  spawnChild: this.makeSpawnFn(claimed),
1990
- logger: new Logger5(`JobRun:${claimed.id}`)
2832
+ logger: new Logger7(`JobRun:${claimed.id}`)
1991
2833
  };
1992
2834
  const attemptsBefore = claimed.attempts ?? 0;
1993
2835
  try {
@@ -1998,7 +2840,7 @@ var JobWorker = class {
1998
2840
  finishedAt: /* @__PURE__ */ new Date(),
1999
2841
  updatedAt: /* @__PURE__ */ new Date(),
2000
2842
  attempts: attemptsBefore + 1
2001
- }).where(eq5(jobRuns.id, claimed.id));
2843
+ }).where(eq7(jobRuns.id, claimed.id));
2002
2844
  } catch (err) {
2003
2845
  const policy = meta.retry;
2004
2846
  const decision = classifyError(err, policy, attemptsBefore);
@@ -2011,9 +2853,9 @@ var JobWorker = class {
2011
2853
  runAt: new Date(Date.now() + delay),
2012
2854
  startedAt: null,
2013
2855
  claimedAt: null,
2014
- error: serialiseError(err, nextAttempts, true),
2856
+ error: serialiseError2(err, nextAttempts, true),
2015
2857
  updatedAt: /* @__PURE__ */ new Date()
2016
- }).where(eq5(jobRuns.id, claimed.id));
2858
+ }).where(eq7(jobRuns.id, claimed.id));
2017
2859
  } else {
2018
2860
  await this.markFailed(claimed, err, nextAttempts);
2019
2861
  }
@@ -2024,9 +2866,9 @@ var JobWorker = class {
2024
2866
  status: "failed",
2025
2867
  attempts: finalAttempts,
2026
2868
  finishedAt: /* @__PURE__ */ new Date(),
2027
- error: serialiseError(err, finalAttempts, false),
2869
+ error: serialiseError2(err, finalAttempts, false),
2028
2870
  updatedAt: /* @__PURE__ */ new Date()
2029
- }).where(eq5(jobRuns.id, claimed.id));
2871
+ }).where(eq7(jobRuns.id, claimed.id));
2030
2872
  if (claimed.parentClosePolicy === "terminate") {
2031
2873
  try {
2032
2874
  await this.orchestrator.cancel(claimed.id, {
@@ -2083,7 +2925,7 @@ var JobWorker = class {
2083
2925
  kind: "task",
2084
2926
  seq,
2085
2927
  status: "failed",
2086
- error: serialiseError(err, nextAttempts, false),
2928
+ error: serialiseError2(err, nextAttempts, false),
2087
2929
  finishedAt: /* @__PURE__ */ new Date(),
2088
2930
  attempts: nextAttempts
2089
2931
  });
@@ -2113,7 +2955,7 @@ var JobWorker = class {
2113
2955
  */
2114
2956
  async nextStepSeq(runId) {
2115
2957
  const [row] = await this.db.execute(
2116
- sql5`SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM job_step WHERE job_run_id = ${runId}`
2958
+ sql6`SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM job_step WHERE job_run_id = ${runId}`
2117
2959
  );
2118
2960
  const maybeRows = row?.rows;
2119
2961
  if (Array.isArray(maybeRows) && maybeRows.length > 0) {
@@ -2129,12 +2971,12 @@ var JobWorker = class {
2129
2971
  // ============================================================================
2130
2972
  };
2131
2973
  JobWorker = __decorateClass([
2132
- Injectable8(),
2133
- __decorateParam(0, Inject8(DRIZZLE)),
2134
- __decorateParam(1, Inject8(JOB_ORCHESTRATOR)),
2135
- __decorateParam(2, Inject8(JOB_RUN_SERVICE)),
2136
- __decorateParam(3, Inject8(JOB_STEP_SERVICE)),
2137
- __decorateParam(4, Inject8(JOB_WORKER_OPTIONS))
2974
+ Injectable9(),
2975
+ __decorateParam(0, Inject9(DRIZZLE)),
2976
+ __decorateParam(1, Inject9(JOB_ORCHESTRATOR)),
2977
+ __decorateParam(2, Inject9(JOB_RUN_SERVICE)),
2978
+ __decorateParam(3, Inject9(JOB_STEP_SERVICE)),
2979
+ __decorateParam(4, Inject9(JOB_WORKER_OPTIONS))
2138
2980
  ], JobWorker);
2139
2981
 
2140
2982
  // runtime/subsystems/jobs/memory-job-store.ts
@@ -2155,7 +2997,7 @@ var MemoryJobStore = class {
2155
2997
 
2156
2998
  // runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
2157
2999
  import { randomUUID as randomUUID3 } from "crypto";
2158
- import { Inject as Inject9, Injectable as Injectable9, Logger as Logger6, Optional as Optional3 } from "@nestjs/common";
3000
+ import { Inject as Inject10, Injectable as Injectable10, Logger as Logger8, Optional as Optional4 } from "@nestjs/common";
2159
3001
  var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
2160
3002
  var TERMINAL_STATUSES2 = [
2161
3003
  "completed",
@@ -2202,7 +3044,7 @@ var MemoryJobOrchestrator = class {
2202
3044
  stepService;
2203
3045
  multiTenant;
2204
3046
  moduleRef;
2205
- logger = new Logger6(MemoryJobOrchestrator.name);
3047
+ logger = new Logger8(MemoryJobOrchestrator.name);
2206
3048
  mutex = new PromiseMutex();
2207
3049
  handlerRegistry = /* @__PURE__ */ new Map();
2208
3050
  /**
@@ -2551,7 +3393,7 @@ var MemoryJobOrchestrator = class {
2551
3393
  run,
2552
3394
  step: this.makeStepFn(run),
2553
3395
  spawnChild: this.makeSpawnFn(run),
2554
- logger: new Logger6(`JobRun:${run.id}`)
3396
+ logger: new Logger8(`JobRun:${run.id}`)
2555
3397
  };
2556
3398
  const attemptsBefore = run.attempts ?? 0;
2557
3399
  try {
@@ -2608,7 +3450,7 @@ var MemoryJobOrchestrator = class {
2608
3450
  kind: "task",
2609
3451
  seq,
2610
3452
  status: "failed",
2611
- error: serialiseError2(err, nextAttempts, false),
3453
+ error: serialiseError3(err, nextAttempts, false),
2612
3454
  finishedAt: /* @__PURE__ */ new Date(),
2613
3455
  attempts: nextAttempts
2614
3456
  });
@@ -2663,7 +3505,7 @@ var MemoryJobOrchestrator = class {
2663
3505
  finishedAt: now,
2664
3506
  updatedAt: now,
2665
3507
  attempts,
2666
- error: serialiseError2(err, attempts, false)
3508
+ error: serialiseError3(err, attempts, false)
2667
3509
  });
2668
3510
  this.unblockQueuedDependents(run.id);
2669
3511
  });
@@ -2694,7 +3536,7 @@ var MemoryJobOrchestrator = class {
2694
3536
  startedAt: null,
2695
3537
  claimedAt: null,
2696
3538
  updatedAt: now,
2697
- error: serialiseError2(err, attempts, true)
3539
+ error: serialiseError3(err, attempts, true)
2698
3540
  });
2699
3541
  });
2700
3542
  }
@@ -2724,9 +3566,9 @@ var MemoryJobOrchestrator = class {
2724
3566
  }
2725
3567
  };
2726
3568
  MemoryJobOrchestrator = __decorateClass([
2727
- Injectable9(),
2728
- __decorateParam(2, Inject9(JOBS_MULTI_TENANT)),
2729
- __decorateParam(3, Optional3())
3569
+ Injectable10(),
3570
+ __decorateParam(2, Inject10(JOBS_MULTI_TENANT)),
3571
+ __decorateParam(3, Optional4())
2730
3572
  ], MemoryJobOrchestrator);
2731
3573
  function classifyError2(err, policy, currentAttempts) {
2732
3574
  if (!policy) return "fail";
@@ -2749,7 +3591,7 @@ function computeBackoff2(policy, attempts) {
2749
3591
  }
2750
3592
  return raw;
2751
3593
  }
2752
- function serialiseError2(err, attempt, retryable) {
3594
+ function serialiseError3(err, attempt, retryable) {
2753
3595
  const e = err;
2754
3596
  return {
2755
3597
  message: e?.message ?? String(err),
@@ -2760,7 +3602,7 @@ function serialiseError2(err, attempt, retryable) {
2760
3602
  }
2761
3603
 
2762
3604
  // runtime/subsystems/jobs/job-run-service.memory-backend.ts
2763
- import { Inject as Inject10, Injectable as Injectable10 } from "@nestjs/common";
3605
+ import { Inject as Inject11, Injectable as Injectable11 } from "@nestjs/common";
2764
3606
  var NON_TERMINAL_STATUSES2 = [
2765
3607
  "pending",
2766
3608
  "running",
@@ -2877,6 +3719,38 @@ var MemoryJobRunService = class {
2877
3719
  createdAt: r.createdAt
2878
3720
  }));
2879
3721
  }
3722
+ async listJobRuns(query = {}) {
3723
+ const limit = clampLimit(query.limit);
3724
+ const tenantCheck = this.tenantPredicate("listJobRuns", query.tenantId);
3725
+ const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;
3726
+ const matched = [];
3727
+ for (const r of this.store.runs.values()) {
3728
+ if (tenantCheck && !tenantCheck(r)) continue;
3729
+ if (query.poolId && r.pool !== query.poolId) continue;
3730
+ if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;
3731
+ if (query.status && r.status !== query.status) continue;
3732
+ if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;
3733
+ matched.push(r);
3734
+ }
3735
+ matched.sort((a, b) => {
3736
+ const dt = b.createdAt.getTime() - a.createdAt.getTime();
3737
+ if (dt !== 0) return dt;
3738
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
3739
+ });
3740
+ const seeked = keyset ? matched.filter((r) => {
3741
+ const ct = r.createdAt.getTime();
3742
+ const kt = keyset.createdAt.getTime();
3743
+ if (ct < kt) return true;
3744
+ if (ct > kt) return false;
3745
+ return r.id < keyset.id;
3746
+ }) : matched;
3747
+ const hasMore = seeked.length > limit;
3748
+ const page = hasMore ? seeked.slice(0, limit) : seeked;
3749
+ const items = page.map(toJobRunSummary);
3750
+ const last = page[page.length - 1];
3751
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
3752
+ return { items, nextCursor };
3753
+ }
2880
3754
  /**
2881
3755
  * Direct lookup. Not on the protocol — concrete-class convenience for
2882
3756
  * tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
@@ -2895,9 +3769,9 @@ var MemoryJobRunService = class {
2895
3769
  }
2896
3770
  };
2897
3771
  MemoryJobRunService = __decorateClass([
2898
- Injectable10(),
2899
- __decorateParam(1, Inject10(JOB_ORCHESTRATOR)),
2900
- __decorateParam(2, Inject10(JOBS_MULTI_TENANT))
3772
+ Injectable11(),
3773
+ __decorateParam(1, Inject11(JOB_ORCHESTRATOR)),
3774
+ __decorateParam(2, Inject11(JOBS_MULTI_TENANT))
2901
3775
  ], MemoryJobRunService);
2902
3776
  function compareBy(a, b, order) {
2903
3777
  switch (order) {
@@ -2915,7 +3789,7 @@ function compareBy(a, b, order) {
2915
3789
 
2916
3790
  // runtime/subsystems/jobs/job-step-service.memory-backend.ts
2917
3791
  import { randomUUID as randomUUID4 } from "crypto";
2918
- import { Injectable as Injectable11 } from "@nestjs/common";
3792
+ import { Injectable as Injectable12 } from "@nestjs/common";
2919
3793
  var MemoryJobStepService = class {
2920
3794
  constructor(store) {
2921
3795
  this.store = store;
@@ -3008,14 +3882,13 @@ var MemoryJobStepService = class {
3008
3882
  }
3009
3883
  };
3010
3884
  MemoryJobStepService = __decorateClass([
3011
- Injectable11()
3885
+ Injectable12()
3012
3886
  ], MemoryJobStepService);
3013
3887
 
3014
3888
  // runtime/subsystems/jobs/jobs-domain.module.ts
3015
3889
  import { Module as Module2 } from "@nestjs/common";
3016
3890
  var JobsDomainModule = class {
3017
3891
  static forRoot(opts) {
3018
- void opts.extensions;
3019
3892
  const multiTenant = opts.multiTenant ?? false;
3020
3893
  const providers = [
3021
3894
  // JOB-8 — boolean provider consumed by the four service-layer backends.
@@ -3034,21 +3907,32 @@ var JobsDomainModule = class {
3034
3907
  providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
3035
3908
  providers.push(MemoryJobRunService);
3036
3909
  providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
3910
+ } else if (opts.backend === "bullmq") {
3911
+ const resolved = resolveBullMqConfig(opts.extensions?.bullmq);
3912
+ providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });
3913
+ providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });
3914
+ providers.push({ provide: JOB_ORCHESTRATOR, useClass: BullMQJobOrchestrator });
3915
+ providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
3916
+ providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
3037
3917
  } else {
3038
3918
  providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });
3039
3919
  providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
3040
3920
  providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
3041
3921
  }
3922
+ const exports = [
3923
+ JOB_ORCHESTRATOR,
3924
+ JOB_RUN_SERVICE,
3925
+ JOB_STEP_SERVICE,
3926
+ JOBS_MULTI_TENANT
3927
+ ];
3928
+ if (opts.backend === "bullmq") {
3929
+ exports.push(BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG);
3930
+ }
3042
3931
  return {
3043
3932
  module: JobsDomainModule,
3044
3933
  global: true,
3045
3934
  providers,
3046
- exports: [
3047
- JOB_ORCHESTRATOR,
3048
- JOB_RUN_SERVICE,
3049
- JOB_STEP_SERVICE,
3050
- JOBS_MULTI_TENANT
3051
- ]
3935
+ exports
3052
3936
  };
3053
3937
  }
3054
3938
  };
@@ -3058,143 +3942,24 @@ JobsDomainModule = __decorateClass([
3058
3942
 
3059
3943
  // runtime/subsystems/jobs/job-worker.module.ts
3060
3944
  import {
3061
- Inject as Inject11,
3062
- Injectable as Injectable12,
3063
- Logger as Logger7,
3945
+ Inject as Inject12,
3946
+ Injectable as Injectable13,
3947
+ Logger as Logger9,
3064
3948
  Module as Module3,
3065
- Optional as Optional4
3949
+ Optional as Optional5
3066
3950
  } from "@nestjs/common";
3067
-
3068
- // runtime/subsystems/jobs/pool-config.loader.ts
3069
- import { existsSync, readFileSync } from "fs";
3070
- import { resolve } from "path";
3071
- import { parse as parseYaml } from "yaml";
3072
- var FRAMEWORK_POOLS = Object.freeze({
3073
- events_inbound: Object.freeze({
3074
- queue: "jobs-events-inbound",
3075
- concurrency: 20,
3076
- reserved: true,
3077
- description: "Inbound events drain (events subsystem outbox)."
3078
- }),
3079
- events_change: Object.freeze({
3080
- queue: "jobs-events-change",
3081
- concurrency: 30,
3082
- reserved: true,
3083
- description: "Change events drain (events subsystem outbox)."
3084
- }),
3085
- events_outbound: Object.freeze({
3086
- queue: "jobs-events-outbound",
3087
- concurrency: 10,
3088
- reserved: true,
3089
- description: "Outbound events drain (events subsystem outbox)."
3090
- }),
3091
- interactive: Object.freeze({
3092
- queue: "jobs-interactive",
3093
- concurrency: 20,
3094
- reserved: false,
3095
- description: "User-facing latency-sensitive jobs."
3096
- }),
3097
- batch: Object.freeze({
3098
- queue: "jobs-batch",
3099
- concurrency: 5,
3100
- reserved: false,
3101
- description: "Default pool for background jobs."
3102
- })
3103
- });
3104
- var RESERVED_POOL_NAMES = new Set(
3105
- Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
3106
- );
3107
- var cache = /* @__PURE__ */ new Map();
3108
- function loadPoolConfig(configPath) {
3109
- const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
3110
- const cached = cache.get(resolved);
3111
- if (cached) return cached;
3112
- const merged = /* @__PURE__ */ new Map();
3113
- for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
3114
- merged.set(name, { ...def });
3115
- }
3116
- if (!existsSync(resolved)) {
3117
- cache.set(resolved, merged);
3118
- return merged;
3119
- }
3120
- let raw;
3121
- try {
3122
- raw = parseYaml(readFileSync(resolved, "utf8"));
3123
- } catch (err) {
3124
- throw new Error(
3125
- `pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
3126
- );
3127
- }
3128
- const userPools = extractUserPools(raw);
3129
- for (const [name, userDef] of Object.entries(userPools)) {
3130
- const existing = merged.get(name);
3131
- if (existing) {
3132
- const next = {
3133
- queue: existing.queue,
3134
- concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
3135
- reserved: existing.reserved,
3136
- description: userDef.description ?? existing.description
3137
- };
3138
- merged.set(name, next);
3139
- continue;
3140
- }
3141
- if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
3142
- throw new Error(
3143
- `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
3144
- );
3145
- }
3146
- if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
3147
- throw new Error(
3148
- `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
3149
- );
3150
- }
3151
- if (userDef.reserved === true) {
3152
- throw new Error(
3153
- `pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
3154
- );
3155
- }
3156
- merged.set(name, {
3157
- queue: userDef.queue,
3158
- concurrency: userDef.concurrency,
3159
- reserved: false,
3160
- description: userDef.description
3161
- });
3162
- }
3163
- cache.set(resolved, merged);
3164
- return merged;
3165
- }
3166
- function allNonReservedPoolNames(config) {
3167
- const out = [];
3168
- for (const [name, def] of config) {
3169
- if (!def.reserved) out.push(name);
3170
- }
3171
- return out;
3172
- }
3173
- function extractUserPools(raw) {
3174
- if (!raw || typeof raw !== "object") return {};
3175
- const jobs2 = raw.jobs;
3176
- if (!jobs2 || typeof jobs2 !== "object") return {};
3177
- const pools = jobs2.pools;
3178
- if (!pools || typeof pools !== "object") return {};
3179
- const out = {};
3180
- for (const [name, def] of Object.entries(pools)) {
3181
- if (!def || typeof def !== "object") continue;
3182
- out[name] = def;
3183
- }
3184
- return out;
3185
- }
3186
-
3187
- // runtime/subsystems/jobs/job-worker.module.ts
3188
3951
  var DEFAULT_SHUTDOWN_TIMEOUT_MS2 = 3e4;
3189
3952
  var JOB_WORKER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_MODULE_OPTIONS");
3190
3953
  var JobWorkerOrchestrator = class {
3191
- constructor(orchestrator, runService, stepService, options, db = null, moduleRef) {
3954
+ constructor(orchestrator, runService, stepService, options, db = null, moduleRef, bullConnection = null, bullConfig = null) {
3192
3955
  this.orchestrator = orchestrator;
3193
3956
  this.runService = runService;
3194
3957
  this.stepService = stepService;
3195
3958
  this.options = options;
3196
3959
  this.db = db;
3197
3960
  this.moduleRef = moduleRef;
3961
+ this.bullConnection = bullConnection;
3962
+ this.bullConfig = bullConfig;
3198
3963
  }
3199
3964
  orchestrator;
3200
3965
  runService;
@@ -3202,7 +3967,9 @@ var JobWorkerOrchestrator = class {
3202
3967
  options;
3203
3968
  db;
3204
3969
  moduleRef;
3205
- logger = new Logger7(JobWorkerOrchestrator.name);
3970
+ bullConnection;
3971
+ bullConfig;
3972
+ logger = new Logger9(JobWorkerOrchestrator.name);
3206
3973
  workers = [];
3207
3974
  // ============================================================================
3208
3975
  // Lifecycle
@@ -3219,7 +3986,7 @@ var JobWorkerOrchestrator = class {
3219
3986
  if (backend !== "memory" && orphaned.length > 0) {
3220
3987
  throw new BootValidationError(orphaned);
3221
3988
  }
3222
- const activePools = this.options.pools ?? allNonReservedPoolNames(poolConfig);
3989
+ const activePools = this.options.pools ? this.options.pools : this.options.allPools ? allPoolNames(poolConfig) : allNonReservedPoolNames(poolConfig);
3223
3990
  for (const poolName of activePools) {
3224
3991
  const def = poolConfig.get(poolName);
3225
3992
  if (!def) {
@@ -3232,11 +3999,11 @@ var JobWorkerOrchestrator = class {
3232
3999
  concurrency: def.concurrency,
3233
4000
  shutdownTimeoutMs: this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS2
3234
4001
  };
3235
- const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : this.spawnWorker(workerOptions);
3236
- worker.onModuleInit();
4002
+ const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : backend === "bullmq" ? this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig) : this.spawnWorker(workerOptions);
4003
+ await worker.onModuleInit();
3237
4004
  this.workers.push(worker);
3238
4005
  this.logger.log(
3239
- `JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency}`
4006
+ `JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency} backend='${backend}'`
3240
4007
  );
3241
4008
  }
3242
4009
  }
@@ -3253,6 +4020,16 @@ var JobWorkerOrchestrator = class {
3253
4020
  }
3254
4021
  }
3255
4022
  this.workers.length = 0;
4023
+ const orch = this.orchestrator;
4024
+ if (typeof orch.closeConnections === "function") {
4025
+ try {
4026
+ await orch.closeConnections();
4027
+ } catch (err) {
4028
+ this.logger.error(
4029
+ `BullMQ orchestrator connection close failed: ${err.message}`
4030
+ );
4031
+ }
4032
+ }
3256
4033
  }
3257
4034
  // ============================================================================
3258
4035
  // Internals
@@ -3314,15 +4091,57 @@ var JobWorkerOrchestrator = class {
3314
4091
  this.moduleRef
3315
4092
  );
3316
4093
  }
4094
+ /**
4095
+ * BULLMQ-1 — spawn a per-pool `BullMQJobWorker`. Requires the Drizzle
4096
+ * client (the worker drives `job_run` as the source of truth) AND the
4097
+ * resolved BullMQ connection (bound by `JobsDomainModule` when
4098
+ * `backend: 'bullmq'`). The queue name is derived identically to the
4099
+ * orchestrator's `dispatch` via `resolvePoolQueueName(pool, …)` so producer
4100
+ * and consumer agree.
4101
+ */
4102
+ spawnBullMQWorker(pool, _queueAlias, concurrency, poolConfig) {
4103
+ if (!this.db) {
4104
+ throw new Error(
4105
+ `JobWorkerModule: BullMQ worker spawning requires the Drizzle client (no DRIZZLE provider available) \u2014 job_run remains the source of truth.`
4106
+ );
4107
+ }
4108
+ if (!this.bullConnection) {
4109
+ throw new Error(
4110
+ `JobWorkerModule: BullMQ worker spawning requires a resolved BULLMQ_CONNECTION. Ensure JobsDomainModule was booted with backend: 'bullmq'.`
4111
+ );
4112
+ }
4113
+ if (!this.moduleRef) {
4114
+ throw new Error(
4115
+ `JobWorkerModule: ModuleRef not available \u2014 cannot construct BullMQJobWorker with handler DI support.`
4116
+ );
4117
+ }
4118
+ const queueName = resolvePoolQueueName(pool, this.bullConfig, poolConfig);
4119
+ return new BullMQJobWorker(
4120
+ this.db,
4121
+ this.orchestrator,
4122
+ this.stepService,
4123
+ {
4124
+ pool,
4125
+ queueName,
4126
+ concurrency,
4127
+ connection: this.bullConnection
4128
+ },
4129
+ this.moduleRef
4130
+ );
4131
+ }
3317
4132
  };
3318
4133
  JobWorkerOrchestrator = __decorateClass([
3319
- Injectable12(),
3320
- __decorateParam(0, Inject11(JOB_ORCHESTRATOR)),
3321
- __decorateParam(1, Inject11(JOB_RUN_SERVICE)),
3322
- __decorateParam(2, Inject11(JOB_STEP_SERVICE)),
3323
- __decorateParam(3, Inject11(JOB_WORKER_MODULE_OPTIONS)),
3324
- __decorateParam(4, Optional4()),
3325
- __decorateParam(4, Inject11(DRIZZLE))
4134
+ Injectable13(),
4135
+ __decorateParam(0, Inject12(JOB_ORCHESTRATOR)),
4136
+ __decorateParam(1, Inject12(JOB_RUN_SERVICE)),
4137
+ __decorateParam(2, Inject12(JOB_STEP_SERVICE)),
4138
+ __decorateParam(3, Inject12(JOB_WORKER_MODULE_OPTIONS)),
4139
+ __decorateParam(4, Optional5()),
4140
+ __decorateParam(4, Inject12(DRIZZLE)),
4141
+ __decorateParam(6, Optional5()),
4142
+ __decorateParam(6, Inject12(BULLMQ_CONNECTION)),
4143
+ __decorateParam(7, Optional5()),
4144
+ __decorateParam(7, Inject12(BULLMQ_RESOLVED_CONFIG))
3326
4145
  ], JobWorkerOrchestrator);
3327
4146
  var JobWorkerModule = class {
3328
4147
  static forRoot(opts) {
@@ -3339,7 +4158,14 @@ var JobWorkerModule = class {
3339
4158
  { provide: JOB_WORKER_MODULE_OPTIONS, useValue: opts },
3340
4159
  JobWorkerOrchestrator
3341
4160
  ],
3342
- exports: []
4161
+ // BULLMQ-1 Phase 1 — export the options token so `BridgeModule`'s
4162
+ // reserved-pool guard (`onModuleInit`) can actually inject it.
4163
+ // Previously `exports: []` left the `@Optional()` inject resolving to
4164
+ // `undefined` and the guard silently no-opped (a dead check). With the
4165
+ // token exported the guard fires for real; consumers that omit the
4166
+ // reserved pools (and don't set `allPools`) now fail fast with
4167
+ // `BridgeReservedPoolsNotPolledError` — which is correct.
4168
+ exports: [JOB_WORKER_MODULE_OPTIONS]
3343
4169
  };
3344
4170
  }
3345
4171
  };
@@ -3355,8 +4181,8 @@ var CACHE_DEFAULT_TTL = /* @__PURE__ */ Symbol("CACHE_DEFAULT_TTL");
3355
4181
  import { Module as Module4 } from "@nestjs/common";
3356
4182
 
3357
4183
  // runtime/subsystems/cache/cache.drizzle-backend.ts
3358
- import { Injectable as Injectable13, Inject as Inject12, Optional as Optional5 } from "@nestjs/common";
3359
- import { gt as gt2, or, like, sql as sql6, eq as eq6 } from "drizzle-orm";
4184
+ import { Injectable as Injectable14, Inject as Inject13, Optional as Optional6 } from "@nestjs/common";
4185
+ import { gt as gt2, or as or3, like, sql as sql7, eq as eq8 } from "drizzle-orm";
3360
4186
 
3361
4187
  // runtime/subsystems/cache/cache.schema.ts
3362
4188
  import { pgTable as pgTable3, text as text3, jsonb as jsonb3, timestamp as timestamp3 } from "drizzle-orm/pg-core";
@@ -3399,7 +4225,7 @@ var DrizzleCacheService = class {
3399
4225
  async get(key) {
3400
4226
  try {
3401
4227
  const rows = await this.db.select().from(cacheEntries).where(
3402
- sql6`${cacheEntries.key} = ${key} AND (${cacheEntries.expiresAt} IS NULL OR ${cacheEntries.expiresAt} > now())`
4228
+ sql7`${cacheEntries.key} = ${key} AND (${cacheEntries.expiresAt} IS NULL OR ${cacheEntries.expiresAt} > now())`
3403
4229
  ).limit(1);
3404
4230
  if (rows.length === 0) return null;
3405
4231
  return rows[0].value;
@@ -3417,7 +4243,7 @@ var DrizzleCacheService = class {
3417
4243
  });
3418
4244
  }
3419
4245
  async delete(key) {
3420
- await this.db.delete(cacheEntries).where(eq6(cacheEntries.key, key));
4246
+ await this.db.delete(cacheEntries).where(eq8(cacheEntries.key, key));
3421
4247
  }
3422
4248
  async invalidateByPrefix(prefix) {
3423
4249
  const escaped = prefix.replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -3450,8 +4276,8 @@ var DrizzleCacheService = class {
3450
4276
  async deleteExpired() {
3451
4277
  try {
3452
4278
  await this.db.delete(cacheEntries).where(
3453
- or(
3454
- gt2(sql6`now()`, cacheEntries.expiresAt)
4279
+ or3(
4280
+ gt2(sql7`now()`, cacheEntries.expiresAt)
3455
4281
  )
3456
4282
  );
3457
4283
  } catch {
@@ -3459,14 +4285,14 @@ var DrizzleCacheService = class {
3459
4285
  }
3460
4286
  };
3461
4287
  DrizzleCacheService = __decorateClass([
3462
- Injectable13(),
3463
- __decorateParam(0, Inject12(DRIZZLE)),
3464
- __decorateParam(1, Optional5()),
3465
- __decorateParam(1, Inject12(CACHE_DEFAULT_TTL))
4288
+ Injectable14(),
4289
+ __decorateParam(0, Inject13(DRIZZLE)),
4290
+ __decorateParam(1, Optional6()),
4291
+ __decorateParam(1, Inject13(CACHE_DEFAULT_TTL))
3466
4292
  ], DrizzleCacheService);
3467
4293
 
3468
4294
  // runtime/subsystems/cache/cache.memory-backend.ts
3469
- import { Injectable as Injectable14, Inject as Inject13, Optional as Optional6 } from "@nestjs/common";
4295
+ import { Injectable as Injectable15, Inject as Inject14, Optional as Optional7 } from "@nestjs/common";
3470
4296
  var MemoryCacheService = class {
3471
4297
  constructor(defaultTtl = null) {
3472
4298
  this.defaultTtl = defaultTtl;
@@ -3540,9 +4366,9 @@ var MemoryCacheService = class {
3540
4366
  }
3541
4367
  };
3542
4368
  MemoryCacheService = __decorateClass([
3543
- Injectable14(),
3544
- __decorateParam(0, Optional6()),
3545
- __decorateParam(0, Inject13(CACHE_DEFAULT_TTL))
4369
+ Injectable15(),
4370
+ __decorateParam(0, Optional7()),
4371
+ __decorateParam(0, Inject14(CACHE_DEFAULT_TTL))
3546
4372
  ], MemoryCacheService);
3547
4373
 
3548
4374
  // runtime/subsystems/cache/cache.module.ts
@@ -3802,24 +4628,27 @@ var OBSERVABILITY_MODULE_OPTIONS = "OBSERVABILITY_MODULE_OPTIONS";
3802
4628
  import { Module as Module6 } from "@nestjs/common";
3803
4629
 
3804
4630
  // runtime/subsystems/observability/observability.service.ts
3805
- import { Inject as Inject14, Injectable as Injectable15, Optional as Optional7 } from "@nestjs/common";
4631
+ import { Inject as Inject15, Injectable as Injectable16, Optional as Optional8 } from "@nestjs/common";
3806
4632
 
3807
4633
  // runtime/subsystems/sync/sync.tokens.ts
3808
4634
  var SYNC_CURSOR_STORE = "SYNC_CURSOR_STORE";
3809
4635
  var SYNC_RUN_RECORDER = "SYNC_RUN_RECORDER";
3810
4636
 
3811
4637
  // runtime/subsystems/observability/observability.service.ts
4638
+ var MAX_TIMELINE_PAGES = 50;
3812
4639
  var ObservabilityService = class {
3813
- constructor(jobRuns2, bridge, syncRuns, cursors) {
4640
+ constructor(jobRuns2, bridge, syncRuns, cursors, events) {
3814
4641
  this.jobRuns = jobRuns2;
3815
4642
  this.bridge = bridge;
3816
4643
  this.syncRuns = syncRuns;
3817
4644
  this.cursors = cursors;
4645
+ this.events = events;
3818
4646
  }
3819
4647
  jobRuns;
3820
4648
  bridge;
3821
4649
  syncRuns;
3822
4650
  cursors;
4651
+ events;
3823
4652
  async getPoolDepths(tenantId) {
3824
4653
  if (!this.jobRuns) return [];
3825
4654
  return this.jobRuns.countByPoolAndStatus(tenantId);
@@ -3840,6 +4669,96 @@ var ObservabilityService = class {
3840
4669
  if (!this.cursors) return [];
3841
4670
  return this.cursors.listAll(tenantId);
3842
4671
  }
4672
+ async listJobRuns(query) {
4673
+ if (!this.jobRuns) {
4674
+ return { ...ObservabilityService.EMPTY_JOB_RUN_PAGE };
4675
+ }
4676
+ return this.jobRuns.listJobRuns(query);
4677
+ }
4678
+ async listEvents(query) {
4679
+ if (!this.events) {
4680
+ return { ...ObservabilityService.EMPTY_EVENT_PAGE };
4681
+ }
4682
+ return this.events.listEvents(query);
4683
+ }
4684
+ async getCorrelationTimeline(rootRunId, tenantId) {
4685
+ const runs = await this.collectRuns(rootRunId, tenantId);
4686
+ const events = await this.collectEvents(rootRunId, tenantId);
4687
+ const entries = [
4688
+ ...runs.map(
4689
+ (run) => ({
4690
+ kind: "job_run",
4691
+ at: run.createdAt,
4692
+ run
4693
+ })
4694
+ ),
4695
+ ...events.map(
4696
+ (event) => ({
4697
+ kind: "event",
4698
+ at: event.occurredAt,
4699
+ event
4700
+ })
4701
+ )
4702
+ ];
4703
+ entries.sort((a, b) => {
4704
+ const dt = a.at.getTime() - b.at.getTime();
4705
+ if (dt !== 0) return dt;
4706
+ if (a.kind === b.kind) return 0;
4707
+ return a.kind === "job_run" ? -1 : 1;
4708
+ });
4709
+ const startedAt = entries.length > 0 ? entries[0].at : null;
4710
+ const lastActivityAt = entries.length > 0 ? entries[entries.length - 1].at : null;
4711
+ return {
4712
+ rootRunId,
4713
+ entries,
4714
+ summary: {
4715
+ runCount: runs.length,
4716
+ eventCount: events.length,
4717
+ startedAt,
4718
+ lastActivityAt
4719
+ }
4720
+ };
4721
+ }
4722
+ /**
4723
+ * Drain every `job_run` sharing `rootRunId` by walking the keyset cursor.
4724
+ * Empty when the jobs subsystem is absent.
4725
+ */
4726
+ async collectRuns(rootRunId, tenantId) {
4727
+ if (!this.jobRuns) return [];
4728
+ const out = [];
4729
+ let cursor;
4730
+ for (let page = 0; page < MAX_TIMELINE_PAGES; page += 1) {
4731
+ const result = await this.jobRuns.listJobRuns({
4732
+ rootRunId,
4733
+ tenantId,
4734
+ cursor
4735
+ });
4736
+ out.push(...result.items);
4737
+ if (!result.nextCursor) break;
4738
+ cursor = result.nextCursor;
4739
+ }
4740
+ return out;
4741
+ }
4742
+ /**
4743
+ * Drain every `domain_event` whose `metadata.rootRunId` matches by walking
4744
+ * the keyset cursor. Empty when the events read port is absent.
4745
+ */
4746
+ async collectEvents(rootRunId, tenantId) {
4747
+ if (!this.events) return [];
4748
+ const out = [];
4749
+ let cursor;
4750
+ for (let page = 0; page < MAX_TIMELINE_PAGES; page += 1) {
4751
+ const result = await this.events.listEvents({
4752
+ rootRunId,
4753
+ tenantId,
4754
+ cursor
4755
+ });
4756
+ out.push(...result.items);
4757
+ if (!result.nextCursor) break;
4758
+ cursor = result.nextCursor;
4759
+ }
4760
+ return out;
4761
+ }
3843
4762
  };
3844
4763
  /**
3845
4764
  * All-zero histogram used when the bridge subsystem is absent. Matches
@@ -3852,23 +4771,34 @@ __publicField(ObservabilityService, "EMPTY_HISTOGRAM", {
3852
4771
  skipped: 0,
3853
4772
  failed: 0
3854
4773
  });
4774
+ /** Empty page used when a sibling read port is absent. */
4775
+ __publicField(ObservabilityService, "EMPTY_JOB_RUN_PAGE", {
4776
+ items: [],
4777
+ nextCursor: null
4778
+ });
4779
+ __publicField(ObservabilityService, "EMPTY_EVENT_PAGE", {
4780
+ items: [],
4781
+ nextCursor: null
4782
+ });
3855
4783
  ObservabilityService = __decorateClass([
3856
- Injectable15(),
3857
- __decorateParam(0, Optional7()),
3858
- __decorateParam(0, Inject14(JOB_RUN_SERVICE)),
3859
- __decorateParam(1, Optional7()),
3860
- __decorateParam(1, Inject14(BRIDGE_DELIVERY_REPO)),
3861
- __decorateParam(2, Optional7()),
3862
- __decorateParam(2, Inject14(SYNC_RUN_RECORDER)),
3863
- __decorateParam(3, Optional7()),
3864
- __decorateParam(3, Inject14(SYNC_CURSOR_STORE))
4784
+ Injectable16(),
4785
+ __decorateParam(0, Optional8()),
4786
+ __decorateParam(0, Inject15(JOB_RUN_SERVICE)),
4787
+ __decorateParam(1, Optional8()),
4788
+ __decorateParam(1, Inject15(BRIDGE_DELIVERY_REPO)),
4789
+ __decorateParam(2, Optional8()),
4790
+ __decorateParam(2, Inject15(SYNC_RUN_RECORDER)),
4791
+ __decorateParam(3, Optional8()),
4792
+ __decorateParam(3, Inject15(SYNC_CURSOR_STORE)),
4793
+ __decorateParam(4, Optional8()),
4794
+ __decorateParam(4, Inject15(EVENT_READ_PORT))
3865
4795
  ], ObservabilityService);
3866
4796
 
3867
4797
  // runtime/subsystems/observability/reporters/bridge-metrics.reporter.ts
3868
4798
  import {
3869
- Inject as Inject15,
3870
- Injectable as Injectable16,
3871
- Logger as Logger8
4799
+ Inject as Inject16,
4800
+ Injectable as Injectable17,
4801
+ Logger as Logger10
3872
4802
  } from "@nestjs/common";
3873
4803
  var BridgeMetricsReporter = class {
3874
4804
  constructor(observability, options) {
@@ -3876,7 +4806,7 @@ var BridgeMetricsReporter = class {
3876
4806
  this.config = options.reporters?.bridgeMetrics;
3877
4807
  }
3878
4808
  observability;
3879
- logger = new Logger8(BridgeMetricsReporter.name);
4809
+ logger = new Logger10(BridgeMetricsReporter.name);
3880
4810
  handle = null;
3881
4811
  config;
3882
4812
  onModuleInit() {
@@ -3927,9 +4857,9 @@ var BridgeMetricsReporter = class {
3927
4857
  }
3928
4858
  };
3929
4859
  BridgeMetricsReporter = __decorateClass([
3930
- Injectable16(),
3931
- __decorateParam(0, Inject15(OBSERVABILITY)),
3932
- __decorateParam(1, Inject15(OBSERVABILITY_MODULE_OPTIONS))
4860
+ Injectable17(),
4861
+ __decorateParam(0, Inject16(OBSERVABILITY)),
4862
+ __decorateParam(1, Inject16(OBSERVABILITY_MODULE_OPTIONS))
3933
4863
  ], BridgeMetricsReporter);
3934
4864
 
3935
4865
  // runtime/subsystems/observability/observability.module.ts
@@ -4223,7 +5153,7 @@ var MemoryOAuthStateStore = class {
4223
5153
 
4224
5154
  // runtime/subsystems/auth/backends/state-store.drizzle-backend.ts
4225
5155
  import { randomBytes as randomBytes3 } from "crypto";
4226
- import { eq as eq7 } from "drizzle-orm";
5156
+ import { eq as eq9 } from "drizzle-orm";
4227
5157
  var DrizzleOAuthStateStore = class {
4228
5158
  constructor(db, opts = {}) {
4229
5159
  this.db = db;
@@ -4247,7 +5177,7 @@ var DrizzleOAuthStateStore = class {
4247
5177
  return state;
4248
5178
  }
4249
5179
  async consume(state) {
4250
- const rows = await this.db.delete(authOAuthState).where(eq7(authOAuthState.state, state)).returning();
5180
+ const rows = await this.db.delete(authOAuthState).where(eq9(authOAuthState.state, state)).returning();
4251
5181
  const row = rows[0];
4252
5182
  if (!row) {
4253
5183
  throw new OAuthStateError(
@@ -4269,7 +5199,7 @@ var DrizzleOAuthStateStore = class {
4269
5199
  import {
4270
5200
  Controller,
4271
5201
  Get,
4272
- Inject as Inject16,
5202
+ Inject as Inject17,
4273
5203
  Param,
4274
5204
  Query,
4275
5205
  Req,
@@ -4371,11 +5301,11 @@ __decorateClass([
4371
5301
  ], AuthController.prototype, "callback", 1);
4372
5302
  AuthController = __decorateClass([
4373
5303
  Controller("auth"),
4374
- __decorateParam(0, Inject16(STRATEGY_REGISTRY)),
4375
- __decorateParam(1, Inject16(AUTH_USER_CONTEXT)),
4376
- __decorateParam(2, Inject16(OAUTH_STATE_STORE)),
4377
- __decorateParam(3, Inject16(AUTH_INTEGRATION_GRANT_SINK)),
4378
- __decorateParam(4, Inject16(AUTH_OPTIONS))
5304
+ __decorateParam(0, Inject17(STRATEGY_REGISTRY)),
5305
+ __decorateParam(1, Inject17(AUTH_USER_CONTEXT)),
5306
+ __decorateParam(2, Inject17(OAUTH_STATE_STORE)),
5307
+ __decorateParam(3, Inject17(AUTH_INTEGRATION_GRANT_SINK)),
5308
+ __decorateParam(4, Inject17(AUTH_OPTIONS))
4379
5309
  ], AuthController);
4380
5310
 
4381
5311
  // runtime/base-classes/tenant-context.ts