@soulcraft/brainy 3.8.3 → 3.9.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.
@@ -240,6 +240,49 @@ export class S3CompatibleStorage extends BaseStorage {
240
240
  throw new Error(`Failed to initialize ${this.serviceType} storage: ${error}`);
241
241
  }
242
242
  }
243
+ /**
244
+ * Set distributed components for multi-node coordination
245
+ * Zero-config: Automatically optimizes based on components provided
246
+ */
247
+ setDistributedComponents(components) {
248
+ this.coordinator = components.coordinator;
249
+ this.shardManager = components.shardManager;
250
+ this.cacheSync = components.cacheSync;
251
+ this.readWriteSeparation = components.readWriteSeparation;
252
+ // Auto-configure based on what's available
253
+ if (this.shardManager) {
254
+ console.log(`🎯 S3 Storage: Sharding enabled with ${this.shardManager.config?.shardCount || 64} shards`);
255
+ }
256
+ if (this.coordinator) {
257
+ console.log(`🤝 S3 Storage: Distributed coordination active (node: ${this.coordinator.nodeId})`);
258
+ }
259
+ if (this.cacheSync) {
260
+ console.log('🔄 S3 Storage: Cache synchronization enabled');
261
+ }
262
+ if (this.readWriteSeparation) {
263
+ console.log(`📖 S3 Storage: Read/write separation with ${this.readWriteSeparation.config?.replicationFactor || 3}x replication`);
264
+ }
265
+ }
266
+ /**
267
+ * Get the S3 key for a noun, using sharding if available
268
+ */
269
+ getNounKey(id) {
270
+ if (this.shardManager) {
271
+ const shardId = this.shardManager.getShardForKey(id);
272
+ return `shards/${shardId}/${this.nounPrefix}${id}.json`;
273
+ }
274
+ return `${this.nounPrefix}${id}.json`;
275
+ }
276
+ /**
277
+ * Get the S3 key for a verb, using sharding if available
278
+ */
279
+ getVerbKey(id) {
280
+ if (this.shardManager) {
281
+ const shardId = this.shardManager.getShardForKey(id);
282
+ return `shards/${shardId}/${this.verbPrefix}${id}.json`;
283
+ }
284
+ return `${this.verbPrefix}${id}.json`;
285
+ }
243
286
  /**
244
287
  * Override base class method to detect S3-specific throttling errors
245
288
  */
@@ -668,7 +711,8 @@ export class S3CompatibleStorage extends BaseStorage {
668
711
  };
669
712
  // Import the PutObjectCommand only when needed
670
713
  const { PutObjectCommand } = await import('@aws-sdk/client-s3');
671
- const key = `${this.nounPrefix}${node.id}.json`;
714
+ // Use sharding if available
715
+ const key = this.getNounKey(node.id);
672
716
  const body = JSON.stringify(serializableNode, null, 2);
673
717
  this.logger.trace(`Saving to key: ${key}`);
674
718
  // Save the node to S3-compatible storage
@@ -1013,10 +1057,10 @@ export class S3CompatibleStorage extends BaseStorage {
1013
1057
  };
1014
1058
  // Import the PutObjectCommand only when needed
1015
1059
  const { PutObjectCommand } = await import('@aws-sdk/client-s3');
1016
- // Save the edge to S3-compatible storage
1060
+ // Save the edge to S3-compatible storage using sharding if available
1017
1061
  await this.s3Client.send(new PutObjectCommand({
1018
1062
  Bucket: this.bucketName,
1019
- Key: `${this.verbPrefix}${edge.id}.json`,
1063
+ Key: this.getVerbKey(edge.id),
1020
1064
  Body: JSON.stringify(serializableEdge, null, 2),
1021
1065
  ContentType: 'application/json'
1022
1066
  }));
@@ -2660,5 +2704,87 @@ export class S3CompatibleStorage extends BaseStorage {
2660
2704
  nextCursor: result.nextCursor
2661
2705
  };
2662
2706
  }
2707
+ /**
2708
+ * Initialize counts from S3 storage
2709
+ */
2710
+ async initializeCounts() {
2711
+ const countsKey = `${this.systemPrefix}counts.json`;
2712
+ try {
2713
+ const { GetObjectCommand } = await import('@aws-sdk/client-s3');
2714
+ // Try to load existing counts
2715
+ const response = await this.s3Client.send(new GetObjectCommand({
2716
+ Bucket: this.bucketName,
2717
+ Key: countsKey
2718
+ }));
2719
+ if (response.Body) {
2720
+ const data = await response.Body.transformToString();
2721
+ const counts = JSON.parse(data);
2722
+ // Restore counts from S3
2723
+ this.entityCounts = new Map(Object.entries(counts.entityCounts || {}));
2724
+ this.verbCounts = new Map(Object.entries(counts.verbCounts || {}));
2725
+ this.totalNounCount = counts.totalNounCount || 0;
2726
+ this.totalVerbCount = counts.totalVerbCount || 0;
2727
+ }
2728
+ }
2729
+ catch (error) {
2730
+ if (error.name !== 'NoSuchKey') {
2731
+ console.error('Error loading counts from S3:', error);
2732
+ }
2733
+ // If counts don't exist, initialize by scanning (one-time operation)
2734
+ await this.initializeCountsFromScan();
2735
+ }
2736
+ }
2737
+ /**
2738
+ * Initialize counts by scanning S3 (fallback for missing counts file)
2739
+ */
2740
+ async initializeCountsFromScan() {
2741
+ // This is expensive but only happens once for legacy data
2742
+ // In production, counts are maintained incrementally
2743
+ try {
2744
+ const { ListObjectsV2Command } = await import('@aws-sdk/client-s3');
2745
+ // Count nouns
2746
+ const nounResponse = await this.s3Client.send(new ListObjectsV2Command({
2747
+ Bucket: this.bucketName,
2748
+ Prefix: this.nounPrefix
2749
+ }));
2750
+ this.totalNounCount = nounResponse.Contents?.filter((obj) => obj.Key?.endsWith('.json')).length || 0;
2751
+ // Count verbs
2752
+ const verbResponse = await this.s3Client.send(new ListObjectsV2Command({
2753
+ Bucket: this.bucketName,
2754
+ Prefix: this.verbPrefix
2755
+ }));
2756
+ this.totalVerbCount = verbResponse.Contents?.filter((obj) => obj.Key?.endsWith('.json')).length || 0;
2757
+ // Save initial counts
2758
+ await this.persistCounts();
2759
+ }
2760
+ catch (error) {
2761
+ console.error('Error initializing counts from S3 scan:', error);
2762
+ }
2763
+ }
2764
+ /**
2765
+ * Persist counts to S3 storage
2766
+ */
2767
+ async persistCounts() {
2768
+ const countsKey = `${this.systemPrefix}counts.json`;
2769
+ try {
2770
+ const { PutObjectCommand } = await import('@aws-sdk/client-s3');
2771
+ const counts = {
2772
+ entityCounts: Object.fromEntries(this.entityCounts),
2773
+ verbCounts: Object.fromEntries(this.verbCounts),
2774
+ totalNounCount: this.totalNounCount,
2775
+ totalVerbCount: this.totalVerbCount,
2776
+ lastUpdated: new Date().toISOString()
2777
+ };
2778
+ await this.s3Client.send(new PutObjectCommand({
2779
+ Bucket: this.bucketName,
2780
+ Key: countsKey,
2781
+ Body: JSON.stringify(counts),
2782
+ ContentType: 'application/json'
2783
+ }));
2784
+ }
2785
+ catch (error) {
2786
+ console.error('Error persisting counts to S3:', error);
2787
+ }
2788
+ }
2663
2789
  }
2664
2790
  //# sourceMappingURL=s3CompatibleStorage.js.map
@@ -337,14 +337,15 @@ export class BaseStorage extends BaseStorageAdapter {
337
337
  }
338
338
  // Check if the adapter has a paginated method for getting nouns
339
339
  if (typeof this.getNounsWithPagination === 'function') {
340
- // Use the adapter's paginated method
340
+ // Use the adapter's paginated method - pass offset directly to adapter
341
341
  const result = await this.getNounsWithPagination({
342
342
  limit,
343
+ offset, // Let the adapter handle offset for O(1) operation
343
344
  cursor,
344
345
  filter: options?.filter
345
346
  });
346
- // Apply offset if needed (some adapters might not support offset)
347
- const items = result.items.slice(offset);
347
+ // Don't slice here - the adapter should handle offset efficiently
348
+ const items = result.items;
348
349
  // CRITICAL SAFETY CHECK: Prevent infinite loops
349
350
  // If we have no items but hasMore is true, force hasMore to false
350
351
  // This prevents pagination bugs from causing infinite loops
@@ -6,7 +6,6 @@ import { HNSWNoun, Vector } from '../coreTypes.js';
6
6
  declare enum CompressionType {
7
7
  NONE = "none",
8
8
  GZIP = "gzip",
9
- BROTLI = "brotli",
10
9
  QUANTIZATION = "quantization",
11
10
  HYBRID = "hybrid"
12
11
  }
@@ -74,14 +73,6 @@ export declare class ReadOnlyOptimizations {
74
73
  * GZIP decompression
75
74
  */
76
75
  private gzipDecompress;
77
- /**
78
- * Brotli compression (placeholder - similar to GZIP)
79
- */
80
- private brotliCompress;
81
- /**
82
- * Brotli decompression (placeholder)
83
- */
84
- private brotliDecompress;
85
76
  /**
86
77
  * Create prebuilt index segments for faster loading
87
78
  */
@@ -7,9 +7,9 @@ var CompressionType;
7
7
  (function (CompressionType) {
8
8
  CompressionType["NONE"] = "none";
9
9
  CompressionType["GZIP"] = "gzip";
10
- CompressionType["BROTLI"] = "brotli";
11
10
  CompressionType["QUANTIZATION"] = "quantization";
12
11
  CompressionType["HYBRID"] = "hybrid";
12
+ // BROTLI removed - was not actually implemented
13
13
  })(CompressionType || (CompressionType = {}));
14
14
  // Vector quantization methods
15
15
  var QuantizationType;
@@ -67,10 +67,7 @@ export class ReadOnlyOptimizations {
67
67
  const gzipBuffer = new Float32Array(vector).buffer;
68
68
  compressedData = await this.gzipCompress(gzipBuffer.slice(0));
69
69
  break;
70
- case CompressionType.BROTLI:
71
- const brotliBuffer = new Float32Array(vector).buffer;
72
- compressedData = await this.brotliCompress(brotliBuffer.slice(0));
73
- break;
70
+ // Brotli removed - was not implemented
74
71
  case CompressionType.HYBRID:
75
72
  // First quantize, then compress
76
73
  const quantized = await this.quantizeVector(vector, segmentId);
@@ -99,9 +96,7 @@ export class ReadOnlyOptimizations {
99
96
  case CompressionType.GZIP:
100
97
  const gzipDecompressed = await this.gzipDecompress(compressedData);
101
98
  return Array.from(new Float32Array(gzipDecompressed));
102
- case CompressionType.BROTLI:
103
- const brotliDecompressed = await this.brotliDecompress(compressedData);
104
- return Array.from(new Float32Array(brotliDecompressed));
99
+ // Brotli removed - was not implemented
105
100
  case CompressionType.HYBRID:
106
101
  const gzipStage = await this.gzipDecompress(compressedData);
107
102
  return this.dequantizeVector(gzipStage, segmentId, originalDimension);
@@ -219,21 +214,7 @@ export class ReadOnlyOptimizations {
219
214
  return compressedData;
220
215
  }
221
216
  }
222
- /**
223
- * Brotli compression (placeholder - similar to GZIP)
224
- */
225
- async brotliCompress(data) {
226
- // Would implement Brotli compression here
227
- console.warn('Brotli compression not implemented, falling back to GZIP');
228
- return this.gzipCompress(data);
229
- }
230
- /**
231
- * Brotli decompression (placeholder)
232
- */
233
- async brotliDecompress(compressedData) {
234
- console.warn('Brotli decompression not implemented, falling back to GZIP');
235
- return this.gzipDecompress(compressedData);
236
- }
217
+ // Brotli methods removed - were not implemented
237
218
  /**
238
219
  * Create prebuilt index segments for faster loading
239
220
  */
@@ -277,8 +258,7 @@ export class ReadOnlyOptimizations {
277
258
  switch (this.config.compression.metadataCompression) {
278
259
  case CompressionType.GZIP:
279
260
  return this.gzipCompress(data.buffer.slice(0));
280
- case CompressionType.BROTLI:
281
- return this.brotliCompress(data.buffer.slice(0));
261
+ // Brotli removed - was not implemented
282
262
  default:
283
263
  return data.buffer.slice(0);
284
264
  }
@@ -329,9 +309,7 @@ export class ReadOnlyOptimizations {
329
309
  case CompressionType.GZIP:
330
310
  decompressed = await this.gzipDecompress(compressedData);
331
311
  break;
332
- case CompressionType.BROTLI:
333
- decompressed = await this.brotliDecompress(compressedData);
334
- break;
312
+ // Brotli removed - was not implemented
335
313
  default:
336
314
  decompressed = compressedData;
337
315
  break;
@@ -269,10 +269,25 @@ export interface BrainyConfig {
269
269
  ttl?: number;
270
270
  };
271
271
  augmentations?: Record<string, any>;
272
+ distributed?: {
273
+ enabled: boolean;
274
+ nodeId?: string;
275
+ nodes?: string[];
276
+ coordinatorUrl?: string;
277
+ shardCount?: number;
278
+ replicationFactor?: number;
279
+ consensus?: 'raft' | 'none';
280
+ transport?: 'tcp' | 'http' | 'udp';
281
+ };
272
282
  warmup?: boolean;
273
283
  realtime?: boolean;
274
284
  multiTenancy?: boolean;
275
285
  telemetry?: boolean;
286
+ disableAutoRebuild?: boolean;
287
+ disableMetrics?: boolean;
288
+ disableAutoOptimize?: boolean;
289
+ batchWrites?: boolean;
290
+ maxConcurrentOperations?: number;
276
291
  verbose?: boolean;
277
292
  silent?: boolean;
278
293
  }
@@ -68,6 +68,11 @@ export declare class MetadataIndexManager {
68
68
  private totalEntitiesByType;
69
69
  private unifiedCache;
70
70
  constructor(storage: StorageAdapter, config?: MetadataIndexConfig);
71
+ /**
72
+ * Lazy load entity counts from storage statistics (O(1) operation)
73
+ * This avoids rebuilding the entire index on startup
74
+ */
75
+ private lazyLoadCounts;
71
76
  /**
72
77
  * Get index key for field and value
73
78
  */
@@ -45,6 +45,30 @@ export class MetadataIndexManager {
45
45
  });
46
46
  // Get global unified cache for coordinated memory management
47
47
  this.unifiedCache = getGlobalCache();
48
+ // Lazy load counts from storage statistics on first access
49
+ this.lazyLoadCounts();
50
+ }
51
+ /**
52
+ * Lazy load entity counts from storage statistics (O(1) operation)
53
+ * This avoids rebuilding the entire index on startup
54
+ */
55
+ async lazyLoadCounts() {
56
+ try {
57
+ // Get statistics from storage (should be O(1) with our FileSystemStorage improvements)
58
+ const stats = await this.storage.getStatistics();
59
+ if (stats && stats.nounCount) {
60
+ // Populate entity counts from storage statistics
61
+ for (const [type, count] of Object.entries(stats.nounCount)) {
62
+ if (typeof count === 'number' && count > 0) {
63
+ this.totalEntitiesByType.set(type, count);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ catch (error) {
69
+ // Silently fail - counts will be populated as entities are added
70
+ // This maintains zero-configuration principle
71
+ }
48
72
  }
49
73
  /**
50
74
  * Get index key for field and value
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Universal Mutex Implementation for Thread-Safe Operations
3
+ * Provides consistent locking across all storage adapters
4
+ * Critical for preventing race conditions in count operations
5
+ */
6
+ export interface MutexInterface {
7
+ acquire(key: string, timeout?: number): Promise<() => void>;
8
+ runExclusive<T>(key: string, fn: () => Promise<T>, timeout?: number): Promise<T>;
9
+ isLocked(key: string): boolean;
10
+ }
11
+ /**
12
+ * In-memory mutex for single-process scenarios
13
+ * Used by MemoryStorage and as fallback for other adapters
14
+ */
15
+ export declare class InMemoryMutex implements MutexInterface {
16
+ private locks;
17
+ acquire(key: string, timeout?: number): Promise<() => void>;
18
+ private release;
19
+ runExclusive<T>(key: string, fn: () => Promise<T>, timeout?: number): Promise<T>;
20
+ isLocked(key: string): boolean;
21
+ }
22
+ /**
23
+ * File-based mutex for multi-process scenarios (Node.js)
24
+ * Uses atomic file operations to prevent TOCTOU races
25
+ */
26
+ export declare class FileMutex implements MutexInterface {
27
+ private fs;
28
+ private path;
29
+ private lockDir;
30
+ private processLocks;
31
+ private lockTimers;
32
+ constructor(lockDir: string);
33
+ acquire(key: string, timeout?: number): Promise<() => void>;
34
+ private release;
35
+ runExclusive<T>(key: string, fn: () => Promise<T>, timeout?: number): Promise<T>;
36
+ isLocked(key: string): boolean;
37
+ /**
38
+ * Clean up all locks held by this process
39
+ */
40
+ cleanup(): Promise<void>;
41
+ }
42
+ /**
43
+ * Factory to create appropriate mutex for the environment
44
+ */
45
+ export declare function createMutex(options?: {
46
+ type?: 'memory' | 'file';
47
+ lockDir?: string;
48
+ }): MutexInterface;
49
+ export declare function getGlobalMutex(): MutexInterface;
50
+ /**
51
+ * Cleanup function for graceful shutdown
52
+ */
53
+ export declare function cleanupMutexes(): Promise<void>;
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Universal Mutex Implementation for Thread-Safe Operations
3
+ * Provides consistent locking across all storage adapters
4
+ * Critical for preventing race conditions in count operations
5
+ */
6
+ /**
7
+ * In-memory mutex for single-process scenarios
8
+ * Used by MemoryStorage and as fallback for other adapters
9
+ */
10
+ export class InMemoryMutex {
11
+ constructor() {
12
+ this.locks = new Map();
13
+ }
14
+ async acquire(key, timeout = 30000) {
15
+ if (!this.locks.has(key)) {
16
+ this.locks.set(key, { queue: [], locked: false });
17
+ }
18
+ const lock = this.locks.get(key);
19
+ if (!lock.locked) {
20
+ lock.locked = true;
21
+ return () => this.release(key);
22
+ }
23
+ // Wait in queue
24
+ return new Promise((resolve, reject) => {
25
+ const timer = setTimeout(() => {
26
+ const index = lock.queue.indexOf(resolver);
27
+ if (index !== -1) {
28
+ lock.queue.splice(index, 1);
29
+ }
30
+ reject(new Error(`Mutex timeout for key: ${key}`));
31
+ }, timeout);
32
+ const resolver = () => {
33
+ clearTimeout(timer);
34
+ lock.locked = true;
35
+ resolve(() => this.release(key));
36
+ };
37
+ lock.queue.push(resolver);
38
+ });
39
+ }
40
+ release(key) {
41
+ const lock = this.locks.get(key);
42
+ if (!lock)
43
+ return;
44
+ if (lock.queue.length > 0) {
45
+ const next = lock.queue.shift();
46
+ next();
47
+ }
48
+ else {
49
+ lock.locked = false;
50
+ // Clean up if no waiters
51
+ if (lock.queue.length === 0) {
52
+ this.locks.delete(key);
53
+ }
54
+ }
55
+ }
56
+ async runExclusive(key, fn, timeout) {
57
+ const release = await this.acquire(key, timeout);
58
+ try {
59
+ return await fn();
60
+ }
61
+ finally {
62
+ release();
63
+ }
64
+ }
65
+ isLocked(key) {
66
+ return this.locks.get(key)?.locked || false;
67
+ }
68
+ }
69
+ /**
70
+ * File-based mutex for multi-process scenarios (Node.js)
71
+ * Uses atomic file operations to prevent TOCTOU races
72
+ */
73
+ export class FileMutex {
74
+ constructor(lockDir) {
75
+ this.processLocks = new Map();
76
+ this.lockTimers = new Map();
77
+ this.lockDir = lockDir;
78
+ // Lazy load Node.js modules
79
+ if (typeof window === 'undefined') {
80
+ this.fs = require('fs');
81
+ this.path = require('path');
82
+ }
83
+ }
84
+ async acquire(key, timeout = 30000) {
85
+ if (!this.fs || !this.path) {
86
+ throw new Error('FileMutex is only available in Node.js environments');
87
+ }
88
+ const lockFile = this.path.join(this.lockDir, `${key}.lock`);
89
+ const lockId = `${Date.now()}_${Math.random()}_${process.pid}`;
90
+ const startTime = Date.now();
91
+ // Ensure lock directory exists
92
+ await this.fs.promises.mkdir(this.lockDir, { recursive: true });
93
+ while (Date.now() - startTime < timeout) {
94
+ try {
95
+ // Atomic lock creation using 'wx' flag
96
+ await this.fs.promises.writeFile(lockFile, JSON.stringify({
97
+ lockId,
98
+ pid: process.pid,
99
+ timestamp: Date.now(),
100
+ expiresAt: Date.now() + timeout
101
+ }), { flag: 'wx' } // Write exclusive - fails if exists
102
+ );
103
+ // Successfully acquired lock
104
+ const release = () => this.release(key, lockFile, lockId);
105
+ this.processLocks.set(key, release);
106
+ // Auto-release on timeout
107
+ const timer = setTimeout(() => {
108
+ release();
109
+ }, timeout);
110
+ this.lockTimers.set(key, timer);
111
+ return release;
112
+ }
113
+ catch (error) {
114
+ if (error.code === 'EEXIST') {
115
+ // Lock exists - check if expired
116
+ try {
117
+ const data = await this.fs.promises.readFile(lockFile, 'utf-8');
118
+ const lock = JSON.parse(data);
119
+ if (lock.expiresAt < Date.now()) {
120
+ // Expired - try to remove
121
+ try {
122
+ await this.fs.promises.unlink(lockFile);
123
+ continue; // Retry acquisition
124
+ }
125
+ catch (unlinkError) {
126
+ if (unlinkError.code !== 'ENOENT') {
127
+ // Someone else removed it, continue
128
+ continue;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ catch {
134
+ // Can't read lock file, assume it's valid
135
+ }
136
+ // Wait before retry
137
+ await new Promise(resolve => setTimeout(resolve, 50));
138
+ }
139
+ else {
140
+ throw error;
141
+ }
142
+ }
143
+ }
144
+ throw new Error(`Failed to acquire mutex for key: ${key} after ${timeout}ms`);
145
+ }
146
+ async release(key, lockFile, lockId) {
147
+ // Clear timer
148
+ const timer = this.lockTimers.get(key);
149
+ if (timer) {
150
+ clearTimeout(timer);
151
+ this.lockTimers.delete(key);
152
+ }
153
+ // Remove from process locks
154
+ this.processLocks.delete(key);
155
+ try {
156
+ // Verify we own the lock before releasing
157
+ const data = await this.fs.promises.readFile(lockFile, 'utf-8');
158
+ const lock = JSON.parse(data);
159
+ if (lock.lockId === lockId) {
160
+ await this.fs.promises.unlink(lockFile);
161
+ }
162
+ }
163
+ catch {
164
+ // Lock already released or doesn't exist
165
+ }
166
+ }
167
+ async runExclusive(key, fn, timeout) {
168
+ const release = await this.acquire(key, timeout);
169
+ try {
170
+ return await fn();
171
+ }
172
+ finally {
173
+ release();
174
+ }
175
+ }
176
+ isLocked(key) {
177
+ return this.processLocks.has(key);
178
+ }
179
+ /**
180
+ * Clean up all locks held by this process
181
+ */
182
+ async cleanup() {
183
+ // Clear all timers
184
+ for (const timer of this.lockTimers.values()) {
185
+ clearTimeout(timer);
186
+ }
187
+ this.lockTimers.clear();
188
+ // Release all locks
189
+ const releases = Array.from(this.processLocks.values());
190
+ await Promise.all(releases.map(release => release()));
191
+ this.processLocks.clear();
192
+ }
193
+ }
194
+ /**
195
+ * Factory to create appropriate mutex for the environment
196
+ */
197
+ export function createMutex(options) {
198
+ const type = options?.type || (typeof window === 'undefined' ? 'file' : 'memory');
199
+ if (type === 'file' && typeof window === 'undefined') {
200
+ const lockDir = options?.lockDir || '.brainy/locks';
201
+ return new FileMutex(lockDir);
202
+ }
203
+ return new InMemoryMutex();
204
+ }
205
+ // Global mutex instance for count operations
206
+ let globalMutex = null;
207
+ export function getGlobalMutex() {
208
+ if (!globalMutex) {
209
+ globalMutex = createMutex();
210
+ }
211
+ return globalMutex;
212
+ }
213
+ /**
214
+ * Cleanup function for graceful shutdown
215
+ */
216
+ export async function cleanupMutexes() {
217
+ if (globalMutex && 'cleanup' in globalMutex) {
218
+ await globalMutex.cleanup();
219
+ }
220
+ }
221
+ //# sourceMappingURL=mutex.js.map
@@ -130,11 +130,24 @@ export function validateFindParams(params) {
130
130
  export function validateAddParams(params) {
131
131
  // Universal truth: must have data or vector
132
132
  if (!params.data && !params.vector) {
133
- throw new Error('must provide either data or vector');
133
+ throw new Error(`Invalid add() parameters: Missing required field 'data'\n` +
134
+ `\nReceived: ${JSON.stringify({
135
+ type: params.type,
136
+ hasMetadata: !!params.metadata,
137
+ hasId: !!params.id
138
+ }, null, 2)}\n` +
139
+ `\nExpected one of:\n` +
140
+ ` { data: 'text to store', type?: 'note', metadata?: {...} }\n` +
141
+ ` { vector: [0.1, 0.2, ...], type?: 'embedding', metadata?: {...} }\n` +
142
+ `\nExamples:\n` +
143
+ ` await brain.add({ data: 'Machine learning is AI', type: 'concept' })\n` +
144
+ ` await brain.add({ data: { title: 'Doc', content: '...' }, type: 'document' })`);
134
145
  }
135
146
  // Validate noun type
136
147
  if (!Object.values(NounType).includes(params.type)) {
137
- throw new Error(`invalid NounType: ${params.type}`);
148
+ throw new Error(`Invalid NounType: '${params.type}'\n` +
149
+ `\nValid types: ${Object.values(NounType).join(', ')}\n` +
150
+ `\nExample: await brain.add({ data: 'text', type: NounType.Note })`);
138
151
  }
139
152
  // Validate vector dimensions if provided
140
153
  if (params.vector) {
@@ -182,8 +195,11 @@ export function validateRelateParams(params) {
182
195
  if (params.from === params.to) {
183
196
  throw new Error('cannot create self-referential relationship');
184
197
  }
185
- // Validate verb type
186
- if (!Object.values(VerbType).includes(params.type)) {
198
+ // Validate verb type - default to RelatedTo if not specified
199
+ if (params.type === undefined) {
200
+ params.type = VerbType.RelatedTo;
201
+ }
202
+ else if (!Object.values(VerbType).includes(params.type)) {
187
203
  throw new Error(`invalid VerbType: ${params.type}`);
188
204
  }
189
205
  // Universal truth: weight must be 0-1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "3.8.3",
3
+ "version": "3.9.1",
4
4
  "description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",