@pattern-stack/codegen 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +3 -0
- package/dist/runtime/subsystems/bridge/bridge.module.js +930 -275
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +3 -0
- package/dist/runtime/subsystems/bridge/index.js +837 -182
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +3 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +92 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +3 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +99 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-keyset-cursor.d.ts +32 -0
- package/dist/runtime/subsystems/events/event-keyset-cursor.js +38 -0
- package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -0
- package/dist/runtime/subsystems/events/event-read.protocol.d.ts +94 -0
- package/dist/runtime/subsystems/events/event-read.protocol.js +9 -0
- package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -0
- package/dist/runtime/subsystems/events/events.module.js +177 -3
- package/dist/runtime/subsystems/events/events.module.js.map +1 -1
- package/dist/runtime/subsystems/events/events.tokens.d.ts +16 -1
- package/dist/runtime/subsystems/events/events.tokens.js +2 -0
- package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
- package/dist/runtime/subsystems/events/index.d.ts +2 -1
- package/dist/runtime/subsystems/events/index.js +178 -3
- package/dist/runtime/subsystems/events/index.js.map +1 -1
- package/dist/runtime/subsystems/index.d.ts +1 -0
- package/dist/runtime/subsystems/index.js +1194 -264
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +98 -0
- package/dist/runtime/subsystems/jobs/bullmq.config.js +143 -0
- package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -0
- package/dist/runtime/subsystems/jobs/index.d.ts +6 -2
- package/dist/runtime/subsystems/jobs/index.js +861 -201
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +107 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +922 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +52 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +57 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +81 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +81 -0
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +74 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +48 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +374 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +42 -4
- package/dist/runtime/subsystems/jobs/job-worker.module.js +832 -178
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +10 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +519 -20
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/pool-config.loader.d.ts +9 -1
- package/dist/runtime/subsystems/jobs/pool-config.loader.js +4 -0
- package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
- package/dist/runtime/subsystems/observability/index.d.ts +4 -3
- package/dist/runtime/subsystems/observability/index.js +109 -2
- package/dist/runtime/subsystems/observability/index.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.module.js +109 -2
- package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.protocol.d.ts +63 -2
- package/dist/runtime/subsystems/observability/observability.service.d.ts +21 -3
- package/dist/runtime/subsystems/observability/observability.service.js +109 -2
- package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -0
- package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -0
- package/dist/src/cli/index.js +30 -6
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/bridge/bridge.module.ts +5 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +109 -3
- package/runtime/subsystems/events/event-bus.memory-backend.ts +103 -1
- package/runtime/subsystems/events/event-keyset-cursor.ts +59 -0
- package/runtime/subsystems/events/event-read.protocol.ts +97 -0
- package/runtime/subsystems/events/events.module.ts +18 -2
- package/runtime/subsystems/events/events.tokens.ts +16 -0
- package/runtime/subsystems/events/index.ts +7 -0
- package/runtime/subsystems/jobs/bullmq.config.ts +125 -0
- package/runtime/subsystems/jobs/index.ts +22 -0
- package/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts +381 -0
- package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +88 -0
- package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +59 -1
- package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +53 -0
- package/runtime/subsystems/jobs/job-run-service.protocol.ts +77 -0
- package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +311 -0
- package/runtime/subsystems/jobs/job-worker.module.ts +124 -10
- package/runtime/subsystems/jobs/jobs-domain.module.ts +40 -21
- package/runtime/subsystems/jobs/pool-config.loader.ts +11 -0
- package/runtime/subsystems/observability/index.ts +8 -0
- package/runtime/subsystems/observability/observability.protocol.ts +76 -0
- package/runtime/subsystems/observability/observability.service.ts +148 -1
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -12
- package/templates/relationship/new/prompt.js +8 -5
- package/templates/subsystem/jobs/worker.ejs.t +30 -7
- package/templates/subsystem/sync/sync-audit.schema.ejs.t +12 -16
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyset (seek) cursor codec for `IEventReadPort.listEvents` (OBS-LIST-1).
|
|
3
|
+
*
|
|
4
|
+
* The list is ordered `occurred_at DESC, id DESC`. The cursor encodes the
|
|
5
|
+
* `(occurredAt, id)` of the last row on the previous page so the next page
|
|
6
|
+
* seeks with `WHERE (occurred_at, id) < (cursorOccurredAt, cursorId)`.
|
|
7
|
+
*
|
|
8
|
+
* The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape
|
|
9
|
+
* is an implementation detail — never parse it outside this module.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the jobs keyset codec; kept separate because the events subsystem
|
|
12
|
+
* must not depend on `runtime/subsystems/jobs/`.
|
|
13
|
+
*/
|
|
14
|
+
interface EventKeyset {
|
|
15
|
+
occurredAt: Date;
|
|
16
|
+
id: string;
|
|
17
|
+
}
|
|
18
|
+
/** Default page size when `limit` is omitted. */
|
|
19
|
+
declare const DEFAULT_EVENT_LIST_LIMIT = 50;
|
|
20
|
+
/** Hard upper bound on page size. */
|
|
21
|
+
declare const MAX_EVENT_LIST_LIMIT = 200;
|
|
22
|
+
/** Clamp a caller-supplied `limit` into `[1, MAX_EVENT_LIST_LIMIT]`. */
|
|
23
|
+
declare function clampEventLimit(limit: number | undefined): number;
|
|
24
|
+
declare function encodeEventCursor(keyset: EventKeyset): string;
|
|
25
|
+
/**
|
|
26
|
+
* Decode an opaque cursor back into its `(occurredAt, id)` keyset. Returns
|
|
27
|
+
* `null` for malformed input so user-supplied garbage is treated as "start
|
|
28
|
+
* from the beginning" rather than throwing.
|
|
29
|
+
*/
|
|
30
|
+
declare function decodeEventCursor(cursor: string): EventKeyset | null;
|
|
31
|
+
|
|
32
|
+
export { DEFAULT_EVENT_LIST_LIMIT, type EventKeyset, MAX_EVENT_LIST_LIMIT, clampEventLimit, decodeEventCursor, encodeEventCursor };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// runtime/subsystems/events/event-keyset-cursor.ts
|
|
2
|
+
var DEFAULT_EVENT_LIST_LIMIT = 50;
|
|
3
|
+
var MAX_EVENT_LIST_LIMIT = 200;
|
|
4
|
+
function clampEventLimit(limit) {
|
|
5
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
6
|
+
return DEFAULT_EVENT_LIST_LIMIT;
|
|
7
|
+
}
|
|
8
|
+
const floored = Math.floor(limit);
|
|
9
|
+
if (floored < 1) return 1;
|
|
10
|
+
if (floored > MAX_EVENT_LIST_LIMIT) return MAX_EVENT_LIST_LIMIT;
|
|
11
|
+
return floored;
|
|
12
|
+
}
|
|
13
|
+
function encodeEventCursor(keyset) {
|
|
14
|
+
const tuple = [keyset.occurredAt.toISOString(), keyset.id];
|
|
15
|
+
return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
|
|
16
|
+
}
|
|
17
|
+
function decodeEventCursor(cursor) {
|
|
18
|
+
try {
|
|
19
|
+
const json = Buffer.from(cursor, "base64url").toString("utf8");
|
|
20
|
+
const parsed = JSON.parse(json);
|
|
21
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) return null;
|
|
22
|
+
const [iso, id] = parsed;
|
|
23
|
+
if (typeof iso !== "string" || typeof id !== "string") return null;
|
|
24
|
+
const occurredAt = new Date(iso);
|
|
25
|
+
if (Number.isNaN(occurredAt.getTime())) return null;
|
|
26
|
+
return { occurredAt, id };
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
DEFAULT_EVENT_LIST_LIMIT,
|
|
33
|
+
MAX_EVENT_LIST_LIMIT,
|
|
34
|
+
clampEventLimit,
|
|
35
|
+
decodeEventCursor,
|
|
36
|
+
encodeEventCursor
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=event-keyset-cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/events/event-keyset-cursor.ts"],"sourcesContent":["/**\n * Keyset (seek) cursor codec for `IEventReadPort.listEvents` (OBS-LIST-1).\n *\n * The list is ordered `occurred_at DESC, id DESC`. The cursor encodes the\n * `(occurredAt, id)` of the last row on the previous page so the next page\n * seeks with `WHERE (occurred_at, id) < (cursorOccurredAt, cursorId)`.\n *\n * The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape\n * is an implementation detail — never parse it outside this module.\n *\n * Mirrors the jobs keyset codec; kept separate because the events subsystem\n * must not depend on `runtime/subsystems/jobs/`.\n */\n\nexport interface EventKeyset {\n occurredAt: Date;\n id: string;\n}\n\n/** Default page size when `limit` is omitted. */\nexport const DEFAULT_EVENT_LIST_LIMIT = 50;\n/** Hard upper bound on page size. */\nexport const MAX_EVENT_LIST_LIMIT = 200;\n\n/** Clamp a caller-supplied `limit` into `[1, MAX_EVENT_LIST_LIMIT]`. */\nexport function clampEventLimit(limit: number | undefined): number {\n if (typeof limit !== 'number' || !Number.isFinite(limit)) {\n return DEFAULT_EVENT_LIST_LIMIT;\n }\n const floored = Math.floor(limit);\n if (floored < 1) return 1;\n if (floored > MAX_EVENT_LIST_LIMIT) return MAX_EVENT_LIST_LIMIT;\n return floored;\n}\n\nexport function encodeEventCursor(keyset: EventKeyset): string {\n const tuple = [keyset.occurredAt.toISOString(), keyset.id];\n return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');\n}\n\n/**\n * Decode an opaque cursor back into its `(occurredAt, id)` keyset. Returns\n * `null` for malformed input so user-supplied garbage is treated as \"start\n * from the beginning\" rather than throwing.\n */\nexport function decodeEventCursor(cursor: string): EventKeyset | null {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf8');\n const parsed = JSON.parse(json) as unknown;\n if (!Array.isArray(parsed) || parsed.length !== 2) return null;\n const [iso, id] = parsed;\n if (typeof iso !== 'string' || typeof id !== 'string') return null;\n const occurredAt = new Date(iso);\n if (Number.isNaN(occurredAt.getTime())) return null;\n return { occurredAt, id };\n } catch {\n return null;\n }\n}\n"],"mappings":";AAoBO,IAAM,2BAA2B;AAEjC,IAAM,uBAAuB;AAG7B,SAAS,gBAAgB,OAAmC;AACjE,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,GAAG;AACxD,WAAO;AAAA,EACT;AACA,QAAM,UAAU,KAAK,MAAM,KAAK;AAChC,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,UAAU,qBAAsB,QAAO;AAC3C,SAAO;AACT;AAEO,SAAS,kBAAkB,QAA6B;AAC7D,QAAM,QAAQ,CAAC,OAAO,WAAW,YAAY,GAAG,OAAO,EAAE;AACzD,SAAO,OAAO,KAAK,KAAK,UAAU,KAAK,GAAG,MAAM,EAAE,SAAS,WAAW;AACxE;AAOO,SAAS,kBAAkB,QAAoC;AACpE,MAAI;AACF,UAAM,OAAO,OAAO,KAAK,QAAQ,WAAW,EAAE,SAAS,MAAM;AAC7D,UAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,EAAG,QAAO;AAC1D,UAAM,CAAC,KAAK,EAAE,IAAI;AAClB,QAAI,OAAO,QAAQ,YAAY,OAAO,OAAO,SAAU,QAAO;AAC9D,UAAM,aAAa,IAAI,KAAK,GAAG;AAC/B,QAAI,OAAO,MAAM,WAAW,QAAQ,CAAC,EAAG,QAAO;AAC/C,WAAO,EAAE,YAAY,GAAG;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { DomainEvent } from './event-bus.protocol.js';
|
|
2
|
+
import '../../types/drizzle.js';
|
|
3
|
+
import 'drizzle-orm/node-postgres';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* IEventReadPort — read-side port over `domain_events` (OBS-LIST-1).
|
|
7
|
+
*
|
|
8
|
+
* The publish/subscribe `IEventBus` (EVENT_BUS) is a *write + dispatch*
|
|
9
|
+
* port; it deliberately does not expose tabular reads beyond `findById`.
|
|
10
|
+
* The observability combiner needs a paginated, filterable list of
|
|
11
|
+
* `domain_events` for its events viewer, so we add a dedicated read port
|
|
12
|
+
* rather than overloading `IEventBus`.
|
|
13
|
+
*
|
|
14
|
+
* Keeping reads on a separate port means:
|
|
15
|
+
* - the combiner can compose it `@Optional()` independently of EVENT_BUS;
|
|
16
|
+
* - the Redis backend (which retains no history) simply does not provide
|
|
17
|
+
* it — there is no "list" semantics to fake;
|
|
18
|
+
* - the write/dispatch surface stays minimal.
|
|
19
|
+
*
|
|
20
|
+
* Both `DrizzleEventBus` and `MemoryEventBus` implement this port (they
|
|
21
|
+
* already hold the rows / in-memory log); `EventsModule.forRoot` binds the
|
|
22
|
+
* `EVENT_READ_PORT` token to the same instance for drizzle/memory backends.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Filter + keyset-pagination input for `IEventReadPort.listEvents`.
|
|
27
|
+
*
|
|
28
|
+
* Ordered `occurred_at DESC, id DESC`. `rootRunId` filters on the JSON
|
|
29
|
+
* `metadata->>'rootRunId'` — the correlation id stamped by the jobs/bridge
|
|
30
|
+
* machinery so an event can be traced back to the run tree that emitted it.
|
|
31
|
+
*/
|
|
32
|
+
interface ListEventsQuery {
|
|
33
|
+
/** Filter on `metadata->>'rootRunId'` (correlation id). */
|
|
34
|
+
rootRunId?: string;
|
|
35
|
+
/** Filter on the first-class `pool` column. */
|
|
36
|
+
poolId?: string;
|
|
37
|
+
/** Filter on the first-class `direction` column (inbound|change|outbound). */
|
|
38
|
+
direction?: string;
|
|
39
|
+
/** Lower bound on `occurred_at` (inclusive). */
|
|
40
|
+
since?: Date;
|
|
41
|
+
/** Opaque keyset cursor from a previous page's `nextCursor`. */
|
|
42
|
+
cursor?: string;
|
|
43
|
+
/** Page size. Backend clamps to a sane default + max. */
|
|
44
|
+
limit?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Multi-tenancy filter on the first-class `tenant_id` column. Only
|
|
47
|
+
* meaningful when the consumer publishes tenant-scoped events
|
|
48
|
+
* (`events.multi_tenant: true`); otherwise leave undefined.
|
|
49
|
+
* - `string` — filter `tenant_id = :tenantId`.
|
|
50
|
+
* - `null` — filter `tenant_id IS NULL`.
|
|
51
|
+
* - `undefined` — no tenant filter.
|
|
52
|
+
*/
|
|
53
|
+
tenantId?: string | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Summary row for the events list. A narrow projection over `domain_events`
|
|
57
|
+
* carrying what the viewer + correlation timeline need. `rootRunId` is
|
|
58
|
+
* surfaced (lifted out of `metadata`) so the timeline can stitch without a
|
|
59
|
+
* second metadata dig.
|
|
60
|
+
*/
|
|
61
|
+
interface EventSummary {
|
|
62
|
+
id: string;
|
|
63
|
+
type: string;
|
|
64
|
+
aggregateId: string;
|
|
65
|
+
aggregateType: string;
|
|
66
|
+
status: string;
|
|
67
|
+
pool: string | null;
|
|
68
|
+
direction: string | null;
|
|
69
|
+
tier: string;
|
|
70
|
+
rootRunId: string | null;
|
|
71
|
+
tenantId: string | null;
|
|
72
|
+
occurredAt: Date;
|
|
73
|
+
processedAt: Date | null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One page of `listEvents` results. `nextCursor` is `null` when there are
|
|
77
|
+
* no more rows.
|
|
78
|
+
*/
|
|
79
|
+
interface EventPage {
|
|
80
|
+
items: EventSummary[];
|
|
81
|
+
nextCursor: string | null;
|
|
82
|
+
}
|
|
83
|
+
interface IEventReadPort {
|
|
84
|
+
/**
|
|
85
|
+
* Paginated, filterable list of `domain_events` (OBS-LIST-1). Newest
|
|
86
|
+
* first (`occurred_at` desc, `id` desc keyset tie-break). Returns an
|
|
87
|
+
* `EventPage` with an opaque `nextCursor` for keyset pagination.
|
|
88
|
+
*/
|
|
89
|
+
listEvents(query?: ListEventsQuery): Promise<EventPage>;
|
|
90
|
+
}
|
|
91
|
+
/** A `DomainEvent` whose metadata may carry a `rootRunId` correlation id. */
|
|
92
|
+
declare function rootRunIdOf(event: DomainEvent): string | null;
|
|
93
|
+
|
|
94
|
+
export { type EventPage, type EventSummary, type IEventReadPort, type ListEventsQuery, rootRunIdOf };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/events/event-read.protocol.ts"],"sourcesContent":["/**\n * IEventReadPort — read-side port over `domain_events` (OBS-LIST-1).\n *\n * The publish/subscribe `IEventBus` (EVENT_BUS) is a *write + dispatch*\n * port; it deliberately does not expose tabular reads beyond `findById`.\n * The observability combiner needs a paginated, filterable list of\n * `domain_events` for its events viewer, so we add a dedicated read port\n * rather than overloading `IEventBus`.\n *\n * Keeping reads on a separate port means:\n * - the combiner can compose it `@Optional()` independently of EVENT_BUS;\n * - the Redis backend (which retains no history) simply does not provide\n * it — there is no \"list\" semantics to fake;\n * - the write/dispatch surface stays minimal.\n *\n * Both `DrizzleEventBus` and `MemoryEventBus` implement this port (they\n * already hold the rows / in-memory log); `EventsModule.forRoot` binds the\n * `EVENT_READ_PORT` token to the same instance for drizzle/memory backends.\n */\n\nimport type { DomainEvent } from './event-bus.protocol';\n\n/**\n * Filter + keyset-pagination input for `IEventReadPort.listEvents`.\n *\n * Ordered `occurred_at DESC, id DESC`. `rootRunId` filters on the JSON\n * `metadata->>'rootRunId'` — the correlation id stamped by the jobs/bridge\n * machinery so an event can be traced back to the run tree that emitted it.\n */\nexport interface ListEventsQuery {\n /** Filter on `metadata->>'rootRunId'` (correlation id). */\n rootRunId?: string;\n /** Filter on the first-class `pool` column. */\n poolId?: string;\n /** Filter on the first-class `direction` column (inbound|change|outbound). */\n direction?: string;\n /** Lower bound on `occurred_at` (inclusive). */\n since?: Date;\n /** Opaque keyset cursor from a previous page's `nextCursor`. */\n cursor?: string;\n /** Page size. Backend clamps to a sane default + max. */\n limit?: number;\n /**\n * Multi-tenancy filter on the first-class `tenant_id` column. Only\n * meaningful when the consumer publishes tenant-scoped events\n * (`events.multi_tenant: true`); otherwise leave undefined.\n * - `string` — filter `tenant_id = :tenantId`.\n * - `null` — filter `tenant_id IS NULL`.\n * - `undefined` — no tenant filter.\n */\n tenantId?: string | null;\n}\n\n/**\n * Summary row for the events list. A narrow projection over `domain_events`\n * carrying what the viewer + correlation timeline need. `rootRunId` is\n * surfaced (lifted out of `metadata`) so the timeline can stitch without a\n * second metadata dig.\n */\nexport interface EventSummary {\n id: string;\n type: string;\n aggregateId: string;\n aggregateType: string;\n status: string;\n pool: string | null;\n direction: string | null;\n tier: string;\n rootRunId: string | null;\n tenantId: string | null;\n occurredAt: Date;\n processedAt: Date | null;\n}\n\n/**\n * One page of `listEvents` results. `nextCursor` is `null` when there are\n * no more rows.\n */\nexport interface EventPage {\n items: EventSummary[];\n nextCursor: string | null;\n}\n\nexport interface IEventReadPort {\n /**\n * Paginated, filterable list of `domain_events` (OBS-LIST-1). Newest\n * first (`occurred_at` desc, `id` desc keyset tie-break). Returns an\n * `EventPage` with an opaque `nextCursor` for keyset pagination.\n */\n listEvents(query?: ListEventsQuery): Promise<EventPage>;\n}\n\n/** A `DomainEvent` whose metadata may carry a `rootRunId` correlation id. */\nexport function rootRunIdOf(event: DomainEvent): string | null {\n const v = event.metadata?.['rootRunId'];\n return typeof v === 'string' ? v : null;\n}\n"],"mappings":";AA6FO,SAAS,YAAY,OAAmC;AAC7D,QAAM,IAAI,MAAM,WAAW,WAAW;AACtC,SAAO,OAAO,MAAM,WAAW,IAAI;AACrC;","names":[]}
|
|
@@ -15,6 +15,7 @@ import { Module } from "@nestjs/common";
|
|
|
15
15
|
|
|
16
16
|
// runtime/subsystems/events/events.tokens.ts
|
|
17
17
|
var EVENT_BUS = "EVENT_BUS";
|
|
18
|
+
var EVENT_READ_PORT = "EVENT_READ_PORT";
|
|
18
19
|
var TYPED_EVENT_BUS = "TYPED_EVENT_BUS";
|
|
19
20
|
var EVENTS_MULTI_TENANT = "EVENTS_MULTI_TENANT";
|
|
20
21
|
var REDIS_URL = /* @__PURE__ */ Symbol("REDIS_URL");
|
|
@@ -25,7 +26,38 @@ var DRIZZLE = "DRIZZLE";
|
|
|
25
26
|
|
|
26
27
|
// runtime/subsystems/events/event-bus.drizzle-backend.ts
|
|
27
28
|
import { Injectable, Inject, Logger, Optional } from "@nestjs/common";
|
|
28
|
-
import { eq, and, inArray, asc } from "drizzle-orm";
|
|
29
|
+
import { eq, and, inArray, asc, desc, gte, lt, or, sql as sql2 } from "drizzle-orm";
|
|
30
|
+
|
|
31
|
+
// runtime/subsystems/events/event-keyset-cursor.ts
|
|
32
|
+
var DEFAULT_EVENT_LIST_LIMIT = 50;
|
|
33
|
+
var MAX_EVENT_LIST_LIMIT = 200;
|
|
34
|
+
function clampEventLimit(limit) {
|
|
35
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
36
|
+
return DEFAULT_EVENT_LIST_LIMIT;
|
|
37
|
+
}
|
|
38
|
+
const floored = Math.floor(limit);
|
|
39
|
+
if (floored < 1) return 1;
|
|
40
|
+
if (floored > MAX_EVENT_LIST_LIMIT) return MAX_EVENT_LIST_LIMIT;
|
|
41
|
+
return floored;
|
|
42
|
+
}
|
|
43
|
+
function encodeEventCursor(keyset) {
|
|
44
|
+
const tuple = [keyset.occurredAt.toISOString(), keyset.id];
|
|
45
|
+
return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
|
|
46
|
+
}
|
|
47
|
+
function decodeEventCursor(cursor) {
|
|
48
|
+
try {
|
|
49
|
+
const json = Buffer.from(cursor, "base64url").toString("utf8");
|
|
50
|
+
const parsed = JSON.parse(json);
|
|
51
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) return null;
|
|
52
|
+
const [iso, id] = parsed;
|
|
53
|
+
if (typeof iso !== "string" || typeof id !== "string") return null;
|
|
54
|
+
const occurredAt = new Date(iso);
|
|
55
|
+
if (Number.isNaN(occurredAt.getTime())) return null;
|
|
56
|
+
return { occurredAt, id };
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
29
61
|
|
|
30
62
|
// runtime/subsystems/events/domain-events.schema.ts
|
|
31
63
|
import {
|
|
@@ -129,6 +161,24 @@ function toInsertValues(event) {
|
|
|
129
161
|
tenantId
|
|
130
162
|
};
|
|
131
163
|
}
|
|
164
|
+
function toEventSummary(r) {
|
|
165
|
+
const metadata = r.metadata ?? void 0;
|
|
166
|
+
const rootRunId = metadata?.["rootRunId"];
|
|
167
|
+
return {
|
|
168
|
+
id: r.id,
|
|
169
|
+
type: r.type,
|
|
170
|
+
aggregateId: r.aggregateId,
|
|
171
|
+
aggregateType: r.aggregateType,
|
|
172
|
+
status: r.status,
|
|
173
|
+
pool: r.pool,
|
|
174
|
+
direction: r.direction,
|
|
175
|
+
tier: r.tier,
|
|
176
|
+
rootRunId: typeof rootRunId === "string" ? rootRunId : null,
|
|
177
|
+
tenantId: r.tenantId,
|
|
178
|
+
occurredAt: r.occurredAt instanceof Date ? r.occurredAt : new Date(r.occurredAt),
|
|
179
|
+
processedAt: r.processedAt == null ? null : r.processedAt instanceof Date ? r.processedAt : new Date(r.processedAt)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
132
182
|
var DrizzleEventBus = class {
|
|
133
183
|
constructor(db, opts, bridgeHook = null) {
|
|
134
184
|
this.db = db;
|
|
@@ -194,6 +244,48 @@ var DrizzleEventBus = class {
|
|
|
194
244
|
};
|
|
195
245
|
}
|
|
196
246
|
// ============================================================================
|
|
247
|
+
// IEventReadPort (OBS-LIST-1)
|
|
248
|
+
// ============================================================================
|
|
249
|
+
async listEvents(query = {}) {
|
|
250
|
+
const limit = clampEventLimit(query.limit);
|
|
251
|
+
const conditions = [];
|
|
252
|
+
if (query.poolId) conditions.push(eq(domainEvents.pool, query.poolId));
|
|
253
|
+
if (query.direction)
|
|
254
|
+
conditions.push(eq(domainEvents.direction, query.direction));
|
|
255
|
+
if (query.since) conditions.push(gte(domainEvents.occurredAt, query.since));
|
|
256
|
+
if (query.rootRunId) {
|
|
257
|
+
conditions.push(
|
|
258
|
+
sql2`${domainEvents.metadata}->>'rootRunId' = ${query.rootRunId}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (query.tenantId !== void 0) {
|
|
262
|
+
conditions.push(
|
|
263
|
+
query.tenantId === null ? sql2`${domainEvents.tenantId} is null` : eq(domainEvents.tenantId, query.tenantId)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (query.cursor) {
|
|
267
|
+
const keyset = decodeEventCursor(query.cursor);
|
|
268
|
+
if (keyset) {
|
|
269
|
+
conditions.push(
|
|
270
|
+
or(
|
|
271
|
+
lt(domainEvents.occurredAt, keyset.occurredAt),
|
|
272
|
+
and(
|
|
273
|
+
eq(domainEvents.occurredAt, keyset.occurredAt),
|
|
274
|
+
lt(domainEvents.id, keyset.id)
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const rows = await this.db.select().from(domainEvents).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(desc(domainEvents.occurredAt), desc(domainEvents.id)).limit(limit + 1);
|
|
281
|
+
const hasMore = rows.length > limit;
|
|
282
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
283
|
+
const items = page.map(toEventSummary);
|
|
284
|
+
const last = page[page.length - 1];
|
|
285
|
+
const nextCursor = hasMore && last ? encodeEventCursor({ occurredAt: last.occurredAt, id: last.id }) : null;
|
|
286
|
+
return { items, nextCursor };
|
|
287
|
+
}
|
|
288
|
+
// ============================================================================
|
|
197
289
|
// Polling
|
|
198
290
|
// ============================================================================
|
|
199
291
|
/**
|
|
@@ -318,6 +410,27 @@ DrizzleEventBus = __decorateClass([
|
|
|
318
410
|
|
|
319
411
|
// runtime/subsystems/events/event-bus.memory-backend.ts
|
|
320
412
|
import { Inject as Inject2, Injectable as Injectable2, Logger as Logger2, Optional as Optional2 } from "@nestjs/common";
|
|
413
|
+
function toEventSummary2(event) {
|
|
414
|
+
const metadata = event.metadata;
|
|
415
|
+
const str = (key) => {
|
|
416
|
+
const v = metadata?.[key];
|
|
417
|
+
return typeof v === "string" ? v : null;
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
id: event.id,
|
|
421
|
+
type: event.type,
|
|
422
|
+
aggregateId: event.aggregateId,
|
|
423
|
+
aggregateType: event.aggregateType,
|
|
424
|
+
status: "processed",
|
|
425
|
+
pool: str("pool"),
|
|
426
|
+
direction: str("direction"),
|
|
427
|
+
tier: str("tier") ?? "domain",
|
|
428
|
+
rootRunId: str("rootRunId"),
|
|
429
|
+
tenantId: str("tenantId"),
|
|
430
|
+
occurredAt: event.occurredAt,
|
|
431
|
+
processedAt: event.occurredAt
|
|
432
|
+
};
|
|
433
|
+
}
|
|
321
434
|
var MemoryEventBus = class {
|
|
322
435
|
logger = new Logger2(MemoryEventBus.name);
|
|
323
436
|
/** All events published since construction (or last clear). */
|
|
@@ -353,6 +466,53 @@ var MemoryEventBus = class {
|
|
|
353
466
|
set.delete(h);
|
|
354
467
|
};
|
|
355
468
|
}
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// IEventReadPort (OBS-LIST-1)
|
|
471
|
+
// ============================================================================
|
|
472
|
+
async listEvents(query = {}) {
|
|
473
|
+
const limit = clampEventLimit(query.limit);
|
|
474
|
+
const keyset = query.cursor ? decodeEventCursor(query.cursor) : null;
|
|
475
|
+
const str = (e, key) => {
|
|
476
|
+
const v = e.metadata?.[key];
|
|
477
|
+
return typeof v === "string" ? v : null;
|
|
478
|
+
};
|
|
479
|
+
const matched = this.publishedEvents.filter((e) => {
|
|
480
|
+
if (query.poolId && str(e, "pool") !== query.poolId) return false;
|
|
481
|
+
if (query.direction && str(e, "direction") !== query.direction)
|
|
482
|
+
return false;
|
|
483
|
+
if (query.rootRunId && str(e, "rootRunId") !== query.rootRunId)
|
|
484
|
+
return false;
|
|
485
|
+
if (query.since && e.occurredAt.getTime() < query.since.getTime())
|
|
486
|
+
return false;
|
|
487
|
+
if (query.tenantId !== void 0) {
|
|
488
|
+
const t = str(e, "tenantId");
|
|
489
|
+
if (query.tenantId === null) {
|
|
490
|
+
if (t !== null) return false;
|
|
491
|
+
} else if (t !== query.tenantId) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
});
|
|
497
|
+
matched.sort((a, b) => {
|
|
498
|
+
const dt = b.occurredAt.getTime() - a.occurredAt.getTime();
|
|
499
|
+
if (dt !== 0) return dt;
|
|
500
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
501
|
+
});
|
|
502
|
+
const seeked = keyset ? matched.filter((e) => {
|
|
503
|
+
const ct = e.occurredAt.getTime();
|
|
504
|
+
const kt = keyset.occurredAt.getTime();
|
|
505
|
+
if (ct < kt) return true;
|
|
506
|
+
if (ct > kt) return false;
|
|
507
|
+
return e.id < keyset.id;
|
|
508
|
+
}) : matched;
|
|
509
|
+
const hasMore = seeked.length > limit;
|
|
510
|
+
const page = hasMore ? seeked.slice(0, limit) : seeked;
|
|
511
|
+
const items = page.map(toEventSummary2);
|
|
512
|
+
const last = page[page.length - 1];
|
|
513
|
+
const nextCursor = hasMore && last ? encodeEventCursor({ occurredAt: last.occurredAt, id: last.id }) : null;
|
|
514
|
+
return { items, nextCursor };
|
|
515
|
+
}
|
|
356
516
|
/** Remove all published events and subscriptions. Useful in beforeEach. */
|
|
357
517
|
clear() {
|
|
358
518
|
this.publishedEvents.length = 0;
|
|
@@ -930,10 +1090,20 @@ var EventsModule = class {
|
|
|
930
1090
|
REDIS_URL
|
|
931
1091
|
]
|
|
932
1092
|
},
|
|
1093
|
+
{
|
|
1094
|
+
// Read port (OBS-LIST-1). Drizzle + memory backends implement
|
|
1095
|
+
// IEventReadPort on the EVENT_BUS instance; the redis backend
|
|
1096
|
+
// retains no history, so EVENT_READ_PORT resolves to `null` and
|
|
1097
|
+
// optional consumers (the observability combiner) degrade to
|
|
1098
|
+
// empty results.
|
|
1099
|
+
provide: EVENT_READ_PORT,
|
|
1100
|
+
useFactory: (options, bus) => options.backend === "redis" ? null : bus,
|
|
1101
|
+
inject: [EVENTS_MODULE_OPTIONS, EVENT_BUS]
|
|
1102
|
+
},
|
|
933
1103
|
TypedEventBus,
|
|
934
1104
|
{ provide: TYPED_EVENT_BUS, useExisting: TypedEventBus }
|
|
935
1105
|
],
|
|
936
|
-
exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
|
|
1106
|
+
exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
|
|
937
1107
|
};
|
|
938
1108
|
}
|
|
939
1109
|
static forRoot(options = { backend: "drizzle" }) {
|
|
@@ -961,9 +1131,13 @@ var EventsModule = class {
|
|
|
961
1131
|
providers: [
|
|
962
1132
|
{ provide: EVENTS_MODULE_OPTIONS, useValue: options },
|
|
963
1133
|
provider,
|
|
1134
|
+
// Read port (OBS-LIST-1): drizzle + memory backends implement
|
|
1135
|
+
// IEventReadPort on the same instance as EVENT_BUS. The redis
|
|
1136
|
+
// backend retains no history and does not provide this token.
|
|
1137
|
+
{ provide: EVENT_READ_PORT, useExisting: EVENT_BUS },
|
|
964
1138
|
...buildTypedBusProviders(multiTenant)
|
|
965
1139
|
],
|
|
966
|
-
exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
|
|
1140
|
+
exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
|
|
967
1141
|
};
|
|
968
1142
|
}
|
|
969
1143
|
};
|