UNPKG

kapcacher

Version:
230 lines (185 loc) 6.16 kB
// (c) SpcFORK - Kaboom Cache System // 2024-05-31 - 3:00 PM import * as kbg from "kaplay"; export const HEADER = { name: 'Cacher', version: '0.0.11' } const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); export class Cacher { private cacheName: string; private _cache: Cache; set cache(v: Cache) { this._cache = v; localStorage.setItem(this.cacheName, JSON.stringify(v)); this.initialized = true; } get cache() { return this._cache; } private initialized = false; static nsBuilder(ns: string, text: string) { return `${ns}~${text}`; } constructor(cacheName: string) { this.cacheName = cacheName; } ensureIsInit() { if (!this.initialized) throw new Error("Cache not initialized, will not function without cache"); } async init() { this.cache = await caches.open(this.cacheName); this.initialized = true; } createNamespace(namespace: string): CacherNamespace { this.ensureIsInit(); return new CacherNamespace(this, namespace); } createKaplayCacher() { this.ensureIsInit(); return new (window as any).KaplayCacher(this); } createSpriteCacher() { this.ensureIsInit(); return new (window as any).SpriteCacher(this); } createSoundCacher() { this.ensureIsInit(); return new (window as any).SoundCacher(this); } } export class CacherNamespace { namespace: string; parent: Cacher; cache: () => Cache; constructor(parent: Cacher, namespace: string) { parent.ensureIsInit(); this.namespace = namespace; this.parent = parent; this.cache = () => this.parent.cache; } nsBuilder = (name: string) => Cacher.nsBuilder(this.namespace, name); async get(name: string): Promise<Response | undefined> { return await this.cache().match(this.nsBuilder(name)); } async put(name: string, response: Response) { await this.cache().put(this.nsBuilder(name), response); } makeRollout(rolloutList: string[], rolloutInterval = 0) { return new CacheRollout(this, rolloutList, rolloutInterval); } } export class CacheRollout { rolloutList: string[]; rolloutInterval: number; parent: CacherNamespace; cache: () => Cache; constructor(parent: CacherNamespace, rolloutList: string[], rolloutInterval = 0) { this.rolloutList = rolloutList; this.rolloutInterval = rolloutInterval; this.parent = parent; this.cache = () => parent.cache(); } async rollout( cb: (name: string, nsName: string, response: Response | void, t: InstanceType<typeof CacheRollout>) => any ): Promise<any[]> { const results = await Promise.all(this.rolloutList.map(async (name) => { const nsName = this.parent.nsBuilder(name); const response = await this.cache().match(nsName); const result = await cb(name, nsName, response, this); if (this.rolloutInterval > 0) await wait(this.rolloutInterval); return result; })); return results; } // @ Throw out broken Caches async tossBrokenCaches(rolloutList: string[]) { let ck = await this.cache().keys(); let arrNotInRLL: string[] = []; for (let req of ck) { let name = req.url.split('/').pop()?.split('~')[1] as string; if (!rolloutList.includes(name)) arrNotInRLL.push(name); } for (let b of arrNotInRLL) await this.cache().delete(b); } } /** * @ Kaplay Cacher * * It's required that you change the namespace, loadEntity, and fetchName (If different file-order) * in order to create your own. */ class KaplayCacher { loadEntity: (name: string, reader: any) => Promise<any> fetchName = (name: string) => `./${this.namespace}/${name}`; cache = () => this.CNS.cache(); parent: Cacher; namespace: string; CNS: CacherNamespace; nsBuilder = (name: string) => Cacher.nsBuilder(this.namespace, name); constructor(parent: Cacher, ns: string) { this.parent = parent; this.namespace = ns; this.CNS = new CacherNamespace(parent, ns); } async fetchAndCache(name: string): Promise<Response> { const response = await fetch(this.fetchName(name)); const imageBlob = await response.blob(); const imageUrl = URL.createObjectURL(imageBlob); let res = new Response(imageBlob); await this.CNS.put(name, res); await this.loadEntity(name, { result: imageUrl }); return res; } async loadCached(name: string, cachedResponse: Response): Promise<Response> { const reader = new FileReader(); reader.onload = async () => await this.loadEntity(name, reader) .catch(async (e) => { console.error(`Failed to load cached entity for ${name}:`, e); cachedResponse = await this.fetchAndCache(name); }); reader.readAsDataURL(await cachedResponse.blob()); return cachedResponse; } async rollout(rolloutList: string[], rolloutInterval = 0) { let rl = this.CNS.makeRollout(rolloutList, rolloutInterval); let res = await rl.rollout(async (name, _nsName, response, _t) => { if (response) await this.loadCached(name, response); else await this.fetchAndCache(name); }) await rl.tossBrokenCaches(rolloutList); return res; } } export const staticPlugin = { ...HEADER, Cacher, KaplayCacher, CacherNamespace, CacheRollout, CacherPlugin } { (window as any).KapCacher = staticPlugin } export function CacherPlugin(kbg: kbg.KaboomCtx) { class SpriteCacher extends KaplayCacher { static namespace = 'sprites'; constructor(parent: Cacher) { super(parent, new.target.namespace) } fetchName = (name: string) => `./${this.namespace}/${name}.png` loadEntity = async (name: string, reader: FileReader) => await kbg.loadSprite(name, reader.result as string); } class SoundCacher extends KaplayCacher { static namespace = 'sounds'; constructor(parent: Cacher) { super(parent, new.target.namespace) } fetchName = (name: string) => `./${this.namespace}/${name}.mp3` loadEntity = async (name: string, reader: FileReader) => await kbg.loadSound(name, reader.result as string); } return { ...staticPlugin, KaplayCacher, SpriteCacher, SoundCacher }; }