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/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * event-emission
3
+ *
4
+ * Lightweight typed event emitter with DOM EventTarget
5
+ * and TC39 Observable compatibility.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ // Re-export from symbols
11
+ export { SymbolObservable } from './symbols';
12
+
13
+ // Augment the global Symbol interface to include observable
14
+ declare global {
15
+ interface SymbolConstructor {
16
+ readonly observable: symbol;
17
+ }
18
+ }
19
+
20
+ // Re-export types
21
+ export type {
22
+ AddEventListenerOptionsLike,
23
+ AsyncIteratorOptions,
24
+ DOMEventLike,
25
+ DOMEventTargetLike,
26
+ EventfulEvent,
27
+ EventsIteratorOptions,
28
+ EventTargetLike,
29
+ InteropOptions,
30
+ Listener,
31
+ MinimalAbortSignal,
32
+ ObservableLike,
33
+ Observer,
34
+ OverflowStrategy,
35
+ Subscription,
36
+ WildcardEvent,
37
+ WildcardListener,
38
+ } from './types';
39
+
40
+ // Re-export errors
41
+ export { BufferOverflowError } from './errors';
42
+
43
+ // Re-export factory and options types
44
+ export type {
45
+ CreateEventTargetObserveOptions,
46
+ CreateEventTargetOptions,
47
+ } from './factory';
48
+ export { createEventTarget } from './factory';
49
+
50
+ // Re-export observation utilities and types
51
+ export type {
52
+ ArrayMutationDetail,
53
+ ArrayMutationMethod,
54
+ ObservableEventMap,
55
+ ObserveOptions,
56
+ PropertyChangeDetail,
57
+ } from './observe';
58
+ export {
59
+ getOriginal,
60
+ isObserved,
61
+ ORIGINAL_TARGET,
62
+ PROXY_MARKER,
63
+ setupEventForwarding,
64
+ } from './observe';
65
+
66
+ // Re-export interop
67
+ export type { FromEventTargetOptions } from './interop';
68
+ export { forwardToEventTarget, fromEventTarget, pipe } from './interop';
69
+
70
+ // Re-export Eventful class
71
+ export { Eventful } from './eventful';
package/src/interop.ts ADDED
@@ -0,0 +1,271 @@
1
+ import { createEventTarget } from './factory';
2
+ import type {
3
+ DOMEventLike,
4
+ DOMEventTargetLike,
5
+ EventTargetLike,
6
+ InteropOptions,
7
+ } from './types';
8
+
9
+ /**
10
+ * Forward events from an Eventful target to a DOM EventTarget.
11
+ *
12
+ * This function sets up a wildcard listener on the source that forwards
13
+ * all events to the DOM target using dispatchEvent.
14
+ *
15
+ * @template E - Event map type of the source.
16
+ * @param source - The Eventful source to forward events from.
17
+ * @param target - The DOM EventTarget to forward events to.
18
+ * @param options - Optional configuration including abort signal.
19
+ * @returns An unsubscribe function that stops forwarding when called.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const events = createEventTarget<{ click: { x: number; y: number } }>();
24
+ * const button = document.getElementById('my-button');
25
+ *
26
+ * // Forward all events to the DOM button
27
+ * const unsubscribe = forwardToEventTarget(events, button);
28
+ *
29
+ * // Now when you dispatch events on the Eventful target,
30
+ * // they will also be dispatched on the DOM element
31
+ * events.dispatchEvent({ type: 'click', detail: { x: 100, y: 200 } });
32
+ *
33
+ * // Stop forwarding
34
+ * unsubscribe();
35
+ * ```
36
+ */
37
+ export function forwardToEventTarget<E extends Record<string, unknown>>(
38
+ source: EventTargetLike<E>,
39
+ target: DOMEventTargetLike,
40
+ options?: InteropOptions,
41
+ ): () => void {
42
+ const unsubscribe = source.addWildcardListener(
43
+ '*',
44
+ (event) => {
45
+ // Create a DOM-like event object
46
+ const domEvent: DOMEventLike = {
47
+ type: event.originalType,
48
+ detail: event.detail,
49
+ };
50
+ target.dispatchEvent(domEvent);
51
+ },
52
+ options,
53
+ );
54
+
55
+ return unsubscribe;
56
+ }
57
+
58
+ /**
59
+ * Options for creating an Eventful target from a DOM EventTarget.
60
+ */
61
+ export interface FromEventTargetOptions extends InteropOptions {
62
+ /**
63
+ * Callback invoked when a listener throws an error.
64
+ * If not provided, errors will be re-thrown.
65
+ */
66
+ onListenerError?: (type: string, error: unknown) => void;
67
+ }
68
+
69
+ /**
70
+ * Create an Eventful target that listens to events from a DOM EventTarget.
71
+ *
72
+ * This function wraps a DOM EventTarget and forwards specified events to a new
73
+ * Eventful target, enabling type-safe event handling and TC39 Observable compatibility.
74
+ *
75
+ * @template E - Event map type where keys are event names and values are event detail types.
76
+ * @param domTarget - The DOM EventTarget to listen to events from.
77
+ * @param eventTypes - Array of event type names to forward from the DOM target.
78
+ * @param options - Optional configuration including abort signal and error handler.
79
+ * @returns An Eventful target with a destroy() method for cleanup.
80
+ *
81
+ * @example Basic usage with DOM element
82
+ * ```typescript
83
+ * const button = document.getElementById('my-button');
84
+ *
85
+ * type ButtonEvents = {
86
+ * click: MouseEvent;
87
+ * focus: FocusEvent;
88
+ * };
89
+ *
90
+ * const events = fromEventTarget<ButtonEvents>(button, ['click', 'focus']);
91
+ *
92
+ * // Type-safe event handling
93
+ * events.addEventListener('click', (event) => {
94
+ * console.log('Button clicked!', event.detail);
95
+ * });
96
+ *
97
+ * // Clean up when done
98
+ * events.destroy();
99
+ * ```
100
+ *
101
+ * @example With AbortSignal for automatic cleanup
102
+ * ```typescript
103
+ * const controller = new AbortController();
104
+ * const events = fromEventTarget<{ input: InputEvent }>(
105
+ * textField,
106
+ * ['input'],
107
+ * { signal: controller.signal }
108
+ * );
109
+ *
110
+ * // Later, abort to clean up all listeners
111
+ * controller.abort();
112
+ * ```
113
+ *
114
+ * @example Using TC39 Observable features
115
+ * ```typescript
116
+ * const events = fromEventTarget<{ scroll: Event }>(window, ['scroll']);
117
+ *
118
+ * // Subscribe with observer pattern
119
+ * const subscription = events.subscribe({
120
+ * next: (event) => console.log('Scrolled!'),
121
+ * complete: () => console.log('Done'),
122
+ * });
123
+ *
124
+ * // Or use async iteration
125
+ * for await (const event of events.events('scroll')) {
126
+ * console.log('Scroll event:', event);
127
+ * }
128
+ * ```
129
+ */
130
+ export function fromEventTarget<E extends Record<string, unknown>>(
131
+ domTarget: DOMEventTargetLike,
132
+ eventTypes: Array<keyof E & string>,
133
+ options?: FromEventTargetOptions,
134
+ ): EventTargetLike<E> & { destroy: () => void } {
135
+ const eventful = createEventTarget<E>({
136
+ onListenerError: options?.onListenerError,
137
+ });
138
+
139
+ const handlers = new Map<string, (event: DOMEventLike) => void>();
140
+
141
+ for (const type of eventTypes) {
142
+ const handler = (event: DOMEventLike) => {
143
+ eventful.dispatchEvent({
144
+ type,
145
+ detail: (event.detail ?? event) as E[typeof type],
146
+ });
147
+ };
148
+ handlers.set(type, handler);
149
+ domTarget.addEventListener(type, handler);
150
+ }
151
+
152
+ // Track abort handler for cleanup
153
+ let onAbort: (() => void) | null = null;
154
+
155
+ // Handle abort signal
156
+ if (options?.signal) {
157
+ onAbort = () => {
158
+ for (const [type, handler] of handlers) {
159
+ domTarget.removeEventListener(type, handler);
160
+ }
161
+ handlers.clear();
162
+ eventful.complete();
163
+ };
164
+ options.signal.addEventListener('abort', onAbort, { once: true });
165
+ if (options.signal.aborted) onAbort();
166
+ }
167
+
168
+ return {
169
+ addEventListener: eventful.addEventListener,
170
+ removeEventListener: eventful.removeEventListener,
171
+ dispatchEvent: eventful.dispatchEvent,
172
+ clear: eventful.clear,
173
+ once: eventful.once,
174
+ removeAllListeners: eventful.removeAllListeners,
175
+ pipe: eventful.pipe,
176
+ addWildcardListener: eventful.addWildcardListener,
177
+ removeWildcardListener: eventful.removeWildcardListener,
178
+ subscribe: eventful.subscribe,
179
+ toObservable: eventful.toObservable,
180
+ complete: eventful.complete,
181
+ get completed() {
182
+ return eventful.completed;
183
+ },
184
+ events: eventful.events,
185
+ destroy: () => {
186
+ // Clean up abort signal listener to prevent memory leak
187
+ if (options?.signal && onAbort) {
188
+ options.signal.removeEventListener('abort', onAbort);
189
+ }
190
+ for (const [type, handler] of handlers) {
191
+ domTarget.removeEventListener(type, handler);
192
+ }
193
+ handlers.clear();
194
+ eventful.complete();
195
+ },
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Pipe events from one Eventful target to another.
201
+ *
202
+ * This function sets up a wildcard listener on the source that forwards all
203
+ * events to the target. Useful for composing event streams, creating event
204
+ * buses, or building hierarchical event systems.
205
+ *
206
+ * @template E - Event map type shared by both source and target.
207
+ * @param source - The Eventful target to pipe events from.
208
+ * @param target - The Eventful target to pipe events to.
209
+ * @param options - Optional configuration including abort signal.
210
+ * @returns An unsubscribe function that stops piping when called.
211
+ *
212
+ * @example Basic event piping
213
+ * ```typescript
214
+ * const userEvents = createEventTarget<{ login: { userId: string } }>();
215
+ * const globalBus = createEventTarget<{ login: { userId: string } }>();
216
+ *
217
+ * // Pipe all user events to global bus
218
+ * const unsubscribe = pipe(userEvents, globalBus);
219
+ *
220
+ * // Events on userEvents now also dispatch on globalBus
221
+ * globalBus.addEventListener('login', (event) => {
222
+ * console.log('User logged in:', event.detail.userId);
223
+ * });
224
+ *
225
+ * userEvents.dispatchEvent({ type: 'login', detail: { userId: '123' } });
226
+ *
227
+ * // Stop piping
228
+ * unsubscribe();
229
+ * ```
230
+ *
231
+ * @example With AbortSignal for automatic cleanup
232
+ * ```typescript
233
+ * const controller = new AbortController();
234
+ * pipe(source, target, { signal: controller.signal });
235
+ *
236
+ * // Later, abort to stop piping
237
+ * controller.abort();
238
+ * ```
239
+ *
240
+ * @example Creating an event hierarchy
241
+ * ```typescript
242
+ * const componentA = createEventTarget<Events>();
243
+ * const componentB = createEventTarget<Events>();
244
+ * const appBus = createEventTarget<Events>();
245
+ *
246
+ * // Both components pipe to the app bus
247
+ * pipe(componentA, appBus);
248
+ * pipe(componentB, appBus);
249
+ *
250
+ * // Listen to all events at the app level
251
+ * appBus.addWildcardListener('*', (event) => {
252
+ * console.log('App event:', event.originalType, event.detail);
253
+ * });
254
+ * ```
255
+ */
256
+ export function pipe<E extends Record<string, unknown>>(
257
+ source: EventTargetLike<E>,
258
+ target: EventTargetLike<E>,
259
+ options?: InteropOptions,
260
+ ): () => void {
261
+ return source.addWildcardListener(
262
+ '*',
263
+ (event) => {
264
+ target.dispatchEvent({
265
+ type: event.originalType,
266
+ detail: event.detail as E[keyof E & string],
267
+ });
268
+ },
269
+ options,
270
+ );
271
+ }