frontend-app-api: filter out failed attachments

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-01-23 13:52:47 +01:00
parent 492503a244
commit efea76beb0
3 changed files with 66 additions and 12 deletions
+1
View File
@@ -6,3 +6,4 @@ Updated error reporting and app tree resolution logic to attribute errors to the
- If an attachment fails to provide the required input data, the error is now attributed to the attachment rather than the parent extension.
- Singleton extension inputs will now only forward attachment errors if the input is required.
- Array extension inputs will now filter out failed attachments instead of failing the entire app tree resolution.
@@ -1677,6 +1677,55 @@ describe('instantiateAppNodeTree', () => {
},
]);
});
it('should filter out failed attachments for non-singleton inputs', () => {
const node = makeNode(
resolveExtensionDefinition(
createExtension({
name: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
inputs: {
children: createExtensionInput([otherDataRef]),
},
output: [inputCountRef],
factory: ({ inputs }) => [
inputCountRef(inputs.children.length),
],
}),
{ namespace: 'app' },
),
);
expect(
createAppNodeInstance({
apis: testApis,
attachments: new Map([
[
'children',
[
attachmentWithoutRequiredData,
makeInstanceWithId(simpleExtension, {
other: 42,
}),
],
],
]),
node,
collector,
})?.getData(inputCountRef),
).toBe(1);
expect(collector.collectErrors()).toEqual([
{
code: 'EXTENSION_INPUT_DATA_MISSING',
message:
"extension 'app/test' could not be attached because its output data ('test') does not match what the input 'children' requires ('other')",
context: {
node: attachmentWithoutRequiredData,
inputName: 'children',
},
},
]);
});
});
});
});
@@ -38,21 +38,22 @@ const INSTANTIATION_FAILED = new Error('Instantiation failed');
function mapWithFailures<T, U>(
iterable: Iterable<T>,
callback: (item: T) => U,
options?: { ignoreFailures?: boolean },
): U[] {
let failed = false;
const results = Array.from(iterable).map(item => {
const results = [];
for (const item of iterable) {
try {
return callback(item);
results.push(callback(item));
} catch (error) {
if (error === INSTANTIATION_FAILED) {
failed = true;
} else {
throw error;
}
return null as any;
}
});
if (failed) {
}
if (failed && !options?.ignoreFailures) {
throw INSTANTIATION_FAILED;
}
return results;
@@ -292,13 +293,16 @@ function resolveV2Inputs(
}
}
return mapWithFailures(attachedNodes, attachment =>
resolveInputDataContainer(
input.extensionData,
attachment,
inputName,
collector,
),
return mapWithFailures(
attachedNodes,
attachment =>
resolveInputDataContainer(
input.extensionData,
attachment,
inputName,
collector,
),
{ ignoreFailures: true },
);
}) as ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput }>;
}