МодуліDataflowControl flowAllmatter
У цьому гайді описано ключові аспекти побудови вузлів Module, які обробляють вкладені графи, враховуючи доступні вузли Input і Output.
Цей гайд спирається виключно на підхід Dataflow для спрощення розуміння коду. Ознайомившись із цим гайдом і наданим прикладом, ви зможете Control flow за допомогою інструкцій у гайді Control flow.
Основна ідея модулів полягає у створенні окремих графів з вузлами Input і Output. Наступним кроком є створення виділеного вузла Module, який відображатиме порти на основі вузлів відповідного типу, указаного в графі.
Для початку давайте створимо вузол, який слугуватиме нашою точкою входу:
export class InputNode extends ClassicPreset.Node {
public value = null;
constructor(public key: string) {
super("Input");
this.addOutput("value", new ClassicPreset.Output(socket, "Number"));
}
data() {
return { value: this.value };
}
}
Визначений користувачем key
має вирішальне значення для асоціації його з вхідним портом вузла Module. Крім того, нам потрібно вказати властивість value
для введення вхідних даних.
Для того, щоб модуль мав хоч якусь користь, необхідний вузол Output
export class OutputNode extends ClassicPreset.Node {
constructor(public key: string) {
super("Output");
this.addInput("value", new ClassicPreset.Input(socket, "Number"));
}
data() {
return {};
}
}
У цьому випадку метод data
повертає порожній об’єкт, оскільки вхідні дані можна отримати за допомогою методу fetchInputs
без необхідності виконання вузла.
Вузол Module, який служить порталом у вкладений граф і відображає вхідні та вихідні порти, є найскладнішим вузлом. Давайте розглянемо спрощений приклад:
export class ModuleNode {
module: null | Module<Schemes> = null;
constructor(path: string) {
super("Module");
this.setModule(path);
}
public async setModule(path: string) {
this.module = findModule(path);
await removeNodeConnections(this.id);
if (this.module) {
const { inputs, outputs } = this.module.getPorts();
syncPorts(this, inputs, outputs);
} else {
syncPorts(this, [], []);
}
}
async data(inputs: Record<string, any>) {
const data = await this.module?.exec(inputs);
return data || {};
}
}
де
findModule
повертає об'єкт, що представляє модуль, дозволяючи доступ до його портів для відображення та виконання вкладеного графуsyncPorts
оновлює вхідні та вихідні порти, видаляючи застарілі та додаючи новіremoveNodeConnections
видаляє всі підключення, дозволяючи нам видаляти порти, коли нам потрібно перемикати модуліМайте на увазі, що внесення будь-яких динамічних змін до вузлів, як показано в цьому прикладі за допомогою syncPorts
, потребує виклику area.update('node', node.id)
.
Щоб запобігти конфліктним викликам із кількох вузлів Module, які використовують один і той самий вкладений граф, обов’язково ініціалізуйте новий редактор і механізм у методі module.exec
.
Ось спрощений приклад того, як може бути реалізований обробник вкладених графів:
function findModule(path: string) {
return {
getPorts() {
const editor = new NodeEditor<Schemes>();
await importGraphByPath(path, editor);
const nodes = editor.getNodes();
const inputs = nodes
.filter((n): n is InputNode => n instanceof InputNode)
.map((n) => n.key);
const outputs = nodes
.filter((n): n is OutputNode => n instanceof OutputNode)
.map((n) => n.key);
return {
inputs,
outputs
};
},
exec: async (inputData: Record<string, any>) => {
const engine = new DataflowEngine<Schemes>();
const editor = new NodeEditor<Schemes>();
editor.use(engine);
await importGraphByPath(path, editor);
const nodes = editor.getNodes();
injectInputs(nodes, inputData);
return retrieveOutputs(nodes, engine);
}
};
де
getPorts
отримує ключі для вузлів Input і Output і повертає їхimportGraphByPath
призначений для завантаження основних вузлів і з’єднань для вашого модуля в редакторКожен виклик створює новий екземпляр редактора, щоб уникнути конфліктів під час обробки графу.
Наступний метод включає введення вхідних даних вузла Module у вузли Input вкладеного графа.
function injectInputs(nodes: Schemes["Node"][], inputData: Record<string, any>) {
const inputNodes = nodes.filter(node => node instanceof InputNode);
inputNodes.forEach((node) => {
// майте на увазі, що вхідних з’єднань може не бути, і ми припускаємо, що можливе максимум одне з’єднання
node.value = inputData[node.key] && inputData[node.key][0];
});
}
Після введення вхідних даних наступним кроком є отримання вихідних даних із вузлів:
async function retrieveOutputs(nodes: Schemes["Node"][], engine: DataflowEngine<Schemes>) {
const outputNodes = nodes.filter(node => node instanceof OutputNode);
// можна обробляти одночасно
const outputs = await Promise.all(outputNodes.map(async node => {
const data = await engine.fetchInputs(node.id);
// ми розглядаємо лише дані з першого з’єднання, оскільки вхідне з’єднання може бути щонайбільше одне
return [node.key, data.value[0]] as const;
}));
return Object.fromEntries(outputs);
}
Перегляньте повний результат на сторінці прикладу Модулі. Крім того, цей підхід реалізовано в додатку Allmatter.