Refresh release metadata immediately before downloading an update and keep automatic checks current while the app remains open. Accept desktop file drops from the first renderer frame, preserve early and rapid drops until initialization completes, and show the live remaining upload size in the sidebar. Add regression coverage for update freshness, startup drag-and-drop ordering, localization, and remaining-byte retry behavior. Bump the application to v2.1.25.
This commit is contained in:
@@ -16,7 +16,28 @@
|
||||
};
|
||||
}
|
||||
|
||||
const api = { createSerializedRunner };
|
||||
function createReadySerializedRunner(task) {
|
||||
let ready = false;
|
||||
let releaseReady;
|
||||
const readyPromise = new Promise((resolve) => { releaseReady = resolve; });
|
||||
const runner = createSerializedRunner(async (...args) => {
|
||||
await readyPromise;
|
||||
return task(...args);
|
||||
});
|
||||
return {
|
||||
run: (...args) => runner.run(...args),
|
||||
flush: () => runner.flush(),
|
||||
ready() {
|
||||
if (ready) return false;
|
||||
ready = true;
|
||||
releaseReady();
|
||||
return true;
|
||||
},
|
||||
get isReady() { return ready; }
|
||||
};
|
||||
}
|
||||
|
||||
const api = { createSerializedRunner, createReadySerializedRunner };
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||||
else if (root) root.SerializedRunner = api;
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
|
||||
+48
-12
@@ -17,6 +17,27 @@ const CACHE_TTL = 10 * 60 * 1000; // 10 min
|
||||
let activeAbort = null;
|
||||
const launchedInstallerPaths = new Set();
|
||||
|
||||
function createUpdateAnnouncementState() {
|
||||
let announcedVersion = '';
|
||||
|
||||
function versionOf(update) {
|
||||
return String(update?.remoteVersion || '').replace(/^v/i, '').trim();
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
canAnnounce(update, rendererReady) {
|
||||
const version = versionOf(update);
|
||||
return Boolean(rendererReady && update?.available && version && version !== announcedVersion);
|
||||
},
|
||||
markAnnounced(update) {
|
||||
announcedVersion = versionOf(update);
|
||||
},
|
||||
reset() {
|
||||
announcedVersion = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentVersion() {
|
||||
return app.getVersion();
|
||||
}
|
||||
@@ -61,17 +82,32 @@ function findLatestYml(assets) {
|
||||
return assets.find(a => /^latest\.yml$/i.test(a.name)) || null;
|
||||
}
|
||||
|
||||
async function fetchJson(url, signal) {
|
||||
function cacheBustedUrl(url) {
|
||||
const parsed = new URL(url);
|
||||
parsed.searchParams.set('_mhu_update', crypto.randomUUID());
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
async function fetchJson(url, signal, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT);
|
||||
const onAbort = () => controller.abort();
|
||||
const forceRefresh = options.forceRefresh === true;
|
||||
const fetchImpl = options.fetchImpl || fetch;
|
||||
if (signal) signal.addEventListener('abort', onAbort);
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
const res = await fetchImpl(forceRefresh ? cacheBustedUrl(url) : url, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
redirect: 'follow'
|
||||
redirect: 'follow',
|
||||
...(forceRefresh ? {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store',
|
||||
Pragma: 'no-cache'
|
||||
}
|
||||
} : {})
|
||||
});
|
||||
const text = await res.text();
|
||||
try {
|
||||
@@ -112,13 +148,14 @@ async function fetchGithubReleaseNotes(remoteVersion, fallback = '', fetchImpl =
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdate() {
|
||||
// Return cached result if fresh
|
||||
if (cachedCheck && (Date.now() - cachedCheckTs) < CACHE_TTL) {
|
||||
async function checkForUpdate(options = {}) {
|
||||
const forceRefresh = options.forceRefresh === true;
|
||||
const fetchImpl = options.fetchImpl || fetch;
|
||||
if (!forceRefresh && cachedCheck && (Date.now() - cachedCheckTs) < CACHE_TTL) {
|
||||
return cachedCheck;
|
||||
}
|
||||
|
||||
const releases = await fetchJson(API_URL);
|
||||
const releases = await fetchJson(API_URL, undefined, { forceRefresh, fetchImpl });
|
||||
|
||||
if (!Array.isArray(releases) || releases.length === 0) {
|
||||
return { available: false };
|
||||
@@ -142,7 +179,7 @@ async function checkForUpdate() {
|
||||
return { available: false, reason: 'Kein Setup-Asset im Release gefunden' };
|
||||
}
|
||||
|
||||
const releaseNotes = await fetchGithubReleaseNotes(remoteVersion, release.body || '');
|
||||
const releaseNotes = await fetchGithubReleaseNotes(remoteVersion, release.body || '', fetchImpl);
|
||||
cachedCheck = {
|
||||
available: true,
|
||||
currentVersion,
|
||||
@@ -210,10 +247,9 @@ async function prepareUpdate(onProgress, options = {}) {
|
||||
// Stage: starting
|
||||
if (onProgress) onProgress({ stage: 'starting', percent: 0 });
|
||||
|
||||
// Check or use cached
|
||||
let check = options.checkResult || cachedCheck;
|
||||
let check = options.checkResult || null;
|
||||
if (!check || !check.available) {
|
||||
check = await checkForUpdate();
|
||||
check = await checkForUpdate({ forceRefresh: true, fetchImpl });
|
||||
}
|
||||
if (!check || !check.available) {
|
||||
throw new Error('Kein Update verfügbar');
|
||||
@@ -353,4 +389,4 @@ function abortUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { checkForUpdate, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate, abortUpdate, isNewer, resolveReleaseVersion, pickSetupAsset, parseLatestYml };
|
||||
module.exports = { checkForUpdate, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate, abortUpdate, isNewer, resolveReleaseVersion, pickSetupAsset, parseLatestYml, createUpdateAnnouncementState };
|
||||
|
||||
Reference in New Issue
Block a user