fix: prevent infinite recursion in rgthree Power Lora Loader adapter (#7)

- Remove node.widgets.splice monkey-patching in rgthree Power Lora Loader adapter to avoid infinite recursion with Vue 3 reactive array proxies on modern ComfyUI frontend.

- Rely on setDirtyCanvas to trigger inspection updates.

- Update test suite to verify no widgets.splice wrapping occurs and reactive proxies do not recurse.
This commit is contained in:
Dariusz L
2026-11-08 23:36:19 +01:00
parent f9d9fe65d6
commit 3237a46ab3
2 changed files with 62 additions and 24 deletions
+53 -1
View File
@@ -1769,7 +1769,7 @@ test('rgthree Power Lora Loader updates strength without reloading workflow anal
]);
});
test('rgthree Power Lora Loader detects removals made through its context menu', async () => {
test('rgthree Power Lora Loader detects removals when canvas is dirtied', async () => {
const configureCustomNodeModelAdapter = eval(
`(${extractMethod(modelResolverSource, 'configureCustomNodeModelAdapter')})`
);
@@ -1807,6 +1807,7 @@ test('rgthree Power Lora Loader detects removals made through its context menu',
configureCustomNodeModelAdapter.call(resolver, node);
node.widgets.splice(1, 1);
node.setDirtyCanvas(true, true);
await Promise.resolve();
assert.deepEqual(calls, [
@@ -1815,6 +1816,57 @@ test('rgthree Power Lora Loader detects removals made through its context menu',
]);
});
test('rgthree Power Lora Loader does not monkey-patch widgets.splice or cause recursion on reactive proxies', () => {
const configureCustomNodeModelAdapter = eval(
`(${extractMethod(modelResolverSource, 'configureCustomNodeModelAdapter')})`
);
const getCustomNodeModelListSignature = eval(
`(${extractMethod(modelResolverSource, 'getCustomNodeModelListSignature')})`
);
const getCustomNodeModelStrengthSignature = eval(
`(${extractMethod(modelResolverSource, 'getCustomNodeModelStrengthSignature')})`
);
const rawWidgets = [
{ name: 'lora_1', value: { on: true, lora: 'first.safetensors', strength: 1 } },
];
const reactiveWidgets = new Proxy(rawWidgets, {
get(target, prop, receiver) {
if (prop === 'splice') {
return function(...args) {
return target[prop].apply(this, args);
};
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value) {
target[prop] = value;
return true;
},
});
const node = {
comfyClass: 'Power Lora Loader (rgthree)',
widgets: reactiveWidgets,
setDirtyCanvas() {},
};
const resolver = {
customNodeModelAdapterStates: new WeakMap(),
getCustomNodeModelListSignature,
getCustomNodeModelStrengthSignature,
scheduleNodeContextMenuAnalysis: () => {},
dialog: {
isWorkflowRefreshSuppressed: () => false,
scheduleActiveWorkflowRefresh: () => {},
},
};
configureCustomNodeModelAdapter.call(resolver, node);
assert.strictEqual(node.widgets.splice.__modelResolverCustomNodeAdapter, undefined);
assert.doesNotThrow(() => {
node.widgets.splice(0, 1);
});
});
test('rgthree dynamic lora widgets bypass the generic full workflow refresh', () => {
const configureNodeContextMenu = eval(
`(${extractMethod(modelResolverSource, 'configureNodeContextMenu')})`
@@ -32,31 +32,17 @@ function observePowerLoraLoader(node, notify) {
typeof node.setDirtyCanvas !== 'function'
|| node.setDirtyCanvas.__modelResolverCustomNodeAdapter
) {
if (!Array.isArray(node.widgets)) return;
} else {
const originalSetDirtyCanvas = node.setDirtyCanvas;
const wrappedSetDirtyCanvas = function() {
const result = originalSetDirtyCanvas.apply(this, arguments);
queueInspection();
return result;
};
wrappedSetDirtyCanvas.__modelResolverCustomNodeAdapter = true;
node.setDirtyCanvas = wrappedSetDirtyCanvas;
return;
}
if (
typeof node.widgets.splice === 'function'
&& !node.widgets.splice.__modelResolverCustomNodeAdapter
) {
const originalSplice = node.widgets.splice;
const wrappedSplice = function() {
const result = originalSplice.apply(this, arguments);
queueInspection();
return result;
};
wrappedSplice.__modelResolverCustomNodeAdapter = true;
node.widgets.splice = wrappedSplice;
}
const originalSetDirtyCanvas = node.setDirtyCanvas;
const wrappedSetDirtyCanvas = function() {
const result = originalSetDirtyCanvas.apply(this, arguments);
queueInspection();
return result;
};
wrappedSetDirtyCanvas.__modelResolverCustomNodeAdapter = true;
node.setDirtyCanvas = wrappedSetDirtyCanvas;
}
export const rgthreePowerLoraLoaderAdapter = Object.freeze({