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