@pattern-stack/codegen 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/consumer-skills/events/authoring-events.md +31 -0
  3. package/dist/{chunk-COGHTKXY.js → chunk-27ETSJ2X.js} +2 -2
  4. package/dist/{chunk-235ZMMJR.js → chunk-3YCUIGPG.js} +10 -10
  5. package/dist/{chunk-Z7PQCAVK.js → chunk-4OC5MSHO.js} +50 -4
  6. package/dist/chunk-4OC5MSHO.js.map +1 -0
  7. package/dist/{chunk-VNBC3VXM.js → chunk-5LXOJGO2.js} +6 -6
  8. package/dist/{chunk-7OVCARTQ.js → chunk-5RT7JGKT.js} +4 -4
  9. package/dist/{chunk-T6C4LFLC.js → chunk-7YGORYZD.js} +4 -4
  10. package/dist/{chunk-PKDS6QIJ.js → chunk-ATVGYF3D.js} +7 -7
  11. package/dist/{chunk-2TVVBC53.js → chunk-BORNCTH3.js} +2 -2
  12. package/dist/{chunk-E6PLM6QG.js → chunk-CUSMC2KK.js} +13 -13
  13. package/dist/{chunk-V4AF6DI4.js → chunk-DUBZOXJC.js} +9 -2
  14. package/dist/{chunk-V4AF6DI4.js.map → chunk-DUBZOXJC.js.map} +1 -1
  15. package/dist/chunk-DUUCU77W.js +211 -0
  16. package/dist/chunk-DUUCU77W.js.map +1 -0
  17. package/dist/{chunk-OFRRBC7M.js → chunk-E2BRT5IB.js} +15 -1
  18. package/dist/chunk-E2BRT5IB.js.map +1 -0
  19. package/dist/{chunk-XKWOJZZ4.js → chunk-E45CSC33.js} +2 -2
  20. package/dist/{chunk-VQOAATIG.js → chunk-FLYF76CU.js} +4 -4
  21. package/dist/{chunk-43SBT72G.js → chunk-I6UXRJ3Q.js} +4 -4
  22. package/dist/{chunk-GM3RMJIJ.js → chunk-INO47JXD.js} +3 -3
  23. package/dist/{chunk-BGULBWKJ.js → chunk-JOBQ6RUU.js} +1 -1
  24. package/dist/chunk-JOBQ6RUU.js.map +1 -0
  25. package/dist/{chunk-VDL5CJ5C.js → chunk-KBO5OOON.js} +9 -9
  26. package/dist/{chunk-OZEPJGMA.js → chunk-KHQ72A5F.js} +54 -6
  27. package/dist/chunk-KHQ72A5F.js.map +1 -0
  28. package/dist/{chunk-F7KN3U6U.js → chunk-KK5A7B2T.js} +27 -1
  29. package/dist/chunk-KK5A7B2T.js.map +1 -0
  30. package/dist/{chunk-B34G6PHD.js → chunk-LARB26EI.js} +77 -10
  31. package/dist/chunk-LARB26EI.js.map +1 -0
  32. package/dist/{chunk-65MO75WM.js → chunk-LQXBQO72.js} +8 -8
  33. package/dist/{chunk-K2I6XIK5.js → chunk-MVKW2BCR.js} +2 -2
  34. package/dist/{chunk-AZLUWG5S.js → chunk-NYJYK6J4.js} +10 -10
  35. package/dist/{chunk-BHZP6LOV.js → chunk-QSJ3J4HE.js} +7 -7
  36. package/dist/{chunk-R6F6KFIL.js → chunk-SGSWVNNB.js} +7 -7
  37. package/dist/chunk-SYVZ4MD2.js +1 -0
  38. package/dist/{chunk-7LKAMLV4.js → chunk-T6SCOJF4.js} +4 -4
  39. package/dist/{chunk-CLWBNXKF.js → chunk-W2UIDI3R.js} +4 -4
  40. package/dist/{chunk-SNH35CNA.js → chunk-WKNOEVWQ.js} +6 -6
  41. package/dist/runtime/base-classes/index.js +17 -17
  42. package/dist/runtime/subsystems/auth/auth.module.js +2 -2
  43. package/dist/runtime/subsystems/auth/index.js +7 -7
  44. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +3 -3
  45. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +4 -4
  46. package/dist/runtime/subsystems/bridge/bridge-delivery.schema.js +2 -2
  47. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +8 -8
  48. package/dist/runtime/subsystems/bridge/bridge.module.js +21 -21
  49. package/dist/runtime/subsystems/bridge/event-flow.service.js +2 -2
  50. package/dist/runtime/subsystems/bridge/index.js +21 -21
  51. package/dist/runtime/subsystems/cache/cache.drizzle-backend.js +2 -2
  52. package/dist/runtime/subsystems/cache/cache.module.js +3 -3
  53. package/dist/runtime/subsystems/cache/index.js +5 -5
  54. package/dist/runtime/subsystems/events/domain-events.schema.js +1 -1
  55. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +19 -32
  56. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +4 -4
  57. package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +18 -1
  58. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +2 -2
  59. package/dist/runtime/subsystems/events/event-bus.protocol.d.ts +45 -1
  60. package/dist/runtime/subsystems/events/event-scheduler.d.ts +96 -0
  61. package/dist/runtime/subsystems/events/event-scheduler.js +25 -0
  62. package/dist/runtime/subsystems/events/event-scheduler.js.map +1 -0
  63. package/dist/runtime/subsystems/events/events-errors.d.ts +12 -1
  64. package/dist/runtime/subsystems/events/events-errors.js +5 -3
  65. package/dist/runtime/subsystems/events/events.module.d.ts +41 -2
  66. package/dist/runtime/subsystems/events/events.module.js +12 -9
  67. package/dist/runtime/subsystems/events/generated/bus.js +3 -3
  68. package/dist/runtime/subsystems/events/generated/index.js +3 -3
  69. package/dist/runtime/subsystems/events/generated/registry.d.ts +6 -0
  70. package/dist/runtime/subsystems/events/generated/registry.js +1 -1
  71. package/dist/runtime/subsystems/events/index.d.ts +4 -3
  72. package/dist/runtime/subsystems/events/index.js +39 -15
  73. package/dist/runtime/subsystems/index.d.ts +1 -0
  74. package/dist/runtime/subsystems/index.js +93 -92
  75. package/dist/runtime/subsystems/integration/build-change-source.js +2 -2
  76. package/dist/runtime/subsystems/integration/index.js +36 -36
  77. package/dist/runtime/subsystems/integration/integration.module.js +4 -4
  78. package/dist/runtime/subsystems/jobs/index.js +46 -46
  79. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +8 -8
  80. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +4 -4
  81. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
  82. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +3 -3
  83. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +3 -3
  84. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -3
  85. package/dist/runtime/subsystems/jobs/job-worker.js +4 -4
  86. package/dist/runtime/subsystems/jobs/job-worker.module.js +13 -13
  87. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +11 -11
  88. package/dist/runtime/subsystems/observability/index.js +3 -3
  89. package/dist/runtime/subsystems/observability/observability.module.js +3 -3
  90. package/dist/runtime/subsystems/observability/observability.service.js +2 -2
  91. package/dist/src/cli/index.js +23 -12
  92. package/dist/src/cli/index.js.map +1 -1
  93. package/dist/src/index.js +12 -12
  94. package/package.json +1 -1
  95. package/runtime/subsystems/events/domain-events.schema.ts +16 -0
  96. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +103 -1
  97. package/runtime/subsystems/events/event-bus.memory-backend.ts +57 -1
  98. package/runtime/subsystems/events/event-bus.protocol.ts +47 -0
  99. package/runtime/subsystems/events/event-scheduler.ts +351 -0
  100. package/runtime/subsystems/events/events-errors.ts +14 -0
  101. package/runtime/subsystems/events/events.module.ts +78 -1
  102. package/runtime/subsystems/events/generated/registry.ts +1 -0
  103. package/runtime/subsystems/events/index.ts +25 -3
  104. package/dist/chunk-B34G6PHD.js.map +0 -1
  105. package/dist/chunk-BGULBWKJ.js.map +0 -1
  106. package/dist/chunk-F7KN3U6U.js.map +0 -1
  107. package/dist/chunk-FN2PYDPP.js +0 -1
  108. package/dist/chunk-OFRRBC7M.js.map +0 -1
  109. package/dist/chunk-OZEPJGMA.js.map +0 -1
  110. package/dist/chunk-Z7PQCAVK.js.map +0 -1
  111. /package/dist/{chunk-COGHTKXY.js.map → chunk-27ETSJ2X.js.map} +0 -0
  112. /package/dist/{chunk-235ZMMJR.js.map → chunk-3YCUIGPG.js.map} +0 -0
  113. /package/dist/{chunk-VNBC3VXM.js.map → chunk-5LXOJGO2.js.map} +0 -0
  114. /package/dist/{chunk-7OVCARTQ.js.map → chunk-5RT7JGKT.js.map} +0 -0
  115. /package/dist/{chunk-T6C4LFLC.js.map → chunk-7YGORYZD.js.map} +0 -0
  116. /package/dist/{chunk-PKDS6QIJ.js.map → chunk-ATVGYF3D.js.map} +0 -0
  117. /package/dist/{chunk-2TVVBC53.js.map → chunk-BORNCTH3.js.map} +0 -0
  118. /package/dist/{chunk-E6PLM6QG.js.map → chunk-CUSMC2KK.js.map} +0 -0
  119. /package/dist/{chunk-XKWOJZZ4.js.map → chunk-E45CSC33.js.map} +0 -0
  120. /package/dist/{chunk-VQOAATIG.js.map → chunk-FLYF76CU.js.map} +0 -0
  121. /package/dist/{chunk-43SBT72G.js.map → chunk-I6UXRJ3Q.js.map} +0 -0
  122. /package/dist/{chunk-GM3RMJIJ.js.map → chunk-INO47JXD.js.map} +0 -0
  123. /package/dist/{chunk-VDL5CJ5C.js.map → chunk-KBO5OOON.js.map} +0 -0
  124. /package/dist/{chunk-65MO75WM.js.map → chunk-LQXBQO72.js.map} +0 -0
  125. /package/dist/{chunk-K2I6XIK5.js.map → chunk-MVKW2BCR.js.map} +0 -0
  126. /package/dist/{chunk-AZLUWG5S.js.map → chunk-NYJYK6J4.js.map} +0 -0
  127. /package/dist/{chunk-BHZP6LOV.js.map → chunk-QSJ3J4HE.js.map} +0 -0
  128. /package/dist/{chunk-R6F6KFIL.js.map → chunk-SGSWVNNB.js.map} +0 -0
  129. /package/dist/{chunk-FN2PYDPP.js.map → chunk-SYVZ4MD2.js.map} +0 -0
  130. /package/dist/{chunk-7LKAMLV4.js.map → chunk-T6SCOJF4.js.map} +0 -0
  131. /package/dist/{chunk-CLWBNXKF.js.map → chunk-W2UIDI3R.js.map} +0 -0
  132. /package/dist/{chunk-SNH35CNA.js.map → chunk-WKNOEVWQ.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-OFRRBC7M.js.map
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-CLWBNXKF.js";
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-XKWOJZZ4.js.map
48
+ //# sourceMappingURL=chunk-E45CSC33.js.map
@@ -4,6 +4,9 @@ import {
4
4
  import {
5
5
  MemoryJobStore
6
6
  } from "./chunk-SNQ3TOWP.js";
7
+ import {
8
+ JOBS_MULTI_TENANT
9
+ } from "./chunk-ZPL74UQN.js";
7
10
  import {
8
11
  JobCollisionError,
9
12
  JobNotReplayableError,
@@ -16,9 +19,6 @@ import {
16
19
  JobKeyFunctionUnavailableError,
17
20
  keySelectorToTemplate
18
21
  } from "./chunk-7P5ODGLA.js";
19
- import {
20
- JOBS_MULTI_TENANT
21
- } from "./chunk-ZPL74UQN.js";
22
22
  import {
23
23
  __decorateClass,
24
24
  __decorateParam
@@ -663,4 +663,4 @@ function serialiseError(err, attempt, retryable) {
663
663
  export {
664
664
  MemoryJobOrchestrator
665
665
  };
666
- //# sourceMappingURL=chunk-VQOAATIG.js.map
666
+ //# sourceMappingURL=chunk-FLYF76CU.js.map
@@ -1,9 +1,9 @@
1
- import {
2
- PollChangeSource
3
- } from "./chunk-4MF3HKJA.js";
4
1
  import {
5
2
  WebhookChangeSource
6
3
  } from "./chunk-TIZXQU26.js";
4
+ import {
5
+ PollChangeSource
6
+ } from "./chunk-4MF3HKJA.js";
7
7
 
8
8
  // runtime/subsystems/integration/build-change-source.ts
9
9
  function buildChangeSource(cfg, fetch, middlewares = []) {
@@ -26,4 +26,4 @@ function buildChangeSource(cfg, fetch, middlewares = []) {
26
26
  export {
27
27
  buildChangeSource
28
28
  };
29
- //# sourceMappingURL=chunk-43SBT72G.js.map
29
+ //# sourceMappingURL=chunk-I6UXRJ3Q.js.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  getEventMetadata
3
- } from "./chunk-BGULBWKJ.js";
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-V4AF6DI4.js";
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-GM3RMJIJ.js.map
92
+ //# sourceMappingURL=chunk-INO47JXD.js.map
@@ -85,4 +85,4 @@ export {
85
85
  eventRegistry,
86
86
  getEventMetadata
87
87
  };
88
- //# sourceMappingURL=chunk-BGULBWKJ.js.map
88
+ //# sourceMappingURL=chunk-JOBQ6RUU.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":[]}
@@ -1,18 +1,18 @@
1
1
  import {
2
- JOBS_WAKE_CHANNEL,
3
- PgNotifyListener
4
- } from "./chunk-Q6LRJ4VI.js";
5
- import {
6
- JOB_HANDLER_REGISTRY
7
- } from "./chunk-7P5ODGLA.js";
2
+ jobRuns
3
+ } from "./chunk-OKXZ63IA.js";
8
4
  import {
9
5
  JOB_ORCHESTRATOR,
10
6
  JOB_RUN_SERVICE,
11
7
  JOB_STEP_SERVICE
12
8
  } from "./chunk-ZPL74UQN.js";
13
9
  import {
14
- jobRuns
15
- } from "./chunk-OKXZ63IA.js";
10
+ JOB_HANDLER_REGISTRY
11
+ } from "./chunk-7P5ODGLA.js";
12
+ import {
13
+ JOBS_WAKE_CHANNEL,
14
+ PgNotifyListener
15
+ } from "./chunk-Q6LRJ4VI.js";
16
16
  import {
17
17
  tokenKey
18
18
  } from "./chunk-GYGNEQSC.js";
@@ -528,4 +528,4 @@ export {
528
528
  buildStaleSweepQuery,
529
529
  JobWorker
530
530
  };
531
- //# sourceMappingURL=chunk-VDL5CJ5C.js.map
531
+ //# sourceMappingURL=chunk-KBO5OOON.js.map
@@ -1,12 +1,16 @@
1
1
  import {
2
2
  TypedEventBus
3
- } from "./chunk-GM3RMJIJ.js";
3
+ } from "./chunk-INO47JXD.js";
4
+ import {
5
+ EventScheduler,
6
+ scheduledEventsFromRegistry
7
+ } from "./chunk-DUUCU77W.js";
4
8
  import {
5
9
  DrizzleEventBus
6
- } from "./chunk-B34G6PHD.js";
10
+ } from "./chunk-LARB26EI.js";
7
11
  import {
8
12
  MemoryEventBus
9
- } from "./chunk-Z7PQCAVK.js";
13
+ } from "./chunk-4OC5MSHO.js";
10
14
  import {
11
15
  EVENTS_MODULE_OPTIONS,
12
16
  EVENTS_MULTI_TENANT,
@@ -19,16 +23,55 @@ import {
19
23
  DRIZZLE
20
24
  } from "./chunk-U64T4YZE.js";
21
25
  import {
22
- __decorateClass
26
+ __decorateClass,
27
+ __decorateParam
23
28
  } from "./chunk-2E224ZSN.js";
24
29
 
25
30
  // runtime/subsystems/events/events.module.ts
26
- import { Module } from "@nestjs/common";
31
+ import {
32
+ Inject,
33
+ Injectable,
34
+ Logger,
35
+ Module,
36
+ Optional
37
+ } from "@nestjs/common";
27
38
  async function loadRedisEventBus() {
28
39
  const specifier = "./event-bus.redis-backend";
29
40
  const mod = await import(specifier);
30
41
  return mod.RedisEventBus;
31
42
  }
43
+ var EventSchedulerLifecycle = class {
44
+ constructor(bus, opts = null) {
45
+ this.bus = bus;
46
+ this.opts = opts;
47
+ }
48
+ bus;
49
+ opts;
50
+ logger = new Logger(EventSchedulerLifecycle.name);
51
+ scheduler = null;
52
+ async onModuleInit() {
53
+ const registry = this.opts?.eventRegistry;
54
+ if (!registry) return;
55
+ if (typeof this.bus.materializeScheduledEvent !== "function") return;
56
+ const schedules = scheduledEventsFromRegistry(registry);
57
+ if (schedules.length === 0) return;
58
+ this.scheduler = new EventScheduler(this.bus, schedules);
59
+ await this.scheduler.start();
60
+ this.logger.log(`EventScheduler wired for ${schedules.length} scheduled event(s).`);
61
+ }
62
+ async onModuleDestroy() {
63
+ if (this.scheduler) {
64
+ this.scheduler.stop();
65
+ this.scheduler = null;
66
+ }
67
+ }
68
+ };
69
+ EventSchedulerLifecycle = __decorateClass([
70
+ Injectable(),
71
+ __decorateParam(0, Inject(EVENT_BUS)),
72
+ __decorateParam(1, Optional()),
73
+ __decorateParam(1, Inject(EVENTS_MODULE_OPTIONS))
74
+ ], EventSchedulerLifecycle);
32
75
  function buildTypedBusProviders(multiTenant, typedBus) {
33
76
  const BusClass = typedBus ?? TypedEventBus;
34
77
  return [
@@ -139,6 +182,10 @@ var EventsModule = class {
139
182
  // IEventReadPort on the same instance as EVENT_BUS. The redis
140
183
  // backend retains no history and does not provide this token.
141
184
  { provide: EVENT_READ_PORT, useExisting: EVENT_BUS },
185
+ // ADR-039 — the scheduler lifecycle. No-op unless `eventRegistry` was
186
+ // threaded AND some event declared `schedule:`. Drizzle/memory only
187
+ // (the redis branch above never reaches here).
188
+ EventSchedulerLifecycle,
142
189
  ...buildTypedBusProviders(multiTenant, options.typedBus)
143
190
  ],
144
191
  exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
@@ -150,6 +197,7 @@ EventsModule = __decorateClass([
150
197
  ], EventsModule);
151
198
 
152
199
  export {
200
+ EventSchedulerLifecycle,
153
201
  EventsModule
154
202
  };
155
- //# sourceMappingURL=chunk-OZEPJGMA.js.map
203
+ //# sourceMappingURL=chunk-KHQ72A5F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/events/events.module.ts"],"sourcesContent":["/**\n * EventsModule — DynamicModule factory for the event bus subsystem.\n *\n * Register once in AppModule:\n * ```typescript\n * @Module({\n * imports: [\n * EventsModule.forRoot({ backend: 'drizzle' }),\n * ],\n * })\n * export class AppModule {}\n * ```\n *\n * Tests swap to the memory backend without touching application code:\n * ```typescript\n * Test.createTestingModule({\n * imports: [EventsModule.forRoot({ backend: 'memory' })],\n * });\n * ```\n *\n * Per-pool drain isolation (EVT-4):\n * ```typescript\n * EventsModule.forRoot({ backend: 'drizzle', pools: ['events_change'] });\n * ```\n * Each process restricts its drain loop to the pools listed here. `pools`\n * is undefined by default → drain all pending rows (backwards-compatible).\n *\n * Typed facade + multi-tenancy (EVT-6):\n * - `TYPED_EVENT_BUS` resolves to the generated `TypedEventBus` wrapping\n * whichever backend is selected.\n * - `multiTenant: true` makes `TypedEventBus.publish` throw\n * `MissingTenantIdError` when the caller forgets `metadata.tenantId`.\n *\n * `global: true` means entity modules do not need to import EventsModule\n * individually — the EVENT_BUS and TYPED_EVENT_BUS tokens are available\n * project-wide.\n *\n * Async configuration (`forRootAsync`):\n * The async factory returns `EventsModuleOptions`; the EVENT_BUS provider\n * then receives its backend dependencies — DRIZZLE for the drizzle\n * backend, REDIS_URL for the redis backend, the resolved options for the\n * memory backend — through a proper `useFactory` so Nest DI wires them\n * correctly. Earlier revisions hand-constructed backends with\n * `new Class()` which silently left `db` / `redisUrl` undefined\n * (issue #108).\n */\nimport {\n Inject,\n Injectable,\n Logger,\n Module,\n Optional,\n type DynamicModule,\n type OnModuleDestroy,\n type OnModuleInit,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport {\n EVENT_BUS,\n EVENT_READ_PORT,\n EVENTS_MODULE_OPTIONS,\n EVENTS_MULTI_TENANT,\n REDIS_URL,\n TYPED_EVENT_BUS,\n} from './events.tokens';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DrizzleEventBus } from './event-bus.drizzle-backend';\nimport { MemoryEventBus } from './event-bus.memory-backend';\nimport type { IEventBus } from './event-bus.protocol';\nimport {\n EventScheduler,\n scheduledEventsFromRegistry,\n type RegistrySchedule,\n} from './event-scheduler';\n// #6 — `RedisEventBus` is lazy-loaded only when `backend: 'redis'` is selected.\n// The file is filtered out of the vendor set for non-redis installs (see\n// `backendFileFilter` in src/cli/commands/subsystem.ts); the dynamic-string\n// import below makes TS treat the specifier as `any` so the consumer's tsc\n// never tries to resolve the absent file.\nimport { TypedEventBus } from './generated/bus';\n\n/**\n * Lazy-load the Redis backend. Routed through a non-literal specifier so\n * the consumer's `tsc` doesn't resolve `./event-bus.redis-backend` at type\n * check time — important because that file is filtered out of drizzle/\n * memory installs (#6).\n */\nasync function loadRedisEventBus(): Promise<new (url: string) => object> {\n // Non-literal specifier — TS gives this an `any` module type, sidestepping\n // resolution of a file that may not be vendored.\n const specifier = './event-bus.redis-backend';\n const mod = (await import(specifier)) as { RedisEventBus: new (url: string) => object };\n return mod.RedisEventBus;\n}\n\nexport interface EventsModuleOptions {\n backend: 'drizzle' | 'memory' | 'redis';\n /**\n * Redis connection URL used when `backend` is `'redis'`.\n * Falls back to the REDIS_URL environment variable, then\n * `redis://localhost:6379` if neither is set.\n */\n redisUrl?: string;\n /**\n * Restrict the drain loop to these pools. Each pool name matches the\n * `domain_events.pool` column (populated from `event.metadata.pool` at\n * publish time). Leave undefined to drain all pending rows.\n *\n * Typical lane split: one process per `events_inbound` /\n * `events_change` / `events_outbound` so a slow outbound handler\n * cannot stall change-event propagation (see ADR-022).\n */\n pools?: string[];\n /**\n * LISTEN-NOTIFY-1 — when `true` (drizzle backend only), the drainer holds a\n * dedicated listener connection and LISTENs on `codegen_events_wake`. Each\n * `publish`/`publishMany` emits an in-tx `pg_notify(codegen_events_wake,\n * <pool>)` so the drainer wakes the moment the publishing transaction commits,\n * instead of waiting for the next poll tick. Polling continues unchanged as\n * the fallback heartbeat; a lost notify degrades to poll latency, never to\n * lost work. Defaults to `false`.\n *\n * Ignored by the memory + redis backends (memory dispatches inline; redis has\n * its own fan-out). Requires a direct (non-transaction-pooler) connection —\n * see the events/jobs config block re: PgBouncer.\n */\n listenNotify?: boolean;\n /**\n * Multi-tenancy opt-in (EVT-6).\n *\n * When `true`, every `TypedEventBus.publish()` call must supply\n * `opts.metadata.tenantId` — otherwise it throws `MissingTenantIdError`.\n * The tenantId is preserved on `event.metadata` and, for the Drizzle\n * backend, written to `domain_events.tenant_id` (EVT-4).\n *\n * Drain-side tenant filtering is deferred — the tenant-context model\n * (per-process vs. per-request vs. async-local-storage) is still\n * unsettled; see ADR-024 §Multi-tenancy. Only the publish-side\n * requirement ships here.\n *\n * Defaults to `false`.\n */\n multiTenant?: boolean;\n /**\n * The generated `TypedEventBus` class to bind to `TYPED_EVENT_BUS`.\n *\n * **Package mode (ADR-037).** When the runtime is imported from\n * `@pattern-stack/codegen` (not vendored), the bundled `./generated/bus`\n * `TypedEventBus` is typed to an EMPTY event union and reads the bundled\n * empty `eventRegistry` — a consumer's `events/*.yaml` are scanned into\n * `src/generated/events/bus.ts` (typed to THEIR union, reading THEIR\n * registry), which the package can't import. The generated subsystem barrel\n * therefore threads that class in here:\n * `EventsModule.forRoot({ ..., typedBus: TypedEventBus })`. Nest constructs\n * it with this module's `EVENT_BUS` + `EVENTS_MULTI_TENANT` providers (the\n * generated class injects the same string-valued tokens, which match across\n * the package boundary).\n *\n * Omitted (vendored mode / tests) ⇒ falls back to the bundled\n * `./generated/bus`, which in a vendored tree IS the consumer's generated\n * file. Without this, a package-mode consumer's typed `publish<'…'>()` calls\n * resolve against the empty union and their events never get the right\n * `pool` / `direction` stamped.\n *\n * Only consulted by `forRoot` (the path the barrel emits); `forRootAsync`\n * keeps the bundled bus.\n */\n typedBus?: Type<unknown>;\n /**\n * ADR-039 — the consumer's generated `eventRegistry`, threaded so the\n * `EventScheduler` can read the `schedule:` block + routing metadata of every\n * scheduled event. Package mode: the generated subsystem barrel passes the\n * consumer's `eventRegistry` (the bundled one is the package's empty/fixture\n * registry, which the package can't see the consumer's events through — same\n * reason `typedBus` is threaded). Omitted ⇒ no scheduler is spawned (vendored\n * tree reads its own bundled registry only if the barrel passes it; tests\n * pass a registry directly).\n *\n * Structural shape: each value needs `schedule?` + `direction` + `pool`. The\n * generated `EventMetadata` satisfies it; typing it loosely here avoids a\n * runtime dependency on the generated types from the module file.\n */\n eventRegistry?: Record<\n string,\n { schedule?: RegistrySchedule; direction: string | null; pool: string | null }\n >;\n}\n\n/**\n * Lifecycle holder for the `EventScheduler` (ADR-039). Registered as a provider\n * on the drizzle/memory `forRoot` branches; Nest drives `onModuleInit` (after\n * the bus is constructed) and `onModuleDestroy`. Reads the scheduled-event set\n * from the threaded `eventRegistry` and starts the materialiser. No-op when\n * nothing declared `schedule:`, or under the redis backend (no outbox history).\n */\n@Injectable()\nexport class EventSchedulerLifecycle implements OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger(EventSchedulerLifecycle.name);\n private scheduler: EventScheduler | null = null;\n\n constructor(\n @Inject(EVENT_BUS) private readonly bus: IEventBus,\n @Optional()\n @Inject(EVENTS_MODULE_OPTIONS)\n private readonly opts: EventsModuleOptions | null = null,\n ) {}\n\n async onModuleInit(): Promise<void> {\n const registry = this.opts?.eventRegistry;\n if (!registry) return;\n if (typeof this.bus.materializeScheduledEvent !== 'function') return; // redis\n const schedules = scheduledEventsFromRegistry(registry);\n if (schedules.length === 0) return;\n this.scheduler = new EventScheduler(this.bus, schedules);\n await this.scheduler.start();\n this.logger.log(`EventScheduler wired for ${schedules.length} scheduled event(s).`);\n }\n\n async onModuleDestroy(): Promise<void> {\n if (this.scheduler) {\n this.scheduler.stop();\n this.scheduler = null;\n }\n }\n}\n\nexport interface EventsModuleAsyncOptions {\n useFactory: (...args: unknown[]) => Promise<EventsModuleOptions> | EventsModuleOptions;\n inject?: unknown[];\n imports?: unknown[];\n}\n\n/**\n * Shared provider set: `TypedEventBus` itself, the `TYPED_EVENT_BUS` token\n * binding, and the resolved `EVENTS_MULTI_TENANT` flag. Returned from one\n * place so every `forRoot` branch and `forRootAsync` agree.\n */\nfunction buildTypedBusProviders(\n multiTenant: boolean,\n typedBus?: Type<unknown>,\n): Provider[] {\n // Package mode threads the consumer's generated `TypedEventBus` (typed to\n // their event union, reading their registry) via `typedBus`; vendored mode\n // omits it and we fall back to the bundled `./generated/bus` (which IS the\n // consumer's generated file in a vendored tree). See `EventsModuleOptions.typedBus`.\n const BusClass = typedBus ?? TypedEventBus;\n return [\n BusClass,\n { provide: TYPED_EVENT_BUS, useExisting: BusClass },\n { provide: EVENTS_MULTI_TENANT, useValue: multiTenant },\n ];\n}\n\n/**\n * Construct the backend instance for the async path, routing constructor\n * arguments through Nest-resolved dependencies.\n *\n * DRIZZLE is declared optional at inject time so that memory-backend\n * consumers aren't required to also import `DatabaseModule`. If the\n * drizzle backend is selected but no DRIZZLE provider is registered, we\n * throw a clear error instead of silently constructing a broken bus.\n */\nasync function buildEventBusAsync(\n options: EventsModuleOptions,\n db: DrizzleClient | null,\n redisUrl: string,\n): Promise<unknown> {\n if (options.backend === 'drizzle') {\n if (!db) {\n throw new Error(\n \"EventsModule.forRootAsync: backend: 'drizzle' selected but DRIZZLE provider is not available. \" +\n 'Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before EventsModule.forRootAsync.',\n );\n }\n return new DrizzleEventBus(db, options);\n }\n if (options.backend === 'redis') {\n // #6: lazy import — the redis backend ships only with `--backend redis`\n // installs; drizzle/memory consumers never touch the file.\n const RedisEventBus = await loadRedisEventBus();\n return new RedisEventBus(redisUrl);\n }\n return new MemoryEventBus(options);\n}\n\n@Module({})\nexport class EventsModule {\n static forRootAsync(asyncOptions: EventsModuleAsyncOptions): DynamicModule {\n return {\n module: EventsModule,\n global: true,\n imports: (asyncOptions.imports ?? []) as Parameters<typeof Module>[0]['imports'],\n providers: [\n {\n provide: EVENTS_MODULE_OPTIONS,\n useFactory: asyncOptions.useFactory,\n inject: (asyncOptions.inject ?? []) as (string | symbol | Function)[],\n },\n {\n provide: EVENTS_MULTI_TENANT,\n useFactory: (options: EventsModuleOptions) => options.multiTenant ?? false,\n inject: [EVENTS_MODULE_OPTIONS],\n },\n {\n provide: REDIS_URL,\n useFactory: (options: EventsModuleOptions) =>\n options.redisUrl ?? process.env['REDIS_URL'] ?? 'redis://localhost:6379',\n inject: [EVENTS_MODULE_OPTIONS],\n },\n {\n provide: EVENT_BUS,\n useFactory: (\n options: EventsModuleOptions,\n db: DrizzleClient | null,\n redisUrl: string,\n ) => buildEventBusAsync(options, db, redisUrl),\n inject: [\n EVENTS_MODULE_OPTIONS,\n { token: DRIZZLE, optional: true },\n REDIS_URL,\n ],\n },\n {\n // Read port (OBS-LIST-1). Drizzle + memory backends implement\n // IEventReadPort on the EVENT_BUS instance; the redis backend\n // retains no history, so EVENT_READ_PORT resolves to `null` and\n // optional consumers (the observability combiner) degrade to\n // empty results.\n provide: EVENT_READ_PORT,\n useFactory: (options: EventsModuleOptions, bus: unknown) =>\n options.backend === 'redis' ? null : bus,\n inject: [EVENTS_MODULE_OPTIONS, EVENT_BUS],\n },\n TypedEventBus,\n { provide: TYPED_EVENT_BUS, useExisting: TypedEventBus },\n ],\n exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],\n };\n }\n\n static forRoot(\n options: EventsModuleOptions = { backend: 'drizzle' },\n ): DynamicModule {\n const multiTenant = options.multiTenant ?? false;\n\n if (options.backend === 'redis') {\n const resolvedUrl =\n options.redisUrl ?? process.env['REDIS_URL'] ?? 'redis://localhost:6379';\n\n return {\n module: EventsModule,\n global: true,\n providers: [\n { provide: EVENTS_MODULE_OPTIONS, useValue: options },\n { provide: REDIS_URL, useValue: resolvedUrl },\n {\n // #6: useFactory + dynamic import so the consumer's tsc never\n // needs to resolve `event-bus.redis-backend.ts` for drizzle/\n // memory installs (the file is filtered out by\n // `backendFileFilter`). Nest awaits async factories + manages\n // lifecycle on the returned instance, so we drop the old bare\n // `RedisEventBus` provider entry.\n provide: EVENT_BUS,\n useFactory: async (url: string): Promise<object> => {\n const RedisEventBus = await loadRedisEventBus();\n return new RedisEventBus(url);\n },\n inject: [REDIS_URL],\n },\n ...buildTypedBusProviders(multiTenant, options.typedBus),\n ],\n exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],\n };\n }\n\n const provider =\n options.backend === 'drizzle'\n ? { provide: EVENT_BUS, useClass: DrizzleEventBus }\n : { provide: EVENT_BUS, useClass: MemoryEventBus };\n\n return {\n module: EventsModule,\n global: true,\n providers: [\n { provide: EVENTS_MODULE_OPTIONS, useValue: options },\n provider,\n // Read port (OBS-LIST-1): drizzle + memory backends implement\n // IEventReadPort on the same instance as EVENT_BUS. The redis\n // backend retains no history and does not provide this token.\n { provide: EVENT_READ_PORT, useExisting: EVENT_BUS },\n // ADR-039 — the scheduler lifecycle. No-op unless `eventRegistry` was\n // threaded AND some event declared `schedule:`. Drizzle/memory only\n // (the redis branch above never reaches here).\n EventSchedulerLifecycle,\n ...buildTypedBusProviders(multiTenant, options.typedBus),\n ],\n exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],\n };\n }\n}\n"],"mappings":"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","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-F7KN3U6U.js.map
4192
+ //# sourceMappingURL=chunk-KK5A7B2T.js.map