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,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,3 @@
1
+ export { createHook } from './create-hook.js';
2
+ export { Facet } from './facet.js';
3
+
@@ -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
+