refactor(frontend): extract workflow identity state

This commit is contained in:
Dariusz L
2026-08-02 13:58:30 +02:00
parent cdcb27acf4
commit 678bcb460a
5 changed files with 174 additions and 218 deletions
+10 -7
View File
@@ -21,6 +21,7 @@ import { startSplitterDrag } from '../web/resolver/utils/splitter_drag.js';
import { baseModelAliasMethods } from '../web/resolver/search/base_model_alias_methods.js';
import { searchHashMethods } from '../web/resolver/search/search_hash_methods.js';
import { missingModelStateMethods } from '../web/resolver/views/missing_model_state_methods.js';
import { workflowIdentityMethods } from '../web/resolver/shell/workflow_identity_methods.js';
import {
getCustomNodeModelAdapter,
getCustomNodeModelCategory,
@@ -1832,13 +1833,15 @@ test('Power Lora Loader strength updates only its Loaded Models chip and cache',
});
test('changed workflow model keeps the selected row and batch checkbox for the same loader slot', () => {
const encodeMissingModelKeyPart = eval(`(${extractMethod(lifecycleGraphMethodsSource, 'encodeMissingModelKeyPart')})`);
const getMissingModelIdentityPart = eval(`(${extractMethod(lifecycleGraphMethodsSource, 'getMissingModelIdentityPart')})`);
const getMissingModelKey = eval(`(${extractMethod(lifecycleGraphMethodsSource, 'getMissingModelKey')})`);
const getMissingModelWorkflowSlotKeys = eval(`(${extractMethod(lifecycleGraphMethodsSource, 'getMissingModelWorkflowSlotKeys')})`);
const findMissingModelReplacement = eval(`(${extractMethod(lifecycleGraphMethodsSource, 'findMissingModelReplacement')})`);
const resolvePreservedMissingModelKey = eval(`(${extractMethod(lifecycleGraphMethodsSource, 'resolvePreservedMissingModelKey')})`);
const remapMissingModelKeys = eval(`(${extractMethod(lifecycleGraphMethodsSource, 'remapMissingModelKeys')})`);
const {
encodeMissingModelKeyPart,
getMissingModelIdentityPart,
getMissingModelKey,
getMissingModelWorkflowSlotKeys,
findMissingModelReplacement,
resolvePreservedMissingModelKey,
remapMissingModelKeys,
} = workflowIdentityMethods;
const dialog = {
encodeMissingModelKeyPart,
getMissingModelIdentityPart,
+2 -54
View File
@@ -1,58 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import assert from 'node:assert/strict';
const projectRoot = path.resolve(import.meta.dirname, '..');
const lifecycleGraphMethodsSource = fs.readFileSync(
path.join(projectRoot, 'web/resolver/shell/lifecycle_graph_methods.js'),
'utf8'
);
function extractMethod(source, methodName) {
const signature = new RegExp(`\\n\\s+${methodName}\\s*\\(`);
const match = signature.exec(source);
assert.ok(match, `Could not find ${methodName}`);
const parenStart = source.indexOf('(', match.index);
let parenDepth = 0;
let parenEnd = -1;
for (let i = parenStart; i < source.length; i += 1) {
if (source[i] === '(') parenDepth += 1;
if (source[i] === ')') parenDepth -= 1;
if (parenDepth === 0) {
parenEnd = i;
break;
}
}
const params = source.slice(parenStart + 1, parenEnd);
const braceStart = source.indexOf('{', parenEnd);
let braceDepth = 0;
for (let i = braceStart; i < source.length; i += 1) {
if (source[i] === '{') braceDepth += 1;
if (source[i] === '}') braceDepth -= 1;
if (braceDepth === 0) {
return `function ${methodName}(${params}) ${source.slice(braceStart, i + 1)}`;
}
}
throw new Error(`Could not parse ${methodName}`);
}
globalThis.getCustomNodeOriginalIdentity = missing => missing.original_identity || '';
const methodNames = [
'encodeMissingModelKeyPart',
'getMissingModelIdentityPart',
'getMissingModelKey',
'getMissingModelWorkflowSlotKeys',
'findMissingModelReplacement',
'resolvePreservedMissingModelKey',
'remapMissingModelKeys',
'getMissingModelDomKey',
];
const methods = Object.fromEntries(methodNames.map(methodName => [
methodName,
eval(`(${extractMethod(lifecycleGraphMethodsSource, methodName)})`),
]));
import { workflowIdentityMethods as methods } from '../web/resolver/shell/workflow_identity_methods.js';
test('missing model identities and keys encode workflow scope deterministically', () => {
const dialog = { ...methods, getMissingModelIdentityPart: methods.getMissingModelIdentityPart };
@@ -65,7 +13,7 @@ test('missing model identities and keys encode workflow scope deterministically'
};
assert.equal(dialog.encodeMissingModelKeyPart('check point'), 'check%20point');
assert.equal(dialog.getMissingModelIdentityPart({ original_identity: 'custom-id' }), 'custom-id');
assert.equal(dialog.getMissingModelIdentityPart({ custom_node_original_identity: 'custom-id' }), 'custom-id');
assert.equal(dialog.getMissingModelIdentityPart(missing), missing.original_path);
assert.equal(
dialog.getMissingModelKey(missing),
+2
View File
@@ -18,6 +18,7 @@ import { searchHashMethods } from "./search/search_hash_methods.js";
import { searchStateMethods } from "./search/search_state_methods.js";
import { missingModelStateMethods } from "./views/missing_model_state_methods.js";
import { modelHashCompareMethods } from "./views/model_hash_compare_methods.js";
import { workflowIdentityMethods } from "./shell/workflow_identity_methods.js";
import { queueMethods } from "./actions/queue_methods.js";
import { queueStorageMethods } from "./actions/queue_storage_methods.js";
import { downloadProgressMethods } from "./actions/download_progress_methods.js";
@@ -345,6 +346,7 @@ applyDialogMethods(
searchStateMethods,
missingModelStateMethods,
modelHashCompareMethods,
workflowIdentityMethods,
searchPanelMethods,
modelInfoMethods,
renderFormatMethods,
@@ -1,5 +1,4 @@
import { app } from "../../../../../scripts/app.js";
import { getCustomNodeOriginalIdentity } from "../custom_nodes/registry.js";
import { safeStorage } from "../utils/html_utils.js";
export const lifecycleGraphMethods = {
async show(workflow = null) {
@@ -804,160 +803,4 @@ export const lifecycleGraphMethods = {
}
},
encodeMissingModelKeyPart(value) {
return encodeURIComponent(String(value ?? '').trim());
},
getMissingModelIdentityPart(missing = {}) {
return getCustomNodeOriginalIdentity(missing)
|| missing.original_path
|| missing.expected_filename
|| missing.name
|| missing.filename
|| missing.urn_string
|| '';
},
getMissingModelKey(missing = {}) {
if (missing.missing_key) {
return String(missing.missing_key);
}
const nodeId = missing.node_id ?? '';
const widgetIndex = missing.widget_index ?? '';
const subgraphId = missing.subgraph_id || '';
const scope = missing.is_top_level !== false ? 'T' : 'F';
const nestedKey = missing.nested_key || '';
const category = missing.category || '';
const identity = this.getMissingModelIdentityPart(missing);
return [
nodeId,
widgetIndex,
subgraphId,
scope,
nestedKey,
category,
identity
].map(value => this.encodeMissingModelKeyPart(value)).join(':');
},
getMissingModelWorkflowSlotKeys(missing = {}) {
const references = [missing];
if (Array.isArray(missing.all_node_refs)) {
references.push(...missing.all_node_refs);
}
const seen = new Set();
const keys = [];
for (const reference of references) {
const nodeId = reference?.node_id;
if (nodeId === undefined || nodeId === null || nodeId === '') continue;
const scope = reference.is_top_level !== false ? 'T' : 'F';
const key = [
scope,
reference.subgraph_id || '',
nodeId,
reference.widget_index ?? '',
reference.nested_key || ''
].map(value => this.encodeMissingModelKeyPart(value)).join(':');
if (seen.has(key)) continue;
seen.add(key);
keys.push(key);
}
return keys;
},
findMissingModelReplacement(previousMissing = {}, missingModels = [], preferredIndex = 0) {
const previousSlots = this.getMissingModelWorkflowSlotKeys(previousMissing);
if (!previousSlots.length || !Array.isArray(missingModels) || !missingModels.length) {
return null;
}
const previousSlotPositions = new Map(
previousSlots.map((slotKey, index) => [slotKey, index])
);
let best = null;
missingModels.forEach((missing, index) => {
const candidateSlots = this.getMissingModelWorkflowSlotKeys(missing);
let slotRank = Number.POSITIVE_INFINITY;
candidateSlots.forEach((slotKey, candidateSlotIndex) => {
const previousSlotIndex = previousSlotPositions.get(slotKey);
if (previousSlotIndex === undefined) return;
slotRank = Math.min(slotRank, previousSlotIndex + candidateSlotIndex);
});
if (!Number.isFinite(slotRank)) return;
const rank = [
slotRank,
Math.abs(index - preferredIndex),
index
];
if (
!best
|| rank[0] < best.rank[0]
|| (rank[0] === best.rank[0] && rank[1] < best.rank[1])
|| (rank[0] === best.rank[0] && rank[1] === best.rank[1] && rank[2] < best.rank[2])
) {
best = { missing, rank };
}
});
return best?.missing || null;
},
resolvePreservedMissingModelKey(missingModels = [], previousMissingModels = [], previousKey = '') {
if (!previousKey || !Array.isArray(missingModels) || !missingModels.length) return '';
const exact = missingModels.find(missing => this.getMissingModelKey(missing) === previousKey);
if (exact) return previousKey;
const previousIndex = previousMissingModels.findIndex(
missing => this.getMissingModelKey(missing) === previousKey
);
if (previousIndex < 0) return '';
const replacement = this.findMissingModelReplacement(
previousMissingModels[previousIndex],
missingModels,
previousIndex
);
return replacement ? this.getMissingModelKey(replacement) : '';
},
remapMissingModelKeys(
missingModels = [],
previousMissingModels = [],
previousKeys = null,
currentMissingModels = null
) {
const currentKeys = new Set(
(Array.isArray(currentMissingModels) ? currentMissingModels : missingModels)
.map(missing => this.getMissingModelKey(missing))
);
const remappedKeys = new Set();
for (const previousKey of previousKeys || []) {
if (
currentKeys.has(previousKey)
&& !missingModels.some(missing => this.getMissingModelKey(missing) === previousKey)
) {
continue;
}
const nextKey = this.resolvePreservedMissingModelKey(
missingModels,
previousMissingModels,
previousKey
);
if (nextKey) remappedKeys.add(nextKey);
}
return remappedKeys;
},
getMissingModelDomKey(missing = {}) {
const key = this.getMissingModelKey(missing);
try {
return btoa(key).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
} catch (_e) {
return key.replace(/[^A-Za-z0-9_-]/g, char => `_${char.charCodeAt(0).toString(16)}_`);
}
}
};
@@ -0,0 +1,160 @@
import { getCustomNodeOriginalIdentity } from "../custom_nodes/registry.js";
export const workflowIdentityMethods = {
encodeMissingModelKeyPart(value) {
return encodeURIComponent(String(value ?? '').trim());
},
getMissingModelIdentityPart(missing = {}) {
return getCustomNodeOriginalIdentity(missing)
|| missing.original_path
|| missing.expected_filename
|| missing.name
|| missing.filename
|| missing.urn_string
|| '';
},
getMissingModelKey(missing = {}) {
if (missing.missing_key) {
return String(missing.missing_key);
}
const nodeId = missing.node_id ?? '';
const widgetIndex = missing.widget_index ?? '';
const subgraphId = missing.subgraph_id || '';
const scope = missing.is_top_level !== false ? 'T' : 'F';
const nestedKey = missing.nested_key || '';
const category = missing.category || '';
const identity = this.getMissingModelIdentityPart(missing);
return [
nodeId,
widgetIndex,
subgraphId,
scope,
nestedKey,
category,
identity
].map(value => this.encodeMissingModelKeyPart(value)).join(':');
},
getMissingModelWorkflowSlotKeys(missing = {}) {
const references = [missing];
if (Array.isArray(missing.all_node_refs)) {
references.push(...missing.all_node_refs);
}
const seen = new Set();
const keys = [];
for (const reference of references) {
const nodeId = reference?.node_id;
if (nodeId === undefined || nodeId === null || nodeId === '') continue;
const scope = reference.is_top_level !== false ? 'T' : 'F';
const key = [
scope,
reference.subgraph_id || '',
nodeId,
reference.widget_index ?? '',
reference.nested_key || ''
].map(value => this.encodeMissingModelKeyPart(value)).join(':');
if (seen.has(key)) continue;
seen.add(key);
keys.push(key);
}
return keys;
},
findMissingModelReplacement(previousMissing = {}, missingModels = [], preferredIndex = 0) {
const previousSlots = this.getMissingModelWorkflowSlotKeys(previousMissing);
if (!previousSlots.length || !Array.isArray(missingModels) || !missingModels.length) {
return null;
}
const previousSlotPositions = new Map(
previousSlots.map((slotKey, index) => [slotKey, index])
);
let best = null;
missingModels.forEach((missing, index) => {
const candidateSlots = this.getMissingModelWorkflowSlotKeys(missing);
let slotRank = Number.POSITIVE_INFINITY;
candidateSlots.forEach((slotKey, candidateSlotIndex) => {
const previousSlotIndex = previousSlotPositions.get(slotKey);
if (previousSlotIndex === undefined) return;
slotRank = Math.min(slotRank, previousSlotIndex + candidateSlotIndex);
});
if (!Number.isFinite(slotRank)) return;
const rank = [
slotRank,
Math.abs(index - preferredIndex),
index
];
if (
!best
|| rank[0] < best.rank[0]
|| (rank[0] === best.rank[0] && rank[1] < best.rank[1])
|| (rank[0] === best.rank[0] && rank[1] === best.rank[1] && rank[2] < best.rank[2])
) {
best = { missing, rank };
}
});
return best?.missing || null;
},
resolvePreservedMissingModelKey(missingModels = [], previousMissingModels = [], previousKey = '') {
if (!previousKey || !Array.isArray(missingModels) || !missingModels.length) return '';
const exact = missingModels.find(missing => this.getMissingModelKey(missing) === previousKey);
if (exact) return previousKey;
const previousIndex = previousMissingModels.findIndex(
missing => this.getMissingModelKey(missing) === previousKey
);
if (previousIndex < 0) return '';
const replacement = this.findMissingModelReplacement(
previousMissingModels[previousIndex],
missingModels,
previousIndex
);
return replacement ? this.getMissingModelKey(replacement) : '';
},
remapMissingModelKeys(
missingModels = [],
previousMissingModels = [],
previousKeys = null,
currentMissingModels = null
) {
const currentKeys = new Set(
(Array.isArray(currentMissingModels) ? currentMissingModels : missingModels)
.map(missing => this.getMissingModelKey(missing))
);
const remappedKeys = new Set();
for (const previousKey of previousKeys || []) {
if (
currentKeys.has(previousKey)
&& !missingModels.some(missing => this.getMissingModelKey(missing) === previousKey)
) {
continue;
}
const nextKey = this.resolvePreservedMissingModelKey(
missingModels,
previousMissingModels,
previousKey
);
if (nextKey) remappedKeys.add(nextKey);
}
return remappedKeys;
},
getMissingModelDomKey(missing = {}) {
const key = this.getMissingModelKey(missing);
try {
return btoa(key).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
} catch (_e) {
return key.replace(/[^A-Za-z0-9_-]/g, char => `_${char.charCodeAt(0).toString(16)}_`);
}
},
};