@pattern-stack/codegen 0.14.0 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- 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.module.d.ts +22 -0
- package/dist/runtime/subsystems/bridge/bridge.module.js +177 -160
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.js +159 -142
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/index.js +161 -148
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/index.js +128 -115
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +128 -6
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +17 -0
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +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.module.d.ts +26 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.js +150 -137
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +133 -124
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/src/cli/index.js +1040 -635
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +8 -1
- package/runtime/subsystems/bridge/bridge.module.ts +26 -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 +13 -1
|
@@ -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/token-key.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","/** 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"],"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":"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","names":[]}
|
|
@@ -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":[]}
|
|
@@ -114,6 +114,18 @@ declare class JobWorkerOrchestrator implements OnModuleInit, OnModuleDestroy {
|
|
|
114
114
|
* without supplying a `DRIZZLE` provider.
|
|
115
115
|
*/
|
|
116
116
|
private readonly db;
|
|
117
|
+
/**
|
|
118
|
+
* ADR-037 (package-mode DI): inject `ModuleRef` EXPLICITLY via `@Inject`
|
|
119
|
+
* rather than relying on `design:paramtypes` reflection. The published
|
|
120
|
+
* package bundle is built without `emitDecoratorMetadata` (tsup/esbuild
|
|
121
|
+
* default), so a by-type injection here would resolve to `undefined` at
|
|
122
|
+
* boot in package mode — breaking the worker entirely (the
|
|
123
|
+
* `ModuleRef not available` throw). Vendored mode happened to work only
|
|
124
|
+
* because the consumer's own `tsc` (emitDecoratorMetadata: true)
|
|
125
|
+
* recompiled the source and emitted the metadata. The explicit token is
|
|
126
|
+
* mode-agnostic. `ModuleRef` is always provided by `@nestjs/core`, so no
|
|
127
|
+
* `@Optional()` is needed (it's a hard dependency of the worker path).
|
|
128
|
+
*/
|
|
117
129
|
private readonly moduleRef?;
|
|
118
130
|
/**
|
|
119
131
|
* BULLMQ-1 — resolved BullMQ connection + config, only bound when the
|
|
@@ -130,7 +142,20 @@ declare class JobWorkerOrchestrator implements OnModuleInit, OnModuleDestroy {
|
|
|
130
142
|
* `@Optional()` so memory-mode boots in `Test.createTestingModule`
|
|
131
143
|
* without supplying a `DRIZZLE` provider.
|
|
132
144
|
*/
|
|
133
|
-
db?: DrizzleClient | null,
|
|
145
|
+
db?: DrizzleClient | null,
|
|
146
|
+
/**
|
|
147
|
+
* ADR-037 (package-mode DI): inject `ModuleRef` EXPLICITLY via `@Inject`
|
|
148
|
+
* rather than relying on `design:paramtypes` reflection. The published
|
|
149
|
+
* package bundle is built without `emitDecoratorMetadata` (tsup/esbuild
|
|
150
|
+
* default), so a by-type injection here would resolve to `undefined` at
|
|
151
|
+
* boot in package mode — breaking the worker entirely (the
|
|
152
|
+
* `ModuleRef not available` throw). Vendored mode happened to work only
|
|
153
|
+
* because the consumer's own `tsc` (emitDecoratorMetadata: true)
|
|
154
|
+
* recompiled the source and emitted the metadata. The explicit token is
|
|
155
|
+
* mode-agnostic. `ModuleRef` is always provided by `@nestjs/core`, so no
|
|
156
|
+
* `@Optional()` is needed (it's a hard dependency of the worker path).
|
|
157
|
+
*/
|
|
158
|
+
moduleRef?: ModuleRef | undefined,
|
|
134
159
|
/**
|
|
135
160
|
* BULLMQ-1 — resolved BullMQ connection + config, only bound when the
|
|
136
161
|
* inner `JobsDomainModule` was booted with `backend: 'bullmq'`. `@Optional()`
|
|
@@ -12,12 +12,13 @@ var __decorateParam = (index2, decorator) => (target, key) => decorator(target,
|
|
|
12
12
|
|
|
13
13
|
// runtime/subsystems/jobs/job-worker.module.ts
|
|
14
14
|
import {
|
|
15
|
-
Inject as
|
|
15
|
+
Inject as Inject8,
|
|
16
16
|
Injectable as Injectable8,
|
|
17
17
|
Logger as Logger4,
|
|
18
18
|
Module as Module2,
|
|
19
19
|
Optional as Optional2
|
|
20
20
|
} from "@nestjs/common";
|
|
21
|
+
import { ModuleRef as ModuleRef2 } from "@nestjs/core";
|
|
21
22
|
|
|
22
23
|
// runtime/subsystems/token-key.ts
|
|
23
24
|
var PKG = "@pattern-stack/codegen";
|
|
@@ -884,8 +885,129 @@ DrizzleJobStepService = __decorateClass([
|
|
|
884
885
|
], DrizzleJobStepService);
|
|
885
886
|
|
|
886
887
|
// runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
|
|
888
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
889
|
+
import { Inject as Inject5, Injectable as Injectable5, Logger as Logger2, Optional } from "@nestjs/common";
|
|
890
|
+
import { ModuleRef } from "@nestjs/core";
|
|
891
|
+
|
|
892
|
+
// runtime/subsystems/jobs/memory-job-store.ts
|
|
893
|
+
var MemoryJobStore = class {
|
|
894
|
+
/** Runs keyed by `id` (single source of truth for status/scope/lineage). */
|
|
895
|
+
runs = /* @__PURE__ */ new Map();
|
|
896
|
+
/** Steps keyed by `job_run_id`; array order matches insertion order. */
|
|
897
|
+
steps = /* @__PURE__ */ new Map();
|
|
898
|
+
/** Job definitions keyed by `type` — memory mirror of the `job` table. */
|
|
899
|
+
jobs = /* @__PURE__ */ new Map();
|
|
900
|
+
/** Reset everything. Tests call this in `beforeEach`. */
|
|
901
|
+
clear() {
|
|
902
|
+
this.runs.clear();
|
|
903
|
+
this.steps.clear();
|
|
904
|
+
this.jobs.clear();
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// runtime/subsystems/jobs/job-step-service.memory-backend.ts
|
|
887
909
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
888
|
-
import { Inject as Inject4, Injectable as Injectable4
|
|
910
|
+
import { Inject as Inject4, Injectable as Injectable4 } from "@nestjs/common";
|
|
911
|
+
var MemoryJobStepService = class {
|
|
912
|
+
// ADR-037 (package-mode DI): explicit `@Inject(MemoryJobStore)` — the
|
|
913
|
+
// published bundle carries no `design:paramtypes`, so a by-type inject
|
|
914
|
+
// would resolve to `undefined` in package mode.
|
|
915
|
+
constructor(store) {
|
|
916
|
+
this.store = store;
|
|
917
|
+
}
|
|
918
|
+
store;
|
|
919
|
+
async findStep(runId, stepId) {
|
|
920
|
+
const rows = this.store.steps.get(runId);
|
|
921
|
+
if (!rows) return null;
|
|
922
|
+
const match = rows.find(
|
|
923
|
+
(r) => r.stepId === stepId && r.status === "completed"
|
|
924
|
+
);
|
|
925
|
+
return match ?? null;
|
|
926
|
+
}
|
|
927
|
+
async recordStep(input) {
|
|
928
|
+
const rows = this.getOrCreateRows(input.jobRunId);
|
|
929
|
+
const existingIdx = rows.findIndex((r) => r.stepId === input.stepId);
|
|
930
|
+
const normalisedInput = input.input ?? null;
|
|
931
|
+
const normalisedOutput = input.output ?? null;
|
|
932
|
+
if (existingIdx >= 0) {
|
|
933
|
+
const prev = rows[existingIdx];
|
|
934
|
+
const next = {
|
|
935
|
+
...prev,
|
|
936
|
+
status: input.status,
|
|
937
|
+
input: normalisedInput ?? prev.input,
|
|
938
|
+
output: normalisedOutput ?? prev.output,
|
|
939
|
+
error: input.error ?? prev.error,
|
|
940
|
+
attempts: input.attempts ?? prev.attempts,
|
|
941
|
+
startedAt: input.startedAt ?? prev.startedAt,
|
|
942
|
+
finishedAt: input.finishedAt ?? prev.finishedAt
|
|
943
|
+
};
|
|
944
|
+
rows[existingIdx] = next;
|
|
945
|
+
return next;
|
|
946
|
+
}
|
|
947
|
+
const seq = input.seq ?? this.nextSeq(rows);
|
|
948
|
+
const row = {
|
|
949
|
+
id: randomUUID2(),
|
|
950
|
+
jobRunId: input.jobRunId,
|
|
951
|
+
stepId: input.stepId,
|
|
952
|
+
kind: input.kind,
|
|
953
|
+
seq,
|
|
954
|
+
status: input.status,
|
|
955
|
+
input: normalisedInput,
|
|
956
|
+
output: normalisedOutput,
|
|
957
|
+
error: input.error ?? null,
|
|
958
|
+
attempts: input.attempts ?? 0,
|
|
959
|
+
startedAt: input.startedAt ?? null,
|
|
960
|
+
finishedAt: input.finishedAt ?? null
|
|
961
|
+
};
|
|
962
|
+
rows.push(row);
|
|
963
|
+
return row;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Replay helper — wipe every step row for a run. Mirrors the `scratch`
|
|
967
|
+
* replay mode of the Drizzle backend (`DELETE FROM job_step WHERE job_run_id = …`).
|
|
968
|
+
*/
|
|
969
|
+
clearStepsForRun(runId) {
|
|
970
|
+
this.store.steps.delete(runId);
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Remove every non-`completed` row for the run. Memoized (`completed`)
|
|
974
|
+
* rows are preserved — this is the `last_checkpoint` / `last_step`
|
|
975
|
+
* semantics the Drizzle backend implements via
|
|
976
|
+
* `DELETE … WHERE status != 'completed'`. Both replay modes route here
|
|
977
|
+
* (Phase 1 collapses `last_step` onto this behaviour; see JOB-3 notes).
|
|
978
|
+
*/
|
|
979
|
+
clearIncompleteSteps(runId) {
|
|
980
|
+
const rows = this.store.steps.get(runId);
|
|
981
|
+
if (!rows) return;
|
|
982
|
+
const kept = rows.filter((r) => r.status === "completed");
|
|
983
|
+
if (kept.length === 0) {
|
|
984
|
+
this.store.steps.delete(runId);
|
|
985
|
+
} else {
|
|
986
|
+
this.store.steps.set(runId, kept);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
getOrCreateRows(runId) {
|
|
990
|
+
let rows = this.store.steps.get(runId);
|
|
991
|
+
if (!rows) {
|
|
992
|
+
rows = [];
|
|
993
|
+
this.store.steps.set(runId, rows);
|
|
994
|
+
}
|
|
995
|
+
return rows;
|
|
996
|
+
}
|
|
997
|
+
nextSeq(rows) {
|
|
998
|
+
let max = 0;
|
|
999
|
+
for (const r of rows) {
|
|
1000
|
+
if (r.seq > max) max = r.seq;
|
|
1001
|
+
}
|
|
1002
|
+
return max + 1;
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
MemoryJobStepService = __decorateClass([
|
|
1006
|
+
Injectable4(),
|
|
1007
|
+
__decorateParam(0, Inject4(MemoryJobStore))
|
|
1008
|
+
], MemoryJobStepService);
|
|
1009
|
+
|
|
1010
|
+
// runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
|
|
889
1011
|
var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
|
|
890
1012
|
var TERMINAL_STATUSES2 = [
|
|
891
1013
|
"completed",
|
|
@@ -1056,7 +1178,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1056
1178
|
}
|
|
1057
1179
|
}
|
|
1058
1180
|
}
|
|
1059
|
-
const newId =
|
|
1181
|
+
const newId = randomUUID3();
|
|
1060
1182
|
let rootRunId = newId;
|
|
1061
1183
|
if (opts.parentRunId) {
|
|
1062
1184
|
const parent = this.store.runs.get(opts.parentRunId);
|
|
@@ -1454,9 +1576,12 @@ var MemoryJobOrchestrator = class {
|
|
|
1454
1576
|
}
|
|
1455
1577
|
};
|
|
1456
1578
|
MemoryJobOrchestrator = __decorateClass([
|
|
1457
|
-
|
|
1458
|
-
__decorateParam(
|
|
1459
|
-
__decorateParam(
|
|
1579
|
+
Injectable5(),
|
|
1580
|
+
__decorateParam(0, Inject5(MemoryJobStore)),
|
|
1581
|
+
__decorateParam(1, Inject5(MemoryJobStepService)),
|
|
1582
|
+
__decorateParam(2, Inject5(JOBS_MULTI_TENANT)),
|
|
1583
|
+
__decorateParam(3, Optional()),
|
|
1584
|
+
__decorateParam(3, Inject5(ModuleRef))
|
|
1460
1585
|
], MemoryJobOrchestrator);
|
|
1461
1586
|
function classifyError(err, policy, currentAttempts) {
|
|
1462
1587
|
if (!policy) return "fail";
|
|
@@ -1490,7 +1615,7 @@ function serialiseError(err, attempt, retryable) {
|
|
|
1490
1615
|
}
|
|
1491
1616
|
|
|
1492
1617
|
// runtime/subsystems/jobs/job-run-service.memory-backend.ts
|
|
1493
|
-
import { Inject as
|
|
1618
|
+
import { Inject as Inject6, Injectable as Injectable6 } from "@nestjs/common";
|
|
1494
1619
|
var NON_TERMINAL_STATUSES2 = [
|
|
1495
1620
|
"pending",
|
|
1496
1621
|
"running",
|
|
@@ -1657,9 +1782,10 @@ var MemoryJobRunService = class {
|
|
|
1657
1782
|
}
|
|
1658
1783
|
};
|
|
1659
1784
|
MemoryJobRunService = __decorateClass([
|
|
1660
|
-
|
|
1661
|
-
__decorateParam(
|
|
1662
|
-
__decorateParam(
|
|
1785
|
+
Injectable6(),
|
|
1786
|
+
__decorateParam(0, Inject6(MemoryJobStore)),
|
|
1787
|
+
__decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
|
|
1788
|
+
__decorateParam(2, Inject6(JOBS_MULTI_TENANT))
|
|
1663
1789
|
], MemoryJobRunService);
|
|
1664
1790
|
function compareBy(a, b, order) {
|
|
1665
1791
|
switch (order) {
|
|
@@ -1675,120 +1801,6 @@ function compareBy(a, b, order) {
|
|
|
1675
1801
|
}
|
|
1676
1802
|
}
|
|
1677
1803
|
|
|
1678
|
-
// runtime/subsystems/jobs/job-step-service.memory-backend.ts
|
|
1679
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
1680
|
-
import { Injectable as Injectable6 } from "@nestjs/common";
|
|
1681
|
-
var MemoryJobStepService = class {
|
|
1682
|
-
constructor(store) {
|
|
1683
|
-
this.store = store;
|
|
1684
|
-
}
|
|
1685
|
-
store;
|
|
1686
|
-
async findStep(runId, stepId) {
|
|
1687
|
-
const rows = this.store.steps.get(runId);
|
|
1688
|
-
if (!rows) return null;
|
|
1689
|
-
const match = rows.find(
|
|
1690
|
-
(r) => r.stepId === stepId && r.status === "completed"
|
|
1691
|
-
);
|
|
1692
|
-
return match ?? null;
|
|
1693
|
-
}
|
|
1694
|
-
async recordStep(input) {
|
|
1695
|
-
const rows = this.getOrCreateRows(input.jobRunId);
|
|
1696
|
-
const existingIdx = rows.findIndex((r) => r.stepId === input.stepId);
|
|
1697
|
-
const normalisedInput = input.input ?? null;
|
|
1698
|
-
const normalisedOutput = input.output ?? null;
|
|
1699
|
-
if (existingIdx >= 0) {
|
|
1700
|
-
const prev = rows[existingIdx];
|
|
1701
|
-
const next = {
|
|
1702
|
-
...prev,
|
|
1703
|
-
status: input.status,
|
|
1704
|
-
input: normalisedInput ?? prev.input,
|
|
1705
|
-
output: normalisedOutput ?? prev.output,
|
|
1706
|
-
error: input.error ?? prev.error,
|
|
1707
|
-
attempts: input.attempts ?? prev.attempts,
|
|
1708
|
-
startedAt: input.startedAt ?? prev.startedAt,
|
|
1709
|
-
finishedAt: input.finishedAt ?? prev.finishedAt
|
|
1710
|
-
};
|
|
1711
|
-
rows[existingIdx] = next;
|
|
1712
|
-
return next;
|
|
1713
|
-
}
|
|
1714
|
-
const seq = input.seq ?? this.nextSeq(rows);
|
|
1715
|
-
const row = {
|
|
1716
|
-
id: randomUUID3(),
|
|
1717
|
-
jobRunId: input.jobRunId,
|
|
1718
|
-
stepId: input.stepId,
|
|
1719
|
-
kind: input.kind,
|
|
1720
|
-
seq,
|
|
1721
|
-
status: input.status,
|
|
1722
|
-
input: normalisedInput,
|
|
1723
|
-
output: normalisedOutput,
|
|
1724
|
-
error: input.error ?? null,
|
|
1725
|
-
attempts: input.attempts ?? 0,
|
|
1726
|
-
startedAt: input.startedAt ?? null,
|
|
1727
|
-
finishedAt: input.finishedAt ?? null
|
|
1728
|
-
};
|
|
1729
|
-
rows.push(row);
|
|
1730
|
-
return row;
|
|
1731
|
-
}
|
|
1732
|
-
/**
|
|
1733
|
-
* Replay helper — wipe every step row for a run. Mirrors the `scratch`
|
|
1734
|
-
* replay mode of the Drizzle backend (`DELETE FROM job_step WHERE job_run_id = …`).
|
|
1735
|
-
*/
|
|
1736
|
-
clearStepsForRun(runId) {
|
|
1737
|
-
this.store.steps.delete(runId);
|
|
1738
|
-
}
|
|
1739
|
-
/**
|
|
1740
|
-
* Remove every non-`completed` row for the run. Memoized (`completed`)
|
|
1741
|
-
* rows are preserved — this is the `last_checkpoint` / `last_step`
|
|
1742
|
-
* semantics the Drizzle backend implements via
|
|
1743
|
-
* `DELETE … WHERE status != 'completed'`. Both replay modes route here
|
|
1744
|
-
* (Phase 1 collapses `last_step` onto this behaviour; see JOB-3 notes).
|
|
1745
|
-
*/
|
|
1746
|
-
clearIncompleteSteps(runId) {
|
|
1747
|
-
const rows = this.store.steps.get(runId);
|
|
1748
|
-
if (!rows) return;
|
|
1749
|
-
const kept = rows.filter((r) => r.status === "completed");
|
|
1750
|
-
if (kept.length === 0) {
|
|
1751
|
-
this.store.steps.delete(runId);
|
|
1752
|
-
} else {
|
|
1753
|
-
this.store.steps.set(runId, kept);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
getOrCreateRows(runId) {
|
|
1757
|
-
let rows = this.store.steps.get(runId);
|
|
1758
|
-
if (!rows) {
|
|
1759
|
-
rows = [];
|
|
1760
|
-
this.store.steps.set(runId, rows);
|
|
1761
|
-
}
|
|
1762
|
-
return rows;
|
|
1763
|
-
}
|
|
1764
|
-
nextSeq(rows) {
|
|
1765
|
-
let max = 0;
|
|
1766
|
-
for (const r of rows) {
|
|
1767
|
-
if (r.seq > max) max = r.seq;
|
|
1768
|
-
}
|
|
1769
|
-
return max + 1;
|
|
1770
|
-
}
|
|
1771
|
-
};
|
|
1772
|
-
MemoryJobStepService = __decorateClass([
|
|
1773
|
-
Injectable6()
|
|
1774
|
-
], MemoryJobStepService);
|
|
1775
|
-
|
|
1776
|
-
// runtime/subsystems/jobs/memory-job-store.ts
|
|
1777
|
-
var MemoryJobStore = class {
|
|
1778
|
-
/** Runs keyed by `id` (single source of truth for status/scope/lineage). */
|
|
1779
|
-
runs = /* @__PURE__ */ new Map();
|
|
1780
|
-
/** Steps keyed by `job_run_id`; array order matches insertion order. */
|
|
1781
|
-
steps = /* @__PURE__ */ new Map();
|
|
1782
|
-
/** Job definitions keyed by `type` — memory mirror of the `job` table. */
|
|
1783
|
-
jobs = /* @__PURE__ */ new Map();
|
|
1784
|
-
/** Reset everything. Tests call this in `beforeEach`. */
|
|
1785
|
-
clear() {
|
|
1786
|
-
this.runs.clear();
|
|
1787
|
-
this.steps.clear();
|
|
1788
|
-
this.jobs.clear();
|
|
1789
|
-
}
|
|
1790
|
-
};
|
|
1791
|
-
|
|
1792
1804
|
// runtime/subsystems/jobs/pool-config.loader.ts
|
|
1793
1805
|
import { existsSync, readFileSync } from "fs";
|
|
1794
1806
|
import { resolve } from "path";
|
|
@@ -2004,7 +2016,7 @@ JobsDomainModule = __decorateClass([
|
|
|
2004
2016
|
], JobsDomainModule);
|
|
2005
2017
|
|
|
2006
2018
|
// runtime/subsystems/jobs/job-worker.ts
|
|
2007
|
-
import { Inject as
|
|
2019
|
+
import { Inject as Inject7, Injectable as Injectable7, Logger as Logger3 } from "@nestjs/common";
|
|
2008
2020
|
import { and as and4, asc as asc2, desc as desc3, eq as eq4, inArray as inArray3, lt as lt2, lte, sql as sql4 } from "drizzle-orm";
|
|
2009
2021
|
var JOB_WORKER_OPTIONS = Symbol.for(tokenKey("jobs", "worker-options"));
|
|
2010
2022
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
@@ -2391,11 +2403,11 @@ var JobWorker = class {
|
|
|
2391
2403
|
};
|
|
2392
2404
|
JobWorker = __decorateClass([
|
|
2393
2405
|
Injectable7(),
|
|
2394
|
-
__decorateParam(0,
|
|
2395
|
-
__decorateParam(1,
|
|
2396
|
-
__decorateParam(2,
|
|
2397
|
-
__decorateParam(3,
|
|
2398
|
-
__decorateParam(4,
|
|
2406
|
+
__decorateParam(0, Inject7(DRIZZLE)),
|
|
2407
|
+
__decorateParam(1, Inject7(JOB_ORCHESTRATOR)),
|
|
2408
|
+
__decorateParam(2, Inject7(JOB_RUN_SERVICE)),
|
|
2409
|
+
__decorateParam(3, Inject7(JOB_STEP_SERVICE)),
|
|
2410
|
+
__decorateParam(4, Inject7(JOB_WORKER_OPTIONS))
|
|
2399
2411
|
], JobWorker);
|
|
2400
2412
|
|
|
2401
2413
|
// runtime/subsystems/jobs/job-worker.module.ts
|
|
@@ -2593,16 +2605,17 @@ var JobWorkerOrchestrator = class {
|
|
|
2593
2605
|
};
|
|
2594
2606
|
JobWorkerOrchestrator = __decorateClass([
|
|
2595
2607
|
Injectable8(),
|
|
2596
|
-
__decorateParam(0,
|
|
2597
|
-
__decorateParam(1,
|
|
2598
|
-
__decorateParam(2,
|
|
2599
|
-
__decorateParam(3,
|
|
2608
|
+
__decorateParam(0, Inject8(JOB_ORCHESTRATOR)),
|
|
2609
|
+
__decorateParam(1, Inject8(JOB_RUN_SERVICE)),
|
|
2610
|
+
__decorateParam(2, Inject8(JOB_STEP_SERVICE)),
|
|
2611
|
+
__decorateParam(3, Inject8(JOB_WORKER_MODULE_OPTIONS)),
|
|
2600
2612
|
__decorateParam(4, Optional2()),
|
|
2601
|
-
__decorateParam(4,
|
|
2613
|
+
__decorateParam(4, Inject8(DRIZZLE)),
|
|
2614
|
+
__decorateParam(5, Inject8(ModuleRef2)),
|
|
2602
2615
|
__decorateParam(6, Optional2()),
|
|
2603
|
-
__decorateParam(6,
|
|
2616
|
+
__decorateParam(6, Inject8(BULLMQ_CONNECTION)),
|
|
2604
2617
|
__decorateParam(7, Optional2()),
|
|
2605
|
-
__decorateParam(7,
|
|
2618
|
+
__decorateParam(7, Inject8(BULLMQ_RESOLVED_CONFIG))
|
|
2606
2619
|
], JobWorkerOrchestrator);
|
|
2607
2620
|
var JobWorkerModule = class {
|
|
2608
2621
|
static forRoot(opts) {
|