From d777fb84b0130b34810309d47cc66dccba8503f3 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Wed, 16 Sep 2026 18:59:28 +0900 Subject: [PATCH] feat: ratchet hand-rolled abort wiring so it can only shrink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 커널이 있는데도 어댑터 24개 파일이 addEventListener("abort")로 같은 race/cleanup을 각자 짠다. 전수 이행은 abort 의미론을 바꾸는 별도 작업이라 한 번에 하지 않고, 개수를 게이트에 고정해 되돌아가지 못하게 한다. 파일을 하나 더 늘려 실제로 막히는 것을 확인했다. REQUIRED_ABORT_CONSUMERS 주석도 고친다. 그 목록은 재검토가 이름으로 지목한 네 파일만 덮는데, 주석은 Image와 Resumable을 지목하면서 정작 그 두 그룹의 런타임 파일(image-cdn-runtime.ts, resumable-upload-runtime.ts)은 목록에 없어 게이트가 초록불인 채로 사본이 유지되고 있었다. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/check-adapter-inventory.ts | 42 +++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/scripts/check-adapter-inventory.ts b/scripts/check-adapter-inventory.ts index 07009a4..0091d1c 100644 --- a/scripts/check-adapter-inventory.ts +++ b/scripts/check-adapter-inventory.ts @@ -130,6 +130,12 @@ async function main(): Promise { // let an unrelated production import satisfy the gate while Image and // Resumable kept their own diverging copies of the same mechanics — which is // exactly how the four hand-written versions drifted apart in the first place. + // + // 이 목록은 재검토가 이름으로 지목한 네 파일만 덮는다. 같은 두 그룹의 + // `image-cdn-runtime.ts`와 `resumable-upload-runtime.ts`는 여기 없고 지금도 + // 자기 abort 사본을 들고 있다. 어댑터 전체로는 24개 파일이 그렇다. 그 전수 + // 이행은 abort 의미론을 바꾸는 별도 작업이라 이 목록으로 강제하지 않고, + // 아래 래칫이 개수가 늘어나는 것만 막는다. const REQUIRED_ABORT_CONSUMERS: readonly string[] = [ "src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts", "src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts", @@ -164,6 +170,7 @@ async function main(): Promise { const PRIMITIVE_PATH = path.resolve( "src/adapters/platform/abortable-operation.ts", ); + const PRIMITIVE_SPECIFIER = "platform/abortable-operation.ts"; for (const consumer of REQUIRED_ABORT_CONSUMERS) { if (!importerSet.has(consumer)) continue; const source = readFileSync(consumer, "utf8"); @@ -181,6 +188,37 @@ async function main(): Promise { } } + // 손수 짠 abort 배선은 줄어들기만 해야 한다. + // + // 커널 `platform/abortable-operation.ts`가 있는데도 어댑터 24개 파일이 + // `addEventListener("abort")`로 같은 race/cleanup을 각자 짠다. 그 전수 이행은 + // 동작이 바뀌는 큰 작업이라 한 번에 하지 않는다. 대신 개수를 여기 고정해 + // 되돌아가지 못하게 한다. 이행으로 숫자가 내려가면 이 상수도 같이 내린다. + // `platform/`은 커널 자신이므로 세지 않는다. + const HAND_ROLLED_ABORT_CEILING = 24; + const handRolledScan = spawnSync( + "git", + ["grep", "-l", 'addEventListener("abort"', "--", "src/adapters"], + { encoding: "utf8" }, + ); + const handRolledFiles = ( + handRolledScan.status === 0 ? handRolledScan.stdout : "" + ) + .split("\n") + .filter(Boolean) + .filter((file) => !file.startsWith("src/adapters/platform/")) + .filter((file) => !readFileSync(file, "utf8").includes(PRIMITIVE_SPECIFIER)) + .sort(); + if (handRolledFiles.length > HAND_ROLLED_ABORT_CEILING) { + problems.push( + `abortable-operation: hand-rolled abort wiring grew to ` + + `${handRolledFiles.length} files (ceiling ${HAND_ROLLED_ABORT_CEILING}). ` + + `Use platform/abortable-operation.ts instead of a new listener pair. ` + + `Current offenders (the new one is whichever this change added): ` + + `${handRolledFiles.join(", ")}`, + ); + } + if (problems.length > 0) { for (const problem of problems) console.error(problem); process.exitCode = 1; @@ -195,7 +233,9 @@ async function main(): Promise { } shared extensions PASS; ` + `fixture node_modules linking PASS; ` + `shared abort primitive: ${importers.length} importers ` + - `(${importers.join(", ")}) PASS`, + `(${importers.join(", ")}) PASS; ` + + `hand-rolled abort wiring: ${handRolledFiles.length}/` + + `${HAND_ROLLED_ABORT_CEILING} files (ratchet) PASS`, ); }