@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
package/tests/proxy.test.ts
DELETED
|
@@ -1,473 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { createStateProxy } from '../src/proxy';
|
|
3
|
-
|
|
4
|
-
describe('2.1 Flat State Proxy Tracking', () => {
|
|
5
|
-
it.each([
|
|
6
|
-
['string', { a: 'str' }, 'a'],
|
|
7
|
-
['number', { a: 1 }, 'a'],
|
|
8
|
-
['boolean', { a: true }, 'a'],
|
|
9
|
-
['null', { a: null }, 'a'],
|
|
10
|
-
['undefined', { a: undefined as unknown }, 'a']
|
|
11
|
-
])('Reading %s key returns correct value', (_, initial, key) => {
|
|
12
|
-
const proxy = createStateProxy(initial as Record<string, unknown>, vi.fn());
|
|
13
|
-
expect((proxy as Record<string, unknown>)[key]).toBe((initial as Record<string, unknown>)[key]);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it.each([
|
|
17
|
-
['string', { a: 'str' }, 'a', 'new-str'],
|
|
18
|
-
['number', { a: 1 }, 'a', 2],
|
|
19
|
-
['boolean', { a: true }, 'a', false],
|
|
20
|
-
['null to a key', { a: 'old' }, 'a', null],
|
|
21
|
-
['undefined to a key', { a: 'old' }, 'a', undefined],
|
|
22
|
-
['0 to a key', { a: 'old' }, 'a', 0],
|
|
23
|
-
['false to a key', { a: 'old' }, 'a', false],
|
|
24
|
-
['empty string to a key', { a: 'old' }, 'a', '']
|
|
25
|
-
])('Writing %s notifies subscribers', (_, initial, key, newValue) => {
|
|
26
|
-
const listener = vi.fn();
|
|
27
|
-
const proxy = createStateProxy(initial as Record<string, unknown>, listener);
|
|
28
|
-
(proxy as Record<string, unknown>)[key] = newValue;
|
|
29
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('Writing same value still notifies subscribers', () => {
|
|
33
|
-
const listener = vi.fn();
|
|
34
|
-
const proxy = createStateProxy({ a: 1 }, listener);
|
|
35
|
-
proxy.a = 1;
|
|
36
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('Multiple writes notify correct number of times', () => {
|
|
40
|
-
const listener = vi.fn();
|
|
41
|
-
const proxy = createStateProxy({ a: 1 }, listener);
|
|
42
|
-
proxy.a = 2;
|
|
43
|
-
proxy.a = 3;
|
|
44
|
-
proxy.a = 4;
|
|
45
|
-
expect(listener).toHaveBeenCalledTimes(3);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('Proxy does not expose internal implementation details', () => {
|
|
49
|
-
const proxy = createStateProxy({ a: 1 }, vi.fn());
|
|
50
|
-
expect((proxy as Record<string, unknown>).__proxy).toBeUndefined();
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('2.2 Nested Object Tracking', () => {
|
|
55
|
-
it.each([
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
-
[1, { l1: { val: 1 } }, (p: any) => p.l1.val],
|
|
58
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
-
[2, { l1: { l2: { val: 2 } } }, (p: any) => p.l1.l2.val],
|
|
60
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
-
[3, { l1: { l2: { l3: { val: 3 } } } }, (p: any) => p.l1.l2.l3.val],
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
-
[5, { l1: { l2: { l3: { l4: { l5: { val: 5 } } } } } }, (p: any) => p.l1.l2.l3.l4.l5.val],
|
|
64
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
-
[10, { l1: { l2: { l3: { l4: { l5: { l6: { l7: { l8: { l9: { l10: { val: 10 } } } } } } } } } } }, (p: any) => p.l1.l2.l3.l4.l5.l6.l7.l8.l9.l10.val]
|
|
66
|
-
])('Reading nested key (%i levels) returns correct value', (val, initial, readFn) => {
|
|
67
|
-
const proxy = createStateProxy(initial, vi.fn());
|
|
68
|
-
expect(readFn(proxy)).toBe(val);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it.each([
|
|
72
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
-
[1, { l1: { val: 0 } }, (p: any) => { p.l1.val = 1; }],
|
|
74
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
-
[2, { l1: { l2: { val: 0 } } }, (p: any) => { p.l1.l2.val = 2; }],
|
|
76
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
-
[3, { l1: { l2: { l3: { val: 0 } } } }, (p: any) => { p.l1.l2.l3.val = 3; }],
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
-
[5, { l1: { l2: { l3: { l4: { l5: { val: 0 } } } } } }, (p: any) => { p.l1.l2.l3.l4.l5.val = 5; }],
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
-
[10, { l1: { l2: { l3: { l4: { l5: { l6: { l7: { l8: { l9: { l10: { val: 0 } } } } } } } } } } }, (p: any) => { p.l1.l2.l3.l4.l5.l6.l7.l8.l9.l10.val = 10; }]
|
|
82
|
-
])('Writing nested key (%i levels) notifies subscribers', (_, initial, writeFn) => {
|
|
83
|
-
const listener = vi.fn();
|
|
84
|
-
const proxy = createStateProxy(initial, listener);
|
|
85
|
-
writeFn(proxy);
|
|
86
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('Adding new key to nested object notifies subscribers', () => {
|
|
90
|
-
const listener = vi.fn();
|
|
91
|
-
const proxy = createStateProxy({ nested: { a: 1 } }, listener);
|
|
92
|
-
(proxy.nested as Record<string, unknown>).b = 2;
|
|
93
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('Deleting key from nested object notifies subscribers', () => {
|
|
97
|
-
const listener = vi.fn();
|
|
98
|
-
const proxy = createStateProxy({ nested: { a: 1 } }, listener);
|
|
99
|
-
delete (proxy.nested as Record<string, unknown>).a;
|
|
100
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('Replacing nested object with new object notifies subscribers', () => {
|
|
104
|
-
const listener = vi.fn();
|
|
105
|
-
const proxy = createStateProxy({ nested: { a: 1 } }, listener);
|
|
106
|
-
proxy.nested = { b: 2 } as unknown as { a: number };
|
|
107
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('New assigned nested object is also tracked (eager wrapping)', () => {
|
|
111
|
-
const listener = vi.fn();
|
|
112
|
-
const proxy = createStateProxy({ nested: { a: 1 } }, listener);
|
|
113
|
-
proxy.nested = { b: 2 } as unknown as { a: number };
|
|
114
|
-
listener.mockClear();
|
|
115
|
-
(proxy.nested as Record<string, number>).b = 3;
|
|
116
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('Replacing nested object — old object is no longer tracked', () => {
|
|
120
|
-
const listener = vi.fn();
|
|
121
|
-
const initialSub = { a: 1 };
|
|
122
|
-
const state = { nested: initialSub as { a: number } | Record<string, number> };
|
|
123
|
-
const proxy = createStateProxy(state, listener);
|
|
124
|
-
proxy.nested = { b: 2 } as Record<string, number>;
|
|
125
|
-
listener.mockClear();
|
|
126
|
-
initialSub.a = 2;
|
|
127
|
-
expect(listener).not.toHaveBeenCalled();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('Two sibling nested objects tracked independently', () => {
|
|
131
|
-
const listener = vi.fn();
|
|
132
|
-
const proxy = createStateProxy({ a: { val: 1 }, b: { val: 2 } }, listener);
|
|
133
|
-
(proxy.a as Record<string, number>).val = 10;
|
|
134
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
135
|
-
(proxy.b as Record<string, number>).val = 20;
|
|
136
|
-
expect(listener).toHaveBeenCalledTimes(2);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('Writing to sibling object does not affect other sibling', () => {
|
|
140
|
-
const proxy = createStateProxy({ a: { val: 1 }, b: { val: 2 } }, vi.fn());
|
|
141
|
-
(proxy.a as Record<string, number>).val = 10;
|
|
142
|
-
expect((proxy.b as Record<string, number>).val).toBe(2);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('null nested value does not crash on read', () => {
|
|
146
|
-
const proxy = createStateProxy({ nested: null }, vi.fn());
|
|
147
|
-
expect(proxy.nested).toBeNull();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('Reassigning nested object to null notifies subscribers', () => {
|
|
151
|
-
const listener = vi.fn();
|
|
152
|
-
const proxy = createStateProxy({ nested: { a: 1 } }, listener);
|
|
153
|
-
(proxy as Record<string, unknown>).nested = null;
|
|
154
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('Reassigning nested object from null to object notifies subscribers', () => {
|
|
158
|
-
const listener = vi.fn();
|
|
159
|
-
const proxy = createStateProxy({ nested: null as { a: number } | null }, listener);
|
|
160
|
-
proxy.nested = { a: 1 };
|
|
161
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('2.3 Array Tracking', () => {
|
|
166
|
-
it('Reading array item by index returns correct value', () => {
|
|
167
|
-
const proxy = createStateProxy({ arr: [1, 2, 3] }, vi.fn());
|
|
168
|
-
expect(proxy.arr[1]).toBe(2);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('Reading array length returns correct value', () => {
|
|
172
|
-
const proxy = createStateProxy({ arr: [1, 2, 3] }, vi.fn());
|
|
173
|
-
expect(proxy.arr.length).toBe(3);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('Setting array item by index notifies subscribers', () => {
|
|
177
|
-
const listener = vi.fn();
|
|
178
|
-
const proxy = createStateProxy({ arr: [1, 2, 3] }, listener);
|
|
179
|
-
proxy.arr[1] = 10;
|
|
180
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it.each([
|
|
184
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
185
|
-
['push()', (p: any) => p.arr.push(4), [1, 2, 3, 4]],
|
|
186
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
187
|
-
['pop()', (p: any) => p.arr.pop(), [1, 2]],
|
|
188
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
189
|
-
['shift()', (p: any) => p.arr.shift(), [2, 3]],
|
|
190
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
-
['unshift()', (p: any) => p.arr.unshift(0), [0, 1, 2, 3]],
|
|
192
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
193
|
-
['splice() removes', (p: any) => p.arr.splice(1, 1), [1, 3]],
|
|
194
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
195
|
-
['splice() adds', (p: any) => p.arr.splice(1, 0, 1.5), [1, 1.5, 2, 3]],
|
|
196
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
197
|
-
['splice() removes and adds', (p: any) => p.arr.splice(1, 1, 2.5), [1, 2.5, 3]],
|
|
198
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
|
-
['sort()', (p: any) => p.arr.sort((a: number, b: number) => b - a), [3, 2, 1]],
|
|
200
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
201
|
-
['reverse()', (p: any) => p.arr.reverse(), [3, 2, 1]]
|
|
202
|
-
])('%s notifies subscribers and modifies correctly', (_, action, expected) => {
|
|
203
|
-
const listener = vi.fn();
|
|
204
|
-
const proxy = createStateProxy({ arr: [1, 2, 3] }, listener);
|
|
205
|
-
action(proxy);
|
|
206
|
-
expect(proxy.arr).toEqual(expected);
|
|
207
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('fill() notifies subscribers', () => {
|
|
211
|
-
const listener = vi.fn();
|
|
212
|
-
const proxy = createStateProxy({ arr: [1, 2, 3] }, listener);
|
|
213
|
-
proxy.arr.fill(0);
|
|
214
|
-
expect(proxy.arr).toEqual([0, 0, 0]);
|
|
215
|
-
expect(listener).toHaveBeenCalled();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('push() multiple items notifies subscribers exactly once', () => {
|
|
219
|
-
const listener = vi.fn();
|
|
220
|
-
const proxy = createStateProxy({ arr: [1] }, listener);
|
|
221
|
-
proxy.arr.push(2, 3, 4);
|
|
222
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('pop() on empty array does not crash', () => {
|
|
226
|
-
const listener = vi.fn();
|
|
227
|
-
const proxy = createStateProxy({ arr: [] }, listener);
|
|
228
|
-
expect(() => proxy.arr.pop()).not.toThrow();
|
|
229
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('splice() on empty array does not crash', () => {
|
|
233
|
-
const listener = vi.fn();
|
|
234
|
-
const proxy = createStateProxy({ arr: [] }, listener);
|
|
235
|
-
expect(() => proxy.arr.splice(0, 1)).not.toThrow();
|
|
236
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it.each([
|
|
240
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
241
|
-
['concat()', (p: any) => p.arr.concat([4]), [1, 2, 3, 4]],
|
|
242
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
243
|
-
['map()', (p: any) => p.arr.map((x: number) => x * 2), [2, 4, 6]],
|
|
244
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
245
|
-
['filter()', (p: any) => p.arr.filter((x: number) => x > 1), [2, 3]]
|
|
246
|
-
])('%s does not mutate original — no notification', (_, action, expectedResult) => {
|
|
247
|
-
const listener = vi.fn();
|
|
248
|
-
const proxy = createStateProxy({ arr: [1, 2, 3] }, listener);
|
|
249
|
-
const result = action(proxy);
|
|
250
|
-
expect(result).toEqual(expectedResult);
|
|
251
|
-
expect(listener).not.toHaveBeenCalled();
|
|
252
|
-
expect(proxy.arr).toEqual([1, 2, 3]);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('forEach() does not mutate original — no notification', () => {
|
|
256
|
-
const listener = vi.fn();
|
|
257
|
-
const proxy = createStateProxy({ arr: [1, 2, 3] }, listener);
|
|
258
|
-
let sum = 0;
|
|
259
|
-
proxy.arr.forEach(x => sum += x);
|
|
260
|
-
expect(sum).toBe(6);
|
|
261
|
-
expect(listener).not.toHaveBeenCalled();
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('Array length assignment notifies subscribers', () => {
|
|
265
|
-
const listener = vi.fn();
|
|
266
|
-
const proxy = createStateProxy({ arr: [1, 2, 3] }, listener);
|
|
267
|
-
proxy.arr.length = 1;
|
|
268
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
269
|
-
expect(proxy.arr).toEqual([1]);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('Direct index assignment beyond length notifies subscribers', () => {
|
|
273
|
-
const listener = vi.fn();
|
|
274
|
-
const proxy = createStateProxy({ arr: [1] }, listener);
|
|
275
|
-
proxy.arr[5] = 10;
|
|
276
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
277
|
-
expect(proxy.arr.length).toBe(6);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('Array of objects — modifying nested object notifies subscribers', () => {
|
|
281
|
-
const listener = vi.fn();
|
|
282
|
-
const proxy = createStateProxy({ arr: [{ id: 1 }] }, listener);
|
|
283
|
-
proxy.arr[0].id = 2;
|
|
284
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('Array push of object — new object is tracked', () => {
|
|
288
|
-
const listener = vi.fn();
|
|
289
|
-
const proxy = createStateProxy({ arr: [] as unknown[] }, listener);
|
|
290
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
291
|
-
proxy.arr.push({ id: 1 } as any);
|
|
292
|
-
listener.mockClear();
|
|
293
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
294
|
-
(proxy.arr[0] as any).id = 2;
|
|
295
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('Nested array inside object is tracked', () => {
|
|
299
|
-
const listener = vi.fn();
|
|
300
|
-
const proxy = createStateProxy({ obj: { arr: [1] } }, listener);
|
|
301
|
-
proxy.obj.arr.push(2);
|
|
302
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('Array inside array is tracked', () => {
|
|
306
|
-
const listener = vi.fn();
|
|
307
|
-
const proxy = createStateProxy({ arr: [[1]] }, listener);
|
|
308
|
-
proxy.arr[0].push(2);
|
|
309
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe('2.4 WeakMap & Proxy Safety', () => {
|
|
314
|
-
it('Same object is not double-proxied', () => {
|
|
315
|
-
const obj = { a: 1 };
|
|
316
|
-
const p1 = createStateProxy(obj, vi.fn());
|
|
317
|
-
const p2 = createStateProxy(obj, vi.fn());
|
|
318
|
-
expect(p1).toBe(p2);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('Proxied object passed to createStore does not double-wrap', () => {
|
|
322
|
-
const obj = { a: 1 };
|
|
323
|
-
const p1 = createStateProxy(obj, vi.fn());
|
|
324
|
-
const p2 = createStateProxy(p1, vi.fn());
|
|
325
|
-
expect(p2).toBe(p1);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('Proxy identity check — proxied object is not strictly equal to raw', () => {
|
|
329
|
-
const obj = { a: 1 };
|
|
330
|
-
const proxy = createStateProxy(obj, vi.fn());
|
|
331
|
-
expect(proxy).not.toBe(obj);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('Proxy does not interfere with instanceof checks', () => {
|
|
335
|
-
class MyClass { }
|
|
336
|
-
const proxy = createStateProxy({ inst: new MyClass() }, vi.fn());
|
|
337
|
-
expect(proxy.inst instanceof MyClass).toBe(true);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it('Proxy does not interfere with typeof checks', () => {
|
|
341
|
-
const proxy = createStateProxy({ a: 1 }, vi.fn());
|
|
342
|
-
expect(typeof proxy).toBe('object');
|
|
343
|
-
expect(typeof proxy.a).toBe('number');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it.each([
|
|
347
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
348
|
-
['Object.keys()', (p: any) => Object.keys(p), ['a', 'b']],
|
|
349
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
350
|
-
['Object.values()', (p: any) => Object.values(p), [1, 2]],
|
|
351
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
352
|
-
['Object.entries()', (p: any) => Object.entries(p), [['a', 1], ['b', 2]]]
|
|
353
|
-
])('%s works correctly on proxied object', (_, fn, expected) => {
|
|
354
|
-
const proxy = createStateProxy({ a: 1, b: 2 }, vi.fn());
|
|
355
|
-
expect(fn(proxy)).toEqual(expected);
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('JSON.stringify() works correctly on proxied object', () => {
|
|
359
|
-
const proxy = createStateProxy({ a: 1 }, vi.fn());
|
|
360
|
-
expect(JSON.stringify(proxy)).toBe('{"a":1}');
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it('Spread operator works correctly on proxied object', () => {
|
|
364
|
-
const proxy = createStateProxy({ a: 1, b: 2 }, vi.fn());
|
|
365
|
-
const spread = { ...proxy };
|
|
366
|
-
expect(spread).toEqual({ a: 1, b: 2 });
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('Destructuring works correctly on proxied object', () => {
|
|
370
|
-
const proxy = createStateProxy({ a: 1, b: 2 }, vi.fn());
|
|
371
|
-
const { a, b } = proxy;
|
|
372
|
-
expect(a).toBe(1);
|
|
373
|
-
expect(b).toBe(2);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it('for...in loop works correctly on proxied object', () => {
|
|
377
|
-
const proxy = createStateProxy({ a: 1, b: 2 }, vi.fn());
|
|
378
|
-
const keys: string[] = [];
|
|
379
|
-
for (const k in proxy) keys.push(k);
|
|
380
|
-
expect(keys).toEqual(['a', 'b']);
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('for...of loop works correctly on proxied array', () => {
|
|
384
|
-
const proxy = createStateProxy({ arr: [1, 2] }, vi.fn());
|
|
385
|
-
const vals: number[] = [];
|
|
386
|
-
for (const v of proxy.arr) vals.push(v);
|
|
387
|
-
expect(vals).toEqual([1, 2]);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('in operator works correctly on proxied object', () => {
|
|
391
|
-
const proxy = createStateProxy({ a: 1 }, vi.fn());
|
|
392
|
-
expect('a' in proxy).toBe(true);
|
|
393
|
-
expect('b' in proxy).toBe(false);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('hasOwnProperty works correctly on proxied object', () => {
|
|
397
|
-
const proxy = createStateProxy({ a: 1 }, vi.fn());
|
|
398
|
-
expect(Object.prototype.hasOwnProperty.call(proxy, 'a')).toBe(true);
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
describe('2.5 Non-Proxied Value Types', () => {
|
|
403
|
-
it.each([
|
|
404
|
-
['Date object', new Date()],
|
|
405
|
-
['RegExp', /test/],
|
|
406
|
-
['Function', () => { }],
|
|
407
|
-
['Map', new Map()],
|
|
408
|
-
['Set', new Set()],
|
|
409
|
-
['Primitive string', 'str'],
|
|
410
|
-
['Primitive number', 42],
|
|
411
|
-
['Primitive boolean', true],
|
|
412
|
-
['null', null],
|
|
413
|
-
['undefined', undefined]
|
|
414
|
-
])('%s is not wrapped in Proxy', (_, val) => {
|
|
415
|
-
const proxy = createStateProxy(val as object, vi.fn());
|
|
416
|
-
expect(proxy).toBe(val);
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it('Class instance is not wrapped in Proxy', () => {
|
|
420
|
-
class MyClass { }
|
|
421
|
-
const inst = new MyClass();
|
|
422
|
-
const proxy = createStateProxy(inst as object, vi.fn());
|
|
423
|
-
expect(proxy).toBe(inst);
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
describe('2.6 Assigning a Proxy Value into Another Property', () => {
|
|
428
|
-
it('Assigning a proxied object to another key unwraps and stores the raw value', () => {
|
|
429
|
-
// proxy.b is itself a proxied nested object; assigning it to proxy.a
|
|
430
|
-
// must hit the rawMap.has(value) === true branch and unwrap it.
|
|
431
|
-
const listener = vi.fn();
|
|
432
|
-
const proxy = createStateProxy({ a: { x: 1 }, b: { x: 2 } }, listener);
|
|
433
|
-
|
|
434
|
-
// Read proxy.b — this returns the proxied wrapper for { x: 2 }
|
|
435
|
-
const proxiedB = proxy.b;
|
|
436
|
-
|
|
437
|
-
// Assign the proxy to proxy.a — exercises the true branch on line 51
|
|
438
|
-
proxy.a = proxiedB as typeof proxy.a;
|
|
439
|
-
expect(listener).toHaveBeenCalled();
|
|
440
|
-
|
|
441
|
-
// After assignment, proxy.a should reflect the correct value
|
|
442
|
-
expect(proxy.a.x).toBe(2);
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it('Object assigned via proxy reference is still tracked after assignment', () => {
|
|
446
|
-
// Ensures the assigned (unwrapped) object gets re-proxied so writes to
|
|
447
|
-
// proxy.a still notify subscribers.
|
|
448
|
-
const listener = vi.fn();
|
|
449
|
-
const proxy = createStateProxy({ a: { x: 1 }, b: { x: 2 } }, listener);
|
|
450
|
-
proxy.a = proxy.b as typeof proxy.a; // true branch: rawMap.has(proxy.b) === true
|
|
451
|
-
listener.mockClear();
|
|
452
|
-
|
|
453
|
-
proxy.a.x = 99;
|
|
454
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
455
|
-
expect(proxy.a.x).toBe(99);
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
describe('2.7 Notification Correctness', () => {
|
|
460
|
-
it('Listener does not fire when no state changes', () => {
|
|
461
|
-
const listener = vi.fn();
|
|
462
|
-
const proxy = createStateProxy({ a: 1 }, listener);
|
|
463
|
-
proxy.a;
|
|
464
|
-
expect(listener).not.toHaveBeenCalled();
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it('Nested write fires listener exactly once', () => {
|
|
468
|
-
const listener = vi.fn();
|
|
469
|
-
const proxy = createStateProxy({ a: { b: 1 } }, listener);
|
|
470
|
-
proxy.a.b = 2;
|
|
471
|
-
expect(listener).toHaveBeenCalledTimes(1);
|
|
472
|
-
});
|
|
473
|
-
});
|
package/tests/registry.test.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 2 spike: Extension registry and no-op extension validation.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
5
|
-
import { createStore } from '../src/store';
|
|
6
|
-
import { registerExtension, __testingOnlyClearExtensions } from '../src/registry';
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
__testingOnlyClearExtensions();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
describe('Extension registry spike', () => {
|
|
13
|
-
it('store works with no extensions', () => {
|
|
14
|
-
const store = createStore({ count: 0, name: 'test' });
|
|
15
|
-
expect(store.getState()).toEqual({ count: 0, name: 'test' });
|
|
16
|
-
store.setState({ count: 1 });
|
|
17
|
-
expect(store.getState()).toEqual({ count: 1, name: 'test' });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('store works with no-op extension registered', () => {
|
|
21
|
-
registerExtension({
|
|
22
|
-
key: '__noop',
|
|
23
|
-
processDefinition: (def) => ({ state: { ...def } }),
|
|
24
|
-
});
|
|
25
|
-
const store = createStore({ count: 0, name: 'test' });
|
|
26
|
-
expect(store.getState()).toEqual({ count: 0, name: 'test' });
|
|
27
|
-
store.setState({ count: 42 });
|
|
28
|
-
expect(store.getState()).toEqual({ count: 42, name: 'test' });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('extension can transform definition (pass-through)', () => {
|
|
32
|
-
registerExtension({
|
|
33
|
-
key: '__passthrough',
|
|
34
|
-
processDefinition: (def) => ({ state: { ...def, extra: 'added' } }),
|
|
35
|
-
});
|
|
36
|
-
const store = createStore({ a: 1 });
|
|
37
|
-
expect(store.getState()).toEqual({ a: 1, extra: 'added' });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('noop extension registers on import and passes through', async () => {
|
|
41
|
-
__testingOnlyClearExtensions();
|
|
42
|
-
await import('../src/extensions/noop');
|
|
43
|
-
const store = createStore({ x: 1, y: 2 });
|
|
44
|
-
expect(store.getState()).toEqual({ x: 1, y: 2 });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('store without async extension has stub methods that throw', async () => {
|
|
48
|
-
__testingOnlyClearExtensions();
|
|
49
|
-
const store = createStore({ count: 0 });
|
|
50
|
-
await expect(store.fetch('count' as never)).rejects.toThrow(/no async key.*Import "storve\/async"/);
|
|
51
|
-
expect(store.getAsyncState('count' as never)).toBeUndefined();
|
|
52
|
-
await store.refetch('count' as never);
|
|
53
|
-
store.invalidate('count' as never);
|
|
54
|
-
store.invalidateAll();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('subscribe during batch with pending changes runs runOnStateChanged', () => {
|
|
58
|
-
__testingOnlyClearExtensions();
|
|
59
|
-
const store = createStore({ a: 1 });
|
|
60
|
-
const listener = vi.fn();
|
|
61
|
-
store.batch(() => {
|
|
62
|
-
store.setState({ a: 2 });
|
|
63
|
-
store.subscribe(listener);
|
|
64
|
-
});
|
|
65
|
-
expect(listener).toHaveBeenCalledWith({ a: 2 });
|
|
66
|
-
});
|
|
67
|
-
});
|