@event-driven-io/emmett-esdb 0.43.0-beta.12 → 0.43.0-beta.14
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 +420 -0
- package/dist/index.cjs +403 -1461
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -41
- package/dist/index.d.ts +52 -41
- package/dist/index.js +384 -1453
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# @event-driven-io/emmett-esdb
|
|
2
|
+
|
|
3
|
+
EventStoreDB adapter for the Emmett event sourcing library, providing event store operations and subscription-based consumers with built-in retry logic.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package connects Emmett to [EventStoreDB](https://www.eventstore.com/), enabling you to persist events to EventStoreDB streams and consume them using subscription-based processors. It handles the translation between Emmett's event sourcing abstractions and EventStoreDB's native client, including optimistic concurrency control, global position tracking, and resilient subscription management.
|
|
8
|
+
|
|
9
|
+
## Key Concepts
|
|
10
|
+
|
|
11
|
+
- **EventStoreDBEventStore**: Extended EventStore interface that returns global positions on append operations and provides consumer factory methods
|
|
12
|
+
- **Consumer**: Background processor that subscribes to EventStoreDB streams and routes events to reactors or projectors
|
|
13
|
+
- **Reactor**: Message handler for side effects (sending emails, calling APIs, triggering workflows)
|
|
14
|
+
- **Projector**: Message handler that builds read models from events
|
|
15
|
+
- **$all subscription**: Subscribe to all events across all streams in EventStoreDB
|
|
16
|
+
- **Stream subscription**: Subscribe to events in a specific stream or category
|
|
17
|
+
- **Checkpoint**: Position tracking for resumable subscriptions (uses stream revision or global position)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @event-driven-io/emmett-esdb @event-driven-io/emmett @eventstore/db-client
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Both `@event-driven-io/emmett` and `@eventstore/db-client` are peer dependencies and must be installed alongside this package.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### Creating an Event Store
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { EventStoreDBClient } from '@eventstore/db-client';
|
|
33
|
+
import { getEventStoreDBEventStore } from '@event-driven-io/emmett-esdb';
|
|
34
|
+
|
|
35
|
+
// Connect to EventStoreDB
|
|
36
|
+
const client = EventStoreDBClient.connectionString(
|
|
37
|
+
'esdb://localhost:2113?tls=false',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Create the Emmett event store adapter
|
|
41
|
+
const eventStore = getEventStoreDBEventStore(client);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Appending Events
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import type { Event } from '@event-driven-io/emmett';
|
|
48
|
+
|
|
49
|
+
// Define your event types
|
|
50
|
+
type GuestCheckedIn = Event<
|
|
51
|
+
'GuestCheckedIn',
|
|
52
|
+
{ guestId: string; roomNumber: string }
|
|
53
|
+
>;
|
|
54
|
+
type GuestCheckedOut = Event<'GuestCheckedOut', { guestId: string }>;
|
|
55
|
+
type GuestStayEvent = GuestCheckedIn | GuestCheckedOut;
|
|
56
|
+
|
|
57
|
+
// Append events to a stream
|
|
58
|
+
const guestId = 'guest-123';
|
|
59
|
+
const streamName = `guestStay-${guestId}`;
|
|
60
|
+
|
|
61
|
+
const result = await eventStore.appendToStream<GuestStayEvent>(streamName, [
|
|
62
|
+
{ type: 'GuestCheckedIn', data: { guestId, roomNumber: '101' } },
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
console.log('Stream version:', result.nextExpectedStreamVersion);
|
|
66
|
+
console.log('Global position:', result.lastEventGlobalPosition);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Reading Events
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Read all events from a stream
|
|
73
|
+
const { events, currentStreamVersion } =
|
|
74
|
+
await eventStore.readStream<GuestStayEvent>(streamName);
|
|
75
|
+
|
|
76
|
+
// Aggregate stream state using a reducer
|
|
77
|
+
const { state, currentStreamVersion } = await eventStore.aggregateStream<
|
|
78
|
+
GuestState,
|
|
79
|
+
GuestStayEvent
|
|
80
|
+
>(streamName, {
|
|
81
|
+
evolve: (state, event) => {
|
|
82
|
+
switch (event.type) {
|
|
83
|
+
case 'GuestCheckedIn':
|
|
84
|
+
return {
|
|
85
|
+
...state,
|
|
86
|
+
status: 'checked-in',
|
|
87
|
+
roomNumber: event.data.roomNumber,
|
|
88
|
+
};
|
|
89
|
+
case 'GuestCheckedOut':
|
|
90
|
+
return { ...state, status: 'checked-out' };
|
|
91
|
+
default:
|
|
92
|
+
return state;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
initialState: () => ({ status: 'unknown', roomNumber: undefined }),
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Subscribing to Events with a Reactor
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import {
|
|
103
|
+
$all,
|
|
104
|
+
eventStoreDBEventStoreConsumer,
|
|
105
|
+
} from '@event-driven-io/emmett-esdb';
|
|
106
|
+
|
|
107
|
+
// Create a consumer that subscribes to all events
|
|
108
|
+
const consumer = eventStoreDBEventStoreConsumer({
|
|
109
|
+
connectionString: 'esdb://localhost:2113?tls=false',
|
|
110
|
+
from: { stream: $all },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Add a reactor for handling events
|
|
114
|
+
consumer.reactor<GuestStayEvent>({
|
|
115
|
+
processorId: 'guest-notifications',
|
|
116
|
+
eachMessage: async (event) => {
|
|
117
|
+
if (event.type === 'GuestCheckedIn') {
|
|
118
|
+
console.log(
|
|
119
|
+
`Guest ${event.data.guestId} checked into room ${event.data.roomNumber}`,
|
|
120
|
+
);
|
|
121
|
+
// Send welcome email, update availability, etc.
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Start consuming
|
|
127
|
+
await consumer.start();
|
|
128
|
+
|
|
129
|
+
// Later, stop gracefully
|
|
130
|
+
await consumer.close();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Building Read Models with a Projector
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import {
|
|
137
|
+
inMemoryProjector,
|
|
138
|
+
inMemorySingleStreamProjection,
|
|
139
|
+
getInMemoryDatabase,
|
|
140
|
+
type ReadEvent,
|
|
141
|
+
} from '@event-driven-io/emmett';
|
|
142
|
+
import { eventStoreDBEventStoreConsumer } from '@event-driven-io/emmett-esdb';
|
|
143
|
+
|
|
144
|
+
// Define your read model
|
|
145
|
+
type GuestStaySummary = {
|
|
146
|
+
_id?: string;
|
|
147
|
+
guestId: string;
|
|
148
|
+
status: 'checked-in' | 'checked-out';
|
|
149
|
+
roomNumber?: string;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Create the projection
|
|
153
|
+
const guestStaySummaryProjection = inMemorySingleStreamProjection<
|
|
154
|
+
GuestStayEvent,
|
|
155
|
+
GuestStaySummary
|
|
156
|
+
>({
|
|
157
|
+
collectionName: 'guestStaySummaries',
|
|
158
|
+
canHandle: ['GuestCheckedIn', 'GuestCheckedOut'],
|
|
159
|
+
evolve: (document, event: ReadEvent<GuestStayEvent>) => {
|
|
160
|
+
switch (event.type) {
|
|
161
|
+
case 'GuestCheckedIn':
|
|
162
|
+
return {
|
|
163
|
+
...document,
|
|
164
|
+
guestId: event.data.guestId,
|
|
165
|
+
status: 'checked-in',
|
|
166
|
+
roomNumber: event.data.roomNumber,
|
|
167
|
+
};
|
|
168
|
+
case 'GuestCheckedOut':
|
|
169
|
+
return { ...document, status: 'checked-out' };
|
|
170
|
+
default:
|
|
171
|
+
return document;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
initialState: () => ({
|
|
175
|
+
guestId: '',
|
|
176
|
+
status: 'checked-out',
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Set up the in-memory database and projector
|
|
181
|
+
const database = getInMemoryDatabase();
|
|
182
|
+
|
|
183
|
+
const projector = inMemoryProjector<GuestStayEvent>({
|
|
184
|
+
processorId: 'guest-summary-projector',
|
|
185
|
+
projection: guestStaySummaryProjection,
|
|
186
|
+
connectionOptions: { database },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Create consumer with the projector
|
|
190
|
+
const consumer = eventStoreDBEventStoreConsumer<GuestStayEvent>({
|
|
191
|
+
connectionString: 'esdb://localhost:2113?tls=false',
|
|
192
|
+
processors: [projector],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await consumer.start();
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## How-to Guides
|
|
199
|
+
|
|
200
|
+
### Subscribe to a Specific Stream
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const consumer = eventStoreDBEventStoreConsumer({
|
|
204
|
+
connectionString: 'esdb://localhost:2113?tls=false',
|
|
205
|
+
from: { stream: 'guestStay-guest-123' },
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Subscribe to a Category Stream
|
|
210
|
+
|
|
211
|
+
EventStoreDB supports category projections with the `$ce-` prefix:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const consumer = eventStoreDBEventStoreConsumer({
|
|
215
|
+
connectionString: 'esdb://localhost:2113?tls=false',
|
|
216
|
+
from: {
|
|
217
|
+
stream: '$ce-guestStay',
|
|
218
|
+
options: { resolveLinkTos: true },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Resume from a Checkpoint
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
consumer.reactor<GuestStayEvent>({
|
|
227
|
+
processorId: 'my-processor',
|
|
228
|
+
startFrom: { lastCheckpoint: 1000n }, // Resume from global position 1000
|
|
229
|
+
eachMessage: async (event) => {
|
|
230
|
+
// Handle event
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Start from Current Position
|
|
236
|
+
|
|
237
|
+
Use `'CURRENT'` to start from where the processor last stopped (requires checkpoint storage):
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
consumer.reactor<GuestStayEvent>({
|
|
241
|
+
processorId: 'my-processor',
|
|
242
|
+
startFrom: 'CURRENT',
|
|
243
|
+
connectionOptions: { database }, // In-memory database for checkpoint storage
|
|
244
|
+
eachMessage: async (event) => {
|
|
245
|
+
// Handle event
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Configure Retry Options
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import type { AsyncRetryOptions } from '@event-driven-io/emmett';
|
|
254
|
+
|
|
255
|
+
const consumer = eventStoreDBEventStoreConsumer({
|
|
256
|
+
connectionString: 'esdb://localhost:2113?tls=false',
|
|
257
|
+
resilience: {
|
|
258
|
+
resubscribeOptions: {
|
|
259
|
+
forever: true,
|
|
260
|
+
minTimeout: 100,
|
|
261
|
+
factor: 1.5,
|
|
262
|
+
} satisfies AsyncRetryOptions,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Use with Optimistic Concurrency
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
271
|
+
|
|
272
|
+
// Expect stream to not exist (first event)
|
|
273
|
+
await eventStore.appendToStream(streamName, events, {
|
|
274
|
+
expectedStreamVersion: STREAM_DOES_NOT_EXIST,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Expect specific version
|
|
278
|
+
await eventStore.appendToStream(streamName, events, {
|
|
279
|
+
expectedStreamVersion: 5n,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Expect stream to exist (any version)
|
|
283
|
+
await eventStore.appendToStream(streamName, events, {
|
|
284
|
+
expectedStreamVersion: STREAM_EXISTS,
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Stop Processing After a Condition
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
consumer.reactor<GuestStayEvent>({
|
|
292
|
+
processorId: 'my-processor',
|
|
293
|
+
stopAfter: (event) => event.metadata.globalPosition >= targetPosition,
|
|
294
|
+
eachMessage: async (event) => {
|
|
295
|
+
// Handle event
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## API Reference
|
|
301
|
+
|
|
302
|
+
### getEventStoreDBEventStore
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
function getEventStoreDBEventStore(
|
|
306
|
+
client: EventStoreDBClient,
|
|
307
|
+
): EventStoreDBEventStore;
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Creates an Emmett event store adapter from an EventStoreDB client.
|
|
311
|
+
|
|
312
|
+
### EventStoreDBEventStore
|
|
313
|
+
|
|
314
|
+
Extended `EventStore` interface with:
|
|
315
|
+
|
|
316
|
+
| Method | Description |
|
|
317
|
+
| ---------------------------------------------- | -------------------------------------------- |
|
|
318
|
+
| `appendToStream(streamName, events, options?)` | Append events and return global position |
|
|
319
|
+
| `readStream(streamName, options?)` | Read events from a stream |
|
|
320
|
+
| `aggregateStream(streamName, options)` | Aggregate stream state using evolve function |
|
|
321
|
+
| `consumer(options?)` | Create a subscription-based consumer |
|
|
322
|
+
|
|
323
|
+
### eventStoreDBEventStoreConsumer
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
function eventStoreDBEventStoreConsumer<MessageType>(
|
|
327
|
+
options: EventStoreDBEventStoreConsumerOptions<MessageType>,
|
|
328
|
+
): EventStoreDBEventStoreConsumer<MessageType>;
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Options:**
|
|
332
|
+
|
|
333
|
+
| Property | Type | Description |
|
|
334
|
+
| ------------------------------- | ------------------------------------ | -------------------------------------------------- |
|
|
335
|
+
| `connectionString` | `string` | EventStoreDB connection string |
|
|
336
|
+
| `client` | `EventStoreDBClient` | Alternative: provide client directly |
|
|
337
|
+
| `from` | `EventStoreDBEventStoreConsumerType` | Stream to subscribe to (`$all` or specific stream) |
|
|
338
|
+
| `processors` | `MessageProcessor[]` | Pre-configured processors |
|
|
339
|
+
| `pulling.batchSize` | `number` | Messages per batch (default: 100) |
|
|
340
|
+
| `resilience.resubscribeOptions` | `AsyncRetryOptions` | Retry configuration |
|
|
341
|
+
|
|
342
|
+
**Consumer Methods:**
|
|
343
|
+
|
|
344
|
+
| Method | Description |
|
|
345
|
+
| -------------------- | ---------------------------- |
|
|
346
|
+
| `reactor(options)` | Create a reactor processor |
|
|
347
|
+
| `projector(options)` | Create a projector processor |
|
|
348
|
+
| `start()` | Start consuming events |
|
|
349
|
+
| `stop()` | Stop consuming (can restart) |
|
|
350
|
+
| `close()` | Stop and clean up resources |
|
|
351
|
+
|
|
352
|
+
### EventStoreDBReadEventMetadata
|
|
353
|
+
|
|
354
|
+
Metadata included with each read event:
|
|
355
|
+
|
|
356
|
+
| Property | Type | Description |
|
|
357
|
+
| ---------------- | -------- | -------------------------- |
|
|
358
|
+
| `eventId` | `string` | Unique event identifier |
|
|
359
|
+
| `streamName` | `string` | Source stream name |
|
|
360
|
+
| `streamPosition` | `bigint` | Position within the stream |
|
|
361
|
+
| `globalPosition` | `bigint` | Position in the global log |
|
|
362
|
+
| `checkpoint` | `bigint` | Position for resumption |
|
|
363
|
+
|
|
364
|
+
### Subscription Start Options
|
|
365
|
+
|
|
366
|
+
| Value | Description |
|
|
367
|
+
| ---------------------------- | ---------------------------------------- |
|
|
368
|
+
| `'BEGINNING'` | Start from the first event |
|
|
369
|
+
| `'END'` | Start from current end (new events only) |
|
|
370
|
+
| `'CURRENT'` | Resume from last stored checkpoint |
|
|
371
|
+
| `{ lastCheckpoint: bigint }` | Resume from specific position |
|
|
372
|
+
|
|
373
|
+
## Architecture
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
+------------------+ +------------------------+ +------------------+
|
|
377
|
+
| Your Code | --> | EventStoreDBEventStore | --> | EventStoreDB |
|
|
378
|
+
+------------------+ +------------------------+ +------------------+
|
|
379
|
+
|
|
|
380
|
+
v
|
|
381
|
+
+------------------+
|
|
382
|
+
| Consumer |
|
|
383
|
+
+------------------+
|
|
384
|
+
|
|
|
385
|
+
+--------------+--------------+
|
|
386
|
+
| |
|
|
387
|
+
v v
|
|
388
|
+
+----------------+ +----------------+
|
|
389
|
+
| Reactor | | Projector |
|
|
390
|
+
| (Side Effects) | | (Read Models) |
|
|
391
|
+
+----------------+ +----------------+
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Data Flow:**
|
|
395
|
+
|
|
396
|
+
1. Events are appended to EventStoreDB through the event store adapter
|
|
397
|
+
2. The consumer subscribes to `$all` or specific streams
|
|
398
|
+
3. Events flow through Node.js Transform streams for backpressure handling
|
|
399
|
+
4. Reactors and projectors process events sequentially
|
|
400
|
+
5. Checkpoints are stored for resumable processing
|
|
401
|
+
|
|
402
|
+
**Retry Behavior:**
|
|
403
|
+
|
|
404
|
+
The adapter includes built-in retry logic for database unavailability (gRPC error code 14). Default retry options:
|
|
405
|
+
|
|
406
|
+
- Retries forever
|
|
407
|
+
- Minimum timeout: 100ms
|
|
408
|
+
- Exponential backoff factor: 1.5
|
|
409
|
+
|
|
410
|
+
## Dependencies
|
|
411
|
+
|
|
412
|
+
### Peer Dependencies
|
|
413
|
+
|
|
414
|
+
- `@event-driven-io/emmett` - Core Emmett library
|
|
415
|
+
- `@eventstore/db-client` (^6.2.1) - Official EventStoreDB JavaScript client
|
|
416
|
+
|
|
417
|
+
### Internal Dependencies
|
|
418
|
+
|
|
419
|
+
- Node.js `stream` module for Transform/Writable stream handling
|
|
420
|
+
- `uuid` for generating consumer IDs
|