BillTracker/services/updateCheckService.js

117 lines
3.6 KiB
JavaScript
Raw Normal View History

2026-05-14 21:00:07 -05:00
/**
* Checks the Forgejo repo for newer releases and compares against the running
* package.json version. Results are cached in memory (1 hour for success,
* 5 minutes for errors) so the status page stays fast under load.
*/
const REPO_API_BASE = process.env.REPO_API_URL
|| 'https://dream.scheller.ltd/api/v1/repos/null/BillTracker';
const TTL_OK_MS = 60 * 60 * 1000; // 1 hour on success
const TTL_ERROR_MS = 5 * 60 * 1000; // 5 min on error (avoid hammering)
const FETCH_TIMEOUT_MS = 8_000;
let _cache = { result: null, expiresAt: 0 };
let _pkg = null;
function getCurrentVersion() {
if (!_pkg) {
try { _pkg = require('../package.json'); } catch { _pkg = { version: '0.0.0' }; }
}
return _pkg.version;
}
// Returns positive if a > b, negative if a < b, 0 if equal.
function compareVersions(a, b) {
const parse = v => String(v).replace(/^v/, '').split('.').map(Number);
const pa = parse(a), pb = parse(b);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const diff = (pa[i] || 0) - (pb[i] || 0);
if (diff !== 0) return diff;
}
return 0;
}
/**
* @param {boolean} force Skip the cache and always hit the API.
* @returns {Promise<Object>} Update status object.
*/
async function checkForUpdates(force = false) {
const now = Date.now();
if (!force && _cache.result && now < _cache.expiresAt) {
return { ..._cache.result, cached: true };
}
const currentVersion = getCurrentVersion();
const checkedAt = new Date().toISOString();
try {
const res = await fetch(`${REPO_API_BASE}/releases/latest`, {
headers: {
'Accept': 'application/json',
'User-Agent': `BillTracker/${currentVersion} UpdateCheck`,
},
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});
// 404 = no releases published yet — treat as up-to-date
if (res.status === 404) {
const result = {
current_version: currentVersion,
latest_version: null,
up_to_date: true,
has_update: false,
latest_release_url: null,
published_at: null,
last_checked_at: checkedAt,
error: null,
cached: false,
};
_cache = { result, expiresAt: now + TTL_OK_MS };
return result;
}
if (!res.ok) throw new Error(`Forgejo API returned HTTP ${res.status}`);
const release = await res.json();
const rawTag = release.tag_name || '';
const latestVersion = rawTag.replace(/^v/, '') || null;
const hasUpdate = latestVersion
? compareVersions(currentVersion, latestVersion) < 0
: false;
const result = {
current_version: currentVersion,
latest_version: latestVersion,
up_to_date: !hasUpdate,
has_update: hasUpdate,
latest_release_url: release.html_url || null,
published_at: release.published_at || null,
last_checked_at: checkedAt,
error: null,
cached: false,
};
_cache = { result, expiresAt: now + TTL_OK_MS };
return result;
} catch (err) {
const result = {
current_version: currentVersion,
latest_version: null,
up_to_date: null, // unknown — network or API failure
has_update: false,
latest_release_url: null,
published_at: null,
last_checked_at: checkedAt,
error: err.message || 'Update check failed',
cached: false,
};
_cache = { result, expiresAt: now + TTL_ERROR_MS };
return result;
}
}
module.exports = { checkForUpdates };