import fetch from 'node-fetch' import crypto from 'crypto' import { config } from './config.js' import promClient from 'prom-client' import { logger } from './logger.js' import { isJSFile } from './util.js' async function fetchData (path, baseUrl, appendix) { const response = await fetch(new URL(path, baseUrl)) if (!response.ok) throw new Error(`Error fetching file: ${path}`) const resBuffer = await response.arrayBuffer() const appendixLength = appendix?.length || 0 const content = Buffer.alloc(resBuffer.byteLength + appendixLength) content.fill(Buffer.from(resBuffer), 0, resBuffer.byteLength) if (appendix) content.write(appendix, resBuffer.byteLength) const sha256Sum = crypto.createHash('sha256').update(content).digest('base64') return [path, { 'content-type': response.headers.get('content-type'), sha256Sum, content }] } const fileCounter = new promClient.Counter({ name: 'manifest_service_file_cache_fetches', help: 'Number of fetched files' }) const fileErrorCounter = new promClient.Counter({ name: 'manifest_service_file_cache_fetch_errors', help: 'Number of errors while fetching files' }) class FileCache { constructor () { this._cache = {} } async warmUp (manifests, deps) { logger.debug('beginning to warm up cache') const cache = Object.fromEntries(await (async function () { const files = Object.keys(deps) const chunkSize = 50 const result = [] while (files.length > 0) { result.push.apply(result, (await Promise.all(files.splice(0, chunkSize).map(async file => { try { const manifest = manifests[file] || Object.values(manifests).find(m => m.file === file || (m?.assets?.indexOf(file) >= 0) || (m?.css?.indexOf(file) >= 0) ) if (!manifest) { logger.error(`could not find manifest for "${file}"`) return null } let appendix if (manifest.css && isJSFile(file)) { const cssString = manifest.css.map(file => `"${file}"`).join(',') appendix = `\n/*injected by ui-middleware*/document.dispatchEvent(new CustomEvent("load-css",{detail:{css:[${cssString}]}}))` } return await fetchData(file, manifest.meta.baseUrl, appendix) } catch (e) { fileErrorCounter.inc() logger.error(e) } }))).filter(data => Array.isArray(data) && data.length === 2)) } fileCounter.inc(result.length) return result }())) this._cache = cache logger.debug('cache warmed up') } async fetchAndStore (path) { if (config.urls.length === 0) await config.load() const [[key, value]] = (await Promise.allSettled(config.urls.map(baseUrl => fetchData(path, baseUrl)))) .filter(r => r.status === 'fulfilled').map(r => r.value) this._cache[key] = value return value } get (path) { return this?._cache[path.slice(1)] || {} } } export const fileCache = new FileCache()