@pyreon/reactivity 0.11.5 → 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.
- package/README.md +2 -2
- package/lib/index.js.map +1 -1
- package/package.json +10 -10
- package/src/computed.ts +5 -5
- package/src/createSelector.ts +2 -2
- package/src/debug.ts +8 -8
- package/src/effect.ts +4 -4
- package/src/index.ts +13 -13
- package/src/reconcile.ts +4 -4
- package/src/resource.ts +4 -4
- package/src/scope.ts +1 -1
- package/src/signal.ts +7 -7
- package/src/store.ts +9 -9
- package/src/tests/batch.test.ts +16 -16
- package/src/tests/bind.test.ts +8 -8
- package/src/tests/branches.test.ts +66 -66
- package/src/tests/cell.test.ts +22 -22
- package/src/tests/computed.test.ts +24 -24
- package/src/tests/createSelector.test.ts +17 -17
- package/src/tests/debug.test.ts +29 -29
- package/src/tests/effect.test.ts +59 -59
- package/src/tests/resource.test.ts +39 -39
- package/src/tests/scope.test.ts +20 -20
- package/src/tests/signal.test.ts +39 -39
- package/src/tests/store.test.ts +64 -64
- package/src/tests/tracking.test.ts +17 -17
- package/src/tests/watch.test.ts +32 -32
- package/src/tracking.ts +1 -1
- package/src/watch.ts +3 -3
package/src/signal.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
2
2
|
declare const process: { env: { NODE_ENV?: string } } | undefined
|
|
3
3
|
|
|
4
|
-
const __DEV__ = typeof process !==
|
|
4
|
+
const __DEV__ = typeof process !== 'undefined' && process?.env?.NODE_ENV !== 'production'
|
|
5
5
|
|
|
6
|
-
import { enqueuePendingNotification, isBatching } from
|
|
7
|
-
import { _notifyTraceListeners, isTracing } from
|
|
8
|
-
import { notifySubscribers, trackSubscriber } from
|
|
6
|
+
import { enqueuePendingNotification, isBatching } from './batch'
|
|
7
|
+
import { _notifyTraceListeners, isTracing } from './debug'
|
|
8
|
+
import { notifySubscribers, trackSubscriber } from './tracking'
|
|
9
9
|
|
|
10
10
|
export interface SignalDebugInfo<T> {
|
|
11
11
|
/** Signal name (set via options or inferred) */
|
|
@@ -153,9 +153,9 @@ export function signal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
|
|
|
153
153
|
if (__DEV__ && args.length > 0) {
|
|
154
154
|
// biome-ignore lint/suspicious/noConsole: dev-only signal misuse warning
|
|
155
155
|
console.warn(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
'[Pyreon] signal() was called with an argument. ' +
|
|
157
|
+
'Use signal.set(value) or signal.update(fn) to write. ' +
|
|
158
|
+
'signal(value) only reads — the argument is ignored.',
|
|
159
159
|
)
|
|
160
160
|
}
|
|
161
161
|
trackSubscriber(read as SignalFn<T>)
|
package/src/store.ts
CHANGED
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
* state.items[0].text = "world" // only text-tracking effects re-run
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { type Signal, signal } from
|
|
16
|
+
import { type Signal, signal } from './signal'
|
|
17
17
|
|
|
18
18
|
// WeakMap: raw object → its reactive proxy (ensures each raw object gets one proxy)
|
|
19
19
|
const proxyCache = new WeakMap<object, object>()
|
|
20
20
|
|
|
21
|
-
const IS_STORE = Symbol(
|
|
21
|
+
const IS_STORE = Symbol('pyreon.store')
|
|
22
22
|
|
|
23
23
|
/** Returns true if the value is a createStore proxy. */
|
|
24
24
|
export function isStore(value: unknown): boolean {
|
|
25
25
|
return (
|
|
26
26
|
value !== null &&
|
|
27
|
-
typeof value ===
|
|
27
|
+
typeof value === 'object' &&
|
|
28
28
|
(value as Record<symbol, unknown>)[IS_STORE] === true
|
|
29
29
|
)
|
|
30
30
|
}
|
|
@@ -58,10 +58,10 @@ function wrap(raw: object): object {
|
|
|
58
58
|
get(target, key) {
|
|
59
59
|
// Pass through the identity marker and non-string/number keys (symbols, etc.)
|
|
60
60
|
if (key === IS_STORE) return true
|
|
61
|
-
if (typeof key ===
|
|
61
|
+
if (typeof key === 'symbol') return (target as Record<symbol, unknown>)[key]
|
|
62
62
|
|
|
63
63
|
// Array length — tracked via dedicated signal for push/pop/splice reactivity
|
|
64
|
-
if (isArray && key ===
|
|
64
|
+
if (isArray && key === 'length') return lengthSig?.()
|
|
65
65
|
|
|
66
66
|
// Non-own properties: prototype methods (forEach, map, push, …)
|
|
67
67
|
// These must be returned untracked so array methods work normally.
|
|
@@ -74,7 +74,7 @@ function wrap(raw: object): object {
|
|
|
74
74
|
const value = getOrCreateSignal(key)()
|
|
75
75
|
|
|
76
76
|
// Deep reactivity: wrap nested objects/arrays transparently
|
|
77
|
-
if (value !== null && typeof value ===
|
|
77
|
+
if (value !== null && typeof value === 'object') {
|
|
78
78
|
return wrap(value as object)
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -82,7 +82,7 @@ function wrap(raw: object): object {
|
|
|
82
82
|
},
|
|
83
83
|
|
|
84
84
|
set(target, key, value) {
|
|
85
|
-
if (typeof key ===
|
|
85
|
+
if (typeof key === 'symbol') {
|
|
86
86
|
;(target as Record<symbol, unknown>)[key] = value
|
|
87
87
|
return true
|
|
88
88
|
}
|
|
@@ -91,7 +91,7 @@ function wrap(raw: object): object {
|
|
|
91
91
|
;(target as Record<PropertyKey, unknown>)[key] = value
|
|
92
92
|
|
|
93
93
|
// Array length set directly (e.g. arr.length = 0)
|
|
94
|
-
if (isArray && key ===
|
|
94
|
+
if (isArray && key === 'length') {
|
|
95
95
|
lengthSig?.set(value as number)
|
|
96
96
|
return true
|
|
97
97
|
}
|
|
@@ -113,7 +113,7 @@ function wrap(raw: object): object {
|
|
|
113
113
|
|
|
114
114
|
deleteProperty(target, key) {
|
|
115
115
|
delete (target as Record<PropertyKey, unknown>)[key]
|
|
116
|
-
if (typeof key !==
|
|
116
|
+
if (typeof key !== 'symbol' && propSignals.has(key)) {
|
|
117
117
|
propSignals.get(key)?.set(undefined)
|
|
118
118
|
propSignals.delete(key)
|
|
119
119
|
}
|
package/src/tests/batch.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { batch, nextTick } from
|
|
2
|
-
import { effect } from
|
|
3
|
-
import { signal } from
|
|
1
|
+
import { batch, nextTick } from '../batch'
|
|
2
|
+
import { effect } from '../effect'
|
|
3
|
+
import { signal } from '../signal'
|
|
4
4
|
|
|
5
|
-
describe(
|
|
6
|
-
test(
|
|
5
|
+
describe('batch', () => {
|
|
6
|
+
test('defers notifications until end of batch', () => {
|
|
7
7
|
const a = signal(1)
|
|
8
8
|
const b = signal(2)
|
|
9
9
|
let runs = 0
|
|
@@ -22,7 +22,7 @@ describe("batch", () => {
|
|
|
22
22
|
expect(runs).toBe(2)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
test(
|
|
25
|
+
test('effect sees final values after batch', () => {
|
|
26
26
|
const s = signal(0)
|
|
27
27
|
let seen = 0
|
|
28
28
|
effect(() => {
|
|
@@ -36,7 +36,7 @@ describe("batch", () => {
|
|
|
36
36
|
expect(seen).toBe(3)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
test(
|
|
39
|
+
test('nested batches flush at outermost end', () => {
|
|
40
40
|
const s = signal(0)
|
|
41
41
|
let runs = 0
|
|
42
42
|
effect(() => {
|
|
@@ -55,7 +55,7 @@ describe("batch", () => {
|
|
|
55
55
|
expect(runs).toBe(2)
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
test(
|
|
58
|
+
test('batch propagates exceptions and still flushes', () => {
|
|
59
59
|
const s = signal(0)
|
|
60
60
|
let seen = 0
|
|
61
61
|
effect(() => {
|
|
@@ -66,15 +66,15 @@ describe("batch", () => {
|
|
|
66
66
|
expect(() => {
|
|
67
67
|
batch(() => {
|
|
68
68
|
s.set(42)
|
|
69
|
-
throw new Error(
|
|
69
|
+
throw new Error('boom')
|
|
70
70
|
})
|
|
71
|
-
}).toThrow(
|
|
71
|
+
}).toThrow('boom')
|
|
72
72
|
|
|
73
73
|
// The batch should still have flushed notifications in the finally block
|
|
74
74
|
expect(seen).toBe(42)
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
test(
|
|
77
|
+
test('batch with no signal changes is a no-op', () => {
|
|
78
78
|
let runs = 0
|
|
79
79
|
const s = signal(0)
|
|
80
80
|
effect(() => {
|
|
@@ -89,7 +89,7 @@ describe("batch", () => {
|
|
|
89
89
|
expect(runs).toBe(1)
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
test(
|
|
92
|
+
test('batch deduplicates same subscriber across multiple signals', () => {
|
|
93
93
|
const a = signal(1)
|
|
94
94
|
const b = signal(2)
|
|
95
95
|
let runs = 0
|
|
@@ -109,7 +109,7 @@ describe("batch", () => {
|
|
|
109
109
|
expect(runs).toBe(2)
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
test(
|
|
112
|
+
test('notifications enqueued during flush land in alternate set', () => {
|
|
113
113
|
const a = signal(0)
|
|
114
114
|
const b = signal(0)
|
|
115
115
|
const log: string[] = []
|
|
@@ -128,11 +128,11 @@ describe("batch", () => {
|
|
|
128
128
|
a.set(1)
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
-
expect(log).toContain(
|
|
132
|
-
expect(log).toContain(
|
|
131
|
+
expect(log).toContain('a=1')
|
|
132
|
+
expect(log).toContain('b=10')
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
-
test(
|
|
135
|
+
test('nextTick resolves after microtasks flush', async () => {
|
|
136
136
|
const s = signal(0)
|
|
137
137
|
let seen = 0
|
|
138
138
|
effect(() => {
|
package/src/tests/bind.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { _bind } from
|
|
2
|
-
import { signal } from
|
|
1
|
+
import { _bind } from '../effect'
|
|
2
|
+
import { signal } from '../signal'
|
|
3
3
|
|
|
4
|
-
describe(
|
|
5
|
-
test(
|
|
4
|
+
describe('_bind (static-dep binding)', () => {
|
|
5
|
+
test('runs the function on first call and tracks deps', () => {
|
|
6
6
|
const s = signal(0)
|
|
7
7
|
let runs = 0
|
|
8
8
|
|
|
@@ -20,7 +20,7 @@ describe("_bind (static-dep binding)", () => {
|
|
|
20
20
|
dispose()
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
test(
|
|
23
|
+
test('dispose stops re-runs', () => {
|
|
24
24
|
const s = signal(0)
|
|
25
25
|
let runs = 0
|
|
26
26
|
|
|
@@ -36,7 +36,7 @@ describe("_bind (static-dep binding)", () => {
|
|
|
36
36
|
expect(runs).toBe(1) // no re-run
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
test(
|
|
39
|
+
test('dispose is idempotent', () => {
|
|
40
40
|
const s = signal(0)
|
|
41
41
|
const dispose = _bind(() => {
|
|
42
42
|
s()
|
|
@@ -46,7 +46,7 @@ describe("_bind (static-dep binding)", () => {
|
|
|
46
46
|
dispose() // should not throw
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
test(
|
|
49
|
+
test('does not re-run after dispose even with multiple deps', () => {
|
|
50
50
|
const a = signal(0)
|
|
51
51
|
const b = signal(0)
|
|
52
52
|
let runs = 0
|
|
@@ -65,7 +65,7 @@ describe("_bind (static-dep binding)", () => {
|
|
|
65
65
|
expect(runs).toBe(1)
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
test(
|
|
68
|
+
test('disposed run callback is a no-op', () => {
|
|
69
69
|
const s = signal(0)
|
|
70
70
|
let runs = 0
|
|
71
71
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Targeted tests for uncovered branches across reactivity package.
|
|
3
3
|
*/
|
|
4
|
-
import { Cell } from
|
|
5
|
-
import { computed } from
|
|
6
|
-
import { createSelector } from
|
|
7
|
-
import { why } from
|
|
8
|
-
import { _bind, effect, renderEffect } from
|
|
9
|
-
import { reconcile } from
|
|
10
|
-
import { signal } from
|
|
11
|
-
import { createStore, isStore } from
|
|
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
12
|
|
|
13
13
|
// ── cell.ts branches: promote listener to Set ─────────────────────────────────
|
|
14
14
|
|
|
15
|
-
describe(
|
|
16
|
-
test(
|
|
15
|
+
describe('Cell listener promotion', () => {
|
|
16
|
+
test('promotes single listener to Set when second listener added', () => {
|
|
17
17
|
const c = new Cell(0)
|
|
18
18
|
const calls: number[] = []
|
|
19
19
|
c.listen(() => calls.push(1))
|
|
@@ -24,7 +24,7 @@ describe("Cell listener promotion", () => {
|
|
|
24
24
|
expect(calls).toEqual([1, 2, 3])
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
test(
|
|
27
|
+
test('subscribe unsubscribes single listener', () => {
|
|
28
28
|
const c = new Cell(0)
|
|
29
29
|
const calls: number[] = []
|
|
30
30
|
const unsub = c.subscribe(() => calls.push(1))
|
|
@@ -36,7 +36,7 @@ describe("Cell listener promotion", () => {
|
|
|
36
36
|
expect(calls).toEqual([1])
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
test(
|
|
39
|
+
test('subscribe unsubscribes from Set', () => {
|
|
40
40
|
const c = new Cell(0)
|
|
41
41
|
const calls: number[] = []
|
|
42
42
|
c.listen(() => calls.push(1))
|
|
@@ -48,7 +48,7 @@ describe("Cell listener promotion", () => {
|
|
|
48
48
|
expect(calls).toEqual([1, 2, 1])
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
test(
|
|
51
|
+
test('promote to Set when _l was unsubscribed (null _l, null _s)', () => {
|
|
52
52
|
const c = new Cell(0)
|
|
53
53
|
const fn1 = () => {}
|
|
54
54
|
// subscribe sets _l, unsub sets _l to null
|
|
@@ -62,7 +62,7 @@ describe("Cell listener promotion", () => {
|
|
|
62
62
|
c.set(1)
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
-
test(
|
|
65
|
+
test('double unsubscribe from single listener is safe', () => {
|
|
66
66
|
const c = new Cell(0)
|
|
67
67
|
const fn1 = () => {}
|
|
68
68
|
const unsub = c.subscribe(fn1)
|
|
@@ -74,8 +74,8 @@ describe("Cell listener promotion", () => {
|
|
|
74
74
|
|
|
75
75
|
// ── computed.ts branches ──────────────────────────────────────────────────────
|
|
76
76
|
|
|
77
|
-
describe(
|
|
78
|
-
test(
|
|
77
|
+
describe('computed branches', () => {
|
|
78
|
+
test('disposed computed does not recompute', () => {
|
|
79
79
|
const s = signal(1)
|
|
80
80
|
const c = computed(() => s() * 2, { equals: Object.is })
|
|
81
81
|
expect(c()).toBe(2)
|
|
@@ -85,7 +85,7 @@ describe("computed branches", () => {
|
|
|
85
85
|
// (it may return stale value or throw — just ensure no crash)
|
|
86
86
|
})
|
|
87
87
|
|
|
88
|
-
test(
|
|
88
|
+
test('computed with custom equals and subscribers', () => {
|
|
89
89
|
const s = signal(1)
|
|
90
90
|
const c = computed(() => s() * 2, { equals: Object.is })
|
|
91
91
|
const values: number[] = []
|
|
@@ -100,7 +100,7 @@ describe("computed branches", () => {
|
|
|
100
100
|
expect(values).toEqual([2, 4])
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
test(
|
|
103
|
+
test('computed without custom equals notifies subscribers on dep change', () => {
|
|
104
104
|
const s = signal(1)
|
|
105
105
|
const c = computed(() => s() * 2)
|
|
106
106
|
const values: number[] = []
|
|
@@ -115,73 +115,73 @@ describe("computed branches", () => {
|
|
|
115
115
|
|
|
116
116
|
// ── createSelector.ts branches ────────────────────────────────────────────────
|
|
117
117
|
|
|
118
|
-
describe(
|
|
119
|
-
test(
|
|
120
|
-
const s = signal<string>(
|
|
118
|
+
describe('createSelector branches', () => {
|
|
119
|
+
test('selector with no matching bucket on old value', () => {
|
|
120
|
+
const s = signal<string>('a')
|
|
121
121
|
const isSelected = createSelector(s)
|
|
122
122
|
// Read "a" — creates bucket for "a"
|
|
123
123
|
effect(() => {
|
|
124
|
-
isSelected(
|
|
124
|
+
isSelected('a')
|
|
125
125
|
})
|
|
126
126
|
// Change to "b" — old bucket "a" exists, new bucket "b" does not
|
|
127
|
-
s.set(
|
|
127
|
+
s.set('b')
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
-
test(
|
|
131
|
-
const s = signal<string>(
|
|
130
|
+
test('selector reuses existing host for same value', () => {
|
|
131
|
+
const s = signal<string>('a')
|
|
132
132
|
const isSelected = createSelector(s)
|
|
133
133
|
const results: boolean[] = []
|
|
134
134
|
effect(() => {
|
|
135
|
-
results.push(isSelected(
|
|
135
|
+
results.push(isSelected('a'))
|
|
136
136
|
})
|
|
137
137
|
// This second effect creates another subscription to same bucket
|
|
138
138
|
effect(() => {
|
|
139
|
-
results.push(isSelected(
|
|
139
|
+
results.push(isSelected('a'))
|
|
140
140
|
})
|
|
141
141
|
expect(results).toEqual([true, true])
|
|
142
|
-
s.set(
|
|
142
|
+
s.set('b')
|
|
143
143
|
// Both should see false
|
|
144
144
|
expect(results).toEqual([true, true, false, false])
|
|
145
145
|
})
|
|
146
146
|
|
|
147
|
-
test(
|
|
148
|
-
const s = signal<string>(
|
|
147
|
+
test('selector handles Object.is equality (no change)', () => {
|
|
148
|
+
const s = signal<string>('a')
|
|
149
149
|
const isSelected = createSelector(s)
|
|
150
150
|
let count = 0
|
|
151
151
|
effect(() => {
|
|
152
|
-
isSelected(
|
|
152
|
+
isSelected('a')
|
|
153
153
|
count++
|
|
154
154
|
})
|
|
155
155
|
expect(count).toBe(1)
|
|
156
156
|
// Same value — Object.is check should skip
|
|
157
|
-
s.set(
|
|
157
|
+
s.set('a')
|
|
158
158
|
expect(count).toBe(1)
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
test(
|
|
162
|
-
const s = signal<string>(
|
|
161
|
+
test('selector query for value with no existing bucket creates one', () => {
|
|
162
|
+
const s = signal<string>('a')
|
|
163
163
|
const isSelected = createSelector(s)
|
|
164
164
|
// Query outside effect — creates a bucket for "z" that has no subscribers
|
|
165
|
-
const result = isSelected(
|
|
165
|
+
const result = isSelected('z')
|
|
166
166
|
expect(result).toBe(false)
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
-
test(
|
|
170
|
-
const s = signal<string>(
|
|
169
|
+
test('selector change when old value has no subscriber bucket', () => {
|
|
170
|
+
const s = signal<string>('a')
|
|
171
171
|
const isSelected = createSelector(s)
|
|
172
172
|
// Only subscribe to "b", not "a"
|
|
173
173
|
effect(() => {
|
|
174
|
-
isSelected(
|
|
174
|
+
isSelected('b')
|
|
175
175
|
})
|
|
176
176
|
// Change from "a" to "b" — old value "a" has no bucket (never queried in effect)
|
|
177
|
-
s.set(
|
|
177
|
+
s.set('b')
|
|
178
178
|
})
|
|
179
179
|
})
|
|
180
180
|
|
|
181
181
|
// ── effect.ts branches ────────────────────────────────────────────────────────
|
|
182
182
|
|
|
183
|
-
describe(
|
|
184
|
-
test(
|
|
183
|
+
describe('effect disposed branches', () => {
|
|
184
|
+
test('disposed effect does not re-run', () => {
|
|
185
185
|
const s = signal(0)
|
|
186
186
|
let count = 0
|
|
187
187
|
const e = effect(() => {
|
|
@@ -194,7 +194,7 @@ describe("effect disposed branches", () => {
|
|
|
194
194
|
expect(count).toBe(1)
|
|
195
195
|
})
|
|
196
196
|
|
|
197
|
-
test(
|
|
197
|
+
test('disposed _bind does not re-run', () => {
|
|
198
198
|
const s = signal(0)
|
|
199
199
|
let count = 0
|
|
200
200
|
const dispose = _bind(() => {
|
|
@@ -209,7 +209,7 @@ describe("effect disposed branches", () => {
|
|
|
209
209
|
dispose()
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
-
test(
|
|
212
|
+
test('disposed renderEffect does not re-run', () => {
|
|
213
213
|
const s = signal(0)
|
|
214
214
|
let count = 0
|
|
215
215
|
const dispose = renderEffect(() => {
|
|
@@ -225,27 +225,27 @@ describe("effect disposed branches", () => {
|
|
|
225
225
|
|
|
226
226
|
// ── store.ts branches ─────────────────────────────────────────────────────────
|
|
227
227
|
|
|
228
|
-
describe(
|
|
229
|
-
test(
|
|
228
|
+
describe('store branches', () => {
|
|
229
|
+
test('setting symbol property', () => {
|
|
230
230
|
const store = createStore({ a: 1 })
|
|
231
|
-
const sym = Symbol(
|
|
232
|
-
;(store as Record<symbol, unknown>)[sym] =
|
|
233
|
-
expect((store as Record<symbol, unknown>)[sym]).toBe(
|
|
231
|
+
const sym = Symbol('test')
|
|
232
|
+
;(store as Record<symbol, unknown>)[sym] = 'hello'
|
|
233
|
+
expect((store as Record<symbol, unknown>)[sym]).toBe('hello')
|
|
234
234
|
})
|
|
235
235
|
|
|
236
|
-
test(
|
|
236
|
+
test('deleteProperty on store', () => {
|
|
237
237
|
const store = createStore<Record<string, unknown>>({ a: 1, b: 2 })
|
|
238
238
|
delete store.b
|
|
239
239
|
expect(store.b).toBeUndefined()
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
-
test(
|
|
242
|
+
test('deleteProperty on store array', () => {
|
|
243
243
|
const store = createStore([1, 2, 3])
|
|
244
|
-
delete (store as unknown as Record<string, unknown>)[
|
|
244
|
+
delete (store as unknown as Record<string, unknown>)['1']
|
|
245
245
|
expect(store[1]).toBeUndefined()
|
|
246
246
|
})
|
|
247
247
|
|
|
248
|
-
test(
|
|
248
|
+
test('deleteProperty on store with reactive subscriber', () => {
|
|
249
249
|
const store = createStore<Record<string, unknown>>({ a: 1, b: 2 })
|
|
250
250
|
// Read 'b' in effect to create propSignal
|
|
251
251
|
let val: unknown
|
|
@@ -261,14 +261,14 @@ describe("store branches", () => {
|
|
|
261
261
|
|
|
262
262
|
// ── reconcile.ts branches ─────────────────────────────────────────────────────
|
|
263
263
|
|
|
264
|
-
describe(
|
|
265
|
-
test(
|
|
264
|
+
describe('reconcile branches', () => {
|
|
265
|
+
test('reconcile array with non-object source items', () => {
|
|
266
266
|
const store = createStore([1, 2, 3])
|
|
267
267
|
reconcile([4, 5], store)
|
|
268
268
|
expect([...store]).toEqual([4, 5])
|
|
269
269
|
})
|
|
270
270
|
|
|
271
|
-
test(
|
|
271
|
+
test('reconcile object with raw (non-store) target value', () => {
|
|
272
272
|
// Create store where nested value isn't yet a store proxy
|
|
273
273
|
const store = createStore<Record<string, unknown>>({ a: 1 })
|
|
274
274
|
// Reconcile with nested object — target.a is a number (not store), so it takes the else branch
|
|
@@ -276,26 +276,26 @@ describe("reconcile branches", () => {
|
|
|
276
276
|
expect((store.a as Record<string, unknown>).nested).toBe(true)
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
-
test(
|
|
279
|
+
test('reconcile array with null source entries', () => {
|
|
280
280
|
const store = createStore([1, null, 3])
|
|
281
281
|
reconcile([null, 2, null], store)
|
|
282
282
|
expect([...store]).toEqual([null, 2, null])
|
|
283
283
|
})
|
|
284
284
|
|
|
285
|
-
test(
|
|
285
|
+
test('reconcile object with null source values', () => {
|
|
286
286
|
const store = createStore<Record<string, unknown>>({ a: { x: 1 }, b: 2 })
|
|
287
287
|
reconcile({ a: null, b: 2 }, store)
|
|
288
288
|
expect(store.a).toBeNull()
|
|
289
289
|
})
|
|
290
290
|
|
|
291
|
-
test(
|
|
291
|
+
test('reconcile array with both source and target as objects (recursive)', () => {
|
|
292
292
|
const store = createStore([{ a: 1 }, { b: 2 }])
|
|
293
293
|
reconcile([{ a: 10 }, { b: 20 }], store)
|
|
294
294
|
expect(store[0]?.a).toBe(10)
|
|
295
295
|
expect(store[1]?.b).toBe(20)
|
|
296
296
|
})
|
|
297
297
|
|
|
298
|
-
test(
|
|
298
|
+
test('reconcile object where target has store-proxied nested object', () => {
|
|
299
299
|
const store = createStore<Record<string, Record<string, number>>>({ nested: { x: 1 } })
|
|
300
300
|
// Access nested to ensure it's proxied as store
|
|
301
301
|
const _val = store.nested?.x
|
|
@@ -304,7 +304,7 @@ describe("reconcile branches", () => {
|
|
|
304
304
|
expect(store.nested?.x).toBe(99)
|
|
305
305
|
})
|
|
306
306
|
|
|
307
|
-
test(
|
|
307
|
+
test('reconcile object where target has raw (non-store) nested object', () => {
|
|
308
308
|
// Don't access nested, so it stays as raw object (not proxied)
|
|
309
309
|
const store = createStore<Record<string, Record<string, number>>>({ nested: { x: 1 } })
|
|
310
310
|
// nested has not been accessed via proxy, so isStore(target.nested) is false
|
|
@@ -316,25 +316,25 @@ describe("reconcile branches", () => {
|
|
|
316
316
|
|
|
317
317
|
// ── debug.ts branches ─────────────────────────────────────────────────────────
|
|
318
318
|
|
|
319
|
-
describe(
|
|
320
|
-
test(
|
|
321
|
-
const s = signal(0, { name:
|
|
319
|
+
describe('debug branches', () => {
|
|
320
|
+
test('why with exactly 1 subscriber shows singular', async () => {
|
|
321
|
+
const s = signal(0, { name: 'single' })
|
|
322
322
|
// Add exactly 1 subscriber
|
|
323
323
|
effect(() => {
|
|
324
324
|
s()
|
|
325
325
|
})
|
|
326
326
|
const logs: string[] = []
|
|
327
327
|
const origLog = console.log
|
|
328
|
-
console.log = (...args: unknown[]) => logs.push(args.join(
|
|
328
|
+
console.log = (...args: unknown[]) => logs.push(args.join(' '))
|
|
329
329
|
why()
|
|
330
330
|
s.set(1)
|
|
331
331
|
// why() auto-disposes via microtask
|
|
332
332
|
await new Promise((r) => setTimeout(r, 10))
|
|
333
333
|
console.log = origLog
|
|
334
|
-
expect(logs.some((l) => l.includes(
|
|
334
|
+
expect(logs.some((l) => l.includes('1 subscriber'))).toBe(true)
|
|
335
335
|
})
|
|
336
336
|
|
|
337
|
-
test(
|
|
337
|
+
test('_notifyTraceListeners with no active listeners is noop', () => {
|
|
338
338
|
// When no listeners registered, this should not throw
|
|
339
339
|
// (tests the early return / null check)
|
|
340
340
|
const s = signal(0)
|