-
Notifications
You must be signed in to change notification settings - Fork 0
Implement micro-kernel protocol: unified component loading #231
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
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
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.
Pull request overview
This PR implements the micro-kernel architecture pattern for ObjectStack, enabling a more declarative approach to component loading. The kernel now accepts a heterogeneous array of application configs, drivers, and plugins, automatically classifying them and initializing them in a phased sequence.
Changes:
- Added RuntimeDriver and RuntimeAppConfig interfaces to support the micro-kernel pattern
- Updated ObjectStackKernel to accept mixed component arrays with automatic type detection and phased initialization (apps → drivers → plugins)
- Enhanced ObjectQLPlugin to auto-detect drivers from the kernel when datasources are not explicitly provided
- Updated documentation with comprehensive micro-kernel architecture guide and usage examples
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Added tsx dependency for the multi-protocol-server example |
| packages/objectstack/runtime/src/index.ts | Core micro-kernel implementation: added component interfaces, type guards, classification logic, phased initialization, and driver management methods |
| packages/objectstack/runtime/README.md | Updated documentation to describe micro-kernel pattern, component types, initialization phases, and migration guide (contains InMemoryDriver vs MemoryDriver naming error) |
| packages/foundation/core/src/plugin.ts | Added auto-detection of drivers from kernel in ObjectQLPlugin when datasources not provided |
| examples/protocols/multi-protocol-server/src/index.ts | Updated example to demonstrate new micro-kernel pattern with heterogeneous component array |
| examples/protocols/multi-protocol-server/package.json | Reordered dependencies alphabetically and updated tsx version |
| MICRO_KERNEL_ARCHITECTURE.md | Comprehensive new documentation covering architecture principles, component types, initialization sequence, usage patterns, and migration guide |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| export interface RuntimeDriver { | ||
| /** Driver name/identifier */ | ||
| name?: string; | ||
| /** Connect to the database */ | ||
| connect?: () => Promise<void>; | ||
| /** Disconnect from the database */ | ||
| disconnect?: () => Promise<void>; | ||
| /** Any additional driver methods */ | ||
| [key: string]: any; | ||
| } |
Copilot
AI
Jan 28, 2026
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 RuntimeDriver interface uses [key: string]: any as an index signature, which defeats TypeScript's type safety and violates the strict typing principle in the custom coding guidelines (CodingGuidelineID: 1000000). According to the guidelines, "NO any" should be used - instead use Generics or unknown with guards.
Consider either:
- Defining the expected driver methods explicitly (find, create, update, delete, findOne, etc.)
- Using a more specific type like
[key: string]: unknownwith proper type guards - Making this a discriminated union if different driver types have different method signatures
| private isRuntimePlugin(component: any): component is RuntimePlugin { | ||
| return ( | ||
| typeof component === 'object' && | ||
| component !== null && | ||
| typeof component.name === 'string' && | ||
| (typeof component.install === 'function' || | ||
| typeof component.onStart === 'function' || | ||
| typeof component.onStop === 'function') | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Type guard for RuntimeDriver | ||
| * @private | ||
| */ | ||
| private isRuntimeDriver(component: any): component is RuntimeDriver { | ||
| return ( | ||
| typeof component === 'object' && | ||
| component !== null && | ||
| (typeof component.connect === 'function' || | ||
| typeof component.find === 'function' || | ||
| typeof component.create === 'function' || | ||
| typeof component.update === 'function' || | ||
| typeof component.delete === 'function') | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Type guard for RuntimeAppConfig | ||
| * @private | ||
| */ | ||
| private isRuntimeAppConfig(component: any): component is RuntimeAppConfig { | ||
| return ( | ||
| typeof component === 'object' && | ||
| component !== null && | ||
| typeof component.name === 'string' && | ||
| // App configs typically have 'objects' or 'label' fields | ||
| (component.objects !== undefined || component.label !== undefined) && | ||
| // Must NOT be a plugin (no lifecycle methods) | ||
| typeof component.install !== 'function' && |
Copilot
AI
Jan 28, 2026
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 type guards use any as the parameter type, which violates the strict typing guidelines (CodingGuidelineID: 1000000). According to the guidelines, "NO any" - use unknown instead.
Consider changing all type guard signatures from (component: any) to (component: unknown) for better type safety.
| drivers.forEach((driver: any, index: number) => { | ||
| // Use driver name if available, otherwise use 'default' for first driver | ||
| const driverName = driver.name || (index === 0 ? 'default' : `driver_${index + 1}`); | ||
| datasources![driverName] = driver; |
Copilot
AI
Jan 28, 2026
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 driver auto-detection logic uses any type for the driver when iterating: drivers.forEach((driver: any, index: number). This violates the strict typing guidelines (CodingGuidelineID: 1000000) which state "NO any".
The type should be RuntimeDriver (which is already imported via the kernel's type) or use a more specific Driver type from @objectql/types. Consider: drivers.forEach((driver: RuntimeDriver, index: number) or better yet, use the Driver type from @objectql/types for consistency.
| const drivers = kernel.getAllDrivers?.(); | ||
| if (drivers && drivers.length > 0) { | ||
| datasources = {}; | ||
| drivers.forEach((driver: any, index: number) => { | ||
| // Use driver name if available, otherwise use 'default' for first driver | ||
| const driverName = driver.name || (index === 0 ? 'default' : `driver_${index + 1}`); | ||
| datasources![driverName] = driver; | ||
| }); |
Copilot
AI
Jan 28, 2026
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 code uses optional chaining kernel.getAllDrivers?.() to safely call the method, but this creates a type safety issue. The ExtendedKernel interface doesn't include getAllDrivers, so TypeScript won't catch if this method is removed or renamed from ObjectStackKernel.
Since getAllDrivers() is a new public method added to ObjectStackKernel in this PR, the optional chaining suggests uncertainty about its existence. Either:
- The method should always exist (remove optional chaining)
- Or proper type checking should be done before calling it
Consider removing the optional chaining since getAllDrivers() is now part of the ObjectStackKernel API.
| import { InMemoryDriver } from '@objectql/driver-memory'; | ||
| import { GraphQLPlugin } from '@objectql/protocol-graphql'; | ||
| import { ObjectQLPlugin } from '@objectql/core'; | ||
|
|
||
| // Define application manifest | ||
| const myApp = { | ||
| name: 'my-app', | ||
| label: 'My Application', | ||
| objects: { | ||
| users: { | ||
| name: 'users', | ||
| label: 'Users', | ||
| fields: { | ||
| name: { type: 'text', label: 'Name' }, | ||
| email: { type: 'email', label: 'Email' } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| // Create kernel with all components | ||
| const kernel = new ObjectStackKernel([ | ||
| myApp, // Application config | ||
| new InMemoryDriver(), // Driver |
Copilot
AI
Jan 28, 2026
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 README uses the incorrect class name InMemoryDriver when importing from '@objectql/driver-memory'. The actual exported class is MemoryDriver (as shown in the example at examples/protocols/multi-protocol-server/src/index.ts and the driver's source code).
The import should be:
import { MemoryDriver } from '@objectql/driver-memory';And the usage should be:
new MemoryDriver(), // Not InMemoryDriver()| // Get datasources - either from config or from kernel drivers | ||
| let datasources = this.config.datasources; | ||
| if (!datasources) { | ||
| // Try to get drivers from kernel (micro-kernel pattern) | ||
| const drivers = kernel.getAllDrivers?.(); | ||
| if (drivers && drivers.length > 0) { | ||
| datasources = {}; | ||
| drivers.forEach((driver: any, index: number) => { | ||
| // Use driver name if available, otherwise use 'default' for first driver | ||
| const driverName = driver.name || (index === 0 ? 'default' : `driver_${index + 1}`); | ||
| datasources![driverName] = driver; | ||
| }); | ||
| console.log(`[${this.name}] Using drivers from kernel:`, Object.keys(datasources)); | ||
| } else { |
Copilot
AI
Jan 28, 2026
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 auto-detection logic for drivers in ObjectQLPlugin lacks test coverage. The repository uses comprehensive automated testing (as evidenced by packages/objectstack/runtime/src/protocol.test.ts), so this new feature should also have tests.
Consider adding tests for:
- Auto-detection when datasources is not provided
- Proper fallback when no drivers are available from kernel
- Driver naming logic (default for first driver, driver_N for others)
- Integration with manually provided datasources
| // Must NOT be a driver (no driver methods) | ||
| typeof component.connect !== 'function' && | ||
| typeof component.find !== 'function' && | ||
| typeof component.create !== 'function' | ||
| ); | ||
| } | ||
|
|
||
| /** Start the kernel */ | ||
| async start(): Promise<void> { | ||
| console.log('[ObjectStackKernel] Starting kernel...'); | ||
|
|
||
| // Start all plugins | ||
| for (const plugin of this.plugins) { | ||
| if (plugin.onStart) { | ||
| await plugin.onStart({ engine: this }); | ||
| try { | ||
| // Phase 1: Load application manifests | ||
| for (const app of this.applications) { | ||
| console.log(`[ObjectStackKernel] Loading application: ${app.name}`); | ||
| if (app.objects) { | ||
| for (const [objName, objConfig] of Object.entries(app.objects)) { | ||
| this.metadata.register('object', { | ||
| id: objName, | ||
| content: objConfig, | ||
| packageName: app.name | ||
| } as any); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Phase 2: Connect drivers | ||
| for (const driver of this.drivers) { | ||
| if (driver.connect) { | ||
| console.log(`[ObjectStackKernel] Connecting driver: ${driver.name || 'unnamed'}`); | ||
| await driver.connect(); | ||
| } | ||
| } | ||
|
|
||
| // Phase 3: Install all plugins | ||
| for (const plugin of this.plugins) { | ||
| if (plugin.install) { | ||
| console.log(`[ObjectStackKernel] Installing plugin: ${plugin.name}`); | ||
| await plugin.install({ engine: this }); | ||
| } | ||
| } | ||
|
|
||
| // Phase 4: Start all plugins | ||
| for (const plugin of this.plugins) { |
Copilot
AI
Jan 28, 2026
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 phase initialization in start() lacks error handling. If any phase fails (e.g., driver connection fails, plugin installation throws), the kernel will be left in an inconsistent state. According to the custom coding guidelines (CodingGuidelineID: 1000000), errors should be thrown as ObjectQLError instead of regular Error, and proper error handling should be in place.
Consider wrapping each phase in try-catch blocks and either:
- Roll back previous phases on failure (disconnect already-connected drivers, etc.)
- Throw a descriptive ObjectQLError indicating which phase failed
- Provide a cleanup mechanism to ensure the kernel can be stopped even if start() fails midway
| await plugin.onStart({ engine: this }); | ||
| } | ||
| } | ||
|
|
||
| console.log('[ObjectStackKernel] Kernel started successfully'); | ||
| } catch (error) { | ||
| console.error('[ObjectStackKernel] Error during kernel startup:', error); | ||
| // Attempt to stop any partially initialized components | ||
| await this.stop().catch(stopError => { | ||
| console.error('[ObjectStackKernel] Error during cleanup after failed startup:', stopError); | ||
| }); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| /** Stop the kernel */ | ||
| async stop(): Promise<void> { | ||
| console.log('[ObjectStackKernel] Stopping kernel...'); | ||
|
|
||
| const errors: Error[] = []; | ||
|
|
||
| // Stop all plugins in reverse order |
Copilot
AI
Jan 28, 2026
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 stop() method lacks error handling when disconnecting drivers or stopping plugins. If a plugin's onStop throws an error, it will prevent subsequent plugins from being stopped and drivers from being disconnected, potentially leaving resources in a leaked state.
Consider wrapping each cleanup operation in a try-catch block to ensure all components are given a chance to clean up, even if some fail. Collect any errors and optionally throw a composite error at the end if any occurred.
| export interface RuntimeAppConfig { | ||
| /** Unique application name */ | ||
| name: string; | ||
| /** Application label */ | ||
| label?: string; | ||
| /** Application description */ | ||
| description?: string; | ||
| /** Objects to register */ | ||
| objects?: Record<string, any>; | ||
| /** Custom metadata */ | ||
| [key: string]: any; | ||
| } |
Copilot
AI
Jan 28, 2026
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 RuntimeAppConfig interface uses [key: string]: any as an index signature, which violates the strict typing principle in the custom coding guidelines (CodingGuidelineID: 1000000). According to the guidelines, "NO any" should be used.
Consider changing the index signature to [key: string]: unknown for better type safety while still allowing custom metadata fields.
| // Must NOT be a plugin (no lifecycle methods) | ||
| typeof component.install !== 'function' && | ||
| typeof component.onStart !== 'function' && | ||
| typeof component.onStop !== 'function' && | ||
| // Must NOT be a driver (no driver methods) | ||
| typeof component.connect !== 'function' && | ||
| typeof component.find !== 'function' && | ||
| typeof component.create !== 'function' |
Copilot
AI
Jan 28, 2026
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 type guard isRuntimeAppConfig has a potential issue with circular logic. It calls isRuntimePlugin and isRuntimeDriver to exclude those types, but those guards check for the presence of specific methods. An object with a name property that has both plugin methods AND driver methods would be classified as a plugin (checked first), but if you later add an object with name and no plugin/driver methods, it would be classified as RuntimeAppConfig even if it's not actually an app config.
Consider adding a more explicit check for RuntimeAppConfig, such as verifying the presence of expected properties like objects or checking that it's a plain object without callable methods (except potentially custom metadata).
| // Must NOT be a plugin (no lifecycle methods) | |
| typeof component.install !== 'function' && | |
| typeof component.onStart !== 'function' && | |
| typeof component.onStop !== 'function' && | |
| // Must NOT be a driver (no driver methods) | |
| typeof component.connect !== 'function' && | |
| typeof component.find !== 'function' && | |
| typeof component.create !== 'function' | |
| // Must NOT be a plugin | |
| !this.isRuntimePlugin(component) && | |
| // Must NOT be a driver | |
| !this.isRuntimeDriver(component) |
The protocol specification requires a micro-kernel architecture where applications, drivers, and plugins are loaded uniformly rather than through separate registration mechanisms.
Changes
Runtime (
@objectql/runtime)ObjectStackKernelconstructor now acceptsKernelComponent[](union ofRuntimePlugin | RuntimeDriver | RuntimeAppConfig) instead ofRuntimePlugin[]onlygetDriver(name?)andgetAllDrivers()methods for plugin access to registered driversCore (
@objectql/core)ObjectQLPluginnow detects drivers from kernel whendatasourcesconfig is omitteddefault,driver_1,driver_2patternProtocol Plugins
No changes required - all three protocol plugins (
graphql,odata-v4,json-rpc) remain compatible.Usage
Before:
After:
Backward compatibility maintained - existing code continues to work.
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
protocol.objectstack.ai/home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.