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.
@@ -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;