138 lines
3.6 KiB
TypeScript
138 lines
3.6 KiB
TypeScript
import type {
|
|
PublicCacheAsset,
|
|
PublicCacheReleaseManifest,
|
|
} from "../../src/application/ports/browser-file-storage/cache-storage-ports.ts";
|
|
import {
|
|
createDefaultPublicCachePolicy,
|
|
type PublicCacheRuntimePolicy,
|
|
} from "../../src/adapters/cache-storage/public-cache-policy.ts";
|
|
import {
|
|
computePublicCacheManifestDigestHex,
|
|
type PublicCacheMutationLock,
|
|
} from "../../src/adapters/cache-storage/public-response-cache-adapter.ts";
|
|
|
|
export 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),
|
|
);
|
|
}
|
|
|
|
export 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);
|
|
}
|
|
}
|
|
|
|
export const immediateLock: PublicCacheMutationLock = {
|
|
async run(_signal, task) {
|
|
return await task();
|
|
},
|
|
};
|
|
|
|
export 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);
|
|
},
|
|
});
|
|
}
|
|
|
|
export 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("");
|
|
}
|
|
|
|
export 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,
|
|
),
|
|
};
|
|
}
|