@storve/core 1.0.1 → 1.0.3
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/LICENSE +21 -0
- package/README.md +993 -26
- package/dist/adapters/indexedDB.cjs +0 -1
- package/dist/adapters/indexedDB.mjs +0 -1
- package/dist/adapters/localStorage.cjs +0 -1
- package/dist/adapters/localStorage.mjs +0 -1
- package/dist/adapters/memory.cjs +0 -1
- package/dist/adapters/memory.mjs +0 -1
- package/dist/adapters/sessionStorage.cjs +0 -1
- package/dist/adapters/sessionStorage.mjs +0 -1
- package/dist/async-entry.d.ts +0 -1
- package/dist/async.cjs +0 -1
- package/dist/async.d.ts +0 -1
- package/dist/async.mjs +0 -1
- package/dist/batch.d.ts +0 -1
- package/dist/compose.d.ts +0 -1
- package/dist/computed-entry.d.ts +0 -1
- package/dist/computed.cjs +0 -1
- package/dist/computed.d.ts +0 -1
- package/dist/computed.mjs +0 -1
- package/dist/devtools/history.d.ts +0 -1
- package/dist/devtools/index.d.ts +0 -1
- package/dist/devtools/redux-bridge.d.ts +0 -1
- package/dist/devtools/snapshots.d.ts +0 -1
- package/dist/devtools/withDevtools.d.ts +0 -1
- package/dist/devtools.cjs +0 -1
- package/dist/devtools.mjs +0 -1
- package/dist/extensions/noop.d.ts +0 -1
- package/dist/index.cjs +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +0 -1
- package/dist/persist/adapters/indexedDB.d.ts +0 -1
- package/dist/persist/adapters/localStorage.d.ts +0 -1
- package/dist/persist/adapters/memory.d.ts +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts +0 -1
- package/dist/persist/debounce.d.ts +0 -1
- package/dist/persist/hydrate.d.ts +0 -1
- package/dist/persist/index.d.ts +0 -1
- package/dist/persist/serialize.d.ts +0 -1
- package/dist/persist.cjs +0 -1
- package/dist/persist.mjs +0 -1
- package/dist/proxy.d.ts +0 -1
- package/dist/registry-qtr1UpFU.js +0 -1
- package/dist/registry-zaKZ1P-s.js +0 -1
- package/dist/registry.d.ts +0 -1
- package/dist/signals/createSignal.d.ts +0 -1
- package/dist/signals/index.d.ts +0 -1
- package/dist/signals/useSignal.d.ts +0 -1
- package/dist/signals.cjs +0 -1
- package/dist/signals.mjs +0 -1
- package/dist/store.d.ts +0 -1
- package/dist/sync/channel.d.ts +0 -1
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/protocol.d.ts +0 -1
- package/dist/sync/withSync.d.ts +0 -1
- package/dist/sync.cjs +0 -1
- package/dist/sync.mjs +0 -1
- package/dist/types.d.ts +0 -1
- package/package.json +9 -3
- package/CHANGELOG.md +0 -151
- package/benchmarks/run.ts +0 -102
- package/benchmarks/week2.md +0 -9
- package/benchmarks/week2.ts +0 -64
- package/benchmarks/week4.md +0 -13
- package/benchmarks/week4.ts +0 -178
- package/benchmarks/week5.md +0 -15
- package/benchmarks/week5.ts +0 -184
- package/coverage/coverage-summary.json +0 -31
- package/dist/adapters/indexedDB.cjs.map +0 -1
- package/dist/adapters/indexedDB.mjs.map +0 -1
- package/dist/adapters/localStorage.cjs.map +0 -1
- package/dist/adapters/localStorage.mjs.map +0 -1
- package/dist/adapters/memory.cjs.map +0 -1
- package/dist/adapters/memory.mjs.map +0 -1
- package/dist/adapters/sessionStorage.cjs.map +0 -1
- package/dist/adapters/sessionStorage.mjs.map +0 -1
- package/dist/async-entry.d.ts.map +0 -1
- package/dist/async.cjs.map +0 -1
- package/dist/async.d.ts.map +0 -1
- package/dist/async.mjs.map +0 -1
- package/dist/batch.d.ts.map +0 -1
- package/dist/compose.d.ts.map +0 -1
- package/dist/computed-entry.d.ts.map +0 -1
- package/dist/computed.cjs.map +0 -1
- package/dist/computed.d.ts.map +0 -1
- package/dist/computed.mjs.map +0 -1
- package/dist/devtools/history.d.ts.map +0 -1
- package/dist/devtools/index.d.ts.map +0 -1
- package/dist/devtools/redux-bridge.d.ts.map +0 -1
- package/dist/devtools/snapshots.d.ts.map +0 -1
- package/dist/devtools/withDevtools.d.ts.map +0 -1
- package/dist/devtools.cjs.map +0 -1
- package/dist/devtools.mjs.map +0 -1
- package/dist/extensions/noop.d.ts.map +0 -1
- package/dist/index.cjs.js +0 -118
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.esm.js +0 -116
- package/dist/index.esm.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/persist/adapters/indexedDB.d.ts.map +0 -1
- package/dist/persist/adapters/localStorage.d.ts.map +0 -1
- package/dist/persist/adapters/memory.d.ts.map +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts.map +0 -1
- package/dist/persist/debounce.d.ts.map +0 -1
- package/dist/persist/hydrate.d.ts.map +0 -1
- package/dist/persist/index.d.ts.map +0 -1
- package/dist/persist/serialize.d.ts.map +0 -1
- package/dist/persist.cjs.map +0 -1
- package/dist/persist.mjs.map +0 -1
- package/dist/proxy.d.ts.map +0 -1
- package/dist/registry-D3X0HSbl.js +0 -26
- package/dist/registry-D3X0HSbl.js.map +0 -1
- package/dist/registry-RDjbeJdx.js +0 -29
- package/dist/registry-RDjbeJdx.js.map +0 -1
- package/dist/registry-qtr1UpFU.js.map +0 -1
- package/dist/registry-zaKZ1P-s.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/signals/createSignal.d.ts.map +0 -1
- package/dist/signals/index.d.ts.map +0 -1
- package/dist/signals/useSignal.d.ts.map +0 -1
- package/dist/signals.cjs.map +0 -1
- package/dist/signals.mjs.map +0 -1
- package/dist/stats.html +0 -4949
- package/dist/store.d.ts.map +0 -1
- package/dist/sync/channel.d.ts.map +0 -1
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/sync/protocol.d.ts.map +0 -1
- package/dist/sync/withSync.d.ts.map +0 -1
- package/dist/sync.cjs.map +0 -1
- package/dist/sync.mjs.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/rollup.config.mjs +0 -44
- package/src/async-entry.ts +0 -6
- package/src/async.ts +0 -240
- package/src/batch.ts +0 -33
- package/src/compose.ts +0 -50
- package/src/computed-entry.ts +0 -6
- package/src/computed.ts +0 -187
- package/src/devtools/history.ts +0 -103
- package/src/devtools/index.ts +0 -5
- package/src/devtools/redux-bridge.ts +0 -70
- package/src/devtools/snapshots.ts +0 -54
- package/src/devtools/withDevtools.ts +0 -196
- package/src/extensions/noop.ts +0 -12
- package/src/index.ts +0 -4
- package/src/persist/adapters/indexedDB.ts +0 -114
- package/src/persist/adapters/localStorage.ts +0 -28
- package/src/persist/adapters/memory.ts +0 -26
- package/src/persist/adapters/sessionStorage.ts +0 -28
- package/src/persist/debounce.ts +0 -28
- package/src/persist/hydrate.ts +0 -60
- package/src/persist/index.ts +0 -141
- package/src/persist/serialize.ts +0 -60
- package/src/proxy.ts +0 -87
- package/src/registry.ts +0 -67
- package/src/signals/createSignal.ts +0 -81
- package/src/signals/index.ts +0 -20
- package/src/signals/useSignal.ts +0 -18
- package/src/store.ts +0 -250
- package/src/sync/channel.ts +0 -15
- package/src/sync/index.ts +0 -3
- package/src/sync/protocol.ts +0 -18
- package/src/sync/withSync.ts +0 -147
- package/src/types.ts +0 -159
- package/tests/async.test.ts +0 -1100
- package/tests/batch.test.ts +0 -41
- package/tests/compose.test.ts +0 -209
- package/tests/computed.test.ts +0 -867
- package/tests/devtools.test.ts +0 -1039
- package/tests/integration/persist.integration.test.ts +0 -258
- package/tests/integration/signals.integration.test.ts +0 -309
- package/tests/integration.test.ts +0 -278
- package/tests/persist/adapters/indexedDB.adapter.test.ts +0 -185
- package/tests/persist/adapters/localStorage.adapter.test.ts +0 -105
- package/tests/persist/adapters/memory.adapter.test.ts +0 -112
- package/tests/persist/adapters/sessionStorage.adapter.test.ts +0 -128
- package/tests/persist/debounce.test.ts +0 -121
- package/tests/persist/hydrate.test.ts +0 -120
- package/tests/persist/migrate.test.ts +0 -208
- package/tests/persist/persist.test.ts +0 -357
- package/tests/persist/serialize.test.ts +0 -128
- package/tests/proxy.test.ts +0 -473
- package/tests/registry.test.ts +0 -67
- package/tests/signals/derived.test.ts +0 -244
- package/tests/signals/inference.test.ts +0 -108
- package/tests/signals/signal.test.ts +0 -348
- package/tests/signals/useSignal.test.tsx +0 -275
- package/tests/store.test.ts +0 -482
- package/tests/stress.test.ts +0 -268
- package/tests/sync.test.ts +0 -576
- package/tests/types.test.ts +0 -32
- package/tests/v0.3.test.ts +0 -813
- package/tree-shake-test.js +0 -1
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -22
- package/vitest_play.ts +0 -7
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
3
|
-
import { createStore } from '../../src/store'
|
|
4
|
-
import { compose } from '../../src/compose'
|
|
5
|
-
import { withPersist } from '../../src/persist/index'
|
|
6
|
-
import { memoryAdapter } from '../../src/persist/adapters/memory'
|
|
7
|
-
import { localStorageAdapter } from '../../src/persist/adapters/localStorage'
|
|
8
|
-
|
|
9
|
-
describe('Persist Integration', () => {
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
vi.useFakeTimers({ now: Date.now() })
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
vi.useRealTimers()
|
|
16
|
-
vi.restoreAllMocks()
|
|
17
|
-
vi.unstubAllGlobals()
|
|
18
|
-
if (typeof localStorage !== 'undefined') localStorage.clear()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
describe('compose + withPersist', () => {
|
|
22
|
-
it('compose(createStore({count:0}), withPersist({key:\'a\', adapter: memoryAdapter(), debounce:0})) works correctly', async () => {
|
|
23
|
-
const adapter = memoryAdapter()
|
|
24
|
-
const store = compose(
|
|
25
|
-
createStore({ count: 0 }),
|
|
26
|
-
withPersist({ key: 'a', adapter, debounce: 0 })
|
|
27
|
-
)
|
|
28
|
-
expect(store.hydrated).toBeInstanceOf(Promise)
|
|
29
|
-
await store.hydrated
|
|
30
|
-
expect(store.getState()).toEqual({ count: 0 })
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('setState on composed store writes to adapter', async () => {
|
|
34
|
-
const adapter = memoryAdapter()
|
|
35
|
-
const store = compose(
|
|
36
|
-
createStore({ count: 0 }),
|
|
37
|
-
withPersist({ key: 'a', adapter, debounce: 0 })
|
|
38
|
-
)
|
|
39
|
-
await store.hydrated
|
|
40
|
-
store.setState({ count: 1 })
|
|
41
|
-
|
|
42
|
-
const raw = await adapter.getItem('a')
|
|
43
|
-
expect(raw).toContain('"count":1')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('hydration restores state on a new composed store using the same adapter instance', async () => {
|
|
47
|
-
const adapter = memoryAdapter()
|
|
48
|
-
const store1 = compose(
|
|
49
|
-
createStore({ count: 0 }),
|
|
50
|
-
withPersist({ key: 'a', adapter, debounce: 0 })
|
|
51
|
-
)
|
|
52
|
-
await store1.hydrated
|
|
53
|
-
store1.setState({ count: 99 })
|
|
54
|
-
|
|
55
|
-
const store2 = compose(
|
|
56
|
-
createStore({ count: 0 }),
|
|
57
|
-
withPersist({ key: 'a', adapter, debounce: 0 })
|
|
58
|
-
)
|
|
59
|
-
await store2.hydrated
|
|
60
|
-
expect(store2.getState()).toEqual({ count: 99 })
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('Full round-trip', () => {
|
|
65
|
-
it('create store → setState → create new store with same adapter → await hydrated → getState matches', async () => {
|
|
66
|
-
const adapter = memoryAdapter()
|
|
67
|
-
const storeA = withPersist(createStore({ val: 'first' }), { key: 'a', adapter, debounce: 0 })
|
|
68
|
-
await storeA.hydrated
|
|
69
|
-
storeA.setState({ val: 'updated' })
|
|
70
|
-
|
|
71
|
-
const storeB = withPersist(createStore({ val: 'default' }), { key: 'a', adapter, debounce: 0 })
|
|
72
|
-
await storeB.hydrated
|
|
73
|
-
|
|
74
|
-
expect(storeB.getState()).toEqual({ val: 'updated' })
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
describe('Multiple stores', () => {
|
|
79
|
-
it('two stores persisting to same adapter with different keys do not interfere', async () => {
|
|
80
|
-
const adapter = memoryAdapter()
|
|
81
|
-
const storeX = withPersist(createStore({ x: 0 }), { key: 'X', adapter, debounce: 0 })
|
|
82
|
-
const storeY = withPersist(createStore({ y: 0 }), { key: 'Y', adapter, debounce: 0 })
|
|
83
|
-
|
|
84
|
-
await storeX.hydrated
|
|
85
|
-
await storeY.hydrated
|
|
86
|
-
|
|
87
|
-
storeX.setState({ x: 1 })
|
|
88
|
-
storeY.setState({ y: 2 })
|
|
89
|
-
|
|
90
|
-
const rawX = await adapter.getItem('X')
|
|
91
|
-
const rawY = await adapter.getItem('Y')
|
|
92
|
-
|
|
93
|
-
expect(rawX).toContain('"x":1')
|
|
94
|
-
expect(rawY).toContain('"y":2')
|
|
95
|
-
expect(rawX).not.toContain('"y":2')
|
|
96
|
-
expect(rawY).not.toContain('"x":1')
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('updating store A does not affect store B\'s persisted data', async () => {
|
|
100
|
-
vi.useRealTimers()
|
|
101
|
-
const adapter = memoryAdapter()
|
|
102
|
-
const storeA = withPersist(createStore({ a: 1 }), { key: 'A', adapter, debounce: 0 })
|
|
103
|
-
const storeB = withPersist(createStore({ b: 0 }), { key: 'B', adapter, debounce: 0 })
|
|
104
|
-
|
|
105
|
-
await storeA.hydrated
|
|
106
|
-
await storeB.hydrated
|
|
107
|
-
|
|
108
|
-
storeB.setState({ b: 1 })
|
|
109
|
-
await Promise.resolve()
|
|
110
|
-
await Promise.resolve()
|
|
111
|
-
|
|
112
|
-
storeA.setState({ a: 99 })
|
|
113
|
-
await Promise.resolve()
|
|
114
|
-
await Promise.resolve()
|
|
115
|
-
|
|
116
|
-
const storeBRehydrated = withPersist(createStore({ b: 0 }), { key: 'B', adapter, debounce: 0 })
|
|
117
|
-
await storeBRehydrated.hydrated
|
|
118
|
-
expect(storeBRehydrated.getState()).toEqual({ b: 1 }) // unchanged
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('stores are fully isolated with different adapters', async () => {
|
|
122
|
-
const storeA = withPersist(createStore({ a: 1 }), { key: 'key', adapter: memoryAdapter(), debounce: 0 })
|
|
123
|
-
const storeB = withPersist(createStore({ b: 2 }), { key: 'key', adapter: memoryAdapter(), debounce: 0 })
|
|
124
|
-
|
|
125
|
-
await storeA.hydrated
|
|
126
|
-
await storeB.hydrated
|
|
127
|
-
|
|
128
|
-
storeA.setState({ a: 99 })
|
|
129
|
-
storeB.setState({ b: 88 })
|
|
130
|
-
|
|
131
|
-
expect(storeA.getState()).toEqual({ a: 99 })
|
|
132
|
-
expect(storeB.getState()).toEqual({ b: 88 })
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
describe('Normal API behaviour', () => {
|
|
137
|
-
it('subscriber is notified on setState', async () => {
|
|
138
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'a', adapter: memoryAdapter(), debounce: 0 })
|
|
139
|
-
await store.hydrated
|
|
140
|
-
|
|
141
|
-
const sub = vi.fn()
|
|
142
|
-
store.subscribe(sub)
|
|
143
|
-
|
|
144
|
-
store.setState({ count: 5 })
|
|
145
|
-
expect(sub).toHaveBeenCalled()
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('subscriber receives correct new state', async () => {
|
|
149
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'a', adapter: memoryAdapter(), debounce: 0 })
|
|
150
|
-
await store.hydrated
|
|
151
|
-
|
|
152
|
-
const sub = vi.fn()
|
|
153
|
-
store.subscribe(sub)
|
|
154
|
-
|
|
155
|
-
store.setState({ count: 10 })
|
|
156
|
-
expect(sub).toHaveBeenCalledWith({ count: 10 })
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
describe('pick isolation', () => {
|
|
161
|
-
it('setting a non-picked key does not write anything to adapter', async () => {
|
|
162
|
-
const adapter = memoryAdapter()
|
|
163
|
-
const store = withPersist(createStore({ a: 1, secret: 'x' }), { key: 'a', adapter, pick: ['a'], debounce: 0 })
|
|
164
|
-
await store.hydrated
|
|
165
|
-
|
|
166
|
-
const spySet = vi.spyOn(adapter, 'setItem')
|
|
167
|
-
store.setState({ secret: 'y' })
|
|
168
|
-
expect(spySet).not.toHaveBeenCalled()
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('setting a picked key writes only picked keys', async () => {
|
|
172
|
-
const adapter = memoryAdapter()
|
|
173
|
-
const store = withPersist(createStore({ a: 1, secret: 'x' }), { key: 'a', adapter, pick: ['a'], debounce: 0 })
|
|
174
|
-
await store.hydrated
|
|
175
|
-
|
|
176
|
-
store.setState({ a: 2, secret: 'y' })
|
|
177
|
-
const raw = await adapter.getItem('a')
|
|
178
|
-
const parsed = JSON.parse(raw!)
|
|
179
|
-
expect(parsed.a).toBe(2)
|
|
180
|
-
expect(parsed.secret).toBeUndefined()
|
|
181
|
-
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
describe('Debounce integration', () => {
|
|
185
|
-
it('rapid setStates result in single adapter write (use vi.useFakeTimers)', async () => {
|
|
186
|
-
const adapter = memoryAdapter()
|
|
187
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'a', adapter, debounce: 100 })
|
|
188
|
-
await store.hydrated
|
|
189
|
-
|
|
190
|
-
const spySet = vi.spyOn(adapter, 'setItem')
|
|
191
|
-
|
|
192
|
-
store.setState({ count: 1 })
|
|
193
|
-
vi.advanceTimersByTime(50)
|
|
194
|
-
store.setState({ count: 2 })
|
|
195
|
-
vi.advanceTimersByTime(50)
|
|
196
|
-
store.setState({ count: 3 })
|
|
197
|
-
vi.advanceTimersByTime(100)
|
|
198
|
-
|
|
199
|
-
expect(spySet).toHaveBeenCalledTimes(1)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('adapter write contains the latest state, not an intermediate one', async () => {
|
|
203
|
-
const adapter = memoryAdapter()
|
|
204
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'a', adapter, debounce: 100 })
|
|
205
|
-
await store.hydrated
|
|
206
|
-
|
|
207
|
-
store.setState({ count: 1 })
|
|
208
|
-
store.setState({ count: 2 })
|
|
209
|
-
store.setState({ count: 3 })
|
|
210
|
-
vi.advanceTimersByTime(100)
|
|
211
|
-
|
|
212
|
-
const raw = await adapter.getItem('a')
|
|
213
|
-
expect(raw).toContain('"count":3')
|
|
214
|
-
})
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
describe('Hydration timing', () => {
|
|
218
|
-
it('store.getState() before hydration contains default state', () => {
|
|
219
|
-
const adapter = memoryAdapter()
|
|
220
|
-
// artificially mock getItem to be slow
|
|
221
|
-
vi.spyOn(adapter, 'getItem').mockReturnValue(new Promise(resolve => setTimeout(() => resolve('{"count":42,"__version":1}'), 100)))
|
|
222
|
-
|
|
223
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'a', adapter })
|
|
224
|
-
expect(store.getState()).toEqual({ count: 0 })
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('store.getState() after awaiting store.hydrated contains persisted state', async () => {
|
|
228
|
-
const adapter = memoryAdapter()
|
|
229
|
-
vi.spyOn(adapter, 'getItem').mockReturnValue(new Promise(resolve => setTimeout(() => resolve('{"count":42,"__version":1}'), 100)))
|
|
230
|
-
|
|
231
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'a', adapter })
|
|
232
|
-
vi.advanceTimersByTime(100)
|
|
233
|
-
await store.hydrated
|
|
234
|
-
expect(store.getState()).toEqual({ count: 42 })
|
|
235
|
-
})
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
describe('localStorage adapter integration', () => {
|
|
239
|
-
it('withPersist + localStorageAdapter writes to window.localStorage correctly', async () => {
|
|
240
|
-
const adapter = localStorageAdapter()
|
|
241
|
-
const store = withPersist(createStore({ num: 0 }), { key: 'storage-key', adapter, debounce: 0 })
|
|
242
|
-
await store.hydrated
|
|
243
|
-
|
|
244
|
-
store.setState({ num: 99 })
|
|
245
|
-
const raw = localStorage.getItem('storage-key')
|
|
246
|
-
expect(raw).toContain('"num":99')
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
it('SSR scenario: localStorageAdapter with window undefined — withPersist completes without error, store uses defaults', async () => {
|
|
250
|
-
vi.stubGlobal('window', undefined)
|
|
251
|
-
const adapter = localStorageAdapter()
|
|
252
|
-
|
|
253
|
-
const store = withPersist(createStore({ num: 10 }), { key: 'ssr-key', adapter, debounce: 0 })
|
|
254
|
-
await expect(store.hydrated).resolves.toBeUndefined()
|
|
255
|
-
expect(store.getState()).toEqual({ num: 10 })
|
|
256
|
-
})
|
|
257
|
-
})
|
|
258
|
-
})
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { createStore } from '../../src/store';
|
|
3
|
-
import type { Store } from '../../src/types';
|
|
4
|
-
import { signal } from '../../src/signals/createSignal';
|
|
5
|
-
import { withPersist } from '../../src/persist/index';
|
|
6
|
-
import { memoryAdapter } from '../../src/persist/adapters/memory';
|
|
7
|
-
import { compose } from '../../src/compose';
|
|
8
|
-
|
|
9
|
-
describe('signals integration', () => {
|
|
10
|
-
interface State {
|
|
11
|
-
count: number;
|
|
12
|
-
name: string;
|
|
13
|
-
secret: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe('signal + plain store', () => {
|
|
17
|
-
it('signal.get() returns correct initial value', () => {
|
|
18
|
-
const store = createStore<State>({ count: 0, name: 'alice', secret: 'shh' });
|
|
19
|
-
const sig = signal(store, 'count');
|
|
20
|
-
expect(sig.get()).toBe(0);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('signal.set(value) updates store and signal.get() reflects new value immediately', () => {
|
|
24
|
-
const store = createStore<State>({ count: 0, name: 'alice', secret: 'shh' });
|
|
25
|
-
const sig = signal(store, 'count');
|
|
26
|
-
sig.set(10);
|
|
27
|
-
expect(store.getState().count).toBe(10);
|
|
28
|
-
expect(sig.get()).toBe(10);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('signal.set(fn) updater form works correctly', () => {
|
|
32
|
-
const store = createStore<State>({ count: 5, name: 'alice', secret: 'shh' });
|
|
33
|
-
const sig = signal(store, 'count');
|
|
34
|
-
sig.set((prev) => prev + 5);
|
|
35
|
-
expect(store.getState().count).toBe(10);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('signal.subscribe() fires when store.setState changes the key', () => {
|
|
39
|
-
const store = createStore<State>({ count: 0, name: 'alice', secret: 'shh' });
|
|
40
|
-
const sig = signal(store, 'count');
|
|
41
|
-
const listener = vi.fn();
|
|
42
|
-
sig.subscribe(listener);
|
|
43
|
-
store.setState({ count: 1 });
|
|
44
|
-
expect(listener).toHaveBeenCalledWith(1);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('signal.subscribe() does NOT fire when store.setState changes unrelated key', () => {
|
|
48
|
-
const store = createStore<State>({ count: 0, name: 'alice', secret: 'shh' });
|
|
49
|
-
const sig = signal(store, 'count');
|
|
50
|
-
const listener = vi.fn();
|
|
51
|
-
sig.subscribe(listener);
|
|
52
|
-
store.setState({ name: 'bob' });
|
|
53
|
-
expect(listener).not.toHaveBeenCalled();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('two signals on same store are fully independent', () => {
|
|
57
|
-
const store = createStore<State>({ count: 0, name: 'alice', secret: 'shh' });
|
|
58
|
-
const sigA = signal(store, 'count');
|
|
59
|
-
const sigB = signal(store, 'name');
|
|
60
|
-
const listenerB = vi.fn();
|
|
61
|
-
sigB.subscribe(listenerB);
|
|
62
|
-
|
|
63
|
-
sigA.set(1);
|
|
64
|
-
expect(listenerB).not.toHaveBeenCalled();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('signal + withPersist', () => {
|
|
69
|
-
it('signal.set(5) causes the store key to be persisted to adapter', async () => {
|
|
70
|
-
const adapter = memoryAdapter();
|
|
71
|
-
const store = withPersist(
|
|
72
|
-
createStore<State>({ count: 0, name: 'alice', secret: 'shh' }),
|
|
73
|
-
{ key: 'test-store', adapter, debounce: 0 }
|
|
74
|
-
);
|
|
75
|
-
const sig = signal(store, 'count');
|
|
76
|
-
|
|
77
|
-
sig.set(42);
|
|
78
|
-
await Promise.resolve(); // wait for microtasks/persist
|
|
79
|
-
|
|
80
|
-
const saved = await adapter.getItem('test-store');
|
|
81
|
-
expect(JSON.parse(saved!).count).toBe(42);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('after rehydration on new store: signal.get() reflects the persisted value', async () => {
|
|
85
|
-
const adapter = memoryAdapter();
|
|
86
|
-
const initialState = { count: 0, name: 'alice', secret: 'shh' };
|
|
87
|
-
|
|
88
|
-
// Store A sets value
|
|
89
|
-
const storeA = withPersist(createStore<State>(initialState), { key: 'x', adapter, debounce: 0 });
|
|
90
|
-
const sigA = signal(storeA, 'count');
|
|
91
|
-
sigA.set(42);
|
|
92
|
-
await Promise.resolve();
|
|
93
|
-
|
|
94
|
-
// Store B rehydrates
|
|
95
|
-
const storeB = withPersist(createStore<State>(initialState), { key: 'x', adapter, debounce: 0 });
|
|
96
|
-
const sigB = signal(storeB, 'count');
|
|
97
|
-
await storeB.hydrated;
|
|
98
|
-
|
|
99
|
-
expect(sigB.get()).toBe(42);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('signal.subscribe() fires after hydration completes with the persisted value', async () => {
|
|
103
|
-
const adapter = memoryAdapter();
|
|
104
|
-
const initialState = { count: 0, name: 'alice', secret: 'shh' };
|
|
105
|
-
|
|
106
|
-
// Persist value 99
|
|
107
|
-
await adapter.setItem('y', JSON.stringify({ count: 99, __version: 1 }));
|
|
108
|
-
|
|
109
|
-
const store = withPersist(createStore<State>(initialState), { key: 'y', adapter, debounce: 0 });
|
|
110
|
-
const sig = signal(store, 'count');
|
|
111
|
-
const listener = vi.fn();
|
|
112
|
-
sig.subscribe(listener);
|
|
113
|
-
|
|
114
|
-
await store.hydrated;
|
|
115
|
-
expect(listener).toHaveBeenCalledWith(99);
|
|
116
|
-
expect(sig.get()).toBe(99);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('signal + compose', () => {
|
|
121
|
-
it('signal works correctly on a store created with compose', () => {
|
|
122
|
-
const adapter = memoryAdapter();
|
|
123
|
-
const store = compose(
|
|
124
|
-
createStore<State>({ count: 0, name: 'a', secret: 's' }),
|
|
125
|
-
(s) => withPersist(s, { key: 'composed', adapter, debounce: 0 })
|
|
126
|
-
) as Store<State>;
|
|
127
|
-
|
|
128
|
-
const sig = signal(store, 'count');
|
|
129
|
-
sig.set(123);
|
|
130
|
-
expect(store.getState().count).toBe(123);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('signal.set() on composed store writes through all enhancers', async () => {
|
|
134
|
-
const adapter = memoryAdapter();
|
|
135
|
-
const store = compose(
|
|
136
|
-
createStore<State>({ count: 0, name: 'a', secret: 's' }),
|
|
137
|
-
(s) => withPersist(s as Store<State>, { key: 'composed-write', adapter, debounce: 0 })
|
|
138
|
-
) as Store<State>;
|
|
139
|
-
|
|
140
|
-
const sig = signal(store, 'count');
|
|
141
|
-
sig.set(500);
|
|
142
|
-
await Promise.resolve();
|
|
143
|
-
|
|
144
|
-
const saved = await adapter.getItem('composed-write');
|
|
145
|
-
expect(JSON.parse(saved!).count).toBe(500);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('signal.get() reads through all enhancers correctly', async () => {
|
|
149
|
-
const adapter = memoryAdapter();
|
|
150
|
-
await adapter.setItem('composed-read', JSON.stringify({ count: 777, __version: 1 }));
|
|
151
|
-
|
|
152
|
-
const store = compose(
|
|
153
|
-
createStore<State>({ count: 0, name: 'a', secret: 's' }),
|
|
154
|
-
(s) => withPersist(s as Store<State>, { key: 'composed-read', adapter, debounce: 0 })
|
|
155
|
-
) as Store<State> & { hydrated: Promise<void> };
|
|
156
|
-
|
|
157
|
-
const sig = signal(store, 'count');
|
|
158
|
-
await store.hydrated;
|
|
159
|
-
expect(sig.get()).toBe(777);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe('derived signal + withPersist', () => {
|
|
164
|
-
it('derived signal reflects persisted base value after hydration', async () => {
|
|
165
|
-
const adapter = memoryAdapter();
|
|
166
|
-
await adapter.setItem('derived-persist', JSON.stringify({ count: 10, __version: 1 }));
|
|
167
|
-
|
|
168
|
-
const store = withPersist(
|
|
169
|
-
createStore<State>({ count: 0, name: 'a', secret: 's' }),
|
|
170
|
-
{ key: 'derived-persist', adapter, debounce: 0 }
|
|
171
|
-
);
|
|
172
|
-
const derivedSig = signal(store, 'count', (v) => v * 2);
|
|
173
|
-
|
|
174
|
-
await store.hydrated;
|
|
175
|
-
expect(derivedSig.get()).toBe(20);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('derived signal subscriber fires after hydration with correct transformed value', async () => {
|
|
179
|
-
const adapter = memoryAdapter();
|
|
180
|
-
await adapter.setItem('derived-sub', JSON.stringify({ count: 5, __version: 1 }));
|
|
181
|
-
|
|
182
|
-
const store = withPersist(
|
|
183
|
-
createStore<State>({ count: 0, name: 'a', secret: 's' }),
|
|
184
|
-
{ key: 'derived-sub', adapter, debounce: 0 }
|
|
185
|
-
);
|
|
186
|
-
const derivedSig = signal(store, 'count', (v) => v + 100);
|
|
187
|
-
const listener = vi.fn();
|
|
188
|
-
derivedSig.subscribe(listener);
|
|
189
|
-
|
|
190
|
-
await store.hydrated;
|
|
191
|
-
expect(listener).toHaveBeenCalledWith(105);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('derived signal set() still throws even on persisted store', () => {
|
|
195
|
-
const adapter = memoryAdapter();
|
|
196
|
-
const store = withPersist(
|
|
197
|
-
createStore<State>({ count: 0, name: 'a', secret: 's' }),
|
|
198
|
-
{ key: 'p', adapter, debounce: 0 }
|
|
199
|
-
);
|
|
200
|
-
const derivedSig = signal(store, 'count', (v) => v);
|
|
201
|
-
expect(() => (derivedSig as unknown as { set: (v: number) => void }).set(1)).toThrow('Storve: cannot call set() on a derived signal. Derived signals are read-only.');
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe('pick filter interaction', () => {
|
|
206
|
-
it('signal.set() on a NON-picked key does NOT write to adapter', async () => {
|
|
207
|
-
const adapter = memoryAdapter();
|
|
208
|
-
const store = withPersist(
|
|
209
|
-
createStore<State>({ count: 1, secret: 'x' } as State),
|
|
210
|
-
{ key: 'pick-test', adapter, pick: ['count'], debounce: 0 }
|
|
211
|
-
);
|
|
212
|
-
await (store as Store<State> & { hydrated: Promise<void> }).hydrated;
|
|
213
|
-
|
|
214
|
-
// First write a picked key so adapter has data
|
|
215
|
-
const countSig = signal(store, 'count');
|
|
216
|
-
countSig.set(5);
|
|
217
|
-
await Promise.resolve();
|
|
218
|
-
|
|
219
|
-
// Now spy on setItem and set a non-picked key
|
|
220
|
-
const spySet = vi.spyOn(adapter, 'setItem');
|
|
221
|
-
const secretSig = signal(store, 'secret');
|
|
222
|
-
secretSig.set('y');
|
|
223
|
-
await Promise.resolve();
|
|
224
|
-
|
|
225
|
-
// setItem should NOT have been called for the non-picked key change
|
|
226
|
-
expect(spySet).not.toHaveBeenCalled();
|
|
227
|
-
|
|
228
|
-
// Adapter should still only have count, not secret
|
|
229
|
-
const saved = await adapter.getItem('pick-test');
|
|
230
|
-
expect(JSON.parse(saved!).secret).toBeUndefined();
|
|
231
|
-
expect(JSON.parse(saved!).count).toBe(5);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('signal.set() on a PICKED key DOES write to adapter', async () => {
|
|
235
|
-
const adapter = memoryAdapter();
|
|
236
|
-
const store = withPersist(
|
|
237
|
-
createStore<State>({ count: 0, name: 'alice', secret: 'shh' }),
|
|
238
|
-
{ key: 'pick-test', adapter, debounce: 0, pick: ['count'] }
|
|
239
|
-
);
|
|
240
|
-
const sig = signal(store, 'count');
|
|
241
|
-
|
|
242
|
-
sig.set(42);
|
|
243
|
-
await Promise.resolve();
|
|
244
|
-
|
|
245
|
-
const saved = await adapter.getItem('pick-test');
|
|
246
|
-
expect(JSON.parse(saved!).count).toBe(42);
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe('Multiple signals, shared adapter', () => {
|
|
251
|
-
it('signals on storeA (key \'A\') and storeB (key \'B\') with same adapter — fully isolated', async () => {
|
|
252
|
-
const adapter = memoryAdapter();
|
|
253
|
-
const storeA = withPersist(createStore<State>({ count: 0 } as State), { key: 'A', adapter, debounce: 0 });
|
|
254
|
-
const storeB = withPersist(createStore<State>({ count: 0 } as State), { key: 'B', adapter, debounce: 0 });
|
|
255
|
-
|
|
256
|
-
const sigA = signal(storeA, 'count');
|
|
257
|
-
const sigB = signal(storeB, 'count');
|
|
258
|
-
|
|
259
|
-
sigA.set(1);
|
|
260
|
-
await Promise.resolve();
|
|
261
|
-
|
|
262
|
-
expect(sigB.get()).toBe(0);
|
|
263
|
-
const savedA = await adapter.getItem('A');
|
|
264
|
-
const savedB = await adapter.getItem('B');
|
|
265
|
-
expect(JSON.parse(savedA!).count).toBe(1);
|
|
266
|
-
expect(savedB).toBeNull();
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('Memory management', () => {
|
|
271
|
-
it('unsubscribing from signal prevents callbacks even after store changes', () => {
|
|
272
|
-
const store = createStore<State>({ count: 0, name: 'a', secret: 's' });
|
|
273
|
-
const sig = signal(store, 'count');
|
|
274
|
-
const listener = vi.fn();
|
|
275
|
-
const unsub = sig.subscribe(listener);
|
|
276
|
-
|
|
277
|
-
unsub();
|
|
278
|
-
store.setState({ count: 1 });
|
|
279
|
-
expect(listener).not.toHaveBeenCalled();
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('unsubscribing from sigA does not affect sigB on same store', () => {
|
|
283
|
-
const store = createStore<State>({ count: 0, name: 'a', secret: 's' });
|
|
284
|
-
const sigA = signal(store, 'count');
|
|
285
|
-
const sigB = signal(store, 'count');
|
|
286
|
-
const lA = vi.fn();
|
|
287
|
-
const lB = vi.fn();
|
|
288
|
-
const unsubA = sigA.subscribe(lA);
|
|
289
|
-
sigB.subscribe(lB);
|
|
290
|
-
|
|
291
|
-
unsubA();
|
|
292
|
-
store.setState({ count: 1 });
|
|
293
|
-
expect(lA).not.toHaveBeenCalled();
|
|
294
|
-
expect(lB).toHaveBeenCalled();
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('after store is garbage collected (no references), signal.subscribe callback does not throw', () => {
|
|
298
|
-
// This is mostly a conceptual test, hard to force GC in JS.
|
|
299
|
-
// But we ensure no dangling strong references that would cause crashes.
|
|
300
|
-
let store: Store<State> | null = createStore<State>({ count: 0, name: 'a', secret: 's' });
|
|
301
|
-
const sig = signal(store, 'count');
|
|
302
|
-
sig.subscribe(() => {});
|
|
303
|
-
store = null;
|
|
304
|
-
// No references to store remain except through signal's closure
|
|
305
|
-
// We're checking for no obvious leaks/dangling pointer issues.
|
|
306
|
-
expect(true).toBe(true);
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
});
|