import { configMap } from './configMap.js' import { getRedisKey, hash } from './util.js' import { logger } from './logger.js' import * as cache from './cache.js' import * as redis from './redis.js' import { Gauge } from 'prom-client' import { getViteManifests } from './manifests.js' let latestVersion const manifestFileEntriesGauge = new Gauge({ name: 'manifest_file_entries', help: 'Number of entries in merged vite manifest (number of all known files)', async collect () { const version = latestVersion this.set({ version }, Object.keys(await getViteManifests({ version })).length) }, labelNames: ['version'] }) const versionUpdateGauge = new Gauge({ name: 'version_update_event', help: 'Timestamp of a version update event', labelNames: ['version'] }) export const fetchLatestVersion = async () => { const infos = await Promise.all(configMap.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.warn(`[Version] 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(`[Version] Cannot fetch manifest from ${baseUrl}. Version info will not be correct.`) } })) return `${hash(infos)}${configMap.salt ? `-${configMap.salt}` : ''}` } export async function getLatestVersion () { if (latestVersion) return latestVersion if (redis.isEnabled()) { const version = await redis.client.get(getRedisKey({ name: 'latestVersion' })) if (version) { logger.info(`[Version] Got initial version from redis: '${version}'`) versionUpdateGauge.setToCurrentTime({ version }) return (latestVersion = version) } } await configMap.load() const version = await fetchLatestVersion() logger.info(`[Version] Fetched initial version: '${version}'`) if (redis.isEnabled()) { redis.pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), version) await redis.client.set(getRedisKey({ name: 'latestVersion' }), version) } return (latestVersion = version) } export function registerLatestVersionListener (client) { if (!redis.isEnabled()) return const key = getRedisKey({ name: 'updateLatestVersion' }) client.subscribe(key, (errs, count) => logger.info(`[Redis] Subscribed to ${key}.`)) client.on('message', async (channel, version) => { if (channel !== key) return logger.info(`[Version] Received 'updateLatestVersion' event. Clearing cache. New version: '${version}'`) await configMap.load() versionUpdateGauge.setToCurrentTime({ version }) cache.clear() latestVersion = version }) } export async function updateVersionProcessor () { logger.info('[Version] Check for new version') await configMap.load() const [storedVersion, fetchedVersion] = await Promise.all([ getLatestVersion(), fetchLatestVersion() ]) // don't wait for the data, can be done in background getViteManifests({ version: fetchedVersion }).then(manifests => { manifestFileEntriesGauge.set({ version: fetchedVersion }, Object.keys(manifests).length) }) if (storedVersion === fetchedVersion) { logger.info(`[Version] No new version has been found. No update needed. Current version: ${storedVersion}`) return fetchedVersion } logger.info(`[Version] Found new source version. Current version: '${storedVersion}', new version: '${fetchedVersion}'`) if (redis.isEnabled()) { const prevProcessedVersion = await redis.client.get(getRedisKey({ name: 'prevProcessedVersion' })) // that means, that between the previous update processing and this one, there was no version change if (prevProcessedVersion === fetchedVersion) { logger.info('[Version] publish update to other nodes.') redis.pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), fetchedVersion) await redis.client.set(getRedisKey({ name: 'latestVersion' }), fetchedVersion) latestVersion = fetchedVersion } else { logger.info(`[Version] do not execute update yet. Store version ${fetchedVersion} as previous version.`) await redis.client.set(getRedisKey({ name: 'prevProcessedVersion' }), fetchedVersion) } } else { versionUpdateGauge.setToCurrentTime({ version: latestVersion }) // if redis is disabled, this will only be trigger by a setInterval and not from a redis event logger.info('[Version] Clear local cache due to version update.') cache.clear() latestVersion = fetchedVersion } return latestVersion }