xcomponent-ai 0.1.1
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/CONTRIBUTING.md +195 -0
- package/LICENSE +45 -0
- package/PERSISTENCE.md +774 -0
- package/README.md +594 -0
- package/dist/agents.d.ts +81 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +405 -0
- package/dist/agents.js.map +1 -0
- package/dist/api.d.ts +36 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +404 -0
- package/dist/api.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +437 -0
- package/dist/cli.js.map +1 -0
- package/dist/component-registry.d.ts +190 -0
- package/dist/component-registry.d.ts.map +1 -0
- package/dist/component-registry.js +382 -0
- package/dist/component-registry.js.map +1 -0
- package/dist/fsm-runtime.d.ts +263 -0
- package/dist/fsm-runtime.d.ts.map +1 -0
- package/dist/fsm-runtime.js +1122 -0
- package/dist/fsm-runtime.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/monitoring.d.ts +68 -0
- package/dist/monitoring.d.ts.map +1 -0
- package/dist/monitoring.js +176 -0
- package/dist/monitoring.js.map +1 -0
- package/dist/persistence.d.ts +100 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +270 -0
- package/dist/persistence.js.map +1 -0
- package/dist/timer-wheel.d.ts +85 -0
- package/dist/timer-wheel.d.ts.map +1 -0
- package/dist/timer-wheel.js +181 -0
- package/dist/timer-wheel.js.map +1 -0
- package/dist/types.d.ts +404 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/dist/websockets.d.ts +32 -0
- package/dist/websockets.d.ts.map +1 -0
- package/dist/websockets.js +117 -0
- package/dist/websockets.js.map +1 -0
- package/package.json +103 -0
package/PERSISTENCE.md
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
# Persistence and Event Sourcing
|
|
2
|
+
|
|
3
|
+
This document covers how to configure and use persistence in xcomponent-ai for event sourcing, snapshots, and production deployments.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Configuration](#configuration)
|
|
9
|
+
- [In-Memory Storage](#in-memory-storage)
|
|
10
|
+
- [Custom Storage Implementations](#custom-storage-implementations)
|
|
11
|
+
- [EventStore Interface](#eventstore-interface)
|
|
12
|
+
- [SnapshotStore Interface](#snapshotstore-interface)
|
|
13
|
+
- [PostgreSQL Implementation](#postgresql-implementation)
|
|
14
|
+
- [MongoDB Implementation](#mongodb-implementation)
|
|
15
|
+
- [Cross-Component Traceability](#cross-component-traceability)
|
|
16
|
+
- [Best Practices](#best-practices)
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
xcomponent-ai supports event sourcing and state snapshots for:
|
|
21
|
+
|
|
22
|
+
- **Event Sourcing**: Full audit trail of all state transitions
|
|
23
|
+
- **Snapshots**: Fast state restoration without replaying all events
|
|
24
|
+
- **Cross-Component Traceability**: Trace events across component boundaries
|
|
25
|
+
- **Disaster Recovery**: Reconstruct system state from persisted events
|
|
26
|
+
- **Debugging**: Analyze event causality chains
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Enable persistence when creating an FSMRuntime:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { FSMRuntime } from './fsm-runtime';
|
|
34
|
+
import { InMemoryEventStore, InMemorySnapshotStore } from './persistence';
|
|
35
|
+
|
|
36
|
+
const runtime = new FSMRuntime(component, {
|
|
37
|
+
// Event sourcing: persist all events
|
|
38
|
+
eventSourcing: true,
|
|
39
|
+
|
|
40
|
+
// Snapshots: periodically save full state
|
|
41
|
+
snapshots: true,
|
|
42
|
+
|
|
43
|
+
// Snapshot interval: save every N transitions (default: 10)
|
|
44
|
+
snapshotInterval: 20,
|
|
45
|
+
|
|
46
|
+
// Custom stores (optional)
|
|
47
|
+
eventStore: new InMemoryEventStore(),
|
|
48
|
+
snapshotStore: new InMemorySnapshotStore(),
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## In-Memory Storage
|
|
53
|
+
|
|
54
|
+
For development and testing, use the built-in in-memory stores:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { InMemoryEventStore, InMemorySnapshotStore } from './persistence';
|
|
58
|
+
|
|
59
|
+
const eventStore = new InMemoryEventStore();
|
|
60
|
+
const snapshotStore = new InMemorySnapshotStore();
|
|
61
|
+
|
|
62
|
+
const runtime = new FSMRuntime(component, {
|
|
63
|
+
eventSourcing: true,
|
|
64
|
+
snapshots: true,
|
|
65
|
+
eventStore,
|
|
66
|
+
snapshotStore,
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Note**: In-memory stores lose data on restart. Use database-backed stores for production.
|
|
71
|
+
|
|
72
|
+
## Custom Storage Implementations
|
|
73
|
+
|
|
74
|
+
### EventStore Interface
|
|
75
|
+
|
|
76
|
+
Implement the `EventStore` interface for custom event storage:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { EventStore, PersistedEvent } from './types';
|
|
80
|
+
|
|
81
|
+
export interface EventStore {
|
|
82
|
+
/**
|
|
83
|
+
* Append event to store
|
|
84
|
+
*/
|
|
85
|
+
append(event: PersistedEvent): Promise<void>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get all events for a specific instance
|
|
89
|
+
*/
|
|
90
|
+
getEventsForInstance(instanceId: string): Promise<PersistedEvent[]>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get events within time range
|
|
94
|
+
*/
|
|
95
|
+
getEventsByTimeRange(startTime: number, endTime: number): Promise<PersistedEvent[]>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get events caused by a specific event (causality)
|
|
99
|
+
*/
|
|
100
|
+
getCausedEvents(eventId: string): Promise<PersistedEvent[]>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get all events (for backup/export)
|
|
104
|
+
*/
|
|
105
|
+
getAllEvents(): Promise<PersistedEvent[]>;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### SnapshotStore Interface
|
|
110
|
+
|
|
111
|
+
Implement the `SnapshotStore` interface for custom snapshot storage:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { SnapshotStore, InstanceSnapshot } from './types';
|
|
115
|
+
|
|
116
|
+
export interface SnapshotStore {
|
|
117
|
+
/**
|
|
118
|
+
* Save instance snapshot
|
|
119
|
+
*/
|
|
120
|
+
saveSnapshot(snapshot: InstanceSnapshot): Promise<void>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get latest snapshot for instance
|
|
124
|
+
*/
|
|
125
|
+
getSnapshot(instanceId: string): Promise<InstanceSnapshot | null>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get all snapshots (for backup/export)
|
|
129
|
+
*/
|
|
130
|
+
getAllSnapshots(): Promise<InstanceSnapshot[]>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delete snapshot
|
|
134
|
+
*/
|
|
135
|
+
deleteSnapshot(instanceId: string): Promise<void>;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## PostgreSQL Implementation
|
|
140
|
+
|
|
141
|
+
Example PostgreSQL-backed persistence:
|
|
142
|
+
|
|
143
|
+
### Schema
|
|
144
|
+
|
|
145
|
+
```sql
|
|
146
|
+
-- Events table
|
|
147
|
+
CREATE TABLE fsm_events (
|
|
148
|
+
id VARCHAR(50) PRIMARY KEY,
|
|
149
|
+
instance_id VARCHAR(50) NOT NULL,
|
|
150
|
+
machine_name VARCHAR(100) NOT NULL,
|
|
151
|
+
component_name VARCHAR(100) NOT NULL,
|
|
152
|
+
event_type VARCHAR(50) NOT NULL,
|
|
153
|
+
event_payload JSONB NOT NULL,
|
|
154
|
+
state_before VARCHAR(50) NOT NULL,
|
|
155
|
+
state_after VARCHAR(50) NOT NULL,
|
|
156
|
+
persisted_at BIGINT NOT NULL,
|
|
157
|
+
caused_by VARCHAR(50)[],
|
|
158
|
+
caused VARCHAR(50)[],
|
|
159
|
+
source_component_name VARCHAR(100),
|
|
160
|
+
target_component_name VARCHAR(100),
|
|
161
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
-- Indexes for performance
|
|
165
|
+
CREATE INDEX idx_events_instance ON fsm_events(instance_id);
|
|
166
|
+
CREATE INDEX idx_events_component ON fsm_events(component_name);
|
|
167
|
+
CREATE INDEX idx_events_machine ON fsm_events(machine_name);
|
|
168
|
+
CREATE INDEX idx_events_timestamp ON fsm_events(persisted_at);
|
|
169
|
+
CREATE INDEX idx_events_caused_by ON fsm_events USING GIN(caused_by);
|
|
170
|
+
|
|
171
|
+
-- Snapshots table
|
|
172
|
+
CREATE TABLE fsm_snapshots (
|
|
173
|
+
instance_id VARCHAR(50) PRIMARY KEY,
|
|
174
|
+
machine_name VARCHAR(100) NOT NULL,
|
|
175
|
+
current_state VARCHAR(50) NOT NULL,
|
|
176
|
+
status VARCHAR(20) NOT NULL,
|
|
177
|
+
context JSONB NOT NULL,
|
|
178
|
+
public_member JSONB,
|
|
179
|
+
snapshot_at BIGINT NOT NULL,
|
|
180
|
+
last_event_id VARCHAR(50) NOT NULL,
|
|
181
|
+
pending_timeouts JSONB,
|
|
182
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
183
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
184
|
+
);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Implementation
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { Pool } from 'pg';
|
|
191
|
+
import { EventStore, PersistedEvent } from './types';
|
|
192
|
+
|
|
193
|
+
export class PostgreSQLEventStore implements EventStore {
|
|
194
|
+
private pool: Pool;
|
|
195
|
+
|
|
196
|
+
constructor(connectionString: string) {
|
|
197
|
+
this.pool = new Pool({ connectionString });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async append(event: PersistedEvent): Promise<void> {
|
|
201
|
+
const query = `
|
|
202
|
+
INSERT INTO fsm_events (
|
|
203
|
+
id, instance_id, machine_name, component_name,
|
|
204
|
+
event_type, event_payload, state_before, state_after,
|
|
205
|
+
persisted_at, caused_by, caused,
|
|
206
|
+
source_component_name, target_component_name
|
|
207
|
+
)
|
|
208
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
await this.pool.query(query, [
|
|
212
|
+
event.id,
|
|
213
|
+
event.instanceId,
|
|
214
|
+
event.machineName,
|
|
215
|
+
event.componentName,
|
|
216
|
+
event.event.type,
|
|
217
|
+
JSON.stringify(event.event.payload),
|
|
218
|
+
event.stateBefore,
|
|
219
|
+
event.stateAfter,
|
|
220
|
+
event.persistedAt,
|
|
221
|
+
event.causedBy || [],
|
|
222
|
+
event.caused || [],
|
|
223
|
+
event.sourceComponentName || null,
|
|
224
|
+
event.targetComponentName || null,
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async getEventsForInstance(instanceId: string): Promise<PersistedEvent[]> {
|
|
229
|
+
const query = `
|
|
230
|
+
SELECT * FROM fsm_events
|
|
231
|
+
WHERE instance_id = $1
|
|
232
|
+
ORDER BY persisted_at ASC
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
const result = await this.pool.query(query, [instanceId]);
|
|
236
|
+
return result.rows.map(row => this.rowToEvent(row));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async getEventsByTimeRange(startTime: number, endTime: number): Promise<PersistedEvent[]> {
|
|
240
|
+
const query = `
|
|
241
|
+
SELECT * FROM fsm_events
|
|
242
|
+
WHERE persisted_at >= $1 AND persisted_at <= $2
|
|
243
|
+
ORDER BY persisted_at ASC
|
|
244
|
+
`;
|
|
245
|
+
|
|
246
|
+
const result = await this.pool.query(query, [startTime, endTime]);
|
|
247
|
+
return result.rows.map(row => this.rowToEvent(row));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async getCausedEvents(eventId: string): Promise<PersistedEvent[]> {
|
|
251
|
+
const query = `
|
|
252
|
+
SELECT * FROM fsm_events
|
|
253
|
+
WHERE $1 = ANY(caused_by)
|
|
254
|
+
ORDER BY persisted_at ASC
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
const result = await this.pool.query(query, [eventId]);
|
|
258
|
+
return result.rows.map(row => this.rowToEvent(row));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async getAllEvents(): Promise<PersistedEvent[]> {
|
|
262
|
+
const query = `
|
|
263
|
+
SELECT * FROM fsm_events
|
|
264
|
+
ORDER BY persisted_at ASC
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
const result = await this.pool.query(query);
|
|
268
|
+
return result.rows.map(row => this.rowToEvent(row));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private rowToEvent(row: any): PersistedEvent {
|
|
272
|
+
return {
|
|
273
|
+
id: row.id,
|
|
274
|
+
instanceId: row.instance_id,
|
|
275
|
+
machineName: row.machine_name,
|
|
276
|
+
componentName: row.component_name,
|
|
277
|
+
event: {
|
|
278
|
+
type: row.event_type,
|
|
279
|
+
payload: JSON.parse(row.event_payload),
|
|
280
|
+
timestamp: row.persisted_at,
|
|
281
|
+
},
|
|
282
|
+
stateBefore: row.state_before,
|
|
283
|
+
stateAfter: row.state_after,
|
|
284
|
+
persistedAt: row.persisted_at,
|
|
285
|
+
causedBy: row.caused_by,
|
|
286
|
+
caused: row.caused,
|
|
287
|
+
sourceComponentName: row.source_component_name,
|
|
288
|
+
targetComponentName: row.target_component_name,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async close(): Promise<void> {
|
|
293
|
+
await this.pool.end();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export class PostgreSQLSnapshotStore implements SnapshotStore {
|
|
298
|
+
private pool: Pool;
|
|
299
|
+
|
|
300
|
+
constructor(connectionString: string) {
|
|
301
|
+
this.pool = new Pool({ connectionString });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async saveSnapshot(snapshot: InstanceSnapshot): Promise<void> {
|
|
305
|
+
const query = `
|
|
306
|
+
INSERT INTO fsm_snapshots (
|
|
307
|
+
instance_id, machine_name, current_state, status,
|
|
308
|
+
context, public_member, snapshot_at, last_event_id,
|
|
309
|
+
pending_timeouts, updated_at
|
|
310
|
+
)
|
|
311
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP)
|
|
312
|
+
ON CONFLICT (instance_id)
|
|
313
|
+
DO UPDATE SET
|
|
314
|
+
current_state = $3,
|
|
315
|
+
status = $4,
|
|
316
|
+
context = $5,
|
|
317
|
+
public_member = $6,
|
|
318
|
+
snapshot_at = $7,
|
|
319
|
+
last_event_id = $8,
|
|
320
|
+
pending_timeouts = $9,
|
|
321
|
+
updated_at = CURRENT_TIMESTAMP
|
|
322
|
+
`;
|
|
323
|
+
|
|
324
|
+
await this.pool.query(query, [
|
|
325
|
+
snapshot.instance.id,
|
|
326
|
+
snapshot.instance.machineName,
|
|
327
|
+
snapshot.instance.currentState,
|
|
328
|
+
snapshot.instance.status,
|
|
329
|
+
JSON.stringify(snapshot.instance.context),
|
|
330
|
+
snapshot.instance.publicMember ? JSON.stringify(snapshot.instance.publicMember) : null,
|
|
331
|
+
snapshot.snapshotAt,
|
|
332
|
+
snapshot.lastEventId,
|
|
333
|
+
snapshot.pendingTimeouts ? JSON.stringify(snapshot.pendingTimeouts) : null,
|
|
334
|
+
]);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async getSnapshot(instanceId: string): Promise<InstanceSnapshot | null> {
|
|
338
|
+
const query = `
|
|
339
|
+
SELECT * FROM fsm_snapshots
|
|
340
|
+
WHERE instance_id = $1
|
|
341
|
+
`;
|
|
342
|
+
|
|
343
|
+
const result = await this.pool.query(query, [instanceId]);
|
|
344
|
+
if (result.rows.length === 0) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const row = result.rows[0];
|
|
349
|
+
return {
|
|
350
|
+
instance: {
|
|
351
|
+
id: row.instance_id,
|
|
352
|
+
machineName: row.machine_name,
|
|
353
|
+
currentState: row.current_state,
|
|
354
|
+
status: row.status,
|
|
355
|
+
context: JSON.parse(row.context),
|
|
356
|
+
publicMember: row.public_member ? JSON.parse(row.public_member) : undefined,
|
|
357
|
+
createdAt: 0, // Not stored
|
|
358
|
+
updatedAt: 0, // Not stored
|
|
359
|
+
},
|
|
360
|
+
snapshotAt: row.snapshot_at,
|
|
361
|
+
lastEventId: row.last_event_id,
|
|
362
|
+
pendingTimeouts: row.pending_timeouts ? JSON.parse(row.pending_timeouts) : undefined,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async getAllSnapshots(): Promise<InstanceSnapshot[]> {
|
|
367
|
+
const query = `SELECT * FROM fsm_snapshots`;
|
|
368
|
+
const result = await this.pool.query(query);
|
|
369
|
+
|
|
370
|
+
return result.rows.map(row => ({
|
|
371
|
+
instance: {
|
|
372
|
+
id: row.instance_id,
|
|
373
|
+
machineName: row.machine_name,
|
|
374
|
+
currentState: row.current_state,
|
|
375
|
+
status: row.status,
|
|
376
|
+
context: JSON.parse(row.context),
|
|
377
|
+
publicMember: row.public_member ? JSON.parse(row.public_member) : undefined,
|
|
378
|
+
createdAt: 0,
|
|
379
|
+
updatedAt: 0,
|
|
380
|
+
},
|
|
381
|
+
snapshotAt: row.snapshot_at,
|
|
382
|
+
lastEventId: row.last_event_id,
|
|
383
|
+
pendingTimeouts: row.pending_timeouts ? JSON.parse(row.pending_timeouts) : undefined,
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async deleteSnapshot(instanceId: string): Promise<void> {
|
|
388
|
+
const query = `DELETE FROM fsm_snapshots WHERE instance_id = $1`;
|
|
389
|
+
await this.pool.query(query, [instanceId]);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async close(): Promise<void> {
|
|
393
|
+
await this.pool.end();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Usage
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { FSMRuntime } from './fsm-runtime';
|
|
402
|
+
import { PostgreSQLEventStore, PostgreSQLSnapshotStore } from './persistence/postgresql';
|
|
403
|
+
|
|
404
|
+
const connectionString = process.env.DATABASE_URL || 'postgresql://user:pass@localhost/xcomponent';
|
|
405
|
+
|
|
406
|
+
const eventStore = new PostgreSQLEventStore(connectionString);
|
|
407
|
+
const snapshotStore = new PostgreSQLSnapshotStore(connectionString);
|
|
408
|
+
|
|
409
|
+
const runtime = new FSMRuntime(component, {
|
|
410
|
+
eventSourcing: true,
|
|
411
|
+
snapshots: true,
|
|
412
|
+
snapshotInterval: 50,
|
|
413
|
+
eventStore,
|
|
414
|
+
snapshotStore,
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## MongoDB Implementation
|
|
419
|
+
|
|
420
|
+
Example MongoDB-backed persistence:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
import { MongoClient, Collection, Db } from 'mongodb';
|
|
424
|
+
import { EventStore, SnapshotStore, PersistedEvent, InstanceSnapshot } from './types';
|
|
425
|
+
|
|
426
|
+
export class MongoDBEventStore implements EventStore {
|
|
427
|
+
private client: MongoClient;
|
|
428
|
+
private db: Db | null = null;
|
|
429
|
+
private events: Collection | null = null;
|
|
430
|
+
|
|
431
|
+
constructor(private uri: string, private dbName: string = 'xcomponent') {}
|
|
432
|
+
|
|
433
|
+
async connect(): Promise<void> {
|
|
434
|
+
this.client = new MongoClient(this.uri);
|
|
435
|
+
await this.client.connect();
|
|
436
|
+
this.db = this.client.db(this.dbName);
|
|
437
|
+
this.events = this.db.collection('events');
|
|
438
|
+
|
|
439
|
+
// Create indexes
|
|
440
|
+
await this.events.createIndex({ instanceId: 1 });
|
|
441
|
+
await this.events.createIndex({ componentName: 1 });
|
|
442
|
+
await this.events.createIndex({ persistedAt: 1 });
|
|
443
|
+
await this.events.createIndex({ 'causedBy': 1 });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async append(event: PersistedEvent): Promise<void> {
|
|
447
|
+
await this.events!.insertOne({
|
|
448
|
+
...event,
|
|
449
|
+
_id: event.id,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async getEventsForInstance(instanceId: string): Promise<PersistedEvent[]> {
|
|
454
|
+
const docs = await this.events!
|
|
455
|
+
.find({ instanceId })
|
|
456
|
+
.sort({ persistedAt: 1 })
|
|
457
|
+
.toArray();
|
|
458
|
+
|
|
459
|
+
return docs.map(doc => this.docToEvent(doc));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async getEventsByTimeRange(startTime: number, endTime: number): Promise<PersistedEvent[]> {
|
|
463
|
+
const docs = await this.events!
|
|
464
|
+
.find({
|
|
465
|
+
persistedAt: { $gte: startTime, $lte: endTime },
|
|
466
|
+
})
|
|
467
|
+
.sort({ persistedAt: 1 })
|
|
468
|
+
.toArray();
|
|
469
|
+
|
|
470
|
+
return docs.map(doc => this.docToEvent(doc));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async getCausedEvents(eventId: string): Promise<PersistedEvent[]> {
|
|
474
|
+
const docs = await this.events!
|
|
475
|
+
.find({ causedBy: eventId })
|
|
476
|
+
.sort({ persistedAt: 1 })
|
|
477
|
+
.toArray();
|
|
478
|
+
|
|
479
|
+
return docs.map(doc => this.docToEvent(doc));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async getAllEvents(): Promise<PersistedEvent[]> {
|
|
483
|
+
const docs = await this.events!
|
|
484
|
+
.find({})
|
|
485
|
+
.sort({ persistedAt: 1 })
|
|
486
|
+
.toArray();
|
|
487
|
+
|
|
488
|
+
return docs.map(doc => this.docToEvent(doc));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private docToEvent(doc: any): PersistedEvent {
|
|
492
|
+
const { _id, ...rest } = doc;
|
|
493
|
+
return { id: _id, ...rest } as PersistedEvent;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async close(): Promise<void> {
|
|
497
|
+
await this.client.close();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export class MongoDBSnapshotStore implements SnapshotStore {
|
|
502
|
+
private client: MongoClient;
|
|
503
|
+
private db: Db | null = null;
|
|
504
|
+
private snapshots: Collection | null = null;
|
|
505
|
+
|
|
506
|
+
constructor(private uri: string, private dbName: string = 'xcomponent') {}
|
|
507
|
+
|
|
508
|
+
async connect(): Promise<void> {
|
|
509
|
+
this.client = new MongoClient(this.uri);
|
|
510
|
+
await this.client.connect();
|
|
511
|
+
this.db = this.client.db(this.dbName);
|
|
512
|
+
this.snapshots = this.db.collection('snapshots');
|
|
513
|
+
|
|
514
|
+
// Create index
|
|
515
|
+
await this.snapshots.createIndex({ 'instance.id': 1 }, { unique: true });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async saveSnapshot(snapshot: InstanceSnapshot): Promise<void> {
|
|
519
|
+
await this.snapshots!.replaceOne(
|
|
520
|
+
{ 'instance.id': snapshot.instance.id },
|
|
521
|
+
snapshot,
|
|
522
|
+
{ upsert: true }
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async getSnapshot(instanceId: string): Promise<InstanceSnapshot | null> {
|
|
527
|
+
const doc = await this.snapshots!.findOne({ 'instance.id': instanceId });
|
|
528
|
+
return doc as InstanceSnapshot | null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async getAllSnapshots(): Promise<InstanceSnapshot[]> {
|
|
532
|
+
const docs = await this.snapshots!.find({}).toArray();
|
|
533
|
+
return docs as InstanceSnapshot[];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async deleteSnapshot(instanceId: string): Promise<void> {
|
|
537
|
+
await this.snapshots!.deleteOne({ 'instance.id': instanceId });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async close(): Promise<void> {
|
|
541
|
+
await this.client.close();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Usage
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
import { FSMRuntime } from './fsm-runtime';
|
|
550
|
+
import { MongoDBEventStore, MongoDBSnapshotStore } from './persistence/mongodb';
|
|
551
|
+
|
|
552
|
+
const mongoUri = process.env.MONGO_URI || 'mongodb://localhost:27017';
|
|
553
|
+
|
|
554
|
+
const eventStore = new MongoDBEventStore(mongoUri, 'xcomponent');
|
|
555
|
+
const snapshotStore = new MongoDBSnapshotStore(mongoUri, 'xcomponent');
|
|
556
|
+
|
|
557
|
+
await eventStore.connect();
|
|
558
|
+
await snapshotStore.connect();
|
|
559
|
+
|
|
560
|
+
const runtime = new FSMRuntime(component, {
|
|
561
|
+
eventSourcing: true,
|
|
562
|
+
snapshots: true,
|
|
563
|
+
eventStore,
|
|
564
|
+
snapshotStore,
|
|
565
|
+
});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## Cross-Component Traceability
|
|
569
|
+
|
|
570
|
+
xcomponent-ai supports tracing events across component boundaries:
|
|
571
|
+
|
|
572
|
+
### Component Name Tracking
|
|
573
|
+
|
|
574
|
+
All persisted events include the component name:
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
interface PersistedEvent {
|
|
578
|
+
id: string;
|
|
579
|
+
instanceId: string;
|
|
580
|
+
machineName: string;
|
|
581
|
+
componentName: string; // Component where event occurred
|
|
582
|
+
// ...
|
|
583
|
+
sourceComponentName?: string; // Optional: source component
|
|
584
|
+
targetComponentName?: string; // Optional: target component
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Tracing Across Components
|
|
589
|
+
|
|
590
|
+
Use ComponentRegistry for cross-component traceability:
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import { ComponentRegistry } from './component-registry';
|
|
594
|
+
|
|
595
|
+
const registry = new ComponentRegistry();
|
|
596
|
+
|
|
597
|
+
// Register components
|
|
598
|
+
registry.registerComponent(orderComponent, orderRuntime);
|
|
599
|
+
registry.registerComponent(inventoryComponent, inventoryRuntime);
|
|
600
|
+
registry.registerComponent(shippingComponent, shippingRuntime);
|
|
601
|
+
|
|
602
|
+
// Get all events across components
|
|
603
|
+
const allEvents = await registry.getAllPersistedEvents();
|
|
604
|
+
|
|
605
|
+
// Trace causality chain across components
|
|
606
|
+
const causalityChain = await registry.traceEventAcrossComponents(rootEventId);
|
|
607
|
+
|
|
608
|
+
// Find instance regardless of component
|
|
609
|
+
const result = registry.findInstance(instanceId);
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### API Endpoints
|
|
613
|
+
|
|
614
|
+
The API server exposes cross-component traceability endpoints:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
# Trace event causality across all components
|
|
618
|
+
GET /api/cross-component/causality/:eventId
|
|
619
|
+
|
|
620
|
+
# Get all events from all components
|
|
621
|
+
GET /api/cross-component/events
|
|
622
|
+
|
|
623
|
+
# Get instance history (searches all components)
|
|
624
|
+
GET /api/cross-component/instance/:instanceId/history
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## Best Practices
|
|
628
|
+
|
|
629
|
+
### 1. **Event Store Selection**
|
|
630
|
+
|
|
631
|
+
- **Development**: Use `InMemoryEventStore`
|
|
632
|
+
- **Production**: Use PostgreSQL, MongoDB, or similar
|
|
633
|
+
- **High Volume**: Consider time-series databases (TimescaleDB, InfluxDB)
|
|
634
|
+
|
|
635
|
+
### 2. **Snapshot Frequency**
|
|
636
|
+
|
|
637
|
+
Balance between restoration speed and storage:
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
const runtime = new FSMRuntime(component, {
|
|
641
|
+
snapshots: true,
|
|
642
|
+
snapshotInterval: 50, // Snapshot every 50 transitions
|
|
643
|
+
});
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
- **Low-frequency transitions**: Lower interval (10-20)
|
|
647
|
+
- **High-frequency transitions**: Higher interval (50-100)
|
|
648
|
+
- **Long-running workflows**: Lower interval for faster recovery
|
|
649
|
+
|
|
650
|
+
### 3. **Event Retention**
|
|
651
|
+
|
|
652
|
+
Implement retention policies to manage storage:
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
// Delete events older than 90 days
|
|
656
|
+
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
|
|
657
|
+
|
|
658
|
+
const oldEvents = await eventStore.getEventsByTimeRange(0, ninetyDaysAgo);
|
|
659
|
+
// Archive or delete old events
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### 4. **Backup Strategy**
|
|
663
|
+
|
|
664
|
+
Regular backups of event and snapshot stores:
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
# PostgreSQL backup
|
|
668
|
+
pg_dump -t fsm_events -t fsm_snapshots dbname > backup.sql
|
|
669
|
+
|
|
670
|
+
# MongoDB backup
|
|
671
|
+
mongodump --db=xcomponent --collection=events --out=backup/
|
|
672
|
+
mongodump --db=xcomponent --collection=snapshots --out=backup/
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### 5. **Disaster Recovery**
|
|
676
|
+
|
|
677
|
+
Restore from snapshots + replay events:
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
// 1. Get latest snapshot
|
|
681
|
+
const snapshot = await snapshotStore.getSnapshot(instanceId);
|
|
682
|
+
|
|
683
|
+
// 2. Restore instance from snapshot
|
|
684
|
+
const runtime = new FSMRuntime(component, persistenceConfig);
|
|
685
|
+
await runtime.restoreFromSnapshot(snapshot);
|
|
686
|
+
|
|
687
|
+
// 3. Replay events after snapshot
|
|
688
|
+
const events = await eventStore.getEventsAfterSnapshot(
|
|
689
|
+
instanceId,
|
|
690
|
+
snapshot.lastEventId
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
for (const event of events) {
|
|
694
|
+
await runtime.sendEvent(instanceId, event.event);
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### 6. **Monitoring**
|
|
699
|
+
|
|
700
|
+
Monitor event store health:
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
// Track event append latency
|
|
704
|
+
const start = Date.now();
|
|
705
|
+
await eventStore.append(event);
|
|
706
|
+
const latency = Date.now() - start;
|
|
707
|
+
|
|
708
|
+
// Alert if latency > threshold
|
|
709
|
+
if (latency > 100) {
|
|
710
|
+
console.warn(`High event store latency: ${latency}ms`);
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### 7. **Connection Pooling**
|
|
715
|
+
|
|
716
|
+
Use connection pools for databases:
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
// PostgreSQL with pooling
|
|
720
|
+
const pool = new Pool({
|
|
721
|
+
connectionString,
|
|
722
|
+
max: 20, // Max connections
|
|
723
|
+
idleTimeoutMillis: 30000, // Close idle connections
|
|
724
|
+
connectionTimeoutMillis: 2000,
|
|
725
|
+
});
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### 8. **Cross-Component Shared Stores**
|
|
729
|
+
|
|
730
|
+
For cross-component traceability, use shared event stores:
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
const sharedEventStore = new PostgreSQLEventStore(connectionString);
|
|
734
|
+
const sharedSnapshotStore = new PostgreSQLSnapshotStore(connectionString);
|
|
735
|
+
|
|
736
|
+
// All components use same stores
|
|
737
|
+
const orderRuntime = new FSMRuntime(orderComponent, {
|
|
738
|
+
eventStore: sharedEventStore,
|
|
739
|
+
snapshotStore: sharedSnapshotStore,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const inventoryRuntime = new FSMRuntime(inventoryComponent, {
|
|
743
|
+
eventStore: sharedEventStore,
|
|
744
|
+
snapshotStore: sharedSnapshotStore,
|
|
745
|
+
});
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
This enables system-wide event tracing and causality analysis.
|
|
749
|
+
|
|
750
|
+
## Environment Variables
|
|
751
|
+
|
|
752
|
+
Recommended environment variable configuration:
|
|
753
|
+
|
|
754
|
+
```bash
|
|
755
|
+
# Database connection
|
|
756
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/xcomponent
|
|
757
|
+
MONGO_URI=mongodb://localhost:27017
|
|
758
|
+
|
|
759
|
+
# Persistence settings
|
|
760
|
+
EVENT_SOURCING_ENABLED=true
|
|
761
|
+
SNAPSHOTS_ENABLED=true
|
|
762
|
+
SNAPSHOT_INTERVAL=50
|
|
763
|
+
|
|
764
|
+
# Retention
|
|
765
|
+
EVENT_RETENTION_DAYS=90
|
|
766
|
+
SNAPSHOT_RETENTION_COUNT=10
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
## Further Reading
|
|
770
|
+
|
|
771
|
+
- [Event Sourcing Pattern](https://martinfowler.com/eaaDev/EventSourcing.html)
|
|
772
|
+
- [CQRS and Event Sourcing](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
|
|
773
|
+
- [PostgreSQL Best Practices](https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server)
|
|
774
|
+
- [MongoDB Performance](https://docs.mongodb.com/manual/administration/analyzing-mongodb-performance/)
|