@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,291 +0,0 @@
1
- import { createSelector } from '../createSelector'
2
- import { effect } from '../effect'
3
- import { signal } from '../signal'
4
-
5
- describe('createSelector', () => {
6
- test('returns true for the currently selected value', () => {
7
- const selected = signal(1)
8
- const isSelected = createSelector(() => selected())
9
-
10
- let result = false
11
- effect(() => {
12
- result = isSelected(1)
13
- })
14
- expect(result).toBe(true)
15
- })
16
-
17
- test('returns false for non-selected values', () => {
18
- const selected = signal(1)
19
- const isSelected = createSelector(() => selected())
20
-
21
- let result = true
22
- effect(() => {
23
- result = isSelected(2)
24
- })
25
- expect(result).toBe(false)
26
- })
27
-
28
- test('only notifies affected subscribers when selection changes', () => {
29
- const selected = signal(1)
30
- const isSelected = createSelector(() => selected())
31
-
32
- let runs1 = 0
33
- let runs2 = 0
34
- let runs3 = 0
35
-
36
- effect(() => {
37
- isSelected(1)
38
- runs1++
39
- })
40
- effect(() => {
41
- isSelected(2)
42
- runs2++
43
- })
44
- effect(() => {
45
- isSelected(3)
46
- runs3++
47
- })
48
-
49
- expect(runs1).toBe(1)
50
- expect(runs2).toBe(1)
51
- expect(runs3).toBe(1)
52
-
53
- // Change selection from 1 to 2: only buckets 1 (deselected) and 2 (newly selected) should fire
54
- selected.set(2)
55
- expect(runs1).toBe(2) // deselected
56
- expect(runs2).toBe(2) // newly selected
57
- expect(runs3).toBe(1) // unaffected
58
- })
59
-
60
- test('does not notify when source changes to the same value', () => {
61
- const selected = signal(1)
62
- const isSelected = createSelector(() => selected())
63
-
64
- let runs = 0
65
- effect(() => {
66
- isSelected(1)
67
- runs++
68
- })
69
-
70
- selected.set(1) // same value
71
- expect(runs).toBe(1)
72
- })
73
-
74
- test('works when changing to a value with no subscribers', () => {
75
- const selected = signal(1)
76
- const isSelected = createSelector(() => selected())
77
-
78
- let runs = 0
79
- effect(() => {
80
- isSelected(1)
81
- runs++
82
- })
83
-
84
- // Change to value 99 which has no subscriber bucket
85
- selected.set(99)
86
- expect(runs).toBe(2) // old bucket (1) notified
87
- })
88
-
89
- test('reuses host objects for the same value', () => {
90
- const selected = signal(1)
91
- const isSelected = createSelector(() => selected())
92
-
93
- let result1 = false
94
- let result2 = false
95
- effect(() => {
96
- result1 = isSelected(1)
97
- })
98
- // Second call with same value should reuse the host
99
- effect(() => {
100
- result2 = isSelected(1)
101
- })
102
-
103
- expect(result1).toBe(true)
104
- expect(result2).toBe(true)
105
-
106
- selected.set(2)
107
- expect(result1).toBe(false)
108
- expect(result2).toBe(false)
109
- })
110
-
111
- test('tracks correctly outside an effect (no activeEffect)', () => {
112
- const selected = signal(1)
113
- const isSelected = createSelector(() => selected())
114
-
115
- // Calling outside an effect should still return the correct boolean
116
- expect(isSelected(1)).toBe(true)
117
- expect(isSelected(2)).toBe(false)
118
- })
119
-
120
- describe('large subscriber sets', () => {
121
- test('many subscribers (20+), only affected buckets notified', () => {
122
- const selected = signal(0)
123
- const isSelected = createSelector(() => selected())
124
-
125
- const runCounts: number[] = []
126
- for (let i = 0; i < 25; i++) {
127
- const idx = i
128
- runCounts.push(0)
129
- effect(() => {
130
- isSelected(idx)
131
- runCounts[idx] = (runCounts[idx] ?? 0) + 1
132
- })
133
- }
134
-
135
- // All effects ran once
136
- const allOnes = runCounts.every((c) => c === 1)
137
- expect(allOnes).toBe(true)
138
-
139
- // Change from 0 to 5: only buckets 0 and 5 should re-run
140
- selected.set(5)
141
- expect(runCounts[0]).toBe(2) // deselected
142
- expect(runCounts[5]).toBe(2) // newly selected
143
- const unaffectedAfterFirst = runCounts.every((c, i) => i === 0 || i === 5 || c === 1)
144
- expect(unaffectedAfterFirst).toBe(true)
145
-
146
- // Change from 5 to 24: only buckets 5 and 24 re-run
147
- selected.set(24)
148
- expect(runCounts[5]).toBe(3)
149
- expect(runCounts[24]).toBe(2)
150
- const unaffectedAfterSecond = runCounts.every((c, i) => [0, 5, 24].includes(i) || c === 1)
151
- expect(unaffectedAfterSecond).toBe(true)
152
- })
153
-
154
- test('selector with undefined and null values', () => {
155
- const selected = signal<string | null | undefined>('a')
156
- const isSelected = createSelector(() => selected())
157
-
158
- let nullRuns = 0
159
- let undefRuns = 0
160
- let aRuns = 0
161
-
162
- effect(() => {
163
- isSelected(null)
164
- nullRuns++
165
- })
166
- effect(() => {
167
- isSelected(undefined)
168
- undefRuns++
169
- })
170
- effect(() => {
171
- isSelected('a')
172
- aRuns++
173
- })
174
-
175
- expect(nullRuns).toBe(1)
176
- expect(undefRuns).toBe(1)
177
- expect(aRuns).toBe(1)
178
-
179
- // Switch to null
180
- selected.set(null)
181
- expect(aRuns).toBe(2) // deselected
182
- expect(nullRuns).toBe(2) // newly selected
183
- expect(undefRuns).toBe(1) // unaffected
184
-
185
- // Switch to undefined
186
- selected.set(undefined)
187
- expect(nullRuns).toBe(3) // deselected
188
- expect(undefRuns).toBe(2) // newly selected
189
- expect(aRuns).toBe(2) // unaffected
190
- })
191
-
192
- test('rapid selector changes', () => {
193
- const selected = signal(0)
194
- const isSelected = createSelector(() => selected())
195
-
196
- let runs0 = 0
197
- let runs1 = 0
198
- let runs2 = 0
199
-
200
- effect(() => {
201
- isSelected(0)
202
- runs0++
203
- })
204
- effect(() => {
205
- isSelected(1)
206
- runs1++
207
- })
208
- effect(() => {
209
- isSelected(2)
210
- runs2++
211
- })
212
-
213
- // Rapid changes: 0 -> 1 -> 2 -> 0
214
- selected.set(1)
215
- selected.set(2)
216
- selected.set(0)
217
-
218
- // Each affected bucket should have been notified for each change
219
- // 0->1: runs0+1, runs1+1
220
- // 1->2: runs1+1, runs2+1
221
- // 2->0: runs2+1, runs0+1
222
- expect(runs0).toBe(3) // initial + deselected + reselected
223
- expect(runs1).toBe(3) // initial + selected + deselected
224
- expect(runs2).toBe(3) // initial + selected + deselected
225
-
226
- // Final state
227
- expect(isSelected(0)).toBe(true)
228
- expect(isSelected(1)).toBe(false)
229
- expect(isSelected(2)).toBe(false)
230
- })
231
- })
232
-
233
- // Regression: pre-fix, the source-tracking effect ran forever AND the
234
- // per-value subs/hosts Maps grew unboundedly. With dynamic value spaces
235
- // (UUIDs, ephemeral IDs) this leaked memory for the lifetime of the
236
- // program. dispose() now stops the effect AND clears both Maps.
237
- describe('dispose', () => {
238
- test('stops source-tracking after dispose', () => {
239
- const selected = signal(0)
240
- const isSelected = createSelector(() => selected())
241
- const runs: number[] = []
242
- effect(() => {
243
- runs.push(isSelected(1) ? 1 : 0)
244
- })
245
- expect(runs).toEqual([0])
246
- selected.set(1)
247
- expect(runs).toEqual([0, 1])
248
-
249
- isSelected.dispose()
250
- // Source change after dispose — the effect that tracks `isSelected(1)`
251
- // should NOT re-run because the selector's internal effect was stopped.
252
- selected.set(2)
253
- expect(runs).toEqual([0, 1])
254
- })
255
-
256
- test('post-dispose calls return last-known result without tracking', () => {
257
- const selected = signal(0)
258
- const isSelected = createSelector(() => selected())
259
- isSelected.dispose()
260
- // Last known value was 0 — calls return its match against the query.
261
- expect(isSelected(0)).toBe(true)
262
- expect(isSelected(1)).toBe(false)
263
- })
264
-
265
- test('dispose is idempotent', () => {
266
- const selected = signal(0)
267
- const isSelected = createSelector(() => selected())
268
- expect(() => {
269
- isSelected.dispose()
270
- isSelected.dispose()
271
- isSelected.dispose()
272
- }).not.toThrow()
273
- })
274
-
275
- test('post-dispose: source no longer propagates to selector', () => {
276
- // Observable consequence of stopping the internal effect — proves
277
- // the per-value Maps are no longer being populated either (because
278
- // the tracking path is bypassed entirely after dispose).
279
- const selected = signal('initial')
280
- const isSelected = createSelector(() => selected())
281
- // Build up the cache by querying many unique values.
282
- for (let i = 0; i < 100; i++) isSelected(`uuid-${i}`)
283
- isSelected.dispose()
284
- // After dispose, the source is no longer tracked — changing it
285
- // doesn't propagate to the selector's internal `current` value.
286
- selected.set('changed')
287
- expect(isSelected('initial')).toBe(true) // last-known wins
288
- expect(isSelected('changed')).toBe(false) // never propagated
289
- })
290
- })
291
- })
@@ -1,196 +0,0 @@
1
- import { _notifyTraceListeners, inspectSignal, isTracing, onSignalUpdate, why } from '../debug'
2
- import { signal } from '../signal'
3
-
4
- describe('debug', () => {
5
- describe('onSignalUpdate / isTracing', () => {
6
- test('isTracing is false by default', () => {
7
- expect(isTracing()).toBe(false)
8
- })
9
-
10
- test('registering a listener enables tracing', () => {
11
- const dispose = onSignalUpdate(() => {})
12
- expect(isTracing()).toBe(true)
13
- dispose()
14
- expect(isTracing()).toBe(false)
15
- })
16
-
17
- test('listener receives signal update events', () => {
18
- const events: { name: string | undefined; prev: unknown; next: unknown }[] = []
19
- const dispose = onSignalUpdate((e) => {
20
- events.push({ name: e.name, prev: e.prev, next: e.next })
21
- })
22
-
23
- const s = signal(1, { name: 'count' })
24
- s.set(2)
25
-
26
- expect(events.length).toBe(1)
27
- expect(events[0]).toEqual({ name: 'count', prev: 1, next: 2 })
28
-
29
- dispose()
30
- })
31
-
32
- test('dispose removes only the specific listener', () => {
33
- let calls1 = 0
34
- let calls2 = 0
35
- const dispose1 = onSignalUpdate(() => calls1++)
36
- const dispose2 = onSignalUpdate(() => calls2++)
37
-
38
- const s = signal(0)
39
- s.set(1)
40
- expect(calls1).toBe(1)
41
- expect(calls2).toBe(1)
42
-
43
- dispose1()
44
-
45
- s.set(2)
46
- expect(calls1).toBe(1) // removed
47
- expect(calls2).toBe(2) // still active
48
-
49
- dispose2()
50
- expect(isTracing()).toBe(false)
51
- })
52
-
53
- test('dispose is safe to call when listeners already null', () => {
54
- const dispose = onSignalUpdate(() => {})
55
- dispose()
56
- expect(isTracing()).toBe(false)
57
- dispose() // should not throw — _traceListeners is null
58
- })
59
-
60
- test('_notifyTraceListeners does nothing when no listeners', () => {
61
- const s = signal(0)
62
- // Should not throw
63
- _notifyTraceListeners(s, 0, 1)
64
- })
65
-
66
- test('event includes stack and timestamp', () => {
67
- let event: { stack: string; timestamp: number } | undefined
68
- const dispose = onSignalUpdate((e) => {
69
- event = { stack: e.stack, timestamp: e.timestamp }
70
- })
71
-
72
- const s = signal(0)
73
- s.set(1)
74
-
75
- expect(event).toBeDefined()
76
- expect(typeof event?.stack).toBe('string')
77
- expect(typeof event?.timestamp).toBe('number')
78
-
79
- dispose()
80
- })
81
- })
82
-
83
- describe('why', () => {
84
- test('logs signal updates to console', async () => {
85
- const logs: unknown[][] = []
86
- const origLog = console.log
87
- console.log = (...args: unknown[]) => logs.push(args)
88
-
89
- const s = signal(1, { name: 'test' })
90
- why()
91
- s.set(2)
92
-
93
- // Wait for microtask (auto-dispose)
94
- await new Promise((r) => queueMicrotask(() => r(undefined)))
95
-
96
- expect(logs.length).toBeGreaterThan(0)
97
- console.log = origLog
98
- })
99
-
100
- test("logs 'no updates' when nothing changes", async () => {
101
- const logs: unknown[][] = []
102
- const origLog = console.log
103
- console.log = (...args: unknown[]) => logs.push(args)
104
-
105
- why()
106
- // No signal updates
107
-
108
- await new Promise((r) => queueMicrotask(() => r(undefined)))
109
-
110
- const noUpdateLog =
111
- logs.find((args) =>
112
- typeof args[0] === 'string' ? args[0].includes('No signal') : false,
113
- ) ||
114
- logs.find((args) => (typeof args[1] === 'string' ? args[1].includes('No signal') : false))
115
- expect(noUpdateLog).toBeDefined()
116
- console.log = origLog
117
- })
118
-
119
- test('calling why() twice is ignored (already active)', async () => {
120
- const logs: unknown[][] = []
121
- const origLog = console.log
122
- console.log = (...args: unknown[]) => logs.push(args)
123
-
124
- why()
125
- why() // should be ignored
126
- const s = signal(0, { name: 'x' })
127
- s.set(1)
128
-
129
- await new Promise((r) => queueMicrotask(() => r(undefined)))
130
- // Should not throw or double-log
131
- console.log = origLog
132
- })
133
-
134
- test('logs anonymous signal name when no name is set', async () => {
135
- const logs: unknown[][] = []
136
- const origLog = console.log
137
- console.log = (...args: unknown[]) => logs.push(args)
138
-
139
- const s = signal(0) // no name
140
- why()
141
- s.set(1)
142
-
143
- await new Promise((r) => queueMicrotask(() => r(undefined)))
144
-
145
- const anonLog = logs.find((args) =>
146
- args.some((a) => typeof a === 'string' && a.includes('anonymous')),
147
- )
148
- expect(anonLog).toBeDefined()
149
- console.log = origLog
150
- })
151
- })
152
-
153
- describe('inspectSignal', () => {
154
- test('prints signal info and returns debug info', () => {
155
- const groupCalls: unknown[][] = []
156
- const logCalls: unknown[][] = []
157
- const origGroup = console.group
158
- const origLog = console.log
159
- const origEnd = console.groupEnd
160
- console.group = (...args: unknown[]) => groupCalls.push(args)
161
- console.log = (...args: unknown[]) => logCalls.push(args)
162
- console.groupEnd = () => {}
163
-
164
- const s = signal(42, { name: 'count' })
165
- const info = inspectSignal(s)
166
-
167
- expect(info.name).toBe('count')
168
- expect(info.value).toBe(42)
169
- expect(info.subscriberCount).toBe(0)
170
- expect(groupCalls.length).toBe(1)
171
- expect(logCalls.length).toBe(2) // value + subscribers
172
-
173
- console.group = origGroup
174
- console.log = origLog
175
- console.groupEnd = origEnd
176
- })
177
-
178
- test('handles anonymous signal', () => {
179
- const origGroup = console.group
180
- const origLog = console.log
181
- const origEnd = console.groupEnd
182
- console.group = () => {}
183
- console.log = () => {}
184
- console.groupEnd = () => {}
185
-
186
- const s = signal(0)
187
- const info = inspectSignal(s)
188
-
189
- expect(info.name).toBeUndefined()
190
-
191
- console.group = origGroup
192
- console.log = origLog
193
- console.groupEnd = origEnd
194
- })
195
- })
196
- })