@objectstack/core 0.6.1 → 0.7.1

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 +7 -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,435 @@
1
+ import { Plugin, PluginContext } from './types.js';
2
+ import type { Logger } from '@objectstack/spec/contracts';
3
+ import { z } from 'zod';
4
+
5
+ /**
6
+ * Service Lifecycle Types
7
+ * Defines how services are instantiated and managed
8
+ */
9
+ export enum ServiceLifecycle {
10
+ /** Single instance shared across all requests */
11
+ SINGLETON = 'singleton',
12
+ /** New instance created for each request */
13
+ TRANSIENT = 'transient',
14
+ /** New instance per scope (e.g., per HTTP request) */
15
+ SCOPED = 'scoped',
16
+ }
17
+
18
+ /**
19
+ * Service Factory
20
+ * Function that creates a service instance
21
+ */
22
+ export type ServiceFactory<T = any> = (ctx: PluginContext) => T | Promise<T>;
23
+
24
+ /**
25
+ * Service Registration Options
26
+ */
27
+ export interface ServiceRegistration {
28
+ name: string;
29
+ factory: ServiceFactory;
30
+ lifecycle: ServiceLifecycle;
31
+ dependencies?: string[];
32
+ }
33
+
34
+ /**
35
+ * Plugin Configuration Validator
36
+ * Uses Zod for runtime validation of plugin configurations
37
+ */
38
+ export interface PluginConfigValidator {
39
+ schema: z.ZodSchema;
40
+ validate(config: any): any;
41
+ }
42
+
43
+ /**
44
+ * Plugin Metadata with Enhanced Features
45
+ */
46
+ export interface PluginMetadata extends Plugin {
47
+ /** Semantic version (e.g., "1.0.0") */
48
+ version: string;
49
+
50
+ /** Configuration schema for validation */
51
+ configSchema?: z.ZodSchema;
52
+
53
+ /** Plugin signature for security verification */
54
+ signature?: string;
55
+
56
+ /** Plugin health check function */
57
+ healthCheck?(): Promise<PluginHealthStatus>;
58
+
59
+ /** Startup timeout in milliseconds (default: 30000) */
60
+ startupTimeout?: number;
61
+
62
+ /** Whether plugin supports hot reload */
63
+ hotReloadable?: boolean;
64
+ }
65
+
66
+ /**
67
+ * Plugin Health Status
68
+ */
69
+ export interface PluginHealthStatus {
70
+ healthy: boolean;
71
+ message?: string;
72
+ details?: Record<string, any>;
73
+ lastCheck?: Date;
74
+ }
75
+
76
+ /**
77
+ * Plugin Load Result
78
+ */
79
+ export interface PluginLoadResult {
80
+ success: boolean;
81
+ plugin?: PluginMetadata;
82
+ error?: Error;
83
+ loadTime?: number;
84
+ }
85
+
86
+ /**
87
+ * Plugin Startup Result
88
+ */
89
+ export interface PluginStartupResult {
90
+ success: boolean;
91
+ pluginName: string;
92
+ startTime?: number;
93
+ error?: Error;
94
+ timedOut?: boolean;
95
+ }
96
+
97
+ /**
98
+ * Version Compatibility Result
99
+ */
100
+ export interface VersionCompatibility {
101
+ compatible: boolean;
102
+ pluginVersion: string;
103
+ requiredVersion?: string;
104
+ message?: string;
105
+ }
106
+
107
+ /**
108
+ * Enhanced Plugin Loader
109
+ * Provides advanced plugin loading capabilities with validation, security, and lifecycle management
110
+ */
111
+ export class PluginLoader {
112
+ private logger: Logger;
113
+ private loadedPlugins: Map<string, PluginMetadata> = new Map();
114
+ private serviceFactories: Map<string, ServiceRegistration> = new Map();
115
+ private serviceInstances: Map<string, any> = new Map();
116
+ private scopedServices: Map<string, Map<string, any>> = new Map();
117
+
118
+ constructor(logger: Logger) {
119
+ this.logger = logger;
120
+ }
121
+
122
+ /**
123
+ * Load a plugin asynchronously with validation
124
+ */
125
+ async loadPlugin(plugin: Plugin): Promise<PluginLoadResult> {
126
+ const startTime = Date.now();
127
+
128
+ try {
129
+ this.logger.info(`Loading plugin: ${plugin.name}`);
130
+
131
+ // Convert to PluginMetadata
132
+ const metadata = this.toPluginMetadata(plugin);
133
+
134
+ // Validate plugin structure
135
+ this.validatePluginStructure(metadata);
136
+
137
+ // Check version compatibility
138
+ const versionCheck = this.checkVersionCompatibility(metadata);
139
+ if (!versionCheck.compatible) {
140
+ throw new Error(`Version incompatible: ${versionCheck.message}`);
141
+ }
142
+
143
+ // Validate configuration if schema is provided
144
+ if (metadata.configSchema) {
145
+ this.validatePluginConfig(metadata);
146
+ }
147
+
148
+ // Verify signature if provided
149
+ if (metadata.signature) {
150
+ await this.verifyPluginSignature(metadata);
151
+ }
152
+
153
+ // Store loaded plugin
154
+ this.loadedPlugins.set(metadata.name, metadata);
155
+
156
+ const loadTime = Date.now() - startTime;
157
+ this.logger.info(`Plugin loaded: ${plugin.name} (${loadTime}ms)`);
158
+
159
+ return {
160
+ success: true,
161
+ plugin: metadata,
162
+ loadTime,
163
+ };
164
+ } catch (error) {
165
+ this.logger.error(`Failed to load plugin: ${plugin.name}`, error as Error);
166
+ return {
167
+ success: false,
168
+ error: error as Error,
169
+ loadTime: Date.now() - startTime,
170
+ };
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Register a service with factory function
176
+ */
177
+ registerServiceFactory(registration: ServiceRegistration): void {
178
+ if (this.serviceFactories.has(registration.name)) {
179
+ throw new Error(`Service factory '${registration.name}' already registered`);
180
+ }
181
+
182
+ this.serviceFactories.set(registration.name, registration);
183
+ this.logger.debug(`Service factory registered: ${registration.name} (${registration.lifecycle})`);
184
+ }
185
+
186
+ /**
187
+ * Get or create a service instance based on lifecycle type
188
+ */
189
+ async getService<T>(name: string, scopeId?: string): Promise<T> {
190
+ const registration = this.serviceFactories.get(name);
191
+
192
+ if (!registration) {
193
+ // Fall back to static service instances
194
+ const instance = this.serviceInstances.get(name);
195
+ if (!instance) {
196
+ throw new Error(`Service '${name}' not found`);
197
+ }
198
+ return instance as T;
199
+ }
200
+
201
+ switch (registration.lifecycle) {
202
+ case ServiceLifecycle.SINGLETON:
203
+ return await this.getSingletonService<T>(registration);
204
+
205
+ case ServiceLifecycle.TRANSIENT:
206
+ return await this.createTransientService<T>(registration);
207
+
208
+ case ServiceLifecycle.SCOPED:
209
+ if (!scopeId) {
210
+ throw new Error(`Scope ID required for scoped service '${name}'`);
211
+ }
212
+ return await this.getScopedService<T>(registration, scopeId);
213
+
214
+ default:
215
+ throw new Error(`Unknown service lifecycle: ${registration.lifecycle}`);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Register a static service instance (legacy support)
221
+ */
222
+ registerService(name: string, service: any): void {
223
+ if (this.serviceInstances.has(name)) {
224
+ throw new Error(`Service '${name}' already registered`);
225
+ }
226
+ this.serviceInstances.set(name, service);
227
+ }
228
+
229
+ /**
230
+ * Detect circular dependencies in service factories
231
+ * Note: This only detects cycles in service dependencies, not plugin dependencies.
232
+ * Plugin dependency cycles are detected in the kernel's resolveDependencies method.
233
+ */
234
+ detectCircularDependencies(): string[] {
235
+ const cycles: string[] = [];
236
+ const visited = new Set<string>();
237
+ const visiting = new Set<string>();
238
+
239
+ const visit = (serviceName: string, path: string[] = []) => {
240
+ if (visiting.has(serviceName)) {
241
+ const cycle = [...path, serviceName].join(' -> ');
242
+ cycles.push(cycle);
243
+ return;
244
+ }
245
+
246
+ if (visited.has(serviceName)) {
247
+ return;
248
+ }
249
+
250
+ visiting.add(serviceName);
251
+
252
+ const registration = this.serviceFactories.get(serviceName);
253
+ if (registration?.dependencies) {
254
+ for (const dep of registration.dependencies) {
255
+ visit(dep, [...path, serviceName]);
256
+ }
257
+ }
258
+
259
+ visiting.delete(serviceName);
260
+ visited.add(serviceName);
261
+ };
262
+
263
+ for (const serviceName of this.serviceFactories.keys()) {
264
+ visit(serviceName);
265
+ }
266
+
267
+ return cycles;
268
+ }
269
+
270
+ /**
271
+ * Check plugin health
272
+ */
273
+ async checkPluginHealth(pluginName: string): Promise<PluginHealthStatus> {
274
+ const plugin = this.loadedPlugins.get(pluginName);
275
+
276
+ if (!plugin) {
277
+ return {
278
+ healthy: false,
279
+ message: 'Plugin not found',
280
+ lastCheck: new Date(),
281
+ };
282
+ }
283
+
284
+ if (!plugin.healthCheck) {
285
+ return {
286
+ healthy: true,
287
+ message: 'No health check defined',
288
+ lastCheck: new Date(),
289
+ };
290
+ }
291
+
292
+ try {
293
+ const status = await plugin.healthCheck();
294
+ return {
295
+ ...status,
296
+ lastCheck: new Date(),
297
+ };
298
+ } catch (error) {
299
+ return {
300
+ healthy: false,
301
+ message: `Health check failed: ${(error as Error).message}`,
302
+ lastCheck: new Date(),
303
+ };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Clear scoped services for a scope
309
+ */
310
+ clearScope(scopeId: string): void {
311
+ this.scopedServices.delete(scopeId);
312
+ this.logger.debug(`Cleared scope: ${scopeId}`);
313
+ }
314
+
315
+ /**
316
+ * Get all loaded plugins
317
+ */
318
+ getLoadedPlugins(): Map<string, PluginMetadata> {
319
+ return new Map(this.loadedPlugins);
320
+ }
321
+
322
+ // Private helper methods
323
+
324
+ private toPluginMetadata(plugin: Plugin): PluginMetadata {
325
+ return {
326
+ ...plugin,
327
+ version: plugin.version || '0.0.0',
328
+ } as PluginMetadata;
329
+ }
330
+
331
+ private validatePluginStructure(plugin: PluginMetadata): void {
332
+ if (!plugin.name) {
333
+ throw new Error('Plugin name is required');
334
+ }
335
+
336
+ if (!plugin.init) {
337
+ throw new Error('Plugin init function is required');
338
+ }
339
+
340
+ if (!this.isValidSemanticVersion(plugin.version)) {
341
+ throw new Error(`Invalid semantic version: ${plugin.version}`);
342
+ }
343
+ }
344
+
345
+ private checkVersionCompatibility(plugin: PluginMetadata): VersionCompatibility {
346
+ // Basic semantic version compatibility check
347
+ // In a real implementation, this would check against kernel version
348
+ const version = plugin.version;
349
+
350
+ if (!this.isValidSemanticVersion(version)) {
351
+ return {
352
+ compatible: false,
353
+ pluginVersion: version,
354
+ message: 'Invalid semantic version format',
355
+ };
356
+ }
357
+
358
+ return {
359
+ compatible: true,
360
+ pluginVersion: version,
361
+ };
362
+ }
363
+
364
+ private isValidSemanticVersion(version: string): boolean {
365
+ const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/;
366
+ return semverRegex.test(version);
367
+ }
368
+
369
+ private validatePluginConfig(plugin: PluginMetadata): void {
370
+ if (!plugin.configSchema) {
371
+ return;
372
+ }
373
+
374
+ // TODO: Configuration validation implementation
375
+ // This requires plugin config to be passed during loading
376
+ // For now, just validate that the schema exists
377
+ this.logger.debug(`Plugin ${plugin.name} has configuration schema (validation not yet implemented)`);
378
+ }
379
+
380
+ private async verifyPluginSignature(plugin: PluginMetadata): Promise<void> {
381
+ if (!plugin.signature) {
382
+ return;
383
+ }
384
+
385
+ // TODO: Plugin signature verification implementation
386
+ // In a real implementation:
387
+ // 1. Extract public key from trusted source
388
+ // 2. Verify signature against plugin code hash
389
+ // 3. Throw error if verification fails
390
+ this.logger.debug(`Plugin ${plugin.name} signature verification (not yet implemented)`);
391
+ }
392
+
393
+ private async getSingletonService<T>(registration: ServiceRegistration): Promise<T> {
394
+ let instance = this.serviceInstances.get(registration.name);
395
+
396
+ if (!instance) {
397
+ // Create instance (would need context)
398
+ instance = await this.createServiceInstance(registration);
399
+ this.serviceInstances.set(registration.name, instance);
400
+ this.logger.debug(`Singleton service created: ${registration.name}`);
401
+ }
402
+
403
+ return instance as T;
404
+ }
405
+
406
+ private async createTransientService<T>(registration: ServiceRegistration): Promise<T> {
407
+ const instance = await this.createServiceInstance(registration);
408
+ this.logger.debug(`Transient service created: ${registration.name}`);
409
+ return instance as T;
410
+ }
411
+
412
+ private async getScopedService<T>(registration: ServiceRegistration, scopeId: string): Promise<T> {
413
+ if (!this.scopedServices.has(scopeId)) {
414
+ this.scopedServices.set(scopeId, new Map());
415
+ }
416
+
417
+ const scope = this.scopedServices.get(scopeId)!;
418
+ let instance = scope.get(registration.name);
419
+
420
+ if (!instance) {
421
+ instance = await this.createServiceInstance(registration);
422
+ scope.set(registration.name, instance);
423
+ this.logger.debug(`Scoped service created: ${registration.name} (scope: ${scopeId})`);
424
+ }
425
+
426
+ return instance as T;
427
+ }
428
+
429
+ private async createServiceInstance(registration: ServiceRegistration): Promise<any> {
430
+ // This is a simplified version - in real implementation,
431
+ // we would need to pass proper context with resolved dependencies
432
+ const mockContext = {} as PluginContext;
433
+ return await registration.factory(mockContext);
434
+ }
435
+ }
package/src/types.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
  /**
4
5
  * PluginContext - Runtime context available to plugins
@@ -47,7 +48,7 @@ export interface PluginContext {
47
48
  /**
48
49
  * Logger instance
49
50
  */
50
- logger: Console;
51
+ logger: Logger;
51
52
 
52
53
  /**
53
54
  * Get the kernel instance (for advanced use cases)
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ },
8
+ });