@soulcraft/cortex 1.3.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/LICENSE +16 -0
- package/README.md +125 -0
- package/dist/graph/NativeGraphAdjacencyIndex.d.ts +92 -0
- package/dist/graph/NativeGraphAdjacencyIndex.js +671 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +23 -0
- package/dist/license.d.ts +18 -0
- package/dist/license.js +172 -0
- package/dist/native/NativeEmbeddingEngine.d.ts +79 -0
- package/dist/native/NativeEmbeddingEngine.js +302 -0
- package/dist/native/NativeRoaringBitmap32.d.ts +114 -0
- package/dist/native/NativeRoaringBitmap32.js +221 -0
- package/dist/native/ffi.d.ts +20 -0
- package/dist/native/ffi.js +48 -0
- package/dist/native/index.d.ts +30 -0
- package/dist/native/index.js +58 -0
- package/dist/native/napi.d.ts +21 -0
- package/dist/native/napi.js +88 -0
- package/dist/native/types.d.ts +710 -0
- package/dist/native/types.js +16 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.js +115 -0
- package/dist/storage/mmapFileSystemStorage.d.ts +24 -0
- package/dist/storage/mmapFileSystemStorage.js +73 -0
- package/dist/utils/NativeMetadataIndex.d.ts +185 -0
- package/dist/utils/NativeMetadataIndex.js +1274 -0
- package/dist/utils/nativeEntityIdMapper.d.ts +84 -0
- package/dist/utils/nativeEntityIdMapper.js +134 -0
- package/native/brainy-native.darwin-arm64.node +0 -0
- package/native/brainy-native.darwin-x64.node +0 -0
- package/native/brainy-native.linux-arm64-gnu.node +0 -0
- package/native/brainy-native.linux-x64-gnu.node +0 -0
- package/native/brainy-native.win32-x64-msvc.node +0 -0
- package/native/index.d.ts +1068 -0
- package/package.json +66 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeGraphAdjacencyIndex — Thin TS wrapper around the Rust graph engine
|
|
3
|
+
*
|
|
4
|
+
* Handles async storage I/O while the Rust engine handles all in-memory
|
|
5
|
+
* data operations (4 LSM-trees, verb tracking, relationship counts).
|
|
6
|
+
*
|
|
7
|
+
* Storage pattern: adapter-controlled I/O with 3-tier fallback.
|
|
8
|
+
* - Tier 1 (mmap): saveBinaryBlob + getBinaryBlobPath → zero-copy queries
|
|
9
|
+
* - Tier 2 (binary blob): saveBinaryBlob only → binary format in memory
|
|
10
|
+
* - Tier 3 (legacy): saveMetadata → base64 JSON
|
|
11
|
+
*
|
|
12
|
+
* UnifiedCache integration stays in TS (getVerbCached, getVerbsBatchCached)
|
|
13
|
+
* since it coordinates cross-subsystem caching and requires async storage access.
|
|
14
|
+
*/
|
|
15
|
+
import { loadNativeModule } from '../native/index.js';
|
|
16
|
+
import { getGlobalCache, prodLog } from '@soulcraft/brainy/internals';
|
|
17
|
+
// Storage prefixes for each tree
|
|
18
|
+
const TREE_PREFIXES = {
|
|
19
|
+
'source': 'graph-lsm-source',
|
|
20
|
+
'target': 'graph-lsm-target',
|
|
21
|
+
'verbs-source': 'graph-lsm-verbs-source',
|
|
22
|
+
'verbs-target': 'graph-lsm-verbs-target',
|
|
23
|
+
};
|
|
24
|
+
export class GraphAdjacencyIndex {
|
|
25
|
+
storage;
|
|
26
|
+
native;
|
|
27
|
+
unifiedCache;
|
|
28
|
+
config;
|
|
29
|
+
initialized = false;
|
|
30
|
+
isRebuilding = false;
|
|
31
|
+
flushTimer;
|
|
32
|
+
rebuildStartTime = 0;
|
|
33
|
+
totalRelationshipsIndexed = 0;
|
|
34
|
+
isCompacting = {};
|
|
35
|
+
// Adapter capability detection (set in constructor)
|
|
36
|
+
hasBinaryBlobs;
|
|
37
|
+
hasMmapPaths;
|
|
38
|
+
get isInitialized() {
|
|
39
|
+
return this.initialized;
|
|
40
|
+
}
|
|
41
|
+
constructor(storage, config = {}) {
|
|
42
|
+
this.storage = storage;
|
|
43
|
+
this.config = {
|
|
44
|
+
maxIndexSize: config.maxIndexSize ?? 100000,
|
|
45
|
+
rebuildThreshold: config.rebuildThreshold ?? 0.1,
|
|
46
|
+
autoOptimize: config.autoOptimize ?? true,
|
|
47
|
+
flushInterval: config.flushInterval ?? 30000,
|
|
48
|
+
memTableThreshold: config.memTableThreshold ?? 100000,
|
|
49
|
+
maxSSTablesPerLevel: config.maxSSTablesPerLevel ?? 10,
|
|
50
|
+
};
|
|
51
|
+
const bindings = loadNativeModule();
|
|
52
|
+
this.native = new bindings.NativeGraphAdjacencyIndex({
|
|
53
|
+
memTableThreshold: this.config.memTableThreshold,
|
|
54
|
+
maxSstablesPerLevel: this.config.maxSSTablesPerLevel,
|
|
55
|
+
});
|
|
56
|
+
this.unifiedCache = getGlobalCache();
|
|
57
|
+
// Detect adapter capabilities via optional method presence
|
|
58
|
+
this.hasBinaryBlobs = typeof storage.saveBinaryBlob === 'function';
|
|
59
|
+
this.hasMmapPaths = typeof storage.getBinaryBlobPath === 'function';
|
|
60
|
+
if (this.hasMmapPaths) {
|
|
61
|
+
prodLog.info('GraphAdjacencyIndex initialized with native Rust engine (4 embedded LSM-trees, mmap SSTable I/O)');
|
|
62
|
+
}
|
|
63
|
+
else if (this.hasBinaryBlobs) {
|
|
64
|
+
prodLog.info('GraphAdjacencyIndex initialized with native Rust engine (4 embedded LSM-trees, binary blob I/O)');
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
prodLog.info('GraphAdjacencyIndex initialized with native Rust engine (4 embedded LSM-trees)');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Initialization
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
async ensureInitialized() {
|
|
74
|
+
if (this.initialized) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Load manifests and SSTables for all 4 trees
|
|
78
|
+
const treeNames = this.native.treeNames();
|
|
79
|
+
for (const treeName of treeNames) {
|
|
80
|
+
await this.loadTreeFromStorage(treeName);
|
|
81
|
+
}
|
|
82
|
+
// Defensive check: if LSM-trees have data but verbIdSet is empty, populate
|
|
83
|
+
const sourceSize = this.native.size();
|
|
84
|
+
if (sourceSize > 0 && this.native.verbIdCount() === 0) {
|
|
85
|
+
prodLog.warn(`GraphAdjacencyIndex: LSM-trees have ${sourceSize} relationships but verbIdSet is empty. ` +
|
|
86
|
+
`Triggering auto-rebuild to restore consistency.`);
|
|
87
|
+
await this.populateVerbIdSetFromStorage();
|
|
88
|
+
}
|
|
89
|
+
this.startAutoFlush();
|
|
90
|
+
this.initialized = true;
|
|
91
|
+
}
|
|
92
|
+
async populateVerbIdSetFromStorage() {
|
|
93
|
+
prodLog.info('GraphAdjacencyIndex: Populating verbIdSet from storage...');
|
|
94
|
+
const startTime = Date.now();
|
|
95
|
+
let hasMore = true;
|
|
96
|
+
let cursor = undefined;
|
|
97
|
+
let count = 0;
|
|
98
|
+
while (hasMore) {
|
|
99
|
+
const result = await this.storage.getVerbs({
|
|
100
|
+
pagination: { limit: 10000, cursor }
|
|
101
|
+
});
|
|
102
|
+
for (const verb of result.items) {
|
|
103
|
+
const verbType = verb.verb || 'unknown';
|
|
104
|
+
this.native.trackVerbId(verb.id, verbType);
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
hasMore = result.hasMore;
|
|
108
|
+
cursor = result.nextCursor;
|
|
109
|
+
}
|
|
110
|
+
const elapsed = Date.now() - startTime;
|
|
111
|
+
prodLog.info(`GraphAdjacencyIndex: Populated verbIdSet with ${count} verb IDs in ${elapsed}ms`);
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Core graph operations
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
async getNeighbors(id, optionsOrDirection) {
|
|
117
|
+
await this.ensureInitialized();
|
|
118
|
+
const options = typeof optionsOrDirection === 'string'
|
|
119
|
+
? { direction: optionsOrDirection }
|
|
120
|
+
: (optionsOrDirection || {});
|
|
121
|
+
const startTime = performance.now();
|
|
122
|
+
const direction = options.direction || 'both';
|
|
123
|
+
const limit = options.limit ?? null;
|
|
124
|
+
const offset = options.offset ?? null;
|
|
125
|
+
const result = this.native.getNeighbors(id, direction, limit, offset);
|
|
126
|
+
const elapsed = performance.now() - startTime;
|
|
127
|
+
if (elapsed > 5.0) {
|
|
128
|
+
prodLog.warn(`GraphAdjacencyIndex: Slow neighbor lookup for ${id}: ${elapsed.toFixed(2)}ms`);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
async getVerbIdsBySource(sourceId, options) {
|
|
133
|
+
await this.ensureInitialized();
|
|
134
|
+
const startTime = performance.now();
|
|
135
|
+
const limit = options?.limit ?? null;
|
|
136
|
+
const offset = options?.offset ?? null;
|
|
137
|
+
const result = this.native.getVerbIdsBySource(sourceId, limit, offset);
|
|
138
|
+
const elapsed = performance.now() - startTime;
|
|
139
|
+
if (elapsed > 5.0) {
|
|
140
|
+
prodLog.warn(`GraphAdjacencyIndex: Slow getVerbIdsBySource for ${sourceId}: ${elapsed.toFixed(2)}ms`);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
async getVerbIdsByTarget(targetId, options) {
|
|
145
|
+
await this.ensureInitialized();
|
|
146
|
+
const startTime = performance.now();
|
|
147
|
+
const limit = options?.limit ?? null;
|
|
148
|
+
const offset = options?.offset ?? null;
|
|
149
|
+
const result = this.native.getVerbIdsByTarget(targetId, limit, offset);
|
|
150
|
+
const elapsed = performance.now() - startTime;
|
|
151
|
+
if (elapsed > 5.0) {
|
|
152
|
+
prodLog.warn(`GraphAdjacencyIndex: Slow getVerbIdsByTarget for ${targetId}: ${elapsed.toFixed(2)}ms`);
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
async getVerbCached(verbId) {
|
|
157
|
+
const cacheKey = `graph:verb:${verbId}`;
|
|
158
|
+
const verb = await this.unifiedCache.get(cacheKey, async () => {
|
|
159
|
+
const loadedVerb = await this.storage.getVerb(verbId);
|
|
160
|
+
if (loadedVerb) {
|
|
161
|
+
this.unifiedCache.set(cacheKey, loadedVerb, 'other', 128, 50);
|
|
162
|
+
}
|
|
163
|
+
return loadedVerb;
|
|
164
|
+
});
|
|
165
|
+
return verb;
|
|
166
|
+
}
|
|
167
|
+
async getVerbsBatchCached(verbIds) {
|
|
168
|
+
const results = new Map();
|
|
169
|
+
const uncached = [];
|
|
170
|
+
for (const verbId of verbIds) {
|
|
171
|
+
const cacheKey = `graph:verb:${verbId}`;
|
|
172
|
+
const cached = this.unifiedCache.getSync(cacheKey);
|
|
173
|
+
if (cached) {
|
|
174
|
+
results.set(verbId, cached);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
uncached.push(verbId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (uncached.length > 0 && this.storage.getVerbsBatch) {
|
|
181
|
+
const loadedVerbs = await this.storage.getVerbsBatch(uncached);
|
|
182
|
+
for (const [verbId, verb] of loadedVerbs.entries()) {
|
|
183
|
+
const cacheKey = `graph:verb:${verbId}`;
|
|
184
|
+
this.unifiedCache.set(cacheKey, verb, 'other', 128, 50);
|
|
185
|
+
results.set(verbId, verb);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Mutation operations
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
async addVerb(verb) {
|
|
194
|
+
await this.ensureInitialized();
|
|
195
|
+
const startTime = performance.now();
|
|
196
|
+
const verbType = verb.verb || 'unknown';
|
|
197
|
+
const flushNeeded = this.native.addVerb(verb.id, verb.source, verb.target, verbType);
|
|
198
|
+
// Flush any trees that hit their threshold
|
|
199
|
+
const flushPromises = [];
|
|
200
|
+
if (flushNeeded.needsFlushSource) {
|
|
201
|
+
flushPromises.push(this.flushTree('source'));
|
|
202
|
+
}
|
|
203
|
+
if (flushNeeded.needsFlushTarget) {
|
|
204
|
+
flushPromises.push(this.flushTree('target'));
|
|
205
|
+
}
|
|
206
|
+
if (flushNeeded.needsFlushVerbsSource) {
|
|
207
|
+
flushPromises.push(this.flushTree('verbs-source'));
|
|
208
|
+
}
|
|
209
|
+
if (flushNeeded.needsFlushVerbsTarget) {
|
|
210
|
+
flushPromises.push(this.flushTree('verbs-target'));
|
|
211
|
+
}
|
|
212
|
+
if (flushPromises.length > 0) {
|
|
213
|
+
await Promise.all(flushPromises);
|
|
214
|
+
}
|
|
215
|
+
const elapsed = performance.now() - startTime;
|
|
216
|
+
this.totalRelationshipsIndexed++;
|
|
217
|
+
if (elapsed > 10.0) {
|
|
218
|
+
prodLog.warn(`GraphAdjacencyIndex: Slow addVerb for ${verb.id}: ${elapsed.toFixed(2)}ms`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async removeVerb(verbId) {
|
|
222
|
+
await this.ensureInitialized();
|
|
223
|
+
// Load verb to get type info
|
|
224
|
+
const verb = await this.getVerbCached(verbId);
|
|
225
|
+
if (!verb)
|
|
226
|
+
return;
|
|
227
|
+
const startTime = performance.now();
|
|
228
|
+
const verbType = verb.verb || 'unknown';
|
|
229
|
+
this.native.removeVerb(verbId, verbType);
|
|
230
|
+
const elapsed = performance.now() - startTime;
|
|
231
|
+
if (elapsed > 5.0) {
|
|
232
|
+
prodLog.warn(`GraphAdjacencyIndex: Slow removeVerb for ${verbId}: ${elapsed.toFixed(2)}ms`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Size, counts, stats
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
size() {
|
|
239
|
+
return this.native.size();
|
|
240
|
+
}
|
|
241
|
+
getRelationshipCountByType(type) {
|
|
242
|
+
return this.native.getRelationshipCountByType(type);
|
|
243
|
+
}
|
|
244
|
+
getTotalRelationshipCount() {
|
|
245
|
+
return this.native.verbIdCount();
|
|
246
|
+
}
|
|
247
|
+
getAllRelationshipCounts() {
|
|
248
|
+
const json = this.native.getAllRelationshipCountsJson();
|
|
249
|
+
const obj = JSON.parse(json);
|
|
250
|
+
return new Map(Object.entries(obj));
|
|
251
|
+
}
|
|
252
|
+
getRelationshipStats() {
|
|
253
|
+
const json = this.native.getRelationshipStatsJson();
|
|
254
|
+
return JSON.parse(json);
|
|
255
|
+
}
|
|
256
|
+
getStats() {
|
|
257
|
+
const stats = this.native.getStats();
|
|
258
|
+
return {
|
|
259
|
+
totalRelationships: stats.totalRelationships,
|
|
260
|
+
sourceNodes: stats.sourceSstableCount,
|
|
261
|
+
targetNodes: stats.targetSstableCount,
|
|
262
|
+
memoryUsage: stats.sourceMemTableMemory + stats.targetMemTableMemory + (stats.verbIdCount * 8),
|
|
263
|
+
lastRebuild: this.rebuildStartTime,
|
|
264
|
+
rebuildTime: this.isRebuilding ? Date.now() - this.rebuildStartTime : 0,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
isHealthy() {
|
|
268
|
+
if (!this.initialized) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
return !this.isRebuilding && this.native.isHealthy();
|
|
272
|
+
}
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Rebuild
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
async rebuild() {
|
|
277
|
+
await this.ensureInitialized();
|
|
278
|
+
if (this.isRebuilding) {
|
|
279
|
+
prodLog.warn('GraphAdjacencyIndex: Rebuild already in progress');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.isRebuilding = true;
|
|
283
|
+
this.rebuildStartTime = Date.now();
|
|
284
|
+
try {
|
|
285
|
+
prodLog.info('GraphAdjacencyIndex: Starting rebuild...');
|
|
286
|
+
// Clear verb tracking (LSM-trees rebuild from storage via addVerb)
|
|
287
|
+
this.native.clearVerbTracking();
|
|
288
|
+
this.totalRelationshipsIndexed = 0;
|
|
289
|
+
const storageType = this.storage?.constructor.name || '';
|
|
290
|
+
const isLocalStorage = storageType === 'FileSystemStorage' ||
|
|
291
|
+
storageType === 'MmapFileSystemStorage' ||
|
|
292
|
+
storageType === 'MemoryStorage';
|
|
293
|
+
let totalVerbs = 0;
|
|
294
|
+
if (isLocalStorage) {
|
|
295
|
+
prodLog.info(`GraphAdjacencyIndex: Using optimized strategy (${storageType})`);
|
|
296
|
+
const result = await this.storage.getVerbs({
|
|
297
|
+
pagination: { limit: 10000000 }
|
|
298
|
+
});
|
|
299
|
+
for (const verb of result.items) {
|
|
300
|
+
const v = verb;
|
|
301
|
+
const graphVerb = {
|
|
302
|
+
id: v.id,
|
|
303
|
+
source: v.source || v.sourceId,
|
|
304
|
+
target: v.target || v.targetId,
|
|
305
|
+
verb: v.verb,
|
|
306
|
+
createdAt: typeof v.createdAt === 'number'
|
|
307
|
+
? { seconds: Math.floor(v.createdAt / 1000), nanoseconds: (v.createdAt % 1000) * 1000000 }
|
|
308
|
+
: v.createdAt || { seconds: 0, nanoseconds: 0 },
|
|
309
|
+
updatedAt: typeof v.updatedAt === 'number'
|
|
310
|
+
? { seconds: Math.floor(v.updatedAt / 1000), nanoseconds: (v.updatedAt % 1000) * 1000000 }
|
|
311
|
+
: v.updatedAt || { seconds: 0, nanoseconds: 0 },
|
|
312
|
+
createdBy: v.createdBy || { augmentation: 'unknown', version: '0.0.0' },
|
|
313
|
+
service: v.service,
|
|
314
|
+
data: v.data,
|
|
315
|
+
embedding: v.vector || v.embedding,
|
|
316
|
+
confidence: v.confidence,
|
|
317
|
+
weight: v.weight
|
|
318
|
+
};
|
|
319
|
+
await this.addVerb(graphVerb);
|
|
320
|
+
totalVerbs++;
|
|
321
|
+
}
|
|
322
|
+
prodLog.info(`GraphAdjacencyIndex: Loaded ${totalVerbs.toLocaleString()} verbs (local storage)`);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
prodLog.info(`GraphAdjacencyIndex: Using cloud pagination strategy (${storageType})`);
|
|
326
|
+
let hasMore = true;
|
|
327
|
+
let cursor = undefined;
|
|
328
|
+
const batchSize = 1000;
|
|
329
|
+
while (hasMore) {
|
|
330
|
+
const result = await this.storage.getVerbs({
|
|
331
|
+
pagination: { limit: batchSize, cursor }
|
|
332
|
+
});
|
|
333
|
+
for (const verb of result.items) {
|
|
334
|
+
const v = verb;
|
|
335
|
+
const graphVerb = {
|
|
336
|
+
id: v.id,
|
|
337
|
+
source: v.source || v.sourceId,
|
|
338
|
+
target: v.target || v.targetId,
|
|
339
|
+
verb: v.verb,
|
|
340
|
+
createdAt: typeof v.createdAt === 'number'
|
|
341
|
+
? { seconds: Math.floor(v.createdAt / 1000), nanoseconds: (v.createdAt % 1000) * 1000000 }
|
|
342
|
+
: v.createdAt || { seconds: 0, nanoseconds: 0 },
|
|
343
|
+
updatedAt: typeof v.updatedAt === 'number'
|
|
344
|
+
? { seconds: Math.floor(v.updatedAt / 1000), nanoseconds: (v.updatedAt % 1000) * 1000000 }
|
|
345
|
+
: v.updatedAt || { seconds: 0, nanoseconds: 0 },
|
|
346
|
+
createdBy: v.createdBy || { augmentation: 'unknown', version: '0.0.0' },
|
|
347
|
+
service: v.service,
|
|
348
|
+
data: v.data,
|
|
349
|
+
embedding: v.vector || v.embedding,
|
|
350
|
+
confidence: v.confidence,
|
|
351
|
+
weight: v.weight
|
|
352
|
+
};
|
|
353
|
+
await this.addVerb(graphVerb);
|
|
354
|
+
totalVerbs++;
|
|
355
|
+
}
|
|
356
|
+
hasMore = result.hasMore;
|
|
357
|
+
cursor = result.nextCursor;
|
|
358
|
+
if (totalVerbs % 10000 === 0) {
|
|
359
|
+
prodLog.info(`GraphAdjacencyIndex: Indexed ${totalVerbs} verbs...`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
prodLog.info(`GraphAdjacencyIndex: Loaded ${totalVerbs.toLocaleString()} verbs (cloud storage)`);
|
|
363
|
+
}
|
|
364
|
+
const rebuildTime = Date.now() - this.rebuildStartTime;
|
|
365
|
+
const memoryUsage = this.calculateMemoryUsage();
|
|
366
|
+
prodLog.info(`GraphAdjacencyIndex: Rebuild complete in ${rebuildTime}ms`);
|
|
367
|
+
prodLog.info(` - Total relationships: ${totalVerbs}`);
|
|
368
|
+
prodLog.info(` - Memory usage: ${(memoryUsage / 1024 / 1024).toFixed(1)}MB`);
|
|
369
|
+
prodLog.info(` - Stats:`, this.native.getStats());
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
this.isRebuilding = false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Flush and close
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
async flush() {
|
|
379
|
+
if (!this.initialized) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const startTime = Date.now();
|
|
383
|
+
const treeNames = this.native.treeNames();
|
|
384
|
+
const flushPromises = treeNames
|
|
385
|
+
.filter(name => !this.native.memTableIsEmpty(name))
|
|
386
|
+
.map(name => this.flushTree(name));
|
|
387
|
+
if (flushPromises.length > 0) {
|
|
388
|
+
await Promise.all(flushPromises);
|
|
389
|
+
}
|
|
390
|
+
const elapsed = Date.now() - startTime;
|
|
391
|
+
prodLog.debug(`GraphAdjacencyIndex: Flush completed in ${elapsed}ms`);
|
|
392
|
+
}
|
|
393
|
+
async close() {
|
|
394
|
+
if (this.flushTimer) {
|
|
395
|
+
clearInterval(this.flushTimer);
|
|
396
|
+
this.flushTimer = undefined;
|
|
397
|
+
}
|
|
398
|
+
if (this.initialized) {
|
|
399
|
+
// Flush all trees that have pending data
|
|
400
|
+
await this.flush();
|
|
401
|
+
}
|
|
402
|
+
prodLog.info('GraphAdjacencyIndex: Shutdown complete');
|
|
403
|
+
}
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
// Private: Per-tree storage I/O (adapter-controlled)
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
/** Blob key for an SSTable: "graph-lsm/{treeName}/{sstableId}" */
|
|
408
|
+
blobKey(treeName, sstableId) {
|
|
409
|
+
return `graph-lsm/${treeName}/${sstableId}`;
|
|
410
|
+
}
|
|
411
|
+
async loadTreeFromStorage(treeName) {
|
|
412
|
+
const prefix = TREE_PREFIXES[treeName];
|
|
413
|
+
if (!prefix)
|
|
414
|
+
return;
|
|
415
|
+
try {
|
|
416
|
+
const metadata = await this.storage.getMetadata(`${prefix}-manifest`);
|
|
417
|
+
if (metadata && metadata.data) {
|
|
418
|
+
const data = metadata.data;
|
|
419
|
+
const manifestJson = JSON.stringify({
|
|
420
|
+
sstables: data.sstables || {},
|
|
421
|
+
lastCompaction: data.lastCompaction || Date.now(),
|
|
422
|
+
totalRelationships: data.totalRelationships || 0,
|
|
423
|
+
});
|
|
424
|
+
this.native.loadManifestJson(treeName, manifestJson);
|
|
425
|
+
await this.loadTreeSSTables(treeName, prefix);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
prodLog.debug(`GraphAdjacencyIndex: No existing manifest for ${treeName}, starting fresh`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async loadTreeSSTables(treeName, prefix) {
|
|
433
|
+
const ids = this.native.getManifestSstableIds(treeName);
|
|
434
|
+
let mmapLoaded = 0;
|
|
435
|
+
let binaryLoaded = 0;
|
|
436
|
+
let legacyLoaded = 0;
|
|
437
|
+
const loadPromises = ids.map(async (sstableId) => {
|
|
438
|
+
try {
|
|
439
|
+
const level = this.native.getManifestSstableLevel(treeName, sstableId);
|
|
440
|
+
if (level === null || level === undefined) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const key = this.blobKey(treeName, sstableId);
|
|
444
|
+
// Tier 1: Try mmap path (getBinaryBlobPath → loadSstableFromFile)
|
|
445
|
+
if (this.hasMmapPaths) {
|
|
446
|
+
const filePath = this.storage.getBinaryBlobPath(key);
|
|
447
|
+
if (filePath) {
|
|
448
|
+
try {
|
|
449
|
+
this.native.loadSstableFromFile(treeName, sstableId, level, filePath);
|
|
450
|
+
mmapLoaded++;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// File may not exist yet (first run), fall through
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Tier 2: Try binary blob (loadBinaryBlob → loadSstableFromBinary)
|
|
459
|
+
if (this.hasBinaryBlobs) {
|
|
460
|
+
const blobData = await this.storage.loadBinaryBlob(key);
|
|
461
|
+
if (blobData) {
|
|
462
|
+
this.native.loadSstableFromBinary(treeName, sstableId, level, blobData);
|
|
463
|
+
binaryLoaded++;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Tier 3: Legacy (base64 in JSON metadata)
|
|
468
|
+
const storageKey = `${prefix}-${sstableId}`;
|
|
469
|
+
const metadata = await this.storage.getMetadata(storageKey);
|
|
470
|
+
if (metadata && metadata.data) {
|
|
471
|
+
const data = metadata.data;
|
|
472
|
+
if (data.type === 'native-lsm-sstable' && data.data) {
|
|
473
|
+
const buffer = Buffer.from(data.data, 'base64');
|
|
474
|
+
this.native.loadSstable(treeName, sstableId, level, buffer);
|
|
475
|
+
legacyLoaded++;
|
|
476
|
+
}
|
|
477
|
+
else if (data.type === 'lsm-sstable' && data.data) {
|
|
478
|
+
prodLog.warn(`GraphAdjacencyIndex: Skipping legacy SSTable ${sstableId} (old TS format)`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
prodLog.warn(`GraphAdjacencyIndex: Failed to load SSTable ${sstableId} for ${treeName}`, error);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
await Promise.all(loadPromises);
|
|
487
|
+
const parts = [];
|
|
488
|
+
if (mmapLoaded > 0)
|
|
489
|
+
parts.push(`${mmapLoaded} mmap`);
|
|
490
|
+
if (binaryLoaded > 0)
|
|
491
|
+
parts.push(`${binaryLoaded} binary`);
|
|
492
|
+
if (legacyLoaded > 0)
|
|
493
|
+
parts.push(`${legacyLoaded} legacy`);
|
|
494
|
+
const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
495
|
+
prodLog.info(`GraphAdjacencyIndex: Loaded ${ids.length} SSTables for ${treeName}${detail}`);
|
|
496
|
+
}
|
|
497
|
+
async flushTree(treeName) {
|
|
498
|
+
if (this.native.memTableIsEmpty(treeName)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const prefix = TREE_PREFIXES[treeName];
|
|
502
|
+
if (!prefix)
|
|
503
|
+
return;
|
|
504
|
+
const startTime = Date.now();
|
|
505
|
+
try {
|
|
506
|
+
// Binary blob flush (adapter-controlled): Rust serializes → adapter writes
|
|
507
|
+
if (this.hasBinaryBlobs) {
|
|
508
|
+
const result = this.native.flushTreeToBinary(treeName);
|
|
509
|
+
if (!result)
|
|
510
|
+
return;
|
|
511
|
+
const key = this.blobKey(treeName, result.sstableId);
|
|
512
|
+
await this.storage.saveBinaryBlob(key, Buffer.from(result.data));
|
|
513
|
+
// If adapter supports mmap, upgrade InMemory → Mapped
|
|
514
|
+
if (this.hasMmapPaths) {
|
|
515
|
+
const filePath = this.storage.getBinaryBlobPath(key);
|
|
516
|
+
if (filePath) {
|
|
517
|
+
try {
|
|
518
|
+
this.native.registerMmapSstable(treeName, result.sstableId, result.level, filePath);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// Non-fatal: InMemory variant still works
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
await this.saveTreeManifest(treeName, prefix);
|
|
526
|
+
const elapsed = Date.now() - startTime;
|
|
527
|
+
prodLog.info(`GraphAdjacencyIndex: Flushed ${treeName} in ${elapsed}ms (${result.entryCount} entries, ${result.relationshipCount} relationships, binary blob)`);
|
|
528
|
+
if (this.native.needsCompaction(treeName, 0)) {
|
|
529
|
+
setImmediate(() => this.compactTree(treeName, 0));
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
// Legacy flush (base64 in JSON)
|
|
534
|
+
const result = this.native.flushTree(treeName);
|
|
535
|
+
if (!result) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
// Persist SSTable via storage adapter (base64 in JSON)
|
|
539
|
+
const storageKey = `${prefix}-${result.sstableId}`;
|
|
540
|
+
await this.storage.saveMetadata(storageKey, {
|
|
541
|
+
noun: 'thing',
|
|
542
|
+
data: {
|
|
543
|
+
type: 'native-lsm-sstable',
|
|
544
|
+
data: Buffer.from(result.data).toString('base64'),
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
// Save manifest
|
|
548
|
+
await this.saveTreeManifest(treeName, prefix);
|
|
549
|
+
const elapsed = Date.now() - startTime;
|
|
550
|
+
prodLog.info(`GraphAdjacencyIndex: Flushed ${treeName} in ${elapsed}ms (${result.entryCount} entries, ${result.relationshipCount} relationships)`);
|
|
551
|
+
// Check if compaction needed
|
|
552
|
+
if (this.native.needsCompaction(treeName, 0)) {
|
|
553
|
+
setImmediate(() => this.compactTree(treeName, 0));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
prodLog.error(`GraphAdjacencyIndex: Failed to flush ${treeName}`, error);
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async compactTree(treeName, level) {
|
|
562
|
+
if (this.isCompacting[treeName]) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
this.isCompacting[treeName] = true;
|
|
566
|
+
const prefix = TREE_PREFIXES[treeName];
|
|
567
|
+
if (!prefix)
|
|
568
|
+
return;
|
|
569
|
+
const startTime = Date.now();
|
|
570
|
+
try {
|
|
571
|
+
// Binary blob compaction (adapter-controlled)
|
|
572
|
+
if (this.hasBinaryBlobs) {
|
|
573
|
+
const result = this.native.compactTreeToBinary(treeName, level);
|
|
574
|
+
if (!result)
|
|
575
|
+
return;
|
|
576
|
+
prodLog.info(`GraphAdjacencyIndex: Compacting ${treeName} L${level} → L${result.newLevel} (binary blob)`);
|
|
577
|
+
// Save new merged SSTable via adapter
|
|
578
|
+
const newKey = this.blobKey(treeName, result.newSstableId);
|
|
579
|
+
await this.storage.saveBinaryBlob(newKey, Buffer.from(result.data));
|
|
580
|
+
// If adapter supports mmap, upgrade InMemory → Mapped
|
|
581
|
+
if (this.hasMmapPaths) {
|
|
582
|
+
const filePath = this.storage.getBinaryBlobPath(newKey);
|
|
583
|
+
if (filePath) {
|
|
584
|
+
try {
|
|
585
|
+
this.native.registerMmapSstable(treeName, result.newSstableId, result.newLevel, filePath);
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// Non-fatal: InMemory variant still works
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Delete old blobs (best-effort)
|
|
593
|
+
for (const oldId of result.oldIds) {
|
|
594
|
+
const oldBlobKey = this.blobKey(treeName, oldId);
|
|
595
|
+
this.storage.deleteBinaryBlob(oldBlobKey).catch(() => { });
|
|
596
|
+
}
|
|
597
|
+
await this.saveTreeManifest(treeName, prefix);
|
|
598
|
+
const elapsed = Date.now() - startTime;
|
|
599
|
+
prodLog.info(`GraphAdjacencyIndex: Compaction ${treeName} L${level} → L${result.newLevel} complete in ${elapsed}ms`);
|
|
600
|
+
if (result.newLevel < 6 && this.native.needsCompaction(treeName, result.newLevel)) {
|
|
601
|
+
setImmediate(() => this.compactTree(treeName, result.newLevel));
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
// Legacy compaction (base64 in JSON)
|
|
606
|
+
const result = this.native.compactTree(treeName, level);
|
|
607
|
+
if (!result) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
prodLog.info(`GraphAdjacencyIndex: Compacting ${treeName} L${level} → L${result.newLevel}`);
|
|
611
|
+
// Save new merged SSTable
|
|
612
|
+
const storageKey = `${prefix}-${result.newSstableId}`;
|
|
613
|
+
await this.storage.saveMetadata(storageKey, {
|
|
614
|
+
noun: 'thing',
|
|
615
|
+
data: {
|
|
616
|
+
type: 'native-lsm-sstable',
|
|
617
|
+
data: Buffer.from(result.newData).toString('base64'),
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
// Best-effort cleanup of old SSTables
|
|
621
|
+
for (const oldId of result.oldIds) {
|
|
622
|
+
const oldKey = `${prefix}-${oldId}`;
|
|
623
|
+
this.storage.saveMetadata(oldKey, {
|
|
624
|
+
noun: 'thing',
|
|
625
|
+
data: { type: 'deleted' },
|
|
626
|
+
}).catch(() => { });
|
|
627
|
+
}
|
|
628
|
+
await this.saveTreeManifest(treeName, prefix);
|
|
629
|
+
const elapsed = Date.now() - startTime;
|
|
630
|
+
prodLog.info(`GraphAdjacencyIndex: Compaction ${treeName} L${level} → L${result.newLevel} complete in ${elapsed}ms`);
|
|
631
|
+
// Check next level
|
|
632
|
+
if (result.newLevel < 6 && this.native.needsCompaction(treeName, result.newLevel)) {
|
|
633
|
+
setImmediate(() => this.compactTree(treeName, result.newLevel));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
prodLog.error(`GraphAdjacencyIndex: Compaction failed for ${treeName} L${level}`, error);
|
|
638
|
+
}
|
|
639
|
+
finally {
|
|
640
|
+
this.isCompacting[treeName] = false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async saveTreeManifest(treeName, prefix) {
|
|
644
|
+
try {
|
|
645
|
+
const manifestJson = this.native.getManifestJson(treeName);
|
|
646
|
+
const manifestData = JSON.parse(manifestJson);
|
|
647
|
+
await this.storage.saveMetadata(`${prefix}-manifest`, {
|
|
648
|
+
noun: 'thing',
|
|
649
|
+
data: manifestData,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
prodLog.error(`GraphAdjacencyIndex: Failed to save manifest for ${treeName}`, error);
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
startAutoFlush() {
|
|
658
|
+
this.flushTimer = setInterval(async () => {
|
|
659
|
+
await this.flush();
|
|
660
|
+
}, this.config.flushInterval);
|
|
661
|
+
}
|
|
662
|
+
calculateMemoryUsage() {
|
|
663
|
+
const stats = this.native.getStats();
|
|
664
|
+
let bytes = 0;
|
|
665
|
+
bytes += stats.sourceMemTableMemory;
|
|
666
|
+
bytes += stats.targetMemTableMemory;
|
|
667
|
+
bytes += stats.verbIdCount * 8;
|
|
668
|
+
return bytes;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
//# sourceMappingURL=NativeGraphAdjacencyIndex.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @soulcraft/cortex — Native Rust acceleration for Brainy
|
|
3
|
+
*
|
|
4
|
+
* When installed alongside @soulcraft/brainy, this package automatically
|
|
5
|
+
* provides native Rust implementations for:
|
|
6
|
+
* - SIMD distance calculations (cosine, euclidean, manhattan, dot product)
|
|
7
|
+
* - Metadata index (full query/mutation engine in Rust)
|
|
8
|
+
* - Graph adjacency index (4 LSM-trees + verb tracking)
|
|
9
|
+
* - Entity ID mapper (UUID ↔ integer bidirectional mapping)
|
|
10
|
+
* - Roaring bitmaps (CRoaring bindings)
|
|
11
|
+
* - Embedding engine (Candle ML — CPU, CUDA, Metal)
|
|
12
|
+
* - Msgpack encoding/decoding
|
|
13
|
+
* - MmapFileSystemStorage adapter (zero-copy SSTables)
|
|
14
|
+
*
|
|
15
|
+
* Usage: `npm install @soulcraft/cortex` — auto-detected, zero config.
|
|
16
|
+
*/
|
|
17
|
+
export { default } from './plugin.js';
|
|
18
|
+
export { loadNativeModule, isNativeAvailable } from './native/index.js';
|
|
19
|
+
export type { NativeBindings } from './native/types.js';
|
|
20
|
+
export { NativeEmbeddingEngine, nativeEmbeddingEngine, cosineSimilarity } from './native/NativeEmbeddingEngine.js';
|
|
21
|
+
export { RoaringBitmap32, RoaringBitmap32Iterator, roaringLibraryInitialize, roaringLibraryIsReady, SerializationFormat, DeserializationFormat } from './native/NativeRoaringBitmap32.js';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|