ntropi 1.0.0-beta.1

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/README.md ADDED
@@ -0,0 +1,293 @@
1
+
2
+ ![Ntropi Logo](https://github.com/Pushpender-18/DeLender/blob/main/ntropi-npm-bg.png?raw=true)
3
+
4
+ <div align="center">
5
+ <div style="margin-top: 8px;">
6
+ <strong>
7
+ <p>A live mock API generator that works without having any actual backend.
8
+ Perfect for frontend development, testing, and prototyping.</p>
9
+ </strong>
10
+ </div>
11
+ </div>
12
+
13
+ ## Table of Contents
14
+
15
+ - [Installation](#installation)
16
+ - [Quick Start](#quick-start)
17
+ - [Usage](#usage)
18
+ - [Basic Commands](#basic-commands)
19
+ - [Command Options](#command-options)
20
+ - [Configuration File](#configuration-file)
21
+ - [Endpoint Properties](#endpoint-properties)
22
+ - [Examples](#examples)
23
+ - [Example 1: Simple API for Frontend Development](#example-1-simple-api-for-frontend-development)
24
+ - [Example 2: Simulate Slow Network](#example-2-simulate-slow-network)
25
+ - [Example 3: Test Error Handling](#example-3-test-error-handling)
26
+ - [Example 4: Multiple Endpoints](#example-4-multiple-endpoints)
27
+ - [Example 5: Add Endpoints](#example-5-add-endpoints)
28
+ - [Example 6: Custom Config File](#example-6-custom-config-file)
29
+ - [Example 7: Complete Workflow](#example-7-complete-workflow)
30
+ - [Advanced Usage](#advanced-usage)
31
+ - [Custom Response Data](#custom-response-data)
32
+ - [Testing Different Scenarios](#testing-different-scenarios)
33
+ - [Tips](#tips)
34
+ - [Troubleshooting](#troubleshooting)
35
+ - [Authors](#authors)
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install -g ntropi
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ Generate a config and run the server:
46
+
47
+ ```bash
48
+ ntropi --api users posts --delay 1000 --failure-rate 10
49
+ ntropi run
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ### Basic Commands
55
+
56
+ #### Run the Mock Server
57
+
58
+ ```bash
59
+ # Run with default config (ntropi-config.json)
60
+ ntropi run
61
+
62
+ # Run with custom config file
63
+ ntropi run --config my-config.json
64
+ ```
65
+
66
+ #### Generate Configuration
67
+
68
+ ```bash
69
+ # Generate default config with default settings
70
+ ntropi
71
+
72
+ # Generate config with specific APIs
73
+ ntropi --api users posts products
74
+
75
+ # Generate config with delay (in milliseconds)
76
+ ntropi --api users --delay 2000
77
+
78
+ # Generate config with failure rate (0-100%)
79
+ ntropi --api users --failure-rate 50
80
+
81
+ # Combine multiple options
82
+ ntropi --api users posts --delay 1000 --failure-rate 25
83
+ ```
84
+
85
+ ### Command Options
86
+
87
+ | Option | Shorthand | Description | Example |
88
+ |--------|-----------|-------------|---------|
89
+ | `--help` | | Show help information | `ntropi --help` |
90
+ | `run` | | Run the mock server | `ntropi run` |
91
+ | `--api` | `-a` | Specify API endpoints | `ntropi --api users posts` |
92
+ | `--delay` | `-d` | Set response delay (ms) | `ntropi --delay 2000` |
93
+ | `--failure-rate` | `-f` | Set failure rate (0-100%) | `ntropi --failure-rate 30` |
94
+ | `--config` | `-c` | Specify config file | `ntropi --config custom.json` |
95
+
96
+ ## Configuration File
97
+
98
+ The configuration file (`ntropi-config.json`) defines your mock API endpoints:
99
+
100
+ ```json
101
+ {
102
+ "endpoints": [
103
+ {
104
+ "path": "/api/users",
105
+ "method": "GET",
106
+ // If data is an array then one
107
+ // item is sent at random
108
+ "data": [
109
+ {"id": 1, "name": "John Doe"},
110
+ {"id": 2, "name": "Jane Smith"}
111
+ ],
112
+ "delay": 1000,
113
+ "failureRate": 10
114
+ },
115
+ {
116
+ "path": "/api/posts",
117
+ "method": "GET",
118
+ // If data is not an array then
119
+ // data is sent as it is
120
+ "data":
121
+ {"id": 1, "title": "Hello World"},
122
+ "delay": 0,
123
+ "failureRate": 0
124
+ }
125
+ ]
126
+ }
127
+ ```
128
+
129
+ **Note:** If data is an array then an item is choosen at random otherwise the data is sent as it is.
130
+
131
+ ### Endpoint Properties
132
+
133
+ - **path**: API endpoint path (e.g., `/api/users`)
134
+ - **method**: HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.)
135
+ - **data**: Response data (can be any valid JSON)
136
+ - **delay**: Response delay in milliseconds
137
+ - **failureRate**: Percentage chance of returning 500 error (0-100)
138
+
139
+ ## Examples
140
+
141
+ ### Example 1: Simple API for Frontend Development
142
+
143
+ ```bash
144
+ # Create a users API with no delay
145
+ ntropi --api users --delay 0 --failure-rate 0
146
+ ntropi run
147
+ ```
148
+
149
+ Access at: `http://localhost:8008/api/users`
150
+
151
+ ### Example 2: Simulate Slow Network
152
+
153
+ ```bash
154
+ # Create API with 3 second delay
155
+ ntropi --api products --delay 3000
156
+ ntropi run
157
+ ```
158
+
159
+ ### Example 3: Test Error Handling
160
+
161
+ ```bash
162
+ # Create API with 50% failure rate
163
+ ntropi --api orders --failure-rate 50
164
+ ntropi run
165
+ ```
166
+
167
+ ### Example 4: Multiple Endpoints
168
+
169
+ ```bash
170
+ # Create multiple APIs with different configurations
171
+ ntropi --api users posts comments products
172
+ ntropi run
173
+ ```
174
+
175
+ Then manually edit `ntropi-config.json` to customize each endpoint.
176
+
177
+ ### Example 5: Add Endpoints
178
+
179
+ ```bash
180
+ # Add APIs to existing configuratoin
181
+ ntropi --api users
182
+ ntropi --api posts
183
+ ntropi --api comments
184
+ ntropi --api products
185
+ ntropi run
186
+ ```
187
+
188
+
189
+ ### Example 6: Custom Config File
190
+
191
+ ```bash
192
+ # Use a specific config file
193
+ ntropi run --config staging-api.json
194
+ ```
195
+
196
+ ### Example 7: Complete Workflow
197
+
198
+ ```bash
199
+ # 1. Generate config with 3 APIs
200
+ ntropi --api users posts comments --delay 500 --failure-rate 5
201
+
202
+ # 2. Edit ntropi-config.json to customize responses
203
+
204
+ # 3. Run the server
205
+ ntropi run
206
+
207
+ # 4. Access your mock APIs
208
+ # GET http://localhost:8008/api/users
209
+ # GET http://localhost:8008/api/posts
210
+ # GET http://localhost:8008/api/comments
211
+ ```
212
+
213
+ ## Advanced Usage
214
+
215
+ ### Custom Response Data
216
+
217
+ Edit `ntropi-config.json` to add custom response data:
218
+
219
+ ```json
220
+ {
221
+ "endpoints": [
222
+ {
223
+ "path": "/api/users/1",
224
+ "method": "GET",
225
+ "data": {
226
+ "id": 1,
227
+ "name": "Alice Johnson",
228
+ "email": "alice@example.com",
229
+ "role": "admin"
230
+ },
231
+ "delay": 500,
232
+ "failureRate": 0
233
+ }
234
+ ]
235
+ }
236
+ ```
237
+
238
+ ### Testing Different Scenarios
239
+
240
+ ```json
241
+ {
242
+ "endpoints": [
243
+ {
244
+ "path": "/api/fast-endpoint",
245
+ "method": "GET",
246
+ "data": {"message": "Lightning fast!"},
247
+ "delay": 0,
248
+ "failureRate": 0
249
+ },
250
+ {
251
+ "path": "/api/slow-endpoint",
252
+ "method": "GET",
253
+ "data": {"message": "Taking my time..."},
254
+ "delay": 5000,
255
+ "failureRate": 0
256
+ },
257
+ {
258
+ "path": "/api/unreliable-endpoint",
259
+ "method": "GET",
260
+ "data": {"message": "Hope I work!"},
261
+ "delay": 1000,
262
+ "failureRate": 80
263
+ }
264
+ ]
265
+ }
266
+ ```
267
+
268
+ ## Tips
269
+
270
+ 1. **Start Simple**: Begin with basic endpoints and add complexity as needed
271
+ 2. **Test Error States**: Use `failureRate` to ensure your app handles errors gracefully
272
+ 3. **Realistic Delays**: Use delays to simulate real-world network conditions
273
+ 4. **Version Control**: Commit your config files to share API mocks with your team
274
+ 5. **Multiple Configs**: Use different config files for different testing scenarios
275
+
276
+ ## Troubleshooting
277
+
278
+ ### Port Already in Use
279
+ If port 8008 is busy, Ntropi will automatically try the next available port.
280
+
281
+ ### Config File Not Found
282
+ Make sure you've generated a config file first or specify the correct path with `--config`.
283
+
284
+ ### Syntax Error in Config
285
+ Validate your JSON file using a JSON validator or linter.
286
+
287
+ ## Authors
288
+
289
+ - [Amitrajeet Konch](https://x.com/amitrajeet7635)
290
+ - [Pushpender Singh](https://x.com/Pushpender20359)
291
+
292
+
293
+
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "ntropi",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "A live mock api generator that works without having any actual backend.",
5
+ "author": "Amitrajeet Konch, Pushpender Singh",
6
+ "type": "module",
7
+ "main": "src/app.js",
8
+ "bin": {
9
+ "ntropi": "src/app.js"
10
+ },
11
+ "scripts": {
12
+ "test": "vitest"
13
+ },
14
+ "dependencies": {
15
+ "dotenv": "^17.2.3",
16
+ "fastify": "^5.7.4"
17
+ },
18
+ "devDependencies": {
19
+ "verdaccio": "^6.2.5",
20
+ "vitest": "^4.0.18"
21
+ }
22
+ }
package/src/app.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run_server } from './libs/backend.js';
4
+ import { parseArgs } from './libs/cli_parser.js';
5
+ import { updateConfig } from './libs/config_updater.js';
6
+
7
+ const args = process.argv.slice(2);
8
+ const CONFIG = parseArgs(args);
9
+
10
+ // Error Handling
11
+ if (CONFIG.error) {
12
+ console.log(`Error: ${CONFIG.error}`);
13
+ process.exit(1);
14
+ }
15
+
16
+ // Help
17
+ if (CONFIG.help) {
18
+ console.log(`
19
+ Usage: node app.js [options]
20
+
21
+ Options:
22
+ --help Show help information
23
+ -a, --api Specify API endpoints to generate (e.g., --api users orders products)
24
+ -d, --delay <num> Set delay in seconds (non-negative integer)
25
+ -f, --failure-rate <num> Set failure rate percentage (0-100)
26
+ -c, --config <file> Specify configuration file (default: ntropi-config.json generated)
27
+ run Start the server with the default configuration file (-c for custom config file)
28
+ `);
29
+ process.exit(0);
30
+ }
31
+
32
+ if (CONFIG.run) {
33
+ // Run Server
34
+ const configPath = CONFIG.config || 'ntropi-config.json';
35
+ await run_server(configPath);
36
+ } else {
37
+ updateConfig(CONFIG);
38
+ }
@@ -0,0 +1,171 @@
1
+ import Fastify from "fastify";
2
+ import dotenv from "dotenv";
3
+ import { getConfigStateManager } from "./config_state_management.js";
4
+
5
+ dotenv.config();
6
+
7
+ let PORT;
8
+ let currentFastifyInstance = null;
9
+
10
+ function getEndpointSignature(endpoint) {
11
+ return `${endpoint.method}:${endpoint.path}`;
12
+ }
13
+
14
+ function hasEndpointChanges(oldConfig, newConfig) {
15
+ const oldSignatures = new Set(oldConfig.endpoints.map(getEndpointSignature));
16
+ const newSignatures = new Set(newConfig.endpoints.map(getEndpointSignature));
17
+
18
+ // Check if any endpoints were added or removed
19
+ if (oldSignatures.size !== newSignatures.size) {
20
+ return true;
21
+ }
22
+
23
+ for (const sig of oldSignatures) {
24
+ if (!newSignatures.has(sig)) {
25
+ return true;
26
+ }
27
+ }
28
+
29
+ for (const sig of newSignatures) {
30
+ if (!oldSignatures.has(sig)) {
31
+ return true;
32
+ }
33
+ }
34
+
35
+ return false;
36
+ }
37
+
38
+ function start_listener(fastify) {
39
+ fastify.listen({ port: PORT}, function (err, address) {
40
+ if (err) {
41
+ PORT = (PORT + 1);
42
+ if (PORT > 65535) {
43
+ console.error("No available ports found. Please free up a port and try again.");
44
+ process.exit(1);
45
+ }
46
+ start_listener(fastify);
47
+ } else {
48
+ console.log(`Server listening at http://localhost:${PORT}/`);
49
+ const configManager = getConfigStateManager();
50
+ const config = configManager.getState();
51
+ output_endpoint_path(PORT, config);
52
+ }
53
+ });
54
+ }
55
+
56
+ function generate_endpoint_path(fastify, config, configManager) {
57
+ for (let endpoint of config.endpoints) {
58
+ fastify.route({
59
+ method: endpoint.method,
60
+ url: endpoint.path,
61
+ handler: async (request, reply) => {
62
+ // Get latest config state for hot reload support
63
+ const currentConfig = configManager.getState();
64
+ const currentEndpoint = currentConfig.endpoints.find(
65
+ ep => ep.path === endpoint.path && ep.method === endpoint.method
66
+ );
67
+
68
+ if (!currentEndpoint) {
69
+ reply.code(404).send({ error: "Endpoint not found in current config" });
70
+ return;
71
+ }
72
+
73
+ // Simulate delay with current config value
74
+ if (currentEndpoint.delay && currentEndpoint.delay > 0) {
75
+ await new Promise(resolve => setTimeout(resolve, currentEndpoint.delay));
76
+ }
77
+
78
+ // Simulate failure with current config value
79
+ if (currentEndpoint.failureRate && Math.random() * 100 < currentEndpoint.failureRate) {
80
+ reply.code(500).send({ error: "Simulated server error" });
81
+ return;
82
+ }
83
+
84
+ if (Array.isArray(currentEndpoint.data)) {
85
+ currentEndpoint.data = currentEndpoint.data[Math.floor(Math.random() * currentEndpoint.data.length)];
86
+ }
87
+ reply.send(currentEndpoint.data);
88
+ }
89
+ });
90
+ }
91
+ }
92
+
93
+ function output_endpoint_path(PORT, config) {
94
+ for (let endpoint of config.endpoints) {
95
+ console.log(`Route: http://localhost:${PORT}${endpoint.path}`);
96
+ }
97
+ }
98
+
99
+ export function printNtropi() {
100
+ console.log(`
101
+ _ _ _ _
102
+ | \\ | | |_ _ __ ___ _ __ (_)
103
+ | \\| | __| '__/ _ \\| '_ \\| |
104
+ | |\\ | |_| | | (_) | |_) | | _
105
+ |_| \\_|\\__|_| \\___/| .__/|_| (_)
106
+ |_|
107
+ `);
108
+ }
109
+
110
+ export async function run_server(configPath = 'ntropi-config.json') {
111
+ printNtropi();
112
+
113
+ // Initialize config state manager
114
+ const configManager = getConfigStateManager(configPath);
115
+ const initResult = configManager.initialize();
116
+
117
+ if (!initResult.success) {
118
+ console.error(`Failed to load config: ${initResult.message}`);
119
+ process.exit(1);
120
+ }
121
+
122
+ let currentConfig = configManager.getState();
123
+
124
+ // Enable hot reload for automatic config updates
125
+ configManager.enableHotReload();
126
+
127
+ // Subscribe to config changes for logging and server restart
128
+ configManager.subscribe((event, state, details) => {
129
+ if (event === 'updated') {
130
+ // Check if endpoints were added or removed
131
+ if (hasEndpointChanges(currentConfig, state)) {
132
+ console.log('Endpoints added or removed - restarting server...');
133
+ restartServer(configManager);
134
+ currentConfig = state;
135
+ } else {
136
+ console.log('Config updated - endpoints will use new delay and failure rate values');
137
+ currentConfig = state;
138
+ }
139
+ } else if (event === 'fallback') {
140
+ console.log('Config update failed - using previous working configuration');
141
+ }
142
+ });
143
+
144
+ startServer(configManager);
145
+ }
146
+
147
+ function startServer(configManager) {
148
+ const config = configManager.getState();
149
+ const fastify = Fastify({ logger: false });
150
+ PORT = Number(process.env.PORT) || 8008;
151
+
152
+ generate_endpoint_path(fastify, config, configManager);
153
+ currentFastifyInstance = fastify;
154
+ start_listener(fastify);
155
+ }
156
+
157
+ async function restartServer(configManager) {
158
+ if (currentFastifyInstance) {
159
+ try {
160
+ await currentFastifyInstance.close();
161
+ console.log('Server stopped');
162
+ } catch (error) {
163
+ console.error('Error stopping server:', error);
164
+ }
165
+ }
166
+
167
+ // Reset to find next available port if needed
168
+ PORT = Number(process.env.PORT) || 8008;
169
+ console.log('Starting server with new configuration...');
170
+ startServer(configManager);
171
+ }
@@ -0,0 +1,111 @@
1
+ import { updateConfig } from "./config_updater.js";
2
+
3
+ export function parseArgs(args) {
4
+ const result = {};
5
+
6
+ for (let i = 0; i < args.length; i++) {
7
+ let arg = args[i];
8
+
9
+ // Handle 'run' command (without dashes)
10
+ if (arg === 'run') {
11
+ result.run = true;
12
+ continue;
13
+ }
14
+
15
+ // Skip if not a flag
16
+ if (!arg.startsWith('-')) {
17
+ continue;
18
+ }
19
+
20
+ // Map shorthand to full argument names
21
+ const shorthandMap = {
22
+ '-d': '--delay',
23
+ '-f': '--failure-rate',
24
+ '-c': '--config',
25
+ '-a': '--api',
26
+ '-r': '--run'
27
+ };
28
+
29
+ if (shorthandMap[arg]) {
30
+ arg = shorthandMap[arg];
31
+ }
32
+
33
+ const validArgs = ['--help', '--delay', '--failure-rate', '--config', "--api", '--run'];
34
+ if (!validArgs.includes(arg)) {
35
+ return { error: `Unknown argument: ${args[i]}` };
36
+ }
37
+
38
+ if (arg === '--help') {
39
+ result.help = true;
40
+ } else if (arg === '--run') {
41
+ result.run = true;
42
+ } else if (arg === '--delay') {
43
+ const delayValue = args[i + 1];
44
+ const delayNum = Number(delayValue);
45
+
46
+ if (isNaN(delayNum) || delayNum < 0 || !Number.isInteger(delayNum)) {
47
+ return { error: 'Delay must be a non-negative integer' };
48
+ }
49
+
50
+ result.delay = delayNum;
51
+ i++;
52
+ } else if (arg === '--api') {
53
+ // Collect all values following --api until the next flag or end of args
54
+ const apiValues = [];
55
+ let j = i + 1;
56
+ while (j < args.length && !args[j].startsWith('--')) {
57
+ apiValues.push(args[j]);
58
+ j++;
59
+ }
60
+
61
+ if (apiValues.length === 0) {
62
+ return { error: 'API argument requires at least one API name' };
63
+ }
64
+
65
+ result.apis = apiValues;
66
+ i = j - 1; // Update i to the last processed value
67
+ } else if (arg === '--failure-rate') {
68
+ const failureRateValue = args[i + 1];
69
+ const failureRateNum = Number(failureRateValue);
70
+
71
+ if (isNaN(failureRateNum) || failureRateNum < 0 || failureRateNum > 100) {
72
+ return { error: 'Failure rate must be between 0 and 100' };
73
+ }
74
+
75
+ result.failureRate = failureRateNum;
76
+ i++;
77
+ } else if (arg === '--config') {
78
+ result.config = args[i + 1];
79
+ i++;
80
+ }
81
+ }
82
+
83
+ if (result.run) {
84
+ if (result.config) {
85
+ return result;
86
+
87
+ }
88
+ result.config = 'ntropi-config.json';
89
+ return result;
90
+ }
91
+
92
+ if (!result.help) {
93
+ if (!result.config) {
94
+ result.config = 'ntropi-config.json';
95
+ if (!result.delay) {
96
+ result.delay = 0;
97
+ }
98
+ if (!result.failureRate) {
99
+ result.failureRate = 0;
100
+ }
101
+ }
102
+ else if(result.config) {
103
+ const response = updateConfig(result);
104
+ if (response.error) {
105
+ console.log(`Error: ${response.error}`);
106
+ }
107
+ }
108
+ }
109
+
110
+ return result;
111
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'fs';
2
+
3
+ export function generateConfig(delay=0, failureRate=0, apis=[], configFileName='ntropi-config.json') {
4
+ let fileConfigurationJSON = {"endpoints": []};
5
+ if (apis.length == 0) {
6
+ fileConfigurationJSON["endpoints"].push(
7
+ {
8
+ 'path': '/api/health_check',
9
+ "method": "GET",
10
+ "data": ["OK", "Healthy", "Working Fine", "All Good", "Up and Running"],
11
+ "delay": delay,
12
+ "failureRate": failureRate
13
+ }
14
+ );
15
+ } else {
16
+ for (let api of apis) {
17
+ fileConfigurationJSON["endpoints"].push(
18
+ {
19
+ 'path': `/api/${api}`,
20
+ "method": "GET",
21
+ "data": [`Response from ${api}`],
22
+ "delay": delay,
23
+ "failureRate": failureRate
24
+ }
25
+ );
26
+ }
27
+ }
28
+
29
+ if (fs.existsSync(configFileName)) {
30
+ return {error: "CONFIG_ALREADY_EXISTS"};
31
+ }
32
+
33
+ fs.writeFileSync(configFileName, JSON.stringify(fileConfigurationJSON, null, 2));
34
+ return {success: true};
35
+
36
+ }
@@ -0,0 +1,183 @@
1
+ import fs from 'fs';
2
+ import { validateConfig } from './config_validator.js';
3
+ import { updateConfig } from './config_updater.js';
4
+
5
+ class ConfigStateManager {
6
+ constructor(configPath = 'ntropi-config.json') {
7
+ this.configPath = configPath;
8
+ this.currentState = null;
9
+ this.lastWorkingState = null;
10
+ this.fileWatcher = null;
11
+ this.subscribers = [];
12
+ this.isInitialized = false;
13
+ }
14
+
15
+ // Initialize the state manager and load initial config
16
+ initialize() {
17
+ if (this.isInitialized) {
18
+ console.warn('Config state manager already initialized');
19
+ return { success: true, state: this.currentState };
20
+ }
21
+
22
+ const result = this.loadConfig();
23
+ if (result.success) {
24
+ this.isInitialized = true;
25
+ this.lastWorkingState = JSON.parse(JSON.stringify(this.currentState));
26
+ }
27
+
28
+ return result;
29
+ }
30
+
31
+ // Load and validate config file
32
+ loadConfig() {
33
+ try {
34
+ if (!fs.existsSync(this.configPath)) {
35
+ console.log('Config file not found, creating default config');
36
+ updateConfig({ config: this.configPath });
37
+ }
38
+
39
+ const fileContent = fs.readFileSync(this.configPath, 'utf-8');
40
+ const parsedConfig = JSON.parse(fileContent);
41
+
42
+ const validation = validateConfig(parsedConfig);
43
+ if (!validation.valid) {
44
+ console.error('Config validation failed:', validation.error);
45
+ return {
46
+ success: false,
47
+ error: 'CONFIG_VALIDATION_FAILED',
48
+ message: 'Config validation failed',
49
+ validationError: validation.error
50
+ };
51
+ }
52
+
53
+ this.currentState = validation.config;
54
+ return { success: true, state: this.currentState };
55
+
56
+ } catch (error) {
57
+ if (error instanceof SyntaxError) {
58
+ return {
59
+ success: false,
60
+ error: 'CONFIG_PARSE_ERROR',
61
+ message: 'Invalid JSON in config file'
62
+ };
63
+ }
64
+ return {
65
+ success: false,
66
+ error: 'CONFIG_LOAD_ERROR',
67
+ message: error.message
68
+ };
69
+ }
70
+ }
71
+
72
+ // Enable hot reload with file watching
73
+ enableHotReload() {
74
+ if (!this.isInitialized) {
75
+ throw new Error('Config state manager must be initialized before enabling hot reload');
76
+ }
77
+
78
+ if (this.fileWatcher) {
79
+ console.warn('Hot reload already enabled');
80
+ return;
81
+ }
82
+
83
+ this.fileWatcher = fs.watch(this.configPath, (eventType) => {
84
+ if (eventType === 'change') {
85
+ this.handleConfigChange();
86
+ }
87
+ });
88
+
89
+ console.log(`Hot reload enabled for ${this.configPath}`);
90
+ }
91
+
92
+ // Handle config file changes
93
+ handleConfigChange() {
94
+ console.log('Config file change detected, reloading...');
95
+
96
+ const result = this.loadConfig();
97
+
98
+ if (result.success) {
99
+ this.lastWorkingState = JSON.parse(JSON.stringify(this.currentState));
100
+ console.log('Config reloaded successfully');
101
+ this.notifySubscribers('updated', this.currentState);
102
+ } else {
103
+ console.error('Config reload failed, falling back to last working state');
104
+ this.currentState = this.lastWorkingState;
105
+ this.notifySubscribers('fallback', this.lastWorkingState, result);
106
+ }
107
+ }
108
+
109
+ // Subscribe to config changes
110
+ subscribe(callback) {
111
+ if (typeof callback !== 'function') {
112
+ throw new Error('Callback must be a function');
113
+ }
114
+ this.subscribers.push(callback);
115
+ return () => this.unsubscribe(callback);
116
+ }
117
+
118
+ // Unsubscribe from config changes
119
+ unsubscribe(callback) {
120
+ this.subscribers = this.subscribers.filter(cb => cb !== callback);
121
+ }
122
+
123
+ // Notify all subscribers of state changes
124
+ notifySubscribers(event, state, details = null) {
125
+ this.subscribers.forEach(callback => {
126
+ try {
127
+ callback(event, state, details);
128
+ } catch (error) {
129
+ console.error('Error in subscriber callback:', error);
130
+ }
131
+ });
132
+ }
133
+
134
+ // Get current config state as JSON
135
+ getState() {
136
+ if (!this.isInitialized) {
137
+ return null;
138
+ }
139
+ return JSON.parse(JSON.stringify(this.currentState));
140
+ }
141
+
142
+ // Get last working config state as JSON
143
+ getLastWorkingState() {
144
+ if (!this.lastWorkingState) {
145
+ return null;
146
+ }
147
+ return JSON.parse(JSON.stringify(this.lastWorkingState));
148
+ }
149
+
150
+ // Disable hot reload
151
+ disableHotReload() {
152
+ if (this.fileWatcher) {
153
+ this.fileWatcher.close();
154
+ this.fileWatcher = null;
155
+ console.log('Hot reload disabled');
156
+ }
157
+ }
158
+
159
+ // Cleanup and shutdown
160
+ shutdown() {
161
+ this.disableHotReload();
162
+ this.subscribers = [];
163
+ this.isInitialized = false;
164
+ console.log('Config state manager shutdown complete');
165
+ }
166
+ }
167
+
168
+ // Singleton instance
169
+ let configStateManagerInstance = null;
170
+
171
+ export function getConfigStateManager(configPath = 'ntropi-config.json') {
172
+ if (!configStateManagerInstance) {
173
+ configStateManagerInstance = new ConfigStateManager(configPath);
174
+ }
175
+ return configStateManagerInstance;
176
+ }
177
+
178
+ export function resetConfigStateManager() {
179
+ if (configStateManagerInstance) {
180
+ configStateManagerInstance.shutdown();
181
+ configStateManagerInstance = null;
182
+ }
183
+ }
@@ -0,0 +1,56 @@
1
+ import fs from "fs";
2
+ import { validateConfig } from "./config_validator.js";
3
+ import { generateConfig } from "./config_generator.js";
4
+
5
+ export function updateConfig(configuration_data) {
6
+ const config_file = configuration_data.config;
7
+ const delay = configuration_data.delay;
8
+ const failureRate = configuration_data.failureRate;
9
+ const apis = configuration_data.apis;
10
+
11
+ if (!fs.existsSync(config_file)) {
12
+ return generateConfig(delay, failureRate, apis, config_file);
13
+ }
14
+
15
+ try {
16
+ const config_data = fs.readFileSync(config_file, "utf-8");
17
+ const result = validateConfig(JSON.parse(config_data));
18
+
19
+ if (!result.valid) {
20
+ return {
21
+ success: false,
22
+ error: result.error
23
+ };
24
+ }
25
+
26
+ let updatedConfig = result.config;
27
+
28
+ // Append new APIs to existing config
29
+ for (let api of apis) {
30
+ // Check if API already exists in config
31
+ const existingEndpoint = updatedConfig.endpoints.find(endpoint => endpoint.path === `/api/${api}`);
32
+ if (!existingEndpoint) {
33
+ updatedConfig.endpoints.push({
34
+ path: `/api/${api}`,
35
+ method: "GET",
36
+ data: [`Response from ${api}`],
37
+ delay: delay,
38
+ failureRate: failureRate
39
+ });
40
+ }
41
+ }
42
+
43
+ // Write updated config back to file
44
+ fs.writeFileSync(config_file, JSON.stringify(updatedConfig, null, 2), "utf-8");
45
+
46
+ return {
47
+ success: true
48
+ };
49
+
50
+ } catch (error) {
51
+ return {
52
+ success: false,
53
+ error: "Error reading or parsing config file: " + error.message
54
+ };
55
+ }
56
+ }
@@ -0,0 +1,109 @@
1
+ export function validateConfig(config) {
2
+ if (!config || typeof config !== 'object') {
3
+ return { valid: false, error: 'Config must be a valid JSON object' };
4
+ }
5
+
6
+ if (!config.endpoints) {
7
+ return { valid: false, error: 'Config must have an "endpoints" property' };
8
+ }
9
+
10
+ if (!Array.isArray(config.endpoints)) {
11
+ return { valid: false, error: 'Endpoints must be an array' };
12
+ }
13
+
14
+ if (config.endpoints.length === 0) {
15
+ return { valid: false, error: 'Endpoints array cannot be empty' };
16
+ }
17
+
18
+ const validatedConfig = {
19
+ endpoints: []
20
+ };
21
+
22
+ for (let i = 0; i < config.endpoints.length; i++) {
23
+ const endpoint = config.endpoints[i];
24
+ const validationResult = validateEndpoint(endpoint, i);
25
+
26
+ if (!validationResult.valid) {
27
+ return validationResult;
28
+ }
29
+
30
+ validatedConfig.endpoints.push(validationResult.endpoint);
31
+ }
32
+
33
+ return {
34
+ valid: true,
35
+ config: validatedConfig
36
+ };
37
+ }
38
+
39
+ function validateEndpoint(endpoint, index) {
40
+ if (!endpoint || typeof endpoint !== 'object') {
41
+ return { valid: false, error: `Endpoint at index ${index} must be a valid object` };
42
+ }
43
+
44
+ if (!endpoint.path) {
45
+ return { valid: false, error: `Endpoint at index ${index} is missing required property: path` };
46
+ }
47
+
48
+ if (typeof endpoint.path !== 'string') {
49
+ return { valid: false, error: `Endpoint at index ${index} has invalid path: must be a string` };
50
+ }
51
+
52
+ if (!endpoint.path.startsWith('/')) {
53
+ return { valid: false, error: `Endpoint at index ${index} has invalid path: must start with /` };
54
+ }
55
+
56
+ if (!endpoint.method) {
57
+ return { valid: false, error: `Endpoint at index ${index} is missing required property: method` };
58
+ }
59
+
60
+ if (typeof endpoint.method !== 'string') {
61
+ return { valid: false, error: `Endpoint at index ${index} has invalid method: must be a string` };
62
+ }
63
+
64
+ const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
65
+ const normalizedMethod = endpoint.method.toUpperCase();
66
+
67
+ if (!validMethods.includes(normalizedMethod)) {
68
+ return { valid: false, error: `Endpoint at index ${index} has invalid method: ${endpoint.method}` };
69
+ }
70
+
71
+ if (!endpoint.data) {
72
+ return { valid: false, error: `Endpoint at index ${index} is missing required property: data` };
73
+ }
74
+
75
+ if (!Array.isArray(endpoint.data)) {
76
+ return { valid: false, error: `Endpoint at index ${index} has invalid data: must be an array` };
77
+ }
78
+
79
+ if (endpoint.data.length === 0) {
80
+ return { valid: false, error: `Endpoint at index ${index} has invalid data: array cannot be empty` };
81
+ }
82
+
83
+ const validatedEndpoint = {
84
+ path: endpoint.path,
85
+ method: normalizedMethod,
86
+ data: endpoint.data,
87
+ delay: 0,
88
+ failureRate: 0
89
+ };
90
+
91
+ if (endpoint.delay !== undefined) {
92
+ if (typeof endpoint.delay !== 'number' || endpoint.delay < 0) {
93
+ return { valid: false, error: `Endpoint at index ${index} has invalid delay: delay must be a non-negative number` };
94
+ }
95
+ validatedEndpoint.delay = endpoint.delay;
96
+ }
97
+
98
+ if (endpoint.failureRate !== undefined) {
99
+ if (typeof endpoint.failureRate !== 'number' || endpoint.failureRate < 0 || endpoint.failureRate > 100) {
100
+ return { valid: false, error: `Endpoint at index ${index} has invalid failureRate: must be between 0 and 100` };
101
+ }
102
+ validatedEndpoint.failureRate = endpoint.failureRate;
103
+ }
104
+
105
+ return {
106
+ valid: true,
107
+ endpoint: validatedEndpoint
108
+ };
109
+ }