@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,287 @@
1
+ /**
2
+ * Service Lifecycle Types
3
+ * Defines how services are instantiated and managed
4
+ */
5
+ export var ServiceLifecycle;
6
+ (function (ServiceLifecycle) {
7
+ /** Single instance shared across all requests */
8
+ ServiceLifecycle["SINGLETON"] = "singleton";
9
+ /** New instance created for each request */
10
+ ServiceLifecycle["TRANSIENT"] = "transient";
11
+ /** New instance per scope (e.g., per HTTP request) */
12
+ ServiceLifecycle["SCOPED"] = "scoped";
13
+ })(ServiceLifecycle || (ServiceLifecycle = {}));
14
+ /**
15
+ * Enhanced Plugin Loader
16
+ * Provides advanced plugin loading capabilities with validation, security, and lifecycle management
17
+ */
18
+ export class PluginLoader {
19
+ constructor(logger) {
20
+ this.loadedPlugins = new Map();
21
+ this.serviceFactories = new Map();
22
+ this.serviceInstances = new Map();
23
+ this.scopedServices = new Map();
24
+ this.logger = logger;
25
+ }
26
+ /**
27
+ * Load a plugin asynchronously with validation
28
+ */
29
+ async loadPlugin(plugin) {
30
+ const startTime = Date.now();
31
+ try {
32
+ this.logger.info(`Loading plugin: ${plugin.name}`);
33
+ // Convert to PluginMetadata
34
+ const metadata = this.toPluginMetadata(plugin);
35
+ // Validate plugin structure
36
+ this.validatePluginStructure(metadata);
37
+ // Check version compatibility
38
+ const versionCheck = this.checkVersionCompatibility(metadata);
39
+ if (!versionCheck.compatible) {
40
+ throw new Error(`Version incompatible: ${versionCheck.message}`);
41
+ }
42
+ // Validate configuration if schema is provided
43
+ if (metadata.configSchema) {
44
+ this.validatePluginConfig(metadata);
45
+ }
46
+ // Verify signature if provided
47
+ if (metadata.signature) {
48
+ await this.verifyPluginSignature(metadata);
49
+ }
50
+ // Store loaded plugin
51
+ this.loadedPlugins.set(metadata.name, metadata);
52
+ const loadTime = Date.now() - startTime;
53
+ this.logger.info(`Plugin loaded: ${plugin.name} (${loadTime}ms)`);
54
+ return {
55
+ success: true,
56
+ plugin: metadata,
57
+ loadTime,
58
+ };
59
+ }
60
+ catch (error) {
61
+ this.logger.error(`Failed to load plugin: ${plugin.name}`, error);
62
+ return {
63
+ success: false,
64
+ error: error,
65
+ loadTime: Date.now() - startTime,
66
+ };
67
+ }
68
+ }
69
+ /**
70
+ * Register a service with factory function
71
+ */
72
+ registerServiceFactory(registration) {
73
+ if (this.serviceFactories.has(registration.name)) {
74
+ throw new Error(`Service factory '${registration.name}' already registered`);
75
+ }
76
+ this.serviceFactories.set(registration.name, registration);
77
+ this.logger.debug(`Service factory registered: ${registration.name} (${registration.lifecycle})`);
78
+ }
79
+ /**
80
+ * Get or create a service instance based on lifecycle type
81
+ */
82
+ async getService(name, scopeId) {
83
+ const registration = this.serviceFactories.get(name);
84
+ if (!registration) {
85
+ // Fall back to static service instances
86
+ const instance = this.serviceInstances.get(name);
87
+ if (!instance) {
88
+ throw new Error(`Service '${name}' not found`);
89
+ }
90
+ return instance;
91
+ }
92
+ switch (registration.lifecycle) {
93
+ case ServiceLifecycle.SINGLETON:
94
+ return await this.getSingletonService(registration);
95
+ case ServiceLifecycle.TRANSIENT:
96
+ return await this.createTransientService(registration);
97
+ case ServiceLifecycle.SCOPED:
98
+ if (!scopeId) {
99
+ throw new Error(`Scope ID required for scoped service '${name}'`);
100
+ }
101
+ return await this.getScopedService(registration, scopeId);
102
+ default:
103
+ throw new Error(`Unknown service lifecycle: ${registration.lifecycle}`);
104
+ }
105
+ }
106
+ /**
107
+ * Register a static service instance (legacy support)
108
+ */
109
+ registerService(name, service) {
110
+ if (this.serviceInstances.has(name)) {
111
+ throw new Error(`Service '${name}' already registered`);
112
+ }
113
+ this.serviceInstances.set(name, service);
114
+ }
115
+ /**
116
+ * Detect circular dependencies in service factories
117
+ * Note: This only detects cycles in service dependencies, not plugin dependencies.
118
+ * Plugin dependency cycles are detected in the kernel's resolveDependencies method.
119
+ */
120
+ detectCircularDependencies() {
121
+ const cycles = [];
122
+ const visited = new Set();
123
+ const visiting = new Set();
124
+ const visit = (serviceName, path = []) => {
125
+ if (visiting.has(serviceName)) {
126
+ const cycle = [...path, serviceName].join(' -> ');
127
+ cycles.push(cycle);
128
+ return;
129
+ }
130
+ if (visited.has(serviceName)) {
131
+ return;
132
+ }
133
+ visiting.add(serviceName);
134
+ const registration = this.serviceFactories.get(serviceName);
135
+ if (registration?.dependencies) {
136
+ for (const dep of registration.dependencies) {
137
+ visit(dep, [...path, serviceName]);
138
+ }
139
+ }
140
+ visiting.delete(serviceName);
141
+ visited.add(serviceName);
142
+ };
143
+ for (const serviceName of this.serviceFactories.keys()) {
144
+ visit(serviceName);
145
+ }
146
+ return cycles;
147
+ }
148
+ /**
149
+ * Check plugin health
150
+ */
151
+ async checkPluginHealth(pluginName) {
152
+ const plugin = this.loadedPlugins.get(pluginName);
153
+ if (!plugin) {
154
+ return {
155
+ healthy: false,
156
+ message: 'Plugin not found',
157
+ lastCheck: new Date(),
158
+ };
159
+ }
160
+ if (!plugin.healthCheck) {
161
+ return {
162
+ healthy: true,
163
+ message: 'No health check defined',
164
+ lastCheck: new Date(),
165
+ };
166
+ }
167
+ try {
168
+ const status = await plugin.healthCheck();
169
+ return {
170
+ ...status,
171
+ lastCheck: new Date(),
172
+ };
173
+ }
174
+ catch (error) {
175
+ return {
176
+ healthy: false,
177
+ message: `Health check failed: ${error.message}`,
178
+ lastCheck: new Date(),
179
+ };
180
+ }
181
+ }
182
+ /**
183
+ * Clear scoped services for a scope
184
+ */
185
+ clearScope(scopeId) {
186
+ this.scopedServices.delete(scopeId);
187
+ this.logger.debug(`Cleared scope: ${scopeId}`);
188
+ }
189
+ /**
190
+ * Get all loaded plugins
191
+ */
192
+ getLoadedPlugins() {
193
+ return new Map(this.loadedPlugins);
194
+ }
195
+ // Private helper methods
196
+ toPluginMetadata(plugin) {
197
+ return {
198
+ ...plugin,
199
+ version: plugin.version || '0.0.0',
200
+ };
201
+ }
202
+ validatePluginStructure(plugin) {
203
+ if (!plugin.name) {
204
+ throw new Error('Plugin name is required');
205
+ }
206
+ if (!plugin.init) {
207
+ throw new Error('Plugin init function is required');
208
+ }
209
+ if (!this.isValidSemanticVersion(plugin.version)) {
210
+ throw new Error(`Invalid semantic version: ${plugin.version}`);
211
+ }
212
+ }
213
+ checkVersionCompatibility(plugin) {
214
+ // Basic semantic version compatibility check
215
+ // In a real implementation, this would check against kernel version
216
+ const version = plugin.version;
217
+ if (!this.isValidSemanticVersion(version)) {
218
+ return {
219
+ compatible: false,
220
+ pluginVersion: version,
221
+ message: 'Invalid semantic version format',
222
+ };
223
+ }
224
+ return {
225
+ compatible: true,
226
+ pluginVersion: version,
227
+ };
228
+ }
229
+ isValidSemanticVersion(version) {
230
+ const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/;
231
+ return semverRegex.test(version);
232
+ }
233
+ validatePluginConfig(plugin) {
234
+ if (!plugin.configSchema) {
235
+ return;
236
+ }
237
+ // TODO: Configuration validation implementation
238
+ // This requires plugin config to be passed during loading
239
+ // For now, just validate that the schema exists
240
+ this.logger.debug(`Plugin ${plugin.name} has configuration schema (validation not yet implemented)`);
241
+ }
242
+ async verifyPluginSignature(plugin) {
243
+ if (!plugin.signature) {
244
+ return;
245
+ }
246
+ // TODO: Plugin signature verification implementation
247
+ // In a real implementation:
248
+ // 1. Extract public key from trusted source
249
+ // 2. Verify signature against plugin code hash
250
+ // 3. Throw error if verification fails
251
+ this.logger.debug(`Plugin ${plugin.name} signature verification (not yet implemented)`);
252
+ }
253
+ async getSingletonService(registration) {
254
+ let instance = this.serviceInstances.get(registration.name);
255
+ if (!instance) {
256
+ // Create instance (would need context)
257
+ instance = await this.createServiceInstance(registration);
258
+ this.serviceInstances.set(registration.name, instance);
259
+ this.logger.debug(`Singleton service created: ${registration.name}`);
260
+ }
261
+ return instance;
262
+ }
263
+ async createTransientService(registration) {
264
+ const instance = await this.createServiceInstance(registration);
265
+ this.logger.debug(`Transient service created: ${registration.name}`);
266
+ return instance;
267
+ }
268
+ async getScopedService(registration, scopeId) {
269
+ if (!this.scopedServices.has(scopeId)) {
270
+ this.scopedServices.set(scopeId, new Map());
271
+ }
272
+ const scope = this.scopedServices.get(scopeId);
273
+ let instance = scope.get(registration.name);
274
+ if (!instance) {
275
+ instance = await this.createServiceInstance(registration);
276
+ scope.set(registration.name, instance);
277
+ this.logger.debug(`Scoped service created: ${registration.name} (scope: ${scopeId})`);
278
+ }
279
+ return instance;
280
+ }
281
+ async createServiceInstance(registration) {
282
+ // This is a simplified version - in real implementation,
283
+ // we would need to pass proper context with resolved dependencies
284
+ const mockContext = {};
285
+ return await registration.factory(mockContext);
286
+ }
287
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plugin-loader.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-loader.test.d.ts","sourceRoot":"","sources":["../src/plugin-loader.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,339 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PluginLoader, ServiceLifecycle } from './plugin-loader';
3
+ import { createLogger } from './logger';
4
+ describe('PluginLoader', () => {
5
+ let loader;
6
+ beforeEach(() => {
7
+ const logger = createLogger({ level: 'error' }); // Suppress logs in tests
8
+ loader = new PluginLoader(logger);
9
+ });
10
+ describe('Plugin Loading', () => {
11
+ it('should load a valid plugin', async () => {
12
+ const plugin = {
13
+ name: 'test-plugin',
14
+ version: '1.0.0',
15
+ init: async () => { },
16
+ };
17
+ const result = await loader.loadPlugin(plugin);
18
+ expect(result.success).toBe(true);
19
+ expect(result.plugin?.name).toBe('test-plugin');
20
+ expect(result.plugin?.version).toBe('1.0.0');
21
+ expect(result.loadTime).toBeGreaterThanOrEqual(0);
22
+ });
23
+ it('should reject plugin with invalid name', async () => {
24
+ const plugin = {
25
+ name: '',
26
+ init: async () => { },
27
+ };
28
+ const result = await loader.loadPlugin(plugin);
29
+ expect(result.success).toBe(false);
30
+ expect(result.error?.message).toContain('name is required');
31
+ });
32
+ it('should reject plugin without init function', async () => {
33
+ const plugin = {
34
+ name: 'invalid-plugin',
35
+ };
36
+ const result = await loader.loadPlugin(plugin);
37
+ expect(result.success).toBe(false);
38
+ expect(result.error?.message).toContain('init function is required');
39
+ });
40
+ it('should use default version 0.0.0 if not provided', async () => {
41
+ const plugin = {
42
+ name: 'no-version',
43
+ init: async () => { },
44
+ };
45
+ const result = await loader.loadPlugin(plugin);
46
+ expect(result.success).toBe(true);
47
+ expect(result.plugin?.version).toBe('0.0.0');
48
+ });
49
+ });
50
+ describe('Version Compatibility', () => {
51
+ it('should accept valid semantic versions', async () => {
52
+ const validVersions = ['1.0.0', '2.3.4', '0.0.1', '10.20.30'];
53
+ for (const version of validVersions) {
54
+ const plugin = {
55
+ name: `plugin-${version}`,
56
+ version,
57
+ init: async () => { },
58
+ };
59
+ const result = await loader.loadPlugin(plugin);
60
+ expect(result.success).toBe(true);
61
+ }
62
+ });
63
+ it('should accept versions with pre-release tags', async () => {
64
+ const plugin = {
65
+ name: 'prerelease',
66
+ version: '1.0.0-alpha.1',
67
+ init: async () => { },
68
+ };
69
+ const result = await loader.loadPlugin(plugin);
70
+ expect(result.success).toBe(true);
71
+ });
72
+ it('should accept versions with build metadata', async () => {
73
+ const plugin = {
74
+ name: 'build-meta',
75
+ version: '1.0.0+20230101',
76
+ init: async () => { },
77
+ };
78
+ const result = await loader.loadPlugin(plugin);
79
+ expect(result.success).toBe(true);
80
+ });
81
+ it('should reject invalid semantic versions', async () => {
82
+ const invalidVersions = ['1.0', 'v1.0.0', '1', 'invalid'];
83
+ for (const version of invalidVersions) {
84
+ const plugin = {
85
+ name: `invalid-${version}`,
86
+ version,
87
+ init: async () => { },
88
+ };
89
+ const result = await loader.loadPlugin(plugin);
90
+ expect(result.success).toBe(false);
91
+ }
92
+ });
93
+ });
94
+ describe('Service Factory Registration', () => {
95
+ it('should register a singleton service factory', () => {
96
+ let callCount = 0;
97
+ const factory = () => {
98
+ callCount++;
99
+ return { value: callCount };
100
+ };
101
+ loader.registerServiceFactory({
102
+ name: 'singleton-service',
103
+ factory,
104
+ lifecycle: ServiceLifecycle.SINGLETON,
105
+ });
106
+ expect(() => {
107
+ loader.registerServiceFactory({
108
+ name: 'singleton-service',
109
+ factory,
110
+ lifecycle: ServiceLifecycle.SINGLETON,
111
+ });
112
+ }).toThrow('already registered');
113
+ });
114
+ it('should register multiple service factories with different names', () => {
115
+ loader.registerServiceFactory({
116
+ name: 'service-1',
117
+ factory: () => ({ id: 1 }),
118
+ lifecycle: ServiceLifecycle.SINGLETON,
119
+ });
120
+ loader.registerServiceFactory({
121
+ name: 'service-2',
122
+ factory: () => ({ id: 2 }),
123
+ lifecycle: ServiceLifecycle.TRANSIENT,
124
+ });
125
+ // Should not throw
126
+ expect(true).toBe(true);
127
+ });
128
+ });
129
+ describe('Service Retrieval with Lifecycle', () => {
130
+ it('should create singleton service only once', async () => {
131
+ let callCount = 0;
132
+ loader.registerServiceFactory({
133
+ name: 'counter',
134
+ factory: () => {
135
+ callCount++;
136
+ return { count: callCount };
137
+ },
138
+ lifecycle: ServiceLifecycle.SINGLETON,
139
+ });
140
+ const service1 = await loader.getService('counter');
141
+ const service2 = await loader.getService('counter');
142
+ expect(callCount).toBe(1);
143
+ expect(service1).toBe(service2);
144
+ });
145
+ it('should create new transient service on each request', async () => {
146
+ let callCount = 0;
147
+ loader.registerServiceFactory({
148
+ name: 'transient',
149
+ factory: () => {
150
+ callCount++;
151
+ return { count: callCount };
152
+ },
153
+ lifecycle: ServiceLifecycle.TRANSIENT,
154
+ });
155
+ const service1 = await loader.getService('transient');
156
+ const service2 = await loader.getService('transient');
157
+ expect(callCount).toBe(2);
158
+ expect(service1).not.toBe(service2);
159
+ expect(service1.count).toBe(1);
160
+ expect(service2.count).toBe(2);
161
+ });
162
+ it('should create scoped service once per scope', async () => {
163
+ let callCount = 0;
164
+ loader.registerServiceFactory({
165
+ name: 'scoped',
166
+ factory: () => {
167
+ callCount++;
168
+ return { count: callCount };
169
+ },
170
+ lifecycle: ServiceLifecycle.SCOPED,
171
+ });
172
+ const scope1Service1 = await loader.getService('scoped', 'scope-1');
173
+ const scope1Service2 = await loader.getService('scoped', 'scope-1');
174
+ const scope2Service1 = await loader.getService('scoped', 'scope-2');
175
+ expect(callCount).toBe(2); // Once per scope
176
+ expect(scope1Service1).toBe(scope1Service2); // Same within scope
177
+ expect(scope1Service1).not.toBe(scope2Service1); // Different across scopes
178
+ });
179
+ it('should throw error for scoped service without scope ID', async () => {
180
+ loader.registerServiceFactory({
181
+ name: 'scoped-no-id',
182
+ factory: () => ({ value: 'test' }),
183
+ lifecycle: ServiceLifecycle.SCOPED,
184
+ });
185
+ await expect(async () => {
186
+ await loader.getService('scoped-no-id');
187
+ }).rejects.toThrow('Scope ID required');
188
+ });
189
+ it('should throw error for non-existent service', async () => {
190
+ await expect(async () => {
191
+ await loader.getService('non-existent');
192
+ }).rejects.toThrow('not found');
193
+ });
194
+ });
195
+ describe('Circular Dependency Detection', () => {
196
+ it('should detect simple circular dependency', () => {
197
+ loader.registerServiceFactory({
198
+ name: 'service-a',
199
+ factory: () => ({}),
200
+ lifecycle: ServiceLifecycle.SINGLETON,
201
+ dependencies: ['service-b'],
202
+ });
203
+ loader.registerServiceFactory({
204
+ name: 'service-b',
205
+ factory: () => ({}),
206
+ lifecycle: ServiceLifecycle.SINGLETON,
207
+ dependencies: ['service-a'],
208
+ });
209
+ const cycles = loader.detectCircularDependencies();
210
+ expect(cycles.length).toBeGreaterThan(0);
211
+ expect(cycles[0]).toContain('service-a');
212
+ expect(cycles[0]).toContain('service-b');
213
+ });
214
+ it('should detect complex circular dependency', () => {
215
+ loader.registerServiceFactory({
216
+ name: 'service-a',
217
+ factory: () => ({}),
218
+ lifecycle: ServiceLifecycle.SINGLETON,
219
+ dependencies: ['service-b'],
220
+ });
221
+ loader.registerServiceFactory({
222
+ name: 'service-b',
223
+ factory: () => ({}),
224
+ lifecycle: ServiceLifecycle.SINGLETON,
225
+ dependencies: ['service-c'],
226
+ });
227
+ loader.registerServiceFactory({
228
+ name: 'service-c',
229
+ factory: () => ({}),
230
+ lifecycle: ServiceLifecycle.SINGLETON,
231
+ dependencies: ['service-a'],
232
+ });
233
+ const cycles = loader.detectCircularDependencies();
234
+ expect(cycles.length).toBeGreaterThan(0);
235
+ });
236
+ it('should not report false positives for valid dependency chains', () => {
237
+ loader.registerServiceFactory({
238
+ name: 'service-a',
239
+ factory: () => ({}),
240
+ lifecycle: ServiceLifecycle.SINGLETON,
241
+ dependencies: ['service-b'],
242
+ });
243
+ loader.registerServiceFactory({
244
+ name: 'service-b',
245
+ factory: () => ({}),
246
+ lifecycle: ServiceLifecycle.SINGLETON,
247
+ dependencies: ['service-c'],
248
+ });
249
+ loader.registerServiceFactory({
250
+ name: 'service-c',
251
+ factory: () => ({}),
252
+ lifecycle: ServiceLifecycle.SINGLETON,
253
+ });
254
+ const cycles = loader.detectCircularDependencies();
255
+ expect(cycles.length).toBe(0);
256
+ });
257
+ });
258
+ describe('Plugin Health Checks', () => {
259
+ it('should return healthy for plugin without health check', async () => {
260
+ const plugin = {
261
+ name: 'no-health-check',
262
+ version: '1.0.0',
263
+ init: async () => { },
264
+ };
265
+ await loader.loadPlugin(plugin);
266
+ const health = await loader.checkPluginHealth('no-health-check');
267
+ expect(health.healthy).toBe(true);
268
+ expect(health.message).toContain('No health check');
269
+ });
270
+ it('should execute plugin health check', async () => {
271
+ const plugin = {
272
+ name: 'with-health-check',
273
+ version: '1.0.0',
274
+ init: async () => { },
275
+ healthCheck: async () => ({
276
+ healthy: true,
277
+ message: 'All systems operational',
278
+ }),
279
+ };
280
+ await loader.loadPlugin(plugin);
281
+ const health = await loader.checkPluginHealth('with-health-check');
282
+ expect(health.healthy).toBe(true);
283
+ expect(health.message).toBe('All systems operational');
284
+ expect(health.lastCheck).toBeInstanceOf(Date);
285
+ });
286
+ it('should handle failing health check', async () => {
287
+ const plugin = {
288
+ name: 'failing-health',
289
+ version: '1.0.0',
290
+ init: async () => { },
291
+ healthCheck: async () => {
292
+ throw new Error('Service unavailable');
293
+ },
294
+ };
295
+ await loader.loadPlugin(plugin);
296
+ const health = await loader.checkPluginHealth('failing-health');
297
+ expect(health.healthy).toBe(false);
298
+ expect(health.message).toContain('Health check failed');
299
+ });
300
+ it('should return not found for unknown plugin', async () => {
301
+ const health = await loader.checkPluginHealth('unknown-plugin');
302
+ expect(health.healthy).toBe(false);
303
+ expect(health.message).toContain('not found');
304
+ });
305
+ });
306
+ describe('Scope Management', () => {
307
+ it('should clear scoped services', async () => {
308
+ let callCount = 0;
309
+ loader.registerServiceFactory({
310
+ name: 'scoped-clear',
311
+ factory: () => {
312
+ callCount++;
313
+ return { count: callCount };
314
+ },
315
+ lifecycle: ServiceLifecycle.SCOPED,
316
+ });
317
+ const service1 = await loader.getService('scoped-clear', 'scope-1');
318
+ expect(service1.count).toBe(1);
319
+ loader.clearScope('scope-1');
320
+ const service2 = await loader.getService('scoped-clear', 'scope-1');
321
+ expect(service2.count).toBe(2); // New instance created
322
+ });
323
+ });
324
+ describe('Static Service Registration', () => {
325
+ it('should register static service instance', () => {
326
+ const service = { value: 'test' };
327
+ loader.registerService('static-service', service);
328
+ expect(() => {
329
+ loader.registerService('static-service', service);
330
+ }).toThrow('already registered');
331
+ });
332
+ it('should retrieve static service', async () => {
333
+ const service = { value: 'static' };
334
+ loader.registerService('static', service);
335
+ const retrieved = await loader.getService('static');
336
+ expect(retrieved).toBe(service);
337
+ });
338
+ });
339
+ });
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ObjectKernel } from './kernel.js';
2
+ import type { Logger } from '@objectstack/spec/contracts';
2
3
  /**
3
4
  * PluginContext - Runtime context available to plugins
4
5
  *
@@ -41,7 +42,7 @@ export interface PluginContext {
41
42
  /**
42
43
  * Logger instance
43
44
  */
44
- logger: Console;
45
+ logger: Logger;
45
46
  /**
46
47
  * Get the kernel instance (for advanced use cases)
47
48
  * @returns Kernel instance
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC1B;;;;OAIG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC;IAElD;;;;;OAKG;IACH,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IAE/B;;OAEG;IACH,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEhC;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE5E;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAErD;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;OAGG;IACH,SAAS,IAAI,YAAY,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACnB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;;;OAIG;IACH,IAAI,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE/C;;;;OAIG;IACH,KAAK,CAAC,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAEjD;;;OAGG;IACH,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACpC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AAE1D;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC1B;;;;OAIG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC;IAElD;;;;;OAKG;IACH,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IAE/B;;OAEG;IACH,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEhC;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE5E;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAErD;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,IAAI,YAAY,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACnB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;;;OAIG;IACH,IAAI,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAE/C;;;;OAIG;IACH,KAAK,CAAC,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAEjD;;;OAGG;IACH,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACpC"}