Files
Azornes-Comfyui-Model-Resolver/web/resolver/shell/workflow_state_methods.js
T

932 lines
37 KiB
JavaScript

import { app } from "../../../../../scripts/app.js";
import { createModuleLogger } from "../../log_system/log_funcs.js";
const log = createModuleLogger('workflow_state_methods');
export const workflowStateMethods = {
getWorkflowSignature(workflow, options = {}) {
if (!workflow) return null;
try {
return JSON.stringify(this.getWorkflowSignatureData(workflow, options));
} catch (error) {
console.warn('Model Resolver: workflow signature generation failed', error);
return null;
}
},
getMissingWorkflowSignature(workflow) {
return this.getWorkflowSignature(workflow, { includeStrength: false });
},
getWorkflowAnalysisRequest(workflow, { forceRescan = false, silent = false } = {}) {
if (!workflow) return null;
const signature = this.getMissingWorkflowSignature(workflow);
const route = this.getActiveWorkflowRouteKey?.() || '';
const existing = this._workflowAnalysisRequest;
if (
!forceRescan
&& existing?.signature === signature
&& existing?.route === route
) {
return existing;
}
const analysisId = `an-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const requestOptions = {
method: 'POST',
body: JSON.stringify({
workflow,
analysis_id: analysisId,
force_rescan: Boolean(forceRescan),
}),
};
if (silent) requestOptions.silent = true;
const request = {
analysisId,
promise: this.fetchJson('/model_resolver/analyze', requestOptions, 'Analyze workflow'),
route,
signature,
};
this._workflowAnalysisRequest = request;
request.promise.then(
() => {
if (this._workflowAnalysisRequest === request) {
this._workflowAnalysisRequest = null;
}
},
() => {
if (this._workflowAnalysisRequest === request) {
this._workflowAnalysisRequest = null;
}
}
);
return request;
},
isWorkflowStrengthWidgetName(name = '') {
const normalized = String(name || '').trim().replace(/[-\s]/g, '_').toLowerCase();
return normalized === 'strength_model'
|| normalized === 'strength_clip'
|| /(?:^|_)(?:strength|model_strength|clip_strength)$/.test(normalized);
},
getWorkflowSignatureData(workflow, { includeStrength = true } = {}) {
const modelExtensionPattern = /\.(ckpt|pt2?|bin|pth|safetensors|pkl|sft|onnx|gguf)(?:$|[?#])/i;
const urnPattern = /^urn:air:[^:]+:[^:]+:[^:]+:\d+@\d+$/i;
const urlPattern = /^https?:\/\//i;
const loraTokenPattern = /<lora:[^>]+>/i;
const modelReferenceKeys = new Set([
'lora',
'ckpt_name',
'checkpoint',
'vae_name',
'clip_name',
'clip_name1',
'clip_name2',
'control_net_name',
'cnet',
'model_name',
'model_path',
'unet_name',
'gguf_name',
'upscale_model_name',
'style_model_name',
'gligen_name',
'audio_encoder_name',
'bg_removal_name',
'photomaker_model_name',
'sam_model_name',
'text_encoder',
'existing_lora',
'hypernetwork_name',
'clip_name3',
'clip_name4'
]);
const modelMetadataKeys = new Set([
'name',
'filename',
'file_name',
'path',
'url',
'model_url',
'directory',
'base_model',
'model_id',
'modelid',
'version_id',
'versionid',
'active',
'on'
]);
if (includeStrength) {
modelMetadataKeys.add('strength');
}
const loraStrengthNodeTypes = new Set([
'LoraLoader',
'LoraLoaderModelOnly',
'LoraLoaderBypass',
'LoraLoaderBypassModelOnly',
'CreateHookLora',
'CreateHookLoraModelOnly'
]);
const loraStrengthKeys = new Set([
'strength_model',
'lora_model_strength',
'model_strength',
'lora_strength',
'strength'
]);
const indexedLoraStrengthPattern = /^lora_\d+_(?:strength|model_strength|clip_strength)$/;
const loraStackStateKeys = new Set(['toggle', 'mode', 'num_loras']);
const staticFallback = {
CheckpointLoaderSimple: [0],
CheckpointLoader: [1],
DiffusersLoader: [0],
unCLIPCheckpointLoader: [0],
ImageOnlyCheckpointLoader: [0],
VAELoader: [0],
VAELoaderKJ: [0],
LoraLoader: [0],
LoraLoaderModelOnly: [0],
LoraLoaderBypass: [0],
LoraLoaderBypassModelOnly: [0],
CreateHookLora: [0],
CreateHookLoraModelOnly: [0],
CreateHookModelAsLora: [0],
CreateHookModelAsLoraModelOnly: [0],
UNETLoader: [0],
LoaderGGUF: [0],
LoaderGGUFAdvanced: [0],
UnetLoaderGGUF: [0],
UnetLoaderGGUFAdvanced: [0],
LatentUpscaleModelLoader: [0],
CLIPLoader: [0],
DualCLIPLoader: [0, 1],
CLIPLoaderGGUF: [0],
ClipLoaderGGUF: [0],
DualCLIPLoaderGGUF: [0, 1],
DualClipLoaderGGUF: [0, 1],
TripleCLIPLoader: [0, 1, 2],
TripleClipLoader: [0, 1, 2],
TripleCLIPLoaderGGUF: [0, 1, 2],
TripleClipLoaderGGUF: [0, 1, 2],
QuadrupleCLIPLoader: [0, 1, 2, 3],
QuadrupleClipLoader: [0, 1, 2, 3],
QuadrupleCLIPLoaderGGUF: [0, 1, 2, 3],
QuadrupleClipLoaderGGUF: [0, 1, 2, 3],
ControlNetLoader: [0],
DiffControlNetLoader: [0],
ControlNetLoaderAdvanced: [0],
ACN_ControlNetLoaderAdvanced: [0],
ACN_DiffControlNetLoaderAdvanced: [0],
CLIPVisionLoader: [0],
StyleModelLoader: [0],
GLIGENLoader: [0],
UpscaleModelLoader: [0],
SAMLoader: [0],
UltralyticsDetectorProvider: [0],
AudioEncoderLoader: [0],
LoadBackgroundRemovalModel: [0],
LoadDA3Model: [0],
FrameInterpolationModelLoader: [0],
LoadMediaPipeFaceLandmarker: [0],
ModelPatchLoader: [0],
LoadMoGeModel: [0],
PhotoMakerLoader: [0],
OpticalFlowLoader: [0],
HypernetworkLoader: [0],
EmbeddingLoader: [0],
LTXVAudioVAELoader: [0],
LowVRAMAudioVAELoader: [0],
LTXVGemmaCLIPModelLoader: [0],
LTXAVTextEncoderLoader: [0, 1]
};
const modelWidgetIndicesByNodeType = { ...staticFallback };
const dynamicRules = this.capabilities?.node_rules;
if (dynamicRules && typeof dynamicRules === 'object') {
for (const [nodeType, widgetsMap] of Object.entries(dynamicRules)) {
if (widgetsMap && typeof widgetsMap === 'object') {
modelWidgetIndicesByNodeType[nodeType] = Object.keys(widgetsMap).map(k => parseInt(k, 10));
}
}
}
const normalizeKey = (key = '') => String(key).replace(/[-\s]/g, '_').toLowerCase();
const isModelWidgetIndex = (nodeType = '', index = -1) => (
modelWidgetIndicesByNodeType[nodeType] || []
).includes(index);
const isRelevantString = (value = '', key = '', isModelWidget = false) => {
const text = String(value).trim();
if (!text) return false;
const normalizedKey = normalizeKey(key);
return isModelWidget
|| modelReferenceKeys.has(normalizedKey)
|| modelMetadataKeys.has(normalizedKey)
|| modelExtensionPattern.test(text)
|| urnPattern.test(text)
|| urlPattern.test(text)
|| loraTokenPattern.test(text);
};
const isRelevantScalarKey = (key = '') => {
const normalizedKey = normalizeKey(key);
return (includeStrength && (
loraStrengthKeys.has(normalizedKey)
|| indexedLoraStrengthPattern.test(normalizedKey)
))
|| loraStackStateKeys.has(normalizedKey)
|| ['active', 'on', 'model_id', 'modelid', 'version_id', 'versionid'].includes(normalizedKey);
};
const normalizeRelevantValue = (value, key = '', isModelWidget = false) => {
if (value == null) return null;
if (typeof value === 'string') {
const text = value.trim();
return isRelevantString(text, key, isModelWidget) ? text : null;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return isRelevantScalarKey(key) ? value : null;
}
if (Array.isArray(value)) {
const items = value
.map(item => normalizeRelevantValue(item, key))
.filter(item => item !== null);
return items.length ? items : null;
}
if (typeof value === 'object') {
const entries = Object.entries(value)
.map(([entryKey, entryValue]) => [entryKey, normalizeRelevantValue(entryValue, entryKey)])
.filter(([, entryValue]) => entryValue !== null)
.sort(([a], [b]) => String(a).localeCompare(String(b)));
return entries.length ? Object.fromEntries(entries) : null;
}
return null;
};
const normalizeWidgetValues = (node = {}) => {
const widgetsValues = Array.isArray(node.widgets_values) ? node.widgets_values : [];
const nodeType = node.type || '';
const widgetValueKey = (index) => {
const widgetInputs = Array.isArray(node.inputs)
? node.inputs.filter(item => item?.widget != null)
: [];
const collections = [
Array.isArray(node.widgets) ? node.widgets : [],
widgetInputs
];
for (const items of collections) {
const item = items[index];
if (!item || typeof item !== 'object') continue;
const widgetName = item.widget && typeof item.widget === 'object'
? item.widget.name
: item.widget;
const name = item.name || widgetName || item.label;
if (name) return name;
}
return '';
};
return widgetsValues
.map((value, index) => {
const key = widgetValueKey(index);
const isModelWidget = isModelWidgetIndex(nodeType, index);
const isLoraStackState = nodeType === 'easy loraStack'
&& loraStackStateKeys.has(normalizeKey(key));
let normalized = normalizeRelevantValue(
value,
key,
isModelWidget || isLoraStackState
);
if (
includeStrength
&& normalized === null
&& loraStrengthNodeTypes.has(nodeType)
&& (
normalizeRelevantValue(widgetsValues[index - 1], widgetValueKey(index - 1)) !== null
|| normalizeRelevantValue(widgetsValues[index - 2], widgetValueKey(index - 2)) !== null
)
&& (typeof value === 'number' || typeof value === 'string')
) {
normalized = String(value).trim();
}
return normalized === null ? null : { index, value: normalized };
})
.filter(Boolean);
};
const normalizeWidgets = (widgets = []) => Array.isArray(widgets)
? widgets.map((widget = {}) => ({
name: widget.name,
widget: widget.widget,
label: widget.label,
type: widget.type
})).filter(widget => Object.values(widget).some(value => value != null && value !== ''))
: [];
const normalizeLinks = (links = []) => Array.isArray(links)
? links.map(link => Array.isArray(link) ? link : [
link?.id,
link?.origin_id,
link?.origin_slot,
link?.target_id,
link?.target_slot,
link?.type
]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
: [];
const normalizeNode = (node = {}) => ({
id: node.id,
type: node.type,
title: node.title || '',
bypassed: node.mode === 4,
inputs: Array.isArray(node.inputs)
? node.inputs.map(input => ({
name: input?.name,
widget: input?.widget,
label: input?.label,
type: input?.type,
link: input?.link
}))
: [],
outputs: Array.isArray(node.outputs)
? node.outputs.map(output => ({
name: output?.name,
type: output?.type,
links: Array.isArray(output?.links) ? [...output.links].sort((a, b) => String(a).localeCompare(String(b))) : []
}))
: [],
widgets: normalizeWidgets(node.widgets),
widgets_values: normalizeWidgetValues(node),
properties: normalizeRelevantValue(node.properties) || {}
});
const normalizeDefinition = (definition = {}) => ({
nodes: Array.isArray(definition.nodes)
? definition.nodes.map(normalizeNode).sort((a, b) => String(a.id).localeCompare(String(b.id)))
: [],
links: normalizeLinks(definition.links)
});
const normalizeDefinitions = (definitions = {}) => {
if (!definitions || typeof definitions !== 'object') return {};
const normalized = {};
if (Array.isArray(definitions.subgraphs)) {
normalized.subgraphs = definitions.subgraphs
.map(subgraph => ({
id: subgraph?.id,
name: subgraph?.name,
...normalizeDefinition(subgraph)
}))
.sort((a, b) => String(a.id || a.name || '').localeCompare(String(b.id || b.name || '')));
}
for (const [key, definition] of Object.entries(definitions)) {
if (key === 'subgraphs' || !definition || typeof definition !== 'object' || Array.isArray(definition)) continue;
if (!Array.isArray(definition.nodes) && !Array.isArray(definition.links)) continue;
normalized[key] = normalizeDefinition(definition);
}
return Object.fromEntries(Object.entries(normalized).sort(([a], [b]) => String(a).localeCompare(String(b))));
};
return {
nodes: Array.isArray(workflow.nodes)
? workflow.nodes.map(normalizeNode).sort((a, b) => String(a.id).localeCompare(String(b.id)))
: [],
links: normalizeLinks(workflow.links),
definitions: normalizeDefinitions(workflow.definitions)
};
},
getActiveWorkflowRouteKey() {
try {
const rootGraph = app?.rootGraph || app?.graph?.rootGraph || app?.graph;
const rootGraphId = rootGraph?.id || rootGraph?._id || rootGraph?.config?.id;
if (rootGraphId) return `#${rootGraphId}`;
return window.location?.hash || '';
} catch (_error) {
return '';
}
},
getWorkflowScopedQueueKey(
route = this.activeWorkflowRouteKey,
signature = this.activeMissingWorkflowSignature || this.activeWorkflowSignature
) {
if (route && signature) return `${route}\n${signature}`;
return route || signature || null;
},
clonePendingResolutions(list = this.pendingResolutions) {
try {
return JSON.parse(JSON.stringify(Array.isArray(list) ? list : []));
} catch (error) {
console.warn('Model Resolver: failed to clone queued selections', error);
return Array.isArray(list) ? list.map(item => ({ ...item })) : [];
}
},
savePendingQueueForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
if (!key) return;
const list = Array.isArray(this.pendingResolutions) ? this.pendingResolutions : [];
if (list.length) {
this.workflowPendingSelections.set(key, this.clonePendingResolutions(list));
} else {
this.workflowPendingSelections.delete(key);
}
},
restorePendingQueueForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
const saved = key ? this.workflowPendingSelections.get(key) : null;
this.pendingResolutions = saved ? this.clonePendingResolutions(saved) : [];
this.rebuildPendingIndex();
this.updateApplyPendingButton?.();
this.updateQueuePanel?.();
this.updateQueueVisibility?.();
},
cloneAnalysisData(data = null) {
if (!data) return null;
try {
return JSON.parse(JSON.stringify(data));
} catch (error) {
console.warn('Model Resolver: failed to clone analysis data', error);
return data;
}
},
saveWorkflowModelCache(cache, signature, data) {
const key = this.getWorkflowScopedQueueKey();
if (!key) return;
if (signature && data) {
cache.set(key, {
signature,
data: this.cloneAnalysisData(data)
});
} else {
cache.delete(key);
}
},
restoreWorkflowModelCache(cache) {
const key = this.getWorkflowScopedQueueKey();
const saved = key ? cache.get(key) : null;
return {
signature: saved?.signature || null,
data: saved?.data ? this.cloneAnalysisData(saved.data) : null
};
},
saveAnalysisCacheForActiveWorkflow() {
this.saveWorkflowModelCache(
this.workflowAnalysisCaches,
this.cachedWorkflowSignature,
this.cachedAnalysisData
);
},
restoreAnalysisCacheForActiveWorkflow() {
const saved = this.restoreWorkflowModelCache(this.workflowAnalysisCaches);
this.cachedWorkflowSignature = saved.signature;
this.cachedAnalysisData = saved.data;
},
saveLoadedModelsCacheForActiveWorkflow() {
this.saveWorkflowModelCache(
this.workflowLoadedModelCaches,
this.cachedLoadedModelsSignature,
this.cachedLoadedModelsData
);
},
restoreLoadedModelsCacheForActiveWorkflow() {
const saved = this.restoreWorkflowModelCache(this.workflowLoadedModelCaches);
this.cachedLoadedModelsSignature = saved.signature;
this.cachedLoadedModelsData = saved.data;
},
invalidateLoadedModelsCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
this.cachedLoadedModelsSignature = null;
this.cachedLoadedModelsData = null;
if (key) {
this.workflowLoadedModelCaches.delete(key);
}
},
cloneDownloadTargetSelections(selections = this.downloadTargetSelections) {
const cloned = new Map();
for (const [missingKey, selection] of (selections || new Map()).entries()) {
try {
cloned.set(missingKey, JSON.parse(JSON.stringify(selection || {})));
} catch (error) {
console.warn('Model Resolver: failed to clone download target selection', error);
cloned.set(missingKey, { ...(selection || {}) });
}
}
return cloned;
},
saveDownloadTargetSelectionsForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
if (!key) return;
const selections = this.downloadTargetSelections instanceof Map
? this.downloadTargetSelections
: new Map();
if (selections.size) {
this.workflowDownloadTargetSelectionCaches.set(
key,
this.cloneDownloadTargetSelections(selections)
);
} else {
this.workflowDownloadTargetSelectionCaches.delete(key);
}
},
restoreDownloadTargetSelectionsForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
const saved = key ? this.workflowDownloadTargetSelectionCaches.get(key) : null;
this.downloadTargetSelections = saved
? this.cloneDownloadTargetSelections(saved)
: new Map();
},
cloneSearchState(state = {}, { preserveActive = false } = {}) {
let clone;
try {
clone = JSON.parse(JSON.stringify(state || {}));
} catch (error) {
console.warn('Model Resolver: failed to clone search state', error);
clone = { ...state };
}
clone.selectedSource = clone.selectedSource || 'all';
clone.selectedBaseModel = clone.selectedBaseModel || this.getDefaultSearchBaseModel?.() || 'auto';
clone.inputMode = clone.inputMode === 'name' ? 'name' : 'link';
clone.manualSearchQuery = String(clone.manualSearchQuery || '');
clone.results = this.mergeSearchResults({}, clone.results || {});
clone.explicitSearchSources = Array.isArray(clone.explicitSearchSources)
? clone.explicitSearchSources
: [];
clone.lastAttemptSources = Array.isArray(clone.lastAttemptSources)
? clone.lastAttemptSources
: [];
clone.lastAttemptBaseModelContext = clone.lastAttemptBaseModelContext || '';
clone.lastAttemptFound = clone.lastAttemptFound ?? null;
clone.lastAttemptError = clone.lastAttemptError || null;
clone.sourceProgress = clone.sourceProgress || {};
clone.activeSearchRunId = preserveActive ? (clone.activeSearchRunId || null) : null;
if (!preserveActive) {
for (const progress of Object.values(clone.sourceProgress)) {
if (!progress || (progress.status !== 'pending' && progress.status !== 'running')) continue;
progress.status = 'error';
progress.percent = 100;
progress.message = 'Search interrupted';
}
}
return clone;
},
cloneSearchResultCache(cache = this.searchResultCache, { preserveActive = false, workflowKey = null } = {}) {
const cloned = new Map();
for (const [missingSearchKey, state] of cache.entries()) {
if (!this.isPersistableSearchState(state)) {
continue;
}
const preserveEntryActive = Boolean(
preserveActive
&& workflowKey
&& state?.activeSearchRunId
&& this.hasBackgroundSearchJob?.(workflowKey, missingSearchKey, state.activeSearchRunId)
);
cloned.set(
missingSearchKey,
this.cloneSearchState(state, { preserveActive: preserveEntryActive })
);
}
return cloned;
},
isPersistableSearchState(state = {}) {
if ((state?.selectedSource || 'all') !== 'all') return true;
const defaultBaseModel = this.getDefaultSearchBaseModel?.() || 'auto';
if ((state?.selectedBaseModel || defaultBaseModel) !== defaultBaseModel) return true;
if (state?.lastAttemptError) return true;
if (this.hasSearchResults(state?.results || {})) return true;
const progressEntries = Object.values(state?.sourceProgress || {});
return Boolean(state?.activeSearchRunId) || progressEntries.some(progress => progress?.status);
},
saveSearchCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
if (!key) return;
const cloned = this.cloneSearchResultCache(this.searchResultCache, {
preserveActive: true,
workflowKey: key
});
if (cloned.size) {
this.workflowSearchResultCaches.set(key, cloned);
} else {
this.workflowSearchResultCaches.delete(key);
}
},
restoreSearchCacheForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
const saved = key ? this.workflowSearchResultCaches.get(key) : null;
this.searchResultCache = saved
? this.cloneSearchResultCache(saved, { preserveActive: true, workflowKey: key })
: new Map();
},
persistSearchStateForActiveWorkflow() {
this.saveSearchCacheForActiveWorkflow();
},
clearAllSearchProgressTimers() {
for (const timer of this.searchProgressTimers.values()) {
clearInterval(timer);
}
this.searchProgressTimers.clear();
for (const timer of this.backendSearchProgressTimers?.values?.() || []) {
clearInterval(timer.interval);
}
this.backendSearchProgressTimers?.clear?.();
},
syncWorkflowScopedQueue(workflow = null) {
const currentWorkflow = workflow || this.getCurrentWorkflow();
const nextRoute = this.getActiveWorkflowRouteKey();
const nextSignature = this.getWorkflowSignature(currentWorkflow);
const nextMissingSignature = this.getMissingWorkflowSignature(currentWorkflow);
const previousKey = this.getWorkflowScopedQueueKey();
const nextKey = this.getWorkflowScopedQueueKey(
nextRoute,
nextMissingSignature || 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.activeMissingWorkflowSignature = nextMissingSignature;
this.preserveSearchCacheAcrossNextWorkflowSync = false;
return;
}
this.savePendingQueueForActiveWorkflow();
this.saveAnalysisCacheForActiveWorkflow();
this.saveLoadedModelsCacheForActiveWorkflow();
this.saveSearchCacheForActiveWorkflow();
this.saveDownloadTargetSelectionsForActiveWorkflow();
this.clearWorkflowScopedState();
this.activeWorkflowRouteKey = nextRoute;
this.activeWorkflowSignature = nextSignature;
this.activeMissingWorkflowSignature = nextMissingSignature;
this.restorePendingQueueForActiveWorkflow();
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() {
const key = this.getWorkflowScopedQueueKey();
this.cachedWorkflowSignature = null;
this.cachedAnalysisData = null;
this.cachedLoadedModelsSignature = null;
this.cachedLoadedModelsData = null;
this._analysisProgressToken = null;
this._workflowDataLoadToken = null;
this._loadedModelsLoadToken = null;
this._loadedModelsProgressToken = null;
for (const [missingSearchKey, state] of this.searchResultCache.entries()) {
if (!this.hasBackgroundSearchJob?.(key, missingSearchKey, state.activeSearchRunId)) {
state.activeSearchRunId = null;
}
}
this.clearAllSearchProgressTimers();
this.searchResultCache.clear();
this.urnResolvePromises.clear();
this.urnLocalMatchPromises.clear();
this.localMatchRefreshTokens?.clear();
this.searchUiRefreshFrames?.clear();
this.downloadTargetSelections?.clear();
this.pendingResolutions = [];
this.rebuildPendingIndex();
this.updateApplyPendingButton?.();
this.updateQueuePanel?.();
this.updateQueueVisibility();
},
beginApplyWorkflowRefreshSuppression() {
this._applyWorkflowRefreshSuppressionDepth = Number(
this._applyWorkflowRefreshSuppressionDepth || 0
) + 1;
this._workflowRefreshGeneration = (this._workflowRefreshGeneration || 0) + 1;
if (this._workflowRefreshTimer) {
clearTimeout(this._workflowRefreshTimer);
this._workflowRefreshTimer = null;
}
if (this._workflowRefreshRetryTimer) {
clearTimeout(this._workflowRefreshRetryTimer);
this._workflowRefreshRetryTimer = null;
}
this._workflowDataLoadToken = null;
this._analysisProgressToken = null;
},
endApplyWorkflowRefreshSuppression() {
this._applyWorkflowRefreshSuppressionDepth = Math.max(
0,
Number(this._applyWorkflowRefreshSuppressionDepth || 0) - 1
);
},
scheduleActiveWorkflowRefresh(reason = 'workflow-change') {
if (!this.isVisible() || Number(this._applyWorkflowRefreshSuppressionDepth || 0) > 0) return;
const generation = (this._workflowRefreshGeneration || 0) + 1;
this._workflowRefreshGeneration = generation;
this._workflowRefreshExpectedRoute = this.getActiveWorkflowRouteKey();
this._workflowRefreshPreviousSignature = this.activeWorkflowSignature;
if (this._workflowRefreshTimer) {
clearTimeout(this._workflowRefreshTimer);
}
if (this._workflowRefreshRetryTimer) {
clearTimeout(this._workflowRefreshRetryTimer);
this._workflowRefreshRetryTimer = null;
}
this._workflowRefreshTimer = setTimeout(() => {
if (generation !== this._workflowRefreshGeneration) return;
this._workflowRefreshTimer = null;
this.refreshForActiveWorkflowChange({
reason,
expectedRoute: this._workflowRefreshExpectedRoute,
previousSignature: this._workflowRefreshPreviousSignature,
attempt: 0,
generation
});
}, 180);
},
async refreshForActiveWorkflowChange({
reason = 'workflow-change',
expectedRoute = '',
previousSignature = null,
attempt = 0,
generation = this._workflowRefreshGeneration,
candidateRoute = null,
candidateSignature = null
} = {}) {
if (
!this.isVisible()
|| Number(this._applyWorkflowRefreshSuppressionDepth || 0) > 0
) return;
if (generation !== this._workflowRefreshGeneration) return;
const currentRoute = this.getActiveWorkflowRouteKey();
if (expectedRoute && currentRoute !== expectedRoute) {
this.scheduleActiveWorkflowRefresh(reason);
return;
}
const workflow = this.getCurrentWorkflow();
const signature = this.getWorkflowSignature(workflow);
const missingSignature = this.getMissingWorkflowSignature(workflow);
const routeChanged = currentRoute !== this.activeWorkflowRouteKey;
const signatureChanged = signature && signature !== this.activeWorkflowSignature;
const missingSignatureChanged = missingSignature
&& missingSignature !== this.activeMissingWorkflowSignature;
const graphStillLooksOld = routeChanged && previousSignature && signature === previousSignature;
const workflowChangeStillPending = (
reason === 'node-widget-change'
&& !routeChanged
&& !signatureChanged
);
if ((!signature || graphStillLooksOld || workflowChangeStillPending) && attempt < 8) {
this._workflowRefreshRetryTimer = setTimeout(() => {
if (generation !== this._workflowRefreshGeneration) return;
this._workflowRefreshRetryTimer = null;
this.refreshForActiveWorkflowChange({
reason,
expectedRoute,
previousSignature,
attempt: attempt + 1,
generation,
candidateRoute,
candidateSignature
});
}, 180 + (attempt * 120));
return;
}
if (!routeChanged && !signatureChanged) return;
if ((candidateRoute !== currentRoute || candidateSignature !== signature) && attempt < 8) {
this._workflowRefreshRetryTimer = setTimeout(() => {
if (generation !== this._workflowRefreshGeneration) return;
this._workflowRefreshRetryTimer = null;
this.refreshForActiveWorkflowChange({
reason,
expectedRoute,
previousSignature,
attempt: attempt + 1,
generation,
candidateRoute: currentRoute,
candidateSignature: signature
});
}, 450);
return;
}
log.debug('Model Resolver: active workflow changed, refreshing current tab', {
reason,
route: currentRoute,
tab: this.activeTab
});
this.preserveSearchCacheAcrossNextWorkflowSync = Boolean(
!routeChanged && reason === 'node-widget-change'
);
this.syncWorkflowScopedQueue(workflow);
if (this.activeTab === 'missing') {
if (!routeChanged && missingSignature && !missingSignatureChanged) {
log.debug('Model Resolver: skipping Missing Models refresh for a strength-only workflow change');
return;
}
if (this.contentElement) this.contentElement.style.overflowY = 'auto';
await this.loadWorkflowData(workflow, {
preserveContent: !routeChanged && reason === 'node-widget-change'
});
} else if (this.activeTab === 'loaded') {
if (this.contentElement) this.contentElement.style.overflowY = 'auto';
await this.loadLoadedModels(workflow, {
preserveContent: !routeChanged && reason === 'node-widget-change'
});
}
}
};