juststore 1.1.2 → 1.2.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/LICENSE +21 -661
- package/README.md +143 -166
- package/dist/atom.d.ts +1 -1
- package/dist/form.d.ts +1 -1
- package/dist/form.js +3 -1
- package/dist/impl.d.ts +1 -1
- package/dist/impl.js +1 -1
- package/dist/kv_store.d.ts +1 -1
- package/dist/kv_store.js +10 -2
- package/dist/memory.d.ts +1 -1
- package/dist/src/atom.d.ts +45 -0
- package/dist/src/atom.js +141 -0
- package/dist/src/form.d.ts +97 -0
- package/dist/src/form.js +176 -0
- package/dist/src/impl.d.ts +128 -0
- package/dist/src/impl.js +644 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +7 -0
- package/dist/src/kv_store.d.ts +29 -0
- package/dist/src/kv_store.js +127 -0
- package/dist/src/local_storage.d.ts +7 -0
- package/dist/src/local_storage.js +43 -0
- package/dist/src/memory.d.ts +54 -0
- package/dist/src/memory.js +55 -0
- package/dist/src/mixed_state.d.ts +20 -0
- package/dist/src/mixed_state.js +45 -0
- package/dist/src/node.d.ts +41 -0
- package/dist/src/node.js +374 -0
- package/dist/src/path.d.ts +136 -0
- package/dist/src/path.js +26 -0
- package/dist/src/root.d.ts +23 -0
- package/dist/src/root.js +81 -0
- package/dist/src/stable_keys.d.ts +4 -0
- package/dist/src/stable_keys.js +31 -0
- package/dist/src/store.d.ts +42 -0
- package/dist/src/store.js +40 -0
- package/dist/src/types.d.ts +143 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +72 -0
- package/dist/src/utils.js +76 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/package.json +61 -60
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { getNestedValue, setNestedValue } from "./impl";
|
|
2
|
+
import { localStorageDelete, localStorageGet, localStorageSet, } from "./local_storage";
|
|
3
|
+
export { getNestedValue, KVStore, setNestedValue };
|
|
4
|
+
class KVStore {
|
|
5
|
+
inMemStorage;
|
|
6
|
+
broadcastChannel;
|
|
7
|
+
memoryOnly;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.inMemStorage = options.inMemStorage;
|
|
10
|
+
this.broadcastChannel = options.broadcastChannel;
|
|
11
|
+
this.memoryOnly = options.memoryOnly;
|
|
12
|
+
}
|
|
13
|
+
getBroadcastChannel() {
|
|
14
|
+
return this.broadcastChannel;
|
|
15
|
+
}
|
|
16
|
+
setBroadcastChannel(broadcastChannel) {
|
|
17
|
+
this.broadcastChannel = broadcastChannel;
|
|
18
|
+
}
|
|
19
|
+
get(key) {
|
|
20
|
+
const [rootKey, path] = splitNSPath(key);
|
|
21
|
+
// Get root object from memory or localStorage
|
|
22
|
+
let rootValue;
|
|
23
|
+
if (this.inMemStorage.has(rootKey)) {
|
|
24
|
+
rootValue = this.inMemStorage.get(rootKey);
|
|
25
|
+
}
|
|
26
|
+
else if (!this.memoryOnly && typeof window !== "undefined") {
|
|
27
|
+
rootValue = localStorageGet(rootKey);
|
|
28
|
+
if (rootValue !== undefined) {
|
|
29
|
+
this.inMemStorage.set(rootKey, rootValue);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// If no path, return root value
|
|
33
|
+
if (!path)
|
|
34
|
+
return rootValue;
|
|
35
|
+
// Traverse to nested value
|
|
36
|
+
return getNestedValue(rootValue, path);
|
|
37
|
+
}
|
|
38
|
+
set(key, value) {
|
|
39
|
+
if (value === undefined) {
|
|
40
|
+
return this.delete(key);
|
|
41
|
+
}
|
|
42
|
+
const [rootKey, path] = splitNSPath(key);
|
|
43
|
+
let rootValue;
|
|
44
|
+
if (!path) {
|
|
45
|
+
// Setting root value directly
|
|
46
|
+
rootValue = value;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Setting nested value
|
|
50
|
+
const currentRoot = this.inMemStorage.get(rootKey) ?? localStorageGet(rootKey) ?? {};
|
|
51
|
+
rootValue = setNestedValue(currentRoot, path, value);
|
|
52
|
+
}
|
|
53
|
+
// Update memory
|
|
54
|
+
this.inMemStorage.set(rootKey, rootValue);
|
|
55
|
+
// Persist to localStorage (unless memoryOnly)
|
|
56
|
+
if (!this.memoryOnly && typeof window !== "undefined") {
|
|
57
|
+
localStorageSet(rootKey, rootValue);
|
|
58
|
+
// Broadcast change to other tabs
|
|
59
|
+
if (this.broadcastChannel) {
|
|
60
|
+
this.broadcastChannel.postMessage({
|
|
61
|
+
type: "set",
|
|
62
|
+
key: rootKey,
|
|
63
|
+
value: rootValue,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
delete(key) {
|
|
69
|
+
const [rootKey, path] = splitNSPath(key);
|
|
70
|
+
if (!path) {
|
|
71
|
+
// Deleting root key
|
|
72
|
+
this.inMemStorage.delete(rootKey);
|
|
73
|
+
if (!this.memoryOnly && typeof window !== "undefined") {
|
|
74
|
+
localStorageDelete(rootKey);
|
|
75
|
+
if (this.broadcastChannel) {
|
|
76
|
+
this.broadcastChannel.postMessage({ type: "delete", key: rootKey });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Deleting nested value
|
|
82
|
+
const currentRoot = this.inMemStorage.get(rootKey) ?? localStorageGet(rootKey);
|
|
83
|
+
if (currentRoot !== undefined) {
|
|
84
|
+
const updatedRoot = setNestedValue(currentRoot, path, undefined);
|
|
85
|
+
this.inMemStorage.set(rootKey, updatedRoot);
|
|
86
|
+
if (!this.memoryOnly && typeof window !== "undefined") {
|
|
87
|
+
localStorageSet(rootKey, updatedRoot);
|
|
88
|
+
if (this.broadcastChannel) {
|
|
89
|
+
this.broadcastChannel.postMessage({
|
|
90
|
+
type: "set",
|
|
91
|
+
key: rootKey,
|
|
92
|
+
value: updatedRoot,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
reset() {
|
|
100
|
+
if (!this.memoryOnly && typeof window !== "undefined") {
|
|
101
|
+
for (const key of this.inMemStorage.keys()) {
|
|
102
|
+
localStorageDelete(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
this.inMemStorage.clear();
|
|
106
|
+
if (this.broadcastChannel) {
|
|
107
|
+
this.broadcastChannel.postMessage({ type: "reset" });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
get size() {
|
|
111
|
+
return this.inMemStorage.size;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Extracts the namespace and path from a full key.
|
|
116
|
+
*
|
|
117
|
+
* @param key - Full key string
|
|
118
|
+
* @returns [namespace, path]
|
|
119
|
+
* @example
|
|
120
|
+
* splitNSPath('app.user.name') // ['app', 'user.name']
|
|
121
|
+
*/
|
|
122
|
+
function splitNSPath(key) {
|
|
123
|
+
const index = key.indexOf(".");
|
|
124
|
+
if (index === -1)
|
|
125
|
+
return [key, ""];
|
|
126
|
+
return [key.slice(0, index), key.slice(index + 1)];
|
|
127
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { localStorageDelete, localStorageGet, localStorageSet };
|
|
2
|
+
/** Read from localStorage (JSON.parse) with prefix; undefined on SSR or error. */
|
|
3
|
+
declare function localStorageGet(key: string): unknown;
|
|
4
|
+
/** Write to localStorage (JSON.stringify); remove key when value is undefined. */
|
|
5
|
+
declare function localStorageSet(key: string, value: unknown): void;
|
|
6
|
+
/** Delete from localStorage with prefix. */
|
|
7
|
+
declare function localStorageDelete(key: string): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// localStorage operations
|
|
2
|
+
const STORAGE_PREFIX = "juststore:";
|
|
3
|
+
export { localStorageDelete, localStorageGet, localStorageSet };
|
|
4
|
+
/** Read from localStorage (JSON.parse) with prefix; undefined on SSR or error. */
|
|
5
|
+
function localStorageGet(key) {
|
|
6
|
+
try {
|
|
7
|
+
if (typeof window === "undefined")
|
|
8
|
+
return undefined;
|
|
9
|
+
const item = localStorage.getItem(`${STORAGE_PREFIX}${key}`);
|
|
10
|
+
return item ? JSON.parse(item) : undefined;
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
console.error("Failed to get key from localStorage", key, e);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Write to localStorage (JSON.stringify); remove key when value is undefined. */
|
|
18
|
+
function localStorageSet(key, value) {
|
|
19
|
+
try {
|
|
20
|
+
if (typeof window === "undefined")
|
|
21
|
+
return;
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
localStorage.removeItem(`${STORAGE_PREFIX}${key}`);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
localStorage.setItem(`${STORAGE_PREFIX}${key}`, JSON.stringify(value));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
console.error("Failed to set key in localStorage", key, value, e);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Delete from localStorage with prefix. */
|
|
34
|
+
function localStorageDelete(key) {
|
|
35
|
+
try {
|
|
36
|
+
if (typeof window === "undefined")
|
|
37
|
+
return;
|
|
38
|
+
localStorage.removeItem(`${STORAGE_PREFIX}${key}`);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.error("Failed to delete key from localStorage", key, e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FieldValues } from "./path";
|
|
2
|
+
import type { State, ValueState } from "./types";
|
|
3
|
+
export { createMemoryStore, type MemoryStore, useMemoryStore };
|
|
4
|
+
/**
|
|
5
|
+
* A component local store with React bindings.
|
|
6
|
+
*
|
|
7
|
+
* - Dot-path addressing for nested values (e.g. "state.ui.theme").
|
|
8
|
+
* - Immutable partial updates with automatic object/array creation.
|
|
9
|
+
* - Fine-grained subscriptions built on useSyncExternalStore.
|
|
10
|
+
* - Type-safe paths using FieldPath.
|
|
11
|
+
* - Dynamic deep access via Proxy for ergonomic usage like `state.a.b.c.use()` and `state.a.b.c.set(v)`.
|
|
12
|
+
*/
|
|
13
|
+
type MemoryStore<T extends FieldValues> = ValueState<T> & {
|
|
14
|
+
[K in keyof T]-?: State<T[K]>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* React hook that creates a component-scoped memory store.
|
|
18
|
+
*
|
|
19
|
+
* Unlike `createStore`, this store is not persisted to localStorage and is
|
|
20
|
+
* unique to each component instance. Useful for complex local state that
|
|
21
|
+
* benefits from the store's path-based API without persistence.
|
|
22
|
+
*
|
|
23
|
+
* @param defaultValue - Initial state shape
|
|
24
|
+
* @returns A proxy providing dynamic path access to the store
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* type SearchState = {
|
|
28
|
+
* query: string
|
|
29
|
+
* filters: { category: string }
|
|
30
|
+
* results: { id: number; name: string }[]
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* function ProductSearch() {
|
|
34
|
+
* const state = useMemoryStore<SearchState>({
|
|
35
|
+
* query: '',
|
|
36
|
+
* filters: { category: 'all' },
|
|
37
|
+
* results: []
|
|
38
|
+
* })
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <>
|
|
42
|
+
* <SearchInput state={state} />
|
|
43
|
+
* <FilterPanel state={state} />
|
|
44
|
+
* </>
|
|
45
|
+
* )
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* function SearchInput({ state }: { state: MemoryStore<SearchState> }) {
|
|
49
|
+
* const query = state.query.use()
|
|
50
|
+
* return <input value={query} onChange={e => state.query.set(e.target.value)} />
|
|
51
|
+
* }
|
|
52
|
+
*/
|
|
53
|
+
declare function useMemoryStore<T extends FieldValues>(defaultValue: T): MemoryStore<T>;
|
|
54
|
+
declare function createMemoryStore<T extends FieldValues>(namespace: string, defaultValue: T): MemoryStore<T>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
import { createRootNode } from "./node";
|
|
3
|
+
import { createStoreRoot } from "./root";
|
|
4
|
+
export { createMemoryStore, useMemoryStore };
|
|
5
|
+
/**
|
|
6
|
+
* React hook that creates a component-scoped memory store.
|
|
7
|
+
*
|
|
8
|
+
* Unlike `createStore`, this store is not persisted to localStorage and is
|
|
9
|
+
* unique to each component instance. Useful for complex local state that
|
|
10
|
+
* benefits from the store's path-based API without persistence.
|
|
11
|
+
*
|
|
12
|
+
* @param defaultValue - Initial state shape
|
|
13
|
+
* @returns A proxy providing dynamic path access to the store
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* type SearchState = {
|
|
17
|
+
* query: string
|
|
18
|
+
* filters: { category: string }
|
|
19
|
+
* results: { id: number; name: string }[]
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* function ProductSearch() {
|
|
23
|
+
* const state = useMemoryStore<SearchState>({
|
|
24
|
+
* query: '',
|
|
25
|
+
* filters: { category: 'all' },
|
|
26
|
+
* results: []
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <>
|
|
31
|
+
* <SearchInput state={state} />
|
|
32
|
+
* <FilterPanel state={state} />
|
|
33
|
+
* </>
|
|
34
|
+
* )
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* function SearchInput({ state }: { state: MemoryStore<SearchState> }) {
|
|
38
|
+
* const query = state.query.use()
|
|
39
|
+
* return <input value={query} onChange={e => state.query.set(e.target.value)} />
|
|
40
|
+
* }
|
|
41
|
+
*/
|
|
42
|
+
function useMemoryStore(defaultValue) {
|
|
43
|
+
const memoryStoreId = useId();
|
|
44
|
+
const namespace = `memory:${memoryStoreId}`;
|
|
45
|
+
const storeApi = createStoreRoot(namespace, defaultValue, {
|
|
46
|
+
memoryOnly: true,
|
|
47
|
+
});
|
|
48
|
+
return createRootNode(storeApi);
|
|
49
|
+
}
|
|
50
|
+
function createMemoryStore(namespace, defaultValue) {
|
|
51
|
+
const storeApi = createStoreRoot(namespace, defaultValue, {
|
|
52
|
+
memoryOnly: true,
|
|
53
|
+
});
|
|
54
|
+
return createRootNode(storeApi);
|
|
55
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReadOnlyState, ValueState } from "./types";
|
|
2
|
+
export { createMixedState };
|
|
3
|
+
/**
|
|
4
|
+
* Creates a mixed state that combines multiple states into a tuple.
|
|
5
|
+
*
|
|
6
|
+
* If one of the states is changed, the mixed state will be updated.
|
|
7
|
+
*
|
|
8
|
+
* @param states - Array of states to combine
|
|
9
|
+
* @returns A new state that provides all values as a tuple
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const mixedState = createMixedState(states.addLoading, states.copyLoading, state.agent)
|
|
13
|
+
*
|
|
14
|
+
* <Render state={mixedState}>
|
|
15
|
+
* {[addLoading, copyLoading, agent] => <SomeComponent />}
|
|
16
|
+
* </Render>
|
|
17
|
+
*/
|
|
18
|
+
declare function createMixedState<T extends readonly unknown[]>(...states: {
|
|
19
|
+
[K in keyof T]-?: ValueState<T[K]>;
|
|
20
|
+
}): ReadOnlyState<T>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { isEqual } from "./impl";
|
|
3
|
+
export { createMixedState };
|
|
4
|
+
/**
|
|
5
|
+
* Creates a mixed state that combines multiple states into a tuple.
|
|
6
|
+
*
|
|
7
|
+
* If one of the states is changed, the mixed state will be updated.
|
|
8
|
+
*
|
|
9
|
+
* @param states - Array of states to combine
|
|
10
|
+
* @returns A new state that provides all values as a tuple
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const mixedState = createMixedState(states.addLoading, states.copyLoading, state.agent)
|
|
14
|
+
*
|
|
15
|
+
* <Render state={mixedState}>
|
|
16
|
+
* {[addLoading, copyLoading, agent] => <SomeComponent />}
|
|
17
|
+
* </Render>
|
|
18
|
+
*/
|
|
19
|
+
function createMixedState(...states) {
|
|
20
|
+
const use = () => states.map((state) => state.use());
|
|
21
|
+
const mixedState = {
|
|
22
|
+
get value() {
|
|
23
|
+
return states.map((state) => state.value);
|
|
24
|
+
},
|
|
25
|
+
use,
|
|
26
|
+
useCompute(fn) {
|
|
27
|
+
const [value, setValue] = useState(fn(this.value));
|
|
28
|
+
const recompute = useCallback(() => {
|
|
29
|
+
const newValue = fn(this.value);
|
|
30
|
+
// skip update if the new value is the same as the previous value
|
|
31
|
+
setValue((prev) => (isEqual(prev, newValue) ? prev : newValue));
|
|
32
|
+
}, [fn]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const unsubscribeFns = states.map((state) => state.subscribe(recompute));
|
|
35
|
+
return () => {
|
|
36
|
+
unsubscribeFns.forEach((unsubscribe) => {
|
|
37
|
+
unsubscribe();
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return value;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
return mixedState;
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FieldPath, FieldPathValue, FieldValues } from "./path";
|
|
2
|
+
import type { State, StoreRoot } from "./types";
|
|
3
|
+
export { createNode, createRootNode, type Extension };
|
|
4
|
+
/**
|
|
5
|
+
* Creates the root proxy node for dynamic path access.
|
|
6
|
+
*
|
|
7
|
+
* This is an internal function that wraps a store API in a Proxy, enabling
|
|
8
|
+
* property-chain syntax like `store.user.profile.name.use()`.
|
|
9
|
+
*
|
|
10
|
+
* @param storeApi - The underlying store API with path-based methods
|
|
11
|
+
* @param initialPath - Starting path segment (default: empty string for root)
|
|
12
|
+
* @returns A proxy that intercepts property access and returns nested proxies or state methods
|
|
13
|
+
*/
|
|
14
|
+
declare function createRootNode<T extends FieldValues, P extends FieldPath<T>>(storeApi: StoreRoot<T>, initialPath?: P): State<FieldPathValue<T, P>>;
|
|
15
|
+
/**
|
|
16
|
+
* Extension interface for adding custom getters/setters to proxy nodes.
|
|
17
|
+
* Used internally by form handling to add error-related methods.
|
|
18
|
+
*/
|
|
19
|
+
type Extension<T extends FieldValues> = {
|
|
20
|
+
/** Custom getter function */
|
|
21
|
+
get?: (path: FieldPath<T>) => any;
|
|
22
|
+
/** Custom setter function; returns true if the set was handled */
|
|
23
|
+
set?: <P extends FieldPath<T>>(path: P, value: FieldPathValue<T, P>) => boolean;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Creates a proxy node for a specific path in the store.
|
|
27
|
+
*
|
|
28
|
+
* The proxy intercepts property access to provide state methods (use, set, value, etc.)
|
|
29
|
+
* and recursively creates child proxies for nested paths. Supports derived state
|
|
30
|
+
* transformations via the `from` and `to` parameters.
|
|
31
|
+
*
|
|
32
|
+
* @param storeApi - The underlying store API
|
|
33
|
+
* @param path - Dot-separated path to this node (e.g., "user.profile.name")
|
|
34
|
+
* @param cache - Shared cache to avoid recreating proxies for the same path
|
|
35
|
+
* @param extensions - Optional custom getters/setters (used by form handling)
|
|
36
|
+
* @param from - Transform function applied when reading values (for derived state)
|
|
37
|
+
* @param to - Transform function applied when writing values (for derived state)
|
|
38
|
+
* @returns A proxy implementing the State interface for the given path
|
|
39
|
+
*/
|
|
40
|
+
declare function createNode<T extends FieldValues>(storeApi: StoreRoot<any>, path: string, cache: Map<string, any>, extensions?: Record<string | symbol, Extension<T>>, from?: typeof unchanged, to?: typeof unchanged): State<T>;
|
|
41
|
+
declare function unchanged(value: any): any;
|