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' import { expect } from 'chai'
describe('Vite manifest parsing', function () { describe('Vite manifest parsing', function () {
......
import { viteToOxManifest } from '../src/manifests.js' import { viteToOxManifest } from '../src/util.js'
import { expect } from 'chai' import { expect } from 'chai'
describe('Vite manifest parsing', function () { describe('Vite manifest parsing', function () {
......
...@@ -2,12 +2,13 @@ import request from 'supertest' ...@@ -2,12 +2,13 @@ import request from 'supertest'
import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js' import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { expect } from 'chai' import { expect } from 'chai'
import * as td from 'testdouble' import * as td from 'testdouble'
import { Response } from 'node-fetch'
describe('UI Middleware', function () { describe('UI Middleware', function () {
let app let app
let fetchConfig let fetchConfig
before(async function () { beforeEach(async function () {
mockConfig({ urls: ['http://ui-server/'] }) mockConfig({ urls: ['http://ui-server/'] })
mockFetch(fetchConfig = { mockFetch(fetchConfig = {
'http://ui-server': { 'http://ui-server': {
...@@ -18,11 +19,8 @@ describe('UI Middleware', function () { ...@@ -18,11 +19,8 @@ describe('UI Middleware', function () {
app = await mockApp() app = await mockApp()
}) })
after(function () { afterEach(function () {
td.reset() td.reset()
})
afterEach(async function () {
process.env.CACHE_TTL = 30000 process.env.CACHE_TTL = 30000
}) })
...@@ -80,6 +78,8 @@ describe('UI Middleware', function () { ...@@ -80,6 +78,8 @@ describe('UI Middleware', function () {
'/example.js': '' '/example.js': ''
} }
// trigger update
await request(app).get('/manifests')
// wait some time // wait some time
await new Promise(resolve => setTimeout(resolve, 10)) await new Promise(resolve => setTimeout(resolve, 10))
...@@ -87,6 +87,42 @@ describe('UI Middleware', function () { ...@@ -87,6 +87,42 @@ describe('UI Middleware', function () {
expect(response2.statusCode).to.equal(200) expect(response2.statusCode).to.equal(200)
expect(response2.body).to.deep.equal([{ namespace: 'other', path: 'example' }]) 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 () { describe('multiple configurations', function () {
......
...@@ -18,9 +18,10 @@ import promClient from 'prom-client' ...@@ -18,9 +18,10 @@ import promClient from 'prom-client'
import swaggerUi from 'swagger-ui-express' import swaggerUi from 'swagger-ui-express'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import fs from 'fs' 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 { fileCache } from './files.js'
import { getMergedMetadata } from './meta.js' import { getMergedMetadata } from './meta.js'
import { viteManifestToDeps } from './util.js'
const swaggerDocument = yaml.load(fs.readFileSync('./src/swagger.yaml', 'utf8')) const swaggerDocument = yaml.load(fs.readFileSync('./src/swagger.yaml', 'utf8'))
......
...@@ -3,7 +3,7 @@ import crypto from 'crypto' ...@@ -3,7 +3,7 @@ import crypto from 'crypto'
import { config } from './config.js' import { config } from './config.js'
import promClient from 'prom-client' import promClient from 'prom-client'
import { logger } from './logger.js' import { logger } from './logger.js'
import { isJSFile } from './util.js' import { isJSFile, viteToOxManifest } from './util.js'
async function fetchData (path, baseUrl, appendix) { async function fetchData (path, baseUrl, appendix) {
const response = await fetch(new URL(path, baseUrl)) const response = await fetch(new URL(path, baseUrl))
...@@ -34,6 +34,10 @@ const fileErrorCounter = new promClient.Counter({ ...@@ -34,6 +34,10 @@ const fileErrorCounter = new promClient.Counter({
class FileCache { class FileCache {
constructor () { constructor () {
this._cache = {} this._cache = {}
this._manifests = {}
this._hash = ''
this._dependencies = {}
this._oxManifests = {}
} }
async warmUp (manifests, deps) { async warmUp (manifests, deps) {
...@@ -69,7 +73,13 @@ class FileCache { ...@@ -69,7 +73,13 @@ class FileCache {
fileCounter.inc(result.length) fileCounter.inc(result.length)
return result return result
}())) }()))
this._cache = cache this._cache = cache
this._manifests = manifests
this._hash = `${+new Date()}.${manifests.__hash__}`
this._dependencies = deps
this._oxManifests = viteToOxManifest(manifests)
logger.debug('cache warmed up') logger.debug('cache warmed up')
} }
...@@ -85,6 +95,22 @@ class FileCache { ...@@ -85,6 +95,22 @@ class FileCache {
get (path) { get (path) {
return this?._cache[path.slice(1)] || {} 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() export const fileCache = new FileCache()
import fetch from 'node-fetch' import fetch from 'node-fetch'
import path from 'path'
import { URL } from 'url' import { URL } from 'url'
import { fileCache } from './files.js' import { fileCache } from './files.js'
import { config } from './config.js' import { config } from './config.js'
import { hash } from './util.js' import { hash, viteManifestToDeps } from './util.js'
import { logger } from './logger.js' import { logger } from './logger.js'
export const loadViteManifests = (() => { export const loadViteManifests = (() => {
...@@ -74,81 +73,24 @@ export const loadViteManifests = (() => { ...@@ -74,81 +73,24 @@ export const loadViteManifests = (() => {
} }
})() })()
export function viteToOxManifest (viteManifests) { export async function getOxManifests () {
const deps = viteManifestToDeps(viteManifests) await loadViteManifests().catch(() => {})
return Object.values(viteManifests) return fileCache.oxManifests
.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 const getOxManifests = (() => { export async function getDependencies () {
let prevHash // simply catch the error here. This might happen, when one of the UI containers is temporarily not available
let oxManifestCache await loadViteManifests().catch(() => {})
return async function getOxManifests () { return fileCache.dependencies
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 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) { export async function getCSSDependenciesFor (file) {
const allDependencies = await getDependencies() const allDependencies = await getDependencies()
const dependencies = allDependencies[file] || [] const dependencies = allDependencies[file] || []
return dependencies.filter(dep => /\.css/i.test(dep)) return dependencies.filter(dep => /\.css/i.test(dep))
} }
export const getVersion = (() => { export async function getVersion () {
let prevHash await loadViteManifests().catch(() => {})
let versionString return fileCache.hash
return async function getVersion () { }
const viteManifest = await loadViteManifests().catch(() => {})
if (viteManifest && viteManifest.__hash__ !== prevHash) {
versionString = `${+new Date()}.${viteManifest.__hash__}`
prevHash = viteManifest.__hash__
}
return versionString
}
})()
...@@ -21,3 +21,36 @@ export function isJSFile (name) { ...@@ -21,3 +21,36 @@ export function isJSFile (name) {
const extname = path.extname(name) const extname = path.extname(name)
return extname === '.js' 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