@soulcraft/brainy 3.20.1 → 3.20.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [3.20.2](https://github.com/soulcraftlabs/brainy/compare/v3.20.1...v3.20.2) (2025-09-30)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **vfs**: resolve VFS race conditions and decompression errors ([1a2661f](https://github.com/soulcraftlabs/brainy/commit/1a2661f))
10
+ - Fixes duplicate directory nodes caused by concurrent writes
11
+ - Fixes file read decompression errors caused by rawData compression state mismatch
12
+ - Adds mutex-based concurrency control for mkdir operations
13
+ - Adds explicit compression tracking for file reads
14
+
5
15
  ### [3.19.1](https://github.com/soulcraftlabs/brainy/compare/v3.19.0...v3.19.1) (2025-09-29)
6
16
 
7
17
  ## [3.19.0](https://github.com/soulcraftlabs/brainy/compare/v3.18.0...v3.19.0) (2025-09-29)
@@ -28,6 +28,7 @@ export declare class VirtualFileSystem implements IVirtualFileSystem {
28
28
  private statCache;
29
29
  private watchers;
30
30
  private backgroundTimer;
31
+ private mkdirLocks;
31
32
  constructor(brain?: Brainy);
32
33
  /**
33
34
  * Initialize the VFS
@@ -25,6 +25,8 @@ export class VirtualFileSystem {
25
25
  this.currentUser = 'system'; // Track current user for collaboration
26
26
  // Background task timer
27
27
  this.backgroundTimer = null;
28
+ // Mutex for preventing race conditions in directory creation
29
+ this.mkdirLocks = new Map();
28
30
  this.brain = brain || new Brainy();
29
31
  this.contentCache = new Map();
30
32
  this.statCache = new Map();
@@ -159,16 +161,20 @@ export class VirtualFileSystem {
159
161
  }
160
162
  // Get content based on storage type
161
163
  let content;
164
+ let isCompressed = false;
162
165
  if (!entity.metadata.storage || entity.metadata.storage.type === 'inline') {
163
166
  // Content stored in metadata for new files, or try entity data for compatibility
164
167
  if (entity.metadata.rawData) {
168
+ // rawData is ALWAYS stored uncompressed as base64
165
169
  content = Buffer.from(entity.metadata.rawData, 'base64');
170
+ isCompressed = false; // rawData is never compressed
166
171
  }
167
172
  else if (!entity.data) {
168
173
  content = Buffer.alloc(0);
169
174
  }
170
175
  else if (Buffer.isBuffer(entity.data)) {
171
176
  content = entity.data;
177
+ isCompressed = entity.metadata.storage?.compressed || false;
172
178
  }
173
179
  else if (typeof entity.data === 'string') {
174
180
  content = Buffer.from(entity.data);
@@ -180,16 +186,18 @@ export class VirtualFileSystem {
180
186
  else if (entity.metadata.storage.type === 'reference') {
181
187
  // Content stored in external storage
182
188
  content = await this.readExternalContent(entity.metadata.storage.key);
189
+ isCompressed = entity.metadata.storage.compressed || false;
183
190
  }
184
191
  else if (entity.metadata.storage.type === 'chunked') {
185
192
  // Content stored in chunks
186
193
  content = await this.readChunkedContent(entity.metadata.storage.chunks);
194
+ isCompressed = entity.metadata.storage.compressed || false;
187
195
  }
188
196
  else {
189
197
  throw new VFSError(VFSErrorCode.EIO, `Unknown storage type: ${entity.metadata.storage.type}`, path, 'readFile');
190
198
  }
191
- // Decompress if needed
192
- if (entity.metadata.storage?.compressed && options?.decompress !== false) {
199
+ // Decompress if needed (but NOT for rawData which is never compressed)
200
+ if (isCompressed && options?.decompress !== false) {
193
201
  content = await this.decompress(content);
194
202
  }
195
203
  // Update access time
@@ -317,7 +325,7 @@ export class VirtualFileSystem {
317
325
  type: this.getFileNounType(mimeType),
318
326
  metadata
319
327
  });
320
- // Create parent-child relationship
328
+ // Create parent-child relationship (no need to check for duplicates on new entities)
321
329
  await this.brain.relate({
322
330
  from: parentId,
323
331
  to: entity,
@@ -498,78 +506,106 @@ export class VirtualFileSystem {
498
506
  */
499
507
  async mkdir(path, options) {
500
508
  await this.ensureInitialized();
501
- // Check if already exists
502
- try {
503
- const existing = await this.pathResolver.resolve(path);
504
- const entity = await this.getEntityById(existing);
505
- if (entity.metadata.vfsType === 'directory') {
506
- if (!options?.recursive) {
507
- throw new VFSError(VFSErrorCode.EEXIST, `Directory exists: ${path}`, path, 'mkdir');
509
+ // Use mutex to prevent race conditions when creating the same directory concurrently
510
+ // If another call is already creating this directory, wait for it to complete
511
+ const existingLock = this.mkdirLocks.get(path);
512
+ if (existingLock) {
513
+ await existingLock;
514
+ // After waiting, check if directory now exists
515
+ try {
516
+ const existing = await this.pathResolver.resolve(path);
517
+ const entity = await this.getEntityById(existing);
518
+ if (entity.metadata.vfsType === 'directory') {
519
+ return; // Directory was created by the other call
508
520
  }
509
- return; // Already exists and recursive is true
510
521
  }
511
- else {
512
- // Path exists but it's not a directory
513
- throw new VFSError(VFSErrorCode.EEXIST, `File exists: ${path}`, path, 'mkdir');
514
- }
515
- }
516
- catch (err) {
517
- // Only proceed if it's a ENOENT error (path doesn't exist)
518
- if (err instanceof VFSError && err.code !== VFSErrorCode.ENOENT) {
519
- throw err; // Re-throw non-ENOENT errors
522
+ catch (err) {
523
+ // Still doesn't exist, proceed to create
520
524
  }
521
- // Doesn't exist, proceed to create
522
525
  }
523
- // Parse path
524
- const parentPath = this.getParentPath(path);
525
- const name = this.getBasename(path);
526
- // Ensure parent exists (recursive mkdir if needed)
527
- let parentId;
528
- if (parentPath === '/' || parentPath === null) {
529
- parentId = this.rootEntityId;
530
- }
531
- else if (options?.recursive) {
532
- parentId = await this.ensureDirectory(parentPath);
533
- }
534
- else {
526
+ // Create a lock promise for this path
527
+ let resolveLock;
528
+ const lockPromise = new Promise(resolve => { resolveLock = resolve; });
529
+ this.mkdirLocks.set(path, lockPromise);
530
+ try {
531
+ // Check if already exists
535
532
  try {
536
- parentId = await this.pathResolver.resolve(parentPath);
533
+ const existing = await this.pathResolver.resolve(path);
534
+ const entity = await this.getEntityById(existing);
535
+ if (entity.metadata.vfsType === 'directory') {
536
+ if (!options?.recursive) {
537
+ throw new VFSError(VFSErrorCode.EEXIST, `Directory exists: ${path}`, path, 'mkdir');
538
+ }
539
+ return; // Already exists and recursive is true
540
+ }
541
+ else {
542
+ // Path exists but it's not a directory
543
+ throw new VFSError(VFSErrorCode.EEXIST, `File exists: ${path}`, path, 'mkdir');
544
+ }
537
545
  }
538
546
  catch (err) {
539
- throw new VFSError(VFSErrorCode.ENOENT, `Parent directory not found: ${parentPath}`, path, 'mkdir');
547
+ // Only proceed if it's a ENOENT error (path doesn't exist)
548
+ if (err instanceof VFSError && err.code !== VFSErrorCode.ENOENT) {
549
+ throw err; // Re-throw non-ENOENT errors
550
+ }
551
+ // Doesn't exist, proceed to create
540
552
  }
541
- }
542
- // Create directory entity
543
- const metadata = {
544
- path,
545
- name,
546
- parent: parentId,
547
- vfsType: 'directory',
548
- size: 0,
549
- permissions: options?.mode || this.config.permissions?.defaultDirectory || 0o755,
550
- owner: 'user',
551
- group: 'users',
552
- accessed: Date.now(),
553
- modified: Date.now(),
554
- ...options?.metadata
555
- };
556
- const entity = await this.brain.add({
557
- data: path, // Directory path as string content
558
- type: NounType.Collection,
559
- metadata
560
- });
561
- // Create parent-child relationship
562
- if (parentId !== entity) { // Don't relate to self (root)
563
- await this.brain.relate({
564
- from: parentId,
565
- to: entity,
566
- type: VerbType.Contains
553
+ // Parse path
554
+ const parentPath = this.getParentPath(path);
555
+ const name = this.getBasename(path);
556
+ // Ensure parent exists (recursive mkdir if needed)
557
+ let parentId;
558
+ if (parentPath === '/' || parentPath === null) {
559
+ parentId = this.rootEntityId;
560
+ }
561
+ else if (options?.recursive) {
562
+ parentId = await this.ensureDirectory(parentPath);
563
+ }
564
+ else {
565
+ try {
566
+ parentId = await this.pathResolver.resolve(parentPath);
567
+ }
568
+ catch (err) {
569
+ throw new VFSError(VFSErrorCode.ENOENT, `Parent directory not found: ${parentPath}`, path, 'mkdir');
570
+ }
571
+ }
572
+ // Create directory entity
573
+ const metadata = {
574
+ path,
575
+ name,
576
+ parent: parentId,
577
+ vfsType: 'directory',
578
+ size: 0,
579
+ permissions: options?.mode || this.config.permissions?.defaultDirectory || 0o755,
580
+ owner: 'user',
581
+ group: 'users',
582
+ accessed: Date.now(),
583
+ modified: Date.now(),
584
+ ...options?.metadata
585
+ };
586
+ const entity = await this.brain.add({
587
+ data: path, // Directory path as string content
588
+ type: NounType.Collection,
589
+ metadata
567
590
  });
591
+ // Create parent-child relationship (no need to check for duplicates on new entities)
592
+ if (parentId !== entity) { // Don't relate to self (root)
593
+ await this.brain.relate({
594
+ from: parentId,
595
+ to: entity,
596
+ type: VerbType.Contains
597
+ });
598
+ }
599
+ // Update path resolver cache
600
+ await this.pathResolver.createPath(path, entity);
601
+ // Trigger watchers
602
+ this.triggerWatchers(path, 'rename');
603
+ }
604
+ finally {
605
+ // Release the lock
606
+ resolveLock();
607
+ this.mkdirLocks.delete(path);
568
608
  }
569
- // Update path resolver cache
570
- await this.pathResolver.createPath(path, entity);
571
- // Trigger watchers
572
- this.triggerWatchers(path, 'rename');
573
609
  }
574
610
  /**
575
611
  * Remove a directory
@@ -1964,17 +2000,20 @@ export class VirtualFileSystem {
1964
2000
  };
1965
2001
  let earliestModified = null;
1966
2002
  let latestModified = null;
1967
- const traverse = async (currentPath) => {
2003
+ const traverse = async (currentPath, isRoot = false) => {
1968
2004
  try {
1969
2005
  const entityId = await this.pathResolver.resolve(currentPath);
1970
2006
  const entity = await this.getEntityById(entityId);
1971
2007
  if (entity.metadata.vfsType === 'directory') {
1972
- stats.directoryCount++;
2008
+ // Don't count the root/starting directory itself
2009
+ if (!isRoot) {
2010
+ stats.directoryCount++;
2011
+ }
1973
2012
  // Traverse children
1974
2013
  const children = await this.readdir(currentPath);
1975
2014
  for (const child of children) {
1976
2015
  const childPath = currentPath === '/' ? `/${child}` : `${currentPath}/${child}`;
1977
- await traverse(childPath);
2016
+ await traverse(childPath, false);
1978
2017
  }
1979
2018
  }
1980
2019
  else if (entity.metadata.vfsType === 'file') {
@@ -2005,7 +2044,7 @@ export class VirtualFileSystem {
2005
2044
  // Skip if path doesn't exist
2006
2045
  }
2007
2046
  };
2008
- await traverse(path);
2047
+ await traverse(path, true);
2009
2048
  // Calculate averages
2010
2049
  if (stats.fileCount > 0) {
2011
2050
  stats.averageFileSize = Math.round(stats.totalSize / stats.fileCount);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "3.20.1",
3
+ "version": "3.20.2",
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",