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.
Files changed (53) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +248 -0
  3. package/bin/cli.js +433 -0
  4. package/package.json +63 -0
  5. package/src/builder/context-resolver.js +62 -0
  6. package/src/builder/dependency-graph-cache.js +105 -0
  7. package/src/builder/dependency-graph.js +141 -0
  8. package/src/builder/facet-validator.js +43 -0
  9. package/src/builder/hook-processor.js +271 -0
  10. package/src/builder/index.js +13 -0
  11. package/src/builder/subsystem-builder.js +104 -0
  12. package/src/builder/utils.js +165 -0
  13. package/src/contract/contracts/hierarchy.contract.js +60 -0
  14. package/src/contract/contracts/index.js +17 -0
  15. package/src/contract/contracts/listeners.contract.js +66 -0
  16. package/src/contract/contracts/processor.contract.js +47 -0
  17. package/src/contract/contracts/queue.contract.js +58 -0
  18. package/src/contract/contracts/router.contract.js +53 -0
  19. package/src/contract/contracts/scheduler.contract.js +65 -0
  20. package/src/contract/contracts/server.contract.js +88 -0
  21. package/src/contract/contracts/speak.contract.js +50 -0
  22. package/src/contract/contracts/storage.contract.js +107 -0
  23. package/src/contract/contracts/websocket.contract.js +90 -0
  24. package/src/contract/facet-contract-registry.js +155 -0
  25. package/src/contract/facet-contract.js +136 -0
  26. package/src/contract/index.js +63 -0
  27. package/src/core/create-hook.js +63 -0
  28. package/src/core/facet.js +189 -0
  29. package/src/core/index.js +3 -0
  30. package/src/hooks/listeners/handler-group-manager.js +88 -0
  31. package/src/hooks/listeners/listener-manager-policies.js +229 -0
  32. package/src/hooks/listeners/listener-manager.js +668 -0
  33. package/src/hooks/listeners/listener-registry.js +176 -0
  34. package/src/hooks/listeners/listener-statistics.js +106 -0
  35. package/src/hooks/listeners/pattern-matcher.js +283 -0
  36. package/src/hooks/listeners/use-listeners.js +164 -0
  37. package/src/hooks/queue/bounded-queue.js +341 -0
  38. package/src/hooks/queue/circular-buffer.js +231 -0
  39. package/src/hooks/queue/subsystem-queue-manager.js +198 -0
  40. package/src/hooks/queue/use-queue.js +96 -0
  41. package/src/hooks/speak/use-speak.js +79 -0
  42. package/src/index.js +49 -0
  43. package/src/manager/facet-manager-transaction.js +45 -0
  44. package/src/manager/facet-manager.js +570 -0
  45. package/src/manager/index.js +3 -0
  46. package/src/system/base-subsystem.js +416 -0
  47. package/src/system/base-subsystem.utils.js +106 -0
  48. package/src/system/index.js +4 -0
  49. package/src/system/standalone-plugin-system.js +70 -0
  50. package/src/utils/debug-flag.js +34 -0
  51. package/src/utils/find-facet.js +30 -0
  52. package/src/utils/logger.js +84 -0
  53. 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
+
@@ -0,0 +1,3 @@
1
+ export { FacetManager } from './facet-manager.js';
2
+ export { FacetManagerTransaction } from './facet-manager-transaction.js';
3
+