From 97bd6da9ac3e4aec29d73d4bf5cae1dc6d513dbf Mon Sep 17 00:00:00 2001 From: thislight Date: Thu, 26 Dec 2024 20:04:26 +0800 Subject: [PATCH] add CachedFetch --- src/platform/cache.ts | 145 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/platform/cache.ts diff --git a/src/platform/cache.ts b/src/platform/cache.ts new file mode 100644 index 0000000..5d5b10a --- /dev/null +++ b/src/platform/cache.ts @@ -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; + keyFor: Keyer; + private transform: Transformer; + + constructor( + cacheBucket: () => Promise, + 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) { + 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 + ): Promise>> { + 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 + ): Promise>> { + 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, 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, 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) { + 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; + } +}