claude-flow 3.5.35 → 3.5.37
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/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/init/settings-generator.js +18 -31
- package/v3/@claude-flow/cli/package.json +1 -1
- package/v3/@claude-flow/shared/dist/core/config/loader.js +17 -1
- package/v3/@claude-flow/shared/dist/core/config/schema.d.ts +769 -175
- package/v3/@claude-flow/shared/dist/core/config/schema.js +3 -1
- package/v3/@claude-flow/shared/dist/events/index.d.ts +2 -0
- package/v3/@claude-flow/shared/dist/events/index.js +2 -0
- package/v3/@claude-flow/shared/dist/events/rvf-event-log.d.ts +82 -0
- package/v3/@claude-flow/shared/dist/events/rvf-event-log.js +340 -0
|
@@ -148,11 +148,13 @@ export const OrchestratorConfigSchema = z.object({
|
|
|
148
148
|
});
|
|
149
149
|
/**
|
|
150
150
|
* Full system configuration schema
|
|
151
|
+
* Uses passthrough() to accept unknown extra keys from user configs
|
|
152
|
+
* without failing validation (e.g., simple key-value pairs, custom fields).
|
|
151
153
|
*/
|
|
152
154
|
export const SystemConfigSchema = z.object({
|
|
153
155
|
orchestrator: OrchestratorConfigSchema,
|
|
154
156
|
memory: MemoryConfigSchema.optional(),
|
|
155
157
|
mcp: MCPServerConfigSchema.optional(),
|
|
156
158
|
swarm: SwarmConfigSchema.optional(),
|
|
157
|
-
});
|
|
159
|
+
}).passthrough();
|
|
158
160
|
//# sourceMappingURL=schema.js.map
|
|
@@ -15,5 +15,7 @@ export { EventStore } from './event-store.js';
|
|
|
15
15
|
export type { EventStoreConfig, EventFilter, EventSnapshot, EventStoreStats, } from './event-store.js';
|
|
16
16
|
export { Projection, AgentStateProjection, TaskHistoryProjection, MemoryIndexProjection, } from './projections.js';
|
|
17
17
|
export type { AgentProjectionState, TaskProjectionState, MemoryProjectionState, } from './projections.js';
|
|
18
|
+
export { RvfEventLog } from './rvf-event-log.js';
|
|
19
|
+
export type { RvfEventLogConfig } from './rvf-event-log.js';
|
|
18
20
|
export { StateReconstructor, createStateReconstructor, AgentAggregate, TaskAggregate, type AggregateRoot, type ReconstructorOptions, } from './state-reconstructor.js';
|
|
19
21
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -15,6 +15,8 @@ export { createAgentSpawnedEvent, createAgentStartedEvent, createAgentStoppedEve
|
|
|
15
15
|
export { EventStore } from './event-store.js';
|
|
16
16
|
// Projections
|
|
17
17
|
export { Projection, AgentStateProjection, TaskHistoryProjection, MemoryIndexProjection, } from './projections.js';
|
|
18
|
+
// RVF Event Log (ADR-057 — zero-dependency alternative to EventStore)
|
|
19
|
+
export { RvfEventLog } from './rvf-event-log.js';
|
|
18
20
|
// State Reconstruction (ADR-007)
|
|
19
21
|
export { StateReconstructor, createStateReconstructor, AgentAggregate, TaskAggregate, } from './state-reconstructor.js';
|
|
20
22
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RVF Event Log (ADR-057 Phase 2)
|
|
3
|
+
*
|
|
4
|
+
* Pure-TypeScript append-only event log that stores events in a binary
|
|
5
|
+
* file format. Replaces the sql.js-dependent EventStore with a zero-
|
|
6
|
+
* dependency alternative.
|
|
7
|
+
*
|
|
8
|
+
* Binary format:
|
|
9
|
+
* File header: 4 bytes — magic "RVFL"
|
|
10
|
+
* Record: 4 bytes (uint32 BE payload length) + N bytes (JSON payload)
|
|
11
|
+
*
|
|
12
|
+
* In-memory indexes are rebuilt on initialize() by replaying the file.
|
|
13
|
+
* Snapshots are stored in a separate `.snap.rvf` file using the same format.
|
|
14
|
+
*
|
|
15
|
+
* @module v3/shared/events/rvf-event-log
|
|
16
|
+
*/
|
|
17
|
+
import { EventEmitter } from 'node:events';
|
|
18
|
+
import type { DomainEvent } from './domain-events.js';
|
|
19
|
+
import type { EventFilter, EventSnapshot, EventStoreStats } from './event-store.js';
|
|
20
|
+
export interface RvfEventLogConfig {
|
|
21
|
+
/** Path to event log file */
|
|
22
|
+
logPath: string;
|
|
23
|
+
/** Enable verbose logging */
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
/** Maximum events before snapshot recommendation */
|
|
26
|
+
snapshotThreshold?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare class RvfEventLog extends EventEmitter {
|
|
29
|
+
private config;
|
|
30
|
+
private initialized;
|
|
31
|
+
/**
|
|
32
|
+
* All events kept in insertion order.
|
|
33
|
+
* Rebuilt from the file on initialize().
|
|
34
|
+
*/
|
|
35
|
+
private events;
|
|
36
|
+
/** Fast lookup: aggregateId -> indices into this.events */
|
|
37
|
+
private aggregateIndex;
|
|
38
|
+
/** Version tracking per aggregate */
|
|
39
|
+
private aggregateVersions;
|
|
40
|
+
/** Snapshots keyed by aggregateId (latest wins) */
|
|
41
|
+
private snapshots;
|
|
42
|
+
/** Path to the companion snapshot file */
|
|
43
|
+
private snapshotPath;
|
|
44
|
+
constructor(config?: Partial<RvfEventLogConfig>);
|
|
45
|
+
/** Create / open the log file and rebuild in-memory indexes. */
|
|
46
|
+
initialize(): Promise<void>;
|
|
47
|
+
/** Flush to disk and release resources. */
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
/** Append a domain event to the log. */
|
|
50
|
+
append(event: DomainEvent): Promise<void>;
|
|
51
|
+
/** Save a snapshot for an aggregate. */
|
|
52
|
+
saveSnapshot(snapshot: EventSnapshot): Promise<void>;
|
|
53
|
+
/** Get events for a specific aggregate, optionally from a version. */
|
|
54
|
+
getEvents(aggregateId: string, fromVersion?: number): Promise<DomainEvent[]>;
|
|
55
|
+
/** Query events with an optional filter (matches EventStore.query API). */
|
|
56
|
+
getAllEvents(filter?: EventFilter): Promise<DomainEvent[]>;
|
|
57
|
+
/** Get latest snapshot for an aggregate. */
|
|
58
|
+
getSnapshot(aggregateId: string): Promise<EventSnapshot | null>;
|
|
59
|
+
/** Return event store statistics. */
|
|
60
|
+
getStats(): Promise<EventStoreStats>;
|
|
61
|
+
/**
|
|
62
|
+
* Flush to disk.
|
|
63
|
+
* For the append-only log this is a no-op because every append() call
|
|
64
|
+
* writes to disk synchronously. Provided for API compatibility with
|
|
65
|
+
* EventStore.
|
|
66
|
+
*/
|
|
67
|
+
persist(): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Replay an RVF file and invoke `handler` for every decoded record.
|
|
70
|
+
* Used both for events and snapshots.
|
|
71
|
+
*/
|
|
72
|
+
private replayFile;
|
|
73
|
+
/** Append a single record to an RVF file. */
|
|
74
|
+
private appendRecord;
|
|
75
|
+
/** Add an event to the in-memory indexes. */
|
|
76
|
+
private indexEvent;
|
|
77
|
+
/** Ensure parent directory exists for a file path. */
|
|
78
|
+
private ensureDirectory;
|
|
79
|
+
/** Guard that throws if initialize() has not been called. */
|
|
80
|
+
private ensureInitialized;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=rvf-event-log.d.ts.map
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RVF Event Log (ADR-057 Phase 2)
|
|
3
|
+
*
|
|
4
|
+
* Pure-TypeScript append-only event log that stores events in a binary
|
|
5
|
+
* file format. Replaces the sql.js-dependent EventStore with a zero-
|
|
6
|
+
* dependency alternative.
|
|
7
|
+
*
|
|
8
|
+
* Binary format:
|
|
9
|
+
* File header: 4 bytes — magic "RVFL"
|
|
10
|
+
* Record: 4 bytes (uint32 BE payload length) + N bytes (JSON payload)
|
|
11
|
+
*
|
|
12
|
+
* In-memory indexes are rebuilt on initialize() by replaying the file.
|
|
13
|
+
* Snapshots are stored in a separate `.snap.rvf` file using the same format.
|
|
14
|
+
*
|
|
15
|
+
* @module v3/shared/events/rvf-event-log
|
|
16
|
+
*/
|
|
17
|
+
import { EventEmitter } from 'node:events';
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, renameSync } from 'node:fs';
|
|
19
|
+
import { dirname } from 'node:path';
|
|
20
|
+
/** Validate a file path is safe */
|
|
21
|
+
function validatePath(p) {
|
|
22
|
+
if (p.includes('\0'))
|
|
23
|
+
throw new Error('Event log path contains null bytes');
|
|
24
|
+
}
|
|
25
|
+
const DEFAULT_CONFIG = {
|
|
26
|
+
logPath: 'events.rvf',
|
|
27
|
+
verbose: false,
|
|
28
|
+
snapshotThreshold: 100,
|
|
29
|
+
};
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Constants
|
|
32
|
+
// =============================================================================
|
|
33
|
+
/** Magic bytes that identify an RVF event log file */
|
|
34
|
+
const MAGIC = Buffer.from('RVFL');
|
|
35
|
+
const MAGIC_LENGTH = 4;
|
|
36
|
+
const LENGTH_PREFIX_BYTES = 4;
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// RvfEventLog Implementation
|
|
39
|
+
// =============================================================================
|
|
40
|
+
export class RvfEventLog extends EventEmitter {
|
|
41
|
+
config;
|
|
42
|
+
initialized = false;
|
|
43
|
+
/**
|
|
44
|
+
* All events kept in insertion order.
|
|
45
|
+
* Rebuilt from the file on initialize().
|
|
46
|
+
*/
|
|
47
|
+
events = [];
|
|
48
|
+
/** Fast lookup: aggregateId -> indices into this.events */
|
|
49
|
+
aggregateIndex = new Map();
|
|
50
|
+
/** Version tracking per aggregate */
|
|
51
|
+
aggregateVersions = new Map();
|
|
52
|
+
/** Snapshots keyed by aggregateId (latest wins) */
|
|
53
|
+
snapshots = new Map();
|
|
54
|
+
/** Path to the companion snapshot file */
|
|
55
|
+
snapshotPath;
|
|
56
|
+
constructor(config = {}) {
|
|
57
|
+
super();
|
|
58
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
59
|
+
this.snapshotPath = this.config.logPath.replace(/\.rvf$/, '.snap.rvf');
|
|
60
|
+
if (this.snapshotPath === this.config.logPath) {
|
|
61
|
+
this.snapshotPath = this.config.logPath + '.snap.rvf';
|
|
62
|
+
}
|
|
63
|
+
validatePath(this.config.logPath);
|
|
64
|
+
}
|
|
65
|
+
// ===========================================================================
|
|
66
|
+
// Lifecycle
|
|
67
|
+
// ===========================================================================
|
|
68
|
+
/** Create / open the log file and rebuild in-memory indexes. */
|
|
69
|
+
async initialize() {
|
|
70
|
+
if (this.initialized)
|
|
71
|
+
return;
|
|
72
|
+
this.ensureDirectory(this.config.logPath);
|
|
73
|
+
// --- events file ---
|
|
74
|
+
if (existsSync(this.config.logPath)) {
|
|
75
|
+
this.replayFile(this.config.logPath, (event) => {
|
|
76
|
+
this.indexEvent(event);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const tmpLog = this.config.logPath + '.tmp';
|
|
81
|
+
writeFileSync(tmpLog, MAGIC);
|
|
82
|
+
renameSync(tmpLog, this.config.logPath);
|
|
83
|
+
}
|
|
84
|
+
// --- snapshots file ---
|
|
85
|
+
if (existsSync(this.snapshotPath)) {
|
|
86
|
+
this.replayFile(this.snapshotPath, (_raw) => {
|
|
87
|
+
const snap = _raw;
|
|
88
|
+
this.snapshots.set(snap.aggregateId, snap);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const tmpSnap = this.snapshotPath + '.tmp';
|
|
93
|
+
writeFileSync(tmpSnap, MAGIC);
|
|
94
|
+
renameSync(tmpSnap, this.snapshotPath);
|
|
95
|
+
}
|
|
96
|
+
this.initialized = true;
|
|
97
|
+
if (this.config.verbose) {
|
|
98
|
+
console.log(`[RvfEventLog] Initialized – ${this.events.length} events, ` +
|
|
99
|
+
`${this.snapshots.size} snapshots`);
|
|
100
|
+
}
|
|
101
|
+
this.emit('initialized');
|
|
102
|
+
}
|
|
103
|
+
/** Flush to disk and release resources. */
|
|
104
|
+
async close() {
|
|
105
|
+
if (!this.initialized)
|
|
106
|
+
return;
|
|
107
|
+
// All data is already on disk (append-only), so just clear memory.
|
|
108
|
+
this.events = [];
|
|
109
|
+
this.aggregateIndex.clear();
|
|
110
|
+
this.aggregateVersions.clear();
|
|
111
|
+
this.snapshots.clear();
|
|
112
|
+
this.initialized = false;
|
|
113
|
+
this.emit('shutdown');
|
|
114
|
+
}
|
|
115
|
+
// ===========================================================================
|
|
116
|
+
// Write Operations
|
|
117
|
+
// ===========================================================================
|
|
118
|
+
/** Append a domain event to the log. */
|
|
119
|
+
async append(event) {
|
|
120
|
+
this.ensureInitialized();
|
|
121
|
+
if (!event.aggregateId || typeof event.aggregateId !== 'string') {
|
|
122
|
+
throw new Error('Event must have a valid aggregateId string');
|
|
123
|
+
}
|
|
124
|
+
if (!event.type || typeof event.type !== 'string') {
|
|
125
|
+
throw new Error('Event must have a valid type string');
|
|
126
|
+
}
|
|
127
|
+
// Assign next version for aggregate
|
|
128
|
+
const currentVersion = this.aggregateVersions.get(event.aggregateId) ?? 0;
|
|
129
|
+
const nextVersion = currentVersion + 1;
|
|
130
|
+
event.version = nextVersion;
|
|
131
|
+
// Persist to disk first (crash-safe ordering)
|
|
132
|
+
this.appendRecord(this.config.logPath, event);
|
|
133
|
+
// Update in-memory state
|
|
134
|
+
this.indexEvent(event);
|
|
135
|
+
this.emit('event:appended', event);
|
|
136
|
+
if (nextVersion % this.config.snapshotThreshold === 0) {
|
|
137
|
+
this.emit('snapshot:recommended', {
|
|
138
|
+
aggregateId: event.aggregateId,
|
|
139
|
+
version: nextVersion,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Save a snapshot for an aggregate. */
|
|
144
|
+
async saveSnapshot(snapshot) {
|
|
145
|
+
this.ensureInitialized();
|
|
146
|
+
this.appendRecord(this.snapshotPath, snapshot);
|
|
147
|
+
this.snapshots.set(snapshot.aggregateId, snapshot);
|
|
148
|
+
this.emit('snapshot:saved', snapshot);
|
|
149
|
+
}
|
|
150
|
+
// ===========================================================================
|
|
151
|
+
// Read Operations
|
|
152
|
+
// ===========================================================================
|
|
153
|
+
/** Get events for a specific aggregate, optionally from a version. */
|
|
154
|
+
async getEvents(aggregateId, fromVersion) {
|
|
155
|
+
this.ensureInitialized();
|
|
156
|
+
const indices = this.aggregateIndex.get(aggregateId);
|
|
157
|
+
if (!indices || indices.length === 0)
|
|
158
|
+
return [];
|
|
159
|
+
let result = indices.map((i) => this.events[i]);
|
|
160
|
+
if (fromVersion !== undefined) {
|
|
161
|
+
result = result.filter((e) => e.version >= fromVersion);
|
|
162
|
+
}
|
|
163
|
+
// Events within an aggregate are already version-ordered because we
|
|
164
|
+
// append in order, but sort defensively.
|
|
165
|
+
return result.sort((a, b) => a.version - b.version);
|
|
166
|
+
}
|
|
167
|
+
/** Query events with an optional filter (matches EventStore.query API). */
|
|
168
|
+
async getAllEvents(filter) {
|
|
169
|
+
this.ensureInitialized();
|
|
170
|
+
if (!filter) {
|
|
171
|
+
return [...this.events].sort((a, b) => a.timestamp - b.timestamp);
|
|
172
|
+
}
|
|
173
|
+
let result = [...this.events];
|
|
174
|
+
// Aggregate ID filter
|
|
175
|
+
if (filter.aggregateIds && filter.aggregateIds.length > 0) {
|
|
176
|
+
const set = new Set(filter.aggregateIds);
|
|
177
|
+
result = result.filter((e) => set.has(e.aggregateId));
|
|
178
|
+
}
|
|
179
|
+
// Aggregate type filter
|
|
180
|
+
if (filter.aggregateTypes && filter.aggregateTypes.length > 0) {
|
|
181
|
+
const set = new Set(filter.aggregateTypes);
|
|
182
|
+
result = result.filter((e) => set.has(e.aggregateType));
|
|
183
|
+
}
|
|
184
|
+
// Event type filter
|
|
185
|
+
if (filter.eventTypes && filter.eventTypes.length > 0) {
|
|
186
|
+
const set = new Set(filter.eventTypes);
|
|
187
|
+
result = result.filter((e) => set.has(e.type));
|
|
188
|
+
}
|
|
189
|
+
// Timestamp filters
|
|
190
|
+
if (filter.afterTimestamp !== undefined) {
|
|
191
|
+
result = result.filter((e) => e.timestamp > filter.afterTimestamp);
|
|
192
|
+
}
|
|
193
|
+
if (filter.beforeTimestamp !== undefined) {
|
|
194
|
+
result = result.filter((e) => e.timestamp < filter.beforeTimestamp);
|
|
195
|
+
}
|
|
196
|
+
// Version filter
|
|
197
|
+
if (filter.fromVersion !== undefined) {
|
|
198
|
+
result = result.filter((e) => e.version >= filter.fromVersion);
|
|
199
|
+
}
|
|
200
|
+
// Sort by timestamp ascending (matches EventStore behaviour)
|
|
201
|
+
result.sort((a, b) => a.timestamp - b.timestamp);
|
|
202
|
+
// Pagination
|
|
203
|
+
if (filter.offset) {
|
|
204
|
+
result = result.slice(filter.offset);
|
|
205
|
+
}
|
|
206
|
+
if (filter.limit) {
|
|
207
|
+
result = result.slice(0, filter.limit);
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
/** Get latest snapshot for an aggregate. */
|
|
212
|
+
async getSnapshot(aggregateId) {
|
|
213
|
+
this.ensureInitialized();
|
|
214
|
+
return this.snapshots.get(aggregateId) ?? null;
|
|
215
|
+
}
|
|
216
|
+
/** Return event store statistics. */
|
|
217
|
+
async getStats() {
|
|
218
|
+
this.ensureInitialized();
|
|
219
|
+
const eventsByType = {};
|
|
220
|
+
const eventsByAggregate = {};
|
|
221
|
+
let oldest = null;
|
|
222
|
+
let newest = null;
|
|
223
|
+
for (const event of this.events) {
|
|
224
|
+
// by type
|
|
225
|
+
eventsByType[event.type] = (eventsByType[event.type] ?? 0) + 1;
|
|
226
|
+
// by aggregate
|
|
227
|
+
eventsByAggregate[event.aggregateId] =
|
|
228
|
+
(eventsByAggregate[event.aggregateId] ?? 0) + 1;
|
|
229
|
+
// timestamp range
|
|
230
|
+
if (oldest === null || event.timestamp < oldest)
|
|
231
|
+
oldest = event.timestamp;
|
|
232
|
+
if (newest === null || event.timestamp > newest)
|
|
233
|
+
newest = event.timestamp;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
totalEvents: this.events.length,
|
|
237
|
+
eventsByType,
|
|
238
|
+
eventsByAggregate,
|
|
239
|
+
oldestEvent: oldest,
|
|
240
|
+
newestEvent: newest,
|
|
241
|
+
snapshotCount: this.snapshots.size,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Flush to disk.
|
|
246
|
+
* For the append-only log this is a no-op because every append() call
|
|
247
|
+
* writes to disk synchronously. Provided for API compatibility with
|
|
248
|
+
* EventStore.
|
|
249
|
+
*/
|
|
250
|
+
async persist() {
|
|
251
|
+
// All records are already flushed on append. Nothing to do.
|
|
252
|
+
if (this.config.verbose) {
|
|
253
|
+
console.log('[RvfEventLog] persist() called — all data already on disk');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// ===========================================================================
|
|
257
|
+
// Private Helpers
|
|
258
|
+
// ===========================================================================
|
|
259
|
+
/**
|
|
260
|
+
* Replay an RVF file and invoke `handler` for every decoded record.
|
|
261
|
+
* Used both for events and snapshots.
|
|
262
|
+
*/
|
|
263
|
+
replayFile(filePath, handler) {
|
|
264
|
+
const buf = readFileSync(filePath);
|
|
265
|
+
// Validate magic
|
|
266
|
+
if (buf.length < MAGIC_LENGTH || buf.subarray(0, MAGIC_LENGTH).compare(MAGIC) !== 0) {
|
|
267
|
+
throw new Error(`[RvfEventLog] Invalid file header in ${filePath}`);
|
|
268
|
+
}
|
|
269
|
+
let offset = MAGIC_LENGTH;
|
|
270
|
+
const MAX_PAYLOAD_SIZE = 100 * 1024 * 1024; // 100MB safety limit
|
|
271
|
+
while (offset + LENGTH_PREFIX_BYTES <= buf.length) {
|
|
272
|
+
const payloadLength = buf.readUInt32BE(offset);
|
|
273
|
+
offset += LENGTH_PREFIX_BYTES;
|
|
274
|
+
if (payloadLength > MAX_PAYLOAD_SIZE) {
|
|
275
|
+
if (this.config.verbose) {
|
|
276
|
+
console.warn(`[RvfEventLog] Payload size ${payloadLength} exceeds safety limit`);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
if (offset + payloadLength > buf.length) {
|
|
281
|
+
// Truncated record — stop reading (crash recovery).
|
|
282
|
+
if (this.config.verbose) {
|
|
283
|
+
console.warn(`[RvfEventLog] Truncated record at offset ${offset - LENGTH_PREFIX_BYTES} — ` +
|
|
284
|
+
`expected ${payloadLength} bytes, have ${buf.length - offset}`);
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
const json = buf.subarray(offset, offset + payloadLength).toString('utf8');
|
|
289
|
+
offset += payloadLength;
|
|
290
|
+
try {
|
|
291
|
+
const record = JSON.parse(json);
|
|
292
|
+
handler(record);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
if (this.config.verbose) {
|
|
296
|
+
console.warn(`[RvfEventLog] Corrupt JSON record skipped`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** Append a single record to an RVF file. */
|
|
302
|
+
appendRecord(filePath, record) {
|
|
303
|
+
const json = JSON.stringify(record);
|
|
304
|
+
const payload = Buffer.from(json, 'utf8');
|
|
305
|
+
const lengthBuf = Buffer.allocUnsafe(LENGTH_PREFIX_BYTES);
|
|
306
|
+
lengthBuf.writeUInt32BE(payload.length, 0);
|
|
307
|
+
appendFileSync(filePath, Buffer.concat([lengthBuf, payload]));
|
|
308
|
+
}
|
|
309
|
+
/** Add an event to the in-memory indexes. */
|
|
310
|
+
indexEvent(event) {
|
|
311
|
+
const idx = this.events.length;
|
|
312
|
+
this.events.push(event);
|
|
313
|
+
// aggregateIndex
|
|
314
|
+
let indices = this.aggregateIndex.get(event.aggregateId);
|
|
315
|
+
if (!indices) {
|
|
316
|
+
indices = [];
|
|
317
|
+
this.aggregateIndex.set(event.aggregateId, indices);
|
|
318
|
+
}
|
|
319
|
+
indices.push(idx);
|
|
320
|
+
// version tracker
|
|
321
|
+
const current = this.aggregateVersions.get(event.aggregateId) ?? 0;
|
|
322
|
+
if (event.version > current) {
|
|
323
|
+
this.aggregateVersions.set(event.aggregateId, event.version);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** Ensure parent directory exists for a file path. */
|
|
327
|
+
ensureDirectory(filePath) {
|
|
328
|
+
const dir = dirname(filePath);
|
|
329
|
+
if (!existsSync(dir)) {
|
|
330
|
+
mkdirSync(dir, { recursive: true });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** Guard that throws if initialize() has not been called. */
|
|
334
|
+
ensureInitialized() {
|
|
335
|
+
if (!this.initialized) {
|
|
336
|
+
throw new Error('RvfEventLog not initialized. Call initialize() first.');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=rvf-event-log.js.map
|