@pyreon/reactivity 0.11.4 → 0.11.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.
@@ -1,18 +1,18 @@
1
- import { Cell, cell } from "../cell"
1
+ import { Cell, cell } from '../cell'
2
2
 
3
- describe("Cell", () => {
4
- test("stores and reads initial value", () => {
3
+ describe('Cell', () => {
4
+ test('stores and reads initial value', () => {
5
5
  const c = cell(42)
6
6
  expect(c.peek()).toBe(42)
7
7
  })
8
8
 
9
- test("set() updates value", () => {
10
- const c = cell("hello")
11
- c.set("world")
12
- expect(c.peek()).toBe("world")
9
+ test('set() updates value', () => {
10
+ const c = cell('hello')
11
+ c.set('world')
12
+ expect(c.peek()).toBe('world')
13
13
  })
14
14
 
15
- test("set() skips when value is the same (Object.is)", () => {
15
+ test('set() skips when value is the same (Object.is)', () => {
16
16
  const c = cell(1)
17
17
  let calls = 0
18
18
  c.listen(() => calls++)
@@ -20,23 +20,23 @@ describe("Cell", () => {
20
20
  expect(calls).toBe(0)
21
21
  })
22
22
 
23
- test("update() applies function to current value", () => {
23
+ test('update() applies function to current value', () => {
24
24
  const c = cell(10)
25
25
  c.update((v) => v + 5)
26
26
  expect(c.peek()).toBe(15)
27
27
  })
28
28
 
29
- test("listen() fires on set()", () => {
30
- const c = cell("a")
29
+ test('listen() fires on set()', () => {
30
+ const c = cell('a')
31
31
  let fired = false
32
32
  c.listen(() => {
33
33
  fired = true
34
34
  })
35
- c.set("b")
35
+ c.set('b')
36
36
  expect(fired).toBe(true)
37
37
  })
38
38
 
39
- test("listen() single-listener fast path (no Set allocated)", () => {
39
+ test('listen() single-listener fast path (no Set allocated)', () => {
40
40
  const c = cell(0)
41
41
  let count = 0
42
42
  c.listen(() => count++)
@@ -47,7 +47,7 @@ describe("Cell", () => {
47
47
  expect(count).toBe(1)
48
48
  })
49
49
 
50
- test("listen() promotes to Set with multiple listeners", () => {
50
+ test('listen() promotes to Set with multiple listeners', () => {
51
51
  const c = cell(0)
52
52
  let a = 0
53
53
  let b = 0
@@ -60,7 +60,7 @@ describe("Cell", () => {
60
60
  expect(b).toBe(1)
61
61
  })
62
62
 
63
- test("subscribe() returns working unsubscribe (single listener)", () => {
63
+ test('subscribe() returns working unsubscribe (single listener)', () => {
64
64
  const c = cell(0)
65
65
  let count = 0
66
66
  const unsub = c.subscribe(() => count++)
@@ -71,7 +71,7 @@ describe("Cell", () => {
71
71
  expect(count).toBe(1) // no more notifications
72
72
  })
73
73
 
74
- test("subscribe() returns working unsubscribe (multi listener)", () => {
74
+ test('subscribe() returns working unsubscribe (multi listener)', () => {
75
75
  const c = cell(0)
76
76
  let a = 0
77
77
  let b = 0
@@ -86,12 +86,12 @@ describe("Cell", () => {
86
86
  expect(b).toBe(1) // unsubscribed
87
87
  })
88
88
 
89
- test("cell() factory returns Cell instance", () => {
90
- const c = cell("x")
89
+ test('cell() factory returns Cell instance', () => {
90
+ const c = cell('x')
91
91
  expect(c).toBeInstanceOf(Cell)
92
92
  })
93
93
 
94
- test("multiple rapid updates notify correctly", () => {
94
+ test('multiple rapid updates notify correctly', () => {
95
95
  const c = cell(0)
96
96
  const values: number[] = []
97
97
  c.listen(() => values.push(c.peek()))
@@ -101,7 +101,7 @@ describe("Cell", () => {
101
101
  expect(values).toEqual([1, 2, 3])
102
102
  })
103
103
 
104
- test("NaN equality (Object.is)", () => {
104
+ test('NaN equality (Object.is)', () => {
105
105
  const c = cell(Number.NaN)
106
106
  let calls = 0
107
107
  c.listen(() => calls++)
@@ -109,7 +109,7 @@ describe("Cell", () => {
109
109
  expect(calls).toBe(0) // Object.is(NaN, NaN) is true
110
110
  })
111
111
 
112
- test("subscribe() unsubscribe works after promotion to Set (regression)", () => {
112
+ test('subscribe() unsubscribe works after promotion to Set (regression)', () => {
113
113
  // Bug: first subscriber's disposer became stale after second subscriber
114
114
  // promoted _l → _s. The disposer checked _l which was now null.
115
115
  const c = cell(0)
@@ -135,7 +135,7 @@ describe("Cell", () => {
135
135
  expect(count2).toBe(2)
136
136
  })
137
137
 
138
- test("subscribe() unsubscribe order: second before first", () => {
138
+ test('subscribe() unsubscribe order: second before first', () => {
139
139
  const c = cell(0)
140
140
  let count1 = 0
141
141
  let count2 = 0
@@ -1,15 +1,15 @@
1
- import { computed } from "../computed"
2
- import { effect } from "../effect"
3
- import { signal } from "../signal"
1
+ import { computed } from '../computed'
2
+ import { effect } from '../effect'
3
+ import { signal } from '../signal'
4
4
 
5
- describe("computed", () => {
6
- test("computes derived value", () => {
5
+ describe('computed', () => {
6
+ test('computes derived value', () => {
7
7
  const s = signal(2)
8
8
  const doubled = computed(() => s() * 2)
9
9
  expect(doubled()).toBe(4)
10
10
  })
11
11
 
12
- test("updates when dependency changes", () => {
12
+ test('updates when dependency changes', () => {
13
13
  const s = signal(3)
14
14
  const tripled = computed(() => s() * 3)
15
15
  expect(tripled()).toBe(9)
@@ -17,7 +17,7 @@ describe("computed", () => {
17
17
  expect(tripled()).toBe(12)
18
18
  })
19
19
 
20
- test("is lazy — does not compute until read", () => {
20
+ test('is lazy — does not compute until read', () => {
21
21
  let computations = 0
22
22
  const s = signal(0)
23
23
  const c = computed(() => {
@@ -29,7 +29,7 @@ describe("computed", () => {
29
29
  expect(computations).toBe(1)
30
30
  })
31
31
 
32
- test("is memoized — does not recompute on repeated reads", () => {
32
+ test('is memoized — does not recompute on repeated reads', () => {
33
33
  let computations = 0
34
34
  const s = signal(5)
35
35
  const c = computed(() => {
@@ -42,7 +42,7 @@ describe("computed", () => {
42
42
  expect(computations).toBe(1)
43
43
  })
44
44
 
45
- test("recomputes only when dirty", () => {
45
+ test('recomputes only when dirty', () => {
46
46
  let computations = 0
47
47
  const s = signal(1)
48
48
  const c = computed(() => {
@@ -58,7 +58,7 @@ describe("computed", () => {
58
58
  expect(computations).toBe(2) // still memoized
59
59
  })
60
60
 
61
- test("chains correctly", () => {
61
+ test('chains correctly', () => {
62
62
  const base = signal(2)
63
63
  const doubled = computed(() => base() * 2)
64
64
  const quadrupled = computed(() => doubled() * 2)
@@ -67,7 +67,7 @@ describe("computed", () => {
67
67
  expect(quadrupled()).toBe(12)
68
68
  })
69
69
 
70
- test("dispose stops recomputation", () => {
70
+ test('dispose stops recomputation', () => {
71
71
  const s = signal(1)
72
72
  let computations = 0
73
73
  const c = computed(() => {
@@ -82,7 +82,7 @@ describe("computed", () => {
82
82
  // (the computed is no longer subscribed to s)
83
83
  })
84
84
 
85
- test("custom equals skips downstream notification when equal", () => {
85
+ test('custom equals skips downstream notification when equal', () => {
86
86
  const s = signal(3)
87
87
  let downstream = 0
88
88
 
@@ -106,7 +106,7 @@ describe("computed", () => {
106
106
  expect(c()).toBe(1)
107
107
  })
108
108
 
109
- test("custom equals with array comparison", () => {
109
+ test('custom equals with array comparison', () => {
110
110
  const items = signal([1, 2, 3])
111
111
  let downstream = 0
112
112
 
@@ -130,7 +130,7 @@ describe("computed", () => {
130
130
  expect(downstream).toBe(2)
131
131
  })
132
132
 
133
- test("computed used as dependency inside an effect (subscribe path)", () => {
133
+ test('computed used as dependency inside an effect (subscribe path)', () => {
134
134
  const s = signal(10)
135
135
  const c = computed(() => s() + 1)
136
136
  let result = 0
@@ -144,7 +144,7 @@ describe("computed", () => {
144
144
  expect(result).toBe(21)
145
145
  })
146
146
 
147
- test("._v returns cached value", () => {
147
+ test('._v returns cached value', () => {
148
148
  const s = signal(5)
149
149
  const doubled = computed(() => s() * 2)
150
150
  // First access triggers computation
@@ -154,7 +154,7 @@ describe("computed", () => {
154
154
  expect(doubled._v).toBe(14)
155
155
  })
156
156
 
157
- test(".direct() fires updater on recompute", () => {
157
+ test('.direct() fires updater on recompute', () => {
158
158
  const s = signal(1)
159
159
  const doubled = computed(() => s() * 2)
160
160
  doubled() // initialize
@@ -176,7 +176,7 @@ describe("computed", () => {
176
176
  expect(called).toBe(2) // disposed, no more calls
177
177
  })
178
178
 
179
- test(".direct() works with equals option", () => {
179
+ test('.direct() works with equals option', () => {
180
180
  const s = signal(1)
181
181
  const clamped = computed(() => Math.min(s(), 10), {
182
182
  equals: (a, b) => a === b,
@@ -198,8 +198,8 @@ describe("computed", () => {
198
198
  expect(called).toBe(2) // equals suppresses
199
199
  })
200
200
 
201
- describe("_v with equals after disposal", () => {
202
- test("_v returns last cached value after dispose()", () => {
201
+ describe('_v with equals after disposal', () => {
202
+ test('_v returns last cached value after dispose()', () => {
203
203
  const s = signal(5)
204
204
  const doubled = computed(() => s() * 2)
205
205
  expect(doubled._v).toBe(10) // triggers initial computation
@@ -215,7 +215,7 @@ describe("computed", () => {
215
215
  expect(doubled._v).toBe(14)
216
216
  })
217
217
 
218
- test("computed with equals: _v only updates when equality check fails", () => {
218
+ test('computed with equals: _v only updates when equality check fails', () => {
219
219
  const s = signal(3)
220
220
  const floored = computed(() => Math.floor(s() / 10), {
221
221
  equals: (a, b) => a === b,
@@ -233,7 +233,7 @@ describe("computed", () => {
233
233
  expect(floored._v).toBe(1)
234
234
  })
235
235
 
236
- test("multiple .direct() updaters on computed, dispose one", () => {
236
+ test('multiple .direct() updaters on computed, dispose one', () => {
237
237
  const s = signal(1)
238
238
  const doubled = computed(() => s() * 2)
239
239
  doubled() // initialize
@@ -267,8 +267,8 @@ describe("computed", () => {
267
267
  })
268
268
  })
269
269
 
270
- describe("diamond pattern cleanup", () => {
271
- test("a -> b, c -> d diamond: d only recomputes once per a change", () => {
270
+ describe('diamond pattern cleanup', () => {
271
+ test('a -> b, c -> d diamond: d only recomputes once per a change', () => {
272
272
  const a = signal(1)
273
273
  const b = computed(() => a() + 1)
274
274
  const c = computed(() => a() + 2)
@@ -288,7 +288,7 @@ describe("computed", () => {
288
288
  expect(dComputations).toBe(2)
289
289
  })
290
290
 
291
- test("dispose middle node in diamond, verify no stale subscriptions", () => {
291
+ test('dispose middle node in diamond, verify no stale subscriptions', () => {
292
292
  const a = signal(1)
293
293
  const b = computed(() => a() * 2)
294
294
  const c = computed(() => a() * 3)
@@ -1,9 +1,9 @@
1
- import { createSelector } from "../createSelector"
2
- import { effect } from "../effect"
3
- import { signal } from "../signal"
1
+ import { createSelector } from '../createSelector'
2
+ import { effect } from '../effect'
3
+ import { signal } from '../signal'
4
4
 
5
- describe("createSelector", () => {
6
- test("returns true for the currently selected value", () => {
5
+ describe('createSelector', () => {
6
+ test('returns true for the currently selected value', () => {
7
7
  const selected = signal(1)
8
8
  const isSelected = createSelector(() => selected())
9
9
 
@@ -14,7 +14,7 @@ describe("createSelector", () => {
14
14
  expect(result).toBe(true)
15
15
  })
16
16
 
17
- test("returns false for non-selected values", () => {
17
+ test('returns false for non-selected values', () => {
18
18
  const selected = signal(1)
19
19
  const isSelected = createSelector(() => selected())
20
20
 
@@ -25,7 +25,7 @@ describe("createSelector", () => {
25
25
  expect(result).toBe(false)
26
26
  })
27
27
 
28
- test("only notifies affected subscribers when selection changes", () => {
28
+ test('only notifies affected subscribers when selection changes', () => {
29
29
  const selected = signal(1)
30
30
  const isSelected = createSelector(() => selected())
31
31
 
@@ -57,7 +57,7 @@ describe("createSelector", () => {
57
57
  expect(runs3).toBe(1) // unaffected
58
58
  })
59
59
 
60
- test("does not notify when source changes to the same value", () => {
60
+ test('does not notify when source changes to the same value', () => {
61
61
  const selected = signal(1)
62
62
  const isSelected = createSelector(() => selected())
63
63
 
@@ -71,7 +71,7 @@ describe("createSelector", () => {
71
71
  expect(runs).toBe(1)
72
72
  })
73
73
 
74
- test("works when changing to a value with no subscribers", () => {
74
+ test('works when changing to a value with no subscribers', () => {
75
75
  const selected = signal(1)
76
76
  const isSelected = createSelector(() => selected())
77
77
 
@@ -86,7 +86,7 @@ describe("createSelector", () => {
86
86
  expect(runs).toBe(2) // old bucket (1) notified
87
87
  })
88
88
 
89
- test("reuses host objects for the same value", () => {
89
+ test('reuses host objects for the same value', () => {
90
90
  const selected = signal(1)
91
91
  const isSelected = createSelector(() => selected())
92
92
 
@@ -108,7 +108,7 @@ describe("createSelector", () => {
108
108
  expect(result2).toBe(false)
109
109
  })
110
110
 
111
- test("tracks correctly outside an effect (no activeEffect)", () => {
111
+ test('tracks correctly outside an effect (no activeEffect)', () => {
112
112
  const selected = signal(1)
113
113
  const isSelected = createSelector(() => selected())
114
114
 
@@ -117,8 +117,8 @@ describe("createSelector", () => {
117
117
  expect(isSelected(2)).toBe(false)
118
118
  })
119
119
 
120
- describe("large subscriber sets", () => {
121
- test("many subscribers (20+), only affected buckets notified", () => {
120
+ describe('large subscriber sets', () => {
121
+ test('many subscribers (20+), only affected buckets notified', () => {
122
122
  const selected = signal(0)
123
123
  const isSelected = createSelector(() => selected())
124
124
 
@@ -151,8 +151,8 @@ describe("createSelector", () => {
151
151
  expect(unaffectedAfterSecond).toBe(true)
152
152
  })
153
153
 
154
- test("selector with undefined and null values", () => {
155
- const selected = signal<string | null | undefined>("a")
154
+ test('selector with undefined and null values', () => {
155
+ const selected = signal<string | null | undefined>('a')
156
156
  const isSelected = createSelector(() => selected())
157
157
 
158
158
  let nullRuns = 0
@@ -168,7 +168,7 @@ describe("createSelector", () => {
168
168
  undefRuns++
169
169
  })
170
170
  effect(() => {
171
- isSelected("a")
171
+ isSelected('a')
172
172
  aRuns++
173
173
  })
174
174
 
@@ -189,7 +189,7 @@ describe("createSelector", () => {
189
189
  expect(aRuns).toBe(2) // unaffected
190
190
  })
191
191
 
192
- test("rapid selector changes", () => {
192
+ test('rapid selector changes', () => {
193
193
  const selected = signal(0)
194
194
  const isSelected = createSelector(() => selected())
195
195
 
@@ -1,35 +1,35 @@
1
- import { _notifyTraceListeners, inspectSignal, isTracing, onSignalUpdate, why } from "../debug"
2
- import { signal } from "../signal"
1
+ import { _notifyTraceListeners, inspectSignal, isTracing, onSignalUpdate, why } from '../debug'
2
+ import { signal } from '../signal'
3
3
 
4
- describe("debug", () => {
5
- describe("onSignalUpdate / isTracing", () => {
6
- test("isTracing is false by default", () => {
4
+ describe('debug', () => {
5
+ describe('onSignalUpdate / isTracing', () => {
6
+ test('isTracing is false by default', () => {
7
7
  expect(isTracing()).toBe(false)
8
8
  })
9
9
 
10
- test("registering a listener enables tracing", () => {
10
+ test('registering a listener enables tracing', () => {
11
11
  const dispose = onSignalUpdate(() => {})
12
12
  expect(isTracing()).toBe(true)
13
13
  dispose()
14
14
  expect(isTracing()).toBe(false)
15
15
  })
16
16
 
17
- test("listener receives signal update events", () => {
17
+ test('listener receives signal update events', () => {
18
18
  const events: { name: string | undefined; prev: unknown; next: unknown }[] = []
19
19
  const dispose = onSignalUpdate((e) => {
20
20
  events.push({ name: e.name, prev: e.prev, next: e.next })
21
21
  })
22
22
 
23
- const s = signal(1, { name: "count" })
23
+ const s = signal(1, { name: 'count' })
24
24
  s.set(2)
25
25
 
26
26
  expect(events.length).toBe(1)
27
- expect(events[0]).toEqual({ name: "count", prev: 1, next: 2 })
27
+ expect(events[0]).toEqual({ name: 'count', prev: 1, next: 2 })
28
28
 
29
29
  dispose()
30
30
  })
31
31
 
32
- test("dispose removes only the specific listener", () => {
32
+ test('dispose removes only the specific listener', () => {
33
33
  let calls1 = 0
34
34
  let calls2 = 0
35
35
  const dispose1 = onSignalUpdate(() => calls1++)
@@ -50,20 +50,20 @@ describe("debug", () => {
50
50
  expect(isTracing()).toBe(false)
51
51
  })
52
52
 
53
- test("dispose is safe to call when listeners already null", () => {
53
+ test('dispose is safe to call when listeners already null', () => {
54
54
  const dispose = onSignalUpdate(() => {})
55
55
  dispose()
56
56
  expect(isTracing()).toBe(false)
57
57
  dispose() // should not throw — _traceListeners is null
58
58
  })
59
59
 
60
- test("_notifyTraceListeners does nothing when no listeners", () => {
60
+ test('_notifyTraceListeners does nothing when no listeners', () => {
61
61
  const s = signal(0)
62
62
  // Should not throw
63
63
  _notifyTraceListeners(s, 0, 1)
64
64
  })
65
65
 
66
- test("event includes stack and timestamp", () => {
66
+ test('event includes stack and timestamp', () => {
67
67
  let event: { stack: string; timestamp: number } | undefined
68
68
  const dispose = onSignalUpdate((e) => {
69
69
  event = { stack: e.stack, timestamp: e.timestamp }
@@ -73,20 +73,20 @@ describe("debug", () => {
73
73
  s.set(1)
74
74
 
75
75
  expect(event).toBeDefined()
76
- expect(typeof event?.stack).toBe("string")
77
- expect(typeof event?.timestamp).toBe("number")
76
+ expect(typeof event?.stack).toBe('string')
77
+ expect(typeof event?.timestamp).toBe('number')
78
78
 
79
79
  dispose()
80
80
  })
81
81
  })
82
82
 
83
- describe("why", () => {
84
- test("logs signal updates to console", async () => {
83
+ describe('why', () => {
84
+ test('logs signal updates to console', async () => {
85
85
  const logs: unknown[][] = []
86
86
  const origLog = console.log
87
87
  console.log = (...args: unknown[]) => logs.push(args)
88
88
 
89
- const s = signal(1, { name: "test" })
89
+ const s = signal(1, { name: 'test' })
90
90
  why()
91
91
  s.set(2)
92
92
 
@@ -109,21 +109,21 @@ describe("debug", () => {
109
109
 
110
110
  const noUpdateLog =
111
111
  logs.find((args) =>
112
- typeof args[0] === "string" ? args[0].includes("No signal") : false,
112
+ typeof args[0] === 'string' ? args[0].includes('No signal') : false,
113
113
  ) ||
114
- logs.find((args) => (typeof args[1] === "string" ? args[1].includes("No signal") : false))
114
+ logs.find((args) => (typeof args[1] === 'string' ? args[1].includes('No signal') : false))
115
115
  expect(noUpdateLog).toBeDefined()
116
116
  console.log = origLog
117
117
  })
118
118
 
119
- test("calling why() twice is ignored (already active)", async () => {
119
+ test('calling why() twice is ignored (already active)', async () => {
120
120
  const logs: unknown[][] = []
121
121
  const origLog = console.log
122
122
  console.log = (...args: unknown[]) => logs.push(args)
123
123
 
124
124
  why()
125
125
  why() // should be ignored
126
- const s = signal(0, { name: "x" })
126
+ const s = signal(0, { name: 'x' })
127
127
  s.set(1)
128
128
 
129
129
  await new Promise((r) => queueMicrotask(() => r(undefined)))
@@ -131,7 +131,7 @@ describe("debug", () => {
131
131
  console.log = origLog
132
132
  })
133
133
 
134
- test("logs anonymous signal name when no name is set", async () => {
134
+ test('logs anonymous signal name when no name is set', async () => {
135
135
  const logs: unknown[][] = []
136
136
  const origLog = console.log
137
137
  console.log = (...args: unknown[]) => logs.push(args)
@@ -143,15 +143,15 @@ describe("debug", () => {
143
143
  await new Promise((r) => queueMicrotask(() => r(undefined)))
144
144
 
145
145
  const anonLog = logs.find((args) =>
146
- args.some((a) => typeof a === "string" && a.includes("anonymous")),
146
+ args.some((a) => typeof a === 'string' && a.includes('anonymous')),
147
147
  )
148
148
  expect(anonLog).toBeDefined()
149
149
  console.log = origLog
150
150
  })
151
151
  })
152
152
 
153
- describe("inspectSignal", () => {
154
- test("prints signal info and returns debug info", () => {
153
+ describe('inspectSignal', () => {
154
+ test('prints signal info and returns debug info', () => {
155
155
  const groupCalls: unknown[][] = []
156
156
  const logCalls: unknown[][] = []
157
157
  const origGroup = console.group
@@ -161,10 +161,10 @@ describe("debug", () => {
161
161
  console.log = (...args: unknown[]) => logCalls.push(args)
162
162
  console.groupEnd = () => {}
163
163
 
164
- const s = signal(42, { name: "count" })
164
+ const s = signal(42, { name: 'count' })
165
165
  const info = inspectSignal(s)
166
166
 
167
- expect(info.name).toBe("count")
167
+ expect(info.name).toBe('count')
168
168
  expect(info.value).toBe(42)
169
169
  expect(info.subscriberCount).toBe(0)
170
170
  expect(groupCalls.length).toBe(1)
@@ -175,7 +175,7 @@ describe("debug", () => {
175
175
  console.groupEnd = origEnd
176
176
  })
177
177
 
178
- test("handles anonymous signal", () => {
178
+ test('handles anonymous signal', () => {
179
179
  const origGroup = console.group
180
180
  const origLog = console.log
181
181
  const origEnd = console.groupEnd