@nestjs-transactional/outbox 1.0.0-alpha.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/LICENSE +21 -0
- package/README.md +396 -0
- package/dist/api/completed-event-publications.d.ts +31 -0
- package/dist/api/completed-event-publications.js +58 -0
- package/dist/api/completed-event-publications.js.map +1 -0
- package/dist/api/failed-event-publications.d.ts +36 -0
- package/dist/api/failed-event-publications.js +75 -0
- package/dist/api/failed-event-publications.js.map +1 -0
- package/dist/api/incomplete-event-publications.d.ts +28 -0
- package/dist/api/incomplete-event-publications.js +72 -0
- package/dist/api/incomplete-event-publications.js.map +1 -0
- package/dist/decorators/inject-decorators.d.ts +85 -0
- package/dist/decorators/inject-decorators.js +100 -0
- package/dist/decorators/inject-decorators.js.map +1 -0
- package/dist/decorators/outbox-events-handler.decorator.d.ts +97 -0
- package/dist/decorators/outbox-events-handler.decorator.js +58 -0
- package/dist/decorators/outbox-events-handler.decorator.js.map +1 -0
- package/dist/dispatcher/data-source-outbox-publisher.d.ts +69 -0
- package/dist/dispatcher/data-source-outbox-publisher.js +127 -0
- package/dist/dispatcher/data-source-outbox-publisher.js.map +1 -0
- package/dist/dispatcher/event-publication-processor.d.ts +69 -0
- package/dist/dispatcher/event-publication-processor.js +192 -0
- package/dist/dispatcher/event-publication-processor.js.map +1 -0
- package/dist/dispatcher/outbox-event-publisher.d.ts +129 -0
- package/dist/dispatcher/outbox-event-publisher.js +172 -0
- package/dist/dispatcher/outbox-event-publisher.js.map +1 -0
- package/dist/dispatcher/processor-options.d.ts +24 -0
- package/dist/dispatcher/processor-options.js +15 -0
- package/dist/dispatcher/processor-options.js.map +1 -0
- package/dist/externalization/errors.d.ts +21 -0
- package/dist/externalization/errors.js +30 -0
- package/dist/externalization/errors.js.map +1 -0
- package/dist/externalization/event-externalizer.d.ts +49 -0
- package/dist/externalization/event-externalizer.js +14 -0
- package/dist/externalization/event-externalizer.js.map +1 -0
- package/dist/externalization/externalization-registry.d.ts +56 -0
- package/dist/externalization/externalization-registry.js +103 -0
- package/dist/externalization/externalization-registry.js.map +1 -0
- package/dist/externalization/externalized.decorator.d.ts +121 -0
- package/dist/externalization/externalized.decorator.js +87 -0
- package/dist/externalization/externalized.decorator.js.map +1 -0
- package/dist/externalization/types.d.ts +39 -0
- package/dist/externalization/types.js +3 -0
- package/dist/externalization/types.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/outbox-event-handler.interface.d.ts +32 -0
- package/dist/interfaces/outbox-event-handler.interface.js +3 -0
- package/dist/interfaces/outbox-event-handler.interface.js.map +1 -0
- package/dist/module/outbox-processing.module.d.ts +29 -0
- package/dist/module/outbox-processing.module.js +66 -0
- package/dist/module/outbox-processing.module.js.map +1 -0
- package/dist/module/outbox.module.d.ts +196 -0
- package/dist/module/outbox.module.js +604 -0
- package/dist/module/outbox.module.js.map +1 -0
- package/dist/recovery/staleness-monitor.d.ts +39 -0
- package/dist/recovery/staleness-monitor.js +118 -0
- package/dist/recovery/staleness-monitor.js.map +1 -0
- package/dist/recovery/startup-recovery.d.ts +37 -0
- package/dist/recovery/startup-recovery.js +55 -0
- package/dist/recovery/startup-recovery.js.map +1 -0
- package/dist/registry/event-publication-registry.d.ts +70 -0
- package/dist/registry/event-publication-registry.js +138 -0
- package/dist/registry/event-publication-registry.js.map +1 -0
- package/dist/registry/listener-registry.d.ts +52 -0
- package/dist/registry/listener-registry.js +67 -0
- package/dist/registry/listener-registry.js.map +1 -0
- package/dist/registry/multi-ds-listener-registrar.d.ts +78 -0
- package/dist/registry/multi-ds-listener-registrar.js +112 -0
- package/dist/registry/multi-ds-listener-registrar.js.map +1 -0
- package/dist/registry/outbox-listener-scanner.d.ts +66 -0
- package/dist/registry/outbox-listener-scanner.js +192 -0
- package/dist/registry/outbox-listener-scanner.js.map +1 -0
- package/dist/repository/event-publication-repository.d.ts +112 -0
- package/dist/repository/event-publication-repository.js +6 -0
- package/dist/repository/event-publication-repository.js.map +1 -0
- package/dist/serialization/event-serializer.d.ts +28 -0
- package/dist/serialization/event-serializer.js +6 -0
- package/dist/serialization/event-serializer.js.map +1 -0
- package/dist/serialization/event-type-registry.d.ts +41 -0
- package/dist/serialization/event-type-registry.js +79 -0
- package/dist/serialization/event-type-registry.js.map +1 -0
- package/dist/serialization/event-type-resolver.d.ts +43 -0
- package/dist/serialization/event-type-resolver.js +66 -0
- package/dist/serialization/event-type-resolver.js.map +1 -0
- package/dist/serialization/json-event-serializer.d.ts +20 -0
- package/dist/serialization/json-event-serializer.js +64 -0
- package/dist/serialization/json-event-serializer.js.map +1 -0
- package/dist/testing/assertable-published-events.d.ts +71 -0
- package/dist/testing/assertable-published-events.js +124 -0
- package/dist/testing/assertable-published-events.js.map +1 -0
- package/dist/testing/in-memory-repository.d.ts +52 -0
- package/dist/testing/in-memory-repository.js +230 -0
- package/dist/testing/in-memory-repository.js.map +1 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +20 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/published-events.d.ts +62 -0
- package/dist/testing/published-events.js +110 -0
- package/dist/testing/published-events.js.map +1 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.js +14 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/tokens/token-utils.d.ts +68 -0
- package/dist/tokens/token-utils.js +98 -0
- package/dist/tokens/token-utils.js.map +1 -0
- package/dist/types/completion-mode.d.ts +16 -0
- package/dist/types/completion-mode.js +20 -0
- package/dist/types/completion-mode.js.map +1 -0
- package/dist/types/errors.d.ts +50 -0
- package/dist/types/errors.js +64 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/event-publication.d.ts +47 -0
- package/dist/types/event-publication.js +3 -0
- package/dist/types/event-publication.js.map +1 -0
- package/dist/types/publication-status.d.ts +26 -0
- package/dist/types/publication-status.js +30 -0
- package/dist/types/publication-status.js.map +1 -0
- package/dist/types/resubmission-options.d.ts +37 -0
- package/dist/types/resubmission-options.js +62 -0
- package/dist/types/resubmission-options.js.map +1 -0
- package/dist/types/staleness-config.d.ts +18 -0
- package/dist/types/staleness-config.js +10 -0
- package/dist/types/staleness-config.js.map +1 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Golovanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# @nestjs-transactional/outbox
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@nestjs-transactional/outbox)
|
|
4
|
+
[](https://github.com/igorgolovanov/nestjs-transactional/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Persistent Event Publication Registry for NestJS — an ORM-agnostic core
|
|
7
|
+
that brings Spring Modulith-equivalent delivery guarantees to the
|
|
8
|
+
`@nestjs-transactional` family of packages.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
`@TransactionalEventsHandler` from `@nestjs-transactional/cqrs` provides
|
|
13
|
+
phase-based dispatching (like Spring Framework core): handlers fire
|
|
14
|
+
`AFTER_COMMIT`, `BEFORE_COMMIT`, `AFTER_ROLLBACK`, or `AFTER_COMPLETION`.
|
|
15
|
+
That covers a lot, but it is purely in-memory — if the process dies
|
|
16
|
+
between commit and handler invocation, the event is lost.
|
|
17
|
+
|
|
18
|
+
`outbox` closes that gap. It gives you:
|
|
19
|
+
|
|
20
|
+
- A persistent **Event Publication Registry** — every handler
|
|
21
|
+
invocation is logged atomically with the business transaction.
|
|
22
|
+
- **Retry on process restart** — publications that were not acknowledged
|
|
23
|
+
before shutdown are replayed on next startup.
|
|
24
|
+
- **Lifecycle states**: `PUBLISHED`, `PROCESSING`, `COMPLETED`, `FAILED`,
|
|
25
|
+
`RESUBMITTED`.
|
|
26
|
+
- **Staleness monitor** — detects publications stuck in `PROCESSING`.
|
|
27
|
+
- **Failed / Incomplete / Completed** query APIs for operators.
|
|
28
|
+
- **Completion modes**: `UPDATE`, `DELETE`, `ARCHIVE`.
|
|
29
|
+
|
|
30
|
+
This package only defines types, the repository SPI, the in-memory
|
|
31
|
+
reference implementation (for tests), and the Nest module wiring. It
|
|
32
|
+
does **not** ship a production persistence backend — that lives in a
|
|
33
|
+
sibling package such as `@nestjs-transactional/outbox-typeorm`.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm add @nestjs-transactional/core @nestjs-transactional/outbox
|
|
39
|
+
# plus a persistence backend, e.g.:
|
|
40
|
+
pnpm add @nestjs-transactional/outbox-typeorm
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Peer dependencies: `@nestjs/common`, `@nestjs/core`, `reflect-metadata`,
|
|
44
|
+
`rxjs`.
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### 1. Module wiring
|
|
49
|
+
|
|
50
|
+
`OutboxModule.forRoot()` provides global infrastructure (registry,
|
|
51
|
+
serializer, processor, recovery). `OutboxModule.forFeature([...])`
|
|
52
|
+
declares the event classes a feature module owns — matches
|
|
53
|
+
`TypeOrmModule.forFeature(...)` ergonomics.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { Module } from '@nestjs/common';
|
|
57
|
+
import { TransactionalModule } from '@nestjs-transactional/core';
|
|
58
|
+
import { OutboxModule, OutboxProcessingModule } from '@nestjs-transactional/outbox';
|
|
59
|
+
|
|
60
|
+
@Module({
|
|
61
|
+
imports: [
|
|
62
|
+
// isGlobal: true is REQUIRED so outbox providers can see
|
|
63
|
+
// TransactionManager across module boundaries.
|
|
64
|
+
TransactionalModule.forRoot({
|
|
65
|
+
isGlobal: true,
|
|
66
|
+
adapters: [/* your adapter registrations */],
|
|
67
|
+
}),
|
|
68
|
+
OutboxModule.forRoot({
|
|
69
|
+
republishOnStartup: true,
|
|
70
|
+
processor: { pollingInterval: 1000, batchSize: 100 },
|
|
71
|
+
staleness: { processing: 60_000, monitorInterval: 30_000 },
|
|
72
|
+
// repository: { provide: EVENT_PUBLICATION_REPOSITORY, useClass: TypeOrmEventPublicationRepository },
|
|
73
|
+
}),
|
|
74
|
+
// Import OutboxProcessingModule ONLY in worker processes — not in
|
|
75
|
+
// API-only apps that merely publish events.
|
|
76
|
+
OutboxProcessingModule,
|
|
77
|
+
// Register the event classes this module emits / consumes.
|
|
78
|
+
OutboxModule.forFeature([OrderPlacedEvent, OrderShippedEvent]),
|
|
79
|
+
],
|
|
80
|
+
})
|
|
81
|
+
export class AppModule {}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Modular pattern (multiple feature modules)
|
|
85
|
+
|
|
86
|
+
Distribute event registrations to the modules that own them:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
@Module({
|
|
90
|
+
imports: [OutboxModule.forFeature([OrderPlacedEvent, OrderCancelledEvent])],
|
|
91
|
+
providers: [OrderService],
|
|
92
|
+
})
|
|
93
|
+
export class OrderModule {}
|
|
94
|
+
|
|
95
|
+
@Module({
|
|
96
|
+
imports: [OutboxModule.forFeature([InventoryReservedEvent])],
|
|
97
|
+
providers: [InventoryService],
|
|
98
|
+
})
|
|
99
|
+
export class InventoryModule {}
|
|
100
|
+
|
|
101
|
+
@Module({
|
|
102
|
+
imports: [
|
|
103
|
+
TransactionalModule.forRoot({ isGlobal: true /* ... */ }),
|
|
104
|
+
OutboxModule.forRoot({ republishOnStartup: true /* ... */ }),
|
|
105
|
+
OrderModule,
|
|
106
|
+
InventoryModule,
|
|
107
|
+
],
|
|
108
|
+
})
|
|
109
|
+
export class AppModule {}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Multiple `forFeature` calls accumulate into the singleton
|
|
113
|
+
`EventTypeRegistry` provided by `forRoot`. Each event class can only
|
|
114
|
+
be registered once — duplicates throw at bootstrap with the offending
|
|
115
|
+
class name:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Error: Event type 'OrderPlacedEvent' already registered.
|
|
119
|
+
Each event type can only be registered once — check for duplicate
|
|
120
|
+
entries across OutboxModule.forFeature() calls.
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`OutboxModule.forFeature([])` is accepted as a no-op (matches
|
|
124
|
+
`TypeOrmModule.forFeature([])`).
|
|
125
|
+
|
|
126
|
+
### 2. Declaring a handler
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { Injectable } from '@nestjs/common';
|
|
130
|
+
import {
|
|
131
|
+
type IOutboxEventHandler,
|
|
132
|
+
OutboxEventsHandler,
|
|
133
|
+
} from '@nestjs-transactional/outbox';
|
|
134
|
+
|
|
135
|
+
@Injectable()
|
|
136
|
+
@OutboxEventsHandler(OrderPlacedEvent)
|
|
137
|
+
export class InventoryReservationHandler
|
|
138
|
+
implements IOutboxEventHandler<OrderPlacedEvent>
|
|
139
|
+
{
|
|
140
|
+
async handle(event: OrderPlacedEvent): Promise<void> {
|
|
141
|
+
// Runs inside a fresh REQUIRES_NEW transaction, only after the
|
|
142
|
+
// publishing transaction has committed. Retried on exception,
|
|
143
|
+
// resumable across restarts.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The decorator accepts either rest params or an options object:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Short form — defaults (newTransaction: true).
|
|
152
|
+
@OutboxEventsHandler(OrderPlacedEvent, OrderCancelledEvent)
|
|
153
|
+
|
|
154
|
+
// Long form — explicit options.
|
|
155
|
+
@OutboxEventsHandler({
|
|
156
|
+
events: [OrderPlacedEvent],
|
|
157
|
+
id: 'inventory.reservation', // stable base id, see "Listener ids" below
|
|
158
|
+
newTransaction: false, // skip the REQUIRES_NEW wrapper
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
See ADR-014 for the rationale behind the class-level shape.
|
|
163
|
+
|
|
164
|
+
### 3. Publishing events
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { Injectable } from '@nestjs/common';
|
|
168
|
+
import { Transactional } from '@nestjs-transactional/core';
|
|
169
|
+
import { OutboxEventPublisher } from '@nestjs-transactional/outbox';
|
|
170
|
+
|
|
171
|
+
@Injectable()
|
|
172
|
+
export class PlaceOrderHandler {
|
|
173
|
+
constructor(private readonly outbox: OutboxEventPublisher) {}
|
|
174
|
+
|
|
175
|
+
@Transactional()
|
|
176
|
+
async handle(command: PlaceOrder): Promise<void> {
|
|
177
|
+
// ...persist business data in the same transaction...
|
|
178
|
+
await this.outbox.publish(new OrderPlacedEvent(command.orderId));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 4. Operator APIs
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const failed = await this.failedEventPublications.findAll();
|
|
187
|
+
await this.failedEventPublications.resubmit(
|
|
188
|
+
ResubmissionOptions.defaults().withBatchSize(50).withMaxAttempts(3),
|
|
189
|
+
);
|
|
190
|
+
await this.completedEventPublications.purge(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Listener ids
|
|
194
|
+
|
|
195
|
+
Every stored publication carries a `listenerId` string that identifies
|
|
196
|
+
which handler class should deliver it. The scanner composes the id as:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
${baseId}#${EventName}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- **No explicit `id`**: baseId is the class name. Example:
|
|
203
|
+
`InventoryReservationHandler#OrderPlacedEvent`.
|
|
204
|
+
- **With `options.id`**: baseId is the supplied string. Example:
|
|
205
|
+
`inventory.reservation#OrderPlacedEvent`.
|
|
206
|
+
|
|
207
|
+
A class that handles multiple event types produces one id per event.
|
|
208
|
+
Renaming the class without supplying an explicit `id` breaks delivery
|
|
209
|
+
for every stored publication under the old id — set `options.id` when
|
|
210
|
+
you want stability across renames.
|
|
211
|
+
|
|
212
|
+
## Externalization (advanced)
|
|
213
|
+
|
|
214
|
+
> The `EventExternalizer` SPI lives in this package. The
|
|
215
|
+
> `@nestjs/microservices` `ClientProxy`-backed implementation ships
|
|
216
|
+
> as a separate package —
|
|
217
|
+
> [`@nestjs-transactional/outbox-microservices`](../outbox-microservices).
|
|
218
|
+
> Without an externalizer bound, `@Externalized` mappings are
|
|
219
|
+
> recorded but never delivered to a broker — local outbox listeners
|
|
220
|
+
> still run.
|
|
221
|
+
>
|
|
222
|
+
> Architecture and design rationale:
|
|
223
|
+
> [ADR-015](../../docs/adr/015-event-externalization-architecture.md),
|
|
224
|
+
> [`docs/architecture/event-externalization.md`](../../docs/architecture/event-externalization.md).
|
|
225
|
+
> Reliability caveat for the `@nestjs/microservices`-backed
|
|
226
|
+
> implementation:
|
|
227
|
+
> [ADR-016](../../docs/adr/016-externalization-reliability-semantics.md).
|
|
228
|
+
|
|
229
|
+
`@Externalized` marks an event class for delivery to an external
|
|
230
|
+
message broker (Kafka topic, RabbitMQ exchange, NATS subject, ...) in
|
|
231
|
+
addition to local outbox listeners. The processor invokes the bound
|
|
232
|
+
`EventExternalizer` AFTER the local listener has succeeded — single
|
|
233
|
+
unit atomicity (DD-019): if either step fails, the publication is
|
|
234
|
+
recorded as `FAILED` and can be resubmitted via `FailedEventPublications`.
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { Externalized } from '@nestjs-transactional/outbox';
|
|
238
|
+
|
|
239
|
+
@Externalized<OrderPlacedEvent>({
|
|
240
|
+
target: 'orders.placed', // broker-side destination
|
|
241
|
+
routingKey: (e) => e.tenantId, // optional, brokers that support it
|
|
242
|
+
headers: (e) => ({ 'x-tenant': e.tenantId }), // static record OR callback
|
|
243
|
+
client: 'KAFKA_CLIENT', // optional ClientProxy token
|
|
244
|
+
})
|
|
245
|
+
export class OrderPlacedEvent {
|
|
246
|
+
constructor(
|
|
247
|
+
readonly orderId: string,
|
|
248
|
+
readonly tenantId: string,
|
|
249
|
+
) {}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Bind a concrete externalizer under the `EVENT_EXTERNALIZER` token —
|
|
254
|
+
this is normally done by an extension package (`outbox-microservices`).
|
|
255
|
+
Custom implementations can register the same way:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { EVENT_EXTERNALIZER, type EventExternalizer } from '@nestjs-transactional/outbox';
|
|
259
|
+
|
|
260
|
+
@Module({
|
|
261
|
+
providers: [
|
|
262
|
+
{
|
|
263
|
+
provide: EVENT_EXTERNALIZER,
|
|
264
|
+
useClass: MyCustomExternalizer, // implements EventExternalizer
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
})
|
|
268
|
+
export class MyAppModule {}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The processor only invokes the externalizer when BOTH a binding under
|
|
272
|
+
`EVENT_EXTERNALIZER` exists AND the event class carries an
|
|
273
|
+
`@Externalized` mapping — keep one without the other if you only need
|
|
274
|
+
half the contract (e.g. metadata-only mappings during a rollout, or a
|
|
275
|
+
generic externalizer that resolves targets some other way). Inspect
|
|
276
|
+
the resolved mapping at runtime via `ExternalizationRegistry`:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { ExternalizationRegistry } from '@nestjs-transactional/outbox';
|
|
280
|
+
|
|
281
|
+
@Injectable()
|
|
282
|
+
export class MyDiagnostics {
|
|
283
|
+
constructor(private readonly externalization: ExternalizationRegistry) {}
|
|
284
|
+
|
|
285
|
+
isExternalized(typeName: string): boolean {
|
|
286
|
+
return this.externalization.has(typeName);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Errors raised by the externalizer are wrapped in `ExternalizationError`
|
|
292
|
+
(carrying `eventType`, `target`, and the underlying cause) and surface
|
|
293
|
+
on the publication's `failureReason` for operator visibility.
|
|
294
|
+
|
|
295
|
+
## Testing utilities
|
|
296
|
+
|
|
297
|
+
Exported via the `/testing` subpath for assertions about which events
|
|
298
|
+
the code under test published. Mirrors Spring Modulith's
|
|
299
|
+
`PublishedEvents` / `AssertablePublishedEvents`.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import {
|
|
303
|
+
PublishedEvents,
|
|
304
|
+
AssertablePublishedEvents,
|
|
305
|
+
} from '@nestjs-transactional/outbox/testing';
|
|
306
|
+
|
|
307
|
+
describe('PlaceOrder', () => {
|
|
308
|
+
let app: TestingModule;
|
|
309
|
+
let assertablePublishedEvents: AssertablePublishedEvents;
|
|
310
|
+
|
|
311
|
+
beforeEach(async () => {
|
|
312
|
+
app = await Test.createTestingModule({
|
|
313
|
+
imports: [
|
|
314
|
+
TransactionalModule.forRoot({ isGlobal: true /* ... */ }),
|
|
315
|
+
OutboxModule.forRoot({}),
|
|
316
|
+
OutboxModule.forFeature([OrderPlacedEvent]),
|
|
317
|
+
],
|
|
318
|
+
providers: [
|
|
319
|
+
PlaceOrderService,
|
|
320
|
+
PublishedEvents,
|
|
321
|
+
AssertablePublishedEvents,
|
|
322
|
+
],
|
|
323
|
+
}).compile();
|
|
324
|
+
await app.init();
|
|
325
|
+
assertablePublishedEvents = app.get(AssertablePublishedEvents);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('publishes OrderPlacedEvent for the placed order', async () => {
|
|
329
|
+
const service = app.get(PlaceOrderService);
|
|
330
|
+
await service.place('order-123');
|
|
331
|
+
|
|
332
|
+
const view = await assertablePublishedEvents.contains(OrderPlacedEvent);
|
|
333
|
+
view.matching((e) => e.orderId, 'order-123').hasSize(1);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('publishes nothing when validation rejects the command', async () => {
|
|
337
|
+
const service = app.get(PlaceOrderService);
|
|
338
|
+
await service.placeInvalid().catch(() => undefined);
|
|
339
|
+
|
|
340
|
+
await assertablePublishedEvents.doesNotContain(OrderPlacedEvent);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`PublishedEvents.ofType(EventType)` returns a fluent `PublishedEventsView`
|
|
346
|
+
for read-only inspection (`.get()`, `.count()`,
|
|
347
|
+
`.matching(predicate)`, `.matching(getter, expected)`).
|
|
348
|
+
`AssertablePublishedEvents.contains(EventType)` is the assertion-first
|
|
349
|
+
counterpart — it throws `PublishedEventsAssertionError` when zero
|
|
350
|
+
events match, and returns an `AssertionView` whose `.matching(...)`
|
|
351
|
+
and `.hasSize(...)` operate synchronously over the already-fetched
|
|
352
|
+
events.
|
|
353
|
+
|
|
354
|
+
The utilities read through the wired `EventPublicationRepository`
|
|
355
|
+
implementation, so the default `InMemoryEventPublicationRepository`
|
|
356
|
+
works out of the box for unit-level tests. Integration tests that use
|
|
357
|
+
`@nestjs-transactional/outbox-typeorm` with a real Postgres get the
|
|
358
|
+
same API for free.
|
|
359
|
+
|
|
360
|
+
## Worked examples
|
|
361
|
+
|
|
362
|
+
- [`basic-outbox`](../../examples/basic-outbox) — `@OutboxEventsHandler` + `OutboxEventPublisher`, in-memory test repository.
|
|
363
|
+
- [`basic-typeorm-outbox`](../../examples/basic-typeorm-outbox) — production-shape outbox with Postgres, atomicity proven via testcontainers.
|
|
364
|
+
- [`multi-datasource-outbox`](../../examples/multi-datasource-outbox) — per-DS `event_publication` tables (ADR-019 multi-`forRoot`).
|
|
365
|
+
- [`shared-database-modular-monolith`](../../examples/shared-database-modular-monolith) — one Postgres, multi-schema, per-module outbox stacks.
|
|
366
|
+
- [`saga-pattern`](../../examples/saga-pattern) — choreographed multi-step business saga over outbox events.
|
|
367
|
+
- [`audit-logging`](../../examples/audit-logging) — cross-DS audit trail with idempotency on the consumer.
|
|
368
|
+
- [`e-commerce-orders`](../../examples/e-commerce-orders) — three-bounded-context flagship wired through outbox integration events.
|
|
369
|
+
|
|
370
|
+
Full catalogue: [examples/README.md](../../examples/README.md).
|
|
371
|
+
|
|
372
|
+
## Status
|
|
373
|
+
|
|
374
|
+
Alpha. Public API may change between 0.x releases. Current
|
|
375
|
+
functionality is exercised end-to-end through the
|
|
376
|
+
[Tier 1–5 example library](../../examples/README.md).
|
|
377
|
+
|
|
378
|
+
Design notes:
|
|
379
|
+
[`docs/roadmap/README.md`](../../docs/roadmap/README.md),
|
|
380
|
+
[`docs/adr/006-outbox-pattern.md`](../../docs/adr/006-outbox-pattern.md).
|
|
381
|
+
|
|
382
|
+
## Inspired by Spring Modulith
|
|
383
|
+
|
|
384
|
+
The design follows
|
|
385
|
+
[Spring Modulith's Event Publication Registry](https://docs.spring.io/spring-modulith/reference/events.html)
|
|
386
|
+
closely — lifecycle states, `@ApplicationModuleListener` semantics,
|
|
387
|
+
completion modes, and staleness monitoring all map one-to-one. The
|
|
388
|
+
deviations from Spring Modulith are limited to what is needed to fit
|
|
389
|
+
the Node.js / NestJS runtime (async workers instead of thread pools,
|
|
390
|
+
AsyncLocalStorage for transaction context, NestJS DI conventions,
|
|
391
|
+
class-level handler decorators aligned with `@nestjs/cqrs`
|
|
392
|
+
conventions — see ADR-014).
|
|
393
|
+
|
|
394
|
+
## License
|
|
395
|
+
|
|
396
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type EventPublicationRepository } from '../repository/event-publication-repository';
|
|
2
|
+
import type { EventPublication } from '../types/event-publication';
|
|
3
|
+
/**
|
|
4
|
+
* Operator-facing query + purge API for publications that have
|
|
5
|
+
* reached the terminal `COMPLETED` state. Equivalent to Spring
|
|
6
|
+
* Modulith's `CompletedEventPublications`.
|
|
7
|
+
*
|
|
8
|
+
* Relevant when the completion mode is `UPDATE` (the default) — the
|
|
9
|
+
* row is kept for audit and needs to be purged eventually.
|
|
10
|
+
*/
|
|
11
|
+
export declare class CompletedEventPublications {
|
|
12
|
+
private readonly repository;
|
|
13
|
+
constructor(repository: EventPublicationRepository);
|
|
14
|
+
/**
|
|
15
|
+
* Return completed publications, optionally filtered by
|
|
16
|
+
* `completionDate` and capped by `limit`.
|
|
17
|
+
*/
|
|
18
|
+
findAll(options?: {
|
|
19
|
+
readonly olderThan?: Date;
|
|
20
|
+
readonly limit?: number;
|
|
21
|
+
}): Promise<EventPublication[]>;
|
|
22
|
+
/** How many publications are currently in `COMPLETED`. */
|
|
23
|
+
count(): Promise<number>;
|
|
24
|
+
/**
|
|
25
|
+
* Delete completed publications. When `olderThan` is provided, only
|
|
26
|
+
* publications whose `completionDate` is strictly before it are
|
|
27
|
+
* removed. Returns the number of rows deleted.
|
|
28
|
+
*/
|
|
29
|
+
purge(olderThan?: Date): Promise<number>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=completed-event-publications.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.CompletedEventPublications = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const event_publication_repository_1 = require("../repository/event-publication-repository");
|
|
18
|
+
/**
|
|
19
|
+
* Operator-facing query + purge API for publications that have
|
|
20
|
+
* reached the terminal `COMPLETED` state. Equivalent to Spring
|
|
21
|
+
* Modulith's `CompletedEventPublications`.
|
|
22
|
+
*
|
|
23
|
+
* Relevant when the completion mode is `UPDATE` (the default) — the
|
|
24
|
+
* row is kept for audit and needs to be purged eventually.
|
|
25
|
+
*/
|
|
26
|
+
let CompletedEventPublications = class CompletedEventPublications {
|
|
27
|
+
repository;
|
|
28
|
+
constructor(repository) {
|
|
29
|
+
this.repository = repository;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Return completed publications, optionally filtered by
|
|
33
|
+
* `completionDate` and capped by `limit`.
|
|
34
|
+
*/
|
|
35
|
+
async findAll(options) {
|
|
36
|
+
return this.repository.findCompleted(options);
|
|
37
|
+
}
|
|
38
|
+
/** How many publications are currently in `COMPLETED`. */
|
|
39
|
+
async count() {
|
|
40
|
+
const completed = await this.repository.findCompleted();
|
|
41
|
+
return completed.length;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Delete completed publications. When `olderThan` is provided, only
|
|
45
|
+
* publications whose `completionDate` is strictly before it are
|
|
46
|
+
* removed. Returns the number of rows deleted.
|
|
47
|
+
*/
|
|
48
|
+
async purge(olderThan) {
|
|
49
|
+
return this.repository.deleteCompleted(olderThan);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
exports.CompletedEventPublications = CompletedEventPublications;
|
|
53
|
+
exports.CompletedEventPublications = CompletedEventPublications = __decorate([
|
|
54
|
+
(0, common_1.Injectable)(),
|
|
55
|
+
__param(0, (0, common_1.Inject)(event_publication_repository_1.EVENT_PUBLICATION_REPOSITORY)),
|
|
56
|
+
__metadata("design:paramtypes", [Object])
|
|
57
|
+
], CompletedEventPublications);
|
|
58
|
+
//# sourceMappingURL=completed-event-publications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"completed-event-publications.js","sourceRoot":"","sources":["../../src/api/completed-event-publications.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoD;AAEpD,6FAGoD;AAGpD;;;;;;;GAOG;AAEI,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IAGlB;IAFnB,YAEmB,UAAsC;QAAtC,eAAU,GAAV,UAAU,CAA4B;IACtD,CAAC;IAEJ;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,OAGb;QACC,OAAO,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAChD,CAAC;IAED,0DAA0D;IAC1D,KAAK,CAAC,KAAK;QACT,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC;QACxD,OAAO,SAAS,CAAC,MAAM,CAAC;IAC1B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,SAAgB;QAC1B,OAAO,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF,CAAA;AA/BY,gEAA0B;qCAA1B,0BAA0B;IADtC,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,2DAA4B,CAAC,CAAA;;GAF5B,0BAA0B,CA+BtC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type EventPublicationRepository } from '../repository/event-publication-repository';
|
|
2
|
+
import type { EventPublication } from '../types/event-publication';
|
|
3
|
+
import { ResubmissionOptions } from '../types/resubmission-options';
|
|
4
|
+
/**
|
|
5
|
+
* Operator-facing query + resubmit API for publications currently in
|
|
6
|
+
* the `FAILED` state. Equivalent to Spring Modulith's
|
|
7
|
+
* `FailedEventPublications`.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FailedEventPublications {
|
|
10
|
+
private readonly repository;
|
|
11
|
+
constructor(repository: EventPublicationRepository);
|
|
12
|
+
/**
|
|
13
|
+
* Return every failed publication matching the optional filters
|
|
14
|
+
* (minimum age in ms since `publicationDate`, cap on
|
|
15
|
+
* `completionAttempts`).
|
|
16
|
+
*/
|
|
17
|
+
findAll(options?: {
|
|
18
|
+
readonly minAge?: number;
|
|
19
|
+
readonly maxAttempts?: number;
|
|
20
|
+
}): Promise<EventPublication[]>;
|
|
21
|
+
/** How many publications are currently in `FAILED`. */
|
|
22
|
+
count(): Promise<number>;
|
|
23
|
+
/**
|
|
24
|
+
* Transition selected failed publications to `RESUBMITTED` so the
|
|
25
|
+
* processor will pick them up again. Returns the number of rows
|
|
26
|
+
* transitioned.
|
|
27
|
+
*
|
|
28
|
+
* Selection order:
|
|
29
|
+
* 1. Pull failed publications matching the `minAge` /
|
|
30
|
+
* `maxCompletionAttempts` filters.
|
|
31
|
+
* 2. Apply the user-supplied `filter` predicate, if any.
|
|
32
|
+
* 3. Keep at most `batchSize` publications.
|
|
33
|
+
*/
|
|
34
|
+
resubmit(options?: ResubmissionOptions): Promise<number>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=failed-event-publications.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FailedEventPublications = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const event_publication_repository_1 = require("../repository/event-publication-repository");
|
|
18
|
+
const publication_status_1 = require("../types/publication-status");
|
|
19
|
+
const resubmission_options_1 = require("../types/resubmission-options");
|
|
20
|
+
/**
|
|
21
|
+
* Operator-facing query + resubmit API for publications currently in
|
|
22
|
+
* the `FAILED` state. Equivalent to Spring Modulith's
|
|
23
|
+
* `FailedEventPublications`.
|
|
24
|
+
*/
|
|
25
|
+
let FailedEventPublications = class FailedEventPublications {
|
|
26
|
+
repository;
|
|
27
|
+
constructor(repository) {
|
|
28
|
+
this.repository = repository;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Return every failed publication matching the optional filters
|
|
32
|
+
* (minimum age in ms since `publicationDate`, cap on
|
|
33
|
+
* `completionAttempts`).
|
|
34
|
+
*/
|
|
35
|
+
async findAll(options) {
|
|
36
|
+
return this.repository.findFailed(options);
|
|
37
|
+
}
|
|
38
|
+
/** How many publications are currently in `FAILED`. */
|
|
39
|
+
async count() {
|
|
40
|
+
const failed = await this.repository.findFailed();
|
|
41
|
+
return failed.length;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Transition selected failed publications to `RESUBMITTED` so the
|
|
45
|
+
* processor will pick them up again. Returns the number of rows
|
|
46
|
+
* transitioned.
|
|
47
|
+
*
|
|
48
|
+
* Selection order:
|
|
49
|
+
* 1. Pull failed publications matching the `minAge` /
|
|
50
|
+
* `maxCompletionAttempts` filters.
|
|
51
|
+
* 2. Apply the user-supplied `filter` predicate, if any.
|
|
52
|
+
* 3. Keep at most `batchSize` publications.
|
|
53
|
+
*/
|
|
54
|
+
async resubmit(options = resubmission_options_1.ResubmissionOptions.defaults()) {
|
|
55
|
+
const failed = await this.repository.findFailed({
|
|
56
|
+
minAge: options.minAge,
|
|
57
|
+
maxAttempts: options.maxCompletionAttempts ?? undefined,
|
|
58
|
+
});
|
|
59
|
+
const filtered = options.filter !== null ? failed.filter(options.filter) : failed;
|
|
60
|
+
const toProcess = filtered.slice(0, options.batchSize);
|
|
61
|
+
for (const pub of toProcess) {
|
|
62
|
+
await this.repository.updateStatus(pub.id, publication_status_1.PublicationStatus.RESUBMITTED, {
|
|
63
|
+
lastResubmissionDate: new Date(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return toProcess.length;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
exports.FailedEventPublications = FailedEventPublications;
|
|
70
|
+
exports.FailedEventPublications = FailedEventPublications = __decorate([
|
|
71
|
+
(0, common_1.Injectable)(),
|
|
72
|
+
__param(0, (0, common_1.Inject)(event_publication_repository_1.EVENT_PUBLICATION_REPOSITORY)),
|
|
73
|
+
__metadata("design:paramtypes", [Object])
|
|
74
|
+
], FailedEventPublications);
|
|
75
|
+
//# sourceMappingURL=failed-event-publications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failed-event-publications.js","sourceRoot":"","sources":["../../src/api/failed-event-publications.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoD;AAEpD,6FAGoD;AAEpD,oEAAgE;AAChE,wEAAoE;AAGpE;;;;GAIG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAGf;IAFnB,YAEmB,UAAsC;QAAtC,eAAU,GAAV,UAAU,CAA4B;IACtD,CAAC;IAEJ;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAC,OAGb;QACC,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAC7C,CAAC;IAED,uDAAuD;IACvD,KAAK,CAAC,KAAK;QACT,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC;QAClD,OAAO,MAAM,CAAC,MAAM,CAAC;IACvB,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,QAAQ,CACZ,UAA+B,0CAAmB,CAAC,QAAQ,EAAE;QAE7D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;YAC9C,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,WAAW,EAAE,OAAO,CAAC,qBAAqB,IAAI,SAAS;SACxD,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QAClF,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAEvD,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC5B,MAAM,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,sCAAiB,CAAC,WAAW,EAAE;gBACxE,oBAAoB,EAAE,IAAI,IAAI,EAAE;aACjC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,SAAS,CAAC,MAAM,CAAC;IAC1B,CAAC;CACF,CAAA;AAtDY,0DAAuB;kCAAvB,uBAAuB;IADnC,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,2DAA4B,CAAC,CAAA;;GAF5B,uBAAuB,CAsDnC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type EventPublicationRepository } from '../repository/event-publication-repository';
|
|
2
|
+
import type { EventPublication } from '../types/event-publication';
|
|
3
|
+
import { ResubmissionOptions } from '../types/resubmission-options';
|
|
4
|
+
/**
|
|
5
|
+
* Operator-facing query + resubmit API for every publication that has
|
|
6
|
+
* not yet reached the terminal `COMPLETED` state. Equivalent to Spring
|
|
7
|
+
* Modulith's `IncompleteEventPublications`.
|
|
8
|
+
*
|
|
9
|
+
* Typical use case: startup / restart recovery — query the backlog,
|
|
10
|
+
* bulk-resubmit retriable rows, let the processor drain.
|
|
11
|
+
*/
|
|
12
|
+
export declare class IncompleteEventPublications {
|
|
13
|
+
private readonly repository;
|
|
14
|
+
constructor(repository: EventPublicationRepository);
|
|
15
|
+
/** Return every publication whose status is not `COMPLETED`. */
|
|
16
|
+
findAll(): Promise<EventPublication[]>;
|
|
17
|
+
/** How many publications are currently not `COMPLETED`. */
|
|
18
|
+
count(): Promise<number>;
|
|
19
|
+
/**
|
|
20
|
+
* Scan every non-completed publication and transition retriable
|
|
21
|
+
* ones (`FAILED`, `PUBLISHED`) to `RESUBMITTED`. Publications
|
|
22
|
+
* currently `PROCESSING` or `RESUBMITTED` are left untouched —
|
|
23
|
+
* `PROCESSING` is owned by a worker and `RESUBMITTED` is already in
|
|
24
|
+
* the retry queue. Returns the number of rows transitioned.
|
|
25
|
+
*/
|
|
26
|
+
resubmitIncompletePublications(options?: ResubmissionOptions): Promise<number>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=incomplete-event-publications.d.ts.map
|