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,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListenerManager Class
|
|
3
|
+
*
|
|
4
|
+
* Manages listener registration and notification for subsystems using pluggable policies.
|
|
5
|
+
* Provides optional listener functionality that subsystems can opt-in to use.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Create listener manager with multiple policy
|
|
9
|
+
* const listenerManager = new ListenerManager({
|
|
10
|
+
* registrationPolicy: 'multiple',
|
|
11
|
+
* debug: true
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Register listeners
|
|
16
|
+
* listenerManager.on('layers/create', (message) => {
|
|
17
|
+
* console.log('Layer created:', message.getBody());
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Notify listeners
|
|
22
|
+
* listenerManager.notifyListeners('layers/create', message);
|
|
23
|
+
*/
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_POLICIES,
|
|
26
|
+
getAvailablePolicies,
|
|
27
|
+
validatePolicyOptions
|
|
28
|
+
} from './listener-manager-policies.js';
|
|
29
|
+
import { ListenerRegistry } from './listener-registry.js';
|
|
30
|
+
import { PatternMatcher } from './pattern-matcher.js';
|
|
31
|
+
import { HandlerGroupManager } from './handler-group-manager.js';
|
|
32
|
+
import { ListenerStatistics } from './listener-statistics.js';
|
|
33
|
+
|
|
34
|
+
export class ListenerManager {
|
|
35
|
+
/**
|
|
36
|
+
* Create a new ListenerManager instance
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} [options={}] - Configuration options
|
|
39
|
+
* @param {string} [options.registrationPolicy='multiple'] - Registration policy
|
|
40
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
41
|
+
* @param {Object} [options.policyOptions={}] - Policy-specific options
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Basic listener manager
|
|
45
|
+
* const listenerManager = new ListenerManager();
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Configured listener manager
|
|
49
|
+
* const listenerManager = new ListenerManager({
|
|
50
|
+
* registrationPolicy: 'single',
|
|
51
|
+
* debug: true,
|
|
52
|
+
* policyOptions: { maxListeners: 5 }
|
|
53
|
+
* });
|
|
54
|
+
*/
|
|
55
|
+
constructor(options = {}) {
|
|
56
|
+
this.registrationPolicy = options.registrationPolicy || 'multiple';
|
|
57
|
+
this.debug = options.debug || false;
|
|
58
|
+
this.policyOptions = options.policyOptions || {};
|
|
59
|
+
|
|
60
|
+
// Policy registry - start with default policies
|
|
61
|
+
this.allowedPolicies = new Map(DEFAULT_POLICIES);
|
|
62
|
+
|
|
63
|
+
// Initialize component modules
|
|
64
|
+
this.registry = new ListenerRegistry({ debug: this.debug });
|
|
65
|
+
this.patternMatcher = new PatternMatcher({ debug: this.debug });
|
|
66
|
+
this.handlerGroupManager = new HandlerGroupManager();
|
|
67
|
+
this.statistics = new ListenerStatistics();
|
|
68
|
+
|
|
69
|
+
if (this.debug) {
|
|
70
|
+
console.log(`ListenerManager: Initialized with policy '${this.registrationPolicy}'`);
|
|
71
|
+
console.log(`ListenerManager: Available policies:`, getAvailablePolicies());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Register a handler group for a specific path
|
|
77
|
+
* Handler groups contain onSuccess, onFailure, and onTimeout callbacks.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} path - Message path to listen for
|
|
80
|
+
* @param {Object} handlers - Handler group object with onSuccess, onFailure, onTimeout
|
|
81
|
+
* @param {Object} [options={}] - Registration options
|
|
82
|
+
* @returns {boolean} Success status
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // Register a handler group
|
|
86
|
+
* listenerManager.registerHandlerGroup('save/msg_123', {
|
|
87
|
+
* onSuccess: (message) => console.log('Success:', message),
|
|
88
|
+
* onFailure: (message) => console.error('Failure:', message),
|
|
89
|
+
* onTimeout: (message) => console.warn('Timeout:', message)
|
|
90
|
+
* });
|
|
91
|
+
*/
|
|
92
|
+
registerHandlerGroup(path, handlers, options = {}) {
|
|
93
|
+
const wrappedHandler = this.handlerGroupManager.wrap(handlers);
|
|
94
|
+
return this.on(path, wrappedHandler, options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Register a listener for a specific path
|
|
99
|
+
* @param {string} path - Message path to listen for
|
|
100
|
+
* @param {Function} handler - Handler function to call when message is received
|
|
101
|
+
* @param {Object} [options={}] - Registration options (for future use)
|
|
102
|
+
* @returns {boolean} Success status
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // Register a listener for layer creation
|
|
106
|
+
* listenerManager.on('layers/create', (message) => {
|
|
107
|
+
* console.log('Layer created:', message.getBody());
|
|
108
|
+
* });
|
|
109
|
+
*/
|
|
110
|
+
on(path, handler, _options = {}) {
|
|
111
|
+
if (typeof handler !== 'function') {
|
|
112
|
+
throw new Error('Handler must be a function');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const existingListeners = this.registry.get(path);
|
|
116
|
+
const policyFunction = this.allowedPolicies.get(this.registrationPolicy);
|
|
117
|
+
|
|
118
|
+
if (!policyFunction) {
|
|
119
|
+
throw new Error(`Unknown policy: ${this.registrationPolicy}. Available: ${Array.from(this.allowedPolicies.keys()).join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Apply policy function
|
|
123
|
+
const result = policyFunction(existingListeners, path, handler, {
|
|
124
|
+
policy: this.registrationPolicy,
|
|
125
|
+
debug: this.debug,
|
|
126
|
+
...this.policyOptions
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (result.success) {
|
|
130
|
+
this.registry.register(path, handler, result.listeners);
|
|
131
|
+
this.statistics.recordRegistration();
|
|
132
|
+
return true;
|
|
133
|
+
} else {
|
|
134
|
+
throw new Error(result.error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Unregister a handler group for a specific path
|
|
140
|
+
* Finds and removes the handler group that matches the provided handlers.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} path - Message path
|
|
143
|
+
* @param {Object} handlers - Handler group object with onSuccess, onFailure, onTimeout
|
|
144
|
+
* @param {Object} [options={}] - Unregistration options
|
|
145
|
+
* @returns {boolean} Success status
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* // Remove a handler group
|
|
149
|
+
* listenerManager.unregisterHandlerGroup('save/msg_123', {
|
|
150
|
+
* onSuccess: handlerGroup.onSuccess,
|
|
151
|
+
* onFailure: handlerGroup.onFailure,
|
|
152
|
+
* onTimeout: handlerGroup.onTimeout
|
|
153
|
+
* });
|
|
154
|
+
*/
|
|
155
|
+
unregisterHandlerGroup(path, handlers, _options = {}) {
|
|
156
|
+
if (!this.registry.has(path)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const existingListeners = this.registry.get(path);
|
|
161
|
+
const wrappedHandler = this.handlerGroupManager.find(existingListeners, handlers);
|
|
162
|
+
|
|
163
|
+
if (wrappedHandler) {
|
|
164
|
+
return this.off(path, wrappedHandler);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Unregister a specific listener for a path
|
|
172
|
+
* @param {string} path - Message path
|
|
173
|
+
* @param {Function} handler - Handler function to remove
|
|
174
|
+
* @returns {boolean} Success status
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* // Remove a specific listener
|
|
178
|
+
* listenerManager.off('layers/create', myHandler);
|
|
179
|
+
*/
|
|
180
|
+
off(path, handler) {
|
|
181
|
+
const removed = this.registry.unregister(path, handler);
|
|
182
|
+
if (removed) {
|
|
183
|
+
this.statistics.recordUnregistration();
|
|
184
|
+
}
|
|
185
|
+
return removed;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Unregister all listeners for a specific path
|
|
190
|
+
* @param {string} path - Message path
|
|
191
|
+
* @returns {number} Number of listeners removed
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* // Remove all listeners for a path
|
|
195
|
+
* const removed = listenerManager.offAll('layers/create');
|
|
196
|
+
*/
|
|
197
|
+
offAll(path) {
|
|
198
|
+
const count = this.registry.unregisterAll(path);
|
|
199
|
+
if (count > 0) {
|
|
200
|
+
// Record multiple unregistrations
|
|
201
|
+
for (let i = 0; i < count; i++) {
|
|
202
|
+
this.statistics.recordUnregistration();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return count;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Clear all listeners
|
|
210
|
+
* @returns {number} Total number of listeners removed
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* // Clear all listeners
|
|
214
|
+
* const removed = listenerManager.clearListeners();
|
|
215
|
+
*/
|
|
216
|
+
clearListeners() {
|
|
217
|
+
const exactRemoved = this.registry.clear();
|
|
218
|
+
const patternRemoved = this.patternMatcher.getTotalCount();
|
|
219
|
+
this.patternMatcher.clear();
|
|
220
|
+
|
|
221
|
+
// Record unregistrations
|
|
222
|
+
for (let i = 0; i < exactRemoved; i++) {
|
|
223
|
+
this.statistics.recordUnregistration();
|
|
224
|
+
}
|
|
225
|
+
for (let i = 0; i < patternRemoved; i++) {
|
|
226
|
+
this.statistics.recordPatternUnregistration();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return exactRemoved + patternRemoved;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if there are listeners for a specific path
|
|
234
|
+
* @param {string} path - Message path
|
|
235
|
+
* @returns {boolean} True if listeners exist
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* if (listenerManager.hasListeners('layers/create')) {
|
|
239
|
+
* console.log('Has listeners for layer creation');
|
|
240
|
+
* }
|
|
241
|
+
*/
|
|
242
|
+
hasListeners(path) {
|
|
243
|
+
return this.registry.has(path);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get the number of listeners for a specific path
|
|
248
|
+
* @param {string} path - Message path
|
|
249
|
+
* @returns {number} Number of listeners
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* const count = listenerManager.getListenerCount('layers/create');
|
|
253
|
+
*/
|
|
254
|
+
getListenerCount(path) {
|
|
255
|
+
return this.registry.getCount(path);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get all listeners for a specific path
|
|
260
|
+
* @param {string} path - Message path
|
|
261
|
+
* @returns {Array<Function>} Array of handler functions
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* const handlers = listenerManager.getListeners('layers/create');
|
|
265
|
+
*/
|
|
266
|
+
getListeners(path) {
|
|
267
|
+
return this.registry.get(path);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get all listeners (for debugging)
|
|
272
|
+
* @returns {Object} Object with paths as keys and handler arrays as values
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* const allListeners = listenerManager.getAllListeners();
|
|
276
|
+
*/
|
|
277
|
+
getAllListeners() {
|
|
278
|
+
return this.registry.getAll();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Emit an event to listeners for a specific path
|
|
283
|
+
* Checks both exact path matches and pattern matches
|
|
284
|
+
* @param {string} path - Message path
|
|
285
|
+
* @param {Message} message - Message to send to listeners
|
|
286
|
+
* @returns {number} Number of listeners notified
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* // Emit event to listeners
|
|
290
|
+
* const notified = listenerManager.emit('layers/create', message);
|
|
291
|
+
*/
|
|
292
|
+
emit(path, message) {
|
|
293
|
+
return this.notifyListeners(path, message);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Notify listeners for a specific path
|
|
298
|
+
* Checks both exact path matches and pattern matches
|
|
299
|
+
* @param {string} path - Message path
|
|
300
|
+
* @param {Message} message - Message to send to listeners
|
|
301
|
+
* @returns {number} Number of listeners notified
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* // Notify listeners after processing a message
|
|
305
|
+
* const notified = listenerManager.notifyListeners('layers/create', message);
|
|
306
|
+
*/
|
|
307
|
+
notifyListeners(path, message) {
|
|
308
|
+
let totalNotified = 0;
|
|
309
|
+
|
|
310
|
+
// 1. Check exact path matches first (existing behavior)
|
|
311
|
+
if (this.registry.has(path)) {
|
|
312
|
+
const handlers = this.registry.get(path);
|
|
313
|
+
totalNotified += this._notifyHandlers(handlers, path, message);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 2. Check pattern matches
|
|
317
|
+
const patternMatches = this.patternMatcher.findMatches(path);
|
|
318
|
+
for (const { patternEntry, params } of patternMatches) {
|
|
319
|
+
totalNotified += this._notifyHandlers(patternEntry.handlers, patternEntry.pattern, message, params);
|
|
320
|
+
this.statistics.recordPatternMatch();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.statistics.recordNotifications(totalNotified);
|
|
324
|
+
|
|
325
|
+
if (this.debug && totalNotified > 0) {
|
|
326
|
+
console.log(`ListenerManager: Notified ${totalNotified} listeners for '${path}'`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return totalNotified;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Notify handlers for a path
|
|
334
|
+
* @param {Array} handlers - Array of handler functions or handler entries
|
|
335
|
+
* @param {string} path - Path for logging
|
|
336
|
+
* @param {Message} message - Message to send
|
|
337
|
+
* @param {Object} [params=null] - Extracted parameters from pattern match
|
|
338
|
+
* @returns {number} Number of handlers notified
|
|
339
|
+
* @private
|
|
340
|
+
*/
|
|
341
|
+
_notifyHandlers(handlers, path, message, params = null) {
|
|
342
|
+
let notified = 0;
|
|
343
|
+
|
|
344
|
+
for (const handlerEntry of handlers) {
|
|
345
|
+
try {
|
|
346
|
+
// Handle priority policy: extract handler from object
|
|
347
|
+
// Priority policy stores: { handler, priority, path }
|
|
348
|
+
// Other policies store: function directly
|
|
349
|
+
const handler = typeof handlerEntry === 'function'
|
|
350
|
+
? handlerEntry
|
|
351
|
+
: (handlerEntry && handlerEntry.handler ? handlerEntry.handler : handlerEntry);
|
|
352
|
+
|
|
353
|
+
if (typeof handler !== 'function') {
|
|
354
|
+
if (this.debug) {
|
|
355
|
+
console.error(`ListenerManager: Handler for '${path}' is not a function:`, handler);
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Call handler with message and params (if pattern match)
|
|
361
|
+
if (params) {
|
|
362
|
+
handler(message, params);
|
|
363
|
+
} else {
|
|
364
|
+
handler(message);
|
|
365
|
+
}
|
|
366
|
+
notified++;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
this.statistics.recordError();
|
|
369
|
+
if (this.debug) {
|
|
370
|
+
console.error(`ListenerManager: Error in listener for '${path}':`, error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return notified;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Notify all listeners (broadcast)
|
|
380
|
+
* @param {Message} message - Message to send to all listeners
|
|
381
|
+
* @returns {number} Total number of listeners notified
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* // Broadcast to all listeners
|
|
385
|
+
* const notified = listenerManager.notifyAllListeners(message);
|
|
386
|
+
*/
|
|
387
|
+
notifyAllListeners(message) {
|
|
388
|
+
let totalNotified = 0;
|
|
389
|
+
|
|
390
|
+
for (const path of this.registry.getPaths()) {
|
|
391
|
+
totalNotified += this.notifyListeners(path, message);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (this.debug && totalNotified > 0) {
|
|
395
|
+
console.log(`ListenerManager: Broadcast to ${totalNotified} listeners`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return totalNotified;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Set the listener registration policy
|
|
403
|
+
* @param {string} policy - Registration policy
|
|
404
|
+
* @param {Object} [options={}] - Policy-specific options
|
|
405
|
+
* @returns {boolean} Success status
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* // Set policy to only allow one listener per path
|
|
409
|
+
* listenerManager.setListenerPolicy('single');
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* // Set policy with options
|
|
413
|
+
* listenerManager.setListenerPolicy('limited', { maxListeners: 5 });
|
|
414
|
+
*/
|
|
415
|
+
setListenerPolicy(policy, options = {}) {
|
|
416
|
+
if (!this.allowedPolicies.has(policy)) {
|
|
417
|
+
throw new Error(`Unknown policy: ${policy}. Available: ${Array.from(this.allowedPolicies.keys()).join(', ')}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Validate policy options
|
|
421
|
+
const validation = validatePolicyOptions(policy, options);
|
|
422
|
+
if (!validation.valid) {
|
|
423
|
+
throw new Error(`Invalid options for policy '${policy}': ${validation.errors.join(', ')}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
this.registrationPolicy = policy;
|
|
427
|
+
this.policyOptions = { ...this.policyOptions, ...options };
|
|
428
|
+
|
|
429
|
+
if (this.debug) {
|
|
430
|
+
console.log(`ListenerManager: Policy set to '${policy}' with options:`, options);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get the current listener registration policy
|
|
438
|
+
* @returns {string} Current policy
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* const policy = listenerManager.getListenerPolicy();
|
|
442
|
+
*/
|
|
443
|
+
getListenerPolicy() {
|
|
444
|
+
return this.registrationPolicy;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Register a new policy
|
|
449
|
+
* @param {string} name - Policy name
|
|
450
|
+
* @param {Function} policyFunction - Policy function
|
|
451
|
+
* @example
|
|
452
|
+
* listenerManager.registerPolicy('my-custom', (existingListeners, path, handler, options) => {
|
|
453
|
+
* // Custom policy logic
|
|
454
|
+
* return { success: true, listeners: [...existingListeners, handler], error: null };
|
|
455
|
+
* });
|
|
456
|
+
*/
|
|
457
|
+
registerPolicy(name, policyFunction) {
|
|
458
|
+
if (typeof policyFunction !== 'function') {
|
|
459
|
+
throw new Error('Policy must be a function');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this.allowedPolicies.set(name, policyFunction);
|
|
463
|
+
|
|
464
|
+
if (this.debug) {
|
|
465
|
+
console.log(`ListenerManager: Registered policy '${name}'`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Unregister a policy
|
|
471
|
+
* @param {string} name - Policy name
|
|
472
|
+
* @example
|
|
473
|
+
* listenerManager.unregisterPolicy('my-custom');
|
|
474
|
+
*/
|
|
475
|
+
unregisterPolicy(name) {
|
|
476
|
+
if (DEFAULT_POLICIES.has(name)) {
|
|
477
|
+
throw new Error(`Cannot unregister default policy: ${name}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const removed = this.allowedPolicies.delete(name);
|
|
481
|
+
|
|
482
|
+
if (this.debug && removed) {
|
|
483
|
+
console.log(`ListenerManager: Unregistered policy '${name}'`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return removed;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Get available policy names
|
|
491
|
+
* @returns {Array<string>} Array of policy names
|
|
492
|
+
* @example
|
|
493
|
+
* const policies = listenerManager.getAvailablePolicies();
|
|
494
|
+
* console.log('Available policies:', policies);
|
|
495
|
+
*/
|
|
496
|
+
getAvailablePolicies() {
|
|
497
|
+
return Array.from(this.allowedPolicies.keys());
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Validate if a listener can be added for a path
|
|
502
|
+
* @param {string} path - Message path
|
|
503
|
+
* @param {Function} handler - Handler function
|
|
504
|
+
* @returns {Object} Validation result
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* const validation = listenerManager.validateListener('layers/create', handler);
|
|
508
|
+
* if (!validation.valid) {
|
|
509
|
+
* console.log('Cannot add listener:', validation.reason);
|
|
510
|
+
* }
|
|
511
|
+
*/
|
|
512
|
+
validateListener(path, handler) {
|
|
513
|
+
if (typeof handler !== 'function') {
|
|
514
|
+
return { valid: false, reason: 'Handler must be a function' };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const existingListeners = this.registry.get(path);
|
|
518
|
+
const policyFunction = this.allowedPolicies.get(this.registrationPolicy);
|
|
519
|
+
|
|
520
|
+
if (!policyFunction) {
|
|
521
|
+
return { valid: false, reason: `Unknown policy: ${this.registrationPolicy}` };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Test the policy function
|
|
525
|
+
const result = policyFunction(existingListeners, path, handler, {
|
|
526
|
+
policy: this.registrationPolicy,
|
|
527
|
+
debug: this.debug,
|
|
528
|
+
...this.policyOptions
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
return { valid: result.success, reason: result.error };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Get listener manager statistics
|
|
536
|
+
* @returns {Object} Statistics object
|
|
537
|
+
*/
|
|
538
|
+
getStatistics() {
|
|
539
|
+
const exactListeners = this.registry.getTotalCount();
|
|
540
|
+
const patternListeners = this.patternMatcher.getTotalCount();
|
|
541
|
+
|
|
542
|
+
return this.statistics.get({
|
|
543
|
+
registrationPolicy: this.registrationPolicy,
|
|
544
|
+
policyOptions: this.policyOptions,
|
|
545
|
+
availablePolicies: this.getAvailablePolicies(),
|
|
546
|
+
totalPaths: this.registry.getPathCount(),
|
|
547
|
+
totalListeners: exactListeners + patternListeners,
|
|
548
|
+
exactPaths: this.registry.getPathCount(),
|
|
549
|
+
exactListeners: exactListeners,
|
|
550
|
+
patternCount: this.patternMatcher.getPatternCount(),
|
|
551
|
+
patternListeners: patternListeners
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Check if a path contains pattern syntax (e.g., {param})
|
|
557
|
+
* @param {string} path - Path to check
|
|
558
|
+
* @returns {boolean} True if path contains pattern syntax
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* listenerManager.isPattern('command/completed/id/{id}'); // true
|
|
562
|
+
* listenerManager.isPattern('command/completed'); // false
|
|
563
|
+
*/
|
|
564
|
+
isPattern(path) {
|
|
565
|
+
return this.patternMatcher.isPattern(path);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Register a listener for a pattern path
|
|
570
|
+
* @param {string} pattern - Pattern path with {param} placeholders (e.g., 'command/completed/id/{id}')
|
|
571
|
+
* @param {Function} handler - Handler function to call when pattern matches
|
|
572
|
+
* @returns {boolean} Success status
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
575
|
+
* // Register a pattern listener
|
|
576
|
+
* listenerManager.onPattern('command/completed/id/{id}', (message, params) => {
|
|
577
|
+
* console.log('Command completed:', params.id);
|
|
578
|
+
* });
|
|
579
|
+
*/
|
|
580
|
+
onPattern(pattern, handler) {
|
|
581
|
+
if (typeof handler !== 'function') {
|
|
582
|
+
throw new Error('Handler must be a function');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const existingHandlers = this.patternMatcher.has(pattern)
|
|
586
|
+
? this.patternMatcher.findMatches(pattern)[0]?.patternEntry?.handlers || []
|
|
587
|
+
: [];
|
|
588
|
+
|
|
589
|
+
const policyFunction = this.allowedPolicies.get(this.registrationPolicy);
|
|
590
|
+
|
|
591
|
+
if (!policyFunction) {
|
|
592
|
+
throw new Error(`Unknown policy: ${this.registrationPolicy}. Available: ${Array.from(this.allowedPolicies.keys()).join(', ')}`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Apply policy function to handlers array
|
|
596
|
+
const result = policyFunction(existingHandlers, pattern, handler, {
|
|
597
|
+
policy: this.registrationPolicy,
|
|
598
|
+
debug: this.debug,
|
|
599
|
+
...this.policyOptions
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
if (result.success) {
|
|
603
|
+
this.patternMatcher.register(pattern, handler, result.listeners);
|
|
604
|
+
this.statistics.recordPatternRegistration();
|
|
605
|
+
return true;
|
|
606
|
+
} else {
|
|
607
|
+
throw new Error(result.error);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Unregister a pattern listener
|
|
613
|
+
* @param {string} pattern - Pattern path
|
|
614
|
+
* @param {Function} handler - Handler function to remove
|
|
615
|
+
* @returns {boolean} Success status
|
|
616
|
+
*
|
|
617
|
+
* @example
|
|
618
|
+
* // Remove a pattern listener
|
|
619
|
+
* listenerManager.offPattern('command/completed/id/{id}', myHandler);
|
|
620
|
+
*/
|
|
621
|
+
offPattern(pattern, handler) {
|
|
622
|
+
const removed = this.patternMatcher.unregister(pattern, handler);
|
|
623
|
+
if (removed) {
|
|
624
|
+
this.statistics.recordPatternUnregistration();
|
|
625
|
+
}
|
|
626
|
+
return removed;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Check if pattern listeners exist for a pattern
|
|
631
|
+
* @param {string} pattern - Pattern path
|
|
632
|
+
* @returns {boolean} True if pattern listeners exist
|
|
633
|
+
*/
|
|
634
|
+
hasPatternListeners(pattern) {
|
|
635
|
+
return this.patternMatcher.has(pattern);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Get pattern listener count for a specific pattern
|
|
640
|
+
* @param {string} pattern - Pattern path
|
|
641
|
+
* @returns {number} Total number of handlers for this pattern
|
|
642
|
+
*/
|
|
643
|
+
getPatternListenerCount(pattern) {
|
|
644
|
+
return this.patternMatcher.getCount(pattern);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Get all registered patterns
|
|
649
|
+
* @returns {Array<string>} Array of pattern strings
|
|
650
|
+
*/
|
|
651
|
+
getRegisteredPatterns() {
|
|
652
|
+
return this.patternMatcher.getPatterns();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Clear statistics and reset state
|
|
657
|
+
*/
|
|
658
|
+
clear() {
|
|
659
|
+
this.statistics.clear();
|
|
660
|
+
this.registry.clear();
|
|
661
|
+
this.patternMatcher.clear();
|
|
662
|
+
|
|
663
|
+
if (this.debug) {
|
|
664
|
+
console.log(`ListenerManager: Cleared all data`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|