@nestjs-transactional/outbox-microservices 1.0.0-alpha.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -266
- package/dist/externalizer/microservices-event-externalizer.d.ts +1 -1
- package/dist/externalizer/microservices-event-externalizer.js +7 -7
- package/dist/externalizer/microservices-event-externalizer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/module/outbox-microservices.module.d.ts +3 -3
- package/dist/module/outbox-microservices.module.js +3 -3
- package/dist/module/outbox-microservices.module.js.map +1 -1
- package/package.json +25 -15
package/README.md
CHANGED
|
@@ -1,173 +1,72 @@
|
|
|
1
1
|
# @nestjs-transactional/outbox-microservices
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@nestjs-transactional/outbox-microservices)
|
|
4
4
|
[](https://github.com/igorgolovanov/nestjs-transactional/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
##
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
##
|
|
53
|
-
|
|
54
|
-
**Read this before adopting the package in production.** The
|
|
55
|
-
`@nestjs/microservices` `ClientProxy.emit()` API this package depends
|
|
56
|
-
on (per [DD-017](../../docs/dd/017-reuse-clients-module.md)) does NOT propagate broker-side
|
|
57
|
-
delivery failures in a way the externalizer can observe. In
|
|
58
|
-
fire-and-forget mode the Observable returned by `emit()` completes
|
|
59
|
-
when the proxy considers the dispatch *handed off to the transport*,
|
|
60
|
-
not when the broker has *durably acknowledged* the message.
|
|
61
|
-
|
|
62
|
-
Concretely, this means:
|
|
63
|
-
|
|
64
|
-
- A `ClientKafka` configured against an unreachable broker can resolve
|
|
65
|
-
`emit()` successfully, the externalizer reports success, and the
|
|
66
|
-
outbox publication is finalised as `COMPLETED` — even though no
|
|
67
|
-
message ever reached a broker.
|
|
68
|
-
- The same applies to RabbitMQ in default fire-and-forget mode and to
|
|
69
|
-
any other transport `@nestjs/microservices` supports.
|
|
70
|
-
- Silent broker failures bypass the outbox retry / staleness /
|
|
71
|
-
resubmit machinery: there is nothing to retry, because as far as
|
|
72
|
-
this layer is concerned delivery succeeded.
|
|
73
|
-
|
|
74
|
-
The outbox still gives you crash-consistent **enqueueing** of events
|
|
75
|
-
and at-least-once **local listener** delivery. What it does NOT give
|
|
76
|
-
you, in this version, is at-least-once **broker-side** delivery
|
|
77
|
-
through `ClientProxy`.
|
|
78
|
-
|
|
79
|
-
[ADR-016](../../docs/adr/016-externalization-reliability-semantics.md)
|
|
80
|
-
documents the finding in full and lays out the future path
|
|
81
|
-
(broker-aware externalizers using native producers under the same
|
|
82
|
-
`EVENT_EXTERNALIZER` SPI).
|
|
83
|
-
[ADR-015](../../docs/adr/015-event-externalization-architecture.md)
|
|
84
|
-
records why this trade-off is acceptable for the v1 scope —
|
|
85
|
-
[`docs/architecture/event-externalization.md`](../../docs/architecture/event-externalization.md)
|
|
86
|
-
has the full sequence diagram and the failure-mode table.
|
|
87
|
-
|
|
88
|
-
### Mitigation strategies for production
|
|
89
|
-
|
|
90
|
-
1. **Configure the underlying `ClientProxy` for stronger
|
|
91
|
-
acknowledgment.** Kafka: `producer.acks: 'all'` plus
|
|
92
|
-
`producer.idempotent: true`. RabbitMQ: confirm-channel via
|
|
93
|
-
`amqp-connection-manager`. NATS: JetStream with explicit ack. The
|
|
94
|
-
package reuses whatever proxy you registered (DD-017) — it does
|
|
95
|
-
not interfere with this configuration.
|
|
96
|
-
|
|
97
|
-
2. **Combine with consumer-side acknowledgment / inbox patterns.**
|
|
98
|
-
Track processed message ids on the receiving system and surface
|
|
99
|
-
gaps to operators. The outbox publication's listener id plus the
|
|
100
|
-
domain event id is enough to deduplicate.
|
|
101
|
-
|
|
102
|
-
3. **Wait for the broker-aware externalizer iteration** when neither
|
|
103
|
-
of the above is feasible. The
|
|
104
|
-
[`EVENT_EXTERNALIZER` SPI](../outbox/src/externalization/event-externalizer.ts)
|
|
105
|
-
is stable; native adapters will plug into the same place without
|
|
106
|
-
client-side changes.
|
|
107
|
-
|
|
108
|
-
## Installation
|
|
6
|
+
Forwards outbox events to a message broker through
|
|
7
|
+
`@nestjs/microservices`.
|
|
8
|
+
|
|
9
|
+
Mark an event `@Externalized`, and once its local handlers have
|
|
10
|
+
completed, [`@nestjs-transactional/outbox`](https://www.npmjs.com/package/@nestjs-transactional/outbox)
|
|
11
|
+
hands it to this package, which emits it over a `ClientProxy` you
|
|
12
|
+
already configured. One implementation covers every transport
|
|
13
|
+
`@nestjs/microservices` supports — Kafka, RabbitMQ, MQTT, Redis, NATS,
|
|
14
|
+
and custom strategies. gRPC is the exception; see below.
|
|
15
|
+
|
|
16
|
+
## What a successful publish means on your transport
|
|
17
|
+
|
|
18
|
+
A publication is marked `COMPLETED` when `emit()` resolves, and what
|
|
19
|
+
`emit()` waits for is not the same on every transport. Kafka and
|
|
20
|
+
RabbitMQ wait for a real broker acknowledgement, so an unreachable
|
|
21
|
+
broker marks the publication `FAILED` and the outbox's retry and
|
|
22
|
+
resubmit machinery engages. NATS and TCP do not wait for anything.
|
|
23
|
+
|
|
24
|
+
| Transport | `emit()` resolves when | Acknowledged by the broker? |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Kafka | `producer.send()` settles; `kafkajs` defaults to `acks: -1`, every in-sync replica | Yes |
|
|
27
|
+
| RabbitMQ | the publisher confirm arrives (`amqp-connection-manager` enables confirms by default) | Yes, but set `persistent: true` |
|
|
28
|
+
| MQTT | PUBACK at QoS 1 and above, immediately at QoS 0 | At QoS 1 and above |
|
|
29
|
+
| Redis | the `PUBLISH` command replies | The server got it, but Redis pub/sub does not persist: only live subscribers receive it |
|
|
30
|
+
| TCP | the message is written to the socket | No |
|
|
31
|
+
| NATS | immediately: core `publish()` returns `void`, and the client resolves unconditionally | No |
|
|
32
|
+
| gRPC | never: `dispatchEvent` throws `Method is not supported in gRPC mode` | Not usable for externalization |
|
|
33
|
+
|
|
34
|
+
Two things worth acting on:
|
|
35
|
+
|
|
36
|
+
- **RabbitMQ publishes non-persistent by default.** NestJS defaults
|
|
37
|
+
`persistent` to `false`, and RabbitMQ confirms a non-persistent
|
|
38
|
+
message without writing it to disk, so a broker restart loses it.
|
|
39
|
+
Pass `persistent: true` in your `ClientsModule.register()` options.
|
|
40
|
+
- **NATS gives you no delivery signal at all.** Core NATS publish is
|
|
41
|
+
fire-and-forget by protocol. If you need a guarantee there, this
|
|
42
|
+
externalizer is not the right one; the `EVENT_EXTERNALIZER` SPI is
|
|
43
|
+
public and a JetStream-based implementation slots into the same
|
|
44
|
+
place.
|
|
45
|
+
|
|
46
|
+
The outbox itself guarantees crash-consistent **enqueueing** and
|
|
47
|
+
at-least-once delivery to **local** handlers regardless of transport.
|
|
48
|
+
Measurements, the reading of each client's `dispatchEvent`, and what
|
|
49
|
+
remains genuinely unguaranteed:
|
|
50
|
+
[ADR-021](https://github.com/igorgolovanov/nestjs-transactional/blob/main/docs/adr/021-externalization-acknowledgement-per-transport.md).
|
|
51
|
+
|
|
52
|
+
## Install
|
|
109
53
|
|
|
110
54
|
```bash
|
|
111
55
|
pnpm add @nestjs-transactional/outbox-microservices @nestjs-transactional/outbox @nestjs/microservices
|
|
112
56
|
```
|
|
113
57
|
|
|
114
|
-
|
|
115
|
-
`reflect-metadata`, and `rxjs` are peer dependencies (already present
|
|
116
|
-
in any NestJS application).
|
|
58
|
+
## Quick start
|
|
117
59
|
|
|
118
|
-
|
|
60
|
+
This package does not create broker connections — you register clients
|
|
61
|
+
the standard way, and it reuses them.
|
|
119
62
|
|
|
120
|
-
|
|
121
|
-
job (DD-017). Configure them through the standard
|
|
122
|
-
`@nestjs/microservices` `ClientsModule`:
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
63
|
+
```ts
|
|
125
64
|
import { ClientsModule, Transport } from '@nestjs/microservices';
|
|
126
|
-
|
|
127
|
-
@Module({
|
|
128
|
-
imports: [
|
|
129
|
-
ClientsModule.register([
|
|
130
|
-
{
|
|
131
|
-
name: 'KAFKA_CLIENT',
|
|
132
|
-
transport: Transport.KAFKA,
|
|
133
|
-
options: {
|
|
134
|
-
client: { brokers: ['localhost:9092'] },
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
]),
|
|
138
|
-
],
|
|
139
|
-
})
|
|
140
|
-
export class AppModule {}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
`OutboxMicroservicesModule.forRoot({ defaultClient: 'KAFKA_CLIENT' })`
|
|
144
|
-
then resolves the same proxy via `ModuleRef.get(token, { strict: false })`
|
|
145
|
-
when an outbox publication is ready to be externalized — no parallel
|
|
146
|
-
connection pool, no second mental model.
|
|
147
|
-
|
|
148
|
-
## Basic example
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
import { Module } from '@nestjs/common';
|
|
152
|
-
import { ClientsModule, Transport } from '@nestjs/microservices';
|
|
153
|
-
import { TransactionalModule } from '@nestjs-transactional/core';
|
|
154
65
|
import { Externalized, OutboxModule } from '@nestjs-transactional/outbox';
|
|
155
66
|
import { OutboxMicroservicesModule } from '@nestjs-transactional/outbox-microservices';
|
|
156
67
|
|
|
157
|
-
@Externalized<OrderPlacedEvent>({
|
|
158
|
-
target: 'orders.placed',
|
|
159
|
-
routingKey: (e) => e.tenantId, // see Limitations — logged, not yet applied
|
|
160
|
-
})
|
|
161
|
-
export class OrderPlacedEvent {
|
|
162
|
-
constructor(
|
|
163
|
-
readonly orderId: string,
|
|
164
|
-
readonly tenantId: string,
|
|
165
|
-
) {}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
68
|
@Module({
|
|
169
69
|
imports: [
|
|
170
|
-
TransactionalModule.forRoot({ isGlobal: true }),
|
|
171
70
|
ClientsModule.register([
|
|
172
71
|
{
|
|
173
72
|
name: 'KAFKA_CLIENT',
|
|
@@ -175,137 +74,85 @@ export class OrderPlacedEvent {
|
|
|
175
74
|
options: { client: { brokers: ['localhost:9092'] } },
|
|
176
75
|
},
|
|
177
76
|
]),
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}),
|
|
77
|
+
|
|
78
|
+
// ...the usual outbox wiring...
|
|
79
|
+
|
|
80
|
+
OutboxMicroservicesModule.forRoot({ defaultClient: 'KAFKA_CLIENT' }),
|
|
183
81
|
],
|
|
184
82
|
})
|
|
185
83
|
export class AppModule {}
|
|
186
84
|
```
|
|
187
85
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
86
|
+
```ts
|
|
87
|
+
@Externalized({ target: 'orders.placed' })
|
|
88
|
+
export class OrderPlacedEvent {
|
|
89
|
+
constructor(public readonly orderId: string) {}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
194
92
|
|
|
195
|
-
|
|
93
|
+
That is the whole integration: events without `@Externalized` stay
|
|
94
|
+
local, and events with it are emitted after their local handlers
|
|
95
|
+
finish. The proxy is resolved at publication time through `ModuleRef`,
|
|
96
|
+
so there is no second connection pool and no parallel configuration to
|
|
97
|
+
keep in sync
|
|
98
|
+
([DD-017](https://github.com/igorgolovanov/nestjs-transactional/blob/main/docs/dd/017-reuse-clients-module.md)).
|
|
196
99
|
|
|
197
|
-
|
|
198
|
-
each event at its broker via the `client` option on `@Externalized`:
|
|
100
|
+
## Routing to several brokers
|
|
199
101
|
|
|
200
|
-
|
|
201
|
-
ClientsModule.register([
|
|
202
|
-
{ name: 'KAFKA_CLIENT', transport: Transport.KAFKA, options: { ... } },
|
|
203
|
-
{ name: 'AMQP_CLIENT', transport: Transport.RMQ, options: { ... } },
|
|
204
|
-
]),
|
|
102
|
+
Name a client per event; `defaultClient` covers the rest.
|
|
205
103
|
|
|
104
|
+
```ts
|
|
206
105
|
@Externalized({ target: 'orders.placed', client: 'KAFKA_CLIENT' })
|
|
207
|
-
class OrderPlacedEvent {
|
|
106
|
+
export class OrderPlacedEvent {}
|
|
208
107
|
|
|
209
|
-
@Externalized({ target: '
|
|
210
|
-
class
|
|
108
|
+
@Externalized({ target: 'billing.invoice', client: 'RABBIT_CLIENT' })
|
|
109
|
+
export class InvoiceIssuedEvent {}
|
|
211
110
|
```
|
|
212
111
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
`
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
`
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
## Async configuration
|
|
239
|
-
|
|
240
|
-
For `defaultClient` that must be resolved from a `ConfigService`:
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
OutboxMicroservicesModule.forRootAsync({
|
|
244
|
-
imports: [ConfigModule],
|
|
245
|
-
inject: [ConfigService],
|
|
246
|
-
useFactory: (config: ConfigService) => ({
|
|
247
|
-
defaultClient: config.getOrThrow<string>('outbox.defaultClient'),
|
|
248
|
-
}),
|
|
249
|
-
}),
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
## Bootstrap validation
|
|
253
|
-
|
|
254
|
-
By default the module resolves `defaultClient` once at
|
|
255
|
-
`OnApplicationBootstrap` and throws a descriptive error if the token
|
|
256
|
-
is unbound — the misconfiguration surfaces before the first event is
|
|
257
|
-
processed. Disable with `validateOnBootstrap: false` when the
|
|
258
|
-
`ClientProxy` registration is wired by an asynchronous factory that
|
|
259
|
-
finishes after the outbox bootstrap (the lookup is then deferred to
|
|
260
|
-
the first `externalize()` call).
|
|
112
|
+
An unresolvable client name fails at bootstrap rather than at the first
|
|
113
|
+
publication, so a typo surfaces on deploy instead of in the middle of
|
|
114
|
+
the night. Pass `validateOnBootstrap: false` to defer resolution if you
|
|
115
|
+
register clients late.
|
|
116
|
+
|
|
117
|
+
## Hardening the delivery
|
|
118
|
+
|
|
119
|
+
The defaults are reasonable on Kafka and RabbitMQ. What is left is
|
|
120
|
+
mostly about not weakening them, and about the consumer side.
|
|
121
|
+
|
|
122
|
+
1. **Do not configure the acknowledgement away.** Kafka `acks: 0`,
|
|
123
|
+
MQTT QoS 0, and RabbitMQ without `persistent: true` each give up a
|
|
124
|
+
guarantee you had for free. `producer.idempotent: true` on Kafka
|
|
125
|
+
additionally protects against duplicates from producer retries.
|
|
126
|
+
This package reuses whatever proxy you registered and does not
|
|
127
|
+
interfere with any of it.
|
|
128
|
+
2. **Deduplicate on the consumer.** At-least-once means duplicates are
|
|
129
|
+
expected, not exceptional. Track processed message ids on the
|
|
130
|
+
receiving side and alert on gaps. The listener id plus the event id
|
|
131
|
+
is enough to identify a message.
|
|
132
|
+
3. **Watch the `FAILED` publications.** A broker rejection now reaches
|
|
133
|
+
you as a `FAILED` row with a readable `failureReason`, which is what
|
|
134
|
+
`FailedEventPublications.resubmit` and the retry scheduler act on.
|
|
135
|
+
That path is only useful if someone is looking at it.
|
|
261
136
|
|
|
262
137
|
## Limitations
|
|
263
138
|
|
|
264
|
-
- **Headers and `routingKey
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
debug level for visibility; the broker-aware message-construction
|
|
270
|
-
iteration ships in a later release. Wrap the event in a
|
|
271
|
-
transport-specific envelope inside your own code if you need them
|
|
272
|
-
before then.
|
|
273
|
-
- **Real-broker integration tests** (Postgres + Kafka / RabbitMQ via
|
|
274
|
-
testcontainers) are not bundled with this package — the unit and
|
|
275
|
-
module specs use a mock `ClientProxy` and cover the SPI contract
|
|
276
|
-
end-to-end without a live broker. The
|
|
277
|
-
[`externalization-with-fallback`](../../examples/externalization-with-fallback/)
|
|
278
|
-
example demonstrates a real-broker setup via docker-compose with
|
|
279
|
-
the ADR-016 silent-success limitation observable end-to-end.
|
|
280
|
-
|
|
281
|
-
## Testing
|
|
282
|
-
|
|
283
|
-
The package's own tests use a mock `ClientProxy` directly. To exercise
|
|
284
|
-
the externalizer in your application's tests, register a stub provider
|
|
285
|
-
under your client's token before the module imports:
|
|
286
|
-
|
|
287
|
-
```typescript
|
|
288
|
-
const moduleRef = await Test.createTestingModule({
|
|
289
|
-
imports: [
|
|
290
|
-
/* ... ClientsModule.register([{ name: 'KAFKA_CLIENT', ... }]) */
|
|
291
|
-
OutboxMicroservicesModule.forRoot({ defaultClient: 'KAFKA_CLIENT' }),
|
|
292
|
-
],
|
|
293
|
-
})
|
|
294
|
-
.overrideProvider('KAFKA_CLIENT')
|
|
295
|
-
.useValue({ emit: jest.fn(() => of(undefined)) })
|
|
296
|
-
.compile();
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
## Worked examples
|
|
139
|
+
- **Headers and `routingKey`** are accepted by `@Externalized` but not
|
|
140
|
+
yet applied to the emitted payload.
|
|
141
|
+
- **gRPC cannot be used** as an externalization transport:
|
|
142
|
+
`ClientGrpcProxy.dispatchEvent` throws.
|
|
143
|
+
- **NATS and TCP give no delivery signal** — see the table at the top.
|
|
300
144
|
|
|
301
|
-
|
|
302
|
-
- [`externalization-multi-broker`](../../examples/externalization-multi-broker) — Kafka + RabbitMQ + Redis pub/sub routed per event via `@Externalized({ client })`.
|
|
303
|
-
- [`externalization-multi-datasource`](../../examples/externalization-multi-datasource) — two physical Postgres × two `ClientProxy` registrations on a single broker.
|
|
304
|
-
- [`externalization-with-fallback`](../../examples/externalization-with-fallback) — ADR-016 silent-success demo + the three production mitigation patterns + `FailedEventPublications.resubmit` recovery flow.
|
|
305
|
-
- [`e-commerce-orders`](../../examples/e-commerce-orders) — flagship application; externalization is the terminal step of the order saga.
|
|
145
|
+
## Documentation
|
|
306
146
|
|
|
307
|
-
|
|
147
|
+
- [Getting started and full docs](https://github.com/igorgolovanov/nestjs-transactional#readme)
|
|
148
|
+
- [Acknowledgement per transport (ADR-021)](https://github.com/igorgolovanov/nestjs-transactional/blob/main/docs/adr/021-externalization-acknowledgement-per-transport.md)
|
|
149
|
+
- [Externalization architecture (ADR-015)](https://github.com/igorgolovanov/nestjs-transactional/blob/main/docs/adr/015-event-externalization-architecture.md)
|
|
150
|
+
- [Architecture: event externalization](https://github.com/igorgolovanov/nestjs-transactional/blob/main/docs/architecture/event-externalization.md)
|
|
151
|
+
- Runnable examples:
|
|
152
|
+
[`externalization-kafka`](https://github.com/igorgolovanov/nestjs-transactional/tree/main/examples/externalization-kafka),
|
|
153
|
+
[`externalization-multi-broker`](https://github.com/igorgolovanov/nestjs-transactional/tree/main/examples/externalization-multi-broker),
|
|
154
|
+
[`externalization-with-fallback`](https://github.com/igorgolovanov/nestjs-transactional/tree/main/examples/externalization-with-fallback)
|
|
308
155
|
|
|
309
156
|
## License
|
|
310
157
|
|
|
311
|
-
MIT
|
|
158
|
+
MIT
|
|
@@ -15,7 +15,7 @@ import { type OutboxMicroservicesOptions } from '../types/options';
|
|
|
15
15
|
* overrides via `@Externalized({ client })` resolve through the same
|
|
16
16
|
* `ModuleRef.get(token, { strict: false })` lookup.
|
|
17
17
|
*
|
|
18
|
-
* **Headers / routingKey limitation
|
|
18
|
+
* **Headers / routingKey limitation:** the
|
|
19
19
|
* `@nestjs/microservices` `ClientProxy.emit` API has no unified
|
|
20
20
|
* headers / routing-key parameter — handling is transport-specific
|
|
21
21
|
* (Kafka headers, AMQP properties, NATS subject suffixes, ...). For
|
|
@@ -32,7 +32,7 @@ const options_1 = require("../types/options");
|
|
|
32
32
|
* overrides via `@Externalized({ client })` resolve through the same
|
|
33
33
|
* `ModuleRef.get(token, { strict: false })` lookup.
|
|
34
34
|
*
|
|
35
|
-
* **Headers / routingKey limitation
|
|
35
|
+
* **Headers / routingKey limitation:** the
|
|
36
36
|
* `@nestjs/microservices` `ClientProxy.emit` API has no unified
|
|
37
37
|
* headers / routing-key parameter — handling is transport-specific
|
|
38
38
|
* (Kafka headers, AMQP properties, NATS subject suffixes, ...). For
|
|
@@ -64,12 +64,12 @@ let MicroservicesEventExternalizer = MicroservicesEventExternalizer_1 = class Mi
|
|
|
64
64
|
this.logger.log(`Externalization configured with default client: ${formatToken(this.options.defaultClient)}`);
|
|
65
65
|
}
|
|
66
66
|
catch (err) {
|
|
67
|
-
const
|
|
67
|
+
const reason = (0, outbox_1.describeThrown)(err);
|
|
68
68
|
throw new Error(`OutboxMicroservicesModule: defaultClient '${formatToken(this.options.defaultClient)}' ` +
|
|
69
69
|
`is not registered in the DI container. Register the ClientProxy via ` +
|
|
70
70
|
`ClientsModule.register() / ClientsModule.registerAsync(), or pass ` +
|
|
71
71
|
`validateOnBootstrap: false to defer resolution to the first event. ` +
|
|
72
|
-
`Original error: ${cause}
|
|
72
|
+
`Original error: ${reason}`, { cause: err });
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
async externalize(event, metadata) {
|
|
@@ -85,17 +85,17 @@ let MicroservicesEventExternalizer = MicroservicesEventExternalizer_1 = class Mi
|
|
|
85
85
|
}
|
|
86
86
|
catch (err) {
|
|
87
87
|
const cause = err instanceof Error ? err : undefined;
|
|
88
|
-
const causeMessage =
|
|
88
|
+
const causeMessage = (0, outbox_1.describeThrown)(err);
|
|
89
89
|
throw new outbox_1.ExternalizationError(`ClientProxy '${formatToken(clientToken)}' not found in the DI container. ` +
|
|
90
90
|
`Ensure it is registered via ClientsModule.register() / registerAsync(). ` +
|
|
91
91
|
`Original error: ${causeMessage}`, metadata.eventType, metadata.target, cause);
|
|
92
92
|
}
|
|
93
93
|
if (metadata.headers !== undefined || metadata.routingKey !== undefined) {
|
|
94
|
-
//
|
|
94
|
+
// Current limitation — ClientProxy.emit has no unified
|
|
95
95
|
// headers / routing-key parameter. Logged for visibility; the
|
|
96
96
|
// broker-aware message construction iteration will route them
|
|
97
97
|
// through transport-specific envelopes.
|
|
98
|
-
this.logger.debug(`${metadata.eventType}: headers/routingKey are not applied to the wire payload in this version (
|
|
98
|
+
this.logger.debug(`${metadata.eventType}: headers/routingKey are not applied to the wire payload in this version (current limitation): ${JSON.stringify({ headers: metadata.headers, routingKey: metadata.routingKey })}`);
|
|
99
99
|
}
|
|
100
100
|
try {
|
|
101
101
|
await (0, rxjs_1.firstValueFrom)(client.emit(metadata.target, event));
|
|
@@ -103,7 +103,7 @@ let MicroservicesEventExternalizer = MicroservicesEventExternalizer_1 = class Mi
|
|
|
103
103
|
}
|
|
104
104
|
catch (err) {
|
|
105
105
|
const cause = err instanceof Error ? err : undefined;
|
|
106
|
-
const causeMessage =
|
|
106
|
+
const causeMessage = (0, outbox_1.describeThrown)(err);
|
|
107
107
|
throw new outbox_1.ExternalizationError(`Failed to publish ${metadata.eventType} to ${metadata.target}: ${causeMessage}`, metadata.eventType, metadata.target, cause);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"microservices-event-externalizer.js","sourceRoot":"","sources":["../../src/externalizer/microservices-event-externalizer.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAMwB;AACxB,uCAAyC;AAEzC,
|
|
1
|
+
{"version":3,"file":"microservices-event-externalizer.js","sourceRoot":"","sources":["../../src/externalizer/microservices-event-externalizer.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { MicroservicesEventExternalizer } from './externalizer/microservices-event-externalizer';
|
|
2
2
|
export { OutboxMicroservicesModule, type OutboxMicroservicesAsyncOptions, } from './module/outbox-microservices.module';
|
|
3
|
-
export { OUTBOX_MICROSERVICES_OPTIONS, type OutboxMicroservicesOptions
|
|
3
|
+
export { OUTBOX_MICROSERVICES_OPTIONS, type OutboxMicroservicesOptions } from './types/options';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,oGAAiG;AAAxF,kJAAA,8BAA8B,OAAA;AACvC,oFAG8C;AAF5C,wIAAA,yBAAyB,OAAA;AAG3B,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,oGAAiG;AAAxF,kJAAA,8BAA8B,OAAA;AACvC,oFAG8C;AAF5C,wIAAA,yBAAyB,OAAA;AAG3B,2CAAgG;AAAvF,uHAAA,4BAA4B,OAAA"}
|
|
@@ -41,17 +41,17 @@ export interface OutboxMicroservicesAsyncOptions extends Pick<ModuleMetadata, 'i
|
|
|
41
41
|
* **Multi-dataSource setups**: a single externalizer covers every
|
|
42
42
|
* dataSource. Per-broker routing — when different events should land
|
|
43
43
|
* on different transports — happens via the per-event
|
|
44
|
-
* `@Externalized({ client })` parameter
|
|
44
|
+
* `@Externalized({ client })` parameter, not via a
|
|
45
45
|
* dataSource-keyed externalizer Map. See `outbox` README for the
|
|
46
46
|
* multi-`OutboxModule.forRoot()` pattern (ADR-019); each per-DS
|
|
47
47
|
* processor injects this same externalizer via `EVENT_EXTERNALIZER`.
|
|
48
48
|
*
|
|
49
|
-
* The module is registered as `@Global()`
|
|
49
|
+
* The module is registered as `@Global()` so the
|
|
50
50
|
* `EVENT_EXTERNALIZER` binding is visible to `OutboxModule`'s
|
|
51
51
|
* sibling-imported per-DS processors without an explicit import
|
|
52
52
|
* chain. Pre-Phase-14.6 the module was non-global, which silently
|
|
53
53
|
* broke the documented usage pattern in multi-module trees — fixed
|
|
54
|
-
*
|
|
54
|
+
* during verification work.
|
|
55
55
|
*/
|
|
56
56
|
export declare class OutboxMicroservicesModule {
|
|
57
57
|
static forRoot(options?: OutboxMicroservicesOptions): DynamicModule;
|
|
@@ -43,17 +43,17 @@ const options_1 = require("../types/options");
|
|
|
43
43
|
* **Multi-dataSource setups**: a single externalizer covers every
|
|
44
44
|
* dataSource. Per-broker routing — when different events should land
|
|
45
45
|
* on different transports — happens via the per-event
|
|
46
|
-
* `@Externalized({ client })` parameter
|
|
46
|
+
* `@Externalized({ client })` parameter, not via a
|
|
47
47
|
* dataSource-keyed externalizer Map. See `outbox` README for the
|
|
48
48
|
* multi-`OutboxModule.forRoot()` pattern (ADR-019); each per-DS
|
|
49
49
|
* processor injects this same externalizer via `EVENT_EXTERNALIZER`.
|
|
50
50
|
*
|
|
51
|
-
* The module is registered as `@Global()`
|
|
51
|
+
* The module is registered as `@Global()` so the
|
|
52
52
|
* `EVENT_EXTERNALIZER` binding is visible to `OutboxModule`'s
|
|
53
53
|
* sibling-imported per-DS processors without an explicit import
|
|
54
54
|
* chain. Pre-Phase-14.6 the module was non-global, which silently
|
|
55
55
|
* broke the documented usage pattern in multi-module trees — fixed
|
|
56
|
-
*
|
|
56
|
+
* during verification work.
|
|
57
57
|
*/
|
|
58
58
|
let OutboxMicroservicesModule = OutboxMicroservicesModule_1 = class OutboxMicroservicesModule {
|
|
59
59
|
static forRoot(options = {}) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outbox-microservices.module.js","sourceRoot":"","sources":["../../src/module/outbox-microservices.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAMwB;AACxB,yDAAkE;AAElE,uGAAkG;AAClG,
|
|
1
|
+
{"version":3,"file":"outbox-microservices.module.js","sourceRoot":"","sources":["../../src/module/outbox-microservices.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAMwB;AACxB,yDAAkE;AAElE,uGAAkG;AAClG,8CAAiG;AAejG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEI,IAAM,yBAAyB,iCAA/B,MAAM,yBAAyB;IACpC,MAAM,CAAC,OAAO,CAAC,UAAsC,EAAE;QACrD,MAAM,SAAS,GAAe;YAC5B,EAAE,OAAO,EAAE,sCAA4B,EAAE,QAAQ,EAAE,OAAO,EAAE;YAC5D,iEAA8B;YAC9B,EAAE,OAAO,EAAE,2BAAkB,EAAE,WAAW,EAAE,iEAA8B,EAAE;SAC7E,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,2BAAyB;YACjC,MAAM,EAAE,IAAI;YACZ,SAAS;YACT,OAAO,EAAE,CAAC,2BAAkB,EAAE,iEAA8B,CAAC;SAC9D,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,YAAY,CAAC,OAAwC;QAC1D,MAAM,SAAS,GAAe;YAC5B;gBACE,OAAO,EAAE,sCAA4B;gBACrC,UAAU,EAAE,OAAO,CAAC,UAAU;gBAC9B,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;aACzD;YACD,iEAA8B;YAC9B,EAAE,OAAO,EAAE,2BAAkB,EAAE,WAAW,EAAE,iEAA8B,EAAE;SAC7E,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,2BAAyB;YACjC,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE;YAC9B,SAAS;YACT,OAAO,EAAE,CAAC,2BAAkB,EAAE,iEAA8B,CAAC;SAC9D,CAAC;IACJ,CAAC;CACF,CAAA;AAnCY,8DAAyB;oCAAzB,yBAAyB;IADrC,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,yBAAyB,CAmCrC"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs-transactional/outbox-microservices",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Event externalization for @nestjs-transactional/outbox via @nestjs/microservices ClientProxy — Spring Modulith @Externalized parity",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"sideEffects": false,
|
|
6
8
|
"author": "Igor Golovanov",
|
|
7
9
|
"repository": {
|
|
8
10
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/igorgolovanov/nestjs-transactional.git",
|
|
11
|
+
"url": "git+https://github.com/igorgolovanov/nestjs-transactional.git",
|
|
10
12
|
"directory": "packages/outbox-microservices"
|
|
11
13
|
},
|
|
12
14
|
"bugs": {
|
|
@@ -43,7 +45,6 @@
|
|
|
43
45
|
},
|
|
44
46
|
"publishConfig": {
|
|
45
47
|
"access": "public",
|
|
46
|
-
"tag": "alpha",
|
|
47
48
|
"provenance": true
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
@@ -52,29 +53,38 @@
|
|
|
52
53
|
"@nestjs/microservices": "^10.0.0 || ^11.0.0",
|
|
53
54
|
"reflect-metadata": "^0.1.13 || ^0.2.0",
|
|
54
55
|
"rxjs": "^7.0.0",
|
|
55
|
-
"@nestjs-transactional/outbox": "^1.
|
|
56
|
+
"@nestjs-transactional/outbox": "^1.1.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
|
-
"@nestjs/common": "^11.
|
|
59
|
-
"@nestjs/core": "^11.
|
|
60
|
-
"@nestjs/microservices": "^11.
|
|
61
|
-
"@nestjs/testing": "^11.
|
|
62
|
-
"@
|
|
63
|
-
"jest": "^
|
|
59
|
+
"@nestjs/common": "^11.2.3",
|
|
60
|
+
"@nestjs/core": "^11.2.3",
|
|
61
|
+
"@nestjs/microservices": "^11.2.3",
|
|
62
|
+
"@nestjs/testing": "^11.2.3",
|
|
63
|
+
"@testcontainers/kafka": "^12.1.0",
|
|
64
|
+
"@types/jest": "^30.0.0",
|
|
65
|
+
"@types/node": "^22.20.1",
|
|
66
|
+
"amqplib": "^2.0.1",
|
|
67
|
+
"jest": "^30.4.2",
|
|
68
|
+
"kafkajs": "^2.2.4",
|
|
64
69
|
"reflect-metadata": "^0.2.2",
|
|
65
70
|
"rxjs": "^7.8.1",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"@nestjs-transactional/
|
|
71
|
+
"testcontainers": "^12.1.0",
|
|
72
|
+
"ts-jest": "^29.4.12",
|
|
73
|
+
"typescript": "^6.0.3",
|
|
74
|
+
"@nestjs-transactional/core": "1.0.0",
|
|
75
|
+
"@nestjs-transactional/outbox": "1.1.0"
|
|
70
76
|
},
|
|
71
77
|
"scripts": {
|
|
72
78
|
"build": "tsc -p tsconfig.build.json",
|
|
73
79
|
"clean": "rimraf dist *.tsbuildinfo coverage",
|
|
74
80
|
"test": "jest",
|
|
75
81
|
"test:watch": "jest --watch",
|
|
82
|
+
"test:integration": "jest --config jest.integration.config.js",
|
|
76
83
|
"test:cov": "jest --coverage",
|
|
77
84
|
"type-check": "tsc --noEmit",
|
|
78
|
-
"lint": "eslint
|
|
85
|
+
"lint": "eslint .",
|
|
86
|
+
"api:check": "api-extractor run",
|
|
87
|
+
"api:update": "api-extractor run --local",
|
|
88
|
+
"publish:check": "publint && attw --pack ."
|
|
79
89
|
}
|
|
80
90
|
}
|