@event-driven-io/emmett 0.43.0-beta.12 → 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,591 @@
1
+ # @event-driven-io/emmett
2
+
3
+ Core event sourcing library for TypeScript providing event stores, command handling, projections, and testing utilities built around the Decider pattern.
4
+
5
+ ## Purpose
6
+
7
+ Emmett provides the foundational abstractions for building event-sourced applications in TypeScript with strong typing and a clean, functional architecture.
8
+
9
+ **Without Emmett, you would have to:**
10
+
11
+ - Manually implement event store interfaces and version management
12
+ - Build your own optimistic concurrency control mechanisms
13
+ - Create custom command handling pipelines with retry logic
14
+ - Write projection infrastructure from scratch
15
+ - Develop testing utilities for event-sourced systems
16
+ - Handle the complexity of message processing and checkpointing
17
+
18
+ ## Key Concepts
19
+
20
+ ### The Decider Pattern
21
+
22
+ Emmett is built around the **Decider pattern**, which separates business logic into three pure functions:
23
+
24
+ ```typescript
25
+ type Decider<State, CommandType, StreamEvent> = {
26
+ // Determines what events occur when a command is applied to current state
27
+ decide: (command: CommandType, state: State) => StreamEvent | StreamEvent[];
28
+
29
+ // Evolves state based on an event (reducer/fold)
30
+ evolve: (currentState: State, event: StreamEvent) => State;
31
+
32
+ // Provides the starting state for a new aggregate
33
+ initialState: () => State;
34
+ };
35
+ ```
36
+
37
+ ### Events and Commands
38
+
39
+ **Events** represent facts that have happened:
40
+
41
+ ```typescript
42
+ type Event<EventType, EventData, EventMetaData> = {
43
+ type: EventType;
44
+ data: EventData;
45
+ metadata?: EventMetaData;
46
+ };
47
+ ```
48
+
49
+ **Commands** represent intentions to change state:
50
+
51
+ ```typescript
52
+ type Command<CommandType, CommandData, CommandMetaData> = {
53
+ type: CommandType;
54
+ data: CommandData;
55
+ metadata?: CommandMetaData;
56
+ };
57
+ ```
58
+
59
+ ### EventStore
60
+
61
+ The `EventStore` interface provides three core operations:
62
+
63
+ - `appendToStream` - Append events to a stream with optimistic concurrency
64
+ - `readStream` - Read events from a stream
65
+ - `aggregateStream` - Rebuild state by folding events with an evolve function
66
+
67
+ ### Optimistic Concurrency
68
+
69
+ Emmett uses `ExpectedStreamVersion` for concurrency control:
70
+
71
+ - Specific version (bigint) - Must match exactly
72
+ - `STREAM_EXISTS` - Stream must have events
73
+ - `STREAM_DOES_NOT_EXIST` - Stream must be new
74
+ - `NO_CONCURRENCY_CHECK` - Skip version validation
75
+
76
+ ## Installation
77
+
78
+ ```bash
79
+ npm install @event-driven-io/emmett
80
+ # or
81
+ pnpm add @event-driven-io/emmett
82
+ # or
83
+ yarn add @event-driven-io/emmett
84
+ ```
85
+
86
+ ### Peer Dependencies
87
+
88
+ ```bash
89
+ npm install uuid async-retry commander ts-node web-streams-polyfill
90
+ npm install -D @types/uuid @types/async-retry
91
+ ```
92
+
93
+ ## Quick Start
94
+
95
+ ### Define Your Domain
96
+
97
+ ```typescript
98
+ import {
99
+ type Event,
100
+ type Command,
101
+ type Decider,
102
+ } from '@event-driven-io/emmett';
103
+
104
+ // Define your events
105
+ type ProductItemAdded = Event<
106
+ 'ProductItemAdded',
107
+ { productId: string; quantity: number; price: number }
108
+ >;
109
+ type ShoppingCartConfirmed = Event<
110
+ 'ShoppingCartConfirmed',
111
+ { confirmedAt: Date }
112
+ >;
113
+
114
+ type ShoppingCartEvent = ProductItemAdded | ShoppingCartConfirmed;
115
+
116
+ // Define your commands
117
+ type AddProductItem = Command<
118
+ 'AddProductItem',
119
+ { productId: string; quantity: number; price: number }
120
+ >;
121
+ type ConfirmShoppingCart = Command<'ConfirmShoppingCart', { now: Date }>;
122
+
123
+ type ShoppingCartCommand = AddProductItem | ConfirmShoppingCart;
124
+
125
+ // Define your state
126
+ type ShoppingCart = {
127
+ status: 'opened' | 'confirmed';
128
+ productItems: Array<{ productId: string; quantity: number; price: number }>;
129
+ };
130
+
131
+ // Create your decider
132
+ const shoppingCartDecider: Decider<
133
+ ShoppingCart,
134
+ ShoppingCartCommand,
135
+ ShoppingCartEvent
136
+ > = {
137
+ decide: (command, state): ShoppingCartEvent | ShoppingCartEvent[] => {
138
+ switch (command.type) {
139
+ case 'AddProductItem':
140
+ return {
141
+ type: 'ProductItemAdded',
142
+ data: command.data,
143
+ };
144
+ case 'ConfirmShoppingCart':
145
+ return {
146
+ type: 'ShoppingCartConfirmed',
147
+ data: { confirmedAt: command.data.now },
148
+ };
149
+ }
150
+ },
151
+ evolve: (state, event): ShoppingCart => {
152
+ switch (event.type) {
153
+ case 'ProductItemAdded':
154
+ return {
155
+ ...state,
156
+ productItems: [...state.productItems, event.data],
157
+ };
158
+ case 'ShoppingCartConfirmed':
159
+ return { ...state, status: 'confirmed' };
160
+ }
161
+ },
162
+ initialState: () => ({ status: 'opened', productItems: [] }),
163
+ };
164
+ ```
165
+
166
+ ### Use the In-Memory Event Store
167
+
168
+ ```typescript
169
+ import {
170
+ getInMemoryEventStore,
171
+ DeciderCommandHandler,
172
+ } from '@event-driven-io/emmett';
173
+
174
+ // Create an event store
175
+ const eventStore = getInMemoryEventStore();
176
+
177
+ // Create a command handler from your decider
178
+ const handle = DeciderCommandHandler({
179
+ ...shoppingCartDecider,
180
+ mapToStreamId: (id) => `shopping_cart-${id}`,
181
+ });
182
+
183
+ // Handle commands
184
+ const cartId = 'cart-123';
185
+
186
+ await handle(eventStore, cartId, {
187
+ type: 'AddProductItem',
188
+ data: { productId: 'shoes-1', quantity: 2, price: 99.99 },
189
+ });
190
+
191
+ await handle(eventStore, cartId, {
192
+ type: 'ConfirmShoppingCart',
193
+ data: { now: new Date() },
194
+ });
195
+
196
+ // Read the stream
197
+ const { events } = await eventStore.readStream(`shopping_cart-${cartId}`);
198
+ console.log(events); // All events for this cart
199
+ ```
200
+
201
+ ## How-to Guides
202
+
203
+ ### Testing with DeciderSpecification
204
+
205
+ Use the BDD-style specification for testing your deciders:
206
+
207
+ ```typescript
208
+ import { DeciderSpecification } from '@event-driven-io/emmett';
209
+
210
+ const spec = DeciderSpecification.for(shoppingCartDecider);
211
+
212
+ // Test: given events, when command, then expected events
213
+ spec([
214
+ {
215
+ type: 'ProductItemAdded',
216
+ data: { productId: 'p1', quantity: 1, price: 10 },
217
+ },
218
+ ])
219
+ .when({ type: 'ConfirmShoppingCart', data: { now: new Date() } })
220
+ .then([
221
+ { type: 'ShoppingCartConfirmed', data: { confirmedAt: expect.any(Date) } },
222
+ ]);
223
+
224
+ // Test that nothing happens
225
+ spec([]).when(someCommand).thenNothingHappened();
226
+
227
+ // Test that an error is thrown
228
+ spec([]).when(invalidCommand).thenThrows(IllegalStateError);
229
+ ```
230
+
231
+ ### Creating Projections
232
+
233
+ Build read models from your events:
234
+
235
+ ```typescript
236
+ import { projection, type ProjectionDefinition } from '@event-driven-io/emmett';
237
+
238
+ type ShoppingCartSummary = {
239
+ id: string;
240
+ itemCount: number;
241
+ totalAmount: number;
242
+ };
243
+
244
+ const shoppingCartSummaryProjection: ProjectionDefinition<
245
+ ShoppingCartEvent,
246
+ any,
247
+ { database: InMemoryDatabase }
248
+ > = projection({
249
+ name: 'shopping-cart-summary',
250
+ canHandle: ['ProductItemAdded', 'ShoppingCartConfirmed'],
251
+ handle: async (events, { database }) => {
252
+ for (const event of events) {
253
+ const cartId = event.metadata.streamName.split('-')[1];
254
+ const collection =
255
+ database.collection<ShoppingCartSummary>('cart-summaries');
256
+
257
+ if (event.type === 'ProductItemAdded') {
258
+ const existing = await collection.findOne({ id: cartId });
259
+ await collection.updateOne(
260
+ { id: cartId },
261
+ {
262
+ id: cartId,
263
+ itemCount: (existing?.itemCount ?? 0) + event.data.quantity,
264
+ totalAmount:
265
+ (existing?.totalAmount ?? 0) +
266
+ event.data.price * event.data.quantity,
267
+ },
268
+ );
269
+ }
270
+ }
271
+ },
272
+ });
273
+ ```
274
+
275
+ ### Using the In-Memory Database
276
+
277
+ For projections and testing:
278
+
279
+ ```typescript
280
+ import { getInMemoryDatabase } from '@event-driven-io/emmett';
281
+
282
+ const database = getInMemoryDatabase();
283
+ const users = database.collection<{ id: string; name: string }>('users');
284
+
285
+ // Insert
286
+ await users.insertOne({ id: '1', name: 'Alice' });
287
+
288
+ // Find
289
+ const user = await users.findOne({ id: '1' });
290
+
291
+ // Update with versioning
292
+ await users.updateOne(
293
+ { id: '1' },
294
+ { id: '1', name: 'Alice Smith' },
295
+ { expectedVersion: 1n },
296
+ );
297
+ ```
298
+
299
+ ### Working with Message Bus
300
+
301
+ Publish and subscribe to events/commands:
302
+
303
+ ```typescript
304
+ import { getInMemoryMessageBus } from '@event-driven-io/emmett';
305
+
306
+ const messageBus = getInMemoryMessageBus();
307
+
308
+ // Subscribe to events
309
+ messageBus.subscribe(
310
+ async (event) => {
311
+ console.log('Received:', event);
312
+ },
313
+ 'ProductItemAdded',
314
+ 'ShoppingCartConfirmed'
315
+ );
316
+
317
+ // Handle commands (only one handler per command type)
318
+ messageBus.handle(
319
+ async (command) => {
320
+ // Process command
321
+ },
322
+ 'AddProductItem'
323
+ );
324
+
325
+ // Publish events
326
+ await messageBus.publish({ type: 'ProductItemAdded', data: { ... } });
327
+
328
+ // Send commands
329
+ await messageBus.send({ type: 'AddProductItem', data: { ... } });
330
+ ```
331
+
332
+ ### Implementing Workflows (Sagas)
333
+
334
+ Coordinate operations across multiple aggregates:
335
+
336
+ ```typescript
337
+ import { Workflow } from '@event-driven-io/emmett';
338
+
339
+ type OrderSagaInput = OrderPlaced | PaymentReceived | ShipmentCreated;
340
+ type OrderSagaOutput = RequestPayment | CreateShipment | CompleteOrder;
341
+
342
+ type OrderSagaState = {
343
+ orderId: string | null;
344
+ paymentReceived: boolean;
345
+ shipped: boolean;
346
+ };
347
+
348
+ const orderWorkflow = Workflow<OrderSagaInput, OrderSagaState, OrderSagaOutput>(
349
+ {
350
+ name: 'order-fulfillment',
351
+ initialState: () => ({
352
+ orderId: null,
353
+ paymentReceived: false,
354
+ shipped: false,
355
+ }),
356
+ decide: (event, state) => {
357
+ switch (event.type) {
358
+ case 'OrderPlaced':
359
+ return {
360
+ type: 'RequestPayment',
361
+ data: { orderId: event.data.orderId },
362
+ };
363
+ case 'PaymentReceived':
364
+ return { type: 'CreateShipment', data: { orderId: state.orderId } };
365
+ case 'ShipmentCreated':
366
+ return { type: 'CompleteOrder', data: { orderId: state.orderId } };
367
+ }
368
+ },
369
+ evolve: (state, event) => {
370
+ switch (event.type) {
371
+ case 'OrderPlaced':
372
+ return { ...state, orderId: event.data.orderId };
373
+ case 'PaymentReceived':
374
+ return { ...state, paymentReceived: true };
375
+ case 'ShipmentCreated':
376
+ return { ...state, shipped: true };
377
+ default:
378
+ return state;
379
+ }
380
+ },
381
+ },
382
+ );
383
+ ```
384
+
385
+ ## API Reference
386
+
387
+ ### Core Exports
388
+
389
+ ```typescript
390
+ // Event Store
391
+ export { EventStore, getInMemoryEventStore, InMemoryEventStore };
392
+ export { ReadStreamOptions, ReadStreamResult };
393
+ export { AggregateStreamOptions, AggregateStreamResult };
394
+ export { AppendToStreamOptions, AppendToStreamResult };
395
+ export { EventStoreSession, EventStoreSessionFactory };
396
+
397
+ // Expected Version
398
+ export {
399
+ ExpectedStreamVersion,
400
+ STREAM_EXISTS,
401
+ STREAM_DOES_NOT_EXIST,
402
+ NO_CONCURRENCY_CHECK,
403
+ ExpectedVersionConflictError,
404
+ };
405
+
406
+ // Command Handling
407
+ export { CommandHandler, DeciderCommandHandler };
408
+ export { CommandHandlerOptions, HandleOptions };
409
+
410
+ // Types
411
+ export { Event, Command, Decider };
412
+ export { ReadEvent, ReadEventMetadata };
413
+ export { Message, RecordedMessage };
414
+ export { BigIntStreamPosition, BigIntGlobalPosition };
415
+
416
+ // Projections
417
+ export { ProjectionDefinition, projection };
418
+ export { inlineProjections, asyncProjections };
419
+
420
+ // Message Bus
421
+ export { MessageBus, CommandBus, EventBus };
422
+ export { getInMemoryMessageBus };
423
+
424
+ // Processors
425
+ export { MessageProcessor, reactor, projector };
426
+ export { Checkpointer, ReactorOptions, ProjectorOptions };
427
+
428
+ // Workflows
429
+ export { Workflow, WorkflowEvent, WorkflowCommand };
430
+
431
+ // Database
432
+ export { getInMemoryDatabase, InMemoryDatabase };
433
+ export { Document, WithId, WithVersion };
434
+
435
+ // Testing
436
+ export { DeciderSpecification, AsyncDeciderSpecification };
437
+ export { WrapEventStore, EventStoreWrapper };
438
+ export { assertTrue, assertEqual, assertDeepEqual, assertThrows };
439
+
440
+ // Utilities
441
+ export { asyncRetry, NoRetries };
442
+ export { JSONParser };
443
+ export { ValidationErrors };
444
+
445
+ // Errors
446
+ export { EmmettError, ConcurrencyError, ValidationError };
447
+ export { IllegalStateError, NotFoundError };
448
+ ```
449
+
450
+ ### EventStore Interface
451
+
452
+ ```typescript
453
+ interface EventStore<ReadEventMetadataType> {
454
+ // Aggregate events into state
455
+ aggregateStream<State, EventType extends Event>(
456
+ streamName: string,
457
+ options: AggregateStreamOptions<State, EventType, ReadEventMetadataType>,
458
+ ): Promise<AggregateStreamResult<State>>;
459
+
460
+ // Read raw events from stream
461
+ readStream<EventType extends Event>(
462
+ streamName: string,
463
+ options?: ReadStreamOptions,
464
+ ): Promise<ReadStreamResult<EventType, ReadEventMetadataType>>;
465
+
466
+ // Append events to stream
467
+ appendToStream<EventType extends Event>(
468
+ streamName: string,
469
+ events: EventType[],
470
+ options?: AppendToStreamOptions,
471
+ ): Promise<AppendToStreamResult>;
472
+ }
473
+ ```
474
+
475
+ ### Decider Type
476
+
477
+ ```typescript
478
+ type Decider<State, CommandType extends Command, StreamEvent extends Event> = {
479
+ decide: (command: CommandType, state: State) => StreamEvent | StreamEvent[];
480
+ evolve: (currentState: State, event: StreamEvent) => State;
481
+ initialState: () => State;
482
+ };
483
+ ```
484
+
485
+ ### Projection Definition
486
+
487
+ ```typescript
488
+ interface ProjectionDefinition<
489
+ EventType,
490
+ EventMetaDataType,
491
+ ProjectionHandlerContext,
492
+ > {
493
+ name?: string;
494
+ canHandle: string[]; // Event types this projection handles
495
+ handle: (
496
+ events: ReadEvent<EventType, EventMetaDataType>[],
497
+ context: ProjectionHandlerContext,
498
+ ) => Promise<void>;
499
+ truncate?: (context: ProjectionHandlerContext) => Promise<void>;
500
+ }
501
+ ```
502
+
503
+ ## Architecture
504
+
505
+ ```
506
+ src/
507
+ ├── index.ts # Main entry point, re-exports all modules
508
+ ├── cli.ts # CLI entry point for emmett command
509
+
510
+ ├── commandHandling/ # Command handler implementations
511
+ │ ├── handleCommand.ts # Generic command handler with retry
512
+ │ └── handleCommandWithDecider.ts # Decider-based command handler
513
+
514
+ ├── eventStore/ # Event store abstractions
515
+ │ ├── eventStore.ts # Core EventStore interface
516
+ │ ├── inMemoryEventStore.ts # In-memory implementation
517
+ │ ├── expectedVersion.ts # Concurrency control
518
+ │ ├── afterCommit/ # Post-commit hooks
519
+ │ ├── projections/ # Inline projection handling
520
+ │ └── subscriptions/ # Streaming subscriptions
521
+
522
+ ├── typing/ # Core type definitions
523
+ │ ├── event.ts # Event types
524
+ │ ├── command.ts # Command types
525
+ │ ├── decider.ts # Decider pattern type
526
+ │ └── message.ts # Message abstractions
527
+
528
+ ├── projections/ # Projection definitions
529
+ │ └── index.ts # ProjectionDefinition, inline/async helpers
530
+
531
+ ├── workflows/ # Saga/workflow pattern
532
+ │ ├── workflow.ts # Workflow type definition
533
+ │ └── workflowProcessor.ts # Workflow processing
534
+
535
+ ├── processors/ # Message processors
536
+ │ ├── processors.ts # MessageProcessor, reactor, projector
537
+ │ └── inMemoryProcessors.ts # In-memory implementations
538
+
539
+ ├── messageBus/ # Message bus abstractions
540
+ │ └── index.ts # MessageBus, CommandBus, EventBus
541
+
542
+ ├── database/ # Document database abstractions
543
+ │ ├── inMemoryDatabase.ts # In-memory document store
544
+ │ └── types.ts # Database types
545
+
546
+ ├── testing/ # Testing utilities
547
+ │ ├── deciderSpecification.ts # BDD-style decider testing
548
+ │ ├── assertions.ts # Test assertions
549
+ │ └── wrapEventStore.ts # Event store test wrapper
550
+
551
+ ├── streaming/ # Stream utilities
552
+ │ ├── collectors/ # Stream collectors (first, last, collect)
553
+ │ ├── decoders/ # Stream decoders
554
+ │ └── generators/ # Stream generators
555
+
556
+ ├── serialization/ # Serialization utilities
557
+ │ └── json/ # JSON parser with BigInt support
558
+
559
+ ├── validation/ # Validation utilities
560
+ │ └── index.ts # Type validators, assertions
561
+
562
+ ├── errors/ # Error types
563
+ │ └── index.ts # EmmettError, ConcurrencyError, etc.
564
+
565
+ ├── utils/ # General utilities
566
+ │ ├── retry.ts # Async retry logic
567
+ │ ├── locking/ # Locking mechanisms
568
+ │ └── collections/ # Collection utilities
569
+
570
+ └── config/ # Configuration
571
+ └── plugins/ # CLI plugin system
572
+ ```
573
+
574
+ ## Dependencies
575
+
576
+ | Package | Version | Purpose |
577
+ | ---------------------- | ------- | ------------------------------------- |
578
+ | `uuid` | ^10.0.0 | UUID generation for event/message IDs |
579
+ | `async-retry` | ^1.3.3 | Retry logic for command handlers |
580
+ | `commander` | ^12.1.0 | CLI framework |
581
+ | `ts-node` | ^10.9.2 | TypeScript execution for CLI |
582
+ | `web-streams-polyfill` | ^4.0.0 | Web Streams API polyfill |
583
+
584
+ ## Related Packages
585
+
586
+ - **[@event-driven-io/emmett-postgresql](../emmett-postgresql)** - PostgreSQL event store adapter
587
+ - **[@event-driven-io/emmett-mongodb](../emmett-mongodb)** - MongoDB event store adapter
588
+ - **[@event-driven-io/emmett-esdb](../emmett-esdb)** - EventStoreDB adapter
589
+ - **[@event-driven-io/emmett-sqlite](../emmett-sqlite)** - SQLite event store adapter
590
+ - **[@event-driven-io/emmett-expressjs](../emmett-expressjs)** - Express.js integration
591
+ - **[@event-driven-io/emmett-fastify](../emmett-fastify)** - Fastify integration