execution-engine
Version:
A TypeScript library for tracing and visualizing code execution workflows.
296 lines (295 loc) • 13.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TraceableEngine = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
const async_hooks_1 = require("async_hooks");
const engineNodeData_model_1 = require("../common/models/engineNodeData.model");
const engineTraceOptions_model_1 = require("../common/models/engineTraceOptions.model");
const executionTrace_model_1 = require("../common/models/executionTrace.model");
const jsonQuery_1 = require("../common/utils/jsonQuery");
const trace_1 = require("../execution/trace");
const executionTimer_1 = require("../timer/executionTimer");
/**
* Represents a class for traceable execution of functions.
*/
class TraceableEngine {
/**
* Initializes a new instance of the TraceableExecution class.
* @param initialTrace - The initial trace to be used.
*/
constructor(initialTrace) {
this.asyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
this.initTrace(initialTrace);
}
static extractIOExecutionTraceWithConfig(ioeExecTrace, extractionConfig) {
try {
if (typeof extractionConfig === 'function') {
return extractionConfig(ioeExecTrace);
}
else if (Array.isArray(extractionConfig)) {
return (0, jsonQuery_1.extract)(ioeExecTrace, extractionConfig);
}
else if (extractionConfig === true) {
return ioeExecTrace;
}
}
catch (e) {
throw new Error(`error when mapping/extracting ExecutionTrace with config: "${extractionConfig}", ${e?.message}`);
}
}
extractNarrativeWithConfig(nodeData, narrativeConfig) {
try {
const narratives = (this.narrativesForNonFoundNodes[nodeData.id] ?? [])?.concat(nodeData.narratives ?? []);
delete this.narrativesForNonFoundNodes[nodeData.id];
if (typeof narrativeConfig === 'function') {
return narratives.concat(narrativeConfig(nodeData));
}
else if (Array.isArray(narrativeConfig)) {
return narratives.concat(narrativeConfig);
}
else if (narrativeConfig === true) {
return narratives;
}
}
catch (e) {
throw new Error(`error when mapping/extracting Narrative with config: "${narrativeConfig}", ${e?.message}`);
}
}
/**
* Initializes the trace with given initialTrace.
*
* @param {EngineTrace} initialTrace - The initial trace to initialize: the nodes and edges.
* @return {TraceableExecution} - The traceable execution object after initialization.
*/
initTrace(initialTrace) {
this.nodes = initialTrace?.filter((b) => b.group === 'nodes') ?? [];
this.edges = initialTrace?.filter((b) => b.group === 'edges') ?? [];
this.narrativesForNonFoundNodes = {};
return this;
}
/**
* Gets the execution trace.
* @returns An array containing nodes and edges of the execution trace.
*/
getTrace() {
return [...this.nodes, ...this.edges];
}
/**
* Gets the nodes of the execution trace.
* @returns An array containing nodes of the execution trace.
*/
getTraceNodes() {
return this.nodes;
}
/**
*
* @param blockFunction
* @param inputs array of arguments given as input to the function `blockFunction` parameter
* @param options execution options it could be simply a trace for instance:
*
* ATTENTION: arrow function as blockFunction ARE NOT RECOMMENDED it will work correctly at the overload inferring level, especially if you do:
* ```
* () => { throw new Error("error example");}
* ```
*/
run(blockFunction, inputs = [], options = {
trace: {
id: [blockFunction.name ? blockFunction.name.replace('bound ', '') : 'function', new Date()?.getTime(), crypto.randomUUID()]?.join('_'),
label: blockFunction.name ? blockFunction.name.replace('bound ', '') : 'function'
},
config: engineTraceOptions_model_1.DEFAULT_TRACE_CONFIG
}) {
const inputHasCircular = inputs.find((i) => i instanceof TraceableEngine);
if (inputHasCircular) {
throw Error(`${blockFunction.name} could not have an instance of TraceableExecution as input, this will create circular dependency on trace`);
}
const nodeTraceConfigFromOptions = (0, engineNodeData_model_1.isEngineNodeTrace)(options) ? undefined : (options.config ?? engineTraceOptions_model_1.DEFAULT_TRACE_CONFIG);
const nodeTraceFromOptions = ((0, engineNodeData_model_1.isEngineNodeTrace)(options) ? options : options.trace) ?? {};
nodeTraceFromOptions.parent = nodeTraceFromOptions?.parent ?? this.asyncLocalStorage.getStore();
const executionTimer = new executionTimer_1.ExecutionTimer();
executionTimer?.start();
const nodeTrace = {
id: [
blockFunction.name ? blockFunction.name.replace('bound ', '') : 'function',
executionTimer?.getStartDate()?.getTime(),
crypto.randomUUID()
]?.join('_'),
label: [
(this.nodes?.length ?? 0) + 1,
nodeTraceFromOptions?.id ?? (blockFunction.name ? blockFunction.name.replace('bound ', '') : 'function')
]?.join(' - '),
...nodeTraceFromOptions
};
return (0, trace_1.executionTrace)(() => this.asyncLocalStorage.run(nodeTrace.id, () => blockFunction.bind(this)(...inputs, nodeTrace)), inputs, (traceContext) => {
// TODO: metadata are not handled in the engine ExecutionTrace for now, to add it later.
delete traceContext.metadata;
delete traceContext.isPromise;
return this.buildTrace.bind(this)(nodeTrace, traceContext, nodeTraceConfigFromOptions);
}, { errorStrategy: nodeTraceConfigFromOptions.errors });
}
/**
* Pushes or appends narratives to a trace node.
* @param nodeId - The ID of the node.
* @param narratives - The narrative or array of narratives to be processed.
* @returns The updated instance of TraceableExecution.
*/
pushNarratives(nodeId, narratives) {
const existingNodeIndex = this.nodes?.findIndex((n) => n.data.id === nodeId);
if (existingNodeIndex >= 0) {
// Node already exists, update its narratives
this.nodes[existingNodeIndex].data = {
...this.nodes[existingNodeIndex]?.data,
narratives: [...(this.nodes[existingNodeIndex]?.data?.narratives ?? []), ...(Array.isArray(narratives) ? narratives : [narratives])]
};
}
else {
// Node doesn't exist, store narratives for later use
this.narrativesForNonFoundNodes[nodeId] = [
...(this.narrativesForNonFoundNodes[nodeId] ?? []),
...(Array.isArray(narratives) ? narratives : [narratives])
];
}
return this;
}
/**
* Retrieves an ordered array of narratives.
*
* @returns {Array<string>} An array that contains the ordered narratives. If no narratives are found, an empty array is returned.
*/
getNarratives() {
return this.nodes?.flatMap?.((n) => n.data?.narratives)?.filter((n) => !!n);
}
buildTrace(nodeTrace, executionTrace, options = engineTraceOptions_model_1.DEFAULT_TRACE_CONFIG, isAutoCreated = false) {
if (nodeTrace.parent && !this.nodes?.find((n) => n.data.id === nodeTrace.parent)) {
this.buildTrace({
id: nodeTrace.parent,
label: nodeTrace.parent
}, { id: nodeTrace.id, errors: executionTrace.errors }, engineTraceOptions_model_1.DEFAULT_TRACE_CONFIG, true);
}
if (!isAutoCreated) {
let parallelEdge = undefined;
if (options.parallel === true) {
parallelEdge = this.edges?.find((edge) => edge.data.parallel && edge.data.parent === nodeTrace.parent);
}
else if (typeof options.parallel === 'string') {
parallelEdge = this.edges?.find((edge) => edge.data.parallel === options.parallel);
}
const previousNodes = !parallelEdge
? this.nodes?.filter((node) => !node.data.abstract &&
node.data.parent === nodeTrace.parent &&
(!options?.parallel || !node.data.parallel || !node.data.parent || !nodeTrace.parent) &&
node.data.id !== nodeTrace.id &&
node.data.parent !== nodeTrace.id &&
node.data.id !== nodeTrace.parent &&
!this.edges.find((e) => e.data.source === node.data.id))
: [];
this.edges = [
...(this.edges ?? []),
...(previousNodes?.map((previousNode) => ({
data: {
id: `${previousNode.data.id}->${nodeTrace.id}`,
source: previousNode.data.id,
target: nodeTrace.id,
parent: nodeTrace.parent,
parallel: options?.parallel
},
group: 'edges'
})) ?? []),
...(parallelEdge
? [
{
data: {
id: `${parallelEdge.data.source}->${nodeTrace.id}`,
source: parallelEdge.data.source,
target: nodeTrace.id,
parent: nodeTrace.parent,
parallel: options?.parallel
},
group: 'edges'
}
]
: [])
];
}
const filteredNodeData = {
...this.filterNodeTrace(nodeTrace),
...this.filterNodeExecutionTrace({ ...executionTrace, ...nodeTrace }, options?.traceExecution)
};
if (filteredNodeData?.narratives?.length) {
this.pushNarratives(nodeTrace.id, filteredNodeData?.narratives);
}
// si ne node existe déjà (un parent auto-créé):
const existingNodeIndex = this.nodes?.findIndex((n) => n.data.id === nodeTrace?.id);
if (existingNodeIndex >= 0) {
this.nodes[existingNodeIndex] = {
data: {
...this.nodes[existingNodeIndex]?.data,
...filteredNodeData,
parallel: options?.parallel,
abstract: isAutoCreated,
updateTime: new Date()
},
group: 'nodes'
};
}
else {
this.nodes?.push({
data: {
...filteredNodeData,
parallel: options?.parallel,
abstract: isAutoCreated,
createTime: new Date()
},
group: 'nodes'
});
}
}
filterNodeTrace(nodeData) {
return {
id: nodeData?.id,
label: nodeData?.label,
...(nodeData?.parent ? { parent: nodeData?.parent } : {}),
...(nodeData?.parallel ? { parallel: nodeData?.parallel } : {}),
...(nodeData?.abstract ? { abstract: nodeData?.abstract } : {}),
...(nodeData?.createTime ? { createTime: nodeData?.createTime } : {}),
...(nodeData?.updateTime ? { updateTime: nodeData?.updateTime } : {})
};
}
filterNodeExecutionTrace(nodeData, doTraceExecution) {
if (!doTraceExecution) {
return {};
}
if (doTraceExecution === true) {
nodeData.narratives = this.extractNarrativeWithConfig(nodeData, true);
return nodeData;
}
if (Array.isArray(doTraceExecution)) {
const execTrace = { id: nodeData.id };
Object.keys(nodeData).forEach((k) => {
if (doTraceExecution.includes(k)) {
execTrace[k] = nodeData[k];
}
});
return execTrace;
}
if ((0, executionTrace_model_1.isExecutionTrace)(doTraceExecution)) {
const execTrace = { id: nodeData.id };
execTrace.inputs = TraceableEngine.extractIOExecutionTraceWithConfig(nodeData.inputs, doTraceExecution.inputs);
execTrace.outputs = TraceableEngine.extractIOExecutionTraceWithConfig(nodeData.outputs, doTraceExecution.outputs);
execTrace.errors = TraceableEngine.extractIOExecutionTraceWithConfig(nodeData.errors, doTraceExecution.errors);
execTrace.narratives = this.extractNarrativeWithConfig(nodeData, doTraceExecution.narratives);
if (doTraceExecution.startTime === true) {
execTrace.startTime = nodeData.startTime;
}
if (doTraceExecution.endTime === true) {
execTrace.endTime = nodeData.endTime;
}
if (doTraceExecution.startTime === true && doTraceExecution.endTime === true) {
execTrace.duration = nodeData.duration;
execTrace.elapsedTime = nodeData.elapsedTime;
}
return execTrace;
}
}
}
exports.TraceableEngine = TraceableEngine;