@pattern-stack/codegen 0.19.0 → 0.20.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 +91 -0
- package/consumer-skills/events/authoring-events.md +31 -0
- package/dist/{chunk-Z7PQCAVK.js → chunk-4OC5MSHO.js} +50 -4
- package/dist/chunk-4OC5MSHO.js.map +1 -0
- package/dist/{chunk-7OVCARTQ.js → chunk-5RT7JGKT.js} +4 -4
- package/dist/{chunk-AZLUWG5S.js → chunk-AJILKWGO.js} +9 -9
- package/dist/{chunk-SNH35CNA.js → chunk-AODME4YK.js} +10 -10
- package/dist/{chunk-PKDS6QIJ.js → chunk-ATVGYF3D.js} +7 -7
- package/dist/{chunk-4MVGAMUA.js → chunk-BK5ICA2F.js} +4 -4
- package/dist/{chunk-2TVVBC53.js → chunk-BORNCTH3.js} +2 -2
- package/dist/{chunk-J7JMVS2B.js → chunk-CZQUOIDY.js} +4 -4
- package/dist/{chunk-V4AF6DI4.js → chunk-DUBZOXJC.js} +9 -2
- package/dist/{chunk-V4AF6DI4.js.map → chunk-DUBZOXJC.js.map} +1 -1
- package/dist/{chunk-VQOAATIG.js → chunk-DUMI2J5M.js} +4 -4
- package/dist/chunk-DUUCU77W.js +211 -0
- package/dist/chunk-DUUCU77W.js.map +1 -0
- package/dist/{chunk-OFRRBC7M.js → chunk-E2BRT5IB.js} +15 -1
- package/dist/chunk-E2BRT5IB.js.map +1 -0
- package/dist/{chunk-XKWOJZZ4.js → chunk-E45CSC33.js} +2 -2
- package/dist/{chunk-B34G6PHD.js → chunk-HCAKMT64.js} +77 -10
- package/dist/chunk-HCAKMT64.js.map +1 -0
- package/dist/{chunk-43SBT72G.js → chunk-I6UXRJ3Q.js} +4 -4
- package/dist/{chunk-GM3RMJIJ.js → chunk-INO47JXD.js} +3 -3
- package/dist/{chunk-BGULBWKJ.js → chunk-JOBQ6RUU.js} +1 -1
- package/dist/chunk-JOBQ6RUU.js.map +1 -0
- package/dist/{chunk-F7KN3U6U.js → chunk-KK5A7B2T.js} +27 -1
- package/dist/chunk-KK5A7B2T.js.map +1 -0
- package/dist/{chunk-65MO75WM.js → chunk-M3TIZGIB.js} +9 -9
- package/dist/{chunk-E6PLM6QG.js → chunk-MB5VVG4Z.js} +8 -8
- package/dist/{chunk-K2I6XIK5.js → chunk-MVKW2BCR.js} +2 -2
- package/dist/{chunk-VDL5CJ5C.js → chunk-P7EZCTIN.js} +8 -8
- package/dist/{chunk-R6F6KFIL.js → chunk-SGSWVNNB.js} +7 -7
- package/dist/chunk-SYVZ4MD2.js +1 -0
- package/dist/{chunk-235ZMMJR.js → chunk-SZYZ4SHF.js} +6 -6
- package/dist/{chunk-7LKAMLV4.js → chunk-T6SCOJF4.js} +4 -4
- package/dist/{chunk-OZEPJGMA.js → chunk-VOYFPR3S.js} +54 -6
- package/dist/chunk-VOYFPR3S.js.map +1 -0
- package/dist/{chunk-CLWBNXKF.js → chunk-W2UIDI3R.js} +4 -4
- 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 +2 -2
- 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 +4 -4
- package/dist/runtime/subsystems/bridge/bridge-delivery.schema.js +2 -2
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +8 -8
- package/dist/runtime/subsystems/bridge/bridge.module.js +19 -19
- package/dist/runtime/subsystems/bridge/event-flow.service.js +2 -2
- package/dist/runtime/subsystems/bridge/index.js +27 -27
- package/dist/runtime/subsystems/events/domain-events.schema.js +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +19 -32
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +4 -4
- package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +18 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +2 -2
- package/dist/runtime/subsystems/events/event-bus.protocol.d.ts +45 -1
- package/dist/runtime/subsystems/events/event-scheduler.d.ts +96 -0
- package/dist/runtime/subsystems/events/event-scheduler.js +25 -0
- package/dist/runtime/subsystems/events/event-scheduler.js.map +1 -0
- package/dist/runtime/subsystems/events/events-errors.d.ts +12 -1
- package/dist/runtime/subsystems/events/events-errors.js +5 -3
- package/dist/runtime/subsystems/events/events.module.d.ts +41 -2
- package/dist/runtime/subsystems/events/events.module.js +12 -9
- package/dist/runtime/subsystems/events/generated/bus.js +3 -3
- package/dist/runtime/subsystems/events/generated/index.js +3 -3
- package/dist/runtime/subsystems/events/generated/registry.d.ts +6 -0
- package/dist/runtime/subsystems/events/generated/registry.js +1 -1
- package/dist/runtime/subsystems/events/index.d.ts +4 -3
- package/dist/runtime/subsystems/events/index.js +39 -15
- package/dist/runtime/subsystems/index.d.ts +1 -0
- package/dist/runtime/subsystems/index.js +99 -98
- package/dist/runtime/subsystems/integration/build-change-source.js +2 -2
- package/dist/runtime/subsystems/integration/index.js +36 -36
- package/dist/runtime/subsystems/integration/integration.module.js +4 -4
- package/dist/runtime/subsystems/jobs/index.js +23 -23
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +3 -3
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +3 -3
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
- package/dist/runtime/subsystems/jobs/job-worker.js +3 -3
- package/dist/runtime/subsystems/jobs/job-worker.module.js +9 -9
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +7 -7
- package/dist/runtime/subsystems/observability/index.js +3 -3
- package/dist/runtime/subsystems/observability/observability.module.js +3 -3
- package/dist/runtime/subsystems/observability/observability.service.js +2 -2
- 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 +38 -15
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.js +12 -12
- package/package.json +1 -1
- package/runtime/subsystems/events/domain-events.schema.ts +16 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +103 -1
- package/runtime/subsystems/events/event-bus.memory-backend.ts +57 -1
- package/runtime/subsystems/events/event-bus.protocol.ts +47 -0
- package/runtime/subsystems/events/event-scheduler.ts +351 -0
- package/runtime/subsystems/events/events-errors.ts +14 -0
- package/runtime/subsystems/events/events.module.ts +78 -1
- package/runtime/subsystems/events/generated/registry.ts +1 -0
- package/runtime/subsystems/events/index.ts +25 -3
- package/dist/chunk-B34G6PHD.js.map +0 -1
- package/dist/chunk-BGULBWKJ.js.map +0 -1
- package/dist/chunk-F7KN3U6U.js.map +0 -1
- package/dist/chunk-FN2PYDPP.js +0 -1
- package/dist/chunk-OFRRBC7M.js.map +0 -1
- package/dist/chunk-OZEPJGMA.js.map +0 -1
- package/dist/chunk-Z7PQCAVK.js.map +0 -1
- /package/dist/{chunk-7OVCARTQ.js.map → chunk-5RT7JGKT.js.map} +0 -0
- /package/dist/{chunk-AZLUWG5S.js.map → chunk-AJILKWGO.js.map} +0 -0
- /package/dist/{chunk-SNH35CNA.js.map → chunk-AODME4YK.js.map} +0 -0
- /package/dist/{chunk-PKDS6QIJ.js.map → chunk-ATVGYF3D.js.map} +0 -0
- /package/dist/{chunk-4MVGAMUA.js.map → chunk-BK5ICA2F.js.map} +0 -0
- /package/dist/{chunk-2TVVBC53.js.map → chunk-BORNCTH3.js.map} +0 -0
- /package/dist/{chunk-J7JMVS2B.js.map → chunk-CZQUOIDY.js.map} +0 -0
- /package/dist/{chunk-VQOAATIG.js.map → chunk-DUMI2J5M.js.map} +0 -0
- /package/dist/{chunk-XKWOJZZ4.js.map → chunk-E45CSC33.js.map} +0 -0
- /package/dist/{chunk-43SBT72G.js.map → chunk-I6UXRJ3Q.js.map} +0 -0
- /package/dist/{chunk-GM3RMJIJ.js.map → chunk-INO47JXD.js.map} +0 -0
- /package/dist/{chunk-65MO75WM.js.map → chunk-M3TIZGIB.js.map} +0 -0
- /package/dist/{chunk-E6PLM6QG.js.map → chunk-MB5VVG4Z.js.map} +0 -0
- /package/dist/{chunk-K2I6XIK5.js.map → chunk-MVKW2BCR.js.map} +0 -0
- /package/dist/{chunk-VDL5CJ5C.js.map → chunk-P7EZCTIN.js.map} +0 -0
- /package/dist/{chunk-R6F6KFIL.js.map → chunk-SGSWVNNB.js.map} +0 -0
- /package/dist/{chunk-FN2PYDPP.js.map → chunk-SYVZ4MD2.js.map} +0 -0
- /package/dist/{chunk-235ZMMJR.js.map → chunk-SZYZ4SHF.js.map} +0 -0
- /package/dist/{chunk-7LKAMLV4.js.map → chunk-T6SCOJF4.js.map} +0 -0
- /package/dist/{chunk-CLWBNXKF.js.map → chunk-W2UIDI3R.js.map} +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ScheduleConfigError
|
|
3
|
+
} from "./chunk-DUBZOXJC.js";
|
|
4
|
+
|
|
5
|
+
// runtime/subsystems/events/event-scheduler.ts
|
|
6
|
+
import { Logger } from "@nestjs/common";
|
|
7
|
+
var UNIT_MS = Object.freeze({
|
|
8
|
+
ms: 1,
|
|
9
|
+
s: 1e3,
|
|
10
|
+
m: 6e4,
|
|
11
|
+
h: 36e5,
|
|
12
|
+
d: 864e5
|
|
13
|
+
});
|
|
14
|
+
function parseEvery(every, eventType) {
|
|
15
|
+
const where = eventType ? ` (event '${eventType}')` : "";
|
|
16
|
+
let ms;
|
|
17
|
+
if (typeof every === "number") {
|
|
18
|
+
ms = every;
|
|
19
|
+
} else if (typeof every === "string") {
|
|
20
|
+
const match = /^\s*([0-9]*\.?[0-9]+)\s*(ms|s|m|h|d)\s*$/.exec(every);
|
|
21
|
+
const value = match?.[1];
|
|
22
|
+
const unit = match?.[2];
|
|
23
|
+
const unitMs = unit ? UNIT_MS[unit] : void 0;
|
|
24
|
+
if (value === void 0 || unitMs === void 0) {
|
|
25
|
+
throw new ScheduleConfigError(
|
|
26
|
+
`schedule.every '${every}'${where} is not a valid duration. Use a number of ms or '<n><unit>' with unit ms|s|m|h|d (e.g. '1h', '30m').`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
ms = Number(value) * unitMs;
|
|
30
|
+
} else {
|
|
31
|
+
throw new ScheduleConfigError(
|
|
32
|
+
`schedule.every${where} must be a duration string or a number of ms; got ${typeof every}.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
36
|
+
throw new ScheduleConfigError(
|
|
37
|
+
`schedule.every${where} resolved to ${ms}ms \u2014 must be a finite, positive duration.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return ms;
|
|
41
|
+
}
|
|
42
|
+
function slotStartFor(atMs, everyMs, align, anchorMs) {
|
|
43
|
+
if (align) return Math.floor(atMs / everyMs) * everyMs;
|
|
44
|
+
if (atMs < anchorMs) return anchorMs;
|
|
45
|
+
return anchorMs + Math.floor((atMs - anchorMs) / everyMs) * everyMs;
|
|
46
|
+
}
|
|
47
|
+
function nextSlotStart(atMs, everyMs, align, anchorMs) {
|
|
48
|
+
return slotStartFor(atMs, everyMs, align, anchorMs) + everyMs;
|
|
49
|
+
}
|
|
50
|
+
var SCHEDULE_KEY_PREFIX = "@schedule/";
|
|
51
|
+
function slotKeyFor(type, slotStartMs) {
|
|
52
|
+
return `${SCHEDULE_KEY_PREFIX}${type}/${slotStartMs}`;
|
|
53
|
+
}
|
|
54
|
+
var DEFAULT_MAX_CATCH_UP_SLOTS = 1e3;
|
|
55
|
+
var SCHEDULE_FLOOR_MS = 1e3;
|
|
56
|
+
function resolveScheduledEvent(type, schedule, direction, pool) {
|
|
57
|
+
if (!direction || !pool) {
|
|
58
|
+
throw new ScheduleConfigError(
|
|
59
|
+
`event '${type}' declares a schedule but has no direction/pool \u2014 a scheduled event must be domain-tier so it can route to the bridge.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
type,
|
|
64
|
+
everyMs: parseEvery(schedule.every, type),
|
|
65
|
+
align: schedule.align ?? true,
|
|
66
|
+
catchUp: schedule.catchUp ?? false,
|
|
67
|
+
maxCatchUpSlots: schedule.maxCatchUpSlots ?? DEFAULT_MAX_CATCH_UP_SLOTS,
|
|
68
|
+
direction,
|
|
69
|
+
pool
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function scheduledEventsFromRegistry(registry) {
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const [type, meta] of Object.entries(registry)) {
|
|
75
|
+
if (!meta?.schedule) continue;
|
|
76
|
+
out.push(resolveScheduledEvent(type, meta.schedule, meta.direction, meta.pool));
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
var EventScheduler = class _EventScheduler {
|
|
81
|
+
constructor(bus, schedules, opts = {}) {
|
|
82
|
+
this.bus = bus;
|
|
83
|
+
this.schedules = schedules;
|
|
84
|
+
this.now = opts.now ?? Date.now;
|
|
85
|
+
this.anchorMs = this.now();
|
|
86
|
+
const smallest = schedules.length ? Math.min(...schedules.map((s) => s.everyMs)) : SCHEDULE_FLOOR_MS;
|
|
87
|
+
this.tickIntervalMs = opts.tickIntervalMs ?? Math.max(smallest, SCHEDULE_FLOOR_MS);
|
|
88
|
+
for (const s of schedules) {
|
|
89
|
+
if (s.everyMs < SCHEDULE_FLOOR_MS) {
|
|
90
|
+
this.logger.warn(
|
|
91
|
+
`schedule for '${s.type}' is every ${s.everyMs}ms \u2014 below the ${SCHEDULE_FLOOR_MS}ms floor; materialise/drain latency dominates, so the cadence is not honoured to that precision.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (typeof bus.materializeScheduledEvent !== "function") {
|
|
96
|
+
this.logger.warn(
|
|
97
|
+
`the configured event bus does not support scheduled-event materialisation; ${schedules.length} schedule(s) will not fire.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
bus;
|
|
102
|
+
schedules;
|
|
103
|
+
logger = new Logger(_EventScheduler.name);
|
|
104
|
+
now;
|
|
105
|
+
timer = null;
|
|
106
|
+
anchorMs;
|
|
107
|
+
tickIntervalMs;
|
|
108
|
+
/** Reconcile-on-boot, then start the tick interval. Idempotent. */
|
|
109
|
+
async start() {
|
|
110
|
+
if (this.schedules.length === 0) return;
|
|
111
|
+
if (typeof this.bus.materializeScheduledEvent !== "function") return;
|
|
112
|
+
await this.materializeBoot();
|
|
113
|
+
if (this.timer) return;
|
|
114
|
+
this.timer = setInterval(() => {
|
|
115
|
+
void this.materializeTick();
|
|
116
|
+
}, this.tickIntervalMs);
|
|
117
|
+
this.timer.unref?.();
|
|
118
|
+
this.logger.log(
|
|
119
|
+
`EventScheduler started: ${this.schedules.length} scheduled event(s), tick=${this.tickIntervalMs}ms.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
/** Stop the tick interval. Idempotent. */
|
|
123
|
+
stop() {
|
|
124
|
+
if (this.timer) {
|
|
125
|
+
clearInterval(this.timer);
|
|
126
|
+
this.timer = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Boot pass — materialise the current slot (or bounded backfill) per event. */
|
|
130
|
+
async materializeBoot() {
|
|
131
|
+
const nowMs = this.now();
|
|
132
|
+
for (const s of this.schedules) {
|
|
133
|
+
try {
|
|
134
|
+
if (s.catchUp) {
|
|
135
|
+
await this.backfill(s, nowMs);
|
|
136
|
+
} else {
|
|
137
|
+
await this.materializeOne(s, slotStartFor(nowMs, s.everyMs, s.align, this.anchorMs));
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.logger.error(
|
|
141
|
+
`boot materialise for '${s.type}' failed: ${err.message}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Tick pass — materialise the current + next slot per event (current covers a
|
|
147
|
+
* tick landing in a fresh slot the boot pass missed). */
|
|
148
|
+
async materializeTick() {
|
|
149
|
+
const nowMs = this.now();
|
|
150
|
+
for (const s of this.schedules) {
|
|
151
|
+
try {
|
|
152
|
+
const current = slotStartFor(nowMs, s.everyMs, s.align, this.anchorMs);
|
|
153
|
+
await this.materializeOne(s, current);
|
|
154
|
+
await this.materializeOne(s, current + s.everyMs);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
this.logger.error(
|
|
157
|
+
`tick materialise for '${s.type}' failed: ${err.message}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async materializeOne(s, slotStartMs) {
|
|
163
|
+
const materialize = this.bus.materializeScheduledEvent;
|
|
164
|
+
if (!materialize) return;
|
|
165
|
+
const slotKey = slotKeyFor(s.type, slotStartMs);
|
|
166
|
+
const { created } = await materialize.call(this.bus, {
|
|
167
|
+
type: s.type,
|
|
168
|
+
slotKey,
|
|
169
|
+
slotStart: new Date(slotStartMs),
|
|
170
|
+
direction: s.direction,
|
|
171
|
+
pool: s.pool
|
|
172
|
+
});
|
|
173
|
+
if (created) {
|
|
174
|
+
this.logger.debug?.(
|
|
175
|
+
`materialised '${s.type}' slot ${new Date(slotStartMs).toISOString()}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Backfill missed slots from the last emitted slot to the current one,
|
|
180
|
+
* bounded by `maxCatchUpSlots`. */
|
|
181
|
+
async backfill(s, nowMs) {
|
|
182
|
+
const current = slotStartFor(nowMs, s.everyMs, s.align, this.anchorMs);
|
|
183
|
+
const lastMs = await this.bus.lastScheduledSlotMs?.(s.type) ?? null;
|
|
184
|
+
let from = lastMs !== null ? lastMs + s.everyMs : current;
|
|
185
|
+
if (from > current) from = current;
|
|
186
|
+
const total = Math.floor((current - from) / s.everyMs) + 1;
|
|
187
|
+
if (total > s.maxCatchUpSlots) {
|
|
188
|
+
const dropped = total - s.maxCatchUpSlots;
|
|
189
|
+
from = current - (s.maxCatchUpSlots - 1) * s.everyMs;
|
|
190
|
+
this.logger.warn(
|
|
191
|
+
`catchUp for '${s.type}' would backfill ${total} slots; capping at ${s.maxCatchUpSlots} (dropping ${dropped} oldest).`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
for (let slot = from; slot <= current; slot += s.everyMs) {
|
|
195
|
+
await this.materializeOne(s, slot);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
parseEvery,
|
|
202
|
+
slotStartFor,
|
|
203
|
+
nextSlotStart,
|
|
204
|
+
SCHEDULE_KEY_PREFIX,
|
|
205
|
+
slotKeyFor,
|
|
206
|
+
SCHEDULE_FLOOR_MS,
|
|
207
|
+
resolveScheduledEvent,
|
|
208
|
+
scheduledEventsFromRegistry,
|
|
209
|
+
EventScheduler
|
|
210
|
+
};
|
|
211
|
+
//# sourceMappingURL=chunk-DUUCU77W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../runtime/subsystems/events/event-scheduler.ts"],"sourcesContent":["/**\n * EventScheduler — declarative time-based emission (ADR-039: time as an event\n * source). Materialises exactly one `domain_events` row per (scheduled event\n * type, slot) on a cadence; ADR-023's three activation tiers — unchanged — then\n * react. The scheduler is a STRICT PRODUCER: it emits facts and does no work\n * (the dealbrain `scheduler.service.ts` shape, generalised onto the outbox).\n *\n * Two entry points, both driven by `EventsModule`'s lifecycle:\n *\n * - **reconcile-on-boot** (`materializeBoot`, at `onModuleInit`) — for every\n * scheduled event type, materialise the CURRENT slot (catch-up off → run\n * once on recovery) or bounded backfill (catch-up on). Boot is when a\n * downtime-healing tick matters most. In the outbox model a removed\n * `schedule:` simply stops being materialised — there's no broker scheduler\n * entry to leave dangling, so the dealbrain ENG-605 \"zombie scheduler\" class\n * of bug is structurally absent; the reconcile half is what we keep.\n * - **tick pass** (`materializeTick` on an interval) — materialise each\n * scheduled event's NEXT (and current) slot so ticks self-perpetuate.\n *\n * Exactly-one-per-slot lives in the DB (the partial UNIQUE expression index on\n * `(type, metadata->>'scheduleSlot')`), reached via\n * `IEventBus.materializeScheduledEvent` → `INSERT … ON CONFLICT DO NOTHING`.\n * The scheduler never READS for an existing slot event (that read is the\n * swe-brain dedupe trap — it matches the still-running incumbent). The slot key\n * is a pure function of (type, slot), so every instance computes the same key\n * and the constraint collapses the race.\n *\n * Drizzle + memory only. The Redis bus retains no outbox history, so slot-key\n * idempotency can't be enforced there (mirrors bridge-on-Redis being\n * unsupported); the scheduler is not wired under `backend: 'redis'`.\n */\nimport { Logger } from '@nestjs/common';\nimport type { IEventBus } from './event-bus.protocol';\nimport { ScheduleConfigError } from './events-errors';\n\n// ─── Duration grammar ────────────────────────────────────────────────────────\n\nconst UNIT_MS: Readonly<Record<string, number>> = Object.freeze({\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n});\n\n/**\n * Parse a `schedule.every` into milliseconds. Accepts a positive number (ms) or\n * a duration string `<number><unit>` (unit ∈ ms|s|m|h|d; decimals allowed).\n * Throws `ScheduleConfigError` synchronously on anything unparseable, ≤0, or\n * non-finite — so a bad schedule surfaces at boot before the tick loop starts.\n */\nexport function parseEvery(every: string | number, eventType?: string): number {\n const where = eventType ? ` (event '${eventType}')` : '';\n let ms: number;\n if (typeof every === 'number') {\n ms = every;\n } else if (typeof every === 'string') {\n const match = /^\\s*([0-9]*\\.?[0-9]+)\\s*(ms|s|m|h|d)\\s*$/.exec(every);\n // Destructure the capture groups; under the consumer's stricter tsc\n // (`noUncheckedIndexedAccess`) regex groups are `string | undefined` and\n // the UNIT_MS lookup is `number | undefined`, so guard both explicitly\n // rather than asserting. A truthy `match` always has both groups for this\n // pattern, but the guard makes that provable (no non-null assertion).\n const value = match?.[1];\n const unit = match?.[2];\n const unitMs = unit ? UNIT_MS[unit] : undefined;\n if (value === undefined || unitMs === undefined) {\n throw new ScheduleConfigError(\n `schedule.every '${every}'${where} is not a valid duration. Use a ` +\n `number of ms or '<n><unit>' with unit ms|s|m|h|d (e.g. '1h', '30m').`,\n );\n }\n ms = Number(value) * unitMs;\n } else {\n throw new ScheduleConfigError(\n `schedule.every${where} must be a duration string or a number of ms; ` +\n `got ${typeof every}.`,\n );\n }\n if (!Number.isFinite(ms) || ms <= 0) {\n throw new ScheduleConfigError(\n `schedule.every${where} resolved to ${ms}ms — must be a finite, positive ` +\n `duration.`,\n );\n }\n return ms;\n}\n\n// ─── Slot math ───────────────────────────────────────────────────────────────\n\n/**\n * The start of the slot containing `atMs`, for a schedule of `everyMs`.\n * - `align: true` (default) — epoch-anchored: `floor(at / every) * every`.\n * - `align: false` — anchored to `anchorMs` (the scheduler's first-run time).\n */\nexport function slotStartFor(\n atMs: number,\n everyMs: number,\n align: boolean,\n anchorMs: number,\n): number {\n if (align) return Math.floor(atMs / everyMs) * everyMs;\n if (atMs < anchorMs) return anchorMs;\n return anchorMs + Math.floor((atMs - anchorMs) / everyMs) * everyMs;\n}\n\n/** The start of the slot AFTER the one containing `atMs`. */\nexport function nextSlotStart(\n atMs: number,\n everyMs: number,\n align: boolean,\n anchorMs: number,\n): number {\n return slotStartFor(atMs, everyMs, align, anchorMs) + everyMs;\n}\n\n/** Prefix every scheduler-materialised `metadata.scheduleSlot` carries — the\n * partial UNIQUE index is scoped to non-null slot keys; this prefix keeps the\n * key namespace unambiguous and greppable. */\nexport const SCHEDULE_KEY_PREFIX = '@schedule/';\n\n/** Deterministic slot key. Pure function of (type, slotStart) — every instance\n * computes the same value, which is what makes the idempotent insert\n * exactly-once. */\nexport function slotKeyFor(type: string, slotStartMs: number): string {\n return `${SCHEDULE_KEY_PREFIX}${type}/${slotStartMs}`;\n}\n\n// ─── Resolved schedule ───────────────────────────────────────────────────────\n\nconst DEFAULT_MAX_CATCH_UP_SLOTS = 1000;\n\n/** Below this floor (== the default outbox poll interval) materialise/drain\n * latency dominates the cadence; allowed but warned once at boot. */\nexport const SCHEDULE_FLOOR_MS = 1_000;\n\n/** One scheduled event the scheduler will materialise. Built from the generated\n * event registry (`schedule` block + direction/pool routing metadata). */\nexport interface ScheduledEvent {\n type: string;\n everyMs: number;\n align: boolean;\n catchUp: boolean;\n maxCatchUpSlots: number;\n /** Routing — from the event's registry metadata (a scheduled event is\n * domain-tier, so both are always present). */\n direction: string;\n pool: string;\n}\n\n/** The raw `schedule` block as it appears in the generated registry entry. */\nexport interface RegistrySchedule {\n every: string | number;\n align?: boolean;\n catchUp?: boolean;\n maxCatchUpSlots?: number;\n}\n\n/** Validate + normalise one registry entry's `schedule` into a `ScheduledEvent`.\n * Throws `ScheduleConfigError` on a malformed `every` (boot backstop — codegen\n * already validated, this catches hand-edits / version skew). */\nexport function resolveScheduledEvent(\n type: string,\n schedule: RegistrySchedule,\n direction: string | null,\n pool: string | null,\n): ScheduledEvent {\n if (!direction || !pool) {\n throw new ScheduleConfigError(\n `event '${type}' declares a schedule but has no direction/pool — a ` +\n `scheduled event must be domain-tier so it can route to the bridge.`,\n );\n }\n return {\n type,\n everyMs: parseEvery(schedule.every, type),\n align: schedule.align ?? true,\n catchUp: schedule.catchUp ?? false,\n maxCatchUpSlots: schedule.maxCatchUpSlots ?? DEFAULT_MAX_CATCH_UP_SLOTS,\n direction,\n pool,\n };\n}\n\n/**\n * Read the scheduled-event set from a generated `eventRegistry`. The registry\n * value shape is structural (`{ schedule?, direction, pool }`) so this stays\n * decoupled from the generated `EventMetadata` type. Returns `[]` when nothing\n * declared `schedule:`.\n */\nexport function scheduledEventsFromRegistry(\n registry: Record<\n string,\n { schedule?: RegistrySchedule; direction: string | null; pool: string | null }\n >,\n): ScheduledEvent[] {\n const out: ScheduledEvent[] = [];\n for (const [type, meta] of Object.entries(registry)) {\n if (!meta?.schedule) continue;\n out.push(resolveScheduledEvent(type, meta.schedule, meta.direction, meta.pool));\n }\n return out;\n}\n\n// ─── EventScheduler ──────────────────────────────────────────────────────────\n\nexport interface EventSchedulerOptions {\n /** Tick cadence (ms). Default = smallest scheduled `every`, floored. Test override. */\n tickIntervalMs?: number;\n /** Injectable clock for deterministic tests. Default `Date.now`. */\n now?: () => number;\n}\n\nexport class EventScheduler {\n private readonly logger = new Logger(EventScheduler.name);\n private readonly now: () => number;\n private timer: ReturnType<typeof setInterval> | null = null;\n private readonly anchorMs: number;\n private readonly tickIntervalMs: number;\n\n constructor(\n private readonly bus: IEventBus,\n private readonly schedules: ReadonlyArray<ScheduledEvent>,\n opts: EventSchedulerOptions = {},\n ) {\n this.now = opts.now ?? Date.now;\n this.anchorMs = this.now();\n const smallest = schedules.length\n ? Math.min(...schedules.map((s) => s.everyMs))\n : SCHEDULE_FLOOR_MS;\n this.tickIntervalMs = opts.tickIntervalMs ?? Math.max(smallest, SCHEDULE_FLOOR_MS);\n for (const s of schedules) {\n if (s.everyMs < SCHEDULE_FLOOR_MS) {\n this.logger.warn(\n `schedule for '${s.type}' is every ${s.everyMs}ms — below the ` +\n `${SCHEDULE_FLOOR_MS}ms floor; materialise/drain latency dominates, ` +\n `so the cadence is not honoured to that precision.`,\n );\n }\n }\n if (typeof bus.materializeScheduledEvent !== 'function') {\n // The backend (e.g. Redis) cannot enforce slot idempotency. Caller\n // should not construct the scheduler for such backends; guard anyway.\n this.logger.warn(\n `the configured event bus does not support scheduled-event ` +\n `materialisation; ${schedules.length} schedule(s) will not fire.`,\n );\n }\n }\n\n /** Reconcile-on-boot, then start the tick interval. Idempotent. */\n async start(): Promise<void> {\n if (this.schedules.length === 0) return;\n if (typeof this.bus.materializeScheduledEvent !== 'function') return;\n await this.materializeBoot();\n if (this.timer) return;\n this.timer = setInterval(() => {\n void this.materializeTick();\n }, this.tickIntervalMs);\n (this.timer as { unref?: () => void }).unref?.();\n this.logger.log(\n `EventScheduler started: ${this.schedules.length} scheduled event(s), ` +\n `tick=${this.tickIntervalMs}ms.`,\n );\n }\n\n /** Stop the tick interval. Idempotent. */\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n /** Boot pass — materialise the current slot (or bounded backfill) per event. */\n async materializeBoot(): Promise<void> {\n const nowMs = this.now();\n for (const s of this.schedules) {\n try {\n if (s.catchUp) {\n await this.backfill(s, nowMs);\n } else {\n await this.materializeOne(s, slotStartFor(nowMs, s.everyMs, s.align, this.anchorMs));\n }\n } catch (err) {\n this.logger.error(\n `boot materialise for '${s.type}' failed: ${(err as Error).message}`,\n );\n }\n }\n }\n\n /** Tick pass — materialise the current + next slot per event (current covers a\n * tick landing in a fresh slot the boot pass missed). */\n async materializeTick(): Promise<void> {\n const nowMs = this.now();\n for (const s of this.schedules) {\n try {\n const current = slotStartFor(nowMs, s.everyMs, s.align, this.anchorMs);\n await this.materializeOne(s, current);\n await this.materializeOne(s, current + s.everyMs);\n } catch (err) {\n this.logger.error(\n `tick materialise for '${s.type}' failed: ${(err as Error).message}`,\n );\n }\n }\n }\n\n private async materializeOne(s: ScheduledEvent, slotStartMs: number): Promise<void> {\n // `materialize*` only runs after `start()`/the boot path confirmed the bus\n // supports materialisation; guard here too (no non-null assertion) so the\n // optional-method call is provably defined.\n const materialize = this.bus.materializeScheduledEvent;\n if (!materialize) return;\n const slotKey = slotKeyFor(s.type, slotStartMs);\n const { created } = await materialize.call(this.bus, {\n type: s.type,\n slotKey,\n slotStart: new Date(slotStartMs),\n direction: s.direction,\n pool: s.pool,\n });\n if (created) {\n this.logger.debug?.(\n `materialised '${s.type}' slot ${new Date(slotStartMs).toISOString()}`,\n );\n }\n }\n\n /** Backfill missed slots from the last emitted slot to the current one,\n * bounded by `maxCatchUpSlots`. */\n private async backfill(s: ScheduledEvent, nowMs: number): Promise<void> {\n const current = slotStartFor(nowMs, s.everyMs, s.align, this.anchorMs);\n const lastMs = (await this.bus.lastScheduledSlotMs?.(s.type)) ?? null;\n let from = lastMs !== null ? lastMs + s.everyMs : current;\n if (from > current) from = current; // last >= current → just (re)try current\n const total = Math.floor((current - from) / s.everyMs) + 1;\n if (total > s.maxCatchUpSlots) {\n const dropped = total - s.maxCatchUpSlots;\n from = current - (s.maxCatchUpSlots - 1) * s.everyMs;\n this.logger.warn(\n `catchUp for '${s.type}' would backfill ${total} slots; capping at ` +\n `${s.maxCatchUpSlots} (dropping ${dropped} oldest).`,\n );\n }\n for (let slot = from; slot <= current; slot += s.everyMs) {\n await this.materializeOne(s, slot);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
pgTable,
|
|
7
7
|
text,
|
|
8
8
|
timestamp,
|
|
9
|
+
uniqueIndex,
|
|
9
10
|
uuid
|
|
10
11
|
} from "drizzle-orm/pg-core";
|
|
11
12
|
import { sql } from "drizzle-orm";
|
|
@@ -57,6 +58,19 @@ var domainEvents = pgTable(
|
|
|
57
58
|
idxDomainEventsTierStatusOccurredAt: index(
|
|
58
59
|
"idx_domain_events_tier_status_occurred_at"
|
|
59
60
|
).on(t.tier, t.status, t.occurredAt),
|
|
61
|
+
/**
|
|
62
|
+
* Scheduling idempotency — partial UNIQUE expression index (ADR-039). The
|
|
63
|
+
* `EventScheduler` materialises one tick per (event type, slot) by inserting
|
|
64
|
+
* with `metadata.scheduleSlot = @schedule/<type>/<slotStartMs>` and
|
|
65
|
+
* `ON CONFLICT DO NOTHING`; this constraint is what makes
|
|
66
|
+
* "exactly one event per slot" true across multi-instance deploys and
|
|
67
|
+
* boot/tick races — no advisory lock, no leader election. Partial on the
|
|
68
|
+
* extracted slot key so it only covers scheduler-materialised rows; ordinary
|
|
69
|
+
* (use-case / webhook) events carry no `scheduleSlot` and are untouched.
|
|
70
|
+
*/
|
|
71
|
+
idxDomainEventsScheduleSlot: uniqueIndex(
|
|
72
|
+
"idx_domain_events_schedule_slot"
|
|
73
|
+
).on(t.type, sql`(${t.metadata} ->> 'scheduleSlot')`).where(sql`${t.metadata} ->> 'scheduleSlot' IS NOT NULL`),
|
|
60
74
|
/**
|
|
61
75
|
* Tier ↔ routing-fields invariant (AUDIT-1):
|
|
62
76
|
* - `tier` is one of `'domain' | 'audit'`.
|
|
@@ -75,4 +89,4 @@ var domainEvents = pgTable(
|
|
|
75
89
|
export {
|
|
76
90
|
domainEvents
|
|
77
91
|
};
|
|
78
|
-
//# sourceMappingURL=chunk-
|
|
92
|
+
//# sourceMappingURL=chunk-E2BRT5IB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../runtime/subsystems/events/domain-events.schema.ts"],"sourcesContent":["/**\n * Drizzle schema for the domain_events outbox table.\n *\n * This table backs the DrizzleEventBus. Events are inserted within the\n * same database transaction as the domain write (outbox pattern). A\n * polling process reads unprocessed rows and dispatches to subscribers.\n *\n * First-class routing columns (EVT-1):\n * - `pool` — populated by DrizzleEventBus.publish() (EVT-4); enables\n * pool-filtered drain queries without unpacking metadata JSON.\n * NULL when `tier='audit'` (audit events are not routed).\n * - `direction` — `inbound` | `change` | `outbound`; mirrors the routing\n * dimension used by jobs' reserved `events_inbound` /\n * `events_change` / `events_outbound` pools.\n * NULL when `tier='audit'`.\n * - `tenant_id` — conditional: emitted only when `events.multi_tenant: true`\n * in `codegen.config.yaml`. The runtime source declares it\n * unconditionally; EVT-8's scaffold template handles the\n * config-driven include/exclude.\n *\n * Audit-tier column (AUDIT-1):\n * - `tier` — `'domain'` | `'audit'`. Defaults to `'domain'`. Audit-tier\n * rows are observability-only (subscribers may observe but\n * the bridge MUST NOT spawn jobs from them); they have null\n * `pool` and `direction` by construction. The CHECK\n * constraint `domain_events_tier_routing_check` enforces\n * `tier='audit' ⇔ (pool IS NULL AND direction IS NULL)`.\n *\n * The `metadata` JSON column continues to carry these values for protocol\n * stability; the first-class columns are an optimization for drain filtering.\n *\n * Indexes (declared below in the index callback):\n * - (status, occurred_at) — polling drain filter\n * - (aggregate_id, aggregate_type) — event replay per aggregate\n * - (pool, status, occurred_at) — per-pool drain filter (EVT-1)\n * - (tier, status, occurred_at) — per-tier filter for the observability\n * viewer's tier toggle (AUDIT-1).\n */\nimport {\n check,\n index,\n jsonb,\n pgTable,\n text,\n timestamp,\n uniqueIndex,\n uuid,\n} from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nexport const domainEvents = pgTable(\n 'domain_events',\n {\n id: uuid('id').primaryKey(),\n type: text('type').notNull(),\n aggregateId: text('aggregate_id').notNull(),\n aggregateType: text('aggregate_type').notNull(),\n payload: jsonb('payload').notNull().$type<Record<string, unknown>>(),\n occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull(),\n processedAt: timestamp('processed_at', { withTimezone: true }),\n /** Lifecycle status: pending | processed | failed */\n status: text('status').notNull().default('pending'),\n /** Error message from the last failed dispatch attempt. */\n error: text('error'),\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n /** Routing pool (e.g. `events_inbound`, `events_change`, `events_outbound`). Populated by DrizzleEventBus.publish() in EVT-4. NULL when `tier='audit'`. */\n pool: text('pool'),\n /** Routing direction: `inbound` | `change` | `outbound`. Populated by DrizzleEventBus.publish() in EVT-4. NULL when `tier='audit'`. */\n direction: text('direction'),\n /**\n * Event tier: `'domain'` (default) or `'audit'`. Audit-tier rows are\n * observability-only and have null `pool`/`direction` by construction —\n * enforced by the `domain_events_tier_routing_check` CHECK constraint\n * declared below. (AUDIT-1)\n */\n tier: text('tier').notNull().default('domain'),\n // conditional: emitted only when events.multi_tenant: true\n tenantId: text('tenant_id'),\n },\n (t) => ({\n /** Polling drain filter (existing — promoted from comment to declaration in EVT-1). */\n idxDomainEventsStatusOccurredAt: index('idx_domain_events_status_occurred_at').on(\n t.status,\n t.occurredAt,\n ),\n /** Event replay per aggregate (existing — promoted from comment to declaration in EVT-1). */\n idxDomainEventsAggregate: index('idx_domain_events_aggregate').on(\n t.aggregateId,\n t.aggregateType,\n ),\n /** Per-pool drain filter (EVT-1). Enables DrizzleEventBus to drain a single pool without scanning all events. */\n idxDomainEventsPoolStatusOccurredAt: index(\n 'idx_domain_events_pool_status_occurred_at',\n ).on(t.pool, t.status, t.occurredAt),\n /** Per-tier filter (AUDIT-1). Backs the observability viewer's tier toggle. */\n idxDomainEventsTierStatusOccurredAt: index(\n 'idx_domain_events_tier_status_occurred_at',\n ).on(t.tier, t.status, t.occurredAt),\n /**\n * Scheduling idempotency — partial UNIQUE expression index (ADR-039). The\n * `EventScheduler` materialises one tick per (event type, slot) by inserting\n * with `metadata.scheduleSlot = @schedule/<type>/<slotStartMs>` and\n * `ON CONFLICT DO NOTHING`; this constraint is what makes\n * \"exactly one event per slot\" true across multi-instance deploys and\n * boot/tick races — no advisory lock, no leader election. Partial on the\n * extracted slot key so it only covers scheduler-materialised rows; ordinary\n * (use-case / webhook) events carry no `scheduleSlot` and are untouched.\n */\n idxDomainEventsScheduleSlot: uniqueIndex(\n 'idx_domain_events_schedule_slot',\n )\n .on(t.type, sql`(${t.metadata} ->> 'scheduleSlot')`)\n .where(sql`${t.metadata} ->> 'scheduleSlot' IS NOT NULL`),\n /**\n * Tier ↔ routing-fields invariant (AUDIT-1):\n * - `tier` is one of `'domain' | 'audit'`.\n * - `tier='audit'` ⇔ `pool IS NULL AND direction IS NULL`.\n * - `tier='domain'` ⇒ `pool` and `direction` are populated (the\n * DrizzleEventBus inserts always supply them; the bus stamps them\n * in AUDIT-3).\n */\n tierRoutingCheck: check(\n 'domain_events_tier_routing_check',\n sql`${t.tier} in ('domain','audit') AND ((${t.tier} = 'audit') = (${t.pool} is null and ${t.direction} is null))`,\n ),\n }),\n);\n\nexport type DomainEventRecord = InferSelectModel<typeof domainEvents>;\n"],"mappings":";AAsCA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,WAAW;AAGb,IAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AAAA,IACE,IAAI,KAAK,IAAI,EAAE,WAAW;AAAA,IAC1B,MAAM,KAAK,MAAM,EAAE,QAAQ;AAAA,IAC3B,aAAa,KAAK,cAAc,EAAE,QAAQ;AAAA,IAC1C,eAAe,KAAK,gBAAgB,EAAE,QAAQ;AAAA,IAC9C,SAAS,MAAM,SAAS,EAAE,QAAQ,EAAE,MAA+B;AAAA,IACnE,YAAY,UAAU,eAAe,EAAE,cAAc,KAAK,CAAC,EAAE,QAAQ;AAAA,IACrE,aAAa,UAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC;AAAA;AAAA,IAE7D,QAAQ,KAAK,QAAQ,EAAE,QAAQ,EAAE,QAAQ,SAAS;AAAA;AAAA,IAElD,OAAO,KAAK,OAAO;AAAA,IACnB,UAAU,MAAM,UAAU,EAAE,MAA+B;AAAA;AAAA,IAE3D,MAAM,KAAK,MAAM;AAAA;AAAA,IAEjB,WAAW,KAAK,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAO3B,MAAM,KAAK,MAAM,EAAE,QAAQ,EAAE,QAAQ,QAAQ;AAAA;AAAA,IAE7C,UAAU,KAAK,WAAW;AAAA,EAC5B;AAAA,EACA,CAAC,OAAO;AAAA;AAAA,IAEN,iCAAiC,MAAM,sCAAsC,EAAE;AAAA,MAC7E,EAAE;AAAA,MACF,EAAE;AAAA,IACJ;AAAA;AAAA,IAEA,0BAA0B,MAAM,6BAA6B,EAAE;AAAA,MAC7D,EAAE;AAAA,MACF,EAAE;AAAA,IACJ;AAAA;AAAA,IAEA,qCAAqC;AAAA,MACnC;AAAA,IACF,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU;AAAA;AAAA,IAEnC,qCAAqC;AAAA,MACnC;AAAA,IACF,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAWnC,6BAA6B;AAAA,MAC3B;AAAA,IACF,EACG,GAAG,EAAE,MAAM,OAAO,EAAE,QAAQ,sBAAsB,EAClD,MAAM,MAAM,EAAE,QAAQ,iCAAiC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAS1D,kBAAkB;AAAA,MAChB;AAAA,MACA,MAAM,EAAE,IAAI,gCAAgC,EAAE,IAAI,kBAAkB,EAAE,IAAI,gBAAgB,EAAE,SAAS;AAAA,IACvG;AAAA,EACF;AACF;","names":[]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-AQFQ4BYM.js";
|
|
4
4
|
import {
|
|
5
5
|
ObservabilityService
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-W2UIDI3R.js";
|
|
7
7
|
import {
|
|
8
8
|
OBSERVABILITY,
|
|
9
9
|
OBSERVABILITY_MODULE_OPTIONS
|
|
@@ -45,4 +45,4 @@ ObservabilityModule = __decorateClass([
|
|
|
45
45
|
export {
|
|
46
46
|
ObservabilityModule
|
|
47
47
|
};
|
|
48
|
-
//# sourceMappingURL=chunk-
|
|
48
|
+
//# sourceMappingURL=chunk-E45CSC33.js.map
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clampEventLimit,
|
|
3
|
-
decodeEventCursor,
|
|
4
|
-
encodeEventCursor
|
|
5
|
-
} from "./chunk-UQ5EHOH2.js";
|
|
6
1
|
import {
|
|
7
2
|
EVENTS_WAKE_CHANNEL,
|
|
8
3
|
PgNotifyListener,
|
|
9
4
|
pgNotify
|
|
10
5
|
} from "./chunk-Q6LRJ4VI.js";
|
|
11
|
-
import {
|
|
12
|
-
EVENTS_MODULE_OPTIONS
|
|
13
|
-
} from "./chunk-H5NH7KPE.js";
|
|
14
6
|
import {
|
|
15
7
|
BRIDGE_OUTBOX_DRAIN_HOOK
|
|
16
8
|
} from "./chunk-4LH67P4U.js";
|
|
17
9
|
import {
|
|
18
10
|
domainEvents
|
|
19
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-E2BRT5IB.js";
|
|
12
|
+
import {
|
|
13
|
+
EVENTS_MODULE_OPTIONS
|
|
14
|
+
} from "./chunk-H5NH7KPE.js";
|
|
15
|
+
import {
|
|
16
|
+
clampEventLimit,
|
|
17
|
+
decodeEventCursor,
|
|
18
|
+
encodeEventCursor
|
|
19
|
+
} from "./chunk-UQ5EHOH2.js";
|
|
20
20
|
import {
|
|
21
21
|
DRIZZLE
|
|
22
22
|
} from "./chunk-U64T4YZE.js";
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "./chunk-2E224ZSN.js";
|
|
27
27
|
|
|
28
28
|
// runtime/subsystems/events/event-bus.drizzle-backend.ts
|
|
29
|
+
import { randomUUID } from "crypto";
|
|
29
30
|
import { Injectable, Inject, Logger, Optional } from "@nestjs/common";
|
|
30
31
|
import { eq, and, inArray, asc, desc, gte, lt, or, sql } from "drizzle-orm";
|
|
31
32
|
var POLL_INTERVAL_MS = 1e3;
|
|
@@ -75,6 +76,10 @@ function toEventSummary(r) {
|
|
|
75
76
|
processedAt: r.processedAt == null ? null : r.processedAt instanceof Date ? r.processedAt : new Date(r.processedAt)
|
|
76
77
|
};
|
|
77
78
|
}
|
|
79
|
+
function isUniqueViolation(err) {
|
|
80
|
+
const code = err;
|
|
81
|
+
return code?.code === "23505" || code?.cause?.code === "23505";
|
|
82
|
+
}
|
|
78
83
|
var DrizzleEventBus = class {
|
|
79
84
|
constructor(db, opts, bridgeHook = null) {
|
|
80
85
|
this.db = db;
|
|
@@ -225,6 +230,68 @@ var DrizzleEventBus = class {
|
|
|
225
230
|
};
|
|
226
231
|
}
|
|
227
232
|
// ============================================================================
|
|
233
|
+
// ADR-039 — scheduled-event materialisation (time as an event source)
|
|
234
|
+
// ============================================================================
|
|
235
|
+
/**
|
|
236
|
+
* Insert one scheduled tick event idempotently. The slot key is stamped onto
|
|
237
|
+
* `metadata.scheduleSlot`; `ON CONFLICT DO NOTHING` against the partial UNIQUE
|
|
238
|
+
* expression index `idx_domain_events_schedule_slot` makes a duplicate insert
|
|
239
|
+
* a no-op — the DB constraint is the exactly-one-event-per-slot invariant.
|
|
240
|
+
*
|
|
241
|
+
* Reuses the standard outbox row shape (pool/direction/metadata) so the
|
|
242
|
+
* existing drain carries the tick like any other event. A LISTEN/NOTIFY wake
|
|
243
|
+
* fires for an immediately-due tick (boot/catch-up rows whose slot is already
|
|
244
|
+
* in the past); a future slot is claimed by polling once `occurred_at` passes.
|
|
245
|
+
*/
|
|
246
|
+
async materializeScheduledEvent(spec) {
|
|
247
|
+
const multiTenant = this.opts.multiTenant ?? false;
|
|
248
|
+
const metadata = {
|
|
249
|
+
pool: spec.pool,
|
|
250
|
+
direction: spec.direction,
|
|
251
|
+
scheduleSlot: spec.slotKey,
|
|
252
|
+
triggerSource: "schedule"
|
|
253
|
+
};
|
|
254
|
+
const base = {
|
|
255
|
+
id: randomUUID(),
|
|
256
|
+
type: spec.type,
|
|
257
|
+
// Payload-free scheduled fact (the dealbrain strict-producer pattern).
|
|
258
|
+
aggregateId: spec.type,
|
|
259
|
+
aggregateType: spec.type,
|
|
260
|
+
payload: {},
|
|
261
|
+
occurredAt: spec.slotStart,
|
|
262
|
+
processedAt: null,
|
|
263
|
+
status: "pending",
|
|
264
|
+
metadata,
|
|
265
|
+
pool: spec.pool,
|
|
266
|
+
direction: spec.direction,
|
|
267
|
+
tier: "domain"
|
|
268
|
+
};
|
|
269
|
+
const values = multiTenant ? { ...base, tenantId: null } : base;
|
|
270
|
+
try {
|
|
271
|
+
await this.db.insert(domainEvents).values(values);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (isUniqueViolation(err)) return { created: false };
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
if (spec.slotStart.getTime() <= Date.now()) {
|
|
277
|
+
await this.emitWakeNotify(this.db, [spec.pool]);
|
|
278
|
+
}
|
|
279
|
+
return { created: true };
|
|
280
|
+
}
|
|
281
|
+
/** Most recent scheduled tick's `occurred_at` (epoch ms) for `type`, or null.
|
|
282
|
+
* Read by the scheduler's catch-up backfill. */
|
|
283
|
+
async lastScheduledSlotMs(type) {
|
|
284
|
+
const rows = await this.db.select({ occurredAt: domainEvents.occurredAt }).from(domainEvents).where(
|
|
285
|
+
and(
|
|
286
|
+
eq(domainEvents.type, type),
|
|
287
|
+
sql`${domainEvents.metadata} ->> 'triggerSource' = 'schedule'`
|
|
288
|
+
)
|
|
289
|
+
).orderBy(desc(domainEvents.occurredAt)).limit(1);
|
|
290
|
+
const row = rows[0];
|
|
291
|
+
if (!row?.occurredAt) return null;
|
|
292
|
+
return row.occurredAt instanceof Date ? row.occurredAt.getTime() : new Date(row.occurredAt).getTime();
|
|
293
|
+
}
|
|
294
|
+
// ============================================================================
|
|
228
295
|
// IEventReadPort (OBS-LIST-1)
|
|
229
296
|
// ============================================================================
|
|
230
297
|
async listEvents(query = {}) {
|
|
@@ -393,4 +460,4 @@ DrizzleEventBus = __decorateClass([
|
|
|
393
460
|
export {
|
|
394
461
|
DrizzleEventBus
|
|
395
462
|
};
|
|
396
|
-
//# sourceMappingURL=chunk-
|
|
463
|
+
//# sourceMappingURL=chunk-HCAKMT64.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../runtime/subsystems/events/event-bus.drizzle-backend.ts"],"sourcesContent":["/**\n * DrizzleEventBus — Postgres-backed event bus using the transactional outbox pattern.\n *\n * Events are inserted into the `domain_events` table within the caller's\n * Drizzle transaction. A background polling loop (started on module init)\n * reads unprocessed events and dispatches them to registered subscribers.\n *\n * When the transaction rolls back, the event is never persisted — no\n * phantom events.\n *\n * Pool awareness (EVT-4):\n * - On `publish`/`publishMany` the backend writes `metadata.pool`,\n * `metadata.direction`, and `metadata.tenantId` into the first-class\n * `pool` / `direction` / `tenant_id` columns (metadata JSON is still\n * written unchanged for protocol stability).\n * - The drain loop filters by `opts.pools` when provided, so separate\n * processes (e.g. one per `events_inbound` / `events_change` /\n * `events_outbound`) can claim only their own lane. `pools: undefined`\n * drains all pending rows (backwards-compatible behaviour).\n *\n * EVT-Q7: No stale-event sweeper. `FOR UPDATE SKIP LOCKED` is\n * self-healing — the row is only locked for the duration of the\n * enclosing polling transaction; the `status='processed'` update happens\n * within that same transaction. There is no `claimed_at` semantic (unlike\n * jobs), so no stale rows can exist.\n *\n * This backend is suitable until you need real-time fan-out or very high\n * throughput. At that point, swap the backend for Redis Streams or similar\n * via EventsModule.forRoot({ backend: '...' }) without touching use cases.\n */\nimport { randomUUID } from 'node:crypto';\nimport { Injectable, OnModuleDestroy, OnModuleInit, Inject, Logger, Optional } from '@nestjs/common';\nimport { eq, and, inArray, asc, desc, gte, lt, or, sql, type SQL } from 'drizzle-orm';\nimport type {\n DomainEvent,\n DrizzleTransaction,\n IEventBus,\n ScheduledEventSpec,\n} from './event-bus.protocol';\nimport type {\n EventPage,\n IEventReadPort,\n ListEventsQuery,\n} from './event-read.protocol';\nimport {\n clampEventLimit,\n decodeEventCursor,\n encodeEventCursor,\n} from './event-keyset-cursor';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { domainEvents, type DomainEventRecord } from './domain-events.schema';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { EVENTS_MODULE_OPTIONS } from './events.tokens';\nimport type { EventsModuleOptions } from './events.module';\nimport { BRIDGE_OUTBOX_DRAIN_HOOK } from '../bridge/bridge.tokens';\nimport type { IBridgeOutboxDrainHook } from '../bridge/bridge.protocol';\nimport {\n EVENTS_WAKE_CHANNEL,\n PgNotifyListener,\n pgNotify,\n} from '../jobs/pg-notify';\n\n/** How long to wait between polling cycles (ms). */\nconst POLL_INTERVAL_MS = 1_000;\n/** Max events claimed per polling cycle to bound memory usage. */\nconst POLL_BATCH_SIZE = 50;\n\n/**\n * Row shape built from `metadata` for writing into `domain_events`. Keeps\n * the per-event extraction logic in one place so publish/publishMany stay\n * in sync.\n */\nfunction toInsertValues(event: DomainEvent, multiTenant: boolean) {\n const metadata = event.metadata ?? undefined;\n const pool = (metadata?.['pool'] as string | undefined) ?? null;\n const direction = (metadata?.['direction'] as string | undefined) ?? null;\n // AUDIT-1: tier defaults to 'domain' when absent. The DB CHECK\n // constraint (`domain_events_tier_routing_check`) enforces the\n // tier ⇔ routing-fields invariant at the storage boundary; no\n // JS-side assertion is needed here.\n const tier = (metadata?.['tier'] as string | undefined) ?? 'domain';\n const base = {\n id: event.id,\n type: event.type,\n aggregateId: event.aggregateId,\n aggregateType: event.aggregateType,\n payload: event.payload,\n occurredAt: event.occurredAt,\n processedAt: null,\n status: 'pending' as const,\n metadata: event.metadata,\n pool,\n direction,\n tier,\n };\n // EVT-8: `tenant_id` is a scaffold-time conditional column, emitted only\n // when `events.multi_tenant: true`. Only write it when multi-tenancy is\n // on — under single-tenant scaffolds the column does not exist, so the\n // key must be omitted from the insert.\n if (!multiTenant) return base;\n const tenantId = (metadata?.['tenantId'] as string | undefined) ?? null;\n return { ...base, tenantId };\n}\n\n/**\n * Project a raw `domain_events` row into the narrow `EventSummary` shape.\n * Shared with the memory backend via this helper kept module-local to each\n * backend (the events subsystem has no cross-backend projection file yet;\n * the two are byte-identical and small).\n */\nfunction toEventSummary(r: DomainEventRecord) {\n const metadata = (r.metadata ?? undefined) as\n | Record<string, unknown>\n | undefined;\n const rootRunId = metadata?.['rootRunId'];\n return {\n id: r.id,\n type: r.type,\n aggregateId: r.aggregateId,\n aggregateType: r.aggregateType,\n status: r.status,\n pool: r.pool,\n direction: r.direction,\n tier: r.tier,\n rootRunId: typeof rootRunId === 'string' ? rootRunId : null,\n // EVT-8: `tenant_id` is a scaffold-time conditional column. Read it\n // structurally so this projection typechecks against both the\n // multi-tenant schema (column present) and the single-tenant schema\n // (column absent → undefined → null).\n tenantId: (r as { tenantId?: string | null }).tenantId ?? null,\n occurredAt:\n r.occurredAt instanceof Date\n ? r.occurredAt\n : new Date(r.occurredAt as unknown as string),\n processedAt:\n r.processedAt == null\n ? null\n : r.processedAt instanceof Date\n ? r.processedAt\n : new Date(r.processedAt as unknown as string),\n };\n}\n\n/**\n * Postgres unique-violation (SQLSTATE 23505) test. Used by the scheduled-event\n * materialiser (ADR-039) to treat a slot-key collision as the\n * already-materialised no-op. Reads `.code` defensively across driver shapes\n * (node-postgres surfaces it on the error, some wrappers nest it on `.cause`).\n */\nfunction isUniqueViolation(err: unknown): boolean {\n const code = (err as { code?: unknown; cause?: { code?: unknown } } | undefined);\n return code?.code === '23505' || code?.cause?.code === '23505';\n}\n\n@Injectable()\nexport class DrizzleEventBus implements IEventBus, IEventReadPort, OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger(DrizzleEventBus.name);\n private polling = false;\n private pollTimer: ReturnType<typeof setTimeout> | null = null;\n private readonly handlers = new Map<string, Set<(event: DomainEvent) => Promise<void>>>();\n private readonly opts: EventsModuleOptions;\n\n // LISTEN-NOTIFY-1 — dedicated wake listener + debounce state. `null` when\n // `listenNotify` is off (the common case); polling is the only driver then.\n private notifyListener: PgNotifyListener | null = null;\n /** True while a wake-driven drain is in flight (debounce gate). */\n private wakeDraining = false;\n /** A notify arrived mid-drain → re-drain once when the current drain ends. */\n private wakeRecheckPending = false;\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Optional() @Inject(EVENTS_MODULE_OPTIONS) opts?: EventsModuleOptions,\n /**\n * Bridge subsystem hook (BRIDGE-4). Optional — when the bridge\n * subsystem is not installed in the consuming app, this token is\n * undefined and the drain skips the bridge block entirely (preserves\n * EVT-4 baseline behaviour).\n *\n * When provided, `processEvent` is invoked once per drained event\n * INSIDE the per-event tx, before `processed_at` is stamped. The\n * hook owns all knowledge of `bridge_delivery + wrapper job_run`\n * shapes; the events subsystem stays unaware of bridge schemas.\n */\n @Optional()\n @Inject(BRIDGE_OUTBOX_DRAIN_HOOK)\n private readonly bridgeHook: IBridgeOutboxDrainHook | null = null,\n ) {\n // Default so direct construction (e.g. integration tests not going\n // through Nest DI) keeps working without an explicit options object.\n this.opts = opts ?? { backend: 'drizzle' };\n }\n\n // ============================================================================\n // Lifecycle\n // ============================================================================\n\n async onModuleInit(): Promise<void> {\n this.polling = true;\n this.schedulePoll();\n\n // LISTEN-NOTIFY-1 — start the wake listener ALONGSIDE the poll timer. A\n // notify for one of this drainer's pools triggers an immediate drain; the\n // interval timer above stays the durability heartbeat. Startup is\n // fire-and-forget — a connect failure self-heals via the listener's backoff.\n if (this.opts.listenNotify) {\n const pool = (this.db as unknown as { $client?: unknown }).$client;\n if (!pool || typeof (pool as { connect?: unknown }).connect !== 'function') {\n this.logger.warn(\n `listen_notify enabled but the Drizzle client exposes no pg Pool ` +\n `($client.connect missing) — falling back to interval polling only.`,\n );\n } else {\n this.notifyListener = new PgNotifyListener({\n channel: EVENTS_WAKE_CHANNEL,\n pool: pool as { connect(): Promise<never> },\n label: 'events',\n onNotify: (payload) => this.onWake(payload),\n });\n await this.notifyListener.start();\n }\n }\n }\n\n async onModuleDestroy(): Promise<void> {\n this.polling = false;\n if (this.pollTimer) {\n clearTimeout(this.pollTimer);\n this.pollTimer = null;\n }\n if (this.notifyListener) {\n try {\n await this.notifyListener.stop();\n } catch (err) {\n this.logger.error(`notify listener stop failed: ${err}`);\n }\n this.notifyListener = null;\n }\n }\n\n /**\n * Wake handler — a `codegen_events_wake` notification arrived. A pool-filtered\n * drainer (`opts.pools` set) ignores payloads naming a pool it doesn't own; an\n * all-pools drainer wakes for any. Debounced: a notify mid-drain just flags a\n * re-check so a burst collapses to at most one extra drain (D3).\n */\n private onWake(payload: string): void {\n if (!this.polling) return;\n const pools = this.opts.pools;\n if (pools && pools.length > 0 && !pools.includes(payload)) return;\n if (this.wakeDraining) {\n this.wakeRecheckPending = true;\n return;\n }\n void this.drainOnWake();\n }\n\n private async drainOnWake(): Promise<void> {\n this.wakeDraining = true;\n try {\n do {\n this.wakeRecheckPending = false;\n await this.processBatch();\n } while (this.wakeRecheckPending && this.polling);\n } catch (err) {\n this.logger.error(`wake drain error: ${err}`);\n } finally {\n this.wakeDraining = false;\n }\n }\n\n // ============================================================================\n // IEventBus\n // ============================================================================\n\n async publish(event: DomainEvent, tx?: DrizzleTransaction): Promise<void> {\n const client = (tx ?? this.db) as DrizzleClient;\n const multiTenant = this.opts.multiTenant ?? false;\n const values = toInsertValues(event, multiTenant);\n await client.insert(domainEvents).values(values);\n // LISTEN-NOTIFY-1 — wake the drainer on commit (D2: emitted through the same\n // `client`, so a rolled-back publish emits no phantom wake). The pool is the\n // payload; the drainer re-runs its own pool-filtered claim on wake.\n await this.emitWakeNotify(client, [values.pool]);\n }\n\n async publishMany(events: DomainEvent[], tx?: DrizzleTransaction): Promise<void> {\n if (events.length === 0) return;\n const client = (tx ?? this.db) as DrizzleClient;\n const multiTenant = this.opts.multiTenant ?? false;\n const valuesList = events.map((e) => toInsertValues(e, multiTenant));\n await client.insert(domainEvents).values(valuesList);\n // De-dup pools so a batch into one lane emits a single wake.\n await this.emitWakeNotify(client, valuesList.map((v) => v.pool));\n }\n\n /**\n * Emit one in-tx `pg_notify(codegen_events_wake, <pool>)` per distinct pool in\n * the just-inserted batch. No-op unless `listenNotify` is on. Best-effort: a\n * notify failure is non-fatal (interval polling still drains the rows), so we\n * log + swallow rather than failing the publish.\n */\n private async emitWakeNotify(\n client: DrizzleClient,\n pools: Array<string | null>,\n ): Promise<void> {\n if (!this.opts.listenNotify) return;\n const distinct = new Set(pools.map((p) => p ?? ''));\n for (const pool of distinct) {\n try {\n await pgNotify(client, EVENTS_WAKE_CHANNEL, pool);\n } catch (err) {\n this.logger.warn(\n `pg_notify(${EVENTS_WAKE_CHANNEL}, '${pool}') failed: ${err} ` +\n `(non-fatal — interval polling still drains the outbox).`,\n );\n }\n }\n }\n\n async findById(eventId: string): Promise<DomainEvent | null> {\n const rows = await this.db\n .select()\n .from(domainEvents)\n .where(eq(domainEvents.id, eventId))\n .limit(1);\n const row = rows[0];\n if (!row) return null;\n return {\n id: row.id,\n type: row.type,\n aggregateId: row.aggregateId,\n aggregateType: row.aggregateType,\n payload: row.payload as Record<string, unknown>,\n occurredAt:\n row.occurredAt instanceof Date\n ? row.occurredAt\n : new Date(row.occurredAt as unknown as string),\n metadata: (row.metadata ?? undefined) as\n | Record<string, unknown>\n | undefined,\n };\n }\n\n subscribe<T extends DomainEvent = DomainEvent>(\n eventType: string,\n handler: (event: T) => Promise<void>,\n ): () => void {\n if (!this.handlers.has(eventType)) {\n this.handlers.set(eventType, new Set());\n }\n const set = this.handlers.get(eventType)!;\n const h = handler as (event: DomainEvent) => Promise<void>;\n set.add(h);\n return () => {\n set.delete(h);\n };\n }\n\n // ============================================================================\n // ADR-039 — scheduled-event materialisation (time as an event source)\n // ============================================================================\n\n /**\n * Insert one scheduled tick event idempotently. The slot key is stamped onto\n * `metadata.scheduleSlot`; `ON CONFLICT DO NOTHING` against the partial UNIQUE\n * expression index `idx_domain_events_schedule_slot` makes a duplicate insert\n * a no-op — the DB constraint is the exactly-one-event-per-slot invariant.\n *\n * Reuses the standard outbox row shape (pool/direction/metadata) so the\n * existing drain carries the tick like any other event. A LISTEN/NOTIFY wake\n * fires for an immediately-due tick (boot/catch-up rows whose slot is already\n * in the past); a future slot is claimed by polling once `occurred_at` passes.\n */\n async materializeScheduledEvent(\n spec: ScheduledEventSpec,\n ): Promise<{ created: boolean }> {\n const multiTenant = this.opts.multiTenant ?? false;\n const metadata: Record<string, unknown> = {\n pool: spec.pool,\n direction: spec.direction,\n scheduleSlot: spec.slotKey,\n triggerSource: 'schedule',\n };\n const base = {\n id: randomUUID(),\n type: spec.type,\n // Payload-free scheduled fact (the dealbrain strict-producer pattern).\n aggregateId: spec.type,\n aggregateType: spec.type,\n payload: {} as Record<string, unknown>,\n occurredAt: spec.slotStart,\n processedAt: null,\n status: 'pending' as const,\n metadata,\n pool: spec.pool,\n direction: spec.direction,\n tier: 'domain' as const,\n };\n const values = multiTenant ? { ...base, tenantId: null } : base;\n\n // The idempotency guard is the partial UNIQUE expression index\n // `idx_domain_events_schedule_slot` on (type, metadata->>'scheduleSlot').\n // Drizzle 0.45's typed `onConflictDoNothing({ target })` only accepts\n // columns, so it can't name an expression index — we instead let the\n // insert run and treat a unique-violation (SQLSTATE 23505) as the\n // already-materialised no-op. This is the exactly-one-per-slot invariant:\n // a concurrent or repeat materialise of the same slot loses the race at\n // the DB and reports `created: false`.\n try {\n await this.db.insert(domainEvents).values(values);\n } catch (err) {\n if (isUniqueViolation(err)) return { created: false };\n throw err;\n }\n\n // Wake the drainer for an already-due tick. A future slot waits for polling.\n if (spec.slotStart.getTime() <= Date.now()) {\n await this.emitWakeNotify(this.db, [spec.pool]);\n }\n return { created: true };\n }\n\n /** Most recent scheduled tick's `occurred_at` (epoch ms) for `type`, or null.\n * Read by the scheduler's catch-up backfill. */\n async lastScheduledSlotMs(type: string): Promise<number | null> {\n const rows = await this.db\n .select({ occurredAt: domainEvents.occurredAt })\n .from(domainEvents)\n .where(\n and(\n eq(domainEvents.type, type),\n sql`${domainEvents.metadata} ->> 'triggerSource' = 'schedule'`,\n ),\n )\n .orderBy(desc(domainEvents.occurredAt))\n .limit(1);\n const row = rows[0];\n if (!row?.occurredAt) return null;\n return row.occurredAt instanceof Date\n ? row.occurredAt.getTime()\n : new Date(row.occurredAt as unknown as string).getTime();\n }\n\n // ============================================================================\n // IEventReadPort (OBS-LIST-1)\n // ============================================================================\n\n async listEvents(query: ListEventsQuery = {}): Promise<EventPage> {\n const limit = clampEventLimit(query.limit);\n const conditions: SQL<unknown>[] = [];\n\n if (query.poolId) conditions.push(eq(domainEvents.pool, query.poolId));\n if (query.direction)\n conditions.push(eq(domainEvents.direction, query.direction));\n if (query.since) conditions.push(gte(domainEvents.occurredAt, query.since));\n if (query.rootRunId) {\n // Filter on the JSON correlation id: metadata->>'rootRunId'.\n conditions.push(\n sql`${domainEvents.metadata}->>'rootRunId' = ${query.rootRunId}`,\n );\n }\n // EVT-8: `tenant_id` is a scaffold-time conditional column (emitted only\n // under `events.multi_tenant: true`). Guard the filter behind the same\n // `multiTenant` flag, and read the column structurally so this backend\n // typechecks against both the multi-tenant schema (column present) and\n // the single-tenant schema (column absent). When multi-tenancy is off\n // there is no `tenant_id` column to filter on.\n if (this.opts.multiTenant && query.tenantId !== undefined) {\n const tenantIdColumn = (\n domainEvents as unknown as { tenantId: typeof domainEvents.pool }\n ).tenantId;\n conditions.push(\n query.tenantId === null\n ? (sql`${tenantIdColumn} is null` as SQL<unknown>)\n : eq(tenantIdColumn, query.tenantId),\n );\n }\n\n // Keyset seek: WHERE (occurred_at, id) < (cursorOccurredAt, cursorId).\n if (query.cursor) {\n const keyset = decodeEventCursor(query.cursor);\n if (keyset) {\n conditions.push(\n or(\n lt(domainEvents.occurredAt, keyset.occurredAt),\n and(\n eq(domainEvents.occurredAt, keyset.occurredAt),\n lt(domainEvents.id, keyset.id),\n ),\n )!,\n );\n }\n }\n\n const rows = (await this.db\n .select()\n .from(domainEvents)\n .where(conditions.length > 0 ? and(...conditions) : undefined)\n .orderBy(desc(domainEvents.occurredAt), desc(domainEvents.id))\n .limit(limit + 1)) as DomainEventRecord[];\n\n const hasMore = rows.length > limit;\n const page = hasMore ? rows.slice(0, limit) : rows;\n const items = page.map(toEventSummary);\n const last = page[page.length - 1];\n const nextCursor =\n hasMore && last\n ? encodeEventCursor({ occurredAt: last.occurredAt, id: last.id })\n : null;\n\n return { items, nextCursor };\n }\n\n // ============================================================================\n // Polling\n // ============================================================================\n\n /**\n * Test-only hook. Runs exactly one drain cycle and returns. Production\n * code goes through `onModuleInit` → `schedulePoll`, which calls the\n * same `processBatch` under a timer.\n */\n async drainOnce(): Promise<void> {\n await this.processBatch();\n }\n\n private schedulePoll(): void {\n if (!this.polling) return;\n this.pollTimer = setTimeout(async () => {\n try {\n await this.processBatch();\n } catch (err) {\n this.logger.error(`Poll cycle error: ${err}`);\n } finally {\n this.schedulePoll();\n }\n }, POLL_INTERVAL_MS);\n }\n\n /**\n * Drain one batch (BRIDGE-4 restructure of EVT-4).\n *\n * Two-phase per drained event:\n *\n * 1. **Per-event transaction** — bridge fanout (`bridgeHook.processEvent`)\n * + `processed_at` stamp. Both write through the same `tx`. A throw\n * inside the tx (only infra-level failures should reach here, since\n * the hook tolerates null direction and registry misses inline)\n * rolls back the bridge inserts AND the `processed_at` stamp; the\n * event re-claims on the next drain cycle. Bridge `UNIQUE\n * (event_id, trigger_id)` makes the retry idempotent.\n *\n * 2. **After commit** — dispatch in-process subscribers (`IEventBus.subscribe`\n * handlers). This deliberately runs OUTSIDE the per-event tx (lead\n * decision 2026-04-22): subscribers are best-effort and must not\n * gate forward progress or roll back bridge fanout. Subscriber\n * errors are caught + logged; `processed_at` is already committed.\n * The old `MAX_RETRIES=3` in-process retry loop and the\n * `failed`-stamping path were removed in BRIDGE-4 along with their\n * coupling.\n *\n * The `processed_at` UPDATE carries `AND status='pending'` (BRIDGE-4\n * tightening — without it, a hypothetical double-claim could double-stamp\n * the timestamp). The per-event tx + `FOR UPDATE SKIP LOCKED` claim\n * make this defensive belt-and-suspenders.\n */\n private async processBatch(): Promise<void> {\n const pools = this.opts.pools;\n\n // Build WHERE: status='pending' [AND pool IN (...)]\n const whereClause: SQL<unknown> = pools && pools.length > 0\n ? (and(eq(domainEvents.status, 'pending'), inArray(domainEvents.pool, pools)) as SQL<unknown>)\n : eq(domainEvents.status, 'pending');\n\n // Claim a batch with FOR UPDATE SKIP LOCKED so multiple pollers don't\n // double-dispatch. The lock is released when the outer transaction\n // commits — which is fine because the immediately-following per-event\n // tx flips status='processed' under its own `AND status='pending'`\n // guard, so a re-claim of the same row in a subsequent batch is a\n // no-op UPDATE.\n const rows = await this.db.transaction(async (tx) => {\n return tx\n .select()\n .from(domainEvents)\n .where(whereClause)\n .orderBy(asc(domainEvents.occurredAt))\n .limit(POLL_BATCH_SIZE)\n .for('update', { skipLocked: true });\n }) as Array<typeof domainEvents.$inferSelect>;\n\n for (const row of rows) {\n const event: DomainEvent = {\n id: row.id,\n type: row.type,\n aggregateId: row.aggregateId,\n aggregateType: row.aggregateType,\n payload: row.payload as Record<string, unknown>,\n occurredAt: row.occurredAt instanceof Date ? row.occurredAt : new Date(row.occurredAt as unknown as string),\n metadata: (row.metadata ?? undefined) as Record<string, unknown> | undefined,\n };\n\n // Phase 1 — per-event tx: bridge fanout + processed_at stamp.\n try {\n await this.db.transaction(async (tx) => {\n if (this.bridgeHook) {\n await this.bridgeHook.processEvent(event, tx);\n }\n await tx\n .update(domainEvents)\n .set({ status: 'processed', processedAt: new Date() })\n .where(\n and(\n eq(domainEvents.id, event.id),\n eq(domainEvents.status, 'pending'),\n ),\n );\n });\n } catch (err) {\n // Infra-level failure inside the per-event tx — bridge inserts\n // and processed_at both rolled back. Log and move on; the next\n // drain cycle re-claims the row. UNIQUE on bridge_delivery makes\n // the retry idempotent.\n this.logger.error(\n `Per-event tx failed for event id=${event.id} type=${event.type}: ${err}`,\n );\n continue;\n }\n\n // Phase 2 — best-effort subscriber dispatch. Errors are logged\n // and discarded; processed_at is already committed. Subscribers\n // are observability + cache-busts + small ancillary work; they\n // must not gate forward progress.\n try {\n await this.dispatch(event);\n } catch (err) {\n this.logger.error(\n `Subscriber dispatch failed for event id=${event.id} type=${event.type} ` +\n `(processed_at already committed; failure does not retry): ${err}`,\n );\n }\n }\n }\n\n private async dispatch(event: DomainEvent): Promise<void> {\n const set = this.handlers.get(event.type);\n if (!set) return;\n\n let firstError: unknown;\n for (const handler of set) {\n try {\n await handler(event);\n } catch (err) {\n this.logger.error(\n `Handler error for event type \"${event.type}\" (id: ${event.id}): ${err}`,\n );\n if (firstError === undefined) {\n firstError = err;\n }\n }\n }\n\n if (firstError !== undefined) {\n throw firstError;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PollChangeSource
|
|
3
|
-
} from "./chunk-4MF3HKJA.js";
|
|
4
1
|
import {
|
|
5
2
|
WebhookChangeSource
|
|
6
3
|
} from "./chunk-TIZXQU26.js";
|
|
4
|
+
import {
|
|
5
|
+
PollChangeSource
|
|
6
|
+
} from "./chunk-4MF3HKJA.js";
|
|
7
7
|
|
|
8
8
|
// runtime/subsystems/integration/build-change-source.ts
|
|
9
9
|
function buildChangeSource(cfg, fetch, middlewares = []) {
|
|
@@ -26,4 +26,4 @@ function buildChangeSource(cfg, fetch, middlewares = []) {
|
|
|
26
26
|
export {
|
|
27
27
|
buildChangeSource
|
|
28
28
|
};
|
|
29
|
-
//# sourceMappingURL=chunk-
|
|
29
|
+
//# sourceMappingURL=chunk-I6UXRJ3Q.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getEventMetadata
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JOBQ6RUU.js";
|
|
4
4
|
import {
|
|
5
5
|
eventPayloadSchemas
|
|
6
6
|
} from "./chunk-JRQO2IOF.js";
|
|
7
7
|
import {
|
|
8
8
|
MissingTenantIdError
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-DUBZOXJC.js";
|
|
10
10
|
import {
|
|
11
11
|
EVENTS_MULTI_TENANT,
|
|
12
12
|
EVENT_BUS
|
|
@@ -89,4 +89,4 @@ TypedEventBus = __decorateClass([
|
|
|
89
89
|
export {
|
|
90
90
|
TypedEventBus
|
|
91
91
|
};
|
|
92
|
-
//# sourceMappingURL=chunk-
|
|
92
|
+
//# sourceMappingURL=chunk-INO47JXD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../runtime/subsystems/events/generated/registry.ts"],"sourcesContent":["// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.\n// Run `codegen entity new --all` to refresh.\n\n\nimport type { EventTypeName } from './types';\n\nexport interface EventMetadata {\n\ttype: EventTypeName;\n\ttier: 'domain' | 'audit';\n\tdirection: 'inbound' | 'change' | 'outbound' | null;\n\tpool: 'events_inbound' | 'events_change' | 'events_outbound' | null;\n\taggregate?: string;\n\tsource?: string;\n\tdestination?: string;\n\tversion: number;\n\tretry: { attempts: number; backoff: 'linear' | 'exponential' };\n\tschedule?: { every: string | number; align: boolean; catchUp: boolean; maxCatchUpSlots: number };\n}\n\nexport const eventRegistry = {\n\t'contact_created': {\n\t\ttype: 'contact_created',\n\t\ttier: 'domain',\n\t\tdirection: 'change',\n\t\tpool: 'events_change',\n\t\taggregate: 'contact',\n\t\tversion: 1,\n\t\tretry: { attempts: 3, backoff: 'exponential' },\n\t},\n\t'contact_marked_champion': {\n\t\ttype: 'contact_marked_champion',\n\t\ttier: 'domain',\n\t\tdirection: 'change',\n\t\tpool: 'events_change',\n\t\taggregate: 'contact',\n\t\tversion: 1,\n\t\tretry: { attempts: 3, backoff: 'exponential' },\n\t},\n\t'contact_merged': {\n\t\ttype: 'contact_merged',\n\t\ttier: 'domain',\n\t\tdirection: 'change',\n\t\tpool: 'events_change',\n\t\taggregate: 'contact',\n\t\tversion: 1,\n\t\tretry: { attempts: 3, backoff: 'exponential' },\n\t},\n\t'crm_sync_started': {\n\t\ttype: 'crm_sync_started',\n\t\ttier: 'audit',\n\t\tdirection: null,\n\t\tpool: null,\n\t\tversion: 1,\n\t\tretry: { attempts: 3, backoff: 'exponential' },\n\t},\n\t'deal_created': {\n\t\ttype: 'deal_created',\n\t\ttier: 'domain',\n\t\tdirection: 'change',\n\t\tpool: 'events_change',\n\t\taggregate: 'deal',\n\t\tversion: 1,\n\t\tretry: { attempts: 3, backoff: 'exponential' },\n\t},\n\t'deal_stage_changed': {\n\t\ttype: 'deal_stage_changed',\n\t\ttier: 'domain',\n\t\tdirection: 'change',\n\t\tpool: 'events_change',\n\t\taggregate: 'deal',\n\t\tversion: 1,\n\t\tretry: { attempts: 3, backoff: 'exponential' },\n\t},\n\t'stripe_payment_received': {\n\t\ttype: 'stripe_payment_received',\n\t\ttier: 'domain',\n\t\tdirection: 'inbound',\n\t\tpool: 'events_inbound',\n\t\tsource: 'stripe',\n\t\tversion: 1,\n\t\tretry: { attempts: 5, backoff: 'exponential' },\n\t},\n\t'webhook_outbound_contact_sync': {\n\t\ttype: 'webhook_outbound_contact_sync',\n\t\ttier: 'domain',\n\t\tdirection: 'outbound',\n\t\tpool: 'events_outbound',\n\t\taggregate: 'contact',\n\t\tdestination: 'crm',\n\t\tversion: 1,\n\t\tretry: { attempts: 3, backoff: 'exponential' },\n\t},\n} as const satisfies Record<EventTypeName, EventMetadata>;\n\nexport function getEventMetadata<T extends EventTypeName>(type: T): EventMetadata {\n\tconst meta = eventRegistry[type];\n\tif (!meta) {\n\t\tthrow new Error(`No registry entry for event type '${String(type)}' — declare events under events/*.yaml and re-run \\`codegen entity new --all\\`.`);\n\t}\n\treturn meta;\n}\n"],"mappings":";AAmBO,IAAM,gBAAgB;AAAA,EAC5B,mBAAmB;AAAA,IAClB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW;AAAA,IACX,MAAM;AAAA,IACN,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO,EAAE,UAAU,GAAG,SAAS,cAAc;AAAA,EAC9C;AAAA,EACA,2BAA2B;AAAA,IAC1B,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW;AAAA,IACX,MAAM;AAAA,IACN,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO,EAAE,UAAU,GAAG,SAAS,cAAc;AAAA,EAC9C;AAAA,EACA,kBAAkB;AAAA,IACjB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW;AAAA,IACX,MAAM;AAAA,IACN,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO,EAAE,UAAU,GAAG,SAAS,cAAc;AAAA,EAC9C;AAAA,EACA,oBAAoB;AAAA,IACnB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW;AAAA,IACX,MAAM;AAAA,IACN,SAAS;AAAA,IACT,OAAO,EAAE,UAAU,GAAG,SAAS,cAAc;AAAA,EAC9C;AAAA,EACA,gBAAgB;AAAA,IACf,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW;AAAA,IACX,MAAM;AAAA,IACN,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO,EAAE,UAAU,GAAG,SAAS,cAAc;AAAA,EAC9C;AAAA,EACA,sBAAsB;AAAA,IACrB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW;AAAA,IACX,MAAM;AAAA,IACN,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO,EAAE,UAAU,GAAG,SAAS,cAAc;AAAA,EAC9C;AAAA,EACA,2BAA2B;AAAA,IAC1B,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW;AAAA,IACX,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,OAAO,EAAE,UAAU,GAAG,SAAS,cAAc;AAAA,EAC9C;AAAA,EACA,iCAAiC;AAAA,IAChC,MAAM;AAAA,IACN,MAAM;AAAA,IACN,WAAW;AAAA,IACX,MAAM;AAAA,IACN,WAAW;AAAA,IACX,aAAa;AAAA,IACb,SAAS;AAAA,IACT,OAAO,EAAE,UAAU,GAAG,SAAS,cAAc;AAAA,EAC9C;AACD;AAEO,SAAS,iBAA0C,MAAwB;AACjF,QAAM,OAAO,cAAc,IAAI;AAC/B,MAAI,CAAC,MAAM;AACV,UAAM,IAAI,MAAM,qCAAqC,OAAO,IAAI,CAAC,sFAAiF;AAAA,EACnJ;AACA,SAAO;AACR;","names":[]}
|
|
@@ -625,6 +625,22 @@ var RetrySchema = z2.object({
|
|
|
625
625
|
attempts: z2.number().int().min(0).max(20),
|
|
626
626
|
backoff: z2.enum(EVENT_BACKOFF_STRATEGIES)
|
|
627
627
|
}).strict();
|
|
628
|
+
var DURATION_RE = /^\s*[0-9]*\.?[0-9]+\s*(ms|s|m|h|d)\s*$/;
|
|
629
|
+
var ScheduleSchema = z2.object({
|
|
630
|
+
every: z2.union([
|
|
631
|
+
z2.string().regex(
|
|
632
|
+
DURATION_RE,
|
|
633
|
+
"schedule.every must be a duration like '1h', '30m', '15s', '500ms', '1d'"
|
|
634
|
+
),
|
|
635
|
+
z2.number().positive().finite()
|
|
636
|
+
]),
|
|
637
|
+
/** Epoch-anchored slot boundaries (default true). */
|
|
638
|
+
align: z2.boolean().optional().default(true),
|
|
639
|
+
/** Backfill missed slots on recovery (default false → run once). */
|
|
640
|
+
catchUp: z2.boolean().optional().default(false),
|
|
641
|
+
/** Upper bound on `catchUp` backfill (default 1000). */
|
|
642
|
+
maxCatchUpSlots: z2.number().int().positive().optional().default(1e3)
|
|
643
|
+
}).strict();
|
|
628
644
|
var SNAKE_CASE_RE = /^[a-z][a-z0-9_]*$/;
|
|
629
645
|
var EventDefinitionSchemaCore = z2.object({
|
|
630
646
|
type: z2.string().regex(
|
|
@@ -645,6 +661,9 @@ var EventDefinitionSchemaCore = z2.object({
|
|
|
645
661
|
attempts: 3,
|
|
646
662
|
backoff: "exponential"
|
|
647
663
|
}),
|
|
664
|
+
// ADR-039 — declarative time-based emission. Optional; when present the
|
|
665
|
+
// platform emits this event on the given cadence (see ScheduleSchema).
|
|
666
|
+
schedule: ScheduleSchema.optional(),
|
|
648
667
|
version: z2.number().int().min(1).optional().default(1),
|
|
649
668
|
description: z2.string().optional()
|
|
650
669
|
}).strict();
|
|
@@ -665,6 +684,13 @@ var EventDefinitionSchemaRefined = EventDefinitionSchemaCore.superRefine(
|
|
|
665
684
|
path: ["direction"]
|
|
666
685
|
});
|
|
667
686
|
}
|
|
687
|
+
if (data.schedule !== void 0) {
|
|
688
|
+
ctx.addIssue({
|
|
689
|
+
code: z2.ZodIssueCode.custom,
|
|
690
|
+
message: `Event '${data.type}' is tier:audit; 'schedule' is not allowed on audit events (they route to no pool and cannot drive the bridge). Make it a domain event with a direction. See ADR-039.`,
|
|
691
|
+
path: ["schedule"]
|
|
692
|
+
});
|
|
693
|
+
}
|
|
668
694
|
return;
|
|
669
695
|
}
|
|
670
696
|
if (data.direction === void 0) {
|
|
@@ -4163,4 +4189,4 @@ export {
|
|
|
4163
4189
|
analyzeDomain,
|
|
4164
4190
|
validateEntities
|
|
4165
4191
|
};
|
|
4166
|
-
//# sourceMappingURL=chunk-
|
|
4192
|
+
//# sourceMappingURL=chunk-KK5A7B2T.js.map
|