@zeix/cause-effect 0.16.1 → 0.17.1
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/.ai-context.md +85 -21
- package/.cursorrules +11 -5
- package/.github/copilot-instructions.md +64 -13
- package/CLAUDE.md +143 -163
- package/LICENSE +1 -1
- package/README.md +248 -333
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +21 -21
- package/archive/list.ts +551 -0
- package/archive/memo.ts +139 -0
- package/{src → archive}/state.ts +13 -11
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +938 -509
- package/index.js +1 -1
- package/index.ts +50 -23
- package/package.json +1 -1
- package/src/classes/collection.ts +282 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +305 -0
- package/src/classes/ref.ts +68 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +26 -53
- package/src/effect.ts +9 -9
- package/src/errors.ts +71 -25
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -34
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +853 -0
- package/test/computed.test.ts +138 -130
- package/test/diff.test.ts +2 -2
- package/test/effect.test.ts +36 -35
- package/test/list.test.ts +754 -0
- package/test/match.test.ts +25 -25
- package/test/ref.test.ts +227 -0
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +70 -119
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +253 -929
- package/types/index.d.ts +12 -9
- package/types/src/classes/collection.d.ts +46 -0
- package/types/src/classes/composite.d.ts +15 -0
- package/types/src/classes/computed.d.ts +97 -0
- package/types/src/classes/list.d.ts +41 -0
- package/types/src/classes/ref.d.ts +39 -0
- package/types/src/classes/state.d.ts +52 -0
- package/types/src/classes/store.d.ts +51 -0
- package/types/src/diff.d.ts +8 -12
- package/types/src/errors.d.ts +17 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -4
- package/src/store.ts +0 -474
- package/types/src/collection.d.ts +0 -26
- package/types/src/computed.d.ts +0 -33
- package/types/src/scheduler.d.ts +0 -55
- package/types/src/state.d.ts +0 -24
- package/types/src/store.d.ts +0 -65
package/test/match.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { Memo, match, resolve, State, Task, UNSET } from '../index.ts'
|
|
3
3
|
|
|
4
4
|
/* === Tests === */
|
|
5
5
|
|
|
6
6
|
describe('Match Function', () => {
|
|
7
7
|
test('should call ok handler for successful resolution', () => {
|
|
8
|
-
const a =
|
|
9
|
-
const b =
|
|
8
|
+
const a = new State(10)
|
|
9
|
+
const b = new State('hello')
|
|
10
10
|
let okCalled = false
|
|
11
11
|
let okValues: { a: number; b: string } | null = null
|
|
12
12
|
|
|
@@ -32,8 +32,8 @@ describe('Match Function', () => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
test('should call nil handler for pending signals', () => {
|
|
35
|
-
const a =
|
|
36
|
-
const b =
|
|
35
|
+
const a = new State(10)
|
|
36
|
+
const b = new State(UNSET)
|
|
37
37
|
let nilCalled = false
|
|
38
38
|
|
|
39
39
|
match(resolve({ a, b }), {
|
|
@@ -52,8 +52,8 @@ describe('Match Function', () => {
|
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
test('should call error handler for error signals', () => {
|
|
55
|
-
const a =
|
|
56
|
-
const b =
|
|
55
|
+
const a = new State(10)
|
|
56
|
+
const b = new Memo(() => {
|
|
57
57
|
throw new Error('Test error')
|
|
58
58
|
})
|
|
59
59
|
let errCalled = false
|
|
@@ -78,7 +78,7 @@ describe('Match Function', () => {
|
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
test('should handle missing optional handlers gracefully', () => {
|
|
81
|
-
const a =
|
|
81
|
+
const a = new State(10)
|
|
82
82
|
const result = resolve({ a })
|
|
83
83
|
|
|
84
84
|
// Should not throw even with only required ok handler (err and nil are optional)
|
|
@@ -92,7 +92,7 @@ describe('Match Function', () => {
|
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
test('should return void always', () => {
|
|
95
|
-
const a =
|
|
95
|
+
const a = new State(42)
|
|
96
96
|
|
|
97
97
|
const returnValue = match(resolve({ a }), {
|
|
98
98
|
ok: () => {
|
|
@@ -105,7 +105,7 @@ describe('Match Function', () => {
|
|
|
105
105
|
})
|
|
106
106
|
|
|
107
107
|
test('should handle handler errors by calling error handler', () => {
|
|
108
|
-
const a =
|
|
108
|
+
const a = new State(10)
|
|
109
109
|
let handlerErrorCalled = false
|
|
110
110
|
let handlerError: Error | null = null
|
|
111
111
|
|
|
@@ -125,7 +125,7 @@ describe('Match Function', () => {
|
|
|
125
125
|
})
|
|
126
126
|
|
|
127
127
|
test('should rethrow handler errors if no error handler available', () => {
|
|
128
|
-
const a =
|
|
128
|
+
const a = new State(10)
|
|
129
129
|
|
|
130
130
|
expect(() => {
|
|
131
131
|
match(resolve({ a }), {
|
|
@@ -137,7 +137,7 @@ describe('Match Function', () => {
|
|
|
137
137
|
})
|
|
138
138
|
|
|
139
139
|
test('should combine existing errors with handler errors', () => {
|
|
140
|
-
const a =
|
|
140
|
+
const a = new Memo(() => {
|
|
141
141
|
throw new Error('Signal error')
|
|
142
142
|
})
|
|
143
143
|
let allErrors: readonly Error[] | null = null
|
|
@@ -167,9 +167,9 @@ describe('Match Function', () => {
|
|
|
167
167
|
})
|
|
168
168
|
|
|
169
169
|
test('should work with complex type inference', () => {
|
|
170
|
-
const user =
|
|
171
|
-
const posts =
|
|
172
|
-
const settings =
|
|
170
|
+
const user = new State({ id: 1, name: 'Alice' })
|
|
171
|
+
const posts = new State([{ id: 1, title: 'Hello' }])
|
|
172
|
+
const settings = new State({ theme: 'dark' })
|
|
173
173
|
|
|
174
174
|
let typeTestPassed = false
|
|
175
175
|
|
|
@@ -194,8 +194,8 @@ describe('Match Function', () => {
|
|
|
194
194
|
})
|
|
195
195
|
|
|
196
196
|
test('should handle side effects only pattern', () => {
|
|
197
|
-
const count =
|
|
198
|
-
const name =
|
|
197
|
+
const count = new State(5)
|
|
198
|
+
const name = new State('test')
|
|
199
199
|
let sideEffectExecuted = false
|
|
200
200
|
let capturedData = ''
|
|
201
201
|
|
|
@@ -214,10 +214,10 @@ describe('Match Function', () => {
|
|
|
214
214
|
})
|
|
215
215
|
|
|
216
216
|
test('should handle multiple error types correctly', () => {
|
|
217
|
-
const error1 =
|
|
217
|
+
const error1 = new Memo(() => {
|
|
218
218
|
throw new Error('First error')
|
|
219
219
|
})
|
|
220
|
-
const error2 =
|
|
220
|
+
const error2 = new Memo(() => {
|
|
221
221
|
throw new Error('Second error')
|
|
222
222
|
})
|
|
223
223
|
let errorMessages: string[] = []
|
|
@@ -240,7 +240,7 @@ describe('Match Function', () => {
|
|
|
240
240
|
const wait = (ms: number) =>
|
|
241
241
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
242
242
|
|
|
243
|
-
const asyncSignal =
|
|
243
|
+
const asyncSignal = new Task(async () => {
|
|
244
244
|
await wait(10)
|
|
245
245
|
return 'async result'
|
|
246
246
|
})
|
|
@@ -280,7 +280,7 @@ describe('Match Function', () => {
|
|
|
280
280
|
})
|
|
281
281
|
|
|
282
282
|
test('should maintain referential transparency', () => {
|
|
283
|
-
const a =
|
|
283
|
+
const a = new State(42)
|
|
284
284
|
const result = resolve({ a })
|
|
285
285
|
let callCount = 0
|
|
286
286
|
|
|
@@ -303,7 +303,7 @@ describe('Match Function', () => {
|
|
|
303
303
|
|
|
304
304
|
describe('Match Function Integration', () => {
|
|
305
305
|
test('should work seamlessly with resolve', () => {
|
|
306
|
-
const data =
|
|
306
|
+
const data = new State({ id: 1, value: 'test' })
|
|
307
307
|
let processed = false
|
|
308
308
|
let processedValue = ''
|
|
309
309
|
|
|
@@ -322,12 +322,12 @@ describe('Match Function Integration', () => {
|
|
|
322
322
|
const wait = (ms: number) =>
|
|
323
323
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
324
324
|
|
|
325
|
-
const syncData =
|
|
326
|
-
const asyncData =
|
|
325
|
+
const syncData = new State('available')
|
|
326
|
+
const asyncData = new Task(async () => {
|
|
327
327
|
await wait(10)
|
|
328
328
|
return 'loaded'
|
|
329
329
|
})
|
|
330
|
-
const errorData =
|
|
330
|
+
const errorData = new Memo(() => {
|
|
331
331
|
throw new Error('Failed to load')
|
|
332
332
|
})
|
|
333
333
|
|
package/test/ref.test.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { expect, mock, test } from 'bun:test'
|
|
2
|
+
import { isRef, Ref } from '../src/classes/ref'
|
|
3
|
+
import { createEffect } from '../src/effect'
|
|
4
|
+
|
|
5
|
+
test('Ref - basic functionality', () => {
|
|
6
|
+
const obj = { name: 'test', value: 42 }
|
|
7
|
+
const ref = new Ref(obj)
|
|
8
|
+
|
|
9
|
+
expect(ref.get()).toBe(obj)
|
|
10
|
+
expect(ref[Symbol.toStringTag]).toBe('Ref')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('Ref - isRef type guard', () => {
|
|
14
|
+
const ref = new Ref({ test: true })
|
|
15
|
+
const notRef = { test: true }
|
|
16
|
+
|
|
17
|
+
expect(isRef(ref)).toBe(true)
|
|
18
|
+
expect(isRef(notRef)).toBe(false)
|
|
19
|
+
expect(isRef(null)).toBe(false)
|
|
20
|
+
expect(isRef(undefined)).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('Ref - validation with guard function', () => {
|
|
24
|
+
const isConfig = (
|
|
25
|
+
value: unknown,
|
|
26
|
+
): value is { host: string; port: number } =>
|
|
27
|
+
typeof value === 'object' &&
|
|
28
|
+
value !== null &&
|
|
29
|
+
'host' in value &&
|
|
30
|
+
'port' in value &&
|
|
31
|
+
typeof value.host === 'string' &&
|
|
32
|
+
typeof value.port === 'number'
|
|
33
|
+
|
|
34
|
+
const validConfig = { host: 'localhost', port: 3000 }
|
|
35
|
+
const invalidConfig = { host: 'localhost' } // missing port
|
|
36
|
+
|
|
37
|
+
expect(() => new Ref(validConfig, isConfig)).not.toThrow()
|
|
38
|
+
expect(() => new Ref(invalidConfig, isConfig)).toThrow()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('Ref - reactive subscriptions', () => {
|
|
42
|
+
const server = { status: 'offline', connections: 0 }
|
|
43
|
+
const ref = new Ref(server)
|
|
44
|
+
|
|
45
|
+
let effectRunCount = 0
|
|
46
|
+
let lastStatus: string = ''
|
|
47
|
+
|
|
48
|
+
createEffect(() => {
|
|
49
|
+
const current = ref.get()
|
|
50
|
+
lastStatus = current.status
|
|
51
|
+
effectRunCount++
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
expect(effectRunCount).toBe(1)
|
|
55
|
+
expect(lastStatus).toBe('offline')
|
|
56
|
+
|
|
57
|
+
// Simulate external change without going through reactive system
|
|
58
|
+
server.status = 'online'
|
|
59
|
+
server.connections = 5
|
|
60
|
+
|
|
61
|
+
// Effect shouldn't re-run yet (reference hasn't changed)
|
|
62
|
+
expect(effectRunCount).toBe(1)
|
|
63
|
+
|
|
64
|
+
// Notify that the external object has changed
|
|
65
|
+
ref.notify()
|
|
66
|
+
|
|
67
|
+
expect(effectRunCount).toBe(2)
|
|
68
|
+
expect(lastStatus).toBe('online')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('Ref - notify triggers watchers even with same reference', () => {
|
|
72
|
+
const fileObj = { path: '/test.txt', size: 100, modified: Date.now() }
|
|
73
|
+
const ref = new Ref(fileObj)
|
|
74
|
+
|
|
75
|
+
const mockCallback = mock(() => {})
|
|
76
|
+
|
|
77
|
+
createEffect(() => {
|
|
78
|
+
ref.get()
|
|
79
|
+
mockCallback()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(mockCallback).toHaveBeenCalledTimes(1)
|
|
83
|
+
|
|
84
|
+
// Simulate file modification (same object reference, different content)
|
|
85
|
+
fileObj.size = 200
|
|
86
|
+
fileObj.modified = Date.now()
|
|
87
|
+
|
|
88
|
+
// Notify about external change
|
|
89
|
+
ref.notify()
|
|
90
|
+
|
|
91
|
+
expect(mockCallback).toHaveBeenCalledTimes(2)
|
|
92
|
+
|
|
93
|
+
// Multiple notifies should trigger multiple times
|
|
94
|
+
ref.notify()
|
|
95
|
+
expect(mockCallback).toHaveBeenCalledTimes(3)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('Ref - multiple effects with same ref', () => {
|
|
99
|
+
const database = { connected: false, queries: 0 }
|
|
100
|
+
const ref = new Ref(database)
|
|
101
|
+
|
|
102
|
+
const effect1Mock = mock(() => {})
|
|
103
|
+
const effect2Mock = mock((_connected: boolean) => {})
|
|
104
|
+
|
|
105
|
+
createEffect(() => {
|
|
106
|
+
ref.get()
|
|
107
|
+
effect1Mock()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
createEffect(() => {
|
|
111
|
+
const db = ref.get()
|
|
112
|
+
effect2Mock(db.connected)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(effect1Mock).toHaveBeenCalledTimes(1)
|
|
116
|
+
expect(effect2Mock).toHaveBeenCalledTimes(1)
|
|
117
|
+
expect(effect2Mock).toHaveBeenCalledWith(false)
|
|
118
|
+
|
|
119
|
+
// Simulate database connection change
|
|
120
|
+
database.connected = true
|
|
121
|
+
database.queries = 10
|
|
122
|
+
ref.notify()
|
|
123
|
+
|
|
124
|
+
expect(effect1Mock).toHaveBeenCalledTimes(2)
|
|
125
|
+
expect(effect2Mock).toHaveBeenCalledTimes(2)
|
|
126
|
+
expect(effect2Mock).toHaveBeenLastCalledWith(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('Ref - with Bun.file() scenario', () => {
|
|
130
|
+
// Mock a file-like object that could change externally
|
|
131
|
+
const fileRef = {
|
|
132
|
+
name: 'config.json',
|
|
133
|
+
size: 1024,
|
|
134
|
+
lastModified: Date.now(),
|
|
135
|
+
// Simulate file methods
|
|
136
|
+
exists: () => true,
|
|
137
|
+
text: () => Promise.resolve('{"version": "1.0"}'),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ref = new Ref(fileRef)
|
|
141
|
+
|
|
142
|
+
let sizeChanges = 0
|
|
143
|
+
createEffect(() => {
|
|
144
|
+
const file = ref.get()
|
|
145
|
+
if (file.size > 1000) sizeChanges++
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(sizeChanges).toBe(1) // Initial run
|
|
149
|
+
|
|
150
|
+
// Simulate file growing (external change)
|
|
151
|
+
fileRef.size = 2048
|
|
152
|
+
fileRef.lastModified = Date.now()
|
|
153
|
+
ref.notify()
|
|
154
|
+
|
|
155
|
+
expect(sizeChanges).toBe(2) // Effect re-ran and condition still met
|
|
156
|
+
|
|
157
|
+
// Simulate file shrinking
|
|
158
|
+
fileRef.size = 500
|
|
159
|
+
ref.notify()
|
|
160
|
+
|
|
161
|
+
expect(sizeChanges).toBe(2) // Effect re-ran but condition no longer met
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('Ref - validation errors', () => {
|
|
165
|
+
// @ts-expect-error deliberatly provoked error
|
|
166
|
+
expect(() => new Ref(null)).toThrow()
|
|
167
|
+
// @ts-expect-error deliberatly provoked error
|
|
168
|
+
expect(() => new Ref(undefined)).toThrow()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('Ref - server config object scenario', () => {
|
|
172
|
+
const config = {
|
|
173
|
+
host: 'localhost',
|
|
174
|
+
port: 3000,
|
|
175
|
+
ssl: false,
|
|
176
|
+
maxConnections: 100,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const configRef = new Ref(config)
|
|
180
|
+
const connectionAttempts: string[] = []
|
|
181
|
+
|
|
182
|
+
createEffect(() => {
|
|
183
|
+
const cfg = configRef.get()
|
|
184
|
+
const protocol = cfg.ssl ? 'https' : 'http'
|
|
185
|
+
connectionAttempts.push(`${protocol}://${cfg.host}:${cfg.port}`)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
expect(connectionAttempts).toEqual(['http://localhost:3000'])
|
|
189
|
+
|
|
190
|
+
// Simulate config reload from file/environment
|
|
191
|
+
config.ssl = true
|
|
192
|
+
config.port = 8443
|
|
193
|
+
configRef.notify()
|
|
194
|
+
|
|
195
|
+
expect(connectionAttempts).toEqual([
|
|
196
|
+
'http://localhost:3000',
|
|
197
|
+
'https://localhost:8443',
|
|
198
|
+
])
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('Ref - handles complex nested objects', () => {
|
|
202
|
+
const apiResponse = {
|
|
203
|
+
status: 200,
|
|
204
|
+
data: {
|
|
205
|
+
users: [{ id: 1, name: 'Alice' }],
|
|
206
|
+
pagination: { page: 1, total: 1 },
|
|
207
|
+
},
|
|
208
|
+
headers: { 'content-type': 'application/json' },
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ref = new Ref(apiResponse)
|
|
212
|
+
let userCount = 0
|
|
213
|
+
|
|
214
|
+
createEffect(() => {
|
|
215
|
+
const response = ref.get()
|
|
216
|
+
userCount = response.data.users.length
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
expect(userCount).toBe(1)
|
|
220
|
+
|
|
221
|
+
// Simulate API response update
|
|
222
|
+
apiResponse.data.users.push({ id: 2, name: 'Bob' })
|
|
223
|
+
apiResponse.data.pagination.total = 2
|
|
224
|
+
ref.notify()
|
|
225
|
+
|
|
226
|
+
expect(userCount).toBe(2)
|
|
227
|
+
})
|
package/test/resolve.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { Memo, resolve, State, Task, UNSET } from '../index.ts'
|
|
3
3
|
|
|
4
4
|
/* === Tests === */
|
|
5
5
|
|
|
6
6
|
describe('Resolve Function', () => {
|
|
7
7
|
test('should return discriminated union for successful resolution', () => {
|
|
8
|
-
const a =
|
|
9
|
-
const b =
|
|
8
|
+
const a = new State(10)
|
|
9
|
+
const b = new State('hello')
|
|
10
10
|
|
|
11
11
|
const result = resolve({ a, b })
|
|
12
12
|
|
|
@@ -20,8 +20,8 @@ describe('Resolve Function', () => {
|
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
test('should return discriminated union for pending signals', () => {
|
|
23
|
-
const a =
|
|
24
|
-
const b =
|
|
23
|
+
const a = new State(10)
|
|
24
|
+
const b = new State(UNSET)
|
|
25
25
|
|
|
26
26
|
const result = resolve({ a, b })
|
|
27
27
|
|
|
@@ -32,8 +32,8 @@ describe('Resolve Function', () => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
test('should return discriminated union for error signals', () => {
|
|
35
|
-
const a =
|
|
36
|
-
const b =
|
|
35
|
+
const a = new State(10)
|
|
36
|
+
const b = new Memo(() => {
|
|
37
37
|
throw new Error('Test error')
|
|
38
38
|
})
|
|
39
39
|
|
|
@@ -49,11 +49,11 @@ describe('Resolve Function', () => {
|
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
test('should handle mixed error and valid signals', () => {
|
|
52
|
-
const valid =
|
|
53
|
-
const error1 =
|
|
52
|
+
const valid = new State('valid')
|
|
53
|
+
const error1 = new Memo(() => {
|
|
54
54
|
throw new Error('Error 1')
|
|
55
55
|
})
|
|
56
|
-
const error2 =
|
|
56
|
+
const error2 = new Memo(() => {
|
|
57
57
|
throw new Error('Error 2')
|
|
58
58
|
})
|
|
59
59
|
|
|
@@ -69,8 +69,8 @@ describe('Resolve Function', () => {
|
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
test('should prioritize pending over errors', () => {
|
|
72
|
-
const pending =
|
|
73
|
-
const error =
|
|
72
|
+
const pending = new State(UNSET)
|
|
73
|
+
const error = new Memo(() => {
|
|
74
74
|
throw new Error('Test error')
|
|
75
75
|
})
|
|
76
76
|
|
|
@@ -91,8 +91,8 @@ describe('Resolve Function', () => {
|
|
|
91
91
|
})
|
|
92
92
|
|
|
93
93
|
test('should handle complex nested object signals', () => {
|
|
94
|
-
const user =
|
|
95
|
-
const settings =
|
|
94
|
+
const user = new State({ name: 'Alice', age: 25 })
|
|
95
|
+
const settings = new State({ theme: 'dark', lang: 'en' })
|
|
96
96
|
|
|
97
97
|
const result = resolve({ user, settings })
|
|
98
98
|
|
|
@@ -109,7 +109,7 @@ describe('Resolve Function', () => {
|
|
|
109
109
|
const wait = (ms: number) =>
|
|
110
110
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
111
111
|
|
|
112
|
-
const asyncSignal =
|
|
112
|
+
const asyncSignal = new Task(async () => {
|
|
113
113
|
await wait(10)
|
|
114
114
|
return 'async result'
|
|
115
115
|
})
|
|
@@ -124,16 +124,14 @@ describe('Resolve Function', () => {
|
|
|
124
124
|
|
|
125
125
|
result = resolve({ asyncSignal })
|
|
126
126
|
expect(result.ok).toBe(true)
|
|
127
|
-
if (result.ok)
|
|
128
|
-
expect(result.values.asyncSignal).toBe('async result')
|
|
129
|
-
}
|
|
127
|
+
if (result.ok) expect(result.values.asyncSignal).toBe('async result')
|
|
130
128
|
})
|
|
131
129
|
|
|
132
130
|
test('should handle async computed signals that error', async () => {
|
|
133
131
|
const wait = (ms: number) =>
|
|
134
132
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
135
133
|
|
|
136
|
-
const asyncError =
|
|
134
|
+
const asyncError = new Task(async () => {
|
|
137
135
|
await wait(10)
|
|
138
136
|
throw new Error('Async error')
|
|
139
137
|
})
|