@pattern-stack/codegen 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/{chunk-WEVWJKOW.js → chunk-24WXSC3C.js} +10 -10
  3. package/dist/{chunk-KMZCQASO.js → chunk-3RWMQC3K.js} +18 -12
  4. package/dist/chunk-3RWMQC3K.js.map +1 -0
  5. package/dist/{chunk-24CWKBK5.js → chunk-4MF3HKJA.js} +3 -3
  6. package/dist/chunk-4MF3HKJA.js.map +1 -0
  7. package/dist/{chunk-4JLJYWJC.js → chunk-4PFF3ED4.js} +98 -10
  8. package/dist/chunk-4PFF3ED4.js.map +1 -0
  9. package/dist/{chunk-32BMMV4H.js → chunk-5RT7JGKT.js} +5 -5
  10. package/dist/{chunk-JRVNVKN6.js → chunk-BHZP6LOV.js} +2 -2
  11. package/dist/{chunk-4MVGAMUA.js → chunk-BK5ICA2F.js} +4 -4
  12. package/dist/{chunk-4RFHUZXU.js → chunk-BULPAAD3.js} +2 -2
  13. package/dist/{chunk-OZZJDRGW.js → chunk-CEWLVVAH.js} +10 -10
  14. package/dist/{chunk-YTN6BKWA.js → chunk-DRCLNYH7.js} +7 -7
  15. package/dist/{chunk-TNXH7BJS.js → chunk-E45CSC33.js} +2 -2
  16. package/dist/{chunk-L7BNNRGI.js → chunk-EBKVKN75.js} +26 -6
  17. package/dist/chunk-EBKVKN75.js.map +1 -0
  18. package/dist/{chunk-EOLLMEAH.js → chunk-EJBK7I4F.js} +3 -3
  19. package/dist/chunk-EJBK7I4F.js.map +1 -0
  20. package/dist/{chunk-K2I6XIK5.js → chunk-KSTZIULO.js} +4 -4
  21. package/dist/{chunk-Z7PQCAVK.js → chunk-LQ6PYFU6.js} +4 -4
  22. package/dist/chunk-MYQIQ27N.js +118 -0
  23. package/dist/chunk-MYQIQ27N.js.map +1 -0
  24. package/dist/{chunk-5Y7W3XR6.js → chunk-OTR44OH6.js} +24 -5
  25. package/dist/chunk-OTR44OH6.js.map +1 -0
  26. package/dist/{chunk-WPXNN6QS.js → chunk-RUYLXR5F.js} +11 -8
  27. package/dist/chunk-RUYLXR5F.js.map +1 -0
  28. package/dist/{chunk-7LKAMLV4.js → chunk-T6SCOJF4.js} +4 -4
  29. package/dist/{chunk-OGIZXGPY.js → chunk-TDEHU73T.js} +4 -4
  30. package/dist/{chunk-I6MG4M3F.js → chunk-VNBC3VXM.js} +2 -2
  31. package/dist/{chunk-YPWODKD5.js → chunk-W2UIDI3R.js} +5 -5
  32. package/dist/chunk-W4HOHZVF.js +1 -0
  33. package/dist/{chunk-WRUUSZDJ.js → chunk-WWGYCIJX.js} +3 -3
  34. package/dist/{chunk-RC23QROE.js → chunk-XDIIVIIK.js} +79 -5
  35. package/dist/chunk-XDIIVIIK.js.map +1 -0
  36. package/dist/{chunk-DCCZB4UC.js → chunk-XWBK3XJK.js} +4 -4
  37. package/dist/{chunk-SR7F3TJY.js → chunk-YK5JEVLX.js} +4 -4
  38. package/dist/{chunk-4OMHBMZJ.js → chunk-YLPAPPLW.js} +3 -3
  39. package/dist/chunk-YLPAPPLW.js.map +1 -0
  40. package/dist/{chunk-BIO6F7YI.js → chunk-ZPL74UQN.js} +4 -2
  41. package/dist/{chunk-BIO6F7YI.js.map → chunk-ZPL74UQN.js.map} +1 -1
  42. package/dist/runtime/base-classes/index.js +15 -15
  43. package/dist/runtime/shared/openapi/index.js +3 -3
  44. package/dist/runtime/subsystems/auth/auth.module.js +3 -3
  45. package/dist/runtime/subsystems/auth/index.js +7 -7
  46. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +3 -3
  47. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +3 -3
  48. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.d.ts +2 -1
  49. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +7 -6
  50. package/dist/runtime/subsystems/bridge/bridge.module.js +17 -16
  51. package/dist/runtime/subsystems/bridge/event-flow.service.js +3 -3
  52. package/dist/runtime/subsystems/bridge/index.js +24 -23
  53. package/dist/runtime/subsystems/cache/cache.module.js +1 -1
  54. package/dist/runtime/subsystems/cache/index.js +3 -3
  55. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +20 -0
  56. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +4 -3
  57. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +2 -2
  58. package/dist/runtime/subsystems/events/events.module.d.ts +14 -0
  59. package/dist/runtime/subsystems/events/events.module.js +6 -5
  60. package/dist/runtime/subsystems/events/index.js +9 -8
  61. package/dist/runtime/subsystems/index.d.ts +5 -1
  62. package/dist/runtime/subsystems/index.js +108 -96
  63. package/dist/runtime/subsystems/integration/build-change-source.js +3 -3
  64. package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
  65. package/dist/runtime/subsystems/integration/index.js +35 -35
  66. package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
  67. package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
  68. package/dist/runtime/subsystems/integration/integration.module.js +5 -5
  69. package/dist/runtime/subsystems/integration/poll-change-source.d.ts +1 -1
  70. package/dist/runtime/subsystems/integration/poll-change-source.js +1 -1
  71. package/dist/runtime/subsystems/integration/webhook-change-source.d.ts +4 -3
  72. package/dist/runtime/subsystems/integration/webhook-change-source.js +1 -1
  73. package/dist/runtime/subsystems/jobs/index.d.ts +2 -1
  74. package/dist/runtime/subsystems/jobs/index.js +30 -18
  75. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +3 -2
  76. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
  77. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +2 -1
  78. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +3 -2
  79. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
  80. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +2 -2
  81. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
  82. package/dist/runtime/subsystems/jobs/job-worker.d.ts +28 -0
  83. package/dist/runtime/subsystems/jobs/job-worker.js +3 -2
  84. package/dist/runtime/subsystems/jobs/job-worker.module.js +9 -8
  85. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +12 -7
  86. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +7 -6
  87. package/dist/runtime/subsystems/jobs/jobs-domain.tokens.d.ts +13 -1
  88. package/dist/runtime/subsystems/jobs/jobs-domain.tokens.js +3 -1
  89. package/dist/runtime/subsystems/jobs/pg-notify.d.ts +85 -0
  90. package/dist/runtime/subsystems/jobs/pg-notify.js +14 -0
  91. package/dist/runtime/subsystems/jobs/pg-notify.js.map +1 -0
  92. package/dist/runtime/subsystems/observability/index.js +7 -7
  93. package/dist/runtime/subsystems/observability/observability.module.js +4 -4
  94. package/dist/runtime/subsystems/observability/observability.service.js +3 -3
  95. package/dist/runtime/subsystems/storage/index.js +4 -4
  96. package/dist/runtime/subsystems/storage/storage.module.js +2 -2
  97. package/dist/src/cli/index.js +57 -19
  98. package/dist/src/cli/index.js.map +1 -1
  99. package/dist/src/index.js +11 -11
  100. package/package.json +1 -1
  101. package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +27 -0
  102. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +108 -4
  103. package/runtime/subsystems/events/events.module.ts +14 -0
  104. package/runtime/subsystems/index.ts +27 -0
  105. package/runtime/subsystems/integration/poll-change-source.ts +10 -7
  106. package/runtime/subsystems/integration/webhook-change-source.ts +12 -8
  107. package/runtime/subsystems/jobs/index.ts +10 -0
  108. package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +29 -2
  109. package/runtime/subsystems/jobs/job-worker.module.ts +11 -0
  110. package/runtime/subsystems/jobs/job-worker.ts +98 -0
  111. package/runtime/subsystems/jobs/jobs-domain.module.ts +22 -7
  112. package/runtime/subsystems/jobs/jobs-domain.tokens.ts +13 -0
  113. package/runtime/subsystems/jobs/pg-notify.ts +216 -0
  114. package/templates/subsystem/events-config/codegen-config-events-block.ejs.t +14 -0
  115. package/templates/subsystem/jobs-config/codegen-config-jobs-block.ejs.t +13 -4
  116. package/dist/chunk-24CWKBK5.js.map +0 -1
  117. package/dist/chunk-4JLJYWJC.js.map +0 -1
  118. package/dist/chunk-4OMHBMZJ.js.map +0 -1
  119. package/dist/chunk-5Y7W3XR6.js.map +0 -1
  120. package/dist/chunk-EOLLMEAH.js.map +0 -1
  121. package/dist/chunk-KMZCQASO.js.map +0 -1
  122. package/dist/chunk-L7BNNRGI.js.map +0 -1
  123. package/dist/chunk-RC23QROE.js.map +0 -1
  124. package/dist/chunk-UTN4GBPQ.js +0 -1
  125. package/dist/chunk-WPXNN6QS.js.map +0 -1
  126. /package/dist/{chunk-WEVWJKOW.js.map → chunk-24WXSC3C.js.map} +0 -0
  127. /package/dist/{chunk-32BMMV4H.js.map → chunk-5RT7JGKT.js.map} +0 -0
  128. /package/dist/{chunk-JRVNVKN6.js.map → chunk-BHZP6LOV.js.map} +0 -0
  129. /package/dist/{chunk-4MVGAMUA.js.map → chunk-BK5ICA2F.js.map} +0 -0
  130. /package/dist/{chunk-4RFHUZXU.js.map → chunk-BULPAAD3.js.map} +0 -0
  131. /package/dist/{chunk-OZZJDRGW.js.map → chunk-CEWLVVAH.js.map} +0 -0
  132. /package/dist/{chunk-YTN6BKWA.js.map → chunk-DRCLNYH7.js.map} +0 -0
  133. /package/dist/{chunk-TNXH7BJS.js.map → chunk-E45CSC33.js.map} +0 -0
  134. /package/dist/{chunk-K2I6XIK5.js.map → chunk-KSTZIULO.js.map} +0 -0
  135. /package/dist/{chunk-Z7PQCAVK.js.map → chunk-LQ6PYFU6.js.map} +0 -0
  136. /package/dist/{chunk-7LKAMLV4.js.map → chunk-T6SCOJF4.js.map} +0 -0
  137. /package/dist/{chunk-OGIZXGPY.js.map → chunk-TDEHU73T.js.map} +0 -0
  138. /package/dist/{chunk-I6MG4M3F.js.map → chunk-VNBC3VXM.js.map} +0 -0
  139. /package/dist/{chunk-YPWODKD5.js.map → chunk-W2UIDI3R.js.map} +0 -0
  140. /package/dist/{chunk-UTN4GBPQ.js.map → chunk-W4HOHZVF.js.map} +0 -0
  141. /package/dist/{chunk-WRUUSZDJ.js.map → chunk-WWGYCIJX.js.map} +0 -0
  142. /package/dist/{chunk-DCCZB4UC.js.map → chunk-XWBK3XJK.js.map} +0 -0
  143. /package/dist/{chunk-SR7F3TJY.js.map → chunk-YK5JEVLX.js.map} +0 -0
@@ -1,21 +1,21 @@
1
1
  import {
2
2
  assertTenantId
3
3
  } from "./chunk-6DWFJNIK.js";
4
+ import {
5
+ JOB_ORCHESTRATOR
6
+ } from "./chunk-ZPL74UQN.js";
4
7
  import {
5
8
  JobHandler,
6
9
  JobHandlerBase
7
10
  } from "./chunk-CO6LUM72.js";
8
- import {
9
- JOB_ORCHESTRATOR
10
- } from "./chunk-BIO6F7YI.js";
11
- import {
12
- EVENT_BUS
13
- } from "./chunk-H5NH7KPE.js";
14
11
  import {
15
12
  BRIDGE_DELIVERY_REPO,
16
13
  BRIDGE_MULTI_TENANT,
17
14
  BRIDGE_REGISTRY
18
15
  } 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-YTN6BKWA.js.map
121
+ //# sourceMappingURL=chunk-DRCLNYH7.js.map
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-AQFQ4BYM.js";
4
4
  import {
5
5
  ObservabilityService
6
- } from "./chunk-YPWODKD5.js";
6
+ } from "./chunk-W2UIDI3R.js";
7
7
  import {
8
8
  OBSERVABILITY,
9
9
  OBSERVABILITY_MODULE_OPTIONS
@@ -45,4 +45,4 @@ ObservabilityModule = __decorateClass([
45
45
  export {
46
46
  ObservabilityModule
47
47
  };
48
- //# sourceMappingURL=chunk-TNXH7BJS.js.map
48
+ //# sourceMappingURL=chunk-E45CSC33.js.map
@@ -1,15 +1,22 @@
1
1
  import {
2
2
  BRIDGE_DELIVERY_JOB_TYPE
3
- } from "./chunk-YTN6BKWA.js";
3
+ } from "./chunk-DRCLNYH7.js";
4
4
  import {
5
5
  bridgeDelivery
6
6
  } from "./chunk-2TVVBC53.js";
7
7
  import {
8
- BRIDGE_REGISTRY
9
- } from "./chunk-4LH67P4U.js";
8
+ JOBS_LISTEN_NOTIFY
9
+ } from "./chunk-ZPL74UQN.js";
10
10
  import {
11
11
  jobRuns
12
12
  } from "./chunk-OKXZ63IA.js";
13
+ import {
14
+ JOBS_WAKE_CHANNEL,
15
+ pgNotify
16
+ } from "./chunk-MYQIQ27N.js";
17
+ import {
18
+ BRIDGE_REGISTRY
19
+ } from "./chunk-4LH67P4U.js";
13
20
  import {
14
21
  __decorateClass,
15
22
  __decorateParam
@@ -25,10 +32,12 @@ var POOL_BY_DIRECTION = {
25
32
  outbound: "events_outbound"
26
33
  };
27
34
  var BridgeOutboxDrainHook = class {
28
- constructor(registry = {}) {
35
+ constructor(registry = {}, listenNotify = false) {
29
36
  this.registry = registry;
37
+ this.listenNotify = listenNotify;
30
38
  }
31
39
  registry;
40
+ listenNotify;
32
41
  logger = new Logger(BridgeOutboxDrainHook.name);
33
42
  warnedNullDirection = false;
34
43
  warnedAuditTypes = /* @__PURE__ */ new Set();
@@ -101,6 +110,15 @@ var BridgeOutboxDrainHook = class {
101
110
  dedupSkips++;
102
111
  continue;
103
112
  }
113
+ if (this.listenNotify) {
114
+ try {
115
+ await pgNotify(tx, JOBS_WAKE_CHANNEL, wrapperPool);
116
+ } catch (err) {
117
+ this.logger.warn(
118
+ `pg_notify(${JOBS_WAKE_CHANNEL}, ${wrapperPool}) failed for wrapper run ${wrapperRunId}: ${err.message} (non-fatal \u2014 the reserved-pool worker still polls).`
119
+ );
120
+ }
121
+ }
104
122
  delivered++;
105
123
  }
106
124
  return {
@@ -125,10 +143,12 @@ var BridgeOutboxDrainHook = class {
125
143
  BridgeOutboxDrainHook = __decorateClass([
126
144
  Injectable(),
127
145
  __decorateParam(0, Optional()),
128
- __decorateParam(0, Inject(BRIDGE_REGISTRY))
146
+ __decorateParam(0, Inject(BRIDGE_REGISTRY)),
147
+ __decorateParam(1, Optional()),
148
+ __decorateParam(1, Inject(JOBS_LISTEN_NOTIFY))
129
149
  ], BridgeOutboxDrainHook);
130
150
 
131
151
  export {
132
152
  BridgeOutboxDrainHook
133
153
  };
134
- //# sourceMappingURL=chunk-L7BNNRGI.js.map
154
+ //# sourceMappingURL=chunk-EBKVKN75.js.map
@@ -0,0 +1 @@
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';\nimport { JOBS_LISTEN_NOTIFY } from '../jobs/jobs-domain.tokens';\nimport { JOBS_WAKE_CHANNEL, pgNotify } from '../jobs/pg-notify';\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 // LISTEN-NOTIFY-1 — when true, the wrapper `job_run` insert below emits an\n // in-tx `pg_notify(codegen_jobs_wake, <wrapperPool>)` so the reserved-pool\n // worker wakes the instant the per-event drain tx commits — otherwise the\n // bridge hop alone would still cost a full poll interval. `@Optional()`\n // defaulting false so the hook keeps working when jobs isn't installed\n // (bridge can drive non-jobs consumers) or in vendored/test wiring.\n @Optional()\n @Inject(JOBS_LISTEN_NOTIFY)\n private readonly listenNotify: boolean = false,\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 // LISTEN-NOTIFY-1 — the wrapper run is real and claimable; wake its\n // reserved-pool worker on commit (D7). Same `tx` as the inserts above, so\n // delivery is gated on the per-event drain tx committing. Best-effort: a\n // notify failure is non-fatal (the reserved-pool worker still polls).\n if (this.listenNotify) {\n try {\n await pgNotify(tx, JOBS_WAKE_CHANNEL, wrapperPool);\n } catch (err) {\n this.logger.warn(\n `pg_notify(${JOBS_WAKE_CHANNEL}, ${wrapperPool}) failed for ` +\n `wrapper run ${wrapperRunId}: ${(err as Error).message} ` +\n `(non-fatal — the reserved-pool worker still polls).`,\n );\n }\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":[]}
@@ -3,10 +3,10 @@ import {
3
3
  } from "./chunk-GM3RMJIJ.js";
4
4
  import {
5
5
  DrizzleEventBus
6
- } from "./chunk-4JLJYWJC.js";
6
+ } from "./chunk-4PFF3ED4.js";
7
7
  import {
8
8
  MemoryEventBus
9
- } from "./chunk-Z7PQCAVK.js";
9
+ } from "./chunk-LQ6PYFU6.js";
10
10
  import {
11
11
  EVENTS_MODULE_OPTIONS,
12
12
  EVENTS_MULTI_TENANT,
@@ -152,4 +152,4 @@ EventsModule = __decorateClass([
152
152
  export {
153
153
  EventsModule
154
154
  };
155
- //# sourceMappingURL=chunk-EOLLMEAH.js.map
155
+ //# sourceMappingURL=chunk-EJBK7I4F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/events/events.module.ts"],"sourcesContent":["/**\n * EventsModule — DynamicModule factory for the event bus subsystem.\n *\n * Register once in AppModule:\n * ```typescript\n * @Module({\n * imports: [\n * EventsModule.forRoot({ backend: 'drizzle' }),\n * ],\n * })\n * export class AppModule {}\n * ```\n *\n * Tests swap to the memory backend without touching application code:\n * ```typescript\n * Test.createTestingModule({\n * imports: [EventsModule.forRoot({ backend: 'memory' })],\n * });\n * ```\n *\n * Per-pool drain isolation (EVT-4):\n * ```typescript\n * EventsModule.forRoot({ backend: 'drizzle', pools: ['events_change'] });\n * ```\n * Each process restricts its drain loop to the pools listed here. `pools`\n * is undefined by default → drain all pending rows (backwards-compatible).\n *\n * Typed facade + multi-tenancy (EVT-6):\n * - `TYPED_EVENT_BUS` resolves to the generated `TypedEventBus` wrapping\n * whichever backend is selected.\n * - `multiTenant: true` makes `TypedEventBus.publish` throw\n * `MissingTenantIdError` when the caller forgets `metadata.tenantId`.\n *\n * `global: true` means entity modules do not need to import EventsModule\n * individually — the EVENT_BUS and TYPED_EVENT_BUS tokens are available\n * project-wide.\n *\n * Async configuration (`forRootAsync`):\n * The async factory returns `EventsModuleOptions`; the EVENT_BUS provider\n * then receives its backend dependencies — DRIZZLE for the drizzle\n * backend, REDIS_URL for the redis backend, the resolved options for the\n * memory backend — through a proper `useFactory` so Nest DI wires them\n * correctly. Earlier revisions hand-constructed backends with\n * `new Class()` which silently left `db` / `redisUrl` undefined\n * (issue #108).\n */\nimport { Module, type DynamicModule, type Provider, type Type } from '@nestjs/common';\nimport {\n EVENT_BUS,\n EVENT_READ_PORT,\n EVENTS_MODULE_OPTIONS,\n EVENTS_MULTI_TENANT,\n REDIS_URL,\n TYPED_EVENT_BUS,\n} from './events.tokens';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DrizzleEventBus } from './event-bus.drizzle-backend';\nimport { MemoryEventBus } from './event-bus.memory-backend';\n// #6 — `RedisEventBus` is lazy-loaded only when `backend: 'redis'` is selected.\n// The file is filtered out of the vendor set for non-redis installs (see\n// `backendFileFilter` in src/cli/commands/subsystem.ts); the dynamic-string\n// import below makes TS treat the specifier as `any` so the consumer's tsc\n// never tries to resolve the absent file.\nimport { TypedEventBus } from './generated/bus';\n\n/**\n * Lazy-load the Redis backend. Routed through a non-literal specifier so\n * the consumer's `tsc` doesn't resolve `./event-bus.redis-backend` at type\n * check time — important because that file is filtered out of drizzle/\n * memory installs (#6).\n */\nasync function loadRedisEventBus(): Promise<new (url: string) => object> {\n // Non-literal specifier — TS gives this an `any` module type, sidestepping\n // resolution of a file that may not be vendored.\n const specifier = './event-bus.redis-backend';\n const mod = (await import(specifier)) as { RedisEventBus: new (url: string) => object };\n return mod.RedisEventBus;\n}\n\nexport interface EventsModuleOptions {\n backend: 'drizzle' | 'memory' | 'redis';\n /**\n * Redis connection URL used when `backend` is `'redis'`.\n * Falls back to the REDIS_URL environment variable, then\n * `redis://localhost:6379` if neither is set.\n */\n redisUrl?: string;\n /**\n * Restrict the drain loop to these pools. Each pool name matches the\n * `domain_events.pool` column (populated from `event.metadata.pool` at\n * publish time). Leave undefined to drain all pending rows.\n *\n * Typical lane split: one process per `events_inbound` /\n * `events_change` / `events_outbound` so a slow outbound handler\n * cannot stall change-event propagation (see ADR-022).\n */\n pools?: string[];\n /**\n * LISTEN-NOTIFY-1 — when `true` (drizzle backend only), the drainer holds a\n * dedicated listener connection and LISTENs on `codegen_events_wake`. Each\n * `publish`/`publishMany` emits an in-tx `pg_notify(codegen_events_wake,\n * <pool>)` so the drainer wakes the moment the publishing transaction commits,\n * instead of waiting for the next poll tick. Polling continues unchanged as\n * the fallback heartbeat; a lost notify degrades to poll latency, never to\n * lost work. Defaults to `false`.\n *\n * Ignored by the memory + redis backends (memory dispatches inline; redis has\n * its own fan-out). Requires a direct (non-transaction-pooler) connection —\n * see the events/jobs config block re: PgBouncer.\n */\n listenNotify?: boolean;\n /**\n * Multi-tenancy opt-in (EVT-6).\n *\n * When `true`, every `TypedEventBus.publish()` call must supply\n * `opts.metadata.tenantId` — otherwise it throws `MissingTenantIdError`.\n * The tenantId is preserved on `event.metadata` and, for the Drizzle\n * backend, written to `domain_events.tenant_id` (EVT-4).\n *\n * Drain-side tenant filtering is deferred — the tenant-context model\n * (per-process vs. per-request vs. async-local-storage) is still\n * unsettled; see ADR-024 §Multi-tenancy. Only the publish-side\n * requirement ships here.\n *\n * Defaults to `false`.\n */\n multiTenant?: boolean;\n /**\n * The generated `TypedEventBus` class to bind to `TYPED_EVENT_BUS`.\n *\n * **Package mode (ADR-037).** When the runtime is imported from\n * `@pattern-stack/codegen` (not vendored), the bundled `./generated/bus`\n * `TypedEventBus` is typed to an EMPTY event union and reads the bundled\n * empty `eventRegistry` — a consumer's `events/*.yaml` are scanned into\n * `src/generated/events/bus.ts` (typed to THEIR union, reading THEIR\n * registry), which the package can't import. The generated subsystem barrel\n * therefore threads that class in here:\n * `EventsModule.forRoot({ ..., typedBus: TypedEventBus })`. Nest constructs\n * it with this module's `EVENT_BUS` + `EVENTS_MULTI_TENANT` providers (the\n * generated class injects the same string-valued tokens, which match across\n * the package boundary).\n *\n * Omitted (vendored mode / tests) ⇒ falls back to the bundled\n * `./generated/bus`, which in a vendored tree IS the consumer's generated\n * file. Without this, a package-mode consumer's typed `publish<'…'>()` calls\n * resolve against the empty union and their events never get the right\n * `pool` / `direction` stamped.\n *\n * Only consulted by `forRoot` (the path the barrel emits); `forRootAsync`\n * keeps the bundled bus.\n */\n typedBus?: Type<unknown>;\n}\n\nexport interface EventsModuleAsyncOptions {\n useFactory: (...args: unknown[]) => Promise<EventsModuleOptions> | EventsModuleOptions;\n inject?: unknown[];\n imports?: unknown[];\n}\n\n/**\n * Shared provider set: `TypedEventBus` itself, the `TYPED_EVENT_BUS` token\n * binding, and the resolved `EVENTS_MULTI_TENANT` flag. Returned from one\n * place so every `forRoot` branch and `forRootAsync` agree.\n */\nfunction buildTypedBusProviders(\n multiTenant: boolean,\n typedBus?: Type<unknown>,\n): Provider[] {\n // Package mode threads the consumer's generated `TypedEventBus` (typed to\n // their event union, reading their registry) via `typedBus`; vendored mode\n // omits it and we fall back to the bundled `./generated/bus` (which IS the\n // consumer's generated file in a vendored tree). See `EventsModuleOptions.typedBus`.\n const BusClass = typedBus ?? TypedEventBus;\n return [\n BusClass,\n { provide: TYPED_EVENT_BUS, useExisting: BusClass },\n { provide: EVENTS_MULTI_TENANT, useValue: multiTenant },\n ];\n}\n\n/**\n * Construct the backend instance for the async path, routing constructor\n * arguments through Nest-resolved dependencies.\n *\n * DRIZZLE is declared optional at inject time so that memory-backend\n * consumers aren't required to also import `DatabaseModule`. If the\n * drizzle backend is selected but no DRIZZLE provider is registered, we\n * throw a clear error instead of silently constructing a broken bus.\n */\nasync function buildEventBusAsync(\n options: EventsModuleOptions,\n db: DrizzleClient | null,\n redisUrl: string,\n): Promise<unknown> {\n if (options.backend === 'drizzle') {\n if (!db) {\n throw new Error(\n \"EventsModule.forRootAsync: backend: 'drizzle' selected but DRIZZLE provider is not available. \" +\n 'Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before EventsModule.forRootAsync.',\n );\n }\n return new DrizzleEventBus(db, options);\n }\n if (options.backend === 'redis') {\n // #6: lazy import — the redis backend ships only with `--backend redis`\n // installs; drizzle/memory consumers never touch the file.\n const RedisEventBus = await loadRedisEventBus();\n return new RedisEventBus(redisUrl);\n }\n return new MemoryEventBus(options);\n}\n\n@Module({})\nexport class EventsModule {\n static forRootAsync(asyncOptions: EventsModuleAsyncOptions): DynamicModule {\n return {\n module: EventsModule,\n global: true,\n imports: (asyncOptions.imports ?? []) as Parameters<typeof Module>[0]['imports'],\n providers: [\n {\n provide: EVENTS_MODULE_OPTIONS,\n useFactory: asyncOptions.useFactory,\n inject: (asyncOptions.inject ?? []) as (string | symbol | Function)[],\n },\n {\n provide: EVENTS_MULTI_TENANT,\n useFactory: (options: EventsModuleOptions) => options.multiTenant ?? false,\n inject: [EVENTS_MODULE_OPTIONS],\n },\n {\n provide: REDIS_URL,\n useFactory: (options: EventsModuleOptions) =>\n options.redisUrl ?? process.env['REDIS_URL'] ?? 'redis://localhost:6379',\n inject: [EVENTS_MODULE_OPTIONS],\n },\n {\n provide: EVENT_BUS,\n useFactory: (\n options: EventsModuleOptions,\n db: DrizzleClient | null,\n redisUrl: string,\n ) => buildEventBusAsync(options, db, redisUrl),\n inject: [\n EVENTS_MODULE_OPTIONS,\n { token: DRIZZLE, optional: true },\n REDIS_URL,\n ],\n },\n {\n // Read port (OBS-LIST-1). Drizzle + memory backends implement\n // IEventReadPort on the EVENT_BUS instance; the redis backend\n // retains no history, so EVENT_READ_PORT resolves to `null` and\n // optional consumers (the observability combiner) degrade to\n // empty results.\n provide: EVENT_READ_PORT,\n useFactory: (options: EventsModuleOptions, bus: unknown) =>\n options.backend === 'redis' ? null : bus,\n inject: [EVENTS_MODULE_OPTIONS, EVENT_BUS],\n },\n TypedEventBus,\n { provide: TYPED_EVENT_BUS, useExisting: TypedEventBus },\n ],\n exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],\n };\n }\n\n static forRoot(\n options: EventsModuleOptions = { backend: 'drizzle' },\n ): DynamicModule {\n const multiTenant = options.multiTenant ?? false;\n\n if (options.backend === 'redis') {\n const resolvedUrl =\n options.redisUrl ?? process.env['REDIS_URL'] ?? 'redis://localhost:6379';\n\n return {\n module: EventsModule,\n global: true,\n providers: [\n { provide: EVENTS_MODULE_OPTIONS, useValue: options },\n { provide: REDIS_URL, useValue: resolvedUrl },\n {\n // #6: useFactory + dynamic import so the consumer's tsc never\n // needs to resolve `event-bus.redis-backend.ts` for drizzle/\n // memory installs (the file is filtered out by\n // `backendFileFilter`). Nest awaits async factories + manages\n // lifecycle on the returned instance, so we drop the old bare\n // `RedisEventBus` provider entry.\n provide: EVENT_BUS,\n useFactory: async (url: string): Promise<object> => {\n const RedisEventBus = await loadRedisEventBus();\n return new RedisEventBus(url);\n },\n inject: [REDIS_URL],\n },\n ...buildTypedBusProviders(multiTenant, options.typedBus),\n ],\n exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],\n };\n }\n\n const provider =\n options.backend === 'drizzle'\n ? { provide: EVENT_BUS, useClass: DrizzleEventBus }\n : { provide: EVENT_BUS, useClass: MemoryEventBus };\n\n return {\n module: EventsModule,\n global: true,\n providers: [\n { provide: EVENTS_MODULE_OPTIONS, useValue: options },\n provider,\n // Read port (OBS-LIST-1): drizzle + memory backends implement\n // IEventReadPort on the same instance as EVENT_BUS. The redis\n // backend retains no history and does not provide this token.\n { provide: EVENT_READ_PORT, useExisting: EVENT_BUS },\n ...buildTypedBusProviders(multiTenant, options.typedBus),\n ],\n exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],\n };\n }\n}\n"],"mappings":"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","names":[]}
@@ -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-K2I6XIK5.js.map
122
+ //# sourceMappingURL=chunk-KSTZIULO.js.map
@@ -1,11 +1,11 @@
1
+ import {
2
+ EVENTS_MODULE_OPTIONS
3
+ } from "./chunk-H5NH7KPE.js";
1
4
  import {
2
5
  clampEventLimit,
3
6
  decodeEventCursor,
4
7
  encodeEventCursor
5
8
  } from "./chunk-UQ5EHOH2.js";
6
- import {
7
- EVENTS_MODULE_OPTIONS
8
- } from "./chunk-H5NH7KPE.js";
9
9
  import {
10
10
  __decorateClass,
11
11
  __decorateParam
@@ -197,4 +197,4 @@ MemoryEventBus = __decorateClass([
197
197
  export {
198
198
  MemoryEventBus
199
199
  };
200
- //# sourceMappingURL=chunk-Z7PQCAVK.js.map
200
+ //# sourceMappingURL=chunk-LQ6PYFU6.js.map
@@ -0,0 +1,118 @@
1
+ // runtime/subsystems/jobs/pg-notify.ts
2
+ import { Logger } from "@nestjs/common";
3
+ import { sql } from "drizzle-orm";
4
+ var JOBS_WAKE_CHANNEL = "codegen_jobs_wake";
5
+ var EVENTS_WAKE_CHANNEL = "codegen_events_wake";
6
+ async function pgNotify(tx, channel, payload) {
7
+ const client = tx;
8
+ await client.execute(sql`select pg_notify(${channel}, ${payload})`);
9
+ }
10
+ var DEFAULT_BACKOFF_MIN_MS = 100;
11
+ var DEFAULT_BACKOFF_MAX_MS = 5e3;
12
+ var PgNotifyListener = class {
13
+ constructor(opts) {
14
+ this.opts = opts;
15
+ this.logger = new Logger(`PgNotifyListener(${opts.label})`);
16
+ this.backoffMinMs = opts.backoffMinMs ?? DEFAULT_BACKOFF_MIN_MS;
17
+ this.backoffMaxMs = opts.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS;
18
+ this.backoffMs = this.backoffMinMs;
19
+ }
20
+ opts;
21
+ logger;
22
+ client = null;
23
+ stopped = false;
24
+ reconnectTimer = null;
25
+ backoffMs;
26
+ backoffMinMs;
27
+ backoffMaxMs;
28
+ /** WARN-once gate so a flapping listener doesn't spam the log. */
29
+ warnedDown = false;
30
+ /** Begin listening. Idempotent-ish: a second call while connected is a no-op. */
31
+ async start() {
32
+ this.stopped = false;
33
+ await this.connect();
34
+ }
35
+ /** Stop listening + release the connection. Safe to call repeatedly. */
36
+ async stop() {
37
+ this.stopped = true;
38
+ if (this.reconnectTimer) {
39
+ clearTimeout(this.reconnectTimer);
40
+ this.reconnectTimer = null;
41
+ }
42
+ await this.releaseClient();
43
+ }
44
+ async connect() {
45
+ if (this.stopped) return;
46
+ try {
47
+ const client = await this.opts.pool.connect();
48
+ client.on("notification", (msg) => {
49
+ if (msg.channel !== this.opts.channel) return;
50
+ try {
51
+ this.opts.onNotify(msg.payload ?? "");
52
+ } catch (err) {
53
+ this.logger.error(`onNotify threw: ${err.message}`);
54
+ }
55
+ });
56
+ client.on("error", (err) => {
57
+ this.logger.debug?.(`listener connection error: ${err.message}`);
58
+ this.handleDrop();
59
+ });
60
+ await client.query(`LISTEN ${this.opts.channel}`);
61
+ this.client = client;
62
+ if (this.warnedDown) {
63
+ this.logger.log(
64
+ `listener reconnected; LISTEN ${this.opts.channel} re-established`
65
+ );
66
+ this.warnedDown = false;
67
+ }
68
+ this.backoffMs = this.backoffMinMs;
69
+ } catch (err) {
70
+ this.handleConnectFailure(err);
71
+ }
72
+ }
73
+ /** Connection dropped after being established → reconnect. */
74
+ handleDrop() {
75
+ if (this.stopped) return;
76
+ void this.releaseClient().finally(() => this.scheduleReconnect());
77
+ }
78
+ /** Initial / reconnect `connect()` threw. */
79
+ handleConnectFailure(err) {
80
+ this.scheduleReconnect(err);
81
+ }
82
+ scheduleReconnect(err) {
83
+ if (this.stopped) return;
84
+ if (!this.warnedDown) {
85
+ this.warnedDown = true;
86
+ this.logger.warn(
87
+ `listener down \u2014 falling back to interval polling until reconnect. Cause: ${err instanceof Error ? err.message : "connection lost"}. (This degrades latency, not durability \u2014 polling still drives all work.)`
88
+ );
89
+ }
90
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
91
+ const delay = this.backoffMs;
92
+ this.backoffMs = Math.min(this.backoffMs * 2, this.backoffMaxMs);
93
+ this.reconnectTimer = setTimeout(() => {
94
+ this.reconnectTimer = null;
95
+ void this.connect();
96
+ }, delay);
97
+ }
98
+ async releaseClient() {
99
+ const client = this.client;
100
+ this.client = null;
101
+ if (!client) return;
102
+ try {
103
+ client.removeAllListeners?.("notification");
104
+ client.removeAllListeners?.("error");
105
+ if (client.release) client.release(true);
106
+ else if (client.end) await client.end();
107
+ } catch {
108
+ }
109
+ }
110
+ };
111
+
112
+ export {
113
+ JOBS_WAKE_CHANNEL,
114
+ EVENTS_WAKE_CHANNEL,
115
+ pgNotify,
116
+ PgNotifyListener
117
+ };
118
+ //# sourceMappingURL=chunk-MYQIQ27N.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 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 /** Stop listening + release the connection. Safe to call repeatedly. */\n async stop(): Promise<void> {\n this.stopped = true;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n await this.releaseClient();\n }\n\n private async connect(): Promise<void> {\n if (this.stopped) return;\n try {\n const client = await this.opts.pool.connect();\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 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 try {\n client.removeAllListeners?.('notification');\n client.removeAllListeners?.('error');\n // A listener client is a checked-out pool connection; release it back\n // with `release(true)` (destroy) so a half-broken socket isn't reused.\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":"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","names":[]}
@@ -6,13 +6,18 @@ import {
6
6
  MissingTenantIdError
7
7
  } from "./chunk-T4BIIU5E.js";
8
8
  import {
9
+ JOBS_LISTEN_NOTIFY,
9
10
  JOBS_MULTI_TENANT
10
- } from "./chunk-BIO6F7YI.js";
11
+ } from "./chunk-ZPL74UQN.js";
11
12
  import {
12
13
  jobRuns,
13
14
  jobSteps,
14
15
  jobs
15
16
  } from "./chunk-OKXZ63IA.js";
17
+ import {
18
+ JOBS_WAKE_CHANNEL,
19
+ pgNotify
20
+ } from "./chunk-MYQIQ27N.js";
16
21
  import {
17
22
  DRIZZLE
18
23
  } from "./chunk-U64T4YZE.js";
@@ -23,7 +28,7 @@ import {
23
28
 
24
29
  // runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts
25
30
  import { randomUUID } from "crypto";
26
- import { Inject, Injectable, Logger } from "@nestjs/common";
31
+ import { Inject, Injectable, Logger, Optional } from "@nestjs/common";
27
32
  import { and, desc, eq, gt, inArray, isNotNull, ne, notInArray, sql } from "drizzle-orm";
28
33
  var TERMINAL_STATUSES = [
29
34
  "completed",
@@ -43,12 +48,14 @@ function evaluateKeyTemplate(template, input) {
43
48
  });
44
49
  }
45
50
  var DrizzleJobOrchestrator = class {
46
- constructor(db, multiTenant) {
51
+ constructor(db, multiTenant, listenNotify = false) {
47
52
  this.db = db;
48
53
  this.multiTenant = multiTenant;
54
+ this.listenNotify = listenNotify;
49
55
  }
50
56
  db;
51
57
  multiTenant;
58
+ listenNotify;
52
59
  // TODO(logging-subsystem): swap to ILogger once ADR-028 lands
53
60
  logger = new Logger(DrizzleJobOrchestrator.name);
54
61
  /**
@@ -157,6 +164,16 @@ var DrizzleJobOrchestrator = class {
157
164
  claimedAt: null,
158
165
  attempts: 0
159
166
  }).returning();
167
+ if (this.listenNotify) {
168
+ const wakePool = inserted.pool;
169
+ try {
170
+ await pgNotify(client, JOBS_WAKE_CHANNEL, wakePool);
171
+ } catch (err) {
172
+ this.logger.warn(
173
+ `pg_notify(${JOBS_WAKE_CHANNEL}, ${wakePool}) failed for run ${inserted.id}: ${err.message} (non-fatal \u2014 interval polling still claims the run).`
174
+ );
175
+ }
176
+ }
160
177
  return inserted;
161
178
  }
162
179
  // ==========================================================================
@@ -341,7 +358,9 @@ var DrizzleJobOrchestrator = class {
341
358
  DrizzleJobOrchestrator = __decorateClass([
342
359
  Injectable(),
343
360
  __decorateParam(0, Inject(DRIZZLE)),
344
- __decorateParam(1, Inject(JOBS_MULTI_TENANT))
361
+ __decorateParam(1, Inject(JOBS_MULTI_TENANT)),
362
+ __decorateParam(2, Optional()),
363
+ __decorateParam(2, Inject(JOBS_LISTEN_NOTIFY))
345
364
  ], DrizzleJobOrchestrator);
346
365
  function notInStatus(statuses) {
347
366
  const negated = statuses.map((s) => ne(jobRuns.status, s));
@@ -353,4 +372,4 @@ export {
353
372
  evaluateKeyTemplate,
354
373
  DrizzleJobOrchestrator
355
374
  };
356
- //# sourceMappingURL=chunk-5Y7W3XR6.js.map
375
+ //# sourceMappingURL=chunk-OTR44OH6.js.map
@@ -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';\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.\n if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {\n const dedupeKey = evaluateKeyTemplate(definition.dedupeKeyTemplate, payload);\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 concurrencyKey = evaluateKeyTemplate(\n definition.concurrencyKeyTemplate,\n payload,\n );\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 =\n definition.dedupeKeyTemplate\n ? evaluateKeyTemplate(definition.dedupeKeyTemplate, payload)\n : null;\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 const concurrencyKeyTemplate =\n (meta.concurrency as { key?: unknown } | undefined)?.key;\n const concurrencyKeyTemplateStr =\n typeof concurrencyKeyTemplate === 'string' ? concurrencyKeyTemplate : null;\n const collisionMode =\n (meta.concurrency?.collisionMode as JobDefinitionRow['collisionMode']) ??\n 'queue';\n const dedupeKeyTemplate =\n (meta.dedupe as { key?: unknown } | undefined)?.key;\n const dedupeKeyTemplateStr =\n typeof dedupeKeyTemplate === 'string' ? dedupeKeyTemplate : null;\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,SAAS,kBAAkB;AAC3B,SAAS,QAAQ,YAAY,QAAQ,gBAAgB;AACrD,SAAS,KAAK,MAAM,IAAI,IAAI,SAAS,WAAW,IAAI,YAAY,WAAW;AAiCpE,IAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKA,IAAM,2BAA2C,CAAC,YAAY,QAAQ;AAEtE,IAAM,qBAAqC,CAAC,WAAW,SAAS;AAUzD,SAAS,oBACd,UACA,OACQ;AACR,SAAO,SAAS,QAAQ,kCAAkC,CAAC,QAAQ,UAAkB;AACnF,UAAM,QAAQ,MAAM,KAAK;AACzB,QAAI,UAAU,UAAa,UAAU,MAAM;AACzC,YAAM,IAAI,6BAA6B,UAAU,KAAK;AAAA,IACxD;AACA,WAAO,OAAO,KAAK;AAAA,EACrB,CAAC;AACH;AAGO,IAAM,yBAAN,MAAyD;AAAA,EAI9D,YACoC,IACU,aAO3B,eAAwB,OACzC;AATkC;AACU;AAO3B;AAAA,EAChB;AAAA,EATiC;AAAA,EACU;AAAA,EAO3B;AAAA;AAAA,EAXF,SAAS,IAAI,OAAO,uBAAuB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBxD,gBACN,QACA,UACe;AACf,QAAI,CAAC,KAAK,YAAa,QAAO;AAC9B,QAAI,aAAa,OAAW,OAAM,IAAI,qBAAqB,MAAM;AACjE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MACJ,MACA,OACA,OAAqB,CAAC,GACtB,IACiB;AACjB,UAAM,UAAW,SAAS,CAAC;AAI3B,UAAM,WAAW,KAAK,gBAAgB,SAAS,KAAK,QAAQ;AAM5D,UAAM,SAAU,MAAM,KAAK;AAG3B,UAAM,CAAC,GAAG,IAAI,MAAM,OACjB,OAAO,EACP,KAAK,IAAI,EACT,MAAM,GAAG,KAAK,MAAM,IAAI,CAAC,EACzB,MAAM,CAAC;AACV,QAAI,CAAC,IAAK,OAAM,IAAI,qBAAqB,IAAI;AAC7C,UAAM,aAAa;AAGnB,QAAI,WAAW,qBAAqB,WAAW,gBAAgB;AAC7D,YAAMA,aAAY,oBAAoB,WAAW,mBAAmB,OAAO;AAC3E,YAAM,cAAc,IAAI,KAAK,KAAK,IAAI,IAAI,WAAW,cAAc;AACnE,YAAM,WAAW,MAAM,OACpB,OAAO,EACP,KAAK,OAAO,EACZ;AAAA,QACC;AAAA,UACE,GAAG,QAAQ,SAAS,IAAI;AAAA,UACxB,GAAG,QAAQ,WAAWA,UAAS;AAAA,UAC/B,GAAG,QAAQ,WAAW,WAAW;AAAA;AAAA,UAEjC,YAAY,wBAAwB;AAAA,QACtC;AAAA,MACF,EACC,QAAQ,KAAK,QAAQ,SAAS,CAAC,EAC/B,MAAM,CAAC;AACV,UAAI,SAAS,SAAS,GAAG;AACvB,eAAO,SAAS,CAAC;AAAA,MACnB;AAAA,IACF;AAGA,QAAI,iBAAgC;AACpC,QAAI,WAAW,wBAAwB;AACrC,uBAAiB;AAAA,QACf,WAAW;AAAA,QACX;AAAA,MACF;AACA,YAAM,WAAW,MAAM,OACpB,OAAO,EACP,KAAK,OAAO,EACZ;AAAA,QACC;AAAA,UACE,GAAG,QAAQ,gBAAgB,cAAc;AAAA,UACzC,QAAQ,QAAQ,QAAQ,kBAAkB;AAAA,QAC5C;AAAA,MACF,EACC,MAAM,CAAC;AACV,UAAI,SAAS,SAAS,GAAG;AACvB,cAAM,YAAY,SAAS,CAAC;AAC5B,gBAAQ,WAAW,eAAe;AAAA,UAChC,KAAK;AACH,kBAAM,IAAI,kBAAkB,MAAM,gBAAgB,SAAS;AAAA,UAC7D,KAAK;AAOH,kBAAM,KAAK,OAAO,UAAU,IAAI;AAAA,cAC9B,SAAS;AAAA,cACT,QAAQ;AAAA,cACR,UAAU,UAAU;AAAA,YACtB,CAAC;AACD;AAAA,UACF,KAAK;AAGH;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAGA,UAAM,QAAQ,WAAW;AACzB,QAAI,YAAoB;AACxB,QAAI,KAAK,aAAa;AACpB,YAAM,CAAC,MAAM,IAAI,MAAM,OACpB,OAAO,EAAE,WAAW,QAAQ,UAAU,CAAC,EACvC,KAAK,OAAO,EACZ,MAAM,GAAG,QAAQ,IAAI,KAAK,WAAW,CAAC,EACtC,MAAM,CAAC;AACV,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI;AAAA,UACR,eAAe,KAAK,WAAW;AAAA,QACjC;AAAA,MACF;AACA,kBAAY,OAAO;AAAA,IACrB;AAEA,UAAM,YACJ,WAAW,oBACP,oBAAoB,WAAW,mBAAmB,OAAO,IACzD;AAEN,UAAM,CAAC,QAAQ,IAAI,MAAM,OACtB,OAAO,OAAO,EACd,OAAO;AAAA,MACN,IAAI;AAAA,MACJ,SAAS;AAAA,MACT,YAAY,WAAW;AAAA,MACvB,aAAa,KAAK,eAAe;AAAA,MACjC;AAAA,MACA,mBAAmB,KAAK,qBAAqB;AAAA,MAC7C,iBAAiB,KAAK,OAAO,cAAc;AAAA,MAC3C,eAAe,KAAK,OAAO,YAAY;AAAA,MACvC;AAAA,MACA,MAAM,KAAK,QAAQ,CAAC;AAAA,MACpB,MAAM,KAAK,QAAQ,WAAW;AAAA,MAC9B,UAAU,KAAK,YAAY,WAAW;AAAA,MACtC;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,eAAe,KAAK,iBAAiB;AAAA,MACrC,YAAY,KAAK,cAAc;AAAA,MAC/B,OAAO,KAAK,SAAS,oBAAI,KAAK;AAAA,MAC9B,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,UAAU;AAAA,IACZ,CAAC,EACA,UAAU;AAQb,QAAI,KAAK,cAAc;AACrB,YAAM,WAAY,SAAuB;AACzC,UAAI;AACF,cAAM,SAAS,QAAQ,mBAAmB,QAAQ;AAAA,MACpD,SAAS,KAAK;AACZ,aAAK,OAAO;AAAA,UACV,aAAa,iBAAiB,KAAK,QAAQ,oBACrC,SAAuB,EAAE,KAAM,IAAc,OAAO;AAAA,QAE5D;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,OAAe,OAAsB,CAAC,GAAkB;AAEnE,UAAM,WAAW,KAAK,gBAAgB,UAAU,KAAK,QAAQ;AAG7D,UAAM,CAAC,MAAM,IAAI,MAAM,KAAK,GACzB,OAAO,EACP,KAAK,OAAO,EACZ,MAAM,GAAG,QAAQ,IAAI,KAAK,CAAC,EAC3B,MAAM,CAAC;AACV,QAAI,CAAC,OAAQ;AAEb,QAAI,KAAK,eAAe,OAAO,aAAa,SAAU;AACtD,QAAI,kBAAkB,SAAS,OAAO,MAAwB,GAAG;AAC/D;AAAA,IACF;AAGA,UAAM,CAAC,SAAS,IAAI,MAAM,KAAK,GAC5B,OAAO,OAAO,EACd,IAAI;AAAA,MACH,QAAQ;AAAA,MACR,YAAY,oBAAI,KAAK;AAAA,MACrB,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC,EACA;AAAA,MACC,IAAI,GAAG,QAAQ,IAAI,KAAK,GAAG,YAAY,CAAC,GAAG,iBAAiB,CAAC,CAAC;AAAA,IAChE,EACC,UAAU;AAEb,QAAI,CAAC,UAAW;AAEhB,QAAI,KAAK,YAAY,MAAO;AAG5B,UAAM,cAAc,MAAM,KAAK,GAC5B,OAAO,EACP,KAAK,OAAO,EACZ;AAAA,MACC;AAAA,QACE,GAAG,QAAQ,WAAW,OAAO,SAAS;AAAA,QACtC,GAAG,QAAQ,IAAI,KAAK;AAAA,QACpB,YAAY,CAAC,GAAG,iBAAiB,CAAC;AAAA,MACpC;AAAA,IACF;AAEF,eAAW,SAAS,aAAa;AAC/B,YAAM,SAAU,MAAoB;AACpC,UAAI,WAAW,UAAW;AAE1B,YAAM,KAAK,GACR,OAAO,OAAO,EACd,IAAI;AAAA,QACH,QAAQ;AAAA,QACR,YAAY,oBAAI,KAAK;AAAA,QACrB,WAAW,oBAAI,KAAK;AAAA,MACtB,CAAC,EACA;AAAA,QACC;AAAA,UACE,GAAG,QAAQ,IAAK,MAAoB,EAAE;AAAA,UACtC,YAAY,CAAC,GAAG,iBAAiB,CAAC;AAAA,QACpC;AAAA,MACF;AAAA,IACJ;AAEA,SAAK,KAAK;AAAA,EACZ;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,OAAgC;AAE3C,UAAM,CAAC,MAAM,IAAI,MAAM,KAAK,GACzB,OAAO,EACP,KAAK,OAAO,EACZ,MAAM,GAAG,QAAQ,IAAI,KAAK,CAAC,EAC3B,MAAM,CAAC;AACV,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,eAAe,KAAK,YAAY;AAAA,IAClD;AACA,UAAM,MAAM;AACZ,QAAI,CAAC,kBAAkB,SAAS,IAAI,MAAwB,GAAG;AAC7D,YAAM,IAAI,sBAAsB,OAAO,IAAI,MAAM;AAAA,IACnD;AAEA,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,GACtB,OAAO,EACP,KAAK,IAAI,EACT,MAAM,GAAG,KAAK,MAAM,IAAI,OAAO,CAAC,EAChC,MAAM,CAAC;AACV,QAAI,CAAC,IAAK,OAAM,IAAI,qBAAqB,IAAI,OAAO;AACpD,UAAM,OAAQ,IAAyB;AAGvC,UAAM,SAAS,MAAM,KAAK,GAAG,YAAY,OAAO,OAAO;AACrD,UAAI,SAAS,WAAW;AACtB,cAAM,GAAG,OAAO,QAAQ,EAAE,MAAM,GAAG,SAAS,UAAU,KAAK,CAAC;AAAA,MAC9D,WAAW,SAAS,aAAa;AAE/B,cAAM,GACH,OAAO,QAAQ,EACf;AAAA,UACC,IAAI,GAAG,SAAS,UAAU,KAAK,GAAG,GAAG,SAAS,QAAQ,WAAW,CAAC;AAAA,QACpE;AAAA,MACJ,OAAO;AAIL,cAAM,GACH,OAAO,QAAQ,EACf;AAAA,UACC,IAAI,GAAG,SAAS,UAAU,KAAK,GAAG,GAAG,SAAS,QAAQ,WAAW,CAAC;AAAA,QACpE;AAAA,MACJ;AAEA,YAAM,CAAC,OAAO,IAAI,MAAM,GACrB,OAAO,OAAO,EACd,IAAI;AAAA,QACH,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,OAAO,oBAAI,KAAK;AAAA,QAChB,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,WAAW,oBAAI,KAAK;AAAA,MACtB,CAAC,EACA,MAAM,GAAG,QAAQ,IAAI,KAAK,CAAC,EAC3B,UAAU;AACb,aAAO;AAAA,IACT,CAAC;AAED,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBA,MAAM,cACJ,SACA,YACiC;AACjC,SAAK;AAEL,eAAW,SAAS,SAAS;AAC3B,YAAM,OAAO,MAAM;AACnB,YAAM,OAAO,KAAK,QAAQ;AAC1B,YAAM,cAAc,KAAK,SAAS;AAAA,QAChC,UAAU;AAAA,QACV,SAAS;AAAA,QACT,QAAQ;AAAA,MACV;AACA,YAAM,yBACH,KAAK,aAA+C;AACvD,YAAM,4BACJ,OAAO,2BAA2B,WAAW,yBAAyB;AACxE,YAAM,gBACH,KAAK,aAAa,iBACnB;AACF,YAAM,oBACH,KAAK,QAA0C;AAClD,YAAM,uBACJ,OAAO,sBAAsB,WAAW,oBAAoB;AAC9D,YAAM,iBAAiB,KAAK,QAAQ,YAAY;AAChD,YAAM,YAAY,KAAK,aAAa;AACpC,YAAM,aAAa,KAAK,cAAc;AACtC,YAAM,kBAAkB,KAAK,OAAO,UAAU;AAK9C,YAAM,kBAAkB;AAKxB,YAAM,KAAK,GACR,OAAO,IAAI,EACX,OAAO;AAAA,QACN,MAAM,MAAM;AAAA,QACZ,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,wBAAwB;AAAA,QACxB;AAAA,QACA,mBAAmB;AAAA,QACnB;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC,EACA,mBAAmB;AAAA,QAClB,QAAQ,KAAK;AAAA,QACb,KAAK;AAAA,UACH,MAAM;AAAA,UACN,iBAAiB;AAAA,UACjB,aAAa;AAAA,UACb,WAAW;AAAA,UACX,wBAAwB;AAAA,UACxB,eAAe;AAAA,UACf,mBAAmB;AAAA,UACnB,gBAAgB;AAAA,UAChB,iBAAiB;AAAA,UACjB,YAAY;AAAA,UACZ,SAAS,MAAM,KAAK,OAAO;AAAA,UAC3B,WAAW;AAAA,QACb;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA,UAAU;AAAA,cACN,KAAK,IAAI;AAAA,cACT,KAAK,WAAW;AAAA,cAChB,KAAK,SAAS;AAAA,cACd,KAAK,sBAAsB;AAAA,cAC3B,KAAK,aAAa;AAAA,cAClB,KAAK,iBAAiB;AAAA,cACtB,KAAK,cAAc;AAAA,cACnB,KAAK,eAAe;AAAA,cACpB,KAAK,UAAU;AAAA,cACf,KAAK,eAAe;AAAA;AAAA,MAE1B,CAAC;AAAA,IACL;AAGA,UAAM,QAAQ,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI;AACvC,UAAM,UACJ,MAAM,WAAW,IACb,MAAM,KAAK,GAAG,OAAO,EAAE,MAAM,KAAK,KAAK,CAAC,EAAE,KAAK,IAAI,IACnD,MAAM,KAAK,GACR,OAAO,EAAE,MAAM,KAAK,KAAK,CAAC,EAC1B,KAAK,IAAI,EACT,MAAM,WAAW,KAAK,MAAM,KAAK,CAAC;AAE3C,WAAO,EAAE,UAAU,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE;AAAA,EAChD;AACF;AAtda,yBAAN;AAAA,EADN,WAAW;AAAA,EAMP,0BAAO,OAAO;AAAA,EACd,0BAAO,iBAAiB;AAAA,EAKxB,4BAAS;AAAA,EACT,0BAAO,kBAAkB;AAAA,GAZjB;AA0db,SAAS,YAAY,UAA0B;AAG7C,QAAM,UAAU,SAAS,IAAI,CAAC,MAAM,GAAG,QAAQ,QAAQ,CAAC,CAAC;AACzD,SAAO,IAAI,GAAG,OAAO;AACvB;","names":["dedupeKey"]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  JobWorker
3
- } from "./chunk-RC23QROE.js";
3
+ } from "./chunk-XDIIVIIK.js";
4
4
  import {
5
5
  JobsDomainModule
6
- } from "./chunk-KMZCQASO.js";
6
+ } from "./chunk-3RWMQC3K.js";
7
7
  import {
8
8
  BootValidationError,
9
9
  ReservedPoolViolationError
@@ -18,14 +18,14 @@ import {
18
18
  allPoolNames,
19
19
  loadPoolConfig
20
20
  } from "./chunk-RHVN6NA7.js";
21
- import {
22
- HandlerRegistry
23
- } from "./chunk-CO6LUM72.js";
24
21
  import {
25
22
  JOB_ORCHESTRATOR,
26
23
  JOB_RUN_SERVICE,
27
24
  JOB_STEP_SERVICE
28
- } from "./chunk-BIO6F7YI.js";
25
+ } from "./chunk-ZPL74UQN.js";
26
+ import {
27
+ HandlerRegistry
28
+ } from "./chunk-CO6LUM72.js";
29
29
  import {
30
30
  tokenKey
31
31
  } from "./chunk-GYGNEQSC.js";
@@ -92,10 +92,13 @@ var JobWorkerOrchestrator = class {
92
92
  `JobWorkerModule: active pool '${poolName}' is not defined in the resolved pool config. Configured pools: [${[...poolConfig.keys()].join(", ")}].`
93
93
  );
94
94
  }
95
+ const drizzleExt = backend === "drizzle" ? this.options.domainModuleExtensions?.drizzle : void 0;
95
96
  const workerOptions = {
96
97
  pool: poolName,
97
98
  concurrency: def.concurrency,
98
- shutdownTimeoutMs: this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS
99
+ shutdownTimeoutMs: this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
100
+ pollIntervalMs: drizzleExt?.pollIntervalMs,
101
+ listenNotify: drizzleExt?.listenNotify
99
102
  };
100
103
  const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : backend === "bullmq" ? await this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig) : this.spawnWorker(workerOptions);
101
104
  await worker.onModuleInit();
@@ -287,4 +290,4 @@ export {
287
290
  JobWorkerOrchestrator,
288
291
  JobWorkerModule
289
292
  };
290
- //# sourceMappingURL=chunk-WPXNN6QS.js.map
293
+ //# sourceMappingURL=chunk-RUYLXR5F.js.map