frontend-app-api: tweak app graph to have an explicit root node

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-10-18 10:55:55 +02:00
parent 2ff857bdb4
commit 6e6458f9c2
4 changed files with 104 additions and 75 deletions
@@ -22,7 +22,7 @@ import {
export const Core = createExtension({
id: 'core',
attachTo: { id: 'root', input: 'default' },
attachTo: { id: 'root', input: 'default' }, // ignored
inputs: {
apis: createExtensionInput({
api: coreExtensionData.apiFactory,
@@ -19,66 +19,70 @@ import { buildAppGraph } from './buildAppGraph';
const extBaseConfig = {
id: 'test',
attachTo: { id: 'root', input: 'default' },
attachTo: { id: 'nonexistent', input: 'nonexistent' },
output: {},
factory() {},
};
const extension = createExtension(extBaseConfig);
const baseSpec = { extension, disabled: false };
const baseSpec = {
extension,
attachTo: { id: 'nonexistent', input: 'nonexistent' },
disabled: false,
};
describe('buildAppGraph', () => {
it('creates an empty graph', () => {
const graph = buildAppGraph([]);
expect([...graph.rootNodes.keys()]).toEqual([]);
expect([...graph.orphanNodes.keys()]).toEqual([]);
it('should fail to create an empty graph', () => {
expect(() => buildAppGraph([])).toThrow(
"No root node with id 'core' found in app graph",
);
});
it('should create a graph with only one node', () => {
const graph = buildAppGraph([{ ...baseSpec, id: 'core' }]);
expect(graph.root).toEqual({
spec: { ...baseSpec, id: 'core' },
edges: { attachments: new Map() },
});
expect(Array.from(graph.orphans)).toEqual([]);
});
it('should create a graph', () => {
const graph = buildAppGraph([
{ ...baseSpec, id: 'a' },
{ ...baseSpec, id: 'b' },
{ ...baseSpec, id: 'c' },
{ ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' },
{ ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' },
{ ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' },
{ ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' },
]);
expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']);
expect([...graph.orphanNodes.keys()]).toEqual(['dx1']);
expect(JSON.parse(JSON.stringify([...graph.rootNodes.values()])))
.toMatchInlineSnapshot(`
const graph = buildAppGraph(
[
{
"id": "a",
{ ...baseSpec, id: 'a' },
{ ...baseSpec, id: 'b' },
{ ...baseSpec, id: 'c' },
{ ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' },
{ ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' },
{ ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' },
{ ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' },
],
'b',
);
expect(JSON.parse(JSON.stringify(graph.root))).toMatchInlineSnapshot(`
{
"attachments": {
"x": [
{
"id": "bx1",
},
{
"id": "bx2",
},
],
"y": [
{
"id": "by1",
},
],
},
{
"attachments": {
"x": [
{
"id": "bx1",
},
{
"id": "bx2",
},
],
"y": [
{
"id": "by1",
},
],
},
"id": "b",
},
{
"id": "c",
},
]
"id": "b",
}
`);
expect(String(graph.rootNodes.get('a'))).toMatchInlineSnapshot(`"<a />"`);
expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(`
expect(String(graph.root)).toMatchInlineSnapshot(`
"<b>
x [
<bx1 />
@@ -89,24 +93,32 @@ describe('buildAppGraph', () => {
]
</b>"
`);
expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`"<c />"`);
const orphans = Array.from(graph.orphans).map(String);
expect(orphans).toMatchInlineSnapshot(`
[
"<a />",
"<c />",
"<dx1 />",
]
`);
});
it('should create a graph out of order', () => {
const graph = buildAppGraph([
{ ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' },
{ ...baseSpec, id: 'a' },
{ ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' },
{ ...baseSpec, id: 'b' },
{ ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' },
{ ...baseSpec, id: 'c' },
{ ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' },
]);
expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']);
expect([...graph.orphanNodes.keys()]).toEqual(['dx1']);
const graph = buildAppGraph(
[
{ ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' },
{ ...baseSpec, id: 'a' },
{ ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' },
{ ...baseSpec, id: 'b' },
{ ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' },
{ ...baseSpec, id: 'c' },
{ ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' },
],
'b',
);
expect(String(graph.rootNodes.get('a'))).toMatchInlineSnapshot(`"<a />"`);
expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(`
expect(String(graph.root)).toMatchInlineSnapshot(`
"<b>
x [
<bx2 />
@@ -117,7 +129,15 @@ describe('buildAppGraph', () => {
]
</b>"
`);
expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`"<c />"`);
const orphans = Array.from(graph.orphans).map(String);
expect(orphans).toMatchInlineSnapshot(`
[
"<a />",
"<c />",
"<dx1 />",
]
`);
});
it('throws an error when duplicated extensions are detected', () => {
@@ -78,9 +78,15 @@ class SerializableAppNode implements AppNode {
* tree with all attachments in the same order as they appear in the input specs array.
* @internal
*/
export function buildAppGraph(specs: AppNodeSpec[]): AppGraph {
export function buildAppGraph(
specs: AppNodeSpec[],
rootNodeId = 'core',
): AppGraph {
const nodes = new Map<string, AppNode>();
const rootNodes = new Map<string, AppNode>();
// A node with the provided rootNodeId must be found in the graph, and it must not be attached to anything
let rootNode: AppNode | undefined = undefined;
// While iterating through the inputs specs we keep track of all nodes that were created
// before their parent, and attach them later when the parent is created.
// As we find the parents and attach the children, we remove them from this map. This means
@@ -99,7 +105,10 @@ export function buildAppGraph(specs: AppNodeSpec[]): AppGraph {
const node = new SerializableAppNode(spec);
nodes.set(spec.id, node);
if (spec.attachTo) {
// TODO: For now we simply ignore the attachTo spec of the root node, but it'd be cleaner if we could avoid defining it
if (spec.id === rootNodeId) {
rootNode = node;
} else {
const parent = nodes.get(spec.attachTo.id);
if (parent) {
(node.edges as Mutable<AppNodeEdges>).attachedTo = {
@@ -123,8 +132,6 @@ export function buildAppGraph(specs: AppNodeSpec[]): AppGraph {
orphansByParent.set(spec.attachTo.id, [orphan]);
}
}
} else {
rootNodes.set(spec.id, node);
}
const orphanedChildren = orphansByParent.get(spec.id);
@@ -142,11 +149,13 @@ export function buildAppGraph(specs: AppNodeSpec[]): AppGraph {
}
}
const orphanNodes = new Map(
Array.from(orphansByParent).flatMap(([, orphans]) =>
orphans.map(({ orphan }) => [orphan.spec.id, orphan]),
),
const orphanNodes = Array.from(orphansByParent).flatMap(([, orphans]) =>
orphans.map(({ orphan }) => orphan),
);
return { rootNodes, orphanNodes };
if (!rootNode) {
throw new Error(`No root node with id '${rootNodeId}' found in app graph`);
}
return { root: rootNode, orphans: orphanNodes };
}
@@ -31,7 +31,7 @@ export type Mutable<T> = {
*/
export interface AppNodeSpec {
readonly id: string;
readonly attachTo?: { id: string; input: string };
readonly attachTo: { id: string; input: string };
readonly extension: Extension<unknown>;
readonly disabled: boolean;
readonly config?: unknown;
@@ -70,6 +70,6 @@ export interface AppNode {
}
export interface AppGraph {
rootNodes: Map<string, AppNode>;
orphanNodes: Map<string, AppNode>;
root: AppNode;
orphans: Iterable<AppNode>;
}