@pattern-stack/codegen 0.15.3 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/consumer-skills/integration/change-sources-and-sinks.md +1 -1
- package/dist/{chunk-GCYKMF22.js → chunk-24WXSC3C.js} +6 -6
- package/dist/{chunk-32DOFN3T.js → chunk-2WDX6I7T.js} +2 -2
- package/dist/{chunk-WWGYCIJX.js → chunk-43SBT72G.js} +2 -2
- package/dist/{chunk-FBGHYQIZ.js → chunk-5LXOJGO2.js} +6 -6
- package/dist/{chunk-32BMMV4H.js → chunk-5RT7JGKT.js} +5 -5
- package/dist/{chunk-3NMCDN7L.js → chunk-5TK7MEN4.js} +2 -2
- package/dist/chunk-5TK7MEN4.js.map +1 -0
- package/dist/{chunk-4H3PETLM.js → chunk-AYC2HEAL.js} +12 -9
- package/dist/chunk-AYC2HEAL.js.map +1 -0
- package/dist/{chunk-27ETSJ2X.js → chunk-COGHTKXY.js} +2 -2
- package/dist/{chunk-IYNSRIGR.js → chunk-CRBVI4GE.js} +5 -5
- package/dist/{chunk-J7JMVS2B.js → chunk-CZQUOIDY.js} +4 -4
- package/dist/{chunk-O37C3YE6.js → chunk-DGYTSCKN.js} +14 -8
- package/dist/chunk-DGYTSCKN.js.map +1 -0
- package/dist/{chunk-L7BNNRGI.js → chunk-DLG62MQY.js} +26 -6
- package/dist/chunk-DLG62MQY.js.map +1 -0
- package/dist/{chunk-TNXH7BJS.js → chunk-E45CSC33.js} +2 -2
- package/dist/{chunk-4JLJYWJC.js → chunk-H6FO2ZDJ.js} +99 -11
- package/dist/chunk-H6FO2ZDJ.js.map +1 -0
- package/dist/{chunk-5Y7W3XR6.js → chunk-IT6FRTEW.js} +30 -11
- package/dist/chunk-IT6FRTEW.js.map +1 -0
- package/dist/{chunk-RC23QROE.js → chunk-JM3T27ZW.js} +78 -4
- package/dist/chunk-JM3T27ZW.js.map +1 -0
- 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-YTN6BKWA.js → chunk-NXNVTXKG.js} +5 -5
- package/dist/{chunk-RDVTWIYY.js → chunk-QSJ3J4HE.js} +5 -5
- package/dist/{chunk-4MVGAMUA.js → chunk-RUSUZZAF.js} +4 -4
- package/dist/{chunk-4RFHUZXU.js → chunk-T4YJRD22.js} +4 -4
- package/dist/{chunk-7YGORYZD.js → chunk-T6C4LFLC.js} +4 -4
- package/dist/{chunk-OGIZXGPY.js → chunk-TDEHU73T.js} +4 -4
- package/dist/{chunk-YLPAPPLW.js → chunk-TIZXQU26.js} +36 -9
- package/dist/chunk-TIZXQU26.js.map +1 -0
- package/dist/{chunk-EOLLMEAH.js → chunk-TKVTEUBD.js} +3 -3
- package/dist/chunk-TKVTEUBD.js.map +1 -0
- package/dist/{chunk-YPWODKD5.js → chunk-W2UIDI3R.js} +5 -5
- package/dist/chunk-W4HOHZVF.js +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/subsystems/analytics/analytics.module.js +2 -2
- package/dist/runtime/subsystems/analytics/index.js +4 -4
- package/dist/runtime/subsystems/auth/auth.module.js +1 -1
- package/dist/runtime/subsystems/auth/index.js +7 -7
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +3 -3
- package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +1 -1
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.d.ts +2 -1
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +6 -5
- package/dist/runtime/subsystems/bridge/bridge.module.js +16 -15
- package/dist/runtime/subsystems/bridge/event-flow.service.js +3 -3
- package/dist/runtime/subsystems/bridge/index.js +16 -15
- 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 +88 -87
- package/dist/runtime/subsystems/integration/build-change-source.js +2 -2
- package/dist/runtime/subsystems/integration/detection-config.schema.d.ts +23 -15
- package/dist/runtime/subsystems/integration/detection-config.schema.js +1 -1
- package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
- package/dist/runtime/subsystems/integration/index.js +17 -17
- 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/integration/webhook-change-source.d.ts +36 -6
- package/dist/runtime/subsystems/integration/webhook-change-source.js +1 -1
- package/dist/runtime/subsystems/jobs/index.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/index.js +42 -30
- 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 +4 -3
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +3 -3
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +3 -3
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -3
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +28 -0
- package/dist/runtime/subsystems/jobs/job-worker.js +4 -3
- package/dist/runtime/subsystems/jobs/job-worker.module.js +11 -10
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +12 -7
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +9 -8
- 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 +53 -15
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +11 -11
- package/dist/src/index.js +9 -9
- 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/integration/detection-config.schema.ts +64 -54
- package/runtime/subsystems/integration/webhook-change-source.ts +187 -133
- 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-3NMCDN7L.js.map +0 -1
- 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-YLPAPPLW.js.map +0 -1
- /package/dist/{chunk-GCYKMF22.js.map → chunk-24WXSC3C.js.map} +0 -0
- /package/dist/{chunk-32DOFN3T.js.map → chunk-2WDX6I7T.js.map} +0 -0
- /package/dist/{chunk-WWGYCIJX.js.map → chunk-43SBT72G.js.map} +0 -0
- /package/dist/{chunk-FBGHYQIZ.js.map → chunk-5LXOJGO2.js.map} +0 -0
- /package/dist/{chunk-32BMMV4H.js.map → chunk-5RT7JGKT.js.map} +0 -0
- /package/dist/{chunk-27ETSJ2X.js.map → chunk-COGHTKXY.js.map} +0 -0
- /package/dist/{chunk-IYNSRIGR.js.map → chunk-CRBVI4GE.js.map} +0 -0
- /package/dist/{chunk-J7JMVS2B.js.map → chunk-CZQUOIDY.js.map} +0 -0
- /package/dist/{chunk-TNXH7BJS.js.map → chunk-E45CSC33.js.map} +0 -0
- /package/dist/{chunk-Z7PQCAVK.js.map → chunk-LQ6PYFU6.js.map} +0 -0
- /package/dist/{chunk-YTN6BKWA.js.map → chunk-NXNVTXKG.js.map} +0 -0
- /package/dist/{chunk-RDVTWIYY.js.map → chunk-QSJ3J4HE.js.map} +0 -0
- /package/dist/{chunk-4MVGAMUA.js.map → chunk-RUSUZZAF.js.map} +0 -0
- /package/dist/{chunk-4RFHUZXU.js.map → chunk-T4YJRD22.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-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.d.ts
CHANGED
|
@@ -1681,11 +1681,11 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
1681
1681
|
}>, z.ZodObject<{
|
|
1682
1682
|
mode: z.ZodLiteral<"webhook">;
|
|
1683
1683
|
webhook: z.ZodObject<{
|
|
1684
|
-
eventIdField: z.ZodString
|
|
1684
|
+
eventIdField: z.ZodOptional<z.ZodString>;
|
|
1685
1685
|
}, "strip", z.ZodTypeAny, {
|
|
1686
|
-
eventIdField
|
|
1686
|
+
eventIdField?: string | undefined;
|
|
1687
1687
|
}, {
|
|
1688
|
-
eventIdField
|
|
1688
|
+
eventIdField?: string | undefined;
|
|
1689
1689
|
}>;
|
|
1690
1690
|
mapping: z.ZodArray<z.ZodObject<{
|
|
1691
1691
|
source: z.ZodString;
|
|
@@ -1715,7 +1715,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
1715
1715
|
}>, "many">>;
|
|
1716
1716
|
}, "strip", z.ZodTypeAny, {
|
|
1717
1717
|
webhook: {
|
|
1718
|
-
eventIdField
|
|
1718
|
+
eventIdField?: string | undefined;
|
|
1719
1719
|
};
|
|
1720
1720
|
mode: "webhook";
|
|
1721
1721
|
mapping: {
|
|
@@ -1730,7 +1730,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
1730
1730
|
}[];
|
|
1731
1731
|
}, {
|
|
1732
1732
|
webhook: {
|
|
1733
|
-
eventIdField
|
|
1733
|
+
eventIdField?: string | undefined;
|
|
1734
1734
|
};
|
|
1735
1735
|
mode: "webhook";
|
|
1736
1736
|
mapping: {
|
|
@@ -2156,7 +2156,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2156
2156
|
}[];
|
|
2157
2157
|
} | {
|
|
2158
2158
|
webhook: {
|
|
2159
|
-
eventIdField
|
|
2159
|
+
eventIdField?: string | undefined;
|
|
2160
2160
|
};
|
|
2161
2161
|
mode: "webhook";
|
|
2162
2162
|
mapping: {
|
|
@@ -2366,7 +2366,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2366
2366
|
}[] | undefined;
|
|
2367
2367
|
} | {
|
|
2368
2368
|
webhook: {
|
|
2369
|
-
eventIdField
|
|
2369
|
+
eventIdField?: string | undefined;
|
|
2370
2370
|
};
|
|
2371
2371
|
mode: "webhook";
|
|
2372
2372
|
mapping: {
|
|
@@ -2576,7 +2576,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2576
2576
|
}[];
|
|
2577
2577
|
} | {
|
|
2578
2578
|
webhook: {
|
|
2579
|
-
eventIdField
|
|
2579
|
+
eventIdField?: string | undefined;
|
|
2580
2580
|
};
|
|
2581
2581
|
mode: "webhook";
|
|
2582
2582
|
mapping: {
|
|
@@ -2786,7 +2786,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2786
2786
|
}[] | undefined;
|
|
2787
2787
|
} | {
|
|
2788
2788
|
webhook: {
|
|
2789
|
-
eventIdField
|
|
2789
|
+
eventIdField?: string | undefined;
|
|
2790
2790
|
};
|
|
2791
2791
|
mode: "webhook";
|
|
2792
2792
|
mapping: {
|
|
@@ -2996,7 +2996,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2996
2996
|
}[];
|
|
2997
2997
|
} | {
|
|
2998
2998
|
webhook: {
|
|
2999
|
-
eventIdField
|
|
2999
|
+
eventIdField?: string | undefined;
|
|
3000
3000
|
};
|
|
3001
3001
|
mode: "webhook";
|
|
3002
3002
|
mapping: {
|
|
@@ -3206,7 +3206,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
3206
3206
|
}[] | undefined;
|
|
3207
3207
|
} | {
|
|
3208
3208
|
webhook: {
|
|
3209
|
-
eventIdField
|
|
3209
|
+
eventIdField?: string | undefined;
|
|
3210
3210
|
};
|
|
3211
3211
|
mode: "webhook";
|
|
3212
3212
|
mapping: {
|
package/dist/src/index.js
CHANGED
|
@@ -44,27 +44,27 @@ import {
|
|
|
44
44
|
validateOrchestrationProject,
|
|
45
45
|
validatePatternComposition,
|
|
46
46
|
validatePatternProject
|
|
47
|
-
} from "../chunk-
|
|
47
|
+
} from "../chunk-2WDX6I7T.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
|
-
import "../chunk-
|
|
56
|
+
import "../chunk-5TK7MEN4.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";
|
|
63
|
-
import "../chunk-
|
|
64
|
+
import "../chunk-43SBT72G.js";
|
|
64
65
|
import "../chunk-4MF3HKJA.js";
|
|
65
|
-
import "../chunk-
|
|
66
|
+
import "../chunk-TIZXQU26.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,10 +22,12 @@
|
|
|
22
22
|
* primitive while emitting `Change<T>.source = 'cdc'`. Long-lived
|
|
23
23
|
* streaming CDC (SFDC Pub-Sub, Debezium) is a separate primitive
|
|
24
24
|
* deferred to #226-8.
|
|
25
|
-
* - `webhook` mode
|
|
26
|
-
*
|
|
25
|
+
* - `webhook` mode's `eventIdField` is optional: `WebhookChangeSource<T>`
|
|
26
|
+
* prefers an `eventId` yielded by the queue iterator and falls back to the
|
|
27
|
+
* `eventIdField` record extraction (precedence: yielded eventId >
|
|
28
|
+
* eventIdField extraction > undefined dedupKey).
|
|
27
29
|
*/
|
|
28
|
-
import { z } from
|
|
30
|
+
import { z } from "zod";
|
|
29
31
|
|
|
30
32
|
// ============================================================================
|
|
31
33
|
// Field mapping — provider field → canonical target
|
|
@@ -37,9 +39,9 @@ import { z } from 'zod';
|
|
|
37
39
|
* etc.); the schema does not enumerate transforms — adapters interpret them.
|
|
38
40
|
*/
|
|
39
41
|
export const FieldMappingSchema = z.object({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
source: z.string().min(1),
|
|
43
|
+
target: z.string().min(1),
|
|
44
|
+
transform: z.string().min(1).optional(),
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
export type FieldMapping = z.infer<typeof FieldMappingSchema>;
|
|
@@ -54,9 +56,9 @@ export type FieldMapping = z.infer<typeof FieldMappingSchema>;
|
|
|
54
56
|
* adapters interpret per provider.
|
|
55
57
|
*/
|
|
56
58
|
export const ResolvedFilterSchema = z.object({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
field: z.string().min(1),
|
|
60
|
+
op: z.enum(["eq", "neq", "in", "nin", "gt", "gte", "lt", "lte"]),
|
|
61
|
+
value: z.unknown(),
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
export type ResolvedFilter = z.infer<typeof ResolvedFilterSchema>;
|
|
@@ -66,23 +68,23 @@ export type ResolvedFilter = z.infer<typeof ResolvedFilterSchema>;
|
|
|
66
68
|
// ============================================================================
|
|
67
69
|
|
|
68
70
|
const SystemModstampCursorSchema = z.object({
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
kind: z.literal("systemModstamp"),
|
|
72
|
+
field: z.string().min(1),
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
const ReplayIdCursorSchema = z.object({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
kind: z.literal("replayId"),
|
|
77
|
+
field: z.string().min(1),
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
const TimestampCursorSchema = z.object({
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
kind: z.literal("timestamp"),
|
|
82
|
+
field: z.string().min(1),
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
const EventIdCursorSchema = z.object({
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
kind: z.literal("eventId"),
|
|
87
|
+
field: z.string().min(1),
|
|
86
88
|
});
|
|
87
89
|
|
|
88
90
|
/**
|
|
@@ -91,8 +93,8 @@ const EventIdCursorSchema = z.object({
|
|
|
91
93
|
* `field` is metadata for codegen/adapters (the response key the token lives on).
|
|
92
94
|
*/
|
|
93
95
|
const HistoryIdCursorSchema = z.object({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
kind: z.literal("historyId"),
|
|
97
|
+
field: z.string().min(1),
|
|
96
98
|
});
|
|
97
99
|
|
|
98
100
|
/**
|
|
@@ -100,17 +102,17 @@ const HistoryIdCursorSchema = z.object({
|
|
|
100
102
|
* same divisibility profile as `historyId`.
|
|
101
103
|
*/
|
|
102
104
|
const SyncTokenCursorSchema = z.object({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
kind: z.literal("syncToken"),
|
|
106
|
+
field: z.string().min(1),
|
|
105
107
|
});
|
|
106
108
|
|
|
107
|
-
export const CursorStrategySchema = z.discriminatedUnion(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
export const CursorStrategySchema = z.discriminatedUnion("kind", [
|
|
110
|
+
SystemModstampCursorSchema,
|
|
111
|
+
ReplayIdCursorSchema,
|
|
112
|
+
TimestampCursorSchema,
|
|
113
|
+
EventIdCursorSchema,
|
|
114
|
+
HistoryIdCursorSchema,
|
|
115
|
+
SyncTokenCursorSchema,
|
|
114
116
|
]);
|
|
115
117
|
|
|
116
118
|
export type CursorStrategy = z.infer<typeof CursorStrategySchema>;
|
|
@@ -135,18 +137,20 @@ export type CursorStrategy = z.infer<typeof CursorStrategySchema>;
|
|
|
135
137
|
* `eventId` is classified atomic conservatively: a generic opaque id is treated
|
|
136
138
|
* all-or-nothing unless a concrete strategy proves it monotonically resumable.
|
|
137
139
|
*/
|
|
138
|
-
export const CURSOR_DIVISIBILITY: Readonly<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
export const CURSOR_DIVISIBILITY: Readonly<
|
|
141
|
+
Record<CursorStrategy["kind"], boolean>
|
|
142
|
+
> = {
|
|
143
|
+
systemModstamp: true,
|
|
144
|
+
timestamp: true,
|
|
145
|
+
replayId: true,
|
|
146
|
+
eventId: false,
|
|
147
|
+
historyId: false,
|
|
148
|
+
syncToken: false,
|
|
145
149
|
};
|
|
146
150
|
|
|
147
151
|
/** Predicate form of {@link CURSOR_DIVISIBILITY}. */
|
|
148
|
-
export function isDivisibleCursor(kind: CursorStrategy[
|
|
149
|
-
|
|
152
|
+
export function isDivisibleCursor(kind: CursorStrategy["kind"]): boolean {
|
|
153
|
+
return CURSOR_DIVISIBILITY[kind];
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
// ============================================================================
|
|
@@ -159,19 +163,25 @@ export function isDivisibleCursor(kind: CursorStrategy['kind']): boolean {
|
|
|
159
163
|
* `field` — used for Stripe-style event endpoints. Defaults to `'poll'`.
|
|
160
164
|
*/
|
|
161
165
|
export const PollDetectionSchema = z.object({
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
cursor: CursorStrategySchema,
|
|
167
|
+
provenance: z.enum(["poll", "cdc"]).optional(),
|
|
164
168
|
});
|
|
165
169
|
|
|
166
170
|
export type PollDetection = z.infer<typeof PollDetectionSchema>;
|
|
167
171
|
|
|
168
172
|
/**
|
|
169
|
-
* Webhook-mode block. `eventIdField
|
|
170
|
-
*
|
|
171
|
-
* `Change<T>.dedupKey
|
|
173
|
+
* Webhook-mode block. `eventIdField`, when present, names the field on the
|
|
174
|
+
* emitted canonical record that `WebhookChangeSource<T>` reads to set
|
|
175
|
+
* `Change<T>.dedupKey` — used only as the fallback when the queue iterator
|
|
176
|
+
* does NOT yield an `eventId` alongside the record.
|
|
177
|
+
*
|
|
178
|
+
* `eventIdField` is **optional**: a queue iterator that always yields an
|
|
179
|
+
* `eventId` (vendor delivery metadata, the preferred channel) need not declare
|
|
180
|
+
* a record field for it. dedupKey precedence is: yielded `eventId` >
|
|
181
|
+
* `eventIdField` record extraction > undefined.
|
|
172
182
|
*/
|
|
173
183
|
export const WebhookDetectionSchema = z.object({
|
|
174
|
-
|
|
184
|
+
eventIdField: z.string().min(1).optional(),
|
|
175
185
|
});
|
|
176
186
|
|
|
177
187
|
export type WebhookDetection = z.infer<typeof WebhookDetectionSchema>;
|
|
@@ -181,17 +191,17 @@ export type WebhookDetection = z.infer<typeof WebhookDetectionSchema>;
|
|
|
181
191
|
// ============================================================================
|
|
182
192
|
|
|
183
193
|
const PollModeSchema = z.object({
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
mode: z.literal("poll"),
|
|
195
|
+
poll: PollDetectionSchema,
|
|
196
|
+
mapping: z.array(FieldMappingSchema).min(1),
|
|
197
|
+
filters: z.array(ResolvedFilterSchema).default([]),
|
|
188
198
|
});
|
|
189
199
|
|
|
190
200
|
const WebhookModeSchema = z.object({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
mode: z.literal("webhook"),
|
|
202
|
+
webhook: WebhookDetectionSchema,
|
|
203
|
+
mapping: z.array(FieldMappingSchema).min(1),
|
|
204
|
+
filters: z.array(ResolvedFilterSchema).default([]),
|
|
195
205
|
});
|
|
196
206
|
|
|
197
207
|
/**
|
|
@@ -201,9 +211,9 @@ const WebhookModeSchema = z.object({
|
|
|
201
211
|
* (Stripe-style event endpoints) is expressed via `mode: 'poll'` with
|
|
202
212
|
* `poll.provenance: 'cdc'`.
|
|
203
213
|
*/
|
|
204
|
-
export const DetectionConfigSchema = z.discriminatedUnion(
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
export const DetectionConfigSchema = z.discriminatedUnion("mode", [
|
|
215
|
+
PollModeSchema,
|
|
216
|
+
WebhookModeSchema,
|
|
207
217
|
]);
|
|
208
218
|
|
|
209
219
|
export type DetectionConfig = z.infer<typeof DetectionConfigSchema>;
|