@pattern-stack/codegen 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/consumer-skills/events/typed-bus-and-outbox.md +1 -1
- package/consumer-skills/subsystems/SKILL.md +56 -0
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +0 -1
- package/dist/runtime/subsystems/bridge/bridge.module.js +294 -710
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +0 -1
- package/dist/runtime/subsystems/bridge/index.js +248 -664
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +18 -10
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/events.module.js +43 -244
- package/dist/runtime/subsystems/events/events.module.js.map +1 -1
- package/dist/runtime/subsystems/events/index.d.ts +0 -1
- package/dist/runtime/subsystems/events/index.js +39 -241
- package/dist/runtime/subsystems/events/index.js.map +1 -1
- package/dist/runtime/subsystems/index.js +174 -791
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +22 -3
- package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -1
- package/dist/runtime/subsystems/jobs/index.d.ts +1 -4
- package/dist/runtime/subsystems/jobs/index.js +87 -506
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +11 -4
- package/dist/runtime/subsystems/jobs/job-worker.module.js +248 -664
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +0 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +89 -391
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/src/cli/index.js +152 -35
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +32 -10
- package/runtime/subsystems/events/events.module.ts +38 -6
- package/runtime/subsystems/events/index.ts +7 -1
- package/runtime/subsystems/jobs/bullmq.config.ts +23 -3
- package/runtime/subsystems/jobs/index.ts +13 -8
- package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +5 -2
- package/runtime/subsystems/jobs/job-worker.module.ts +27 -7
- package/runtime/subsystems/jobs/jobs-domain.module.ts +27 -2
- package/templates/subsystem/events/domain-events.schema.ejs.t +43 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts","../../../../runtime/constants/tokens.ts","../../../../runtime/subsystems/jobs/job-orchestration.schema.ts","../../../../runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts","../../../../runtime/subsystems/jobs/jobs-errors.ts","../../../../runtime/subsystems/jobs/jobs-domain.tokens.ts","../../../../runtime/subsystems/jobs/pool-config.loader.ts","../../../../runtime/subsystems/jobs/bullmq.config.ts"],"sourcesContent":["/**\n * BullMQJobOrchestrator — BullMQ-backed implementation of `IJobOrchestrator`\n * (BULLMQ-1, ADR-022 §58 — the reserved \"Phase 6+\" backend, now built).\n *\n * Split-of-responsibility (spec §\"Postgres + BullMQ coordination\"):\n * - Postgres `job_run` stays the **domain source of truth** — scoping,\n * hierarchy (`parent_run_id`/`root_run_id`), dedupe/concurrency state,\n * `listForScope`. All of that is the Drizzle backend's job and is reused\n * verbatim by extending `DrizzleJobOrchestrator`.\n * - BullMQ owns the **claim/dispatch** half. `start` adds a job to the\n * pool's queue (or to a FlowProducer flow when parented); the BullMQ\n * `Worker` (see `job-worker.bullmq-backend.ts`) consumes it and runs the\n * handler through the existing `JobHandlerBase` path. `cancel` removes\n * the queued job; `replay` re-adds it after the shared DB reset.\n *\n * This is **additive**: the Drizzle backend, the core protocol, and app code\n * are untouched. Consumers flip `jobs.backend: bullmq` with no code change —\n * the same `IJobOrchestrator` surface is satisfied.\n *\n * `jobId` (spec §Gotcha 1): BullMQ treats `:` as a Redis key separator and\n * consumers use `vendor:externalId`-shaped idempotency keys, so we derive the\n * `jobId` as `sha1(idempotencyKey)` — colon-safe and stable (same logical key\n * → same id → BullMQ-native dedup). When no dedupe key is configured we fall\n * back to the `job_run.id` (a fresh UUID), which is already colon-safe.\n */\nimport { createHash } from 'node:crypto';\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport { eq } from 'drizzle-orm';\n// `bullmq` is an OPTIONAL peer dependency. Only TYPE imports here — types are\n// erased at compile time and never resolve `'bullmq'` at runtime, so a\n// `drizzle`-only consumer who didn't install bullmq can still load this file\n// (it is statically imported by `jobs-domain.module.ts`). The VALUE\n// constructors (`Queue`, `FlowProducer`) are loaded lazily via `await\n// import('bullmq')` in `loadBullMq()` — mirrors\n// `event-bus.redis-backend.ts:createRedisClient`. See BULLMQ-1 §Lazy import.\nimport type { ConnectionOptions, FlowProducer, Queue } from 'bullmq';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, jobs, type JobDefinitionRow } from './job-orchestration.schema';\nimport { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend';\nimport type {\n CancelOptions,\n JobRun,\n StartOptions,\n} from './job-orchestrator.protocol';\nimport { JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport {\n BULLMQ_CONNECTION,\n resolvePoolQueueName,\n type BullMqResolvedConfig,\n BULLMQ_RESOLVED_CONFIG,\n} from './bullmq.config';\n\n/**\n * Derive a colon-safe, stable BullMQ `jobId` from a logical idempotency key.\n *\n * SHA-1 over the raw key. Collision analysis (spec §Gotcha 1, resolved during\n * implementation): SHA-1's 160-bit space makes an accidental collision between\n * two *distinct* logical keys astronomically unlikely at any realistic job\n * volume (the birthday bound is ~2^80 keys before a 50% collision chance —\n * orders of magnitude beyond any job throughput). SHA-1's cryptographic\n * weakness is irrelevant here: there is no adversary forging idempotency keys,\n * and even a forged collision only deduplicates two jobs that the caller chose\n * to key identically. We therefore accept SHA-1 with no mitigation. The *same*\n * logical key intentionally maps to the *same* jobId — that is the dedup\n * mechanism, not a collision.\n */\nexport function sha1JobId(idempotencyKey: string): string {\n return createHash('sha1').update(idempotencyKey).digest('hex');\n}\n\n// Constructor types for the lazily-loaded `bullmq` value exports. Typed via\n// `typeof` the type-only imports so the cached ctors stay strongly typed\n// without a runtime `import`.\ntype QueueCtor = typeof import('bullmq').Queue;\ntype FlowProducerCtor = typeof import('bullmq').FlowProducer;\n\n@Injectable()\nexport class BullMQJobOrchestrator extends DrizzleJobOrchestrator {\n // TODO(logging-subsystem): swap to ILogger once ADR-028 lands\n private readonly bullLogger = new Logger(BullMQJobOrchestrator.name);\n\n /** Lazily-opened `Queue` handles, one per pool. */\n private readonly queues = new Map<string, Queue>();\n /** Single FlowProducer for parent/child hierarchies. Lazily opened. */\n private _flow: FlowProducer | null = null;\n\n /**\n * Cached `bullmq` value constructors, populated by `loadBullMq()` on first\n * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching\n * a queue). Kept off the import graph so a `drizzle`-only consumer never\n * resolves the optional `'bullmq'` package.\n */\n private QueueCtor: QueueCtor | null = null;\n private FlowProducerCtor: FlowProducerCtor | null = null;\n private bullMqLoad: Promise<void> | null = null;\n\n /**\n * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is\n * `private` (can't be redeclared even privately in a subclass), and the\n * spec forbids touching that file — so the subclass keeps its own handle\n * under a distinct name (same instance, passed through to `super`) for the\n * cancel-cascade snapshot + definition/run loads below.\n */\n private readonly bullDb: DrizzleClient;\n\n constructor(\n @Inject(DRIZZLE) db: DrizzleClient,\n @Inject(JOBS_MULTI_TENANT) multiTenant: boolean,\n @Inject(BULLMQ_CONNECTION) private readonly connection: ConnectionOptions,\n @Optional()\n @Inject(BULLMQ_RESOLVED_CONFIG)\n private readonly bullConfig: BullMqResolvedConfig | null = null,\n ) {\n super(db, multiTenant);\n this.bullDb = db;\n }\n\n /**\n * Lazily load the optional `bullmq` package and cache its value\n * constructors. Idempotent (single in-flight promise). Throws a friendly,\n * actionable error when the consumer selected `backend: 'bullmq'` but did\n * not install the package — mirrors `createRedisClient` in the redis event\n * backend. Must be `await`ed before any `queueFor`/`flow` access.\n */\n private async loadBullMq(): Promise<void> {\n if (this.QueueCtor && this.FlowProducerCtor) return;\n if (!this.bullMqLoad) {\n this.bullMqLoad = (async () => {\n try {\n const mod = await import('bullmq');\n this.QueueCtor = mod.Queue;\n this.FlowProducerCtor = mod.FlowProducer;\n } catch {\n throw new Error(\n 'BullMQ backend requires the \"bullmq\" package. Install it with: npm install bullmq',\n );\n }\n })();\n }\n await this.bullMqLoad;\n }\n\n /**\n * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await\n * loadBullMq()` first so `QueueCtor` is populated.\n */\n private queueFor(pool: string): Queue {\n if (!this.QueueCtor) {\n throw new Error('BullMQJobOrchestrator: queueFor called before loadBullMq()');\n }\n const name = resolvePoolQueueName(pool, this.bullConfig);\n let q = this.queues.get(name);\n if (!q) {\n q = new this.QueueCtor(name, { connection: this.connection });\n this.queues.set(name, q);\n }\n return q;\n }\n\n private flow(): FlowProducer {\n if (!this.FlowProducerCtor) {\n throw new Error('BullMQJobOrchestrator: flow called before loadBullMq()');\n }\n if (!this._flow) {\n this._flow = new this.FlowProducerCtor({ connection: this.connection });\n }\n return this._flow;\n }\n\n // ==========================================================================\n // start — Postgres insert (super) + BullMQ dispatch\n // ==========================================================================\n\n override async start(\n type: string,\n input: unknown,\n opts: StartOptions = {},\n tx?: DrizzleTransaction,\n ): Promise<JobRun> {\n // (1) Postgres remains source of truth — the Drizzle backend handles the\n // job-definition lookup, dedupe short-circuit, concurrency collision,\n // parent/root resolution, and the `job_run` INSERT. If dedupe\n // short-circuited it returns the incumbent row whose dispatch already\n // happened on the original start; we must not enqueue again.\n const run = await super.start(type, input, opts, tx);\n\n // Dedupe returned an existing run (its createdAt predates this call) —\n // BullMQ-native dedup already covered the dispatch. Skip re-enqueue.\n // We detect this by checking the run was freshly created in THIS call:\n // a brand-new run has status 'pending' and zero attempts AND its id is\n // not yet known to BullMQ. The cheapest reliable signal is the dedupe\n // path's contract: super.start returns the incumbent unchanged. Since we\n // cannot distinguish purely from the row, we rely on `jobId` idempotency\n // — re-adding with the same jobId is a no-op in BullMQ, so the enqueue is\n // safe to attempt unconditionally.\n\n await this.dispatch(run, type);\n return run;\n }\n\n /**\n * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a\n * `parentRunId` we attach it to the parent's existing BullMQ job through the\n * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in\n * its own graph. (The FlowProducer is reserved for whole-tree atomic\n * submits, exposed as an opt-in extension via `flowProducer()`; runtime\n * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the\n * correct primitive here.)\n *\n * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is\n * present (so the same logical key dedups), else the `job_run.id` UUID\n * (already colon-free).\n *\n * The domain `parentClosePolicy` cascade is still enforced in Postgres by\n * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,\n * not the authority.\n */\n private async dispatch(run: JobRun, type: string): Promise<void> {\n await this.loadBullMq();\n const def = await this.loadDefinition(type);\n const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;\n\n const jobOpts: Record<string, unknown> = {\n jobId,\n ...this.retryOpts(def),\n ...this.dedupeOpts(run, def),\n };\n\n if (run.parentRunId) {\n const parentRow = await this.loadRun(run.parentRunId);\n if (parentRow) {\n const parentJobId = parentRow.dedupeKey\n ? sha1JobId(parentRow.dedupeKey)\n : parentRow.id;\n jobOpts.parent = {\n id: parentJobId,\n queue: resolvePoolQueueName(parentRow.pool, this.bullConfig),\n };\n }\n }\n\n // The processor reads the authoritative input from `job_run`; the payload\n // carries the runId so it can load the row, plus type/input for logging.\n const payload = { runId: run.id, type, input: run.input };\n await this.queueFor(run.pool).add(type, payload, jobOpts);\n }\n\n /**\n * Opt-in extension (spec §Extensions): expose the FlowProducer for\n * consumers that want to submit a whole parent/child DAG atomically up\n * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —\n * code using it is not portable to the Drizzle backend. Async because it\n * lazily loads the optional `bullmq` package on first use.\n */\n async flowProducer(): Promise<FlowProducer> {\n await this.loadBullMq();\n return this.flow();\n }\n\n private retryOpts(def: JobDefinitionRow): {\n attempts?: number;\n backoff?: { type: 'fixed' | 'exponential'; delay: number };\n } {\n const policy = def.retryPolicy;\n if (!policy) return {};\n return {\n attempts: policy.attempts,\n backoff: {\n type: policy.backoff === 'exponential' ? 'exponential' : 'fixed',\n delay: policy.baseMs,\n },\n };\n }\n\n private dedupeOpts(\n run: JobRun,\n def: JobDefinitionRow,\n ): { deduplication?: { id: string; ttl?: number } } {\n if (!run.dedupeKey || !def.dedupeWindowMs) return {};\n return {\n deduplication: {\n id: sha1JobId(run.dedupeKey),\n ttl: def.dedupeWindowMs,\n },\n };\n }\n\n // ==========================================================================\n // cancel — Postgres cascade (super) + remove from queue\n // ==========================================================================\n\n override async cancel(runId: string, opts: CancelOptions = {}): Promise<void> {\n // Snapshot the subtree BEFORE the DB cascade flips rows to canceled, so we\n // can remove every affected BullMQ job. We read the target's rootRunId and\n // the non-terminal descendants the same way the Drizzle cascade does.\n const target = await this.loadRun(runId);\n\n await super.cancel(runId, opts);\n\n if (!target) return;\n await this.loadBullMq();\n // Remove the target's own queued job.\n await this.removeFromQueue(target);\n\n if (opts.cascade === false) return;\n\n // Remove descendants' queued jobs (the DB rows were just canceled by\n // super.cancel; we mirror that into BullMQ so workers don't pick them up).\n const descendants = await this.bullDb\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.rootRunId, target.rootRunId));\n for (const child of descendants) {\n if (child.id === runId) continue;\n await this.removeFromQueue(child as JobRun);\n }\n }\n\n private async removeFromQueue(run: JobRun): Promise<void> {\n const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;\n try {\n const job = await this.queueFor(run.pool).getJob(jobId);\n if (job) await job.remove();\n } catch (err) {\n // A job already moved to active/completed cannot always be removed;\n // the Postgres cancel is authoritative either way.\n this.bullLogger.warn(\n `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${(err as Error).message}`,\n );\n }\n }\n\n // ==========================================================================\n // replay — Postgres reset (super) + re-enqueue\n // ==========================================================================\n\n override async replay(runId: string): Promise<JobRun> {\n const run = await super.replay(runId);\n await this.dispatch(run, run.jobType);\n return run;\n }\n\n // ==========================================================================\n // Internals\n // ==========================================================================\n\n private async loadDefinition(type: string): Promise<JobDefinitionRow> {\n const [def] = await this.bullDb\n .select()\n .from(jobs)\n .where(eq(jobs.type, type))\n .limit(1);\n if (!def) {\n throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);\n }\n return def as JobDefinitionRow;\n }\n\n private async loadRun(id: string): Promise<JobRun | null> {\n const [row] = await this.bullDb\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, id))\n .limit(1);\n return (row as JobRun) ?? null;\n }\n\n /** Close all open queue + flow connections. Called on module destroy. */\n async closeConnections(): Promise<void> {\n for (const q of this.queues.values()) {\n await q.close().catch(() => undefined);\n }\n this.queues.clear();\n if (this._flow) {\n await this._flow.close().catch(() => undefined);\n this._flow = null;\n }\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n","/**\n * Drizzle schema for the job orchestration domain (ADR-022).\n *\n * Three tables model the lifecycle of a durable job:\n * - `job` — definitions keyed by handler type (e.g. 'onboarding').\n * - `job_run` — one row per attempt to execute a job; worker claims\n * rows directly via SELECT ... FOR UPDATE SKIP LOCKED.\n * - `job_step` — individual steps within a run; memoises output for replay.\n *\n * Phase 1 ships only this layer. There is no `job_queue` table, no executor\n * port — see ADR-022 and `.claude/skills/jobs/SKILL.md` for the rationale.\n */\nimport {\n pgEnum,\n pgTable,\n uuid,\n text,\n jsonb,\n integer,\n timestamp,\n index,\n uniqueIndex,\n} from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport type { InferSelectModel } from 'drizzle-orm';\n\n// ─── Internal $type<> helpers ───────────────────────────────────────────────\n// Annotation types for jsonb columns only. JOB-2 defines the public protocol\n// types; these remain private to this file.\n\ntype RetryPolicy = {\n attempts: number;\n backoff: 'fixed' | 'exponential';\n baseMs: number;\n nonRetryableErrors?: string[];\n};\n\ntype JobRunError = {\n message: string;\n stack?: string;\n retryable: boolean;\n attempt: number;\n};\n\n// ─── Enums ──────────────────────────────────────────────────────────────────\n\nexport const jobRunStatusEnum = pgEnum('job_run_status', [\n 'pending',\n 'running',\n 'waiting',\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n]);\n\n// extended in ADR-027: tool_call | llm_call | wait | checkpoint | message\nexport const jobStepKindEnum = pgEnum('job_step_kind', ['task']);\n\nexport const jobStepStatusEnum = pgEnum('job_step_status', [\n 'pending',\n 'running',\n 'completed',\n 'failed',\n 'skipped',\n]);\n\nexport const collisionModeEnum = pgEnum('job_collision_mode', [\n 'queue',\n 'reject',\n 'replace',\n]);\n\nexport const replayFromEnum = pgEnum('job_replay_from', [\n 'scratch',\n 'last_step',\n 'last_checkpoint',\n]);\n\nexport const parentClosePolicyEnum = pgEnum('job_parent_close_policy', [\n 'terminate',\n 'cancel',\n 'abandon',\n]);\n\n// Phase 3 placeholder — see ADR-025\nexport const waitKindEnum = pgEnum('job_wait_kind', ['signal']);\n\n// Phase 2 may add more sources; requires Atlas migration\nexport const triggerSourceEnum = pgEnum('job_trigger_source', [\n 'manual',\n 'schedule',\n 'event',\n 'parent',\n]);\n\n// ─── job ────────────────────────────────────────────────────────────────────\n\nexport const jobs = pgTable('job', {\n type: text('type').primaryKey(),\n version: integer('version').notNull().default(1),\n pool: text('pool').notNull(),\n scopeEntityType: text('scope_entity_type'),\n retryPolicy: jsonb('retry_policy').notNull().$type<RetryPolicy>(),\n timeoutMs: integer('timeout_ms'),\n concurrencyKeyTemplate: text('concurrency_key_template'),\n collisionMode: collisionModeEnum('collision_mode').notNull().default('queue'),\n dedupeKeyTemplate: text('dedupe_key_template'),\n dedupeWindowMs: integer('dedupe_window_ms'),\n priorityDefault: integer('priority_default').notNull().default(0),\n replayFrom: replayFromEnum('replay_from').notNull().default('last_checkpoint'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n});\n\nexport type JobDefinitionRow = InferSelectModel<typeof jobs>;\n\n// ─── job_run ────────────────────────────────────────────────────────────────\n\nexport const jobRuns = pgTable(\n 'job_run',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobType: text('job_type').notNull().references(() => jobs.type),\n jobVersion: integer('job_version').notNull(),\n parentRunId: uuid('parent_run_id').references((): any => jobRuns.id),\n /**\n * Service generates `id` client-side via randomUUID() and sets\n * root_run_id = id for root runs (single INSERT, no self-FK race).\n */\n rootRunId: uuid('root_run_id').notNull(),\n parentClosePolicy: parentClosePolicyEnum('parent_close_policy')\n .notNull()\n .default('terminate'),\n scopeEntityType: text('scope_entity_type'),\n scopeEntityId: text('scope_entity_id'),\n tenantId: text('tenant_id'),\n tags: jsonb('tags').notNull().default({}).$type<Record<string, string>>(),\n pool: text('pool').notNull(),\n priority: integer('priority').notNull().default(0),\n concurrencyKey: text('concurrency_key'),\n dedupeKey: text('dedupe_key'),\n status: jobRunStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').notNull().$type<Record<string, unknown>>(),\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n triggerSource: triggerSourceEnum('trigger_source').notNull(),\n triggerRef: text('trigger_ref'),\n runAt: timestamp('run_at', { withTimezone: true }).notNull().defaultNow(),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n claimedAt: timestamp('claimed_at', { withTimezone: true }),\n attempts: integer('attempts').notNull().default(0),\n // Phase 3 placeholder — see ADR-025\n waitKind: waitKindEnum('wait_kind'),\n // Phase 3 placeholder — see ADR-025\n resumeToken: text('resume_token'),\n // Phase 3 placeholder — see ADR-025\n waitDeadline: timestamp('wait_deadline', { withTimezone: true }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n /** Claim query: ORDER BY priority DESC, run_at ASC. */\n idxJobRunClaim: index('idx_job_run_claim').on(t.status, t.pool, t.runAt),\n /** Tree traversal / cascade cancel. */\n idxJobRunRoot: index('idx_job_run_root').on(t.rootRunId),\n /** listForScope query. */\n idxJobRunScope: index('idx_job_run_scope').on(t.scopeEntityType, t.scopeEntityId),\n /** Idempotency collapse — partial index. */\n idxJobRunDedupe: index('idx_job_run_dedupe')\n .on(t.jobType, t.dedupeKey)\n .where(sql`${t.dedupeKey} IS NOT NULL`),\n /** Collision check — partial index. */\n idxJobRunConcurrency: index('idx_job_run_concurrency')\n .on(t.concurrencyKey)\n .where(\n sql`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`,\n ),\n }),\n);\n\nexport type JobRunRow = InferSelectModel<typeof jobRuns>;\n\n// ─── job_step ───────────────────────────────────────────────────────────────\n\nexport const jobSteps = pgTable(\n 'job_step',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobRunId: uuid('job_run_id').notNull().references(() => jobRuns.id),\n stepId: text('step_id').notNull(),\n kind: jobStepKindEnum('kind').notNull().default('task'),\n /**\n * Monotonic within run. integer (max ~2B per run) is sufficient —\n * downgraded from ADR-022's bigint; revisit only if a single run\n * ever exceeds 2 billion steps.\n */\n seq: integer('seq').notNull(),\n status: jobStepStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').$type<Record<string, unknown>>(),\n /** Memoised on success for replay. */\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n attempts: integer('attempts').notNull().default(0),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n },\n (t) => ({\n /** No duplicate step IDs per run. */\n idxJobStepRunStep: uniqueIndex('idx_job_step_run_step').on(t.jobRunId, t.stepId),\n /** Ordered timeline reads. */\n idxJobStepTimeline: index('idx_job_step_timeline').on(t.jobRunId, t.seq),\n }),\n);\n\nexport type JobStepRow = InferSelectModel<typeof jobSteps>;\n","/**\n * DrizzleJobOrchestrator — Postgres-backed implementation of\n * `IJobOrchestrator` (ADR-022, JOB-3).\n *\n * Single-layer architecture: `start` writes a single `job_run` row; the\n * `JobWorker` polling loop claims it directly via `FOR UPDATE SKIP LOCKED`.\n * No `job_queue` table, no executor port. See `docs/specs/JOB-3.md`.\n */\nimport { randomUUID } from 'node:crypto';\nimport { Inject, Injectable, Logger } from '@nestjs/common';\nimport { and, desc, eq, gt, inArray, isNotNull, ne, notInArray, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\nimport { DRIZZLE } from '../../constants/tokens';\nimport {\n jobRuns,\n jobs,\n type JobDefinitionRow,\n type JobRunRow,\n} from './job-orchestration.schema';\nimport type {\n CancelOptions,\n IJobOrchestrator,\n JobPoolDef,\n JobRun,\n JobUpsertEntry,\n StartOptions,\n} from './job-orchestrator.protocol';\nimport {\n JobCollisionError,\n JobNotReplayableError,\n JobTemplateFieldMissingError,\n JobTypeNotFoundError,\n MissingTenantIdError,\n} from './jobs-errors';\nimport { jobSteps } from './job-orchestration.schema';\nimport { JOBS_MULTI_TENANT } from './jobs-domain.tokens';\n\n/**\n * Terminal statuses — transitions into these are final. Used by `cancel`\n * (to short-circuit idempotently) and by `replay` (as the guard gate).\n */\nexport const TERMINAL_STATUSES = [\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n] as const;\ntype TerminalStatus = (typeof TERMINAL_STATUSES)[number];\ntype JobRunStatus = JobRunRow['status'];\n\n/** Statuses excluded from dedupe window matches per ADR-022. */\nconst DEDUPE_EXCLUDED_STATUSES: JobRunStatus[] = ['canceled', 'failed'];\n/** Statuses that count as in-flight for concurrency collision checks. */\nconst IN_FLIGHT_STATUSES: JobRunStatus[] = ['pending', 'running'];\n\n/**\n * Substitute `{{field}}` placeholders against the input payload.\n *\n * Implementation decision (JOB-3, 2026-04-19): simple `{{field}}` single-key\n * substitution, no dotted paths, no Mustache/Handlebars dependency. A missing\n * field throws `JobTemplateFieldMissingError` synchronously — cheaper than\n * discovering the misconfiguration at claim time.\n */\nexport function evaluateKeyTemplate(\n template: string,\n input: Record<string, unknown>,\n): string {\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_match, field: string) => {\n const value = input[field];\n if (value === undefined || value === null) {\n throw new JobTemplateFieldMissingError(template, field);\n }\n return String(value);\n });\n}\n\n@Injectable()\nexport class DrizzleJobOrchestrator implements IJobOrchestrator {\n // TODO(logging-subsystem): swap to ILogger once ADR-028 lands\n private readonly logger = new Logger(DrizzleJobOrchestrator.name);\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n ) {}\n\n /**\n * JOB-8 — resolve `tenantId` for a mutating / targeted-read call.\n * Returns the tenant value that should be written to the row (or compared\n * against in a WHERE clause). When `multiTenant` is off, the column is\n * forced to `null` regardless of what callers pass. When on, `undefined`\n * throws; `null` and strings pass through untouched.\n */\n private resolveTenantId(\n method: string,\n tenantId: string | null | undefined,\n ): string | null {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return tenantId;\n }\n\n // ==========================================================================\n // start\n // ==========================================================================\n\n async start(\n type: string,\n input: unknown,\n opts: StartOptions = {},\n tx?: DrizzleTransaction,\n ): Promise<JobRun> {\n const payload = (input ?? {}) as Record<string, unknown>;\n\n // JOB-8 — resolve tenant gate up front so `multi_tenant=true` +\n // undefined surfaces before any row is touched.\n const tenantId = this.resolveTenantId('start', opts.tenantId);\n\n // BRIDGE-7: thread the optional caller tx through every read/write\n // in this method so EventFlowService.publishAndStart can bundle the\n // outbox insert, the eager job_run insert, and (for Case B) the\n // bridge_delivery pre-write into a single transaction.\n const client = (tx ?? this.db) as DrizzleClient;\n\n // 1a. Load job definition.\n const [def] = await client\n .select()\n .from(jobs)\n .where(eq(jobs.type, type))\n .limit(1);\n if (!def) throw new JobTypeNotFoundError(type);\n const definition = def as JobDefinitionRow;\n\n // 1b. Dedupe check.\n if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {\n const dedupeKey = evaluateKeyTemplate(definition.dedupeKeyTemplate, payload);\n const windowStart = new Date(Date.now() - definition.dedupeWindowMs);\n const existing = await client\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.jobType, type),\n eq(jobRuns.dedupeKey, dedupeKey),\n gt(jobRuns.createdAt, windowStart),\n // status NOT IN ('canceled', 'failed')\n notInStatus(DEDUPE_EXCLUDED_STATUSES),\n ),\n )\n .orderBy(desc(jobRuns.createdAt))\n .limit(1);\n if (existing.length > 0) {\n return existing[0] as JobRun;\n }\n }\n\n // 1c. Concurrency collision check.\n let concurrencyKey: string | null = null;\n if (definition.concurrencyKeyTemplate) {\n concurrencyKey = evaluateKeyTemplate(\n definition.concurrencyKeyTemplate,\n payload,\n );\n const inFlight = await client\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.concurrencyKey, concurrencyKey),\n inArray(jobRuns.status, IN_FLIGHT_STATUSES),\n ),\n )\n .limit(1);\n if (inFlight.length > 0) {\n const incumbent = inFlight[0] as JobRun;\n switch (definition.collisionMode) {\n case 'reject':\n throw new JobCollisionError(type, concurrencyKey, incumbent);\n case 'replace':\n // JOB-8 — thread the incumbent's own tenantId through the\n // internal cascade. Without this, every `replace`-collision\n // start() under multiTenant=true throws MissingTenantIdError\n // from the inner cancel() call instead of cancelling the\n // incumbent. Mirrors the memory backend's `cancelLocked(\n // incumbent.id, ..., incumbent.tenantId)` pattern.\n await this.cancel(incumbent.id, {\n cascade: true,\n reason: 'replaced',\n tenantId: incumbent.tenantId,\n });\n break;\n case 'queue':\n // Fall through — row is inserted; claim query gates it until\n // the incumbent transitions (see JobWorker.processRun queue gate).\n break;\n }\n }\n }\n\n // 1d. Resolve id + rootRunId, INSERT.\n const newId = randomUUID();\n let rootRunId: string = newId;\n if (opts.parentRunId) {\n const [parent] = await client\n .select({ rootRunId: jobRuns.rootRunId })\n .from(jobRuns)\n .where(eq(jobRuns.id, opts.parentRunId))\n .limit(1);\n if (!parent) {\n throw new Error(\n `parentRunId ${opts.parentRunId} does not reference an existing job_run`,\n );\n }\n rootRunId = parent.rootRunId;\n }\n\n const dedupeKey =\n definition.dedupeKeyTemplate\n ? evaluateKeyTemplate(definition.dedupeKeyTemplate, payload)\n : null;\n\n const [inserted] = await client\n .insert(jobRuns)\n .values({\n id: newId,\n jobType: type,\n jobVersion: definition.version,\n parentRunId: opts.parentRunId ?? null,\n rootRunId,\n parentClosePolicy: opts.parentClosePolicy ?? 'terminate',\n scopeEntityType: opts.scope?.entityType ?? null,\n scopeEntityId: opts.scope?.entityId ?? null,\n tenantId,\n tags: opts.tags ?? {},\n pool: opts.pool ?? definition.pool,\n priority: opts.priority ?? definition.priorityDefault,\n concurrencyKey,\n dedupeKey,\n status: 'pending',\n input: payload,\n output: null,\n error: null,\n triggerSource: opts.triggerSource ?? 'manual',\n triggerRef: opts.triggerRef ?? null,\n runAt: opts.runAt ?? new Date(),\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n attempts: 0,\n })\n .returning();\n\n return inserted as JobRun;\n }\n\n // ==========================================================================\n // cancel\n // ==========================================================================\n\n async cancel(runId: string, opts: CancelOptions = {}): Promise<void> {\n // JOB-8 — resolve tenant gate up front (strict undefined-throws).\n const tenantId = this.resolveTenantId('cancel', opts.tenantId);\n\n // Load target.\n const [target] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!target) return;\n // JOB-8 — cross-tenant cancel is a silent no-op (no existence leak).\n if (this.multiTenant && target.tenantId !== tenantId) return;\n if (TERMINAL_STATUSES.includes(target.status as TerminalStatus)) {\n return; // idempotent\n }\n\n // Atomic transition, guarded against concurrent terminal moves.\n const [cancelled] = await this.db\n .update(jobRuns)\n .set({\n status: 'canceled',\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(\n and(eq(jobRuns.id, runId), notInStatus([...TERMINAL_STATUSES])),\n )\n .returning();\n\n if (!cancelled) return; // lost the race; already terminal\n\n if (opts.cascade === false) return;\n\n // Fetch descendants and branch on parent_close_policy.\n const descendants = await this.db\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.rootRunId, target.rootRunId),\n ne(jobRuns.id, runId),\n notInStatus([...TERMINAL_STATUSES]),\n ),\n );\n\n for (const child of descendants) {\n const policy = (child as JobRunRow).parentClosePolicy;\n if (policy === 'abandon') continue;\n // 'terminate' | 'cancel' — both transition the child to canceled.\n await this.db\n .update(jobRuns)\n .set({\n status: 'canceled',\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(\n and(\n eq(jobRuns.id, (child as JobRunRow).id),\n notInStatus([...TERMINAL_STATUSES]),\n ),\n );\n }\n\n void opts.reason; // reserved for future audit logging\n }\n\n // ==========================================================================\n // replay\n // ==========================================================================\n\n async replay(runId: string): Promise<JobRun> {\n // Load target + its job definition (we need replay_from).\n const [target] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!target) {\n throw new Error(`replay: run ${runId} not found`);\n }\n const run = target as JobRunRow;\n if (!TERMINAL_STATUSES.includes(run.status as TerminalStatus)) {\n throw new JobNotReplayableError(runId, run.status);\n }\n\n const [def] = await this.db\n .select()\n .from(jobs)\n .where(eq(jobs.type, run.jobType))\n .limit(1);\n if (!def) throw new JobTypeNotFoundError(run.jobType);\n const mode = (def as JobDefinitionRow).replayFrom;\n\n // Atomic: step reset + run reset must commit together.\n const result = await this.db.transaction(async (tx) => {\n if (mode === 'scratch') {\n await tx.delete(jobSteps).where(eq(jobSteps.jobRunId, runId));\n } else if (mode === 'last_step') {\n // Delete only non-completed step rows — completed steps stay memoised.\n await tx\n .delete(jobSteps)\n .where(\n and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, 'completed')),\n );\n } else {\n // 'last_checkpoint' — Phase 1 has no explicit checkpoint markers, so\n // behaviour collapses to `last_step`. See docs/specs/JOB-3.md\n // \"Implementation Decisions\" — planned divergence in a later phase.\n await tx\n .delete(jobSteps)\n .where(\n and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, 'completed')),\n );\n }\n\n const [updated] = await tx\n .update(jobRuns)\n .set({\n status: 'pending',\n attempts: 0,\n runAt: new Date(),\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n error: null,\n output: null,\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, runId))\n .returning();\n return updated as JobRunRow;\n });\n\n return result as JobRun;\n }\n\n // ==========================================================================\n // upsertJobRows — boot-time materialisation of `job` definitions\n // ==========================================================================\n\n /**\n * Hash-gated `INSERT … ON CONFLICT (type) DO UPDATE … WHERE` per Q3\n * resolution (2026-04-19): the `UPDATE` branch executes only when one\n * of the persisted metadata fields differs from the incoming payload;\n * `version` bumps only on real change; concurrent boots with identical\n * content are idempotent no-ops.\n *\n * Why this shape (not `DO NOTHING`, not advisory locks):\n * - `DO NOTHING` would let an old-version instance leave a stale row\n * that a new-version instance can't overwrite during a rolling deploy.\n * - Advisory locks add latency and leak risk under crashes.\n * - The `WHERE … IS DISTINCT FROM …` clause makes the conditional\n * atomic — no read-modify-write race on `version` between concurrent\n * boots.\n *\n * Orphan detection: a single `SELECT type FROM job WHERE type NOT IN (...)`\n * returns the types present in DB but absent from `entries`. Caller (boot\n * validator) decides whether to throw `BootValidationError`.\n */\n async upsertJobRows(\n entries: JobUpsertEntry[],\n poolConfig: ReadonlyMap<string, JobPoolDef>,\n ): Promise<{ orphaned: string[] }> {\n void poolConfig; // pool validation is the module's responsibility; orchestrator just persists\n\n for (const entry of entries) {\n const meta = entry.meta;\n const pool = meta.pool ?? 'batch';\n const retryPolicy = meta.retry ?? {\n attempts: 1,\n backoff: 'fixed' as const,\n baseMs: 0,\n };\n const concurrencyKeyTemplate =\n (meta.concurrency as { key?: unknown } | undefined)?.key;\n const concurrencyKeyTemplateStr =\n typeof concurrencyKeyTemplate === 'string' ? concurrencyKeyTemplate : null;\n const collisionMode =\n (meta.concurrency?.collisionMode as JobDefinitionRow['collisionMode']) ??\n 'queue';\n const dedupeKeyTemplate =\n (meta.dedupe as { key?: unknown } | undefined)?.key;\n const dedupeKeyTemplateStr =\n typeof dedupeKeyTemplate === 'string' ? dedupeKeyTemplate : null;\n const dedupeWindowMs = meta.dedupe?.windowMs ?? null;\n const timeoutMs = meta.timeoutMs ?? null;\n const replayFrom = meta.replayFrom ?? 'last_checkpoint';\n const scopeEntityType = meta.scope?.entity ?? null;\n // Q3 resolution: priority_default and replay_from are part of the\n // hashed metadata even though they aren't currently set via decorator\n // metadata above (priority_default has no `@JobHandler` field yet).\n // Default to 0 to keep UPDATE branch quiet across deploys.\n const priorityDefault = 0;\n\n // Hash-gated upsert: every metadata column appears in the WHERE clause\n // so the UPDATE branch only fires on a real change. `version` bumps\n // exactly when the WHERE matches.\n await this.db\n .insert(jobs)\n .values({\n type: entry.type,\n version: 1,\n pool,\n scopeEntityType,\n retryPolicy,\n timeoutMs,\n concurrencyKeyTemplate: concurrencyKeyTemplateStr,\n collisionMode,\n dedupeKeyTemplate: dedupeKeyTemplateStr,\n dedupeWindowMs,\n priorityDefault,\n replayFrom,\n })\n .onConflictDoUpdate({\n target: jobs.type,\n set: {\n pool: sql`EXCLUDED.pool`,\n scopeEntityType: sql`EXCLUDED.scope_entity_type`,\n retryPolicy: sql`EXCLUDED.retry_policy`,\n timeoutMs: sql`EXCLUDED.timeout_ms`,\n concurrencyKeyTemplate: sql`EXCLUDED.concurrency_key_template`,\n collisionMode: sql`EXCLUDED.collision_mode`,\n dedupeKeyTemplate: sql`EXCLUDED.dedupe_key_template`,\n dedupeWindowMs: sql`EXCLUDED.dedupe_window_ms`,\n priorityDefault: sql`EXCLUDED.priority_default`,\n replayFrom: sql`EXCLUDED.replay_from`,\n version: sql`${jobs.version} + 1`,\n updatedAt: sql`now()`,\n },\n // The hash gate: every field listed in the Q3 resolution appears\n // here. `IS DISTINCT FROM` is the null-safe inequality operator;\n // jsonb cast to text gives stable comparison without invoking a\n // dedicated hash column (avoids a JOB-1 schema migration).\n setWhere: sql`\n ${jobs.pool} IS DISTINCT FROM EXCLUDED.pool OR\n ${jobs.retryPolicy}::text IS DISTINCT FROM EXCLUDED.retry_policy::text OR\n ${jobs.timeoutMs} IS DISTINCT FROM EXCLUDED.timeout_ms OR\n ${jobs.concurrencyKeyTemplate} IS DISTINCT FROM EXCLUDED.concurrency_key_template OR\n ${jobs.collisionMode} IS DISTINCT FROM EXCLUDED.collision_mode OR\n ${jobs.dedupeKeyTemplate} IS DISTINCT FROM EXCLUDED.dedupe_key_template OR\n ${jobs.dedupeWindowMs} IS DISTINCT FROM EXCLUDED.dedupe_window_ms OR\n ${jobs.priorityDefault} IS DISTINCT FROM EXCLUDED.priority_default OR\n ${jobs.replayFrom} IS DISTINCT FROM EXCLUDED.replay_from OR\n ${jobs.scopeEntityType} IS DISTINCT FROM EXCLUDED.scope_entity_type\n `,\n });\n }\n\n // Orphan detection: any `job` row whose type is not in the registry.\n const types = entries.map((e) => e.type);\n const orphans =\n types.length === 0\n ? await this.db.select({ type: jobs.type }).from(jobs)\n : await this.db\n .select({ type: jobs.type })\n .from(jobs)\n .where(notInArray(jobs.type, types));\n\n return { orphaned: orphans.map((o) => o.type) };\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction notInStatus(statuses: JobRunStatus[]) {\n // Drizzle's inArray composes with `not` via negation helper; use raw sql\n // to stay readable. `inArray` + `.not()` isn't idiomatic in 0.45.\n const negated = statuses.map((s) => ne(jobRuns.status, s));\n return and(...negated);\n}\n\n// `isNotNull` + `gt` imports are retained for potential future use; silence\n// unused-import lint by re-exporting via `void`.\nvoid isNotNull;\n","/**\n * Typed errors for the job orchestration domain (ADR-022, JOB-3).\n *\n * All thrown by the Drizzle orchestrator (and mirrored by the Memory\n * backend in JOB-4). They exist as classes so consumers can `instanceof`\n * them in catch blocks and exception filters can map them to HTTP codes.\n */\nimport type { JobRun } from './job-orchestrator.protocol';\n\n/**\n * `start(type, …)` was called for a job type that has no row in the `job`\n * table. At runtime this usually means the handler was not decorated or the\n * boot validator (JOB-5) has not registered it yet.\n */\nexport class JobTypeNotFoundError extends Error {\n override readonly name = 'JobTypeNotFoundError';\n constructor(public readonly jobType: string) {\n super(`No job definition registered for type '${jobType}'.`);\n }\n}\n\n/**\n * Thrown by `start` when `collision_mode === 'reject'` and a non-terminal\n * run with the same `concurrency_key` already exists. Carries the incumbent\n * so callers can surface its id or subscribe to its completion event.\n */\nexport class JobCollisionError extends Error {\n override readonly name = 'JobCollisionError';\n constructor(\n public readonly jobType: string,\n public readonly concurrencyKey: string,\n public readonly incumbent: JobRun,\n ) {\n super(\n `Job type '${jobType}' has an in-flight run with concurrency_key ` +\n `'${concurrencyKey}' (incumbent ${incumbent.id}); collision_mode=reject.`,\n );\n }\n}\n\n/**\n * `replay` was called on a run that is not in a replayable terminal state\n * (i.e. still `pending` / `running` / `waiting`). Replay always spawns\n * fresh execution and therefore requires the source run to be settled.\n */\nexport class JobNotReplayableError extends Error {\n override readonly name = 'JobNotReplayableError';\n constructor(\n public readonly runId: string,\n public readonly currentStatus: string,\n ) {\n super(\n `Run ${runId} is not replayable from status '${currentStatus}'. ` +\n `Only 'completed', 'failed', 'timed_out', and 'canceled' are eligible.`,\n );\n }\n}\n\n/**\n * A `concurrency_key_template` or `dedupe_key_template` referenced a field\n * that is not present on the input payload. Caught at `start` time so the\n * caller sees the misconfiguration synchronously rather than at claim time.\n */\nexport class JobTemplateFieldMissingError extends Error {\n override readonly name = 'JobTemplateFieldMissingError';\n constructor(\n public readonly template: string,\n public readonly field: string,\n ) {\n super(\n `Template '${template}' references input field '${field}' which is ` +\n `missing or undefined on the payload.`,\n );\n }\n}\n\n/**\n * Thrown by the four multi-tenant-aware service-layer backends (JOB-8)\n * when `JobsDomainModule` was configured with `multiTenant: true` but the\n * caller did not pass a `tenantId` in the relevant options object.\n *\n * **Strict enforcement rationale (resolved 2026-04-18).** Cross-tenant data\n * leakage is the worst class of bug a multi-tenant system can ship; surfacing\n * the misuse loudly at the call site (rather than silently defaulting to\n * `null` or to the \"last tenant seen\") prevents both accidental global\n * writes and sneaky reads that return a union of tenants.\n *\n * - `undefined` `tenantId` → throw this error.\n * - Explicit `null` `tenantId` → passes; opts the call into cross-tenant\n * background work (e.g. a nightly housekeeping job that must scan all\n * tenants). The row is persisted with `tenant_id = NULL`.\n */\nexport class MissingTenantIdError extends Error {\n override readonly name = 'MissingTenantIdError';\n constructor(public readonly method: string) {\n super(\n `MissingTenantIdError: JobsDomainModule was configured with ` +\n `multiTenant=true but ${method} was called without tenantId ` +\n `(undefined). Pass an explicit tenantId, or pass null for ` +\n `cross-tenant work.`,\n );\n }\n}\n\n/**\n * Thrown by `JobWorkerModule.onModuleInit` (Drizzle backend only) when the\n * `job` table contains type rows for which no `@JobHandler` is registered\n * in the running process. Surfaces every orphaned type at once so a single\n * boot tells the operator everything to clean up.\n *\n * Skipped entirely in memory mode (Q4 resolution 2026-04-19) — the memory\n * backend has no DB rows to validate; `MemoryJobOrchestrator.start()`\n * throws `JobTypeNotFoundError` synchronously for unknown types instead.\n */\nexport class BootValidationError extends Error {\n override readonly name = 'BootValidationError';\n constructor(public readonly missingHandlers: string[]) {\n super(\n `BootValidationError: ${missingHandlers.length} orphaned job type(s) ` +\n `in 'job' table with no matching @JobHandler in the running process: ` +\n `[${missingHandlers.join(', ')}]. Either register the handler(s) or ` +\n `remove the rows.`,\n );\n }\n}\n\n/**\n * Thrown by `JobWorkerModule.onModuleInit` when one or more `@JobHandler`\n * classes target a `reserved: true` pool from the resolved pool config\n * (the three `events_*` pools are reserved for the events subsystem\n * outbox drain). Listing every offender on a single boot avoids the\n * fix-one-restart-fix-next loop.\n */\nexport class ReservedPoolViolationError extends Error {\n override readonly name = 'ReservedPoolViolationError';\n constructor(\n public readonly offenders: ReadonlyArray<{\n handlerClass: string;\n pool: string;\n }>,\n ) {\n super(\n `ReservedPoolViolationError: ${offenders.length} @JobHandler(s) target ` +\n `reserved pools — reserved pools are framework-only:\\n` +\n offenders\n .map((o) => ` - ${o.handlerClass} → pool='${o.pool}'`)\n .join('\\n'),\n );\n }\n}\n","/**\n * Injection tokens for the job orchestration domain layer (ADR-022, JOB-2).\n *\n * Consumer code injects these symbols via `@Inject(JOB_ORCHESTRATOR)` etc.;\n * concrete backends (JOB-3 Drizzle, JOB-4 Memory) provide the implementations\n * through `JobsDomainModule.forRoot({ backend })` in JOB-5.\n *\n * Each token is a unique `Symbol` — guaranteed distinct from every other\n * Symbol at runtime, which is exactly the uniqueness guarantee Nest's DI\n * container relies on for token-based lookup.\n */\nexport const JOB_ORCHESTRATOR = Symbol('JOB_ORCHESTRATOR');\nexport const JOB_RUN_SERVICE = Symbol('JOB_RUN_SERVICE');\nexport const JOB_STEP_SERVICE = Symbol('JOB_STEP_SERVICE');\n\n/**\n * Multi-tenancy opt-in flag (JOB-8). Bound to the boolean passed in via\n * `JobsDomainModule.forRoot({ multiTenant })`, defaulting to `false`.\n *\n * When `true`, the four service-layer backends (Drizzle + Memory orchestrator\n * and run-service) enforce `tenantId` on every mutating / targeted-read call:\n * `start`, `cancel`, `listForScope`, `cancelForScope`, `rescheduleForScope`.\n * Missing (`undefined`) `tenantId` throws `MissingTenantIdError`; explicit\n * `null` opts into cross-tenant background work and passes through.\n *\n * The JobWorker claim loop is **cross-tenant by design** — the worker has no\n * tenant context; `tenantId` is populated at write time and enforced on\n * targeted reads. See docs/specs/JOB-8.md.\n */\nexport const JOBS_MULTI_TENANT = Symbol('JOBS_MULTI_TENANT');\n","/**\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":"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","names":["Inject","Injectable","Logger","eq","sql","dedupeKey","sql","jobs","Logger","eq","Injectable","Inject"]}
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts","../../../../runtime/constants/tokens.ts","../../../../runtime/subsystems/jobs/job-orchestration.schema.ts","../../../../runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts","../../../../runtime/subsystems/jobs/jobs-errors.ts","../../../../runtime/subsystems/jobs/jobs-domain.tokens.ts","../../../../runtime/subsystems/jobs/pool-config.loader.ts","../../../../runtime/subsystems/jobs/bullmq.config.ts"],"sourcesContent":["/**\n * BullMQJobOrchestrator — BullMQ-backed implementation of `IJobOrchestrator`\n * (BULLMQ-1, ADR-022 §58 — the reserved \"Phase 6+\" backend, now built).\n *\n * Split-of-responsibility (spec §\"Postgres + BullMQ coordination\"):\n * - Postgres `job_run` stays the **domain source of truth** — scoping,\n * hierarchy (`parent_run_id`/`root_run_id`), dedupe/concurrency state,\n * `listForScope`. All of that is the Drizzle backend's job and is reused\n * verbatim by extending `DrizzleJobOrchestrator`.\n * - BullMQ owns the **claim/dispatch** half. `start` adds a job to the\n * pool's queue (or to a FlowProducer flow when parented); the BullMQ\n * `Worker` (see `job-worker.bullmq-backend.ts`) consumes it and runs the\n * handler through the existing `JobHandlerBase` path. `cancel` removes\n * the queued job; `replay` re-adds it after the shared DB reset.\n *\n * This is **additive**: the Drizzle backend, the core protocol, and app code\n * are untouched. Consumers flip `jobs.backend: bullmq` with no code change —\n * the same `IJobOrchestrator` surface is satisfied.\n *\n * `jobId` (spec §Gotcha 1): BullMQ treats `:` as a Redis key separator and\n * consumers use `vendor:externalId`-shaped idempotency keys, so we derive the\n * `jobId` as `sha1(idempotencyKey)` — colon-safe and stable (same logical key\n * → same id → BullMQ-native dedup). When no dedupe key is configured we fall\n * back to the `job_run.id` (a fresh UUID), which is already colon-safe.\n */\nimport { createHash } from 'node:crypto';\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport { eq } from 'drizzle-orm';\n// `bullmq` is an OPTIONAL peer dependency. Only TYPE imports here — types are\n// erased at compile time and never resolve `'bullmq'` at runtime, so a\n// `drizzle`-only consumer who didn't install bullmq can still load this file\n// (it is statically imported by `jobs-domain.module.ts`). The VALUE\n// constructors (`Queue`, `FlowProducer`) are loaded lazily via `await\n// import('bullmq')` in `loadBullMq()` — mirrors\n// `event-bus.redis-backend.ts:createRedisClient`. See BULLMQ-1 §Lazy import.\nimport type { ConnectionOptions, FlowProducer, Queue } from 'bullmq';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, jobs, type JobDefinitionRow } from './job-orchestration.schema';\nimport { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend';\nimport type {\n CancelOptions,\n JobRun,\n StartOptions,\n} from './job-orchestrator.protocol';\nimport { JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport {\n BULLMQ_CONNECTION,\n resolvePoolQueueName,\n type BullMqResolvedConfig,\n BULLMQ_RESOLVED_CONFIG,\n} from './bullmq.config';\n\n/**\n * Derive a colon-safe, stable BullMQ `jobId` from a logical idempotency key.\n *\n * SHA-1 over the raw key. Collision analysis (spec §Gotcha 1, resolved during\n * implementation): SHA-1's 160-bit space makes an accidental collision between\n * two *distinct* logical keys astronomically unlikely at any realistic job\n * volume (the birthday bound is ~2^80 keys before a 50% collision chance —\n * orders of magnitude beyond any job throughput). SHA-1's cryptographic\n * weakness is irrelevant here: there is no adversary forging idempotency keys,\n * and even a forged collision only deduplicates two jobs that the caller chose\n * to key identically. We therefore accept SHA-1 with no mitigation. The *same*\n * logical key intentionally maps to the *same* jobId — that is the dedup\n * mechanism, not a collision.\n */\nexport function sha1JobId(idempotencyKey: string): string {\n return createHash('sha1').update(idempotencyKey).digest('hex');\n}\n\n// Constructor types for the lazily-loaded `bullmq` value exports. Typed via\n// `typeof` the type-only imports so the cached ctors stay strongly typed\n// without a runtime `import`.\ntype QueueCtor = typeof import('bullmq').Queue;\ntype FlowProducerCtor = typeof import('bullmq').FlowProducer;\n\n@Injectable()\nexport class BullMQJobOrchestrator extends DrizzleJobOrchestrator {\n // TODO(logging-subsystem): swap to ILogger once ADR-028 lands\n private readonly bullLogger = new Logger(BullMQJobOrchestrator.name);\n\n /** Lazily-opened `Queue` handles, one per pool. */\n private readonly queues = new Map<string, Queue>();\n /** Single FlowProducer for parent/child hierarchies. Lazily opened. */\n private _flow: FlowProducer | null = null;\n\n /**\n * Cached `bullmq` value constructors, populated by `loadBullMq()` on first\n * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching\n * a queue). Kept off the import graph so a `drizzle`-only consumer never\n * resolves the optional `'bullmq'` package.\n */\n private QueueCtor: QueueCtor | null = null;\n private FlowProducerCtor: FlowProducerCtor | null = null;\n private bullMqLoad: Promise<void> | null = null;\n\n /**\n * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is\n * `private` (can't be redeclared even privately in a subclass), and the\n * spec forbids touching that file — so the subclass keeps its own handle\n * under a distinct name (same instance, passed through to `super`) for the\n * cancel-cascade snapshot + definition/run loads below.\n */\n private readonly bullDb: DrizzleClient;\n\n constructor(\n @Inject(DRIZZLE) db: DrizzleClient,\n @Inject(JOBS_MULTI_TENANT) multiTenant: boolean,\n @Inject(BULLMQ_CONNECTION) private readonly connection: ConnectionOptions,\n @Optional()\n @Inject(BULLMQ_RESOLVED_CONFIG)\n private readonly bullConfig: BullMqResolvedConfig | null = null,\n ) {\n super(db, multiTenant);\n this.bullDb = db;\n }\n\n /**\n * Lazily load the optional `bullmq` package and cache its value\n * constructors. Idempotent (single in-flight promise). Throws a friendly,\n * actionable error when the consumer selected `backend: 'bullmq'` but did\n * not install the package — mirrors `createRedisClient` in the redis event\n * backend. Must be `await`ed before any `queueFor`/`flow` access.\n */\n private async loadBullMq(): Promise<void> {\n if (this.QueueCtor && this.FlowProducerCtor) return;\n if (!this.bullMqLoad) {\n this.bullMqLoad = (async () => {\n try {\n const mod = await import('bullmq');\n this.QueueCtor = mod.Queue;\n this.FlowProducerCtor = mod.FlowProducer;\n } catch {\n throw new Error(\n 'BullMQ backend requires the \"bullmq\" package. Install it with: npm install bullmq',\n );\n }\n })();\n }\n await this.bullMqLoad;\n }\n\n /**\n * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await\n * loadBullMq()` first so `QueueCtor` is populated.\n */\n private queueFor(pool: string): Queue {\n if (!this.QueueCtor) {\n throw new Error('BullMQJobOrchestrator: queueFor called before loadBullMq()');\n }\n const name = resolvePoolQueueName(pool, this.bullConfig);\n let q = this.queues.get(name);\n if (!q) {\n q = new this.QueueCtor(name, { connection: this.connection });\n this.queues.set(name, q);\n }\n return q;\n }\n\n private flow(): FlowProducer {\n if (!this.FlowProducerCtor) {\n throw new Error('BullMQJobOrchestrator: flow called before loadBullMq()');\n }\n if (!this._flow) {\n this._flow = new this.FlowProducerCtor({ connection: this.connection });\n }\n return this._flow;\n }\n\n // ==========================================================================\n // start — Postgres insert (super) + BullMQ dispatch\n // ==========================================================================\n\n override async start(\n type: string,\n input: unknown,\n opts: StartOptions = {},\n tx?: DrizzleTransaction,\n ): Promise<JobRun> {\n // (1) Postgres remains source of truth — the Drizzle backend handles the\n // job-definition lookup, dedupe short-circuit, concurrency collision,\n // parent/root resolution, and the `job_run` INSERT. If dedupe\n // short-circuited it returns the incumbent row whose dispatch already\n // happened on the original start; we must not enqueue again.\n const run = await super.start(type, input, opts, tx);\n\n // Dedupe returned an existing run (its createdAt predates this call) —\n // BullMQ-native dedup already covered the dispatch. Skip re-enqueue.\n // We detect this by checking the run was freshly created in THIS call:\n // a brand-new run has status 'pending' and zero attempts AND its id is\n // not yet known to BullMQ. The cheapest reliable signal is the dedupe\n // path's contract: super.start returns the incumbent unchanged. Since we\n // cannot distinguish purely from the row, we rely on `jobId` idempotency\n // — re-adding with the same jobId is a no-op in BullMQ, so the enqueue is\n // safe to attempt unconditionally.\n\n await this.dispatch(run, type);\n return run;\n }\n\n /**\n * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a\n * `parentRunId` we attach it to the parent's existing BullMQ job through the\n * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in\n * its own graph. (The FlowProducer is reserved for whole-tree atomic\n * submits, exposed as an opt-in extension via `flowProducer()`; runtime\n * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the\n * correct primitive here.)\n *\n * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is\n * present (so the same logical key dedups), else the `job_run.id` UUID\n * (already colon-free).\n *\n * The domain `parentClosePolicy` cascade is still enforced in Postgres by\n * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,\n * not the authority.\n */\n private async dispatch(run: JobRun, type: string): Promise<void> {\n await this.loadBullMq();\n const def = await this.loadDefinition(type);\n const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;\n\n const jobOpts: Record<string, unknown> = {\n jobId,\n ...this.retryOpts(def),\n ...this.dedupeOpts(run, def),\n };\n\n if (run.parentRunId) {\n const parentRow = await this.loadRun(run.parentRunId);\n if (parentRow) {\n const parentJobId = parentRow.dedupeKey\n ? sha1JobId(parentRow.dedupeKey)\n : parentRow.id;\n jobOpts.parent = {\n id: parentJobId,\n queue: resolvePoolQueueName(parentRow.pool, this.bullConfig),\n };\n }\n }\n\n // The processor reads the authoritative input from `job_run`; the payload\n // carries the runId so it can load the row, plus type/input for logging.\n const payload = { runId: run.id, type, input: run.input };\n await this.queueFor(run.pool).add(type, payload, jobOpts);\n }\n\n /**\n * Opt-in extension (spec §Extensions): expose the FlowProducer for\n * consumers that want to submit a whole parent/child DAG atomically up\n * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —\n * code using it is not portable to the Drizzle backend. Async because it\n * lazily loads the optional `bullmq` package on first use.\n */\n async flowProducer(): Promise<FlowProducer> {\n await this.loadBullMq();\n return this.flow();\n }\n\n private retryOpts(def: JobDefinitionRow): {\n attempts?: number;\n backoff?: { type: 'fixed' | 'exponential'; delay: number };\n } {\n const policy = def.retryPolicy;\n if (!policy) return {};\n return {\n attempts: policy.attempts,\n backoff: {\n type: policy.backoff === 'exponential' ? 'exponential' : 'fixed',\n delay: policy.baseMs,\n },\n };\n }\n\n private dedupeOpts(\n run: JobRun,\n def: JobDefinitionRow,\n ): { deduplication?: { id: string; ttl?: number } } {\n if (!run.dedupeKey || !def.dedupeWindowMs) return {};\n return {\n deduplication: {\n id: sha1JobId(run.dedupeKey),\n ttl: def.dedupeWindowMs,\n },\n };\n }\n\n // ==========================================================================\n // cancel — Postgres cascade (super) + remove from queue\n // ==========================================================================\n\n override async cancel(runId: string, opts: CancelOptions = {}): Promise<void> {\n // Snapshot the subtree BEFORE the DB cascade flips rows to canceled, so we\n // can remove every affected BullMQ job. We read the target's rootRunId and\n // the non-terminal descendants the same way the Drizzle cascade does.\n const target = await this.loadRun(runId);\n\n await super.cancel(runId, opts);\n\n if (!target) return;\n await this.loadBullMq();\n // Remove the target's own queued job.\n await this.removeFromQueue(target);\n\n if (opts.cascade === false) return;\n\n // Remove descendants' queued jobs (the DB rows were just canceled by\n // super.cancel; we mirror that into BullMQ so workers don't pick them up).\n const descendants = await this.bullDb\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.rootRunId, target.rootRunId));\n for (const child of descendants) {\n if (child.id === runId) continue;\n await this.removeFromQueue(child as JobRun);\n }\n }\n\n private async removeFromQueue(run: JobRun): Promise<void> {\n const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;\n try {\n const job = await this.queueFor(run.pool).getJob(jobId);\n if (job) await job.remove();\n } catch (err) {\n // A job already moved to active/completed cannot always be removed;\n // the Postgres cancel is authoritative either way.\n this.bullLogger.warn(\n `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${(err as Error).message}`,\n );\n }\n }\n\n // ==========================================================================\n // replay — Postgres reset (super) + re-enqueue\n // ==========================================================================\n\n override async replay(runId: string): Promise<JobRun> {\n const run = await super.replay(runId);\n await this.dispatch(run, run.jobType);\n return run;\n }\n\n // ==========================================================================\n // Internals\n // ==========================================================================\n\n private async loadDefinition(type: string): Promise<JobDefinitionRow> {\n const [def] = await this.bullDb\n .select()\n .from(jobs)\n .where(eq(jobs.type, type))\n .limit(1);\n if (!def) {\n throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);\n }\n return def as JobDefinitionRow;\n }\n\n private async loadRun(id: string): Promise<JobRun | null> {\n const [row] = await this.bullDb\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, id))\n .limit(1);\n return (row as JobRun) ?? null;\n }\n\n /** Close all open queue + flow connections. Called on module destroy. */\n async closeConnections(): Promise<void> {\n for (const q of this.queues.values()) {\n await q.close().catch(() => undefined);\n }\n this.queues.clear();\n if (this._flow) {\n await this._flow.close().catch(() => undefined);\n this._flow = null;\n }\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n","/**\n * Drizzle schema for the job orchestration domain (ADR-022).\n *\n * Three tables model the lifecycle of a durable job:\n * - `job` — definitions keyed by handler type (e.g. 'onboarding').\n * - `job_run` — one row per attempt to execute a job; worker claims\n * rows directly via SELECT ... FOR UPDATE SKIP LOCKED.\n * - `job_step` — individual steps within a run; memoises output for replay.\n *\n * Phase 1 ships only this layer. There is no `job_queue` table, no executor\n * port — see ADR-022 and `.claude/skills/jobs/SKILL.md` for the rationale.\n */\nimport {\n pgEnum,\n pgTable,\n uuid,\n text,\n jsonb,\n integer,\n timestamp,\n index,\n uniqueIndex,\n} from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport type { InferSelectModel } from 'drizzle-orm';\n\n// ─── Internal $type<> helpers ───────────────────────────────────────────────\n// Annotation types for jsonb columns only. JOB-2 defines the public protocol\n// types; these remain private to this file.\n\ntype RetryPolicy = {\n attempts: number;\n backoff: 'fixed' | 'exponential';\n baseMs: number;\n nonRetryableErrors?: string[];\n};\n\ntype JobRunError = {\n message: string;\n stack?: string;\n retryable: boolean;\n attempt: number;\n};\n\n// ─── Enums ──────────────────────────────────────────────────────────────────\n\nexport const jobRunStatusEnum = pgEnum('job_run_status', [\n 'pending',\n 'running',\n 'waiting',\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n]);\n\n// extended in ADR-027: tool_call | llm_call | wait | checkpoint | message\nexport const jobStepKindEnum = pgEnum('job_step_kind', ['task']);\n\nexport const jobStepStatusEnum = pgEnum('job_step_status', [\n 'pending',\n 'running',\n 'completed',\n 'failed',\n 'skipped',\n]);\n\nexport const collisionModeEnum = pgEnum('job_collision_mode', [\n 'queue',\n 'reject',\n 'replace',\n]);\n\nexport const replayFromEnum = pgEnum('job_replay_from', [\n 'scratch',\n 'last_step',\n 'last_checkpoint',\n]);\n\nexport const parentClosePolicyEnum = pgEnum('job_parent_close_policy', [\n 'terminate',\n 'cancel',\n 'abandon',\n]);\n\n// Phase 3 placeholder — see ADR-025\nexport const waitKindEnum = pgEnum('job_wait_kind', ['signal']);\n\n// Phase 2 may add more sources; requires Atlas migration\nexport const triggerSourceEnum = pgEnum('job_trigger_source', [\n 'manual',\n 'schedule',\n 'event',\n 'parent',\n]);\n\n// ─── job ────────────────────────────────────────────────────────────────────\n\nexport const jobs = pgTable('job', {\n type: text('type').primaryKey(),\n version: integer('version').notNull().default(1),\n pool: text('pool').notNull(),\n scopeEntityType: text('scope_entity_type'),\n retryPolicy: jsonb('retry_policy').notNull().$type<RetryPolicy>(),\n timeoutMs: integer('timeout_ms'),\n concurrencyKeyTemplate: text('concurrency_key_template'),\n collisionMode: collisionModeEnum('collision_mode').notNull().default('queue'),\n dedupeKeyTemplate: text('dedupe_key_template'),\n dedupeWindowMs: integer('dedupe_window_ms'),\n priorityDefault: integer('priority_default').notNull().default(0),\n replayFrom: replayFromEnum('replay_from').notNull().default('last_checkpoint'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n});\n\nexport type JobDefinitionRow = InferSelectModel<typeof jobs>;\n\n// ─── job_run ────────────────────────────────────────────────────────────────\n\nexport const jobRuns = pgTable(\n 'job_run',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobType: text('job_type').notNull().references(() => jobs.type),\n jobVersion: integer('job_version').notNull(),\n parentRunId: uuid('parent_run_id').references((): any => jobRuns.id),\n /**\n * Service generates `id` client-side via randomUUID() and sets\n * root_run_id = id for root runs (single INSERT, no self-FK race).\n */\n rootRunId: uuid('root_run_id').notNull(),\n parentClosePolicy: parentClosePolicyEnum('parent_close_policy')\n .notNull()\n .default('terminate'),\n scopeEntityType: text('scope_entity_type'),\n scopeEntityId: text('scope_entity_id'),\n tenantId: text('tenant_id'),\n tags: jsonb('tags').notNull().default({}).$type<Record<string, string>>(),\n pool: text('pool').notNull(),\n priority: integer('priority').notNull().default(0),\n concurrencyKey: text('concurrency_key'),\n dedupeKey: text('dedupe_key'),\n status: jobRunStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').notNull().$type<Record<string, unknown>>(),\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n triggerSource: triggerSourceEnum('trigger_source').notNull(),\n triggerRef: text('trigger_ref'),\n runAt: timestamp('run_at', { withTimezone: true }).notNull().defaultNow(),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n claimedAt: timestamp('claimed_at', { withTimezone: true }),\n attempts: integer('attempts').notNull().default(0),\n // Phase 3 placeholder — see ADR-025\n waitKind: waitKindEnum('wait_kind'),\n // Phase 3 placeholder — see ADR-025\n resumeToken: text('resume_token'),\n // Phase 3 placeholder — see ADR-025\n waitDeadline: timestamp('wait_deadline', { withTimezone: true }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n /** Claim query: ORDER BY priority DESC, run_at ASC. */\n idxJobRunClaim: index('idx_job_run_claim').on(t.status, t.pool, t.runAt),\n /** Tree traversal / cascade cancel. */\n idxJobRunRoot: index('idx_job_run_root').on(t.rootRunId),\n /** listForScope query. */\n idxJobRunScope: index('idx_job_run_scope').on(t.scopeEntityType, t.scopeEntityId),\n /** Idempotency collapse — partial index. */\n idxJobRunDedupe: index('idx_job_run_dedupe')\n .on(t.jobType, t.dedupeKey)\n .where(sql`${t.dedupeKey} IS NOT NULL`),\n /** Collision check — partial index. */\n idxJobRunConcurrency: index('idx_job_run_concurrency')\n .on(t.concurrencyKey)\n .where(\n sql`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`,\n ),\n }),\n);\n\nexport type JobRunRow = InferSelectModel<typeof jobRuns>;\n\n// ─── job_step ───────────────────────────────────────────────────────────────\n\nexport const jobSteps = pgTable(\n 'job_step',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobRunId: uuid('job_run_id').notNull().references(() => jobRuns.id),\n stepId: text('step_id').notNull(),\n kind: jobStepKindEnum('kind').notNull().default('task'),\n /**\n * Monotonic within run. integer (max ~2B per run) is sufficient —\n * downgraded from ADR-022's bigint; revisit only if a single run\n * ever exceeds 2 billion steps.\n */\n seq: integer('seq').notNull(),\n status: jobStepStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').$type<Record<string, unknown>>(),\n /** Memoised on success for replay. */\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n attempts: integer('attempts').notNull().default(0),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n },\n (t) => ({\n /** No duplicate step IDs per run. */\n idxJobStepRunStep: uniqueIndex('idx_job_step_run_step').on(t.jobRunId, t.stepId),\n /** Ordered timeline reads. */\n idxJobStepTimeline: index('idx_job_step_timeline').on(t.jobRunId, t.seq),\n }),\n);\n\nexport type JobStepRow = InferSelectModel<typeof jobSteps>;\n","/**\n * DrizzleJobOrchestrator — Postgres-backed implementation of\n * `IJobOrchestrator` (ADR-022, JOB-3).\n *\n * Single-layer architecture: `start` writes a single `job_run` row; the\n * `JobWorker` polling loop claims it directly via `FOR UPDATE SKIP LOCKED`.\n * No `job_queue` table, no executor port. See `docs/specs/JOB-3.md`.\n */\nimport { randomUUID } from 'node:crypto';\nimport { Inject, Injectable, Logger } from '@nestjs/common';\nimport { and, desc, eq, gt, inArray, isNotNull, ne, notInArray, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\nimport { DRIZZLE } from '../../constants/tokens';\nimport {\n jobRuns,\n jobs,\n type JobDefinitionRow,\n type JobRunRow,\n} from './job-orchestration.schema';\nimport type {\n CancelOptions,\n IJobOrchestrator,\n JobPoolDef,\n JobRun,\n JobUpsertEntry,\n StartOptions,\n} from './job-orchestrator.protocol';\nimport {\n JobCollisionError,\n JobNotReplayableError,\n JobTemplateFieldMissingError,\n JobTypeNotFoundError,\n MissingTenantIdError,\n} from './jobs-errors';\nimport { jobSteps } from './job-orchestration.schema';\nimport { JOBS_MULTI_TENANT } from './jobs-domain.tokens';\n\n/**\n * Terminal statuses — transitions into these are final. Used by `cancel`\n * (to short-circuit idempotently) and by `replay` (as the guard gate).\n */\nexport const TERMINAL_STATUSES = [\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n] as const;\ntype TerminalStatus = (typeof TERMINAL_STATUSES)[number];\ntype JobRunStatus = JobRunRow['status'];\n\n/** Statuses excluded from dedupe window matches per ADR-022. */\nconst DEDUPE_EXCLUDED_STATUSES: JobRunStatus[] = ['canceled', 'failed'];\n/** Statuses that count as in-flight for concurrency collision checks. */\nconst IN_FLIGHT_STATUSES: JobRunStatus[] = ['pending', 'running'];\n\n/**\n * Substitute `{{field}}` placeholders against the input payload.\n *\n * Implementation decision (JOB-3, 2026-04-19): simple `{{field}}` single-key\n * substitution, no dotted paths, no Mustache/Handlebars dependency. A missing\n * field throws `JobTemplateFieldMissingError` synchronously — cheaper than\n * discovering the misconfiguration at claim time.\n */\nexport function evaluateKeyTemplate(\n template: string,\n input: Record<string, unknown>,\n): string {\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_match, field: string) => {\n const value = input[field];\n if (value === undefined || value === null) {\n throw new JobTemplateFieldMissingError(template, field);\n }\n return String(value);\n });\n}\n\n@Injectable()\nexport class DrizzleJobOrchestrator implements IJobOrchestrator {\n // TODO(logging-subsystem): swap to ILogger once ADR-028 lands\n private readonly logger = new Logger(DrizzleJobOrchestrator.name);\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n ) {}\n\n /**\n * JOB-8 — resolve `tenantId` for a mutating / targeted-read call.\n * Returns the tenant value that should be written to the row (or compared\n * against in a WHERE clause). When `multiTenant` is off, the column is\n * forced to `null` regardless of what callers pass. When on, `undefined`\n * throws; `null` and strings pass through untouched.\n */\n private resolveTenantId(\n method: string,\n tenantId: string | null | undefined,\n ): string | null {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return tenantId;\n }\n\n // ==========================================================================\n // start\n // ==========================================================================\n\n async start(\n type: string,\n input: unknown,\n opts: StartOptions = {},\n tx?: DrizzleTransaction,\n ): Promise<JobRun> {\n const payload = (input ?? {}) as Record<string, unknown>;\n\n // JOB-8 — resolve tenant gate up front so `multi_tenant=true` +\n // undefined surfaces before any row is touched.\n const tenantId = this.resolveTenantId('start', opts.tenantId);\n\n // BRIDGE-7: thread the optional caller tx through every read/write\n // in this method so EventFlowService.publishAndStart can bundle the\n // outbox insert, the eager job_run insert, and (for Case B) the\n // bridge_delivery pre-write into a single transaction.\n const client = (tx ?? this.db) as DrizzleClient;\n\n // 1a. Load job definition.\n const [def] = await client\n .select()\n .from(jobs)\n .where(eq(jobs.type, type))\n .limit(1);\n if (!def) throw new JobTypeNotFoundError(type);\n const definition = def as JobDefinitionRow;\n\n // 1b. Dedupe check.\n if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {\n const dedupeKey = evaluateKeyTemplate(definition.dedupeKeyTemplate, payload);\n const windowStart = new Date(Date.now() - definition.dedupeWindowMs);\n const existing = await client\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.jobType, type),\n eq(jobRuns.dedupeKey, dedupeKey),\n gt(jobRuns.createdAt, windowStart),\n // status NOT IN ('canceled', 'failed')\n notInStatus(DEDUPE_EXCLUDED_STATUSES),\n ),\n )\n .orderBy(desc(jobRuns.createdAt))\n .limit(1);\n if (existing.length > 0) {\n return existing[0] as JobRun;\n }\n }\n\n // 1c. Concurrency collision check.\n let concurrencyKey: string | null = null;\n if (definition.concurrencyKeyTemplate) {\n concurrencyKey = evaluateKeyTemplate(\n definition.concurrencyKeyTemplate,\n payload,\n );\n const inFlight = await client\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.concurrencyKey, concurrencyKey),\n inArray(jobRuns.status, IN_FLIGHT_STATUSES),\n ),\n )\n .limit(1);\n if (inFlight.length > 0) {\n const incumbent = inFlight[0] as JobRun;\n switch (definition.collisionMode) {\n case 'reject':\n throw new JobCollisionError(type, concurrencyKey, incumbent);\n case 'replace':\n // JOB-8 — thread the incumbent's own tenantId through the\n // internal cascade. Without this, every `replace`-collision\n // start() under multiTenant=true throws MissingTenantIdError\n // from the inner cancel() call instead of cancelling the\n // incumbent. Mirrors the memory backend's `cancelLocked(\n // incumbent.id, ..., incumbent.tenantId)` pattern.\n await this.cancel(incumbent.id, {\n cascade: true,\n reason: 'replaced',\n tenantId: incumbent.tenantId,\n });\n break;\n case 'queue':\n // Fall through — row is inserted; claim query gates it until\n // the incumbent transitions (see JobWorker.processRun queue gate).\n break;\n }\n }\n }\n\n // 1d. Resolve id + rootRunId, INSERT.\n const newId = randomUUID();\n let rootRunId: string = newId;\n if (opts.parentRunId) {\n const [parent] = await client\n .select({ rootRunId: jobRuns.rootRunId })\n .from(jobRuns)\n .where(eq(jobRuns.id, opts.parentRunId))\n .limit(1);\n if (!parent) {\n throw new Error(\n `parentRunId ${opts.parentRunId} does not reference an existing job_run`,\n );\n }\n rootRunId = parent.rootRunId;\n }\n\n const dedupeKey =\n definition.dedupeKeyTemplate\n ? evaluateKeyTemplate(definition.dedupeKeyTemplate, payload)\n : null;\n\n const [inserted] = await client\n .insert(jobRuns)\n .values({\n id: newId,\n jobType: type,\n jobVersion: definition.version,\n parentRunId: opts.parentRunId ?? null,\n rootRunId,\n parentClosePolicy: opts.parentClosePolicy ?? 'terminate',\n scopeEntityType: opts.scope?.entityType ?? null,\n scopeEntityId: opts.scope?.entityId ?? null,\n tenantId,\n tags: opts.tags ?? {},\n pool: opts.pool ?? definition.pool,\n priority: opts.priority ?? definition.priorityDefault,\n concurrencyKey,\n dedupeKey,\n status: 'pending',\n input: payload,\n output: null,\n error: null,\n triggerSource: opts.triggerSource ?? 'manual',\n triggerRef: opts.triggerRef ?? null,\n runAt: opts.runAt ?? new Date(),\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n attempts: 0,\n })\n .returning();\n\n return inserted as JobRun;\n }\n\n // ==========================================================================\n // cancel\n // ==========================================================================\n\n async cancel(runId: string, opts: CancelOptions = {}): Promise<void> {\n // JOB-8 — resolve tenant gate up front (strict undefined-throws).\n const tenantId = this.resolveTenantId('cancel', opts.tenantId);\n\n // Load target.\n const [target] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!target) return;\n // JOB-8 — cross-tenant cancel is a silent no-op (no existence leak).\n if (this.multiTenant && target.tenantId !== tenantId) return;\n if (TERMINAL_STATUSES.includes(target.status as TerminalStatus)) {\n return; // idempotent\n }\n\n // Atomic transition, guarded against concurrent terminal moves.\n const [cancelled] = await this.db\n .update(jobRuns)\n .set({\n status: 'canceled',\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(\n and(eq(jobRuns.id, runId), notInStatus([...TERMINAL_STATUSES])),\n )\n .returning();\n\n if (!cancelled) return; // lost the race; already terminal\n\n if (opts.cascade === false) return;\n\n // Fetch descendants and branch on parent_close_policy.\n const descendants = await this.db\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.rootRunId, target.rootRunId),\n ne(jobRuns.id, runId),\n notInStatus([...TERMINAL_STATUSES]),\n ),\n );\n\n for (const child of descendants) {\n const policy = (child as JobRunRow).parentClosePolicy;\n if (policy === 'abandon') continue;\n // 'terminate' | 'cancel' — both transition the child to canceled.\n await this.db\n .update(jobRuns)\n .set({\n status: 'canceled',\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(\n and(\n eq(jobRuns.id, (child as JobRunRow).id),\n notInStatus([...TERMINAL_STATUSES]),\n ),\n );\n }\n\n void opts.reason; // reserved for future audit logging\n }\n\n // ==========================================================================\n // replay\n // ==========================================================================\n\n async replay(runId: string): Promise<JobRun> {\n // Load target + its job definition (we need replay_from).\n const [target] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!target) {\n throw new Error(`replay: run ${runId} not found`);\n }\n const run = target as JobRunRow;\n if (!TERMINAL_STATUSES.includes(run.status as TerminalStatus)) {\n throw new JobNotReplayableError(runId, run.status);\n }\n\n const [def] = await this.db\n .select()\n .from(jobs)\n .where(eq(jobs.type, run.jobType))\n .limit(1);\n if (!def) throw new JobTypeNotFoundError(run.jobType);\n const mode = (def as JobDefinitionRow).replayFrom;\n\n // Atomic: step reset + run reset must commit together.\n const result = await this.db.transaction(async (tx) => {\n if (mode === 'scratch') {\n await tx.delete(jobSteps).where(eq(jobSteps.jobRunId, runId));\n } else if (mode === 'last_step') {\n // Delete only non-completed step rows — completed steps stay memoised.\n await tx\n .delete(jobSteps)\n .where(\n and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, 'completed')),\n );\n } else {\n // 'last_checkpoint' — Phase 1 has no explicit checkpoint markers, so\n // behaviour collapses to `last_step`. See docs/specs/JOB-3.md\n // \"Implementation Decisions\" — planned divergence in a later phase.\n await tx\n .delete(jobSteps)\n .where(\n and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, 'completed')),\n );\n }\n\n const [updated] = await tx\n .update(jobRuns)\n .set({\n status: 'pending',\n attempts: 0,\n runAt: new Date(),\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n error: null,\n output: null,\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, runId))\n .returning();\n return updated as JobRunRow;\n });\n\n return result as JobRun;\n }\n\n // ==========================================================================\n // upsertJobRows — boot-time materialisation of `job` definitions\n // ==========================================================================\n\n /**\n * Hash-gated `INSERT … ON CONFLICT (type) DO UPDATE … WHERE` per Q3\n * resolution (2026-04-19): the `UPDATE` branch executes only when one\n * of the persisted metadata fields differs from the incoming payload;\n * `version` bumps only on real change; concurrent boots with identical\n * content are idempotent no-ops.\n *\n * Why this shape (not `DO NOTHING`, not advisory locks):\n * - `DO NOTHING` would let an old-version instance leave a stale row\n * that a new-version instance can't overwrite during a rolling deploy.\n * - Advisory locks add latency and leak risk under crashes.\n * - The `WHERE … IS DISTINCT FROM …` clause makes the conditional\n * atomic — no read-modify-write race on `version` between concurrent\n * boots.\n *\n * Orphan detection: a single `SELECT type FROM job WHERE type NOT IN (...)`\n * returns the types present in DB but absent from `entries`. Caller (boot\n * validator) decides whether to throw `BootValidationError`.\n */\n async upsertJobRows(\n entries: JobUpsertEntry[],\n poolConfig: ReadonlyMap<string, JobPoolDef>,\n ): Promise<{ orphaned: string[] }> {\n void poolConfig; // pool validation is the module's responsibility; orchestrator just persists\n\n for (const entry of entries) {\n const meta = entry.meta;\n const pool = meta.pool ?? 'batch';\n const retryPolicy = meta.retry ?? {\n attempts: 1,\n backoff: 'fixed' as const,\n baseMs: 0,\n };\n const concurrencyKeyTemplate =\n (meta.concurrency as { key?: unknown } | undefined)?.key;\n const concurrencyKeyTemplateStr =\n typeof concurrencyKeyTemplate === 'string' ? concurrencyKeyTemplate : null;\n const collisionMode =\n (meta.concurrency?.collisionMode as JobDefinitionRow['collisionMode']) ??\n 'queue';\n const dedupeKeyTemplate =\n (meta.dedupe as { key?: unknown } | undefined)?.key;\n const dedupeKeyTemplateStr =\n typeof dedupeKeyTemplate === 'string' ? dedupeKeyTemplate : null;\n const dedupeWindowMs = meta.dedupe?.windowMs ?? null;\n const timeoutMs = meta.timeoutMs ?? null;\n const replayFrom = meta.replayFrom ?? 'last_checkpoint';\n const scopeEntityType = meta.scope?.entity ?? null;\n // Q3 resolution: priority_default and replay_from are part of the\n // hashed metadata even though they aren't currently set via decorator\n // metadata above (priority_default has no `@JobHandler` field yet).\n // Default to 0 to keep UPDATE branch quiet across deploys.\n const priorityDefault = 0;\n\n // Hash-gated upsert: every metadata column appears in the WHERE clause\n // so the UPDATE branch only fires on a real change. `version` bumps\n // exactly when the WHERE matches.\n await this.db\n .insert(jobs)\n .values({\n type: entry.type,\n version: 1,\n pool,\n scopeEntityType,\n retryPolicy,\n timeoutMs,\n concurrencyKeyTemplate: concurrencyKeyTemplateStr,\n collisionMode,\n dedupeKeyTemplate: dedupeKeyTemplateStr,\n dedupeWindowMs,\n priorityDefault,\n replayFrom,\n })\n .onConflictDoUpdate({\n target: jobs.type,\n set: {\n pool: sql`EXCLUDED.pool`,\n scopeEntityType: sql`EXCLUDED.scope_entity_type`,\n retryPolicy: sql`EXCLUDED.retry_policy`,\n timeoutMs: sql`EXCLUDED.timeout_ms`,\n concurrencyKeyTemplate: sql`EXCLUDED.concurrency_key_template`,\n collisionMode: sql`EXCLUDED.collision_mode`,\n dedupeKeyTemplate: sql`EXCLUDED.dedupe_key_template`,\n dedupeWindowMs: sql`EXCLUDED.dedupe_window_ms`,\n priorityDefault: sql`EXCLUDED.priority_default`,\n replayFrom: sql`EXCLUDED.replay_from`,\n version: sql`${jobs.version} + 1`,\n updatedAt: sql`now()`,\n },\n // The hash gate: every field listed in the Q3 resolution appears\n // here. `IS DISTINCT FROM` is the null-safe inequality operator;\n // jsonb cast to text gives stable comparison without invoking a\n // dedicated hash column (avoids a JOB-1 schema migration).\n setWhere: sql`\n ${jobs.pool} IS DISTINCT FROM EXCLUDED.pool OR\n ${jobs.retryPolicy}::text IS DISTINCT FROM EXCLUDED.retry_policy::text OR\n ${jobs.timeoutMs} IS DISTINCT FROM EXCLUDED.timeout_ms OR\n ${jobs.concurrencyKeyTemplate} IS DISTINCT FROM EXCLUDED.concurrency_key_template OR\n ${jobs.collisionMode} IS DISTINCT FROM EXCLUDED.collision_mode OR\n ${jobs.dedupeKeyTemplate} IS DISTINCT FROM EXCLUDED.dedupe_key_template OR\n ${jobs.dedupeWindowMs} IS DISTINCT FROM EXCLUDED.dedupe_window_ms OR\n ${jobs.priorityDefault} IS DISTINCT FROM EXCLUDED.priority_default OR\n ${jobs.replayFrom} IS DISTINCT FROM EXCLUDED.replay_from OR\n ${jobs.scopeEntityType} IS DISTINCT FROM EXCLUDED.scope_entity_type\n `,\n });\n }\n\n // Orphan detection: any `job` row whose type is not in the registry.\n const types = entries.map((e) => e.type);\n const orphans =\n types.length === 0\n ? await this.db.select({ type: jobs.type }).from(jobs)\n : await this.db\n .select({ type: jobs.type })\n .from(jobs)\n .where(notInArray(jobs.type, types));\n\n return { orphaned: orphans.map((o) => o.type) };\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction notInStatus(statuses: JobRunStatus[]) {\n // Drizzle's inArray composes with `not` via negation helper; use raw sql\n // to stay readable. `inArray` + `.not()` isn't idiomatic in 0.45.\n const negated = statuses.map((s) => ne(jobRuns.status, s));\n return and(...negated);\n}\n\n// `isNotNull` + `gt` imports are retained for potential future use; silence\n// unused-import lint by re-exporting via `void`.\nvoid isNotNull;\n","/**\n * Typed errors for the job orchestration domain (ADR-022, JOB-3).\n *\n * All thrown by the Drizzle orchestrator (and mirrored by the Memory\n * backend in JOB-4). They exist as classes so consumers can `instanceof`\n * them in catch blocks and exception filters can map them to HTTP codes.\n */\nimport type { JobRun } from './job-orchestrator.protocol';\n\n/**\n * `start(type, …)` was called for a job type that has no row in the `job`\n * table. At runtime this usually means the handler was not decorated or the\n * boot validator (JOB-5) has not registered it yet.\n */\nexport class JobTypeNotFoundError extends Error {\n override readonly name = 'JobTypeNotFoundError';\n constructor(public readonly jobType: string) {\n super(`No job definition registered for type '${jobType}'.`);\n }\n}\n\n/**\n * Thrown by `start` when `collision_mode === 'reject'` and a non-terminal\n * run with the same `concurrency_key` already exists. Carries the incumbent\n * so callers can surface its id or subscribe to its completion event.\n */\nexport class JobCollisionError extends Error {\n override readonly name = 'JobCollisionError';\n constructor(\n public readonly jobType: string,\n public readonly concurrencyKey: string,\n public readonly incumbent: JobRun,\n ) {\n super(\n `Job type '${jobType}' has an in-flight run with concurrency_key ` +\n `'${concurrencyKey}' (incumbent ${incumbent.id}); collision_mode=reject.`,\n );\n }\n}\n\n/**\n * `replay` was called on a run that is not in a replayable terminal state\n * (i.e. still `pending` / `running` / `waiting`). Replay always spawns\n * fresh execution and therefore requires the source run to be settled.\n */\nexport class JobNotReplayableError extends Error {\n override readonly name = 'JobNotReplayableError';\n constructor(\n public readonly runId: string,\n public readonly currentStatus: string,\n ) {\n super(\n `Run ${runId} is not replayable from status '${currentStatus}'. ` +\n `Only 'completed', 'failed', 'timed_out', and 'canceled' are eligible.`,\n );\n }\n}\n\n/**\n * A `concurrency_key_template` or `dedupe_key_template` referenced a field\n * that is not present on the input payload. Caught at `start` time so the\n * caller sees the misconfiguration synchronously rather than at claim time.\n */\nexport class JobTemplateFieldMissingError extends Error {\n override readonly name = 'JobTemplateFieldMissingError';\n constructor(\n public readonly template: string,\n public readonly field: string,\n ) {\n super(\n `Template '${template}' references input field '${field}' which is ` +\n `missing or undefined on the payload.`,\n );\n }\n}\n\n/**\n * Thrown by the four multi-tenant-aware service-layer backends (JOB-8)\n * when `JobsDomainModule` was configured with `multiTenant: true` but the\n * caller did not pass a `tenantId` in the relevant options object.\n *\n * **Strict enforcement rationale (resolved 2026-04-18).** Cross-tenant data\n * leakage is the worst class of bug a multi-tenant system can ship; surfacing\n * the misuse loudly at the call site (rather than silently defaulting to\n * `null` or to the \"last tenant seen\") prevents both accidental global\n * writes and sneaky reads that return a union of tenants.\n *\n * - `undefined` `tenantId` → throw this error.\n * - Explicit `null` `tenantId` → passes; opts the call into cross-tenant\n * background work (e.g. a nightly housekeeping job that must scan all\n * tenants). The row is persisted with `tenant_id = NULL`.\n */\nexport class MissingTenantIdError extends Error {\n override readonly name = 'MissingTenantIdError';\n constructor(public readonly method: string) {\n super(\n `MissingTenantIdError: JobsDomainModule was configured with ` +\n `multiTenant=true but ${method} was called without tenantId ` +\n `(undefined). Pass an explicit tenantId, or pass null for ` +\n `cross-tenant work.`,\n );\n }\n}\n\n/**\n * Thrown by `JobWorkerModule.onModuleInit` (Drizzle backend only) when the\n * `job` table contains type rows for which no `@JobHandler` is registered\n * in the running process. Surfaces every orphaned type at once so a single\n * boot tells the operator everything to clean up.\n *\n * Skipped entirely in memory mode (Q4 resolution 2026-04-19) — the memory\n * backend has no DB rows to validate; `MemoryJobOrchestrator.start()`\n * throws `JobTypeNotFoundError` synchronously for unknown types instead.\n */\nexport class BootValidationError extends Error {\n override readonly name = 'BootValidationError';\n constructor(public readonly missingHandlers: string[]) {\n super(\n `BootValidationError: ${missingHandlers.length} orphaned job type(s) ` +\n `in 'job' table with no matching @JobHandler in the running process: ` +\n `[${missingHandlers.join(', ')}]. Either register the handler(s) or ` +\n `remove the rows.`,\n );\n }\n}\n\n/**\n * Thrown by `JobWorkerModule.onModuleInit` when one or more `@JobHandler`\n * classes target a `reserved: true` pool from the resolved pool config\n * (the three `events_*` pools are reserved for the events subsystem\n * outbox drain). Listing every offender on a single boot avoids the\n * fix-one-restart-fix-next loop.\n */\nexport class ReservedPoolViolationError extends Error {\n override readonly name = 'ReservedPoolViolationError';\n constructor(\n public readonly offenders: ReadonlyArray<{\n handlerClass: string;\n pool: string;\n }>,\n ) {\n super(\n `ReservedPoolViolationError: ${offenders.length} @JobHandler(s) target ` +\n `reserved pools — reserved pools are framework-only:\\n` +\n offenders\n .map((o) => ` - ${o.handlerClass} → pool='${o.pool}'`)\n .join('\\n'),\n );\n }\n}\n","/**\n * Injection tokens for the job orchestration domain layer (ADR-022, JOB-2).\n *\n * Consumer code injects these symbols via `@Inject(JOB_ORCHESTRATOR)` etc.;\n * concrete backends (JOB-3 Drizzle, JOB-4 Memory) provide the implementations\n * through `JobsDomainModule.forRoot({ backend })` in JOB-5.\n *\n * Each token is a unique `Symbol` — guaranteed distinct from every other\n * Symbol at runtime, which is exactly the uniqueness guarantee Nest's DI\n * container relies on for token-based lookup.\n */\nexport const JOB_ORCHESTRATOR = Symbol('JOB_ORCHESTRATOR');\nexport const JOB_RUN_SERVICE = Symbol('JOB_RUN_SERVICE');\nexport const JOB_STEP_SERVICE = Symbol('JOB_STEP_SERVICE');\n\n/**\n * Multi-tenancy opt-in flag (JOB-8). Bound to the boolean passed in via\n * `JobsDomainModule.forRoot({ multiTenant })`, defaulting to `false`.\n *\n * When `true`, the four service-layer backends (Drizzle + Memory orchestrator\n * and run-service) enforce `tenantId` on every mutating / targeted-read call:\n * `start`, `cancel`, `listForScope`, `cancelForScope`, `rescheduleForScope`.\n * Missing (`undefined`) `tenantId` throws `MissingTenantIdError`; explicit\n * `null` opts into cross-tenant background work and passes through.\n *\n * The JobWorker claim loop is **cross-tenant by design** — the worker has no\n * tenant context; `tenantId` is populated at write time and enforced on\n * targeted reads. See docs/specs/JOB-8.md.\n */\nexport const JOBS_MULTI_TENANT = Symbol('JOBS_MULTI_TENANT');\n","/**\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 { loadPoolConfig, type PoolConfig } from './pool-config.loader';\n\n/**\n * #6 — Structural mirror of BullMQ's `ConnectionOptions`. Declared locally\n * so this config file (which ships into EVERY jobs install, drizzle or\n * bullmq) does NOT need the `bullmq` peer dep resolved by the consumer's\n * tsc. The bullmq backend internally casts to the real `ConnectionOptions`\n * — that file is only vendored when `--backend bullmq` is selected\n * (see `backendFileFilter`).\n *\n * Accepts the `{ url }` shape this resolver emits, plus the host/port/\n * password/db form BullMQ also accepts, with an open index for any extra\n * ioredis options consumers may flow through.\n */\nexport type BullMqConnectionOptions = {\n url?: string;\n host?: string;\n port?: number;\n password?: string;\n db?: number;\n [key: string]: unknown;\n};\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: BullMqConnectionOptions;\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 },\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":"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","names":["Inject","Injectable","Logger","eq","sql","dedupeKey","sql","jobs","Logger","eq","Injectable","Inject"]}
|
|
@@ -207,6 +207,9 @@ var BullMQJobWorker = class _BullMQJobWorker {
|
|
|
207
207
|
}
|
|
208
208
|
this.worker = new WorkerCtor(
|
|
209
209
|
this.options.queueName,
|
|
210
|
+
// #6 / noImplicitAny — explicit annotations so the file stays
|
|
211
|
+
// strict-clean even when consumers compile under a stricter tsconfig
|
|
212
|
+
// than the one used to type-check the runtime tree.
|
|
210
213
|
(job) => this.process(job),
|
|
211
214
|
{
|
|
212
215
|
connection: this.options.connection,
|