UNPKG

dflow

Version:

is a minimal Dataflow programming engine

2 lines (1 loc) 13.6 kB
const generateItemId=(itemMap,idPrefix,wantedId)=>{if(wantedId&&!itemMap.has(wantedId))return wantedId;const id=`${idPrefix}${itemMap.size}`;return itemMap.has(id)?generateItemId(itemMap,idPrefix):id};export class Dflow{context;nodesCatalog;nodesMap=new Map;edgesMap=new Map;executionReport=null;constructor({nodesCatalog}){this.nodesCatalog={...nodesCatalog,...coreNodesCatalog};this.context={}}static dataTypes=["null","boolean","number","string","array","object","DflowId"];clear(){this.nodesMap.clear();this.edgesMap.clear()}connect(sourceNode,sourcePosition=0){return{to:(targetNode,targetPosition=0)=>{const sourceOutput=sourceNode.output(sourcePosition);const targetInput=targetNode.input(targetPosition);this.newEdge({source:[sourceNode.id,sourceOutput.id],target:[targetNode.id,targetInput.id]})}}}deleteEdge(edgeId){const edge=this.getEdgeById(edgeId);const[targetNodeId,targetInputId]=edge.target;const targetNode=this.getNodeById(targetNodeId);const targetInput=targetNode.getInputById(targetInputId);targetInput.source=void 0;this.edgesMap.delete(edgeId)}deleteNode(nodeId){const node=this.getNodeById(nodeId);for(const edge of this.edges){const{source:[sourceNodeId],target:[targetNodeId]}=edge;if(sourceNodeId===node.id||targetNodeId===node.id){this.deleteEdge(edge.id)}}this.nodesMap.delete(nodeId)}executeFunction(functionId,args){const nodeConnections=this.nodeConnections;const childrenNodeIds=Dflow.childrenOfNodeId(functionId,nodeConnections);const returnNodeIds=[];for(const childrenNodeId of childrenNodeIds){const node=this.getNodeById(childrenNodeId);if(node.kind===DflowNodeReturn.kind){returnNodeIds.push(node.id)}}const nodeIdsInsideFunction=returnNodeIds.reduce((accumulator,returnNodeId,index,array)=>{const ancestors=Dflow.ancestorsOfNodeId(returnNodeId,nodeConnections);const result=accumulator.concat(ancestors);return index===array.length?[...new Set(result)]:result},[]);const nodeIds=Dflow.sortNodesByLevel([...returnNodeIds,...nodeIdsInsideFunction],nodeConnections);for(const nodeId of nodeIds){const node=this.getNodeById(nodeId);switch(node.kind){case DflowNodeArgument.kind:{const position=node.input(0).data;const index=typeof position==="number"&&!isNaN(position)?Math.max(position,0):0;node.output(0).data=args[index];break}case DflowNodeReturn.kind:{return node.input(1).data}default:{if(node.run.constructor.name==="AsyncFunction"){throw new DflowErrorCannotExecuteAsyncFunction}node.run();this.executionReport?.steps?.push(Dflow.executionNodeInfo(node))}}}}getEdgeById(id){const item=this.edgesMap.get(id);if(!item)throw new DflowErrorItemNotFound("edge",{id});return item}getNodeById(id){const item=this.nodesMap.get(id);if(!item)throw new DflowErrorItemNotFound("node",{id});return item}newNode(arg){const NodeClass=this.nodesCatalog[arg.kind]??DflowNodeUnknown;const id=generateItemId(this.nodesMap,"n",arg.id);const inputs=NodeClass.inputs?.map((definition,i)=>{const obj=arg.inputs?.[i];const id2=obj?.id??`i${i}`;return{id:id2,...obj,...definition}})??[];const outputs=NodeClass.outputs?.map((definition,i)=>{const obj=arg.outputs?.[i];const id2=obj?.id??`o${i}`;return{id:id2,...obj,...definition}})??[];const node=new NodeClass({id,kind:arg.kind,host:this,inputs,outputs});this.nodesMap.set(node.id,node);return node}newEdge(arg){const id=generateItemId(this.edgesMap,"e",arg.id);const edge={...arg,id};this.edgesMap.set(edge.id,edge);const[sourceNodeId,sourceOutputId]=edge.source;const[targetNodeId,targetInputId]=edge.target;const sourceNode=this.getNodeById(sourceNodeId);const targetNode=this.getNodeById(targetNodeId);const sourceOutput=sourceNode.getOutputById(sourceOutputId);const targetInput=targetNode.getInputById(targetInputId);if(!Dflow.canConnect(sourceOutput.types,targetInput.types)){throw new DflowErrorCannotConnectSourceToTarget({source:[sourceNode.id,sourceOutput.id],target:[targetNode.id,targetInput.id]})}targetInput.source=sourceOutput;return edge}get edges(){return[...this.edgesMap.values()].map(({id,source,target})=>({id,source,target}))}get nodes(){return[...this.nodesMap.values()].map(item=>item.toJSON())}get nodeConnections(){return[...this.edgesMap.values()].map(edge=>({sourceId:edge.source[0],targetId:edge.target[0]}))}get nodeIdsInsideFunctions(){const ancestorsOfReturnNodes=[];for(const node of[...this.nodesMap.values()]){if(node.kind==="return"){ancestorsOfReturnNodes.push(Dflow.ancestorsOfNodeId(node.id,this.nodeConnections))}}return[...new Set(ancestorsOfReturnNodes.flat())]}async run(){const executionReport={start:Date.now(),end:Date.now(),steps:[]};const nodeIdsExcluded=this.nodeIdsInsideFunctions;const nodeIds=Dflow.sortNodesByLevel([...this.nodesMap.keys()].filter(nodeId=>!nodeIdsExcluded.includes(nodeId)),this.nodeConnections);for(const nodeId of nodeIds){const node=this.nodesMap.get(nodeId);if(!node.inputsDataAreValid){const error=new DflowErrorInvalidInputData(nodeId);executionReport.steps.push(Dflow.executionNodeInfo(node,error.toJSON()));node.clearOutputs();continue}if(node.run.constructor.name==="AsyncFunction"){await node.run()}else{node.run()}executionReport.steps.push(Dflow.executionNodeInfo(node))}executionReport.end=Date.now();this.executionReport=executionReport}toJSON(){return{nodes:[...this.nodesMap.values()].map(item=>item.toJSON()),edges:[...this.edgesMap.values()].map(({id,source:s,target:t})=>({id,s,t}))}}static ancestorsOfNodeId(nodeId,nodeConnections){const parentsNodeIds=Dflow.parentsOfNodeId(nodeId,nodeConnections);if(parentsNodeIds.length===0)return[];return parentsNodeIds.reduce((accumulator,parentNodeId,index,array)=>{const ancestors=Dflow.ancestorsOfNodeId(parentNodeId,nodeConnections);const result=accumulator.concat(ancestors);return index===array.length-1?[...new Set(array.concat(result))]:result},[])}static canConnect(sourceTypes,targetTypes){if(sourceTypes.length===0||targetTypes.length===0)return true;return targetTypes.some(dataType=>sourceTypes.includes(dataType))}static childrenOfNodeId(nodeId,nodeConnections){return nodeConnections.filter(({sourceId})=>nodeId===sourceId).map(({targetId})=>targetId)}static executionNodeInfo=(node,error)=>{const{id,k,o}=node.toJSON();const info={id,k};if(o)info.o=o;if(error)info.err=error;return info};static inferDataType(arg){if(arg===null)return["null"];if(typeof arg==="boolean")return["boolean"];if(typeof arg==="string")return["string"];if(Dflow.isNumber(arg))return["number"];if(Dflow.isArray(arg))return["array"];if(Dflow.isObject(arg))return["object"];return[]}static levelOfNodeId(nodeId,nodeConnections){const parentsNodeIds=Dflow.parentsOfNodeId(nodeId,nodeConnections);if(parentsNodeIds.length===0)return 0;let maxLevel=0;for(const parentNodeId of parentsNodeIds){const level=Dflow.levelOfNodeId(parentNodeId,nodeConnections);maxLevel=Math.max(level,maxLevel)}return maxLevel+1}static input(typing=[],rest){return{types:typeof typing==="string"?[typing]:typing,...rest}}static output(typing=[],rest){return{types:typeof typing==="string"?[typing]:typing,...rest}}static parentsOfNodeId(nodeId,nodeConnections){return nodeConnections.filter(({targetId})=>nodeId===targetId).map(({sourceId})=>sourceId)}static sortNodesByLevel(nodeIds,nodeConnections){const levelOf={};for(const nodeId of nodeIds){levelOf[nodeId]=Dflow.levelOfNodeId(nodeId,nodeConnections)}return nodeIds.slice().sort((a,b)=>levelOf[a]<=levelOf[b]?-1:1)}static isArray(arg){return Array.isArray(arg)&&arg.every(Dflow.isDflowData)}static isDflowId(arg){return typeof arg==="string"&&arg!==""||typeof arg==="number"}static isObject(arg){return typeof arg==="object"&&arg!==null&&!Array.isArray(arg)&&Object.values(arg).every(Dflow.isDflowData)}static isNumber(arg){return typeof arg==="number"&&!isNaN(arg)&&Number.isFinite(arg)}static isDflowData(arg){if(arg===void 0)return false;return arg===null||typeof arg==="boolean"||typeof arg==="string"||Dflow.isNumber(arg)||Dflow.isObject(arg)||Dflow.isArray(arg)||Dflow.isDflowId(arg)}static isValidDataType(types,data){if(types.length===0)return true;return types.some(dataType=>dataType==="null"?data===null:dataType==="boolean"?typeof data==="boolean":dataType==="string"?typeof data==="string":dataType==="number"?Dflow.isNumber(data):dataType==="object"?Dflow.isObject(data):dataType==="array"?Dflow.isArray(data):dataType==="DflowId"?Dflow.isDflowId(data):false)}}export class DflowInput{id;name;nodeId;types;source;optional;constructor({id,name,nodeId,optional,types}){if(name)this.name=name;this.types=types;this.nodeId=nodeId;this.id=id;if(optional)this.optional=optional}get data(){return this.source?.data}toJSON(){return{id:this.id}}}export class DflowOutput{id;name;nodeId;types;value;constructor({id,data,name,nodeId,types}){if(name)this.name=name;this.types=types;this.nodeId=nodeId;this.id=id;this.value=data}get data(){return this.value}set data(arg){if(arg===void 0){this.value===void 0;return}const{types}=this;if(types.length===0&&Dflow.isDflowData(arg)||types.includes("null")&&arg===null||types.includes("boolean")&&typeof arg==="boolean"||types.includes("string")&&typeof arg==="string"||types.includes("number")&&Dflow.isNumber(arg)||types.includes("object")&&Dflow.isObject(arg)||types.includes("array")&&Dflow.isArray(arg)||types.includes("DflowId")&&Dflow.isDflowId(arg)){this.value=arg}}clear(){this.value=void 0}toJSON(){const obj={id:this.id};if(this.value!==void 0)obj.d=this.value;return obj}}export class DflowNode{id;inputsMap=new Map;outputsMap=new Map;inputPosition=[];outputPosition=[];kind;host;constructor({id,kind,inputs=[],outputs=[],host}){this.id=id;this.host=host;this.kind=kind;for(const obj of inputs){const id2=generateItemId(this.inputsMap,"i",obj.id);const input2=new DflowInput({...obj,id:id2,nodeId:this.id});this.inputsMap.set(id2,input2);this.inputPosition.push(id2)}for(const obj of outputs){const id2=generateItemId(this.outputsMap,"o",obj.id);const output2=new DflowOutput({...obj,id:id2,nodeId:this.id});this.outputsMap.set(id2,output2);this.outputPosition.push(id2)}}get inputsDataAreValid(){for(const{data,types,optional}of this.inputsMap.values()){if(optional&&data===void 0)continue;if(Dflow.isValidDataType(types,data))continue;return false}return true}clearOutputs(){for(const output2 of this.outputsMap.values())output2.clear()}getInputById(id){const item=this.inputsMap.get(id);if(!item)throw new DflowErrorItemNotFound("input",{id});return item}input(position){const id=this.inputPosition[position];if(!id){throw new DflowErrorItemNotFound("input",{id:this.id,nodeId:this.id,position})}return this.getInputById(id)}getOutputById(id){const item=this.outputsMap.get(id);if(!item){throw new DflowErrorItemNotFound("output",{id,nodeId:this.id})}return item}output(position){const id=this.outputPosition[position];if(!id){throw new DflowErrorItemNotFound("output",{nodeId:this.id,position})}return this.getOutputById(id)}run(){}toJSON(){const obj={id:this.id,k:this.kind};const inputs=[...this.inputsMap.values()].map(item=>item.toJSON());if(inputs.length>0)obj.i=inputs;const outputs=[...this.outputsMap.values()].map(item=>item.toJSON());if(outputs.length>0)obj.o=outputs;return obj}}const{input,output}=Dflow;class DflowNodeArgument extends DflowNode{static kind="argument";static inputs=[input("number",{name:"position",optional:true})];static outputs=[output()]}class DflowNodeData extends DflowNode{static kind="data";static outputs=[output()];constructor({outputs,...rest}){super({outputs:outputs?.map(output2=>({...output2,types:Dflow.inferDataType(output2.data)})),...rest})}}class DflowNodeFunction extends DflowNode{static kind="function";static outputs=[output("DflowId",{name:"id"})];constructor(arg){super(arg);this.output(0).data=this.id}}class DflowNodeReturn extends DflowNode{static kind="return";static inputs=[input("DflowId",{name:"functionId"}),input([],{name:"value"})]}export class DflowNodeUnknown extends DflowNode{}export const coreNodesCatalog={[DflowNodeArgument.kind]:DflowNodeArgument,[DflowNodeData.kind]:DflowNodeData,[DflowNodeFunction.kind]:DflowNodeFunction,[DflowNodeReturn.kind]:DflowNodeReturn};export class DflowErrorCannotConnectSourceToTarget extends Error{source;target;static code="01";static message({s,t}){return`Cannot connect source ${s.join()} to target ${t.join()}`}constructor({source,target}){super(DflowErrorCannotConnectSourceToTarget.message({s:source,t:target}));this.source=source;this.target=target}toJSON(){return{_:DflowErrorCannotConnectSourceToTarget.code,s:this.source,t:this.target}}}export class DflowErrorInvalidInputData extends Error{static code="02";nodeId;static message({nId:nodeId}){return`Invalid input data in node ${nodeId}`}constructor(nodeId){super(DflowErrorInvalidInputData.message({nId:nodeId}));this.nodeId=nodeId}toJSON(){return{_:DflowErrorInvalidInputData.code,nId:this.nodeId}}}export class DflowErrorItemNotFound extends Error{static code="03";item;info;static message({item,id,nId:nodeId,p:position}){return`Not found ${[`item=${item}`,id?`id=${id}`:"",nodeId?`nodeId=${nodeId}`:"",position?`position=${position}`:""].filter(str=>str!=="").join()}`}constructor(item,info={}){super(DflowErrorItemNotFound.message({item,id:info.id,nId:info.nodeId,p:info.position}));this.item=item;this.info=info}toJSON(){const{item,info:{id,nodeId,position}}=this;const obj={item,_:DflowErrorItemNotFound.code};if(id)obj.id=id;if(nodeId)obj.nId=nodeId;if(position)obj.p=position;return obj}}export class DflowErrorCannotExecuteAsyncFunction extends Error{static code="04";static message(){return"dflow executeFunction() cannot execute async functions"}constructor(){super(DflowErrorCannotExecuteAsyncFunction.message())}toJSON(){return{_:DflowErrorCannotExecuteAsyncFunction.code}}}