@pattern-stack/codegen 0.8.0 → 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 +70 -0
- package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +1 -0
- package/dist/runtime/subsystems/auth/index.d.ts +2 -0
- package/dist/runtime/subsystems/auth/index.js +55 -0
- package/dist/runtime/subsystems/auth/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/middleware/requester-context.d.ts +81 -0
- package/dist/runtime/subsystems/auth/middleware/requester-context.js +60 -0
- package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -0
- package/dist/runtime/subsystems/auth/protocols/user-context.d.ts +18 -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 +2 -0
- package/dist/runtime/subsystems/index.js +1198 -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 +43 -7
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/auth/index.ts +8 -0
- package/runtime/subsystems/auth/middleware/requester-context.ts +141 -0
- package/runtime/subsystems/auth/protocols/user-context.ts +17 -0
- 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
package/package.json
CHANGED
|
@@ -112,5 +112,13 @@ export {
|
|
|
112
112
|
// Controller
|
|
113
113
|
export { AuthController } from './controllers/auth.controller';
|
|
114
114
|
|
|
115
|
+
// Middleware — RequesterContext boundary (bridges auth → ambient tenant scope)
|
|
116
|
+
export {
|
|
117
|
+
installRequesterContext,
|
|
118
|
+
makeRequesterContextMiddleware,
|
|
119
|
+
resolveRequesterContext,
|
|
120
|
+
type RequesterContextOptions,
|
|
121
|
+
} from './middleware/requester-context';
|
|
122
|
+
|
|
115
123
|
// Module
|
|
116
124
|
export { AuthModule, type AuthModuleOptions } from './auth.module';
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequesterContext boundary install — bridges authentication to ambient
|
|
3
|
+
* tenant scoping.
|
|
4
|
+
*
|
|
5
|
+
* This is the missing link that makes `BaseRepository`'s ambient scoping
|
|
6
|
+
* (see `base-classes/tenant-context.ts`) actually engage on HTTP requests:
|
|
7
|
+
* it reads the requester off each request (via the consumer-bound
|
|
8
|
+
* `IUserContext`) and runs the rest of the request inside `withRequester(...)`,
|
|
9
|
+
* so every downstream repository read/write is automatically scoped — no
|
|
10
|
+
* threaded `userId`.
|
|
11
|
+
*
|
|
12
|
+
* ## Wiring (one line in your bootstrap)
|
|
13
|
+
*
|
|
14
|
+
* In `main.ts`, after `NestFactory.create`:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { installRequesterContext } from './shared/subsystems/auth/middleware/requester-context';
|
|
18
|
+
* const app = await NestFactory.create(AppModule);
|
|
19
|
+
* installRequesterContext(app); // no-op + warn if AUTH_USER_CONTEXT is unbound
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* `installRequesterContext` resolves `AUTH_USER_CONTEXT` from the root DI
|
|
23
|
+
* container (so it sees the binding the consumer provides in AppModule) and
|
|
24
|
+
* registers a global Express middleware. Pairs with Swagger's `@ApiBearerAuth`
|
|
25
|
+
* "Authorize" button: paste a token there and every request it sends now flows
|
|
26
|
+
* through this boundary into a scoped repository call.
|
|
27
|
+
*
|
|
28
|
+
* ## Trust + failure model
|
|
29
|
+
*
|
|
30
|
+
* - The middleware TRUSTS whatever `IUserContext` returns — authentication and
|
|
31
|
+
* authorization (validating the token, deciding which scope a requester may
|
|
32
|
+
* claim) are the `IUserContext` implementation's job, exactly as for a
|
|
33
|
+
* hand-threaded `userId`.
|
|
34
|
+
* - When the requester cannot be resolved (no/invalid credentials — e.g. a
|
|
35
|
+
* public route, or the OAuth callback itself), the request proceeds WITHOUT
|
|
36
|
+
* an ambient context (`onUnresolved: 'unscoped'`, the default). A
|
|
37
|
+
* `userTracking` repo in lenient mode then runs unscoped; in strict mode it
|
|
38
|
+
* throws downstream — which is correct: unauthenticated callers must not
|
|
39
|
+
* reach scoped data. Set `onUnresolved: 'reject'` to fail the request at the
|
|
40
|
+
* boundary instead.
|
|
41
|
+
*/
|
|
42
|
+
import type { INestApplication } from '@nestjs/common';
|
|
43
|
+
import {
|
|
44
|
+
withRequester,
|
|
45
|
+
type RequesterContext,
|
|
46
|
+
} from '../../../base-classes/tenant-context';
|
|
47
|
+
import { AUTH_USER_CONTEXT } from '../auth.tokens';
|
|
48
|
+
import type { IUserContext } from '../protocols/user-context';
|
|
49
|
+
|
|
50
|
+
/** Minimal Express-style middleware signature (avoids an `express` dep). */
|
|
51
|
+
type NextFn = (err?: unknown) => void;
|
|
52
|
+
type RequestHandler = (req: unknown, res: unknown, next: NextFn) => void;
|
|
53
|
+
|
|
54
|
+
export interface RequesterContextOptions {
|
|
55
|
+
/**
|
|
56
|
+
* What to do when `IUserContext` cannot resolve a requester (throws, or
|
|
57
|
+
* returns no `userId`).
|
|
58
|
+
* - `'unscoped'` (default): proceed without a context — public routes work;
|
|
59
|
+
* scoped repos run unscoped (lenient) or throw downstream (strict).
|
|
60
|
+
* - `'reject'`: fail the request at the boundary (`next(error)`).
|
|
61
|
+
*/
|
|
62
|
+
onUnresolved?: 'unscoped' | 'reject';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the ambient context for a request: prefer the richer
|
|
67
|
+
* `resolveRequester` (org/superuser), else derive plain `'user'` scope from
|
|
68
|
+
* `getCurrentUserId`. Returns `undefined` when no requester can be determined.
|
|
69
|
+
*/
|
|
70
|
+
export async function resolveRequesterContext(
|
|
71
|
+
userContext: IUserContext,
|
|
72
|
+
req: unknown,
|
|
73
|
+
): Promise<RequesterContext | undefined> {
|
|
74
|
+
if (typeof userContext.resolveRequester === 'function') {
|
|
75
|
+
const ctx = await userContext.resolveRequester(req);
|
|
76
|
+
return ctx?.userId ? ctx : undefined;
|
|
77
|
+
}
|
|
78
|
+
const userId = await userContext.getCurrentUserId(req);
|
|
79
|
+
return userId ? { userId, organizationId: null } : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the global middleware. Runs the remainder of the request inside
|
|
84
|
+
* `withRequester(...)` so the ambient context propagates through every `await`
|
|
85
|
+
* to downstream repositories.
|
|
86
|
+
*/
|
|
87
|
+
export function makeRequesterContextMiddleware(
|
|
88
|
+
userContext: IUserContext,
|
|
89
|
+
options: RequesterContextOptions = {},
|
|
90
|
+
): RequestHandler {
|
|
91
|
+
const onUnresolved = options.onUnresolved ?? 'unscoped';
|
|
92
|
+
return (req, _res, next) => {
|
|
93
|
+
resolveRequesterContext(userContext, req).then(
|
|
94
|
+
(ctx) => {
|
|
95
|
+
if (!ctx) {
|
|
96
|
+
next();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// als.run executes its callback synchronously; Express dispatches the
|
|
100
|
+
// rest of the pipeline inside next(), so all downstream handlers (and
|
|
101
|
+
// their awaits) inherit this context.
|
|
102
|
+
withRequester(ctx, async () => {
|
|
103
|
+
next();
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
(err) => {
|
|
107
|
+
if (onUnresolved === 'reject') {
|
|
108
|
+
next(err);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
next();
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Register the requester-context boundary on a Nest app. Resolves
|
|
119
|
+
* `AUTH_USER_CONTEXT` from the root container (so it sees the consumer's
|
|
120
|
+
* AppModule binding) and installs the global middleware. No-ops with a warning
|
|
121
|
+
* when `AUTH_USER_CONTEXT` is not bound, so calling it unconditionally in
|
|
122
|
+
* bootstrap is safe.
|
|
123
|
+
*/
|
|
124
|
+
export function installRequesterContext(
|
|
125
|
+
app: INestApplication,
|
|
126
|
+
options: RequesterContextOptions = {},
|
|
127
|
+
): void {
|
|
128
|
+
const userContext = app.get<IUserContext>(AUTH_USER_CONTEXT, {
|
|
129
|
+
strict: false,
|
|
130
|
+
});
|
|
131
|
+
if (!userContext) {
|
|
132
|
+
// eslint-disable-next-line no-console
|
|
133
|
+
console.warn(
|
|
134
|
+
'[auth] installRequesterContext: AUTH_USER_CONTEXT is not bound — ' +
|
|
135
|
+
'request scoping NOT installed. Provide an IUserContext under ' +
|
|
136
|
+
'AUTH_USER_CONTEXT in your AppModule to enable ambient tenant scoping.',
|
|
137
|
+
);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
app.use(makeRequesterContextMiddleware(userContext, options));
|
|
141
|
+
}
|
|
@@ -17,6 +17,23 @@
|
|
|
17
17
|
* dependency on `express` / `fastify` / NestJS request types. The concrete
|
|
18
18
|
* adapter narrows it (e.g. via a `Request` import).
|
|
19
19
|
*/
|
|
20
|
+
import type { RequesterContext } from '../../../base-classes/tenant-context';
|
|
21
|
+
|
|
20
22
|
export interface IUserContext {
|
|
21
23
|
getCurrentUserId(req: unknown): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Optional richer resolution of the full ambient requester context — the
|
|
26
|
+
* org/superuser dimensions on top of `userId`. When implemented, the
|
|
27
|
+
* `RequesterContextMiddleware` (see `../middleware/requester-context`) uses
|
|
28
|
+
* it verbatim to scope reads/writes; when omitted, the middleware falls back
|
|
29
|
+
* to `{ userId: await getCurrentUserId(req), organizationId: null }` (plain
|
|
30
|
+
* `'user'` scope).
|
|
31
|
+
*
|
|
32
|
+
* Implement this when the app supports org-shared (`'org'`) or admin
|
|
33
|
+
* (`'superuser'`) data visibility — resolve `organizationId` + the
|
|
34
|
+
* `orgUserIds` member list here, at the trust boundary, so repositories stay
|
|
35
|
+
* single-table. AUTHORIZATION (which scope a requester may claim) is the
|
|
36
|
+
* implementation's responsibility; the repo trusts what this returns.
|
|
37
|
+
*/
|
|
38
|
+
resolveRequester?(req: unknown): Promise<RequesterContext>;
|
|
22
39
|
}
|
|
@@ -149,6 +149,11 @@ export class BridgeModule implements OnModuleInit {
|
|
|
149
149
|
|
|
150
150
|
async onModuleInit(): Promise<void> {
|
|
151
151
|
if (!this.workerOpts) return;
|
|
152
|
+
// BULLMQ-1 Phase 1 — `allPools: true` activates every pool (reserved
|
|
153
|
+
// `events_*` included), so the reserved-pool guarantee holds by
|
|
154
|
+
// construction. Short-circuit pass without inspecting the (typically
|
|
155
|
+
// omitted) explicit `pools` list.
|
|
156
|
+
if (this.workerOpts.allPools) return;
|
|
152
157
|
const activePools = this.workerOpts.pools ?? [];
|
|
153
158
|
const missing = BRIDGE_RESERVED_POOLS.filter(
|
|
154
159
|
(p) => !activePools.includes(p),
|
|
@@ -29,10 +29,20 @@
|
|
|
29
29
|
* via EventsModule.forRoot({ backend: '...' }) without touching use cases.
|
|
30
30
|
*/
|
|
31
31
|
import { Injectable, OnModuleDestroy, OnModuleInit, Inject, Logger, Optional } from '@nestjs/common';
|
|
32
|
-
import { eq, and, inArray, asc, type SQL } from 'drizzle-orm';
|
|
32
|
+
import { eq, and, inArray, asc, desc, gte, lt, or, sql, type SQL } from 'drizzle-orm';
|
|
33
33
|
import type { DomainEvent, DrizzleTransaction, IEventBus } from './event-bus.protocol';
|
|
34
|
+
import type {
|
|
35
|
+
EventPage,
|
|
36
|
+
IEventReadPort,
|
|
37
|
+
ListEventsQuery,
|
|
38
|
+
} from './event-read.protocol';
|
|
39
|
+
import {
|
|
40
|
+
clampEventLimit,
|
|
41
|
+
decodeEventCursor,
|
|
42
|
+
encodeEventCursor,
|
|
43
|
+
} from './event-keyset-cursor';
|
|
34
44
|
import type { DrizzleClient } from '../../types/drizzle';
|
|
35
|
-
import { domainEvents } from './domain-events.schema';
|
|
45
|
+
import { domainEvents, type DomainEventRecord } from './domain-events.schema';
|
|
36
46
|
import { DRIZZLE } from '../../constants/tokens';
|
|
37
47
|
import { EVENTS_MODULE_OPTIONS } from './events.tokens';
|
|
38
48
|
import type { EventsModuleOptions } from './events.module';
|
|
@@ -76,8 +86,43 @@ function toInsertValues(event: DomainEvent) {
|
|
|
76
86
|
};
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Project a raw `domain_events` row into the narrow `EventSummary` shape.
|
|
91
|
+
* Shared with the memory backend via this helper kept module-local to each
|
|
92
|
+
* backend (the events subsystem has no cross-backend projection file yet;
|
|
93
|
+
* the two are byte-identical and small).
|
|
94
|
+
*/
|
|
95
|
+
function toEventSummary(r: DomainEventRecord) {
|
|
96
|
+
const metadata = (r.metadata ?? undefined) as
|
|
97
|
+
| Record<string, unknown>
|
|
98
|
+
| undefined;
|
|
99
|
+
const rootRunId = metadata?.['rootRunId'];
|
|
100
|
+
return {
|
|
101
|
+
id: r.id,
|
|
102
|
+
type: r.type,
|
|
103
|
+
aggregateId: r.aggregateId,
|
|
104
|
+
aggregateType: r.aggregateType,
|
|
105
|
+
status: r.status,
|
|
106
|
+
pool: r.pool,
|
|
107
|
+
direction: r.direction,
|
|
108
|
+
tier: r.tier,
|
|
109
|
+
rootRunId: typeof rootRunId === 'string' ? rootRunId : null,
|
|
110
|
+
tenantId: r.tenantId,
|
|
111
|
+
occurredAt:
|
|
112
|
+
r.occurredAt instanceof Date
|
|
113
|
+
? r.occurredAt
|
|
114
|
+
: new Date(r.occurredAt as unknown as string),
|
|
115
|
+
processedAt:
|
|
116
|
+
r.processedAt == null
|
|
117
|
+
? null
|
|
118
|
+
: r.processedAt instanceof Date
|
|
119
|
+
? r.processedAt
|
|
120
|
+
: new Date(r.processedAt as unknown as string),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
79
124
|
@Injectable()
|
|
80
|
-
export class DrizzleEventBus implements IEventBus, OnModuleInit, OnModuleDestroy {
|
|
125
|
+
export class DrizzleEventBus implements IEventBus, IEventReadPort, OnModuleInit, OnModuleDestroy {
|
|
81
126
|
private readonly logger = new Logger(DrizzleEventBus.name);
|
|
82
127
|
private polling = false;
|
|
83
128
|
private pollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -178,6 +223,67 @@ export class DrizzleEventBus implements IEventBus, OnModuleInit, OnModuleDestroy
|
|
|
178
223
|
};
|
|
179
224
|
}
|
|
180
225
|
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// IEventReadPort (OBS-LIST-1)
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
async listEvents(query: ListEventsQuery = {}): Promise<EventPage> {
|
|
231
|
+
const limit = clampEventLimit(query.limit);
|
|
232
|
+
const conditions: SQL<unknown>[] = [];
|
|
233
|
+
|
|
234
|
+
if (query.poolId) conditions.push(eq(domainEvents.pool, query.poolId));
|
|
235
|
+
if (query.direction)
|
|
236
|
+
conditions.push(eq(domainEvents.direction, query.direction));
|
|
237
|
+
if (query.since) conditions.push(gte(domainEvents.occurredAt, query.since));
|
|
238
|
+
if (query.rootRunId) {
|
|
239
|
+
// Filter on the JSON correlation id: metadata->>'rootRunId'.
|
|
240
|
+
conditions.push(
|
|
241
|
+
sql`${domainEvents.metadata}->>'rootRunId' = ${query.rootRunId}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (query.tenantId !== undefined) {
|
|
245
|
+
conditions.push(
|
|
246
|
+
query.tenantId === null
|
|
247
|
+
? (sql`${domainEvents.tenantId} is null` as SQL<unknown>)
|
|
248
|
+
: eq(domainEvents.tenantId, query.tenantId),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Keyset seek: WHERE (occurred_at, id) < (cursorOccurredAt, cursorId).
|
|
253
|
+
if (query.cursor) {
|
|
254
|
+
const keyset = decodeEventCursor(query.cursor);
|
|
255
|
+
if (keyset) {
|
|
256
|
+
conditions.push(
|
|
257
|
+
or(
|
|
258
|
+
lt(domainEvents.occurredAt, keyset.occurredAt),
|
|
259
|
+
and(
|
|
260
|
+
eq(domainEvents.occurredAt, keyset.occurredAt),
|
|
261
|
+
lt(domainEvents.id, keyset.id),
|
|
262
|
+
),
|
|
263
|
+
)!,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const rows = (await this.db
|
|
269
|
+
.select()
|
|
270
|
+
.from(domainEvents)
|
|
271
|
+
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
272
|
+
.orderBy(desc(domainEvents.occurredAt), desc(domainEvents.id))
|
|
273
|
+
.limit(limit + 1)) as DomainEventRecord[];
|
|
274
|
+
|
|
275
|
+
const hasMore = rows.length > limit;
|
|
276
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
277
|
+
const items = page.map(toEventSummary);
|
|
278
|
+
const last = page[page.length - 1];
|
|
279
|
+
const nextCursor =
|
|
280
|
+
hasMore && last
|
|
281
|
+
? encodeEventCursor({ occurredAt: last.occurredAt, id: last.id })
|
|
282
|
+
: null;
|
|
283
|
+
|
|
284
|
+
return { items, nextCursor };
|
|
285
|
+
}
|
|
286
|
+
|
|
181
287
|
// ============================================================================
|
|
182
288
|
// Polling
|
|
183
289
|
// ============================================================================
|
|
@@ -21,11 +21,52 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
|
23
23
|
import type { DomainEvent, IEventBus } from './event-bus.protocol';
|
|
24
|
+
import type {
|
|
25
|
+
EventPage,
|
|
26
|
+
EventSummary,
|
|
27
|
+
IEventReadPort,
|
|
28
|
+
ListEventsQuery,
|
|
29
|
+
} from './event-read.protocol';
|
|
30
|
+
import {
|
|
31
|
+
clampEventLimit,
|
|
32
|
+
decodeEventCursor,
|
|
33
|
+
encodeEventCursor,
|
|
34
|
+
} from './event-keyset-cursor';
|
|
24
35
|
import { EVENTS_MODULE_OPTIONS } from './events.tokens';
|
|
25
36
|
import type { EventsModuleOptions } from './events.module';
|
|
26
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Project an in-memory `DomainEvent` into the narrow `EventSummary` shape.
|
|
40
|
+
* The memory backend has no first-class columns, so `pool` / `direction` /
|
|
41
|
+
* `tier` / `tenantId` / `rootRunId` are read from `metadata` (mirroring how
|
|
42
|
+
* the Drizzle backend stamps them onto columns at publish time). `status`
|
|
43
|
+
* is reported as `'processed'` — the memory bus dispatches synchronously,
|
|
44
|
+
* so once an event is in `publishedEvents` it has been handled.
|
|
45
|
+
*/
|
|
46
|
+
function toEventSummary(event: DomainEvent): EventSummary {
|
|
47
|
+
const metadata = event.metadata;
|
|
48
|
+
const str = (key: string): string | null => {
|
|
49
|
+
const v = metadata?.[key];
|
|
50
|
+
return typeof v === 'string' ? v : null;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
id: event.id,
|
|
54
|
+
type: event.type,
|
|
55
|
+
aggregateId: event.aggregateId,
|
|
56
|
+
aggregateType: event.aggregateType,
|
|
57
|
+
status: 'processed',
|
|
58
|
+
pool: str('pool'),
|
|
59
|
+
direction: str('direction'),
|
|
60
|
+
tier: str('tier') ?? 'domain',
|
|
61
|
+
rootRunId: str('rootRunId'),
|
|
62
|
+
tenantId: str('tenantId'),
|
|
63
|
+
occurredAt: event.occurredAt,
|
|
64
|
+
processedAt: event.occurredAt,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
27
68
|
@Injectable()
|
|
28
|
-
export class MemoryEventBus implements IEventBus {
|
|
69
|
+
export class MemoryEventBus implements IEventBus, IEventReadPort {
|
|
29
70
|
private readonly logger = new Logger(MemoryEventBus.name);
|
|
30
71
|
|
|
31
72
|
/** All events published since construction (or last clear). */
|
|
@@ -86,6 +127,67 @@ export class MemoryEventBus implements IEventBus {
|
|
|
86
127
|
};
|
|
87
128
|
}
|
|
88
129
|
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// IEventReadPort (OBS-LIST-1)
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
async listEvents(query: ListEventsQuery = {}): Promise<EventPage> {
|
|
135
|
+
const limit = clampEventLimit(query.limit);
|
|
136
|
+
const keyset = query.cursor ? decodeEventCursor(query.cursor) : null;
|
|
137
|
+
|
|
138
|
+
const str = (e: DomainEvent, key: string): string | null => {
|
|
139
|
+
const v = e.metadata?.[key];
|
|
140
|
+
return typeof v === 'string' ? v : null;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const matched = this.publishedEvents.filter((e) => {
|
|
144
|
+
if (query.poolId && str(e, 'pool') !== query.poolId) return false;
|
|
145
|
+
if (query.direction && str(e, 'direction') !== query.direction)
|
|
146
|
+
return false;
|
|
147
|
+
if (query.rootRunId && str(e, 'rootRunId') !== query.rootRunId)
|
|
148
|
+
return false;
|
|
149
|
+
if (query.since && e.occurredAt.getTime() < query.since.getTime())
|
|
150
|
+
return false;
|
|
151
|
+
if (query.tenantId !== undefined) {
|
|
152
|
+
const t = str(e, 'tenantId');
|
|
153
|
+
if (query.tenantId === null) {
|
|
154
|
+
if (t !== null) return false;
|
|
155
|
+
} else if (t !== query.tenantId) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Order occurred_at DESC, id DESC to match the Drizzle backend keyset.
|
|
163
|
+
matched.sort((a, b) => {
|
|
164
|
+
const dt = b.occurredAt.getTime() - a.occurredAt.getTime();
|
|
165
|
+
if (dt !== 0) return dt;
|
|
166
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const seeked = keyset
|
|
170
|
+
? matched.filter((e) => {
|
|
171
|
+
const ct = e.occurredAt.getTime();
|
|
172
|
+
const kt = keyset.occurredAt.getTime();
|
|
173
|
+
if (ct < kt) return true;
|
|
174
|
+
if (ct > kt) return false;
|
|
175
|
+
return e.id < keyset.id;
|
|
176
|
+
})
|
|
177
|
+
: matched;
|
|
178
|
+
|
|
179
|
+
const hasMore = seeked.length > limit;
|
|
180
|
+
const page = hasMore ? seeked.slice(0, limit) : seeked;
|
|
181
|
+
const items = page.map(toEventSummary);
|
|
182
|
+
const last = page[page.length - 1];
|
|
183
|
+
const nextCursor =
|
|
184
|
+
hasMore && last
|
|
185
|
+
? encodeEventCursor({ occurredAt: last.occurredAt, id: last.id })
|
|
186
|
+
: null;
|
|
187
|
+
|
|
188
|
+
return { items, nextCursor };
|
|
189
|
+
}
|
|
190
|
+
|
|
89
191
|
/** Remove all published events and subscriptions. Useful in beforeEach. */
|
|
90
192
|
clear(): void {
|
|
91
193
|
this.publishedEvents.length = 0;
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
|
|
15
|
+
export interface EventKeyset {
|
|
16
|
+
occurredAt: Date;
|
|
17
|
+
id: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Default page size when `limit` is omitted. */
|
|
21
|
+
export const DEFAULT_EVENT_LIST_LIMIT = 50;
|
|
22
|
+
/** Hard upper bound on page size. */
|
|
23
|
+
export const MAX_EVENT_LIST_LIMIT = 200;
|
|
24
|
+
|
|
25
|
+
/** Clamp a caller-supplied `limit` into `[1, MAX_EVENT_LIST_LIMIT]`. */
|
|
26
|
+
export function clampEventLimit(limit: number | undefined): number {
|
|
27
|
+
if (typeof limit !== 'number' || !Number.isFinite(limit)) {
|
|
28
|
+
return DEFAULT_EVENT_LIST_LIMIT;
|
|
29
|
+
}
|
|
30
|
+
const floored = Math.floor(limit);
|
|
31
|
+
if (floored < 1) return 1;
|
|
32
|
+
if (floored > MAX_EVENT_LIST_LIMIT) return MAX_EVENT_LIST_LIMIT;
|
|
33
|
+
return floored;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function encodeEventCursor(keyset: EventKeyset): string {
|
|
37
|
+
const tuple = [keyset.occurredAt.toISOString(), keyset.id];
|
|
38
|
+
return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Decode an opaque cursor back into its `(occurredAt, id)` keyset. Returns
|
|
43
|
+
* `null` for malformed input so user-supplied garbage is treated as "start
|
|
44
|
+
* from the beginning" rather than throwing.
|
|
45
|
+
*/
|
|
46
|
+
export function decodeEventCursor(cursor: string): EventKeyset | null {
|
|
47
|
+
try {
|
|
48
|
+
const json = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
49
|
+
const parsed = JSON.parse(json) as unknown;
|
|
50
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) return null;
|
|
51
|
+
const [iso, id] = parsed;
|
|
52
|
+
if (typeof iso !== 'string' || typeof id !== 'string') return null;
|
|
53
|
+
const occurredAt = new Date(iso);
|
|
54
|
+
if (Number.isNaN(occurredAt.getTime())) return null;
|
|
55
|
+
return { occurredAt, id };
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IEventReadPort — read-side port over `domain_events` (OBS-LIST-1).
|
|
3
|
+
*
|
|
4
|
+
* The publish/subscribe `IEventBus` (EVENT_BUS) is a *write + dispatch*
|
|
5
|
+
* port; it deliberately does not expose tabular reads beyond `findById`.
|
|
6
|
+
* The observability combiner needs a paginated, filterable list of
|
|
7
|
+
* `domain_events` for its events viewer, so we add a dedicated read port
|
|
8
|
+
* rather than overloading `IEventBus`.
|
|
9
|
+
*
|
|
10
|
+
* Keeping reads on a separate port means:
|
|
11
|
+
* - the combiner can compose it `@Optional()` independently of EVENT_BUS;
|
|
12
|
+
* - the Redis backend (which retains no history) simply does not provide
|
|
13
|
+
* it — there is no "list" semantics to fake;
|
|
14
|
+
* - the write/dispatch surface stays minimal.
|
|
15
|
+
*
|
|
16
|
+
* Both `DrizzleEventBus` and `MemoryEventBus` implement this port (they
|
|
17
|
+
* already hold the rows / in-memory log); `EventsModule.forRoot` binds the
|
|
18
|
+
* `EVENT_READ_PORT` token to the same instance for drizzle/memory backends.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { DomainEvent } from './event-bus.protocol';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Filter + keyset-pagination input for `IEventReadPort.listEvents`.
|
|
25
|
+
*
|
|
26
|
+
* Ordered `occurred_at DESC, id DESC`. `rootRunId` filters on the JSON
|
|
27
|
+
* `metadata->>'rootRunId'` — the correlation id stamped by the jobs/bridge
|
|
28
|
+
* machinery so an event can be traced back to the run tree that emitted it.
|
|
29
|
+
*/
|
|
30
|
+
export interface ListEventsQuery {
|
|
31
|
+
/** Filter on `metadata->>'rootRunId'` (correlation id). */
|
|
32
|
+
rootRunId?: string;
|
|
33
|
+
/** Filter on the first-class `pool` column. */
|
|
34
|
+
poolId?: string;
|
|
35
|
+
/** Filter on the first-class `direction` column (inbound|change|outbound). */
|
|
36
|
+
direction?: string;
|
|
37
|
+
/** Lower bound on `occurred_at` (inclusive). */
|
|
38
|
+
since?: Date;
|
|
39
|
+
/** Opaque keyset cursor from a previous page's `nextCursor`. */
|
|
40
|
+
cursor?: string;
|
|
41
|
+
/** Page size. Backend clamps to a sane default + max. */
|
|
42
|
+
limit?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Multi-tenancy filter on the first-class `tenant_id` column. Only
|
|
45
|
+
* meaningful when the consumer publishes tenant-scoped events
|
|
46
|
+
* (`events.multi_tenant: true`); otherwise leave undefined.
|
|
47
|
+
* - `string` — filter `tenant_id = :tenantId`.
|
|
48
|
+
* - `null` — filter `tenant_id IS NULL`.
|
|
49
|
+
* - `undefined` — no tenant filter.
|
|
50
|
+
*/
|
|
51
|
+
tenantId?: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Summary row for the events list. A narrow projection over `domain_events`
|
|
56
|
+
* carrying what the viewer + correlation timeline need. `rootRunId` is
|
|
57
|
+
* surfaced (lifted out of `metadata`) so the timeline can stitch without a
|
|
58
|
+
* second metadata dig.
|
|
59
|
+
*/
|
|
60
|
+
export interface EventSummary {
|
|
61
|
+
id: string;
|
|
62
|
+
type: string;
|
|
63
|
+
aggregateId: string;
|
|
64
|
+
aggregateType: string;
|
|
65
|
+
status: string;
|
|
66
|
+
pool: string | null;
|
|
67
|
+
direction: string | null;
|
|
68
|
+
tier: string;
|
|
69
|
+
rootRunId: string | null;
|
|
70
|
+
tenantId: string | null;
|
|
71
|
+
occurredAt: Date;
|
|
72
|
+
processedAt: Date | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One page of `listEvents` results. `nextCursor` is `null` when there are
|
|
77
|
+
* no more rows.
|
|
78
|
+
*/
|
|
79
|
+
export interface EventPage {
|
|
80
|
+
items: EventSummary[];
|
|
81
|
+
nextCursor: string | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface IEventReadPort {
|
|
85
|
+
/**
|
|
86
|
+
* Paginated, filterable list of `domain_events` (OBS-LIST-1). Newest
|
|
87
|
+
* first (`occurred_at` desc, `id` desc keyset tie-break). Returns an
|
|
88
|
+
* `EventPage` with an opaque `nextCursor` for keyset pagination.
|
|
89
|
+
*/
|
|
90
|
+
listEvents(query?: ListEventsQuery): Promise<EventPage>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A `DomainEvent` whose metadata may carry a `rootRunId` correlation id. */
|
|
94
|
+
export function rootRunIdOf(event: DomainEvent): string | null {
|
|
95
|
+
const v = event.metadata?.['rootRunId'];
|
|
96
|
+
return typeof v === 'string' ? v : null;
|
|
97
|
+
}
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
import { Module, type DynamicModule, type Provider } from '@nestjs/common';
|
|
48
48
|
import {
|
|
49
49
|
EVENT_BUS,
|
|
50
|
+
EVENT_READ_PORT,
|
|
50
51
|
EVENTS_MODULE_OPTIONS,
|
|
51
52
|
EVENTS_MULTI_TENANT,
|
|
52
53
|
REDIS_URL,
|
|
@@ -180,10 +181,21 @@ export class EventsModule {
|
|
|
180
181
|
REDIS_URL,
|
|
181
182
|
],
|
|
182
183
|
},
|
|
184
|
+
{
|
|
185
|
+
// Read port (OBS-LIST-1). Drizzle + memory backends implement
|
|
186
|
+
// IEventReadPort on the EVENT_BUS instance; the redis backend
|
|
187
|
+
// retains no history, so EVENT_READ_PORT resolves to `null` and
|
|
188
|
+
// optional consumers (the observability combiner) degrade to
|
|
189
|
+
// empty results.
|
|
190
|
+
provide: EVENT_READ_PORT,
|
|
191
|
+
useFactory: (options: EventsModuleOptions, bus: unknown) =>
|
|
192
|
+
options.backend === 'redis' ? null : bus,
|
|
193
|
+
inject: [EVENTS_MODULE_OPTIONS, EVENT_BUS],
|
|
194
|
+
},
|
|
183
195
|
TypedEventBus,
|
|
184
196
|
{ provide: TYPED_EVENT_BUS, useExisting: TypedEventBus },
|
|
185
197
|
],
|
|
186
|
-
exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],
|
|
198
|
+
exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],
|
|
187
199
|
};
|
|
188
200
|
}
|
|
189
201
|
|
|
@@ -222,9 +234,13 @@ export class EventsModule {
|
|
|
222
234
|
providers: [
|
|
223
235
|
{ provide: EVENTS_MODULE_OPTIONS, useValue: options },
|
|
224
236
|
provider,
|
|
237
|
+
// Read port (OBS-LIST-1): drizzle + memory backends implement
|
|
238
|
+
// IEventReadPort on the same instance as EVENT_BUS. The redis
|
|
239
|
+
// backend retains no history and does not provide this token.
|
|
240
|
+
{ provide: EVENT_READ_PORT, useExisting: EVENT_BUS },
|
|
225
241
|
...buildTypedBusProviders(multiTenant),
|
|
226
242
|
],
|
|
227
|
-
exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],
|
|
243
|
+
exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],
|
|
228
244
|
};
|
|
229
245
|
}
|
|
230
246
|
}
|
|
@@ -11,6 +11,22 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export const EVENT_BUS = 'EVENT_BUS' as const;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Injection token for the read-side `IEventReadPort` over `domain_events`
|
|
16
|
+
* (OBS-LIST-1).
|
|
17
|
+
*
|
|
18
|
+
* Bound by `EventsModule.forRoot` to the same backend instance as
|
|
19
|
+
* `EVENT_BUS` for the `drizzle` and `memory` backends (both implement
|
|
20
|
+
* `IEventReadPort`). The `redis` backend retains no history and therefore
|
|
21
|
+
* does NOT provide this token — consumers composing it (e.g. the
|
|
22
|
+
* observability combiner) inject it `@Optional()` and degrade to empty
|
|
23
|
+
* results.
|
|
24
|
+
*
|
|
25
|
+
* String constant (not Symbol) so it matches by value across import
|
|
26
|
+
* boundaries — same convention as `EVENT_BUS`.
|
|
27
|
+
*/
|
|
28
|
+
export const EVENT_READ_PORT = 'EVENT_READ_PORT' as const;
|
|
29
|
+
|
|
14
30
|
/**
|
|
15
31
|
* Injection token for the generated `TypedEventBus` facade.
|
|
16
32
|
*
|