@pattern-stack/codegen 0.17.1 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/{chunk-HPS554L4.js → chunk-235ZMMJR.js} +6 -6
  3. package/dist/{chunk-BK5ICA2F.js → chunk-4MVGAMUA.js} +4 -4
  4. package/dist/{chunk-SFQRETXJ.js → chunk-65MO75WM.js} +9 -9
  5. package/dist/{chunk-5RT7JGKT.js → chunk-7OVCARTQ.js} +4 -4
  6. package/dist/{chunk-JA7GJDNI.js → chunk-ATVGYF3D.js} +7 -7
  7. package/dist/{chunk-HOIRY5XP.js → chunk-AZLUWG5S.js} +9 -9
  8. package/dist/{chunk-4PFF3ED4.js → chunk-B34G6PHD.js} +10 -10
  9. package/dist/{chunk-W2UIDI3R.js → chunk-CLWBNXKF.js} +4 -4
  10. package/dist/{chunk-FVNAU7VO.js → chunk-E6PLM6QG.js} +8 -8
  11. package/dist/{chunk-43SBT72G.js → chunk-I6UXRJ3Q.js} +4 -4
  12. package/dist/{chunk-PSDVGPQR.js → chunk-KZDHMZ45.js} +5 -5
  13. package/dist/{chunk-EJBK7I4F.js → chunk-OZEPJGMA.js} +3 -3
  14. package/dist/{chunk-MYQIQ27N.js → chunk-Q6LRJ4VI.js} +51 -2
  15. package/dist/chunk-Q6LRJ4VI.js.map +1 -0
  16. package/dist/{chunk-SGSWVNNB.js → chunk-R6F6KFIL.js} +7 -7
  17. package/dist/{chunk-FWRL7BZ5.js → chunk-VDL5CJ5C.js} +26 -16
  18. package/dist/chunk-VDL5CJ5C.js.map +1 -0
  19. package/dist/{chunk-DUMI2J5M.js → chunk-VQOAATIG.js} +4 -4
  20. package/dist/{chunk-E45CSC33.js → chunk-XKWOJZZ4.js} +2 -2
  21. package/dist/{chunk-LQ6PYFU6.js → chunk-Z7PQCAVK.js} +4 -4
  22. package/dist/runtime/base-classes/index.js +24 -24
  23. package/dist/runtime/shared/openapi/index.js +3 -3
  24. package/dist/runtime/subsystems/auth/auth.module.js +1 -1
  25. package/dist/runtime/subsystems/auth/index.js +8 -8
  26. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +3 -3
  27. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +1 -1
  28. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +6 -6
  29. package/dist/runtime/subsystems/bridge/bridge.module.js +15 -15
  30. package/dist/runtime/subsystems/bridge/event-flow.service.js +2 -2
  31. package/dist/runtime/subsystems/bridge/index.js +15 -15
  32. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +4 -4
  33. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +2 -2
  34. package/dist/runtime/subsystems/events/events.module.js +6 -6
  35. package/dist/runtime/subsystems/events/index.js +9 -9
  36. package/dist/runtime/subsystems/index.js +70 -70
  37. package/dist/runtime/subsystems/integration/build-change-source.js +2 -2
  38. package/dist/runtime/subsystems/integration/index.js +30 -30
  39. package/dist/runtime/subsystems/integration/integration.module.js +4 -4
  40. package/dist/runtime/subsystems/jobs/index.js +23 -23
  41. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +3 -3
  42. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +3 -3
  43. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
  44. package/dist/runtime/subsystems/jobs/job-worker.d.ts +8 -0
  45. package/dist/runtime/subsystems/jobs/job-worker.js +3 -3
  46. package/dist/runtime/subsystems/jobs/job-worker.module.js +9 -9
  47. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +7 -7
  48. package/dist/runtime/subsystems/jobs/pg-notify.d.ts +25 -1
  49. package/dist/runtime/subsystems/jobs/pg-notify.js +1 -1
  50. package/dist/runtime/subsystems/observability/index.js +3 -3
  51. package/dist/runtime/subsystems/observability/observability.module.js +3 -3
  52. package/dist/runtime/subsystems/observability/observability.service.js +2 -2
  53. package/dist/runtime/subsystems/storage/index.js +4 -4
  54. package/dist/runtime/subsystems/storage/storage.module.js +2 -2
  55. package/dist/src/cli/index.js +11 -11
  56. package/dist/src/index.js +9 -9
  57. package/package.json +1 -1
  58. package/runtime/subsystems/jobs/job-worker.ts +29 -11
  59. package/runtime/subsystems/jobs/pg-notify.ts +63 -3
  60. package/dist/chunk-FWRL7BZ5.js.map +0 -1
  61. package/dist/chunk-MYQIQ27N.js.map +0 -1
  62. /package/dist/{chunk-HPS554L4.js.map → chunk-235ZMMJR.js.map} +0 -0
  63. /package/dist/{chunk-BK5ICA2F.js.map → chunk-4MVGAMUA.js.map} +0 -0
  64. /package/dist/{chunk-SFQRETXJ.js.map → chunk-65MO75WM.js.map} +0 -0
  65. /package/dist/{chunk-5RT7JGKT.js.map → chunk-7OVCARTQ.js.map} +0 -0
  66. /package/dist/{chunk-JA7GJDNI.js.map → chunk-ATVGYF3D.js.map} +0 -0
  67. /package/dist/{chunk-HOIRY5XP.js.map → chunk-AZLUWG5S.js.map} +0 -0
  68. /package/dist/{chunk-4PFF3ED4.js.map → chunk-B34G6PHD.js.map} +0 -0
  69. /package/dist/{chunk-W2UIDI3R.js.map → chunk-CLWBNXKF.js.map} +0 -0
  70. /package/dist/{chunk-FVNAU7VO.js.map → chunk-E6PLM6QG.js.map} +0 -0
  71. /package/dist/{chunk-43SBT72G.js.map → chunk-I6UXRJ3Q.js.map} +0 -0
  72. /package/dist/{chunk-PSDVGPQR.js.map → chunk-KZDHMZ45.js.map} +0 -0
  73. /package/dist/{chunk-EJBK7I4F.js.map → chunk-OZEPJGMA.js.map} +0 -0
  74. /package/dist/{chunk-SGSWVNNB.js.map → chunk-R6F6KFIL.js.map} +0 -0
  75. /package/dist/{chunk-DUMI2J5M.js.map → chunk-VQOAATIG.js.map} +0 -0
  76. /package/dist/{chunk-E45CSC33.js.map → chunk-XKWOJZZ4.js.map} +0 -0
  77. /package/dist/{chunk-LQ6PYFU6.js.map → chunk-Z7PQCAVK.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.17.2] — 2026-06-04
8
+
9
+ **Shutdown leak fix** (LISTEN-NOTIFY-2; swe-brain dogfood). With
10
+ `listen_notify: true` (the LISTEN/NOTIFY wake extension shipped in 0.16.0), a
11
+ Nest app that booted and then `app.close()`d — e.g. a boot-check / CI smoke step —
12
+ never exited: at least one `LISTEN codegen_jobs_wake` client survived
13
+ `app.close()`, holding an ESTABLISHED Postgres socket open forever (two swe-brain
14
+ CI runs hung for hours). Backward-compatible; affects only consumers that opted
15
+ into `listen_notify`.
16
+
17
+ ### Fixed
18
+
19
+ - **`PgNotifyListener.stop()` is race-safe against an in-flight `connect()`**
20
+ (LISTEN-NOTIFY-2 RC1 — the defect that actually fired). `connect()` checked
21
+ `this.stopped` only at entry, then `await pool.connect()`, wired handlers,
22
+ issued `LISTEN`, and assigned `this.client` last. A `stop()` arriving during
23
+ the checkout await ran `releaseClient()` against a still-null `this.client`
24
+ (released nothing); the resuming `connect()` then assigned the client and
25
+ issued `LISTEN` — leaking a checked-out connection with no owner left to
26
+ release it. With 5–6 listeners (one per jobs pool + the events drainer) all
27
+ starting at bootstrap and a tight `app.close()`, the race fired on ~1 of 6
28
+ listeners — exactly the observed signature (one survivor, the rest clean).
29
+ Now `connect()` re-checks `stopped` after the checkout AND after `LISTEN`,
30
+ destroying the just-acquired client and bailing before assignment; `stop()`
31
+ tracks and awaits the in-flight connect promise before its own release, so
32
+ `app.close()` can't return while a checkout is still mid-flight. Releases use
33
+ `release(true)` (destroy) so a half-listening socket is never reused.
34
+ - **`JobWorker.onModuleDestroy` stops the wake listener on EVERY destroy path**
35
+ (LISTEN-NOTIFY-2 RC2 — latent). The listener `stop()` lived only on the first
36
+ (non-`shuttingDown`) branch, so a SIGTERM-then-Nest double-destroy hit the
37
+ `if (this.shuttingDown) { …; return; }` early return and skipped it, leaking
38
+ the listener under the normal SIGTERM shutdown path. Teardown is now an
39
+ idempotent `stopNotifyListener()` called unconditionally at the top of every
40
+ destroy. `DrizzleEventBus` already stopped its listener unconditionally; it
41
+ shared `PgNotifyListener` and so benefits from the RC1 fix directly.
42
+
7
43
  ## [0.17.1] — 2026-06-04
8
44
 
9
45
  **Two dogfood fixes that bit the same swe-brain mutation drain** (ADR-0009
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  JobWorker
3
- } from "./chunk-FWRL7BZ5.js";
3
+ } from "./chunk-VDL5CJ5C.js";
4
4
  import {
5
5
  JobsDomainModule
6
- } from "./chunk-HOIRY5XP.js";
6
+ } from "./chunk-AZLUWG5S.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-7P5ODGLA.js";
21
24
  import {
22
25
  JOB_ORCHESTRATOR,
23
26
  JOB_RUN_SERVICE,
24
27
  JOB_STEP_SERVICE
25
28
  } from "./chunk-ZPL74UQN.js";
26
- import {
27
- HandlerRegistry
28
- } from "./chunk-7P5ODGLA.js";
29
29
  import {
30
30
  tokenKey
31
31
  } from "./chunk-GYGNEQSC.js";
@@ -290,4 +290,4 @@ export {
290
290
  JobWorkerOrchestrator,
291
291
  JobWorkerModule
292
292
  };
293
- //# sourceMappingURL=chunk-HPS554L4.js.map
293
+ //# sourceMappingURL=chunk-235ZMMJR.js.map
@@ -1,12 +1,12 @@
1
- import {
2
- STORAGE
3
- } from "./chunk-NYBCQZC7.js";
4
1
  import {
5
2
  LocalStorageBackend
6
3
  } from "./chunk-JWNHNUYL.js";
7
4
  import {
8
5
  MemoryStorageBackend
9
6
  } from "./chunk-3SZFUTXE.js";
7
+ import {
8
+ STORAGE
9
+ } from "./chunk-NYBCQZC7.js";
10
10
  import {
11
11
  __decorateClass
12
12
  } from "./chunk-2E224ZSN.js";
@@ -37,4 +37,4 @@ StorageModule = __decorateClass([
37
37
  export {
38
38
  StorageModule
39
39
  };
40
- //# sourceMappingURL=chunk-BK5ICA2F.js.map
40
+ //# sourceMappingURL=chunk-4MVGAMUA.js.map
@@ -1,22 +1,22 @@
1
1
  import {
2
2
  BRIDGE_DELIVERY_JOB_TYPE
3
- } from "./chunk-SGSWVNNB.js";
3
+ } from "./chunk-R6F6KFIL.js";
4
4
  import {
5
5
  bridgeDelivery
6
6
  } from "./chunk-2TVVBC53.js";
7
- import {
8
- JOBS_LISTEN_NOTIFY
9
- } from "./chunk-ZPL74UQN.js";
10
- import {
11
- jobRuns
12
- } from "./chunk-OKXZ63IA.js";
13
7
  import {
14
8
  JOBS_WAKE_CHANNEL,
15
9
  pgNotify
16
- } from "./chunk-MYQIQ27N.js";
10
+ } from "./chunk-Q6LRJ4VI.js";
11
+ import {
12
+ JOBS_LISTEN_NOTIFY
13
+ } from "./chunk-ZPL74UQN.js";
17
14
  import {
18
15
  BRIDGE_REGISTRY
19
16
  } from "./chunk-4LH67P4U.js";
17
+ import {
18
+ jobRuns
19
+ } from "./chunk-OKXZ63IA.js";
20
20
  import {
21
21
  __decorateClass,
22
22
  __decorateParam
@@ -151,4 +151,4 @@ BridgeOutboxDrainHook = __decorateClass([
151
151
  export {
152
152
  BridgeOutboxDrainHook
153
153
  };
154
- //# sourceMappingURL=chunk-SFQRETXJ.js.map
154
+ //# sourceMappingURL=chunk-65MO75WM.js.map
@@ -4,14 +4,14 @@ import {
4
4
  import {
5
5
  JOB_ORCHESTRATOR
6
6
  } from "./chunk-ZPL74UQN.js";
7
+ import {
8
+ EVENT_BUS
9
+ } from "./chunk-H5NH7KPE.js";
7
10
  import {
8
11
  BRIDGE_DELIVERY_REPO,
9
12
  BRIDGE_MULTI_TENANT,
10
13
  BRIDGE_REGISTRY
11
14
  } from "./chunk-4LH67P4U.js";
12
- import {
13
- EVENT_BUS
14
- } from "./chunk-H5NH7KPE.js";
15
15
  import {
16
16
  DRIZZLE
17
17
  } from "./chunk-U64T4YZE.js";
@@ -106,4 +106,4 @@ EventFlowService = __decorateClass([
106
106
  export {
107
107
  EventFlowService
108
108
  };
109
- //# sourceMappingURL=chunk-5RT7JGKT.js.map
109
+ //# sourceMappingURL=chunk-7OVCARTQ.js.map
@@ -1,6 +1,3 @@
1
- import {
2
- MemoryCursorStore
3
- } from "./chunk-AHV4GDYM.js";
4
1
  import {
5
2
  DrizzleIntegrationRunRecorder
6
3
  } from "./chunk-YK5JEVLX.js";
@@ -10,6 +7,12 @@ import {
10
7
  import {
11
8
  PostgresCursorStore
12
9
  } from "./chunk-XWBK3XJK.js";
10
+ import {
11
+ MemoryCursorStore
12
+ } from "./chunk-AHV4GDYM.js";
13
+ import {
14
+ DeepEqualDiffer
15
+ } from "./chunk-JEINYUJH.js";
13
16
  import {
14
17
  INTEGRATION_CURSOR_STORE,
15
18
  INTEGRATION_FIELD_DIFFER,
@@ -17,9 +20,6 @@ import {
17
20
  INTEGRATION_MULTI_TENANT,
18
21
  INTEGRATION_RUN_RECORDER
19
22
  } from "./chunk-S7C6TIIF.js";
20
- import {
21
- DeepEqualDiffer
22
- } from "./chunk-JEINYUJH.js";
23
23
  import {
24
24
  __decorateClass
25
25
  } from "./chunk-2E224ZSN.js";
@@ -84,4 +84,4 @@ IntegrationModule = __decorateClass([
84
84
  export {
85
85
  IntegrationModule
86
86
  };
87
- //# sourceMappingURL=chunk-JA7GJDNI.js.map
87
+ //# sourceMappingURL=chunk-ATVGYF3D.js.map
@@ -1,21 +1,21 @@
1
+ import {
2
+ DrizzleJobRunService
3
+ } from "./chunk-VNBC3VXM.js";
4
+ import {
5
+ MemoryJobRunService
6
+ } from "./chunk-BHZP6LOV.js";
1
7
  import {
2
8
  DrizzleJobStepService
3
9
  } from "./chunk-DV4RV2DC.js";
4
10
  import {
5
11
  DrizzleJobOrchestrator
6
- } from "./chunk-FVNAU7VO.js";
12
+ } from "./chunk-E6PLM6QG.js";
7
13
  import {
8
14
  MemoryJobOrchestrator
9
- } from "./chunk-DUMI2J5M.js";
15
+ } from "./chunk-VQOAATIG.js";
10
16
  import {
11
17
  MemoryJobStepService
12
18
  } from "./chunk-PNZSGAB2.js";
13
- import {
14
- DrizzleJobRunService
15
- } from "./chunk-VNBC3VXM.js";
16
- import {
17
- MemoryJobRunService
18
- } from "./chunk-BHZP6LOV.js";
19
19
  import {
20
20
  MemoryJobStore
21
21
  } from "./chunk-SNQ3TOWP.js";
@@ -114,4 +114,4 @@ JobsDomainModule = __decorateClass([
114
114
  export {
115
115
  JobsDomainModule
116
116
  };
117
- //# sourceMappingURL=chunk-HOIRY5XP.js.map
117
+ //# sourceMappingURL=chunk-AZLUWG5S.js.map
@@ -1,22 +1,22 @@
1
+ import {
2
+ clampEventLimit,
3
+ decodeEventCursor,
4
+ encodeEventCursor
5
+ } from "./chunk-UQ5EHOH2.js";
1
6
  import {
2
7
  EVENTS_WAKE_CHANNEL,
3
8
  PgNotifyListener,
4
9
  pgNotify
5
- } from "./chunk-MYQIQ27N.js";
10
+ } from "./chunk-Q6LRJ4VI.js";
11
+ import {
12
+ EVENTS_MODULE_OPTIONS
13
+ } from "./chunk-H5NH7KPE.js";
6
14
  import {
7
15
  BRIDGE_OUTBOX_DRAIN_HOOK
8
16
  } from "./chunk-4LH67P4U.js";
9
17
  import {
10
18
  domainEvents
11
19
  } from "./chunk-OFRRBC7M.js";
12
- import {
13
- EVENTS_MODULE_OPTIONS
14
- } from "./chunk-H5NH7KPE.js";
15
- import {
16
- clampEventLimit,
17
- decodeEventCursor,
18
- encodeEventCursor
19
- } from "./chunk-UQ5EHOH2.js";
20
20
  import {
21
21
  DRIZZLE
22
22
  } from "./chunk-U64T4YZE.js";
@@ -393,4 +393,4 @@ DrizzleEventBus = __decorateClass([
393
393
  export {
394
394
  DrizzleEventBus
395
395
  };
396
- //# sourceMappingURL=chunk-4PFF3ED4.js.map
396
+ //# sourceMappingURL=chunk-B34G6PHD.js.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  JOB_RUN_SERVICE
3
3
  } from "./chunk-ZPL74UQN.js";
4
- import {
5
- BRIDGE_DELIVERY_REPO
6
- } from "./chunk-4LH67P4U.js";
7
4
  import {
8
5
  EVENT_READ_PORT
9
6
  } from "./chunk-H5NH7KPE.js";
7
+ import {
8
+ BRIDGE_DELIVERY_REPO
9
+ } from "./chunk-4LH67P4U.js";
10
10
  import {
11
11
  INTEGRATION_CURSOR_STORE,
12
12
  INTEGRATION_RUN_RECORDER
@@ -181,4 +181,4 @@ ObservabilityService = __decorateClass([
181
181
  export {
182
182
  ObservabilityService
183
183
  };
184
- //# sourceMappingURL=chunk-W2UIDI3R.js.map
184
+ //# sourceMappingURL=chunk-CLWBNXKF.js.map
@@ -6,22 +6,22 @@ import {
6
6
  MissingTenantIdError
7
7
  } from "./chunk-T4BIIU5E.js";
8
8
  import {
9
- JOBS_LISTEN_NOTIFY,
10
- JOBS_MULTI_TENANT
11
- } from "./chunk-ZPL74UQN.js";
9
+ JOBS_WAKE_CHANNEL,
10
+ pgNotify
11
+ } from "./chunk-Q6LRJ4VI.js";
12
12
  import {
13
13
  keySelectorToTemplate,
14
14
  resolveJobKey
15
15
  } from "./chunk-7P5ODGLA.js";
16
+ import {
17
+ JOBS_LISTEN_NOTIFY,
18
+ JOBS_MULTI_TENANT
19
+ } from "./chunk-ZPL74UQN.js";
16
20
  import {
17
21
  jobRuns,
18
22
  jobSteps,
19
23
  jobs
20
24
  } from "./chunk-OKXZ63IA.js";
21
- import {
22
- JOBS_WAKE_CHANNEL,
23
- pgNotify
24
- } from "./chunk-MYQIQ27N.js";
25
25
  import {
26
26
  DRIZZLE
27
27
  } from "./chunk-U64T4YZE.js";
@@ -393,4 +393,4 @@ export {
393
393
  evaluateKeyTemplate,
394
394
  DrizzleJobOrchestrator
395
395
  };
396
- //# sourceMappingURL=chunk-FVNAU7VO.js.map
396
+ //# sourceMappingURL=chunk-E6PLM6QG.js.map
@@ -1,9 +1,9 @@
1
- import {
2
- PollChangeSource
3
- } from "./chunk-4MF3HKJA.js";
4
1
  import {
5
2
  WebhookChangeSource
6
3
  } from "./chunk-TIZXQU26.js";
4
+ import {
5
+ PollChangeSource
6
+ } from "./chunk-4MF3HKJA.js";
7
7
 
8
8
  // runtime/subsystems/integration/build-change-source.ts
9
9
  function buildChangeSource(cfg, fetch, middlewares = []) {
@@ -26,4 +26,4 @@ function buildChangeSource(cfg, fetch, middlewares = []) {
26
26
  export {
27
27
  buildChangeSource
28
28
  };
29
- //# sourceMappingURL=chunk-43SBT72G.js.map
29
+ //# sourceMappingURL=chunk-I6UXRJ3Q.js.map
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-5A432NZJ.js";
4
4
  import {
5
5
  EventFlowService
6
- } from "./chunk-5RT7JGKT.js";
6
+ } from "./chunk-7OVCARTQ.js";
7
7
  import {
8
8
  BRIDGE_RESERVED_POOLS
9
9
  } from "./chunk-EDKJU5BO.js";
@@ -15,16 +15,16 @@ import {
15
15
  } from "./chunk-4DOJBQTP.js";
16
16
  import {
17
17
  BridgeOutboxDrainHook
18
- } from "./chunk-SFQRETXJ.js";
18
+ } from "./chunk-65MO75WM.js";
19
19
  import {
20
20
  BridgeDeliveryHandler
21
- } from "./chunk-SGSWVNNB.js";
21
+ } from "./chunk-R6F6KFIL.js";
22
22
  import {
23
23
  BridgeReservedPoolsNotPolledError
24
24
  } from "./chunk-NXXDZ6ZF.js";
25
25
  import {
26
26
  JOB_WORKER_MODULE_OPTIONS
27
- } from "./chunk-HPS554L4.js";
27
+ } from "./chunk-235ZMMJR.js";
28
28
  import {
29
29
  BRIDGE_DELIVERY_REPO,
30
30
  BRIDGE_MODULE_OPTIONS,
@@ -119,4 +119,4 @@ BridgeModule = __decorateClass([
119
119
  export {
120
120
  BridgeModule
121
121
  };
122
- //# sourceMappingURL=chunk-PSDVGPQR.js.map
122
+ //# sourceMappingURL=chunk-KZDHMZ45.js.map
@@ -3,10 +3,10 @@ import {
3
3
  } from "./chunk-GM3RMJIJ.js";
4
4
  import {
5
5
  DrizzleEventBus
6
- } from "./chunk-4PFF3ED4.js";
6
+ } from "./chunk-B34G6PHD.js";
7
7
  import {
8
8
  MemoryEventBus
9
- } from "./chunk-LQ6PYFU6.js";
9
+ } from "./chunk-Z7PQCAVK.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-EJBK7I4F.js.map
155
+ //# sourceMappingURL=chunk-OZEPJGMA.js.map
@@ -27,24 +27,60 @@ var PgNotifyListener = class {
27
27
  backoffMaxMs;
28
28
  /** WARN-once gate so a flapping listener doesn't spam the log. */
29
29
  warnedDown = false;
30
+ /**
31
+ * LISTEN-NOTIFY-2 — the in-flight `connect()` promise, set while a checkout is
32
+ * mid-`await`. `stop()` awaits it so a `stop()` that races a still-resolving
33
+ * `connect()` can't return before the connect either assigns `this.client`
34
+ * (then released by `releaseClient`) or self-releases the checked-out client.
35
+ * Without this, a `stop()` arriving during `pool.connect()`'s await saw
36
+ * `this.client === null` (nothing to release), then `connect()` resumed,
37
+ * assigned the client, and issued `LISTEN` — leaking an ESTABLISHED socket
38
+ * holding `LISTEN <channel>` forever past `app.close()`.
39
+ */
40
+ connecting = null;
30
41
  /** Begin listening. Idempotent-ish: a second call while connected is a no-op. */
31
42
  async start() {
32
43
  this.stopped = false;
33
44
  await this.connect();
34
45
  }
35
- /** Stop listening + release the connection. Safe to call repeatedly. */
46
+ /**
47
+ * Stop listening + release the connection. Safe to call repeatedly and
48
+ * race-safe against an in-flight `connect()` (LISTEN-NOTIFY-2): it sets
49
+ * `stopped` first (so a resuming `connect()` self-releases its checkout),
50
+ * then awaits any in-flight connect, then releases whatever client landed.
51
+ */
36
52
  async stop() {
37
53
  this.stopped = true;
38
54
  if (this.reconnectTimer) {
39
55
  clearTimeout(this.reconnectTimer);
40
56
  this.reconnectTimer = null;
41
57
  }
58
+ const inflight = this.connecting;
59
+ if (inflight) {
60
+ try {
61
+ await inflight;
62
+ } catch {
63
+ }
64
+ }
42
65
  await this.releaseClient();
43
66
  }
44
67
  async connect() {
45
68
  if (this.stopped) return;
69
+ const attempt = this.doConnect();
70
+ this.connecting = attempt;
71
+ try {
72
+ await attempt;
73
+ } finally {
74
+ if (this.connecting === attempt) this.connecting = null;
75
+ }
76
+ }
77
+ async doConnect() {
46
78
  try {
47
79
  const client = await this.opts.pool.connect();
80
+ if (this.stopped) {
81
+ await this.releaseRawClient(client);
82
+ return;
83
+ }
48
84
  client.on("notification", (msg) => {
49
85
  if (msg.channel !== this.opts.channel) return;
50
86
  try {
@@ -58,6 +94,10 @@ var PgNotifyListener = class {
58
94
  this.handleDrop();
59
95
  });
60
96
  await client.query(`LISTEN ${this.opts.channel}`);
97
+ if (this.stopped) {
98
+ await this.releaseRawClient(client);
99
+ return;
100
+ }
61
101
  this.client = client;
62
102
  if (this.warnedDown) {
63
103
  this.logger.log(
@@ -99,6 +139,15 @@ var PgNotifyListener = class {
99
139
  const client = this.client;
100
140
  this.client = null;
101
141
  if (!client) return;
142
+ await this.releaseRawClient(client);
143
+ }
144
+ /**
145
+ * Tear down a raw checked-out client (LISTEN-NOTIFY-2). Used both by the
146
+ * normal `releaseClient()` path and by the connect-vs-stop race bail-outs,
147
+ * where the client was checked out but never assigned to `this.client`.
148
+ * Destroys (`release(true)`) so a half-listening socket is never reused.
149
+ */
150
+ async releaseRawClient(client) {
102
151
  try {
103
152
  client.removeAllListeners?.("notification");
104
153
  client.removeAllListeners?.("error");
@@ -115,4 +164,4 @@ export {
115
164
  pgNotify,
116
165
  PgNotifyListener
117
166
  };
118
- //# sourceMappingURL=chunk-MYQIQ27N.js.map
167
+ //# sourceMappingURL=chunk-Q6LRJ4VI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/jobs/pg-notify.ts"],"sourcesContent":["/**\n * PgNotifyListener + pgNotify — Postgres LISTEN/NOTIFY wakeups\n * (LISTEN-NOTIFY-1, dogfood gap #7).\n *\n * The drizzle jobs worker and events outbox drainer poll on an interval today\n * (default 1 s/hop). With `listen_notify` enabled, a row write that makes work\n * claimable emits an in-transaction `pg_notify(...)`; a dedicated listener\n * connection wakes the polling loop the moment the writing transaction commits.\n *\n * Two halves:\n * - `pgNotify(tx, channel, payload)` — fire an in-tx `pg_notify`. MUST be\n * called with the SAME transaction handle as the row write it announces, so\n * Postgres delivers it only on commit (the transactional-outbox guarantee).\n * - `PgNotifyListener` — owns a single long-lived `pg.PoolClient`, issues\n * `LISTEN <channel>`, forwards each notification's payload to an owner\n * callback, debounces bursts, and reconnects with capped backoff on drop.\n *\n * **Polling never stops.** This is a wake-early optimisation layered ON TOP of\n * interval polling. A lost notification (listener down, pooler eats the LISTEN,\n * etc.) degrades to today's poll latency, never to lost work — the claim/drain\n * query remains the source of truth.\n *\n * **PgBouncer caveat:** session-scoped `LISTEN` does not survive a\n * transaction-mode pooler. `listen_notify` requires a direct (or session-mode)\n * connection; behind a transaction pooler notifies are simply never received and\n * the system degrades to polling. See the jobs config block / skill.\n */\n// TODO(logging-subsystem): swap to ILogger once ADR-028 lands\nimport { Logger } from '@nestjs/common';\nimport { sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\n\n/** Channel the jobs worker LISTENs on; payload = pool name. */\nexport const JOBS_WAKE_CHANNEL = 'codegen_jobs_wake';\n/** Channel the events drainer LISTENs on; payload = event pool (or ''). */\nexport const EVENTS_WAKE_CHANNEL = 'codegen_events_wake';\n\n/**\n * Emit an in-transaction `pg_notify`. Call with the SAME `tx`/client handle as\n * the row write being announced so delivery is gated on commit. `payload` is a\n * short plain string (a pool name); it is NOT JSON — the wake is a hint and the\n * subsequent claim/drain query is authoritative. Channel names are framework\n * constants (never user input), so the `set_config`-free literal-channel form is\n * safe; the payload is bound as a parameter.\n */\nexport async function pgNotify(\n tx: DrizzleClient | DrizzleTransaction,\n channel: string,\n payload: string,\n): Promise<void> {\n const client = tx as DrizzleClient;\n // `pg_notify(channel, payload)` is the function form (vs the `NOTIFY chan,\n // 'payload'` statement form) precisely because it accepts bound parameters —\n // the payload is parameterised, never string-concatenated.\n await client.execute(sql`select pg_notify(${channel}, ${payload})`);\n}\n\n/** Minimal structural view of the `pg` Client/PoolClient surface we touch. */\ninterface PgListenClient {\n query(text: string): Promise<unknown>;\n on(event: 'notification', cb: (msg: { channel: string; payload?: string }) => void): void;\n on(event: 'error', cb: (err: Error) => void): void;\n removeAllListeners?: (event?: string) => void;\n release?: (err?: boolean) => void;\n end?: () => Promise<void>;\n}\n\n/** Minimal structural view of the `pg` Pool's `connect()`. */\ninterface PgPoolish {\n connect(): Promise<PgListenClient>;\n}\n\nconst DEFAULT_BACKOFF_MIN_MS = 100;\nconst DEFAULT_BACKOFF_MAX_MS = 5_000;\n\nexport interface PgNotifyListenerOptions {\n /** Channel to LISTEN on. */\n channel: string;\n /**\n * The underlying `pg.Pool` — obtained from `drizzleClient.$client`. A\n * dedicated `PoolClient` is checked out and held for the listener's lifetime\n * (separate from the query pool so a slow query never delays a wake).\n */\n pool: PgPoolish;\n /**\n * Called for every notification on `channel`, with the raw payload string\n * (`''` when Postgres delivers an empty payload). The owner decides whether\n * the payload is relevant (e.g. \"is this one of my pools?\") and debounces its\n * own claim cycle.\n */\n onNotify: (payload: string) => void;\n /** Label used in log lines (e.g. 'jobs:interactive', 'events'). */\n label: string;\n backoffMinMs?: number;\n backoffMaxMs?: number;\n}\n\n/**\n * Holds a dedicated listener connection and forwards notifications to `onNotify`.\n * Reconnects with capped exponential backoff on drop; logs the first failure +\n * the recovery exactly once each so a flapping connection doesn't flood logs.\n */\nexport class PgNotifyListener {\n private readonly logger: Logger;\n private client: PgListenClient | null = null;\n private stopped = false;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private backoffMs: number;\n private readonly backoffMinMs: number;\n private readonly backoffMaxMs: number;\n /** WARN-once gate so a flapping listener doesn't spam the log. */\n private warnedDown = false;\n /**\n * LISTEN-NOTIFY-2 — the in-flight `connect()` promise, set while a checkout is\n * mid-`await`. `stop()` awaits it so a `stop()` that races a still-resolving\n * `connect()` can't return before the connect either assigns `this.client`\n * (then released by `releaseClient`) or self-releases the checked-out client.\n * Without this, a `stop()` arriving during `pool.connect()`'s await saw\n * `this.client === null` (nothing to release), then `connect()` resumed,\n * assigned the client, and issued `LISTEN` — leaking an ESTABLISHED socket\n * holding `LISTEN <channel>` forever past `app.close()`.\n */\n private connecting: Promise<void> | null = null;\n\n constructor(private readonly opts: PgNotifyListenerOptions) {\n this.logger = new Logger(`PgNotifyListener(${opts.label})`);\n this.backoffMinMs = opts.backoffMinMs ?? DEFAULT_BACKOFF_MIN_MS;\n this.backoffMaxMs = opts.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS;\n this.backoffMs = this.backoffMinMs;\n }\n\n /** Begin listening. Idempotent-ish: a second call while connected is a no-op. */\n async start(): Promise<void> {\n this.stopped = false;\n await this.connect();\n }\n\n /**\n * Stop listening + release the connection. Safe to call repeatedly and\n * race-safe against an in-flight `connect()` (LISTEN-NOTIFY-2): it sets\n * `stopped` first (so a resuming `connect()` self-releases its checkout),\n * then awaits any in-flight connect, then releases whatever client landed.\n */\n async stop(): Promise<void> {\n this.stopped = true;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n // Await an in-flight checkout so we don't return while a client is still\n // mid-`pool.connect()`. The resuming `connect()` sees `stopped` and either\n // self-releases its checkout or assigns `this.client`; either way the\n // `releaseClient()` below mops up.\n const inflight = this.connecting;\n if (inflight) {\n try {\n await inflight;\n } catch {\n // connect failures are handled inside connect(); ignore here.\n }\n }\n await this.releaseClient();\n }\n\n private async connect(): Promise<void> {\n if (this.stopped) return;\n // Track this checkout so a racing stop() can await it (LISTEN-NOTIFY-2).\n const attempt = this.doConnect();\n this.connecting = attempt;\n try {\n await attempt;\n } finally {\n if (this.connecting === attempt) this.connecting = null;\n }\n }\n\n private async doConnect(): Promise<void> {\n try {\n const client = await this.opts.pool.connect();\n // Re-check AFTER the await resolves: a stop() may have fired while this\n // checkout was in flight. If so, release the just-checked-out client\n // right here and bail BEFORE wiring handlers / issuing LISTEN — otherwise\n // we'd leak an ESTABLISHED listener socket past shutdown (LISTEN-NOTIFY-2).\n if (this.stopped) {\n await this.releaseRawClient(client);\n return;\n }\n client.on('notification', (msg) => {\n if (msg.channel !== this.opts.channel) return;\n try {\n this.opts.onNotify(msg.payload ?? '');\n } catch (err) {\n this.logger.error(`onNotify threw: ${(err as Error).message}`);\n }\n });\n client.on('error', (err) => {\n // A connection-level error is the signal to reconnect. Don't double-log\n // here — scheduleReconnect owns the WARN-once.\n this.logger.debug?.(`listener connection error: ${err.message}`);\n this.handleDrop();\n });\n await client.query(`LISTEN ${this.opts.channel}`);\n // A stop() could have fired during the LISTEN round-trip too — same guard.\n if (this.stopped) {\n await this.releaseRawClient(client);\n return;\n }\n this.client = client;\n // Recovery: only announce if we had previously warned about being down.\n if (this.warnedDown) {\n this.logger.log(\n `listener reconnected; LISTEN ${this.opts.channel} re-established`,\n );\n this.warnedDown = false;\n }\n this.backoffMs = this.backoffMinMs;\n } catch (err) {\n this.handleConnectFailure(err);\n }\n }\n\n /** Connection dropped after being established → reconnect. */\n private handleDrop(): void {\n if (this.stopped) return;\n void this.releaseClient().finally(() => this.scheduleReconnect());\n }\n\n /** Initial / reconnect `connect()` threw. */\n private handleConnectFailure(err: unknown): void {\n this.scheduleReconnect(err);\n }\n\n private scheduleReconnect(err?: unknown): void {\n if (this.stopped) return;\n if (!this.warnedDown) {\n this.warnedDown = true;\n this.logger.warn(\n `listener down — falling back to interval polling until reconnect. ` +\n `Cause: ${err instanceof Error ? err.message : 'connection lost'}. ` +\n `(This degrades latency, not durability — polling still drives all work.)`,\n );\n }\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n const delay = this.backoffMs;\n this.backoffMs = Math.min(this.backoffMs * 2, this.backoffMaxMs);\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n void this.connect();\n }, delay);\n }\n\n private async releaseClient(): Promise<void> {\n const client = this.client;\n this.client = null;\n if (!client) return;\n await this.releaseRawClient(client);\n }\n\n /**\n * Tear down a raw checked-out client (LISTEN-NOTIFY-2). Used both by the\n * normal `releaseClient()` path and by the connect-vs-stop race bail-outs,\n * where the client was checked out but never assigned to `this.client`.\n * Destroys (`release(true)`) so a half-listening socket is never reused.\n */\n private async releaseRawClient(client: PgListenClient): Promise<void> {\n try {\n client.removeAllListeners?.('notification');\n client.removeAllListeners?.('error');\n if (client.release) client.release(true);\n else if (client.end) await client.end();\n } catch {\n // best-effort teardown\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -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";
7
4
  import {
8
5
  JobHandler,
9
6
  JobHandlerBase
10
7
  } from "./chunk-7P5ODGLA.js";
8
+ import {
9
+ JOB_ORCHESTRATOR
10
+ } from "./chunk-ZPL74UQN.js";
11
+ import {
12
+ EVENT_BUS
13
+ } from "./chunk-H5NH7KPE.js";
11
14
  import {
12
15
  BRIDGE_DELIVERY_REPO,
13
16
  BRIDGE_MULTI_TENANT,
14
17
  BRIDGE_REGISTRY
15
18
  } from "./chunk-4LH67P4U.js";
16
- import {
17
- EVENT_BUS
18
- } from "./chunk-H5NH7KPE.js";
19
19
  import {
20
20
  __decorateClass,
21
21
  __decorateParam
@@ -118,4 +118,4 @@ export {
118
118
  BRIDGE_DELIVERY_JOB_TYPE,
119
119
  BridgeDeliveryHandler
120
120
  };
121
- //# sourceMappingURL=chunk-SGSWVNNB.js.map
121
+ //# sourceMappingURL=chunk-R6F6KFIL.js.map
@@ -1,18 +1,18 @@
1
+ import {
2
+ JOBS_WAKE_CHANNEL,
3
+ PgNotifyListener
4
+ } from "./chunk-Q6LRJ4VI.js";
5
+ import {
6
+ JOB_HANDLER_REGISTRY
7
+ } from "./chunk-7P5ODGLA.js";
1
8
  import {
2
9
  JOB_ORCHESTRATOR,
3
10
  JOB_RUN_SERVICE,
4
11
  JOB_STEP_SERVICE
5
12
  } from "./chunk-ZPL74UQN.js";
6
- import {
7
- JOB_HANDLER_REGISTRY
8
- } from "./chunk-7P5ODGLA.js";
9
13
  import {
10
14
  jobRuns
11
15
  } from "./chunk-OKXZ63IA.js";
12
- import {
13
- JOBS_WAKE_CHANNEL,
14
- PgNotifyListener
15
- } from "./chunk-MYQIQ27N.js";
16
16
  import {
17
17
  tokenKey
18
18
  } from "./chunk-GYGNEQSC.js";
@@ -199,6 +199,7 @@ var JobWorker = class {
199
199
  }
200
200
  }
201
201
  async onModuleDestroy() {
202
+ await this.stopNotifyListener();
202
203
  if (this.shuttingDown) {
203
204
  await this.drainInFlight();
204
205
  return;
@@ -213,14 +214,6 @@ var JobWorker = class {
213
214
  this.sweeperTimer = null;
214
215
  }
215
216
  process.removeListener("SIGTERM", this.sigtermHandler);
216
- if (this.notifyListener) {
217
- try {
218
- await this.notifyListener.stop();
219
- } catch (err) {
220
- this.logger.error(`notify listener stop failed: ${err.message}`);
221
- }
222
- this.notifyListener = null;
223
- }
224
217
  await this.drainInFlight();
225
218
  try {
226
219
  await this.db.update(jobRuns).set({ status: "pending", claimedAt: null, startedAt: null }).where(
@@ -240,6 +233,23 @@ var JobWorker = class {
240
233
  timeout
241
234
  ]);
242
235
  }
236
+ /**
237
+ * LISTEN-NOTIFY-2 — stop + drop the wake listener. Idempotent: a second call
238
+ * (SIGTERM + Nest destroy) finds `notifyListener` already null and no-ops.
239
+ * `PgNotifyListener.stop()` is itself race-safe against an in-flight
240
+ * `connect()`, so even a destroy that arrives microseconds after `start()`
241
+ * releases the listener socket rather than leaking it.
242
+ */
243
+ async stopNotifyListener() {
244
+ const listener = this.notifyListener;
245
+ if (!listener) return;
246
+ this.notifyListener = null;
247
+ try {
248
+ await listener.stop();
249
+ } catch (err) {
250
+ this.logger.error(`notify listener stop failed: ${err.message}`);
251
+ }
252
+ }
243
253
  // ============================================================================
244
254
  // Poll loop
245
255
  // ============================================================================
@@ -518,4 +528,4 @@ export {
518
528
  buildStaleSweepQuery,
519
529
  JobWorker
520
530
  };
521
- //# sourceMappingURL=chunk-FWRL7BZ5.js.map
531
+ //# sourceMappingURL=chunk-VDL5CJ5C.js.map