@devilsdev/rag-pipeline-utils 2.2.1 → 2.3.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 +260 -19
- package/contracts/contract-schema.json +189 -0
- package/dist/index.cjs +19 -0
- package/dist/index.mjs +15 -1
- package/package.json +3 -2
- package/src/cli/commands/wizard.js +627 -0
- package/src/cli/input-sanitization-middleware.js +356 -0
- package/src/cli/templates/development.template.json +64 -0
- package/src/cli/templates/minimal.template.json +33 -0
- package/src/cli/templates/production.template.json +104 -0
- package/src/cli/templates/testing.template.json +45 -0
- package/src/config/connection-pool-config.json +259 -0
- package/src/config/memory-optimization.js +414 -0
- package/src/config/redaction-patterns.json +150 -0
- package/src/config/sanitization-rules.json +323 -0
- package/src/core/plugin-registry.js +266 -20
- package/src/dag/core/error-context.js +391 -0
- package/src/dag/core/execution-scheduler.js +350 -0
- package/src/dag/core/topology-validation.js +415 -0
- package/src/dag/dag-engine.js +26 -424
- package/src/dev/README.md +126 -0
- package/src/dev/dev-server.js +356 -0
- package/src/dev/examples/hot-reload-usage.js +355 -0
- package/src/dev/hot-reload.js +807 -0
- package/src/enterprise/sso-integration.js +67 -25
- package/src/index.js +19 -0
- package/src/observability/event-logger.js +62 -2
- package/src/plugins/optimized-openai-embedder.js +169 -0
- package/src/security/jwt-config.js +431 -0
- package/src/security/jwt-validator.js +701 -0
- package/src/utils/api-client-pool-integration.js +436 -0
- package/src/utils/batch-processor.js +710 -0
- package/src/utils/connection-pool.js +605 -0
- package/src/utils/contract-validator.js +460 -0
- package/src/utils/error-formatter.js +603 -0
- package/src/utils/examples/error-formatter-usage.js +351 -0
- package/src/utils/input-sanitizer.js +967 -0
- package/src/utils/memory-monitor.js +800 -0
- package/src/utils/secure-logger-config.js +352 -0
- package/src/utils/secure-logger.js +576 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hot Reload Manager for Development
|
|
5
|
+
*
|
|
6
|
+
* Provides hot reloading functionality for plugins and configuration
|
|
7
|
+
* during development without requiring application restart.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - File watching for plugins and configuration
|
|
11
|
+
* - Dynamic plugin reloading
|
|
12
|
+
* - State preservation during compatible reloads
|
|
13
|
+
* - Reload events and hooks
|
|
14
|
+
* - Error handling with notifications
|
|
15
|
+
*
|
|
16
|
+
* @module dev/hot-reload
|
|
17
|
+
* @since 2.4.0
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { EventEmitter } = require('events');
|
|
21
|
+
const chokidar = require('chokidar');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const {
|
|
25
|
+
createError,
|
|
26
|
+
wrapError,
|
|
27
|
+
ERROR_CODES,
|
|
28
|
+
} = require('../utils/error-formatter');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hot Reload Manager
|
|
32
|
+
*
|
|
33
|
+
* Manages file watching and hot reloading of plugins and configuration.
|
|
34
|
+
*/
|
|
35
|
+
class HotReloadManager extends EventEmitter {
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
super();
|
|
38
|
+
|
|
39
|
+
this.options = {
|
|
40
|
+
enabled: options.enabled !== false,
|
|
41
|
+
watchPaths: options.watchPaths || ['./src/plugins', './src/mocks'],
|
|
42
|
+
configPath: options.configPath || '.ragrc.json',
|
|
43
|
+
debounceDelay: options.debounceDelay || 300,
|
|
44
|
+
preserveState: options.preserveState !== false,
|
|
45
|
+
verbose: options.verbose || false,
|
|
46
|
+
ignorePatterns: options.ignorePatterns || [
|
|
47
|
+
'**/node_modules/**',
|
|
48
|
+
'**/*.test.js',
|
|
49
|
+
'**/*.spec.js',
|
|
50
|
+
'**/.git/**',
|
|
51
|
+
],
|
|
52
|
+
...options,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Internal state
|
|
56
|
+
this.watchers = [];
|
|
57
|
+
this.pluginRegistry = options.pluginRegistry || null;
|
|
58
|
+
this.reloadQueue = new Map();
|
|
59
|
+
this.debounceTimers = new Map();
|
|
60
|
+
this.pluginState = new Map();
|
|
61
|
+
this.reloadHistory = [];
|
|
62
|
+
this.isReloading = false;
|
|
63
|
+
|
|
64
|
+
// Statistics
|
|
65
|
+
this.stats = {
|
|
66
|
+
totalReloads: 0,
|
|
67
|
+
successfulReloads: 0,
|
|
68
|
+
failedReloads: 0,
|
|
69
|
+
configReloads: 0,
|
|
70
|
+
pluginReloads: 0,
|
|
71
|
+
averageReloadTime: 0,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Lifecycle hooks
|
|
75
|
+
this.hooks = {
|
|
76
|
+
beforeReload: [],
|
|
77
|
+
afterReload: [],
|
|
78
|
+
onError: [],
|
|
79
|
+
onStatePreserve: [],
|
|
80
|
+
onStateRestore: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (this.options.enabled) {
|
|
84
|
+
this._log('Hot reload manager initialized');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Start watching for file changes
|
|
90
|
+
*
|
|
91
|
+
* @returns {Promise<void>}
|
|
92
|
+
*/
|
|
93
|
+
async start() {
|
|
94
|
+
if (!this.options.enabled) {
|
|
95
|
+
this._log('Hot reload is disabled');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this._log('Starting hot reload manager...');
|
|
100
|
+
|
|
101
|
+
// Watch plugin files
|
|
102
|
+
await this._watchPlugins();
|
|
103
|
+
|
|
104
|
+
// Watch configuration file
|
|
105
|
+
await this._watchConfig();
|
|
106
|
+
|
|
107
|
+
this.emit('started');
|
|
108
|
+
this._log('Hot reload manager started successfully');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stop watching for file changes
|
|
113
|
+
*
|
|
114
|
+
* @returns {Promise<void>}
|
|
115
|
+
*/
|
|
116
|
+
async stop() {
|
|
117
|
+
this._log('Stopping hot reload manager...');
|
|
118
|
+
|
|
119
|
+
// Close all watchers
|
|
120
|
+
for (const watcher of this.watchers) {
|
|
121
|
+
await watcher.close();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.watchers = [];
|
|
125
|
+
this.emit('stopped');
|
|
126
|
+
this._log('Hot reload manager stopped');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Watch plugin files
|
|
131
|
+
*
|
|
132
|
+
* @private
|
|
133
|
+
* @returns {Promise<void>}
|
|
134
|
+
*/
|
|
135
|
+
async _watchPlugins() {
|
|
136
|
+
const watcher = chokidar.watch(this.options.watchPaths, {
|
|
137
|
+
ignored: this.options.ignorePatterns,
|
|
138
|
+
persistent: true,
|
|
139
|
+
ignoreInitial: true,
|
|
140
|
+
awaitWriteFinish: {
|
|
141
|
+
stabilityThreshold: 100,
|
|
142
|
+
pollInterval: 50,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
watcher.on('change', (filePath) => {
|
|
147
|
+
this._handlePluginChange(filePath);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
watcher.on('add', (filePath) => {
|
|
151
|
+
this._log(`New plugin detected: ${filePath}`);
|
|
152
|
+
this.emit('plugin:added', { path: filePath });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
watcher.on('unlink', (filePath) => {
|
|
156
|
+
this._log(`Plugin removed: ${filePath}`);
|
|
157
|
+
this.emit('plugin:removed', { path: filePath });
|
|
158
|
+
this._handlePluginRemoval(filePath);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
watcher.on('error', (error) => {
|
|
162
|
+
this._handleError('Plugin watcher error', error);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.watchers.push(watcher);
|
|
166
|
+
this._log(`Watching plugins in: ${this.options.watchPaths.join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Watch configuration file
|
|
171
|
+
*
|
|
172
|
+
* @private
|
|
173
|
+
* @returns {Promise<void>}
|
|
174
|
+
*/
|
|
175
|
+
async _watchConfig() {
|
|
176
|
+
if (!this.options.configPath) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const watcher = chokidar.watch(this.options.configPath, {
|
|
181
|
+
persistent: true,
|
|
182
|
+
ignoreInitial: true,
|
|
183
|
+
awaitWriteFinish: {
|
|
184
|
+
stabilityThreshold: 200,
|
|
185
|
+
pollInterval: 100,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
watcher.on('change', () => {
|
|
190
|
+
this._handleConfigChange();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
watcher.on('error', (error) => {
|
|
194
|
+
this._handleError('Config watcher error', error);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
this.watchers.push(watcher);
|
|
198
|
+
this._log(`Watching configuration: ${this.options.configPath}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Handle plugin file change
|
|
203
|
+
*
|
|
204
|
+
* @private
|
|
205
|
+
* @param {string} filePath - Changed file path
|
|
206
|
+
*/
|
|
207
|
+
_handlePluginChange(filePath) {
|
|
208
|
+
this._log(`Plugin changed: ${filePath}`);
|
|
209
|
+
|
|
210
|
+
// Debounce rapid changes
|
|
211
|
+
if (this.debounceTimers.has(filePath)) {
|
|
212
|
+
clearTimeout(this.debounceTimers.get(filePath));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const timer = setTimeout(() => {
|
|
216
|
+
this._reloadPlugin(filePath);
|
|
217
|
+
this.debounceTimers.delete(filePath);
|
|
218
|
+
}, this.options.debounceDelay);
|
|
219
|
+
|
|
220
|
+
this.debounceTimers.set(filePath, timer);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Handle configuration change
|
|
225
|
+
*
|
|
226
|
+
* @private
|
|
227
|
+
*/
|
|
228
|
+
_handleConfigChange() {
|
|
229
|
+
this._log('Configuration changed');
|
|
230
|
+
|
|
231
|
+
// Debounce config changes
|
|
232
|
+
if (this.debounceTimers.has('config')) {
|
|
233
|
+
clearTimeout(this.debounceTimers.get('config'));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const timer = setTimeout(() => {
|
|
237
|
+
this._reloadConfig();
|
|
238
|
+
this.debounceTimers.delete('config');
|
|
239
|
+
}, this.options.debounceDelay);
|
|
240
|
+
|
|
241
|
+
this.debounceTimers.set('config', timer);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Reload a plugin
|
|
246
|
+
*
|
|
247
|
+
* @private
|
|
248
|
+
* @param {string} filePath - Plugin file path
|
|
249
|
+
* @returns {Promise<void>}
|
|
250
|
+
*/
|
|
251
|
+
async _reloadPlugin(filePath) {
|
|
252
|
+
if (this.isReloading) {
|
|
253
|
+
this.reloadQueue.set(filePath, 'plugin');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.isReloading = true;
|
|
258
|
+
const startTime = Date.now();
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
this._log(`Reloading plugin: ${filePath}`);
|
|
262
|
+
|
|
263
|
+
// Execute before reload hooks
|
|
264
|
+
await this._executeHooks('beforeReload', {
|
|
265
|
+
type: 'plugin',
|
|
266
|
+
path: filePath,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Preserve state if enabled
|
|
270
|
+
let preservedState = null;
|
|
271
|
+
if (this.options.preserveState) {
|
|
272
|
+
preservedState = await this._preservePluginState(filePath);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Clear module cache
|
|
276
|
+
const resolvedPath = require.resolve(path.resolve(filePath));
|
|
277
|
+
delete require.cache[resolvedPath];
|
|
278
|
+
|
|
279
|
+
// Reload the plugin
|
|
280
|
+
const plugin = require(resolvedPath);
|
|
281
|
+
|
|
282
|
+
// Restore state if preserved
|
|
283
|
+
if (preservedState && this.options.preserveState) {
|
|
284
|
+
await this._restorePluginState(plugin, preservedState);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Update plugin registry if available
|
|
288
|
+
if (this.pluginRegistry) {
|
|
289
|
+
await this._updatePluginRegistry(filePath, plugin);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const reloadTime = Date.now() - startTime;
|
|
293
|
+
|
|
294
|
+
// Update statistics
|
|
295
|
+
this.stats.totalReloads++;
|
|
296
|
+
this.stats.successfulReloads++;
|
|
297
|
+
this.stats.pluginReloads++;
|
|
298
|
+
this._updateAverageReloadTime(reloadTime);
|
|
299
|
+
|
|
300
|
+
// Record reload
|
|
301
|
+
this._recordReload({
|
|
302
|
+
type: 'plugin',
|
|
303
|
+
path: filePath,
|
|
304
|
+
success: true,
|
|
305
|
+
duration: reloadTime,
|
|
306
|
+
timestamp: new Date().toISOString(),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Execute after reload hooks
|
|
310
|
+
await this._executeHooks('afterReload', {
|
|
311
|
+
type: 'plugin',
|
|
312
|
+
path: filePath,
|
|
313
|
+
plugin,
|
|
314
|
+
duration: reloadTime,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
this.emit('plugin:reloaded', {
|
|
318
|
+
path: filePath,
|
|
319
|
+
plugin,
|
|
320
|
+
duration: reloadTime,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
this._log(`Plugin reloaded successfully: ${filePath} (${reloadTime}ms)`);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
this.stats.totalReloads++;
|
|
326
|
+
this.stats.failedReloads++;
|
|
327
|
+
|
|
328
|
+
this._recordReload({
|
|
329
|
+
type: 'plugin',
|
|
330
|
+
path: filePath,
|
|
331
|
+
success: false,
|
|
332
|
+
error: error.message,
|
|
333
|
+
timestamp: new Date().toISOString(),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const enhancedError = wrapError(error, ERROR_CODES.HOT_RELOAD_FAILED, {
|
|
337
|
+
path: filePath,
|
|
338
|
+
reason: error.message,
|
|
339
|
+
watchPaths: this.options.watchPaths.join(', '),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
this._handleError(`Failed to reload plugin: ${filePath}`, enhancedError);
|
|
343
|
+
|
|
344
|
+
this.emit('plugin:reload-failed', {
|
|
345
|
+
path: filePath,
|
|
346
|
+
error: enhancedError.message,
|
|
347
|
+
suggestions: enhancedError.suggestions,
|
|
348
|
+
});
|
|
349
|
+
} finally {
|
|
350
|
+
this.isReloading = false;
|
|
351
|
+
|
|
352
|
+
// Process queued reloads
|
|
353
|
+
await this._processReloadQueue();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Reload configuration
|
|
359
|
+
*
|
|
360
|
+
* @private
|
|
361
|
+
* @returns {Promise<void>}
|
|
362
|
+
*/
|
|
363
|
+
async _reloadConfig() {
|
|
364
|
+
if (this.isReloading) {
|
|
365
|
+
this.reloadQueue.set('config', 'config');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this.isReloading = true;
|
|
370
|
+
const startTime = Date.now();
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
this._log(`Reloading configuration: ${this.options.configPath}`);
|
|
374
|
+
|
|
375
|
+
// Execute before reload hooks
|
|
376
|
+
await this._executeHooks('beforeReload', {
|
|
377
|
+
type: 'config',
|
|
378
|
+
path: this.options.configPath,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Read and parse configuration
|
|
382
|
+
const configContent = fs.readFileSync(this.options.configPath, 'utf-8');
|
|
383
|
+
const config = JSON.parse(configContent);
|
|
384
|
+
|
|
385
|
+
// Validate configuration (if validator provided)
|
|
386
|
+
if (this.options.configValidator) {
|
|
387
|
+
const validation = this.options.configValidator(config);
|
|
388
|
+
if (!validation.valid) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Invalid configuration: ${validation.errors?.map((e) => e.message).join(', ')}`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const reloadTime = Date.now() - startTime;
|
|
396
|
+
|
|
397
|
+
// Update statistics
|
|
398
|
+
this.stats.totalReloads++;
|
|
399
|
+
this.stats.successfulReloads++;
|
|
400
|
+
this.stats.configReloads++;
|
|
401
|
+
this._updateAverageReloadTime(reloadTime);
|
|
402
|
+
|
|
403
|
+
// Record reload
|
|
404
|
+
this._recordReload({
|
|
405
|
+
type: 'config',
|
|
406
|
+
path: this.options.configPath,
|
|
407
|
+
success: true,
|
|
408
|
+
duration: reloadTime,
|
|
409
|
+
timestamp: new Date().toISOString(),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Execute after reload hooks
|
|
413
|
+
await this._executeHooks('afterReload', {
|
|
414
|
+
type: 'config',
|
|
415
|
+
path: this.options.configPath,
|
|
416
|
+
config,
|
|
417
|
+
duration: reloadTime,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
this.emit('config:reloaded', {
|
|
421
|
+
path: this.options.configPath,
|
|
422
|
+
config,
|
|
423
|
+
duration: reloadTime,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
this._log(
|
|
427
|
+
`Configuration reloaded successfully: ${this.options.configPath} (${reloadTime}ms)`,
|
|
428
|
+
);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
this.stats.totalReloads++;
|
|
431
|
+
this.stats.failedReloads++;
|
|
432
|
+
|
|
433
|
+
this._recordReload({
|
|
434
|
+
type: 'config',
|
|
435
|
+
path: this.options.configPath,
|
|
436
|
+
success: false,
|
|
437
|
+
error: error.message,
|
|
438
|
+
timestamp: new Date().toISOString(),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const enhancedError = wrapError(error, ERROR_CODES.HOT_RELOAD_FAILED, {
|
|
442
|
+
path: this.options.configPath,
|
|
443
|
+
reason: error.message,
|
|
444
|
+
watchPaths: this.options.watchPaths.join(', '),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
this._handleError(
|
|
448
|
+
`Failed to reload configuration: ${this.options.configPath}`,
|
|
449
|
+
enhancedError,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
this.emit('config:reload-failed', {
|
|
453
|
+
path: this.options.configPath,
|
|
454
|
+
error: enhancedError.message,
|
|
455
|
+
suggestions: enhancedError.suggestions,
|
|
456
|
+
});
|
|
457
|
+
} finally {
|
|
458
|
+
this.isReloading = false;
|
|
459
|
+
|
|
460
|
+
// Process queued reloads
|
|
461
|
+
await this._processReloadQueue();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Handle plugin removal
|
|
467
|
+
*
|
|
468
|
+
* @private
|
|
469
|
+
* @param {string} filePath - Removed plugin path
|
|
470
|
+
*/
|
|
471
|
+
_handlePluginRemoval(filePath) {
|
|
472
|
+
// Clear from cache
|
|
473
|
+
try {
|
|
474
|
+
const resolvedPath = require.resolve(path.resolve(filePath));
|
|
475
|
+
delete require.cache[resolvedPath];
|
|
476
|
+
} catch (error) {
|
|
477
|
+
// Plugin might not be in cache
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Clear preserved state
|
|
481
|
+
this.pluginState.delete(filePath);
|
|
482
|
+
|
|
483
|
+
// Notify plugin registry
|
|
484
|
+
if (this.pluginRegistry) {
|
|
485
|
+
// Plugin registry should handle removal
|
|
486
|
+
this.emit('plugin:removed:registry', { path: filePath });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Preserve plugin state before reload
|
|
492
|
+
*
|
|
493
|
+
* @private
|
|
494
|
+
* @param {string} filePath - Plugin file path
|
|
495
|
+
* @returns {Promise<any>} Preserved state
|
|
496
|
+
*/
|
|
497
|
+
async _preservePluginState(filePath) {
|
|
498
|
+
try {
|
|
499
|
+
const resolvedPath = require.resolve(path.resolve(filePath));
|
|
500
|
+
const plugin = require.cache[resolvedPath]?.exports;
|
|
501
|
+
|
|
502
|
+
if (!plugin) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let state = null;
|
|
507
|
+
|
|
508
|
+
// Try to get state from plugin
|
|
509
|
+
if (typeof plugin.getState === 'function') {
|
|
510
|
+
state = await plugin.getState();
|
|
511
|
+
} else if (plugin.state) {
|
|
512
|
+
state = { ...plugin.state };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (state) {
|
|
516
|
+
this.pluginState.set(filePath, state);
|
|
517
|
+
|
|
518
|
+
await this._executeHooks('onStatePreserve', {
|
|
519
|
+
path: filePath,
|
|
520
|
+
state,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
this._log(`State preserved for: ${filePath}`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return state;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
const enhancedError = wrapError(
|
|
529
|
+
error,
|
|
530
|
+
ERROR_CODES.HOT_RELOAD_STATE_ERROR,
|
|
531
|
+
{
|
|
532
|
+
reason: error.message,
|
|
533
|
+
},
|
|
534
|
+
);
|
|
535
|
+
this._log(
|
|
536
|
+
`Failed to preserve state for ${filePath}: ${enhancedError.message}`,
|
|
537
|
+
);
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Restore plugin state after reload
|
|
544
|
+
*
|
|
545
|
+
* @private
|
|
546
|
+
* @param {object} plugin - Reloaded plugin
|
|
547
|
+
* @param {any} state - Preserved state
|
|
548
|
+
* @returns {Promise<void>}
|
|
549
|
+
*/
|
|
550
|
+
async _restorePluginState(plugin, state) {
|
|
551
|
+
if (!state) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
// Try to restore state to plugin
|
|
557
|
+
if (typeof plugin.setState === 'function') {
|
|
558
|
+
await plugin.setState(state);
|
|
559
|
+
} else if (plugin.state !== undefined) {
|
|
560
|
+
plugin.state = { ...state };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
await this._executeHooks('onStateRestore', {
|
|
564
|
+
plugin,
|
|
565
|
+
state,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
this._log('State restored successfully');
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const enhancedError = wrapError(
|
|
571
|
+
error,
|
|
572
|
+
ERROR_CODES.HOT_RELOAD_STATE_ERROR,
|
|
573
|
+
{
|
|
574
|
+
reason: error.message,
|
|
575
|
+
},
|
|
576
|
+
);
|
|
577
|
+
this._log(`Failed to restore state: ${enhancedError.message}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Update plugin registry with reloaded plugin
|
|
583
|
+
*
|
|
584
|
+
* @private
|
|
585
|
+
* @param {string} filePath - Plugin file path
|
|
586
|
+
* @param {object} plugin - Reloaded plugin
|
|
587
|
+
* @returns {Promise<void>}
|
|
588
|
+
*/
|
|
589
|
+
async _updatePluginRegistry(filePath, plugin) {
|
|
590
|
+
// Extract plugin name and type from file path or plugin metadata
|
|
591
|
+
const pluginName = path.basename(filePath, path.extname(filePath));
|
|
592
|
+
|
|
593
|
+
// Notify registry of updated plugin
|
|
594
|
+
if (typeof this.pluginRegistry.updatePlugin === 'function') {
|
|
595
|
+
await this.pluginRegistry.updatePlugin(pluginName, plugin);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
this._log(`Plugin registry updated: ${pluginName}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Process queued reloads
|
|
603
|
+
*
|
|
604
|
+
* @private
|
|
605
|
+
* @returns {Promise<void>}
|
|
606
|
+
*/
|
|
607
|
+
async _processReloadQueue() {
|
|
608
|
+
if (this.reloadQueue.size === 0) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const [[key, type]] = this.reloadQueue.entries();
|
|
613
|
+
this.reloadQueue.delete(key);
|
|
614
|
+
|
|
615
|
+
if (type === 'plugin') {
|
|
616
|
+
await this._reloadPlugin(key);
|
|
617
|
+
} else if (type === 'config') {
|
|
618
|
+
await this._reloadConfig();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Execute lifecycle hooks
|
|
624
|
+
*
|
|
625
|
+
* @private
|
|
626
|
+
* @param {string} hookName - Hook name
|
|
627
|
+
* @param {object} data - Hook data
|
|
628
|
+
* @returns {Promise<void>}
|
|
629
|
+
*/
|
|
630
|
+
async _executeHooks(hookName, data) {
|
|
631
|
+
const hooks = this.hooks[hookName] || [];
|
|
632
|
+
|
|
633
|
+
for (const hook of hooks) {
|
|
634
|
+
try {
|
|
635
|
+
await hook(data);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
this._log(`Hook ${hookName} failed: ${error.message}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Record reload in history
|
|
644
|
+
*
|
|
645
|
+
* @private
|
|
646
|
+
* @param {object} record - Reload record
|
|
647
|
+
*/
|
|
648
|
+
_recordReload(record) {
|
|
649
|
+
this.reloadHistory.push(record);
|
|
650
|
+
|
|
651
|
+
// Keep only last 100 records
|
|
652
|
+
if (this.reloadHistory.length > 100) {
|
|
653
|
+
this.reloadHistory.shift();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Update average reload time
|
|
659
|
+
*
|
|
660
|
+
* @private
|
|
661
|
+
* @param {number} duration - Reload duration
|
|
662
|
+
*/
|
|
663
|
+
_updateAverageReloadTime(duration) {
|
|
664
|
+
const total =
|
|
665
|
+
this.stats.averageReloadTime * (this.stats.successfulReloads - 1);
|
|
666
|
+
this.stats.averageReloadTime =
|
|
667
|
+
(total + duration) / this.stats.successfulReloads;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Handle errors
|
|
672
|
+
*
|
|
673
|
+
* @private
|
|
674
|
+
* @param {string} message - Error message
|
|
675
|
+
* @param {Error} error - Error object
|
|
676
|
+
*/
|
|
677
|
+
_handleError(message, error) {
|
|
678
|
+
this._log(`ERROR: ${message}: ${error.message}`, true);
|
|
679
|
+
|
|
680
|
+
this._executeHooks('onError', {
|
|
681
|
+
message,
|
|
682
|
+
error,
|
|
683
|
+
stack: error.stack,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
this.emit('error', {
|
|
687
|
+
message,
|
|
688
|
+
error: error.message,
|
|
689
|
+
stack: error.stack,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Log message
|
|
695
|
+
*
|
|
696
|
+
* @private
|
|
697
|
+
* @param {string} message - Log message
|
|
698
|
+
* @param {boolean} isError - Whether this is an error
|
|
699
|
+
*/
|
|
700
|
+
_log(message, isError = false) {
|
|
701
|
+
if (this.options.verbose || isError) {
|
|
702
|
+
const prefix = '[HotReload]';
|
|
703
|
+
if (isError) {
|
|
704
|
+
console.error(`${prefix} ${message}`);
|
|
705
|
+
} else {
|
|
706
|
+
console.log(`${prefix} ${message}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Register a lifecycle hook
|
|
713
|
+
*
|
|
714
|
+
* @param {string} hookName - Hook name (beforeReload, afterReload, onError, etc.)
|
|
715
|
+
* @param {Function} callback - Hook callback
|
|
716
|
+
*/
|
|
717
|
+
registerHook(hookName, callback) {
|
|
718
|
+
if (!this.hooks[hookName]) {
|
|
719
|
+
throw new Error(`Unknown hook: ${hookName}`);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
this.hooks[hookName].push(callback);
|
|
723
|
+
this._log(`Hook registered: ${hookName}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Unregister a lifecycle hook
|
|
728
|
+
*
|
|
729
|
+
* @param {string} hookName - Hook name
|
|
730
|
+
* @param {Function} callback - Hook callback
|
|
731
|
+
*/
|
|
732
|
+
unregisterHook(hookName, callback) {
|
|
733
|
+
if (!this.hooks[hookName]) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const index = this.hooks[hookName].indexOf(callback);
|
|
738
|
+
if (index !== -1) {
|
|
739
|
+
this.hooks[hookName].splice(index, 1);
|
|
740
|
+
this._log(`Hook unregistered: ${hookName}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Get reload statistics
|
|
746
|
+
*
|
|
747
|
+
* @returns {object} Reload statistics
|
|
748
|
+
*/
|
|
749
|
+
getStats() {
|
|
750
|
+
return {
|
|
751
|
+
...this.stats,
|
|
752
|
+
reloadHistory: [...this.reloadHistory],
|
|
753
|
+
isEnabled: this.options.enabled,
|
|
754
|
+
isReloading: this.isReloading,
|
|
755
|
+
queuedReloads: this.reloadQueue.size,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Get reload history
|
|
761
|
+
*
|
|
762
|
+
* @returns {Array<object>} Reload history
|
|
763
|
+
*/
|
|
764
|
+
getHistory() {
|
|
765
|
+
return [...this.reloadHistory];
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Clear reload history
|
|
770
|
+
*/
|
|
771
|
+
clearHistory() {
|
|
772
|
+
this.reloadHistory = [];
|
|
773
|
+
this._log('Reload history cleared');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Manually trigger a reload
|
|
778
|
+
*
|
|
779
|
+
* @param {string} filePath - File path to reload
|
|
780
|
+
* @param {string} type - Type ('plugin' or 'config')
|
|
781
|
+
* @returns {Promise<void>}
|
|
782
|
+
*/
|
|
783
|
+
async triggerReload(filePath, type = 'plugin') {
|
|
784
|
+
if (type === 'plugin') {
|
|
785
|
+
await this._reloadPlugin(filePath);
|
|
786
|
+
} else if (type === 'config') {
|
|
787
|
+
await this._reloadConfig();
|
|
788
|
+
} else {
|
|
789
|
+
throw new Error(`Unknown reload type: ${type}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Create hot reload manager
|
|
796
|
+
*
|
|
797
|
+
* @param {object} options - Manager options
|
|
798
|
+
* @returns {HotReloadManager} Hot reload manager instance
|
|
799
|
+
*/
|
|
800
|
+
function createHotReloadManager(options = {}) {
|
|
801
|
+
return new HotReloadManager(options);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
module.exports = {
|
|
805
|
+
HotReloadManager,
|
|
806
|
+
createHotReloadManager,
|
|
807
|
+
};
|