UNPKG

enkanetwork

Version:

API wrapper for enka.network written on TypeScript which provides localization, caching and convenience

111 lines (110 loc) 4.18 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FileSystemAdapter = void 0; const node_fs_1 = __importDefault(require("node:fs")); const promises_1 = __importDefault(require("node:fs/promises")); const node_path_1 = __importDefault(require("node:path")); /** * Caches image assets on the local filesystem. On `resolve`: * - if the file is already cached, returns a local reference; * - otherwise enqueues a deduplicated background download and (by default) returns the * origin URL until the file is available, after which subsequent calls return the * local reference. * * Downloads are best-effort and never throw out of `resolve`. Use `flush` to await all * pending downloads (e.g. to warm the cache before rendering). */ class FileSystemAdapter { directory; pathPrefix; baseUrl; extension; fallbackToOrigin; userAgent; concurrency; cached = new Set(); pending = new Map(); queue = []; active = 0; constructor({ directory = node_path_1.default.resolve(process.cwd(), ".enka-cache", "ui"), pathPrefix, baseUrl = "https://enka.network/ui/", extension = ".png", fallbackToOrigin = true, userAgent = "enkanetwork-asset-cache", concurrency = 8, } = {}) { this.directory = directory; // Default to the storage directory (with a trailing separator) so `resolve` returns a real path. this.pathPrefix = pathPrefix ?? `${directory}${node_path_1.default.sep}`; this.baseUrl = baseUrl; this.extension = extension; this.fallbackToOrigin = fallbackToOrigin; this.userAgent = userAgent; this.concurrency = concurrency; this.init(); } init() { node_fs_1.default.mkdirSync(this.directory, { recursive: true }); for (const entry of node_fs_1.default.readdirSync(this.directory)) { if (entry.endsWith(this.extension)) this.cached.add(entry.slice(0, -this.extension.length)); } } localRef(filename) { return `${this.pathPrefix}${filename}${this.extension}`; } originUrl(filename) { return `${this.baseUrl}${filename}${this.extension}`; } async fetchToDisk(filename) { try { const res = await fetch(this.originUrl(filename), { ...(this.userAgent && { headers: { "User-Agent": this.userAgent }, }), }); if (!res.ok) return; await promises_1.default.writeFile(node_path_1.default.resolve(this.directory, `${filename}${this.extension}`), Buffer.from(await res.arrayBuffer())); this.cached.add(filename); } catch { // Best-effort: leave uncached so it is retried on the next resolve. } } next() { if (this.active >= this.concurrency) return; const start = this.queue.shift(); if (start) start(); } // Schedules a download, respecting the concurrency cap. schedule(filename) { const promise = new Promise((resolve) => { const start = () => { this.active++; this.fetchToDisk(filename).finally(() => { this.active--; resolve(); this.next(); }); }; if (this.active < this.concurrency) start(); else this.queue.push(start); }).finally(() => this.pending.delete(filename)); this.pending.set(filename, promise); } resolve(filename) { if (this.cached.has(filename)) return this.localRef(filename); if (!this.pending.has(filename)) this.schedule(filename); return this.fallbackToOrigin ? this.originUrl(filename) : this.localRef(filename); } async flush() { await Promise.all(this.pending.values()); } } exports.FileSystemAdapter = FileSystemAdapter;