@zeix/cause-effect 0.12.4 → 0.13.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.
@@ -10,157 +10,174 @@ const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
10
10
  describe('Effect', function () {
11
11
 
12
12
  test('should be triggered after a state change', function() {
13
- const cause = state('foo');
14
- let count = 0;
15
- effect((_value) => {
16
- count++;
17
- }, cause);
18
- expect(count).toBe(1);
19
- cause.set('bar');
20
- expect(count).toBe(2);
21
- });
13
+ const cause = state('foo')
14
+ let count = 0
15
+ cause.tap(() => {
16
+ count++
17
+ })
18
+ expect(count).toBe(1)
19
+ cause.set('bar')
20
+ expect(count).toBe(2)
21
+ })
22
22
 
23
23
  test('should be triggered after computed async signals resolve without waterfalls', async function() {
24
24
  const a = computed(async () => {
25
- await wait(100);
26
- return 10;
27
- });
25
+ await wait(100)
26
+ return 10
27
+ })
28
28
  const b = computed(async () => {
29
- await wait(100);
30
- return 20;
31
- });
32
- let result = 0;
33
- let count = 0;
34
- effect((aValue, bValue) => {
35
- result = aValue + bValue;
36
- count++;
37
- }, a, b);
38
- expect(result).toBe(0);
39
- expect(count).toBe(0);
40
- await wait(110);
41
- expect(result).toBe(30);
42
- expect(count).toBe(1);
43
- });
29
+ await wait(100)
30
+ return 20
31
+ })
32
+ let result = 0
33
+ let count = 0
34
+ effect({
35
+ signals: [a, b],
36
+ ok: (aValue, bValue) => {
37
+ result = aValue + bValue
38
+ count++
39
+ }
40
+ })
41
+ expect(result).toBe(0)
42
+ expect(count).toBe(0)
43
+ await wait(110)
44
+ expect(result).toBe(30)
45
+ expect(count).toBe(1)
46
+ })
44
47
 
45
48
  test('should be triggered repeatedly after repeated state change', async function() {
46
- const cause = state(0);
47
- let result = 0;
48
- let count = 0;
49
- effect((res) => {
50
- result = res;
51
- count++;
52
- }, cause);
49
+ const cause = state(0)
50
+ let result = 0
51
+ let count = 0
52
+ cause.tap(res => {
53
+ result = res
54
+ count++
55
+ })
53
56
  for (let i = 0; i < 10; i++) {
54
- cause.set(i);
55
- expect(result).toBe(i);
57
+ cause.set(i)
58
+ expect(result).toBe(i)
56
59
  expect(count).toBe(i + 1); // + 1 for effect initialization
57
60
  }
58
- });
61
+ })
59
62
 
60
63
  test('should handle errors in effects', function() {
61
- const a = state(1);
62
- const b = computed(() => {
63
- if (a.get() > 5) throw new Error('Value too high');
64
- return a.get() * 2;
65
- });
66
- let normalCallCount = 0;
67
- let errorCallCount = 0;
68
- effect({
69
- ok: (_bValue) => {
70
- // console.log('Normal effect:', _bValue);
71
- normalCallCount++;
64
+ const a = state(1)
65
+ const b = a.map(v => {
66
+ if (v > 5) throw new Error('Value too high')
67
+ return v * 2
68
+ })
69
+ let normalCallCount = 0
70
+ let errorCallCount = 0
71
+ b.tap({
72
+ ok: () => {
73
+ // console.log('Normal effect:', value)
74
+ normalCallCount++
72
75
  },
73
- err: (error) => {
74
- // console.log('Error effect:', error);
75
- errorCallCount++;
76
- expect(error.message).toBe('Value too high');
76
+ err: error => {
77
+ // console.log('Error effect:', error)
78
+ errorCallCount++
79
+ expect(error.message).toBe('Value too high')
77
80
  }
78
- }, b);
81
+ })
79
82
 
80
83
  // Normal case
81
- a.set(2);
82
- expect(normalCallCount).toBe(2);
83
- expect(errorCallCount).toBe(0);
84
+ a.set(2)
85
+ expect(normalCallCount).toBe(2)
86
+ expect(errorCallCount).toBe(0)
84
87
 
85
88
  // Error case
86
- a.set(6);
87
- expect(normalCallCount).toBe(2);
88
- expect(errorCallCount).toBe(1);
89
+ a.set(6)
90
+ expect(normalCallCount).toBe(2)
91
+ expect(errorCallCount).toBe(1)
89
92
 
90
93
  // Back to normal
91
- a.set(3);
92
- expect(normalCallCount).toBe(3);
93
- expect(errorCallCount).toBe(1);
94
- });
94
+ a.set(3)
95
+ expect(normalCallCount).toBe(3)
96
+ expect(errorCallCount).toBe(1)
97
+ })
95
98
 
96
99
  test('should handle UNSET values in effects', async function() {
97
100
  const a = computed(async () => {
98
- await wait(100);
99
- return 42;
100
- });
101
- let normalCallCount = 0;
102
- let nilCount = 0;
103
- effect({
104
- ok: (aValue) => {
105
- normalCallCount++;
106
- expect(aValue).toBe(42);
101
+ await wait(100)
102
+ return 42
103
+ })
104
+ let normalCallCount = 0
105
+ let nilCount = 0
106
+ a.tap({
107
+ ok: aValue => {
108
+ normalCallCount++
109
+ expect(aValue).toBe(42)
107
110
  },
108
111
  nil: () => {
109
112
  nilCount++
110
113
  }
111
- }, a);
112
-
113
- expect(normalCallCount).toBe(0);
114
- expect(nilCount).toBe(1);
115
- expect(a.get()).toBe(UNSET);
116
- await wait(110);
117
- expect(normalCallCount).toBe(2); // + 1 for effect initialization
118
- expect(a.get()).toBe(42);
119
- });
114
+ })
115
+
116
+ expect(normalCallCount).toBe(0)
117
+ expect(nilCount).toBe(1)
118
+ expect(a.get()).toBe(UNSET)
119
+ await wait(110)
120
+ expect(normalCallCount).toBe(1)
121
+ expect(nilCount).toBe(1)
122
+ expect(a.get()).toBe(42)
123
+ })
120
124
 
121
125
  test('should log error to console when error is not handled', () => {
122
- // Mock console.error
123
- const originalConsoleError = console.error;
124
- const mockConsoleError = mock((message: string, error: Error) => {});
125
- console.error = mockConsoleError;
126
-
127
- try {
128
- const a = state(1);
129
- const b = computed(() => {
130
- if (a.get() > 5) throw new Error('Value too high');
131
- return a.get() * 2;
132
- });
133
-
134
- // Create an effect without explicit error handling
135
- effect(() => {
136
- b.get();
137
- });
138
-
139
- // This should trigger the error
140
- a.set(6);
141
-
142
- // Check if console.error was called with the expected message
143
- expect(mockConsoleError).toHaveBeenCalledWith(
144
- 'Unhandled error in effect:',
145
- expect.any(Error)
146
- );
147
-
148
- // Check the error message
149
- const [, error] = mockConsoleError.mock.calls[0];
150
- expect(error.message).toBe('Value too high');
151
-
152
- } finally {
153
- // Restore the original console.error
154
- console.error = originalConsoleError;
155
- }
156
- });
126
+ // Mock console.error
127
+ const originalConsoleError = console.error
128
+ const mockConsoleError = mock(() => {})
129
+ console.error = mockConsoleError
130
+
131
+ try {
132
+ const a = state(1)
133
+ const b = a.map(v => {
134
+ if (v > 5) throw new Error('Value too high')
135
+ return v * 2
136
+ })
137
+
138
+ // Create an effect without explicit error handling
139
+ b.tap(() => {})
140
+
141
+ // This should trigger the error
142
+ a.set(6)
143
+
144
+ // Check if console.error was called with the error
145
+ expect(mockConsoleError).toHaveBeenCalledWith(
146
+ expect.any(Error)
147
+ )
148
+
149
+ // Check the error message
150
+ const error = (mockConsoleError as ReturnType<typeof mock>).mock.calls[0][0] as Error
151
+ expect(error.message).toBe('Value too high')
152
+
153
+ } finally {
154
+ // Restore the original console.error
155
+ console.error = originalConsoleError
156
+ }
157
+ })
158
+
159
+ test('should clean up subscriptions when disposed', () => {
160
+ const count = state(42)
161
+ let received = 0
162
+
163
+ const cleanup = count.tap(value => {
164
+ received = value
165
+ })
166
+
167
+ count.set(43)
168
+ expect(received).toBe(43)
169
+
170
+ cleanup()
171
+ count.set(44)
172
+ expect(received).toBe(43) // Should not update after dispose
173
+ })
157
174
 
158
175
  test('should detect and throw error for circular dependencies in effects', () => {
159
176
  let okCount = 0
160
177
  let errCount = 0
161
178
  const count = state(0)
162
179
 
163
- effect({
180
+ count.tap({
164
181
  ok: () => {
165
182
  okCount++
166
183
  // This effect updates the signal it depends on, creating a circular dependency
@@ -171,11 +188,11 @@ describe('Effect', function () {
171
188
  expect(e).toBeInstanceOf(Error)
172
189
  expect(e.message).toBe('Circular dependency in effect detected')
173
190
  }
174
- }, count)
191
+ })
175
192
 
176
193
  // Verify that the count was changed only once due to the circular dependency error
177
194
  expect(count.get()).toBe(1)
178
195
  expect(okCount).toBe(1)
179
196
  expect(errCount).toBe(1)
180
197
  })
181
- });
198
+ })
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
- import { state, isComputed, UNSET } from '../'
2
+ import { isComputed, isState, state, UNSET } from '../'
3
3
 
4
4
  /* === Utility Functions === */
5
5
 
@@ -9,207 +9,215 @@ const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
9
9
 
10
10
  describe('State', function () {
11
11
 
12
+ describe("State type guard", () => {
13
+ test("isState identifies state signals", () => {
14
+ const count = state(42)
15
+ expect(isState(count)).toBe(true)
16
+ expect(isComputed(count)).toBe(false)
17
+ })
18
+ })
19
+
12
20
  describe('Boolean cause', function () {
13
21
 
14
22
  test('should be boolean', function () {
15
- const cause = state(false);
16
- expect(typeof cause.get()).toBe('boolean');
17
- });
23
+ const cause = state(false)
24
+ expect(typeof cause.get()).toBe('boolean')
25
+ })
18
26
 
19
27
  test('should set initial value to false', function () {
20
- const cause = state(false);
21
- expect(cause.get()).toBe(false);
22
- });
28
+ const cause = state(false)
29
+ expect(cause.get()).toBe(false)
30
+ })
23
31
 
24
32
  test('should set initial value to true', function () {
25
- const cause = state(true);
26
- expect(cause.get()).toBe(true);
27
- });
33
+ const cause = state(true)
34
+ expect(cause.get()).toBe(true)
35
+ })
28
36
 
29
37
  test('should set new value with .set(true)', function () {
30
- const cause = state(false);
31
- cause.set(true);
32
- expect(cause.get()).toBe(true);
33
- });
38
+ const cause = state(false)
39
+ cause.set(true)
40
+ expect(cause.get()).toBe(true)
41
+ })
34
42
 
35
43
  test('should toggle initial value with .set(v => !v)', function () {
36
- const cause = state(false);
37
- cause.update((v) => !v);
38
- expect(cause.get()).toBe(true);
39
- });
44
+ const cause = state(false)
45
+ cause.update((v) => !v)
46
+ expect(cause.get()).toBe(true)
47
+ })
40
48
 
41
- });
49
+ })
42
50
 
43
51
  describe('Number cause', function () {
44
52
 
45
53
  test('should be number', function () {
46
- const cause = state(0);
47
- expect(typeof cause.get()).toBe('number');
48
- });
54
+ const cause = state(0)
55
+ expect(typeof cause.get()).toBe('number')
56
+ })
49
57
 
50
58
  test('should set initial value to 0', function () {
51
- const cause = state(0);
52
- expect(cause.get()).toBe(0);
53
- });
59
+ const cause = state(0)
60
+ expect(cause.get()).toBe(0)
61
+ })
54
62
 
55
63
  test('should set new value with .set(42)', function () {
56
- const cause = state(0);
57
- cause.set(42);
58
- expect(cause.get()).toBe(42);
59
- });
64
+ const cause = state(0)
65
+ cause.set(42)
66
+ expect(cause.get()).toBe(42)
67
+ })
60
68
 
61
69
  test('should increment value with .set(v => ++v)', function () {
62
- const cause = state(0);
63
- cause.update(v => ++v);
64
- expect(cause.get()).toBe(1);
65
- });
70
+ const cause = state(0)
71
+ cause.update(v => ++v)
72
+ expect(cause.get()).toBe(1)
73
+ })
66
74
 
67
- });
75
+ })
68
76
 
69
77
  describe('String cause', function () {
70
78
 
71
79
  test('should be string', function () {
72
- const cause = state('foo');
73
- expect(typeof cause.get()).toBe('string');
74
- });
80
+ const cause = state('foo')
81
+ expect(typeof cause.get()).toBe('string')
82
+ })
75
83
 
76
84
  test('should set initial value to "foo"', function () {
77
- const cause = state('foo');
78
- expect(cause.get()).toBe('foo');
79
- });
85
+ const cause = state('foo')
86
+ expect(cause.get()).toBe('foo')
87
+ })
80
88
 
81
89
  test('should set new value with .set("bar")', function () {
82
- const cause = state('foo');
83
- cause.set('bar');
84
- expect(cause.get()).toBe('bar');
85
- });
90
+ const cause = state('foo')
91
+ cause.set('bar')
92
+ expect(cause.get()).toBe('bar')
93
+ })
86
94
 
87
95
  test('should upper case value with .set(v => v.toUpperCase())', function () {
88
- const cause = state('foo');
89
- cause.update(v => v ? v.toUpperCase() : '');
90
- expect(cause.get()).toBe("FOO");
91
- });
96
+ const cause = state('foo')
97
+ cause.update(v => v ? v.toUpperCase() : '')
98
+ expect(cause.get()).toBe("FOO")
99
+ })
92
100
 
93
- });
101
+ })
94
102
 
95
103
  describe('Array cause', function () {
96
104
 
97
105
  test('should be array', function () {
98
- const cause = state([1, 2, 3]);
99
- expect(Array.isArray(cause.get())).toBe(true);
100
- });
106
+ const cause = state([1, 2, 3])
107
+ expect(Array.isArray(cause.get())).toBe(true)
108
+ })
101
109
 
102
110
  test('should set initial value to [1, 2, 3]', function () {
103
- const cause = state([1, 2, 3]);
104
- expect(cause.get()).toEqual([1, 2, 3]);
105
- });
111
+ const cause = state([1, 2, 3])
112
+ expect(cause.get()).toEqual([1, 2, 3])
113
+ })
106
114
 
107
115
  test('should set new value with .set([4, 5, 6])', function () {
108
- const cause = state([1, 2, 3]);
109
- cause.set([4, 5, 6]);
110
- expect(cause.get()).toEqual([4, 5, 6]);
111
- });
116
+ const cause = state([1, 2, 3])
117
+ cause.set([4, 5, 6])
118
+ expect(cause.get()).toEqual([4, 5, 6])
119
+ })
112
120
 
113
121
  test('should reflect current value of array after modification', function () {
114
- const array = [1, 2, 3];
115
- const cause = state(array);
122
+ const array = [1, 2, 3]
123
+ const cause = state(array)
116
124
  array.push(4); // don't do this! the result will be correct, but we can't trigger effects
117
- expect(cause.get()).toEqual([1, 2, 3, 4]);
118
- });
125
+ expect(cause.get()).toEqual([1, 2, 3, 4])
126
+ })
119
127
 
120
128
  test('should set new value with .set([...array, 4])', function () {
121
- const array = [1, 2, 3];
122
- const cause = state(array);
129
+ const array = [1, 2, 3]
130
+ const cause = state(array)
123
131
  cause.set([...array, 4]); // use destructuring instead!
124
- expect(cause.get()).toEqual([1, 2, 3, 4]);
125
- });
132
+ expect(cause.get()).toEqual([1, 2, 3, 4])
133
+ })
126
134
 
127
- });
135
+ })
128
136
 
129
137
  describe('Object cause', function () {
130
138
 
131
139
  test('should be object', function () {
132
- const cause = state({ a: 'a', b: 1 });
133
- expect(typeof cause.get()).toBe('object');
134
- });
140
+ const cause = state({ a: 'a', b: 1 })
141
+ expect(typeof cause.get()).toBe('object')
142
+ })
135
143
 
136
144
  test('should set initial value to { a: "a", b: 1 }', function () {
137
- const cause = state({ a: 'a', b: 1 });
138
- expect(cause.get()).toEqual({ a: 'a', b: 1 });
139
- });
145
+ const cause = state({ a: 'a', b: 1 })
146
+ expect(cause.get()).toEqual({ a: 'a', b: 1 })
147
+ })
140
148
 
141
149
  test('should set new value with .set({ c: true })', function () {
142
- const cause = state<Record<string, any>>({ a: 'a', b: 1 });
143
- cause.set({ c: true });
144
- expect(cause.get()).toEqual({ c: true });
145
- });
150
+ const cause = state<Record<string, any>>({ a: 'a', b: 1 })
151
+ cause.set({ c: true })
152
+ expect(cause.get()).toEqual({ c: true })
153
+ })
146
154
 
147
155
  test('should reflect current value of object after modification', function () {
148
- const obj = { a: 'a', b: 1 };
149
- const cause = state<Record<string, any>>(obj);
156
+ const obj = { a: 'a', b: 1 }
157
+ const cause = state<Record<string, any>>(obj)
150
158
  // @ts-expect-error
151
159
  obj.c = true; // don't do this! the result will be correct, but we can't trigger effects
152
- expect(cause.get()).toEqual({ a: 'a', b: 1, c: true });
153
- });
160
+ expect(cause.get()).toEqual({ a: 'a', b: 1, c: true })
161
+ })
154
162
 
155
163
  test('should set new value with .set({...obj, c: true})', function () {
156
- const obj = { a: 'a', b: 1 };
157
- const cause = state<Record<string, any>>(obj);
164
+ const obj = { a: 'a', b: 1 }
165
+ const cause = state<Record<string, any>>(obj)
158
166
  cause.set({...obj, c: true}); // use destructuring instead!
159
- expect(cause.get()).toEqual({ a: 'a', b: 1, c: true });
160
- });
167
+ expect(cause.get()).toEqual({ a: 'a', b: 1, c: true })
168
+ })
161
169
 
162
- });
170
+ })
163
171
 
164
172
  describe('Map method', function () {
165
173
 
166
174
  test('should return a computed signal', function() {
167
- const cause = state(42);
168
- const double = cause.map(v => v * 2);
169
- expect(isComputed(double)).toBe(true);
170
- expect(double.get()).toBe(84);
171
- });
175
+ const cause = state(42)
176
+ const double = cause.map(v => v * 2)
177
+ expect(isComputed(double)).toBe(true)
178
+ expect(double.get()).toBe(84)
179
+ })
172
180
 
173
181
  test('should return a computed signal for an async function', async function() {
174
- const cause = state(42);
175
- const asyncDouble = cause.map(async v => {
176
- await wait(100);
177
- return v * 2;
178
- });
179
- expect(isComputed(asyncDouble)).toBe(true);
180
- expect(asyncDouble.get()).toBe(UNSET);
181
- await wait(110);
182
- expect(asyncDouble.get()).toBe(84);
183
- });
182
+ const cause = state(42)
183
+ const asyncDouble = cause.map(async value => {
184
+ await wait(100)
185
+ return value * 2
186
+ })
187
+ expect(isComputed(asyncDouble)).toBe(true)
188
+ expect(asyncDouble.get()).toBe(UNSET)
189
+ await wait(110)
190
+ expect(asyncDouble.get()).toBe(84)
191
+ })
184
192
 
185
- });
193
+ })
186
194
 
187
- describe('Match method', function () {
195
+ describe('Tap method', function () {
188
196
 
189
197
  test('should create an effect that reacts on signal changes', function() {
190
- const cause = state(42);
191
- let okCount = 0;
192
- let nilCount = 0;
193
- let result = 0;
194
- cause.match({
198
+ const cause = state(42)
199
+ let okCount = 0
200
+ let nilCount = 0
201
+ let result = 0
202
+ cause.tap({
195
203
  ok: v => {
196
- result = v;
204
+ result = v
197
205
  okCount++
198
206
  },
199
207
  nil: () => {
200
208
  nilCount++
201
209
  }
202
210
  })
203
- cause.set(43);
211
+ cause.set(43)
204
212
  expect(okCount).toBe(2); // + 1 for effect initialization
205
- expect(nilCount).toBe(0);
206
- expect(result).toBe(43);
213
+ expect(nilCount).toBe(0)
214
+ expect(result).toBe(43)
207
215
 
208
- cause.set(UNSET);
209
- expect(okCount).toBe(2);
210
- expect(nilCount).toBe(1);
211
- });
216
+ cause.set(UNSET)
217
+ expect(okCount).toBe(2)
218
+ expect(nilCount).toBe(1)
219
+ })
212
220
 
213
- });
221
+ })
214
222
 
215
223
  });