@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,81 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PluginHealthMonitor } from './health-monitor.js';
3
+ import { createLogger } from './logger.js';
4
+ import type { PluginHealthCheck } from '@objectstack/spec/system';
5
+
6
+ describe('PluginHealthMonitor', () => {
7
+ let monitor: PluginHealthMonitor;
8
+ let logger: ReturnType<typeof createLogger>;
9
+
10
+ beforeEach(() => {
11
+ logger = createLogger({ level: 'silent' });
12
+ monitor = new PluginHealthMonitor(logger);
13
+ });
14
+
15
+ it('should register plugin for health monitoring', () => {
16
+ const config: PluginHealthCheck = {
17
+ interval: 5000,
18
+ timeout: 1000,
19
+ failureThreshold: 3,
20
+ successThreshold: 1,
21
+ autoRestart: false,
22
+ maxRestartAttempts: 3,
23
+ restartBackoff: 'exponential',
24
+ };
25
+
26
+ monitor.registerPlugin('test-plugin', config);
27
+ expect(monitor.getHealthStatus('test-plugin')).toBe('unknown');
28
+ });
29
+
30
+ it('should report healthy status initially', () => {
31
+ const config: PluginHealthCheck = {
32
+ interval: 5000,
33
+ timeout: 1000,
34
+ failureThreshold: 3,
35
+ successThreshold: 1,
36
+ autoRestart: false,
37
+ maxRestartAttempts: 3,
38
+ restartBackoff: 'fixed',
39
+ };
40
+
41
+ monitor.registerPlugin('test-plugin', config);
42
+ expect(monitor.getHealthStatus('test-plugin')).toBe('unknown');
43
+ });
44
+
45
+ it('should get all health statuses', () => {
46
+ const config: PluginHealthCheck = {
47
+ interval: 5000,
48
+ timeout: 1000,
49
+ failureThreshold: 3,
50
+ successThreshold: 1,
51
+ autoRestart: false,
52
+ maxRestartAttempts: 3,
53
+ restartBackoff: 'linear',
54
+ };
55
+
56
+ monitor.registerPlugin('plugin1', config);
57
+ monitor.registerPlugin('plugin2', config);
58
+
59
+ const statuses = monitor.getAllHealthStatuses();
60
+ expect(statuses.size).toBe(2);
61
+ expect(statuses.has('plugin1')).toBe(true);
62
+ expect(statuses.has('plugin2')).toBe(true);
63
+ });
64
+
65
+ it('should shutdown cleanly', () => {
66
+ const config: PluginHealthCheck = {
67
+ interval: 5000,
68
+ timeout: 1000,
69
+ failureThreshold: 3,
70
+ successThreshold: 1,
71
+ autoRestart: false,
72
+ maxRestartAttempts: 3,
73
+ restartBackoff: 'exponential',
74
+ };
75
+
76
+ monitor.registerPlugin('test-plugin', config);
77
+ monitor.shutdown();
78
+
79
+ expect(monitor.getAllHealthStatuses().size).toBe(0);
80
+ });
81
+ });
@@ -0,0 +1,316 @@
1
+ import type {
2
+ PluginHealthStatus,
3
+ PluginHealthCheck,
4
+ PluginHealthReport
5
+ } from '@objectstack/spec/system';
6
+ import type { ObjectLogger } from './logger.js';
7
+ import type { Plugin } from './types.js';
8
+
9
+ /**
10
+ * Plugin Health Monitor
11
+ *
12
+ * Monitors plugin health status and performs automatic recovery actions.
13
+ * Implements the advanced lifecycle health monitoring protocol.
14
+ */
15
+ export class PluginHealthMonitor {
16
+ private logger: ObjectLogger;
17
+ private healthChecks = new Map<string, PluginHealthCheck>();
18
+ private healthStatus = new Map<string, PluginHealthStatus>();
19
+ private healthReports = new Map<string, PluginHealthReport>();
20
+ private checkIntervals = new Map<string, NodeJS.Timeout>();
21
+ private failureCounters = new Map<string, number>();
22
+ private successCounters = new Map<string, number>();
23
+ private restartAttempts = new Map<string, number>();
24
+
25
+ constructor(logger: ObjectLogger) {
26
+ this.logger = logger.child({ component: 'HealthMonitor' });
27
+ }
28
+
29
+ /**
30
+ * Register a plugin for health monitoring
31
+ */
32
+ registerPlugin(pluginName: string, config: PluginHealthCheck): void {
33
+ this.healthChecks.set(pluginName, config);
34
+ this.healthStatus.set(pluginName, 'unknown');
35
+ this.failureCounters.set(pluginName, 0);
36
+ this.successCounters.set(pluginName, 0);
37
+ this.restartAttempts.set(pluginName, 0);
38
+
39
+ this.logger.info('Plugin registered for health monitoring', {
40
+ plugin: pluginName,
41
+ interval: config.interval
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Start monitoring a plugin
47
+ */
48
+ startMonitoring(pluginName: string, plugin: Plugin): void {
49
+ const config = this.healthChecks.get(pluginName);
50
+ if (!config) {
51
+ this.logger.warn('Cannot start monitoring - plugin not registered', { plugin: pluginName });
52
+ return;
53
+ }
54
+
55
+ // Clear any existing interval
56
+ this.stopMonitoring(pluginName);
57
+
58
+ // Set up periodic health checks
59
+ const interval = setInterval(() => {
60
+ this.performHealthCheck(pluginName, plugin, config).catch(error => {
61
+ this.logger.error('Health check failed with error', {
62
+ plugin: pluginName,
63
+ error
64
+ });
65
+ });
66
+ }, config.interval);
67
+
68
+ this.checkIntervals.set(pluginName, interval);
69
+ this.logger.info('Health monitoring started', { plugin: pluginName });
70
+
71
+ // Perform initial health check
72
+ this.performHealthCheck(pluginName, plugin, config).catch(error => {
73
+ this.logger.error('Initial health check failed', {
74
+ plugin: pluginName,
75
+ error
76
+ });
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Stop monitoring a plugin
82
+ */
83
+ stopMonitoring(pluginName: string): void {
84
+ const interval = this.checkIntervals.get(pluginName);
85
+ if (interval) {
86
+ clearInterval(interval);
87
+ this.checkIntervals.delete(pluginName);
88
+ this.logger.info('Health monitoring stopped', { plugin: pluginName });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Perform a health check on a plugin
94
+ */
95
+ private async performHealthCheck(
96
+ pluginName: string,
97
+ plugin: Plugin,
98
+ config: PluginHealthCheck
99
+ ): Promise<void> {
100
+ const startTime = Date.now();
101
+ let status: PluginHealthStatus = 'healthy';
102
+ let message: string | undefined;
103
+ const checks: Array<{ name: string; status: 'passed' | 'failed' | 'warning'; message?: string }> = [];
104
+
105
+ try {
106
+ // Check if plugin has a custom health check method
107
+ if (config.checkMethod && typeof (plugin as any)[config.checkMethod] === 'function') {
108
+ const checkResult = await Promise.race([
109
+ (plugin as any)[config.checkMethod](),
110
+ this.timeout(config.timeout, `Health check timeout after ${config.timeout}ms`)
111
+ ]);
112
+
113
+ if (checkResult === false || (checkResult && checkResult.status === 'unhealthy')) {
114
+ status = 'unhealthy';
115
+ message = checkResult?.message || 'Custom health check failed';
116
+ checks.push({ name: config.checkMethod, status: 'failed', message });
117
+ } else {
118
+ checks.push({ name: config.checkMethod, status: 'passed' });
119
+ }
120
+ } else {
121
+ // Default health check - just verify plugin is loaded
122
+ checks.push({ name: 'plugin-loaded', status: 'passed' });
123
+ }
124
+
125
+ // Update counters based on result
126
+ if (status === 'healthy') {
127
+ this.successCounters.set(pluginName, (this.successCounters.get(pluginName) || 0) + 1);
128
+ this.failureCounters.set(pluginName, 0);
129
+
130
+ // Recover from unhealthy state if we have enough successes
131
+ const currentStatus = this.healthStatus.get(pluginName);
132
+ if (currentStatus === 'unhealthy' || currentStatus === 'degraded') {
133
+ const successCount = this.successCounters.get(pluginName) || 0;
134
+ if (successCount >= config.successThreshold) {
135
+ this.healthStatus.set(pluginName, 'healthy');
136
+ this.logger.info('Plugin recovered to healthy state', { plugin: pluginName });
137
+ } else {
138
+ this.healthStatus.set(pluginName, 'recovering');
139
+ }
140
+ } else {
141
+ this.healthStatus.set(pluginName, 'healthy');
142
+ }
143
+ } else {
144
+ this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
145
+ this.successCounters.set(pluginName, 0);
146
+
147
+ const failureCount = this.failureCounters.get(pluginName) || 0;
148
+ if (failureCount >= config.failureThreshold) {
149
+ this.healthStatus.set(pluginName, 'unhealthy');
150
+ this.logger.warn('Plugin marked as unhealthy', {
151
+ plugin: pluginName,
152
+ failures: failureCount
153
+ });
154
+
155
+ // Attempt auto-restart if configured
156
+ if (config.autoRestart) {
157
+ await this.attemptRestart(pluginName, plugin, config);
158
+ }
159
+ } else {
160
+ this.healthStatus.set(pluginName, 'degraded');
161
+ }
162
+ }
163
+ } catch (error) {
164
+ status = 'failed';
165
+ message = error instanceof Error ? error.message : 'Unknown error';
166
+ this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
167
+ this.healthStatus.set(pluginName, 'failed');
168
+
169
+ checks.push({
170
+ name: 'health-check',
171
+ status: 'failed',
172
+ message: message
173
+ });
174
+
175
+ this.logger.error('Health check exception', {
176
+ plugin: pluginName,
177
+ error
178
+ });
179
+ }
180
+
181
+ // Create health report
182
+ const report: PluginHealthReport = {
183
+ status: this.healthStatus.get(pluginName) || 'unknown',
184
+ timestamp: new Date().toISOString(),
185
+ message,
186
+ metrics: {
187
+ uptime: Date.now() - startTime,
188
+ },
189
+ checks: checks.length > 0 ? checks : undefined,
190
+ };
191
+
192
+ this.healthReports.set(pluginName, report);
193
+ }
194
+
195
+ /**
196
+ * Attempt to restart a plugin
197
+ */
198
+ private async attemptRestart(
199
+ pluginName: string,
200
+ plugin: Plugin,
201
+ config: PluginHealthCheck
202
+ ): Promise<void> {
203
+ const attempts = this.restartAttempts.get(pluginName) || 0;
204
+
205
+ if (attempts >= config.maxRestartAttempts) {
206
+ this.logger.error('Max restart attempts reached, giving up', {
207
+ plugin: pluginName,
208
+ attempts
209
+ });
210
+ this.healthStatus.set(pluginName, 'failed');
211
+ return;
212
+ }
213
+
214
+ this.restartAttempts.set(pluginName, attempts + 1);
215
+
216
+ // Calculate backoff delay
217
+ const delay = this.calculateBackoff(attempts, config.restartBackoff);
218
+
219
+ this.logger.info('Scheduling plugin restart', {
220
+ plugin: pluginName,
221
+ attempt: attempts + 1,
222
+ delay
223
+ });
224
+
225
+ await new Promise(resolve => setTimeout(resolve, delay));
226
+
227
+ try {
228
+ // Call destroy and init to restart
229
+ if (plugin.destroy) {
230
+ await plugin.destroy();
231
+ }
232
+
233
+ // Note: Full restart would require kernel context
234
+ // This is a simplified version - actual implementation would need kernel integration
235
+ this.logger.info('Plugin restarted', { plugin: pluginName });
236
+
237
+ // Reset counters on successful restart
238
+ this.failureCounters.set(pluginName, 0);
239
+ this.successCounters.set(pluginName, 0);
240
+ this.healthStatus.set(pluginName, 'recovering');
241
+ } catch (error) {
242
+ this.logger.error('Plugin restart failed', {
243
+ plugin: pluginName,
244
+ error
245
+ });
246
+ this.healthStatus.set(pluginName, 'failed');
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Calculate backoff delay for restarts
252
+ */
253
+ private calculateBackoff(attempt: number, strategy: 'fixed' | 'linear' | 'exponential'): number {
254
+ const baseDelay = 1000; // 1 second base
255
+
256
+ switch (strategy) {
257
+ case 'fixed':
258
+ return baseDelay;
259
+ case 'linear':
260
+ return baseDelay * (attempt + 1);
261
+ case 'exponential':
262
+ return baseDelay * Math.pow(2, attempt);
263
+ default:
264
+ return baseDelay;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Get current health status of a plugin
270
+ */
271
+ getHealthStatus(pluginName: string): PluginHealthStatus | undefined {
272
+ return this.healthStatus.get(pluginName);
273
+ }
274
+
275
+ /**
276
+ * Get latest health report for a plugin
277
+ */
278
+ getHealthReport(pluginName: string): PluginHealthReport | undefined {
279
+ return this.healthReports.get(pluginName);
280
+ }
281
+
282
+ /**
283
+ * Get all health statuses
284
+ */
285
+ getAllHealthStatuses(): Map<string, PluginHealthStatus> {
286
+ return new Map(this.healthStatus);
287
+ }
288
+
289
+ /**
290
+ * Shutdown health monitor
291
+ */
292
+ shutdown(): void {
293
+ // Stop all monitoring intervals
294
+ for (const pluginName of this.checkIntervals.keys()) {
295
+ this.stopMonitoring(pluginName);
296
+ }
297
+
298
+ this.healthChecks.clear();
299
+ this.healthStatus.clear();
300
+ this.healthReports.clear();
301
+ this.failureCounters.clear();
302
+ this.successCounters.clear();
303
+ this.restartAttempts.clear();
304
+
305
+ this.logger.info('Health monitor shutdown complete');
306
+ }
307
+
308
+ /**
309
+ * Timeout helper
310
+ */
311
+ private timeout<T>(ms: number, message: string): Promise<T> {
312
+ return new Promise((_, reject) => {
313
+ setTimeout(() => reject(new Error(message)), ms);
314
+ });
315
+ }
316
+ }