@@ -17,12 +17,27 @@ let localToken = process.env.SIMDECK_LOCAL_TOKEN || providerToken;
1717const registerIntervalMs = Number (
1818 process . env . SIMDECK_PROVIDER_REGISTER_INTERVAL_MS || 15000 ,
1919) ;
20+ const maxConcurrentRequests = Math . max (
21+ 1 ,
22+ Number ( process . env . SIMDECK_PROVIDER_MAX_CONCURRENT_REQUESTS || 8 ) ,
23+ ) ;
24+ const proxyTimeoutMs = Math . max (
25+ 1000 ,
26+ Number ( process . env . SIMDECK_PROVIDER_PROXY_TIMEOUT_MS || 25000 ) ,
27+ ) ;
28+ const simulatorListCacheTtlMs = Math . max (
29+ 0 ,
30+ Number ( process . env . SIMDECK_PROVIDER_SIMULATORS_CACHE_MS || 5000 ) ,
31+ ) ;
2032const providerId =
2133 process . env . SIMDECK_STUDIO_PROVIDER_ID || stableLocalProviderId ( ) ;
2234
2335let stopped = false ;
2436let lastRegisterAt = 0 ;
2537let registered = false ;
38+ const activeRequests = new Set ( ) ;
39+ const responseCache = new Map ( ) ;
40+ const inFlightCache = new Map ( ) ;
2641
2742if ( isMainModule ( ) ) {
2843 for ( const signal of [ "SIGINT" , "SIGTERM" , "SIGHUP" ] ) {
@@ -47,12 +62,15 @@ if (isMainModule()) {
4762 localToken = providerToken ;
4863 }
4964
50- console . log ( `[simdeck-provider-bridge] ${ publicUrl } ` ) ;
51-
5265 await registerProvider ( ) ;
66+ console . log ( `[simdeck-provider-bridge] ${ publicUrl } ` ) ;
5367
5468 while ( ! stopped ) {
5569 try {
70+ if ( activeRequests . size >= maxConcurrentRequests ) {
71+ await Promise . race ( [ ...activeRequests , sleep ( 50 ) ] ) ;
72+ continue ;
73+ }
5674 if ( Date . now ( ) - lastRegisterAt > registerIntervalMs ) {
5775 await registerProvider ( ) ;
5876 }
@@ -67,7 +85,7 @@ if (isMainModule()) {
6785 await sleep ( 250 ) ;
6886 continue ;
6987 }
70- await handleRequest ( next . request ) ;
88+ runProviderRequest ( next . request ) ;
7189 } catch ( error ) {
7290 console . error (
7391 `[simdeck-provider-bridge] ${ error instanceof Error ? error . message : String ( error ) } ` ,
@@ -76,6 +94,9 @@ if (isMainModule()) {
7694 }
7795 }
7896 } finally {
97+ if ( activeRequests . size > 0 ) {
98+ await Promise . allSettled ( activeRequests ) ;
99+ }
79100 if ( registered ) {
80101 await markProviderExpired ( ) ;
81102 }
@@ -158,42 +179,10 @@ async function localProviderMetadata() {
158179
159180async function handleRequest ( request ) {
160181 try {
161- const target = new URL ( request . path , `${ localUrl } /` ) ;
162- if ( ! target . searchParams . has ( "simdeckToken" ) ) {
163- target . searchParams . set ( "simdeckToken" , localToken ) ;
164- }
165- const headers = new Headers ( request . headers || { } ) ;
166- headers . set ( "x-simdeck-token" , localToken ) ;
167- headers . delete ( "host" ) ;
168- headers . delete ( "content-length" ) ;
169- const response = await fetch ( target , {
170- body : request . bodyBase64
171- ? Buffer . from ( request . bodyBase64 , "base64" )
172- : undefined ,
173- headers,
174- method : request . method ,
175- } ) ;
176- const responseHeaders = { } ;
177- for ( const [ name , value ] of response . headers . entries ( ) ) {
178- const lower = name . toLowerCase ( ) ;
179- if (
180- lower === "connection" ||
181- lower === "content-encoding" ||
182- lower === "content-length" ||
183- lower === "transfer-encoding"
184- ) {
185- continue ;
186- }
187- responseHeaders [ name ] = value ;
188- }
189- const responseBodyBase64 = Buffer . from (
190- await response . arrayBuffer ( ) ,
191- ) . toString ( "base64" ) ;
182+ const responsePayload = await cachedProxyResponse ( request ) ;
192183 await complete ( {
193184 requestId : request . id ,
194- responseStatus : response . status ,
195- responseHeaders,
196- responseBodyBase64,
185+ ...responsePayload ,
197186 } ) ;
198187 } catch ( error ) {
199188 await complete ( {
@@ -203,11 +192,106 @@ async function handleRequest(request) {
203192 }
204193}
205194
195+ function runProviderRequest ( request ) {
196+ const task = handleRequest ( request ) . catch ( ( error ) => {
197+ console . error (
198+ `[simdeck-provider-bridge] request ${ request . id } failed: ${ error instanceof Error ? error . message : String ( error ) } ` ,
199+ ) ;
200+ } ) ;
201+ activeRequests . add ( task ) ;
202+ task . finally ( ( ) => {
203+ activeRequests . delete ( task ) ;
204+ } ) ;
205+ }
206+
207+ async function cachedProxyResponse ( request ) {
208+ const cacheKey = cacheKeyForRequest ( request ) ;
209+ if ( ! cacheKey || simulatorListCacheTtlMs <= 0 ) {
210+ return proxyLocalRequest ( request ) ;
211+ }
212+
213+ const cached = responseCache . get ( cacheKey ) ;
214+ if ( cached && Date . now ( ) - cached . updatedAt <= simulatorListCacheTtlMs ) {
215+ return cached . payload ;
216+ }
217+
218+ const pending = inFlightCache . get ( cacheKey ) ;
219+ if ( pending ) {
220+ return pending ;
221+ }
222+
223+ const pendingRequest = proxyLocalRequest ( request )
224+ . then ( ( payload ) => {
225+ if ( payload . responseStatus >= 200 && payload . responseStatus < 300 ) {
226+ responseCache . set ( cacheKey , { payload, updatedAt : Date . now ( ) } ) ;
227+ }
228+ return payload ;
229+ } )
230+ . finally ( ( ) => {
231+ inFlightCache . delete ( cacheKey ) ;
232+ } ) ;
233+ inFlightCache . set ( cacheKey , pendingRequest ) ;
234+ return pendingRequest ;
235+ }
236+
237+ function cacheKeyForRequest ( request ) {
238+ if ( request . method !== "GET" ) {
239+ return "" ;
240+ }
241+ const target = new URL ( request . path , `${ localUrl } /` ) ;
242+ target . searchParams . delete ( "simdeckToken" ) ;
243+ if ( target . pathname !== "/api/simulators" ) {
244+ return "" ;
245+ }
246+ return `${ target . pathname } ?${ target . searchParams . toString ( ) } ` ;
247+ }
248+
249+ async function proxyLocalRequest ( request ) {
250+ const target = new URL ( request . path , `${ localUrl } /` ) ;
251+ if ( ! target . searchParams . has ( "simdeckToken" ) ) {
252+ target . searchParams . set ( "simdeckToken" , localToken ) ;
253+ }
254+ const headers = new Headers ( request . headers || { } ) ;
255+ headers . set ( "x-simdeck-token" , localToken ) ;
256+ headers . delete ( "host" ) ;
257+ headers . delete ( "content-length" ) ;
258+ const response = await fetch ( target , {
259+ body : request . bodyBase64
260+ ? Buffer . from ( request . bodyBase64 , "base64" )
261+ : undefined ,
262+ headers,
263+ method : request . method ,
264+ signal : AbortSignal . timeout ( proxyTimeoutMs ) ,
265+ } ) ;
266+ const responseHeaders = { } ;
267+ for ( const [ name , value ] of response . headers . entries ( ) ) {
268+ const lower = name . toLowerCase ( ) ;
269+ if (
270+ lower === "connection" ||
271+ lower === "content-encoding" ||
272+ lower === "content-length" ||
273+ lower === "transfer-encoding"
274+ ) {
275+ continue ;
276+ }
277+ responseHeaders [ name ] = value ;
278+ }
279+ const responseBodyBase64 = Buffer . from ( await response . arrayBuffer ( ) ) . toString (
280+ "base64" ,
281+ ) ;
282+ return {
283+ responseStatus : response . status ,
284+ responseHeaders,
285+ responseBodyBase64,
286+ } ;
287+ }
288+
206289async function localJson ( path ) {
207290 const target = new URL ( path , `${ localUrl } /` ) ;
208291 target . searchParams . set ( "simdeckToken" , localToken ) ;
209292 const response = await fetch ( target , {
210293 headers : { "x-simdeck-token" : localToken } ,
294+ signal : AbortSignal . timeout ( Math . min ( proxyTimeoutMs , 5000 ) ) ,
211295 } ) ;
212296 if ( ! response . ok ) {
213297 throw new Error (
0 commit comments