import fetch from 'node-fetch' import crypto from 'crypto' import { config } from './config.js' import { getRedisKey, isJSFile } from './util.js' import { getCSSDependenciesFor, getViteManifests } from './manifests.js' import * as cache from './cache.js' import { logger } from './logger.js' import { NotFoundError } from './errors.js' import * as redis from './redis.js' export function createWritable (body) { if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(body) return body } export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) { const [response, dependencies] = await Promise.all([ fetch(new URL(path, baseUrl)), isJSFile(path) && getCSSDependenciesFor({ file: path.substr(1), version }) ]) if (!response.ok) throw new NotFoundError(`Error fetching file: ${path}`) const cssString = dependencies && dependencies.map(file => `"${file}"`).join(',') const appendix = cssString && `\n/*injected by ui-middleware*/document.dispatchEvent(new CustomEvent("load-css",{detail:{css:[${cssString}]}}))` const resBuffer = await response.arrayBuffer() const appendixLength = appendix?.length || 0 const body = Buffer.alloc(resBuffer.byteLength + appendixLength) body.fill(Buffer.from(resBuffer), 0, resBuffer.byteLength) if (appendix) body.write(appendix, resBuffer.byteLength) const sha256Sum = crypto.createHash('sha256').update(body).digest('base64') return { body, sha256Sum, headers: { 'content-type': response.headers.get('content-type'), dependencies } } } export async function fetchFileWithHeaders ({ path, version }) { const viteManifests = await getViteManifests({ version }) const module = viteManifests[path.substr(1)] if (module?.meta?.baseUrl) { try { return fetchFileWithHeadersFromBaseUrl(path, module.meta.baseUrl, version) } catch (err) { logger.debug(`[Files] File ${path} had a baseUrl but could not be found on that server: ${err}`) } } return Promise.any(config.urls.map(baseUrl => fetchFileWithHeadersFromBaseUrl(path, baseUrl, version))) } export function getFile ({ version, path }) { const key = getRedisKey({ version, name: `${path}` }) // try to get the file synchronously. const data = cache.getCache()[key] if (data) { logger.debug(`[Files] Resolve from memory: ${key}`) return data } // if synchronously does not work, store the async promise for further requests const promise = (async () => { const bodyKey = getRedisKey({ version, name: `${path}:body` }) const metaKey = getRedisKey({ version, name: `${path}:meta` }) if (redis.isEnabled()) { const [body, meta = '{}'] = await Promise.all([ redis.client.getBuffer(bodyKey), redis.client.get(metaKey) ]) if (body) { logger.debug(`[Files] Resolve from redis: ${key}`) return (cache.getCache()[key] = { body, ...JSON.parse(meta) }) } } const dataFromServer = await fetchFileWithHeaders({ version, path }) if (redis.isEnabled()) { logger.debug(`[Files] Store in redis: ${key}`) const { body, ...rest } = dataFromServer redis.client.set(bodyKey, createWritable(body)) redis.client.set(metaKey, JSON.stringify(rest)) } // overwrite cache with synchronous data logger.debug(`[Files] Store in memory: ${key}`) return (cache.getCache()[key] = dataFromServer) })() // temporary set to promise cache.getCache()[key] = promise return promise }