frontend: ignore stale local-match refresh responses after workflow switch

This commit is contained in:
Dariusz L
2026-09-10 00:51:19 +02:00
parent df90db5431
commit 2d1bc22909
2 changed files with 87 additions and 8 deletions
@@ -6265,6 +6265,77 @@ test('download snapshots with identical model keys remain separated by workflow'
);
});
function createLocalRefreshRaceDialog() {
const dialog = {
scope: 'workflow-a',
missingModels: [],
getCurrentDownloadWorkflowScopeIdentity() { return this.scope; },
getMissingModelKey(missing) { return missing.key; },
getDownloadedLocalMatchTarget(missing, filename) { return filename; },
getMissingModelDomKey(missing) { return missing.key; },
getWorkflowScopedQueueKey() { return this.scope; },
mergeLocalMatches(previous, next) { return [...next, ...previous]; },
refreshLocalMatchesUiForMissing(missing) { this.rendered = missing; }
};
for (const name of [
'getDownloadWorkflowScopeIdentity', 'getDownloadMissingIdentity',
'getDownloadStateKey', 'getDownloadProgressStore', 'cloneLocalMatches',
'rememberDownloadSnapshotForMissing', 'rememberDownloadedLocalMatchesForMissing',
'persistLocalMatchesInAnalysisCache', 'restoreDownloadedLocalMatchesForMissing',
'getLocalMatchRefreshKey', 'beginLocalMatchRefresh', 'isCurrentLocalMatchRefresh',
'finishLocalMatchRefresh', 'refreshLocalMatchesForDownloadedMissing'
]) {
dialog[name] = eval(`(${extractMethod(resolveDownloadMethodsSource, name)})`);
}
return dialog;
}
test('completed local matches survive a later workflow analysis', () => {
const dialog = createLocalRefreshRaceDialog();
const missing = { key: 'krea2', matches: [] };
const exact = { confidence: 100 };
dialog.rememberDownloadedLocalMatchesForMissing(missing, [exact]);
const replacement = { key: 'krea2', matches: [] };
dialog.restoreDownloadedLocalMatchesForMissing(replacement, dialog.scope);
assert.deepEqual(replacement.matches, [exact]);
dialog.scope = 'workflow-b';
const unrelated = { key: 'krea2', matches: [] };
dialog.restoreDownloadedLocalMatchesForMissing(unrelated, dialog.scope);
assert.deepEqual(unrelated.matches, []);
});
test('download local refresh updates the model replaced while scanning', async () => {
const dialog = createLocalRefreshRaceDialog();
const original = { key: 'krea2', matches: [{ confidence: 80 }] };
dialog.missingModels = [original];
let complete;
dialog.fetchLocalMatches = () => new Promise(resolve => { complete = resolve; });
const pending = dialog.refreshLocalMatchesForDownloadedMissing(original, 'krea2.safetensors');
const replacement = { key: 'krea2', matches: [{ confidence: 80 }] };
dialog.missingModels = [replacement];
complete({ matches: [{ confidence: 100 }] });
await pending;
assert.equal(replacement.matches[0].confidence, 100);
assert.equal(dialog.rendered, replacement);
});
test('download local refresh ignores a response after switching workflows', async () => {
const dialog = createLocalRefreshRaceDialog();
const original = { key: 'krea2', matches: [] };
dialog.missingModels = [original];
let complete;
dialog.fetchLocalMatches = () => new Promise(resolve => { complete = resolve; });
const pending = dialog.refreshLocalMatchesForDownloadedMissing(original, 'krea2.safetensors');
dialog.scope = 'workflow-b';
const unrelated = { key: 'krea2', matches: [] };
dialog.missingModels = [unrelated];
complete({ matches: [{ confidence: 100 }] });
assert.equal(await pending, null);
assert.deepEqual(unrelated.matches, []);
assert.equal(dialog.rendered, undefined);
assert.equal(dialog.downloadProgressByMissingKey, undefined);
});
test('pending cancelling state cannot be overwritten by stale downloading progress', () => {
const applyPendingDownloadStatus = eval(`(${extractMethod(resolveDownloadMethodsSource, 'applyPendingDownloadStatus')})`);
const info = {
@@ -450,9 +450,8 @@ export const resolveDownloadMethods = {
getDownloadStateKey(missing, context = null) {
const missingKey = this.getDownloadMissingIdentity(missing);
const workflowScope = context
? this.getDownloadWorkflowScopeIdentity(context)
: this.getCurrentDownloadWorkflowScopeIdentity();
const workflowScope = (context && this.getDownloadWorkflowScopeIdentity(context))
|| this.getCurrentDownloadWorkflowScopeIdentity();
return workflowScope ? `${workflowScope}::${missingKey}` : missingKey;
},
@@ -1464,15 +1463,16 @@ export const resolveDownloadMethods = {
nextDomChildren.forEach(child => container.appendChild(child));
},
async refreshLocalMatchesForDownloadedMissing(missing, downloadedFilename, { progressDiv = null, category = '', downloadPath = '', downloadDirectory = '' } = {}) {
async refreshLocalMatchesForDownloadedMissing(missing, downloadedFilename, { progressDiv = null, category = '', downloadPath = '', downloadDirectory = '', workflowScope = null } = {}) {
if (!missing) return [];
workflowScope ??= this.getCurrentDownloadWorkflowScopeIdentity();
if (workflowScope !== this.getCurrentDownloadWorkflowScopeIdentity()) return null;
const targetFilename = this.getDownloadedLocalMatchTarget(missing, downloadedFilename);
if (!targetFilename) return [];
const missingKey = this.getMissingModelKey(missing);
const refreshToken = this.beginLocalMatchRefresh(missing);
const currentMissing = (this.missingModels || []).find(item => this.getMissingModelKey(item) === missingKey) || missing;
try {
if (progressDiv) {
@@ -1492,8 +1492,11 @@ export const resolveDownloadMethods = {
}
const data = await this.fetchLocalMatches(targetFilename, category || missing.category || '', true);
if (workflowScope !== this.getCurrentDownloadWorkflowScopeIdentity()) return null;
// Linking can replace the model list while the scan is in flight.
const currentMissing = (this.missingModels || []).find(item => this.getMissingModelKey(item) === missingKey) || missing;
if (!this.isCurrentLocalMatchRefresh(refreshToken.key, refreshToken.token)) {
return currentMissing.matches || [];
return null;
}
const matches = Array.isArray(data.matches) ? data.matches : [];
@@ -1625,14 +1628,17 @@ export const resolveDownloadMethods = {
}
},
async refreshAfterDownload(missing, downloadedFilename, { progressDiv = null, downloadBtn = null, category = '', downloadPath = '', downloadDirectory = '', alreadyExists = false } = {}) {
async refreshAfterDownload(missing, downloadedFilename, { progressDiv = null, downloadBtn = null, category = '', downloadPath = '', downloadDirectory = '', alreadyExists = false, workflowScope = null } = {}) {
workflowScope ??= this.getCurrentDownloadWorkflowScopeIdentity();
try {
const matches = await this.refreshLocalMatchesForDownloadedMissing(missing, downloadedFilename, {
progressDiv,
category,
downloadPath,
downloadDirectory
downloadDirectory,
workflowScope
});
if (matches === null || workflowScope !== this.getCurrentDownloadWorkflowScopeIdentity()) return;
const downloadedLower = String(downloadedFilename || '').toLowerCase();
const perfectMatch = matches.find(match => {
const matchFilename = match.filename || match.model?.filename || '';
@@ -1668,6 +1674,7 @@ export const resolveDownloadMethods = {
);
} catch (error) {
if (workflowScope !== this.getCurrentDownloadWorkflowScopeIdentity()) return;
console.error('Model Resolver: Error refreshing after download:', error);
const snapshot = this.rememberDownloadSnapshotForMissing(missing, {
downloadId: null,
@@ -2218,6 +2225,7 @@ export const resolveDownloadMethods = {
category,
downloadPath: progress.path || info.downloadPath || '',
downloadDirectory: progress.directory || info.downloadDirectory || '',
workflowScope: this.getDownloadWorkflowScopeIdentity(info),
alreadyExists
});
}, 500);