event-emission 0.1.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 +21 -0
- package/README.md +331 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/eventful.d.ts +153 -0
- package/dist/eventful.d.ts.map +1 -0
- package/dist/factory.d.ts +89 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/index.cjs +1126 -0
- package/dist/index.cjs.map +15 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1093 -0
- package/dist/index.js.map +15 -0
- package/dist/interop.d.ts +163 -0
- package/dist/interop.d.ts.map +1 -0
- package/dist/observe.d.ts +182 -0
- package/dist/observe.d.ts.map +1 -0
- package/dist/symbols.d.ts +6 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +107 -0
- package/src/errors.ts +10 -0
- package/src/eventful.ts +323 -0
- package/src/factory.ts +948 -0
- package/src/index.ts +71 -0
- package/src/interop.ts +271 -0
- package/src/observe.ts +734 -0
- package/src/symbols.ts +12 -0
- package/src/types.ts +206 -0
package/src/factory.ts
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import { BufferOverflowError } from './errors';
|
|
2
|
+
import {
|
|
3
|
+
createObservableProxy,
|
|
4
|
+
type ObservableEventMap,
|
|
5
|
+
type ObserveOptions,
|
|
6
|
+
} from './observe';
|
|
7
|
+
import { SymbolObservable } from './symbols';
|
|
8
|
+
import type {
|
|
9
|
+
AsyncIteratorOptions,
|
|
10
|
+
EventfulEvent,
|
|
11
|
+
EventTargetLike,
|
|
12
|
+
Listener,
|
|
13
|
+
Observer,
|
|
14
|
+
WildcardEvent,
|
|
15
|
+
WildcardListener,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a pattern matches a wildcard pattern.
|
|
20
|
+
*/
|
|
21
|
+
function matchesWildcard(eventType: string, pattern: string): boolean {
|
|
22
|
+
if (pattern === '*') return true;
|
|
23
|
+
if (pattern.endsWith(':*')) {
|
|
24
|
+
const namespace = pattern.slice(0, -2);
|
|
25
|
+
return eventType.startsWith(namespace + ':');
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for createEventTarget.
|
|
32
|
+
*
|
|
33
|
+
* @property onListenerError - Custom error handler called when a listener throws.
|
|
34
|
+
* If not provided, errors are emitted as 'error' events or re-thrown.
|
|
35
|
+
*/
|
|
36
|
+
export interface CreateEventTargetOptions {
|
|
37
|
+
/** Custom error handler for listener errors. Receives event type and error. */
|
|
38
|
+
onListenerError?: (type: string, error: unknown) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Options for createEventTarget with observe mode.
|
|
43
|
+
* Extends CreateEventTargetOptions with proxy observation settings.
|
|
44
|
+
*
|
|
45
|
+
* @property observe - Must be true to enable observation mode.
|
|
46
|
+
* @property deep - If true, nested objects are also observed (default: false).
|
|
47
|
+
* @property cloneStrategy - Strategy for cloning previous state: 'shallow', 'deep', or 'path'.
|
|
48
|
+
*/
|
|
49
|
+
export interface CreateEventTargetObserveOptions
|
|
50
|
+
extends CreateEventTargetOptions, ObserveOptions {
|
|
51
|
+
/** Must be true to enable observation mode. */
|
|
52
|
+
observe: true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates a type-safe event target with DOM EventTarget and TC39 Observable compatibility.
|
|
57
|
+
*
|
|
58
|
+
* @template E - Event map type where keys are event names and values are event detail types.
|
|
59
|
+
* @param opts - Optional configuration options.
|
|
60
|
+
* @returns A type-safe event target implementing EventTargetLike.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // Define event types
|
|
65
|
+
* type Events = {
|
|
66
|
+
* 'user:login': { userId: string };
|
|
67
|
+
* 'user:logout': { reason: string };
|
|
68
|
+
* };
|
|
69
|
+
*
|
|
70
|
+
* // Create event target
|
|
71
|
+
* const events = createEventTarget<Events>();
|
|
72
|
+
*
|
|
73
|
+
* // Add typed listener
|
|
74
|
+
* events.addEventListener('user:login', (event) => {
|
|
75
|
+
* console.log(`User logged in: ${event.detail.userId}`);
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* // Dispatch typed event
|
|
79
|
+
* events.dispatchEvent({ type: 'user:login', detail: { userId: '123' } });
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @overload Creates a basic event target
|
|
83
|
+
*/
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic constraint requires any
|
|
85
|
+
export function createEventTarget<E extends Record<string, any>>(
|
|
86
|
+
opts?: CreateEventTargetOptions,
|
|
87
|
+
): EventTargetLike<E>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates an observable proxy that dispatches events when properties change.
|
|
91
|
+
*
|
|
92
|
+
* @template T - The type of object to observe.
|
|
93
|
+
* @param target - The object to wrap with an observable proxy.
|
|
94
|
+
* @param opts - Configuration options with observe: true.
|
|
95
|
+
* @returns The proxied object with EventTargetLike methods mixed in.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // Create observable state
|
|
100
|
+
* const state = createEventTarget({ count: 0, user: { name: 'Alice' } }, {
|
|
101
|
+
* observe: true,
|
|
102
|
+
* deep: true,
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Listen for any update
|
|
106
|
+
* state.addEventListener('update', (event) => {
|
|
107
|
+
* console.log('State changed:', event.detail.current);
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* // Listen for specific property changes
|
|
111
|
+
* state.addEventListener('update:count', (event) => {
|
|
112
|
+
* console.log('Count changed to:', event.detail.value);
|
|
113
|
+
* });
|
|
114
|
+
*
|
|
115
|
+
* // Mutations trigger events automatically
|
|
116
|
+
* state.count = 1; // Triggers 'update' and 'update:count'
|
|
117
|
+
* state.user.name = 'Bob'; // Triggers 'update' and 'update:user.name'
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* @overload Wraps an object with a Proxy that dispatches events on mutations
|
|
121
|
+
*/
|
|
122
|
+
export function createEventTarget<T extends object>(
|
|
123
|
+
target: T,
|
|
124
|
+
opts: CreateEventTargetObserveOptions,
|
|
125
|
+
): T & EventTargetLike<ObservableEventMap<T>>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Creates a type-safe event target with DOM EventTarget and TC39 Observable compatibility.
|
|
129
|
+
*
|
|
130
|
+
* This is the main factory function for creating event emitters. It supports two modes:
|
|
131
|
+
*
|
|
132
|
+
* 1. **Basic Mode**: Creates a standalone event target for pub/sub messaging.
|
|
133
|
+
* 2. **Observe Mode**: Wraps an object with a Proxy that automatically dispatches
|
|
134
|
+
* events when properties are modified.
|
|
135
|
+
*
|
|
136
|
+
* Listener errors are handled via 'error' event: if a listener throws,
|
|
137
|
+
* an 'error' event is emitted. If no 'error' listener is registered,
|
|
138
|
+
* the error is re-thrown (Node.js behavior).
|
|
139
|
+
*
|
|
140
|
+
* @param targetOrOpts - Either the object to observe, or configuration options.
|
|
141
|
+
* @param opts - Configuration options when first argument is an object to observe.
|
|
142
|
+
* @returns Either an EventTargetLike or a proxied object with EventTargetLike methods.
|
|
143
|
+
*/
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic constraint requires any
|
|
145
|
+
export function createEventTarget<T extends object, E extends Record<string, any>>(
|
|
146
|
+
targetOrOpts?: T | CreateEventTargetOptions,
|
|
147
|
+
opts?: CreateEventTargetObserveOptions,
|
|
148
|
+
): EventTargetLike<E> | (T & EventTargetLike<ObservableEventMap<T>>) {
|
|
149
|
+
// Handle observe mode - opts.observe must be explicitly true
|
|
150
|
+
if (opts?.observe === true && targetOrOpts && typeof targetOrOpts === 'object') {
|
|
151
|
+
const target = targetOrOpts as T;
|
|
152
|
+
const eventTarget = createEventTargetInternal<ObservableEventMap<T>>({
|
|
153
|
+
onListenerError: opts.onListenerError,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const proxy = createObservableProxy(target, eventTarget, {
|
|
157
|
+
deep: opts.deep,
|
|
158
|
+
cloneStrategy: opts.cloneStrategy,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Copy eventTarget methods onto the proxy
|
|
162
|
+
// Use defineProperty to avoid triggering proxy traps
|
|
163
|
+
const methodNames = [
|
|
164
|
+
'addEventListener',
|
|
165
|
+
'removeEventListener',
|
|
166
|
+
'dispatchEvent',
|
|
167
|
+
'clear',
|
|
168
|
+
'once',
|
|
169
|
+
'removeAllListeners',
|
|
170
|
+
'pipe',
|
|
171
|
+
'addWildcardListener',
|
|
172
|
+
'removeWildcardListener',
|
|
173
|
+
'complete',
|
|
174
|
+
'subscribe',
|
|
175
|
+
'toObservable',
|
|
176
|
+
'events',
|
|
177
|
+
] as const;
|
|
178
|
+
|
|
179
|
+
for (const name of methodNames) {
|
|
180
|
+
Object.defineProperty(proxy, name, {
|
|
181
|
+
value: eventTarget[name],
|
|
182
|
+
writable: false,
|
|
183
|
+
enumerable: false,
|
|
184
|
+
configurable: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add completed getter
|
|
189
|
+
Object.defineProperty(proxy, 'completed', {
|
|
190
|
+
get: () => eventTarget.completed,
|
|
191
|
+
enumerable: false,
|
|
192
|
+
configurable: true,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return proxy as T & EventTargetLike<ObservableEventMap<T>>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Original behavior
|
|
199
|
+
return createEventTargetInternal<E>(
|
|
200
|
+
targetOrOpts as CreateEventTargetOptions | undefined,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic constraint requires any
|
|
205
|
+
function createEventTargetInternal<E extends Record<string, any>>(
|
|
206
|
+
opts?: CreateEventTargetOptions,
|
|
207
|
+
): EventTargetLike<E> {
|
|
208
|
+
const listeners = new Map<string, Set<Listener<E[keyof E]>>>();
|
|
209
|
+
const wildcardListeners = new Set<WildcardListener<E>>();
|
|
210
|
+
let isCompleted = false;
|
|
211
|
+
const completionCallbacks = new Set<() => void>();
|
|
212
|
+
|
|
213
|
+
// Helper to handle listener errors: emit 'error' event or re-throw if no listener
|
|
214
|
+
const handleListenerError = (eventType: string, error: unknown) => {
|
|
215
|
+
// Prevent infinite recursion if 'error' listener itself throws
|
|
216
|
+
if (eventType === 'error') return;
|
|
217
|
+
|
|
218
|
+
// If custom error handler provided, use it
|
|
219
|
+
if (opts?.onListenerError) {
|
|
220
|
+
opts.onListenerError(eventType, error);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const errorListeners = listeners.get('error');
|
|
225
|
+
if (errorListeners && errorListeners.size > 0) {
|
|
226
|
+
// Emit 'error' event with the error as detail
|
|
227
|
+
for (const rec of Array.from(errorListeners)) {
|
|
228
|
+
try {
|
|
229
|
+
void rec.fn({ type: 'error', detail: error } as EventfulEvent<E[keyof E]>);
|
|
230
|
+
} catch {
|
|
231
|
+
// Swallow errors from error handlers to prevent infinite loops
|
|
232
|
+
}
|
|
233
|
+
if (rec.once) errorListeners.delete(rec);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
// No 'error' listener - re-throw (Node.js behavior)
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const notifyWildcardListeners = (eventType: string, detail: E[keyof E]) => {
|
|
242
|
+
if (wildcardListeners.size === 0) return;
|
|
243
|
+
|
|
244
|
+
for (const rec of Array.from(wildcardListeners)) {
|
|
245
|
+
if (!matchesWildcard(eventType, rec.pattern)) continue;
|
|
246
|
+
|
|
247
|
+
const wildcardEvent: WildcardEvent<E> = {
|
|
248
|
+
type: rec.pattern,
|
|
249
|
+
originalType: eventType as keyof E & string,
|
|
250
|
+
detail,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const res = rec.fn(wildcardEvent);
|
|
255
|
+
if (res && typeof res.then === 'function') {
|
|
256
|
+
res.catch((error) => {
|
|
257
|
+
try {
|
|
258
|
+
handleListenerError(eventType, error);
|
|
259
|
+
} catch (rethrown) {
|
|
260
|
+
// Re-throw async errors via queueMicrotask to preserve stack trace
|
|
261
|
+
queueMicrotask(() => {
|
|
262
|
+
throw rethrown;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
handleListenerError(eventType, error);
|
|
269
|
+
} finally {
|
|
270
|
+
if (rec.once) wildcardListeners.delete(rec);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const addEventListener: EventTargetLike<E>['addEventListener'] = (
|
|
276
|
+
type,
|
|
277
|
+
listener,
|
|
278
|
+
options,
|
|
279
|
+
) => {
|
|
280
|
+
if (isCompleted) {
|
|
281
|
+
// Return no-op unsubscribe if already completed
|
|
282
|
+
return () => {};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const opts2 = options ?? {};
|
|
286
|
+
const record: Listener<E[keyof E]> = {
|
|
287
|
+
fn: listener as Listener<E[keyof E]>['fn'],
|
|
288
|
+
once: opts2.once,
|
|
289
|
+
signal: opts2.signal,
|
|
290
|
+
};
|
|
291
|
+
let set = listeners.get(type);
|
|
292
|
+
if (!set) {
|
|
293
|
+
set = new Set();
|
|
294
|
+
listeners.set(type, set);
|
|
295
|
+
}
|
|
296
|
+
set.add(record);
|
|
297
|
+
const unsubscribe = () => {
|
|
298
|
+
const setNow = listeners.get(type);
|
|
299
|
+
setNow?.delete(record);
|
|
300
|
+
if (record.signal && record.abortHandler) {
|
|
301
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
if (opts2.signal) {
|
|
305
|
+
const onAbort = () => unsubscribe();
|
|
306
|
+
record.abortHandler = onAbort;
|
|
307
|
+
opts2.signal.addEventListener('abort', onAbort, { once: true });
|
|
308
|
+
if (opts2.signal.aborted) onAbort();
|
|
309
|
+
}
|
|
310
|
+
return unsubscribe;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const addWildcardListener: EventTargetLike<E>['addWildcardListener'] = (
|
|
314
|
+
pattern,
|
|
315
|
+
listener,
|
|
316
|
+
options,
|
|
317
|
+
) => {
|
|
318
|
+
if (isCompleted) return () => {};
|
|
319
|
+
|
|
320
|
+
const opts2 = options ?? {};
|
|
321
|
+
const record: WildcardListener<E> = {
|
|
322
|
+
fn: listener,
|
|
323
|
+
pattern,
|
|
324
|
+
once: opts2.once,
|
|
325
|
+
signal: opts2.signal,
|
|
326
|
+
};
|
|
327
|
+
wildcardListeners.add(record);
|
|
328
|
+
|
|
329
|
+
const unsubscribe = () => {
|
|
330
|
+
wildcardListeners.delete(record);
|
|
331
|
+
if (record.signal && record.abortHandler) {
|
|
332
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
if (opts2.signal) {
|
|
337
|
+
const onAbort = () => unsubscribe();
|
|
338
|
+
record.abortHandler = onAbort;
|
|
339
|
+
opts2.signal.addEventListener('abort', onAbort, { once: true });
|
|
340
|
+
if (opts2.signal.aborted) onAbort();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return unsubscribe;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const removeWildcardListener: EventTargetLike<E>['removeWildcardListener'] = (
|
|
347
|
+
pattern,
|
|
348
|
+
listener,
|
|
349
|
+
) => {
|
|
350
|
+
for (const record of wildcardListeners) {
|
|
351
|
+
if (record.pattern === pattern && record.fn === listener) {
|
|
352
|
+
wildcardListeners.delete(record);
|
|
353
|
+
if (record.signal && record.abortHandler) {
|
|
354
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const dispatchEvent: EventTargetLike<E>['dispatchEvent'] = (event) => {
|
|
362
|
+
if (isCompleted) return false;
|
|
363
|
+
|
|
364
|
+
// Notify wildcard listeners first (no overhead if none registered)
|
|
365
|
+
notifyWildcardListeners(event.type, event.detail as E[keyof E]);
|
|
366
|
+
|
|
367
|
+
const set = listeners.get(event.type);
|
|
368
|
+
if (!set || set.size === 0) return true;
|
|
369
|
+
for (const rec of Array.from(set)) {
|
|
370
|
+
try {
|
|
371
|
+
const res = rec.fn(event as EventfulEvent<E[keyof E]>);
|
|
372
|
+
if (res && typeof res.then === 'function') {
|
|
373
|
+
res.catch((error) => {
|
|
374
|
+
try {
|
|
375
|
+
handleListenerError(event.type, error);
|
|
376
|
+
} catch (rethrown) {
|
|
377
|
+
// Re-throw async errors via queueMicrotask to preserve stack trace
|
|
378
|
+
queueMicrotask(() => {
|
|
379
|
+
throw rethrown;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
handleListenerError(event.type, error);
|
|
386
|
+
} finally {
|
|
387
|
+
if (rec.once) set.delete(rec);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return true;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const removeEventListener: EventTargetLike<E>['removeEventListener'] = (
|
|
394
|
+
type,
|
|
395
|
+
listener,
|
|
396
|
+
) => {
|
|
397
|
+
const set = listeners.get(type);
|
|
398
|
+
if (!set) return;
|
|
399
|
+
|
|
400
|
+
for (const record of set) {
|
|
401
|
+
if (record.fn === listener) {
|
|
402
|
+
set.delete(record);
|
|
403
|
+
if (record.signal && record.abortHandler) {
|
|
404
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const clear = () => {
|
|
412
|
+
// Clean up abort handlers before clearing
|
|
413
|
+
for (const set of listeners.values()) {
|
|
414
|
+
for (const record of set) {
|
|
415
|
+
if (record.signal && record.abortHandler) {
|
|
416
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
set.clear();
|
|
420
|
+
}
|
|
421
|
+
listeners.clear();
|
|
422
|
+
|
|
423
|
+
// Clear wildcard listeners too
|
|
424
|
+
for (const record of wildcardListeners) {
|
|
425
|
+
if (record.signal && record.abortHandler) {
|
|
426
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
wildcardListeners.clear();
|
|
430
|
+
// Note: clear() does NOT trigger completion callbacks or set isCompleted
|
|
431
|
+
// Use complete() for that
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// New ergonomics
|
|
435
|
+
|
|
436
|
+
const once: EventTargetLike<E>['once'] = (type, listener, options) => {
|
|
437
|
+
return addEventListener(type, listener, { ...options, once: true });
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const removeAllListeners: EventTargetLike<E>['removeAllListeners'] = (type) => {
|
|
441
|
+
if (type !== undefined) {
|
|
442
|
+
const set = listeners.get(type);
|
|
443
|
+
if (set) {
|
|
444
|
+
// Clean up abort handlers before clearing
|
|
445
|
+
for (const record of set) {
|
|
446
|
+
if (record.signal && record.abortHandler) {
|
|
447
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
set.clear();
|
|
451
|
+
listeners.delete(type);
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
// Clear all listeners for all types
|
|
455
|
+
for (const set of listeners.values()) {
|
|
456
|
+
for (const record of set) {
|
|
457
|
+
if (record.signal && record.abortHandler) {
|
|
458
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
set.clear();
|
|
462
|
+
}
|
|
463
|
+
listeners.clear();
|
|
464
|
+
|
|
465
|
+
// Clear wildcard listeners too
|
|
466
|
+
for (const record of wildcardListeners) {
|
|
467
|
+
if (record.signal && record.abortHandler) {
|
|
468
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
wildcardListeners.clear();
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Pipe events from this emitter to another target.
|
|
477
|
+
*
|
|
478
|
+
* **Limitation**: Only forwards events for types that already have listeners
|
|
479
|
+
* when pipe() is called. Events for types registered afterward won't be piped.
|
|
480
|
+
*
|
|
481
|
+
* To ensure all events are piped, add at least one listener for each event type
|
|
482
|
+
* before calling pipe().
|
|
483
|
+
*/
|
|
484
|
+
const pipe: EventTargetLike<E>['pipe'] = (target, mapFn) => {
|
|
485
|
+
if (isCompleted) {
|
|
486
|
+
return () => {};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const unsubscribes: Array<() => void> = [];
|
|
490
|
+
|
|
491
|
+
// Subscribe to all current and future events by listening to each event type
|
|
492
|
+
// We need to track event types we've subscribed to
|
|
493
|
+
const subscribedTypes = new Set<string>();
|
|
494
|
+
|
|
495
|
+
const subscribeToType = (type: string) => {
|
|
496
|
+
if (subscribedTypes.has(type)) return;
|
|
497
|
+
subscribedTypes.add(type);
|
|
498
|
+
|
|
499
|
+
const unsub = addEventListener(type as keyof E & string, (event) => {
|
|
500
|
+
if (mapFn) {
|
|
501
|
+
const mapped = mapFn(event);
|
|
502
|
+
if (mapped !== null) {
|
|
503
|
+
// Type assertion via unknown is needed because mapFn output type matches target's event map
|
|
504
|
+
target.dispatchEvent(
|
|
505
|
+
mapped as unknown as Parameters<typeof target.dispatchEvent>[0],
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
// Type assertion via unknown is needed because caller ensures E and T are compatible
|
|
510
|
+
target.dispatchEvent(
|
|
511
|
+
event as unknown as Parameters<typeof target.dispatchEvent>[0],
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
unsubscribes.push(unsub);
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// Subscribe to all existing event types
|
|
519
|
+
for (const type of listeners.keys()) {
|
|
520
|
+
subscribeToType(type);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Clean up on completion
|
|
524
|
+
const completionUnsub = () => {
|
|
525
|
+
for (const unsub of unsubscribes) {
|
|
526
|
+
unsub();
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
completionCallbacks.add(completionUnsub);
|
|
530
|
+
|
|
531
|
+
return () => {
|
|
532
|
+
completionCallbacks.delete(completionUnsub);
|
|
533
|
+
for (const unsub of unsubscribes) {
|
|
534
|
+
unsub();
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const complete = () => {
|
|
540
|
+
if (isCompleted) return;
|
|
541
|
+
isCompleted = true;
|
|
542
|
+
|
|
543
|
+
// Trigger completion callbacks (pipes, subscriptions)
|
|
544
|
+
for (const cb of completionCallbacks) {
|
|
545
|
+
try {
|
|
546
|
+
cb();
|
|
547
|
+
} catch (err) {
|
|
548
|
+
// Completion callback errors use handleListenerError
|
|
549
|
+
// Use 'complete' as the event type for these errors
|
|
550
|
+
try {
|
|
551
|
+
handleListenerError('complete', err);
|
|
552
|
+
} catch {
|
|
553
|
+
// Swallow if no error listener
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
completionCallbacks.clear();
|
|
558
|
+
|
|
559
|
+
// Clear all listeners
|
|
560
|
+
for (const set of listeners.values()) {
|
|
561
|
+
for (const record of set) {
|
|
562
|
+
if (record.signal && record.abortHandler) {
|
|
563
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
set.clear();
|
|
567
|
+
}
|
|
568
|
+
listeners.clear();
|
|
569
|
+
|
|
570
|
+
// Clear wildcard listeners
|
|
571
|
+
for (const record of wildcardListeners) {
|
|
572
|
+
if (record.signal && record.abortHandler) {
|
|
573
|
+
record.signal.removeEventListener('abort', record.abortHandler);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
wildcardListeners.clear();
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// Observable interop
|
|
580
|
+
const subscribe: EventTargetLike<E>['subscribe'] = (
|
|
581
|
+
type,
|
|
582
|
+
observerOrNext,
|
|
583
|
+
error,
|
|
584
|
+
completeHandler,
|
|
585
|
+
) => {
|
|
586
|
+
let observer: Observer<EventfulEvent<E[keyof E & string]>>;
|
|
587
|
+
|
|
588
|
+
if (typeof observerOrNext === 'function') {
|
|
589
|
+
observer = {
|
|
590
|
+
next: observerOrNext as (value: EventfulEvent<E[keyof E & string]>) => void,
|
|
591
|
+
error,
|
|
592
|
+
complete: completeHandler,
|
|
593
|
+
};
|
|
594
|
+
} else {
|
|
595
|
+
observer = (observerOrNext ?? {}) as Observer<EventfulEvent<E[keyof E & string]>>;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let closed = false;
|
|
599
|
+
|
|
600
|
+
if (isCompleted) {
|
|
601
|
+
// Already completed, call complete immediately
|
|
602
|
+
if (observer.complete) {
|
|
603
|
+
try {
|
|
604
|
+
observer.complete();
|
|
605
|
+
} catch {
|
|
606
|
+
// Swallow
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
unsubscribe: () => {
|
|
611
|
+
closed = true;
|
|
612
|
+
},
|
|
613
|
+
get closed() {
|
|
614
|
+
return closed || isCompleted;
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const unsub = addEventListener(type, (event) => {
|
|
620
|
+
if (closed) return;
|
|
621
|
+
if (observer.next) {
|
|
622
|
+
try {
|
|
623
|
+
observer.next(event as EventfulEvent<E[keyof E & string]>);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
if (observer.error) {
|
|
626
|
+
try {
|
|
627
|
+
observer.error(err);
|
|
628
|
+
} catch {
|
|
629
|
+
// Swallow
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Track completion callback
|
|
637
|
+
const onComplete = () => {
|
|
638
|
+
if (closed) return;
|
|
639
|
+
closed = true;
|
|
640
|
+
if (observer.complete) {
|
|
641
|
+
try {
|
|
642
|
+
observer.complete();
|
|
643
|
+
} catch {
|
|
644
|
+
// Swallow
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
completionCallbacks.add(onComplete);
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
unsubscribe: () => {
|
|
652
|
+
if (closed) return;
|
|
653
|
+
closed = true;
|
|
654
|
+
completionCallbacks.delete(onComplete);
|
|
655
|
+
unsub();
|
|
656
|
+
},
|
|
657
|
+
get closed() {
|
|
658
|
+
return closed || isCompleted;
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const toObservable: EventTargetLike<E>['toObservable'] = () => {
|
|
664
|
+
const observable = {
|
|
665
|
+
subscribe: (
|
|
666
|
+
observerOrNext?:
|
|
667
|
+
| Observer<EventfulEvent<E[keyof E]>>
|
|
668
|
+
| ((value: EventfulEvent<E[keyof E]>) => void),
|
|
669
|
+
errorFn?: (error: unknown) => void,
|
|
670
|
+
completeFn?: () => void,
|
|
671
|
+
) => {
|
|
672
|
+
// For the full observable, we listen to all events via wildcard
|
|
673
|
+
let next: ((value: EventfulEvent<E[keyof E]>) => void) | undefined;
|
|
674
|
+
let error: ((error: unknown) => void) | undefined;
|
|
675
|
+
let completeCallback: (() => void) | undefined;
|
|
676
|
+
|
|
677
|
+
if (typeof observerOrNext === 'function') {
|
|
678
|
+
next = observerOrNext;
|
|
679
|
+
error = errorFn;
|
|
680
|
+
completeCallback = completeFn;
|
|
681
|
+
} else if (observerOrNext) {
|
|
682
|
+
next = observerOrNext.next?.bind(observerOrNext);
|
|
683
|
+
error = observerOrNext.error?.bind(observerOrNext);
|
|
684
|
+
completeCallback = observerOrNext.complete?.bind(observerOrNext);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let closed = false;
|
|
688
|
+
|
|
689
|
+
if (isCompleted) {
|
|
690
|
+
if (completeCallback) {
|
|
691
|
+
try {
|
|
692
|
+
completeCallback();
|
|
693
|
+
} catch {
|
|
694
|
+
// Swallow
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
unsubscribe: () => {
|
|
699
|
+
closed = true;
|
|
700
|
+
},
|
|
701
|
+
get closed() {
|
|
702
|
+
return true;
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const wildcardListener = (event: WildcardEvent<E>) => {
|
|
708
|
+
if (closed) return;
|
|
709
|
+
if (next) {
|
|
710
|
+
try {
|
|
711
|
+
next({ type: event.originalType, detail: event.detail });
|
|
712
|
+
} catch (err) {
|
|
713
|
+
if (error) {
|
|
714
|
+
try {
|
|
715
|
+
error(err);
|
|
716
|
+
} catch {
|
|
717
|
+
// Swallow
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const unsubscribe = addWildcardListener('*', wildcardListener);
|
|
725
|
+
|
|
726
|
+
const onComplete = () => {
|
|
727
|
+
if (closed) return;
|
|
728
|
+
closed = true;
|
|
729
|
+
if (completeCallback) {
|
|
730
|
+
try {
|
|
731
|
+
completeCallback();
|
|
732
|
+
} catch {
|
|
733
|
+
// Swallow
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
completionCallbacks.add(onComplete);
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
unsubscribe: () => {
|
|
741
|
+
if (closed) return;
|
|
742
|
+
closed = true;
|
|
743
|
+
unsubscribe();
|
|
744
|
+
completionCallbacks.delete(onComplete);
|
|
745
|
+
},
|
|
746
|
+
get closed() {
|
|
747
|
+
return closed || isCompleted;
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
},
|
|
751
|
+
[SymbolObservable]() {
|
|
752
|
+
return observable;
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
return observable;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// Async iterator
|
|
759
|
+
function events<K extends keyof E & string>(
|
|
760
|
+
type: K,
|
|
761
|
+
options?: AsyncIteratorOptions,
|
|
762
|
+
): AsyncIterableIterator<EventfulEvent<E[K]>> {
|
|
763
|
+
// If already completed, return an iterator that immediately yields done
|
|
764
|
+
if (isCompleted) {
|
|
765
|
+
return {
|
|
766
|
+
[Symbol.asyncIterator]() {
|
|
767
|
+
return this;
|
|
768
|
+
},
|
|
769
|
+
next(): Promise<IteratorResult<EventfulEvent<E[K]>>> {
|
|
770
|
+
return Promise.resolve({
|
|
771
|
+
value: undefined as unknown as EventfulEvent<E[K]>,
|
|
772
|
+
done: true,
|
|
773
|
+
});
|
|
774
|
+
},
|
|
775
|
+
return(): Promise<IteratorResult<EventfulEvent<E[K]>>> {
|
|
776
|
+
return Promise.resolve({
|
|
777
|
+
value: undefined as unknown as EventfulEvent<E[K]>,
|
|
778
|
+
done: true,
|
|
779
|
+
});
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const signal = options?.signal;
|
|
785
|
+
const bufferSize = options?.bufferSize ?? Infinity;
|
|
786
|
+
const overflowStrategy = options?.overflowStrategy ?? 'drop-oldest';
|
|
787
|
+
|
|
788
|
+
const buffer: Array<EventfulEvent<E[K]>> = [];
|
|
789
|
+
let resolve: ((result: IteratorResult<EventfulEvent<E[K]>>) => void) | null = null;
|
|
790
|
+
let done = false;
|
|
791
|
+
let hasOverflow = false;
|
|
792
|
+
|
|
793
|
+
const unsub = addEventListener(type, (event) => {
|
|
794
|
+
if (done) return;
|
|
795
|
+
|
|
796
|
+
if (resolve) {
|
|
797
|
+
// Someone is waiting, resolve immediately
|
|
798
|
+
const r = resolve;
|
|
799
|
+
resolve = null;
|
|
800
|
+
r({ value: event, done: false });
|
|
801
|
+
} else {
|
|
802
|
+
// Buffer the event
|
|
803
|
+
if (buffer.length >= bufferSize && bufferSize !== Infinity) {
|
|
804
|
+
switch (overflowStrategy) {
|
|
805
|
+
case 'drop-oldest':
|
|
806
|
+
buffer.shift();
|
|
807
|
+
buffer.push(event);
|
|
808
|
+
break;
|
|
809
|
+
case 'drop-latest':
|
|
810
|
+
// Don't add the new event
|
|
811
|
+
break;
|
|
812
|
+
case 'throw':
|
|
813
|
+
unsub();
|
|
814
|
+
completionCallbacks.delete(onComplete);
|
|
815
|
+
done = true;
|
|
816
|
+
hasOverflow = true;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
buffer.push(event);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// Handle completion
|
|
826
|
+
const onComplete = () => {
|
|
827
|
+
done = true;
|
|
828
|
+
if (resolve) {
|
|
829
|
+
const r = resolve;
|
|
830
|
+
resolve = null;
|
|
831
|
+
r({ value: undefined as unknown as EventfulEvent<E[K]>, done: true });
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
completionCallbacks.add(onComplete);
|
|
835
|
+
|
|
836
|
+
// Handle abort signal
|
|
837
|
+
let onAbort: (() => void) | null = null;
|
|
838
|
+
if (signal) {
|
|
839
|
+
onAbort = () => {
|
|
840
|
+
done = true;
|
|
841
|
+
completionCallbacks.delete(onComplete);
|
|
842
|
+
unsub();
|
|
843
|
+
if (resolve) {
|
|
844
|
+
const r = resolve;
|
|
845
|
+
resolve = null;
|
|
846
|
+
r({ value: undefined as unknown as EventfulEvent<E[K]>, done: true });
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
850
|
+
if (signal.aborted) onAbort();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const iterator: AsyncIterableIterator<EventfulEvent<E[K]>> = {
|
|
854
|
+
[Symbol.asyncIterator]() {
|
|
855
|
+
return this;
|
|
856
|
+
},
|
|
857
|
+
async next(): Promise<IteratorResult<EventfulEvent<E[K]>>> {
|
|
858
|
+
// Drain buffered events first, even if done
|
|
859
|
+
if (buffer.length > 0) {
|
|
860
|
+
return { value: buffer.shift()!, done: false };
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// After buffer is drained, check for overflow error
|
|
864
|
+
if (hasOverflow) {
|
|
865
|
+
hasOverflow = false;
|
|
866
|
+
throw new BufferOverflowError(type, bufferSize);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (done) {
|
|
870
|
+
return { value: undefined as unknown as EventfulEvent<E[K]>, done: true };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Prevent concurrent next() calls - if there's already a pending promise, reject
|
|
874
|
+
if (resolve !== null) {
|
|
875
|
+
return Promise.reject(
|
|
876
|
+
new Error(
|
|
877
|
+
'Concurrent calls to next() are not supported on this async iterator',
|
|
878
|
+
),
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Wait for next event
|
|
883
|
+
return new Promise<IteratorResult<EventfulEvent<E[K]>>>((_resolve, _reject) => {
|
|
884
|
+
if (done) {
|
|
885
|
+
_resolve({ value: undefined as unknown as EventfulEvent<E[K]>, done: true });
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (hasOverflow) {
|
|
889
|
+
hasOverflow = false;
|
|
890
|
+
_reject(new BufferOverflowError(type, bufferSize));
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
resolve = _resolve;
|
|
894
|
+
});
|
|
895
|
+
},
|
|
896
|
+
return(): Promise<IteratorResult<EventfulEvent<E[K]>>> {
|
|
897
|
+
// Resolve any pending promise before cleanup
|
|
898
|
+
if (resolve) {
|
|
899
|
+
const r = resolve;
|
|
900
|
+
resolve = null;
|
|
901
|
+
r({ value: undefined as unknown as EventfulEvent<E[K]>, done: true });
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
done = true;
|
|
905
|
+
completionCallbacks.delete(onComplete);
|
|
906
|
+
unsub();
|
|
907
|
+
|
|
908
|
+
// Clean up abort signal listener
|
|
909
|
+
if (signal && onAbort) {
|
|
910
|
+
signal.removeEventListener('abort', onAbort);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return Promise.resolve({
|
|
914
|
+
value: undefined as unknown as EventfulEvent<E[K]>,
|
|
915
|
+
done: true,
|
|
916
|
+
});
|
|
917
|
+
},
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
return iterator;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const target: EventTargetLike<E> = {
|
|
924
|
+
addEventListener,
|
|
925
|
+
removeEventListener,
|
|
926
|
+
dispatchEvent,
|
|
927
|
+
clear,
|
|
928
|
+
once,
|
|
929
|
+
removeAllListeners,
|
|
930
|
+
pipe,
|
|
931
|
+
addWildcardListener,
|
|
932
|
+
removeWildcardListener,
|
|
933
|
+
complete,
|
|
934
|
+
get completed() {
|
|
935
|
+
return isCompleted;
|
|
936
|
+
},
|
|
937
|
+
subscribe,
|
|
938
|
+
toObservable,
|
|
939
|
+
events,
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// Add Symbol.observable - return an observable that emits all events from all types
|
|
943
|
+
(target as EventTargetLike<E> & { [key: symbol]: unknown })[SymbolObservable] = () => {
|
|
944
|
+
return toObservable();
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
return target;
|
|
948
|
+
}
|