@@ -85,6 +85,20 @@ export class MetadataManager implements IMetadataService {
8585 // Dependency tracking: "type:name" -> dependencies
8686 private dependencies = new Map < string , MetadataDependency [ ] > ( ) ;
8787
88+ // Short-lived cache for list() results. Built primarily to break the
89+ // deadlock that occurs when security/permission middleware calls
90+ // `list('permission')` from inside a user-initiated DB transaction: the
91+ // DatabaseLoader's `engine.find('sys_metadata', ...)` would then try to
92+ // acquire a fresh knex connection while the transaction is still holding
93+ // SQLite's single connection — knex waits the full `acquireConnectionTimeout`
94+ // (60s) before returning []. The cache absorbs the repeated lookups so the
95+ // loader is only hit once per TTL window.
96+ //
97+ // Invalidated on every `register()` / `unregister()` to keep CRUD writes
98+ // visible to subsequent reads.
99+ private listCache = new Map < string , { ts : number ; items : unknown [ ] } > ( ) ;
100+ private static readonly LIST_CACHE_TTL_MS = 30_000 ;
101+
88102 // Realtime service for event publishing
89103 private realtimeService ?: IRealtimeService ;
90104
@@ -232,6 +246,7 @@ export class MetadataManager implements IMetadataService {
232246 this . registry . set ( type , new Map ( ) ) ;
233247 }
234248 this . registry . get ( type ) ! . set ( name , data ) ;
249+ this . invalidateListCache ( type ) ;
235250
236251 // Persist only to database-backed loaders that declare write capability.
237252 // FilesystemLoader is read-only at runtime — writing to it can crash in
@@ -285,6 +300,15 @@ export class MetadataManager implements IMetadataService {
285300 * List all metadata items of a given type
286301 */
287302 async list ( type : string ) : Promise < unknown [ ] > {
303+ // Short-TTL cache: see field comment on `listCache`. Skip when called
304+ // from tests / hot reloads that rely on always-fresh reads — we only
305+ // cache positive (non-empty) hits or repeated hits with a stable miss
306+ // signature.
307+ const cached = this . listCache . get ( type ) ;
308+ if ( cached && Date . now ( ) - cached . ts < MetadataManager . LIST_CACHE_TTL_MS ) {
309+ return cached . items ;
310+ }
311+
288312 const items = new Map < string , unknown > ( ) ;
289313
290314 // From in-memory registry
@@ -310,7 +334,17 @@ export class MetadataManager implements IMetadataService {
310334 }
311335 }
312336
313- return Array . from ( items . values ( ) ) ;
337+ const result = Array . from ( items . values ( ) ) ;
338+ this . cacheListResult ( type , result ) ;
339+ return result ;
340+ }
341+ private cacheListResult ( type : string , items : unknown [ ] ) : void {
342+ this . listCache . set ( type , { ts : Date . now ( ) , items } ) ;
343+ }
344+
345+ /** Internal helper: drop the cached `list()` result for a type. */
346+ private invalidateListCache ( type : string ) : void {
347+ this . listCache . delete ( type ) ;
314348 }
315349
316350 /**
@@ -326,6 +360,7 @@ export class MetadataManager implements IMetadataService {
326360 this . registry . delete ( type ) ;
327361 }
328362 }
363+ this . invalidateListCache ( type ) ;
329364
330365 // Delete only from database-backed loaders that declare write capability
331366 for ( const loader of this . loaders . values ( ) ) {
0 commit comments