nest-live-flow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nest Live Flow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # Nest Live Flow
2
+
3
+ A powerful NestJS library for real-time application architecture visualization and flow tracing with Total.js Flow integration. Monitor your application's dependency graph, trace execution flows, and visualize your architecture in real-time.
4
+
5
+ ## Features
6
+
7
+ - 🔍 **Automatic Architecture Discovery**: Automatically scans and maps your NestJS application's modules, controllers, and services
8
+ - 📊 **Real-time Flow Tracing**: Traces method calls and data flow through your application using OpenTelemetry
9
+ - 🎨 **Visual Architecture Dashboard**: Integrates with Total.js Flow for beautiful architecture visualization
10
+ - 🚀 **Zero Configuration**: Works out of the box with minimal setup
11
+ - 🔧 **Flexible Integration**: Supports both decorator-based and programmatic tracing
12
+ - 📈 **Performance Monitoring**: Track execution times and identify bottlenecks
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install nest-live-flow
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### 1. Initialize in main.ts
23
+
24
+ ```typescript
25
+ import { NestFactory } from '@nestjs/core';
26
+ import { initFlow } from 'nest-live-flow';
27
+ import { AppModule } from './app.module';
28
+
29
+ async function bootstrap() {
30
+ // Initialize flow tracing before creating the app
31
+ initFlow('http://localhost:8000'); // Your Total.js Flow URL
32
+
33
+ const app = await NestFactory.create(AppModule);
34
+ await app.listen(3000);
35
+ }
36
+ bootstrap();
37
+ ```
38
+
39
+ ### 2. Import the Module
40
+
41
+ ```typescript
42
+ import { Module } from '@nestjs/common';
43
+ import { NestLiveFlowModule } from 'nest-live-flow';
44
+
45
+ @Module({
46
+ imports: [
47
+ NestLiveFlowModule, // Add this to your root module
48
+ // ... your other modules
49
+ ],
50
+ })
51
+ export class AppModule {}
52
+ ```
53
+
54
+ ### 3. Use Flow Tracing (Optional)
55
+
56
+ #### Decorator-based Tracing
57
+
58
+ ```typescript
59
+ import { Injectable } from '@nestjs/common';
60
+ import { FlowTrace } from 'nest-live-flow';
61
+
62
+ @Injectable()
63
+ export class UserService {
64
+ @FlowTrace('Get User by ID')
65
+ async getUserById(id: string) {
66
+ // Your service logic
67
+ return { id, name: 'John Doe' };
68
+ }
69
+
70
+ @FlowTrace('Create New User')
71
+ async createUser(userData: any) {
72
+ // Your service logic
73
+ return { id: '123', ...userData };
74
+ }
75
+ }
76
+ ```
77
+
78
+ #### Programmatic Tracing
79
+
80
+ ```typescript
81
+ import { Injectable } from '@nestjs/common';
82
+ import { flowTrace } from 'nest-live-flow';
83
+
84
+ @Injectable()
85
+ export class ProductService {
86
+ async getProducts() {
87
+ return flowTrace('Fetch All Products', async () => {
88
+ // Your service logic
89
+ return await this.database.findAll();
90
+ });
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## Configuration
96
+
97
+ ### Environment Variables
98
+
99
+ Create a `.env` file in your project root:
100
+
101
+ ```env
102
+ TOTALJS_FLOW_URL=http://localhost:8000
103
+ ```
104
+
105
+ ### Advanced Configuration
106
+
107
+ ```typescript
108
+ import { initFlow } from 'nest-live-flow';
109
+
110
+ // Initialize with custom options
111
+ initFlow('http://localhost:8000', {
112
+ serviceName: 'my-nestjs-app',
113
+ enableConsoleLogging: true,
114
+ batchTimeout: 2000,
115
+ });
116
+ ```
117
+
118
+ ## Architecture Visualization
119
+
120
+ Once configured, your application architecture will be automatically:
121
+
122
+ 1. **Scanned**: All modules, controllers, and services are discovered
123
+ 2. **Mapped**: Dependencies and relationships are identified
124
+ 3. **Visualized**: Real-time architecture graph in Total.js Flow
125
+ 4. **Traced**: Method calls and data flow are tracked and displayed
126
+
127
+ ## API Reference
128
+
129
+ ### Core Functions
130
+
131
+ #### `initFlow(totalJsUrl: string)`
132
+ Initializes the flow tracing system. Must be called before `NestFactory.create()`.
133
+
134
+ #### `flowTrace<T>(name: string, fn: () => T | Promise<T>): Promise<T>`
135
+ Programmatically traces a function execution.
136
+
137
+ ### Decorators
138
+
139
+ #### `@FlowTrace(name?: string)`
140
+ Decorator for automatic method tracing.
141
+
142
+ ### Modules
143
+
144
+ #### `NestLiveFlowModule`
145
+ The main module to import in your application.
146
+
147
+ ### Services
148
+
149
+ #### `FlowScannerService`
150
+ Service that handles architecture discovery and scanning.
151
+
152
+ #### `ServiceInstrumentor`
153
+ Service for manual instrumentation of existing services.
154
+
155
+ ## Integration with Total.js Flow
156
+
157
+ This library is designed to work with [Total.js Flow](https://www.totaljs.com/flow/).
158
+
159
+ ### Setting up Total.js Flow
160
+
161
+ 1. Install Total.js Flow:
162
+ ```bash
163
+ npm install -g @totaljs/flow
164
+ ```
165
+
166
+ 2. Start Total.js Flow:
167
+ ```bash
168
+ flow
169
+ ```
170
+
171
+ 3. Access the dashboard at `http://localhost:8000`
172
+
173
+ ## Examples
174
+
175
+ ### Basic NestJS Application
176
+
177
+ ```typescript
178
+ // main.ts
179
+ import { NestFactory } from '@nestjs/core';
180
+ import { initFlow } from 'nest-live-flow';
181
+ import { AppModule } from './app.module';
182
+
183
+ async function bootstrap() {
184
+ initFlow('http://localhost:8000');
185
+ const app = await NestFactory.create(AppModule);
186
+ await app.listen(3000);
187
+ }
188
+ bootstrap();
189
+
190
+ // app.module.ts
191
+ import { Module } from '@nestjs/common';
192
+ import { NestLiveFlowModule } from 'nest-live-flow';
193
+ import { UsersModule } from './users/users.module';
194
+
195
+ @Module({
196
+ imports: [
197
+ NestLiveFlowModule,
198
+ UsersModule,
199
+ ],
200
+ })
201
+ export class AppModule {}
202
+
203
+ // users/users.service.ts
204
+ import { Injectable } from '@nestjs/common';
205
+ import { FlowTrace } from 'nest-live-flow';
206
+
207
+ @Injectable()
208
+ export class UsersService {
209
+ @FlowTrace()
210
+ findAll() {
211
+ return [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
212
+ }
213
+
214
+ @FlowTrace('Create User Operation')
215
+ create(user: any) {
216
+ return { id: Date.now(), ...user };
217
+ }
218
+ }
219
+ ```
220
+
221
+ ## Requirements
222
+
223
+ - Node.js 18+
224
+ - NestJS 10+ or 11+
225
+ - Total.js Flow (for visualization)
226
+
227
+ ## License
228
+
229
+ MIT
230
+
231
+ ## Contributing
232
+
233
+ Contributions are welcome! Please feel free to submit a Pull Request.
234
+
235
+ ## Support
236
+
237
+ If you encounter any issues or have questions, please file an issue on our GitHub repository.
package/dist/core.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Initializes the tracing engine. Call this in main.ts before NestFactory.
3
+ * @param totalJsUrl - The URL of the Total.js backend
4
+ * @throws Error if initialization fails
5
+ */
6
+ export declare function initFlow(totalJsUrl: string): void;
7
+ /**
8
+ * Wraps any function to trace its execution and data flow.
9
+ * @param name - The display name for the trace span
10
+ * @param fn - The function to trace (can be sync or async)
11
+ * @returns Promise resolving to the function result
12
+ * @throws Error if tracing is not initialized
13
+ */
14
+ export declare function flowTrace<T>(name: string, fn: () => T | Promise<T>): Promise<T>;
package/dist/core.js ADDED
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initFlow = initFlow;
4
+ exports.flowTrace = flowTrace;
5
+ const api_1 = require("@opentelemetry/api");
6
+ const resources_1 = require("@opentelemetry/resources");
7
+ const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
8
+ const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
9
+ const exporter_1 = require("./exporter");
10
+ let tracer = null;
11
+ /**
12
+ * Initializes the tracing engine. Call this in main.ts before NestFactory.
13
+ * @param totalJsUrl - The URL of the Total.js backend
14
+ * @throws Error if initialization fails
15
+ */
16
+ function initFlow(totalJsUrl) {
17
+ if (!totalJsUrl) {
18
+ throw new Error('Total.js URL is required for flow initialization');
19
+ }
20
+ try {
21
+ const provider = new sdk_trace_node_1.NodeTracerProvider({
22
+ resource: (0, resources_1.resourceFromAttributes)({
23
+ [semantic_conventions_1.ATTR_SERVICE_NAME]: 'nestjs-app',
24
+ }),
25
+ spanProcessors: [
26
+ new sdk_trace_node_1.SimpleSpanProcessor(new exporter_1.TotalJsExporter(totalJsUrl)),
27
+ ],
28
+ });
29
+ provider.register();
30
+ tracer = api_1.trace.getTracer('nest-live-flow');
31
+ console.log('✨ Nest-Live-Flow: OpenTelemetry Engine Initialized');
32
+ }
33
+ catch (error) {
34
+ console.error('❌ Failed to initialize Flow tracing:', error);
35
+ throw error;
36
+ }
37
+ }
38
+ /**
39
+ * Wraps any function to trace its execution and data flow.
40
+ * @param name - The display name for the trace span
41
+ * @param fn - The function to trace (can be sync or async)
42
+ * @returns Promise resolving to the function result
43
+ * @throws Error if tracing is not initialized
44
+ */
45
+ async function flowTrace(name, fn) {
46
+ console.log(`🔍 flowTrace called for: ${name}`);
47
+ if (!tracer) {
48
+ console.error('❌ Flow tracing not initialized. Call initFlow() first.');
49
+ throw new Error('Flow tracing not initialized. Call initFlow() first.');
50
+ }
51
+ if (!name?.trim()) {
52
+ console.error('❌ Trace name cannot be empty');
53
+ throw new Error('Trace name cannot be empty');
54
+ }
55
+ console.log(`🟢 Starting active span for: ${name}`);
56
+ return tracer.startActiveSpan(name, async (span) => {
57
+ console.log(`📍 Inside span for: ${name}`);
58
+ const startTime = Date.now();
59
+ try {
60
+ console.log(`⚙️ Executing function for: ${name}`);
61
+ const result = await fn();
62
+ console.log(`✅ Function completed for: ${name}`);
63
+ // Safely serialize result for data flow visualization
64
+ try {
65
+ const serializedResult = typeof result === 'object' && result !== null
66
+ ? JSON.stringify(result)
67
+ : String(result);
68
+ // Limit attribute size to prevent memory issues
69
+ const maxLength = 1000;
70
+ const truncatedResult = serializedResult.length > maxLength
71
+ ? `${serializedResult.substring(0, maxLength)}...`
72
+ : serializedResult;
73
+ span.setAttribute('flow.data', truncatedResult);
74
+ console.log(`📊 Set span data for: ${name}`);
75
+ }
76
+ catch {
77
+ span.setAttribute('flow.data', '[Unable to serialize result]');
78
+ console.log(`⚠️ Unable to serialize result for: ${name}`);
79
+ }
80
+ span.setAttribute('flow.duration', Date.now() - startTime);
81
+ span.setStatus({ code: api_1.SpanStatusCode.OK });
82
+ console.log(`🏆 Span completed successfully for: ${name}`);
83
+ return result;
84
+ }
85
+ catch (error) {
86
+ console.error(`💥 Error in span for ${name}:`, error);
87
+ span.setStatus({
88
+ code: api_1.SpanStatusCode.ERROR,
89
+ message: error instanceof Error ? error.message : 'Unknown error',
90
+ });
91
+ span.setAttribute('flow.error', error instanceof Error ? error.message : String(error));
92
+ throw error;
93
+ }
94
+ finally {
95
+ console.log(`🔚 Ending span for: ${name}`);
96
+ span.end();
97
+ }
98
+ });
99
+ }
@@ -0,0 +1,56 @@
1
+ import { OnApplicationBootstrap } from '@nestjs/common';
2
+ import { ModulesContainer } from '@nestjs/core';
3
+ import 'reflect-metadata';
4
+ export declare class FlowScannerService implements OnApplicationBootstrap {
5
+ private readonly modulesContainer;
6
+ constructor(modulesContainer: ModulesContainer);
7
+ onApplicationBootstrap(): Promise<void>;
8
+ /**
9
+ * Safely extracts the module name from a module reference
10
+ */
11
+ private getModuleName;
12
+ /**
13
+ * Safely extracts the wrapper name from a controller/provider wrapper
14
+ */
15
+ private getWrapperName;
16
+ /**
17
+ * Generic name extraction with type safety
18
+ */
19
+ private extractName;
20
+ /**
21
+ * Checks if an object has a valid collection (controllers, providers, etc.)
22
+ */
23
+ private hasValidCollection;
24
+ /**
25
+ * Scans all modules and builds the dependency graph
26
+ */
27
+ private scanAllModules;
28
+ /**
29
+ * Processes a single module and its components
30
+ */
31
+ private processModule;
32
+ /**
33
+ * Processes controllers within a module
34
+ */
35
+ private processControllers;
36
+ /**
37
+ * Processes providers/services within a module
38
+ */
39
+ private processProviders;
40
+ /**
41
+ * Processes module imports
42
+ */
43
+ private processImports;
44
+ /**
45
+ * Adds edges between controllers and their injected services
46
+ */
47
+ private addServiceDependencyEdges;
48
+ /**
49
+ * Extracts constructor dependencies using reflection
50
+ */
51
+ private extractConstructorDependencies;
52
+ /**
53
+ * Syncs the discovered architecture to Total.js Flow
54
+ */
55
+ private syncToTotalJs;
56
+ }
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.FlowScannerService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const core_1 = require("@nestjs/core");
18
+ const axios_1 = __importDefault(require("axios"));
19
+ require("reflect-metadata");
20
+ let FlowScannerService = class FlowScannerService {
21
+ constructor(modulesContainer) {
22
+ this.modulesContainer = modulesContainer;
23
+ }
24
+ async onApplicationBootstrap() {
25
+ try {
26
+ const nodeMap = new Map();
27
+ const connections = [];
28
+ this.scanAllModules(nodeMap, connections);
29
+ this.addServiceDependencyEdges(nodeMap, connections);
30
+ const nodes = Array.from(nodeMap.values());
31
+ await this.syncToTotalJs({ nodes, connections });
32
+ console.log(`✅ Discovered ${nodes.length} components and ${connections.length} connections`);
33
+ }
34
+ catch (error) {
35
+ console.error('❌ Failed to scan modules:', error);
36
+ throw error;
37
+ }
38
+ }
39
+ /**
40
+ * Safely extracts the module name from a module reference
41
+ */
42
+ getModuleName(modRef) {
43
+ return this.extractName(modRef);
44
+ }
45
+ /**
46
+ * Safely extracts the wrapper name from a controller/provider wrapper
47
+ */
48
+ getWrapperName(wrapper) {
49
+ return this.extractName(wrapper);
50
+ }
51
+ /**
52
+ * Generic name extraction with type safety
53
+ */
54
+ extractName(obj) {
55
+ return obj?.metatype?.name && typeof obj.metatype.name === 'string'
56
+ ? obj.metatype.name
57
+ : null;
58
+ }
59
+ /**
60
+ * Checks if an object has a valid collection (controllers, providers, etc.)
61
+ */
62
+ hasValidCollection(obj, property) {
63
+ const moduleRef = obj;
64
+ const collection = moduleRef?.[property];
65
+ // Check if it's a Map-like collection with values method
66
+ return (collection != null &&
67
+ typeof collection === 'object' &&
68
+ 'values' in collection &&
69
+ typeof collection.values === 'function');
70
+ }
71
+ /**
72
+ * Scans all modules and builds the dependency graph
73
+ */
74
+ scanAllModules(nodeMap, connections) {
75
+ for (const modRef of this.modulesContainer.values()) {
76
+ this.processModule(modRef, nodeMap, connections);
77
+ }
78
+ }
79
+ /**
80
+ * Processes a single module and its components
81
+ */
82
+ processModule(modRef, nodeMap, connections) {
83
+ const moduleName = this.getModuleName(modRef);
84
+ if (!moduleName)
85
+ return;
86
+ // Add module node
87
+ nodeMap.set(moduleName, {
88
+ id: moduleName,
89
+ name: moduleName,
90
+ type: 'module',
91
+ });
92
+ // Process controllers
93
+ this.processControllers(modRef, moduleName, nodeMap, connections);
94
+ // Process providers/services
95
+ this.processProviders(modRef, moduleName, nodeMap, connections);
96
+ // Process module imports
97
+ this.processImports(modRef, moduleName, nodeMap, connections);
98
+ }
99
+ /**
100
+ * Processes controllers within a module
101
+ */
102
+ processControllers(modRef, moduleName, nodeMap, connections) {
103
+ if (!this.hasValidCollection(modRef, 'controllers'))
104
+ return;
105
+ const controllersMap = modRef.controllers;
106
+ for (const ctrlWrapper of controllersMap.values()) {
107
+ const ctrlName = this.getWrapperName(ctrlWrapper);
108
+ if (!ctrlName)
109
+ continue;
110
+ nodeMap.set(ctrlName, {
111
+ id: ctrlName,
112
+ name: ctrlName,
113
+ type: 'controller',
114
+ });
115
+ connections.push({ from: moduleName, to: ctrlName });
116
+ }
117
+ }
118
+ /**
119
+ * Processes providers/services within a module
120
+ */
121
+ processProviders(modRef, moduleName, nodeMap, connections) {
122
+ if (!this.hasValidCollection(modRef, 'providers'))
123
+ return;
124
+ const providersMap = modRef.providers;
125
+ for (const provWrapper of providersMap.values()) {
126
+ const provName = this.getWrapperName(provWrapper);
127
+ if (!provName || provName === moduleName)
128
+ continue;
129
+ nodeMap.set(provName, {
130
+ id: provName,
131
+ name: provName,
132
+ type: 'service',
133
+ });
134
+ connections.push({ from: moduleName, to: provName });
135
+ }
136
+ }
137
+ /**
138
+ * Processes module imports
139
+ */
140
+ processImports(modRef, moduleName, nodeMap, connections) {
141
+ if (!modRef.imports || !modRef.imports.forEach)
142
+ return;
143
+ modRef.imports.forEach((imported) => {
144
+ const importedName = this.getModuleName(imported);
145
+ if (importedName && importedName !== moduleName) {
146
+ nodeMap.set(importedName, {
147
+ id: importedName,
148
+ name: importedName,
149
+ type: 'module',
150
+ });
151
+ connections.push({ from: moduleName, to: importedName });
152
+ }
153
+ });
154
+ }
155
+ /**
156
+ * Adds edges between controllers and their injected services
157
+ */
158
+ addServiceDependencyEdges(nodeMap, connections) {
159
+ for (const modRef of this.modulesContainer.values()) {
160
+ if (!this.hasValidCollection(modRef, 'controllers'))
161
+ continue;
162
+ const moduleRef = modRef;
163
+ for (const ctrlWrapper of moduleRef.controllers.values()) {
164
+ const ctrlName = this.getWrapperName(ctrlWrapper);
165
+ if (!ctrlName || !ctrlWrapper.instance)
166
+ continue;
167
+ this.extractConstructorDependencies(ctrlWrapper, ctrlName, nodeMap, connections);
168
+ }
169
+ }
170
+ }
171
+ /**
172
+ * Extracts constructor dependencies using reflection
173
+ */
174
+ extractConstructorDependencies(wrapper, ctrlName, nodeMap, connections) {
175
+ try {
176
+ const instance = wrapper.instance;
177
+ const proto = Object.getPrototypeOf(instance);
178
+ const ctor = proto?.constructor;
179
+ if (!ctor)
180
+ return;
181
+ const paramTypes = Reflect.getMetadata('design:paramtypes', ctor) || [];
182
+ for (const param of paramTypes) {
183
+ if (typeof param === 'function' &&
184
+ param['name'] &&
185
+ nodeMap.has(param['name'])) {
186
+ connections.push({ from: ctrlName, to: param['name'] });
187
+ }
188
+ }
189
+ }
190
+ catch (error) {
191
+ console.warn(`Failed to extract dependencies for ${ctrlName}:`, error);
192
+ }
193
+ }
194
+ /**
195
+ * Syncs the discovered architecture to Total.js Flow
196
+ */
197
+ async syncToTotalJs(data) {
198
+ const TOTAL_JS_URL = 'http://localhost:8000';
199
+ try {
200
+ const response = await axios_1.default.post(`${TOTAL_JS_URL}/api/flow/design`, data, {
201
+ timeout: 5000,
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ },
205
+ });
206
+ if (response.status === 200) {
207
+ console.log('✅ Total.js Flow Design Synced Successfully');
208
+ }
209
+ else {
210
+ console.warn(`⚠️ Unexpected response status: ${response.status}`);
211
+ }
212
+ }
213
+ catch (error) {
214
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
215
+ if (axios_1.default.isAxiosError(error)) {
216
+ if (error.code === 'ECONNREFUSED') {
217
+ console.error('❌ Total.js Flow server is not running. Please start it on port 8000.');
218
+ }
219
+ else {
220
+ console.error(`❌ Sync Failed: ${error.response?.status || 'Network error'} - ${errorMessage}`);
221
+ }
222
+ }
223
+ else {
224
+ console.error(`❌ Sync Failed: ${errorMessage}`);
225
+ }
226
+ // Don't rethrow - this shouldn't break the application startup
227
+ }
228
+ }
229
+ };
230
+ exports.FlowScannerService = FlowScannerService;
231
+ exports.FlowScannerService = FlowScannerService = __decorate([
232
+ (0, common_1.Injectable)(),
233
+ __metadata("design:paramtypes", [core_1.ModulesContainer])
234
+ ], FlowScannerService);
@@ -0,0 +1,8 @@
1
+ import { ExportResult } from '@opentelemetry/core';
2
+ import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
3
+ export declare class TotalJsExporter implements SpanExporter {
4
+ private readonly url;
5
+ constructor(url: string);
6
+ export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
7
+ shutdown(): Promise<void>;
8
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TotalJsExporter = void 0;
7
+ const core_1 = require("@opentelemetry/core");
8
+ const axios_1 = __importDefault(require("axios"));
9
+ class TotalJsExporter {
10
+ constructor(url) {
11
+ this.url = url;
12
+ }
13
+ export(spans, resultCallback) {
14
+ console.log(`📦 TotalJsExporter: Exporting ${spans.length} spans`);
15
+ for (const span of spans) {
16
+ const data = span.attributes?.['flow.data'] || null;
17
+ const duration = span.duration[1] / 1000000;
18
+ console.log(`📡 Exporting span:`, {
19
+ name: span.name,
20
+ duration: duration,
21
+ hasData: !!data,
22
+ url: `${this.url}/api/pulse`,
23
+ });
24
+ const pulseData = {
25
+ id: span.name, // Matches the Controller/Service name
26
+ status: 'pulse',
27
+ duration: duration,
28
+ data,
29
+ };
30
+ console.log(`🚀 Sending pulse data:`, pulseData);
31
+ axios_1.default
32
+ .post(`${this.url}/api/pulse`, pulseData)
33
+ .then((response) => {
34
+ console.log(`✅ Pulse sent successfully for ${span.name}:`, response.status);
35
+ })
36
+ .catch((err) => {
37
+ if (err instanceof Error) {
38
+ console.error(`❌ Pulse send error for ${span.name}:`, err.message);
39
+ }
40
+ else {
41
+ console.error(`❌ Pulse send error for ${span.name}:`, err);
42
+ }
43
+ });
44
+ }
45
+ console.log(`📋 Export completed, calling result callback`);
46
+ resultCallback({ code: core_1.ExportResultCode.SUCCESS });
47
+ }
48
+ shutdown() {
49
+ return Promise.resolve();
50
+ }
51
+ }
52
+ exports.TotalJsExporter = TotalJsExporter;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Decorator to automatically trace method execution and send pulse data to Total.js Flow
3
+ * @param traceName Optional custom trace name, defaults to ClassName.methodName
4
+ */
5
+ export declare function FlowTrace(traceName?: string): (target: any, propertyName: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FlowTrace = FlowTrace;
4
+ const index_1 = require("./index");
5
+ /**
6
+ * Decorator to automatically trace method execution and send pulse data to Total.js Flow
7
+ * @param traceName Optional custom trace name, defaults to ClassName.methodName
8
+ */
9
+ function FlowTrace(traceName) {
10
+ return function (target, propertyName, descriptor) {
11
+ const method = descriptor.value;
12
+ const className = target?.constructor?.name;
13
+ const defaultTraceName = `${className}`;
14
+ const finalTraceName = traceName || defaultTraceName;
15
+ descriptor.value = async function (...args) {
16
+ return (0, index_1.flowTrace)(finalTraceName, async () => {
17
+ return await method.apply(this, args);
18
+ });
19
+ };
20
+ return descriptor;
21
+ };
22
+ }
@@ -0,0 +1,12 @@
1
+ import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
2
+ import { Observable } from 'rxjs';
3
+ /**
4
+ * Global interceptor that automatically traces all controller method executions
5
+ */
6
+ export declare class FlowInterceptor implements NestInterceptor {
7
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
8
+ /**
9
+ * Sends flow pulse without blocking the main request
10
+ */
11
+ private sendFlowPulse;
12
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.FlowInterceptor = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const operators_1 = require("rxjs/operators");
12
+ const index_1 = require("./index");
13
+ /**
14
+ * Global interceptor that automatically traces all controller method executions
15
+ */
16
+ let FlowInterceptor = class FlowInterceptor {
17
+ intercept(context, next) {
18
+ const className = context.getClass().name;
19
+ const methodName = context.getHandler().name;
20
+ const displayName = `${className}.${methodName}`;
21
+ console.log(`🎯 INTERCEPTOR TRIGGERED: ${displayName}`);
22
+ // Execute the handler and capture the result for flow tracing
23
+ return next.handle().pipe((0, operators_1.tap)({
24
+ next: (result) => {
25
+ console.log(`✅ ${displayName} completed, sending pulse...`);
26
+ // Trigger flow trace with the actual result (non-blocking)
27
+ this.sendFlowPulse(displayName, result);
28
+ },
29
+ error: (error) => {
30
+ const errorMessage = typeof error === 'object' && error !== null && 'message' in error
31
+ ? error.message
32
+ : String(error);
33
+ console.error(`❌ ${displayName} failed:`, errorMessage);
34
+ // Send pulse for errors too
35
+ this.sendFlowPulse(displayName, { error: errorMessage });
36
+ },
37
+ complete: () => {
38
+ console.log(`🏁 ${displayName} stream completed`);
39
+ },
40
+ }));
41
+ }
42
+ /**
43
+ * Sends flow pulse without blocking the main request
44
+ */
45
+ sendFlowPulse(displayName, result) {
46
+ console.log(`📡 Sending flow pulse for ${displayName}...`);
47
+ // Use flowTrace to send the pulse
48
+ (0, index_1.flowTrace)(displayName, () => {
49
+ console.log(`🔄 Executing flowTrace for ${displayName}`);
50
+ return result;
51
+ })
52
+ .then(() => {
53
+ console.log(`📊 Flow pulse sent successfully for ${displayName}`);
54
+ })
55
+ .catch((error) => {
56
+ console.error(`⚠️ Flow pulse failed for ${displayName}:`, typeof error === 'object' && error !== null && 'message' in error
57
+ ? error.message
58
+ : error);
59
+ });
60
+ }
61
+ };
62
+ exports.FlowInterceptor = FlowInterceptor;
63
+ exports.FlowInterceptor = FlowInterceptor = __decorate([
64
+ (0, common_1.Injectable)()
65
+ ], FlowInterceptor);
@@ -0,0 +1,2 @@
1
+ export declare class NestLiveFlowModule {
2
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.NestLiveFlowModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const core_1 = require("@nestjs/core");
12
+ const discovery_service_1 = require("./discovery.service");
13
+ const flow_interceptor_1 = require("./flow.interceptor");
14
+ let NestLiveFlowModule = class NestLiveFlowModule {
15
+ };
16
+ exports.NestLiveFlowModule = NestLiveFlowModule;
17
+ exports.NestLiveFlowModule = NestLiveFlowModule = __decorate([
18
+ (0, common_1.Global)(),
19
+ (0, common_1.Module)({
20
+ imports: [core_1.DiscoveryModule],
21
+ providers: [
22
+ discovery_service_1.FlowScannerService,
23
+ {
24
+ provide: core_1.APP_INTERCEPTOR,
25
+ useClass: flow_interceptor_1.FlowInterceptor,
26
+ },
27
+ ],
28
+ exports: [discovery_service_1.FlowScannerService],
29
+ })
30
+ ], NestLiveFlowModule);
@@ -0,0 +1,7 @@
1
+ export { initFlow, flowTrace } from './core';
2
+ export { FlowTrace } from './flow.decorator';
3
+ export { FlowInterceptor } from './flow.interceptor';
4
+ export { NestLiveFlowModule } from './flow.module';
5
+ export { FlowScannerService } from './discovery.service';
6
+ export { ServiceInstrumentor } from './service-instrumentor';
7
+ export { TotalJsExporter } from './exporter';
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TotalJsExporter = exports.ServiceInstrumentor = exports.FlowScannerService = exports.NestLiveFlowModule = exports.FlowInterceptor = exports.FlowTrace = exports.flowTrace = exports.initFlow = void 0;
4
+ // Core initialization functions
5
+ var core_1 = require("./core");
6
+ Object.defineProperty(exports, "initFlow", { enumerable: true, get: function () { return core_1.initFlow; } });
7
+ Object.defineProperty(exports, "flowTrace", { enumerable: true, get: function () { return core_1.flowTrace; } });
8
+ // Decorators and Interceptors
9
+ var flow_decorator_1 = require("./flow.decorator");
10
+ Object.defineProperty(exports, "FlowTrace", { enumerable: true, get: function () { return flow_decorator_1.FlowTrace; } });
11
+ var flow_interceptor_1 = require("./flow.interceptor");
12
+ Object.defineProperty(exports, "FlowInterceptor", { enumerable: true, get: function () { return flow_interceptor_1.FlowInterceptor; } });
13
+ // Module and Services
14
+ var flow_module_1 = require("./flow.module");
15
+ Object.defineProperty(exports, "NestLiveFlowModule", { enumerable: true, get: function () { return flow_module_1.NestLiveFlowModule; } });
16
+ var discovery_service_1 = require("./discovery.service");
17
+ Object.defineProperty(exports, "FlowScannerService", { enumerable: true, get: function () { return discovery_service_1.FlowScannerService; } });
18
+ // Service Instrumentor for manual instrumentation
19
+ var service_instrumentor_1 = require("./service-instrumentor");
20
+ Object.defineProperty(exports, "ServiceInstrumentor", { enumerable: true, get: function () { return service_instrumentor_1.ServiceInstrumentor; } });
21
+ // Exporter for custom integrations
22
+ var exporter_1 = require("./exporter");
23
+ Object.defineProperty(exports, "TotalJsExporter", { enumerable: true, get: function () { return exporter_1.TotalJsExporter; } });
@@ -0,0 +1,11 @@
1
+ import { OnApplicationBootstrap } from '@nestjs/common';
2
+ import { ModulesContainer } from '@nestjs/core';
3
+ export declare class ServiceInstrumentor implements OnApplicationBootstrap {
4
+ private readonly modulesContainer;
5
+ constructor(modulesContainer: ModulesContainer);
6
+ onApplicationBootstrap(): Promise<void>;
7
+ private instrumentServices;
8
+ private instrumentInstance;
9
+ private wrapMethod;
10
+ private shouldSkip;
11
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ServiceInstrumentor = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const core_1 = require("@nestjs/core");
15
+ const index_1 = require("./index");
16
+ let ServiceInstrumentor = class ServiceInstrumentor {
17
+ constructor(modulesContainer) {
18
+ this.modulesContainer = modulesContainer;
19
+ }
20
+ async onApplicationBootstrap() {
21
+ // Temporarily disable ServiceInstrumentor to avoid recursive instrumentation issues
22
+ // TODO: Implement a better approach that doesn't instrument itself
23
+ console.log('⚠️ ServiceInstrumentor disabled to prevent recursive instrumentation');
24
+ }
25
+ instrumentServices() {
26
+ for (const module of this.modulesContainer.values()) {
27
+ // Instrument providers (services)
28
+ for (const provider of module.providers.values()) {
29
+ if (provider.instance && provider.metatype) {
30
+ this.instrumentInstance(provider.instance, provider.metatype.name);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ instrumentInstance(instance, className) {
36
+ if (!instance || typeof instance !== 'object')
37
+ return;
38
+ // Skip if already instrumented
39
+ if (instance.__flowInstrumented)
40
+ return;
41
+ // Skip internal NestJS classes and our own flow classes
42
+ if (this.shouldSkip(className))
43
+ return;
44
+ const prototype = Object.getPrototypeOf(instance);
45
+ const methodNames = Object.getOwnPropertyNames(prototype).filter((name) => {
46
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
47
+ return (name !== 'constructor' &&
48
+ typeof descriptor?.value === 'function' &&
49
+ !name.startsWith('_') &&
50
+ !name.includes('Symbol'));
51
+ });
52
+ methodNames.forEach((methodName) => {
53
+ const originalMethod = instance[methodName];
54
+ if (typeof originalMethod === 'function') {
55
+ instance[methodName] = this.wrapMethod(originalMethod, className, methodName);
56
+ }
57
+ });
58
+ // Mark as instrumented
59
+ instance.__flowInstrumented = true;
60
+ }
61
+ wrapMethod(originalMethod, className, methodName) {
62
+ return async function (...args) {
63
+ const traceName = `${className}.${methodName}`;
64
+ return (0, index_1.flowTrace)(traceName, async () => {
65
+ const result = originalMethod.apply(this, args);
66
+ // Handle both sync and async methods
67
+ return result instanceof Promise ? await result : result;
68
+ });
69
+ };
70
+ }
71
+ shouldSkip(className) {
72
+ const skipPrefixes = [
73
+ 'Flow',
74
+ 'Discovery',
75
+ 'Metadata',
76
+ 'Module',
77
+ 'Reflector',
78
+ 'Application',
79
+ 'Internal',
80
+ 'Logger',
81
+ 'Config',
82
+ ];
83
+ return (skipPrefixes.some((prefix) => className.startsWith(prefix)) ||
84
+ className.includes('Wrapper') ||
85
+ className.includes('Container') ||
86
+ className.includes('Factory'));
87
+ }
88
+ };
89
+ exports.ServiceInstrumentor = ServiceInstrumentor;
90
+ exports.ServiceInstrumentor = ServiceInstrumentor = __decorate([
91
+ (0, common_1.Injectable)(),
92
+ __metadata("design:paramtypes", [core_1.ModulesContainer])
93
+ ], ServiceInstrumentor);
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "nest-live-flow",
3
+ "version": "1.0.0",
4
+ "description": "A NestJS library for live application architecture visualization and flow tracing with Total.js Flow integration",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/**/*",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepare": "npm run build",
15
+ "test": "jest",
16
+ "lint": "eslint \"src/**/*.ts\" --fix",
17
+ "format": "prettier --write \"src/**/*.ts\""
18
+ },
19
+ "keywords": [
20
+ "nestjs",
21
+ "tracing",
22
+ "architecture",
23
+ "visualization",
24
+ "totaljs",
25
+ "flow",
26
+ "opentelemetry",
27
+ "dependency-injection",
28
+ "live-monitoring"
29
+ ],
30
+ "author": "Michael Musni <mike.musni@me.com>",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/michaelmusni/nest-live-flow.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/michaelmusni/nest-live-flow/issues"
38
+ },
39
+ "homepage": "https://github.com/michaelmusni/nest-live-flow#readme",
40
+ "peerDependencies": {
41
+ "@nestjs/common": "^10.0.0 || ^11.0.0",
42
+ "@nestjs/core": "^10.0.0 || ^11.0.0",
43
+ "reflect-metadata": "^0.1.13 || ^0.2.0"
44
+ },
45
+ "dependencies": {
46
+ "@opentelemetry/api": "^1.9.0",
47
+ "@opentelemetry/resources": "^2.2.0",
48
+ "@opentelemetry/sdk-trace-base": "^2.2.0",
49
+ "@opentelemetry/sdk-trace-node": "^2.2.0",
50
+ "@opentelemetry/semantic-conventions": "^1.38.0",
51
+ "axios": "^1.13.2"
52
+ },
53
+ "devDependencies": {
54
+ "@nestjs/common": "^11.0.1",
55
+ "@nestjs/core": "^11.0.1",
56
+ "@types/node": "^22.10.7",
57
+ "eslint": "^9.18.0",
58
+ "prettier": "^3.4.2",
59
+ "typescript": "^5.7.3",
60
+ "jest": "^30.0.0",
61
+ "ts-jest": "^29.2.5"
62
+ },
63
+ "engines": {
64
+ "node": ">=18.0.0"
65
+ }
66
+ }