Skip to content

Commit dc6f3a0

Browse files
authored
Merge pull request #597 from objectstack-ai/copilot/add-query-profiling-hook
2 parents 2eeb296 + a5a5742 commit dc6f3a0

7 files changed

Lines changed: 188 additions & 0 deletions

File tree

packages/core/src/kernel-base.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ export abstract class ObjectKernelBase {
8585
return this.services.get<T>(name);
8686
}
8787
},
88+
replaceService: <T>(name: string, implementation: T): void => {
89+
if (this.services instanceof Map) {
90+
if (!this.services.has(name)) {
91+
throw new Error(`[Kernel] Service '${name}' not found. Use registerService() to add new services.`);
92+
}
93+
this.services.set(name, implementation);
94+
} else {
95+
// IServiceRegistry implementation
96+
if (!this.services.has(name)) {
97+
throw new Error(`[Kernel] Service '${name}' not found. Use registerService() to add new services.`);
98+
}
99+
this.services.register(name, implementation);
100+
}
101+
this.logger.info(`Service '${name}' replaced`, { service: name });
102+
},
88103
hook: (name, handler) => {
89104
if (!this.hooks.has(name)) {
90105
this.hooks.set(name, []);

packages/core/src/kernel.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,4 +534,91 @@ describe('ObjectKernel', () => {
534534
}).rejects.toThrow('not running');
535535
});
536536
});
537+
538+
describe('Service Replacement', () => {
539+
it('should replace an existing service via replaceService', async () => {
540+
const originalService = { value: 'original' };
541+
const replacementService = { value: 'replaced' };
542+
543+
const plugin: Plugin = {
544+
name: 'register-plugin',
545+
version: '1.0.0',
546+
init: async (ctx) => {
547+
ctx.registerService('metadata', originalService);
548+
},
549+
};
550+
551+
const optimizationPlugin: Plugin = {
552+
name: 'optimization-plugin',
553+
version: '1.0.0',
554+
dependencies: ['register-plugin'],
555+
init: async (ctx) => {
556+
const existing = ctx.getService('metadata');
557+
expect(existing).toBe(originalService);
558+
ctx.replaceService('metadata', replacementService);
559+
},
560+
};
561+
562+
await kernel.use(plugin);
563+
await kernel.use(optimizationPlugin);
564+
await kernel.bootstrap();
565+
566+
const result = kernel.getService('metadata');
567+
expect(result).toBe(replacementService);
568+
569+
await kernel.shutdown();
570+
});
571+
572+
it('should throw when replacing a non-existent service', async () => {
573+
const plugin: Plugin = {
574+
name: 'bad-replace-plugin',
575+
version: '1.0.0',
576+
init: async (ctx) => {
577+
expect(() => {
578+
ctx.replaceService('nonexistent', { value: 'test' });
579+
}).toThrow("Service 'nonexistent' not found");
580+
},
581+
};
582+
583+
await kernel.use(plugin);
584+
await kernel.bootstrap();
585+
await kernel.shutdown();
586+
});
587+
588+
it('should allow decorator pattern via replaceService', async () => {
589+
const original = {
590+
getData: () => 'raw-data',
591+
};
592+
593+
const plugin: Plugin = {
594+
name: 'data-plugin',
595+
version: '1.0.0',
596+
init: async (ctx) => {
597+
ctx.registerService('data', original);
598+
},
599+
};
600+
601+
const wrapperPlugin: Plugin = {
602+
name: 'wrapper-plugin',
603+
version: '1.0.0',
604+
dependencies: ['data-plugin'],
605+
init: async (ctx) => {
606+
const existing = ctx.getService<typeof original>('data');
607+
const decorated = {
608+
getData: () => `cached(${existing.getData()})`,
609+
};
610+
ctx.replaceService('data', decorated);
611+
},
612+
};
613+
614+
await kernel.use(plugin);
615+
await kernel.use(wrapperPlugin);
616+
await kernel.bootstrap();
617+
618+
const result = kernel.getService<typeof original>('data');
619+
expect(result.getData()).toBe('cached(raw-data)');
620+
621+
await kernel.shutdown();
622+
});
623+
});
537624
});

packages/core/src/kernel.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ export class ObjectKernel {
119119
throw new Error(`[Kernel] Service '${name}' not found`);
120120
}
121121
},
122+
replaceService: <T>(name: string, implementation: T): void => {
123+
const hasService = this.services.has(name) || this.pluginLoader.hasService(name);
124+
if (!hasService) {
125+
throw new Error(`[Kernel] Service '${name}' not found. Use registerService() to add new services.`);
126+
}
127+
this.services.set(name, implementation);
128+
this.pluginLoader.replaceService(name, implementation);
129+
this.logger.info(`Service '${name}' replaced`, { service: name });
130+
},
122131
hook: (name, handler) => {
123132
if (!this.hooks.has(name)) {
124133
this.hooks.set(name, []);

packages/core/src/lite-kernel.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,52 @@ describe('LiteKernel with Configurable Logger', () => {
197197
await browserKernel.shutdown();
198198
});
199199
});
200+
201+
describe('Service Replacement', () => {
202+
it('should replace an existing service via replaceService', async () => {
203+
const originalService = { value: 'original' };
204+
const replacementService = { value: 'replaced' };
205+
206+
const plugin: Plugin = {
207+
name: 'register-plugin',
208+
init: async (ctx) => {
209+
ctx.registerService('metadata', originalService);
210+
},
211+
};
212+
213+
const optimizationPlugin: Plugin = {
214+
name: 'optimization-plugin',
215+
dependencies: ['register-plugin'],
216+
init: async (ctx) => {
217+
const existing = ctx.getService('metadata');
218+
expect(existing).toBe(originalService);
219+
ctx.replaceService('metadata', replacementService);
220+
},
221+
};
222+
223+
kernel.use(plugin);
224+
kernel.use(optimizationPlugin);
225+
await kernel.bootstrap();
226+
227+
const result = kernel.getService('metadata');
228+
expect(result).toBe(replacementService);
229+
230+
await kernel.shutdown();
231+
});
232+
233+
it('should throw when replacing a non-existent service', async () => {
234+
const plugin: Plugin = {
235+
name: 'bad-replace-plugin',
236+
init: async (ctx) => {
237+
expect(() => {
238+
ctx.replaceService('nonexistent', { value: 'test' });
239+
}).toThrow("Service 'nonexistent' not found");
240+
},
241+
};
242+
243+
kernel.use(plugin);
244+
await kernel.bootstrap();
245+
await kernel.shutdown();
246+
});
247+
});
200248
});

packages/core/src/plugin-loader.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,18 @@ export class PluginLoader {
248248
this.serviceInstances.set(name, service);
249249
}
250250

251+
/**
252+
* Replace an existing service instance.
253+
* Used by optimization plugins to swap kernel internals.
254+
* @throws Error if service does not exist
255+
*/
256+
replaceService(name: string, service: any): void {
257+
if (!this.hasService(name)) {
258+
throw new Error(`Service '${name}' not found`);
259+
}
260+
this.serviceInstances.set(name, service);
261+
}
262+
251263
/**
252264
* Check if a service is registered (either as instance or factory)
253265
*/

packages/core/src/security/plugin-permission-enforcer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,12 @@ export class SecurePluginContext implements PluginContext {
394394
return this.baseContext.getService<T>(name);
395395
}
396396

397+
replaceService<T>(name: string, implementation: T): void {
398+
// Check permission before replacing service
399+
this.permissionEnforcer.enforceServiceAccess(this.pluginName, name);
400+
this.baseContext.replaceService(name, implementation);
401+
}
402+
397403
getServices(): Map<string, any> {
398404
// Return all services (no permission check for listing)
399405
return this.baseContext.getServices();

packages/core/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ export interface PluginContext {
2828
*/
2929
getService<T>(name: string): T;
3030

31+
/**
32+
* Replace an existing service with a new implementation.
33+
* Useful for optimization plugins that wrap or swap kernel internals
34+
* (e.g., metadata registry, connection pooling).
35+
*
36+
* @param name - Service name to replace
37+
* @param implementation - New service implementation
38+
* @throws Error if the service does not exist
39+
*/
40+
replaceService<T>(name: string, implementation: T): void;
41+
3142
/**
3243
* Get all registered services
3344
*/

0 commit comments

Comments
 (0)