enkanetwork
Version:
API wrapper for enka.network written on TypeScript which provides localization, caching and convenience
111 lines (110 loc) • 4.18 kB
JavaScript
"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;