feat: ratchet hand-rolled abort wiring so it can only shrink

커널이 있는데도 어댑터 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) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-16 18:59:28 +09:00
co-authored by Claude Opus 5
parent 5d9ad4f74a
commit d777fb84b0
+41 -1
View File
@@ -130,6 +130,12 @@ async function main(): Promise<void> {
// 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<void> {
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<void> {
}
}
// 손수 짠 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<void> {
} 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`,
);
}