@zeix/cause-effect 0.17.3 → 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 -232
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +199 -143
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +232 -197
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1325 -997
- package/index.js +1 -1
- package/index.ts +58 -74
- package/package.json +4 -1
- package/src/errors.ts +118 -74
- 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 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +466 -706
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- 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 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- 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 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- 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 -78
package/test/ref.test.ts
DELETED
|
@@ -1,353 +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
|
-
|
|
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, { guard: isConfig })).not.toThrow()
|
|
38
|
-
expect(() => new Ref(invalidConfig, { guard: 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
|
-
})
|
|
228
|
-
|
|
229
|
-
test('Ref - options.watched lazy resource management', async () => {
|
|
230
|
-
// 1. Create Ref with current Date
|
|
231
|
-
let counter = 0
|
|
232
|
-
let intervalId: Timer | undefined
|
|
233
|
-
const ref = new Ref(new Date(), {
|
|
234
|
-
watched: () => {
|
|
235
|
-
intervalId = setInterval(() => {
|
|
236
|
-
counter++
|
|
237
|
-
}, 10)
|
|
238
|
-
},
|
|
239
|
-
unwatched: () => {
|
|
240
|
-
if (intervalId) {
|
|
241
|
-
clearInterval(intervalId)
|
|
242
|
-
intervalId = undefined
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
// 2. Counter should not be running yet
|
|
248
|
-
expect(counter).toBe(0)
|
|
249
|
-
|
|
250
|
-
// Wait a bit to ensure counter doesn't increment
|
|
251
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
252
|
-
expect(counter).toBe(0)
|
|
253
|
-
expect(intervalId).toBeUndefined()
|
|
254
|
-
|
|
255
|
-
// 3. Effect subscribes by .get()ting the signal value
|
|
256
|
-
const effectCleanup = createEffect(() => {
|
|
257
|
-
ref.get()
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
// 4. Counter should now be running
|
|
261
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
262
|
-
expect(counter).toBeGreaterThan(0)
|
|
263
|
-
expect(intervalId).toBeDefined()
|
|
264
|
-
|
|
265
|
-
// 5. Call effect cleanup, which should stop internal watcher and unsubscribe
|
|
266
|
-
effectCleanup()
|
|
267
|
-
const counterAfterStop = counter
|
|
268
|
-
|
|
269
|
-
// 6. Ref signal should call #unwatch() and counter should stop incrementing
|
|
270
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
271
|
-
expect(counter).toBe(counterAfterStop) // Counter should not have incremented
|
|
272
|
-
expect(intervalId).toBeUndefined() // Interval should be cleared
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
test('Ref - options.watched exception handling', async () => {
|
|
276
|
-
const ref = new Ref(
|
|
277
|
-
{ test: 'value' },
|
|
278
|
-
{
|
|
279
|
-
watched: () => {
|
|
280
|
-
throwingCallbackCalled = true
|
|
281
|
-
throw new Error('Test error in watched callback')
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
// Mock console.error to capture error logs
|
|
287
|
-
const originalError = console.error
|
|
288
|
-
const errorSpy = mock(() => {})
|
|
289
|
-
console.error = errorSpy
|
|
290
|
-
|
|
291
|
-
let throwingCallbackCalled = false
|
|
292
|
-
|
|
293
|
-
// Subscribe to trigger watched callback
|
|
294
|
-
const effectCleanup = createEffect(() => {
|
|
295
|
-
ref.get()
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
// Both callbacks should have been called despite the exception
|
|
299
|
-
expect(throwingCallbackCalled).toBe(true)
|
|
300
|
-
|
|
301
|
-
// Error should have been logged
|
|
302
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
303
|
-
'Error in effect callback:',
|
|
304
|
-
expect.any(Error),
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
// Cleanup
|
|
308
|
-
effectCleanup()
|
|
309
|
-
console.error = originalError
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
test('Ref - options.unwatched exception handling', async () => {
|
|
313
|
-
const ref = new Ref(
|
|
314
|
-
{ test: 'value' },
|
|
315
|
-
{
|
|
316
|
-
watched: () => {},
|
|
317
|
-
unwatched: () => {
|
|
318
|
-
cleanup1Called = true
|
|
319
|
-
throw new Error('Test error in cleanup function')
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
// Mock console.error to capture error logs
|
|
325
|
-
const originalError = console.error
|
|
326
|
-
const errorSpy = mock(() => {})
|
|
327
|
-
console.error = errorSpy
|
|
328
|
-
|
|
329
|
-
let cleanup1Called = false
|
|
330
|
-
|
|
331
|
-
// Subscribe and then unsubscribe to trigger cleanup
|
|
332
|
-
const effectCleanup = createEffect(() => {
|
|
333
|
-
ref.get()
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
// Unsubscribe to trigger cleanup functions
|
|
337
|
-
effectCleanup()
|
|
338
|
-
|
|
339
|
-
// Wait a bit for cleanup to complete
|
|
340
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
341
|
-
|
|
342
|
-
// Both cleanup functions should have been called despite the exception
|
|
343
|
-
expect(cleanup1Called).toBe(true)
|
|
344
|
-
|
|
345
|
-
// Error should have been logged
|
|
346
|
-
expect(errorSpy).toHaveBeenCalledWith(
|
|
347
|
-
'Error in effect cleanup:',
|
|
348
|
-
expect.any(Error),
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
// Cleanup
|
|
352
|
-
console.error = originalError
|
|
353
|
-
})
|
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,45 +0,0 @@
|
|
|
1
|
-
import type { Signal } from '../signal';
|
|
2
|
-
import { type SignalOptions } 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
|
-
deriveCollection: <R extends {}>(callback: CollectionCallback<R, T>) => DerivedCollection<R, T>;
|
|
18
|
-
readonly length: number;
|
|
19
|
-
};
|
|
20
|
-
declare const TYPE_COLLECTION: "Collection";
|
|
21
|
-
declare class DerivedCollection<T extends {}, U extends {}> implements Collection<T> {
|
|
22
|
-
#private;
|
|
23
|
-
constructor(source: CollectionSource<U> | (() => CollectionSource<U>), callback: CollectionCallback<T, U>, options?: SignalOptions<T[]>);
|
|
24
|
-
get [Symbol.toStringTag](): 'Collection';
|
|
25
|
-
get [Symbol.isConcatSpreadable](): true;
|
|
26
|
-
[Symbol.iterator](): IterableIterator<Computed<T>>;
|
|
27
|
-
keys(): IterableIterator<string>;
|
|
28
|
-
get(): T[];
|
|
29
|
-
at(index: number): Computed<T> | undefined;
|
|
30
|
-
byKey(key: string): Computed<T> | undefined;
|
|
31
|
-
keyAt(index: number): string | undefined;
|
|
32
|
-
indexOfKey(key: string): number;
|
|
33
|
-
deriveCollection<R extends {}>(callback: (sourceValue: T) => R, options?: SignalOptions<R[]>): DerivedCollection<R, T>;
|
|
34
|
-
deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>, options?: SignalOptions<R[]>): DerivedCollection<R, T>;
|
|
35
|
-
get length(): number;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Check if a value is a collection signal
|
|
39
|
-
*
|
|
40
|
-
* @since 0.17.2
|
|
41
|
-
* @param {unknown} value - Value to check
|
|
42
|
-
* @returns {boolean} - True if value is a collection signal, false otherwise
|
|
43
|
-
*/
|
|
44
|
-
declare const isCollection: <T extends {}>(value: unknown) => value is Collection<T>;
|
|
45
|
-
export { type Collection, type CollectionSource, type CollectionCallback, DerivedCollection, isCollection, TYPE_COLLECTION, };
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { type SignalOptions } from '../system';
|
|
2
|
-
type Computed<T extends {}> = {
|
|
3
|
-
readonly [Symbol.toStringTag]: 'Computed';
|
|
4
|
-
get(): T;
|
|
5
|
-
};
|
|
6
|
-
type ComputedOptions<T extends {}> = SignalOptions<T> & {
|
|
7
|
-
initialValue?: T;
|
|
8
|
-
};
|
|
9
|
-
type MemoCallback<T extends {} & {
|
|
10
|
-
then?: undefined;
|
|
11
|
-
}> = (oldValue: T) => T;
|
|
12
|
-
type TaskCallback<T extends {} & {
|
|
13
|
-
then?: undefined;
|
|
14
|
-
}> = (oldValue: T, abort: AbortSignal) => Promise<T>;
|
|
15
|
-
declare const TYPE_COMPUTED: "Computed";
|
|
16
|
-
/**
|
|
17
|
-
* Create a new memoized signal for a synchronous function.
|
|
18
|
-
*
|
|
19
|
-
* @since 0.17.0
|
|
20
|
-
* @param {MemoCallback<T>} callback - Callback function to compute the memoized value
|
|
21
|
-
* @param {T} [initialValue = UNSET] - Initial value of the signal
|
|
22
|
-
* @throws {InvalidCallbackError} If the callback is not an sync function
|
|
23
|
-
* @throws {InvalidSignalValueError} If the initial value is not valid
|
|
24
|
-
*/
|
|
25
|
-
declare class Memo<T extends {}> {
|
|
26
|
-
#private;
|
|
27
|
-
constructor(callback: MemoCallback<T>, options?: ComputedOptions<T>);
|
|
28
|
-
get [Symbol.toStringTag](): 'Computed';
|
|
29
|
-
/**
|
|
30
|
-
* Return the memoized value after computing it if necessary.
|
|
31
|
-
*
|
|
32
|
-
* @returns {T}
|
|
33
|
-
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
34
|
-
* @throws {Error} If an error occurs during computation
|
|
35
|
-
*/
|
|
36
|
-
get(): T;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Create a new task signals that memoizes the result of an asynchronous function.
|
|
40
|
-
*
|
|
41
|
-
* @since 0.17.0
|
|
42
|
-
* @param {TaskCallback<T>} callback - The asynchronous function to compute the memoized value
|
|
43
|
-
* @param {T} [initialValue = UNSET] - Initial value of the signal
|
|
44
|
-
* @throws {InvalidCallbackError} If the callback is not an async function
|
|
45
|
-
* @throws {InvalidSignalValueError} If the initial value is not valid
|
|
46
|
-
*/
|
|
47
|
-
declare class Task<T extends {}> {
|
|
48
|
-
#private;
|
|
49
|
-
constructor(callback: TaskCallback<T>, options?: ComputedOptions<T>);
|
|
50
|
-
get [Symbol.toStringTag](): 'Computed';
|
|
51
|
-
/**
|
|
52
|
-
* Return the memoized value after executing the async function if necessary.
|
|
53
|
-
*
|
|
54
|
-
* @returns {T}
|
|
55
|
-
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
56
|
-
* @throws {Error} If an error occurs during computation
|
|
57
|
-
*/
|
|
58
|
-
get(): T;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Create a derived signal from existing signals
|
|
62
|
-
*
|
|
63
|
-
* @since 0.9.0
|
|
64
|
-
* @param {MemoCallback<T> | TaskCallback<T>} callback - Computation callback function
|
|
65
|
-
* @param {ComputedOptions<T>} options - Optional configuration
|
|
66
|
-
*/
|
|
67
|
-
declare const createComputed: <T extends {}>(callback: TaskCallback<T> | MemoCallback<T>, options?: ComputedOptions<T>) => Task<T> | Memo<T>;
|
|
68
|
-
/**
|
|
69
|
-
* Check if a value is a computed signal
|
|
70
|
-
*
|
|
71
|
-
* @since 0.9.0
|
|
72
|
-
* @param {unknown} value - Value to check
|
|
73
|
-
* @returns {boolean} - True if value is a computed signal, false otherwise
|
|
74
|
-
*/
|
|
75
|
-
declare const isComputed: <T extends {}>(value: unknown) => value is Memo<T>;
|
|
76
|
-
/**
|
|
77
|
-
* Check if the provided value is a callback that may be used as input for createSignal() to derive a computed state
|
|
78
|
-
*
|
|
79
|
-
* @since 0.12.0
|
|
80
|
-
* @param {unknown} value - Value to check
|
|
81
|
-
* @returns {boolean} - True if value is a sync callback, false otherwise
|
|
82
|
-
*/
|
|
83
|
-
declare const isMemoCallback: <T extends {} & {
|
|
84
|
-
then?: undefined;
|
|
85
|
-
}>(value: unknown) => value is MemoCallback<T>;
|
|
86
|
-
/**
|
|
87
|
-
* Check if the provided value is a callback that may be used as input for createSignal() to derive a computed state
|
|
88
|
-
*
|
|
89
|
-
* @since 0.17.0
|
|
90
|
-
* @param {unknown} value - Value to check
|
|
91
|
-
* @returns {boolean} - True if value is an async callback, false otherwise
|
|
92
|
-
*/
|
|
93
|
-
declare const isTaskCallback: <T extends {}>(value: unknown) => value is TaskCallback<T>;
|
|
94
|
-
export { TYPE_COMPUTED, createComputed, isComputed, isMemoCallback, isTaskCallback, Memo, Task, type Computed, type ComputedOptions, type MemoCallback, type TaskCallback, };
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { type UnknownArray } from '../diff';
|
|
2
|
-
import { type SignalOptions } from '../system';
|
|
3
|
-
import { DerivedCollection } from './collection';
|
|
4
|
-
import { State } from './state';
|
|
5
|
-
type ArrayToRecord<T extends UnknownArray> = {
|
|
6
|
-
[key: string]: T extends Array<infer U extends {}> ? U : never;
|
|
7
|
-
};
|
|
8
|
-
type KeyConfig<T> = string | ((item: T) => string);
|
|
9
|
-
type ListOptions<T extends {}> = SignalOptions<T> & {
|
|
10
|
-
keyConfig?: KeyConfig<T>;
|
|
11
|
-
};
|
|
12
|
-
declare const TYPE_LIST: "List";
|
|
13
|
-
declare class List<T extends {}> {
|
|
14
|
-
#private;
|
|
15
|
-
constructor(initialValue: T[], options?: ListOptions<T>);
|
|
16
|
-
get [Symbol.toStringTag](): 'List';
|
|
17
|
-
get [Symbol.isConcatSpreadable](): true;
|
|
18
|
-
[Symbol.iterator](): IterableIterator<State<T>>;
|
|
19
|
-
get length(): number;
|
|
20
|
-
get(): T[];
|
|
21
|
-
set(newValue: T[]): void;
|
|
22
|
-
update(fn: (oldValue: T[]) => T[]): void;
|
|
23
|
-
at(index: number): State<T> | undefined;
|
|
24
|
-
keys(): IterableIterator<string>;
|
|
25
|
-
byKey(key: string): State<T> | undefined;
|
|
26
|
-
keyAt(index: number): string | undefined;
|
|
27
|
-
indexOfKey(key: string): number;
|
|
28
|
-
add(value: T): string;
|
|
29
|
-
remove(keyOrIndex: string | number): void;
|
|
30
|
-
sort(compareFn?: (a: T, b: T) => number): void;
|
|
31
|
-
splice(start: number, deleteCount?: number, ...items: T[]): T[];
|
|
32
|
-
deriveCollection<R extends {}>(callback: (sourceValue: T) => R, options?: SignalOptions<R[]>): DerivedCollection<R, T>;
|
|
33
|
-
deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>, options?: SignalOptions<R[]>): DerivedCollection<R, T>;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Check if the provided value is a List instance
|
|
37
|
-
*
|
|
38
|
-
* @since 0.15.0
|
|
39
|
-
* @param {unknown} value - Value to check
|
|
40
|
-
* @returns {boolean} - True if the value is a List instance, false otherwise
|
|
41
|
-
*/
|
|
42
|
-
declare const isList: <T extends {}>(value: unknown) => value is List<T>;
|
|
43
|
-
export { isList, List, TYPE_LIST, type ArrayToRecord, type KeyConfig, type ListOptions, };
|