asljs-eventful 0.1.6 → 0.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/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright (c) 2025 Alex Netkachov
3
+ Copyright (c) 2025 Alexandrite Software Ltd
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -32,7 +32,7 @@ const obj =
32
32
  eventful(
33
33
  { },
34
34
  { trace:
35
- (object, action, payload) => {
35
+ (action, payload) => {
36
36
  console.log(
37
37
  `Action: ${action}`,
38
38
  payload);
@@ -40,10 +40,10 @@ const obj =
40
40
 
41
41
  // Tracing actions include:
42
42
  // - 'new' on creation: payload { object }
43
- // - 'on' when subscribing: payload { event, listener }
44
- // - 'off' when unsubscribing: payload { event, listener }
45
- // - 'emit' for sync emit: payload { event, args, listeners }
46
- // - 'emitAsync' for async emit: payload { event, args, listeners }
43
+ // - 'on' when subscribing: payload { object, event, listener }
44
+ // - 'off' when unsubscribing: payload { object, event, listener }
45
+ // - 'emit' for sync emit: payload { object, event, args, listeners }
46
+ // - 'emitAsync' for async emit: payload { object, event, args, listeners }
47
47
  ```
48
48
 
49
49
  Custom error handler for listener errors:
@@ -53,10 +53,10 @@ const obj =
53
53
  eventful(
54
54
  { },
55
55
  { error:
56
- (err, { object, event, listener }) => {
56
+ ({ error, object, event, listener }) => {
57
57
  console.error(
58
58
  `Error in listener for event "${event}"`,
59
- err);
59
+ error);
60
60
  } });
61
61
  ```
62
62
 
@@ -69,22 +69,32 @@ const obj =
69
69
  { strict: true });
70
70
  ```
71
71
 
72
- Change the error handler or trace function globally:
72
+ ### Global Events
73
+
74
+ `eventful` is also a global emitter. When you create an enhanced object via `eventful(target, options)`, its lifecycle and actions are traced via the per-instance `trace` hook and also emitted as global events on `eventful`.
73
75
 
74
76
  ```js
75
- eventful.options.trace =
76
- (object, action, payload) =>
77
- console.log(
78
- `Action: ${action}`,
79
- payload);
80
-
81
- eventful.options.error =
82
- (err, { object, event, listener }) =>
83
- console.error(
84
- `Error in listener for event "${event}"`,
85
- err);
77
+ const offNew =
78
+ eventful.on(
79
+ 'new',
80
+ ({ object }) => {
81
+ console.log('created', object);
82
+ });
83
+
84
+ const offError =
85
+ eventful.on(
86
+ 'error',
87
+ ({ error, object, event }) => {
88
+ console.error('listener error', event, error);
89
+ });
90
+
91
+ // Later
92
+ offNew();
93
+ offError();
86
94
  ```
87
95
 
96
+ Note: if a **global** `eventful.on('error', ...)` listener throws, `eventful` throws a `ListenerError` (an `Error` subclass with fields `{ error, object, event, listener }`) to avoid an infinite error loop.
97
+
88
98
  ## API
89
99
 
90
100
  ### eventful([target], [options])
@@ -94,12 +104,8 @@ a new empty object is created.
94
104
 
95
105
  - `target` (Object): The object to be enhanced with event capabilities.
96
106
  - `options` (Object): Configuration options.
97
- - `error` (Function | null): Custom error handler for listener errors
98
- `(err, { object, event, listener })`. Defaults to `null`. Only called when
99
- provided.
100
- - `trace` (Function | null): Custom trace hook `(object, action, payload)`
101
- for `new`, `on`, `off`, `emit`, `emitAsync`. Defaults to `null`. Only
102
- called when provided.
107
+ - `error` (Function | null): Optional error hook called with `{ error, object, event, listener }`.
108
+ - `trace` (Function | null): Optional trace hook called with `(action, payload)`.
103
109
  - `strict` (Boolean): If true, propagates listener errors; otherwise they
104
110
  are isolated. Defaults to false.
105
111
 
@@ -107,7 +113,7 @@ a new empty object is created.
107
113
 
108
114
  Registers a listener for the specified event.
109
115
 
110
- - `event` (String): The event name.
116
+ - `event` (String | Symbol): The event name.
111
117
  - `listener` (Function): The callback function to be invoked when the event is
112
118
  emitted.
113
119
 
@@ -118,7 +124,7 @@ Returns a function to remove the listener.
118
124
  Registers a one-time listener for the specified event. The listener is removed
119
125
  after its first invocation.
120
126
 
121
- - `event` (String): The event name.
127
+ - `event` (String | Symbol): The event name.
122
128
  - `listener` (Function): The callback function to be invoked when the event is
123
129
  emitted.
124
130
 
@@ -139,7 +145,7 @@ obj.emit('tick', 2); // no-op; already unsubscribed
139
145
 
140
146
  Removes a listener for the specified event.
141
147
 
142
- - `event` (String): The event name.
148
+ - `event` (String | Symbol): The event name.
143
149
  - `listener` (Function): The callback function to be removed.
144
150
 
145
151
  ### emit(event, ...args)
@@ -147,7 +153,7 @@ Removes a listener for the specified event.
147
153
  Emits the specified event, invoking all registered listeners with the provided
148
154
  arguments.
149
155
 
150
- - `event` (String): The event name.
156
+ - `event` (String | Symbol): The event name.
151
157
  - `...args` (Any): Arguments to pass to the listeners.
152
158
 
153
159
  ### emitAsync(event, ...args)
@@ -156,7 +162,7 @@ Emits the specified event asynchronously, running listeners in parallel.
156
162
  In non-strict mode, all listeners run and rejections are isolated; in strict
157
163
  mode, the first rejection causes the returned promise to reject.
158
164
 
159
- - `event` (String): The event name.
165
+ - `event` (String | Symbol): The event name.
160
166
  - `...args` (Any): Arguments to pass to the listeners.
161
167
 
162
168
  Returns a Promise that resolves when all listeners have been invoked.
@@ -165,7 +171,7 @@ Returns a Promise that resolves when all listeners have been invoked.
165
171
 
166
172
  Checks if there are any listeners registered for the specified event.
167
173
 
168
- - `event` (String): The event name.
174
+ - `event` (String | Symbol): The event name.
169
175
 
170
176
  Returns `true` if there are listeners, otherwise `false`.
171
177
 
@@ -0,0 +1,2 @@
1
+ import type { EventfulFn } from './types.js';
2
+ export declare const eventful: EventfulFn;
@@ -0,0 +1,148 @@
1
+ import { ListenerError } from './types.js';
2
+ import { eventTypeGuard, functionTypeGuard, isFunction, isObject, } from './guards.js';
3
+ const eventfulImpl = (object = Object.create(null), options = {}) => {
4
+ if (!isObject(object)
5
+ && !isFunction(object)) {
6
+ throw new TypeError('Expect an object or a function.');
7
+ }
8
+ for (const method of ['on', 'once', 'off', 'emit', 'emitAsync', 'has']) {
9
+ if (object[method] !== undefined) {
10
+ throw new Error(`Method "${method}" already exists.`);
11
+ }
12
+ }
13
+ const { strict = false, trace = null, error = null } = options;
14
+ const traceHook = typeof trace === 'function'
15
+ ? trace
16
+ : null;
17
+ const errorHook = typeof error === 'function'
18
+ ? error
19
+ : null;
20
+ const enhanced = object !== eventful;
21
+ const traceFn = (action, payload) => {
22
+ traceHook?.(action, payload);
23
+ if (enhanced) {
24
+ eventful.emit(action, payload);
25
+ }
26
+ };
27
+ traceFn('new', { object });
28
+ const emptySet = new Set();
29
+ const map = new Map();
30
+ const properties = { enumerable: false,
31
+ configurable: true,
32
+ writable: true };
33
+ Object.defineProperties(object, { on: Object.assign({ value: on }, properties),
34
+ once: Object.assign({ value: once }, properties),
35
+ off: Object.assign({ value: off }, properties),
36
+ emit: Object.assign({ value: emit }, properties),
37
+ emitAsync: Object.assign({ value: emitAsync }, properties),
38
+ has: Object.assign({ value: has }, properties) });
39
+ return object;
40
+ function add(event, listener) {
41
+ let listeners = map.get(event);
42
+ if (!listeners) {
43
+ map.set(event, listeners = new Set());
44
+ }
45
+ listeners.add(listener);
46
+ }
47
+ function remove(event, listener) {
48
+ const listeners = map.get(event);
49
+ if (!listeners)
50
+ return false;
51
+ const deleted = listeners.delete(listener);
52
+ if (listeners.size === 0)
53
+ map.delete(event);
54
+ return deleted;
55
+ }
56
+ function reportListenerError(event, listener, err) {
57
+ const errorArgs = { error: err,
58
+ object: object,
59
+ event,
60
+ listener };
61
+ errorHook?.(errorArgs);
62
+ if (object === eventful
63
+ && event === 'error') {
64
+ throw new ListenerError('Error in a global error listener.', err, object, event, listener);
65
+ }
66
+ eventful.emit('error', errorArgs);
67
+ }
68
+ function on(event, listener) {
69
+ eventTypeGuard(event);
70
+ functionTypeGuard(listener);
71
+ traceFn('on', { object,
72
+ event,
73
+ listener });
74
+ add(event, listener);
75
+ let active = true;
76
+ return () => active
77
+ ? ((active = false), remove(event, listener))
78
+ : false;
79
+ }
80
+ function once(event, listener) {
81
+ eventTypeGuard(event);
82
+ functionTypeGuard(listener);
83
+ const off = on(event, (...args) => {
84
+ off();
85
+ listener(...args);
86
+ });
87
+ return off;
88
+ }
89
+ function off(event, listener) {
90
+ eventTypeGuard(event);
91
+ functionTypeGuard(listener);
92
+ traceFn('off', { object,
93
+ event,
94
+ listener });
95
+ return remove(event, listener);
96
+ }
97
+ function has(event) {
98
+ eventTypeGuard(event);
99
+ return (map.get(event)?.size ?? 0) > 0;
100
+ }
101
+ function emit(event, ...args) {
102
+ eventTypeGuard(event);
103
+ const listeners = map.get(event)
104
+ || emptySet;
105
+ traceFn('emit', { object,
106
+ listeners: [...listeners],
107
+ event,
108
+ args });
109
+ if (listeners.size === 0)
110
+ return;
111
+ for (const listener of listeners) {
112
+ try {
113
+ listener(...args);
114
+ }
115
+ catch (err) {
116
+ reportListenerError(event, listener, err);
117
+ if (strict)
118
+ throw err;
119
+ }
120
+ }
121
+ }
122
+ async function emitAsync(event, ...args) {
123
+ eventTypeGuard(event);
124
+ const listeners = map.get(event)
125
+ || emptySet;
126
+ traceFn('emitAsync', { object,
127
+ listeners: [...listeners],
128
+ event,
129
+ args });
130
+ if (listeners.size === 0)
131
+ return;
132
+ const calls = [...listeners].map(async (listener) => {
133
+ try {
134
+ await listener(...args);
135
+ }
136
+ catch (err) {
137
+ reportListenerError(event, listener, err);
138
+ if (strict)
139
+ throw err;
140
+ }
141
+ });
142
+ await (strict
143
+ ? Promise.all(calls)
144
+ : Promise.allSettled(calls));
145
+ }
146
+ };
147
+ export const eventful = eventfulImpl;
148
+ eventful(eventful);
@@ -0,0 +1,5 @@
1
+ import type { EventName } from './types.js';
2
+ export declare function eventTypeGuard(value: any): asserts value is EventName;
3
+ export declare function isFunction(value: any): value is Function;
4
+ export declare function isObject(value: any): value is object;
5
+ export declare function functionTypeGuard(value: any): asserts value is Function;
package/dist/guards.js ADDED
@@ -0,0 +1,18 @@
1
+ export function eventTypeGuard(value) {
2
+ if (typeof value !== 'string'
3
+ && typeof value !== 'symbol') {
4
+ throw new TypeError('Expect event to be a string or symbol.');
5
+ }
6
+ }
7
+ export function isFunction(value) {
8
+ return typeof value === 'function';
9
+ }
10
+ export function isObject(value) {
11
+ return typeof value === 'object'
12
+ && value !== null;
13
+ }
14
+ export function functionTypeGuard(value) {
15
+ if (!isFunction(value)) {
16
+ throw new TypeError('Expect a function.');
17
+ }
18
+ }
@@ -0,0 +1,97 @@
1
+ export type EventName = string | symbol;
2
+ export type EventMap = Record<EventName, any[]>;
3
+ export type Listener<Args extends any[] = any[]> = (...args: Args) => any;
4
+ export interface ListenerErrorArgs {
5
+ error: any;
6
+ object: object | Function;
7
+ event: EventName;
8
+ listener: Function;
9
+ }
10
+ export declare class ListenerError extends Error implements ListenerErrorArgs {
11
+ error: any;
12
+ object: object | Function;
13
+ event: EventName;
14
+ listener: Function;
15
+ constructor(message: string, error: any, object: object | Function, event: EventName, listener: Function);
16
+ }
17
+ type TraceAction = 'new' | 'on' | 'off' | 'emit' | 'emitAsync';
18
+ type TracePayloadByAction = {
19
+ new: {
20
+ object: object | Function;
21
+ };
22
+ on: {
23
+ object: object | Function;
24
+ event: EventName;
25
+ listener: Function;
26
+ };
27
+ off: {
28
+ object: object | Function;
29
+ event: EventName;
30
+ listener: Function;
31
+ };
32
+ emit: {
33
+ object: object | Function;
34
+ listeners: Function[];
35
+ event: EventName;
36
+ args: any[];
37
+ };
38
+ emitAsync: {
39
+ object: object | Function;
40
+ listeners: Function[];
41
+ event: EventName;
42
+ args: any[];
43
+ };
44
+ };
45
+ export type TraceFn = <A extends TraceAction>(action: A, args: TracePayloadByAction[A]) => void;
46
+ export type EventfulFn = EventfulFactory & Eventful;
47
+ export interface EventfulFactory {
48
+ <T extends object | Function | undefined, E extends EventMap = EventMap>(object?: T, options?: EventfulOptions): (T extends undefined ? {} : T) & Eventful<E>;
49
+ }
50
+ export interface EventfulOptions {
51
+ /**
52
+ * If true, exceptions from listeners are propagated (fail fast).
53
+ * When false, errors are isolated (ignored) after calling `error` hook.
54
+ */
55
+ strict?: boolean;
56
+ /**
57
+ * Optional tracing hook. Receives action name and a safe payload.
58
+ * Actions include: 'new', 'on', 'off', 'emit', 'emitAsync'.
59
+ * Use to integrate with your logger without exposing internals.
60
+ */
61
+ trace?: TraceFn | null;
62
+ /**
63
+ * Optional error hook. Receives structured context of listener failures
64
+ * (error, object, event, listener). Called for sync and async errors.
65
+ */
66
+ error?: ((error: ListenerErrorArgs) => void) | null;
67
+ }
68
+ export interface Eventful<E extends EventMap = EventMap> {
69
+ /**
70
+ * Subscribe to an event. Returns an unsubscribe function.
71
+ */
72
+ on(event: keyof E & EventName, listener: Listener<E[keyof E & EventName]>): () => boolean;
73
+ /**
74
+ * Subscribe once to an event. Returns an unsubscribe function
75
+ * (called automatically).
76
+ */
77
+ once(event: keyof E & EventName, listener: Listener<E[keyof E & EventName]>): () => boolean;
78
+ /**
79
+ * Unsubscribe a previously registered listener. Returns true if removed.
80
+ */
81
+ off(event: keyof E & EventName, listener: Listener<E[keyof E & EventName]>): boolean;
82
+ /**
83
+ * Emit an event synchronously. All listeners run in order.
84
+ * Errors are isolated (ignored) unless `strict` is true.
85
+ */
86
+ emit(event: keyof E & EventName, ...args: E[keyof E & EventName]): void;
87
+ /**
88
+ * Emit an event and wait for all listeners (run in parallel).
89
+ * Errors are isolated (ignored) unless `strict` is true.
90
+ */
91
+ emitAsync(event: keyof E & EventName, ...args: E[keyof E & EventName]): Promise<void>;
92
+ /**
93
+ * Returns true if there is at least one listener for the event.
94
+ */
95
+ has(event: keyof E & EventName): boolean;
96
+ }
97
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ export class ListenerError extends Error {
2
+ constructor(message, error, object, event, listener) {
3
+ super(message);
4
+ this.name = 'ListenerError';
5
+ this.error = error;
6
+ this.object = object;
7
+ this.event = event;
8
+ this.listener = listener;
9
+ }
10
+ }
package/eventful.d.ts CHANGED
@@ -1,105 +1,14 @@
1
-
2
- export type Listener<Args extends any[] = any[]> =
3
- (...args: Args) => any;
4
-
5
- export type EventMap =
6
- Record<string | symbol, any[]>;
7
-
8
- export interface Eventful<E extends EventMap =
9
- Record<string | symbol, any[]>> {
10
- /** Subscribe to an event. Returns an unsubscribe function. */
11
- on<K extends keyof E>(
12
- event: K,
13
- listener: Listener<E[K]>
14
- ): () => boolean;
15
-
16
- /** Subscribe once to an event. Returns an unsubscribe function (called automatically). */
17
- once<K extends keyof E>(
18
- event: K,
19
- listener: Listener<E[K]>
20
- ): () => boolean;
21
-
22
- /** Unsubscribe a previously registered listener. */
23
- off<K extends keyof E>(
24
- event: K,
25
- listener: Listener<E[K]>
26
- ): boolean;
27
-
28
- /** Emit an event synchronously. */
29
- emit<K extends keyof E>(
30
- event: K,
31
- ...args: E[K]
32
- ): void;
33
-
34
- /** Emit an event and wait for all listeners (run in parallel, errors isolated unless strict). */
35
- emitAsync<K extends keyof E>(
36
- event: K,
37
- ...args: E[K]
38
- ): Promise<void>;
39
-
40
- /** Returns true if there is at least one listener for the event. */
41
- has<K extends keyof E>(
42
- event: K
43
- ): boolean;
44
- }
45
-
46
-
47
- export interface ErrorContext {
48
- object: object | Function;
49
- event: string | symbol;
50
- listener: Function;
51
- }
52
-
53
- type TracePayload =
54
- | {
55
- event: string | symbol;
56
- args: any[];
57
- listeners?: Function[];
58
- }
59
- | {
60
- object: object | Function;
61
- };
62
-
63
- export interface EventfulOptions {
64
- /** If true, exceptions from listeners are propagated. Otherwise they are swallowed after calling `error`. */
65
- strict?: boolean;
66
-
67
- /** Optional tracing hook; defaults to `eventful.trace`. */
68
- trace?: ((
69
- object: object | Function,
70
- action: string,
71
- payload?: TracePayload
72
- ) => void) | null;
73
-
74
- /** Optional error hook; defaults to `eventful.error`. */
75
- error?: ((
76
- err: unknown,
77
- context: ErrorContext
78
- ) => void) | null;
79
- }
80
-
81
- export interface EventfulFactory {
82
- <T extends object | Function | undefined,
83
- E extends EventMap = Record<string | symbol, any[]>>(
84
- object?: T,
85
- options?: EventfulOptions
86
- ): (T extends undefined ? {} : T) & Eventful<E>;
87
-
88
- /** Global hooks you can override. */
89
- options: {
90
- /** Default tracer. Replace to integrate with your logger. */
91
- trace: (
92
- object: object | Function,
93
- action: string,
94
- payload?: TracePayload
95
- ) => void | null;
96
-
97
- /** Default error handler. Replace to integrate with your logger. */
98
- error: (
99
- err: unknown,
100
- context: ErrorContext
101
- ) => void | null;
102
- };
103
- }
104
-
105
- export const eventful: EventfulFactory;
1
+ export {
2
+ eventful
3
+ } from './dist/eventful.js';
4
+
5
+ export type {
6
+ EventName,
7
+ EventMap,
8
+ Eventful,
9
+ EventfulFactory,
10
+ EventfulOptions,
11
+ Listener,
12
+ ListenerError,
13
+ TraceFn
14
+ } from './dist/types.js';
package/eventful.js CHANGED
@@ -1,407 +1,3 @@
1
- /**
2
- * @typedef {Object} ErrorContext
3
- * @property {Object|Function} object The object emitting the event.
4
- * @property {string|symbol} event The event name.
5
- * @property {Function} listener The listener function that caused the error.
6
- */
7
-
8
- /**
9
- * @typedef {Object} EventfulOptions
10
- * @property {boolean} [strict] If true, exceptions from listeners are propagated.
11
- * @property {(object: Object|Function, action: string, payload: any) => void} [trace] Optional tracing hook.
12
- * @property {(err: any, context: ErrorContext) => void} [error] Optional error hook.
13
- */
14
-
15
- /**
16
- * A mixin that adds event emitter capabilities to an object.
17
- *
18
- * The method returns the original object enhanced with the event
19
- * subscribing and emitting methods.
20
- *
21
- * If no object is provided, a new empty object is created and enhanced.
22
- *
23
- * The options can configure the behavior of the event emitter.
24
- *
25
- * @param {Object|Function} object The object to enhance.
26
- * @param {EventfulOptions} options The options to configure the event emitter.
27
- * @returns {Object|Function} The enhanced object.
28
- *
29
- * @throws {TypeError} If the first argument is not an object or function.
30
- */
31
- const eventful =
32
- (object = Object.create(null),
33
- options = { }) =>
34
- {
35
- if ('object' !== typeof object
36
- && 'function' !== typeof object)
37
- {
38
- throw new TypeError(
39
- 'Expect an object or a function.');
40
- }
41
-
42
- const emptySet =
43
- new Set();
44
-
45
- const { strict = false,
46
- trace = null,
47
- error = null } =
48
- options;
49
-
50
- const globalOptions =
51
- eventful.options;
52
-
53
- const traceFn =
54
- trace
55
- || globalOptions.trace;
56
-
57
- if (isFunction(traceFn)) {
58
- traceFn(
59
- object,
60
- 'new');
61
- }
62
-
63
- for (const method of
64
- [ 'on',
65
- 'once',
66
- 'off',
67
- 'emit',
68
- 'emitAsync',
69
- 'has' ])
70
- {
71
- if (method in object) {
72
- throw new Error(
73
- `Method "${method}" already exists.`);
74
- }
75
- }
76
-
77
- /** @type {Map<string|symbol, Set<Function>>} */
78
- const map =
79
- new Map();
80
-
81
- const add =
82
- (event, listener) => {
83
- eventTypeGuard(event);
84
- functionTypeGuard(listener);
85
-
86
- let listeners =
87
- map.get(event);
88
-
89
- if (!listeners) {
90
- listeners = new Set();
91
- map.set(event, listeners);
92
- }
93
-
94
- listeners.add(listener);
95
- };
96
-
97
- const remove =
98
- (event, listener) => {
99
- eventTypeGuard(event);
100
- functionTypeGuard(listener);
101
-
102
- const listeners =
103
- map.get(event);
104
-
105
- if (!listeners)
106
- return false;
107
-
108
- const deleted =
109
- listeners.delete(listener);
110
-
111
- if (listeners.size === 0)
112
- map.delete(event);
113
-
114
- return deleted;
115
- };
116
-
117
- const getListeners =
118
- event => {
119
- eventTypeGuard(event);
120
-
121
- return map.get(event)
122
- || emptySet;
123
- };
124
-
125
- /**
126
- * Subscribe to an event.
127
- *
128
- * @type {(event: string|symbol, listener: Function) => () => boolean}
129
- */
130
- const on =
131
- (event, listener) => {
132
- eventTypeGuard(event);
133
- functionTypeGuard(listener);
134
-
135
- const traceFn =
136
- trace
137
- || globalOptions.trace;
138
-
139
- if (isFunction(traceFn)) {
140
- traceFn(
141
- object,
142
- 'on',
143
- { event,
144
- listener });
145
- }
146
-
147
- add(
148
- event,
149
- listener);
150
-
151
- let active = true;
152
-
153
- return () => {
154
- if (!active)
155
- return false;
156
-
157
- active = false;
158
-
159
- return remove(
160
- event,
161
- listener);
162
- };
163
- };
164
-
165
- /**
166
- * Subscribe to an event for a single occurrence.
167
- *
168
- * @type {(event: string|symbol, listener: Function) => () => boolean}
169
- */
170
- const once =
171
- (event, listener) => {
172
- eventTypeGuard(event);
173
- functionTypeGuard(listener);
174
-
175
- const off =
176
- on(
177
- event,
178
- (...args) => {
179
- off();
180
- listener(...args);
181
- });
182
-
183
- return off;
184
- };
185
-
186
-
187
- /** @type {(event: string|symbol) => boolean} */
188
- const has =
189
- event => {
190
- eventTypeGuard(event);
191
-
192
- return map.get(event)?.size > 0
193
- || false;
194
- };
195
-
196
- /**
197
- * Emit an event synchronously.
198
- * All listeners run in order.
199
- * Errors are isolated (ignored) unless `strict` is true.
200
- *
201
- * @param {string|symbol} event
202
- * @param {...any} args
203
- */
204
- const emit =
205
- (event, ...args) => {
206
- eventTypeGuard(event);
207
-
208
- const listeners =
209
- getListeners(event);
210
-
211
- const traceFn =
212
- trace
213
- || globalOptions.trace;
214
-
215
- if (isFunction(traceFn)) {
216
- traceFn(
217
- object,
218
- 'emit',
219
- { listeners: [...listeners],
220
- event,
221
- args });
222
- }
223
-
224
- if (listeners.size === 0)
225
- return;
226
-
227
- for (const listener of listeners) {
228
- try {
229
- listener(...args);
230
- } catch (err) {
231
- const errorFn =
232
- error
233
- || globalOptions.error;
234
-
235
- if (isFunction(errorFn)) {
236
- const context =
237
- { object,
238
- event,
239
- listener };
240
-
241
- errorFn(
242
- err,
243
- context);
244
- }
245
-
246
- if (strict)
247
- throw err;
248
- }
249
- }
250
- };
251
-
252
- /**
253
- * Emit an event and wait for all listeners (run in parallel).
254
- * Errors are isolated (ignored) so all listeners run.
255
- *
256
- * @param {string|symbol} event
257
- * @param {...any} args
258
- * @returns {Promise<void>}
259
- */
260
- const emitAsync =
261
- async (event, ...args) => {
262
- eventTypeGuard(event);
263
-
264
- const listeners =
265
- getListeners(event);
266
-
267
- const traceFn =
268
- trace
269
- || globalOptions.trace;
270
-
271
- if (isFunction(traceFn)) {
272
- traceFn(
273
- object,
274
- 'emitAsync',
275
- { listeners: [...listeners],
276
- event,
277
- args });
278
- }
279
-
280
- if (listeners.size === 0)
281
- return;
282
-
283
- const calls =
284
- Array.from(listeners)
285
- .map(listener =>
286
- new Promise(
287
- (resolve, reject) => {
288
- try {
289
- Promise.resolve(
290
- listener(...args))
291
- .then(
292
- resolve,
293
- err => {
294
- const errorFn =
295
- error
296
- || globalOptions.error;
297
-
298
- if (isFunction(errorFn)) {
299
- const context =
300
- { object,
301
- event,
302
- listener };
303
-
304
- errorFn(
305
- err,
306
- context);
307
- }
308
-
309
- reject(err);
310
- });
311
- } catch (err) {
312
- const errorFn =
313
- error
314
- || globalOptions.error;
315
-
316
- if (isFunction(errorFn)) {
317
- const context =
318
- { object,
319
- event,
320
- listener };
321
-
322
- errorFn(
323
- err,
324
- context);
325
- }
326
-
327
- reject(err);
328
- }
329
- }));
330
-
331
- if (strict)
332
- await Promise.all(calls);
333
- else
334
- await Promise.allSettled(calls);
335
- };
336
-
337
- /**
338
- * Unsubscribe from an event.
339
- *
340
- * @param {string} event The event name.
341
- * @param {Function} listener The handling function.
342
- * @returns {boolean} True if unsubscribed, false if not found.
343
- */
344
- const off =
345
- (event, listener) => {
346
- eventTypeGuard(event);
347
- functionTypeGuard(listener);
348
-
349
- const traceFn =
350
- trace
351
- || globalOptions.trace;
352
-
353
- if (isFunction(traceFn)) {
354
- traceFn(
355
- object,
356
- 'off',
357
- { event,
358
- listener });
359
- }
360
-
361
- return remove(
362
- event,
363
- listener);
364
- };
365
-
366
- const attributes =
367
- { enumerable: false,
368
- configurable: true,
369
- writable: true };
370
-
371
- Object.defineProperties(
372
- object,
373
- { on: { value: on, ...attributes },
374
- once: { value: once, ...attributes },
375
- off: { value: off, ...attributes },
376
- emit: { value: emit, ...attributes },
377
- emitAsync: { value: emitAsync, ...attributes },
378
- has: { value: has, ...attributes } });
379
-
380
- return object;
381
- };
382
-
383
- eventful.options =
384
- { trace: null,
385
- error: null };
386
-
387
- function eventTypeGuard(value) {
388
- if ('string' !== typeof value
389
- && 'symbol' !== typeof value)
390
- {
391
- throw new TypeError(
392
- 'Expect event to be a string or symbol.');
393
- }
394
- }
395
-
396
- function isFunction(value) {
397
- return 'function' === typeof value;
398
- }
399
-
400
- function functionTypeGuard(value) {
401
- if (!isFunction(value)) {
402
- throw new TypeError(
403
- 'Expect a function.');
404
- }
405
- }
406
-
407
- export { eventful };
1
+ export {
2
+ eventful
3
+ } from './dist/eventful.js';
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "asljs-eventful",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight event helper adding on/off/emit to any object.",
5
+ "files": [
6
+ "dist/**",
7
+ "eventful.js",
8
+ "eventful.d.ts",
9
+ "README.md",
10
+ "LICENSE.md"
11
+ ],
5
12
  "keywords": [
6
13
  "events",
7
14
  "javascript",
@@ -20,12 +27,23 @@
20
27
  "type": "module",
21
28
  "main": "eventful.js",
22
29
  "types": "eventful.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./eventful.d.ts",
33
+ "default": "./eventful.js"
34
+ }
35
+ },
23
36
  "directories": {
24
37
  "doc": "docs"
25
38
  },
26
39
  "scripts": {
27
- "test": "node --test",
28
- "test:watch": "node --watch --test",
29
- "coverage": "NODE_V8_COVERAGE=.coverage node --test && node -e \"console.log('Coverage in .coverage (use c8/istanbul if you want reports)')\""
40
+ "build": "tsc -p .",
41
+ "build:tests": "tsc -p tsconfig.tests.json",
42
+ "lint": "eslint .",
43
+ "lint:fix": "eslint . --fix",
44
+ "prepack": "npm run build",
45
+ "test": "npm run build && npm run build:tests && node --test .tests/*.test.js",
46
+ "test:watch": "npm run build && npm run build:tests && node --watch --test .tests/*.test.js",
47
+ "coverage": "npm run build && npm run build:tests && NODE_V8_COVERAGE=.coverage node --test .tests/*.test.js && node -e \"console.log('Coverage in .coverage (use c8/istanbul if you want reports)')\""
30
48
  }
31
49
  }
@@ -1,305 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { eventful } from '../eventful.js';
4
-
5
- test(
6
- 'eventful does not create a new object',
7
- () => {
8
- const original = { };
9
-
10
- const enhanced =
11
- eventful(original);
12
-
13
- assert.equal(
14
- enhanced,
15
- original);
16
- });
17
-
18
- test(
19
- 'eventful extends a function',
20
- () => {
21
- const original =
22
- () => { };
23
-
24
- const enhanced =
25
- eventful(original);
26
-
27
- assert.equal(
28
- enhanced,
29
- original);
30
- });
31
-
32
- test(
33
- 'trace is called on creation with action "new"',
34
- () => {
35
- const tracer =
36
- createTracer();
37
-
38
- const object =
39
- eventful(
40
- { },
41
- { trace: tracer.trace });
42
-
43
- const creation =
44
- tracer.getFirstTraceByAction('new');
45
-
46
- assert.ok(creation);
47
-
48
- assert.equal(
49
- creation.object,
50
- object);
51
- });
52
-
53
- test(
54
- 'eventful creates an empty event emitter object',
55
- () => {
56
- const obj =
57
- eventful();
58
-
59
- assert.ok(obj);
60
- assert.equal(typeof obj.on, 'function');
61
- assert.equal(typeof obj.off, 'function');
62
- assert.equal(typeof obj.emit, 'function');
63
- assert.equal(typeof obj.emitAsync, 'function');
64
- assert.equal(typeof obj.has, 'function');
65
- });
66
-
67
- test(
68
- 'eventful throws when an object has one of the event emitter methods',
69
- () => {
70
- for (const method of
71
- [ 'on',
72
- 'once',
73
- 'off',
74
- 'emit',
75
- 'emitAsync',
76
- 'has'])
77
- {
78
- assert.throws(
79
- () => eventful({ [method]: () => { } }));
80
- }
81
- });
82
-
83
- test(
84
- 'exceptions in listeners are suppressed by default',
85
- async () => {
86
- const obj =
87
- eventful();
88
-
89
- obj.on(
90
- 'test',
91
- () => { throw new Error('test error'); });
92
-
93
- await assert.doesNotReject(
94
- () => obj.emitAsync('test'));
95
- });
96
-
97
- test(
98
- 'strict mode propagates exceptions in listeners',
99
- async () => {
100
- const obj =
101
- eventful(
102
- { },
103
- { error: () => { },
104
- strict: true });
105
-
106
- obj.on(
107
- 'test',
108
- () => { throw new Error('test error'); });
109
-
110
- await assert.rejects(
111
- () => obj.emitAsync('test'));
112
- });
113
-
114
- test(
115
- 'async listener rejection is suppressed by default',
116
- async () => {
117
- const obj =
118
- eventful(
119
- { },
120
- { error: () => {} });
121
-
122
- obj.on(
123
- 'test',
124
- async () => { throw new Error('async fail'); });
125
-
126
- await assert.doesNotReject(
127
- () => obj.emitAsync('test'));
128
- });
129
-
130
- test(
131
- 'non-strict runs other listeners even if one fails',
132
- async () => {
133
- const obj =
134
- eventful(
135
- { },
136
- { error: () => {} });
137
-
138
- let ran = 0;
139
-
140
- obj.on(
141
- 'test',
142
- () => { ran += 1; });
143
-
144
- obj.on(
145
- 'test',
146
- () => { throw new Error('boom'); });
147
-
148
- obj.on(
149
- 'test',
150
- () => { ran += 1; });
151
-
152
- await assert.doesNotReject(
153
- () => obj.emitAsync('test'));
154
-
155
- assert.equal(ran, 2);
156
- });
157
-
158
- test(
159
- 'trace receives safe payload and action names',
160
- async () => {
161
- const tracer =
162
- createTracer();
163
-
164
- const obj =
165
- eventful(
166
- { },
167
- { trace: tracer.trace });
168
-
169
- const off = obj.on('e', () => {});
170
- obj.emit('e', 1, 2);
171
- await obj.emitAsync('e', 3, 4);
172
- off();
173
-
174
- // Expect actions at least 'on', 'emit', 'emitAsync'
175
- assert.ok(tracer.getFirstTraceByAction('on'));
176
- assert.ok(tracer.getFirstTraceByAction('emit'));
177
- assert.ok(tracer.getFirstTraceByAction('emitAsync'));
178
-
179
- const emitTrace =
180
- tracer.getFirstTraceByAction('emit');
181
-
182
- assert.ok(Array.isArray(emitTrace.payload.listeners));
183
- assert.equal(emitTrace.payload.event, 'e');
184
- assert.deepEqual(emitTrace.payload.args, [1, 2]);
185
-
186
- const emitAsyncTrace =
187
- tracer.getFirstTraceByAction('emitAsync');
188
-
189
- assert.ok(Array.isArray(emitAsyncTrace.payload.listeners));
190
- assert.equal(emitAsyncTrace.payload.event, 'e');
191
- assert.deepEqual(emitAsyncTrace.payload.args, [3, 4]);
192
- });
193
-
194
- test(
195
- 'error hook runs for async rejection (non-strict)',
196
- async () => {
197
- let errors = 0;
198
- const obj =
199
- eventful(
200
- { },
201
- { error: () => { errors += 1; } });
202
-
203
- obj.on('e', async () => { throw new Error('reject'); });
204
-
205
- await assert.doesNotReject(() => obj.emitAsync('e'));
206
- assert.equal(errors, 1);
207
- });
208
-
209
- test(
210
- 'has reflects subscribe and unsubscribe',
211
- () => {
212
- const obj =
213
- eventful();
214
-
215
- assert.equal(
216
- obj.has('x'),
217
- false);
218
-
219
- const off =
220
- obj.on(
221
- 'x',
222
- () => { });
223
-
224
- assert.equal(
225
- obj.has('x'),
226
- true);
227
-
228
- off();
229
-
230
- assert.equal(
231
- obj.has('x'),
232
- false);
233
- });
234
-
235
- test(
236
- 'strict mode propagates async rejections',
237
- async () => {
238
- const obj =
239
- eventful(
240
- { },
241
- { error: () => { },
242
- strict: true });
243
-
244
- obj.on(
245
- 'e',
246
- async () => { throw new Error('nope'); });
247
-
248
- await assert.rejects(() => obj.emitAsync('e'));
249
- });
250
-
251
- test(
252
- 'emit ignores errors when no error hook (non-strict)',
253
- () => {
254
- const obj =
255
- eventful();
256
-
257
- obj.on(
258
- 'x',
259
- () => { throw new Error('boom'); });
260
-
261
- assert.doesNotThrow(
262
- () => obj.emit('x'));
263
- });
264
-
265
- test(
266
- 'event must be string or symbol',
267
- () => {
268
- const obj =
269
- eventful();
270
-
271
- assert.throws(
272
- () => obj.on(123, () => {}),
273
- TypeError);
274
-
275
- assert.throws(
276
- () => obj.emit(123),
277
- TypeError);
278
-
279
- const s =
280
- Symbol('e');
281
-
282
- assert.doesNotThrow(
283
- () => obj.on(s, () => {}));
284
-
285
- assert.doesNotThrow(
286
- () => obj.emit(s));
287
- });
288
-
289
- function createTracer() {
290
- const traces = [];
291
-
292
- return {
293
- trace:
294
- (object, action, payload) =>
295
- traces.push({ object, action, payload }),
296
- getTraces:
297
- () => traces,
298
- getTracesByAction:
299
- action =>
300
- traces.filter(t => t.action === action),
301
- getFirstTraceByAction:
302
- action =>
303
- traces.find(t => t.action === action)
304
- };
305
- }