@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 +591 -0
- package/dist/index.cjs +145 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -14
- package/dist/index.d.ts +30 -14
- package/dist/index.js +83 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|