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,231 @@
1
+ /**
2
+ * CircularBuffer Class
3
+ *
4
+ * A high-performance circular buffer implementation for O(1) enqueue/dequeue operations.
5
+ * Replaces array-based queue to eliminate O(n) Array.shift() overhead.
6
+ *
7
+ * Performance improvements over array-based queue:
8
+ * - Enqueue: O(1) vs O(1) (same)
9
+ * - Dequeue: O(1) vs O(n) (10-100x faster for large queues)
10
+ * - Memory: Predictable vs variable (better for GC)
11
+ * - Cache: Better locality vs scattered
12
+ *
13
+ * @example
14
+ * // Create buffer with capacity 1000
15
+ * const buffer = new CircularBuffer(1000);
16
+ *
17
+ * @example
18
+ * // Use buffer
19
+ * buffer.enqueue({ msg: message, options: {} });
20
+ * const item = buffer.dequeue();
21
+ *
22
+ * @example
23
+ * // Check status
24
+ * console.log('Full:', buffer.isFull());
25
+ * console.log('Size:', buffer.size());
26
+ */
27
+ export class CircularBuffer {
28
+ /**
29
+ * Create a new CircularBuffer instance
30
+ *
31
+ * @param {number} capacity - Maximum number of items the buffer can hold
32
+ *
33
+ * @example
34
+ * const buffer = new CircularBuffer(1000);
35
+ */
36
+ constructor(capacity) {
37
+ if (!capacity || capacity <= 0) {
38
+ throw new Error('CircularBuffer: capacity must be positive');
39
+ }
40
+
41
+ this.capacity = capacity;
42
+ this.buffer = new Array(capacity);
43
+ this.head = 0; // Read position
44
+ this.tail = 0; // Write position
45
+ this._size = 0; // Current number of items
46
+ }
47
+
48
+ /**
49
+ * Add an item to the buffer
50
+ *
51
+ * Time complexity: O(1)
52
+ *
53
+ * @param {any} item - Item to enqueue
54
+ * @returns {boolean} True if successfully enqueued, false if buffer is full
55
+ *
56
+ * @example
57
+ * const success = buffer.enqueue({ msg: message, options: {} });
58
+ * if (!success) {
59
+ * console.log('Buffer is full');
60
+ * }
61
+ */
62
+ enqueue(item) {
63
+ if (this.isFull()) {
64
+ return false;
65
+ }
66
+
67
+ this.buffer[this.tail] = item;
68
+ this.tail = (this.tail + 1) % this.capacity;
69
+ this._size++;
70
+ return true;
71
+ }
72
+
73
+ /**
74
+ * Remove and return the oldest item from the buffer
75
+ *
76
+ * Time complexity: O(1)
77
+ *
78
+ * @returns {any|null} The oldest item, or null if buffer is empty
79
+ *
80
+ * @example
81
+ * const item = buffer.dequeue();
82
+ * if (item) {
83
+ * console.log('Got item:', item);
84
+ * }
85
+ */
86
+ dequeue() {
87
+ if (this.isEmpty()) {
88
+ return null;
89
+ }
90
+
91
+ const item = this.buffer[this.head];
92
+ this.buffer[this.head] = null; // Allow garbage collection
93
+ this.head = (this.head + 1) % this.capacity;
94
+ this._size--;
95
+ return item;
96
+ }
97
+
98
+ /**
99
+ * Remove the oldest item without returning it (for drop-oldest policy)
100
+ *
101
+ * Time complexity: O(1)
102
+ *
103
+ * @returns {boolean} True if item was dropped, false if buffer was empty
104
+ *
105
+ * @example
106
+ * if (buffer.isFull()) {
107
+ * buffer.dropOldest();
108
+ * buffer.enqueue(newItem);
109
+ * }
110
+ */
111
+ dropOldest() {
112
+ if (this.isEmpty()) {
113
+ return false;
114
+ }
115
+
116
+ this.buffer[this.head] = null; // Allow garbage collection
117
+ this.head = (this.head + 1) % this.capacity;
118
+ this._size--;
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * Peek at the oldest item without removing it
124
+ *
125
+ * Time complexity: O(1)
126
+ *
127
+ * @returns {any|null} The oldest item, or null if buffer is empty
128
+ *
129
+ * @example
130
+ * const next = buffer.peek();
131
+ * console.log('Next item:', next);
132
+ */
133
+ peek() {
134
+ if (this.isEmpty()) {
135
+ return null;
136
+ }
137
+ return this.buffer[this.head];
138
+ }
139
+
140
+ /**
141
+ * Check if buffer is full
142
+ *
143
+ * @returns {boolean} True if buffer is at capacity
144
+ */
145
+ isFull() {
146
+ return this._size === this.capacity;
147
+ }
148
+
149
+ /**
150
+ * Check if buffer is empty
151
+ *
152
+ * @returns {boolean} True if buffer has no items
153
+ */
154
+ isEmpty() {
155
+ return this._size === 0;
156
+ }
157
+
158
+ /**
159
+ * Get current number of items in buffer
160
+ *
161
+ * @returns {number} Number of items
162
+ */
163
+ size() {
164
+ return this._size;
165
+ }
166
+
167
+ /**
168
+ * Get buffer capacity
169
+ *
170
+ * @returns {number} Maximum capacity
171
+ */
172
+ getCapacity() {
173
+ return this.capacity;
174
+ }
175
+
176
+ /**
177
+ * Clear all items from buffer
178
+ *
179
+ * Time complexity: O(n) - must clear references for GC
180
+ *
181
+ * @example
182
+ * buffer.clear();
183
+ * console.log('Size:', buffer.size()); // 0
184
+ */
185
+ clear() {
186
+ // Clear references to allow garbage collection
187
+ for (let i = 0; i < this.capacity; i++) {
188
+ this.buffer[i] = null;
189
+ }
190
+ this.head = 0;
191
+ this.tail = 0;
192
+ this._size = 0;
193
+ }
194
+
195
+ /**
196
+ * Convert buffer to array (for debugging/inspection)
197
+ *
198
+ * Time complexity: O(n)
199
+ *
200
+ * @returns {Array} Array of items in order (oldest to newest)
201
+ *
202
+ * @example
203
+ * console.log('Items:', buffer.toArray());
204
+ */
205
+ toArray() {
206
+ const result = [];
207
+ let count = this._size;
208
+ let index = this.head;
209
+
210
+ while (count > 0) {
211
+ result.push(this.buffer[index]);
212
+ index = (index + 1) % this.capacity;
213
+ count--;
214
+ }
215
+
216
+ return result;
217
+ }
218
+
219
+ /**
220
+ * Get utilization percentage
221
+ *
222
+ * @returns {number} Percentage full (0-100)
223
+ *
224
+ * @example
225
+ * console.log('Buffer is', buffer.utilization(), '% full');
226
+ */
227
+ utilization() {
228
+ return (this._size / this.capacity) * 100;
229
+ }
230
+ }
231
+
@@ -0,0 +1,198 @@
1
+ /**
2
+ * SubsystemQueueManager Class
3
+ *
4
+ * Manages the message queue for a subsystem. Handles queue initialization,
5
+ * operations (enqueue, dequeue, clear), status queries, and statistics.
6
+ *
7
+ * This class isolates queue management concerns from BaseSubsystem,
8
+ * providing a clean interface for queue operations.
9
+ *
10
+ * @example
11
+ * // Created internally by BaseSubsystem
12
+ * const queueManager = new SubsystemQueueManager({
13
+ * capacity: 1000,
14
+ * policy: 'drop-oldest',
15
+ * debug: true,
16
+ * subsystemName: 'canvas',
17
+ * onQueueFull: () => statistics.recordQueueFull()
18
+ * });
19
+ *
20
+ * // Enqueue a message
21
+ * const success = queueManager.enqueue({ msg: message, options: {} });
22
+ *
23
+ * // Get queue status
24
+ * const status = queueManager.getStatus();
25
+ *
26
+ * // Dequeue next message
27
+ * const next = queueManager.dequeue();
28
+ */
29
+ import { BoundedQueue } from './bounded-queue.js';
30
+
31
+ export class SubsystemQueueManager {
32
+ /**
33
+ * Create a new SubsystemQueueManager instance
34
+ *
35
+ * @param {Object} options - Configuration options
36
+ * @param {number} [options.capacity=1000] - Queue capacity
37
+ * @param {string} [options.policy='drop-oldest'] - Queue overflow policy
38
+ * @param {boolean} [options.debug=false] - Enable debug logging
39
+ * @param {string} options.subsystemName - Subsystem name for logging
40
+ * @param {Function} [options.onQueueFull] - Callback when queue becomes full: () => void
41
+ *
42
+ * @example
43
+ * const queueManager = new SubsystemQueueManager({
44
+ * capacity: 2000,
45
+ * policy: 'drop-oldest',
46
+ * debug: true,
47
+ * subsystemName: 'canvas',
48
+ * onQueueFull: () => console.log('Queue is full!')
49
+ * });
50
+ */
51
+ constructor(options) {
52
+ if (!options.subsystemName || typeof options.subsystemName !== 'string') {
53
+ throw new Error('SubsystemQueueManager: subsystemName is required');
54
+ }
55
+
56
+ this.subsystemName = options.subsystemName;
57
+ this.debug = options.debug || false;
58
+
59
+ // Initialize bounded queue
60
+ this.queue = new BoundedQueue(
61
+ options.capacity || 1000,
62
+ options.policy || 'drop-oldest'
63
+ );
64
+
65
+ // Set queue debug mode
66
+ this.queue.setDebug(this.debug);
67
+
68
+ // Listen to queue full events
69
+ if (options.onQueueFull) {
70
+ this.queue.on('full', options.onQueueFull);
71
+ }
72
+
73
+ if (this.debug) {
74
+ console.log(`SubsystemQueueManager ${this.subsystemName}: Initialized with capacity ${this.queue.getCapacity()}`);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Enqueue a message-options pair
80
+ *
81
+ * @param {{msg: Message, options: Object}} pair - Message-options pair to enqueue
82
+ * @returns {boolean} True if successfully enqueued
83
+ *
84
+ * @example
85
+ * const success = queueManager.enqueue({ msg: message, options: {} });
86
+ */
87
+ enqueue(pair) {
88
+ return this.queue.enqueue(pair);
89
+ }
90
+
91
+ /**
92
+ * Dequeue the next message-options pair
93
+ *
94
+ * @returns {{msg: Message, options: Object}|null} Next pair or null if empty
95
+ *
96
+ * @example
97
+ * const next = queueManager.dequeue();
98
+ * if (next) {
99
+ * const { msg, options } = next;
100
+ * }
101
+ */
102
+ dequeue() {
103
+ return this.queue.dequeue();
104
+ }
105
+
106
+ /**
107
+ * Get current queue size
108
+ *
109
+ * @returns {number} Number of messages in queue
110
+ */
111
+ size() {
112
+ return this.queue.size();
113
+ }
114
+
115
+ /**
116
+ * Get queue capacity
117
+ *
118
+ * @returns {number} Maximum queue capacity
119
+ */
120
+ getCapacity() {
121
+ return this.queue.getCapacity();
122
+ }
123
+
124
+ /**
125
+ * Check if queue is empty
126
+ *
127
+ * @returns {boolean} True if queue is empty
128
+ */
129
+ isEmpty() {
130
+ return this.queue.isEmpty();
131
+ }
132
+
133
+ /**
134
+ * Check if queue is full
135
+ *
136
+ * @returns {boolean} True if queue is full
137
+ */
138
+ isFull() {
139
+ return this.queue.isFull();
140
+ }
141
+
142
+ /**
143
+ * Clear all messages from the queue
144
+ *
145
+ * @example
146
+ * queueManager.clear();
147
+ */
148
+ clear() {
149
+ this.queue.clear();
150
+ if (this.debug) {
151
+ console.log(`SubsystemQueueManager ${this.subsystemName}: Queue cleared`);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Get queue status information
157
+ *
158
+ * @param {Object} [additionalState={}] - Additional state to include (e.g., isProcessing, isPaused)
159
+ * @returns {Object} Queue status object
160
+ *
161
+ * @example
162
+ * const status = queueManager.getStatus({ isProcessing: true, isPaused: false });
163
+ * // Returns: { size, capacity, utilization, isEmpty, isFull, isProcessing, isPaused }
164
+ */
165
+ getStatus(additionalState = {}) {
166
+ const size = this.queue.size();
167
+ const capacity = this.queue.getCapacity();
168
+
169
+ return {
170
+ size,
171
+ capacity,
172
+ utilization: size / capacity,
173
+ isEmpty: this.queue.isEmpty(),
174
+ isFull: this.queue.isFull(),
175
+ ...additionalState
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Get queue statistics
181
+ *
182
+ * @returns {Object} Queue statistics from underlying BoundedQueue
183
+ */
184
+ getStatistics() {
185
+ return this.queue.getStatistics();
186
+ }
187
+
188
+ /**
189
+ * Get the underlying BoundedQueue instance
190
+ * (for direct access when needed)
191
+ *
192
+ * @returns {BoundedQueue} The underlying queue instance
193
+ */
194
+ getQueue() {
195
+ return this.queue;
196
+ }
197
+ }
198
+
@@ -0,0 +1,96 @@
1
+ /**
2
+ * useQueue Hook
3
+ *
4
+ * Provides queue management functionality to subsystems.
5
+ * Wraps SubsystemQueueManager and exposes queue operations.
6
+ *
7
+ * @param {Object} ctx - Context object containing config.queue for queue configuration
8
+ * @param {Object} api - Subsystem API being built
9
+ * @param {BaseSubsystem} subsystem - Subsystem instance
10
+ * @returns {Facet} Facet object with queue methods
11
+ */
12
+ import { SubsystemQueueManager } from './subsystem-queue-manager.js';
13
+ import { Facet } from '../../core/facet.js';
14
+ import { createHook } from '../../core/create-hook.js';
15
+ import { getDebugFlag } from '../../utils/debug-flag.js';
16
+ import { findFacet } from '../../utils/find-facet.js';
17
+
18
+ export const useQueue = createHook({
19
+ kind: 'queue',
20
+ version: '1.0.0',
21
+ overwrite: false,
22
+ required: [], // statistics is optional, not required
23
+ attach: true,
24
+ source: import.meta.url,
25
+ contract: 'queue',
26
+ // eslint-disable-next-line no-unused-vars
27
+ fn: (ctx, api, _subsystem) => {
28
+ const { name } = api;
29
+ const config = ctx.config?.queue || {};
30
+
31
+ // Get statistics hook if available (for onQueueFull callback)
32
+ const statisticsResult = findFacet(api.__facets, 'statistics');
33
+ const statisticsFacet = statisticsResult ? statisticsResult.facet : null;
34
+
35
+ // Create queue manager
36
+ const queueManager = new SubsystemQueueManager({
37
+ capacity: config.capacity || 1000,
38
+ policy: config.policy || 'drop-oldest',
39
+ debug: getDebugFlag(config, ctx),
40
+ subsystemName: name,
41
+ onQueueFull: () => {
42
+ if (statisticsFacet?._statistics) {
43
+ statisticsFacet._statistics.recordQueueFull();
44
+ }
45
+ }
46
+ });
47
+
48
+ // Get underlying queue for direct access
49
+ const queue = queueManager.getQueue();
50
+
51
+ return new Facet('queue', { attach: true, source: import.meta.url, contract: 'queue' })
52
+ .add({
53
+ /**
54
+ * Get queue status
55
+ * @param {Object} [additionalState={}] - Additional state (e.g., isProcessing, isPaused)
56
+ * @returns {Object} Queue status object
57
+ */
58
+ getQueueStatus(additionalState = {}) {
59
+ return queueManager.getStatus(additionalState);
60
+ },
61
+
62
+ /**
63
+ * Expose queue property for direct access
64
+ * Must have .capacity and methods like .remove()
65
+ */
66
+ queue,
67
+
68
+ /**
69
+ * Clear all messages from the queue
70
+ */
71
+ clearQueue() {
72
+ queueManager.clear();
73
+ },
74
+
75
+ /**
76
+ * Check if queue has messages to process
77
+ * @returns {boolean} True if queue has messages
78
+ */
79
+ hasMessagesToProcess() {
80
+ return !queueManager.isEmpty();
81
+ },
82
+
83
+ /**
84
+ * Select next message to process
85
+ * @returns {{msg: Message, options: Object}|null} Message-options pair or null
86
+ */
87
+ selectNextMessage() {
88
+ return queueManager.dequeue();
89
+ },
90
+
91
+ // Expose queue manager for internal use by other hooks
92
+ _queueManager: queueManager
93
+ });
94
+ }
95
+ });
96
+
@@ -0,0 +1,79 @@
1
+ /**
2
+ * useSpeak Hook
3
+ *
4
+ * Provides speaking/printing functionality to subsystems.
5
+ * A simple "hello world" example hook that implements the speak contract.
6
+ *
7
+ * @param {Object} ctx - Context object containing config.speak for speak configuration
8
+ * @param {Object} api - Subsystem API being built
9
+ * @param {BaseSubsystem} subsystem - Subsystem instance
10
+ * @returns {Facet} Facet object with speak methods
11
+ */
12
+ import { Facet } from '../../core/facet.js';
13
+ import { createHook } from '../../core/create-hook.js';
14
+ import { getDebugFlag } from '../../utils/debug-flag.js';
15
+ import { createLogger } from '../../utils/logger.js';
16
+
17
+ export const useSpeak = createHook({
18
+ kind: 'speak',
19
+ version: '1.0.0',
20
+ overwrite: false,
21
+ required: [],
22
+ attach: true,
23
+ source: import.meta.url,
24
+ contract: 'speak',
25
+ // eslint-disable-next-line no-unused-vars
26
+ fn: (ctx, api, _subsystem) => {
27
+ const { name } = api;
28
+ const config = ctx.config?.speak || {};
29
+ const debug = getDebugFlag(config, ctx);
30
+ const logger = createLogger(debug, `useSpeak ${name}`);
31
+
32
+ // Default output function (can be overridden via config)
33
+ const outputFn = config.output || console.log;
34
+ const prefix = config.prefix || '';
35
+
36
+ return new Facet('speak', { attach: true, source: import.meta.url, contract: 'speak' })
37
+ .add({
38
+ /**
39
+ * Say a message (without newline)
40
+ * @param {string} message - Message to say
41
+ * @returns {void}
42
+ *
43
+ * @example
44
+ * subsystem.speak.say('Hello');
45
+ * subsystem.speak.say(' World');
46
+ * // Output: "Hello World"
47
+ */
48
+ say(message) {
49
+ if (typeof message !== 'string') {
50
+ throw new Error('speak.say() requires a string message');
51
+ }
52
+ outputFn(prefix + message);
53
+ if (debug) {
54
+ logger.log(`Said: ${message}`);
55
+ }
56
+ },
57
+
58
+ /**
59
+ * Say a message with a newline
60
+ * @param {string} message - Message to say
61
+ * @returns {void}
62
+ *
63
+ * @example
64
+ * subsystem.speak.sayLine('Hello, World!');
65
+ * // Output: "Hello, World!\n"
66
+ */
67
+ sayLine(message) {
68
+ if (typeof message !== 'string') {
69
+ throw new Error('speak.sayLine() requires a string message');
70
+ }
71
+ outputFn(prefix + message);
72
+ if (debug) {
73
+ logger.log(`Said line: ${message}`);
74
+ }
75
+ }
76
+ });
77
+ }
78
+ });
79
+
package/src/index.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Mycelia Plugin System
3
+ *
4
+ * Main entry point for the plugin system.
5
+ * Exports all core classes, hooks, and utilities.
6
+ */
7
+
8
+ // Core exports
9
+ export { createHook } from './core/create-hook.js';
10
+ export { Facet } from './core/facet.js';
11
+
12
+ // Manager exports
13
+ export { FacetManager } from './manager/facet-manager.js';
14
+ export { FacetManagerTransaction } from './manager/facet-manager-transaction.js';
15
+
16
+ // Builder exports
17
+ export { SubsystemBuilder } from './builder/subsystem-builder.js';
18
+ export { DependencyGraphCache } from './builder/dependency-graph-cache.js';
19
+
20
+ // System exports
21
+ export { BaseSubsystem } from './system/base-subsystem.js';
22
+ export { StandalonePluginSystem } from './system/standalone-plugin-system.js';
23
+ export { collectChildren, buildChildren, disposeChildren } from './system/base-subsystem.utils.js';
24
+
25
+ // Contract exports
26
+ export { FacetContract, createFacetContract } from './contract/facet-contract.js';
27
+ export { FacetContractRegistry, defaultContractRegistry } from './contract/index.js';
28
+
29
+ // Export all contracts
30
+ export * from './contract/contracts/index.js';
31
+
32
+ // Hook exports
33
+ export { useListeners } from './hooks/listeners/use-listeners.js';
34
+ export { useQueue } from './hooks/queue/use-queue.js';
35
+ export { useSpeak } from './hooks/speak/use-speak.js';
36
+
37
+ // Utility exports
38
+ export { createLogger, createSubsystemLogger } from './utils/logger.js';
39
+ export { getDebugFlag } from './utils/debug-flag.js';
40
+ export { findFacet } from './utils/find-facet.js';
41
+ export {
42
+ parseVersion,
43
+ isValidSemver,
44
+ compareVersions,
45
+ satisfiesRange,
46
+ getDefaultVersion,
47
+ validateVersion
48
+ } from './utils/semver.js';
49
+
@@ -0,0 +1,45 @@
1
+ export class FacetManagerTransaction {
2
+ #txnStack = []; // tracks nested transaction frames
3
+ #facetManager;
4
+ #subsystem;
5
+
6
+ constructor(facetManager, subsystem) {
7
+ this.#facetManager = facetManager;
8
+ this.#subsystem = subsystem;
9
+ }
10
+
11
+ /** Begin a transaction frame */
12
+ beginTransaction() {
13
+ this.#txnStack.push({ added: [] });
14
+ }
15
+
16
+ /** Commit current transaction frame */
17
+ commit() {
18
+ if (!this.#txnStack.length) throw new Error('FacetManagerTransaction.commit: no active transaction');
19
+ this.#txnStack.pop();
20
+ }
21
+
22
+ /** Roll back current transaction frame: dispose + remove in reverse add order */
23
+ async rollback() {
24
+ if (!this.#txnStack.length) throw new Error('FacetManagerTransaction.rollback: no active transaction');
25
+ const frame = this.#txnStack.pop();
26
+ for (let i = frame.added.length - 1; i >= 0; i--) {
27
+ const k = frame.added[i];
28
+ const facet = this.#facetManager.find(k);
29
+ try { facet?.dispose?.(this.#subsystem); } catch { /* best-effort disposal */ }
30
+ this.#facetManager.remove(k);
31
+ }
32
+ }
33
+
34
+ /** Track addition in current transaction frame */
35
+ trackAddition(kind) {
36
+ const frame = this.#txnStack[this.#txnStack.length - 1];
37
+ if (frame) frame.added.push(kind);
38
+ }
39
+
40
+ /** Check if there's an active transaction */
41
+ hasActiveTransaction() {
42
+ return this.#txnStack.length > 0;
43
+ }
44
+ }
45
+