mycelia-kernel-plugin 1.3.0 → 1.4.0

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.
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Instrumentation Utilities
3
+ *
4
+ * Provides timing instrumentation for debugging build, initialization, and disposal phases.
5
+ * Helps identify slow hooks and facets when debugging performance issues.
6
+ */
7
+
8
+ import { createSubsystemLogger } from './logger.js';
9
+
10
+ /**
11
+ * Default thresholds for timing warnings (in milliseconds)
12
+ */
13
+ const DEFAULT_THRESHOLDS = {
14
+ hookExecution: 50, // Warn if hook execution takes > 50ms
15
+ facetInit: 100, // Warn if facet init takes > 100ms
16
+ facetDispose: 50, // Warn if facet dispose takes > 50ms
17
+ };
18
+
19
+ /**
20
+ * Check if instrumentation is enabled
21
+ *
22
+ * @param {BaseSubsystem} subsystem - Subsystem instance
23
+ * @returns {boolean} True if instrumentation is enabled
24
+ */
25
+ export function isInstrumentationEnabled(subsystem) {
26
+ // Enable if debug is on, or if instrumentation is explicitly enabled
27
+ return subsystem?.debug === true || subsystem?.ctx?.instrumentation === true;
28
+ }
29
+
30
+ /**
31
+ * Get timing thresholds from config or use defaults
32
+ *
33
+ * @param {BaseSubsystem} subsystem - Subsystem instance
34
+ * @returns {Object} Thresholds object
35
+ */
36
+ function getThresholds(subsystem) {
37
+ const config = subsystem?.ctx?.config?.instrumentation || {};
38
+ return {
39
+ hookExecution: config.hookExecutionThreshold ?? DEFAULT_THRESHOLDS.hookExecution,
40
+ facetInit: config.facetInitThreshold ?? DEFAULT_THRESHOLDS.facetInit,
41
+ facetDispose: config.facetDisposeThreshold ?? DEFAULT_THRESHOLDS.facetDispose,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Time a hook execution and log if it exceeds threshold
47
+ *
48
+ * @param {Function} hook - Hook function to execute
49
+ * @param {Object} resolvedCtx - Resolved context
50
+ * @param {Object} api - Subsystem API
51
+ * @param {BaseSubsystem} subsystem - Subsystem instance
52
+ * @returns {*} Result of hook execution
53
+ */
54
+ export function instrumentHookExecution(hook, resolvedCtx, api, subsystem) {
55
+ if (!isInstrumentationEnabled(subsystem)) {
56
+ // No instrumentation - just execute
57
+ return hook(resolvedCtx, api, subsystem);
58
+ }
59
+
60
+ const logger = createSubsystemLogger(subsystem);
61
+ const hookKind = hook.kind || '<unknown>';
62
+ const hookSource = hook.source || '<unknown>';
63
+ const thresholds = getThresholds(subsystem);
64
+
65
+ const start = performance.now();
66
+ let result;
67
+ try {
68
+ result = hook(resolvedCtx, api, subsystem);
69
+ } finally {
70
+ const duration = performance.now() - start;
71
+
72
+ if (duration > thresholds.hookExecution) {
73
+ logger.warn(
74
+ `⚠️ Slow hook execution: '${hookKind}' took ${duration.toFixed(2)}ms ` +
75
+ `(threshold: ${thresholds.hookExecution}ms) [${hookSource}]`
76
+ );
77
+ } else {
78
+ logger.log(`✓ Hook '${hookKind}' executed in ${duration.toFixed(2)}ms [${hookSource}]`);
79
+ }
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * Time a facet initialization callback and log if it exceeds threshold
87
+ *
88
+ * @param {Facet} facet - Facet instance
89
+ * @param {Object} ctx - Context object
90
+ * @param {Object} api - Subsystem API
91
+ * @param {BaseSubsystem} subsystem - Subsystem instance
92
+ * @param {Function} initCallback - The init callback to execute
93
+ * @returns {Promise<void>}
94
+ */
95
+ export async function instrumentFacetInit(facet, ctx, api, subsystem, initCallback) {
96
+ if (!initCallback) {
97
+ return;
98
+ }
99
+
100
+ if (!isInstrumentationEnabled(subsystem)) {
101
+ // No instrumentation - call callback directly
102
+ return initCallback({ ctx, api, subsystem, facet });
103
+ }
104
+
105
+ const logger = createSubsystemLogger(subsystem);
106
+ const facetKind = facet.getKind?.() || '<unknown>';
107
+ const facetSource = facet.getSource?.() || '<unknown>';
108
+ const thresholds = getThresholds(subsystem);
109
+
110
+ const start = performance.now();
111
+ try {
112
+ await initCallback({ ctx, api, subsystem, facet });
113
+ } finally {
114
+ const duration = performance.now() - start;
115
+
116
+ if (duration > thresholds.facetInit) {
117
+ logger.warn(
118
+ `⚠️ Slow facet initialization: '${facetKind}' took ${duration.toFixed(2)}ms ` +
119
+ `(threshold: ${thresholds.facetInit}ms) [${facetSource}]`
120
+ );
121
+ } else {
122
+ logger.log(`✓ Facet '${facetKind}' initialized in ${duration.toFixed(2)}ms [${facetSource}]`);
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Time a facet disposal callback and warn if it exceeds threshold
129
+ *
130
+ * @param {Facet} facet - Facet instance
131
+ * @param {BaseSubsystem} subsystem - Subsystem instance
132
+ * @param {Function} disposeCallback - The dispose callback to execute
133
+ * @returns {Promise<void>}
134
+ */
135
+ export async function instrumentDisposeCallback(facet, subsystem, disposeCallback) {
136
+ if (!disposeCallback) {
137
+ return;
138
+ }
139
+
140
+ if (!isInstrumentationEnabled(subsystem)) {
141
+ // No instrumentation - call callback directly
142
+ return disposeCallback(facet);
143
+ }
144
+
145
+ const logger = createSubsystemLogger(subsystem);
146
+ const facetKind = facet.getKind?.() || '<unknown>';
147
+ const facetSource = facet.getSource?.() || '<unknown>';
148
+ const thresholds = getThresholds(subsystem);
149
+
150
+ const start = performance.now();
151
+ let errorOccurred = false;
152
+ try {
153
+ await disposeCallback(facet);
154
+ const duration = performance.now() - start;
155
+
156
+ if (duration > thresholds.facetDispose) {
157
+ logger.warn(
158
+ `⚠️ Slow facet disposal: '${facetKind}' took ${duration.toFixed(2)}ms ` +
159
+ `(threshold: ${thresholds.facetDispose}ms) [${facetSource}]`
160
+ );
161
+ } else {
162
+ logger.log(`✓ Facet '${facetKind}' disposed in ${duration.toFixed(2)}ms [${facetSource}]`);
163
+ }
164
+ } catch (error) {
165
+ errorOccurred = true;
166
+ const duration = performance.now() - start;
167
+ logger.warn(
168
+ `⚠️ Facet disposal error: '${facetKind}' failed after ${duration.toFixed(2)}ms ` +
169
+ `[${facetSource}]: ${error.message}`
170
+ );
171
+ // Re-throw so disposeAll can catch and handle it
172
+ throw error;
173
+ } finally {
174
+ // Ensure timing is logged even if error occurred
175
+ if (!errorOccurred) {
176
+ // Already logged in try block
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Time the entire build phase and log summary
183
+ *
184
+ * @param {BaseSubsystem} subsystem - Subsystem instance
185
+ * @param {Function} buildFn - Build function to execute
186
+ * @returns {Promise<void>}
187
+ */
188
+ export async function instrumentBuildPhase(subsystem, buildFn) {
189
+ if (!isInstrumentationEnabled(subsystem)) {
190
+ // No instrumentation - just build
191
+ return buildFn();
192
+ }
193
+
194
+ const logger = createSubsystemLogger(subsystem);
195
+ const start = performance.now();
196
+
197
+ try {
198
+ await buildFn();
199
+ } finally {
200
+ const duration = performance.now() - start;
201
+ logger.log(`📦 Build phase completed in ${duration.toFixed(2)}ms`);
202
+ }
203
+ }
204
+
@@ -1,11 +1,12 @@
1
1
  import { StandalonePluginSystem } from '../system/standalone-plugin-system.js';
2
+ import { BaseSubsystem } from '../system/base-subsystem.js';
2
3
  import { deepMerge } from '../builder/context-resolver.js';
3
4
 
4
5
  /**
5
- * useBase - Fluent API Builder for StandalonePluginSystem
6
+ * useBase - Fluent API Builder for StandalonePluginSystem / BaseSubsystem
6
7
  *
7
8
  * Provides a convenient, chainable API for creating and configuring
8
- * StandalonePluginSystem instances.
9
+ * StandalonePluginSystem or BaseSubsystem instances via a fluent API.
9
10
  *
10
11
  * @param {string} name - Unique name for the plugin system
11
12
  * @param {Object} [options={}] - Initial configuration options
@@ -14,6 +15,9 @@ import { deepMerge } from '../builder/context-resolver.js';
14
15
  * @param {Array} [options.defaultHooks=[]] - Optional default hooks to install
15
16
  * @returns {UseBaseBuilder} Builder instance with fluent API
16
17
  *
18
+ * By default it uses StandalonePluginSystem; call .setBase(BaseSubsystem)
19
+ * to build a subsystem instead.
20
+ *
17
21
  * @example
18
22
  * ```javascript
19
23
  * import { useBase } from 'mycelia-kernel-plugin';
@@ -35,24 +39,73 @@ export function useBase(name, options = {}) {
35
39
  throw new Error('useBase: name must be a non-empty string');
36
40
  }
37
41
 
38
- // Create the system instance
39
- const system = new StandalonePluginSystem(name, options);
40
-
41
- // Create builder with fluent API
42
- return new UseBaseBuilder(system);
42
+ // Create builder with fluent API (system will be created lazily)
43
+ return new UseBaseBuilder(name, options);
43
44
  }
44
45
 
45
46
  /**
46
- * UseBaseBuilder - Fluent API builder for StandalonePluginSystem
47
+ * UseBaseBuilder - Fluent API builder for StandalonePluginSystem or BaseSubsystem
47
48
  *
48
49
  * Provides chainable methods for configuring and building the system.
49
50
  */
50
51
  class UseBaseBuilder {
51
- #system;
52
+ #name;
53
+ #options;
54
+ #BaseClass = StandalonePluginSystem; // Default to StandalonePluginSystem
55
+ #system = null; // Lazy initialization
52
56
  #pendingConfig = {};
53
57
 
54
- constructor(system) {
55
- this.#system = system;
58
+ constructor(name, options) {
59
+ this.#name = name;
60
+ this.#options = options;
61
+ }
62
+
63
+ /**
64
+ * Get or create the system instance (lazy initialization)
65
+ * @private
66
+ */
67
+ #getSystem() {
68
+ if (!this.#system) {
69
+ this.#system = new this.#BaseClass(this.#name, this.#options);
70
+ }
71
+ return this.#system;
72
+ }
73
+
74
+ /**
75
+ * Set the base class for the system
76
+ *
77
+ * @param {Function} BaseClass - The base class to use. Must be BaseSubsystem or any class that extends BaseSubsystem
78
+ * @returns {UseBaseBuilder} This builder for chaining
79
+ *
80
+ * @example
81
+ * ```javascript
82
+ * import { BaseSubsystem } from 'mycelia-kernel-plugin';
83
+ *
84
+ * builder.setBase(BaseSubsystem);
85
+ * ```
86
+ *
87
+ * @example
88
+ * ```javascript
89
+ * // Must be called before any methods that use the system
90
+ * const system = await useBase('my-app')
91
+ * .setBase(BaseSubsystem)
92
+ * .use(useDatabase)
93
+ * .build();
94
+ * ```
95
+ */
96
+ setBase(BaseClass) {
97
+ if (this.#system) {
98
+ throw new Error('useBase.setBase: cannot change base class after system is created');
99
+ }
100
+ if (typeof BaseClass !== 'function') {
101
+ throw new Error('useBase.setBase: BaseClass must be a constructor function');
102
+ }
103
+ // Validate it's a subclass of BaseSubsystem
104
+ if (BaseClass !== BaseSubsystem && !(BaseClass.prototype instanceof BaseSubsystem)) {
105
+ throw new Error('useBase.setBase: BaseClass must be BaseSubsystem or a subclass of BaseSubsystem');
106
+ }
107
+ this.#BaseClass = BaseClass;
108
+ return this;
56
109
  }
57
110
 
58
111
  /**
@@ -70,7 +123,7 @@ class UseBaseBuilder {
70
123
  if (typeof hook !== 'function') {
71
124
  throw new Error('useBase.use: hook must be a function');
72
125
  }
73
- this.#system.use(hook);
126
+ this.#getSystem().use(hook);
74
127
  return this;
75
128
  }
76
129
 
@@ -93,6 +146,75 @@ class UseBaseBuilder {
93
146
  return this;
94
147
  }
95
148
 
149
+ /**
150
+ * Register multiple hooks at once
151
+ *
152
+ * @param {Array<Function>} hooks - Array of hook functions to register
153
+ * @returns {UseBaseBuilder} This builder for chaining
154
+ *
155
+ * @example
156
+ * ```javascript
157
+ * builder.useMultiple([useDatabase, useCache, useAuth]);
158
+ * ```
159
+ *
160
+ * @example
161
+ * ```javascript
162
+ * // Can be combined with other methods
163
+ * builder
164
+ * .use(useLogger)
165
+ * .useMultiple([useDatabase, useCache])
166
+ * .use(useAuth);
167
+ * ```
168
+ */
169
+ useMultiple(hooks) {
170
+ if (!Array.isArray(hooks)) {
171
+ throw new Error('useBase.useMultiple: hooks must be an array');
172
+ }
173
+
174
+ const system = this.#getSystem();
175
+ for (const hook of hooks) {
176
+ if (typeof hook !== 'function') {
177
+ throw new Error('useBase.useMultiple: all hooks must be functions');
178
+ }
179
+ system.use(hook);
180
+ }
181
+
182
+ return this;
183
+ }
184
+
185
+ /**
186
+ * Conditionally register multiple hooks
187
+ *
188
+ * @param {boolean} condition - Whether to register the hooks
189
+ * @param {Array<Function>} hooks - Array of hook functions to register if condition is true
190
+ * @returns {UseBaseBuilder} This builder for chaining
191
+ *
192
+ * @example
193
+ * ```javascript
194
+ * builder.useIfMultiple(process.env.NODE_ENV === 'development', [
195
+ * useDebugTools,
196
+ * useDevLogger
197
+ * ]);
198
+ * ```
199
+ *
200
+ * @example
201
+ * ```javascript
202
+ * const optionalHooks = [];
203
+ * if (enableCache) optionalHooks.push(useCache);
204
+ * if (enableAuth) optionalHooks.push(useAuth);
205
+ *
206
+ * builder
207
+ * .use(useDatabase)
208
+ * .useIfMultiple(optionalHooks.length > 0, optionalHooks);
209
+ * ```
210
+ */
211
+ useIfMultiple(condition, hooks) {
212
+ if (condition) {
213
+ return this.useMultiple(hooks);
214
+ }
215
+ return this;
216
+ }
217
+
96
218
  /**
97
219
  * Add or update configuration for a specific facet kind
98
220
  *
@@ -122,13 +244,18 @@ class UseBaseBuilder {
122
244
  throw new Error('useBase.config: kind must be a non-empty string');
123
245
  }
124
246
 
125
- // Initialize config object if needed
126
- if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
127
- this.#system.ctx.config = {};
247
+ // Get existing config for this kind (from pending or system if created)
248
+ let existingConfig;
249
+ if (this.#system) {
250
+ if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
251
+ this.#system.ctx.config = {};
252
+ }
253
+ existingConfig = this.#system.ctx.config[kind];
254
+ } else {
255
+ // System not created yet, check pending config
256
+ existingConfig = this.#pendingConfig[kind];
128
257
  }
129
258
 
130
- // Get existing config for this kind
131
- const existingConfig = this.#system.ctx.config[kind];
132
259
  const pendingConfig = this.#pendingConfig[kind];
133
260
 
134
261
  // Determine the base config (existing or pending)
@@ -153,6 +280,49 @@ class UseBaseBuilder {
153
280
  return this;
154
281
  }
155
282
 
283
+ /**
284
+ * Add or update configurations for multiple facet kinds at once
285
+ *
286
+ * Configurations are merged when possible (deep merge for objects).
287
+ *
288
+ * @param {Object} configs - Object where keys are facet kinds and values are configurations
289
+ * @returns {UseBaseBuilder} This builder for chaining
290
+ *
291
+ * @example
292
+ * ```javascript
293
+ * builder.configMultiple({
294
+ * database: { host: 'localhost', port: 5432 },
295
+ * cache: { ttl: 3600 },
296
+ * auth: { secret: 'abc123' }
297
+ * });
298
+ * ```
299
+ *
300
+ * @example
301
+ * ```javascript
302
+ * // Merge configurations
303
+ * builder
304
+ * .configMultiple({
305
+ * database: { host: 'localhost' },
306
+ * cache: { ttl: 3600 }
307
+ * })
308
+ * .configMultiple({
309
+ * database: { port: 5432 }, // Merges with existing
310
+ * auth: { secret: 'abc123' }
311
+ * });
312
+ * ```
313
+ */
314
+ configMultiple(configs) {
315
+ if (!configs || typeof configs !== 'object' || Array.isArray(configs)) {
316
+ throw new Error('useBase.configMultiple: configs must be an object');
317
+ }
318
+
319
+ for (const [kind, config] of Object.entries(configs)) {
320
+ this.config(kind, config);
321
+ }
322
+
323
+ return this;
324
+ }
325
+
156
326
  /**
157
327
  * Add an initialization callback
158
328
  *
@@ -170,7 +340,7 @@ class UseBaseBuilder {
170
340
  if (typeof callback !== 'function') {
171
341
  throw new Error('useBase.onInit: callback must be a function');
172
342
  }
173
- this.#system.onInit(callback);
343
+ this.#getSystem().onInit(callback);
174
344
  return this;
175
345
  }
176
346
 
@@ -191,7 +361,7 @@ class UseBaseBuilder {
191
361
  if (typeof callback !== 'function') {
192
362
  throw new Error('useBase.onDispose: callback must be a function');
193
363
  }
194
- this.#system.onDispose(callback);
364
+ this.#getSystem().onDispose(callback);
195
365
  return this;
196
366
  }
197
367
 
@@ -212,16 +382,18 @@ class UseBaseBuilder {
212
382
  * ```
213
383
  */
214
384
  async build(ctx = {}) {
385
+ const system = this.#getSystem();
386
+
215
387
  // Apply pending configurations
216
388
  if (Object.keys(this.#pendingConfig).length > 0) {
217
389
  // Merge pending config into system config
218
- if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
219
- this.#system.ctx.config = {};
390
+ if (!system.ctx.config || typeof system.ctx.config !== 'object') {
391
+ system.ctx.config = {};
220
392
  }
221
393
 
222
394
  // Deep merge pending configs
223
395
  for (const [kind, config] of Object.entries(this.#pendingConfig)) {
224
- const existing = this.#system.ctx.config[kind];
396
+ const existing = system.ctx.config[kind];
225
397
  if (
226
398
  existing &&
227
399
  typeof existing === 'object' &&
@@ -230,9 +402,9 @@ class UseBaseBuilder {
230
402
  typeof config === 'object' &&
231
403
  !Array.isArray(config)
232
404
  ) {
233
- this.#system.ctx.config[kind] = deepMerge(existing, config);
405
+ system.ctx.config[kind] = deepMerge(existing, config);
234
406
  } else {
235
- this.#system.ctx.config[kind] = config;
407
+ system.ctx.config[kind] = config;
236
408
  }
237
409
  }
238
410
 
@@ -243,20 +415,23 @@ class UseBaseBuilder {
243
415
  // Merge any additional context
244
416
  if (ctx && typeof ctx === 'object' && !Array.isArray(ctx)) {
245
417
  if (ctx.config && typeof ctx.config === 'object' && !Array.isArray(ctx.config)) {
246
- if (!this.#system.ctx.config) {
247
- this.#system.ctx.config = {};
418
+ if (!system.ctx.config) {
419
+ system.ctx.config = {};
248
420
  }
249
- this.#system.ctx.config = deepMerge(this.#system.ctx.config, ctx.config);
421
+ system.ctx.config = deepMerge(system.ctx.config, ctx.config);
250
422
  }
251
423
  // Merge other ctx properties (shallow)
252
- Object.assign(this.#system.ctx, ctx);
424
+ Object.assign(system.ctx, ctx);
253
425
  }
254
426
 
255
427
  // Build the system
256
- await this.#system.build(ctx);
428
+ await system.build(ctx);
257
429
 
258
430
  // Return the system instance
259
- return this.#system;
431
+ return system;
260
432
  }
261
433
  }
262
434
 
435
+
436
+
437
+