@objectstack/core 0.9.1 → 1.0.0
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 +21 -0
- package/PHASE2_IMPLEMENTATION.md +388 -0
- package/README.md +12 -341
- package/REFACTORING_SUMMARY.md +40 -0
- package/dist/api-registry-plugin.test.js +23 -21
- package/dist/api-registry.test.js +2 -2
- 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 +89 -31
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +430 -73
- package/dist/kernel.test.js +375 -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 +2 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +26 -7
- package/dist/plugin-loader.d.ts +15 -0
- package/dist/plugin-loader.d.ts.map +1 -1
- package/dist/plugin-loader.js +40 -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-permission-enforcer.d.ts +1 -1
- 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 +3 -2
- package/src/api-registry-plugin.test.ts +23 -21
- package/src/api-registry.test.ts +2 -2
- 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 +471 -134
- package/src/kernel.ts +518 -76
- package/src/lite-kernel.test.ts +200 -0
- package/src/lite-kernel.ts +135 -0
- package/src/logger.ts +28 -7
- package/src/plugin-loader.test.ts +10 -1
- package/src/plugin-loader.ts +49 -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-permission-enforcer.test.ts +1 -1
- package/src/security/plugin-permission-enforcer.ts +1 -1
- 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
package/src/kernel.ts
CHANGED
|
@@ -1,135 +1,577 @@
|
|
|
1
|
-
import { Plugin } from './types.js';
|
|
1
|
+
import { Plugin, PluginContext } from './types.js';
|
|
2
2
|
import { createLogger, ObjectLogger } from './logger.js';
|
|
3
3
|
import type { LoggerConfig } from '@objectstack/spec/system';
|
|
4
|
-
import {
|
|
4
|
+
import { ServiceRequirementDef } from '@objectstack/spec/system';
|
|
5
|
+
import { PluginLoader, PluginMetadata, ServiceLifecycle, ServiceFactory, PluginStartupResult } from './plugin-loader.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
* Enhanced Kernel Configuration
|
|
9
|
+
*/
|
|
10
|
+
export interface ObjectKernelConfig {
|
|
11
|
+
logger?: Partial<LoggerConfig>;
|
|
12
|
+
|
|
13
|
+
/** Default plugin startup timeout in milliseconds */
|
|
14
|
+
defaultStartupTimeout?: number;
|
|
15
|
+
|
|
16
|
+
/** Whether to enable graceful shutdown */
|
|
17
|
+
gracefulShutdown?: boolean;
|
|
18
|
+
|
|
19
|
+
/** Graceful shutdown timeout in milliseconds */
|
|
20
|
+
shutdownTimeout?: number;
|
|
21
|
+
|
|
22
|
+
/** Whether to rollback on startup failure */
|
|
23
|
+
rollbackOnFailure?: boolean;
|
|
24
|
+
|
|
25
|
+
/** Whether to skip strict system requirement validation (Critical for testing) */
|
|
26
|
+
skipSystemValidation?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enhanced ObjectKernel with Advanced Plugin Management
|
|
15
31
|
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
32
|
+
* Extends the basic ObjectKernel with:
|
|
33
|
+
* - Async plugin loading with validation
|
|
34
|
+
* - Version compatibility checking
|
|
35
|
+
* - Plugin signature verification
|
|
36
|
+
* - Configuration validation (Zod)
|
|
37
|
+
* - Factory-based dependency injection
|
|
38
|
+
* - Service lifecycle management (singleton/transient/scoped)
|
|
39
|
+
* - Circular dependency detection
|
|
40
|
+
* - Lazy loading services
|
|
41
|
+
* - Graceful shutdown
|
|
42
|
+
* - Plugin startup timeout control
|
|
43
|
+
* - Startup failure rollback
|
|
44
|
+
* - Plugin health checks
|
|
20
45
|
*/
|
|
21
|
-
export class ObjectKernel
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
export class ObjectKernel {
|
|
47
|
+
private plugins: Map<string, PluginMetadata> = new Map();
|
|
48
|
+
private services: Map<string, any> = new Map();
|
|
49
|
+
private hooks: Map<string, Array<(...args: any[]) => void | Promise<void>>> = new Map();
|
|
50
|
+
private state: 'idle' | 'initializing' | 'running' | 'stopping' | 'stopped' = 'idle';
|
|
51
|
+
private logger: ObjectLogger;
|
|
52
|
+
private context: PluginContext;
|
|
53
|
+
private pluginLoader: PluginLoader;
|
|
54
|
+
private config: ObjectKernelConfig;
|
|
55
|
+
private startedPlugins: Set<string> = new Set();
|
|
56
|
+
private pluginStartTimes: Map<string, number> = new Map();
|
|
57
|
+
private shutdownHandlers: Array<() => Promise<void>> = [];
|
|
58
|
+
|
|
59
|
+
constructor(config: ObjectKernelConfig = {}) {
|
|
60
|
+
this.config = {
|
|
61
|
+
defaultStartupTimeout: 30000, // 30 seconds
|
|
62
|
+
gracefulShutdown: true,
|
|
63
|
+
shutdownTimeout: 60000, // 60 seconds
|
|
64
|
+
rollbackOnFailure: true,
|
|
65
|
+
...config,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this.logger = createLogger(config.logger);
|
|
69
|
+
this.pluginLoader = new PluginLoader(this.logger);
|
|
25
70
|
|
|
26
|
-
// Initialize context
|
|
27
|
-
this.context =
|
|
71
|
+
// Initialize context
|
|
72
|
+
this.context = {
|
|
73
|
+
registerService: (name, service) => {
|
|
74
|
+
this.registerService(name, service);
|
|
75
|
+
},
|
|
76
|
+
getService: <T>(name: string) => {
|
|
77
|
+
// 1. Try direct service map first (synchronous cache)
|
|
78
|
+
const service = this.services.get(name);
|
|
79
|
+
if (service) {
|
|
80
|
+
return service as T;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Try to get from plugin loader cache (Sync access to factories)
|
|
84
|
+
const loaderService = this.pluginLoader.getServiceInstance<T>(name);
|
|
85
|
+
if (loaderService) {
|
|
86
|
+
// Cache it locally for faster next access
|
|
87
|
+
this.services.set(name, loaderService);
|
|
88
|
+
return loaderService;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Try to get from plugin loader (support async factories)
|
|
92
|
+
try {
|
|
93
|
+
const service = this.pluginLoader.getService(name);
|
|
94
|
+
if (service instanceof Promise) {
|
|
95
|
+
// If we found it in the loader but not in the sync map, it's likely a factory-based service or still loading
|
|
96
|
+
throw new Error(`Service '${name}' is async - use await`);
|
|
97
|
+
}
|
|
98
|
+
return service as T;
|
|
99
|
+
} catch (error: any) {
|
|
100
|
+
if (error.message?.includes('is async')) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Re-throw critical factory errors instead of masking them as "not found"
|
|
105
|
+
// If the error came from the factory execution (e.g. database connection failed), we must see it.
|
|
106
|
+
// "Service '${name}' not found" comes from PluginLoader.getService fallback.
|
|
107
|
+
const isNotFoundError = error.message === `Service '${name}' not found`;
|
|
108
|
+
|
|
109
|
+
if (!isNotFoundError) {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
throw new Error(`[Kernel] Service '${name}' not found`);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
hook: (name, handler) => {
|
|
117
|
+
if (!this.hooks.has(name)) {
|
|
118
|
+
this.hooks.set(name, []);
|
|
119
|
+
}
|
|
120
|
+
this.hooks.get(name)!.push(handler);
|
|
121
|
+
},
|
|
122
|
+
trigger: async (name, ...args) => {
|
|
123
|
+
const handlers = this.hooks.get(name) || [];
|
|
124
|
+
for (const handler of handlers) {
|
|
125
|
+
await handler(...args);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
getServices: () => {
|
|
129
|
+
return new Map(this.services);
|
|
130
|
+
},
|
|
131
|
+
logger: this.logger,
|
|
132
|
+
getKernel: () => this as any, // Type compatibility
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
this.pluginLoader.setContext(this.context);
|
|
136
|
+
|
|
137
|
+
// Register shutdown handler
|
|
138
|
+
if (this.config.gracefulShutdown) {
|
|
139
|
+
this.registerShutdownSignals();
|
|
140
|
+
}
|
|
28
141
|
}
|
|
29
142
|
|
|
30
143
|
/**
|
|
31
|
-
* Register a plugin
|
|
32
|
-
* @param plugin - Plugin instance
|
|
144
|
+
* Register a plugin with enhanced validation
|
|
33
145
|
*/
|
|
34
|
-
use(plugin: Plugin): this {
|
|
35
|
-
this.
|
|
146
|
+
async use(plugin: Plugin): Promise<this> {
|
|
147
|
+
if (this.state !== 'idle') {
|
|
148
|
+
throw new Error('[Kernel] Cannot register plugins after bootstrap has started');
|
|
149
|
+
}
|
|
36
150
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
151
|
+
// Load plugin through enhanced loader
|
|
152
|
+
const result = await this.pluginLoader.loadPlugin(plugin);
|
|
153
|
+
|
|
154
|
+
if (!result.success || !result.plugin) {
|
|
155
|
+
throw new Error(`Failed to load plugin: ${plugin.name} - ${result.error?.message}`);
|
|
40
156
|
}
|
|
41
157
|
|
|
42
|
-
|
|
158
|
+
const pluginMeta = result.plugin;
|
|
159
|
+
this.plugins.set(pluginMeta.name, pluginMeta);
|
|
160
|
+
|
|
161
|
+
this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
|
|
162
|
+
plugin: pluginMeta.name,
|
|
163
|
+
version: pluginMeta.version,
|
|
164
|
+
});
|
|
165
|
+
|
|
43
166
|
return this;
|
|
44
167
|
}
|
|
45
168
|
|
|
46
169
|
/**
|
|
47
|
-
*
|
|
48
|
-
* 1. Resolve dependencies (topological sort)
|
|
49
|
-
* 2. Init phase - plugins register services
|
|
50
|
-
* 3. Start phase - plugins execute business logic
|
|
51
|
-
* 4. Trigger 'kernel:ready' hook
|
|
170
|
+
* Register a service instance directly
|
|
52
171
|
*/
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.
|
|
172
|
+
registerService<T>(name: string, service: T): this {
|
|
173
|
+
if (this.services.has(name)) {
|
|
174
|
+
throw new Error(`[Kernel] Service '${name}' already registered`);
|
|
175
|
+
}
|
|
176
|
+
this.services.set(name, service);
|
|
177
|
+
this.pluginLoader.registerService(name, service);
|
|
178
|
+
this.logger.info(`Service '${name}' registered`, { service: name });
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
58
181
|
|
|
59
|
-
|
|
60
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Register a service factory with lifecycle management
|
|
184
|
+
*/
|
|
185
|
+
registerServiceFactory<T>(
|
|
186
|
+
name: string,
|
|
187
|
+
factory: ServiceFactory<T>,
|
|
188
|
+
lifecycle: ServiceLifecycle = ServiceLifecycle.SINGLETON,
|
|
189
|
+
dependencies?: string[]
|
|
190
|
+
): this {
|
|
191
|
+
this.pluginLoader.registerServiceFactory({
|
|
192
|
+
name,
|
|
193
|
+
factory,
|
|
194
|
+
lifecycle,
|
|
195
|
+
dependencies,
|
|
196
|
+
});
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
61
199
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Validate Critical System Requirements
|
|
202
|
+
*/
|
|
203
|
+
private validateSystemRequirements() {
|
|
204
|
+
if (this.config.skipSystemValidation) {
|
|
205
|
+
this.logger.debug('System requirement validation skipped');
|
|
206
|
+
return;
|
|
66
207
|
}
|
|
67
208
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
209
|
+
this.logger.debug('Validating system service requirements...');
|
|
210
|
+
const missingServices: string[] = [];
|
|
211
|
+
const missingCoreServices: string[] = [];
|
|
71
212
|
|
|
72
|
-
|
|
73
|
-
|
|
213
|
+
// Iterate through all defined requirements
|
|
214
|
+
for (const [serviceName, criticality] of Object.entries(ServiceRequirementDef)) {
|
|
215
|
+
const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
|
|
216
|
+
|
|
217
|
+
if (!hasService) {
|
|
218
|
+
if (criticality === 'required') {
|
|
219
|
+
this.logger.error(`CRITICAL: Required service missing: ${serviceName}`);
|
|
220
|
+
missingServices.push(serviceName);
|
|
221
|
+
} else if (criticality === 'core') {
|
|
222
|
+
this.logger.warn(`CORE: Core service missing, functionality may be degraded: ${serviceName}`);
|
|
223
|
+
missingCoreServices.push(serviceName);
|
|
224
|
+
} else {
|
|
225
|
+
this.logger.info(`Info: Optional service not present: ${serviceName}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
74
228
|
}
|
|
75
229
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
230
|
+
if (missingServices.length > 0) {
|
|
231
|
+
const errorMsg = `System failed to start. Missing critical services: ${missingServices.join(', ')}`;
|
|
232
|
+
this.logger.error(errorMsg);
|
|
233
|
+
throw new Error(errorMsg);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (missingCoreServices.length > 0) {
|
|
237
|
+
this.logger.warn(`System started with degraded capabilities. Missing core services: ${missingCoreServices.join(', ')}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.logger.info('System requirement check passed');
|
|
81
241
|
}
|
|
82
242
|
|
|
83
243
|
/**
|
|
84
|
-
*
|
|
85
|
-
* Calls destroy on all plugins in reverse order
|
|
244
|
+
* Bootstrap the kernel with enhanced features
|
|
86
245
|
*/
|
|
87
|
-
async
|
|
88
|
-
|
|
246
|
+
async bootstrap(): Promise<void> {
|
|
247
|
+
if (this.state !== 'idle') {
|
|
248
|
+
throw new Error('[Kernel] Kernel already bootstrapped');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.state = 'initializing';
|
|
252
|
+
this.logger.info('Bootstrap started');
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// Check for circular dependencies
|
|
256
|
+
const cycles = this.pluginLoader.detectCircularDependencies();
|
|
257
|
+
if (cycles.length > 0) {
|
|
258
|
+
this.logger.warn('Circular service dependencies detected:', { cycles });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Resolve plugin dependencies
|
|
262
|
+
const orderedPlugins = this.resolveDependencies();
|
|
263
|
+
|
|
264
|
+
// Phase 1: Init - Plugins register services
|
|
265
|
+
this.logger.info('Phase 1: Init plugins');
|
|
266
|
+
for (const plugin of orderedPlugins) {
|
|
267
|
+
await this.initPluginWithTimeout(plugin);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Phase 2: Start - Plugins execute business logic
|
|
271
|
+
this.logger.info('Phase 2: Start plugins');
|
|
272
|
+
this.state = 'running';
|
|
273
|
+
|
|
274
|
+
for (const plugin of orderedPlugins) {
|
|
275
|
+
const result = await this.startPluginWithTimeout(plugin);
|
|
276
|
+
|
|
277
|
+
if (!result.success) {
|
|
278
|
+
this.logger.error(`Plugin startup failed: ${plugin.name}`, result.error);
|
|
279
|
+
|
|
280
|
+
if (this.config.rollbackOnFailure) {
|
|
281
|
+
this.logger.warn('Rolling back started plugins...');
|
|
282
|
+
await this.rollbackStartedPlugins();
|
|
283
|
+
throw new Error(`Plugin ${plugin.name} failed to start - rollback complete`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Phase 3: Trigger kernel:ready hook
|
|
289
|
+
this.validateSystemRequirements(); // Final check before ready
|
|
290
|
+
this.logger.debug('Triggering kernel:ready hook');
|
|
291
|
+
await this.context.trigger('kernel:ready');
|
|
292
|
+
|
|
293
|
+
this.logger.info('✅ Bootstrap complete');
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.state = 'stopped';
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
89
298
|
}
|
|
90
299
|
|
|
91
300
|
/**
|
|
92
|
-
* Graceful shutdown
|
|
301
|
+
* Graceful shutdown with timeout
|
|
93
302
|
*/
|
|
94
|
-
async
|
|
95
|
-
if (this.state === 'stopped') {
|
|
96
|
-
this.logger.warn('Kernel already stopped');
|
|
303
|
+
async shutdown(): Promise<void> {
|
|
304
|
+
if (this.state === 'stopped' || this.state === 'stopping') {
|
|
305
|
+
this.logger.warn('Kernel already stopped or stopping');
|
|
97
306
|
return;
|
|
98
307
|
}
|
|
99
308
|
|
|
309
|
+
if (this.state !== 'running') {
|
|
310
|
+
throw new Error('[Kernel] Kernel not running');
|
|
311
|
+
}
|
|
312
|
+
|
|
100
313
|
this.state = 'stopping';
|
|
101
|
-
this.logger.info('
|
|
314
|
+
this.logger.info('Graceful shutdown started');
|
|
102
315
|
|
|
103
|
-
|
|
104
|
-
|
|
316
|
+
try {
|
|
317
|
+
// Create shutdown promise with timeout
|
|
318
|
+
const shutdownPromise = this.performShutdown();
|
|
319
|
+
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
320
|
+
setTimeout(() => {
|
|
321
|
+
reject(new Error('Shutdown timeout exceeded'));
|
|
322
|
+
}, this.config.shutdownTimeout);
|
|
323
|
+
});
|
|
105
324
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
325
|
+
// Race between shutdown and timeout
|
|
326
|
+
await Promise.race([shutdownPromise, timeoutPromise]);
|
|
327
|
+
|
|
328
|
+
this.state = 'stopped';
|
|
329
|
+
this.logger.info('✅ Graceful shutdown complete');
|
|
330
|
+
} catch (error) {
|
|
331
|
+
this.logger.error('Shutdown error - forcing stop', error as Error);
|
|
332
|
+
this.state = 'stopped';
|
|
333
|
+
throw error;
|
|
334
|
+
} finally {
|
|
335
|
+
// Cleanup logger resources
|
|
336
|
+
await this.logger.destroy();
|
|
110
337
|
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Check health of a specific plugin
|
|
342
|
+
*/
|
|
343
|
+
async checkPluginHealth(pluginName: string): Promise<any> {
|
|
344
|
+
return await this.pluginLoader.checkPluginHealth(pluginName);
|
|
345
|
+
}
|
|
111
346
|
|
|
112
|
-
|
|
113
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Check health of all plugins
|
|
349
|
+
*/
|
|
350
|
+
async checkAllPluginsHealth(): Promise<Map<string, any>> {
|
|
351
|
+
const results = new Map();
|
|
114
352
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
353
|
+
for (const pluginName of this.plugins.keys()) {
|
|
354
|
+
const health = await this.checkPluginHealth(pluginName);
|
|
355
|
+
results.set(pluginName, health);
|
|
118
356
|
}
|
|
357
|
+
|
|
358
|
+
return results;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get plugin startup metrics
|
|
363
|
+
*/
|
|
364
|
+
getPluginMetrics(): Map<string, number> {
|
|
365
|
+
return new Map(this.pluginStartTimes);
|
|
119
366
|
}
|
|
120
367
|
|
|
121
368
|
/**
|
|
122
|
-
* Get a service
|
|
123
|
-
* Convenience method for external access
|
|
369
|
+
* Get a service (sync helper)
|
|
124
370
|
*/
|
|
125
371
|
getService<T>(name: string): T {
|
|
126
372
|
return this.context.getService<T>(name);
|
|
127
373
|
}
|
|
128
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Get a service asynchronously (supports factories)
|
|
377
|
+
*/
|
|
378
|
+
async getServiceAsync<T>(name: string, scopeId?: string): Promise<T> {
|
|
379
|
+
return await this.pluginLoader.getService<T>(name, scopeId);
|
|
380
|
+
}
|
|
381
|
+
|
|
129
382
|
/**
|
|
130
383
|
* Check if kernel is running
|
|
131
384
|
*/
|
|
132
385
|
isRunning(): boolean {
|
|
133
386
|
return this.state === 'running';
|
|
134
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get kernel state
|
|
391
|
+
*/
|
|
392
|
+
getState(): string {
|
|
393
|
+
return this.state;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Private methods
|
|
397
|
+
|
|
398
|
+
private async initPluginWithTimeout(plugin: PluginMetadata): Promise<void> {
|
|
399
|
+
const timeout = plugin.startupTimeout || this.config.defaultStartupTimeout!;
|
|
400
|
+
|
|
401
|
+
this.logger.debug(`Init: ${plugin.name}`, { plugin: plugin.name });
|
|
402
|
+
|
|
403
|
+
const initPromise = plugin.init(this.context);
|
|
404
|
+
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
reject(new Error(`Plugin ${plugin.name} init timeout after ${timeout}ms`));
|
|
407
|
+
}, timeout);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await Promise.race([initPromise, timeoutPromise]);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private async startPluginWithTimeout(plugin: PluginMetadata): Promise<PluginStartupResult> {
|
|
414
|
+
if (!plugin.start) {
|
|
415
|
+
return { success: true, pluginName: plugin.name };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const timeout = plugin.startupTimeout || this.config.defaultStartupTimeout!;
|
|
419
|
+
const startTime = Date.now();
|
|
420
|
+
|
|
421
|
+
this.logger.debug(`Start: ${plugin.name}`, { plugin: plugin.name });
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const startPromise = plugin.start(this.context);
|
|
425
|
+
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
426
|
+
setTimeout(() => {
|
|
427
|
+
reject(new Error(`Plugin ${plugin.name} start timeout after ${timeout}ms`));
|
|
428
|
+
}, timeout);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
await Promise.race([startPromise, timeoutPromise]);
|
|
432
|
+
|
|
433
|
+
const duration = Date.now() - startTime;
|
|
434
|
+
this.startedPlugins.add(plugin.name);
|
|
435
|
+
this.pluginStartTimes.set(plugin.name, duration);
|
|
436
|
+
|
|
437
|
+
this.logger.debug(`Plugin started: ${plugin.name} (${duration}ms)`);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
pluginName: plugin.name,
|
|
442
|
+
startTime: duration,
|
|
443
|
+
};
|
|
444
|
+
} catch (error) {
|
|
445
|
+
const duration = Date.now() - startTime;
|
|
446
|
+
const isTimeout = (error as Error).message.includes('timeout');
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
success: false,
|
|
450
|
+
pluginName: plugin.name,
|
|
451
|
+
error: error as Error,
|
|
452
|
+
startTime: duration,
|
|
453
|
+
timedOut: isTimeout,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private async rollbackStartedPlugins(): Promise<void> {
|
|
459
|
+
const pluginsToRollback = Array.from(this.startedPlugins).reverse();
|
|
460
|
+
|
|
461
|
+
for (const pluginName of pluginsToRollback) {
|
|
462
|
+
const plugin = this.plugins.get(pluginName);
|
|
463
|
+
if (plugin?.destroy) {
|
|
464
|
+
try {
|
|
465
|
+
this.logger.debug(`Rollback: ${pluginName}`);
|
|
466
|
+
await plugin.destroy();
|
|
467
|
+
} catch (error) {
|
|
468
|
+
this.logger.error(`Rollback failed for ${pluginName}`, error as Error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
this.startedPlugins.clear();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async performShutdown(): Promise<void> {
|
|
477
|
+
// Trigger shutdown hook
|
|
478
|
+
await this.context.trigger('kernel:shutdown');
|
|
479
|
+
|
|
480
|
+
// Destroy plugins in reverse order
|
|
481
|
+
const orderedPlugins = Array.from(this.plugins.values()).reverse();
|
|
482
|
+
for (const plugin of orderedPlugins) {
|
|
483
|
+
if (plugin.destroy) {
|
|
484
|
+
this.logger.debug(`Destroy: ${plugin.name}`, { plugin: plugin.name });
|
|
485
|
+
try {
|
|
486
|
+
await plugin.destroy();
|
|
487
|
+
} catch (error) {
|
|
488
|
+
this.logger.error(`Error destroying plugin ${plugin.name}`, error as Error);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Execute custom shutdown handlers
|
|
494
|
+
for (const handler of this.shutdownHandlers) {
|
|
495
|
+
try {
|
|
496
|
+
await handler();
|
|
497
|
+
} catch (error) {
|
|
498
|
+
this.logger.error('Shutdown handler error', error as Error);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private resolveDependencies(): PluginMetadata[] {
|
|
504
|
+
const resolved: PluginMetadata[] = [];
|
|
505
|
+
const visited = new Set<string>();
|
|
506
|
+
const visiting = new Set<string>();
|
|
507
|
+
|
|
508
|
+
const visit = (pluginName: string) => {
|
|
509
|
+
if (visited.has(pluginName)) return;
|
|
510
|
+
|
|
511
|
+
if (visiting.has(pluginName)) {
|
|
512
|
+
throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const plugin = this.plugins.get(pluginName);
|
|
516
|
+
if (!plugin) {
|
|
517
|
+
throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
visiting.add(pluginName);
|
|
521
|
+
|
|
522
|
+
// Visit dependencies first
|
|
523
|
+
const deps = plugin.dependencies || [];
|
|
524
|
+
for (const dep of deps) {
|
|
525
|
+
if (!this.plugins.has(dep)) {
|
|
526
|
+
throw new Error(`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`);
|
|
527
|
+
}
|
|
528
|
+
visit(dep);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
visiting.delete(pluginName);
|
|
532
|
+
visited.add(pluginName);
|
|
533
|
+
resolved.push(plugin);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// Visit all plugins
|
|
537
|
+
for (const pluginName of this.plugins.keys()) {
|
|
538
|
+
visit(pluginName);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return resolved;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private registerShutdownSignals(): void {
|
|
545
|
+
const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
|
|
546
|
+
let shutdownInProgress = false;
|
|
547
|
+
|
|
548
|
+
const handleShutdown = async (signal: string) => {
|
|
549
|
+
if (shutdownInProgress) {
|
|
550
|
+
this.logger.warn(`Shutdown already in progress, ignoring ${signal}`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
shutdownInProgress = true;
|
|
555
|
+
this.logger.info(`Received ${signal} - initiating graceful shutdown`);
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
await this.shutdown();
|
|
559
|
+
process.exit(0);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
this.logger.error('Shutdown failed', error as Error);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
for (const signal of signals) {
|
|
567
|
+
process.on(signal, () => handleShutdown(signal));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Register a custom shutdown handler
|
|
573
|
+
*/
|
|
574
|
+
onShutdown(handler: () => Promise<void>): void {
|
|
575
|
+
this.shutdownHandlers.push(handler);
|
|
576
|
+
}
|
|
135
577
|
}
|