@alwatr/signal 5.2.1 → 6.0.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.
@@ -5,23 +5,37 @@ import {delay} from '@alwatr/delay';
5
5
  describe('StateSignal', () => {
6
6
  /** @type {StateSignal<number>} */
7
7
  let signal;
8
- const signalId = 'test-state-signal';
8
+ const name = 'test-state-signal';
9
9
 
10
10
  beforeEach(() => {
11
- signal = new StateSignal({signalId, initialValue: 0});
11
+ signal = new StateSignal({name, initialValue: 0});
12
12
  });
13
13
 
14
14
  afterEach(() => {
15
15
  signal.destroy();
16
16
  });
17
17
 
18
- it('should be defined and have the correct signalId and initial value', () => {
18
+ it('should be defined and have the correct name and initial value', () => {
19
19
  expect(StateSignal).toBeDefined();
20
20
  expect(signal).toBeInstanceOf(StateSignal);
21
- expect(signal.signalId).toBe(signalId);
21
+ expect(signal.name).toBe(name);
22
22
  expect(signal.get()).toBe(0);
23
23
  });
24
24
 
25
+ it('should notify subscriber with receivePrevious', async () => {
26
+ const callback = jest.fn();
27
+ const newValue = 42;
28
+
29
+ signal.subscribe(callback);
30
+ await delay.nextMacrotask();
31
+ expect(callback).toHaveBeenCalledTimes(1);
32
+ expect(callback).toHaveBeenCalledWith(0);
33
+ signal.set(newValue);
34
+ await delay.nextMacrotask();
35
+ expect(callback).toHaveBeenCalledTimes(2);
36
+ expect(callback).toHaveBeenCalledWith(newValue);
37
+ });
38
+
25
39
  it('should notify subscribers when value changes', async () => {
26
40
  const callback = jest.fn();
27
41
  const newValue = 42;
@@ -140,7 +154,7 @@ describe('StateSignal', () => {
140
154
  it('should notify subscribers multiple times for object values when they change', async () => {
141
155
  const callback = jest.fn();
142
156
  const value = {a: 1};
143
- const signal = new StateSignal({signalId: 'object-signal', initialValue: value});
157
+ const signal = new StateSignal({name: 'object-signal', initialValue: value});
144
158
 
145
159
  signal.subscribe(callback, {receivePrevious: false});
146
160
 
@@ -193,11 +207,12 @@ describe('StateSignal', () => {
193
207
 
194
208
  signal.subscribe(errorCallback, {receivePrevious: false});
195
209
  signal.subscribe(normalCallback, {receivePrevious: false});
196
- signal.set(1);
197
-
210
+ signal.set(5);
211
+ signal.set(10);
198
212
  await delay.nextMacrotask();
199
- expect(errorCallback).toHaveBeenCalledTimes(1);
200
- expect(normalCallback).toHaveBeenCalledTimes(1);
213
+ expect(errorCallback).toHaveBeenCalledTimes(2);
214
+ expect(normalCallback).toHaveBeenCalledTimes(2);
215
+ expect(normalCallback).toHaveBeenCalledWith(10)
201
216
  });
202
217
 
203
218
  describe('destroyed signal', () => {
@@ -206,23 +221,23 @@ describe('StateSignal', () => {
206
221
  });
207
222
 
208
223
  it('should throw an error when set is called on a destroyed signal', () => {
209
- expect(() => signal.set(1)).toThrow(`Cannot interact with a destroyed signal (id: ${signalId})`);
224
+ expect(() => signal.set(1)).toThrow(`Cannot interact with a destroyed signal (id: ${name})`);
210
225
  });
211
226
 
212
227
  it('should throw an error when subscribe is called on a destroyed signal', () => {
213
- expect(() => signal.subscribe(jest.fn())).toThrow(`Cannot interact with a destroyed signal (id: ${signalId})`);
228
+ expect(() => signal.subscribe(jest.fn())).toThrow(`Cannot interact with a destroyed signal (id: ${name})`);
214
229
  });
215
230
 
216
231
  it('should throw an error when untilNext is called on a destroyed signal', () => {
217
- expect(() => signal.untilNext()).toThrow(`Cannot interact with a destroyed signal (id: ${signalId})`);
232
+ expect(() => signal.untilNext()).toThrow(`Cannot interact with a destroyed signal (id: ${name})`);
218
233
  });
219
234
 
220
235
  it('should throw an error when accessing value on a destroyed signal', () => {
221
- expect(() => signal.get()).toThrow(`Cannot interact with a destroyed signal (id: ${signalId})`);
236
+ expect(() => signal.get()).toThrow(`Cannot interact with a destroyed signal (id: ${name})`);
222
237
  });
223
238
 
224
239
  it('should not notify any listeners after being destroyed', async () => {
225
- const localSignal = new StateSignal({signalId: 'local', initialValue: 0});
240
+ const localSignal = new StateSignal({name: 'local', initialValue: 0});
226
241
  const callback = jest.fn();
227
242
  localSignal.subscribe(callback, {receivePrevious: false});
228
243
 
@@ -0,0 +1,206 @@
1
+ import {describe, beforeEach, afterEach, it, expect, jest} from '@jest/globals';
2
+ import {ComputedSignal, createDebouncedSignal, StateSignal} from '@alwatr/signal';
3
+
4
+ describe('createDebouncedSignal', () => {
5
+ /** @type {ComputedSignal<number>} */
6
+ let debouncedSignal;
7
+ /** @type {StateSignal<number>} */
8
+ let sourceSignal;
9
+ const name = 'test-debounce-signal';
10
+ /**
11
+ * @type {import("jest-mock").Mock<import("jest-mock").UnknownFunction>}
12
+ */
13
+ let mockFunc;
14
+
15
+ beforeEach(() => {
16
+ mockFunc = jest.fn();
17
+ jest.useFakeTimers();
18
+ sourceSignal = new StateSignal({name, initialValue: 0});
19
+ });
20
+
21
+ afterEach(() => {
22
+ jest.clearAllTimers();
23
+ jest.useRealTimers();
24
+ sourceSignal?.destroy();
25
+ debouncedSignal?.destroy();
26
+ });
27
+
28
+ it('should create a debounced signal with default config', () => {
29
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
30
+ expect(debouncedSignal.get()).toBe(0);
31
+ expect(debouncedSignal.name).toBe(`${name}-debounced`);
32
+ });
33
+
34
+ it('should debounce updates with trailing edge', async () => {
35
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
36
+ await jest.advanceTimersByTimeAsync(1);
37
+ sourceSignal.set(1);
38
+ sourceSignal.set(2);
39
+ expect(debouncedSignal.get()).toBe(0);
40
+ await jest.advanceTimersByTimeAsync(110);
41
+ expect(debouncedSignal.get()).toBe(2);
42
+ });
43
+
44
+ it('should support leading edge', async () => {
45
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100, leading: true});
46
+ await jest.advanceTimersByTimeAsync(1);
47
+ sourceSignal.set(1);
48
+ await jest.advanceTimersByTimeAsync(1);
49
+ expect(debouncedSignal.get()).toBe(1);
50
+ sourceSignal.set(2);
51
+ await jest.advanceTimersByTimeAsync(10);
52
+ expect(debouncedSignal.get()).toBe(1);
53
+ await jest.advanceTimersByTimeAsync(100);
54
+ expect(debouncedSignal.get()).toBe(2);
55
+ });
56
+
57
+ it('should support trailing edge', async () => {
58
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100, trailing: true});
59
+ await jest.advanceTimersByTimeAsync(1);
60
+ sourceSignal.set(1);
61
+ sourceSignal.set(2);
62
+ await jest.advanceTimersByTimeAsync(1);
63
+ expect(debouncedSignal.get()).toBe(0);
64
+ await jest.advanceTimersByTimeAsync(100);
65
+ expect(debouncedSignal.get()).toBe(2);
66
+ });
67
+
68
+ it('should cancel debounced updates on destroy', async () => {
69
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
70
+ await jest.advanceTimersByTimeAsync(1);
71
+ sourceSignal.set(1);
72
+ await jest.advanceTimersByTimeAsync(1);
73
+ debouncedSignal.destroy();
74
+ expect(() => debouncedSignal.get()).toThrow(); // Should throw on access after
75
+ });
76
+
77
+ it('should call onDestroy callback if provided', () => {
78
+ const onDestroyMock = jest.fn();
79
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100, onDestroy: onDestroyMock});
80
+ debouncedSignal.destroy();
81
+ expect(onDestroyMock).toHaveBeenCalled();
82
+ });
83
+
84
+ it('should use custom name if provided', () => {
85
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100, name: 'custom-debounced'});
86
+ expect(debouncedSignal.name).toBe('custom-debounced');
87
+ });
88
+
89
+ it('should handle multiple rapid updates correctly', async () => {
90
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
91
+ await jest.advanceTimersByTimeAsync(1);
92
+ sourceSignal.set(1);
93
+ await jest.advanceTimersByTimeAsync(50);
94
+ sourceSignal.set(2);
95
+ await jest.advanceTimersByTimeAsync(50);
96
+ sourceSignal.set(3);
97
+ expect(debouncedSignal.get()).toBe(0);
98
+ await jest.advanceTimersByTimeAsync(110);
99
+ expect(debouncedSignal.get()).toBe(3);
100
+ });
101
+
102
+ it('should notify subscriber with receivePrevious', async () => {
103
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
104
+ const callback = jest.fn();
105
+ debouncedSignal.subscribe(callback);
106
+ await jest.advanceTimersByTimeAsync(1);
107
+ expect(callback).toHaveBeenCalledTimes(1);
108
+ expect(callback).toHaveBeenCalledWith(0);
109
+ });
110
+
111
+ it('should notify subscribers when debounced value changes', async () => {
112
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
113
+ const callback = jest.fn();
114
+ debouncedSignal.subscribe(callback, {receivePrevious: false});
115
+ sourceSignal.set(1);
116
+ await jest.advanceTimersByTimeAsync(110);
117
+ expect(callback).toHaveBeenCalledTimes(1);
118
+ expect(callback).toHaveBeenCalledWith(1);
119
+ });
120
+
121
+ it('should not notify if debounced value does not change', async () => {
122
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
123
+ const callback = jest.fn();
124
+ debouncedSignal.subscribe(callback, {receivePrevious: false});
125
+ sourceSignal.set(0); // Same as initial
126
+ await jest.advanceTimersByTimeAsync(110);
127
+ expect(callback).not.toHaveBeenCalled();
128
+ });
129
+
130
+ it('should notify multiple subscribers', async () => {
131
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
132
+ const callback1 = jest.fn();
133
+ const callback2 = jest.fn();
134
+ debouncedSignal.subscribe(callback1, {receivePrevious: false});
135
+ debouncedSignal.subscribe(callback2, {receivePrevious: false});
136
+ sourceSignal.set(5);
137
+ await jest.advanceTimersByTimeAsync(110);
138
+ expect(callback1).toHaveBeenCalledTimes(1);
139
+ expect(callback1).toHaveBeenCalledWith(5);
140
+ expect(callback2).toHaveBeenCalledTimes(1);
141
+ expect(callback2).toHaveBeenCalledWith(5);
142
+ });
143
+
144
+ it('should not notify unsubscribed listeners', async () => {
145
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
146
+ const callback = jest.fn();
147
+ const subscription = debouncedSignal.subscribe(callback, {receivePrevious: false});
148
+ subscription.unsubscribe();
149
+ sourceSignal.set(10);
150
+ await jest.advanceTimersByTimeAsync(110);
151
+ expect(callback).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it('should handle subscriptions with the "once" option', async () => {
155
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
156
+ const callback = jest.fn();
157
+ debouncedSignal.subscribe(callback, {once: true, receivePrevious: false});
158
+ sourceSignal.set(10);
159
+ await jest.advanceTimersByTimeAsync(110);
160
+ expect(callback).toHaveBeenCalledTimes(1);
161
+ expect(callback).toHaveBeenCalledWith(10);
162
+ sourceSignal.set(20);
163
+ await jest.advanceTimersByTimeAsync(110);
164
+ expect(callback).toHaveBeenCalledTimes(1); // Should not be called again
165
+ });
166
+
167
+ it('should resolve untilNext() with the next debounced value', async () => {
168
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
169
+ const untilNextPromise = debouncedSignal.untilNext();
170
+ sourceSignal.set(5);
171
+ await jest.advanceTimersByTimeAsync(110);
172
+ await expect(untilNextPromise).resolves.toBe(5);
173
+ });
174
+
175
+ it('should continue notifying other subscribers if one callback throws an error', async () => {
176
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
177
+ const callback1 = jest.fn(() => {
178
+ throw new Error('Test error');
179
+ });
180
+ const callback2 = jest.fn();
181
+ debouncedSignal.subscribe(callback1, {receivePrevious: false});
182
+ debouncedSignal.subscribe(callback2, {receivePrevious: false});
183
+ sourceSignal.set(10);
184
+ await jest.advanceTimersByTimeAsync(110);
185
+ expect(callback1).toHaveBeenCalledTimes(1);
186
+ expect(callback2).toHaveBeenCalledTimes(1);
187
+ });
188
+
189
+ it('should update without any subscribers', async () => {
190
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
191
+ expect(debouncedSignal.get()).toBe(0);
192
+ sourceSignal.set(7);
193
+ await jest.advanceTimersByTimeAsync(110);
194
+ expect(debouncedSignal.get()).toBe(7);
195
+ });
196
+
197
+ it('should not notify after destroy', async () => {
198
+ debouncedSignal = createDebouncedSignal(sourceSignal, {delay: 100});
199
+ const callback = jest.fn();
200
+ debouncedSignal.subscribe(callback, {receivePrevious: false});
201
+ debouncedSignal.destroy();
202
+ sourceSignal.set(10);
203
+ await jest.advanceTimersByTimeAsync(110);
204
+ expect(callback).not.toHaveBeenCalled();
205
+ });
206
+ });