UNPKG

@cadenza.io/core

Version:

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

1,634 lines (1,613 loc) 89.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { DebounceTask: () => DebounceTask, EphemeralTask: () => EphemeralTask, GraphContext: () => GraphContext, GraphRegistry: () => GraphRegistry, GraphRoutine: () => GraphRoutine, GraphRun: () => GraphRun, SignalEmitter: () => SignalEmitter, SignalParticipant: () => SignalParticipant, SignalTask: () => SignalTask, Task: () => Task, default: () => index_default }); module.exports = __toCommonJS(index_exports); // src/engine/SignalBroker.ts var SignalBroker = class _SignalBroker { // execId -> emitted signals constructor() { this.debug = false; this.signalObservers = /* @__PURE__ */ new Map(); this.emitStacks = /* @__PURE__ */ new Map(); this.addSignal("meta.signal_broker.added"); } /** * Singleton instance for signal management. * @returns The broker instance. */ static get instance() { if (!this.instance_) { this.instance_ = new _SignalBroker(); } return this.instance_; } setDebug(value) { this.debug = value; } sanitizeSignalName(signalName) { if (signalName.length > 100) { throw new Error("Signal name must be less than 100 characters"); } if (signalName.includes(" ")) { throw new Error("Signal name must not contain spaces"); } if (signalName.includes("\\")) { throw new Error("Signal name must not contain backslashes"); } if (/[A-Z]/.test(signalName.split(".").slice(1).join("."))) { throw new Error( "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." ); } } /** * Initializes with runners. * @param runner Standard runner for user signals. * @param metaRunner Meta runner for 'meta.' signals (suppresses further meta-emits). */ bootstrap(runner, metaRunner) { this.runner = runner; this.metaRunner = metaRunner; } init() { Cadenza.createDebounceMetaTask( "Execute and clear queued signals", () => { for (const [id, signals] of this.emitStacks.entries()) { signals.forEach((context, signal) => { this.execute(signal, context); signals.delete(signal); }); this.emitStacks.delete(id); } return true; }, "Executes queued signals and clears the stack", 500, { maxWait: 1e4 } ).doOn("meta.process_signal_queue_requested"); this.getSignalsTask = Cadenza.createMetaTask("Get signals", (ctx) => { return { __signals: Array.from(this.signalObservers.keys()), ...ctx }; }); } /** * Observes a signal with a routine/task. * @param signal The signal (e.g., 'domain.action', 'domain.*' for wildcards). * @param routineOrTask The observer. * @edge Duplicates ignored; supports wildcards for broad listening. */ observe(signal, routineOrTask) { this.addSignal(signal); this.signalObservers.get(signal).tasks.add(routineOrTask); } /** * Unsubscribes a routine/task from a signal. * @param signal The signal. * @param routineOrTask The observer. * @edge Removes all instances if duplicate; deletes if empty. */ unsubscribe(signal, routineOrTask) { const obs = this.signalObservers.get(signal); if (obs) { obs.tasks.delete(routineOrTask); if (obs.tasks.size === 0) { this.signalObservers.delete(signal); } } } /** * Emits a signal and bubbles to matching wildcards/parents (e.g., 'a.b.action' triggers 'a.b.action', 'a.b.*', 'a.*'). * @param signal The signal name. * @param context The payload. * @edge Fire-and-forget; guards against loops per execId (from context.__graphExecId). * @edge For distribution, SignalTask can prefix and proxy remote. */ emit(signal, context = {}) { const execId = context.__routineExecId || "global"; if (!this.emitStacks.has(execId)) this.emitStacks.set(execId, /* @__PURE__ */ new Map()); const stack = this.emitStacks.get(execId); stack.set(signal, context); let executed = false; try { executed = this.execute(signal, context); } finally { if (executed) stack.delete(signal); if (stack.size === 0) this.emitStacks.delete(execId); } } execute(signal, context) { let executed; executed = this.executeListener(signal, context); const parts = signal.slice(0, Math.max(signal.lastIndexOf(":"), signal.lastIndexOf("."))).split("."); for (let i = parts.length; i > 0; i--) { const parent = parts.slice(0, i).join("."); executed = executed || this.executeListener(parent + ".*", context); } if (this.debug) { console.log( `Emitted signal ${signal} with context ${JSON.stringify(context)}`, executed ? "\u2705" : "\u274C" ); } return executed; } executeListener(signal, context) { const obs = this.signalObservers.get(signal); const runner = signal.startsWith("meta") ? this.metaRunner : this.runner; if (obs && obs.tasks.size && runner) { obs.fn(runner, Array.from(obs.tasks), context); return true; } return false; } addSignal(signal) { if (!this.signalObservers.has(signal)) { this.sanitizeSignalName(signal); this.signalObservers.set(signal, { fn: (runner, tasks, context) => runner.run(tasks, context), tasks: /* @__PURE__ */ new Set() }); this.emit("meta.signal_broker.added", { __signalName: signal }); } } // TODO schedule signals /** * Lists all observed signals. * @returns Array of signals. */ listObservedSignals() { return Array.from(this.signalObservers.keys()); } reset() { this.emitStacks.clear(); this.signalObservers.clear(); } }; // src/engine/GraphRunner.ts var import_uuid6 = require("uuid"); // src/engine/GraphRun.ts var import_uuid = require("uuid"); // src/utils/ColorRandomizer.ts var ColorRandomizer = class { constructor(numberOfSteps = 200, spread = 30) { this.stepCounter = 0; this.numberOfSteps = numberOfSteps; this.spread = spread; this.range = Math.floor(numberOfSteps / this.spread); } rainbow(numOfSteps, step) { let r, g, b; const h = step / numOfSteps; const i = ~~(h * 6); const f = h * 6 - i; const q = 1 - f; switch (i % 6) { case 0: r = 1; g = f; b = 0; break; case 1: r = q; g = 1; b = 0; break; case 2: r = 0; g = 1; b = f; break; case 3: r = 0; g = q; b = 1; break; case 4: r = f; g = 0; b = 1; break; case 5: r = 1; g = 0; b = q; break; default: r = 0; g = 0; b = 0; break; } const c = "#" + ("00" + (~~(r * 255)).toString(16)).slice(-2) + ("00" + (~~(g * 255)).toString(16)).slice(-2) + ("00" + (~~(b * 255)).toString(16)).slice(-2); return c; } getRandomColor() { this.stepCounter++; if (this.stepCounter > this.numberOfSteps) { this.stepCounter = 1; } const randomStep = this.stepCounter * this.range % this.numberOfSteps - this.range + Math.floor(this.stepCounter / this.spread); return this.rainbow(this.numberOfSteps, randomStep); } }; // src/engine/exporters/vue-flow/VueFlowExportVisitor.ts var VueFlowExportVisitor = class { constructor() { this.nodeCount = 0; this.elements = []; this.index = 0; this.numberOfLayerNodes = 0; this.contextToColor = {}; this.colorRandomizer = new ColorRandomizer(); } visitLayer(layer) { const snapshot = layer.export(); this.numberOfLayerNodes = snapshot.__numberOfNodes; this.index = 0; } visitNode(node) { const snapshot = node.export(); if (!this.contextToColor[snapshot.__context.__id]) { this.contextToColor[snapshot.__context.__id] = this.colorRandomizer.getRandomColor(); } const color = this.contextToColor[snapshot.__context.__id]; this.elements.push({ id: snapshot.__id.slice(0, 8), label: snapshot.__task.__name, position: { x: snapshot.__task.__layerIndex * 500, y: -50 * this.numberOfLayerNodes * 0.5 + (this.index * 60 + 30) }, sourcePosition: "right", targetPosition: "left", style: { backgroundColor: `${color}`, width: "180px" }, data: { executionTime: snapshot.__executionTime, executionStart: snapshot.__executionStart, executionEnd: snapshot.__executionEnd, description: snapshot.__task.__description, functionString: snapshot.__task.__functionString, context: snapshot.__context.__context, layerIndex: snapshot.__task.__layerIndex } }); for (const [index, nextNodeId] of snapshot.__nextNodes.entries()) { this.elements.push({ id: `${snapshot.__id.slice(0, 8)}-${index}`, source: snapshot.__id.slice(0, 8), target: nextNodeId.slice(0, 8) }); } this.index++; this.nodeCount++; } visitTask(task) { const snapshot = task.export(); this.elements.push({ id: snapshot.__id.slice(0, 8), label: snapshot.__name, position: { x: snapshot.__layerIndex * 300, y: this.index * 50 + 30 }, sourcePosition: "right", targetPosition: "left", data: { description: snapshot.__description, functionString: snapshot.__functionString, layerIndex: snapshot.__layerIndex } }); for (const [index, nextTaskId] of snapshot.__nextTasks.entries()) { this.elements.push({ id: `${snapshot.__id.slice(0, 8)}-${index}`, source: snapshot.__id.slice(0, 8), target: nextTaskId.slice(0, 8) }); } this.index++; this.nodeCount++; } getElements() { return this.elements; } getNodeCount() { return this.nodeCount; } }; // src/engine/exporters/vue-flow/VueFlowExporter.ts var VueFlowExporter = class { exportGraph(graph) { const exporterVisitor = new VueFlowExportVisitor(); const layers = graph.getIterator(); while (layers.hasNext()) { const layer = layers.next(); layer.accept(exporterVisitor); } return { elements: exporterVisitor.getElements(), numberOfNodes: exporterVisitor.getNodeCount() }; } exportStaticGraph(graph) { const exporterVisitor = new VueFlowExportVisitor(); let prevTask = null; for (const task of graph) { if (task === prevTask) { continue; } const tasks = task.getIterator(); const exportedTaskIds = []; while (tasks.hasNext()) { const task2 = tasks.next(); if (task2 && !exportedTaskIds.includes(task2.id)) { exportedTaskIds.push(task2.id); task2.accept(exporterVisitor); } } prevTask = task; } return { elements: exporterVisitor.getElements(), numberOfNodes: exporterVisitor.getNodeCount() }; } }; // src/engine/GraphRun.ts var GraphRun = class { constructor(strategy) { this.id = (0, import_uuid.v4)(); this.strategy = strategy; this.strategy.setRunInstance(this); this.exporter = new VueFlowExporter(); } setGraph(graph) { this.graph = graph; } addNode(node) { this.strategy.addNode(node); } // Composite function / Command execution run() { return this.strategy.run(); } // Composite function destroy() { var _a; (_a = this.graph) == null ? void 0 : _a.destroy(); this.graph = void 0; this.exporter = void 0; } // Composite function log() { var _a; console.log("vvvvvvvvvvvvvvvvv"); console.log("GraphRun"); console.log("vvvvvvvvvvvvvvvvv"); (_a = this.graph) == null ? void 0 : _a.log(); console.log("================="); } // Memento export() { var _a, _b; if (this.exporter && this.graph) { const data = this.strategy.export(); return { __id: this.id, __label: (_a = data.__startTime) != null ? _a : this.id, __graph: (_b = this.exporter) == null ? void 0 : _b.exportGraph(this.graph), __data: data }; } return { __id: this.id, __label: this.id, __graph: void 0, __data: {} }; } // Export Strategy setExporter(exporter) { this.exporter = exporter; } }; // src/graph/execution/GraphNode.ts var import_uuid3 = require("uuid"); // src/graph/context/GraphContext.ts var import_uuid2 = require("uuid"); // src/utils/tools.ts function deepCloneFilter(input, filterOut = () => false) { if (input === null || typeof input !== "object") { return input; } const visited = /* @__PURE__ */ new WeakMap(); const stack = []; const output = Array.isArray(input) ? [] : {}; stack.push({ source: input, target: output }); visited.set(input, output); while (stack.length) { const { source, target, key } = stack.pop(); const currentTarget = key !== void 0 ? target[key] : target; for (const [k, value] of Object.entries(source)) { if (filterOut(k)) continue; if (value && typeof value === "object") { if (visited.has(value)) { currentTarget[k] = visited.get(value); continue; } const clonedValue = Array.isArray(value) ? [] : {}; currentTarget[k] = clonedValue; visited.set(value, clonedValue); stack.push({ source: value, target: currentTarget, key: k }); } else { currentTarget[k] = value; } } } return output; } // src/graph/context/GraphContext.ts var GraphContext = class _GraphContext { // __keys, frozen constructor(context) { if (Array.isArray(context)) { throw new Error("Array contexts not supported"); } this.fullContext = context; this.userData = Object.fromEntries( Object.entries(this.fullContext).filter(([key]) => !key.startsWith("__")) ); this.metaData = Object.fromEntries( Object.entries(this.fullContext).filter(([key]) => key.startsWith("__")) ); this.id = (0, import_uuid2.v4)(); } /** * Gets frozen user data (read-only, no clone). * @returns Frozen user context. */ getContext() { return this.userData; } getClonedContext() { return deepCloneFilter(this.userData); } /** * Gets full raw context (cloned for safety). * @returns Cloned full context. */ getFullContext() { return this.fullContext; } /** * Gets frozen metadata (read-only). * @returns Frozen metadata object. */ getMetaData() { return this.metaData; } /** * Clones this context (new instance). * @returns New GraphContext. */ clone() { return this.mutate(this.fullContext); } /** * Creates new context from data (via registry). * @param context New data. * @returns New GraphContext. */ mutate(context) { return new _GraphContext(context); } /** * Combines with another for uniques (joins userData). * @param otherContext The other. * @returns New combined GraphContext. * @edge Appends other.userData to joinedContexts in userData. */ combine(otherContext) { const newUser = { ...this.userData }; newUser.joinedContexts = this.userData.joinedContexts ? [...this.userData.joinedContexts] : [this.userData]; const otherUser = otherContext.userData; if (Array.isArray(otherUser.joinedContexts)) { newUser.joinedContexts.push(...otherUser.joinedContexts); } else { newUser.joinedContexts.push(otherUser); } const newFull = { ...this.fullContext, ...otherContext.fullContext, ...newUser }; return new _GraphContext(newFull); } /** * Exports the context. * @returns Exported object. */ export() { return { __id: this.id, __context: this.getFullContext() }; } }; // src/graph/iterators/GraphNodeIterator.ts var GraphNodeIterator = class { constructor(node) { this.currentLayer = []; this.nextLayer = []; this.index = 0; this.currentNode = node; this.currentLayer = [node]; } hasNext() { return !!this.currentNode; } next() { const nextNode = this.currentNode; if (!nextNode) { return void 0; } this.nextLayer.push(...nextNode.mapNext((n) => n)); this.index++; if (this.index === this.currentLayer.length) { this.currentLayer = this.nextLayer; this.nextLayer = []; this.index = 0; } this.currentNode = this.currentLayer.length ? this.currentLayer[this.index] : void 0; return nextNode; } }; // src/interfaces/SignalEmitter.ts var SignalEmitter = class { /** * Constructor for signal emitters. * @param silent If true, suppresses all emissions (e.g., for meta-runners to avoid loops; affects all emits). */ constructor(silent = false) { this.silent = silent; } /** * Emits a signal via the broker if not silent. * @param signal The signal name. * @param data Optional payload (defaults to empty object). * @edge No emission if silent; for metrics in silent mode, consider override or separate method. */ emit(signal, data = {}) { if (this.silent) { return; } Cadenza.broker.emit(signal, data); } /** * Emits a signal via the broker even if silent. * @param signal The signal name. * @param data Optional payload (defaults to empty object). */ emitMetric(signal, data = {}) { Cadenza.broker.emit(signal, data); } }; // src/graph/execution/GraphNode.ts var GraphNode = class _GraphNode extends SignalEmitter { constructor(task, context, routineExecId, prevNodes = [], debug = false) { super(task.isMeta); this.divided = false; this.splitGroupId = ""; this.processing = false; this.subgraphComplete = false; this.graphComplete = false; this.previousNodes = []; this.nextNodes = []; this.executionTime = 0; this.executionStart = 0; this.failed = false; this.errored = false; this.destroyed = false; this.debug = false; this.task = task; this.context = context; this.previousNodes = prevNodes; this.id = (0, import_uuid3.v4)(); this.routineExecId = routineExecId; this.splitGroupId = routineExecId; this.debug = debug; } setDebug(value) { this.debug = value; } isUnique() { return this.task.isUnique; } isMeta() { return this.task.isMeta; } isProcessed() { return this.divided; } isProcessing() { return this.processing; } subgraphDone() { return this.subgraphComplete; } graphDone() { return this.graphComplete; } isEqualTo(node) { return this.sharesTaskWith(node) && this.sharesContextWith(node) && this.isPartOfSameGraph(node); } isPartOfSameGraph(node) { return this.routineExecId === node.routineExecId; } sharesTaskWith(node) { return this.task.id === node.task.id; } sharesContextWith(node) { return this.context.id === node.context.id; } getLayerIndex() { return this.task.layerIndex; } getConcurrency() { return this.task.concurrency; } getTag() { return this.task.getTag(this.context); } scheduleOn(layer) { let shouldSchedule = true; const nodes = layer.getNodesByRoutineExecId(this.routineExecId); for (const node of nodes) { if (node.isEqualTo(this)) { shouldSchedule = false; break; } if (node.sharesTaskWith(this) && node.isUnique()) { node.consume(this); shouldSchedule = false; break; } } if (shouldSchedule) { this.layer = layer; layer.add(this); this.emit("meta.node.scheduled", { ...this.lightExport(), __scheduled: Date.now() }); } } start() { if (this.executionStart === 0) { this.executionStart = Date.now(); } const memento = this.lightExport(); if (this.previousNodes.length === 0) { this.emit("meta.node.started_routine_execution", memento); } if (this.debug) { this.log(); } this.emit("meta.node.started", memento); return this.executionStart; } end() { if (this.executionStart === 0) { return 0; } this.processing = false; const end = Date.now(); this.executionTime = end - this.executionStart; const memento = this.lightExport(); if (this.errored || this.failed) { this.emit("meta.node.errored", memento); } this.emit("meta.node.ended", memento); if (this.graphDone()) { this.emit( `meta.node.ended_routine_execution:${this.routineExecId}`, memento ); } return end; } execute() { if (!this.divided && !this.processing) { this.processing = true; const inputValidation = this.task.validateInput( this.context.getContext() ); if (inputValidation !== true) { this.onError(inputValidation.__validationErrors); this.postProcess(); return this.nextNodes; } try { this.result = this.work(); } catch (e) { this.onError(e); } if (this.result instanceof Promise) { return this.processAsync(); } this.postProcess(); } return this.nextNodes; } async processAsync() { try { this.result = await this.result; } catch (e) { this.onError(e); } this.postProcess(); return this.nextNodes; } work() { return this.task.execute(this.context, this.onProgress.bind(this)); } onProgress(progress) { var _a, _b, _c; progress = Math.min(Math.max(0, progress), 1); this.emit(`meta.node.progress:${this.routineExecId}`, { __nodeId: this.id, __routineExecId: this.routineExecId, __progress: progress, __weight: this.task.progressWeight / ((_c = (_b = (_a = this.layer) == null ? void 0 : _a.getNodesByRoutineExecId(this.routineExecId)) == null ? void 0 : _b.length) != null ? _c : 1) }); } postProcess() { if (typeof this.result === "string") { this.onError( `Returning strings is not allowed. Returned: ${this.result}` ); } if (Array.isArray(this.result)) { this.onError(`Returning arrays is not allowed. Returned: ${this.result}`); } this.nextNodes = this.divide(); if (this.nextNodes.length === 0) { this.completeSubgraph(); } if (this.errored || this.failed) { this.task.emitOnFailSignals(this.context); } else { this.task.emitSignals(this.context); } this.end(); } onError(error, errorData = {}) { this.result = { ...this.context.getFullContext(), __error: `Node error: ${error}`, error: `Node error: ${error}`, returnedValue: this.result, ...errorData }; this.migrate(this.result); this.errored = true; } divide() { var _a; const newNodes = []; if (((_a = this.result) == null ? void 0 : _a.next) && typeof this.result.next === "function") { const generator = this.result; let current = generator.next(); while (!current.done && current.value !== void 0) { const outputValidation = this.task.validateOutput(current.value); if (outputValidation !== true) { this.onError(outputValidation.__validationErrors); break; } else { newNodes.push(...this.generateNewNodes(current.value)); current = generator.next(); } } } else if (this.result !== void 0 && !this.errored) { newNodes.push(...this.generateNewNodes(this.result)); if (typeof this.result !== "boolean") { const outputValidation = this.task.validateOutput(this.result); if (outputValidation !== true) { this.onError(outputValidation.__validationErrors); } this.migrate({ ...this.result, ...this.context.getMetaData() }); } } if (this.errored) { newNodes.push( ...this.task.mapNext( (t) => this.clone().split((0, import_uuid3.v4)()).differentiate(t).migrate({ ...this.result }), true ) ); } this.divided = true; this.migrate({ ...this.context.getFullContext(), __nextNodes: newNodes.map((n) => n.id) }); return newNodes; } generateNewNodes(result) { const groupId = (0, import_uuid3.v4)(); const newNodes = []; if (typeof result !== "boolean") { const failed = result.failed !== void 0 && result.failed || result.error !== void 0; newNodes.push( ...this.task.mapNext((t) => { const context = t.isUnique ? { joinedContexts: [ { ...result, taskName: this.task.name, __nodeId: this.id } ], ...this.context.getMetaData() } : { ...result, ...this.context.getMetaData() }; return this.clone().split(groupId).differentiate(t).migrate(context); }, failed) ); this.failed = failed; } else { const shouldContinue = result; if (shouldContinue) { newNodes.push( ...this.task.mapNext((t) => { const newNode = this.clone().split(groupId).differentiate(t); if (t.isUnique) { newNode.migrate({ joinedContexts: [ { ...this.context.getContext(), taskName: this.task.name, __nodeId: this.id } ], ...this.context.getMetaData() }); } return newNode; }) ); } } return newNodes; } differentiate(task) { this.task = task; return this; } migrate(ctx) { this.context = new GraphContext(ctx); return this; } split(id) { this.splitGroupId = id; return this; } clone() { return new _GraphNode( this.task, this.context, this.routineExecId, [this], this.debug ); } consume(node) { this.context = this.context.combine(node.context); this.previousNodes = this.previousNodes.concat(node.previousNodes); node.completeSubgraph(); node.changeIdentity(this.id); node.destroy(); } changeIdentity(id) { this.id = id; } completeSubgraph() { for (const node of this.nextNodes) { if (!node.subgraphDone()) { return; } } this.subgraphComplete = true; if (this.previousNodes.length === 0) { this.completeGraph(); return; } this.previousNodes.forEach((n) => n.completeSubgraph()); } completeGraph() { this.graphComplete = true; this.nextNodes.forEach((n) => n.completeGraph()); } destroy() { this.context = null; this.task = null; this.nextNodes = []; this.previousNodes.forEach( (n) => n.nextNodes.splice(n.nextNodes.indexOf(this), 1) ); this.previousNodes = []; this.result = void 0; this.layer = void 0; this.destroyed = true; } getIterator() { return new GraphNodeIterator(this); } mapNext(callback) { return this.nextNodes.map(callback); } accept(visitor) { visitor.visitNode(this); } export() { return { __id: this.id, __task: this.task.export(), __context: this.context.export(), __result: this.result, __executionTime: this.executionTime, __executionStart: this.executionStart, __executionEnd: this.executionStart + this.executionTime, __nextNodes: this.nextNodes.map((node) => node.id), __previousNodes: this.previousNodes.map((node) => node.id), __routineExecId: this.routineExecId, __isProcessing: this.processing, __isMeta: this.isMeta(), __graphComplete: this.graphComplete, __failed: this.failed, __errored: this.errored, __isUnique: this.isUnique(), __splitGroupId: this.splitGroupId, __tag: this.getTag() }; } lightExport() { return { __id: this.id, __task: { __id: this.task.id, __name: this.task.name }, __context: this.context.export(), __executionTime: this.executionTime, __executionStart: this.executionStart, __nextNodes: this.nextNodes.map((node) => node.id), __previousNodes: this.previousNodes.map((node) => node.id), __routineExecId: this.routineExecId, __isProcessing: this.processing, __graphComplete: this.graphComplete, __isMeta: this.isMeta(), __failed: this.failed, __errored: this.errored, __isUnique: this.isUnique(), __splitGroupId: this.splitGroupId, __tag: this.getTag() }; } log() { console.log(this.task.name, this.context.getContext(), this.routineExecId); } }; // src/graph/definition/GraphRoutine.ts var import_uuid4 = require("uuid"); // src/interfaces/SignalParticipant.ts var SignalParticipant = class extends SignalEmitter { constructor() { super(...arguments); this.signalsToEmit = /* @__PURE__ */ new Set(); // Use Set to prevent duplicates this.signalsToEmitOnFail = /* @__PURE__ */ new Set(); this.observedSignals = /* @__PURE__ */ new Set(); } /** * Subscribes to signals (chainable). * @param signals The signal names. * @returns This for chaining. * @edge Duplicates ignored; assumes broker.observe binds this as handler. */ doOn(...signals) { signals.forEach((signal) => { if (this.observedSignals.has(signal)) return; Cadenza.broker.observe(signal, this); this.observedSignals.add(signal); }); return this; } /** * Sets signals to emit post-execution (chainable). * @param signals The signal names. * @returns This for chaining. */ emits(...signals) { signals.forEach((signal) => this.signalsToEmit.add(signal)); return this; } emitsOnFail(...signals) { signals.forEach((signal) => this.signalsToEmitOnFail.add(signal)); return this; } /** * Unsubscribes from all observed signals. * @returns This for chaining. */ unsubscribeAll() { this.observedSignals.forEach( (signal) => Cadenza.broker.unsubscribe(signal, this) ); this.observedSignals.clear(); return this; } /** * Unsubscribes from specific signals. * @param signals The signals. * @returns This for chaining. * @edge No-op if not subscribed. */ unsubscribe(...signals) { signals.forEach((signal) => { if (this.observedSignals.has(signal)) { Cadenza.broker.unsubscribe(signal, this); this.observedSignals.delete(signal); } }); return this; } /** * Detaches specific emitted signals. * @param signals The signals. * @returns This for chaining. */ detachSignals(...signals) { signals.forEach((signal) => this.signalsToEmit.delete(signal)); return this; } /** * Detaches all emitted signals. * @returns This for chaining. */ detachAllSignals() { this.signalsToEmit.clear(); return this; } /** * Emits attached signals. * @param context The context for emission. * @edge If isMeta (from Task), suppresses further "meta.*" to prevent loops. */ emitSignals(context) { this.signalsToEmit.forEach((signal) => { this.emit(signal, context); }); } /** * Emits attached fail signals. * @param context The context for emission. * @edge If isMeta (from Task), suppresses further "meta.*" to prevent loops. */ emitOnFailSignals(context) { this.signalsToEmitOnFail.forEach((signal) => { this.emit(signal, context); }); } /** * Destroys the participant (unsub/detach). */ destroy() { this.unsubscribeAll(); this.detachAllSignals(); } }; // src/graph/definition/GraphRoutine.ts var GraphRoutine = class extends SignalParticipant { constructor(name, tasks, description, isMeta = false) { super(); this.isMeta = false; this.tasks = /* @__PURE__ */ new Set(); this.id = (0, import_uuid4.v4)(); this.name = name; this.description = description; this.isMeta = isMeta; tasks.forEach((t) => this.tasks.add(t)); this.emit("meta.routine.created", { __routine: this }); } /** * Applies callback to starting tasks. * @param callBack The callback. * @returns Promise if async. */ async forEachTask(callBack) { const promises = []; for (const task of this.tasks) { const res = callBack(task); if (res instanceof Promise) promises.push(res); } await Promise.all(promises); } /** * Sets global ID. * @param id The ID. */ setGlobalId(id) { const oldId = this.id; this.id = id; this.emit("meta.routine.global_id_set", { __id: this.id, __oldId: oldId }); } // Removed emitsSignals per clarification (routines as listeners only) /** * Destroys the routine. */ destroy() { this.tasks.clear(); this.emit("meta.routine.destroyed", { __id: this.id }); } }; // src/graph/definition/Task.ts var import_uuid5 = require("uuid"); // src/graph/iterators/TaskIterator.ts var TaskIterator = class { constructor(task) { this.currentLayer = /* @__PURE__ */ new Set(); this.nextLayer = /* @__PURE__ */ new Set(); this.iterator = this.currentLayer[Symbol.iterator](); this.currentTask = task; this.currentLayer.add(task); } hasNext() { return !!this.currentTask; } next() { const nextTask = this.currentTask; if (!nextTask) { return void 0; } nextTask.mapNext((t) => this.nextLayer.add(t)); let next = this.iterator.next(); if (next.value === void 0) { this.currentLayer.clear(); this.currentLayer = this.nextLayer; this.nextLayer = /* @__PURE__ */ new Set(); this.iterator = this.currentLayer[Symbol.iterator](); next = this.iterator.next(); } this.currentTask = next.value; return nextTask; } }; // src/graph/definition/Task.ts var Task = class extends SignalParticipant { /** * Constructs a Task (static definition). * @param name Name. * @param task Function. * @param description Description. * @param concurrency Limit. * @param timeout ms. * @param register Register via signal (default true). * @param isUnique * @param isMeta * @param getTagCallback * @param inputSchema * @param validateInputContext * @param outputSchema * @param validateOutputContext * @edge Emits 'meta.task.created' with { __task: this } for seed. */ constructor(name, task, description = "", concurrency = 0, timeout = 0, register = true, isUnique = false, isMeta = false, getTagCallback = void 0, inputSchema = void 0, validateInputContext = false, outputSchema = void 0, validateOutputContext = false) { super(); this.isMeta = false; this.isUnique = false; this.throttled = false; this.isSignal = false; this.isDeputy = false; this.isEphemeral = false; this.inputContextSchema = void 0; this.validateInputContext = false; this.outputContextSchema = void 0; this.validateOutputContext = false; this.layerIndex = 0; this.progressWeight = 0; this.nextTasks = /* @__PURE__ */ new Set(); this.onFailTasks = /* @__PURE__ */ new Set(); this.predecessorTasks = /* @__PURE__ */ new Set(); this.destroyed = false; this.id = (0, import_uuid5.v4)(); this.name = name; this.taskFunction = task.bind(this); this.description = description; this.concurrency = concurrency; this.timeout = timeout; this.isUnique = isUnique; this.isMeta = isMeta; this.inputContextSchema = inputSchema; this.validateInputContext = validateInputContext; this.outputContextSchema = outputSchema; this.validateOutputContext = validateOutputContext; if (getTagCallback) { this.getTag = (context) => getTagCallback(context, this); this.throttled = true; } if (register) { this.emit("meta.task.created", { __task: this }); } } getTag(context) { return this.id; } setGlobalId(id) { const oldId = this.id; this.id = id; this.emit("meta.task.global_id_set", { __id: this.id, __oldId: oldId }); } setTimeout(timeout) { this.timeout = timeout; } setConcurrency(concurrency) { this.concurrency = concurrency; } setProgressWeight(weight) { this.progressWeight = weight; } setInputContextSchema(schema) { this.inputContextSchema = schema; } setOutputContextSchema(schema) { this.outputContextSchema = schema; } setValidateInputContext(value) { this.validateInputContext = value; } setValidateOutputContext(value) { this.validateOutputContext = value; } /** * Validates a context deeply against a schema. * @param data - The data to validate (input context or output result). * @param schema - The schema definition. * @param path - The current path for error reporting (default: 'root'). * @returns { { valid: boolean, errors: Record<string, string> } } - Validation result with detailed errors if invalid. * @description Recursively checks types, required fields, and constraints; allows extra properties not in schema. */ validateSchema(data, schema, path = "context") { const errors = {}; if (!schema || typeof schema !== "object") return { valid: true, errors }; const required = schema.required || []; for (const key of required) { if (!(key in data)) { errors[`${path}.${key}`] = `Required field '${key}' is missing`; } } const properties = schema.properties || {}; for (const [key, value] of Object.entries(data)) { if (key in properties) { const prop = properties[key]; const propType = prop.type; if (propType === "string" && typeof value !== "string") { errors[`${path}.${key}`] = `Expected 'string' for '${key}', got '${typeof value}'`; } else if (propType === "number" && typeof value !== "number") { errors[`${path}.${key}`] = `Expected 'number' for '${key}', got '${typeof value}'`; } else if (propType === "boolean" && typeof value !== "boolean") { errors[`${path}.${key}`] = `Expected 'boolean' for '${key}', got '${typeof value}'`; } else if (propType === "array" && !Array.isArray(value)) { errors[`${path}.${key}`] = `Expected 'array' for '${key}', got '${typeof value}'`; } else if (propType === "object" && (typeof value !== "object" || value === null || Array.isArray(value))) { errors[`${path}.${key}`] = `Expected 'object' for '${key}', got '${typeof value}'`; } else if (propType === "array" && prop.items) { if (Array.isArray(value)) { value.forEach((item, index) => { const subValidation = this.validateSchema( item, prop.items, `${path}.${key}[${index}]` ); if (!subValidation.valid) { Object.assign(errors, subValidation.errors); } }); } } else if (propType === "object" && !Array.isArray(value) && value !== null) { const subValidation = this.validateSchema( value, prop, `${path}.${key}` ); if (!subValidation.valid) { Object.assign(errors, subValidation.errors); } } const constraints = prop.constraints || {}; if (typeof value === "string") { if (constraints.minLength && value.length < constraints.minLength) { errors[`${path}.${key}`] = `String '${key}' shorter than minLength ${constraints.minLength}`; } if (constraints.maxLength && value.length > constraints.maxLength) { errors[`${path}.${key}`] = `String '${key}' exceeds maxLength ${constraints.maxLength}`; } if (constraints.pattern && !new RegExp(constraints.pattern).test(value)) { errors[`${path}.${key}`] = `String '${key}' does not match pattern ${constraints.pattern}`; } } else if (typeof value === "number") { if (constraints.min && value < constraints.min) { errors[`${path}.${key}`] = `Number '${key}' below min ${constraints.min}`; } if (constraints.max && value > constraints.max) { errors[`${path}.${key}`] = `Number '${key}' exceeds max ${constraints.max}`; } if (constraints.multipleOf && value % constraints.multipleOf !== 0) { errors[`${path}.${key}`] = `Number '${key}' not multiple of ${constraints.multipleOf}`; } } else if (constraints.enum && !constraints.enum.includes(value)) { errors[`${path}.${key}`] = `Value '${value}' for '${key}' not in enum ${JSON.stringify(constraints.enum)}`; } else if (constraints.format) { const formats = { email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, url: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/, "date-time": /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/, uuid: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, custom: /.*/ // Placeholder; override with prop.constraints.pattern if present }; const regex = formats[constraints.format] || new RegExp(constraints.pattern || ".*"); if (typeof value === "string" && !regex.test(value)) { errors[`${path}.${key}`] = `Value '${value}' for '${key}' does not match format '${constraints.format}'`; } } else if (constraints.oneOf && !constraints.oneOf.includes(value)) { errors[`${path}.${key}`] = `Value '${value}' for '${key}' not in oneOf ${JSON.stringify(constraints.oneOf)}`; } } else if (schema.strict) { errors[`${path}.${key}`] = `Key '${key}' is not allowed`; } } if (Object.keys(errors).length > 0) { return { valid: false, errors }; } return { valid: true, errors: {} }; } validateInput(context) { if (this.validateInputContext) { const validationResult = this.validateSchema( context, this.inputContextSchema ); if (!validationResult.valid) { this.emit("meta.task.validationFailed", { __taskId: this.id, __context: context, __errors: validationResult.errors }); return { errored: true, __error: "Input context validation failed", __validationErrors: JSON.stringify(validationResult.errors) }; } } return true; } validateOutput(context) { if (this.validateOutputContext) { const validationResult = this.validateSchema( context, this.outputContextSchema ); if (!validationResult.valid) { this.emit("meta.task.outputValidationFailed", { __taskId: this.id, __result: context, __errors: validationResult.errors }); return { errored: true, __error: "Output context validation failed", __validationErrors: JSON.stringify(validationResult.errors) }; } } return true; } /** * Executes the task function after optional input validation. * @param context - The GraphContext to validate and execute. * @param progressCallback - Callback for progress updates. * @returns TaskResult from the taskFunction or error object on validation failure. * @edge If validateInputContext is true, validates context; on failure, emits 'meta.task.validationFailed' with detailed errors. * @edge If validateOutputContext is true, validates output; on failure, emits 'meta.task.outputValidationFailed' with detailed errors. */ execute(context, progressCallback) { return this.taskFunction(context.getClonedContext(), progressCallback); } doAfter(...tasks) { for (const pred of tasks) { if (this.predecessorTasks.has(pred)) continue; pred.nextTasks.add(this); this.predecessorTasks.add(pred); this.updateLayerFromPredecessors(); if (this.hasCycle()) { this.decouple(pred); throw new Error(`Cycle adding pred ${pred.name} to ${this.name}`); } } this.updateProgressWeights(); return this; } then(...tasks) { for (const next of tasks) { if (this.nextTasks.has(next)) continue; this.nextTasks.add(next); next.predecessorTasks.add(this); next.updateLayerFromPredecessors(); if (next.hasCycle()) { this.decouple(next); throw new Error(`Cycle adding next ${next.name} to ${this.name}`); } } this.updateProgressWeights(); return this; } decouple(task) { if (task.nextTasks.has(this)) { task.nextTasks.delete(this); this.predecessorTasks.delete(task); } if (task.onFailTasks.has(this)) { task.onFailTasks.delete(this); this.predecessorTasks.delete(task); } this.updateLayerFromPredecessors(); } doOnFail(...tasks) { for (const task of tasks) { if (this.onFailTasks.has(task)) continue; this.onFailTasks.add(task); task.predecessorTasks.add(this); task.updateLayerFromPredecessors(); if (task.hasCycle()) { this.decouple(task); throw new Error(`Cycle adding onFail ${task.name} to ${this.name}`); } } return this; } updateProgressWeights() { const layers = this.getSubgraphLayers(); const numLayers = layers.size; if (numLayers === 0) return; const weightPerLayer = 1 / numLayers; layers.forEach((tasksInLayer) => { const numTasks = tasksInLayer.size; if (numTasks === 0) return; tasksInLayer.forEach( (task) => task.progressWeight = weightPerLayer / numTasks ); }); } getSubgraphLayers() { const layers = /* @__PURE__ */ new Map(); const queue = [this]; const visited = /* @__PURE__ */ new Set(); while (queue.length) { const task = queue.shift(); if (visited.has(task)) continue; visited.add(task); if (!layers.has(task.layerIndex)) layers.set(task.layerIndex, /* @__PURE__ */ new Set()); layers.get(task.layerIndex).add(task); task.nextTasks.forEach((next) => queue.push(next)); } return layers; } updateLayerFromPredecessors() { let maxPred = 0; this.predecessorTasks.forEach( (pred) => maxPred = Math.max(maxPred, pred.layerIndex) ); this.layerIndex = maxPred + 1; const queue = Array.from(this.nextTasks); while (queue.length) { const next = queue.shift(); next.updateLayerFromPredecessors(); next.nextTasks.forEach((n) => queue.push(n)); } } hasCycle() { const visited = /* @__PURE__ */ new Set(); const recStack = /* @__PURE__ */ new Set(); const dfs = (task) => { if (recStack.has(task)) return true; if (visited.has(task)) return false; visited.add(task); recStack.add(task); for (const next of task.nextTasks) { if (dfs(next)) return true; } recStack.delete(task); return false; }; return dfs(this); } mapNext(callback, failed = false) { const tasks = failed ? Array.from(this.onFailTasks) : Array.from(this.nextTasks); return tasks.map(callback); } mapPrevious(callback) { return Array.from(this.predecessorTasks).map(callback); } destroy() { super.destroy(); this.predecessorTasks.forEach((pred) => pred.nextTasks.delete(this)); this.nextTasks.forEach((next) => next.predecessorTasks.delete(this)); this.onFailTasks.forEach((fail) => fail.predecessorTasks.delete(this)); this.nextTasks.clear(); this.predecessorTasks.clear(); this.onFailTasks.clear(); this.destroyed = true; this.emit("meta.task.destroyed", { __id: this.id }); } export() { return { __id: this.id, __name: this.name, __description: this.description, __layerIndex: this.layerIndex, __isUnique: this.isUnique, __isMeta: this.isMeta, __isSignal: this.isSignal, __eventTriggers: this.observedSignals, __attachedEvents: this.signalsToEmit, __isDeputy: this.isDeputy, __throttled: this.throttled, __isEphemeral: this.isEphemeral, __concurrency: this.concurrency, __timeout: this.timeout, __functionString: this.taskFunction.toString(), __getTagCallback: this.getTag.toString(), __inputSchema: this.inputContextSchema, __validateInputContext: this.validateInputContext, __outputSchema: this.outputContextSchema, __validateOutputContext: this.validateOutputContext, __nextTasks: Array.from(this.nextTasks).map((t) => t.id), __onFailTasks: Array.from(this.onFailTasks).map((t) => t.id), __previousTasks: Array.from(this.predecessorTasks).map((t) => t.id) }; } getIterator() { return new TaskIterator(this); } accept(visitor) { visitor.visitTask(this); } log() { console.log(this.na