@ddd-ts-kit/events 0.0.0-stage → 0.1.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/README.md +462 -2
- package/dist/asyncapi/async-api-document.d.ts +15 -0
- package/dist/asyncapi/async-api-document.d.ts.map +1 -0
- package/dist/asyncapi/async-api-document.js +2 -0
- package/dist/asyncapi/async-api-document.js.map +1 -0
- package/dist/asyncapi/async-api-info.d.ts +6 -0
- package/dist/asyncapi/async-api-info.d.ts.map +1 -0
- package/dist/asyncapi/async-api-info.js +2 -0
- package/dist/asyncapi/async-api-info.js.map +1 -0
- package/dist/asyncapi/async-api-server.d.ts +7 -0
- package/dist/asyncapi/async-api-server.d.ts.map +1 -0
- package/dist/asyncapi/async-api-server.js +2 -0
- package/dist/asyncapi/async-api-server.js.map +1 -0
- package/dist/asyncapi/check-async-api-drift.d.ts +6 -0
- package/dist/asyncapi/check-async-api-drift.d.ts.map +1 -0
- package/dist/asyncapi/check-async-api-drift.js +6 -0
- package/dist/asyncapi/check-async-api-drift.js.map +1 -0
- package/dist/asyncapi/generate-async-api.d.ts +16 -0
- package/dist/asyncapi/generate-async-api.d.ts.map +1 -0
- package/dist/asyncapi/generate-async-api.js +116 -0
- package/dist/asyncapi/generate-async-api.js.map +1 -0
- package/dist/asyncapi/stringify-async-api.d.ts +4 -0
- package/dist/asyncapi/stringify-async-api.d.ts.map +1 -0
- package/dist/asyncapi/stringify-async-api.js +5 -0
- package/dist/asyncapi/stringify-async-api.js.map +1 -0
- package/dist/asyncapi.d.ts +7 -0
- package/dist/asyncapi.d.ts.map +1 -0
- package/dist/asyncapi.js +7 -0
- package/dist/asyncapi.js.map +1 -0
- package/dist/catalog/generate-event-catalog.d.ts +12 -0
- package/dist/catalog/generate-event-catalog.d.ts.map +1 -0
- package/dist/catalog/generate-event-catalog.js +63 -0
- package/dist/catalog/generate-event-catalog.js.map +1 -0
- package/dist/catalog/payload-field.d.ts +8 -0
- package/dist/catalog/payload-field.d.ts.map +1 -0
- package/dist/catalog/payload-field.js +2 -0
- package/dist/catalog/payload-field.js.map +1 -0
- package/dist/catalog/payload-fields.d.ts +7 -0
- package/dist/catalog/payload-fields.d.ts.map +1 -0
- package/dist/catalog/payload-fields.js +105 -0
- package/dist/catalog/payload-fields.js.map +1 -0
- package/dist/definition/any-integration-event-definition.d.ts +4 -0
- package/dist/definition/any-integration-event-definition.d.ts.map +1 -0
- package/dist/definition/any-integration-event-definition.js +2 -0
- package/dist/definition/any-integration-event-definition.js.map +1 -0
- package/dist/definition/define-integration-event.d.ts +15 -0
- package/dist/definition/define-integration-event.d.ts.map +1 -0
- package/dist/definition/define-integration-event.js +73 -0
- package/dist/definition/define-integration-event.js.map +1 -0
- package/dist/definition/event-meta.d.ts +7 -0
- package/dist/definition/event-meta.d.ts.map +1 -0
- package/dist/definition/event-meta.js +2 -0
- package/dist/definition/event-meta.js.map +1 -0
- package/dist/definition/event-parse-error.d.ts +14 -0
- package/dist/definition/event-parse-error.d.ts.map +1 -0
- package/dist/definition/event-parse-error.js +2 -0
- package/dist/definition/event-parse-error.js.map +1 -0
- package/dist/definition/integration-event-definition.d.ts +20 -0
- package/dist/definition/integration-event-definition.d.ts.map +1 -0
- package/dist/definition/integration-event-definition.js +2 -0
- package/dist/definition/integration-event-definition.js.map +1 -0
- package/dist/definition/invalid-event-definition-error.d.ts +4 -0
- package/dist/definition/invalid-event-definition-error.d.ts.map +1 -0
- package/dist/definition/invalid-event-definition-error.js +7 -0
- package/dist/definition/invalid-event-definition-error.js.map +1 -0
- package/dist/definition/invalid-event-payload-error.d.ts +9 -0
- package/dist/definition/invalid-event-payload-error.d.ts.map +1 -0
- package/dist/definition/invalid-event-payload-error.js +16 -0
- package/dist/definition/invalid-event-payload-error.js.map +1 -0
- package/dist/definition/payload-of.d.ts +3 -0
- package/dist/definition/payload-of.d.ts.map +1 -0
- package/dist/definition/payload-of.js +2 -0
- package/dist/definition/payload-of.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/json-schema/as-schema.d.ts +3 -0
- package/dist/internal/json-schema/as-schema.d.ts.map +1 -0
- package/dist/internal/json-schema/as-schema.js +5 -0
- package/dist/internal/json-schema/as-schema.js.map +1 -0
- package/dist/internal/json-schema/compare-text.d.ts +2 -0
- package/dist/internal/json-schema/compare-text.d.ts.map +1 -0
- package/dist/internal/json-schema/compare-text.js +5 -0
- package/dist/internal/json-schema/compare-text.js.map +1 -0
- package/dist/internal/json-schema/is-record.d.ts +2 -0
- package/dist/internal/json-schema/is-record.d.ts.map +1 -0
- package/dist/internal/json-schema/is-record.js +4 -0
- package/dist/internal/json-schema/is-record.js.map +1 -0
- package/dist/internal/json-schema/local-ref-name.d.ts +3 -0
- package/dist/internal/json-schema/local-ref-name.d.ts.map +1 -0
- package/dist/internal/json-schema/local-ref-name.js +6 -0
- package/dist/internal/json-schema/local-ref-name.js.map +1 -0
- package/dist/internal/json-schema/rewrite-local-refs.d.ts +2 -0
- package/dist/internal/json-schema/rewrite-local-refs.d.ts.map +1 -0
- package/dist/internal/json-schema/rewrite-local-refs.js +19 -0
- package/dist/internal/json-schema/rewrite-local-refs.js.map +1 -0
- package/dist/internal/json-schema/sort-keys.d.ts +2 -0
- package/dist/internal/json-schema/sort-keys.d.ts.map +1 -0
- package/dist/internal/json-schema/sort-keys.js +5 -0
- package/dist/internal/json-schema/sort-keys.js.map +1 -0
- package/dist/internal/json-schema/split-definitions.d.ts +8 -0
- package/dist/internal/json-schema/split-definitions.d.ts.map +1 -0
- package/dist/internal/json-schema/split-definitions.js +13 -0
- package/dist/internal/json-schema/split-definitions.js.map +1 -0
- package/dist/outbox/duplicate-outbox-event-error.d.ts +5 -0
- package/dist/outbox/duplicate-outbox-event-error.d.ts.map +1 -0
- package/dist/outbox/duplicate-outbox-event-error.js +8 -0
- package/dist/outbox/duplicate-outbox-event-error.js.map +1 -0
- package/dist/outbox/in-memory-outbox-store.d.ts +32 -0
- package/dist/outbox/in-memory-outbox-store.d.ts.map +1 -0
- package/dist/outbox/in-memory-outbox-store.js +121 -0
- package/dist/outbox/in-memory-outbox-store.js.map +1 -0
- package/dist/outbox/outbox-publisher.d.ts +14 -0
- package/dist/outbox/outbox-publisher.d.ts.map +1 -0
- package/dist/outbox/outbox-publisher.js +18 -0
- package/dist/outbox/outbox-publisher.js.map +1 -0
- package/dist/outbox/outbox-record.d.ts +14 -0
- package/dist/outbox/outbox-record.d.ts.map +1 -0
- package/dist/outbox/outbox-record.js +2 -0
- package/dist/outbox/outbox-record.js.map +1 -0
- package/dist/outbox/outbox-stats.d.ts +9 -0
- package/dist/outbox/outbox-stats.d.ts.map +1 -0
- package/dist/outbox/outbox-stats.js +2 -0
- package/dist/outbox/outbox-stats.js.map +1 -0
- package/dist/outbox/outbox-status.d.ts +2 -0
- package/dist/outbox/outbox-status.d.ts.map +1 -0
- package/dist/outbox/outbox-status.js +2 -0
- package/dist/outbox/outbox-status.js.map +1 -0
- package/dist/outbox/outbox-store.d.ts +33 -0
- package/dist/outbox/outbox-store.d.ts.map +1 -0
- package/dist/outbox/outbox-store.js +13 -0
- package/dist/outbox/outbox-store.js.map +1 -0
- package/dist/publishing/unknown-event-error.d.ts +7 -0
- package/dist/publishing/unknown-event-error.d.ts.map +1 -0
- package/dist/publishing/unknown-event-error.js +12 -0
- package/dist/publishing/unknown-event-error.js.map +1 -0
- package/dist/publishing/validating-publisher.d.ts +12 -0
- package/dist/publishing/validating-publisher.d.ts.map +1 -0
- package/dist/publishing/validating-publisher.js +33 -0
- package/dist/publishing/validating-publisher.js.map +1 -0
- package/dist/registry/create-event-registry.d.ts +4 -0
- package/dist/registry/create-event-registry.d.ts.map +1 -0
- package/dist/registry/create-event-registry.js +5 -0
- package/dist/registry/create-event-registry.js.map +1 -0
- package/dist/registry/duplicate-event-definition-error.d.ts +4 -0
- package/dist/registry/duplicate-event-definition-error.d.ts.map +1 -0
- package/dist/registry/duplicate-event-definition-error.js +7 -0
- package/dist/registry/duplicate-event-definition-error.js.map +1 -0
- package/dist/registry/event-registry.d.ts +11 -0
- package/dist/registry/event-registry.d.ts.map +1 -0
- package/dist/registry/event-registry.js +33 -0
- package/dist/registry/event-registry.js.map +1 -0
- package/dist/relay/claim-window.d.ts +6 -0
- package/dist/relay/claim-window.d.ts.map +1 -0
- package/dist/relay/claim-window.js +2 -0
- package/dist/relay/claim-window.js.map +1 -0
- package/dist/relay/error-code.d.ts +6 -0
- package/dist/relay/error-code.d.ts.map +1 -0
- package/dist/relay/error-code.js +16 -0
- package/dist/relay/error-code.js.map +1 -0
- package/dist/relay/event-key.d.ts +3 -0
- package/dist/relay/event-key.d.ts.map +1 -0
- package/dist/relay/event-key.js +2 -0
- package/dist/relay/event-key.js.map +1 -0
- package/dist/relay/hold-until.d.ts +3 -0
- package/dist/relay/hold-until.d.ts.map +1 -0
- package/dist/relay/hold-until.js +2 -0
- package/dist/relay/hold-until.js.map +1 -0
- package/dist/relay/in-memory-relay-signal.d.ts +9 -0
- package/dist/relay/in-memory-relay-signal.d.ts.map +1 -0
- package/dist/relay/in-memory-relay-signal.js +13 -0
- package/dist/relay/in-memory-relay-signal.js.map +1 -0
- package/dist/relay/key-order.d.ts +35 -0
- package/dist/relay/key-order.d.ts.map +1 -0
- package/dist/relay/key-order.js +102 -0
- package/dist/relay/key-order.js.map +1 -0
- package/dist/relay/keyed-record.d.ts +6 -0
- package/dist/relay/keyed-record.d.ts.map +1 -0
- package/dist/relay/keyed-record.js +2 -0
- package/dist/relay/keyed-record.js.map +1 -0
- package/dist/relay/outbox-backoff.d.ts +9 -0
- package/dist/relay/outbox-backoff.d.ts.map +1 -0
- package/dist/relay/outbox-backoff.js +2 -0
- package/dist/relay/outbox-backoff.js.map +1 -0
- package/dist/relay/outbox-ordering.d.ts +7 -0
- package/dist/relay/outbox-ordering.d.ts.map +1 -0
- package/dist/relay/outbox-ordering.js +2 -0
- package/dist/relay/outbox-ordering.js.map +1 -0
- package/dist/relay/outbox-relay-failure.d.ts +12 -0
- package/dist/relay/outbox-relay-failure.d.ts.map +1 -0
- package/dist/relay/outbox-relay-failure.js +2 -0
- package/dist/relay/outbox-relay-failure.js.map +1 -0
- package/dist/relay/outbox-relay.d.ts +85 -0
- package/dist/relay/outbox-relay.d.ts.map +1 -0
- package/dist/relay/outbox-relay.js +193 -0
- package/dist/relay/outbox-relay.js.map +1 -0
- package/dist/relay/record-group.d.ts +6 -0
- package/dist/relay/record-group.d.ts.map +1 -0
- package/dist/relay/record-group.js +2 -0
- package/dist/relay/record-group.js.map +1 -0
- package/dist/relay/relay-signal.d.ts +15 -0
- package/dist/relay/relay-signal.d.ts.map +1 -0
- package/dist/relay/relay-signal.js +12 -0
- package/dist/relay/relay-signal.js.map +1 -0
- package/dist/subscription/consume.d.ts +18 -0
- package/dist/subscription/consume.d.ts.map +1 -0
- package/dist/subscription/consume.js +10 -0
- package/dist/subscription/consume.js.map +1 -0
- package/dist/subscription/event-scope-options.d.ts +13 -0
- package/dist/subscription/event-scope-options.d.ts.map +1 -0
- package/dist/subscription/event-scope-options.js +2 -0
- package/dist/subscription/event-scope-options.js.map +1 -0
- package/dist/subscription/event-subscription.d.ts +6 -0
- package/dist/subscription/event-subscription.d.ts.map +1 -0
- package/dist/subscription/event-subscription.js +2 -0
- package/dist/subscription/event-subscription.js.map +1 -0
- package/dist/subscription/on.d.ts +26 -0
- package/dist/subscription/on.d.ts.map +1 -0
- package/dist/subscription/on.js +62 -0
- package/dist/subscription/on.js.map +1 -0
- package/dist/subscription/releasable-processed-event-store.d.ts +14 -0
- package/dist/subscription/releasable-processed-event-store.d.ts.map +1 -0
- package/dist/subscription/releasable-processed-event-store.js +2 -0
- package/dist/subscription/releasable-processed-event-store.js.map +1 -0
- package/dist/subscription/subscribe-all.d.ts +5 -0
- package/dist/subscription/subscribe-all.d.ts.map +1 -0
- package/dist/subscription/subscribe-all.js +12 -0
- package/dist/subscription/subscribe-all.js.map +1 -0
- package/package.json +37 -4
package/README.md
CHANGED
|
@@ -1,3 +1,463 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ddd-ts-kit/events
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Integration events between bounded contexts, independent of any broker: typed declarations,
|
|
4
|
+
a registry, validated publication and consumption, a transactional outbox, and the AsyncAPI
|
|
5
|
+
document and event catalog generated from the declarations.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i @ddd-ts-kit/events @ddd-ts-kit/validation zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Declare
|
|
12
|
+
|
|
13
|
+
One declaration per event and version, in the producer's `published-language/`. It gives the
|
|
14
|
+
payload type, validates what is published and consumed, and feeds the generators.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// contexts/ordering/published-language/order-placed.v1.integration-event.ts
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { zodSchema } from '@ddd-ts-kit/validation/zod';
|
|
20
|
+
import { defineIntegrationEvent, type PayloadOf } from '@ddd-ts-kit/events';
|
|
21
|
+
|
|
22
|
+
export const orderPlaced = defineIntegrationEvent({
|
|
23
|
+
type: 'ordering.order.placed', // <context>.<aggregate>.<fact>, kebab-case segments
|
|
24
|
+
version: 1,
|
|
25
|
+
description: 'An order was placed by a customer.',
|
|
26
|
+
payload: zodSchema(
|
|
27
|
+
z.object({
|
|
28
|
+
orderId: z.string().describe('Identifier of the order.'),
|
|
29
|
+
total: z.object({ amount: z.number().int(), currency: z.enum(['EUR', 'USD']) }),
|
|
30
|
+
lines: z.array(z.object({ sku: z.string(), quantity: z.number().int().positive() })),
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type OrderPlaced = PayloadOf<typeof orderPlaced>;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A breaking change is a new declaration with `version: 2`; mark the old one `deprecated: true`
|
|
39
|
+
and keep publishing both until the consumers have moved.
|
|
40
|
+
|
|
41
|
+
## Publish through the outbox
|
|
42
|
+
|
|
43
|
+
A publisher is a handler of a domain event of its own context: it maps the fact to the public
|
|
44
|
+
payload and publishes to an `IntegrationEventPublisher`, knowing nothing else. With a
|
|
45
|
+
database, that publisher is the `OutboxPublisher`: it only stores. Domain events are dispatched
|
|
46
|
+
at the end of the unit of work of the command, before its commit (the `before-commit` phase,
|
|
47
|
+
the default), so the integration event commits or rolls back with the business write: the
|
|
48
|
+
relay publishes it after the commit, never for a command that rolled back. A publisher is
|
|
49
|
+
never declared `phase: 'after-commit'`: the transaction that would hold its outbox row is over.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// contexts/ordering/application/integration/publishers/order-placed.publisher.ts
|
|
53
|
+
import { IdGenerator, IntegrationEventPublisher } from '@ddd-ts-kit/kernel';
|
|
54
|
+
import { DomainEventHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
55
|
+
|
|
56
|
+
@DomainEventHandler(OrderPlacedEvent, { name: 'ordering.order.placed' }, [IntegrationEventPublisher, IdGenerator])
|
|
57
|
+
export class OrderPlacedPublisher implements DomainEventHandler<OrderPlacedEvent> {
|
|
58
|
+
constructor(
|
|
59
|
+
private readonly publisher: IntegrationEventPublisher,
|
|
60
|
+
private readonly ids: IdGenerator,
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
async handle(event: OrderPlacedEvent): Promise<void> {
|
|
64
|
+
const published = orderPlaced.create(
|
|
65
|
+
{ orderId: event.aggregateId, total: event.total, lines: event.lines },
|
|
66
|
+
{ id: this.ids.next(), occurredAt: event.occurredAt },
|
|
67
|
+
); // throws InvalidEventPayloadError: publishing an invalid event is a bug
|
|
68
|
+
|
|
69
|
+
await this.publisher.publish([published]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The correlation id is not passed by hand: `scopedPublisher` stamps it (see "The execution
|
|
75
|
+
context of a consumed event").
|
|
76
|
+
|
|
77
|
+
The composition root binds the store under its port, `OutboxStore`, and replaces the
|
|
78
|
+
publisher of the kernel by the outbox. Ports are abstract classes and the class is what a
|
|
79
|
+
module binds and resolves:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// platform/messaging.module.ts
|
|
83
|
+
import { defineContextModule } from '@ddd-ts-kit/di';
|
|
84
|
+
import { IntegrationEventPublisher } from '@ddd-ts-kit/kernel';
|
|
85
|
+
import { createEventRegistry, OutboxPublisher, OutboxStore, validatingPublisher } from '@ddd-ts-kit/events';
|
|
86
|
+
|
|
87
|
+
const registry = createEventRegistry([orderPlaced, invoiceIssued]);
|
|
88
|
+
|
|
89
|
+
export const messagingModule = defineContextModule({
|
|
90
|
+
name: 'messaging',
|
|
91
|
+
register(builder) {
|
|
92
|
+
// The adapter of your database, or InMemoryOutboxStore in a single process.
|
|
93
|
+
builder.bind(OutboxStore).toClass(SqlOutboxStore, [Database]);
|
|
94
|
+
builder
|
|
95
|
+
.rebind(IntegrationEventPublisher)
|
|
96
|
+
.toFactory((resolve) =>
|
|
97
|
+
validatingPublisher(new OutboxPublisher(resolve(OutboxStore)), registry, { unknown: 'reject' }),
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// main.ts
|
|
105
|
+
import { createApplication, kernelModule } from '@ddd-ts-kit/di';
|
|
106
|
+
import { Clock, Logger } from '@ddd-ts-kit/kernel';
|
|
107
|
+
import { OutboxRelay, OutboxStore } from '@ddd-ts-kit/events';
|
|
108
|
+
|
|
109
|
+
const app = createApplication({ modules: [kernelModule(), persistenceModule, messagingModule, orderingModule] });
|
|
110
|
+
const logger = app.resolve(Logger);
|
|
111
|
+
|
|
112
|
+
const relay = new OutboxRelay({
|
|
113
|
+
store: app.resolve(OutboxStore),
|
|
114
|
+
publisher: broker, // the real provider: any IntegrationEventPublisher
|
|
115
|
+
clock: app.resolve(Clock),
|
|
116
|
+
maxAttempts: 8,
|
|
117
|
+
onFailure: (failure) => logger.warn('outbox relay failure', { ...failure }), // codes only, never an error message
|
|
118
|
+
});
|
|
119
|
+
relay.start({ intervalMs: 500 });
|
|
120
|
+
process.on('SIGTERM', () => void relay.stop()); // waits for the tick in progress
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
An adapter `implements OutboxStore` and never extends it: the port has abstract members only.
|
|
124
|
+
Without a container the same objects are built by hand:
|
|
125
|
+
`new OutboxPublisher(store)`, `new OutboxRelay({ store, publisher: broker, clock })`.
|
|
126
|
+
|
|
127
|
+
`relay.tick()` claims a batch, publishes each event, marks it published, or schedules a retry
|
|
128
|
+
with an exponential backoff; after `maxAttempts` the record is `dead` until
|
|
129
|
+
`store.retryDead(ids, now)`. `store.stats()` gives `{ pending, failed, dead }` for a health
|
|
130
|
+
check, `store.purgePublished(before)` keeps the table small.
|
|
131
|
+
|
|
132
|
+
**Tracing across the outbox.** The envelope carries `traceparent`, and every outbox store
|
|
133
|
+
persists and restores it exactly (the contract suite checks it). With
|
|
134
|
+
`kernelModule({ instrumentation: traceApplication() })` the context of the request is stamped
|
|
135
|
+
when the event is appended, and `new OutboxRelay({ store, publisher: tracedPublisher(broker), clock })`
|
|
136
|
+
continues that trace when it publishes later: the relay has no span of its own, and a tick
|
|
137
|
+
that publishes nothing makes none. `OutboxPublisher` names itself `messagingSystem: 'outbox'`,
|
|
138
|
+
which `validatingPublisher` and `scopedPublisher` pass on. See the README of
|
|
139
|
+
`@ddd-ts-kit/tracing`.
|
|
140
|
+
|
|
141
|
+
Delivery is at least once. **By default ordering is not guaranteed, not even per aggregate**:
|
|
142
|
+
a failed event is retried while its successors go out, and several relays may run side by
|
|
143
|
+
side. When order matters, carry a sequence or a version in the payload, or ask the relay for
|
|
144
|
+
it (below).
|
|
145
|
+
|
|
146
|
+
### Waking the relay instead of polling fast
|
|
147
|
+
|
|
148
|
+
A relay can sleep on a long interval and still publish at once, when the store tells it that
|
|
149
|
+
something was appended. `wakeOn` takes a `RelaySignal`, a port of one method
|
|
150
|
+
(`subscribe(listener): { unsubscribe() }`): PostgreSQL `LISTEN/NOTIFY`, a change stream,
|
|
151
|
+
or `InMemoryRelaySignal` in a single process. A module binds it like the store,
|
|
152
|
+
`builder.bind(RelaySignal).toClass(PostgresRelaySignal, [Database])`, and the relay takes
|
|
153
|
+
`wakeOn: app.resolve(RelaySignal)`.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const signal = new InMemoryRelaySignal();
|
|
157
|
+
const store = new InMemoryOutboxStore({ signal }); // notifies after each append
|
|
158
|
+
const relay = new OutboxRelay({ store, publisher: broker, clock, wakeOn: signal });
|
|
159
|
+
relay.start({ intervalMs: 30_000 }); // the interval is now only the safety net
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Bursts are coalesced: one tick runs, at most one more is queued, whatever the number of
|
|
163
|
+
notifications. **Polling stays as the safety net**: a signal is a hint, and a notification
|
|
164
|
+
missed while the listener reconnects or the process restarts must only delay the publication
|
|
165
|
+
until the next interval, never lose it. So keep `intervalMs` at the longest delay you accept
|
|
166
|
+
for that case, and never rely on the signal alone. A transactional store must notify after
|
|
167
|
+
the commit (PostgreSQL does it by itself: `NOTIFY` is delivered at commit).
|
|
168
|
+
|
|
169
|
+
### Ordering per key
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const relay = new OutboxRelay({
|
|
173
|
+
store,
|
|
174
|
+
publisher: broker,
|
|
175
|
+
clock,
|
|
176
|
+
ordering: 'per-key',
|
|
177
|
+
keyOf: (event) => (orderPlaced.is(event) ? event.payload.orderId : undefined),
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
With `ordering: 'per-key'` a record is never published while an earlier record with the same
|
|
182
|
+
key is still pending or failed. The price is head-of-line blocking: one event that cannot be
|
|
183
|
+
published holds back every later event of its key (and only of its key) until it goes out or
|
|
184
|
+
is dead. Events for which `keyOf` gives `undefined` are not ordered.
|
|
185
|
+
|
|
186
|
+
What it relies on, and what it does not give:
|
|
187
|
+
|
|
188
|
+
- **Run a single relay.** The memory of which record blocks which key lives in the relay, not
|
|
189
|
+
in the store: the `OutboxStore` port is unchanged, and `claimBatch` alone cannot tell that an
|
|
190
|
+
earlier record is waiting for its retry. Two relays side by side publish concurrently.
|
|
191
|
+
- **A restart forgets it.** If the process restarts while a record is in backoff or leased,
|
|
192
|
+
a later record of the same key that becomes due first is published first, once. Strict
|
|
193
|
+
ordering across restarts needs the key stored with the record and a claim that skips keys
|
|
194
|
+
with an earlier unpublished row: a change of the port and of the table, not done here.
|
|
195
|
+
- A record held back is rescheduled with the head of its key and shows in `stats().failed`
|
|
196
|
+
with the code `OUTBOX_HELD_FOR_ORDER`; the claims it spends waiting are not counted as
|
|
197
|
+
delivery attempts by this relay.
|
|
198
|
+
- A **dead** record stops blocking its key (`onFailure` reports it with `dead: true`). Set
|
|
199
|
+
`maxAttempts: Infinity` if a key must rather stay blocked than skip an event.
|
|
200
|
+
- The broker and the consumer must keep the order too: one partition or one queue with a
|
|
201
|
+
single active consumer per key, and a consumer that does not retry out of order.
|
|
202
|
+
|
|
203
|
+
An `OutboxStore` leases what it hands out: `claimBatch` hides the claimed records for the
|
|
204
|
+
lease of the store, so a relay that dies lets them come back. Adapters are held to
|
|
205
|
+
`outboxStoreContract` of `@ddd-ts-kit/testing/contracts`.
|
|
206
|
+
|
|
207
|
+
## Consume idempotently
|
|
208
|
+
|
|
209
|
+
With `@ddd-ts-kit/di`, a declared integration event handler is idempotent with nothing to
|
|
210
|
+
write: its idempotency mark and its work run in one unit of work whenever a `UnitOfWork` and a
|
|
211
|
+
`ProcessedEventStore` are bound, which `kernelModule()` does. The composition root builds no
|
|
212
|
+
handler by hand:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// contexts/billing/application/integration/handlers/invoice-on-order-placed.integration-handler.ts
|
|
216
|
+
import { CommandBus, type IntegrationEvent } from '@ddd-ts-kit/kernel';
|
|
217
|
+
import { IntegrationEventHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
218
|
+
import { orderPlaced, type OrderPlaced } from '@/contexts/ordering/published-language/index.js';
|
|
219
|
+
|
|
220
|
+
@IntegrationEventHandler(orderPlaced, { consumer: 'billing.invoice-on-order-placed' }, [CommandBus])
|
|
221
|
+
export class InvoiceOnOrderPlaced {
|
|
222
|
+
constructor(private readonly commands: CommandBus) {}
|
|
223
|
+
|
|
224
|
+
async handle(event: IntegrationEvent<OrderPlaced>): Promise<void> {
|
|
225
|
+
await this.commands.execute(new IssueInvoice(event.payload.orderId, event.payload.total));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`consume` is the tool of a project that wires without di. It gives the same semantics to a
|
|
231
|
+
plain function:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// contexts/billing/billing.subscriptions.ts
|
|
235
|
+
import { consume, subscribeAll } from '@ddd-ts-kit/events';
|
|
236
|
+
|
|
237
|
+
export const billingSubscriptions = ({ commandBus, processed, unitOfWork }: BillingDependencies) => [
|
|
238
|
+
consume(orderPlaced, { consumer: 'billing.invoice-on-order-placed', processed, unitOfWork }, async (event) => {
|
|
239
|
+
await commandBus.execute(new IssueInvoice(event.payload.orderId, event.payload.total));
|
|
240
|
+
}),
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
// main.ts
|
|
244
|
+
const subscription = subscribeAll(subscriber, billingSubscriptions(dependencies));
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The handler receives a typed, validated event. An event of another version of the same type
|
|
248
|
+
is skipped (subscribe to it with its own declaration); an invalid payload throws, so the
|
|
249
|
+
provider retries or dead-letters.
|
|
250
|
+
|
|
251
|
+
`consume` runs the deduplication and the handler in **one unit of work**: the mark in the
|
|
252
|
+
processed-event store and the state change of the consumer commit together. A redelivery is
|
|
253
|
+
handled once per consumer, and an event whose handler threw is handled again when it comes
|
|
254
|
+
back, because its mark was rolled back with the rest. The command bus middleware joins that
|
|
255
|
+
unit of work (a nested `run` joins the outer transaction), so the handler needs nothing
|
|
256
|
+
special.
|
|
257
|
+
|
|
258
|
+
`on(definition, handler, { consumer, processed, unitOfWork })` is the same thing with
|
|
259
|
+
optional parts, for a consumer that needs no deduplication or has no transaction. Mind one
|
|
260
|
+
case: with `processed` and no `unitOfWork`, the mark is taken before the handler runs. If the
|
|
261
|
+
handler then fails, a transactional store keeps the mark (it was committed on its own) and the
|
|
262
|
+
redelivery is skipped: the event is lost for that consumer. Without a database, use a store
|
|
263
|
+
that can take its mark back, a `ReleasableProcessedEventStore` such as the
|
|
264
|
+
`InMemoryProcessedEventStore` of the kernel: `on` and `consume` release the mark of a handler
|
|
265
|
+
that failed. `ReleasableProcessedEventStore` is a type, not a port of its own: it is the
|
|
266
|
+
`ProcessedEventStore` port of the kernel with its optional `release` made mandatory, and such
|
|
267
|
+
a store is bound under `ProcessedEventStore`.
|
|
268
|
+
|
|
269
|
+
### The execution context of a consumed event
|
|
270
|
+
|
|
271
|
+
`consume` and `on` take the `ExecutionScope` of the kernel and run everything, the unit of
|
|
272
|
+
work and the idempotency mark included, inside it:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
consume(orderPlaced, { consumer: 'billing.invoice-on-order-placed', processed, unitOfWork, scope }, handler);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
| Field of the context | Value |
|
|
279
|
+
|---|---|
|
|
280
|
+
| `origin` | `event` |
|
|
281
|
+
| `correlationId` | the `correlationId` of the event, or its `id` when it names none |
|
|
282
|
+
| `causationId` | the `id` of the event |
|
|
283
|
+
| `actor` | `systemActor(consumer)`, unless `actorOf(event)` answers one |
|
|
284
|
+
|
|
285
|
+
So the repositories of the consuming context stamp audit columns with
|
|
286
|
+
`system:billing.invoice-on-order-placed`, and a log line of the consumer carries the correlation
|
|
287
|
+
of the request that started it all. The envelope carries no actor by default, on purpose: an
|
|
288
|
+
actor is personal data, and a broker keeps messages. A project that decides to put a reference
|
|
289
|
+
in its own payload or envelope maps it with `actorOf: (event) => Actor | undefined`.
|
|
290
|
+
|
|
291
|
+
On the publishing side, `scopedPublisher` of the kernel fills the correlation in, so use cases
|
|
292
|
+
never pass it by hand:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { ExecutionScope, IntegrationEventPublisher, scopedPublisher } from '@ddd-ts-kit/kernel';
|
|
296
|
+
|
|
297
|
+
builder
|
|
298
|
+
.rebind(IntegrationEventPublisher) // what handlers publish to
|
|
299
|
+
.toFactory((resolve) => scopedPublisher(new OutboxPublisher(resolve(OutboxStore)), resolve(ExecutionScope)));
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
An event created with a `correlationId` keeps it; one published outside of any execution
|
|
303
|
+
context is left as it is. It is a decorator, not an option of `OutboxPublisher` or of
|
|
304
|
+
`defineIntegrationEvent(...).create`: both stay pure, and the stamp applies to every provider.
|
|
305
|
+
|
|
306
|
+
## Generate the AsyncAPI document and the catalog
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
// scripts/generate-contracts.ts
|
|
310
|
+
import { generateEventCatalog } from '@ddd-ts-kit/events';
|
|
311
|
+
import { checkAsyncApiDrift, generateAsyncApi, stringifyAsyncApi } from '@ddd-ts-kit/events/asyncapi';
|
|
312
|
+
|
|
313
|
+
const document = generateAsyncApi({
|
|
314
|
+
info: { title: 'Shop events', version: '1.4.0' },
|
|
315
|
+
registry,
|
|
316
|
+
servers: { production: { host: 'broker.example.com:5671', protocol: 'amqps' } },
|
|
317
|
+
});
|
|
318
|
+
await writeFile('docs/integration/asyncapi.json', stringifyAsyncApi(document));
|
|
319
|
+
await writeFile(
|
|
320
|
+
'docs/integration/event-catalog.md',
|
|
321
|
+
generateEventCatalog(registry, { consumers: { 'ordering.order.placed': ['billing', 'shipping'] } }),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// In CI: fail when the committed file is stale.
|
|
325
|
+
if (checkAsyncApiDrift(document, await readFile('docs/integration/asyncapi.json', 'utf8')).drift) {
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The AsyncAPI 3.0.0 document has one channel per event type (its address is the type), one
|
|
331
|
+
message per version whose payload is the envelope (`id`, `type`, `version`, `occurredAt`,
|
|
332
|
+
`payload`, `traceparent`, `correlationId`), and one `send` operation per channel. The catalog
|
|
333
|
+
is Markdown: one section per producer, a table of versions per event, and the payload fields
|
|
334
|
+
flattened (`customer.email`, `lines[].sku`). Both outputs depend only on the declarations,
|
|
335
|
+
never on their registration order, so they can be committed and diffed.
|
|
336
|
+
A project generated by the CLI has this script as `scripts/events.ts`, with the same two
|
|
337
|
+
paths.
|
|
338
|
+
|
|
339
|
+
## Choosing the strategy for your transport
|
|
340
|
+
|
|
341
|
+
The rule is one, whatever the tools: the handler writes the event to the outbox in the
|
|
342
|
+
transaction of the command, a relay publishes it afterwards, the consumer deduplicates. What
|
|
343
|
+
changes with the transport is how the relay runs and what the consumer side must do.
|
|
344
|
+
|
|
345
|
+
| Transport | Relay | Consumer side |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| same process, no broker | polling relay to the in-memory bus (or woken by the database: PostgreSQL `LISTEN/NOTIFY`) | processed-event store in the consumer's transaction |
|
|
348
|
+
| RabbitMQ and other queue brokers | polling relay publishing with publisher confirms, mark published on confirm | durable queue per consumer, manual ack after commit, dead-letter exchange after bounded retries |
|
|
349
|
+
| Kafka and other logs | polling relay, or change data capture reading the outbox table (Debezium outbox router); key = aggregate id for per-aggregate order | consumer group per consumer, offset committed after the handler's transaction, processed-event store for redelivery |
|
|
350
|
+
| cloud queues and topics (SQS/SNS, Pub/Sub) | polling relay, batch publish | visibility timeout above the handler's worst case, redrive policy to a dead-letter queue |
|
|
351
|
+
|
|
352
|
+
Without a database there is nothing to be atomic with: publish to the bus directly, no outbox.
|
|
353
|
+
|
|
354
|
+
The kit has a provider package for RabbitMQ (`@ddd-ts-kit/rabbitmq`), Kafka
|
|
355
|
+
(`@ddd-ts-kit/kafka`), NATS JetStream (`@ddd-ts-kit/nats`) and Redis Streams
|
|
356
|
+
(`@ddd-ts-kit/redis/event-bus`); each README says how it meets the requirements below and what it does
|
|
357
|
+
not guarantee.
|
|
358
|
+
|
|
359
|
+
### Same process, no broker
|
|
360
|
+
|
|
361
|
+
- **Relay mode.** `OutboxRelay` publishing to the `InMemoryEventBus`. Wake it with `wakeOn`
|
|
362
|
+
(the in-memory signal, or `LISTEN/NOTIFY` on PostgreSQL) and keep a long interval as the
|
|
363
|
+
safety net.
|
|
364
|
+
- **Publisher requirements.** None beyond the port. The in-memory bus runs the handlers inside
|
|
365
|
+
`publish` and rejects when one fails: the relay then retries the record, so the retry
|
|
366
|
+
policy of the consumers IS the backoff of the relay.
|
|
367
|
+
- **Consumer requirements.** `consume` with the processed-event store and the unit of work of
|
|
368
|
+
the consuming context. A redelivery runs every handler of the type again, so every one of
|
|
369
|
+
them deduplicates.
|
|
370
|
+
- **Ordering.** `per-key` gives it here with nothing else to set up: one relay, one process,
|
|
371
|
+
handlers awaited in order (the restart caveat of the relay still applies).
|
|
372
|
+
- **Dead-lettering.** The outbox is the dead-letter store: a record is `dead` after
|
|
373
|
+
`maxAttempts`, and comes back with `store.retryDead(ids, now)`.
|
|
374
|
+
- **Monitor.** `stats()`: `dead > 0` is an alert; `failed` rising means a consumer keeps
|
|
375
|
+
failing; the age of the oldest pending record is the delivery delay.
|
|
376
|
+
|
|
377
|
+
### Queue brokers (RabbitMQ, ActiveMQ)
|
|
378
|
+
|
|
379
|
+
- **Relay mode.** Polling or woken relay. The record is marked published only when the broker
|
|
380
|
+
confirmed it.
|
|
381
|
+
- **Publisher requirements.** `publish` must resolve only on the confirmation of the broker
|
|
382
|
+
(publisher confirms) and reject otherwise, with durable exchanges and persistent messages.
|
|
383
|
+
A confirmation lost after the broker took the message makes the relay publish it again:
|
|
384
|
+
duplicates are expected. `@ddd-ts-kit/rabbitmq` implements this row.
|
|
385
|
+
- **Consumer requirements.** One durable queue per consumer and event type, manual
|
|
386
|
+
acknowledgement after the unit of work of the handler committed, `consume` for the
|
|
387
|
+
deduplication. Never acknowledge before the commit.
|
|
388
|
+
- **Ordering.** None by default: retries and competing consumers reorder. Per key only with
|
|
389
|
+
the `per-key` relay AND a single active consumer on the queue AND no retry queue, which is
|
|
390
|
+
rarely worth it; prefer a version in the payload.
|
|
391
|
+
- **Dead-lettering.** Two places. Publishing side: the outbox (`dead`, `retryDead`).
|
|
392
|
+
Consuming side: a bounded number of delayed retries, then a dead queue per consumer queue.
|
|
393
|
+
- **Monitor.** Outbox `stats()`, and on the broker the depth of each work queue (a consumer
|
|
394
|
+
that lags), of each dead queue (must be empty) and the unacknowledged count.
|
|
395
|
+
|
|
396
|
+
### Logs (Kafka, Redpanda, Pulsar, Kinesis)
|
|
397
|
+
|
|
398
|
+
- **Relay mode.** Polling relay, or change data capture (below).
|
|
399
|
+
- **Publisher requirements.** Idempotent producer with `acks=all`; `publish` resolves on the
|
|
400
|
+
acknowledgement of the write. The message key is the aggregate id, so one aggregate lives in
|
|
401
|
+
one partition. Topic per event type or per context, chosen once.
|
|
402
|
+
- **Consumer requirements.** One consumer group per consuming context. The offset is committed
|
|
403
|
+
after the transaction of the handler, never before; a crash in between redelivers, hence
|
|
404
|
+
the processed-event store. A handler that keeps failing blocks its partition: retry in
|
|
405
|
+
place a bounded number of times, then publish the record to a dead-letter topic and commit.
|
|
406
|
+
- **Ordering.** Per partition, so per key, provided the relay keeps it: `ordering: 'per-key'`
|
|
407
|
+
with `keyOf` returning the same aggregate id, a single relay, and
|
|
408
|
+
`max.in.flight.requests.per.connection` at most 5 with idempotence on.
|
|
409
|
+
- **Dead-lettering.** Outbox on the publishing side, a dead-letter topic per consumer group on
|
|
410
|
+
the consuming side.
|
|
411
|
+
- **Monitor.** Outbox `stats()`, consumer lag per group and partition, dead-letter topic size.
|
|
412
|
+
|
|
413
|
+
### Cloud queues and topics (SQS/SNS, Pub/Sub)
|
|
414
|
+
|
|
415
|
+
- **Relay mode.** Polling relay. Batch publication (10 entries per call on SQS and SNS) is
|
|
416
|
+
allowed, but the result is per entry: publish, then reject if any entry failed, so the relay
|
|
417
|
+
retries; the entries that did pass are published twice, which the consumer absorbs.
|
|
418
|
+
- **Publisher requirements.** Topic with one subscription (queue) per consumer. `publish`
|
|
419
|
+
resolves on the service acknowledgement. Message attributes carry type, version and trace.
|
|
420
|
+
- **Consumer requirements.** Visibility timeout (ack deadline) above the worst case of the
|
|
421
|
+
handler, extended while it runs if needed; delete (ack) the message after the commit.
|
|
422
|
+
`consume` for the deduplication: these services redeliver by design.
|
|
423
|
+
- **Ordering.** None on standard queues. FIFO queues and ordering keys give it per message
|
|
424
|
+
group, at a throughput cost, with the `per-key` relay and the group id = `keyOf`.
|
|
425
|
+
- **Dead-lettering.** Outbox on the publishing side; a redrive policy (`maxReceiveCount`) to a
|
|
426
|
+
dead-letter queue per consumer queue.
|
|
427
|
+
- **Monitor.** Outbox `stats()`, age of the oldest message per queue, dead-letter queue depth.
|
|
428
|
+
|
|
429
|
+
### What to monitor on the outbox
|
|
430
|
+
|
|
431
|
+
`store.stats()` gives `{ pending, failed, dead }`; expose them as gauges.
|
|
432
|
+
|
|
433
|
+
| Signal | Meaning | Reaction |
|
|
434
|
+
|---|---|---|
|
|
435
|
+
| `dead > 0` | records gave up after `maxAttempts` | alert; fix the cause, then `retryDead` |
|
|
436
|
+
| `failed > 0` for long | the provider refuses or is down; under `per-key`, held records count here too (`OUTBOX_HELD_FOR_ORDER`) | look at the codes given to `onFailure` |
|
|
437
|
+
| `pending` growing | the relay is slower than the producers, or stopped | check that it runs; raise `batchSize` |
|
|
438
|
+
| age of the oldest pending record | the real delivery delay | alert above your tolerance; it is one query on the table (`min(created_at) where status = 'pending'`), `stats()` does not carry it |
|
|
439
|
+
| `onFailure({ kind: 'tick' })` | the store itself failed | database health |
|
|
440
|
+
|
|
441
|
+
Purge published records regularly (`purgePublished(before)`), or the table and its index grow
|
|
442
|
+
without bound.
|
|
443
|
+
|
|
444
|
+
### Where change data capture fits
|
|
445
|
+
|
|
446
|
+
Change data capture (Debezium and its outbox event router, or a logical replication reader)
|
|
447
|
+
replaces **the relay, never the table**. Handlers still write the event to the outbox table
|
|
448
|
+
in the transaction of the command; instead of a process polling that table, the capture
|
|
449
|
+
reads the same rows from the transaction log of the database and publishes them, in commit
|
|
450
|
+
order. Consequences:
|
|
451
|
+
|
|
452
|
+
- nothing changes in the handlers, in `OutboxPublisher` or in the consumers;
|
|
453
|
+
- do not run an `OutboxRelay` on the same table for the same destination: one or the other;
|
|
454
|
+
- `status`, `attempts` and `next_attempt_at` are no longer maintained: retries and failures
|
|
455
|
+
are those of the connector, so monitor the connector (lag, failed tasks) instead of
|
|
456
|
+
`stats()`, and purge by age;
|
|
457
|
+
- the event router must map the columns: `id` to the message id, `type` to the topic or
|
|
458
|
+
routing key, the aggregate id to the message key, `payload` and the trace columns to the
|
|
459
|
+
body and headers, so the consumer receives the same envelope as from the relay;
|
|
460
|
+
- ordering per key comes for free (the log is ordered), which is the main reason to choose it.
|
|
461
|
+
|
|
462
|
+
Writing to the broker from the handler and "capturing" the business tables instead is not
|
|
463
|
+
this pattern: it publishes rows, not events, and couples consumers to the schema.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { JsonSchema } from '@ddd-ts-kit/validation';
|
|
2
|
+
import type { AsyncApiInfo } from './async-api-info.js';
|
|
3
|
+
import type { AsyncApiServer } from './async-api-server.js';
|
|
4
|
+
export interface AsyncApiDocument {
|
|
5
|
+
readonly asyncapi: '3.0.0';
|
|
6
|
+
readonly info: AsyncApiInfo;
|
|
7
|
+
readonly servers?: Record<string, AsyncApiServer>;
|
|
8
|
+
readonly channels: Record<string, JsonSchema>;
|
|
9
|
+
readonly operations: Record<string, JsonSchema>;
|
|
10
|
+
readonly components: {
|
|
11
|
+
readonly messages: Record<string, JsonSchema>;
|
|
12
|
+
readonly schemas?: Record<string, JsonSchema>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=async-api-document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-api-document.d.ts","sourceRoot":"","sources":["../../src/asyncapi/async-api-document.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAChD,QAAQ,CAAC,UAAU,EAAE;QACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;KAC/C,CAAC;CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-api-document.js","sourceRoot":"","sources":["../../src/asyncapi/async-api-document.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-api-info.d.ts","sourceRoot":"","sources":["../../src/asyncapi/async-api-info.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-api-info.js","sourceRoot":"","sources":["../../src/asyncapi/async-api-info.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-api-server.d.ts","sourceRoot":"","sources":["../../src/asyncapi/async-api-server.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wCAAwC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-api-server.js","sourceRoot":"","sources":["../../src/asyncapi/async-api-server.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AsyncApiDocument } from './async-api-document.js';
|
|
2
|
+
/** Compares the generated document with the committed one, ignoring line-ending style. */
|
|
3
|
+
export declare function checkAsyncApiDrift(document: AsyncApiDocument, existingFileContent: string): {
|
|
4
|
+
drift: boolean;
|
|
5
|
+
};
|
|
6
|
+
//# sourceMappingURL=check-async-api-drift.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-async-api-drift.d.ts","sourceRoot":"","sources":["../../src/asyncapi/check-async-api-drift.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,CAE9G"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { stringifyAsyncApi } from './stringify-async-api.js';
|
|
2
|
+
/** Compares the generated document with the committed one, ignoring line-ending style. */
|
|
3
|
+
export function checkAsyncApiDrift(document, existingFileContent) {
|
|
4
|
+
return { drift: stringifyAsyncApi(document) !== existingFileContent.replaceAll('\r\n', '\n') };
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=check-async-api-drift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-async-api-drift.js","sourceRoot":"","sources":["../../src/asyncapi/check-async-api-drift.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D,0FAA0F;AAC1F,MAAM,UAAU,kBAAkB,CAAC,QAA0B,EAAE,mBAA2B;IACxF,OAAO,EAAE,KAAK,EAAE,iBAAiB,CAAC,QAAQ,CAAC,KAAK,mBAAmB,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;AACjG,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EventRegistry } from '../registry/event-registry.js';
|
|
2
|
+
import type { AsyncApiDocument } from './async-api-document.js';
|
|
3
|
+
import type { AsyncApiInfo } from './async-api-info.js';
|
|
4
|
+
import type { AsyncApiServer } from './async-api-server.js';
|
|
5
|
+
export interface GenerateAsyncApiOptions {
|
|
6
|
+
readonly info: AsyncApiInfo;
|
|
7
|
+
readonly registry: EventRegistry;
|
|
8
|
+
readonly servers?: Readonly<Record<string, AsyncApiServer>>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* One channel per event type (its address is the type), one message per version, one `send`
|
|
12
|
+
* operation per channel. The output depends only on the registry content, never on the
|
|
13
|
+
* registration order, so the document can be committed and diffed in CI.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateAsyncApi(options: GenerateAsyncApiOptions): AsyncApiDocument;
|
|
16
|
+
//# sourceMappingURL=generate-async-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-async-api.d.ts","sourceRoot":"","sources":["../../src/asyncapi/generate-async-api.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;CAC7D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAwBnF"}
|