From 8059ff43a583a9f970ad11faf27f04c5ae1aa3b0 Mon Sep 17 00:00:00 2001 From: Yamada Dev Date: Tue, 17 Mar 2026 23:13:28 +0900 Subject: [PATCH 1/3] fix(core): Prevent closure memory leak in setTimeout for abort Use `controller.abort.bind(controller)` instead of an arrow function `() => controller.abort()` to avoid capturing the surrounding scope in the closure. The arrow function unnecessarily retains references to large objects (response body, streams, deps) for the lifetime of the timer, preventing them from being garbage collected. Co-Authored-By: Claude Opus 4.6 --- src/core/git/gitHubArchive.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/git/gitHubArchive.ts b/src/core/git/gitHubArchive.ts index e5e96fa57..dd1aef178 100644 --- a/src/core/git/gitHubArchive.ts +++ b/src/core/git/gitHubArchive.ts @@ -112,7 +112,8 @@ const downloadAndExtractArchive = async ( deps: ArchiveDownloadDeps = defaultDeps, ): Promise => { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); + const abort = controller.abort.bind(controller); + const timeoutId = setTimeout(abort, timeout); try { const response = await deps.fetch(archiveUrl, { From a1306f14be32df46aa206ae258b49fc09f34335c Mon Sep 17 00:00:00 2001 From: Yamada Dev Date: Tue, 17 Mar 2026 23:25:17 +0900 Subject: [PATCH 2/3] fix(website): Prevent closure memory leaks in setTimeout and setInterval - usePackRequest.ts: Use `controller.abort.bind(controller, 'timeout')` instead of an arrow function to avoid capturing the surrounding scope - cache.ts: Save setInterval ID for cleanup, use `.bind()` instead of arrow function, add `.unref()` to not block process exit, and add `dispose()` method for proper resource cleanup Co-Authored-By: Claude Opus 4.6 --- website/client/composables/usePackRequest.ts | 8 +++----- website/server/src/domains/pack/utils/cache.ts | 10 +++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/website/client/composables/usePackRequest.ts b/website/client/composables/usePackRequest.ts index f5b36beeb..6a508e20d 100644 --- a/website/client/composables/usePackRequest.ts +++ b/website/client/composables/usePackRequest.ts @@ -74,11 +74,9 @@ export function usePackRequest() { inputRepositoryUrl.value = inputUrl.value; // Set up automatic timeout - const timeoutId = setTimeout(() => { - if (requestController) { - requestController.abort('timeout'); - } - }, TIMEOUT_MS); + // Use .bind() to avoid capturing the surrounding scope in the closure + const abort = requestController.abort.bind(requestController, 'timeout'); + const timeoutId = setTimeout(abort, TIMEOUT_MS); try { await handlePackRequest( diff --git a/website/server/src/domains/pack/utils/cache.ts b/website/server/src/domains/pack/utils/cache.ts index dff638017..a7ea0f195 100644 --- a/website/server/src/domains/pack/utils/cache.ts +++ b/website/server/src/domains/pack/utils/cache.ts @@ -13,12 +13,20 @@ interface CacheEntry { export class RequestCache { private cache: Map = new Map(); private readonly ttl: number; + private readonly cleanupIntervalId: ReturnType; constructor(ttlInSeconds = 60) { this.ttl = ttlInSeconds * 1000; // Set up periodic cache cleanup - setInterval(() => this.cleanup(), ttlInSeconds * 1000); + // Use .bind() to avoid capturing the surrounding scope in the closure + this.cleanupIntervalId = setInterval(this.cleanup.bind(this), ttlInSeconds * 1000); + this.cleanupIntervalId.unref(); + } + + dispose(): void { + clearInterval(this.cleanupIntervalId); + this.cache.clear(); } async get(key: string): Promise { From cf9e7c90e69e1492a0095d6c5a4b436360f4e09a Mon Sep 17 00:00:00 2001 From: Yamada Dev Date: Wed, 18 Mar 2026 00:41:20 +0900 Subject: [PATCH 3/3] refactor(core): Inline .bind() calls in setTimeout/setInterval Address review feedback by inlining the bound abort functions directly into setTimeout calls instead of using intermediate variables. Co-Authored-By: Claude Opus 4.6 --- src/core/git/gitHubArchive.ts | 3 +-- website/client/composables/usePackRequest.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/core/git/gitHubArchive.ts b/src/core/git/gitHubArchive.ts index dd1aef178..209592735 100644 --- a/src/core/git/gitHubArchive.ts +++ b/src/core/git/gitHubArchive.ts @@ -112,8 +112,7 @@ const downloadAndExtractArchive = async ( deps: ArchiveDownloadDeps = defaultDeps, ): Promise => { const controller = new AbortController(); - const abort = controller.abort.bind(controller); - const timeoutId = setTimeout(abort, timeout); + const timeoutId = setTimeout(controller.abort.bind(controller), timeout); try { const response = await deps.fetch(archiveUrl, { diff --git a/website/client/composables/usePackRequest.ts b/website/client/composables/usePackRequest.ts index 6a508e20d..9fb583119 100644 --- a/website/client/composables/usePackRequest.ts +++ b/website/client/composables/usePackRequest.ts @@ -75,8 +75,7 @@ export function usePackRequest() { // Set up automatic timeout // Use .bind() to avoid capturing the surrounding scope in the closure - const abort = requestController.abort.bind(requestController, 'timeout'); - const timeoutId = setTimeout(abort, TIMEOUT_MS); + const timeoutId = setTimeout(requestController.abort.bind(requestController, 'timeout'), TIMEOUT_MS); try { await handlePackRequest(