-
Notifications
You must be signed in to change notification settings - Fork 2
feat(core): Add production-grade plugin lifecycle management and DI #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f4aef87
Initial plan
Copilot 73f1eec
feat(core): Implement P0 microkernel features - plugin loader, DI, li…
Copilot 47c6255
docs(core): Add comprehensive documentation and examples for enhanced…
Copilot 0fb5d78
fix(core): Address code review feedback - improve error handling and …
Copilot 8926877
docs(core): Add implementation summary document
Copilot 5b7e854
删除 IMPLEMENTATION_SUMMARY.md
hotlong File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,380 @@ | ||
| # Enhanced ObjectKernel - Advanced Plugin Features | ||
|
|
||
| This document describes the enhanced features added to ObjectKernel for production-grade plugin management. | ||
|
|
||
| ## Overview | ||
|
|
||
| The `EnhancedObjectKernel` extends the basic `ObjectKernel` with enterprise-grade features for plugin lifecycle management, dependency injection, and operational resilience. | ||
|
|
||
| ## Features | ||
|
|
||
| ### 1. Enhanced Plugin Loading | ||
|
|
||
| Async plugin loading with comprehensive validation: | ||
|
|
||
| ```typescript | ||
| import { EnhancedObjectKernel, PluginMetadata } from '@objectstack/core'; | ||
|
|
||
| const kernel = new EnhancedObjectKernel({ | ||
| logger: { level: 'info' }, | ||
| defaultStartupTimeout: 30000, // 30 seconds | ||
| gracefulShutdown: true, | ||
| shutdownTimeout: 60000, // 60 seconds | ||
| rollbackOnFailure: true, // Rollback on startup failure | ||
| }); | ||
|
|
||
| // Plugin with version and timeout | ||
| const myPlugin: PluginMetadata = { | ||
| name: 'my-plugin', | ||
| version: '1.2.3', // Semantic version required | ||
| startupTimeout: 10000, // Override default timeout | ||
|
|
||
| async init(ctx) { | ||
| // Register services | ||
| ctx.registerService('my-service', serviceInstance); | ||
| }, | ||
|
|
||
| async start(ctx) { | ||
| // Start business logic | ||
| }, | ||
|
|
||
| async destroy() { | ||
| // Cleanup resources | ||
| }, | ||
|
|
||
| // Optional: Health check | ||
| async healthCheck() { | ||
| return { | ||
| healthy: true, | ||
| message: 'Service is running', | ||
| details: { connections: 10 } | ||
| }; | ||
| } | ||
| }; | ||
|
|
||
| await kernel.use(myPlugin); | ||
| await kernel.bootstrap(); | ||
| ``` | ||
|
|
||
| ### 2. Advanced Dependency Injection | ||
|
|
||
| Factory-based service registration with lifecycle management: | ||
|
|
||
| ```typescript | ||
| import { ServiceLifecycle } from '@objectstack/core'; | ||
|
|
||
| // Singleton: Created once, shared across all requests | ||
| kernel.registerServiceFactory( | ||
| 'database', | ||
| async (ctx) => { | ||
| const db = await connectToDatabase(); | ||
| return db; | ||
| }, | ||
| ServiceLifecycle.SINGLETON | ||
| ); | ||
|
|
||
| // Transient: New instance on every request | ||
| kernel.registerServiceFactory( | ||
| 'request-id', | ||
| () => generateUUID(), | ||
| ServiceLifecycle.TRANSIENT | ||
| ); | ||
|
|
||
| // Scoped: One instance per scope (e.g., per HTTP request) | ||
| kernel.registerServiceFactory( | ||
| 'user-session', | ||
| async (ctx) => { | ||
| return new UserSession(); | ||
| }, | ||
| ServiceLifecycle.SCOPED | ||
| ); | ||
|
|
||
| // Get service (async) | ||
| const db = await kernel.getServiceAsync('database'); | ||
|
|
||
| // Get scoped service | ||
| const session = await kernel.getServiceAsync('user-session', 'request-123'); | ||
| ``` | ||
|
|
||
| ### 3. Service Dependencies | ||
|
|
||
| Declare service dependencies for proper initialization order: | ||
|
|
||
| ```typescript | ||
| kernel.registerServiceFactory( | ||
| 'api-client', | ||
| async (ctx) => { | ||
| const auth = await ctx.getService('auth-service'); | ||
| return new ApiClient(auth); | ||
| }, | ||
| ServiceLifecycle.SINGLETON, | ||
| ['auth-service'] // Dependencies | ||
| ); | ||
|
|
||
| // Detect circular dependencies | ||
| const cycles = kernel['pluginLoader'].detectCircularDependencies(); | ||
| if (cycles.length > 0) { | ||
| console.error('Circular dependencies detected:', cycles); | ||
| } | ||
| ``` | ||
|
|
||
| ### 4. Plugin Timeout Control | ||
|
|
||
| Prevent plugins from hanging during startup: | ||
|
|
||
| ```typescript | ||
| const plugin: PluginMetadata = { | ||
| name: 'slow-plugin', | ||
| version: '1.0.0', | ||
| startupTimeout: 5000, // 5 second timeout | ||
|
|
||
| async init(ctx) { | ||
| // If this takes longer than 5s, it will timeout | ||
| await slowInitialization(); | ||
| } | ||
| }; | ||
|
|
||
| await kernel.use(plugin); | ||
|
|
||
| try { | ||
| await kernel.bootstrap(); | ||
| } catch (error) { | ||
| // Error: Plugin slow-plugin init timeout after 5000ms | ||
| } | ||
| ``` | ||
|
|
||
| ### 5. Startup Failure Rollback | ||
|
|
||
| Automatically rollback started plugins if any plugin fails: | ||
|
|
||
| ```typescript | ||
| const plugin1: Plugin = { | ||
| name: 'plugin-1', | ||
| version: '1.0.0', | ||
| async init() {}, | ||
| async start() { | ||
| // Starts successfully | ||
| }, | ||
| async destroy() { | ||
| console.log('Rolling back plugin-1'); | ||
| } | ||
| }; | ||
|
|
||
| const plugin2: Plugin = { | ||
| name: 'plugin-2', | ||
| version: '1.0.0', | ||
| async init() {}, | ||
| async start() { | ||
| throw new Error('Startup failed!'); | ||
| } | ||
| }; | ||
|
|
||
| await kernel.use(plugin1); | ||
| await kernel.use(plugin2); | ||
|
|
||
| try { | ||
| await kernel.bootstrap(); | ||
| } catch (error) { | ||
| // plugin-1 will be automatically destroyed (rolled back) | ||
| // Error: Plugin plugin-2 failed to start - rollback complete | ||
| } | ||
| ``` | ||
|
|
||
| ### 6. Plugin Health Checks | ||
|
|
||
| Monitor plugin health at runtime: | ||
|
|
||
| ```typescript | ||
| const plugin: PluginMetadata = { | ||
| name: 'database-plugin', | ||
| version: '1.0.0', | ||
|
|
||
| async init(ctx) { | ||
| // Initialize database connection | ||
| }, | ||
|
|
||
| async healthCheck() { | ||
| const isConnected = await checkDatabaseConnection(); | ||
| return { | ||
| healthy: isConnected, | ||
| message: isConnected ? 'Connected' : 'Disconnected', | ||
| details: { | ||
| connections: 10, | ||
| responseTime: 50 | ||
| } | ||
| }; | ||
| } | ||
| }; | ||
|
|
||
| await kernel.use(plugin); | ||
| await kernel.bootstrap(); | ||
|
|
||
| // Check individual plugin health | ||
| const health = await kernel.checkPluginHealth('database-plugin'); | ||
| console.log(health); | ||
| // { healthy: true, message: 'Connected', details: {...}, lastCheck: Date } | ||
|
|
||
| // Check all plugins health | ||
| const allHealth = await kernel.checkAllPluginsHealth(); | ||
| for (const [pluginName, health] of allHealth) { | ||
| console.log(`${pluginName}: ${health.healthy ? '✅' : '❌'}`); | ||
| } | ||
| ``` | ||
|
|
||
| ### 7. Performance Metrics | ||
|
|
||
| Track plugin startup times: | ||
|
|
||
| ```typescript | ||
| await kernel.bootstrap(); | ||
|
|
||
| const metrics = kernel.getPluginMetrics(); | ||
| for (const [pluginName, startTime] of metrics) { | ||
| console.log(`${pluginName}: ${startTime}ms`); | ||
| } | ||
| // plugin-1: 150ms | ||
| // plugin-2: 320ms | ||
| // plugin-3: 45ms | ||
| ``` | ||
|
|
||
| ### 8. Graceful Shutdown | ||
|
|
||
| Properly cleanup resources on shutdown: | ||
|
|
||
| ```typescript | ||
| const kernel = new EnhancedObjectKernel({ | ||
| gracefulShutdown: true, | ||
| shutdownTimeout: 60000 // 60 second timeout | ||
| }); | ||
|
|
||
| // Register custom shutdown handler | ||
| kernel.onShutdown(async () => { | ||
| console.log('Closing database connections...'); | ||
| await db.close(); | ||
| }); | ||
|
|
||
| // Graceful shutdown | ||
| process.on('SIGTERM', async () => { | ||
| await kernel.shutdown(); | ||
| process.exit(0); | ||
| }); | ||
|
|
||
| // Manual shutdown | ||
| await kernel.shutdown(); | ||
| // Triggers: | ||
| // 1. kernel:shutdown hook | ||
| // 2. Plugin destroy() in reverse order | ||
| // 3. Custom shutdown handlers | ||
| // 4. Logger cleanup | ||
| ``` | ||
|
|
||
| ### 9. Version Compatibility | ||
|
|
||
| Plugins must use semantic versioning: | ||
|
|
||
| ```typescript | ||
| // Valid versions | ||
| '1.0.0' | ||
| '2.3.4' | ||
| '1.0.0-alpha.1' | ||
| '1.0.0+20230101' | ||
|
|
||
| // Invalid versions (will be rejected) | ||
| '1.0' | ||
| 'v1.0.0' | ||
| 'latest' | ||
| ``` | ||
|
|
||
| ### 10. Plugin Configuration Validation | ||
|
|
||
| Use Zod schemas to validate plugin configuration: | ||
|
|
||
| ```typescript | ||
| import { z } from 'zod'; | ||
|
|
||
| const MyPluginConfigSchema = z.object({ | ||
| apiKey: z.string(), | ||
| timeout: z.number().min(1000).max(30000), | ||
| retries: z.number().int().min(0).default(3) | ||
| }); | ||
|
|
||
| const plugin: PluginMetadata = { | ||
| name: 'my-plugin', | ||
| version: '1.0.0', | ||
| configSchema: MyPluginConfigSchema, | ||
|
|
||
| async init(ctx) { | ||
| // Config is validated before init is called | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| ## Migration from ObjectKernel | ||
|
|
||
| To migrate from `ObjectKernel` to `EnhancedObjectKernel`: | ||
|
|
||
| ```typescript | ||
| // Before | ||
| import { ObjectKernel } from '@objectstack/core'; | ||
| const kernel = new ObjectKernel(); | ||
|
|
||
| // After | ||
| import { EnhancedObjectKernel } from '@objectstack/core'; | ||
| const kernel = new EnhancedObjectKernel({ | ||
| logger: { level: 'info' }, | ||
| gracefulShutdown: true, | ||
| rollbackOnFailure: true | ||
| }); | ||
| ``` | ||
|
|
||
| Both kernels are compatible - `EnhancedObjectKernel` is a superset of `ObjectKernel`. | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Always set timeouts**: Configure `startupTimeout` to prevent hanging plugins | ||
| 2. **Implement health checks**: Monitor plugin health at runtime | ||
| 3. **Use semantic versioning**: Ensures compatibility and proper dependency resolution | ||
| 4. **Enable rollback**: Set `rollbackOnFailure: true` to prevent partial startup states | ||
| 5. **Handle shutdown**: Implement `destroy()` to cleanup resources properly | ||
| 6. **Monitor metrics**: Track startup times to identify slow plugins | ||
| 7. **Use service factories**: Prefer factories over static instances for better control | ||
| 8. **Declare dependencies**: Use the dependencies array for proper initialization order | ||
|
|
||
| ## API Reference | ||
|
|
||
| ### EnhancedObjectKernel | ||
|
|
||
| - `constructor(config: EnhancedKernelConfig)` | ||
| - `async use(plugin: Plugin): Promise<this>` | ||
| - `registerServiceFactory<T>(name, factory, lifecycle, dependencies?): this` | ||
| - `async bootstrap(): Promise<void>` | ||
| - `async shutdown(): Promise<void>` | ||
| - `async checkPluginHealth(pluginName: string): Promise<PluginHealthStatus>` | ||
| - `async checkAllPluginsHealth(): Promise<Map<string, PluginHealthStatus>>` | ||
| - `getPluginMetrics(): Map<string, number>` | ||
| - `async getServiceAsync<T>(name: string, scopeId?: string): Promise<T>` | ||
| - `onShutdown(handler: () => Promise<void>): void` | ||
| - `getState(): string` | ||
| - `isRunning(): boolean` | ||
|
|
||
| ### ServiceLifecycle | ||
|
|
||
| - `SINGLETON`: Single instance shared across all requests | ||
| - `TRANSIENT`: New instance created for each request | ||
| - `SCOPED`: New instance per scope (e.g., per HTTP request) | ||
|
|
||
| ### PluginMetadata | ||
|
|
||
| Extended `Plugin` interface with: | ||
| - `version: string` - Semantic version | ||
| - `configSchema?: z.ZodSchema` - Configuration schema | ||
| - `signature?: string` - Plugin signature for verification | ||
| - `healthCheck?(): Promise<PluginHealthStatus>` - Health check function | ||
| - `startupTimeout?: number` - Startup timeout in milliseconds | ||
| - `hotReloadable?: boolean` - Whether plugin supports hot reload | ||
|
|
||
| ## Examples | ||
|
|
||
| See the test files for comprehensive examples: | ||
| - `packages/core/src/enhanced-kernel.test.ts` | ||
| - `packages/core/src/plugin-loader.test.ts` | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation example on line 107 shows using
ctx.getService('auth-service'), but ctx.getService is synchronous according to the PluginContext interface (line 27 in types.ts). This should either bectx.getService('auth-service')without await (if it's already registered), or the example should acknowledge that accessing services with dependencies from factory functions is currently not properly supported due to the mock context issue in PluginLoader.createServiceInstance.