@ddd-ts-kit/kafka 0.0.0-stage → 0.1.1
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 +397 -2
- package/dist/codec/decode.d.ts +4 -0
- package/dist/codec/decode.d.ts.map +1 -0
- package/dist/codec/decode.js +39 -0
- package/dist/codec/decode.js.map +1 -0
- package/dist/codec/encode.d.ts +9 -0
- package/dist/codec/encode.d.ts.map +1 -0
- package/dist/codec/encode.js +22 -0
- package/dist/codec/encode.js.map +1 -0
- package/dist/codec/headers.d.ts +13 -0
- package/dist/codec/headers.d.ts.map +1 -0
- package/dist/codec/headers.js +13 -0
- package/dist/codec/headers.js.map +1 -0
- package/dist/codec/incoming-record.d.ts +8 -0
- package/dist/codec/incoming-record.d.ts.map +1 -0
- package/dist/codec/incoming-record.js +2 -0
- package/dist/codec/incoming-record.js.map +1 -0
- package/dist/codec/outgoing-record.d.ts +7 -0
- package/dist/codec/outgoing-record.d.ts.map +1 -0
- package/dist/codec/outgoing-record.js +2 -0
- package/dist/codec/outgoing-record.js.map +1 -0
- package/dist/codec/republication.d.ts +5 -0
- package/dist/codec/republication.d.ts.map +1 -0
- package/dist/codec/republication.js +8 -0
- package/dist/codec/republication.js.map +1 -0
- package/dist/codec/routing.d.ts +7 -0
- package/dist/codec/routing.d.ts.map +1 -0
- package/dist/codec/routing.js +2 -0
- package/dist/codec/routing.js.map +1 -0
- package/dist/codec/source-of.d.ts +4 -0
- package/dist/codec/source-of.d.ts.map +1 -0
- package/dist/codec/source-of.js +5 -0
- package/dist/codec/source-of.js.map +1 -0
- package/dist/codec/text-header.d.ts +4 -0
- package/dist/codec/text-header.d.ts.map +1 -0
- package/dist/codec/text-header.js +7 -0
- package/dist/codec/text-header.js.map +1 -0
- package/dist/connection/broker-link.d.ts +44 -0
- package/dist/connection/broker-link.d.ts.map +1 -0
- package/dist/connection/broker-link.js +145 -0
- package/dist/connection/broker-link.js.map +1 -0
- package/dist/connection/cluster-of.d.ts +8 -0
- package/dist/connection/cluster-of.d.ts.map +1 -0
- package/dist/connection/cluster-of.js +16 -0
- package/dist/connection/cluster-of.js.map +1 -0
- package/dist/connection/rebalance.d.ts +7 -0
- package/dist/connection/rebalance.d.ts.map +1 -0
- package/dist/connection/rebalance.js +2 -0
- package/dist/connection/rebalance.js.map +1 -0
- package/dist/consumption/attempts.d.ts +16 -0
- package/dist/consumption/attempts.d.ts.map +1 -0
- package/dist/consumption/attempts.js +23 -0
- package/dist/consumption/attempts.js.map +1 -0
- package/dist/consumption/backoff.d.ts +12 -0
- package/dist/consumption/backoff.d.ts.map +1 -0
- package/dist/consumption/backoff.js +20 -0
- package/dist/consumption/backoff.js.map +1 -0
- package/dist/consumption/consumption.d.ts +18 -0
- package/dist/consumption/consumption.d.ts.map +1 -0
- package/dist/consumption/consumption.js +33 -0
- package/dist/consumption/consumption.js.map +1 -0
- package/dist/consumption/delivery-postponed.d.ts +9 -0
- package/dist/consumption/delivery-postponed.d.ts.map +1 -0
- package/dist/consumption/delivery-postponed.js +12 -0
- package/dist/consumption/delivery-postponed.js.map +1 -0
- package/dist/consumption/failure-of.d.ts +7 -0
- package/dist/consumption/failure-of.d.ts.map +1 -0
- package/dist/consumption/failure-of.js +18 -0
- package/dist/consumption/failure-of.js.map +1 -0
- package/dist/consumption/group-consumer.d.ts +60 -0
- package/dist/consumption/group-consumer.d.ts.map +1 -0
- package/dist/consumption/group-consumer.js +145 -0
- package/dist/consumption/group-consumer.js.map +1 -0
- package/dist/consumption/poison-message.d.ts +3 -0
- package/dist/consumption/poison-message.d.ts.map +1 -0
- package/dist/consumption/poison-message.js +3 -0
- package/dist/consumption/poison-message.js.map +1 -0
- package/dist/consumption/postpone.d.ts +9 -0
- package/dist/consumption/postpone.d.ts.map +1 -0
- package/dist/consumption/postpone.js +30 -0
- package/dist/consumption/postpone.js.map +1 -0
- package/dist/consumption/record-handling.d.ts +58 -0
- package/dist/consumption/record-handling.d.ts.map +1 -0
- package/dist/consumption/record-handling.js +183 -0
- package/dist/consumption/record-handling.js.map +1 -0
- package/dist/consumption/timers.d.ts +7 -0
- package/dist/consumption/timers.d.ts.map +1 -0
- package/dist/consumption/timers.js +17 -0
- package/dist/consumption/timers.js.map +1 -0
- package/dist/errors/broker-unavailable-error.d.ts +8 -0
- package/dist/errors/broker-unavailable-error.d.ts.map +1 -0
- package/dist/errors/broker-unavailable-error.js +12 -0
- package/dist/errors/broker-unavailable-error.js.map +1 -0
- package/dist/errors/consumer-name-required-error.d.ts +5 -0
- package/dist/errors/consumer-name-required-error.d.ts.map +1 -0
- package/dist/errors/consumer-name-required-error.js +8 -0
- package/dist/errors/consumer-name-required-error.js.map +1 -0
- package/dist/errors/event-bus-closed-error.d.ts +5 -0
- package/dist/errors/event-bus-closed-error.d.ts.map +1 -0
- package/dist/errors/event-bus-closed-error.js +8 -0
- package/dist/errors/event-bus-closed-error.js.map +1 -0
- package/dist/errors/failure-code.d.ts +6 -0
- package/dist/errors/failure-code.d.ts.map +1 -0
- package/dist/errors/failure-code.js +15 -0
- package/dist/errors/failure-code.js.map +1 -0
- package/dist/errors/publish-not-acknowledged-error.d.ts +14 -0
- package/dist/errors/publish-not-acknowledged-error.d.ts.map +1 -0
- package/dist/errors/publish-not-acknowledged-error.js +18 -0
- package/dist/errors/publish-not-acknowledged-error.js.map +1 -0
- package/dist/errors/reason-of.d.ts +3 -0
- package/dist/errors/reason-of.d.ts.map +1 -0
- package/dist/errors/reason-of.js +11 -0
- package/dist/errors/reason-of.js.map +1 -0
- package/dist/errors/within.d.ts +3 -0
- package/dist/errors/within.d.ts.map +1 -0
- package/dist/errors/within.js +17 -0
- package/dist/errors/within.js.map +1 -0
- package/dist/ignore.d.ts +3 -0
- package/dist/ignore.d.ts.map +1 -0
- package/dist/ignore.js +5 -0
- package/dist/ignore.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/kafka-event-bus.d.ts +58 -0
- package/dist/kafka-event-bus.d.ts.map +1 -0
- package/dist/kafka-event-bus.js +147 -0
- package/dist/kafka-event-bus.js.map +1 -0
- package/dist/messaging-system.d.ts +3 -0
- package/dist/messaging-system.d.ts.map +1 -0
- package/dist/messaging-system.js +3 -0
- package/dist/messaging-system.js.map +1 -0
- package/dist/options/kafka-delivery.d.ts +15 -0
- package/dist/options/kafka-delivery.d.ts.map +1 -0
- package/dist/options/kafka-delivery.js +2 -0
- package/dist/options/kafka-delivery.js.map +1 -0
- package/dist/options/kafka-event-bus-options.d.ts +42 -0
- package/dist/options/kafka-event-bus-options.d.ts.map +1 -0
- package/dist/options/kafka-event-bus-options.js +2 -0
- package/dist/options/kafka-event-bus-options.js.map +1 -0
- package/dist/options/kafka-failure.d.ts +19 -0
- package/dist/options/kafka-failure.d.ts.map +1 -0
- package/dist/options/kafka-failure.js +2 -0
- package/dist/options/kafka-failure.js.map +1 -0
- package/dist/options/reconnect-options.d.ts +9 -0
- package/dist/options/reconnect-options.d.ts.map +1 -0
- package/dist/options/reconnect-options.js +2 -0
- package/dist/options/reconnect-options.js.map +1 -0
- package/dist/options/retry-options.d.ts +7 -0
- package/dist/options/retry-options.d.ts.map +1 -0
- package/dist/options/retry-options.js +2 -0
- package/dist/options/retry-options.js.map +1 -0
- package/dist/options/settings-of.d.ts +4 -0
- package/dist/options/settings-of.d.ts.map +1 -0
- package/dist/options/settings-of.js +47 -0
- package/dist/options/settings-of.js.map +1 -0
- package/dist/options/settings.d.ts +27 -0
- package/dist/options/settings.d.ts.map +1 -0
- package/dist/options/settings.js +2 -0
- package/dist/options/settings.js.map +1 -0
- package/dist/options/topic-options.d.ts +9 -0
- package/dist/options/topic-options.d.ts.map +1 -0
- package/dist/options/topic-options.js +2 -0
- package/dist/options/topic-options.js.map +1 -0
- package/dist/publication/publications.d.ts +30 -0
- package/dist/publication/publications.d.ts.map +1 -0
- package/dist/publication/publications.js +83 -0
- package/dist/publication/publications.js.map +1 -0
- package/dist/replay/dead-letter-replay.d.ts +31 -0
- package/dist/replay/dead-letter-replay.d.ts.map +1 -0
- package/dist/replay/dead-letter-replay.js +97 -0
- package/dist/replay/dead-letter-replay.js.map +1 -0
- package/dist/secrecy/conceal.d.ts +8 -0
- package/dist/secrecy/conceal.d.ts.map +1 -0
- package/dist/secrecy/conceal.js +12 -0
- package/dist/secrecy/conceal.js.map +1 -0
- package/dist/topology/consumer-binding.d.ts +6 -0
- package/dist/topology/consumer-binding.d.ts.map +1 -0
- package/dist/topology/consumer-binding.js +2 -0
- package/dist/topology/consumer-binding.js.map +1 -0
- package/dist/topology/dead-topic-of.d.ts +3 -0
- package/dist/topology/dead-topic-of.d.ts.map +1 -0
- package/dist/topology/dead-topic-of.js +5 -0
- package/dist/topology/dead-topic-of.js.map +1 -0
- package/dist/topology/describe-topic.d.ts +5 -0
- package/dist/topology/describe-topic.d.ts.map +1 -0
- package/dist/topology/describe-topic.js +5 -0
- package/dist/topology/describe-topic.js.map +1 -0
- package/dist/topology/group-description.d.ts +5 -0
- package/dist/topology/group-description.d.ts.map +1 -0
- package/dist/topology/group-description.js +2 -0
- package/dist/topology/group-description.js.map +1 -0
- package/dist/topology/kafka-topology.d.ts +8 -0
- package/dist/topology/kafka-topology.d.ts.map +1 -0
- package/dist/topology/kafka-topology.js +2 -0
- package/dist/topology/kafka-topology.js.map +1 -0
- package/dist/topology/replay-group-of.d.ts +3 -0
- package/dist/topology/replay-group-of.d.ts.map +1 -0
- package/dist/topology/replay-group-of.js +5 -0
- package/dist/topology/replay-group-of.js.map +1 -0
- package/dist/topology/topic-description.d.ts +10 -0
- package/dist/topology/topic-description.d.ts.map +1 -0
- package/dist/topology/topic-description.js +2 -0
- package/dist/topology/topic-description.js.map +1 -0
- package/dist/topology/topology-of.d.ts +5 -0
- package/dist/topology/topology-of.d.ts.map +1 -0
- package/dist/topology/topology-of.js +25 -0
- package/dist/topology/topology-of.js.map +1 -0
- package/dist/topology/topology-settings.d.ts +7 -0
- package/dist/topology/topology-settings.d.ts.map +1 -0
- package/dist/topology/topology-settings.js +2 -0
- package/dist/topology/topology-settings.js.map +1 -0
- package/package.json +37 -4
package/README.md
CHANGED
|
@@ -1,3 +1,398 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ddd-ts-kit/kafka
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The integration event provider for Apache Kafka: an `EventBus` of the kernel that publishes
|
|
4
|
+
with an idempotent producer and `acks=all`, so the outbox relay can mark a record published
|
|
5
|
+
safely, and consumes with one consumer group per consuming context, offsets committed after
|
|
6
|
+
the handlers resolved, a bounded number of delayed retries in place and a dead-letter topic.
|
|
7
|
+
It is the "Kafka and other logs" row of the strategy table of `docs/ARCHITECTURE.md`,
|
|
8
|
+
implemented, and it passes `eventBusContract` of `@ddd-ts-kit/testing`.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
npm i @ddd-ts-kit/kafka @ddd-ts-kit/events @confluentinc/kafka-javascript
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Tested with `@confluentinc/kafka-javascript` 1.11 (librdkafka 2.16) against Apache Kafka 4.3
|
|
15
|
+
in KRaft mode, on Node 24. Other brokers speaking the Kafka protocol should work; only Apache
|
|
16
|
+
Kafka was run.
|
|
17
|
+
|
|
18
|
+
## The client
|
|
19
|
+
|
|
20
|
+
`@confluentinc/kafka-javascript` is the client maintained by Confluent: a binding of
|
|
21
|
+
librdkafka with a promise API in the manner of KafkaJS, which is what this package uses.
|
|
22
|
+
|
|
23
|
+
| Candidate | Looked at on 2026-10-10 | Verdict |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `kafkajs` 2.2.4 | last release in February 2023 | not maintained: ruled out |
|
|
26
|
+
| `@platformatic/kafka` 2.14.0 | pure JavaScript, 2.2 MB, released almost weekly, about 500 000 downloads a week, requires Node 22.22 or 24.6 | a serious option without native code; its consumer group protocol is a young reimplementation and its engine range is above the one of this kit (22.12) |
|
|
27
|
+
| `@confluentinc/kafka-javascript` 1.11.0 | released 2026-10-07, about 830 000 downloads a week, librdkafka underneath | chosen: the idempotent producer, the rebalance protocol and the reconnection are those of the reference C client, which is where a provider must not be approximately right |
|
|
28
|
+
|
|
29
|
+
The price is a native addon. Nothing is compiled when a prebuilt binary exists: the install
|
|
30
|
+
script of the package downloads one (13 MB, 30 MB installed) from the GitHub releases of the
|
|
31
|
+
client. Prebuilt binaries exist for Linux x64 and arm64 on glibc and musl, macOS arm64 and
|
|
32
|
+
Windows x64, for Node 18 to 24. Verified here: Ubuntu with Node 24.21, and the images
|
|
33
|
+
`node:24-alpine` and `node:24-bookworm-slim`, each without a compiler, in about five seconds.
|
|
34
|
+
|
|
35
|
+
What it asks of CI and of images:
|
|
36
|
+
|
|
37
|
+
- the install script must run: no `--ignore-scripts`, and with a package manager that asks
|
|
38
|
+
for install scripts to be approved (npm 11 warns, pnpm blocks), approve this package; the
|
|
39
|
+
client documents npm as supported and Yarn and pnpm as experimental;
|
|
40
|
+
- the install needs to reach `github.com`; behind a proxy that forbids it, mirror the binary
|
|
41
|
+
and point `node-pre-gyp` at the mirror, or the install falls back to compiling librdkafka,
|
|
42
|
+
which needs Python, make and a C++ compiler and takes minutes;
|
|
43
|
+
- build the image on the libc it runs on: a `node_modules` installed on Debian does not load
|
|
44
|
+
on Alpine;
|
|
45
|
+
- nothing else is needed at run time: the binary links only to libc and libstdc++.
|
|
46
|
+
|
|
47
|
+
## Composition root
|
|
48
|
+
|
|
49
|
+
Handlers never see this package. They publish to the outbox; the relay publishes to Kafka;
|
|
50
|
+
each consuming context listens through the bus under its own name. The bus is given to the
|
|
51
|
+
application as two ports of the kernel: `IntegrationEventPublisher` stays the outbox, and
|
|
52
|
+
`EventSubscribers` hands each context the subscriber of its own consumer name.
|
|
53
|
+
|
|
54
|
+
`init --broker kafka` writes this wiring in the platform of the project: the bus is built by
|
|
55
|
+
`platform/events/create-event-provider.ts`, `platform/platform.module.ts` rebinds
|
|
56
|
+
`EventSubscribers` on it, and `main.ts` gives it, traced, to the outbox relay. By hand, the
|
|
57
|
+
same thing reads:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// platform/messaging.module.ts
|
|
61
|
+
import { defineContextModule } from '@ddd-ts-kit/di';
|
|
62
|
+
import { EventSubscribers, IntegrationEventPublisher, type IntegrationEvent } from '@ddd-ts-kit/kernel';
|
|
63
|
+
import { OutboxPublisher, OutboxStore } from '@ddd-ts-kit/events';
|
|
64
|
+
import { KafkaEventBus } from '@ddd-ts-kit/kafka';
|
|
65
|
+
|
|
66
|
+
// The ordering key, given to the relay and to the bus: the aggregate id an event is about.
|
|
67
|
+
export const aggregateOf = (event: IntegrationEvent): string | undefined =>
|
|
68
|
+
orderPlaced.is(event) ? event.payload.orderId : undefined;
|
|
69
|
+
|
|
70
|
+
export const bus = new KafkaEventBus({
|
|
71
|
+
brokers: config.kafkaBrokers, // 'kafka-1:9092,kafka-2:9092' or a list
|
|
72
|
+
client: { 'security.protocol': 'sasl_ssl', 'sasl.mechanism': 'SCRAM-SHA-512' /* ... */ },
|
|
73
|
+
keyOf: aggregateOf, // default: the event id, which orders nothing
|
|
74
|
+
publishedTypes: ['billing.invoice.issued'], // their topics are created by assertTopology
|
|
75
|
+
topics: { partitions: 6, replicationFactor: 3 }, // default: those of the broker
|
|
76
|
+
retry: { maxAttempts: 5, delayMs: 5_000 }, // the defaults
|
|
77
|
+
onFailure: (failure) => logger.warn('event bus failure', { ...failure }), // codes only, never an error message
|
|
78
|
+
onDelivery: ({ consumer, topic, outcome }) => deliveries.add(1, { consumer, topic, outcome }),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const messagingModule = defineContextModule({
|
|
82
|
+
name: 'messaging',
|
|
83
|
+
register(builder) {
|
|
84
|
+
// Publishing side: handlers write to the outbox, in the transaction of the command.
|
|
85
|
+
builder.rebind(IntegrationEventPublisher).toClass(OutboxPublisher, [OutboxStore]);
|
|
86
|
+
// Consuming side: one subscriber per consuming context, named after it.
|
|
87
|
+
builder.rebind(EventSubscribers).toValue({ forContext: (name) => bus.forConsumer(name) });
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// contexts/billing/application/integration/handlers/invoice-on-order-placed.integration-handler.ts
|
|
94
|
+
import { CommandBus, systemActor, type IntegrationEvent } from '@ddd-ts-kit/kernel';
|
|
95
|
+
import { IntegrationEventHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
96
|
+
|
|
97
|
+
@IntegrationEventHandler(orderPlaced, { consumer: 'billing.invoice-on-order-placed' }, [CommandBus])
|
|
98
|
+
export class InvoiceOnOrderPlacedIntegrationHandler {
|
|
99
|
+
constructor(private readonly commands: CommandBus) {}
|
|
100
|
+
|
|
101
|
+
// Nobody is calling: the handler acts as a system actor named after its consumer.
|
|
102
|
+
readonly actor = systemActor('billing.invoice-on-order-placed');
|
|
103
|
+
|
|
104
|
+
async handle(event: IntegrationEvent<OrderPlacedPayload>): Promise<void> {
|
|
105
|
+
await this.commands.execute(new IssueInvoice(this.actor, event.payload.orderId));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// main.ts
|
|
112
|
+
import { contextModule, createApplication, kernelModule } from '@ddd-ts-kit/di';
|
|
113
|
+
import { Clock } from '@ddd-ts-kit/kernel';
|
|
114
|
+
import { OutboxRelay, OutboxStore } from '@ddd-ts-kit/events';
|
|
115
|
+
import { aggregateOf, bus, messagingModule } from '@/platform/messaging.module.js';
|
|
116
|
+
|
|
117
|
+
const app = createApplication({
|
|
118
|
+
modules: [
|
|
119
|
+
kernelModule(),
|
|
120
|
+
persistenceModule,
|
|
121
|
+
messagingModule,
|
|
122
|
+
contextModule('ordering', orderingDeclarations),
|
|
123
|
+
contextModule('billing', billingDeclarations),
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// The relay target is the bus. A record is marked published when every in-sync replica has it.
|
|
128
|
+
const relay = new OutboxRelay({
|
|
129
|
+
store: app.resolve(OutboxStore),
|
|
130
|
+
publisher: bus,
|
|
131
|
+
clock: app.resolve(Clock),
|
|
132
|
+
ordering: 'per-key', // one running relay: the events of an aggregate leave in order
|
|
133
|
+
keyOf: aggregateOf,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await app.start(); // subscribes the handler of billing on bus.forConsumer('billing')
|
|
137
|
+
await bus.assertTopology(); // fail at startup, not at the first record
|
|
138
|
+
relay.start({ intervalMs: 500 });
|
|
139
|
+
|
|
140
|
+
process.on('SIGTERM', async () => {
|
|
141
|
+
await relay.stop(); // the tick in progress finishes
|
|
142
|
+
await app.stop(); // the handlers are unsubscribed
|
|
143
|
+
await bus.close(); // consumers stop, handlers in progress finish and commit
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- `persistenceModule` binds `OutboxStore`, and rebinds `UnitOfWork` and `ProcessedEventStore`
|
|
148
|
+
to the database: see the README of the persistence adapter. A declared integration event
|
|
149
|
+
handler is subscribed on the subscriber of its own context (`forContext` receives the name
|
|
150
|
+
of its module), and since `UnitOfWork` and `ProcessedEventStore` are bound, its idempotency
|
|
151
|
+
mark and its work commit in one unit of work: a redelivery is deduplicated, a failure
|
|
152
|
+
leaves nothing behind.
|
|
153
|
+
- Without an outbox the bus is the event bus of the kernel module, and handlers publish
|
|
154
|
+
straight to the broker:
|
|
155
|
+
`kernelModule({ eventBus: bus, eventSubscribers: () => ({ forContext: (name) => bus.forConsumer(name) }) })`.
|
|
156
|
+
- Without a container the same objects are wired by hand: `bus.forConsumer('billing')` is an
|
|
157
|
+
`IntegrationEventSubscriber`, to give to `subscribeAll` and `consume` of `@ddd-ts-kit/events`.
|
|
158
|
+
- `publish(events)` resolves only when every in-sync replica acknowledged **every** record of
|
|
159
|
+
the batch and rejects otherwise (`PUBLISH_NOT_ACKNOWLEDGED` with the code of librdkafka in
|
|
160
|
+
`reason`, `BROKER_UNAVAILABLE`, `EVENT_BUS_CLOSED`). Nothing is buffered for later: the
|
|
161
|
+
outbox is the buffer.
|
|
162
|
+
- A consumer name is required to subscribe, and it is the consumer group id, as is:
|
|
163
|
+
`bus.forConsumer('billing')` gives an `IntegrationEventSubscriber` for one context on the
|
|
164
|
+
clients of the bus (a modular monolith has several), and the `consumer` option names the
|
|
165
|
+
one `bus.subscribe` works for. A context extracted to its own service keeps its name, so
|
|
166
|
+
its group and its offsets: nothing changes on the cluster.
|
|
167
|
+
- Tracing is put around both sides from outside, as with any provider: `tracedPublisher(bus)`
|
|
168
|
+
as the relay target, and on the consuming side the instrumentation of the kernel module
|
|
169
|
+
(`kernelModule({ instrumentation: traceApplication() })`), which gives each declared handler
|
|
170
|
+
one span `process <consumer>` per attempt. `tracedSubscriber(bus.forConsumer(name))` is for
|
|
171
|
+
a composition without `kernelModule`. The bus carries `traceparent` and `correlationId`
|
|
172
|
+
untouched, in the envelope and in the record headers.
|
|
173
|
+
- The correlation makes the round trip. Publish through `scopedPublisher(publisher, scope)` of
|
|
174
|
+
the kernel: a declared handler (or `consume(definition, { ..., scope }, handler)` by hand,
|
|
175
|
+
or a callback wrapped with `inEventScope`) runs in an execution context whose
|
|
176
|
+
`correlationId` is the one of the request that published, whose `causationId` is the event,
|
|
177
|
+
and a retry runs in the same one.
|
|
178
|
+
- Change data capture (the Debezium outbox event router) can replace the relay and the
|
|
179
|
+
publishing side of this bus; give it the same topics, keys and headers. The consuming side
|
|
180
|
+
stays.
|
|
181
|
+
|
|
182
|
+
## Topology
|
|
183
|
+
|
|
184
|
+
`bus.describeTopology()` returns the topics and the groups as data, for a review or a
|
|
185
|
+
generated document; `bus.assertTopology()` creates the topics that do not exist and resolves
|
|
186
|
+
once the consumers have their partitions. For `forConsumer('billing')` subscribed to
|
|
187
|
+
`ordering.order.placed`:
|
|
188
|
+
|
|
189
|
+
| Object | Name | Properties |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| events topic | `ordering.order.placed` | `topicOf(type)`, by default the type; partitions and replication of `topics`, or of the broker |
|
|
192
|
+
| consumer group | `billing` | subscribed to the events topics of its types; starts at the earliest record |
|
|
193
|
+
| dead-letter topic | `billing.ordering.order.placed.dead` | `<consumer>.<topic>.dead`; written by this consumer only |
|
|
194
|
+
| replay group | `billing.replay` | exists once `replayDead` ran; its committed offsets on the dead-letter topics say what was replayed |
|
|
195
|
+
|
|
196
|
+
One topic per event type is the default. `topicOf: (type) => type.split('.')[0]` gives one
|
|
197
|
+
topic per producing context instead: a consumer then reads every type of that context and
|
|
198
|
+
skips, by committing them, those it did not subscribe to; this is what keeps the order
|
|
199
|
+
between two types of one aggregate.
|
|
200
|
+
|
|
201
|
+
Topics are created on demand too: the first publication of a type creates its topic, the
|
|
202
|
+
first subscription creates the events topic and the dead-letter topic. Automatic creation by
|
|
203
|
+
the broker is never relied on (it gives one partition and the default replication, silently).
|
|
204
|
+
An existing topic is never altered: `topics` applies at creation only. With
|
|
205
|
+
`topics: { create: false }` the bus creates nothing and a missing topic fails the
|
|
206
|
+
publication.
|
|
207
|
+
|
|
208
|
+
A record is the envelope as JSON (`id`, `type`, `version`, `occurredAt`, `payload`,
|
|
209
|
+
`traceparent`, `correlationId`), its key is `keyOf(event)`, and its headers are `x-event-id`,
|
|
210
|
+
`x-event-type`, `x-event-version`, `traceparent` and `x-correlation-id`. Any Kafka client can
|
|
211
|
+
consume it.
|
|
212
|
+
|
|
213
|
+
Two consumers of one type are two groups and both receive every event. Two instances of one
|
|
214
|
+
consumer are two members of its group: each partition, so each key, goes to one of them.
|
|
215
|
+
|
|
216
|
+
## Ordering
|
|
217
|
+
|
|
218
|
+
Kafka orders the records of one partition, and the key chooses the partition. So:
|
|
219
|
+
|
|
220
|
+
1. Give `keyOf` the aggregate id. The default key is the event id: every event is then alone
|
|
221
|
+
in its order.
|
|
222
|
+
2. Run the relay with `ordering: 'per-key'` and the same `keyOf`, or the order is lost before
|
|
223
|
+
Kafka sees the events.
|
|
224
|
+
3. The bus does the rest: one publication writes the records of a topic in the order of the
|
|
225
|
+
batch (idempotent producer, so a retry of the client cannot reorder or duplicate them), a
|
|
226
|
+
consumer handles the records of a partition one at a time, and a failed record is retried
|
|
227
|
+
**in place**, see below.
|
|
228
|
+
|
|
229
|
+
`concurrency` is the number of partitions a consumer handles at once (default 1); the records
|
|
230
|
+
of one partition are never handled concurrently.
|
|
231
|
+
|
|
232
|
+
## Retry and dead-letter behaviour
|
|
233
|
+
|
|
234
|
+
1. An offset is committed after every handler subscribed to the type of the record resolved,
|
|
235
|
+
never before, and never automatically. A declared `@IntegrationEventHandler` (or `consume`
|
|
236
|
+
of `@ddd-ts-kit/events` by hand) commits its work and its idempotency mark in one unit of
|
|
237
|
+
work: the commit of the offset then follows that commit, and a redelivery is deduplicated.
|
|
238
|
+
2. When a handler throws, the bus pauses the partition of the record for `retry.delayMs` and
|
|
239
|
+
reads the same record again: no hot loop, the other partitions keep flowing, and the
|
|
240
|
+
consumer keeps polling, so the wait neither delays a rebalance nor counts against
|
|
241
|
+
`max.poll.interval.ms`.
|
|
242
|
+
3. At the `retry.maxAttempts`-th failed delivery the record is copied to the dead-letter
|
|
243
|
+
topic with `x-dead-reason: max-attempts`, `x-error-code`, `x-attempt` and `x-source`
|
|
244
|
+
(`<topic>/<partition>/<offset>`), its offset is committed and the partition moves on. A
|
|
245
|
+
failing record holds its partition for `(maxAttempts - 1) x delayMs` plus the time of its
|
|
246
|
+
handlers (20 s with the defaults), never for ever.
|
|
247
|
+
4. A record whose value is not an envelope (unparseable, or without `id`, `type`, `version`,
|
|
248
|
+
`occurredAt`) is poison: it goes straight to the dead-letter topic with
|
|
249
|
+
`x-dead-reason: poison` and `x-error-code: POISON_MESSAGE`, and no handler sees it.
|
|
250
|
+
5. Only codes travel: `x-error-code` is the `code` of the error or its class name. The message
|
|
251
|
+
of an error is never logged, reported or attached, because it may quote payload data. The
|
|
252
|
+
client logs messages and stacks of what it catches: the bus turns its logger off.
|
|
253
|
+
|
|
254
|
+
**Why retry in place and not through retry topics.** A retry topic lets the partition move on
|
|
255
|
+
at the first failure, and that is exactly the problem: the next event of the same aggregate
|
|
256
|
+
is handled before the one that failed. A database that was down for two seconds would apply
|
|
257
|
+
"order paid" before "order placed". Retrying in place keeps the order of a key through any
|
|
258
|
+
failure that heals within the attempts; the cost is that the other keys of that partition
|
|
259
|
+
wait too, for a bounded time. The order is given up only at the dead-letter step: the events
|
|
260
|
+
that follow a dead one are handled without it, and a replay brings it back late. A consumer
|
|
261
|
+
that cannot tolerate that must stop on the dead letter itself (carry a version in the
|
|
262
|
+
payload, refuse a gap).
|
|
263
|
+
|
|
264
|
+
Delivery is at least once. The copy-then-commit order means a crash in between delivers the
|
|
265
|
+
record twice, never zero times.
|
|
266
|
+
|
|
267
|
+
**Rebalances.** Offsets are committed one record at a time, for handled records only, so a
|
|
268
|
+
partition that moves to another member is resumed exactly after the last handled record.
|
|
269
|
+
A handler in progress when its partition is taken away finishes; its commit is then refused,
|
|
270
|
+
and the new owner handles the record again: the processed-event store absorbs it. The count
|
|
271
|
+
of attempts lives in the memory of the member: a record being retried when its partition
|
|
272
|
+
moves, or when the process restarts, starts a fresh budget.
|
|
273
|
+
|
|
274
|
+
**Lost connections.** librdkafka reconnects by itself, producers and consumers alike, with
|
|
275
|
+
its own backoff. While the cluster is away `publish` rejects after `publishTimeoutMs` (30 s
|
|
276
|
+
by default) with `PUBLISH_NOT_ACKNOWLEDGED`, so the outbox relay keeps the records and
|
|
277
|
+
retries them; a publication that was cut may have been written, and its retry is then a
|
|
278
|
+
second record with the same event id, which consumers deduplicate. A consumer that could not
|
|
279
|
+
start, commit or write a dead letter reports `kind: 'connection'` and tries again after the
|
|
280
|
+
`reconnect` backoff (200 ms doubling up to 30 s), without moving past the record.
|
|
281
|
+
|
|
282
|
+
**Credentials.** The `client` properties (SASL password, TLS keys) go to the client library when the bus is built and are kept nowhere else. A `KafkaEventBus` that is logged, spread or
|
|
283
|
+
serialised shows `KafkaEventBus [clients concealed]` and nothing else: its fields are not enumerable,
|
|
284
|
+
and `toJSON` and `util.inspect` answer that name. The failures it reports are codes. What the
|
|
285
|
+
client library itself shows when one of its own objects is inspected is not in the hands of
|
|
286
|
+
this package: do not log those.
|
|
287
|
+
|
|
288
|
+
**What a handler is told.** Next to the event, each handler receives the delivery
|
|
289
|
+
(`EventDelivery` of the kernel): `{ attempt, maxAttempts, system: 'kafka', destination, consumer }`,
|
|
290
|
+
where `destination` is the topic. Handlers of the application ignore it; `@ddd-ts-kit/tracing` reads it
|
|
291
|
+
to make one CONSUMER span per attempt and to mark the redeliveries and the attempt after which
|
|
292
|
+
the event is buried. The bus names itself `messagingSystem: 'kafka'`, and `traceparent` travels in
|
|
293
|
+
the envelope and as a real header, intact across a retry.
|
|
294
|
+
|
|
295
|
+
## Operations
|
|
296
|
+
|
|
297
|
+
What to watch:
|
|
298
|
+
|
|
299
|
+
| Signal | Where | Meaning |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| outbox `stats()` (`pending`, `failed`, `dead`) | the outbox store | the publishing side: see `@ddd-ts-kit/events` |
|
|
302
|
+
| lag of group `<consumer>` on its events topics | cluster (`kafka-consumer-groups.sh --describe`, an exporter) | the consumer lags, is stopped, or is retrying a record: a lag that does not move is a partition held by a failing record |
|
|
303
|
+
| lag of group `<consumer>.replay` on `<consumer>.<topic>.dead`, or the end offset of that topic before any replay | cluster | the dead-letter depth: must be 0, alert on anything else |
|
|
304
|
+
| `onDelivery` counted by `outcome` | this bus | rate of `retried`, `dead`, `poison` against `acknowledged` |
|
|
305
|
+
| `onFailure` with `kind: 'connection'` | this bus | consumers that could not start, commit or dead-letter, with the code of librdkafka |
|
|
306
|
+
| `bus.deliveriesInFlight` | this bus | handlers running now; stuck above 0 means a handler that hangs |
|
|
307
|
+
| `bus.partitionsAssigned` | this bus | 0 on an instance that subscribed means it consumes nothing (more instances than partitions, or not joined) |
|
|
308
|
+
| under-replicated partitions, `min.insync.replicas` | cluster | below 2 in-sync replicas, `acks=all` protects nothing |
|
|
309
|
+
|
|
310
|
+
Replaying dead letters, once the cause is fixed:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
const replayed = await bus.replayDead({ consumer: 'billing', type: 'ordering.order.placed' }, 100);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
It copies up to that many records from the dead-letter topic back to the topic they came
|
|
317
|
+
from, with the header `x-replay-for: billing`, and commits its progress on the dead-letter
|
|
318
|
+
topic as the group `billing.replay`. The consumers of this kit other than `billing` skip a
|
|
319
|
+
record marked for someone else; a consumer written without this kit sees the event a second
|
|
320
|
+
time and must deduplicate on `x-event-id`. The replayed record goes to the end of the topic,
|
|
321
|
+
so after the events that followed it. Look at `x-dead-reason` and `x-error-code` first:
|
|
322
|
+
replaying a poison record only kills it again. Dead-letter topics are ordinary topics: give
|
|
323
|
+
them a retention that leaves time to look.
|
|
324
|
+
|
|
325
|
+
To stop a consumer for good, delete its group, its replay group and its dead-letter topics.
|
|
326
|
+
Unsubscribing a handler does not move the group: a record whose type has no handler left
|
|
327
|
+
holds its partition until a handler comes back or the bus closes.
|
|
328
|
+
|
|
329
|
+
## Limits
|
|
330
|
+
|
|
331
|
+
- **A failing record delays its whole partition**, for `(maxAttempts - 1) x delayMs` plus the
|
|
332
|
+
time of its handlers. Keep that well under the latency the other aggregates can accept.
|
|
333
|
+
- **One delay.** The wait is the same for every attempt, no exponential backoff.
|
|
334
|
+
- **One commit per record**, synchronous: the throughput of a partition is bounded by the
|
|
335
|
+
handler plus a commit round trip. It is the price of never committing an unhandled record.
|
|
336
|
+
- **A handler must finish within `maxHandlingMs`** (`max.poll.interval.ms`, 5 minutes by
|
|
337
|
+
default). Beyond it the member is taken for dead, its partitions move, and the record is
|
|
338
|
+
handled twice, concurrently.
|
|
339
|
+
- **A new group starts at the earliest record** of its topics (`startFrom: 'earliest'`): a
|
|
340
|
+
consumer deployed after the producers misses nothing, and a consumer added a year later
|
|
341
|
+
reads a year of retained events. Choose `latest` deliberately, knowing that what is
|
|
342
|
+
published between the creation of the group and its first assignment is then skipped.
|
|
343
|
+
- **A retry runs every handler of the type again**, those that succeeded included, when
|
|
344
|
+
several handlers of one consumer subscribe to the same type. Each must be idempotent.
|
|
345
|
+
- **A batch is not atomic.** When `publish` rejects, some records may have been written. The
|
|
346
|
+
relay publishes one by one and retries, consumers deduplicate. Transactions are not used.
|
|
347
|
+
- **The idempotent producer deduplicates the retries of the client, not those of the relay.**
|
|
348
|
+
A record published again by the relay is a second record: the processed-event store is not
|
|
349
|
+
optional.
|
|
350
|
+
- **Durability is a cluster setting.** `acks=all` waits for the in-sync replicas, whatever
|
|
351
|
+
their number: use a replication factor of 3 with `min.insync.replicas=2`. The defaults of a
|
|
352
|
+
single-node broker acknowledge with one copy.
|
|
353
|
+
- **`publish` does not fail fast while the cluster is away**: it waits `publishTimeoutMs`.
|
|
354
|
+
Lower it for a relay that must notice quickly; it is also the bound of a healthy but slow
|
|
355
|
+
acknowledgement.
|
|
356
|
+
- **Topic and group names** are the event type and the consumer name as given: they must fit
|
|
357
|
+
the 249 characters and the `[a-zA-Z0-9._-]` alphabet of Kafka, dead-letter suffix included.
|
|
358
|
+
- **Partitions of an existing topic are not changed** by the bus, and adding partitions by
|
|
359
|
+
hand moves keys to other partitions: the order of a key is lost across that change.
|
|
360
|
+
- The native client is loaded in the process: a crash of librdkafka is a crash of the
|
|
361
|
+
process, and `client` is passed to it unchecked.
|
|
362
|
+
|
|
363
|
+
Not verified by the test suite: a cluster of several brokers (leader election, an in-sync
|
|
364
|
+
replica set that shrinks, `min.insync.replicas`), TLS and SASL, the cooperative assignor and
|
|
365
|
+
the next-generation group protocol (`group.protocol=consumer`), a consumer process killed
|
|
366
|
+
mid-handler, a fatal producer error (the bus then opens a new producer; the path is not
|
|
367
|
+
exercised), brokers older than 4.3, and Debezium in place of the relay.
|
|
368
|
+
|
|
369
|
+
## Other providers
|
|
370
|
+
|
|
371
|
+
`@ddd-ts-kit/rabbitmq` and `@ddd-ts-kit/nats` implement the same two ports and pass the same
|
|
372
|
+
contract suite. The three share the envelope, the header names (`traceparent`,
|
|
373
|
+
`x-correlation-id`, `x-event-version`, `x-attempt`, `x-error-code`, `x-dead-reason`), the
|
|
374
|
+
option names (`consumer`, `retry`, `reconnect`, `onFailure`, `onDelivery`) and the methods
|
|
375
|
+
(`forConsumer`, `assertTopology`, `describeTopology`, `replayDead`, `close`). Kafka is the one
|
|
376
|
+
to choose when the order of the events of an aggregate matters.
|
|
377
|
+
|
|
378
|
+
## Testing
|
|
379
|
+
|
|
380
|
+
The suites need a real broker and are skipped without `DDD_TS_KIT_TEST_KAFKA_BROKERS`:
|
|
381
|
+
|
|
382
|
+
```
|
|
383
|
+
docker run -d --rm --name ddd-ts-kit-test-kafka -p 127.0.0.1:9092:9092 -p 127.0.0.1:9094:9094 -e KAFKA_NODE_ID=1 -e KAFKA_PROCESS_ROLES=broker,controller -e KAFKA_LISTENERS=PLAINTEXT://:9092,RELAYED://:9094,CONTROLLER://:9093 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092,RELAYED://127.0.0.1:9095 -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,RELAYED:PLAINTEXT -e KAFKA_CONTROLLER_QUORUM_VOTERS=1@localhost:9093 -e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER -e KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 -e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 -e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 -e KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0 apache/kafka:4.3.1
|
|
384
|
+
DDD_TS_KIT_TEST_KAFKA_BROKERS=127.0.0.1:9092 DDD_TS_KIT_TEST_KAFKA_RELAY=127.0.0.1:9095=127.0.0.1:9094 npx vitest run packages/kafka
|
|
385
|
+
docker rm -f ddd-ts-kit-test-kafka
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
A Kafka client connects to the addresses the broker advertises, so a network failure cannot
|
|
389
|
+
be produced by pointing the bus at a relay: the broker has a second listener, `RELAYED`,
|
|
390
|
+
that advertises `127.0.0.1:9095`, where the tests listen and relay to the port behind it
|
|
391
|
+
(9094). `DDD_TS_KIT_TEST_KAFKA_RELAY` gives both addresses; without it the three network
|
|
392
|
+
failure tests are skipped and the rest runs.
|
|
393
|
+
|
|
394
|
+
They run the contract suite on topics of one and of three partitions, then the behaviours of
|
|
395
|
+
this provider, one suite per behaviour (`publishing`, `consuming`, `correlation`, `retries`
|
|
396
|
+
in place, `dead-letters`, `partitions` for the order per key through a retry and a second
|
|
397
|
+
instance joining, `topology`, `connection-loss` with the acknowledged publication cut
|
|
398
|
+
mid-flight, `closing`) and the whole chain from the outbox to an idempotent consumer. Every test works on topics and groups of its own and deletes them.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IntegrationEvent } from '@ddd-ts-kit/kernel';
|
|
2
|
+
/** The envelope exactly as published, or `undefined` for a value that is not one (poison). */
|
|
3
|
+
export declare function decode(value: Buffer | null): IntegrationEvent | undefined;
|
|
4
|
+
//# sourceMappingURL=decode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/codec/decode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,8FAA8F;AAC9F,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,gBAAgB,GAAG,SAAS,CAgBzE"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The envelope exactly as published, or `undefined` for a value that is not one (poison). */
|
|
2
|
+
export function decode(value) {
|
|
3
|
+
const body = value === null ? undefined : parsed(value);
|
|
4
|
+
if (!isEnvelope(body)) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
id: body.id,
|
|
9
|
+
type: body.type,
|
|
10
|
+
version: body.version,
|
|
11
|
+
occurredAt: body.occurredAt,
|
|
12
|
+
payload: body.payload,
|
|
13
|
+
...(typeof body.traceparent === 'string' ? { traceparent: body.traceparent } : {}),
|
|
14
|
+
...(typeof body.correlationId === 'string' ? { correlationId: body.correlationId } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function parsed(content) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(content.toString('utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function isEnvelope(body) {
|
|
26
|
+
if (typeof body !== 'object' || body === null) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return (hasText(body, 'id') &&
|
|
30
|
+
hasText(body, 'type') &&
|
|
31
|
+
hasText(body, 'occurredAt') &&
|
|
32
|
+
'version' in body &&
|
|
33
|
+
typeof body.version === 'number');
|
|
34
|
+
}
|
|
35
|
+
function hasText(body, field) {
|
|
36
|
+
const value = Reflect.get(body, field);
|
|
37
|
+
return typeof value === 'string' && value.length > 0;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=decode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode.js","sourceRoot":"","sources":["../../src/codec/decode.ts"],"names":[],"mappings":"AAEA,8FAA8F;AAC9F,MAAM,UAAU,MAAM,CAAC,KAAoB;IACzC,MAAM,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAExD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,GAAG,CAAC,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,GAAG,CAAC,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACzF,CAAC;AACJ,CAAC;AAYD,SAAS,MAAM,CAAC,OAAe;IAC7B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,IAAa;IAC/B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAC9C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,CACL,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC;QACnB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC;QAC3B,SAAS,IAAI,IAAI;QACjB,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,CACjC,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,IAAY,EAAE,KAAa;IAC1C,MAAM,KAAK,GAAY,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAEhD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IntegrationEvent } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import type { OutgoingRecord } from './outgoing-record.js';
|
|
3
|
+
import type { Routing } from './routing.js';
|
|
4
|
+
/**
|
|
5
|
+
* The value is the whole envelope as JSON, so any Kafka client can consume it. The headers
|
|
6
|
+
* repeat what a router, a tracer or an operator needs without opening the value.
|
|
7
|
+
*/
|
|
8
|
+
export declare function encode(event: IntegrationEvent, routing: Routing): OutgoingRecord;
|
|
9
|
+
//# sourceMappingURL=encode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/codec/encode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAQ3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,cAAc,CAehF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CORRELATION_HEADER, EVENT_ID_HEADER, EVENT_TYPE_HEADER, EVENT_VERSION_HEADER, TRACEPARENT_HEADER, } from './headers.js';
|
|
2
|
+
/**
|
|
3
|
+
* The value is the whole envelope as JSON, so any Kafka client can consume it. The headers
|
|
4
|
+
* repeat what a router, a tracer or an operator needs without opening the value.
|
|
5
|
+
*/
|
|
6
|
+
export function encode(event, routing) {
|
|
7
|
+
return {
|
|
8
|
+
topic: routing.topicOf(event.type),
|
|
9
|
+
message: {
|
|
10
|
+
key: routing.keyOf(event) ?? null,
|
|
11
|
+
value: JSON.stringify(event),
|
|
12
|
+
headers: {
|
|
13
|
+
[EVENT_ID_HEADER]: event.id,
|
|
14
|
+
[EVENT_TYPE_HEADER]: event.type,
|
|
15
|
+
[EVENT_VERSION_HEADER]: String(event.version),
|
|
16
|
+
...(event.traceparent === undefined ? {} : { [TRACEPARENT_HEADER]: event.traceparent }),
|
|
17
|
+
...(event.correlationId === undefined ? {} : { [CORRELATION_HEADER]: event.correlationId }),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=encode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.js","sourceRoot":"","sources":["../../src/codec/encode.ts"],"names":[],"mappings":"AACA,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAItB;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,KAAuB,EAAE,OAAgB;IAC9D,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;QAClC,OAAO,EAAE;YACP,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,IAAI;YACjC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;YAC5B,OAAO,EAAE;gBACP,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,EAAE;gBAC3B,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,IAAI;gBAC/B,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;gBAC7C,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,kBAAkB,CAAC,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;gBACvF,GAAG,CAAC,KAAK,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,kBAAkB,CAAC,EAAE,KAAK,CAAC,aAAa,EAAE,CAAC;aAC5F;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const EVENT_ID_HEADER = "x-event-id";
|
|
2
|
+
export declare const EVENT_TYPE_HEADER = "x-event-type";
|
|
3
|
+
export declare const EVENT_VERSION_HEADER = "x-event-version";
|
|
4
|
+
export declare const TRACEPARENT_HEADER = "traceparent";
|
|
5
|
+
export declare const CORRELATION_HEADER = "x-correlation-id";
|
|
6
|
+
export declare const ATTEMPT_HEADER = "x-attempt";
|
|
7
|
+
export declare const ERROR_CODE_HEADER = "x-error-code";
|
|
8
|
+
export declare const DEAD_REASON_HEADER = "x-dead-reason";
|
|
9
|
+
/** Where a dead record came from: `<topic>/<partition>/<offset>`. */
|
|
10
|
+
export declare const SOURCE_HEADER = "x-source";
|
|
11
|
+
/** Set on a replayed record: the one consumer it is meant for. The others skip it. */
|
|
12
|
+
export declare const REPLAY_FOR_HEADER = "x-replay-for";
|
|
13
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/codec/headers.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,eAAe,CAAC;AAC5C,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAChD,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAChD,eAAO,MAAM,kBAAkB,qBAAqB,CAAC;AACrD,eAAO,MAAM,cAAc,cAAc,CAAC;AAC1C,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAChD,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAClD,qEAAqE;AACrE,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,sFAAsF;AACtF,eAAO,MAAM,iBAAiB,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const EVENT_ID_HEADER = 'x-event-id';
|
|
2
|
+
export const EVENT_TYPE_HEADER = 'x-event-type';
|
|
3
|
+
export const EVENT_VERSION_HEADER = 'x-event-version';
|
|
4
|
+
export const TRACEPARENT_HEADER = 'traceparent';
|
|
5
|
+
export const CORRELATION_HEADER = 'x-correlation-id';
|
|
6
|
+
export const ATTEMPT_HEADER = 'x-attempt';
|
|
7
|
+
export const ERROR_CODE_HEADER = 'x-error-code';
|
|
8
|
+
export const DEAD_REASON_HEADER = 'x-dead-reason';
|
|
9
|
+
/** Where a dead record came from: `<topic>/<partition>/<offset>`. */
|
|
10
|
+
export const SOURCE_HEADER = 'x-source';
|
|
11
|
+
/** Set on a replayed record: the one consumer it is meant for. The others skip it. */
|
|
12
|
+
export const REPLAY_FOR_HEADER = 'x-replay-for';
|
|
13
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/codec/headers.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC;AAC5C,MAAM,CAAC,MAAM,iBAAiB,GAAG,cAAc,CAAC;AAChD,MAAM,CAAC,MAAM,oBAAoB,GAAG,iBAAiB,CAAC;AACtD,MAAM,CAAC,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAChD,MAAM,CAAC,MAAM,kBAAkB,GAAG,kBAAkB,CAAC;AACrD,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAC;AAC1C,MAAM,CAAC,MAAM,iBAAiB,GAAG,cAAc,CAAC;AAChD,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAClD,qEAAqE;AACrE,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AACxC,sFAAsF;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAG,cAAc,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { KafkaJS } from '@confluentinc/kafka-javascript';
|
|
2
|
+
/** A record as a consumer receives it. */
|
|
3
|
+
export interface IncomingRecord {
|
|
4
|
+
readonly topic: string;
|
|
5
|
+
readonly partition: number;
|
|
6
|
+
readonly message: KafkaJS.KafkaMessage;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=incoming-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"incoming-record.d.ts","sourceRoot":"","sources":["../../src/codec/incoming-record.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAE9D,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC;CACxC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"incoming-record.js","sourceRoot":"","sources":["../../src/codec/incoming-record.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outgoing-record.d.ts","sourceRoot":"","sources":["../../src/codec/outgoing-record.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAE9D,8CAA8C;AAC9C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC;CACnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outgoing-record.js","sourceRoot":"","sources":["../../src/codec/outgoing-record.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IncomingRecord } from './incoming-record.js';
|
|
2
|
+
import type { OutgoingRecord } from './outgoing-record.js';
|
|
3
|
+
/** The same record, key and value untouched, for another topic with `headers` added or replaced. */
|
|
4
|
+
export declare function republication({ message }: IncomingRecord, topic: string, headers: Record<string, string>): OutgoingRecord;
|
|
5
|
+
//# sourceMappingURL=republication.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"republication.d.ts","sourceRoot":"","sources":["../../src/codec/republication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,oGAAoG;AACpG,wBAAgB,aAAa,CAC3B,EAAE,OAAO,EAAE,EAAE,cAAc,EAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,cAAc,CAKhB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The same record, key and value untouched, for another topic with `headers` added or replaced. */
|
|
2
|
+
export function republication({ message }, topic, headers) {
|
|
3
|
+
return {
|
|
4
|
+
topic,
|
|
5
|
+
message: { key: message.key, value: message.value, headers: { ...message.headers, ...headers } },
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=republication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"republication.js","sourceRoot":"","sources":["../../src/codec/republication.ts"],"names":[],"mappings":"AAGA,oGAAoG;AACpG,MAAM,UAAU,aAAa,CAC3B,EAAE,OAAO,EAAkB,EAC3B,KAAa,EACb,OAA+B;IAE/B,OAAO;QACL,KAAK;QACL,OAAO,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,EAAE;KACjG,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IntegrationEvent } from '@ddd-ts-kit/kernel';
|
|
2
|
+
/** Where an event goes and under which key. */
|
|
3
|
+
export interface Routing {
|
|
4
|
+
readonly topicOf: (type: string) => string;
|
|
5
|
+
readonly keyOf: (event: IntegrationEvent) => string | undefined;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/codec/routing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,+CAA+C;AAC/C,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,MAAM,GAAG,SAAS,CAAC;CACjE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.js","sourceRoot":"","sources":["../../src/codec/routing.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-of.d.ts","sourceRoot":"","sources":["../../src/codec/source-of.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,sEAAsE;AACtE,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,cAAc,GAAG,MAAM,CAE9E"}
|