@@ -248,6 +248,56 @@ export const compileOpenApiSpec = (
248248 return yield * compileOpenApiDocument ( doc ) ;
249249 } ) ;
250250
251+ /**
252+ * Bounded reuse of compiled specs for the serve-side fallback paths (health
253+ * checks, candidate listing, tools/invoke legacy fallbacks). These paths run
254+ * per request, and the UI auto-fires the health endpoints on every page mount,
255+ * so recompiling a multi-MB spec on each call grows the dev server heap until
256+ * the process hits the V8 limit. The stored spec is content-addressed: the
257+ * config's `specHash` keys the blob the text is loaded from, so the hash is a
258+ * free, already-computed identity (same hash means byte-identical text means
259+ * identical compile output), and a spec update writes a new hash so stale
260+ * entries age out naturally.
261+ *
262+ * An insertion-ordered Map serves as a tiny LRU: hits are re-inserted to
263+ * refresh recency and the oldest entry is evicted past capacity. Capacity 4
264+ * covers the handful of integrations a page's probes touch at once while
265+ * keeping worst-case retention to a few compiled documents.
266+ */
267+ const COMPILED_SPEC_CACHE_CAPACITY = 4 ;
268+ const compiledSpecCache = new Map < string , CompiledOpenApiSpec > ( ) ;
269+
270+ /** Test-only reset so unit tests can observe cold-cache behavior. */
271+ export const clearCompiledOpenApiSpecCache = ( ) : void => {
272+ compiledSpecCache . clear ( ) ;
273+ } ;
274+
275+ /** Compile through the module-level LRU. `specHash` is the content hash the
276+ * spec text was loaded by (`OpenApiIntegrationConfig.specHash`). A missing
277+ * hash (legacy config shape) bypasses the cache rather than paying to hash
278+ * multi-MB text on the request path. */
279+ export const compileOpenApiSpecCached = (
280+ specHash : string | null | undefined ,
281+ specText : string ,
282+ ) : Effect . Effect < CompiledOpenApiSpec , OpenApiParseError | OpenApiExtractionError > =>
283+ Effect . gen ( function * ( ) {
284+ if ( specHash == null ) return yield * compileOpenApiSpec ( specText ) ;
285+ const hit = compiledSpecCache . get ( specHash ) ;
286+ if ( hit !== undefined ) {
287+ compiledSpecCache . delete ( specHash ) ;
288+ compiledSpecCache . set ( specHash , hit ) ;
289+ return hit ;
290+ }
291+ const compiled = yield * compileOpenApiSpec ( specText ) ;
292+ compiledSpecCache . set ( specHash , compiled ) ;
293+ while ( compiledSpecCache . size > COMPILED_SPEC_CACHE_CAPACITY ) {
294+ const oldest = compiledSpecCache . keys ( ) . next ( ) . value ;
295+ if ( oldest === undefined ) break ;
296+ compiledSpecCache . delete ( oldest ) ;
297+ }
298+ return compiled ;
299+ } ) ;
300+
251301export const openApiToolDefsFromCompiled = ( compiled : CompiledOpenApiSpec ) : readonly ToolDef [ ] =>
252302 compiled . definitions . map ( ( def ) : ToolDef => {
253303 const returnsFile = Option . match ( def . operation . responseBody , {
@@ -543,7 +593,7 @@ export const resolveOpenApiBackedTools = ({
543593 }
544594 const specText = yield * loadOpenApiSpecText ( storage , openApiConfig ) ;
545595 if ( specText == null ) return { tools : [ ] , definitions : { } } ;
546- const compiled = yield * compileOpenApiSpec ( specText ) . pipe (
596+ const compiled = yield * compileOpenApiSpecCached ( openApiConfig . specHash , specText ) . pipe (
547597 Effect . catch ( ( ) => Effect . succeed ( null ) ) ,
548598 ) ;
549599 if ( ! compiled ) return { tools : [ ] , definitions : { } } ;
@@ -577,7 +627,9 @@ export const invokeOpenApiBackedTool = (input: {
577627 const compiled =
578628 specText == null
579629 ? null
580- : yield * compileOpenApiSpec ( specText ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( null ) ) ) ;
630+ : yield * compileOpenApiSpecCached ( config . specHash , specText ) . pipe (
631+ Effect . catch ( ( ) => Effect . succeed ( null ) ) ,
632+ ) ;
581633 binding = compiled
582634 ? openApiStoredOperationsFromCompiled ( integration , compiled ) . find (
583635 ( op ) => op . toolName === input . toolRow . name ,
@@ -716,7 +768,7 @@ const resolveHealthCheckBinding = (
716768 Effect . catch ( ( ) => Effect . succeed ( null ) ) ,
717769 ) ;
718770 if ( specText == null ) return undefined ;
719- const compiled = yield * compileOpenApiSpec ( specText ) . pipe (
771+ const compiled = yield * compileOpenApiSpecCached ( config . specHash , specText ) . pipe (
720772 Effect . catch ( ( ) => Effect . succeed ( null ) ) ,
721773 ) ;
722774 if ( ! compiled ) return undefined ;
@@ -878,7 +930,9 @@ export const listHealthCheckCandidatesOpenApi = (input: {
878930 const compiled =
879931 specText == null
880932 ? null
881- : yield * compileOpenApiSpec ( specText ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( null ) ) ) ;
933+ : yield * compileOpenApiSpecCached ( config . specHash , specText ) . pipe (
934+ Effect . catch ( ( ) => Effect . succeed ( null ) ) ,
935+ ) ;
882936 if ( compiled ) {
883937 for ( const def of compiled . definitions ) {
884938 const summary =
0 commit comments