@event-driven-io/emmett-esdb 0.43.0-beta.11 → 0.43.0-beta.13

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 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