@fbsm/saga-nestjs 0.0.1-beta.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 +506 -0
- package/dist/index.cjs +290 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +57 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +267 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# @fbsm/saga-nestjs
|
|
2
|
+
|
|
3
|
+
NestJS integration for the saga choreography library. Provides a dynamic module, decorators for auto-discovery, and injectable providers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fbsm/saga-nestjs @fbsm/saga-core @fbsm/saga-transport-kafka
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API Reference
|
|
12
|
+
|
|
13
|
+
### `SagaModule`
|
|
14
|
+
|
|
15
|
+
Dynamic NestJS module. Register once globally.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Synchronous
|
|
19
|
+
SagaModule.forRoot(options: SagaModuleOptions): DynamicModule
|
|
20
|
+
|
|
21
|
+
// Asynchronous (e.g., with ConfigService)
|
|
22
|
+
SagaModule.forRootAsync(options: SagaModuleAsyncOptions): DynamicModule
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**`SagaModuleOptions`** (extends `RunnerOptions`):
|
|
26
|
+
|
|
27
|
+
| Field | Type | Default | Description |
|
|
28
|
+
|-------|------|---------|-------------|
|
|
29
|
+
| `serviceName` | `string` | — | Consumer group prefix (`${serviceName}-group`) |
|
|
30
|
+
| `transport` | `SagaTransport` | — | Transport implementation (e.g., `KafkaTransport`) |
|
|
31
|
+
| `retryPolicy.maxRetries` | `number` | `3` | Max retry attempts for `SagaRetryableError` |
|
|
32
|
+
| `retryPolicy.initialDelayMs` | `number` | `200` | Initial retry delay in ms (doubles each attempt) |
|
|
33
|
+
| `fromBeginning` | `boolean` | `false` | Read from beginning of topics |
|
|
34
|
+
| `topicPrefix` | `string` | `''` | Prefix prepended to eventType for topic names |
|
|
35
|
+
| `otel.enabled` | `boolean` | `false` | Enable OpenTelemetry tracing and W3C context propagation |
|
|
36
|
+
| `otel.exporterUrl` | `string` | — | OTel exporter URL |
|
|
37
|
+
| `logger` | `SagaLogger` | `ConsoleSagaLogger` | Custom logger |
|
|
38
|
+
|
|
39
|
+
**Async configuration**:
|
|
40
|
+
```typescript
|
|
41
|
+
SagaModule.forRootAsync({
|
|
42
|
+
imports: [ConfigModule],
|
|
43
|
+
inject: [ConfigService],
|
|
44
|
+
useFactory: (config: ConfigService) => ({
|
|
45
|
+
serviceName: config.get('SERVICE_NAME'),
|
|
46
|
+
transport: new KafkaTransport({
|
|
47
|
+
brokers: config.get('KAFKA_BROKERS').split(','),
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `@SagaParticipant()`
|
|
54
|
+
|
|
55
|
+
Class decorator. Marks a class for auto-discovery by the saga runner. Must extend `SagaParticipantBase`.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
@Injectable()
|
|
59
|
+
@SagaParticipant()
|
|
60
|
+
export class PaymentParticipant extends SagaParticipantBase {
|
|
61
|
+
readonly serviceId = 'payment-service';
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `@SagaHandler(...eventTypes, options?)`
|
|
66
|
+
|
|
67
|
+
Method decorator. Registers a method as the handler for one or more event types. Accepts an optional options object as the last argument.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Single event type
|
|
71
|
+
@SagaHandler('order.created')
|
|
72
|
+
async handleOrderCreated(event: IncomingEvent, emit: Emit) {}
|
|
73
|
+
|
|
74
|
+
// Multiple event types
|
|
75
|
+
@SagaHandler('inventory.failed', 'inventory.compensated')
|
|
76
|
+
async handleInventoryIssue(event: IncomingEvent, emit: Emit) {}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**`SagaHandlerOptions`**:
|
|
80
|
+
|
|
81
|
+
| Field | Type | Description |
|
|
82
|
+
|-------|------|-------------|
|
|
83
|
+
| `final` | `boolean` | Marks the handler as the last step. Auto-adds `hint: 'final'` to all emitted events. |
|
|
84
|
+
| `fork` | `boolean \| ForkConfig` | Every `emit()` inside the handler creates a new sub-saga. The framework generates a new `sagaId`, adds `hint: 'fork'`, and propagates `parentSagaId`/`rootSagaId`. |
|
|
85
|
+
|
|
86
|
+
`final` and `fork` are **mutually exclusive** — using both throws `SagaInvalidHandlerConfigError`.
|
|
87
|
+
|
|
88
|
+
**Constraint**: Each event type must have exactly one handler across all participants. Duplicate registrations throw `SagaDuplicateHandlerError`.
|
|
89
|
+
|
|
90
|
+
### `SagaParticipantBase`
|
|
91
|
+
|
|
92
|
+
Abstract base class for saga participants.
|
|
93
|
+
|
|
94
|
+
| Property / Method | Type | Description |
|
|
95
|
+
|-------------------|------|-------------|
|
|
96
|
+
| `serviceId` | `string` (abstract) | Unique service identifier |
|
|
97
|
+
| `on` | `Record<string, EventHandler>` | Auto-populated handler registry |
|
|
98
|
+
| `onRetryExhausted?()` | `(event, error, emit) => Promise<void>` | Called when `SagaRetryableError` exceeds `maxRetries` |
|
|
99
|
+
|
|
100
|
+
### `SagaPublisherProvider`
|
|
101
|
+
|
|
102
|
+
Injectable service to initiate sagas or emit events. See [Core Functions](../doc/core-functions.md) for detailed semantics.
|
|
103
|
+
|
|
104
|
+
| Method | Description |
|
|
105
|
+
|--------|-------------|
|
|
106
|
+
| `start(fn, opts?)` | Start a new root saga |
|
|
107
|
+
| `startChild(fn, opts?)` | Start a child saga linked to the current context |
|
|
108
|
+
| `emit(params)` | Emit an event in the current saga context |
|
|
109
|
+
| `emitToParent(params \| fn)` | Emit to the parent saga |
|
|
110
|
+
| `forSaga(sagaId, parentCtx?, causationId?)` | Get a bound `Emit` function for manual use |
|
|
111
|
+
|
|
112
|
+
### Types
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
type Emit = <T extends object>(params: EmitParams<T>) => Promise<void>;
|
|
116
|
+
|
|
117
|
+
interface EmitParams<T extends object = Record<string, unknown>> {
|
|
118
|
+
eventType: string; // Event type (also used as topic name)
|
|
119
|
+
stepName: string; // Logical step name for tracing
|
|
120
|
+
stepDescription?: string;
|
|
121
|
+
payload: T; // Event payload
|
|
122
|
+
hint?: EventHint; // 'compensation' | 'final' | 'fork'
|
|
123
|
+
key?: string; // Optional partition key
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface IncomingEvent<T = Record<string, unknown>> {
|
|
127
|
+
eventId: string;
|
|
128
|
+
sagaId: string;
|
|
129
|
+
parentSagaId?: string;
|
|
130
|
+
rootSagaId: string;
|
|
131
|
+
causationId: string;
|
|
132
|
+
eventType: string;
|
|
133
|
+
sagaName?: string;
|
|
134
|
+
sagaDescription?: string;
|
|
135
|
+
stepName: string;
|
|
136
|
+
stepDescription?: string;
|
|
137
|
+
occurredAt: string;
|
|
138
|
+
payload: T;
|
|
139
|
+
key?: string;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
See [Concepts](../doc/concepts.md) for detailed explanations of each field.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Examples
|
|
148
|
+
|
|
149
|
+
### 1. Simple Handler + Emit
|
|
150
|
+
|
|
151
|
+
A basic participant that handles one event and emits another.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// payment.participant.ts
|
|
155
|
+
import { Injectable } from '@nestjs/common';
|
|
156
|
+
import {
|
|
157
|
+
SagaParticipant,
|
|
158
|
+
SagaParticipantBase,
|
|
159
|
+
SagaHandler,
|
|
160
|
+
} from '@fbsm/saga-nestjs';
|
|
161
|
+
import type { IncomingEvent, Emit } from '@fbsm/saga-core';
|
|
162
|
+
|
|
163
|
+
@Injectable()
|
|
164
|
+
@SagaParticipant()
|
|
165
|
+
export class PaymentParticipant extends SagaParticipantBase {
|
|
166
|
+
readonly serviceId = 'payment-service';
|
|
167
|
+
|
|
168
|
+
@SagaHandler('order.created')
|
|
169
|
+
async handleOrderCreated(event: IncomingEvent, emit: Emit): Promise<void> {
|
|
170
|
+
const { orderId, amount } = event.payload as {
|
|
171
|
+
orderId: string;
|
|
172
|
+
amount: number;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Process payment...
|
|
176
|
+
const transactionId = `txn-${Date.now()}`;
|
|
177
|
+
|
|
178
|
+
await emit({
|
|
179
|
+
eventType: 'payment.completed',
|
|
180
|
+
stepName: 'process-payment',
|
|
181
|
+
payload: { orderId, transactionId, amount },
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// orders.controller.ts
|
|
189
|
+
import { Controller, Post, Body } from '@nestjs/common';
|
|
190
|
+
import { SagaPublisherProvider } from '@fbsm/saga-nestjs';
|
|
191
|
+
|
|
192
|
+
@Controller('orders')
|
|
193
|
+
export class OrdersController {
|
|
194
|
+
constructor(private readonly sagaPublisher: SagaPublisherProvider) {}
|
|
195
|
+
|
|
196
|
+
@Post()
|
|
197
|
+
async create(@Body() body: { amount: number }) {
|
|
198
|
+
const { sagaId } = await this.sagaPublisher.start(async () => {
|
|
199
|
+
await this.sagaPublisher.emit({
|
|
200
|
+
eventType: 'order.created',
|
|
201
|
+
stepName: 'create-order',
|
|
202
|
+
payload: { orderId: `order-${Date.now()}`, amount: body.amount },
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return { sagaId };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Flow**: `POST /orders` → `order.created` → `PaymentParticipant` → `payment.completed`
|
|
212
|
+
|
|
213
|
+
### 2. Complex: Fork + Fan-Out
|
|
214
|
+
|
|
215
|
+
A handler that forks N sub-sagas and coordinates their completion via `emitToParent()`.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// bulk-orchestration.participant.ts
|
|
219
|
+
import { Injectable } from '@nestjs/common';
|
|
220
|
+
import {
|
|
221
|
+
SagaParticipant,
|
|
222
|
+
SagaParticipantBase,
|
|
223
|
+
SagaHandler,
|
|
224
|
+
SagaPublisherProvider,
|
|
225
|
+
} from '@fbsm/saga-nestjs';
|
|
226
|
+
import type { IncomingEvent, Emit } from '@fbsm/saga-core';
|
|
227
|
+
|
|
228
|
+
@Injectable()
|
|
229
|
+
@SagaParticipant()
|
|
230
|
+
export class BulkOrchestrationParticipant extends SagaParticipantBase {
|
|
231
|
+
readonly serviceId = 'bulk-orchestration';
|
|
232
|
+
|
|
233
|
+
private completionCount = new Map<string, { total: number; done: number }>();
|
|
234
|
+
|
|
235
|
+
constructor(private readonly sagaPublisher: SagaPublisherProvider) {
|
|
236
|
+
super();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Each emit() inside this handler creates a separate sub-saga
|
|
240
|
+
@SagaHandler('bulk.requested', { fork: true })
|
|
241
|
+
async handleBulkRequested(event: IncomingEvent, emit: Emit): Promise<void> {
|
|
242
|
+
const { batchId, items } = event.payload as {
|
|
243
|
+
batchId: string;
|
|
244
|
+
items: string[];
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
this.completionCount.set(batchId, { total: items.length, done: 0 });
|
|
248
|
+
|
|
249
|
+
// N emits = N sub-sagas (each gets its own sagaId)
|
|
250
|
+
for (const item of items) {
|
|
251
|
+
await emit({
|
|
252
|
+
eventType: 'item.processing.requested',
|
|
253
|
+
stepName: 'request-item-processing',
|
|
254
|
+
payload: { batchId, item },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Called when each sub-saga completes
|
|
260
|
+
@SagaHandler('item.processing.completed')
|
|
261
|
+
async handleItemCompleted(event: IncomingEvent): Promise<void> {
|
|
262
|
+
const { batchId } = event.payload as { batchId: string };
|
|
263
|
+
|
|
264
|
+
const counter = this.completionCount.get(batchId);
|
|
265
|
+
if (!counter) return;
|
|
266
|
+
|
|
267
|
+
counter.done++;
|
|
268
|
+
|
|
269
|
+
// Fan-in: when all sub-sagas complete, notify parent
|
|
270
|
+
if (counter.done >= counter.total) {
|
|
271
|
+
this.completionCount.delete(batchId);
|
|
272
|
+
|
|
273
|
+
await this.sagaPublisher.emitToParent({
|
|
274
|
+
eventType: 'bulk.completed',
|
|
275
|
+
stepName: 'complete-bulk',
|
|
276
|
+
payload: { batchId, totalProcessed: counter.total },
|
|
277
|
+
hint: 'final',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// item-processor.participant.ts
|
|
286
|
+
@Injectable()
|
|
287
|
+
@SagaParticipant()
|
|
288
|
+
export class ItemProcessorParticipant extends SagaParticipantBase {
|
|
289
|
+
readonly serviceId = 'item-processor';
|
|
290
|
+
|
|
291
|
+
@SagaHandler('item.processing.requested', { final: true })
|
|
292
|
+
async handleProcessing(event: IncomingEvent, emit: Emit): Promise<void> {
|
|
293
|
+
const { batchId, item } = event.payload as {
|
|
294
|
+
batchId: string;
|
|
295
|
+
item: string;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Process item...
|
|
299
|
+
|
|
300
|
+
await emit({
|
|
301
|
+
eventType: 'item.processing.completed',
|
|
302
|
+
stepName: 'process-item',
|
|
303
|
+
payload: { batchId, item, status: 'done' },
|
|
304
|
+
});
|
|
305
|
+
// hint: 'final' auto-added by framework
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Flow**:
|
|
311
|
+
```
|
|
312
|
+
POST /bulk → bulk.requested [fork x N]
|
|
313
|
+
Sub-saga 1: item.processing.requested → item.processing.completed [final]
|
|
314
|
+
Sub-saga 2: item.processing.requested → item.processing.completed [final]
|
|
315
|
+
Sub-saga N: item.processing.requested → item.processing.completed [final]
|
|
316
|
+
← emitToParent() when all complete
|
|
317
|
+
→ bulk.completed [final]
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### 3. AsyncLocalStorage Context (Fork + Final)
|
|
321
|
+
|
|
322
|
+
Full flow showing how AsyncLocalStorage context propagates through `start()` → handler → fork → final → `emitToParent()`.
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// orchestration.participant.ts
|
|
326
|
+
@Injectable()
|
|
327
|
+
@SagaParticipant()
|
|
328
|
+
export class OrchestrationParticipant extends SagaParticipantBase {
|
|
329
|
+
readonly serviceId = 'orchestration';
|
|
330
|
+
|
|
331
|
+
// Fork: each emit creates a sub-saga with its own context
|
|
332
|
+
// Inside the handler, SagaContext.current() returns the PARENT saga context
|
|
333
|
+
// The framework wraps each emit in a NEW context for the sub-saga
|
|
334
|
+
@SagaHandler('task.requested', { fork: true })
|
|
335
|
+
async handleTaskRequested(event: IncomingEvent, emit: Emit): Promise<void> {
|
|
336
|
+
const { taskId } = event.payload as { taskId: string };
|
|
337
|
+
|
|
338
|
+
// This emit runs in a sub-saga context automatically:
|
|
339
|
+
// - New sagaId generated
|
|
340
|
+
// - parentSagaId = event.sagaId (the parent)
|
|
341
|
+
// - rootSagaId inherited
|
|
342
|
+
// - hint: 'fork' auto-added
|
|
343
|
+
await emit({
|
|
344
|
+
eventType: 'validation.requested',
|
|
345
|
+
stepName: 'request-validation',
|
|
346
|
+
payload: { taskId },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Parent receives the result when sub-saga calls emitToParent()
|
|
351
|
+
@SagaHandler('task.completed', { final: true })
|
|
352
|
+
async handleTaskCompleted(event: IncomingEvent, emit: Emit): Promise<void> {
|
|
353
|
+
const { taskId, result } = event.payload as {
|
|
354
|
+
taskId: string;
|
|
355
|
+
result: string;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
await emit({
|
|
359
|
+
eventType: 'task.done',
|
|
360
|
+
stepName: 'finish-task',
|
|
361
|
+
payload: { taskId, result },
|
|
362
|
+
});
|
|
363
|
+
// hint: 'final' auto-added
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// validator.participant.ts
|
|
370
|
+
@Injectable()
|
|
371
|
+
@SagaParticipant()
|
|
372
|
+
export class ValidatorParticipant extends SagaParticipantBase {
|
|
373
|
+
readonly serviceId = 'validator';
|
|
374
|
+
|
|
375
|
+
constructor(private readonly sagaPublisher: SagaPublisherProvider) {
|
|
376
|
+
super();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Final handler in the sub-saga
|
|
380
|
+
@SagaHandler('validation.requested', { final: true })
|
|
381
|
+
async handleValidation(event: IncomingEvent, emit: Emit): Promise<void> {
|
|
382
|
+
const { taskId } = event.payload as { taskId: string };
|
|
383
|
+
|
|
384
|
+
// Emit within the sub-saga (hint: 'final' auto-added)
|
|
385
|
+
await emit({
|
|
386
|
+
eventType: 'validation.completed',
|
|
387
|
+
stepName: 'validate',
|
|
388
|
+
payload: { taskId, valid: true },
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Report back to parent saga
|
|
392
|
+
// emitToParent reads parentSagaId from AsyncLocalStorage
|
|
393
|
+
await this.sagaPublisher.emitToParent({
|
|
394
|
+
eventType: 'task.completed',
|
|
395
|
+
stepName: 'report-to-parent',
|
|
396
|
+
payload: { taskId, result: 'validated' },
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// controller
|
|
404
|
+
@Post('tasks')
|
|
405
|
+
async createTask() {
|
|
406
|
+
// start() creates a root saga context in AsyncLocalStorage
|
|
407
|
+
const { sagaId } = await this.sagaPublisher.start(async () => {
|
|
408
|
+
// emit() reads sagaId from ALS automatically
|
|
409
|
+
await this.sagaPublisher.emit({
|
|
410
|
+
eventType: 'task.requested',
|
|
411
|
+
stepName: 'create-task',
|
|
412
|
+
payload: { taskId: `task-${Date.now()}` },
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
return { sagaId };
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Context flow**:
|
|
420
|
+
```
|
|
421
|
+
start() → ALS context: { sagaId: A, rootSagaId: A }
|
|
422
|
+
→ task.requested (handler runs in context A)
|
|
423
|
+
→ fork creates: { sagaId: B, rootSagaId: A, parentSagaId: A }
|
|
424
|
+
→ validation.requested (handler runs in context B)
|
|
425
|
+
→ emit() reads from ALS → publishes on saga B
|
|
426
|
+
→ emitToParent() reads parentSagaId from ALS → publishes on saga A
|
|
427
|
+
→ task.completed (handler runs in context A, { final: true })
|
|
428
|
+
→ task.done [final]
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 4. Manual Mode (Without AsyncLocalStorage)
|
|
432
|
+
|
|
433
|
+
For when you need full control or can't use callbacks (e.g., Express middleware, testing).
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import { Controller, Post, Body } from '@nestjs/common';
|
|
437
|
+
import { v7 as uuidv7 } from 'uuid';
|
|
438
|
+
import { SagaPublisherProvider } from '@fbsm/saga-nestjs';
|
|
439
|
+
|
|
440
|
+
@Controller('orders')
|
|
441
|
+
export class ManualOrdersController {
|
|
442
|
+
constructor(private readonly sagaPublisher: SagaPublisherProvider) {}
|
|
443
|
+
|
|
444
|
+
@Post()
|
|
445
|
+
async create(@Body() body: { amount: number }) {
|
|
446
|
+
// Generate your own saga ID
|
|
447
|
+
const sagaId = uuidv7();
|
|
448
|
+
|
|
449
|
+
// Get a bound emit function (no ALS needed)
|
|
450
|
+
const emit = this.sagaPublisher.forSaga(sagaId);
|
|
451
|
+
|
|
452
|
+
await emit({
|
|
453
|
+
eventType: 'order.created',
|
|
454
|
+
stepName: 'create-order',
|
|
455
|
+
payload: { orderId: `order-${Date.now()}`, amount: body.amount },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return { sagaId };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
@Post('with-child')
|
|
462
|
+
async createWithChild(@Body() body: { amount: number }) {
|
|
463
|
+
const sagaId = uuidv7();
|
|
464
|
+
const rootEmit = this.sagaPublisher.forSaga(sagaId);
|
|
465
|
+
|
|
466
|
+
await rootEmit({
|
|
467
|
+
eventType: 'order.created',
|
|
468
|
+
stepName: 'create-order',
|
|
469
|
+
payload: { orderId: `order-${Date.now()}`, amount: body.amount },
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Manually create a child saga
|
|
473
|
+
const childSagaId = uuidv7();
|
|
474
|
+
const childEmit = this.sagaPublisher.forSaga(childSagaId, {
|
|
475
|
+
parentSagaId: sagaId,
|
|
476
|
+
rootSagaId: sagaId,
|
|
477
|
+
}, sagaId); // causationId
|
|
478
|
+
|
|
479
|
+
await childEmit({
|
|
480
|
+
eventType: 'fulfillment.started',
|
|
481
|
+
stepName: 'start-fulfillment',
|
|
482
|
+
payload: { orderId: `order-${Date.now()}` },
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
return { sagaId, childSagaId };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**When to use manual mode**:
|
|
491
|
+
- Testing: create emit functions with known saga IDs
|
|
492
|
+
- Non-callback patterns: when wrapping in a callback is impractical
|
|
493
|
+
- Migration: gradually adopting the library in existing code
|
|
494
|
+
- External triggers: when the saga ID is provided externally
|
|
495
|
+
|
|
496
|
+
> **Tip**: Prefer the callback style (`start(fn)`) for new code — it handles context propagation automatically and is less error-prone.
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Further Reading
|
|
501
|
+
|
|
502
|
+
- [Concepts](../doc/concepts.md) — sagaId, hint, eventType, and other domain terms
|
|
503
|
+
- [Core Functions](../doc/core-functions.md) — detailed semantics of emit, emitToParent, etc.
|
|
504
|
+
- [@fbsm/saga-core API](../saga-core/README.md) — framework-agnostic core reference
|
|
505
|
+
- [@fbsm/saga-transport-kafka API](../saga-transport-kafka/README.md) — Kafka transport options
|
|
506
|
+
- [Example Projects](../../examples/README.md) — production-realistic demo applications
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
20
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
21
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
22
|
+
if (decorator = decorators[i])
|
|
23
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
24
|
+
if (kind && result) __defProp(target, key, result);
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
28
|
+
|
|
29
|
+
// src/index.ts
|
|
30
|
+
var index_exports = {};
|
|
31
|
+
__export(index_exports, {
|
|
32
|
+
SAGA_OPTIONS_TOKEN: () => SAGA_OPTIONS_TOKEN,
|
|
33
|
+
SAGA_TRANSPORT_TOKEN: () => SAGA_TRANSPORT_TOKEN,
|
|
34
|
+
SagaDuplicateHandlerError: () => import_saga_core4.SagaDuplicateHandlerError,
|
|
35
|
+
SagaError: () => import_saga_core4.SagaError,
|
|
36
|
+
SagaHandler: () => SagaHandler,
|
|
37
|
+
SagaModule: () => SagaModule,
|
|
38
|
+
SagaParseError: () => import_saga_core4.SagaParseError,
|
|
39
|
+
SagaParticipant: () => SagaParticipant,
|
|
40
|
+
SagaParticipantBase: () => SagaParticipantBase,
|
|
41
|
+
SagaPublisherProvider: () => SagaPublisherProvider,
|
|
42
|
+
SagaRetryableError: () => import_saga_core4.SagaRetryableError,
|
|
43
|
+
SagaTransportNotConnectedError: () => import_saga_core4.SagaTransportNotConnectedError
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/saga.module.ts
|
|
48
|
+
var import_common3 = require("@nestjs/common");
|
|
49
|
+
var import_core2 = require("@nestjs/core");
|
|
50
|
+
var import_saga_core3 = require("@fbsm/saga-core");
|
|
51
|
+
|
|
52
|
+
// src/constants.ts
|
|
53
|
+
var SAGA_OPTIONS_TOKEN = /* @__PURE__ */ Symbol("SAGA_OPTIONS_TOKEN");
|
|
54
|
+
var SAGA_TRANSPORT_TOKEN = /* @__PURE__ */ Symbol("SAGA_TRANSPORT_TOKEN");
|
|
55
|
+
var SAGA_PARTICIPANT_METADATA = /* @__PURE__ */ Symbol("SAGA_PARTICIPANT_METADATA");
|
|
56
|
+
var SAGA_HANDLER_METADATA = /* @__PURE__ */ Symbol("SAGA_HANDLER_METADATA");
|
|
57
|
+
var SAGA_HANDLER_OPTIONS_METADATA = /* @__PURE__ */ Symbol("SAGA_HANDLER_OPTIONS_METADATA");
|
|
58
|
+
|
|
59
|
+
// src/providers/saga-runner.provider.ts
|
|
60
|
+
var import_common = require("@nestjs/common");
|
|
61
|
+
var import_core = require("@nestjs/core");
|
|
62
|
+
var import_saga_core = require("@fbsm/saga-core");
|
|
63
|
+
var SagaRunnerProvider = class {
|
|
64
|
+
constructor(discoveryService, registry, runner) {
|
|
65
|
+
this.discoveryService = discoveryService;
|
|
66
|
+
this.registry = registry;
|
|
67
|
+
this.runner = runner;
|
|
68
|
+
}
|
|
69
|
+
logger = new import_common.Logger("SagaRunner");
|
|
70
|
+
async onModuleInit() {
|
|
71
|
+
const providers = this.discoveryService.getProviders();
|
|
72
|
+
for (const wrapper of providers) {
|
|
73
|
+
const instance = wrapper.instance;
|
|
74
|
+
if (!instance || !instance.constructor) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const isParticipant = Reflect.getMetadata(
|
|
78
|
+
SAGA_PARTICIPANT_METADATA,
|
|
79
|
+
instance.constructor
|
|
80
|
+
);
|
|
81
|
+
if (!isParticipant) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const handlesMap = Reflect.getMetadata(SAGA_HANDLER_METADATA, instance.constructor);
|
|
85
|
+
if (!handlesMap || handlesMap.size === 0) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const on = {};
|
|
89
|
+
for (const [eventType, methodName] of handlesMap.entries()) {
|
|
90
|
+
const method = instance[methodName];
|
|
91
|
+
if (typeof method === "function") {
|
|
92
|
+
on[eventType] = method.bind(instance);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if ("on" in instance && typeof instance.on === "object") {
|
|
96
|
+
Object.assign(instance.on, on);
|
|
97
|
+
}
|
|
98
|
+
const handlerOptionsMap = Reflect.getMetadata(SAGA_HANDLER_OPTIONS_METADATA, instance.constructor);
|
|
99
|
+
const handlerOptions = {};
|
|
100
|
+
if (handlerOptionsMap) {
|
|
101
|
+
for (const [eventType, opts] of handlerOptionsMap.entries()) {
|
|
102
|
+
handlerOptions[eventType] = { final: opts.final, fork: !!opts.fork };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const serviceId = instance.serviceId ?? "unknown";
|
|
106
|
+
this.logger.log(
|
|
107
|
+
`Registered participant "${serviceId}" handling: [${Object.keys(on).join(", ")}]`
|
|
108
|
+
);
|
|
109
|
+
this.registry.register({
|
|
110
|
+
serviceId,
|
|
111
|
+
on,
|
|
112
|
+
handlerOptions: Object.keys(handlerOptions).length > 0 ? handlerOptions : void 0,
|
|
113
|
+
onRetryExhausted: typeof instance.onRetryExhausted === "function" ? instance.onRetryExhausted.bind(instance) : void 0
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
await this.runner.start();
|
|
117
|
+
}
|
|
118
|
+
async onModuleDestroy() {
|
|
119
|
+
await this.runner.stop();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
SagaRunnerProvider = __decorateClass([
|
|
123
|
+
(0, import_common.Injectable)(),
|
|
124
|
+
__decorateParam(0, (0, import_common.Inject)(import_core.DiscoveryService)),
|
|
125
|
+
__decorateParam(1, (0, import_common.Inject)(import_saga_core.SagaRegistry)),
|
|
126
|
+
__decorateParam(2, (0, import_common.Inject)(import_saga_core.SagaRunner))
|
|
127
|
+
], SagaRunnerProvider);
|
|
128
|
+
|
|
129
|
+
// src/providers/saga-publisher.provider.ts
|
|
130
|
+
var import_common2 = require("@nestjs/common");
|
|
131
|
+
var import_saga_core2 = require("@fbsm/saga-core");
|
|
132
|
+
var SagaPublisherProvider = class {
|
|
133
|
+
constructor(publisher) {
|
|
134
|
+
this.publisher = publisher;
|
|
135
|
+
}
|
|
136
|
+
start(fn, opts) {
|
|
137
|
+
return this.publisher.start(fn, opts);
|
|
138
|
+
}
|
|
139
|
+
startChild(fn, opts) {
|
|
140
|
+
return this.publisher.startChild(fn, opts);
|
|
141
|
+
}
|
|
142
|
+
emit(params) {
|
|
143
|
+
return this.publisher.emit(params);
|
|
144
|
+
}
|
|
145
|
+
emitToParent(paramsOrFn) {
|
|
146
|
+
return this.publisher.emitToParent(paramsOrFn);
|
|
147
|
+
}
|
|
148
|
+
forSaga(sagaId, parentCtx, causationId) {
|
|
149
|
+
return this.publisher.forSaga(sagaId, parentCtx, causationId);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
SagaPublisherProvider = __decorateClass([
|
|
153
|
+
(0, import_common2.Injectable)(),
|
|
154
|
+
__decorateParam(0, (0, import_common2.Inject)(import_saga_core2.SagaPublisher))
|
|
155
|
+
], SagaPublisherProvider);
|
|
156
|
+
|
|
157
|
+
// src/saga.module.ts
|
|
158
|
+
var SagaModule = class {
|
|
159
|
+
static forRoot(options) {
|
|
160
|
+
const otelCtx = (0, import_saga_core3.createOtelContext)(options.otel?.enabled ?? false);
|
|
161
|
+
const registry = new import_saga_core3.SagaRegistry();
|
|
162
|
+
const parser = new import_saga_core3.SagaParser(otelCtx);
|
|
163
|
+
const publisher = new import_saga_core3.SagaPublisher(options.transport, otelCtx, options.topicPrefix);
|
|
164
|
+
const runner = new import_saga_core3.SagaRunner(registry, options.transport, publisher, parser, options, otelCtx, options.logger);
|
|
165
|
+
return {
|
|
166
|
+
module: SagaModule,
|
|
167
|
+
imports: [import_core2.DiscoveryModule],
|
|
168
|
+
global: true,
|
|
169
|
+
providers: [
|
|
170
|
+
{ provide: SAGA_OPTIONS_TOKEN, useValue: options },
|
|
171
|
+
{ provide: SAGA_TRANSPORT_TOKEN, useValue: options.transport },
|
|
172
|
+
{ provide: import_saga_core3.SagaRegistry, useValue: registry },
|
|
173
|
+
{ provide: import_saga_core3.SagaParser, useValue: parser },
|
|
174
|
+
{ provide: import_saga_core3.SagaPublisher, useValue: publisher },
|
|
175
|
+
{ provide: import_saga_core3.SagaRunner, useValue: runner },
|
|
176
|
+
SagaRunnerProvider,
|
|
177
|
+
SagaPublisherProvider
|
|
178
|
+
],
|
|
179
|
+
exports: [SagaPublisherProvider, import_saga_core3.SagaPublisher, SAGA_OPTIONS_TOKEN]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
static forRootAsync(options) {
|
|
183
|
+
const asyncProviders = [
|
|
184
|
+
{
|
|
185
|
+
provide: SAGA_OPTIONS_TOKEN,
|
|
186
|
+
useFactory: options.useFactory,
|
|
187
|
+
inject: options.inject ?? []
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
provide: SAGA_TRANSPORT_TOKEN,
|
|
191
|
+
useFactory: (opts) => opts.transport,
|
|
192
|
+
inject: [SAGA_OPTIONS_TOKEN]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
provide: import_saga_core3.SagaRegistry,
|
|
196
|
+
useFactory: () => new import_saga_core3.SagaRegistry()
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
provide: import_saga_core3.SagaParser,
|
|
200
|
+
useFactory: (opts) => {
|
|
201
|
+
const otelCtx = (0, import_saga_core3.createOtelContext)(opts.otel?.enabled ?? false);
|
|
202
|
+
return new import_saga_core3.SagaParser(otelCtx);
|
|
203
|
+
},
|
|
204
|
+
inject: [SAGA_OPTIONS_TOKEN]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
provide: import_saga_core3.SagaPublisher,
|
|
208
|
+
useFactory: (opts) => {
|
|
209
|
+
const otelCtx = (0, import_saga_core3.createOtelContext)(opts.otel?.enabled ?? false);
|
|
210
|
+
return new import_saga_core3.SagaPublisher(opts.transport, otelCtx, opts.topicPrefix);
|
|
211
|
+
},
|
|
212
|
+
inject: [SAGA_OPTIONS_TOKEN]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
provide: import_saga_core3.SagaRunner,
|
|
216
|
+
useFactory: (registry, publisher, parser, opts) => {
|
|
217
|
+
const otelCtx = (0, import_saga_core3.createOtelContext)(opts.otel?.enabled ?? false);
|
|
218
|
+
return new import_saga_core3.SagaRunner(registry, opts.transport, publisher, parser, opts, otelCtx, opts.logger);
|
|
219
|
+
},
|
|
220
|
+
inject: [import_saga_core3.SagaRegistry, import_saga_core3.SagaPublisher, import_saga_core3.SagaParser, SAGA_OPTIONS_TOKEN]
|
|
221
|
+
}
|
|
222
|
+
];
|
|
223
|
+
return {
|
|
224
|
+
module: SagaModule,
|
|
225
|
+
imports: [...options.imports ?? [], import_core2.DiscoveryModule],
|
|
226
|
+
global: true,
|
|
227
|
+
providers: [...asyncProviders, SagaRunnerProvider, SagaPublisherProvider],
|
|
228
|
+
exports: [SagaPublisherProvider, import_saga_core3.SagaPublisher, SAGA_OPTIONS_TOKEN]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
SagaModule = __decorateClass([
|
|
233
|
+
(0, import_common3.Module)({})
|
|
234
|
+
], SagaModule);
|
|
235
|
+
|
|
236
|
+
// src/decorators/saga-handler.decorator.ts
|
|
237
|
+
function SagaHandler(...args) {
|
|
238
|
+
let eventTypes;
|
|
239
|
+
let options = {};
|
|
240
|
+
const lastArg = args[args.length - 1];
|
|
241
|
+
if (typeof lastArg === "object" && lastArg !== null) {
|
|
242
|
+
options = lastArg;
|
|
243
|
+
eventTypes = args.slice(0, -1);
|
|
244
|
+
} else {
|
|
245
|
+
eventTypes = args;
|
|
246
|
+
}
|
|
247
|
+
return (target, propertyKey) => {
|
|
248
|
+
const existingMap = Reflect.getMetadata(SAGA_HANDLER_METADATA, target.constructor) ?? /* @__PURE__ */ new Map();
|
|
249
|
+
const existingOptions = Reflect.getMetadata(SAGA_HANDLER_OPTIONS_METADATA, target.constructor) ?? /* @__PURE__ */ new Map();
|
|
250
|
+
for (const eventType of eventTypes) {
|
|
251
|
+
existingMap.set(eventType, propertyKey);
|
|
252
|
+
if (options.final || options.fork) {
|
|
253
|
+
existingOptions.set(eventType, options);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
Reflect.defineMetadata(SAGA_HANDLER_METADATA, existingMap, target.constructor);
|
|
257
|
+
Reflect.defineMetadata(SAGA_HANDLER_OPTIONS_METADATA, existingOptions, target.constructor);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/decorators/saga-participant.decorator.ts
|
|
262
|
+
function SagaParticipant() {
|
|
263
|
+
return (target) => {
|
|
264
|
+
Reflect.defineMetadata(SAGA_PARTICIPANT_METADATA, true, target);
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/saga-participant-base.ts
|
|
269
|
+
var SagaParticipantBase = class {
|
|
270
|
+
on = {};
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/index.ts
|
|
274
|
+
var import_saga_core4 = require("@fbsm/saga-core");
|
|
275
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
276
|
+
0 && (module.exports = {
|
|
277
|
+
SAGA_OPTIONS_TOKEN,
|
|
278
|
+
SAGA_TRANSPORT_TOKEN,
|
|
279
|
+
SagaDuplicateHandlerError,
|
|
280
|
+
SagaError,
|
|
281
|
+
SagaHandler,
|
|
282
|
+
SagaModule,
|
|
283
|
+
SagaParseError,
|
|
284
|
+
SagaParticipant,
|
|
285
|
+
SagaParticipantBase,
|
|
286
|
+
SagaPublisherProvider,
|
|
287
|
+
SagaRetryableError,
|
|
288
|
+
SagaTransportNotConnectedError
|
|
289
|
+
});
|
|
290
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/saga.module.ts","../src/constants.ts","../src/providers/saga-runner.provider.ts","../src/providers/saga-publisher.provider.ts","../src/decorators/saga-handler.decorator.ts","../src/decorators/saga-participant.decorator.ts","../src/saga-participant-base.ts"],"sourcesContent":["// Module\nexport { SagaModule } from './saga.module';\n\n// Provider\nexport { SagaPublisherProvider } from './providers/saga-publisher.provider';\n\n// Decorators\nexport { SagaHandler } from './decorators/saga-handler.decorator';\nexport type { SagaHandlerOptions } from './decorators/saga-handler.decorator';\nexport { SagaParticipant } from './decorators/saga-participant.decorator';\n\n// Base class\nexport { SagaParticipantBase } from './saga-participant-base';\n\n// Constants (tokens for advanced usage)\nexport { SAGA_OPTIONS_TOKEN, SAGA_TRANSPORT_TOKEN } from './constants';\n\n// Options\nexport type { SagaModuleOptions, SagaModuleAsyncOptions } from './saga-module-options.interface';\n\n// Re-exports from core for consumer convenience\nexport {\n SagaError,\n SagaRetryableError,\n SagaDuplicateHandlerError,\n SagaParseError,\n SagaTransportNotConnectedError,\n} from '@fbsm/saga-core';\nexport type {\n SagaEvent,\n IncomingEvent,\n Emit,\n EmitParams,\n EventHint,\n EventHandler,\n ParentSagaContext,\n HandlerConfig,\n SagaTransport,\n} from '@fbsm/saga-core';\n","import { Module, DynamicModule, type Provider } from '@nestjs/common';\nimport { DiscoveryModule } from '@nestjs/core';\nimport {\n SagaRunner,\n SagaRegistry,\n SagaPublisher,\n SagaParser,\n createOtelContext,\n} from '@fbsm/saga-core';\nimport { SAGA_OPTIONS_TOKEN, SAGA_TRANSPORT_TOKEN } from './constants';\nimport type { SagaModuleOptions, SagaModuleAsyncOptions } from './saga-module-options.interface';\nimport { SagaRunnerProvider } from './providers/saga-runner.provider';\nimport { SagaPublisherProvider } from './providers/saga-publisher.provider';\n\n@Module({})\nexport class SagaModule {\n static forRoot(options: SagaModuleOptions): DynamicModule {\n const otelCtx = createOtelContext(options.otel?.enabled ?? false);\n const registry = new SagaRegistry();\n const parser = new SagaParser(otelCtx);\n const publisher = new SagaPublisher(options.transport, otelCtx, options.topicPrefix);\n const runner = new SagaRunner(registry, options.transport, publisher, parser, options, otelCtx, options.logger);\n\n return {\n module: SagaModule,\n imports: [DiscoveryModule],\n global: true,\n providers: [\n { provide: SAGA_OPTIONS_TOKEN, useValue: options },\n { provide: SAGA_TRANSPORT_TOKEN, useValue: options.transport },\n { provide: SagaRegistry, useValue: registry },\n { provide: SagaParser, useValue: parser },\n { provide: SagaPublisher, useValue: publisher },\n { provide: SagaRunner, useValue: runner },\n SagaRunnerProvider,\n SagaPublisherProvider,\n ],\n exports: [SagaPublisherProvider, SagaPublisher, SAGA_OPTIONS_TOKEN],\n };\n }\n\n static forRootAsync(options: SagaModuleAsyncOptions): DynamicModule {\n const asyncProviders: Provider[] = [\n {\n provide: SAGA_OPTIONS_TOKEN,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n },\n {\n provide: SAGA_TRANSPORT_TOKEN,\n useFactory: (opts: SagaModuleOptions) => opts.transport,\n inject: [SAGA_OPTIONS_TOKEN],\n },\n {\n provide: SagaRegistry,\n useFactory: () => new SagaRegistry(),\n },\n {\n provide: SagaParser,\n useFactory: (opts: SagaModuleOptions) => {\n const otelCtx = createOtelContext(opts.otel?.enabled ?? false);\n return new SagaParser(otelCtx);\n },\n inject: [SAGA_OPTIONS_TOKEN],\n },\n {\n provide: SagaPublisher,\n useFactory: (opts: SagaModuleOptions) => {\n const otelCtx = createOtelContext(opts.otel?.enabled ?? false);\n return new SagaPublisher(opts.transport, otelCtx, opts.topicPrefix);\n },\n inject: [SAGA_OPTIONS_TOKEN],\n },\n {\n provide: SagaRunner,\n useFactory: (\n registry: SagaRegistry,\n publisher: SagaPublisher,\n parser: SagaParser,\n opts: SagaModuleOptions,\n ) => {\n const otelCtx = createOtelContext(opts.otel?.enabled ?? false);\n return new SagaRunner(registry, opts.transport, publisher, parser, opts, otelCtx, opts.logger);\n },\n inject: [SagaRegistry, SagaPublisher, SagaParser, SAGA_OPTIONS_TOKEN],\n },\n ];\n\n return {\n module: SagaModule,\n imports: [...(options.imports ?? []), DiscoveryModule],\n global: true,\n providers: [...asyncProviders, SagaRunnerProvider, SagaPublisherProvider],\n exports: [SagaPublisherProvider, SagaPublisher, SAGA_OPTIONS_TOKEN],\n };\n }\n}\n","export const SAGA_OPTIONS_TOKEN = Symbol('SAGA_OPTIONS_TOKEN');\nexport const SAGA_TRANSPORT_TOKEN = Symbol('SAGA_TRANSPORT_TOKEN');\nexport const SAGA_PARTICIPANT_METADATA = Symbol('SAGA_PARTICIPANT_METADATA');\nexport const SAGA_HANDLER_METADATA = Symbol('SAGA_HANDLER_METADATA');\nexport const SAGA_HANDLER_OPTIONS_METADATA = Symbol('SAGA_HANDLER_OPTIONS_METADATA');\n","import { Inject, Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';\nimport { DiscoveryService } from '@nestjs/core';\nimport { SagaRunner, SagaRegistry } from '@fbsm/saga-core';\nimport type { EventHandler } from '@fbsm/saga-core';\nimport { SAGA_PARTICIPANT_METADATA, SAGA_HANDLER_METADATA, SAGA_HANDLER_OPTIONS_METADATA } from '../constants';\nimport type { SagaHandlerOptions } from '../decorators/saga-handler.decorator';\nimport type { HandlerConfig } from '@fbsm/saga-core';\n\n@Injectable()\nexport class SagaRunnerProvider implements OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger('SagaRunner');\n\n constructor(\n @Inject(DiscoveryService) private readonly discoveryService: DiscoveryService,\n @Inject(SagaRegistry) private readonly registry: SagaRegistry,\n @Inject(SagaRunner) private readonly runner: SagaRunner,\n ) {}\n\n async onModuleInit(): Promise<void> {\n const providers = this.discoveryService.getProviders();\n\n for (const wrapper of providers) {\n const instance = wrapper.instance;\n if (!instance || !instance.constructor) {\n continue;\n }\n\n const isParticipant = Reflect.getMetadata(\n SAGA_PARTICIPANT_METADATA,\n instance.constructor,\n );\n\n if (!isParticipant) {\n continue;\n }\n\n const handlesMap: Map<string, string | symbol> | undefined =\n Reflect.getMetadata(SAGA_HANDLER_METADATA, instance.constructor);\n\n if (!handlesMap || handlesMap.size === 0) {\n continue;\n }\n\n const on: Record<string, EventHandler<any>> = {};\n \n for (const [eventType, methodName] of handlesMap.entries()) {\n const method = (instance as any)[methodName];\n if (typeof method === 'function') {\n on[eventType] = method.bind(instance);\n }\n }\n\n // Populate the `on` property if instance extends SagaParticipantBase\n if ('on' in instance && typeof instance.on === 'object') {\n Object.assign(instance.on, on);\n }\n\n // Extract handler options (final, fork, etc.) from decorator metadata\n const handlerOptionsMap: Map<string, SagaHandlerOptions> | undefined =\n Reflect.getMetadata(SAGA_HANDLER_OPTIONS_METADATA, instance.constructor);\n\n const handlerOptions: Record<string, HandlerConfig> = {};\n if (handlerOptionsMap) {\n for (const [eventType, opts] of handlerOptionsMap.entries()) {\n handlerOptions[eventType] = { final: opts.final, fork: !!opts.fork };\n }\n }\n\n const serviceId = (instance as any).serviceId ?? 'unknown';\n this.logger.log(\n `Registered participant \"${serviceId}\" handling: [${Object.keys(on).join(', ')}]`,\n );\n\n this.registry.register({\n serviceId,\n on,\n handlerOptions: Object.keys(handlerOptions).length > 0 ? handlerOptions : undefined,\n onRetryExhausted:\n typeof (instance as any).onRetryExhausted === 'function'\n ? (instance as any).onRetryExhausted.bind(instance)\n : undefined,\n });\n }\n\n await this.runner.start();\n }\n\n async onModuleDestroy(): Promise<void> {\n await this.runner.stop();\n }\n}\n","import { Inject, Injectable } from '@nestjs/common';\nimport type { Emit, EmitParams, ParentSagaContext, SagaStartOptions } from '@fbsm/saga-core';\nimport { SagaPublisher } from '@fbsm/saga-core';\n\n@Injectable()\nexport class SagaPublisherProvider {\n constructor(@Inject(SagaPublisher) private readonly publisher: SagaPublisher) {}\n\n start<R>(fn: () => R | Promise<R>, opts?: SagaStartOptions): Promise<{ sagaId: string; result: Awaited<R> }> {\n return this.publisher.start(fn, opts);\n }\n\n startChild<R>(fn: () => R | Promise<R>, opts?: SagaStartOptions): Promise<{ sagaId: string; result: Awaited<R> }> {\n return this.publisher.startChild(fn, opts);\n }\n\n emit<T extends object>(params: EmitParams<T>): Promise<void> {\n return this.publisher.emit(params);\n }\n\n emitToParent<T extends object>(paramsOrFn: EmitParams<T> | (() => void | Promise<void>)): Promise<void> {\n return this.publisher.emitToParent(paramsOrFn);\n }\n\n forSaga(sagaId: string, parentCtx?: ParentSagaContext, causationId?: string): Emit {\n return this.publisher.forSaga(sagaId, parentCtx, causationId);\n }\n}\n","import { SAGA_HANDLER_METADATA, SAGA_HANDLER_OPTIONS_METADATA } from '../constants';\n\nimport type { ForkConfig } from '@fbsm/saga-core';\n\nexport interface SagaHandlerOptions {\n final?: boolean;\n fork?: boolean | ForkConfig;\n}\n\nexport function SagaHandler(\n ...args: [...string[]] | [...string[], SagaHandlerOptions]\n): MethodDecorator {\n let eventTypes: string[];\n let options: SagaHandlerOptions = {};\n\n const lastArg = args[args.length - 1];\n if (typeof lastArg === 'object' && lastArg !== null) {\n options = lastArg as SagaHandlerOptions;\n eventTypes = args.slice(0, -1) as string[];\n } else {\n eventTypes = args as string[];\n }\n\n return (target, propertyKey) => {\n const existingMap: Map<string, string | symbol> =\n Reflect.getMetadata(SAGA_HANDLER_METADATA, target.constructor) ?? new Map();\n const existingOptions: Map<string, SagaHandlerOptions> =\n Reflect.getMetadata(SAGA_HANDLER_OPTIONS_METADATA, target.constructor) ?? new Map();\n\n for (const eventType of eventTypes) {\n existingMap.set(eventType, propertyKey);\n if (options.final || options.fork) {\n existingOptions.set(eventType, options);\n }\n }\n\n Reflect.defineMetadata(SAGA_HANDLER_METADATA, existingMap, target.constructor);\n Reflect.defineMetadata(SAGA_HANDLER_OPTIONS_METADATA, existingOptions, target.constructor);\n };\n}\n","import { SAGA_PARTICIPANT_METADATA } from '../constants';\n\nexport function SagaParticipant(): ClassDecorator {\n return (target) => {\n Reflect.defineMetadata(SAGA_PARTICIPANT_METADATA, true, target);\n };\n}\n","import type {\n SagaParticipant,\n EventHandler,\n IncomingEvent,\n Emit,\n} from '@fbsm/saga-core';\nimport { SagaRetryableError } from '@fbsm/saga-core';\n\nexport abstract class SagaParticipantBase implements SagaParticipant {\n abstract readonly serviceId: string;\n\n readonly on: Record<string, EventHandler<any>> = {};\n\n onRetryExhausted?(\n event: IncomingEvent,\n error: SagaRetryableError,\n emit: Emit,\n ): Promise<void>;\n}\n"],"mappings":"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","names":["import_common","import_core","import_saga_core","import_common","import_saga_core","import_saga_core"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { RunnerOptions, SagaTransport, SagaLogger, SagaPublisher, SagaStartOptions, EmitParams, ParentSagaContext, Emit, ForkConfig, SagaParticipant as SagaParticipant$1, EventHandler, IncomingEvent, SagaRetryableError } from '@fbsm/saga-core';
|
|
3
|
+
export { Emit, EmitParams, EventHandler, EventHint, HandlerConfig, IncomingEvent, ParentSagaContext, SagaDuplicateHandlerError, SagaError, SagaEvent, SagaParseError, SagaRetryableError, SagaTransport, SagaTransportNotConnectedError } from '@fbsm/saga-core';
|
|
4
|
+
|
|
5
|
+
interface SagaModuleOptions extends RunnerOptions {
|
|
6
|
+
transport: SagaTransport;
|
|
7
|
+
otel?: {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
exporterUrl?: string;
|
|
10
|
+
};
|
|
11
|
+
logger?: SagaLogger;
|
|
12
|
+
}
|
|
13
|
+
interface SagaModuleAsyncOptions {
|
|
14
|
+
useFactory: (...args: any[]) => Promise<SagaModuleOptions> | SagaModuleOptions;
|
|
15
|
+
inject?: any[];
|
|
16
|
+
imports?: any[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare class SagaModule {
|
|
20
|
+
static forRoot(options: SagaModuleOptions): DynamicModule;
|
|
21
|
+
static forRootAsync(options: SagaModuleAsyncOptions): DynamicModule;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare class SagaPublisherProvider {
|
|
25
|
+
private readonly publisher;
|
|
26
|
+
constructor(publisher: SagaPublisher);
|
|
27
|
+
start<R>(fn: () => R | Promise<R>, opts?: SagaStartOptions): Promise<{
|
|
28
|
+
sagaId: string;
|
|
29
|
+
result: Awaited<R>;
|
|
30
|
+
}>;
|
|
31
|
+
startChild<R>(fn: () => R | Promise<R>, opts?: SagaStartOptions): Promise<{
|
|
32
|
+
sagaId: string;
|
|
33
|
+
result: Awaited<R>;
|
|
34
|
+
}>;
|
|
35
|
+
emit<T extends object>(params: EmitParams<T>): Promise<void>;
|
|
36
|
+
emitToParent<T extends object>(paramsOrFn: EmitParams<T> | (() => void | Promise<void>)): Promise<void>;
|
|
37
|
+
forSaga(sagaId: string, parentCtx?: ParentSagaContext, causationId?: string): Emit;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SagaHandlerOptions {
|
|
41
|
+
final?: boolean;
|
|
42
|
+
fork?: boolean | ForkConfig;
|
|
43
|
+
}
|
|
44
|
+
declare function SagaHandler(...args: [...string[]] | [...string[], SagaHandlerOptions]): MethodDecorator;
|
|
45
|
+
|
|
46
|
+
declare function SagaParticipant(): ClassDecorator;
|
|
47
|
+
|
|
48
|
+
declare abstract class SagaParticipantBase implements SagaParticipant$1 {
|
|
49
|
+
abstract readonly serviceId: string;
|
|
50
|
+
readonly on: Record<string, EventHandler<any>>;
|
|
51
|
+
onRetryExhausted?(event: IncomingEvent, error: SagaRetryableError, emit: Emit): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare const SAGA_OPTIONS_TOKEN: unique symbol;
|
|
55
|
+
declare const SAGA_TRANSPORT_TOKEN: unique symbol;
|
|
56
|
+
|
|
57
|
+
export { SAGA_OPTIONS_TOKEN, SAGA_TRANSPORT_TOKEN, SagaHandler, type SagaHandlerOptions, SagaModule, type SagaModuleAsyncOptions, type SagaModuleOptions, SagaParticipant, SagaParticipantBase, SagaPublisherProvider };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { RunnerOptions, SagaTransport, SagaLogger, SagaPublisher, SagaStartOptions, EmitParams, ParentSagaContext, Emit, ForkConfig, SagaParticipant as SagaParticipant$1, EventHandler, IncomingEvent, SagaRetryableError } from '@fbsm/saga-core';
|
|
3
|
+
export { Emit, EmitParams, EventHandler, EventHint, HandlerConfig, IncomingEvent, ParentSagaContext, SagaDuplicateHandlerError, SagaError, SagaEvent, SagaParseError, SagaRetryableError, SagaTransport, SagaTransportNotConnectedError } from '@fbsm/saga-core';
|
|
4
|
+
|
|
5
|
+
interface SagaModuleOptions extends RunnerOptions {
|
|
6
|
+
transport: SagaTransport;
|
|
7
|
+
otel?: {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
exporterUrl?: string;
|
|
10
|
+
};
|
|
11
|
+
logger?: SagaLogger;
|
|
12
|
+
}
|
|
13
|
+
interface SagaModuleAsyncOptions {
|
|
14
|
+
useFactory: (...args: any[]) => Promise<SagaModuleOptions> | SagaModuleOptions;
|
|
15
|
+
inject?: any[];
|
|
16
|
+
imports?: any[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare class SagaModule {
|
|
20
|
+
static forRoot(options: SagaModuleOptions): DynamicModule;
|
|
21
|
+
static forRootAsync(options: SagaModuleAsyncOptions): DynamicModule;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare class SagaPublisherProvider {
|
|
25
|
+
private readonly publisher;
|
|
26
|
+
constructor(publisher: SagaPublisher);
|
|
27
|
+
start<R>(fn: () => R | Promise<R>, opts?: SagaStartOptions): Promise<{
|
|
28
|
+
sagaId: string;
|
|
29
|
+
result: Awaited<R>;
|
|
30
|
+
}>;
|
|
31
|
+
startChild<R>(fn: () => R | Promise<R>, opts?: SagaStartOptions): Promise<{
|
|
32
|
+
sagaId: string;
|
|
33
|
+
result: Awaited<R>;
|
|
34
|
+
}>;
|
|
35
|
+
emit<T extends object>(params: EmitParams<T>): Promise<void>;
|
|
36
|
+
emitToParent<T extends object>(paramsOrFn: EmitParams<T> | (() => void | Promise<void>)): Promise<void>;
|
|
37
|
+
forSaga(sagaId: string, parentCtx?: ParentSagaContext, causationId?: string): Emit;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SagaHandlerOptions {
|
|
41
|
+
final?: boolean;
|
|
42
|
+
fork?: boolean | ForkConfig;
|
|
43
|
+
}
|
|
44
|
+
declare function SagaHandler(...args: [...string[]] | [...string[], SagaHandlerOptions]): MethodDecorator;
|
|
45
|
+
|
|
46
|
+
declare function SagaParticipant(): ClassDecorator;
|
|
47
|
+
|
|
48
|
+
declare abstract class SagaParticipantBase implements SagaParticipant$1 {
|
|
49
|
+
abstract readonly serviceId: string;
|
|
50
|
+
readonly on: Record<string, EventHandler<any>>;
|
|
51
|
+
onRetryExhausted?(event: IncomingEvent, error: SagaRetryableError, emit: Emit): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare const SAGA_OPTIONS_TOKEN: unique symbol;
|
|
55
|
+
declare const SAGA_TRANSPORT_TOKEN: unique symbol;
|
|
56
|
+
|
|
57
|
+
export { SAGA_OPTIONS_TOKEN, SAGA_TRANSPORT_TOKEN, SagaHandler, type SagaHandlerOptions, SagaModule, type SagaModuleAsyncOptions, type SagaModuleOptions, SagaParticipant, SagaParticipantBase, SagaPublisherProvider };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
4
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
+
if (decorator = decorators[i])
|
|
7
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
+
if (kind && result) __defProp(target, key, result);
|
|
9
|
+
return result;
|
|
10
|
+
};
|
|
11
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
12
|
+
|
|
13
|
+
// src/saga.module.ts
|
|
14
|
+
import { Module } from "@nestjs/common";
|
|
15
|
+
import { DiscoveryModule } from "@nestjs/core";
|
|
16
|
+
import {
|
|
17
|
+
SagaRunner as SagaRunner2,
|
|
18
|
+
SagaRegistry as SagaRegistry2,
|
|
19
|
+
SagaPublisher as SagaPublisher2,
|
|
20
|
+
SagaParser,
|
|
21
|
+
createOtelContext
|
|
22
|
+
} from "@fbsm/saga-core";
|
|
23
|
+
|
|
24
|
+
// src/constants.ts
|
|
25
|
+
var SAGA_OPTIONS_TOKEN = /* @__PURE__ */ Symbol("SAGA_OPTIONS_TOKEN");
|
|
26
|
+
var SAGA_TRANSPORT_TOKEN = /* @__PURE__ */ Symbol("SAGA_TRANSPORT_TOKEN");
|
|
27
|
+
var SAGA_PARTICIPANT_METADATA = /* @__PURE__ */ Symbol("SAGA_PARTICIPANT_METADATA");
|
|
28
|
+
var SAGA_HANDLER_METADATA = /* @__PURE__ */ Symbol("SAGA_HANDLER_METADATA");
|
|
29
|
+
var SAGA_HANDLER_OPTIONS_METADATA = /* @__PURE__ */ Symbol("SAGA_HANDLER_OPTIONS_METADATA");
|
|
30
|
+
|
|
31
|
+
// src/providers/saga-runner.provider.ts
|
|
32
|
+
import { Inject, Injectable, Logger } from "@nestjs/common";
|
|
33
|
+
import { DiscoveryService } from "@nestjs/core";
|
|
34
|
+
import { SagaRunner, SagaRegistry } from "@fbsm/saga-core";
|
|
35
|
+
var SagaRunnerProvider = class {
|
|
36
|
+
constructor(discoveryService, registry, runner) {
|
|
37
|
+
this.discoveryService = discoveryService;
|
|
38
|
+
this.registry = registry;
|
|
39
|
+
this.runner = runner;
|
|
40
|
+
}
|
|
41
|
+
logger = new Logger("SagaRunner");
|
|
42
|
+
async onModuleInit() {
|
|
43
|
+
const providers = this.discoveryService.getProviders();
|
|
44
|
+
for (const wrapper of providers) {
|
|
45
|
+
const instance = wrapper.instance;
|
|
46
|
+
if (!instance || !instance.constructor) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const isParticipant = Reflect.getMetadata(
|
|
50
|
+
SAGA_PARTICIPANT_METADATA,
|
|
51
|
+
instance.constructor
|
|
52
|
+
);
|
|
53
|
+
if (!isParticipant) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const handlesMap = Reflect.getMetadata(SAGA_HANDLER_METADATA, instance.constructor);
|
|
57
|
+
if (!handlesMap || handlesMap.size === 0) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const on = {};
|
|
61
|
+
for (const [eventType, methodName] of handlesMap.entries()) {
|
|
62
|
+
const method = instance[methodName];
|
|
63
|
+
if (typeof method === "function") {
|
|
64
|
+
on[eventType] = method.bind(instance);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if ("on" in instance && typeof instance.on === "object") {
|
|
68
|
+
Object.assign(instance.on, on);
|
|
69
|
+
}
|
|
70
|
+
const handlerOptionsMap = Reflect.getMetadata(SAGA_HANDLER_OPTIONS_METADATA, instance.constructor);
|
|
71
|
+
const handlerOptions = {};
|
|
72
|
+
if (handlerOptionsMap) {
|
|
73
|
+
for (const [eventType, opts] of handlerOptionsMap.entries()) {
|
|
74
|
+
handlerOptions[eventType] = { final: opts.final, fork: !!opts.fork };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const serviceId = instance.serviceId ?? "unknown";
|
|
78
|
+
this.logger.log(
|
|
79
|
+
`Registered participant "${serviceId}" handling: [${Object.keys(on).join(", ")}]`
|
|
80
|
+
);
|
|
81
|
+
this.registry.register({
|
|
82
|
+
serviceId,
|
|
83
|
+
on,
|
|
84
|
+
handlerOptions: Object.keys(handlerOptions).length > 0 ? handlerOptions : void 0,
|
|
85
|
+
onRetryExhausted: typeof instance.onRetryExhausted === "function" ? instance.onRetryExhausted.bind(instance) : void 0
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
await this.runner.start();
|
|
89
|
+
}
|
|
90
|
+
async onModuleDestroy() {
|
|
91
|
+
await this.runner.stop();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
SagaRunnerProvider = __decorateClass([
|
|
95
|
+
Injectable(),
|
|
96
|
+
__decorateParam(0, Inject(DiscoveryService)),
|
|
97
|
+
__decorateParam(1, Inject(SagaRegistry)),
|
|
98
|
+
__decorateParam(2, Inject(SagaRunner))
|
|
99
|
+
], SagaRunnerProvider);
|
|
100
|
+
|
|
101
|
+
// src/providers/saga-publisher.provider.ts
|
|
102
|
+
import { Inject as Inject2, Injectable as Injectable2 } from "@nestjs/common";
|
|
103
|
+
import { SagaPublisher } from "@fbsm/saga-core";
|
|
104
|
+
var SagaPublisherProvider = class {
|
|
105
|
+
constructor(publisher) {
|
|
106
|
+
this.publisher = publisher;
|
|
107
|
+
}
|
|
108
|
+
start(fn, opts) {
|
|
109
|
+
return this.publisher.start(fn, opts);
|
|
110
|
+
}
|
|
111
|
+
startChild(fn, opts) {
|
|
112
|
+
return this.publisher.startChild(fn, opts);
|
|
113
|
+
}
|
|
114
|
+
emit(params) {
|
|
115
|
+
return this.publisher.emit(params);
|
|
116
|
+
}
|
|
117
|
+
emitToParent(paramsOrFn) {
|
|
118
|
+
return this.publisher.emitToParent(paramsOrFn);
|
|
119
|
+
}
|
|
120
|
+
forSaga(sagaId, parentCtx, causationId) {
|
|
121
|
+
return this.publisher.forSaga(sagaId, parentCtx, causationId);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
SagaPublisherProvider = __decorateClass([
|
|
125
|
+
Injectable2(),
|
|
126
|
+
__decorateParam(0, Inject2(SagaPublisher))
|
|
127
|
+
], SagaPublisherProvider);
|
|
128
|
+
|
|
129
|
+
// src/saga.module.ts
|
|
130
|
+
var SagaModule = class {
|
|
131
|
+
static forRoot(options) {
|
|
132
|
+
const otelCtx = createOtelContext(options.otel?.enabled ?? false);
|
|
133
|
+
const registry = new SagaRegistry2();
|
|
134
|
+
const parser = new SagaParser(otelCtx);
|
|
135
|
+
const publisher = new SagaPublisher2(options.transport, otelCtx, options.topicPrefix);
|
|
136
|
+
const runner = new SagaRunner2(registry, options.transport, publisher, parser, options, otelCtx, options.logger);
|
|
137
|
+
return {
|
|
138
|
+
module: SagaModule,
|
|
139
|
+
imports: [DiscoveryModule],
|
|
140
|
+
global: true,
|
|
141
|
+
providers: [
|
|
142
|
+
{ provide: SAGA_OPTIONS_TOKEN, useValue: options },
|
|
143
|
+
{ provide: SAGA_TRANSPORT_TOKEN, useValue: options.transport },
|
|
144
|
+
{ provide: SagaRegistry2, useValue: registry },
|
|
145
|
+
{ provide: SagaParser, useValue: parser },
|
|
146
|
+
{ provide: SagaPublisher2, useValue: publisher },
|
|
147
|
+
{ provide: SagaRunner2, useValue: runner },
|
|
148
|
+
SagaRunnerProvider,
|
|
149
|
+
SagaPublisherProvider
|
|
150
|
+
],
|
|
151
|
+
exports: [SagaPublisherProvider, SagaPublisher2, SAGA_OPTIONS_TOKEN]
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
static forRootAsync(options) {
|
|
155
|
+
const asyncProviders = [
|
|
156
|
+
{
|
|
157
|
+
provide: SAGA_OPTIONS_TOKEN,
|
|
158
|
+
useFactory: options.useFactory,
|
|
159
|
+
inject: options.inject ?? []
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
provide: SAGA_TRANSPORT_TOKEN,
|
|
163
|
+
useFactory: (opts) => opts.transport,
|
|
164
|
+
inject: [SAGA_OPTIONS_TOKEN]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
provide: SagaRegistry2,
|
|
168
|
+
useFactory: () => new SagaRegistry2()
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
provide: SagaParser,
|
|
172
|
+
useFactory: (opts) => {
|
|
173
|
+
const otelCtx = createOtelContext(opts.otel?.enabled ?? false);
|
|
174
|
+
return new SagaParser(otelCtx);
|
|
175
|
+
},
|
|
176
|
+
inject: [SAGA_OPTIONS_TOKEN]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
provide: SagaPublisher2,
|
|
180
|
+
useFactory: (opts) => {
|
|
181
|
+
const otelCtx = createOtelContext(opts.otel?.enabled ?? false);
|
|
182
|
+
return new SagaPublisher2(opts.transport, otelCtx, opts.topicPrefix);
|
|
183
|
+
},
|
|
184
|
+
inject: [SAGA_OPTIONS_TOKEN]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
provide: SagaRunner2,
|
|
188
|
+
useFactory: (registry, publisher, parser, opts) => {
|
|
189
|
+
const otelCtx = createOtelContext(opts.otel?.enabled ?? false);
|
|
190
|
+
return new SagaRunner2(registry, opts.transport, publisher, parser, opts, otelCtx, opts.logger);
|
|
191
|
+
},
|
|
192
|
+
inject: [SagaRegistry2, SagaPublisher2, SagaParser, SAGA_OPTIONS_TOKEN]
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
return {
|
|
196
|
+
module: SagaModule,
|
|
197
|
+
imports: [...options.imports ?? [], DiscoveryModule],
|
|
198
|
+
global: true,
|
|
199
|
+
providers: [...asyncProviders, SagaRunnerProvider, SagaPublisherProvider],
|
|
200
|
+
exports: [SagaPublisherProvider, SagaPublisher2, SAGA_OPTIONS_TOKEN]
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
SagaModule = __decorateClass([
|
|
205
|
+
Module({})
|
|
206
|
+
], SagaModule);
|
|
207
|
+
|
|
208
|
+
// src/decorators/saga-handler.decorator.ts
|
|
209
|
+
function SagaHandler(...args) {
|
|
210
|
+
let eventTypes;
|
|
211
|
+
let options = {};
|
|
212
|
+
const lastArg = args[args.length - 1];
|
|
213
|
+
if (typeof lastArg === "object" && lastArg !== null) {
|
|
214
|
+
options = lastArg;
|
|
215
|
+
eventTypes = args.slice(0, -1);
|
|
216
|
+
} else {
|
|
217
|
+
eventTypes = args;
|
|
218
|
+
}
|
|
219
|
+
return (target, propertyKey) => {
|
|
220
|
+
const existingMap = Reflect.getMetadata(SAGA_HANDLER_METADATA, target.constructor) ?? /* @__PURE__ */ new Map();
|
|
221
|
+
const existingOptions = Reflect.getMetadata(SAGA_HANDLER_OPTIONS_METADATA, target.constructor) ?? /* @__PURE__ */ new Map();
|
|
222
|
+
for (const eventType of eventTypes) {
|
|
223
|
+
existingMap.set(eventType, propertyKey);
|
|
224
|
+
if (options.final || options.fork) {
|
|
225
|
+
existingOptions.set(eventType, options);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
Reflect.defineMetadata(SAGA_HANDLER_METADATA, existingMap, target.constructor);
|
|
229
|
+
Reflect.defineMetadata(SAGA_HANDLER_OPTIONS_METADATA, existingOptions, target.constructor);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/decorators/saga-participant.decorator.ts
|
|
234
|
+
function SagaParticipant() {
|
|
235
|
+
return (target) => {
|
|
236
|
+
Reflect.defineMetadata(SAGA_PARTICIPANT_METADATA, true, target);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/saga-participant-base.ts
|
|
241
|
+
var SagaParticipantBase = class {
|
|
242
|
+
on = {};
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/index.ts
|
|
246
|
+
import {
|
|
247
|
+
SagaError,
|
|
248
|
+
SagaRetryableError,
|
|
249
|
+
SagaDuplicateHandlerError,
|
|
250
|
+
SagaParseError,
|
|
251
|
+
SagaTransportNotConnectedError
|
|
252
|
+
} from "@fbsm/saga-core";
|
|
253
|
+
export {
|
|
254
|
+
SAGA_OPTIONS_TOKEN,
|
|
255
|
+
SAGA_TRANSPORT_TOKEN,
|
|
256
|
+
SagaDuplicateHandlerError,
|
|
257
|
+
SagaError,
|
|
258
|
+
SagaHandler,
|
|
259
|
+
SagaModule,
|
|
260
|
+
SagaParseError,
|
|
261
|
+
SagaParticipant,
|
|
262
|
+
SagaParticipantBase,
|
|
263
|
+
SagaPublisherProvider,
|
|
264
|
+
SagaRetryableError,
|
|
265
|
+
SagaTransportNotConnectedError
|
|
266
|
+
};
|
|
267
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/saga.module.ts","../src/constants.ts","../src/providers/saga-runner.provider.ts","../src/providers/saga-publisher.provider.ts","../src/decorators/saga-handler.decorator.ts","../src/decorators/saga-participant.decorator.ts","../src/saga-participant-base.ts","../src/index.ts"],"sourcesContent":["import { Module, DynamicModule, type Provider } from '@nestjs/common';\nimport { DiscoveryModule } from '@nestjs/core';\nimport {\n SagaRunner,\n SagaRegistry,\n SagaPublisher,\n SagaParser,\n createOtelContext,\n} from '@fbsm/saga-core';\nimport { SAGA_OPTIONS_TOKEN, SAGA_TRANSPORT_TOKEN } from './constants';\nimport type { SagaModuleOptions, SagaModuleAsyncOptions } from './saga-module-options.interface';\nimport { SagaRunnerProvider } from './providers/saga-runner.provider';\nimport { SagaPublisherProvider } from './providers/saga-publisher.provider';\n\n@Module({})\nexport class SagaModule {\n static forRoot(options: SagaModuleOptions): DynamicModule {\n const otelCtx = createOtelContext(options.otel?.enabled ?? false);\n const registry = new SagaRegistry();\n const parser = new SagaParser(otelCtx);\n const publisher = new SagaPublisher(options.transport, otelCtx, options.topicPrefix);\n const runner = new SagaRunner(registry, options.transport, publisher, parser, options, otelCtx, options.logger);\n\n return {\n module: SagaModule,\n imports: [DiscoveryModule],\n global: true,\n providers: [\n { provide: SAGA_OPTIONS_TOKEN, useValue: options },\n { provide: SAGA_TRANSPORT_TOKEN, useValue: options.transport },\n { provide: SagaRegistry, useValue: registry },\n { provide: SagaParser, useValue: parser },\n { provide: SagaPublisher, useValue: publisher },\n { provide: SagaRunner, useValue: runner },\n SagaRunnerProvider,\n SagaPublisherProvider,\n ],\n exports: [SagaPublisherProvider, SagaPublisher, SAGA_OPTIONS_TOKEN],\n };\n }\n\n static forRootAsync(options: SagaModuleAsyncOptions): DynamicModule {\n const asyncProviders: Provider[] = [\n {\n provide: SAGA_OPTIONS_TOKEN,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n },\n {\n provide: SAGA_TRANSPORT_TOKEN,\n useFactory: (opts: SagaModuleOptions) => opts.transport,\n inject: [SAGA_OPTIONS_TOKEN],\n },\n {\n provide: SagaRegistry,\n useFactory: () => new SagaRegistry(),\n },\n {\n provide: SagaParser,\n useFactory: (opts: SagaModuleOptions) => {\n const otelCtx = createOtelContext(opts.otel?.enabled ?? false);\n return new SagaParser(otelCtx);\n },\n inject: [SAGA_OPTIONS_TOKEN],\n },\n {\n provide: SagaPublisher,\n useFactory: (opts: SagaModuleOptions) => {\n const otelCtx = createOtelContext(opts.otel?.enabled ?? false);\n return new SagaPublisher(opts.transport, otelCtx, opts.topicPrefix);\n },\n inject: [SAGA_OPTIONS_TOKEN],\n },\n {\n provide: SagaRunner,\n useFactory: (\n registry: SagaRegistry,\n publisher: SagaPublisher,\n parser: SagaParser,\n opts: SagaModuleOptions,\n ) => {\n const otelCtx = createOtelContext(opts.otel?.enabled ?? false);\n return new SagaRunner(registry, opts.transport, publisher, parser, opts, otelCtx, opts.logger);\n },\n inject: [SagaRegistry, SagaPublisher, SagaParser, SAGA_OPTIONS_TOKEN],\n },\n ];\n\n return {\n module: SagaModule,\n imports: [...(options.imports ?? []), DiscoveryModule],\n global: true,\n providers: [...asyncProviders, SagaRunnerProvider, SagaPublisherProvider],\n exports: [SagaPublisherProvider, SagaPublisher, SAGA_OPTIONS_TOKEN],\n };\n }\n}\n","export const SAGA_OPTIONS_TOKEN = Symbol('SAGA_OPTIONS_TOKEN');\nexport const SAGA_TRANSPORT_TOKEN = Symbol('SAGA_TRANSPORT_TOKEN');\nexport const SAGA_PARTICIPANT_METADATA = Symbol('SAGA_PARTICIPANT_METADATA');\nexport const SAGA_HANDLER_METADATA = Symbol('SAGA_HANDLER_METADATA');\nexport const SAGA_HANDLER_OPTIONS_METADATA = Symbol('SAGA_HANDLER_OPTIONS_METADATA');\n","import { Inject, Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';\nimport { DiscoveryService } from '@nestjs/core';\nimport { SagaRunner, SagaRegistry } from '@fbsm/saga-core';\nimport type { EventHandler } from '@fbsm/saga-core';\nimport { SAGA_PARTICIPANT_METADATA, SAGA_HANDLER_METADATA, SAGA_HANDLER_OPTIONS_METADATA } from '../constants';\nimport type { SagaHandlerOptions } from '../decorators/saga-handler.decorator';\nimport type { HandlerConfig } from '@fbsm/saga-core';\n\n@Injectable()\nexport class SagaRunnerProvider implements OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger('SagaRunner');\n\n constructor(\n @Inject(DiscoveryService) private readonly discoveryService: DiscoveryService,\n @Inject(SagaRegistry) private readonly registry: SagaRegistry,\n @Inject(SagaRunner) private readonly runner: SagaRunner,\n ) {}\n\n async onModuleInit(): Promise<void> {\n const providers = this.discoveryService.getProviders();\n\n for (const wrapper of providers) {\n const instance = wrapper.instance;\n if (!instance || !instance.constructor) {\n continue;\n }\n\n const isParticipant = Reflect.getMetadata(\n SAGA_PARTICIPANT_METADATA,\n instance.constructor,\n );\n\n if (!isParticipant) {\n continue;\n }\n\n const handlesMap: Map<string, string | symbol> | undefined =\n Reflect.getMetadata(SAGA_HANDLER_METADATA, instance.constructor);\n\n if (!handlesMap || handlesMap.size === 0) {\n continue;\n }\n\n const on: Record<string, EventHandler<any>> = {};\n \n for (const [eventType, methodName] of handlesMap.entries()) {\n const method = (instance as any)[methodName];\n if (typeof method === 'function') {\n on[eventType] = method.bind(instance);\n }\n }\n\n // Populate the `on` property if instance extends SagaParticipantBase\n if ('on' in instance && typeof instance.on === 'object') {\n Object.assign(instance.on, on);\n }\n\n // Extract handler options (final, fork, etc.) from decorator metadata\n const handlerOptionsMap: Map<string, SagaHandlerOptions> | undefined =\n Reflect.getMetadata(SAGA_HANDLER_OPTIONS_METADATA, instance.constructor);\n\n const handlerOptions: Record<string, HandlerConfig> = {};\n if (handlerOptionsMap) {\n for (const [eventType, opts] of handlerOptionsMap.entries()) {\n handlerOptions[eventType] = { final: opts.final, fork: !!opts.fork };\n }\n }\n\n const serviceId = (instance as any).serviceId ?? 'unknown';\n this.logger.log(\n `Registered participant \"${serviceId}\" handling: [${Object.keys(on).join(', ')}]`,\n );\n\n this.registry.register({\n serviceId,\n on,\n handlerOptions: Object.keys(handlerOptions).length > 0 ? handlerOptions : undefined,\n onRetryExhausted:\n typeof (instance as any).onRetryExhausted === 'function'\n ? (instance as any).onRetryExhausted.bind(instance)\n : undefined,\n });\n }\n\n await this.runner.start();\n }\n\n async onModuleDestroy(): Promise<void> {\n await this.runner.stop();\n }\n}\n","import { Inject, Injectable } from '@nestjs/common';\nimport type { Emit, EmitParams, ParentSagaContext, SagaStartOptions } from '@fbsm/saga-core';\nimport { SagaPublisher } from '@fbsm/saga-core';\n\n@Injectable()\nexport class SagaPublisherProvider {\n constructor(@Inject(SagaPublisher) private readonly publisher: SagaPublisher) {}\n\n start<R>(fn: () => R | Promise<R>, opts?: SagaStartOptions): Promise<{ sagaId: string; result: Awaited<R> }> {\n return this.publisher.start(fn, opts);\n }\n\n startChild<R>(fn: () => R | Promise<R>, opts?: SagaStartOptions): Promise<{ sagaId: string; result: Awaited<R> }> {\n return this.publisher.startChild(fn, opts);\n }\n\n emit<T extends object>(params: EmitParams<T>): Promise<void> {\n return this.publisher.emit(params);\n }\n\n emitToParent<T extends object>(paramsOrFn: EmitParams<T> | (() => void | Promise<void>)): Promise<void> {\n return this.publisher.emitToParent(paramsOrFn);\n }\n\n forSaga(sagaId: string, parentCtx?: ParentSagaContext, causationId?: string): Emit {\n return this.publisher.forSaga(sagaId, parentCtx, causationId);\n }\n}\n","import { SAGA_HANDLER_METADATA, SAGA_HANDLER_OPTIONS_METADATA } from '../constants';\n\nimport type { ForkConfig } from '@fbsm/saga-core';\n\nexport interface SagaHandlerOptions {\n final?: boolean;\n fork?: boolean | ForkConfig;\n}\n\nexport function SagaHandler(\n ...args: [...string[]] | [...string[], SagaHandlerOptions]\n): MethodDecorator {\n let eventTypes: string[];\n let options: SagaHandlerOptions = {};\n\n const lastArg = args[args.length - 1];\n if (typeof lastArg === 'object' && lastArg !== null) {\n options = lastArg as SagaHandlerOptions;\n eventTypes = args.slice(0, -1) as string[];\n } else {\n eventTypes = args as string[];\n }\n\n return (target, propertyKey) => {\n const existingMap: Map<string, string | symbol> =\n Reflect.getMetadata(SAGA_HANDLER_METADATA, target.constructor) ?? new Map();\n const existingOptions: Map<string, SagaHandlerOptions> =\n Reflect.getMetadata(SAGA_HANDLER_OPTIONS_METADATA, target.constructor) ?? new Map();\n\n for (const eventType of eventTypes) {\n existingMap.set(eventType, propertyKey);\n if (options.final || options.fork) {\n existingOptions.set(eventType, options);\n }\n }\n\n Reflect.defineMetadata(SAGA_HANDLER_METADATA, existingMap, target.constructor);\n Reflect.defineMetadata(SAGA_HANDLER_OPTIONS_METADATA, existingOptions, target.constructor);\n };\n}\n","import { SAGA_PARTICIPANT_METADATA } from '../constants';\n\nexport function SagaParticipant(): ClassDecorator {\n return (target) => {\n Reflect.defineMetadata(SAGA_PARTICIPANT_METADATA, true, target);\n };\n}\n","import type {\n SagaParticipant,\n EventHandler,\n IncomingEvent,\n Emit,\n} from '@fbsm/saga-core';\nimport { SagaRetryableError } from '@fbsm/saga-core';\n\nexport abstract class SagaParticipantBase implements SagaParticipant {\n abstract readonly serviceId: string;\n\n readonly on: Record<string, EventHandler<any>> = {};\n\n onRetryExhausted?(\n event: IncomingEvent,\n error: SagaRetryableError,\n emit: Emit,\n ): Promise<void>;\n}\n","// Module\nexport { SagaModule } from './saga.module';\n\n// Provider\nexport { SagaPublisherProvider } from './providers/saga-publisher.provider';\n\n// Decorators\nexport { SagaHandler } from './decorators/saga-handler.decorator';\nexport type { SagaHandlerOptions } from './decorators/saga-handler.decorator';\nexport { SagaParticipant } from './decorators/saga-participant.decorator';\n\n// Base class\nexport { SagaParticipantBase } from './saga-participant-base';\n\n// Constants (tokens for advanced usage)\nexport { SAGA_OPTIONS_TOKEN, SAGA_TRANSPORT_TOKEN } from './constants';\n\n// Options\nexport type { SagaModuleOptions, SagaModuleAsyncOptions } from './saga-module-options.interface';\n\n// Re-exports from core for consumer convenience\nexport {\n SagaError,\n SagaRetryableError,\n SagaDuplicateHandlerError,\n SagaParseError,\n SagaTransportNotConnectedError,\n} from '@fbsm/saga-core';\nexport type {\n SagaEvent,\n IncomingEvent,\n Emit,\n EmitParams,\n EventHint,\n EventHandler,\n ParentSagaContext,\n HandlerConfig,\n SagaTransport,\n} from '@fbsm/saga-core';\n"],"mappings":"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","names":["SagaRunner","SagaRegistry","SagaPublisher","Inject","Injectable","Injectable","Inject","SagaRegistry","SagaPublisher","SagaRunner"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fbsm/saga-nestjs",
|
|
3
|
+
"version": "0.0.1-beta.0",
|
|
4
|
+
"description": "NestJS integration for saga choreography: dynamic module, decorators, auto-discovery",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "fbsm",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/fbsm/saga-library",
|
|
10
|
+
"directory": "packages/saga-nestjs"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"saga",
|
|
14
|
+
"nestjs",
|
|
15
|
+
"choreography",
|
|
16
|
+
"microservices",
|
|
17
|
+
"event-driven"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/index.d.cts",
|
|
34
|
+
"default": "./dist/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@fbsm/saga-core": "0.0.1-beta.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@nestjs/common": "^10.0.0",
|
|
46
|
+
"@nestjs/core": "^10.0.0",
|
|
47
|
+
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@nestjs/common": "^10.0.0",
|
|
51
|
+
"@nestjs/core": "^10.0.0",
|
|
52
|
+
"@nestjs/testing": "^10.0.0",
|
|
53
|
+
"reflect-metadata": "^0.2.0",
|
|
54
|
+
"rxjs": "^7.8.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"dev": "tsup --watch",
|
|
59
|
+
"typecheck": "tsc --noEmit"
|
|
60
|
+
}
|
|
61
|
+
}
|