@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.
- package/{ENHANCED_FEATURES.md → ADVANCED_FEATURES.md} +13 -13
- package/CHANGELOG.md +15 -0
- package/PHASE2_IMPLEMENTATION.md +388 -0
- package/README.md +60 -11
- package/REFACTORING_SUMMARY.md +40 -0
- package/dist/api-registry-plugin.test.js +20 -20
- package/dist/dependency-resolver.d.ts +62 -0
- package/dist/dependency-resolver.d.ts.map +1 -0
- package/dist/dependency-resolver.js +317 -0
- package/dist/dependency-resolver.test.d.ts +2 -0
- package/dist/dependency-resolver.test.d.ts.map +1 -0
- package/dist/dependency-resolver.test.js +241 -0
- package/dist/health-monitor.d.ts +65 -0
- package/dist/health-monitor.d.ts.map +1 -0
- package/dist/health-monitor.js +269 -0
- package/dist/health-monitor.test.d.ts +2 -0
- package/dist/health-monitor.test.d.ts.map +1 -0
- package/dist/health-monitor.test.js +68 -0
- package/dist/hot-reload.d.ts +79 -0
- package/dist/hot-reload.d.ts.map +1 -0
- package/dist/hot-reload.js +313 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/kernel-base.d.ts +2 -2
- package/dist/kernel-base.js +2 -2
- package/dist/kernel.d.ts +79 -31
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +383 -73
- package/dist/kernel.test.js +373 -122
- package/dist/lite-kernel.d.ts +55 -0
- package/dist/lite-kernel.d.ts.map +1 -0
- package/dist/lite-kernel.js +112 -0
- package/dist/lite-kernel.test.d.ts +2 -0
- package/dist/lite-kernel.test.d.ts.map +1 -0
- package/dist/lite-kernel.test.js +161 -0
- package/dist/logger.d.ts +3 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +61 -18
- package/dist/plugin-loader.d.ts +11 -0
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +34 -10
- package/dist/plugin-loader.test.js +9 -0
- package/dist/security/index.d.ts +3 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +4 -0
- package/dist/security/permission-manager.d.ts +96 -0
- package/dist/security/permission-manager.d.ts.map +1 -0
- package/dist/security/permission-manager.js +235 -0
- package/dist/security/permission-manager.test.d.ts +2 -0
- package/dist/security/permission-manager.test.d.ts.map +1 -0
- package/dist/security/permission-manager.test.js +220 -0
- package/dist/security/plugin-signature-verifier.js +3 -3
- package/dist/security/sandbox-runtime.d.ts +115 -0
- package/dist/security/sandbox-runtime.d.ts.map +1 -0
- package/dist/security/sandbox-runtime.js +310 -0
- package/dist/security/security-scanner.d.ts +92 -0
- package/dist/security/security-scanner.d.ts.map +1 -0
- package/dist/security/security-scanner.js +273 -0
- package/examples/{enhanced-kernel-example.ts → kernel-features-example.ts} +6 -6
- package/examples/phase2-integration.ts +355 -0
- package/package.json +2 -2
- package/src/api-registry-plugin.test.ts +20 -20
- package/src/dependency-resolver.test.ts +287 -0
- package/src/dependency-resolver.ts +388 -0
- package/src/health-monitor.test.ts +81 -0
- package/src/health-monitor.ts +316 -0
- package/src/hot-reload.ts +388 -0
- package/src/index.ts +6 -1
- package/src/kernel-base.ts +2 -2
- package/src/kernel.test.ts +469 -134
- package/src/kernel.ts +464 -78
- package/src/lite-kernel.test.ts +200 -0
- package/src/lite-kernel.ts +135 -0
- package/src/logger.ts +64 -18
- package/src/plugin-loader.test.ts +10 -1
- package/src/plugin-loader.ts +42 -13
- package/src/security/index.ts +19 -0
- package/src/security/permission-manager.test.ts +256 -0
- package/src/security/permission-manager.ts +336 -0
- package/src/security/plugin-signature-verifier.ts +3 -3
- package/src/security/sandbox-runtime.ts +432 -0
- package/src/security/security-scanner.ts +365 -0
- package/dist/enhanced-kernel.d.ts +0 -103
- package/dist/enhanced-kernel.d.ts.map +0 -1
- package/dist/enhanced-kernel.js +0 -403
- package/dist/enhanced-kernel.test.d.ts +0 -2
- package/dist/enhanced-kernel.test.d.ts.map +0 -1
- package/dist/enhanced-kernel.test.js +0 -412
- package/src/enhanced-kernel.test.ts +0 -535
- 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 @@
|
|
|
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
|
+
});
|