@cadenza.io/core
Version:
This is a framework for building asynchronous graphs and flows of tasks and signals.
1,675 lines (1,657 loc) • 105 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,
GraphRunner: () => GraphRunner,
SignalBroker: () => SignalBroker,
SignalEmitter: () => SignalEmitter,
SignalTask: () => SignalTask,
Task: () => Task,
default: () => index_default
});
module.exports = __toCommonJS(index_exports);
// 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;
}
function formatTimestamp(timestamp) {
return new Date(timestamp).toISOString();
}
// src/engine/SignalBroker.ts
var SignalBroker = class _SignalBroker {
// execId -> emitted signals
constructor() {
this.debug = false;
this.verbose = 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;
}
setVerbose(value) {
this.verbose = value;
}
validateSignalName(signalName) {
if (signalName.length > 100) {
throw new Error(
`Signal name must be less than 100 characters: ${signalName}`
);
}
if (signalName.includes(" ")) {
throw new Error(`Signal name must not contain spaces: ${signalName}"`);
}
if (signalName.includes("\\")) {
throw new Error(
`Signal name must not contain backslashes: ${signalName}`
);
}
if (/[A-Z]/.test(signalName.split(":")[0].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: ${signalName}`
);
}
}
/**
* 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.createMetaTask(
"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"
).doOn("meta.process_signal_queue_requested").emits("meta.signal_broker.queue_empty");
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";
delete context.__routineExecId;
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) {
var _a, _b, _c;
const isMeta = signal.startsWith("meta.");
const isSubMeta = signal.startsWith("sub_meta.") || context.__isSubMeta;
const isMetric = (_a = context.__signalEmission) == null ? void 0 : _a.isMetric;
if (!isSubMeta && (!isMeta || this.debug)) {
const emittedAt = Date.now();
context.__signalEmission = {
...context.__signalEmission,
signalName: signal,
emittedAt: formatTimestamp(emittedAt),
consumed: false,
consumedBy: null,
isMeta
};
} else if (isSubMeta) {
context.__isSubMeta = true;
delete context.__signalEmission;
} else {
delete context.__signalEmission;
}
if (this.debug && (!isMetric && !isSubMeta || this.verbose)) {
console.log(
`EMITTING ${signal} to listeners ${(_c = (_b = this.signalObservers.get(signal)) == null ? void 0 : _b.tasks.size) != null ? _c : 0} with context ${this.verbose ? JSON.stringify(context) : JSON.stringify(context).slice(0, 100)}`
);
}
let executed;
executed = this.executeListener(signal, context);
if (!isSubMeta) {
const parts = signal.slice(0, Math.max(signal.lastIndexOf(":"), signal.lastIndexOf("."))).split(".");
for (let i = parts.length; i > -1; i--) {
const parent = parts.slice(0, i).join(".");
executed = executed || this.executeListener(parent + ".*", context);
}
}
return executed;
}
executeListener(signal, context) {
const obs = this.signalObservers.get(signal);
const isMeta = signal.startsWith("meta");
const runner = isMeta ? this.metaRunner : this.runner;
if (obs && obs.tasks.size && runner) {
obs.fn(runner, Array.from(obs.tasks), context);
return true;
}
return false;
}
addSignal(signal) {
let _signal = signal;
if (!this.signalObservers.has(_signal)) {
this.validateSignalName(_signal);
this.signalObservers.set(_signal, {
fn: (runner, tasks, context) => runner.run(tasks, context),
tasks: /* @__PURE__ */ new Set()
});
const sections = _signal.split(":");
if (sections.length === 2) {
_signal = sections[0];
if (!this.signalObservers.has(sections[0])) {
this.signalObservers.set(_signal, {
fn: (runner, tasks, context) => runner.run(tasks, context),
tasks: /* @__PURE__ */ new Set()
});
} else {
return;
}
}
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_uuid4 = 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 exportedTaskNames = [];
while (tasks.hasNext()) {
const task2 = tasks.next();
if (task2 && !exportedTaskNames.includes(task2.name)) {
exportedTaskNames.push(task2.name);
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");
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;
}
getClonedFullContext() {
return deepCloneFilter(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.
* @param signal The signal name.
* @param data Optional payload (defaults to empty object).
*/
emit(signal, data = {}) {
Cadenza.broker.emit(signal, data);
}
/**
* Emits a signal via the broker if not silent.
* @param signal The signal name.
* @param data Optional payload (defaults to empty object).
*/
emitMetrics(signal, data = {}) {
if (this.silent) {
return;
}
Cadenza.broker.emit(signal, data);
}
};
// src/utils/promise.ts
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// src/graph/execution/GraphNode.ts
var GraphNode = class _GraphNode extends SignalEmitter {
constructor(task, context, routineExecId, prevNodes = [], debug = false, verbose = false) {
var _a;
super(
task.isMeta && !debug || task.isSubMeta || ((_a = context == null ? void 0 : context.getMetadata()) == null ? void 0 : _a.__isSubMeta)
);
this.divided = false;
this.splitGroupId = "";
this.processing = false;
this.subgraphComplete = false;
this.graphComplete = false;
this.result = false;
this.retryCount = 0;
this.retryDelay = 0;
this.retries = 0;
this.previousNodes = [];
this.nextNodes = [];
this.executionTime = 0;
this.executionStart = 0;
this.failed = false;
this.errored = false;
this.destroyed = false;
this.debug = false;
this.verbose = false;
this.id = (0, import_uuid3.v4)();
this.task = task;
this.context = context;
this.retryCount = task.retryCount;
this.retryDelay = task.retryDelay;
this.previousNodes = prevNodes;
this.routineExecId = routineExecId;
this.splitGroupId = routineExecId;
this.debug = debug;
this.verbose = verbose;
}
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.name === node.task.name;
}
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) {
var _a, _b, _c;
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);
const context = this.context.getFullContext();
const scheduledAt = Date.now();
this.emitMetricsWithMetadata("meta.node.scheduled", {
data: {
uuid: this.id,
routineExecutionId: this.routineExecId,
executionTraceId: (_b = context.__executionTraceId) != null ? _b : (_a = context.__metadata) == null ? void 0 : _a.__executionTraceId,
context: this.context.export(),
taskName: this.task.name,
taskVersion: this.task.version,
isMeta: this.isMeta(),
isScheduled: true,
splitGroupId: this.splitGroupId,
created: formatTimestamp(scheduledAt)
}
});
this.previousNodes.forEach((node) => {
this.emitMetricsWithMetadata("meta.node.mapped", {
data: {
taskExecutionId: this.id,
previousTaskExecutionId: node.id,
executionCount: "increment"
},
filter: {
taskName: this.task.name,
taskVersion: this.task.version,
previousTaskName: node.task.name,
previousTaskVersion: node.task.version
}
});
});
if (((_c = context.__signalEmission) == null ? void 0 : _c.consumed) === false && (!this.isMeta() || this.debug)) {
this.emitMetricsWithMetadata("meta.node.consumed_signal", {
data: {
signalName: context.__signalEmission.signalName,
taskName: this.task.name,
taskVersion: this.task.version,
taskExecutionId: this.id,
consumedAt: formatTimestamp(scheduledAt)
}
});
context.__signalEmission.consumed = true;
context.__signalEmission.consumedBy = this.id;
}
}
}
start() {
if (this.executionStart === 0) {
this.executionStart = Date.now();
}
if (this.previousNodes.length === 0) {
this.emitMetricsWithMetadata("meta.node.started_routine_execution", {
data: {
isRunning: true,
started: formatTimestamp(this.executionStart)
},
filter: { uuid: this.routineExecId }
});
}
if (this.debug && !this.task.isSubMeta && !this.context.getMetadata().__isSubMeta || this.verbose) {
this.log();
}
this.emitMetricsWithMetadata("meta.node.started", {
data: {
isRunning: true,
started: formatTimestamp(this.executionStart)
},
filter: { uuid: this.id }
});
return this.executionStart;
}
end() {
if (this.executionStart === 0) {
return 0;
}
this.processing = false;
const end = Date.now();
this.executionTime = end - this.executionStart;
const context = this.context.getFullContext();
if (this.errored || this.failed) {
this.emitMetricsWithMetadata("meta.node.errored", {
data: {
isRunning: false,
errored: this.errored,
failed: this.failed,
errorMessage: context.__error
},
filter: { uuid: this.id }
});
}
this.emitMetricsWithMetadata("meta.node.ended", {
data: {
isRunning: false,
isComplete: true,
resultContext: this.context.export(),
errored: this.errored,
failed: this.failed,
errorMessage: context.__error,
progress: 1,
ended: formatTimestamp(end)
},
filter: { uuid: this.id }
});
if (this.graphDone()) {
this.emitMetricsWithMetadata(
`meta.node.ended_routine_execution:${this.routineExecId}`,
{
data: {
isRunning: false,
isComplete: true,
resultContext: this.context.export(),
progress: 1,
ended: formatTimestamp(end)
},
filter: { uuid: this.routineExecId }
}
);
}
return end;
}
execute() {
if (!this.divided && !this.processing) {
this.processing = true;
const inputValidation = this.task.validateInput(
this.isMeta() ? this.context.getMetadata() : this.context.getContext()
);
if (inputValidation !== true) {
this.onError(inputValidation.__validationErrors);
this.postProcess();
return this.nextNodes;
}
this.result = this.work();
if (this.result instanceof Promise) {
return this.executeAsync();
}
const nextNodes = this.postProcess();
if (nextNodes instanceof Promise) {
return nextNodes;
}
this.nextNodes = nextNodes;
}
return this.nextNodes;
}
async workAsync() {
try {
this.result = await this.result;
} catch (e) {
const result = await this.retryAsync(e);
if (result === e) {
this.onError(e);
}
}
}
async executeAsync() {
await this.workAsync();
const nextNodes = this.postProcess();
if (nextNodes instanceof Promise) {
return nextNodes;
}
this.nextNodes = nextNodes;
return this.nextNodes;
}
work() {
try {
const result = this.task.execute(
this.context,
this.emitWithMetadata.bind(this),
this.onProgress.bind(this)
);
if (result.errored || result.failed) {
return this.retry(result);
}
return result;
} catch (e) {
const result = this.retry(e);
return result.then((result2) => {
if (result2 !== e) {
return result2;
}
this.onError(e);
return this.result;
});
}
}
emitWithMetadata(signal, ctx) {
const data = { ...ctx };
if (!this.task.isHidden) {
data.__signalEmission = {
taskName: this.task.name,
taskVersion: this.task.version,
taskExecutionId: this.id
};
data.__metadata = {
__routineExecId: this.routineExecId
};
}
this.emit(signal, data);
}
emitMetricsWithMetadata(signal, ctx) {
const data = { ...ctx };
if (!this.task.isHidden) {
data.__signalEmission = {
taskName: this.task.name,
taskVersion: this.task.version,
taskExecutionId: this.id,
isMetric: true
};
data.__metadata = {
__routineExecId: this.routineExecId
};
}
this.emitMetrics(signal, data);
}
onProgress(progress) {
var _a, _b;
progress = Math.min(Math.max(0, progress), 1);
this.emitMetricsWithMetadata("meta.node.progress", {
data: {
progress
},
filter: {
uuid: this.id
}
});
this.emitMetricsWithMetadata(
`meta.node.routine_execution_progress:${this.routineExecId}`,
{
data: {
progress: progress * this.task.progressWeight / ((_b = (_a = this.layer) == null ? void 0 : _a.getIdenticalNodes(this).length) != null ? _b : 1)
},
filter: {
uuid: this.routineExecId
}
}
);
}
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}`);
}
const nextNodes = this.divide();
if (nextNodes instanceof Promise) {
return this.postProcessAsync(nextNodes);
}
this.nextNodes = nextNodes;
this.finalize();
return this.nextNodes;
}
async postProcessAsync(nextNodes) {
this.nextNodes = await nextNodes;
this.finalize();
return this.nextNodes;
}
finalize() {
if (this.nextNodes.length === 0) {
this.completeSubgraph();
}
if (this.errored || this.failed) {
this.task.mapOnFailSignals(
(signal) => this.emitWithMetadata(signal, this.context.getFullContext())
);
} else if (this.result !== void 0 && this.result !== false) {
this.task.mapSignals(
(signal) => this.emitWithMetadata(signal, this.context.getFullContext())
);
}
this.end();
}
onError(error, errorData = {}) {
this.result = {
...this.context.getFullContext(),
__error: `Node error: ${error}`,
__retries: this.retries,
error: `Node error: ${error}`,
returnedValue: this.result,
...errorData
};
this.migrate(this.result);
this.errored = true;
}
async retry(prevResult) {
if (this.retryCount === 0) {
return prevResult;
}
await this.delayRetry();
return this.work();
}
async retryAsync(prevResult) {
if (this.retryCount === 0) {
return prevResult;
}
await this.delayRetry();
this.result = this.work();
return this.workAsync();
}
async delayRetry() {
this.retryCount--;
this.retries++;
await sleep(this.retryDelay);
this.retryDelay *= this.task.retryDelayFactor;
if (this.retryDelay > this.task.retryDelayMax) {
this.retryDelay = this.task.retryDelayMax;
}
}
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();
if (current instanceof Promise) {
return this.divideAsync(current);
}
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.divided = true;
this.migrate({
...this.result,
...this.context.getMetadata(),
__nextNodes: newNodes.map((n) => n.id),
__retries: this.retries
});
return newNodes;
}
}
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),
__retries: this.retries
});
return newNodes;
}
async divideAsync(current) {
const nextNodes = [];
const _current = await current;
const outputValidation = this.task.validateOutput(_current.value);
if (outputValidation !== true) {
this.onError(outputValidation.__validationErrors);
return nextNodes;
} else {
nextNodes.push(...this.generateNewNodes(_current.value));
}
for await (const result of this.result) {
const outputValidation2 = this.task.validateOutput(result);
if (outputValidation2 !== true) {
this.onError(outputValidation2.__validationErrors);
return [];
} else {
nextNodes.push(...this.generateNewNodes(result));
}
}
this.divided = true;
return nextNodes;
}
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) {
var _a, _b;
this.task = task;
this.retryCount = task.retryCount;
this.retryDelay = task.retryDelay;
this.silent = task.isMeta && !this.debug || task.isSubMeta || ((_b = (_a = this.context) == null ? void 0 : _a.getMetadata()) == null ? void 0 : _b.__isSubMeta);
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,
this.verbose
);
}
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: {
__name: this.task.name,
__version: this.task.version
},
__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(
"Node EXECUTION:",
this.task.name,
JSON.stringify(this.context.getFullContext())
);
}
};
// src/graph/definition/GraphRoutine.ts
var GraphRoutine = class extends SignalEmitter {
constructor(name, tasks, description, isMeta = false) {
super();
this.version = 1;
this.isMeta = false;
this.tasks = /* @__PURE__ */ new Set();
this.observedSignals = /* @__PURE__ */ new Set();
this.name = name;
this.description = description;
this.isMeta = isMeta;
this.emit("meta.routine.created", {
data: {
name: this.name,
version: this.version,
description: this.description,
isMeta: this.isMeta
},
__routineInstance: this
});
tasks.forEach((t) => {
this.tasks.add(t);
this.emit("meta.routine.task_added", {
data: {
taskName: t.name,
taskVersion: t.version,
routineName: this.name,
routineVersion: this.version
}
});
});
}
/**
* 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 Version.
* @param version The Version.
*/
setVersion(version) {
this.version = version;
this.emit("meta.routine.global_version_set", { version: this.version });
}
/**
* 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;
}
/**
* 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;
}
/**
* Destroys the routine.
*/
destroy() {
this.unsubscribeAll();
this.tasks.clear();
this.emit("meta.routine.destroyed", {
data: { deleted: true },
filter: { name: this.name, version: this.version }
});
}
};
// 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 SignalEmitter {
/**
* 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 isSubMeta
* @param isHidden
* @param getTagCallback
* @param inputSchema
* @param validateInputContext
* @param outputSchema
* @param validateOutputContext
* @param retryCount
* @param retryDelay
* @param retryDelayMax
* @param retryDelayFactor
* @edge Emits 'meta.task.created' with { __task: this } for seed.
*/
constructor(name, task, description = "", concurrency = 0, timeout = 0, register = true, isUnique = false, isMeta = false, isSubMeta = false, isHidden = false, getTagCallback = void 0, inputSchema = void 0, validateInputContext = false, outputSchema = void 0, validateOutputContext = false, retryCount = 0, retryDelay = 0, retryDelayMax = 0, retryDelayFactor = 1) {
super(isSubMeta || isHidden);
this.version = 1;
this.isMeta = false;
this.isSubMeta = false;
this.isHidden = false;
this.isUnique = false;
this.throttled = false;
this.isSignal = false;
this.isDeputy = false;
this.isEphemeral = false;
this.isDebounce = false;
this.inputContextSchema = void 0;
this.validateInputContext = false;
this.outputContextSchema = void 0;
this.validateOutputContext = false;
this.retryCount = 0;
this.retryDelay = 0;
this.retryDelayMax = 0;
this.retryDelayFactor = 1;
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.emitsSignals = /* @__PURE__ */ new Set();
this.signalsToEmitAfter = /* @__PURE__ */ new Set();
this.signalsToEmitOnFail = /* @__PURE__ */ new Set();
this.observedSignals = /* @__PURE__ */ new Set();
this.name = name;
this.taskFunction = task.bind(this);
this.description = description;
this.concurrency = concurrency;
this.timeout = timeout;
this.isUnique = isUnique;
this.isMeta = isMeta;
this.isSubMeta = isSubMeta;
this.isHidden = isHidden;
this.inputContextSchema = inputSchema;
this.validateInputContext = validateInputContext;
this.outputContextSchema = outputSchema;
this.validateOutputContext = validateOutputContext;
this.retryCount = retryCount;
this.retryDelay = retryDelay;
this.retryDelayMax = retryDelayMax;
this.retryDelayFactor = retryDelayFactor;
if (getTagCallback) {
this.getTag = (context) => getTagCallback(context, this);
this.throttled = true;
}
if (register && !this.isHidden && !this.isSubMeta) {
const { __functionString, __getTagCallback } = this.export();
this.emitWithMetadata("meta.task.created", {
data: {
name: this.name,
version: this.version,
description: this.description,
functionString: __functionString,
tagIdGetter: __getTagCallback,
layerIndex: this.layerIndex,
concurrency: this.concurrency,
retryCount: this.retryCount,
retryDelay: this.retryDelay,
retryDelayMax: this.retryDelayMax,
retryDelayFactor: this.retryDelayFactor,
timeout: this.timeout,
isUnique: this.isUnique,
isSignal: this.isSignal,
isThrottled: this.throttled,
isDebounce: this.isDebounce,
isEphemeral: this.isEphemeral,
isMeta: this.isMeta,
validateInputContext: this.validateInputContext,
validateOutputContext: this.validateOutputContext,
inputContextSchema: this.inputContextSchema,
outputContextSchema: this.outputContextSchema
},
__taskInstance: this
});
}
}
getTag(context) {
return this.name;
}
setVersion(version) {
this.version = version;
this.emitWithMetadata("meta.task.version_set", {
__version: this.version
});
}
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;
}
emitWithMetadata(signal, ctx = {}) {
const data = { ...ctx };
if (!this.isHidden && !this.isSubMeta) {
data.__signalEmission = {
taskName: this.name,
taskVersion: this.version
};
}
this.emit(signal, data);
}
emitMetricsWithMetadata(signal, ctx = {}) {
const data = { ...ctx };
if (!this.isHidden && !this.isSubMeta) {
data.__signalEmission = {
taskName: this.name,
taskVersion: this.version,
isMetric: true
};
}
this.emitMetrics(signal, data);
}
/**
* 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 === "any") {
continue;
}
if ((value === void 0 || value === null) && !prop.strict) {
continue;
}
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,