feat: preserve search state and resolution aliases across workflow transitions
- Propagate missing_search_key through resolution queue and download resolution paths so cache lookups remain consistent after workflow re-analysis or active searches. - Set preserveSearchCacheAcrossNextWorkflowSync before reloading workflow data following batch queue/resolve operations. - Remember applied resolu tions (rememberAppliedResolvedSelections) after both queue and direct downloads so alias tracking is not lost. - getMissingSearchKey now short-circuits on missing_search_key and search_key fields instead of always deriving from node identity, fixing spurious cache misses. - Guard settleInactiveSearchProgress so non-active workflow states only update the cache when the active workflow hosts a matching background search job. - After workflow analysis, call applyResolvedSelectionAliases so analysis data reflects user selections. - When switching workflows, clone and merge the previous workflow search-result cache and migrate background search jobs that are still running, so active searches are not interrupted or dropped.
This commit is contained in:
@@ -1762,6 +1762,7 @@ export const queueMethods = {
|
||||
const missingKey = this.getMissingModelKey(missing);
|
||||
const resolution = {
|
||||
missing_key: missingKey,
|
||||
missing_search_key: this.getMissingSearchKey?.(missing),
|
||||
node_id: missing.node_id,
|
||||
widget_index: missing.widget_index,
|
||||
resolved_path: resolvedModel.path,
|
||||
@@ -1869,6 +1870,7 @@ export const queueMethods = {
|
||||
this.pendingResolutions = remainingSelections;
|
||||
this.rebuildPendingIndex();
|
||||
this.savePendingQueueForActiveWorkflow();
|
||||
this.preserveSearchCacheAcrossNextWorkflowSync = true;
|
||||
this.syncWorkflowScopedQueue?.(data.workflow);
|
||||
|
||||
this.pendingResolutions = remainingSelections;
|
||||
@@ -2056,6 +2058,7 @@ export const queueMethods = {
|
||||
this.appliedResolvedSelectionAliases.set(key, {
|
||||
key,
|
||||
category: selection.category || '',
|
||||
searchKey: selection.missing_search_key || selection.search_key || '',
|
||||
refs: this.getResolvedSelectionRefs(selection),
|
||||
tokens: this.getResolvedSelectionTokenSet(this.getResolvedSelectionPathCandidates(selection))
|
||||
});
|
||||
@@ -2110,6 +2113,9 @@ export const queueMethods = {
|
||||
if (matchedKeys.has(key)) continue;
|
||||
if (!this.doesResolvedModelMatchAlias(model, alias)) continue;
|
||||
model.missing_key = key;
|
||||
if (alias.searchKey) {
|
||||
model.missing_search_key = alias.searchKey;
|
||||
}
|
||||
matchedKeys.add(key);
|
||||
break;
|
||||
}
|
||||
@@ -2133,6 +2139,9 @@ export const queueMethods = {
|
||||
return {
|
||||
...(sourceMissing || {}),
|
||||
missing_key: this.getResolutionQueueKey(selection),
|
||||
missing_search_key: selection.missing_search_key
|
||||
|| sourceMissing?.missing_search_key
|
||||
|| (sourceMissing ? this.getMissingSearchKey?.(sourceMissing) : ''),
|
||||
__isOptimisticResolved: true,
|
||||
node_id: selection.node_id ?? sourceMissing?.node_id,
|
||||
widget_index: selection.widget_index ?? sourceMissing?.widget_index,
|
||||
|
||||
@@ -29,7 +29,11 @@ export const resolveDownloadMethods = {
|
||||
const nodeRefs = missing.all_node_refs || [missing];
|
||||
log.debug('nodeRefs count:', nodeRefs?.length, 'is_lora_v2:', nodeRefs?.[0]?.is_lora_v2);
|
||||
|
||||
const missingKey = this.getMissingModelKey?.(missing);
|
||||
const missingSearchKey = this.getMissingSearchKey?.(missing);
|
||||
const resolutions = nodeRefs.map(ref => ({
|
||||
missing_key: missingKey,
|
||||
missing_search_key: missingSearchKey,
|
||||
node_id: ref.node_id,
|
||||
widget_index: ref.widget_index,
|
||||
resolved_path: resolvedModel.path,
|
||||
@@ -61,9 +65,11 @@ export const resolveDownloadMethods = {
|
||||
const count = resolutions.length;
|
||||
const refText = count > 1 ? ` (${count} references)` : '';
|
||||
this.showNotification(`✓ Model linked successfully: ${modelName}${refText}`, 'success');
|
||||
this.rememberAppliedResolvedSelections?.(resolutions);
|
||||
|
||||
// Reload dialog using the updated workflow from API response
|
||||
// This ensures we're analyzing the correct updated workflow
|
||||
this.preserveSearchCacheAcrossNextWorkflowSync = true;
|
||||
await this.loadWorkflowData(data.workflow, { force: true });
|
||||
} else {
|
||||
this.showNotification('Failed to resolve model: ' + (data.error || 'Unknown error'), 'error');
|
||||
@@ -102,6 +108,8 @@ export const resolveDownloadMethods = {
|
||||
|
||||
if (perfectMatch && perfectMatch.model) {
|
||||
resolutions.push({
|
||||
missing_key: this.getMissingModelKey?.(missing),
|
||||
missing_search_key: this.getMissingSearchKey?.(missing),
|
||||
node_id: missing.node_id,
|
||||
widget_index: missing.widget_index,
|
||||
resolved_path: perfectMatch.model.path,
|
||||
@@ -140,9 +148,11 @@ export const resolveDownloadMethods = {
|
||||
`✓ Successfully linked ${resolutions.length} model${resolutions.length > 1 ? 's' : ''}!`,
|
||||
'success'
|
||||
);
|
||||
this.rememberAppliedResolvedSelections?.(resolutions);
|
||||
|
||||
// Reload dialog using the updated workflow from API response (if dialog is visible)
|
||||
if (this.contentElement) {
|
||||
this.preserveSearchCacheAcrossNextWorkflowSync = true;
|
||||
await this.loadWorkflowData(resolveData.workflow, { force: true });
|
||||
}
|
||||
|
||||
@@ -2160,6 +2170,11 @@ export const resolveDownloadMethods = {
|
||||
if (currentJob?.runId === searchRunId) {
|
||||
this.backgroundSearchJobs.delete(backgroundJobKey);
|
||||
}
|
||||
for (const [jobKey, job] of Array.from(this.backgroundSearchJobs?.entries?.() || [])) {
|
||||
if (job?.runId === searchRunId && job?.missingSearchKey === missingSearchKey) {
|
||||
this.backgroundSearchJobs.delete(jobKey);
|
||||
}
|
||||
}
|
||||
if (!completedSearchRun) {
|
||||
this.settleInactiveSearchProgress?.(missing, state, {
|
||||
workflowKey,
|
||||
|
||||
@@ -7,6 +7,13 @@ export const searchPanelMethods = {
|
||||
* Build stable cache key for a missing model entry
|
||||
*/
|
||||
getMissingSearchKey(missing) {
|
||||
if (missing?.missing_search_key) {
|
||||
return String(missing.missing_search_key);
|
||||
}
|
||||
if (missing?.search_key) {
|
||||
return String(missing.search_key);
|
||||
}
|
||||
|
||||
const nodeId = missing?.node_id ?? '';
|
||||
const widgetIndex = missing?.widget_index ?? '';
|
||||
const subgraphId = missing?.subgraph_id ?? '';
|
||||
@@ -526,14 +533,45 @@ export const searchPanelMethods = {
|
||||
|
||||
if (workflowKey === this.getWorkflowScopedQueueKey()) {
|
||||
this.searchResultCache.set(missingSearchKey, state);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeWorkflowKey = this.getWorkflowScopedQueueKey();
|
||||
const activeState = this.searchResultCache?.get(missingSearchKey);
|
||||
const runId = state.activeSearchRunId || activeState?.activeSearchRunId;
|
||||
const mirrorsActiveSearch = Boolean(
|
||||
activeState === state
|
||||
|| (
|
||||
runId
|
||||
&& activeState?.activeSearchRunId === runId
|
||||
&& this.hasBackgroundSearchJob(activeWorkflowKey, missingSearchKey, runId)
|
||||
)
|
||||
);
|
||||
if (mirrorsActiveSearch) {
|
||||
this.searchResultCache.set(missingSearchKey, state);
|
||||
this.saveSearchCacheForActiveWorkflow?.();
|
||||
}
|
||||
},
|
||||
|
||||
refreshSearchUiForMissing(missing, state = null, { workflowKey = this.getWorkflowScopedQueueKey() } = {}) {
|
||||
if (!missing) return;
|
||||
if (workflowKey && workflowKey !== this.getWorkflowScopedQueueKey()) return;
|
||||
|
||||
const currentState = state || this.searchResultCache.get(this.getMissingSearchKey(missing));
|
||||
const activeWorkflowKey = this.getWorkflowScopedQueueKey();
|
||||
const missingSearchKey = this.getMissingSearchKey(missing);
|
||||
let currentState = state || this.searchResultCache.get(missingSearchKey);
|
||||
if (workflowKey && workflowKey !== activeWorkflowKey) {
|
||||
const activeState = this.searchResultCache.get(missingSearchKey);
|
||||
const runId = currentState?.activeSearchRunId || activeState?.activeSearchRunId;
|
||||
const mirrorsActiveSearch = Boolean(
|
||||
activeState === currentState
|
||||
|| (
|
||||
runId
|
||||
&& activeState?.activeSearchRunId === runId
|
||||
&& this.hasBackgroundSearchJob(activeWorkflowKey, missingSearchKey, runId)
|
||||
)
|
||||
);
|
||||
if (!mirrorsActiveSearch) return;
|
||||
currentState = activeState || currentState;
|
||||
}
|
||||
this.refreshMissingSourcesSummary(missing);
|
||||
this.updateBatchFooterButtons?.();
|
||||
|
||||
@@ -1228,8 +1266,13 @@ export const searchPanelMethods = {
|
||||
const runId = state.activeSearchRunId;
|
||||
if (!runId || !this.hasBackgroundSearchJob(workflowKey, missingSearchKey, runId)) continue;
|
||||
|
||||
const job = this.getBackgroundSearchJob(workflowKey, missingSearchKey);
|
||||
for (const [source, progress] of Object.entries(state.sourceProgress || {})) {
|
||||
if (progress?.status !== 'running') continue;
|
||||
const progressId = job?.sourceProgressIds?.get?.(source);
|
||||
if (progressId) {
|
||||
this.startBackendSearchProgressPolling?.(state, missing, source, runId, progressId, { workflowKey });
|
||||
}
|
||||
this.startEstimatedSearchProgress(state, missing, null, source, runId, { workflowKey });
|
||||
}
|
||||
this.refreshSearchUiForMissing(missing, state, { workflowKey });
|
||||
|
||||
@@ -210,6 +210,7 @@ export const lifecycleGraphMethods = {
|
||||
}
|
||||
await progressPromise;
|
||||
}
|
||||
this.applyResolvedSelectionAliasesToAnalysisData?.(data);
|
||||
if (this._workflowDataLoadToken === loadToken) {
|
||||
this.cachedWorkflowSignature = workflowSignature;
|
||||
this.cachedAnalysisData = data;
|
||||
|
||||
@@ -560,10 +560,31 @@ export const workflowStateMethods = {
|
||||
const nextSignature = this.getWorkflowSignature(currentWorkflow);
|
||||
const previousKey = this.getWorkflowScopedQueueKey();
|
||||
const nextKey = this.getWorkflowScopedQueueKey(nextRoute, nextSignature);
|
||||
const preserveSearchCache = Boolean(this.preserveSearchCacheAcrossNextWorkflowSync);
|
||||
const previousSearchCache = preserveSearchCache
|
||||
? this.cloneSearchResultCache(this.searchResultCache, {
|
||||
preserveActive: true,
|
||||
workflowKey: previousKey
|
||||
})
|
||||
: null;
|
||||
const preservedActiveSearchStates = new Map();
|
||||
const preservedActiveSearchMissingModels = [];
|
||||
if (preserveSearchCache && previousKey && nextKey && nextKey !== previousKey) {
|
||||
for (const [missingSearchKey, state] of this.searchResultCache.entries()) {
|
||||
const runId = state?.activeSearchRunId;
|
||||
if (!runId || !this.hasBackgroundSearchJob?.(previousKey, missingSearchKey, runId)) continue;
|
||||
const job = this.getBackgroundSearchJob?.(previousKey, missingSearchKey);
|
||||
preservedActiveSearchStates.set(missingSearchKey, { state, job });
|
||||
if (job?.missing) {
|
||||
preservedActiveSearchMissingModels.push(job.missing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextKey || nextKey === previousKey) {
|
||||
this.activeWorkflowRouteKey = nextRoute;
|
||||
this.activeWorkflowSignature = nextSignature;
|
||||
this.preserveSearchCacheAcrossNextWorkflowSync = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -579,7 +600,32 @@ export const workflowStateMethods = {
|
||||
this.restoreAnalysisCacheForActiveWorkflow();
|
||||
this.restoreLoadedModelsCacheForActiveWorkflow();
|
||||
this.restoreSearchCacheForActiveWorkflow();
|
||||
if (preserveSearchCache && previousSearchCache?.size) {
|
||||
const mergedSearchCache = this.searchResultCache instanceof Map
|
||||
? this.searchResultCache
|
||||
: new Map();
|
||||
for (const [missingSearchKey, state] of previousSearchCache.entries()) {
|
||||
if (!mergedSearchCache.has(missingSearchKey)) {
|
||||
mergedSearchCache.set(missingSearchKey, state);
|
||||
}
|
||||
}
|
||||
for (const [missingSearchKey, preserved] of preservedActiveSearchStates.entries()) {
|
||||
mergedSearchCache.set(missingSearchKey, preserved.state);
|
||||
if (preserved.job) {
|
||||
this.backgroundSearchJobs?.set(
|
||||
this.getBackgroundSearchJobKey(nextKey, missingSearchKey),
|
||||
preserved.job
|
||||
);
|
||||
}
|
||||
}
|
||||
this.searchResultCache = mergedSearchCache;
|
||||
this.saveSearchCacheForActiveWorkflow();
|
||||
}
|
||||
this.preserveSearchCacheAcrossNextWorkflowSync = false;
|
||||
this.restoreDownloadTargetSelectionsForActiveWorkflow();
|
||||
if (preservedActiveSearchMissingModels.length) {
|
||||
this.syncSearchProgressAfterResume?.(preservedActiveSearchMissingModels, { workflowKey: nextKey });
|
||||
}
|
||||
},
|
||||
|
||||
clearWorkflowScopedState() {
|
||||
|
||||
Reference in New Issue
Block a user