evnty 5.1.1 → 5.2.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/event.ts ADDED
@@ -0,0 +1,408 @@
1
+ import { Callback, Listener, FilterFunction, Predicate, Mapper, Reducer, Action, Fn, Emitter, MaybePromise, Promiseable } from './types.js';
2
+ import { Disposer } from './async.js';
3
+ import { Signal } from './signal.js';
4
+ import { ListenerRegistry } from './listener-registry.js';
5
+ import { DispatchResult } from './dispatch-result.js';
6
+
7
+ export type Unsubscribe = Action;
8
+
9
+ /**
10
+ * @internal
11
+ */
12
+ export class EventIterator<T> implements AsyncIterator<T, void, void> {
13
+ #signal: Signal<T>;
14
+
15
+ constructor(signal: Signal<T>) {
16
+ this.#signal = signal;
17
+ }
18
+
19
+ async next(): Promise<IteratorResult<T, void>> {
20
+ try {
21
+ const value = await this.#signal.receive();
22
+ return { value, done: false };
23
+ } catch {
24
+ return { value: undefined, done: true };
25
+ }
26
+ }
27
+
28
+ async return(): Promise<IteratorResult<T, void>> {
29
+ return { value: undefined, done: true };
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Multi-listener event emitter with async support.
35
+ * All registered listeners are called for each emission, and their return
36
+ * values are collected in a DispatchResult. Supports async iteration and
37
+ * an `onDispose` callback for cleanup.
38
+ *
39
+ * Differs from:
40
+ * - Signal: Event has persistent listeners; Signal is promise-based (receive per round)
41
+ * - Sequence: Event broadcasts to all listeners; Sequence is a single-consumer queue
42
+ *
43
+ * @template T - The type of value emitted to listeners (event payload)
44
+ * @template R - The return type of listener functions
45
+ */
46
+ export class Event<T = unknown, R = unknown> implements Emitter<T, DispatchResult<void | R>>, Promiseable<T>, Promise<T>, Disposable, AsyncIterable<T> {
47
+ #listeners = new ListenerRegistry<[T], R | void>();
48
+ #signal = new Signal<T>();
49
+ #disposer: Disposer;
50
+ #onDispose?: Callback;
51
+ #sink?: Fn<[T], DispatchResult<void | R>>;
52
+
53
+ readonly [Symbol.toStringTag] = 'Event';
54
+
55
+ /**
56
+ * Creates a new event.
57
+ *
58
+ * @param onDispose - A function to call on the event disposal.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * // Create a click event.
63
+ * const clickEvent = new Event<[x: number, y: number], void>();
64
+ * clickEvent.on(([x, y]) => console.log(`Clicked at ${x}, ${y}`));
65
+ * clickEvent.emit([10, 20]);
66
+ * ```
67
+ */
68
+ constructor(onDispose?: Callback) {
69
+ this.#onDispose = onDispose;
70
+ this.#disposer = new Disposer(this);
71
+ }
72
+
73
+ /**
74
+ * Returns a bound emit function for use as a callback.
75
+ * Useful for passing to other APIs that expect a function.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * const event = new Event<string>();
80
+ * someApi.onMessage(event.sink);
81
+ * ```
82
+ */
83
+ get sink(): Fn<[T], DispatchResult<void | R>> {
84
+ return (this.#sink ??= this.emit.bind(this));
85
+ }
86
+
87
+ /**
88
+ * DOM EventListener interface compatibility.
89
+ * Allows the event to be used directly with addEventListener.
90
+ */
91
+ handleEvent(event: T): void {
92
+ this.emit(event);
93
+ }
94
+
95
+ /**
96
+ * The number of listeners for the event.
97
+ */
98
+ get size(): number {
99
+ return this.#listeners.size;
100
+ }
101
+
102
+ /**
103
+ * Emits a value to all registered listeners.
104
+ * Each listener is called with the value and their return values are collected.
105
+ *
106
+ * @param value - The value to emit to all listeners.
107
+ * @returns A DispatchResult containing all listener return values.
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * const event = new Event<string, number>();
112
+ * event.on(str => str.length);
113
+ * const result = event.emit('hello');
114
+ * await result.all(); // [5]
115
+ * ```
116
+ */
117
+ emit(value: T): DispatchResult<void | R> {
118
+ this.#signal.emit(value);
119
+ return new DispatchResult(this.#listeners.dispatch(value));
120
+ }
121
+
122
+ /**
123
+ * Checks if the given listener is NOT registered for this event.
124
+ *
125
+ * @param listener - The listener function to check against the registered listeners.
126
+ * @returns `true` if the listener is not already registered; otherwise, `false`.
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * // Check if a listener is not already added
131
+ * if (event.lacks(myListener)) {
132
+ * event.on(myListener);
133
+ * }
134
+ * ```
135
+ */
136
+ lacks(listener: Listener<T, R>): boolean {
137
+ return !this.#listeners.has(listener);
138
+ }
139
+
140
+ /**
141
+ * Checks if the given listener is registered for this event.
142
+ *
143
+ * @param listener - The listener function to check.
144
+ * @returns `true` if the listener is currently registered; otherwise, `false`.
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * // Verify if a listener is registered
149
+ * if (event.has(myListener)) {
150
+ * console.log('Listener is already registered');
151
+ * }
152
+ * ```
153
+ */
154
+ has(listener: Listener<T, R>): boolean {
155
+ return this.#listeners.has(listener);
156
+ }
157
+
158
+ /**
159
+ * Removes a specific listener from this event.
160
+ *
161
+ * @param listener - The listener to remove.
162
+ * @returns The event instance for chaining.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * // Remove a listener
167
+ * event.off(myListener);
168
+ * ```
169
+ */
170
+ off(listener: Listener<T, R>): this {
171
+ this.#listeners.off(listener);
172
+ return this;
173
+ }
174
+
175
+ /**
176
+ * Registers a listener that is called on every emission.
177
+ *
178
+ * @param listener - The function to call when the event occurs.
179
+ * @returns A function that removes this listener when called.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * const unsubscribe = event.on((data) => {
184
+ * console.log('Event data:', data);
185
+ * });
186
+ * ```
187
+ */
188
+ on(listener: Listener<T, R>): Unsubscribe {
189
+ this.#listeners.on(listener);
190
+ return () => void this.off(listener);
191
+ }
192
+
193
+ /**
194
+ * Registers a listener that is called only once on the next emission, then auto-removed.
195
+ *
196
+ * @param listener - The listener to trigger once.
197
+ * @returns A function that removes this listener when called (if it hasn't fired yet).
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * const cancel = event.once((data) => {
202
+ * console.log('Received data once:', data);
203
+ * });
204
+ * ```
205
+ */
206
+ once(listener: Listener<T, R>): Unsubscribe {
207
+ this.#listeners.once(listener);
208
+ return () => void this.off(listener);
209
+ }
210
+
211
+ /**
212
+ * Removes all listeners from the event.
213
+ * Does not dispose the event - new listeners can still be added after clearing.
214
+ *
215
+ * @returns The event instance for chaining.
216
+ */
217
+ clear(): this {
218
+ this.#listeners.clear();
219
+ return this;
220
+ }
221
+
222
+ /**
223
+ * Waits for the next emission and returns the emitted value.
224
+ *
225
+ * @returns A promise that resolves with the next emitted value.
226
+ */
227
+ receive(): Promise<T> {
228
+ return this.#signal.receive();
229
+ }
230
+
231
+ then<OK = T, ERR = never>(onfulfilled?: Fn<[T], MaybePromise<OK>> | null, onrejected?: Fn<[unknown], MaybePromise<ERR>> | null): Promise<OK | ERR> {
232
+ return this.receive().then(onfulfilled, onrejected);
233
+ }
234
+
235
+ catch<ERR = never>(onrejected?: Fn<[unknown], MaybePromise<ERR>> | null): Promise<T | ERR> {
236
+ return this.receive().catch(onrejected);
237
+ }
238
+
239
+ finally(onfinally?: Action | null): Promise<T> {
240
+ return this.receive().finally(onfinally);
241
+ }
242
+
243
+ /**
244
+ * Waits for the next emission via `receive()` and wraps the outcome in a
245
+ * `PromiseSettledResult` - always resolves, never rejects.
246
+ *
247
+ * @returns A promise that resolves with the settled result.
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * const result = await event.settle();
252
+ * if (result.status === 'fulfilled') {
253
+ * console.log('Value:', result.value);
254
+ * } else {
255
+ * console.error('Reason:', result.reason);
256
+ * }
257
+ * ```
258
+ */
259
+ settle(): Promise<PromiseSettledResult<T>> {
260
+ return this.receive().then(
261
+ (value) => ({ status: 'fulfilled', value }) as const,
262
+ (reason: unknown) => ({ status: 'rejected', reason }) as const,
263
+ );
264
+ }
265
+
266
+ [Symbol.asyncIterator](): AsyncIterator<T, void, void> {
267
+ return new EventIterator(this.#signal);
268
+ }
269
+
270
+ dispose(): void {
271
+ this[Symbol.dispose]();
272
+ }
273
+
274
+ [Symbol.dispose](): void {
275
+ if (this.#disposer[Symbol.dispose]()) {
276
+ this.#signal[Symbol.dispose]();
277
+ this.#listeners.clear();
278
+ void this.#onDispose?.();
279
+ }
280
+ }
281
+ }
282
+
283
+ export type EventParameters<T> = T extends Event<infer P, any> ? P : never;
284
+
285
+ export type EventReturn<T> = T extends Event<any, infer R> ? R : never;
286
+
287
+ export type AllEventsParameters<T extends Event<any, any>[]> = { [K in keyof T]: EventParameters<T[K]> }[number];
288
+
289
+ export type AllEventsResults<T extends Event<any, any>[]> = { [K in keyof T]: EventReturn<T[K]> }[number];
290
+
291
+ /**
292
+ * Merges multiple events into a single event that triggers whenever any source triggers.
293
+ * Disposing the merged event unsubscribes from all sources.
294
+ *
295
+ * @param events - The events to merge.
296
+ * @returns A new Event that forwards emissions from all sources.
297
+ *
298
+ * @example
299
+ * ```typescript
300
+ * const mouseEvent = createEvent<MouseEvent>();
301
+ * const keyboardEvent = createEvent<KeyboardEvent>();
302
+ * const inputEvent = merge(mouseEvent, keyboardEvent);
303
+ * inputEvent.on(event => console.log('Input event:', event));
304
+ * ```
305
+ */
306
+ export const merge = <Events extends Event<any, any>[]>(...events: Events): Event<AllEventsParameters<Events>, AllEventsResults<Events>> => {
307
+ const mergedEvent = new Event<AllEventsParameters<Events>, AllEventsResults<Events>>(() => {
308
+ for (const event of events) {
309
+ event.off(mergedEvent.sink);
310
+ }
311
+ });
312
+
313
+ for (const event of events) {
314
+ event.on(mergedEvent.sink);
315
+ }
316
+ return mergedEvent;
317
+ };
318
+
319
+ /**
320
+ * Creates a periodic event that emits an incrementing counter (starting from 0) at a fixed interval.
321
+ * Disposing the event clears the interval.
322
+ *
323
+ * @param interval - The interval in milliseconds.
324
+ * @returns An Event that triggers at the specified interval.
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * const tickEvent = createInterval(1000);
329
+ * tickEvent.on(tickNumber => console.log('Tick:', tickNumber));
330
+ * ```
331
+ */
332
+ export const createInterval = <R = unknown>(interval: number): Event<number, R> => {
333
+ let counter = 0;
334
+ const intervalEvent = new Event<number, R>(() => clearInterval(timerId));
335
+ const timerId: ReturnType<typeof setInterval> = setInterval(() => {
336
+ intervalEvent.emit(counter++);
337
+ }, interval);
338
+ return intervalEvent;
339
+ };
340
+
341
+ /**
342
+ * Creates a new Event instance for multi-listener event handling.
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * const messageEvent = createEvent<string>();
347
+ * messageEvent.on(msg => console.log('Received:', msg));
348
+ * messageEvent.emit('Hello'); // All listeners receive 'Hello'
349
+ *
350
+ * // Listeners can return values, collected via DispatchResult
351
+ * const validateEvent = createEvent<string, boolean>();
352
+ * validateEvent.on(str => str.length > 0);
353
+ * validateEvent.on(str => str.length < 100);
354
+ * const results = await validateEvent.emit('test').all(); // [true, true]
355
+ * ```
356
+ */
357
+ export const createEvent = <T = unknown, R = unknown>(): Event<T, R> => new Event<T, R>();
358
+
359
+ export default createEvent;
360
+
361
+ /**
362
+ * Extracts the listener function type from an Event type.
363
+ * Useful for type-safe listener definitions.
364
+ *
365
+ * @template E - The Event type to extract the listener type from
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * type MyEvent = Event<string, boolean>;
370
+ * type MyListener = EventHandler<MyEvent>; // (value: string) => boolean | Promise<boolean>
371
+ * ```
372
+ */
373
+ export type EventHandler<E> = E extends Event<infer T, infer R> ? Listener<T, R> : never;
374
+
375
+ /**
376
+ * Extracts a filter function type for an Event's parameters.
377
+ * Used for creating type-safe event filters.
378
+ *
379
+ * @template E - The Event type to create a filter for
380
+ */
381
+ export type EventFilter<E> = FilterFunction<EventParameters<E>>;
382
+
383
+ /**
384
+ * Extracts a predicate function type for an Event's parameters.
385
+ * Used for type narrowing with event values.
386
+ *
387
+ * @template E - The Event type to create a predicate for
388
+ * @template P - The narrowed type that the predicate validates
389
+ */
390
+ export type EventPredicate<E, P extends EventParameters<E>> = Predicate<EventParameters<E>, P>;
391
+
392
+ /**
393
+ * Extracts a mapper function type for transforming Event parameters.
394
+ * Used for creating type-safe event value transformations.
395
+ *
396
+ * @template E - The Event type to create a mapper for
397
+ * @template M - The target type to map event values to
398
+ */
399
+ export type EventMapper<E, M> = Mapper<EventParameters<E>, M>;
400
+
401
+ /**
402
+ * Extracts a reducer function type for Event parameters.
403
+ * Used for creating type-safe event value reducers.
404
+ *
405
+ * @template E - The Event type to create a reducer for
406
+ * @template R - The accumulator type for the reduction
407
+ */
408
+ export type EventReducer<E, R> = Reducer<EventParameters<E>, R>;