kapcacher
Version:
A Cacher API for Kaplay.
230 lines (185 loc) • 6.16 kB
text/typescript
// (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
};
}