@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
JavaScript
"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