Skip to content

Commit 44153d9

Browse files
committed
Make download cancellation workflow-scoped and prevent stale progress from reviving cancelled downloads
- Introduce workflow-scoped download state keys so active downloads and progress are isolated per workflow tab/route. - Add a transient 'cancelling' status path and a finalizeCancelledDownloadFrontend helper to avoid flicker and prevent stale progress responses from recreating completed/cancelled UI state. - Guard active-download lookups by current workflow scope to reduce cross-workflow interference.
1 parent 6ed34b6 commit 44153d9

1 file changed

Lines changed: 157 additions & 22 deletions

File tree

web/resolver/actions/resolve_download_methods.js

Lines changed: 157 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -515,14 +515,52 @@ export const resolveDownloadMethods = {
515515
return this.downloadProgressByMissingKey;
516516
},
517517

518-
getDownloadStateKey(missing) {
518+
getDownloadWorkflowScopeIdentity(context = {}) {
519+
const workflowKey = String(context.workflowKey || context.workflow_key || '').trim();
520+
return String(
521+
context.workflowTabId
522+
|| context.workflow_tab_id
523+
|| context.workflowId
524+
|| context.workflow_id
525+
|| context.workflowRouteKey
526+
|| context.workflow_route_key
527+
|| workflowKey.split('\n')[0]
528+
|| ''
529+
).trim();
530+
},
531+
532+
getCurrentDownloadWorkflowScopeIdentity() {
533+
const context = this.getActiveWorkflowTabContext?.() || {};
534+
return this.getDownloadWorkflowScopeIdentity({
535+
...context,
536+
workflowKey: this.getWorkflowScopedQueueKey?.() || '',
537+
workflowRouteKey: context.workflowRouteKey || this.getActiveWorkflowRouteKey?.() || this.activeWorkflowRouteKey || '',
538+
workflowId: context.workflowId || this.getActiveWorkflowId?.() || ''
539+
});
540+
},
541+
542+
isDownloadInCurrentWorkflowScope(info = {}) {
543+
const downloadScope = this.getDownloadWorkflowScopeIdentity(info);
544+
const currentScope = this.getCurrentDownloadWorkflowScopeIdentity();
545+
return !downloadScope || !currentScope || downloadScope === currentScope;
546+
},
547+
548+
getDownloadMissingIdentity(missing) {
519549
return this.getMissingSearchKey?.(missing) || this.getMissingModelKey(missing);
520550
},
521551

552+
getDownloadStateKey(missing, context = null) {
553+
const missingKey = this.getDownloadMissingIdentity(missing);
554+
const workflowScope = context
555+
? this.getDownloadWorkflowScopeIdentity(context)
556+
: this.getCurrentDownloadWorkflowScopeIdentity();
557+
return workflowScope ? `${workflowScope}::${missingKey}` : missingKey;
558+
},
559+
522560
rememberDownloadSnapshotForMissing(missing, snapshot = {}) {
523561
if (!missing) return null;
524562

525-
const key = this.getDownloadStateKey(missing);
563+
const key = this.getDownloadStateKey(missing, snapshot);
526564
const store = this.getDownloadProgressStore();
527565
const previous = store.get(key) || {};
528566
const progress = {
@@ -914,7 +952,11 @@ export const resolveDownloadMethods = {
914952
// started. Rebind it by the verified download/path match so the next
915953
// polling tick cannot repaint Local Matches from stale state.
916954
const activeEntry = this.activeDownloads?.[activeDownload.download_id];
917-
if (activeEntry && activeEntry.missing !== missing) {
955+
if (
956+
activeEntry
957+
&& (!this.isDownloadInCurrentWorkflowScope || this.isDownloadInCurrentWorkflowScope(activeEntry))
958+
&& activeEntry.missing !== missing
959+
) {
918960
activeEntry.missing = missing;
919961
}
920962
}
@@ -995,6 +1037,49 @@ export const resolveDownloadMethods = {
9951037
return Array.isArray(info.missing?.matches) ? info.missing.matches : [];
9961038
},
9971039

1040+
finalizeCancelledDownloadFrontend(downloadId, info = {}, progress = {}) {
1041+
if (!downloadId) return;
1042+
1043+
this.clearPendingDownloadStatus?.(info);
1044+
this.removeCancelledDownloadLocalMatches?.(info, progress);
1045+
1046+
const store = this.getDownloadProgressStore?.();
1047+
const toCancelledSnapshot = (snapshot = {}) => ({
1048+
...snapshot,
1049+
downloadId,
1050+
missing: snapshot.missing || info.missing || null,
1051+
progress: {
1052+
...(snapshot.progress || {}),
1053+
...progress,
1054+
status: 'cancelled',
1055+
speed: 0
1056+
},
1057+
status: 'cancelled',
1058+
message: 'Download cancelled - incomplete file removed',
1059+
type: 'warning',
1060+
isActive: false,
1061+
updatedAt: Date.now()
1062+
});
1063+
let cancelledSnapshot = toCancelledSnapshot(info.statusSnapshot || {});
1064+
if (store instanceof Map) {
1065+
for (const [key, snapshot] of store.entries()) {
1066+
if (String(snapshot?.downloadId || '') === String(downloadId)) {
1067+
const terminalSnapshot = toCancelledSnapshot(snapshot);
1068+
store.set(key, terminalSnapshot);
1069+
cancelledSnapshot = terminalSnapshot;
1070+
}
1071+
}
1072+
}
1073+
1074+
delete this.activeDownloads?.[downloadId];
1075+
1076+
const elements = this.resolveDownloadUiElements?.(info) || {};
1077+
this.renderDownloadSnapshot?.(downloadId, cancelledSnapshot, elements);
1078+
this.refreshLocalMatchesUiForMissing?.(info.missing);
1079+
this.updateDownloadAllButtonState?.();
1080+
this.updateQueuePanel?.();
1081+
},
1082+
9981083
persistLocalMatchesInAnalysisCache(missing, matches = []) {
9991084
if (!missing || !Array.isArray(matches)) return;
10001085

@@ -1094,10 +1179,14 @@ export const resolveDownloadMethods = {
10941179

10951180
getActiveDownloadEntryForMissing(missing) {
10961181
if (!missing) return null;
1097-
const missingKey = this.getDownloadStateKey(missing);
1182+
const missingKey = this.getDownloadMissingIdentity(missing);
10981183

10991184
for (const [downloadId, info] of Object.entries(this.activeDownloads || {})) {
1100-
if (info?.missing && this.getDownloadStateKey(info.missing) === missingKey) {
1185+
if (
1186+
info?.missing
1187+
&& this.isDownloadInCurrentWorkflowScope(info)
1188+
&& this.getDownloadMissingIdentity(info.missing) === missingKey
1189+
) {
11011190
return { downloadId, info };
11021191
}
11031192
}
@@ -1106,9 +1195,13 @@ export const resolveDownloadMethods = {
11061195

11071196
getActiveDownloadEntriesForMissing(missing) {
11081197
if (!missing) return [];
1109-
const missingKey = this.getDownloadStateKey(missing);
1198+
const missingKey = this.getDownloadMissingIdentity(missing);
11101199
return Object.entries(this.activeDownloads || {})
1111-
.filter(([, info]) => info?.missing && this.getDownloadStateKey(info.missing) === missingKey)
1200+
.filter(([, info]) => (
1201+
info?.missing
1202+
&& this.isDownloadInCurrentWorkflowScope(info)
1203+
&& this.getDownloadMissingIdentity(info.missing) === missingKey
1204+
))
11121205
.map(([downloadId, info]) => ({ downloadId, info }));
11131206
},
11141207

@@ -1883,14 +1976,16 @@ export const resolveDownloadMethods = {
18831976
desiredStatus === 'downloading' && currentStatus === 'paused'
18841977
) || (
18851978
desiredStatus === 'paused' && (currentStatus === 'downloading' || currentStatus === 'starting')
1979+
) || (
1980+
desiredStatus === 'cancelling' && ['starting', 'downloading', 'paused'].includes(currentStatus)
18861981
);
18871982
if (!canHoldDesiredStatus) return progress;
18881983

18891984
return {
18901985
...progress,
18911986
backend_status: currentStatus,
18921987
status: desiredStatus,
1893-
speed: desiredStatus === 'paused' ? 0 : progress.speed
1988+
speed: desiredStatus === 'paused' || desiredStatus === 'cancelling' ? 0 : progress.speed
18941989
};
18951990
},
18961991

@@ -1903,6 +1998,9 @@ export const resolveDownloadMethods = {
19031998

19041999
try {
19052000
let progress = await this.fetchJson(`/model_resolver/progress/${downloadId}`, { silent: true }, 'Get download progress');
2001+
// Cancel may have completed while this request was in flight. Never
2002+
// let a stale progress response recreate a globally finalized bar.
2003+
if (this.activeDownloads?.[downloadId] !== info) return;
19062004
progress = this.applyPendingDownloadStatus(info, progress);
19072005
const snapshot = this.rememberDownloadUiState(downloadId, info, progress, { isActive: true });
19082006
const { progressDiv, downloadBtn } = this.resolveDownloadUiElements(info);
@@ -1921,6 +2019,12 @@ export const resolveDownloadMethods = {
19212019
// Continue polling
19222020
setTimeout(() => this.pollDownloadProgress(downloadId), progress.status === 'paused' ? 1500 : 1000);
19232021

2022+
} else if (progress.status === 'cancelling') {
2023+
this.renderDownloadSnapshot(downloadId, snapshot, { progressDiv, downloadBtn });
2024+
this.refreshLocalMatchesUiForMissing?.(missing);
2025+
this.updateQueuePanel?.();
2026+
setTimeout(() => this.pollDownloadProgress(downloadId), 500);
2027+
19242028
} else if (progress.status === 'completed') {
19252029
const alreadyExists = Boolean(progress.already_exists);
19262030
const completedSnapshot = this.rememberDownloadUiState(downloadId, info, progress, {
@@ -1972,17 +2076,7 @@ export const resolveDownloadMethods = {
19722076
this.refreshLocalMatchesUiForMissing?.(missing);
19732077

19742078
} else if (progress.status === 'cancelled') {
1975-
const cancelledSnapshot = this.rememberDownloadUiState(downloadId, info, progress, {
1976-
status: 'cancelled',
1977-
type: 'warning',
1978-
isActive: false
1979-
});
1980-
this.renderDownloadSnapshot(downloadId, cancelledSnapshot, { progressDiv, downloadBtn });
1981-
this.removeCancelledDownloadLocalMatches?.(info, progress);
1982-
delete this.activeDownloads[downloadId];
1983-
this.updateDownloadAllButtonState();
1984-
this.updateQueuePanel?.();
1985-
this.refreshLocalMatchesUiForMissing?.(missing);
2079+
this.finalizeCancelledDownloadFrontend?.(downloadId, info, progress);
19862080
this.showNotification('Download cancelled', 'info');
19872081

19882082
} else {
@@ -2033,7 +2127,7 @@ export const resolveDownloadMethods = {
20332127
cancelDownload(downloadId) {
20342128
const info = this.activeDownloads[downloadId];
20352129
if (info) {
2036-
this.clearPendingDownloadStatus(info);
2130+
this.setPendingDownloadStatus(info, 'cancelling', 30000);
20372131
const snapshot = this.rememberDownloadUiState(
20382132
downloadId,
20392133
info,
@@ -2054,14 +2148,14 @@ export const resolveDownloadMethods = {
20542148
method: 'POST'
20552149
}, 'Cancel download').then(() => {
20562150
if (!info) return;
2057-
this.removeCancelledDownloadLocalMatches?.(info, {
2151+
this.finalizeCancelledDownloadFrontend?.(downloadId, info, {
20582152
...(info.lastProgress || {}),
20592153
status: 'cancelled',
20602154
filename: info.lastProgress?.filename || info.filename || '',
20612155
path: info.lastProgress?.path || info.downloadPath || '',
20622156
directory: info.lastProgress?.directory || info.downloadDirectory || ''
20632157
});
2064-
this.refreshLocalMatchesUiForMissing?.(info.missing);
2158+
this.showNotification('Download cancelled', 'info');
20652159
}).catch((error) => {
20662160
console.error('Model Resolver: Cancel error:', error);
20672161
this.showNotification('Failed to cancel download', 'error');
@@ -2070,6 +2164,29 @@ export const resolveDownloadMethods = {
20702164

20712165
pauseDownload(downloadId) {
20722166
const info = this.activeDownloads[downloadId];
2167+
const currentStatus = String(
2168+
info?.pendingDownloadStatus
2169+
|| info?.lastStatus
2170+
|| info?.lastProgress?.status
2171+
|| ''
2172+
).toLowerCase();
2173+
if (currentStatus === 'cancelling' || currentStatus === 'cancelled') {
2174+
if (info) {
2175+
const snapshot = this.rememberDownloadUiState(
2176+
downloadId,
2177+
info,
2178+
{ ...(info.lastProgress || {}), status: 'cancelling', speed: 0 },
2179+
{
2180+
status: 'cancelling',
2181+
message: 'Cancelling download...',
2182+
type: 'info',
2183+
isActive: true
2184+
}
2185+
);
2186+
this.renderDownloadSnapshot(downloadId, snapshot, this.resolveDownloadUiElements(info));
2187+
}
2188+
return;
2189+
}
20732190
const previousProgress = info?.lastProgress ? { ...info.lastProgress } : null;
20742191
if (info) {
20752192
this.setPendingDownloadStatus(info, 'paused');
@@ -2095,6 +2212,24 @@ export const resolveDownloadMethods = {
20952212
this.showNotification('Download paused', 'info');
20962213
}).catch((error) => {
20972214
console.error('Model Resolver: Pause error:', error);
2215+
const cancellationInProgress = /being cancelled|cancell?ing/i.test(String(error?.message || ''));
2216+
if (info && cancellationInProgress) {
2217+
this.setPendingDownloadStatus(info, 'cancelling', 30000);
2218+
const snapshot = this.rememberDownloadUiState(
2219+
downloadId,
2220+
info,
2221+
{ ...(info.lastProgress || {}), status: 'cancelling', speed: 0 },
2222+
{
2223+
status: 'cancelling',
2224+
message: 'Cancelling download...',
2225+
type: 'info',
2226+
isActive: true
2227+
}
2228+
);
2229+
this.renderDownloadSnapshot(downloadId, snapshot, this.resolveDownloadUiElements(info));
2230+
this.updateQueuePanel?.();
2231+
return;
2232+
}
20982233
if (info && previousProgress) {
20992234
this.clearPendingDownloadStatus(info);
21002235
const snapshot = this.rememberDownloadUiState(

0 commit comments

Comments
 (0)