cyclecad 1.3.2 → 2.0.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.
- package/DRAWING_MODULE_INTEGRATION.md +633 -0
- package/README.md +124 -296
- package/app/index.html +2 -0
- package/app/js/brep-kernel.js +853 -0
- package/app/js/kernel.js +684 -0
- package/app/js/modules/assembly-module.js +582 -0
- package/app/js/modules/brep-module.js +583 -0
- package/app/js/modules/drawing-module.js +883 -0
- package/app/js/modules/operations-module.js +660 -0
- package/app/js/modules/simulation-module.js +834 -0
- package/app/js/modules/sketch-module.js +720 -0
- package/app/js/modules/step-module.js +510 -0
- package/app/js/modules/viewport-module.js +530 -0
- package/fusion360-gap-analysis.html +636 -0
- package/package.json +1 -1
package/app/js/kernel.js
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cycleCAD Microkernel
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, modular architecture for pluggable CAD components.
|
|
5
|
+
* Manages module lifecycle, dependencies, lazy loading, hot-swapping,
|
|
6
|
+
* and inter-module communication via events and commands.
|
|
7
|
+
*
|
|
8
|
+
* @module kernel
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Module lifecycle states
|
|
14
|
+
* @enum {string}
|
|
15
|
+
*/
|
|
16
|
+
const ModuleState = {
|
|
17
|
+
REGISTERED: 'registered',
|
|
18
|
+
LOADING: 'loading',
|
|
19
|
+
ACTIVE: 'active',
|
|
20
|
+
INACTIVE: 'inactive',
|
|
21
|
+
UNLOADING: 'unloading',
|
|
22
|
+
UNLOADED: 'unloaded',
|
|
23
|
+
ERROR: 'error',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Kernel class — manages all modules and inter-module communication
|
|
28
|
+
*/
|
|
29
|
+
class Kernel {
|
|
30
|
+
constructor(config = {}) {
|
|
31
|
+
this.config = {
|
|
32
|
+
memoryBudget: 512, // MB
|
|
33
|
+
autoGC: true,
|
|
34
|
+
...config,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Registries
|
|
38
|
+
this.modules = new Map(); // id → {definition, state, instance, metadata}
|
|
39
|
+
this.commands = new Map(); // 'module.cmd' → handler
|
|
40
|
+
this.shortcuts = new Map(); // 'shortcut' → 'module.cmd'
|
|
41
|
+
|
|
42
|
+
// Event bus
|
|
43
|
+
this.eventListeners = new Map(); // event → Set of handlers
|
|
44
|
+
this.onceListeners = new Map(); // event → Map(handler → true)
|
|
45
|
+
|
|
46
|
+
// Shared state (accessible to all modules)
|
|
47
|
+
this.state = {
|
|
48
|
+
_values: new Map(),
|
|
49
|
+
_watchers: new Map(), // key → Set of handlers
|
|
50
|
+
|
|
51
|
+
set: (key, value) => {
|
|
52
|
+
const oldValue = this.state._values.get(key);
|
|
53
|
+
this.state._values.set(key, value);
|
|
54
|
+
|
|
55
|
+
// Notify watchers
|
|
56
|
+
const watchers = this.state._watchers.get(key);
|
|
57
|
+
if (watchers) {
|
|
58
|
+
watchers.forEach(handler => {
|
|
59
|
+
try {
|
|
60
|
+
handler(value, oldValue);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(`[Kernel] State watcher error for "${key}":`, err);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
get: (key) => this.state._values.get(key),
|
|
69
|
+
|
|
70
|
+
watch: (key, handler) => {
|
|
71
|
+
if (!this.state._watchers.has(key)) {
|
|
72
|
+
this.state._watchers.set(key, new Set());
|
|
73
|
+
}
|
|
74
|
+
this.state._watchers.get(key).add(handler);
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
this.state._watchers.get(key).delete(handler);
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
all: () => Object.fromEntries(this.state._values),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Memory manager
|
|
85
|
+
this.memory = {
|
|
86
|
+
_estimates: new Map(), // moduleId → MB
|
|
87
|
+
|
|
88
|
+
usage: () => {
|
|
89
|
+
let total = 0;
|
|
90
|
+
for (const [moduleId, state] of this.modules.entries()) {
|
|
91
|
+
if (state.state !== ModuleState.UNLOADED && state.state !== ModuleState.ERROR) {
|
|
92
|
+
total += this.memory._estimates.get(moduleId) || 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return total;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
pressure: () => {
|
|
99
|
+
return Math.min(1, this.memory.usage() / this.config.memoryBudget);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
budget: this.config.memoryBudget,
|
|
103
|
+
|
|
104
|
+
gc: () => {
|
|
105
|
+
if (this.config.autoGC && this.memory.pressure() > 0.8) {
|
|
106
|
+
this._evictLRU();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Module load order tracking (for LRU eviction)
|
|
112
|
+
this._lastUsed = new Map(); // moduleId → timestamp
|
|
113
|
+
|
|
114
|
+
// Module dependencies tracking
|
|
115
|
+
this._dependents = new Map(); // moduleId → Set of modules that depend on it
|
|
116
|
+
|
|
117
|
+
console.log('[Kernel] Initialized with memory budget:', this.config.memoryBudget, 'MB');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Register a module definition
|
|
122
|
+
* @param {Object} definition - Module definition object
|
|
123
|
+
* @returns {boolean} Success
|
|
124
|
+
*/
|
|
125
|
+
register(definition) {
|
|
126
|
+
if (!definition.id || !definition.name) {
|
|
127
|
+
console.error('[Kernel] Module registration requires id and name', definition);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (this.modules.has(definition.id)) {
|
|
132
|
+
console.warn(`[Kernel] Module "${definition.id}" already registered, replacing`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.modules.set(definition.id, {
|
|
136
|
+
definition,
|
|
137
|
+
state: ModuleState.REGISTERED,
|
|
138
|
+
instance: null,
|
|
139
|
+
metadata: {
|
|
140
|
+
loadedAt: null,
|
|
141
|
+
activatedAt: null,
|
|
142
|
+
lastUsedAt: null,
|
|
143
|
+
errorMessage: null,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Track dependencies for reverse lookup
|
|
148
|
+
if (definition.dependencies) {
|
|
149
|
+
definition.dependencies.forEach(dep => {
|
|
150
|
+
if (!this._dependents.has(dep)) {
|
|
151
|
+
this._dependents.set(dep, new Set());
|
|
152
|
+
}
|
|
153
|
+
this._dependents.get(dep).add(definition.id);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(`[Kernel] Module registered: "${definition.id}" (${definition.category})`);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get module info
|
|
163
|
+
* @param {string} moduleId
|
|
164
|
+
* @returns {Object|null}
|
|
165
|
+
*/
|
|
166
|
+
get(moduleId) {
|
|
167
|
+
const module = this.modules.get(moduleId);
|
|
168
|
+
if (!module) return null;
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
id: module.definition.id,
|
|
172
|
+
name: module.definition.name,
|
|
173
|
+
version: module.definition.version,
|
|
174
|
+
category: module.definition.category,
|
|
175
|
+
state: module.state,
|
|
176
|
+
dependencies: module.definition.dependencies || [],
|
|
177
|
+
replaces: module.definition.replaces || [],
|
|
178
|
+
memory: this.memory._estimates.get(moduleId) || 0,
|
|
179
|
+
...module.metadata,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* List all registered modules
|
|
185
|
+
* @returns {Array<Object>}
|
|
186
|
+
*/
|
|
187
|
+
list() {
|
|
188
|
+
return Array.from(this.modules.keys()).map(id => this.get(id));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* List modules by category
|
|
193
|
+
* @param {string} category
|
|
194
|
+
* @returns {Array<Object>}
|
|
195
|
+
*/
|
|
196
|
+
listByCategory(category) {
|
|
197
|
+
return this.list().filter(m => m.category === category);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Load a module (async)
|
|
202
|
+
* Resolves dependencies first, then calls module.load()
|
|
203
|
+
* @param {string} moduleId
|
|
204
|
+
* @returns {Promise<boolean>}
|
|
205
|
+
*/
|
|
206
|
+
async load(moduleId) {
|
|
207
|
+
const module = this.modules.get(moduleId);
|
|
208
|
+
if (!module) {
|
|
209
|
+
console.error(`[Kernel] Module "${moduleId}" not found`);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Already loaded or loading
|
|
214
|
+
if (module.state === ModuleState.ACTIVE ||
|
|
215
|
+
module.state === ModuleState.INACTIVE ||
|
|
216
|
+
module.state === ModuleState.LOADING) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Mark as loading
|
|
221
|
+
module.state = ModuleState.LOADING;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
// Resolve dependencies first
|
|
225
|
+
if (module.definition.dependencies) {
|
|
226
|
+
for (const depId of module.definition.dependencies) {
|
|
227
|
+
const depLoaded = await this.load(depId);
|
|
228
|
+
if (!depLoaded) {
|
|
229
|
+
throw new Error(`Dependency "${depId}" failed to load`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Call module's load hook
|
|
235
|
+
if (typeof module.definition.load === 'function') {
|
|
236
|
+
await module.definition.load(this);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.metadata.loadedAt = Date.now();
|
|
240
|
+
module.state = ModuleState.INACTIVE;
|
|
241
|
+
|
|
242
|
+
this.emit('module:loaded', { id: moduleId });
|
|
243
|
+
console.log(`[Kernel] Module loaded: "${moduleId}"`);
|
|
244
|
+
|
|
245
|
+
return true;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
module.state = ModuleState.ERROR;
|
|
248
|
+
module.metadata.errorMessage = err.message;
|
|
249
|
+
this.emit('module:error', { id: moduleId, error: err });
|
|
250
|
+
console.error(`[Kernel] Module load failed: "${moduleId}"`, err);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Activate a module (load first if needed, then call activate hook)
|
|
257
|
+
* @param {string} moduleId
|
|
258
|
+
* @returns {Promise<boolean>}
|
|
259
|
+
*/
|
|
260
|
+
async activate(moduleId) {
|
|
261
|
+
const module = this.modules.get(moduleId);
|
|
262
|
+
if (!module) {
|
|
263
|
+
console.error(`[Kernel] Module "${moduleId}" not found`);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Already active
|
|
268
|
+
if (module.state === ModuleState.ACTIVE) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Load first if needed
|
|
273
|
+
if (module.state === ModuleState.REGISTERED || module.state === ModuleState.ERROR) {
|
|
274
|
+
const loaded = await this.load(moduleId);
|
|
275
|
+
if (!loaded) return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
// Call activate hook
|
|
280
|
+
if (typeof module.definition.activate === 'function') {
|
|
281
|
+
await module.definition.activate(this);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Register commands from this module
|
|
285
|
+
if (module.definition.provides?.commands) {
|
|
286
|
+
const prefix = moduleId;
|
|
287
|
+
for (const [cmd, handler] of Object.entries(module.definition.provides.commands)) {
|
|
288
|
+
const fullName = `${prefix}.${cmd}`;
|
|
289
|
+
this.commands.set(fullName, handler);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Register shortcuts from this module
|
|
294
|
+
if (module.definition.provides?.ui?.shortcuts) {
|
|
295
|
+
for (const [key, cmd] of Object.entries(module.definition.provides.ui.shortcuts)) {
|
|
296
|
+
this.shortcuts.set(key, cmd);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.metadata.activatedAt = Date.now();
|
|
301
|
+
module.state = ModuleState.ACTIVE;
|
|
302
|
+
this._lastUsed.set(moduleId, Date.now());
|
|
303
|
+
|
|
304
|
+
this.emit('module:activated', { id: moduleId });
|
|
305
|
+
console.log(`[Kernel] Module activated: "${moduleId}"`);
|
|
306
|
+
|
|
307
|
+
return true;
|
|
308
|
+
} catch (err) {
|
|
309
|
+
module.state = ModuleState.ERROR;
|
|
310
|
+
module.metadata.errorMessage = err.message;
|
|
311
|
+
this.emit('module:error', { id: moduleId, error: err });
|
|
312
|
+
console.error(`[Kernel] Module activation failed: "${moduleId}"`, err);
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Deactivate a module (keep in memory, call deactivate hook)
|
|
319
|
+
* @param {string} moduleId
|
|
320
|
+
* @returns {Promise<boolean>}
|
|
321
|
+
*/
|
|
322
|
+
async deactivate(moduleId) {
|
|
323
|
+
const module = this.modules.get(moduleId);
|
|
324
|
+
if (!module) {
|
|
325
|
+
console.error(`[Kernel] Module "${moduleId}" not found`);
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (module.state !== ModuleState.ACTIVE) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
// Call deactivate hook
|
|
335
|
+
if (typeof module.definition.deactivate === 'function') {
|
|
336
|
+
await module.definition.deactivate(this);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Unregister commands
|
|
340
|
+
for (const cmd of this.commands.keys()) {
|
|
341
|
+
if (cmd.startsWith(moduleId + '.')) {
|
|
342
|
+
this.commands.delete(cmd);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Unregister shortcuts
|
|
347
|
+
for (const [key, cmd] of this.shortcuts.entries()) {
|
|
348
|
+
if (cmd.startsWith(moduleId + '.')) {
|
|
349
|
+
this.shortcuts.delete(key);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.state = ModuleState.INACTIVE;
|
|
354
|
+
this.emit('module:deactivated', { id: moduleId });
|
|
355
|
+
console.log(`[Kernel] Module deactivated: "${moduleId}"`);
|
|
356
|
+
|
|
357
|
+
return true;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error(`[Kernel] Module deactivation failed: "${moduleId}"`, err);
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Unload a module (full cleanup, free memory)
|
|
366
|
+
* @param {string} moduleId
|
|
367
|
+
* @returns {Promise<boolean>}
|
|
368
|
+
*/
|
|
369
|
+
async unload(moduleId) {
|
|
370
|
+
const module = this.modules.get(moduleId);
|
|
371
|
+
if (!module) {
|
|
372
|
+
console.error(`[Kernel] Module "${moduleId}" not found`);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (module.state === ModuleState.UNLOADED) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
// Deactivate first if active
|
|
382
|
+
if (module.state === ModuleState.ACTIVE) {
|
|
383
|
+
await this.deactivate(moduleId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.state = ModuleState.UNLOADING;
|
|
387
|
+
|
|
388
|
+
// Call unload hook
|
|
389
|
+
if (typeof module.definition.unload === 'function') {
|
|
390
|
+
await module.definition.unload(this);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.state = ModuleState.UNLOADED;
|
|
394
|
+
module.instance = null;
|
|
395
|
+
this.memory._estimates.delete(moduleId);
|
|
396
|
+
this._lastUsed.delete(moduleId);
|
|
397
|
+
|
|
398
|
+
this.emit('module:unloaded', { id: moduleId });
|
|
399
|
+
console.log(`[Kernel] Module unloaded: "${moduleId}"`);
|
|
400
|
+
|
|
401
|
+
return true;
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.error(`[Kernel] Module unload failed: "${moduleId}"`, err);
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Hot-swap one module for another
|
|
410
|
+
* @param {string} oldModuleId
|
|
411
|
+
* @param {string} newModuleId
|
|
412
|
+
* @returns {Promise<boolean>}
|
|
413
|
+
*/
|
|
414
|
+
async swap(oldModuleId, newModuleId) {
|
|
415
|
+
const oldModule = this.modules.get(oldModuleId);
|
|
416
|
+
const newModule = this.modules.get(newModuleId);
|
|
417
|
+
|
|
418
|
+
if (!oldModule || !newModule) {
|
|
419
|
+
console.error('[Kernel] Swap: modules not found', { oldModuleId, newModuleId });
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Verify that newModule claims to replace oldModule
|
|
424
|
+
if (newModule.definition.replaces && !newModule.definition.replaces.includes(oldModuleId)) {
|
|
425
|
+
console.warn(`[Kernel] Module "${newModuleId}" doesn't declare replaces: ["${oldModuleId}"]`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.log(`[Kernel] Swapping "${oldModuleId}" → "${newModuleId}"`);
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
// Emit swapping event so modules can migrate state
|
|
432
|
+
this.emit('module:swapping', { oldId: oldModuleId, newId: newModuleId });
|
|
433
|
+
|
|
434
|
+
const wasActive = oldModule.state === ModuleState.ACTIVE;
|
|
435
|
+
|
|
436
|
+
// Deactivate old
|
|
437
|
+
await this.deactivate(oldModuleId);
|
|
438
|
+
await this.unload(oldModuleId);
|
|
439
|
+
|
|
440
|
+
// Activate new
|
|
441
|
+
const loaded = await this.load(newModuleId);
|
|
442
|
+
if (!loaded) {
|
|
443
|
+
console.error('[Kernel] Failed to load replacement module');
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (wasActive) {
|
|
448
|
+
const activated = await this.activate(newModuleId);
|
|
449
|
+
if (!activated) {
|
|
450
|
+
console.error('[Kernel] Failed to activate replacement module');
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this.emit('module:swapped', { oldId: oldModuleId, newId: newModuleId });
|
|
456
|
+
console.log(`[Kernel] Swap complete`);
|
|
457
|
+
|
|
458
|
+
return true;
|
|
459
|
+
} catch (err) {
|
|
460
|
+
console.error('[Kernel] Swap failed:', err);
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Load all modules in a category
|
|
467
|
+
* @param {string} category
|
|
468
|
+
* @returns {Promise<number>} Number of successfully loaded modules
|
|
469
|
+
*/
|
|
470
|
+
async loadAll(category) {
|
|
471
|
+
const modules = this.listByCategory(category);
|
|
472
|
+
let loaded = 0;
|
|
473
|
+
|
|
474
|
+
for (const mod of modules) {
|
|
475
|
+
const success = await this.load(mod.id);
|
|
476
|
+
if (success) loaded++;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return loaded;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Execute a named command
|
|
484
|
+
* Auto-loads the command's module if not already loaded
|
|
485
|
+
* @param {string} commandName - e.g., "brep.makeBox"
|
|
486
|
+
* @param {Object} params - Command parameters
|
|
487
|
+
* @returns {Promise<any>} Command result
|
|
488
|
+
*/
|
|
489
|
+
async exec(commandName, params = {}) {
|
|
490
|
+
// Check if command exists
|
|
491
|
+
if (this.commands.has(commandName)) {
|
|
492
|
+
const handler = this.commands.get(commandName);
|
|
493
|
+
this._lastUsed.set(this._getModuleFromCommand(commandName), Date.now());
|
|
494
|
+
return await handler(params);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Try to auto-load the module and retry
|
|
498
|
+
const moduleId = this._getModuleFromCommand(commandName);
|
|
499
|
+
const loaded = await this.load(moduleId);
|
|
500
|
+
|
|
501
|
+
if (!loaded) {
|
|
502
|
+
throw new Error(`Module "${moduleId}" not found`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Activate to register commands
|
|
506
|
+
const activated = await this.activate(moduleId);
|
|
507
|
+
if (!activated) {
|
|
508
|
+
throw new Error(`Module "${moduleId}" failed to activate`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Try again
|
|
512
|
+
if (this.commands.has(commandName)) {
|
|
513
|
+
const handler = this.commands.get(commandName);
|
|
514
|
+
return await handler(params);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
throw new Error(`Command "${commandName}" not found`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Check if a command exists
|
|
522
|
+
* @param {string} commandName
|
|
523
|
+
* @returns {boolean}
|
|
524
|
+
*/
|
|
525
|
+
hasCommand(commandName) {
|
|
526
|
+
return this.commands.has(commandName);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* List all registered commands
|
|
531
|
+
* @returns {Array<string>}
|
|
532
|
+
*/
|
|
533
|
+
listCommands() {
|
|
534
|
+
return Array.from(this.commands.keys());
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Execute a keyboard shortcut
|
|
539
|
+
* @param {string} shortcut - e.g., "Ctrl+B"
|
|
540
|
+
* @returns {Promise<any>} Command result
|
|
541
|
+
*/
|
|
542
|
+
async execShortcut(shortcut) {
|
|
543
|
+
const commandName = this.shortcuts.get(shortcut);
|
|
544
|
+
if (!commandName) {
|
|
545
|
+
console.warn(`[Kernel] No command bound to shortcut "${shortcut}"`);
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return await this.exec(commandName, { fromShortcut: true });
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Subscribe to an event
|
|
554
|
+
* @param {string} event - Event name or pattern (e.g., "module:*")
|
|
555
|
+
* @param {Function} handler
|
|
556
|
+
*/
|
|
557
|
+
on(event, handler) {
|
|
558
|
+
if (!this.eventListeners.has(event)) {
|
|
559
|
+
this.eventListeners.set(event, new Set());
|
|
560
|
+
}
|
|
561
|
+
this.eventListeners.get(event).add(handler);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Unsubscribe from an event
|
|
566
|
+
* @param {string} event
|
|
567
|
+
* @param {Function} handler
|
|
568
|
+
*/
|
|
569
|
+
off(event, handler) {
|
|
570
|
+
const listeners = this.eventListeners.get(event);
|
|
571
|
+
if (listeners) {
|
|
572
|
+
listeners.delete(handler);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Subscribe to an event, fire only once
|
|
578
|
+
* @param {string} event
|
|
579
|
+
* @param {Function} handler
|
|
580
|
+
*/
|
|
581
|
+
once(event, handler) {
|
|
582
|
+
const wrapper = (...args) => {
|
|
583
|
+
handler(...args);
|
|
584
|
+
this.off(event, wrapper);
|
|
585
|
+
};
|
|
586
|
+
this.on(event, wrapper);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Emit an event
|
|
591
|
+
* @param {string} event
|
|
592
|
+
* @param {Object} data
|
|
593
|
+
*/
|
|
594
|
+
emit(event, data = {}) {
|
|
595
|
+
// Direct listeners
|
|
596
|
+
const listeners = this.eventListeners.get(event);
|
|
597
|
+
if (listeners) {
|
|
598
|
+
listeners.forEach(handler => {
|
|
599
|
+
try {
|
|
600
|
+
handler(data);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
console.error(`[Kernel] Event handler error for "${event}":`, err);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Wildcard listeners (e.g., "module:*" matches "module:loaded")
|
|
608
|
+
const eventPrefix = event.split(':')[0];
|
|
609
|
+
const wildcardEvent = eventPrefix + ':*';
|
|
610
|
+
const wildcardListeners = this.eventListeners.get(wildcardEvent);
|
|
611
|
+
if (wildcardListeners) {
|
|
612
|
+
wildcardListeners.forEach(handler => {
|
|
613
|
+
try {
|
|
614
|
+
handler(data);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
console.error(`[Kernel] Wildcard event handler error for "${wildcardEvent}":`, err);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Internal: extract module ID from command name
|
|
624
|
+
* @private
|
|
625
|
+
*/
|
|
626
|
+
_getModuleFromCommand(commandName) {
|
|
627
|
+
return commandName.split('.')[0];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Internal: evict least-recently-used inactive modules
|
|
632
|
+
* @private
|
|
633
|
+
*/
|
|
634
|
+
_evictLRU() {
|
|
635
|
+
const inactive = Array.from(this.modules.entries())
|
|
636
|
+
.filter(([id, m]) => m.state === ModuleState.INACTIVE)
|
|
637
|
+
.sort((a, b) => {
|
|
638
|
+
const timeA = this._lastUsed.get(a[0]) || 0;
|
|
639
|
+
const timeB = this._lastUsed.get(b[0]) || 0;
|
|
640
|
+
return timeA - timeB; // oldest first
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
if (inactive.length > 0) {
|
|
644
|
+
const [lruId] = inactive[0];
|
|
645
|
+
console.log(`[Kernel] Memory pressure > 0.8, evicting "${lruId}"`);
|
|
646
|
+
this.unload(lruId);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Get detailed kernel status
|
|
652
|
+
* @returns {Object}
|
|
653
|
+
*/
|
|
654
|
+
status() {
|
|
655
|
+
return {
|
|
656
|
+
modules: {
|
|
657
|
+
registered: this.modules.size,
|
|
658
|
+
active: Array.from(this.modules.values()).filter(m => m.state === ModuleState.ACTIVE).length,
|
|
659
|
+
inactive: Array.from(this.modules.values()).filter(m => m.state === ModuleState.INACTIVE).length,
|
|
660
|
+
error: Array.from(this.modules.values()).filter(m => m.state === ModuleState.ERROR).length,
|
|
661
|
+
},
|
|
662
|
+
commands: this.commands.size,
|
|
663
|
+
shortcuts: this.shortcuts.size,
|
|
664
|
+
memory: {
|
|
665
|
+
usage: this.memory.usage(),
|
|
666
|
+
budget: this.memory.budget,
|
|
667
|
+
pressure: (this.memory.pressure() * 100).toFixed(1) + '%',
|
|
668
|
+
},
|
|
669
|
+
state: {
|
|
670
|
+
keys: this.state._values.size,
|
|
671
|
+
watchers: this.state._watchers.size,
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Create singleton kernel instance
|
|
678
|
+
const kernel = new Kernel();
|
|
679
|
+
|
|
680
|
+
// Export and attach to window
|
|
681
|
+
export default kernel;
|
|
682
|
+
if (typeof window !== 'undefined') {
|
|
683
|
+
window.kernel = kernel;
|
|
684
|
+
}
|