@objectstack/core 0.9.0 → 0.9.2

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 (91) hide show
  1. package/{ENHANCED_FEATURES.md → ADVANCED_FEATURES.md} +13 -13
  2. package/CHANGELOG.md +15 -0
  3. package/PHASE2_IMPLEMENTATION.md +388 -0
  4. package/README.md +60 -11
  5. package/REFACTORING_SUMMARY.md +40 -0
  6. package/dist/api-registry-plugin.test.js +20 -20
  7. package/dist/dependency-resolver.d.ts +62 -0
  8. package/dist/dependency-resolver.d.ts.map +1 -0
  9. package/dist/dependency-resolver.js +317 -0
  10. package/dist/dependency-resolver.test.d.ts +2 -0
  11. package/dist/dependency-resolver.test.d.ts.map +1 -0
  12. package/dist/dependency-resolver.test.js +241 -0
  13. package/dist/health-monitor.d.ts +65 -0
  14. package/dist/health-monitor.d.ts.map +1 -0
  15. package/dist/health-monitor.js +269 -0
  16. package/dist/health-monitor.test.d.ts +2 -0
  17. package/dist/health-monitor.test.d.ts.map +1 -0
  18. package/dist/health-monitor.test.js +68 -0
  19. package/dist/hot-reload.d.ts +79 -0
  20. package/dist/hot-reload.d.ts.map +1 -0
  21. package/dist/hot-reload.js +313 -0
  22. package/dist/index.d.ts +4 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/kernel-base.d.ts +2 -2
  26. package/dist/kernel-base.js +2 -2
  27. package/dist/kernel.d.ts +79 -31
  28. package/dist/kernel.d.ts.map +1 -1
  29. package/dist/kernel.js +383 -73
  30. package/dist/kernel.test.js +373 -122
  31. package/dist/lite-kernel.d.ts +55 -0
  32. package/dist/lite-kernel.d.ts.map +1 -0
  33. package/dist/lite-kernel.js +112 -0
  34. package/dist/lite-kernel.test.d.ts +2 -0
  35. package/dist/lite-kernel.test.d.ts.map +1 -0
  36. package/dist/lite-kernel.test.js +161 -0
  37. package/dist/logger.d.ts +3 -2
  38. package/dist/logger.d.ts.map +1 -1
  39. package/dist/logger.js +61 -18
  40. package/dist/plugin-loader.d.ts +11 -0
  41. package/dist/plugin-loader.d.ts.map +1 -1
  42. package/dist/plugin-loader.js +34 -10
  43. package/dist/plugin-loader.test.js +9 -0
  44. package/dist/security/index.d.ts +3 -0
  45. package/dist/security/index.d.ts.map +1 -1
  46. package/dist/security/index.js +4 -0
  47. package/dist/security/permission-manager.d.ts +96 -0
  48. package/dist/security/permission-manager.d.ts.map +1 -0
  49. package/dist/security/permission-manager.js +235 -0
  50. package/dist/security/permission-manager.test.d.ts +2 -0
  51. package/dist/security/permission-manager.test.d.ts.map +1 -0
  52. package/dist/security/permission-manager.test.js +220 -0
  53. package/dist/security/plugin-signature-verifier.js +3 -3
  54. package/dist/security/sandbox-runtime.d.ts +115 -0
  55. package/dist/security/sandbox-runtime.d.ts.map +1 -0
  56. package/dist/security/sandbox-runtime.js +310 -0
  57. package/dist/security/security-scanner.d.ts +92 -0
  58. package/dist/security/security-scanner.d.ts.map +1 -0
  59. package/dist/security/security-scanner.js +273 -0
  60. package/examples/{enhanced-kernel-example.ts → kernel-features-example.ts} +6 -6
  61. package/examples/phase2-integration.ts +355 -0
  62. package/package.json +2 -2
  63. package/src/api-registry-plugin.test.ts +20 -20
  64. package/src/dependency-resolver.test.ts +287 -0
  65. package/src/dependency-resolver.ts +388 -0
  66. package/src/health-monitor.test.ts +81 -0
  67. package/src/health-monitor.ts +316 -0
  68. package/src/hot-reload.ts +388 -0
  69. package/src/index.ts +6 -1
  70. package/src/kernel-base.ts +2 -2
  71. package/src/kernel.test.ts +469 -134
  72. package/src/kernel.ts +464 -78
  73. package/src/lite-kernel.test.ts +200 -0
  74. package/src/lite-kernel.ts +135 -0
  75. package/src/logger.ts +64 -18
  76. package/src/plugin-loader.test.ts +10 -1
  77. package/src/plugin-loader.ts +42 -13
  78. package/src/security/index.ts +19 -0
  79. package/src/security/permission-manager.test.ts +256 -0
  80. package/src/security/permission-manager.ts +336 -0
  81. package/src/security/plugin-signature-verifier.ts +3 -3
  82. package/src/security/sandbox-runtime.ts +432 -0
  83. package/src/security/security-scanner.ts +365 -0
  84. package/dist/enhanced-kernel.d.ts +0 -103
  85. package/dist/enhanced-kernel.d.ts.map +0 -1
  86. package/dist/enhanced-kernel.js +0 -403
  87. package/dist/enhanced-kernel.test.d.ts +0 -2
  88. package/dist/enhanced-kernel.test.d.ts.map +0 -1
  89. package/dist/enhanced-kernel.test.js +0 -412
  90. package/src/enhanced-kernel.test.ts +0 -535
  91. package/src/enhanced-kernel.ts +0 -496
@@ -0,0 +1,241 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { SemanticVersionManager, DependencyResolver } from './dependency-resolver.js';
3
+ import { createLogger } from './logger.js';
4
+ describe('SemanticVersionManager', () => {
5
+ describe('parse', () => {
6
+ it('should parse standard semver', () => {
7
+ const version = SemanticVersionManager.parse('1.2.3');
8
+ expect(version).toEqual({
9
+ major: 1,
10
+ minor: 2,
11
+ patch: 3,
12
+ preRelease: undefined,
13
+ build: undefined,
14
+ });
15
+ });
16
+ it('should parse semver with pre-release', () => {
17
+ const version = SemanticVersionManager.parse('1.2.3-alpha.1');
18
+ expect(version).toEqual({
19
+ major: 1,
20
+ minor: 2,
21
+ patch: 3,
22
+ preRelease: 'alpha.1',
23
+ build: undefined,
24
+ });
25
+ });
26
+ it('should parse semver with build metadata', () => {
27
+ const version = SemanticVersionManager.parse('1.2.3+build.123');
28
+ expect(version).toEqual({
29
+ major: 1,
30
+ minor: 2,
31
+ patch: 3,
32
+ preRelease: undefined,
33
+ build: 'build.123',
34
+ });
35
+ });
36
+ it('should parse semver with both pre-release and build', () => {
37
+ const version = SemanticVersionManager.parse('1.2.3-beta.2+build.456');
38
+ expect(version).toEqual({
39
+ major: 1,
40
+ minor: 2,
41
+ patch: 3,
42
+ preRelease: 'beta.2',
43
+ build: 'build.456',
44
+ });
45
+ });
46
+ it('should handle v prefix', () => {
47
+ const version = SemanticVersionManager.parse('v1.2.3');
48
+ expect(version.major).toBe(1);
49
+ expect(version.minor).toBe(2);
50
+ expect(version.patch).toBe(3);
51
+ });
52
+ });
53
+ describe('compare', () => {
54
+ it('should compare major versions', () => {
55
+ const v1 = SemanticVersionManager.parse('2.0.0');
56
+ const v2 = SemanticVersionManager.parse('1.0.0');
57
+ expect(SemanticVersionManager.compare(v1, v2)).toBeGreaterThan(0);
58
+ expect(SemanticVersionManager.compare(v2, v1)).toBeLessThan(0);
59
+ });
60
+ it('should compare minor versions', () => {
61
+ const v1 = SemanticVersionManager.parse('1.2.0');
62
+ const v2 = SemanticVersionManager.parse('1.1.0');
63
+ expect(SemanticVersionManager.compare(v1, v2)).toBeGreaterThan(0);
64
+ });
65
+ it('should compare patch versions', () => {
66
+ const v1 = SemanticVersionManager.parse('1.0.2');
67
+ const v2 = SemanticVersionManager.parse('1.0.1');
68
+ expect(SemanticVersionManager.compare(v1, v2)).toBeGreaterThan(0);
69
+ });
70
+ it('should handle equal versions', () => {
71
+ const v1 = SemanticVersionManager.parse('1.2.3');
72
+ const v2 = SemanticVersionManager.parse('1.2.3');
73
+ expect(SemanticVersionManager.compare(v1, v2)).toBe(0);
74
+ });
75
+ it('should treat pre-release as lower than release', () => {
76
+ const v1 = SemanticVersionManager.parse('1.0.0-alpha');
77
+ const v2 = SemanticVersionManager.parse('1.0.0');
78
+ expect(SemanticVersionManager.compare(v1, v2)).toBeLessThan(0);
79
+ });
80
+ });
81
+ describe('satisfies', () => {
82
+ it('should match exact version', () => {
83
+ const version = SemanticVersionManager.parse('1.2.3');
84
+ expect(SemanticVersionManager.satisfies(version, '1.2.3')).toBe(true);
85
+ expect(SemanticVersionManager.satisfies(version, '1.2.4')).toBe(false);
86
+ });
87
+ it('should match caret range', () => {
88
+ const version = SemanticVersionManager.parse('1.2.5');
89
+ expect(SemanticVersionManager.satisfies(version, '^1.2.3')).toBe(true);
90
+ expect(SemanticVersionManager.satisfies(version, '^1.3.0')).toBe(false);
91
+ expect(SemanticVersionManager.satisfies(version, '^2.0.0')).toBe(false);
92
+ });
93
+ it('should match tilde range', () => {
94
+ const version = SemanticVersionManager.parse('1.2.5');
95
+ expect(SemanticVersionManager.satisfies(version, '~1.2.3')).toBe(true);
96
+ expect(SemanticVersionManager.satisfies(version, '~1.3.0')).toBe(false);
97
+ });
98
+ it('should match greater than or equal', () => {
99
+ const version = SemanticVersionManager.parse('1.2.5');
100
+ expect(SemanticVersionManager.satisfies(version, '>=1.2.3')).toBe(true);
101
+ expect(SemanticVersionManager.satisfies(version, '>=1.2.5')).toBe(true);
102
+ expect(SemanticVersionManager.satisfies(version, '>=1.3.0')).toBe(false);
103
+ });
104
+ it('should match less than', () => {
105
+ const version = SemanticVersionManager.parse('1.2.5');
106
+ expect(SemanticVersionManager.satisfies(version, '<1.3.0')).toBe(true);
107
+ expect(SemanticVersionManager.satisfies(version, '<1.2.5')).toBe(false);
108
+ });
109
+ it('should match range', () => {
110
+ const version = SemanticVersionManager.parse('1.2.5');
111
+ expect(SemanticVersionManager.satisfies(version, '1.2.0 - 1.3.0')).toBe(true);
112
+ expect(SemanticVersionManager.satisfies(version, '1.3.0 - 1.4.0')).toBe(false);
113
+ });
114
+ it('should match wildcard', () => {
115
+ const version = SemanticVersionManager.parse('1.2.5');
116
+ expect(SemanticVersionManager.satisfies(version, '*')).toBe(true);
117
+ expect(SemanticVersionManager.satisfies(version, 'latest')).toBe(true);
118
+ });
119
+ });
120
+ describe('getCompatibilityLevel', () => {
121
+ it('should detect fully compatible versions', () => {
122
+ const from = SemanticVersionManager.parse('1.2.3');
123
+ const to = SemanticVersionManager.parse('1.2.3');
124
+ expect(SemanticVersionManager.getCompatibilityLevel(from, to)).toBe('fully-compatible');
125
+ });
126
+ it('should detect backward compatible versions', () => {
127
+ const from = SemanticVersionManager.parse('1.2.3');
128
+ const to = SemanticVersionManager.parse('1.3.0');
129
+ expect(SemanticVersionManager.getCompatibilityLevel(from, to)).toBe('backward-compatible');
130
+ });
131
+ it('should detect breaking changes', () => {
132
+ const from = SemanticVersionManager.parse('1.2.3');
133
+ const to = SemanticVersionManager.parse('2.0.0');
134
+ expect(SemanticVersionManager.getCompatibilityLevel(from, to)).toBe('breaking-changes');
135
+ });
136
+ it('should detect incompatible (downgrade)', () => {
137
+ const from = SemanticVersionManager.parse('1.3.0');
138
+ const to = SemanticVersionManager.parse('1.2.0');
139
+ expect(SemanticVersionManager.getCompatibilityLevel(from, to)).toBe('incompatible');
140
+ });
141
+ });
142
+ });
143
+ describe('DependencyResolver', () => {
144
+ let resolver;
145
+ let logger;
146
+ beforeEach(() => {
147
+ logger = createLogger({ level: 'silent' });
148
+ resolver = new DependencyResolver(logger);
149
+ });
150
+ describe('resolve', () => {
151
+ it('should resolve dependencies in topological order', () => {
152
+ const plugins = new Map([
153
+ ['a', { dependencies: [] }],
154
+ ['b', { dependencies: ['a'] }],
155
+ ['c', { dependencies: ['a', 'b'] }],
156
+ ]);
157
+ const order = resolver.resolve(plugins);
158
+ expect(order.indexOf('a')).toBeLessThan(order.indexOf('b'));
159
+ expect(order.indexOf('b')).toBeLessThan(order.indexOf('c'));
160
+ });
161
+ it('should handle plugins with no dependencies', () => {
162
+ const plugins = new Map([
163
+ ['a', { dependencies: [] }],
164
+ ['b', { dependencies: [] }],
165
+ ]);
166
+ const order = resolver.resolve(plugins);
167
+ expect(order).toHaveLength(2);
168
+ expect(order).toContain('a');
169
+ expect(order).toContain('b');
170
+ });
171
+ it('should detect circular dependencies', () => {
172
+ const plugins = new Map([
173
+ ['a', { dependencies: ['b'] }],
174
+ ['b', { dependencies: ['a'] }],
175
+ ]);
176
+ expect(() => resolver.resolve(plugins)).toThrow('Circular dependency');
177
+ });
178
+ it('should detect missing dependencies', () => {
179
+ const plugins = new Map([
180
+ ['a', { dependencies: ['missing'] }],
181
+ ]);
182
+ expect(() => resolver.resolve(plugins)).toThrow('Missing dependency');
183
+ });
184
+ });
185
+ describe('detectConflicts', () => {
186
+ it('should detect version mismatches', () => {
187
+ const plugins = new Map([
188
+ ['core', { version: '1.0.0', dependencies: {} }],
189
+ ['plugin-a', { version: '1.0.0', dependencies: { core: '^2.0.0' } }],
190
+ ]);
191
+ const conflicts = resolver.detectConflicts(plugins);
192
+ expect(conflicts.length).toBeGreaterThan(0);
193
+ expect(conflicts[0].type).toBe('version-mismatch');
194
+ });
195
+ it('should return no conflicts for compatible versions', () => {
196
+ const plugins = new Map([
197
+ ['core', { version: '1.2.0', dependencies: {} }],
198
+ ['plugin-a', { version: '1.0.0', dependencies: { core: '^1.0.0' } }],
199
+ ]);
200
+ const conflicts = resolver.detectConflicts(plugins);
201
+ expect(conflicts.length).toBe(0);
202
+ });
203
+ });
204
+ describe('findBestVersion', () => {
205
+ it('should find highest matching version', () => {
206
+ const available = ['1.0.0', '1.1.0', '1.2.0', '2.0.0'];
207
+ const constraints = ['^1.0.0'];
208
+ const best = resolver.findBestVersion(available, constraints);
209
+ expect(best).toBe('1.2.0');
210
+ });
211
+ it('should satisfy all constraints', () => {
212
+ const available = ['1.0.0', '1.1.0', '1.2.0', '2.0.0'];
213
+ const constraints = ['^1.0.0', '>=1.1.0', '<2.0.0'];
214
+ const best = resolver.findBestVersion(available, constraints);
215
+ expect(best).toBe('1.2.0');
216
+ });
217
+ it('should return undefined if no version satisfies', () => {
218
+ const available = ['1.0.0', '1.1.0'];
219
+ const constraints = ['^2.0.0'];
220
+ const best = resolver.findBestVersion(available, constraints);
221
+ expect(best).toBeUndefined();
222
+ });
223
+ });
224
+ describe('isAcyclic', () => {
225
+ it('should detect acyclic graph', () => {
226
+ const deps = new Map([
227
+ ['a', []],
228
+ ['b', ['a']],
229
+ ['c', ['a', 'b']],
230
+ ]);
231
+ expect(resolver.isAcyclic(deps)).toBe(true);
232
+ });
233
+ it('should detect cyclic graph', () => {
234
+ const deps = new Map([
235
+ ['a', ['b']],
236
+ ['b', ['a']],
237
+ ]);
238
+ expect(resolver.isAcyclic(deps)).toBe(false);
239
+ });
240
+ });
241
+ });
@@ -0,0 +1,65 @@
1
+ import type { PluginHealthStatus, PluginHealthCheck, PluginHealthReport } from '@objectstack/spec/system';
2
+ import type { ObjectLogger } from './logger.js';
3
+ import type { Plugin } from './types.js';
4
+ /**
5
+ * Plugin Health Monitor
6
+ *
7
+ * Monitors plugin health status and performs automatic recovery actions.
8
+ * Implements the advanced lifecycle health monitoring protocol.
9
+ */
10
+ export declare class PluginHealthMonitor {
11
+ private logger;
12
+ private healthChecks;
13
+ private healthStatus;
14
+ private healthReports;
15
+ private checkIntervals;
16
+ private failureCounters;
17
+ private successCounters;
18
+ private restartAttempts;
19
+ constructor(logger: ObjectLogger);
20
+ /**
21
+ * Register a plugin for health monitoring
22
+ */
23
+ registerPlugin(pluginName: string, config: PluginHealthCheck): void;
24
+ /**
25
+ * Start monitoring a plugin
26
+ */
27
+ startMonitoring(pluginName: string, plugin: Plugin): void;
28
+ /**
29
+ * Stop monitoring a plugin
30
+ */
31
+ stopMonitoring(pluginName: string): void;
32
+ /**
33
+ * Perform a health check on a plugin
34
+ */
35
+ private performHealthCheck;
36
+ /**
37
+ * Attempt to restart a plugin
38
+ */
39
+ private attemptRestart;
40
+ /**
41
+ * Calculate backoff delay for restarts
42
+ */
43
+ private calculateBackoff;
44
+ /**
45
+ * Get current health status of a plugin
46
+ */
47
+ getHealthStatus(pluginName: string): PluginHealthStatus | undefined;
48
+ /**
49
+ * Get latest health report for a plugin
50
+ */
51
+ getHealthReport(pluginName: string): PluginHealthReport | undefined;
52
+ /**
53
+ * Get all health statuses
54
+ */
55
+ getAllHealthStatuses(): Map<string, PluginHealthStatus>;
56
+ /**
57
+ * Shutdown health monitor
58
+ */
59
+ shutdown(): void;
60
+ /**
61
+ * Timeout helper
62
+ */
63
+ private timeout;
64
+ }
65
+ //# sourceMappingURL=health-monitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-monitor.d.ts","sourceRoot":"","sources":["../src/health-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;;GAKG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,aAAa,CAAyC;IAC9D,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,eAAe,CAA6B;IACpD,OAAO,CAAC,eAAe,CAA6B;IACpD,OAAO,CAAC,eAAe,CAA6B;gBAExC,MAAM,EAAE,YAAY;IAIhC;;OAEG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAanE;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAgCzD;;OAEG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IASxC;;OAEG;YACW,kBAAkB;IAoGhC;;OAEG;YACW,cAAc;IAoD5B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAInE;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAInE;;OAEG;IACH,oBAAoB,IAAI,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC;IAIvD;;OAEG;IACH,QAAQ,IAAI,IAAI;IAgBhB;;OAEG;IACH,OAAO,CAAC,OAAO;CAKhB"}
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Plugin Health Monitor
3
+ *
4
+ * Monitors plugin health status and performs automatic recovery actions.
5
+ * Implements the advanced lifecycle health monitoring protocol.
6
+ */
7
+ export class PluginHealthMonitor {
8
+ constructor(logger) {
9
+ this.healthChecks = new Map();
10
+ this.healthStatus = new Map();
11
+ this.healthReports = new Map();
12
+ this.checkIntervals = new Map();
13
+ this.failureCounters = new Map();
14
+ this.successCounters = new Map();
15
+ this.restartAttempts = new Map();
16
+ this.logger = logger.child({ component: 'HealthMonitor' });
17
+ }
18
+ /**
19
+ * Register a plugin for health monitoring
20
+ */
21
+ registerPlugin(pluginName, config) {
22
+ this.healthChecks.set(pluginName, config);
23
+ this.healthStatus.set(pluginName, 'unknown');
24
+ this.failureCounters.set(pluginName, 0);
25
+ this.successCounters.set(pluginName, 0);
26
+ this.restartAttempts.set(pluginName, 0);
27
+ this.logger.info('Plugin registered for health monitoring', {
28
+ plugin: pluginName,
29
+ interval: config.interval
30
+ });
31
+ }
32
+ /**
33
+ * Start monitoring a plugin
34
+ */
35
+ startMonitoring(pluginName, plugin) {
36
+ const config = this.healthChecks.get(pluginName);
37
+ if (!config) {
38
+ this.logger.warn('Cannot start monitoring - plugin not registered', { plugin: pluginName });
39
+ return;
40
+ }
41
+ // Clear any existing interval
42
+ this.stopMonitoring(pluginName);
43
+ // Set up periodic health checks
44
+ const interval = setInterval(() => {
45
+ this.performHealthCheck(pluginName, plugin, config).catch(error => {
46
+ this.logger.error('Health check failed with error', {
47
+ plugin: pluginName,
48
+ error
49
+ });
50
+ });
51
+ }, config.interval);
52
+ this.checkIntervals.set(pluginName, interval);
53
+ this.logger.info('Health monitoring started', { plugin: pluginName });
54
+ // Perform initial health check
55
+ this.performHealthCheck(pluginName, plugin, config).catch(error => {
56
+ this.logger.error('Initial health check failed', {
57
+ plugin: pluginName,
58
+ error
59
+ });
60
+ });
61
+ }
62
+ /**
63
+ * Stop monitoring a plugin
64
+ */
65
+ stopMonitoring(pluginName) {
66
+ const interval = this.checkIntervals.get(pluginName);
67
+ if (interval) {
68
+ clearInterval(interval);
69
+ this.checkIntervals.delete(pluginName);
70
+ this.logger.info('Health monitoring stopped', { plugin: pluginName });
71
+ }
72
+ }
73
+ /**
74
+ * Perform a health check on a plugin
75
+ */
76
+ async performHealthCheck(pluginName, plugin, config) {
77
+ const startTime = Date.now();
78
+ let status = 'healthy';
79
+ let message;
80
+ const checks = [];
81
+ try {
82
+ // Check if plugin has a custom health check method
83
+ if (config.checkMethod && typeof plugin[config.checkMethod] === 'function') {
84
+ const checkResult = await Promise.race([
85
+ plugin[config.checkMethod](),
86
+ this.timeout(config.timeout, `Health check timeout after ${config.timeout}ms`)
87
+ ]);
88
+ if (checkResult === false || (checkResult && checkResult.status === 'unhealthy')) {
89
+ status = 'unhealthy';
90
+ message = checkResult?.message || 'Custom health check failed';
91
+ checks.push({ name: config.checkMethod, status: 'failed', message });
92
+ }
93
+ else {
94
+ checks.push({ name: config.checkMethod, status: 'passed' });
95
+ }
96
+ }
97
+ else {
98
+ // Default health check - just verify plugin is loaded
99
+ checks.push({ name: 'plugin-loaded', status: 'passed' });
100
+ }
101
+ // Update counters based on result
102
+ if (status === 'healthy') {
103
+ this.successCounters.set(pluginName, (this.successCounters.get(pluginName) || 0) + 1);
104
+ this.failureCounters.set(pluginName, 0);
105
+ // Recover from unhealthy state if we have enough successes
106
+ const currentStatus = this.healthStatus.get(pluginName);
107
+ if (currentStatus === 'unhealthy' || currentStatus === 'degraded') {
108
+ const successCount = this.successCounters.get(pluginName) || 0;
109
+ if (successCount >= config.successThreshold) {
110
+ this.healthStatus.set(pluginName, 'healthy');
111
+ this.logger.info('Plugin recovered to healthy state', { plugin: pluginName });
112
+ }
113
+ else {
114
+ this.healthStatus.set(pluginName, 'recovering');
115
+ }
116
+ }
117
+ else {
118
+ this.healthStatus.set(pluginName, 'healthy');
119
+ }
120
+ }
121
+ else {
122
+ this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
123
+ this.successCounters.set(pluginName, 0);
124
+ const failureCount = this.failureCounters.get(pluginName) || 0;
125
+ if (failureCount >= config.failureThreshold) {
126
+ this.healthStatus.set(pluginName, 'unhealthy');
127
+ this.logger.warn('Plugin marked as unhealthy', {
128
+ plugin: pluginName,
129
+ failures: failureCount
130
+ });
131
+ // Attempt auto-restart if configured
132
+ if (config.autoRestart) {
133
+ await this.attemptRestart(pluginName, plugin, config);
134
+ }
135
+ }
136
+ else {
137
+ this.healthStatus.set(pluginName, 'degraded');
138
+ }
139
+ }
140
+ }
141
+ catch (error) {
142
+ status = 'failed';
143
+ message = error instanceof Error ? error.message : 'Unknown error';
144
+ this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
145
+ this.healthStatus.set(pluginName, 'failed');
146
+ checks.push({
147
+ name: 'health-check',
148
+ status: 'failed',
149
+ message: message
150
+ });
151
+ this.logger.error('Health check exception', {
152
+ plugin: pluginName,
153
+ error
154
+ });
155
+ }
156
+ // Create health report
157
+ const report = {
158
+ status: this.healthStatus.get(pluginName) || 'unknown',
159
+ timestamp: new Date().toISOString(),
160
+ message,
161
+ metrics: {
162
+ uptime: Date.now() - startTime,
163
+ },
164
+ checks: checks.length > 0 ? checks : undefined,
165
+ };
166
+ this.healthReports.set(pluginName, report);
167
+ }
168
+ /**
169
+ * Attempt to restart a plugin
170
+ */
171
+ async attemptRestart(pluginName, plugin, config) {
172
+ const attempts = this.restartAttempts.get(pluginName) || 0;
173
+ if (attempts >= config.maxRestartAttempts) {
174
+ this.logger.error('Max restart attempts reached, giving up', {
175
+ plugin: pluginName,
176
+ attempts
177
+ });
178
+ this.healthStatus.set(pluginName, 'failed');
179
+ return;
180
+ }
181
+ this.restartAttempts.set(pluginName, attempts + 1);
182
+ // Calculate backoff delay
183
+ const delay = this.calculateBackoff(attempts, config.restartBackoff);
184
+ this.logger.info('Scheduling plugin restart', {
185
+ plugin: pluginName,
186
+ attempt: attempts + 1,
187
+ delay
188
+ });
189
+ await new Promise(resolve => setTimeout(resolve, delay));
190
+ try {
191
+ // Call destroy and init to restart
192
+ if (plugin.destroy) {
193
+ await plugin.destroy();
194
+ }
195
+ // Note: Full restart would require kernel context
196
+ // This is a simplified version - actual implementation would need kernel integration
197
+ this.logger.info('Plugin restarted', { plugin: pluginName });
198
+ // Reset counters on successful restart
199
+ this.failureCounters.set(pluginName, 0);
200
+ this.successCounters.set(pluginName, 0);
201
+ this.healthStatus.set(pluginName, 'recovering');
202
+ }
203
+ catch (error) {
204
+ this.logger.error('Plugin restart failed', {
205
+ plugin: pluginName,
206
+ error
207
+ });
208
+ this.healthStatus.set(pluginName, 'failed');
209
+ }
210
+ }
211
+ /**
212
+ * Calculate backoff delay for restarts
213
+ */
214
+ calculateBackoff(attempt, strategy) {
215
+ const baseDelay = 1000; // 1 second base
216
+ switch (strategy) {
217
+ case 'fixed':
218
+ return baseDelay;
219
+ case 'linear':
220
+ return baseDelay * (attempt + 1);
221
+ case 'exponential':
222
+ return baseDelay * Math.pow(2, attempt);
223
+ default:
224
+ return baseDelay;
225
+ }
226
+ }
227
+ /**
228
+ * Get current health status of a plugin
229
+ */
230
+ getHealthStatus(pluginName) {
231
+ return this.healthStatus.get(pluginName);
232
+ }
233
+ /**
234
+ * Get latest health report for a plugin
235
+ */
236
+ getHealthReport(pluginName) {
237
+ return this.healthReports.get(pluginName);
238
+ }
239
+ /**
240
+ * Get all health statuses
241
+ */
242
+ getAllHealthStatuses() {
243
+ return new Map(this.healthStatus);
244
+ }
245
+ /**
246
+ * Shutdown health monitor
247
+ */
248
+ shutdown() {
249
+ // Stop all monitoring intervals
250
+ for (const pluginName of this.checkIntervals.keys()) {
251
+ this.stopMonitoring(pluginName);
252
+ }
253
+ this.healthChecks.clear();
254
+ this.healthStatus.clear();
255
+ this.healthReports.clear();
256
+ this.failureCounters.clear();
257
+ this.successCounters.clear();
258
+ this.restartAttempts.clear();
259
+ this.logger.info('Health monitor shutdown complete');
260
+ }
261
+ /**
262
+ * Timeout helper
263
+ */
264
+ timeout(ms, message) {
265
+ return new Promise((_, reject) => {
266
+ setTimeout(() => reject(new Error(message)), ms);
267
+ });
268
+ }
269
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=health-monitor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-monitor.test.d.ts","sourceRoot":"","sources":["../src/health-monitor.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PluginHealthMonitor } from './health-monitor.js';
3
+ import { createLogger } from './logger.js';
4
+ describe('PluginHealthMonitor', () => {
5
+ let monitor;
6
+ let logger;
7
+ beforeEach(() => {
8
+ logger = createLogger({ level: 'silent' });
9
+ monitor = new PluginHealthMonitor(logger);
10
+ });
11
+ it('should register plugin for health monitoring', () => {
12
+ const config = {
13
+ interval: 5000,
14
+ timeout: 1000,
15
+ failureThreshold: 3,
16
+ successThreshold: 1,
17
+ autoRestart: false,
18
+ maxRestartAttempts: 3,
19
+ restartBackoff: 'exponential',
20
+ };
21
+ monitor.registerPlugin('test-plugin', config);
22
+ expect(monitor.getHealthStatus('test-plugin')).toBe('unknown');
23
+ });
24
+ it('should report healthy status initially', () => {
25
+ const config = {
26
+ interval: 5000,
27
+ timeout: 1000,
28
+ failureThreshold: 3,
29
+ successThreshold: 1,
30
+ autoRestart: false,
31
+ maxRestartAttempts: 3,
32
+ restartBackoff: 'fixed',
33
+ };
34
+ monitor.registerPlugin('test-plugin', config);
35
+ expect(monitor.getHealthStatus('test-plugin')).toBe('unknown');
36
+ });
37
+ it('should get all health statuses', () => {
38
+ const config = {
39
+ interval: 5000,
40
+ timeout: 1000,
41
+ failureThreshold: 3,
42
+ successThreshold: 1,
43
+ autoRestart: false,
44
+ maxRestartAttempts: 3,
45
+ restartBackoff: 'linear',
46
+ };
47
+ monitor.registerPlugin('plugin1', config);
48
+ monitor.registerPlugin('plugin2', config);
49
+ const statuses = monitor.getAllHealthStatuses();
50
+ expect(statuses.size).toBe(2);
51
+ expect(statuses.has('plugin1')).toBe(true);
52
+ expect(statuses.has('plugin2')).toBe(true);
53
+ });
54
+ it('should shutdown cleanly', () => {
55
+ const config = {
56
+ interval: 5000,
57
+ timeout: 1000,
58
+ failureThreshold: 3,
59
+ successThreshold: 1,
60
+ autoRestart: false,
61
+ maxRestartAttempts: 3,
62
+ restartBackoff: 'exponential',
63
+ };
64
+ monitor.registerPlugin('test-plugin', config);
65
+ monitor.shutdown();
66
+ expect(monitor.getAllHealthStatuses().size).toBe(0);
67
+ });
68
+ });