@pattern-stack/codegen 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/{job-orchestrator.protocol-BwsBd37o.d.ts → job-orchestrator.protocol-CHOEqBDk.d.ts} +36 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +2 -2
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +5 -1
- package/dist/runtime/subsystems/bridge/bridge.module.js +930 -275
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +4 -1
- package/dist/runtime/subsystems/bridge/index.js +837 -182
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +3 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +92 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +3 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +99 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-keyset-cursor.d.ts +32 -0
- package/dist/runtime/subsystems/events/event-keyset-cursor.js +38 -0
- package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -0
- package/dist/runtime/subsystems/events/event-read.protocol.d.ts +94 -0
- package/dist/runtime/subsystems/events/event-read.protocol.js +9 -0
- package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -0
- package/dist/runtime/subsystems/events/events.module.js +177 -3
- package/dist/runtime/subsystems/events/events.module.js.map +1 -1
- package/dist/runtime/subsystems/events/events.tokens.d.ts +16 -1
- package/dist/runtime/subsystems/events/events.tokens.js +2 -0
- package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
- package/dist/runtime/subsystems/events/index.d.ts +2 -1
- package/dist/runtime/subsystems/events/index.js +178 -3
- package/dist/runtime/subsystems/events/index.js.map +1 -1
- package/dist/runtime/subsystems/index.d.ts +3 -2
- package/dist/runtime/subsystems/index.js +1194 -264
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +98 -0
- package/dist/runtime/subsystems/jobs/bullmq.config.js +143 -0
- package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -0
- package/dist/runtime/subsystems/jobs/index.d.ts +8 -3
- package/dist/runtime/subsystems/jobs/index.js +861 -201
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-handler.base.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +108 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +922 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +53 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +57 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +4 -2
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +81 -1
- 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 +4 -2
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +81 -0
- 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 +76 -2
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +49 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +374 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +44 -5
- package/dist/runtime/subsystems/jobs/job-worker.module.js +832 -178
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +10 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +519 -20
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/pool-config.loader.d.ts +9 -1
- package/dist/runtime/subsystems/jobs/pool-config.loader.js +4 -0
- package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
- package/dist/runtime/subsystems/observability/index.d.ts +4 -3
- package/dist/runtime/subsystems/observability/index.js +109 -2
- package/dist/runtime/subsystems/observability/index.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.module.js +109 -2
- package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.protocol.d.ts +64 -3
- package/dist/runtime/subsystems/observability/observability.service.d.ts +22 -4
- package/dist/runtime/subsystems/observability/observability.service.js +109 -2
- package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +3 -2
- package/dist/runtime/subsystems/observability/reporters/index.d.ts +3 -2
- package/dist/src/cli/index.js +30 -6
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +5 -1
- package/runtime/subsystems/bridge/bridge.module.ts +5 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +109 -3
- package/runtime/subsystems/events/event-bus.memory-backend.ts +103 -1
- package/runtime/subsystems/events/event-keyset-cursor.ts +59 -0
- package/runtime/subsystems/events/event-read.protocol.ts +97 -0
- package/runtime/subsystems/events/events.module.ts +18 -2
- package/runtime/subsystems/events/events.tokens.ts +16 -0
- package/runtime/subsystems/events/index.ts +7 -0
- package/runtime/subsystems/jobs/bullmq.config.ts +125 -0
- package/runtime/subsystems/jobs/index.ts +22 -0
- package/runtime/subsystems/jobs/job-handler.base.ts +36 -0
- package/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts +381 -0
- package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +88 -0
- package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +59 -1
- package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +53 -0
- package/runtime/subsystems/jobs/job-run-service.protocol.ts +77 -0
- package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +311 -0
- package/runtime/subsystems/jobs/job-worker.module.ts +124 -10
- package/runtime/subsystems/jobs/jobs-domain.module.ts +40 -21
- package/runtime/subsystems/jobs/pool-config.loader.ts +11 -0
- package/runtime/subsystems/observability/index.ts +8 -0
- package/runtime/subsystems/observability/observability.protocol.ts +76 -0
- package/runtime/subsystems/observability/observability.service.ts +148 -1
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -12
- package/templates/relationship/new/prompt.js +8 -5
- package/templates/subsystem/jobs/worker.ejs.t +30 -7
- package/templates/subsystem/sync/sync-audit.schema.ejs.t +12 -16
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { JobRunRow } from './job-orchestration.schema.js';
|
|
2
|
+
import { JobRunSummary } from './job-run-service.protocol.js';
|
|
3
|
+
import 'drizzle-orm/pg-core';
|
|
4
|
+
import 'drizzle-orm';
|
|
5
|
+
import '../../../job-orchestrator.protocol-CHOEqBDk.js';
|
|
6
|
+
import '../events/event-bus.protocol.js';
|
|
7
|
+
import '../../types/drizzle.js';
|
|
8
|
+
import 'drizzle-orm/node-postgres';
|
|
9
|
+
import '@nestjs/common';
|
|
10
|
+
import '../events/generated/types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Keyset (seek) cursor codec for `IJobRunService.listJobRuns` (OBS-LIST-1).
|
|
14
|
+
*
|
|
15
|
+
* The list is ordered `created_at DESC, id DESC`. The cursor encodes the
|
|
16
|
+
* `(createdAt, id)` of the last row on the previous page so the next page
|
|
17
|
+
* can seek with `WHERE (created_at, id) < (cursorCreatedAt, cursorId)`
|
|
18
|
+
* rather than an `OFFSET`. Keyset pagination stays O(log n) on deep pages
|
|
19
|
+
* and is stable as new rows arrive at the head.
|
|
20
|
+
*
|
|
21
|
+
* The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape
|
|
22
|
+
* is an implementation detail — never parse it outside this module.
|
|
23
|
+
*
|
|
24
|
+
* Also hosts `toJobRunSummary`, the single `JobRunRow → JobRunSummary`
|
|
25
|
+
* projection shared by both backends so the narrow shape stays in sync.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
interface JobRunKeyset {
|
|
29
|
+
/** `created_at` of the last row on the previous page. */
|
|
30
|
+
createdAt: Date;
|
|
31
|
+
/** `id` (UUID) tie-break of the last row on the previous page. */
|
|
32
|
+
id: string;
|
|
33
|
+
}
|
|
34
|
+
/** Default page size when `limit` is omitted. */
|
|
35
|
+
declare const DEFAULT_LIST_LIMIT = 50;
|
|
36
|
+
/** Hard upper bound on page size to keep a single read bounded. */
|
|
37
|
+
declare const MAX_LIST_LIMIT = 200;
|
|
38
|
+
/** Clamp a caller-supplied `limit` into `[1, MAX_LIST_LIMIT]`. */
|
|
39
|
+
declare function clampLimit(limit: number | undefined): number;
|
|
40
|
+
declare function encodeKeysetCursor(keyset: JobRunKeyset): string;
|
|
41
|
+
/**
|
|
42
|
+
* Decode an opaque cursor back into its `(createdAt, id)` keyset. Returns
|
|
43
|
+
* `null` for a malformed cursor so callers can treat garbage input as
|
|
44
|
+
* "start from the beginning" rather than throwing on user-supplied data.
|
|
45
|
+
*/
|
|
46
|
+
declare function decodeKeysetCursor(cursor: string): JobRunKeyset | null;
|
|
47
|
+
/**
|
|
48
|
+
* Project a raw `job_run` row into the narrow `JobRunSummary` shape exposed
|
|
49
|
+
* by `listJobRuns`. `errorMessage` is pulled from the jsonb `error.message`.
|
|
50
|
+
*/
|
|
51
|
+
declare function toJobRunSummary(r: JobRunRow): JobRunSummary;
|
|
52
|
+
|
|
53
|
+
export { DEFAULT_LIST_LIMIT, type JobRunKeyset, MAX_LIST_LIMIT, clampLimit, decodeKeysetCursor, encodeKeysetCursor, toJobRunSummary };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// runtime/subsystems/jobs/job-run-keyset-cursor.ts
|
|
2
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
3
|
+
var MAX_LIST_LIMIT = 200;
|
|
4
|
+
function clampLimit(limit) {
|
|
5
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
6
|
+
return DEFAULT_LIST_LIMIT;
|
|
7
|
+
}
|
|
8
|
+
const floored = Math.floor(limit);
|
|
9
|
+
if (floored < 1) return 1;
|
|
10
|
+
if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
|
|
11
|
+
return floored;
|
|
12
|
+
}
|
|
13
|
+
function encodeKeysetCursor(keyset) {
|
|
14
|
+
const tuple = [keyset.createdAt.toISOString(), keyset.id];
|
|
15
|
+
return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
|
|
16
|
+
}
|
|
17
|
+
function decodeKeysetCursor(cursor) {
|
|
18
|
+
try {
|
|
19
|
+
const json = Buffer.from(cursor, "base64url").toString("utf8");
|
|
20
|
+
const parsed = JSON.parse(json);
|
|
21
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) return null;
|
|
22
|
+
const [iso, id] = parsed;
|
|
23
|
+
if (typeof iso !== "string" || typeof id !== "string") return null;
|
|
24
|
+
const createdAt = new Date(iso);
|
|
25
|
+
if (Number.isNaN(createdAt.getTime())) return null;
|
|
26
|
+
return { createdAt, id };
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function toJobRunSummary(r) {
|
|
32
|
+
return {
|
|
33
|
+
runId: r.id,
|
|
34
|
+
rootRunId: r.rootRunId,
|
|
35
|
+
jobType: r.jobType,
|
|
36
|
+
pool: r.pool,
|
|
37
|
+
status: r.status,
|
|
38
|
+
scopeEntityType: r.scopeEntityType,
|
|
39
|
+
scopeEntityId: r.scopeEntityId,
|
|
40
|
+
tenantId: r.tenantId,
|
|
41
|
+
attempts: r.attempts,
|
|
42
|
+
errorMessage: r.error?.message ?? null,
|
|
43
|
+
runAt: r.runAt,
|
|
44
|
+
startedAt: r.startedAt,
|
|
45
|
+
finishedAt: r.finishedAt,
|
|
46
|
+
createdAt: r.createdAt
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
DEFAULT_LIST_LIMIT,
|
|
51
|
+
MAX_LIST_LIMIT,
|
|
52
|
+
clampLimit,
|
|
53
|
+
decodeKeysetCursor,
|
|
54
|
+
encodeKeysetCursor,
|
|
55
|
+
toJobRunSummary
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=job-run-keyset-cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-run-keyset-cursor.ts"],"sourcesContent":["/**\n * Keyset (seek) cursor codec for `IJobRunService.listJobRuns` (OBS-LIST-1).\n *\n * The list is ordered `created_at DESC, id DESC`. The cursor encodes the\n * `(createdAt, id)` of the last row on the previous page so the next page\n * can seek with `WHERE (created_at, id) < (cursorCreatedAt, cursorId)`\n * rather than an `OFFSET`. Keyset pagination stays O(log n) on deep pages\n * and is stable as new rows arrive at the head.\n *\n * The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape\n * is an implementation detail — never parse it outside this module.\n *\n * Also hosts `toJobRunSummary`, the single `JobRunRow → JobRunSummary`\n * projection shared by both backends so the narrow shape stays in sync.\n */\nimport type { JobRunRow } from './job-orchestration.schema';\nimport type { JobRunSummary } from './job-run-service.protocol';\n\nexport interface JobRunKeyset {\n /** `created_at` of the last row on the previous page. */\n createdAt: Date;\n /** `id` (UUID) tie-break of the last row on the previous page. */\n id: string;\n}\n\n/** Default page size when `limit` is omitted. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Hard upper bound on page size to keep a single read bounded. */\nexport const MAX_LIST_LIMIT = 200;\n\n/** Clamp a caller-supplied `limit` into `[1, MAX_LIST_LIMIT]`. */\nexport function clampLimit(limit: number | undefined): number {\n if (typeof limit !== 'number' || !Number.isFinite(limit)) {\n return DEFAULT_LIST_LIMIT;\n }\n const floored = Math.floor(limit);\n if (floored < 1) return 1;\n if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;\n return floored;\n}\n\nexport function encodeKeysetCursor(keyset: JobRunKeyset): string {\n const tuple = [keyset.createdAt.toISOString(), keyset.id];\n return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');\n}\n\n/**\n * Decode an opaque cursor back into its `(createdAt, id)` keyset. Returns\n * `null` for a malformed cursor so callers can treat garbage input as\n * \"start from the beginning\" rather than throwing on user-supplied data.\n */\nexport function decodeKeysetCursor(cursor: string): JobRunKeyset | null {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf8');\n const parsed = JSON.parse(json) as unknown;\n if (!Array.isArray(parsed) || parsed.length !== 2) return null;\n const [iso, id] = parsed;\n if (typeof iso !== 'string' || typeof id !== 'string') return null;\n const createdAt = new Date(iso);\n if (Number.isNaN(createdAt.getTime())) return null;\n return { createdAt, id };\n } catch {\n return null;\n }\n}\n\n/**\n * Project a raw `job_run` row into the narrow `JobRunSummary` shape exposed\n * by `listJobRuns`. `errorMessage` is pulled from the jsonb `error.message`.\n */\nexport function toJobRunSummary(r: JobRunRow): JobRunSummary {\n return {\n runId: r.id,\n rootRunId: r.rootRunId,\n jobType: r.jobType,\n pool: r.pool,\n status: r.status,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n runAt: r.runAt,\n startedAt: r.startedAt,\n finishedAt: r.finishedAt,\n createdAt: r.createdAt,\n };\n}\n"],"mappings":";AA0BO,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,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;","names":[]}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { DrizzleClient } from '../../types/drizzle.js';
|
|
2
|
-
import { I as IJobOrchestrator, i as JobRun } from '../../../job-orchestrator.protocol-
|
|
3
|
-
import { IJobRunService, ListForScopeOptions, CancelForScopeOptions, RescheduleForScopeOptions, PoolStatusCount, JobRunFailure } from './job-run-service.protocol.js';
|
|
2
|
+
import { I as IJobOrchestrator, i as JobRun } from '../../../job-orchestrator.protocol-CHOEqBDk.js';
|
|
3
|
+
import { IJobRunService, ListForScopeOptions, CancelForScopeOptions, RescheduleForScopeOptions, PoolStatusCount, JobRunFailure, ListJobRunsQuery, JobRunPage } from './job-run-service.protocol.js';
|
|
4
4
|
import 'drizzle-orm/node-postgres';
|
|
5
5
|
import '../events/event-bus.protocol.js';
|
|
6
6
|
import './job-orchestration.schema.js';
|
|
7
7
|
import 'drizzle-orm/pg-core';
|
|
8
8
|
import 'drizzle-orm';
|
|
9
9
|
import '@nestjs/common';
|
|
10
|
+
import '../events/generated/types.js';
|
|
10
11
|
|
|
11
12
|
declare class DrizzleJobRunService implements IJobRunService {
|
|
12
13
|
private readonly db;
|
|
@@ -25,6 +26,7 @@ declare class DrizzleJobRunService implements IJobRunService {
|
|
|
25
26
|
rescheduleForScope(entityType: string, entityId: string, newRunAt: Date, opts?: RescheduleForScopeOptions): Promise<void>;
|
|
26
27
|
countByPoolAndStatus(tenantId?: string | null): Promise<PoolStatusCount[]>;
|
|
27
28
|
listRecentFailed(limit: number, tenantId?: string | null): Promise<JobRunFailure[]>;
|
|
29
|
+
listJobRuns(query?: ListJobRunsQuery): Promise<JobRunPage>;
|
|
28
30
|
/**
|
|
29
31
|
* Internal helper used by cascade paths (not on the public protocol).
|
|
30
32
|
* Exposed as a public method on the concrete class so infrastructure
|
|
@@ -12,7 +12,7 @@ var __decorateParam = (index2, decorator) => (target, key) => decorator(target,
|
|
|
12
12
|
|
|
13
13
|
// runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
|
|
14
14
|
import { Inject, Injectable } from "@nestjs/common";
|
|
15
|
-
import { and, asc, desc, eq, inArray, isNull, sql as sql2 } from "drizzle-orm";
|
|
15
|
+
import { and, asc, desc, eq, gte, inArray, isNull, lt, or, sql as sql2 } from "drizzle-orm";
|
|
16
16
|
|
|
17
17
|
// runtime/constants/tokens.ts
|
|
18
18
|
var DRIZZLE = "DRIZZLE";
|
|
@@ -171,6 +171,55 @@ var jobSteps = pgTable(
|
|
|
171
171
|
})
|
|
172
172
|
);
|
|
173
173
|
|
|
174
|
+
// runtime/subsystems/jobs/job-run-keyset-cursor.ts
|
|
175
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
176
|
+
var MAX_LIST_LIMIT = 200;
|
|
177
|
+
function clampLimit(limit) {
|
|
178
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
179
|
+
return DEFAULT_LIST_LIMIT;
|
|
180
|
+
}
|
|
181
|
+
const floored = Math.floor(limit);
|
|
182
|
+
if (floored < 1) return 1;
|
|
183
|
+
if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
|
|
184
|
+
return floored;
|
|
185
|
+
}
|
|
186
|
+
function encodeKeysetCursor(keyset) {
|
|
187
|
+
const tuple = [keyset.createdAt.toISOString(), keyset.id];
|
|
188
|
+
return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
|
|
189
|
+
}
|
|
190
|
+
function decodeKeysetCursor(cursor) {
|
|
191
|
+
try {
|
|
192
|
+
const json = Buffer.from(cursor, "base64url").toString("utf8");
|
|
193
|
+
const parsed = JSON.parse(json);
|
|
194
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) return null;
|
|
195
|
+
const [iso, id] = parsed;
|
|
196
|
+
if (typeof iso !== "string" || typeof id !== "string") return null;
|
|
197
|
+
const createdAt = new Date(iso);
|
|
198
|
+
if (Number.isNaN(createdAt.getTime())) return null;
|
|
199
|
+
return { createdAt, id };
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function toJobRunSummary(r) {
|
|
205
|
+
return {
|
|
206
|
+
runId: r.id,
|
|
207
|
+
rootRunId: r.rootRunId,
|
|
208
|
+
jobType: r.jobType,
|
|
209
|
+
pool: r.pool,
|
|
210
|
+
status: r.status,
|
|
211
|
+
scopeEntityType: r.scopeEntityType,
|
|
212
|
+
scopeEntityId: r.scopeEntityId,
|
|
213
|
+
tenantId: r.tenantId,
|
|
214
|
+
attempts: r.attempts,
|
|
215
|
+
errorMessage: r.error?.message ?? null,
|
|
216
|
+
runAt: r.runAt,
|
|
217
|
+
startedAt: r.startedAt,
|
|
218
|
+
finishedAt: r.finishedAt,
|
|
219
|
+
createdAt: r.createdAt
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
174
223
|
// runtime/subsystems/jobs/jobs-domain.tokens.ts
|
|
175
224
|
var JOB_ORCHESTRATOR = /* @__PURE__ */ Symbol("JOB_ORCHESTRATOR");
|
|
176
225
|
var JOBS_MULTI_TENANT = /* @__PURE__ */ Symbol("JOBS_MULTI_TENANT");
|
|
@@ -310,6 +359,37 @@ var DrizzleJobRunService = class {
|
|
|
310
359
|
createdAt: r.createdAt
|
|
311
360
|
}));
|
|
312
361
|
}
|
|
362
|
+
async listJobRuns(query = {}) {
|
|
363
|
+
const limit = clampLimit(query.limit);
|
|
364
|
+
const conditions = [];
|
|
365
|
+
const tenantCond = this.tenantCondition("listJobRuns", query.tenantId);
|
|
366
|
+
if (tenantCond) conditions.push(tenantCond);
|
|
367
|
+
if (query.poolId) conditions.push(eq(jobRuns.pool, query.poolId));
|
|
368
|
+
if (query.rootRunId) conditions.push(eq(jobRuns.rootRunId, query.rootRunId));
|
|
369
|
+
if (query.status) conditions.push(eq(jobRuns.status, query.status));
|
|
370
|
+
if (query.since) conditions.push(gte(jobRuns.createdAt, query.since));
|
|
371
|
+
if (query.cursor) {
|
|
372
|
+
const keyset = decodeKeysetCursor(query.cursor);
|
|
373
|
+
if (keyset) {
|
|
374
|
+
conditions.push(
|
|
375
|
+
or(
|
|
376
|
+
lt(jobRuns.createdAt, keyset.createdAt),
|
|
377
|
+
and(
|
|
378
|
+
eq(jobRuns.createdAt, keyset.createdAt),
|
|
379
|
+
lt(jobRuns.id, keyset.id)
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const rows = await this.db.select().from(jobRuns).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(desc(jobRuns.createdAt), desc(jobRuns.id)).limit(limit + 1);
|
|
386
|
+
const hasMore = rows.length > limit;
|
|
387
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
388
|
+
const items = page.map(toJobRunSummary);
|
|
389
|
+
const last = page[page.length - 1];
|
|
390
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
|
|
391
|
+
return { items, nextCursor };
|
|
392
|
+
}
|
|
313
393
|
/**
|
|
314
394
|
* Internal helper used by cascade paths (not on the public protocol).
|
|
315
395
|
* Exposed as a public method on the concrete class so infrastructure
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-run-service.drizzle-backend.ts","../../../../runtime/constants/tokens.ts","../../../../runtime/subsystems/jobs/job-orchestration.schema.ts","../../../../runtime/subsystems/jobs/jobs-domain.tokens.ts","../../../../runtime/subsystems/jobs/jobs-errors.ts"],"sourcesContent":["/**\n * DrizzleJobRunService — scope-oriented reads and bulk operations against\n * `job_run` (ADR-022, JOB-3).\n *\n * Separate from the orchestrator because the access pattern differs: this\n * service scans by `(scope_entity_type, scope_entity_id)` via\n * `idx_job_run_scope`, whereas orchestrator mutates individual runs by id.\n */\nimport { Inject, Injectable } from '@nestjs/common';\nimport { and, asc, desc, eq, inArray, isNull, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, type JobRunRow } from './job-orchestration.schema';\nimport type { JobRun } from './job-orchestrator.protocol';\nimport type {\n IJobRunService,\n ListForScopeOptions,\n CancelForScopeOptions,\n RescheduleForScopeOptions,\n PoolStatusCount,\n JobRunFailure,\n} from './job-run-service.protocol';\nimport type { IJobOrchestrator } from './job-orchestrator.protocol';\nimport { JOB_ORCHESTRATOR, JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport { MissingTenantIdError } from './jobs-errors';\n\nconst NON_TERMINAL_STATUSES: JobRunRow['status'][] = [\n 'pending',\n 'running',\n 'waiting',\n];\n\n@Injectable()\nexport class DrizzleJobRunService implements IJobRunService {\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n ) {}\n\n /**\n * JOB-8 — produce the tenant WHERE fragment (or `null` to opt out).\n * Returns `null` when multi-tenancy is off (caller skips the predicate).\n * Throws `MissingTenantIdError` when on + `undefined`.\n * When on + explicit `null`, filters `tenant_id IS NULL`.\n */\n private tenantCondition(\n method: string,\n tenantId: string | null | undefined,\n ) {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return tenantId === null\n ? isNull(jobRuns.tenantId)\n : eq(jobRuns.tenantId, tenantId);\n }\n\n async listForScope(\n entityType: string,\n entityId: string,\n opts: ListForScopeOptions = {},\n ): Promise<JobRun[]> {\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n ];\n const tenantCond = this.tenantCondition('listForScope', opts.tenantId);\n if (tenantCond) conditions.push(tenantCond);\n if (opts.status) {\n if (Array.isArray(opts.status)) {\n conditions.push(inArray(jobRuns.status, opts.status));\n } else {\n conditions.push(eq(jobRuns.status, opts.status));\n }\n }\n if (opts.jobType) {\n conditions.push(eq(jobRuns.jobType, opts.jobType));\n }\n\n const orderCol = (() => {\n switch (opts.orderBy) {\n case 'created_at asc':\n return asc(jobRuns.createdAt);\n case 'run_at desc':\n return desc(jobRuns.runAt);\n case 'run_at asc':\n return asc(jobRuns.runAt);\n case 'created_at desc':\n default:\n return desc(jobRuns.createdAt);\n }\n })();\n\n let q = this.db\n .select()\n .from(jobRuns)\n .where(and(...conditions))\n .orderBy(orderCol)\n .$dynamic();\n\n if (typeof opts.limit === 'number') {\n q = q.limit(opts.limit);\n }\n if (typeof opts.offset === 'number') {\n q = q.offset(opts.offset);\n }\n\n const rows = await q;\n return rows as JobRun[];\n }\n\n async cancelForScope(\n entityType: string,\n entityId: string,\n opts: CancelForScopeOptions = {},\n ): Promise<void> {\n const tenantCond = this.tenantCondition('cancelForScope', opts.tenantId);\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n inArray(jobRuns.status, NON_TERMINAL_STATUSES),\n ];\n if (tenantCond) conditions.push(tenantCond);\n\n const rows = await this.db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(and(...conditions));\n\n for (const { id } of rows) {\n // Propagate the tenant gate into cascade-cancel. The scope query has\n // already narrowed to this tenant; passing `tenantId` through keeps\n // the orchestrator's per-row guard consistent under multi-tenant mode.\n await this.orchestrator.cancel(id, {\n cascade: true,\n tenantId: opts.tenantId,\n });\n }\n }\n\n async rescheduleForScope(\n entityType: string,\n entityId: string,\n newRunAt: Date,\n opts: RescheduleForScopeOptions = {},\n ): Promise<void> {\n const tenantCond = this.tenantCondition('rescheduleForScope', opts.tenantId);\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n eq(jobRuns.status, 'pending'),\n ];\n if (tenantCond) conditions.push(tenantCond);\n\n await this.db\n .update(jobRuns)\n .set({ runAt: newRunAt, updatedAt: new Date() })\n .where(and(...conditions));\n }\n\n async countByPoolAndStatus(\n tenantId?: string | null,\n ): Promise<PoolStatusCount[]> {\n const tenantCond = this.tenantCondition('countByPoolAndStatus', tenantId);\n const rows = await this.db\n .select({\n pool: jobRuns.pool,\n status: jobRuns.status,\n count: sql<number>`count(*)::int`.as('count'),\n })\n .from(jobRuns)\n .where(tenantCond ?? undefined)\n .groupBy(jobRuns.pool, jobRuns.status);\n\n return rows.map((r) => ({\n pool: r.pool,\n status: r.status,\n count: Number(r.count),\n }));\n }\n\n async listRecentFailed(\n limit: number,\n tenantId?: string | null,\n ): Promise<JobRunFailure[]> {\n const conditions = [eq(jobRuns.status, 'failed' as const)];\n const tenantCond = this.tenantCondition('listRecentFailed', tenantId);\n if (tenantCond) conditions.push(tenantCond);\n\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(and(...conditions))\n .orderBy(desc(jobRuns.finishedAt), desc(jobRuns.updatedAt))\n .limit(limit);\n\n return rows.map((r) => ({\n runId: r.id,\n jobType: r.jobType,\n pool: r.pool,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n failedAt: r.finishedAt ?? r.updatedAt,\n createdAt: r.createdAt,\n }));\n }\n\n /**\n * Internal helper used by cascade paths (not on the public protocol).\n * Exposed as a public method on the concrete class so infrastructure\n * code (cascade tests, debug tools) can call it without a cast.\n */\n async findByRootRunId(rootRunId: string): Promise<JobRun[]> {\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.rootRunId, rootRunId));\n return rows as JobRun[];\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n","/**\n * Drizzle schema for the job orchestration domain (ADR-022).\n *\n * Three tables model the lifecycle of a durable job:\n * - `job` — definitions keyed by handler type (e.g. 'onboarding').\n * - `job_run` — one row per attempt to execute a job; worker claims\n * rows directly via SELECT ... FOR UPDATE SKIP LOCKED.\n * - `job_step` — individual steps within a run; memoises output for replay.\n *\n * Phase 1 ships only this layer. There is no `job_queue` table, no executor\n * port — see ADR-022 and `.claude/skills/jobs/SKILL.md` for the rationale.\n */\nimport {\n pgEnum,\n pgTable,\n uuid,\n text,\n jsonb,\n integer,\n timestamp,\n index,\n uniqueIndex,\n} from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport type { InferSelectModel } from 'drizzle-orm';\n\n// ─── Internal $type<> helpers ───────────────────────────────────────────────\n// Annotation types for jsonb columns only. JOB-2 defines the public protocol\n// types; these remain private to this file.\n\ntype RetryPolicy = {\n attempts: number;\n backoff: 'fixed' | 'exponential';\n baseMs: number;\n nonRetryableErrors?: string[];\n};\n\ntype JobRunError = {\n message: string;\n stack?: string;\n retryable: boolean;\n attempt: number;\n};\n\n// ─── Enums ──────────────────────────────────────────────────────────────────\n\nexport const jobRunStatusEnum = pgEnum('job_run_status', [\n 'pending',\n 'running',\n 'waiting',\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n]);\n\n// extended in ADR-027: tool_call | llm_call | wait | checkpoint | message\nexport const jobStepKindEnum = pgEnum('job_step_kind', ['task']);\n\nexport const jobStepStatusEnum = pgEnum('job_step_status', [\n 'pending',\n 'running',\n 'completed',\n 'failed',\n 'skipped',\n]);\n\nexport const collisionModeEnum = pgEnum('job_collision_mode', [\n 'queue',\n 'reject',\n 'replace',\n]);\n\nexport const replayFromEnum = pgEnum('job_replay_from', [\n 'scratch',\n 'last_step',\n 'last_checkpoint',\n]);\n\nexport const parentClosePolicyEnum = pgEnum('job_parent_close_policy', [\n 'terminate',\n 'cancel',\n 'abandon',\n]);\n\n// Phase 3 placeholder — see ADR-025\nexport const waitKindEnum = pgEnum('job_wait_kind', ['signal']);\n\n// Phase 2 may add more sources; requires Atlas migration\nexport const triggerSourceEnum = pgEnum('job_trigger_source', [\n 'manual',\n 'schedule',\n 'event',\n 'parent',\n]);\n\n// ─── job ────────────────────────────────────────────────────────────────────\n\nexport const jobs = pgTable('job', {\n type: text('type').primaryKey(),\n version: integer('version').notNull().default(1),\n pool: text('pool').notNull(),\n scopeEntityType: text('scope_entity_type'),\n retryPolicy: jsonb('retry_policy').notNull().$type<RetryPolicy>(),\n timeoutMs: integer('timeout_ms'),\n concurrencyKeyTemplate: text('concurrency_key_template'),\n collisionMode: collisionModeEnum('collision_mode').notNull().default('queue'),\n dedupeKeyTemplate: text('dedupe_key_template'),\n dedupeWindowMs: integer('dedupe_window_ms'),\n priorityDefault: integer('priority_default').notNull().default(0),\n replayFrom: replayFromEnum('replay_from').notNull().default('last_checkpoint'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n});\n\nexport type JobDefinitionRow = InferSelectModel<typeof jobs>;\n\n// ─── job_run ────────────────────────────────────────────────────────────────\n\nexport const jobRuns = pgTable(\n 'job_run',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobType: text('job_type').notNull().references(() => jobs.type),\n jobVersion: integer('job_version').notNull(),\n parentRunId: uuid('parent_run_id').references((): any => jobRuns.id),\n /**\n * Service generates `id` client-side via randomUUID() and sets\n * root_run_id = id for root runs (single INSERT, no self-FK race).\n */\n rootRunId: uuid('root_run_id').notNull(),\n parentClosePolicy: parentClosePolicyEnum('parent_close_policy')\n .notNull()\n .default('terminate'),\n scopeEntityType: text('scope_entity_type'),\n scopeEntityId: text('scope_entity_id'),\n tenantId: text('tenant_id'),\n tags: jsonb('tags').notNull().default({}).$type<Record<string, string>>(),\n pool: text('pool').notNull(),\n priority: integer('priority').notNull().default(0),\n concurrencyKey: text('concurrency_key'),\n dedupeKey: text('dedupe_key'),\n status: jobRunStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').notNull().$type<Record<string, unknown>>(),\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n triggerSource: triggerSourceEnum('trigger_source').notNull(),\n triggerRef: text('trigger_ref'),\n runAt: timestamp('run_at', { withTimezone: true }).notNull().defaultNow(),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n claimedAt: timestamp('claimed_at', { withTimezone: true }),\n attempts: integer('attempts').notNull().default(0),\n // Phase 3 placeholder — see ADR-025\n waitKind: waitKindEnum('wait_kind'),\n // Phase 3 placeholder — see ADR-025\n resumeToken: text('resume_token'),\n // Phase 3 placeholder — see ADR-025\n waitDeadline: timestamp('wait_deadline', { withTimezone: true }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n /** Claim query: ORDER BY priority DESC, run_at ASC. */\n idxJobRunClaim: index('idx_job_run_claim').on(t.status, t.pool, t.runAt),\n /** Tree traversal / cascade cancel. */\n idxJobRunRoot: index('idx_job_run_root').on(t.rootRunId),\n /** listForScope query. */\n idxJobRunScope: index('idx_job_run_scope').on(t.scopeEntityType, t.scopeEntityId),\n /** Idempotency collapse — partial index. */\n idxJobRunDedupe: index('idx_job_run_dedupe')\n .on(t.jobType, t.dedupeKey)\n .where(sql`${t.dedupeKey} IS NOT NULL`),\n /** Collision check — partial index. */\n idxJobRunConcurrency: index('idx_job_run_concurrency')\n .on(t.concurrencyKey)\n .where(\n sql`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`,\n ),\n }),\n);\n\nexport type JobRunRow = InferSelectModel<typeof jobRuns>;\n\n// ─── job_step ───────────────────────────────────────────────────────────────\n\nexport const jobSteps = pgTable(\n 'job_step',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobRunId: uuid('job_run_id').notNull().references(() => jobRuns.id),\n stepId: text('step_id').notNull(),\n kind: jobStepKindEnum('kind').notNull().default('task'),\n /**\n * Monotonic within run. integer (max ~2B per run) is sufficient —\n * downgraded from ADR-022's bigint; revisit only if a single run\n * ever exceeds 2 billion steps.\n */\n seq: integer('seq').notNull(),\n status: jobStepStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').$type<Record<string, unknown>>(),\n /** Memoised on success for replay. */\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n attempts: integer('attempts').notNull().default(0),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n },\n (t) => ({\n /** No duplicate step IDs per run. */\n idxJobStepRunStep: uniqueIndex('idx_job_step_run_step').on(t.jobRunId, t.stepId),\n /** Ordered timeline reads. */\n idxJobStepTimeline: index('idx_job_step_timeline').on(t.jobRunId, t.seq),\n }),\n);\n\nexport type JobStepRow = InferSelectModel<typeof jobSteps>;\n","/**\n * Injection tokens for the job orchestration domain layer (ADR-022, JOB-2).\n *\n * Consumer code injects these symbols via `@Inject(JOB_ORCHESTRATOR)` etc.;\n * concrete backends (JOB-3 Drizzle, JOB-4 Memory) provide the implementations\n * through `JobsDomainModule.forRoot({ backend })` in JOB-5.\n *\n * Each token is a unique `Symbol` — guaranteed distinct from every other\n * Symbol at runtime, which is exactly the uniqueness guarantee Nest's DI\n * container relies on for token-based lookup.\n */\nexport const JOB_ORCHESTRATOR = Symbol('JOB_ORCHESTRATOR');\nexport const JOB_RUN_SERVICE = Symbol('JOB_RUN_SERVICE');\nexport const JOB_STEP_SERVICE = Symbol('JOB_STEP_SERVICE');\n\n/**\n * Multi-tenancy opt-in flag (JOB-8). Bound to the boolean passed in via\n * `JobsDomainModule.forRoot({ multiTenant })`, defaulting to `false`.\n *\n * When `true`, the four service-layer backends (Drizzle + Memory orchestrator\n * and run-service) enforce `tenantId` on every mutating / targeted-read call:\n * `start`, `cancel`, `listForScope`, `cancelForScope`, `rescheduleForScope`.\n * Missing (`undefined`) `tenantId` throws `MissingTenantIdError`; explicit\n * `null` opts into cross-tenant background work and passes through.\n *\n * The JobWorker claim loop is **cross-tenant by design** — the worker has no\n * tenant context; `tenantId` is populated at write time and enforced on\n * targeted reads. See docs/specs/JOB-8.md.\n */\nexport const JOBS_MULTI_TENANT = Symbol('JOBS_MULTI_TENANT');\n","/**\n * Typed errors for the job orchestration domain (ADR-022, JOB-3).\n *\n * All thrown by the Drizzle orchestrator (and mirrored by the Memory\n * backend in JOB-4). They exist as classes so consumers can `instanceof`\n * them in catch blocks and exception filters can map them to HTTP codes.\n */\nimport type { JobRun } from './job-orchestrator.protocol';\n\n/**\n * `start(type, …)` was called for a job type that has no row in the `job`\n * table. At runtime this usually means the handler was not decorated or the\n * boot validator (JOB-5) has not registered it yet.\n */\nexport class JobTypeNotFoundError extends Error {\n override readonly name = 'JobTypeNotFoundError';\n constructor(public readonly jobType: string) {\n super(`No job definition registered for type '${jobType}'.`);\n }\n}\n\n/**\n * Thrown by `start` when `collision_mode === 'reject'` and a non-terminal\n * run with the same `concurrency_key` already exists. Carries the incumbent\n * so callers can surface its id or subscribe to its completion event.\n */\nexport class JobCollisionError extends Error {\n override readonly name = 'JobCollisionError';\n constructor(\n public readonly jobType: string,\n public readonly concurrencyKey: string,\n public readonly incumbent: JobRun,\n ) {\n super(\n `Job type '${jobType}' has an in-flight run with concurrency_key ` +\n `'${concurrencyKey}' (incumbent ${incumbent.id}); collision_mode=reject.`,\n );\n }\n}\n\n/**\n * `replay` was called on a run that is not in a replayable terminal state\n * (i.e. still `pending` / `running` / `waiting`). Replay always spawns\n * fresh execution and therefore requires the source run to be settled.\n */\nexport class JobNotReplayableError extends Error {\n override readonly name = 'JobNotReplayableError';\n constructor(\n public readonly runId: string,\n public readonly currentStatus: string,\n ) {\n super(\n `Run ${runId} is not replayable from status '${currentStatus}'. ` +\n `Only 'completed', 'failed', 'timed_out', and 'canceled' are eligible.`,\n );\n }\n}\n\n/**\n * A `concurrency_key_template` or `dedupe_key_template` referenced a field\n * that is not present on the input payload. Caught at `start` time so the\n * caller sees the misconfiguration synchronously rather than at claim time.\n */\nexport class JobTemplateFieldMissingError extends Error {\n override readonly name = 'JobTemplateFieldMissingError';\n constructor(\n public readonly template: string,\n public readonly field: string,\n ) {\n super(\n `Template '${template}' references input field '${field}' which is ` +\n `missing or undefined on the payload.`,\n );\n }\n}\n\n/**\n * Thrown by the four multi-tenant-aware service-layer backends (JOB-8)\n * when `JobsDomainModule` was configured with `multiTenant: true` but the\n * caller did not pass a `tenantId` in the relevant options object.\n *\n * **Strict enforcement rationale (resolved 2026-04-18).** Cross-tenant data\n * leakage is the worst class of bug a multi-tenant system can ship; surfacing\n * the misuse loudly at the call site (rather than silently defaulting to\n * `null` or to the \"last tenant seen\") prevents both accidental global\n * writes and sneaky reads that return a union of tenants.\n *\n * - `undefined` `tenantId` → throw this error.\n * - Explicit `null` `tenantId` → passes; opts the call into cross-tenant\n * background work (e.g. a nightly housekeeping job that must scan all\n * tenants). The row is persisted with `tenant_id = NULL`.\n */\nexport class MissingTenantIdError extends Error {\n override readonly name = 'MissingTenantIdError';\n constructor(public readonly method: string) {\n super(\n `MissingTenantIdError: JobsDomainModule was configured with ` +\n `multiTenant=true but ${method} was called without tenantId ` +\n `(undefined). Pass an explicit tenantId, or pass null for ` +\n `cross-tenant work.`,\n );\n }\n}\n\n/**\n * Thrown by `JobWorkerModule.onModuleInit` (Drizzle backend only) when the\n * `job` table contains type rows for which no `@JobHandler` is registered\n * in the running process. Surfaces every orphaned type at once so a single\n * boot tells the operator everything to clean up.\n *\n * Skipped entirely in memory mode (Q4 resolution 2026-04-19) — the memory\n * backend has no DB rows to validate; `MemoryJobOrchestrator.start()`\n * throws `JobTypeNotFoundError` synchronously for unknown types instead.\n */\nexport class BootValidationError extends Error {\n override readonly name = 'BootValidationError';\n constructor(public readonly missingHandlers: string[]) {\n super(\n `BootValidationError: ${missingHandlers.length} orphaned job type(s) ` +\n `in 'job' table with no matching @JobHandler in the running process: ` +\n `[${missingHandlers.join(', ')}]. Either register the handler(s) or ` +\n `remove the rows.`,\n );\n }\n}\n\n/**\n * Thrown by `JobWorkerModule.onModuleInit` when one or more `@JobHandler`\n * classes target a `reserved: true` pool from the resolved pool config\n * (the three `events_*` pools are reserved for the events subsystem\n * outbox drain). Listing every offender on a single boot avoids the\n * fix-one-restart-fix-next loop.\n */\nexport class ReservedPoolViolationError extends Error {\n override readonly name = 'ReservedPoolViolationError';\n constructor(\n public readonly offenders: ReadonlyArray<{\n handlerClass: string;\n pool: string;\n }>,\n ) {\n super(\n `ReservedPoolViolationError: ${offenders.length} @JobHandler(s) target ` +\n `reserved pools — reserved pools are framework-only:\\n` +\n offenders\n .map((o) => ` - ${o.handlerClass} → pool='${o.pool}'`)\n .join('\\n'),\n );\n }\n}\n"],"mappings":"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","names":["sql","sql"]}
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-run-service.drizzle-backend.ts","../../../../runtime/constants/tokens.ts","../../../../runtime/subsystems/jobs/job-orchestration.schema.ts","../../../../runtime/subsystems/jobs/job-run-keyset-cursor.ts","../../../../runtime/subsystems/jobs/jobs-domain.tokens.ts","../../../../runtime/subsystems/jobs/jobs-errors.ts"],"sourcesContent":["/**\n * DrizzleJobRunService — scope-oriented reads and bulk operations against\n * `job_run` (ADR-022, JOB-3).\n *\n * Separate from the orchestrator because the access pattern differs: this\n * service scans by `(scope_entity_type, scope_entity_id)` via\n * `idx_job_run_scope`, whereas orchestrator mutates individual runs by id.\n */\nimport { Inject, Injectable } from '@nestjs/common';\nimport { and, asc, desc, eq, gte, inArray, isNull, lt, or, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, type JobRunRow } from './job-orchestration.schema';\nimport type { JobRun } from './job-orchestrator.protocol';\nimport type {\n IJobRunService,\n ListForScopeOptions,\n CancelForScopeOptions,\n RescheduleForScopeOptions,\n PoolStatusCount,\n JobRunFailure,\n ListJobRunsQuery,\n JobRunPage,\n JobRunSummary,\n} from './job-run-service.protocol';\nimport {\n clampLimit,\n decodeKeysetCursor,\n encodeKeysetCursor,\n toJobRunSummary,\n} from './job-run-keyset-cursor';\nimport type { IJobOrchestrator } from './job-orchestrator.protocol';\nimport { JOB_ORCHESTRATOR, JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport { MissingTenantIdError } from './jobs-errors';\n\nconst NON_TERMINAL_STATUSES: JobRunRow['status'][] = [\n 'pending',\n 'running',\n 'waiting',\n];\n\n@Injectable()\nexport class DrizzleJobRunService implements IJobRunService {\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n ) {}\n\n /**\n * JOB-8 — produce the tenant WHERE fragment (or `null` to opt out).\n * Returns `null` when multi-tenancy is off (caller skips the predicate).\n * Throws `MissingTenantIdError` when on + `undefined`.\n * When on + explicit `null`, filters `tenant_id IS NULL`.\n */\n private tenantCondition(\n method: string,\n tenantId: string | null | undefined,\n ) {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return tenantId === null\n ? isNull(jobRuns.tenantId)\n : eq(jobRuns.tenantId, tenantId);\n }\n\n async listForScope(\n entityType: string,\n entityId: string,\n opts: ListForScopeOptions = {},\n ): Promise<JobRun[]> {\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n ];\n const tenantCond = this.tenantCondition('listForScope', opts.tenantId);\n if (tenantCond) conditions.push(tenantCond);\n if (opts.status) {\n if (Array.isArray(opts.status)) {\n conditions.push(inArray(jobRuns.status, opts.status));\n } else {\n conditions.push(eq(jobRuns.status, opts.status));\n }\n }\n if (opts.jobType) {\n conditions.push(eq(jobRuns.jobType, opts.jobType));\n }\n\n const orderCol = (() => {\n switch (opts.orderBy) {\n case 'created_at asc':\n return asc(jobRuns.createdAt);\n case 'run_at desc':\n return desc(jobRuns.runAt);\n case 'run_at asc':\n return asc(jobRuns.runAt);\n case 'created_at desc':\n default:\n return desc(jobRuns.createdAt);\n }\n })();\n\n let q = this.db\n .select()\n .from(jobRuns)\n .where(and(...conditions))\n .orderBy(orderCol)\n .$dynamic();\n\n if (typeof opts.limit === 'number') {\n q = q.limit(opts.limit);\n }\n if (typeof opts.offset === 'number') {\n q = q.offset(opts.offset);\n }\n\n const rows = await q;\n return rows as JobRun[];\n }\n\n async cancelForScope(\n entityType: string,\n entityId: string,\n opts: CancelForScopeOptions = {},\n ): Promise<void> {\n const tenantCond = this.tenantCondition('cancelForScope', opts.tenantId);\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n inArray(jobRuns.status, NON_TERMINAL_STATUSES),\n ];\n if (tenantCond) conditions.push(tenantCond);\n\n const rows = await this.db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(and(...conditions));\n\n for (const { id } of rows) {\n // Propagate the tenant gate into cascade-cancel. The scope query has\n // already narrowed to this tenant; passing `tenantId` through keeps\n // the orchestrator's per-row guard consistent under multi-tenant mode.\n await this.orchestrator.cancel(id, {\n cascade: true,\n tenantId: opts.tenantId,\n });\n }\n }\n\n async rescheduleForScope(\n entityType: string,\n entityId: string,\n newRunAt: Date,\n opts: RescheduleForScopeOptions = {},\n ): Promise<void> {\n const tenantCond = this.tenantCondition('rescheduleForScope', opts.tenantId);\n const conditions = [\n eq(jobRuns.scopeEntityType, entityType),\n eq(jobRuns.scopeEntityId, entityId),\n eq(jobRuns.status, 'pending'),\n ];\n if (tenantCond) conditions.push(tenantCond);\n\n await this.db\n .update(jobRuns)\n .set({ runAt: newRunAt, updatedAt: new Date() })\n .where(and(...conditions));\n }\n\n async countByPoolAndStatus(\n tenantId?: string | null,\n ): Promise<PoolStatusCount[]> {\n const tenantCond = this.tenantCondition('countByPoolAndStatus', tenantId);\n const rows = await this.db\n .select({\n pool: jobRuns.pool,\n status: jobRuns.status,\n count: sql<number>`count(*)::int`.as('count'),\n })\n .from(jobRuns)\n .where(tenantCond ?? undefined)\n .groupBy(jobRuns.pool, jobRuns.status);\n\n return rows.map((r) => ({\n pool: r.pool,\n status: r.status,\n count: Number(r.count),\n }));\n }\n\n async listRecentFailed(\n limit: number,\n tenantId?: string | null,\n ): Promise<JobRunFailure[]> {\n const conditions = [eq(jobRuns.status, 'failed' as const)];\n const tenantCond = this.tenantCondition('listRecentFailed', tenantId);\n if (tenantCond) conditions.push(tenantCond);\n\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(and(...conditions))\n .orderBy(desc(jobRuns.finishedAt), desc(jobRuns.updatedAt))\n .limit(limit);\n\n return rows.map((r) => ({\n runId: r.id,\n jobType: r.jobType,\n pool: r.pool,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n failedAt: r.finishedAt ?? r.updatedAt,\n createdAt: r.createdAt,\n }));\n }\n\n async listJobRuns(query: ListJobRunsQuery = {}): Promise<JobRunPage> {\n const limit = clampLimit(query.limit);\n const conditions = [];\n\n const tenantCond = this.tenantCondition('listJobRuns', query.tenantId);\n if (tenantCond) conditions.push(tenantCond);\n if (query.poolId) conditions.push(eq(jobRuns.pool, query.poolId));\n if (query.rootRunId) conditions.push(eq(jobRuns.rootRunId, query.rootRunId));\n if (query.status) conditions.push(eq(jobRuns.status, query.status));\n if (query.since) conditions.push(gte(jobRuns.createdAt, query.since));\n\n // Keyset seek: WHERE (created_at, id) < (cursorCreatedAt, cursorId),\n // expanded into a SARGable OR so the same `created_at` index is used.\n if (query.cursor) {\n const keyset = decodeKeysetCursor(query.cursor);\n if (keyset) {\n conditions.push(\n or(\n lt(jobRuns.createdAt, keyset.createdAt),\n and(\n eq(jobRuns.createdAt, keyset.createdAt),\n lt(jobRuns.id, keyset.id),\n ),\n )!,\n );\n }\n }\n\n // Fetch one extra row to determine whether a next page exists without a\n // separate COUNT.\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(conditions.length > 0 ? and(...conditions) : undefined)\n .orderBy(desc(jobRuns.createdAt), desc(jobRuns.id))\n .limit(limit + 1);\n\n const hasMore = rows.length > limit;\n const page = hasMore ? rows.slice(0, limit) : rows;\n const items = page.map(toJobRunSummary);\n const last = page[page.length - 1];\n const nextCursor =\n hasMore && last\n ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id })\n : null;\n\n return { items, nextCursor };\n }\n\n /**\n * Internal helper used by cascade paths (not on the public protocol).\n * Exposed as a public method on the concrete class so infrastructure\n * code (cascade tests, debug tools) can call it without a cast.\n */\n async findByRootRunId(rootRunId: string): Promise<JobRun[]> {\n const rows = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.rootRunId, rootRunId));\n return rows as JobRun[];\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n","/**\n * Drizzle schema for the job orchestration domain (ADR-022).\n *\n * Three tables model the lifecycle of a durable job:\n * - `job` — definitions keyed by handler type (e.g. 'onboarding').\n * - `job_run` — one row per attempt to execute a job; worker claims\n * rows directly via SELECT ... FOR UPDATE SKIP LOCKED.\n * - `job_step` — individual steps within a run; memoises output for replay.\n *\n * Phase 1 ships only this layer. There is no `job_queue` table, no executor\n * port — see ADR-022 and `.claude/skills/jobs/SKILL.md` for the rationale.\n */\nimport {\n pgEnum,\n pgTable,\n uuid,\n text,\n jsonb,\n integer,\n timestamp,\n index,\n uniqueIndex,\n} from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport type { InferSelectModel } from 'drizzle-orm';\n\n// ─── Internal $type<> helpers ───────────────────────────────────────────────\n// Annotation types for jsonb columns only. JOB-2 defines the public protocol\n// types; these remain private to this file.\n\ntype RetryPolicy = {\n attempts: number;\n backoff: 'fixed' | 'exponential';\n baseMs: number;\n nonRetryableErrors?: string[];\n};\n\ntype JobRunError = {\n message: string;\n stack?: string;\n retryable: boolean;\n attempt: number;\n};\n\n// ─── Enums ──────────────────────────────────────────────────────────────────\n\nexport const jobRunStatusEnum = pgEnum('job_run_status', [\n 'pending',\n 'running',\n 'waiting',\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n]);\n\n// extended in ADR-027: tool_call | llm_call | wait | checkpoint | message\nexport const jobStepKindEnum = pgEnum('job_step_kind', ['task']);\n\nexport const jobStepStatusEnum = pgEnum('job_step_status', [\n 'pending',\n 'running',\n 'completed',\n 'failed',\n 'skipped',\n]);\n\nexport const collisionModeEnum = pgEnum('job_collision_mode', [\n 'queue',\n 'reject',\n 'replace',\n]);\n\nexport const replayFromEnum = pgEnum('job_replay_from', [\n 'scratch',\n 'last_step',\n 'last_checkpoint',\n]);\n\nexport const parentClosePolicyEnum = pgEnum('job_parent_close_policy', [\n 'terminate',\n 'cancel',\n 'abandon',\n]);\n\n// Phase 3 placeholder — see ADR-025\nexport const waitKindEnum = pgEnum('job_wait_kind', ['signal']);\n\n// Phase 2 may add more sources; requires Atlas migration\nexport const triggerSourceEnum = pgEnum('job_trigger_source', [\n 'manual',\n 'schedule',\n 'event',\n 'parent',\n]);\n\n// ─── job ────────────────────────────────────────────────────────────────────\n\nexport const jobs = pgTable('job', {\n type: text('type').primaryKey(),\n version: integer('version').notNull().default(1),\n pool: text('pool').notNull(),\n scopeEntityType: text('scope_entity_type'),\n retryPolicy: jsonb('retry_policy').notNull().$type<RetryPolicy>(),\n timeoutMs: integer('timeout_ms'),\n concurrencyKeyTemplate: text('concurrency_key_template'),\n collisionMode: collisionModeEnum('collision_mode').notNull().default('queue'),\n dedupeKeyTemplate: text('dedupe_key_template'),\n dedupeWindowMs: integer('dedupe_window_ms'),\n priorityDefault: integer('priority_default').notNull().default(0),\n replayFrom: replayFromEnum('replay_from').notNull().default('last_checkpoint'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n});\n\nexport type JobDefinitionRow = InferSelectModel<typeof jobs>;\n\n// ─── job_run ────────────────────────────────────────────────────────────────\n\nexport const jobRuns = pgTable(\n 'job_run',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobType: text('job_type').notNull().references(() => jobs.type),\n jobVersion: integer('job_version').notNull(),\n parentRunId: uuid('parent_run_id').references((): any => jobRuns.id),\n /**\n * Service generates `id` client-side via randomUUID() and sets\n * root_run_id = id for root runs (single INSERT, no self-FK race).\n */\n rootRunId: uuid('root_run_id').notNull(),\n parentClosePolicy: parentClosePolicyEnum('parent_close_policy')\n .notNull()\n .default('terminate'),\n scopeEntityType: text('scope_entity_type'),\n scopeEntityId: text('scope_entity_id'),\n tenantId: text('tenant_id'),\n tags: jsonb('tags').notNull().default({}).$type<Record<string, string>>(),\n pool: text('pool').notNull(),\n priority: integer('priority').notNull().default(0),\n concurrencyKey: text('concurrency_key'),\n dedupeKey: text('dedupe_key'),\n status: jobRunStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').notNull().$type<Record<string, unknown>>(),\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n triggerSource: triggerSourceEnum('trigger_source').notNull(),\n triggerRef: text('trigger_ref'),\n runAt: timestamp('run_at', { withTimezone: true }).notNull().defaultNow(),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n claimedAt: timestamp('claimed_at', { withTimezone: true }),\n attempts: integer('attempts').notNull().default(0),\n // Phase 3 placeholder — see ADR-025\n waitKind: waitKindEnum('wait_kind'),\n // Phase 3 placeholder — see ADR-025\n resumeToken: text('resume_token'),\n // Phase 3 placeholder — see ADR-025\n waitDeadline: timestamp('wait_deadline', { withTimezone: true }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n /** Claim query: ORDER BY priority DESC, run_at ASC. */\n idxJobRunClaim: index('idx_job_run_claim').on(t.status, t.pool, t.runAt),\n /** Tree traversal / cascade cancel. */\n idxJobRunRoot: index('idx_job_run_root').on(t.rootRunId),\n /** listForScope query. */\n idxJobRunScope: index('idx_job_run_scope').on(t.scopeEntityType, t.scopeEntityId),\n /** Idempotency collapse — partial index. */\n idxJobRunDedupe: index('idx_job_run_dedupe')\n .on(t.jobType, t.dedupeKey)\n .where(sql`${t.dedupeKey} IS NOT NULL`),\n /** Collision check — partial index. */\n idxJobRunConcurrency: index('idx_job_run_concurrency')\n .on(t.concurrencyKey)\n .where(\n sql`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`,\n ),\n }),\n);\n\nexport type JobRunRow = InferSelectModel<typeof jobRuns>;\n\n// ─── job_step ───────────────────────────────────────────────────────────────\n\nexport const jobSteps = pgTable(\n 'job_step',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n jobRunId: uuid('job_run_id').notNull().references(() => jobRuns.id),\n stepId: text('step_id').notNull(),\n kind: jobStepKindEnum('kind').notNull().default('task'),\n /**\n * Monotonic within run. integer (max ~2B per run) is sufficient —\n * downgraded from ADR-022's bigint; revisit only if a single run\n * ever exceeds 2 billion steps.\n */\n seq: integer('seq').notNull(),\n status: jobStepStatusEnum('status').notNull().default('pending'),\n input: jsonb('input').$type<Record<string, unknown>>(),\n /** Memoised on success for replay. */\n output: jsonb('output').$type<Record<string, unknown>>(),\n error: jsonb('error').$type<JobRunError>(),\n attempts: integer('attempts').notNull().default(0),\n startedAt: timestamp('started_at', { withTimezone: true }),\n finishedAt: timestamp('finished_at', { withTimezone: true }),\n },\n (t) => ({\n /** No duplicate step IDs per run. */\n idxJobStepRunStep: uniqueIndex('idx_job_step_run_step').on(t.jobRunId, t.stepId),\n /** Ordered timeline reads. */\n idxJobStepTimeline: index('idx_job_step_timeline').on(t.jobRunId, t.seq),\n }),\n);\n\nexport type JobStepRow = InferSelectModel<typeof jobSteps>;\n","/**\n * Keyset (seek) cursor codec for `IJobRunService.listJobRuns` (OBS-LIST-1).\n *\n * The list is ordered `created_at DESC, id DESC`. The cursor encodes the\n * `(createdAt, id)` of the last row on the previous page so the next page\n * can seek with `WHERE (created_at, id) < (cursorCreatedAt, cursorId)`\n * rather than an `OFFSET`. Keyset pagination stays O(log n) on deep pages\n * and is stable as new rows arrive at the head.\n *\n * The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape\n * is an implementation detail — never parse it outside this module.\n *\n * Also hosts `toJobRunSummary`, the single `JobRunRow → JobRunSummary`\n * projection shared by both backends so the narrow shape stays in sync.\n */\nimport type { JobRunRow } from './job-orchestration.schema';\nimport type { JobRunSummary } from './job-run-service.protocol';\n\nexport interface JobRunKeyset {\n /** `created_at` of the last row on the previous page. */\n createdAt: Date;\n /** `id` (UUID) tie-break of the last row on the previous page. */\n id: string;\n}\n\n/** Default page size when `limit` is omitted. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Hard upper bound on page size to keep a single read bounded. */\nexport const MAX_LIST_LIMIT = 200;\n\n/** Clamp a caller-supplied `limit` into `[1, MAX_LIST_LIMIT]`. */\nexport function clampLimit(limit: number | undefined): number {\n if (typeof limit !== 'number' || !Number.isFinite(limit)) {\n return DEFAULT_LIST_LIMIT;\n }\n const floored = Math.floor(limit);\n if (floored < 1) return 1;\n if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;\n return floored;\n}\n\nexport function encodeKeysetCursor(keyset: JobRunKeyset): string {\n const tuple = [keyset.createdAt.toISOString(), keyset.id];\n return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');\n}\n\n/**\n * Decode an opaque cursor back into its `(createdAt, id)` keyset. Returns\n * `null` for a malformed cursor so callers can treat garbage input as\n * \"start from the beginning\" rather than throwing on user-supplied data.\n */\nexport function decodeKeysetCursor(cursor: string): JobRunKeyset | null {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf8');\n const parsed = JSON.parse(json) as unknown;\n if (!Array.isArray(parsed) || parsed.length !== 2) return null;\n const [iso, id] = parsed;\n if (typeof iso !== 'string' || typeof id !== 'string') return null;\n const createdAt = new Date(iso);\n if (Number.isNaN(createdAt.getTime())) return null;\n return { createdAt, id };\n } catch {\n return null;\n }\n}\n\n/**\n * Project a raw `job_run` row into the narrow `JobRunSummary` shape exposed\n * by `listJobRuns`. `errorMessage` is pulled from the jsonb `error.message`.\n */\nexport function toJobRunSummary(r: JobRunRow): JobRunSummary {\n return {\n runId: r.id,\n rootRunId: r.rootRunId,\n jobType: r.jobType,\n pool: r.pool,\n status: r.status,\n scopeEntityType: r.scopeEntityType,\n scopeEntityId: r.scopeEntityId,\n tenantId: r.tenantId,\n attempts: r.attempts,\n errorMessage: r.error?.message ?? null,\n runAt: r.runAt,\n startedAt: r.startedAt,\n finishedAt: r.finishedAt,\n createdAt: r.createdAt,\n };\n}\n","/**\n * Injection tokens for the job orchestration domain layer (ADR-022, JOB-2).\n *\n * Consumer code injects these symbols via `@Inject(JOB_ORCHESTRATOR)` etc.;\n * concrete backends (JOB-3 Drizzle, JOB-4 Memory) provide the implementations\n * through `JobsDomainModule.forRoot({ backend })` in JOB-5.\n *\n * Each token is a unique `Symbol` — guaranteed distinct from every other\n * Symbol at runtime, which is exactly the uniqueness guarantee Nest's DI\n * container relies on for token-based lookup.\n */\nexport const JOB_ORCHESTRATOR = Symbol('JOB_ORCHESTRATOR');\nexport const JOB_RUN_SERVICE = Symbol('JOB_RUN_SERVICE');\nexport const JOB_STEP_SERVICE = Symbol('JOB_STEP_SERVICE');\n\n/**\n * Multi-tenancy opt-in flag (JOB-8). Bound to the boolean passed in via\n * `JobsDomainModule.forRoot({ multiTenant })`, defaulting to `false`.\n *\n * When `true`, the four service-layer backends (Drizzle + Memory orchestrator\n * and run-service) enforce `tenantId` on every mutating / targeted-read call:\n * `start`, `cancel`, `listForScope`, `cancelForScope`, `rescheduleForScope`.\n * Missing (`undefined`) `tenantId` throws `MissingTenantIdError`; explicit\n * `null` opts into cross-tenant background work and passes through.\n *\n * The JobWorker claim loop is **cross-tenant by design** — the worker has no\n * tenant context; `tenantId` is populated at write time and enforced on\n * targeted reads. See docs/specs/JOB-8.md.\n */\nexport const JOBS_MULTI_TENANT = Symbol('JOBS_MULTI_TENANT');\n","/**\n * Typed errors for the job orchestration domain (ADR-022, JOB-3).\n *\n * All thrown by the Drizzle orchestrator (and mirrored by the Memory\n * backend in JOB-4). They exist as classes so consumers can `instanceof`\n * them in catch blocks and exception filters can map them to HTTP codes.\n */\nimport type { JobRun } from './job-orchestrator.protocol';\n\n/**\n * `start(type, …)` was called for a job type that has no row in the `job`\n * table. At runtime this usually means the handler was not decorated or the\n * boot validator (JOB-5) has not registered it yet.\n */\nexport class JobTypeNotFoundError extends Error {\n override readonly name = 'JobTypeNotFoundError';\n constructor(public readonly jobType: string) {\n super(`No job definition registered for type '${jobType}'.`);\n }\n}\n\n/**\n * Thrown by `start` when `collision_mode === 'reject'` and a non-terminal\n * run with the same `concurrency_key` already exists. Carries the incumbent\n * so callers can surface its id or subscribe to its completion event.\n */\nexport class JobCollisionError extends Error {\n override readonly name = 'JobCollisionError';\n constructor(\n public readonly jobType: string,\n public readonly concurrencyKey: string,\n public readonly incumbent: JobRun,\n ) {\n super(\n `Job type '${jobType}' has an in-flight run with concurrency_key ` +\n `'${concurrencyKey}' (incumbent ${incumbent.id}); collision_mode=reject.`,\n );\n }\n}\n\n/**\n * `replay` was called on a run that is not in a replayable terminal state\n * (i.e. still `pending` / `running` / `waiting`). Replay always spawns\n * fresh execution and therefore requires the source run to be settled.\n */\nexport class JobNotReplayableError extends Error {\n override readonly name = 'JobNotReplayableError';\n constructor(\n public readonly runId: string,\n public readonly currentStatus: string,\n ) {\n super(\n `Run ${runId} is not replayable from status '${currentStatus}'. ` +\n `Only 'completed', 'failed', 'timed_out', and 'canceled' are eligible.`,\n );\n }\n}\n\n/**\n * A `concurrency_key_template` or `dedupe_key_template` referenced a field\n * that is not present on the input payload. Caught at `start` time so the\n * caller sees the misconfiguration synchronously rather than at claim time.\n */\nexport class JobTemplateFieldMissingError extends Error {\n override readonly name = 'JobTemplateFieldMissingError';\n constructor(\n public readonly template: string,\n public readonly field: string,\n ) {\n super(\n `Template '${template}' references input field '${field}' which is ` +\n `missing or undefined on the payload.`,\n );\n }\n}\n\n/**\n * Thrown by the four multi-tenant-aware service-layer backends (JOB-8)\n * when `JobsDomainModule` was configured with `multiTenant: true` but the\n * caller did not pass a `tenantId` in the relevant options object.\n *\n * **Strict enforcement rationale (resolved 2026-04-18).** Cross-tenant data\n * leakage is the worst class of bug a multi-tenant system can ship; surfacing\n * the misuse loudly at the call site (rather than silently defaulting to\n * `null` or to the \"last tenant seen\") prevents both accidental global\n * writes and sneaky reads that return a union of tenants.\n *\n * - `undefined` `tenantId` → throw this error.\n * - Explicit `null` `tenantId` → passes; opts the call into cross-tenant\n * background work (e.g. a nightly housekeeping job that must scan all\n * tenants). The row is persisted with `tenant_id = NULL`.\n */\nexport class MissingTenantIdError extends Error {\n override readonly name = 'MissingTenantIdError';\n constructor(public readonly method: string) {\n super(\n `MissingTenantIdError: JobsDomainModule was configured with ` +\n `multiTenant=true but ${method} was called without tenantId ` +\n `(undefined). Pass an explicit tenantId, or pass null for ` +\n `cross-tenant work.`,\n );\n }\n}\n\n/**\n * Thrown by `JobWorkerModule.onModuleInit` (Drizzle backend only) when the\n * `job` table contains type rows for which no `@JobHandler` is registered\n * in the running process. Surfaces every orphaned type at once so a single\n * boot tells the operator everything to clean up.\n *\n * Skipped entirely in memory mode (Q4 resolution 2026-04-19) — the memory\n * backend has no DB rows to validate; `MemoryJobOrchestrator.start()`\n * throws `JobTypeNotFoundError` synchronously for unknown types instead.\n */\nexport class BootValidationError extends Error {\n override readonly name = 'BootValidationError';\n constructor(public readonly missingHandlers: string[]) {\n super(\n `BootValidationError: ${missingHandlers.length} orphaned job type(s) ` +\n `in 'job' table with no matching @JobHandler in the running process: ` +\n `[${missingHandlers.join(', ')}]. Either register the handler(s) or ` +\n `remove the rows.`,\n );\n }\n}\n\n/**\n * Thrown by `JobWorkerModule.onModuleInit` when one or more `@JobHandler`\n * classes target a `reserved: true` pool from the resolved pool config\n * (the three `events_*` pools are reserved for the events subsystem\n * outbox drain). Listing every offender on a single boot avoids the\n * fix-one-restart-fix-next loop.\n */\nexport class ReservedPoolViolationError extends Error {\n override readonly name = 'ReservedPoolViolationError';\n constructor(\n public readonly offenders: ReadonlyArray<{\n handlerClass: string;\n pool: string;\n }>,\n ) {\n super(\n `ReservedPoolViolationError: ${offenders.length} @JobHandler(s) target ` +\n `reserved pools — reserved pools are framework-only:\\n` +\n offenders\n .map((o) => ` - ${o.handlerClass} → pool='${o.pool}'`)\n .join('\\n'),\n );\n }\n}\n"],"mappings":"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","names":["sql","sql"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as IJobOrchestrator, i as JobRun } from '../../../job-orchestrator.protocol-
|
|
2
|
-
import { IJobRunService, ListForScopeOptions, CancelForScopeOptions, RescheduleForScopeOptions, PoolStatusCount, JobRunFailure } from './job-run-service.protocol.js';
|
|
1
|
+
import { I as IJobOrchestrator, i as JobRun } from '../../../job-orchestrator.protocol-CHOEqBDk.js';
|
|
2
|
+
import { IJobRunService, ListForScopeOptions, CancelForScopeOptions, RescheduleForScopeOptions, PoolStatusCount, JobRunFailure, ListJobRunsQuery, JobRunPage } from './job-run-service.protocol.js';
|
|
3
3
|
import { MemoryJobStore } from './memory-job-store.js';
|
|
4
4
|
import '../events/event-bus.protocol.js';
|
|
5
5
|
import '../../types/drizzle.js';
|
|
@@ -8,6 +8,7 @@ import './job-orchestration.schema.js';
|
|
|
8
8
|
import 'drizzle-orm/pg-core';
|
|
9
9
|
import 'drizzle-orm';
|
|
10
10
|
import '@nestjs/common';
|
|
11
|
+
import '../events/generated/types.js';
|
|
11
12
|
|
|
12
13
|
declare class MemoryJobRunService implements IJobRunService {
|
|
13
14
|
private readonly store;
|
|
@@ -26,6 +27,7 @@ declare class MemoryJobRunService implements IJobRunService {
|
|
|
26
27
|
rescheduleForScope(entityType: string, entityId: string, newRunAt: Date, opts?: RescheduleForScopeOptions): Promise<void>;
|
|
27
28
|
countByPoolAndStatus(tenantId?: string | null): Promise<PoolStatusCount[]>;
|
|
28
29
|
listRecentFailed(limit: number, tenantId?: string | null): Promise<JobRunFailure[]>;
|
|
30
|
+
listJobRuns(query?: ListJobRunsQuery): Promise<JobRunPage>;
|
|
29
31
|
/**
|
|
30
32
|
* Direct lookup. Not on the protocol — concrete-class convenience for
|
|
31
33
|
* tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
|
|
@@ -13,6 +13,55 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
13
13
|
// runtime/subsystems/jobs/job-run-service.memory-backend.ts
|
|
14
14
|
import { Inject, Injectable } from "@nestjs/common";
|
|
15
15
|
|
|
16
|
+
// runtime/subsystems/jobs/job-run-keyset-cursor.ts
|
|
17
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
18
|
+
var MAX_LIST_LIMIT = 200;
|
|
19
|
+
function clampLimit(limit) {
|
|
20
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
21
|
+
return DEFAULT_LIST_LIMIT;
|
|
22
|
+
}
|
|
23
|
+
const floored = Math.floor(limit);
|
|
24
|
+
if (floored < 1) return 1;
|
|
25
|
+
if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
|
|
26
|
+
return floored;
|
|
27
|
+
}
|
|
28
|
+
function encodeKeysetCursor(keyset) {
|
|
29
|
+
const tuple = [keyset.createdAt.toISOString(), keyset.id];
|
|
30
|
+
return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
|
|
31
|
+
}
|
|
32
|
+
function decodeKeysetCursor(cursor) {
|
|
33
|
+
try {
|
|
34
|
+
const json = Buffer.from(cursor, "base64url").toString("utf8");
|
|
35
|
+
const parsed = JSON.parse(json);
|
|
36
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) return null;
|
|
37
|
+
const [iso, id] = parsed;
|
|
38
|
+
if (typeof iso !== "string" || typeof id !== "string") return null;
|
|
39
|
+
const createdAt = new Date(iso);
|
|
40
|
+
if (Number.isNaN(createdAt.getTime())) return null;
|
|
41
|
+
return { createdAt, id };
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function toJobRunSummary(r) {
|
|
47
|
+
return {
|
|
48
|
+
runId: r.id,
|
|
49
|
+
rootRunId: r.rootRunId,
|
|
50
|
+
jobType: r.jobType,
|
|
51
|
+
pool: r.pool,
|
|
52
|
+
status: r.status,
|
|
53
|
+
scopeEntityType: r.scopeEntityType,
|
|
54
|
+
scopeEntityId: r.scopeEntityId,
|
|
55
|
+
tenantId: r.tenantId,
|
|
56
|
+
attempts: r.attempts,
|
|
57
|
+
errorMessage: r.error?.message ?? null,
|
|
58
|
+
runAt: r.runAt,
|
|
59
|
+
startedAt: r.startedAt,
|
|
60
|
+
finishedAt: r.finishedAt,
|
|
61
|
+
createdAt: r.createdAt
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
16
65
|
// runtime/subsystems/jobs/jobs-domain.tokens.ts
|
|
17
66
|
var JOB_ORCHESTRATOR = /* @__PURE__ */ Symbol("JOB_ORCHESTRATOR");
|
|
18
67
|
var JOBS_MULTI_TENANT = /* @__PURE__ */ Symbol("JOBS_MULTI_TENANT");
|
|
@@ -146,6 +195,38 @@ var MemoryJobRunService = class {
|
|
|
146
195
|
createdAt: r.createdAt
|
|
147
196
|
}));
|
|
148
197
|
}
|
|
198
|
+
async listJobRuns(query = {}) {
|
|
199
|
+
const limit = clampLimit(query.limit);
|
|
200
|
+
const tenantCheck = this.tenantPredicate("listJobRuns", query.tenantId);
|
|
201
|
+
const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;
|
|
202
|
+
const matched = [];
|
|
203
|
+
for (const r of this.store.runs.values()) {
|
|
204
|
+
if (tenantCheck && !tenantCheck(r)) continue;
|
|
205
|
+
if (query.poolId && r.pool !== query.poolId) continue;
|
|
206
|
+
if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;
|
|
207
|
+
if (query.status && r.status !== query.status) continue;
|
|
208
|
+
if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;
|
|
209
|
+
matched.push(r);
|
|
210
|
+
}
|
|
211
|
+
matched.sort((a, b) => {
|
|
212
|
+
const dt = b.createdAt.getTime() - a.createdAt.getTime();
|
|
213
|
+
if (dt !== 0) return dt;
|
|
214
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
215
|
+
});
|
|
216
|
+
const seeked = keyset ? matched.filter((r) => {
|
|
217
|
+
const ct = r.createdAt.getTime();
|
|
218
|
+
const kt = keyset.createdAt.getTime();
|
|
219
|
+
if (ct < kt) return true;
|
|
220
|
+
if (ct > kt) return false;
|
|
221
|
+
return r.id < keyset.id;
|
|
222
|
+
}) : matched;
|
|
223
|
+
const hasMore = seeked.length > limit;
|
|
224
|
+
const page = hasMore ? seeked.slice(0, limit) : seeked;
|
|
225
|
+
const items = page.map(toJobRunSummary);
|
|
226
|
+
const last = page[page.length - 1];
|
|
227
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
|
|
228
|
+
return { items, nextCursor };
|
|
229
|
+
}
|
|
149
230
|
/**
|
|
150
231
|
* Direct lookup. Not on the protocol — concrete-class convenience for
|
|
151
232
|
* tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
|