@pattern-stack/codegen 0.13.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{job-orchestrator.protocol-CHOEqBDk.d.ts → job-orchestrator.protocol-CARhMLCO.d.ts} +1 -1
- package/dist/runtime/subsystems/analytics/analytics.module.js +6 -2
- package/dist/runtime/subsystems/analytics/analytics.module.js.map +1 -1
- package/dist/runtime/subsystems/analytics/analytics.tokens.d.ts +0 -11
- package/dist/runtime/subsystems/analytics/analytics.tokens.js +6 -2
- package/dist/runtime/subsystems/analytics/analytics.tokens.js.map +1 -1
- package/dist/runtime/subsystems/analytics/cube-backend.js +6 -2
- package/dist/runtime/subsystems/analytics/cube-backend.js.map +1 -1
- package/dist/runtime/subsystems/analytics/index.js +6 -2
- package/dist/runtime/subsystems/analytics/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/auth.module.js +12 -6
- package/dist/runtime/subsystems/auth/auth.module.js.map +1 -1
- package/dist/runtime/subsystems/auth/auth.tokens.d.ts +0 -28
- package/dist/runtime/subsystems/auth/auth.tokens.js +12 -8
- package/dist/runtime/subsystems/auth/auth.tokens.js.map +1 -1
- package/dist/runtime/subsystems/auth/controllers/auth.controller.js +12 -5
- package/dist/runtime/subsystems/auth/controllers/auth.controller.js.map +1 -1
- package/dist/runtime/subsystems/auth/index.js +12 -8
- package/dist/runtime/subsystems/auth/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/middleware/requester-context.js +12 -1
- package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +10 -2
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.js +2 -2
- package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +10 -2
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/bridge.module.js +186 -168
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge.protocol.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.js +9 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/index.js +168 -150
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.js +6 -1
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/cache/cache.memory-backend.js +6 -1
- package/dist/runtime/subsystems/cache/cache.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/cache/cache.module.js +6 -2
- package/dist/runtime/subsystems/cache/cache.module.js.map +1 -1
- package/dist/runtime/subsystems/cache/cache.tokens.d.ts +0 -10
- package/dist/runtime/subsystems/cache/cache.tokens.js +6 -2
- package/dist/runtime/subsystems/cache/cache.tokens.js.map +1 -1
- package/dist/runtime/subsystems/cache/index.js +6 -2
- package/dist/runtime/subsystems/cache/index.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +5 -0
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +5 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js +5 -1
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/events.module.js +5 -1
- package/dist/runtime/subsystems/events/events.module.js.map +1 -1
- package/dist/runtime/subsystems/events/events.tokens.d.ts +5 -11
- package/dist/runtime/subsystems/events/events.tokens.js +5 -1
- package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/bus.js +5 -0
- package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/index.js +5 -0
- package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
- package/dist/runtime/subsystems/events/index.js +5 -1
- package/dist/runtime/subsystems/events/index.js.map +1 -1
- package/dist/runtime/subsystems/index.d.ts +2 -2
- package/dist/runtime/subsystems/index.js +186 -168
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +0 -9
- package/dist/runtime/subsystems/jobs/bullmq.config.js +6 -2
- package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -1
- package/dist/runtime/subsystems/jobs/index.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/index.js +141 -124
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-handler.base.js +5 -1
- package/dist/runtime/subsystems/jobs/job-handler.base.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +10 -3
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +8 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +137 -7
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +8 -2
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +25 -2
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-step-service.memory-backend.js +25 -2
- package/dist/runtime/subsystems/jobs/job-step-service.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +5 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.js +10 -4
- package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +31 -3
- package/dist/runtime/subsystems/jobs/job-worker.module.js +163 -145
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +144 -130
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.tokens.d.ts +0 -11
- package/dist/runtime/subsystems/jobs/jobs-domain.tokens.js +8 -4
- package/dist/runtime/subsystems/jobs/jobs-domain.tokens.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +1 -1
- package/dist/runtime/subsystems/observability/index.d.ts +1 -1
- package/dist/runtime/subsystems/observability/index.js +9 -1
- package/dist/runtime/subsystems/observability/index.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.module.js +9 -1
- package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.protocol.d.ts +1 -1
- package/dist/runtime/subsystems/observability/observability.service.d.ts +1 -1
- package/dist/runtime/subsystems/observability/observability.service.js +9 -1
- package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -1
- package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -1
- package/dist/runtime/subsystems/storage/index.js +5 -1
- package/dist/runtime/subsystems/storage/index.js.map +1 -1
- package/dist/runtime/subsystems/storage/storage.module.js +5 -1
- package/dist/runtime/subsystems/storage/storage.module.js.map +1 -1
- package/dist/runtime/subsystems/storage/storage.tokens.d.ts +0 -8
- package/dist/runtime/subsystems/storage/storage.tokens.js +5 -1
- package/dist/runtime/subsystems/storage/storage.tokens.js.map +1 -1
- package/dist/runtime/subsystems/token-key.d.ts +7 -0
- package/dist/runtime/subsystems/token-key.js +8 -0
- package/dist/runtime/subsystems/token-key.js.map +1 -0
- package/dist/src/cli/index.js +1160 -694
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +5 -1
- package/runtime/subsystems/analytics/analytics.tokens.ts +6 -2
- package/runtime/subsystems/auth/auth.tokens.ts +15 -8
- package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +8 -1
- package/runtime/subsystems/cache/cache.tokens.ts +7 -2
- package/runtime/subsystems/events/events.tokens.ts +8 -1
- package/runtime/subsystems/index.ts +5 -1
- package/runtime/subsystems/jobs/bullmq.config.ts +5 -2
- package/runtime/subsystems/jobs/job-handler.base.ts +6 -1
- package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +8 -3
- package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +4 -1
- package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +7 -2
- package/runtime/subsystems/jobs/job-worker.module.ts +18 -2
- package/runtime/subsystems/jobs/job-worker.ts +4 -1
- package/runtime/subsystems/jobs/jobs-domain.tokens.ts +10 -7
- package/runtime/subsystems/storage/storage.tokens.ts +6 -1
- package/runtime/subsystems/token-key.ts +7 -0
- package/src/config/runtime-mode.mjs +82 -0
- package/templates/entity/new/backend/modules/core/integration-source.ejs.t +3 -2
- package/templates/entity/new/clean-lite-ps/controller.ejs.t +1 -1
- package/templates/entity/new/clean-lite-ps/module.ejs.t +1 -1
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +8 -2
- package/templates/entity/new/clean-lite-ps/repository.ejs.t +4 -4
- package/templates/entity/new/clean-lite-ps/service.ejs.t +4 -4
- package/templates/entity/new/prompt.js +49 -10
|
@@ -65,9 +65,15 @@ function toJobRunSummary(r) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// runtime/subsystems/token-key.ts
|
|
69
|
+
var PKG = "@pattern-stack/codegen";
|
|
70
|
+
var tokenKey = (area, name) => `${PKG}.${area}.${name}`;
|
|
71
|
+
|
|
68
72
|
// runtime/subsystems/jobs/jobs-domain.tokens.ts
|
|
69
|
-
var JOB_ORCHESTRATOR =
|
|
70
|
-
var
|
|
73
|
+
var JOB_ORCHESTRATOR = Symbol.for(tokenKey("jobs", "orchestrator"));
|
|
74
|
+
var JOB_RUN_SERVICE = Symbol.for(tokenKey("jobs", "run-service"));
|
|
75
|
+
var JOB_STEP_SERVICE = Symbol.for(tokenKey("jobs", "step-service"));
|
|
76
|
+
var JOBS_MULTI_TENANT = Symbol.for(tokenKey("jobs", "multi-tenant"));
|
|
71
77
|
|
|
72
78
|
// runtime/subsystems/jobs/jobs-errors.ts
|
|
73
79
|
var MissingTenantIdError = class extends Error {
|
|
@@ -81,6 +87,22 @@ var MissingTenantIdError = class extends Error {
|
|
|
81
87
|
name = "MissingTenantIdError";
|
|
82
88
|
};
|
|
83
89
|
|
|
90
|
+
// runtime/subsystems/jobs/memory-job-store.ts
|
|
91
|
+
var MemoryJobStore = class {
|
|
92
|
+
/** Runs keyed by `id` (single source of truth for status/scope/lineage). */
|
|
93
|
+
runs = /* @__PURE__ */ new Map();
|
|
94
|
+
/** Steps keyed by `job_run_id`; array order matches insertion order. */
|
|
95
|
+
steps = /* @__PURE__ */ new Map();
|
|
96
|
+
/** Job definitions keyed by `type` — memory mirror of the `job` table. */
|
|
97
|
+
jobs = /* @__PURE__ */ new Map();
|
|
98
|
+
/** Reset everything. Tests call this in `beforeEach`. */
|
|
99
|
+
clear() {
|
|
100
|
+
this.runs.clear();
|
|
101
|
+
this.steps.clear();
|
|
102
|
+
this.jobs.clear();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
84
106
|
// runtime/subsystems/jobs/job-run-service.memory-backend.ts
|
|
85
107
|
var NON_TERMINAL_STATUSES = [
|
|
86
108
|
"pending",
|
|
@@ -249,6 +271,7 @@ var MemoryJobRunService = class {
|
|
|
249
271
|
};
|
|
250
272
|
MemoryJobRunService = __decorateClass([
|
|
251
273
|
Injectable(),
|
|
274
|
+
__decorateParam(0, Inject(MemoryJobStore)),
|
|
252
275
|
__decorateParam(1, Inject(JOB_ORCHESTRATOR)),
|
|
253
276
|
__decorateParam(2, Inject(JOBS_MULTI_TENANT))
|
|
254
277
|
], MemoryJobRunService);
|
|
@@ -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 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
|
+
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-run-service.memory-backend.ts","../../../../runtime/subsystems/jobs/job-run-keyset-cursor.ts","../../../../runtime/subsystems/token-key.ts","../../../../runtime/subsystems/jobs/jobs-domain.tokens.ts","../../../../runtime/subsystems/jobs/jobs-errors.ts","../../../../runtime/subsystems/jobs/memory-job-store.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 // ADR-037 (package-mode DI): explicit `@Inject(MemoryJobStore)` — the\n // published bundle carries no `design:paramtypes`, so a by-type inject\n // would resolve to `undefined` in package mode.\n @Inject(MemoryJobStore) 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","/** Canonical package namespace for cross-boundary DI token keys. MUST be a hardcoded\n * constant (NOT derived from package.json) so a vendored copy — which lives inside the\n * CONSUMER's package — produces the identical key and the two copies share the symbol. */\nexport const PKG = '@pattern-stack/codegen';\n// TODO(token-version): if/when a runtime contract version is adopted, inject it HERE only\n// (e.g. `${PKG}#${ABI}.${area}.${name}`) — this helper is the single chokepoint.\nexport const tokenKey = (area: string, name: string): string => `${PKG}.${area}.${name}`;\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 namespaced `Symbol.for(...)` (ADR-037, via `tokenKey()`) —\n * distinct per key, so Nest's DI lookup is unambiguous, AND matching by VALUE\n * across import boundaries so the package and a (legacy) vendored runtime copy\n * resolve to the same symbol.\n */\nimport { tokenKey } from '../token-key';\n\nexport const JOB_ORCHESTRATOR = Symbol.for(tokenKey('jobs', 'orchestrator'));\nexport const JOB_RUN_SERVICE = Symbol.for(tokenKey('jobs', 'run-service'));\nexport const JOB_STEP_SERVICE = Symbol.for(tokenKey('jobs', '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.for(tokenKey('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","/**\n * MemoryJobStore — the shared in-memory backing for the three memory-backend\n * services (ADR-022, JOB-4).\n *\n * Plain class, not `@Injectable()`. Wired as a `useValue` provider in\n * JOB-5's `JobsDomainModule.forRoot({ backend: 'memory' })` so unit tests\n * can keep a direct reference for `beforeEach` resets.\n *\n * All three memory services receive the same `MemoryJobStore` instance via\n * constructor injection; the store owns mutable state, the services are\n * stateless mutators.\n */\nimport type {\n JobDefinitionRow,\n JobRunRow,\n JobStepRow,\n} from './job-orchestration.schema';\n\nexport class MemoryJobStore {\n /** Runs keyed by `id` (single source of truth for status/scope/lineage). */\n readonly runs: Map<string, JobRunRow> = new Map();\n\n /** Steps keyed by `job_run_id`; array order matches insertion order. */\n readonly steps: Map<string, JobStepRow[]> = new Map();\n\n /** Job definitions keyed by `type` — memory mirror of the `job` table. */\n readonly jobs: Map<string, JobDefinitionRow> = new Map();\n\n /** Reset everything. Tests call this in `beforeEach`. */\n clear(): void {\n this.runs.clear();\n this.steps.clear();\n this.jobs.clear();\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAQA,SAAS,QAAQ,kBAAkB;;;ACkB5B,IAAM,qBAAqB;AAE3B,IAAM,iBAAiB;AAGvB,SAAS,WAAW,OAAmC;AAC5D,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,GAAG;AACxD,WAAO;AAAA,EACT;AACA,QAAM,UAAU,KAAK,MAAM,KAAK;AAChC,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,UAAU,eAAgB,QAAO;AACrC,SAAO;AACT;AAEO,SAAS,mBAAmB,QAA8B;AAC/D,QAAM,QAAQ,CAAC,OAAO,UAAU,YAAY,GAAG,OAAO,EAAE;AACxD,SAAO,OAAO,KAAK,KAAK,UAAU,KAAK,GAAG,MAAM,EAAE,SAAS,WAAW;AACxE;AAOO,SAAS,mBAAmB,QAAqC;AACtE,MAAI;AACF,UAAM,OAAO,OAAO,KAAK,QAAQ,WAAW,EAAE,SAAS,MAAM;AAC7D,UAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,EAAG,QAAO;AAC1D,UAAM,CAAC,KAAK,EAAE,IAAI;AAClB,QAAI,OAAO,QAAQ,YAAY,OAAO,OAAO,SAAU,QAAO;AAC9D,UAAM,YAAY,IAAI,KAAK,GAAG;AAC9B,QAAI,OAAO,MAAM,UAAU,QAAQ,CAAC,EAAG,QAAO;AAC9C,WAAO,EAAE,WAAW,GAAG;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,gBAAgB,GAA6B;AAC3D,SAAO;AAAA,IACL,OAAO,EAAE;AAAA,IACT,WAAW,EAAE;AAAA,IACb,aAAa,EAAE;AAAA,IACf,eAAe,EAAE;AAAA,IACjB,YAAY,EAAE;AAAA,IACd,SAAS,EAAE;AAAA,IACX,MAAM,EAAE;AAAA,IACR,QAAQ,EAAE;AAAA,IACV,iBAAiB,EAAE;AAAA,IACnB,eAAe,EAAE;AAAA,IACjB,UAAU,EAAE;AAAA,IACZ,UAAU,EAAE;AAAA,IACZ,cAAc,EAAE,OAAO,WAAW;AAAA,IAClC,OAAO,EAAE;AAAA,IACT,WAAW,EAAE;AAAA,IACb,YAAY,EAAE;AAAA,IACd,WAAW,EAAE;AAAA,EACf;AACF;;;ACvFO,IAAM,MAAM;AAGZ,IAAM,WAAW,CAAC,MAAc,SAAyB,GAAG,GAAG,IAAI,IAAI,IAAI,IAAI;;;ACQ/E,IAAM,mBAAmB,OAAO,IAAI,SAAS,QAAQ,cAAc,CAAC;AACpE,IAAM,kBAAkB,OAAO,IAAI,SAAS,QAAQ,aAAa,CAAC;AAClE,IAAM,mBAAmB,OAAO,IAAI,SAAS,QAAQ,cAAc,CAAC;AAgBpE,IAAM,oBAAoB,OAAO,IAAI,SAAS,QAAQ,cAAc,CAAC;;;AC4DrE,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAE9C,YAA4B,QAAgB;AAC1C;AAAA,MACE,mFAC0B,MAAM;AAAA,IAGlC;AAN0B;AAAA,EAO5B;AAAA,EAP4B;AAAA,EADV,OAAO;AAS3B;;;ACpFO,IAAM,iBAAN,MAAqB;AAAA;AAAA,EAEjB,OAA+B,oBAAI,IAAI;AAAA;AAAA,EAGvC,QAAmC,oBAAI,IAAI;AAAA;AAAA,EAG3C,OAAsC,oBAAI,IAAI;AAAA;AAAA,EAGvD,QAAc;AACZ,SAAK,KAAK,MAAM;AAChB,SAAK,MAAM,MAAM;AACjB,SAAK,KAAK,MAAM;AAAA,EAClB;AACF;;;ALFA,IAAM,wBAA+C;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,sBAAN,MAAoD;AAAA,EACzD,YAI2C,OACE,cACC,aAC5C;AAHyC;AACE;AACC;AAAA,EAC3C;AAAA,EAHwC;AAAA,EACE;AAAA,EACC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAStC,gBACN,QACA,UACoC;AACpC,QAAI,CAAC,KAAK,YAAa,QAAO;AAC9B,QAAI,aAAa,OAAW,OAAM,IAAI,qBAAqB,MAAM;AACjE,WAAO,CAAC,MAAM,EAAE,aAAa;AAAA,EAC/B;AAAA,EAEA,MAAM,aACJ,YACA,UACA,OAA4B,CAAC,GACV;AACnB,UAAM,eAAe,KAAK,SACtB,MAAM,QAAQ,KAAK,MAAM,IACvB,IAAI,IAAI,KAAK,MAAM,IACnB,oBAAI,IAAI,CAAC,KAAK,MAAM,CAAC,IACvB;AACJ,UAAM,cAAc,KAAK,gBAAgB,gBAAgB,KAAK,QAAQ;AAEtE,UAAM,OAAoB,CAAC;AAC3B,eAAW,KAAK,KAAK,MAAM,KAAK,OAAO,GAAG;AACxC,UAAI,EAAE,oBAAoB,WAAY;AACtC,UAAI,EAAE,kBAAkB,SAAU;AAClC,UAAI,gBAAgB,CAAC,aAAa,IAAI,EAAE,MAAM,EAAG;AACjD,UAAI,KAAK,WAAW,EAAE,YAAY,KAAK,QAAS;AAChD,UAAI,eAAe,CAAC,YAAY,CAAC,EAAG;AACpC,WAAK,KAAK,CAAC;AAAA,IACb;AAEA,UAAM,UAAU,KAAK,WAAW;AAChC,SAAK,KAAK,CAAC,GAAG,MAAM,UAAU,GAAG,GAAG,OAAO,CAAC;AAE5C,UAAM,SAAS,KAAK,UAAU;AAC9B,UAAM,QAAQ,KAAK;AACnB,UAAM,SACJ,OAAO,UAAU,WAAW,KAAK,MAAM,QAAQ,SAAS,KAAK,IAAI,KAAK,MAAM,MAAM;AACpF,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,eACJ,YACA,UACA,OAA8B,CAAC,GAChB;AACf,UAAM,cAAc,KAAK,gBAAgB,kBAAkB,KAAK,QAAQ;AAExE,UAAM,MAAgB,CAAC;AACvB,eAAW,KAAK,KAAK,MAAM,KAAK,OAAO,GAAG;AACxC,UAAI,EAAE,oBAAoB,WAAY;AACtC,UAAI,EAAE,kBAAkB,SAAU;AAClC,UAAI,CAAC,sBAAsB,SAAS,EAAE,MAAM,EAAG;AAC/C,UAAI,eAAe,CAAC,YAAY,CAAC,EAAG;AACpC,UAAI,KAAK,EAAE,EAAE;AAAA,IACf;AACA,eAAW,MAAM,KAAK;AAIpB,YAAM,KAAK,aAAa,OAAO,IAAI;AAAA,QACjC,SAAS;AAAA,QACT,UAAU,KAAK;AAAA,MACjB,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,mBACJ,YACA,UACA,UACA,OAAkC,CAAC,GACpB;AACf,UAAM,cAAc,KAAK,gBAAgB,sBAAsB,KAAK,QAAQ;AAC5E,eAAW,KAAK,KAAK,MAAM,KAAK,OAAO,GAAG;AACxC,UAAI,EAAE,oBAAoB,WAAY;AACtC,UAAI,EAAE,kBAAkB,SAAU;AAClC,UAAI,EAAE,WAAW,UAAW;AAC5B,UAAI,eAAe,CAAC,YAAY,CAAC,EAAG;AACpC,WAAK,MAAM,KAAK,IAAI,EAAE,IAAI;AAAA,QACxB,GAAG;AAAA,QACH,OAAO;AAAA,QACP,WAAW,oBAAI,KAAK;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,qBACJ,UAC4B;AAC5B,UAAM,cAAc,KAAK,gBAAgB,wBAAwB,QAAQ;AACzE,UAAM,MAAM,oBAAI,IAA6B;AAC7C,eAAW,KAAK,KAAK,MAAM,KAAK,OAAO,GAAG;AACxC,UAAI,eAAe,CAAC,YAAY,CAAC,EAAG;AACpC,YAAM,MAAM,GAAG,EAAE,IAAI,KAAK,EAAE,MAAM;AAClC,YAAM,MAAM,IAAI,IAAI,GAAG;AACvB,UAAI,KAAK;AACP,YAAI,SAAS;AAAA,MACf,OAAO;AACL,YAAI,IAAI,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,EAAE,QAAQ,OAAO,EAAE,CAAC;AAAA,MAC3D;AAAA,IACF;AACA,WAAO,MAAM,KAAK,IAAI,OAAO,CAAC;AAAA,EAChC;AAAA,EAEA,MAAM,iBACJ,OACA,UAC0B;AAC1B,UAAM,cAAc,KAAK,gBAAgB,oBAAoB,QAAQ;AACrE,UAAM,SAAsB,CAAC;AAC7B,eAAW,KAAK,KAAK,MAAM,KAAK,OAAO,GAAG;AACxC,UAAI,EAAE,WAAW,SAAU;AAC3B,UAAI,eAAe,CAAC,YAAY,CAAC,EAAG;AACpC,aAAO,KAAK,CAAC;AAAA,IACf;AACA,WAAO,KAAK,CAAC,GAAG,MAAM;AACpB,YAAM,MAAM,EAAE,cAAc,EAAE,WAAW,QAAQ;AACjD,YAAM,MAAM,EAAE,cAAc,EAAE,WAAW,QAAQ;AACjD,aAAO,KAAK;AAAA,IACd,CAAC;AACD,WAAO,OAAO,MAAM,GAAG,KAAK,EAAE,IAAI,CAAC,OAAO;AAAA,MACxC,OAAO,EAAE;AAAA,MACT,SAAS,EAAE;AAAA,MACX,MAAM,EAAE;AAAA,MACR,iBAAiB,EAAE;AAAA,MACnB,eAAe,EAAE;AAAA,MACjB,UAAU,EAAE;AAAA,MACZ,UAAU,EAAE;AAAA,MACZ,cAAc,EAAE,OAAO,WAAW;AAAA,MAClC,UAAU,EAAE,cAAc,EAAE;AAAA,MAC5B,WAAW,EAAE;AAAA,IACf,EAAE;AAAA,EACJ;AAAA,EAEA,MAAM,YAAY,QAA0B,CAAC,GAAwB;AACnE,UAAM,QAAQ,WAAW,MAAM,KAAK;AACpC,UAAM,cAAc,KAAK,gBAAgB,eAAe,MAAM,QAAQ;AACtE,UAAM,SAAS,MAAM,SAAS,mBAAmB,MAAM,MAAM,IAAI;AAEjE,UAAM,UAAuB,CAAC;AAC9B,eAAW,KAAK,KAAK,MAAM,KAAK,OAAO,GAAG;AACxC,UAAI,eAAe,CAAC,YAAY,CAAC,EAAG;AACpC,UAAI,MAAM,UAAU,EAAE,SAAS,MAAM,OAAQ;AAC7C,UAAI,MAAM,aAAa,EAAE,cAAc,MAAM,UAAW;AACxD,UAAI,MAAM,UAAU,EAAE,WAAW,MAAM,OAAQ;AAC/C,UAAI,MAAM,SAAS,EAAE,UAAU,QAAQ,IAAI,MAAM,MAAM,QAAQ,EAAG;AAClE,cAAQ,KAAK,CAAC;AAAA,IAChB;AAGA,YAAQ,KAAK,CAAC,GAAG,MAAM;AACrB,YAAM,KAAK,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ;AACvD,UAAI,OAAO,EAAG,QAAO;AACrB,aAAO,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK;AAAA,IAC9C,CAAC;AAGD,UAAM,SAAS,SACX,QAAQ,OAAO,CAAC,MAAM;AACpB,YAAM,KAAK,EAAE,UAAU,QAAQ;AAC/B,YAAM,KAAK,OAAO,UAAU,QAAQ;AACpC,UAAI,KAAK,GAAI,QAAO;AACpB,UAAI,KAAK,GAAI,QAAO;AACpB,aAAO,EAAE,KAAK,OAAO;AAAA,IACvB,CAAC,IACD;AAEJ,UAAM,UAAU,OAAO,SAAS;AAChC,UAAM,OAAO,UAAU,OAAO,MAAM,GAAG,KAAK,IAAI;AAChD,UAAM,QAAQ,KAAK,IAAI,eAAe;AACtC,UAAM,OAAO,KAAK,KAAK,SAAS,CAAC;AACjC,UAAM,aACJ,WAAW,OACP,mBAAmB,EAAE,WAAW,KAAK,WAAW,IAAI,KAAK,GAAG,CAAC,IAC7D;AAEN,WAAO,EAAE,OAAO,WAAW;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAS,OAA8B;AACrC,WAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,KAAK;AAAA,EACxC;AAAA;AAAA,EAGA,gBAAgB,WAA6B;AAC3C,UAAM,MAAmB,CAAC;AAC1B,eAAW,KAAK,KAAK,MAAM,KAAK,OAAO,GAAG;AACxC,UAAI,EAAE,cAAc,UAAW,KAAI,KAAK,CAAC;AAAA,IAC3C;AACA,WAAO;AAAA,EACT;AACF;AArNa,sBAAN;AAAA,EADN,WAAW;AAAA,EAMP,0BAAO,cAAc;AAAA,EACrB,0BAAO,gBAAgB;AAAA,EACvB,0BAAO,iBAAiB;AAAA,GAPhB;AAuNb,SAAS,UACP,GACA,GACA,OACQ;AACR,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,aAAO,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ;AAAA,IACrD,KAAK;AACH,aAAO,EAAE,MAAM,QAAQ,IAAI,EAAE,MAAM,QAAQ;AAAA,IAC7C,KAAK;AACH,aAAO,EAAE,MAAM,QAAQ,IAAI,EAAE,MAAM,QAAQ;AAAA,IAC7C,KAAK;AAAA,IACL;AACE,aAAO,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ;AAAA,EACvD;AACF;","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as JobRun } from '../../../job-orchestrator.protocol-
|
|
1
|
+
import { i as JobRun } from '../../../job-orchestrator.protocol-CARhMLCO.js';
|
|
2
2
|
import '../events/event-bus.protocol.js';
|
|
3
3
|
import '../../types/drizzle.js';
|
|
4
4
|
import 'drizzle-orm/node-postgres';
|
|
@@ -8,11 +8,33 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
8
8
|
if (kind && result) __defProp(target, key, result);
|
|
9
9
|
return result;
|
|
10
10
|
};
|
|
11
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
11
12
|
|
|
12
13
|
// runtime/subsystems/jobs/job-step-service.memory-backend.ts
|
|
13
14
|
import { randomUUID } from "crypto";
|
|
14
|
-
import { Injectable } from "@nestjs/common";
|
|
15
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
16
|
+
|
|
17
|
+
// runtime/subsystems/jobs/memory-job-store.ts
|
|
18
|
+
var MemoryJobStore = class {
|
|
19
|
+
/** Runs keyed by `id` (single source of truth for status/scope/lineage). */
|
|
20
|
+
runs = /* @__PURE__ */ new Map();
|
|
21
|
+
/** Steps keyed by `job_run_id`; array order matches insertion order. */
|
|
22
|
+
steps = /* @__PURE__ */ new Map();
|
|
23
|
+
/** Job definitions keyed by `type` — memory mirror of the `job` table. */
|
|
24
|
+
jobs = /* @__PURE__ */ new Map();
|
|
25
|
+
/** Reset everything. Tests call this in `beforeEach`. */
|
|
26
|
+
clear() {
|
|
27
|
+
this.runs.clear();
|
|
28
|
+
this.steps.clear();
|
|
29
|
+
this.jobs.clear();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// runtime/subsystems/jobs/job-step-service.memory-backend.ts
|
|
15
34
|
var MemoryJobStepService = class {
|
|
35
|
+
// ADR-037 (package-mode DI): explicit `@Inject(MemoryJobStore)` — the
|
|
36
|
+
// published bundle carries no `design:paramtypes`, so a by-type inject
|
|
37
|
+
// would resolve to `undefined` in package mode.
|
|
16
38
|
constructor(store) {
|
|
17
39
|
this.store = store;
|
|
18
40
|
}
|
|
@@ -104,7 +126,8 @@ var MemoryJobStepService = class {
|
|
|
104
126
|
}
|
|
105
127
|
};
|
|
106
128
|
MemoryJobStepService = __decorateClass([
|
|
107
|
-
Injectable()
|
|
129
|
+
Injectable(),
|
|
130
|
+
__decorateParam(0, Inject(MemoryJobStore))
|
|
108
131
|
], MemoryJobStepService);
|
|
109
132
|
export {
|
|
110
133
|
MemoryJobStepService
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-step-service.memory-backend.ts"],"sourcesContent":["/**\n * MemoryJobStepService — in-memory implementation of `IJobStepService`\n * (ADR-022, JOB-4).\n *\n * Mirrors `DrizzleJobStepService` but against plain Maps. `findStep` only\n * returns `completed` rows (memoization cache hit); anything non-completed\n * is invisible so the ctx.step fn re-runs on replay / retry exactly like\n * the Drizzle backend (which deletes non-completed rows on replay).\n */\nimport { randomUUID } from 'node:crypto';\nimport { Injectable } from '@nestjs/common';\nimport type { JobStepRow } from './job-orchestration.schema';\nimport type {\n IJobStepService,\n JobStep,\n RecordStepInput,\n} from './job-step-service.protocol';\nimport { MemoryJobStore } from './memory-job-store';\n\n@Injectable()\nexport class MemoryJobStepService implements IJobStepService {\n constructor(private readonly store: MemoryJobStore) {}\n\n async findStep(runId: string, stepId: string): Promise<JobStep | null> {\n const rows = this.store.steps.get(runId);\n if (!rows) return null;\n const match = rows.find(\n (r) => r.stepId === stepId && r.status === 'completed',\n );\n return (match ?? null) as JobStep | null;\n }\n\n async recordStep(input: RecordStepInput): Promise<JobStep> {\n const rows = this.getOrCreateRows(input.jobRunId);\n const existingIdx = rows.findIndex((r) => r.stepId === input.stepId);\n\n const normalisedInput =\n (input.input ?? null) as Record<string, unknown> | null;\n const normalisedOutput =\n (input.output ?? null) as Record<string, unknown> | null;\n\n if (existingIdx >= 0) {\n const prev = rows[existingIdx]!;\n const next: JobStepRow = {\n ...prev,\n status: input.status,\n input: normalisedInput ?? prev.input,\n output: normalisedOutput ?? prev.output,\n error: input.error ?? prev.error,\n attempts: input.attempts ?? prev.attempts,\n startedAt: input.startedAt ?? prev.startedAt,\n finishedAt: input.finishedAt ?? prev.finishedAt,\n };\n rows[existingIdx] = next;\n return next as JobStep;\n }\n\n const seq = input.seq ?? this.nextSeq(rows);\n const row: JobStepRow = {\n id: randomUUID(),\n jobRunId: input.jobRunId,\n stepId: input.stepId,\n kind: input.kind,\n seq,\n status: input.status,\n input: normalisedInput,\n output: normalisedOutput,\n error: input.error ?? null,\n attempts: input.attempts ?? 0,\n startedAt: input.startedAt ?? null,\n finishedAt: input.finishedAt ?? null,\n };\n rows.push(row);\n return row as JobStep;\n }\n\n /**\n * Replay helper — wipe every step row for a run. Mirrors the `scratch`\n * replay mode of the Drizzle backend (`DELETE FROM job_step WHERE job_run_id = …`).\n */\n clearStepsForRun(runId: string): void {\n this.store.steps.delete(runId);\n }\n\n /**\n * Remove every non-`completed` row for the run. Memoized (`completed`)\n * rows are preserved — this is the `last_checkpoint` / `last_step`\n * semantics the Drizzle backend implements via\n * `DELETE … WHERE status != 'completed'`. Both replay modes route here\n * (Phase 1 collapses `last_step` onto this behaviour; see JOB-3 notes).\n */\n clearIncompleteSteps(runId: string): void {\n const rows = this.store.steps.get(runId);\n if (!rows) return;\n const kept = rows.filter((r) => r.status === 'completed');\n if (kept.length === 0) {\n this.store.steps.delete(runId);\n } else {\n this.store.steps.set(runId, kept);\n }\n }\n\n private getOrCreateRows(runId: string): JobStepRow[] {\n let rows = this.store.steps.get(runId);\n if (!rows) {\n rows = [];\n this.store.steps.set(runId, rows);\n }\n return rows;\n }\n\n private nextSeq(rows: JobStepRow[]): number {\n let max = 0;\n for (const r of rows) {\n if (r.seq > max) max = r.seq;\n }\n return max + 1;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-step-service.memory-backend.ts","../../../../runtime/subsystems/jobs/memory-job-store.ts"],"sourcesContent":["/**\n * MemoryJobStepService — in-memory implementation of `IJobStepService`\n * (ADR-022, JOB-4).\n *\n * Mirrors `DrizzleJobStepService` but against plain Maps. `findStep` only\n * returns `completed` rows (memoization cache hit); anything non-completed\n * is invisible so the ctx.step fn re-runs on replay / retry exactly like\n * the Drizzle backend (which deletes non-completed rows on replay).\n */\nimport { randomUUID } from 'node:crypto';\nimport { Inject, Injectable } from '@nestjs/common';\nimport type { JobStepRow } from './job-orchestration.schema';\nimport type {\n IJobStepService,\n JobStep,\n RecordStepInput,\n} from './job-step-service.protocol';\nimport { MemoryJobStore } from './memory-job-store';\n\n@Injectable()\nexport class MemoryJobStepService implements IJobStepService {\n // ADR-037 (package-mode DI): explicit `@Inject(MemoryJobStore)` — the\n // published bundle carries no `design:paramtypes`, so a by-type inject\n // would resolve to `undefined` in package mode.\n constructor(\n @Inject(MemoryJobStore) private readonly store: MemoryJobStore,\n ) {}\n\n async findStep(runId: string, stepId: string): Promise<JobStep | null> {\n const rows = this.store.steps.get(runId);\n if (!rows) return null;\n const match = rows.find(\n (r) => r.stepId === stepId && r.status === 'completed',\n );\n return (match ?? null) as JobStep | null;\n }\n\n async recordStep(input: RecordStepInput): Promise<JobStep> {\n const rows = this.getOrCreateRows(input.jobRunId);\n const existingIdx = rows.findIndex((r) => r.stepId === input.stepId);\n\n const normalisedInput =\n (input.input ?? null) as Record<string, unknown> | null;\n const normalisedOutput =\n (input.output ?? null) as Record<string, unknown> | null;\n\n if (existingIdx >= 0) {\n const prev = rows[existingIdx]!;\n const next: JobStepRow = {\n ...prev,\n status: input.status,\n input: normalisedInput ?? prev.input,\n output: normalisedOutput ?? prev.output,\n error: input.error ?? prev.error,\n attempts: input.attempts ?? prev.attempts,\n startedAt: input.startedAt ?? prev.startedAt,\n finishedAt: input.finishedAt ?? prev.finishedAt,\n };\n rows[existingIdx] = next;\n return next as JobStep;\n }\n\n const seq = input.seq ?? this.nextSeq(rows);\n const row: JobStepRow = {\n id: randomUUID(),\n jobRunId: input.jobRunId,\n stepId: input.stepId,\n kind: input.kind,\n seq,\n status: input.status,\n input: normalisedInput,\n output: normalisedOutput,\n error: input.error ?? null,\n attempts: input.attempts ?? 0,\n startedAt: input.startedAt ?? null,\n finishedAt: input.finishedAt ?? null,\n };\n rows.push(row);\n return row as JobStep;\n }\n\n /**\n * Replay helper — wipe every step row for a run. Mirrors the `scratch`\n * replay mode of the Drizzle backend (`DELETE FROM job_step WHERE job_run_id = …`).\n */\n clearStepsForRun(runId: string): void {\n this.store.steps.delete(runId);\n }\n\n /**\n * Remove every non-`completed` row for the run. Memoized (`completed`)\n * rows are preserved — this is the `last_checkpoint` / `last_step`\n * semantics the Drizzle backend implements via\n * `DELETE … WHERE status != 'completed'`. Both replay modes route here\n * (Phase 1 collapses `last_step` onto this behaviour; see JOB-3 notes).\n */\n clearIncompleteSteps(runId: string): void {\n const rows = this.store.steps.get(runId);\n if (!rows) return;\n const kept = rows.filter((r) => r.status === 'completed');\n if (kept.length === 0) {\n this.store.steps.delete(runId);\n } else {\n this.store.steps.set(runId, kept);\n }\n }\n\n private getOrCreateRows(runId: string): JobStepRow[] {\n let rows = this.store.steps.get(runId);\n if (!rows) {\n rows = [];\n this.store.steps.set(runId, rows);\n }\n return rows;\n }\n\n private nextSeq(rows: JobStepRow[]): number {\n let max = 0;\n for (const r of rows) {\n if (r.seq > max) max = r.seq;\n }\n return max + 1;\n }\n}\n","/**\n * MemoryJobStore — the shared in-memory backing for the three memory-backend\n * services (ADR-022, JOB-4).\n *\n * Plain class, not `@Injectable()`. Wired as a `useValue` provider in\n * JOB-5's `JobsDomainModule.forRoot({ backend: 'memory' })` so unit tests\n * can keep a direct reference for `beforeEach` resets.\n *\n * All three memory services receive the same `MemoryJobStore` instance via\n * constructor injection; the store owns mutable state, the services are\n * stateless mutators.\n */\nimport type {\n JobDefinitionRow,\n JobRunRow,\n JobStepRow,\n} from './job-orchestration.schema';\n\nexport class MemoryJobStore {\n /** Runs keyed by `id` (single source of truth for status/scope/lineage). */\n readonly runs: Map<string, JobRunRow> = new Map();\n\n /** Steps keyed by `job_run_id`; array order matches insertion order. */\n readonly steps: Map<string, JobStepRow[]> = new Map();\n\n /** Job definitions keyed by `type` — memory mirror of the `job` table. */\n readonly jobs: Map<string, JobDefinitionRow> = new Map();\n\n /** Reset everything. Tests call this in `beforeEach`. */\n clear(): void {\n this.runs.clear();\n this.steps.clear();\n this.jobs.clear();\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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-
|
|
4
|
+
import { I as IJobOrchestrator } from '../../../job-orchestrator.protocol-CARhMLCO.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';
|
|
@@ -156,8 +156,13 @@ var jobSteps = pgTable(
|
|
|
156
156
|
})
|
|
157
157
|
);
|
|
158
158
|
|
|
159
|
+
// runtime/subsystems/token-key.ts
|
|
160
|
+
var PKG = "@pattern-stack/codegen";
|
|
161
|
+
var tokenKey = (area, name) => `${PKG}.${area}.${name}`;
|
|
162
|
+
|
|
159
163
|
// runtime/subsystems/jobs/job-handler.base.ts
|
|
160
164
|
var JOB_HANDLER_REGISTRY = /* @__PURE__ */ new Map();
|
|
165
|
+
var JOB_HANDLER_METADATA_KEY = Symbol.for(tokenKey("jobs", "handler-metadata"));
|
|
161
166
|
var HandlerRegistry;
|
|
162
167
|
((HandlerRegistry2) => {
|
|
163
168
|
function getAll() {
|
|
@@ -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 // #6 / noImplicitAny — explicit annotations so the file stays\n // strict-clean even when consumers compile under a stricter tsconfig\n // than the one used to type-check the runtime tree.\n (job: Job<BullJobPayload>) => this.process(job),\n {\n connection: this.options.connection,\n concurrency: this.options.concurrency,\n },\n );\n this.worker.on('failed', (job: Job<BullJobPayload> | undefined, err: Error) => {\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"]}
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-worker.bullmq-backend.ts","../../../../runtime/subsystems/jobs/job-orchestration.schema.ts","../../../../runtime/subsystems/token-key.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 // #6 / noImplicitAny — explicit annotations so the file stays\n // strict-clean even when consumers compile under a stricter tsconfig\n // than the one used to type-check the runtime tree.\n (job: Job<BullJobPayload>) => this.process(job),\n {\n connection: this.options.connection,\n concurrency: this.options.concurrency,\n },\n );\n this.worker.on('failed', (job: Job<BullJobPayload> | undefined, err: Error) => {\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","/** Canonical package namespace for cross-boundary DI token keys. MUST be a hardcoded\n * constant (NOT derived from package.json) so a vendored copy — which lives inside the\n * CONSUMER's package — produces the identical key and the two copies share the symbol. */\nexport const PKG = '@pattern-stack/codegen';\n// TODO(token-version): if/when a runtime contract version is adopted, inject it HERE only\n// (e.g. `${PKG}#${ABI}.${area}.${name}`) — this helper is the single chokepoint.\nexport const tokenKey = (area: string, name: string): string => `${PKG}.${area}.${name}`;\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 { tokenKey } from '../token-key';\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\n// ADR-037: namespaced `Symbol.for(...)` (via `tokenKey()`) so the reflection-metadata\n// key matches by value across import boundaries (the @JobHandler decorator and the\n// reader may resolve different runtime copies). Distinct from the DI tokens but\n// subject to the same dual-package identity hazard.\nexport const JOB_HANDLER_METADATA_KEY = Symbol.for(tokenKey('jobs', 'handler-metadata'));\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"]}
|
|
@@ -3,7 +3,7 @@ import { OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|
|
3
3
|
import { ModuleRef } from '@nestjs/core';
|
|
4
4
|
import { DrizzleClient } from '../../types/drizzle.js';
|
|
5
5
|
import { JobRunRow } from './job-orchestration.schema.js';
|
|
6
|
-
import { I as IJobOrchestrator, R as RetryPolicy } from '../../../job-orchestrator.protocol-
|
|
6
|
+
import { I as IJobOrchestrator, R as RetryPolicy } from '../../../job-orchestrator.protocol-CARhMLCO.js';
|
|
7
7
|
import { IJobRunService } from './job-run-service.protocol.js';
|
|
8
8
|
import { IJobStepService } from './job-step-service.protocol.js';
|
|
9
9
|
import 'drizzle-orm/node-postgres';
|
|
@@ -14,6 +14,10 @@ var __decorateParam = (index2, decorator) => (target, key) => decorator(target,
|
|
|
14
14
|
import { Inject, Injectable, Logger } from "@nestjs/common";
|
|
15
15
|
import { and, asc, desc, eq, inArray, lt, lte, sql as sql2 } from "drizzle-orm";
|
|
16
16
|
|
|
17
|
+
// runtime/subsystems/token-key.ts
|
|
18
|
+
var PKG = "@pattern-stack/codegen";
|
|
19
|
+
var tokenKey = (area, name) => `${PKG}.${area}.${name}`;
|
|
20
|
+
|
|
17
21
|
// runtime/constants/tokens.ts
|
|
18
22
|
var DRIZZLE = "DRIZZLE";
|
|
19
23
|
|
|
@@ -172,12 +176,14 @@ var jobSteps = pgTable(
|
|
|
172
176
|
);
|
|
173
177
|
|
|
174
178
|
// runtime/subsystems/jobs/jobs-domain.tokens.ts
|
|
175
|
-
var JOB_ORCHESTRATOR =
|
|
176
|
-
var JOB_RUN_SERVICE =
|
|
177
|
-
var JOB_STEP_SERVICE =
|
|
179
|
+
var JOB_ORCHESTRATOR = Symbol.for(tokenKey("jobs", "orchestrator"));
|
|
180
|
+
var JOB_RUN_SERVICE = Symbol.for(tokenKey("jobs", "run-service"));
|
|
181
|
+
var JOB_STEP_SERVICE = Symbol.for(tokenKey("jobs", "step-service"));
|
|
182
|
+
var JOBS_MULTI_TENANT = Symbol.for(tokenKey("jobs", "multi-tenant"));
|
|
178
183
|
|
|
179
184
|
// runtime/subsystems/jobs/job-handler.base.ts
|
|
180
185
|
var JOB_HANDLER_REGISTRY = /* @__PURE__ */ new Map();
|
|
186
|
+
var JOB_HANDLER_METADATA_KEY = Symbol.for(tokenKey("jobs", "handler-metadata"));
|
|
181
187
|
var HandlerRegistry;
|
|
182
188
|
((HandlerRegistry2) => {
|
|
183
189
|
function getAll() {
|
|
@@ -191,7 +197,7 @@ var HandlerRegistry;
|
|
|
191
197
|
})(HandlerRegistry || (HandlerRegistry = {}));
|
|
192
198
|
|
|
193
199
|
// runtime/subsystems/jobs/job-worker.ts
|
|
194
|
-
var JOB_WORKER_OPTIONS =
|
|
200
|
+
var JOB_WORKER_OPTIONS = Symbol.for(tokenKey("jobs", "worker-options"));
|
|
195
201
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
196
202
|
var DEFAULT_STALE_SWEEPER_INTERVAL_MS = 6e4;
|
|
197
203
|
var DEFAULT_STALE_THRESHOLD_MS = 5 * 6e4;
|