@zeix/cause-effect 0.17.2 → 0.18.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/.ai-context.md +163 -226
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/.zed/settings.json +3 -0
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +197 -202
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +241 -220
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1326 -1174
- package/index.js +1 -1
- package/index.ts +58 -85
- package/package.json +9 -6
- package/src/errors.ts +118 -70
- package/src/graph.ts +601 -0
- package/src/nodes/collection.ts +474 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +588 -0
- package/src/nodes/memo.ts +120 -0
- package/src/nodes/sensor.ts +139 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +383 -0
- package/src/nodes/task.ts +146 -0
- package/src/signal.ts +112 -64
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -69
- package/test/benchmark.test.ts +473 -485
- package/test/collection.test.ts +455 -955
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +332 -857
- package/test/memo.test.ts +380 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -271
- package/test/store.test.ts +346 -898
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -19
- package/types/src/graph.d.ts +208 -0
- package/types/src/nodes/collection.d.ts +64 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +65 -0
- package/types/src/nodes/memo.d.ts +57 -0
- package/types/src/nodes/sensor.d.ts +75 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +73 -0
- package/types/src/signal.d.ts +43 -28
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -688
- package/archive/collection.ts +0 -310
- package/archive/computed.ts +0 -198
- package/archive/list.ts +0 -544
- package/archive/memo.ts +0 -140
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -357
- package/archive/task.ts +0 -191
- package/src/classes/collection.ts +0 -298
- package/src/classes/composite.ts +0 -171
- package/src/classes/computed.ts +0 -392
- package/src/classes/list.ts +0 -310
- package/src/classes/ref.ts +0 -96
- package/src/classes/state.ts +0 -131
- package/src/classes/store.ts +0 -227
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -96
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -275
- package/test/computed.test.ts +0 -1126
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -381
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -47
- package/types/src/classes/composite.d.ts +0 -15
- package/types/src/classes/computed.d.ts +0 -114
- package/types/src/classes/list.d.ts +0 -41
- package/types/src/classes/ref.d.ts +0 -48
- package/types/src/classes/state.d.ts +0 -61
- package/types/src/classes/store.d.ts +0 -51
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -81
package/test/ref.test.ts
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { expect, mock, test } from 'bun:test'
|
|
2
|
-
import { isRef, Ref } from '../src/classes/ref'
|
|
3
|
-
import { createEffect } from '../src/effect'
|
|
4
|
-
import { HOOK_WATCH } from '../src/system'
|
|
5
|
-
|
|
6
|
-
test('Ref - basic functionality', () => {
|
|
7
|
-
const obj = { name: 'test', value: 42 }
|
|
8
|
-
const ref = new Ref(obj)
|
|
9
|
-
|
|
10
|
-
expect(ref.get()).toBe(obj)
|
|
11
|
-
expect(ref[Symbol.toStringTag]).toBe('Ref')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test('Ref - isRef type guard', () => {
|
|
15
|
-
const ref = new Ref({ test: true })
|
|
16
|
-
const notRef = { test: true }
|
|
17
|
-
|
|
18
|
-
expect(isRef(ref)).toBe(true)
|
|
19
|
-
expect(isRef(notRef)).toBe(false)
|
|
20
|
-
expect(isRef(null)).toBe(false)
|
|
21
|
-
expect(isRef(undefined)).toBe(false)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('Ref - validation with guard function', () => {
|
|
25
|
-
const isConfig = (
|
|
26
|
-
value: unknown,
|
|
27
|
-
): value is { host: string; port: number } =>
|
|
28
|
-
typeof value === 'object' &&
|
|
29
|
-
value !== null &&
|
|
30
|
-
'host' in value &&
|
|
31
|
-
'port' in value &&
|
|
32
|
-
typeof value.host === 'string' &&
|
|
33
|
-
typeof value.port === 'number'
|
|
34
|
-
|
|
35
|
-
const validConfig = { host: 'localhost', port: 3000 }
|
|
36
|
-
const invalidConfig = { host: 'localhost' } // missing port
|
|
37
|
-
|
|
38
|
-
expect(() => new Ref(validConfig, isConfig)).not.toThrow()
|
|
39
|
-
expect(() => new Ref(invalidConfig, isConfig)).toThrow()
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('Ref - reactive subscriptions', () => {
|
|
43
|
-
const server = { status: 'offline', connections: 0 }
|
|
44
|
-
const ref = new Ref(server)
|
|
45
|
-
|
|
46
|
-
let effectRunCount = 0
|
|
47
|
-
let lastStatus: string = ''
|
|
48
|
-
|
|
49
|
-
createEffect(() => {
|
|
50
|
-
const current = ref.get()
|
|
51
|
-
lastStatus = current.status
|
|
52
|
-
effectRunCount++
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
expect(effectRunCount).toBe(1)
|
|
56
|
-
expect(lastStatus).toBe('offline')
|
|
57
|
-
|
|
58
|
-
// Simulate external change without going through reactive system
|
|
59
|
-
server.status = 'online'
|
|
60
|
-
server.connections = 5
|
|
61
|
-
|
|
62
|
-
// Effect shouldn't re-run yet (reference hasn't changed)
|
|
63
|
-
expect(effectRunCount).toBe(1)
|
|
64
|
-
|
|
65
|
-
// Notify that the external object has changed
|
|
66
|
-
ref.notify()
|
|
67
|
-
|
|
68
|
-
expect(effectRunCount).toBe(2)
|
|
69
|
-
expect(lastStatus).toBe('online')
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('Ref - notify triggers watchers even with same reference', () => {
|
|
73
|
-
const fileObj = { path: '/test.txt', size: 100, modified: Date.now() }
|
|
74
|
-
const ref = new Ref(fileObj)
|
|
75
|
-
|
|
76
|
-
const mockCallback = mock(() => {})
|
|
77
|
-
|
|
78
|
-
createEffect(() => {
|
|
79
|
-
ref.get()
|
|
80
|
-
mockCallback()
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
expect(mockCallback).toHaveBeenCalledTimes(1)
|
|
84
|
-
|
|
85
|
-
// Simulate file modification (same object reference, different content)
|
|
86
|
-
fileObj.size = 200
|
|
87
|
-
fileObj.modified = Date.now()
|
|
88
|
-
|
|
89
|
-
// Notify about external change
|
|
90
|
-
ref.notify()
|
|
91
|
-
|
|
92
|
-
expect(mockCallback).toHaveBeenCalledTimes(2)
|
|
93
|
-
|
|
94
|
-
// Multiple notifies should trigger multiple times
|
|
95
|
-
ref.notify()
|
|
96
|
-
expect(mockCallback).toHaveBeenCalledTimes(3)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('Ref - multiple effects with same ref', () => {
|
|
100
|
-
const database = { connected: false, queries: 0 }
|
|
101
|
-
const ref = new Ref(database)
|
|
102
|
-
|
|
103
|
-
const effect1Mock = mock(() => {})
|
|
104
|
-
const effect2Mock = mock((_connected: boolean) => {})
|
|
105
|
-
|
|
106
|
-
createEffect(() => {
|
|
107
|
-
ref.get()
|
|
108
|
-
effect1Mock()
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
createEffect(() => {
|
|
112
|
-
const db = ref.get()
|
|
113
|
-
effect2Mock(db.connected)
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
expect(effect1Mock).toHaveBeenCalledTimes(1)
|
|
117
|
-
expect(effect2Mock).toHaveBeenCalledTimes(1)
|
|
118
|
-
expect(effect2Mock).toHaveBeenCalledWith(false)
|
|
119
|
-
|
|
120
|
-
// Simulate database connection change
|
|
121
|
-
database.connected = true
|
|
122
|
-
database.queries = 10
|
|
123
|
-
ref.notify()
|
|
124
|
-
|
|
125
|
-
expect(effect1Mock).toHaveBeenCalledTimes(2)
|
|
126
|
-
expect(effect2Mock).toHaveBeenCalledTimes(2)
|
|
127
|
-
expect(effect2Mock).toHaveBeenLastCalledWith(true)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test('Ref - with Bun.file() scenario', () => {
|
|
131
|
-
// Mock a file-like object that could change externally
|
|
132
|
-
const fileRef = {
|
|
133
|
-
name: 'config.json',
|
|
134
|
-
size: 1024,
|
|
135
|
-
lastModified: Date.now(),
|
|
136
|
-
// Simulate file methods
|
|
137
|
-
exists: () => true,
|
|
138
|
-
text: () => Promise.resolve('{"version": "1.0"}'),
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const ref = new Ref(fileRef)
|
|
142
|
-
|
|
143
|
-
let sizeChanges = 0
|
|
144
|
-
createEffect(() => {
|
|
145
|
-
const file = ref.get()
|
|
146
|
-
if (file.size > 1000) sizeChanges++
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
expect(sizeChanges).toBe(1) // Initial run
|
|
150
|
-
|
|
151
|
-
// Simulate file growing (external change)
|
|
152
|
-
fileRef.size = 2048
|
|
153
|
-
fileRef.lastModified = Date.now()
|
|
154
|
-
ref.notify()
|
|
155
|
-
|
|
156
|
-
expect(sizeChanges).toBe(2) // Effect re-ran and condition still met
|
|
157
|
-
|
|
158
|
-
// Simulate file shrinking
|
|
159
|
-
fileRef.size = 500
|
|
160
|
-
ref.notify()
|
|
161
|
-
|
|
162
|
-
expect(sizeChanges).toBe(2) // Effect re-ran but condition no longer met
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
test('Ref - validation errors', () => {
|
|
166
|
-
// @ts-expect-error deliberatly provoked error
|
|
167
|
-
expect(() => new Ref(null)).toThrow()
|
|
168
|
-
// @ts-expect-error deliberatly provoked error
|
|
169
|
-
expect(() => new Ref(undefined)).toThrow()
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
test('Ref - server config object scenario', () => {
|
|
173
|
-
const config = {
|
|
174
|
-
host: 'localhost',
|
|
175
|
-
port: 3000,
|
|
176
|
-
ssl: false,
|
|
177
|
-
maxConnections: 100,
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const configRef = new Ref(config)
|
|
181
|
-
const connectionAttempts: string[] = []
|
|
182
|
-
|
|
183
|
-
createEffect(() => {
|
|
184
|
-
const cfg = configRef.get()
|
|
185
|
-
const protocol = cfg.ssl ? 'https' : 'http'
|
|
186
|
-
connectionAttempts.push(`${protocol}://${cfg.host}:${cfg.port}`)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
expect(connectionAttempts).toEqual(['http://localhost:3000'])
|
|
190
|
-
|
|
191
|
-
// Simulate config reload from file/environment
|
|
192
|
-
config.ssl = true
|
|
193
|
-
config.port = 8443
|
|
194
|
-
configRef.notify()
|
|
195
|
-
|
|
196
|
-
expect(connectionAttempts).toEqual([
|
|
197
|
-
'http://localhost:3000',
|
|
198
|
-
'https://localhost:8443',
|
|
199
|
-
])
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
test('Ref - handles complex nested objects', () => {
|
|
203
|
-
const apiResponse = {
|
|
204
|
-
status: 200,
|
|
205
|
-
data: {
|
|
206
|
-
users: [{ id: 1, name: 'Alice' }],
|
|
207
|
-
pagination: { page: 1, total: 1 },
|
|
208
|
-
},
|
|
209
|
-
headers: { 'content-type': 'application/json' },
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const ref = new Ref(apiResponse)
|
|
213
|
-
let userCount = 0
|
|
214
|
-
|
|
215
|
-
createEffect(() => {
|
|
216
|
-
const response = ref.get()
|
|
217
|
-
userCount = response.data.users.length
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
expect(userCount).toBe(1)
|
|
221
|
-
|
|
222
|
-
// Simulate API response update
|
|
223
|
-
apiResponse.data.users.push({ id: 2, name: 'Bob' })
|
|
224
|
-
apiResponse.data.pagination.total = 2
|
|
225
|
-
ref.notify()
|
|
226
|
-
|
|
227
|
-
expect(userCount).toBe(2)
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
test('Ref - HOOK_WATCH lazy resource management', async () => {
|
|
231
|
-
// 1. Create Ref with current Date
|
|
232
|
-
const currentDate = new Date()
|
|
233
|
-
const ref = new Ref(currentDate)
|
|
234
|
-
|
|
235
|
-
let counter = 0
|
|
236
|
-
let intervalId: Timer | undefined
|
|
237
|
-
|
|
238
|
-
// 2. Add HOOK_WATCH callback that starts setInterval and returns cleanup
|
|
239
|
-
const cleanupHookCallback = ref.on(HOOK_WATCH, () => {
|
|
240
|
-
intervalId = setInterval(() => {
|
|
241
|
-
counter++
|
|
242
|
-
}, 10) // Use short interval for faster test
|
|
243
|
-
|
|
244
|
-
// Return cleanup function to clear interval
|
|
245
|
-
return () => {
|
|
246
|
-
if (intervalId) {
|
|
247
|
-
clearInterval(intervalId)
|
|
248
|
-
intervalId = undefined
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
// 3. Counter should not be running yet
|
|
254
|
-
expect(counter).toBe(0)
|
|
255
|
-
|
|
256
|
-
// Wait a bit to ensure counter doesn't increment
|
|
257
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
258
|
-
expect(counter).toBe(0)
|
|
259
|
-
expect(intervalId).toBeUndefined()
|
|
260
|
-
|
|
261
|
-
// 4. Effect subscribes by .get()ting the signal value
|
|
262
|
-
const effectCleanup = createEffect(() => {
|
|
263
|
-
ref.get()
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
// 5. Counter should now be running
|
|
267
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
268
|
-
expect(counter).toBeGreaterThan(0)
|
|
269
|
-
expect(intervalId).toBeDefined()
|
|
270
|
-
|
|
271
|
-
// 6. Call effect cleanup, which should stop internal watcher and unsubscribe
|
|
272
|
-
effectCleanup()
|
|
273
|
-
const counterAfterStop = counter
|
|
274
|
-
|
|
275
|
-
// 7. Ref signal should call #unwatch() and counter should stop incrementing
|
|
276
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
277
|
-
expect(counter).toBe(counterAfterStop) // Counter should not have incremented
|
|
278
|
-
expect(intervalId).toBeUndefined() // Interval should be cleared
|
|
279
|
-
|
|
280
|
-
// Clean up hook callback registration
|
|
281
|
-
cleanupHookCallback()
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
test('Ref - HOOK_WATCH exception handling', async () => {
|
|
285
|
-
const ref = new Ref({ test: 'value' })
|
|
286
|
-
|
|
287
|
-
// Mock console.error to capture error logs
|
|
288
|
-
const originalError = console.error
|
|
289
|
-
const errorSpy = mock(() => {})
|
|
290
|
-
console.error = errorSpy
|
|
291
|
-
|
|
292
|
-
let successfulCallbackCalled = false
|
|
293
|
-
let throwingCallbackCalled = false
|
|
294
|
-
|
|
295
|
-
// Add callback that throws an exception
|
|
296
|
-
const cleanup1 = ref.on(HOOK_WATCH, () => {
|
|
297
|
-
throwingCallbackCalled = true
|
|
298
|
-
throw new Error('Test error in HOOK_WATCH callback')
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
// Add callback that works normally
|
|
302
|
-
const cleanup2 = ref.on(HOOK_WATCH, () => {
|
|
303
|
-
successfulCallbackCalled = true
|
|
304
|
-
return () => {
|
|
305
|
-
// cleanup function
|
|
306
|
-
}
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
// Subscribe to trigger HOOK_WATCH callbacks
|
|
310
|
-
const effectCleanup = createEffect(() => {
|
|
311
|
-
ref.get()
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
// Both callbacks should have been called despite the exception
|
|
315
|
-
expect(throwingCallbackCalled).toBe(true)
|
|
316
|
-
expect(successfulCallbackCalled).toBe(true)
|
|
317
|
-
|
|
318
|
-
// Error should have been logged
|
|
319
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
320
|
-
'Error in effect callback:',
|
|
321
|
-
expect.any(Error),
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
// Cleanup
|
|
325
|
-
effectCleanup()
|
|
326
|
-
cleanup1()
|
|
327
|
-
cleanup2()
|
|
328
|
-
console.error = originalError
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
test('Ref - cleanup function exception handling', async () => {
|
|
332
|
-
const ref = new Ref({ test: 'value' })
|
|
333
|
-
|
|
334
|
-
// Mock console.error to capture error logs
|
|
335
|
-
const originalError = console.error
|
|
336
|
-
const errorSpy = mock(() => {})
|
|
337
|
-
console.error = errorSpy
|
|
338
|
-
|
|
339
|
-
let cleanup1Called = false
|
|
340
|
-
let cleanup2Called = false
|
|
341
|
-
|
|
342
|
-
// Add callbacks with cleanup functions, one throws
|
|
343
|
-
const hookCleanup1 = ref.on(HOOK_WATCH, () => {
|
|
344
|
-
return () => {
|
|
345
|
-
cleanup1Called = true
|
|
346
|
-
throw new Error('Test error in cleanup function')
|
|
347
|
-
}
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
const hookCleanup2 = ref.on(HOOK_WATCH, () => {
|
|
351
|
-
return () => {
|
|
352
|
-
cleanup2Called = true
|
|
353
|
-
}
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
// Subscribe and then unsubscribe to trigger cleanup
|
|
357
|
-
const effectCleanup = createEffect(() => {
|
|
358
|
-
ref.get()
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
// Unsubscribe to trigger cleanup functions
|
|
362
|
-
effectCleanup()
|
|
363
|
-
|
|
364
|
-
// Wait a bit for cleanup to complete
|
|
365
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
366
|
-
|
|
367
|
-
// Both cleanup functions should have been called despite the exception
|
|
368
|
-
expect(cleanup1Called).toBe(true)
|
|
369
|
-
expect(cleanup2Called).toBe(true)
|
|
370
|
-
|
|
371
|
-
// Error should have been logged
|
|
372
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
373
|
-
'Error in effect cleanup:',
|
|
374
|
-
expect.any(Error),
|
|
375
|
-
)
|
|
376
|
-
|
|
377
|
-
// Cleanup
|
|
378
|
-
hookCleanup1()
|
|
379
|
-
hookCleanup2()
|
|
380
|
-
console.error = originalError
|
|
381
|
-
})
|
package/test/resolve.test.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { Memo, resolve, State, Task, UNSET } from '../index.ts'
|
|
3
|
-
|
|
4
|
-
/* === Tests === */
|
|
5
|
-
|
|
6
|
-
describe('Resolve Function', () => {
|
|
7
|
-
test('should return discriminated union for successful resolution', () => {
|
|
8
|
-
const a = new State(10)
|
|
9
|
-
const b = new State('hello')
|
|
10
|
-
|
|
11
|
-
const result = resolve({ a, b })
|
|
12
|
-
|
|
13
|
-
expect(result.ok).toBe(true)
|
|
14
|
-
if (result.ok) {
|
|
15
|
-
expect(result.values.a).toBe(10)
|
|
16
|
-
expect(result.values.b).toBe('hello')
|
|
17
|
-
expect(result.errors).toBeUndefined()
|
|
18
|
-
expect(result.pending).toBeUndefined()
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('should return discriminated union for pending signals', () => {
|
|
23
|
-
const a = new State(10)
|
|
24
|
-
const b = new State(UNSET)
|
|
25
|
-
|
|
26
|
-
const result = resolve({ a, b })
|
|
27
|
-
|
|
28
|
-
expect(result.ok).toBe(false)
|
|
29
|
-
expect(result.pending).toBe(true)
|
|
30
|
-
expect(result.values).toBeUndefined()
|
|
31
|
-
expect(result.errors).toBeUndefined()
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('should return discriminated union for error signals', () => {
|
|
35
|
-
const a = new State(10)
|
|
36
|
-
const b = new Memo(() => {
|
|
37
|
-
throw new Error('Test error')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const result = resolve({ a, b })
|
|
41
|
-
|
|
42
|
-
expect(result.ok).toBe(false)
|
|
43
|
-
expect(result.pending).toBeUndefined()
|
|
44
|
-
expect(result.values).toBeUndefined()
|
|
45
|
-
expect(result.errors).toBeDefined()
|
|
46
|
-
if (result.errors) {
|
|
47
|
-
expect(result.errors[0].message).toBe('Test error')
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('should handle mixed error and valid signals', () => {
|
|
52
|
-
const valid = new State('valid')
|
|
53
|
-
const error1 = new Memo(() => {
|
|
54
|
-
throw new Error('Error 1')
|
|
55
|
-
})
|
|
56
|
-
const error2 = new Memo(() => {
|
|
57
|
-
throw new Error('Error 2')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
const result = resolve({ valid, error1, error2 })
|
|
61
|
-
|
|
62
|
-
expect(result.ok).toBe(false)
|
|
63
|
-
expect(result.errors).toBeDefined()
|
|
64
|
-
if (result.errors) {
|
|
65
|
-
expect(result.errors).toHaveLength(2)
|
|
66
|
-
expect(result.errors[0].message).toBe('Error 1')
|
|
67
|
-
expect(result.errors[1].message).toBe('Error 2')
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('should prioritize pending over errors', () => {
|
|
72
|
-
const pending = new State(UNSET)
|
|
73
|
-
const error = new Memo(() => {
|
|
74
|
-
throw new Error('Test error')
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
const result = resolve({ pending, error })
|
|
78
|
-
|
|
79
|
-
expect(result.ok).toBe(false)
|
|
80
|
-
expect(result.pending).toBe(true)
|
|
81
|
-
expect(result.errors).toBeUndefined()
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test('should handle empty signals object', () => {
|
|
85
|
-
const result = resolve({})
|
|
86
|
-
|
|
87
|
-
expect(result.ok).toBe(true)
|
|
88
|
-
if (result.ok) {
|
|
89
|
-
expect(result.values).toEqual({})
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
test('should handle complex nested object signals', () => {
|
|
94
|
-
const user = new State({ name: 'Alice', age: 25 })
|
|
95
|
-
const settings = new State({ theme: 'dark', lang: 'en' })
|
|
96
|
-
|
|
97
|
-
const result = resolve({ user, settings })
|
|
98
|
-
|
|
99
|
-
expect(result.ok).toBe(true)
|
|
100
|
-
if (result.ok) {
|
|
101
|
-
expect(result.values.user.name).toBe('Alice')
|
|
102
|
-
expect(result.values.user.age).toBe(25)
|
|
103
|
-
expect(result.values.settings.theme).toBe('dark')
|
|
104
|
-
expect(result.values.settings.lang).toBe('en')
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test('should handle async computed signals that resolve', async () => {
|
|
109
|
-
const wait = (ms: number) =>
|
|
110
|
-
new Promise(resolve => setTimeout(resolve, ms))
|
|
111
|
-
|
|
112
|
-
const asyncSignal = new Task(async () => {
|
|
113
|
-
await wait(10)
|
|
114
|
-
return 'async result'
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
// Initially should be pending
|
|
118
|
-
let result = resolve({ asyncSignal })
|
|
119
|
-
expect(result.ok).toBe(false)
|
|
120
|
-
expect(result.pending).toBe(true)
|
|
121
|
-
|
|
122
|
-
// Wait for resolution
|
|
123
|
-
await wait(20)
|
|
124
|
-
|
|
125
|
-
result = resolve({ asyncSignal })
|
|
126
|
-
expect(result.ok).toBe(true)
|
|
127
|
-
if (result.ok) expect(result.values.asyncSignal).toBe('async result')
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test('should handle async computed signals that error', async () => {
|
|
131
|
-
const wait = (ms: number) =>
|
|
132
|
-
new Promise(resolve => setTimeout(resolve, ms))
|
|
133
|
-
|
|
134
|
-
const asyncError = new Task(async () => {
|
|
135
|
-
await wait(10)
|
|
136
|
-
throw new Error('Async error')
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// Initially should be pending
|
|
140
|
-
let result = resolve({ asyncError })
|
|
141
|
-
expect(result.ok).toBe(false)
|
|
142
|
-
expect(result.pending).toBe(true)
|
|
143
|
-
|
|
144
|
-
// Wait for error
|
|
145
|
-
await wait(20)
|
|
146
|
-
|
|
147
|
-
result = resolve({ asyncError })
|
|
148
|
-
expect(result.ok).toBe(false)
|
|
149
|
-
expect(result.errors).toBeDefined()
|
|
150
|
-
if (result.errors) {
|
|
151
|
-
expect(result.errors[0].message).toBe('Async error')
|
|
152
|
-
}
|
|
153
|
-
})
|
|
154
|
-
})
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { Signal } from '../signal';
|
|
2
|
-
import { type Cleanup, type Hook, type HookCallback } from '../system';
|
|
3
|
-
import { type Computed } from './computed';
|
|
4
|
-
import { type List } from './list';
|
|
5
|
-
type CollectionSource<T extends {}> = List<T> | Collection<T>;
|
|
6
|
-
type CollectionCallback<T extends {}, U extends {}> = ((sourceValue: U) => T) | ((sourceValue: U, abort: AbortSignal) => Promise<T>);
|
|
7
|
-
type Collection<T extends {}> = {
|
|
8
|
-
readonly [Symbol.toStringTag]: 'Collection';
|
|
9
|
-
readonly [Symbol.isConcatSpreadable]: true;
|
|
10
|
-
[Symbol.iterator](): IterableIterator<Signal<T>>;
|
|
11
|
-
keys(): IterableIterator<string>;
|
|
12
|
-
get: () => T[];
|
|
13
|
-
at: (index: number) => Signal<T> | undefined;
|
|
14
|
-
byKey: (key: string) => Signal<T> | undefined;
|
|
15
|
-
keyAt: (index: number) => string | undefined;
|
|
16
|
-
indexOfKey: (key: string) => number | undefined;
|
|
17
|
-
on: <K extends Hook>(type: K, callback: HookCallback) => Cleanup;
|
|
18
|
-
deriveCollection: <R extends {}>(callback: CollectionCallback<R, T>) => DerivedCollection<R, T>;
|
|
19
|
-
readonly length: number;
|
|
20
|
-
};
|
|
21
|
-
declare const TYPE_COLLECTION: "Collection";
|
|
22
|
-
declare class DerivedCollection<T extends {}, U extends {}> implements Collection<T> {
|
|
23
|
-
#private;
|
|
24
|
-
constructor(source: CollectionSource<U> | (() => CollectionSource<U>), callback: CollectionCallback<T, U>);
|
|
25
|
-
get [Symbol.toStringTag](): 'Collection';
|
|
26
|
-
get [Symbol.isConcatSpreadable](): true;
|
|
27
|
-
[Symbol.iterator](): IterableIterator<Computed<T>>;
|
|
28
|
-
keys(): IterableIterator<string>;
|
|
29
|
-
get(): T[];
|
|
30
|
-
at(index: number): Computed<T> | undefined;
|
|
31
|
-
byKey(key: string): Computed<T> | undefined;
|
|
32
|
-
keyAt(index: number): string | undefined;
|
|
33
|
-
indexOfKey(key: string): number;
|
|
34
|
-
on(type: Hook, callback: HookCallback): Cleanup;
|
|
35
|
-
deriveCollection<R extends {}>(callback: (sourceValue: T) => R): DerivedCollection<R, T>;
|
|
36
|
-
deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>): DerivedCollection<R, T>;
|
|
37
|
-
get length(): number;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Check if a value is a collection signal
|
|
41
|
-
*
|
|
42
|
-
* @since 0.17.2
|
|
43
|
-
* @param {unknown} value - Value to check
|
|
44
|
-
* @returns {boolean} - True if value is a collection signal, false otherwise
|
|
45
|
-
*/
|
|
46
|
-
declare const isCollection: <T extends {}>(value: unknown) => value is Collection<T>;
|
|
47
|
-
export { type Collection, type CollectionSource, type CollectionCallback, DerivedCollection, isCollection, TYPE_COLLECTION, };
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { DiffResult, UnknownRecord } from '../diff';
|
|
2
|
-
import type { Signal } from '../signal';
|
|
3
|
-
import { type Cleanup, type HookCallback } from '../system';
|
|
4
|
-
type CompositeHook = 'add' | 'change' | 'remove';
|
|
5
|
-
declare class Composite<T extends UnknownRecord, S extends Signal<T[keyof T] & {}>> {
|
|
6
|
-
#private;
|
|
7
|
-
signals: Map<string, S>;
|
|
8
|
-
constructor(values: T, validate: <K extends keyof T & string>(key: K, value: unknown) => value is T[K] & {}, create: <V extends T[keyof T] & {}>(value: V) => S);
|
|
9
|
-
add<K extends keyof T & string>(key: K, value: T[K]): boolean;
|
|
10
|
-
remove<K extends keyof T & string>(key: K): boolean;
|
|
11
|
-
change(changes: DiffResult, initialRun?: boolean): boolean;
|
|
12
|
-
clear(): boolean;
|
|
13
|
-
on(type: CompositeHook, callback: HookCallback): Cleanup;
|
|
14
|
-
}
|
|
15
|
-
export { Composite, type CompositeHook as CompositeListeners };
|