@axijs/emitter 1.0.0 → 1.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/README.md CHANGED
@@ -19,9 +19,10 @@ yarn add @axijs/emitter
19
19
 
20
20
  ## Features
21
21
 
22
- - **Strictly Typed**: Full TypeScript support for event arguments.
22
+ - **Strictly Typed**: Full TypeScript support for emitted values.
23
23
  - **No Magic Strings**: Object-based emitters instead of string keys (e.g., `on('event-name')`).
24
24
  - **State Emitting**: `StateEmitter` remembers the last emitted value and immediately triggers new subscribers.
25
+ - **Distinct Updates**: `StateEmitter` can optionally skip emitting unchanged values.
25
26
  - **Composite Subscriptions**: Easily group multiple subscriptions and teardown logic into a single `Subscription` object to prevent memory leaks.
26
27
 
27
28
  ## Usage
@@ -32,23 +33,18 @@ Create an isolated, strongly-typed event emitter.
32
33
  ```typescript
33
34
  import { Emitter } from '@axijs/emitter';
34
35
 
35
- // Define an emitter that expects a string and a number
36
- const onPlayerMove = new Emitter<[string, number]>();
36
+ const onPlayerMove = new Emitter<string>();
37
37
 
38
- // Subscribe to the event
39
- const sub = onPlayerMove.subscribe((player, x) => {
40
- console.log(`${player} moved to ${x}`);
38
+ const sub = onPlayerMove.subscribe((player) => {
39
+ console.log(`Player moved: ${player}`);
41
40
  });
42
41
 
43
- // Fire the event
44
- onPlayerMove.emit('Alice', 10);
42
+ onPlayerMove.emit('Alice');
45
43
 
46
- // Subscribe to fire only once
47
- onPlayerMove.once((player, x) => {
44
+ onPlayerMove.once((player) => {
48
45
  console.log('This will only run once!');
49
46
  });
50
47
 
51
- // Unsubscribe when no longer needed
52
48
  sub.unsubscribe();
53
49
  ```
54
50
 
@@ -59,19 +55,61 @@ It holds the latest value and immediately provides it to new subscribers.
59
55
  ```typescript
60
56
  import { StateEmitter } from '@axijs/emitter';
61
57
 
62
- // Initialize with a default state
63
- const health = new StateEmitter<[number]>([100]);
58
+ const health = new StateEmitter<number>(100);
64
59
 
65
- // New subscribers immediately receive the current state (100)
66
60
  health.subscribe((currentHealth) => {
67
- console.log(`Health is: ${currentHealth}`);
61
+ console.log(`Health is: ${currentHealth}`); // Immediately logs: "Health is: 100"
68
62
  });
69
63
 
70
- // Emit a new state. All listeners will be notified.
71
- health.emit(80);
64
+ health.emit(80); // Logs: "Health is: 80"
72
65
 
73
- // Get current state synchronously without subscribing
74
- console.log(health.value); // [80]
66
+ console.log(health.value); // 80
67
+ ```
68
+
69
+ #### Distinct Values & Custom Comparison
70
+ You can prevent the emitter from firing consecutively with the same value by using the `StateEmitterOptions`.
71
+
72
+ ```typescript
73
+ export interface StateEmitterOptions<T> {
74
+ distinct?: boolean;
75
+ compare?: (prev: T, next: T) => boolean;
76
+ }
77
+ ```
78
+
79
+ When `distinct: true` is set, the emitter checks if the new value is the same as the previous one.
80
+ **By default, it uses simple reference equality (`Object.is`)**, which is fast and works perfectly for primitives.
81
+
82
+ ```typescript
83
+ // Prevents emitting the same primitive value twice
84
+ const status = new StateEmitter('idle', { distinct: true });
85
+ status.emit('idle'); // Ignored
86
+ ```
87
+
88
+ For complex objects, or if you want to apply custom comparison logic, you can provide a `compare` callback.
89
+ This is useful when you only care about specific fields changing (like an `id`) or when using a deep equal function from a library.
90
+
91
+ ```typescript
92
+ interface User {
93
+ id: number;
94
+ name: string;
95
+ }
96
+
97
+ const activeUser = new StateEmitter<User>(
98
+ { id: 1, name: 'Alice' },
99
+ {
100
+ distinct: true,
101
+ // Custom logic: objects are considered the same if their IDs match
102
+ compare: (prev, next) => prev.id === next.id
103
+ }
104
+ );
105
+
106
+ activeUser.subscribe(user => console.log('User changed:', user.name));
107
+
108
+ // Will NOT emit, because the 'id' is still 1
109
+ activeUser.emit({ id: 1, name: 'Alice Updated' });
110
+
111
+ // WILL emit, because the 'id' has changed
112
+ activeUser.emit({ id: 2, name: 'Bob' });
75
113
  ```
76
114
 
77
115
  ### 3. Composite Subscriptions
@@ -80,8 +118,8 @@ Group multiple unsubscriptions together. Very useful for cleaning up UI componen
80
118
  ```typescript
81
119
  import { Emitter, Subscription } from '@axijs/emitter';
82
120
 
83
- const onJump = new Emitter<[]>();
84
- const onShoot = new Emitter<[]>();
121
+ const onJump = new Emitter<void>();
122
+ const onShoot = new Emitter<void>();
85
123
 
86
124
  const masterSub = new Subscription();
87
125
 
@@ -101,12 +139,8 @@ masterSub.unsubscribe();
101
139
 
102
140
  ## API Documentation
103
141
 
104
- For a complete list of classes, interfaces, and methods, please visit the [API Documentation](https://github.com/axijs/tools/tree/main/packages/emitter/docs/api).
105
-
106
- ## License
107
-
108
- MIT
109
- ```
142
+ For a complete list of classes, interfaces,
143
+ and methods, please visit the [API Documentation](https://github.com/axijs/tools/tree/main/packages/emitter/docs/api).
110
144
 
111
145
  ## License
112
146
 
package/dist/index.d.mts CHANGED
@@ -1,16 +1,17 @@
1
+ type ListenerType<T> = (value: T) => void;
1
2
  /**
2
3
  * Defines the public, read-only contract for an event emitter.
3
4
  * It allows subscribing to an event but not emitting it.
4
- * @template T A tuple representing the types of the event arguments.
5
+ * @template T The type of the emitted value.
5
6
  */
6
- type Subscribable<T extends any[]> = {
7
+ type Subscribable<T> = {
7
8
  readonly listenerCount: number;
8
9
  /**
9
10
  * Subscribes a listener to this event.
10
11
  * @returns A function to unsubscribe the listener.
11
12
  */
12
- subscribe(listener: (...args: T) => void): Unsubscribable;
13
- unsubscribe(listener: (...args: T) => void): boolean;
13
+ subscribe(listener: (value: T) => void): Unsubscribable;
14
+ unsubscribe(listener: (value: T) => void): boolean;
14
15
  clear(): void;
15
16
  };
16
17
  /**
@@ -51,9 +52,9 @@ declare class Subscription implements Unsubscribable {
51
52
  /**
52
53
  * A minimal, type-safe event emitter for a single event.
53
54
  * It does not manage state, it only manages subscribers and event dispatching.
54
- * @template T A tuple representing the types of the event arguments.
55
+ * @template T The type of the emitted value.
55
56
  */
56
- declare class Emitter<T extends any[]> implements Subscribable<T> {
57
+ declare class Emitter<T = void> implements Subscribable<T> {
57
58
  private listeners;
58
59
  /**
59
60
  * Returns the number of listeners.
@@ -63,54 +64,61 @@ declare class Emitter<T extends any[]> implements Subscribable<T> {
63
64
  * Subscribes a listener to this event.
64
65
  * @returns A Subscription object to manage the unsubscription.
65
66
  */
66
- subscribe(listener: (...args: T) => void): Subscription;
67
+ subscribe(listener: ListenerType<T>): Subscription;
67
68
  /**
68
69
  * Subscribes a listener that triggers only once and then automatically unsubscribes.
69
70
  * @param listener The callback function to execute once.
70
71
  * @returns A Subscription object (can be used to cancel before the event fires).
71
72
  */
72
- once(listener: (...args: T) => void): Subscription;
73
+ once(listener: ListenerType<T>): Subscription;
73
74
  /**
74
- * Manually unsubscribe by listener
75
- * @returns returns true if an listener has been removed, or false if the listener does not exist.
75
+ * Manually unsubscribe by listener.
76
+ * @returns returns true if a listener has been removed, or false if it does not exist.
76
77
  */
77
- unsubscribe(listener: (...args: T) => void): boolean;
78
+ unsubscribe(listener: ListenerType<T>): boolean;
78
79
  /**
79
80
  * Dispatches the event to all subscribed listeners.
80
81
  */
81
- emit(...args: T): void;
82
+ emit(value: T): void;
82
83
  /**
83
84
  * Clears all listeners.
84
85
  */
85
86
  clear(): void;
86
87
  }
87
88
 
89
+ interface StateEmitterOptions<T> {
90
+ distinct?: boolean;
91
+ compare?: (prev: T, next: T) => boolean;
92
+ }
88
93
  /**
89
94
  * An Emitter that stores the last emitted value.
90
95
  * New subscribers immediately receive the last value upon subscription.
91
96
  */
92
- declare class StateEmitter<T extends any[]> extends Emitter<T> {
97
+ declare class StateEmitter<T> extends Emitter<T> {
93
98
  private _lastValue;
99
+ private readonly _distinct;
100
+ private readonly _compare;
94
101
  /**
95
102
  * @param initialValue Optional initial value to set.
103
+ * @param options Optional behavior flags.
96
104
  */
97
- constructor(initialValue?: T);
105
+ constructor(initialValue?: T, options?: StateEmitterOptions<T>);
98
106
  /**
99
107
  * Gets the current value synchronously without subscribing.
100
108
  */
101
109
  get value(): T | undefined;
102
110
  /**
103
111
  * Updates the state and notifies all listeners.
104
- * @param args The new value(s).
112
+ * @param value The new value.
105
113
  */
106
- emit(...args: T): void;
114
+ emit(value: T): void;
107
115
  /**
108
116
  * Subscribes to the event. If a value exists, the listener is called immediately.
109
117
  * @param listener The callback function.
110
118
  * @returns A Subscription object.
111
119
  */
112
- subscribe(listener: (...args: T) => void): Subscription;
120
+ subscribe(listener: (value: T) => void): Subscription;
113
121
  clear(): void;
114
122
  }
115
123
 
116
- export { Emitter, StateEmitter, type Subscribable, Subscription, type Unsubscribable };
124
+ export { Emitter, type ListenerType, StateEmitter, type StateEmitterOptions, type Subscribable, Subscription, type Unsubscribable };
package/dist/index.d.ts CHANGED
@@ -1,16 +1,17 @@
1
+ type ListenerType<T> = (value: T) => void;
1
2
  /**
2
3
  * Defines the public, read-only contract for an event emitter.
3
4
  * It allows subscribing to an event but not emitting it.
4
- * @template T A tuple representing the types of the event arguments.
5
+ * @template T The type of the emitted value.
5
6
  */
6
- type Subscribable<T extends any[]> = {
7
+ type Subscribable<T> = {
7
8
  readonly listenerCount: number;
8
9
  /**
9
10
  * Subscribes a listener to this event.
10
11
  * @returns A function to unsubscribe the listener.
11
12
  */
12
- subscribe(listener: (...args: T) => void): Unsubscribable;
13
- unsubscribe(listener: (...args: T) => void): boolean;
13
+ subscribe(listener: (value: T) => void): Unsubscribable;
14
+ unsubscribe(listener: (value: T) => void): boolean;
14
15
  clear(): void;
15
16
  };
16
17
  /**
@@ -51,9 +52,9 @@ declare class Subscription implements Unsubscribable {
51
52
  /**
52
53
  * A minimal, type-safe event emitter for a single event.
53
54
  * It does not manage state, it only manages subscribers and event dispatching.
54
- * @template T A tuple representing the types of the event arguments.
55
+ * @template T The type of the emitted value.
55
56
  */
56
- declare class Emitter<T extends any[]> implements Subscribable<T> {
57
+ declare class Emitter<T = void> implements Subscribable<T> {
57
58
  private listeners;
58
59
  /**
59
60
  * Returns the number of listeners.
@@ -63,54 +64,61 @@ declare class Emitter<T extends any[]> implements Subscribable<T> {
63
64
  * Subscribes a listener to this event.
64
65
  * @returns A Subscription object to manage the unsubscription.
65
66
  */
66
- subscribe(listener: (...args: T) => void): Subscription;
67
+ subscribe(listener: ListenerType<T>): Subscription;
67
68
  /**
68
69
  * Subscribes a listener that triggers only once and then automatically unsubscribes.
69
70
  * @param listener The callback function to execute once.
70
71
  * @returns A Subscription object (can be used to cancel before the event fires).
71
72
  */
72
- once(listener: (...args: T) => void): Subscription;
73
+ once(listener: ListenerType<T>): Subscription;
73
74
  /**
74
- * Manually unsubscribe by listener
75
- * @returns returns true if an listener has been removed, or false if the listener does not exist.
75
+ * Manually unsubscribe by listener.
76
+ * @returns returns true if a listener has been removed, or false if it does not exist.
76
77
  */
77
- unsubscribe(listener: (...args: T) => void): boolean;
78
+ unsubscribe(listener: ListenerType<T>): boolean;
78
79
  /**
79
80
  * Dispatches the event to all subscribed listeners.
80
81
  */
81
- emit(...args: T): void;
82
+ emit(value: T): void;
82
83
  /**
83
84
  * Clears all listeners.
84
85
  */
85
86
  clear(): void;
86
87
  }
87
88
 
89
+ interface StateEmitterOptions<T> {
90
+ distinct?: boolean;
91
+ compare?: (prev: T, next: T) => boolean;
92
+ }
88
93
  /**
89
94
  * An Emitter that stores the last emitted value.
90
95
  * New subscribers immediately receive the last value upon subscription.
91
96
  */
92
- declare class StateEmitter<T extends any[]> extends Emitter<T> {
97
+ declare class StateEmitter<T> extends Emitter<T> {
93
98
  private _lastValue;
99
+ private readonly _distinct;
100
+ private readonly _compare;
94
101
  /**
95
102
  * @param initialValue Optional initial value to set.
103
+ * @param options Optional behavior flags.
96
104
  */
97
- constructor(initialValue?: T);
105
+ constructor(initialValue?: T, options?: StateEmitterOptions<T>);
98
106
  /**
99
107
  * Gets the current value synchronously without subscribing.
100
108
  */
101
109
  get value(): T | undefined;
102
110
  /**
103
111
  * Updates the state and notifies all listeners.
104
- * @param args The new value(s).
112
+ * @param value The new value.
105
113
  */
106
- emit(...args: T): void;
114
+ emit(value: T): void;
107
115
  /**
108
116
  * Subscribes to the event. If a value exists, the listener is called immediately.
109
117
  * @param listener The callback function.
110
118
  * @returns A Subscription object.
111
119
  */
112
- subscribe(listener: (...args: T) => void): Subscription;
120
+ subscribe(listener: (value: T) => void): Subscription;
113
121
  clear(): void;
114
122
  }
115
123
 
116
- export { Emitter, StateEmitter, type Subscribable, Subscription, type Unsubscribable };
124
+ export { Emitter, type ListenerType, StateEmitter, type StateEmitterOptions, type Subscribable, Subscription, type Unsubscribable };
package/dist/index.js CHANGED
@@ -26,11 +26,6 @@ __export(index_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(index_exports);
28
28
 
29
- // ../ensure/dist/index.mjs
30
- function isFunction(value) {
31
- return typeof value === "function";
32
- }
33
-
34
29
  // src/subscription.ts
35
30
  var Subscription = class {
36
31
  _closed = false;
@@ -55,11 +50,12 @@ var Subscription = class {
55
50
  * @param teardown A function or another Unsubscribable object to be managed.
56
51
  */
57
52
  add(teardown) {
53
+ const isFunction = typeof teardown === "function";
58
54
  if (this._closed) {
59
- isFunction(teardown) ? teardown() : teardown.unsubscribe();
55
+ isFunction ? teardown() : teardown.unsubscribe();
60
56
  return;
61
57
  }
62
- this._teardowns.push(isFunction(teardown) ? teardown : () => teardown.unsubscribe());
58
+ this._teardowns.push(isFunction ? teardown : () => teardown.unsubscribe());
63
59
  }
64
60
  /**
65
61
  * Disposes the resources held by the subscription.
@@ -96,15 +92,15 @@ var Emitter = class {
96
92
  * @returns A Subscription object (can be used to cancel before the event fires).
97
93
  */
98
94
  once(listener) {
99
- const wrapper = (...args) => {
95
+ const wrapper = (value) => {
100
96
  this.unsubscribe(wrapper);
101
- listener(...args);
97
+ listener(value);
102
98
  };
103
99
  return this.subscribe(wrapper);
104
100
  }
105
101
  /**
106
- * Manually unsubscribe by listener
107
- * @returns returns true if an listener has been removed, or false if the listener does not exist.
102
+ * Manually unsubscribe by listener.
103
+ * @returns returns true if a listener has been removed, or false if it does not exist.
108
104
  */
109
105
  unsubscribe(listener) {
110
106
  return this.listeners.delete(listener);
@@ -112,8 +108,8 @@ var Emitter = class {
112
108
  /**
113
109
  * Dispatches the event to all subscribed listeners.
114
110
  */
115
- emit(...args) {
116
- this.listeners.forEach((listener) => listener(...args));
111
+ emit(value) {
112
+ this.listeners.forEach((listener) => listener(value));
117
113
  }
118
114
  /**
119
115
  * Clears all listeners.
@@ -126,12 +122,17 @@ var Emitter = class {
126
122
  // src/state-emitter.ts
127
123
  var StateEmitter = class extends Emitter {
128
124
  _lastValue;
125
+ _distinct;
126
+ _compare;
129
127
  /**
130
128
  * @param initialValue Optional initial value to set.
129
+ * @param options Optional behavior flags.
131
130
  */
132
- constructor(initialValue) {
131
+ constructor(initialValue, options = {}) {
133
132
  super();
134
133
  this._lastValue = initialValue ?? void 0;
134
+ this._distinct = options.distinct ?? false;
135
+ this._compare = options.compare ?? Object.is;
135
136
  }
136
137
  /**
137
138
  * Gets the current value synchronously without subscribing.
@@ -141,11 +142,14 @@ var StateEmitter = class extends Emitter {
141
142
  }
142
143
  /**
143
144
  * Updates the state and notifies all listeners.
144
- * @param args The new value(s).
145
+ * @param value The new value.
145
146
  */
146
- emit(...args) {
147
- this._lastValue = args;
148
- super.emit(...args);
147
+ emit(value) {
148
+ if (this._distinct && this._lastValue !== void 0 && this._compare(this._lastValue, value)) {
149
+ return;
150
+ }
151
+ this._lastValue = value;
152
+ super.emit(value);
149
153
  }
150
154
  /**
151
155
  * Subscribes to the event. If a value exists, the listener is called immediately.
@@ -155,7 +159,7 @@ var StateEmitter = class extends Emitter {
155
159
  subscribe(listener) {
156
160
  const unsubscribe = super.subscribe(listener);
157
161
  if (this._lastValue !== void 0) {
158
- listener(...this._lastValue);
162
+ listener(this._lastValue);
159
163
  }
160
164
  return unsubscribe;
161
165
  }
package/dist/index.mjs CHANGED
@@ -1,8 +1,3 @@
1
- // ../ensure/dist/index.mjs
2
- function isFunction(value) {
3
- return typeof value === "function";
4
- }
5
-
6
1
  // src/subscription.ts
7
2
  var Subscription = class {
8
3
  _closed = false;
@@ -27,11 +22,12 @@ var Subscription = class {
27
22
  * @param teardown A function or another Unsubscribable object to be managed.
28
23
  */
29
24
  add(teardown) {
25
+ const isFunction = typeof teardown === "function";
30
26
  if (this._closed) {
31
- isFunction(teardown) ? teardown() : teardown.unsubscribe();
27
+ isFunction ? teardown() : teardown.unsubscribe();
32
28
  return;
33
29
  }
34
- this._teardowns.push(isFunction(teardown) ? teardown : () => teardown.unsubscribe());
30
+ this._teardowns.push(isFunction ? teardown : () => teardown.unsubscribe());
35
31
  }
36
32
  /**
37
33
  * Disposes the resources held by the subscription.
@@ -68,15 +64,15 @@ var Emitter = class {
68
64
  * @returns A Subscription object (can be used to cancel before the event fires).
69
65
  */
70
66
  once(listener) {
71
- const wrapper = (...args) => {
67
+ const wrapper = (value) => {
72
68
  this.unsubscribe(wrapper);
73
- listener(...args);
69
+ listener(value);
74
70
  };
75
71
  return this.subscribe(wrapper);
76
72
  }
77
73
  /**
78
- * Manually unsubscribe by listener
79
- * @returns returns true if an listener has been removed, or false if the listener does not exist.
74
+ * Manually unsubscribe by listener.
75
+ * @returns returns true if a listener has been removed, or false if it does not exist.
80
76
  */
81
77
  unsubscribe(listener) {
82
78
  return this.listeners.delete(listener);
@@ -84,8 +80,8 @@ var Emitter = class {
84
80
  /**
85
81
  * Dispatches the event to all subscribed listeners.
86
82
  */
87
- emit(...args) {
88
- this.listeners.forEach((listener) => listener(...args));
83
+ emit(value) {
84
+ this.listeners.forEach((listener) => listener(value));
89
85
  }
90
86
  /**
91
87
  * Clears all listeners.
@@ -98,12 +94,17 @@ var Emitter = class {
98
94
  // src/state-emitter.ts
99
95
  var StateEmitter = class extends Emitter {
100
96
  _lastValue;
97
+ _distinct;
98
+ _compare;
101
99
  /**
102
100
  * @param initialValue Optional initial value to set.
101
+ * @param options Optional behavior flags.
103
102
  */
104
- constructor(initialValue) {
103
+ constructor(initialValue, options = {}) {
105
104
  super();
106
105
  this._lastValue = initialValue ?? void 0;
106
+ this._distinct = options.distinct ?? false;
107
+ this._compare = options.compare ?? Object.is;
107
108
  }
108
109
  /**
109
110
  * Gets the current value synchronously without subscribing.
@@ -113,11 +114,14 @@ var StateEmitter = class extends Emitter {
113
114
  }
114
115
  /**
115
116
  * Updates the state and notifies all listeners.
116
- * @param args The new value(s).
117
+ * @param value The new value.
117
118
  */
118
- emit(...args) {
119
- this._lastValue = args;
120
- super.emit(...args);
119
+ emit(value) {
120
+ if (this._distinct && this._lastValue !== void 0 && this._compare(this._lastValue, value)) {
121
+ return;
122
+ }
123
+ this._lastValue = value;
124
+ super.emit(value);
121
125
  }
122
126
  /**
123
127
  * Subscribes to the event. If a value exists, the listener is called immediately.
@@ -127,7 +131,7 @@ var StateEmitter = class extends Emitter {
127
131
  subscribe(listener) {
128
132
  const unsubscribe = super.subscribe(listener);
129
133
  if (this._lastValue !== void 0) {
130
- listener(...this._lastValue);
134
+ listener(this._lastValue);
131
135
  }
132
136
  return unsubscribe;
133
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axijs/emitter",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A minimalistic, type-safe library for single-event and state emitting",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/axijs/tools/tree/main/packages/emitter",