@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.
Files changed (214) hide show
  1. package/README.md +397 -2
  2. package/dist/codec/decode.d.ts +4 -0
  3. package/dist/codec/decode.d.ts.map +1 -0
  4. package/dist/codec/decode.js +39 -0
  5. package/dist/codec/decode.js.map +1 -0
  6. package/dist/codec/encode.d.ts +9 -0
  7. package/dist/codec/encode.d.ts.map +1 -0
  8. package/dist/codec/encode.js +22 -0
  9. package/dist/codec/encode.js.map +1 -0
  10. package/dist/codec/headers.d.ts +13 -0
  11. package/dist/codec/headers.d.ts.map +1 -0
  12. package/dist/codec/headers.js +13 -0
  13. package/dist/codec/headers.js.map +1 -0
  14. package/dist/codec/incoming-record.d.ts +8 -0
  15. package/dist/codec/incoming-record.d.ts.map +1 -0
  16. package/dist/codec/incoming-record.js +2 -0
  17. package/dist/codec/incoming-record.js.map +1 -0
  18. package/dist/codec/outgoing-record.d.ts +7 -0
  19. package/dist/codec/outgoing-record.d.ts.map +1 -0
  20. package/dist/codec/outgoing-record.js +2 -0
  21. package/dist/codec/outgoing-record.js.map +1 -0
  22. package/dist/codec/republication.d.ts +5 -0
  23. package/dist/codec/republication.d.ts.map +1 -0
  24. package/dist/codec/republication.js +8 -0
  25. package/dist/codec/republication.js.map +1 -0
  26. package/dist/codec/routing.d.ts +7 -0
  27. package/dist/codec/routing.d.ts.map +1 -0
  28. package/dist/codec/routing.js +2 -0
  29. package/dist/codec/routing.js.map +1 -0
  30. package/dist/codec/source-of.d.ts +4 -0
  31. package/dist/codec/source-of.d.ts.map +1 -0
  32. package/dist/codec/source-of.js +5 -0
  33. package/dist/codec/source-of.js.map +1 -0
  34. package/dist/codec/text-header.d.ts +4 -0
  35. package/dist/codec/text-header.d.ts.map +1 -0
  36. package/dist/codec/text-header.js +7 -0
  37. package/dist/codec/text-header.js.map +1 -0
  38. package/dist/connection/broker-link.d.ts +44 -0
  39. package/dist/connection/broker-link.d.ts.map +1 -0
  40. package/dist/connection/broker-link.js +145 -0
  41. package/dist/connection/broker-link.js.map +1 -0
  42. package/dist/connection/cluster-of.d.ts +8 -0
  43. package/dist/connection/cluster-of.d.ts.map +1 -0
  44. package/dist/connection/cluster-of.js +16 -0
  45. package/dist/connection/cluster-of.js.map +1 -0
  46. package/dist/connection/rebalance.d.ts +7 -0
  47. package/dist/connection/rebalance.d.ts.map +1 -0
  48. package/dist/connection/rebalance.js +2 -0
  49. package/dist/connection/rebalance.js.map +1 -0
  50. package/dist/consumption/attempts.d.ts +16 -0
  51. package/dist/consumption/attempts.d.ts.map +1 -0
  52. package/dist/consumption/attempts.js +23 -0
  53. package/dist/consumption/attempts.js.map +1 -0
  54. package/dist/consumption/backoff.d.ts +12 -0
  55. package/dist/consumption/backoff.d.ts.map +1 -0
  56. package/dist/consumption/backoff.js +20 -0
  57. package/dist/consumption/backoff.js.map +1 -0
  58. package/dist/consumption/consumption.d.ts +18 -0
  59. package/dist/consumption/consumption.d.ts.map +1 -0
  60. package/dist/consumption/consumption.js +33 -0
  61. package/dist/consumption/consumption.js.map +1 -0
  62. package/dist/consumption/delivery-postponed.d.ts +9 -0
  63. package/dist/consumption/delivery-postponed.d.ts.map +1 -0
  64. package/dist/consumption/delivery-postponed.js +12 -0
  65. package/dist/consumption/delivery-postponed.js.map +1 -0
  66. package/dist/consumption/failure-of.d.ts +7 -0
  67. package/dist/consumption/failure-of.d.ts.map +1 -0
  68. package/dist/consumption/failure-of.js +18 -0
  69. package/dist/consumption/failure-of.js.map +1 -0
  70. package/dist/consumption/group-consumer.d.ts +60 -0
  71. package/dist/consumption/group-consumer.d.ts.map +1 -0
  72. package/dist/consumption/group-consumer.js +145 -0
  73. package/dist/consumption/group-consumer.js.map +1 -0
  74. package/dist/consumption/poison-message.d.ts +3 -0
  75. package/dist/consumption/poison-message.d.ts.map +1 -0
  76. package/dist/consumption/poison-message.js +3 -0
  77. package/dist/consumption/poison-message.js.map +1 -0
  78. package/dist/consumption/postpone.d.ts +9 -0
  79. package/dist/consumption/postpone.d.ts.map +1 -0
  80. package/dist/consumption/postpone.js +30 -0
  81. package/dist/consumption/postpone.js.map +1 -0
  82. package/dist/consumption/record-handling.d.ts +58 -0
  83. package/dist/consumption/record-handling.d.ts.map +1 -0
  84. package/dist/consumption/record-handling.js +183 -0
  85. package/dist/consumption/record-handling.js.map +1 -0
  86. package/dist/consumption/timers.d.ts +7 -0
  87. package/dist/consumption/timers.d.ts.map +1 -0
  88. package/dist/consumption/timers.js +17 -0
  89. package/dist/consumption/timers.js.map +1 -0
  90. package/dist/errors/broker-unavailable-error.d.ts +8 -0
  91. package/dist/errors/broker-unavailable-error.d.ts.map +1 -0
  92. package/dist/errors/broker-unavailable-error.js +12 -0
  93. package/dist/errors/broker-unavailable-error.js.map +1 -0
  94. package/dist/errors/consumer-name-required-error.d.ts +5 -0
  95. package/dist/errors/consumer-name-required-error.d.ts.map +1 -0
  96. package/dist/errors/consumer-name-required-error.js +8 -0
  97. package/dist/errors/consumer-name-required-error.js.map +1 -0
  98. package/dist/errors/event-bus-closed-error.d.ts +5 -0
  99. package/dist/errors/event-bus-closed-error.d.ts.map +1 -0
  100. package/dist/errors/event-bus-closed-error.js +8 -0
  101. package/dist/errors/event-bus-closed-error.js.map +1 -0
  102. package/dist/errors/failure-code.d.ts +6 -0
  103. package/dist/errors/failure-code.d.ts.map +1 -0
  104. package/dist/errors/failure-code.js +15 -0
  105. package/dist/errors/failure-code.js.map +1 -0
  106. package/dist/errors/publish-not-acknowledged-error.d.ts +14 -0
  107. package/dist/errors/publish-not-acknowledged-error.d.ts.map +1 -0
  108. package/dist/errors/publish-not-acknowledged-error.js +18 -0
  109. package/dist/errors/publish-not-acknowledged-error.js.map +1 -0
  110. package/dist/errors/reason-of.d.ts +3 -0
  111. package/dist/errors/reason-of.d.ts.map +1 -0
  112. package/dist/errors/reason-of.js +11 -0
  113. package/dist/errors/reason-of.js.map +1 -0
  114. package/dist/errors/within.d.ts +3 -0
  115. package/dist/errors/within.d.ts.map +1 -0
  116. package/dist/errors/within.js +17 -0
  117. package/dist/errors/within.js.map +1 -0
  118. package/dist/ignore.d.ts +3 -0
  119. package/dist/ignore.d.ts.map +1 -0
  120. package/dist/ignore.js +5 -0
  121. package/dist/ignore.js.map +1 -0
  122. package/dist/index.d.ts +20 -0
  123. package/dist/index.d.ts.map +1 -0
  124. package/dist/index.js +10 -0
  125. package/dist/index.js.map +1 -0
  126. package/dist/kafka-event-bus.d.ts +58 -0
  127. package/dist/kafka-event-bus.d.ts.map +1 -0
  128. package/dist/kafka-event-bus.js +147 -0
  129. package/dist/kafka-event-bus.js.map +1 -0
  130. package/dist/messaging-system.d.ts +3 -0
  131. package/dist/messaging-system.d.ts.map +1 -0
  132. package/dist/messaging-system.js +3 -0
  133. package/dist/messaging-system.js.map +1 -0
  134. package/dist/options/kafka-delivery.d.ts +15 -0
  135. package/dist/options/kafka-delivery.d.ts.map +1 -0
  136. package/dist/options/kafka-delivery.js +2 -0
  137. package/dist/options/kafka-delivery.js.map +1 -0
  138. package/dist/options/kafka-event-bus-options.d.ts +42 -0
  139. package/dist/options/kafka-event-bus-options.d.ts.map +1 -0
  140. package/dist/options/kafka-event-bus-options.js +2 -0
  141. package/dist/options/kafka-event-bus-options.js.map +1 -0
  142. package/dist/options/kafka-failure.d.ts +19 -0
  143. package/dist/options/kafka-failure.d.ts.map +1 -0
  144. package/dist/options/kafka-failure.js +2 -0
  145. package/dist/options/kafka-failure.js.map +1 -0
  146. package/dist/options/reconnect-options.d.ts +9 -0
  147. package/dist/options/reconnect-options.d.ts.map +1 -0
  148. package/dist/options/reconnect-options.js +2 -0
  149. package/dist/options/reconnect-options.js.map +1 -0
  150. package/dist/options/retry-options.d.ts +7 -0
  151. package/dist/options/retry-options.d.ts.map +1 -0
  152. package/dist/options/retry-options.js +2 -0
  153. package/dist/options/retry-options.js.map +1 -0
  154. package/dist/options/settings-of.d.ts +4 -0
  155. package/dist/options/settings-of.d.ts.map +1 -0
  156. package/dist/options/settings-of.js +47 -0
  157. package/dist/options/settings-of.js.map +1 -0
  158. package/dist/options/settings.d.ts +27 -0
  159. package/dist/options/settings.d.ts.map +1 -0
  160. package/dist/options/settings.js +2 -0
  161. package/dist/options/settings.js.map +1 -0
  162. package/dist/options/topic-options.d.ts +9 -0
  163. package/dist/options/topic-options.d.ts.map +1 -0
  164. package/dist/options/topic-options.js +2 -0
  165. package/dist/options/topic-options.js.map +1 -0
  166. package/dist/publication/publications.d.ts +30 -0
  167. package/dist/publication/publications.d.ts.map +1 -0
  168. package/dist/publication/publications.js +83 -0
  169. package/dist/publication/publications.js.map +1 -0
  170. package/dist/replay/dead-letter-replay.d.ts +31 -0
  171. package/dist/replay/dead-letter-replay.d.ts.map +1 -0
  172. package/dist/replay/dead-letter-replay.js +97 -0
  173. package/dist/replay/dead-letter-replay.js.map +1 -0
  174. package/dist/secrecy/conceal.d.ts +8 -0
  175. package/dist/secrecy/conceal.d.ts.map +1 -0
  176. package/dist/secrecy/conceal.js +12 -0
  177. package/dist/secrecy/conceal.js.map +1 -0
  178. package/dist/topology/consumer-binding.d.ts +6 -0
  179. package/dist/topology/consumer-binding.d.ts.map +1 -0
  180. package/dist/topology/consumer-binding.js +2 -0
  181. package/dist/topology/consumer-binding.js.map +1 -0
  182. package/dist/topology/dead-topic-of.d.ts +3 -0
  183. package/dist/topology/dead-topic-of.d.ts.map +1 -0
  184. package/dist/topology/dead-topic-of.js +5 -0
  185. package/dist/topology/dead-topic-of.js.map +1 -0
  186. package/dist/topology/describe-topic.d.ts +5 -0
  187. package/dist/topology/describe-topic.d.ts.map +1 -0
  188. package/dist/topology/describe-topic.js +5 -0
  189. package/dist/topology/describe-topic.js.map +1 -0
  190. package/dist/topology/group-description.d.ts +5 -0
  191. package/dist/topology/group-description.d.ts.map +1 -0
  192. package/dist/topology/group-description.js +2 -0
  193. package/dist/topology/group-description.js.map +1 -0
  194. package/dist/topology/kafka-topology.d.ts +8 -0
  195. package/dist/topology/kafka-topology.d.ts.map +1 -0
  196. package/dist/topology/kafka-topology.js +2 -0
  197. package/dist/topology/kafka-topology.js.map +1 -0
  198. package/dist/topology/replay-group-of.d.ts +3 -0
  199. package/dist/topology/replay-group-of.d.ts.map +1 -0
  200. package/dist/topology/replay-group-of.js +5 -0
  201. package/dist/topology/replay-group-of.js.map +1 -0
  202. package/dist/topology/topic-description.d.ts +10 -0
  203. package/dist/topology/topic-description.d.ts.map +1 -0
  204. package/dist/topology/topic-description.js +2 -0
  205. package/dist/topology/topic-description.js.map +1 -0
  206. package/dist/topology/topology-of.d.ts +5 -0
  207. package/dist/topology/topology-of.d.ts.map +1 -0
  208. package/dist/topology/topology-of.js +25 -0
  209. package/dist/topology/topology-of.js.map +1 -0
  210. package/dist/topology/topology-settings.d.ts +7 -0
  211. package/dist/topology/topology-settings.d.ts.map +1 -0
  212. package/dist/topology/topology-settings.js +2 -0
  213. package/dist/topology/topology-settings.js.map +1 -0
  214. package/package.json +37 -4
package/README.md CHANGED
@@ -1,3 +1,398 @@
1
- # Temporary Holding Version
1
+ # @ddd-ts-kit/kafka
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=incoming-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incoming-record.js","sourceRoot":"","sources":["../../src/codec/incoming-record.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ import type { KafkaJS } from '@confluentinc/kafka-javascript';
2
+ /** A record ready to be sent to its topic. */
3
+ export interface OutgoingRecord {
4
+ readonly topic: string;
5
+ readonly message: KafkaJS.Message;
6
+ }
7
+ //# sourceMappingURL=outgoing-record.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=outgoing-record.js.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing.js","sourceRoot":"","sources":["../../src/codec/routing.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ import type { IncomingRecord } from './incoming-record.js';
2
+ /** Where a record sits in the log: `<topic>/<partition>/<offset>`. */
3
+ export declare function sourceOf({ topic, partition, message }: IncomingRecord): string;
4
+ //# sourceMappingURL=source-of.d.ts.map
@@ -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"}