Created
April 12, 2021 22:40
-
-
Save ccnokes/61379a503acc311f7114339ba348af08 to your computer and use it in GitHub Desktop.
A persistent, async store based on Cache API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A persistent, async store based on Cache API | |
* NOTE this is experimental | |
**/ | |
type Options = { | |
name: string | |
version: number | |
userId: string | |
type: "json" | "text" | "blob" | |
debug?: boolean | |
} | |
export class CacheStore { | |
private keyDelimiter = "-"; | |
private userId: string; | |
/** the user passed name */ | |
private readonly shortName: string; | |
/** has the userId and version in it */ | |
public readonly name: string; | |
public readonly version: number; | |
public readonly type: "json" | "text" | "blob"; | |
public readonly debug: boolean; | |
static isSupported() { | |
return 'caches' in self; | |
} | |
constructor(opts: Options) { | |
const { name, version, userId, type, debug = false } = opts; | |
this.shortName = name; | |
// NOTE the `name` is important -- it includes the userId and version in it so that caches can't conflict with each other | |
this.name = [userId, name, version].join(this.keyDelimiter); | |
this.version = version; | |
this.userId = userId; | |
this.type = type; | |
this.debug = debug; | |
} | |
private id(id: string) { | |
return `${location.origin}/__CacheStore__/${this.name}/${id}`; | |
} | |
private parseName(name: string) { | |
const [userId, shortName, version] = name.split(this.keyDelimiter); | |
return { userId, shortName, version: version && parseInt(version, 10) }; | |
} | |
private async parseResponseBody(response: Response): Promise<object | Blob | string> { | |
switch (this.type) { | |
case "json": | |
return response.json(); | |
case "blob": | |
return response.blob(); | |
case "text": | |
return response.text(); | |
} | |
} | |
private encodeValue(val: object | Blob | string): string | Blob { | |
if (this.type === "json") { | |
return JSON.stringify(val); | |
} else { | |
return (val as unknown) as any; | |
} | |
} | |
private log(...args: any[]) { | |
if (this.debug) { | |
console.log(`[CacheStore] ${this.name}: `, ...args); | |
} | |
} | |
/** | |
* Asynchronously checks if there are old caches to delete. You don't have to await this | |
* because if there are old caches, they won't interfere with or match on the new one. | |
*/ | |
async cleanUp() { | |
const existingCaches = await caches.keys(); | |
const cachesToDelete = existingCaches.filter((cacheName) => { | |
const { userId, shortName, version } = this.parseName(cacheName); | |
return ( | |
shortName === this.shortName && userId === this.userId && version !== this.version | |
); | |
}); | |
if (cachesToDelete.length > 0) this.log('deleting caches', cachesToDelete); | |
return await Promise.all( | |
cachesToDelete.map((cacheName) => caches.delete(cacheName)) | |
); | |
} | |
async has(id: string) { | |
const cache = await caches.open(this.name); | |
const res = await cache.match(this.id(id)); | |
if (res) { | |
if (this.checkExpired(res)) { | |
this.log(`${id} expired, deleting.`); | |
this.delete(id); | |
return false; | |
} | |
} | |
this.log(`has ${id}: ${!!res}`, res); | |
return !!res; | |
} | |
private checkExpired(response: Response) { | |
const expires = response.headers.get('CacheStore-Expires'); | |
if (expires) { | |
return Date.now() >= parseInt(expires, 10); | |
} else { | |
return false; | |
} | |
} | |
async get(id: string) { | |
const cache = await caches.open(this.name); | |
const res = await cache.match(this.id(id)); | |
if (res) { | |
if (this.checkExpired(res)) { | |
this.log(`${id} expired, deleting.`); | |
this.delete(id); | |
return null; | |
} | |
this.log(`cache hit on ${id}`); | |
return this.parseResponseBody(res); | |
} else { | |
return null; | |
} | |
} | |
async set(_id: string, val: object | Blob | string | Document, expires?: Date) { | |
const cache = await caches.open(this.name); | |
const id = this.id(_id); | |
this.log(`set on ${id}`); | |
return await cache.put(id, new Response(this.encodeValue(val), { | |
headers: { | |
'CacheStore-Date': String(new Date().getTime()), | |
'CacheStore-Expires': expires ? String(expires.getTime()) : '' | |
} | |
})); | |
} | |
async delete(id: string) { | |
const cache = await caches.open(this.name); | |
return await cache.delete(this.id(id)); | |
} | |
deleteAll() { | |
return caches.delete(this.name); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment