@pattern-stack/codegen 0.15.3 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/consumer-skills/integration/change-sources-and-sinks.md +1 -1
  3. package/dist/{chunk-GCYKMF22.js → chunk-24WXSC3C.js} +6 -6
  4. package/dist/{chunk-32DOFN3T.js → chunk-2WDX6I7T.js} +2 -2
  5. package/dist/{chunk-WWGYCIJX.js → chunk-43SBT72G.js} +2 -2
  6. package/dist/{chunk-FBGHYQIZ.js → chunk-5LXOJGO2.js} +6 -6
  7. package/dist/{chunk-32BMMV4H.js → chunk-5RT7JGKT.js} +5 -5
  8. package/dist/{chunk-3NMCDN7L.js → chunk-5TK7MEN4.js} +2 -2
  9. package/dist/chunk-5TK7MEN4.js.map +1 -0
  10. package/dist/{chunk-4H3PETLM.js → chunk-AYC2HEAL.js} +12 -9
  11. package/dist/chunk-AYC2HEAL.js.map +1 -0
  12. package/dist/{chunk-27ETSJ2X.js → chunk-COGHTKXY.js} +2 -2
  13. package/dist/{chunk-IYNSRIGR.js → chunk-CRBVI4GE.js} +5 -5
  14. package/dist/{chunk-J7JMVS2B.js → chunk-CZQUOIDY.js} +4 -4
  15. package/dist/{chunk-O37C3YE6.js → chunk-DGYTSCKN.js} +14 -8
  16. package/dist/chunk-DGYTSCKN.js.map +1 -0
  17. package/dist/{chunk-L7BNNRGI.js → chunk-DLG62MQY.js} +26 -6
  18. package/dist/chunk-DLG62MQY.js.map +1 -0
  19. package/dist/{chunk-TNXH7BJS.js → chunk-E45CSC33.js} +2 -2
  20. package/dist/{chunk-4JLJYWJC.js → chunk-H6FO2ZDJ.js} +99 -11
  21. package/dist/chunk-H6FO2ZDJ.js.map +1 -0
  22. package/dist/{chunk-5Y7W3XR6.js → chunk-IT6FRTEW.js} +30 -11
  23. package/dist/chunk-IT6FRTEW.js.map +1 -0
  24. package/dist/{chunk-RC23QROE.js → chunk-JM3T27ZW.js} +78 -4
  25. package/dist/chunk-JM3T27ZW.js.map +1 -0
  26. package/dist/{chunk-Z7PQCAVK.js → chunk-LQ6PYFU6.js} +4 -4
  27. package/dist/chunk-MYQIQ27N.js +118 -0
  28. package/dist/chunk-MYQIQ27N.js.map +1 -0
  29. package/dist/{chunk-YTN6BKWA.js → chunk-NXNVTXKG.js} +5 -5
  30. package/dist/{chunk-RDVTWIYY.js → chunk-QSJ3J4HE.js} +5 -5
  31. package/dist/{chunk-4MVGAMUA.js → chunk-RUSUZZAF.js} +4 -4
  32. package/dist/{chunk-4RFHUZXU.js → chunk-T4YJRD22.js} +4 -4
  33. package/dist/{chunk-7YGORYZD.js → chunk-T6C4LFLC.js} +4 -4
  34. package/dist/{chunk-OGIZXGPY.js → chunk-TDEHU73T.js} +4 -4
  35. package/dist/{chunk-YLPAPPLW.js → chunk-TIZXQU26.js} +36 -9
  36. package/dist/chunk-TIZXQU26.js.map +1 -0
  37. package/dist/{chunk-EOLLMEAH.js → chunk-TKVTEUBD.js} +3 -3
  38. package/dist/chunk-TKVTEUBD.js.map +1 -0
  39. package/dist/{chunk-YPWODKD5.js → chunk-W2UIDI3R.js} +5 -5
  40. package/dist/chunk-W4HOHZVF.js +1 -0
  41. package/dist/{chunk-DCCZB4UC.js → chunk-XWBK3XJK.js} +4 -4
  42. package/dist/{chunk-SR7F3TJY.js → chunk-YK5JEVLX.js} +4 -4
  43. package/dist/{chunk-BIO6F7YI.js → chunk-ZPL74UQN.js} +4 -2
  44. package/dist/{chunk-BIO6F7YI.js.map → chunk-ZPL74UQN.js.map} +1 -1
  45. package/dist/runtime/base-classes/index.js +22 -22
  46. package/dist/runtime/subsystems/analytics/analytics.module.js +2 -2
  47. package/dist/runtime/subsystems/analytics/index.js +4 -4
  48. package/dist/runtime/subsystems/auth/auth.module.js +1 -1
  49. package/dist/runtime/subsystems/auth/index.js +7 -7
  50. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +3 -3
  51. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +1 -1
  52. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.d.ts +2 -1
  53. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +6 -5
  54. package/dist/runtime/subsystems/bridge/bridge.module.js +16 -15
  55. package/dist/runtime/subsystems/bridge/event-flow.service.js +3 -3
  56. package/dist/runtime/subsystems/bridge/index.js +16 -15
  57. package/dist/runtime/subsystems/cache/cache.drizzle-backend.js +2 -2
  58. package/dist/runtime/subsystems/cache/cache.module.js +3 -3
  59. package/dist/runtime/subsystems/cache/index.js +5 -5
  60. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +20 -0
  61. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +4 -3
  62. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +2 -2
  63. package/dist/runtime/subsystems/events/events.module.d.ts +14 -0
  64. package/dist/runtime/subsystems/events/events.module.js +6 -5
  65. package/dist/runtime/subsystems/events/index.js +12 -11
  66. package/dist/runtime/subsystems/index.js +88 -87
  67. package/dist/runtime/subsystems/integration/build-change-source.js +2 -2
  68. package/dist/runtime/subsystems/integration/detection-config.schema.d.ts +23 -15
  69. package/dist/runtime/subsystems/integration/detection-config.schema.js +1 -1
  70. package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
  71. package/dist/runtime/subsystems/integration/index.js +17 -17
  72. package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
  73. package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
  74. package/dist/runtime/subsystems/integration/integration.module.js +4 -4
  75. package/dist/runtime/subsystems/integration/webhook-change-source.d.ts +36 -6
  76. package/dist/runtime/subsystems/integration/webhook-change-source.js +1 -1
  77. package/dist/runtime/subsystems/jobs/index.d.ts +2 -1
  78. package/dist/runtime/subsystems/jobs/index.js +42 -30
  79. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +6 -5
  80. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
  81. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +2 -1
  82. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +4 -3
  83. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +3 -3
  84. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +3 -3
  85. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
  86. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -3
  87. package/dist/runtime/subsystems/jobs/job-worker.d.ts +28 -0
  88. package/dist/runtime/subsystems/jobs/job-worker.js +4 -3
  89. package/dist/runtime/subsystems/jobs/job-worker.module.js +11 -10
  90. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +12 -7
  91. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +9 -8
  92. package/dist/runtime/subsystems/jobs/jobs-domain.tokens.d.ts +13 -1
  93. package/dist/runtime/subsystems/jobs/jobs-domain.tokens.js +3 -1
  94. package/dist/runtime/subsystems/jobs/pg-notify.d.ts +85 -0
  95. package/dist/runtime/subsystems/jobs/pg-notify.js +14 -0
  96. package/dist/runtime/subsystems/jobs/pg-notify.js.map +1 -0
  97. package/dist/runtime/subsystems/observability/index.js +4 -4
  98. package/dist/runtime/subsystems/observability/observability.module.js +4 -4
  99. package/dist/runtime/subsystems/observability/observability.service.js +3 -3
  100. package/dist/runtime/subsystems/storage/index.js +4 -4
  101. package/dist/runtime/subsystems/storage/storage.module.js +2 -2
  102. package/dist/src/cli/index.js +53 -15
  103. package/dist/src/cli/index.js.map +1 -1
  104. package/dist/src/index.d.ts +11 -11
  105. package/dist/src/index.js +9 -9
  106. package/package.json +1 -1
  107. package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +27 -0
  108. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +108 -4
  109. package/runtime/subsystems/events/events.module.ts +14 -0
  110. package/runtime/subsystems/integration/detection-config.schema.ts +64 -54
  111. package/runtime/subsystems/integration/webhook-change-source.ts +187 -133
  112. package/runtime/subsystems/jobs/index.ts +10 -0
  113. package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +29 -2
  114. package/runtime/subsystems/jobs/job-worker.module.ts +11 -0
  115. package/runtime/subsystems/jobs/job-worker.ts +98 -0
  116. package/runtime/subsystems/jobs/jobs-domain.module.ts +22 -7
  117. package/runtime/subsystems/jobs/jobs-domain.tokens.ts +13 -0
  118. package/runtime/subsystems/jobs/pg-notify.ts +216 -0
  119. package/templates/subsystem/events-config/codegen-config-events-block.ejs.t +14 -0
  120. package/templates/subsystem/jobs-config/codegen-config-jobs-block.ejs.t +13 -4
  121. package/dist/chunk-3NMCDN7L.js.map +0 -1
  122. package/dist/chunk-4H3PETLM.js.map +0 -1
  123. package/dist/chunk-4JLJYWJC.js.map +0 -1
  124. package/dist/chunk-5Y7W3XR6.js.map +0 -1
  125. package/dist/chunk-EOLLMEAH.js.map +0 -1
  126. package/dist/chunk-L7BNNRGI.js.map +0 -1
  127. package/dist/chunk-O37C3YE6.js.map +0 -1
  128. package/dist/chunk-RC23QROE.js.map +0 -1
  129. package/dist/chunk-UTN4GBPQ.js +0 -1
  130. package/dist/chunk-YLPAPPLW.js.map +0 -1
  131. /package/dist/{chunk-GCYKMF22.js.map → chunk-24WXSC3C.js.map} +0 -0
  132. /package/dist/{chunk-32DOFN3T.js.map → chunk-2WDX6I7T.js.map} +0 -0
  133. /package/dist/{chunk-WWGYCIJX.js.map → chunk-43SBT72G.js.map} +0 -0
  134. /package/dist/{chunk-FBGHYQIZ.js.map → chunk-5LXOJGO2.js.map} +0 -0
  135. /package/dist/{chunk-32BMMV4H.js.map → chunk-5RT7JGKT.js.map} +0 -0
  136. /package/dist/{chunk-27ETSJ2X.js.map → chunk-COGHTKXY.js.map} +0 -0
  137. /package/dist/{chunk-IYNSRIGR.js.map → chunk-CRBVI4GE.js.map} +0 -0
  138. /package/dist/{chunk-J7JMVS2B.js.map → chunk-CZQUOIDY.js.map} +0 -0
  139. /package/dist/{chunk-TNXH7BJS.js.map → chunk-E45CSC33.js.map} +0 -0
  140. /package/dist/{chunk-Z7PQCAVK.js.map → chunk-LQ6PYFU6.js.map} +0 -0
  141. /package/dist/{chunk-YTN6BKWA.js.map → chunk-NXNVTXKG.js.map} +0 -0
  142. /package/dist/{chunk-RDVTWIYY.js.map → chunk-QSJ3J4HE.js.map} +0 -0
  143. /package/dist/{chunk-4MVGAMUA.js.map → chunk-RUSUZZAF.js.map} +0 -0
  144. /package/dist/{chunk-4RFHUZXU.js.map → chunk-T4YJRD22.js.map} +0 -0
  145. /package/dist/{chunk-7YGORYZD.js.map → chunk-T6C4LFLC.js.map} +0 -0
  146. /package/dist/{chunk-OGIZXGPY.js.map → chunk-TDEHU73T.js.map} +0 -0
  147. /package/dist/{chunk-YPWODKD5.js.map → chunk-W2UIDI3R.js.map} +0 -0
  148. /package/dist/{chunk-UTN4GBPQ.js.map → chunk-W4HOHZVF.js.map} +0 -0
  149. /package/dist/{chunk-DCCZB4UC.js.map → chunk-XWBK3XJK.js.map} +0 -0
  150. /package/dist/{chunk-SR7F3TJY.js.map → chunk-YK5JEVLX.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../runtime/subsystems/bridge/bridge-outbox-drain-hook.ts"],"sourcesContent":["/**\n * BridgeOutboxDrainHook — drains-time bridge fanout writer (BRIDGE-4,\n * ADR-023 Phase 2).\n *\n * Implements `IBridgeOutboxDrainHook`. Called by `DrizzleEventBus`'s\n * modified `processBatch` once per drained event, INSIDE the per-event\n * transaction. For every trigger registered against the event's type in\n * the codegen-emitted `bridgeRegistry`, writes:\n *\n * 1. `bridge_delivery` ledger row — `INSERT … ON CONFLICT (event_id,\n * trigger_id) DO NOTHING RETURNING id`. Empty result ⇒ Case B\n * facade-eager pre-write OR drain-replay collision; skip wrapper\n * insert for that trigger; sibling triggers still fire.\n * 2. `job_run` wrapper row — `type='@framework/bridge_delivery'`,\n * `pool='events_<direction>'`, `input={ deliveryId }`,\n * `trigger_source='event'`, `trigger_ref=event.id`. The wrapper is\n * what the framework `BridgeDeliveryHandler` (BRIDGE-5) eventually\n * claims via the worker that polls the corresponding reserved pool.\n *\n * Null `event.metadata.direction` is tolerated: the hook logs a one-line\n * warning per event and returns zeros without writing rows. The drain's\n * `processed_at` stamp + subscriber dispatch still fire normally.\n * Direction is null only for events published via the legacy\n * `IEventBus.publish(...)` path (`TypedEventBus.publish` always sets it);\n * such events are out of scope for bridge fanout.\n *\n * The wrapper insert generates its own `id` via Drizzle's `defaultRandom`\n * — we don't `RETURNING id` because nobody needs it at drain time;\n * `BridgeDeliveryHandler` later looks up the wrapper via the\n * `bridge_delivery.wrapper_run_id` link if needed. This keeps the drain\n * one-round-trip-per-trigger.\n */\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport { randomUUID } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\n\nimport type { DomainEvent, DrizzleTransaction } from '../events/event-bus.protocol';\nimport { bridgeDelivery } from './bridge-delivery.schema';\nimport { jobRuns } from '../jobs/job-orchestration.schema';\n\nimport { BRIDGE_REGISTRY } from './bridge.tokens';\nimport type {\n BridgeOutboxDrainResult,\n BridgeRegistry,\n BridgeTriggerEntry,\n IBridgeOutboxDrainHook,\n} from './bridge.protocol';\nimport { BRIDGE_DELIVERY_JOB_TYPE } from './bridge-delivery-handler';\nimport type { EventTypeName } from '../events/event-registry';\n\n/** Reserved pools the wrapper rows route into; ADR-022 / ADR-024. */\nconst POOL_BY_DIRECTION: Record<string, string> = {\n inbound: 'events_inbound',\n change: 'events_change',\n outbound: 'events_outbound',\n};\n\n@Injectable()\nexport class BridgeOutboxDrainHook implements IBridgeOutboxDrainHook {\n private readonly logger = new Logger(BridgeOutboxDrainHook.name);\n private warnedNullDirection = false;\n private readonly warnedAuditTypes = new Set<string>();\n\n constructor(\n @Optional()\n @Inject(BRIDGE_REGISTRY)\n private readonly registry: BridgeRegistry = {},\n ) {}\n\n async processEvent(\n event: DomainEvent,\n tx: DrizzleTransaction,\n ): Promise<BridgeOutboxDrainResult> {\n // Audit-tier guard (defense-in-depth — AUDIT-4). Audit events are not\n // bridge-eligible: the codegen-side validator (AUDIT-2) blocks the\n // registry from listing them as triggers. Reaching this branch means\n // registry/runtime drift — an out-of-band `bridge_trigger` insert, or\n // version skew during deploy. Refuse fanout, surface drift via WARN.\n if (event.metadata?.['tier'] === 'audit') {\n this.warnAuditBlockedOnce(event);\n return {\n delivered: 0,\n dedupSkips: 0,\n triggerCount: 0,\n auditBlocked: 1,\n };\n }\n\n const triggers = this.lookupTriggers(event.type);\n if (triggers.length === 0) {\n return {\n delivered: 0,\n dedupSkips: 0,\n triggerCount: 0,\n auditBlocked: 0,\n };\n }\n\n const direction =\n (event.metadata?.['direction'] as string | undefined) ?? null;\n const tenantId =\n (event.metadata?.['tenantId'] as string | null | undefined) ?? null;\n const wrapperPool = direction ? POOL_BY_DIRECTION[direction] : undefined;\n\n if (!wrapperPool) {\n // Null direction (or an unrecognised one — defensive). Bridge\n // fanout requires a routed wrapper pool; without one we can't\n // spawn. Log once per process so misconfiguration surfaces.\n if (!this.warnedNullDirection) {\n this.warnedNullDirection = true;\n this.logger.warn(\n `Skipping bridge fanout for events with null/unknown direction. ` +\n `event.id=${event.id} event.type=${event.type} ` +\n `direction=${String(direction)}. The wrapper pool is derived ` +\n `from direction (events_<direction>); publishers must use ` +\n `TypedEventBus.publish() so direction is stamped on the ` +\n `outbox row.`,\n );\n }\n return {\n delivered: 0,\n dedupSkips: 0,\n triggerCount: triggers.length,\n auditBlocked: 0,\n };\n }\n\n let delivered = 0;\n let dedupSkips = 0;\n const client = tx as unknown as {\n insert: (table: unknown) => {\n values: (v: unknown) => {\n onConflictDoNothing: (opts: unknown) => {\n returning: (cols: unknown) => Promise<{ id: string }[]>;\n };\n } & {\n // wrapper insert path — no ON CONFLICT\n // (typed loosely via the same helper return shape)\n };\n };\n };\n\n for (const trigger of triggers) {\n const deliveryId = randomUUID();\n const wrapperRunId = randomUUID();\n\n // FK ORDER (BRIDGE / 0.15.2): `bridge_delivery.wrapper_run_id` REFERENCES\n // `job_run(id)` is a plain (non-deferrable) FK, so the referenced\n // wrapper `job_run` MUST exist before the delivery row that points at it\n // is inserted — otherwise Postgres rejects the delivery insert\n // immediately. (The codegen unit tests mock `tx`, so they never\n // exercised this ordering against a real FK; package-mode bridge\n // deliveries are the first to do so.) We therefore insert the wrapper\n // run FIRST, then the delivery. Idempotency is unchanged: the delivery\n // keeps its `ON CONFLICT (event_id, trigger_id) DO NOTHING RETURNING`,\n // and when the delivery conflicts (outbox replay or facade-eager Case B)\n // we DELETE the just-inserted orphan wrapper run in the same tx, so a\n // skipped delivery leaves no stray `job_run` for a worker to claim.\n\n // 1. Wrapper job_run insert. We carry the deliveryId into the wrapper\n // input so BridgeDeliveryHandler.run(ctx) can locate the row via\n // repo.findDeliveryById(ctx.input.deliveryId).\n await (tx as unknown as { insert: typeof client.insert })\n .insert(jobRuns)\n .values({\n id: wrapperRunId,\n jobType: BRIDGE_DELIVERY_JOB_TYPE,\n jobVersion: 1,\n rootRunId: wrapperRunId,\n pool: wrapperPool,\n status: 'pending',\n input: { deliveryId },\n triggerSource: 'event',\n triggerRef: event.id,\n tenantId,\n });\n\n // 2. bridge_delivery insert with ON CONFLICT DO NOTHING + RETURNING.\n const inserted = await (tx as unknown as {\n insert: typeof client.insert;\n })\n .insert(bridgeDelivery)\n .values({\n id: deliveryId,\n eventId: event.id,\n triggerId: trigger.triggerId,\n wrapperRunId,\n status: 'pending',\n tenantId,\n })\n .onConflictDoNothing({\n target: [bridgeDelivery.eventId, bridgeDelivery.triggerId],\n })\n .returning({ id: bridgeDelivery.id });\n\n if (inserted.length === 0) {\n // Case B (facade pre-wrote `delivered`) or drain replay — the delivery\n // already exists, so this trigger is a no-op. Remove the orphan wrapper\n // run we speculatively inserted above so no worker claims it. Sibling\n // triggers still fire.\n await (tx as unknown as {\n delete: (table: unknown) => {\n where: (cond: unknown) => Promise<unknown>;\n };\n })\n .delete(jobRuns)\n .where(eq(jobRuns.id, wrapperRunId));\n dedupSkips++;\n continue;\n }\n\n delivered++;\n }\n\n return {\n delivered,\n dedupSkips,\n triggerCount: triggers.length,\n auditBlocked: 0,\n };\n }\n\n private warnAuditBlockedOnce(event: DomainEvent): void {\n if (this.warnedAuditTypes.has(event.type)) return;\n this.warnedAuditTypes.add(event.type);\n this.logger.warn(\n `Bridge guard blocked audit-tier event '${event.type}' (event.id=${event.id}). ` +\n `Registry says this event is not bridge-eligible; a bridge_trigger row exists ` +\n `out-of-band. Investigate registry/runtime drift.`,\n );\n }\n\n private lookupTriggers(\n eventType: string,\n ): BridgeTriggerEntry[] {\n const candidates = this.registry[eventType as EventTypeName];\n return (candidates ?? []) as BridgeTriggerEntry[];\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../runtime/subsystems/jobs/jobs-domain.module.ts"],"sourcesContent":["/**\n * JobsDomainModule — `DynamicModule.forRoot({ backend })` factory wiring\n * the three jobs-domain protocol tokens to a backend implementation\n * (ADR-022, JOB-5).\n *\n * Mirrors `EventsModule.forRoot()` exactly:\n * - `global: true` so consumer entity modules don't have to import this\n * individually — `JOB_ORCHESTRATOR` / `JOB_RUN_SERVICE` /\n * `JOB_STEP_SERVICE` are available project-wide.\n * - One backend at a time (Drizzle for production, Memory for tests).\n *\n * Backend swappability follows the core/extension protocol from CLAUDE.md:\n * the three tokens are the **core contract**; backend-specific tunables\n * live under `extensions.<backend>` so opting into a feature is explicit\n * and the type system reserves the slot.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport { DRIZZLE } from '../../constants/tokens';\nimport {\n JOB_ORCHESTRATOR,\n JOB_RUN_SERVICE,\n JOB_STEP_SERVICE,\n JOBS_MULTI_TENANT,\n} from './jobs-domain.tokens';\nimport { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend';\nimport { DrizzleJobRunService } from './job-run-service.drizzle-backend';\nimport { DrizzleJobStepService } from './job-step-service.drizzle-backend';\n// #6 — `BullMQJobOrchestrator` is lazy-loaded only when `backend: 'bullmq'`\n// is selected. The backend file is filtered out of drizzle/memory installs\n// (see `backendFileFilter`); a non-literal dynamic import below sidesteps\n// consumer-side tsc resolution of an absent file.\nimport { MemoryJobOrchestrator } from './job-orchestrator.memory-backend';\nimport { MemoryJobRunService } from './job-run-service.memory-backend';\nimport { MemoryJobStepService } from './job-step-service.memory-backend';\nimport { MemoryJobStore } from './memory-job-store';\nimport {\n BULLMQ_CONNECTION,\n BULLMQ_RESOLVED_CONFIG,\n resolveBullMqConfig,\n type BullMqExtensionsConfig,\n} from './bullmq.config';\n\n/**\n * Drizzle backend extensions surface. None are wired into the Drizzle\n * orchestrator yet — this is the **typed reservation** for the LISTEN/NOTIFY\n * + tunable poll-interval extensions called out in ADR-022. App code\n * passing these today is parsed but not yet dispatched; when the\n * Drizzle orchestrator grows the consumer hooks, opt-in code paths will\n * read directly from these fields.\n */\nexport interface DrizzleBackendExtensions {\n /** Use Postgres LISTEN/NOTIFY to wake the polling loop. Default false. */\n listenNotify?: boolean;\n /** Polling interval when LISTEN/NOTIFY is off (ms). Default 1000. */\n pollIntervalMs?: number;\n}\n\nexport interface JobsDomainModuleOptions {\n backend: 'drizzle' | 'memory' | 'bullmq';\n /**\n * Backend-specific extensions. Only the matching backend's extensions\n * are read at boot; non-matching keys are ignored. This is the\n * core/extension protocol surface — see CLAUDE.md.\n */\n extensions?: {\n drizzle?: DrizzleBackendExtensions;\n /**\n * BullMQ backend extensions (BULLMQ-1). Snake_case mirrors the YAML\n * under `jobs.extensions.bullmq`. `redis_url` falls back to\n * `process.env.REDIS_URL` then `redis://localhost:6379`.\n */\n bullmq?: BullMqExtensionsConfig;\n };\n /** Multi-tenancy opt-in. Wired by JOB-8; module signature stays stable. */\n multiTenant?: boolean;\n}\n\n@Module({})\nexport class JobsDomainModule {\n static forRoot(opts: JobsDomainModuleOptions): DynamicModule {\n const multiTenant = opts.multiTenant ?? false;\n\n const providers: Provider[] = [\n // JOB-8 — boolean provider consumed by the four service-layer backends.\n // Always provided (even when `multiTenant === false`) so `@Inject`\n // always resolves; backends short-circuit the enforcement path when\n // the value is `false`. See `jobs-domain.tokens.ts` for the claim-loop\n // cross-tenant-by-design decision.\n { provide: JOBS_MULTI_TENANT, useValue: multiTenant },\n ];\n\n if (opts.backend === 'memory') {\n // The store is a plain class — wired as a singleton `useValue` so\n // unit tests can pull it out via `.get(MemoryJobStore)` for direct\n // assertions (matches the access pattern in JOB-4 specs).\n const store = new MemoryJobStore();\n providers.push({ provide: MemoryJobStore, useValue: store });\n providers.push(MemoryJobStepService);\n providers.push({ provide: JOB_STEP_SERVICE, useExisting: MemoryJobStepService });\n providers.push(MemoryJobOrchestrator);\n providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });\n providers.push(MemoryJobRunService);\n providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });\n } else if (opts.backend === 'bullmq') {\n // BULLMQ-1 — BullMQ orchestrator over a Postgres source of truth. The\n // run/step services stay Drizzle (domain reads + `listForScope` are\n // Postgres queries, unchanged per spec). Only the orchestrator's\n // claim/dispatch half swaps to BullMQ.\n //\n // #6 — the bullmq backend module is filtered out of drizzle/memory\n // installs (no `bullmq` peer dep, no consumer-side tsc compile of an\n // unused file). The factory below dynamic-imports it via a non-literal\n // specifier so TS treats the module type as `any` and never tries to\n // resolve the absent file on a drizzle/memory consumer.\n const resolved = resolveBullMqConfig(opts.extensions?.bullmq);\n providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });\n providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });\n providers.push({\n provide: JOB_ORCHESTRATOR,\n useFactory: async (...args: unknown[]): Promise<object> => {\n const specifier = './job-orchestrator.bullmq-backend';\n const mod = (await import(specifier)) as {\n BullMQJobOrchestrator: new (...args: unknown[]) => object;\n };\n return new mod.BullMQJobOrchestrator(...args);\n },\n // The bullmq orchestrator constructor mirrors DrizzleJobOrchestrator's\n // injection list: DRIZZLE + JOBS_MULTI_TENANT + the resolved BullMQ\n // tokens. Importing token references would force a static dep on the\n // tokens file in this module's import graph; using the existing\n // symbols already in scope is sufficient.\n inject: [DRIZZLE, JOBS_MULTI_TENANT, BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG],\n });\n providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });\n providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });\n } else {\n providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });\n providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });\n providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });\n }\n\n const exports = [\n JOB_ORCHESTRATOR,\n JOB_RUN_SERVICE,\n JOB_STEP_SERVICE,\n JOBS_MULTI_TENANT,\n ];\n // BULLMQ-1 — only export the BullMQ tokens when they were actually\n // provided. Nest throws \"exported but not provided\" otherwise. Exported so\n // JobWorkerModule (which imports this module) can read the resolved\n // connection/config to spawn BullMQ workers.\n if (opts.backend === 'bullmq') {\n exports.push(BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG);\n }\n\n return {\n module: JobsDomainModule,\n global: true,\n providers,\n exports,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBA,SAAS,cAAiD;AA8DnD,IAAM,mBAAN,MAAuB;AAAA,EAC5B,OAAO,QAAQ,MAA8C;AAC3D,UAAM,cAAc,KAAK,eAAe;AAExC,UAAM,YAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAM5B,EAAE,SAAS,mBAAmB,UAAU,YAAY;AAAA,IACtD;AAEA,QAAI,KAAK,YAAY,UAAU;AAI7B,YAAM,QAAQ,IAAI,eAAe;AACjC,gBAAU,KAAK,EAAE,SAAS,gBAAgB,UAAU,MAAM,CAAC;AAC3D,gBAAU,KAAK,oBAAoB;AACnC,gBAAU,KAAK,EAAE,SAAS,kBAAkB,aAAa,qBAAqB,CAAC;AAC/E,gBAAU,KAAK,qBAAqB;AACpC,gBAAU,KAAK,EAAE,SAAS,kBAAkB,aAAa,sBAAsB,CAAC;AAChF,gBAAU,KAAK,mBAAmB;AAClC,gBAAU,KAAK,EAAE,SAAS,iBAAiB,aAAa,oBAAoB,CAAC;AAAA,IAC/E,WAAW,KAAK,YAAY,UAAU;AAWpC,YAAM,WAAW,oBAAoB,KAAK,YAAY,MAAM;AAC5D,gBAAU,KAAK,EAAE,SAAS,mBAAmB,UAAU,SAAS,WAAW,CAAC;AAC5E,gBAAU,KAAK,EAAE,SAAS,wBAAwB,UAAU,SAAS,CAAC;AACtE,gBAAU,KAAK;AAAA,QACb,SAAS;AAAA,QACT,YAAY,UAAU,SAAqC;AACzD,gBAAM,YAAY;AAClB,gBAAM,MAAO,MAAM,OAAO;AAG1B,iBAAO,IAAI,IAAI,sBAAsB,GAAG,IAAI;AAAA,QAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMA,QAAQ,CAAC,SAAS,mBAAmB,mBAAmB,sBAAsB;AAAA,MAChF,CAAC;AACD,gBAAU,KAAK,EAAE,SAAS,iBAAiB,UAAU,qBAAqB,CAAC;AAC3E,gBAAU,KAAK,EAAE,SAAS,kBAAkB,UAAU,sBAAsB,CAAC;AAAA,IAC/E,OAAO;AACL,gBAAU,KAAK,EAAE,SAAS,kBAAkB,UAAU,uBAAuB,CAAC;AAC9E,gBAAU,KAAK,EAAE,SAAS,iBAAiB,UAAU,qBAAqB,CAAC;AAC3E,gBAAU,KAAK,EAAE,SAAS,kBAAkB,UAAU,sBAAsB,CAAC;AAAA,IAC/E;AAEA,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAKA,QAAI,KAAK,YAAY,UAAU;AAC7B,cAAQ,KAAK,mBAAmB,sBAAsB;AAAA,IACxD;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AApFa,mBAAN;AAAA,EADN,OAAO,CAAC,CAAC;AAAA,GACG;","names":[]}
@@ -1 +0,0 @@
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';\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\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 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\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\n async onModuleDestroy(): Promise<void> {\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 // 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 +0,0 @@
1
- //# sourceMappingURL=chunk-UTN4GBPQ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../runtime/subsystems/integration/webhook-change-source.ts"],"sourcesContent":["/**\n * Integration subsystem — `WebhookChangeSource<T>` primitive (#226-4, ADR-033).\n *\n * Generic webhook-mode `IChangeSource<T>` implementation parameterized by a\n * parsed `DetectionConfig` (webhook mode) and a consumer-supplied\n * `WebhookFetchCallback<T>` that iterates a consumer-owned inbound staging\n * queue. The primitive owns:\n *\n * - canonical `Change<T>.source = 'webhook'` stamping;\n * - `dedupKey` derivation from the configured `webhook.eventIdField` on\n * the emitted record;\n * - `externalId` derivation: the mapping entry whose `target === 'external_id'`\n * names — via its `source` — the field on the emitted record that carries\n * the canonical external id (mirrors `PollChangeSource`);\n * - middleware-chain composition via the locked `ChangeMiddleware<T>` shape\n * (#226-1) — same composition seam as the poll primitive.\n *\n * The primitive is **passive**: it iterates whatever the consumer-owned\n * queue yields. It does NOT synchronously drive the orchestrator, does NOT\n * own a transport, and does NOT manage acks. The inbound staging table\n * schema is consumer-owned and deferred per ADR-0002 §Phase 4 — the\n * `WebhookFetchCallback<T>` is the queue contract the consumer injects.\n *\n * Shape locks (decision memo Q5, mirrored from poll primitive):\n * - `WebhookFetchContext = { subscription, cursor }` — explicitly NO\n * `userId` / `tenantId`. Run-scope identity is closed over by the\n * consumer at queue construction or resolved inside the callback via\n * consumer services. There are no `filters` on the webhook context —\n * filtering is done at registration / on the staging row, not at the\n * port seam (the queue is already filtered by the time the primitive\n * iterates).\n *\n * Long-lived streaming CDC primitives (SFDC Pub-Sub gRPC, Debezium/Kafka,\n * Postgres logical replication) are deferred to `#226-8` — they need a\n * fundamentally different lifecycle (`subscribe(onChange, onError)`,\n * server-paced backpressure, ack-on-yield) and shouldn't be retrofitted\n * into either this primitive or the poll primitive.\n */\n\nimport type { DetectionConfig } from './detection-config.schema';\nimport type {\n Change,\n IChangeSource,\n IntegrationSubscriptionView,\n} from './integration-change-source.protocol';\nimport type {\n ChangeIterator,\n ChangeMiddleware,\n} from './integration-middleware.protocol';\n\n// ============================================================================\n// Cursor + queue callback shapes\n// ============================================================================\n\n/**\n * Opaque webhook cursor shape. Webhook mode typically has a cursor of\n * `{ ts: ISO-string }` (last drained staging-row timestamp) but the\n * primitive treats it as opaque. Consumer-owned queue iterators interpret\n * it however the staging schema needs.\n */\nexport type WebhookCursor = unknown;\n\n/**\n * Context the primitive forwards to the queue iterator. Locked to exactly\n * two fields per the same Q5 reasoning that locks `PollFetchContext` — no\n * `userId` / `tenantId`.\n */\nexport interface WebhookFetchContext {\n readonly subscription: IntegrationSubscriptionView;\n readonly cursor: WebhookCursor | null;\n}\n\n/**\n * Consumer-supplied queue iterator. Returns an async iterable of\n * `{ record }` pairs — the consumer drains the inbound staging queue and\n * emits already-mapped canonical records `T`. The primitive stamps\n * `source: 'webhook'` and `dedupKey` from the record's configured\n * `webhook.eventIdField`; the consumer is the one who decided when a\n * staging row is \"ready\" to drain.\n *\n * Webhook mode has no per-record cursor advance — the staging-row drain\n * order is consumer policy (FIFO by ingestion timestamp, by event id, etc.)\n * and is opaque to the primitive. The orchestrator's last-yielded cursor\n * is whatever the consumer chooses to surface, if anything.\n */\nexport type WebhookFetchCallback<T> = (\n ctx: WebhookFetchContext,\n) => AsyncIterable<{ record: T; cursor?: WebhookCursor }>;\n\n// ============================================================================\n// Constructor options\n// ============================================================================\n\nexport interface WebhookChangeSourceOptions<T> {\n /** Consumer-supplied inbound queue iterator. */\n readonly queue: WebhookFetchCallback<T>;\n /**\n * Parsed detection config. MUST be `mode: 'webhook'`; the constructor\n * throws if a poll config is supplied. Codegen-emitted factories call\n * `DetectionConfigSchema.parse(...)` upstream so this is a safety net,\n * not the primary validation point.\n */\n readonly config: DetectionConfig;\n /**\n * Optional middleware chain. Same shape and composition rules as\n * `PollChangeSource` — first element is the outermost layer.\n */\n readonly middlewares?: ReadonlyArray<ChangeMiddleware<T>>;\n /**\n * Optional human label for run logs (e.g. `'stripe-webhook-charge'`).\n * Defaults to a derived label based on the mapping at construction.\n */\n readonly label?: string;\n}\n\n// ============================================================================\n// WebhookChangeSource<T>\n// ============================================================================\n\nexport class WebhookChangeSource<T> implements IChangeSource<T> {\n public readonly label: string;\n\n private readonly queue: WebhookFetchCallback<T>;\n private readonly externalIdSourceField: string;\n private readonly eventIdSourceField: string;\n private readonly composed: ChangeIterator<T>;\n\n constructor(opts: WebhookChangeSourceOptions<T>) {\n if (opts.config.mode !== 'webhook') {\n throw new Error(\n `WebhookChangeSource requires DetectionConfig.mode === 'webhook'; got '${(opts.config as { mode: string }).mode}'`,\n );\n }\n const config = opts.config;\n\n // Field mapping: locate the entry whose canonical `target` is `external_id`\n // — mirrors the poll primitive's contract. Adapters emit records\n // already-mapped; the primitive needs to know which key on T carries the\n // external id so it can stamp `Change.externalId`. That key is the\n // mapping's `source` (the field on the emitted record), NOT its `target`\n // (the canonical column) — they differ whenever the canonical record is\n // vendor-neutral camelCase (e.g. `source: 'externalId'` → `target: 'external_id'`).\n const externalIdMapping = config.mapping.find(\n (m) => m.target === 'external_id',\n );\n if (!externalIdMapping) {\n throw new Error(\n \"WebhookChangeSource: DetectionConfig.mapping must include an entry with target 'external_id' so emitted Change<T>.externalId can be populated\",\n );\n }\n this.externalIdSourceField = externalIdMapping.source;\n this.eventIdSourceField = config.webhook.eventIdField;\n\n this.queue = opts.queue;\n\n this.label =\n opts.label ?? `webhook-change-source:${externalIdMapping.source}`;\n\n // Compose middleware chain — same shape as PollChangeSource.\n const inner: ChangeIterator<T> = (sub, cur) => this.fetch(sub, cur);\n const middlewares = opts.middlewares ?? [];\n this.composed = middlewares.reduceRight<ChangeIterator<T>>(\n (next, mw) => mw(next),\n inner,\n );\n }\n\n listChanges(\n subscription: IntegrationSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n return this.composed(subscription, cursor);\n }\n\n private async *fetch(\n subscription: IntegrationSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n const ctx: WebhookFetchContext = {\n subscription,\n cursor: cursor as WebhookCursor | null,\n };\n\n for await (const { record, cursor: nextCursor } of this.queue(ctx)) {\n const externalIdRaw = (record as Record<string, unknown>)[\n this.externalIdSourceField\n ];\n if (typeof externalIdRaw !== 'string' || externalIdRaw.length === 0) {\n throw new Error(\n `WebhookChangeSource: record missing string '${this.externalIdSourceField}' — emitted records MUST carry the canonical external id keyed by the mapping source`,\n );\n }\n const eventIdRaw = (record as Record<string, unknown>)[\n this.eventIdSourceField\n ];\n if (typeof eventIdRaw !== 'string' || eventIdRaw.length === 0) {\n throw new Error(\n `WebhookChangeSource: record missing string '${this.eventIdSourceField}' — webhook records MUST carry the event id (DetectionConfig.webhook.eventIdField) so Change<T>.dedupKey can be populated`,\n );\n }\n\n const change: Change<T> = {\n externalId: externalIdRaw,\n // Webhook mode cannot distinguish create vs. update vs. delete on\n // its own — the orchestrator's diff stage handles classification.\n // Tombstone / soft-delete detection is consumer-driven (same as\n // poll mode — see ADR-033).\n operation: 'updated',\n record,\n cursor: nextCursor ?? null,\n source: 'webhook',\n dedupKey: eventIdRaw,\n };\n yield change;\n }\n }\n}\n"],"mappings":";AAuHO,IAAM,sBAAN,MAAyD;AAAA,EAC9C;AAAA,EAEC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAqC;AAC/C,QAAI,KAAK,OAAO,SAAS,WAAW;AAClC,YAAM,IAAI;AAAA,QACR,yEAA0E,KAAK,OAA4B,IAAI;AAAA,MACjH;AAAA,IACF;AACA,UAAM,SAAS,KAAK;AASpB,UAAM,oBAAoB,OAAO,QAAQ;AAAA,MACvC,CAAC,MAAM,EAAE,WAAW;AAAA,IACtB;AACA,QAAI,CAAC,mBAAmB;AACtB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,SAAK,wBAAwB,kBAAkB;AAC/C,SAAK,qBAAqB,OAAO,QAAQ;AAEzC,SAAK,QAAQ,KAAK;AAElB,SAAK,QACH,KAAK,SAAS,yBAAyB,kBAAkB,MAAM;AAGjE,UAAM,QAA2B,CAAC,KAAK,QAAQ,KAAK,MAAM,KAAK,GAAG;AAClE,UAAM,cAAc,KAAK,eAAe,CAAC;AACzC,SAAK,WAAW,YAAY;AAAA,MAC1B,CAAC,MAAM,OAAO,GAAG,IAAI;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,YACE,cACA,QAC0B;AAC1B,WAAO,KAAK,SAAS,cAAc,MAAM;AAAA,EAC3C;AAAA,EAEA,OAAe,MACb,cACA,QAC0B;AAC1B,UAAM,MAA2B;AAAA,MAC/B;AAAA,MACA;AAAA,IACF;AAEA,qBAAiB,EAAE,QAAQ,QAAQ,WAAW,KAAK,KAAK,MAAM,GAAG,GAAG;AAClE,YAAM,gBAAiB,OACrB,KAAK,qBACP;AACA,UAAI,OAAO,kBAAkB,YAAY,cAAc,WAAW,GAAG;AACnE,cAAM,IAAI;AAAA,UACR,+CAA+C,KAAK,qBAAqB;AAAA,QAC3E;AAAA,MACF;AACA,YAAM,aAAc,OAClB,KAAK,kBACP;AACA,UAAI,OAAO,eAAe,YAAY,WAAW,WAAW,GAAG;AAC7D,cAAM,IAAI;AAAA,UACR,+CAA+C,KAAK,kBAAkB;AAAA,QACxE;AAAA,MACF;AAEA,YAAM,SAAoB;AAAA,QACxB,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,QAKZ,WAAW;AAAA,QACX;AAAA,QACA,QAAQ,cAAc;AAAA,QACtB,QAAQ;AAAA,QACR,UAAU;AAAA,MACZ;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;","names":[]}