diff --git a/spec/dependencies_test.js b/spec/dependencies_test.js index 8eedefa1349600e8b1ce860e44def5383ca257c5..f7b109a38e41152e2db4d50e8401edc0a129af55 100644 --- a/spec/dependencies_test.js +++ b/spec/dependencies_test.js @@ -1,4 +1,4 @@ -import { viteManifestToDeps } from '../src/manifests.js' +import { viteManifestToDeps } from '../src/util.js' import { expect } from 'chai' describe('Vite manifest parsing', function () { diff --git a/spec/manifest_parsing_test.js b/spec/manifest_parsing_test.js index 89f980f03789df51a9697dab1b8e0d5e1b8ff478..bed5ecddbd3ca392265051e4fd4463e9b22dbc63 100644 --- a/spec/manifest_parsing_test.js +++ b/spec/manifest_parsing_test.js @@ -1,4 +1,4 @@ -import { viteToOxManifest } from '../src/manifests.js' +import { viteToOxManifest } from '../src/util.js' import { expect } from 'chai' describe('Vite manifest parsing', function () { diff --git a/spec/server_test.js b/spec/server_test.js index a29dd3e1b3aaee28f8d3e720afadcb008fd74f67..08c4afa8ae40b8542a63b98f742c8f3f7bf80741 100644 --- a/spec/server_test.js +++ b/spec/server_test.js @@ -2,12 +2,13 @@ import request from 'supertest' import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js' import { expect } from 'chai' import * as td from 'testdouble' +import { Response } from 'node-fetch' describe('UI Middleware', function () { let app let fetchConfig - before(async function () { + beforeEach(async function () { mockConfig({ urls: ['http://ui-server/'] }) mockFetch(fetchConfig = { 'http://ui-server': { @@ -18,11 +19,8 @@ describe('UI Middleware', function () { app = await mockApp() }) - after(function () { + afterEach(function () { td.reset() - }) - - afterEach(async function () { process.env.CACHE_TTL = 30000 }) @@ -80,6 +78,8 @@ describe('UI Middleware', function () { '/example.js': '' } + // trigger update + await request(app).get('/manifests') // wait some time await new Promise(resolve => setTimeout(resolve, 10)) @@ -87,6 +87,42 @@ describe('UI Middleware', function () { expect(response2.statusCode).to.equal(200) expect(response2.body).to.deep.equal([{ namespace: 'other', path: 'example' }]) }) + + it('only updates the version hash when the caches are warm', async function () { + process.env.CACHE_TTL = 0 + + // fetch the file to get the initial version + let response = await request(app).get('/example.js') + expect(response.text).to.equal('') + const version = response.headers.version + await new Promise(resolve => setTimeout(resolve, 1)) + + // update resources + let resolveExampleJs + fetchConfig['http://ui-server'] = { + '/manifest.json': generateSimpleViteManifest({ 'example.js': 'other' }), + '/example.js': () => new Promise(resolve => (resolveExampleJs = resolve)) + } + + // fetch file again while the update is still processing + // this will also trigger the update + response = await request(app).get('/example.js') + expect(response.text).to.equal('') + expect(response.headers.version).to.equal(version) + + // fetch once again. this will not trigger an update of the file-cache + response = await request(app).get('/example.js') + expect(response.text).to.equal('') + expect(response.headers.version).to.equal(version) + + // resolve the response to the example js file. This will finish the cache warmup + resolveExampleJs(new Response('new content')) + + // fetch the file again. Content and version should be updated + response = await request(app).get('/example.js') + expect(response.text).to.equal('new content') + expect(response.headers.version).not.to.equal(version) + }) }) describe('multiple configurations', function () { diff --git a/src/createApp.js b/src/createApp.js index 4841e0e3fac7d3f8d440959c933ec709dd863582..9ca9690c6c12ad34e82c4b872549e9f95a3a6e2a 100644 --- a/src/createApp.js +++ b/src/createApp.js @@ -18,9 +18,10 @@ import promClient from 'prom-client' import swaggerUi from 'swagger-ui-express' import yaml from 'js-yaml' import fs from 'fs' -import { getCSSDependenciesFor, getDependencies, getOxManifests, getVersion, loadViteManifests, viteManifestToDeps } from './manifests.js' +import { getCSSDependenciesFor, getDependencies, getOxManifests, getVersion, loadViteManifests } from './manifests.js' import { fileCache } from './files.js' import { getMergedMetadata } from './meta.js' +import { viteManifestToDeps } from './util.js' const swaggerDocument = yaml.load(fs.readFileSync('./src/swagger.yaml', 'utf8')) diff --git a/src/files.js b/src/files.js index c264499ce8878bcf6abf7c8aae7348034db28843..d7b4af44af9e2dc99bae62500b32a4ed89de7071 100644 --- a/src/files.js +++ b/src/files.js @@ -3,7 +3,7 @@ import crypto from 'crypto' import { config } from './config.js' import promClient from 'prom-client' import { logger } from './logger.js' -import { isJSFile } from './util.js' +import { isJSFile, viteToOxManifest } from './util.js' async function fetchData (path, baseUrl, appendix) { const response = await fetch(new URL(path, baseUrl)) @@ -34,6 +34,10 @@ const fileErrorCounter = new promClient.Counter({ class FileCache { constructor () { this._cache = {} + this._manifests = {} + this._hash = '' + this._dependencies = {} + this._oxManifests = {} } async warmUp (manifests, deps) { @@ -69,7 +73,13 @@ class FileCache { 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') } @@ -85,6 +95,22 @@ class FileCache { 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() diff --git a/src/manifests.js b/src/manifests.js index 07c61747bd3fb405d1a986262b676d8a31857951..6801ea15aa9ff9850ba8e9408f96addf95516a18 100644 --- a/src/manifests.js +++ b/src/manifests.js @@ -1,9 +1,8 @@ import fetch from 'node-fetch' -import path from 'path' import { URL } from 'url' import { fileCache } from './files.js' import { config } from './config.js' -import { hash } from './util.js' +import { hash, viteManifestToDeps } from './util.js' import { logger } from './logger.js' export const loadViteManifests = (() => { @@ -74,81 +73,24 @@ export const loadViteManifests = (() => { } })() -export function viteToOxManifest (viteManifests) { - const deps = viteManifestToDeps(viteManifests) - return Object.values(viteManifests) - .filter(manifest => Array.isArray(manifest?.meta?.manifests)) - .map(manifest => - manifest.meta.manifests.map(oxManifest => { - const dependencies = deps[manifest.file] - const data = { - ...oxManifest, - path: manifest.file.slice(0, -path.parse(manifest.file).ext.length) - } - if (dependencies?.length > 0) data.dependencies = dependencies - return data - }) - ) - .flat() +export async function getOxManifests () { + await loadViteManifests().catch(() => {}) + return fileCache.oxManifests } -export const getOxManifests = (() => { - let prevHash - let oxManifestCache - return async function getOxManifests () { - const viteManifest = await loadViteManifests().catch(() => {}) - if (viteManifest && viteManifest.__hash__ !== prevHash) { - oxManifestCache = viteToOxManifest(viteManifest) - prevHash = viteManifest.__hash__ - } - return oxManifestCache - } -})() - -export function viteManifestToDeps (viteManifest) { - const deps = {} - for (const [codePoint, { isEntry, file, imports, css, assets }] of Object.entries(viteManifest)) { - if (isEntry && codePoint.endsWith('.html')) deps[codePoint] = [file] - if (Array.isArray(assets)) assets.forEach(asset => { deps[asset] = [] }) - if (Array.isArray(css)) css.forEach(css => { deps[css] = [] }) - deps[file] = [] - .concat(imports?.map(path => viteManifest[path].file)) - .concat(css) - .concat(assets) - .filter(Boolean) - } - return deps +export async function getDependencies () { + // simply catch the error here. This might happen, when one of the UI containers is temporarily not available + await loadViteManifests().catch(() => {}) + return fileCache.dependencies } -export const getDependencies = (() => { - let prevHash - let depCache - return async function getDependencies () { - // simply catch the error here. This might happen, when one of the UI containers is temporarily not available - const viteManifest = await loadViteManifests().catch(() => {}) - if (viteManifest && viteManifest.__hash__ !== prevHash) { - depCache = viteManifestToDeps(viteManifest) - prevHash = viteManifest.__hash__ - } - return depCache - } -})() - export async function getCSSDependenciesFor (file) { const allDependencies = await getDependencies() const dependencies = allDependencies[file] || [] return dependencies.filter(dep => /\.css/i.test(dep)) } -export const getVersion = (() => { - let prevHash - let versionString - return async function getVersion () { - const viteManifest = await loadViteManifests().catch(() => {}) - if (viteManifest && viteManifest.__hash__ !== prevHash) { - versionString = `${+new Date()}.${viteManifest.__hash__}` - prevHash = viteManifest.__hash__ - } - return versionString - } -})() +export async function getVersion () { + await loadViteManifests().catch(() => {}) + return fileCache.hash +} diff --git a/src/util.js b/src/util.js index 55c804338bf0eb5c4a790b68187b7f9dacaf631a..018a517395797976d9b7e8c6b70b7c598e763e5b 100644 --- a/src/util.js +++ b/src/util.js @@ -21,3 +21,36 @@ export function isJSFile (name) { const extname = path.extname(name) return extname === '.js' } + +export function viteManifestToDeps (viteManifest) { + const deps = {} + for (const [codePoint, { isEntry, file, imports, css, assets }] of Object.entries(viteManifest)) { + if (isEntry && codePoint.endsWith('.html')) deps[codePoint] = [file] + if (Array.isArray(assets)) assets.forEach(asset => { deps[asset] = [] }) + if (Array.isArray(css)) css.forEach(css => { deps[css] = [] }) + deps[file] = [] + .concat(imports?.map(path => viteManifest[path].file)) + .concat(css) + .concat(assets) + .filter(Boolean) + } + return deps +} + +export function viteToOxManifest (viteManifests) { + const deps = viteManifestToDeps(viteManifests) + return Object.values(viteManifests) + .filter(manifest => Array.isArray(manifest?.meta?.manifests)) + .map(manifest => + manifest.meta.manifests.map(oxManifest => { + const dependencies = deps[manifest.file] + const data = { + ...oxManifest, + path: manifest.file.slice(0, -path.parse(manifest.file).ext.length) + } + if (dependencies?.length > 0) data.dependencies = dependencies + return data + }) + ) + .flat() +}