@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
@@ -12,18 +12,18 @@ var __decorateParam = (index4, decorator) => (target, key2) => decorator(target,
12
12
 
13
13
  // runtime/subsystems/bridge/bridge.module.ts
14
14
  import {
15
- Inject as Inject12,
15
+ Inject as Inject13,
16
16
  Module as Module3,
17
- Optional as Optional7
17
+ Optional as Optional8
18
18
  } from "@nestjs/common";
19
19
 
20
20
  // runtime/subsystems/jobs/job-worker.module.ts
21
21
  import {
22
- Inject as Inject7,
23
- Injectable as Injectable8,
24
- Logger as Logger4,
22
+ Inject as Inject8,
23
+ Injectable as Injectable9,
24
+ Logger as Logger6,
25
25
  Module as Module2,
26
- Optional as Optional2
26
+ Optional as Optional3
27
27
  } from "@nestjs/common";
28
28
 
29
29
  // runtime/constants/tokens.ts
@@ -642,7 +642,58 @@ function notInStatus(statuses) {
642
642
 
643
643
  // runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
644
644
  import { Inject as Inject2, Injectable as Injectable2 } from "@nestjs/common";
645
- import { and as and2, asc, desc as desc2, eq as eq2, inArray as inArray2, isNull, sql as sql3 } from "drizzle-orm";
645
+ import { and as and2, asc, desc as desc2, eq as eq2, gte, inArray as inArray2, isNull, lt, or, sql as sql3 } from "drizzle-orm";
646
+
647
+ // runtime/subsystems/jobs/job-run-keyset-cursor.ts
648
+ var DEFAULT_LIST_LIMIT = 50;
649
+ var MAX_LIST_LIMIT = 200;
650
+ function clampLimit(limit) {
651
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
652
+ return DEFAULT_LIST_LIMIT;
653
+ }
654
+ const floored = Math.floor(limit);
655
+ if (floored < 1) return 1;
656
+ if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
657
+ return floored;
658
+ }
659
+ function encodeKeysetCursor(keyset) {
660
+ const tuple = [keyset.createdAt.toISOString(), keyset.id];
661
+ return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
662
+ }
663
+ function decodeKeysetCursor(cursor) {
664
+ try {
665
+ const json = Buffer.from(cursor, "base64url").toString("utf8");
666
+ const parsed = JSON.parse(json);
667
+ if (!Array.isArray(parsed) || parsed.length !== 2) return null;
668
+ const [iso, id] = parsed;
669
+ if (typeof iso !== "string" || typeof id !== "string") return null;
670
+ const createdAt = new Date(iso);
671
+ if (Number.isNaN(createdAt.getTime())) return null;
672
+ return { createdAt, id };
673
+ } catch {
674
+ return null;
675
+ }
676
+ }
677
+ function toJobRunSummary(r) {
678
+ return {
679
+ runId: r.id,
680
+ rootRunId: r.rootRunId,
681
+ jobType: r.jobType,
682
+ pool: r.pool,
683
+ status: r.status,
684
+ scopeEntityType: r.scopeEntityType,
685
+ scopeEntityId: r.scopeEntityId,
686
+ tenantId: r.tenantId,
687
+ attempts: r.attempts,
688
+ errorMessage: r.error?.message ?? null,
689
+ runAt: r.runAt,
690
+ startedAt: r.startedAt,
691
+ finishedAt: r.finishedAt,
692
+ createdAt: r.createdAt
693
+ };
694
+ }
695
+
696
+ // runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
646
697
  var NON_TERMINAL_STATUSES = [
647
698
  "pending",
648
699
  "running",
@@ -765,6 +816,37 @@ var DrizzleJobRunService = class {
765
816
  createdAt: r.createdAt
766
817
  }));
767
818
  }
819
+ async listJobRuns(query = {}) {
820
+ const limit = clampLimit(query.limit);
821
+ const conditions = [];
822
+ const tenantCond = this.tenantCondition("listJobRuns", query.tenantId);
823
+ if (tenantCond) conditions.push(tenantCond);
824
+ if (query.poolId) conditions.push(eq2(jobRuns.pool, query.poolId));
825
+ if (query.rootRunId) conditions.push(eq2(jobRuns.rootRunId, query.rootRunId));
826
+ if (query.status) conditions.push(eq2(jobRuns.status, query.status));
827
+ if (query.since) conditions.push(gte(jobRuns.createdAt, query.since));
828
+ if (query.cursor) {
829
+ const keyset = decodeKeysetCursor(query.cursor);
830
+ if (keyset) {
831
+ conditions.push(
832
+ or(
833
+ lt(jobRuns.createdAt, keyset.createdAt),
834
+ and2(
835
+ eq2(jobRuns.createdAt, keyset.createdAt),
836
+ lt(jobRuns.id, keyset.id)
837
+ )
838
+ )
839
+ );
840
+ }
841
+ }
842
+ const rows = await this.db.select().from(jobRuns).where(conditions.length > 0 ? and2(...conditions) : void 0).orderBy(desc2(jobRuns.createdAt), desc2(jobRuns.id)).limit(limit + 1);
843
+ const hasMore = rows.length > limit;
844
+ const page = hasMore ? rows.slice(0, limit) : rows;
845
+ const items = page.map(toJobRunSummary);
846
+ const last = page[page.length - 1];
847
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
848
+ return { items, nextCursor };
849
+ }
768
850
  /**
769
851
  * Internal helper used by cascade paths (not on the public protocol).
770
852
  * Exposed as a public method on the concrete class so infrastructure
@@ -826,9 +908,394 @@ DrizzleJobStepService = __decorateClass([
826
908
  __decorateParam(0, Inject3(DRIZZLE))
827
909
  ], DrizzleJobStepService);
828
910
 
911
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
912
+ import { createHash } from "crypto";
913
+ import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
914
+ import { eq as eq4 } from "drizzle-orm";
915
+
916
+ // runtime/subsystems/jobs/pool-config.loader.ts
917
+ import { existsSync, readFileSync } from "fs";
918
+ import { resolve } from "path";
919
+ import { parse as parseYaml } from "yaml";
920
+ var FRAMEWORK_POOLS = Object.freeze({
921
+ events_inbound: Object.freeze({
922
+ queue: "jobs-events-inbound",
923
+ concurrency: 20,
924
+ reserved: true,
925
+ description: "Inbound events drain (events subsystem outbox)."
926
+ }),
927
+ events_change: Object.freeze({
928
+ queue: "jobs-events-change",
929
+ concurrency: 30,
930
+ reserved: true,
931
+ description: "Change events drain (events subsystem outbox)."
932
+ }),
933
+ events_outbound: Object.freeze({
934
+ queue: "jobs-events-outbound",
935
+ concurrency: 10,
936
+ reserved: true,
937
+ description: "Outbound events drain (events subsystem outbox)."
938
+ }),
939
+ interactive: Object.freeze({
940
+ queue: "jobs-interactive",
941
+ concurrency: 20,
942
+ reserved: false,
943
+ description: "User-facing latency-sensitive jobs."
944
+ }),
945
+ batch: Object.freeze({
946
+ queue: "jobs-batch",
947
+ concurrency: 5,
948
+ reserved: false,
949
+ description: "Default pool for background jobs."
950
+ })
951
+ });
952
+ var RESERVED_POOL_NAMES = new Set(
953
+ Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
954
+ );
955
+ var cache = /* @__PURE__ */ new Map();
956
+ function loadPoolConfig(configPath) {
957
+ const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
958
+ const cached = cache.get(resolved);
959
+ if (cached) return cached;
960
+ const merged = /* @__PURE__ */ new Map();
961
+ for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
962
+ merged.set(name, { ...def });
963
+ }
964
+ if (!existsSync(resolved)) {
965
+ cache.set(resolved, merged);
966
+ return merged;
967
+ }
968
+ let raw;
969
+ try {
970
+ raw = parseYaml(readFileSync(resolved, "utf8"));
971
+ } catch (err) {
972
+ throw new Error(
973
+ `pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
974
+ );
975
+ }
976
+ const userPools = extractUserPools(raw);
977
+ for (const [name, userDef] of Object.entries(userPools)) {
978
+ const existing = merged.get(name);
979
+ if (existing) {
980
+ const next = {
981
+ queue: existing.queue,
982
+ concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
983
+ reserved: existing.reserved,
984
+ description: userDef.description ?? existing.description
985
+ };
986
+ merged.set(name, next);
987
+ continue;
988
+ }
989
+ if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
990
+ throw new Error(
991
+ `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
992
+ );
993
+ }
994
+ if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
995
+ throw new Error(
996
+ `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
997
+ );
998
+ }
999
+ if (userDef.reserved === true) {
1000
+ throw new Error(
1001
+ `pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
1002
+ );
1003
+ }
1004
+ merged.set(name, {
1005
+ queue: userDef.queue,
1006
+ concurrency: userDef.concurrency,
1007
+ reserved: false,
1008
+ description: userDef.description
1009
+ });
1010
+ }
1011
+ cache.set(resolved, merged);
1012
+ return merged;
1013
+ }
1014
+ function allNonReservedPoolNames(config) {
1015
+ const out = [];
1016
+ for (const [name, def] of config) {
1017
+ if (!def.reserved) out.push(name);
1018
+ }
1019
+ return out;
1020
+ }
1021
+ function allPoolNames(config) {
1022
+ return [...config.keys()];
1023
+ }
1024
+ function extractUserPools(raw) {
1025
+ if (!raw || typeof raw !== "object") return {};
1026
+ const jobs2 = raw.jobs;
1027
+ if (!jobs2 || typeof jobs2 !== "object") return {};
1028
+ const pools = jobs2.pools;
1029
+ if (!pools || typeof pools !== "object") return {};
1030
+ const out = {};
1031
+ for (const [name, def] of Object.entries(pools)) {
1032
+ if (!def || typeof def !== "object") continue;
1033
+ out[name] = def;
1034
+ }
1035
+ return out;
1036
+ }
1037
+
1038
+ // runtime/subsystems/jobs/bullmq.config.ts
1039
+ var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
1040
+ var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
1041
+ var DEFAULT_REDIS_URL = "redis://localhost:6379";
1042
+ var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
1043
+ function resolveBullMqConfig(ext) {
1044
+ const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
1045
+ const resolved = {
1046
+ connection: { url },
1047
+ queuePrefix: ext?.queue_prefix
1048
+ };
1049
+ if (ext?.bull_board?.enabled) {
1050
+ resolved.bullBoard = {
1051
+ enabled: true,
1052
+ mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
1053
+ };
1054
+ }
1055
+ return resolved;
1056
+ }
1057
+ function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
1058
+ const alias = poolConfig.get(pool)?.queue ?? pool;
1059
+ const prefix = config?.queuePrefix;
1060
+ return prefix ? `${prefix}:${alias}` : alias;
1061
+ }
1062
+
1063
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
1064
+ function sha1JobId(idempotencyKey) {
1065
+ return createHash("sha1").update(idempotencyKey).digest("hex");
1066
+ }
1067
+ var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
1068
+ constructor(db, multiTenant, connection, bullConfig = null) {
1069
+ super(db, multiTenant);
1070
+ this.connection = connection;
1071
+ this.bullConfig = bullConfig;
1072
+ this.bullDb = db;
1073
+ }
1074
+ connection;
1075
+ bullConfig;
1076
+ // TODO(logging-subsystem): swap to ILogger once ADR-028 lands
1077
+ bullLogger = new Logger2(BullMQJobOrchestrator.name);
1078
+ /** Lazily-opened `Queue` handles, one per pool. */
1079
+ queues = /* @__PURE__ */ new Map();
1080
+ /** Single FlowProducer for parent/child hierarchies. Lazily opened. */
1081
+ _flow = null;
1082
+ /**
1083
+ * Cached `bullmq` value constructors, populated by `loadBullMq()` on first
1084
+ * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
1085
+ * a queue). Kept off the import graph so a `drizzle`-only consumer never
1086
+ * resolves the optional `'bullmq'` package.
1087
+ */
1088
+ QueueCtor = null;
1089
+ FlowProducerCtor = null;
1090
+ bullMqLoad = null;
1091
+ /**
1092
+ * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
1093
+ * `private` (can't be redeclared even privately in a subclass), and the
1094
+ * spec forbids touching that file — so the subclass keeps its own handle
1095
+ * under a distinct name (same instance, passed through to `super`) for the
1096
+ * cancel-cascade snapshot + definition/run loads below.
1097
+ */
1098
+ bullDb;
1099
+ /**
1100
+ * Lazily load the optional `bullmq` package and cache its value
1101
+ * constructors. Idempotent (single in-flight promise). Throws a friendly,
1102
+ * actionable error when the consumer selected `backend: 'bullmq'` but did
1103
+ * not install the package — mirrors `createRedisClient` in the redis event
1104
+ * backend. Must be `await`ed before any `queueFor`/`flow` access.
1105
+ */
1106
+ async loadBullMq() {
1107
+ if (this.QueueCtor && this.FlowProducerCtor) return;
1108
+ if (!this.bullMqLoad) {
1109
+ this.bullMqLoad = (async () => {
1110
+ try {
1111
+ const mod = await import("bullmq");
1112
+ this.QueueCtor = mod.Queue;
1113
+ this.FlowProducerCtor = mod.FlowProducer;
1114
+ } catch {
1115
+ throw new Error(
1116
+ 'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
1117
+ );
1118
+ }
1119
+ })();
1120
+ }
1121
+ await this.bullMqLoad;
1122
+ }
1123
+ /**
1124
+ * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
1125
+ * loadBullMq()` first so `QueueCtor` is populated.
1126
+ */
1127
+ queueFor(pool) {
1128
+ if (!this.QueueCtor) {
1129
+ throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
1130
+ }
1131
+ const name = resolvePoolQueueName(pool, this.bullConfig);
1132
+ let q = this.queues.get(name);
1133
+ if (!q) {
1134
+ q = new this.QueueCtor(name, { connection: this.connection });
1135
+ this.queues.set(name, q);
1136
+ }
1137
+ return q;
1138
+ }
1139
+ flow() {
1140
+ if (!this.FlowProducerCtor) {
1141
+ throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
1142
+ }
1143
+ if (!this._flow) {
1144
+ this._flow = new this.FlowProducerCtor({ connection: this.connection });
1145
+ }
1146
+ return this._flow;
1147
+ }
1148
+ // ==========================================================================
1149
+ // start — Postgres insert (super) + BullMQ dispatch
1150
+ // ==========================================================================
1151
+ async start(type, input, opts = {}, tx) {
1152
+ const run = await super.start(type, input, opts, tx);
1153
+ await this.dispatch(run, type);
1154
+ return run;
1155
+ }
1156
+ /**
1157
+ * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
1158
+ * `parentRunId` we attach it to the parent's existing BullMQ job through the
1159
+ * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
1160
+ * its own graph. (The FlowProducer is reserved for whole-tree atomic
1161
+ * submits, exposed as an opt-in extension via `flowProducer()`; runtime
1162
+ * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
1163
+ * correct primitive here.)
1164
+ *
1165
+ * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
1166
+ * present (so the same logical key dedups), else the `job_run.id` UUID
1167
+ * (already colon-free).
1168
+ *
1169
+ * The domain `parentClosePolicy` cascade is still enforced in Postgres by
1170
+ * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
1171
+ * not the authority.
1172
+ */
1173
+ async dispatch(run, type) {
1174
+ await this.loadBullMq();
1175
+ const def = await this.loadDefinition(type);
1176
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
1177
+ const jobOpts = {
1178
+ jobId,
1179
+ ...this.retryOpts(def),
1180
+ ...this.dedupeOpts(run, def)
1181
+ };
1182
+ if (run.parentRunId) {
1183
+ const parentRow = await this.loadRun(run.parentRunId);
1184
+ if (parentRow) {
1185
+ const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
1186
+ jobOpts.parent = {
1187
+ id: parentJobId,
1188
+ queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
1189
+ };
1190
+ }
1191
+ }
1192
+ const payload = { runId: run.id, type, input: run.input };
1193
+ await this.queueFor(run.pool).add(type, payload, jobOpts);
1194
+ }
1195
+ /**
1196
+ * Opt-in extension (spec §Extensions): expose the FlowProducer for
1197
+ * consumers that want to submit a whole parent/child DAG atomically up
1198
+ * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
1199
+ * code using it is not portable to the Drizzle backend. Async because it
1200
+ * lazily loads the optional `bullmq` package on first use.
1201
+ */
1202
+ async flowProducer() {
1203
+ await this.loadBullMq();
1204
+ return this.flow();
1205
+ }
1206
+ retryOpts(def) {
1207
+ const policy = def.retryPolicy;
1208
+ if (!policy) return {};
1209
+ return {
1210
+ attempts: policy.attempts,
1211
+ backoff: {
1212
+ type: policy.backoff === "exponential" ? "exponential" : "fixed",
1213
+ delay: policy.baseMs
1214
+ }
1215
+ };
1216
+ }
1217
+ dedupeOpts(run, def) {
1218
+ if (!run.dedupeKey || !def.dedupeWindowMs) return {};
1219
+ return {
1220
+ deduplication: {
1221
+ id: sha1JobId(run.dedupeKey),
1222
+ ttl: def.dedupeWindowMs
1223
+ }
1224
+ };
1225
+ }
1226
+ // ==========================================================================
1227
+ // cancel — Postgres cascade (super) + remove from queue
1228
+ // ==========================================================================
1229
+ async cancel(runId, opts = {}) {
1230
+ const target = await this.loadRun(runId);
1231
+ await super.cancel(runId, opts);
1232
+ if (!target) return;
1233
+ await this.loadBullMq();
1234
+ await this.removeFromQueue(target);
1235
+ if (opts.cascade === false) return;
1236
+ const descendants = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.rootRunId, target.rootRunId));
1237
+ for (const child of descendants) {
1238
+ if (child.id === runId) continue;
1239
+ await this.removeFromQueue(child);
1240
+ }
1241
+ }
1242
+ async removeFromQueue(run) {
1243
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
1244
+ try {
1245
+ const job = await this.queueFor(run.pool).getJob(jobId);
1246
+ if (job) await job.remove();
1247
+ } catch (err) {
1248
+ this.bullLogger.warn(
1249
+ `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
1250
+ );
1251
+ }
1252
+ }
1253
+ // ==========================================================================
1254
+ // replay — Postgres reset (super) + re-enqueue
1255
+ // ==========================================================================
1256
+ async replay(runId) {
1257
+ const run = await super.replay(runId);
1258
+ await this.dispatch(run, run.jobType);
1259
+ return run;
1260
+ }
1261
+ // ==========================================================================
1262
+ // Internals
1263
+ // ==========================================================================
1264
+ async loadDefinition(type) {
1265
+ const [def] = await this.bullDb.select().from(jobs).where(eq4(jobs.type, type)).limit(1);
1266
+ if (!def) {
1267
+ throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
1268
+ }
1269
+ return def;
1270
+ }
1271
+ async loadRun(id) {
1272
+ const [row] = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.id, id)).limit(1);
1273
+ return row ?? null;
1274
+ }
1275
+ /** Close all open queue + flow connections. Called on module destroy. */
1276
+ async closeConnections() {
1277
+ for (const q of this.queues.values()) {
1278
+ await q.close().catch(() => void 0);
1279
+ }
1280
+ this.queues.clear();
1281
+ if (this._flow) {
1282
+ await this._flow.close().catch(() => void 0);
1283
+ this._flow = null;
1284
+ }
1285
+ }
1286
+ };
1287
+ BullMQJobOrchestrator = __decorateClass([
1288
+ Injectable4(),
1289
+ __decorateParam(0, Inject4(DRIZZLE)),
1290
+ __decorateParam(1, Inject4(JOBS_MULTI_TENANT)),
1291
+ __decorateParam(2, Inject4(BULLMQ_CONNECTION)),
1292
+ __decorateParam(3, Optional()),
1293
+ __decorateParam(3, Inject4(BULLMQ_RESOLVED_CONFIG))
1294
+ ], BullMQJobOrchestrator);
1295
+
829
1296
  // runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
830
1297
  import { randomUUID as randomUUID2 } from "crypto";
831
- import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
1298
+ import { Inject as Inject5, Injectable as Injectable5, Logger as Logger3, Optional as Optional2 } from "@nestjs/common";
832
1299
  var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
833
1300
  var TERMINAL_STATUSES2 = [
834
1301
  "completed",
@@ -875,7 +1342,7 @@ var MemoryJobOrchestrator = class {
875
1342
  stepService;
876
1343
  multiTenant;
877
1344
  moduleRef;
878
- logger = new Logger2(MemoryJobOrchestrator.name);
1345
+ logger = new Logger3(MemoryJobOrchestrator.name);
879
1346
  mutex = new PromiseMutex();
880
1347
  handlerRegistry = /* @__PURE__ */ new Map();
881
1348
  /**
@@ -1224,7 +1691,7 @@ var MemoryJobOrchestrator = class {
1224
1691
  run,
1225
1692
  step: this.makeStepFn(run),
1226
1693
  spawnChild: this.makeSpawnFn(run),
1227
- logger: new Logger2(`JobRun:${run.id}`)
1694
+ logger: new Logger3(`JobRun:${run.id}`)
1228
1695
  };
1229
1696
  const attemptsBefore = run.attempts ?? 0;
1230
1697
  try {
@@ -1397,9 +1864,9 @@ var MemoryJobOrchestrator = class {
1397
1864
  }
1398
1865
  };
1399
1866
  MemoryJobOrchestrator = __decorateClass([
1400
- Injectable4(),
1401
- __decorateParam(2, Inject4(JOBS_MULTI_TENANT)),
1402
- __decorateParam(3, Optional())
1867
+ Injectable5(),
1868
+ __decorateParam(2, Inject5(JOBS_MULTI_TENANT)),
1869
+ __decorateParam(3, Optional2())
1403
1870
  ], MemoryJobOrchestrator);
1404
1871
  function classifyError(err, policy, currentAttempts) {
1405
1872
  if (!policy) return "fail";
@@ -1433,7 +1900,7 @@ function serialiseError(err, attempt, retryable) {
1433
1900
  }
1434
1901
 
1435
1902
  // runtime/subsystems/jobs/job-run-service.memory-backend.ts
1436
- import { Inject as Inject5, Injectable as Injectable5 } from "@nestjs/common";
1903
+ import { Inject as Inject6, Injectable as Injectable6 } from "@nestjs/common";
1437
1904
  var NON_TERMINAL_STATUSES2 = [
1438
1905
  "pending",
1439
1906
  "running",
@@ -1550,6 +2017,38 @@ var MemoryJobRunService = class {
1550
2017
  createdAt: r.createdAt
1551
2018
  }));
1552
2019
  }
2020
+ async listJobRuns(query = {}) {
2021
+ const limit = clampLimit(query.limit);
2022
+ const tenantCheck = this.tenantPredicate("listJobRuns", query.tenantId);
2023
+ const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;
2024
+ const matched = [];
2025
+ for (const r of this.store.runs.values()) {
2026
+ if (tenantCheck && !tenantCheck(r)) continue;
2027
+ if (query.poolId && r.pool !== query.poolId) continue;
2028
+ if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;
2029
+ if (query.status && r.status !== query.status) continue;
2030
+ if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;
2031
+ matched.push(r);
2032
+ }
2033
+ matched.sort((a, b) => {
2034
+ const dt = b.createdAt.getTime() - a.createdAt.getTime();
2035
+ if (dt !== 0) return dt;
2036
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
2037
+ });
2038
+ const seeked = keyset ? matched.filter((r) => {
2039
+ const ct = r.createdAt.getTime();
2040
+ const kt = keyset.createdAt.getTime();
2041
+ if (ct < kt) return true;
2042
+ if (ct > kt) return false;
2043
+ return r.id < keyset.id;
2044
+ }) : matched;
2045
+ const hasMore = seeked.length > limit;
2046
+ const page = hasMore ? seeked.slice(0, limit) : seeked;
2047
+ const items = page.map(toJobRunSummary);
2048
+ const last = page[page.length - 1];
2049
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
2050
+ return { items, nextCursor };
2051
+ }
1553
2052
  /**
1554
2053
  * Direct lookup. Not on the protocol — concrete-class convenience for
1555
2054
  * tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
@@ -1568,9 +2067,9 @@ var MemoryJobRunService = class {
1568
2067
  }
1569
2068
  };
1570
2069
  MemoryJobRunService = __decorateClass([
1571
- Injectable5(),
1572
- __decorateParam(1, Inject5(JOB_ORCHESTRATOR)),
1573
- __decorateParam(2, Inject5(JOBS_MULTI_TENANT))
2070
+ Injectable6(),
2071
+ __decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
2072
+ __decorateParam(2, Inject6(JOBS_MULTI_TENANT))
1574
2073
  ], MemoryJobRunService);
1575
2074
  function compareBy(a, b, order) {
1576
2075
  switch (order) {
@@ -1588,7 +2087,7 @@ function compareBy(a, b, order) {
1588
2087
 
1589
2088
  // runtime/subsystems/jobs/job-step-service.memory-backend.ts
1590
2089
  import { randomUUID as randomUUID3 } from "crypto";
1591
- import { Injectable as Injectable6 } from "@nestjs/common";
2090
+ import { Injectable as Injectable7 } from "@nestjs/common";
1592
2091
  var MemoryJobStepService = class {
1593
2092
  constructor(store) {
1594
2093
  this.store = store;
@@ -1677,195 +2176,86 @@ var MemoryJobStepService = class {
1677
2176
  for (const r of rows) {
1678
2177
  if (r.seq > max) max = r.seq;
1679
2178
  }
1680
- return max + 1;
1681
- }
1682
- };
1683
- MemoryJobStepService = __decorateClass([
1684
- Injectable6()
1685
- ], MemoryJobStepService);
1686
-
1687
- // runtime/subsystems/jobs/memory-job-store.ts
1688
- var MemoryJobStore = class {
1689
- /** Runs keyed by `id` (single source of truth for status/scope/lineage). */
1690
- runs = /* @__PURE__ */ new Map();
1691
- /** Steps keyed by `job_run_id`; array order matches insertion order. */
1692
- steps = /* @__PURE__ */ new Map();
1693
- /** Job definitions keyed by `type` — memory mirror of the `job` table. */
1694
- jobs = /* @__PURE__ */ new Map();
1695
- /** Reset everything. Tests call this in `beforeEach`. */
1696
- clear() {
1697
- this.runs.clear();
1698
- this.steps.clear();
1699
- this.jobs.clear();
1700
- }
1701
- };
1702
-
1703
- // runtime/subsystems/jobs/jobs-domain.module.ts
1704
- var JobsDomainModule = class {
1705
- static forRoot(opts) {
1706
- void opts.extensions;
1707
- const multiTenant = opts.multiTenant ?? false;
1708
- const providers = [
1709
- // JOB-8 — boolean provider consumed by the four service-layer backends.
1710
- // Always provided (even when `multiTenant === false`) so `@Inject`
1711
- // always resolves; backends short-circuit the enforcement path when
1712
- // the value is `false`. See `jobs-domain.tokens.ts` for the claim-loop
1713
- // cross-tenant-by-design decision.
1714
- { provide: JOBS_MULTI_TENANT, useValue: multiTenant }
1715
- ];
1716
- if (opts.backend === "memory") {
1717
- const store = new MemoryJobStore();
1718
- providers.push({ provide: MemoryJobStore, useValue: store });
1719
- providers.push(MemoryJobStepService);
1720
- providers.push({ provide: JOB_STEP_SERVICE, useExisting: MemoryJobStepService });
1721
- providers.push(MemoryJobOrchestrator);
1722
- providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
1723
- providers.push(MemoryJobRunService);
1724
- providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
1725
- } else {
1726
- providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });
1727
- providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
1728
- providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
1729
- }
1730
- return {
1731
- module: JobsDomainModule,
1732
- global: true,
1733
- providers,
1734
- exports: [
1735
- JOB_ORCHESTRATOR,
1736
- JOB_RUN_SERVICE,
1737
- JOB_STEP_SERVICE,
1738
- JOBS_MULTI_TENANT
1739
- ]
1740
- };
1741
- }
1742
- };
1743
- JobsDomainModule = __decorateClass([
1744
- Module({})
1745
- ], JobsDomainModule);
1746
-
1747
- // runtime/subsystems/jobs/pool-config.loader.ts
1748
- import { existsSync, readFileSync } from "fs";
1749
- import { resolve } from "path";
1750
- import { parse as parseYaml } from "yaml";
1751
- var FRAMEWORK_POOLS = Object.freeze({
1752
- events_inbound: Object.freeze({
1753
- queue: "jobs-events-inbound",
1754
- concurrency: 20,
1755
- reserved: true,
1756
- description: "Inbound events drain (events subsystem outbox)."
1757
- }),
1758
- events_change: Object.freeze({
1759
- queue: "jobs-events-change",
1760
- concurrency: 30,
1761
- reserved: true,
1762
- description: "Change events drain (events subsystem outbox)."
1763
- }),
1764
- events_outbound: Object.freeze({
1765
- queue: "jobs-events-outbound",
1766
- concurrency: 10,
1767
- reserved: true,
1768
- description: "Outbound events drain (events subsystem outbox)."
1769
- }),
1770
- interactive: Object.freeze({
1771
- queue: "jobs-interactive",
1772
- concurrency: 20,
1773
- reserved: false,
1774
- description: "User-facing latency-sensitive jobs."
1775
- }),
1776
- batch: Object.freeze({
1777
- queue: "jobs-batch",
1778
- concurrency: 5,
1779
- reserved: false,
1780
- description: "Default pool for background jobs."
1781
- })
1782
- });
1783
- var RESERVED_POOL_NAMES = new Set(
1784
- Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
1785
- );
1786
- var cache = /* @__PURE__ */ new Map();
1787
- function loadPoolConfig(configPath) {
1788
- const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
1789
- const cached = cache.get(resolved);
1790
- if (cached) return cached;
1791
- const merged = /* @__PURE__ */ new Map();
1792
- for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
1793
- merged.set(name, { ...def });
1794
- }
1795
- if (!existsSync(resolved)) {
1796
- cache.set(resolved, merged);
1797
- return merged;
1798
- }
1799
- let raw;
1800
- try {
1801
- raw = parseYaml(readFileSync(resolved, "utf8"));
1802
- } catch (err) {
1803
- throw new Error(
1804
- `pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
1805
- );
1806
- }
1807
- const userPools = extractUserPools(raw);
1808
- for (const [name, userDef] of Object.entries(userPools)) {
1809
- const existing = merged.get(name);
1810
- if (existing) {
1811
- const next = {
1812
- queue: existing.queue,
1813
- concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
1814
- reserved: existing.reserved,
1815
- description: userDef.description ?? existing.description
1816
- };
1817
- merged.set(name, next);
1818
- continue;
1819
- }
1820
- if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
1821
- throw new Error(
1822
- `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
1823
- );
1824
- }
1825
- if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
1826
- throw new Error(
1827
- `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
1828
- );
1829
- }
1830
- if (userDef.reserved === true) {
1831
- throw new Error(
1832
- `pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
1833
- );
1834
- }
1835
- merged.set(name, {
1836
- queue: userDef.queue,
1837
- concurrency: userDef.concurrency,
1838
- reserved: false,
1839
- description: userDef.description
1840
- });
2179
+ return max + 1;
1841
2180
  }
1842
- cache.set(resolved, merged);
1843
- return merged;
1844
- }
1845
- function allNonReservedPoolNames(config) {
1846
- const out = [];
1847
- for (const [name, def] of config) {
1848
- if (!def.reserved) out.push(name);
2181
+ };
2182
+ MemoryJobStepService = __decorateClass([
2183
+ Injectable7()
2184
+ ], MemoryJobStepService);
2185
+
2186
+ // runtime/subsystems/jobs/memory-job-store.ts
2187
+ var MemoryJobStore = class {
2188
+ /** Runs keyed by `id` (single source of truth for status/scope/lineage). */
2189
+ runs = /* @__PURE__ */ new Map();
2190
+ /** Steps keyed by `job_run_id`; array order matches insertion order. */
2191
+ steps = /* @__PURE__ */ new Map();
2192
+ /** Job definitions keyed by `type` — memory mirror of the `job` table. */
2193
+ jobs = /* @__PURE__ */ new Map();
2194
+ /** Reset everything. Tests call this in `beforeEach`. */
2195
+ clear() {
2196
+ this.runs.clear();
2197
+ this.steps.clear();
2198
+ this.jobs.clear();
1849
2199
  }
1850
- return out;
1851
- }
1852
- function extractUserPools(raw) {
1853
- if (!raw || typeof raw !== "object") return {};
1854
- const jobs2 = raw.jobs;
1855
- if (!jobs2 || typeof jobs2 !== "object") return {};
1856
- const pools = jobs2.pools;
1857
- if (!pools || typeof pools !== "object") return {};
1858
- const out = {};
1859
- for (const [name, def] of Object.entries(pools)) {
1860
- if (!def || typeof def !== "object") continue;
1861
- out[name] = def;
2200
+ };
2201
+
2202
+ // runtime/subsystems/jobs/jobs-domain.module.ts
2203
+ var JobsDomainModule = class {
2204
+ static forRoot(opts) {
2205
+ const multiTenant = opts.multiTenant ?? false;
2206
+ const providers = [
2207
+ // JOB-8 boolean provider consumed by the four service-layer backends.
2208
+ // Always provided (even when `multiTenant === false`) so `@Inject`
2209
+ // always resolves; backends short-circuit the enforcement path when
2210
+ // the value is `false`. See `jobs-domain.tokens.ts` for the claim-loop
2211
+ // cross-tenant-by-design decision.
2212
+ { provide: JOBS_MULTI_TENANT, useValue: multiTenant }
2213
+ ];
2214
+ if (opts.backend === "memory") {
2215
+ const store = new MemoryJobStore();
2216
+ providers.push({ provide: MemoryJobStore, useValue: store });
2217
+ providers.push(MemoryJobStepService);
2218
+ providers.push({ provide: JOB_STEP_SERVICE, useExisting: MemoryJobStepService });
2219
+ providers.push(MemoryJobOrchestrator);
2220
+ providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
2221
+ providers.push(MemoryJobRunService);
2222
+ providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
2223
+ } else if (opts.backend === "bullmq") {
2224
+ const resolved = resolveBullMqConfig(opts.extensions?.bullmq);
2225
+ providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });
2226
+ providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });
2227
+ providers.push({ provide: JOB_ORCHESTRATOR, useClass: BullMQJobOrchestrator });
2228
+ providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
2229
+ providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
2230
+ } else {
2231
+ providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });
2232
+ providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
2233
+ providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
2234
+ }
2235
+ const exports = [
2236
+ JOB_ORCHESTRATOR,
2237
+ JOB_RUN_SERVICE,
2238
+ JOB_STEP_SERVICE,
2239
+ JOBS_MULTI_TENANT
2240
+ ];
2241
+ if (opts.backend === "bullmq") {
2242
+ exports.push(BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG);
2243
+ }
2244
+ return {
2245
+ module: JobsDomainModule,
2246
+ global: true,
2247
+ providers,
2248
+ exports
2249
+ };
1862
2250
  }
1863
- return out;
1864
- }
2251
+ };
2252
+ JobsDomainModule = __decorateClass([
2253
+ Module({})
2254
+ ], JobsDomainModule);
1865
2255
 
1866
2256
  // runtime/subsystems/jobs/job-worker.ts
1867
- import { Inject as Inject6, Injectable as Injectable7, Logger as Logger3 } from "@nestjs/common";
1868
- import { and as and4, asc as asc2, desc as desc3, eq as eq4, inArray as inArray3, lt, lte, sql as sql4 } from "drizzle-orm";
2257
+ import { Inject as Inject7, Injectable as Injectable8, Logger as Logger4 } from "@nestjs/common";
2258
+ import { and as and4, asc as asc2, desc as desc3, eq as eq5, inArray as inArray3, lt as lt2, lte, sql as sql4 } from "drizzle-orm";
1869
2259
  var JOB_WORKER_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_OPTIONS");
1870
2260
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
1871
2261
  var DEFAULT_STALE_SWEEPER_INTERVAL_MS = 6e4;
@@ -1928,7 +2318,7 @@ var JobWorker = class {
1928
2318
  stepService;
1929
2319
  options;
1930
2320
  moduleRef;
1931
- logger = new Logger3(JobWorker.name);
2321
+ logger = new Logger4(JobWorker.name);
1932
2322
  shuttingDown = false;
1933
2323
  inFlight = /* @__PURE__ */ new Set();
1934
2324
  pollTimer = null;
@@ -1969,7 +2359,7 @@ var JobWorker = class {
1969
2359
  await this.drainInFlight();
1970
2360
  try {
1971
2361
  await this.db.update(jobRuns).set({ status: "pending", claimedAt: null, startedAt: null }).where(
1972
- and4(eq4(jobRuns.status, "running"), eq4(jobRuns.pool, this.options.pool))
2362
+ and4(eq5(jobRuns.status, "running"), eq5(jobRuns.pool, this.options.pool))
1973
2363
  );
1974
2364
  } catch (err) {
1975
2365
  this.logger.error(`shutdown reset failed: ${err.message}`);
@@ -2019,8 +2409,8 @@ var JobWorker = class {
2019
2409
  return this.db.transaction(async (tx) => {
2020
2410
  const candidates = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
2021
2411
  and4(
2022
- eq4(jobRuns.status, "pending"),
2023
- eq4(jobRuns.pool, pool),
2412
+ eq5(jobRuns.status, "pending"),
2413
+ eq5(jobRuns.pool, pool),
2024
2414
  lte(jobRuns.runAt, /* @__PURE__ */ new Date())
2025
2415
  )
2026
2416
  ).orderBy(desc3(jobRuns.priority), asc2(jobRuns.runAt)).limit(1).for("update", { skipLocked: true });
@@ -2031,7 +2421,7 @@ var JobWorker = class {
2031
2421
  claimedAt: /* @__PURE__ */ new Date(),
2032
2422
  startedAt: /* @__PURE__ */ new Date(),
2033
2423
  updatedAt: /* @__PURE__ */ new Date()
2034
- }).where(eq4(jobRuns.id, candidate.id)).returning();
2424
+ }).where(eq5(jobRuns.id, candidate.id)).returning();
2035
2425
  return claimed ?? null;
2036
2426
  });
2037
2427
  }
@@ -2049,7 +2439,7 @@ var JobWorker = class {
2049
2439
  await this.db.transaction(async (tx) => {
2050
2440
  const threshold = new Date(Date.now() - this.staleThresholdMs);
2051
2441
  const stale = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
2052
- and4(eq4(jobRuns.status, "running"), lt(jobRuns.claimedAt, threshold))
2442
+ and4(eq5(jobRuns.status, "running"), lt2(jobRuns.claimedAt, threshold))
2053
2443
  ).for("update", { skipLocked: true });
2054
2444
  if (stale.length === 0) return;
2055
2445
  const ids = stale.map((r) => r.id);
@@ -2082,8 +2472,8 @@ var JobWorker = class {
2082
2472
  if (claimed.concurrencyKey) {
2083
2473
  const inflight = await this.db.select({ id: jobRuns.id }).from(jobRuns).where(
2084
2474
  and4(
2085
- eq4(jobRuns.concurrencyKey, claimed.concurrencyKey),
2086
- eq4(jobRuns.status, "running")
2475
+ eq5(jobRuns.concurrencyKey, claimed.concurrencyKey),
2476
+ eq5(jobRuns.status, "running")
2087
2477
  )
2088
2478
  );
2089
2479
  const other = inflight.find((r) => r.id !== claimed.id);
@@ -2093,7 +2483,7 @@ var JobWorker = class {
2093
2483
  claimedAt: null,
2094
2484
  startedAt: null,
2095
2485
  updatedAt: /* @__PURE__ */ new Date()
2096
- }).where(eq4(jobRuns.id, claimed.id));
2486
+ }).where(eq5(jobRuns.id, claimed.id));
2097
2487
  return;
2098
2488
  }
2099
2489
  }
@@ -2108,7 +2498,7 @@ var JobWorker = class {
2108
2498
  run: claimed,
2109
2499
  step: this.makeStepFn(claimed),
2110
2500
  spawnChild: this.makeSpawnFn(claimed),
2111
- logger: new Logger3(`JobRun:${claimed.id}`)
2501
+ logger: new Logger4(`JobRun:${claimed.id}`)
2112
2502
  };
2113
2503
  const attemptsBefore = claimed.attempts ?? 0;
2114
2504
  try {
@@ -2119,7 +2509,7 @@ var JobWorker = class {
2119
2509
  finishedAt: /* @__PURE__ */ new Date(),
2120
2510
  updatedAt: /* @__PURE__ */ new Date(),
2121
2511
  attempts: attemptsBefore + 1
2122
- }).where(eq4(jobRuns.id, claimed.id));
2512
+ }).where(eq5(jobRuns.id, claimed.id));
2123
2513
  } catch (err) {
2124
2514
  const policy = meta.retry;
2125
2515
  const decision = classifyError2(err, policy, attemptsBefore);
@@ -2134,7 +2524,7 @@ var JobWorker = class {
2134
2524
  claimedAt: null,
2135
2525
  error: serialiseError2(err, nextAttempts, true),
2136
2526
  updatedAt: /* @__PURE__ */ new Date()
2137
- }).where(eq4(jobRuns.id, claimed.id));
2527
+ }).where(eq5(jobRuns.id, claimed.id));
2138
2528
  } else {
2139
2529
  await this.markFailed(claimed, err, nextAttempts);
2140
2530
  }
@@ -2147,7 +2537,7 @@ var JobWorker = class {
2147
2537
  finishedAt: /* @__PURE__ */ new Date(),
2148
2538
  error: serialiseError2(err, finalAttempts, false),
2149
2539
  updatedAt: /* @__PURE__ */ new Date()
2150
- }).where(eq4(jobRuns.id, claimed.id));
2540
+ }).where(eq5(jobRuns.id, claimed.id));
2151
2541
  if (claimed.parentClosePolicy === "terminate") {
2152
2542
  try {
2153
2543
  await this.orchestrator.cancel(claimed.id, {
@@ -2250,25 +2640,228 @@ var JobWorker = class {
2250
2640
  // ============================================================================
2251
2641
  };
2252
2642
  JobWorker = __decorateClass([
2253
- Injectable7(),
2254
- __decorateParam(0, Inject6(DRIZZLE)),
2255
- __decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
2256
- __decorateParam(2, Inject6(JOB_RUN_SERVICE)),
2257
- __decorateParam(3, Inject6(JOB_STEP_SERVICE)),
2258
- __decorateParam(4, Inject6(JOB_WORKER_OPTIONS))
2643
+ Injectable8(),
2644
+ __decorateParam(0, Inject7(DRIZZLE)),
2645
+ __decorateParam(1, Inject7(JOB_ORCHESTRATOR)),
2646
+ __decorateParam(2, Inject7(JOB_RUN_SERVICE)),
2647
+ __decorateParam(3, Inject7(JOB_STEP_SERVICE)),
2648
+ __decorateParam(4, Inject7(JOB_WORKER_OPTIONS))
2259
2649
  ], JobWorker);
2260
2650
 
2651
+ // runtime/subsystems/jobs/job-worker.bullmq-backend.ts
2652
+ import { Logger as Logger5 } from "@nestjs/common";
2653
+ import { eq as eq6 } from "drizzle-orm";
2654
+ function serialiseError3(err, attempt, retryable) {
2655
+ const e = err;
2656
+ return {
2657
+ message: e?.message ?? String(err),
2658
+ stack: e?.stack,
2659
+ retryable,
2660
+ attempt
2661
+ };
2662
+ }
2663
+ var BullMQJobWorker = class _BullMQJobWorker {
2664
+ constructor(db, orchestrator, stepService, options, moduleRef) {
2665
+ this.db = db;
2666
+ this.orchestrator = orchestrator;
2667
+ this.stepService = stepService;
2668
+ this.options = options;
2669
+ this.moduleRef = moduleRef;
2670
+ }
2671
+ db;
2672
+ orchestrator;
2673
+ stepService;
2674
+ options;
2675
+ moduleRef;
2676
+ logger = new Logger5(_BullMQJobWorker.name);
2677
+ worker = null;
2678
+ async onModuleInit() {
2679
+ let WorkerCtor;
2680
+ try {
2681
+ const mod = await import("bullmq");
2682
+ WorkerCtor = mod.Worker;
2683
+ } catch {
2684
+ throw new Error(
2685
+ 'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
2686
+ );
2687
+ }
2688
+ this.worker = new WorkerCtor(
2689
+ this.options.queueName,
2690
+ (job) => this.process(job),
2691
+ {
2692
+ connection: this.options.connection,
2693
+ concurrency: this.options.concurrency
2694
+ }
2695
+ );
2696
+ this.worker.on("failed", (job, err) => {
2697
+ if (!job) return;
2698
+ const attemptsMade = job.attemptsMade;
2699
+ const maxAttempts = job.opts.attempts ?? 1;
2700
+ if (attemptsMade >= maxAttempts) {
2701
+ void this.markFailed(job.data.runId, err, attemptsMade);
2702
+ }
2703
+ });
2704
+ this.logger.log(
2705
+ `BullMQ worker started: pool='${this.options.pool}' queue='${this.options.queueName}' concurrency=${this.options.concurrency}`
2706
+ );
2707
+ }
2708
+ async onModuleDestroy() {
2709
+ if (this.worker) {
2710
+ await this.worker.close();
2711
+ this.worker = null;
2712
+ }
2713
+ }
2714
+ /**
2715
+ * Process one BullMQ job. Returns the handler output (stored by BullMQ as
2716
+ * the job return value AND written to `job_run.output`). Throws on handler
2717
+ * failure so BullMQ applies the retry policy.
2718
+ */
2719
+ async process(job) {
2720
+ const { runId } = job.data;
2721
+ const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
2722
+ if (!row) {
2723
+ this.logger.warn(`process: job_run ${runId} not found; skipping`);
2724
+ return {};
2725
+ }
2726
+ const run = row;
2727
+ if (run.status === "canceled") {
2728
+ return {};
2729
+ }
2730
+ const registryEntry = JOB_HANDLER_REGISTRY.get(run.jobType);
2731
+ if (!registryEntry) {
2732
+ throw new Error(
2733
+ `No handler registered for jobType='${run.jobType}' (run ${run.id})`
2734
+ );
2735
+ }
2736
+ await this.db.update(jobRuns).set({
2737
+ status: "running",
2738
+ claimedAt: /* @__PURE__ */ new Date(),
2739
+ startedAt: /* @__PURE__ */ new Date(),
2740
+ attempts: job.attemptsMade + 1,
2741
+ updatedAt: /* @__PURE__ */ new Date()
2742
+ }).where(eq6(jobRuns.id, run.id));
2743
+ const HandlerClass = registryEntry.handlerClass;
2744
+ const handler = this.moduleRef.get(
2745
+ HandlerClass,
2746
+ { strict: false }
2747
+ );
2748
+ const ctx = {
2749
+ input: run.input,
2750
+ run,
2751
+ step: this.makeStepFn(run),
2752
+ spawnChild: this.makeSpawnFn(run),
2753
+ logger: new Logger5(`JobRun:${run.id}`)
2754
+ };
2755
+ const output = await handler.run(ctx);
2756
+ await this.db.update(jobRuns).set({
2757
+ status: "completed",
2758
+ output: output ?? {},
2759
+ finishedAt: /* @__PURE__ */ new Date(),
2760
+ updatedAt: /* @__PURE__ */ new Date()
2761
+ }).where(eq6(jobRuns.id, run.id));
2762
+ return output ?? {};
2763
+ }
2764
+ async markFailed(runId, err, finalAttempts) {
2765
+ const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
2766
+ if (!row) return;
2767
+ const run = row;
2768
+ await this.db.update(jobRuns).set({
2769
+ status: "failed",
2770
+ attempts: finalAttempts,
2771
+ finishedAt: /* @__PURE__ */ new Date(),
2772
+ error: serialiseError3(err, finalAttempts, false),
2773
+ updatedAt: /* @__PURE__ */ new Date()
2774
+ }).where(eq6(jobRuns.id, runId));
2775
+ if (run.parentClosePolicy === "terminate") {
2776
+ try {
2777
+ await this.orchestrator.cancel(run.id, {
2778
+ cascade: true,
2779
+ reason: "parent-failed",
2780
+ tenantId: run.tenantId
2781
+ });
2782
+ } catch (cascadeErr) {
2783
+ this.logger.warn(
2784
+ `cascade on failed run ${run.id}: ${cascadeErr.message}`
2785
+ );
2786
+ }
2787
+ }
2788
+ }
2789
+ // ── ctx.step / ctx.spawnChild (mirror JobWorker) ──────────────────────────
2790
+ makeStepFn(run) {
2791
+ return async (stepId, fn, _opts) => {
2792
+ void _opts;
2793
+ const existing = await this.stepService.findStep(run.id, stepId);
2794
+ if (existing?.status === "completed") {
2795
+ return existing.output;
2796
+ }
2797
+ const nextAttempts = (existing?.attempts ?? 0) + 1;
2798
+ const seq = nextAttempts;
2799
+ await this.stepService.recordStep({
2800
+ jobRunId: run.id,
2801
+ stepId,
2802
+ kind: "task",
2803
+ seq,
2804
+ status: "running",
2805
+ startedAt: /* @__PURE__ */ new Date(),
2806
+ attempts: nextAttempts
2807
+ });
2808
+ try {
2809
+ const output = await fn();
2810
+ await this.stepService.recordStep({
2811
+ jobRunId: run.id,
2812
+ stepId,
2813
+ kind: "task",
2814
+ seq,
2815
+ status: "completed",
2816
+ output,
2817
+ finishedAt: /* @__PURE__ */ new Date(),
2818
+ attempts: nextAttempts
2819
+ });
2820
+ return output;
2821
+ } catch (err) {
2822
+ await this.stepService.recordStep({
2823
+ jobRunId: run.id,
2824
+ stepId,
2825
+ kind: "task",
2826
+ seq,
2827
+ status: "failed",
2828
+ error: serialiseError3(err, nextAttempts, false),
2829
+ finishedAt: /* @__PURE__ */ new Date(),
2830
+ attempts: nextAttempts
2831
+ });
2832
+ throw err;
2833
+ }
2834
+ };
2835
+ }
2836
+ makeSpawnFn(run) {
2837
+ return async (type, input, opts) => {
2838
+ return this.orchestrator.start(type, input, {
2839
+ parentRunId: run.id,
2840
+ parentClosePolicy: opts?.closePolicy,
2841
+ runAt: opts?.runAt,
2842
+ priority: opts?.priority,
2843
+ tags: opts?.tags,
2844
+ triggerSource: "parent",
2845
+ triggerRef: run.id,
2846
+ tenantId: run.tenantId
2847
+ });
2848
+ };
2849
+ }
2850
+ };
2851
+
2261
2852
  // runtime/subsystems/jobs/job-worker.module.ts
2262
2853
  var DEFAULT_SHUTDOWN_TIMEOUT_MS2 = 3e4;
2263
2854
  var JOB_WORKER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_MODULE_OPTIONS");
2264
2855
  var JobWorkerOrchestrator = class {
2265
- constructor(orchestrator, runService, stepService, options, db = null, moduleRef) {
2856
+ constructor(orchestrator, runService, stepService, options, db = null, moduleRef, bullConnection = null, bullConfig = null) {
2266
2857
  this.orchestrator = orchestrator;
2267
2858
  this.runService = runService;
2268
2859
  this.stepService = stepService;
2269
2860
  this.options = options;
2270
2861
  this.db = db;
2271
2862
  this.moduleRef = moduleRef;
2863
+ this.bullConnection = bullConnection;
2864
+ this.bullConfig = bullConfig;
2272
2865
  }
2273
2866
  orchestrator;
2274
2867
  runService;
@@ -2276,7 +2869,9 @@ var JobWorkerOrchestrator = class {
2276
2869
  options;
2277
2870
  db;
2278
2871
  moduleRef;
2279
- logger = new Logger4(JobWorkerOrchestrator.name);
2872
+ bullConnection;
2873
+ bullConfig;
2874
+ logger = new Logger6(JobWorkerOrchestrator.name);
2280
2875
  workers = [];
2281
2876
  // ============================================================================
2282
2877
  // Lifecycle
@@ -2293,7 +2888,7 @@ var JobWorkerOrchestrator = class {
2293
2888
  if (backend !== "memory" && orphaned.length > 0) {
2294
2889
  throw new BootValidationError(orphaned);
2295
2890
  }
2296
- const activePools = this.options.pools ?? allNonReservedPoolNames(poolConfig);
2891
+ const activePools = this.options.pools ? this.options.pools : this.options.allPools ? allPoolNames(poolConfig) : allNonReservedPoolNames(poolConfig);
2297
2892
  for (const poolName of activePools) {
2298
2893
  const def = poolConfig.get(poolName);
2299
2894
  if (!def) {
@@ -2306,11 +2901,11 @@ var JobWorkerOrchestrator = class {
2306
2901
  concurrency: def.concurrency,
2307
2902
  shutdownTimeoutMs: this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS2
2308
2903
  };
2309
- const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : this.spawnWorker(workerOptions);
2310
- worker.onModuleInit();
2904
+ const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : backend === "bullmq" ? this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig) : this.spawnWorker(workerOptions);
2905
+ await worker.onModuleInit();
2311
2906
  this.workers.push(worker);
2312
2907
  this.logger.log(
2313
- `JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency}`
2908
+ `JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency} backend='${backend}'`
2314
2909
  );
2315
2910
  }
2316
2911
  }
@@ -2327,6 +2922,16 @@ var JobWorkerOrchestrator = class {
2327
2922
  }
2328
2923
  }
2329
2924
  this.workers.length = 0;
2925
+ const orch = this.orchestrator;
2926
+ if (typeof orch.closeConnections === "function") {
2927
+ try {
2928
+ await orch.closeConnections();
2929
+ } catch (err) {
2930
+ this.logger.error(
2931
+ `BullMQ orchestrator connection close failed: ${err.message}`
2932
+ );
2933
+ }
2934
+ }
2330
2935
  }
2331
2936
  // ============================================================================
2332
2937
  // Internals
@@ -2388,15 +2993,57 @@ var JobWorkerOrchestrator = class {
2388
2993
  this.moduleRef
2389
2994
  );
2390
2995
  }
2996
+ /**
2997
+ * BULLMQ-1 — spawn a per-pool `BullMQJobWorker`. Requires the Drizzle
2998
+ * client (the worker drives `job_run` as the source of truth) AND the
2999
+ * resolved BullMQ connection (bound by `JobsDomainModule` when
3000
+ * `backend: 'bullmq'`). The queue name is derived identically to the
3001
+ * orchestrator's `dispatch` via `resolvePoolQueueName(pool, …)` so producer
3002
+ * and consumer agree.
3003
+ */
3004
+ spawnBullMQWorker(pool, _queueAlias, concurrency, poolConfig) {
3005
+ if (!this.db) {
3006
+ throw new Error(
3007
+ `JobWorkerModule: BullMQ worker spawning requires the Drizzle client (no DRIZZLE provider available) \u2014 job_run remains the source of truth.`
3008
+ );
3009
+ }
3010
+ if (!this.bullConnection) {
3011
+ throw new Error(
3012
+ `JobWorkerModule: BullMQ worker spawning requires a resolved BULLMQ_CONNECTION. Ensure JobsDomainModule was booted with backend: 'bullmq'.`
3013
+ );
3014
+ }
3015
+ if (!this.moduleRef) {
3016
+ throw new Error(
3017
+ `JobWorkerModule: ModuleRef not available \u2014 cannot construct BullMQJobWorker with handler DI support.`
3018
+ );
3019
+ }
3020
+ const queueName = resolvePoolQueueName(pool, this.bullConfig, poolConfig);
3021
+ return new BullMQJobWorker(
3022
+ this.db,
3023
+ this.orchestrator,
3024
+ this.stepService,
3025
+ {
3026
+ pool,
3027
+ queueName,
3028
+ concurrency,
3029
+ connection: this.bullConnection
3030
+ },
3031
+ this.moduleRef
3032
+ );
3033
+ }
2391
3034
  };
2392
3035
  JobWorkerOrchestrator = __decorateClass([
2393
- Injectable8(),
2394
- __decorateParam(0, Inject7(JOB_ORCHESTRATOR)),
2395
- __decorateParam(1, Inject7(JOB_RUN_SERVICE)),
2396
- __decorateParam(2, Inject7(JOB_STEP_SERVICE)),
2397
- __decorateParam(3, Inject7(JOB_WORKER_MODULE_OPTIONS)),
2398
- __decorateParam(4, Optional2()),
2399
- __decorateParam(4, Inject7(DRIZZLE))
3036
+ Injectable9(),
3037
+ __decorateParam(0, Inject8(JOB_ORCHESTRATOR)),
3038
+ __decorateParam(1, Inject8(JOB_RUN_SERVICE)),
3039
+ __decorateParam(2, Inject8(JOB_STEP_SERVICE)),
3040
+ __decorateParam(3, Inject8(JOB_WORKER_MODULE_OPTIONS)),
3041
+ __decorateParam(4, Optional3()),
3042
+ __decorateParam(4, Inject8(DRIZZLE)),
3043
+ __decorateParam(6, Optional3()),
3044
+ __decorateParam(6, Inject8(BULLMQ_CONNECTION)),
3045
+ __decorateParam(7, Optional3()),
3046
+ __decorateParam(7, Inject8(BULLMQ_RESOLVED_CONFIG))
2400
3047
  ], JobWorkerOrchestrator);
2401
3048
  var JobWorkerModule = class {
2402
3049
  static forRoot(opts) {
@@ -2413,7 +3060,14 @@ var JobWorkerModule = class {
2413
3060
  { provide: JOB_WORKER_MODULE_OPTIONS, useValue: opts },
2414
3061
  JobWorkerOrchestrator
2415
3062
  ],
2416
- exports: []
3063
+ // BULLMQ-1 Phase 1 — export the options token so `BridgeModule`'s
3064
+ // reserved-pool guard (`onModuleInit`) can actually inject it.
3065
+ // Previously `exports: []` left the `@Optional()` inject resolving to
3066
+ // `undefined` and the guard silently no-opped (a dead check). With the
3067
+ // token exported the guard fires for real; consumers that omit the
3068
+ // reserved pools (and don't set `allPools`) now fail fast with
3069
+ // `BridgeReservedPoolsNotPolledError` — which is correct.
3070
+ exports: [JOB_WORKER_MODULE_OPTIONS]
2417
3071
  };
2418
3072
  }
2419
3073
  };
@@ -2575,8 +3229,8 @@ var MemoryBridgeDeliveryRepo = class {
2575
3229
  };
2576
3230
 
2577
3231
  // runtime/subsystems/bridge/bridge-delivery.drizzle-backend.ts
2578
- import { Inject as Inject8, Injectable as Injectable9, Optional as Optional3 } from "@nestjs/common";
2579
- import { eq as eq5, and as and5, gte, isNull as isNull2, sql as sql7 } from "drizzle-orm";
3232
+ import { Inject as Inject9, Injectable as Injectable10, Optional as Optional4 } from "@nestjs/common";
3233
+ import { eq as eq7, and as and5, gte as gte2, isNull as isNull2, sql as sql7 } from "drizzle-orm";
2580
3234
 
2581
3235
  // runtime/subsystems/bridge/bridge-delivery.schema.ts
2582
3236
  import {
@@ -2763,14 +3417,14 @@ var DrizzleBridgeDeliveryRepo = class {
2763
3417
  async findDelivery(eventId, triggerId) {
2764
3418
  const rows = await this.db.select().from(bridgeDelivery).where(
2765
3419
  and5(
2766
- eq5(bridgeDelivery.eventId, eventId),
2767
- eq5(bridgeDelivery.triggerId, triggerId)
3420
+ eq7(bridgeDelivery.eventId, eventId),
3421
+ eq7(bridgeDelivery.triggerId, triggerId)
2768
3422
  )
2769
3423
  ).limit(1);
2770
3424
  return rows[0] ?? null;
2771
3425
  }
2772
3426
  async findDeliveryById(id) {
2773
- const rows = await this.db.select().from(bridgeDelivery).where(eq5(bridgeDelivery.id, id)).limit(1);
3427
+ const rows = await this.db.select().from(bridgeDelivery).where(eq7(bridgeDelivery.id, id)).limit(1);
2774
3428
  return rows[0] ?? null;
2775
3429
  }
2776
3430
  async markDelivered(id, userRunId, tx) {
@@ -2779,15 +3433,15 @@ var DrizzleBridgeDeliveryRepo = class {
2779
3433
  status: "delivered",
2780
3434
  userRunId,
2781
3435
  deliveredAt: /* @__PURE__ */ new Date()
2782
- }).where(eq5(bridgeDelivery.id, id));
3436
+ }).where(eq7(bridgeDelivery.id, id));
2783
3437
  }
2784
3438
  async markSkipped(id, reason, tx) {
2785
3439
  const client = tx ?? this.db;
2786
- await client.update(bridgeDelivery).set({ status: "skipped", skipReason: reason }).where(eq5(bridgeDelivery.id, id));
3440
+ await client.update(bridgeDelivery).set({ status: "skipped", skipReason: reason }).where(eq7(bridgeDelivery.id, id));
2787
3441
  }
2788
3442
  async markFailed(id, error, tx) {
2789
3443
  const client = tx ?? this.db;
2790
- await client.update(bridgeDelivery).set({ status: "failed", error }).where(eq5(bridgeDelivery.id, id));
3444
+ await client.update(bridgeDelivery).set({ status: "failed", error }).where(eq7(bridgeDelivery.id, id));
2791
3445
  }
2792
3446
  /**
2793
3447
  * Observability read — see `IJobBridge.getStatusHistogram` JSDoc for the
@@ -2811,11 +3465,11 @@ var DrizzleBridgeDeliveryRepo = class {
2811
3465
  throw new RangeError("windowHours must be positive");
2812
3466
  }
2813
3467
  const cutoff = sql7`now() - make_interval(hours => ${windowHours})`;
2814
- const conditions = [gte(bridgeDelivery.attemptedAt, cutoff)];
3468
+ const conditions = [gte2(bridgeDelivery.attemptedAt, cutoff)];
2815
3469
  if (tenantId === null) {
2816
3470
  conditions.push(isNull2(bridgeDelivery.tenantId));
2817
3471
  } else if (typeof tenantId === "string") {
2818
- conditions.push(eq5(bridgeDelivery.tenantId, tenantId));
3472
+ conditions.push(eq7(bridgeDelivery.tenantId, tenantId));
2819
3473
  }
2820
3474
  const rows = await this.db.select({
2821
3475
  status: bridgeDelivery.status,
@@ -2834,18 +3488,18 @@ var DrizzleBridgeDeliveryRepo = class {
2834
3488
  }
2835
3489
  };
2836
3490
  DrizzleBridgeDeliveryRepo = __decorateClass([
2837
- Injectable9(),
2838
- __decorateParam(0, Inject8(DRIZZLE)),
2839
- __decorateParam(1, Optional3()),
2840
- __decorateParam(1, Inject8(BRIDGE_MULTI_TENANT))
3491
+ Injectable10(),
3492
+ __decorateParam(0, Inject9(DRIZZLE)),
3493
+ __decorateParam(1, Optional4()),
3494
+ __decorateParam(1, Inject9(BRIDGE_MULTI_TENANT))
2841
3495
  ], DrizzleBridgeDeliveryRepo);
2842
3496
 
2843
3497
  // runtime/subsystems/bridge/bridge-outbox-drain-hook.ts
2844
- import { Inject as Inject10, Injectable as Injectable11, Logger as Logger6, Optional as Optional5 } from "@nestjs/common";
3498
+ import { Inject as Inject11, Injectable as Injectable12, Logger as Logger8, Optional as Optional6 } from "@nestjs/common";
2845
3499
  import { randomUUID as randomUUID5 } from "crypto";
2846
3500
 
2847
3501
  // runtime/subsystems/bridge/bridge-delivery-handler.ts
2848
- import { Inject as Inject9, Injectable as Injectable10, Logger as Logger5, Optional as Optional4 } from "@nestjs/common";
3502
+ import { Inject as Inject10, Injectable as Injectable11, Logger as Logger7, Optional as Optional5 } from "@nestjs/common";
2849
3503
 
2850
3504
  // runtime/subsystems/events/events.tokens.ts
2851
3505
  var EVENT_BUS = "EVENT_BUS";
@@ -2867,7 +3521,7 @@ var BridgeDeliveryHandler = class extends JobHandlerBase {
2867
3521
  events;
2868
3522
  registry;
2869
3523
  multiTenant;
2870
- classLogger = new Logger5(BridgeDeliveryHandler.name);
3524
+ classLogger = new Logger7(BridgeDeliveryHandler.name);
2871
3525
  async run(ctx) {
2872
3526
  const { deliveryId } = ctx.input;
2873
3527
  const delivery = await this.repo.findDeliveryById(deliveryId);
@@ -2928,18 +3582,18 @@ var BridgeDeliveryHandler = class extends JobHandlerBase {
2928
3582
  }
2929
3583
  };
2930
3584
  BridgeDeliveryHandler = __decorateClass([
2931
- Injectable10(),
3585
+ Injectable11(),
2932
3586
  JobHandler(BRIDGE_DELIVERY_JOB_TYPE, {
2933
3587
  pool: "events_change",
2934
3588
  retry: { attempts: 3, backoff: "exponential", baseMs: 250 },
2935
3589
  replayFrom: "last_step"
2936
3590
  }),
2937
- __decorateParam(0, Inject9(BRIDGE_DELIVERY_REPO)),
2938
- __decorateParam(1, Inject9(JOB_ORCHESTRATOR)),
2939
- __decorateParam(2, Inject9(EVENT_BUS)),
2940
- __decorateParam(3, Inject9(BRIDGE_REGISTRY)),
2941
- __decorateParam(4, Optional4()),
2942
- __decorateParam(4, Inject9(BRIDGE_MULTI_TENANT))
3591
+ __decorateParam(0, Inject10(BRIDGE_DELIVERY_REPO)),
3592
+ __decorateParam(1, Inject10(JOB_ORCHESTRATOR)),
3593
+ __decorateParam(2, Inject10(EVENT_BUS)),
3594
+ __decorateParam(3, Inject10(BRIDGE_REGISTRY)),
3595
+ __decorateParam(4, Optional5()),
3596
+ __decorateParam(4, Inject10(BRIDGE_MULTI_TENANT))
2943
3597
  ], BridgeDeliveryHandler);
2944
3598
 
2945
3599
  // runtime/subsystems/bridge/bridge-outbox-drain-hook.ts
@@ -2953,7 +3607,7 @@ var BridgeOutboxDrainHook = class {
2953
3607
  this.registry = registry;
2954
3608
  }
2955
3609
  registry;
2956
- logger = new Logger6(BridgeOutboxDrainHook.name);
3610
+ logger = new Logger8(BridgeOutboxDrainHook.name);
2957
3611
  warnedNullDirection = false;
2958
3612
  warnedAuditTypes = /* @__PURE__ */ new Set();
2959
3613
  async processEvent(event, tx) {
@@ -3046,13 +3700,13 @@ var BridgeOutboxDrainHook = class {
3046
3700
  }
3047
3701
  };
3048
3702
  BridgeOutboxDrainHook = __decorateClass([
3049
- Injectable11(),
3050
- __decorateParam(0, Optional5()),
3051
- __decorateParam(0, Inject10(BRIDGE_REGISTRY))
3703
+ Injectable12(),
3704
+ __decorateParam(0, Optional6()),
3705
+ __decorateParam(0, Inject11(BRIDGE_REGISTRY))
3052
3706
  ], BridgeOutboxDrainHook);
3053
3707
 
3054
3708
  // runtime/subsystems/bridge/event-flow.service.ts
3055
- import { Inject as Inject11, Injectable as Injectable12, Optional as Optional6 } from "@nestjs/common";
3709
+ import { Inject as Inject12, Injectable as Injectable13, Optional as Optional7 } from "@nestjs/common";
3056
3710
  var EventFlowService = class {
3057
3711
  constructor(db, eventBus, orchestrator, bridgeRepo, registry = {}, multiTenant = false) {
3058
3712
  this.db = db;
@@ -3123,15 +3777,15 @@ var EventFlowService = class {
3123
3777
  }
3124
3778
  };
3125
3779
  EventFlowService = __decorateClass([
3126
- Injectable12(),
3127
- __decorateParam(0, Inject11(DRIZZLE)),
3128
- __decorateParam(1, Inject11(EVENT_BUS)),
3129
- __decorateParam(2, Inject11(JOB_ORCHESTRATOR)),
3130
- __decorateParam(3, Inject11(BRIDGE_DELIVERY_REPO)),
3131
- __decorateParam(4, Optional6()),
3132
- __decorateParam(4, Inject11(BRIDGE_REGISTRY)),
3133
- __decorateParam(5, Optional6()),
3134
- __decorateParam(5, Inject11(BRIDGE_MULTI_TENANT))
3780
+ Injectable13(),
3781
+ __decorateParam(0, Inject12(DRIZZLE)),
3782
+ __decorateParam(1, Inject12(EVENT_BUS)),
3783
+ __decorateParam(2, Inject12(JOB_ORCHESTRATOR)),
3784
+ __decorateParam(3, Inject12(BRIDGE_DELIVERY_REPO)),
3785
+ __decorateParam(4, Optional7()),
3786
+ __decorateParam(4, Inject12(BRIDGE_REGISTRY)),
3787
+ __decorateParam(5, Optional7()),
3788
+ __decorateParam(5, Inject12(BRIDGE_MULTI_TENANT))
3135
3789
  ], EventFlowService);
3136
3790
 
3137
3791
  // runtime/subsystems/bridge/generated/registry.ts
@@ -3197,6 +3851,7 @@ var BridgeModule = class {
3197
3851
  }
3198
3852
  async onModuleInit() {
3199
3853
  if (!this.workerOpts) return;
3854
+ if (this.workerOpts.allPools) return;
3200
3855
  const activePools = this.workerOpts.pools ?? [];
3201
3856
  const missing = BRIDGE_RESERVED_POOLS.filter(
3202
3857
  (p) => !activePools.includes(p)
@@ -3208,8 +3863,8 @@ var BridgeModule = class {
3208
3863
  };
3209
3864
  BridgeModule = __decorateClass([
3210
3865
  Module3({}),
3211
- __decorateParam(0, Optional7()),
3212
- __decorateParam(0, Inject12(JOB_WORKER_MODULE_OPTIONS))
3866
+ __decorateParam(0, Optional8()),
3867
+ __decorateParam(0, Inject13(JOB_WORKER_MODULE_OPTIONS))
3213
3868
  ], BridgeModule);
3214
3869
  export {
3215
3870
  BridgeModule