refactor: 프론트엔드 리펙토링

This commit is contained in:
donghyeon-ka
2026-09-18 22:05:42 +09:00
parent 5cc41467ae
commit ec7f20e2ee
100 changed files with 6005 additions and 2867 deletions
+7 -321
View File
@@ -10,135 +10,17 @@ import {
type PublicCacheRuntimePolicy,
} from "../../src/adapters/cache-storage/public-cache-policy.ts";
import {
computePublicCacheManifestDigestHex,
createPublicResponseCacheAdapter,
type PublicCacheMutationLock,
} from "../../src/adapters/cache-storage/public-response-cache-adapter.ts";
class MemoryCache {
readonly responses: Array<Readonly<{
request: Request;
response: Response;
}>> = [];
async match(request: RequestInfo | URL): Promise<Response | undefined> {
const url =
request instanceof Request ? request.url : new URL(String(request)).href;
const nativeRequest =
request instanceof Request ? request : new Request(url);
return this.responses
.find(
(entry) =>
entry.request.url === url &&
varyMatches(entry.request, nativeRequest, entry.response),
)
?.response.clone();
}
async put(request: RequestInfo | URL, response: Response): Promise<void> {
const url =
request instanceof Request ? request.url : new URL(String(request)).href;
const nativeRequest =
request instanceof Request ? request.clone() : new Request(url);
const existing = this.responses.findIndex(
(entry) =>
entry.request.url === url &&
varyMatches(entry.request, nativeRequest, response),
);
const entry = {
request: nativeRequest,
response: response.clone(),
};
if (existing >= 0) this.responses.splice(existing, 1, entry);
else this.responses.push(entry);
}
}
function varyMatches(
storedRequest: Request,
incomingRequest: Request,
response: Response,
): boolean {
const vary = response.headers.get("vary");
if (!vary) return true;
return vary
.split(",")
.map((name) => name.trim().toLowerCase())
.every(
(name) =>
storedRequest.headers.get(name) ===
incomingRequest.headers.get(name),
);
}
class MemoryCacheStorage {
readonly caches = new Map<string, MemoryCache>();
async open(name: string): Promise<Cache> {
let cache = this.caches.get(name);
if (!cache) {
cache = new MemoryCache();
this.caches.set(name, cache);
}
return cache as unknown as Cache;
}
async keys(): Promise<string[]> {
return [...this.caches.keys()];
}
async delete(name: string): Promise<boolean> {
return this.caches.delete(name);
}
}
const immediateLock: PublicCacheMutationLock = {
async run(_signal, task) {
return await task();
},
};
function deferred<Value>() {
let settle: ((value: Value) => void) | undefined;
const promise = new Promise<Value>((resolve) => {
settle = resolve;
});
return Object.freeze({
promise,
resolve(value: Value): void {
settle?.(value);
},
});
}
async function digestHex(bytes: Uint8Array): Promise<string> {
const digest = await globalThis.crypto.subtle.digest(
"SHA-256",
Uint8Array.from(bytes),
);
return [...new Uint8Array(digest)]
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
async function manifestFor(
releaseRegistryId: string,
assets: readonly PublicCacheAsset[],
policy: PublicCacheRuntimePolicy = createDefaultPublicCachePolicy(
"https://assets.example.test",
),
): Promise<PublicCacheReleaseManifest> {
return {
releaseRegistryId,
assets,
manifestDigestHex: await computePublicCacheManifestDigestHex(
globalThis.crypto,
releaseRegistryId,
assets,
policy,
),
};
}
import {
MemoryCacheStorage,
deferred,
digestHex,
immediateLock,
manifestFor,
} from "./public-response-cache-fixture.ts";
describe("public response Cache Storage adapter", () => {
it("rejects a policy that enables variants but strips Vary", () => {
@@ -1513,199 +1395,3 @@ describe("public response Cache Storage adapter", () => {
expect(await cacheStorage.keys()).toEqual([]);
});
});
/**
* STO-RR-04 / STO-RR-05. A release cache that is currently serving traffic is
* the last thing a repair may destroy. A transient marker read failure is not
* evidence of damage, and a repair that has not yet fetched anything has not
* yet earned the right to delete what still works.
*/
describe("public response cache repair is failure-atomic", () => {
async function stagedRelease(releaseRegistryId: string) {
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const firstBytes = new Uint8Array([1, 1, 1, 1]);
const secondBytes = new Uint8Array([2, 2, 2, 2]);
const assets: readonly PublicCacheAsset[] = [
{
absoluteUrl: "https://assets.example.test/first.js",
expectedByteLength: firstBytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(firstBytes),
},
},
{
absoluteUrl: "https://assets.example.test/second.js",
expectedByteLength: secondBytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(secondBytes),
},
},
];
const bodies = new Map<string, Uint8Array>([
[assets[0]!.absoluteUrl, firstBytes],
[assets[1]!.absoluteUrl, secondBytes],
]);
const cacheStorage = new MemoryCacheStorage();
const fetchLog: string[] = [];
let failFrom: string | null = null;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async (request: Request) => {
fetchLog.push(request.url);
if (failFrom !== null && request.url === failFrom) {
throw new TypeError("network is down");
}
const body = bodies.get(request.url);
if (!body) throw new TypeError(`unknown asset ${request.url}`);
return new Response(Uint8Array.from(body), {
headers: {
"cache-control": "public",
"content-type": "application/javascript",
},
});
},
});
const manifest = await manifestFor(releaseRegistryId, assets, policy);
expect(await adapter.admin.stageRelease(manifest)).toMatchObject({
ok: true,
});
expect(
await adapter.admin.activateRelease(
manifest.releaseRegistryId,
manifest.manifestDigestHex,
),
).toMatchObject({ ok: true });
const cacheName = [...cacheStorage.caches.keys()].find((name) =>
name.includes(releaseRegistryId),
);
if (!cacheName) throw new Error("staged cache missing");
return {
adapter,
assets,
cacheName,
cacheStorage,
fetchLog,
manifest,
setFailure(url: string | null) {
failFrom = url;
},
};
}
it("does not delete an active candidate when the marker read fails transiently", async () => {
const release = await stagedRelease("transient-marker");
const cache = release.cacheStorage.caches.get(release.cacheName)!;
const realMatch = cache.match.bind(cache);
let markerReads = 0;
const assetUrls = new Set(release.assets.map((asset) => asset.absoluteUrl));
cache.match = async (request: RequestInfo | URL) => {
const url =
request instanceof Request ? request.url : String(request);
if (!assetUrls.has(url)) {
markerReads += 1;
throw new DOMException("Storage is busy", "InvalidStateError");
}
return await realMatch(request);
};
const restaged = await release.adapter.admin.stageRelease(release.manifest);
expect(markerReads).toBeGreaterThan(0);
expect(restaged.ok).toBe(false);
expect(release.cacheStorage.caches.has(release.cacheName)).toBe(true);
cache.match = realMatch;
expect(
await release.adapter.responses.matchActiveExact({
absoluteUrl: release.assets[0]!.absoluteUrl,
}),
).toMatchObject({ ok: true });
});
it("keeps every healthy asset when one repair fetch fails", async () => {
const release = await stagedRelease("partial-repair");
const cache = release.cacheStorage.caches.get(release.cacheName)!;
// Corrupt only the first asset's stored bytes.
const corrupted = cache.responses.findIndex(
(entry) => entry.request.url === release.assets[0]!.absoluteUrl,
);
expect(corrupted).toBeGreaterThanOrEqual(0);
cache.responses.splice(corrupted, 1);
release.setFailure(release.assets[0]!.absoluteUrl);
const restaged = await release.adapter.admin.stageRelease(release.manifest);
expect(restaged.ok).toBe(false);
// The cache still exists and the healthy asset is still served.
expect(release.cacheStorage.caches.has(release.cacheName)).toBe(true);
expect(
await release.adapter.responses.matchActiveExact({
absoluteUrl: release.assets[1]!.absoluteUrl,
}),
).toMatchObject({ ok: true });
});
it("still removes a candidate this call created when staging fails", async () => {
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([7, 7, 7, 7]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/fresh.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: {
algorithm: "SHA-256",
digestHex: await digestHex(bytes),
},
};
const cacheStorage = new MemoryCacheStorage();
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: immediateLock,
policy,
fetcher: async () => {
throw new TypeError("network is down");
},
});
const manifest = await manifestFor("fresh-release", [asset], policy);
expect(await adapter.admin.stageRelease(manifest)).toMatchObject({
ok: false,
});
expect(await cacheStorage.keys()).toEqual([]);
});
it("repairs an evicted asset in place and keeps the release usable", async () => {
const release = await stagedRelease("in-place-repair");
const cache = release.cacheStorage.caches.get(release.cacheName)!;
const evicted = cache.responses.findIndex(
(entry) => entry.request.url === release.assets[1]!.absoluteUrl,
);
cache.responses.splice(evicted, 1);
expect(
await release.adapter.admin.stageRelease(release.manifest),
).toMatchObject({ ok: true });
expect(
await release.adapter.responses.matchActiveExact({
absoluteUrl: release.assets[1]!.absoluteUrl,
}),
).toMatchObject({ ok: true });
expect(
await release.adapter.responses.matchActiveExact({
absoluteUrl: release.assets[0]!.absoluteUrl,
}),
).toMatchObject({ ok: true });
});
});