@pattern-stack/codegen 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/{job-orchestrator.protocol-BwsBd37o.d.ts → job-orchestrator.protocol-CHOEqBDk.d.ts} +36 -1
  2. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +2 -2
  3. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
  4. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
  5. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +2 -1
  6. package/dist/runtime/subsystems/bridge/bridge.module.js +3 -0
  7. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  8. package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
  9. package/dist/runtime/subsystems/bridge/index.d.ts +1 -1
  10. package/dist/runtime/subsystems/bridge/index.js +3 -0
  11. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  12. package/dist/runtime/subsystems/index.d.ts +2 -2
  13. package/dist/runtime/subsystems/index.js +3 -0
  14. package/dist/runtime/subsystems/index.js.map +1 -1
  15. package/dist/runtime/subsystems/jobs/index.d.ts +2 -1
  16. package/dist/runtime/subsystems/jobs/index.js +3 -0
  17. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  18. package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +2 -1
  19. package/dist/runtime/subsystems/jobs/job-handler.base.js.map +1 -1
  20. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +2 -1
  21. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +2 -1
  22. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +2 -1
  23. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
  24. package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +2 -1
  25. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +2 -1
  26. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +3 -0
  27. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -1
  28. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +2 -1
  29. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +3 -0
  30. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
  31. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +2 -1
  32. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +3 -0
  33. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
  34. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +11 -2
  35. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +2 -1
  36. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
  37. package/dist/runtime/subsystems/jobs/job-worker.d.ts +2 -1
  38. package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
  39. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +3 -2
  40. package/dist/runtime/subsystems/jobs/job-worker.module.js +3 -0
  41. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  42. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +3 -0
  43. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  44. package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +2 -1
  45. package/dist/runtime/subsystems/observability/index.d.ts +1 -1
  46. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +1 -1
  47. package/dist/runtime/subsystems/observability/observability.service.d.ts +1 -1
  48. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +2 -2
  49. package/dist/runtime/subsystems/observability/reporters/index.d.ts +2 -2
  50. package/package.json +5 -1
  51. package/runtime/subsystems/jobs/job-handler.base.ts +36 -0
  52. package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +3 -0
  53. package/runtime/subsystems/jobs/job-run-service.protocol.ts +9 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../runtime/subsystems/jobs/job-run-service.drizzle-backend.ts","../../../../runtime/constants/tokens.ts","../../../../runtime/subsystems/jobs/job-orchestration.schema.ts","../../../../runtime/subsystems/jobs/job-run-keyset-cursor.ts","../../../../runtime/subsystems/jobs/jobs-domain.tokens.ts","../../../../runtime/subsystems/jobs/jobs-errors.ts"],"sourcesContent":["/**\n * DrizzleJobRunService — scope-oriented reads and bulk operations against\n * `job_run` (ADR-022, JOB-3).\n *\n * Separate from the orchestrator because the access pattern differs: this\n * service scans by `(scope_entity_type, scope_entity_id)` via\n * `idx_job_run_scope`, whereas orchestrator mutates individual runs by id.\n */\nimport { Inject, Injectable } from '@nestjs/common';\nimport { and, asc, desc, eq, gte, inArray, isNull, lt, or, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, type JobRunRow } from './job-orchestration.schema';\nimport type { JobRun } from './job-orchestrator.protocol';\nimport type {\n IJobRunService,\n ListForScopeOptions,\n CancelForScopeOptions,\n RescheduleForScopeOptions,\n PoolStatusCount,\n JobRunFailure,\n ListJobRunsQuery,\n JobRunPage,\n JobRunSummary,\n} from './job-run-service.protocol';\nimport {\n clampLimit,\n decodeKeysetCursor,\n encodeKeysetCursor,\n toJobRunSummary,\n} from './job-run-keyset-cursor';\nimport type { IJobOrchestrator } from './job-orchestrator.protocol';\nimport { JOB_ORCHESTRATOR, JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport { MissingTenantIdError } from './jobs-errors';\n\nconst NON_TERMINAL_STATUSES: JobRunRow['status'][] = [\n 'pending',\n 'running',\n 'waiting',\n];\n\n@Injectable()\nexport class DrizzleJobRunService implements IJobRunService {\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n ) {}\n\n /**\n * JOB-8 — produce the tenant WHERE fragment (or `null` to opt out).\n * Returns `null` when multi-tenancy is off (caller skips the predicate).\n * Throws `MissingTenantIdError` when on + `undefined`.\n * When on + explicit `null`, filters `tenant_id IS NULL`.\n */\n private tenantCondition(\n method: string,\n tenantId: string | null | undefined,\n ) {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return tenantId === null\n ? isNull(jobRuns.tenantId)\n : eq(jobRuns.tenantId, tenantId);\n }\n\n async listForScope(\n entityType: string,\n entityId: string,\n opts: ListForScopeOptions = {},\n ): Promise<JobRun[]> {\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n ];\n const tenantCond = this.tenantCondition('listForScope', opts.tenantId);\n if (tenantCond) conditions.push(tenantCond);\n if (opts.status) {\n if (Array.isArray(opts.status)) {\n conditions.push(inArray(jobRuns.status, opts.status));\n } else {\n conditions.push(eq(jobRuns.status, opts.status));\n }\n }\n if (opts.jobType) {\n conditions.push(eq(jobRuns.jobType, opts.jobType));\n }\n\n const orderCol = (() => {\n switch (opts.orderBy) {\n case 'created_at asc':\n return asc(jobRuns.createdAt);\n case 'run_at desc':\n return desc(jobRuns.runAt);\n case 'run_at asc':\n return asc(jobRuns.runAt);\n case 'created_at desc':\n default:\n return desc(jobRuns.createdAt);\n }\n })();\n\n let q = this.db\n .select()\n .from(jobRuns)\n .where(and(...conditions))\n .orderBy(orderCol)\n .$dynamic();\n\n if (typeof opts.limit === 'number') {\n q = q.limit(opts.limit);\n }\n if (typeof opts.offset === 'number') {\n q = q.offset(opts.offset);\n }\n\n const rows = await q;\n return rows as JobRun[];\n }\n\n async cancelForScope(\n entityType: string,\n entityId: string,\n opts: CancelForScopeOptions = {},\n ): Promise<void> {\n const tenantCond = this.tenantCondition('cancelForScope', opts.tenantId);\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n inArray(jobRuns.status, NON_TERMINAL_STATUSES),\n ];\n if (tenantCond) conditions.push(tenantCond);\n\n const rows = await this.db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(and(...conditions));\n\n for (const { id } of rows) {\n // Propagate the tenant gate into cascade-cancel. The scope query has\n // already narrowed to this tenant; passing `tenantId` through keeps\n // the orchestrator's per-row guard consistent under multi-tenant mode.\n await this.orchestrator.cancel(id, {\n cascade: true,\n tenantId: opts.tenantId,\n });\n }\n }\n\n async rescheduleForScope(\n entityType: string,\n entityId: string,\n newRunAt: Date,\n opts: RescheduleForScopeOptions = {},\n ): Promise<void> {\n const tenantCond = this.tenantCondition('rescheduleForScope', opts.tenantId);\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n eq(jobRuns.status, 'pending'),\n ];\n if (tenantCond) conditions.push(tenantCond);\n\n await this.db\n .update(jobRuns)\n .set({ runAt: newRunAt, updatedAt: new Date() })\n .where(and(...conditions));\n }\n\n async countByPoolAndStatus(\n tenantId?: string | null,\n ): Promise<PoolStatusCount[]> {\n const tenantCond = this.tenantCondition('countByPoolAndStatus', tenantId);\n const rows = await this.db\n .select({\n pool: jobRuns.pool,\n status: jobRuns.status,\n count: sql<number>`count(*)::int`.as('count'),\n })\n .from(jobRuns)\n .where(tenantCond ?? undefined)\n .groupBy(jobRuns.pool, jobRuns.status);\n\n return rows.map((r) => ({\n pool: r.pool,\n status: r.status,\n count: Number(r.count),\n }));\n }\n\n async listRecentFailed(\n limit: number,\n tenantId?: string | null,\n ): Promise<JobRunFailure[]> {\n const conditions = [eq(jobRuns.status, 'failed' as const)];\n const tenantCond = this.tenantCondition('listRecentFailed', tenantId);\n if (tenantCond) conditions.push(tenantCond);\n\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(and(...conditions))\n .orderBy(desc(jobRuns.finishedAt), desc(jobRuns.updatedAt))\n .limit(limit);\n\n return rows.map((r) => ({\n runId: r.id,\n jobType: r.jobType,\n pool: r.pool,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n failedAt: r.finishedAt ?? r.updatedAt,\n createdAt: r.createdAt,\n }));\n }\n\n async listJobRuns(query: ListJobRunsQuery = {}): Promise<JobRunPage> {\n const limit = clampLimit(query.limit);\n const conditions = [];\n\n const tenantCond = this.tenantCondition('listJobRuns', query.tenantId);\n if (tenantCond) conditions.push(tenantCond);\n if (query.poolId) conditions.push(eq(jobRuns.pool, query.poolId));\n if (query.rootRunId) conditions.push(eq(jobRuns.rootRunId, query.rootRunId));\n if (query.status) conditions.push(eq(jobRuns.status, query.status));\n if (query.since) conditions.push(gte(jobRuns.createdAt, query.since));\n\n // Keyset seek: WHERE (created_at, id) < (cursorCreatedAt, cursorId),\n // expanded into a SARGable OR so the same `created_at` index is used.\n if (query.cursor) {\n const keyset = decodeKeysetCursor(query.cursor);\n if (keyset) {\n conditions.push(\n or(\n lt(jobRuns.createdAt, keyset.createdAt),\n and(\n eq(jobRuns.createdAt, keyset.createdAt),\n lt(jobRuns.id, keyset.id),\n ),\n )!,\n );\n }\n }\n\n // Fetch one extra row to determine whether a next page exists without a\n // separate COUNT.\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(conditions.length > 0 ? and(...conditions) : undefined)\n .orderBy(desc(jobRuns.createdAt), desc(jobRuns.id))\n .limit(limit + 1);\n\n const hasMore = rows.length > limit;\n const page = hasMore ? rows.slice(0, limit) : rows;\n const items = page.map(toJobRunSummary);\n const last = page[page.length - 1];\n const nextCursor =\n hasMore && last\n ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id })\n : null;\n\n return { items, nextCursor };\n }\n\n /**\n * Internal helper used by cascade paths (not on the public protocol).\n * Exposed as a public method on the concrete class so infrastructure\n * code (cascade tests, debug tools) can call it without a cast.\n */\n async findByRootRunId(rootRunId: string): Promise<JobRun[]> {\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.rootRunId, rootRunId));\n return rows as JobRun[];\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 * Keyset (seek) cursor codec for `IJobRunService.listJobRuns` (OBS-LIST-1).\n *\n * The list is ordered `created_at DESC, id DESC`. The cursor encodes the\n * `(createdAt, id)` of the last row on the previous page so the next page\n * can seek with `WHERE (created_at, id) < (cursorCreatedAt, cursorId)`\n * rather than an `OFFSET`. Keyset pagination stays O(log n) on deep pages\n * and is stable as new rows arrive at the head.\n *\n * The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape\n * is an implementation detail — never parse it outside this module.\n *\n * Also hosts `toJobRunSummary`, the single `JobRunRow → JobRunSummary`\n * projection shared by both backends so the narrow shape stays in sync.\n */\nimport type { JobRunRow } from './job-orchestration.schema';\nimport type { JobRunSummary } from './job-run-service.protocol';\n\nexport interface JobRunKeyset {\n /** `created_at` of the last row on the previous page. */\n createdAt: Date;\n /** `id` (UUID) tie-break of the last row on the previous page. */\n id: string;\n}\n\n/** Default page size when `limit` is omitted. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Hard upper bound on page size to keep a single read bounded. */\nexport const MAX_LIST_LIMIT = 200;\n\n/** Clamp a caller-supplied `limit` into `[1, MAX_LIST_LIMIT]`. */\nexport function clampLimit(limit: number | undefined): number {\n if (typeof limit !== 'number' || !Number.isFinite(limit)) {\n return DEFAULT_LIST_LIMIT;\n }\n const floored = Math.floor(limit);\n if (floored < 1) return 1;\n if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;\n return floored;\n}\n\nexport function encodeKeysetCursor(keyset: JobRunKeyset): string {\n const tuple = [keyset.createdAt.toISOString(), keyset.id];\n return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');\n}\n\n/**\n * Decode an opaque cursor back into its `(createdAt, id)` keyset. Returns\n * `null` for a malformed cursor so callers can treat garbage input as\n * \"start from the beginning\" rather than throwing on user-supplied data.\n */\nexport function decodeKeysetCursor(cursor: string): JobRunKeyset | null {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf8');\n const parsed = JSON.parse(json) as unknown;\n if (!Array.isArray(parsed) || parsed.length !== 2) return null;\n const [iso, id] = parsed;\n if (typeof iso !== 'string' || typeof id !== 'string') return null;\n const createdAt = new Date(iso);\n if (Number.isNaN(createdAt.getTime())) return null;\n return { createdAt, id };\n } catch {\n return null;\n }\n}\n\n/**\n * Project a raw `job_run` row into the narrow `JobRunSummary` shape exposed\n * by `listJobRuns`. `errorMessage` is pulled from the jsonb `error.message`.\n */\nexport function toJobRunSummary(r: JobRunRow): JobRunSummary {\n return {\n runId: r.id,\n rootRunId: r.rootRunId,\n jobType: r.jobType,\n pool: r.pool,\n status: r.status,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n runAt: r.runAt,\n startedAt: r.startedAt,\n finishedAt: r.finishedAt,\n createdAt: r.createdAt,\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 * 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"],"mappings":"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","names":["sql","sql"]}
1
+ {"version":3,"sources":["../../../../runtime/subsystems/jobs/job-run-service.drizzle-backend.ts","../../../../runtime/constants/tokens.ts","../../../../runtime/subsystems/jobs/job-orchestration.schema.ts","../../../../runtime/subsystems/jobs/job-run-keyset-cursor.ts","../../../../runtime/subsystems/jobs/jobs-domain.tokens.ts","../../../../runtime/subsystems/jobs/jobs-errors.ts"],"sourcesContent":["/**\n * DrizzleJobRunService — scope-oriented reads and bulk operations against\n * `job_run` (ADR-022, JOB-3).\n *\n * Separate from the orchestrator because the access pattern differs: this\n * service scans by `(scope_entity_type, scope_entity_id)` via\n * `idx_job_run_scope`, whereas orchestrator mutates individual runs by id.\n */\nimport { Inject, Injectable } from '@nestjs/common';\nimport { and, asc, desc, eq, gte, inArray, isNull, lt, or, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, type JobRunRow } from './job-orchestration.schema';\nimport type { JobRun } from './job-orchestrator.protocol';\nimport type {\n IJobRunService,\n ListForScopeOptions,\n CancelForScopeOptions,\n RescheduleForScopeOptions,\n PoolStatusCount,\n JobRunFailure,\n ListJobRunsQuery,\n JobRunPage,\n JobRunSummary,\n} from './job-run-service.protocol';\nimport {\n clampLimit,\n decodeKeysetCursor,\n encodeKeysetCursor,\n toJobRunSummary,\n} from './job-run-keyset-cursor';\nimport type { IJobOrchestrator } from './job-orchestrator.protocol';\nimport { JOB_ORCHESTRATOR, JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport { MissingTenantIdError } from './jobs-errors';\n\nconst NON_TERMINAL_STATUSES: JobRunRow['status'][] = [\n 'pending',\n 'running',\n 'waiting',\n];\n\n@Injectable()\nexport class DrizzleJobRunService implements IJobRunService {\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n ) {}\n\n /**\n * JOB-8 — produce the tenant WHERE fragment (or `null` to opt out).\n * Returns `null` when multi-tenancy is off (caller skips the predicate).\n * Throws `MissingTenantIdError` when on + `undefined`.\n * When on + explicit `null`, filters `tenant_id IS NULL`.\n */\n private tenantCondition(\n method: string,\n tenantId: string | null | undefined,\n ) {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return tenantId === null\n ? isNull(jobRuns.tenantId)\n : eq(jobRuns.tenantId, tenantId);\n }\n\n async listForScope(\n entityType: string,\n entityId: string,\n opts: ListForScopeOptions = {},\n ): Promise<JobRun[]> {\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n ];\n const tenantCond = this.tenantCondition('listForScope', opts.tenantId);\n if (tenantCond) conditions.push(tenantCond);\n if (opts.status) {\n if (Array.isArray(opts.status)) {\n conditions.push(inArray(jobRuns.status, opts.status));\n } else {\n conditions.push(eq(jobRuns.status, opts.status));\n }\n }\n if (opts.jobType) {\n conditions.push(eq(jobRuns.jobType, opts.jobType));\n }\n\n const orderCol = (() => {\n switch (opts.orderBy) {\n case 'created_at asc':\n return asc(jobRuns.createdAt);\n case 'run_at desc':\n return desc(jobRuns.runAt);\n case 'run_at asc':\n return asc(jobRuns.runAt);\n case 'created_at desc':\n default:\n return desc(jobRuns.createdAt);\n }\n })();\n\n let q = this.db\n .select()\n .from(jobRuns)\n .where(and(...conditions))\n .orderBy(orderCol)\n .$dynamic();\n\n if (typeof opts.limit === 'number') {\n q = q.limit(opts.limit);\n }\n if (typeof opts.offset === 'number') {\n q = q.offset(opts.offset);\n }\n\n const rows = await q;\n return rows as JobRun[];\n }\n\n async cancelForScope(\n entityType: string,\n entityId: string,\n opts: CancelForScopeOptions = {},\n ): Promise<void> {\n const tenantCond = this.tenantCondition('cancelForScope', opts.tenantId);\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n inArray(jobRuns.status, NON_TERMINAL_STATUSES),\n ];\n if (tenantCond) conditions.push(tenantCond);\n\n const rows = await this.db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(and(...conditions));\n\n for (const { id } of rows) {\n // Propagate the tenant gate into cascade-cancel. The scope query has\n // already narrowed to this tenant; passing `tenantId` through keeps\n // the orchestrator's per-row guard consistent under multi-tenant mode.\n await this.orchestrator.cancel(id, {\n cascade: true,\n tenantId: opts.tenantId,\n });\n }\n }\n\n async rescheduleForScope(\n entityType: string,\n entityId: string,\n newRunAt: Date,\n opts: RescheduleForScopeOptions = {},\n ): Promise<void> {\n const tenantCond = this.tenantCondition('rescheduleForScope', opts.tenantId);\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n eq(jobRuns.status, 'pending'),\n ];\n if (tenantCond) conditions.push(tenantCond);\n\n await this.db\n .update(jobRuns)\n .set({ runAt: newRunAt, updatedAt: new Date() })\n .where(and(...conditions));\n }\n\n async countByPoolAndStatus(\n tenantId?: string | null,\n ): Promise<PoolStatusCount[]> {\n const tenantCond = this.tenantCondition('countByPoolAndStatus', tenantId);\n const rows = await this.db\n .select({\n pool: jobRuns.pool,\n status: jobRuns.status,\n count: sql<number>`count(*)::int`.as('count'),\n })\n .from(jobRuns)\n .where(tenantCond ?? undefined)\n .groupBy(jobRuns.pool, jobRuns.status);\n\n return rows.map((r) => ({\n pool: r.pool,\n status: r.status,\n count: Number(r.count),\n }));\n }\n\n async listRecentFailed(\n limit: number,\n tenantId?: string | null,\n ): Promise<JobRunFailure[]> {\n const conditions = [eq(jobRuns.status, 'failed' as const)];\n const tenantCond = this.tenantCondition('listRecentFailed', tenantId);\n if (tenantCond) conditions.push(tenantCond);\n\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(and(...conditions))\n .orderBy(desc(jobRuns.finishedAt), desc(jobRuns.updatedAt))\n .limit(limit);\n\n return rows.map((r) => ({\n runId: r.id,\n jobType: r.jobType,\n pool: r.pool,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n failedAt: r.finishedAt ?? r.updatedAt,\n createdAt: r.createdAt,\n }));\n }\n\n async listJobRuns(query: ListJobRunsQuery = {}): Promise<JobRunPage> {\n const limit = clampLimit(query.limit);\n const conditions = [];\n\n const tenantCond = this.tenantCondition('listJobRuns', query.tenantId);\n if (tenantCond) conditions.push(tenantCond);\n if (query.poolId) conditions.push(eq(jobRuns.pool, query.poolId));\n if (query.rootRunId) conditions.push(eq(jobRuns.rootRunId, query.rootRunId));\n if (query.status) conditions.push(eq(jobRuns.status, query.status));\n if (query.since) conditions.push(gte(jobRuns.createdAt, query.since));\n\n // Keyset seek: WHERE (created_at, id) < (cursorCreatedAt, cursorId),\n // expanded into a SARGable OR so the same `created_at` index is used.\n if (query.cursor) {\n const keyset = decodeKeysetCursor(query.cursor);\n if (keyset) {\n conditions.push(\n or(\n lt(jobRuns.createdAt, keyset.createdAt),\n and(\n eq(jobRuns.createdAt, keyset.createdAt),\n lt(jobRuns.id, keyset.id),\n ),\n )!,\n );\n }\n }\n\n // Fetch one extra row to determine whether a next page exists without a\n // separate COUNT.\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(conditions.length > 0 ? and(...conditions) : undefined)\n .orderBy(desc(jobRuns.createdAt), desc(jobRuns.id))\n .limit(limit + 1);\n\n const hasMore = rows.length > limit;\n const page = hasMore ? rows.slice(0, limit) : rows;\n const items = page.map(toJobRunSummary);\n const last = page[page.length - 1];\n const nextCursor =\n hasMore && last\n ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id })\n : null;\n\n return { items, nextCursor };\n }\n\n /**\n * Internal helper used by cascade paths (not on the public protocol).\n * Exposed as a public method on the concrete class so infrastructure\n * code (cascade tests, debug tools) can call it without a cast.\n */\n async findByRootRunId(rootRunId: string): Promise<JobRun[]> {\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.rootRunId, rootRunId));\n return rows as JobRun[];\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 * Keyset (seek) cursor codec for `IJobRunService.listJobRuns` (OBS-LIST-1).\n *\n * The list is ordered `created_at DESC, id DESC`. The cursor encodes the\n * `(createdAt, id)` of the last row on the previous page so the next page\n * can seek with `WHERE (created_at, id) < (cursorCreatedAt, cursorId)`\n * rather than an `OFFSET`. Keyset pagination stays O(log n) on deep pages\n * and is stable as new rows arrive at the head.\n *\n * The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape\n * is an implementation detail — never parse it outside this module.\n *\n * Also hosts `toJobRunSummary`, the single `JobRunRow → JobRunSummary`\n * projection shared by both backends so the narrow shape stays in sync.\n */\nimport type { JobRunRow } from './job-orchestration.schema';\nimport type { JobRunSummary } from './job-run-service.protocol';\n\nexport interface JobRunKeyset {\n /** `created_at` of the last row on the previous page. */\n createdAt: Date;\n /** `id` (UUID) tie-break of the last row on the previous page. */\n id: string;\n}\n\n/** Default page size when `limit` is omitted. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Hard upper bound on page size to keep a single read bounded. */\nexport const MAX_LIST_LIMIT = 200;\n\n/** Clamp a caller-supplied `limit` into `[1, MAX_LIST_LIMIT]`. */\nexport function clampLimit(limit: number | undefined): number {\n if (typeof limit !== 'number' || !Number.isFinite(limit)) {\n return DEFAULT_LIST_LIMIT;\n }\n const floored = Math.floor(limit);\n if (floored < 1) return 1;\n if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;\n return floored;\n}\n\nexport function encodeKeysetCursor(keyset: JobRunKeyset): string {\n const tuple = [keyset.createdAt.toISOString(), keyset.id];\n return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');\n}\n\n/**\n * Decode an opaque cursor back into its `(createdAt, id)` keyset. Returns\n * `null` for a malformed cursor so callers can treat garbage input as\n * \"start from the beginning\" rather than throwing on user-supplied data.\n */\nexport function decodeKeysetCursor(cursor: string): JobRunKeyset | null {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf8');\n const parsed = JSON.parse(json) as unknown;\n if (!Array.isArray(parsed) || parsed.length !== 2) return null;\n const [iso, id] = parsed;\n if (typeof iso !== 'string' || typeof id !== 'string') return null;\n const createdAt = new Date(iso);\n if (Number.isNaN(createdAt.getTime())) return null;\n return { createdAt, id };\n } catch {\n return null;\n }\n}\n\n/**\n * Project a raw `job_run` row into the narrow `JobRunSummary` shape exposed\n * by `listJobRuns`. `errorMessage` is pulled from the jsonb `error.message`.\n */\nexport function toJobRunSummary(r: JobRunRow): JobRunSummary {\n return {\n runId: r.id,\n rootRunId: r.rootRunId,\n parentRunId: r.parentRunId,\n triggerSource: r.triggerSource,\n triggerRef: r.triggerRef,\n jobType: r.jobType,\n pool: r.pool,\n status: r.status,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n runAt: r.runAt,\n startedAt: r.startedAt,\n finishedAt: r.finishedAt,\n createdAt: r.createdAt,\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 * 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"],"mappings":"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","names":["sql","sql"]}
@@ -1,4 +1,4 @@
1
- import { I as IJobOrchestrator, i as JobRun } from '../../../job-orchestrator.protocol-BwsBd37o.js';
1
+ import { I as IJobOrchestrator, i as JobRun } from '../../../job-orchestrator.protocol-CHOEqBDk.js';
2
2
  import { IJobRunService, ListForScopeOptions, CancelForScopeOptions, RescheduleForScopeOptions, PoolStatusCount, JobRunFailure, ListJobRunsQuery, JobRunPage } from './job-run-service.protocol.js';
3
3
  import { MemoryJobStore } from './memory-job-store.js';
4
4
  import '../events/event-bus.protocol.js';
@@ -8,6 +8,7 @@ import './job-orchestration.schema.js';
8
8
  import 'drizzle-orm/pg-core';
9
9
  import 'drizzle-orm';
10
10
  import '@nestjs/common';
11
+ import '../events/generated/types.js';
11
12
 
12
13
  declare class MemoryJobRunService implements IJobRunService {
13
14
  private readonly store;
@@ -47,6 +47,9 @@ function toJobRunSummary(r) {
47
47
  return {
48
48
  runId: r.id,
49
49
  rootRunId: r.rootRunId,
50
+ parentRunId: r.parentRunId,
51
+ triggerSource: r.triggerSource,
52
+ triggerRef: r.triggerRef,
50
53
  jobType: r.jobType,
51
54
  pool: r.pool,
52
55
  status: r.status,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../runtime/subsystems/jobs/job-run-service.memory-backend.ts","../../../../runtime/subsystems/jobs/job-run-keyset-cursor.ts","../../../../runtime/subsystems/jobs/jobs-domain.tokens.ts","../../../../runtime/subsystems/jobs/jobs-errors.ts"],"sourcesContent":["/**\n * MemoryJobRunService — scope-oriented queries and bulk ops over the\n * in-memory run store (ADR-022, JOB-4).\n *\n * Mirrors `DrizzleJobRunService` but scans `MemoryJobStore.runs.values()`.\n * Cancel delegates back to the orchestrator so cascade semantics stay in\n * one place.\n */\nimport { Inject, Injectable } from '@nestjs/common';\nimport type { JobRunRow } from './job-orchestration.schema';\nimport type { JobRun } from './job-orchestrator.protocol';\nimport type {\n IJobRunService,\n ListForScopeOptions,\n CancelForScopeOptions,\n RescheduleForScopeOptions,\n PoolStatusCount,\n JobRunFailure,\n ListJobRunsQuery,\n JobRunPage,\n} from './job-run-service.protocol';\nimport {\n clampLimit,\n decodeKeysetCursor,\n encodeKeysetCursor,\n toJobRunSummary,\n} from './job-run-keyset-cursor';\nimport type { IJobOrchestrator } from './job-orchestrator.protocol';\nimport { JOB_ORCHESTRATOR, JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport { MissingTenantIdError } from './jobs-errors';\nimport { MemoryJobStore } from './memory-job-store';\n\nconst NON_TERMINAL_STATUSES: JobRunRow['status'][] = [\n 'pending',\n 'running',\n 'waiting',\n];\n\n@Injectable()\nexport class MemoryJobRunService implements IJobRunService {\n constructor(\n private readonly store: MemoryJobStore,\n @Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n ) {}\n\n /**\n * JOB-8 — produce a per-row predicate for the tenant gate.\n * Returns `null` when multi-tenancy is off (caller doesn't check).\n * Throws when on + `undefined`; matches `tenant_id IS NULL` on explicit\n * `null` to support cross-tenant background work.\n */\n private tenantPredicate(\n method: string,\n tenantId: string | null | undefined,\n ): ((r: JobRunRow) => boolean) | null {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return (r) => r.tenantId === tenantId;\n }\n\n async listForScope(\n entityType: string,\n entityId: string,\n opts: ListForScopeOptions = {},\n ): Promise<JobRun[]> {\n const statusFilter = opts.status\n ? Array.isArray(opts.status)\n ? new Set(opts.status)\n : new Set([opts.status])\n : null;\n const tenantCheck = this.tenantPredicate('listForScope', opts.tenantId);\n\n const rows: JobRunRow[] = [];\n for (const r of this.store.runs.values()) {\n if (r.scopeEntityType !== entityType) continue;\n if (r.scopeEntityId !== entityId) continue;\n if (statusFilter && !statusFilter.has(r.status)) continue;\n if (opts.jobType && r.jobType !== opts.jobType) continue;\n if (tenantCheck && !tenantCheck(r)) continue;\n rows.push(r);\n }\n\n const orderBy = opts.orderBy ?? 'created_at desc';\n rows.sort((a, b) => compareBy(a, b, orderBy));\n\n const offset = opts.offset ?? 0;\n const limit = opts.limit;\n const sliced =\n typeof limit === 'number' ? rows.slice(offset, offset + limit) : rows.slice(offset);\n return sliced as JobRun[];\n }\n\n async cancelForScope(\n entityType: string,\n entityId: string,\n opts: CancelForScopeOptions = {},\n ): Promise<void> {\n const tenantCheck = this.tenantPredicate('cancelForScope', opts.tenantId);\n\n const ids: string[] = [];\n for (const r of this.store.runs.values()) {\n if (r.scopeEntityType !== entityType) continue;\n if (r.scopeEntityId !== entityId) continue;\n if (!NON_TERMINAL_STATUSES.includes(r.status)) continue;\n if (tenantCheck && !tenantCheck(r)) continue;\n ids.push(r.id);\n }\n for (const id of ids) {\n // Propagate the tenant gate through the orchestrator's cancel so the\n // internal per-row guard passes (no surprise MissingTenantIdError\n // once the scope query has already narrowed to this tenant).\n await this.orchestrator.cancel(id, {\n cascade: true,\n tenantId: opts.tenantId,\n });\n }\n }\n\n async rescheduleForScope(\n entityType: string,\n entityId: string,\n newRunAt: Date,\n opts: RescheduleForScopeOptions = {},\n ): Promise<void> {\n const tenantCheck = this.tenantPredicate('rescheduleForScope', opts.tenantId);\n for (const r of this.store.runs.values()) {\n if (r.scopeEntityType !== entityType) continue;\n if (r.scopeEntityId !== entityId) continue;\n if (r.status !== 'pending') continue;\n if (tenantCheck && !tenantCheck(r)) continue;\n this.store.runs.set(r.id, {\n ...r,\n runAt: newRunAt,\n updatedAt: new Date(),\n });\n }\n }\n\n async countByPoolAndStatus(\n tenantId?: string | null,\n ): Promise<PoolStatusCount[]> {\n const tenantCheck = this.tenantPredicate('countByPoolAndStatus', tenantId);\n const map = new Map<string, PoolStatusCount>();\n for (const r of this.store.runs.values()) {\n if (tenantCheck && !tenantCheck(r)) continue;\n const key = `${r.pool}\\0${r.status}`;\n const cur = map.get(key);\n if (cur) {\n cur.count += 1;\n } else {\n map.set(key, { pool: r.pool, status: r.status, count: 1 });\n }\n }\n return Array.from(map.values());\n }\n\n async listRecentFailed(\n limit: number,\n tenantId?: string | null,\n ): Promise<JobRunFailure[]> {\n const tenantCheck = this.tenantPredicate('listRecentFailed', tenantId);\n const failed: JobRunRow[] = [];\n for (const r of this.store.runs.values()) {\n if (r.status !== 'failed') continue;\n if (tenantCheck && !tenantCheck(r)) continue;\n failed.push(r);\n }\n failed.sort((a, b) => {\n const at = (a.finishedAt ?? a.updatedAt).getTime();\n const bt = (b.finishedAt ?? b.updatedAt).getTime();\n return bt - at;\n });\n return failed.slice(0, limit).map((r) => ({\n runId: r.id,\n jobType: r.jobType,\n pool: r.pool,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n failedAt: r.finishedAt ?? r.updatedAt,\n createdAt: r.createdAt,\n }));\n }\n\n async listJobRuns(query: ListJobRunsQuery = {}): Promise<JobRunPage> {\n const limit = clampLimit(query.limit);\n const tenantCheck = this.tenantPredicate('listJobRuns', query.tenantId);\n const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;\n\n const matched: JobRunRow[] = [];\n for (const r of this.store.runs.values()) {\n if (tenantCheck && !tenantCheck(r)) continue;\n if (query.poolId && r.pool !== query.poolId) continue;\n if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;\n if (query.status && r.status !== query.status) continue;\n if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;\n matched.push(r);\n }\n\n // Order created_at DESC, id DESC to match the Drizzle backend's keyset.\n matched.sort((a, b) => {\n const dt = b.createdAt.getTime() - a.createdAt.getTime();\n if (dt !== 0) return dt;\n return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;\n });\n\n // Keyset seek: drop everything at/after the cursor's (created_at, id).\n const seeked = keyset\n ? matched.filter((r) => {\n const ct = r.createdAt.getTime();\n const kt = keyset.createdAt.getTime();\n if (ct < kt) return true;\n if (ct > kt) return false;\n return r.id < keyset.id;\n })\n : matched;\n\n const hasMore = seeked.length > limit;\n const page = hasMore ? seeked.slice(0, limit) : seeked;\n const items = page.map(toJobRunSummary);\n const last = page[page.length - 1];\n const nextCursor =\n hasMore && last\n ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id })\n : null;\n\n return { items, nextCursor };\n }\n\n /**\n * Direct lookup. Not on the protocol — concrete-class convenience for\n * tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both\n * are debug / test helpers that sidestep the orchestrator.\n */\n findById(runId: string): JobRun | null {\n return (this.store.runs.get(runId) ?? null) as JobRun | null;\n }\n\n /** Public counterpart to the Drizzle backend's `findByRootRunId` helper. */\n findByRootRunId(rootRunId: string): JobRun[] {\n const out: JobRunRow[] = [];\n for (const r of this.store.runs.values()) {\n if (r.rootRunId === rootRunId) out.push(r);\n }\n return out as JobRun[];\n }\n}\n\nfunction compareBy(\n a: JobRunRow,\n b: JobRunRow,\n order: Exclude<ListForScopeOptions['orderBy'], undefined>,\n): number {\n switch (order) {\n case 'created_at asc':\n return a.createdAt.getTime() - b.createdAt.getTime();\n case 'run_at desc':\n return b.runAt.getTime() - a.runAt.getTime();\n case 'run_at asc':\n return a.runAt.getTime() - b.runAt.getTime();\n case 'created_at desc':\n default:\n return b.createdAt.getTime() - a.createdAt.getTime();\n }\n}\n","/**\n * Keyset (seek) cursor codec for `IJobRunService.listJobRuns` (OBS-LIST-1).\n *\n * The list is ordered `created_at DESC, id DESC`. The cursor encodes the\n * `(createdAt, id)` of the last row on the previous page so the next page\n * can seek with `WHERE (created_at, id) < (cursorCreatedAt, cursorId)`\n * rather than an `OFFSET`. Keyset pagination stays O(log n) on deep pages\n * and is stable as new rows arrive at the head.\n *\n * The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape\n * is an implementation detail — never parse it outside this module.\n *\n * Also hosts `toJobRunSummary`, the single `JobRunRow → JobRunSummary`\n * projection shared by both backends so the narrow shape stays in sync.\n */\nimport type { JobRunRow } from './job-orchestration.schema';\nimport type { JobRunSummary } from './job-run-service.protocol';\n\nexport interface JobRunKeyset {\n /** `created_at` of the last row on the previous page. */\n createdAt: Date;\n /** `id` (UUID) tie-break of the last row on the previous page. */\n id: string;\n}\n\n/** Default page size when `limit` is omitted. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Hard upper bound on page size to keep a single read bounded. */\nexport const MAX_LIST_LIMIT = 200;\n\n/** Clamp a caller-supplied `limit` into `[1, MAX_LIST_LIMIT]`. */\nexport function clampLimit(limit: number | undefined): number {\n if (typeof limit !== 'number' || !Number.isFinite(limit)) {\n return DEFAULT_LIST_LIMIT;\n }\n const floored = Math.floor(limit);\n if (floored < 1) return 1;\n if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;\n return floored;\n}\n\nexport function encodeKeysetCursor(keyset: JobRunKeyset): string {\n const tuple = [keyset.createdAt.toISOString(), keyset.id];\n return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');\n}\n\n/**\n * Decode an opaque cursor back into its `(createdAt, id)` keyset. Returns\n * `null` for a malformed cursor so callers can treat garbage input as\n * \"start from the beginning\" rather than throwing on user-supplied data.\n */\nexport function decodeKeysetCursor(cursor: string): JobRunKeyset | null {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf8');\n const parsed = JSON.parse(json) as unknown;\n if (!Array.isArray(parsed) || parsed.length !== 2) return null;\n const [iso, id] = parsed;\n if (typeof iso !== 'string' || typeof id !== 'string') return null;\n const createdAt = new Date(iso);\n if (Number.isNaN(createdAt.getTime())) return null;\n return { createdAt, id };\n } catch {\n return null;\n }\n}\n\n/**\n * Project a raw `job_run` row into the narrow `JobRunSummary` shape exposed\n * by `listJobRuns`. `errorMessage` is pulled from the jsonb `error.message`.\n */\nexport function toJobRunSummary(r: JobRunRow): JobRunSummary {\n return {\n runId: r.id,\n rootRunId: r.rootRunId,\n jobType: r.jobType,\n pool: r.pool,\n status: r.status,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n runAt: r.runAt,\n startedAt: r.startedAt,\n finishedAt: r.finishedAt,\n createdAt: r.createdAt,\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 * 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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../runtime/subsystems/jobs/job-run-service.memory-backend.ts","../../../../runtime/subsystems/jobs/job-run-keyset-cursor.ts","../../../../runtime/subsystems/jobs/jobs-domain.tokens.ts","../../../../runtime/subsystems/jobs/jobs-errors.ts"],"sourcesContent":["/**\n * MemoryJobRunService — scope-oriented queries and bulk ops over the\n * in-memory run store (ADR-022, JOB-4).\n *\n * Mirrors `DrizzleJobRunService` but scans `MemoryJobStore.runs.values()`.\n * Cancel delegates back to the orchestrator so cascade semantics stay in\n * one place.\n */\nimport { Inject, Injectable } from '@nestjs/common';\nimport type { JobRunRow } from './job-orchestration.schema';\nimport type { JobRun } from './job-orchestrator.protocol';\nimport type {\n IJobRunService,\n ListForScopeOptions,\n CancelForScopeOptions,\n RescheduleForScopeOptions,\n PoolStatusCount,\n JobRunFailure,\n ListJobRunsQuery,\n JobRunPage,\n} from './job-run-service.protocol';\nimport {\n clampLimit,\n decodeKeysetCursor,\n encodeKeysetCursor,\n toJobRunSummary,\n} from './job-run-keyset-cursor';\nimport type { IJobOrchestrator } from './job-orchestrator.protocol';\nimport { JOB_ORCHESTRATOR, JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport { MissingTenantIdError } from './jobs-errors';\nimport { MemoryJobStore } from './memory-job-store';\n\nconst NON_TERMINAL_STATUSES: JobRunRow['status'][] = [\n 'pending',\n 'running',\n 'waiting',\n];\n\n@Injectable()\nexport class MemoryJobRunService implements IJobRunService {\n constructor(\n private readonly store: MemoryJobStore,\n @Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n ) {}\n\n /**\n * JOB-8 — produce a per-row predicate for the tenant gate.\n * Returns `null` when multi-tenancy is off (caller doesn't check).\n * Throws when on + `undefined`; matches `tenant_id IS NULL` on explicit\n * `null` to support cross-tenant background work.\n */\n private tenantPredicate(\n method: string,\n tenantId: string | null | undefined,\n ): ((r: JobRunRow) => boolean) | null {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return (r) => r.tenantId === tenantId;\n }\n\n async listForScope(\n entityType: string,\n entityId: string,\n opts: ListForScopeOptions = {},\n ): Promise<JobRun[]> {\n const statusFilter = opts.status\n ? Array.isArray(opts.status)\n ? new Set(opts.status)\n : new Set([opts.status])\n : null;\n const tenantCheck = this.tenantPredicate('listForScope', opts.tenantId);\n\n const rows: JobRunRow[] = [];\n for (const r of this.store.runs.values()) {\n if (r.scopeEntityType !== entityType) continue;\n if (r.scopeEntityId !== entityId) continue;\n if (statusFilter && !statusFilter.has(r.status)) continue;\n if (opts.jobType && r.jobType !== opts.jobType) continue;\n if (tenantCheck && !tenantCheck(r)) continue;\n rows.push(r);\n }\n\n const orderBy = opts.orderBy ?? 'created_at desc';\n rows.sort((a, b) => compareBy(a, b, orderBy));\n\n const offset = opts.offset ?? 0;\n const limit = opts.limit;\n const sliced =\n typeof limit === 'number' ? rows.slice(offset, offset + limit) : rows.slice(offset);\n return sliced as JobRun[];\n }\n\n async cancelForScope(\n entityType: string,\n entityId: string,\n opts: CancelForScopeOptions = {},\n ): Promise<void> {\n const tenantCheck = this.tenantPredicate('cancelForScope', opts.tenantId);\n\n const ids: string[] = [];\n for (const r of this.store.runs.values()) {\n if (r.scopeEntityType !== entityType) continue;\n if (r.scopeEntityId !== entityId) continue;\n if (!NON_TERMINAL_STATUSES.includes(r.status)) continue;\n if (tenantCheck && !tenantCheck(r)) continue;\n ids.push(r.id);\n }\n for (const id of ids) {\n // Propagate the tenant gate through the orchestrator's cancel so the\n // internal per-row guard passes (no surprise MissingTenantIdError\n // once the scope query has already narrowed to this tenant).\n await this.orchestrator.cancel(id, {\n cascade: true,\n tenantId: opts.tenantId,\n });\n }\n }\n\n async rescheduleForScope(\n entityType: string,\n entityId: string,\n newRunAt: Date,\n opts: RescheduleForScopeOptions = {},\n ): Promise<void> {\n const tenantCheck = this.tenantPredicate('rescheduleForScope', opts.tenantId);\n for (const r of this.store.runs.values()) {\n if (r.scopeEntityType !== entityType) continue;\n if (r.scopeEntityId !== entityId) continue;\n if (r.status !== 'pending') continue;\n if (tenantCheck && !tenantCheck(r)) continue;\n this.store.runs.set(r.id, {\n ...r,\n runAt: newRunAt,\n updatedAt: new Date(),\n });\n }\n }\n\n async countByPoolAndStatus(\n tenantId?: string | null,\n ): Promise<PoolStatusCount[]> {\n const tenantCheck = this.tenantPredicate('countByPoolAndStatus', tenantId);\n const map = new Map<string, PoolStatusCount>();\n for (const r of this.store.runs.values()) {\n if (tenantCheck && !tenantCheck(r)) continue;\n const key = `${r.pool}\\0${r.status}`;\n const cur = map.get(key);\n if (cur) {\n cur.count += 1;\n } else {\n map.set(key, { pool: r.pool, status: r.status, count: 1 });\n }\n }\n return Array.from(map.values());\n }\n\n async listRecentFailed(\n limit: number,\n tenantId?: string | null,\n ): Promise<JobRunFailure[]> {\n const tenantCheck = this.tenantPredicate('listRecentFailed', tenantId);\n const failed: JobRunRow[] = [];\n for (const r of this.store.runs.values()) {\n if (r.status !== 'failed') continue;\n if (tenantCheck && !tenantCheck(r)) continue;\n failed.push(r);\n }\n failed.sort((a, b) => {\n const at = (a.finishedAt ?? a.updatedAt).getTime();\n const bt = (b.finishedAt ?? b.updatedAt).getTime();\n return bt - at;\n });\n return failed.slice(0, limit).map((r) => ({\n runId: r.id,\n jobType: r.jobType,\n pool: r.pool,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n failedAt: r.finishedAt ?? r.updatedAt,\n createdAt: r.createdAt,\n }));\n }\n\n async listJobRuns(query: ListJobRunsQuery = {}): Promise<JobRunPage> {\n const limit = clampLimit(query.limit);\n const tenantCheck = this.tenantPredicate('listJobRuns', query.tenantId);\n const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;\n\n const matched: JobRunRow[] = [];\n for (const r of this.store.runs.values()) {\n if (tenantCheck && !tenantCheck(r)) continue;\n if (query.poolId && r.pool !== query.poolId) continue;\n if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;\n if (query.status && r.status !== query.status) continue;\n if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;\n matched.push(r);\n }\n\n // Order created_at DESC, id DESC to match the Drizzle backend's keyset.\n matched.sort((a, b) => {\n const dt = b.createdAt.getTime() - a.createdAt.getTime();\n if (dt !== 0) return dt;\n return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;\n });\n\n // Keyset seek: drop everything at/after the cursor's (created_at, id).\n const seeked = keyset\n ? matched.filter((r) => {\n const ct = r.createdAt.getTime();\n const kt = keyset.createdAt.getTime();\n if (ct < kt) return true;\n if (ct > kt) return false;\n return r.id < keyset.id;\n })\n : matched;\n\n const hasMore = seeked.length > limit;\n const page = hasMore ? seeked.slice(0, limit) : seeked;\n const items = page.map(toJobRunSummary);\n const last = page[page.length - 1];\n const nextCursor =\n hasMore && last\n ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id })\n : null;\n\n return { items, nextCursor };\n }\n\n /**\n * Direct lookup. Not on the protocol — concrete-class convenience for\n * tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both\n * are debug / test helpers that sidestep the orchestrator.\n */\n findById(runId: string): JobRun | null {\n return (this.store.runs.get(runId) ?? null) as JobRun | null;\n }\n\n /** Public counterpart to the Drizzle backend's `findByRootRunId` helper. */\n findByRootRunId(rootRunId: string): JobRun[] {\n const out: JobRunRow[] = [];\n for (const r of this.store.runs.values()) {\n if (r.rootRunId === rootRunId) out.push(r);\n }\n return out as JobRun[];\n }\n}\n\nfunction compareBy(\n a: JobRunRow,\n b: JobRunRow,\n order: Exclude<ListForScopeOptions['orderBy'], undefined>,\n): number {\n switch (order) {\n case 'created_at asc':\n return a.createdAt.getTime() - b.createdAt.getTime();\n case 'run_at desc':\n return b.runAt.getTime() - a.runAt.getTime();\n case 'run_at asc':\n return a.runAt.getTime() - b.runAt.getTime();\n case 'created_at desc':\n default:\n return b.createdAt.getTime() - a.createdAt.getTime();\n }\n}\n","/**\n * Keyset (seek) cursor codec for `IJobRunService.listJobRuns` (OBS-LIST-1).\n *\n * The list is ordered `created_at DESC, id DESC`. The cursor encodes the\n * `(createdAt, id)` of the last row on the previous page so the next page\n * can seek with `WHERE (created_at, id) < (cursorCreatedAt, cursorId)`\n * rather than an `OFFSET`. Keyset pagination stays O(log n) on deep pages\n * and is stable as new rows arrive at the head.\n *\n * The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape\n * is an implementation detail — never parse it outside this module.\n *\n * Also hosts `toJobRunSummary`, the single `JobRunRow → JobRunSummary`\n * projection shared by both backends so the narrow shape stays in sync.\n */\nimport type { JobRunRow } from './job-orchestration.schema';\nimport type { JobRunSummary } from './job-run-service.protocol';\n\nexport interface JobRunKeyset {\n /** `created_at` of the last row on the previous page. */\n createdAt: Date;\n /** `id` (UUID) tie-break of the last row on the previous page. */\n id: string;\n}\n\n/** Default page size when `limit` is omitted. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Hard upper bound on page size to keep a single read bounded. */\nexport const MAX_LIST_LIMIT = 200;\n\n/** Clamp a caller-supplied `limit` into `[1, MAX_LIST_LIMIT]`. */\nexport function clampLimit(limit: number | undefined): number {\n if (typeof limit !== 'number' || !Number.isFinite(limit)) {\n return DEFAULT_LIST_LIMIT;\n }\n const floored = Math.floor(limit);\n if (floored < 1) return 1;\n if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;\n return floored;\n}\n\nexport function encodeKeysetCursor(keyset: JobRunKeyset): string {\n const tuple = [keyset.createdAt.toISOString(), keyset.id];\n return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');\n}\n\n/**\n * Decode an opaque cursor back into its `(createdAt, id)` keyset. Returns\n * `null` for a malformed cursor so callers can treat garbage input as\n * \"start from the beginning\" rather than throwing on user-supplied data.\n */\nexport function decodeKeysetCursor(cursor: string): JobRunKeyset | null {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf8');\n const parsed = JSON.parse(json) as unknown;\n if (!Array.isArray(parsed) || parsed.length !== 2) return null;\n const [iso, id] = parsed;\n if (typeof iso !== 'string' || typeof id !== 'string') return null;\n const createdAt = new Date(iso);\n if (Number.isNaN(createdAt.getTime())) return null;\n return { createdAt, id };\n } catch {\n return null;\n }\n}\n\n/**\n * Project a raw `job_run` row into the narrow `JobRunSummary` shape exposed\n * by `listJobRuns`. `errorMessage` is pulled from the jsonb `error.message`.\n */\nexport function toJobRunSummary(r: JobRunRow): JobRunSummary {\n return {\n runId: r.id,\n rootRunId: r.rootRunId,\n parentRunId: r.parentRunId,\n triggerSource: r.triggerSource,\n triggerRef: r.triggerRef,\n jobType: r.jobType,\n pool: r.pool,\n status: r.status,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n runAt: r.runAt,\n startedAt: r.startedAt,\n finishedAt: r.finishedAt,\n createdAt: r.createdAt,\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 * 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"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- import { i as JobRun } from '../../../job-orchestrator.protocol-BwsBd37o.js';
1
+ import { i as JobRun } from '../../../job-orchestrator.protocol-CHOEqBDk.js';
2
2
  import '../events/event-bus.protocol.js';
3
3
  import '../../types/drizzle.js';
4
4
  import 'drizzle-orm/node-postgres';
@@ -6,6 +6,7 @@ import './job-orchestration.schema.js';
6
6
  import 'drizzle-orm/pg-core';
7
7
  import 'drizzle-orm';
8
8
  import '@nestjs/common';
9
+ import '../events/generated/types.js';
9
10
 
10
11
  /**
11
12
  * IJobRunService — scope-oriented queries and bulk operations over
@@ -93,11 +94,19 @@ interface ListJobRunsQuery {
93
94
  /**
94
95
  * Summary row for the `job_run` list (OBS-LIST-1). A narrow projection over
95
96
  * `JobRun` carrying the columns a runs viewer renders. `rootRunId` is
96
- * included so the correlation timeline can stitch runs to events.
97
+ * included so the correlation timeline can stitch runs to events; the
98
+ * lineage fields (`parentRunId`, `triggerSource`, `triggerRef`) let a viewer
99
+ * render the run tree and explain why each run was started without a second
100
+ * fetch. `triggerSource` is non-null (the column is `NOT NULL`); the literal
101
+ * union mirrors `job_trigger_source` — `JobRun['triggerSource']` is optional
102
+ * and would admit `undefined`, which this projection never produces.
97
103
  */
98
104
  interface JobRunSummary {
99
105
  runId: string;
100
106
  rootRunId: string;
107
+ parentRunId: string | null;
108
+ triggerSource: 'manual' | 'schedule' | 'event' | 'parent';
109
+ triggerRef: string | null;
101
110
  jobType: string;
102
111
  pool: string;
103
112
  status: JobRun['status'];
@@ -1,7 +1,7 @@
1
1
  import { ModuleRef } from '@nestjs/core';
2
2
  import { ConnectionOptions } from 'bullmq';
3
3
  import { DrizzleClient } from '../../types/drizzle.js';
4
- import { I as IJobOrchestrator } from '../../../job-orchestrator.protocol-BwsBd37o.js';
4
+ import { I as IJobOrchestrator } from '../../../job-orchestrator.protocol-CHOEqBDk.js';
5
5
  import { IJobStepService } from './job-step-service.protocol.js';
6
6
  import 'drizzle-orm/node-postgres';
7
7
  import '../events/event-bus.protocol.js';
@@ -9,6 +9,7 @@ import './job-orchestration.schema.js';
9
9
  import 'drizzle-orm/pg-core';
10
10
  import 'drizzle-orm';
11
11
  import '@nestjs/common';
12
+ import '../events/generated/types.js';
12
13
 
13
14
  /**
14
15
  * Options for a single per-pool BullMQ worker.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../runtime/subsystems/jobs/job-worker.bullmq-backend.ts","../../../../runtime/subsystems/jobs/job-orchestration.schema.ts","../../../../runtime/subsystems/jobs/job-handler.base.ts"],"sourcesContent":["/**\n * BullMQJobWorker — BullMQ-backed claim/dispatch worker (BULLMQ-1).\n *\n * Replaces the Drizzle `JobWorker` polling loop with one BullMQ `Worker` per\n * active pool. BullMQ owns claim (its native atomic BRPOPLPUSH), concurrency\n * (`{ concurrency }`), and retry/backoff (job opts set by the orchestrator) —\n * so this class is thinner than the Drizzle poller: no claim query, no stale\n * sweeper, no backoff math.\n *\n * The processor still drives the domain through Postgres `job_run` (the\n * source of truth) and runs the user handler through the existing\n * `JobHandlerBase` contract (`ctx.input` / `ctx.step` / `ctx.spawnChild`),\n * identical to the Drizzle path — only the claim mechanism differs.\n *\n * BullMQ job (runId) → load job_run → mark running → resolve handler via\n * ModuleRef → run(ctx) → mark completed / let BullMQ retry on throw.\n *\n * On a thrown handler error we rethrow so BullMQ applies the job's `attempts`/\n * `backoff` policy; the final failure (attempts exhausted) is mirrored to\n * `job_run.status='failed'` in the `failed` event handler.\n */\nimport { Logger } from '@nestjs/common';\nimport type { ModuleRef } from '@nestjs/core';\n// `bullmq` is an OPTIONAL peer dependency — TYPE imports ONLY here. `Worker`,\n// `Job`, `ConnectionOptions` are erased at compile time and never resolve\n// `'bullmq'` at runtime. The `Worker` VALUE constructor is loaded lazily via\n// `await import('bullmq')` in `onModuleInit` (mirrors\n// `event-bus.redis-backend.ts:createRedisClient`). See BULLMQ-1 §Lazy import.\nimport type { Worker, Job, ConnectionOptions } from 'bullmq';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { jobRuns, type JobRunRow } from './job-orchestration.schema';\nimport type { IJobOrchestrator, JobRun } from './job-orchestrator.protocol';\nimport type { IJobStepService } from './job-step-service.protocol';\nimport {\n JOB_HANDLER_REGISTRY,\n type JobContext,\n type JobHandlerBase,\n type SpawnChildOptions,\n type StepOptions,\n} from './job-handler.base';\n\ninterface BullJobPayload {\n runId: string;\n type: string;\n input: unknown;\n}\n\nfunction serialiseError(err: unknown, attempt: number, retryable: boolean) {\n const e = err as { message?: string; stack?: string } | undefined;\n return {\n message: (e?.message ?? String(err)) as string,\n stack: e?.stack,\n retryable,\n attempt,\n };\n}\n\n/**\n * Options for a single per-pool BullMQ worker.\n */\nexport interface BullMQJobWorkerOptions {\n /** Logical pool name (matches `job_run.pool`). */\n pool: string;\n /** Fully-resolved BullMQ queue name to consume. */\n queueName: string;\n /** Max concurrent in-flight processors. */\n concurrency: number;\n /** ioredis-compatible connection. */\n connection: ConnectionOptions;\n}\n\nexport class BullMQJobWorker {\n private readonly logger = new Logger(BullMQJobWorker.name);\n private worker: Worker | null = null;\n\n constructor(\n private readonly db: DrizzleClient,\n private readonly orchestrator: IJobOrchestrator,\n private readonly stepService: IJobStepService,\n private readonly options: BullMQJobWorkerOptions,\n private readonly moduleRef: ModuleRef,\n ) {}\n\n async onModuleInit(): Promise<void> {\n let WorkerCtor: typeof import('bullmq').Worker;\n try {\n const mod = await import('bullmq');\n WorkerCtor = mod.Worker;\n } catch {\n throw new Error(\n 'BullMQ backend requires the \"bullmq\" package. Install it with: npm install bullmq',\n );\n }\n this.worker = new WorkerCtor(\n this.options.queueName,\n (job) => this.process(job as Job<BullJobPayload>),\n {\n connection: this.options.connection,\n concurrency: this.options.concurrency,\n },\n );\n this.worker.on('failed', (job, err) => {\n // BullMQ fires `failed` after EACH attempt; only mirror to job_run when\n // attempts are exhausted (BullMQ will not retry further).\n if (!job) return;\n const attemptsMade = job.attemptsMade;\n const maxAttempts = job.opts.attempts ?? 1;\n if (attemptsMade >= maxAttempts) {\n void this.markFailed(job.data.runId, err, attemptsMade);\n }\n });\n this.logger.log(\n `BullMQ worker started: pool='${this.options.pool}' queue='${this.options.queueName}' concurrency=${this.options.concurrency}`,\n );\n }\n\n async onModuleDestroy(): Promise<void> {\n if (this.worker) {\n await this.worker.close();\n this.worker = null;\n }\n }\n\n /**\n * Process one BullMQ job. Returns the handler output (stored by BullMQ as\n * the job return value AND written to `job_run.output`). Throws on handler\n * failure so BullMQ applies the retry policy.\n */\n private async process(job: Job<BullJobPayload>): Promise<unknown> {\n const { runId } = job.data;\n const [row] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!row) {\n // Domain row vanished (canceled + removed). Treat as a no-op success so\n // BullMQ doesn't retry a job whose authoritative state is gone.\n this.logger.warn(`process: job_run ${runId} not found; skipping`);\n return {};\n }\n const run = row as JobRunRow;\n\n // Canceled in Postgres after enqueue but before claim — honour the domain\n // decision and skip without running the handler.\n if (run.status === 'canceled') {\n return {};\n }\n\n const registryEntry = JOB_HANDLER_REGISTRY.get(run.jobType);\n if (!registryEntry) {\n throw new Error(\n `No handler registered for jobType='${run.jobType}' (run ${run.id})`,\n );\n }\n\n // Mark running (mirrors the Drizzle worker's claim transition).\n await this.db\n .update(jobRuns)\n .set({\n status: 'running',\n claimedAt: new Date(),\n startedAt: new Date(),\n attempts: job.attemptsMade + 1,\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, run.id));\n\n const HandlerClass = registryEntry.handlerClass;\n const handler = this.moduleRef.get(\n HandlerClass as unknown as new (...args: unknown[]) => unknown,\n { strict: false },\n ) as JobHandlerBase<unknown>;\n\n const ctx: JobContext<unknown> = {\n input: run.input,\n run: run as JobRun,\n step: this.makeStepFn(run),\n spawnChild: this.makeSpawnFn(run),\n logger: new Logger(`JobRun:${run.id}`),\n };\n\n const output = (await handler.run(ctx)) as\n | Record<string, unknown>\n | undefined;\n\n await this.db\n .update(jobRuns)\n .set({\n status: 'completed',\n output: (output ?? {}) as Record<string, unknown>,\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, run.id));\n\n return output ?? {};\n }\n\n private async markFailed(\n runId: string,\n err: unknown,\n finalAttempts: number,\n ): Promise<void> {\n const [row] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!row) return;\n const run = row as JobRunRow;\n await this.db\n .update(jobRuns)\n .set({\n status: 'failed',\n attempts: finalAttempts,\n finishedAt: new Date(),\n error: serialiseError(err, finalAttempts, false),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, runId));\n\n // Parent-close-policy cascade — identical semantics to the Drizzle worker.\n if (run.parentClosePolicy === 'terminate') {\n try {\n await this.orchestrator.cancel(run.id, {\n cascade: true,\n reason: 'parent-failed',\n tenantId: run.tenantId,\n });\n } catch (cascadeErr) {\n this.logger.warn(\n `cascade on failed run ${run.id}: ${(cascadeErr as Error).message}`,\n );\n }\n }\n }\n\n // ── ctx.step / ctx.spawnChild (mirror JobWorker) ──────────────────────────\n\n private makeStepFn(run: JobRunRow) {\n return async <TOutput>(\n stepId: string,\n fn: () => Promise<TOutput>,\n _opts?: StepOptions,\n ): Promise<TOutput> => {\n void _opts;\n const existing = await this.stepService.findStep(run.id, stepId);\n if (existing?.status === 'completed') {\n return existing.output as TOutput;\n }\n const nextAttempts = (existing?.attempts ?? 0) + 1;\n const seq = nextAttempts; // BullMQ path: seq is per-step attempt index\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'running',\n startedAt: new Date(),\n attempts: nextAttempts,\n });\n try {\n const output = await fn();\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'completed',\n output: output as Record<string, unknown> | undefined,\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n return output;\n } catch (err) {\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'failed',\n error: serialiseError(err, nextAttempts, false),\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n throw err;\n }\n };\n }\n\n private makeSpawnFn(run: JobRunRow) {\n return async (\n type: string,\n input: unknown,\n opts?: SpawnChildOptions,\n ): Promise<JobRun> => {\n return this.orchestrator.start(type, input, {\n parentRunId: run.id,\n parentClosePolicy: opts?.closePolicy,\n runAt: opts?.runAt,\n priority: opts?.priority,\n tags: opts?.tags,\n triggerSource: 'parent',\n triggerRef: run.id,\n tenantId: run.tenantId,\n });\n };\n }\n}\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 * Handler base class, JobContext, @JobHandler decorator, and policy types\n * for the job orchestration domain (ADR-022, JOB-2).\n *\n * User-authored jobs subclass `JobHandlerBase<TInput, TOutput>` and decorate\n * the class with `@JobHandler<TInput>('job_type', meta)`. The decorator\n * 1. stores metadata via `Reflect.defineMetadata` so Nest's reflector can\n * pick it up at module boot, and\n * 2. populates `JOB_HANDLER_REGISTRY` — a module-singleton map consumed by\n * `JobWorkerModule` (JOB-5) to materialise `job` rows and resolve\n * handler classes during claim/execute.\n *\n * No runtime orchestration lives here; this file is a pure type + decorator\n * surface so downstream PRs (JOB-3..JOB-5) can implement against a stable\n * shape.\n */\n// TODO(logging-subsystem): swap to ILogger once ADR-028 lands\nimport type { Logger } from '@nestjs/common';\nimport type { JobRun } from './job-orchestrator.protocol';\n\n// ─── ParentClosePolicy ──────────────────────────────────────────────────────\n\n/**\n * What happens to running child runs when a parent enters a terminal state.\n * Stored on the child at spawn; changes to the parent after spawn do NOT\n * retroactively rewrite children.\n */\nexport enum ParentClosePolicy {\n Terminate = 'terminate',\n Cancel = 'cancel',\n Abandon = 'abandon',\n}\n\n// ─── Policy types ───────────────────────────────────────────────────────────\n\nexport interface RetryPolicy {\n attempts: number;\n backoff: 'fixed' | 'exponential';\n baseMs: number;\n nonRetryableErrors?: string[];\n}\n\nexport interface ConcurrencyPolicy<TInput> {\n key: (input: TInput) => string;\n collisionMode: 'queue' | 'reject' | 'replace';\n}\n\nexport interface DedupePolicy<TInput> {\n key: (input: TInput) => string;\n windowMs: number;\n}\n\n/**\n * Declarative scope reference. `TScope` is parameterised so JOB-7 can narrow\n * `entity` to the generated `ScopeEntityType` union at the call site without\n * modifying this file (OQ-1 resolution, 2026-04-20).\n */\nexport interface ScopeRef<TInput, TScope extends string = string> {\n entity: TScope;\n from: (input: TInput) => string;\n}\n\nexport interface JobHandlerMeta<TInput> {\n pool?: string;\n scope?: ScopeRef<TInput>;\n retry?: RetryPolicy;\n concurrency?: ConcurrencyPolicy<TInput>;\n dedupe?: DedupePolicy<TInput>;\n timeoutMs?: number;\n replayFrom?: 'scratch' | 'last_step' | 'last_checkpoint';\n}\n\n// ─── Runtime option shapes ──────────────────────────────────────────────────\n\nexport interface StepOptions {\n retry?: RetryPolicy;\n timeoutMs?: number;\n}\n\nexport interface SpawnChildOptions {\n closePolicy?: ParentClosePolicy;\n runAt?: Date;\n priority?: number;\n tags?: Record<string, string>;\n}\n\n// ─── JobContext ─────────────────────────────────────────────────────────────\n\nexport interface JobContext<TInput> {\n readonly input: TInput;\n readonly run: JobRun;\n step<TOutput>(\n stepId: string,\n fn: () => Promise<TOutput>,\n opts?: StepOptions,\n ): Promise<TOutput>;\n spawnChild(type: string, input: unknown, opts?: SpawnChildOptions): Promise<JobRun>;\n readonly logger: Logger;\n // NOT in Phase 1 — deferred to ADR-025:\n // waitFor(kind, token, opts)\n // signal(token, payload)\n // sleep(ms)\n}\n\n// ─── JobHandlerBase ─────────────────────────────────────────────────────────\n\nexport abstract class JobHandlerBase<TInput, TOutput = unknown> {\n abstract run(ctx: JobContext<TInput>): Promise<TOutput>;\n}\n\n// ─── Registry + decorator ───────────────────────────────────────────────────\n\n/**\n * Module-singleton map keyed by job type. Populated by the `@JobHandler`\n * decorator at class definition time; consumed by `JobWorkerModule` (JOB-5)\n * to upsert `job` rows and resolve handler classes during claim/execute.\n */\nexport const JOB_HANDLER_REGISTRY = new Map<\n string,\n {\n type: string;\n meta: JobHandlerMeta<unknown>;\n handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;\n }\n>();\n\nexport const JOB_HANDLER_METADATA_KEY = Symbol('JobHandlerMeta');\n\n/**\n * Class decorator that registers a handler with the job type, the full\n * metadata shape, and the target class constructor.\n *\n * Duplicate-type behaviour (OQ-3, resolved 2026-04-18):\n * - `NODE_ENV === 'production'` → throw; silent overwrite in prod is a\n * correctness bug.\n * - `NODE_ENV === 'test'` → silent overwrite (tests intentionally\n * re-register handlers).\n * - otherwise (dev) → `console.warn` + overwrite. `console`\n * is used intentionally instead of the Nest `Logger` — decorators run\n * at module-load time before any Nest container exists.\n */\nexport function JobHandler<TInput>(\n type: string,\n meta: JobHandlerMeta<TInput>,\n): ClassDecorator {\n return (target) => {\n if (JOB_HANDLER_REGISTRY.has(type)) {\n const env = process.env.NODE_ENV;\n if (env === 'production') {\n throw new Error(\n `[JobHandler] Duplicate registration for job type '${type}'. ` +\n `Each @JobHandler must declare a unique type.`,\n );\n }\n if (env !== 'test') {\n // eslint-disable-next-line no-console\n console.warn(\n `[JobHandler] Duplicate registration for job type '${type}'. ` +\n `Overwriting previous handler — this is almost certainly a bug.`,\n );\n }\n }\n\n Reflect.defineMetadata(JOB_HANDLER_METADATA_KEY, { type, meta }, target);\n JOB_HANDLER_REGISTRY.set(type, {\n type,\n meta: meta as JobHandlerMeta<unknown>,\n handlerClass: target as unknown as new (\n ...args: unknown[]\n ) => JobHandlerBase<unknown>,\n });\n };\n}\n\n// ─── HandlerRegistry — read helpers consumed by JobWorkerModule (JOB-5) ─────\n\n/**\n * Single entry shape returned by `HandlerRegistry.getAll()` / `.get()` and\n * exposed to `JobWorkerModule.onModuleInit` for boot-time upserts.\n *\n * Structurally compatible with `IJobOrchestrator.upsertJobRows`'s\n * `JobUpsertEntry` so the worker module can pass entries through verbatim\n * without re-mapping.\n */\nexport interface HandlerRegistryEntry {\n type: string;\n meta: JobHandlerMeta<unknown>;\n handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;\n}\n\n/**\n * Read facade over `JOB_HANDLER_REGISTRY`. The decorator's write path is\n * unchanged; this namespace exists so consumers (the worker module, tests)\n * don't import the raw `Map` and accidentally mutate it.\n */\nexport namespace HandlerRegistry {\n /** All registered entries in insertion order. */\n export function getAll(): HandlerRegistryEntry[] {\n return Array.from(JOB_HANDLER_REGISTRY.values());\n }\n\n /** Lookup by job type, or `undefined` if no `@JobHandler` is registered. */\n export function get(type: string): HandlerRegistryEntry | undefined {\n return JOB_HANDLER_REGISTRY.get(type);\n }\n}\n"],"mappings":"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","names":["HandlerRegistry"]}
1
+ {"version":3,"sources":["../../../../runtime/subsystems/jobs/job-worker.bullmq-backend.ts","../../../../runtime/subsystems/jobs/job-orchestration.schema.ts","../../../../runtime/subsystems/jobs/job-handler.base.ts"],"sourcesContent":["/**\n * BullMQJobWorker — BullMQ-backed claim/dispatch worker (BULLMQ-1).\n *\n * Replaces the Drizzle `JobWorker` polling loop with one BullMQ `Worker` per\n * active pool. BullMQ owns claim (its native atomic BRPOPLPUSH), concurrency\n * (`{ concurrency }`), and retry/backoff (job opts set by the orchestrator) —\n * so this class is thinner than the Drizzle poller: no claim query, no stale\n * sweeper, no backoff math.\n *\n * The processor still drives the domain through Postgres `job_run` (the\n * source of truth) and runs the user handler through the existing\n * `JobHandlerBase` contract (`ctx.input` / `ctx.step` / `ctx.spawnChild`),\n * identical to the Drizzle path — only the claim mechanism differs.\n *\n * BullMQ job (runId) → load job_run → mark running → resolve handler via\n * ModuleRef → run(ctx) → mark completed / let BullMQ retry on throw.\n *\n * On a thrown handler error we rethrow so BullMQ applies the job's `attempts`/\n * `backoff` policy; the final failure (attempts exhausted) is mirrored to\n * `job_run.status='failed'` in the `failed` event handler.\n */\nimport { Logger } from '@nestjs/common';\nimport type { ModuleRef } from '@nestjs/core';\n// `bullmq` is an OPTIONAL peer dependency — TYPE imports ONLY here. `Worker`,\n// `Job`, `ConnectionOptions` are erased at compile time and never resolve\n// `'bullmq'` at runtime. The `Worker` VALUE constructor is loaded lazily via\n// `await import('bullmq')` in `onModuleInit` (mirrors\n// `event-bus.redis-backend.ts:createRedisClient`). See BULLMQ-1 §Lazy import.\nimport type { Worker, Job, ConnectionOptions } from 'bullmq';\nimport { eq } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { jobRuns, type JobRunRow } from './job-orchestration.schema';\nimport type { IJobOrchestrator, JobRun } from './job-orchestrator.protocol';\nimport type { IJobStepService } from './job-step-service.protocol';\nimport {\n JOB_HANDLER_REGISTRY,\n type JobContext,\n type JobHandlerBase,\n type SpawnChildOptions,\n type StepOptions,\n} from './job-handler.base';\n\ninterface BullJobPayload {\n runId: string;\n type: string;\n input: unknown;\n}\n\nfunction serialiseError(err: unknown, attempt: number, retryable: boolean) {\n const e = err as { message?: string; stack?: string } | undefined;\n return {\n message: (e?.message ?? String(err)) as string,\n stack: e?.stack,\n retryable,\n attempt,\n };\n}\n\n/**\n * Options for a single per-pool BullMQ worker.\n */\nexport interface BullMQJobWorkerOptions {\n /** Logical pool name (matches `job_run.pool`). */\n pool: string;\n /** Fully-resolved BullMQ queue name to consume. */\n queueName: string;\n /** Max concurrent in-flight processors. */\n concurrency: number;\n /** ioredis-compatible connection. */\n connection: ConnectionOptions;\n}\n\nexport class BullMQJobWorker {\n private readonly logger = new Logger(BullMQJobWorker.name);\n private worker: Worker | null = null;\n\n constructor(\n private readonly db: DrizzleClient,\n private readonly orchestrator: IJobOrchestrator,\n private readonly stepService: IJobStepService,\n private readonly options: BullMQJobWorkerOptions,\n private readonly moduleRef: ModuleRef,\n ) {}\n\n async onModuleInit(): Promise<void> {\n let WorkerCtor: typeof import('bullmq').Worker;\n try {\n const mod = await import('bullmq');\n WorkerCtor = mod.Worker;\n } catch {\n throw new Error(\n 'BullMQ backend requires the \"bullmq\" package. Install it with: npm install bullmq',\n );\n }\n this.worker = new WorkerCtor(\n this.options.queueName,\n (job) => this.process(job as Job<BullJobPayload>),\n {\n connection: this.options.connection,\n concurrency: this.options.concurrency,\n },\n );\n this.worker.on('failed', (job, err) => {\n // BullMQ fires `failed` after EACH attempt; only mirror to job_run when\n // attempts are exhausted (BullMQ will not retry further).\n if (!job) return;\n const attemptsMade = job.attemptsMade;\n const maxAttempts = job.opts.attempts ?? 1;\n if (attemptsMade >= maxAttempts) {\n void this.markFailed(job.data.runId, err, attemptsMade);\n }\n });\n this.logger.log(\n `BullMQ worker started: pool='${this.options.pool}' queue='${this.options.queueName}' concurrency=${this.options.concurrency}`,\n );\n }\n\n async onModuleDestroy(): Promise<void> {\n if (this.worker) {\n await this.worker.close();\n this.worker = null;\n }\n }\n\n /**\n * Process one BullMQ job. Returns the handler output (stored by BullMQ as\n * the job return value AND written to `job_run.output`). Throws on handler\n * failure so BullMQ applies the retry policy.\n */\n private async process(job: Job<BullJobPayload>): Promise<unknown> {\n const { runId } = job.data;\n const [row] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!row) {\n // Domain row vanished (canceled + removed). Treat as a no-op success so\n // BullMQ doesn't retry a job whose authoritative state is gone.\n this.logger.warn(`process: job_run ${runId} not found; skipping`);\n return {};\n }\n const run = row as JobRunRow;\n\n // Canceled in Postgres after enqueue but before claim — honour the domain\n // decision and skip without running the handler.\n if (run.status === 'canceled') {\n return {};\n }\n\n const registryEntry = JOB_HANDLER_REGISTRY.get(run.jobType);\n if (!registryEntry) {\n throw new Error(\n `No handler registered for jobType='${run.jobType}' (run ${run.id})`,\n );\n }\n\n // Mark running (mirrors the Drizzle worker's claim transition).\n await this.db\n .update(jobRuns)\n .set({\n status: 'running',\n claimedAt: new Date(),\n startedAt: new Date(),\n attempts: job.attemptsMade + 1,\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, run.id));\n\n const HandlerClass = registryEntry.handlerClass;\n const handler = this.moduleRef.get(\n HandlerClass as unknown as new (...args: unknown[]) => unknown,\n { strict: false },\n ) as JobHandlerBase<unknown>;\n\n const ctx: JobContext<unknown> = {\n input: run.input,\n run: run as JobRun,\n step: this.makeStepFn(run),\n spawnChild: this.makeSpawnFn(run),\n logger: new Logger(`JobRun:${run.id}`),\n };\n\n const output = (await handler.run(ctx)) as\n | Record<string, unknown>\n | undefined;\n\n await this.db\n .update(jobRuns)\n .set({\n status: 'completed',\n output: (output ?? {}) as Record<string, unknown>,\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, run.id));\n\n return output ?? {};\n }\n\n private async markFailed(\n runId: string,\n err: unknown,\n finalAttempts: number,\n ): Promise<void> {\n const [row] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!row) return;\n const run = row as JobRunRow;\n await this.db\n .update(jobRuns)\n .set({\n status: 'failed',\n attempts: finalAttempts,\n finishedAt: new Date(),\n error: serialiseError(err, finalAttempts, false),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, runId));\n\n // Parent-close-policy cascade — identical semantics to the Drizzle worker.\n if (run.parentClosePolicy === 'terminate') {\n try {\n await this.orchestrator.cancel(run.id, {\n cascade: true,\n reason: 'parent-failed',\n tenantId: run.tenantId,\n });\n } catch (cascadeErr) {\n this.logger.warn(\n `cascade on failed run ${run.id}: ${(cascadeErr as Error).message}`,\n );\n }\n }\n }\n\n // ── ctx.step / ctx.spawnChild (mirror JobWorker) ──────────────────────────\n\n private makeStepFn(run: JobRunRow) {\n return async <TOutput>(\n stepId: string,\n fn: () => Promise<TOutput>,\n _opts?: StepOptions,\n ): Promise<TOutput> => {\n void _opts;\n const existing = await this.stepService.findStep(run.id, stepId);\n if (existing?.status === 'completed') {\n return existing.output as TOutput;\n }\n const nextAttempts = (existing?.attempts ?? 0) + 1;\n const seq = nextAttempts; // BullMQ path: seq is per-step attempt index\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'running',\n startedAt: new Date(),\n attempts: nextAttempts,\n });\n try {\n const output = await fn();\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'completed',\n output: output as Record<string, unknown> | undefined,\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n return output;\n } catch (err) {\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'failed',\n error: serialiseError(err, nextAttempts, false),\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n throw err;\n }\n };\n }\n\n private makeSpawnFn(run: JobRunRow) {\n return async (\n type: string,\n input: unknown,\n opts?: SpawnChildOptions,\n ): Promise<JobRun> => {\n return this.orchestrator.start(type, input, {\n parentRunId: run.id,\n parentClosePolicy: opts?.closePolicy,\n runAt: opts?.runAt,\n priority: opts?.priority,\n tags: opts?.tags,\n triggerSource: 'parent',\n triggerRef: run.id,\n tenantId: run.tenantId,\n });\n };\n }\n}\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 * Handler base class, JobContext, @JobHandler decorator, and policy types\n * for the job orchestration domain (ADR-022, JOB-2).\n *\n * User-authored jobs subclass `JobHandlerBase<TInput, TOutput>` and decorate\n * the class with `@JobHandler<TInput>('job_type', meta)`. The decorator\n * 1. stores metadata via `Reflect.defineMetadata` so Nest's reflector can\n * pick it up at module boot, and\n * 2. populates `JOB_HANDLER_REGISTRY` — a module-singleton map consumed by\n * `JobWorkerModule` (JOB-5) to materialise `job` rows and resolve\n * handler classes during claim/execute.\n *\n * No runtime orchestration lives here; this file is a pure type + decorator\n * surface so downstream PRs (JOB-3..JOB-5) can implement against a stable\n * shape.\n */\n// TODO(logging-subsystem): swap to ILogger once ADR-028 lands\nimport type { Logger } from '@nestjs/common';\nimport type { EventOfType, EventTypeName } from '../events/generated/types';\nimport type { JobRun } from './job-orchestrator.protocol';\n\n// ─── ParentClosePolicy ──────────────────────────────────────────────────────\n\n/**\n * What happens to running child runs when a parent enters a terminal state.\n * Stored on the child at spawn; changes to the parent after spawn do NOT\n * retroactively rewrite children.\n */\nexport enum ParentClosePolicy {\n Terminate = 'terminate',\n Cancel = 'cancel',\n Abandon = 'abandon',\n}\n\n// ─── Policy types ───────────────────────────────────────────────────────────\n\nexport interface RetryPolicy {\n attempts: number;\n backoff: 'fixed' | 'exponential';\n baseMs: number;\n nonRetryableErrors?: string[];\n}\n\nexport interface ConcurrencyPolicy<TInput> {\n key: (input: TInput) => string;\n collisionMode: 'queue' | 'reject' | 'replace';\n}\n\nexport interface DedupePolicy<TInput> {\n key: (input: TInput) => string;\n windowMs: number;\n}\n\n/**\n * Declarative scope reference. `TScope` is parameterised so JOB-7 can narrow\n * `entity` to the generated `ScopeEntityType` union at the call site without\n * modifying this file (OQ-1 resolution, 2026-04-20).\n */\nexport interface ScopeRef<TInput, TScope extends string = string> {\n entity: TScope;\n from: (input: TInput) => string;\n}\n\n/**\n * Bridge trigger authoring shape (BRIDGE-6 follow-up — BRIDGE-6 shipped the\n * generator + runtime for `@JobHandler({ triggers })` but never added the\n * authoring field to this type; the generator's tests scan source as strings,\n * so a real decorator was never compiled and the gap went uncaught).\n *\n * Declared on `@JobHandler({ triggers })`; the codegen bridge-registry\n * generator (`src/cli/shared/bridge-registry-generator.ts`) scans these from\n * source and emits `bridge/generated/registry.ts`, validating each `event`\n * against the generated `eventRegistry` at `gen-all`. The distributed union\n * narrows `map`/`when` per `event`, so callbacks are typed against the event\n * payload (ADR-023, \"typed against PayloadOfType<T>\").\n *\n * Typed against events' generated types — the same `import type` coupling the\n * bridge already has (erased at runtime). `jobs` must NOT import `bridge`, so\n * the post-gen `BridgeTriggerEntry` is deliberately not referenced here;\n * `triggerId`/`jobType` are computed by the generator, not authored.\n */\nexport type JobTrigger<TInput> = {\n [T in EventTypeName]: {\n /** Event type that fires this trigger. Validated against `eventRegistry`. */\n event: T;\n /** Maps the event to the job input. Inlined verbatim into the registry. */\n map: (event: EventOfType<T>) => TInput;\n /** Optional guard; `false` → wrapper records `status='skipped'`. */\n when?: (event: EventOfType<T>) => boolean;\n };\n}[EventTypeName];\n\nexport interface JobHandlerMeta<TInput> {\n pool?: string;\n scope?: ScopeRef<TInput>;\n retry?: RetryPolicy;\n concurrency?: ConcurrencyPolicy<TInput>;\n dedupe?: DedupePolicy<TInput>;\n timeoutMs?: number;\n replayFrom?: 'scratch' | 'last_step' | 'last_checkpoint';\n /**\n * Bridge triggers (ADR-023 Tier 3). Codegen scans these into `bridgeRegistry`;\n * the framework `BridgeDeliveryHandler` starts this job per matched event.\n * Absent for jobs started directly or via `IEventFlow.publishAndStart`.\n */\n triggers?: readonly JobTrigger<TInput>[];\n}\n\n// ─── Runtime option shapes ──────────────────────────────────────────────────\n\nexport interface StepOptions {\n retry?: RetryPolicy;\n timeoutMs?: number;\n}\n\nexport interface SpawnChildOptions {\n closePolicy?: ParentClosePolicy;\n runAt?: Date;\n priority?: number;\n tags?: Record<string, string>;\n}\n\n// ─── JobContext ─────────────────────────────────────────────────────────────\n\nexport interface JobContext<TInput> {\n readonly input: TInput;\n readonly run: JobRun;\n step<TOutput>(\n stepId: string,\n fn: () => Promise<TOutput>,\n opts?: StepOptions,\n ): Promise<TOutput>;\n spawnChild(type: string, input: unknown, opts?: SpawnChildOptions): Promise<JobRun>;\n readonly logger: Logger;\n // NOT in Phase 1 — deferred to ADR-025:\n // waitFor(kind, token, opts)\n // signal(token, payload)\n // sleep(ms)\n}\n\n// ─── JobHandlerBase ─────────────────────────────────────────────────────────\n\nexport abstract class JobHandlerBase<TInput, TOutput = unknown> {\n abstract run(ctx: JobContext<TInput>): Promise<TOutput>;\n}\n\n// ─── Registry + decorator ───────────────────────────────────────────────────\n\n/**\n * Module-singleton map keyed by job type. Populated by the `@JobHandler`\n * decorator at class definition time; consumed by `JobWorkerModule` (JOB-5)\n * to upsert `job` rows and resolve handler classes during claim/execute.\n */\nexport const JOB_HANDLER_REGISTRY = new Map<\n string,\n {\n type: string;\n meta: JobHandlerMeta<unknown>;\n handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;\n }\n>();\n\nexport const JOB_HANDLER_METADATA_KEY = Symbol('JobHandlerMeta');\n\n/**\n * Class decorator that registers a handler with the job type, the full\n * metadata shape, and the target class constructor.\n *\n * Duplicate-type behaviour (OQ-3, resolved 2026-04-18):\n * - `NODE_ENV === 'production'` → throw; silent overwrite in prod is a\n * correctness bug.\n * - `NODE_ENV === 'test'` → silent overwrite (tests intentionally\n * re-register handlers).\n * - otherwise (dev) → `console.warn` + overwrite. `console`\n * is used intentionally instead of the Nest `Logger` — decorators run\n * at module-load time before any Nest container exists.\n */\nexport function JobHandler<TInput>(\n type: string,\n meta: JobHandlerMeta<TInput>,\n): ClassDecorator {\n return (target) => {\n if (JOB_HANDLER_REGISTRY.has(type)) {\n const env = process.env.NODE_ENV;\n if (env === 'production') {\n throw new Error(\n `[JobHandler] Duplicate registration for job type '${type}'. ` +\n `Each @JobHandler must declare a unique type.`,\n );\n }\n if (env !== 'test') {\n // eslint-disable-next-line no-console\n console.warn(\n `[JobHandler] Duplicate registration for job type '${type}'. ` +\n `Overwriting previous handler — this is almost certainly a bug.`,\n );\n }\n }\n\n Reflect.defineMetadata(JOB_HANDLER_METADATA_KEY, { type, meta }, target);\n JOB_HANDLER_REGISTRY.set(type, {\n type,\n meta: meta as JobHandlerMeta<unknown>,\n handlerClass: target as unknown as new (\n ...args: unknown[]\n ) => JobHandlerBase<unknown>,\n });\n };\n}\n\n// ─── HandlerRegistry — read helpers consumed by JobWorkerModule (JOB-5) ─────\n\n/**\n * Single entry shape returned by `HandlerRegistry.getAll()` / `.get()` and\n * exposed to `JobWorkerModule.onModuleInit` for boot-time upserts.\n *\n * Structurally compatible with `IJobOrchestrator.upsertJobRows`'s\n * `JobUpsertEntry` so the worker module can pass entries through verbatim\n * without re-mapping.\n */\nexport interface HandlerRegistryEntry {\n type: string;\n meta: JobHandlerMeta<unknown>;\n handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;\n}\n\n/**\n * Read facade over `JOB_HANDLER_REGISTRY`. The decorator's write path is\n * unchanged; this namespace exists so consumers (the worker module, tests)\n * don't import the raw `Map` and accidentally mutate it.\n */\nexport namespace HandlerRegistry {\n /** All registered entries in insertion order. */\n export function getAll(): HandlerRegistryEntry[] {\n return Array.from(JOB_HANDLER_REGISTRY.values());\n }\n\n /** Lookup by job type, or `undefined` if no `@JobHandler` is registered. */\n export function get(type: string): HandlerRegistryEntry | undefined {\n return JOB_HANDLER_REGISTRY.get(type);\n }\n}\n"],"mappings":"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","names":["HandlerRegistry"]}