import { getContentTypeForFilename, trimLeadingSlash, extname, joinurl, } from "./helpers.js"; import { KnotClient } from "./knot-client.js"; class FileCache { constructor({ maxSize } = {}) { this.cache = new Map(); this.maxSize = maxSize; } get(filename) { return this.cache.get(filename) ?? null; } set(filename, content) { this.cache.set(filename, content); // Evict oldest item if cache is full if (this.maxSize && this.cache.size > this.maxSize) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } } clear() { this.cache.clear(); } } export class PagesService { constructor({ knotDomain, ownerDid, repoName, branch = "main", baseDir = "/", notFoundFilepath = null, cache, }) { this.knotDomain = knotDomain; this.ownerDid = ownerDid; this.repoName = repoName; this.branch = branch; this.baseDir = baseDir; this.notFoundFilepath = notFoundFilepath; this.client = new KnotClient({ domain: knotDomain, ownerDid, repoName, branch, }); this.fileCache = null; if (cache) { console.log("Enabling cache for", this.ownerDid, this.repoName); this.fileCache = new FileCache({ maxSize: 100 }); } } async getFileContent(filename) { const cachedContent = this.fileCache?.get(filename); if (cachedContent) { console.log("Cache hit for", filename); return cachedContent; } let content = null; const blob = await this.client.getBlob(filename); if (blob.isBinary) { content = await this.client.getRaw(filename); } else { content = blob.content; } if (this.fileCache && content) { const contentSize = Buffer.isBuffer(content) ? content.length : Buffer.byteLength(content, "utf8"); // Cache unless content is too large (5MB) if (contentSize < 5 * 1024 * 1024) { this.fileCache.set(filename, content); } } return content; } async getPage(route) { let filePath = route; const extension = extname(filePath); if (!extension) { filePath = joinurl(filePath, "index.html"); } const fullPath = joinurl(this.baseDir, trimLeadingSlash(filePath)); const content = await this.getFileContent(fullPath); if (!content) { return this.get404(); } return { status: 200, content, contentType: getContentTypeForFilename(fullPath), }; } async get404() { if (this.notFoundFilepath) { const fullPath = joinurl( this.baseDir, trimLeadingSlash(this.notFoundFilepath) ); const content = await this.getFileContent(fullPath); if (!content) { console.warn("'Not found' file not found", fullPath); return { status: 404, content: "Not Found", contentType: "text/plain" }; } return { status: 404, content, contentType: getContentTypeForFilename(this.notFoundFilepath), }; } return { status: 404, content: "Not Found", contentType: "text/plain" }; } async clearCache() { if (!this.fileCache) { console.log("No cache to clear for", this.ownerDid, this.repoName); return; } console.log("Clearing cache for", this.ownerDid, this.repoName); this.fileCache.clear(); } }