refactor(frontend): extract workflow identity state
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)}_`);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user