@ddd-ts-kit/nats 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 +344 -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 +8 -0
- package/dist/codec/encode.d.ts.map +1 -0
- package/dist/codec/encode.js +19 -0
- package/dist/codec/encode.js.map +1 -0
- package/dist/codec/headers.d.ts +14 -0
- package/dist/codec/headers.d.ts.map +1 -0
- package/dist/codec/headers.js +14 -0
- package/dist/codec/headers.js.map +1 -0
- package/dist/codec/publication.d.ts +7 -0
- package/dist/codec/publication.d.ts.map +1 -0
- package/dist/codec/publication.js +2 -0
- package/dist/codec/publication.js.map +1 -0
- package/dist/codec/republication.d.ts +11 -0
- package/dist/codec/republication.d.ts.map +1 -0
- package/dist/codec/republication.js +16 -0
- package/dist/codec/republication.js.map +1 -0
- package/dist/connection/broker-link.d.ts +49 -0
- package/dist/connection/broker-link.d.ts.map +1 -0
- package/dist/connection/broker-link.js +160 -0
- package/dist/connection/broker-link.js.map +1 -0
- package/dist/consumption/consumption.d.ts +56 -0
- package/dist/consumption/consumption.d.ts.map +1 -0
- package/dist/consumption/consumption.js +223 -0
- package/dist/consumption/consumption.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/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/track.d.ts +3 -0
- package/dist/consumption/track.d.ts.map +1 -0
- package/dist/consumption/track.js +9 -0
- package/dist/consumption/track.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 +8 -0
- package/dist/errors/failure-code.d.ts.map +1 -0
- package/dist/errors/failure-code.js +19 -0
- package/dist/errors/failure-code.js.map +1 -0
- package/dist/errors/publish-not-acknowledged-error.d.ts +11 -0
- package/dist/errors/publish-not-acknowledged-error.d.ts.map +1 -0
- package/dist/errors/publish-not-acknowledged-error.js +16 -0
- package/dist/errors/publish-not-acknowledged-error.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 +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.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/nats-event-bus.d.ts +57 -0
- package/dist/nats-event-bus.d.ts.map +1 -0
- package/dist/nats-event-bus.js +168 -0
- package/dist/nats-event-bus.js.map +1 -0
- package/dist/options/nats-delivery.d.ts +14 -0
- package/dist/options/nats-delivery.d.ts.map +1 -0
- package/dist/options/nats-delivery.js +2 -0
- package/dist/options/nats-delivery.js.map +1 -0
- package/dist/options/nats-event-bus-options.d.ts +40 -0
- package/dist/options/nats-event-bus-options.d.ts.map +1 -0
- package/dist/options/nats-event-bus-options.js +3 -0
- package/dist/options/nats-event-bus-options.js.map +1 -0
- package/dist/options/nats-failure.d.ts +17 -0
- package/dist/options/nats-failure.d.ts.map +1 -0
- package/dist/options/nats-failure.js +2 -0
- package/dist/options/nats-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 +5 -0
- package/dist/options/settings-of.d.ts.map +1 -0
- package/dist/options/settings-of.js +55 -0
- package/dist/options/settings-of.js.map +1 -0
- package/dist/options/settings.d.ts +23 -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/stream-storage.d.ts +2 -0
- package/dist/options/stream-storage.d.ts.map +1 -0
- package/dist/options/stream-storage.js +2 -0
- package/dist/options/stream-storage.js.map +1 -0
- package/dist/publication/publications.d.ts +25 -0
- package/dist/publication/publications.d.ts.map +1 -0
- package/dist/publication/publications.js +47 -0
- package/dist/publication/publications.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/binding-names-of.d.ts +5 -0
- package/dist/topology/binding-names-of.d.ts.map +1 -0
- package/dist/topology/binding-names-of.js +13 -0
- package/dist/topology/binding-names-of.js.map +1 -0
- package/dist/topology/binding-names.d.ts +10 -0
- package/dist/topology/binding-names.d.ts.map +1 -0
- package/dist/topology/binding-names.js +2 -0
- package/dist/topology/binding-names.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/consumer-description.d.ts +9 -0
- package/dist/topology/consumer-description.d.ts.map +1 -0
- package/dist/topology/consumer-description.js +2 -0
- package/dist/topology/consumer-description.js.map +1 -0
- package/dist/topology/dead-stream-of.d.ts +2 -0
- package/dist/topology/dead-stream-of.d.ts.map +1 -0
- package/dist/topology/dead-stream-of.js +4 -0
- package/dist/topology/dead-stream-of.js.map +1 -0
- package/dist/topology/declare-topology.d.ts +8 -0
- package/dist/topology/declare-topology.d.ts.map +1 -0
- package/dist/topology/declare-topology.js +58 -0
- package/dist/topology/declare-topology.js.map +1 -0
- package/dist/topology/nats-topology.d.ts +8 -0
- package/dist/topology/nats-topology.d.ts.map +1 -0
- package/dist/topology/nats-topology.js +2 -0
- package/dist/topology/nats-topology.js.map +1 -0
- package/dist/topology/stream-description.d.ts +11 -0
- package/dist/topology/stream-description.d.ts.map +1 -0
- package/dist/topology/stream-description.js +2 -0
- package/dist/topology/stream-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 +29 -0
- package/dist/topology/topology-of.js.map +1 -0
- package/dist/topology/topology-settings.d.ts +11 -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 +39 -4
package/README.md
CHANGED
|
@@ -1,3 +1,345 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ddd-ts-kit/nats
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The integration event provider for NATS JetStream: an `EventBus` of the kernel that publishes
|
|
4
|
+
to a stream and waits for its acknowledgement, with the event id as message id so the stream
|
|
5
|
+
drops a relay retry, and consumes from one durable pull consumer per consumer and event type
|
|
6
|
+
with explicit acknowledgement, a bounded number of delayed redeliveries and a dead-letter
|
|
7
|
+
stream. It is the JetStream equivalent of the "queue brokers" row of the strategy table of
|
|
8
|
+
`docs/ARCHITECTURE.md`, and it passes `eventBusContract` of `@ddd-ts-kit/testing`.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
npm i @ddd-ts-kit/nats @ddd-ts-kit/events @nats-io/transport-node @nats-io/jetstream
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Tested with `@nats-io/transport-node` and `@nats-io/jetstream` 3.4 against NATS Server 2.15
|
|
15
|
+
started with `-js`, on Node 24. Core NATS has no persistence: only JetStream fits the ports.
|
|
16
|
+
|
|
17
|
+
## The client
|
|
18
|
+
|
|
19
|
+
The modular client of the NATS project: `@nats-io/transport-node` for the connection,
|
|
20
|
+
`@nats-io/jetstream` for streams and consumers. Both are at 3.4.0 (published 2026-05-13),
|
|
21
|
+
pure JavaScript, 1.7 MB installed with their three dependencies, without install script, and
|
|
22
|
+
downloaded about 400 000 and 300 000 times a week. The legacy `nats` package (2.29) still
|
|
23
|
+
receives fixes, but it is the previous line of the client; nothing made it necessary.
|
|
24
|
+
|
|
25
|
+
## Composition root
|
|
26
|
+
|
|
27
|
+
Handlers never see this package. They publish to the outbox; the relay publishes to NATS;
|
|
28
|
+
each consuming context listens through the bus under its own name. The bus is given to the
|
|
29
|
+
application as two ports of the kernel: `IntegrationEventPublisher` stays the outbox, and
|
|
30
|
+
`EventSubscribers` hands each context the subscriber of its own consumer name.
|
|
31
|
+
|
|
32
|
+
`init --broker nats` writes this wiring in the platform of the project: the bus is built by
|
|
33
|
+
`platform/events/create-event-provider.ts`, `platform/platform.module.ts` rebinds
|
|
34
|
+
`EventSubscribers` on it, and `main.ts` gives it, traced, to the outbox relay. By hand, the
|
|
35
|
+
same thing reads:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// platform/messaging.module.ts
|
|
39
|
+
import { defineContextModule } from '@ddd-ts-kit/di';
|
|
40
|
+
import { EventSubscribers, IntegrationEventPublisher } from '@ddd-ts-kit/kernel';
|
|
41
|
+
import { OutboxPublisher, OutboxStore } from '@ddd-ts-kit/events';
|
|
42
|
+
import { NatsEventBus } from '@ddd-ts-kit/nats';
|
|
43
|
+
|
|
44
|
+
export const bus = new NatsEventBus({
|
|
45
|
+
connection: config.natsUrl, // or a list, or options of the client, or a factory opening a new connection
|
|
46
|
+
stream: 'integration-events', // the default
|
|
47
|
+
subjectPrefix: 'events', // the default: events go to events.<event type>
|
|
48
|
+
replicas: 3, // default 1
|
|
49
|
+
retry: { maxAttempts: 5, delayMs: 5_000 }, // the defaults
|
|
50
|
+
prefetch: 10, // the default
|
|
51
|
+
onFailure: (failure) => logger.warn('event bus failure', { ...failure }), // codes only, never an error message
|
|
52
|
+
onDelivery: ({ consumer, outcome }) => deliveries.add(1, { consumer, outcome }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const messagingModule = defineContextModule({
|
|
56
|
+
name: 'messaging',
|
|
57
|
+
register(builder) {
|
|
58
|
+
// Publishing side: handlers write to the outbox, in the transaction of the command.
|
|
59
|
+
builder.rebind(IntegrationEventPublisher).toClass(OutboxPublisher, [OutboxStore]);
|
|
60
|
+
// Consuming side: one subscriber per consuming context, named after it.
|
|
61
|
+
builder.rebind(EventSubscribers).toValue({ forContext: (name) => bus.forConsumer(name) });
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// contexts/billing/application/integration/handlers/invoice-on-order-placed.integration-handler.ts
|
|
68
|
+
import { CommandBus, systemActor, type IntegrationEvent } from '@ddd-ts-kit/kernel';
|
|
69
|
+
import { IntegrationEventHandler } from '@ddd-ts-kit/kernel/declarations';
|
|
70
|
+
|
|
71
|
+
@IntegrationEventHandler(orderPlaced, { consumer: 'billing.invoice-on-order-placed' }, [CommandBus])
|
|
72
|
+
export class InvoiceOnOrderPlacedIntegrationHandler {
|
|
73
|
+
constructor(private readonly commands: CommandBus) {}
|
|
74
|
+
|
|
75
|
+
// Nobody is calling: the handler acts as a system actor named after its consumer.
|
|
76
|
+
readonly actor = systemActor('billing.invoice-on-order-placed');
|
|
77
|
+
|
|
78
|
+
async handle(event: IntegrationEvent<OrderPlacedPayload>): Promise<void> {
|
|
79
|
+
await this.commands.execute(new IssueInvoice(this.actor, event.payload.orderId));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// main.ts
|
|
86
|
+
import { contextModule, createApplication, kernelModule } from '@ddd-ts-kit/di';
|
|
87
|
+
import { Clock } from '@ddd-ts-kit/kernel';
|
|
88
|
+
import { OutboxRelay, OutboxStore } from '@ddd-ts-kit/events';
|
|
89
|
+
import { bus, messagingModule } from '@/platform/messaging.module.js';
|
|
90
|
+
|
|
91
|
+
const app = createApplication({
|
|
92
|
+
modules: [
|
|
93
|
+
kernelModule(),
|
|
94
|
+
persistenceModule,
|
|
95
|
+
messagingModule,
|
|
96
|
+
contextModule('ordering', orderingDeclarations),
|
|
97
|
+
contextModule('billing', billingDeclarations),
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// The relay target is the bus. A record is marked published when the stream acknowledged it.
|
|
102
|
+
const relay = new OutboxRelay({ store: app.resolve(OutboxStore), publisher: bus, clock: app.resolve(Clock) });
|
|
103
|
+
|
|
104
|
+
await app.start(); // subscribes the handler of billing on bus.forConsumer('billing')
|
|
105
|
+
await bus.assertTopology(); // fail at startup, not at the first message
|
|
106
|
+
relay.start({ intervalMs: 500 });
|
|
107
|
+
|
|
108
|
+
process.on('SIGTERM', async () => {
|
|
109
|
+
await relay.stop(); // the tick in progress finishes
|
|
110
|
+
await app.stop(); // the handlers are unsubscribed
|
|
111
|
+
await bus.close(); // consumers stop, handlers in progress finish and acknowledge
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `persistenceModule` binds `OutboxStore`, and rebinds `UnitOfWork` and `ProcessedEventStore`
|
|
116
|
+
to the database: see the README of the persistence adapter. A declared integration event
|
|
117
|
+
handler is subscribed on the subscriber of its own context (`forContext` receives the name
|
|
118
|
+
of its module), and since `UnitOfWork` and `ProcessedEventStore` are bound, its idempotency
|
|
119
|
+
mark and its work commit in one unit of work: a redelivery is deduplicated, a failure
|
|
120
|
+
leaves nothing behind.
|
|
121
|
+
- Without an outbox the bus is the event bus of the kernel module, and handlers publish
|
|
122
|
+
straight to the broker:
|
|
123
|
+
`kernelModule({ eventBus: bus, eventSubscribers: () => ({ forContext: (name) => bus.forConsumer(name) }) })`.
|
|
124
|
+
- Without a container the same objects are wired by hand: `bus.forConsumer('billing')` is an
|
|
125
|
+
`IntegrationEventSubscriber`, to give to `subscribeAll` and `consume` of `@ddd-ts-kit/events`.
|
|
126
|
+
- `publish(events)` resolves only when the stream acknowledged **every** message of the batch
|
|
127
|
+
as stored and rejects otherwise (`PUBLISH_NOT_ACKNOWLEDGED`, `BROKER_UNAVAILABLE`,
|
|
128
|
+
`EVENT_BUS_CLOSED`). Nothing is buffered for later: the outbox is the buffer.
|
|
129
|
+
- Each message carries the header `Nats-Msg-Id` = the event id. The stream drops a second
|
|
130
|
+
message with the same id inside its duplicate window (`duplicateWindowMs`, **two minutes**
|
|
131
|
+
by default, set when the stream is created) and still acknowledges it: a relay that
|
|
132
|
+
retries a publication whose acknowledgement was lost does not create a second message.
|
|
133
|
+
The window covers a retry, not a replay hours later: consumers still deduplicate.
|
|
134
|
+
- A consumer name is required to subscribe: `bus.forConsumer('billing')` gives an
|
|
135
|
+
`IntegrationEventSubscriber` for one context on the shared connection (a modular monolith
|
|
136
|
+
has several), and the `consumer` option names the one `bus.subscribe` works for (a service
|
|
137
|
+
that is a single context).
|
|
138
|
+
- Tracing is put around both sides from outside, as with any provider: `tracedPublisher(bus)`
|
|
139
|
+
as the relay target, and on the consuming side the instrumentation of the kernel module
|
|
140
|
+
(`kernelModule({ instrumentation: traceApplication() })`), which gives each declared handler
|
|
141
|
+
one span `process <consumer>` per attempt. `tracedSubscriber(bus.forConsumer(name))` is for
|
|
142
|
+
a composition without `kernelModule`. The bus carries `traceparent` and `correlationId`
|
|
143
|
+
untouched, in the envelope and in the message headers.
|
|
144
|
+
- The correlation makes the round trip. Publish through `scopedPublisher(publisher, scope)` of
|
|
145
|
+
the kernel: a declared handler (or `consume(definition, { ..., scope }, handler)` by hand,
|
|
146
|
+
or a callback wrapped with `inEventScope`) runs in an execution context whose
|
|
147
|
+
`correlationId` is the one of the request that published, whose `causationId` is the event,
|
|
148
|
+
and a redelivery runs in the same one.
|
|
149
|
+
- A context extracted to its own service keeps its consumer name and so its durable
|
|
150
|
+
consumers: nothing changes on the server.
|
|
151
|
+
|
|
152
|
+
## Topology
|
|
153
|
+
|
|
154
|
+
`bus.describeTopology()` returns the streams and the durable consumers as data, for a review
|
|
155
|
+
or a generated document; `bus.assertTopology()` creates them, idempotently. The bus also
|
|
156
|
+
creates them on every new connection. For `forConsumer('billing')` subscribed to
|
|
157
|
+
`ordering.order.placed`:
|
|
158
|
+
|
|
159
|
+
| Object | Name | Properties |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| events stream | `integration-events` | subjects `events.>`, limits retention, file storage, `replicas`, duplicate window 2 min |
|
|
162
|
+
| subject | `events.ordering.order.placed` | `<subjectPrefix>.<event type>`: the type is several subject tokens, matched in full by the filter |
|
|
163
|
+
| durable consumer | `billing_ordering_order_placed` | pull, filter subject above, explicit acknowledgement, deliver all, `ack_wait` = `ackWaitMs` (30 s), `max_deliver` = 2 x `retry.maxAttempts` |
|
|
164
|
+
| dead-letter stream | `integration-events-dead` | subjects `dead.events.>`, same storage and replicas |
|
|
165
|
+
| dead-letter subject | `dead.events.billing.ordering.order.placed` | `dead.<subjectPrefix>.<consumer>.<event type>`: one per consumer and type |
|
|
166
|
+
|
|
167
|
+
A durable name holds neither dot nor wildcard: every character of the consumer name and of
|
|
168
|
+
the type outside `[A-Za-z0-9_-]` becomes `_`.
|
|
169
|
+
|
|
170
|
+
A message is the envelope as JSON (`id`, `type`, `version`, `occurredAt`, `payload`,
|
|
171
|
+
`traceparent`, `correlationId`) with the headers `Nats-Msg-Id` (the event id),
|
|
172
|
+
`x-event-type`, `x-event-version`, `traceparent` and `x-correlation-id`. Any NATS client can
|
|
173
|
+
consume it.
|
|
174
|
+
|
|
175
|
+
Two consumers of one type have a durable consumer each and both receive every event. Two
|
|
176
|
+
instances of one consumer pull from the same durable consumer: each event goes to one of them.
|
|
177
|
+
|
|
178
|
+
An existing stream is never altered: the bus accepts a stream of the same name whatever its
|
|
179
|
+
limits, so retention, maximum age and size are yours to set on it. An existing durable
|
|
180
|
+
consumer takes the `ack_wait` and the `max_deliver` of the bus that declares it.
|
|
181
|
+
|
|
182
|
+
## Ordering
|
|
183
|
+
|
|
184
|
+
None. The stream keeps the publication order, but a consumer handles `prefetch` messages at
|
|
185
|
+
once, competing instances interleave, and a redelivery comes back after the messages that
|
|
186
|
+
followed. Carry a version in the payload, or use the Kafka provider when the order of an
|
|
187
|
+
aggregate matters.
|
|
188
|
+
|
|
189
|
+
## Retry and dead-letter behaviour
|
|
190
|
+
|
|
191
|
+
1. A message is acknowledged after every handler subscribed to its durable consumer
|
|
192
|
+
resolved. A declared `@IntegrationEventHandler` (or `consume` of `@ddd-ts-kit/events` by
|
|
193
|
+
hand) commits its work and its idempotency mark in one unit of work: the acknowledgement
|
|
194
|
+
then follows that commit, and a redelivery is deduplicated.
|
|
195
|
+
2. When a handler throws, the bus refuses the message with a delay (`nak` with
|
|
196
|
+
`retry.delayMs`): the server delivers it again after that delay, to any instance. The
|
|
197
|
+
attempt number is the delivery count of the server. No hot loop.
|
|
198
|
+
3. At the `retry.maxAttempts`-th failed delivery the bus publishes a copy to the dead-letter
|
|
199
|
+
subject, with `x-dead-reason: max-attempts`, `x-error-code`, `x-attempt` and `x-source`
|
|
200
|
+
(`<stream>/<sequence>`), waits for the acknowledgement of the dead-letter stream, then
|
|
201
|
+
terminates the original (`term`). If the copy cannot be stored the message is refused
|
|
202
|
+
with the delay instead, and parked at a later delivery.
|
|
203
|
+
4. A message whose body is not an envelope (unparseable, or without `id`, `type`, `version`,
|
|
204
|
+
`occurredAt`) is poison: it goes straight to the dead-letter stream with
|
|
205
|
+
`x-dead-reason: poison` and `x-error-code: POISON_MESSAGE`, and no handler sees it.
|
|
206
|
+
5. Only codes travel: `x-error-code` is the `code` of the error or its class name. The message
|
|
207
|
+
of an error is never logged, reported or attached, because it may quote payload data.
|
|
208
|
+
|
|
209
|
+
**Why an explicit copy and not the advisory.** When a message reaches `max_deliver` the
|
|
210
|
+
server stops delivering it and emits `$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES`. That
|
|
211
|
+
advisory is a core NATS message: lost if nobody listens at that instant, it carries a stream
|
|
212
|
+
sequence and no failure code, and the message itself stays in the stream where no consumer
|
|
213
|
+
will read it again. The bus knows the failure code and knows it is the last attempt, so it
|
|
214
|
+
parks the message itself, with an acknowledged publication.
|
|
215
|
+
|
|
216
|
+
**Why `max_deliver` is twice `maxAttempts`.** The delivery count also grows when a delivery
|
|
217
|
+
ends without a verdict: a process killed mid-handler, an acknowledgement wait that expired,
|
|
218
|
+
a connection cut. With `max_deliver` equal to `maxAttempts`, such a delivery could use the
|
|
219
|
+
last one, and the server would then drop the message from the consumer silently. The bus
|
|
220
|
+
parks at `maxAttempts`; the server limit is a backstop above it. A message that exhausts
|
|
221
|
+
even that stays in the stream, undelivered: the advisory above is the alert to have.
|
|
222
|
+
|
|
223
|
+
Delivery is at least once. The copy-then-terminate order means a crash in between delivers
|
|
224
|
+
the message again and parks it again: the dead-letter stream keeps one copy, because the copy
|
|
225
|
+
has a message id of its own (`<durable>.<sequence>`).
|
|
226
|
+
|
|
227
|
+
On a lost connection the client reconnects by itself and its pull consumers resume: streams
|
|
228
|
+
and durable consumers live on the server, nothing has to be declared again. While the server
|
|
229
|
+
is away `publish` rejects at once with `BROKER_UNAVAILABLE`, and a publication in flight
|
|
230
|
+
rejects after `publishTimeoutMs` (5 s by default) with `PUBLISH_NOT_ACKNOWLEDGED`, so the
|
|
231
|
+
outbox relay keeps the records and retries them. Messages that were being handled are
|
|
232
|
+
delivered again after `ackWaitMs`. The bus opens the first connection itself, with an
|
|
233
|
+
exponential backoff (`reconnect`, 200 ms doubling up to 30 s), and a new one if the client
|
|
234
|
+
ever gives up.
|
|
235
|
+
|
|
236
|
+
**Credentials.** The URL or the options of `connection` are held by the function that opens the connection, in no field. A `NatsEventBus` that is logged, spread or
|
|
237
|
+
serialised shows `NatsEventBus [connection concealed]` and nothing else: its fields are not enumerable,
|
|
238
|
+
and `toJSON` and `util.inspect` answer that name. The failures it reports are codes. What the
|
|
239
|
+
client library itself shows when one of its own objects is inspected is not in the hands of
|
|
240
|
+
this package: do not log those.
|
|
241
|
+
|
|
242
|
+
**What a handler is told.** Next to the event, each handler receives the delivery
|
|
243
|
+
(`EventDelivery` of the kernel): `{ attempt, maxAttempts, system: 'nats', destination, consumer }`,
|
|
244
|
+
where `destination` is the subject. Handlers of the application ignore it; `@ddd-ts-kit/tracing` reads it
|
|
245
|
+
to make one CONSUMER span per attempt and to mark the redeliveries and the attempt after which
|
|
246
|
+
the event is buried. The bus names itself `messagingSystem: 'nats'`, and `traceparent` travels in
|
|
247
|
+
the envelope and as a real header, intact across a retry.
|
|
248
|
+
|
|
249
|
+
## Operations
|
|
250
|
+
|
|
251
|
+
What to watch:
|
|
252
|
+
|
|
253
|
+
| Signal | Where | Meaning |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| outbox `stats()` (`pending`, `failed`, `dead`) | the outbox store | the publishing side: see `@ddd-ts-kit/events` |
|
|
256
|
+
| `num_pending` of `<consumer>_<type>` | server (`nats consumer info`, the exporter) | the consumer lags or is stopped |
|
|
257
|
+
| `num_ack_pending` of the same | server | handled now or waiting for a redelivery; a plateau means a handler that keeps failing |
|
|
258
|
+
| messages in `<stream>-dead`, per subject | server (`nats stream subjects <stream>-dead`) | the dead-letter depth: must be 0, alert on anything else |
|
|
259
|
+
| `$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.<stream>.>` | server | a message the bus could not park before the server gave up on it |
|
|
260
|
+
| `onDelivery` counted by `outcome` | this bus | rate of `retried`, `dead`, `poison` against `acknowledged` |
|
|
261
|
+
| `onFailure` with `kind: 'connection'` | this bus | disconnections (`CONNECTION_LOST`) and requests the server refused (`JETSTREAM_<code>`) |
|
|
262
|
+
| `bus.deliveriesInFlight` | this bus | handlers running now; stuck above 0 means a handler that hangs |
|
|
263
|
+
|
|
264
|
+
Replaying dead messages, once the cause is fixed:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const replayed = await bus.replayDead({ consumer: 'billing', type: 'ordering.order.placed' }, 100);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
It moves up to that many messages of that consumer from the dead-letter stream back to the
|
|
271
|
+
subject they came from, oldest first, each with an acknowledged copy before it is deleted
|
|
272
|
+
from the dead-letter stream, and with the header `x-replay-for` naming the durable consumer.
|
|
273
|
+
The other consumers of this kit acknowledge and skip a message marked for someone else; a
|
|
274
|
+
consumer written without this kit sees the event a second time and must deduplicate. The
|
|
275
|
+
copy is a new message: its delivery count starts again. Look at `x-dead-reason` and
|
|
276
|
+
`x-error-code` first: replaying a poison message only kills it again.
|
|
277
|
+
|
|
278
|
+
To stop a consumer for good, delete its durable consumers: unsubscribing leaves them, and a
|
|
279
|
+
stream with `interest` retention would keep their messages for ever.
|
|
280
|
+
|
|
281
|
+
## Limits
|
|
282
|
+
|
|
283
|
+
- **No ordering**, see above.
|
|
284
|
+
- **One delay.** The wait is the same for every attempt. The `backoff` list of a JetStream
|
|
285
|
+
consumer is not used: it applies to expired acknowledgement waits, not to refusals.
|
|
286
|
+
- **A handler must finish within `ackWaitMs`** (30 s by default). Beyond it the server
|
|
287
|
+
delivers the message to another instance while the first still runs. The bus sends no
|
|
288
|
+
in-progress signal: raise `ackWaitMs` above the worst case of the handlers.
|
|
289
|
+
- **The duplicate window is short** and belongs to the stream: two minutes unless the stream
|
|
290
|
+
was created otherwise, and an existing stream keeps its own. It absorbs a relay retry, not
|
|
291
|
+
a record revived from the dead state of the outbox an hour later.
|
|
292
|
+
- **Retention is `limits`**, without limits: the events stream grows until you set a maximum
|
|
293
|
+
age or size on it. Do not switch it to `workqueue`: two consumers could no longer both get
|
|
294
|
+
every event. With `interest`, every durable consumer must exist before the first
|
|
295
|
+
publication it cares about.
|
|
296
|
+
- **A consumer created after a publication receives it** (deliver policy `all`, as long as
|
|
297
|
+
the stream retained it): a new consuming context replays the retained history of its types.
|
|
298
|
+
- **A redelivery runs every handler of the durable consumer again**, those that succeeded
|
|
299
|
+
included, when several handlers of one consumer subscribe to the same type. Each must be
|
|
300
|
+
idempotent.
|
|
301
|
+
- **A batch is not atomic.** When `publish` rejects, some events of the batch may have been
|
|
302
|
+
stored. The relay publishes one by one and retries; the stream deduplicates, then consumers.
|
|
303
|
+
- **An event no consumer listens to is stored anyway**: there is no equivalent of
|
|
304
|
+
`onUnroutable`.
|
|
305
|
+
- **Two names that differ only by a replaced character collide**: `billing.eu` and
|
|
306
|
+
`billing_eu` are the same durable consumer.
|
|
307
|
+
- **`replayDead` reads the dead-letter stream directly**, which needs `allow_direct` on it:
|
|
308
|
+
the bus sets it on the stream it creates; set it yourself on one you created.
|
|
309
|
+
- **A connection factory must open a new connection at each call** and should keep the
|
|
310
|
+
reconnection of the client unlimited (`maxReconnectAttempts: -1`, what the bus sets when it
|
|
311
|
+
is given a URL); the bus closes what it opened.
|
|
312
|
+
- Closing while the server is away does not wait for the acknowledgements in flight to be
|
|
313
|
+
sent: those messages are delivered again.
|
|
314
|
+
|
|
315
|
+
Not verified by the test suite: a cluster of several servers (replicas above 1, leader
|
|
316
|
+
election, a stream that loses its quorum), TLS and authentication, an acknowledgement wait
|
|
317
|
+
that expires during a handler, a consumer process killed mid-handler, the `MAX_DELIVERIES`
|
|
318
|
+
path beyond the bus limit, a durable consumer or a stream deleted while the bus runs (the bus
|
|
319
|
+
then reopens its connection and declares everything again; the path is not exercised),
|
|
320
|
+
`interest` retention, servers older than 2.15, and the WebSocket transport.
|
|
321
|
+
|
|
322
|
+
## Other providers
|
|
323
|
+
|
|
324
|
+
`@ddd-ts-kit/rabbitmq` and `@ddd-ts-kit/kafka` implement the same two ports and pass the same
|
|
325
|
+
contract suite. The three share the envelope, the header names (`traceparent`,
|
|
326
|
+
`x-correlation-id`, `x-event-version`, `x-attempt`, `x-error-code`, `x-dead-reason`), the
|
|
327
|
+
option names (`consumer`, `retry`, `reconnect`, `onFailure`, `onDelivery`) and the methods
|
|
328
|
+
(`forConsumer`, `assertTopology`, `describeTopology`, `replayDead`, `close`).
|
|
329
|
+
|
|
330
|
+
## Testing
|
|
331
|
+
|
|
332
|
+
The suites need a real server and are skipped without `DDD_TS_KIT_TEST_NATS_URL`:
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
docker run -d --rm --name ddd-ts-kit-test-nats -p 127.0.0.1:4222:4222 nats:2.15.0-alpine -js
|
|
336
|
+
DDD_TS_KIT_TEST_NATS_URL=nats://127.0.0.1:4222 npx vitest run packages/nats
|
|
337
|
+
docker rm -f ddd-ts-kit-test-nats
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
They run the contract suite on file and on memory storage, then the behaviours of this
|
|
341
|
+
provider, one suite per behaviour (`publishing`, with the deduplication inside and outside
|
|
342
|
+
the duplicate window, `consuming`, `correlation`, `retries`, `dead-letters`, `topology`,
|
|
343
|
+
`connection-loss` through a TCP relay the test cuts, `closing`) and the whole chain from the
|
|
344
|
+
outbox to an idempotent consumer. Every test works on streams of its own and
|
|
345
|
+
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 body that is not one (poison). */
|
|
3
|
+
export declare function decode(data: Uint8Array): 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,6FAA6F;AAC7F,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,gBAAgB,GAAG,SAAS,CAgBrE"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The envelope exactly as published, or `undefined` for a body that is not one (poison). */
|
|
2
|
+
export function decode(data) {
|
|
3
|
+
const body = parsed(data);
|
|
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(data) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
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,6FAA6F;AAC7F,MAAM,UAAU,MAAM,CAAC,IAAgB;IACrC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAE1B,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,IAAgB;IAC9B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,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,8 @@
|
|
|
1
|
+
import type { IntegrationEvent } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import type { Publication } from './publication.js';
|
|
3
|
+
/**
|
|
4
|
+
* The body is the whole envelope as JSON, so any NATS client can consume it. The headers
|
|
5
|
+
* repeat what the stream, a tracer or an operator needs without opening the body.
|
|
6
|
+
*/
|
|
7
|
+
export declare function encode(event: IntegrationEvent): Publication;
|
|
8
|
+
//# sourceMappingURL=encode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/codec/encode.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAe3D"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { headers as emptyHeaders } from '@nats-io/transport-node';
|
|
2
|
+
import { CORRELATION_HEADER, EVENT_TYPE_HEADER, EVENT_VERSION_HEADER, TRACEPARENT_HEADER } from './headers.js';
|
|
3
|
+
/**
|
|
4
|
+
* The body is the whole envelope as JSON, so any NATS client can consume it. The headers
|
|
5
|
+
* repeat what the stream, a tracer or an operator needs without opening the body.
|
|
6
|
+
*/
|
|
7
|
+
export function encode(event) {
|
|
8
|
+
const headers = emptyHeaders();
|
|
9
|
+
headers.set(EVENT_TYPE_HEADER, event.type);
|
|
10
|
+
headers.set(EVENT_VERSION_HEADER, String(event.version));
|
|
11
|
+
if (event.traceparent !== undefined) {
|
|
12
|
+
headers.set(TRACEPARENT_HEADER, event.traceparent);
|
|
13
|
+
}
|
|
14
|
+
if (event.correlationId !== undefined) {
|
|
15
|
+
headers.set(CORRELATION_HEADER, event.correlationId);
|
|
16
|
+
}
|
|
17
|
+
return { data: JSON.stringify(event), messageId: event.id, headers };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=encode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.js","sourceRoot":"","sources":["../../src/codec/encode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAElE,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAG/G;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,KAAuB;IAC5C,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;IAE/B,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IAEzD,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACpC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IACrD,CAAC;IAED,IAAI,KAAK,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The header the stream deduplicates on: the event id. */
|
|
2
|
+
export declare const MESSAGE_ID_HEADER = "Nats-Msg-Id";
|
|
3
|
+
export declare const EVENT_TYPE_HEADER = "x-event-type";
|
|
4
|
+
export declare const EVENT_VERSION_HEADER = "x-event-version";
|
|
5
|
+
export declare const TRACEPARENT_HEADER = "traceparent";
|
|
6
|
+
export declare const CORRELATION_HEADER = "x-correlation-id";
|
|
7
|
+
export declare const ATTEMPT_HEADER = "x-attempt";
|
|
8
|
+
export declare const ERROR_CODE_HEADER = "x-error-code";
|
|
9
|
+
export declare const DEAD_REASON_HEADER = "x-dead-reason";
|
|
10
|
+
/** Where a dead message came from: `<stream>/<sequence>`. */
|
|
11
|
+
export declare const SOURCE_HEADER = "x-source";
|
|
12
|
+
/** Set on a replayed message: the one durable consumer it is meant for. The others skip it. */
|
|
13
|
+
export declare const REPLAY_FOR_HEADER = "x-replay-for";
|
|
14
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/codec/headers.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAC/C,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,6DAA6D;AAC7D,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,+FAA+F;AAC/F,eAAO,MAAM,iBAAiB,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The header the stream deduplicates on: the event id. */
|
|
2
|
+
export const MESSAGE_ID_HEADER = 'Nats-Msg-Id';
|
|
3
|
+
export const EVENT_TYPE_HEADER = 'x-event-type';
|
|
4
|
+
export const EVENT_VERSION_HEADER = 'x-event-version';
|
|
5
|
+
export const TRACEPARENT_HEADER = 'traceparent';
|
|
6
|
+
export const CORRELATION_HEADER = 'x-correlation-id';
|
|
7
|
+
export const ATTEMPT_HEADER = 'x-attempt';
|
|
8
|
+
export const ERROR_CODE_HEADER = 'x-error-code';
|
|
9
|
+
export const DEAD_REASON_HEADER = 'x-dead-reason';
|
|
10
|
+
/** Where a dead message came from: `<stream>/<sequence>`. */
|
|
11
|
+
export const SOURCE_HEADER = 'x-source';
|
|
12
|
+
/** Set on a replayed message: the one durable consumer it is meant for. The others skip it. */
|
|
13
|
+
export const REPLAY_FOR_HEADER = 'x-replay-for';
|
|
14
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/codec/headers.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAC;AAC/C,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,6DAA6D;AAC7D,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AACxC,+FAA+F;AAC/F,MAAM,CAAC,MAAM,iBAAiB,GAAG,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publication.d.ts","sourceRoot":"","sources":["../../src/codec/publication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publication.js","sourceRoot":"","sources":["../../src/codec/publication.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type MsgHdrs } from '@nats-io/transport-node';
|
|
2
|
+
import type { Publication } from './publication.js';
|
|
3
|
+
export interface Republished {
|
|
4
|
+
readonly data: Uint8Array;
|
|
5
|
+
readonly headers: MsgHdrs | undefined;
|
|
6
|
+
/** Unique for this copy: the stream must not take it for a duplicate of the original. */
|
|
7
|
+
readonly messageId: string;
|
|
8
|
+
}
|
|
9
|
+
/** The same message, for another subject, with `added` headers set or replaced. */
|
|
10
|
+
export declare function republication(message: Republished, added: Record<string, string>): Publication;
|
|
11
|
+
//# 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,EAA2B,KAAK,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,yFAAyF;IACzF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,WAAW,CAc9F"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { headers as emptyHeaders } from '@nats-io/transport-node';
|
|
2
|
+
import { MESSAGE_ID_HEADER } from './headers.js';
|
|
3
|
+
/** The same message, for another subject, with `added` headers set or replaced. */
|
|
4
|
+
export function republication(message, added) {
|
|
5
|
+
const headers = emptyHeaders();
|
|
6
|
+
for (const [name, values] of message.headers ?? []) {
|
|
7
|
+
for (const value of name === MESSAGE_ID_HEADER ? [] : values) {
|
|
8
|
+
headers.append(name, value);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
for (const [name, value] of Object.entries(added)) {
|
|
12
|
+
headers.set(name, value);
|
|
13
|
+
}
|
|
14
|
+
return { data: message.data, messageId: message.messageId, headers };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=republication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"republication.js","sourceRoot":"","sources":["../../src/codec/republication.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAgB,MAAM,yBAAyB,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAUjD,mFAAmF;AACnF,MAAM,UAAU,aAAa,CAAC,OAAoB,EAAE,KAA6B;IAC/E,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;IAE/B,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACnD,KAAK,MAAM,KAAK,IAAI,IAAI,KAAK,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;YAC7D,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import type { NatsConnection } from '@nats-io/transport-node';
|
|
3
|
+
import type { ReconnectOptions } from '../options/reconnect-options.js';
|
|
4
|
+
export interface LinkHooks {
|
|
5
|
+
/** Sets streams and consumers up on a fresh connection. A failure retries. */
|
|
6
|
+
opened(connection: NatsConnection): Promise<void>;
|
|
7
|
+
/** The connection is closed for good: what was set up on it is void. */
|
|
8
|
+
lost(): void;
|
|
9
|
+
failed(code: string): void;
|
|
10
|
+
}
|
|
11
|
+
export interface LinkSettings {
|
|
12
|
+
readonly open: () => Promise<NatsConnection>;
|
|
13
|
+
readonly reconnect: Required<ReconnectOptions>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The one connection of a bus. The client reconnects by itself and resumes its consumers;
|
|
17
|
+
* the link opens the first connection, with a delay that grows with each failed attempt,
|
|
18
|
+
* knows when the server is away, and opens another connection if the client ever gives up.
|
|
19
|
+
*/
|
|
20
|
+
export declare class BrokerLink {
|
|
21
|
+
private readonly settings;
|
|
22
|
+
private readonly hooks;
|
|
23
|
+
private connection;
|
|
24
|
+
private opening;
|
|
25
|
+
private retry;
|
|
26
|
+
private failures;
|
|
27
|
+
private away;
|
|
28
|
+
private closed;
|
|
29
|
+
constructor(settings: LinkSettings, hooks: LinkHooks);
|
|
30
|
+
/**
|
|
31
|
+
* The live connection. Opens the first one, joins an attempt in progress, and fails at
|
|
32
|
+
* once while the server is away so that a publisher never hangs on it.
|
|
33
|
+
*/
|
|
34
|
+
opened(): Promise<NatsConnection>;
|
|
35
|
+
/** Drops the connection so that everything is rebuilt, after a consumer died on its own. */
|
|
36
|
+
restart(): void;
|
|
37
|
+
/** Sends what is buffered, acknowledgements included, unless the server is away; disconnects. */
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
toJSON(): string;
|
|
40
|
+
[inspect.custom](): string;
|
|
41
|
+
private present;
|
|
42
|
+
private open;
|
|
43
|
+
private establish;
|
|
44
|
+
private connect;
|
|
45
|
+
private watch;
|
|
46
|
+
private lost;
|
|
47
|
+
private failed;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=broker-link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broker-link.d.ts","sourceRoot":"","sources":["../../src/connection/broker-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAK9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAGxE,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,MAAM,CAAC,UAAU,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,wEAAwE;IACxE,IAAI,IAAI,IAAI,CAAC;IACb,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;CAChD;AAID;;;;GAIG;AACH,qBAAa,UAAU;IAcnB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAdxB,OAAO,CAAC,UAAU,CAA6B;IAE/C,OAAO,CAAC,OAAO,CAAsC;IAErD,OAAO,CAAC,KAAK,CAA4C;IAEzD,OAAO,CAAC,QAAQ,CAAK;IAErB,OAAO,CAAC,IAAI,CAAS;IAErB,OAAO,CAAC,MAAM,CAAS;IAEvB,YACmB,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,SAAS,EAGlC;IAED;;;OAGG;IACG,MAAM,IAAI,OAAO,CAAC,cAAc,CAAC,CAkBtC;IAED,4FAA4F;IAC5F,OAAO,IAAI,IAAI,CAOd;IAED,iGAAiG;IAC3F,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAa3B;IAED,MAAM,IAAI,MAAM,CAEf;IAED,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAEzB;IAED,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,IAAI;YAmBE,SAAS;YAuBT,OAAO;YASP,KAAK;IAmBnB,OAAO,CAAC,IAAI;IAaZ,OAAO,CAAC,MAAM;CAgBf"}
|