@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,357 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { createStore } from '../../src/store'
|
|
3
|
-
import { withPersist } from '../../src/persist/index'
|
|
4
|
-
import { memoryAdapter } from '../../src/persist/adapters/memory'
|
|
5
|
-
|
|
6
|
-
describe('withPersist', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
vi.useFakeTimers({ now: Date.now() })
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
vi.useRealTimers()
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
describe('Direct form', () => {
|
|
16
|
-
it('withPersist(store, options) returns a store with same getState/setState/subscribe', () => {
|
|
17
|
-
const store = createStore({ count: 0 })
|
|
18
|
-
const adapter = memoryAdapter()
|
|
19
|
-
const enhanced = withPersist(store, { key: 'test', adapter })
|
|
20
|
-
|
|
21
|
-
expect(typeof enhanced.getState).toBe('function')
|
|
22
|
-
expect(typeof enhanced.setState).toBe('function')
|
|
23
|
-
expect(typeof enhanced.subscribe).toBe('function')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('enhanced store has a hydrated property that is a Promise', () => {
|
|
27
|
-
const store = createStore({ count: 0 })
|
|
28
|
-
const adapter = memoryAdapter()
|
|
29
|
-
const enhanced = withPersist(store, { key: 'test', adapter })
|
|
30
|
-
|
|
31
|
-
expect(enhanced.hydrated).toBeInstanceOf(Promise)
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
describe('Curried form', () => {
|
|
36
|
-
it('withPersist(options) returns a function', () => {
|
|
37
|
-
const adapter = memoryAdapter()
|
|
38
|
-
const enhancer = withPersist({ key: 'test', adapter })
|
|
39
|
-
expect(typeof enhancer).toBe('function')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('that function accepts a store and returns an enhanced store', () => {
|
|
43
|
-
const store = createStore({ count: 0 })
|
|
44
|
-
const adapter = memoryAdapter()
|
|
45
|
-
const enhancer = withPersist({ key: 'test', adapter })
|
|
46
|
-
const enhanced = enhancer(store)
|
|
47
|
-
|
|
48
|
-
expect(enhanced.getState()).toEqual({ count: 0 })
|
|
49
|
-
expect(enhanced.hydrated).toBeInstanceOf(Promise)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('both forms produce identical behaviour', async () => {
|
|
53
|
-
const adapter1 = memoryAdapter()
|
|
54
|
-
const adapter2 = memoryAdapter()
|
|
55
|
-
|
|
56
|
-
const store1 = withPersist(createStore({ a: 1 }), { key: 'x', adapter: adapter1 })
|
|
57
|
-
const store2 = withPersist({ key: 'x', adapter: adapter2 })(createStore({ a: 1 }))
|
|
58
|
-
|
|
59
|
-
await store1.hydrated
|
|
60
|
-
await store2.hydrated
|
|
61
|
-
|
|
62
|
-
expect(store1.getState()).toEqual({ a: 1 })
|
|
63
|
-
expect(store2.getState()).toEqual({ a: 1 })
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe('Hydration', () => {
|
|
68
|
-
it('store is hydrated from adapter on init', async () => {
|
|
69
|
-
const adapter = memoryAdapter()
|
|
70
|
-
// manually seed the adapter
|
|
71
|
-
await adapter.setItem('test', JSON.stringify({ count: 42, __version: 1 }))
|
|
72
|
-
|
|
73
|
-
const store = createStore({ count: 0 })
|
|
74
|
-
const enhanced = withPersist(store, { key: 'test', adapter })
|
|
75
|
-
|
|
76
|
-
await enhanced.hydrated
|
|
77
|
-
expect(enhanced.getState()).toEqual({ count: 42 })
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('hydrated Promise resolves after hydration completes', async () => {
|
|
81
|
-
const adapter = memoryAdapter()
|
|
82
|
-
const store = createStore({ count: 0 })
|
|
83
|
-
const enhanced = withPersist(store, { key: 'test', adapter })
|
|
84
|
-
|
|
85
|
-
let resolved = false
|
|
86
|
-
enhanced.hydrated.then(() => { resolved = true })
|
|
87
|
-
|
|
88
|
-
// Since it's an async operation, it isn't resolved synchronously
|
|
89
|
-
// but will be resolved in the microtask queue
|
|
90
|
-
await Promise.resolve()
|
|
91
|
-
// the adapter methods return promises, so give it a tick
|
|
92
|
-
for (let i = 0; i < 5; i++) await Promise.resolve()
|
|
93
|
-
|
|
94
|
-
expect(resolved).toBe(true)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('store state reflects persisted values after hydration', async () => {
|
|
98
|
-
const adapter = memoryAdapter()
|
|
99
|
-
await adapter.setItem('test', JSON.stringify({ name: 'Alice', __version: 1 }))
|
|
100
|
-
|
|
101
|
-
const store = createStore({ name: 'Bob', age: 30 })
|
|
102
|
-
const enhanced = withPersist(store, { key: 'test', adapter })
|
|
103
|
-
|
|
104
|
-
await enhanced.hydrated
|
|
105
|
-
// only name is hydrated, age is kept
|
|
106
|
-
expect(enhanced.getState()).toEqual({ name: 'Alice', age: 30 })
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('store state is not overwritten if adapter has no data', async () => {
|
|
110
|
-
const adapter = memoryAdapter() // empty
|
|
111
|
-
const store = createStore({ count: 5 })
|
|
112
|
-
const enhanced = withPersist(store, { key: 'test', adapter })
|
|
113
|
-
|
|
114
|
-
await enhanced.hydrated
|
|
115
|
-
expect(enhanced.getState()).toEqual({ count: 5 })
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('hydration merges into existing state — does not replace unrelated keys', async () => {
|
|
119
|
-
const adapter = memoryAdapter()
|
|
120
|
-
await adapter.setItem('test', JSON.stringify({ a: 10, __version: 1 }))
|
|
121
|
-
|
|
122
|
-
const store = createStore({ a: 1, b: 2 })
|
|
123
|
-
const enhanced = withPersist(store, { key: 'test', adapter })
|
|
124
|
-
|
|
125
|
-
await enhanced.hydrated
|
|
126
|
-
expect(enhanced.getState()).toEqual({ a: 10, b: 2 })
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
describe('Writing on setState', () => {
|
|
131
|
-
it('calling setState causes persisted keys to be written to adapter', async () => {
|
|
132
|
-
const adapter = memoryAdapter()
|
|
133
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'test', adapter, debounce: 0 })
|
|
134
|
-
await store.hydrated
|
|
135
|
-
|
|
136
|
-
store.setState({ count: 5 })
|
|
137
|
-
const value = await adapter.getItem('test')
|
|
138
|
-
expect(value).toContain('"count":5')
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('written value is valid JSON containing the persisted keys', async () => {
|
|
142
|
-
const adapter = memoryAdapter()
|
|
143
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'test', adapter, debounce: 0 })
|
|
144
|
-
await store.hydrated
|
|
145
|
-
|
|
146
|
-
store.setState({ count: 42 })
|
|
147
|
-
const raw = await adapter.getItem('test')
|
|
148
|
-
const parsed = JSON.parse(raw!)
|
|
149
|
-
expect(parsed.count).toBe(42)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('written value contains __version field', async () => {
|
|
153
|
-
const adapter = memoryAdapter()
|
|
154
|
-
const store = withPersist(createStore({ a: 1 }), { key: 'test', adapter, debounce: 0 })
|
|
155
|
-
await store.hydrated
|
|
156
|
-
|
|
157
|
-
store.setState({ a: 2 })
|
|
158
|
-
const raw = await adapter.getItem('test')
|
|
159
|
-
const parsed = JSON.parse(raw!)
|
|
160
|
-
expect(parsed.__version).toBe(1) // default version is 1
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('unrelated keys (not in pick) are NOT written to adapter', async () => {
|
|
164
|
-
const adapter = memoryAdapter()
|
|
165
|
-
const store = withPersist(createStore({ a: 1, b: 2 }), { key: 'test', adapter, pick: ['a'], debounce: 0 })
|
|
166
|
-
await store.hydrated
|
|
167
|
-
|
|
168
|
-
store.setState({ b: 5 })
|
|
169
|
-
// This state change triggers write, but b shouldn't be picked
|
|
170
|
-
const raw = await adapter.getItem('test')
|
|
171
|
-
|
|
172
|
-
// If the write actually happened or not, the result must not contain 'b'
|
|
173
|
-
if (raw) {
|
|
174
|
-
const parsed = JSON.parse(raw)
|
|
175
|
-
expect(parsed.b).toBeUndefined()
|
|
176
|
-
expect(parsed.a).toBe(1)
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('debounce: write does not fire immediately when debounce > 0', async () => {
|
|
181
|
-
const adapter = memoryAdapter()
|
|
182
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'test', adapter, debounce: 100 })
|
|
183
|
-
await store.hydrated
|
|
184
|
-
|
|
185
|
-
store.setState({ count: 1 })
|
|
186
|
-
const raw = await adapter.getItem('test')
|
|
187
|
-
expect(raw).toBeNull() // not written yet
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('debounce: write fires after debounce delay elapses', async () => {
|
|
191
|
-
const adapter = memoryAdapter()
|
|
192
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'test', adapter, debounce: 100 })
|
|
193
|
-
await store.hydrated
|
|
194
|
-
|
|
195
|
-
store.setState({ count: 1 })
|
|
196
|
-
vi.advanceTimersByTime(100)
|
|
197
|
-
|
|
198
|
-
const raw = await adapter.getItem('test')
|
|
199
|
-
expect(raw).toContain('"count":1')
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('debounce: multiple rapid setStates result in only one write', async () => {
|
|
203
|
-
const adapter = memoryAdapter()
|
|
204
|
-
const spy = vi.spyOn(adapter, 'setItem')
|
|
205
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'test', adapter, debounce: 100 })
|
|
206
|
-
await store.hydrated
|
|
207
|
-
|
|
208
|
-
spy.mockClear()
|
|
209
|
-
|
|
210
|
-
store.setState({ count: 1 })
|
|
211
|
-
vi.advanceTimersByTime(50)
|
|
212
|
-
store.setState({ count: 2 })
|
|
213
|
-
vi.advanceTimersByTime(50)
|
|
214
|
-
store.setState({ count: 3 })
|
|
215
|
-
vi.advanceTimersByTime(100)
|
|
216
|
-
|
|
217
|
-
expect(spy).toHaveBeenCalledTimes(1)
|
|
218
|
-
const raw = await adapter.getItem('test')
|
|
219
|
-
expect(raw).toContain('"count":3')
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
it('debounce: 0 causes immediate write on every setState', async () => {
|
|
223
|
-
const adapter = memoryAdapter()
|
|
224
|
-
const spy = vi.spyOn(adapter, 'setItem')
|
|
225
|
-
const store = withPersist(createStore({ count: 0 }), { key: 'test', adapter, debounce: 0 })
|
|
226
|
-
await store.hydrated
|
|
227
|
-
|
|
228
|
-
spy.mockClear()
|
|
229
|
-
|
|
230
|
-
store.setState({ count: 1 })
|
|
231
|
-
store.setState({ count: 2 })
|
|
232
|
-
|
|
233
|
-
expect(spy).toHaveBeenCalledTimes(2)
|
|
234
|
-
const raw = await adapter.getItem('test')
|
|
235
|
-
expect(raw).toContain('"count":2')
|
|
236
|
-
})
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
describe('pick option', () => {
|
|
240
|
-
it('only picked keys are written to adapter', async () => {
|
|
241
|
-
const adapter = memoryAdapter()
|
|
242
|
-
const store = withPersist(createStore({ a: 1, b: 2, c: 3 }), { key: 'test', adapter, pick: ['a', 'c'], debounce: 0 })
|
|
243
|
-
await store.hydrated
|
|
244
|
-
|
|
245
|
-
store.setState({ a: 10 })
|
|
246
|
-
const raw = await adapter.getItem('test')
|
|
247
|
-
const parsed = JSON.parse(raw!)
|
|
248
|
-
|
|
249
|
-
expect(parsed.a).toBe(10)
|
|
250
|
-
expect(parsed.c).toBe(3)
|
|
251
|
-
expect(parsed.b).toBeUndefined()
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('non-picked keys are not present in the serialized value', async () => {
|
|
255
|
-
const adapter = memoryAdapter()
|
|
256
|
-
const store = withPersist(createStore({ a: 1, secret: 'password' }), { key: 'test', adapter, pick: ['a'], debounce: 0 })
|
|
257
|
-
await store.hydrated
|
|
258
|
-
|
|
259
|
-
store.setState({ a: 2 })
|
|
260
|
-
const raw = await adapter.getItem('test')
|
|
261
|
-
expect(raw).not.toContain('password')
|
|
262
|
-
expect(raw).not.toContain('secret')
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
it('if pick is omitted, all keys are persisted', async () => {
|
|
266
|
-
const adapter = memoryAdapter()
|
|
267
|
-
const store = withPersist(createStore({ a: 1, b: 2 }), { key: 'test', adapter, debounce: 0 }) // pick omitted
|
|
268
|
-
await store.hydrated
|
|
269
|
-
|
|
270
|
-
store.setState({ a: 2 })
|
|
271
|
-
const raw = await adapter.getItem('test')
|
|
272
|
-
const parsed = JSON.parse(raw!)
|
|
273
|
-
expect(parsed.a).toBe(2)
|
|
274
|
-
expect(parsed.b).toBe(2)
|
|
275
|
-
})
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
describe('version option', () => {
|
|
279
|
-
it('__version in stored JSON matches the version option', async () => {
|
|
280
|
-
const adapter = memoryAdapter()
|
|
281
|
-
const store = withPersist(createStore({ a: 1 }), { key: 'test', adapter, version: 5, debounce: 0 })
|
|
282
|
-
await store.hydrated
|
|
283
|
-
|
|
284
|
-
store.setState({ a: 2 })
|
|
285
|
-
const raw = await adapter.getItem('test')
|
|
286
|
-
const parsed = JSON.parse(raw!)
|
|
287
|
-
expect(parsed.__version).toBe(5)
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
it('defaults to version 1 when not specified', async () => {
|
|
291
|
-
const adapter = memoryAdapter()
|
|
292
|
-
const store = withPersist(createStore({ a: 1 }), { key: 'test', adapter, debounce: 0 })
|
|
293
|
-
await store.hydrated
|
|
294
|
-
|
|
295
|
-
store.setState({ a: 2 })
|
|
296
|
-
const raw = await adapter.getItem('test')
|
|
297
|
-
const parsed = JSON.parse(raw!)
|
|
298
|
-
expect(parsed.__version).toBe(1)
|
|
299
|
-
})
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
describe('migrate option', () => {
|
|
303
|
-
it('migrate is called during hydration when stored version differs', async () => {
|
|
304
|
-
const adapter = memoryAdapter()
|
|
305
|
-
await adapter.setItem('test', JSON.stringify({ oldKey: 'val', __version: 1 }))
|
|
306
|
-
|
|
307
|
-
const migrate = vi.fn(() => ({ newKey: 'val' }))
|
|
308
|
-
const store = withPersist(createStore({ newKey: '' }), { key: 'test', adapter, version: 2, migrate })
|
|
309
|
-
|
|
310
|
-
await store.hydrated
|
|
311
|
-
expect(migrate).toHaveBeenCalled()
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
it('store is populated with migrated values', async () => {
|
|
315
|
-
const adapter = memoryAdapter()
|
|
316
|
-
await adapter.setItem('test', JSON.stringify({ old: 'val', __version: 1 }))
|
|
317
|
-
|
|
318
|
-
const migrate = vi.fn((state: Partial<{ new: string, old?: string }>) => ({ new: state.old }))
|
|
319
|
-
const store = withPersist(createStore<{ new: string, old?: string }>({ new: '' }), { key: 'test', adapter, version: 2, migrate })
|
|
320
|
-
|
|
321
|
-
await store.hydrated
|
|
322
|
-
expect(store.getState()).toEqual({ new: 'val' })
|
|
323
|
-
})
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
describe('Normal store API preserved', () => {
|
|
327
|
-
it('subscribe still works correctly after withPersist', async () => {
|
|
328
|
-
const adapter = memoryAdapter()
|
|
329
|
-
const store = withPersist(createStore({ a: 1 }), { key: 'test', adapter })
|
|
330
|
-
|
|
331
|
-
const sub = vi.fn()
|
|
332
|
-
store.subscribe(sub)
|
|
333
|
-
|
|
334
|
-
store.setState({ a: 2 })
|
|
335
|
-
expect(sub).toHaveBeenCalledWith({ a: 2 })
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
it('setState still notifies subscribers', async () => {
|
|
339
|
-
const adapter = memoryAdapter()
|
|
340
|
-
const store = withPersist(createStore({ a: 1 }), { key: 'test', adapter })
|
|
341
|
-
|
|
342
|
-
const sub = vi.fn()
|
|
343
|
-
store.subscribe(sub)
|
|
344
|
-
|
|
345
|
-
store.setState(state => ({ a: state.a + 1 }))
|
|
346
|
-
expect(sub).toHaveBeenCalledWith({ a: 2 })
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
it('getState still returns correct state', async () => {
|
|
350
|
-
const adapter = memoryAdapter()
|
|
351
|
-
const store = withPersist(createStore({ a: 1 }), { key: 'test', adapter })
|
|
352
|
-
|
|
353
|
-
store.setState({ a: 5 })
|
|
354
|
-
expect(store.getState()).toEqual({ a: 5 })
|
|
355
|
-
})
|
|
356
|
-
})
|
|
357
|
-
})
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { pick, toJSON, fromJSON } from '../../src/persist/serialize'
|
|
3
|
-
|
|
4
|
-
describe('pick', () => {
|
|
5
|
-
it('returns only specified keys from a flat object', () => {
|
|
6
|
-
const state = { a: 1, b: 2, c: 3 }
|
|
7
|
-
// TypeScript: picked result is typed as Partial<T>
|
|
8
|
-
const result: Partial<typeof state> = pick(state, ['a', 'c'])
|
|
9
|
-
expect(result).toEqual({ a: 1, c: 3 })
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('returns full state when keys is undefined', () => {
|
|
13
|
-
const state = { a: 1, b: 2 }
|
|
14
|
-
const result = pick(state)
|
|
15
|
-
expect(result).toEqual({ a: 1, b: 2 })
|
|
16
|
-
expect(result).not.toBe(state) // still a new object? or same? "Returns a new object"
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('returns full state when keys is empty array', () => {
|
|
20
|
-
const state = { a: 1, b: 2 }
|
|
21
|
-
const result = pick(state, [])
|
|
22
|
-
expect(result).toEqual({ a: 1, b: 2 })
|
|
23
|
-
expect(result).not.toBe(state)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('returns empty object when keys don\'t exist in state', () => {
|
|
27
|
-
const state = { a: 1, b: 2 }
|
|
28
|
-
// @ts-expect-error testing invalid keys
|
|
29
|
-
const result = pick(state, ['z'])
|
|
30
|
-
expect(result).toEqual({})
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('does not mutate the original state object', () => {
|
|
34
|
-
const state = { a: 1, b: 2 }
|
|
35
|
-
const result = pick(state, ['a'])
|
|
36
|
-
expect(result).toEqual({ a: 1 })
|
|
37
|
-
expect(state).toEqual({ a: 1, b: 2 })
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('works with nested objects (picks the top-level key, not deep keys)', () => {
|
|
41
|
-
const state = { nested: { x: 1, y: 2 }, other: 3 }
|
|
42
|
-
const result = pick(state, ['nested'])
|
|
43
|
-
expect(result).toEqual({ nested: { x: 1, y: 2 } })
|
|
44
|
-
// check it picks top level ref
|
|
45
|
-
expect(result.nested).toBe(state.nested)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('works when state has a single key', () => {
|
|
49
|
-
const state = { single: 'value' }
|
|
50
|
-
const result = pick(state, ['single'])
|
|
51
|
-
expect(result).toEqual({ single: 'value' })
|
|
52
|
-
})
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
describe('toJSON', () => {
|
|
56
|
-
it('serializes a flat object correctly', () => {
|
|
57
|
-
const value = { a: 1, b: 'two', c: true }
|
|
58
|
-
expect(toJSON(value)).toBe('{"a":1,"b":"two","c":true}')
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('serializes nested objects correctly', () => {
|
|
62
|
-
const value = { a: { b: { c: 1 } } }
|
|
63
|
-
expect(toJSON(value)).toBe('{"a":{"b":{"c":1}}}')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('serializes arrays correctly', () => {
|
|
67
|
-
const value = [1, 'two', { three: 3 }]
|
|
68
|
-
expect(toJSON(value)).toBe('[1,"two",{"three":3}]')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('serializes null, numbers, booleans correctly', () => {
|
|
72
|
-
expect(toJSON(null)).toBe('null')
|
|
73
|
-
expect(toJSON(42)).toBe('42')
|
|
74
|
-
expect(toJSON(true)).toBe('true')
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('throws a descriptive error when value contains circular reference', () => {
|
|
78
|
-
const circular: Record<string, unknown> = {}
|
|
79
|
-
circular.self = circular
|
|
80
|
-
expect(() => toJSON(circular)).toThrow(/circular|serialize|JSON/i)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('throws a descriptive error when value contains a BigInt', () => {
|
|
84
|
-
expect(() => toJSON({ num: BigInt(42) })).toThrow(/BigInt|serialize|JSON/i)
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
describe('fromJSON', () => {
|
|
89
|
-
it('parses a valid JSON string back to original object', () => {
|
|
90
|
-
const raw = '{"a":1,"b":"two","c":true}'
|
|
91
|
-
const parsed = fromJSON<{a: number; b: string; c: boolean}>(raw)
|
|
92
|
-
expect(parsed).toEqual({ a: 1, b: 'two', c: true })
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('parses nested objects correctly', () => {
|
|
96
|
-
const raw = '{"a":{"b":{"c":1}}}'
|
|
97
|
-
const parsed = fromJSON<{a: {b: {c: number}}}>(raw)
|
|
98
|
-
expect(parsed).toEqual({ a: { b: { c: 1 } } })
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('parses arrays correctly', () => {
|
|
102
|
-
const raw = '[1,"two",{"three":3}]'
|
|
103
|
-
const parsed = fromJSON<unknown[]>(raw)
|
|
104
|
-
expect(parsed).toEqual([1, 'two', { three: 3 }])
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('throws a descriptive error on malformed JSON string', () => {
|
|
108
|
-
const raw = '{"a":1' // missing closing brace
|
|
109
|
-
expect(() => fromJSON(raw)).toThrow(/JSON|parse/i)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('throws a descriptive error when raw is null', () => {
|
|
113
|
-
// @ts-expect-error testing null input
|
|
114
|
-
expect(() => fromJSON(null)).toThrow(/null|undefined|parse/i)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('throws a descriptive error when raw is undefined', () => {
|
|
118
|
-
// @ts-expect-error testing undefined input
|
|
119
|
-
expect(() => fromJSON(undefined)).toThrow(/null|undefined|parse/i)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('round-trip: toJSON then fromJSON returns deep-equal original value', () => {
|
|
123
|
-
const original = { a: 1, nested: { b: [1, 2, 3] }, c: null, d: true }
|
|
124
|
-
const serialized = toJSON(original)
|
|
125
|
-
const reHydrated = fromJSON<typeof original>(serialized)
|
|
126
|
-
expect(reHydrated).toEqual(original)
|
|
127
|
-
})
|
|
128
|
-
})
|