UNPKG

execution-engine

Version:

A TypeScript library for tracing and visualizing code execution workflows.

296 lines (295 loc) 13.6 kB
"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;