@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,84 +0,0 @@
1
- import { _bind } from '../effect'
2
- import { signal } from '../signal'
3
-
4
- describe('_bind (static-dep binding)', () => {
5
- test('runs the function on first call and tracks deps', () => {
6
- const s = signal(0)
7
- let runs = 0
8
-
9
- const dispose = _bind(() => {
10
- s()
11
- runs++
12
- })
13
-
14
- expect(runs).toBe(1)
15
-
16
- // Deps tracked on first run, re-runs on signal change
17
- s.set(1)
18
- expect(runs).toBe(2)
19
-
20
- dispose()
21
- })
22
-
23
- test('dispose stops re-runs', () => {
24
- const s = signal(0)
25
- let runs = 0
26
-
27
- const dispose = _bind(() => {
28
- s()
29
- runs++
30
- })
31
-
32
- expect(runs).toBe(1)
33
-
34
- dispose()
35
- s.set(1)
36
- expect(runs).toBe(1) // no re-run
37
- })
38
-
39
- test('dispose is idempotent', () => {
40
- const s = signal(0)
41
- const dispose = _bind(() => {
42
- s()
43
- })
44
-
45
- dispose()
46
- dispose() // should not throw
47
- })
48
-
49
- test('does not re-run after dispose even with multiple deps', () => {
50
- const a = signal(0)
51
- const b = signal(0)
52
- let runs = 0
53
-
54
- const dispose = _bind(() => {
55
- a()
56
- b()
57
- runs++
58
- })
59
-
60
- expect(runs).toBe(1)
61
-
62
- dispose()
63
- a.set(1)
64
- b.set(1)
65
- expect(runs).toBe(1)
66
- })
67
-
68
- test('disposed run callback is a no-op', () => {
69
- const s = signal(0)
70
- let runs = 0
71
-
72
- const dispose = _bind(() => {
73
- s()
74
- runs++
75
- })
76
-
77
- expect(runs).toBe(1)
78
-
79
- // Dispose then trigger — the run function should bail out
80
- dispose()
81
- s.set(5)
82
- expect(runs).toBe(1)
83
- })
84
- })
@@ -1,343 +0,0 @@
1
- /**
2
- * Targeted tests for uncovered branches across reactivity package.
3
- */
4
- import { Cell } from '../cell'
5
- import { computed } from '../computed'
6
- import { createSelector } from '../createSelector'
7
- import { why } from '../debug'
8
- import { _bind, effect, renderEffect } from '../effect'
9
- import { reconcile } from '../reconcile'
10
- import { signal } from '../signal'
11
- import { createStore, isStore } from '../store'
12
-
13
- // ── cell.ts branches: promote listener to Set ─────────────────────────────────
14
-
15
- describe('Cell listener promotion', () => {
16
- test('promotes single listener to Set when second listener added', () => {
17
- const c = new Cell(0)
18
- const calls: number[] = []
19
- c.listen(() => calls.push(1))
20
- c.listen(() => calls.push(2))
21
- // Third listen: _s already exists (false branch of `if (!this._s)`)
22
- c.listen(() => calls.push(3))
23
- c.set(1)
24
- expect(calls).toEqual([1, 2, 3])
25
- })
26
-
27
- test('subscribe unsubscribes single listener', () => {
28
- const c = new Cell(0)
29
- const calls: number[] = []
30
- const unsub = c.subscribe(() => calls.push(1))
31
- c.set(1)
32
- expect(calls).toEqual([1])
33
- unsub()
34
- c.set(2)
35
- // Should not fire after unsubscribe
36
- expect(calls).toEqual([1])
37
- })
38
-
39
- test('subscribe unsubscribes from Set', () => {
40
- const c = new Cell(0)
41
- const calls: number[] = []
42
- c.listen(() => calls.push(1))
43
- const unsub = c.subscribe(() => calls.push(2))
44
- c.set(1)
45
- expect(calls).toEqual([1, 2])
46
- unsub()
47
- c.set(2)
48
- expect(calls).toEqual([1, 2, 1])
49
- })
50
-
51
- test('promote to Set when _l was unsubscribed (null _l, null _s)', () => {
52
- const c = new Cell(0)
53
- const fn1 = () => {}
54
- // subscribe sets _l, unsub sets _l to null
55
- const unsub = c.subscribe(fn1)
56
- unsub()
57
- // Now _l is null and _s is null — next listen goes to fast path (!_l && !_s)
58
- const fn2 = () => {}
59
- c.listen(fn2)
60
- // Add another to force promotion — _l is fn2, _s is null → promotes with _l
61
- c.listen(() => {})
62
- c.set(1)
63
- })
64
-
65
- test('double unsubscribe from single listener is safe', () => {
66
- const c = new Cell(0)
67
- const fn1 = () => {}
68
- const unsub = c.subscribe(fn1)
69
- unsub()
70
- // Second call — _l is null, so `this._l === listener` is false
71
- unsub()
72
- })
73
- })
74
-
75
- // ── computed.ts branches ──────────────────────────────────────────────────────
76
-
77
- describe('computed branches', () => {
78
- test('disposed computed does not recompute', () => {
79
- const s = signal(1)
80
- const c = computed(() => s() * 2, { equals: Object.is })
81
- expect(c()).toBe(2)
82
- c.dispose()
83
- s.set(5)
84
- // After dispose, the computed should not update
85
- // (it may return stale value or throw — just ensure no crash)
86
- })
87
-
88
- test('computed with custom equals and subscribers', () => {
89
- const s = signal(1)
90
- const c = computed(() => s() * 2, { equals: Object.is })
91
- const values: number[] = []
92
- effect(() => {
93
- values.push(c())
94
- })
95
- expect(values).toEqual([2])
96
- s.set(2)
97
- expect(values).toEqual([2, 4])
98
- // Same value — should not notify
99
- s.set(2)
100
- expect(values).toEqual([2, 4])
101
- })
102
-
103
- test('computed without custom equals notifies subscribers on dep change', () => {
104
- const s = signal(1)
105
- const c = computed(() => s() * 2)
106
- const values: number[] = []
107
- effect(() => {
108
- values.push(c())
109
- })
110
- expect(values).toEqual([2])
111
- s.set(2)
112
- expect(values).toEqual([2, 4])
113
- })
114
- })
115
-
116
- // ── createSelector.ts branches ────────────────────────────────────────────────
117
-
118
- describe('createSelector branches', () => {
119
- test('selector with no matching bucket on old value', () => {
120
- const s = signal<string>('a')
121
- const isSelected = createSelector(s)
122
- // Read "a" — creates bucket for "a"
123
- effect(() => {
124
- isSelected('a')
125
- })
126
- // Change to "b" — old bucket "a" exists, new bucket "b" does not
127
- s.set('b')
128
- })
129
-
130
- test('selector reuses existing host for same value', () => {
131
- const s = signal<string>('a')
132
- const isSelected = createSelector(s)
133
- const results: boolean[] = []
134
- effect(() => {
135
- results.push(isSelected('a'))
136
- })
137
- // This second effect creates another subscription to same bucket
138
- effect(() => {
139
- results.push(isSelected('a'))
140
- })
141
- expect(results).toEqual([true, true])
142
- s.set('b')
143
- // Both should see false
144
- expect(results).toEqual([true, true, false, false])
145
- })
146
-
147
- test('selector handles Object.is equality (no change)', () => {
148
- const s = signal<string>('a')
149
- const isSelected = createSelector(s)
150
- let count = 0
151
- effect(() => {
152
- isSelected('a')
153
- count++
154
- })
155
- expect(count).toBe(1)
156
- // Same value — Object.is check should skip
157
- s.set('a')
158
- expect(count).toBe(1)
159
- })
160
-
161
- test('selector query for value with no existing bucket creates one', () => {
162
- const s = signal<string>('a')
163
- const isSelected = createSelector(s)
164
- // Query outside effect — creates a bucket for "z" that has no subscribers
165
- const result = isSelected('z')
166
- expect(result).toBe(false)
167
- })
168
-
169
- test('selector change when old value has no subscriber bucket', () => {
170
- const s = signal<string>('a')
171
- const isSelected = createSelector(s)
172
- // Only subscribe to "b", not "a"
173
- effect(() => {
174
- isSelected('b')
175
- })
176
- // Change from "a" to "b" — old value "a" has no bucket (never queried in effect)
177
- s.set('b')
178
- })
179
- })
180
-
181
- // ── effect.ts branches ────────────────────────────────────────────────────────
182
-
183
- describe('effect disposed branches', () => {
184
- test('disposed effect does not re-run', () => {
185
- const s = signal(0)
186
- let count = 0
187
- const e = effect(() => {
188
- s()
189
- count++
190
- })
191
- expect(count).toBe(1)
192
- e.dispose()
193
- s.set(1)
194
- expect(count).toBe(1)
195
- })
196
-
197
- test('disposed _bind does not re-run', () => {
198
- const s = signal(0)
199
- let count = 0
200
- const dispose = _bind(() => {
201
- s()
202
- count++
203
- })
204
- expect(count).toBe(1)
205
- dispose()
206
- s.set(1)
207
- expect(count).toBe(1)
208
- // Double dispose is safe
209
- dispose()
210
- })
211
-
212
- test('disposed renderEffect does not re-run', () => {
213
- const s = signal(0)
214
- let count = 0
215
- const dispose = renderEffect(() => {
216
- s()
217
- count++
218
- })
219
- expect(count).toBe(1)
220
- dispose()
221
- s.set(1)
222
- expect(count).toBe(1)
223
- })
224
- })
225
-
226
- // ── store.ts branches ─────────────────────────────────────────────────────────
227
-
228
- describe('store branches', () => {
229
- test('setting symbol property', () => {
230
- const store = createStore({ a: 1 })
231
- const sym = Symbol('test')
232
- ;(store as Record<symbol, unknown>)[sym] = 'hello'
233
- expect((store as Record<symbol, unknown>)[sym]).toBe('hello')
234
- })
235
-
236
- test('deleteProperty on store', () => {
237
- const store = createStore<Record<string, unknown>>({ a: 1, b: 2 })
238
- delete store.b
239
- expect(store.b).toBeUndefined()
240
- })
241
-
242
- test('deleteProperty on store array', () => {
243
- const store = createStore([1, 2, 3])
244
- delete (store as unknown as Record<string, unknown>)['1']
245
- expect(store[1]).toBeUndefined()
246
- })
247
-
248
- test('deleteProperty on store with reactive subscriber', () => {
249
- const store = createStore<Record<string, unknown>>({ a: 1, b: 2 })
250
- // Read 'b' in effect to create propSignal
251
- let val: unknown
252
- effect(() => {
253
- val = store.b
254
- })
255
- expect(val).toBe(2)
256
- delete store.b
257
- // Signal should be set to undefined and deleted from map
258
- expect(val).toBeUndefined()
259
- })
260
- })
261
-
262
- // ── reconcile.ts branches ─────────────────────────────────────────────────────
263
-
264
- describe('reconcile branches', () => {
265
- test('reconcile array with non-object source items', () => {
266
- const store = createStore([1, 2, 3])
267
- reconcile([4, 5], store)
268
- expect([...store]).toEqual([4, 5])
269
- })
270
-
271
- test('reconcile object with raw (non-store) target value', () => {
272
- // Create store where nested value isn't yet a store proxy
273
- const store = createStore<Record<string, unknown>>({ a: 1 })
274
- // Reconcile with nested object — target.a is a number (not store), so it takes the else branch
275
- reconcile({ a: { nested: true } }, store)
276
- expect((store.a as Record<string, unknown>).nested).toBe(true)
277
- })
278
-
279
- test('reconcile array with null source entries', () => {
280
- const store = createStore([1, null, 3])
281
- reconcile([null, 2, null], store)
282
- expect([...store]).toEqual([null, 2, null])
283
- })
284
-
285
- test('reconcile object with null source values', () => {
286
- const store = createStore<Record<string, unknown>>({ a: { x: 1 }, b: 2 })
287
- reconcile({ a: null, b: 2 }, store)
288
- expect(store.a).toBeNull()
289
- })
290
-
291
- test('reconcile array with both source and target as objects (recursive)', () => {
292
- const store = createStore([{ a: 1 }, { b: 2 }])
293
- reconcile([{ a: 10 }, { b: 20 }], store)
294
- expect(store[0]?.a).toBe(10)
295
- expect(store[1]?.b).toBe(20)
296
- })
297
-
298
- test('reconcile object where target has store-proxied nested object', () => {
299
- const store = createStore<Record<string, Record<string, number>>>({ nested: { x: 1 } })
300
- // Access nested to ensure it's proxied as store
301
- const _val = store.nested?.x
302
- expect(isStore(store.nested!)).toBe(true)
303
- reconcile({ nested: { x: 99 } }, store)
304
- expect(store.nested?.x).toBe(99)
305
- })
306
-
307
- test('reconcile object where target has raw (non-store) nested object', () => {
308
- // Don't access nested, so it stays as raw object (not proxied)
309
- const store = createStore<Record<string, Record<string, number>>>({ nested: { x: 1 } })
310
- // nested has not been accessed via proxy, so isStore(target.nested) is false
311
- // This should hit the `else { target[key] = sv }` branch at line 78
312
- reconcile({ nested: { x: 99 } }, store)
313
- expect(store.nested?.x).toBe(99)
314
- })
315
- })
316
-
317
- // ── debug.ts branches ─────────────────────────────────────────────────────────
318
-
319
- describe('debug branches', () => {
320
- test('why with exactly 1 subscriber shows singular', async () => {
321
- const s = signal(0, { name: 'single' })
322
- // Add exactly 1 subscriber
323
- effect(() => {
324
- s()
325
- })
326
- const logs: string[] = []
327
- const origLog = console.log
328
- console.log = (...args: unknown[]) => logs.push(args.join(' '))
329
- why()
330
- s.set(1)
331
- // why() auto-disposes via microtask
332
- await new Promise((r) => setTimeout(r, 10))
333
- console.log = origLog
334
- expect(logs.some((l) => l.includes('1 subscriber'))).toBe(true)
335
- })
336
-
337
- test('_notifyTraceListeners with no active listeners is noop', () => {
338
- // When no listeners registered, this should not throw
339
- // (tests the early return / null check)
340
- const s = signal(0)
341
- s.set(1) // triggers _notifyTraceListeners internally but no listeners active
342
- })
343
- })
@@ -1,159 +0,0 @@
1
- import { Cell, cell } from '../cell'
2
-
3
- describe('Cell', () => {
4
- test('stores and reads initial value', () => {
5
- const c = cell(42)
6
- expect(c.peek()).toBe(42)
7
- })
8
-
9
- test('set() updates value', () => {
10
- const c = cell('hello')
11
- c.set('world')
12
- expect(c.peek()).toBe('world')
13
- })
14
-
15
- test('set() skips when value is the same (Object.is)', () => {
16
- const c = cell(1)
17
- let calls = 0
18
- c.listen(() => calls++)
19
- c.set(1)
20
- expect(calls).toBe(0)
21
- })
22
-
23
- test('update() applies function to current value', () => {
24
- const c = cell(10)
25
- c.update((v) => v + 5)
26
- expect(c.peek()).toBe(15)
27
- })
28
-
29
- test('listen() fires on set()', () => {
30
- const c = cell('a')
31
- let fired = false
32
- c.listen(() => {
33
- fired = true
34
- })
35
- c.set('b')
36
- expect(fired).toBe(true)
37
- })
38
-
39
- test('listen() single-listener fast path (no Set allocated)', () => {
40
- const c = cell(0)
41
- let count = 0
42
- c.listen(() => count++)
43
- // Should use _l fast path, not _s Set
44
- expect(c._s).toBeNull()
45
- expect(c._l).not.toBeNull()
46
- c.set(1)
47
- expect(count).toBe(1)
48
- })
49
-
50
- test('listen() promotes to Set with multiple listeners', () => {
51
- const c = cell(0)
52
- let a = 0
53
- let b = 0
54
- c.listen(() => a++)
55
- c.listen(() => b++)
56
- expect(c._s).not.toBeNull()
57
- expect(c._l).toBeNull()
58
- c.set(1)
59
- expect(a).toBe(1)
60
- expect(b).toBe(1)
61
- })
62
-
63
- test('subscribe() returns working unsubscribe (single listener)', () => {
64
- const c = cell(0)
65
- let count = 0
66
- const unsub = c.subscribe(() => count++)
67
- c.set(1)
68
- expect(count).toBe(1)
69
- unsub()
70
- c.set(2)
71
- expect(count).toBe(1) // no more notifications
72
- })
73
-
74
- test('subscribe() returns working unsubscribe (multi listener)', () => {
75
- const c = cell(0)
76
- let a = 0
77
- let b = 0
78
- c.listen(() => a++)
79
- const unsub = c.subscribe(() => b++)
80
- c.set(1)
81
- expect(a).toBe(1)
82
- expect(b).toBe(1)
83
- unsub()
84
- c.set(2)
85
- expect(a).toBe(2)
86
- expect(b).toBe(1) // unsubscribed
87
- })
88
-
89
- test('cell() factory returns Cell instance', () => {
90
- const c = cell('x')
91
- expect(c).toBeInstanceOf(Cell)
92
- })
93
-
94
- test('multiple rapid updates notify correctly', () => {
95
- const c = cell(0)
96
- const values: number[] = []
97
- c.listen(() => values.push(c.peek()))
98
- c.set(1)
99
- c.set(2)
100
- c.set(3)
101
- expect(values).toEqual([1, 2, 3])
102
- })
103
-
104
- test('NaN equality (Object.is)', () => {
105
- const c = cell(Number.NaN)
106
- let calls = 0
107
- c.listen(() => calls++)
108
- c.set(Number.NaN)
109
- expect(calls).toBe(0) // Object.is(NaN, NaN) is true
110
- })
111
-
112
- test('subscribe() unsubscribe works after promotion to Set (regression)', () => {
113
- // Bug: first subscriber's disposer became stale after second subscriber
114
- // promoted _l → _s. The disposer checked _l which was now null.
115
- const c = cell(0)
116
- let count1 = 0
117
- let count2 = 0
118
- const unsub1 = c.subscribe(() => count1++)
119
- const unsub2 = c.subscribe(() => count2++)
120
-
121
- c.set(1)
122
- expect(count1).toBe(1)
123
- expect(count2).toBe(1)
124
-
125
- // Unsubscribe first listener — must remove from _s Set
126
- unsub1()
127
- c.set(2)
128
- expect(count1).toBe(1) // should NOT fire again
129
- expect(count2).toBe(2)
130
-
131
- // Unsubscribe second listener
132
- unsub2()
133
- c.set(3)
134
- expect(count1).toBe(1)
135
- expect(count2).toBe(2)
136
- })
137
-
138
- test('subscribe() unsubscribe order: second before first', () => {
139
- const c = cell(0)
140
- let count1 = 0
141
- let count2 = 0
142
- const unsub1 = c.subscribe(() => count1++)
143
- const unsub2 = c.subscribe(() => count2++)
144
-
145
- c.set(1)
146
- expect(count1).toBe(1)
147
- expect(count2).toBe(1)
148
-
149
- unsub2()
150
- c.set(2)
151
- expect(count1).toBe(2)
152
- expect(count2).toBe(1)
153
-
154
- unsub1()
155
- c.set(3)
156
- expect(count1).toBe(2)
157
- expect(count2).toBe(1)
158
- })
159
- })