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:
+31
-8
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user