@pattern-stack/codegen 0.27.0 → 0.27.2

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 (40) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/{chunk-E5FJWOMP.js → chunk-3AM722ZH.js} +15 -6
  3. package/dist/chunk-3AM722ZH.js.map +1 -0
  4. package/dist/{chunk-COGHTKXY.js → chunk-7625PLY7.js} +4 -4
  5. package/dist/{chunk-IN3EWFB4.js → chunk-BHZP6LOV.js} +4 -4
  6. package/dist/{chunk-HEOISQ6W.js → chunk-EGXFEZ2N.js} +2 -2
  7. package/dist/{chunk-3VEVGL74.js → chunk-VNBC3VXM.js} +4 -4
  8. package/dist/{chunk-AC6T2JUX.js → chunk-XW4XKN3F.js} +9 -9
  9. package/dist/{chunk-PNCOUFFI.js → chunk-Y6UZMYGX.js} +4 -3
  10. package/dist/chunk-Y6UZMYGX.js.map +1 -0
  11. package/dist/{chunk-VDVEGTSW.js → chunk-YHVZAL6U.js} +2 -2
  12. package/dist/{chunk-IXAE6BN6.js → chunk-YQA5PMOD.js} +8 -8
  13. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +1 -1
  14. package/dist/runtime/subsystems/bridge/bridge.module.js +11 -11
  15. package/dist/runtime/subsystems/bridge/bridge.protocol.d.ts +20 -14
  16. package/dist/runtime/subsystems/bridge/index.js +17 -17
  17. package/dist/runtime/subsystems/cache/cache.module.js +2 -2
  18. package/dist/runtime/subsystems/cache/index.js +4 -4
  19. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +1 -1
  20. package/dist/runtime/subsystems/events/events.module.js +2 -2
  21. package/dist/runtime/subsystems/events/index.js +2 -2
  22. package/dist/runtime/subsystems/index.js +42 -42
  23. package/dist/runtime/subsystems/jobs/index.js +11 -11
  24. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +2 -2
  25. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
  26. package/dist/runtime/subsystems/jobs/job-worker.module.js +7 -7
  27. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +6 -6
  28. package/package.json +1 -1
  29. package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +34 -11
  30. package/runtime/subsystems/bridge/bridge.protocol.ts +20 -14
  31. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +16 -4
  32. package/dist/chunk-E5FJWOMP.js.map +0 -1
  33. package/dist/chunk-PNCOUFFI.js.map +0 -1
  34. /package/dist/{chunk-COGHTKXY.js.map → chunk-7625PLY7.js.map} +0 -0
  35. /package/dist/{chunk-IN3EWFB4.js.map → chunk-BHZP6LOV.js.map} +0 -0
  36. /package/dist/{chunk-HEOISQ6W.js.map → chunk-EGXFEZ2N.js.map} +0 -0
  37. /package/dist/{chunk-3VEVGL74.js.map → chunk-VNBC3VXM.js.map} +0 -0
  38. /package/dist/{chunk-AC6T2JUX.js.map → chunk-XW4XKN3F.js.map} +0 -0
  39. /package/dist/{chunk-VDVEGTSW.js.map → chunk-YHVZAL6U.js.map} +0 -0
  40. /package/dist/{chunk-IXAE6BN6.js.map → chunk-YQA5PMOD.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,53 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.27.2] — 2026-06-08
6
+
7
+ ### Fixed
8
+
9
+ - **Bridge audit-tier guard no longer emits a false-positive WARN on every
10
+ audit event (#535).** `BridgeOutboxDrainHook.processEvent` ran its audit-tier
11
+ guard *before* the trigger lookup, so it fired — and logged a WARN — for every
12
+ `tier:audit` event, while asserting "a `bridge_trigger` row exists out-of-band"
13
+ (a registry/runtime-drift claim it never actually checked). Audit-tier
14
+ lifecycle events (`connection.created`, `connection.field_changed`, …) share
15
+ the outbox with domain events and carry no triggers, so the warning fired on
16
+ ordinary, correct operation. Dogfood discovery from swe-brain. Fix: look up
17
+ triggers first, then branch — `tier:audit` **with** a registered trigger is
18
+ genuine drift (WARN, now naming the offending trigger id, + `auditBlocked:1`,
19
+ no fanout); `tier:audit` with **no** trigger is the benign shared-outbox case
20
+ (returns zeros, silent). The SELECT-level `tier='audit'` filter was rejected:
21
+ audit events still need the normal drain (`processed_at` stamp + subscriber
22
+ dispatch); only bridge fanout skips them. Revises the original AUDIT-4
23
+ top-of-method guard (`ai-docs/specs/issue-242/plan.md` §AUDIT-4 revision note).
24
+ To make a workflow react to a lifecycle moment, emit a typed domain
25
+ change-fact — do not make the audit event bridge-eligible (new bridge skill
26
+ rule).
27
+
28
+ ## [0.27.1] — 2026-06-07
29
+
30
+ ### Fixed
31
+
32
+ - **Scheduled-event drain no longer claims future slots early (#533).**
33
+ `DrizzleEventBus.processBatch()` composed a status-only claim
34
+ (`status='pending'` [+ pool IN …]) with no readiness predicate, so the 1s
35
+ fallback poll grabbed the `EventScheduler`'s pre-materialised *next* slot
36
+ (`occurred_at = slotStart`, in the future) on the very next cycle and stamped
37
+ `processed_at = now()` — contradicting `materializeScheduledEvent`'s own
38
+ contract ("a future slot is claimed by polling once `occurred_at` passes").
39
+ Symptoms: event-log rows reading "N minutes from now" yet already
40
+ `status:processed`, and schedule-driven triggers firing up to one interval
41
+ ahead of their slot. Fix: add `occurred_at <= now` to the claim WHERE (both
42
+ the pooled and pool-less branches). Normal events publish with
43
+ `occurred_at = now()`, so the gate is transparent to them.
44
+
45
+ ## [0.27.0] — 2026-06-07
46
+
47
+ ### Added
48
+
49
+ - **Pagination by default (#532): `Page<T>` list emit + `store.<entity>.useData()`.**
50
+ See PR #532. (CHANGELOG entry backfilled in 0.27.1.)
51
+
5
52
  ## [0.26.1] — 2026-06-07
6
53
 
7
54
  ### Fixed
@@ -42,16 +42,24 @@ var BridgeOutboxDrainHook = class {
42
42
  warnedNullDirection = false;
43
43
  warnedAuditTypes = /* @__PURE__ */ new Set();
44
44
  async processEvent(event, tx) {
45
+ const triggers = this.lookupTriggers(event.type);
45
46
  if (event.metadata?.["tier"] === "audit") {
46
- this.warnAuditBlockedOnce(event);
47
+ if (triggers.length > 0) {
48
+ this.warnAuditBlockedOnce(event, triggers);
49
+ return {
50
+ delivered: 0,
51
+ dedupSkips: 0,
52
+ triggerCount: 0,
53
+ auditBlocked: 1
54
+ };
55
+ }
47
56
  return {
48
57
  delivered: 0,
49
58
  dedupSkips: 0,
50
59
  triggerCount: 0,
51
- auditBlocked: 1
60
+ auditBlocked: 0
52
61
  };
53
62
  }
54
- const triggers = this.lookupTriggers(event.type);
55
63
  if (triggers.length === 0) {
56
64
  return {
57
65
  delivered: 0,
@@ -128,11 +136,12 @@ var BridgeOutboxDrainHook = class {
128
136
  auditBlocked: 0
129
137
  };
130
138
  }
131
- warnAuditBlockedOnce(event) {
139
+ warnAuditBlockedOnce(event, triggers) {
132
140
  if (this.warnedAuditTypes.has(event.type)) return;
133
141
  this.warnedAuditTypes.add(event.type);
142
+ const triggerIds = triggers.map((t) => t.triggerId).join(", ");
134
143
  this.logger.warn(
135
- `Bridge guard blocked audit-tier event '${event.type}' (event.id=${event.id}). Registry says this event is not bridge-eligible; a bridge_trigger row exists out-of-band. Investigate registry/runtime drift.`
144
+ `Bridge guard blocked audit-tier event '${event.type}' (event.id=${event.id}). Audit events are not bridge-eligible, yet the registry has trigger(s) registered against this type [${triggerIds}] \u2014 an out-of-band bridge_trigger row or version skew. Investigate registry/runtime drift.`
136
145
  );
137
146
  }
138
147
  lookupTriggers(eventType) {
@@ -151,4 +160,4 @@ BridgeOutboxDrainHook = __decorateClass([
151
160
  export {
152
161
  BridgeOutboxDrainHook
153
162
  };
154
- //# sourceMappingURL=chunk-E5FJWOMP.js.map
163
+ //# sourceMappingURL=chunk-3AM722ZH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/bridge/bridge-outbox-drain-hook.ts"],"sourcesContent":["/**\n * BridgeOutboxDrainHook — drains-time bridge fanout writer (BRIDGE-4,\n * ADR-023 Phase 2).\n *\n * Implements `IBridgeOutboxDrainHook`. Called by `DrizzleEventBus`'s\n * modified `processBatch` once per drained event, INSIDE the per-event\n * transaction. For every trigger registered against the event's type in\n * the codegen-emitted `bridgeRegistry`, writes:\n *\n * 1. `bridge_delivery` ledger row — `INSERT … ON CONFLICT (event_id,\n * trigger_id) DO NOTHING RETURNING id`. Empty result ⇒ Case B\n * facade-eager pre-write OR drain-replay collision; skip wrapper\n * insert for that trigger; sibling triggers still fire.\n * 2. `job_run` wrapper row — `type='@framework/bridge_delivery'`,\n * `pool='events_<direction>'`, `input={ deliveryId }`,\n * `trigger_source='event'`, `trigger_ref=event.id`. The wrapper is\n * what the framework `BridgeDeliveryHandler` (BRIDGE-5) eventually\n * claims via the worker that polls the corresponding reserved pool.\n *\n * Null `event.metadata.direction` is tolerated: the hook logs a one-line\n * warning per event and returns zeros without writing rows. The drain's\n * `processed_at` stamp + subscriber dispatch still fire normally.\n * Direction is null only for events published via the legacy\n * `IEventBus.publish(...)` path (`TypedEventBus.publish` always sets it);\n * such events are out of scope for bridge fanout.\n *\n * The wrapper insert generates its own `id` via Drizzle's `defaultRandom`\n * — we don't `RETURNING id` because nobody needs it at drain time;\n * `BridgeDeliveryHandler` later looks up the wrapper via the\n * `bridge_delivery.wrapper_run_id` link if needed. This keeps the drain\n * one-round-trip-per-trigger.\n */\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport { randomUUID } from 'node:crypto';\nimport { eq } from 'drizzle-orm';\n\nimport type { DomainEvent, DrizzleTransaction } from '../events/event-bus.protocol';\nimport { bridgeDelivery } from './bridge-delivery.schema';\nimport { jobRuns } from '../jobs/job-orchestration.schema';\n\nimport { BRIDGE_REGISTRY } from './bridge.tokens';\nimport type {\n BridgeOutboxDrainResult,\n BridgeRegistry,\n BridgeTriggerEntry,\n IBridgeOutboxDrainHook,\n} from './bridge.protocol';\nimport { BRIDGE_DELIVERY_JOB_TYPE } from './bridge-delivery-handler';\nimport type { EventTypeName } from '../events/event-registry';\nimport { JOBS_LISTEN_NOTIFY } from '../jobs/jobs-domain.tokens';\nimport { JOBS_WAKE_CHANNEL, pgNotify } from '../jobs/pg-notify';\n\n/** Reserved pools the wrapper rows route into; ADR-022 / ADR-024. */\nconst POOL_BY_DIRECTION: Record<string, string> = {\n inbound: 'events_inbound',\n change: 'events_change',\n outbound: 'events_outbound',\n};\n\n@Injectable()\nexport class BridgeOutboxDrainHook implements IBridgeOutboxDrainHook {\n private readonly logger = new Logger(BridgeOutboxDrainHook.name);\n private warnedNullDirection = false;\n private readonly warnedAuditTypes = new Set<string>();\n\n constructor(\n @Optional()\n @Inject(BRIDGE_REGISTRY)\n private readonly registry: BridgeRegistry = {},\n // LISTEN-NOTIFY-1 — when true, the wrapper `job_run` insert below emits an\n // in-tx `pg_notify(codegen_jobs_wake, <wrapperPool>)` so the reserved-pool\n // worker wakes the instant the per-event drain tx commits — otherwise the\n // bridge hop alone would still cost a full poll interval. `@Optional()`\n // defaulting false so the hook keeps working when jobs isn't installed\n // (bridge can drive non-jobs consumers) or in vendored/test wiring.\n @Optional()\n @Inject(JOBS_LISTEN_NOTIFY)\n private readonly listenNotify: boolean = false,\n ) {}\n\n async processEvent(\n event: DomainEvent,\n tx: DrizzleTransaction,\n ): Promise<BridgeOutboxDrainResult> {\n const triggers = this.lookupTriggers(event.type);\n\n // Audit-tier guard (defense-in-depth — AUDIT-4). Audit events are\n // bridge-inert by design: the codegen-side validator (AUDIT-2) blocks the\n // registry from listing them as triggers, so an audit event should never\n // have a matched trigger. The guard runs AFTER the lookup so it can tell\n // apart the two cases that share `tier === 'audit'`:\n // - triggers present → genuine registry/runtime drift (an out-of-band\n // `bridge_trigger` insert, or version skew during deploy). Refuse\n // fanout and surface the drift via WARN — now a verified claim, not a\n // guess. (Revises the original top-of-method guard, which warned for\n // every audit event: audit-tier lifecycle events like\n // `connection.created` routinely share the outbox with domain events,\n // so the unconditional warning was a false positive. See\n // ai-docs/specs/issue-242/plan.md §AUDIT-4 revision note.)\n // - no triggers → the common, benign case. Stay silent; auditBlocked: 0.\n if (event.metadata?.['tier'] === 'audit') {\n if (triggers.length > 0) {\n this.warnAuditBlockedOnce(event, triggers);\n return {\n delivered: 0,\n dedupSkips: 0,\n triggerCount: 0,\n auditBlocked: 1,\n };\n }\n return {\n delivered: 0,\n dedupSkips: 0,\n triggerCount: 0,\n auditBlocked: 0,\n };\n }\n\n if (triggers.length === 0) {\n return {\n delivered: 0,\n dedupSkips: 0,\n triggerCount: 0,\n auditBlocked: 0,\n };\n }\n\n const direction =\n (event.metadata?.['direction'] as string | undefined) ?? null;\n const tenantId =\n (event.metadata?.['tenantId'] as string | null | undefined) ?? null;\n const wrapperPool = direction ? POOL_BY_DIRECTION[direction] : undefined;\n\n if (!wrapperPool) {\n // Null direction (or an unrecognised one — defensive). Bridge\n // fanout requires a routed wrapper pool; without one we can't\n // spawn. Log once per process so misconfiguration surfaces.\n if (!this.warnedNullDirection) {\n this.warnedNullDirection = true;\n this.logger.warn(\n `Skipping bridge fanout for events with null/unknown direction. ` +\n `event.id=${event.id} event.type=${event.type} ` +\n `direction=${String(direction)}. The wrapper pool is derived ` +\n `from direction (events_<direction>); publishers must use ` +\n `TypedEventBus.publish() so direction is stamped on the ` +\n `outbox row.`,\n );\n }\n return {\n delivered: 0,\n dedupSkips: 0,\n triggerCount: triggers.length,\n auditBlocked: 0,\n };\n }\n\n let delivered = 0;\n let dedupSkips = 0;\n const client = tx as unknown as {\n insert: (table: unknown) => {\n values: (v: unknown) => {\n onConflictDoNothing: (opts: unknown) => {\n returning: (cols: unknown) => Promise<{ id: string }[]>;\n };\n } & {\n // wrapper insert path — no ON CONFLICT\n // (typed loosely via the same helper return shape)\n };\n };\n };\n\n for (const trigger of triggers) {\n const deliveryId = randomUUID();\n const wrapperRunId = randomUUID();\n\n // FK ORDER (BRIDGE / 0.15.2): `bridge_delivery.wrapper_run_id` REFERENCES\n // `job_run(id)` is a plain (non-deferrable) FK, so the referenced\n // wrapper `job_run` MUST exist before the delivery row that points at it\n // is inserted — otherwise Postgres rejects the delivery insert\n // immediately. (The codegen unit tests mock `tx`, so they never\n // exercised this ordering against a real FK; package-mode bridge\n // deliveries are the first to do so.) We therefore insert the wrapper\n // run FIRST, then the delivery. Idempotency is unchanged: the delivery\n // keeps its `ON CONFLICT (event_id, trigger_id) DO NOTHING RETURNING`,\n // and when the delivery conflicts (outbox replay or facade-eager Case B)\n // we DELETE the just-inserted orphan wrapper run in the same tx, so a\n // skipped delivery leaves no stray `job_run` for a worker to claim.\n\n // 1. Wrapper job_run insert. We carry the deliveryId into the wrapper\n // input so BridgeDeliveryHandler.run(ctx) can locate the row via\n // repo.findDeliveryById(ctx.input.deliveryId).\n await (tx as unknown as { insert: typeof client.insert })\n .insert(jobRuns)\n .values({\n id: wrapperRunId,\n jobType: BRIDGE_DELIVERY_JOB_TYPE,\n jobVersion: 1,\n rootRunId: wrapperRunId,\n pool: wrapperPool,\n status: 'pending',\n input: { deliveryId },\n triggerSource: 'event',\n triggerRef: event.id,\n tenantId,\n });\n\n // 2. bridge_delivery insert with ON CONFLICT DO NOTHING + RETURNING.\n const inserted = await (tx as unknown as {\n insert: typeof client.insert;\n })\n .insert(bridgeDelivery)\n .values({\n id: deliveryId,\n eventId: event.id,\n triggerId: trigger.triggerId,\n wrapperRunId,\n status: 'pending',\n tenantId,\n })\n .onConflictDoNothing({\n target: [bridgeDelivery.eventId, bridgeDelivery.triggerId],\n })\n .returning({ id: bridgeDelivery.id });\n\n if (inserted.length === 0) {\n // Case B (facade pre-wrote `delivered`) or drain replay — the delivery\n // already exists, so this trigger is a no-op. Remove the orphan wrapper\n // run we speculatively inserted above so no worker claims it. Sibling\n // triggers still fire.\n await (tx as unknown as {\n delete: (table: unknown) => {\n where: (cond: unknown) => Promise<unknown>;\n };\n })\n .delete(jobRuns)\n .where(eq(jobRuns.id, wrapperRunId));\n dedupSkips++;\n continue;\n }\n\n // LISTEN-NOTIFY-1 — the wrapper run is real and claimable; wake its\n // reserved-pool worker on commit (D7). Same `tx` as the inserts above, so\n // delivery is gated on the per-event drain tx committing. Best-effort: a\n // notify failure is non-fatal (the reserved-pool worker still polls).\n if (this.listenNotify) {\n try {\n await pgNotify(tx, JOBS_WAKE_CHANNEL, wrapperPool);\n } catch (err) {\n this.logger.warn(\n `pg_notify(${JOBS_WAKE_CHANNEL}, ${wrapperPool}) failed for ` +\n `wrapper run ${wrapperRunId}: ${(err as Error).message} ` +\n `(non-fatal — the reserved-pool worker still polls).`,\n );\n }\n }\n\n delivered++;\n }\n\n return {\n delivered,\n dedupSkips,\n triggerCount: triggers.length,\n auditBlocked: 0,\n };\n }\n\n private warnAuditBlockedOnce(\n event: DomainEvent,\n triggers: BridgeTriggerEntry[],\n ): void {\n if (this.warnedAuditTypes.has(event.type)) return;\n this.warnedAuditTypes.add(event.type);\n const triggerIds = triggers.map((t) => t.triggerId).join(', ');\n this.logger.warn(\n `Bridge guard blocked audit-tier event '${event.type}' (event.id=${event.id}). ` +\n `Audit events are not bridge-eligible, yet the registry has trigger(s) ` +\n `registered against this type [${triggerIds}] — an out-of-band bridge_trigger ` +\n `row or version skew. Investigate registry/runtime drift.`,\n );\n }\n\n private lookupTriggers(\n eventType: string,\n ): BridgeTriggerEntry[] {\n const candidates = this.registry[eventType as EventTypeName];\n return (candidates ?? []) as BridgeTriggerEntry[];\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,9 +1,9 @@
1
- import {
2
- DrizzleCacheService
3
- } from "./chunk-T6C4LFLC.js";
4
1
  import {
5
2
  MemoryCacheService
6
3
  } from "./chunk-IF5I3DAA.js";
4
+ import {
5
+ DrizzleCacheService
6
+ } from "./chunk-T6C4LFLC.js";
7
7
  import {
8
8
  CACHE,
9
9
  CACHE_DEFAULT_TTL
@@ -81,4 +81,4 @@ CacheModule = __decorateClass([
81
81
  export {
82
82
  CacheModule
83
83
  };
84
- //# sourceMappingURL=chunk-COGHTKXY.js.map
84
+ //# sourceMappingURL=chunk-7625PLY7.js.map
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  MemoryJobStore
3
3
  } from "./chunk-SNQ3TOWP.js";
4
+ import {
5
+ MissingTenantIdError
6
+ } from "./chunk-T4BIIU5E.js";
4
7
  import {
5
8
  clampLimit,
6
9
  decodeKeysetCursor,
7
10
  encodeKeysetCursor,
8
11
  toJobRunSummary
9
12
  } from "./chunk-L3LZWWSX.js";
10
- import {
11
- MissingTenantIdError
12
- } from "./chunk-T4BIIU5E.js";
13
13
  import {
14
14
  JOBS_MULTI_TENANT,
15
15
  JOB_ORCHESTRATOR
@@ -209,4 +209,4 @@ function compareBy(a, b, order) {
209
209
  export {
210
210
  MemoryJobRunService
211
211
  };
212
- //# sourceMappingURL=chunk-IN3EWFB4.js.map
212
+ //# sourceMappingURL=chunk-BHZP6LOV.js.map
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-7B7MMDOJ.js";
4
4
  import {
5
5
  JobsDomainModule
6
- } from "./chunk-AC6T2JUX.js";
6
+ } from "./chunk-XW4XKN3F.js";
7
7
  import {
8
8
  BootValidationError,
9
9
  ReservedPoolViolationError
@@ -295,4 +295,4 @@ export {
295
295
  JobWorkerOrchestrator,
296
296
  JobWorkerModule
297
297
  };
298
- //# sourceMappingURL=chunk-HEOISQ6W.js.map
298
+ //# sourceMappingURL=chunk-EGXFEZ2N.js.map
@@ -1,12 +1,12 @@
1
+ import {
2
+ MissingTenantIdError
3
+ } from "./chunk-T4BIIU5E.js";
1
4
  import {
2
5
  clampLimit,
3
6
  decodeKeysetCursor,
4
7
  encodeKeysetCursor,
5
8
  toJobRunSummary
6
9
  } from "./chunk-L3LZWWSX.js";
7
- import {
8
- MissingTenantIdError
9
- } from "./chunk-T4BIIU5E.js";
10
10
  import {
11
11
  JOBS_MULTI_TENANT,
12
12
  JOB_ORCHESTRATOR
@@ -198,4 +198,4 @@ DrizzleJobRunService = __decorateClass([
198
198
  export {
199
199
  DrizzleJobRunService
200
200
  };
201
- //# sourceMappingURL=chunk-3VEVGL74.js.map
201
+ //# sourceMappingURL=chunk-VNBC3VXM.js.map
@@ -1,3 +1,9 @@
1
+ import {
2
+ DrizzleJobStepService
3
+ } from "./chunk-DV4RV2DC.js";
4
+ import {
5
+ DrizzleJobOrchestrator
6
+ } from "./chunk-E6PLM6QG.js";
1
7
  import {
2
8
  MemoryJobOrchestrator
3
9
  } from "./chunk-VQOAATIG.js";
@@ -6,19 +12,13 @@ import {
6
12
  } from "./chunk-PNZSGAB2.js";
7
13
  import {
8
14
  DrizzleJobRunService
9
- } from "./chunk-3VEVGL74.js";
15
+ } from "./chunk-VNBC3VXM.js";
10
16
  import {
11
17
  MemoryJobRunService
12
- } from "./chunk-IN3EWFB4.js";
18
+ } from "./chunk-BHZP6LOV.js";
13
19
  import {
14
20
  MemoryJobStore
15
21
  } from "./chunk-SNQ3TOWP.js";
16
- import {
17
- DrizzleJobStepService
18
- } from "./chunk-DV4RV2DC.js";
19
- import {
20
- DrizzleJobOrchestrator
21
- } from "./chunk-E6PLM6QG.js";
22
22
  import {
23
23
  BULLMQ_CONNECTION,
24
24
  BULLMQ_RESOLVED_CONFIG,
@@ -114,4 +114,4 @@ JobsDomainModule = __decorateClass([
114
114
  export {
115
115
  JobsDomainModule
116
116
  };
117
- //# sourceMappingURL=chunk-AC6T2JUX.js.map
117
+ //# sourceMappingURL=chunk-XW4XKN3F.js.map
@@ -28,7 +28,7 @@ import {
28
28
  // runtime/subsystems/events/event-bus.drizzle-backend.ts
29
29
  import { randomUUID } from "crypto";
30
30
  import { Injectable, Inject, Logger, Optional } from "@nestjs/common";
31
- import { eq, and, inArray, asc, desc, gte, lt, or, sql } from "drizzle-orm";
31
+ import { eq, and, inArray, asc, desc, gte, lt, lte, or, sql } from "drizzle-orm";
32
32
  var POLL_INTERVAL_MS = 1e3;
33
33
  var POLL_BATCH_SIZE = 50;
34
34
  function toInsertValues(event, multiTenant) {
@@ -388,7 +388,8 @@ var DrizzleEventBus = class {
388
388
  */
389
389
  async processBatch() {
390
390
  const pools = this.opts.pools;
391
- const whereClause = pools && pools.length > 0 ? and(eq(domainEvents.status, "pending"), inArray(domainEvents.pool, pools)) : eq(domainEvents.status, "pending");
391
+ const ready = lte(domainEvents.occurredAt, /* @__PURE__ */ new Date());
392
+ const whereClause = pools && pools.length > 0 ? and(eq(domainEvents.status, "pending"), ready, inArray(domainEvents.pool, pools)) : and(eq(domainEvents.status, "pending"), ready);
392
393
  const rows = await this.db.transaction(async (tx) => {
393
394
  return tx.select().from(domainEvents).where(whereClause).orderBy(asc(domainEvents.occurredAt)).limit(POLL_BATCH_SIZE).for("update", { skipLocked: true });
394
395
  });
@@ -462,4 +463,4 @@ DrizzleEventBus = __decorateClass([
462
463
  export {
463
464
  DrizzleEventBus
464
465
  };
465
- //# sourceMappingURL=chunk-PNCOUFFI.js.map
466
+ //# sourceMappingURL=chunk-Y6UZMYGX.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, lte, 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 // Use a BARE (no-target) `ON CONFLICT DO NOTHING`: Drizzle 0.45's typed\n // `onConflictDoNothing({ target })` only accepts columns so it can't NAME\n // the expression index, but the no-arg form emits target-less\n // `ON CONFLICT DO NOTHING`, which Postgres applies to ANY unique\n // constraint/index — including this expression index. `.returning({ id })`\n // then gives us the rowcount discriminator: zero rows back == the slot was\n // already materialised (DO NOTHING fired), so `created: false`. This keeps\n // the happy path off the exception channel — a repeat materialise no longer\n // raises SQLSTATE 23505, so Postgres logs no scary `duplicate key value\n // violates unique constraint` ERROR line on every colliding boot/tick.\n //\n // The unique-violation catch is retained as a fallback for the genuine\n // concurrent-insert race window (two sessions clear the conflict check and\n // both attempt the insert in the same instant) and for backends whose\n // driver surfaces a 23505 rather than honouring DO NOTHING; in both cases\n // it collapses to the same `created: false` no-op.\n let inserted: Array<{ id: string }>;\n try {\n inserted = await this.db\n .insert(domainEvents)\n .values(values)\n .onConflictDoNothing()\n .returning({ id: domainEvents.id });\n } catch (err) {\n if (isUniqueViolation(err)) return { created: false };\n throw err;\n }\n if (inserted.length === 0) return { created: false };\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 occurred_at <= now [AND pool IN (...)].\n //\n // The readiness gate (`occurred_at <= now`) is what makes a scheduler-\n // materialised *future* slot wait. The EventScheduler pre-inserts the next\n // slot with `occurred_at = slotStart` in the future and deliberately does\n // NOT NOTIFY-wake the drainer for it (see materializeScheduledEvent: a\n // future slot is claimed by polling once its `occurred_at` passes). Without\n // this predicate the claim is status-only, so the very next poll grabs the\n // future-dated row and stamps `processed_at = now()` early — surfacing rows\n // that read \"N minutes from now\" yet are already processed, and firing\n // scheduled triggers up to one interval ahead of their slot. Normal events\n // publish with `occurred_at = now()`, so the gate is transparent to them.\n const ready = lte(domainEvents.occurredAt, new Date());\n const whereClause: SQL<unknown> = pools && pools.length > 0\n ? (and(eq(domainEvents.status, 'pending'), ready, inArray(domainEvents.pool, pools)) as SQL<unknown>)\n : (and(eq(domainEvents.status, 'pending'), ready) as SQL<unknown>);\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":[]}
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-DUUCU77W.js";
11
11
  import {
12
12
  DrizzleEventBus
13
- } from "./chunk-PNCOUFFI.js";
13
+ } from "./chunk-Y6UZMYGX.js";
14
14
  import {
15
15
  EVENTS_MODULE_OPTIONS,
16
16
  EVENTS_MULTI_TENANT,
@@ -200,4 +200,4 @@ export {
200
200
  EventSchedulerLifecycle,
201
201
  EventsModule
202
202
  };
203
- //# sourceMappingURL=chunk-VDVEGTSW.js.map
203
+ //# sourceMappingURL=chunk-YHVZAL6U.js.map
@@ -1,30 +1,30 @@
1
1
  import {
2
2
  bridgeRegistry
3
3
  } from "./chunk-5A432NZJ.js";
4
+ import {
5
+ BridgeOutboxDrainHook
6
+ } from "./chunk-3AM722ZH.js";
4
7
  import {
5
8
  EventFlowService
6
9
  } from "./chunk-7OVCARTQ.js";
7
10
  import {
8
11
  BRIDGE_RESERVED_POOLS
9
12
  } from "./chunk-EDKJU5BO.js";
13
+ import {
14
+ BridgeDeliveryHandler
15
+ } from "./chunk-R6F6KFIL.js";
10
16
  import {
11
17
  DrizzleBridgeDeliveryRepo
12
18
  } from "./chunk-MVKW2BCR.js";
13
19
  import {
14
20
  MemoryBridgeDeliveryRepo
15
21
  } from "./chunk-4DOJBQTP.js";
16
- import {
17
- BridgeOutboxDrainHook
18
- } from "./chunk-E5FJWOMP.js";
19
- import {
20
- BridgeDeliveryHandler
21
- } from "./chunk-R6F6KFIL.js";
22
22
  import {
23
23
  BridgeReservedPoolsNotPolledError
24
24
  } from "./chunk-NXXDZ6ZF.js";
25
25
  import {
26
26
  JOB_WORKER_MODULE_OPTIONS
27
- } from "./chunk-HEOISQ6W.js";
27
+ } from "./chunk-EGXFEZ2N.js";
28
28
  import {
29
29
  BRIDGE_DELIVERY_REPO,
30
30
  BRIDGE_MODULE_OPTIONS,
@@ -119,4 +119,4 @@ BridgeModule = __decorateClass([
119
119
  export {
120
120
  BridgeModule
121
121
  };
122
- //# sourceMappingURL=chunk-IXAE6BN6.js.map
122
+ //# sourceMappingURL=chunk-YQA5PMOD.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  BridgeOutboxDrainHook
3
- } from "../../../chunk-E5FJWOMP.js";
3
+ } from "../../../chunk-3AM722ZH.js";
4
4
  import "../../../chunk-R6F6KFIL.js";
5
5
  import "../../../chunk-6DWFJNIK.js";
6
6
  import "../../../chunk-BORNCTH3.js";
@@ -1,28 +1,28 @@
1
1
  import {
2
2
  BridgeModule
3
- } from "../../../chunk-IXAE6BN6.js";
3
+ } from "../../../chunk-YQA5PMOD.js";
4
4
  import "../../../chunk-5A432NZJ.js";
5
+ import "../../../chunk-3AM722ZH.js";
5
6
  import "../../../chunk-7OVCARTQ.js";
6
7
  import "../../../chunk-EDKJU5BO.js";
7
- import "../../../chunk-MVKW2BCR.js";
8
- import "../../../chunk-4DOJBQTP.js";
9
- import "../../../chunk-E5FJWOMP.js";
10
8
  import "../../../chunk-R6F6KFIL.js";
9
+ import "../../../chunk-MVKW2BCR.js";
11
10
  import "../../../chunk-6DWFJNIK.js";
11
+ import "../../../chunk-4DOJBQTP.js";
12
12
  import "../../../chunk-BORNCTH3.js";
13
13
  import "../../../chunk-NXXDZ6ZF.js";
14
- import "../../../chunk-HEOISQ6W.js";
14
+ import "../../../chunk-EGXFEZ2N.js";
15
15
  import "../../../chunk-7B7MMDOJ.js";
16
- import "../../../chunk-AC6T2JUX.js";
16
+ import "../../../chunk-XW4XKN3F.js";
17
+ import "../../../chunk-DV4RV2DC.js";
18
+ import "../../../chunk-E6PLM6QG.js";
17
19
  import "../../../chunk-VQOAATIG.js";
18
20
  import "../../../chunk-PNZSGAB2.js";
19
- import "../../../chunk-3VEVGL74.js";
20
- import "../../../chunk-IN3EWFB4.js";
21
+ import "../../../chunk-VNBC3VXM.js";
22
+ import "../../../chunk-BHZP6LOV.js";
21
23
  import "../../../chunk-SNQ3TOWP.js";
22
- import "../../../chunk-L3LZWWSX.js";
23
- import "../../../chunk-DV4RV2DC.js";
24
- import "../../../chunk-E6PLM6QG.js";
25
24
  import "../../../chunk-T4BIIU5E.js";
25
+ import "../../../chunk-L3LZWWSX.js";
26
26
  import "../../../chunk-I6MVCB5A.js";
27
27
  import "../../../chunk-RHVN6NA7.js";
28
28
  import "../../../chunk-Q6LRJ4VI.js";
@@ -266,14 +266,16 @@ type BridgeRegistry = {
266
266
  * `triggerCount`: total triggers matched in the registry for this event;
267
267
  * `triggerCount === delivered + dedupSkips`.
268
268
  *
269
- * `auditBlocked`: number of events the dispatcher refused to fan out
270
- * because their `metadata.tier === 'audit'`. Audit-tier events are not
271
- * bridge-eligible; codegen errors block the registry from listing them
272
- * as triggers, so a non-zero value here indicates registry/runtime drift
273
- * (an out-of-band `bridge_trigger` insert, version skew during deploy).
274
- * Per-event: `0` when the guard does not fire, `1` when it does. Add it
275
- * to per-batch logging if your drain caller aggregates results. See
276
- * ai-docs/specs/issue-242/plan.md §AUDIT-4.
269
+ * `auditBlocked`: number of audit-tier events the dispatcher refused to fan
270
+ * out *because they had a trigger registered against them*. Audit-tier events
271
+ * are not bridge-eligible; codegen errors block the registry from listing them
272
+ * as triggers, so a non-zero value here indicates genuine registry/runtime
273
+ * drift (an out-of-band `bridge_trigger` insert, version skew during deploy).
274
+ * A benign audit-tier event one with no matched trigger, the common case for
275
+ * lifecycle events sharing the outbox returns `0` and produces no log.
276
+ * Per-event: `0` when the guard does not fire (including benign audit events),
277
+ * `1` when it fires on drift. Add it to per-batch logging if your drain caller
278
+ * aggregates results. See ai-docs/specs/issue-242/plan.md §AUDIT-4.
277
279
  */
278
280
  interface BridgeOutboxDrainResult {
279
281
  delivered: number;
@@ -307,13 +309,17 @@ interface IBridgeOutboxDrainHook {
307
309
  * job_run` row pairs for every matched trigger via the supplied `tx`.
308
310
  *
309
311
  * Behaviour:
310
- * 0. **Audit-tier guard (defense-in-depth).** If
311
- * `event.metadata.tier === 'audit'`, returns
312
+ * 0. **Audit-tier guard (defense-in-depth).** Runs *after* the registry
313
+ * lookup (step 1). If `event.metadata.tier === 'audit'` AND a trigger
314
+ * is registered against the type (genuine drift — AUDIT-2 should have
315
+ * prevented it), returns
312
316
  * `{ delivered: 0, dedupSkips: 0, triggerCount: 0, auditBlocked: 1 }`
313
- * immediately and logs a per-`(event_type, process)` WARN. The
314
- * codegen-side validator (AUDIT-2) is the primary enforcement;
315
- * this guard catches out-of-band `bridge_trigger` inserts and
316
- * version skew during deploy. See
317
+ * and logs a per-`(event_type, process)` WARN naming the offending
318
+ * trigger id(s). If the audit event has *no* matched trigger (the
319
+ * common, benign case lifecycle events sharing the outbox), returns
320
+ * all zeros (`auditBlocked: 0`) silently. The codegen-side validator
321
+ * (AUDIT-2) is the primary enforcement; this guard catches out-of-band
322
+ * `bridge_trigger` inserts and version skew. See
317
323
  * ai-docs/specs/issue-242/plan.md §AUDIT-4.
318
324
  * 1. Looks up `bridgeRegistry[event.type]`. No matches → returns
319
325
  * `{ delivered: 0, dedupSkips: 0, triggerCount: 0, auditBlocked: 0 }`;
@@ -1,29 +1,29 @@
1
1
  import {
2
2
  BridgeModule
3
- } from "../../../chunk-IXAE6BN6.js";
3
+ } from "../../../chunk-YQA5PMOD.js";
4
4
  import "../../../chunk-5A432NZJ.js";
5
+ import {
6
+ BridgeOutboxDrainHook
7
+ } from "../../../chunk-3AM722ZH.js";
5
8
  import {
6
9
  EventFlowService
7
10
  } from "../../../chunk-7OVCARTQ.js";
8
11
  import {
9
12
  BRIDGE_RESERVED_POOLS
10
13
  } from "../../../chunk-EDKJU5BO.js";
11
- import {
12
- DrizzleBridgeDeliveryRepo
13
- } from "../../../chunk-MVKW2BCR.js";
14
- import {
15
- MemoryBridgeDeliveryRepo
16
- } from "../../../chunk-4DOJBQTP.js";
17
- import {
18
- BridgeOutboxDrainHook
19
- } from "../../../chunk-E5FJWOMP.js";
20
14
  import {
21
15
  BRIDGE_DELIVERY_JOB_TYPE,
22
16
  BridgeDeliveryHandler
23
17
  } from "../../../chunk-R6F6KFIL.js";
18
+ import {
19
+ DrizzleBridgeDeliveryRepo
20
+ } from "../../../chunk-MVKW2BCR.js";
24
21
  import {
25
22
  assertTenantId
26
23
  } from "../../../chunk-6DWFJNIK.js";
24
+ import {
25
+ MemoryBridgeDeliveryRepo
26
+ } from "../../../chunk-4DOJBQTP.js";
27
27
  import {
28
28
  bridgeDelivery,
29
29
  bridgeDeliveryStatusEnum
@@ -33,18 +33,18 @@ import {
33
33
  MissingTenantIdError,
34
34
  UniqueConstraintError
35
35
  } from "../../../chunk-NXXDZ6ZF.js";
36
- import "../../../chunk-HEOISQ6W.js";
36
+ import "../../../chunk-EGXFEZ2N.js";
37
37
  import "../../../chunk-7B7MMDOJ.js";
38
- import "../../../chunk-AC6T2JUX.js";
38
+ import "../../../chunk-XW4XKN3F.js";
39
+ import "../../../chunk-DV4RV2DC.js";
40
+ import "../../../chunk-E6PLM6QG.js";
39
41
  import "../../../chunk-VQOAATIG.js";
40
42
  import "../../../chunk-PNZSGAB2.js";
41
- import "../../../chunk-3VEVGL74.js";
42
- import "../../../chunk-IN3EWFB4.js";
43
+ import "../../../chunk-VNBC3VXM.js";
44
+ import "../../../chunk-BHZP6LOV.js";
43
45
  import "../../../chunk-SNQ3TOWP.js";
44
- import "../../../chunk-L3LZWWSX.js";
45
- import "../../../chunk-DV4RV2DC.js";
46
- import "../../../chunk-E6PLM6QG.js";
47
46
  import "../../../chunk-T4BIIU5E.js";
47
+ import "../../../chunk-L3LZWWSX.js";
48
48
  import "../../../chunk-I6MVCB5A.js";
49
49
  import "../../../chunk-RHVN6NA7.js";
50
50
  import "../../../chunk-Q6LRJ4VI.js";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  CacheModule
3
- } from "../../../chunk-COGHTKXY.js";
4
- import "../../../chunk-T6C4LFLC.js";
3
+ } from "../../../chunk-7625PLY7.js";
5
4
  import "../../../chunk-IF5I3DAA.js";
5
+ import "../../../chunk-T6C4LFLC.js";
6
6
  import "../../../chunk-FASRXRX5.js";
7
7
  import "../../../chunk-L6FTY45T.js";
8
8
  import "../../../chunk-GYGNEQSC.js";
@@ -1,13 +1,13 @@
1
1
  import "../../../chunk-IWAOY6KC.js";
2
2
  import {
3
3
  CacheModule
4
- } from "../../../chunk-COGHTKXY.js";
5
- import {
6
- DrizzleCacheService
7
- } from "../../../chunk-T6C4LFLC.js";
4
+ } from "../../../chunk-7625PLY7.js";
8
5
  import {
9
6
  MemoryCacheService
10
7
  } from "../../../chunk-IF5I3DAA.js";
8
+ import {
9
+ DrizzleCacheService
10
+ } from "../../../chunk-T6C4LFLC.js";
11
11
  import {
12
12
  cacheEntries
13
13
  } from "../../../chunk-FASRXRX5.js";