@semiont/event-sourcing 0.2.28-build.40
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 +407 -0
- package/dist/index.d.ts +606 -0
- package/dist/index.js +1170 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# @semiont/event-sourcing
|
|
2
|
+
|
|
3
|
+
[](https://github.com/The-AI-Alliance/semiont/actions/workflows/package-tests.yml?query=branch%3Amain+is%3Asuccess+job%3A%22Test+event-sourcing%22)
|
|
4
|
+
[](https://www.npmjs.com/package/@semiont/event-sourcing)
|
|
5
|
+
[](https://github.com/The-AI-Alliance/semiont/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
Event sourcing infrastructure for [Semiont](https://github.com/The-AI-Alliance/semiont) - provides event persistence, pub/sub, and materialized views for building event-driven applications.
|
|
8
|
+
|
|
9
|
+
## What is Event Sourcing?
|
|
10
|
+
|
|
11
|
+
Event sourcing is a pattern where state changes are stored as a sequence of immutable events. Instead of storing current state, you store the history of events that led to the current state.
|
|
12
|
+
|
|
13
|
+
**Benefits:**
|
|
14
|
+
- **Complete audit trail** - Every change is recorded with timestamp and user
|
|
15
|
+
- **Time travel** - Rebuild state at any point in history
|
|
16
|
+
- **Event replay** - Reprocess events to rebuild views or fix bugs
|
|
17
|
+
- **Microservices-ready** - Events enable distributed systems to stay in sync
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @semiont/event-sourcing
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Prerequisites:**
|
|
26
|
+
- Node.js >= 20.18.1
|
|
27
|
+
- `@semiont/core` and `@semiont/api-client` (peer dependencies)
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
EventStore,
|
|
34
|
+
FilesystemViewStorage,
|
|
35
|
+
type IdentifierConfig,
|
|
36
|
+
} from '@semiont/event-sourcing';
|
|
37
|
+
import { resourceId, userId } from '@semiont/core';
|
|
38
|
+
|
|
39
|
+
// 1. Create event store
|
|
40
|
+
const eventStore = new EventStore(
|
|
41
|
+
{
|
|
42
|
+
basePath: './data',
|
|
43
|
+
dataDir: './data/events',
|
|
44
|
+
enableSharding: true,
|
|
45
|
+
maxEventsPerFile: 10000,
|
|
46
|
+
},
|
|
47
|
+
new FilesystemViewStorage('./data'),
|
|
48
|
+
{ baseUrl: 'http://localhost:4000' }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// 2. Append events
|
|
52
|
+
const event = await eventStore.appendEvent({
|
|
53
|
+
type: 'resource.created',
|
|
54
|
+
resourceId: resourceId('doc-abc123'),
|
|
55
|
+
userId: userId('user@example.com'),
|
|
56
|
+
payload: {
|
|
57
|
+
name: 'My Document',
|
|
58
|
+
format: 'text/plain',
|
|
59
|
+
contentChecksum: 'sha256:...',
|
|
60
|
+
entityTypes: [],
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 3. Subscribe to events
|
|
65
|
+
eventStore.bus.subscribe(
|
|
66
|
+
resourceId('doc-abc123'),
|
|
67
|
+
async (storedEvent) => {
|
|
68
|
+
console.log('Event received:', storedEvent.event.type);
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// 4. Query events
|
|
73
|
+
const events = await eventStore.log.queryEvents(
|
|
74
|
+
resourceId('doc-abc123'),
|
|
75
|
+
{ eventTypes: ['resource.created', 'annotation.added'] }
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Architecture
|
|
80
|
+
|
|
81
|
+
The event-sourcing package follows a layered architecture with clear separation of concerns:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
┌─────────────────────────────────────────┐
|
|
85
|
+
│ EventStore │ ← Orchestration
|
|
86
|
+
│ (coordinates log, bus, views) │
|
|
87
|
+
└─────────────────────────────────────────┘
|
|
88
|
+
│ │ │
|
|
89
|
+
┌────┘ ┌────┘ └────┐
|
|
90
|
+
▼ ▼ ▼
|
|
91
|
+
┌────────┐ ┌──────────┐ ┌──────────────┐
|
|
92
|
+
│EventLog│ │ EventBus │ │ ViewManager │
|
|
93
|
+
│(persist) │ (pub/sub)│ │ (materialize)│
|
|
94
|
+
└────────┘ └──────────┘ └──────────────┘
|
|
95
|
+
│ │ │
|
|
96
|
+
▼ ▼ ▼
|
|
97
|
+
┌──────────┐ ┌──────────────┐ ┌─────────────┐
|
|
98
|
+
│EventStorage EventSubscriptions ViewStorage │
|
|
99
|
+
│(JSONL files) (in-memory) (JSON files) │
|
|
100
|
+
└──────────┘ └──────────────┘ └─────────────┘
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Key Components:**
|
|
104
|
+
|
|
105
|
+
- **EventStore** - Orchestration layer that coordinates event operations
|
|
106
|
+
- **EventLog** - Append-only event persistence with JSONL storage
|
|
107
|
+
- **EventBus** - Pub/sub notifications for real-time event processing
|
|
108
|
+
- **ViewManager** - Materialized view updates from event streams
|
|
109
|
+
- **EventStorage** - Filesystem storage with sharding for scalability
|
|
110
|
+
- **ViewStorage** - Materialized view persistence (current state)
|
|
111
|
+
|
|
112
|
+
## Core Concepts
|
|
113
|
+
|
|
114
|
+
### Events
|
|
115
|
+
|
|
116
|
+
Events are immutable records of state changes:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import type { ResourceEvent, StoredEvent } from '@semiont/core';
|
|
120
|
+
|
|
121
|
+
// Event to append (before storage)
|
|
122
|
+
const event: Omit<ResourceEvent, 'id' | 'timestamp'> = {
|
|
123
|
+
type: 'resource.created',
|
|
124
|
+
resourceId: resourceId('doc-123'),
|
|
125
|
+
userId: userId('user@example.com'),
|
|
126
|
+
payload: { /* event-specific data */ },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Stored event (after persistence)
|
|
130
|
+
const stored: StoredEvent = {
|
|
131
|
+
event: {
|
|
132
|
+
id: eventId('evt-456'),
|
|
133
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
134
|
+
...event,
|
|
135
|
+
},
|
|
136
|
+
metadata: {
|
|
137
|
+
sequenceNumber: 1,
|
|
138
|
+
checksum: 'sha256:...',
|
|
139
|
+
version: '1.0',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Event Types
|
|
145
|
+
|
|
146
|
+
Semiont uses a hierarchical event type system:
|
|
147
|
+
|
|
148
|
+
- `resource.created` - New resource created
|
|
149
|
+
- `resource.cloned` - Resource cloned from another
|
|
150
|
+
- `resource.archived` / `resource.unarchived` - Archive status changed
|
|
151
|
+
- `annotation.added` / `annotation.deleted` - Annotations modified
|
|
152
|
+
- `annotation.body.updated` - Annotation body changed
|
|
153
|
+
- `entitytag.added` / `entitytag.removed` - Entity type tags modified
|
|
154
|
+
- `entitytype.added` - New entity type registered (system-level)
|
|
155
|
+
|
|
156
|
+
### Materialized Views
|
|
157
|
+
|
|
158
|
+
Views are projections of event streams into queryable state:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import type { ResourceView } from '@semiont/event-sourcing';
|
|
162
|
+
|
|
163
|
+
// A view contains both metadata and annotations
|
|
164
|
+
const view: ResourceView = {
|
|
165
|
+
resource: {
|
|
166
|
+
'@id': 'http://localhost:4000/resources/doc-123',
|
|
167
|
+
name: 'My Document',
|
|
168
|
+
representations: [/* ... */],
|
|
169
|
+
entityTypes: ['Person', 'Organization'],
|
|
170
|
+
},
|
|
171
|
+
annotations: {
|
|
172
|
+
annotations: [/* ... */],
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Views are automatically updated when events are appended.
|
|
178
|
+
|
|
179
|
+
## Documentation
|
|
180
|
+
|
|
181
|
+
📚 **[Event Store Guide](./docs/EventStore.md)** - EventStore API and orchestration
|
|
182
|
+
|
|
183
|
+
📖 **[Event Log Guide](./docs/EventLog.md)** - Event persistence and storage
|
|
184
|
+
|
|
185
|
+
🔔 **[Event Bus Guide](./docs/EventBus.md)** - Pub/sub and subscriptions
|
|
186
|
+
|
|
187
|
+
🔍 **[Views Guide](./docs/Views.md)** - Materialized views and projections
|
|
188
|
+
|
|
189
|
+
⚙️ **[Configuration Guide](./docs/Configuration.md)** - Setup and options
|
|
190
|
+
|
|
191
|
+
## Key Features
|
|
192
|
+
|
|
193
|
+
- **Type-safe** - Full TypeScript support with branded types from `@semiont/core`
|
|
194
|
+
- **Filesystem-based** - No external database required (JSONL for events, JSON for views)
|
|
195
|
+
- **Sharded storage** - Automatic sharding for scalability (65,536 shards using Jump Consistent Hash)
|
|
196
|
+
- **Real-time** - Pub/sub subscriptions for live event processing
|
|
197
|
+
- **Event replay** - Rebuild views from event history at any time
|
|
198
|
+
- **Framework-agnostic** - Pure TypeScript, no web framework dependencies
|
|
199
|
+
|
|
200
|
+
## Use Cases
|
|
201
|
+
|
|
202
|
+
✅ **CLI tools** - Build offline tools that use event sourcing without the full backend
|
|
203
|
+
|
|
204
|
+
✅ **Worker processes** - Separate microservices that process events independently
|
|
205
|
+
|
|
206
|
+
✅ **Testing** - Isolated event stores for unit/integration tests
|
|
207
|
+
|
|
208
|
+
✅ **Analytics** - Process event streams for metrics and insights
|
|
209
|
+
|
|
210
|
+
✅ **Audit systems** - Complete history of all changes with provenance
|
|
211
|
+
|
|
212
|
+
❌ **Not for frontend** - Use `@semiont/react-ui` hooks for frontend applications
|
|
213
|
+
|
|
214
|
+
## API Overview
|
|
215
|
+
|
|
216
|
+
### EventStore
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const store = new EventStore(storageConfig, viewStorage, identifierConfig);
|
|
220
|
+
|
|
221
|
+
// Append event (coordinates persistence → view → notification)
|
|
222
|
+
const stored = await store.appendEvent(event);
|
|
223
|
+
|
|
224
|
+
// Access components
|
|
225
|
+
store.log // EventLog - persistence
|
|
226
|
+
store.bus // EventBus - pub/sub
|
|
227
|
+
store.views // ViewManager - views
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### EventLog
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// Append event to log
|
|
234
|
+
const stored = await eventLog.append(event, resourceId);
|
|
235
|
+
|
|
236
|
+
// Get all events for resource
|
|
237
|
+
const events = await eventLog.getEvents(resourceId);
|
|
238
|
+
|
|
239
|
+
// Query with filter
|
|
240
|
+
const filtered = await eventLog.queryEvents(resourceId, {
|
|
241
|
+
eventTypes: ['annotation.added'],
|
|
242
|
+
fromSequence: 10,
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### EventBus
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// Subscribe to resource events
|
|
250
|
+
const sub = eventBus.subscribe(resourceId, async (event) => {
|
|
251
|
+
console.log('Event:', event.event.type);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Subscribe to all system events
|
|
255
|
+
const globalSub = eventBus.subscribeGlobal(async (event) => {
|
|
256
|
+
console.log('System event:', event.event.type);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Unsubscribe
|
|
260
|
+
sub.unsubscribe();
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### ViewManager
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Materialize resource view from events
|
|
267
|
+
await viewManager.materializeResource(
|
|
268
|
+
resourceId,
|
|
269
|
+
event,
|
|
270
|
+
() => eventLog.getEvents(resourceId)
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Get materialized view
|
|
274
|
+
const view = await viewStorage.get(resourceId);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Storage Format
|
|
278
|
+
|
|
279
|
+
### Events (JSONL)
|
|
280
|
+
|
|
281
|
+
Events are stored in append-only JSONL files with sharding:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
data/
|
|
285
|
+
events/
|
|
286
|
+
ab/ # Shard level 1 (256 directories)
|
|
287
|
+
cd/ # Shard level 2 (256 subdirectories)
|
|
288
|
+
doc-abc123.jsonl # Event log for resource
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Each line in the JSONL file is a complete `StoredEvent`:
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{"event":{"id":"evt-1","type":"resource.created","timestamp":"2024-01-01T00:00:00Z","resourceId":"doc-abc123","userId":"user@example.com","payload":{}},"metadata":{"sequenceNumber":1,"checksum":"sha256:...","version":"1.0"}}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Views (JSON)
|
|
298
|
+
|
|
299
|
+
Materialized views are stored as JSON files with the same sharding:
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
data/
|
|
303
|
+
projections/
|
|
304
|
+
resources/
|
|
305
|
+
ab/
|
|
306
|
+
cd/
|
|
307
|
+
doc-abc123.json # Materialized view
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Performance
|
|
311
|
+
|
|
312
|
+
- **Sharding** - 65,536 shards using Jump Consistent Hash prevents filesystem bottlenecks
|
|
313
|
+
- **Append-only** - JSONL writes are fast (no updates, only appends)
|
|
314
|
+
- **In-memory subscriptions** - Pub/sub has zero I/O overhead
|
|
315
|
+
- **Lazy view materialization** - Views only built on demand or when events occur
|
|
316
|
+
|
|
317
|
+
## Error Handling
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
try {
|
|
321
|
+
await eventStore.appendEvent(event);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error.code === 'ENOENT') {
|
|
324
|
+
// Storage directory doesn't exist
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Testing
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { EventStore, FilesystemViewStorage } from '@semiont/event-sourcing';
|
|
334
|
+
import { describe, it, beforeEach } from 'vitest';
|
|
335
|
+
|
|
336
|
+
describe('Event sourcing', () => {
|
|
337
|
+
let eventStore: EventStore;
|
|
338
|
+
|
|
339
|
+
beforeEach(() => {
|
|
340
|
+
eventStore = new EventStore(
|
|
341
|
+
{ basePath: './test-data', dataDir: './test-data', enableSharding: false },
|
|
342
|
+
new FilesystemViewStorage('./test-data'),
|
|
343
|
+
{ baseUrl: 'http://localhost:4000' }
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should append and retrieve events', async () => {
|
|
348
|
+
const event = await eventStore.appendEvent({
|
|
349
|
+
type: 'resource.created',
|
|
350
|
+
resourceId: resourceId('test-1'),
|
|
351
|
+
userId: userId('test@example.com'),
|
|
352
|
+
payload: {},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const events = await eventStore.log.getEvents(resourceId('test-1'));
|
|
356
|
+
expect(events).toHaveLength(1);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Examples
|
|
362
|
+
|
|
363
|
+
### Building a CLI Tool
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { EventStore, FilesystemViewStorage } from '@semiont/event-sourcing';
|
|
367
|
+
import { resourceId, userId } from '@semiont/core';
|
|
368
|
+
|
|
369
|
+
async function rebuildViews(basePath: string) {
|
|
370
|
+
const store = new EventStore(
|
|
371
|
+
{ basePath, dataDir: basePath, enableSharding: true },
|
|
372
|
+
new FilesystemViewStorage(basePath),
|
|
373
|
+
{ baseUrl: 'http://localhost:4000' }
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const resourceIds = await store.log.getAllResourceIds();
|
|
377
|
+
console.log(`Rebuilding ${resourceIds.length} resources...`);
|
|
378
|
+
|
|
379
|
+
for (const id of resourceIds) {
|
|
380
|
+
const events = await store.log.getEvents(id);
|
|
381
|
+
console.log(`Resource ${id}: ${events.length} events`);
|
|
382
|
+
// Views are automatically materialized by ViewManager
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Event Processing Worker
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
async function startWorker() {
|
|
391
|
+
const store = new EventStore(/* config */);
|
|
392
|
+
|
|
393
|
+
// Subscribe to all annotation events
|
|
394
|
+
store.bus.subscribeGlobal(async (event) => {
|
|
395
|
+
if (event.event.type === 'annotation.added') {
|
|
396
|
+
console.log('Processing annotation:', event.event.payload);
|
|
397
|
+
// Custom processing logic here
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
console.log('Worker started, listening for events...');
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## License
|
|
406
|
+
|
|
407
|
+
Apache-2.0
|