Problem
RegisterRoute pushes every registered route into the module-level array routesList (declared line 651), and nothing anywhere ever removes an entry — the only operations on it are .push (line 671) and .map (line 689 in the public getRegisteredRoutes). This is out of step with the rest of the registration lifecycle: the framework's RegisteringProxy tracks registrations per module and prunes them on unload via unregisterModule (interface-core proxies.js:152), and the API implementation's registeredRoutes Map is deleted on unregister (implementations/api/index.js:52-54). AntelopeJS is a dynamic/hot-reload modular framework, so whenever a controller-defining module is reloaded or unloaded, its decorators re-run RegisterRoute with fresh incrementing ids, appending brand-new entries to routesList while the old ones are never released.
Location: src/index.ts:671
Severity: 🟢 low
Category: unbounded-growth
Performance impact
Each stale entry is a shallow copy of a RouteHandler that references the controller callback, its proto (prototype), and the computed parameters/properties closures, so every leaked entry pins the entire previous version of that module's controller graph in memory, preventing GC. In long-running dev sessions (frequent hot reloads) or any production setup that dynamically swaps modules, routesList and retained memory grow without bound, and getRegisteredRoutes() returns an ever-growing array of duplicated/stale routes while allocating a larger array on every call.
Suggested fix
Eliminate the parallel routesList and derive getRegisteredRoutes() directly from the already-pruned routesProxy.registered map, which is the single source of truth. That map stores { module, args } keyed by the string id, and RegisterRoute passes the enriched handler as the sole arg (routesProxy.register(id.toString(), enriched)), so getRegisteredRoutes can iterate routesProxy.registered and, for each [id, { args: [handler] }], emit { id, location, method, callbackName: handler.callback.name || 'anonymous', parameters, properties, priority, module }. This makes stale entries disappear automatically on unregister/unregisterModule and removes the growing-array allocation. If routesList must be retained for its numeric ids, instead wire routesProxy.onUnregister((id) => routesList.delete(id)) (switching routesList to a Map keyed by the same string id) so entries are removed on the same lifecycle event that already cleans up the real routes.
Verification notes
Confirmed in src/index.ts: routesList (line 651) is only ever pushed to (671) and mapped over (689) — no removal path — whereas its sibling routesProxy (a RegisteringProxy) is properly pruned on teardown, since interface-core's ModuleDestroyed handler calls proxy.unregisterModule (proxies.js:167) which the api implementation uses to delete from registeredRoutes and drop the real handler (implementations/api/index.js:52-54). Because ReloadModule re-runs the controller decorators (ProcessCallback line 731 -> RegisterRoute with a fresh nextId), every reload appends new RouteHandler shallow copies that retain callback/proto/parameters, pinning the prior module version's controller graph while old entries are never freed, and getRegisteredRoutes() returns an ever-growing duplicated/stale array. It's a real, correctly-diagnosed unbounded leak, but calibrated low because it only accrues on the reload/unload path (not the request hot path), the per-reload footprint is small, and the only observed consumer is a debug utility.
Found by an automated AI performance review (multi-agent analysis + independent adversarial verification of each finding).
Problem
RegisterRoute pushes every registered route into the module-level array
routesList(declared line 651), and nothing anywhere ever removes an entry — the only operations on it are.push(line 671) and.map(line 689 in the public getRegisteredRoutes). This is out of step with the rest of the registration lifecycle: the framework's RegisteringProxy tracks registrations per module and prunes them on unload via unregisterModule (interface-core proxies.js:152), and the API implementation'sregisteredRoutesMap is deleted on unregister (implementations/api/index.js:52-54). AntelopeJS is a dynamic/hot-reload modular framework, so whenever a controller-defining module is reloaded or unloaded, its decorators re-run RegisterRoute with fresh incrementing ids, appending brand-new entries to routesList while the old ones are never released.Location:
src/index.ts:671Severity: 🟢 low
Category:
unbounded-growthPerformance impact
Each stale entry is a shallow copy of a RouteHandler that references the controller callback, its
proto(prototype), and the computed parameters/properties closures, so every leaked entry pins the entire previous version of that module's controller graph in memory, preventing GC. In long-running dev sessions (frequent hot reloads) or any production setup that dynamically swaps modules, routesList and retained memory grow without bound, and getRegisteredRoutes() returns an ever-growing array of duplicated/stale routes while allocating a larger array on every call.Suggested fix
Eliminate the parallel
routesListand derive getRegisteredRoutes() directly from the already-prunedroutesProxy.registeredmap, which is the single source of truth. That map stores{ module, args }keyed by the string id, and RegisterRoute passes the enriched handler as the sole arg (routesProxy.register(id.toString(), enriched)), so getRegisteredRoutes can iterateroutesProxy.registeredand, for each[id, { args: [handler] }], emit{ id, location, method, callbackName: handler.callback.name || 'anonymous', parameters, properties, priority, module }. This makes stale entries disappear automatically on unregister/unregisterModule and removes the growing-array allocation. IfroutesListmust be retained for its numeric ids, instead wireroutesProxy.onUnregister((id) => routesList.delete(id))(switching routesList to a Map keyed by the same string id) so entries are removed on the same lifecycle event that already cleans up the real routes.Verification notes
Confirmed in src/index.ts:
routesList(line 651) is only ever pushed to (671) and mapped over (689) — no removal path — whereas its siblingroutesProxy(a RegisteringProxy) is properly pruned on teardown, since interface-core's ModuleDestroyed handler callsproxy.unregisterModule(proxies.js:167) which the api implementation uses to delete fromregisteredRoutesand drop the real handler (implementations/api/index.js:52-54). Because ReloadModule re-runs the controller decorators (ProcessCallback line 731 -> RegisterRoute with a fresh nextId), every reload appends new RouteHandler shallow copies that retaincallback/proto/parameters, pinning the prior module version's controller graph while old entries are never freed, and getRegisteredRoutes() returns an ever-growing duplicated/stale array. It's a real, correctly-diagnosed unbounded leak, but calibrated low because it only accrues on the reload/unload path (not the request hot path), the per-reload footprint is small, and the only observed consumer is a debug utility.Found by an automated AI performance review (multi-agent analysis + independent adversarial verification of each finding).