// cache.js export class Cache { constructor(root, onResolve) { this.root = root; this.onResolve = onResolve || (() => { }); this.handlers = new Map(); this.promises = new Map(); this.data = new Map(); } register(kind, handler) { this.handlers.set(kind, handler); } // Used by internal loaders (like models loading parent models) getAsync(id, kind) { this.getSync(id, kind); // Triggers the load if it hasn't started const [namespace, resource] = id.includes(':') ? id.split(':', 2) : ["minecraft", id]; const key = `${kind}:${namespace}:${resource}`; if (this.promises.has(key)) { return this.promises.get(key); } return Promise.resolve(this.data.get(key)); } // Used by the Engine for immediate, non-blocking rendering getSync(id, kind) { const [namespace, resource] = id.includes(':') ? id.split(':', 2) : ["minecraft", id]; const normalizedId = `${namespace}:${resource}`; const key = `${kind}:${normalizedId}`; const url = `${this.root}/${namespace}/${kind}/${resource}`; // 1. If we already have the real data, return it instantly if (this.data.has(key)) return this.data.get(key); // 2. If we haven't even started loading it yet, kick off the fetch if (!this.promises.has(key)) { const handler = this.handlers.get(kind); if (!handler) throw new Error(`No handler registered for resource kind: ${kind}`); if (handler.prepare) handler.prepare(this, normalizedId, url); const promise = handler.process(this, normalizedId, url).then(result => { this.data.set(key, result); this.onResolve(); // Ping the engine to re-render! return result; }).catch(err => { console.warn(`Failed to load ${key}:`, err); const fallback = handler.getFallback(normalizedId); this.data.set(key, fallback); this.onResolve(); return fallback; }); this.promises.set(key, promise); } // 3. Always return the synchronous fallback while the promise resolves in the background return this.handlers.get(kind).getFallback(normalizedId); } }