@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.
- package/CHANGELOG.md +41 -0
- package/README.md +26 -14
- package/dist/core/computed-signal.d.ts +4 -4
- package/dist/core/computed-signal.d.ts.map +1 -1
- package/dist/core/effect-signal.d.ts +4 -4
- package/dist/core/effect-signal.d.ts.map +1 -1
- package/dist/core/event-signal.d.ts +2 -2
- package/dist/core/event-signal.d.ts.map +1 -1
- package/dist/core/signal-base.d.ts +1 -1
- package/dist/core/signal-base.d.ts.map +1 -1
- package/dist/core/state-signal.d.ts +2 -1
- package/dist/core/state-signal.d.ts.map +1 -1
- package/dist/creators/computed.d.ts +3 -3
- package/dist/creators/effect.d.ts +2 -2
- package/dist/creators/event.d.ts +1 -1
- package/dist/creators/state.d.ts +1 -1
- package/dist/main.cjs +2 -621
- package/dist/main.cjs.map +2 -2
- package/dist/main.mjs +2 -586
- package/dist/main.mjs.map +2 -2
- package/dist/operators/debounce.d.ts +1 -1
- package/dist/operators/filter.d.ts +3 -3
- package/dist/operators/filter.d.ts.map +1 -1
- package/dist/operators/map.d.ts +3 -3
- package/dist/operators/map.d.ts.map +1 -1
- package/dist/type.d.ts +6 -6
- package/dist/type.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/core/computed-signal.test.js +23 -7
- package/src/core/effect-signal.test.js +2 -2
- package/src/core/event-signal.test.js +25 -9
- package/src/core/state-signal.test.js +29 -14
- package/src/operators/debounce.test.js +206 -0
|
@@ -5,23 +5,37 @@ import {delay} from '@alwatr/delay';
|
|
|
5
5
|
describe('StateSignal', () => {
|
|
6
6
|
/** @type {StateSignal<number>} */
|
|
7
7
|
let signal;
|
|
8
|
-
const
|
|
8
|
+
const name = 'test-state-signal';
|
|
9
9
|
|
|
10
10
|
beforeEach(() => {
|
|
11
|
-
signal = new StateSignal({
|
|
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
|
|
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.
|
|
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({
|
|
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(
|
|
197
|
-
|
|
210
|
+
signal.set(5);
|
|
211
|
+
signal.set(10);
|
|
198
212
|
await delay.nextMacrotask();
|
|
199
|
-
expect(errorCallback).toHaveBeenCalledTimes(
|
|
200
|
-
expect(normalCallback).toHaveBeenCalledTimes(
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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({
|
|
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
|
+
});
|