@axijs/emitter 1.0.1 → 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
 
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
@@ -92,15 +92,15 @@ var Emitter = class {
92
92
  * @returns A Subscription object (can be used to cancel before the event fires).
93
93
  */
94
94
  once(listener) {
95
- const wrapper = (...args) => {
95
+ const wrapper = (value) => {
96
96
  this.unsubscribe(wrapper);
97
- listener(...args);
97
+ listener(value);
98
98
  };
99
99
  return this.subscribe(wrapper);
100
100
  }
101
101
  /**
102
- * Manually unsubscribe by listener
103
- * @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.
104
104
  */
105
105
  unsubscribe(listener) {
106
106
  return this.listeners.delete(listener);
@@ -108,8 +108,8 @@ var Emitter = class {
108
108
  /**
109
109
  * Dispatches the event to all subscribed listeners.
110
110
  */
111
- emit(...args) {
112
- this.listeners.forEach((listener) => listener(...args));
111
+ emit(value) {
112
+ this.listeners.forEach((listener) => listener(value));
113
113
  }
114
114
  /**
115
115
  * Clears all listeners.
@@ -122,12 +122,17 @@ var Emitter = class {
122
122
  // src/state-emitter.ts
123
123
  var StateEmitter = class extends Emitter {
124
124
  _lastValue;
125
+ _distinct;
126
+ _compare;
125
127
  /**
126
128
  * @param initialValue Optional initial value to set.
129
+ * @param options Optional behavior flags.
127
130
  */
128
- constructor(initialValue) {
131
+ constructor(initialValue, options = {}) {
129
132
  super();
130
133
  this._lastValue = initialValue ?? void 0;
134
+ this._distinct = options.distinct ?? false;
135
+ this._compare = options.compare ?? Object.is;
131
136
  }
132
137
  /**
133
138
  * Gets the current value synchronously without subscribing.
@@ -137,11 +142,14 @@ var StateEmitter = class extends Emitter {
137
142
  }
138
143
  /**
139
144
  * Updates the state and notifies all listeners.
140
- * @param args The new value(s).
145
+ * @param value The new value.
141
146
  */
142
- emit(...args) {
143
- this._lastValue = args;
144
- 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);
145
153
  }
146
154
  /**
147
155
  * Subscribes to the event. If a value exists, the listener is called immediately.
@@ -151,7 +159,7 @@ var StateEmitter = class extends Emitter {
151
159
  subscribe(listener) {
152
160
  const unsubscribe = super.subscribe(listener);
153
161
  if (this._lastValue !== void 0) {
154
- listener(...this._lastValue);
162
+ listener(this._lastValue);
155
163
  }
156
164
  return unsubscribe;
157
165
  }
package/dist/index.mjs CHANGED
@@ -64,15 +64,15 @@ var Emitter = class {
64
64
  * @returns A Subscription object (can be used to cancel before the event fires).
65
65
  */
66
66
  once(listener) {
67
- const wrapper = (...args) => {
67
+ const wrapper = (value) => {
68
68
  this.unsubscribe(wrapper);
69
- listener(...args);
69
+ listener(value);
70
70
  };
71
71
  return this.subscribe(wrapper);
72
72
  }
73
73
  /**
74
- * Manually unsubscribe by listener
75
- * @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.
76
76
  */
77
77
  unsubscribe(listener) {
78
78
  return this.listeners.delete(listener);
@@ -80,8 +80,8 @@ var Emitter = class {
80
80
  /**
81
81
  * Dispatches the event to all subscribed listeners.
82
82
  */
83
- emit(...args) {
84
- this.listeners.forEach((listener) => listener(...args));
83
+ emit(value) {
84
+ this.listeners.forEach((listener) => listener(value));
85
85
  }
86
86
  /**
87
87
  * Clears all listeners.
@@ -94,12 +94,17 @@ var Emitter = class {
94
94
  // src/state-emitter.ts
95
95
  var StateEmitter = class extends Emitter {
96
96
  _lastValue;
97
+ _distinct;
98
+ _compare;
97
99
  /**
98
100
  * @param initialValue Optional initial value to set.
101
+ * @param options Optional behavior flags.
99
102
  */
100
- constructor(initialValue) {
103
+ constructor(initialValue, options = {}) {
101
104
  super();
102
105
  this._lastValue = initialValue ?? void 0;
106
+ this._distinct = options.distinct ?? false;
107
+ this._compare = options.compare ?? Object.is;
103
108
  }
104
109
  /**
105
110
  * Gets the current value synchronously without subscribing.
@@ -109,11 +114,14 @@ var StateEmitter = class extends Emitter {
109
114
  }
110
115
  /**
111
116
  * Updates the state and notifies all listeners.
112
- * @param args The new value(s).
117
+ * @param value The new value.
113
118
  */
114
- emit(...args) {
115
- this._lastValue = args;
116
- 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);
117
125
  }
118
126
  /**
119
127
  * Subscribes to the event. If a value exists, the listener is called immediately.
@@ -123,7 +131,7 @@ var StateEmitter = class extends Emitter {
123
131
  subscribe(listener) {
124
132
  const unsubscribe = super.subscribe(listener);
125
133
  if (this._lastValue !== void 0) {
126
- listener(...this._lastValue);
134
+ listener(this._lastValue);
127
135
  }
128
136
  return unsubscribe;
129
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axijs/emitter",
3
- "version": "1.0.1",
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",