UNPKG

@cadenza.io/core

Version:

This is a framework for building asynchronous graphs and flows of tasks and signals.

1 lines 170 kB
{"version":3,"sources":["../src/engine/SignalBroker.ts","../src/engine/GraphRunner.ts","../src/engine/GraphRun.ts","../src/utils/ColorRandomizer.ts","../src/engine/exporters/vue-flow/VueFlowExportVisitor.ts","../src/engine/exporters/vue-flow/VueFlowExporter.ts","../src/graph/execution/GraphNode.ts","../src/graph/context/GraphContext.ts","../src/utils/tools.ts","../src/graph/iterators/GraphNodeIterator.ts","../src/interfaces/SignalEmitter.ts","../src/graph/definition/GraphRoutine.ts","../src/interfaces/SignalParticipant.ts","../src/graph/definition/Task.ts","../src/graph/iterators/TaskIterator.ts","../src/registry/GraphRegistry.ts","../src/graph/definition/DebounceTask.ts","../src/graph/definition/EphemeralTask.ts","../src/interfaces/ExecutionChain.ts","../src/graph/iterators/GraphLayerIterator.ts","../src/interfaces/GraphLayer.ts","../src/graph/execution/SyncGraphLayer.ts","../src/interfaces/GraphBuilder.ts","../src/engine/builders/GraphBreadthFirstBuilder.ts","../src/interfaces/GraphRunStrategy.ts","../src/utils/promise.ts","../src/engine/ThrottleEngine.ts","../src/graph/execution/AsyncGraphLayer.ts","../src/engine/builders/GraphAsyncQueueBuilder.ts","../src/engine/strategy/GraphAsyncRun.ts","../src/engine/strategy/GraphStandardRun.ts","../src/Cadenza.ts","../src/graph/definition/SignalTask.ts","../src/index.ts"],"sourcesContent":["import GraphRunner from \"./GraphRunner\";\nimport { AnyObject } from \"../types/global\";\nimport Task from \"../graph/definition/Task\";\nimport GraphRoutine from \"../graph/definition/GraphRoutine\";\nimport Cadenza from \"../Cadenza\";\n\nexport default class SignalBroker {\n private static instance_: SignalBroker;\n\n /**\n * Singleton instance for signal management.\n * @returns The broker instance.\n */\n static get instance(): SignalBroker {\n if (!this.instance_) {\n this.instance_ = new SignalBroker();\n }\n return this.instance_;\n }\n\n private debug: boolean = false;\n\n setDebug(value: boolean) {\n this.debug = value;\n }\n\n protected sanitizeSignalName(signalName: string) {\n if (signalName.length > 100) {\n throw new Error(\"Signal name must be less than 100 characters\");\n }\n\n if (signalName.includes(\" \")) {\n throw new Error(\"Signal name must not contain spaces\");\n }\n\n if (signalName.includes(\"\\\\\")) {\n throw new Error(\"Signal name must not contain backslashes\");\n }\n\n if (/[A-Z]/.test(signalName.split(\".\").slice(1).join(\".\"))) {\n throw new Error(\n \"Signal name must not contain uppercase letters in the middle of the signal name. It is only allowed in the first part of the signal name.\",\n );\n }\n }\n\n protected runner: GraphRunner | undefined;\n protected metaRunner: GraphRunner | undefined;\n\n public getSignalsTask: Task | undefined;\n\n protected signalObservers: Map<\n string,\n {\n fn: (\n runner: GraphRunner,\n tasks: (Task | GraphRoutine)[],\n context: AnyObject,\n ) => void;\n tasks: Set<Task | GraphRoutine>;\n }\n > = new Map();\n\n protected emitStacks: Map<string, Map<string, AnyObject>> = new Map(); // execId -> emitted signals\n\n protected constructor() {\n this.addSignal(\"meta.signal_broker.added\");\n }\n\n /**\n * Initializes with runners.\n * @param runner Standard runner for user signals.\n * @param metaRunner Meta runner for 'meta.' signals (suppresses further meta-emits).\n */\n bootstrap(runner: GraphRunner, metaRunner: GraphRunner): void {\n this.runner = runner;\n this.metaRunner = metaRunner;\n }\n\n init() {\n Cadenza.createDebounceMetaTask(\n \"Execute and clear queued signals\",\n () => {\n for (const [id, signals] of this.emitStacks.entries()) {\n signals.forEach((context, signal) => {\n this.execute(signal, context);\n signals.delete(signal);\n });\n\n this.emitStacks.delete(id);\n }\n return true;\n },\n \"Executes queued signals and clears the stack\",\n 500,\n { maxWait: 10000 },\n ).doOn(\"meta.process_signal_queue_requested\");\n\n this.getSignalsTask = Cadenza.createMetaTask(\"Get signals\", (ctx) => {\n return {\n __signals: Array.from(this.signalObservers.keys()),\n ...ctx,\n };\n });\n }\n\n /**\n * Observes a signal with a routine/task.\n * @param signal The signal (e.g., 'domain.action', 'domain.*' for wildcards).\n * @param routineOrTask The observer.\n * @edge Duplicates ignored; supports wildcards for broad listening.\n */\n observe(signal: string, routineOrTask: Task | GraphRoutine): void {\n this.addSignal(signal);\n this.signalObservers.get(signal)!.tasks.add(routineOrTask);\n }\n\n /**\n * Unsubscribes a routine/task from a signal.\n * @param signal The signal.\n * @param routineOrTask The observer.\n * @edge Removes all instances if duplicate; deletes if empty.\n */\n unsubscribe(signal: string, routineOrTask: Task | GraphRoutine): void {\n const obs = this.signalObservers.get(signal);\n if (obs) {\n obs.tasks.delete(routineOrTask);\n if (obs.tasks.size === 0) {\n this.signalObservers.delete(signal);\n }\n }\n }\n\n /**\n * Emits a signal and bubbles to matching wildcards/parents (e.g., 'a.b.action' triggers 'a.b.action', 'a.b.*', 'a.*').\n * @param signal The signal name.\n * @param context The payload.\n * @edge Fire-and-forget; guards against loops per execId (from context.__graphExecId).\n * @edge For distribution, SignalTask can prefix and proxy remote.\n */\n emit(signal: string, context: AnyObject = {}): void {\n const execId = context.__routineExecId || \"global\"; // Assume from metadata\n if (!this.emitStacks.has(execId)) this.emitStacks.set(execId, new Map());\n\n const stack = this.emitStacks.get(execId)!;\n stack.set(signal, context);\n\n let executed = false;\n try {\n executed = this.execute(signal, context);\n } finally {\n if (executed) stack.delete(signal);\n if (stack.size === 0) this.emitStacks.delete(execId);\n }\n }\n\n execute(signal: string, context: AnyObject): boolean {\n let executed;\n executed = this.executeListener(signal, context); // Exact signal\n\n const parts = signal\n .slice(0, Math.max(signal.lastIndexOf(\":\"), signal.lastIndexOf(\".\")))\n .split(\".\");\n for (let i = parts.length; i > 0; i--) {\n const parent = parts.slice(0, i).join(\".\");\n executed = executed || this.executeListener(parent + \".*\", context); // Wildcard\n }\n\n if (this.debug) {\n console.log(\n `Emitted signal ${signal} with context ${JSON.stringify(context)}`,\n executed ? \"✅\" : \"❌\",\n );\n // TODO\n }\n\n return executed;\n }\n\n private executeListener(signal: string, context: AnyObject): boolean {\n const obs = this.signalObservers.get(signal);\n const runner = signal.startsWith(\"meta\") ? this.metaRunner : this.runner;\n if (obs && obs.tasks.size && runner) {\n obs.fn(runner, Array.from(obs.tasks), context);\n return true;\n }\n return false;\n }\n\n private addSignal(signal: string): void {\n if (!this.signalObservers.has(signal)) {\n this.sanitizeSignalName(signal);\n this.signalObservers.set(signal, {\n fn: (\n runner: GraphRunner,\n tasks: (Task | GraphRoutine)[],\n context: AnyObject,\n ) => runner.run(tasks, context),\n tasks: new Set(),\n });\n\n this.emit(\"meta.signal_broker.added\", { __signalName: signal });\n }\n }\n\n // TODO schedule signals\n\n /**\n * Lists all observed signals.\n * @returns Array of signals.\n */\n listObservedSignals(): string[] {\n return Array.from(this.signalObservers.keys());\n }\n\n reset() {\n this.emitStacks.clear();\n this.signalObservers.clear();\n }\n}\n","import { v4 as uuid } from \"uuid\";\nimport Task from \"../graph/definition/Task\";\nimport GraphRun from \"./GraphRun\";\nimport GraphNode from \"../graph/execution/GraphNode\";\nimport GraphRunStrategy from \"../interfaces/GraphRunStrategy\";\nimport { AnyObject } from \"../types/global\";\nimport GraphRoutine from \"../graph/definition/GraphRoutine\";\nimport SignalEmitter from \"../interfaces/SignalEmitter\";\nimport Cadenza from \"../Cadenza\";\nimport GraphRegistry from \"../registry/GraphRegistry\";\nimport GraphContext from \"../graph/context/GraphContext\";\n\nexport default class GraphRunner extends SignalEmitter {\n readonly id: string;\n protected currentRun: GraphRun;\n private debug: boolean = false;\n protected isRunning: boolean = false;\n private readonly isMeta: boolean = false;\n\n protected strategy: GraphRunStrategy;\n\n /**\n * Constructs a runner.\n * @param isMeta Meta flag (default false).\n * @edge Creates 'Start run' meta-task chained to registry gets.\n */\n constructor(isMeta: boolean = false) {\n super(isMeta);\n this.id = uuid();\n this.isMeta = isMeta;\n this.strategy = Cadenza.runStrategy.PARALLEL;\n this.currentRun = new GraphRun(this.strategy);\n }\n\n init() {\n if (this.isMeta) return;\n\n Cadenza.createMetaTask(\n \"Start run\",\n this.startRun.bind(this),\n \"Starts a run\",\n ).doAfter(\n GraphRegistry.instance.getTaskByName,\n GraphRegistry.instance.getRoutineByName,\n );\n }\n\n /**\n * Adds tasks/routines to current run.\n * @param tasks Tasks/routines.\n * @param context Context (defaults {}).\n * @edge Flattens routines to tasks; generates routineExecId if not in context.\n * @edge Emits 'meta.runner.added_tasks' with metadata.\n * @edge Empty tasks warns no-op.\n */\n protected addTasks(\n tasks: Task | GraphRoutine | (Task | GraphRoutine)[],\n context: AnyObject = {},\n ): void {\n let _tasks = Array.isArray(tasks) ? tasks : [tasks];\n if (_tasks.length === 0) {\n console.warn(\"No tasks/routines to add.\");\n return;\n }\n\n let routineName = _tasks.map((t) => t.name).join(\" | \");\n let isMeta = _tasks.every((t) => t.isMeta);\n let routineId = null;\n\n const allTasks = _tasks.flatMap((t) => {\n if (t instanceof GraphRoutine) {\n routineName = t.name;\n isMeta = t.isMeta;\n routineId = t.id;\n const routineTasks: Task[] = [];\n t.forEachTask((task: Task) => routineTasks.push(task));\n return routineTasks;\n }\n return t;\n });\n\n const ctx = new GraphContext(context || {});\n\n const routineExecId = context.__routineExecId ?? uuid();\n context.__routineExecId = routineExecId;\n\n const data = {\n __routineExecId: routineExecId,\n __routineName: routineName,\n __isMeta: isMeta,\n __routineId: routineId,\n __context: ctx.export(),\n __previousRoutineExecution: context.__metaData?.__routineExecId ?? null,\n __contractId:\n context.__metaData?.__contractId ?? context.__contractId ?? null,\n __task: {\n __name: routineName,\n },\n __scheduled: Date.now(),\n };\n\n this.emit(\"meta.runner.added_tasks\", data);\n\n allTasks.forEach((task) =>\n this.currentRun.addNode(\n new GraphNode(task, ctx, routineExecId, [], this.debug),\n ),\n );\n }\n\n /**\n * Runs tasks/routines.\n * @param tasks Optional tasks/routines.\n * @param context Optional context.\n * @returns Current/last run (Promise if async).\n * @edge If running, returns current; else runs and resets.\n */\n public run(\n tasks?: Task | GraphRoutine | (Task | GraphRoutine)[],\n context?: AnyObject,\n ): GraphRun | Promise<GraphRun> {\n if (tasks) {\n this.addTasks(tasks, context ?? {});\n }\n\n if (this.isRunning) {\n return this.currentRun;\n }\n\n if (this.currentRun) {\n this.isRunning = true;\n const runResult = this.currentRun.run();\n\n if (runResult instanceof Promise) {\n return this.runAsync(runResult);\n }\n }\n\n return this.reset();\n }\n\n private async runAsync(run: Promise<void>): Promise<GraphRun> {\n await run;\n return this.reset();\n }\n\n protected reset(): GraphRun {\n this.isRunning = false;\n\n const lastRun = this.currentRun;\n\n if (!this.debug) {\n this.destroy();\n }\n\n this.currentRun = new GraphRun(this.strategy);\n\n return lastRun;\n }\n\n public setDebug(value: boolean): void {\n this.debug = value;\n }\n\n public destroy(): void {\n this.currentRun.destroy();\n }\n\n public setStrategy(strategy: GraphRunStrategy): void {\n this.strategy = strategy;\n if (!this.isRunning) {\n this.currentRun = new GraphRun(this.strategy);\n }\n }\n\n private startRun(context: AnyObject): boolean {\n if (context.__task || context.__routine) {\n const routine = context.__task ?? context.__routine;\n this.run(routine, context);\n return true;\n } else {\n context.errored = true;\n context.__error = \"No routine or task defined.\";\n return false;\n }\n }\n}\n","import { v4 as uuid } from \"uuid\";\nimport GraphNode from \"../graph/execution/GraphNode\";\nimport GraphExporter from \"../interfaces/GraphExporter\";\nimport SyncGraphLayer from \"../graph/execution/SyncGraphLayer\";\nimport GraphRunStrategy from \"../interfaces/GraphRunStrategy\";\nimport GraphLayer from \"../interfaces/GraphLayer\";\nimport VueFlowExporter from \"./exporters/vue-flow/VueFlowExporter\";\n\nexport interface RunJson {\n __id: string;\n __label: string;\n __graph: any;\n __data: any;\n}\n\n// A unique execution of the graph\nexport default class GraphRun {\n readonly id: string;\n private graph: GraphLayer | undefined;\n // @ts-ignore\n private strategy: GraphRunStrategy;\n private exporter: GraphExporter | undefined;\n\n constructor(strategy: GraphRunStrategy) {\n this.id = uuid();\n this.strategy = strategy;\n this.strategy.setRunInstance(this);\n this.exporter = new VueFlowExporter();\n }\n\n setGraph(graph: GraphLayer) {\n this.graph = graph;\n }\n\n addNode(node: GraphNode) {\n this.strategy.addNode(node);\n }\n\n // Composite function / Command execution\n run(): void | Promise<void> {\n return this.strategy.run();\n }\n\n // Composite function\n destroy() {\n this.graph?.destroy();\n this.graph = undefined;\n this.exporter = undefined;\n }\n\n // Composite function\n log() {\n console.log(\"vvvvvvvvvvvvvvvvv\");\n console.log(\"GraphRun\");\n console.log(\"vvvvvvvvvvvvvvvvv\");\n this.graph?.log();\n console.log(\"=================\");\n }\n\n // Memento\n export(): RunJson {\n if (this.exporter && this.graph) {\n const data = this.strategy.export();\n return {\n __id: this.id,\n __label: data.__startTime ?? this.id,\n __graph: this.exporter?.exportGraph(this.graph as SyncGraphLayer),\n __data: data,\n };\n }\n\n return {\n __id: this.id,\n __label: this.id,\n __graph: undefined,\n __data: {},\n };\n }\n\n // Export Strategy\n setExporter(exporter: GraphExporter) {\n this.exporter = exporter;\n }\n}\n","export default class ColorRandomizer {\n numberOfSteps: number;\n stepCounter = 0;\n spread: number;\n range: number;\n\n constructor(numberOfSteps: number = 200, spread: number = 30) {\n this.numberOfSteps = numberOfSteps;\n this.spread = spread;\n this.range = Math.floor(numberOfSteps / this.spread);\n }\n\n private rainbow(numOfSteps: number, step: number) {\n // This function generates vibrant, \"evenly spaced\" colours (i.e. no clustering). This is ideal for creating easily distinguishable vibrant markers in Google Maps and other apps.\n // Adam Cole, 2011-Sept-14\n // HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript\n let r, g, b;\n const h = step / numOfSteps;\n const i = ~~(h * 6);\n const f = h * 6 - i;\n const q = 1 - f;\n switch (i % 6) {\n case 0:\n r = 1;\n g = f;\n b = 0;\n break;\n case 1:\n r = q;\n g = 1;\n b = 0;\n break;\n case 2:\n r = 0;\n g = 1;\n b = f;\n break;\n case 3:\n r = 0;\n g = q;\n b = 1;\n break;\n case 4:\n r = f;\n g = 0;\n b = 1;\n break;\n case 5:\n r = 1;\n g = 0;\n b = q;\n break;\n default:\n r = 0;\n g = 0;\n b = 0;\n break;\n }\n // @ts-ignore\n const c =\n \"#\" +\n (\"00\" + (~~(r * 255)).toString(16)).slice(-2) +\n (\"00\" + (~~(g * 255)).toString(16)).slice(-2) +\n (\"00\" + (~~(b * 255)).toString(16)).slice(-2);\n return c;\n }\n\n getRandomColor() {\n this.stepCounter++;\n\n if (this.stepCounter > this.numberOfSteps) {\n this.stepCounter = 1;\n }\n\n const randomStep =\n ((this.stepCounter * this.range) % this.numberOfSteps) -\n this.range +\n Math.floor(this.stepCounter / this.spread);\n\n return this.rainbow(this.numberOfSteps, randomStep);\n }\n}\n","import GraphVisitor from \"../../../interfaces/GraphVisitor\";\nimport SyncGraphLayer from \"../../../graph/execution/SyncGraphLayer\";\nimport GraphNode from \"../../../graph/execution/GraphNode\";\nimport Task from \"../../../graph/definition/Task\";\nimport ColorRandomizer from \"../../../utils/ColorRandomizer\";\n\nexport default class VueFlowExportVisitor implements GraphVisitor {\n private nodeCount = 0;\n private elements: any[] = [];\n private index = 0;\n private numberOfLayerNodes = 0;\n private contextToColor: { [id: string]: string } = {};\n private colorRandomizer = new ColorRandomizer();\n\n visitLayer(layer: SyncGraphLayer): any {\n const snapshot = layer.export();\n\n this.numberOfLayerNodes = snapshot.__numberOfNodes;\n this.index = 0;\n }\n\n visitNode(node: GraphNode): any {\n const snapshot = node.export();\n\n if (!this.contextToColor[snapshot.__context.__id]) {\n this.contextToColor[snapshot.__context.__id] =\n this.colorRandomizer.getRandomColor();\n }\n\n const color = this.contextToColor[snapshot.__context.__id];\n\n this.elements.push({\n id: snapshot.__id.slice(0, 8),\n label: snapshot.__task.__name,\n position: {\n x: snapshot.__task.__layerIndex * 500,\n y: -50 * this.numberOfLayerNodes * 0.5 + (this.index * 60 + 30),\n },\n sourcePosition: \"right\",\n targetPosition: \"left\",\n style: { backgroundColor: `${color}`, width: \"180px\" },\n data: {\n executionTime: snapshot.__executionTime,\n executionStart: snapshot.__executionStart,\n executionEnd: snapshot.__executionEnd,\n description: snapshot.__task.__description,\n functionString: snapshot.__task.__functionString,\n context: snapshot.__context.__context,\n layerIndex: snapshot.__task.__layerIndex,\n },\n });\n\n for (const [index, nextNodeId] of snapshot.__nextNodes.entries()) {\n this.elements.push({\n id: `${snapshot.__id.slice(0, 8)}-${index}`,\n source: snapshot.__id.slice(0, 8),\n target: nextNodeId.slice(0, 8),\n });\n }\n\n this.index++;\n this.nodeCount++;\n }\n\n visitTask(task: Task) {\n const snapshot = task.export();\n\n this.elements.push({\n id: snapshot.__id.slice(0, 8),\n label: snapshot.__name,\n position: { x: snapshot.__layerIndex * 300, y: this.index * 50 + 30 },\n sourcePosition: \"right\",\n targetPosition: \"left\",\n data: {\n description: snapshot.__description,\n functionString: snapshot.__functionString,\n layerIndex: snapshot.__layerIndex,\n },\n });\n\n for (const [index, nextTaskId] of snapshot.__nextTasks.entries()) {\n this.elements.push({\n id: `${snapshot.__id.slice(0, 8)}-${index}`,\n source: snapshot.__id.slice(0, 8),\n target: nextTaskId.slice(0, 8),\n });\n }\n\n this.index++;\n this.nodeCount++;\n }\n\n getElements() {\n return this.elements;\n }\n\n getNodeCount() {\n return this.nodeCount;\n }\n}\n","import GraphExporter from \"../../../interfaces/GraphExporter\";\nimport SyncGraphLayer from \"../../../graph/execution/SyncGraphLayer\";\nimport VueFlowExportVisitor from \"./VueFlowExportVisitor\";\nimport Task from \"../../../graph/definition/Task\";\n\nexport default class VueFlowExporter implements GraphExporter {\n exportGraph(graph: SyncGraphLayer): any {\n const exporterVisitor = new VueFlowExportVisitor();\n const layers = graph.getIterator();\n while (layers.hasNext()) {\n const layer = layers.next();\n layer.accept(exporterVisitor);\n }\n\n return {\n elements: exporterVisitor.getElements(),\n numberOfNodes: exporterVisitor.getNodeCount(),\n };\n }\n\n exportStaticGraph(graph: Task[]) {\n const exporterVisitor = new VueFlowExportVisitor();\n\n let prevTask = null;\n for (const task of graph) {\n if (task === prevTask) {\n continue;\n }\n\n const tasks = task.getIterator();\n const exportedTaskIds: string[] = [];\n\n while (tasks.hasNext()) {\n const task = tasks.next();\n if (task && !exportedTaskIds.includes(task.id)) {\n exportedTaskIds.push(task.id);\n task.accept(exporterVisitor);\n }\n }\n\n prevTask = task;\n }\n\n return {\n elements: exporterVisitor.getElements(),\n numberOfNodes: exporterVisitor.getNodeCount(),\n };\n }\n}\n","import { v4 as uuid } from \"uuid\";\nimport Task from \"../definition/Task\";\nimport GraphContext from \"../context/GraphContext\";\nimport Graph from \"../../interfaces/Graph\";\nimport GraphVisitor from \"../../interfaces/GraphVisitor\";\nimport GraphNodeIterator from \"../iterators/GraphNodeIterator\";\nimport SignalEmitter from \"../../interfaces/SignalEmitter\";\nimport GraphLayer from \"../../interfaces/GraphLayer\";\nimport { AnyObject } from \"../../types/global\";\n\nexport default class GraphNode extends SignalEmitter implements Graph {\n id: string;\n routineExecId: string;\n private task: Task;\n private context: GraphContext;\n private layer: GraphLayer | undefined;\n private divided: boolean = false;\n private splitGroupId: string = \"\";\n private processing: boolean = false;\n private subgraphComplete: boolean = false;\n private graphComplete: boolean = false;\n private result: unknown;\n private previousNodes: GraphNode[] = [];\n private nextNodes: GraphNode[] = [];\n private executionTime: number = 0;\n private executionStart: number = 0;\n private failed: boolean = false;\n private errored: boolean = false;\n destroyed: boolean = false;\n protected debug: boolean = false;\n\n constructor(\n task: Task,\n context: GraphContext,\n routineExecId: string,\n prevNodes: GraphNode[] = [],\n debug: boolean = false,\n ) {\n super(task.isMeta);\n this.task = task;\n this.context = context;\n this.previousNodes = prevNodes;\n this.id = uuid();\n this.routineExecId = routineExecId;\n this.splitGroupId = routineExecId;\n this.debug = debug;\n }\n\n setDebug(value: boolean) {\n this.debug = value;\n }\n\n public isUnique() {\n return this.task.isUnique;\n }\n\n public isMeta() {\n return this.task.isMeta;\n }\n\n public isProcessed() {\n return this.divided;\n }\n\n public isProcessing() {\n return this.processing;\n }\n\n public subgraphDone() {\n return this.subgraphComplete;\n }\n\n public graphDone() {\n return this.graphComplete;\n }\n\n public isEqualTo(node: GraphNode) {\n return (\n this.sharesTaskWith(node) &&\n this.sharesContextWith(node) &&\n this.isPartOfSameGraph(node)\n );\n }\n\n public isPartOfSameGraph(node: GraphNode) {\n return this.routineExecId === node.routineExecId;\n }\n\n public sharesTaskWith(node: GraphNode) {\n return this.task.id === node.task.id;\n }\n\n public sharesContextWith(node: GraphNode) {\n return this.context.id === node.context.id;\n }\n\n public getLayerIndex() {\n return this.task.layerIndex;\n }\n\n public getConcurrency() {\n return this.task.concurrency;\n }\n\n public getTag() {\n return this.task.getTag(this.context);\n }\n\n public scheduleOn(layer: GraphLayer) {\n let shouldSchedule = true;\n const nodes = layer.getNodesByRoutineExecId(this.routineExecId);\n for (const node of nodes) {\n if (node.isEqualTo(this)) {\n shouldSchedule = false;\n break;\n }\n\n if (node.sharesTaskWith(this) && node.isUnique()) {\n node.consume(this);\n shouldSchedule = false;\n break;\n }\n }\n\n if (shouldSchedule) {\n this.layer = layer;\n layer.add(this);\n this.emit(\"meta.node.scheduled\", {\n ...this.lightExport(),\n __scheduled: Date.now(),\n });\n }\n }\n\n public start() {\n if (this.executionStart === 0) {\n this.executionStart = Date.now();\n }\n\n const memento = this.lightExport();\n if (this.previousNodes.length === 0) {\n this.emit(\"meta.node.started_routine_execution\", memento);\n }\n\n if (this.debug) {\n this.log();\n }\n\n this.emit(\"meta.node.started\", memento);\n\n return this.executionStart;\n }\n\n public end() {\n if (this.executionStart === 0) {\n return 0;\n }\n\n this.processing = false;\n const end = Date.now();\n this.executionTime = end - this.executionStart;\n\n const memento = this.lightExport();\n if (this.errored || this.failed) {\n this.emit(\"meta.node.errored\", memento);\n }\n\n this.emit(\"meta.node.ended\", memento);\n\n if (this.graphDone()) {\n // TODO Reminder, Service registry should be listening to this event, (updateSelf)\n this.emit(\n `meta.node.ended_routine_execution:${this.routineExecId}`,\n memento,\n );\n }\n\n return end;\n }\n\n public execute() {\n if (!this.divided && !this.processing) {\n this.processing = true;\n\n const inputValidation = this.task.validateInput(\n this.context.getContext(),\n );\n if (inputValidation !== true) {\n this.onError(inputValidation.__validationErrors);\n this.postProcess();\n return this.nextNodes;\n }\n\n try {\n this.result = this.work();\n } catch (e: unknown) {\n this.onError(e);\n }\n\n if (this.result instanceof Promise) {\n return this.processAsync();\n }\n\n this.postProcess();\n }\n\n return this.nextNodes;\n }\n\n private async processAsync() {\n try {\n this.result = await this.result;\n } catch (e: unknown) {\n this.onError(e);\n }\n\n this.postProcess();\n\n return this.nextNodes;\n }\n\n private work() {\n return this.task.execute(this.context, this.onProgress.bind(this));\n }\n\n private onProgress(progress: number) {\n progress = Math.min(Math.max(0, progress), 1);\n\n this.emit(`meta.node.progress:${this.routineExecId}`, {\n __nodeId: this.id,\n __routineExecId: this.routineExecId,\n __progress: progress,\n __weight:\n this.task.progressWeight /\n (this.layer?.getNodesByRoutineExecId(this.routineExecId)?.length ?? 1),\n });\n }\n\n private postProcess() {\n if (typeof this.result === \"string\") {\n this.onError(\n `Returning strings is not allowed. Returned: ${this.result}`,\n );\n }\n\n if (Array.isArray(this.result)) {\n this.onError(`Returning arrays is not allowed. Returned: ${this.result}`);\n }\n\n this.nextNodes = this.divide();\n\n if (this.nextNodes.length === 0) {\n this.completeSubgraph();\n }\n\n if (this.errored || this.failed) {\n this.task.emitOnFailSignals(this.context);\n } else {\n this.task.emitSignals(this.context);\n }\n\n this.end();\n }\n\n private onError(error: unknown, errorData: AnyObject = {}) {\n this.result = {\n ...this.context.getFullContext(),\n __error: `Node error: ${error}`,\n error: `Node error: ${error}`,\n returnedValue: this.result,\n ...errorData,\n };\n this.migrate(this.result);\n this.errored = true;\n }\n\n private divide(): GraphNode[] {\n const newNodes: GraphNode[] = [];\n\n if (\n (this.result as Generator)?.next &&\n typeof (this.result as Generator).next === \"function\"\n ) {\n const generator = this.result as Generator;\n let current = generator.next();\n while (!current.done && current.value !== undefined) {\n const outputValidation = this.task.validateOutput(current.value as any);\n if (outputValidation !== true) {\n this.onError(outputValidation.__validationErrors);\n break;\n } else {\n newNodes.push(...this.generateNewNodes(current.value));\n current = generator.next();\n }\n }\n } else if (this.result !== undefined && !this.errored) {\n newNodes.push(...this.generateNewNodes(this.result));\n\n if (typeof this.result !== \"boolean\") {\n const outputValidation = this.task.validateOutput(this.result as any);\n if (outputValidation !== true) {\n this.onError(outputValidation.__validationErrors);\n }\n this.migrate({ ...this.result, ...this.context.getMetaData() });\n }\n }\n\n if (this.errored) {\n newNodes.push(\n ...this.task.mapNext(\n (t: Task) =>\n this.clone()\n .split(uuid())\n .differentiate(t)\n .migrate({ ...(this.result as any) }),\n true,\n ),\n );\n }\n\n this.divided = true;\n this.migrate({\n ...this.context.getFullContext(),\n __nextNodes: newNodes.map((n) => n.id),\n });\n\n return newNodes;\n }\n\n private generateNewNodes(result: any) {\n const groupId = uuid();\n const newNodes = [];\n if (typeof result !== \"boolean\") {\n const failed =\n (result.failed !== undefined && result.failed) ||\n result.error !== undefined;\n newNodes.push(\n ...(this.task.mapNext((t: Task) => {\n const context = t.isUnique\n ? {\n joinedContexts: [\n { ...result, taskName: this.task.name, __nodeId: this.id },\n ],\n ...this.context.getMetaData(),\n }\n : { ...result, ...this.context.getMetaData() };\n return this.clone().split(groupId).differentiate(t).migrate(context);\n }, failed) as GraphNode[]),\n );\n\n this.failed = failed;\n } else {\n const shouldContinue = result;\n if (shouldContinue) {\n newNodes.push(\n ...(this.task.mapNext((t: Task) => {\n const newNode = this.clone().split(groupId).differentiate(t);\n if (t.isUnique) {\n newNode.migrate({\n joinedContexts: [\n {\n ...this.context.getContext(),\n taskName: this.task.name,\n __nodeId: this.id,\n },\n ],\n ...this.context.getMetaData(),\n });\n }\n\n return newNode;\n }) as GraphNode[]),\n );\n }\n }\n\n return newNodes;\n }\n\n private differentiate(task: Task): GraphNode {\n this.task = task;\n return this;\n }\n\n private migrate(ctx: any): GraphNode {\n this.context = new GraphContext(ctx);\n return this;\n }\n\n private split(id: string): GraphNode {\n this.splitGroupId = id;\n return this;\n }\n\n public clone(): GraphNode {\n return new GraphNode(\n this.task,\n this.context,\n this.routineExecId,\n [this],\n this.debug,\n );\n }\n\n public consume(node: GraphNode) {\n this.context = this.context.combine(node.context);\n this.previousNodes = this.previousNodes.concat(node.previousNodes);\n node.completeSubgraph();\n node.changeIdentity(this.id);\n node.destroy();\n }\n\n private changeIdentity(id: string) {\n this.id = id;\n }\n\n private completeSubgraph() {\n for (const node of this.nextNodes) {\n if (!node.subgraphDone()) {\n return;\n }\n }\n\n this.subgraphComplete = true;\n\n if (this.previousNodes.length === 0) {\n this.completeGraph();\n return;\n }\n\n this.previousNodes.forEach((n) => n.completeSubgraph());\n }\n\n private completeGraph() {\n this.graphComplete = true;\n this.nextNodes.forEach((n) => n.completeGraph());\n }\n\n public destroy() {\n // @ts-ignore\n this.context = null;\n // @ts-ignore\n this.task = null;\n this.nextNodes = [];\n this.previousNodes.forEach((n) =>\n n.nextNodes.splice(n.nextNodes.indexOf(this), 1),\n );\n this.previousNodes = [];\n this.result = undefined;\n this.layer = undefined;\n this.destroyed = true;\n }\n\n public getIterator() {\n return new GraphNodeIterator(this);\n }\n\n public mapNext(callback: (node: GraphNode) => any) {\n return this.nextNodes.map(callback);\n }\n\n public accept(visitor: GraphVisitor) {\n visitor.visitNode(this);\n }\n\n public export() {\n return {\n __id: this.id,\n __task: this.task.export(),\n __context: this.context.export(),\n __result: this.result,\n __executionTime: this.executionTime,\n __executionStart: this.executionStart,\n __executionEnd: this.executionStart + this.executionTime,\n __nextNodes: this.nextNodes.map((node) => node.id),\n __previousNodes: this.previousNodes.map((node) => node.id),\n __routineExecId: this.routineExecId,\n __isProcessing: this.processing,\n __isMeta: this.isMeta(),\n __graphComplete: this.graphComplete,\n __failed: this.failed,\n __errored: this.errored,\n __isUnique: this.isUnique(),\n __splitGroupId: this.splitGroupId,\n __tag: this.getTag(),\n };\n }\n\n lightExport() {\n return {\n __id: this.id,\n __task: {\n __id: this.task.id,\n __name: this.task.name,\n },\n __context: this.context.export(),\n __executionTime: this.executionTime,\n __executionStart: this.executionStart,\n __nextNodes: this.nextNodes.map((node) => node.id),\n __previousNodes: this.previousNodes.map((node) => node.id),\n __routineExecId: this.routineExecId,\n __isProcessing: this.processing,\n __graphComplete: this.graphComplete,\n __isMeta: this.isMeta(),\n __failed: this.failed,\n __errored: this.errored,\n __isUnique: this.isUnique(),\n __splitGroupId: this.splitGroupId,\n __tag: this.getTag(),\n };\n }\n\n public log() {\n console.log(this.task.name, this.context.getContext(), this.routineExecId);\n }\n}\n","import { v4 as uuid } from \"uuid\";\nimport { deepCloneFilter } from \"../../utils/tools\";\nimport { AnyObject } from \"../../types/global\";\n\nexport default class GraphContext {\n readonly id: string;\n private readonly fullContext: AnyObject; // Raw (for internal)\n private readonly userData: AnyObject; // Filtered, frozen\n private readonly metaData: AnyObject; // __keys, frozen\n\n constructor(context: AnyObject) {\n if (Array.isArray(context)) {\n throw new Error(\"Array contexts not supported\"); // Per clarification\n }\n this.fullContext = context; // Clone once\n this.userData = Object.fromEntries(\n Object.entries(this.fullContext).filter(([key]) => !key.startsWith(\"__\")),\n );\n this.metaData = Object.fromEntries(\n Object.entries(this.fullContext).filter(([key]) => key.startsWith(\"__\")),\n );\n this.id = uuid();\n }\n\n /**\n * Gets frozen user data (read-only, no clone).\n * @returns Frozen user context.\n */\n getContext(): AnyObject {\n return this.userData;\n }\n\n getClonedContext(): AnyObject {\n return deepCloneFilter(this.userData);\n }\n\n /**\n * Gets full raw context (cloned for safety).\n * @returns Cloned full context.\n */\n getFullContext(): AnyObject {\n return this.fullContext;\n }\n\n /**\n * Gets frozen metadata (read-only).\n * @returns Frozen metadata object.\n */\n getMetaData(): AnyObject {\n return this.metaData;\n }\n\n /**\n * Clones this context (new instance).\n * @returns New GraphContext.\n */\n clone(): GraphContext {\n return this.mutate(this.fullContext);\n }\n\n /**\n * Creates new context from data (via registry).\n * @param context New data.\n * @returns New GraphContext.\n */\n mutate(context: AnyObject): GraphContext {\n return new GraphContext(context);\n }\n\n /**\n * Combines with another for uniques (joins userData).\n * @param otherContext The other.\n * @returns New combined GraphContext.\n * @edge Appends other.userData to joinedContexts in userData.\n */\n combine(otherContext: GraphContext): GraphContext {\n const newUser = { ...this.userData };\n newUser.joinedContexts = this.userData.joinedContexts\n ? [...this.userData.joinedContexts]\n : [this.userData];\n\n const otherUser = otherContext.userData;\n if (Array.isArray(otherUser.joinedContexts)) {\n newUser.joinedContexts.push(...otherUser.joinedContexts);\n } else {\n newUser.joinedContexts.push(otherUser);\n }\n\n const newFull = {\n ...this.fullContext,\n ...otherContext.fullContext,\n ...newUser,\n };\n return new GraphContext(newFull);\n }\n\n /**\n * Exports the context.\n * @returns Exported object.\n */\n export(): { __id: string; __context: AnyObject } {\n return {\n __id: this.id,\n __context: this.getFullContext(),\n };\n }\n}\n","/**\n * Deep clones an input with optional filter.\n * @param input The input to clone.\n * @param filterOut Predicate to skip keys (true = skip).\n * @returns Cloned input.\n * @edge Handles arrays/objects; skips cycles with visited.\n * @edge Primitives returned as-is; functions copied by reference.\n */\nexport function deepCloneFilter<T>(\n input: T,\n filterOut: (key: string) => boolean = () => false,\n): T {\n if (input === null || typeof input !== \"object\") {\n return input;\n }\n\n const visited = new WeakMap<any, any>(); // For cycle detection\n\n const stack: Array<{ source: any; target: any; key?: string }> = [];\n const output = Array.isArray(input) ? [] : {};\n\n stack.push({ source: input, target: output });\n visited.set(input, output);\n\n while (stack.length) {\n const { source, target, key } = stack.pop()!;\n const currentTarget = key !== undefined ? target[key] : target;\n\n for (const [k, value] of Object.entries(source)) {\n if (filterOut(k)) continue;\n\n if (value && typeof value === \"object\") {\n if (visited.has(value)) {\n currentTarget[k] = visited.get(value); // Cycle: link to existing clone\n continue;\n }\n\n const clonedValue = Array.isArray(value) ? [] : {};\n currentTarget[k] = clonedValue;\n visited.set(value, clonedValue);\n\n stack.push({ source: value, target: currentTarget, key: k });\n } else {\n currentTarget[k] = value;\n }\n }\n }\n\n return output as T;\n}\n\nexport function formatTimestamp(timestamp: number) {\n return new Date(timestamp).toISOString();\n}\n","import Iterator from \"../../interfaces/Iterator\";\nimport GraphNode from \"../execution/GraphNode\";\n\nexport default class GraphNodeIterator implements Iterator {\n currentNode: GraphNode | undefined;\n currentLayer: GraphNode[] = [];\n nextLayer: GraphNode[] = [];\n index: number = 0;\n\n constructor(node: GraphNode) {\n this.currentNode = node;\n this.currentLayer = [node];\n }\n\n hasNext(): boolean {\n return !!this.currentNode;\n }\n\n next(): any {\n const nextNode = this.currentNode;\n\n if (!nextNode) {\n return undefined;\n }\n\n this.nextLayer.push(...nextNode.mapNext((n: GraphNode) => n));\n\n this.index++;\n\n if (this.index === this.currentLayer.length) {\n this.currentLayer = this.nextLayer;\n this.nextLayer = [];\n this.index = 0;\n }\n\n this.currentNode = this.currentLayer.length\n ? this.currentLayer[this.index]\n : undefined;\n\n return nextNode;\n }\n}\n","import Cadenza from \"../Cadenza\";\n\nexport default abstract class SignalEmitter {\n protected silent: boolean;\n\n /**\n * Constructor for signal emitters.\n * @param silent If true, suppresses all emissions (e.g., for meta-runners to avoid loops; affects all emits).\n */\n constructor(silent: boolean = false) {\n this.silent = silent;\n }\n\n /**\n * Emits a signal via the broker if not silent.\n * @param signal The signal name.\n * @param data Optional payload (defaults to empty object).\n * @edge No emission if silent; for metrics in silent mode, consider override or separate method.\n */\n emit(signal: string, data: any = {}): void {\n if (this.silent) {\n return;\n }\n Cadenza.broker.emit(signal, data);\n }\n\n /**\n * Emits a signal via the broker even if silent.\n * @param signal The signal name.\n * @param data Optional payload (defaults to empty object).\n */\n emitMetric(signal: string, data: any = {}): void {\n Cadenza.broker.emit(signal, data); // Ignore silent\n }\n}\n","import { v4 as uuid } from \"uuid\";\nimport Task from \"./Task\";\nimport SignalParticipant from \"../../interfaces/SignalParticipant\";\n\nexport default class GraphRoutine extends SignalParticipant {\n id: string;\n readonly name: string;\n readonly description: string;\n readonly isMeta: boolean = false;\n private tasks: Set<Task> = new Set();\n\n constructor(\n name: string,\n tasks: Task[],\n description: string,\n isMeta: boolean = false,\n ) {\n super();\n this.id = uuid();\n this.name = name;\n this.description = description;\n this.isMeta = isMeta;\n tasks.forEach((t) => this.tasks.add(t));\n this.emit(\"meta.routine.created\", { __routine: this });\n }\n\n /**\n * Applies callback to starting tasks.\n * @param callBack The callback.\n * @returns Promise if async.\n */\n public async forEachTask(callBack: (task: Task) => any): Promise<void> {\n const promises = [];\n for (const task of this.tasks) {\n const res = callBack(task);\n if (res instanceof Promise) promises.push(res);\n }\n await Promise.all(promises);\n }\n\n /**\n * Sets global ID.\n * @param id The ID.\n */\n public setGlobalId(id: string): void {\n const oldId = this.id;\n this.id = id;\n this.emit(\"meta.routine.global_id_set\", { __id: this.id, __oldId: oldId });\n }\n\n // Removed emitsSignals per clarification (routines as listeners only)\n\n /**\n * Destroys the routine.\n */\n public destroy(): void {\n this.tasks.clear();\n this.emit(\"meta.routine.destroyed\", { __id: this.id });\n }\n}\n","import SignalEmitter from \"./SignalEmitter\";\nimport GraphContext from \"../graph/context/GraphContext\";\nimport Cadenza from \"../Cadenza\";\n\nexport default class SignalParticipant extends SignalEmitter {\n protected signalsToEmit: Set<string> = new Set(); // Use Set to prevent duplicates\n protected signalsToEmitOnFail: Set<string> = new Set();\n protected observedSignals: Set<string> = new Set();\n\n /**\n * Subscribes to signals (chainable).\n * @param signals The signal names.\n * @returns This for chaining.\n * @edge Duplicates ignored; assumes broker.observe binds this as handler.\n */\n doOn(...signals: string[]): this {\n signals.forEach((signal) => {\n if (this.observedSignals.has(signal)) return;\n Cadenza.broker.observe(signal, this as any);\n this.observedSignals.add(signal);\n });\n return this;\n }\n\n /**\n * Sets signals to emit post-execution (chainable).\n * @param signals The signal names.\n * @returns This for chaining.\n */\n emits(...signals: string[]): this {\n signals.forEach((signal) => this.signalsToEmit.add(signal));\n return this;\n }\n\n emitsOnFail(...signals: string[]): this {\n signals.forEach((signal) => this.signalsToEmitOnFail.add(signal));\n return this;\n }\n\n /**\n * Unsubscribes from all observed signals.\n * @returns This for chaining.\n */\n unsubscribeAll(): this {\n this.observedSignals.forEach((signal) =>\n Cadenza.broker.unsubscribe(signal, this as any),\n );\n this.observedSignals.clear();\n return this;\n }\n\n /**\n * Unsubscribes from specific signals.\n * @param signals The signals.\n * @returns This for chaining.\n * @edge No-op if not subscribed.\n */\n unsubscribe(...signals: string[]): this {\n signals.forEach((signal) => {\n if (this.observedSignals.has(signal)) {\n Cadenza.broker.unsubscribe(signal, this as any);\n this.observedSignals.delete(signal);\n }\n });\n return this;\n }\n\n /**\n * Detaches specific emitted signals.\n * @param signals The signals.\n * @returns This for chaining.\n */\n detachSignals(...signals: string[]): this {\n signals.forEach((signal) => this.signalsToEmit.delete(signal));\n return this;\n }\n\n /**\n * Detaches all emitted signals.\n * @returns This for chaining.\n */\n detachAllSignals(): this {\n this.signalsToEmit.clear();\n return this;\n }\n\n /**\n * Emits attached signals.\n * @param context The context for emission.\n * @edge If isMeta (from Task), suppresses further \"meta.*\" to prevent loops.\n */\n emitSignals(context: GraphContext): void {\n this.signalsToEmit.forEach((signal) => {\n // if ((this as any).isMeta && signal.startsWith('meta.')) return; // Suppress meta recursion if isMeta\n this.emit(signal, context);\n });\n }\n\n /**\n * Emits attached fail signals.\n * @param context The context for emission.\n * @edge If isMeta (from Task), suppresses further \"meta.*\" to prevent loops.\n */\n emitOnFailSignals(context: GraphContext): void {\n this.signalsToEmitOnFail.forEach((signal) => {\n // if ((this as any).isMeta && signal.startsWith('meta.')) return; // Suppress meta recursion if isMeta\n this.emit(signal, context);\n });\n }\n\n /**\n * Destroys the participant (unsub/detach).\n */\n destroy(): void {\n this.unsubscribeAll();\n this.detachAllSignals();\n }\n}\n","import { v4 as uuid } from \"uuid\";\nimport GraphContext from \"../context/GraphContext\";\nimport GraphVisitor from \"../../interfaces/GraphVisitor\";\nimport TaskIterator from \"../iterators/TaskIterator\";\nimport Graph from \"../../interfaces/Graph\";\nimport { AnyObject } from \"../../types/global\";\nimport SignalParticipant from \"../../interfaces/SignalParticipant\";\nimport { SchemaDefinition } from \"../../types/schema\";\n\nexport type TaskFunction = (\n context: AnyObject,\n progressCallback: (progress: number) => void,\n) => TaskResult;\nexport type TaskResult = boolean | object | Generator | Promise<any> | void;\nexport type ThrottleTagGetter = (context?: AnyObject, task?: Task) => string;\n\nexport default class Task extends SignalParticipant implements Graph {\n id: string;\n readonly name: string;\n readonly description: string;\n concurrency: number;\n timeout: number;\n readonly isMeta: boolean = false;\n readonly isUnique: boolean = false;\n readonly throttled: boolean = false;\n\n readonly isSignal: boolean = false;\n readonly isDeputy: boolean = false;\n readonly isEphemeral: boolean = false;\n\n protected inputContextSchema: SchemaDefinition | undefined = undefined;\n protected validateInputContext: boolean = false;\n protected outputContextSchema: SchemaDefinition | undefined = undefined;\n protected validateOutputContext: boolean = false;\n\n layerIndex: number = 0;\n progressWeight: number = 0;\n private nextTasks: Set<Task> = new Set();\n private onFailTasks: Set<Task> = new Set();\n private predecessorTasks: Set<Task> = new Set();\n destroyed: boolean = false;\n\n protected readonly taskFunction: TaskFunction;\n\n /**\n * Constructs a Task (static definition).\n * @param name Name.\n * @param task Function.\n * @param description Description.\n * @param concurrency Limit.\n * @param timeout ms.\n * @param register Register via signal (default true).\n * @param isUnique\n * @param isMeta\n * @param getTagCallback\n * @param inputSchema\n * @param validateInputContext\n * @param outputSchema\n * @param validateOutputContext\n * @edge Emits 'meta.task.created' with { __task: this } for seed.\n */\n constructor(\n name: string,\n task: TaskFunction,\n description: string = \"\",\n concurrency: number = 0,\n timeout: number = 0,\n register: boolean = true,\n isUnique: boolean = false,\n isMeta: boolean = false,\n getTagCallback: ThrottleTagGetter | undefined = undefined,\n inputSchema: SchemaDefinition | undefined = undefined,\n validateInputContext: boolean = false,\n outputSchema: SchemaDefinition | undefined = undefined,\n validateOutputContext: boolean = false,\n ) {\n super();\n this.id = uuid();\n this.name = name;\n this.taskFunction = task.bind(this);\n this.description = description;\n this.concurrency = concurrency;\n