@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
@@ -0,0 +1,98 @@
1
+ import { ConnectionOptions } from 'bullmq';
2
+ import { PoolConfig } from './pool-config.loader.js';
3
+
4
+ /**
5
+ * BullMQ backend configuration surface (BULLMQ-1, ADR-022 extension slot).
6
+ *
7
+ * The core `IJobOrchestrator` contract is backend-agnostic; everything in
8
+ * this file is BullMQ-specific and lives behind the
9
+ * `jobs.extensions.bullmq.*` config namespace (CLAUDE.md core/extension
10
+ * protocol). The Drizzle backend never reads any of it.
11
+ */
12
+
13
+ /**
14
+ * Typed shape of `codegen.config.yaml: jobs.extensions.bullmq`. Snake_case
15
+ * because it mirrors the YAML the consumer authors.
16
+ *
17
+ * ```yaml
18
+ * jobs:
19
+ * backend: bullmq
20
+ * extensions:
21
+ * bullmq:
22
+ * redis_url: redis://localhost:6379 # or env REDIS_URL
23
+ * queue_prefix: myapp # optional namespace (ADR-022 OQ)
24
+ * bull_board:
25
+ * enabled: true
26
+ * mount_path: /api/admin/queues
27
+ * ```
28
+ */
29
+ interface BullMqExtensionsConfig {
30
+ /**
31
+ * Redis/Valkey connection URL. When omitted, the runtime resolves
32
+ * `process.env.REDIS_URL`, then falls back to `redis://localhost:6379`.
33
+ */
34
+ redis_url?: string;
35
+ /**
36
+ * Optional queue-name prefix to avoid collisions when several codegen apps
37
+ * share one Redis (ADR-022 §"BullMQ queue naming collisions"). Applied to
38
+ * every pool queue alias.
39
+ */
40
+ queue_prefix?: string;
41
+ /**
42
+ * Bull Board dashboard — opt-in extension (not core). Mounting is the
43
+ * consumer's responsibility (it needs the consumer's Express/Nest adapter +
44
+ * admin auth); we only carry the config. See README + spec §Extensions.
45
+ */
46
+ bull_board?: {
47
+ enabled: boolean;
48
+ mount_path?: string;
49
+ };
50
+ }
51
+ /**
52
+ * The runtime form after `redis_url`/env resolution. This is what the
53
+ * orchestrator + worker actually consume.
54
+ */
55
+ interface BullMqResolvedConfig {
56
+ connection: ConnectionOptions;
57
+ queuePrefix?: string;
58
+ bullBoard?: {
59
+ enabled: boolean;
60
+ mountPath: string;
61
+ };
62
+ }
63
+ /** DI token for the resolved BullMQ `ConnectionOptions` (ioredis-compatible). */
64
+ declare const BULLMQ_CONNECTION: unique symbol;
65
+ /** DI token for the full resolved BullMQ config (prefix + bull board). */
66
+ declare const BULLMQ_RESOLVED_CONFIG: unique symbol;
67
+ /**
68
+ * Resolve the BullMQ runtime config from the extension block.
69
+ *
70
+ * Precedence for the connection URL:
71
+ * 1. explicit `extensions.bullmq.redis_url`
72
+ * 2. `process.env.REDIS_URL`
73
+ * 3. `redis://localhost:6379`
74
+ *
75
+ * Returns a `{ url }` connection shape — BullMQ/ioredis accept a URL string
76
+ * via the `{ url }` ConnectionOptions form.
77
+ */
78
+ declare function resolveBullMqConfig(ext: BullMqExtensionsConfig | undefined): BullMqResolvedConfig;
79
+ /**
80
+ * Resolve the BullMQ queue name for a *logical pool name*. The orchestrator
81
+ * and worker MUST agree on this mapping or jobs are enqueued onto a queue
82
+ * nobody consumes. Both derive it identically:
83
+ *
84
+ * 1. Look up the pool's `queue` alias (e.g. `jobs-batch`) in the resolved
85
+ * pool config — the same alias `JobWorkerModule.onModuleInit` logs and
86
+ * that the BullMQ `Worker` binds to.
87
+ * 2. Fall back to the logical pool name when the pool is unknown (defensive;
88
+ * still a stable, colon-free identifier).
89
+ * 3. Apply the optional `queue_prefix` namespace for multi-app Redis
90
+ * sharing — `:` is fine in the *queue name* (it is only forbidden in the
91
+ * `jobId`, hence the sha1 there).
92
+ *
93
+ * `poolConfig` defaults to the cached `loadPoolConfig()` so callers that only
94
+ * hold the logical pool name (the orchestrator) don't need to thread the map.
95
+ */
96
+ declare function resolvePoolQueueName(pool: string, config: BullMqResolvedConfig | null | undefined, poolConfig?: PoolConfig): string;
97
+
98
+ export { BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG, type BullMqExtensionsConfig, type BullMqResolvedConfig, resolveBullMqConfig, resolvePoolQueueName };
@@ -0,0 +1,143 @@
1
+ // runtime/subsystems/jobs/pool-config.loader.ts
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { parse as parseYaml } from "yaml";
5
+ var FRAMEWORK_POOLS = Object.freeze({
6
+ events_inbound: Object.freeze({
7
+ queue: "jobs-events-inbound",
8
+ concurrency: 20,
9
+ reserved: true,
10
+ description: "Inbound events drain (events subsystem outbox)."
11
+ }),
12
+ events_change: Object.freeze({
13
+ queue: "jobs-events-change",
14
+ concurrency: 30,
15
+ reserved: true,
16
+ description: "Change events drain (events subsystem outbox)."
17
+ }),
18
+ events_outbound: Object.freeze({
19
+ queue: "jobs-events-outbound",
20
+ concurrency: 10,
21
+ reserved: true,
22
+ description: "Outbound events drain (events subsystem outbox)."
23
+ }),
24
+ interactive: Object.freeze({
25
+ queue: "jobs-interactive",
26
+ concurrency: 20,
27
+ reserved: false,
28
+ description: "User-facing latency-sensitive jobs."
29
+ }),
30
+ batch: Object.freeze({
31
+ queue: "jobs-batch",
32
+ concurrency: 5,
33
+ reserved: false,
34
+ description: "Default pool for background jobs."
35
+ })
36
+ });
37
+ var RESERVED_POOL_NAMES = new Set(
38
+ Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
39
+ );
40
+ var cache = /* @__PURE__ */ new Map();
41
+ function loadPoolConfig(configPath) {
42
+ const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
43
+ const cached = cache.get(resolved);
44
+ if (cached) return cached;
45
+ const merged = /* @__PURE__ */ new Map();
46
+ for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
47
+ merged.set(name, { ...def });
48
+ }
49
+ if (!existsSync(resolved)) {
50
+ cache.set(resolved, merged);
51
+ return merged;
52
+ }
53
+ let raw;
54
+ try {
55
+ raw = parseYaml(readFileSync(resolved, "utf8"));
56
+ } catch (err) {
57
+ throw new Error(
58
+ `pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
59
+ );
60
+ }
61
+ const userPools = extractUserPools(raw);
62
+ for (const [name, userDef] of Object.entries(userPools)) {
63
+ const existing = merged.get(name);
64
+ if (existing) {
65
+ const next = {
66
+ queue: existing.queue,
67
+ concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
68
+ reserved: existing.reserved,
69
+ description: userDef.description ?? existing.description
70
+ };
71
+ merged.set(name, next);
72
+ continue;
73
+ }
74
+ if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
75
+ throw new Error(
76
+ `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
77
+ );
78
+ }
79
+ if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
80
+ throw new Error(
81
+ `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
82
+ );
83
+ }
84
+ if (userDef.reserved === true) {
85
+ throw new Error(
86
+ `pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
87
+ );
88
+ }
89
+ merged.set(name, {
90
+ queue: userDef.queue,
91
+ concurrency: userDef.concurrency,
92
+ reserved: false,
93
+ description: userDef.description
94
+ });
95
+ }
96
+ cache.set(resolved, merged);
97
+ return merged;
98
+ }
99
+ function extractUserPools(raw) {
100
+ if (!raw || typeof raw !== "object") return {};
101
+ const jobs = raw.jobs;
102
+ if (!jobs || typeof jobs !== "object") return {};
103
+ const pools = jobs.pools;
104
+ if (!pools || typeof pools !== "object") return {};
105
+ const out = {};
106
+ for (const [name, def] of Object.entries(pools)) {
107
+ if (!def || typeof def !== "object") continue;
108
+ out[name] = def;
109
+ }
110
+ return out;
111
+ }
112
+
113
+ // runtime/subsystems/jobs/bullmq.config.ts
114
+ var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
115
+ var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
116
+ var DEFAULT_REDIS_URL = "redis://localhost:6379";
117
+ var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
118
+ function resolveBullMqConfig(ext) {
119
+ const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
120
+ const resolved = {
121
+ connection: { url },
122
+ queuePrefix: ext?.queue_prefix
123
+ };
124
+ if (ext?.bull_board?.enabled) {
125
+ resolved.bullBoard = {
126
+ enabled: true,
127
+ mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
128
+ };
129
+ }
130
+ return resolved;
131
+ }
132
+ function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
133
+ const alias = poolConfig.get(pool)?.queue ?? pool;
134
+ const prefix = config?.queuePrefix;
135
+ return prefix ? `${prefix}:${alias}` : alias;
136
+ }
137
+ export {
138
+ BULLMQ_CONNECTION,
139
+ BULLMQ_RESOLVED_CONFIG,
140
+ resolveBullMqConfig,
141
+ resolvePoolQueueName
142
+ };
143
+ //# sourceMappingURL=bullmq.config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../runtime/subsystems/jobs/pool-config.loader.ts","../../../../runtime/subsystems/jobs/bullmq.config.ts"],"sourcesContent":["/**\n * Pool config loader for the job orchestration domain (ADR-022, JOB-5).\n *\n * Reads `codegen.config.yaml: jobs.pools` from `process.cwd()` (or an\n * explicit `configPath` for tests), merges user-defined pools onto the five\n * framework defaults, and returns the resolved `Map<string, PoolDefinition>`\n * consumed by `JobWorkerModule.onModuleInit` and `JobsDomainModule`'s\n * config-validator surface.\n *\n * Invariants:\n * - User cannot flip `reserved: true` on a framework pool — silently\n * preserved. The three `events_*` pools are reserved infrastructure\n * for the events outbox drain.\n * - User-defined pools cannot set `reserved: true` — `reserved` is\n * framework-only metadata.\n * - Missing `codegen.config.yaml` is not an error; loader returns the\n * framework defaults verbatim.\n *\n * Result is cached at module scope after first call so repeated reads (e.g.\n * a worker module + a one-off scaffold validator in the same process) hit\n * the same parse. Tests that pass `configPath` skip the cache and isolate.\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\n\nexport interface PoolDefinition {\n /** Routing identifier — reused as the per-pool worker queue name. */\n queue: string;\n /** Max parallel in-flight `processRun` calls for this pool's worker. */\n concurrency: number;\n /** `true` ⇒ user `@JobHandler` may not target it. Framework-only. */\n reserved: boolean;\n /** Free-text annotation surfaced in admin UIs / logs. */\n description?: string;\n}\n\nexport type PoolConfig = Map<string, PoolDefinition>;\n\n/**\n * Five framework defaults. Three reserved `events_*` pools drain the\n * `IEventBus` outbox (one per `DomainEvent.direction`); `interactive` and\n * `batch` are user-default pools (`batch` is the `@JobHandler` default\n * when no `pool` is specified).\n */\nexport const FRAMEWORK_POOLS: Readonly<Record<string, PoolDefinition>> = Object.freeze({\n events_inbound: Object.freeze({\n queue: 'jobs-events-inbound',\n concurrency: 20,\n reserved: true,\n description: 'Inbound events drain (events subsystem outbox).',\n }),\n events_change: Object.freeze({\n queue: 'jobs-events-change',\n concurrency: 30,\n reserved: true,\n description: 'Change events drain (events subsystem outbox).',\n }),\n events_outbound: Object.freeze({\n queue: 'jobs-events-outbound',\n concurrency: 10,\n reserved: true,\n description: 'Outbound events drain (events subsystem outbox).',\n }),\n interactive: Object.freeze({\n queue: 'jobs-interactive',\n concurrency: 20,\n reserved: false,\n description: 'User-facing latency-sensitive jobs.',\n }),\n batch: Object.freeze({\n queue: 'jobs-batch',\n concurrency: 5,\n reserved: false,\n description: 'Default pool for background jobs.',\n }),\n});\n\n/** Names of the framework reserved pools. Cheap inline lookup for the worker. */\nexport const RESERVED_POOL_NAMES: ReadonlySet<string> = new Set(\n Object.entries(FRAMEWORK_POOLS)\n .filter(([, def]) => def.reserved)\n .map(([name]) => name),\n);\n\n/**\n * Cache by absolute config path. The `cwd` default is normalised before\n * lookup so two callers passing the same path share the cache; explicit\n * test-only paths cache separately.\n */\nconst cache = new Map<string, PoolConfig>();\n\n/**\n * Reset the loader cache. Test-only — not exported from the package\n * `index.ts`. Useful for tests that mutate `process.cwd()` between cases.\n */\nexport function _resetPoolConfigCacheForTests(): void {\n cache.clear();\n}\n\n/**\n * Resolve the merged pool config.\n *\n * @param configPath optional absolute or cwd-relative path; defaults to\n * `${process.cwd()}/codegen.config.yaml`.\n */\nexport function loadPoolConfig(configPath?: string): PoolConfig {\n const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);\n const cached = cache.get(resolved);\n if (cached) return cached;\n\n const merged = new Map<string, PoolDefinition>();\n // Seed with framework defaults first — they always take precedence on\n // `reserved` and provide defaults for `queue` / `concurrency` if user\n // overrides only some fields.\n for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {\n merged.set(name, { ...def });\n }\n\n if (!existsSync(resolved)) {\n cache.set(resolved, merged);\n return merged;\n }\n\n let raw: unknown;\n try {\n raw = parseYaml(readFileSync(resolved, 'utf8'));\n } catch (err) {\n throw new Error(\n `pool-config.loader: failed to parse YAML at ${resolved}: ${(err as Error).message}`,\n );\n }\n\n const userPools = extractUserPools(raw);\n for (const [name, userDef] of Object.entries(userPools)) {\n const existing = merged.get(name);\n if (existing) {\n // Framework pool — user may tweak concurrency + description but\n // cannot flip `reserved`. `queue` is frozen too (reserved framework\n // pools' queue identifiers are part of the cross-subsystem contract\n // with the events outbox drain).\n const next: PoolDefinition = {\n queue: existing.queue,\n concurrency:\n typeof userDef.concurrency === 'number'\n ? userDef.concurrency\n : existing.concurrency,\n reserved: existing.reserved,\n description: userDef.description ?? existing.description,\n };\n merged.set(name, next);\n continue;\n }\n // User-defined pool. Validate required fields; reject reserved.\n if (typeof userDef.queue !== 'string' || userDef.queue.length === 0) {\n throw new Error(\n `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`,\n );\n }\n if (typeof userDef.concurrency !== 'number' || userDef.concurrency <= 0) {\n throw new Error(\n `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`,\n );\n }\n if (userDef.reserved === true) {\n throw new Error(\n `pool-config.loader: user-defined pool '${name}' cannot set ` +\n `'reserved: true' — reserved is framework-only.`,\n );\n }\n merged.set(name, {\n queue: userDef.queue,\n concurrency: userDef.concurrency,\n reserved: false,\n description: userDef.description,\n });\n }\n\n cache.set(resolved, merged);\n return merged;\n}\n\n/**\n * Names of every non-reserved pool in the resolved config. The default\n * worker activation set when `JobWorkerModuleOptions.pools` is omitted —\n * the worker process never claims the reserved `events_*` pools by\n * default; those are bound by the events subsystem's outbox bridge.\n */\nexport function allNonReservedPoolNames(config: PoolConfig): string[] {\n const out: string[] = [];\n for (const [name, def] of config) {\n if (!def.reserved) out.push(name);\n }\n return out;\n}\n\n/**\n * Names of **every** pool in the resolved config, reserved `events_*` lanes\n * included. The activation set for a standalone worker booted with\n * `JobWorkerModule.forRoot({ allPools: true })` (BULLMQ-1 Phase 1) — the\n * single worker process drains both user pools and the bridge's reserved\n * pools so wrapper `job_run` rows are never stranded.\n */\nexport function allPoolNames(config: PoolConfig): string[] {\n return [...config.keys()];\n}\n\n// ─── internals ──────────────────────────────────────────────────────────────\n\ninterface UserPoolShape {\n queue?: string;\n concurrency?: number;\n reserved?: boolean;\n description?: string;\n}\n\nfunction extractUserPools(raw: unknown): Record<string, UserPoolShape> {\n if (!raw || typeof raw !== 'object') return {};\n const jobs = (raw as { jobs?: unknown }).jobs;\n if (!jobs || typeof jobs !== 'object') return {};\n const pools = (jobs as { pools?: unknown }).pools;\n if (!pools || typeof pools !== 'object') return {};\n const out: Record<string, UserPoolShape> = {};\n for (const [name, def] of Object.entries(pools as Record<string, unknown>)) {\n if (!def || typeof def !== 'object') continue;\n out[name] = def as UserPoolShape;\n }\n return out;\n}\n","/**\n * BullMQ backend configuration surface (BULLMQ-1, ADR-022 extension slot).\n *\n * The core `IJobOrchestrator` contract is backend-agnostic; everything in\n * this file is BullMQ-specific and lives behind the\n * `jobs.extensions.bullmq.*` config namespace (CLAUDE.md core/extension\n * protocol). The Drizzle backend never reads any of it.\n */\nimport type { ConnectionOptions } from 'bullmq';\nimport { loadPoolConfig, type PoolConfig } from './pool-config.loader';\n\n/**\n * Typed shape of `codegen.config.yaml: jobs.extensions.bullmq`. Snake_case\n * because it mirrors the YAML the consumer authors.\n *\n * ```yaml\n * jobs:\n * backend: bullmq\n * extensions:\n * bullmq:\n * redis_url: redis://localhost:6379 # or env REDIS_URL\n * queue_prefix: myapp # optional namespace (ADR-022 OQ)\n * bull_board:\n * enabled: true\n * mount_path: /api/admin/queues\n * ```\n */\nexport interface BullMqExtensionsConfig {\n /**\n * Redis/Valkey connection URL. When omitted, the runtime resolves\n * `process.env.REDIS_URL`, then falls back to `redis://localhost:6379`.\n */\n redis_url?: string;\n /**\n * Optional queue-name prefix to avoid collisions when several codegen apps\n * share one Redis (ADR-022 §\"BullMQ queue naming collisions\"). Applied to\n * every pool queue alias.\n */\n queue_prefix?: string;\n /**\n * Bull Board dashboard — opt-in extension (not core). Mounting is the\n * consumer's responsibility (it needs the consumer's Express/Nest adapter +\n * admin auth); we only carry the config. See README + spec §Extensions.\n */\n bull_board?: {\n enabled: boolean;\n mount_path?: string;\n };\n}\n\n/**\n * The runtime form after `redis_url`/env resolution. This is what the\n * orchestrator + worker actually consume.\n */\nexport interface BullMqResolvedConfig {\n connection: ConnectionOptions;\n queuePrefix?: string;\n bullBoard?: { enabled: boolean; mountPath: string };\n}\n\n/** DI token for the resolved BullMQ `ConnectionOptions` (ioredis-compatible). */\nexport const BULLMQ_CONNECTION = Symbol('BULLMQ_CONNECTION');\n\n/** DI token for the full resolved BullMQ config (prefix + bull board). */\nexport const BULLMQ_RESOLVED_CONFIG = Symbol('BULLMQ_RESOLVED_CONFIG');\n\nconst DEFAULT_REDIS_URL = 'redis://localhost:6379';\nconst DEFAULT_BULL_BOARD_MOUNT = '/admin/queues';\n\n/**\n * Resolve the BullMQ runtime config from the extension block.\n *\n * Precedence for the connection URL:\n * 1. explicit `extensions.bullmq.redis_url`\n * 2. `process.env.REDIS_URL`\n * 3. `redis://localhost:6379`\n *\n * Returns a `{ url }` connection shape — BullMQ/ioredis accept a URL string\n * via the `{ url }` ConnectionOptions form.\n */\nexport function resolveBullMqConfig(\n ext: BullMqExtensionsConfig | undefined,\n): BullMqResolvedConfig {\n const url =\n ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;\n\n const resolved: BullMqResolvedConfig = {\n connection: { url } as ConnectionOptions,\n queuePrefix: ext?.queue_prefix,\n };\n if (ext?.bull_board?.enabled) {\n resolved.bullBoard = {\n enabled: true,\n mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT,\n };\n }\n return resolved;\n}\n\n/**\n * Resolve the BullMQ queue name for a *logical pool name*. The orchestrator\n * and worker MUST agree on this mapping or jobs are enqueued onto a queue\n * nobody consumes. Both derive it identically:\n *\n * 1. Look up the pool's `queue` alias (e.g. `jobs-batch`) in the resolved\n * pool config — the same alias `JobWorkerModule.onModuleInit` logs and\n * that the BullMQ `Worker` binds to.\n * 2. Fall back to the logical pool name when the pool is unknown (defensive;\n * still a stable, colon-free identifier).\n * 3. Apply the optional `queue_prefix` namespace for multi-app Redis\n * sharing — `:` is fine in the *queue name* (it is only forbidden in the\n * `jobId`, hence the sha1 there).\n *\n * `poolConfig` defaults to the cached `loadPoolConfig()` so callers that only\n * hold the logical pool name (the orchestrator) don't need to thread the map.\n */\nexport function resolvePoolQueueName(\n pool: string,\n config: BullMqResolvedConfig | null | undefined,\n poolConfig: PoolConfig = loadPoolConfig(),\n): string {\n const alias = poolConfig.get(pool)?.queue ?? pool;\n const prefix = config?.queuePrefix;\n return prefix ? `${prefix}:${alias}` : alias;\n}\n"],"mappings":";AAsBA,SAAS,YAAY,oBAAoB;AACzC,SAAS,eAAe;AACxB,SAAS,SAAS,iBAAiB;AAqB5B,IAAM,kBAA4D,OAAO,OAAO;AAAA,EACrF,gBAAgB,OAAO,OAAO;AAAA,IAC5B,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,EACf,CAAC;AAAA,EACD,eAAe,OAAO,OAAO;AAAA,IAC3B,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,EACf,CAAC;AAAA,EACD,iBAAiB,OAAO,OAAO;AAAA,IAC7B,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,EACf,CAAC;AAAA,EACD,aAAa,OAAO,OAAO;AAAA,IACzB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,EACf,CAAC;AAAA,EACD,OAAO,OAAO,OAAO;AAAA,IACnB,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,EACf,CAAC;AACH,CAAC;AAGM,IAAM,sBAA2C,IAAI;AAAA,EAC1D,OAAO,QAAQ,eAAe,EAC3B,OAAO,CAAC,CAAC,EAAE,GAAG,MAAM,IAAI,QAAQ,EAChC,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI;AACzB;AAOA,IAAM,QAAQ,oBAAI,IAAwB;AAgBnC,SAAS,eAAe,YAAiC;AAC9D,QAAM,WAAW,QAAQ,cAAc,GAAG,QAAQ,IAAI,CAAC,sBAAsB;AAC7E,QAAM,SAAS,MAAM,IAAI,QAAQ;AACjC,MAAI,OAAQ,QAAO;AAEnB,QAAM,SAAS,oBAAI,IAA4B;AAI/C,aAAW,CAAC,MAAM,GAAG,KAAK,OAAO,QAAQ,eAAe,GAAG;AACzD,WAAO,IAAI,MAAM,EAAE,GAAG,IAAI,CAAC;AAAA,EAC7B;AAEA,MAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,UAAM,IAAI,UAAU,MAAM;AAC1B,WAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,aAAa,UAAU,MAAM,CAAC;AAAA,EAChD,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,+CAA+C,QAAQ,KAAM,IAAc,OAAO;AAAA,IACpF;AAAA,EACF;AAEA,QAAM,YAAY,iBAAiB,GAAG;AACtC,aAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,SAAS,GAAG;AACvD,UAAM,WAAW,OAAO,IAAI,IAAI;AAChC,QAAI,UAAU;AAKZ,YAAM,OAAuB;AAAA,QAC3B,OAAO,SAAS;AAAA,QAChB,aACE,OAAO,QAAQ,gBAAgB,WAC3B,QAAQ,cACR,SAAS;AAAA,QACf,UAAU,SAAS;AAAA,QACnB,aAAa,QAAQ,eAAe,SAAS;AAAA,MAC/C;AACA,aAAO,IAAI,MAAM,IAAI;AACrB;AAAA,IACF;AAEA,QAAI,OAAO,QAAQ,UAAU,YAAY,QAAQ,MAAM,WAAW,GAAG;AACnE,YAAM,IAAI;AAAA,QACR,6BAA6B,IAAI;AAAA,MACnC;AAAA,IACF;AACA,QAAI,OAAO,QAAQ,gBAAgB,YAAY,QAAQ,eAAe,GAAG;AACvE,YAAM,IAAI;AAAA,QACR,6BAA6B,IAAI;AAAA,MACnC;AAAA,IACF;AACA,QAAI,QAAQ,aAAa,MAAM;AAC7B,YAAM,IAAI;AAAA,QACR,0CAA0C,IAAI;AAAA,MAEhD;AAAA,IACF;AACA,WAAO,IAAI,MAAM;AAAA,MACf,OAAO,QAAQ;AAAA,MACf,aAAa,QAAQ;AAAA,MACrB,UAAU;AAAA,MACV,aAAa,QAAQ;AAAA,IACvB,CAAC;AAAA,EACH;AAEA,QAAM,IAAI,UAAU,MAAM;AAC1B,SAAO;AACT;AAoCA,SAAS,iBAAiB,KAA6C;AACrE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAC7C,QAAM,OAAQ,IAA2B;AACzC,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO,CAAC;AAC/C,QAAM,QAAS,KAA6B;AAC5C,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO,CAAC;AACjD,QAAM,MAAqC,CAAC;AAC5C,aAAW,CAAC,MAAM,GAAG,KAAK,OAAO,QAAQ,KAAgC,GAAG;AAC1E,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAU;AACrC,QAAI,IAAI,IAAI;AAAA,EACd;AACA,SAAO;AACT;;;ACvKO,IAAM,oBAAoB,uBAAO,mBAAmB;AAGpD,IAAM,yBAAyB,uBAAO,wBAAwB;AAErE,IAAM,oBAAoB;AAC1B,IAAM,2BAA2B;AAa1B,SAAS,oBACd,KACsB;AACtB,QAAM,MACJ,KAAK,aAAa,QAAQ,IAAI,aAAa;AAE7C,QAAM,WAAiC;AAAA,IACrC,YAAY,EAAE,IAAI;AAAA,IAClB,aAAa,KAAK;AAAA,EACpB;AACA,MAAI,KAAK,YAAY,SAAS;AAC5B,aAAS,YAAY;AAAA,MACnB,SAAS;AAAA,MACT,WAAW,IAAI,WAAW,cAAc;AAAA,IAC1C;AAAA,EACF;AACA,SAAO;AACT;AAmBO,SAAS,qBACd,MACA,QACA,aAAyB,eAAe,GAChC;AACR,QAAM,QAAQ,WAAW,IAAI,IAAI,GAAG,SAAS;AAC7C,QAAM,SAAS,QAAQ;AACvB,SAAO,SAAS,GAAG,MAAM,IAAI,KAAK,KAAK;AACzC;","names":[]}
@@ -1,11 +1,14 @@
1
1
  export { JobDefinitionRow, JobRunRow, JobStepRow, collisionModeEnum, jobRunStatusEnum, jobRuns, jobStepKindEnum, jobStepStatusEnum, jobSteps, jobs, parentClosePolicyEnum, replayFromEnum, triggerSourceEnum, waitKindEnum } from './job-orchestration.schema.js';
2
2
  export { JOBS_MULTI_TENANT, JOB_ORCHESTRATOR, JOB_RUN_SERVICE, JOB_STEP_SERVICE } from './jobs-domain.tokens.js';
3
3
  export { C as CancelOptions, a as ConcurrencyPolicy, D as DedupePolicy, H as HandlerRegistry, b as HandlerRegistryEntry, I as IJobOrchestrator, J as JOB_HANDLER_METADATA_KEY, c as JOB_HANDLER_REGISTRY, d as JobContext, e as JobHandler, f as JobHandlerBase, g as JobHandlerMeta, h as JobPoolDef, i as JobRun, j as JobUpsertEntry, P as ParentClosePolicy, R as RetryPolicy, S as ScopeRef, k as SpawnChildOptions, l as StartOptions, m as StepOptions } from '../../../job-orchestrator.protocol-BwsBd37o.js';
4
- export { CancelForScopeOptions, IJobRunService, JobRunFailure, ListForScopeOptions, PoolStatusCount, RescheduleForScopeOptions } from './job-run-service.protocol.js';
4
+ export { CancelForScopeOptions, IJobRunService, JobRunFailure, JobRunPage, JobRunSummary, ListForScopeOptions, ListJobRunsQuery, PoolStatusCount, RescheduleForScopeOptions } from './job-run-service.protocol.js';
5
5
  export { IJobStepService, JobStep, RecordStepInput } from './job-step-service.protocol.js';
6
6
  export { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend.js';
7
7
  export { DrizzleJobRunService } from './job-run-service.drizzle-backend.js';
8
8
  export { DrizzleJobStepService } from './job-step-service.drizzle-backend.js';
9
+ export { BullMQJobOrchestrator, sha1JobId } from './job-orchestrator.bullmq-backend.js';
10
+ export { BullMQJobWorker, BullMQJobWorkerOptions } from './job-worker.bullmq-backend.js';
11
+ export { BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG, BullMqExtensionsConfig, BullMqResolvedConfig, resolveBullMqConfig, resolvePoolQueueName } from './bullmq.config.js';
9
12
  export { JOB_WORKER_OPTIONS, JobWorker, JobWorkerOptions, buildClaimQuery, buildStaleSweepQuery, classifyError, computeBackoff } from './job-worker.js';
10
13
  export { BootValidationError, JobCollisionError, JobNotReplayableError, JobTemplateFieldMissingError, JobTypeNotFoundError, MissingTenantIdError, ReservedPoolViolationError } from './jobs-errors.js';
11
14
  export { MemoryJobStore } from './memory-job-store.js';
@@ -14,11 +17,12 @@ export { MemoryJobRunService } from './job-run-service.memory-backend.js';
14
17
  export { MemoryJobStepService } from './job-step-service.memory-backend.js';
15
18
  export { DrizzleBackendExtensions, JobsDomainModule, JobsDomainModuleOptions } from './jobs-domain.module.js';
16
19
  export { JobWorkerModule, JobWorkerModuleOptions, JobWorkerOrchestrator } from './job-worker.module.js';
17
- export { FRAMEWORK_POOLS, PoolConfig, PoolDefinition, RESERVED_POOL_NAMES, allNonReservedPoolNames, loadPoolConfig } from './pool-config.loader.js';
20
+ export { FRAMEWORK_POOLS, PoolConfig, PoolDefinition, RESERVED_POOL_NAMES, allNonReservedPoolNames, allPoolNames, loadPoolConfig } from './pool-config.loader.js';
18
21
  import 'drizzle-orm/pg-core';
19
22
  import 'drizzle-orm';
20
23
  import '../events/event-bus.protocol.js';
21
24
  import '../../types/drizzle.js';
22
25
  import 'drizzle-orm/node-postgres';
23
26
  import '@nestjs/common';
27
+ import 'bullmq';
24
28
  import '@nestjs/core';