@pattern-stack/codegen 0.8.0 → 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 (118) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +1 -0
  3. package/dist/runtime/subsystems/auth/index.d.ts +2 -0
  4. package/dist/runtime/subsystems/auth/index.js +55 -0
  5. package/dist/runtime/subsystems/auth/index.js.map +1 -1
  6. package/dist/runtime/subsystems/auth/middleware/requester-context.d.ts +81 -0
  7. package/dist/runtime/subsystems/auth/middleware/requester-context.js +60 -0
  8. package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -0
  9. package/dist/runtime/subsystems/auth/protocols/user-context.d.ts +18 -0
  10. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
  11. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
  12. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +3 -0
  13. package/dist/runtime/subsystems/bridge/bridge.module.js +930 -275
  14. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  15. package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
  16. package/dist/runtime/subsystems/bridge/index.d.ts +3 -0
  17. package/dist/runtime/subsystems/bridge/index.js +837 -182
  18. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  19. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +3 -1
  20. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +92 -1
  21. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
  22. package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +3 -1
  23. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +99 -0
  24. package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
  25. package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
  26. package/dist/runtime/subsystems/events/event-keyset-cursor.d.ts +32 -0
  27. package/dist/runtime/subsystems/events/event-keyset-cursor.js +38 -0
  28. package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -0
  29. package/dist/runtime/subsystems/events/event-read.protocol.d.ts +94 -0
  30. package/dist/runtime/subsystems/events/event-read.protocol.js +9 -0
  31. package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -0
  32. package/dist/runtime/subsystems/events/events.module.js +177 -3
  33. package/dist/runtime/subsystems/events/events.module.js.map +1 -1
  34. package/dist/runtime/subsystems/events/events.tokens.d.ts +16 -1
  35. package/dist/runtime/subsystems/events/events.tokens.js +2 -0
  36. package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
  37. package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
  38. package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
  39. package/dist/runtime/subsystems/events/index.d.ts +2 -1
  40. package/dist/runtime/subsystems/events/index.js +178 -3
  41. package/dist/runtime/subsystems/events/index.js.map +1 -1
  42. package/dist/runtime/subsystems/index.d.ts +2 -0
  43. package/dist/runtime/subsystems/index.js +1198 -264
  44. package/dist/runtime/subsystems/index.js.map +1 -1
  45. package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +98 -0
  46. package/dist/runtime/subsystems/jobs/bullmq.config.js +143 -0
  47. package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -0
  48. package/dist/runtime/subsystems/jobs/index.d.ts +6 -2
  49. package/dist/runtime/subsystems/jobs/index.js +861 -201
  50. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  51. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +107 -0
  52. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +922 -0
  53. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -0
  54. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +52 -0
  55. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +57 -0
  56. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -0
  57. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +2 -1
  58. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +81 -1
  59. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
  60. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +2 -1
  61. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +81 -0
  62. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
  63. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +74 -1
  64. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +48 -0
  65. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +374 -0
  66. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -0
  67. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +42 -4
  68. package/dist/runtime/subsystems/jobs/job-worker.module.js +832 -178
  69. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  70. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +10 -1
  71. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +519 -20
  72. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  73. package/dist/runtime/subsystems/jobs/pool-config.loader.d.ts +9 -1
  74. package/dist/runtime/subsystems/jobs/pool-config.loader.js +4 -0
  75. package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
  76. package/dist/runtime/subsystems/observability/index.d.ts +4 -3
  77. package/dist/runtime/subsystems/observability/index.js +109 -2
  78. package/dist/runtime/subsystems/observability/index.js.map +1 -1
  79. package/dist/runtime/subsystems/observability/observability.module.js +109 -2
  80. package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
  81. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +63 -2
  82. package/dist/runtime/subsystems/observability/observability.service.d.ts +21 -3
  83. package/dist/runtime/subsystems/observability/observability.service.js +109 -2
  84. package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
  85. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -0
  86. package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -0
  87. package/dist/src/cli/index.js +43 -7
  88. package/dist/src/cli/index.js.map +1 -1
  89. package/package.json +1 -1
  90. package/runtime/subsystems/auth/index.ts +8 -0
  91. package/runtime/subsystems/auth/middleware/requester-context.ts +141 -0
  92. package/runtime/subsystems/auth/protocols/user-context.ts +17 -0
  93. package/runtime/subsystems/bridge/bridge.module.ts +5 -0
  94. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +109 -3
  95. package/runtime/subsystems/events/event-bus.memory-backend.ts +103 -1
  96. package/runtime/subsystems/events/event-keyset-cursor.ts +59 -0
  97. package/runtime/subsystems/events/event-read.protocol.ts +97 -0
  98. package/runtime/subsystems/events/events.module.ts +18 -2
  99. package/runtime/subsystems/events/events.tokens.ts +16 -0
  100. package/runtime/subsystems/events/index.ts +7 -0
  101. package/runtime/subsystems/jobs/bullmq.config.ts +125 -0
  102. package/runtime/subsystems/jobs/index.ts +22 -0
  103. package/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts +381 -0
  104. package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +88 -0
  105. package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +59 -1
  106. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +53 -0
  107. package/runtime/subsystems/jobs/job-run-service.protocol.ts +77 -0
  108. package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +311 -0
  109. package/runtime/subsystems/jobs/job-worker.module.ts +124 -10
  110. package/runtime/subsystems/jobs/jobs-domain.module.ts +40 -21
  111. package/runtime/subsystems/jobs/pool-config.loader.ts +11 -0
  112. package/runtime/subsystems/observability/index.ts +8 -0
  113. package/runtime/subsystems/observability/observability.protocol.ts +76 -0
  114. package/runtime/subsystems/observability/observability.service.ts +148 -1
  115. package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -12
  116. package/templates/relationship/new/prompt.js +8 -5
  117. package/templates/subsystem/jobs/worker.ejs.t +30 -7
  118. package/templates/subsystem/sync/sync-audit.schema.ejs.t +12 -16
@@ -12,11 +12,11 @@ var __decorateParam = (index2, decorator) => (target, key) => decorator(target,
12
12
 
13
13
  // runtime/subsystems/jobs/job-worker.module.ts
14
14
  import {
15
- Inject as Inject7,
16
- Injectable as Injectable8,
17
- Logger as Logger4,
15
+ Inject as Inject8,
16
+ Injectable as Injectable9,
17
+ Logger as Logger6,
18
18
  Module as Module2,
19
- Optional as Optional2
19
+ Optional as Optional3
20
20
  } from "@nestjs/common";
21
21
 
22
22
  // runtime/constants/tokens.ts
@@ -609,7 +609,58 @@ function notInStatus(statuses) {
609
609
 
610
610
  // runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
611
611
  import { Inject as Inject2, Injectable as Injectable2 } from "@nestjs/common";
612
- import { and as and2, asc, desc as desc2, eq as eq2, inArray as inArray2, isNull, sql as sql3 } from "drizzle-orm";
612
+ import { and as and2, asc, desc as desc2, eq as eq2, gte, inArray as inArray2, isNull, lt, or, sql as sql3 } from "drizzle-orm";
613
+
614
+ // runtime/subsystems/jobs/job-run-keyset-cursor.ts
615
+ var DEFAULT_LIST_LIMIT = 50;
616
+ var MAX_LIST_LIMIT = 200;
617
+ function clampLimit(limit) {
618
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
619
+ return DEFAULT_LIST_LIMIT;
620
+ }
621
+ const floored = Math.floor(limit);
622
+ if (floored < 1) return 1;
623
+ if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
624
+ return floored;
625
+ }
626
+ function encodeKeysetCursor(keyset) {
627
+ const tuple = [keyset.createdAt.toISOString(), keyset.id];
628
+ return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
629
+ }
630
+ function decodeKeysetCursor(cursor) {
631
+ try {
632
+ const json = Buffer.from(cursor, "base64url").toString("utf8");
633
+ const parsed = JSON.parse(json);
634
+ if (!Array.isArray(parsed) || parsed.length !== 2) return null;
635
+ const [iso, id] = parsed;
636
+ if (typeof iso !== "string" || typeof id !== "string") return null;
637
+ const createdAt = new Date(iso);
638
+ if (Number.isNaN(createdAt.getTime())) return null;
639
+ return { createdAt, id };
640
+ } catch {
641
+ return null;
642
+ }
643
+ }
644
+ function toJobRunSummary(r) {
645
+ return {
646
+ runId: r.id,
647
+ rootRunId: r.rootRunId,
648
+ jobType: r.jobType,
649
+ pool: r.pool,
650
+ status: r.status,
651
+ scopeEntityType: r.scopeEntityType,
652
+ scopeEntityId: r.scopeEntityId,
653
+ tenantId: r.tenantId,
654
+ attempts: r.attempts,
655
+ errorMessage: r.error?.message ?? null,
656
+ runAt: r.runAt,
657
+ startedAt: r.startedAt,
658
+ finishedAt: r.finishedAt,
659
+ createdAt: r.createdAt
660
+ };
661
+ }
662
+
663
+ // runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
613
664
  var NON_TERMINAL_STATUSES = [
614
665
  "pending",
615
666
  "running",
@@ -732,6 +783,37 @@ var DrizzleJobRunService = class {
732
783
  createdAt: r.createdAt
733
784
  }));
734
785
  }
786
+ async listJobRuns(query = {}) {
787
+ const limit = clampLimit(query.limit);
788
+ const conditions = [];
789
+ const tenantCond = this.tenantCondition("listJobRuns", query.tenantId);
790
+ if (tenantCond) conditions.push(tenantCond);
791
+ if (query.poolId) conditions.push(eq2(jobRuns.pool, query.poolId));
792
+ if (query.rootRunId) conditions.push(eq2(jobRuns.rootRunId, query.rootRunId));
793
+ if (query.status) conditions.push(eq2(jobRuns.status, query.status));
794
+ if (query.since) conditions.push(gte(jobRuns.createdAt, query.since));
795
+ if (query.cursor) {
796
+ const keyset = decodeKeysetCursor(query.cursor);
797
+ if (keyset) {
798
+ conditions.push(
799
+ or(
800
+ lt(jobRuns.createdAt, keyset.createdAt),
801
+ and2(
802
+ eq2(jobRuns.createdAt, keyset.createdAt),
803
+ lt(jobRuns.id, keyset.id)
804
+ )
805
+ )
806
+ );
807
+ }
808
+ }
809
+ 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);
810
+ const hasMore = rows.length > limit;
811
+ const page = hasMore ? rows.slice(0, limit) : rows;
812
+ const items = page.map(toJobRunSummary);
813
+ const last = page[page.length - 1];
814
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
815
+ return { items, nextCursor };
816
+ }
735
817
  /**
736
818
  * Internal helper used by cascade paths (not on the public protocol).
737
819
  * Exposed as a public method on the concrete class so infrastructure
@@ -793,9 +875,394 @@ DrizzleJobStepService = __decorateClass([
793
875
  __decorateParam(0, Inject3(DRIZZLE))
794
876
  ], DrizzleJobStepService);
795
877
 
878
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
879
+ import { createHash } from "crypto";
880
+ import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
881
+ import { eq as eq4 } from "drizzle-orm";
882
+
883
+ // runtime/subsystems/jobs/pool-config.loader.ts
884
+ import { existsSync, readFileSync } from "fs";
885
+ import { resolve } from "path";
886
+ import { parse as parseYaml } from "yaml";
887
+ var FRAMEWORK_POOLS = Object.freeze({
888
+ events_inbound: Object.freeze({
889
+ queue: "jobs-events-inbound",
890
+ concurrency: 20,
891
+ reserved: true,
892
+ description: "Inbound events drain (events subsystem outbox)."
893
+ }),
894
+ events_change: Object.freeze({
895
+ queue: "jobs-events-change",
896
+ concurrency: 30,
897
+ reserved: true,
898
+ description: "Change events drain (events subsystem outbox)."
899
+ }),
900
+ events_outbound: Object.freeze({
901
+ queue: "jobs-events-outbound",
902
+ concurrency: 10,
903
+ reserved: true,
904
+ description: "Outbound events drain (events subsystem outbox)."
905
+ }),
906
+ interactive: Object.freeze({
907
+ queue: "jobs-interactive",
908
+ concurrency: 20,
909
+ reserved: false,
910
+ description: "User-facing latency-sensitive jobs."
911
+ }),
912
+ batch: Object.freeze({
913
+ queue: "jobs-batch",
914
+ concurrency: 5,
915
+ reserved: false,
916
+ description: "Default pool for background jobs."
917
+ })
918
+ });
919
+ var RESERVED_POOL_NAMES = new Set(
920
+ Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
921
+ );
922
+ var cache = /* @__PURE__ */ new Map();
923
+ function loadPoolConfig(configPath) {
924
+ const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
925
+ const cached = cache.get(resolved);
926
+ if (cached) return cached;
927
+ const merged = /* @__PURE__ */ new Map();
928
+ for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
929
+ merged.set(name, { ...def });
930
+ }
931
+ if (!existsSync(resolved)) {
932
+ cache.set(resolved, merged);
933
+ return merged;
934
+ }
935
+ let raw;
936
+ try {
937
+ raw = parseYaml(readFileSync(resolved, "utf8"));
938
+ } catch (err) {
939
+ throw new Error(
940
+ `pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
941
+ );
942
+ }
943
+ const userPools = extractUserPools(raw);
944
+ for (const [name, userDef] of Object.entries(userPools)) {
945
+ const existing = merged.get(name);
946
+ if (existing) {
947
+ const next = {
948
+ queue: existing.queue,
949
+ concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
950
+ reserved: existing.reserved,
951
+ description: userDef.description ?? existing.description
952
+ };
953
+ merged.set(name, next);
954
+ continue;
955
+ }
956
+ if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
957
+ throw new Error(
958
+ `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
959
+ );
960
+ }
961
+ if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
962
+ throw new Error(
963
+ `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
964
+ );
965
+ }
966
+ if (userDef.reserved === true) {
967
+ throw new Error(
968
+ `pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
969
+ );
970
+ }
971
+ merged.set(name, {
972
+ queue: userDef.queue,
973
+ concurrency: userDef.concurrency,
974
+ reserved: false,
975
+ description: userDef.description
976
+ });
977
+ }
978
+ cache.set(resolved, merged);
979
+ return merged;
980
+ }
981
+ function allNonReservedPoolNames(config) {
982
+ const out = [];
983
+ for (const [name, def] of config) {
984
+ if (!def.reserved) out.push(name);
985
+ }
986
+ return out;
987
+ }
988
+ function allPoolNames(config) {
989
+ return [...config.keys()];
990
+ }
991
+ function extractUserPools(raw) {
992
+ if (!raw || typeof raw !== "object") return {};
993
+ const jobs2 = raw.jobs;
994
+ if (!jobs2 || typeof jobs2 !== "object") return {};
995
+ const pools = jobs2.pools;
996
+ if (!pools || typeof pools !== "object") return {};
997
+ const out = {};
998
+ for (const [name, def] of Object.entries(pools)) {
999
+ if (!def || typeof def !== "object") continue;
1000
+ out[name] = def;
1001
+ }
1002
+ return out;
1003
+ }
1004
+
1005
+ // runtime/subsystems/jobs/bullmq.config.ts
1006
+ var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
1007
+ var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
1008
+ var DEFAULT_REDIS_URL = "redis://localhost:6379";
1009
+ var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
1010
+ function resolveBullMqConfig(ext) {
1011
+ const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
1012
+ const resolved = {
1013
+ connection: { url },
1014
+ queuePrefix: ext?.queue_prefix
1015
+ };
1016
+ if (ext?.bull_board?.enabled) {
1017
+ resolved.bullBoard = {
1018
+ enabled: true,
1019
+ mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
1020
+ };
1021
+ }
1022
+ return resolved;
1023
+ }
1024
+ function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
1025
+ const alias = poolConfig.get(pool)?.queue ?? pool;
1026
+ const prefix = config?.queuePrefix;
1027
+ return prefix ? `${prefix}:${alias}` : alias;
1028
+ }
1029
+
1030
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
1031
+ function sha1JobId(idempotencyKey) {
1032
+ return createHash("sha1").update(idempotencyKey).digest("hex");
1033
+ }
1034
+ var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
1035
+ constructor(db, multiTenant, connection, bullConfig = null) {
1036
+ super(db, multiTenant);
1037
+ this.connection = connection;
1038
+ this.bullConfig = bullConfig;
1039
+ this.bullDb = db;
1040
+ }
1041
+ connection;
1042
+ bullConfig;
1043
+ // TODO(logging-subsystem): swap to ILogger once ADR-028 lands
1044
+ bullLogger = new Logger2(BullMQJobOrchestrator.name);
1045
+ /** Lazily-opened `Queue` handles, one per pool. */
1046
+ queues = /* @__PURE__ */ new Map();
1047
+ /** Single FlowProducer for parent/child hierarchies. Lazily opened. */
1048
+ _flow = null;
1049
+ /**
1050
+ * Cached `bullmq` value constructors, populated by `loadBullMq()` on first
1051
+ * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
1052
+ * a queue). Kept off the import graph so a `drizzle`-only consumer never
1053
+ * resolves the optional `'bullmq'` package.
1054
+ */
1055
+ QueueCtor = null;
1056
+ FlowProducerCtor = null;
1057
+ bullMqLoad = null;
1058
+ /**
1059
+ * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
1060
+ * `private` (can't be redeclared even privately in a subclass), and the
1061
+ * spec forbids touching that file — so the subclass keeps its own handle
1062
+ * under a distinct name (same instance, passed through to `super`) for the
1063
+ * cancel-cascade snapshot + definition/run loads below.
1064
+ */
1065
+ bullDb;
1066
+ /**
1067
+ * Lazily load the optional `bullmq` package and cache its value
1068
+ * constructors. Idempotent (single in-flight promise). Throws a friendly,
1069
+ * actionable error when the consumer selected `backend: 'bullmq'` but did
1070
+ * not install the package — mirrors `createRedisClient` in the redis event
1071
+ * backend. Must be `await`ed before any `queueFor`/`flow` access.
1072
+ */
1073
+ async loadBullMq() {
1074
+ if (this.QueueCtor && this.FlowProducerCtor) return;
1075
+ if (!this.bullMqLoad) {
1076
+ this.bullMqLoad = (async () => {
1077
+ try {
1078
+ const mod = await import("bullmq");
1079
+ this.QueueCtor = mod.Queue;
1080
+ this.FlowProducerCtor = mod.FlowProducer;
1081
+ } catch {
1082
+ throw new Error(
1083
+ 'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
1084
+ );
1085
+ }
1086
+ })();
1087
+ }
1088
+ await this.bullMqLoad;
1089
+ }
1090
+ /**
1091
+ * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
1092
+ * loadBullMq()` first so `QueueCtor` is populated.
1093
+ */
1094
+ queueFor(pool) {
1095
+ if (!this.QueueCtor) {
1096
+ throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
1097
+ }
1098
+ const name = resolvePoolQueueName(pool, this.bullConfig);
1099
+ let q = this.queues.get(name);
1100
+ if (!q) {
1101
+ q = new this.QueueCtor(name, { connection: this.connection });
1102
+ this.queues.set(name, q);
1103
+ }
1104
+ return q;
1105
+ }
1106
+ flow() {
1107
+ if (!this.FlowProducerCtor) {
1108
+ throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
1109
+ }
1110
+ if (!this._flow) {
1111
+ this._flow = new this.FlowProducerCtor({ connection: this.connection });
1112
+ }
1113
+ return this._flow;
1114
+ }
1115
+ // ==========================================================================
1116
+ // start — Postgres insert (super) + BullMQ dispatch
1117
+ // ==========================================================================
1118
+ async start(type, input, opts = {}, tx) {
1119
+ const run = await super.start(type, input, opts, tx);
1120
+ await this.dispatch(run, type);
1121
+ return run;
1122
+ }
1123
+ /**
1124
+ * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
1125
+ * `parentRunId` we attach it to the parent's existing BullMQ job through the
1126
+ * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
1127
+ * its own graph. (The FlowProducer is reserved for whole-tree atomic
1128
+ * submits, exposed as an opt-in extension via `flowProducer()`; runtime
1129
+ * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
1130
+ * correct primitive here.)
1131
+ *
1132
+ * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
1133
+ * present (so the same logical key dedups), else the `job_run.id` UUID
1134
+ * (already colon-free).
1135
+ *
1136
+ * The domain `parentClosePolicy` cascade is still enforced in Postgres by
1137
+ * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
1138
+ * not the authority.
1139
+ */
1140
+ async dispatch(run, type) {
1141
+ await this.loadBullMq();
1142
+ const def = await this.loadDefinition(type);
1143
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
1144
+ const jobOpts = {
1145
+ jobId,
1146
+ ...this.retryOpts(def),
1147
+ ...this.dedupeOpts(run, def)
1148
+ };
1149
+ if (run.parentRunId) {
1150
+ const parentRow = await this.loadRun(run.parentRunId);
1151
+ if (parentRow) {
1152
+ const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
1153
+ jobOpts.parent = {
1154
+ id: parentJobId,
1155
+ queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
1156
+ };
1157
+ }
1158
+ }
1159
+ const payload = { runId: run.id, type, input: run.input };
1160
+ await this.queueFor(run.pool).add(type, payload, jobOpts);
1161
+ }
1162
+ /**
1163
+ * Opt-in extension (spec §Extensions): expose the FlowProducer for
1164
+ * consumers that want to submit a whole parent/child DAG atomically up
1165
+ * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
1166
+ * code using it is not portable to the Drizzle backend. Async because it
1167
+ * lazily loads the optional `bullmq` package on first use.
1168
+ */
1169
+ async flowProducer() {
1170
+ await this.loadBullMq();
1171
+ return this.flow();
1172
+ }
1173
+ retryOpts(def) {
1174
+ const policy = def.retryPolicy;
1175
+ if (!policy) return {};
1176
+ return {
1177
+ attempts: policy.attempts,
1178
+ backoff: {
1179
+ type: policy.backoff === "exponential" ? "exponential" : "fixed",
1180
+ delay: policy.baseMs
1181
+ }
1182
+ };
1183
+ }
1184
+ dedupeOpts(run, def) {
1185
+ if (!run.dedupeKey || !def.dedupeWindowMs) return {};
1186
+ return {
1187
+ deduplication: {
1188
+ id: sha1JobId(run.dedupeKey),
1189
+ ttl: def.dedupeWindowMs
1190
+ }
1191
+ };
1192
+ }
1193
+ // ==========================================================================
1194
+ // cancel — Postgres cascade (super) + remove from queue
1195
+ // ==========================================================================
1196
+ async cancel(runId, opts = {}) {
1197
+ const target = await this.loadRun(runId);
1198
+ await super.cancel(runId, opts);
1199
+ if (!target) return;
1200
+ await this.loadBullMq();
1201
+ await this.removeFromQueue(target);
1202
+ if (opts.cascade === false) return;
1203
+ const descendants = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.rootRunId, target.rootRunId));
1204
+ for (const child of descendants) {
1205
+ if (child.id === runId) continue;
1206
+ await this.removeFromQueue(child);
1207
+ }
1208
+ }
1209
+ async removeFromQueue(run) {
1210
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
1211
+ try {
1212
+ const job = await this.queueFor(run.pool).getJob(jobId);
1213
+ if (job) await job.remove();
1214
+ } catch (err) {
1215
+ this.bullLogger.warn(
1216
+ `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
1217
+ );
1218
+ }
1219
+ }
1220
+ // ==========================================================================
1221
+ // replay — Postgres reset (super) + re-enqueue
1222
+ // ==========================================================================
1223
+ async replay(runId) {
1224
+ const run = await super.replay(runId);
1225
+ await this.dispatch(run, run.jobType);
1226
+ return run;
1227
+ }
1228
+ // ==========================================================================
1229
+ // Internals
1230
+ // ==========================================================================
1231
+ async loadDefinition(type) {
1232
+ const [def] = await this.bullDb.select().from(jobs).where(eq4(jobs.type, type)).limit(1);
1233
+ if (!def) {
1234
+ throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
1235
+ }
1236
+ return def;
1237
+ }
1238
+ async loadRun(id) {
1239
+ const [row] = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.id, id)).limit(1);
1240
+ return row ?? null;
1241
+ }
1242
+ /** Close all open queue + flow connections. Called on module destroy. */
1243
+ async closeConnections() {
1244
+ for (const q of this.queues.values()) {
1245
+ await q.close().catch(() => void 0);
1246
+ }
1247
+ this.queues.clear();
1248
+ if (this._flow) {
1249
+ await this._flow.close().catch(() => void 0);
1250
+ this._flow = null;
1251
+ }
1252
+ }
1253
+ };
1254
+ BullMQJobOrchestrator = __decorateClass([
1255
+ Injectable4(),
1256
+ __decorateParam(0, Inject4(DRIZZLE)),
1257
+ __decorateParam(1, Inject4(JOBS_MULTI_TENANT)),
1258
+ __decorateParam(2, Inject4(BULLMQ_CONNECTION)),
1259
+ __decorateParam(3, Optional()),
1260
+ __decorateParam(3, Inject4(BULLMQ_RESOLVED_CONFIG))
1261
+ ], BullMQJobOrchestrator);
1262
+
796
1263
  // runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
797
1264
  import { randomUUID as randomUUID2 } from "crypto";
798
- import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
1265
+ import { Inject as Inject5, Injectable as Injectable5, Logger as Logger3, Optional as Optional2 } from "@nestjs/common";
799
1266
  var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
800
1267
  var TERMINAL_STATUSES2 = [
801
1268
  "completed",
@@ -842,7 +1309,7 @@ var MemoryJobOrchestrator = class {
842
1309
  stepService;
843
1310
  multiTenant;
844
1311
  moduleRef;
845
- logger = new Logger2(MemoryJobOrchestrator.name);
1312
+ logger = new Logger3(MemoryJobOrchestrator.name);
846
1313
  mutex = new PromiseMutex();
847
1314
  handlerRegistry = /* @__PURE__ */ new Map();
848
1315
  /**
@@ -1191,7 +1658,7 @@ var MemoryJobOrchestrator = class {
1191
1658
  run,
1192
1659
  step: this.makeStepFn(run),
1193
1660
  spawnChild: this.makeSpawnFn(run),
1194
- logger: new Logger2(`JobRun:${run.id}`)
1661
+ logger: new Logger3(`JobRun:${run.id}`)
1195
1662
  };
1196
1663
  const attemptsBefore = run.attempts ?? 0;
1197
1664
  try {
@@ -1364,9 +1831,9 @@ var MemoryJobOrchestrator = class {
1364
1831
  }
1365
1832
  };
1366
1833
  MemoryJobOrchestrator = __decorateClass([
1367
- Injectable4(),
1368
- __decorateParam(2, Inject4(JOBS_MULTI_TENANT)),
1369
- __decorateParam(3, Optional())
1834
+ Injectable5(),
1835
+ __decorateParam(2, Inject5(JOBS_MULTI_TENANT)),
1836
+ __decorateParam(3, Optional2())
1370
1837
  ], MemoryJobOrchestrator);
1371
1838
  function classifyError(err, policy, currentAttempts) {
1372
1839
  if (!policy) return "fail";
@@ -1400,7 +1867,7 @@ function serialiseError(err, attempt, retryable) {
1400
1867
  }
1401
1868
 
1402
1869
  // runtime/subsystems/jobs/job-run-service.memory-backend.ts
1403
- import { Inject as Inject5, Injectable as Injectable5 } from "@nestjs/common";
1870
+ import { Inject as Inject6, Injectable as Injectable6 } from "@nestjs/common";
1404
1871
  var NON_TERMINAL_STATUSES2 = [
1405
1872
  "pending",
1406
1873
  "running",
@@ -1517,6 +1984,38 @@ var MemoryJobRunService = class {
1517
1984
  createdAt: r.createdAt
1518
1985
  }));
1519
1986
  }
1987
+ async listJobRuns(query = {}) {
1988
+ const limit = clampLimit(query.limit);
1989
+ const tenantCheck = this.tenantPredicate("listJobRuns", query.tenantId);
1990
+ const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;
1991
+ const matched = [];
1992
+ for (const r of this.store.runs.values()) {
1993
+ if (tenantCheck && !tenantCheck(r)) continue;
1994
+ if (query.poolId && r.pool !== query.poolId) continue;
1995
+ if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;
1996
+ if (query.status && r.status !== query.status) continue;
1997
+ if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;
1998
+ matched.push(r);
1999
+ }
2000
+ matched.sort((a, b) => {
2001
+ const dt = b.createdAt.getTime() - a.createdAt.getTime();
2002
+ if (dt !== 0) return dt;
2003
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
2004
+ });
2005
+ const seeked = keyset ? matched.filter((r) => {
2006
+ const ct = r.createdAt.getTime();
2007
+ const kt = keyset.createdAt.getTime();
2008
+ if (ct < kt) return true;
2009
+ if (ct > kt) return false;
2010
+ return r.id < keyset.id;
2011
+ }) : matched;
2012
+ const hasMore = seeked.length > limit;
2013
+ const page = hasMore ? seeked.slice(0, limit) : seeked;
2014
+ const items = page.map(toJobRunSummary);
2015
+ const last = page[page.length - 1];
2016
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
2017
+ return { items, nextCursor };
2018
+ }
1520
2019
  /**
1521
2020
  * Direct lookup. Not on the protocol — concrete-class convenience for
1522
2021
  * tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
@@ -1535,9 +2034,9 @@ var MemoryJobRunService = class {
1535
2034
  }
1536
2035
  };
1537
2036
  MemoryJobRunService = __decorateClass([
1538
- Injectable5(),
1539
- __decorateParam(1, Inject5(JOB_ORCHESTRATOR)),
1540
- __decorateParam(2, Inject5(JOBS_MULTI_TENANT))
2037
+ Injectable6(),
2038
+ __decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
2039
+ __decorateParam(2, Inject6(JOBS_MULTI_TENANT))
1541
2040
  ], MemoryJobRunService);
1542
2041
  function compareBy(a, b, order) {
1543
2042
  switch (order) {
@@ -1555,7 +2054,7 @@ function compareBy(a, b, order) {
1555
2054
 
1556
2055
  // runtime/subsystems/jobs/job-step-service.memory-backend.ts
1557
2056
  import { randomUUID as randomUUID3 } from "crypto";
1558
- import { Injectable as Injectable6 } from "@nestjs/common";
2057
+ import { Injectable as Injectable7 } from "@nestjs/common";
1559
2058
  var MemoryJobStepService = class {
1560
2059
  constructor(store) {
1561
2060
  this.store = store;
@@ -1648,7 +2147,7 @@ var MemoryJobStepService = class {
1648
2147
  }
1649
2148
  };
1650
2149
  MemoryJobStepService = __decorateClass([
1651
- Injectable6()
2150
+ Injectable7()
1652
2151
  ], MemoryJobStepService);
1653
2152
 
1654
2153
  // runtime/subsystems/jobs/memory-job-store.ts
@@ -1670,7 +2169,6 @@ var MemoryJobStore = class {
1670
2169
  // runtime/subsystems/jobs/jobs-domain.module.ts
1671
2170
  var JobsDomainModule = class {
1672
2171
  static forRoot(opts) {
1673
- void opts.extensions;
1674
2172
  const multiTenant = opts.multiTenant ?? false;
1675
2173
  const providers = [
1676
2174
  // JOB-8 — boolean provider consumed by the four service-layer backends.
@@ -1689,21 +2187,32 @@ var JobsDomainModule = class {
1689
2187
  providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
1690
2188
  providers.push(MemoryJobRunService);
1691
2189
  providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
2190
+ } else if (opts.backend === "bullmq") {
2191
+ const resolved = resolveBullMqConfig(opts.extensions?.bullmq);
2192
+ providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });
2193
+ providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });
2194
+ providers.push({ provide: JOB_ORCHESTRATOR, useClass: BullMQJobOrchestrator });
2195
+ providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
2196
+ providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
1692
2197
  } else {
1693
2198
  providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });
1694
2199
  providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
1695
2200
  providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
1696
2201
  }
2202
+ const exports = [
2203
+ JOB_ORCHESTRATOR,
2204
+ JOB_RUN_SERVICE,
2205
+ JOB_STEP_SERVICE,
2206
+ JOBS_MULTI_TENANT
2207
+ ];
2208
+ if (opts.backend === "bullmq") {
2209
+ exports.push(BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG);
2210
+ }
1697
2211
  return {
1698
2212
  module: JobsDomainModule,
1699
2213
  global: true,
1700
2214
  providers,
1701
- exports: [
1702
- JOB_ORCHESTRATOR,
1703
- JOB_RUN_SERVICE,
1704
- JOB_STEP_SERVICE,
1705
- JOBS_MULTI_TENANT
1706
- ]
2215
+ exports
1707
2216
  };
1708
2217
  }
1709
2218
  };
@@ -1711,128 +2220,9 @@ JobsDomainModule = __decorateClass([
1711
2220
  Module({})
1712
2221
  ], JobsDomainModule);
1713
2222
 
1714
- // runtime/subsystems/jobs/pool-config.loader.ts
1715
- import { existsSync, readFileSync } from "fs";
1716
- import { resolve } from "path";
1717
- import { parse as parseYaml } from "yaml";
1718
- var FRAMEWORK_POOLS = Object.freeze({
1719
- events_inbound: Object.freeze({
1720
- queue: "jobs-events-inbound",
1721
- concurrency: 20,
1722
- reserved: true,
1723
- description: "Inbound events drain (events subsystem outbox)."
1724
- }),
1725
- events_change: Object.freeze({
1726
- queue: "jobs-events-change",
1727
- concurrency: 30,
1728
- reserved: true,
1729
- description: "Change events drain (events subsystem outbox)."
1730
- }),
1731
- events_outbound: Object.freeze({
1732
- queue: "jobs-events-outbound",
1733
- concurrency: 10,
1734
- reserved: true,
1735
- description: "Outbound events drain (events subsystem outbox)."
1736
- }),
1737
- interactive: Object.freeze({
1738
- queue: "jobs-interactive",
1739
- concurrency: 20,
1740
- reserved: false,
1741
- description: "User-facing latency-sensitive jobs."
1742
- }),
1743
- batch: Object.freeze({
1744
- queue: "jobs-batch",
1745
- concurrency: 5,
1746
- reserved: false,
1747
- description: "Default pool for background jobs."
1748
- })
1749
- });
1750
- var RESERVED_POOL_NAMES = new Set(
1751
- Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
1752
- );
1753
- var cache = /* @__PURE__ */ new Map();
1754
- function loadPoolConfig(configPath) {
1755
- const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
1756
- const cached = cache.get(resolved);
1757
- if (cached) return cached;
1758
- const merged = /* @__PURE__ */ new Map();
1759
- for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
1760
- merged.set(name, { ...def });
1761
- }
1762
- if (!existsSync(resolved)) {
1763
- cache.set(resolved, merged);
1764
- return merged;
1765
- }
1766
- let raw;
1767
- try {
1768
- raw = parseYaml(readFileSync(resolved, "utf8"));
1769
- } catch (err) {
1770
- throw new Error(
1771
- `pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
1772
- );
1773
- }
1774
- const userPools = extractUserPools(raw);
1775
- for (const [name, userDef] of Object.entries(userPools)) {
1776
- const existing = merged.get(name);
1777
- if (existing) {
1778
- const next = {
1779
- queue: existing.queue,
1780
- concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
1781
- reserved: existing.reserved,
1782
- description: userDef.description ?? existing.description
1783
- };
1784
- merged.set(name, next);
1785
- continue;
1786
- }
1787
- if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
1788
- throw new Error(
1789
- `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
1790
- );
1791
- }
1792
- if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
1793
- throw new Error(
1794
- `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
1795
- );
1796
- }
1797
- if (userDef.reserved === true) {
1798
- throw new Error(
1799
- `pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
1800
- );
1801
- }
1802
- merged.set(name, {
1803
- queue: userDef.queue,
1804
- concurrency: userDef.concurrency,
1805
- reserved: false,
1806
- description: userDef.description
1807
- });
1808
- }
1809
- cache.set(resolved, merged);
1810
- return merged;
1811
- }
1812
- function allNonReservedPoolNames(config) {
1813
- const out = [];
1814
- for (const [name, def] of config) {
1815
- if (!def.reserved) out.push(name);
1816
- }
1817
- return out;
1818
- }
1819
- function extractUserPools(raw) {
1820
- if (!raw || typeof raw !== "object") return {};
1821
- const jobs2 = raw.jobs;
1822
- if (!jobs2 || typeof jobs2 !== "object") return {};
1823
- const pools = jobs2.pools;
1824
- if (!pools || typeof pools !== "object") return {};
1825
- const out = {};
1826
- for (const [name, def] of Object.entries(pools)) {
1827
- if (!def || typeof def !== "object") continue;
1828
- out[name] = def;
1829
- }
1830
- return out;
1831
- }
1832
-
1833
2223
  // runtime/subsystems/jobs/job-worker.ts
1834
- import { Inject as Inject6, Injectable as Injectable7, Logger as Logger3 } from "@nestjs/common";
1835
- import { and as and4, asc as asc2, desc as desc3, eq as eq4, inArray as inArray3, lt, lte, sql as sql4 } from "drizzle-orm";
2224
+ import { Inject as Inject7, Injectable as Injectable8, Logger as Logger4 } from "@nestjs/common";
2225
+ 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";
1836
2226
  var JOB_WORKER_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_OPTIONS");
1837
2227
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
1838
2228
  var DEFAULT_STALE_SWEEPER_INTERVAL_MS = 6e4;
@@ -1895,7 +2285,7 @@ var JobWorker = class {
1895
2285
  stepService;
1896
2286
  options;
1897
2287
  moduleRef;
1898
- logger = new Logger3(JobWorker.name);
2288
+ logger = new Logger4(JobWorker.name);
1899
2289
  shuttingDown = false;
1900
2290
  inFlight = /* @__PURE__ */ new Set();
1901
2291
  pollTimer = null;
@@ -1936,7 +2326,7 @@ var JobWorker = class {
1936
2326
  await this.drainInFlight();
1937
2327
  try {
1938
2328
  await this.db.update(jobRuns).set({ status: "pending", claimedAt: null, startedAt: null }).where(
1939
- and4(eq4(jobRuns.status, "running"), eq4(jobRuns.pool, this.options.pool))
2329
+ and4(eq5(jobRuns.status, "running"), eq5(jobRuns.pool, this.options.pool))
1940
2330
  );
1941
2331
  } catch (err) {
1942
2332
  this.logger.error(`shutdown reset failed: ${err.message}`);
@@ -1986,8 +2376,8 @@ var JobWorker = class {
1986
2376
  return this.db.transaction(async (tx) => {
1987
2377
  const candidates = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
1988
2378
  and4(
1989
- eq4(jobRuns.status, "pending"),
1990
- eq4(jobRuns.pool, pool),
2379
+ eq5(jobRuns.status, "pending"),
2380
+ eq5(jobRuns.pool, pool),
1991
2381
  lte(jobRuns.runAt, /* @__PURE__ */ new Date())
1992
2382
  )
1993
2383
  ).orderBy(desc3(jobRuns.priority), asc2(jobRuns.runAt)).limit(1).for("update", { skipLocked: true });
@@ -1998,7 +2388,7 @@ var JobWorker = class {
1998
2388
  claimedAt: /* @__PURE__ */ new Date(),
1999
2389
  startedAt: /* @__PURE__ */ new Date(),
2000
2390
  updatedAt: /* @__PURE__ */ new Date()
2001
- }).where(eq4(jobRuns.id, candidate.id)).returning();
2391
+ }).where(eq5(jobRuns.id, candidate.id)).returning();
2002
2392
  return claimed ?? null;
2003
2393
  });
2004
2394
  }
@@ -2016,7 +2406,7 @@ var JobWorker = class {
2016
2406
  await this.db.transaction(async (tx) => {
2017
2407
  const threshold = new Date(Date.now() - this.staleThresholdMs);
2018
2408
  const stale = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
2019
- and4(eq4(jobRuns.status, "running"), lt(jobRuns.claimedAt, threshold))
2409
+ and4(eq5(jobRuns.status, "running"), lt2(jobRuns.claimedAt, threshold))
2020
2410
  ).for("update", { skipLocked: true });
2021
2411
  if (stale.length === 0) return;
2022
2412
  const ids = stale.map((r) => r.id);
@@ -2049,8 +2439,8 @@ var JobWorker = class {
2049
2439
  if (claimed.concurrencyKey) {
2050
2440
  const inflight = await this.db.select({ id: jobRuns.id }).from(jobRuns).where(
2051
2441
  and4(
2052
- eq4(jobRuns.concurrencyKey, claimed.concurrencyKey),
2053
- eq4(jobRuns.status, "running")
2442
+ eq5(jobRuns.concurrencyKey, claimed.concurrencyKey),
2443
+ eq5(jobRuns.status, "running")
2054
2444
  )
2055
2445
  );
2056
2446
  const other = inflight.find((r) => r.id !== claimed.id);
@@ -2060,7 +2450,7 @@ var JobWorker = class {
2060
2450
  claimedAt: null,
2061
2451
  startedAt: null,
2062
2452
  updatedAt: /* @__PURE__ */ new Date()
2063
- }).where(eq4(jobRuns.id, claimed.id));
2453
+ }).where(eq5(jobRuns.id, claimed.id));
2064
2454
  return;
2065
2455
  }
2066
2456
  }
@@ -2075,7 +2465,7 @@ var JobWorker = class {
2075
2465
  run: claimed,
2076
2466
  step: this.makeStepFn(claimed),
2077
2467
  spawnChild: this.makeSpawnFn(claimed),
2078
- logger: new Logger3(`JobRun:${claimed.id}`)
2468
+ logger: new Logger4(`JobRun:${claimed.id}`)
2079
2469
  };
2080
2470
  const attemptsBefore = claimed.attempts ?? 0;
2081
2471
  try {
@@ -2086,7 +2476,7 @@ var JobWorker = class {
2086
2476
  finishedAt: /* @__PURE__ */ new Date(),
2087
2477
  updatedAt: /* @__PURE__ */ new Date(),
2088
2478
  attempts: attemptsBefore + 1
2089
- }).where(eq4(jobRuns.id, claimed.id));
2479
+ }).where(eq5(jobRuns.id, claimed.id));
2090
2480
  } catch (err) {
2091
2481
  const policy = meta.retry;
2092
2482
  const decision = classifyError2(err, policy, attemptsBefore);
@@ -2101,7 +2491,7 @@ var JobWorker = class {
2101
2491
  claimedAt: null,
2102
2492
  error: serialiseError2(err, nextAttempts, true),
2103
2493
  updatedAt: /* @__PURE__ */ new Date()
2104
- }).where(eq4(jobRuns.id, claimed.id));
2494
+ }).where(eq5(jobRuns.id, claimed.id));
2105
2495
  } else {
2106
2496
  await this.markFailed(claimed, err, nextAttempts);
2107
2497
  }
@@ -2114,7 +2504,7 @@ var JobWorker = class {
2114
2504
  finishedAt: /* @__PURE__ */ new Date(),
2115
2505
  error: serialiseError2(err, finalAttempts, false),
2116
2506
  updatedAt: /* @__PURE__ */ new Date()
2117
- }).where(eq4(jobRuns.id, claimed.id));
2507
+ }).where(eq5(jobRuns.id, claimed.id));
2118
2508
  if (claimed.parentClosePolicy === "terminate") {
2119
2509
  try {
2120
2510
  await this.orchestrator.cancel(claimed.id, {
@@ -2217,25 +2607,228 @@ var JobWorker = class {
2217
2607
  // ============================================================================
2218
2608
  };
2219
2609
  JobWorker = __decorateClass([
2220
- Injectable7(),
2221
- __decorateParam(0, Inject6(DRIZZLE)),
2222
- __decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
2223
- __decorateParam(2, Inject6(JOB_RUN_SERVICE)),
2224
- __decorateParam(3, Inject6(JOB_STEP_SERVICE)),
2225
- __decorateParam(4, Inject6(JOB_WORKER_OPTIONS))
2610
+ Injectable8(),
2611
+ __decorateParam(0, Inject7(DRIZZLE)),
2612
+ __decorateParam(1, Inject7(JOB_ORCHESTRATOR)),
2613
+ __decorateParam(2, Inject7(JOB_RUN_SERVICE)),
2614
+ __decorateParam(3, Inject7(JOB_STEP_SERVICE)),
2615
+ __decorateParam(4, Inject7(JOB_WORKER_OPTIONS))
2226
2616
  ], JobWorker);
2227
2617
 
2618
+ // runtime/subsystems/jobs/job-worker.bullmq-backend.ts
2619
+ import { Logger as Logger5 } from "@nestjs/common";
2620
+ import { eq as eq6 } from "drizzle-orm";
2621
+ function serialiseError3(err, attempt, retryable) {
2622
+ const e = err;
2623
+ return {
2624
+ message: e?.message ?? String(err),
2625
+ stack: e?.stack,
2626
+ retryable,
2627
+ attempt
2628
+ };
2629
+ }
2630
+ var BullMQJobWorker = class _BullMQJobWorker {
2631
+ constructor(db, orchestrator, stepService, options, moduleRef) {
2632
+ this.db = db;
2633
+ this.orchestrator = orchestrator;
2634
+ this.stepService = stepService;
2635
+ this.options = options;
2636
+ this.moduleRef = moduleRef;
2637
+ }
2638
+ db;
2639
+ orchestrator;
2640
+ stepService;
2641
+ options;
2642
+ moduleRef;
2643
+ logger = new Logger5(_BullMQJobWorker.name);
2644
+ worker = null;
2645
+ async onModuleInit() {
2646
+ let WorkerCtor;
2647
+ try {
2648
+ const mod = await import("bullmq");
2649
+ WorkerCtor = mod.Worker;
2650
+ } catch {
2651
+ throw new Error(
2652
+ 'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
2653
+ );
2654
+ }
2655
+ this.worker = new WorkerCtor(
2656
+ this.options.queueName,
2657
+ (job) => this.process(job),
2658
+ {
2659
+ connection: this.options.connection,
2660
+ concurrency: this.options.concurrency
2661
+ }
2662
+ );
2663
+ this.worker.on("failed", (job, err) => {
2664
+ if (!job) return;
2665
+ const attemptsMade = job.attemptsMade;
2666
+ const maxAttempts = job.opts.attempts ?? 1;
2667
+ if (attemptsMade >= maxAttempts) {
2668
+ void this.markFailed(job.data.runId, err, attemptsMade);
2669
+ }
2670
+ });
2671
+ this.logger.log(
2672
+ `BullMQ worker started: pool='${this.options.pool}' queue='${this.options.queueName}' concurrency=${this.options.concurrency}`
2673
+ );
2674
+ }
2675
+ async onModuleDestroy() {
2676
+ if (this.worker) {
2677
+ await this.worker.close();
2678
+ this.worker = null;
2679
+ }
2680
+ }
2681
+ /**
2682
+ * Process one BullMQ job. Returns the handler output (stored by BullMQ as
2683
+ * the job return value AND written to `job_run.output`). Throws on handler
2684
+ * failure so BullMQ applies the retry policy.
2685
+ */
2686
+ async process(job) {
2687
+ const { runId } = job.data;
2688
+ const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
2689
+ if (!row) {
2690
+ this.logger.warn(`process: job_run ${runId} not found; skipping`);
2691
+ return {};
2692
+ }
2693
+ const run = row;
2694
+ if (run.status === "canceled") {
2695
+ return {};
2696
+ }
2697
+ const registryEntry = JOB_HANDLER_REGISTRY.get(run.jobType);
2698
+ if (!registryEntry) {
2699
+ throw new Error(
2700
+ `No handler registered for jobType='${run.jobType}' (run ${run.id})`
2701
+ );
2702
+ }
2703
+ await this.db.update(jobRuns).set({
2704
+ status: "running",
2705
+ claimedAt: /* @__PURE__ */ new Date(),
2706
+ startedAt: /* @__PURE__ */ new Date(),
2707
+ attempts: job.attemptsMade + 1,
2708
+ updatedAt: /* @__PURE__ */ new Date()
2709
+ }).where(eq6(jobRuns.id, run.id));
2710
+ const HandlerClass = registryEntry.handlerClass;
2711
+ const handler = this.moduleRef.get(
2712
+ HandlerClass,
2713
+ { strict: false }
2714
+ );
2715
+ const ctx = {
2716
+ input: run.input,
2717
+ run,
2718
+ step: this.makeStepFn(run),
2719
+ spawnChild: this.makeSpawnFn(run),
2720
+ logger: new Logger5(`JobRun:${run.id}`)
2721
+ };
2722
+ const output = await handler.run(ctx);
2723
+ await this.db.update(jobRuns).set({
2724
+ status: "completed",
2725
+ output: output ?? {},
2726
+ finishedAt: /* @__PURE__ */ new Date(),
2727
+ updatedAt: /* @__PURE__ */ new Date()
2728
+ }).where(eq6(jobRuns.id, run.id));
2729
+ return output ?? {};
2730
+ }
2731
+ async markFailed(runId, err, finalAttempts) {
2732
+ const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
2733
+ if (!row) return;
2734
+ const run = row;
2735
+ await this.db.update(jobRuns).set({
2736
+ status: "failed",
2737
+ attempts: finalAttempts,
2738
+ finishedAt: /* @__PURE__ */ new Date(),
2739
+ error: serialiseError3(err, finalAttempts, false),
2740
+ updatedAt: /* @__PURE__ */ new Date()
2741
+ }).where(eq6(jobRuns.id, runId));
2742
+ if (run.parentClosePolicy === "terminate") {
2743
+ try {
2744
+ await this.orchestrator.cancel(run.id, {
2745
+ cascade: true,
2746
+ reason: "parent-failed",
2747
+ tenantId: run.tenantId
2748
+ });
2749
+ } catch (cascadeErr) {
2750
+ this.logger.warn(
2751
+ `cascade on failed run ${run.id}: ${cascadeErr.message}`
2752
+ );
2753
+ }
2754
+ }
2755
+ }
2756
+ // ── ctx.step / ctx.spawnChild (mirror JobWorker) ──────────────────────────
2757
+ makeStepFn(run) {
2758
+ return async (stepId, fn, _opts) => {
2759
+ void _opts;
2760
+ const existing = await this.stepService.findStep(run.id, stepId);
2761
+ if (existing?.status === "completed") {
2762
+ return existing.output;
2763
+ }
2764
+ const nextAttempts = (existing?.attempts ?? 0) + 1;
2765
+ const seq = nextAttempts;
2766
+ await this.stepService.recordStep({
2767
+ jobRunId: run.id,
2768
+ stepId,
2769
+ kind: "task",
2770
+ seq,
2771
+ status: "running",
2772
+ startedAt: /* @__PURE__ */ new Date(),
2773
+ attempts: nextAttempts
2774
+ });
2775
+ try {
2776
+ const output = await fn();
2777
+ await this.stepService.recordStep({
2778
+ jobRunId: run.id,
2779
+ stepId,
2780
+ kind: "task",
2781
+ seq,
2782
+ status: "completed",
2783
+ output,
2784
+ finishedAt: /* @__PURE__ */ new Date(),
2785
+ attempts: nextAttempts
2786
+ });
2787
+ return output;
2788
+ } catch (err) {
2789
+ await this.stepService.recordStep({
2790
+ jobRunId: run.id,
2791
+ stepId,
2792
+ kind: "task",
2793
+ seq,
2794
+ status: "failed",
2795
+ error: serialiseError3(err, nextAttempts, false),
2796
+ finishedAt: /* @__PURE__ */ new Date(),
2797
+ attempts: nextAttempts
2798
+ });
2799
+ throw err;
2800
+ }
2801
+ };
2802
+ }
2803
+ makeSpawnFn(run) {
2804
+ return async (type, input, opts) => {
2805
+ return this.orchestrator.start(type, input, {
2806
+ parentRunId: run.id,
2807
+ parentClosePolicy: opts?.closePolicy,
2808
+ runAt: opts?.runAt,
2809
+ priority: opts?.priority,
2810
+ tags: opts?.tags,
2811
+ triggerSource: "parent",
2812
+ triggerRef: run.id,
2813
+ tenantId: run.tenantId
2814
+ });
2815
+ };
2816
+ }
2817
+ };
2818
+
2228
2819
  // runtime/subsystems/jobs/job-worker.module.ts
2229
2820
  var DEFAULT_SHUTDOWN_TIMEOUT_MS2 = 3e4;
2230
2821
  var JOB_WORKER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_MODULE_OPTIONS");
2231
2822
  var JobWorkerOrchestrator = class {
2232
- constructor(orchestrator, runService, stepService, options, db = null, moduleRef) {
2823
+ constructor(orchestrator, runService, stepService, options, db = null, moduleRef, bullConnection = null, bullConfig = null) {
2233
2824
  this.orchestrator = orchestrator;
2234
2825
  this.runService = runService;
2235
2826
  this.stepService = stepService;
2236
2827
  this.options = options;
2237
2828
  this.db = db;
2238
2829
  this.moduleRef = moduleRef;
2830
+ this.bullConnection = bullConnection;
2831
+ this.bullConfig = bullConfig;
2239
2832
  }
2240
2833
  orchestrator;
2241
2834
  runService;
@@ -2243,7 +2836,9 @@ var JobWorkerOrchestrator = class {
2243
2836
  options;
2244
2837
  db;
2245
2838
  moduleRef;
2246
- logger = new Logger4(JobWorkerOrchestrator.name);
2839
+ bullConnection;
2840
+ bullConfig;
2841
+ logger = new Logger6(JobWorkerOrchestrator.name);
2247
2842
  workers = [];
2248
2843
  // ============================================================================
2249
2844
  // Lifecycle
@@ -2260,7 +2855,7 @@ var JobWorkerOrchestrator = class {
2260
2855
  if (backend !== "memory" && orphaned.length > 0) {
2261
2856
  throw new BootValidationError(orphaned);
2262
2857
  }
2263
- const activePools = this.options.pools ?? allNonReservedPoolNames(poolConfig);
2858
+ const activePools = this.options.pools ? this.options.pools : this.options.allPools ? allPoolNames(poolConfig) : allNonReservedPoolNames(poolConfig);
2264
2859
  for (const poolName of activePools) {
2265
2860
  const def = poolConfig.get(poolName);
2266
2861
  if (!def) {
@@ -2273,11 +2868,11 @@ var JobWorkerOrchestrator = class {
2273
2868
  concurrency: def.concurrency,
2274
2869
  shutdownTimeoutMs: this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS2
2275
2870
  };
2276
- const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : this.spawnWorker(workerOptions);
2277
- worker.onModuleInit();
2871
+ const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : backend === "bullmq" ? this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig) : this.spawnWorker(workerOptions);
2872
+ await worker.onModuleInit();
2278
2873
  this.workers.push(worker);
2279
2874
  this.logger.log(
2280
- `JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency}`
2875
+ `JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency} backend='${backend}'`
2281
2876
  );
2282
2877
  }
2283
2878
  }
@@ -2294,6 +2889,16 @@ var JobWorkerOrchestrator = class {
2294
2889
  }
2295
2890
  }
2296
2891
  this.workers.length = 0;
2892
+ const orch = this.orchestrator;
2893
+ if (typeof orch.closeConnections === "function") {
2894
+ try {
2895
+ await orch.closeConnections();
2896
+ } catch (err) {
2897
+ this.logger.error(
2898
+ `BullMQ orchestrator connection close failed: ${err.message}`
2899
+ );
2900
+ }
2901
+ }
2297
2902
  }
2298
2903
  // ============================================================================
2299
2904
  // Internals
@@ -2355,15 +2960,57 @@ var JobWorkerOrchestrator = class {
2355
2960
  this.moduleRef
2356
2961
  );
2357
2962
  }
2963
+ /**
2964
+ * BULLMQ-1 — spawn a per-pool `BullMQJobWorker`. Requires the Drizzle
2965
+ * client (the worker drives `job_run` as the source of truth) AND the
2966
+ * resolved BullMQ connection (bound by `JobsDomainModule` when
2967
+ * `backend: 'bullmq'`). The queue name is derived identically to the
2968
+ * orchestrator's `dispatch` via `resolvePoolQueueName(pool, …)` so producer
2969
+ * and consumer agree.
2970
+ */
2971
+ spawnBullMQWorker(pool, _queueAlias, concurrency, poolConfig) {
2972
+ if (!this.db) {
2973
+ throw new Error(
2974
+ `JobWorkerModule: BullMQ worker spawning requires the Drizzle client (no DRIZZLE provider available) \u2014 job_run remains the source of truth.`
2975
+ );
2976
+ }
2977
+ if (!this.bullConnection) {
2978
+ throw new Error(
2979
+ `JobWorkerModule: BullMQ worker spawning requires a resolved BULLMQ_CONNECTION. Ensure JobsDomainModule was booted with backend: 'bullmq'.`
2980
+ );
2981
+ }
2982
+ if (!this.moduleRef) {
2983
+ throw new Error(
2984
+ `JobWorkerModule: ModuleRef not available \u2014 cannot construct BullMQJobWorker with handler DI support.`
2985
+ );
2986
+ }
2987
+ const queueName = resolvePoolQueueName(pool, this.bullConfig, poolConfig);
2988
+ return new BullMQJobWorker(
2989
+ this.db,
2990
+ this.orchestrator,
2991
+ this.stepService,
2992
+ {
2993
+ pool,
2994
+ queueName,
2995
+ concurrency,
2996
+ connection: this.bullConnection
2997
+ },
2998
+ this.moduleRef
2999
+ );
3000
+ }
2358
3001
  };
2359
3002
  JobWorkerOrchestrator = __decorateClass([
2360
- Injectable8(),
2361
- __decorateParam(0, Inject7(JOB_ORCHESTRATOR)),
2362
- __decorateParam(1, Inject7(JOB_RUN_SERVICE)),
2363
- __decorateParam(2, Inject7(JOB_STEP_SERVICE)),
2364
- __decorateParam(3, Inject7(JOB_WORKER_MODULE_OPTIONS)),
2365
- __decorateParam(4, Optional2()),
2366
- __decorateParam(4, Inject7(DRIZZLE))
3003
+ Injectable9(),
3004
+ __decorateParam(0, Inject8(JOB_ORCHESTRATOR)),
3005
+ __decorateParam(1, Inject8(JOB_RUN_SERVICE)),
3006
+ __decorateParam(2, Inject8(JOB_STEP_SERVICE)),
3007
+ __decorateParam(3, Inject8(JOB_WORKER_MODULE_OPTIONS)),
3008
+ __decorateParam(4, Optional3()),
3009
+ __decorateParam(4, Inject8(DRIZZLE)),
3010
+ __decorateParam(6, Optional3()),
3011
+ __decorateParam(6, Inject8(BULLMQ_CONNECTION)),
3012
+ __decorateParam(7, Optional3()),
3013
+ __decorateParam(7, Inject8(BULLMQ_RESOLVED_CONFIG))
2367
3014
  ], JobWorkerOrchestrator);
2368
3015
  var JobWorkerModule = class {
2369
3016
  static forRoot(opts) {
@@ -2380,7 +3027,14 @@ var JobWorkerModule = class {
2380
3027
  { provide: JOB_WORKER_MODULE_OPTIONS, useValue: opts },
2381
3028
  JobWorkerOrchestrator
2382
3029
  ],
2383
- exports: []
3030
+ // BULLMQ-1 Phase 1 — export the options token so `BridgeModule`'s
3031
+ // reserved-pool guard (`onModuleInit`) can actually inject it.
3032
+ // Previously `exports: []` left the `@Optional()` inject resolving to
3033
+ // `undefined` and the guard silently no-opped (a dead check). With the
3034
+ // token exported the guard fires for real; consumers that omit the
3035
+ // reserved pools (and don't set `allPools`) now fail fast with
3036
+ // `BridgeReservedPoolsNotPolledError` — which is correct.
3037
+ exports: [JOB_WORKER_MODULE_OPTIONS]
2384
3038
  };
2385
3039
  }
2386
3040
  };