@pyreon/reactivity 0.24.5 → 0.24.6
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/package.json +1 -4
- package/src/batch.ts +0 -196
- package/src/cell.ts +0 -72
- package/src/computed.ts +0 -313
- package/src/createSelector.ts +0 -109
- package/src/debug.ts +0 -134
- package/src/effect.ts +0 -467
- package/src/env.d.ts +0 -6
- package/src/index.ts +0 -60
- package/src/lpih.ts +0 -227
- package/src/manifest.ts +0 -660
- package/src/reactive-devtools.ts +0 -494
- package/src/reactive-trace.ts +0 -142
- package/src/reconcile.ts +0 -118
- package/src/resource.ts +0 -84
- package/src/scope.ts +0 -123
- package/src/signal.ts +0 -261
- package/src/store.ts +0 -250
- package/src/tests/batch.test.ts +0 -751
- package/src/tests/bind.test.ts +0 -84
- package/src/tests/branches.test.ts +0 -343
- package/src/tests/cell.test.ts +0 -159
- package/src/tests/computed.test.ts +0 -436
- package/src/tests/coverage-hardening.test.ts +0 -471
- package/src/tests/createSelector.test.ts +0 -291
- package/src/tests/debug.test.ts +0 -196
- package/src/tests/effect.test.ts +0 -464
- package/src/tests/fanout-repro.test.ts +0 -179
- package/src/tests/lpih-source-location.test.ts +0 -277
- package/src/tests/lpih.test.ts +0 -351
- package/src/tests/manifest-snapshot.test.ts +0 -96
- package/src/tests/reactive-devtools-treeshake.test.ts +0 -48
- package/src/tests/reactive-devtools.test.ts +0 -296
- package/src/tests/reactive-trace.test.ts +0 -102
- package/src/tests/reconcile-security.test.ts +0 -45
- package/src/tests/resource.test.ts +0 -326
- package/src/tests/scope.test.ts +0 -231
- package/src/tests/signal.test.ts +0 -368
- package/src/tests/store.test.ts +0 -286
- package/src/tests/tracking.test.ts +0 -158
- package/src/tests/vue-parity.test.ts +0 -191
- package/src/tests/watch.test.ts +0 -246
- package/src/tracking.ts +0 -139
- package/src/watch.ts +0 -68
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
import { computed } from '../computed'
|
|
2
|
-
import { effect } from '../effect'
|
|
3
|
-
import { signal } from '../signal'
|
|
4
|
-
|
|
5
|
-
describe('computed', () => {
|
|
6
|
-
test('computes derived value', () => {
|
|
7
|
-
const s = signal(2)
|
|
8
|
-
const doubled = computed(() => s() * 2)
|
|
9
|
-
expect(doubled()).toBe(4)
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
test('updates when dependency changes', () => {
|
|
13
|
-
const s = signal(3)
|
|
14
|
-
const tripled = computed(() => s() * 3)
|
|
15
|
-
expect(tripled()).toBe(9)
|
|
16
|
-
s.set(4)
|
|
17
|
-
expect(tripled()).toBe(12)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('is lazy — does not compute until read', () => {
|
|
21
|
-
let computations = 0
|
|
22
|
-
const s = signal(0)
|
|
23
|
-
const c = computed(() => {
|
|
24
|
-
computations++
|
|
25
|
-
return s() + 1
|
|
26
|
-
})
|
|
27
|
-
expect(computations).toBe(0)
|
|
28
|
-
c() // first read
|
|
29
|
-
expect(computations).toBe(1)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('is memoized — does not recompute on repeated reads', () => {
|
|
33
|
-
let computations = 0
|
|
34
|
-
const s = signal(5)
|
|
35
|
-
const c = computed(() => {
|
|
36
|
-
computations++
|
|
37
|
-
return s() * 2
|
|
38
|
-
})
|
|
39
|
-
c()
|
|
40
|
-
c()
|
|
41
|
-
c()
|
|
42
|
-
expect(computations).toBe(1)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test('recomputes only when dirty', () => {
|
|
46
|
-
let computations = 0
|
|
47
|
-
const s = signal(1)
|
|
48
|
-
const c = computed(() => {
|
|
49
|
-
computations++
|
|
50
|
-
return s()
|
|
51
|
-
})
|
|
52
|
-
c()
|
|
53
|
-
expect(computations).toBe(1)
|
|
54
|
-
s.set(2)
|
|
55
|
-
c()
|
|
56
|
-
expect(computations).toBe(2)
|
|
57
|
-
c()
|
|
58
|
-
expect(computations).toBe(2) // still memoized
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test('chains correctly', () => {
|
|
62
|
-
const base = signal(2)
|
|
63
|
-
const doubled = computed(() => base() * 2)
|
|
64
|
-
const quadrupled = computed(() => doubled() * 2)
|
|
65
|
-
expect(quadrupled()).toBe(8)
|
|
66
|
-
base.set(3)
|
|
67
|
-
expect(quadrupled()).toBe(12)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test('dispose stops recomputation', () => {
|
|
71
|
-
const s = signal(1)
|
|
72
|
-
let computations = 0
|
|
73
|
-
const c = computed(() => {
|
|
74
|
-
computations++
|
|
75
|
-
return s() * 2
|
|
76
|
-
})
|
|
77
|
-
c() // initial
|
|
78
|
-
expect(computations).toBe(1)
|
|
79
|
-
c.dispose()
|
|
80
|
-
s.set(2)
|
|
81
|
-
// After dispose, reading returns stale value and does not recompute
|
|
82
|
-
// (the computed is no longer subscribed to s)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('custom equals skips downstream notification when equal', () => {
|
|
86
|
-
const s = signal(3)
|
|
87
|
-
let downstream = 0
|
|
88
|
-
|
|
89
|
-
const c = computed(() => Math.floor(s() / 10), {
|
|
90
|
-
equals: (a, b) => a === b,
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
effect(() => {
|
|
94
|
-
c()
|
|
95
|
-
downstream++
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
expect(downstream).toBe(1)
|
|
99
|
-
expect(c()).toBe(0)
|
|
100
|
-
|
|
101
|
-
s.set(5) // Math.floor(5/10) = 0, same as before
|
|
102
|
-
expect(downstream).toBe(1) // no downstream update
|
|
103
|
-
|
|
104
|
-
s.set(15) // Math.floor(15/10) = 1, different
|
|
105
|
-
expect(downstream).toBe(2)
|
|
106
|
-
expect(c()).toBe(1)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
test('custom equals with array comparison', () => {
|
|
110
|
-
const items = signal([1, 2, 3])
|
|
111
|
-
let downstream = 0
|
|
112
|
-
|
|
113
|
-
const sorted = computed(() => items().slice().sort(), {
|
|
114
|
-
equals: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]),
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
effect(() => {
|
|
118
|
-
sorted()
|
|
119
|
-
downstream++
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
expect(downstream).toBe(1)
|
|
123
|
-
|
|
124
|
-
// Set to same content in different array — equals returns true, no notification
|
|
125
|
-
items.set([1, 2, 3])
|
|
126
|
-
expect(downstream).toBe(1)
|
|
127
|
-
|
|
128
|
-
// Actually different content
|
|
129
|
-
items.set([1, 2, 4])
|
|
130
|
-
expect(downstream).toBe(2)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test('computed used as dependency inside an effect (subscribe path)', () => {
|
|
134
|
-
const s = signal(10)
|
|
135
|
-
const c = computed(() => s() + 1)
|
|
136
|
-
let result = 0
|
|
137
|
-
|
|
138
|
-
effect(() => {
|
|
139
|
-
result = c()
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
expect(result).toBe(11)
|
|
143
|
-
s.set(20)
|
|
144
|
-
expect(result).toBe(21)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
test('._v returns cached value', () => {
|
|
148
|
-
const s = signal(5)
|
|
149
|
-
const doubled = computed(() => s() * 2)
|
|
150
|
-
// First access triggers computation
|
|
151
|
-
expect(doubled._v).toBe(10)
|
|
152
|
-
s.set(7)
|
|
153
|
-
// _v triggers recompute when dirty
|
|
154
|
-
expect(doubled._v).toBe(14)
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test('.direct() fires updater on recompute', () => {
|
|
158
|
-
const s = signal(1)
|
|
159
|
-
const doubled = computed(() => s() * 2)
|
|
160
|
-
doubled() // initialize
|
|
161
|
-
|
|
162
|
-
let called = 0
|
|
163
|
-
const dispose = doubled.direct(() => {
|
|
164
|
-
called++
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
s.set(2)
|
|
168
|
-
expect(called).toBe(1)
|
|
169
|
-
expect(doubled._v).toBe(4)
|
|
170
|
-
|
|
171
|
-
s.set(3)
|
|
172
|
-
expect(called).toBe(2)
|
|
173
|
-
|
|
174
|
-
dispose()
|
|
175
|
-
s.set(4)
|
|
176
|
-
expect(called).toBe(2) // disposed, no more calls
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
test('.direct() works with equals option', () => {
|
|
180
|
-
const s = signal(1)
|
|
181
|
-
const clamped = computed(() => Math.min(s(), 10), {
|
|
182
|
-
equals: (a, b) => a === b,
|
|
183
|
-
})
|
|
184
|
-
clamped() // initialize
|
|
185
|
-
|
|
186
|
-
let called = 0
|
|
187
|
-
clamped.direct(() => {
|
|
188
|
-
called++
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
s.set(5)
|
|
192
|
-
expect(called).toBe(1)
|
|
193
|
-
|
|
194
|
-
// Same clamped result — equals returns true, no notification
|
|
195
|
-
s.set(10)
|
|
196
|
-
expect(called).toBe(2)
|
|
197
|
-
s.set(11) // clamped to 10, same as before
|
|
198
|
-
expect(called).toBe(2) // equals suppresses
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
test('.direct() registrations do not accumulate under churn (bounded, like signal._d)', () => {
|
|
202
|
-
// Regression for the never-compacted-array leak: a long-lived
|
|
203
|
-
// computed whose direct updaters register+dispose repeatedly (e.g.
|
|
204
|
-
// <For> rows re-mounting) must keep its live set bounded to LIVE
|
|
205
|
-
// registrations, not grow one permanent dead slot per ever-
|
|
206
|
-
// registered binding (which also made `recompute` O(total-ever)).
|
|
207
|
-
const s = signal(0)
|
|
208
|
-
const c = computed(() => s() * 2)
|
|
209
|
-
c() // initialize
|
|
210
|
-
const internal = c as unknown as { _d: Set<() => void> | null }
|
|
211
|
-
|
|
212
|
-
for (let i = 0; i < 10_000; i++) {
|
|
213
|
-
const dispose = c.direct(() => {})
|
|
214
|
-
dispose()
|
|
215
|
-
}
|
|
216
|
-
expect(internal._d!.size).toBe(0)
|
|
217
|
-
|
|
218
|
-
// One live binding survives → notify/iterate cost is O(live), not 10k.
|
|
219
|
-
let fired = 0
|
|
220
|
-
const dispose = c.direct(() => {
|
|
221
|
-
fired++
|
|
222
|
-
})
|
|
223
|
-
expect(internal._d!.size).toBe(1)
|
|
224
|
-
s.set(1)
|
|
225
|
-
expect(fired).toBe(1)
|
|
226
|
-
dispose()
|
|
227
|
-
expect(internal._d!.size).toBe(0)
|
|
228
|
-
s.set(2)
|
|
229
|
-
expect(fired).toBe(1) // disposed updater not invoked
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
describe('_v with equals after disposal', () => {
|
|
233
|
-
test('_v returns last cached value after dispose()', () => {
|
|
234
|
-
const s = signal(5)
|
|
235
|
-
const doubled = computed(() => s() * 2)
|
|
236
|
-
expect(doubled._v).toBe(10) // triggers initial computation
|
|
237
|
-
|
|
238
|
-
s.set(7)
|
|
239
|
-
expect(doubled._v).toBe(14) // recomputes
|
|
240
|
-
|
|
241
|
-
doubled.dispose()
|
|
242
|
-
s.set(100)
|
|
243
|
-
// After dispose, _v returns the last cached value (14)
|
|
244
|
-
// because dirty flag is not set (no subscription) and the
|
|
245
|
-
// disposed guard prevents recomputation
|
|
246
|
-
expect(doubled._v).toBe(14)
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
test('computed with equals: _v only updates when equality check fails', () => {
|
|
250
|
-
const s = signal(3)
|
|
251
|
-
const floored = computed(() => Math.floor(s() / 10), {
|
|
252
|
-
equals: (a, b) => a === b,
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
expect(floored._v).toBe(0) // Math.floor(3/10) = 0
|
|
256
|
-
|
|
257
|
-
s.set(5) // Math.floor(5/10) = 0, same
|
|
258
|
-
expect(floored._v).toBe(0)
|
|
259
|
-
|
|
260
|
-
s.set(15) // Math.floor(15/10) = 1, different
|
|
261
|
-
expect(floored._v).toBe(1)
|
|
262
|
-
|
|
263
|
-
s.set(19) // Math.floor(19/10) = 1, same
|
|
264
|
-
expect(floored._v).toBe(1)
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
test('multiple .direct() updaters on computed, dispose one', () => {
|
|
268
|
-
const s = signal(1)
|
|
269
|
-
const doubled = computed(() => s() * 2)
|
|
270
|
-
doubled() // initialize
|
|
271
|
-
|
|
272
|
-
let calls1 = 0
|
|
273
|
-
let calls2 = 0
|
|
274
|
-
let calls3 = 0
|
|
275
|
-
|
|
276
|
-
const dispose1 = doubled.direct(() => {
|
|
277
|
-
calls1++
|
|
278
|
-
})
|
|
279
|
-
doubled.direct(() => {
|
|
280
|
-
calls2++
|
|
281
|
-
})
|
|
282
|
-
doubled.direct(() => {
|
|
283
|
-
calls3++
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
s.set(2)
|
|
287
|
-
expect(calls1).toBe(1)
|
|
288
|
-
expect(calls2).toBe(1)
|
|
289
|
-
expect(calls3).toBe(1)
|
|
290
|
-
|
|
291
|
-
dispose1()
|
|
292
|
-
// Read to reset dirty flag so next change triggers recompute notification
|
|
293
|
-
doubled()
|
|
294
|
-
s.set(3)
|
|
295
|
-
expect(calls1).toBe(1) // disposed
|
|
296
|
-
expect(calls2).toBe(2) // still active
|
|
297
|
-
expect(calls3).toBe(2) // still active
|
|
298
|
-
})
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
describe('diamond pattern cleanup', () => {
|
|
302
|
-
test('a -> b, c -> d diamond: d only recomputes once per a change', () => {
|
|
303
|
-
const a = signal(1)
|
|
304
|
-
const b = computed(() => a() + 1)
|
|
305
|
-
const c = computed(() => a() + 2)
|
|
306
|
-
|
|
307
|
-
let dComputations = 0
|
|
308
|
-
const d = computed(() => {
|
|
309
|
-
dComputations++
|
|
310
|
-
return b() + c()
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
expect(d()).toBe(5) // b=2 + c=3
|
|
314
|
-
expect(dComputations).toBe(1)
|
|
315
|
-
|
|
316
|
-
a.set(2) // b=3, c=4
|
|
317
|
-
expect(d()).toBe(7)
|
|
318
|
-
// d should only recompute once (lazy evaluation avoids double recompute)
|
|
319
|
-
expect(dComputations).toBe(2)
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
test('dispose middle node in diamond, verify no stale subscriptions', () => {
|
|
323
|
-
const a = signal(1)
|
|
324
|
-
const b = computed(() => a() * 2)
|
|
325
|
-
const c = computed(() => a() * 3)
|
|
326
|
-
|
|
327
|
-
let dRuns = 0
|
|
328
|
-
const d = computed(() => {
|
|
329
|
-
dRuns++
|
|
330
|
-
return b() + c()
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
expect(d()).toBe(5) // b=2 + c=3
|
|
334
|
-
expect(dRuns).toBe(1)
|
|
335
|
-
|
|
336
|
-
// Dispose b — it will no longer recompute from a
|
|
337
|
-
b.dispose()
|
|
338
|
-
|
|
339
|
-
a.set(2)
|
|
340
|
-
// d still recomputes because c changed, but b returns stale value (2)
|
|
341
|
-
expect(d()).toBe(8) // b=2 (stale) + c=6
|
|
342
|
-
expect(dRuns).toBe(2)
|
|
343
|
-
|
|
344
|
-
// Verify a no longer notifies b's subscribers
|
|
345
|
-
a.set(3)
|
|
346
|
-
expect(d()).toBe(11) // b=2 (still stale) + c=9
|
|
347
|
-
expect(dRuns).toBe(3)
|
|
348
|
-
})
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
// ─── Audit bug #1 (extension): async computed warning ─────────────────
|
|
352
|
-
describe('async function warning', () => {
|
|
353
|
-
test('warns when called with an async arrow function', () => {
|
|
354
|
-
const warns: string[] = []
|
|
355
|
-
const orig = console.warn
|
|
356
|
-
console.warn = (...args: unknown[]) => warns.push(args.join(' '))
|
|
357
|
-
try {
|
|
358
|
-
const asyncFn = async (): Promise<number> => 42
|
|
359
|
-
// Cast through `unknown` because async is intentionally NOT in
|
|
360
|
-
// computed()'s type signature — that's the point. The runtime
|
|
361
|
-
// warn catches what the type system would normally reject.
|
|
362
|
-
computed(asyncFn as unknown as () => number)
|
|
363
|
-
} finally {
|
|
364
|
-
console.warn = orig
|
|
365
|
-
}
|
|
366
|
-
expect(warns.some((m) => m.includes('computed'))).toBe(true)
|
|
367
|
-
expect(warns.some((m) => m.includes('createResource'))).toBe(true)
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
test('does NOT warn for synchronous computed callbacks', () => {
|
|
371
|
-
const warns: string[] = []
|
|
372
|
-
const orig = console.warn
|
|
373
|
-
console.warn = (...args: unknown[]) => warns.push(args.join(' '))
|
|
374
|
-
try {
|
|
375
|
-
computed(() => 42)
|
|
376
|
-
} finally {
|
|
377
|
-
console.warn = orig
|
|
378
|
-
}
|
|
379
|
-
expect(warns.some((m) => m.includes('async function'))).toBe(false)
|
|
380
|
-
})
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
// M6 audit gap (c): creating a computed inside another computed's recompute
|
|
384
|
-
// body. Edge case — the inner computed registers its `recompute` as a
|
|
385
|
-
// signal subscriber DURING the outer's evaluation. The new computed should
|
|
386
|
-
// track the outer's source dependency correctly and not crash the recompute.
|
|
387
|
-
describe('computed-in-computed recompute (regression)', () => {
|
|
388
|
-
test('creating a computed inside a computed body is safe and tracks correctly', () => {
|
|
389
|
-
const source = signal(1)
|
|
390
|
-
let innerCreated = 0
|
|
391
|
-
|
|
392
|
-
// Outer computed creates a new computed each time it runs.
|
|
393
|
-
const outer = computed(() => {
|
|
394
|
-
const v = source()
|
|
395
|
-
innerCreated++
|
|
396
|
-
const inner = computed(() => v * 2)
|
|
397
|
-
return inner()
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
// First read — outer creates inner #1, returns 2.
|
|
401
|
-
expect(outer()).toBe(2)
|
|
402
|
-
expect(innerCreated).toBe(1)
|
|
403
|
-
|
|
404
|
-
// Source change — outer recomputes, creates inner #2, returns 4.
|
|
405
|
-
source.set(2)
|
|
406
|
-
expect(outer()).toBe(4)
|
|
407
|
-
expect(innerCreated).toBe(2)
|
|
408
|
-
|
|
409
|
-
// Verify the previously-created inner computeds didn't capture stale
|
|
410
|
-
// tracking — the latest outer() value reflects the latest source.
|
|
411
|
-
source.set(10)
|
|
412
|
-
expect(outer()).toBe(20)
|
|
413
|
-
expect(innerCreated).toBe(3)
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
test('inner computed reads outer source signal — no double-track or recompute leak', () => {
|
|
417
|
-
const source = signal(1)
|
|
418
|
-
let outerRuns = 0
|
|
419
|
-
|
|
420
|
-
const outer = computed(() => {
|
|
421
|
-
outerRuns++
|
|
422
|
-
const v = source()
|
|
423
|
-
// Inner reads the same source — should not double-subscribe outer.
|
|
424
|
-
const inner = computed(() => source() + v)
|
|
425
|
-
return inner()
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
expect(outer()).toBe(2)
|
|
429
|
-
expect(outerRuns).toBe(1)
|
|
430
|
-
|
|
431
|
-
source.set(5)
|
|
432
|
-
expect(outer()).toBe(10)
|
|
433
|
-
expect(outerRuns).toBe(2) // not 3 or more (no double-fire)
|
|
434
|
-
})
|
|
435
|
-
})
|
|
436
|
-
})
|