@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.
Files changed (40) hide show
  1. package/README.md +260 -19
  2. package/contracts/contract-schema.json +189 -0
  3. package/dist/index.cjs +19 -0
  4. package/dist/index.mjs +15 -1
  5. package/package.json +3 -2
  6. package/src/cli/commands/wizard.js +627 -0
  7. package/src/cli/input-sanitization-middleware.js +356 -0
  8. package/src/cli/templates/development.template.json +64 -0
  9. package/src/cli/templates/minimal.template.json +33 -0
  10. package/src/cli/templates/production.template.json +104 -0
  11. package/src/cli/templates/testing.template.json +45 -0
  12. package/src/config/connection-pool-config.json +259 -0
  13. package/src/config/memory-optimization.js +414 -0
  14. package/src/config/redaction-patterns.json +150 -0
  15. package/src/config/sanitization-rules.json +323 -0
  16. package/src/core/plugin-registry.js +266 -20
  17. package/src/dag/core/error-context.js +391 -0
  18. package/src/dag/core/execution-scheduler.js +350 -0
  19. package/src/dag/core/topology-validation.js +415 -0
  20. package/src/dag/dag-engine.js +26 -424
  21. package/src/dev/README.md +126 -0
  22. package/src/dev/dev-server.js +356 -0
  23. package/src/dev/examples/hot-reload-usage.js +355 -0
  24. package/src/dev/hot-reload.js +807 -0
  25. package/src/enterprise/sso-integration.js +67 -25
  26. package/src/index.js +19 -0
  27. package/src/observability/event-logger.js +62 -2
  28. package/src/plugins/optimized-openai-embedder.js +169 -0
  29. package/src/security/jwt-config.js +431 -0
  30. package/src/security/jwt-validator.js +701 -0
  31. package/src/utils/api-client-pool-integration.js +436 -0
  32. package/src/utils/batch-processor.js +710 -0
  33. package/src/utils/connection-pool.js +605 -0
  34. package/src/utils/contract-validator.js +460 -0
  35. package/src/utils/error-formatter.js +603 -0
  36. package/src/utils/examples/error-formatter-usage.js +351 -0
  37. package/src/utils/input-sanitizer.js +967 -0
  38. package/src/utils/memory-monitor.js +800 -0
  39. package/src/utils/secure-logger-config.js +352 -0
  40. 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
+ };