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,63 @@
|
|
|
1
|
+
import { isValidSemver, getDefaultVersion } from '../utils/semver.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook Factory Function
|
|
5
|
+
*
|
|
6
|
+
* Creates a hook function with attached metadata describing its behavior.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options - Hook configuration
|
|
9
|
+
* @param {string} options.kind - Facet kind identifier (e.g., 'router', 'queue')
|
|
10
|
+
* @param {string} [options.version='0.0.0'] - Semantic version (e.g., '1.0.0', '2.1.3-alpha')
|
|
11
|
+
* @param {boolean} [options.overwrite=false] - Whether this hook can overwrite an existing hook of the same kind
|
|
12
|
+
* @param {Array<string>} [options.required=[]] - Array of facet kinds this hook depends on
|
|
13
|
+
* @param {boolean} [options.attach=false] - Whether the resulting facet should be attached to the subsystem
|
|
14
|
+
* @param {string} options.source - File location/URL where the hook is defined (e.g., import.meta.url)
|
|
15
|
+
* @param {Function} options.fn - Hook function: (ctx, api, subsystem) => Facet
|
|
16
|
+
* @param {string} [options.contract=null] - Optional contract name (string) for this hook
|
|
17
|
+
* @returns {Function} Hook function with attached metadata
|
|
18
|
+
*/
|
|
19
|
+
export function createHook({ kind, version, overwrite = false, required = [], attach = false, source, fn, contract = null }) {
|
|
20
|
+
if (!kind || typeof kind !== 'string') {
|
|
21
|
+
throw new Error('createHook: kind must be a non-empty string');
|
|
22
|
+
}
|
|
23
|
+
if (!source || typeof source !== 'string') {
|
|
24
|
+
throw new Error('createHook: source must be a non-empty string');
|
|
25
|
+
}
|
|
26
|
+
if (typeof fn !== 'function') {
|
|
27
|
+
throw new Error('createHook: fn must be a function');
|
|
28
|
+
}
|
|
29
|
+
if (contract !== null && (typeof contract !== 'string' || !contract.trim())) {
|
|
30
|
+
throw new Error('createHook: contract must be a non-empty string or null');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Validate version if provided, otherwise use default
|
|
34
|
+
const hookVersion = version || getDefaultVersion();
|
|
35
|
+
if (!isValidSemver(hookVersion)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`createHook: invalid semver version "${hookVersion}" for hook "${kind}". ` +
|
|
38
|
+
'Must follow format: MAJOR.MINOR.PATCH (e.g., "1.0.0", "2.1.3-alpha")'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hook = function(ctx, api, subsystem) {
|
|
43
|
+
// Pass contract name and version to hook function via context
|
|
44
|
+
const hookCtx = {
|
|
45
|
+
...ctx,
|
|
46
|
+
__contract: contract || null,
|
|
47
|
+
__version: hookVersion
|
|
48
|
+
};
|
|
49
|
+
return fn(hookCtx, api, subsystem);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Attach metadata to the hook function
|
|
53
|
+
hook.kind = kind;
|
|
54
|
+
hook.version = hookVersion;
|
|
55
|
+
hook.overwrite = overwrite;
|
|
56
|
+
hook.required = Array.isArray(required) ? [...required] : [];
|
|
57
|
+
hook.attach = attach;
|
|
58
|
+
hook.source = source;
|
|
59
|
+
hook.contract = contract || null;
|
|
60
|
+
|
|
61
|
+
return hook;
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { getDefaultVersion, isValidSemver } from '../utils/semver.js';
|
|
2
|
+
|
|
3
|
+
export class Facet {
|
|
4
|
+
#kind;
|
|
5
|
+
#version;
|
|
6
|
+
#attach;
|
|
7
|
+
#required;
|
|
8
|
+
#isInit = false;
|
|
9
|
+
#initCallback = null;
|
|
10
|
+
#disposeCallback = null;
|
|
11
|
+
#source;
|
|
12
|
+
#overwrite;
|
|
13
|
+
#contract = null;
|
|
14
|
+
orderIndex = null; // Public property: index in the orderedKinds array when added via addMany
|
|
15
|
+
|
|
16
|
+
constructor(kind, { attach = false, required = [], source, overwrite = false, contract = null, version } = {}) {
|
|
17
|
+
if (!kind || typeof kind !== 'string') {
|
|
18
|
+
throw new Error('Facet: kind must be a non-empty string');
|
|
19
|
+
}
|
|
20
|
+
if (contract !== null && (typeof contract !== 'string' || !contract.trim())) {
|
|
21
|
+
throw new Error('Facet: contract must be a non-empty string or null');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate version if provided, otherwise use default
|
|
25
|
+
const facetVersion = version || getDefaultVersion();
|
|
26
|
+
if (!isValidSemver(facetVersion)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Facet: invalid semver version "${facetVersion}" for facet "${kind}". ` +
|
|
29
|
+
'Must follow format: MAJOR.MINOR.PATCH (e.g., "1.0.0", "2.1.3-alpha")'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.#kind = kind;
|
|
34
|
+
this.#version = facetVersion;
|
|
35
|
+
this.#attach = !!attach;
|
|
36
|
+
this.#required = Array.isArray(required) ? [...new Set(required)] : [];
|
|
37
|
+
this.#source = source;
|
|
38
|
+
this.#overwrite = !!overwrite;
|
|
39
|
+
this.#contract = contract || null;
|
|
40
|
+
this.orderIndex = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
add(object) {
|
|
44
|
+
if (!object || typeof object !== 'object') {
|
|
45
|
+
throw new Error('Facet.add: object must be a non-null object');
|
|
46
|
+
}
|
|
47
|
+
if (this.#isInit) {
|
|
48
|
+
throw new Error(`Facet '${this.#kind}': cannot mutate after init()`);
|
|
49
|
+
}
|
|
50
|
+
// Use Object.getOwnPropertyDescriptors to properly copy getters, setters, and other property descriptors
|
|
51
|
+
const descriptors = Object.getOwnPropertyDescriptors(object);
|
|
52
|
+
|
|
53
|
+
// Filter out properties that already exist on this instance
|
|
54
|
+
const filteredDescriptors = {};
|
|
55
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
56
|
+
const descriptor = descriptors[key];
|
|
57
|
+
// Skip if property already exists on this instance (own property)
|
|
58
|
+
if (Object.prototype.hasOwnProperty.call(this, key)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Also check if it exists on the prototype and is non-configurable
|
|
63
|
+
const protoDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), key);
|
|
64
|
+
if (protoDescriptor && !protoDescriptor.configurable) {
|
|
65
|
+
// Skip non-configurable properties from prototype (like Function.prototype.name)
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Create a new descriptor, preserving getters/setters but making it configurable
|
|
69
|
+
const newDescriptor = {
|
|
70
|
+
configurable: true,
|
|
71
|
+
enumerable: descriptor.enumerable !== false
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Copy the appropriate property descriptor fields
|
|
75
|
+
if (descriptor.get || descriptor.set) {
|
|
76
|
+
// It's a getter/setter
|
|
77
|
+
if (descriptor.get) newDescriptor.get = descriptor.get;
|
|
78
|
+
if (descriptor.set) newDescriptor.set = descriptor.set;
|
|
79
|
+
} else {
|
|
80
|
+
// It's a value property
|
|
81
|
+
newDescriptor.value = descriptor.value;
|
|
82
|
+
newDescriptor.writable = descriptor.writable !== false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
filteredDescriptors[key] = newDescriptor;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Define properties one by one, skipping any that fail (e.g., non-configurable existing properties)
|
|
89
|
+
for (const key of Reflect.ownKeys(filteredDescriptors)) {
|
|
90
|
+
const descriptor = filteredDescriptors[key];
|
|
91
|
+
try {
|
|
92
|
+
Object.defineProperty(this, key, descriptor);
|
|
93
|
+
} catch {
|
|
94
|
+
// Skip properties that can't be defined (e.g., read-only properties like 'name')
|
|
95
|
+
// This can happen if a property exists on the prototype chain and is non-configurable
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onInit(cb) {
|
|
104
|
+
if (typeof cb !== 'function') {
|
|
105
|
+
throw new Error(`Facet '${this.#kind}': onInit callback must be a function`);
|
|
106
|
+
}
|
|
107
|
+
if (this.#isInit) {
|
|
108
|
+
throw new Error(`Facet '${this.#kind}': onInit must be set before init()`);
|
|
109
|
+
}
|
|
110
|
+
this.#initCallback = cb;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onDispose(cb) {
|
|
115
|
+
if (typeof cb !== 'function') {
|
|
116
|
+
throw new Error(`Facet '${this.#kind}': onDispose callback must be a function`);
|
|
117
|
+
}
|
|
118
|
+
this.#disposeCallback = cb;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async init(ctx, api, subsystem) {
|
|
123
|
+
if (this.#isInit) return;
|
|
124
|
+
if (this.#initCallback) {
|
|
125
|
+
await this.#initCallback({ ctx, api, subsystem, facet: this });
|
|
126
|
+
}
|
|
127
|
+
this.#isInit = true;
|
|
128
|
+
Object.freeze(this);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async dispose() {
|
|
132
|
+
if (this.#disposeCallback) {
|
|
133
|
+
await this.#disposeCallback(this);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---- Dependency management ----
|
|
138
|
+
|
|
139
|
+
/** Add a dependency before initialization. */
|
|
140
|
+
addDependency(dep) {
|
|
141
|
+
if (this.#isInit) {
|
|
142
|
+
throw new Error(`Facet '${this.#kind}': cannot modify dependencies after init()`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof dep !== 'string' || !dep.trim()) {
|
|
145
|
+
throw new Error(`Facet '${this.#kind}': dependency must be a non-empty string`);
|
|
146
|
+
}
|
|
147
|
+
if (!this.#required.includes(dep)) {
|
|
148
|
+
this.#required.push(dep);
|
|
149
|
+
}
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Remove a dependency before initialization. */
|
|
154
|
+
removeDependency(dep) {
|
|
155
|
+
if (this.#isInit) {
|
|
156
|
+
throw new Error(`Facet '${this.#kind}': cannot modify dependencies after init()`);
|
|
157
|
+
}
|
|
158
|
+
const idx = this.#required.indexOf(dep);
|
|
159
|
+
if (idx !== -1) this.#required.splice(idx, 1);
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- Order index management ----
|
|
164
|
+
|
|
165
|
+
/** Set the order index (position in orderedKinds array). Can only be set before init(). */
|
|
166
|
+
setOrderIndex(index) {
|
|
167
|
+
if (this.#isInit) {
|
|
168
|
+
throw new Error(`Facet '${this.#kind}': cannot set order index after init()`);
|
|
169
|
+
}
|
|
170
|
+
if (typeof index !== 'number' || index < 0 || !Number.isInteger(index)) {
|
|
171
|
+
throw new Error(`Facet '${this.#kind}': order index must be a non-negative integer`);
|
|
172
|
+
}
|
|
173
|
+
this.orderIndex = index;
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- Introspection ----
|
|
178
|
+
|
|
179
|
+
getKind() { return this.#kind; }
|
|
180
|
+
getVersion() { return this.#version; }
|
|
181
|
+
shouldAttach() { return this.#attach; }
|
|
182
|
+
shouldOverwrite() { return this.#overwrite; }
|
|
183
|
+
getDependencies() { return this.#required.slice(); }
|
|
184
|
+
hasDependency(dep) { return this.#required.includes(dep); }
|
|
185
|
+
hasDependencies() { return this.#required.length > 0; }
|
|
186
|
+
getSource() { return this.#source; }
|
|
187
|
+
getContract() { return this.#contract; }
|
|
188
|
+
}
|
|
189
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HandlerGroupManager Class
|
|
3
|
+
*
|
|
4
|
+
* Manages handler groups (onSuccess, onFailure, onTimeout callbacks).
|
|
5
|
+
* Wraps handler groups into functions that can be registered as listeners.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const manager = new HandlerGroupManager();
|
|
9
|
+
* const wrapped = manager.wrap({
|
|
10
|
+
* onSuccess: (msg) => console.log('Success'),
|
|
11
|
+
* onFailure: (msg) => console.error('Failure'),
|
|
12
|
+
* onTimeout: (msg) => console.warn('Timeout')
|
|
13
|
+
* });
|
|
14
|
+
*/
|
|
15
|
+
export class HandlerGroupManager {
|
|
16
|
+
/**
|
|
17
|
+
* Create a new HandlerGroupManager instance
|
|
18
|
+
*/
|
|
19
|
+
constructor() {
|
|
20
|
+
// No state needed
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wrap a handler group into a function with attached properties
|
|
25
|
+
* Handler groups contain onSuccess, onFailure, and onTimeout callbacks.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} handlers - Handler group object with onSuccess, onFailure, onTimeout
|
|
28
|
+
* @returns {Function} Wrapped handler function with attached properties
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const wrapped = manager.wrap({
|
|
32
|
+
* onSuccess: (message) => console.log('Success:', message),
|
|
33
|
+
* onFailure: (message) => console.error('Failure:', message),
|
|
34
|
+
* onTimeout: (message) => console.warn('Timeout:', message)
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
wrap(handlers) {
|
|
38
|
+
if (!handlers || typeof handlers !== 'object') {
|
|
39
|
+
throw new Error('Handler group must be an object');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Wrap handler group into a function with attached properties
|
|
43
|
+
// eslint-disable-next-line no-unused-vars
|
|
44
|
+
const wrappedHandler = function(_message, ..._args) {
|
|
45
|
+
// This function will be called by ListenerManager
|
|
46
|
+
// Subsystems can access individual callbacks via properties
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Attach handler group properties
|
|
50
|
+
wrappedHandler.onSuccess = handlers.onSuccess;
|
|
51
|
+
wrappedHandler.onFailure = handlers.onFailure;
|
|
52
|
+
wrappedHandler.onTimeout = handlers.onTimeout;
|
|
53
|
+
wrappedHandler._isHandlerGroup = true;
|
|
54
|
+
|
|
55
|
+
return wrappedHandler;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find a wrapped handler that matches the provided handler group
|
|
60
|
+
*
|
|
61
|
+
* @param {Array<Function>} listeners - Array of listener functions
|
|
62
|
+
* @param {Object} handlers - Handler group object to match
|
|
63
|
+
* @returns {Function|null} Matching wrapped handler or null
|
|
64
|
+
*/
|
|
65
|
+
find(listeners, handlers) {
|
|
66
|
+
if (!handlers || typeof handlers !== 'object') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return listeners.find(listener =>
|
|
71
|
+
listener._isHandlerGroup &&
|
|
72
|
+
listener.onSuccess === handlers.onSuccess &&
|
|
73
|
+
listener.onFailure === handlers.onFailure &&
|
|
74
|
+
listener.onTimeout === handlers.onTimeout
|
|
75
|
+
) || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a handler is a handler group
|
|
80
|
+
*
|
|
81
|
+
* @param {Function} handler - Handler function to check
|
|
82
|
+
* @returns {boolean} True if handler is a handler group
|
|
83
|
+
*/
|
|
84
|
+
isHandlerGroup(handler) {
|
|
85
|
+
return handler && handler._isHandlerGroup === true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listener Manager Policies
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that implement different listener registration policies.
|
|
5
|
+
* Each policy function takes existing listeners and returns new listener state.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Multiple listeners allowed
|
|
9
|
+
* const result = multiplePolicy([handler1], 'layers/create', handler2, options);
|
|
10
|
+
* // result.listeners = [handler1, handler2]
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Only one listener allowed
|
|
14
|
+
* const result = singlePolicy([handler1], 'layers/create', handler2, options);
|
|
15
|
+
* // result.success = false, result.error = "Only one listener allowed..."
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Multiple Policy - Allow multiple listeners per path
|
|
20
|
+
* @param {Array<Function>} existingListeners - Current handlers for this path
|
|
21
|
+
* @param {string} path - Message path
|
|
22
|
+
* @param {Function} handler - New handler to register
|
|
23
|
+
* @param {Object} options - Policy options
|
|
24
|
+
* @param {string} options.policy - Policy name
|
|
25
|
+
* @param {boolean} options.debug - Debug flag
|
|
26
|
+
* @returns {Object} Registration result
|
|
27
|
+
*/
|
|
28
|
+
export function multiplePolicy(existingListeners, path, handler, options) {
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
listeners: [...existingListeners, handler],
|
|
32
|
+
error: null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Single Policy - Only allow one listener per path
|
|
38
|
+
* @param {Array<Function>} existingListeners - Current handlers for this path
|
|
39
|
+
* @param {string} path - Message path
|
|
40
|
+
* @param {Function} handler - New handler to register
|
|
41
|
+
* @param {Object} options - Policy options
|
|
42
|
+
* @param {string} options.policy - Policy name
|
|
43
|
+
* @param {boolean} options.debug - Debug flag
|
|
44
|
+
* @returns {Object} Registration result
|
|
45
|
+
*/
|
|
46
|
+
export function singlePolicy(existingListeners, path, handler, options) {
|
|
47
|
+
if (existingListeners.length > 0) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
listeners: existingListeners,
|
|
51
|
+
error: `Only one listener allowed for path '${path}' with 'single' policy`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
listeners: [handler],
|
|
58
|
+
error: null
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Replace Policy - Replace all existing listeners with new one
|
|
64
|
+
* @param {Array<Function>} existingListeners - Current handlers for this path
|
|
65
|
+
* @param {string} path - Message path
|
|
66
|
+
* @param {Function} handler - New handler to register
|
|
67
|
+
* @param {Object} options - Policy options
|
|
68
|
+
* @param {string} options.policy - Policy name
|
|
69
|
+
* @param {boolean} options.debug - Debug flag
|
|
70
|
+
* @returns {Object} Registration result
|
|
71
|
+
*/
|
|
72
|
+
export function replacePolicy(existingListeners, path, handler, options) {
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
listeners: [handler], // Replace all existing
|
|
76
|
+
error: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Append Policy - Always add new listener to end (same as multiple)
|
|
82
|
+
* @param {Array<Function>} existingListeners - Current handlers for this path
|
|
83
|
+
* @param {string} path - Message path
|
|
84
|
+
* @param {Function} handler - New handler to register
|
|
85
|
+
* @param {Object} options - Policy options
|
|
86
|
+
* @param {string} options.policy - Policy name
|
|
87
|
+
* @param {boolean} options.debug - Debug flag
|
|
88
|
+
* @returns {Object} Registration result
|
|
89
|
+
*/
|
|
90
|
+
export function appendPolicy(existingListeners, path, handler, options) {
|
|
91
|
+
return {
|
|
92
|
+
success: true,
|
|
93
|
+
listeners: [...existingListeners, handler],
|
|
94
|
+
error: null
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Prepend Policy - Always add new listener to beginning
|
|
100
|
+
* @param {Array<Function>} existingListeners - Current handlers for this path
|
|
101
|
+
* @param {string} path - Message path
|
|
102
|
+
* @param {Function} handler - New handler to register
|
|
103
|
+
* @param {Object} options - Policy options
|
|
104
|
+
* @param {string} options.policy - Policy name
|
|
105
|
+
* @param {boolean} options.debug - Debug flag
|
|
106
|
+
* @returns {Object} Registration result
|
|
107
|
+
*/
|
|
108
|
+
export function prependPolicy(existingListeners, path, handler, options) {
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
listeners: [handler, ...existingListeners],
|
|
112
|
+
error: null
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Priority Policy - Add listener with priority, sort by priority
|
|
118
|
+
* @param {Array<Function>} existingListeners - Current handlers for this path
|
|
119
|
+
* @param {string} path - Message path
|
|
120
|
+
* @param {Function} handler - New handler to register
|
|
121
|
+
* @param {Object} options - Policy options
|
|
122
|
+
* @param {string} options.policy - Policy name
|
|
123
|
+
* @param {boolean} options.debug - Debug flag
|
|
124
|
+
* @param {number} options.priority - Handler priority (higher = first)
|
|
125
|
+
* @returns {Object} Registration result
|
|
126
|
+
*/
|
|
127
|
+
export function priorityPolicy(existingListeners, path, handler, options) {
|
|
128
|
+
const priority = options.priority || 0;
|
|
129
|
+
|
|
130
|
+
// Add priority metadata to handler
|
|
131
|
+
const prioritizedHandler = {
|
|
132
|
+
handler,
|
|
133
|
+
priority,
|
|
134
|
+
path
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Add to existing listeners
|
|
138
|
+
const newListeners = [...existingListeners, prioritizedHandler];
|
|
139
|
+
|
|
140
|
+
// Sort by priority (highest first)
|
|
141
|
+
newListeners.sort((a, b) => b.priority - a.priority);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
success: true,
|
|
145
|
+
listeners: newListeners,
|
|
146
|
+
error: null
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Limited Policy - Allow only up to maxListeners per path
|
|
152
|
+
* @param {Array<Function>} existingListeners - Current handlers for this path
|
|
153
|
+
* @param {string} path - Message path
|
|
154
|
+
* @param {Function} handler - New handler to register
|
|
155
|
+
* @param {Object} options - Policy options
|
|
156
|
+
* @param {string} options.policy - Policy name
|
|
157
|
+
* @param {boolean} options.debug - Debug flag
|
|
158
|
+
* @param {number} options.maxListeners - Maximum listeners allowed
|
|
159
|
+
* @returns {Object} Registration result
|
|
160
|
+
*/
|
|
161
|
+
export function limitedPolicy(existingListeners, path, handler, options) {
|
|
162
|
+
const maxListeners = options.maxListeners || 10;
|
|
163
|
+
|
|
164
|
+
if (existingListeners.length >= maxListeners) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
listeners: existingListeners,
|
|
168
|
+
error: `Maximum ${maxListeners} listeners allowed for path '${path}' with 'limited' policy`
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: true,
|
|
174
|
+
listeners: [...existingListeners, handler],
|
|
175
|
+
error: null
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Default policy registry
|
|
181
|
+
*/
|
|
182
|
+
export const DEFAULT_POLICIES = new Map([
|
|
183
|
+
['multiple', multiplePolicy],
|
|
184
|
+
['single', singlePolicy],
|
|
185
|
+
['replace', replacePolicy],
|
|
186
|
+
['append', appendPolicy],
|
|
187
|
+
['prepend', prependPolicy],
|
|
188
|
+
['priority', priorityPolicy],
|
|
189
|
+
['limited', limitedPolicy]
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get available policy names
|
|
194
|
+
* @returns {Array<string>} Array of policy names
|
|
195
|
+
*/
|
|
196
|
+
export function getAvailablePolicies() {
|
|
197
|
+
return Array.from(DEFAULT_POLICIES.keys());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validate policy options
|
|
202
|
+
* @param {string} policyName - Policy name
|
|
203
|
+
* @param {Object} options - Policy options
|
|
204
|
+
* @returns {Object} Validation result
|
|
205
|
+
*/
|
|
206
|
+
export function validatePolicyOptions(policyName, options) {
|
|
207
|
+
const errors = [];
|
|
208
|
+
|
|
209
|
+
switch (policyName) {
|
|
210
|
+
case 'priority':
|
|
211
|
+
if (options.priority !== undefined) {
|
|
212
|
+
if (typeof options.priority !== 'number') {
|
|
213
|
+
errors.push('priority must be a number');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case 'limited':
|
|
219
|
+
if (options.maxListeners !== undefined) {
|
|
220
|
+
if (typeof options.maxListeners !== 'number' || options.maxListeners < 1) {
|
|
221
|
+
errors.push('maxListeners must be a positive number');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { valid: errors.length === 0, errors };
|
|
228
|
+
}
|
|
229
|
+
|