@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,278 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { createStore } from '../src/store';
|
|
3
|
-
|
|
4
|
-
describe('3.1 Real World Scenarios', () => {
|
|
5
|
-
describe('Counter store', () => {
|
|
6
|
-
it('increment, decrement, reset', () => {
|
|
7
|
-
const store = createStore({ count: 0 });
|
|
8
|
-
store.setState((s) => ({ count: s.count + 1 }));
|
|
9
|
-
expect(store.getState().count).toBe(1);
|
|
10
|
-
store.setState((s) => ({ count: s.count - 1 }));
|
|
11
|
-
expect(store.getState().count).toBe(0);
|
|
12
|
-
store.setState({ count: 0 });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('multiple components subscribed', () => {
|
|
16
|
-
const store = createStore({ count: 0 });
|
|
17
|
-
const s1 = vi.fn(), s2 = vi.fn();
|
|
18
|
-
store.subscribe(s1); store.subscribe(s2);
|
|
19
|
-
store.setState({ count: 1 });
|
|
20
|
-
expect(s1).toHaveBeenCalled();
|
|
21
|
-
expect(s2).toHaveBeenCalled();
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('Todo store', () => {
|
|
26
|
-
it('add todo', () => {
|
|
27
|
-
const store = createStore({ todos: [] as unknown[] });
|
|
28
|
-
store.setState(s => ({ todos: [...s.todos, { id: 1 }] }));
|
|
29
|
-
expect(store.getState().todos.length).toBe(1);
|
|
30
|
-
});
|
|
31
|
-
it('remove todo', () => {
|
|
32
|
-
const store = createStore({ todos: [{ id: 1 }] });
|
|
33
|
-
store.setState(() => ({ todos: [] }));
|
|
34
|
-
expect(store.getState().todos.length).toBe(0);
|
|
35
|
-
});
|
|
36
|
-
it('toggle todo complete', () => {
|
|
37
|
-
const store = createStore({ todos: [{ id: 1, done: false }] });
|
|
38
|
-
store.setState(() => ({ todos: [{ id: 1, done: true }] }));
|
|
39
|
-
expect(store.getState().todos[0].done).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
it('filter completed todos via computed (derived)', () => {
|
|
42
|
-
const store = createStore({ todos: [{ id: 1, done: true }, { id: 2, done: false }] });
|
|
43
|
-
const completed = store.getState().todos.filter(t => t.done);
|
|
44
|
-
expect(completed.length).toBe(1);
|
|
45
|
-
});
|
|
46
|
-
it('clear all todos', () => {
|
|
47
|
-
const store = createStore({ todos: [{ id: 1 }] });
|
|
48
|
-
store.setState({ todos: [] });
|
|
49
|
-
expect(store.getState().todos.length).toBe(0);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('User profile store', () => {
|
|
54
|
-
it.each([
|
|
55
|
-
['update name', { user: { name: 'A', address: { city: 'X' } } }, { user: { name: 'B', address: { city: 'X' } } }],
|
|
56
|
-
['update nested address', { user: { name: 'A', address: { city: 'X' } } }, { user: { name: 'A', address: { city: 'Y' } } }],
|
|
57
|
-
['replace entire nested object', { user: { name: 'A', address: { city: 'X' } } }, { user: { name: 'B', address: { city: 'Y' } } }]
|
|
58
|
-
])('%s', (_, initial, expected) => {
|
|
59
|
-
const store = createStore(initial);
|
|
60
|
-
store.setState(expected);
|
|
61
|
-
expect(store.getState()).toEqual(expected);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('Shopping cart store', () => {
|
|
66
|
-
it.each([
|
|
67
|
-
['add item', { items: [] }, { items: [{ id: 1, qty: 1 }] }],
|
|
68
|
-
['remove item', { items: [{ id: 1 }] }, { items: [] }],
|
|
69
|
-
['update quantity', { items: [{ id: 1, qty: 1 }] }, { items: [{ id: 1, qty: 2 }] }],
|
|
70
|
-
['clear cart', { items: [{ id: 1 }] }, { items: [] }]
|
|
71
|
-
])('%s', (_, initial, expected) => {
|
|
72
|
-
const store = createStore(initial as Record<string, unknown>);
|
|
73
|
-
store.setState(expected as Record<string, unknown>);
|
|
74
|
-
expect(store.getState()).toEqual(expected);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('total computed from items', () => {
|
|
78
|
-
const store = createStore({ items: [{ price: 10, qty: 2 }, { price: 5, qty: 1 }] });
|
|
79
|
-
const total = store.getState().items.reduce((acc, item) => acc + item.price * item.qty, 0);
|
|
80
|
-
expect(total).toBe(25);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('Theme store', () => {
|
|
85
|
-
it('toggle dark/light mode', () => {
|
|
86
|
-
const store = createStore({ theme: 'light' });
|
|
87
|
-
store.setState({ theme: 'dark' });
|
|
88
|
-
expect(store.getState().theme).toBe('dark');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('Auth store', () => {
|
|
93
|
-
it('login sets user', () => {
|
|
94
|
-
const store = createStore({ user: null as ({ id: number } | null) });
|
|
95
|
-
store.setState({ user: { id: 1 } });
|
|
96
|
-
expect(store.getState().user).toEqual({ id: 1 });
|
|
97
|
-
});
|
|
98
|
-
it('logout clears user', () => {
|
|
99
|
-
const store = createStore({ user: { id: 1 } as ({ id: number } | null) });
|
|
100
|
-
store.setState({ user: null });
|
|
101
|
-
expect(store.getState().user).toBeNull();
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('Pagination store', () => {
|
|
106
|
-
it.each([
|
|
107
|
-
['next page', { page: 1 }, { page: 2 }],
|
|
108
|
-
['previous page', { page: 2 }, { page: 1 }],
|
|
109
|
-
['jump to page', { page: 1 }, { page: 5 }]
|
|
110
|
-
])('%s', (_, initial, expected) => {
|
|
111
|
-
const store = createStore(initial);
|
|
112
|
-
store.setState(expected);
|
|
113
|
-
expect(store.getState()).toEqual(expected);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('Form store', () => {
|
|
118
|
-
it.each([
|
|
119
|
-
['update field value', { f1: '', f2: '' }, { f1: 'A', f2: '' }],
|
|
120
|
-
['reset all fields', { f1: 'A', f2: 'B' }, { f1: '', f2: '' }]
|
|
121
|
-
])('%s', (_, initial, expected) => {
|
|
122
|
-
const store = createStore(initial);
|
|
123
|
-
store.setState(expected);
|
|
124
|
-
expect(store.getState()).toEqual(expected);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('validate fields', () => {
|
|
128
|
-
const store = createStore({ email: 'test', errors: {} as Record<string, unknown> });
|
|
129
|
-
if (!store.getState().email.includes('@')) {
|
|
130
|
-
store.setState({ errors: { email: 'invalid' } });
|
|
131
|
-
}
|
|
132
|
-
expect(store.getState().errors.email).toBe('invalid');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('Multi-store', () => {
|
|
137
|
-
it('two stores are fully independent', () => {
|
|
138
|
-
const s1 = createStore({ a: 1 });
|
|
139
|
-
const s2 = createStore({ b: 2 });
|
|
140
|
-
s1.setState({ a: 10 });
|
|
141
|
-
expect(s2.getState().b).toBe(2);
|
|
142
|
-
});
|
|
143
|
-
it('store A change does not notify store B subscribers', () => {
|
|
144
|
-
const s1 = createStore({ a: 1 });
|
|
145
|
-
const s2 = createStore({ b: 2 });
|
|
146
|
-
const l2 = vi.fn();
|
|
147
|
-
s2.subscribe(l2);
|
|
148
|
-
s1.setState({ a: 10 });
|
|
149
|
-
expect(l2).not.toHaveBeenCalled();
|
|
150
|
-
});
|
|
151
|
-
it('both stores can be subscribed simultaneously', () => {
|
|
152
|
-
const s1 = createStore({ a: 1 });
|
|
153
|
-
const s2 = createStore({ b: 2 });
|
|
154
|
-
const l1 = vi.fn(), l2 = vi.fn();
|
|
155
|
-
s1.subscribe(l1); s2.subscribe(l2);
|
|
156
|
-
s1.setState({ a: 10 });
|
|
157
|
-
s2.setState({ b: 20 });
|
|
158
|
-
expect(l1).toHaveBeenCalledTimes(1);
|
|
159
|
-
expect(l2).toHaveBeenCalledTimes(1);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('3.2 Subscription Lifecycle', () => {
|
|
165
|
-
it('Subscribe → update → unsubscribe → update — listener not called after', () => {
|
|
166
|
-
const store = createStore({ a: 1 });
|
|
167
|
-
const listener = vi.fn();
|
|
168
|
-
const unsub = store.subscribe(listener);
|
|
169
|
-
store.setState({ a: 2 });
|
|
170
|
-
unsub();
|
|
171
|
-
store.setState({ a: 3 });
|
|
172
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('Subscribe → update → re-subscribe → update — listener called again', () => {
|
|
176
|
-
const store = createStore({ a: 1 });
|
|
177
|
-
const listener = vi.fn();
|
|
178
|
-
let unsub = store.subscribe(listener);
|
|
179
|
-
store.setState({ a: 2 });
|
|
180
|
-
unsub();
|
|
181
|
-
unsub = store.subscribe(listener);
|
|
182
|
-
store.setState({ a: 3 });
|
|
183
|
-
expect(listener).toHaveBeenCalledTimes(2);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('Subscribe multiple → unsubscribe all → update — no listeners called', () => {
|
|
187
|
-
const store = createStore({ a: 1 });
|
|
188
|
-
const l1 = vi.fn(), l2 = vi.fn();
|
|
189
|
-
const u1 = store.subscribe(l1);
|
|
190
|
-
const u2 = store.subscribe(l2);
|
|
191
|
-
u1(); u2();
|
|
192
|
-
store.setState({ a: 2 });
|
|
193
|
-
expect(l1).not.toHaveBeenCalled();
|
|
194
|
-
expect(l2).not.toHaveBeenCalled();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('Subscribe → many updates → unsubscribe — correct call count', () => {
|
|
198
|
-
const store = createStore({ a: 1 });
|
|
199
|
-
const listener = vi.fn();
|
|
200
|
-
const unsub = store.subscribe(listener);
|
|
201
|
-
for (let i = 0; i < 10; i++) store.setState({ a: i });
|
|
202
|
-
unsub();
|
|
203
|
-
expect(listener).toHaveBeenCalledTimes(10);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('Subscribe inside subscribe callback — nested subscription works', () => {
|
|
207
|
-
const store = createStore({ a: 1 });
|
|
208
|
-
const l2 = vi.fn();
|
|
209
|
-
let once = false;
|
|
210
|
-
store.subscribe(() => {
|
|
211
|
-
if (!once) { store.subscribe(l2); once = true; }
|
|
212
|
-
});
|
|
213
|
-
store.setState({ a: 2 });
|
|
214
|
-
store.setState({ a: 3 });
|
|
215
|
-
expect(l2).toHaveBeenCalled();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('Unsubscribe inside subscribe callback — safe to call mid-notification', () => {
|
|
219
|
-
const store = createStore({ a: 1 });
|
|
220
|
-
const l1 = vi.fn();
|
|
221
|
-
const unsub1 = store.subscribe(() => {
|
|
222
|
-
l1();
|
|
223
|
-
unsub1();
|
|
224
|
-
});
|
|
225
|
-
store.setState({ a: 2 });
|
|
226
|
-
store.setState({ a: 3 });
|
|
227
|
-
expect(l1).toHaveBeenCalledTimes(1);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('Store with zero subscribers — setState does not throw', () => {
|
|
231
|
-
const store = createStore({ a: 1 });
|
|
232
|
-
expect(() => store.setState({ a: 2 })).not.toThrow();
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('Store with zero subscribers — getState still works', () => {
|
|
236
|
-
const store = createStore({ a: 1 });
|
|
237
|
-
store.setState({ a: 2 });
|
|
238
|
-
expect(store.getState().a).toBe(2);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
describe('3.3 State Immutability', () => {
|
|
243
|
-
it('Mutating returned getState() does not affect store state', () => {
|
|
244
|
-
const store = createStore({ a: 1 });
|
|
245
|
-
const state = store.getState();
|
|
246
|
-
const listener = vi.fn();
|
|
247
|
-
store.subscribe(listener);
|
|
248
|
-
(state as Record<string, unknown>).a = 2;
|
|
249
|
-
expect(listener).not.toHaveBeenCalled();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('Mutating original definition object does not affect store state', () => {
|
|
253
|
-
const initial = { a: 1 };
|
|
254
|
-
const store = createStore(initial);
|
|
255
|
-
const listener = vi.fn();
|
|
256
|
-
store.subscribe(listener);
|
|
257
|
-
initial.a = 2;
|
|
258
|
-
expect(listener).not.toHaveBeenCalled();
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('Mutating setState argument after calling setState has no effect', () => {
|
|
262
|
-
const store = createStore({ obj: { val: 1 } });
|
|
263
|
-
const update = { obj: { val: 2 } };
|
|
264
|
-
store.setState(update);
|
|
265
|
-
const listener = vi.fn();
|
|
266
|
-
store.subscribe(listener);
|
|
267
|
-
update.obj.val = 3;
|
|
268
|
-
expect(listener).not.toHaveBeenCalled();
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('Two stores created from same definition object are independent', () => {
|
|
272
|
-
const def = { a: 1 };
|
|
273
|
-
const s1 = createStore({ ...def });
|
|
274
|
-
const s2 = createStore({ ...def });
|
|
275
|
-
s1.setState({ a: 2 });
|
|
276
|
-
expect(s2.getState().a).toBe(1);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { indexedDBAdapter } from '../../../src/persist/adapters/indexedDB'
|
|
3
|
-
|
|
4
|
-
function createIDBMock() {
|
|
5
|
-
const store = new Map<string, string>()
|
|
6
|
-
const mockDB = {
|
|
7
|
-
objectStoreNames: { contains: vi.fn(() => true) },
|
|
8
|
-
createObjectStore: vi.fn(),
|
|
9
|
-
transaction: vi.fn(() => ({
|
|
10
|
-
objectStore: vi.fn(() => ({
|
|
11
|
-
get: vi.fn((key: string) => {
|
|
12
|
-
const req: { onsuccess: (() => void) | null, onerror: (() => void) | null, result: string | null } = {
|
|
13
|
-
onsuccess: null,
|
|
14
|
-
onerror: null,
|
|
15
|
-
result: store.get(key) ?? null
|
|
16
|
-
}
|
|
17
|
-
setTimeout(() => req.onsuccess?.(), 0)
|
|
18
|
-
return req
|
|
19
|
-
}),
|
|
20
|
-
put: vi.fn((value: string, key: string) => {
|
|
21
|
-
store.set(key, value)
|
|
22
|
-
const req: { onsuccess: (() => void) | null, onerror: (() => void) | null } = { onsuccess: null, onerror: null }
|
|
23
|
-
setTimeout(() => req.onsuccess?.(), 0)
|
|
24
|
-
return req
|
|
25
|
-
}),
|
|
26
|
-
delete: vi.fn((key: string) => {
|
|
27
|
-
store.delete(key)
|
|
28
|
-
const req: { onsuccess: (() => void) | null, onerror: (() => void) | null } = { onsuccess: null, onerror: null }
|
|
29
|
-
setTimeout(() => req.onsuccess?.(), 0)
|
|
30
|
-
return req
|
|
31
|
-
}),
|
|
32
|
-
}))
|
|
33
|
-
}))
|
|
34
|
-
}
|
|
35
|
-
return { store, mockDB }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('indexedDBAdapter', () => {
|
|
39
|
-
let store: Map<string, string>
|
|
40
|
-
let mockDB: ReturnType<typeof createIDBMock>['mockDB']
|
|
41
|
-
let openSpy: ReturnType<typeof vi.fn>
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
const mock = createIDBMock()
|
|
45
|
-
store = mock.store
|
|
46
|
-
mockDB = mock.mockDB
|
|
47
|
-
|
|
48
|
-
openSpy = vi.fn(() => {
|
|
49
|
-
const request: {
|
|
50
|
-
onsuccess: (() => void) | null
|
|
51
|
-
onerror: (() => void) | null
|
|
52
|
-
onupgradeneeded: (() => void) | null
|
|
53
|
-
result: typeof mockDB
|
|
54
|
-
} = {
|
|
55
|
-
onsuccess: null,
|
|
56
|
-
onerror: null,
|
|
57
|
-
onupgradeneeded: null,
|
|
58
|
-
result: mockDB
|
|
59
|
-
}
|
|
60
|
-
setTimeout(() => request.onsuccess?.(), 0)
|
|
61
|
-
return request
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
vi.stubGlobal('indexedDB', { open: openSpy })
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
afterEach(() => {
|
|
68
|
-
vi.restoreAllMocks()
|
|
69
|
-
vi.unstubAllGlobals()
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
describe('Normal environment (indexedDB available)', () => {
|
|
73
|
-
it('getItem returns null for a key that was never set', async () => {
|
|
74
|
-
const adapter = indexedDBAdapter()
|
|
75
|
-
const result = await adapter.getItem('missing')
|
|
76
|
-
expect(result).toBeNull()
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('getItem returns correct value after setItem', async () => {
|
|
80
|
-
const adapter = indexedDBAdapter()
|
|
81
|
-
await adapter.setItem('key', 'value')
|
|
82
|
-
const result = await adapter.getItem('key')
|
|
83
|
-
expect(result).toBe('value')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('setItem stores value correctly', async () => {
|
|
87
|
-
const adapter = indexedDBAdapter()
|
|
88
|
-
await adapter.setItem('key', 'value')
|
|
89
|
-
expect(store.get('key')).toBe('value')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('setItem overwrites existing value', async () => {
|
|
93
|
-
const adapter = indexedDBAdapter()
|
|
94
|
-
await adapter.setItem('key', 'first')
|
|
95
|
-
await adapter.setItem('key', 'second')
|
|
96
|
-
const result = await adapter.getItem('key')
|
|
97
|
-
expect(result).toBe('second')
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('removeItem removes the key (getItem returns null after)', async () => {
|
|
101
|
-
const adapter = indexedDBAdapter()
|
|
102
|
-
await adapter.setItem('delete-me', 'please')
|
|
103
|
-
await adapter.removeItem('delete-me')
|
|
104
|
-
const result = await adapter.getItem('delete-me')
|
|
105
|
-
expect(result).toBeNull()
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('removeItem is a no-op when key does not exist', async () => {
|
|
109
|
-
const adapter = indexedDBAdapter()
|
|
110
|
-
await expect(adapter.removeItem('never-set')).resolves.toBeUndefined()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('DB is opened only once across multiple operations (lazy + shared promise)', async () => {
|
|
114
|
-
const adapter = indexedDBAdapter()
|
|
115
|
-
await adapter.setItem('a', '1')
|
|
116
|
-
await adapter.getItem('a')
|
|
117
|
-
await adapter.removeItem('a')
|
|
118
|
-
|
|
119
|
-
expect(openSpy).toHaveBeenCalledTimes(1)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('custom dbName is used when provided', async () => {
|
|
123
|
-
const adapter = indexedDBAdapter('custom-db')
|
|
124
|
-
await adapter.getItem('test')
|
|
125
|
-
expect(openSpy).toHaveBeenCalledWith('custom-db', 1)
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
describe('Error handling', () => {
|
|
130
|
-
beforeEach(() => {
|
|
131
|
-
// make open fail
|
|
132
|
-
openSpy.mockImplementation(() => {
|
|
133
|
-
const request: { onerror: (() => void) | null } = { onerror: null }
|
|
134
|
-
setTimeout(() => request.onerror?.(), 0)
|
|
135
|
-
return request
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('when DB open fails, getItem resolves to null without throwing', async () => {
|
|
140
|
-
const adapter = indexedDBAdapter()
|
|
141
|
-
const result = await adapter.getItem('key')
|
|
142
|
-
expect(result).toBeNull()
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('when DB open fails, setItem resolves without throwing', async () => {
|
|
146
|
-
const adapter = indexedDBAdapter()
|
|
147
|
-
await expect(adapter.setItem('key', 'val')).resolves.toBeUndefined()
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('when DB open fails, a warning is logged (vi.spyOn console.warn)', async () => {
|
|
151
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
152
|
-
const adapter = indexedDBAdapter()
|
|
153
|
-
await adapter.getItem('key')
|
|
154
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[storve] Failed to open IndexedDB'))
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
describe('SSR guard (indexedDB undefined)', () => {
|
|
159
|
-
beforeEach(() => {
|
|
160
|
-
vi.stubGlobal('indexedDB', undefined)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('getItem returns Promise<null> without throwing', async () => {
|
|
164
|
-
const adapter = indexedDBAdapter()
|
|
165
|
-
const result = await adapter.getItem('ssr-key')
|
|
166
|
-
expect(result).toBeNull()
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('setItem returns Promise<void> without throwing', async () => {
|
|
170
|
-
const adapter = indexedDBAdapter()
|
|
171
|
-
await expect(adapter.setItem('ssr-key', 'value')).resolves.toBeUndefined()
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('removeItem returns Promise<void> without throwing', async () => {
|
|
175
|
-
const adapter = indexedDBAdapter()
|
|
176
|
-
await expect(adapter.removeItem('ssr-key')).resolves.toBeUndefined()
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('indexedDB is never accessed', async () => {
|
|
180
|
-
const adapter = indexedDBAdapter()
|
|
181
|
-
await adapter.getItem('key')
|
|
182
|
-
expect(openSpy).not.toHaveBeenCalled()
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
})
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { vi, describe, it, expect, afterEach } from 'vitest'
|
|
3
|
-
import { localStorageAdapter } from '../../../src/persist/adapters/localStorage'
|
|
4
|
-
|
|
5
|
-
describe('localStorageAdapter', () => {
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
vi.restoreAllMocks()
|
|
8
|
-
vi.unstubAllGlobals()
|
|
9
|
-
if (typeof localStorage !== 'undefined') localStorage.clear()
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
describe('browser environment', () => {
|
|
13
|
-
it('getItem returns null for a key that does not exist', async () => {
|
|
14
|
-
const adapter = localStorageAdapter()
|
|
15
|
-
const result = await adapter.getItem('missing')
|
|
16
|
-
expect(result).toBeNull()
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('getItem returns the correct value after setItem', async () => {
|
|
20
|
-
const adapter = localStorageAdapter()
|
|
21
|
-
await adapter.setItem('key', 'value')
|
|
22
|
-
const result = await adapter.getItem('key')
|
|
23
|
-
expect(result).toBe('value')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('setItem stores value in window.localStorage correctly', async () => {
|
|
27
|
-
const adapter = localStorageAdapter()
|
|
28
|
-
await adapter.setItem('direct', 'data')
|
|
29
|
-
expect(localStorage.getItem('direct')).toBe('data')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('setItem overwrites existing value for the same key', async () => {
|
|
33
|
-
const adapter = localStorageAdapter()
|
|
34
|
-
await adapter.setItem('key', 'first')
|
|
35
|
-
await adapter.setItem('key', 'second')
|
|
36
|
-
const result = await adapter.getItem('key')
|
|
37
|
-
expect(result).toBe('second')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('removeItem removes the key (getItem returns null after)', async () => {
|
|
41
|
-
const adapter = localStorageAdapter()
|
|
42
|
-
await adapter.setItem('delete-me', 'please')
|
|
43
|
-
await adapter.removeItem('delete-me')
|
|
44
|
-
const result = await adapter.getItem('delete-me')
|
|
45
|
-
expect(result).toBeNull()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('removeItem is a no-op when key does not exist (no error)', async () => {
|
|
49
|
-
const adapter = localStorageAdapter()
|
|
50
|
-
expect(() => adapter.removeItem('never-set')).not.toThrow()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('delegates directly to window.localStorage — verify with vi.spyOn', async () => {
|
|
54
|
-
const spySet = vi.spyOn(Storage.prototype, 'setItem')
|
|
55
|
-
const spyGet = vi.spyOn(Storage.prototype, 'getItem')
|
|
56
|
-
const spyRemove = vi.spyOn(Storage.prototype, 'removeItem')
|
|
57
|
-
|
|
58
|
-
const adapter = localStorageAdapter()
|
|
59
|
-
await adapter.setItem('spyKey', 'spyValue')
|
|
60
|
-
expect(spySet).toHaveBeenCalledWith('spyKey', 'spyValue')
|
|
61
|
-
|
|
62
|
-
await adapter.getItem('spyKey')
|
|
63
|
-
expect(spyGet).toHaveBeenCalledWith('spyKey')
|
|
64
|
-
|
|
65
|
-
await adapter.removeItem('spyKey')
|
|
66
|
-
expect(spyRemove).toHaveBeenCalledWith('spyKey')
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe('SSR — window undefined', () => {
|
|
71
|
-
it('getItem returns null without throwing', async () => {
|
|
72
|
-
vi.stubGlobal('window', undefined)
|
|
73
|
-
const adapter = localStorageAdapter()
|
|
74
|
-
const result = await adapter.getItem('ssr-key')
|
|
75
|
-
expect(result).toBeNull()
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('setItem does nothing without throwing', async () => {
|
|
79
|
-
vi.stubGlobal('window', undefined)
|
|
80
|
-
const adapter = localStorageAdapter()
|
|
81
|
-
expect(() => adapter.setItem('ssr-key', 'value')).not.toThrow()
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('removeItem does nothing without throwing', async () => {
|
|
85
|
-
vi.stubGlobal('window', undefined)
|
|
86
|
-
const adapter = localStorageAdapter()
|
|
87
|
-
expect(() => adapter.removeItem('ssr-key')).not.toThrow()
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('localStorage is never accessed when window is undefined', async () => {
|
|
91
|
-
const spyGet = vi.spyOn(Storage.prototype, 'getItem')
|
|
92
|
-
const spySet = vi.spyOn(Storage.prototype, 'setItem')
|
|
93
|
-
|
|
94
|
-
vi.stubGlobal('window', undefined)
|
|
95
|
-
const adapter = localStorageAdapter()
|
|
96
|
-
|
|
97
|
-
await adapter.getItem('key')
|
|
98
|
-
await adapter.setItem('key', 'val')
|
|
99
|
-
await adapter.removeItem('key')
|
|
100
|
-
|
|
101
|
-
expect(spyGet).not.toHaveBeenCalled()
|
|
102
|
-
expect(spySet).not.toHaveBeenCalled()
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
})
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { memoryAdapter } from '../../../src/persist/adapters/memory'
|
|
3
|
-
|
|
4
|
-
describe('memoryAdapter', () => {
|
|
5
|
-
describe('getItem', () => {
|
|
6
|
-
it('returns null for a key that has never been set', async () => {
|
|
7
|
-
const adapter = memoryAdapter()
|
|
8
|
-
const result = await adapter.getItem('missing')
|
|
9
|
-
expect(result).toBeNull()
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('returns the correct value after setItem has been called', async () => {
|
|
13
|
-
const adapter = memoryAdapter()
|
|
14
|
-
await adapter.setItem('key', 'value')
|
|
15
|
-
const result = await adapter.getItem('key')
|
|
16
|
-
expect(result).toBe('value')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('returns null after removeItem has been called on that key', async () => {
|
|
20
|
-
const adapter = memoryAdapter()
|
|
21
|
-
await adapter.setItem('key', 'value')
|
|
22
|
-
await adapter.removeItem('key')
|
|
23
|
-
const result = await adapter.getItem('key')
|
|
24
|
-
expect(result).toBeNull()
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('is case-sensitive (key \'Count\' and key \'count\' are different)', async () => {
|
|
28
|
-
const adapter = memoryAdapter()
|
|
29
|
-
await adapter.setItem('Count', '1')
|
|
30
|
-
await adapter.setItem('count', '2')
|
|
31
|
-
|
|
32
|
-
const countUpper = await adapter.getItem('Count')
|
|
33
|
-
const countLower = await adapter.getItem('count')
|
|
34
|
-
|
|
35
|
-
expect(countUpper).toBe('1')
|
|
36
|
-
expect(countLower).toBe('2')
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
describe('setItem', () => {
|
|
41
|
-
it('stores a string value correctly', async () => {
|
|
42
|
-
const adapter = memoryAdapter()
|
|
43
|
-
await adapter.setItem('token', 'abc')
|
|
44
|
-
expect(await adapter.getItem('token')).toBe('abc')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('overwrites an existing value for the same key', async () => {
|
|
48
|
-
const adapter = memoryAdapter()
|
|
49
|
-
await adapter.setItem('key', 'first')
|
|
50
|
-
await adapter.setItem('key', 'second')
|
|
51
|
-
expect(await adapter.getItem('key')).toBe('second')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('stores multiple keys independently', async () => {
|
|
55
|
-
const adapter = memoryAdapter()
|
|
56
|
-
await adapter.setItem('a', '1')
|
|
57
|
-
await adapter.setItem('b', '2')
|
|
58
|
-
|
|
59
|
-
expect(await adapter.getItem('a')).toBe('1')
|
|
60
|
-
expect(await adapter.getItem('b')).toBe('2')
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('removeItem', () => {
|
|
65
|
-
it('removes an existing key (getItem returns null after)', async () => {
|
|
66
|
-
const adapter = memoryAdapter()
|
|
67
|
-
await adapter.setItem('target', 'content')
|
|
68
|
-
await adapter.removeItem('target')
|
|
69
|
-
expect(await adapter.getItem('target')).toBeNull()
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('is a no-op when key does not exist (no error thrown)', async () => {
|
|
73
|
-
const adapter = memoryAdapter()
|
|
74
|
-
// should not throw
|
|
75
|
-
expect(() => adapter.removeItem('never-set')).not.toThrow()
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
describe('Isolation', () => {
|
|
80
|
-
it('two separate memoryAdapter() instances do not share data', async () => {
|
|
81
|
-
const adapter1 = memoryAdapter()
|
|
82
|
-
const adapter2 = memoryAdapter()
|
|
83
|
-
|
|
84
|
-
await adapter1.setItem('key', 'value')
|
|
85
|
-
expect(await adapter2.getItem('key')).toBeNull()
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('setting a key in instance A does not affect instance B', async () => {
|
|
89
|
-
const adapterA = memoryAdapter()
|
|
90
|
-
const adapterB = memoryAdapter()
|
|
91
|
-
|
|
92
|
-
await adapterA.setItem('shared', 'A')
|
|
93
|
-
await adapterB.setItem('shared', 'B')
|
|
94
|
-
|
|
95
|
-
expect(await adapterA.getItem('shared')).toBe('A')
|
|
96
|
-
expect(await adapterB.getItem('shared')).toBe('B')
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('clearing instance A does not affect instance B', async () => {
|
|
100
|
-
const adapterA = memoryAdapter()
|
|
101
|
-
const adapterB = memoryAdapter()
|
|
102
|
-
|
|
103
|
-
await adapterA.setItem('key', 'value')
|
|
104
|
-
await adapterB.setItem('key', 'value')
|
|
105
|
-
|
|
106
|
-
await adapterA.removeItem('key')
|
|
107
|
-
|
|
108
|
-
expect(await adapterA.getItem('key')).toBeNull()
|
|
109
|
-
expect(await adapterB.getItem('key')).toBe('value')
|
|
110
|
-
})
|
|
111
|
-
})
|
|
112
|
-
})
|