@@ -33,7 +33,7 @@ export type WatchCallback = (event: MetadataWatchEvent) => void | Promise<void>;
3333 * Main metadata manager class
3434 */
3535export class MetadataManager {
36- private loader : MetadataLoader ;
36+ private loaders : Map < string , MetadataLoader > = new Map ( ) ;
3737 private serializers : Map < MetadataFormat , MetadataSerializer > ;
3838 private logger : Logger ;
3939 private watcher ?: FSWatcher ;
@@ -59,149 +59,142 @@ export class MetadataManager {
5959 this . serializers . set ( 'javascript' , new TypeScriptSerializer ( 'javascript' ) ) ;
6060 }
6161
62- // Initialize loader
62+ // Initialize Default Filesystem Loader
63+ // This is treated as the "Primary" source for now
6364 const rootDir = config . rootDir || process . cwd ( ) ;
64- this . loader = new FilesystemLoader ( rootDir , this . serializers , this . logger ) ;
65+ this . registerLoader ( new FilesystemLoader ( rootDir , this . serializers , this . logger ) ) ;
6566
6667 // Start watching if enabled
6768 if ( config . watch ) {
6869 this . startWatching ( ) ;
6970 }
7071 }
7172
73+ /**
74+ * Register a new metadata loader (data source)
75+ */
76+ registerLoader ( loader : MetadataLoader ) {
77+ this . loaders . set ( loader . contract . name , loader ) ;
78+ this . logger . info ( `Registered metadata loader: ${ loader . contract . name } (${ loader . contract . protocol } )` ) ;
79+ }
80+
7281 /**
7382 * Load a single metadata item
83+ * Iterates through registered loaders until found
7484 */
7585 async load < T = any > (
7686 type : string ,
7787 name : string ,
7888 options ?: MetadataLoadOptions
7989 ) : Promise < T | null > {
80- const result = await this . loader . load ( type , name , options ) ;
81- return result . data ;
90+ // Priority: Database > Filesystem (Implementation-dependent)
91+ // For now, we just iterate.
92+ for ( const loader of this . loaders . values ( ) ) {
93+ try {
94+ const result = await loader . load ( type , name , options ) ;
95+ if ( result . data ) {
96+ return result . data ;
97+ }
98+ } catch ( e ) {
99+ this . logger . warn ( `Loader ${ loader . contract . name } failed to load ${ type } :${ name } ` , { error : e } ) ;
100+ }
101+ }
102+ return null ;
82103 }
83104
84105 /**
85106 * Load multiple metadata items
107+ * Aggregates results from all loaders
86108 */
87109 async loadMany < T = any > (
88110 type : string ,
89111 options ?: MetadataLoadOptions
90112 ) : Promise < T [ ] > {
91- return this . loader . loadMany < T > ( type , options ) ;
113+ const results : T [ ] = [ ] ;
114+ const seen = new Set < string > ( ) ; // De-duplication key needed? For now, simple aggregation
115+
116+ for ( const loader of this . loaders . values ( ) ) {
117+ try {
118+ const items = await loader . loadMany < T > ( type , options ) ;
119+ for ( const item of items ) {
120+ // TODO: Deduplicate based on 'name' if property exists
121+ results . push ( item ) ;
122+ }
123+ } catch ( e ) {
124+ this . logger . warn ( `Loader ${ loader . contract . name } failed to loadMany ${ type } ` , { error : e } ) ;
125+ }
126+ }
127+ return results ;
92128 }
93129
94130 /**
95131 * Save metadata to disk
96132 */
133+ /**
134+ * Save metadata item
135+ */
97136 async save < T = any > (
98137 type : string ,
99138 name : string ,
100139 data : T ,
101140 options ?: MetadataSaveOptions
102141 ) : Promise < MetadataSaveResult > {
103- const startTime = Date . now ( ) ;
104- const {
105- format = 'typescript' ,
106- prettify = true ,
107- indent = 2 ,
108- sortKeys = false ,
109- backup = false ,
110- overwrite = true ,
111- atomic = true ,
112- path : customPath ,
113- } = options || { } ;
114-
115- try {
116- // Get serializer
117- const serializer = this . serializers . get ( format ) ;
118- if ( ! serializer ) {
119- throw new Error ( `No serializer found for format: ${ format } ` ) ;
142+ const targetLoader = ( options as any ) ?. loader ;
143+
144+ // Find suitable loader
145+ let loader : MetadataLoader | undefined ;
146+
147+ if ( targetLoader ) {
148+ loader = this . loaders . get ( targetLoader ) ;
149+ if ( ! loader ) {
150+ throw new Error ( `Loader not found: ${ targetLoader } ` ) ;
120151 }
121-
122- // Determine file path
123- const typeDir = path . join ( this . config . rootDir || process . cwd ( ) , type ) ;
124- const fileName = `${ name } ${ serializer . getExtension ( ) } ` ;
125- const filePath = customPath || path . join ( typeDir , fileName ) ;
126-
127- // Check if file exists
128- if ( ! overwrite ) {
129- try {
130- await fs . access ( filePath ) ;
131- throw new Error ( `File already exists: ${ filePath } ` ) ;
132- } catch ( error ) {
133- // File doesn't exist, continue
134- if ( ( error as NodeJS . ErrnoException ) . code !== 'ENOENT' ) {
135- throw error ;
152+ } else {
153+ // Default to 'filesystem' or first writable
154+ loader = this . loaders . get ( 'filesystem' ) ;
155+ if ( ! loader ) {
156+ for ( const l of this . loaders . values ( ) ) {
157+ if ( l . save ) {
158+ loader = l ;
159+ break ;
136160 }
137161 }
138162 }
163+ }
139164
140- // Create directory if it doesn't exist
141- await fs . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
142-
143- // Create backup if requested
144- let backupPath : string | undefined ;
145- if ( backup ) {
146- try {
147- await fs . access ( filePath ) ;
148- backupPath = `${ filePath } .bak` ;
149- await fs . copyFile ( filePath , backupPath ) ;
150- } catch {
151- // File doesn't exist, no backup needed
152- }
153- }
154-
155- // Serialize data
156- const content = serializer . serialize ( data , {
157- prettify,
158- indent,
159- sortKeys,
160- } ) ;
161-
162- // Write to disk (atomic or direct)
163- if ( atomic ) {
164- const tempPath = `${ filePath } .tmp` ;
165- await fs . writeFile ( tempPath , content , 'utf-8' ) ;
166- await fs . rename ( tempPath , filePath ) ;
167- } else {
168- await fs . writeFile ( filePath , content , 'utf-8' ) ;
169- }
165+ if ( ! loader ) {
166+ throw new Error ( `No loader available for saving type: ${ type } ` ) ;
167+ }
170168
171- // Get stats
172- const stats = await fs . stat ( filePath ) ;
173- const etag = this . generateETag ( content ) ;
174-
175- return {
176- success : true ,
177- path : filePath ,
178- etag,
179- size : stats . size ,
180- saveTime : Date . now ( ) - startTime ,
181- backupPath,
182- } ;
183- } catch ( error ) {
184- this . logger . error ( 'Failed to save metadata' , undefined , {
185- type,
186- name,
187- error : error instanceof Error ? error . message : String ( error ) ,
188- } ) ;
189- throw error ;
169+ if ( ! loader . save ) {
170+ throw new Error ( `Loader '${ loader . contract ?. name } ' does not support saving` ) ;
190171 }
172+
173+ return loader . save ( type , name , data , options ) ;
191174 }
192175
193176 /**
194177 * Check if metadata item exists
195178 */
196179 async exists ( type : string , name : string ) : Promise < boolean > {
197- return this . loader . exists ( type , name ) ;
180+ for ( const loader of this . loaders . values ( ) ) {
181+ if ( await loader . exists ( type , name ) ) {
182+ return true ;
183+ }
184+ }
185+ return false ;
198186 }
199187
200188 /**
201189 * List all items of a type
202190 */
203191 async list ( type : string ) : Promise < string [ ] > {
204- return this . loader . list ( type ) ;
192+ const items = new Set < string > ( ) ;
193+ for ( const loader of this . loaders . values ( ) ) {
194+ const result = await loader . list ( type ) ;
195+ result . forEach ( item => items . add ( item ) ) ;
196+ }
197+ return Array . from ( items ) ;
205198 }
206199
207200 /**
0 commit comments