@pattern-stack/codegen 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
  3. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
  4. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +3 -0
  5. package/dist/runtime/subsystems/bridge/bridge.module.js +930 -275
  6. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  7. package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
  8. package/dist/runtime/subsystems/bridge/index.d.ts +3 -0
  9. package/dist/runtime/subsystems/bridge/index.js +837 -182
  10. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  11. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +3 -1
  12. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +92 -1
  13. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
  14. package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +3 -1
  15. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +99 -0
  16. package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
  17. package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
  18. package/dist/runtime/subsystems/events/event-keyset-cursor.d.ts +32 -0
  19. package/dist/runtime/subsystems/events/event-keyset-cursor.js +38 -0
  20. package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -0
  21. package/dist/runtime/subsystems/events/event-read.protocol.d.ts +94 -0
  22. package/dist/runtime/subsystems/events/event-read.protocol.js +9 -0
  23. package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -0
  24. package/dist/runtime/subsystems/events/events.module.js +177 -3
  25. package/dist/runtime/subsystems/events/events.module.js.map +1 -1
  26. package/dist/runtime/subsystems/events/events.tokens.d.ts +16 -1
  27. package/dist/runtime/subsystems/events/events.tokens.js +2 -0
  28. package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
  29. package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
  30. package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
  31. package/dist/runtime/subsystems/events/index.d.ts +2 -1
  32. package/dist/runtime/subsystems/events/index.js +178 -3
  33. package/dist/runtime/subsystems/events/index.js.map +1 -1
  34. package/dist/runtime/subsystems/index.d.ts +1 -0
  35. package/dist/runtime/subsystems/index.js +1194 -264
  36. package/dist/runtime/subsystems/index.js.map +1 -1
  37. package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +98 -0
  38. package/dist/runtime/subsystems/jobs/bullmq.config.js +143 -0
  39. package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -0
  40. package/dist/runtime/subsystems/jobs/index.d.ts +6 -2
  41. package/dist/runtime/subsystems/jobs/index.js +861 -201
  42. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  43. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +107 -0
  44. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +922 -0
  45. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -0
  46. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +52 -0
  47. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +57 -0
  48. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -0
  49. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +2 -1
  50. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +81 -1
  51. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
  52. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +2 -1
  53. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +81 -0
  54. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
  55. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +74 -1
  56. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +48 -0
  57. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +374 -0
  58. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -0
  59. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +42 -4
  60. package/dist/runtime/subsystems/jobs/job-worker.module.js +832 -178
  61. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  62. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +10 -1
  63. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +519 -20
  64. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  65. package/dist/runtime/subsystems/jobs/pool-config.loader.d.ts +9 -1
  66. package/dist/runtime/subsystems/jobs/pool-config.loader.js +4 -0
  67. package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
  68. package/dist/runtime/subsystems/observability/index.d.ts +4 -3
  69. package/dist/runtime/subsystems/observability/index.js +109 -2
  70. package/dist/runtime/subsystems/observability/index.js.map +1 -1
  71. package/dist/runtime/subsystems/observability/observability.module.js +109 -2
  72. package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
  73. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +63 -2
  74. package/dist/runtime/subsystems/observability/observability.service.d.ts +21 -3
  75. package/dist/runtime/subsystems/observability/observability.service.js +109 -2
  76. package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
  77. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -0
  78. package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -0
  79. package/dist/src/cli/index.js +30 -6
  80. package/dist/src/cli/index.js.map +1 -1
  81. package/package.json +1 -1
  82. package/runtime/subsystems/bridge/bridge.module.ts +5 -0
  83. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +109 -3
  84. package/runtime/subsystems/events/event-bus.memory-backend.ts +103 -1
  85. package/runtime/subsystems/events/event-keyset-cursor.ts +59 -0
  86. package/runtime/subsystems/events/event-read.protocol.ts +97 -0
  87. package/runtime/subsystems/events/events.module.ts +18 -2
  88. package/runtime/subsystems/events/events.tokens.ts +16 -0
  89. package/runtime/subsystems/events/index.ts +7 -0
  90. package/runtime/subsystems/jobs/bullmq.config.ts +125 -0
  91. package/runtime/subsystems/jobs/index.ts +22 -0
  92. package/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts +381 -0
  93. package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +88 -0
  94. package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +59 -1
  95. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +53 -0
  96. package/runtime/subsystems/jobs/job-run-service.protocol.ts +77 -0
  97. package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +311 -0
  98. package/runtime/subsystems/jobs/job-worker.module.ts +124 -10
  99. package/runtime/subsystems/jobs/jobs-domain.module.ts +40 -21
  100. package/runtime/subsystems/jobs/pool-config.loader.ts +11 -0
  101. package/runtime/subsystems/observability/index.ts +8 -0
  102. package/runtime/subsystems/observability/observability.protocol.ts +76 -0
  103. package/runtime/subsystems/observability/observability.service.ts +148 -1
  104. package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -12
  105. package/templates/relationship/new/prompt.js +8 -5
  106. package/templates/subsystem/jobs/worker.ejs.t +30 -7
  107. package/templates/subsystem/sync/sync-audit.schema.ejs.t +12 -16
@@ -0,0 +1,922 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ var __decorateParam = (index2, decorator) => (target, key) => decorator(target, key, index2);
12
+
13
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
14
+ import { createHash } from "crypto";
15
+ import { Inject as Inject2, Injectable as Injectable2, Logger as Logger2, Optional } from "@nestjs/common";
16
+ import { eq as eq2 } from "drizzle-orm";
17
+
18
+ // runtime/constants/tokens.ts
19
+ var DRIZZLE = "DRIZZLE";
20
+
21
+ // runtime/subsystems/jobs/job-orchestration.schema.ts
22
+ import {
23
+ pgEnum,
24
+ pgTable,
25
+ uuid,
26
+ text,
27
+ jsonb,
28
+ integer,
29
+ timestamp,
30
+ index,
31
+ uniqueIndex
32
+ } from "drizzle-orm/pg-core";
33
+ import { sql } from "drizzle-orm";
34
+ var jobRunStatusEnum = pgEnum("job_run_status", [
35
+ "pending",
36
+ "running",
37
+ "waiting",
38
+ "completed",
39
+ "failed",
40
+ "timed_out",
41
+ "canceled"
42
+ ]);
43
+ var jobStepKindEnum = pgEnum("job_step_kind", ["task"]);
44
+ var jobStepStatusEnum = pgEnum("job_step_status", [
45
+ "pending",
46
+ "running",
47
+ "completed",
48
+ "failed",
49
+ "skipped"
50
+ ]);
51
+ var collisionModeEnum = pgEnum("job_collision_mode", [
52
+ "queue",
53
+ "reject",
54
+ "replace"
55
+ ]);
56
+ var replayFromEnum = pgEnum("job_replay_from", [
57
+ "scratch",
58
+ "last_step",
59
+ "last_checkpoint"
60
+ ]);
61
+ var parentClosePolicyEnum = pgEnum("job_parent_close_policy", [
62
+ "terminate",
63
+ "cancel",
64
+ "abandon"
65
+ ]);
66
+ var waitKindEnum = pgEnum("job_wait_kind", ["signal"]);
67
+ var triggerSourceEnum = pgEnum("job_trigger_source", [
68
+ "manual",
69
+ "schedule",
70
+ "event",
71
+ "parent"
72
+ ]);
73
+ var jobs = pgTable("job", {
74
+ type: text("type").primaryKey(),
75
+ version: integer("version").notNull().default(1),
76
+ pool: text("pool").notNull(),
77
+ scopeEntityType: text("scope_entity_type"),
78
+ retryPolicy: jsonb("retry_policy").notNull().$type(),
79
+ timeoutMs: integer("timeout_ms"),
80
+ concurrencyKeyTemplate: text("concurrency_key_template"),
81
+ collisionMode: collisionModeEnum("collision_mode").notNull().default("queue"),
82
+ dedupeKeyTemplate: text("dedupe_key_template"),
83
+ dedupeWindowMs: integer("dedupe_window_ms"),
84
+ priorityDefault: integer("priority_default").notNull().default(0),
85
+ replayFrom: replayFromEnum("replay_from").notNull().default("last_checkpoint"),
86
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
87
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
88
+ });
89
+ var jobRuns = pgTable(
90
+ "job_run",
91
+ {
92
+ id: uuid("id").primaryKey().defaultRandom(),
93
+ jobType: text("job_type").notNull().references(() => jobs.type),
94
+ jobVersion: integer("job_version").notNull(),
95
+ parentRunId: uuid("parent_run_id").references(() => jobRuns.id),
96
+ /**
97
+ * Service generates `id` client-side via randomUUID() and sets
98
+ * root_run_id = id for root runs (single INSERT, no self-FK race).
99
+ */
100
+ rootRunId: uuid("root_run_id").notNull(),
101
+ parentClosePolicy: parentClosePolicyEnum("parent_close_policy").notNull().default("terminate"),
102
+ scopeEntityType: text("scope_entity_type"),
103
+ scopeEntityId: text("scope_entity_id"),
104
+ tenantId: text("tenant_id"),
105
+ tags: jsonb("tags").notNull().default({}).$type(),
106
+ pool: text("pool").notNull(),
107
+ priority: integer("priority").notNull().default(0),
108
+ concurrencyKey: text("concurrency_key"),
109
+ dedupeKey: text("dedupe_key"),
110
+ status: jobRunStatusEnum("status").notNull().default("pending"),
111
+ input: jsonb("input").notNull().$type(),
112
+ output: jsonb("output").$type(),
113
+ error: jsonb("error").$type(),
114
+ triggerSource: triggerSourceEnum("trigger_source").notNull(),
115
+ triggerRef: text("trigger_ref"),
116
+ runAt: timestamp("run_at", { withTimezone: true }).notNull().defaultNow(),
117
+ startedAt: timestamp("started_at", { withTimezone: true }),
118
+ finishedAt: timestamp("finished_at", { withTimezone: true }),
119
+ claimedAt: timestamp("claimed_at", { withTimezone: true }),
120
+ attempts: integer("attempts").notNull().default(0),
121
+ // Phase 3 placeholder — see ADR-025
122
+ waitKind: waitKindEnum("wait_kind"),
123
+ // Phase 3 placeholder — see ADR-025
124
+ resumeToken: text("resume_token"),
125
+ // Phase 3 placeholder — see ADR-025
126
+ waitDeadline: timestamp("wait_deadline", { withTimezone: true }),
127
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
128
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
129
+ },
130
+ (t) => ({
131
+ /** Claim query: ORDER BY priority DESC, run_at ASC. */
132
+ idxJobRunClaim: index("idx_job_run_claim").on(t.status, t.pool, t.runAt),
133
+ /** Tree traversal / cascade cancel. */
134
+ idxJobRunRoot: index("idx_job_run_root").on(t.rootRunId),
135
+ /** listForScope query. */
136
+ idxJobRunScope: index("idx_job_run_scope").on(t.scopeEntityType, t.scopeEntityId),
137
+ /** Idempotency collapse — partial index. */
138
+ idxJobRunDedupe: index("idx_job_run_dedupe").on(t.jobType, t.dedupeKey).where(sql`${t.dedupeKey} IS NOT NULL`),
139
+ /** Collision check — partial index. */
140
+ idxJobRunConcurrency: index("idx_job_run_concurrency").on(t.concurrencyKey).where(
141
+ sql`${t.concurrencyKey} IS NOT NULL AND ${t.status} IN ('pending','running')`
142
+ )
143
+ })
144
+ );
145
+ var jobSteps = pgTable(
146
+ "job_step",
147
+ {
148
+ id: uuid("id").primaryKey().defaultRandom(),
149
+ jobRunId: uuid("job_run_id").notNull().references(() => jobRuns.id),
150
+ stepId: text("step_id").notNull(),
151
+ kind: jobStepKindEnum("kind").notNull().default("task"),
152
+ /**
153
+ * Monotonic within run. integer (max ~2B per run) is sufficient —
154
+ * downgraded from ADR-022's bigint; revisit only if a single run
155
+ * ever exceeds 2 billion steps.
156
+ */
157
+ seq: integer("seq").notNull(),
158
+ status: jobStepStatusEnum("status").notNull().default("pending"),
159
+ input: jsonb("input").$type(),
160
+ /** Memoised on success for replay. */
161
+ output: jsonb("output").$type(),
162
+ error: jsonb("error").$type(),
163
+ attempts: integer("attempts").notNull().default(0),
164
+ startedAt: timestamp("started_at", { withTimezone: true }),
165
+ finishedAt: timestamp("finished_at", { withTimezone: true })
166
+ },
167
+ (t) => ({
168
+ /** No duplicate step IDs per run. */
169
+ idxJobStepRunStep: uniqueIndex("idx_job_step_run_step").on(t.jobRunId, t.stepId),
170
+ /** Ordered timeline reads. */
171
+ idxJobStepTimeline: index("idx_job_step_timeline").on(t.jobRunId, t.seq)
172
+ })
173
+ );
174
+
175
+ // runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts
176
+ import { randomUUID } from "crypto";
177
+ import { Inject, Injectable, Logger } from "@nestjs/common";
178
+ import { and, desc, eq, gt, inArray, isNotNull, ne, notInArray, sql as sql2 } from "drizzle-orm";
179
+
180
+ // runtime/subsystems/jobs/jobs-errors.ts
181
+ var JobTypeNotFoundError = class extends Error {
182
+ constructor(jobType) {
183
+ super(`No job definition registered for type '${jobType}'.`);
184
+ this.jobType = jobType;
185
+ }
186
+ jobType;
187
+ name = "JobTypeNotFoundError";
188
+ };
189
+ var JobCollisionError = class extends Error {
190
+ constructor(jobType, concurrencyKey, incumbent) {
191
+ super(
192
+ `Job type '${jobType}' has an in-flight run with concurrency_key '${concurrencyKey}' (incumbent ${incumbent.id}); collision_mode=reject.`
193
+ );
194
+ this.jobType = jobType;
195
+ this.concurrencyKey = concurrencyKey;
196
+ this.incumbent = incumbent;
197
+ }
198
+ jobType;
199
+ concurrencyKey;
200
+ incumbent;
201
+ name = "JobCollisionError";
202
+ };
203
+ var JobNotReplayableError = class extends Error {
204
+ constructor(runId, currentStatus) {
205
+ super(
206
+ `Run ${runId} is not replayable from status '${currentStatus}'. Only 'completed', 'failed', 'timed_out', and 'canceled' are eligible.`
207
+ );
208
+ this.runId = runId;
209
+ this.currentStatus = currentStatus;
210
+ }
211
+ runId;
212
+ currentStatus;
213
+ name = "JobNotReplayableError";
214
+ };
215
+ var JobTemplateFieldMissingError = class extends Error {
216
+ constructor(template, field) {
217
+ super(
218
+ `Template '${template}' references input field '${field}' which is missing or undefined on the payload.`
219
+ );
220
+ this.template = template;
221
+ this.field = field;
222
+ }
223
+ template;
224
+ field;
225
+ name = "JobTemplateFieldMissingError";
226
+ };
227
+ var MissingTenantIdError = class extends Error {
228
+ constructor(method) {
229
+ super(
230
+ `MissingTenantIdError: JobsDomainModule was configured with multiTenant=true but ${method} was called without tenantId (undefined). Pass an explicit tenantId, or pass null for cross-tenant work.`
231
+ );
232
+ this.method = method;
233
+ }
234
+ method;
235
+ name = "MissingTenantIdError";
236
+ };
237
+
238
+ // runtime/subsystems/jobs/jobs-domain.tokens.ts
239
+ var JOBS_MULTI_TENANT = /* @__PURE__ */ Symbol("JOBS_MULTI_TENANT");
240
+
241
+ // runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts
242
+ var TERMINAL_STATUSES = [
243
+ "completed",
244
+ "failed",
245
+ "timed_out",
246
+ "canceled"
247
+ ];
248
+ var DEDUPE_EXCLUDED_STATUSES = ["canceled", "failed"];
249
+ var IN_FLIGHT_STATUSES = ["pending", "running"];
250
+ function evaluateKeyTemplate(template, input) {
251
+ return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_match, field) => {
252
+ const value = input[field];
253
+ if (value === void 0 || value === null) {
254
+ throw new JobTemplateFieldMissingError(template, field);
255
+ }
256
+ return String(value);
257
+ });
258
+ }
259
+ var DrizzleJobOrchestrator = class {
260
+ constructor(db, multiTenant) {
261
+ this.db = db;
262
+ this.multiTenant = multiTenant;
263
+ }
264
+ db;
265
+ multiTenant;
266
+ // TODO(logging-subsystem): swap to ILogger once ADR-028 lands
267
+ logger = new Logger(DrizzleJobOrchestrator.name);
268
+ /**
269
+ * JOB-8 — resolve `tenantId` for a mutating / targeted-read call.
270
+ * Returns the tenant value that should be written to the row (or compared
271
+ * against in a WHERE clause). When `multiTenant` is off, the column is
272
+ * forced to `null` regardless of what callers pass. When on, `undefined`
273
+ * throws; `null` and strings pass through untouched.
274
+ */
275
+ resolveTenantId(method, tenantId) {
276
+ if (!this.multiTenant) return null;
277
+ if (tenantId === void 0) throw new MissingTenantIdError(method);
278
+ return tenantId;
279
+ }
280
+ // ==========================================================================
281
+ // start
282
+ // ==========================================================================
283
+ async start(type, input, opts = {}, tx) {
284
+ const payload = input ?? {};
285
+ const tenantId = this.resolveTenantId("start", opts.tenantId);
286
+ const client = tx ?? this.db;
287
+ const [def] = await client.select().from(jobs).where(eq(jobs.type, type)).limit(1);
288
+ if (!def) throw new JobTypeNotFoundError(type);
289
+ const definition = def;
290
+ if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {
291
+ const dedupeKey2 = evaluateKeyTemplate(definition.dedupeKeyTemplate, payload);
292
+ const windowStart = new Date(Date.now() - definition.dedupeWindowMs);
293
+ const existing = await client.select().from(jobRuns).where(
294
+ and(
295
+ eq(jobRuns.jobType, type),
296
+ eq(jobRuns.dedupeKey, dedupeKey2),
297
+ gt(jobRuns.createdAt, windowStart),
298
+ // status NOT IN ('canceled', 'failed')
299
+ notInStatus(DEDUPE_EXCLUDED_STATUSES)
300
+ )
301
+ ).orderBy(desc(jobRuns.createdAt)).limit(1);
302
+ if (existing.length > 0) {
303
+ return existing[0];
304
+ }
305
+ }
306
+ let concurrencyKey = null;
307
+ if (definition.concurrencyKeyTemplate) {
308
+ concurrencyKey = evaluateKeyTemplate(
309
+ definition.concurrencyKeyTemplate,
310
+ payload
311
+ );
312
+ const inFlight = await client.select().from(jobRuns).where(
313
+ and(
314
+ eq(jobRuns.concurrencyKey, concurrencyKey),
315
+ inArray(jobRuns.status, IN_FLIGHT_STATUSES)
316
+ )
317
+ ).limit(1);
318
+ if (inFlight.length > 0) {
319
+ const incumbent = inFlight[0];
320
+ switch (definition.collisionMode) {
321
+ case "reject":
322
+ throw new JobCollisionError(type, concurrencyKey, incumbent);
323
+ case "replace":
324
+ await this.cancel(incumbent.id, {
325
+ cascade: true,
326
+ reason: "replaced",
327
+ tenantId: incumbent.tenantId
328
+ });
329
+ break;
330
+ case "queue":
331
+ break;
332
+ }
333
+ }
334
+ }
335
+ const newId = randomUUID();
336
+ let rootRunId = newId;
337
+ if (opts.parentRunId) {
338
+ const [parent] = await client.select({ rootRunId: jobRuns.rootRunId }).from(jobRuns).where(eq(jobRuns.id, opts.parentRunId)).limit(1);
339
+ if (!parent) {
340
+ throw new Error(
341
+ `parentRunId ${opts.parentRunId} does not reference an existing job_run`
342
+ );
343
+ }
344
+ rootRunId = parent.rootRunId;
345
+ }
346
+ const dedupeKey = definition.dedupeKeyTemplate ? evaluateKeyTemplate(definition.dedupeKeyTemplate, payload) : null;
347
+ const [inserted] = await client.insert(jobRuns).values({
348
+ id: newId,
349
+ jobType: type,
350
+ jobVersion: definition.version,
351
+ parentRunId: opts.parentRunId ?? null,
352
+ rootRunId,
353
+ parentClosePolicy: opts.parentClosePolicy ?? "terminate",
354
+ scopeEntityType: opts.scope?.entityType ?? null,
355
+ scopeEntityId: opts.scope?.entityId ?? null,
356
+ tenantId,
357
+ tags: opts.tags ?? {},
358
+ pool: opts.pool ?? definition.pool,
359
+ priority: opts.priority ?? definition.priorityDefault,
360
+ concurrencyKey,
361
+ dedupeKey,
362
+ status: "pending",
363
+ input: payload,
364
+ output: null,
365
+ error: null,
366
+ triggerSource: opts.triggerSource ?? "manual",
367
+ triggerRef: opts.triggerRef ?? null,
368
+ runAt: opts.runAt ?? /* @__PURE__ */ new Date(),
369
+ startedAt: null,
370
+ finishedAt: null,
371
+ claimedAt: null,
372
+ attempts: 0
373
+ }).returning();
374
+ return inserted;
375
+ }
376
+ // ==========================================================================
377
+ // cancel
378
+ // ==========================================================================
379
+ async cancel(runId, opts = {}) {
380
+ const tenantId = this.resolveTenantId("cancel", opts.tenantId);
381
+ const [target] = await this.db.select().from(jobRuns).where(eq(jobRuns.id, runId)).limit(1);
382
+ if (!target) return;
383
+ if (this.multiTenant && target.tenantId !== tenantId) return;
384
+ if (TERMINAL_STATUSES.includes(target.status)) {
385
+ return;
386
+ }
387
+ const [cancelled] = await this.db.update(jobRuns).set({
388
+ status: "canceled",
389
+ finishedAt: /* @__PURE__ */ new Date(),
390
+ updatedAt: /* @__PURE__ */ new Date()
391
+ }).where(
392
+ and(eq(jobRuns.id, runId), notInStatus([...TERMINAL_STATUSES]))
393
+ ).returning();
394
+ if (!cancelled) return;
395
+ if (opts.cascade === false) return;
396
+ const descendants = await this.db.select().from(jobRuns).where(
397
+ and(
398
+ eq(jobRuns.rootRunId, target.rootRunId),
399
+ ne(jobRuns.id, runId),
400
+ notInStatus([...TERMINAL_STATUSES])
401
+ )
402
+ );
403
+ for (const child of descendants) {
404
+ const policy = child.parentClosePolicy;
405
+ if (policy === "abandon") continue;
406
+ await this.db.update(jobRuns).set({
407
+ status: "canceled",
408
+ finishedAt: /* @__PURE__ */ new Date(),
409
+ updatedAt: /* @__PURE__ */ new Date()
410
+ }).where(
411
+ and(
412
+ eq(jobRuns.id, child.id),
413
+ notInStatus([...TERMINAL_STATUSES])
414
+ )
415
+ );
416
+ }
417
+ void opts.reason;
418
+ }
419
+ // ==========================================================================
420
+ // replay
421
+ // ==========================================================================
422
+ async replay(runId) {
423
+ const [target] = await this.db.select().from(jobRuns).where(eq(jobRuns.id, runId)).limit(1);
424
+ if (!target) {
425
+ throw new Error(`replay: run ${runId} not found`);
426
+ }
427
+ const run = target;
428
+ if (!TERMINAL_STATUSES.includes(run.status)) {
429
+ throw new JobNotReplayableError(runId, run.status);
430
+ }
431
+ const [def] = await this.db.select().from(jobs).where(eq(jobs.type, run.jobType)).limit(1);
432
+ if (!def) throw new JobTypeNotFoundError(run.jobType);
433
+ const mode = def.replayFrom;
434
+ const result = await this.db.transaction(async (tx) => {
435
+ if (mode === "scratch") {
436
+ await tx.delete(jobSteps).where(eq(jobSteps.jobRunId, runId));
437
+ } else if (mode === "last_step") {
438
+ await tx.delete(jobSteps).where(
439
+ and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, "completed"))
440
+ );
441
+ } else {
442
+ await tx.delete(jobSteps).where(
443
+ and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, "completed"))
444
+ );
445
+ }
446
+ const [updated] = await tx.update(jobRuns).set({
447
+ status: "pending",
448
+ attempts: 0,
449
+ runAt: /* @__PURE__ */ new Date(),
450
+ startedAt: null,
451
+ finishedAt: null,
452
+ claimedAt: null,
453
+ error: null,
454
+ output: null,
455
+ updatedAt: /* @__PURE__ */ new Date()
456
+ }).where(eq(jobRuns.id, runId)).returning();
457
+ return updated;
458
+ });
459
+ return result;
460
+ }
461
+ // ==========================================================================
462
+ // upsertJobRows — boot-time materialisation of `job` definitions
463
+ // ==========================================================================
464
+ /**
465
+ * Hash-gated `INSERT … ON CONFLICT (type) DO UPDATE … WHERE` per Q3
466
+ * resolution (2026-04-19): the `UPDATE` branch executes only when one
467
+ * of the persisted metadata fields differs from the incoming payload;
468
+ * `version` bumps only on real change; concurrent boots with identical
469
+ * content are idempotent no-ops.
470
+ *
471
+ * Why this shape (not `DO NOTHING`, not advisory locks):
472
+ * - `DO NOTHING` would let an old-version instance leave a stale row
473
+ * that a new-version instance can't overwrite during a rolling deploy.
474
+ * - Advisory locks add latency and leak risk under crashes.
475
+ * - The `WHERE … IS DISTINCT FROM …` clause makes the conditional
476
+ * atomic — no read-modify-write race on `version` between concurrent
477
+ * boots.
478
+ *
479
+ * Orphan detection: a single `SELECT type FROM job WHERE type NOT IN (...)`
480
+ * returns the types present in DB but absent from `entries`. Caller (boot
481
+ * validator) decides whether to throw `BootValidationError`.
482
+ */
483
+ async upsertJobRows(entries, poolConfig) {
484
+ void poolConfig;
485
+ for (const entry of entries) {
486
+ const meta = entry.meta;
487
+ const pool = meta.pool ?? "batch";
488
+ const retryPolicy = meta.retry ?? {
489
+ attempts: 1,
490
+ backoff: "fixed",
491
+ baseMs: 0
492
+ };
493
+ const concurrencyKeyTemplate = meta.concurrency?.key;
494
+ const concurrencyKeyTemplateStr = typeof concurrencyKeyTemplate === "string" ? concurrencyKeyTemplate : null;
495
+ const collisionMode = meta.concurrency?.collisionMode ?? "queue";
496
+ const dedupeKeyTemplate = meta.dedupe?.key;
497
+ const dedupeKeyTemplateStr = typeof dedupeKeyTemplate === "string" ? dedupeKeyTemplate : null;
498
+ const dedupeWindowMs = meta.dedupe?.windowMs ?? null;
499
+ const timeoutMs = meta.timeoutMs ?? null;
500
+ const replayFrom = meta.replayFrom ?? "last_checkpoint";
501
+ const scopeEntityType = meta.scope?.entity ?? null;
502
+ const priorityDefault = 0;
503
+ await this.db.insert(jobs).values({
504
+ type: entry.type,
505
+ version: 1,
506
+ pool,
507
+ scopeEntityType,
508
+ retryPolicy,
509
+ timeoutMs,
510
+ concurrencyKeyTemplate: concurrencyKeyTemplateStr,
511
+ collisionMode,
512
+ dedupeKeyTemplate: dedupeKeyTemplateStr,
513
+ dedupeWindowMs,
514
+ priorityDefault,
515
+ replayFrom
516
+ }).onConflictDoUpdate({
517
+ target: jobs.type,
518
+ set: {
519
+ pool: sql2`EXCLUDED.pool`,
520
+ scopeEntityType: sql2`EXCLUDED.scope_entity_type`,
521
+ retryPolicy: sql2`EXCLUDED.retry_policy`,
522
+ timeoutMs: sql2`EXCLUDED.timeout_ms`,
523
+ concurrencyKeyTemplate: sql2`EXCLUDED.concurrency_key_template`,
524
+ collisionMode: sql2`EXCLUDED.collision_mode`,
525
+ dedupeKeyTemplate: sql2`EXCLUDED.dedupe_key_template`,
526
+ dedupeWindowMs: sql2`EXCLUDED.dedupe_window_ms`,
527
+ priorityDefault: sql2`EXCLUDED.priority_default`,
528
+ replayFrom: sql2`EXCLUDED.replay_from`,
529
+ version: sql2`${jobs.version} + 1`,
530
+ updatedAt: sql2`now()`
531
+ },
532
+ // The hash gate: every field listed in the Q3 resolution appears
533
+ // here. `IS DISTINCT FROM` is the null-safe inequality operator;
534
+ // jsonb cast to text gives stable comparison without invoking a
535
+ // dedicated hash column (avoids a JOB-1 schema migration).
536
+ setWhere: sql2`
537
+ ${jobs.pool} IS DISTINCT FROM EXCLUDED.pool OR
538
+ ${jobs.retryPolicy}::text IS DISTINCT FROM EXCLUDED.retry_policy::text OR
539
+ ${jobs.timeoutMs} IS DISTINCT FROM EXCLUDED.timeout_ms OR
540
+ ${jobs.concurrencyKeyTemplate} IS DISTINCT FROM EXCLUDED.concurrency_key_template OR
541
+ ${jobs.collisionMode} IS DISTINCT FROM EXCLUDED.collision_mode OR
542
+ ${jobs.dedupeKeyTemplate} IS DISTINCT FROM EXCLUDED.dedupe_key_template OR
543
+ ${jobs.dedupeWindowMs} IS DISTINCT FROM EXCLUDED.dedupe_window_ms OR
544
+ ${jobs.priorityDefault} IS DISTINCT FROM EXCLUDED.priority_default OR
545
+ ${jobs.replayFrom} IS DISTINCT FROM EXCLUDED.replay_from OR
546
+ ${jobs.scopeEntityType} IS DISTINCT FROM EXCLUDED.scope_entity_type
547
+ `
548
+ });
549
+ }
550
+ const types = entries.map((e) => e.type);
551
+ const orphans = types.length === 0 ? await this.db.select({ type: jobs.type }).from(jobs) : await this.db.select({ type: jobs.type }).from(jobs).where(notInArray(jobs.type, types));
552
+ return { orphaned: orphans.map((o) => o.type) };
553
+ }
554
+ };
555
+ DrizzleJobOrchestrator = __decorateClass([
556
+ Injectable(),
557
+ __decorateParam(0, Inject(DRIZZLE)),
558
+ __decorateParam(1, Inject(JOBS_MULTI_TENANT))
559
+ ], DrizzleJobOrchestrator);
560
+ function notInStatus(statuses) {
561
+ const negated = statuses.map((s) => ne(jobRuns.status, s));
562
+ return and(...negated);
563
+ }
564
+
565
+ // runtime/subsystems/jobs/pool-config.loader.ts
566
+ import { existsSync, readFileSync } from "fs";
567
+ import { resolve } from "path";
568
+ import { parse as parseYaml } from "yaml";
569
+ var FRAMEWORK_POOLS = Object.freeze({
570
+ events_inbound: Object.freeze({
571
+ queue: "jobs-events-inbound",
572
+ concurrency: 20,
573
+ reserved: true,
574
+ description: "Inbound events drain (events subsystem outbox)."
575
+ }),
576
+ events_change: Object.freeze({
577
+ queue: "jobs-events-change",
578
+ concurrency: 30,
579
+ reserved: true,
580
+ description: "Change events drain (events subsystem outbox)."
581
+ }),
582
+ events_outbound: Object.freeze({
583
+ queue: "jobs-events-outbound",
584
+ concurrency: 10,
585
+ reserved: true,
586
+ description: "Outbound events drain (events subsystem outbox)."
587
+ }),
588
+ interactive: Object.freeze({
589
+ queue: "jobs-interactive",
590
+ concurrency: 20,
591
+ reserved: false,
592
+ description: "User-facing latency-sensitive jobs."
593
+ }),
594
+ batch: Object.freeze({
595
+ queue: "jobs-batch",
596
+ concurrency: 5,
597
+ reserved: false,
598
+ description: "Default pool for background jobs."
599
+ })
600
+ });
601
+ var RESERVED_POOL_NAMES = new Set(
602
+ Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
603
+ );
604
+ var cache = /* @__PURE__ */ new Map();
605
+ function loadPoolConfig(configPath) {
606
+ const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
607
+ const cached = cache.get(resolved);
608
+ if (cached) return cached;
609
+ const merged = /* @__PURE__ */ new Map();
610
+ for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
611
+ merged.set(name, { ...def });
612
+ }
613
+ if (!existsSync(resolved)) {
614
+ cache.set(resolved, merged);
615
+ return merged;
616
+ }
617
+ let raw;
618
+ try {
619
+ raw = parseYaml(readFileSync(resolved, "utf8"));
620
+ } catch (err) {
621
+ throw new Error(
622
+ `pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
623
+ );
624
+ }
625
+ const userPools = extractUserPools(raw);
626
+ for (const [name, userDef] of Object.entries(userPools)) {
627
+ const existing = merged.get(name);
628
+ if (existing) {
629
+ const next = {
630
+ queue: existing.queue,
631
+ concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
632
+ reserved: existing.reserved,
633
+ description: userDef.description ?? existing.description
634
+ };
635
+ merged.set(name, next);
636
+ continue;
637
+ }
638
+ if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
639
+ throw new Error(
640
+ `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
641
+ );
642
+ }
643
+ if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
644
+ throw new Error(
645
+ `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
646
+ );
647
+ }
648
+ if (userDef.reserved === true) {
649
+ throw new Error(
650
+ `pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
651
+ );
652
+ }
653
+ merged.set(name, {
654
+ queue: userDef.queue,
655
+ concurrency: userDef.concurrency,
656
+ reserved: false,
657
+ description: userDef.description
658
+ });
659
+ }
660
+ cache.set(resolved, merged);
661
+ return merged;
662
+ }
663
+ function extractUserPools(raw) {
664
+ if (!raw || typeof raw !== "object") return {};
665
+ const jobs2 = raw.jobs;
666
+ if (!jobs2 || typeof jobs2 !== "object") return {};
667
+ const pools = jobs2.pools;
668
+ if (!pools || typeof pools !== "object") return {};
669
+ const out = {};
670
+ for (const [name, def] of Object.entries(pools)) {
671
+ if (!def || typeof def !== "object") continue;
672
+ out[name] = def;
673
+ }
674
+ return out;
675
+ }
676
+
677
+ // runtime/subsystems/jobs/bullmq.config.ts
678
+ var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
679
+ var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
680
+ function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
681
+ const alias = poolConfig.get(pool)?.queue ?? pool;
682
+ const prefix = config?.queuePrefix;
683
+ return prefix ? `${prefix}:${alias}` : alias;
684
+ }
685
+
686
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
687
+ function sha1JobId(idempotencyKey) {
688
+ return createHash("sha1").update(idempotencyKey).digest("hex");
689
+ }
690
+ var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
691
+ constructor(db, multiTenant, connection, bullConfig = null) {
692
+ super(db, multiTenant);
693
+ this.connection = connection;
694
+ this.bullConfig = bullConfig;
695
+ this.bullDb = db;
696
+ }
697
+ connection;
698
+ bullConfig;
699
+ // TODO(logging-subsystem): swap to ILogger once ADR-028 lands
700
+ bullLogger = new Logger2(BullMQJobOrchestrator.name);
701
+ /** Lazily-opened `Queue` handles, one per pool. */
702
+ queues = /* @__PURE__ */ new Map();
703
+ /** Single FlowProducer for parent/child hierarchies. Lazily opened. */
704
+ _flow = null;
705
+ /**
706
+ * Cached `bullmq` value constructors, populated by `loadBullMq()` on first
707
+ * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
708
+ * a queue). Kept off the import graph so a `drizzle`-only consumer never
709
+ * resolves the optional `'bullmq'` package.
710
+ */
711
+ QueueCtor = null;
712
+ FlowProducerCtor = null;
713
+ bullMqLoad = null;
714
+ /**
715
+ * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
716
+ * `private` (can't be redeclared even privately in a subclass), and the
717
+ * spec forbids touching that file — so the subclass keeps its own handle
718
+ * under a distinct name (same instance, passed through to `super`) for the
719
+ * cancel-cascade snapshot + definition/run loads below.
720
+ */
721
+ bullDb;
722
+ /**
723
+ * Lazily load the optional `bullmq` package and cache its value
724
+ * constructors. Idempotent (single in-flight promise). Throws a friendly,
725
+ * actionable error when the consumer selected `backend: 'bullmq'` but did
726
+ * not install the package — mirrors `createRedisClient` in the redis event
727
+ * backend. Must be `await`ed before any `queueFor`/`flow` access.
728
+ */
729
+ async loadBullMq() {
730
+ if (this.QueueCtor && this.FlowProducerCtor) return;
731
+ if (!this.bullMqLoad) {
732
+ this.bullMqLoad = (async () => {
733
+ try {
734
+ const mod = await import("bullmq");
735
+ this.QueueCtor = mod.Queue;
736
+ this.FlowProducerCtor = mod.FlowProducer;
737
+ } catch {
738
+ throw new Error(
739
+ 'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
740
+ );
741
+ }
742
+ })();
743
+ }
744
+ await this.bullMqLoad;
745
+ }
746
+ /**
747
+ * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
748
+ * loadBullMq()` first so `QueueCtor` is populated.
749
+ */
750
+ queueFor(pool) {
751
+ if (!this.QueueCtor) {
752
+ throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
753
+ }
754
+ const name = resolvePoolQueueName(pool, this.bullConfig);
755
+ let q = this.queues.get(name);
756
+ if (!q) {
757
+ q = new this.QueueCtor(name, { connection: this.connection });
758
+ this.queues.set(name, q);
759
+ }
760
+ return q;
761
+ }
762
+ flow() {
763
+ if (!this.FlowProducerCtor) {
764
+ throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
765
+ }
766
+ if (!this._flow) {
767
+ this._flow = new this.FlowProducerCtor({ connection: this.connection });
768
+ }
769
+ return this._flow;
770
+ }
771
+ // ==========================================================================
772
+ // start — Postgres insert (super) + BullMQ dispatch
773
+ // ==========================================================================
774
+ async start(type, input, opts = {}, tx) {
775
+ const run = await super.start(type, input, opts, tx);
776
+ await this.dispatch(run, type);
777
+ return run;
778
+ }
779
+ /**
780
+ * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
781
+ * `parentRunId` we attach it to the parent's existing BullMQ job through the
782
+ * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
783
+ * its own graph. (The FlowProducer is reserved for whole-tree atomic
784
+ * submits, exposed as an opt-in extension via `flowProducer()`; runtime
785
+ * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
786
+ * correct primitive here.)
787
+ *
788
+ * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
789
+ * present (so the same logical key dedups), else the `job_run.id` UUID
790
+ * (already colon-free).
791
+ *
792
+ * The domain `parentClosePolicy` cascade is still enforced in Postgres by
793
+ * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
794
+ * not the authority.
795
+ */
796
+ async dispatch(run, type) {
797
+ await this.loadBullMq();
798
+ const def = await this.loadDefinition(type);
799
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
800
+ const jobOpts = {
801
+ jobId,
802
+ ...this.retryOpts(def),
803
+ ...this.dedupeOpts(run, def)
804
+ };
805
+ if (run.parentRunId) {
806
+ const parentRow = await this.loadRun(run.parentRunId);
807
+ if (parentRow) {
808
+ const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
809
+ jobOpts.parent = {
810
+ id: parentJobId,
811
+ queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
812
+ };
813
+ }
814
+ }
815
+ const payload = { runId: run.id, type, input: run.input };
816
+ await this.queueFor(run.pool).add(type, payload, jobOpts);
817
+ }
818
+ /**
819
+ * Opt-in extension (spec §Extensions): expose the FlowProducer for
820
+ * consumers that want to submit a whole parent/child DAG atomically up
821
+ * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
822
+ * code using it is not portable to the Drizzle backend. Async because it
823
+ * lazily loads the optional `bullmq` package on first use.
824
+ */
825
+ async flowProducer() {
826
+ await this.loadBullMq();
827
+ return this.flow();
828
+ }
829
+ retryOpts(def) {
830
+ const policy = def.retryPolicy;
831
+ if (!policy) return {};
832
+ return {
833
+ attempts: policy.attempts,
834
+ backoff: {
835
+ type: policy.backoff === "exponential" ? "exponential" : "fixed",
836
+ delay: policy.baseMs
837
+ }
838
+ };
839
+ }
840
+ dedupeOpts(run, def) {
841
+ if (!run.dedupeKey || !def.dedupeWindowMs) return {};
842
+ return {
843
+ deduplication: {
844
+ id: sha1JobId(run.dedupeKey),
845
+ ttl: def.dedupeWindowMs
846
+ }
847
+ };
848
+ }
849
+ // ==========================================================================
850
+ // cancel — Postgres cascade (super) + remove from queue
851
+ // ==========================================================================
852
+ async cancel(runId, opts = {}) {
853
+ const target = await this.loadRun(runId);
854
+ await super.cancel(runId, opts);
855
+ if (!target) return;
856
+ await this.loadBullMq();
857
+ await this.removeFromQueue(target);
858
+ if (opts.cascade === false) return;
859
+ const descendants = await this.bullDb.select().from(jobRuns).where(eq2(jobRuns.rootRunId, target.rootRunId));
860
+ for (const child of descendants) {
861
+ if (child.id === runId) continue;
862
+ await this.removeFromQueue(child);
863
+ }
864
+ }
865
+ async removeFromQueue(run) {
866
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
867
+ try {
868
+ const job = await this.queueFor(run.pool).getJob(jobId);
869
+ if (job) await job.remove();
870
+ } catch (err) {
871
+ this.bullLogger.warn(
872
+ `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
873
+ );
874
+ }
875
+ }
876
+ // ==========================================================================
877
+ // replay — Postgres reset (super) + re-enqueue
878
+ // ==========================================================================
879
+ async replay(runId) {
880
+ const run = await super.replay(runId);
881
+ await this.dispatch(run, run.jobType);
882
+ return run;
883
+ }
884
+ // ==========================================================================
885
+ // Internals
886
+ // ==========================================================================
887
+ async loadDefinition(type) {
888
+ const [def] = await this.bullDb.select().from(jobs).where(eq2(jobs.type, type)).limit(1);
889
+ if (!def) {
890
+ throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
891
+ }
892
+ return def;
893
+ }
894
+ async loadRun(id) {
895
+ const [row] = await this.bullDb.select().from(jobRuns).where(eq2(jobRuns.id, id)).limit(1);
896
+ return row ?? null;
897
+ }
898
+ /** Close all open queue + flow connections. Called on module destroy. */
899
+ async closeConnections() {
900
+ for (const q of this.queues.values()) {
901
+ await q.close().catch(() => void 0);
902
+ }
903
+ this.queues.clear();
904
+ if (this._flow) {
905
+ await this._flow.close().catch(() => void 0);
906
+ this._flow = null;
907
+ }
908
+ }
909
+ };
910
+ BullMQJobOrchestrator = __decorateClass([
911
+ Injectable2(),
912
+ __decorateParam(0, Inject2(DRIZZLE)),
913
+ __decorateParam(1, Inject2(JOBS_MULTI_TENANT)),
914
+ __decorateParam(2, Inject2(BULLMQ_CONNECTION)),
915
+ __decorateParam(3, Optional()),
916
+ __decorateParam(3, Inject2(BULLMQ_RESOLVED_CONFIG))
917
+ ], BullMQJobOrchestrator);
918
+ export {
919
+ BullMQJobOrchestrator,
920
+ sha1JobId
921
+ };
922
+ //# sourceMappingURL=job-orchestrator.bullmq-backend.js.map