From 32b50eb5a5270f403d73a8bf29365008b30ab3e7 Mon Sep 17 00:00:00 2001
From: Richard Petersen <richard.petersen@open-xchange.com>
Date: Mon, 14 Mar 2022 17:14:45 +0100
Subject: [PATCH] Improve the version detection

---
 spec/headers_test.js | 73 ++++++++++++++++++++++++++++++++-
 src/createApp.js     |  2 +-
 src/manifests.js     | 96 +++++++++++++++++++++++++++-----------------
 3 files changed, 132 insertions(+), 39 deletions(-)

diff --git a/spec/headers_test.js b/spec/headers_test.js
index f2122fd..f499f03 100644
--- a/spec/headers_test.js
+++ b/spec/headers_test.js
@@ -37,7 +37,7 @@ describe('Responses contain custom headers', function () {
   it('index.html has version', async function () {
     const response = await request(app).get('/index.html')
     expect(response.statusCode).to.equal(200)
-    expect(response.headers.version).to.equal('3038606729')
+    expect(response.headers.version).to.equal('3215668592')
   })
 
   it('javascript file contains dependencies', async function () {
@@ -45,4 +45,75 @@ describe('Responses contain custom headers', function () {
     expect(response.statusCode).to.equal(200)
     expect(response.headers.dependencies).to.equal('main.css')
   })
+
+  describe('with different files', function () {
+    beforeEach(async function () {
+      td.reset()
+      mockConfig({ urls: ['http://ui-server/'] })
+      mockFetch({
+        'http://ui-server': {
+          '/manifest.json': generateSimpleViteManifest({
+            'index.html': {}
+          }),
+          '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } })
+        }
+      })
+      app = await mockApp()
+    })
+
+    it('index.html has version', async function () {
+      const response = await request(app).get('/index.html')
+      expect(response.statusCode).to.equal(200)
+      // important here is, that it is different than in the test without meta.json
+      expect(response.headers.version).to.equal('3961519424')
+    })
+  })
+
+  describe('with meta.json', function () {
+    beforeEach(async function () {
+      td.reset()
+      mockConfig({ urls: ['http://ui-server/'] })
+      mockFetch({
+        'http://ui-server': {
+          '/manifest.json': generateSimpleViteManifest({
+            'index.html': {}
+          }),
+          '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
+          '/meta.json': { commitSha: '1234567890' }
+        }
+      })
+      app = await mockApp()
+    })
+
+    it('index.html has version', async function () {
+      const response = await request(app).get('/index.html')
+      expect(response.statusCode).to.equal(200)
+      // important here is, that it is different than in the test without meta.json
+      expect(response.headers.version).to.equal('1487554813')
+    })
+  })
+
+  describe('with different meta.json', function () {
+    beforeEach(async function () {
+      td.reset()
+      mockConfig({ urls: ['http://ui-server/'] })
+      mockFetch({
+        'http://ui-server': {
+          '/manifest.json': generateSimpleViteManifest({
+            'index.html': {}
+          }),
+          '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
+          '/meta.json': { commitSha: '0987654321' }
+        }
+      })
+      app = await mockApp()
+    })
+
+    it('index.html has version', async function () {
+      const response = await request(app).get('/index.html')
+      expect(response.statusCode).to.equal(200)
+      // important here is, that it is different than in the test without meta.json
+      expect(response.headers.version).to.equal('319344871')
+    })
+  })
 })
diff --git a/src/createApp.js b/src/createApp.js
index e6dcb38..f6998f6 100644
--- a/src/createApp.js
+++ b/src/createApp.js
@@ -42,7 +42,7 @@ export function createApp () {
   const startupCheck = new health.StartupCheck('warmup cache', async function () {
     const stopTimer = startUpTimeGauge.startTimer()
     try {
-      const viteManifests = await loadViteManifests()
+      const viteManifests = await loadViteManifests({ warmUp: false })
       // also need to load ox manifests here, to make sure the cache is warm
       await getOxManifests()
       const deps = viteManifestToDeps(viteManifests)
diff --git a/src/manifests.js b/src/manifests.js
index 6801ea1..1598854 100644
--- a/src/manifests.js
+++ b/src/manifests.js
@@ -6,17 +6,42 @@ import { hash, viteManifestToDeps } from './util.js'
 import { logger } from './logger.js'
 
 export const loadViteManifests = (() => {
-  let cachePromise
+  let lastManifest
   let lastCacheTime
   let lastHash
 
+  async function getHash () {
+    await config.load()
+    const infos = await Promise.all(config.urls.map(async baseUrl => {
+      try {
+        const response = await fetch(new URL('meta.json', baseUrl))
+        if (!response.ok) throw new Error()
+        const meta = await response.json()
+        const version = meta.commitSha || meta.buildDate || meta.version
+        if (!version) throw new Error()
+        return version
+      } catch (err) {
+        logger.debug(`UI container at ${baseUrl} does not have meta.json. Fall back to version hash based on manifest.`)
+      }
+      try {
+        const response = await fetch(new URL('manifest.json', baseUrl))
+        if (!response.ok) throw new Error()
+        const manifest = await response.json()
+        return hash(manifest)
+      } catch (err) {
+        logger.error(`Cannot fetch manifest from ${baseUrl}. Version info will not be correct.`)
+      }
+    }))
+    return hash(infos)
+  }
+
   async function reload () {
     await config.load()
     // vite manifests contains a set of objects with the vite-manifests
     // from the corresponding registered services
     const viteManifests = await Promise.all(config.urls.map(async baseUrl => {
       // fetch the manifests
-      const result = await fetch(new URL('/manifest.json', baseUrl))
+      const result = await fetch(new URL('manifest.json', baseUrl))
       if (!result.ok) throw new Error(`Failed to load manifest for url ${result.url} (Status: ${result.status}: ${result.statusText})`)
       try {
         const manifest = await result.json()
@@ -32,55 +57,52 @@ export const loadViteManifests = (() => {
     }))
 
     // combine all manifests by keys. With duplicates, last wins
-    const viteManifest = viteManifests.reduce((memo, manifest) => Object.assign(memo, manifest), {})
-    Object.defineProperty(viteManifest, '__hash__', {
-      enumerable: false,
-      writable: true
-    })
-
-    try {
-      viteManifest.__hash__ = hash(viteManifests)
-    } catch (err) {
-      logger.error(`Failed to calculate hash: ${err.message}`)
-    }
-
-    return viteManifest
+    return viteManifests.reduce((memo, manifest) => Object.assign(memo, manifest), {})
   }
 
-  return function loadViteManifests ({ useCache = true } = {}) {
+  return function loadViteManifests ({ useCache = true, warmUp = true } = {}) {
     const CACHE_TTL = parseInt(process.env.CACHE_TTL)
     const timeElapsed = () => +((+new Date() - lastCacheTime) / 1000).toFixed(2)
 
-    if (!cachePromise || useCache === false || +new Date() > lastCacheTime + CACHE_TTL) {
-      cachePromise = reload()
-      cachePromise.then(manifests => {
+    if (!lastManifest || useCache === false || +new Date() > lastCacheTime + CACHE_TTL) {
+      const promise = (async () => {
+        const newHash = await getHash()
+        if (lastManifest && lastHash === newHash) return
+        const manifest = await reload()
         if (useCache) logger.info(`reloaded manifests after ${timeElapsed()} seconds`)
-        // update cache promise
-        const newHash = manifests.__hash__
-        if (newHash !== lastHash) {
-          if (lastHash) {
-            const deps = viteManifestToDeps(manifests)
-            // asynchronously rewarm the cache
-            fileCache.warmUp(manifests, deps)
-          }
 
-          lastHash = newHash
+        // cache data
+        lastHash = newHash
+        lastManifest = manifest
+
+        if (warmUp) {
+          const deps = viteManifestToDeps(manifest)
+          // asynchronously rewarm the cache
+          fileCache.warmUp(manifest, deps)
         }
-      })
+
+        Object.defineProperty(manifest, '__hash__', {
+          enumerable: false,
+          writable: true,
+          value: newHash
+        })
+        return manifest
+      })().catch(err => logger.error(`Could not reload manifests: ${err}`))
+      lastManifest = lastManifest || promise
+
       lastCacheTime = +new Date()
     }
-    return cachePromise
+    return lastManifest
   }
 })()
 
-export async function getOxManifests () {
-  await loadViteManifests().catch(() => {})
+export function getOxManifests () {
+  loadViteManifests()
   return fileCache.oxManifests
 }
 
-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(() => {})
+export function getDependencies () {
+  loadViteManifests()
   return fileCache.dependencies
 }
 
@@ -90,7 +112,7 @@ export async function getCSSDependenciesFor (file) {
   return dependencies.filter(dep => /\.css/i.test(dep))
 }
 
-export async function getVersion () {
-  await loadViteManifests().catch(() => {})
+export function getVersion () {
+  loadViteManifests()
   return fileCache.hash
 }
-- 
GitLab