mycelia-kernel-plugin 1.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/LICENSE +22 -0
- package/README.md +248 -0
- package/bin/cli.js +433 -0
- package/package.json +63 -0
- package/src/builder/context-resolver.js +62 -0
- package/src/builder/dependency-graph-cache.js +105 -0
- package/src/builder/dependency-graph.js +141 -0
- package/src/builder/facet-validator.js +43 -0
- package/src/builder/hook-processor.js +271 -0
- package/src/builder/index.js +13 -0
- package/src/builder/subsystem-builder.js +104 -0
- package/src/builder/utils.js +165 -0
- package/src/contract/contracts/hierarchy.contract.js +60 -0
- package/src/contract/contracts/index.js +17 -0
- package/src/contract/contracts/listeners.contract.js +66 -0
- package/src/contract/contracts/processor.contract.js +47 -0
- package/src/contract/contracts/queue.contract.js +58 -0
- package/src/contract/contracts/router.contract.js +53 -0
- package/src/contract/contracts/scheduler.contract.js +65 -0
- package/src/contract/contracts/server.contract.js +88 -0
- package/src/contract/contracts/speak.contract.js +50 -0
- package/src/contract/contracts/storage.contract.js +107 -0
- package/src/contract/contracts/websocket.contract.js +90 -0
- package/src/contract/facet-contract-registry.js +155 -0
- package/src/contract/facet-contract.js +136 -0
- package/src/contract/index.js +63 -0
- package/src/core/create-hook.js +63 -0
- package/src/core/facet.js +189 -0
- package/src/core/index.js +3 -0
- package/src/hooks/listeners/handler-group-manager.js +88 -0
- package/src/hooks/listeners/listener-manager-policies.js +229 -0
- package/src/hooks/listeners/listener-manager.js +668 -0
- package/src/hooks/listeners/listener-registry.js +176 -0
- package/src/hooks/listeners/listener-statistics.js +106 -0
- package/src/hooks/listeners/pattern-matcher.js +283 -0
- package/src/hooks/listeners/use-listeners.js +164 -0
- package/src/hooks/queue/bounded-queue.js +341 -0
- package/src/hooks/queue/circular-buffer.js +231 -0
- package/src/hooks/queue/subsystem-queue-manager.js +198 -0
- package/src/hooks/queue/use-queue.js +96 -0
- package/src/hooks/speak/use-speak.js +79 -0
- package/src/index.js +49 -0
- package/src/manager/facet-manager-transaction.js +45 -0
- package/src/manager/facet-manager.js +570 -0
- package/src/manager/index.js +3 -0
- package/src/system/base-subsystem.js +416 -0
- package/src/system/base-subsystem.utils.js +106 -0
- package/src/system/index.js +4 -0
- package/src/system/standalone-plugin-system.js +70 -0
- package/src/utils/debug-flag.js +34 -0
- package/src/utils/find-facet.js +30 -0
- package/src/utils/logger.js +84 -0
- package/src/utils/semver.js +221 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import { FacetManagerTransaction } from './facet-manager-transaction.js';
|
|
2
|
+
import { createSubsystemLogger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
export class FacetManager {
|
|
5
|
+
#facets = new Map(); // Map<kind, Array<facet>> - stores arrays of facets per kind, sorted by orderIndex
|
|
6
|
+
#subsystem;
|
|
7
|
+
#txn;
|
|
8
|
+
|
|
9
|
+
constructor(subsystem) {
|
|
10
|
+
this.#subsystem = subsystem;
|
|
11
|
+
this.#txn = new FacetManagerTransaction(this, subsystem);
|
|
12
|
+
return new Proxy(this, {
|
|
13
|
+
get: (t, p) => {
|
|
14
|
+
if (typeof t[p] === 'function') return t[p].bind(t);
|
|
15
|
+
if (p in t) return t[p];
|
|
16
|
+
// Map access: check if property exists as a facet key
|
|
17
|
+
// Return the last facet (highest orderIndex) for backward compatibility
|
|
18
|
+
if (t.#facets.has(p)) {
|
|
19
|
+
const facets = t.#facets.get(p);
|
|
20
|
+
return Array.isArray(facets) ? facets[facets.length - 1] : facets;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
},
|
|
24
|
+
set: (t, p, v) => {
|
|
25
|
+
if (p in t) {
|
|
26
|
+
t[p] = v;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
// Map assignment: set as facet (for backward compatibility, store as array)
|
|
30
|
+
if (!t.#facets.has(p)) {
|
|
31
|
+
t.#facets.set(p, []);
|
|
32
|
+
}
|
|
33
|
+
const facets = t.#facets.get(p);
|
|
34
|
+
if (Array.isArray(facets)) {
|
|
35
|
+
facets.push(v);
|
|
36
|
+
// Sort by orderIndex (nulls go to end)
|
|
37
|
+
facets.sort((a, b) => {
|
|
38
|
+
const aIdx = a.orderIndex ?? Infinity;
|
|
39
|
+
const bIdx = b.orderIndex ?? Infinity;
|
|
40
|
+
return aIdx - bIdx;
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
t.#facets.set(p, [v]);
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
has: (t, p) => p in t || t.#facets.has(p),
|
|
48
|
+
ownKeys: (t) => [...Object.keys(t), ...t.#facets.keys()]
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Begin a transaction frame */
|
|
53
|
+
beginTransaction() {
|
|
54
|
+
this.#txn.beginTransaction();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Commit current transaction frame */
|
|
58
|
+
commit() {
|
|
59
|
+
this.#txn.commit();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Roll back current transaction frame: dispose + remove in reverse add order */
|
|
63
|
+
async rollback() {
|
|
64
|
+
await this.#txn.rollback();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Add → (optional init) → (optional attach). No overwrites. Rolls back current facet on init failure. */
|
|
68
|
+
async add(kind, facet, opts = { init: false, attach: false, ctx: undefined, api: undefined }) {
|
|
69
|
+
if (!kind || typeof kind !== 'string') throw new Error('FacetManager.add: kind must be a non-empty string');
|
|
70
|
+
if (!facet || typeof facet !== 'object') throw new Error('FacetManager.add: facet must be an object');
|
|
71
|
+
if (this.#facets.has(kind)) throw new Error(`FacetManager.add: facet '${kind}' already exists`);
|
|
72
|
+
|
|
73
|
+
// 1) Register so deps can be discovered during init()
|
|
74
|
+
// Store as array for consistency with addMany
|
|
75
|
+
this.#facets.set(kind, [facet]);
|
|
76
|
+
|
|
77
|
+
// Track for outer transaction rollback (if any)
|
|
78
|
+
this.#txn.trackAddition(kind);
|
|
79
|
+
|
|
80
|
+
// 2) Init now
|
|
81
|
+
try {
|
|
82
|
+
if (opts.init && typeof facet.init === 'function') {
|
|
83
|
+
await facet.init(opts.ctx, opts.api, this.#subsystem);
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// local rollback for this facet
|
|
87
|
+
try { facet?.dispose?.(this.#subsystem); } catch { /* best-effort disposal */ }
|
|
88
|
+
// Remove facet from array
|
|
89
|
+
const facets = this.#facets.get(kind);
|
|
90
|
+
if (Array.isArray(facets)) {
|
|
91
|
+
const index = facets.indexOf(facet);
|
|
92
|
+
if (index !== -1) {
|
|
93
|
+
facets.splice(index, 1);
|
|
94
|
+
if (facets.length === 0) {
|
|
95
|
+
this.#facets.delete(kind);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
this.#facets.delete(kind);
|
|
100
|
+
}
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 3) Attach after successful init
|
|
105
|
+
if (opts.attach && facet.shouldAttach?.()) {
|
|
106
|
+
this.attach(kind);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Group facets by dependency level for parallel initialization.
|
|
114
|
+
* Uses the topological sort order to identify facets that can be initialized in parallel.
|
|
115
|
+
* Facets at the same level (all dependencies already initialized) can be initialized in parallel.
|
|
116
|
+
*
|
|
117
|
+
* Since orderedKinds is topologically sorted, we can group facets by finding the maximum
|
|
118
|
+
* level of their dependencies. Facets with no dependencies go to level 0, facets that
|
|
119
|
+
* depend on level 0 facets go to level 1, etc.
|
|
120
|
+
*
|
|
121
|
+
* @param {string[]} orderedKinds - Topologically sorted facet kinds
|
|
122
|
+
* @param {Object} facetsByKind - Map of facet kind to Facet instance
|
|
123
|
+
* @returns {string[][]} Array of arrays, where each inner array contains facet kinds at the same dependency level
|
|
124
|
+
*/
|
|
125
|
+
#groupByDependencyLevel(orderedKinds, facetsByKind) {
|
|
126
|
+
// Build dependency map: kind -> Set of dependencies
|
|
127
|
+
const dependencyMap = new Map();
|
|
128
|
+
|
|
129
|
+
for (const kind of orderedKinds) {
|
|
130
|
+
const facet = facetsByKind[kind];
|
|
131
|
+
const deps = (typeof facet?.getDependencies === 'function' && facet.getDependencies()) || [];
|
|
132
|
+
// Only include dependencies that are in orderedKinds
|
|
133
|
+
const processedDeps = deps.filter(dep => orderedKinds.includes(dep));
|
|
134
|
+
dependencyMap.set(kind, new Set(processedDeps));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Build a map of kind -> level for quick lookup
|
|
138
|
+
const kindToLevel = new Map();
|
|
139
|
+
const levels = [];
|
|
140
|
+
|
|
141
|
+
for (const kind of orderedKinds) {
|
|
142
|
+
const deps = dependencyMap.get(kind) || new Set();
|
|
143
|
+
|
|
144
|
+
// Find the maximum level of any dependency
|
|
145
|
+
let targetLevel = 0;
|
|
146
|
+
if (deps.size > 0) {
|
|
147
|
+
for (const dep of deps) {
|
|
148
|
+
const depLevel = kindToLevel.get(dep);
|
|
149
|
+
if (depLevel !== undefined) {
|
|
150
|
+
targetLevel = Math.max(targetLevel, depLevel + 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Ensure levels array is large enough
|
|
156
|
+
while (levels.length <= targetLevel) {
|
|
157
|
+
levels.push([]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
levels[targetLevel].push(kind);
|
|
161
|
+
kindToLevel.set(kind, targetLevel);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Filter out empty levels
|
|
165
|
+
return levels.filter(level => level.length > 0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Bulk add with automatic rollback on any failure.
|
|
169
|
+
* Initializes facets in parallel when they're at the same dependency level.
|
|
170
|
+
*/
|
|
171
|
+
async addMany(orderedKinds, facetsByKind, opts = { init: true, attach: true, ctx: undefined, api: undefined }) {
|
|
172
|
+
this.beginTransaction();
|
|
173
|
+
try {
|
|
174
|
+
// Set order index for each facet based on its position in orderedKinds
|
|
175
|
+
for (let i = 0; i < orderedKinds.length; i++) {
|
|
176
|
+
const kind = orderedKinds[i];
|
|
177
|
+
const facet = facetsByKind[kind];
|
|
178
|
+
if (facet && typeof facet.setOrderIndex === 'function') {
|
|
179
|
+
facet.setOrderIndex(i);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Group facets by dependency level for parallel initialization
|
|
184
|
+
const levels = this.#groupByDependencyLevel(orderedKinds, facetsByKind);
|
|
185
|
+
|
|
186
|
+
// Process each level sequentially, but facets within a level in parallel
|
|
187
|
+
for (const level of levels) {
|
|
188
|
+
// First, register all facets at this level (so they're available for dependency lookups)
|
|
189
|
+
for (const kind of level) {
|
|
190
|
+
const facet = facetsByKind[kind];
|
|
191
|
+
if (!facet || typeof facet !== 'object') {
|
|
192
|
+
throw new Error(`FacetManager.addMany: invalid facet for kind '${kind}'`);
|
|
193
|
+
}
|
|
194
|
+
// Check if facet already exists
|
|
195
|
+
if (this.#facets.has(kind)) {
|
|
196
|
+
const existingFacets = this.#facets.get(kind);
|
|
197
|
+
const existingFacetsArray = Array.isArray(existingFacets) ? existingFacets : [existingFacets];
|
|
198
|
+
|
|
199
|
+
// Check if this is the same facet instance (added during verify phase)
|
|
200
|
+
const isSameInstance = existingFacetsArray.includes(facet);
|
|
201
|
+
|
|
202
|
+
if (isSameInstance) {
|
|
203
|
+
// Same facet instance - already registered during verify
|
|
204
|
+
// Just track it for initialization, don't add it again
|
|
205
|
+
this.#txn.trackAddition(kind);
|
|
206
|
+
continue; // Skip registration, but it will be initialized below
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Different facet instance - check if we can overwrite
|
|
210
|
+
const canOverwrite = facet.shouldOverwrite?.() === true;
|
|
211
|
+
if (!canOverwrite) {
|
|
212
|
+
throw new Error(`FacetManager.addMany: facet '${kind}' already exists and new facet does not allow overwrite`);
|
|
213
|
+
}
|
|
214
|
+
// Overwrite allowed - dispose old facets but keep them in the array for find() by orderIndex
|
|
215
|
+
// Reuse existingFacets from above
|
|
216
|
+
if (Array.isArray(existingFacets)) {
|
|
217
|
+
// Dispose all existing facets
|
|
218
|
+
for (const oldFacet of existingFacets) {
|
|
219
|
+
try {
|
|
220
|
+
oldFacet?.dispose?.(this.#subsystem);
|
|
221
|
+
} catch {
|
|
222
|
+
// Best-effort disposal
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
// Legacy: single facet (shouldn't happen, but handle it)
|
|
227
|
+
try {
|
|
228
|
+
existingFacets?.dispose?.(this.#subsystem);
|
|
229
|
+
} catch {
|
|
230
|
+
// Best-effort disposal
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Remove from subsystem property if attached
|
|
234
|
+
if (kind in this.#subsystem) {
|
|
235
|
+
try {
|
|
236
|
+
delete this.#subsystem[kind];
|
|
237
|
+
} catch {
|
|
238
|
+
// Best-effort cleanup
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Register facet (but don't init yet)
|
|
244
|
+
// Store as array, sorted by orderIndex
|
|
245
|
+
if (!this.#facets.has(kind)) {
|
|
246
|
+
this.#facets.set(kind, []);
|
|
247
|
+
}
|
|
248
|
+
const facets = this.#facets.get(kind);
|
|
249
|
+
if (Array.isArray(facets)) {
|
|
250
|
+
// Only add if not already in the array (prevent duplicates)
|
|
251
|
+
if (!facets.includes(facet)) {
|
|
252
|
+
facets.push(facet);
|
|
253
|
+
// Sort by orderIndex (nulls go to end)
|
|
254
|
+
facets.sort((a, b) => {
|
|
255
|
+
const aIdx = a.orderIndex ?? Infinity;
|
|
256
|
+
const bIdx = b.orderIndex ?? Infinity;
|
|
257
|
+
return aIdx - bIdx;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// Legacy: convert to array
|
|
262
|
+
this.#facets.set(kind, [this.#facets.get(kind), facet]);
|
|
263
|
+
}
|
|
264
|
+
this.#txn.trackAddition(kind);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Then, initialize all facets at this level in parallel
|
|
268
|
+
const initPromises = level.map(async (kind) => {
|
|
269
|
+
const facet = facetsByKind[kind];
|
|
270
|
+
if (!facet) return; // Skip if facet not found
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// Initialize facet (only if not already initialized)
|
|
274
|
+
if (opts.init && typeof facet.init === 'function') {
|
|
275
|
+
await facet.init(opts.ctx, opts.api, this.#subsystem);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Attach facet after successful init
|
|
279
|
+
if (opts.attach && facet.shouldAttach?.()) {
|
|
280
|
+
// Only attach if not already attached (same instance check)
|
|
281
|
+
// Check both if property exists and if it's the same instance
|
|
282
|
+
const alreadyAttached = kind in this.#subsystem && this.#subsystem[kind] === facet;
|
|
283
|
+
if (!alreadyAttached) {
|
|
284
|
+
this.attach(kind);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
// Local rollback for this facet
|
|
289
|
+
try {
|
|
290
|
+
facet?.dispose?.(this.#subsystem);
|
|
291
|
+
} catch {
|
|
292
|
+
/* best-effort disposal */
|
|
293
|
+
}
|
|
294
|
+
// Remove facet from array
|
|
295
|
+
const facets = this.#facets.get(kind);
|
|
296
|
+
if (Array.isArray(facets)) {
|
|
297
|
+
const index = facets.indexOf(facet);
|
|
298
|
+
if (index !== -1) {
|
|
299
|
+
facets.splice(index, 1);
|
|
300
|
+
if (facets.length === 0) {
|
|
301
|
+
this.#facets.delete(kind);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
this.#facets.delete(kind);
|
|
306
|
+
}
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Wait for all facets at this level to initialize
|
|
312
|
+
await Promise.all(initPromises);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.commit();
|
|
316
|
+
} catch (err) {
|
|
317
|
+
await this.rollback();
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Attach facet directly to subsystem. Allows overwrite if facet.shouldOverwrite() returns true. */
|
|
323
|
+
attach(facetKind) {
|
|
324
|
+
if (!facetKind || typeof facetKind !== 'string') {
|
|
325
|
+
throw new Error('FacetManager.attach: facetKind must be a non-empty string');
|
|
326
|
+
}
|
|
327
|
+
const facet = this.find(facetKind);
|
|
328
|
+
if (!facet) throw new Error(`FacetManager.attach: facet '${facetKind}' not found`);
|
|
329
|
+
|
|
330
|
+
// Check if property already exists and is actually a facet (not the API object or other properties)
|
|
331
|
+
if (facetKind in this.#subsystem) {
|
|
332
|
+
const existingValue = this.#subsystem[facetKind];
|
|
333
|
+
// Skip if it's the API object (which has __facets property) - don't overwrite it!
|
|
334
|
+
if (existingValue && typeof existingValue === 'object' && '__facets' in existingValue && existingValue !== facet) {
|
|
335
|
+
// This is the API object, not a facet - skip attachment to avoid overwriting it
|
|
336
|
+
const logger = createSubsystemLogger(this.#subsystem);
|
|
337
|
+
logger.log(`Skipping attachment of facet '${facetKind}' - property name conflicts with subsystem API object`);
|
|
338
|
+
return facet;
|
|
339
|
+
} else if (existingValue === facet) {
|
|
340
|
+
// If it's the same facet instance, no need to re-attach
|
|
341
|
+
return facet;
|
|
342
|
+
} else {
|
|
343
|
+
// Different facet instance - check if we can overwrite
|
|
344
|
+
const canOverwrite = facet.shouldOverwrite?.() === true;
|
|
345
|
+
if (!canOverwrite) {
|
|
346
|
+
throw new Error(`FacetManager.attach: cannot attach '${facetKind}' – property already exists on subsystem and facet does not allow overwrite`);
|
|
347
|
+
}
|
|
348
|
+
// Overwrite allowed - replace the property
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.#subsystem[facetKind] = facet;
|
|
353
|
+
const logger = createSubsystemLogger(this.#subsystem);
|
|
354
|
+
logger.log(`Attached facet '${facetKind}' to subsystem`);
|
|
355
|
+
return facet;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
remove(kind) {
|
|
359
|
+
if (!kind || typeof kind !== 'string') return false;
|
|
360
|
+
if (!this.#facets.has(kind)) return false;
|
|
361
|
+
|
|
362
|
+
// Dispose all facets of this kind
|
|
363
|
+
const facets = this.#facets.get(kind);
|
|
364
|
+
if (Array.isArray(facets)) {
|
|
365
|
+
for (const facet of facets) {
|
|
366
|
+
try {
|
|
367
|
+
facet?.dispose?.(this.#subsystem);
|
|
368
|
+
} catch {
|
|
369
|
+
// Best-effort disposal
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
// Legacy: single facet
|
|
374
|
+
try {
|
|
375
|
+
facets?.dispose?.(this.#subsystem);
|
|
376
|
+
} catch {
|
|
377
|
+
// Best-effort disposal
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.#facets.delete(kind);
|
|
382
|
+
if (kind in this.#subsystem) {
|
|
383
|
+
try { delete this.#subsystem[kind]; } catch { /* best-effort cleanup */ }
|
|
384
|
+
}
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Find a facet by kind and optional orderIndex
|
|
390
|
+
* @param {string} kind - Facet kind to find
|
|
391
|
+
* @param {number} [orderIndex] - Optional order index. If provided, returns facet at that index. If not, returns the last facet (highest orderIndex).
|
|
392
|
+
* @returns {Object|undefined} Facet instance or undefined if not found
|
|
393
|
+
*/
|
|
394
|
+
find(kind, orderIndex = undefined) {
|
|
395
|
+
if (!kind || typeof kind !== 'string') return undefined;
|
|
396
|
+
const facets = this.#facets.get(kind);
|
|
397
|
+
if (!facets) return undefined;
|
|
398
|
+
|
|
399
|
+
// Handle legacy: single facet (not an array)
|
|
400
|
+
if (!Array.isArray(facets)) {
|
|
401
|
+
return facets;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// If orderIndex is provided, find facet with that exact orderIndex
|
|
405
|
+
if (orderIndex !== undefined) {
|
|
406
|
+
if (typeof orderIndex !== 'number' || orderIndex < 0 || !Number.isInteger(orderIndex)) {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
return facets.find(f => f.orderIndex === orderIndex);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Otherwise, return the facet with the highest orderIndex
|
|
413
|
+
// (null/undefined orderIndex values are treated as -Infinity)
|
|
414
|
+
if (facets.length === 0) return undefined;
|
|
415
|
+
let maxFacet = facets[0];
|
|
416
|
+
let maxIndex = maxFacet.orderIndex ?? -Infinity;
|
|
417
|
+
for (const facet of facets) {
|
|
418
|
+
const idx = facet.orderIndex ?? -Infinity;
|
|
419
|
+
if (idx > maxIndex) {
|
|
420
|
+
maxIndex = idx;
|
|
421
|
+
maxFacet = facet;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return maxFacet;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get a facet by its index in the array of facets of that kind
|
|
429
|
+
* @param {string} kind - Facet kind to find
|
|
430
|
+
* @param {number} index - Zero-based index in the array of facets of this kind
|
|
431
|
+
* @returns {Object|undefined} Facet instance or undefined if not found
|
|
432
|
+
*/
|
|
433
|
+
getByIndex(kind, index) {
|
|
434
|
+
if (!kind || typeof kind !== 'string') return undefined;
|
|
435
|
+
if (typeof index !== 'number' || index < 0 || !Number.isInteger(index)) {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
const facets = this.#facets.get(kind);
|
|
439
|
+
if (!facets) return undefined;
|
|
440
|
+
|
|
441
|
+
// Handle legacy: single facet (not an array)
|
|
442
|
+
if (!Array.isArray(facets)) {
|
|
443
|
+
return index === 0 ? facets : undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Return facet at the specified index
|
|
447
|
+
return facets[index];
|
|
448
|
+
}
|
|
449
|
+
has(kind) { if (!kind || typeof kind !== 'string') return false; return this.#facets.has(kind); }
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Get the count of facets of the given kind
|
|
453
|
+
* @param {string} kind - Facet kind to check
|
|
454
|
+
* @returns {number} Number of facets of this kind (0 if none, 1 if single, >1 if multiple)
|
|
455
|
+
*/
|
|
456
|
+
getCount(kind) {
|
|
457
|
+
if (!kind || typeof kind !== 'string') return 0;
|
|
458
|
+
const facets = this.#facets.get(kind);
|
|
459
|
+
if (!facets) return 0;
|
|
460
|
+
|
|
461
|
+
// Handle legacy: single facet (not an array)
|
|
462
|
+
if (!Array.isArray(facets)) {
|
|
463
|
+
return 1;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return facets.length;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Check if there are multiple facets of the given kind
|
|
471
|
+
* @param {string} kind - Facet kind to check
|
|
472
|
+
* @returns {boolean} True if there are multiple facets of this kind, false otherwise
|
|
473
|
+
*/
|
|
474
|
+
hasMultiple(kind) {
|
|
475
|
+
return this.getCount(kind) > 1;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
getAllKinds() { return [...this.#facets.keys()]; }
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get all facets of a specific kind, or all facets grouped by kind.
|
|
482
|
+
* @param {string} [kind] - Optional facet kind to retrieve. If provided, returns array of facets for that kind.
|
|
483
|
+
* @returns {Array<Object>|Object} If kind is provided, returns array of facets. Otherwise returns map of kind -> last facet (for backward compatibility).
|
|
484
|
+
*/
|
|
485
|
+
getAll(kind) {
|
|
486
|
+
// If kind is provided, return array of facets for that kind
|
|
487
|
+
if (kind !== undefined) {
|
|
488
|
+
if (!kind || typeof kind !== 'string') return [];
|
|
489
|
+
const facets = this.#facets.get(kind);
|
|
490
|
+
if (!facets) return [];
|
|
491
|
+
return Array.isArray(facets) ? [...facets] : [facets]; // Return a copy to prevent external modification
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Return map of kind -> last facet (for backward compatibility)
|
|
495
|
+
const result = {};
|
|
496
|
+
for (const [k, facets] of this.#facets.entries()) {
|
|
497
|
+
if (Array.isArray(facets)) {
|
|
498
|
+
result[k] = facets.length > 0 ? facets[facets.length - 1] : undefined;
|
|
499
|
+
} else {
|
|
500
|
+
result[k] = facets;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
size() {
|
|
506
|
+
// Count unique kinds (not total facets)
|
|
507
|
+
return this.#facets.size;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
clear() {
|
|
511
|
+
// Dispose all facets before clearing
|
|
512
|
+
for (const [, facets] of this.#facets.entries()) {
|
|
513
|
+
if (Array.isArray(facets)) {
|
|
514
|
+
for (const facet of facets) {
|
|
515
|
+
try {
|
|
516
|
+
facet?.dispose?.(this.#subsystem);
|
|
517
|
+
} catch {
|
|
518
|
+
// Best-effort disposal
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
// Legacy: single facet
|
|
523
|
+
try {
|
|
524
|
+
facets?.dispose?.(this.#subsystem);
|
|
525
|
+
} catch {
|
|
526
|
+
// Best-effort disposal
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
this.#facets.clear();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Legacy helper (kept for compatibility) */
|
|
534
|
+
async initAll(subsystem) {
|
|
535
|
+
for (const [, facet] of this.#facets) {
|
|
536
|
+
if (typeof facet.init === 'function') {
|
|
537
|
+
await facet.init(subsystem);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Dispose all facets; best-effort */
|
|
543
|
+
async disposeAll(subsystem) {
|
|
544
|
+
const errors = [];
|
|
545
|
+
for (const [kind, facets] of this.#facets) {
|
|
546
|
+
if (Array.isArray(facets)) {
|
|
547
|
+
for (const facet of facets) {
|
|
548
|
+
if (typeof facet.dispose === 'function') {
|
|
549
|
+
try { await facet.dispose(subsystem); }
|
|
550
|
+
catch (e) { errors.push({ kind, error: e }); }
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
// Legacy: single facet
|
|
555
|
+
if (typeof facets.dispose === 'function') {
|
|
556
|
+
try { await facets.dispose(subsystem); }
|
|
557
|
+
catch (e) { errors.push({ kind, error: e }); }
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (errors.length) {
|
|
562
|
+
const logger = createSubsystemLogger(subsystem);
|
|
563
|
+
logger.error('Some facets failed to dispose', errors);
|
|
564
|
+
}
|
|
565
|
+
this.clear();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
[Symbol.iterator]() { return this.#facets[Symbol.iterator](); }
|
|
569
|
+
}
|
|
570
|
+
|