Skip to content

Instantly share code, notes, and snippets.

@ccnokes
Created April 12, 2021 22:40
Show Gist options
  • Save ccnokes/61379a503acc311f7114339ba348af08 to your computer and use it in GitHub Desktop.
Save ccnokes/61379a503acc311f7114339ba348af08 to your computer and use it in GitHub Desktop.
A persistent, async store based on Cache API
/**
* 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