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,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
|
+
|