From 447fec9c393f428ed9c1ca2edc27a312626166e4 Mon Sep 17 00:00:00 2001 From: Akram Date: Sun, 10 Dec 2023 15:54:02 +0100 Subject: [PATCH] feat: add execution engine decorators @engine() and @run() feat: add examples of ExecutionEngine using decorators and update the README feat: add weather example for more advanced exection-engine usage through decorators docs: update EngineTask Class and @engine/@run Decorators feat: remove bound prefix from function name to not figure in trace --- README.md | 69 +++-- docs/EngineTask.md | 45 +++ docs/README.md | 76 +++++ examples/usage.ts | 6 +- examples/usage2.json | 47 ++++ examples/usage2.ts | 28 ++ examples/weather.json | 282 +++++++++++++++++++ examples/weather.ts | 84 ++++++ package-lock.json | 17 +- package.json | 1 + src/engine/executionEngineDecorators.spec.ts | 135 +++++++++ src/engine/executionEngineDecorators.ts | 77 +++++ src/index.ts | 1 + src/trace/traceableExecution.ts | 20 +- tsconfig.json | 1 + yarn.lock | 5 + 16 files changed, 846 insertions(+), 48 deletions(-) create mode 100644 docs/EngineTask.md create mode 100644 examples/usage2.json create mode 100644 examples/usage2.ts create mode 100644 examples/weather.json create mode 100644 examples/weather.ts create mode 100644 src/engine/executionEngineDecorators.spec.ts create mode 100644 src/engine/executionEngineDecorators.ts diff --git a/README.md b/README.md index d4b5b60..66c0b84 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ yarn add execution-engine ## Usage 📚 -- example: +### Example 1: Basic Usage ```typescript import { ExecutionEngine } from "execution-engine"; @@ -42,38 +42,58 @@ import { ExecutionEngine } from "execution-engine"; const engine = new ExecutionEngine(); // for sync functions: -const result1 = engine.run((param) => `result1 for ${param}`, ['param1']); +const res1 = engine.run((param) => `result1 for ${param}`, ['param1']); // for async functions: -const result2 = await engine.run(async (param) => `result2 for ${param}`, [result1.outputs]); +const res2 = await engine.run(async (param) => `result2 for ${param}`, [res1.outputs]); // Retrieve the trace const trace = engine.getTrace(); console.log('Trace:', trace); ``` -- The `result` object contains function output (`outputs`), input parameters (`inputs`), and other attributes related to - the engine. It has the following structure: +You can: + +- view the **complete code** in [examples/usage.ts](examples/usage.ts) +- inspect the **trace output** in [examples/usage.json](examples/usage.json). +- visualize the **trace graph** using the json-to-graph online tool. [→ See the result ←](https://tabkram.github.io/json-to-graph/?data=https://raw.githubusercontent.com/tabkram/execution-engine/main/examples/usage.json) + +### Example 2: Usage with Decorators ```typescript -result = { - // An array containing the input values passed to thunction: - inputs: [ - "param1" - ], - // The output value returned by the function: - outputs: "result1 for param1", - // The start time of the function execution: - startTime: Date, - // The end time of the function execution: - endTime: Date, - // The duration of the function execution in milliseconds: - duration: number, - // ...other properties depending on the configuration and trace options. +import { engine, run } from "execution-engine"; + +@engine({ id: "uniqueEngineId" }) +class MyClass extends EngineTask { + @run() + myMethod1(param: string) { + return `result1 for ${param}`; + } + + @run() + async myMethod2(param: string) { + return `result2 for ${param}`; + } } + +const myInstance = new MyClass(); +myInstance.myMethod2("param1"); +await myInstance.myMethod2("param2"); + +// Retrieve the trace +const trace = myInstance.engine.getTrace(); +console.log("Trace:", trace); ``` -- The `trace` object is an array containing **nodes** and **edges**. It has the following structure: +You can: + +- view the **complete code** in [examples/usage2.ts](examples/usage2.ts) +- inspect the **trace output** in [examples/usage2.json](examples/usage2.json) +- visualize the **trace graph** using the json-to-graph online tool. [→ See the result ←](https://tabkram.github.io/json-to-graph/?data=https://raw.githubusercontent.com/tabkram/execution-engine/main/examples/usage2.json) + +### Understanding the Trace 🧭 + +The `trace` object is an array containing **nodes** and **edges**. It has the following structure: ```typescript trace = [ @@ -81,7 +101,7 @@ trace = [ data: { id: function_uuid1, label: "function" - //... other properties of the "result1" of the executed function as mentioned above + //... other properties of the result of the executed function as mentioned above }, group: nodes }, @@ -89,7 +109,7 @@ trace = [ data: { id: function_uuid2, label: "function" - //... other properties of the "result2" of the executed function as mentioned above + //... other properties of the result of the executed function as mentioned above }, group: nodes }, @@ -105,11 +125,6 @@ trace = [ ]; ``` -- Visualize the `trace` object using the json-to-graph online tool. [→ See the result ←](https://tabkram.github.io/json-to-graph/?data=https://raw.githubusercontent.com/tabkram/execution-engine/main/examples/usage.json) - -You can view the complete code in [examples/usage.ts](examples/usage.ts) and inspect the entire trace output -in [examples/usage.json](examples/usage.json). - ## Examples 📘 For additional usage examples, please explore the __[/examples](examples)__ directory in this repository. diff --git a/docs/EngineTask.md b/docs/EngineTask.md new file mode 100644 index 0000000..84e3876 --- /dev/null +++ b/docs/EngineTask.md @@ -0,0 +1,45 @@ +# EngineTask + +The `EngineTask` class represents an abstract task with a reference to the `ExecutionEngine`. It serves as a base class for integrating execution engine capabilities into other classes. + +## @engine Decorator + +The `@engine` decorator enhances a class with execution engine capabilities. It takes a configuration object as an argument, allowing you to customize the behavior of the associated engine. + +### Usage + +```typescript +@engine({ id: "uniqueEngineId" }) +class MyClass extends EngineTask { + // Class implementation +} +``` + +#### Explanation + +The `@engine` decorator is applied to a class to inject execution engine capabilities. The configuration object passed as an argument provides a unique identifier (`id`) for the associated engine. This allows multiple classes to use different engines, each with its own configuration. + +## @run Decorator + +The `@run` decorator enables tracing for decorated methods. It takes trace options as an optional argument, allowing you to fine-tune the tracing behavior. + +### Usage + +```typescript +class MyClass extends EngineTask { + @run() + myMethod1(param: string) { + // Method implementation + } + + @run() + async myMethod2(param: string) { + // Async method implementation + } +``` + +#### Explanation + +The `@run` decorator is applied to methods within a class to enable tracing for their executions. The optional trace options allow you to customize the tracing behavior for specific methods. For example, you can configure whether a method should be traced asynchronously or set additional options for the trace. + +This section provides a detailed explanation of how to use the `@engine` and `@run` decorators along with the `EngineTask` class. By understanding the purpose and usage of these decorators, you can effectively integrate execution engine features and tracing into your TypeScript classes, tailoring them to the specific requirements of your project. Adjust the import statements and decorator parameters based on your actual implementation. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index a81aa9e..89445d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,10 +15,79 @@ A TypeScript library for tracing and visualizing code execution workflows ## Installation 📦 +Use [npm](https://www.npmjs.com/package/execution-engine) package manager: + ```bash npm install execution-engine ``` +Or use the [yarn](https://yarnpkg.com/package?name=execution-engine) package manager: + +```bash +yarn add execution-engine +``` + +## Usage 📚 + +### Example 1: Basic Usage + +```typescript +import { ExecutionEngine } from "execution-engine"; + +const engine = new ExecutionEngine(); + +// for sync functions: +const res1 = engine.run((param) => `result1 for ${param}`, ['param1']); + +// for async functions: +const res2 = await engine.run(async (param) => `result2 for ${param}`, [res1.outputs]); + +// Retrieve the trace +const trace = engine.getTrace(); +console.log('Trace:', trace); +``` + +You can: + +- view the **complete code** in [examples/usage.ts](examples/usage.ts) +- inspect the **trace output** in [examples/usage.json](examples/usage.json). +- visualize the **trace graph** using the json-to-graph online + tool. [→ See the result ←](https://tabkram.github.io/json-to-graph/?data=https://raw.githubusercontent.com/tabkram/execution-engine/main/examples/usage.json) + +### Example 2: Usage with Decorators + +```typescript +import { engine, run } from "execution-engine"; + +@engine({ id: "uniqueEngineId" }) +class MyClass extends EngineTask { + @run() + myMethod1(param: string) { + return `result1 for ${param}`; + } + + @run() + async myMethod2(param: string) { + return `result2 for ${param}`; + } +} + +const myInstance = new MyClass(); +myInstance.myMethod2("param1"); +await myInstance.myMethod2("param2"); + +// Retrieve the trace +const trace = myInstance.engine.getTrace(); +console.log("Trace:", trace); +``` + +You can: + +- view the **complete code** in [examples/usage2.ts](examples/usage2.ts) +- inspect the **trace output** in [examples/usage2.json](examples/usage2.json) +- visualize the **trace graph** using the json-to-graph online + tool. [→ See the result ←](https://tabkram.github.io/json-to-graph/?data=https://raw.githubusercontent.com/tabkram/execution-engine/main/examples/usage2.json) + ## Components 🧩 ### ExecutionTimer @@ -35,3 +104,10 @@ The __[ExecutionEngine](./ExecutionEngine.md)__ enhances traceable execution by capturing additional relevant information. It builds upon the functionality of [TraceableExecution](./TraceableExecution.md). +### EngineTask Class and @engine/@run Decorators + +The __[EngineTask](./EngineTask.md)__ class works in conjunction with the `@engine` decorator and the `@run` decorator +to integrate the ExecutionEngine into your classes, providing tracing capabilities for methods. + +For more details and usage examples, refer to the source code in __[EngineTask](./EngineTask.md)__. + diff --git a/examples/usage.ts b/examples/usage.ts index 33af8c7..cbae73f 100644 --- a/examples/usage.ts +++ b/examples/usage.ts @@ -5,10 +5,10 @@ export async function run() { const engine = new ExecutionEngine(); // for sync functions: - const result1 = engine.run((param) => `result1 for ${param}`, ['param1']); + const res1 = engine.run((param) => `result1 for ${param}`, ['param1']); // for async functions: - const result2 = await engine.run(async (param) => `result2 for ${param}`, [result1.outputs]); + const res2 = await engine.run(async (param) => `result2 for ${param}`, [res1.outputs]); // Retrieve the trace const trace = engine.getTrace(); @@ -16,4 +16,4 @@ export async function run() { writeTrace(jsonString); } -run(); +run().then(); diff --git a/examples/usage2.json b/examples/usage2.json new file mode 100644 index 0000000..f52466b --- /dev/null +++ b/examples/usage2.json @@ -0,0 +1,47 @@ +[ + { + "data": { + "id": "bound myMethod1_1702207617652_13a5a286-3150-4950-b67a-8e03a797b770", + "label": "bound myMethod1", + "inputs": [ + "param1" + ], + "outputs": "result1 for param1", + "startTime": "2023-12-10T11:26:57.652Z", + "endTime": "2023-12-10T11:26:57.653Z", + "duration": 0.40799999237060547, + "elapsedTime": "0.408 ms", + "parallel": false, + "abstract": false, + "createTime": "2023-12-10T11:26:57.653Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "bound myMethod2_1702207617653_daeb25cd-1822-48d8-9744-a460bfe9f82f", + "label": "bound myMethod2", + "inputs": [ + "param2" + ], + "outputs": "result2 for param2", + "startTime": "2023-12-10T11:26:57.653Z", + "endTime": "2023-12-10T11:26:57.653Z", + "duration": 0.2049999237060547, + "elapsedTime": "0.205 ms", + "parallel": false, + "abstract": false, + "createTime": "2023-12-10T11:26:57.653Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "bound myMethod1_1702207617652_13a5a286-3150-4950-b67a-8e03a797b770->bound myMethod2_1702207617653_daeb25cd-1822-48d8-9744-a460bfe9f82f", + "source": "bound myMethod1_1702207617652_13a5a286-3150-4950-b67a-8e03a797b770", + "target": "bound myMethod2_1702207617653_daeb25cd-1822-48d8-9744-a460bfe9f82f", + "parallel": false + }, + "group": "edges" + } +] \ No newline at end of file diff --git a/examples/usage2.ts b/examples/usage2.ts new file mode 100644 index 0000000..199a504 --- /dev/null +++ b/examples/usage2.ts @@ -0,0 +1,28 @@ +import { engine, EngineTask, run } from '../src'; +import { writeTrace } from './common/writeTrace'; + +@engine({ id: 'uniqueEngineId' }) +class MyClass extends EngineTask { + @run() + myMethod1(param: string) { + return `result1 for ${param}`; + } + + @run() + async myMethod2(param: string) { + return `result2 for ${param}`; + } +} + +export async function generate() { + const myInstance = new MyClass(); + myInstance.myMethod1('param1'); + await myInstance.myMethod2('param2'); + + // Retrieve the trace + const trace = myInstance.engine.getTrace(); + const jsonString = JSON.stringify(trace, null, 2); + writeTrace(jsonString); +} + +generate().then(); diff --git a/examples/weather.json b/examples/weather.json new file mode 100644 index 0000000..07e45ba --- /dev/null +++ b/examples/weather.json @@ -0,0 +1,282 @@ +[ + { + "data": { + "id": "bound fetchCurrentTemperature_1702216345479_1b134ccc-eecc-44e1-957c-376dec9ebfb5", + "label": "bound fetchCurrentTemperature", + "inputs": [ + "Monastir" + ], + "outputs": "Current Temperature in Monastir: 25°C", + "startTime": "2023-12-10T13:52:25.479Z", + "endTime": "2023-12-10T13:52:25.480Z", + "duration": 0.5530421733856201, + "elapsedTime": "0.553 ms", + "parallel": false, + "abstract": false, + "createTime": "2023-12-10T13:52:25.480Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "bound fetchDailyForecast_1702216345480_7e1a045f-8c56-4ab7-a054-0fead082e080", + "label": "bound fetchDailyForecast", + "inputs": [ + "Monastir" + ], + "outputs": "Daily Forecast in Monastir: Sunny", + "startTime": "2023-12-10T13:52:25.480Z", + "endTime": "2023-12-10T13:52:25.480Z", + "duration": 0.1039581298828125, + "elapsedTime": "0.104 ms", + "parallel": false, + "abstract": false, + "createTime": "2023-12-10T13:52:25.480Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "bound recommendation_1702216345480_8f0b20f0-786f-4aeb-a641-78bad609dbd7", + "label": "bound recommendation", + "errors": [ + { + "name": "Error", + "message": "Next year too far!, could not decide for Monastir" + } + ], + "parallel": false, + "abstract": false, + "createTime": "2023-12-10T13:52:25.481Z", + "inputs": [ + "Monastir" + ], + "outputs": [ + "As daily Forecast in Monastir is Daily Forecast in Monastir: Sunny and the temperature is Current Temperature in Monastir: 25°C, vigilance is GREEN and you can go out", + null + ], + "startTime": "2023-12-10T13:52:25.480Z", + "endTime": "2023-12-10T13:52:25.481Z", + "duration": 0.7205419540405273, + "elapsedTime": "0.721 ms", + "updateTime": "2023-12-10T13:52:25.481Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "bound decideIfIShouldGoOutNextYear_1702216345480_fd4c9ad1-1e69-41cd-aff0-9b9082ea33d4", + "label": "3 - bound decideIfIShouldGoOutNextYear", + "parent": "bound recommendation_1702216345480_8f0b20f0-786f-4aeb-a641-78bad609dbd7", + "inputs": [ + "Monastir" + ], + "errors": [ + { + "name": "Error", + "message": "Next year too far!, could not decide for Monastir" + } + ], + "startTime": "2023-12-10T13:52:25.480Z", + "endTime": "2023-12-10T13:52:25.481Z", + "duration": 0.13033390045166016, + "elapsedTime": "0.130 ms", + "parallel": true, + "abstract": false, + "createTime": "2023-12-10T13:52:25.481Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "decideIfIShouldGoOut_custom_id", + "label": "3 - decideIfIShouldGoOut_custom_id", + "parallel": true, + "abstract": false, + "createTime": "2023-12-10T13:52:25.481Z", + "narratives": [ + "Narrative 0 GoOut", + "Narrative 1 GoOut", + "Narrative 2 GoOut" + ], + "parent": "bound recommendation_1702216345480_8f0b20f0-786f-4aeb-a641-78bad609dbd7", + "inputs": [ + "Monastir" + ], + "outputs": "As daily Forecast in Monastir is Daily Forecast in Monastir: Sunny and the temperature is Current Temperature in Monastir: 25°C, vigilance is GREEN and you can go out", + "updateTime": "2023-12-10T13:52:25.481Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "bound fetchCurrentTemperature_1702216345480_a0464478-44d7-4215-b9bb-20fbac4b377d", + "label": "bound fetchCurrentTemperature", + "parent": "decideIfIShouldGoOut_custom_id", + "inputs": [ + "Monastir" + ], + "outputs": "Current Temperature in Monastir: 25°C", + "startTime": "2023-12-10T13:52:25.480Z", + "endTime": "2023-12-10T13:52:25.481Z", + "duration": 0.24079203605651855, + "elapsedTime": "0.241 ms", + "parallel": false, + "abstract": false, + "createTime": "2023-12-10T13:52:25.481Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "bound fetchDailyForecast_1702216345481_348e3ae4-c639-48bc-915a-8ec1ab9eb7dc", + "label": "bound fetchDailyForecast", + "parent": "decideIfIShouldGoOut_custom_id", + "inputs": [ + "Monastir" + ], + "outputs": "Daily Forecast in Monastir: Sunny", + "startTime": "2023-12-10T13:52:25.481Z", + "endTime": "2023-12-10T13:52:25.481Z", + "duration": 0.03716707229614258, + "elapsedTime": "0.037 ms", + "parallel": false, + "abstract": false, + "createTime": "2023-12-10T13:52:25.481Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "function_1702216345481_5a2d51ad-e2e6-4256-9884-cc26868b91b5", + "label": "color", + "parent": "decideIfIShouldGoOut_custom_id", + "inputs": [ + "Current Temperature in Monastir: 25°C", + "Daily Forecast in Monastir: Sunny" + ], + "outputs": "GREEN", + "startTime": "2023-12-10T13:52:25.481Z", + "endTime": "2023-12-10T13:52:25.481Z", + "duration": 0.01750016212463379, + "elapsedTime": "0.018 ms", + "parallel": true, + "abstract": false, + "createTime": "2023-12-10T13:52:25.481Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "function_1702216345481_988e1087-e959-4b69-b344-159e3b317a2f", + "label": "decide", + "parent": "decideIfIShouldGoOut_custom_id", + "inputs": [ + "Current Temperature in Monastir: 25°C", + "Daily Forecast in Monastir: Sunny" + ], + "outputs": "go out", + "startTime": "2023-12-10T13:52:25.481Z", + "endTime": "2023-12-10T13:52:25.481Z", + "duration": 0.08037519454956055, + "elapsedTime": "0.080 ms", + "parallel": true, + "abstract": false, + "createTime": "2023-12-10T13:52:25.481Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "bound validateDecision_1702216345481_026692e0-b3da-4240-a119-fcdc99ce7e70", + "label": "bound validateDecision", + "inputs": [ + "As daily Forecast in Monastir is Daily Forecast in Monastir: Sunny and the temperature is Current Temperature in Monastir: 25°C, vigilance is GREEN and you can go out" + ], + "outputs": true, + "startTime": "2023-12-10T13:52:25.481Z", + "endTime": "2023-12-10T13:52:25.481Z", + "duration": 0.07454109191894531, + "elapsedTime": "0.075 ms", + "parallel": false, + "abstract": false, + "createTime": "2023-12-10T13:52:25.481Z" + }, + "group": "nodes" + }, + { + "data": { + "id": "bound fetchCurrentTemperature_1702216345479_1b134ccc-eecc-44e1-957c-376dec9ebfb5->bound fetchDailyForecast_1702216345480_7e1a045f-8c56-4ab7-a054-0fead082e080", + "source": "bound fetchCurrentTemperature_1702216345479_1b134ccc-eecc-44e1-957c-376dec9ebfb5", + "target": "bound fetchDailyForecast_1702216345480_7e1a045f-8c56-4ab7-a054-0fead082e080", + "parallel": false + }, + "group": "edges" + }, + { + "data": { + "id": "bound fetchCurrentTemperature_1702216345480_a0464478-44d7-4215-b9bb-20fbac4b377d->bound fetchDailyForecast_1702216345481_348e3ae4-c639-48bc-915a-8ec1ab9eb7dc", + "source": "bound fetchCurrentTemperature_1702216345480_a0464478-44d7-4215-b9bb-20fbac4b377d", + "target": "bound fetchDailyForecast_1702216345481_348e3ae4-c639-48bc-915a-8ec1ab9eb7dc", + "parent": "decideIfIShouldGoOut_custom_id", + "parallel": false + }, + "group": "edges" + }, + { + "data": { + "id": "bound fetchDailyForecast_1702216345481_348e3ae4-c639-48bc-915a-8ec1ab9eb7dc->function_1702216345481_5a2d51ad-e2e6-4256-9884-cc26868b91b5", + "source": "bound fetchDailyForecast_1702216345481_348e3ae4-c639-48bc-915a-8ec1ab9eb7dc", + "target": "function_1702216345481_5a2d51ad-e2e6-4256-9884-cc26868b91b5", + "parent": "decideIfIShouldGoOut_custom_id", + "parallel": true + }, + "group": "edges" + }, + { + "data": { + "id": "bound fetchDailyForecast_1702216345481_348e3ae4-c639-48bc-915a-8ec1ab9eb7dc->function_1702216345481_988e1087-e959-4b69-b344-159e3b317a2f", + "source": "bound fetchDailyForecast_1702216345481_348e3ae4-c639-48bc-915a-8ec1ab9eb7dc", + "target": "function_1702216345481_988e1087-e959-4b69-b344-159e3b317a2f", + "parent": "decideIfIShouldGoOut_custom_id", + "parallel": true + }, + "group": "edges" + }, + { + "data": { + "id": "bound fetchDailyForecast_1702216345480_7e1a045f-8c56-4ab7-a054-0fead082e080->bound recommendation_1702216345480_8f0b20f0-786f-4aeb-a641-78bad609dbd7", + "source": "bound fetchDailyForecast_1702216345480_7e1a045f-8c56-4ab7-a054-0fead082e080", + "target": "bound recommendation_1702216345480_8f0b20f0-786f-4aeb-a641-78bad609dbd7", + "parallel": false + }, + "group": "edges" + }, + { + "data": { + "id": "bound recommendation_1702216345480_8f0b20f0-786f-4aeb-a641-78bad609dbd7->bound validateDecision_1702216345481_026692e0-b3da-4240-a119-fcdc99ce7e70", + "source": "bound recommendation_1702216345480_8f0b20f0-786f-4aeb-a641-78bad609dbd7", + "target": "bound validateDecision_1702216345481_026692e0-b3da-4240-a119-fcdc99ce7e70", + "parallel": false + }, + "group": "edges" + }, + { + "data": { + "id": "bound validateDecision_1702216345481_e28a1df1-285f-4a45-b3d0-38703a730e3d", + "label": "bound validateDecision", + "inputs": [ + "As daily Forecast in Monastir is Daily Forecast in Monastir: Sunny and the temperature is Current Temperature in Monastir: 25°C, vigilance is GREEN and you can go out" + ], + "outputs": true, + "startTime": "2023-12-10T13:52:25.481Z", + "endTime": "2023-12-10T13:52:25.481Z", + "duration": 0.012207984924316406, + "elapsedTime": "0.012 ms", + "parallel": false, + "abstract": false, + "createTime": "2023-12-10T13:52:25.481Z" + }, + "group": "nodes" + } +] \ No newline at end of file diff --git a/examples/weather.ts b/examples/weather.ts new file mode 100644 index 0000000..a84177c --- /dev/null +++ b/examples/weather.ts @@ -0,0 +1,84 @@ +import { engine, EngineTask, run } from '../src'; +import { writeTrace } from './common/writeTrace'; + +@engine({ id: 'whetherEngine' }) +class MyWeatherTask extends EngineTask { + @run() + async fetchCurrentTemperature(city: string) { + return Promise.resolve(`Current Temperature in ${city}: 25°C`); + } + + @run() + async fetchDailyForecast(city: string) { + return Promise.resolve(`Daily Forecast in ${city}: Sunny`); + } + + @run() + async recommendation(city: string): Promise<[string, void]> { + const vigilanceTask = new MyVigilanceTask(); + return Promise.all([vigilanceTask.decideIfIShouldGoOut(city), vigilanceTask.decideIfIShouldGoOutNextYear(city)]); + } +} + +@engine({ id: 'whetherEngine' }) +class MyVigilanceTask extends EngineTask { + @run({ + trace: { id: 'decideIfIShouldGoOut_custom_id', narratives: ['Narrative 0 GoOut'] }, + config: { parallel: true, traceExecution: { inputs: true, outputs: true, narratives: ['Narrative 1 GoOut', 'Narrative 2 GoOut'] } } + }) + async decideIfIShouldGoOut(city: string) { + const temperature = await new MyWeatherTask().fetchCurrentTemperature(city); + const forecast = await new MyWeatherTask().fetchDailyForecast(city); + + const color = this.engine.run((t, f) => 'GREEN', [temperature, forecast], { + trace: { label: 'color' }, + config: { parallel: true, traceExecution: true } + })?.outputs; + + const decision = this.engine.run((t, f) => 'go out', [temperature, forecast], { + trace: { label: 'decide' }, + config: { parallel: true, traceExecution: true } + })?.outputs; + + return Promise.resolve( + `As daily Forecast in ${city} is ${forecast} and the temperature is ${temperature}, vigilance is ${color} and you can ${decision}` + ); + } + + @run({ config: { parallel: true, errors: 'catch', traceExecution: true } }) + async decideIfIShouldGoOutNextYear(city: string) { + throw new Error(`Next year too far!, could not decide for ${city}`); + } + + @run() + validateDecision(stringDecision: string) { + return stringDecision?.includes('GREEN'); + } +} + +@engine({ id: 'VigilanceValidationEngine' }) +class MyIndependantVigilanceTask extends MyVigilanceTask {} + +export async function generate() { + const myWeatherTaskInstance = new MyWeatherTask(); + + await myWeatherTaskInstance.fetchCurrentTemperature('Monastir'); + await myWeatherTaskInstance.fetchDailyForecast('Monastir'); + const response = await myWeatherTaskInstance.recommendation('Monastir'); + + //call validation: + const myVigilanceInstance = new MyVigilanceTask(); + myVigilanceInstance.validateDecision(response[0]); + + const myWeatherTaskInstanceTraceAfterValidation = myWeatherTaskInstance.engine.getTrace(); + + //call independent validation: as it is decorated with engineID: "VigilanceValidationEngine" + const myValidationTask = new MyIndependantVigilanceTask(); + myValidationTask.validateDecision(response[0]); + const myValidationIndependentTrace = myValidationTask.engine.getTrace(); + + const jsonString = JSON.stringify(myWeatherTaskInstanceTraceAfterValidation.concat(myValidationIndependentTrace), null, 2); + writeTrace(jsonString); +} + +generate().then(); diff --git a/package-lock.json b/package-lock.json index 7e5b72e..5166e9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "husky": "^8.0.3", "jest": "^29.7.0", + "prettier": "^3.1.1", "ts-jest": "^29.1.1", "typescript": "^5.2.2" }, @@ -6394,11 +6395,10 @@ } }, "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12481,11 +12481,10 @@ "dev": true }, "prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", - "dev": true, - "peer": true + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "dev": true }, "prettier-linter-helpers": { "version": "1.0.0", diff --git a/package.json b/package.json index b9d28e1..01bdd30 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "husky": "^8.0.3", "jest": "^29.7.0", + "prettier": "^3.1.1", "ts-jest": "^29.1.1", "typescript": "^5.2.2" }, diff --git a/src/engine/executionEngineDecorators.spec.ts b/src/engine/executionEngineDecorators.spec.ts new file mode 100644 index 0000000..13bfafd --- /dev/null +++ b/src/engine/executionEngineDecorators.spec.ts @@ -0,0 +1,135 @@ +import { engine, EngineTask, run } from './executionEngineDecorators'; + +describe('decorators', () => { + it('should apply the engine decorator to a class', () => { + const id = 'testId'; + + @engine({ id }) + class TestClass extends EngineTask {} + + const instance = new TestClass(); + expect(instance.engine).toBeDefined(); + }); + + it('should apply the run decorator to a method', async () => { + const id = 'testId'; + + @engine({ id }) + class TestClass { + @run() + async testMethod() { + return 'Test Result'; + } + } + + const instance = new TestClass(); + const result = await instance.testMethod(); + + expect(result).toBe('Test Result'); + }); + + it('should apply the run decorator with options to a method', async () => { + const id = 'testId'; + const traceOptions = { + /* your trace options here */ + }; + + @engine({ id }) + class TestClass { + @run(traceOptions) + async testMethod() { + return 'Test Result'; + } + } + + const instance = new TestClass(); + const result = await instance.testMethod(); + + expect(result).toBe('Test Result'); + }); + + describe('TraceableExecution without initialTrace', () => { + @engine({ id: 'whetherEngine' }) + class MyWeatherTask extends EngineTask { + @run() + async fetchCurrentTemperature(city: string) { + return Promise.resolve(`Current Temperature in ${city}: 25°C`); + } + + @run() + async fetchDailyForecast(city: string) { + return Promise.resolve(`Daily Forecast in ${city}: Sunny`); + } + + @run() + async recommendation(city: string): Promise<[string, void]> { + const vigilanceTask = new MyVigilanceTask(); + return Promise.all([vigilanceTask.decideIfIShouldGoOut(city), vigilanceTask.decideIfIShouldGoOutNextYear(city)]); + } + } + + @engine({ id: 'whetherEngine' }) + class MyVigilanceTask extends EngineTask { + @run({ + trace: { id: 'decideIfIShouldGoOut_custom_id', narratives: ['Narrative 0 GoOut'] }, + config: { parallel: true, traceExecution: { inputs: true, outputs: true, narratives: ['Narrative 1 GoOut', 'Narrative 2 GoOut'] } } + }) + async decideIfIShouldGoOut(city: string) { + const temperature = await new MyWeatherTask().fetchCurrentTemperature(city); + const forecast = await new MyWeatherTask().fetchDailyForecast(city); + + const color = this.engine.run(() => 'GREEN', [temperature, forecast], { + trace: { label: 'color' }, + config: { parallel: true, traceExecution: true } + })?.outputs; + + const decision = this.engine.run(() => 'go out', [temperature, forecast], { + trace: { label: 'decide' }, + config: { parallel: true, traceExecution: true } + })?.outputs; + + return Promise.resolve( + `As daily Forecast in ${city} is ${forecast} and the temperature is ${temperature}, vigilance is ${color} and you can ${decision}` + ); + } + + @run({ config: { parallel: true, errors: 'catch', traceExecution: true } }) + async decideIfIShouldGoOutNextYear(city: string) { + throw new Error(`Next year too far!, could not decide for ${city}`); + } + + @run() + validateDecision(stringDecision: string) { + return stringDecision?.includes('GREEN'); + } + } + + @engine({ id: 'VigilanceValidationEngine' }) + class MyIndependantVigilanceTask extends MyVigilanceTask {} + + it('should create a trace of consecutive user-related actions', async () => { + const myWeatherTaskInstance = new MyWeatherTask(); + + await myWeatherTaskInstance.fetchCurrentTemperature('Monastir'); + await myWeatherTaskInstance.fetchDailyForecast('Monastir'); + const response = await myWeatherTaskInstance.recommendation('Monastir'); + const myWeatherTaskInstanceTraceBeforeValidation = myWeatherTaskInstance.engine.getTrace(); + + //call validation: + const myVigilanceInstance = new MyVigilanceTask(); + myVigilanceInstance.validateDecision(response[0]); + + const myWeatherTaskInstanceTraceAfterValidation = myWeatherTaskInstance.engine.getTrace(); + const myVigilanceInstanceAfterValidation = myVigilanceInstance.engine.getTrace(); + expect(myWeatherTaskInstanceTraceBeforeValidation?.length).toEqual(14); + expect(myWeatherTaskInstanceTraceAfterValidation?.length).toEqual(16); + expect(myVigilanceInstanceAfterValidation).toEqual(myWeatherTaskInstanceTraceAfterValidation); + + //call independent validation: as it is decorated with engineID: "VigilanceValidationEngine" + const myValidationTask = new MyIndependantVigilanceTask(); + myValidationTask.validateDecision(response[0]); + const myValidationIndependentTrace = myValidationTask.engine.getTrace(); + expect(myValidationIndependentTrace?.length).toEqual(1); + }); + }); +}); diff --git a/src/engine/executionEngineDecorators.ts b/src/engine/executionEngineDecorators.ts new file mode 100644 index 0000000..d1031a1 --- /dev/null +++ b/src/engine/executionEngineDecorators.ts @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TraceOptions } from '../trace/trace.model'; +import { ExecutionEngine } from './executionEngine'; + +// eslint-disable-next-line @typescript-eslint/ban-types +function isAsync(func: Function): boolean { + return func.constructor.name === 'AsyncFunction'; +} + +const executionEngines: { + [key: string]: ExecutionEngine; +} = {}; + +/** + * Represents an abstract EngineTask with a reference to the ExecutionEngine. + * + * @abstract + */ +export abstract class EngineTask { + engine: ExecutionEngine; +} + +/** + * A class decorator that enhances a class with execution engine capabilities. + * @param options - Configuration options for the execution engine. + */ +export function engine(options: { id: string }): ClassDecorator { + /** + * The actual decorator function. + * @param target - The target class. + */ + return (target: any) => { + const originalConstructor = target; + + /** + * A new constructor function that incorporates the execution engine. + * @param args - Arguments passed to the constructor. + */ + const newConstructor: any = function (...args: any[]) { + const instance = new originalConstructor(...args); + if (!executionEngines[options.id]) { + executionEngines[options.id] = new ExecutionEngine(); + } + instance.engine = executionEngines[options.id]; + return instance; + }; + + // Optionally, you can copy prototype methods from the original constructor to the new one + Object.setPrototypeOf(newConstructor, originalConstructor.prototype); + + // Return the new constructor + return newConstructor; + }; +} + +/** + * A method decorator that enables tracing for the decorated method. + * @param options - Trace options for the execution. + * @returns A decorator function. + */ +export function run(options?: TraceOptions, O>) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Store the original method + const originalMethod = descriptor.value; + + // Modify the descriptor's value property + descriptor.value = function (...args: any[]) { + if (isAsync(originalMethod)) { + return this.engine.run(originalMethod.bind(this), args, options)?.then((r) => r.outputs); + } else { + this.engine.run(originalMethod.bind(this), args, options)?.outputs; + } + }; + + return descriptor; + }; +} diff --git a/src/index.ts b/src/index.ts index 8e909de..330829e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './engine/executionEngine'; +export * from './engine/executionEngineDecorators'; export * from './trace/trace.model'; export * from './trace/traceableExecution'; export * from './timer/executionTimer'; diff --git a/src/trace/traceableExecution.ts b/src/trace/traceableExecution.ts index 23de542..a33293f 100644 --- a/src/trace/traceableExecution.ts +++ b/src/trace/traceableExecution.ts @@ -25,8 +25,6 @@ function isAsync(func: Function): boolean { return func.constructor.name === 'AsyncFunction'; } -export type TraceableRunnerOptions = TraceOptions, unknown> | TraceOptions, unknown>['trace']; - /** * Represents a class for traceable execution of functions. */ @@ -166,8 +164,8 @@ export class TraceableExecution { inputs: Array = [], options: TraceOptions, O> | TraceOptions, O>['trace'] = { trace: { - id: [blockFunction.name ? blockFunction.name : 'function', new Date()?.getTime(), uuidv4()]?.join('_'), - label: blockFunction.name ? blockFunction.name : 'function' + id: [blockFunction.name ? blockFunction.name.replace('bound ', '') : 'function', new Date()?.getTime(), uuidv4()]?.join('_'), + label: blockFunction.name ? blockFunction.name.replace('bound ', '') : 'function' }, config: DEFAULT_TRACE_CONFIG } @@ -184,10 +182,15 @@ export class TraceableExecution { const executionTimer = new ExecutionTimer(); executionTimer?.start(); const nodeTrace: NodeData = { - id: [blockFunction.name ? blockFunction.name : 'function', executionTimer?.getStartDate()?.getTime(), uuidv4()]?.join('_'), - label: [(this.nodes?.length ?? 0) + 1, nodeTraceFromOptions?.id ?? (blockFunction.name ? blockFunction.name : 'function')]?.join( - ' - ' - ), + id: [ + blockFunction.name ? blockFunction.name.replace('bound ', '') : 'function', + executionTimer?.getStartDate()?.getTime(), + uuidv4() + ]?.join('_'), + label: [ + (this.nodes?.length ?? 0) + 1, + nodeTraceFromOptions?.id ?? (blockFunction.name ? blockFunction.name.replace('bound ', '') : 'function') + ]?.join(' - '), ...nodeTraceFromOptions }; @@ -332,7 +335,6 @@ export class TraceableExecution { parallelEdge = this.edges?.find((edge) => edge.data.parallel === options.parallel); } - this.edges?.find((edge) => edge.data.parallel && edge.data.parent === nodeTrace.parent); const previousNodes = !parallelEdge ? this.nodes?.filter( (node) => diff --git a/tsconfig.json b/tsconfig.json index 896ee92..4db8788 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "experimentalDecorators": true, "target": "es2021", "module": "commonjs", "lib": ["es2021"], diff --git a/yarn.lock b/yarn.lock index 4085103..b6b0e2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3538,6 +3538,11 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" +prettier@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848" + integrity sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw== + pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz"