Skip to content

Commit 29228f5

Browse files
Copilothotlong
andcommitted
Phase 2: Refactor ObjectKernel to use ObjectKernelBase (reduce from 219 to 135 lines)
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 26ec96b commit 29228f5

File tree

4 files changed

+517
-125
lines changed

4 files changed

+517
-125
lines changed

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Provides plugin system, dependency injection, and lifecycle management.
66
*/
77

8+
export * from './kernel-base.js';
89
export * from './kernel.js';
910
export * from './types.js';
1011
export * from './logger.js';

packages/core/src/kernel-base.ts

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import type { Plugin, PluginContext } from './types.js';
2+
import type { Logger } from '@objectstack/spec/contracts';
3+
import type { IServiceRegistry } from '@objectstack/spec/contracts';
4+
5+
/**
6+
* Kernel state machine
7+
*/
8+
export type KernelState = 'idle' | 'initializing' | 'running' | 'stopping' | 'stopped';
9+
10+
/**
11+
* ObjectKernelBase - Abstract Base Class for Microkernel
12+
*
13+
* Provides common functionality for both ObjectKernel and EnhancedObjectKernel:
14+
* - Plugin management (Map storage)
15+
* - Dependency resolution (topological sort)
16+
* - Hook/Event system
17+
* - Context creation
18+
* - State validation
19+
*
20+
* This eliminates ~120 lines of duplicate code between the two implementations.
21+
*/
22+
export abstract class ObjectKernelBase {
23+
protected plugins: Map<string, Plugin> = new Map();
24+
protected services: IServiceRegistry | Map<string, any> = new Map();
25+
protected hooks: Map<string, Array<(...args: any[]) => void | Promise<void>>> = new Map();
26+
protected state: KernelState = 'idle';
27+
protected logger: Logger;
28+
protected context!: PluginContext;
29+
30+
constructor(logger: Logger) {
31+
this.logger = logger;
32+
}
33+
34+
/**
35+
* Validate kernel state
36+
* @param requiredState - Required state for the operation
37+
* @throws Error if current state doesn't match
38+
*/
39+
protected validateState(requiredState: KernelState): void {
40+
if (this.state !== requiredState) {
41+
throw new Error(
42+
`[Kernel] Invalid state: expected '${requiredState}', got '${this.state}'`
43+
);
44+
}
45+
}
46+
47+
/**
48+
* Validate kernel is in idle state (for plugin registration)
49+
*/
50+
protected validateIdle(): void {
51+
if (this.state !== 'idle') {
52+
throw new Error('[Kernel] Cannot register plugins after bootstrap has started');
53+
}
54+
}
55+
56+
/**
57+
* Create the plugin context
58+
* Subclasses can override to customize context creation
59+
*/
60+
protected createContext(): PluginContext {
61+
return {
62+
registerService: (name, service) => {
63+
if (this.services instanceof Map) {
64+
if (this.services.has(name)) {
65+
throw new Error(`[Kernel] Service '${name}' already registered`);
66+
}
67+
this.services.set(name, service);
68+
} else {
69+
// IServiceRegistry implementation
70+
this.services.register(name, service);
71+
}
72+
this.logger.info(`Service '${name}' registered`, { service: name });
73+
},
74+
getService: <T>(name: string): T => {
75+
if (this.services instanceof Map) {
76+
const service = this.services.get(name);
77+
if (!service) {
78+
throw new Error(`[Kernel] Service '${name}' not found`);
79+
}
80+
return service as T;
81+
} else {
82+
// IServiceRegistry implementation
83+
return this.services.get<T>(name);
84+
}
85+
},
86+
hook: (name, handler) => {
87+
if (!this.hooks.has(name)) {
88+
this.hooks.set(name, []);
89+
}
90+
this.hooks.get(name)!.push(handler);
91+
},
92+
trigger: async (name, ...args) => {
93+
const handlers = this.hooks.get(name) || [];
94+
for (const handler of handlers) {
95+
await handler(...args);
96+
}
97+
},
98+
getServices: () => {
99+
if (this.services instanceof Map) {
100+
return new Map(this.services);
101+
} else {
102+
// For IServiceRegistry, we need to return the underlying Map
103+
// This is a compatibility method
104+
return new Map();
105+
}
106+
},
107+
logger: this.logger,
108+
getKernel: () => this as any,
109+
};
110+
}
111+
112+
/**
113+
* Resolve plugin dependencies using topological sort
114+
* @returns Ordered list of plugins (dependencies first)
115+
*/
116+
protected resolveDependencies(): Plugin[] {
117+
const resolved: Plugin[] = [];
118+
const visited = new Set<string>();
119+
const visiting = new Set<string>();
120+
121+
const visit = (pluginName: string) => {
122+
if (visited.has(pluginName)) return;
123+
124+
if (visiting.has(pluginName)) {
125+
throw new Error(`[Kernel] Circular dependency detected: ${pluginName}`);
126+
}
127+
128+
const plugin = this.plugins.get(pluginName);
129+
if (!plugin) {
130+
throw new Error(`[Kernel] Plugin '${pluginName}' not found`);
131+
}
132+
133+
visiting.add(pluginName);
134+
135+
// Visit dependencies first
136+
const deps = plugin.dependencies || [];
137+
for (const dep of deps) {
138+
if (!this.plugins.has(dep)) {
139+
throw new Error(
140+
`[Kernel] Dependency '${dep}' not found for plugin '${pluginName}'`
141+
);
142+
}
143+
visit(dep);
144+
}
145+
146+
visiting.delete(pluginName);
147+
visited.add(pluginName);
148+
resolved.push(plugin);
149+
};
150+
151+
// Visit all plugins
152+
for (const pluginName of this.plugins.keys()) {
153+
visit(pluginName);
154+
}
155+
156+
return resolved;
157+
}
158+
159+
/**
160+
* Run plugin init phase
161+
* @param plugin - Plugin to initialize
162+
*/
163+
protected async runPluginInit(plugin: Plugin): Promise<void> {
164+
const pluginName = plugin.name;
165+
this.logger.info(`Initializing plugin: ${pluginName}`);
166+
167+
try {
168+
await plugin.init(this.context);
169+
this.logger.info(`Plugin initialized: ${pluginName}`);
170+
} catch (error) {
171+
this.logger.error(`Plugin init failed: ${pluginName}`, error as Error);
172+
throw error;
173+
}
174+
}
175+
176+
/**
177+
* Run plugin start phase
178+
* @param plugin - Plugin to start
179+
*/
180+
protected async runPluginStart(plugin: Plugin): Promise<void> {
181+
if (!plugin.start) return;
182+
183+
const pluginName = plugin.name;
184+
this.logger.info(`Starting plugin: ${pluginName}`);
185+
186+
try {
187+
await plugin.start(this.context);
188+
this.logger.info(`Plugin started: ${pluginName}`);
189+
} catch (error) {
190+
this.logger.error(`Plugin start failed: ${pluginName}`, error as Error);
191+
throw error;
192+
}
193+
}
194+
195+
/**
196+
* Run plugin destroy phase
197+
* @param plugin - Plugin to destroy
198+
*/
199+
protected async runPluginDestroy(plugin: Plugin): Promise<void> {
200+
if (!plugin.destroy) return;
201+
202+
const pluginName = plugin.name;
203+
this.logger.info(`Destroying plugin: ${pluginName}`);
204+
205+
try {
206+
await plugin.destroy();
207+
this.logger.info(`Plugin destroyed: ${pluginName}`);
208+
} catch (error) {
209+
this.logger.error(`Plugin destroy failed: ${pluginName}`, error as Error);
210+
throw error;
211+
}
212+
}
213+
214+
/**
215+
* Trigger a hook with all registered handlers
216+
* @param name - Hook name
217+
* @param args - Arguments to pass to handlers
218+
*/
219+
protected async triggerHook(name: string, ...args: any[]): Promise<void> {
220+
const handlers = this.hooks.get(name) || [];
221+
this.logger.debug(`Triggering hook: ${name}`, {
222+
hook: name,
223+
handlerCount: handlers.length
224+
});
225+
226+
for (const handler of handlers) {
227+
try {
228+
await handler(...args);
229+
} catch (error) {
230+
this.logger.error(`Hook handler failed: ${name}`, error as Error);
231+
// Continue with other handlers even if one fails
232+
}
233+
}
234+
}
235+
236+
/**
237+
* Get current kernel state
238+
*/
239+
getState(): KernelState {
240+
return this.state;
241+
}
242+
243+
/**
244+
* Get all registered plugins
245+
*/
246+
getPlugins(): Map<string, Plugin> {
247+
return new Map(this.plugins);
248+
}
249+
250+
/**
251+
* Abstract methods to be implemented by subclasses
252+
*/
253+
abstract use(plugin: Plugin): this | Promise<this>;
254+
abstract bootstrap(): Promise<void>;
255+
abstract destroy(): Promise<void>;
256+
}

0 commit comments

Comments
 (0)