@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 ADDED
@@ -0,0 +1,407 @@
1
+ # @semiont/event-sourcing
2
+
3
+ [![Tests](https://github.com/The-AI-Alliance/semiont/actions/workflows/package-tests.yml/badge.svg)](https://github.com/The-AI-Alliance/semiont/actions/workflows/package-tests.yml?query=branch%3Amain+is%3Asuccess+job%3A%22Test+event-sourcing%22)
4
+ [![npm version](https://img.shields.io/npm/v/@semiont/event-sourcing.svg)](https://www.npmjs.com/package/@semiont/event-sourcing)
5
+ [![License](https://img.shields.io/npm/l/@semiont/event-sourcing.svg)](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