diff --git a/src/plugins/dataflow/alternative/README.md b/src/plugins/dataflow/alternative/README.md new file mode 100644 index 0000000000..fa1b533de2 --- /dev/null +++ b/src/plugins/dataflow/alternative/README.md @@ -0,0 +1,19 @@ +**This folder contains files that aren't actually used.** + +This is a demo of system that could replace the Rete model and engine. + +The demo is incomplete: +- it is missing caching of the data results. So it might be less efficient then Rete. +- it doesn't include MST for the nodes which would be needed for simple serialization of the model. +- it doesn't have an example of "parameters", these would be things used by the data functions in addition to the inputs when they compute their value. For example the number node has a parameter which is the number. +- it doesn't include an example of time. Several of our nodes vary their value based on time, for example the ramp node and the new wait feature of the hold node. + +The "parameters" and time would impact how the caching works. +The MST based nodes might impact the TS work. + +One of the useful parts of the demo is the types. The nodes can define their input and output ports using objects that would be available at runtime. And these same definitions are used by the type system to compute the input parameters to the data function. + +This works by using a few features of TS: +- *`as const`*: this tells TS a object is literal all of the way down. This makes all parts of readonly. And it also means any usage of its types return the literal values not the types. So you get back `"foo"` instead of `string`. +- *mapped types*: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html this allows you to convert one type to another with the same keys but different types for the values. +- *indexed access types*: https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html this lets you look up a type by a property name on another type. diff --git a/src/plugins/dataflow/alternative/df-alt-core.ts b/src/plugins/dataflow/alternative/df-alt-core.ts new file mode 100644 index 0000000000..a52ef93149 --- /dev/null +++ b/src/plugins/dataflow/alternative/df-alt-core.ts @@ -0,0 +1,77 @@ +// abstract class Port { +// type: string; +// } + +// This interface is used as a map for the type system +export interface PortTypes { + number: number; + string: string; + boolean: boolean; +} + +// New types can be supported by redefining PortTypes with an additional +// type +export interface PortMap { + [index: string]: { type: keyof PortTypes } +} + +// This make the props readonly but that seems OK +export type DataParams = { + [Property in keyof Type]: PortTypes[Type[Property]["type"]] +}; + +export interface Connection { + nodeId: string, + nodePort: string +} + +type Connections = { + -readonly [Property in keyof Type]?: Connection +}; + +export interface GNodeType { + id: string; + inputConnections?: Record; + data: (inputs: any) => unknown; +} + +export abstract class GNode + implements GNodeType +{ + inputDefinitions: Inputs; + outputDefinitions: Outputs; + + id: string; + + inputConnections: Inputs extends PortMap ? Connections : undefined; + + // Replace the "any" return type with + abstract data(inputs: Inputs extends PortMap ? DataParams : undefined): + Outputs extends PortMap ? DataParams : undefined; +} + +// Common output type shared by many nodes +export const valueNodeOutputs = { + value: { type: "number" } +} as const; + +export type ValueNodeOutputsType = typeof valueNodeOutputs; + +// Additional port types can in theory be added by re-opening the PortTypes interface +// Like this +// interface PortTypes { +// newType: { special: string} +// } +// +// And then they can be referred to like this: +// const specialNodeInputs = { +// a: { type: "newType" } +// } as const; +// +// And then DataParams will handle the correctly: +// type SpecialNodeDataParams = DataParams; +// +// However Typescript won't let you re-open an interface that is imported. +// So this would work to let some other module add its own type to the core. +// In our case, it seems fine to have a fixed set of port types so it isn't +// necessary to solve this. diff --git a/src/plugins/dataflow/alternative/df-alt-demo.ts b/src/plugins/dataflow/alternative/df-alt-demo.ts new file mode 100644 index 0000000000..40a0119561 --- /dev/null +++ b/src/plugins/dataflow/alternative/df-alt-demo.ts @@ -0,0 +1,29 @@ +// Demo of running the engine with a simple graph +// This can be run with +// npx tsx src/plugins/dataflow/alternative/df-alt-demo.ts + +import { GNodeType } from "./df-alt-core"; +import { engineFetch } from "./df-alt-engine"; +import { ValueNode } from "./df-alt-num-node"; +import { SumNode } from "./df-alt-sum-node"; + +const value1 = new ValueNode(); +value1.id = "1"; +const value2 = new ValueNode(); +value2.id = "2"; + +const sum = new SumNode(); +sum.id = "3"; +sum.inputConnections = {}; +sum.inputConnections.left = {nodeId: value1.id, nodePort: "value"}; +sum.inputConnections.right = {nodeId: value2.id, nodePort: "value"}; + +const nodeMap: Record = { + 1: value1, + 2: value2, + 3: sum +}; + +const fetchedResult = engineFetch(sum, nodeMap); + +console.log("fetchedResult", fetchedResult); diff --git a/src/plugins/dataflow/alternative/df-alt-engine.ts b/src/plugins/dataflow/alternative/df-alt-engine.ts new file mode 100644 index 0000000000..2a66ad99f1 --- /dev/null +++ b/src/plugins/dataflow/alternative/df-alt-engine.ts @@ -0,0 +1,55 @@ +import { GNodeType } from "./df-alt-core"; + + +export function engineFetch(startNode: GNodeType, nodeMap: Record ) { + console.log({"startNode.inputConnections": startNode.inputConnections}); + const predecessors: GNodeType[] = [startNode]; + getPredecessors(startNode, nodeMap, predecessors); + const nodeOutputs: Record = {}; + console.log({predecessors}); + + // Go backward through the predecessors. + // Since they are ordered depth first and we are assuming a non-cyclic graph, + // this will guarantee the inputs to each predecessor will be available. + for (let index = predecessors.length - 1; index >= 0; index--) { + const node = predecessors[index]; + if (!node) continue; + const inputValues = getInputValues(node, nodeOutputs); + nodeOutputs[node.id] = node.data(inputValues); + } + return nodeOutputs[startNode.id]; +} + +// This will do a depth first traversal of the tree +function getPredecessors(node: GNodeType, nodeMap: Record, predecessors: GNodeType[]) { + Object.values(node.inputConnections || {}).forEach(conn => { + const inputNode = conn && nodeMap[conn.nodeId]; + if (inputNode) { + predecessors.push(inputNode); + getPredecessors(inputNode, nodeMap, predecessors); + } + }); +} + +function getInputValues(node: GNodeType, nodeOutputs: Record) { + const inputValues: Record = {}; + if (node.inputConnections) { + Object.entries(node.inputConnections).forEach(([inputPort, connection]) => { + if (!connection) return; + const { nodeId: predecessorId, nodePort: predecessorPort } = connection; + const nodeOutput = nodeOutputs[predecessorId]; + if (!nodeOutput) { + console.warn("Output from predecessor is not available yet", + {currentNodeId: node.id, predecessorId, predecessorPort}); + return; + } + if (!nodeOutput[predecessorPort]) { + console.warn("Predecessor didn't provide value on port of connection", + {currentNodeId: node.id, predecessorId, predecessorPort}); + return; + } + inputValues[inputPort] = nodeOutput[predecessorPort]; + }); + } + return inputValues; +} diff --git a/src/plugins/dataflow/alternative/df-alt-num-node.ts b/src/plugins/dataflow/alternative/df-alt-num-node.ts new file mode 100644 index 0000000000..0f0fb965c4 --- /dev/null +++ b/src/plugins/dataflow/alternative/df-alt-num-node.ts @@ -0,0 +1,9 @@ +import { GNode, ValueNodeOutputsType, valueNodeOutputs } from "./df-alt-core"; + +export class ValueNode extends GNode { + public outputDefinitions = valueNodeOutputs; + + data() { + return { value: 1} ; + } +} diff --git a/src/plugins/dataflow/alternative/df-alt-sum-node.ts b/src/plugins/dataflow/alternative/df-alt-sum-node.ts new file mode 100644 index 0000000000..ef6c4ca815 --- /dev/null +++ b/src/plugins/dataflow/alternative/df-alt-sum-node.ts @@ -0,0 +1,29 @@ +import { DataParams, GNode, ValueNodeOutputsType, valueNodeOutputs } from "./df-alt-core"; + +// This *can't* be typed with: +// const sumNodeInputs: PortMap = { ... } +// If it is, then `typeof sumNodeInputs` returns PortMap instead of the literal. +// DataParams needs the literal so it can generate the type. +// The `as const` at the end tells TS to make the whole object a literal not just +// top level keys. +// This means that the runtime definition can't be enforced at definition +// time, but when it is used in DataParams the type is enforced. +const sumNodeInputs = { + left: { type: "number" }, + right: { type: "number" }, +} as const; + +// This creates an type of +// { left: number, right: number } +type SumNodeDataParams = DataParams; + +export class SumNode extends GNode { + public inputDefinitions = sumNodeInputs; + public outputDefinitions = valueNodeOutputs; + + // I'm not sure why the `: SumNodeDataParams` is necessary here it should be picked up + // from the generic abstract class + data({left, right}: SumNodeDataParams) { + return { value: (left + right) }; + } +}