@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
package/dist/src/index.js
CHANGED
|
@@ -46,17 +46,18 @@ import {
|
|
|
46
46
|
validatePatternProject
|
|
47
47
|
} from "../chunk-32DOFN3T.js";
|
|
48
48
|
import "../chunk-KVOWSC5S.js";
|
|
49
|
-
import "../chunk-
|
|
49
|
+
import "../chunk-24WXSC3C.js";
|
|
50
50
|
import "../chunk-EO2QPOKH.js";
|
|
51
51
|
import "../chunk-PRWIX6UW.js";
|
|
52
|
-
import "../chunk-
|
|
52
|
+
import "../chunk-XWBK3XJK.js";
|
|
53
53
|
import "../chunk-AHV4GDYM.js";
|
|
54
|
-
import "../chunk-
|
|
54
|
+
import "../chunk-YK5JEVLX.js";
|
|
55
55
|
import "../chunk-SQDOBLBP.js";
|
|
56
56
|
import "../chunk-3NMCDN7L.js";
|
|
57
57
|
import "../chunk-4KNXX6TI.js";
|
|
58
58
|
import "../chunk-3CJFPU6Q.js";
|
|
59
|
-
import "../chunk-
|
|
59
|
+
import "../chunk-TDEHU73T.js";
|
|
60
|
+
import "../chunk-S7C6TIIF.js";
|
|
60
61
|
import "../chunk-MZ6GV4YF.js";
|
|
61
62
|
import "../chunk-LG57S2SC.js";
|
|
62
63
|
import "../chunk-HNWZFNKP.js";
|
|
@@ -64,7 +65,6 @@ import "../chunk-WWGYCIJX.js";
|
|
|
64
65
|
import "../chunk-4MF3HKJA.js";
|
|
65
66
|
import "../chunk-YLPAPPLW.js";
|
|
66
67
|
import "../chunk-36U5UGIO.js";
|
|
67
|
-
import "../chunk-S7C6TIIF.js";
|
|
68
68
|
import "../chunk-U64T4YZE.js";
|
|
69
69
|
import "../chunk-2E224ZSN.js";
|
|
70
70
|
export {
|
package/package.json
CHANGED
|
@@ -47,6 +47,8 @@ import type {
|
|
|
47
47
|
} from './bridge.protocol';
|
|
48
48
|
import { BRIDGE_DELIVERY_JOB_TYPE } from './bridge-delivery-handler';
|
|
49
49
|
import type { EventTypeName } from '../events/event-registry';
|
|
50
|
+
import { JOBS_LISTEN_NOTIFY } from '../jobs/jobs-domain.tokens';
|
|
51
|
+
import { JOBS_WAKE_CHANNEL, pgNotify } from '../jobs/pg-notify';
|
|
50
52
|
|
|
51
53
|
/** Reserved pools the wrapper rows route into; ADR-022 / ADR-024. */
|
|
52
54
|
const POOL_BY_DIRECTION: Record<string, string> = {
|
|
@@ -65,6 +67,15 @@ export class BridgeOutboxDrainHook implements IBridgeOutboxDrainHook {
|
|
|
65
67
|
@Optional()
|
|
66
68
|
@Inject(BRIDGE_REGISTRY)
|
|
67
69
|
private readonly registry: BridgeRegistry = {},
|
|
70
|
+
// LISTEN-NOTIFY-1 — when true, the wrapper `job_run` insert below emits an
|
|
71
|
+
// in-tx `pg_notify(codegen_jobs_wake, <wrapperPool>)` so the reserved-pool
|
|
72
|
+
// worker wakes the instant the per-event drain tx commits — otherwise the
|
|
73
|
+
// bridge hop alone would still cost a full poll interval. `@Optional()`
|
|
74
|
+
// defaulting false so the hook keeps working when jobs isn't installed
|
|
75
|
+
// (bridge can drive non-jobs consumers) or in vendored/test wiring.
|
|
76
|
+
@Optional()
|
|
77
|
+
@Inject(JOBS_LISTEN_NOTIFY)
|
|
78
|
+
private readonly listenNotify: boolean = false,
|
|
68
79
|
) {}
|
|
69
80
|
|
|
70
81
|
async processEvent(
|
|
@@ -209,6 +220,22 @@ export class BridgeOutboxDrainHook implements IBridgeOutboxDrainHook {
|
|
|
209
220
|
continue;
|
|
210
221
|
}
|
|
211
222
|
|
|
223
|
+
// LISTEN-NOTIFY-1 — the wrapper run is real and claimable; wake its
|
|
224
|
+
// reserved-pool worker on commit (D7). Same `tx` as the inserts above, so
|
|
225
|
+
// delivery is gated on the per-event drain tx committing. Best-effort: a
|
|
226
|
+
// notify failure is non-fatal (the reserved-pool worker still polls).
|
|
227
|
+
if (this.listenNotify) {
|
|
228
|
+
try {
|
|
229
|
+
await pgNotify(tx, JOBS_WAKE_CHANNEL, wrapperPool);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
this.logger.warn(
|
|
232
|
+
`pg_notify(${JOBS_WAKE_CHANNEL}, ${wrapperPool}) failed for ` +
|
|
233
|
+
`wrapper run ${wrapperRunId}: ${(err as Error).message} ` +
|
|
234
|
+
`(non-fatal — the reserved-pool worker still polls).`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
212
239
|
delivered++;
|
|
213
240
|
}
|
|
214
241
|
|
|
@@ -48,6 +48,11 @@ import { EVENTS_MODULE_OPTIONS } from './events.tokens';
|
|
|
48
48
|
import type { EventsModuleOptions } from './events.module';
|
|
49
49
|
import { BRIDGE_OUTBOX_DRAIN_HOOK } from '../bridge/bridge.tokens';
|
|
50
50
|
import type { IBridgeOutboxDrainHook } from '../bridge/bridge.protocol';
|
|
51
|
+
import {
|
|
52
|
+
EVENTS_WAKE_CHANNEL,
|
|
53
|
+
PgNotifyListener,
|
|
54
|
+
pgNotify,
|
|
55
|
+
} from '../jobs/pg-notify';
|
|
51
56
|
|
|
52
57
|
/** How long to wait between polling cycles (ms). */
|
|
53
58
|
const POLL_INTERVAL_MS = 1_000;
|
|
@@ -138,6 +143,14 @@ export class DrizzleEventBus implements IEventBus, IEventReadPort, OnModuleInit,
|
|
|
138
143
|
private readonly handlers = new Map<string, Set<(event: DomainEvent) => Promise<void>>>();
|
|
139
144
|
private readonly opts: EventsModuleOptions;
|
|
140
145
|
|
|
146
|
+
// LISTEN-NOTIFY-1 — dedicated wake listener + debounce state. `null` when
|
|
147
|
+
// `listenNotify` is off (the common case); polling is the only driver then.
|
|
148
|
+
private notifyListener: PgNotifyListener | null = null;
|
|
149
|
+
/** True while a wake-driven drain is in flight (debounce gate). */
|
|
150
|
+
private wakeDraining = false;
|
|
151
|
+
/** A notify arrived mid-drain → re-drain once when the current drain ends. */
|
|
152
|
+
private wakeRecheckPending = false;
|
|
153
|
+
|
|
141
154
|
constructor(
|
|
142
155
|
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
143
156
|
@Optional() @Inject(EVENTS_MODULE_OPTIONS) opts?: EventsModuleOptions,
|
|
@@ -168,6 +181,28 @@ export class DrizzleEventBus implements IEventBus, IEventReadPort, OnModuleInit,
|
|
|
168
181
|
async onModuleInit(): Promise<void> {
|
|
169
182
|
this.polling = true;
|
|
170
183
|
this.schedulePoll();
|
|
184
|
+
|
|
185
|
+
// LISTEN-NOTIFY-1 — start the wake listener ALONGSIDE the poll timer. A
|
|
186
|
+
// notify for one of this drainer's pools triggers an immediate drain; the
|
|
187
|
+
// interval timer above stays the durability heartbeat. Startup is
|
|
188
|
+
// fire-and-forget — a connect failure self-heals via the listener's backoff.
|
|
189
|
+
if (this.opts.listenNotify) {
|
|
190
|
+
const pool = (this.db as unknown as { $client?: unknown }).$client;
|
|
191
|
+
if (!pool || typeof (pool as { connect?: unknown }).connect !== 'function') {
|
|
192
|
+
this.logger.warn(
|
|
193
|
+
`listen_notify enabled but the Drizzle client exposes no pg Pool ` +
|
|
194
|
+
`($client.connect missing) — falling back to interval polling only.`,
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
this.notifyListener = new PgNotifyListener({
|
|
198
|
+
channel: EVENTS_WAKE_CHANNEL,
|
|
199
|
+
pool: pool as { connect(): Promise<never> },
|
|
200
|
+
label: 'events',
|
|
201
|
+
onNotify: (payload) => this.onWake(payload),
|
|
202
|
+
});
|
|
203
|
+
await this.notifyListener.start();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
171
206
|
}
|
|
172
207
|
|
|
173
208
|
async onModuleDestroy(): Promise<void> {
|
|
@@ -176,6 +211,45 @@ export class DrizzleEventBus implements IEventBus, IEventReadPort, OnModuleInit,
|
|
|
176
211
|
clearTimeout(this.pollTimer);
|
|
177
212
|
this.pollTimer = null;
|
|
178
213
|
}
|
|
214
|
+
if (this.notifyListener) {
|
|
215
|
+
try {
|
|
216
|
+
await this.notifyListener.stop();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
this.logger.error(`notify listener stop failed: ${err}`);
|
|
219
|
+
}
|
|
220
|
+
this.notifyListener = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Wake handler — a `codegen_events_wake` notification arrived. A pool-filtered
|
|
226
|
+
* drainer (`opts.pools` set) ignores payloads naming a pool it doesn't own; an
|
|
227
|
+
* all-pools drainer wakes for any. Debounced: a notify mid-drain just flags a
|
|
228
|
+
* re-check so a burst collapses to at most one extra drain (D3).
|
|
229
|
+
*/
|
|
230
|
+
private onWake(payload: string): void {
|
|
231
|
+
if (!this.polling) return;
|
|
232
|
+
const pools = this.opts.pools;
|
|
233
|
+
if (pools && pools.length > 0 && !pools.includes(payload)) return;
|
|
234
|
+
if (this.wakeDraining) {
|
|
235
|
+
this.wakeRecheckPending = true;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
void this.drainOnWake();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async drainOnWake(): Promise<void> {
|
|
242
|
+
this.wakeDraining = true;
|
|
243
|
+
try {
|
|
244
|
+
do {
|
|
245
|
+
this.wakeRecheckPending = false;
|
|
246
|
+
await this.processBatch();
|
|
247
|
+
} while (this.wakeRecheckPending && this.polling);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
this.logger.error(`wake drain error: ${err}`);
|
|
250
|
+
} finally {
|
|
251
|
+
this.wakeDraining = false;
|
|
252
|
+
}
|
|
179
253
|
}
|
|
180
254
|
|
|
181
255
|
// ============================================================================
|
|
@@ -185,16 +259,46 @@ export class DrizzleEventBus implements IEventBus, IEventReadPort, OnModuleInit,
|
|
|
185
259
|
async publish(event: DomainEvent, tx?: DrizzleTransaction): Promise<void> {
|
|
186
260
|
const client = (tx ?? this.db) as DrizzleClient;
|
|
187
261
|
const multiTenant = this.opts.multiTenant ?? false;
|
|
188
|
-
|
|
262
|
+
const values = toInsertValues(event, multiTenant);
|
|
263
|
+
await client.insert(domainEvents).values(values);
|
|
264
|
+
// LISTEN-NOTIFY-1 — wake the drainer on commit (D2: emitted through the same
|
|
265
|
+
// `client`, so a rolled-back publish emits no phantom wake). The pool is the
|
|
266
|
+
// payload; the drainer re-runs its own pool-filtered claim on wake.
|
|
267
|
+
await this.emitWakeNotify(client, [values.pool]);
|
|
189
268
|
}
|
|
190
269
|
|
|
191
270
|
async publishMany(events: DomainEvent[], tx?: DrizzleTransaction): Promise<void> {
|
|
192
271
|
if (events.length === 0) return;
|
|
193
272
|
const client = (tx ?? this.db) as DrizzleClient;
|
|
194
273
|
const multiTenant = this.opts.multiTenant ?? false;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
274
|
+
const valuesList = events.map((e) => toInsertValues(e, multiTenant));
|
|
275
|
+
await client.insert(domainEvents).values(valuesList);
|
|
276
|
+
// De-dup pools so a batch into one lane emits a single wake.
|
|
277
|
+
await this.emitWakeNotify(client, valuesList.map((v) => v.pool));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Emit one in-tx `pg_notify(codegen_events_wake, <pool>)` per distinct pool in
|
|
282
|
+
* the just-inserted batch. No-op unless `listenNotify` is on. Best-effort: a
|
|
283
|
+
* notify failure is non-fatal (interval polling still drains the rows), so we
|
|
284
|
+
* log + swallow rather than failing the publish.
|
|
285
|
+
*/
|
|
286
|
+
private async emitWakeNotify(
|
|
287
|
+
client: DrizzleClient,
|
|
288
|
+
pools: Array<string | null>,
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
if (!this.opts.listenNotify) return;
|
|
291
|
+
const distinct = new Set(pools.map((p) => p ?? ''));
|
|
292
|
+
for (const pool of distinct) {
|
|
293
|
+
try {
|
|
294
|
+
await pgNotify(client, EVENTS_WAKE_CHANNEL, pool);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
this.logger.warn(
|
|
297
|
+
`pg_notify(${EVENTS_WAKE_CHANNEL}, '${pool}') failed: ${err} ` +
|
|
298
|
+
`(non-fatal — interval polling still drains the outbox).`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
198
302
|
}
|
|
199
303
|
|
|
200
304
|
async findById(eventId: string): Promise<DomainEvent | null> {
|
|
@@ -96,6 +96,20 @@ export interface EventsModuleOptions {
|
|
|
96
96
|
* cannot stall change-event propagation (see ADR-022).
|
|
97
97
|
*/
|
|
98
98
|
pools?: string[];
|
|
99
|
+
/**
|
|
100
|
+
* LISTEN-NOTIFY-1 — when `true` (drizzle backend only), the drainer holds a
|
|
101
|
+
* dedicated listener connection and LISTENs on `codegen_events_wake`. Each
|
|
102
|
+
* `publish`/`publishMany` emits an in-tx `pg_notify(codegen_events_wake,
|
|
103
|
+
* <pool>)` so the drainer wakes the moment the publishing transaction commits,
|
|
104
|
+
* instead of waiting for the next poll tick. Polling continues unchanged as
|
|
105
|
+
* the fallback heartbeat; a lost notify degrades to poll latency, never to
|
|
106
|
+
* lost work. Defaults to `false`.
|
|
107
|
+
*
|
|
108
|
+
* Ignored by the memory + redis backends (memory dispatches inline; redis has
|
|
109
|
+
* its own fan-out). Requires a direct (non-transaction-pooler) connection —
|
|
110
|
+
* see the events/jobs config block re: PgBouncer.
|
|
111
|
+
*/
|
|
112
|
+
listenNotify?: boolean;
|
|
99
113
|
/**
|
|
100
114
|
* Multi-tenancy opt-in (EVT-6).
|
|
101
115
|
*
|
|
@@ -22,6 +22,7 @@ export {
|
|
|
22
22
|
JOB_RUN_SERVICE,
|
|
23
23
|
JOB_STEP_SERVICE,
|
|
24
24
|
JOBS_MULTI_TENANT,
|
|
25
|
+
JOBS_LISTEN_NOTIFY,
|
|
25
26
|
} from './jobs-domain.tokens';
|
|
26
27
|
|
|
27
28
|
// ─── JOB-2: orchestrator protocol ──────────────────────────────────────────
|
|
@@ -111,6 +112,15 @@ export {
|
|
|
111
112
|
buildStaleSweepQuery,
|
|
112
113
|
} from './job-worker';
|
|
113
114
|
export type { JobWorkerOptions } from './job-worker';
|
|
115
|
+
|
|
116
|
+
// ─── LISTEN-NOTIFY-1: Postgres LISTEN/NOTIFY wakeups ───────────────────────
|
|
117
|
+
export {
|
|
118
|
+
PgNotifyListener,
|
|
119
|
+
pgNotify,
|
|
120
|
+
JOBS_WAKE_CHANNEL,
|
|
121
|
+
EVENTS_WAKE_CHANNEL,
|
|
122
|
+
} from './pg-notify';
|
|
123
|
+
export type { PgNotifyListenerOptions } from './pg-notify';
|
|
114
124
|
export {
|
|
115
125
|
JobCollisionError,
|
|
116
126
|
JobNotReplayableError,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* No `job_queue` table, no executor port. See `docs/specs/JOB-3.md`.
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
|
-
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
10
|
+
import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
|
11
11
|
import { and, desc, eq, gt, inArray, isNotNull, ne, notInArray, sql } from 'drizzle-orm';
|
|
12
12
|
import type { DrizzleClient } from '../../types/drizzle';
|
|
13
13
|
import type { DrizzleTransaction } from '../events/event-bus.protocol';
|
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
MissingTenantIdError,
|
|
35
35
|
} from './jobs-errors';
|
|
36
36
|
import { jobSteps } from './job-orchestration.schema';
|
|
37
|
-
import { JOBS_MULTI_TENANT } from './jobs-domain.tokens';
|
|
37
|
+
import { JOBS_MULTI_TENANT, JOBS_LISTEN_NOTIFY } from './jobs-domain.tokens';
|
|
38
|
+
import { JOBS_WAKE_CHANNEL, pgNotify } from './pg-notify';
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Terminal statuses — transitions into these are final. Used by `cancel`
|
|
@@ -83,6 +84,13 @@ export class DrizzleJobOrchestrator implements IJobOrchestrator {
|
|
|
83
84
|
constructor(
|
|
84
85
|
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
85
86
|
@Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,
|
|
87
|
+
// LISTEN-NOTIFY-1 — when true, `start()` emits an in-tx
|
|
88
|
+
// `pg_notify(codegen_jobs_wake, <pool>)` so a `listen_notify` worker wakes
|
|
89
|
+
// on enqueue-commit. `@Optional()` defaulting to false so direct
|
|
90
|
+
// construction (integration tests not going through DI) keeps working.
|
|
91
|
+
@Optional()
|
|
92
|
+
@Inject(JOBS_LISTEN_NOTIFY)
|
|
93
|
+
private readonly listenNotify: boolean = false,
|
|
86
94
|
) {}
|
|
87
95
|
|
|
88
96
|
/**
|
|
@@ -251,6 +259,25 @@ export class DrizzleJobOrchestrator implements IJobOrchestrator {
|
|
|
251
259
|
})
|
|
252
260
|
.returning();
|
|
253
261
|
|
|
262
|
+
// LISTEN-NOTIFY-1 — wake a listening worker the instant this enqueue
|
|
263
|
+
// commits. Emitted through the SAME `client` (the caller's tx when one was
|
|
264
|
+
// passed, else the pool) so delivery is gated on commit — a rolled-back
|
|
265
|
+
// enqueue emits no phantom wake (D2). The pool name is the payload; the
|
|
266
|
+
// worker re-runs its own pool-filtered claim query on wake. Polling is the
|
|
267
|
+
// fallback, so a failed notify is non-fatal: log + continue.
|
|
268
|
+
if (this.listenNotify) {
|
|
269
|
+
const wakePool = (inserted as JobRunRow).pool;
|
|
270
|
+
try {
|
|
271
|
+
await pgNotify(client, JOBS_WAKE_CHANNEL, wakePool);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
this.logger.warn(
|
|
274
|
+
`pg_notify(${JOBS_WAKE_CHANNEL}, ${wakePool}) failed for run ` +
|
|
275
|
+
`${(inserted as JobRunRow).id}: ${(err as Error).message} ` +
|
|
276
|
+
`(non-fatal — interval polling still claims the run).`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
254
281
|
return inserted as JobRun;
|
|
255
282
|
}
|
|
256
283
|
|
|
@@ -245,11 +245,22 @@ export class JobWorkerOrchestrator implements OnModuleInit, OnModuleDestroy {
|
|
|
245
245
|
// naming; it MUST NOT be passed as the claim-filter pool, or the
|
|
246
246
|
// worker will never match any row and the pool silently never
|
|
247
247
|
// drains. See v0.4.4 fix notes.
|
|
248
|
+
// LISTEN-NOTIFY-1 — thread the drizzle extension knobs into each spawned
|
|
249
|
+
// worker. `pollIntervalMs` was always honored by JobWorker but never
|
|
250
|
+
// received a config value; `listenNotify` is the new wake opt-in. Only
|
|
251
|
+
// the drizzle backend reads these (bullmq has native wakeups + its own
|
|
252
|
+
// queue topology), so we ignore them under `backend: 'bullmq'`.
|
|
253
|
+
const drizzleExt =
|
|
254
|
+
backend === 'drizzle'
|
|
255
|
+
? this.options.domainModuleExtensions?.drizzle
|
|
256
|
+
: undefined;
|
|
248
257
|
const workerOptions: JobWorkerOptions = {
|
|
249
258
|
pool: poolName,
|
|
250
259
|
concurrency: def.concurrency,
|
|
251
260
|
shutdownTimeoutMs:
|
|
252
261
|
this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
262
|
+
pollIntervalMs: drizzleExt?.pollIntervalMs,
|
|
263
|
+
listenNotify: drizzleExt?.listenNotify,
|
|
253
264
|
};
|
|
254
265
|
const worker = this.options.workerFactory
|
|
255
266
|
? this.options.workerFactory(workerOptions)
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type SpawnChildOptions,
|
|
38
38
|
type StepOptions,
|
|
39
39
|
} from './job-handler.base';
|
|
40
|
+
import { JOBS_WAKE_CHANNEL, PgNotifyListener } from './pg-notify';
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Options accepted by `JobWorker`. JOB-5 threads these through module
|
|
@@ -59,6 +60,14 @@ export interface JobWorkerOptions {
|
|
|
59
60
|
staleThresholdMs?: number;
|
|
60
61
|
/** Max ms to wait for in-flight drain on SIGTERM. Default 30_000. */
|
|
61
62
|
shutdownTimeoutMs?: number;
|
|
63
|
+
/**
|
|
64
|
+
* LISTEN-NOTIFY-1 — when true, hold a dedicated listener connection and
|
|
65
|
+
* LISTEN on `codegen_jobs_wake`. A notification naming this worker's `pool`
|
|
66
|
+
* triggers an immediate (debounced) claim cycle, so an enqueue is claimed in
|
|
67
|
+
* milliseconds instead of waiting for the next `pollIntervalMs` tick. Polling
|
|
68
|
+
* continues unchanged as the fallback heartbeat. Default false.
|
|
69
|
+
*/
|
|
70
|
+
listenNotify?: boolean;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
// ADR-037: namespaced `Symbol.for(...)` (via `tokenKey()`) — matches by value
|
|
@@ -192,6 +201,15 @@ export class JobWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
192
201
|
private readonly staleThresholdMs: number;
|
|
193
202
|
private readonly shutdownTimeoutMs: number;
|
|
194
203
|
|
|
204
|
+
// LISTEN-NOTIFY-1 — dedicated listener + debounce state. `null` when
|
|
205
|
+
// `listenNotify` is off (the common case); polling is the only driver then.
|
|
206
|
+
private readonly listenNotifyEnabled: boolean;
|
|
207
|
+
private notifyListener: PgNotifyListener | null = null;
|
|
208
|
+
/** True while a wake-driven claim cycle is in flight (debounce gate). */
|
|
209
|
+
private wakeDraining = false;
|
|
210
|
+
/** A notify arrived mid-cycle → re-check once when the cycle ends. */
|
|
211
|
+
private wakeRecheckPending = false;
|
|
212
|
+
|
|
195
213
|
constructor(
|
|
196
214
|
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
197
215
|
@Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,
|
|
@@ -206,6 +224,7 @@ export class JobWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
206
224
|
this.staleThresholdMs = options.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
|
|
207
225
|
this.shutdownTimeoutMs =
|
|
208
226
|
options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
227
|
+
this.listenNotifyEnabled = options.listenNotify ?? false;
|
|
209
228
|
|
|
210
229
|
this.sigtermHandler = () => {
|
|
211
230
|
if (this.sigtermHandled) return;
|
|
@@ -227,6 +246,74 @@ export class JobWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
227
246
|
void this.sweepStaleClaims();
|
|
228
247
|
}, this.staleSweeperIntervalMs);
|
|
229
248
|
process.on('SIGTERM', this.sigtermHandler);
|
|
249
|
+
|
|
250
|
+
// LISTEN-NOTIFY-1 — start the wake listener ALONGSIDE the poll timer (never
|
|
251
|
+
// instead). A notify for this worker's pool drives an immediate claim cycle;
|
|
252
|
+
// the interval timer above stays the durability heartbeat. Listener startup
|
|
253
|
+
// is fire-and-forget: a connect failure self-heals via the listener's own
|
|
254
|
+
// backoff, and until it's up the poll loop is the sole driver.
|
|
255
|
+
if (this.listenNotifyEnabled) {
|
|
256
|
+
// The DRIZZLE provider wraps a `pg.Pool`, exposed by drizzle as `$client`.
|
|
257
|
+
const pool = (this.db as unknown as { $client?: unknown }).$client;
|
|
258
|
+
if (!pool || typeof (pool as { connect?: unknown }).connect !== 'function') {
|
|
259
|
+
this.logger.warn(
|
|
260
|
+
`listen_notify enabled but the Drizzle client exposes no pg Pool ` +
|
|
261
|
+
`($client.connect missing) — falling back to interval polling only.`,
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
this.notifyListener = new PgNotifyListener({
|
|
265
|
+
channel: JOBS_WAKE_CHANNEL,
|
|
266
|
+
pool: pool as { connect(): Promise<never> },
|
|
267
|
+
label: `jobs:${this.options.pool}`,
|
|
268
|
+
onNotify: (payload) => this.onWake(payload),
|
|
269
|
+
});
|
|
270
|
+
void this.notifyListener.start();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Wake handler — a `codegen_jobs_wake` notification arrived. Only payloads
|
|
277
|
+
* naming THIS worker's pool are relevant (other pools have their own workers).
|
|
278
|
+
* Debounced: if a claim cycle is already running we just flag a re-check so a
|
|
279
|
+
* burst of N enqueues collapses to at most one extra cycle (D3).
|
|
280
|
+
*/
|
|
281
|
+
private onWake(payload: string): void {
|
|
282
|
+
if (this.shuttingDown) return;
|
|
283
|
+
if (payload !== this.options.pool) return;
|
|
284
|
+
if (this.wakeDraining) {
|
|
285
|
+
this.wakeRecheckPending = true;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
void this.drainOnWake();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Claim-until-empty on a wake. Unlike the interval `pollAndProcess` (one
|
|
293
|
+
* claim per tick), a wake drains greedily up to the concurrency ceiling so a
|
|
294
|
+
* burst that arrived together is dispatched without waiting for N ticks. The
|
|
295
|
+
* `wakeRecheckPending` flag coalesces notifies that land mid-drain.
|
|
296
|
+
*/
|
|
297
|
+
private async drainOnWake(): Promise<void> {
|
|
298
|
+
this.wakeDraining = true;
|
|
299
|
+
try {
|
|
300
|
+
do {
|
|
301
|
+
this.wakeRecheckPending = false;
|
|
302
|
+
// Claim while there's capacity; pollAndProcess no-ops at the ceiling.
|
|
303
|
+
let progressed = true;
|
|
304
|
+
while (
|
|
305
|
+
progressed &&
|
|
306
|
+
!this.shuttingDown &&
|
|
307
|
+
this.inFlight.size < this.options.concurrency
|
|
308
|
+
) {
|
|
309
|
+
const before = this.inFlight.size;
|
|
310
|
+
await this.pollAndProcess();
|
|
311
|
+
progressed = this.inFlight.size > before;
|
|
312
|
+
}
|
|
313
|
+
} while (this.wakeRecheckPending && !this.shuttingDown);
|
|
314
|
+
} finally {
|
|
315
|
+
this.wakeDraining = false;
|
|
316
|
+
}
|
|
230
317
|
}
|
|
231
318
|
|
|
232
319
|
async onModuleDestroy(): Promise<void> {
|
|
@@ -246,6 +333,17 @@ export class JobWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
246
333
|
}
|
|
247
334
|
process.removeListener('SIGTERM', this.sigtermHandler);
|
|
248
335
|
|
|
336
|
+
// LISTEN-NOTIFY-1 — release the listener connection so the process can exit
|
|
337
|
+
// cleanly. Best-effort; a failure here doesn't block the drain.
|
|
338
|
+
if (this.notifyListener) {
|
|
339
|
+
try {
|
|
340
|
+
await this.notifyListener.stop();
|
|
341
|
+
} catch (err) {
|
|
342
|
+
this.logger.error(`notify listener stop failed: ${(err as Error).message}`);
|
|
343
|
+
}
|
|
344
|
+
this.notifyListener = null;
|
|
345
|
+
}
|
|
346
|
+
|
|
249
347
|
await this.drainInFlight();
|
|
250
348
|
|
|
251
349
|
// Any rows still `running` past timeout → release back to pending.
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
JOB_RUN_SERVICE,
|
|
22
22
|
JOB_STEP_SERVICE,
|
|
23
23
|
JOBS_MULTI_TENANT,
|
|
24
|
+
JOBS_LISTEN_NOTIFY,
|
|
24
25
|
} from './jobs-domain.tokens';
|
|
25
26
|
import { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend';
|
|
26
27
|
import { DrizzleJobRunService } from './job-run-service.drizzle-backend';
|
|
@@ -41,17 +42,22 @@ import {
|
|
|
41
42
|
} from './bullmq.config';
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
* Drizzle backend extensions surface
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
45
|
+
* Drizzle backend extensions surface (LISTEN-NOTIFY-1 wires both fields).
|
|
46
|
+
*
|
|
47
|
+
* - `listenNotify` → provided as `JOBS_LISTEN_NOTIFY` so the orchestrator emits
|
|
48
|
+
* in-tx `pg_notify` on enqueue, and threaded into each spawned `JobWorker`
|
|
49
|
+
* (which holds the listener connection). Off by default.
|
|
50
|
+
* - `pollIntervalMs` → threaded into the spawned `JobWorker`'s
|
|
51
|
+
* `JobWorkerOptions.pollIntervalMs` (the worker already honored this; it just
|
|
52
|
+
* never received a config value). Default 1000.
|
|
53
|
+
*
|
|
54
|
+
* Both run ALONGSIDE interval polling — `listenNotify` only adds an early wake;
|
|
55
|
+
* polling remains the durability heartbeat.
|
|
50
56
|
*/
|
|
51
57
|
export interface DrizzleBackendExtensions {
|
|
52
58
|
/** Use Postgres LISTEN/NOTIFY to wake the polling loop. Default false. */
|
|
53
59
|
listenNotify?: boolean;
|
|
54
|
-
/** Polling interval
|
|
60
|
+
/** Polling interval (ms). Default 1000. */
|
|
55
61
|
pollIntervalMs?: number;
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -79,6 +85,11 @@ export interface JobsDomainModuleOptions {
|
|
|
79
85
|
export class JobsDomainModule {
|
|
80
86
|
static forRoot(opts: JobsDomainModuleOptions): DynamicModule {
|
|
81
87
|
const multiTenant = opts.multiTenant ?? false;
|
|
88
|
+
// LISTEN-NOTIFY-1 — drizzle-only extension. `listen_notify` is meaningless
|
|
89
|
+
// for memory (no DB) and redundant for bullmq (native wakeups); only the
|
|
90
|
+
// drizzle backend's orchestrator reads it.
|
|
91
|
+
const listenNotify =
|
|
92
|
+
opts.backend === 'drizzle' && Boolean(opts.extensions?.drizzle?.listenNotify);
|
|
82
93
|
|
|
83
94
|
const providers: Provider[] = [
|
|
84
95
|
// JOB-8 — boolean provider consumed by the four service-layer backends.
|
|
@@ -87,6 +98,9 @@ export class JobsDomainModule {
|
|
|
87
98
|
// the value is `false`. See `jobs-domain.tokens.ts` for the claim-loop
|
|
88
99
|
// cross-tenant-by-design decision.
|
|
89
100
|
{ provide: JOBS_MULTI_TENANT, useValue: multiTenant },
|
|
101
|
+
// LISTEN-NOTIFY-1 — always provided so the orchestrator's `@Inject`
|
|
102
|
+
// resolves; the orchestrator skips the `pg_notify` emit when `false`.
|
|
103
|
+
{ provide: JOBS_LISTEN_NOTIFY, useValue: listenNotify },
|
|
90
104
|
];
|
|
91
105
|
|
|
92
106
|
if (opts.backend === 'memory') {
|
|
@@ -144,6 +158,7 @@ export class JobsDomainModule {
|
|
|
144
158
|
JOB_RUN_SERVICE,
|
|
145
159
|
JOB_STEP_SERVICE,
|
|
146
160
|
JOBS_MULTI_TENANT,
|
|
161
|
+
JOBS_LISTEN_NOTIFY,
|
|
147
162
|
];
|
|
148
163
|
// BULLMQ-1 — only export the BullMQ tokens when they were actually
|
|
149
164
|
// provided. Nest throws "exported but not provided" otherwise. Exported so
|
|
@@ -31,3 +31,16 @@ export const JOB_STEP_SERVICE = Symbol.for(tokenKey('jobs', 'step-service'));
|
|
|
31
31
|
* targeted reads. See docs/specs/JOB-8.md.
|
|
32
32
|
*/
|
|
33
33
|
export const JOBS_MULTI_TENANT = Symbol.for(tokenKey('jobs', 'multi-tenant'));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* LISTEN/NOTIFY wakeup opt-in flag (LISTEN-NOTIFY-1). Bound to
|
|
37
|
+
* `JobsDomainModule.forRoot({ extensions: { drizzle: { listenNotify } } })`,
|
|
38
|
+
* defaulting to `false`.
|
|
39
|
+
*
|
|
40
|
+
* When `true`, the Drizzle orchestrator emits an in-transaction
|
|
41
|
+
* `pg_notify(codegen_jobs_wake, <pool>)` on every `start()` INSERT so a worker
|
|
42
|
+
* with `listen_notify` enabled wakes the moment the enqueue commits. Off by
|
|
43
|
+
* default; polling is unchanged. The flag is read by `DrizzleJobOrchestrator`
|
|
44
|
+
* and by the bridge outbox drain hook (its wrapper `job_run` inserts notify too).
|
|
45
|
+
*/
|
|
46
|
+
export const JOBS_LISTEN_NOTIFY = Symbol.for(tokenKey('jobs', 'listen-notify'));
|