dflow
Version:
is a minimal Dataflow programming engine
673 lines (671 loc) • 17.4 kB
TypeScript
/**
* Every dflow item has an identifier unique in its scope.
* A node or edge id is unique in its graph.
* An input or output id is unique in its node.
*/
export type DflowId = number | string;
/**
* A `DflowData` represents any data that can be serialized into JSON.
*/
export type DflowData =
| null
| boolean
| number
| string
| DflowArray
| DflowObject;
/** @ignore */
export type DflowObject = {
[Key in string]?: DflowData;
};
/** @ignore */
export type DflowArray = DflowData[];
export type DflowDataType = (typeof Dflow.dataTypes)[number];
/**
* Every dflow item (`DflowNode`, `DflowEdge`, etc.) and
* every dflow error (`DflowErrorItemNotFound`, `DflowErrorInvalidInputData`, etc.)
* is serializable into JSON.
*/
export interface DflowSerializable<Data extends DflowData> {
/**
* Return serializable data,
* i.e. an object that can be converted to JSON format.
* It will be called by `JSON.stringify`.
*/
toJSON(): Data;
}
export type DflowConstructorArg = {
nodesCatalog: DflowNodesCatalog;
};
/**
* `Dflow` represents a program as an executable graph.
* A graph can contain nodes and edges.
* Nodes are executed, sorted by their connections.
*/
export declare class Dflow
implements DflowSerializable<DflowSerializableGraph> {
readonly context: Record<string, unknown>;
readonly nodesCatalog: DflowNodesCatalog;
/** @ignore */
private nodesMap;
/** @ignore */
private edgesMap;
executionReport: DflowExecutionReport | null;
constructor({ nodesCatalog }: DflowConstructorArg);
static dataTypes: string[];
/**
* Empty graph.
*/
clear(): void;
/**
* Connect node A to node B.
*
* @example
* ```ts
* dflow.connect(nodeA).to(nodeB);
* ```
*
* Both `connect()` and `to()` accept an optional second parameter:
* the *position*, which defaults to 0.
*
* @example
* ```ts
* dflow.connect(nodeA, outputPosition).to(nodeB, inputPosition);
* ```
*
* @throws {DflowErrorItemNotFound}
*/
connect(sourceNode: DflowNode, sourcePosition?: number): {
to: (targetNode: DflowNode, targetPosition?: number) => void;
};
/**
* Delete edge with given id.
* @throws {DflowErrorItemNotFound}
*/
deleteEdge(edgeId: DflowId): void;
/**
* Delete node with given id.
* @throws {DflowErrorItemNotFound}
*/
deleteNode(nodeId: DflowId): void;
executeFunction(functionId: DflowId, args: DflowArray): DflowData | undefined;
/**
* @throws {DflowErrorItemNotFound}
*/
getEdgeById(id: DflowId): DflowEdge;
/**
* @throws {DflowErrorItemNotFound}
*/
getNodeById(id: DflowId): DflowNode;
newNode(arg: {
kind: string;
id?: DflowId;
inputs?: {
id?: DflowId;
}[];
outputs?: {
id?: DflowId;
data?: DflowData;
}[];
}): DflowNode;
/**
* @throws {DflowErrorItemNotFound}
*/
newEdge(
arg: {
id?: DflowId;
} & Pick<DflowEdge, "source" | "target">,
): DflowEdge;
/**
* List edge objects.
*/
get edges(): Pick<DflowEdge, "id" | "source" | "target">[];
/**
* List node objects.
*/
get nodes(): DflowSerializableNode[];
/** @ignore */
get nodeConnections(): DflowNodeConnection[];
/** @ignore */
get nodeIdsInsideFunctions(): DflowId[];
/**
* Execute all nodes, sorted by their connections.
*/
run(): Promise<void>;
/** @ignore */
toJSON(): DflowSerializableGraph;
/** @ignore */
static ancestorsOfNodeId(
nodeId: DflowId,
nodeConnections: DflowNodeConnection[],
): DflowId[];
/**
* Check that types of source are compatible with types of target.
* @ignore
*/
static canConnect(
sourceTypes: DflowDataType[],
targetTypes: DflowDataType[],
): boolean;
/** @ignore */
static childrenOfNodeId(nodeId: DflowId, nodeConnections: {
sourceId: DflowId;
targetId: DflowId;
}[]): DflowId[];
/** @ignore */
static executionNodeInfo: (
node: DflowNode,
error?: DflowSerializableError,
) => DflowExecutionNodeInfo;
/**
* Infer `DflowDataType` of given argument.
*/
static inferDataType(arg: unknown): DflowDataType[];
/** @ignore */
static levelOfNodeId(
nodeId: DflowId,
nodeConnections: DflowNodeConnection[],
): number;
/**
* `Dlow.input()` is a `DflowInputDefinition` helper.
*
* @example
* ```ts
* const { input } = Dflow;
*
* export class Echo extends DflowNode {
* static kind = "echo";
* static inputs = [input("string")];
* run () {
* console.log(this.input(0).data as string);
* }
* }
* ```
*
* Input with `number` type.
*
* @example
* ```ts
* Dflow.input("number")
* ```
*
* Optional `number` input.
*
* @example
*
* ```ts
* Dflow.input("number", { optional: true })
* ```
*
* Input that accepts both `number` and `string` type.
*
* @example
*
* ```ts
* Dflow.input(["number", "string"])
* ```
*
* Input with any type.
*
* @example
* ```ts
* Dflow.input()
* ```
*
* Input with type `array` and name.
*
* @example
* ```ts
* Dflow.input("array", { name: "list" })
* ```
*
* Input with any type and named "foo".
*
* @example
* ```ts
* Dflow.input([], { name: "foo" })
* ```
*/
static input(
typing?: DflowDataType | DflowDataType[],
rest?: Omit<DflowInputDefinition, "types">,
): DflowInputDefinition;
/**
* `Dflow.output()` is a `DflowOutputDefinition` helper.
*
* @example
* ```ts
* const { output } = Dflow;
*
* export class MathPI extends DflowNode {
* static kind = "mathPI";
* static outputs = [output("number", { name: "π", data: Math.PI })];
* }
* ```
*
* Named output with `number` type.
*
* @example
* ```ts
* Dflow.output("number", { name: "answer" })
* ```
*
* @see {@link Dflow.input} for other similar examples.
*
* `DflowOutputDefinition` has also an optional `data` attribute.
*
* @example
* ```ts
* Dflow.output("number", { data: 42, name: "answer" })
* ```
*/
static output(
typing?: DflowDataType | DflowDataType[],
rest?: Omit<DflowOutputDefinition, "types">,
): DflowOutputDefinition;
/** @ignore */
static parentsOfNodeId(nodeId: DflowId, nodeConnections: {
sourceId: DflowId;
targetId: DflowId;
}[]): DflowId[];
/** @ignore */
static sortNodesByLevel(
nodeIds: DflowId[],
nodeConnections: DflowNodeConnection[],
): DflowId[];
/**
* Type guard for `DflowArray`.
* It checks recursively that every element is some `DflowData`.
*/
static isArray(arg: unknown): arg is DflowArray;
/**
* Type guard for `DflowId`.
*/
static isDflowId(arg: unknown): arg is DflowId;
/**
* Type guard for `DflowObject`.
* It checks recursively that every value is some `DflowData`.
*/
static isObject(arg: unknown): arg is DflowObject;
/**
* Type guard for a valid number, i.e. finite and not `NaN`.
*/
static isNumber(arg: unknown): arg is number;
/**
* Type guard for `DflowData`.
*/
static isDflowData(arg: unknown): arg is DflowData;
/**
* Validate that data belongs to some of given types.
*/
static isValidDataType(types: DflowDataType[], data: unknown): boolean;
}
/**
* `DflowIO` is a base type for `DflowInput` and `DflowOutput`.
*/
type DflowIO = {
readonly id: DflowId;
readonly name?: string;
readonly nodeId: DflowId;
readonly types: DflowDataType[];
};
/**
* A `DflowNode` describes its inputs as a list of `DflowInputDefinition`.
* @example
* ```json
* {
* "name": "label",
* "types": ["string"],
* "optional": true
* }
* ```
*/
export type DflowInputDefinition = {
name?: string;
types: DflowDataType[];
optional?: boolean;
};
export type DflowSerializableInput = {
id: DflowId;
};
/**
* A `DflowInput` is a node input.
*
* @implements DflowSerializable<DflowSerializableInput>
*/
export declare class DflowInput
implements DflowIO, DflowSerializable<DflowSerializableInput> {
readonly id: DflowId;
readonly name?: string;
readonly nodeId: DflowId;
readonly types: DflowDataType[];
source?: DflowOutput;
/**
* By default an input is **not** `optional`.
* If an input is not `optional` and its data is not defined then its node will not be executed.
* If an input is `optional`, then its node will be executed even if the inputs has no data.
*/
optional?: boolean;
constructor({ id, name, nodeId, optional, types }: {
id: DflowId;
nodeId: DflowId;
} & DflowInputDefinition);
/**
* An input data is a reference to its connected output data, if any.
*/
get data(): DflowData | undefined;
/** @ignore */
toJSON(): DflowSerializableInput;
}
/**
* A `DflowNode` describes its outputs as a list of `DflowOutputDefinition`.
* @example
* ```json
* {
* "name": "sum",
* "types": ["number"],
* }
* ```
*/
export type DflowOutputDefinition = {
name?: string;
types: DflowDataType[];
data?: DflowData;
};
export type DflowSerializableOutput = {
id: DflowId;
/** data */
d?: DflowData;
};
/**
* A `DflowOutput` is a node output.
*
* @implements DflowSerializable<DflowSerializableOutput>
*/
export declare class DflowOutput
implements DflowIO, DflowSerializable<DflowSerializableOutput> {
readonly id: DflowId;
readonly name?: string;
readonly nodeId: DflowId;
readonly types: DflowDataType[];
private value;
constructor({ id, data, name, nodeId, types }: {
id: DflowId;
nodeId: DflowId;
} & DflowOutputDefinition);
get data(): DflowData | undefined;
set data(arg: unknown);
clear(): void;
/** @ignore */
toJSON(): DflowSerializableOutput;
}
export type DflowSerializableNode = {
id: DflowId;
/** kind */
k: DflowNode["kind"];
/** inputs */
i?: DflowSerializableInput[];
/** outputs */
o?: DflowSerializableOutput[];
};
/**
* `DflowNode` constructor accepts a single argument.
*
* You can import `DflowNodeConstructorArg` type as a helper,
* for example if you need to create a node that does something in the constructor.
*
* @example
* ```ts
* class DflowNodeFunction extends DflowNode {
* static kind = "function";
* static outputs = [output("DflowId", { name: "id" })];
* constructor(arg: DflowNodeConstructorArg) {
* super(arg);
* this.output(0).data = this.id;
* }
* }
* ```
*/
export type DflowNodeConstructorArg =
& Pick<DflowNode, "id" | "kind" | "host">
& {
inputs?: ({
id?: DflowId;
} & DflowInputDefinition)[];
outputs?: ({
id?: DflowId;
} & DflowOutputDefinition)[];
};
/**
* `DflowNode` represents a block of code: it can have inputs and outputs.
*
* Extend it to create a node.
*
* @example
* ```ts
* const { input, output } = DflowNode;
* class Addition extends DflowNode {
* static kind = "addition";
* static inputs = [input("number"), input("number")];
* static outputs = [output("number")];
* run() {
* this.output(0).data = (this.input(0).data as number) +
* (this.input(1).data as number);
* }
* }
* ```
*
* @implements DflowSerializable<DflowSerializableNode>
*/
export declare class DflowNode
implements DflowSerializable<DflowSerializableNode> {
readonly id: DflowId;
/** @ignore */
private inputsMap;
/** @ignore */
private outputsMap;
/** @ignore */
private inputPosition;
/** @ignore */
private outputPosition;
/**
* Every dflow node must have its own `kind` that is used as *key*
* to address it in the nodes catalog.
*/
readonly kind: string;
/**
* `DflowNode` has a reference to its `Dflow` host.
* It can be used in the node `run()` implementation.
*/
readonly host: Dflow;
constructor({ id, kind, inputs, outputs, host }: DflowNodeConstructorArg);
get inputsDataAreValid(): boolean;
clearOutputs(): void;
/**
* @throws {DflowErrorItemNotFound}
*/
getInputById(id: DflowId): DflowInput;
/**
* Get input by position.
*
* @throws {DflowErrorItemNotFound}
*/
input(position: number): DflowInput;
/**
* @throws {DflowErrorItemNotFound}
*/
getOutputById(id: DflowId): DflowOutput;
/**
* Get output by position.
*
* @throws {DflowErrorItemNotFound}
*/
output(position: number): DflowOutput;
/** @ignore this method, it should be overridden. */
run(): void | Promise<void>;
/** @ignore */
toJSON(): DflowSerializableNode;
}
export type DflowSerializableEdge = {
id: DflowId;
s: DflowEdge["source"];
t: DflowEdge["target"];
};
/**
* `DflowEdge` connects an `DflowOutput` to a `DflowInput`.
*/
export type DflowEdge = {
readonly id: DflowId;
/**
* Path to output.
*/
readonly source: [nodeId: DflowId, outputId: DflowId];
/**
* Path to input.
*/
readonly target: [nodeId: DflowId, inputId: DflowId];
};
/**
* A class extending `DflowNode` must implement `DflowNodeDefinition` interface,
* to be used as a value in a `DflowNodesCatalog`.
*/
export interface DflowNodeDefinition {
new (arg: DflowNodeConstructorArg): DflowNode;
kind: DflowNode["kind"];
inputs?: DflowInputDefinition[];
outputs?: DflowOutputDefinition[];
}
/**
* A `DflowNodesCatalog` is a record containing node classes indexed by their kind.
*
* @example
* ```ts
* const nodesCatalog: DflowNodesCatalog = {
* myNode: MyNodeClass
* }
* ```
*/
export type DflowNodesCatalog = Record<DflowNode["kind"], DflowNodeDefinition>;
/**
* Contains info about node execution, that is:
* the serialized node except its inputs; an error, if any.
*/
export type DflowExecutionNodeInfo = Omit<DflowSerializableNode, "i"> & {
/** Error during execution */
err?: DflowSerializableError;
};
export type DflowExecutionReport = {
start: number;
end: number;
steps: DflowExecutionNodeInfo[];
};
export type DflowSerializableGraph = {
nodes: DflowSerializableNode[];
edges: DflowSerializableEdge[];
};
type DflowNodeConnection = {
sourceId: DflowId;
targetId: DflowId;
};
/**
* This class is used to instantiate a new node which `kind` was not found in `nodesCatalog`.
* The "unknown" node class is not included in `coreNodesCatalog`.
*/
export declare class DflowNodeUnknown extends DflowNode {
}
/** Builtin nodes, always included in `nodesCatalog`. */
export declare const coreNodesCatalog: DflowNodesCatalog;
export type DflowSerializableErrorCode = {
/** error code */
_: string;
};
export type DflowSerializableError =
| DflowSerializableErrorItemNotFound
| DflowSerializableErrorInvalidInputData
| DflowSerializableErrorCannotConnectSourceToTarget
| DflowSerializableErrorCannotExecuteAsyncFunction;
export type DflowSerializableErrorCannotConnectSourceToTarget =
& DflowSerializableErrorCode
& {
/** source */
s: DflowErrorCannotConnectSourceToTarget["source"];
/** target */
t: DflowErrorCannotConnectSourceToTarget["target"];
};
export declare class DflowErrorCannotConnectSourceToTarget extends Error
implements
DflowSerializable<DflowSerializableErrorCannotConnectSourceToTarget> {
readonly source: DflowEdge["source"];
readonly target: DflowEdge["target"];
static code: string;
static message(
{ s, t }: Omit<DflowSerializableErrorCannotConnectSourceToTarget, "_">,
): string;
constructor(
{ source, target }: Pick<
DflowErrorCannotConnectSourceToTarget,
"source" | "target"
>,
);
/** @ignore */
toJSON(): DflowSerializableErrorCannotConnectSourceToTarget;
}
export type DflowSerializableErrorInvalidInputData =
& DflowSerializableErrorCode
& {
/** nodeId */
nId: DflowErrorInvalidInputData["nodeId"];
};
export declare class DflowErrorInvalidInputData extends Error
implements DflowSerializable<DflowSerializableErrorInvalidInputData> {
static code: string;
readonly nodeId: DflowId;
static message(
{ nId: nodeId }: Omit<DflowSerializableErrorInvalidInputData, "_">,
): string;
constructor(nodeId: DflowErrorInvalidInputData["nodeId"]);
/** @ignore */
toJSON(): DflowSerializableErrorInvalidInputData;
}
export type DflowSerializableErrorItemNotFound = DflowSerializableErrorCode & {
item: DflowErrorItemNotFound["item"];
id?: DflowErrorItemNotFound["info"]["id"];
/** nodeId */
nId?: DflowErrorItemNotFound["info"]["nodeId"];
/** position */
p?: DflowErrorItemNotFound["info"]["position"];
};
export declare class DflowErrorItemNotFound extends Error
implements DflowSerializable<DflowSerializableErrorItemNotFound> {
static code: string;
readonly item: "node" | "edge" | "input" | "output";
readonly info: Partial<{
id: DflowId;
nodeId: DflowId;
position: number;
}>;
static message(
{ item, id, nId: nodeId, p: position }: Omit<
DflowSerializableErrorItemNotFound,
"_"
>,
): string;
constructor(
item: DflowErrorItemNotFound["item"],
info?: DflowErrorItemNotFound["info"],
);
/** @ignore */
toJSON(): DflowSerializableErrorItemNotFound;
}
export type DflowSerializableErrorCannotExecuteAsyncFunction =
DflowSerializableErrorCode;
export declare class DflowErrorCannotExecuteAsyncFunction extends Error
implements
DflowSerializable<DflowSerializableErrorCannotExecuteAsyncFunction> {
static code: string;
static message(): string;
constructor();
/** @ignore */
toJSON(): DflowSerializableErrorCode;
}
export {};