command-stream 0.9.0 → 0.9.2
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/js/src/$.ansi.mjs +147 -0
- package/js/src/$.mjs +49 -6382
- package/js/src/$.process-runner-base.mjs +563 -0
- package/js/src/$.process-runner-execution.mjs +1497 -0
- package/js/src/$.process-runner-orchestration.mjs +250 -0
- package/js/src/$.process-runner-pipeline.mjs +1162 -0
- package/js/src/$.process-runner-stream-kill.mjs +312 -0
- package/js/src/$.process-runner-virtual.mjs +297 -0
- package/js/src/$.quote.mjs +161 -0
- package/js/src/$.result.mjs +23 -0
- package/js/src/$.shell-settings.mjs +84 -0
- package/js/src/$.shell.mjs +157 -0
- package/js/src/$.state.mjs +401 -0
- package/js/src/$.stream-emitter.mjs +111 -0
- package/js/src/$.stream-utils.mjs +390 -0
- package/js/src/$.trace.mjs +36 -0
- package/js/src/$.utils.mjs +2 -23
- package/js/src/$.virtual-commands.mjs +113 -0
- package/js/src/commands/$.which.mjs +3 -1
- package/js/src/commands/index.mjs +24 -0
- package/js/src/shell-parser.mjs +125 -83
- package/js/tests/resource-cleanup-internals.test.mjs +22 -24
- package/js/tests/sigint-cleanup.test.mjs +3 -0
- package/package.json +1 -1
- package/rust/src/ansi.rs +194 -0
- package/rust/src/events.rs +305 -0
- package/rust/src/lib.rs +71 -60
- package/rust/src/macros.rs +165 -0
- package/rust/src/pipeline.rs +411 -0
- package/rust/src/quote.rs +161 -0
- package/rust/src/state.rs +333 -0
- package/rust/src/stream.rs +369 -0
- package/rust/src/trace.rs +152 -0
- package/rust/src/utils.rs +53 -158
- package/rust/tests/events.rs +207 -0
- package/rust/tests/macros.rs +77 -0
- package/rust/tests/pipeline.rs +93 -0
- package/rust/tests/state.rs +207 -0
- package/rust/tests/stream.rs +102 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// Global state management for command-stream
|
|
2
|
+
// Handles signal handlers, process tracking, and cleanup
|
|
3
|
+
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { trace } from './$.trace.mjs';
|
|
6
|
+
import { clearShellCache } from './$.shell.mjs';
|
|
7
|
+
import { resetAnsiConfig } from './$.ansi.mjs';
|
|
8
|
+
|
|
9
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
10
|
+
|
|
11
|
+
// Save initial working directory for restoration
|
|
12
|
+
const initialWorkingDirectory = process.cwd();
|
|
13
|
+
|
|
14
|
+
// Track parent stream state for graceful shutdown
|
|
15
|
+
let parentStreamsMonitored = false;
|
|
16
|
+
|
|
17
|
+
// Set of active ProcessRunner instances
|
|
18
|
+
export const activeProcessRunners = new Set();
|
|
19
|
+
|
|
20
|
+
// Track if SIGINT handler has been installed
|
|
21
|
+
let sigintHandlerInstalled = false;
|
|
22
|
+
let sigintHandler = null; // Store reference to remove it later
|
|
23
|
+
|
|
24
|
+
// Global shell settings (use a proxy for modules that need direct property access)
|
|
25
|
+
const globalShellSettings = {
|
|
26
|
+
errexit: false, // set -e equivalent: exit on error
|
|
27
|
+
verbose: false, // set -v equivalent: print commands
|
|
28
|
+
xtrace: false, // set -x equivalent: trace execution
|
|
29
|
+
pipefail: false, // set -o pipefail equivalent: pipe failure detection
|
|
30
|
+
nounset: false, // set -u equivalent: error on undefined variables
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Export the globalShellSettings object
|
|
34
|
+
export { globalShellSettings };
|
|
35
|
+
|
|
36
|
+
// Virtual commands registry
|
|
37
|
+
export const virtualCommands = new Map();
|
|
38
|
+
let virtualCommandsEnabled = true;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the current shell settings
|
|
42
|
+
* @returns {object} Current shell settings
|
|
43
|
+
*/
|
|
44
|
+
export function getShellSettings() {
|
|
45
|
+
return globalShellSettings;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set shell settings
|
|
50
|
+
* @param {object} settings - Settings to apply
|
|
51
|
+
*/
|
|
52
|
+
export function setShellSettings(settings) {
|
|
53
|
+
Object.assign(globalShellSettings, settings);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reset shell settings to defaults
|
|
58
|
+
*/
|
|
59
|
+
export function resetShellSettings() {
|
|
60
|
+
globalShellSettings.errexit = false;
|
|
61
|
+
globalShellSettings.verbose = false;
|
|
62
|
+
globalShellSettings.xtrace = false;
|
|
63
|
+
globalShellSettings.pipefail = false;
|
|
64
|
+
globalShellSettings.nounset = false;
|
|
65
|
+
globalShellSettings.noglob = false;
|
|
66
|
+
globalShellSettings.allexport = false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if virtual commands are enabled
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function isVirtualCommandsEnabled() {
|
|
74
|
+
return virtualCommandsEnabled;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Enable virtual commands
|
|
79
|
+
*/
|
|
80
|
+
export function enableVirtualCommands() {
|
|
81
|
+
trace('VirtualCommands', () => 'Enabling virtual commands');
|
|
82
|
+
virtualCommandsEnabled = true;
|
|
83
|
+
return virtualCommandsEnabled;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Disable virtual commands
|
|
88
|
+
*/
|
|
89
|
+
export function disableVirtualCommands() {
|
|
90
|
+
trace('VirtualCommands', () => 'Disabling virtual commands');
|
|
91
|
+
virtualCommandsEnabled = false;
|
|
92
|
+
return virtualCommandsEnabled;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Find active runners (child processes and virtual commands)
|
|
97
|
+
* @returns {Array} Active runners
|
|
98
|
+
*/
|
|
99
|
+
function findActiveRunners() {
|
|
100
|
+
const activeChildren = [];
|
|
101
|
+
for (const runner of activeProcessRunners) {
|
|
102
|
+
if (!runner.finished) {
|
|
103
|
+
if (runner.child && runner.child.pid) {
|
|
104
|
+
activeChildren.push(runner);
|
|
105
|
+
} else if (!runner.child) {
|
|
106
|
+
activeChildren.push(runner);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return activeChildren;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Send SIGINT to a child process
|
|
115
|
+
* @param {object} runner - ProcessRunner instance
|
|
116
|
+
*/
|
|
117
|
+
function sendSigintToChild(runner) {
|
|
118
|
+
trace('ProcessRunner', () => `Sending SIGINT to child ${runner.child.pid}`);
|
|
119
|
+
if (isBun) {
|
|
120
|
+
runner.child.kill('SIGINT');
|
|
121
|
+
} else {
|
|
122
|
+
try {
|
|
123
|
+
process.kill(-runner.child.pid, 'SIGINT');
|
|
124
|
+
} catch (_err) {
|
|
125
|
+
process.kill(runner.child.pid, 'SIGINT');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Forward SIGINT to all active runners
|
|
132
|
+
* @param {Array} activeChildren - Active runners to signal
|
|
133
|
+
*/
|
|
134
|
+
function forwardSigintToRunners(activeChildren) {
|
|
135
|
+
for (const runner of activeChildren) {
|
|
136
|
+
try {
|
|
137
|
+
if (runner.child && runner.child.pid) {
|
|
138
|
+
sendSigintToChild(runner);
|
|
139
|
+
} else {
|
|
140
|
+
trace('ProcessRunner', () => 'Cancelling virtual command');
|
|
141
|
+
runner.kill('SIGINT');
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
trace('ProcessRunner', () => `Error forwarding SIGINT: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Handle exit after SIGINT forwarding
|
|
151
|
+
* @param {boolean} hasOtherHandlers - Whether other handlers exist
|
|
152
|
+
* @param {number} activeCount - Number of active children
|
|
153
|
+
*/
|
|
154
|
+
function handleSigintExit(hasOtherHandlers, activeCount) {
|
|
155
|
+
trace('ProcessRunner', () => `SIGINT forwarded to ${activeCount} processes`);
|
|
156
|
+
if (!hasOtherHandlers) {
|
|
157
|
+
trace('ProcessRunner', () => 'No other handlers, exiting with code 130');
|
|
158
|
+
if (process.stdout && typeof process.stdout.write === 'function') {
|
|
159
|
+
process.stdout.write('', () => process.exit(130));
|
|
160
|
+
} else {
|
|
161
|
+
process.exit(130);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if our handler is installed
|
|
168
|
+
* @returns {boolean}
|
|
169
|
+
*/
|
|
170
|
+
function isOurHandlerInstalled() {
|
|
171
|
+
const currentListeners = process.listeners('SIGINT');
|
|
172
|
+
return currentListeners.some((l) => {
|
|
173
|
+
const str = l.toString();
|
|
174
|
+
// Look for our unique marker or helper function names
|
|
175
|
+
return (
|
|
176
|
+
str.includes('findActiveRunners') ||
|
|
177
|
+
str.includes('forwardSigintToRunners') ||
|
|
178
|
+
str.includes('handleSigintExit') ||
|
|
179
|
+
// Legacy detection for backwards compatibility
|
|
180
|
+
(str.includes('activeProcessRunners') &&
|
|
181
|
+
str.includes('ProcessRunner') &&
|
|
182
|
+
str.includes('activeChildren'))
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Install SIGINT handler for graceful shutdown
|
|
189
|
+
*/
|
|
190
|
+
export function installSignalHandlers() {
|
|
191
|
+
const hasOurHandler = isOurHandlerInstalled();
|
|
192
|
+
|
|
193
|
+
if (sigintHandlerInstalled && hasOurHandler) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (sigintHandlerInstalled && !hasOurHandler) {
|
|
198
|
+
sigintHandlerInstalled = false;
|
|
199
|
+
sigintHandler = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
trace('SignalHandler', () => `Installing SIGINT handler`);
|
|
203
|
+
sigintHandlerInstalled = true;
|
|
204
|
+
|
|
205
|
+
sigintHandler = () => {
|
|
206
|
+
const hasOtherHandlers = process.listeners('SIGINT').length > 1;
|
|
207
|
+
const activeChildren = findActiveRunners();
|
|
208
|
+
|
|
209
|
+
if (activeChildren.length === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
forwardSigintToRunners(activeChildren);
|
|
214
|
+
handleSigintExit(hasOtherHandlers, activeChildren.length);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
process.on('SIGINT', sigintHandler);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Uninstall SIGINT handler
|
|
222
|
+
*/
|
|
223
|
+
export function uninstallSignalHandlers() {
|
|
224
|
+
if (!sigintHandlerInstalled || !sigintHandler) {
|
|
225
|
+
trace(
|
|
226
|
+
'SignalHandler',
|
|
227
|
+
() => 'SIGINT handler not installed or missing, skipping removal'
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
trace(
|
|
233
|
+
'SignalHandler',
|
|
234
|
+
() =>
|
|
235
|
+
`Removing SIGINT handler | ${JSON.stringify({ activeRunners: activeProcessRunners.size })}`
|
|
236
|
+
);
|
|
237
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
238
|
+
sigintHandlerInstalled = false;
|
|
239
|
+
sigintHandler = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Check if a listener is a command-stream SIGINT handler
|
|
244
|
+
* @param {Function} listener - Listener function
|
|
245
|
+
* @returns {boolean}
|
|
246
|
+
*/
|
|
247
|
+
function isCommandStreamListener(listener) {
|
|
248
|
+
const str = listener.toString();
|
|
249
|
+
return (
|
|
250
|
+
str.includes('findActiveRunners') ||
|
|
251
|
+
str.includes('forwardSigintToRunners') ||
|
|
252
|
+
str.includes('handleSigintExit') ||
|
|
253
|
+
str.includes('activeProcessRunners') ||
|
|
254
|
+
str.includes('ProcessRunner') ||
|
|
255
|
+
str.includes('activeChildren')
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Force cleanup of all command-stream SIGINT handlers and state - for testing
|
|
261
|
+
*/
|
|
262
|
+
export function forceCleanupAll() {
|
|
263
|
+
const sigintListeners = process.listeners('SIGINT');
|
|
264
|
+
const commandStreamListeners = sigintListeners.filter(
|
|
265
|
+
isCommandStreamListener
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
commandStreamListeners.forEach((listener) => {
|
|
269
|
+
process.removeListener('SIGINT', listener);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
activeProcessRunners.clear();
|
|
273
|
+
sigintHandlerInstalled = false;
|
|
274
|
+
sigintHandler = null;
|
|
275
|
+
|
|
276
|
+
trace('SignalHandler', () => `Force cleanup completed`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Monitor parent streams for graceful shutdown
|
|
281
|
+
*/
|
|
282
|
+
export function monitorParentStreams() {
|
|
283
|
+
if (parentStreamsMonitored) {
|
|
284
|
+
trace('StreamMonitor', () => 'Parent streams already monitored, skipping');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
trace('StreamMonitor', () => 'Setting up parent stream monitoring');
|
|
288
|
+
parentStreamsMonitored = true;
|
|
289
|
+
|
|
290
|
+
const checkParentStream = (stream, name) => {
|
|
291
|
+
if (stream && typeof stream.on === 'function') {
|
|
292
|
+
stream.on('close', () => {
|
|
293
|
+
trace(
|
|
294
|
+
'ProcessRunner',
|
|
295
|
+
() =>
|
|
296
|
+
`Parent ${name} closed - triggering graceful shutdown | ${JSON.stringify({ activeProcesses: activeProcessRunners.size }, null, 2)}`
|
|
297
|
+
);
|
|
298
|
+
for (const runner of activeProcessRunners) {
|
|
299
|
+
if (runner._handleParentStreamClosure) {
|
|
300
|
+
runner._handleParentStreamClosure();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
checkParentStream(process.stdout, 'stdout');
|
|
308
|
+
checkParentStream(process.stderr, 'stderr');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Reset parent stream monitoring flag (for testing)
|
|
313
|
+
*/
|
|
314
|
+
export function resetParentStreamMonitoring() {
|
|
315
|
+
parentStreamsMonitored = false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get a valid fallback directory
|
|
320
|
+
* @returns {string} Fallback directory path
|
|
321
|
+
*/
|
|
322
|
+
function getFallbackDirectory() {
|
|
323
|
+
if (process.env.HOME && fs.existsSync(process.env.HOME)) {
|
|
324
|
+
return process.env.HOME;
|
|
325
|
+
}
|
|
326
|
+
if (fs.existsSync('/workspace/command-stream')) {
|
|
327
|
+
return '/workspace/command-stream';
|
|
328
|
+
}
|
|
329
|
+
return '/';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Restore working directory to initial or fallback
|
|
334
|
+
*/
|
|
335
|
+
function restoreWorkingDirectory() {
|
|
336
|
+
try {
|
|
337
|
+
let currentDir;
|
|
338
|
+
try {
|
|
339
|
+
currentDir = process.cwd();
|
|
340
|
+
} catch (_e) {
|
|
341
|
+
currentDir = null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (currentDir && currentDir === initialWorkingDirectory) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (fs.existsSync(initialWorkingDirectory)) {
|
|
349
|
+
process.chdir(initialWorkingDirectory);
|
|
350
|
+
} else {
|
|
351
|
+
const fallback = getFallbackDirectory();
|
|
352
|
+
process.chdir(fallback);
|
|
353
|
+
}
|
|
354
|
+
} catch (_e) {
|
|
355
|
+
try {
|
|
356
|
+
process.chdir(getFallbackDirectory());
|
|
357
|
+
} catch (e2) {
|
|
358
|
+
console.error('FATAL: Cannot set any working directory!', e2);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Cleanup all active runners
|
|
365
|
+
*/
|
|
366
|
+
function cleanupActiveRunners() {
|
|
367
|
+
for (const runner of activeProcessRunners) {
|
|
368
|
+
if (!runner) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
if (!runner.started && runner._cleanup) {
|
|
373
|
+
runner._cleanup();
|
|
374
|
+
} else if (runner.kill) {
|
|
375
|
+
runner.kill();
|
|
376
|
+
}
|
|
377
|
+
} catch (_e) {
|
|
378
|
+
// Ignore errors
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Complete global state reset for testing - clears all library state
|
|
385
|
+
*/
|
|
386
|
+
export function resetGlobalState() {
|
|
387
|
+
restoreWorkingDirectory();
|
|
388
|
+
cleanupActiveRunners();
|
|
389
|
+
forceCleanupAll();
|
|
390
|
+
clearShellCache();
|
|
391
|
+
parentStreamsMonitored = false;
|
|
392
|
+
resetShellSettings();
|
|
393
|
+
virtualCommandsEnabled = true;
|
|
394
|
+
resetAnsiConfig();
|
|
395
|
+
|
|
396
|
+
if (virtualCommands.size === 0) {
|
|
397
|
+
import('./commands/index.mjs').catch(() => {});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
trace('GlobalState', () => 'Global state reset completed');
|
|
401
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// EventEmitter-like implementation for stream events
|
|
2
|
+
// Provides a minimal event emitter for ProcessRunner
|
|
3
|
+
|
|
4
|
+
import { trace } from './$.trace.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Simple EventEmitter-like implementation for stream events
|
|
8
|
+
* Used as base class for ProcessRunner
|
|
9
|
+
*/
|
|
10
|
+
export class StreamEmitter {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.listeners = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a listener for an event
|
|
17
|
+
* @param {string} event - Event name
|
|
18
|
+
* @param {function} listener - Event handler
|
|
19
|
+
* @returns {this} For chaining
|
|
20
|
+
*/
|
|
21
|
+
on(event, listener) {
|
|
22
|
+
trace(
|
|
23
|
+
'StreamEmitter',
|
|
24
|
+
() =>
|
|
25
|
+
`on() called | ${JSON.stringify({
|
|
26
|
+
event,
|
|
27
|
+
hasExistingListeners: this.listeners.has(event),
|
|
28
|
+
listenerCount: this.listeners.get(event)?.length || 0,
|
|
29
|
+
})}`
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (!this.listeners.has(event)) {
|
|
33
|
+
this.listeners.set(event, []);
|
|
34
|
+
}
|
|
35
|
+
this.listeners.get(event).push(listener);
|
|
36
|
+
|
|
37
|
+
// No auto-start - explicit start() or await will start the process
|
|
38
|
+
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register a one-time listener for an event
|
|
44
|
+
* @param {string} event - Event name
|
|
45
|
+
* @param {function} listener - Event handler
|
|
46
|
+
* @returns {this} For chaining
|
|
47
|
+
*/
|
|
48
|
+
once(event, listener) {
|
|
49
|
+
trace('StreamEmitter', () => `once() called for event: ${event}`);
|
|
50
|
+
const onceWrapper = (...args) => {
|
|
51
|
+
this.off(event, onceWrapper);
|
|
52
|
+
listener(...args);
|
|
53
|
+
};
|
|
54
|
+
return this.on(event, onceWrapper);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Emit an event to all registered listeners
|
|
59
|
+
* @param {string} event - Event name
|
|
60
|
+
* @param {...*} args - Arguments to pass to listeners
|
|
61
|
+
* @returns {this} For chaining
|
|
62
|
+
*/
|
|
63
|
+
emit(event, ...args) {
|
|
64
|
+
const eventListeners = this.listeners.get(event);
|
|
65
|
+
trace(
|
|
66
|
+
'StreamEmitter',
|
|
67
|
+
() =>
|
|
68
|
+
`Emitting event | ${JSON.stringify({
|
|
69
|
+
event,
|
|
70
|
+
hasListeners: !!eventListeners,
|
|
71
|
+
listenerCount: eventListeners?.length || 0,
|
|
72
|
+
})}`
|
|
73
|
+
);
|
|
74
|
+
if (eventListeners) {
|
|
75
|
+
// Create a copy to avoid issues if listeners modify the array
|
|
76
|
+
const listenersToCall = [...eventListeners];
|
|
77
|
+
for (const listener of listenersToCall) {
|
|
78
|
+
listener(...args);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Remove a listener for an event
|
|
86
|
+
* @param {string} event - Event name
|
|
87
|
+
* @param {function} listener - Event handler to remove
|
|
88
|
+
* @returns {this} For chaining
|
|
89
|
+
*/
|
|
90
|
+
off(event, listener) {
|
|
91
|
+
trace(
|
|
92
|
+
'StreamEmitter',
|
|
93
|
+
() =>
|
|
94
|
+
`off() called | ${JSON.stringify({
|
|
95
|
+
event,
|
|
96
|
+
hasListeners: !!this.listeners.get(event),
|
|
97
|
+
listenerCount: this.listeners.get(event)?.length || 0,
|
|
98
|
+
})}`
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const eventListeners = this.listeners.get(event);
|
|
102
|
+
if (eventListeners) {
|
|
103
|
+
const index = eventListeners.indexOf(listener);
|
|
104
|
+
if (index !== -1) {
|
|
105
|
+
eventListeners.splice(index, 1);
|
|
106
|
+
trace('StreamEmitter', () => `Removed listener at index ${index}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
}
|