@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.
@@ -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,9 @@
1
+ 'use strict';
2
+
3
+ // useStore hook goes here
4
+ function useStore() {
5
+ return {};
6
+ }
7
+
8
+ exports.useStore = useStore;
9
+ //# sourceMappingURL=index.cjs.js.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":";;;;AAIM,SAAU,YAAY,CAAC,CAAU,EAAE,CAAU,EAAA;AAC/C,IAAA,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAAE,QAAA,OAAO,IAAI;AAChC,IAAA,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;AAAE,QAAA,OAAO,KAAK;IAC5F,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;AAC5B,IAAA,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;AAAE,QAAA,OAAO,KAAK;AAC/C,IAAA,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE;AACnB,QAAA,IACI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;AAC3C,YAAA,CAAC,MAAM,CAAC,EAAE,CACL,CAA6B,CAAC,CAAC,CAAC,EAChC,CAA6B,CAAC,CAAC,CAAC,CACpC;AACH,YAAA,OAAO,KAAK;IAClB;AACA,IAAA,OAAO,IAAI;AACf;AAEM,SAAU,QAAQ,CACpB,KAAe,EACf,QAAyB,EAAA;AAEzB,IAAA,MAAM,UAAU,GAAGA,YAAM,CAAgB,SAAS,CAAC;AACnD,IAAA,MAAM,SAAS,GAAGA,YAAM,CAAC,KAAK,CAAC;AAC/B,IAAA,MAAM,SAAS,GAAGA,YAAM,CAAkB,IAAI,CAAC;AAE/C,IAAA,MAAM,SAAS,GAAGC,iBAAW,CACzB,CAAC,QAAoB,KAAI;AACrB,QAAA,OAAO,KAAK,CAAC,SAAS,CAAC,MAAK;AACxB,YAAA,SAAS,CAAC,OAAO,GAAG,IAAI;AACxB,YAAA,QAAQ,EAAE;AACd,QAAA,CAAC,CAAC;AACN,IAAA,CAAC,EACD,CAAC,KAAK,CAAC,CACV;AAED,IAAA,MAAM,WAAW,GAAGA,iBAAW,CAAC,MAAQ;AACpC,QAAA,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE;;AAG9B,QAAA,IAAI,KAAK,KAAK,SAAS,CAAC,OAAO,EAAE;AAC7B,YAAA,SAAS,CAAC,OAAO,GAAG,KAAK;AACzB,YAAA,UAAU,CAAC,OAAO,GAAG,SAAS;AAC9B,YAAA,SAAS,CAAC,OAAO,GAAG,IAAI;QAC5B;QAEA,IAAI,QAAQ,EAAE;AACV,YAAA,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC;YAC5B,IAAI,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC;gBAAE,OAAO,UAAU,CAAC,OAAY;YACvE,IACI,OAAO,IAAI,KAAK,QAAQ;AACxB,gBAAA,IAAI,KAAK,IAAI;gBACb,UAAU,CAAC,OAAO,KAAK,SAAS;gBAChC,YAAY,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,EACxC;gBACE,OAAO,UAAU,CAAC,OAAY;YAClC;AACA,YAAA,UAAU,CAAC,OAAO,GAAG,IAAI;AACzB,YAAA,OAAO,IAAI;QACf;;QAGA,IAAI,SAAS,CAAC,OAAO,IAAI,UAAU,CAAC,OAAO,KAAK,SAAS,EAAE;AACvD,YAAA,SAAS,CAAC,OAAO,GAAG,KAAK;YACzB,UAAU,CAAC,OAAO,GAAG,EAAE,GAAG,KAAK,EAAO,CAAA;QAC1C;QACA,OAAO,UAAU,CAAC,OAAY;AAClC,IAAA,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAErB,MAAM,MAAM,GAAGC,0BAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,CAAC;;IAGxE,IAAI,QAAQ,EAAE;AACV,QAAA,OAAO,MAA8B;IACzC;;AAGA,IAAA,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,MAAgB,EAAE,KAAK,CAAC,OAAO,CAAyB;AACrF;;AC9CA;;;;;AAKG;AACG,SAAU,WAAW,CAAmB,KAAe,EAAA;IAMzD,MAAM,QAAQ,GAAG,KAA6B;IAE9C,MAAM,SAAS,GAAGD,iBAAW,CACzB,CAAC,QAAoB,KAAK,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,EACnD,CAAC,KAAK,CAAC,CACV;AAED,IAAA,MAAM,WAAW,GAAGA,iBAAW,CAAC,MAAK;AACjC,QAAA,MAAM,SAAS,GAAG,QAAQ,CAAC,UAAU;AACrC,QAAA,IAAI,CAAC,SAAS;AAAE,YAAA,OAAO,EAAE;;AAEzB,QAAA,OAAO,CAAA,EAAG,SAAS,CAAC,MAAM,CAAC,MAAM,CAAA,CAAA,EAAI,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE;AACnE,IAAA,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;AAEd,IAAAC,0BAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,CAAC;;;IAIzD,OAAO;AACH,QAAA,OAAO,EAAE,QAAQ,CAAC,OAAO,IAAI,KAAK;AAClC,QAAA,OAAO,EAAE,QAAQ,CAAC,OAAO,IAAI,KAAK;AAClC,QAAA,OAAO,EAAE,QAAQ,CAAC,OAAO,IAAI,EAAE;AAC/B,QAAA,SAAS,EAAE,QAAQ,CAAC,SAAS,IAAI,EAAE;KACtC;AACL;;;;;"}
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export { useStore } from './useStore';
3
+ export { useDevtools } from './useDevtools';
4
+ export type { Selector } from './types';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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,7 @@
1
+ // useStore hook goes here
2
+ function useStore() {
3
+ return {};
4
+ }
5
+
6
+ export { useStore };
7
+ //# sourceMappingURL=index.esm.js.map
@@ -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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"}
@@ -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
@@ -0,0 +1,4 @@
1
+ export * from './types'
2
+ export { useStore } from './useStore'
3
+ export { useDevtools } from './useDevtools'
4
+ export type { Selector } from './types'
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
+
@@ -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
+ }