Skip to content

feat(core): add replaceService to PluginContext#597

Merged
hotlong merged 3 commits intomainfrom
copilot/add-query-profiling-hook
Feb 11, 2026
Merged

feat(core): add replaceService to PluginContext#597
hotlong merged 3 commits intomainfrom
copilot/add-query-profiling-hook

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 10, 2026

Implements upstream spec change §5.1.2 from objectql DESIGN_CORE_REFACTOR.md. The downstream OptimizationsPlugin needs to replace/enhance kernel internals (metadata registry, connection pooling, hook manager), but PluginContext has no service replacement API.

§5.1.1 (Query Profiling) required no changes — OperationContext and EngineMiddleware are already exported.

Changes

  • PluginContext interface (types.ts): New replaceService<T>(name, implementation) method — throws if service doesn't exist
  • ObjectKernelBase (kernel-base.ts): Implementation for LiteKernel path (Map + IServiceRegistry)
  • ObjectKernel (kernel.ts): Implementation with dual-write to services Map + PluginLoader (consistent with existing registerService pattern)
  • PluginLoader (plugin-loader.ts): replaceService with existence validation
  • SecurePluginContext (plugin-permission-enforcer.ts): Delegates through permission enforcement via enforceServiceAccess
  • Tests: 5 new tests across kernel.test.ts and lite-kernel.test.ts (basic replace, missing service error, decorator pattern)

Usage — decorator pattern for optimization plugins

// In OptimizationsPlugin.init():
const existing = ctx.getService('metadata');
const optimized = new OptimizedMetadataRegistry(existing);
ctx.replaceService('metadata', optimized);

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Feb 10, 2026 10:26pm
spec Error Error Feb 10, 2026 10:26pm

Request Review

Copilot AI and others added 2 commits February 10, 2026 21:51
…gration hooks

Add replaceService<T>(name, implementation) to PluginContext interface,
enabling plugins to replace/enhance kernel internals (metadata registry,
hook manager, connection pooling) using the decorator pattern.

Implements upstream spec change 5.1.2 from objectql DESIGN_CORE_REFACTOR.md.

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add query profiling hook extension point for ObjectQL feat(core): add replaceService to PluginContext Feb 10, 2026
Copilot AI requested a review from hotlong February 10, 2026 21:54
@hotlong hotlong marked this pull request as ready for review February 11, 2026 01:43
Copilot AI review requested due to automatic review settings February 11, 2026 01:43
@hotlong hotlong merged commit dc6f3a0 into main Feb 11, 2026
2 of 3 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a replaceService() API to PluginContext so plugins (notably optimization/decorator-style plugins) can swap existing kernel services at runtime, aligning core with the upstream refactor spec.

Changes:

  • Extend PluginContext with replaceService<T>(name, implementation) and wire it through ObjectKernel, ObjectKernelBase (LiteKernel), PluginLoader, and SecurePluginContext.
  • Implement existence validation + replacement behavior across the kernel and loader.
  • Add new tests in kernel.test.ts and lite-kernel.test.ts covering basic replacement and error cases.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/core/src/types.ts Adds PluginContext.replaceService() to the public interface.
packages/core/src/kernel.ts Implements replaceService() in the primary kernel context with dual-write + logging.
packages/core/src/kernel-base.ts Implements replaceService() for LiteKernel / base-kernel context.
packages/core/src/plugin-loader.ts Adds replaceService() to update loader-held service instances with existence validation.
packages/core/src/security/plugin-permission-enforcer.ts Exposes replaceService() via SecurePluginContext with permission enforcement.
packages/core/src/kernel.test.ts Adds ObjectKernel tests for service replacement + decorator pattern.
packages/core/src/lite-kernel.test.ts Adds LiteKernel tests for service replacement + missing-service error.

Comment on lines +36 to +40
* @param name - Service name to replace
* @param implementation - New service implementation
* @throws Error if the service does not exist
*/
replaceService<T>(name: string, implementation: T): void;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PluginContext now requires replaceService, but several typed test mocks in the repo construct PluginContext objects without this method (e.g. packages/plugins/plugin-auth/src/auth-plugin.test.ts and packages/core/src/security/plugin-permission-enforcer.test.ts). This will cause TypeScript compile errors; update those mocks to include a replaceService stub (or use a helper factory for PluginContext test doubles).

Suggested change
* @param name - Service name to replace
* @param implementation - New service implementation
* @throws Error if the service does not exist
*/
replaceService<T>(name: string, implementation: T): void;
* NOTE: This is optional for backward compatibility with existing plugins
* and test contexts that may not implement service replacement.
*
* @param name - Service name to replace
* @param implementation - New service implementation
* @throws Error if the service does not exist
*/
replaceService?<T>(name: string, implementation: T): void;

Copilot uses AI. Check for mistakes.
// IServiceRegistry implementation
if (!this.services.has(name)) {
throw new Error(`[Kernel] Service '${name}' not found. Use registerService() to add new services.`);
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the IServiceRegistry branch, replaceService calls this.services.register(name, implementation) after confirming the service already exists. IServiceRegistry.register() is specified to throw if the name is already registered, so this path will fail whenever services is an IServiceRegistry. Use unregister(name) (or a dedicated replace/set API) before re-registering.

Suggested change
}
}
// unregister existing service before re-registering to avoid duplicate registration errors
this.services.unregister(name);

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +261
replaceService(name: string, service: any): void {
if (!this.hasService(name)) {
throw new Error(`Service '${name}' not found`);
}
this.serviceInstances.set(name, service);
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PluginLoader.replaceService() sets serviceInstances, but PluginLoader.getService() always prefers a serviceFactory when one exists. That means replacing a service that was registered via registerServiceFactory() will not take effect for async consumers (getService/getServiceAsync). Consider making getService() prefer an explicitly replaced instance, or have replaceService() remove/override the factory registration and clear any scoped caches for that service.

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +129
replaceService: <T>(name: string, implementation: T): void => {
const hasService = this.services.has(name) || this.pluginLoader.hasService(name);
if (!hasService) {
throw new Error(`[Kernel] Service '${name}' not found. Use registerService() to add new services.`);
}
this.services.set(name, implementation);
this.pluginLoader.replaceService(name, implementation);
this.logger.info(`Service '${name}' replaced`, { service: name });
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectKernel’s replaceService currently allows replacement when the service exists only as a factory (via pluginLoader.hasService). However, without additional changes in PluginLoader, the replacement won’t affect kernel.getServiceAsync() (factory path still wins). Either constrain replaceService to instance-registered services only, or update the loader semantics so a replacement overrides factory resolution too.

Copilot uses AI. Check for mistakes.
Comment on lines +397 to +401
replaceService<T>(name: string, implementation: T): void {
// Check permission before replacing service
this.permissionEnforcer.enforceServiceAccess(this.pluginName, name);
this.baseContext.replaceService(name, implementation);
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecurePluginContext.replaceService uses enforceServiceAccess, which appears to represent read/access permission. Replacing a service is a much higher-privilege operation (it can hijack kernel internals even if a plugin can only “access” the service). Consider introducing and enforcing a distinct capability for service replacement (or restrict replacement to trusted/core plugins) rather than reusing the read-access check.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants