@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
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
createState,
|
|
6
|
+
createTask,
|
|
7
|
+
isMemo,
|
|
8
|
+
isTask,
|
|
9
|
+
UnsetSignalValueError,
|
|
10
|
+
} from '../index.ts'
|
|
11
|
+
|
|
12
|
+
/* === Utility Functions === */
|
|
13
|
+
|
|
14
|
+
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
15
|
+
|
|
16
|
+
/* === Tests === */
|
|
17
|
+
|
|
18
|
+
describe('Task', () => {
|
|
19
|
+
describe('createTask', () => {
|
|
20
|
+
test('should resolve async computation', async () => {
|
|
21
|
+
const task = createTask(
|
|
22
|
+
async () => {
|
|
23
|
+
await wait(50)
|
|
24
|
+
return 42
|
|
25
|
+
},
|
|
26
|
+
{ value: 0 },
|
|
27
|
+
)
|
|
28
|
+
expect(task.get()).toBe(0)
|
|
29
|
+
await wait(60)
|
|
30
|
+
expect(task.get()).toBe(42)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('should have Symbol.toStringTag of "Task"', () => {
|
|
34
|
+
const task = createTask(async () => 1, { value: 0 })
|
|
35
|
+
expect(task[Symbol.toStringTag]).toBe('Task')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('should throw UnsetSignalValueError before resolution with no initial value', () => {
|
|
39
|
+
const task = createTask(async () => {
|
|
40
|
+
await wait(50)
|
|
41
|
+
return 42
|
|
42
|
+
})
|
|
43
|
+
expect(() => task.get()).toThrow(UnsetSignalValueError)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('isTask', () => {
|
|
48
|
+
test('should identify task signals', () => {
|
|
49
|
+
expect(isTask(createTask(async () => 1, { value: 0 }))).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('should return false for non-task values', () => {
|
|
53
|
+
expect(isTask(42)).toBe(false)
|
|
54
|
+
expect(isTask(null)).toBe(false)
|
|
55
|
+
expect(isTask({})).toBe(false)
|
|
56
|
+
expect(isMemo(createTask(async () => 1, { value: 0 }))).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('isPending', () => {
|
|
61
|
+
test('should return true while computation is in-flight', async () => {
|
|
62
|
+
const task = createTask(
|
|
63
|
+
async () => {
|
|
64
|
+
await wait(50)
|
|
65
|
+
return 42
|
|
66
|
+
},
|
|
67
|
+
{ value: 0 },
|
|
68
|
+
)
|
|
69
|
+
task.get() // trigger computation
|
|
70
|
+
expect(task.isPending()).toBe(true)
|
|
71
|
+
await wait(60)
|
|
72
|
+
task.get() // read resolved value
|
|
73
|
+
expect(task.isPending()).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('should return false before first get()', () => {
|
|
77
|
+
const task = createTask(async () => 42, { value: 0 })
|
|
78
|
+
expect(task.isPending()).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('abort', () => {
|
|
83
|
+
test('should abort the current computation', async () => {
|
|
84
|
+
let completed = false
|
|
85
|
+
const task = createTask(
|
|
86
|
+
async (_prev, signal) => {
|
|
87
|
+
await wait(50)
|
|
88
|
+
if (!signal.aborted) completed = true
|
|
89
|
+
return 42
|
|
90
|
+
},
|
|
91
|
+
{ value: 0 },
|
|
92
|
+
)
|
|
93
|
+
task.get() // trigger computation
|
|
94
|
+
expect(task.isPending()).toBe(true)
|
|
95
|
+
task.abort()
|
|
96
|
+
expect(task.isPending()).toBe(false)
|
|
97
|
+
await wait(60)
|
|
98
|
+
expect(completed).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('Dependency Tracking', () => {
|
|
103
|
+
test('should re-execute when dependencies change', async () => {
|
|
104
|
+
const source = createState(1)
|
|
105
|
+
const task = createTask(
|
|
106
|
+
async () => {
|
|
107
|
+
const val = source.get() // dependency tracked before await
|
|
108
|
+
await wait(50)
|
|
109
|
+
return val * 2
|
|
110
|
+
},
|
|
111
|
+
{ value: 0 },
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
let result = 0
|
|
115
|
+
createEffect(() => {
|
|
116
|
+
result = task.get()
|
|
117
|
+
})
|
|
118
|
+
expect(result).toBe(0)
|
|
119
|
+
await wait(60)
|
|
120
|
+
expect(result).toBe(2)
|
|
121
|
+
|
|
122
|
+
source.set(5)
|
|
123
|
+
await wait(60)
|
|
124
|
+
expect(result).toBe(10)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('should work with downstream memos', async () => {
|
|
128
|
+
const status = createState('pending')
|
|
129
|
+
const task = createTask(async () => {
|
|
130
|
+
await wait(50)
|
|
131
|
+
status.set('success')
|
|
132
|
+
return 42
|
|
133
|
+
})
|
|
134
|
+
const derived = createMemo(() => {
|
|
135
|
+
try {
|
|
136
|
+
return task.get() + 1
|
|
137
|
+
} catch {
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
expect(derived.get()).toBe(0)
|
|
142
|
+
expect(status.get()).toBe('pending')
|
|
143
|
+
await wait(60)
|
|
144
|
+
expect(derived.get()).toBe(43)
|
|
145
|
+
expect(status.get()).toBe('success')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('should run tasks in parallel without waterfalls', async () => {
|
|
149
|
+
const a = createTask(
|
|
150
|
+
async () => {
|
|
151
|
+
await wait(80)
|
|
152
|
+
return 10
|
|
153
|
+
},
|
|
154
|
+
{ value: 0 },
|
|
155
|
+
)
|
|
156
|
+
const b = createTask(
|
|
157
|
+
async () => {
|
|
158
|
+
await wait(80)
|
|
159
|
+
return 20
|
|
160
|
+
},
|
|
161
|
+
{ value: 0 },
|
|
162
|
+
)
|
|
163
|
+
const sum = createMemo(() => a.get() + b.get(), { value: 0 })
|
|
164
|
+
expect(sum.get()).toBe(0)
|
|
165
|
+
await wait(90)
|
|
166
|
+
expect(sum.get()).toBe(30)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('AbortSignal', () => {
|
|
171
|
+
test('should signal abort when dependency changes during computation', async () => {
|
|
172
|
+
const source = createState(1)
|
|
173
|
+
let wasAborted = false
|
|
174
|
+
const task = createTask(
|
|
175
|
+
async (_prev, signal) => {
|
|
176
|
+
const val = source.get()
|
|
177
|
+
await wait(100)
|
|
178
|
+
if (signal.aborted) wasAborted = true
|
|
179
|
+
return val
|
|
180
|
+
},
|
|
181
|
+
{ value: 0 },
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
task.get() // start computation
|
|
185
|
+
await wait(10)
|
|
186
|
+
source.set(2) // change dependency mid-flight
|
|
187
|
+
|
|
188
|
+
await wait(110)
|
|
189
|
+
expect(wasAborted).toBe(true)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('should coalesce multiple rapid changes into one recomputation', async () => {
|
|
193
|
+
const source = createState(1)
|
|
194
|
+
let computationCount = 0
|
|
195
|
+
const task = createTask(
|
|
196
|
+
async () => {
|
|
197
|
+
computationCount++
|
|
198
|
+
await wait(100)
|
|
199
|
+
return source.get()
|
|
200
|
+
},
|
|
201
|
+
{ value: 0 },
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
task.get()
|
|
205
|
+
expect(computationCount).toBe(1)
|
|
206
|
+
|
|
207
|
+
source.set(2)
|
|
208
|
+
source.set(3)
|
|
209
|
+
source.set(4)
|
|
210
|
+
await wait(210)
|
|
211
|
+
|
|
212
|
+
expect(task.get()).toBe(4)
|
|
213
|
+
expect(computationCount).toBe(1)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('Error Handling', () => {
|
|
218
|
+
test('should propagate async errors on get()', async () => {
|
|
219
|
+
const task = createTask(
|
|
220
|
+
async () => {
|
|
221
|
+
await wait(50)
|
|
222
|
+
throw new Error('async failure')
|
|
223
|
+
},
|
|
224
|
+
{ value: 0 },
|
|
225
|
+
)
|
|
226
|
+
task.get()
|
|
227
|
+
await wait(60)
|
|
228
|
+
expect(() => task.get()).toThrow('async failure')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('should recover from errors when dependency changes', async () => {
|
|
232
|
+
const source = createState(1)
|
|
233
|
+
const task = createTask(
|
|
234
|
+
async () => {
|
|
235
|
+
const value = source.get()
|
|
236
|
+
await wait(50)
|
|
237
|
+
if (value === 2) throw new Error('bad value')
|
|
238
|
+
return value
|
|
239
|
+
},
|
|
240
|
+
{ value: 0 },
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
task.get()
|
|
244
|
+
await wait(60)
|
|
245
|
+
expect(task.get()).toBe(1)
|
|
246
|
+
|
|
247
|
+
source.set(2)
|
|
248
|
+
task.get()
|
|
249
|
+
await wait(60)
|
|
250
|
+
expect(() => task.get()).toThrow('bad value')
|
|
251
|
+
|
|
252
|
+
source.set(3)
|
|
253
|
+
task.get()
|
|
254
|
+
await wait(60)
|
|
255
|
+
expect(task.get()).toBe(3)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe('options.value (prev)', () => {
|
|
260
|
+
test('should return initial value before resolution', () => {
|
|
261
|
+
const task = createTask(
|
|
262
|
+
async () => {
|
|
263
|
+
await wait(50)
|
|
264
|
+
return 42
|
|
265
|
+
},
|
|
266
|
+
{ value: 10 },
|
|
267
|
+
)
|
|
268
|
+
expect(task.get()).toBe(10)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('should pass initial value as prev to first computation', async () => {
|
|
272
|
+
let receivedPrev: number | undefined
|
|
273
|
+
const task = createTask(
|
|
274
|
+
async prev => {
|
|
275
|
+
receivedPrev = prev
|
|
276
|
+
await wait(50)
|
|
277
|
+
return prev + 5
|
|
278
|
+
},
|
|
279
|
+
{ value: 10 },
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
expect(task.get()).toBe(10)
|
|
283
|
+
await wait(60)
|
|
284
|
+
expect(task.get()).toBe(15)
|
|
285
|
+
expect(receivedPrev).toBe(10)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('should pass previous resolved value on recomputation', async () => {
|
|
289
|
+
const source = createState(1)
|
|
290
|
+
const receivedPrevs: number[] = []
|
|
291
|
+
const task = createTask(
|
|
292
|
+
async prev => {
|
|
293
|
+
const val = source.get() // dependency tracked before await
|
|
294
|
+
receivedPrevs.push(prev)
|
|
295
|
+
await wait(50)
|
|
296
|
+
return val + prev
|
|
297
|
+
},
|
|
298
|
+
{ value: 0 },
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
let result = 0
|
|
302
|
+
createEffect(() => {
|
|
303
|
+
result = task.get()
|
|
304
|
+
})
|
|
305
|
+
await wait(60)
|
|
306
|
+
expect(result).toBe(1) // 0 + 1
|
|
307
|
+
|
|
308
|
+
source.set(2)
|
|
309
|
+
await wait(60)
|
|
310
|
+
expect(result).toBe(3) // 1 + 2
|
|
311
|
+
expect(receivedPrevs).toEqual([0, 1])
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe('options.equals', () => {
|
|
316
|
+
test('should use custom equality to skip propagation after resolution', async () => {
|
|
317
|
+
const source = createState(1)
|
|
318
|
+
let effectCount = 0
|
|
319
|
+
const task = createTask(
|
|
320
|
+
async () => {
|
|
321
|
+
const val = source.get() // dependency tracked before await
|
|
322
|
+
await wait(50)
|
|
323
|
+
return { x: val % 2 }
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
value: { x: -1 },
|
|
327
|
+
equals: (a, b) => a.x === b.x,
|
|
328
|
+
},
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
createEffect(() => {
|
|
332
|
+
task.get()
|
|
333
|
+
effectCount++
|
|
334
|
+
})
|
|
335
|
+
await wait(60) // first resolution: { x: 1 }
|
|
336
|
+
|
|
337
|
+
source.set(3) // still odd — result will be { x: 1 }, structurally equal
|
|
338
|
+
await wait(60)
|
|
339
|
+
const countAfterEqual = effectCount
|
|
340
|
+
|
|
341
|
+
source.set(2) // now even — result will be { x: 0 }, different
|
|
342
|
+
await wait(60)
|
|
343
|
+
|
|
344
|
+
// After the structurally different result resolves, effect should run again
|
|
345
|
+
expect(effectCount).toBeGreaterThan(countAfterEqual)
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
describe('options.guard', () => {
|
|
350
|
+
test('should validate initial value against guard', () => {
|
|
351
|
+
expect(() => {
|
|
352
|
+
createTask(async () => 42, {
|
|
353
|
+
// @ts-expect-error - Testing invalid input
|
|
354
|
+
value: 'foo',
|
|
355
|
+
guard: (v): v is number => typeof v === 'number',
|
|
356
|
+
})
|
|
357
|
+
}).toThrow('[Task] Signal value "foo" is invalid')
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
test('should accept initial value that passes guard', () => {
|
|
361
|
+
const task = createTask(async () => 42, {
|
|
362
|
+
value: 10,
|
|
363
|
+
guard: (v): v is number => typeof v === 'number',
|
|
364
|
+
})
|
|
365
|
+
expect(task.get()).toBe(10)
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
describe('Input Validation', () => {
|
|
370
|
+
test('should throw InvalidCallbackError for sync callback', () => {
|
|
371
|
+
expect(() => {
|
|
372
|
+
// @ts-expect-error - Testing invalid input
|
|
373
|
+
createTask((_a: unknown) => 42)
|
|
374
|
+
}).toThrow('[Task] Callback (_a) => 42 is invalid')
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
test('should throw InvalidCallbackError for non-function callback', () => {
|
|
378
|
+
// @ts-expect-error - Testing invalid input
|
|
379
|
+
expect(() => createTask(null)).toThrow(
|
|
380
|
+
'[Task] Callback null is invalid',
|
|
381
|
+
)
|
|
382
|
+
// @ts-expect-error - Testing invalid input
|
|
383
|
+
expect(() => createTask(42)).toThrow(
|
|
384
|
+
'[Task] Callback 42 is invalid',
|
|
385
|
+
)
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('should throw NullishSignalValueError for null initial value', () => {
|
|
389
|
+
expect(() => {
|
|
390
|
+
// @ts-expect-error - Testing invalid input
|
|
391
|
+
createTask(async () => 42, { value: null })
|
|
392
|
+
}).toThrow('[Task] Signal value cannot be null or undefined')
|
|
393
|
+
})
|
|
394
|
+
})
|
|
395
|
+
})
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
createScope,
|
|
6
|
+
createState,
|
|
7
|
+
untrack,
|
|
8
|
+
} from '../index.ts'
|
|
9
|
+
|
|
10
|
+
/* === Tests === */
|
|
11
|
+
|
|
12
|
+
describe('untrack', () => {
|
|
13
|
+
test('should return the value of the callback', () => {
|
|
14
|
+
const result = untrack(() => 42)
|
|
15
|
+
expect(result).toBe(42)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('should read a signal without tracking it', () => {
|
|
19
|
+
const tracked = createState('tracked')
|
|
20
|
+
const untracked = createState('untracked')
|
|
21
|
+
let count = 0
|
|
22
|
+
createEffect((): undefined => {
|
|
23
|
+
tracked.get()
|
|
24
|
+
untrack(() => untracked.get())
|
|
25
|
+
count++
|
|
26
|
+
})
|
|
27
|
+
expect(count).toBe(1)
|
|
28
|
+
|
|
29
|
+
// changing tracked signal should re-run the effect
|
|
30
|
+
tracked.set('changed')
|
|
31
|
+
expect(count).toBe(2)
|
|
32
|
+
|
|
33
|
+
// changing untracked signal should not re-run the effect
|
|
34
|
+
untracked.set('changed')
|
|
35
|
+
expect(count).toBe(2)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('should not track dependencies in memos', () => {
|
|
39
|
+
const a = createState(1)
|
|
40
|
+
const b = createState(2)
|
|
41
|
+
const sum = createMemo(() => a.get() + untrack(() => b.get()))
|
|
42
|
+
expect(sum.get()).toBe(3)
|
|
43
|
+
|
|
44
|
+
// changing a should recompute
|
|
45
|
+
a.set(10)
|
|
46
|
+
expect(sum.get()).toBe(12)
|
|
47
|
+
|
|
48
|
+
// changing b should not recompute (stale value of b used)
|
|
49
|
+
b.set(20)
|
|
50
|
+
expect(sum.get()).toBe(12)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('should prevent dependency pollution from subcomponent creation', () => {
|
|
54
|
+
const parentSignal = createState('parent')
|
|
55
|
+
let parentRuns = 0
|
|
56
|
+
let childRuns = 0
|
|
57
|
+
|
|
58
|
+
const dispose = createScope(() => {
|
|
59
|
+
createEffect((): undefined => {
|
|
60
|
+
parentSignal.get()
|
|
61
|
+
parentRuns++
|
|
62
|
+
|
|
63
|
+
// Simulate subcomponent: create local state + effect
|
|
64
|
+
// Without untrack, childSignal.get() in the child effect
|
|
65
|
+
// would link to the parent effect during initial run
|
|
66
|
+
untrack(() => {
|
|
67
|
+
const childSignal = createState('child')
|
|
68
|
+
createEffect((): undefined => {
|
|
69
|
+
childSignal.get()
|
|
70
|
+
childRuns++
|
|
71
|
+
})
|
|
72
|
+
childSignal.set('updated')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
expect(parentRuns).toBe(1)
|
|
78
|
+
expect(childRuns).toBe(2) // initial + update
|
|
79
|
+
|
|
80
|
+
// parent should re-run when its own signal changes
|
|
81
|
+
parentSignal.set('changed')
|
|
82
|
+
expect(parentRuns).toBe(2)
|
|
83
|
+
|
|
84
|
+
dispose()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('should prevent parent effect from re-running on child signal changes', () => {
|
|
88
|
+
const show = createState(true)
|
|
89
|
+
let parentRuns = 0
|
|
90
|
+
let childValue = ''
|
|
91
|
+
|
|
92
|
+
const dispose = createScope(() => {
|
|
93
|
+
createEffect((): undefined => {
|
|
94
|
+
parentRuns++
|
|
95
|
+
if (show.get()) {
|
|
96
|
+
// Subcomponent with its own reactive state
|
|
97
|
+
untrack(() => {
|
|
98
|
+
const label = createState('hello')
|
|
99
|
+
createEffect((): undefined => {
|
|
100
|
+
childValue = label.get()
|
|
101
|
+
})
|
|
102
|
+
label.set('world')
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(parentRuns).toBe(1)
|
|
109
|
+
expect(childValue).toBe('world')
|
|
110
|
+
|
|
111
|
+
// toggling show re-runs parent (it's tracked)
|
|
112
|
+
show.set(false)
|
|
113
|
+
expect(parentRuns).toBe(2)
|
|
114
|
+
|
|
115
|
+
dispose()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('should nest correctly', () => {
|
|
119
|
+
const a = createState(1)
|
|
120
|
+
const b = createState(2)
|
|
121
|
+
const c = createState(3)
|
|
122
|
+
let count = 0
|
|
123
|
+
createEffect((): undefined => {
|
|
124
|
+
a.get()
|
|
125
|
+
untrack(() => {
|
|
126
|
+
b.get()
|
|
127
|
+
untrack(() => {
|
|
128
|
+
c.get()
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
count++
|
|
132
|
+
})
|
|
133
|
+
expect(count).toBe(1)
|
|
134
|
+
|
|
135
|
+
a.set(10)
|
|
136
|
+
expect(count).toBe(2)
|
|
137
|
+
|
|
138
|
+
b.set(20)
|
|
139
|
+
expect(count).toBe(2)
|
|
140
|
+
|
|
141
|
+
c.set(30)
|
|
142
|
+
expect(count).toBe(2)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('should restore tracking after untrack completes', () => {
|
|
146
|
+
const before = createState('before')
|
|
147
|
+
const during = createState('during')
|
|
148
|
+
const after = createState('after')
|
|
149
|
+
let count = 0
|
|
150
|
+
createEffect((): undefined => {
|
|
151
|
+
before.get()
|
|
152
|
+
untrack(() => during.get())
|
|
153
|
+
after.get()
|
|
154
|
+
count++
|
|
155
|
+
})
|
|
156
|
+
expect(count).toBe(1)
|
|
157
|
+
|
|
158
|
+
before.set('x')
|
|
159
|
+
expect(count).toBe(2)
|
|
160
|
+
|
|
161
|
+
during.set('x')
|
|
162
|
+
expect(count).toBe(2)
|
|
163
|
+
|
|
164
|
+
after.set('x')
|
|
165
|
+
expect(count).toBe(3)
|
|
166
|
+
})
|
|
167
|
+
})
|
package/types/index.d.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.
|
|
3
|
+
* @version 0.18.0
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
|
-
export {
|
|
7
|
-
export { type
|
|
8
|
-
export { type
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
16
|
-
export { type
|
|
17
|
-
export {
|
|
18
|
-
export { batch, type Cleanup, createWatcher, flush, notifyOf, type SignalOptions, subscribeTo, track, UNSET, untrack, type Watcher, } from './src/system';
|
|
19
|
-
export { isAbortError, isAsyncFunction, isFunction, isNumber, isObjectOfType, isRecord, isRecordOrArray, isString, isSymbol, valueString, } from './src/util';
|
|
6
|
+
export { CircularDependencyError, type Guard, InvalidCallbackError, InvalidSignalValueError, NullishSignalValueError, RequiredOwnerError, UnsetSignalValueError, } from './src/errors';
|
|
7
|
+
export { batch, type Cleanup, type ComputedOptions, createScope, type EffectCallback, type MemoCallback, type Signal, type SignalOptions, SKIP_EQUALITY, type TaskCallback, untrack, } from './src/graph';
|
|
8
|
+
export { type Collection, type CollectionCallback, type CollectionOptions, createCollection, type DeriveCollectionCallback, isCollection, } from './src/nodes/collection';
|
|
9
|
+
export { createEffect, type MatchHandlers, type MaybePromise, match, } from './src/nodes/effect';
|
|
10
|
+
export { createList, type DiffResult, isEqual, isList, type KeyConfig, type List, type ListOptions, } from './src/nodes/list';
|
|
11
|
+
export { createMemo, isMemo, type Memo } from './src/nodes/memo';
|
|
12
|
+
export { createSensor, isSensor, type Sensor, type SensorCallback, } from './src/nodes/sensor';
|
|
13
|
+
export { createState, isState, type State, type UpdateCallback, } from './src/nodes/state';
|
|
14
|
+
export { createStore, isStore, type Store, type StoreOptions, } from './src/nodes/store';
|
|
15
|
+
export { createTask, isTask, type Task } from './src/nodes/task';
|
|
16
|
+
export { createComputed, createMutableSignal, createSignal, isComputed, isMutableSignal, isSignal, type MutableSignal, } from './src/signal';
|
|
17
|
+
export { isAsyncFunction, isFunction, isObjectOfType, isRecord, valueString, } from './src/util';
|
package/types/src/errors.d.ts
CHANGED
|
@@ -1,29 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
type
|
|
1
|
+
/**
|
|
2
|
+
* A type guard function that validates whether an unknown value is of type T.
|
|
3
|
+
* Used to ensure type safety when updating signals.
|
|
4
|
+
*
|
|
5
|
+
* @template T - The type to guard against
|
|
6
|
+
* @param value - The value to check
|
|
7
|
+
* @returns True if the value is of type T
|
|
8
|
+
*/
|
|
9
|
+
type Guard<T extends {}> = (value: unknown) => value is T;
|
|
10
|
+
/**
|
|
11
|
+
* Error thrown on re-entrance on an already running function.
|
|
12
|
+
*/
|
|
3
13
|
declare class CircularDependencyError extends Error {
|
|
14
|
+
/**
|
|
15
|
+
* Constructs a new CircularDependencyError.
|
|
16
|
+
*
|
|
17
|
+
* @param where - The location where the error occurred.
|
|
18
|
+
*/
|
|
4
19
|
constructor(where: string);
|
|
5
20
|
}
|
|
6
|
-
|
|
7
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Error thrown when a signal value is null or undefined.
|
|
23
|
+
*/
|
|
24
|
+
declare class NullishSignalValueError extends TypeError {
|
|
25
|
+
/**
|
|
26
|
+
* Constructs a new NullishSignalValueError.
|
|
27
|
+
*
|
|
28
|
+
* @param where - The location where the error occurred.
|
|
29
|
+
*/
|
|
30
|
+
constructor(where: string);
|
|
8
31
|
}
|
|
9
|
-
|
|
10
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Error thrown when a signal is read before it has a value.
|
|
34
|
+
*/
|
|
35
|
+
declare class UnsetSignalValueError extends Error {
|
|
36
|
+
/**
|
|
37
|
+
* Constructs a new UnsetSignalValueError.
|
|
38
|
+
*
|
|
39
|
+
* @param where - The location where the error occurred.
|
|
40
|
+
*/
|
|
41
|
+
constructor(where: string);
|
|
11
42
|
}
|
|
12
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Error thrown when a signal value is invalid.
|
|
45
|
+
*/
|
|
46
|
+
declare class InvalidSignalValueError extends TypeError {
|
|
47
|
+
/**
|
|
48
|
+
* Constructs a new InvalidSignalValueError.
|
|
49
|
+
*
|
|
50
|
+
* @param where - The location where the error occurred.
|
|
51
|
+
* @param value - The invalid value.
|
|
52
|
+
*/
|
|
13
53
|
constructor(where: string, value: unknown);
|
|
14
54
|
}
|
|
15
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Error thrown when a callback is invalid.
|
|
57
|
+
*/
|
|
58
|
+
declare class InvalidCallbackError extends TypeError {
|
|
59
|
+
/**
|
|
60
|
+
* Constructs a new InvalidCallbackError.
|
|
61
|
+
*
|
|
62
|
+
* @param where - The location where the error occurred.
|
|
63
|
+
* @param value - The invalid value.
|
|
64
|
+
*/
|
|
16
65
|
constructor(where: string, value: unknown);
|
|
17
66
|
}
|
|
18
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Error thrown when an API requiring an owner is called without one.
|
|
69
|
+
*/
|
|
70
|
+
declare class RequiredOwnerError extends Error {
|
|
71
|
+
/**
|
|
72
|
+
* Constructs a new RequiredOwnerError.
|
|
73
|
+
*
|
|
74
|
+
* @param where - The location where the error occurred.
|
|
75
|
+
*/
|
|
19
76
|
constructor(where: string);
|
|
20
77
|
}
|
|
21
|
-
declare class
|
|
22
|
-
constructor(
|
|
78
|
+
declare class DuplicateKeyError extends Error {
|
|
79
|
+
constructor(where: string, key: string, value?: unknown);
|
|
23
80
|
}
|
|
24
|
-
declare function
|
|
25
|
-
declare
|
|
26
|
-
declare
|
|
27
|
-
declare
|
|
28
|
-
|
|
29
|
-
export { type Guard, CircularDependencyError, DuplicateKeyError, InvalidCallbackError, InvalidCollectionSourceError, InvalidSignalValueError, NullishSignalValueError, ReadonlySignalError, assert, createError, validateCallback, validateSignalValue, guardMutableSignal, };
|
|
81
|
+
declare function validateSignalValue<T extends {}>(where: string, value: unknown, guard?: Guard<T>): asserts value is T;
|
|
82
|
+
declare function validateReadValue<T extends {}>(where: string, value: T | null | undefined): asserts value is T;
|
|
83
|
+
declare function validateCallback(where: string, value: unknown): asserts value is (...args: unknown[]) => unknown;
|
|
84
|
+
declare function validateCallback<T>(where: string, value: unknown, guard: (value: unknown) => value is T): asserts value is T;
|
|
85
|
+
export { type Guard, CircularDependencyError, NullishSignalValueError, InvalidSignalValueError, UnsetSignalValueError, InvalidCallbackError, RequiredOwnerError, DuplicateKeyError, validateSignalValue, validateReadValue, validateCallback, };
|