From d213940d2e4c37588fcf810860f5878054be9de2 Mon Sep 17 00:00:00 2001
From: Richard Petersen <richard.petersen@open-xchange.com>
Date: Mon, 28 Feb 2022 14:22:56 +0100
Subject: [PATCH] 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
---
 spec/dependencies_test.js     |  2 +-
 spec/manifest_parsing_test.js |  2 +-
 spec/server_test.js           | 46 +++++++++++++++++---
 src/createApp.js              |  3 +-
 src/files.js                  | 28 +++++++++++-
 src/manifests.js              | 82 +++++------------------------------
 src/util.js                   | 33 ++++++++++++++
 7 files changed, 117 insertions(+), 79 deletions(-)

diff --git a/spec/dependencies_test.js b/spec/dependencies_test.js
index 8eedefa..f7b109a 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 89f980f..bed5ecd 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 a29dd3e..08c4afa 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 4841e0e..9ca9690 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 c264499..d7b4af4 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 07c6174..6801ea1 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 55c8043..018a517 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()
+}
-- 
GitLab