@pattern-stack/codegen 0.17.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/consumer-skills/integration/audit-and-detection.md +29 -4
- package/dist/{chunk-GJDEPTPY.js → chunk-235ZMMJR.js} +8 -8
- package/dist/{chunk-DTXH24LR.js → chunk-65MO75WM.js} +9 -9
- package/dist/{chunk-5RT7JGKT.js → chunk-7OVCARTQ.js} +4 -4
- package/dist/{chunk-CO6LUM72.js → chunk-7P5ODGLA.js} +34 -2
- package/dist/chunk-7P5ODGLA.js.map +1 -0
- package/dist/{chunk-P3AYBRP6.js → chunk-ATVGYF3D.js} +11 -5
- package/dist/chunk-ATVGYF3D.js.map +1 -0
- package/dist/{chunk-3MAZ4TQH.js → chunk-AZLUWG5S.js} +9 -9
- package/dist/{chunk-UTNWFHJF.js → chunk-B34G6PHD.js} +10 -10
- package/dist/{chunk-CDLWYZVQ.js → chunk-BHZP6LOV.js} +7 -7
- package/dist/{chunk-W2UIDI3R.js → chunk-CLWBNXKF.js} +4 -4
- package/dist/{chunk-OTR44OH6.js → chunk-E6PLM6QG.js} +34 -13
- package/dist/chunk-E6PLM6QG.js.map +1 -0
- package/dist/{chunk-43SBT72G.js → chunk-I6UXRJ3Q.js} +4 -4
- package/dist/{chunk-36U5UGIO.js → chunk-JEINYUJH.js} +8 -5
- package/dist/chunk-JEINYUJH.js.map +1 -0
- package/dist/{chunk-K2I6XIK5.js → chunk-KSTZIULO.js} +4 -4
- package/dist/{chunk-L3VJ47BU.js → chunk-KZDHMZ45.js} +6 -6
- package/dist/{chunk-RHYNACZS.js → chunk-OZEPJGMA.js} +2 -2
- package/dist/{chunk-MYQIQ27N.js → chunk-Q6LRJ4VI.js} +51 -2
- package/dist/chunk-Q6LRJ4VI.js.map +1 -0
- package/dist/{chunk-NXNVTXKG.js → chunk-R6F6KFIL.js} +5 -5
- package/dist/{chunk-7LKAMLV4.js → chunk-T6SCOJF4.js} +4 -4
- package/dist/{chunk-OGIZXGPY.js → chunk-TDEHU73T.js} +4 -4
- package/dist/{chunk-OITTYGJS.js → chunk-VDL5CJ5C.js} +24 -14
- package/dist/chunk-VDL5CJ5C.js.map +1 -0
- package/dist/{chunk-3VEVGL74.js → chunk-VNBC3VXM.js} +4 -4
- package/dist/{chunk-BULPAAD3.js → chunk-VQOAATIG.js} +42 -11
- package/dist/chunk-VQOAATIG.js.map +1 -0
- package/dist/{chunk-E45CSC33.js → chunk-XKWOJZZ4.js} +2 -2
- package/dist/{chunk-DCCZB4UC.js → chunk-XWBK3XJK.js} +4 -4
- package/dist/{chunk-4GLNY5V6.js → chunk-Y7GDG744.js} +5 -5
- package/dist/{chunk-SR7F3TJY.js → chunk-YK5JEVLX.js} +4 -4
- package/dist/{job-orchestrator.protocol-DubMVbm9.d.ts → job-orchestrator.protocol-ZuJ3ow-O.d.ts} +77 -3
- package/dist/runtime/base-classes/index.js +12 -12
- package/dist/runtime/shared/openapi/index.js +5 -5
- package/dist/runtime/shared/openapi/registry.js +2 -2
- package/dist/runtime/subsystems/auth/auth.module.js +3 -3
- package/dist/runtime/subsystems/auth/index.js +12 -12
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +3 -3
- package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +3 -3
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +7 -7
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/bridge.module.js +19 -19
- package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.js +2 -2
- package/dist/runtime/subsystems/bridge/index.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/index.js +21 -21
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +4 -4
- package/dist/runtime/subsystems/events/events.module.js +5 -5
- package/dist/runtime/subsystems/events/index.js +7 -7
- package/dist/runtime/subsystems/index.d.ts +1 -1
- package/dist/runtime/subsystems/index.js +100 -100
- package/dist/runtime/subsystems/integration/build-change-source.js +2 -2
- package/dist/runtime/subsystems/integration/deep-equal.differ.d.ts +19 -0
- package/dist/runtime/subsystems/integration/deep-equal.differ.js +1 -1
- package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
- package/dist/runtime/subsystems/integration/index.js +39 -39
- package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/integration/integration.module.d.ts +20 -0
- package/dist/runtime/subsystems/integration/integration.module.js +6 -6
- package/dist/runtime/subsystems/jobs/index.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/index.js +27 -27
- package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-handler.base.js +11 -3
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +6 -5
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +3 -2
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +11 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
- package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
- package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +9 -1
- package/dist/runtime/subsystems/jobs/job-worker.js +3 -3
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.js +11 -11
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +9 -9
- package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +1 -1
- package/dist/runtime/subsystems/jobs/pg-notify.d.ts +25 -1
- package/dist/runtime/subsystems/jobs/pg-notify.js +1 -1
- package/dist/runtime/subsystems/observability/index.d.ts +1 -1
- package/dist/runtime/subsystems/observability/index.js +3 -3
- package/dist/runtime/subsystems/observability/observability.module.js +3 -3
- package/dist/runtime/subsystems/observability/observability.protocol.d.ts +1 -1
- package/dist/runtime/subsystems/observability/observability.service.d.ts +1 -1
- package/dist/runtime/subsystems/observability/observability.service.js +2 -2
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -1
- package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -1
- package/dist/runtime/subsystems/storage/index.js +1 -1
- package/dist/runtime/subsystems/storage/storage.module.js +1 -1
- package/dist/src/cli/index.js +38 -16
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.js +13 -13
- package/package.json +1 -1
- package/runtime/subsystems/integration/deep-equal.differ.ts +34 -5
- package/runtime/subsystems/integration/integration.module.ts +26 -2
- package/runtime/subsystems/jobs/job-handler.base.ts +115 -2
- package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +43 -16
- package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +58 -18
- package/runtime/subsystems/jobs/job-worker.ts +29 -11
- package/runtime/subsystems/jobs/pg-notify.ts +63 -3
- package/templates/subsystem/integration-config/codegen-config-integration-block.ejs.t +17 -0
- package/dist/chunk-36U5UGIO.js.map +0 -1
- package/dist/chunk-BULPAAD3.js.map +0 -1
- package/dist/chunk-CO6LUM72.js.map +0 -1
- package/dist/chunk-MYQIQ27N.js.map +0 -1
- package/dist/chunk-OITTYGJS.js.map +0 -1
- package/dist/chunk-OTR44OH6.js.map +0 -1
- package/dist/chunk-P3AYBRP6.js.map +0 -1
- /package/dist/{chunk-GJDEPTPY.js.map → chunk-235ZMMJR.js.map} +0 -0
- /package/dist/{chunk-DTXH24LR.js.map → chunk-65MO75WM.js.map} +0 -0
- /package/dist/{chunk-5RT7JGKT.js.map → chunk-7OVCARTQ.js.map} +0 -0
- /package/dist/{chunk-3MAZ4TQH.js.map → chunk-AZLUWG5S.js.map} +0 -0
- /package/dist/{chunk-UTNWFHJF.js.map → chunk-B34G6PHD.js.map} +0 -0
- /package/dist/{chunk-CDLWYZVQ.js.map → chunk-BHZP6LOV.js.map} +0 -0
- /package/dist/{chunk-W2UIDI3R.js.map → chunk-CLWBNXKF.js.map} +0 -0
- /package/dist/{chunk-43SBT72G.js.map → chunk-I6UXRJ3Q.js.map} +0 -0
- /package/dist/{chunk-K2I6XIK5.js.map → chunk-KSTZIULO.js.map} +0 -0
- /package/dist/{chunk-L3VJ47BU.js.map → chunk-KZDHMZ45.js.map} +0 -0
- /package/dist/{chunk-RHYNACZS.js.map → chunk-OZEPJGMA.js.map} +0 -0
- /package/dist/{chunk-NXNVTXKG.js.map → chunk-R6F6KFIL.js.map} +0 -0
- /package/dist/{chunk-7LKAMLV4.js.map → chunk-T6SCOJF4.js.map} +0 -0
- /package/dist/{chunk-OGIZXGPY.js.map → chunk-TDEHU73T.js.map} +0 -0
- /package/dist/{chunk-3VEVGL74.js.map → chunk-VNBC3VXM.js.map} +0 -0
- /package/dist/{chunk-E45CSC33.js.map → chunk-XKWOJZZ4.js.map} +0 -0
- /package/dist/{chunk-DCCZB4UC.js.map → chunk-XWBK3XJK.js.map} +0 -0
- /package/dist/{chunk-4GLNY5V6.js.map → chunk-Y7GDG744.js.map} +0 -0
- /package/dist/{chunk-SR7F3TJY.js.map → chunk-YK5JEVLX.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts"],"sourcesContent":["/**\n * DrizzleJobOrchestrator — Postgres-backed implementation of\n * `IJobOrchestrator` (ADR-022, JOB-3).\n *\n * Single-layer architecture: `start` writes a single `job_run` row; the\n * `JobWorker` polling loop claims it directly via `FOR UPDATE SKIP LOCKED`.\n * No `job_queue` table, no executor port. See `docs/specs/JOB-3.md`.\n */\nimport { randomUUID } from 'node:crypto';\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport { and, desc, eq, gt, inArray, isNotNull, ne, notInArray, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\nimport { DRIZZLE } from '../../constants/tokens';\nimport {\n jobRuns,\n jobs,\n type JobDefinitionRow,\n type JobRunRow,\n} from './job-orchestration.schema';\nimport type {\n CancelOptions,\n IJobOrchestrator,\n JobPoolDef,\n JobRun,\n JobUpsertEntry,\n StartOptions,\n} from './job-orchestrator.protocol';\nimport {\n JobCollisionError,\n JobNotReplayableError,\n JobTemplateFieldMissingError,\n JobTypeNotFoundError,\n MissingTenantIdError,\n} from './jobs-errors';\nimport { jobSteps } from './job-orchestration.schema';\nimport { JOBS_MULTI_TENANT, JOBS_LISTEN_NOTIFY } from './jobs-domain.tokens';\nimport { JOBS_WAKE_CHANNEL, pgNotify } from './pg-notify';\nimport {\n keySelectorToTemplate,\n resolveJobKey,\n type JobKeySelector,\n} from './job-handler.base';\n\n/**\n * Terminal statuses — transitions into these are final. Used by `cancel`\n * (to short-circuit idempotently) and by `replay` (as the guard gate).\n */\nexport const TERMINAL_STATUSES = [\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n] as const;\ntype TerminalStatus = (typeof TERMINAL_STATUSES)[number];\ntype JobRunStatus = JobRunRow['status'];\n\n/** Statuses excluded from dedupe window matches per ADR-022. */\nconst DEDUPE_EXCLUDED_STATUSES: JobRunStatus[] = ['canceled', 'failed'];\n/** Statuses that count as in-flight for concurrency collision checks. */\nconst IN_FLIGHT_STATUSES: JobRunStatus[] = ['pending', 'running'];\n\n/**\n * Substitute `{{field}}` placeholders against the input payload.\n *\n * Implementation decision (JOB-3, 2026-04-19): simple `{{field}}` single-key\n * substitution, no dotted paths, no Mustache/Handlebars dependency. A missing\n * field throws `JobTemplateFieldMissingError` synchronously — cheaper than\n * discovering the misconfiguration at claim time.\n */\nexport function evaluateKeyTemplate(\n template: string,\n input: Record<string, unknown>,\n): string {\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_match, field: string) => {\n const value = input[field];\n if (value === undefined || value === null) {\n throw new JobTemplateFieldMissingError(template, field);\n }\n return String(value);\n });\n}\n\n@Injectable()\nexport class DrizzleJobOrchestrator implements IJobOrchestrator {\n // TODO(logging-subsystem): swap to ILogger once ADR-028 lands\n private readonly logger = new Logger(DrizzleJobOrchestrator.name);\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n // LISTEN-NOTIFY-1 — when true, `start()` emits an in-tx\n // `pg_notify(codegen_jobs_wake, <pool>)` so a `listen_notify` worker wakes\n // on enqueue-commit. `@Optional()` defaulting to false so direct\n // construction (integration tests not going through DI) keeps working.\n @Optional()\n @Inject(JOBS_LISTEN_NOTIFY)\n private readonly listenNotify: boolean = false,\n ) {}\n\n /**\n * JOB-8 — resolve `tenantId` for a mutating / targeted-read call.\n * Returns the tenant value that should be written to the row (or compared\n * against in a WHERE clause). When `multiTenant` is off, the column is\n * forced to `null` regardless of what callers pass. When on, `undefined`\n * throws; `null` and strings pass through untouched.\n */\n private resolveTenantId(\n method: string,\n tenantId: string | null | undefined,\n ): string | null {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return tenantId;\n }\n\n // ==========================================================================\n // start\n // ==========================================================================\n\n async start(\n type: string,\n input: unknown,\n opts: StartOptions = {},\n tx?: DrizzleTransaction,\n ): Promise<JobRun> {\n const payload = (input ?? {}) as Record<string, unknown>;\n\n // JOB-8 — resolve tenant gate up front so `multi_tenant=true` +\n // undefined surfaces before any row is touched.\n const tenantId = this.resolveTenantId('start', opts.tenantId);\n\n // BRIDGE-7: thread the optional caller tx through every read/write\n // in this method so EventFlowService.publishAndStart can bundle the\n // outbox insert, the eager job_run insert, and (for Case B) the\n // bridge_delivery pre-write into a single transaction.\n const client = (tx ?? this.db) as DrizzleClient;\n\n // 1a. Load job definition.\n const [def] = await client\n .select()\n .from(jobs)\n .where(eq(jobs.type, type))\n .limit(1);\n if (!def) throw new JobTypeNotFoundError(type);\n const definition = def as JobDefinitionRow;\n\n // 1b. Dedupe check. JOB-FN-KEY: `resolveJobKey` honors both the `{{field}}`\n // template AND a function key persisted as `FN_KEY_SENTINEL` (re-resolved\n // live from JOB_HANDLER_REGISTRY).\n if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {\n const dedupeKey = resolveJobKey(\n 'dedupe',\n type,\n definition.dedupeKeyTemplate,\n payload,\n evaluateKeyTemplate,\n ) as string;\n const windowStart = new Date(Date.now() - definition.dedupeWindowMs);\n const existing = await client\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.jobType, type),\n eq(jobRuns.dedupeKey, dedupeKey),\n gt(jobRuns.createdAt, windowStart),\n // status NOT IN ('canceled', 'failed')\n notInStatus(DEDUPE_EXCLUDED_STATUSES),\n ),\n )\n .orderBy(desc(jobRuns.createdAt))\n .limit(1);\n if (existing.length > 0) {\n return existing[0] as JobRun;\n }\n }\n\n // 1c. Concurrency collision check.\n let concurrencyKey: string | null = null;\n if (definition.concurrencyKeyTemplate) {\n // Non-null cast: the branch guard proves the template is present, so the\n // resolver never returns null here (it only nulls on a null template).\n concurrencyKey = resolveJobKey(\n 'concurrency',\n type,\n definition.concurrencyKeyTemplate,\n payload,\n evaluateKeyTemplate,\n ) as string;\n const inFlight = await client\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.concurrencyKey, concurrencyKey),\n inArray(jobRuns.status, IN_FLIGHT_STATUSES),\n ),\n )\n .limit(1);\n if (inFlight.length > 0) {\n const incumbent = inFlight[0] as JobRun;\n switch (definition.collisionMode) {\n case 'reject':\n throw new JobCollisionError(type, concurrencyKey, incumbent);\n case 'replace':\n // JOB-8 — thread the incumbent's own tenantId through the\n // internal cascade. Without this, every `replace`-collision\n // start() under multiTenant=true throws MissingTenantIdError\n // from the inner cancel() call instead of cancelling the\n // incumbent. Mirrors the memory backend's `cancelLocked(\n // incumbent.id, ..., incumbent.tenantId)` pattern.\n await this.cancel(incumbent.id, {\n cascade: true,\n reason: 'replaced',\n tenantId: incumbent.tenantId,\n });\n break;\n case 'queue':\n // Fall through — row is inserted; claim query gates it until\n // the incumbent transitions (see JobWorker.processRun queue gate).\n break;\n }\n }\n }\n\n // 1d. Resolve id + rootRunId, INSERT.\n const newId = randomUUID();\n let rootRunId: string = newId;\n if (opts.parentRunId) {\n const [parent] = await client\n .select({ rootRunId: jobRuns.rootRunId })\n .from(jobRuns)\n .where(eq(jobRuns.id, opts.parentRunId))\n .limit(1);\n if (!parent) {\n throw new Error(\n `parentRunId ${opts.parentRunId} does not reference an existing job_run`,\n );\n }\n rootRunId = parent.rootRunId;\n }\n\n const dedupeKey = resolveJobKey(\n 'dedupe',\n type,\n definition.dedupeKeyTemplate,\n payload,\n evaluateKeyTemplate,\n );\n\n const [inserted] = await client\n .insert(jobRuns)\n .values({\n id: newId,\n jobType: type,\n jobVersion: definition.version,\n parentRunId: opts.parentRunId ?? null,\n rootRunId,\n parentClosePolicy: opts.parentClosePolicy ?? 'terminate',\n scopeEntityType: opts.scope?.entityType ?? null,\n scopeEntityId: opts.scope?.entityId ?? null,\n tenantId,\n tags: opts.tags ?? {},\n pool: opts.pool ?? definition.pool,\n priority: opts.priority ?? definition.priorityDefault,\n concurrencyKey,\n dedupeKey,\n status: 'pending',\n input: payload,\n output: null,\n error: null,\n triggerSource: opts.triggerSource ?? 'manual',\n triggerRef: opts.triggerRef ?? null,\n runAt: opts.runAt ?? new Date(),\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n attempts: 0,\n })\n .returning();\n\n // LISTEN-NOTIFY-1 — wake a listening worker the instant this enqueue\n // commits. Emitted through the SAME `client` (the caller's tx when one was\n // passed, else the pool) so delivery is gated on commit — a rolled-back\n // enqueue emits no phantom wake (D2). The pool name is the payload; the\n // worker re-runs its own pool-filtered claim query on wake. Polling is the\n // fallback, so a failed notify is non-fatal: log + continue.\n if (this.listenNotify) {\n const wakePool = (inserted as JobRunRow).pool;\n try {\n await pgNotify(client, JOBS_WAKE_CHANNEL, wakePool);\n } catch (err) {\n this.logger.warn(\n `pg_notify(${JOBS_WAKE_CHANNEL}, ${wakePool}) failed for run ` +\n `${(inserted as JobRunRow).id}: ${(err as Error).message} ` +\n `(non-fatal — interval polling still claims the run).`,\n );\n }\n }\n\n return inserted as JobRun;\n }\n\n // ==========================================================================\n // cancel\n // ==========================================================================\n\n async cancel(runId: string, opts: CancelOptions = {}): Promise<void> {\n // JOB-8 — resolve tenant gate up front (strict undefined-throws).\n const tenantId = this.resolveTenantId('cancel', opts.tenantId);\n\n // Load target.\n const [target] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!target) return;\n // JOB-8 — cross-tenant cancel is a silent no-op (no existence leak).\n if (this.multiTenant && target.tenantId !== tenantId) return;\n if (TERMINAL_STATUSES.includes(target.status as TerminalStatus)) {\n return; // idempotent\n }\n\n // Atomic transition, guarded against concurrent terminal moves.\n const [cancelled] = await this.db\n .update(jobRuns)\n .set({\n status: 'canceled',\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(\n and(eq(jobRuns.id, runId), notInStatus([...TERMINAL_STATUSES])),\n )\n .returning();\n\n if (!cancelled) return; // lost the race; already terminal\n\n if (opts.cascade === false) return;\n\n // Fetch descendants and branch on parent_close_policy.\n const descendants = await this.db\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.rootRunId, target.rootRunId),\n ne(jobRuns.id, runId),\n notInStatus([...TERMINAL_STATUSES]),\n ),\n );\n\n for (const child of descendants) {\n const policy = (child as JobRunRow).parentClosePolicy;\n if (policy === 'abandon') continue;\n // 'terminate' | 'cancel' — both transition the child to canceled.\n await this.db\n .update(jobRuns)\n .set({\n status: 'canceled',\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(\n and(\n eq(jobRuns.id, (child as JobRunRow).id),\n notInStatus([...TERMINAL_STATUSES]),\n ),\n );\n }\n\n void opts.reason; // reserved for future audit logging\n }\n\n // ==========================================================================\n // replay\n // ==========================================================================\n\n async replay(runId: string): Promise<JobRun> {\n // Load target + its job definition (we need replay_from).\n const [target] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!target) {\n throw new Error(`replay: run ${runId} not found`);\n }\n const run = target as JobRunRow;\n if (!TERMINAL_STATUSES.includes(run.status as TerminalStatus)) {\n throw new JobNotReplayableError(runId, run.status);\n }\n\n const [def] = await this.db\n .select()\n .from(jobs)\n .where(eq(jobs.type, run.jobType))\n .limit(1);\n if (!def) throw new JobTypeNotFoundError(run.jobType);\n const mode = (def as JobDefinitionRow).replayFrom;\n\n // Atomic: step reset + run reset must commit together.\n const result = await this.db.transaction(async (tx) => {\n if (mode === 'scratch') {\n await tx.delete(jobSteps).where(eq(jobSteps.jobRunId, runId));\n } else if (mode === 'last_step') {\n // Delete only non-completed step rows — completed steps stay memoised.\n await tx\n .delete(jobSteps)\n .where(\n and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, 'completed')),\n );\n } else {\n // 'last_checkpoint' — Phase 1 has no explicit checkpoint markers, so\n // behaviour collapses to `last_step`. See docs/specs/JOB-3.md\n // \"Implementation Decisions\" — planned divergence in a later phase.\n await tx\n .delete(jobSteps)\n .where(\n and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, 'completed')),\n );\n }\n\n const [updated] = await tx\n .update(jobRuns)\n .set({\n status: 'pending',\n attempts: 0,\n runAt: new Date(),\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n error: null,\n output: null,\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, runId))\n .returning();\n return updated as JobRunRow;\n });\n\n return result as JobRun;\n }\n\n // ==========================================================================\n // upsertJobRows — boot-time materialisation of `job` definitions\n // ==========================================================================\n\n /**\n * Hash-gated `INSERT … ON CONFLICT (type) DO UPDATE … WHERE` per Q3\n * resolution (2026-04-19): the `UPDATE` branch executes only when one\n * of the persisted metadata fields differs from the incoming payload;\n * `version` bumps only on real change; concurrent boots with identical\n * content are idempotent no-ops.\n *\n * Why this shape (not `DO NOTHING`, not advisory locks):\n * - `DO NOTHING` would let an old-version instance leave a stale row\n * that a new-version instance can't overwrite during a rolling deploy.\n * - Advisory locks add latency and leak risk under crashes.\n * - The `WHERE … IS DISTINCT FROM …` clause makes the conditional\n * atomic — no read-modify-write race on `version` between concurrent\n * boots.\n *\n * Orphan detection: a single `SELECT type FROM job WHERE type NOT IN (...)`\n * returns the types present in DB but absent from `entries`. Caller (boot\n * validator) decides whether to throw `BootValidationError`.\n */\n async upsertJobRows(\n entries: JobUpsertEntry[],\n poolConfig: ReadonlyMap<string, JobPoolDef>,\n ): Promise<{ orphaned: string[] }> {\n void poolConfig; // pool validation is the module's responsibility; orchestrator just persists\n\n for (const entry of entries) {\n const meta = entry.meta;\n const pool = meta.pool ?? 'batch';\n const retryPolicy = meta.retry ?? {\n attempts: 1,\n backoff: 'fixed' as const,\n baseMs: 0,\n };\n // JOB-FN-KEY (0.16.2): both authored key forms are honored. A `{{field}}`\n // string is persisted verbatim; a function is persisted as\n // `FN_KEY_SENTINEL` (non-null so the collision/dedupe path engages, and\n // hash-stable so the definition-hash gate doesn't churn on every boot —\n // the function identity can't be hashed). `start()` re-resolves the live\n // function from `JOB_HANDLER_REGISTRY`. The pre-0.16.2 `typeof === string\n // ? … : null` dropped function keys to null, so `collisionMode` silently\n // never engaged.\n const concurrencyKeyTemplateStr = keySelectorToTemplate(\n meta.concurrency?.key as JobKeySelector<unknown> | undefined,\n );\n const collisionMode =\n (meta.concurrency?.collisionMode as JobDefinitionRow['collisionMode']) ??\n 'queue';\n const dedupeKeyTemplateStr = keySelectorToTemplate(\n meta.dedupe?.key as JobKeySelector<unknown> | undefined,\n );\n const dedupeWindowMs = meta.dedupe?.windowMs ?? null;\n const timeoutMs = meta.timeoutMs ?? null;\n const replayFrom = meta.replayFrom ?? 'last_checkpoint';\n const scopeEntityType = meta.scope?.entity ?? null;\n // Q3 resolution: priority_default and replay_from are part of the\n // hashed metadata even though they aren't currently set via decorator\n // metadata above (priority_default has no `@JobHandler` field yet).\n // Default to 0 to keep UPDATE branch quiet across deploys.\n const priorityDefault = 0;\n\n // Hash-gated upsert: every metadata column appears in the WHERE clause\n // so the UPDATE branch only fires on a real change. `version` bumps\n // exactly when the WHERE matches.\n await this.db\n .insert(jobs)\n .values({\n type: entry.type,\n version: 1,\n pool,\n scopeEntityType,\n retryPolicy,\n timeoutMs,\n concurrencyKeyTemplate: concurrencyKeyTemplateStr,\n collisionMode,\n dedupeKeyTemplate: dedupeKeyTemplateStr,\n dedupeWindowMs,\n priorityDefault,\n replayFrom,\n })\n .onConflictDoUpdate({\n target: jobs.type,\n set: {\n pool: sql`EXCLUDED.pool`,\n scopeEntityType: sql`EXCLUDED.scope_entity_type`,\n retryPolicy: sql`EXCLUDED.retry_policy`,\n timeoutMs: sql`EXCLUDED.timeout_ms`,\n concurrencyKeyTemplate: sql`EXCLUDED.concurrency_key_template`,\n collisionMode: sql`EXCLUDED.collision_mode`,\n dedupeKeyTemplate: sql`EXCLUDED.dedupe_key_template`,\n dedupeWindowMs: sql`EXCLUDED.dedupe_window_ms`,\n priorityDefault: sql`EXCLUDED.priority_default`,\n replayFrom: sql`EXCLUDED.replay_from`,\n version: sql`${jobs.version} + 1`,\n updatedAt: sql`now()`,\n },\n // The hash gate: every field listed in the Q3 resolution appears\n // here. `IS DISTINCT FROM` is the null-safe inequality operator;\n // jsonb cast to text gives stable comparison without invoking a\n // dedicated hash column (avoids a JOB-1 schema migration).\n setWhere: sql`\n ${jobs.pool} IS DISTINCT FROM EXCLUDED.pool OR\n ${jobs.retryPolicy}::text IS DISTINCT FROM EXCLUDED.retry_policy::text OR\n ${jobs.timeoutMs} IS DISTINCT FROM EXCLUDED.timeout_ms OR\n ${jobs.concurrencyKeyTemplate} IS DISTINCT FROM EXCLUDED.concurrency_key_template OR\n ${jobs.collisionMode} IS DISTINCT FROM EXCLUDED.collision_mode OR\n ${jobs.dedupeKeyTemplate} IS DISTINCT FROM EXCLUDED.dedupe_key_template OR\n ${jobs.dedupeWindowMs} IS DISTINCT FROM EXCLUDED.dedupe_window_ms OR\n ${jobs.priorityDefault} IS DISTINCT FROM EXCLUDED.priority_default OR\n ${jobs.replayFrom} IS DISTINCT FROM EXCLUDED.replay_from OR\n ${jobs.scopeEntityType} IS DISTINCT FROM EXCLUDED.scope_entity_type\n `,\n });\n }\n\n // Orphan detection: any `job` row whose type is not in the registry.\n const types = entries.map((e) => e.type);\n const orphans =\n types.length === 0\n ? await this.db.select({ type: jobs.type }).from(jobs)\n : await this.db\n .select({ type: jobs.type })\n .from(jobs)\n .where(notInArray(jobs.type, types));\n\n return { orphaned: orphans.map((o) => o.type) };\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction notInStatus(statuses: JobRunStatus[]) {\n // Drizzle's inArray composes with `not` via negation helper; use raw sql\n // to stay readable. `inArray` + `.not()` isn't idiomatic in 0.45.\n const negated = statuses.map((s) => ne(jobRuns.status, s));\n return and(...negated);\n}\n\n// `isNotNull` + `gt` imports are retained for potential future use; silence\n// unused-import lint by re-exporting via `void`.\nvoid isNotNull;\n"],"mappings":"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","names":["dedupeKey"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PollChangeSource
|
|
3
|
-
} from "./chunk-4MF3HKJA.js";
|
|
4
1
|
import {
|
|
5
2
|
WebhookChangeSource
|
|
6
3
|
} from "./chunk-TIZXQU26.js";
|
|
4
|
+
import {
|
|
5
|
+
PollChangeSource
|
|
6
|
+
} from "./chunk-4MF3HKJA.js";
|
|
7
7
|
|
|
8
8
|
// runtime/subsystems/integration/build-change-source.ts
|
|
9
9
|
function buildChangeSource(cfg, fetch, middlewares = []) {
|
|
@@ -26,4 +26,4 @@ function buildChangeSource(cfg, fetch, middlewares = []) {
|
|
|
26
26
|
export {
|
|
27
27
|
buildChangeSource
|
|
28
28
|
};
|
|
29
|
-
//# sourceMappingURL=chunk-
|
|
29
|
+
//# sourceMappingURL=chunk-I6UXRJ3Q.js.map
|
|
@@ -21,11 +21,14 @@ var DEFAULT_IGNORE_FIELDS = /* @__PURE__ */ new Set([
|
|
|
21
21
|
var DeepEqualDiffer = class {
|
|
22
22
|
ignore;
|
|
23
23
|
constructor(opts = {}) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const merged = new Set(DEFAULT_IGNORE_FIELDS);
|
|
25
|
+
if (opts.ignore) {
|
|
26
|
+
for (const field of opts.ignore) merged.add(field);
|
|
27
|
+
}
|
|
28
|
+
if (opts.unignore) {
|
|
29
|
+
for (const field of opts.unignore) merged.delete(field);
|
|
28
30
|
}
|
|
31
|
+
this.ignore = merged;
|
|
29
32
|
}
|
|
30
33
|
diff(existing, incoming, providerChangedFields) {
|
|
31
34
|
if (existing === null) {
|
|
@@ -104,4 +107,4 @@ function deepEqualObject(a, b) {
|
|
|
104
107
|
export {
|
|
105
108
|
DeepEqualDiffer
|
|
106
109
|
};
|
|
107
|
-
//# sourceMappingURL=chunk-
|
|
110
|
+
//# sourceMappingURL=chunk-JEINYUJH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../runtime/subsystems/integration/deep-equal.differ.ts"],"sourcesContent":["/**\n * DeepEqualDiffer — default `IFieldDiffer<T>` for the integration subsystem (SYNC-5).\n *\n * Walks every field of `incoming` against `existing`, emitting a structured\n * per-field diff (`{ from, to }`) for every field whose value changed.\n * Returns `'noop'` when the record is unchanged.\n *\n * Design decisions (extracted from the upstream consumer + HS-9 findings):\n *\n * 1. **Ignore list** — row metadata that sinks/services stamp unconditionally\n * so upstream cannot reasonably disagree:\n * `id`, `createdAt`, `updatedAt`, `deletedAt`, `type`,\n * `lastModifiedAt`, `fields`, `providerMetadata`\n * (`fields` is the EAV bag — it's diffed by the sink's EAV dual-write\n * path, not at the canonical-record layer.) Consumers augment the list via\n * `options.ignore` and — when a default is domain data for their entity —\n * REMOVE a default via `options.unignore` (e.g. an entity whose\n * `deletedAt` is a vendor-observed retraction tombstone, not row metadata;\n * see `DeepEqualDifferOptions.unignore`).\n *\n * 2. **`providerChangedFields` hint (CDC)** — when present, restricts the\n * comparison to the hinted field set. The hint is advisory; fields in\n * the ignore list are still filtered out even when hinted. Provider\n * hints are field-NAME-level; they don't override the ignore rules.\n *\n * 3. **Date → ISO string** — `Date` instances are normalized to\n * `toISOString()` before comparison. Sinks return `Date` from the DB\n * driver; adapters typically deliver strings. Direct `===` would\n * always say \"changed.\"\n *\n * 4. **Decimal-string vs number** — Postgres `numeric` columns return as\n * strings through Drizzle; adapters deliver numbers. When one side is a\n * number and the other is a numeric string that parses to the same\n * number, they're equal. The normalizer does NOT coerce non-numeric\n * strings, and it preserves zero-vs-null distinction.\n *\n * 5. **null-existing path** — `diff(null, incoming)` produces a full\n * created-shape diff (`{from: null, to: <value>}` for every non-ignored\n * field). Orchestrator sees this and records `operation: 'created'`.\n */\nimport { Injectable } from '@nestjs/common';\nimport type {\n DiffResult,\n FieldDiff,\n IFieldDiffer,\n} from './integration-field-diff.protocol';\n\n/**\n * Default ignore list. Keep in integration with consumer canonical-record shapes —\n * adding a row-metadata field here means no integration will ever mark it changed.\n *\n * Includes the columns contributed by the `external_id_tracking` behavior\n * (`external_id`/`externalId`, `provider`, `provider_metadata`/`providerMetadata`).\n * These are integration-tracking metadata, not domain attributes: they ride on the\n * canonical record but must never register as a field change (the external id\n * is the record's identity, not a mutable value). Listed in both snake_case\n * and camelCase so the differ ignores them regardless of the consumer's\n * canonical projection casing.\n */\nconst DEFAULT_IGNORE_FIELDS: ReadonlySet<string> = new Set([\n 'id',\n 'createdAt',\n 'updatedAt',\n 'deletedAt',\n 'type',\n 'lastModifiedAt',\n 'fields',\n 'external_id',\n 'externalId',\n 'provider',\n 'provider_metadata',\n 'providerMetadata',\n]);\n\nexport interface DeepEqualDifferOptions {\n /**\n * Extra field names to ignore in addition to the defaults. Consumers can\n * pass `['integration_version']` etc. to augment the base list; values here are\n * merged (not replaced) with `DEFAULT_IGNORE_FIELDS`.\n */\n readonly ignore?: readonly string[];\n\n /**\n * Field names to REMOVE from the default ignore list — the inverse of\n * `ignore`. Use this to declare that a normally-metadata column is in fact\n * DOMAIN DATA for this entity and must register as a field change.\n *\n * The canonical case (swe-brain ADR-0008 §1, the gap this knob closes):\n * `deletedAt` is in `DEFAULT_IGNORE_FIELDS` because most sinks stamp it as\n * row metadata sinks own unconditionally. But an entity with\n * `softDelete: false` and a domain-owned `deleted_at` carries the\n * vendor-observed retraction tombstone ON the canonical record (a Slack\n * `message_deleted` → `deletedAt`). Without un-ignoring it, the tombstone\n * overlay diffs to `'noop'`, the upsert is skipped, and `deleted_at` never\n * lands. `unignore: ['deletedAt']` makes the differ treat it as domain data.\n *\n * Applied AFTER `ignore` is merged, so `unignore` wins on a field listed in\n * both. Subtracting a field not in the (merged) ignore set is a harmless\n * no-op. Does not touch `DEFAULT_IGNORE_FIELDS` for any other instance.\n */\n readonly unignore?: readonly string[];\n}\n\n@Injectable()\nexport class DeepEqualDiffer<T extends Record<string, unknown>>\n implements IFieldDiffer<T>\n{\n private readonly ignore: ReadonlySet<string>;\n\n constructor(opts: DeepEqualDifferOptions = {}) {\n const merged = new Set<string>(DEFAULT_IGNORE_FIELDS);\n if (opts.ignore) {\n for (const field of opts.ignore) merged.add(field);\n }\n // `unignore` is subtracted last so it wins over a field that also appears\n // in `ignore` or the defaults — \"this column is domain data here.\"\n if (opts.unignore) {\n for (const field of opts.unignore) merged.delete(field);\n }\n this.ignore = merged;\n }\n\n diff(\n existing: T | null,\n incoming: T,\n providerChangedFields?: string[],\n ): DiffResult {\n // Created-shape: every non-ignored field becomes `{from: null, to}`.\n if (existing === null) {\n const out: FieldDiff = {};\n for (const key of Object.keys(incoming)) {\n if (this.ignore.has(key)) continue;\n const value = (incoming as Record<string, unknown>)[key];\n // Skip fields that are themselves null/undefined — a created record\n // doesn't need to declare \"this field is null now\" for every\n // untouched column.\n if (value === null || value === undefined) continue;\n out[key] = { from: null, to: value };\n }\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n\n // Field set to compare. `providerChangedFields` narrows to a hint set;\n // ignored fields are filtered out regardless of hint.\n const candidates = new Set<string>();\n if (providerChangedFields && providerChangedFields.length > 0) {\n for (const key of providerChangedFields) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n } else {\n for (const key of Object.keys(incoming)) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n // Also include keys that exist on existing but not on incoming —\n // e.g. a field that was cleared. This would otherwise be missed when\n // incoming carries an undefined column we drop from the iteration.\n for (const key of Object.keys(existing)) {\n if (this.ignore.has(key)) continue;\n if (!(key in (incoming as Record<string, unknown>))) continue;\n candidates.add(key);\n }\n }\n\n const out: FieldDiff = {};\n for (const key of candidates) {\n const before = (existing as Record<string, unknown>)[key];\n const after = (incoming as Record<string, unknown>)[key];\n if (!isEqual(before, after)) {\n out[key] = { from: before ?? null, to: after ?? null };\n }\n }\n\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n}\n\n// ─── equality helpers ───────────────────────────────────────────────────────\n\n/**\n * Field-level equality with the canonical-integration normalizations:\n * - Date → toISOString (adapters deliver strings)\n * - numeric-string vs number → numeric equality when both parse\n * - deep equality for plain objects/arrays (single-level is enough for\n * canonical records; nested records travel as jsonb columns where the\n * sink already owns the comparison)\n */\nfunction isEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n\n const na = normalize(a);\n const nb = normalize(b);\n if (na === nb) return true;\n\n // After normalization: both may still be non-primitive objects.\n if (\n typeof na === 'object' &&\n typeof nb === 'object' &&\n na !== null &&\n nb !== null\n ) {\n return deepEqualObject(na as Record<string, unknown>, nb as Record<string, unknown>);\n }\n\n // Numeric string ↔ number: when one side is a number and the other is a\n // string that parses to the same finite number.\n const numericEqual = maybeNumericEqual(na, nb) || maybeNumericEqual(nb, na);\n return numericEqual;\n}\n\nfunction normalize(value: unknown): unknown {\n if (value instanceof Date) return value.toISOString();\n return value;\n}\n\nfunction maybeNumericEqual(a: unknown, b: unknown): boolean {\n // a is string-shape, b is number — parse a and compare. Only when the\n // string looks numeric AND the parse round-trips (no silent NaN pass-\n // through on non-numeric strings).\n if (typeof a !== 'string' || typeof b !== 'number') return false;\n if (a.trim() === '') return false;\n const parsed = Number(a);\n if (!Number.isFinite(parsed)) return false;\n return parsed === b;\n}\n\nfunction deepEqualObject(\n a: Record<string, unknown>,\n b: Record<string, unknown>,\n): boolean {\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!(key in b)) return false;\n if (!isEqual(a[key], b[key])) return false;\n }\n return true;\n}\n"],"mappings":"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","names":["out"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
assertTenantId
|
|
3
|
-
} from "./chunk-6DWFJNIK.js";
|
|
4
1
|
import {
|
|
5
2
|
bridgeDelivery
|
|
6
3
|
} from "./chunk-2TVVBC53.js";
|
|
4
|
+
import {
|
|
5
|
+
assertTenantId
|
|
6
|
+
} from "./chunk-6DWFJNIK.js";
|
|
7
7
|
import {
|
|
8
8
|
BRIDGE_MULTI_TENANT
|
|
9
9
|
} from "./chunk-4LH67P4U.js";
|
|
@@ -119,4 +119,4 @@ DrizzleBridgeDeliveryRepo = __decorateClass([
|
|
|
119
119
|
export {
|
|
120
120
|
DrizzleBridgeDeliveryRepo
|
|
121
121
|
};
|
|
122
|
-
//# sourceMappingURL=chunk-
|
|
122
|
+
//# sourceMappingURL=chunk-KSTZIULO.js.map
|
|
@@ -3,28 +3,28 @@ import {
|
|
|
3
3
|
} from "./chunk-5A432NZJ.js";
|
|
4
4
|
import {
|
|
5
5
|
EventFlowService
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7OVCARTQ.js";
|
|
7
7
|
import {
|
|
8
8
|
BRIDGE_RESERVED_POOLS
|
|
9
9
|
} from "./chunk-EDKJU5BO.js";
|
|
10
10
|
import {
|
|
11
11
|
DrizzleBridgeDeliveryRepo
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-KSTZIULO.js";
|
|
13
13
|
import {
|
|
14
14
|
MemoryBridgeDeliveryRepo
|
|
15
15
|
} from "./chunk-4DOJBQTP.js";
|
|
16
16
|
import {
|
|
17
17
|
BridgeOutboxDrainHook
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-65MO75WM.js";
|
|
19
19
|
import {
|
|
20
20
|
BridgeDeliveryHandler
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-R6F6KFIL.js";
|
|
22
22
|
import {
|
|
23
23
|
BridgeReservedPoolsNotPolledError
|
|
24
24
|
} from "./chunk-NXXDZ6ZF.js";
|
|
25
25
|
import {
|
|
26
26
|
JOB_WORKER_MODULE_OPTIONS
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-235ZMMJR.js";
|
|
28
28
|
import {
|
|
29
29
|
BRIDGE_DELIVERY_REPO,
|
|
30
30
|
BRIDGE_MODULE_OPTIONS,
|
|
@@ -119,4 +119,4 @@ BridgeModule = __decorateClass([
|
|
|
119
119
|
export {
|
|
120
120
|
BridgeModule
|
|
121
121
|
};
|
|
122
|
-
//# sourceMappingURL=chunk-
|
|
122
|
+
//# sourceMappingURL=chunk-KZDHMZ45.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-GM3RMJIJ.js";
|
|
4
4
|
import {
|
|
5
5
|
DrizzleEventBus
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-B34G6PHD.js";
|
|
7
7
|
import {
|
|
8
8
|
MemoryEventBus
|
|
9
9
|
} from "./chunk-Z7PQCAVK.js";
|
|
@@ -152,4 +152,4 @@ EventsModule = __decorateClass([
|
|
|
152
152
|
export {
|
|
153
153
|
EventsModule
|
|
154
154
|
};
|
|
155
|
-
//# sourceMappingURL=chunk-
|
|
155
|
+
//# sourceMappingURL=chunk-OZEPJGMA.js.map
|
|
@@ -27,24 +27,60 @@ var PgNotifyListener = class {
|
|
|
27
27
|
backoffMaxMs;
|
|
28
28
|
/** WARN-once gate so a flapping listener doesn't spam the log. */
|
|
29
29
|
warnedDown = false;
|
|
30
|
+
/**
|
|
31
|
+
* LISTEN-NOTIFY-2 — the in-flight `connect()` promise, set while a checkout is
|
|
32
|
+
* mid-`await`. `stop()` awaits it so a `stop()` that races a still-resolving
|
|
33
|
+
* `connect()` can't return before the connect either assigns `this.client`
|
|
34
|
+
* (then released by `releaseClient`) or self-releases the checked-out client.
|
|
35
|
+
* Without this, a `stop()` arriving during `pool.connect()`'s await saw
|
|
36
|
+
* `this.client === null` (nothing to release), then `connect()` resumed,
|
|
37
|
+
* assigned the client, and issued `LISTEN` — leaking an ESTABLISHED socket
|
|
38
|
+
* holding `LISTEN <channel>` forever past `app.close()`.
|
|
39
|
+
*/
|
|
40
|
+
connecting = null;
|
|
30
41
|
/** Begin listening. Idempotent-ish: a second call while connected is a no-op. */
|
|
31
42
|
async start() {
|
|
32
43
|
this.stopped = false;
|
|
33
44
|
await this.connect();
|
|
34
45
|
}
|
|
35
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Stop listening + release the connection. Safe to call repeatedly and
|
|
48
|
+
* race-safe against an in-flight `connect()` (LISTEN-NOTIFY-2): it sets
|
|
49
|
+
* `stopped` first (so a resuming `connect()` self-releases its checkout),
|
|
50
|
+
* then awaits any in-flight connect, then releases whatever client landed.
|
|
51
|
+
*/
|
|
36
52
|
async stop() {
|
|
37
53
|
this.stopped = true;
|
|
38
54
|
if (this.reconnectTimer) {
|
|
39
55
|
clearTimeout(this.reconnectTimer);
|
|
40
56
|
this.reconnectTimer = null;
|
|
41
57
|
}
|
|
58
|
+
const inflight = this.connecting;
|
|
59
|
+
if (inflight) {
|
|
60
|
+
try {
|
|
61
|
+
await inflight;
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
42
65
|
await this.releaseClient();
|
|
43
66
|
}
|
|
44
67
|
async connect() {
|
|
45
68
|
if (this.stopped) return;
|
|
69
|
+
const attempt = this.doConnect();
|
|
70
|
+
this.connecting = attempt;
|
|
71
|
+
try {
|
|
72
|
+
await attempt;
|
|
73
|
+
} finally {
|
|
74
|
+
if (this.connecting === attempt) this.connecting = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async doConnect() {
|
|
46
78
|
try {
|
|
47
79
|
const client = await this.opts.pool.connect();
|
|
80
|
+
if (this.stopped) {
|
|
81
|
+
await this.releaseRawClient(client);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
48
84
|
client.on("notification", (msg) => {
|
|
49
85
|
if (msg.channel !== this.opts.channel) return;
|
|
50
86
|
try {
|
|
@@ -58,6 +94,10 @@ var PgNotifyListener = class {
|
|
|
58
94
|
this.handleDrop();
|
|
59
95
|
});
|
|
60
96
|
await client.query(`LISTEN ${this.opts.channel}`);
|
|
97
|
+
if (this.stopped) {
|
|
98
|
+
await this.releaseRawClient(client);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
61
101
|
this.client = client;
|
|
62
102
|
if (this.warnedDown) {
|
|
63
103
|
this.logger.log(
|
|
@@ -99,6 +139,15 @@ var PgNotifyListener = class {
|
|
|
99
139
|
const client = this.client;
|
|
100
140
|
this.client = null;
|
|
101
141
|
if (!client) return;
|
|
142
|
+
await this.releaseRawClient(client);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Tear down a raw checked-out client (LISTEN-NOTIFY-2). Used both by the
|
|
146
|
+
* normal `releaseClient()` path and by the connect-vs-stop race bail-outs,
|
|
147
|
+
* where the client was checked out but never assigned to `this.client`.
|
|
148
|
+
* Destroys (`release(true)`) so a half-listening socket is never reused.
|
|
149
|
+
*/
|
|
150
|
+
async releaseRawClient(client) {
|
|
102
151
|
try {
|
|
103
152
|
client.removeAllListeners?.("notification");
|
|
104
153
|
client.removeAllListeners?.("error");
|
|
@@ -115,4 +164,4 @@ export {
|
|
|
115
164
|
pgNotify,
|
|
116
165
|
PgNotifyListener
|
|
117
166
|
};
|
|
118
|
-
//# sourceMappingURL=chunk-
|
|
167
|
+
//# sourceMappingURL=chunk-Q6LRJ4VI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../runtime/subsystems/jobs/pg-notify.ts"],"sourcesContent":["/**\n * PgNotifyListener + pgNotify — Postgres LISTEN/NOTIFY wakeups\n * (LISTEN-NOTIFY-1, dogfood gap #7).\n *\n * The drizzle jobs worker and events outbox drainer poll on an interval today\n * (default 1 s/hop). With `listen_notify` enabled, a row write that makes work\n * claimable emits an in-transaction `pg_notify(...)`; a dedicated listener\n * connection wakes the polling loop the moment the writing transaction commits.\n *\n * Two halves:\n * - `pgNotify(tx, channel, payload)` — fire an in-tx `pg_notify`. MUST be\n * called with the SAME transaction handle as the row write it announces, so\n * Postgres delivers it only on commit (the transactional-outbox guarantee).\n * - `PgNotifyListener` — owns a single long-lived `pg.PoolClient`, issues\n * `LISTEN <channel>`, forwards each notification's payload to an owner\n * callback, debounces bursts, and reconnects with capped backoff on drop.\n *\n * **Polling never stops.** This is a wake-early optimisation layered ON TOP of\n * interval polling. A lost notification (listener down, pooler eats the LISTEN,\n * etc.) degrades to today's poll latency, never to lost work — the claim/drain\n * query remains the source of truth.\n *\n * **PgBouncer caveat:** session-scoped `LISTEN` does not survive a\n * transaction-mode pooler. `listen_notify` requires a direct (or session-mode)\n * connection; behind a transaction pooler notifies are simply never received and\n * the system degrades to polling. See the jobs config block / skill.\n */\n// TODO(logging-subsystem): swap to ILogger once ADR-028 lands\nimport { Logger } from '@nestjs/common';\nimport { sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\n\n/** Channel the jobs worker LISTENs on; payload = pool name. */\nexport const JOBS_WAKE_CHANNEL = 'codegen_jobs_wake';\n/** Channel the events drainer LISTENs on; payload = event pool (or ''). */\nexport const EVENTS_WAKE_CHANNEL = 'codegen_events_wake';\n\n/**\n * Emit an in-transaction `pg_notify`. Call with the SAME `tx`/client handle as\n * the row write being announced so delivery is gated on commit. `payload` is a\n * short plain string (a pool name); it is NOT JSON — the wake is a hint and the\n * subsequent claim/drain query is authoritative. Channel names are framework\n * constants (never user input), so the `set_config`-free literal-channel form is\n * safe; the payload is bound as a parameter.\n */\nexport async function pgNotify(\n tx: DrizzleClient | DrizzleTransaction,\n channel: string,\n payload: string,\n): Promise<void> {\n const client = tx as DrizzleClient;\n // `pg_notify(channel, payload)` is the function form (vs the `NOTIFY chan,\n // 'payload'` statement form) precisely because it accepts bound parameters —\n // the payload is parameterised, never string-concatenated.\n await client.execute(sql`select pg_notify(${channel}, ${payload})`);\n}\n\n/** Minimal structural view of the `pg` Client/PoolClient surface we touch. */\ninterface PgListenClient {\n query(text: string): Promise<unknown>;\n on(event: 'notification', cb: (msg: { channel: string; payload?: string }) => void): void;\n on(event: 'error', cb: (err: Error) => void): void;\n removeAllListeners?: (event?: string) => void;\n release?: (err?: boolean) => void;\n end?: () => Promise<void>;\n}\n\n/** Minimal structural view of the `pg` Pool's `connect()`. */\ninterface PgPoolish {\n connect(): Promise<PgListenClient>;\n}\n\nconst DEFAULT_BACKOFF_MIN_MS = 100;\nconst DEFAULT_BACKOFF_MAX_MS = 5_000;\n\nexport interface PgNotifyListenerOptions {\n /** Channel to LISTEN on. */\n channel: string;\n /**\n * The underlying `pg.Pool` — obtained from `drizzleClient.$client`. A\n * dedicated `PoolClient` is checked out and held for the listener's lifetime\n * (separate from the query pool so a slow query never delays a wake).\n */\n pool: PgPoolish;\n /**\n * Called for every notification on `channel`, with the raw payload string\n * (`''` when Postgres delivers an empty payload). The owner decides whether\n * the payload is relevant (e.g. \"is this one of my pools?\") and debounces its\n * own claim cycle.\n */\n onNotify: (payload: string) => void;\n /** Label used in log lines (e.g. 'jobs:interactive', 'events'). */\n label: string;\n backoffMinMs?: number;\n backoffMaxMs?: number;\n}\n\n/**\n * Holds a dedicated listener connection and forwards notifications to `onNotify`.\n * Reconnects with capped exponential backoff on drop; logs the first failure +\n * the recovery exactly once each so a flapping connection doesn't flood logs.\n */\nexport class PgNotifyListener {\n private readonly logger: Logger;\n private client: PgListenClient | null = null;\n private stopped = false;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private backoffMs: number;\n private readonly backoffMinMs: number;\n private readonly backoffMaxMs: number;\n /** WARN-once gate so a flapping listener doesn't spam the log. */\n private warnedDown = false;\n /**\n * LISTEN-NOTIFY-2 — the in-flight `connect()` promise, set while a checkout is\n * mid-`await`. `stop()` awaits it so a `stop()` that races a still-resolving\n * `connect()` can't return before the connect either assigns `this.client`\n * (then released by `releaseClient`) or self-releases the checked-out client.\n * Without this, a `stop()` arriving during `pool.connect()`'s await saw\n * `this.client === null` (nothing to release), then `connect()` resumed,\n * assigned the client, and issued `LISTEN` — leaking an ESTABLISHED socket\n * holding `LISTEN <channel>` forever past `app.close()`.\n */\n private connecting: Promise<void> | null = null;\n\n constructor(private readonly opts: PgNotifyListenerOptions) {\n this.logger = new Logger(`PgNotifyListener(${opts.label})`);\n this.backoffMinMs = opts.backoffMinMs ?? DEFAULT_BACKOFF_MIN_MS;\n this.backoffMaxMs = opts.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS;\n this.backoffMs = this.backoffMinMs;\n }\n\n /** Begin listening. Idempotent-ish: a second call while connected is a no-op. */\n async start(): Promise<void> {\n this.stopped = false;\n await this.connect();\n }\n\n /**\n * Stop listening + release the connection. Safe to call repeatedly and\n * race-safe against an in-flight `connect()` (LISTEN-NOTIFY-2): it sets\n * `stopped` first (so a resuming `connect()` self-releases its checkout),\n * then awaits any in-flight connect, then releases whatever client landed.\n */\n async stop(): Promise<void> {\n this.stopped = true;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n // Await an in-flight checkout so we don't return while a client is still\n // mid-`pool.connect()`. The resuming `connect()` sees `stopped` and either\n // self-releases its checkout or assigns `this.client`; either way the\n // `releaseClient()` below mops up.\n const inflight = this.connecting;\n if (inflight) {\n try {\n await inflight;\n } catch {\n // connect failures are handled inside connect(); ignore here.\n }\n }\n await this.releaseClient();\n }\n\n private async connect(): Promise<void> {\n if (this.stopped) return;\n // Track this checkout so a racing stop() can await it (LISTEN-NOTIFY-2).\n const attempt = this.doConnect();\n this.connecting = attempt;\n try {\n await attempt;\n } finally {\n if (this.connecting === attempt) this.connecting = null;\n }\n }\n\n private async doConnect(): Promise<void> {\n try {\n const client = await this.opts.pool.connect();\n // Re-check AFTER the await resolves: a stop() may have fired while this\n // checkout was in flight. If so, release the just-checked-out client\n // right here and bail BEFORE wiring handlers / issuing LISTEN — otherwise\n // we'd leak an ESTABLISHED listener socket past shutdown (LISTEN-NOTIFY-2).\n if (this.stopped) {\n await this.releaseRawClient(client);\n return;\n }\n client.on('notification', (msg) => {\n if (msg.channel !== this.opts.channel) return;\n try {\n this.opts.onNotify(msg.payload ?? '');\n } catch (err) {\n this.logger.error(`onNotify threw: ${(err as Error).message}`);\n }\n });\n client.on('error', (err) => {\n // A connection-level error is the signal to reconnect. Don't double-log\n // here — scheduleReconnect owns the WARN-once.\n this.logger.debug?.(`listener connection error: ${err.message}`);\n this.handleDrop();\n });\n await client.query(`LISTEN ${this.opts.channel}`);\n // A stop() could have fired during the LISTEN round-trip too — same guard.\n if (this.stopped) {\n await this.releaseRawClient(client);\n return;\n }\n this.client = client;\n // Recovery: only announce if we had previously warned about being down.\n if (this.warnedDown) {\n this.logger.log(\n `listener reconnected; LISTEN ${this.opts.channel} re-established`,\n );\n this.warnedDown = false;\n }\n this.backoffMs = this.backoffMinMs;\n } catch (err) {\n this.handleConnectFailure(err);\n }\n }\n\n /** Connection dropped after being established → reconnect. */\n private handleDrop(): void {\n if (this.stopped) return;\n void this.releaseClient().finally(() => this.scheduleReconnect());\n }\n\n /** Initial / reconnect `connect()` threw. */\n private handleConnectFailure(err: unknown): void {\n this.scheduleReconnect(err);\n }\n\n private scheduleReconnect(err?: unknown): void {\n if (this.stopped) return;\n if (!this.warnedDown) {\n this.warnedDown = true;\n this.logger.warn(\n `listener down — falling back to interval polling until reconnect. ` +\n `Cause: ${err instanceof Error ? err.message : 'connection lost'}. ` +\n `(This degrades latency, not durability — polling still drives all work.)`,\n );\n }\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n const delay = this.backoffMs;\n this.backoffMs = Math.min(this.backoffMs * 2, this.backoffMaxMs);\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n void this.connect();\n }, delay);\n }\n\n private async releaseClient(): Promise<void> {\n const client = this.client;\n this.client = null;\n if (!client) return;\n await this.releaseRawClient(client);\n }\n\n /**\n * Tear down a raw checked-out client (LISTEN-NOTIFY-2). Used both by the\n * normal `releaseClient()` path and by the connect-vs-stop race bail-outs,\n * where the client was checked out but never assigned to `this.client`.\n * Destroys (`release(true)`) so a half-listening socket is never reused.\n */\n private async releaseRawClient(client: PgListenClient): Promise<void> {\n try {\n client.removeAllListeners?.('notification');\n client.removeAllListeners?.('error');\n if (client.release) client.release(true);\n else if (client.end) await client.end();\n } catch {\n // best-effort teardown\n }\n }\n}\n"],"mappings":";AA4BA,SAAS,cAAc;AACvB,SAAS,WAAW;AAKb,IAAM,oBAAoB;AAE1B,IAAM,sBAAsB;AAUnC,eAAsB,SACpB,IACA,SACA,SACe;AACf,QAAM,SAAS;AAIf,QAAM,OAAO,QAAQ,uBAAuB,OAAO,KAAK,OAAO,GAAG;AACpE;AAiBA,IAAM,yBAAyB;AAC/B,IAAM,yBAAyB;AA6BxB,IAAM,mBAAN,MAAuB;AAAA,EAsB5B,YAA6B,MAA+B;AAA/B;AAC3B,SAAK,SAAS,IAAI,OAAO,oBAAoB,KAAK,KAAK,GAAG;AAC1D,SAAK,eAAe,KAAK,gBAAgB;AACzC,SAAK,eAAe,KAAK,gBAAgB;AACzC,SAAK,YAAY,KAAK;AAAA,EACxB;AAAA,EAL6B;AAAA,EArBZ;AAAA,EACT,SAAgC;AAAA,EAChC,UAAU;AAAA,EACV,iBAAuD;AAAA,EACvD;AAAA,EACS;AAAA,EACA;AAAA;AAAA,EAET,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWb,aAAmC;AAAA;AAAA,EAU3C,MAAM,QAAuB;AAC3B,SAAK,UAAU;AACf,UAAM,KAAK,QAAQ;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,OAAsB;AAC1B,SAAK,UAAU;AACf,QAAI,KAAK,gBAAgB;AACvB,mBAAa,KAAK,cAAc;AAChC,WAAK,iBAAiB;AAAA,IACxB;AAKA,UAAM,WAAW,KAAK;AACtB,QAAI,UAAU;AACZ,UAAI;AACF,cAAM;AAAA,MACR,QAAQ;AAAA,MAER;AAAA,IACF;AACA,UAAM,KAAK,cAAc;AAAA,EAC3B;AAAA,EAEA,MAAc,UAAyB;AACrC,QAAI,KAAK,QAAS;AAElB,UAAM,UAAU,KAAK,UAAU;AAC/B,SAAK,aAAa;AAClB,QAAI;AACF,YAAM;AAAA,IACR,UAAE;AACA,UAAI,KAAK,eAAe,QAAS,MAAK,aAAa;AAAA,IACrD;AAAA,EACF;AAAA,EAEA,MAAc,YAA2B;AACvC,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,KAAK,KAAK,QAAQ;AAK5C,UAAI,KAAK,SAAS;AAChB,cAAM,KAAK,iBAAiB,MAAM;AAClC;AAAA,MACF;AACA,aAAO,GAAG,gBAAgB,CAAC,QAAQ;AACjC,YAAI,IAAI,YAAY,KAAK,KAAK,QAAS;AACvC,YAAI;AACF,eAAK,KAAK,SAAS,IAAI,WAAW,EAAE;AAAA,QACtC,SAAS,KAAK;AACZ,eAAK,OAAO,MAAM,mBAAoB,IAAc,OAAO,EAAE;AAAA,QAC/D;AAAA,MACF,CAAC;AACD,aAAO,GAAG,SAAS,CAAC,QAAQ;AAG1B,aAAK,OAAO,QAAQ,8BAA8B,IAAI,OAAO,EAAE;AAC/D,aAAK,WAAW;AAAA,MAClB,CAAC;AACD,YAAM,OAAO,MAAM,UAAU,KAAK,KAAK,OAAO,EAAE;AAEhD,UAAI,KAAK,SAAS;AAChB,cAAM,KAAK,iBAAiB,MAAM;AAClC;AAAA,MACF;AACA,WAAK,SAAS;AAEd,UAAI,KAAK,YAAY;AACnB,aAAK,OAAO;AAAA,UACV,gCAAgC,KAAK,KAAK,OAAO;AAAA,QACnD;AACA,aAAK,aAAa;AAAA,MACpB;AACA,WAAK,YAAY,KAAK;AAAA,IACxB,SAAS,KAAK;AACZ,WAAK,qBAAqB,GAAG;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA,EAGQ,aAAmB;AACzB,QAAI,KAAK,QAAS;AAClB,SAAK,KAAK,cAAc,EAAE,QAAQ,MAAM,KAAK,kBAAkB,CAAC;AAAA,EAClE;AAAA;AAAA,EAGQ,qBAAqB,KAAoB;AAC/C,SAAK,kBAAkB,GAAG;AAAA,EAC5B;AAAA,EAEQ,kBAAkB,KAAqB;AAC7C,QAAI,KAAK,QAAS;AAClB,QAAI,CAAC,KAAK,YAAY;AACpB,WAAK,aAAa;AAClB,WAAK,OAAO;AAAA,QACV,iFACY,eAAe,QAAQ,IAAI,UAAU,iBAAiB;AAAA,MAEpE;AAAA,IACF;AACA,QAAI,KAAK,eAAgB,cAAa,KAAK,cAAc;AACzD,UAAM,QAAQ,KAAK;AACnB,SAAK,YAAY,KAAK,IAAI,KAAK,YAAY,GAAG,KAAK,YAAY;AAC/D,SAAK,iBAAiB,WAAW,MAAM;AACrC,WAAK,iBAAiB;AACtB,WAAK,KAAK,QAAQ;AAAA,IACpB,GAAG,KAAK;AAAA,EACV;AAAA,EAEA,MAAc,gBAA+B;AAC3C,UAAM,SAAS,KAAK;AACpB,SAAK,SAAS;AACd,QAAI,CAAC,OAAQ;AACb,UAAM,KAAK,iBAAiB,MAAM;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,iBAAiB,QAAuC;AACpE,QAAI;AACF,aAAO,qBAAqB,cAAc;AAC1C,aAAO,qBAAqB,OAAO;AACnC,UAAI,OAAO,QAAS,QAAO,QAAQ,IAAI;AAAA,eAC9B,OAAO,IAAK,OAAM,OAAO,IAAI;AAAA,IACxC,QAAQ;AAAA,IAER;AAAA,EACF;AACF;","names":[]}
|
|
@@ -4,18 +4,18 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
JobHandler,
|
|
6
6
|
JobHandlerBase
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7P5ODGLA.js";
|
|
8
8
|
import {
|
|
9
9
|
JOB_ORCHESTRATOR
|
|
10
10
|
} from "./chunk-ZPL74UQN.js";
|
|
11
|
+
import {
|
|
12
|
+
EVENT_BUS
|
|
13
|
+
} from "./chunk-H5NH7KPE.js";
|
|
11
14
|
import {
|
|
12
15
|
BRIDGE_DELIVERY_REPO,
|
|
13
16
|
BRIDGE_MULTI_TENANT,
|
|
14
17
|
BRIDGE_REGISTRY
|
|
15
18
|
} from "./chunk-4LH67P4U.js";
|
|
16
|
-
import {
|
|
17
|
-
EVENT_BUS
|
|
18
|
-
} from "./chunk-H5NH7KPE.js";
|
|
19
19
|
import {
|
|
20
20
|
__decorateClass,
|
|
21
21
|
__decorateParam
|
|
@@ -118,4 +118,4 @@ export {
|
|
|
118
118
|
BRIDGE_DELIVERY_JOB_TYPE,
|
|
119
119
|
BridgeDeliveryHandler
|
|
120
120
|
};
|
|
121
|
-
//# sourceMappingURL=chunk-
|
|
121
|
+
//# sourceMappingURL=chunk-R6F6KFIL.js.map
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EnvEncryptionKey
|
|
3
3
|
} from "./chunk-IP4OO26U.js";
|
|
4
|
+
import {
|
|
5
|
+
AuthController
|
|
6
|
+
} from "./chunk-SZVPIHWE.js";
|
|
4
7
|
import {
|
|
5
8
|
DrizzleOAuthStateStore
|
|
6
9
|
} from "./chunk-N5OTOWTP.js";
|
|
7
10
|
import {
|
|
8
11
|
MemoryOAuthStateStore
|
|
9
12
|
} from "./chunk-QLTJSCE6.js";
|
|
10
|
-
import {
|
|
11
|
-
AuthController
|
|
12
|
-
} from "./chunk-SZVPIHWE.js";
|
|
13
13
|
import {
|
|
14
14
|
AUTH_OPTIONS,
|
|
15
15
|
ENCRYPTION_KEY,
|
|
@@ -89,4 +89,4 @@ AuthModule = __decorateClass([
|
|
|
89
89
|
export {
|
|
90
90
|
AuthModule
|
|
91
91
|
};
|
|
92
|
-
//# sourceMappingURL=chunk-
|
|
92
|
+
//# sourceMappingURL=chunk-T6SCOJF4.js.map
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
assertTenantId
|
|
3
|
-
} from "./chunk-MZ6GV4YF.js";
|
|
4
1
|
import {
|
|
5
2
|
INTEGRATION_CHANGE_SOURCE,
|
|
6
3
|
INTEGRATION_CURSOR_STORE,
|
|
@@ -9,6 +6,9 @@ import {
|
|
|
9
6
|
INTEGRATION_RUN_RECORDER,
|
|
10
7
|
INTEGRATION_SINK
|
|
11
8
|
} from "./chunk-S7C6TIIF.js";
|
|
9
|
+
import {
|
|
10
|
+
assertTenantId
|
|
11
|
+
} from "./chunk-MZ6GV4YF.js";
|
|
12
12
|
import {
|
|
13
13
|
__decorateClass,
|
|
14
14
|
__decorateParam
|
|
@@ -219,4 +219,4 @@ ExecuteIntegrationUseCase = __decorateClass([
|
|
|
219
219
|
export {
|
|
220
220
|
ExecuteIntegrationUseCase
|
|
221
221
|
};
|
|
222
|
-
//# sourceMappingURL=chunk-
|
|
222
|
+
//# sourceMappingURL=chunk-TDEHU73T.js.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JOBS_WAKE_CHANNEL,
|
|
3
|
+
PgNotifyListener
|
|
4
|
+
} from "./chunk-Q6LRJ4VI.js";
|
|
1
5
|
import {
|
|
2
6
|
JOB_HANDLER_REGISTRY
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7P5ODGLA.js";
|
|
4
8
|
import {
|
|
5
9
|
JOB_ORCHESTRATOR,
|
|
6
10
|
JOB_RUN_SERVICE,
|
|
@@ -9,10 +13,6 @@ import {
|
|
|
9
13
|
import {
|
|
10
14
|
jobRuns
|
|
11
15
|
} from "./chunk-OKXZ63IA.js";
|
|
12
|
-
import {
|
|
13
|
-
JOBS_WAKE_CHANNEL,
|
|
14
|
-
PgNotifyListener
|
|
15
|
-
} from "./chunk-MYQIQ27N.js";
|
|
16
16
|
import {
|
|
17
17
|
tokenKey
|
|
18
18
|
} from "./chunk-GYGNEQSC.js";
|
|
@@ -199,6 +199,7 @@ var JobWorker = class {
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
async onModuleDestroy() {
|
|
202
|
+
await this.stopNotifyListener();
|
|
202
203
|
if (this.shuttingDown) {
|
|
203
204
|
await this.drainInFlight();
|
|
204
205
|
return;
|
|
@@ -213,14 +214,6 @@ var JobWorker = class {
|
|
|
213
214
|
this.sweeperTimer = null;
|
|
214
215
|
}
|
|
215
216
|
process.removeListener("SIGTERM", this.sigtermHandler);
|
|
216
|
-
if (this.notifyListener) {
|
|
217
|
-
try {
|
|
218
|
-
await this.notifyListener.stop();
|
|
219
|
-
} catch (err) {
|
|
220
|
-
this.logger.error(`notify listener stop failed: ${err.message}`);
|
|
221
|
-
}
|
|
222
|
-
this.notifyListener = null;
|
|
223
|
-
}
|
|
224
217
|
await this.drainInFlight();
|
|
225
218
|
try {
|
|
226
219
|
await this.db.update(jobRuns).set({ status: "pending", claimedAt: null, startedAt: null }).where(
|
|
@@ -240,6 +233,23 @@ var JobWorker = class {
|
|
|
240
233
|
timeout
|
|
241
234
|
]);
|
|
242
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* LISTEN-NOTIFY-2 — stop + drop the wake listener. Idempotent: a second call
|
|
238
|
+
* (SIGTERM + Nest destroy) finds `notifyListener` already null and no-ops.
|
|
239
|
+
* `PgNotifyListener.stop()` is itself race-safe against an in-flight
|
|
240
|
+
* `connect()`, so even a destroy that arrives microseconds after `start()`
|
|
241
|
+
* releases the listener socket rather than leaking it.
|
|
242
|
+
*/
|
|
243
|
+
async stopNotifyListener() {
|
|
244
|
+
const listener = this.notifyListener;
|
|
245
|
+
if (!listener) return;
|
|
246
|
+
this.notifyListener = null;
|
|
247
|
+
try {
|
|
248
|
+
await listener.stop();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
this.logger.error(`notify listener stop failed: ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
243
253
|
// ============================================================================
|
|
244
254
|
// Poll loop
|
|
245
255
|
// ============================================================================
|
|
@@ -518,4 +528,4 @@ export {
|
|
|
518
528
|
buildStaleSweepQuery,
|
|
519
529
|
JobWorker
|
|
520
530
|
};
|
|
521
|
-
//# sourceMappingURL=chunk-
|
|
531
|
+
//# sourceMappingURL=chunk-VDL5CJ5C.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../runtime/subsystems/jobs/job-worker.ts"],"sourcesContent":["/**\n * JobWorker — backend-agnostic tick loop for the job orchestration domain\n * (ADR-022, JOB-3).\n *\n * One worker instance per active pool. On `onModuleInit` it starts two\n * intervals: the poll loop (claim → process → repeat) and the stale-claim\n * sweeper. On `onModuleDestroy` / SIGTERM it drains in-flight work and\n * releases still-`running` rows back to `pending` so a replacement worker\n * can resume with step memoization intact.\n *\n * The claim query is the beating heart: `SELECT … FOR UPDATE SKIP LOCKED`\n * inside a single transaction. Multiple worker processes share the table\n * without serialising on row locks.\n */\n// TODO(logging-subsystem): swap to ILogger once ADR-028 lands\nimport { Inject, Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from '@nestjs/common';\nimport { ModuleRef } from '@nestjs/core';\nimport { and, asc, desc, eq, inArray, lt, lte, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { tokenKey } from '../token-key';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, type JobRunRow } from './job-orchestration.schema';\nimport type { IJobOrchestrator, JobRun } from './job-orchestrator.protocol';\nimport type { IJobRunService } from './job-run-service.protocol';\nimport type { IJobStepService } from './job-step-service.protocol';\nimport {\n JOB_ORCHESTRATOR,\n JOB_RUN_SERVICE,\n JOB_STEP_SERVICE,\n} from './jobs-domain.tokens';\nimport {\n JOB_HANDLER_REGISTRY,\n JobHandlerBase,\n type JobContext,\n type JobHandlerMeta,\n type RetryPolicy,\n type SpawnChildOptions,\n type StepOptions,\n} from './job-handler.base';\nimport { JOBS_WAKE_CHANNEL, PgNotifyListener } from './pg-notify';\n\n/**\n * Options accepted by `JobWorker`. JOB-5 threads these through module\n * `.forRoot()` config; supplied here as a plain DI-constructor argument\n * so the worker compiles standalone.\n */\nexport interface JobWorkerOptions {\n /** Pool name this worker claims from. Matches `job.pool`. */\n pool: string;\n /** Max concurrent in-flight `processRun` calls. */\n concurrency: number;\n /** Poll interval in ms. Default 1000. */\n pollIntervalMs?: number;\n /** Stale sweep interval in ms. Default 60_000. */\n staleSweeperIntervalMs?: number;\n /**\n * Threshold beyond which a `running` row is presumed stranded by a\n * crashed worker. Default 5 min. Must be >= 2× max handler duration.\n */\n staleThresholdMs?: number;\n /** Max ms to wait for in-flight drain on SIGTERM. Default 30_000. */\n shutdownTimeoutMs?: number;\n /**\n * LISTEN-NOTIFY-1 — when true, hold a dedicated listener connection and\n * LISTEN on `codegen_jobs_wake`. A notification naming this worker's `pool`\n * triggers an immediate (debounced) claim cycle, so an enqueue is claimed in\n * milliseconds instead of waiting for the next `pollIntervalMs` tick. Polling\n * continues unchanged as the fallback heartbeat. Default false.\n */\n listenNotify?: boolean;\n}\n\n// ADR-037: namespaced `Symbol.for(...)` (via `tokenKey()`) — matches by value\n// across runtime copies.\nexport const JOB_WORKER_OPTIONS = Symbol.for(tokenKey('jobs', 'worker-options'));\n\nconst DEFAULT_POLL_INTERVAL_MS = 1_000;\nconst DEFAULT_STALE_SWEEPER_INTERVAL_MS = 60_000;\nconst DEFAULT_STALE_THRESHOLD_MS = 5 * 60_000;\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;\n\nconst TERMINAL_STATUSES: JobRunRow['status'][] = [\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n];\n\n// ─── Pure helpers (exported for unit tests) ────────────────────────────────\n\n/**\n * Backoff delay in ms for the Nth attempt (1-indexed). Supports both\n * policy modes. Exponential is capped at `Number.MAX_SAFE_INTEGER` so\n * pathological attempt counts don't overflow.\n */\nexport function computeBackoff(policy: RetryPolicy, attempts: number): number {\n const base = Math.max(policy.baseMs, 0);\n if (policy.backoff === 'fixed') {\n return base;\n }\n // exponential: baseMs * 2^(attempts-1)\n const exponent = Math.max(attempts - 1, 0);\n if (exponent >= 53) return Number.MAX_SAFE_INTEGER; // 2^53 overflow guard\n const raw = base * Math.pow(2, exponent);\n if (!Number.isFinite(raw) || raw >= Number.MAX_SAFE_INTEGER) {\n return Number.MAX_SAFE_INTEGER;\n }\n return raw;\n}\n\n/**\n * Decide whether an error should be retried under the given policy.\n * Matches `nonRetryableErrors` by `.name` OR `.code`. Returns\n * - `'retry'` if attempts remain and the error isn't blacklisted,\n * - `'fail'` otherwise (terminal failure).\n */\nexport function classifyError(\n err: unknown,\n policy: RetryPolicy | undefined,\n currentAttempts: number,\n): 'retry' | 'fail' {\n if (!policy) return 'fail';\n const errObj = err as { name?: string; code?: string } | undefined;\n const name = errObj?.name;\n const code = errObj?.code;\n const nonRetryable = policy.nonRetryableErrors ?? [];\n if (nonRetryable.some((n) => n === name || n === code)) return 'fail';\n if (currentAttempts + 1 >= policy.attempts) return 'fail';\n return 'retry';\n}\n\n/**\n * Build the raw claim-candidate select. Exported so tests can inspect\n * `.toSQL()` without spinning up the full worker. Matches JOB-3 §4 and\n * ADR-022 \"Claim query (Drizzle backend)\".\n */\nexport function buildClaimQuery(db: DrizzleClient, pool: string) {\n return db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.status, 'pending'),\n eq(jobRuns.pool, pool),\n lte(jobRuns.runAt, new Date()),\n ),\n )\n .orderBy(desc(jobRuns.priority), asc(jobRuns.runAt))\n .limit(1)\n .for('update', { skipLocked: true });\n}\n\n/**\n * Build the stale-claim sweep candidate select. `FOR UPDATE SKIP LOCKED`\n * per OQ-2 resolution (2026-04-19): per-worker sweeper, safe without\n * leader election because the update is self-gating.\n */\nexport function buildStaleSweepQuery(\n db: DrizzleClient,\n staleThresholdMs: number,\n) {\n const threshold = new Date(Date.now() - staleThresholdMs);\n return db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.status, 'running'),\n lt(jobRuns.claimedAt, threshold),\n ),\n )\n .for('update', { skipLocked: true });\n}\n\n// ─── Error serialisation ───────────────────────────────────────────────────\n\nfunction serialiseError(err: unknown, attempt: number, retryable: boolean) {\n const e = err as { message?: string; stack?: string; code?: string } | undefined;\n return {\n message: (e?.message ?? String(err)) as string,\n stack: e?.stack,\n retryable,\n attempt,\n };\n}\n\n// ─── JobWorker ─────────────────────────────────────────────────────────────\n\n@Injectable()\nexport class JobWorker implements OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger(JobWorker.name);\n private shuttingDown = false;\n private readonly inFlight = new Set<Promise<void>>();\n private pollTimer: ReturnType<typeof setInterval> | null = null;\n private sweeperTimer: ReturnType<typeof setInterval> | null = null;\n private sigtermHandled = false;\n private readonly sigtermHandler: () => void;\n\n private readonly pollIntervalMs: number;\n private readonly staleSweeperIntervalMs: number;\n private readonly staleThresholdMs: number;\n private readonly shutdownTimeoutMs: number;\n\n // LISTEN-NOTIFY-1 — dedicated listener + debounce state. `null` when\n // `listenNotify` is off (the common case); polling is the only driver then.\n private readonly listenNotifyEnabled: boolean;\n private notifyListener: PgNotifyListener | null = null;\n /** True while a wake-driven claim cycle is in flight (debounce gate). */\n private wakeDraining = false;\n /** A notify arrived mid-cycle → re-check once when the cycle ends. */\n private wakeRecheckPending = false;\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,\n @Inject(JOB_RUN_SERVICE) private readonly runService: IJobRunService,\n @Inject(JOB_STEP_SERVICE) private readonly stepService: IJobStepService,\n @Inject(JOB_WORKER_OPTIONS) private readonly options: JobWorkerOptions,\n private readonly moduleRef: ModuleRef,\n ) {\n this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n this.staleSweeperIntervalMs =\n options.staleSweeperIntervalMs ?? DEFAULT_STALE_SWEEPER_INTERVAL_MS;\n this.staleThresholdMs = options.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;\n this.shutdownTimeoutMs =\n options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;\n this.listenNotifyEnabled = options.listenNotify ?? false;\n\n this.sigtermHandler = () => {\n if (this.sigtermHandled) return;\n this.sigtermHandled = true;\n void this.onModuleDestroy();\n };\n void this.runService; // reserved for future scope-aware cancellation paths\n }\n\n // ============================================================================\n // Lifecycle\n // ============================================================================\n\n onModuleInit(): void {\n this.pollTimer = setInterval(() => {\n void this.pollAndProcess();\n }, this.pollIntervalMs);\n this.sweeperTimer = setInterval(() => {\n void this.sweepStaleClaims();\n }, this.staleSweeperIntervalMs);\n process.on('SIGTERM', this.sigtermHandler);\n\n // LISTEN-NOTIFY-1 — start the wake listener ALONGSIDE the poll timer (never\n // instead). A notify for this worker's pool drives an immediate claim cycle;\n // the interval timer above stays the durability heartbeat. Listener startup\n // is fire-and-forget: a connect failure self-heals via the listener's own\n // backoff, and until it's up the poll loop is the sole driver.\n if (this.listenNotifyEnabled) {\n // The DRIZZLE provider wraps a `pg.Pool`, exposed by drizzle as `$client`.\n const pool = (this.db as unknown as { $client?: unknown }).$client;\n if (!pool || typeof (pool as { connect?: unknown }).connect !== 'function') {\n this.logger.warn(\n `listen_notify enabled but the Drizzle client exposes no pg Pool ` +\n `($client.connect missing) — falling back to interval polling only.`,\n );\n } else {\n this.notifyListener = new PgNotifyListener({\n channel: JOBS_WAKE_CHANNEL,\n pool: pool as { connect(): Promise<never> },\n label: `jobs:${this.options.pool}`,\n onNotify: (payload) => this.onWake(payload),\n });\n void this.notifyListener.start();\n }\n }\n }\n\n /**\n * Wake handler — a `codegen_jobs_wake` notification arrived. Only payloads\n * naming THIS worker's pool are relevant (other pools have their own workers).\n * Debounced: if a claim cycle is already running we just flag a re-check so a\n * burst of N enqueues collapses to at most one extra cycle (D3).\n */\n private onWake(payload: string): void {\n if (this.shuttingDown) return;\n if (payload !== this.options.pool) return;\n if (this.wakeDraining) {\n this.wakeRecheckPending = true;\n return;\n }\n void this.drainOnWake();\n }\n\n /**\n * Claim-until-empty on a wake. Unlike the interval `pollAndProcess` (one\n * claim per tick), a wake drains greedily up to the concurrency ceiling so a\n * burst that arrived together is dispatched without waiting for N ticks. The\n * `wakeRecheckPending` flag coalesces notifies that land mid-drain.\n */\n private async drainOnWake(): Promise<void> {\n this.wakeDraining = true;\n try {\n do {\n this.wakeRecheckPending = false;\n // Claim while there's capacity; pollAndProcess no-ops at the ceiling.\n let progressed = true;\n while (\n progressed &&\n !this.shuttingDown &&\n this.inFlight.size < this.options.concurrency\n ) {\n const before = this.inFlight.size;\n await this.pollAndProcess();\n progressed = this.inFlight.size > before;\n }\n } while (this.wakeRecheckPending && !this.shuttingDown);\n } finally {\n this.wakeDraining = false;\n }\n }\n\n async onModuleDestroy(): Promise<void> {\n // LISTEN-NOTIFY-2 — release the listener connection on EVERY destroy path,\n // including the `shuttingDown` early-return below (reached when SIGTERM and\n // Nest's onModuleDestroy both fire) and a destroy with no prior SIGTERM.\n // Previously this lived only on the first (non-shuttingDown) branch, so a\n // double-fire — or a stop() that raced the listener's own in-flight\n // connect() — left a `LISTEN codegen_jobs_wake` socket open past\n // `app.close()`, hanging the process. `PgNotifyListener.stop()` is itself\n // idempotent + connect-race-safe (LISTEN-NOTIFY-2). Best-effort; a failure\n // here doesn't block the drain.\n await this.stopNotifyListener();\n\n if (this.shuttingDown) {\n // Still drain, but don't tear intervals down twice.\n await this.drainInFlight();\n return;\n }\n this.shuttingDown = true;\n if (this.pollTimer) {\n clearInterval(this.pollTimer);\n this.pollTimer = null;\n }\n if (this.sweeperTimer) {\n clearInterval(this.sweeperTimer);\n this.sweeperTimer = null;\n }\n process.removeListener('SIGTERM', this.sigtermHandler);\n\n await this.drainInFlight();\n\n // Any rows still `running` past timeout → release back to pending.\n try {\n await this.db\n .update(jobRuns)\n .set({ status: 'pending', claimedAt: null, startedAt: null })\n .where(\n and(eq(jobRuns.status, 'running'), eq(jobRuns.pool, this.options.pool)),\n );\n } catch (err) {\n this.logger.error(`shutdown reset failed: ${(err as Error).message}`);\n }\n }\n\n private async drainInFlight(): Promise<void> {\n if (this.inFlight.size === 0) return;\n const timeout = new Promise<void>((resolve) =>\n setTimeout(resolve, this.shutdownTimeoutMs),\n );\n await Promise.race([\n Promise.allSettled([...this.inFlight]).then(() => undefined),\n timeout,\n ]);\n }\n\n /**\n * LISTEN-NOTIFY-2 — stop + drop the wake listener. Idempotent: a second call\n * (SIGTERM + Nest destroy) finds `notifyListener` already null and no-ops.\n * `PgNotifyListener.stop()` is itself race-safe against an in-flight\n * `connect()`, so even a destroy that arrives microseconds after `start()`\n * releases the listener socket rather than leaking it.\n */\n private async stopNotifyListener(): Promise<void> {\n const listener = this.notifyListener;\n if (!listener) return;\n this.notifyListener = null;\n try {\n await listener.stop();\n } catch (err) {\n this.logger.error(`notify listener stop failed: ${(err as Error).message}`);\n }\n }\n\n // ============================================================================\n // Poll loop\n // ============================================================================\n\n async pollAndProcess(): Promise<void> {\n if (this.shuttingDown) return;\n if (this.inFlight.size >= this.options.concurrency) return;\n\n let claimed: JobRunRow | null;\n try {\n claimed = await this.claimNext(this.options.pool);\n } catch (err) {\n this.logger.error(`claimNext failed: ${(err as Error).message}`);\n return;\n }\n if (!claimed) return;\n\n const run = claimed;\n const promise = this.processRun(run).catch((err) => {\n this.logger.error(\n `processRun(${run.id}) unhandled: ${(err as Error).message}`,\n );\n });\n this.inFlight.add(promise);\n promise.finally(() => {\n this.inFlight.delete(promise);\n });\n }\n\n /**\n * Claim the next runnable row from the pool. Transaction ensures the\n * select-candidate + update-to-running pair is atomic; FOR UPDATE SKIP\n * LOCKED lets multiple workers share the table without serialising.\n */\n async claimNext(pool: string): Promise<JobRunRow | null> {\n return this.db.transaction(async (tx) => {\n const candidates = await tx\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.status, 'pending'),\n eq(jobRuns.pool, pool),\n lte(jobRuns.runAt, new Date()),\n ),\n )\n .orderBy(desc(jobRuns.priority), asc(jobRuns.runAt))\n .limit(1)\n .for('update', { skipLocked: true });\n const candidate = candidates[0];\n if (!candidate) return null;\n\n const [claimed] = await tx\n .update(jobRuns)\n .set({\n status: 'running',\n claimedAt: new Date(),\n startedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, candidate.id))\n .returning();\n return (claimed ?? null) as JobRunRow | null;\n });\n }\n\n // ============================================================================\n // Stale claim sweeper\n // ============================================================================\n\n /**\n * Release rows whose `claimed_at` is older than the threshold. Safe to\n * run concurrently across workers — the two-phase tx (select-for-update\n * then update) guarantees each stranded row is only reset once.\n */\n async sweepStaleClaims(): Promise<void> {\n if (this.shuttingDown) return;\n try {\n await this.db.transaction(async (tx) => {\n const threshold = new Date(Date.now() - this.staleThresholdMs);\n const stale = await tx\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(eq(jobRuns.status, 'running'), lt(jobRuns.claimedAt, threshold)),\n )\n .for('update', { skipLocked: true });\n if (stale.length === 0) return;\n const ids = stale.map((r) => r.id);\n await tx\n .update(jobRuns)\n .set({ status: 'pending', claimedAt: null, startedAt: null })\n .where(inArray(jobRuns.id, ids));\n for (const id of ids) {\n this.logger.warn(`Recovered stale claim on run ${id}`);\n }\n });\n } catch (err) {\n this.logger.error(`sweepStaleClaims failed: ${(err as Error).message}`);\n }\n }\n\n // ============================================================================\n // processRun\n // ============================================================================\n\n private async processRun(claimed: JobRunRow): Promise<void> {\n const registryEntry = JOB_HANDLER_REGISTRY.get(claimed.jobType);\n\n // (a) Missing handler — defensive; JOB-5 boot validator should have caught.\n if (!registryEntry) {\n this.logger.error(\n `No handler registered for jobType='${claimed.jobType}' (run ${claimed.id})`,\n );\n await this.markFailed(\n claimed,\n new Error(`No handler registered for jobType='${claimed.jobType}'`),\n /*finalAttempts*/ (claimed.attempts ?? 0) + 1,\n );\n return;\n }\n\n // (b) Concurrency-queue release gate — defer if another run with the\n // same key is already `running`.\n if (claimed.concurrencyKey) {\n const inflight = await this.db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.concurrencyKey, claimed.concurrencyKey),\n eq(jobRuns.status, 'running'),\n ),\n );\n const other = inflight.find((r) => r.id !== claimed.id);\n if (other) {\n await this.db\n .update(jobRuns)\n .set({\n status: 'pending',\n claimedAt: null,\n startedAt: null,\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, claimed.id));\n return;\n }\n }\n\n const meta = registryEntry.meta as JobHandlerMeta<unknown>;\n const HandlerClass = registryEntry.handlerClass;\n\n // (c) Build JobContext. Resolve the handler instance from Nest's DI\n // graph so its `@Inject` constructor params (which may come from\n // any module in the app graph) are satisfied. `moduleRef.create()`\n // would otherwise instantiate a fresh class within JobWorkerModule's\n // scope only — which blows up with \"not a provider of the current\n // module\" for any handler that consumes a service from a peer\n // module (e.g. CrmSyncJob injecting CrmSyncFactory from CrmModule).\n // Consequence: handlers MUST be registered as providers in their\n // owning module (@Injectable + `providers: [HandlerClass]`). The\n // @JobHandler decorator handles registry registration only, not DI.\n // See the jobs skill's handler-authoring.md for the registration\n // rule.\n const handler = this.moduleRef.get(\n HandlerClass as unknown as new (...args: unknown[]) => unknown,\n { strict: false },\n ) as JobHandlerBase<unknown>;\n const ctx: JobContext<unknown> = {\n input: claimed.input,\n run: claimed as JobRun,\n step: this.makeStepFn(claimed),\n spawnChild: this.makeSpawnFn(claimed),\n logger: new Logger(`JobRun:${claimed.id}`),\n };\n\n const attemptsBefore = claimed.attempts ?? 0;\n try {\n // (d) Run the handler.\n const output = (await handler.run(ctx)) as Record<string, unknown> | undefined;\n // (e) Success.\n await this.db\n .update(jobRuns)\n .set({\n status: 'completed',\n output: (output ?? {}) as Record<string, unknown>,\n finishedAt: new Date(),\n updatedAt: new Date(),\n attempts: attemptsBefore + 1,\n })\n .where(eq(jobRuns.id, claimed.id));\n } catch (err) {\n // (f) Error classification + retry/fail.\n const policy = meta.retry;\n const decision = classifyError(err, policy, attemptsBefore);\n const nextAttempts = attemptsBefore + 1;\n if (decision === 'retry' && policy) {\n const delay = computeBackoff(policy, nextAttempts);\n await this.db\n .update(jobRuns)\n .set({\n status: 'pending',\n attempts: nextAttempts,\n runAt: new Date(Date.now() + delay),\n startedAt: null,\n claimedAt: null,\n error: serialiseError(err, nextAttempts, true),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, claimed.id));\n } else {\n await this.markFailed(claimed, err, nextAttempts);\n }\n }\n }\n\n private async markFailed(\n claimed: JobRunRow,\n err: unknown,\n finalAttempts: number,\n ): Promise<void> {\n await this.db\n .update(jobRuns)\n .set({\n status: 'failed',\n attempts: finalAttempts,\n finishedAt: new Date(),\n error: serialiseError(err, finalAttempts, false),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, claimed.id));\n\n // Parent-close-policy cascade: if this run has children under the same\n // root_run_id and this run's own parentClosePolicy is 'terminate', cascade.\n if (claimed.parentClosePolicy === 'terminate') {\n try {\n // JOB-8 — thread the run's own tenantId so the orchestrator's\n // multi-tenant gate passes. Without this, every terminate-policy\n // cascade throws MissingTenantIdError under multiTenant=true and\n // the outer catch silently swallows it — children never cancel.\n await this.orchestrator.cancel(claimed.id, {\n cascade: true,\n reason: 'parent-failed',\n tenantId: claimed.tenantId,\n });\n } catch (cascadeErr) {\n // cancel is idempotent; failure here is unusual but not fatal.\n this.logger.warn(\n `cascade on failed run ${claimed.id}: ${(cascadeErr as Error).message}`,\n );\n }\n }\n }\n\n // ============================================================================\n // ctx.step / ctx.spawnChild builders\n // ============================================================================\n\n private makeStepFn(run: JobRunRow) {\n return async <TOutput>(\n stepId: string,\n fn: () => Promise<TOutput>,\n _opts?: StepOptions,\n ): Promise<TOutput> => {\n void _opts;\n const existing = await this.stepService.findStep(run.id, stepId);\n if (existing?.status === 'completed') {\n return existing.output as TOutput;\n }\n\n const seq = await this.nextStepSeq(run.id);\n const startedAt = new Date();\n const nextAttempts = (existing?.attempts ?? 0) + 1;\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'running',\n startedAt,\n attempts: nextAttempts,\n });\n try {\n const output = await fn();\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'completed',\n output: output as Record<string, unknown> | undefined,\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n return output;\n } catch (err) {\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'failed',\n error: serialiseError(err, nextAttempts, false),\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n throw err;\n }\n };\n }\n\n private makeSpawnFn(run: JobRunRow) {\n return async (\n type: string,\n input: unknown,\n opts?: SpawnChildOptions,\n ): Promise<JobRun> => {\n return this.orchestrator.start(type, input, {\n parentRunId: run.id,\n parentClosePolicy: opts?.closePolicy,\n runAt: opts?.runAt,\n priority: opts?.priority,\n tags: opts?.tags,\n triggerSource: 'parent',\n triggerRef: run.id,\n });\n };\n }\n\n /**\n * Allocate the next `seq` for a given run. SELECT-max approach — runs\n * typically have <100 steps so the scan is cheap, and correctness across\n * retries is more important than the microseconds saved by an in-memory\n * counter (which would drift if the worker crashes mid-run and another\n * worker resumes via stale-claim sweep).\n */\n private async nextStepSeq(runId: string): Promise<number> {\n const result = await this.db.execute(\n sql`SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM job_step WHERE job_run_id = ${runId}`,\n );\n // Driver shape varies and is NOT uniformly array-iterable, so we must\n // never array-destructure the raw result (that throws `{} is not iterable`\n // on the node-postgres `Result` object, which exposes `.rows` instead of\n // being an array — first hit by package-mode bridge deliveries on\n // `drizzle-orm/node-postgres`). Normalise to a row array first, then read.\n // - node-postgres `db.execute(sql)` → `{ rows: [{ next }], ... }`\n // - some drivers / future shapes → a plain `[{ next }]` array\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const raw = result as any;\n const rows: Array<{ next?: unknown }> = Array.isArray(raw)\n ? raw\n : Array.isArray(raw?.rows)\n ? raw.rows\n : [];\n const next = rows[0]?.next;\n return typeof next === 'undefined' ? 1 : Number(next);\n }\n\n // ============================================================================\n // (suppress unused-import noise)\n // ============================================================================\n}\n\n// Terminal statuses re-exported for JOB-4 parity imports.\nexport { TERMINAL_STATUSES };\n"],"mappings":"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","names":[]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MissingTenantIdError
|
|
3
|
+
} from "./chunk-T4BIIU5E.js";
|
|
1
4
|
import {
|
|
2
5
|
clampLimit,
|
|
3
6
|
decodeKeysetCursor,
|
|
4
7
|
encodeKeysetCursor,
|
|
5
8
|
toJobRunSummary
|
|
6
9
|
} from "./chunk-L3LZWWSX.js";
|
|
7
|
-
import {
|
|
8
|
-
MissingTenantIdError
|
|
9
|
-
} from "./chunk-T4BIIU5E.js";
|
|
10
10
|
import {
|
|
11
11
|
JOBS_MULTI_TENANT,
|
|
12
12
|
JOB_ORCHESTRATOR
|
|
@@ -198,4 +198,4 @@ DrizzleJobRunService = __decorateClass([
|
|
|
198
198
|
export {
|
|
199
199
|
DrizzleJobRunService
|
|
200
200
|
};
|
|
201
|
-
//# sourceMappingURL=chunk-
|
|
201
|
+
//# sourceMappingURL=chunk-VNBC3VXM.js.map
|