@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.
- package/CHANGELOG.md +7 -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,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:
|
|
51
|
+
logger: Logger;
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Get the kernel instance (for advanced use cases)
|