2025-09-26 10:17:59 -07:00
/ * *
* VFS Tree Utilities
* Provides safe tree operations that prevent common recursion issues
* /
import { VFSEntity , VFSDirent } from './types.js'
export interface TreeNode {
name : string
path : string
type : 'file' | 'directory'
entityId? : string
children? : TreeNode [ ]
metadata? : any
}
export interface TreeOptions {
maxDepth? : number
includeHidden? : boolean
filter ? : ( node : VFSEntity ) = > boolean
sort ? : 'name' | 'modified' | 'size'
expandAll? : boolean
}
/ * *
* Tree utility functions for VFS
* These functions ensure proper tree structure without recursion issues
* /
export class VFSTreeUtils {
/ * *
* Build a safe tree structure from VFS entities
* Guarantees no directory appears as its own child
* /
static buildTree (
entities : VFSEntity [ ] ,
rootPath : string = '/' ,
options : TreeOptions = { }
) : TreeNode {
const pathToEntity = new Map < string , VFSEntity > ( )
const pathToNode = new Map < string , TreeNode > ( )
// First pass: index all entities by path
for ( const entity of entities ) {
const path = entity . metadata . path
// Critical: Skip if entity IS the root we're building from
if ( path === rootPath ) {
continue
}
pathToEntity . set ( path , entity )
}
// Create root node
const rootNode : TreeNode = {
name : rootPath === '/' ? 'root' : rootPath . split ( '/' ) . pop ( ) ! ,
path : rootPath ,
type : 'directory' ,
children : [ ]
}
pathToNode . set ( rootPath , rootNode )
// Second pass: build tree structure
const sortedPaths = Array . from ( pathToEntity . keys ( ) ) . sort ( )
for ( const path of sortedPaths ) {
const entity = pathToEntity . get ( path ) !
// Apply filter if provided
if ( options . filter && ! options . filter ( entity ) ) {
continue
}
// Skip hidden files if requested
if ( ! options . includeHidden && entity . metadata . name . startsWith ( '.' ) ) {
continue
}
// Create node for this entity
const node : TreeNode = {
name : entity.metadata.name ,
path : entity.metadata.path ,
2025-09-26 10:19:37 -07:00
type : entity . metadata . vfsType === 'directory' ? 'directory' : 'file' ,
2025-09-26 10:17:59 -07:00
entityId : entity.id ,
metadata : entity.metadata
}
if ( entity . metadata . vfsType === 'directory' ) {
node . children = [ ]
}
pathToNode . set ( path , node )
// Find parent and attach
const parentPath = this . getParentPath ( path )
const parentNode = pathToNode . get ( parentPath )
if ( parentNode && parentNode . children ) {
parentNode . children . push ( node )
}
}
// Sort children if requested
if ( options . sort ) {
this . sortTreeNodes ( rootNode , options . sort )
}
// Apply depth limit if specified
if ( options . maxDepth !== undefined ) {
this . limitDepth ( rootNode , options . maxDepth )
}
return rootNode
}
/ * *
* Get direct children only - guaranteed no self - inclusion
* /
static getDirectChildren (
entities : VFSEntity [ ] ,
parentPath : string
) : VFSEntity [ ] {
const children : VFSEntity [ ] = [ ]
const parentDepth = parentPath === '/' ? 0 : parentPath.split ( '/' ) . length - 1
for ( const entity of entities ) {
const path = entity . metadata . path
// Critical check 1: Skip if this IS the parent
if ( path === parentPath ) {
continue
}
// Check if entity is a direct child
if ( path . startsWith ( parentPath ) ) {
const relativePath = parentPath === '/'
? path . substring ( 1 )
: path . substring ( parentPath . length + 1 )
// Direct child has no additional slashes
if ( ! relativePath . includes ( '/' ) ) {
children . push ( entity )
}
}
}
return children
}
/ * *
* Get all descendants ( recursive children )
* /
static getDescendants (
entities : VFSEntity [ ] ,
ancestorPath : string ,
includeAncestor : boolean = false
) : VFSEntity [ ] {
const descendants : VFSEntity [ ] = [ ]
for ( const entity of entities ) {
const path = entity . metadata . path
// Include ancestor only if explicitly requested
if ( path === ancestorPath ) {
if ( includeAncestor ) {
descendants . push ( entity )
}
continue
}
// Check if entity is under ancestor path
const prefix = ancestorPath === '/' ? '/' : ancestorPath + '/'
if ( path . startsWith ( prefix ) ) {
descendants . push ( entity )
}
}
return descendants
}
/ * *
* Flatten a tree structure back to a list
* /
static flattenTree ( node : TreeNode ) : TreeNode [ ] {
const result : TreeNode [ ] = [ node ]
if ( node . children ) {
for ( const child of node . children ) {
result . push ( . . . this . flattenTree ( child ) )
}
}
return result
}
/ * *
* Find a node in the tree by path
* /
static findNode ( root : TreeNode , targetPath : string ) : TreeNode | null {
if ( root . path === targetPath ) {
return root
}
if ( root . children ) {
for ( const child of root . children ) {
const found = this . findNode ( child , targetPath )
if ( found ) return found
}
}
return null
}
/ * *
* Calculate tree statistics
* /
static getTreeStats ( node : TreeNode ) : {
totalNodes : number
files : number
directories : number
maxDepth : number
totalSize? : number
} {
let stats = {
totalNodes : 0 ,
files : 0 ,
directories : 0 ,
maxDepth : 0 ,
totalSize : 0
}
function traverse ( n : TreeNode , depth : number ) {
stats . totalNodes ++
stats . maxDepth = Math . max ( stats . maxDepth , depth )
if ( n . type === 'file' ) {
stats . files ++
if ( n . metadata ? . size ) {
stats . totalSize += n . metadata . size
}
} else {
stats . directories ++
}
if ( n . children ) {
for ( const child of n . children ) {
traverse ( child , depth + 1 )
}
}
}
traverse ( node , 0 )
return stats
}
// Helper methods
private static getParentPath ( path : string ) : string {
if ( path === '/' ) return '/'
const parts = path . split ( '/' )
parts . pop ( )
return parts . length === 1 ? '/' : parts . join ( '/' )
}
private static sortTreeNodes ( node : TreeNode , sortBy : 'name' | 'modified' | 'size' ) : void {
if ( ! node . children ) return
node . children . sort ( ( a , b ) = > {
// Directories first, then files
if ( a . type !== b . type ) {
return a . type === 'directory' ? - 1 : 1
}
switch ( sortBy ) {
case 'name' :
return a . name . localeCompare ( b . name )
case 'modified' :
const aTime = a . metadata ? . modified || 0
const bTime = b . metadata ? . modified || 0
return bTime - aTime
case 'size' :
const aSize = a . metadata ? . size || 0
const bSize = b . metadata ? . size || 0
return bSize - aSize
default :
return 0
}
} )
// Recursively sort children
for ( const child of node . children ) {
this . sortTreeNodes ( child , sortBy )
}
}
private static limitDepth ( node : TreeNode , maxDepth : number , currentDepth : number = 0 ) : void {
if ( currentDepth >= maxDepth ) {
delete node . children
return
}
if ( node . children ) {
for ( const child of node . children ) {
this . limitDepth ( child , maxDepth , currentDepth + 1 )
}
}
}
/ * *
* Validate tree structure - ensures no recursion
* /
static validateTree ( node : TreeNode , visited : Set < string > = new Set ( ) ) : {
valid : boolean
errors : string [ ]
} {
const errors : string [ ] = [ ]
// Check for cycles
if ( visited . has ( node . path ) ) {
errors . push ( ` Cycle detected at path: ${ node . path } ` )
return { valid : false , errors }
}
visited . add ( node . path )
// Check children
if ( node . children ) {
const childPaths = new Set < string > ( )
for ( const child of node . children ) {
// Check for duplicate children
if ( childPaths . has ( child . path ) ) {
errors . push ( ` Duplicate child path: ${ child . path } ` )
}
childPaths . add ( child . path )
// Check child is not parent
if ( child . path === node . path ) {
errors . push ( ` Directory contains itself: ${ node . path } ` )
}
// Check child is actually under parent
if ( node . path !== '/' && ! child . path . startsWith ( node . path + '/' ) ) {
errors . push ( ` Child ${ child . path } not under parent ${ node . path } ` )
}
// Recursively validate children
const childValidation = this . validateTree ( child , new Set ( visited ) )
errors . push ( . . . childValidation . errors )
}
}
return {
valid : errors.length === 0 ,
errors
}
}
}