@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/ENHANCED_FEATURES.md +380 -0
  3. package/README.md +299 -12
  4. package/dist/contracts/data-engine.d.ts +39 -22
  5. package/dist/contracts/data-engine.d.ts.map +1 -1
  6. package/dist/contracts/logger.d.ts +63 -0
  7. package/dist/contracts/logger.d.ts.map +1 -0
  8. package/dist/contracts/logger.js +1 -0
  9. package/dist/enhanced-kernel.d.ts +103 -0
  10. package/dist/enhanced-kernel.d.ts.map +1 -0
  11. package/dist/enhanced-kernel.js +403 -0
  12. package/dist/enhanced-kernel.test.d.ts +2 -0
  13. package/dist/enhanced-kernel.test.d.ts.map +1 -0
  14. package/dist/enhanced-kernel.test.js +412 -0
  15. package/dist/index.d.ts +11 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +10 -2
  18. package/dist/kernel-base.d.ts +84 -0
  19. package/dist/kernel-base.d.ts.map +1 -0
  20. package/dist/kernel-base.js +219 -0
  21. package/dist/kernel.d.ts +11 -18
  22. package/dist/kernel.d.ts.map +1 -1
  23. package/dist/kernel.js +43 -114
  24. package/dist/kernel.test.d.ts +2 -0
  25. package/dist/kernel.test.d.ts.map +1 -0
  26. package/dist/kernel.test.js +161 -0
  27. package/dist/logger.d.ts +70 -0
  28. package/dist/logger.d.ts.map +1 -0
  29. package/dist/logger.js +268 -0
  30. package/dist/logger.test.d.ts +2 -0
  31. package/dist/logger.test.d.ts.map +1 -0
  32. package/dist/logger.test.js +92 -0
  33. package/dist/plugin-loader.d.ts +148 -0
  34. package/dist/plugin-loader.d.ts.map +1 -0
  35. package/dist/plugin-loader.js +287 -0
  36. package/dist/plugin-loader.test.d.ts +2 -0
  37. package/dist/plugin-loader.test.d.ts.map +1 -0
  38. package/dist/plugin-loader.test.js +339 -0
  39. package/dist/types.d.ts +2 -1
  40. package/dist/types.d.ts.map +1 -1
  41. package/examples/enhanced-kernel-example.ts +309 -0
  42. package/package.json +19 -4
  43. package/src/contracts/data-engine.ts +46 -24
  44. package/src/contracts/logger.ts +70 -0
  45. package/src/enhanced-kernel.test.ts +535 -0
  46. package/src/enhanced-kernel.ts +496 -0
  47. package/src/index.ts +23 -2
  48. package/src/kernel-base.ts +256 -0
  49. package/src/kernel.test.ts +200 -0
  50. package/src/kernel.ts +55 -129
  51. package/src/logger.test.ts +116 -0
  52. package/src/logger.ts +306 -0
  53. package/src/plugin-loader.test.ts +412 -0
  54. package/src/plugin-loader.ts +435 -0
  55. package/src/types.ts +2 -1
  56. 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
+ });