Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
files.js 3.50 KiB
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, viteToOxManifest } 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 = {}
    this._manifests = {}
    this._hash = ''
    this._dependencies = {}
    this._oxManifests = {}
  }

  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
    this._manifests = manifests
    this._hash = `${+new Date()}.${manifests.__hash__}`
    this._dependencies = deps
    this._oxManifests = viteToOxManifest(manifests)

    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)] || {}
  }

  get manifests () {
    return this?._manifests
  }

  get hash () {
    return this?._hash
  }

  get dependencies () {
    return this?._dependencies
  }

  get oxManifests () {
    return this?._oxManifests
  }
}

export const fileCache = new FileCache()