Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Demo version of a dataflow replacement #2238

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/plugins/dataflow/alternative/README.md
Original file line number Diff line number Diff line change
@@ -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.
77 changes: 77 additions & 0 deletions src/plugins/dataflow/alternative/df-alt-core.ts
Original file line number Diff line number Diff line change
@@ -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<Type extends PortMap> = {
[Property in keyof Type]: PortTypes[Type[Property]["type"]]
};

export interface Connection {
nodeId: string,
nodePort: string
}

type Connections<Type extends PortMap> = {
-readonly [Property in keyof Type]?: Connection
};

export interface GNodeType {
id: string;
inputConnections?: Record<string, Connection | undefined>;
data: (inputs: any) => unknown;
}

export abstract class GNode<Inputs extends PortMap | undefined, Outputs extends PortMap | undefined>
implements GNodeType
{
inputDefinitions: Inputs;
outputDefinitions: Outputs;

id: string;

inputConnections: Inputs extends PortMap ? Connections<Inputs> : undefined;

// Replace the "any" return type with
abstract data(inputs: Inputs extends PortMap ? DataParams<Inputs> : undefined):
Outputs extends PortMap ? DataParams<Outputs> : 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<typeof specialNodeInputs>;
//
// 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.
29 changes: 29 additions & 0 deletions src/plugins/dataflow/alternative/df-alt-demo.ts
Original file line number Diff line number Diff line change
@@ -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<string, GNodeType> = {
1: value1,
2: value2,
3: sum
};

const fetchedResult = engineFetch(sum, nodeMap);

console.log("fetchedResult", fetchedResult);
55 changes: 55 additions & 0 deletions src/plugins/dataflow/alternative/df-alt-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { GNodeType } from "./df-alt-core";


export function engineFetch(startNode: GNodeType, nodeMap: Record<string, GNodeType> ) {
console.log({"startNode.inputConnections": startNode.inputConnections});
const predecessors: GNodeType[] = [startNode];
getPredecessors(startNode, nodeMap, predecessors);
const nodeOutputs: Record<string, any> = {};
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<string, GNodeType>, 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<string, any>) {
const inputValues: Record<string, any> = {};
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;
}
9 changes: 9 additions & 0 deletions src/plugins/dataflow/alternative/df-alt-num-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { GNode, ValueNodeOutputsType, valueNodeOutputs } from "./df-alt-core";

export class ValueNode extends GNode<undefined, ValueNodeOutputsType > {
public outputDefinitions = valueNodeOutputs;

data() {
return { value: 1} ;
}
}
29 changes: 29 additions & 0 deletions src/plugins/dataflow/alternative/df-alt-sum-node.ts
Original file line number Diff line number Diff line change
@@ -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<typeof sumNodeInputs>;

export class SumNode extends GNode<typeof sumNodeInputs, ValueNodeOutputsType> {
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) };
}
}
Loading