@objectstack/core 0.6.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/ENHANCED_FEATURES.md +380 -0
- package/README.md +299 -12
- package/dist/contracts/data-engine.d.ts +39 -22
- package/dist/contracts/data-engine.d.ts.map +1 -1
- package/dist/contracts/logger.d.ts +63 -0
- package/dist/contracts/logger.d.ts.map +1 -0
- package/dist/contracts/logger.js +1 -0
- package/dist/enhanced-kernel.d.ts +103 -0
- package/dist/enhanced-kernel.d.ts.map +1 -0
- package/dist/enhanced-kernel.js +403 -0
- package/dist/enhanced-kernel.test.d.ts +2 -0
- package/dist/enhanced-kernel.test.d.ts.map +1 -0
- package/dist/enhanced-kernel.test.js +412 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/kernel-base.d.ts +84 -0
- package/dist/kernel-base.d.ts.map +1 -0
- package/dist/kernel-base.js +219 -0
- package/dist/kernel.d.ts +11 -18
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +43 -114
- package/dist/kernel.test.d.ts +2 -0
- package/dist/kernel.test.d.ts.map +1 -0
- package/dist/kernel.test.js +161 -0
- package/dist/logger.d.ts +70 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +268 -0
- package/dist/logger.test.d.ts +2 -0
- package/dist/logger.test.d.ts.map +1 -0
- package/dist/logger.test.js +92 -0
- package/dist/plugin-loader.d.ts +148 -0
- package/dist/plugin-loader.d.ts.map +1 -0
- package/dist/plugin-loader.js +287 -0
- package/dist/plugin-loader.test.d.ts +2 -0
- package/dist/plugin-loader.test.d.ts.map +1 -0
- package/dist/plugin-loader.test.js +339 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/examples/enhanced-kernel-example.ts +309 -0
- package/package.json +19 -4
- package/src/contracts/data-engine.ts +46 -24
- package/src/contracts/logger.ts +70 -0
- package/src/enhanced-kernel.test.ts +535 -0
- package/src/enhanced-kernel.ts +496 -0
- package/src/index.ts +23 -2
- package/src/kernel-base.ts +256 -0
- package/src/kernel.test.ts +200 -0
- package/src/kernel.ts +55 -129
- package/src/logger.test.ts +116 -0
- package/src/logger.ts +306 -0
- package/src/plugin-loader.test.ts +412 -0
- package/src/plugin-loader.ts +435 -0
- package/src/types.ts +2 -1
- package/vitest.config.ts +8 -0
|
@@ -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,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { ObjectKernel } from './kernel';
|
|
3
|
+
import type { Plugin } from './types';
|
|
4
|
+
|
|
5
|
+
describe('ObjectKernel with Configurable Logger', () => {
|
|
6
|
+
let kernel: ObjectKernel;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
kernel = new ObjectKernel();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('Logger Configuration', () => {
|
|
13
|
+
it('should create kernel with default logger', () => {
|
|
14
|
+
expect(kernel).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should create kernel with custom logger config', async () => {
|
|
18
|
+
const customKernel = new ObjectKernel({
|
|
19
|
+
logger: {
|
|
20
|
+
level: 'debug',
|
|
21
|
+
format: 'pretty',
|
|
22
|
+
sourceLocation: true
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(customKernel).toBeDefined();
|
|
27
|
+
|
|
28
|
+
// Cleanup
|
|
29
|
+
await customKernel.bootstrap();
|
|
30
|
+
await customKernel.shutdown();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should create kernel with file logging config', async () => {
|
|
34
|
+
const fileKernel = new ObjectKernel({
|
|
35
|
+
logger: {
|
|
36
|
+
level: 'info',
|
|
37
|
+
format: 'json',
|
|
38
|
+
file: '/tmp/test-kernel.log'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(fileKernel).toBeDefined();
|
|
43
|
+
|
|
44
|
+
// Cleanup
|
|
45
|
+
await fileKernel.bootstrap();
|
|
46
|
+
await fileKernel.shutdown();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Plugin Context Logger', () => {
|
|
51
|
+
it('should provide logger to plugins', async () => {
|
|
52
|
+
let loggerReceived = false;
|
|
53
|
+
|
|
54
|
+
const testPlugin: Plugin = {
|
|
55
|
+
name: 'test-plugin',
|
|
56
|
+
init: async (ctx) => {
|
|
57
|
+
if (ctx.logger) {
|
|
58
|
+
loggerReceived = true;
|
|
59
|
+
ctx.logger.info('Plugin initialized', { plugin: 'test-plugin' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
kernel.use(testPlugin);
|
|
65
|
+
await kernel.bootstrap();
|
|
66
|
+
|
|
67
|
+
expect(loggerReceived).toBe(true);
|
|
68
|
+
|
|
69
|
+
await kernel.shutdown();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should allow plugins to use all log levels', async () => {
|
|
73
|
+
const logCalls: string[] = [];
|
|
74
|
+
|
|
75
|
+
const loggingPlugin: Plugin = {
|
|
76
|
+
name: 'logging-plugin',
|
|
77
|
+
init: async (ctx) => {
|
|
78
|
+
ctx.logger.debug('Debug message');
|
|
79
|
+
logCalls.push('debug');
|
|
80
|
+
|
|
81
|
+
ctx.logger.info('Info message');
|
|
82
|
+
logCalls.push('info');
|
|
83
|
+
|
|
84
|
+
ctx.logger.warn('Warning message');
|
|
85
|
+
logCalls.push('warn');
|
|
86
|
+
|
|
87
|
+
ctx.logger.error('Error message');
|
|
88
|
+
logCalls.push('error');
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
kernel.use(loggingPlugin);
|
|
93
|
+
await kernel.bootstrap();
|
|
94
|
+
|
|
95
|
+
expect(logCalls).toContain('debug');
|
|
96
|
+
expect(logCalls).toContain('info');
|
|
97
|
+
expect(logCalls).toContain('warn');
|
|
98
|
+
expect(logCalls).toContain('error');
|
|
99
|
+
|
|
100
|
+
await kernel.shutdown();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should support metadata in logs', async () => {
|
|
104
|
+
const metadataPlugin: Plugin = {
|
|
105
|
+
name: 'metadata-plugin',
|
|
106
|
+
init: async (ctx) => {
|
|
107
|
+
ctx.logger.info('User action', {
|
|
108
|
+
userId: '123',
|
|
109
|
+
action: 'create',
|
|
110
|
+
resource: 'document'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
kernel.use(metadataPlugin);
|
|
116
|
+
await kernel.bootstrap();
|
|
117
|
+
|
|
118
|
+
await kernel.shutdown();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Kernel Lifecycle Logging', () => {
|
|
123
|
+
it('should log bootstrap process', async () => {
|
|
124
|
+
const plugin: Plugin = {
|
|
125
|
+
name: 'lifecycle-test',
|
|
126
|
+
init: async () => {
|
|
127
|
+
// Init logic
|
|
128
|
+
},
|
|
129
|
+
start: async () => {
|
|
130
|
+
// Start logic
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
kernel.use(plugin);
|
|
135
|
+
await kernel.bootstrap();
|
|
136
|
+
|
|
137
|
+
expect(kernel.isRunning()).toBe(true);
|
|
138
|
+
|
|
139
|
+
await kernel.shutdown();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should log shutdown process', async () => {
|
|
143
|
+
const plugin: Plugin = {
|
|
144
|
+
name: 'shutdown-test',
|
|
145
|
+
init: async () => {},
|
|
146
|
+
destroy: async () => {
|
|
147
|
+
// Cleanup
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
kernel.use(plugin);
|
|
152
|
+
await kernel.bootstrap();
|
|
153
|
+
await kernel.shutdown();
|
|
154
|
+
|
|
155
|
+
expect(kernel.getState()).toBe('stopped');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('Environment Compatibility', () => {
|
|
160
|
+
it('should work in Node.js environment', async () => {
|
|
161
|
+
const nodeKernel = new ObjectKernel({
|
|
162
|
+
logger: {
|
|
163
|
+
level: 'info',
|
|
164
|
+
format: 'json'
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const plugin: Plugin = {
|
|
169
|
+
name: 'node-test',
|
|
170
|
+
init: async (ctx) => {
|
|
171
|
+
ctx.logger.info('Running in Node.js');
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
nodeKernel.use(plugin);
|
|
176
|
+
await nodeKernel.bootstrap();
|
|
177
|
+
await nodeKernel.shutdown();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should support browser-friendly logging', async () => {
|
|
181
|
+
const browserKernel = new ObjectKernel({
|
|
182
|
+
logger: {
|
|
183
|
+
level: 'info',
|
|
184
|
+
format: 'pretty'
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const plugin: Plugin = {
|
|
189
|
+
name: 'browser-test',
|
|
190
|
+
init: async (ctx) => {
|
|
191
|
+
ctx.logger.info('Browser-friendly format');
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
browserKernel.use(plugin);
|
|
196
|
+
await browserKernel.bootstrap();
|
|
197
|
+
await browserKernel.shutdown();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|