fix: prioritize managed backup refreshes

Separate navigation and authoritative load generations so view refreshes cannot invalidate in-flight create or delete refreshes. Ignore navigation responses started during active operations, while allowing a later successful view load to replace old error state after the operation ends.
This commit is contained in:
Sucukdeluxe
2026-08-22 12:04:02 +02:00
parent 5ebeea3220
commit f7c6ee3bab
2 changed files with 121 additions and 21 deletions
+31 -8
View File
@@ -64,7 +64,10 @@ let uploading = false;
let healthCheckRunning = false; let healthCheckRunning = false;
let managedOnlineBackups = []; let managedOnlineBackups = [];
let managedOnlineBackupsAuthoritative = false; let managedOnlineBackupsAuthoritative = false;
let managedOnlineBackupLoadGeneration = 0; let managedOnlineBackupOperationGeneration = 0;
let managedOnlineBackupNavigationLoadGeneration = 0;
let managedOnlineBackupAuthoritativeLoadGeneration = 0;
let managedOnlineBackupActiveOperations = 0;
let onlineBackupStatusContextGeneration = 0; let onlineBackupStatusContextGeneration = 0;
const managedOnlineBackupOperationQueues = new Map(); const managedOnlineBackupOperationQueues = new Map();
@@ -2645,10 +2648,16 @@ function beginOnlineBackupStatusContext() {
} }
function beginManagedOnlineBackupOperation() { function beginManagedOnlineBackupOperation() {
managedOnlineBackupLoadGeneration++; managedOnlineBackupActiveOperations++;
managedOnlineBackupOperationGeneration++;
managedOnlineBackupNavigationLoadGeneration++;
return beginOnlineBackupStatusContext(); return beginOnlineBackupStatusContext();
} }
function endManagedOnlineBackupOperation() {
managedOnlineBackupActiveOperations = Math.max(0, managedOnlineBackupActiveOperations - 1);
}
function setOnlineBackupStatus(message, state = '', statusContext = null) { function setOnlineBackupStatus(message, state = '', statusContext = null) {
const context = statusContext === null ? beginOnlineBackupStatusContext() : statusContext; const context = statusContext === null ? beginOnlineBackupStatusContext() : statusContext;
if (context !== onlineBackupStatusContextGeneration) return false; if (context !== onlineBackupStatusContextGeneration) return false;
@@ -2738,13 +2747,20 @@ function renderManagedOnlineBackups() {
} }
async function loadManagedOnlineBackups({ statusContext = null } = {}) { async function loadManagedOnlineBackups({ statusContext = null } = {}) {
const generation = ++managedOnlineBackupLoadGeneration; const authoritative = statusContext !== null;
const status = document.getElementById('onlineBackupStatus'); const operationGeneration = managedOnlineBackupOperationGeneration;
const loaderStatusContext = statusContext === null ? onlineBackupStatusContextGeneration : statusContext; const startedDuringOperation = managedOnlineBackupActiveOperations > 0;
const mayUpdateStatus = statusContext !== null || !status || (!status.textContent && !status.dataset.state); const generation = authoritative
? ++managedOnlineBackupAuthoritativeLoadGeneration
: ++managedOnlineBackupNavigationLoadGeneration;
const loaderStatusContext = authoritative || !startedDuringOperation ? statusContext ?? beginOnlineBackupStatusContext() : null;
const mayUpdateStatus = loaderStatusContext !== null;
try { try {
const result = await window.api.listManagedOnlineBackups(); const result = await window.api.listManagedOnlineBackups();
if (generation !== managedOnlineBackupLoadGeneration) return false; const stale = authoritative
? generation !== managedOnlineBackupAuthoritativeLoadGeneration || operationGeneration !== managedOnlineBackupOperationGeneration
: generation !== managedOnlineBackupNavigationLoadGeneration || operationGeneration !== managedOnlineBackupOperationGeneration || startedDuringOperation;
if (stale) return false;
if (!result?.ok || !Array.isArray(result.entries)) { if (!result?.ok || !Array.isArray(result.entries)) {
invalidateManagedOnlineBackups(); invalidateManagedOnlineBackups();
if (mayUpdateStatus) setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error', loaderStatusContext); if (mayUpdateStatus) setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error', loaderStatusContext);
@@ -2754,7 +2770,10 @@ async function loadManagedOnlineBackups({ statusContext = null } = {}) {
if (mayUpdateStatus) setOnlineBackupStatus('', '', loaderStatusContext); if (mayUpdateStatus) setOnlineBackupStatus('', '', loaderStatusContext);
return true; return true;
} catch { } catch {
if (generation === managedOnlineBackupLoadGeneration) { const current = authoritative
? generation === managedOnlineBackupAuthoritativeLoadGeneration && operationGeneration === managedOnlineBackupOperationGeneration
: generation === managedOnlineBackupNavigationLoadGeneration && operationGeneration === managedOnlineBackupOperationGeneration && !startedDuringOperation;
if (current) {
invalidateManagedOnlineBackups(); invalidateManagedOnlineBackups();
if (mayUpdateStatus) setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error', loaderStatusContext); if (mayUpdateStatus) setOnlineBackupStatus('Online-Sicherungen konnten nicht geladen werden', 'error', loaderStatusContext);
} }
@@ -2774,6 +2793,7 @@ function enqueueManagedOnlineBackupOperation(id, action, operation) {
managedOnlineBackupOperationQueues.set(id, state); managedOnlineBackupOperationQueues.set(id, state);
renderManagedOnlineBackups(); renderManagedOnlineBackups();
return promise.finally(() => { return promise.finally(() => {
endManagedOnlineBackupOperation();
if (managedOnlineBackupOperationQueues.get(id) !== state) return; if (managedOnlineBackupOperationQueues.get(id) !== state) return;
managedOnlineBackupOperationQueues.delete(id); managedOnlineBackupOperationQueues.delete(id);
renderManagedOnlineBackups(); renderManagedOnlineBackups();
@@ -2809,6 +2829,7 @@ async function deleteManagedOnlineBackup(entry) {
if (!hasManagedOnlineBackup(id)) { if (!hasManagedOnlineBackup(id)) {
const statusContext = beginManagedOnlineBackupOperation(); const statusContext = beginManagedOnlineBackupOperation();
setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error', statusContext); setOnlineBackupStatus('Online-Sicherung konnte nicht gelöscht werden', 'error', statusContext);
endManagedOnlineBackupOperation();
return; return;
} }
if (managedOnlineBackupOperationQueues.get(id)?.action === 'delete') { if (managedOnlineBackupOperationQueues.get(id)?.action === 'delete') {
@@ -2844,6 +2865,7 @@ async function doOnlineBackupCreate() {
openOnlineBackupView(); openOnlineBackupView();
const statusContext = beginManagedOnlineBackupOperation(); const statusContext = beginManagedOnlineBackupOperation();
setOnlineBackupStatus('Nicht alle Einstellungen konnten gespeichert werden', 'error', statusContext); setOnlineBackupStatus('Nicht alle Einstellungen konnten gespeichert werden', 'error', statusContext);
endManagedOnlineBackupOperation();
doOnlineBackupCreate.busy = false; doOnlineBackupCreate.busy = false;
return; return;
} }
@@ -2863,6 +2885,7 @@ async function doOnlineBackupCreate() {
} catch { } catch {
setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error', statusContext); setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error', statusContext);
} finally { } finally {
endManagedOnlineBackupOperation();
if (createButton?.isConnected) createButton.disabled = false; if (createButton?.isConnected) createButton.disabled = false;
doOnlineBackupCreate.busy = false; doOnlineBackupCreate.busy = false;
} }
+90 -13
View File
@@ -79,9 +79,15 @@ const managedOnlineBackupListResponses = [
] }, ] },
{ ok: false, error: 'Online-Sicherungen konnten nicht geladen werden' } { ok: false, error: 'Online-Sicherungen konnten nicht geladen werden' }
]; ];
const managedOnlineBackupAdditionalListResponses = new Map([
[6, { ok: true, entries: [{ id: 'KKKKKKKKKKKKKKKKKKKKKK', displayKey: 'MHU2-KLMN…1111', createdAt: '2026-08-30T08:00:00.000Z' }] }],
[8, { ok: true, entries: [{ id: 'NNNNNNNNNNNNNNNNNNNNNN', displayKey: 'MHU2-NMNB…2222', createdAt: '2026-08-30T10:00:00.000Z' }] }],
[9, { ok: true, entries: [{ id: 'PPPPPPPPPPPPPPPPPPPPPP', displayKey: 'MHU2-POIU…3333', createdAt: '2026-08-30T11:00:00.000Z' }] }],
[10, { ok: true, entries: [{ id: 'JJJJJJJJJJJJJJJJJJJJJJ', displayKey: 'MHU2-JKLO…4444', createdAt: '2026-08-30T12:00:00.000Z' }] }]
]);
let managedOnlineBackupListIndex = 0; let managedOnlineBackupListIndex = 0;
let managedOnlineBackupCreateIndex = 0; let managedOnlineBackupCreateIndex = 0;
let pendingManagedOnlineBackupList = null; const pendingManagedOnlineBackupLists = new Map();
let pendingManagedOnlineBackupCopy = null; let pendingManagedOnlineBackupCopy = null;
let pendingManagedOnlineBackupDelete = null; let pendingManagedOnlineBackupDelete = null;
contextBridge.exposeInMainWorld('api', { contextBridge.exposeInMainWorld('api', {
@@ -92,22 +98,23 @@ contextBridge.exposeInMainWorld('api', {
listManagedOnlineBackups() { listManagedOnlineBackups() {
managedOnlineBackupListIndex++; managedOnlineBackupListIndex++;
managedOnlineBackupProbeCalls.push(['list', managedOnlineBackupListIndex]); managedOnlineBackupProbeCalls.push(['list', managedOnlineBackupListIndex]);
if (managedOnlineBackupListIndex === 5) { if (managedOnlineBackupListIndex === 5 || managedOnlineBackupListIndex === 7) {
return new Promise(resolve => { pendingManagedOnlineBackupList = resolve; }); return new Promise(resolve => { pendingManagedOnlineBackupLists.set(managedOnlineBackupListIndex, resolve); });
} }
return Promise.resolve(managedOnlineBackupListResponses[managedOnlineBackupListIndex - 1]); return Promise.resolve(managedOnlineBackupListResponses[managedOnlineBackupListIndex - 1] || managedOnlineBackupAdditionalListResponses.get(managedOnlineBackupListIndex));
}, },
releaseManagedOnlineBackupList() { releaseManagedOnlineBackupList(index) {
const resolve = pendingManagedOnlineBackupList; const resolve = pendingManagedOnlineBackupLists.get(index);
pendingManagedOnlineBackupList = null; pendingManagedOnlineBackupLists.delete(index);
resolve({ ok: true, entries: [ const entry = index === 5
{ id: 'XXXXXXXXXXXXXXXXXXXXXX', displayKey: 'MHU2-XCVB…9753', createdAt: '2026-08-28T12:00:00.000Z' } ? { id: 'XXXXXXXXXXXXXXXXXXXXXX', displayKey: 'MHU2-XCVB…9753', createdAt: '2026-08-28T12:00:00.000Z' }
] }); : { id: 'HHHHHHHHHHHHHHHHHHHHHH', displayKey: 'MHU2-HJKL…8642', createdAt: '2026-08-30T09:00:00.000Z' };
resolve({ ok: true, entries: [entry] });
}, },
createManagedOnlineBackup() { createManagedOnlineBackup() {
managedOnlineBackupCreateIndex++; managedOnlineBackupCreateIndex++;
managedOnlineBackupProbeCalls.push(['create', managedOnlineBackupCreateIndex]); managedOnlineBackupProbeCalls.push(['create', managedOnlineBackupCreateIndex]);
if (managedOnlineBackupCreateIndex === 3) return Promise.resolve({ ok: false, error: 'Online-Sicherung konnte nicht erstellt werden' }); if (managedOnlineBackupCreateIndex === 3 || managedOnlineBackupCreateIndex === 5) return Promise.resolve({ ok: false, error: 'Online-Sicherung konnte nicht erstellt werden' });
const entry = managedOnlineBackupCreateIndex === 1 const entry = managedOnlineBackupCreateIndex === 1
? { id: 'CCCCCCCCCCCCCCCCCCCCCC', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' } ? { id: 'CCCCCCCCCCCCCCCCCCCCCC', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' }
: { id: 'FFFFFFFFFFFFFFFFFFFFFF', displayKey: 'MHU2-FGHJ…8642', createdAt: '2026-08-27T12:00:00.000Z' }; : { id: 'FFFFFFFFFFFFFFFFFFFFFF', displayKey: 'MHU2-FGHJ…8642', createdAt: '2026-08-27T12:00:00.000Z' };
@@ -206,13 +213,48 @@ contextBridge.exposeInMainWorld('api', {
text: document.getElementById('onlineBackupStatus').textContent, text: document.getElementById('onlineBackupStatus').textContent,
state: document.getElementById('onlineBackupStatus').dataset.state state: document.getElementById('onlineBackupStatus').dataset.state
}; };
window.api.releaseManagedOnlineBackupList(); window.api.releaseManagedOnlineBackupList(5);
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
const staleNavigationResponse = { const staleNavigationResponse = {
keys: [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent), keys: [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent),
status: document.getElementById('onlineBackupStatus').textContent, status: document.getElementById('onlineBackupStatus').textContent,
statusState: document.getElementById('onlineBackupStatus').dataset.state statusState: document.getElementById('onlineBackupStatus').dataset.state
}; };
const concurrentCreate = doOnlineBackupCreate();
await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length === 7);
const statusBeforeConcurrentNavigation = {
text: document.getElementById('onlineBackupStatus').textContent,
state: document.getElementById('onlineBackupStatus').dataset.state
};
navigation.click();
await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length === 8);
await new Promise(resolve => setTimeout(resolve, 0));
const concurrentNavigationState = {
keys: [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent),
status: document.getElementById('onlineBackupStatus').textContent,
statusState: document.getElementById('onlineBackupStatus').dataset.state
};
window.api.releaseManagedOnlineBackupList(7);
await concurrentCreate;
const authoritativeCreateResult = {
keys: [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent),
status: document.getElementById('onlineBackupStatus').textContent,
statusState: document.getElementById('onlineBackupStatus').dataset.state
};
const failedCreateBeforeNavigation = doOnlineBackupCreate();
await failedCreateBeforeNavigation;
const completedOperationError = {
text: document.getElementById('onlineBackupStatus').textContent,
state: document.getElementById('onlineBackupStatus').dataset.state
};
navigation.click();
await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length === 10);
await new Promise(resolve => setTimeout(resolve, 0));
const navigationAfterOperationError = {
keys: [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent),
status: document.getElementById('onlineBackupStatus').textContent,
statusState: document.getElementById('onlineBackupStatus').dataset.state
};
const calls = await window.api.getManagedOnlineBackupProbeCalls(); const calls = await window.api.getManagedOnlineBackupProbeCalls();
return { return {
initialKeys, initialKeys,
@@ -232,6 +274,11 @@ contextBridge.exposeInMainWorld('api', {
refreshFailure, refreshFailure,
statusBeforeStaleNavigationResponse, statusBeforeStaleNavigationResponse,
staleNavigationResponse, staleNavigationResponse,
statusBeforeConcurrentNavigation,
concurrentNavigationState,
authoritativeCreateResult,
completedOperationError,
navigationAfterOperationError,
confirmation, confirmation,
calls, calls,
secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent) secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent)
@@ -401,6 +448,29 @@ app.whenReady().then(async () => {
status: 'Online backup could not be created', status: 'Online backup could not be created',
statusState: 'error' statusState: 'error'
}); });
assert.deepEqual(result.onlineBackupBehavior.statusBeforeConcurrentNavigation, {
text: 'Encrypting and saving settings…',
state: 'busy'
});
assert.deepEqual(result.onlineBackupBehavior.concurrentNavigationState, {
keys: ['MHU2-GHJK…7531'],
status: 'Encrypting and saving settings…',
statusState: 'busy'
});
assert.deepEqual(result.onlineBackupBehavior.authoritativeCreateResult, {
keys: ['MHU2-HJKL…8642'],
status: 'New key created. Older keys remain valid.',
statusState: 'success'
});
assert.deepEqual(result.onlineBackupBehavior.completedOperationError, {
text: 'Online backup could not be created',
state: 'error'
});
assert.deepEqual(result.onlineBackupBehavior.navigationAfterOperationError, {
keys: ['MHU2-JKLO…4444'],
status: '',
statusState: ''
});
assert.equal(result.onlineBackupBehavior.afterCreate.length, 3); assert.equal(result.onlineBackupBehavior.afterCreate.length, 3);
assert.deepEqual(result.onlineBackupBehavior.confirmation, { assert.deepEqual(result.onlineBackupBehavior.confirmation, {
title: 'Online-Backup löschen', title: 'Online-Backup löschen',
@@ -418,7 +488,14 @@ app.whenReady().then(async () => {
['create', 2], ['create', 2],
['list', 4], ['list', 4],
['list', 5], ['list', 5],
['create', 3] ['create', 3],
['list', 6],
['create', 4],
['list', 7],
['list', 8],
['list', 9],
['create', 5],
['list', 10]
]); ]);
assert.equal(result.onlineBackupBehavior.secretInBody, false); assert.equal(result.onlineBackupBehavior.secretInBody, false);
assert.ok(Math.abs(result.onlineBackupLayout.german.createRight - result.onlineBackupLayout.german.contentRight) <= 1); assert.ok(Math.abs(result.onlineBackupLayout.german.createRight - result.onlineBackupLayout.german.contentRight) <= 1);