add CachedFetch

This commit is contained in:
thislight 2024-12-26 20:04:26 +08:00
parent 71bdb21602
commit 97bd6da9ac
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E

145
src/platform/cache.ts Normal file
View file

@ -0,0 +1,145 @@
import { addMinutes, formatRFC7231 } from "date-fns";
import {
createRenderEffect,
createResource,
untrack,
} from "solid-js";
export function createCacheBucket(name: string) {
let bucket: Cache | undefined;
return async () => {
if (bucket) {
return bucket;
}
bucket = await self.caches.open(name);
return bucket;
};
}
export type FetchRequest = {
url: string;
headers?: HeadersInit | Headers;
};
async function searchCache(request: Request) {
return await self.caches.match(request);
}
/**
* Create a {@link fetch} helper with additional caching support.
*/
export class CachedFetch<
Transformer extends (response: Response) => any,
Keyer extends (...args: any[]) => FetchRequest,
> {
private cacheBucket: () => Promise<Cache>;
keyFor: Keyer;
private transform: Transformer;
constructor(
cacheBucket: () => Promise<Cache>,
keyFor: Keyer,
tranformer: Transformer,
) {
this.cacheBucket = cacheBucket;
this.keyFor = keyFor;
this.transform = tranformer;
}
private async validateCache(request: Request) {
const buk = await this.cacheBucket();
const response = await fetch(request);
buk.put(request, response.clone());
return response;
}
private request(...args: Parameters<Keyer>) {
const { url, ...init } = this.keyFor(...args);
const request = new Request(url, init);
return request;
}
/**
* Race between the cache and the network result,
* use the fastest result.
*
* The cache will be revalidated.
*/
async fastest(
...args: Parameters<Keyer>
): Promise<Awaited<ReturnType<Transformer>>> {
const request = this.request(...args);
const validating = this.validateCache(request);
const searching = searchCache(request);
const earlyResult = await Promise.race([validating, searching]);
if (earlyResult) {
return await this.transform(earlyResult);
}
return await this.transform(await validating);
}
/**
* Validate and return the result.
*/
async validate(
...args: Parameters<Keyer>
): Promise<Awaited<ReturnType<Transformer>>> {
return await this.transform(
await this.validateCache(this.request(...args)),
);
}
/** Set a response as the cache.
* Recommend to set `Expires` or `Cache-Control` to limit its live time.
*/
async set(key: Parameters<Keyer>, response: Response) {
const buk = await this.cacheBucket();
await buk.put(this.request(...key), response);
}
/** Set a json object as the cache.
* Only available for 5 minutes.
*/
async setJson(key: Parameters<Keyer>, object: unknown) {
const response = new Response(JSON.stringify(object), {
status: 200,
headers: {
"Content-Type": "application/json",
Expires: formatRFC7231(addMinutes(new Date(), 5)),
"X-Cache-Src": "set",
},
});
await this.set(key, response);
}
/**
* Return a resource, using the cache at first, and revalidate
* later.
*/
cachedAndRevalidate(args: () => Parameters<Keyer>) {
const res = createResource(args, (p) => this.validate(...p));
const checkCacheIfStillLoading = async () => {
const saved = await searchCache(this.request(...args()));
if (!saved) {
return;
}
const transformed = await this.transform(saved);
if (res[0].loading) {
res[1].mutate(transformed);
}
};
createRenderEffect(() => void untrack(() => checkCacheIfStillLoading()));
return res;
}
}