Skip to content
Snippets Groups Projects
Commit d213940d authored by richard.petersen's avatar richard.petersen :sailboat:
Browse files

Fix: OXUIB-1380 - UI forever (enough) broken after update

Root cause: Version has was updated before file cache
Solution: Keep all updates in one place and switch to new version at the same time
parent d98ba424
No related branches found
No related tags found
No related merge requests found
import { viteManifestToDeps } from '../src/manifests.js'
import { viteManifestToDeps } from '../src/util.js'
import { expect } from 'chai'
describe('Vite manifest parsing', function () {
......
import { viteToOxManifest } from '../src/manifests.js'
import { viteToOxManifest } from '../src/util.js'
import { expect } from 'chai'
describe('Vite manifest parsing', function () {
......
......@@ -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 () {
......
......@@ -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'))
......
......@@ -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()
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
}
......@@ -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()
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment