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' import zlib from 'node:zlib' import { promisify } from 'node:util' const gzip = promisify(zlib.gzip) const compressFileSize = Number(process.env.COMPRESS_FILE_SIZE) const compressionMimeTypes = (process.env.COMPRESS_FILE_TYPES || '').split(' ') const compressionWhitelistRegex = new RegExp(`^(${compressionMimeTypes.join('|')})$`) export function createWritable (body) { if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(body) return body } async function createFileBuffer (response, dependencies) { 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 buffer = Buffer.alloc(resBuffer.byteLength + appendixLength) buffer.fill(Buffer.from(resBuffer), 0, resBuffer.byteLength) if (appendix) buffer.write(appendix, resBuffer.byteLength) return buffer } 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 result = { body: await createFileBuffer(response, dependencies), headers: { 'content-type': response.headers.get('content-type'), dependencies } } if (result.body.length > compressFileSize && compressionWhitelistRegex.test(result.headers['content-type'])) { result.body = await gzip(result.body) result.headers['content-encoding'] = 'gzip' } return result } 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 }