@event-driven-io/emmett-sqlite 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,493 @@
1
+ # @event-driven-io/emmett-sqlite
2
+
3
+ SQLite event store adapter for the Emmett event sourcing library, providing persistent event storage with stream management, projections, and subscription-based event processing.
4
+
5
+ ## Purpose
6
+
7
+ This package provides a SQLite-based implementation of the Emmett event store interface. It enables event sourcing applications to persist events to a SQLite database with support for:
8
+
9
+ - File-based and in-memory database storage
10
+ - WAL (Write-Ahead Logging) mode for improved concurrency
11
+ - Automatic schema management
12
+ - Inline and async projections for read models
13
+ - Polling-based consumers for background event processing
14
+ - Optimistic concurrency control
15
+
16
+ SQLite is an excellent choice for development, testing, embedded applications, and scenarios where a lightweight, serverless database is preferred.
17
+
18
+ ## Key Concepts
19
+
20
+ ### Event Store
21
+
22
+ The `SQLiteEventStore` interface extends Emmett's base `EventStore` with SQLite-specific functionality:
23
+
24
+ - **Append events** to streams with optimistic concurrency control
25
+ - **Read streams** to retrieve events for a specific aggregate
26
+ - **Aggregate streams** to rebuild state using an evolve function
27
+ - **Consumer** for background event processing
28
+
29
+ ### Database Schema
30
+
31
+ The event store uses three tables (prefixed with `emt_`):
32
+
33
+ | Table | Purpose |
34
+ | ------------------- | ------------------------------------------- |
35
+ | `emt_streams` | Stream metadata (stream ID, position, type) |
36
+ | `emt_messages` | Events with global position ordering |
37
+ | `emt_subscriptions` | Processor checkpoint positions |
38
+
39
+ ### Consumers and Processors
40
+
41
+ - **Consumer**: Coordinates multiple processors, manages polling lifecycle
42
+ - **Processor**: Handles event batches, maintains checkpoint position
43
+ - **Projection Processor**: Specialized processor for updating read models
44
+
45
+ ### Projections
46
+
47
+ Two projection types are supported:
48
+
49
+ - **Inline projections**: Execute within the append transaction for immediate consistency
50
+ - **Async projections**: Run via consumers for eventual consistency
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ npm install @event-driven-io/emmett-sqlite
56
+ # or
57
+ pnpm add @event-driven-io/emmett-sqlite
58
+ # or
59
+ yarn add @event-driven-io/emmett-sqlite
60
+ ```
61
+
62
+ ### Peer Dependencies
63
+
64
+ This package requires the following peer dependencies:
65
+
66
+ ```bash
67
+ npm install @event-driven-io/emmett sqlite3
68
+ ```
69
+
70
+ ## Quick Start
71
+
72
+ ### Basic Event Store Setup
73
+
74
+ ```typescript
75
+ import { getSQLiteEventStore } from '@event-driven-io/emmett-sqlite';
76
+
77
+ // File-based database
78
+ const eventStore = getSQLiteEventStore({
79
+ fileName: './events.db',
80
+ schema: {
81
+ autoMigration: 'CreateOrUpdate', // Automatically create tables
82
+ },
83
+ });
84
+
85
+ // In-memory database (useful for testing)
86
+ import { InMemorySQLiteDatabase } from '@event-driven-io/emmett-sqlite';
87
+
88
+ const inMemoryEventStore = getSQLiteEventStore({
89
+ fileName: InMemorySQLiteDatabase, // ':memory:'
90
+ schema: {
91
+ autoMigration: 'CreateOrUpdate',
92
+ },
93
+ });
94
+ ```
95
+
96
+ ### Appending Events
97
+
98
+ ```typescript
99
+ import type { Event } from '@event-driven-io/emmett';
100
+
101
+ // Define your event types
102
+ type ProductItemAdded = Event<
103
+ 'ProductItemAdded',
104
+ { productItem: { productId: string; quantity: number; price: number } }
105
+ >;
106
+
107
+ type DiscountApplied = Event<
108
+ 'DiscountApplied',
109
+ { percent: number; couponId: string }
110
+ >;
111
+
112
+ type ShoppingCartEvent = ProductItemAdded | DiscountApplied;
113
+
114
+ // Append events to a stream
115
+ const streamId = 'shopping_cart-123';
116
+
117
+ const result = await eventStore.appendToStream<ShoppingCartEvent>(streamId, [
118
+ {
119
+ type: 'ProductItemAdded',
120
+ data: {
121
+ productItem: { productId: 'shoes', quantity: 2, price: 100 },
122
+ },
123
+ },
124
+ ]);
125
+
126
+ // Append with optimistic concurrency
127
+ await eventStore.appendToStream<ShoppingCartEvent>(
128
+ streamId,
129
+ [
130
+ {
131
+ type: 'DiscountApplied',
132
+ data: { percent: 10, couponId: 'SAVE10' },
133
+ },
134
+ ],
135
+ { expectedStreamVersion: result.nextExpectedStreamVersion },
136
+ );
137
+ ```
138
+
139
+ ### Reading Events
140
+
141
+ ```typescript
142
+ // Read all events from a stream
143
+ const { events, currentStreamVersion } = await eventStore.readStream(streamId);
144
+
145
+ for (const event of events) {
146
+ console.log(`Event: ${event.type}`, event.data);
147
+ }
148
+ ```
149
+
150
+ ### Aggregating State
151
+
152
+ ```typescript
153
+ type ShoppingCart = {
154
+ productItems: Array<{ productId: string; quantity: number; price: number }>;
155
+ totalAmount: number;
156
+ };
157
+
158
+ const evolve = (
159
+ state: ShoppingCart,
160
+ event: ShoppingCartEvent,
161
+ ): ShoppingCart => {
162
+ switch (event.type) {
163
+ case 'ProductItemAdded': {
164
+ const item = event.data.productItem;
165
+ return {
166
+ productItems: [...state.productItems, item],
167
+ totalAmount: state.totalAmount + item.price * item.quantity,
168
+ };
169
+ }
170
+ case 'DiscountApplied':
171
+ return {
172
+ ...state,
173
+ totalAmount: state.totalAmount * (1 - event.data.percent / 100),
174
+ };
175
+ }
176
+ };
177
+
178
+ const initialState = (): ShoppingCart => ({
179
+ productItems: [],
180
+ totalAmount: 0,
181
+ });
182
+
183
+ const { state, currentStreamVersion } = await eventStore.aggregateStream(
184
+ streamId,
185
+ { evolve, initialState },
186
+ );
187
+
188
+ console.log('Cart total:', state.totalAmount);
189
+ ```
190
+
191
+ ## How-to Guides
192
+
193
+ ### Using Inline Projections
194
+
195
+ Inline projections execute within the same transaction as the event append, ensuring immediate consistency.
196
+
197
+ ```typescript
198
+ import {
199
+ getSQLiteEventStore,
200
+ sqliteRawSQLProjection,
201
+ } from '@event-driven-io/emmett-sqlite';
202
+
203
+ const shoppingCartSummaryProjection = sqliteRawSQLProjection<ShoppingCartEvent>(
204
+ (event, context) => {
205
+ switch (event.type) {
206
+ case 'ProductItemAdded': {
207
+ const { quantity, price } = event.data.productItem;
208
+ return `
209
+ INSERT INTO shopping_cart_summary (id, item_count, total)
210
+ VALUES ('${event.metadata.streamName}', ${quantity}, ${quantity * price})
211
+ ON CONFLICT (id) DO UPDATE SET
212
+ item_count = item_count + ${quantity},
213
+ total = total + ${quantity * price};
214
+ `;
215
+ }
216
+ case 'DiscountApplied':
217
+ return `
218
+ UPDATE shopping_cart_summary
219
+ SET total = total * (100 - ${event.data.percent}) / 100
220
+ WHERE id = '${event.metadata.streamName}';
221
+ `;
222
+ }
223
+ },
224
+ 'ProductItemAdded',
225
+ 'DiscountApplied',
226
+ );
227
+
228
+ const eventStore = getSQLiteEventStore({
229
+ fileName: './events.db',
230
+ projections: [{ type: 'inline', projection: shoppingCartSummaryProjection }],
231
+ });
232
+ ```
233
+
234
+ ### Using Background Consumers
235
+
236
+ Consumers poll for new events and process them with registered processors.
237
+
238
+ ```typescript
239
+ import {
240
+ getSQLiteEventStore,
241
+ sqliteProcessor,
242
+ } from '@event-driven-io/emmett-sqlite';
243
+
244
+ const eventStore = getSQLiteEventStore({
245
+ fileName: './events.db',
246
+ });
247
+
248
+ // Create a consumer
249
+ const consumer = eventStore.consumer<ShoppingCartEvent>();
250
+
251
+ // Register a processor
252
+ consumer.processor({
253
+ processorId: 'notification-sender',
254
+ eachMessage: async (event, context) => {
255
+ if (event.type === 'ProductItemAdded') {
256
+ console.log('New item added:', event.data.productItem);
257
+ // Send notification, update cache, etc.
258
+ }
259
+ },
260
+ });
261
+
262
+ // Start consuming
263
+ await consumer.start();
264
+
265
+ // Stop when done
266
+ await consumer.stop();
267
+ await consumer.close();
268
+ ```
269
+
270
+ ### Using Projection Processors
271
+
272
+ Projection processors provide a convenient way to run projections asynchronously.
273
+
274
+ ```typescript
275
+ import { sqliteProjectionProcessor } from '@event-driven-io/emmett-sqlite';
276
+
277
+ const consumer = eventStore.consumer<ShoppingCartEvent>();
278
+
279
+ consumer.processor({
280
+ projection: shoppingCartSummaryProjection,
281
+ processorId: 'cart-summary-projection',
282
+ startFrom: 'CURRENT', // Resume from last checkpoint
283
+ });
284
+
285
+ await consumer.start();
286
+ ```
287
+
288
+ ### Configuring Polling Behavior
289
+
290
+ ```typescript
291
+ const consumer = eventStore.consumer<ShoppingCartEvent>({
292
+ pulling: {
293
+ batchSize: 100, // Events per batch (default: 100)
294
+ pullingFrequencyInMs: 50, // Polling interval (default: 50ms)
295
+ },
296
+ });
297
+ ```
298
+
299
+ ### Manual Schema Management
300
+
301
+ ```typescript
302
+ import {
303
+ getSQLiteEventStore,
304
+ sqliteConnection,
305
+ createEventStoreSchema,
306
+ } from '@event-driven-io/emmett-sqlite';
307
+
308
+ // Create connection
309
+ const connection = sqliteConnection({ fileName: './events.db' });
310
+
311
+ // Manually create schema
312
+ await createEventStoreSchema(connection);
313
+
314
+ // Create event store without auto-migration
315
+ const eventStore = getSQLiteEventStore({
316
+ fileName: './events.db',
317
+ schema: {
318
+ autoMigration: 'None',
319
+ },
320
+ });
321
+ ```
322
+
323
+ ### Using Shared In-Memory Database
324
+
325
+ For testing scenarios where multiple connections need to share the same in-memory database:
326
+
327
+ ```typescript
328
+ import { InMemorySharedCacheSQLiteDatabase } from '@event-driven-io/emmett-sqlite';
329
+
330
+ const eventStore = getSQLiteEventStore({
331
+ fileName: InMemorySharedCacheSQLiteDatabase, // 'file::memory:?cache=shared'
332
+ });
333
+ ```
334
+
335
+ ### Using Before-Commit Hooks
336
+
337
+ Execute custom logic before events are committed:
338
+
339
+ ```typescript
340
+ const eventStore = getSQLiteEventStore({
341
+ fileName: './events.db',
342
+ hooks: {
343
+ onBeforeCommit: async (events, context) => {
344
+ // Custom validation, logging, or side effects
345
+ for (const event of events) {
346
+ console.log('About to commit:', event.type);
347
+ }
348
+ },
349
+ },
350
+ });
351
+ ```
352
+
353
+ ## API Reference
354
+
355
+ ### getSQLiteEventStore
356
+
357
+ Creates a new SQLite event store instance.
358
+
359
+ ```typescript
360
+ function getSQLiteEventStore(
361
+ options: SQLiteEventStoreOptions,
362
+ ): SQLiteEventStore;
363
+ ```
364
+
365
+ **Options:**
366
+
367
+ | Property | Type | Description |
368
+ | ---------------------- | ------------------------------------------------------ | -------------------------------------------------- |
369
+ | `fileName` | `string \| ':memory:' \| 'file::memory:?cache=shared'` | Database file path or in-memory identifier |
370
+ | `schema.autoMigration` | `'None' \| 'CreateOrUpdate'` | Schema creation mode (default: `'CreateOrUpdate'`) |
371
+ | `projections` | `ProjectionRegistration[]` | Inline projections to register |
372
+ | `hooks.onBeforeCommit` | `BeforeEventStoreCommitHandler` | Hook called before event commit |
373
+
374
+ ### SQLiteEventStore
375
+
376
+ | Method | Description |
377
+ | ---------------------------------------------- | ------------------------- |
378
+ | `appendToStream(streamName, events, options?)` | Append events to a stream |
379
+ | `readStream(streamName, options?)` | Read events from a stream |
380
+ | `aggregateStream(streamName, options)` | Rebuild state from events |
381
+ | `consumer(options?)` | Create an event consumer |
382
+
383
+ ### sqliteConnection
384
+
385
+ Creates a SQLite database connection with transaction support.
386
+
387
+ ```typescript
388
+ function sqliteConnection(options: { fileName: string }): SQLiteConnection;
389
+ ```
390
+
391
+ **SQLiteConnection interface:**
392
+
393
+ | Method | Description |
394
+ | ------------------------------ | --------------------------------------- |
395
+ | `command(sql, params?)` | Execute a write command |
396
+ | `query<T>(sql, params?)` | Execute a query returning multiple rows |
397
+ | `querySingle<T>(sql, params?)` | Execute a query returning a single row |
398
+ | `withTransaction<T>(fn)` | Execute function within a transaction |
399
+ | `close()` | Close the connection |
400
+
401
+ ### Projection Helpers
402
+
403
+ | Function | Description |
404
+ | ----------------------------------------------------- | --------------------------------------------------- |
405
+ | `sqliteProjection(definition)` | Create a projection definition |
406
+ | `sqliteRawSQLProjection(handler, ...eventTypes)` | Create projection returning raw SQL per event |
407
+ | `sqliteRawBatchSQLProjection(handler, ...eventTypes)` | Create projection returning raw SQL array for batch |
408
+
409
+ ### Consumer Types
410
+
411
+ | Type | Description |
412
+ | ---------------------------- | ---------------------------------------------- |
413
+ | `SQLiteEventStoreConsumer` | Consumer interface with start/stop lifecycle |
414
+ | `SQLiteProcessor` | Processor interface for handling event batches |
415
+ | `SQLiteProjectionDefinition` | Projection definition type |
416
+
417
+ ## Architecture
418
+
419
+ ```
420
+ +------------------+
421
+ | Application |
422
+ +--------+---------+
423
+ |
424
+ +--------+--------+--------+
425
+ | |
426
+ Commands/Queries Consumers
427
+ | |
428
+ +------------v------------+ +--------v--------+
429
+ | SQLiteEventStore | | SQLiteConsumer |
430
+ | - appendToStream() | | - processor() |
431
+ | - readStream() | | - start() |
432
+ | - aggregateStream() | | - stop() |
433
+ +------------+------------+ +--------+--------+
434
+ | |
435
+ +------------+-------------+
436
+ |
437
+ +---------v---------+
438
+ | SQLiteConnection |
439
+ | - command() |
440
+ | - query() |
441
+ | - withTransaction|
442
+ +---------+---------+
443
+ |
444
+ +---------v---------+
445
+ | SQLite |
446
+ | (WAL mode) |
447
+ | |
448
+ | +---------------+ |
449
+ | | emt_streams | |
450
+ | +---------------+ |
451
+ | | emt_messages | |
452
+ | +---------------+ |
453
+ | | emt_subscript | |
454
+ | +---------------+ |
455
+ +-------------------+
456
+ ```
457
+
458
+ ### Event Flow
459
+
460
+ 1. **Write Path**: Events are appended via `appendToStream()`, stored in `emt_messages` with auto-incremented global position
461
+ 2. **Read Path**: Events are read via `readStream()` or `aggregateStream()` for state reconstruction
462
+ 3. **Projection Path (Inline)**: Projections execute within the append transaction
463
+ 4. **Projection Path (Async)**: Consumers poll `emt_messages`, processors handle batches, checkpoints stored in `emt_subscriptions`
464
+
465
+ ### Concurrency Model
466
+
467
+ - SQLite WAL mode enables concurrent reads during writes
468
+ - Optimistic concurrency via expected stream version checks
469
+ - Consumers use polling (not push) for event delivery
470
+ - Processor checkpoints enable resume-from-position
471
+
472
+ ## Dependencies
473
+
474
+ ### Peer Dependencies
475
+
476
+ | Package | Version | Purpose |
477
+ | ------------------------- | -------- | --------------------------------------- |
478
+ | `@event-driven-io/emmett` | `0.38.3` | Core event sourcing types and utilities |
479
+ | `sqlite3` | `^5.1.7` | SQLite database driver |
480
+
481
+ ### Internal Dependencies
482
+
483
+ | Package | Purpose |
484
+ | ------------------------ | --------------------- |
485
+ | `@event-driven-io/dumbo` | SQL utilities |
486
+ | `uuid` | Message ID generation |
487
+
488
+ ## Related Packages
489
+
490
+ - [@event-driven-io/emmett](https://www.npmjs.com/package/@event-driven-io/emmett) - Core library
491
+ - [@event-driven-io/emmett-postgresql](https://www.npmjs.com/package/@event-driven-io/emmett-postgresql) - PostgreSQL adapter
492
+ - [@event-driven-io/emmett-mongodb](https://www.npmjs.com/package/@event-driven-io/emmett-mongodb) - MongoDB adapter
493
+ - [@event-driven-io/emmett-esdb](https://www.npmjs.com/package/@event-driven-io/emmett-esdb) - EventStoreDB adapter