feat: 기능 추가 과정중
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
|
||||
test("keeps A active across a failed B stage, activates B with A rollback, and cleans only owned stale caches", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
const result = await page.evaluate(async () => {
|
||||
const adapterModulePath =
|
||||
"/src/adapters/cache-storage/public-response-cache-adapter.ts";
|
||||
const policyModulePath =
|
||||
"/src/adapters/cache-storage/public-cache-policy.ts";
|
||||
const {
|
||||
computePublicCacheManifestDigestHex,
|
||||
createPublicCacheWebLock,
|
||||
createPublicResponseCacheAdapter,
|
||||
} = (await import(
|
||||
/* @vite-ignore */ adapterModulePath
|
||||
)) as typeof import("../../src/adapters/cache-storage/public-response-cache-adapter.ts");
|
||||
const { createDefaultPublicCachePolicy } = (await import(
|
||||
/* @vite-ignore */ policyModulePath
|
||||
)) as typeof import("../../src/adapters/cache-storage/public-cache-policy.ts");
|
||||
|
||||
const suffix = crypto.randomUUID().replaceAll("-", "");
|
||||
const releaseAId = `browser-a-${suffix.slice(0, 20)}`;
|
||||
const releaseBId = `browser-b-${suffix.slice(0, 20)}`;
|
||||
const unrelatedName = `unrelated-${suffix}`;
|
||||
const policy = {
|
||||
...createDefaultPublicCachePolicy(location.origin),
|
||||
ownedCachePrefix: `cap-${suffix.slice(0, 24)}:`,
|
||||
mutationLockName: `cap-${suffix}:mutation`,
|
||||
};
|
||||
const payloads = {
|
||||
[`${location.origin}/cache-${suffix}-a.js`]: {
|
||||
bytes: new TextEncoder().encode("verified-release-a"),
|
||||
contentType: "application/javascript",
|
||||
},
|
||||
[`${location.origin}/cache-${suffix}-b.js`]: {
|
||||
bytes: new TextEncoder().encode("verified-release-b"),
|
||||
contentType: "application/javascript",
|
||||
},
|
||||
[`${location.origin}/cache-${suffix}-b.css`]: {
|
||||
bytes: new TextEncoder().encode(".release-b{display:block}"),
|
||||
contentType: "text/css",
|
||||
},
|
||||
} as const;
|
||||
const [aUrl, bUrl, bSecondUrl] = Object.keys(payloads);
|
||||
|
||||
const digestHex = async (bytes: Uint8Array): Promise<string> => {
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
Uint8Array.from(bytes),
|
||||
);
|
||||
return Array.from(
|
||||
new Uint8Array(digest),
|
||||
(byte) => byte.toString(16).padStart(2, "0"),
|
||||
).join("");
|
||||
};
|
||||
const asset = async (absoluteUrl: string) => {
|
||||
const payload = payloads[absoluteUrl as keyof typeof payloads];
|
||||
return {
|
||||
absoluteUrl,
|
||||
expectedByteLength: payload.bytes.byteLength,
|
||||
expectedContentType: payload.contentType,
|
||||
integrity: {
|
||||
algorithm: "SHA-256" as const,
|
||||
digestHex: await digestHex(payload.bytes),
|
||||
},
|
||||
};
|
||||
};
|
||||
const assetsA = [await asset(aUrl!)];
|
||||
const assetsB = [
|
||||
await asset(bUrl!),
|
||||
await asset(bSecondUrl!),
|
||||
];
|
||||
const manifestA = {
|
||||
releaseRegistryId: releaseAId,
|
||||
assets: assetsA,
|
||||
manifestDigestHex:
|
||||
await computePublicCacheManifestDigestHex(
|
||||
crypto,
|
||||
releaseAId,
|
||||
assetsA,
|
||||
policy,
|
||||
),
|
||||
};
|
||||
const manifestB = {
|
||||
releaseRegistryId: releaseBId,
|
||||
assets: assetsB,
|
||||
manifestDigestHex:
|
||||
await computePublicCacheManifestDigestHex(
|
||||
crypto,
|
||||
releaseBId,
|
||||
assetsB,
|
||||
policy,
|
||||
),
|
||||
};
|
||||
let corruptSecondBAsset = true;
|
||||
const adapter = createPublicResponseCacheAdapter({
|
||||
cacheStorage: caches,
|
||||
crypto,
|
||||
mutationLock: createPublicCacheWebLock(
|
||||
navigator.locks,
|
||||
policy.mutationLockName,
|
||||
),
|
||||
policy,
|
||||
fetcher: async (request) => {
|
||||
if (request.signal.aborted) {
|
||||
throw new DOMException("Aborted.", "AbortError");
|
||||
}
|
||||
const payload =
|
||||
payloads[request.url as keyof typeof payloads];
|
||||
if (!payload) throw new TypeError("Unexpected cache URL.");
|
||||
const responseBytes =
|
||||
corruptSecondBAsset && request.url === bSecondUrl
|
||||
? new Uint8Array(payload.bytes.byteLength)
|
||||
: Uint8Array.from(payload.bytes);
|
||||
return new Response(responseBytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"cache-control":
|
||||
"public, max-age=31536000, immutable",
|
||||
"content-type": payload.contentType,
|
||||
"x-account-id": "must-not-persist",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const readActive = async (absoluteUrl: string) => {
|
||||
const matched = await adapter.responses.matchActiveExact({
|
||||
absoluteUrl,
|
||||
});
|
||||
if (!matched.ok || !matched.value) return matched;
|
||||
const streamed: number[] = [];
|
||||
for await (const chunk of matched.value.body.stream(
|
||||
new AbortController().signal,
|
||||
)) {
|
||||
if (!chunk.ok) return chunk;
|
||||
streamed.push(...chunk.value);
|
||||
}
|
||||
return {
|
||||
ok: true as const,
|
||||
streamed,
|
||||
headerNames: matched.value.headers.map(([name]) => name),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const stageA = await adapter.admin.stageRelease(manifestA);
|
||||
const activateA = await adapter.admin.activateRelease(
|
||||
manifestA.releaseRegistryId,
|
||||
manifestA.manifestDigestHex,
|
||||
);
|
||||
const matchedABeforeFailedB = await readActive(aUrl!);
|
||||
|
||||
const failedStageB =
|
||||
await adapter.admin.stageRelease(manifestB);
|
||||
const afterFailedB = await adapter.admin.inspect();
|
||||
const matchedAAfterFailedB = await readActive(aUrl!);
|
||||
|
||||
corruptSecondBAsset = false;
|
||||
const stageB = await adapter.admin.stageRelease(manifestB);
|
||||
const activateB = await adapter.admin.activateRelease(
|
||||
manifestB.releaseRegistryId,
|
||||
manifestB.manifestDigestHex,
|
||||
);
|
||||
const matchedB = await readActive(bUrl!);
|
||||
const afterActivateB = await adapter.admin.inspect();
|
||||
|
||||
const releaseBCacheName = (await caches.keys()).find((name) =>
|
||||
name.startsWith(
|
||||
`${policy.ownedCachePrefix}release:${releaseBId}:`,
|
||||
),
|
||||
);
|
||||
if (!releaseBCacheName) {
|
||||
throw new Error("B release cache not found.");
|
||||
}
|
||||
const releaseBCache = await caches.open(releaseBCacheName);
|
||||
await releaseBCache.put(
|
||||
new Request(bUrl!),
|
||||
new Response(new Uint8Array([0]), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"cache-control": "public",
|
||||
"content-type": "application/javascript",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const poisoned = await adapter.responses.matchActiveExact({
|
||||
absoluteUrl: bUrl!,
|
||||
});
|
||||
|
||||
await caches.open(`${policy.ownedCachePrefix}stale`);
|
||||
await caches.open(unrelatedName);
|
||||
const cleanup = await adapter.admin.cleanupOwned();
|
||||
const names = await caches.keys();
|
||||
return {
|
||||
stageA,
|
||||
activateA,
|
||||
matchedABeforeFailedB,
|
||||
failedStageB,
|
||||
afterFailedB,
|
||||
matchedAAfterFailedB,
|
||||
stageB,
|
||||
activateB,
|
||||
matchedB,
|
||||
afterActivateB,
|
||||
poisoned,
|
||||
cleanup,
|
||||
unrelatedPreserved: names.includes(unrelatedName),
|
||||
activeBPreserved: names.some((name) =>
|
||||
name.startsWith(
|
||||
`${policy.ownedCachePrefix}release:${releaseBId}:`,
|
||||
),
|
||||
),
|
||||
rollbackAPreserved: names.some((name) =>
|
||||
name.startsWith(
|
||||
`${policy.ownedCachePrefix}release:${releaseAId}:`,
|
||||
),
|
||||
),
|
||||
remainingOwnedCount: names.filter((name) =>
|
||||
name.startsWith(policy.ownedCachePrefix),
|
||||
).length,
|
||||
expectedABytes: [...payloads[aUrl! as keyof typeof payloads].bytes],
|
||||
expectedBBytes: [...payloads[bUrl! as keyof typeof payloads].bytes],
|
||||
};
|
||||
} finally {
|
||||
for (const name of await caches.keys()) {
|
||||
if (
|
||||
name.startsWith(policy.ownedCachePrefix) ||
|
||||
name === unrelatedName
|
||||
) {
|
||||
await caches.delete(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.stageA).toMatchObject({
|
||||
ok: true,
|
||||
value: { entryCount: 1 },
|
||||
});
|
||||
expect(result.activateA).toMatchObject({ ok: true });
|
||||
expect(result.matchedABeforeFailedB).toMatchObject({
|
||||
ok: true,
|
||||
streamed: result.expectedABytes,
|
||||
});
|
||||
expect(result.failedStageB).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "INTEGRITY_FAILED" },
|
||||
});
|
||||
expect(result.afterFailedB).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
activeReleaseRegistryId: expect.stringMatching(/^browser-a-/u),
|
||||
releaseCandidates: [
|
||||
expect.objectContaining({
|
||||
releaseRegistryId: expect.stringMatching(/^browser-a-/u),
|
||||
verified: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result.matchedAAfterFailedB).toMatchObject({
|
||||
ok: true,
|
||||
streamed: result.expectedABytes,
|
||||
});
|
||||
expect(result.stageB).toMatchObject({
|
||||
ok: true,
|
||||
value: { entryCount: 2 },
|
||||
});
|
||||
expect(result.activateB).toMatchObject({ ok: true });
|
||||
expect(result.matchedB).toMatchObject({
|
||||
ok: true,
|
||||
streamed: result.expectedBBytes,
|
||||
});
|
||||
expect(
|
||||
(
|
||||
result.matchedB as Readonly<{
|
||||
headerNames: readonly string[];
|
||||
}>
|
||||
).headerNames,
|
||||
).not.toContain("x-account-id");
|
||||
expect(result.afterActivateB).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
activeReleaseRegistryId: expect.stringMatching(/^browser-b-/u),
|
||||
releaseCandidates: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
releaseRegistryId: expect.stringMatching(/^browser-a-/u),
|
||||
verified: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
releaseRegistryId: expect.stringMatching(/^browser-b-/u),
|
||||
verified: true,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(result.poisoned).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "INTEGRITY_FAILED" },
|
||||
});
|
||||
expect(result.cleanup).toMatchObject({
|
||||
ok: true,
|
||||
value: { deletedOwnedCaches: 1, retainedOwnedCaches: 3 },
|
||||
});
|
||||
expect(result.unrelatedPreserved).toBe(true);
|
||||
expect(result.activeBPreserved).toBe(true);
|
||||
expect(result.rollbackAPreserved).toBe(true);
|
||||
expect(result.remainingOwnedCount).toBe(3);
|
||||
});
|
||||
Reference in New Issue
Block a user