@storve/react 1.0.0
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/benchmarks/week3.ts +102 -0
- package/coverage/coverage-summary.json +5 -0
- package/dist/index.cjs +97 -0
- package/dist/index.cjs.js +9 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +7 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.mjs +94 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/useDevtools.d.ts +23 -0
- package/dist/useDevtools.d.ts.map +1 -0
- package/dist/useStore.d.ts +5 -0
- package/dist/useStore.d.ts.map +1 -0
- package/package.json +40 -0
- package/rollup.config.mjs +25 -0
- package/src/index.ts +4 -0
- package/src/types.ts +16 -0
- package/src/useDevtools.ts +74 -0
- package/src/useStore.ts +83 -0
- package/test_output.txt +234 -0
- package/tests/computed.react.test.tsx +71 -0
- package/tests/concurrent.test.tsx +101 -0
- package/tests/index.test.tsx +29 -0
- package/tests/integration.test.tsx +135 -0
- package/tests/lifecycle.test.tsx +148 -0
- package/tests/selector.test.tsx +288 -0
- package/tests/setup.ts +7 -0
- package/tests/useDevtools.test.tsx +80 -0
- package/tests/useStore.test.tsx +233 -0
- package/tsconfig.json +16 -0
- package/vitest.config.mts +30 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { performance } from 'perf_hooks'
|
|
2
|
+
import { createStore } from '@storve/core'
|
|
3
|
+
|
|
4
|
+
type BenchmarkResult = {
|
|
5
|
+
operation: string
|
|
6
|
+
averageMs: string
|
|
7
|
+
status: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function bench(label: string, fn: () => void, iterations = 100_000): BenchmarkResult {
|
|
11
|
+
for (let i = 0; i < 1000; i++) fn()
|
|
12
|
+
|
|
13
|
+
const start = performance.now()
|
|
14
|
+
for (let i = 0; i < iterations; i++) fn()
|
|
15
|
+
const avg = (performance.now() - start) / iterations
|
|
16
|
+
|
|
17
|
+
const limits: Record<string, number> = {
|
|
18
|
+
'useStore() subscription setup': 0.5,
|
|
19
|
+
'useStore() subscription cleanup': 0.5,
|
|
20
|
+
'selector execution (primitive)': 0.1,
|
|
21
|
+
'selector execution (derived)': 0.1,
|
|
22
|
+
'setState() write + notify (10 subs)': 1,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const limit = limits[label] ?? 1
|
|
26
|
+
const status = avg <= limit ? '✅ PASS' : '❌ FAIL'
|
|
27
|
+
return { operation: label, averageMs: avg.toFixed(8) + 'ms', status }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function runBenchmarks(): void {
|
|
31
|
+
console.log('\n⚡ Storve React Adapter — Benchmark Results\n')
|
|
32
|
+
|
|
33
|
+
const results: BenchmarkResult[] = []
|
|
34
|
+
|
|
35
|
+
// 1. Subscription setup
|
|
36
|
+
const store1 = createStore({ count: 0 })
|
|
37
|
+
results.push(bench('useStore() subscription setup', () => {
|
|
38
|
+
const unsub = store1.subscribe(() => {})
|
|
39
|
+
unsub()
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
// 2. Subscription cleanup
|
|
43
|
+
const store2 = createStore({ count: 0 })
|
|
44
|
+
const unsubs: Array<() => void> = []
|
|
45
|
+
results.push(bench('useStore() subscription cleanup', () => {
|
|
46
|
+
const unsub = store2.subscribe(() => {})
|
|
47
|
+
unsubs.push(unsub)
|
|
48
|
+
unsubs.pop()?.()
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
// 3. Selector execution primitive
|
|
52
|
+
const store3 = createStore({ count: 42, name: 'test' })
|
|
53
|
+
results.push(bench('selector execution (primitive)', () => {
|
|
54
|
+
const state = store3.getState()
|
|
55
|
+
void state.count
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
// 4. Selector execution derived
|
|
59
|
+
const store4 = createStore({ a: 10, b: 20 })
|
|
60
|
+
results.push(bench('selector execution (derived)', () => {
|
|
61
|
+
const state = store4.getState()
|
|
62
|
+
void (state.a + state.b)
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
// 5. setState + notify
|
|
66
|
+
const store5 = createStore({ count: 0 })
|
|
67
|
+
for (let i = 0; i < 10; i++) store5.subscribe(() => {})
|
|
68
|
+
let c = 0
|
|
69
|
+
results.push(bench('setState() write + notify (10 subs)', () => {
|
|
70
|
+
store5.setState({ count: c++ })
|
|
71
|
+
}, 10_000))
|
|
72
|
+
|
|
73
|
+
const colWidths = { operation: 45, averageMs: 20, status: 10 }
|
|
74
|
+
const header =
|
|
75
|
+
'Operation'.padEnd(colWidths.operation) +
|
|
76
|
+
'Average Time'.padEnd(colWidths.averageMs) +
|
|
77
|
+
'Status'
|
|
78
|
+
const divider = '-'.repeat(header.length)
|
|
79
|
+
|
|
80
|
+
console.log(header)
|
|
81
|
+
console.log(divider)
|
|
82
|
+
|
|
83
|
+
let allPassed = true
|
|
84
|
+
for (const r of results) {
|
|
85
|
+
if (r.status.includes('FAIL')) allPassed = false
|
|
86
|
+
console.log(
|
|
87
|
+
r.operation.padEnd(colWidths.operation) +
|
|
88
|
+
r.averageMs.padEnd(colWidths.averageMs) +
|
|
89
|
+
r.status
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(divider)
|
|
94
|
+
console.log(allPassed
|
|
95
|
+
? '\n✅ All benchmarks passed!\n'
|
|
96
|
+
: '\n❌ Some benchmarks failed!\n'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if (!allPassed) process.exit(1)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
runBenchmarks()
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{"total": {"lines":{"total":89,"covered":89,"skipped":0,"pct":100},"statements":{"total":89,"covered":89,"skipped":0,"pct":100},"functions":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":43,"covered":38,"skipped":0,"pct":88.37},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"}}
|
|
2
|
+
,"/Users/dipanshusrivastava/Desktop/React Flux/packages/storve-react/src/index.ts": {"lines":{"total":1,"covered":1,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":1,"covered":1,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
|
3
|
+
,"/Users/dipanshusrivastava/Desktop/React Flux/packages/storve-react/src/useDevtools.ts": {"lines":{"total":21,"covered":21,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":21,"covered":21,"skipped":0,"pct":100},"branches":{"total":8,"covered":3,"skipped":0,"pct":37.5}}
|
|
4
|
+
,"/Users/dipanshusrivastava/Desktop/React Flux/packages/storve-react/src/useStore.ts": {"lines":{"total":67,"covered":67,"skipped":0,"pct":100},"functions":{"total":2,"covered":2,"skipped":0,"pct":100},"statements":{"total":67,"covered":67,"skipped":0,"pct":100},"branches":{"total":35,"covered":35,"skipped":0,"pct":100}}
|
|
5
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
function shallowEqual(a, b) {
|
|
6
|
+
if (Object.is(a, b))
|
|
7
|
+
return true;
|
|
8
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null)
|
|
9
|
+
return false;
|
|
10
|
+
const keysA = Object.keys(a);
|
|
11
|
+
const keysB = Object.keys(b);
|
|
12
|
+
if (keysA.length !== keysB.length)
|
|
13
|
+
return false;
|
|
14
|
+
for (const k of keysA) {
|
|
15
|
+
if (!Object.prototype.hasOwnProperty.call(b, k) ||
|
|
16
|
+
!Object.is(a[k], b[k]))
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
function useStore(store, selector) {
|
|
22
|
+
const lastResult = react.useRef(undefined);
|
|
23
|
+
const hasUpdate = react.useRef(false);
|
|
24
|
+
const lastStore = react.useRef(null);
|
|
25
|
+
const subscribe = react.useCallback((callback) => {
|
|
26
|
+
return store.subscribe(() => {
|
|
27
|
+
hasUpdate.current = true;
|
|
28
|
+
callback();
|
|
29
|
+
});
|
|
30
|
+
}, [store]);
|
|
31
|
+
const getSnapshot = react.useCallback(() => {
|
|
32
|
+
const state = store.getState();
|
|
33
|
+
// Invalidate cache when store reference changes
|
|
34
|
+
if (store !== lastStore.current) {
|
|
35
|
+
lastStore.current = store;
|
|
36
|
+
lastResult.current = undefined;
|
|
37
|
+
hasUpdate.current = true;
|
|
38
|
+
}
|
|
39
|
+
if (selector) {
|
|
40
|
+
const next = selector(state);
|
|
41
|
+
if (Object.is(next, lastResult.current))
|
|
42
|
+
return lastResult.current;
|
|
43
|
+
if (typeof next === 'object' &&
|
|
44
|
+
next !== null &&
|
|
45
|
+
lastResult.current !== undefined &&
|
|
46
|
+
shallowEqual(next, lastResult.current)) {
|
|
47
|
+
return lastResult.current;
|
|
48
|
+
}
|
|
49
|
+
lastResult.current = next;
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
// No selector: return shallow copy when store updated
|
|
53
|
+
if (hasUpdate.current || lastResult.current === undefined) {
|
|
54
|
+
hasUpdate.current = false;
|
|
55
|
+
lastResult.current = { ...state }; // ✅ just cache state, don't merge actions here
|
|
56
|
+
}
|
|
57
|
+
return lastResult.current;
|
|
58
|
+
}, [store, selector]);
|
|
59
|
+
const result = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
60
|
+
// Handle selector and no-selector cases separately
|
|
61
|
+
if (selector) {
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
// No selector — merge actions at return time only, not inside getSnapshot
|
|
65
|
+
return Object.assign({}, result, store.actions);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A React hook that subscribes to a devtools-enabled store and returns reactive devtools state.
|
|
70
|
+
*
|
|
71
|
+
* @param store - The Storve store instance (must be wrapped with withDevtools)
|
|
72
|
+
* @returns An object containing canUndo, canRedo, history, and snapshots.
|
|
73
|
+
*/
|
|
74
|
+
function useDevtools(store) {
|
|
75
|
+
const devStore = store;
|
|
76
|
+
const subscribe = react.useCallback((callback) => store.subscribe(callback), [store]);
|
|
77
|
+
const getSnapshot = react.useCallback(() => {
|
|
78
|
+
const internals = devStore.__devtools;
|
|
79
|
+
if (!internals)
|
|
80
|
+
return '';
|
|
81
|
+
// Combine stable markers to identify changes that require re-render
|
|
82
|
+
return `${internals.buffer.cursor}-${internals.snapshots.size}`;
|
|
83
|
+
}, [devStore]);
|
|
84
|
+
react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
85
|
+
// Return the actual properties from the store
|
|
86
|
+
// These are augmented by withDevtools extension
|
|
87
|
+
return {
|
|
88
|
+
canUndo: devStore.canUndo ?? false,
|
|
89
|
+
canRedo: devStore.canRedo ?? false,
|
|
90
|
+
history: devStore.history ?? [],
|
|
91
|
+
snapshots: devStore.snapshots ?? [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
exports.useDevtools = useDevtools;
|
|
96
|
+
exports.useStore = useStore;
|
|
97
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/useStore.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;SAEgB,QAAQ,GAAA;AACpB,IAAA,OAAO,EAAE;AACb;;;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/useStore.ts","../src/useDevtools.ts"],"sourcesContent":[null,null],"names":["useRef","useCallback","useSyncExternalStore"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/useStore.ts"],"sourcesContent":[null],"names":[],"mappings":"AAAA;SAEgB,QAAQ,GAAA;AACpB,IAAA,OAAO,EAAE;AACb;;;;"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useRef, useCallback, useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
function shallowEqual(a, b) {
|
|
4
|
+
if (Object.is(a, b))
|
|
5
|
+
return true;
|
|
6
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null)
|
|
7
|
+
return false;
|
|
8
|
+
const keysA = Object.keys(a);
|
|
9
|
+
const keysB = Object.keys(b);
|
|
10
|
+
if (keysA.length !== keysB.length)
|
|
11
|
+
return false;
|
|
12
|
+
for (const k of keysA) {
|
|
13
|
+
if (!Object.prototype.hasOwnProperty.call(b, k) ||
|
|
14
|
+
!Object.is(a[k], b[k]))
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
function useStore(store, selector) {
|
|
20
|
+
const lastResult = useRef(undefined);
|
|
21
|
+
const hasUpdate = useRef(false);
|
|
22
|
+
const lastStore = useRef(null);
|
|
23
|
+
const subscribe = useCallback((callback) => {
|
|
24
|
+
return store.subscribe(() => {
|
|
25
|
+
hasUpdate.current = true;
|
|
26
|
+
callback();
|
|
27
|
+
});
|
|
28
|
+
}, [store]);
|
|
29
|
+
const getSnapshot = useCallback(() => {
|
|
30
|
+
const state = store.getState();
|
|
31
|
+
// Invalidate cache when store reference changes
|
|
32
|
+
if (store !== lastStore.current) {
|
|
33
|
+
lastStore.current = store;
|
|
34
|
+
lastResult.current = undefined;
|
|
35
|
+
hasUpdate.current = true;
|
|
36
|
+
}
|
|
37
|
+
if (selector) {
|
|
38
|
+
const next = selector(state);
|
|
39
|
+
if (Object.is(next, lastResult.current))
|
|
40
|
+
return lastResult.current;
|
|
41
|
+
if (typeof next === 'object' &&
|
|
42
|
+
next !== null &&
|
|
43
|
+
lastResult.current !== undefined &&
|
|
44
|
+
shallowEqual(next, lastResult.current)) {
|
|
45
|
+
return lastResult.current;
|
|
46
|
+
}
|
|
47
|
+
lastResult.current = next;
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
// No selector: return shallow copy when store updated
|
|
51
|
+
if (hasUpdate.current || lastResult.current === undefined) {
|
|
52
|
+
hasUpdate.current = false;
|
|
53
|
+
lastResult.current = { ...state }; // ✅ just cache state, don't merge actions here
|
|
54
|
+
}
|
|
55
|
+
return lastResult.current;
|
|
56
|
+
}, [store, selector]);
|
|
57
|
+
const result = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
58
|
+
// Handle selector and no-selector cases separately
|
|
59
|
+
if (selector) {
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
// No selector — merge actions at return time only, not inside getSnapshot
|
|
63
|
+
return Object.assign({}, result, store.actions);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A React hook that subscribes to a devtools-enabled store and returns reactive devtools state.
|
|
68
|
+
*
|
|
69
|
+
* @param store - The Storve store instance (must be wrapped with withDevtools)
|
|
70
|
+
* @returns An object containing canUndo, canRedo, history, and snapshots.
|
|
71
|
+
*/
|
|
72
|
+
function useDevtools(store) {
|
|
73
|
+
const devStore = store;
|
|
74
|
+
const subscribe = useCallback((callback) => store.subscribe(callback), [store]);
|
|
75
|
+
const getSnapshot = useCallback(() => {
|
|
76
|
+
const internals = devStore.__devtools;
|
|
77
|
+
if (!internals)
|
|
78
|
+
return '';
|
|
79
|
+
// Combine stable markers to identify changes that require re-render
|
|
80
|
+
return `${internals.buffer.cursor}-${internals.snapshots.size}`;
|
|
81
|
+
}, [devStore]);
|
|
82
|
+
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
83
|
+
// Return the actual properties from the store
|
|
84
|
+
// These are augmented by withDevtools extension
|
|
85
|
+
return {
|
|
86
|
+
canUndo: devStore.canUndo ?? false,
|
|
87
|
+
canRedo: devStore.canRedo ?? false,
|
|
88
|
+
history: devStore.history ?? [],
|
|
89
|
+
snapshots: devStore.snapshots ?? [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { useDevtools, useStore };
|
|
94
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/useStore.ts","../src/useDevtools.ts"],"sourcesContent":[null,null],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Store, StoreState, StoreActions } from '@storve/core';
|
|
2
|
+
/**
|
|
3
|
+
* Selector function — derives a value from store state
|
|
4
|
+
*/
|
|
5
|
+
export type Selector<D extends object, S> = (state: StoreState<D>) => S;
|
|
6
|
+
/**
|
|
7
|
+
* Overloaded useStore hook type
|
|
8
|
+
*/
|
|
9
|
+
export type UseStoreResult<D extends object, S = StoreState<D>> = S & StoreActions<D>;
|
|
10
|
+
export type UseStore = {
|
|
11
|
+
<D extends object>(store: Store<D>): UseStoreResult<D>;
|
|
12
|
+
<D extends object, S>(store: Store<D>, selector: Selector<D, S>): UseStoreResult<D, S>;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEnE;;GAEG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;AAEvE;;GAEG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;AAErF,MAAM,MAAM,QAAQ,GAAG;IACnB,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;IACtD,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;CACzF,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Store } from '@storve/core';
|
|
2
|
+
/**
|
|
3
|
+
* A single entry in the devtools history buffer.
|
|
4
|
+
* Mirrors the HistoryEntry type from storve/devtools without requiring a subpath import.
|
|
5
|
+
*/
|
|
6
|
+
export interface HistoryEntry<S> {
|
|
7
|
+
state: S;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
actionName: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A React hook that subscribes to a devtools-enabled store and returns reactive devtools state.
|
|
13
|
+
*
|
|
14
|
+
* @param store - The Storve store instance (must be wrapped with withDevtools)
|
|
15
|
+
* @returns An object containing canUndo, canRedo, history, and snapshots.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useDevtools<S extends object>(store: Store<S>): {
|
|
18
|
+
canUndo: boolean;
|
|
19
|
+
canRedo: boolean;
|
|
20
|
+
history: readonly HistoryEntry<S>[];
|
|
21
|
+
snapshots: readonly string[];
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=useDevtools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDevtools.d.ts","sourceRoot":"","sources":["../src/useDevtools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C;;;GAGG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACtB;AAyBD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,SAAS,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;IACpC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC,CAyBA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Store, StoreState } from '@storve/core';
|
|
2
|
+
import type { Selector, UseStoreResult } from './types';
|
|
3
|
+
export declare function shallowEqual(a: unknown, b: unknown): boolean;
|
|
4
|
+
export declare function useStore<D extends object, S = StoreState<D>>(store: Store<D>, selector?: Selector<D, S>): UseStoreResult<D, S>;
|
|
5
|
+
//# sourceMappingURL=useStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStore.d.ts","sourceRoot":"","sources":["../src/useStore.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEvD,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAgB5D;AAED,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,EACxD,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EACf,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GAC1B,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAyDtB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@storve/react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.cjs",
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "rollup -c",
|
|
16
|
+
"test": "vitest run --coverage && npx tsx benchmarks/week3.ts",
|
|
17
|
+
"test:watch": "vitest --coverage",
|
|
18
|
+
"test:coverage": "vitest run --coverage",
|
|
19
|
+
"test:bench": "npx tsx benchmarks/week3.ts",
|
|
20
|
+
"lint": "eslint src --ext .ts,.tsx"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@storve/core": "1.0.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
30
|
+
"@testing-library/react": "^16.3.2",
|
|
31
|
+
"@types/react": "^18.2.0",
|
|
32
|
+
"@types/react-dom": "^18.2.0",
|
|
33
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
34
|
+
"jsdom": "^24.1.3",
|
|
35
|
+
"react": "^18.3.1",
|
|
36
|
+
"react-dom": "^18.3.1",
|
|
37
|
+
"tsx": "^4.21.0",
|
|
38
|
+
"vitest": "^2.1.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import typescript from '@rollup/plugin-typescript';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
input: 'src/index.ts',
|
|
5
|
+
external: ['react', '@storve/core'],
|
|
6
|
+
output: [
|
|
7
|
+
{
|
|
8
|
+
file: 'dist/index.cjs',
|
|
9
|
+
format: 'cjs',
|
|
10
|
+
sourcemap: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
file: 'dist/index.mjs',
|
|
14
|
+
format: 'es',
|
|
15
|
+
sourcemap: true,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
plugins: [
|
|
19
|
+
typescript({
|
|
20
|
+
tsconfig: './tsconfig.json',
|
|
21
|
+
declaration: true,
|
|
22
|
+
declarationDir: 'dist',
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
};
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Store, StoreState, StoreActions } from '@storve/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Selector function — derives a value from store state
|
|
5
|
+
*/
|
|
6
|
+
export type Selector<D extends object, S> = (state: StoreState<D>) => S
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Overloaded useStore hook type
|
|
10
|
+
*/
|
|
11
|
+
export type UseStoreResult<D extends object, S = StoreState<D>> = S & StoreActions<D>
|
|
12
|
+
|
|
13
|
+
export type UseStore = {
|
|
14
|
+
<D extends object>(store: Store<D>): UseStoreResult<D>
|
|
15
|
+
<D extends object, S>(store: Store<D>, selector: Selector<D, S>): UseStoreResult<D, S>
|
|
16
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useSyncExternalStore, useCallback } from 'react';
|
|
2
|
+
import type { Store } from '@storve/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A single entry in the devtools history buffer.
|
|
6
|
+
* Mirrors the HistoryEntry type from storve/devtools without requiring a subpath import.
|
|
7
|
+
*/
|
|
8
|
+
export interface HistoryEntry<S> {
|
|
9
|
+
state: S;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
actionName: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Internal shape of the __devtools property attached by withDevtools.
|
|
16
|
+
* Defined locally to avoid importing internal storve implementation details.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
interface DevtoolsShape<S> {
|
|
20
|
+
buffer: { entries: HistoryEntry<S>[]; cursor: number; capacity: number };
|
|
21
|
+
snapshots: Map<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Augmented store type with devtools properties.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
type StoreWithDevtools<S extends object> = Store<S> & {
|
|
29
|
+
__devtools?: DevtoolsShape<S>;
|
|
30
|
+
canUndo?: boolean;
|
|
31
|
+
canRedo?: boolean;
|
|
32
|
+
history?: readonly HistoryEntry<S>[];
|
|
33
|
+
snapshots?: readonly string[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A React hook that subscribes to a devtools-enabled store and returns reactive devtools state.
|
|
39
|
+
*
|
|
40
|
+
* @param store - The Storve store instance (must be wrapped with withDevtools)
|
|
41
|
+
* @returns An object containing canUndo, canRedo, history, and snapshots.
|
|
42
|
+
*/
|
|
43
|
+
export function useDevtools<S extends object>(store: Store<S>): {
|
|
44
|
+
canUndo: boolean;
|
|
45
|
+
canRedo: boolean;
|
|
46
|
+
history: readonly HistoryEntry<S>[];
|
|
47
|
+
snapshots: readonly string[];
|
|
48
|
+
} {
|
|
49
|
+
const devStore = store as StoreWithDevtools<S>;
|
|
50
|
+
|
|
51
|
+
const subscribe = useCallback(
|
|
52
|
+
(callback: () => void) => store.subscribe(callback),
|
|
53
|
+
[store]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const getSnapshot = useCallback(() => {
|
|
57
|
+
const internals = devStore.__devtools;
|
|
58
|
+
if (!internals) return '';
|
|
59
|
+
// Combine stable markers to identify changes that require re-render
|
|
60
|
+
return `${internals.buffer.cursor}-${internals.snapshots.size}`;
|
|
61
|
+
}, [devStore]);
|
|
62
|
+
|
|
63
|
+
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
64
|
+
|
|
65
|
+
// Return the actual properties from the store
|
|
66
|
+
// These are augmented by withDevtools extension
|
|
67
|
+
return {
|
|
68
|
+
canUndo: devStore.canUndo ?? false,
|
|
69
|
+
canRedo: devStore.canRedo ?? false,
|
|
70
|
+
history: devStore.history ?? [],
|
|
71
|
+
snapshots: devStore.snapshots ?? [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
package/src/useStore.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useSyncExternalStore, useRef, useCallback } from 'react'
|
|
2
|
+
import type { Store, StoreState } from '@storve/core'
|
|
3
|
+
import type { Selector, UseStoreResult } from './types'
|
|
4
|
+
|
|
5
|
+
export function shallowEqual(a: unknown, b: unknown): boolean {
|
|
6
|
+
if (Object.is(a, b)) return true
|
|
7
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false
|
|
8
|
+
const keysA = Object.keys(a)
|
|
9
|
+
const keysB = Object.keys(b)
|
|
10
|
+
if (keysA.length !== keysB.length) return false
|
|
11
|
+
for (const k of keysA) {
|
|
12
|
+
if (
|
|
13
|
+
!Object.prototype.hasOwnProperty.call(b, k) ||
|
|
14
|
+
!Object.is(
|
|
15
|
+
(a as Record<string, unknown>)[k],
|
|
16
|
+
(b as Record<string, unknown>)[k]
|
|
17
|
+
)
|
|
18
|
+
) return false
|
|
19
|
+
}
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useStore<D extends object, S = StoreState<D>>(
|
|
24
|
+
store: Store<D>,
|
|
25
|
+
selector?: Selector<D, S>
|
|
26
|
+
): UseStoreResult<D, S> {
|
|
27
|
+
const lastResult = useRef<S | undefined>(undefined)
|
|
28
|
+
const hasUpdate = useRef(false)
|
|
29
|
+
const lastStore = useRef<Store<D> | null>(null)
|
|
30
|
+
|
|
31
|
+
const subscribe = useCallback(
|
|
32
|
+
(callback: () => void) => {
|
|
33
|
+
return store.subscribe(() => {
|
|
34
|
+
hasUpdate.current = true
|
|
35
|
+
callback()
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
[store]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const getSnapshot = useCallback((): S => {
|
|
42
|
+
const state = store.getState()
|
|
43
|
+
|
|
44
|
+
// Invalidate cache when store reference changes
|
|
45
|
+
if (store !== lastStore.current) {
|
|
46
|
+
lastStore.current = store
|
|
47
|
+
lastResult.current = undefined
|
|
48
|
+
hasUpdate.current = true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (selector) {
|
|
52
|
+
const next = selector(state)
|
|
53
|
+
if (Object.is(next, lastResult.current)) return lastResult.current as S
|
|
54
|
+
if (
|
|
55
|
+
typeof next === 'object' &&
|
|
56
|
+
next !== null &&
|
|
57
|
+
lastResult.current !== undefined &&
|
|
58
|
+
shallowEqual(next, lastResult.current)
|
|
59
|
+
) {
|
|
60
|
+
return lastResult.current as S
|
|
61
|
+
}
|
|
62
|
+
lastResult.current = next
|
|
63
|
+
return next
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// No selector: return shallow copy when store updated
|
|
67
|
+
if (hasUpdate.current || lastResult.current === undefined) {
|
|
68
|
+
hasUpdate.current = false
|
|
69
|
+
lastResult.current = { ...state } as S // ✅ just cache state, don't merge actions here
|
|
70
|
+
}
|
|
71
|
+
return lastResult.current as S
|
|
72
|
+
}, [store, selector])
|
|
73
|
+
|
|
74
|
+
const result = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
75
|
+
|
|
76
|
+
// Handle selector and no-selector cases separately
|
|
77
|
+
if (selector) {
|
|
78
|
+
return result as UseStoreResult<D, S>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// No selector — merge actions at return time only, not inside getSnapshot
|
|
82
|
+
return Object.assign({}, result as object, store.actions) as UseStoreResult<D, S>
|
|
83
|
+
}
|