@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.
Files changed (44) hide show
  1. package/package.json +1 -4
  2. package/src/batch.ts +0 -196
  3. package/src/cell.ts +0 -72
  4. package/src/computed.ts +0 -313
  5. package/src/createSelector.ts +0 -109
  6. package/src/debug.ts +0 -134
  7. package/src/effect.ts +0 -467
  8. package/src/env.d.ts +0 -6
  9. package/src/index.ts +0 -60
  10. package/src/lpih.ts +0 -227
  11. package/src/manifest.ts +0 -660
  12. package/src/reactive-devtools.ts +0 -494
  13. package/src/reactive-trace.ts +0 -142
  14. package/src/reconcile.ts +0 -118
  15. package/src/resource.ts +0 -84
  16. package/src/scope.ts +0 -123
  17. package/src/signal.ts +0 -261
  18. package/src/store.ts +0 -250
  19. package/src/tests/batch.test.ts +0 -751
  20. package/src/tests/bind.test.ts +0 -84
  21. package/src/tests/branches.test.ts +0 -343
  22. package/src/tests/cell.test.ts +0 -159
  23. package/src/tests/computed.test.ts +0 -436
  24. package/src/tests/coverage-hardening.test.ts +0 -471
  25. package/src/tests/createSelector.test.ts +0 -291
  26. package/src/tests/debug.test.ts +0 -196
  27. package/src/tests/effect.test.ts +0 -464
  28. package/src/tests/fanout-repro.test.ts +0 -179
  29. package/src/tests/lpih-source-location.test.ts +0 -277
  30. package/src/tests/lpih.test.ts +0 -351
  31. package/src/tests/manifest-snapshot.test.ts +0 -96
  32. package/src/tests/reactive-devtools-treeshake.test.ts +0 -48
  33. package/src/tests/reactive-devtools.test.ts +0 -296
  34. package/src/tests/reactive-trace.test.ts +0 -102
  35. package/src/tests/reconcile-security.test.ts +0 -45
  36. package/src/tests/resource.test.ts +0 -326
  37. package/src/tests/scope.test.ts +0 -231
  38. package/src/tests/signal.test.ts +0 -368
  39. package/src/tests/store.test.ts +0 -286
  40. package/src/tests/tracking.test.ts +0 -158
  41. package/src/tests/vue-parity.test.ts +0 -191
  42. package/src/tests/watch.test.ts +0 -246
  43. package/src/tracking.ts +0 -139
  44. 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
- })