add CachedFetch
This commit is contained in:
parent
71bdb21602
commit
97bd6da9ac
1 changed files with 145 additions and 0 deletions
145
src/platform/cache.ts
Normal file
145
src/platform/cache.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue