@pattern-stack/codegen 0.15.3 → 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.
- package/CHANGELOG.md +48 -0
- package/dist/{chunk-GCYKMF22.js → chunk-24WXSC3C.js} +6 -6
- package/dist/{chunk-O37C3YE6.js → chunk-3RWMQC3K.js} +23 -17
- package/dist/chunk-3RWMQC3K.js.map +1 -0
- package/dist/{chunk-4JLJYWJC.js → chunk-4PFF3ED4.js} +98 -10
- package/dist/chunk-4PFF3ED4.js.map +1 -0
- package/dist/{chunk-32BMMV4H.js → chunk-5RT7JGKT.js} +5 -5
- package/dist/{chunk-RDVTWIYY.js → chunk-BHZP6LOV.js} +8 -8
- package/dist/{chunk-4MVGAMUA.js → chunk-BK5ICA2F.js} +4 -4
- package/dist/{chunk-4RFHUZXU.js → chunk-BULPAAD3.js} +2 -2
- package/dist/{chunk-IYNSRIGR.js → chunk-CEWLVVAH.js} +6 -6
- package/dist/{chunk-27ETSJ2X.js → chunk-COGHTKXY.js} +2 -2
- package/dist/{chunk-J7JMVS2B.js → chunk-DKKFTHHI.js} +4 -4
- package/dist/{chunk-YTN6BKWA.js → chunk-DRCLNYH7.js} +7 -7
- package/dist/{chunk-TNXH7BJS.js → chunk-E45CSC33.js} +2 -2
- package/dist/{chunk-L7BNNRGI.js → chunk-EBKVKN75.js} +26 -6
- package/dist/chunk-EBKVKN75.js.map +1 -0
- package/dist/{chunk-EOLLMEAH.js → chunk-EJBK7I4F.js} +3 -3
- package/dist/chunk-EJBK7I4F.js.map +1 -0
- package/dist/{chunk-K2I6XIK5.js → chunk-KSTZIULO.js} +4 -4
- package/dist/{chunk-Z7PQCAVK.js → chunk-LQ6PYFU6.js} +4 -4
- package/dist/chunk-MYQIQ27N.js +118 -0
- package/dist/chunk-MYQIQ27N.js.map +1 -0
- package/dist/{chunk-5Y7W3XR6.js → chunk-OTR44OH6.js} +24 -5
- package/dist/chunk-OTR44OH6.js.map +1 -0
- package/dist/{chunk-4H3PETLM.js → chunk-RUYLXR5F.js} +15 -12
- package/dist/chunk-RUYLXR5F.js.map +1 -0
- package/dist/{chunk-7YGORYZD.js → chunk-T6C4LFLC.js} +4 -4
- package/dist/{chunk-OGIZXGPY.js → chunk-TDEHU73T.js} +4 -4
- package/dist/{chunk-FBGHYQIZ.js → chunk-VNBC3VXM.js} +5 -5
- package/dist/{chunk-YPWODKD5.js → chunk-W2UIDI3R.js} +5 -5
- package/dist/chunk-W4HOHZVF.js +1 -0
- package/dist/{chunk-RC23QROE.js → chunk-XDIIVIIK.js} +79 -5
- package/dist/chunk-XDIIVIIK.js.map +1 -0
- package/dist/{chunk-DCCZB4UC.js → chunk-XWBK3XJK.js} +4 -4
- package/dist/{chunk-SR7F3TJY.js → chunk-YK5JEVLX.js} +4 -4
- package/dist/{chunk-BIO6F7YI.js → chunk-ZPL74UQN.js} +4 -2
- package/dist/{chunk-BIO6F7YI.js.map → chunk-ZPL74UQN.js.map} +1 -1
- package/dist/runtime/base-classes/index.js +22 -22
- package/dist/runtime/shared/openapi/index.js +3 -3
- package/dist/runtime/subsystems/analytics/analytics.module.js +2 -2
- package/dist/runtime/subsystems/analytics/index.js +4 -4
- package/dist/runtime/subsystems/auth/index.js +1 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +3 -3
- package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +3 -3
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.d.ts +2 -1
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +7 -6
- package/dist/runtime/subsystems/bridge/bridge.module.js +20 -19
- package/dist/runtime/subsystems/bridge/event-flow.service.js +3 -3
- package/dist/runtime/subsystems/bridge/index.js +22 -21
- package/dist/runtime/subsystems/cache/cache.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/cache/cache.module.js +3 -3
- package/dist/runtime/subsystems/cache/index.js +5 -5
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +20 -0
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +4 -3
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +2 -2
- package/dist/runtime/subsystems/events/events.module.d.ts +14 -0
- package/dist/runtime/subsystems/events/events.module.js +6 -5
- package/dist/runtime/subsystems/events/index.js +12 -11
- package/dist/runtime/subsystems/index.js +65 -64
- package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
- package/dist/runtime/subsystems/integration/index.js +14 -14
- package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/integration/integration.module.js +4 -4
- package/dist/runtime/subsystems/jobs/index.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/index.js +44 -32
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +6 -5
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +3 -2
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +3 -3
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +3 -3
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +28 -0
- package/dist/runtime/subsystems/jobs/job-worker.js +3 -2
- package/dist/runtime/subsystems/jobs/job-worker.module.js +12 -11
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +12 -7
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +10 -9
- package/dist/runtime/subsystems/jobs/jobs-domain.tokens.d.ts +13 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.tokens.js +3 -1
- package/dist/runtime/subsystems/jobs/pg-notify.d.ts +85 -0
- package/dist/runtime/subsystems/jobs/pg-notify.js +14 -0
- package/dist/runtime/subsystems/jobs/pg-notify.js.map +1 -0
- package/dist/runtime/subsystems/observability/index.js +4 -4
- package/dist/runtime/subsystems/observability/observability.module.js +4 -4
- package/dist/runtime/subsystems/observability/observability.service.js +3 -3
- package/dist/runtime/subsystems/storage/index.js +4 -4
- package/dist/runtime/subsystems/storage/storage.module.js +2 -2
- package/dist/src/cli/index.js +49 -11
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.js +5 -5
- package/package.json +1 -1
- package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +27 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +108 -4
- package/runtime/subsystems/events/events.module.ts +14 -0
- package/runtime/subsystems/jobs/index.ts +10 -0
- package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +29 -2
- package/runtime/subsystems/jobs/job-worker.module.ts +11 -0
- package/runtime/subsystems/jobs/job-worker.ts +98 -0
- package/runtime/subsystems/jobs/jobs-domain.module.ts +22 -7
- package/runtime/subsystems/jobs/jobs-domain.tokens.ts +13 -0
- package/runtime/subsystems/jobs/pg-notify.ts +216 -0
- package/templates/subsystem/events-config/codegen-config-events-block.ejs.t +14 -0
- package/templates/subsystem/jobs-config/codegen-config-jobs-block.ejs.t +13 -4
- package/dist/chunk-4H3PETLM.js.map +0 -1
- package/dist/chunk-4JLJYWJC.js.map +0 -1
- package/dist/chunk-5Y7W3XR6.js.map +0 -1
- package/dist/chunk-EOLLMEAH.js.map +0 -1
- package/dist/chunk-L7BNNRGI.js.map +0 -1
- package/dist/chunk-O37C3YE6.js.map +0 -1
- package/dist/chunk-RC23QROE.js.map +0 -1
- package/dist/chunk-UTN4GBPQ.js +0 -1
- /package/dist/{chunk-GCYKMF22.js.map → chunk-24WXSC3C.js.map} +0 -0
- /package/dist/{chunk-32BMMV4H.js.map → chunk-5RT7JGKT.js.map} +0 -0
- /package/dist/{chunk-RDVTWIYY.js.map → chunk-BHZP6LOV.js.map} +0 -0
- /package/dist/{chunk-4MVGAMUA.js.map → chunk-BK5ICA2F.js.map} +0 -0
- /package/dist/{chunk-4RFHUZXU.js.map → chunk-BULPAAD3.js.map} +0 -0
- /package/dist/{chunk-IYNSRIGR.js.map → chunk-CEWLVVAH.js.map} +0 -0
- /package/dist/{chunk-27ETSJ2X.js.map → chunk-COGHTKXY.js.map} +0 -0
- /package/dist/{chunk-J7JMVS2B.js.map → chunk-DKKFTHHI.js.map} +0 -0
- /package/dist/{chunk-YTN6BKWA.js.map → chunk-DRCLNYH7.js.map} +0 -0
- /package/dist/{chunk-TNXH7BJS.js.map → chunk-E45CSC33.js.map} +0 -0
- /package/dist/{chunk-K2I6XIK5.js.map → chunk-KSTZIULO.js.map} +0 -0
- /package/dist/{chunk-Z7PQCAVK.js.map → chunk-LQ6PYFU6.js.map} +0 -0
- /package/dist/{chunk-7YGORYZD.js.map → chunk-T6C4LFLC.js.map} +0 -0
- /package/dist/{chunk-OGIZXGPY.js.map → chunk-TDEHU73T.js.map} +0 -0
- /package/dist/{chunk-FBGHYQIZ.js.map → chunk-VNBC3VXM.js.map} +0 -0
- /package/dist/{chunk-YPWODKD5.js.map → chunk-W2UIDI3R.js.map} +0 -0
- /package/dist/{chunk-UTN4GBPQ.js.map → chunk-W4HOHZVF.js.map} +0 -0
- /package/dist/{chunk-DCCZB4UC.js.map → chunk-XWBK3XJK.js.map} +0 -0
- /package/dist/{chunk-SR7F3TJY.js.map → chunk-YK5JEVLX.js.map} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import "../../../chunk-
|
|
1
|
+
import "../../../chunk-W4HOHZVF.js";
|
|
2
2
|
import {
|
|
3
3
|
JobWorkerModule,
|
|
4
4
|
JobWorkerOrchestrator
|
|
5
|
-
} from "../../../chunk-
|
|
5
|
+
} from "../../../chunk-RUYLXR5F.js";
|
|
6
6
|
import {
|
|
7
7
|
JOB_WORKER_OPTIONS,
|
|
8
8
|
JobWorker,
|
|
@@ -10,42 +10,28 @@ import {
|
|
|
10
10
|
buildStaleSweepQuery,
|
|
11
11
|
classifyError,
|
|
12
12
|
computeBackoff
|
|
13
|
-
} from "../../../chunk-
|
|
13
|
+
} from "../../../chunk-XDIIVIIK.js";
|
|
14
14
|
import {
|
|
15
15
|
JobsDomainModule
|
|
16
|
-
} from "../../../chunk-
|
|
17
|
-
import {
|
|
18
|
-
DrizzleJobRunService
|
|
19
|
-
} from "../../../chunk-FBGHYQIZ.js";
|
|
20
|
-
import {
|
|
21
|
-
MemoryJobRunService
|
|
22
|
-
} from "../../../chunk-RDVTWIYY.js";
|
|
23
|
-
import "../../../chunk-L3LZWWSX.js";
|
|
16
|
+
} from "../../../chunk-3RWMQC3K.js";
|
|
24
17
|
import {
|
|
25
18
|
DrizzleJobStepService
|
|
26
19
|
} from "../../../chunk-DV4RV2DC.js";
|
|
27
|
-
import {
|
|
28
|
-
BULLMQ_CONNECTION,
|
|
29
|
-
BULLMQ_RESOLVED_CONFIG,
|
|
30
|
-
resolveBullMqConfig,
|
|
31
|
-
resolvePoolQueueName
|
|
32
|
-
} from "../../../chunk-I6MVCB5A.js";
|
|
33
|
-
import {
|
|
34
|
-
FRAMEWORK_POOLS,
|
|
35
|
-
RESERVED_POOL_NAMES,
|
|
36
|
-
allNonReservedPoolNames,
|
|
37
|
-
allPoolNames,
|
|
38
|
-
loadPoolConfig
|
|
39
|
-
} from "../../../chunk-RHVN6NA7.js";
|
|
40
20
|
import {
|
|
41
21
|
DrizzleJobOrchestrator
|
|
42
|
-
} from "../../../chunk-
|
|
22
|
+
} from "../../../chunk-OTR44OH6.js";
|
|
43
23
|
import {
|
|
44
24
|
MemoryJobOrchestrator
|
|
45
|
-
} from "../../../chunk-
|
|
25
|
+
} from "../../../chunk-BULPAAD3.js";
|
|
46
26
|
import {
|
|
47
27
|
MemoryJobStepService
|
|
48
28
|
} from "../../../chunk-PNZSGAB2.js";
|
|
29
|
+
import {
|
|
30
|
+
DrizzleJobRunService
|
|
31
|
+
} from "../../../chunk-VNBC3VXM.js";
|
|
32
|
+
import {
|
|
33
|
+
MemoryJobRunService
|
|
34
|
+
} from "../../../chunk-BHZP6LOV.js";
|
|
49
35
|
import {
|
|
50
36
|
MemoryJobStore
|
|
51
37
|
} from "../../../chunk-SNQ3TOWP.js";
|
|
@@ -58,6 +44,27 @@ import {
|
|
|
58
44
|
MissingTenantIdError,
|
|
59
45
|
ReservedPoolViolationError
|
|
60
46
|
} from "../../../chunk-T4BIIU5E.js";
|
|
47
|
+
import "../../../chunk-L3LZWWSX.js";
|
|
48
|
+
import {
|
|
49
|
+
BULLMQ_CONNECTION,
|
|
50
|
+
BULLMQ_RESOLVED_CONFIG,
|
|
51
|
+
resolveBullMqConfig,
|
|
52
|
+
resolvePoolQueueName
|
|
53
|
+
} from "../../../chunk-I6MVCB5A.js";
|
|
54
|
+
import {
|
|
55
|
+
FRAMEWORK_POOLS,
|
|
56
|
+
RESERVED_POOL_NAMES,
|
|
57
|
+
allNonReservedPoolNames,
|
|
58
|
+
allPoolNames,
|
|
59
|
+
loadPoolConfig
|
|
60
|
+
} from "../../../chunk-RHVN6NA7.js";
|
|
61
|
+
import {
|
|
62
|
+
JOBS_LISTEN_NOTIFY,
|
|
63
|
+
JOBS_MULTI_TENANT,
|
|
64
|
+
JOB_ORCHESTRATOR,
|
|
65
|
+
JOB_RUN_SERVICE,
|
|
66
|
+
JOB_STEP_SERVICE
|
|
67
|
+
} from "../../../chunk-ZPL74UQN.js";
|
|
61
68
|
import {
|
|
62
69
|
HandlerRegistry,
|
|
63
70
|
JOB_HANDLER_METADATA_KEY,
|
|
@@ -66,12 +73,6 @@ import {
|
|
|
66
73
|
JobHandlerBase,
|
|
67
74
|
ParentClosePolicy
|
|
68
75
|
} from "../../../chunk-CO6LUM72.js";
|
|
69
|
-
import {
|
|
70
|
-
JOBS_MULTI_TENANT,
|
|
71
|
-
JOB_ORCHESTRATOR,
|
|
72
|
-
JOB_RUN_SERVICE,
|
|
73
|
-
JOB_STEP_SERVICE
|
|
74
|
-
} from "../../../chunk-BIO6F7YI.js";
|
|
75
76
|
import {
|
|
76
77
|
collisionModeEnum,
|
|
77
78
|
jobRunStatusEnum,
|
|
@@ -85,6 +86,12 @@ import {
|
|
|
85
86
|
triggerSourceEnum,
|
|
86
87
|
waitKindEnum
|
|
87
88
|
} from "../../../chunk-OKXZ63IA.js";
|
|
89
|
+
import {
|
|
90
|
+
EVENTS_WAKE_CHANNEL,
|
|
91
|
+
JOBS_WAKE_CHANNEL,
|
|
92
|
+
PgNotifyListener,
|
|
93
|
+
pgNotify
|
|
94
|
+
} from "../../../chunk-MYQIQ27N.js";
|
|
88
95
|
import "../../../chunk-GYGNEQSC.js";
|
|
89
96
|
import "../../../chunk-U64T4YZE.js";
|
|
90
97
|
import "../../../chunk-2E224ZSN.js";
|
|
@@ -95,9 +102,12 @@ export {
|
|
|
95
102
|
DrizzleJobOrchestrator,
|
|
96
103
|
DrizzleJobRunService,
|
|
97
104
|
DrizzleJobStepService,
|
|
105
|
+
EVENTS_WAKE_CHANNEL,
|
|
98
106
|
FRAMEWORK_POOLS,
|
|
99
107
|
HandlerRegistry,
|
|
108
|
+
JOBS_LISTEN_NOTIFY,
|
|
100
109
|
JOBS_MULTI_TENANT,
|
|
110
|
+
JOBS_WAKE_CHANNEL,
|
|
101
111
|
JOB_HANDLER_METADATA_KEY,
|
|
102
112
|
JOB_HANDLER_REGISTRY,
|
|
103
113
|
JOB_ORCHESTRATOR,
|
|
@@ -120,6 +130,7 @@ export {
|
|
|
120
130
|
MemoryJobStore,
|
|
121
131
|
MissingTenantIdError,
|
|
122
132
|
ParentClosePolicy,
|
|
133
|
+
PgNotifyListener,
|
|
123
134
|
RESERVED_POOL_NAMES,
|
|
124
135
|
ReservedPoolViolationError,
|
|
125
136
|
allNonReservedPoolNames,
|
|
@@ -137,6 +148,7 @@ export {
|
|
|
137
148
|
jobs,
|
|
138
149
|
loadPoolConfig,
|
|
139
150
|
parentClosePolicyEnum,
|
|
151
|
+
pgNotify,
|
|
140
152
|
replayFromEnum,
|
|
141
153
|
resolveBullMqConfig,
|
|
142
154
|
resolvePoolQueueName,
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DrizzleJobOrchestrator
|
|
3
|
+
} from "../../../chunk-OTR44OH6.js";
|
|
4
|
+
import "../../../chunk-T4BIIU5E.js";
|
|
1
5
|
import {
|
|
2
6
|
BULLMQ_CONNECTION,
|
|
3
7
|
BULLMQ_RESOLVED_CONFIG,
|
|
4
8
|
resolvePoolQueueName
|
|
5
9
|
} from "../../../chunk-I6MVCB5A.js";
|
|
6
10
|
import "../../../chunk-RHVN6NA7.js";
|
|
7
|
-
import {
|
|
8
|
-
DrizzleJobOrchestrator
|
|
9
|
-
} from "../../../chunk-5Y7W3XR6.js";
|
|
10
|
-
import "../../../chunk-T4BIIU5E.js";
|
|
11
11
|
import {
|
|
12
12
|
JOBS_MULTI_TENANT
|
|
13
|
-
} from "../../../chunk-
|
|
13
|
+
} from "../../../chunk-ZPL74UQN.js";
|
|
14
14
|
import {
|
|
15
15
|
jobRuns,
|
|
16
16
|
jobs
|
|
17
17
|
} from "../../../chunk-OKXZ63IA.js";
|
|
18
|
+
import "../../../chunk-MYQIQ27N.js";
|
|
18
19
|
import "../../../chunk-GYGNEQSC.js";
|
|
19
20
|
import {
|
|
20
21
|
DRIZZLE
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts"],"sourcesContent":["/**\n * BullMQJobOrchestrator — BullMQ-backed implementation of `IJobOrchestrator`\n * (BULLMQ-1, ADR-022 §58 — the reserved \"Phase 6+\" backend, now built).\n *\n * Split-of-responsibility (spec §\"Postgres + BullMQ coordination\"):\n * - Postgres `job_run` stays the **domain source of truth** — scoping,\n * hierarchy (`parent_run_id`/`root_run_id`), dedupe/concurrency state,\n * `listForScope`. All of that is the Drizzle backend's job and is reused\n * verbatim by extending `DrizzleJobOrchestrator`.\n * - BullMQ owns the **claim/dispatch** half. `start` adds a job to the\n * pool's queue (or to a FlowProducer flow when parented); the BullMQ\n * `Worker` (see `job-worker.bullmq-backend.ts`) consumes it and runs the\n * handler through the existing `JobHandlerBase` path. `cancel` removes\n * the queued job; `replay` re-adds it after the shared DB reset.\n *\n * This is **additive**: the Drizzle backend, the core protocol, and app code\n * are untouched. Consumers flip `jobs.backend: bullmq` with no code change —\n * the same `IJobOrchestrator` surface is satisfied.\n *\n * `jobId` (spec §Gotcha 1): BullMQ treats `:` as a Redis key separator and\n * consumers use `vendor:externalId`-shaped idempotency keys, so we derive the\n * `jobId` as `sha1(idempotencyKey)` — colon-safe and stable (same logical key\n * → same id → BullMQ-native dedup). When no dedupe key is configured we fall\n * back to the `job_run.id` (a fresh UUID), which is already colon-safe.\n */\nimport { createHash } from 'node:crypto';\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport { eq } from 'drizzle-orm';\n// `bullmq` is an OPTIONAL peer dependency. Only TYPE imports here — types are\n// erased at compile time and never resolve `'bullmq'` at runtime, so a\n// `drizzle`-only consumer who didn't install bullmq can still load this file\n// (it is statically imported by `jobs-domain.module.ts`). The VALUE\n// constructors (`Queue`, `FlowProducer`) are loaded lazily via `await\n// import('bullmq')` in `loadBullMq()` — mirrors\n// `event-bus.redis-backend.ts:createRedisClient`. See BULLMQ-1 §Lazy import.\nimport type { ConnectionOptions, FlowProducer, Queue } from 'bullmq';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, jobs, type JobDefinitionRow } from './job-orchestration.schema';\nimport { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend';\nimport type {\n CancelOptions,\n JobRun,\n StartOptions,\n} from './job-orchestrator.protocol';\nimport { JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport {\n BULLMQ_CONNECTION,\n resolvePoolQueueName,\n type BullMqResolvedConfig,\n BULLMQ_RESOLVED_CONFIG,\n} from './bullmq.config';\n\n/**\n * Derive a colon-safe, stable BullMQ `jobId` from a logical idempotency key.\n *\n * SHA-1 over the raw key. Collision analysis (spec §Gotcha 1, resolved during\n * implementation): SHA-1's 160-bit space makes an accidental collision between\n * two *distinct* logical keys astronomically unlikely at any realistic job\n * volume (the birthday bound is ~2^80 keys before a 50% collision chance —\n * orders of magnitude beyond any job throughput). SHA-1's cryptographic\n * weakness is irrelevant here: there is no adversary forging idempotency keys,\n * and even a forged collision only deduplicates two jobs that the caller chose\n * to key identically. We therefore accept SHA-1 with no mitigation. The *same*\n * logical key intentionally maps to the *same* jobId — that is the dedup\n * mechanism, not a collision.\n */\nexport function sha1JobId(idempotencyKey: string): string {\n return createHash('sha1').update(idempotencyKey).digest('hex');\n}\n\n// Constructor types for the lazily-loaded `bullmq` value exports. Typed via\n// `typeof` the type-only imports so the cached ctors stay strongly typed\n// without a runtime `import`.\ntype QueueCtor = typeof import('bullmq').Queue;\ntype FlowProducerCtor = typeof import('bullmq').FlowProducer;\n\n@Injectable()\nexport class BullMQJobOrchestrator extends DrizzleJobOrchestrator {\n // TODO(logging-subsystem): swap to ILogger once ADR-028 lands\n private readonly bullLogger = new Logger(BullMQJobOrchestrator.name);\n\n /** Lazily-opened `Queue` handles, one per pool. */\n private readonly queues = new Map<string, Queue>();\n /** Single FlowProducer for parent/child hierarchies. Lazily opened. */\n private _flow: FlowProducer | null = null;\n\n /**\n * Cached `bullmq` value constructors, populated by `loadBullMq()` on first\n * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching\n * a queue). Kept off the import graph so a `drizzle`-only consumer never\n * resolves the optional `'bullmq'` package.\n */\n private QueueCtor: QueueCtor | null = null;\n private FlowProducerCtor: FlowProducerCtor | null = null;\n private bullMqLoad: Promise<void> | null = null;\n\n /**\n * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is\n * `private` (can't be redeclared even privately in a subclass), and the\n * spec forbids touching that file — so the subclass keeps its own handle\n * under a distinct name (same instance, passed through to `super`) for the\n * cancel-cascade snapshot + definition/run loads below.\n */\n private readonly bullDb: DrizzleClient;\n\n constructor(\n @Inject(DRIZZLE) db: DrizzleClient,\n @Inject(JOBS_MULTI_TENANT) multiTenant: boolean,\n @Inject(BULLMQ_CONNECTION) private readonly connection: ConnectionOptions,\n @Optional()\n @Inject(BULLMQ_RESOLVED_CONFIG)\n private readonly bullConfig: BullMqResolvedConfig | null = null,\n ) {\n super(db, multiTenant);\n this.bullDb = db;\n }\n\n /**\n * Lazily load the optional `bullmq` package and cache its value\n * constructors. Idempotent (single in-flight promise). Throws a friendly,\n * actionable error when the consumer selected `backend: 'bullmq'` but did\n * not install the package — mirrors `createRedisClient` in the redis event\n * backend. Must be `await`ed before any `queueFor`/`flow` access.\n */\n private async loadBullMq(): Promise<void> {\n if (this.QueueCtor && this.FlowProducerCtor) return;\n if (!this.bullMqLoad) {\n this.bullMqLoad = (async () => {\n try {\n const mod = await import('bullmq');\n this.QueueCtor = mod.Queue;\n this.FlowProducerCtor = mod.FlowProducer;\n } catch {\n throw new Error(\n 'BullMQ backend requires the \"bullmq\" package. Install it with: npm install bullmq',\n );\n }\n })();\n }\n await this.bullMqLoad;\n }\n\n /**\n * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await\n * loadBullMq()` first so `QueueCtor` is populated.\n */\n private queueFor(pool: string): Queue {\n if (!this.QueueCtor) {\n throw new Error('BullMQJobOrchestrator: queueFor called before loadBullMq()');\n }\n const name = resolvePoolQueueName(pool, this.bullConfig);\n let q = this.queues.get(name);\n if (!q) {\n q = new this.QueueCtor(name, { connection: this.connection });\n this.queues.set(name, q);\n }\n return q;\n }\n\n private flow(): FlowProducer {\n if (!this.FlowProducerCtor) {\n throw new Error('BullMQJobOrchestrator: flow called before loadBullMq()');\n }\n if (!this._flow) {\n this._flow = new this.FlowProducerCtor({ connection: this.connection });\n }\n return this._flow;\n }\n\n // ==========================================================================\n // start — Postgres insert (super) + BullMQ dispatch\n // ==========================================================================\n\n override async start(\n type: string,\n input: unknown,\n opts: StartOptions = {},\n tx?: DrizzleTransaction,\n ): Promise<JobRun> {\n // (1) Postgres remains source of truth — the Drizzle backend handles the\n // job-definition lookup, dedupe short-circuit, concurrency collision,\n // parent/root resolution, and the `job_run` INSERT. If dedupe\n // short-circuited it returns the incumbent row whose dispatch already\n // happened on the original start; we must not enqueue again.\n const run = await super.start(type, input, opts, tx);\n\n // Dedupe returned an existing run (its createdAt predates this call) —\n // BullMQ-native dedup already covered the dispatch. Skip re-enqueue.\n // We detect this by checking the run was freshly created in THIS call:\n // a brand-new run has status 'pending' and zero attempts AND its id is\n // not yet known to BullMQ. The cheapest reliable signal is the dedupe\n // path's contract: super.start returns the incumbent unchanged. Since we\n // cannot distinguish purely from the row, we rely on `jobId` idempotency\n // — re-adding with the same jobId is a no-op in BullMQ, so the enqueue is\n // safe to attempt unconditionally.\n\n await this.dispatch(run, type);\n return run;\n }\n\n /**\n * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a\n * `parentRunId` we attach it to the parent's existing BullMQ job through the\n * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in\n * its own graph. (The FlowProducer is reserved for whole-tree atomic\n * submits, exposed as an opt-in extension via `flowProducer()`; runtime\n * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the\n * correct primitive here.)\n *\n * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is\n * present (so the same logical key dedups), else the `job_run.id` UUID\n * (already colon-free).\n *\n * The domain `parentClosePolicy` cascade is still enforced in Postgres by\n * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,\n * not the authority.\n */\n private async dispatch(run: JobRun, type: string): Promise<void> {\n await this.loadBullMq();\n const def = await this.loadDefinition(type);\n const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;\n\n const jobOpts: Record<string, unknown> = {\n jobId,\n ...this.retryOpts(def),\n ...this.dedupeOpts(run, def),\n };\n\n if (run.parentRunId) {\n const parentRow = await this.loadRun(run.parentRunId);\n if (parentRow) {\n const parentJobId = parentRow.dedupeKey\n ? sha1JobId(parentRow.dedupeKey)\n : parentRow.id;\n jobOpts.parent = {\n id: parentJobId,\n queue: resolvePoolQueueName(parentRow.pool, this.bullConfig),\n };\n }\n }\n\n // The processor reads the authoritative input from `job_run`; the payload\n // carries the runId so it can load the row, plus type/input for logging.\n const payload = { runId: run.id, type, input: run.input };\n await this.queueFor(run.pool).add(type, payload, jobOpts);\n }\n\n /**\n * Opt-in extension (spec §Extensions): expose the FlowProducer for\n * consumers that want to submit a whole parent/child DAG atomically up\n * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —\n * code using it is not portable to the Drizzle backend. Async because it\n * lazily loads the optional `bullmq` package on first use.\n */\n async flowProducer(): Promise<FlowProducer> {\n await this.loadBullMq();\n return this.flow();\n }\n\n private retryOpts(def: JobDefinitionRow): {\n attempts?: number;\n backoff?: { type: 'fixed' | 'exponential'; delay: number };\n } {\n const policy = def.retryPolicy;\n if (!policy) return {};\n return {\n attempts: policy.attempts,\n backoff: {\n type: policy.backoff === 'exponential' ? 'exponential' : 'fixed',\n delay: policy.baseMs,\n },\n };\n }\n\n private dedupeOpts(\n run: JobRun,\n def: JobDefinitionRow,\n ): { deduplication?: { id: string; ttl?: number } } {\n if (!run.dedupeKey || !def.dedupeWindowMs) return {};\n return {\n deduplication: {\n id: sha1JobId(run.dedupeKey),\n ttl: def.dedupeWindowMs,\n },\n };\n }\n\n // ==========================================================================\n // cancel — Postgres cascade (super) + remove from queue\n // ==========================================================================\n\n override async cancel(runId: string, opts: CancelOptions = {}): Promise<void> {\n // Snapshot the subtree BEFORE the DB cascade flips rows to canceled, so we\n // can remove every affected BullMQ job. We read the target's rootRunId and\n // the non-terminal descendants the same way the Drizzle cascade does.\n const target = await this.loadRun(runId);\n\n await super.cancel(runId, opts);\n\n if (!target) return;\n await this.loadBullMq();\n // Remove the target's own queued job.\n await this.removeFromQueue(target);\n\n if (opts.cascade === false) return;\n\n // Remove descendants' queued jobs (the DB rows were just canceled by\n // super.cancel; we mirror that into BullMQ so workers don't pick them up).\n const descendants = await this.bullDb\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.rootRunId, target.rootRunId));\n for (const child of descendants) {\n if (child.id === runId) continue;\n await this.removeFromQueue(child as JobRun);\n }\n }\n\n private async removeFromQueue(run: JobRun): Promise<void> {\n const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;\n try {\n const job = await this.queueFor(run.pool).getJob(jobId);\n if (job) await job.remove();\n } catch (err) {\n // A job already moved to active/completed cannot always be removed;\n // the Postgres cancel is authoritative either way.\n this.bullLogger.warn(\n `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${(err as Error).message}`,\n );\n }\n }\n\n // ==========================================================================\n // replay — Postgres reset (super) + re-enqueue\n // ==========================================================================\n\n override async replay(runId: string): Promise<JobRun> {\n const run = await super.replay(runId);\n await this.dispatch(run, run.jobType);\n return run;\n }\n\n // ==========================================================================\n // Internals\n // ==========================================================================\n\n private async loadDefinition(type: string): Promise<JobDefinitionRow> {\n const [def] = await this.bullDb\n .select()\n .from(jobs)\n .where(eq(jobs.type, type))\n .limit(1);\n if (!def) {\n throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);\n }\n return def as JobDefinitionRow;\n }\n\n private async loadRun(id: string): Promise<JobRun | null> {\n const [row] = await this.bullDb\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, id))\n .limit(1);\n return (row as JobRun) ?? null;\n }\n\n /** Close all open queue + flow connections. Called on module destroy. */\n async closeConnections(): Promise<void> {\n for (const q of this.queues.values()) {\n await q.close().catch(() => undefined);\n }\n this.queues.clear();\n if (this._flow) {\n await this._flow.close().catch(() => undefined);\n this._flow = null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts"],"sourcesContent":["/**\n * BullMQJobOrchestrator — BullMQ-backed implementation of `IJobOrchestrator`\n * (BULLMQ-1, ADR-022 §58 — the reserved \"Phase 6+\" backend, now built).\n *\n * Split-of-responsibility (spec §\"Postgres + BullMQ coordination\"):\n * - Postgres `job_run` stays the **domain source of truth** — scoping,\n * hierarchy (`parent_run_id`/`root_run_id`), dedupe/concurrency state,\n * `listForScope`. All of that is the Drizzle backend's job and is reused\n * verbatim by extending `DrizzleJobOrchestrator`.\n * - BullMQ owns the **claim/dispatch** half. `start` adds a job to the\n * pool's queue (or to a FlowProducer flow when parented); the BullMQ\n * `Worker` (see `job-worker.bullmq-backend.ts`) consumes it and runs the\n * handler through the existing `JobHandlerBase` path. `cancel` removes\n * the queued job; `replay` re-adds it after the shared DB reset.\n *\n * This is **additive**: the Drizzle backend, the core protocol, and app code\n * are untouched. Consumers flip `jobs.backend: bullmq` with no code change —\n * the same `IJobOrchestrator` surface is satisfied.\n *\n * `jobId` (spec §Gotcha 1): BullMQ treats `:` as a Redis key separator and\n * consumers use `vendor:externalId`-shaped idempotency keys, so we derive the\n * `jobId` as `sha1(idempotencyKey)` — colon-safe and stable (same logical key\n * → same id → BullMQ-native dedup). When no dedupe key is configured we fall\n * back to the `job_run.id` (a fresh UUID), which is already colon-safe.\n */\nimport { createHash } from 'node:crypto';\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport { eq } from 'drizzle-orm';\n// `bullmq` is an OPTIONAL peer dependency. Only TYPE imports here — types are\n// erased at compile time and never resolve `'bullmq'` at runtime, so a\n// `drizzle`-only consumer who didn't install bullmq can still load this file\n// (it is statically imported by `jobs-domain.module.ts`). The VALUE\n// constructors (`Queue`, `FlowProducer`) are loaded lazily via `await\n// import('bullmq')` in `loadBullMq()` — mirrors\n// `event-bus.redis-backend.ts:createRedisClient`. See BULLMQ-1 §Lazy import.\nimport type { ConnectionOptions, FlowProducer, Queue } from 'bullmq';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, jobs, type JobDefinitionRow } from './job-orchestration.schema';\nimport { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend';\nimport type {\n CancelOptions,\n JobRun,\n StartOptions,\n} from './job-orchestrator.protocol';\nimport { JOBS_MULTI_TENANT } from './jobs-domain.tokens';\nimport {\n BULLMQ_CONNECTION,\n resolvePoolQueueName,\n type BullMqResolvedConfig,\n BULLMQ_RESOLVED_CONFIG,\n} from './bullmq.config';\n\n/**\n * Derive a colon-safe, stable BullMQ `jobId` from a logical idempotency key.\n *\n * SHA-1 over the raw key. Collision analysis (spec §Gotcha 1, resolved during\n * implementation): SHA-1's 160-bit space makes an accidental collision between\n * two *distinct* logical keys astronomically unlikely at any realistic job\n * volume (the birthday bound is ~2^80 keys before a 50% collision chance —\n * orders of magnitude beyond any job throughput). SHA-1's cryptographic\n * weakness is irrelevant here: there is no adversary forging idempotency keys,\n * and even a forged collision only deduplicates two jobs that the caller chose\n * to key identically. We therefore accept SHA-1 with no mitigation. The *same*\n * logical key intentionally maps to the *same* jobId — that is the dedup\n * mechanism, not a collision.\n */\nexport function sha1JobId(idempotencyKey: string): string {\n return createHash('sha1').update(idempotencyKey).digest('hex');\n}\n\n// Constructor types for the lazily-loaded `bullmq` value exports. Typed via\n// `typeof` the type-only imports so the cached ctors stay strongly typed\n// without a runtime `import`.\ntype QueueCtor = typeof import('bullmq').Queue;\ntype FlowProducerCtor = typeof import('bullmq').FlowProducer;\n\n@Injectable()\nexport class BullMQJobOrchestrator extends DrizzleJobOrchestrator {\n // TODO(logging-subsystem): swap to ILogger once ADR-028 lands\n private readonly bullLogger = new Logger(BullMQJobOrchestrator.name);\n\n /** Lazily-opened `Queue` handles, one per pool. */\n private readonly queues = new Map<string, Queue>();\n /** Single FlowProducer for parent/child hierarchies. Lazily opened. */\n private _flow: FlowProducer | null = null;\n\n /**\n * Cached `bullmq` value constructors, populated by `loadBullMq()` on first\n * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching\n * a queue). Kept off the import graph so a `drizzle`-only consumer never\n * resolves the optional `'bullmq'` package.\n */\n private QueueCtor: QueueCtor | null = null;\n private FlowProducerCtor: FlowProducerCtor | null = null;\n private bullMqLoad: Promise<void> | null = null;\n\n /**\n * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is\n * `private` (can't be redeclared even privately in a subclass), and the\n * spec forbids touching that file — so the subclass keeps its own handle\n * under a distinct name (same instance, passed through to `super`) for the\n * cancel-cascade snapshot + definition/run loads below.\n */\n private readonly bullDb: DrizzleClient;\n\n constructor(\n @Inject(DRIZZLE) db: DrizzleClient,\n @Inject(JOBS_MULTI_TENANT) multiTenant: boolean,\n @Inject(BULLMQ_CONNECTION) private readonly connection: ConnectionOptions,\n @Optional()\n @Inject(BULLMQ_RESOLVED_CONFIG)\n private readonly bullConfig: BullMqResolvedConfig | null = null,\n ) {\n super(db, multiTenant);\n this.bullDb = db;\n }\n\n /**\n * Lazily load the optional `bullmq` package and cache its value\n * constructors. Idempotent (single in-flight promise). Throws a friendly,\n * actionable error when the consumer selected `backend: 'bullmq'` but did\n * not install the package — mirrors `createRedisClient` in the redis event\n * backend. Must be `await`ed before any `queueFor`/`flow` access.\n */\n private async loadBullMq(): Promise<void> {\n if (this.QueueCtor && this.FlowProducerCtor) return;\n if (!this.bullMqLoad) {\n this.bullMqLoad = (async () => {\n try {\n const mod = await import('bullmq');\n this.QueueCtor = mod.Queue;\n this.FlowProducerCtor = mod.FlowProducer;\n } catch {\n throw new Error(\n 'BullMQ backend requires the \"bullmq\" package. Install it with: npm install bullmq',\n );\n }\n })();\n }\n await this.bullMqLoad;\n }\n\n /**\n * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await\n * loadBullMq()` first so `QueueCtor` is populated.\n */\n private queueFor(pool: string): Queue {\n if (!this.QueueCtor) {\n throw new Error('BullMQJobOrchestrator: queueFor called before loadBullMq()');\n }\n const name = resolvePoolQueueName(pool, this.bullConfig);\n let q = this.queues.get(name);\n if (!q) {\n q = new this.QueueCtor(name, { connection: this.connection });\n this.queues.set(name, q);\n }\n return q;\n }\n\n private flow(): FlowProducer {\n if (!this.FlowProducerCtor) {\n throw new Error('BullMQJobOrchestrator: flow called before loadBullMq()');\n }\n if (!this._flow) {\n this._flow = new this.FlowProducerCtor({ connection: this.connection });\n }\n return this._flow;\n }\n\n // ==========================================================================\n // start — Postgres insert (super) + BullMQ dispatch\n // ==========================================================================\n\n override async start(\n type: string,\n input: unknown,\n opts: StartOptions = {},\n tx?: DrizzleTransaction,\n ): Promise<JobRun> {\n // (1) Postgres remains source of truth — the Drizzle backend handles the\n // job-definition lookup, dedupe short-circuit, concurrency collision,\n // parent/root resolution, and the `job_run` INSERT. If dedupe\n // short-circuited it returns the incumbent row whose dispatch already\n // happened on the original start; we must not enqueue again.\n const run = await super.start(type, input, opts, tx);\n\n // Dedupe returned an existing run (its createdAt predates this call) —\n // BullMQ-native dedup already covered the dispatch. Skip re-enqueue.\n // We detect this by checking the run was freshly created in THIS call:\n // a brand-new run has status 'pending' and zero attempts AND its id is\n // not yet known to BullMQ. The cheapest reliable signal is the dedupe\n // path's contract: super.start returns the incumbent unchanged. Since we\n // cannot distinguish purely from the row, we rely on `jobId` idempotency\n // — re-adding with the same jobId is a no-op in BullMQ, so the enqueue is\n // safe to attempt unconditionally.\n\n await this.dispatch(run, type);\n return run;\n }\n\n /**\n * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a\n * `parentRunId` we attach it to the parent's existing BullMQ job through the\n * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in\n * its own graph. (The FlowProducer is reserved for whole-tree atomic\n * submits, exposed as an opt-in extension via `flowProducer()`; runtime\n * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the\n * correct primitive here.)\n *\n * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is\n * present (so the same logical key dedups), else the `job_run.id` UUID\n * (already colon-free).\n *\n * The domain `parentClosePolicy` cascade is still enforced in Postgres by\n * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,\n * not the authority.\n */\n private async dispatch(run: JobRun, type: string): Promise<void> {\n await this.loadBullMq();\n const def = await this.loadDefinition(type);\n const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;\n\n const jobOpts: Record<string, unknown> = {\n jobId,\n ...this.retryOpts(def),\n ...this.dedupeOpts(run, def),\n };\n\n if (run.parentRunId) {\n const parentRow = await this.loadRun(run.parentRunId);\n if (parentRow) {\n const parentJobId = parentRow.dedupeKey\n ? sha1JobId(parentRow.dedupeKey)\n : parentRow.id;\n jobOpts.parent = {\n id: parentJobId,\n queue: resolvePoolQueueName(parentRow.pool, this.bullConfig),\n };\n }\n }\n\n // The processor reads the authoritative input from `job_run`; the payload\n // carries the runId so it can load the row, plus type/input for logging.\n const payload = { runId: run.id, type, input: run.input };\n await this.queueFor(run.pool).add(type, payload, jobOpts);\n }\n\n /**\n * Opt-in extension (spec §Extensions): expose the FlowProducer for\n * consumers that want to submit a whole parent/child DAG atomically up\n * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —\n * code using it is not portable to the Drizzle backend. Async because it\n * lazily loads the optional `bullmq` package on first use.\n */\n async flowProducer(): Promise<FlowProducer> {\n await this.loadBullMq();\n return this.flow();\n }\n\n private retryOpts(def: JobDefinitionRow): {\n attempts?: number;\n backoff?: { type: 'fixed' | 'exponential'; delay: number };\n } {\n const policy = def.retryPolicy;\n if (!policy) return {};\n return {\n attempts: policy.attempts,\n backoff: {\n type: policy.backoff === 'exponential' ? 'exponential' : 'fixed',\n delay: policy.baseMs,\n },\n };\n }\n\n private dedupeOpts(\n run: JobRun,\n def: JobDefinitionRow,\n ): { deduplication?: { id: string; ttl?: number } } {\n if (!run.dedupeKey || !def.dedupeWindowMs) return {};\n return {\n deduplication: {\n id: sha1JobId(run.dedupeKey),\n ttl: def.dedupeWindowMs,\n },\n };\n }\n\n // ==========================================================================\n // cancel — Postgres cascade (super) + remove from queue\n // ==========================================================================\n\n override async cancel(runId: string, opts: CancelOptions = {}): Promise<void> {\n // Snapshot the subtree BEFORE the DB cascade flips rows to canceled, so we\n // can remove every affected BullMQ job. We read the target's rootRunId and\n // the non-terminal descendants the same way the Drizzle cascade does.\n const target = await this.loadRun(runId);\n\n await super.cancel(runId, opts);\n\n if (!target) return;\n await this.loadBullMq();\n // Remove the target's own queued job.\n await this.removeFromQueue(target);\n\n if (opts.cascade === false) return;\n\n // Remove descendants' queued jobs (the DB rows were just canceled by\n // super.cancel; we mirror that into BullMQ so workers don't pick them up).\n const descendants = await this.bullDb\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.rootRunId, target.rootRunId));\n for (const child of descendants) {\n if (child.id === runId) continue;\n await this.removeFromQueue(child as JobRun);\n }\n }\n\n private async removeFromQueue(run: JobRun): Promise<void> {\n const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;\n try {\n const job = await this.queueFor(run.pool).getJob(jobId);\n if (job) await job.remove();\n } catch (err) {\n // A job already moved to active/completed cannot always be removed;\n // the Postgres cancel is authoritative either way.\n this.bullLogger.warn(\n `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${(err as Error).message}`,\n );\n }\n }\n\n // ==========================================================================\n // replay — Postgres reset (super) + re-enqueue\n // ==========================================================================\n\n override async replay(runId: string): Promise<JobRun> {\n const run = await super.replay(runId);\n await this.dispatch(run, run.jobType);\n return run;\n }\n\n // ==========================================================================\n // Internals\n // ==========================================================================\n\n private async loadDefinition(type: string): Promise<JobDefinitionRow> {\n const [def] = await this.bullDb\n .select()\n .from(jobs)\n .where(eq(jobs.type, type))\n .limit(1);\n if (!def) {\n throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);\n }\n return def as JobDefinitionRow;\n }\n\n private async loadRun(id: string): Promise<JobRun | null> {\n const [row] = await this.bullDb\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, id))\n .limit(1);\n return (row as JobRun) ?? null;\n }\n\n /** Close all open queue + flow connections. Called on module destroy. */\n async closeConnections(): Promise<void> {\n for (const q of this.queues.values()) {\n await q.close().catch(() => undefined);\n }\n this.queues.clear();\n if (this._flow) {\n await this._flow.close().catch(() => undefined);\n this._flow = null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -25,8 +25,9 @@ declare function evaluateKeyTemplate(template: string, input: Record<string, unk
|
|
|
25
25
|
declare class DrizzleJobOrchestrator implements IJobOrchestrator {
|
|
26
26
|
private readonly db;
|
|
27
27
|
private readonly multiTenant;
|
|
28
|
+
private readonly listenNotify;
|
|
28
29
|
private readonly logger;
|
|
29
|
-
constructor(db: DrizzleClient, multiTenant: boolean);
|
|
30
|
+
constructor(db: DrizzleClient, multiTenant: boolean, listenNotify?: boolean);
|
|
30
31
|
/**
|
|
31
32
|
* JOB-8 — resolve `tenantId` for a mutating / targeted-read call.
|
|
32
33
|
* Returns the tenant value that should be written to the row (or compared
|
|
@@ -2,10 +2,11 @@ import {
|
|
|
2
2
|
DrizzleJobOrchestrator,
|
|
3
3
|
TERMINAL_STATUSES,
|
|
4
4
|
evaluateKeyTemplate
|
|
5
|
-
} from "../../../chunk-
|
|
5
|
+
} from "../../../chunk-OTR44OH6.js";
|
|
6
6
|
import "../../../chunk-T4BIIU5E.js";
|
|
7
|
-
import "../../../chunk-
|
|
7
|
+
import "../../../chunk-ZPL74UQN.js";
|
|
8
8
|
import "../../../chunk-OKXZ63IA.js";
|
|
9
|
+
import "../../../chunk-MYQIQ27N.js";
|
|
9
10
|
import "../../../chunk-GYGNEQSC.js";
|
|
10
11
|
import "../../../chunk-U64T4YZE.js";
|
|
11
12
|
import "../../../chunk-2E224ZSN.js";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MemoryJobOrchestrator
|
|
3
|
-
} from "../../../chunk-
|
|
3
|
+
} from "../../../chunk-BULPAAD3.js";
|
|
4
4
|
import "../../../chunk-PNZSGAB2.js";
|
|
5
5
|
import "../../../chunk-SNQ3TOWP.js";
|
|
6
6
|
import "../../../chunk-T4BIIU5E.js";
|
|
7
|
+
import "../../../chunk-ZPL74UQN.js";
|
|
7
8
|
import "../../../chunk-CO6LUM72.js";
|
|
8
|
-
import "../../../chunk-BIO6F7YI.js";
|
|
9
9
|
import "../../../chunk-GYGNEQSC.js";
|
|
10
10
|
import "../../../chunk-2E224ZSN.js";
|
|
11
11
|
export {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DrizzleJobRunService
|
|
3
|
-
} from "../../../chunk-
|
|
4
|
-
import "../../../chunk-L3LZWWSX.js";
|
|
3
|
+
} from "../../../chunk-VNBC3VXM.js";
|
|
5
4
|
import "../../../chunk-T4BIIU5E.js";
|
|
6
|
-
import "../../../chunk-
|
|
5
|
+
import "../../../chunk-L3LZWWSX.js";
|
|
6
|
+
import "../../../chunk-ZPL74UQN.js";
|
|
7
7
|
import "../../../chunk-OKXZ63IA.js";
|
|
8
8
|
import "../../../chunk-GYGNEQSC.js";
|
|
9
9
|
import "../../../chunk-U64T4YZE.js";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MemoryJobRunService
|
|
3
|
-
} from "../../../chunk-
|
|
4
|
-
import "../../../chunk-L3LZWWSX.js";
|
|
3
|
+
} from "../../../chunk-BHZP6LOV.js";
|
|
5
4
|
import "../../../chunk-SNQ3TOWP.js";
|
|
6
5
|
import "../../../chunk-T4BIIU5E.js";
|
|
7
|
-
import "../../../chunk-
|
|
6
|
+
import "../../../chunk-L3LZWWSX.js";
|
|
7
|
+
import "../../../chunk-ZPL74UQN.js";
|
|
8
8
|
import "../../../chunk-GYGNEQSC.js";
|
|
9
9
|
import "../../../chunk-2E224ZSN.js";
|
|
10
10
|
export {
|
|
@@ -32,6 +32,14 @@ interface JobWorkerOptions {
|
|
|
32
32
|
staleThresholdMs?: number;
|
|
33
33
|
/** Max ms to wait for in-flight drain on SIGTERM. Default 30_000. */
|
|
34
34
|
shutdownTimeoutMs?: number;
|
|
35
|
+
/**
|
|
36
|
+
* LISTEN-NOTIFY-1 — when true, hold a dedicated listener connection and
|
|
37
|
+
* LISTEN on `codegen_jobs_wake`. A notification naming this worker's `pool`
|
|
38
|
+
* triggers an immediate (debounced) claim cycle, so an enqueue is claimed in
|
|
39
|
+
* milliseconds instead of waiting for the next `pollIntervalMs` tick. Polling
|
|
40
|
+
* continues unchanged as the fallback heartbeat. Default false.
|
|
41
|
+
*/
|
|
42
|
+
listenNotify?: boolean;
|
|
35
43
|
}
|
|
36
44
|
declare const JOB_WORKER_OPTIONS: unique symbol;
|
|
37
45
|
declare const TERMINAL_STATUSES: JobRunRow['status'][];
|
|
@@ -154,8 +162,28 @@ declare class JobWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
154
162
|
private readonly staleSweeperIntervalMs;
|
|
155
163
|
private readonly staleThresholdMs;
|
|
156
164
|
private readonly shutdownTimeoutMs;
|
|
165
|
+
private readonly listenNotifyEnabled;
|
|
166
|
+
private notifyListener;
|
|
167
|
+
/** True while a wake-driven claim cycle is in flight (debounce gate). */
|
|
168
|
+
private wakeDraining;
|
|
169
|
+
/** A notify arrived mid-cycle → re-check once when the cycle ends. */
|
|
170
|
+
private wakeRecheckPending;
|
|
157
171
|
constructor(db: DrizzleClient, orchestrator: IJobOrchestrator, runService: IJobRunService, stepService: IJobStepService, options: JobWorkerOptions, moduleRef: ModuleRef);
|
|
158
172
|
onModuleInit(): void;
|
|
173
|
+
/**
|
|
174
|
+
* Wake handler — a `codegen_jobs_wake` notification arrived. Only payloads
|
|
175
|
+
* naming THIS worker's pool are relevant (other pools have their own workers).
|
|
176
|
+
* Debounced: if a claim cycle is already running we just flag a re-check so a
|
|
177
|
+
* burst of N enqueues collapses to at most one extra cycle (D3).
|
|
178
|
+
*/
|
|
179
|
+
private onWake;
|
|
180
|
+
/**
|
|
181
|
+
* Claim-until-empty on a wake. Unlike the interval `pollAndProcess` (one
|
|
182
|
+
* claim per tick), a wake drains greedily up to the concurrency ceiling so a
|
|
183
|
+
* burst that arrived together is dispatched without waiting for N ticks. The
|
|
184
|
+
* `wakeRecheckPending` flag coalesces notifies that land mid-drain.
|
|
185
|
+
*/
|
|
186
|
+
private drainOnWake;
|
|
159
187
|
onModuleDestroy(): Promise<void>;
|
|
160
188
|
private drainInFlight;
|
|
161
189
|
pollAndProcess(): Promise<void>;
|
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
buildStaleSweepQuery,
|
|
7
7
|
classifyError,
|
|
8
8
|
computeBackoff
|
|
9
|
-
} from "../../../chunk-
|
|
9
|
+
} from "../../../chunk-XDIIVIIK.js";
|
|
10
|
+
import "../../../chunk-ZPL74UQN.js";
|
|
10
11
|
import "../../../chunk-CO6LUM72.js";
|
|
11
|
-
import "../../../chunk-BIO6F7YI.js";
|
|
12
12
|
import "../../../chunk-OKXZ63IA.js";
|
|
13
|
+
import "../../../chunk-MYQIQ27N.js";
|
|
13
14
|
import "../../../chunk-GYGNEQSC.js";
|
|
14
15
|
import "../../../chunk-U64T4YZE.js";
|
|
15
16
|
import "../../../chunk-2E224ZSN.js";
|
|
@@ -2,23 +2,24 @@ import {
|
|
|
2
2
|
JOB_WORKER_MODULE_OPTIONS,
|
|
3
3
|
JobWorkerModule,
|
|
4
4
|
JobWorkerOrchestrator
|
|
5
|
-
} from "../../../chunk-
|
|
6
|
-
import "../../../chunk-
|
|
7
|
-
import "../../../chunk-
|
|
8
|
-
import "../../../chunk-FBGHYQIZ.js";
|
|
9
|
-
import "../../../chunk-RDVTWIYY.js";
|
|
10
|
-
import "../../../chunk-L3LZWWSX.js";
|
|
5
|
+
} from "../../../chunk-RUYLXR5F.js";
|
|
6
|
+
import "../../../chunk-XDIIVIIK.js";
|
|
7
|
+
import "../../../chunk-3RWMQC3K.js";
|
|
11
8
|
import "../../../chunk-DV4RV2DC.js";
|
|
12
|
-
import "../../../chunk-
|
|
13
|
-
import "../../../chunk-
|
|
14
|
-
import "../../../chunk-5Y7W3XR6.js";
|
|
15
|
-
import "../../../chunk-4RFHUZXU.js";
|
|
9
|
+
import "../../../chunk-OTR44OH6.js";
|
|
10
|
+
import "../../../chunk-BULPAAD3.js";
|
|
16
11
|
import "../../../chunk-PNZSGAB2.js";
|
|
12
|
+
import "../../../chunk-VNBC3VXM.js";
|
|
13
|
+
import "../../../chunk-BHZP6LOV.js";
|
|
17
14
|
import "../../../chunk-SNQ3TOWP.js";
|
|
18
15
|
import "../../../chunk-T4BIIU5E.js";
|
|
16
|
+
import "../../../chunk-L3LZWWSX.js";
|
|
17
|
+
import "../../../chunk-I6MVCB5A.js";
|
|
18
|
+
import "../../../chunk-RHVN6NA7.js";
|
|
19
|
+
import "../../../chunk-ZPL74UQN.js";
|
|
19
20
|
import "../../../chunk-CO6LUM72.js";
|
|
20
|
-
import "../../../chunk-BIO6F7YI.js";
|
|
21
21
|
import "../../../chunk-OKXZ63IA.js";
|
|
22
|
+
import "../../../chunk-MYQIQ27N.js";
|
|
22
23
|
import "../../../chunk-GYGNEQSC.js";
|
|
23
24
|
import "../../../chunk-U64T4YZE.js";
|
|
24
25
|
import "../../../chunk-2E224ZSN.js";
|
|
@@ -20,17 +20,22 @@ import './pool-config.loader.js';
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Drizzle backend extensions surface
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* Drizzle backend extensions surface (LISTEN-NOTIFY-1 wires both fields).
|
|
24
|
+
*
|
|
25
|
+
* - `listenNotify` → provided as `JOBS_LISTEN_NOTIFY` so the orchestrator emits
|
|
26
|
+
* in-tx `pg_notify` on enqueue, and threaded into each spawned `JobWorker`
|
|
27
|
+
* (which holds the listener connection). Off by default.
|
|
28
|
+
* - `pollIntervalMs` → threaded into the spawned `JobWorker`'s
|
|
29
|
+
* `JobWorkerOptions.pollIntervalMs` (the worker already honored this; it just
|
|
30
|
+
* never received a config value). Default 1000.
|
|
31
|
+
*
|
|
32
|
+
* Both run ALONGSIDE interval polling — `listenNotify` only adds an early wake;
|
|
33
|
+
* polling remains the durability heartbeat.
|
|
29
34
|
*/
|
|
30
35
|
interface DrizzleBackendExtensions {
|
|
31
36
|
/** Use Postgres LISTEN/NOTIFY to wake the polling loop. Default false. */
|
|
32
37
|
listenNotify?: boolean;
|
|
33
|
-
/** Polling interval
|
|
38
|
+
/** Polling interval (ms). Default 1000. */
|
|
34
39
|
pollIntervalMs?: number;
|
|
35
40
|
}
|
|
36
41
|
interface JobsDomainModuleOptions {
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
JobsDomainModule
|
|
3
|
-
} from "../../../chunk-
|
|
4
|
-
import "../../../chunk-FBGHYQIZ.js";
|
|
5
|
-
import "../../../chunk-RDVTWIYY.js";
|
|
6
|
-
import "../../../chunk-L3LZWWSX.js";
|
|
3
|
+
} from "../../../chunk-3RWMQC3K.js";
|
|
7
4
|
import "../../../chunk-DV4RV2DC.js";
|
|
8
|
-
import "../../../chunk-
|
|
9
|
-
import "../../../chunk-
|
|
10
|
-
import "../../../chunk-5Y7W3XR6.js";
|
|
11
|
-
import "../../../chunk-4RFHUZXU.js";
|
|
5
|
+
import "../../../chunk-OTR44OH6.js";
|
|
6
|
+
import "../../../chunk-BULPAAD3.js";
|
|
12
7
|
import "../../../chunk-PNZSGAB2.js";
|
|
8
|
+
import "../../../chunk-VNBC3VXM.js";
|
|
9
|
+
import "../../../chunk-BHZP6LOV.js";
|
|
13
10
|
import "../../../chunk-SNQ3TOWP.js";
|
|
14
11
|
import "../../../chunk-T4BIIU5E.js";
|
|
12
|
+
import "../../../chunk-L3LZWWSX.js";
|
|
13
|
+
import "../../../chunk-I6MVCB5A.js";
|
|
14
|
+
import "../../../chunk-RHVN6NA7.js";
|
|
15
|
+
import "../../../chunk-ZPL74UQN.js";
|
|
15
16
|
import "../../../chunk-CO6LUM72.js";
|
|
16
|
-
import "../../../chunk-BIO6F7YI.js";
|
|
17
17
|
import "../../../chunk-OKXZ63IA.js";
|
|
18
|
+
import "../../../chunk-MYQIQ27N.js";
|
|
18
19
|
import "../../../chunk-GYGNEQSC.js";
|
|
19
20
|
import "../../../chunk-U64T4YZE.js";
|
|
20
21
|
import "../../../chunk-2E224ZSN.js";
|
|
@@ -16,5 +16,17 @@ declare const JOB_STEP_SERVICE: unique symbol;
|
|
|
16
16
|
* targeted reads. See docs/specs/JOB-8.md.
|
|
17
17
|
*/
|
|
18
18
|
declare const JOBS_MULTI_TENANT: unique symbol;
|
|
19
|
+
/**
|
|
20
|
+
* LISTEN/NOTIFY wakeup opt-in flag (LISTEN-NOTIFY-1). Bound to
|
|
21
|
+
* `JobsDomainModule.forRoot({ extensions: { drizzle: { listenNotify } } })`,
|
|
22
|
+
* defaulting to `false`.
|
|
23
|
+
*
|
|
24
|
+
* When `true`, the Drizzle orchestrator emits an in-transaction
|
|
25
|
+
* `pg_notify(codegen_jobs_wake, <pool>)` on every `start()` INSERT so a worker
|
|
26
|
+
* with `listen_notify` enabled wakes the moment the enqueue commits. Off by
|
|
27
|
+
* default; polling is unchanged. The flag is read by `DrizzleJobOrchestrator`
|
|
28
|
+
* and by the bridge outbox drain hook (its wrapper `job_run` inserts notify too).
|
|
29
|
+
*/
|
|
30
|
+
declare const JOBS_LISTEN_NOTIFY: unique symbol;
|
|
19
31
|
|
|
20
|
-
export { JOBS_MULTI_TENANT, JOB_ORCHESTRATOR, JOB_RUN_SERVICE, JOB_STEP_SERVICE };
|
|
32
|
+
export { JOBS_LISTEN_NOTIFY, JOBS_MULTI_TENANT, JOB_ORCHESTRATOR, JOB_RUN_SERVICE, JOB_STEP_SERVICE };
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
JOBS_LISTEN_NOTIFY,
|
|
2
3
|
JOBS_MULTI_TENANT,
|
|
3
4
|
JOB_ORCHESTRATOR,
|
|
4
5
|
JOB_RUN_SERVICE,
|
|
5
6
|
JOB_STEP_SERVICE
|
|
6
|
-
} from "../../../chunk-
|
|
7
|
+
} from "../../../chunk-ZPL74UQN.js";
|
|
7
8
|
import "../../../chunk-GYGNEQSC.js";
|
|
8
9
|
import "../../../chunk-2E224ZSN.js";
|
|
9
10
|
export {
|
|
11
|
+
JOBS_LISTEN_NOTIFY,
|
|
10
12
|
JOBS_MULTI_TENANT,
|
|
11
13
|
JOB_ORCHESTRATOR,
|
|
12
14
|
JOB_RUN_SERVICE,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { DrizzleClient } from '../../types/drizzle.js';
|
|
2
|
+
import { DrizzleTransaction } from '../events/event-bus.protocol.js';
|
|
3
|
+
import 'drizzle-orm/node-postgres';
|
|
4
|
+
|
|
5
|
+
/** Channel the jobs worker LISTENs on; payload = pool name. */
|
|
6
|
+
declare const JOBS_WAKE_CHANNEL = "codegen_jobs_wake";
|
|
7
|
+
/** Channel the events drainer LISTENs on; payload = event pool (or ''). */
|
|
8
|
+
declare const EVENTS_WAKE_CHANNEL = "codegen_events_wake";
|
|
9
|
+
/**
|
|
10
|
+
* Emit an in-transaction `pg_notify`. Call with the SAME `tx`/client handle as
|
|
11
|
+
* the row write being announced so delivery is gated on commit. `payload` is a
|
|
12
|
+
* short plain string (a pool name); it is NOT JSON — the wake is a hint and the
|
|
13
|
+
* subsequent claim/drain query is authoritative. Channel names are framework
|
|
14
|
+
* constants (never user input), so the `set_config`-free literal-channel form is
|
|
15
|
+
* safe; the payload is bound as a parameter.
|
|
16
|
+
*/
|
|
17
|
+
declare function pgNotify(tx: DrizzleClient | DrizzleTransaction, channel: string, payload: string): Promise<void>;
|
|
18
|
+
/** Minimal structural view of the `pg` Client/PoolClient surface we touch. */
|
|
19
|
+
interface PgListenClient {
|
|
20
|
+
query(text: string): Promise<unknown>;
|
|
21
|
+
on(event: 'notification', cb: (msg: {
|
|
22
|
+
channel: string;
|
|
23
|
+
payload?: string;
|
|
24
|
+
}) => void): void;
|
|
25
|
+
on(event: 'error', cb: (err: Error) => void): void;
|
|
26
|
+
removeAllListeners?: (event?: string) => void;
|
|
27
|
+
release?: (err?: boolean) => void;
|
|
28
|
+
end?: () => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/** Minimal structural view of the `pg` Pool's `connect()`. */
|
|
31
|
+
interface PgPoolish {
|
|
32
|
+
connect(): Promise<PgListenClient>;
|
|
33
|
+
}
|
|
34
|
+
interface PgNotifyListenerOptions {
|
|
35
|
+
/** Channel to LISTEN on. */
|
|
36
|
+
channel: string;
|
|
37
|
+
/**
|
|
38
|
+
* The underlying `pg.Pool` — obtained from `drizzleClient.$client`. A
|
|
39
|
+
* dedicated `PoolClient` is checked out and held for the listener's lifetime
|
|
40
|
+
* (separate from the query pool so a slow query never delays a wake).
|
|
41
|
+
*/
|
|
42
|
+
pool: PgPoolish;
|
|
43
|
+
/**
|
|
44
|
+
* Called for every notification on `channel`, with the raw payload string
|
|
45
|
+
* (`''` when Postgres delivers an empty payload). The owner decides whether
|
|
46
|
+
* the payload is relevant (e.g. "is this one of my pools?") and debounces its
|
|
47
|
+
* own claim cycle.
|
|
48
|
+
*/
|
|
49
|
+
onNotify: (payload: string) => void;
|
|
50
|
+
/** Label used in log lines (e.g. 'jobs:interactive', 'events'). */
|
|
51
|
+
label: string;
|
|
52
|
+
backoffMinMs?: number;
|
|
53
|
+
backoffMaxMs?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Holds a dedicated listener connection and forwards notifications to `onNotify`.
|
|
57
|
+
* Reconnects with capped exponential backoff on drop; logs the first failure +
|
|
58
|
+
* the recovery exactly once each so a flapping connection doesn't flood logs.
|
|
59
|
+
*/
|
|
60
|
+
declare class PgNotifyListener {
|
|
61
|
+
private readonly opts;
|
|
62
|
+
private readonly logger;
|
|
63
|
+
private client;
|
|
64
|
+
private stopped;
|
|
65
|
+
private reconnectTimer;
|
|
66
|
+
private backoffMs;
|
|
67
|
+
private readonly backoffMinMs;
|
|
68
|
+
private readonly backoffMaxMs;
|
|
69
|
+
/** WARN-once gate so a flapping listener doesn't spam the log. */
|
|
70
|
+
private warnedDown;
|
|
71
|
+
constructor(opts: PgNotifyListenerOptions);
|
|
72
|
+
/** Begin listening. Idempotent-ish: a second call while connected is a no-op. */
|
|
73
|
+
start(): Promise<void>;
|
|
74
|
+
/** Stop listening + release the connection. Safe to call repeatedly. */
|
|
75
|
+
stop(): Promise<void>;
|
|
76
|
+
private connect;
|
|
77
|
+
/** Connection dropped after being established → reconnect. */
|
|
78
|
+
private handleDrop;
|
|
79
|
+
/** Initial / reconnect `connect()` threw. */
|
|
80
|
+
private handleConnectFailure;
|
|
81
|
+
private scheduleReconnect;
|
|
82
|
+
private releaseClient;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { EVENTS_WAKE_CHANNEL, JOBS_WAKE_CHANNEL, PgNotifyListener, type PgNotifyListenerOptions, pgNotify };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EVENTS_WAKE_CHANNEL,
|
|
3
|
+
JOBS_WAKE_CHANNEL,
|
|
4
|
+
PgNotifyListener,
|
|
5
|
+
pgNotify
|
|
6
|
+
} from "../../../chunk-MYQIQ27N.js";
|
|
7
|
+
import "../../../chunk-2E224ZSN.js";
|
|
8
|
+
export {
|
|
9
|
+
EVENTS_WAKE_CHANNEL,
|
|
10
|
+
JOBS_WAKE_CHANNEL,
|
|
11
|
+
PgNotifyListener,
|
|
12
|
+
pgNotify
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=pg-notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|