juststore 0.0.1 → 0.0.2

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/dist/impl.js CHANGED
@@ -3,6 +3,13 @@ import isEqual from 'react-fast-compare';
3
3
  import { localStorageDelete, localStorageGet, localStorageSet } from './local_storage';
4
4
  export { getNestedValue, getSnapshot, joinPath, notifyListeners, produce, setLeaf, useDebounce, useObject, useSubscribe };
5
5
  const memoryStore = new Map();
6
+ /**
7
+ * Joins a namespace and path into a full key string.
8
+ *
9
+ * @param namespace - The store namespace (root key)
10
+ * @param path - Optional dot-separated path within the namespace
11
+ * @returns Combined key string (e.g., "app.user.name")
12
+ */
6
13
  function joinPath(namespace, path) {
7
14
  if (!path)
8
15
  return namespace;
@@ -32,22 +39,27 @@ function getNestedValue(obj, path) {
32
39
  }
33
40
  return current;
34
41
  }
42
+ /**
43
+ * Creates a deep clone of an object, optimized for common cases.
44
+ *
45
+ * Uses fast paths for primitives and arrays of primitives, falling back
46
+ * to structuredClone for complex objects. Returns null if cloning fails.
47
+ *
48
+ * @param obj - The value to clone
49
+ * @returns A deep copy of the value, or null if cloning fails
50
+ */
35
51
  function tryStructuredClone(obj) {
36
52
  if (obj === null || obj === undefined)
37
53
  return null;
38
54
  if (typeof obj !== 'object')
39
55
  return obj;
40
- // Fast path for array type
41
56
  if (Array.isArray(obj)) {
42
- // Check if array needs deep cloning (contains objects/arrays)
43
57
  const needsDeepClone = obj.some(item => item !== null && typeof item === 'object');
44
58
  if (!needsDeepClone) {
45
- // Array of primitives - just return new array reference
46
59
  return [...obj];
47
60
  }
48
61
  return obj.map(item => tryStructuredClone(item));
49
62
  }
50
- // Fallback to structuredClone for complex objects
51
63
  try {
52
64
  return structuredClone(obj);
53
65
  }
@@ -56,10 +68,16 @@ function tryStructuredClone(obj) {
56
68
  }
57
69
  }
58
70
  /**
59
- * Immutable set/delete of a nested value using a dot-separated path.
60
- * - Creates intermediate nodes (array if next segment is a numeric index).
61
- * - If value is undefined, deletes object key or removes array index.
62
- * - Returns a new root object/array; when path is empty, returns value.
71
+ * Immutably sets or deletes a nested value using a dot-separated path.
72
+ *
73
+ * Creates intermediate objects or arrays as needed based on whether the next
74
+ * path segment is numeric. When value is undefined, the key is deleted from
75
+ * objects or the index is spliced from arrays.
76
+ *
77
+ * @param obj - The root object to update
78
+ * @param path - Dot-separated path to the target location
79
+ * @param value - The value to set, or undefined to delete
80
+ * @returns A new root object with the change applied
63
81
  */
64
82
  function setNestedValue(obj, path, value) {
65
83
  if (!path)
@@ -111,17 +129,36 @@ function setNestedValue(obj, path, value) {
111
129
  }
112
130
  return result;
113
131
  }
114
- /** Extract the root namespace from a full key (e.g. "ns.a.b" => "ns"). */
132
+ /**
133
+ * Extracts the root namespace from a full key.
134
+ *
135
+ * @param key - Full key string (e.g., "app.user.name")
136
+ * @returns The first segment (e.g., "app")
137
+ */
115
138
  function getRootKey(key) {
116
139
  return key.split('.')[0];
117
140
  }
118
- /** Extract the nested path from a full key (e.g. "ns.a.b" => "a.b"). */
141
+ /**
142
+ * Extracts the nested path from a full key, excluding the namespace.
143
+ *
144
+ * @param key - Full key string (e.g., "app.user.name")
145
+ * @returns The path after the namespace (e.g., "user.name")
146
+ */
119
147
  function getPath(key) {
120
148
  const segments = key.split('.');
121
149
  return segments.slice(1).join('.');
122
150
  }
123
- // Helper function to notify listeners hierarchically
124
- /** Notify exact, root, and affected child listeners for a given key change. */
151
+ /**
152
+ * Notifies all relevant listeners when a value changes.
153
+ *
154
+ * Handles three types of listeners:
155
+ * 1. Exact match - listeners subscribed to the exact changed path
156
+ * 2. Root listeners - listeners on the namespace root (for full-store subscriptions)
157
+ * 3. Child listeners - listeners on nested paths that may be affected by the change
158
+ *
159
+ * Child listeners are only notified if their specific value actually changed,
160
+ * determined by deep equality comparison.
161
+ */
125
162
  function notifyListeners(key, oldValue, newValue, skipRoot = false, skipChildren = false) {
126
163
  const rootKey = skipRoot ? null : key.split('.').slice(0, 2).join('.');
127
164
  const keyPrefix = skipChildren ? null : key + '.';
@@ -282,7 +319,13 @@ if (broadcastChannel) {
282
319
  });
283
320
  }
284
321
  const listeners = new Map();
285
- /** Subscribe to changes for a key; returns an unsubscribe function. */
322
+ /**
323
+ * Subscribes to changes for a specific key.
324
+ *
325
+ * @param key - The full key path to subscribe to
326
+ * @param listener - Callback invoked when the value changes
327
+ * @returns An unsubscribe function to remove the listener
328
+ */
286
329
  function subscribe(key, listener) {
287
330
  if (!listeners.has(key)) {
288
331
  listeners.set(key, new Set());
@@ -298,7 +341,17 @@ function subscribe(key, listener) {
298
341
  }
299
342
  };
300
343
  }
301
- /** Core mutation function that updates store and notifies listeners. */
344
+ /**
345
+ * Core mutation function that updates the store and notifies listeners.
346
+ *
347
+ * Handles both setting and deleting values, with optimizations to skip
348
+ * unnecessary updates when the value hasn't changed.
349
+ *
350
+ * @param key - The full key path to update
351
+ * @param value - The new value, or undefined to delete
352
+ * @param skipUpdate - When true, skips notifying listeners
353
+ * @param memoryOnly - When true, skips localStorage persistence
354
+ */
302
355
  function produce(key, value, skipUpdate = false, memoryOnly = false) {
303
356
  const current = store.get(key);
304
357
  if (value === undefined) {
@@ -315,13 +368,32 @@ function produce(key, value, skipUpdate = false, memoryOnly = false) {
315
368
  // Notify listeners hierarchically with old and new values
316
369
  notifyListeners(key, current, value);
317
370
  }
318
- /** React hook: subscribe to and read a namespaced path value. */
371
+ /**
372
+ * React hook that subscribes to and reads a value at a path.
373
+ *
374
+ * Uses useSyncExternalStore for tear-free reads and automatic re-rendering
375
+ * when the subscribed value changes.
376
+ *
377
+ * @param key - The namespace or full key
378
+ * @param path - Optional path within the namespace
379
+ * @returns The current value at the path, or undefined if not set
380
+ */
319
381
  function useObject(key, path) {
320
382
  const fullKey = joinPath(key, path);
321
383
  const value = useSyncExternalStore(listener => subscribe(fullKey, listener), () => getSnapshot(fullKey), () => getSnapshot(fullKey));
322
384
  return value;
323
385
  }
324
- /** React hook: subscribe to and read a namespaced path debounced value. */
386
+ /**
387
+ * React hook that subscribes to a value with debounced updates.
388
+ *
389
+ * The returned value only updates after the specified delay has passed
390
+ * since the last change, useful for expensive operations like search.
391
+ *
392
+ * @param key - The namespace or full key
393
+ * @param path - Path within the namespace
394
+ * @param delay - Debounce delay in milliseconds
395
+ * @returns The debounced value at the path
396
+ */
325
397
  function useDebounce(key, path, delay) {
326
398
  const fullKey = joinPath(key, path);
327
399
  const currentValue = useSyncExternalStore(listener => subscribe(fullKey, listener), () => getSnapshot(fullKey), () => getSnapshot(fullKey));
@@ -344,7 +416,16 @@ function useDebounce(key, path, delay) {
344
416
  }, [currentValue, delay, debouncedValue]);
345
417
  return debouncedValue;
346
418
  }
347
- /** Effectful subscription helper that calls onChange with the latest value. */
419
+ /**
420
+ * React hook for side effects when a value changes.
421
+ *
422
+ * Unlike `use()`, this doesn't cause re-renders. Instead, it calls the
423
+ * provided callback whenever the value changes, useful for syncing with
424
+ * external systems or triggering effects.
425
+ *
426
+ * @param key - The full key path to subscribe to
427
+ * @param onChange - Callback invoked with the new value on each change
428
+ */
348
429
  function useSubscribe(key, onChange) {
349
430
  const onChangeRef = useRef(onChange);
350
431
  useEffect(() => {
@@ -358,7 +439,15 @@ function useSubscribe(key, onChange) {
358
439
  return unsubscribe;
359
440
  }, [key]);
360
441
  }
361
- /** Set a leaf value under namespace.path, optionally skipping notifications. */
442
+ /**
443
+ * Sets a value at a specific path within a namespace.
444
+ *
445
+ * @param key - The namespace
446
+ * @param path - Path within the namespace
447
+ * @param value - The value to set, or undefined to delete
448
+ * @param skipUpdate - When true, skips notifying listeners
449
+ * @param memoryOnly - When true, skips localStorage persistence
450
+ */
362
451
  function setLeaf(key, path, value, skipUpdate = false, memoryOnly = false) {
363
452
  const fullKey = joinPath(key, path);
364
453
  produce(fullKey, value, skipUpdate, memoryOnly);
package/dist/memory.d.ts CHANGED
@@ -14,8 +14,40 @@ type MemoryStore<T extends FieldValues> = State<T> & {
14
14
  [K in keyof T]: NonNullable<T[K]> extends object ? DeepProxy<T[K]> : State<T[K]>;
15
15
  };
16
16
  /**
17
- * Create a memory store. The returned object is a Proxy that
18
- * exposes the base API (use/set/value/reset/...) and also allows deep, dynamic
19
- * access: e.g. `store.user.profile.name.use()` or `store.todos.at(0).title.set('x')`.
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
+ * }
20
52
  */
21
53
  declare function useMemoryStore<T extends FieldValues>(defaultValue: T): MemoryStore<T>;
package/dist/memory.js CHANGED
@@ -3,9 +3,41 @@ import { createRootNode } from './node';
3
3
  import { createStoreRoot } from './root';
4
4
  export { useMemoryStore };
5
5
  /**
6
- * Create a memory store. The returned object is a Proxy that
7
- * exposes the base API (use/set/value/reset/...) and also allows deep, dynamic
8
- * access: e.g. `store.user.profile.name.use()` or `store.todos.at(0).title.set('x')`.
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
+ * }
9
41
  */
10
42
  function useMemoryStore(defaultValue) {
11
43
  const memoryStoreId = useId();
@@ -1,5 +1,9 @@
1
1
  import type { Prettify, State } from './types';
2
2
  export { createMixedState, type MixedState };
3
+ /**
4
+ * A combined state that aggregates multiple independent states into a tuple.
5
+ * Provides read-only access via `value`, `use`, `Render`, and `Show`.
6
+ */
3
7
  type MixedState<T extends readonly unknown[]> = Prettify<Pick<State<Readonly<T>>, 'value' | 'use' | 'Render' | 'Show'>>;
4
8
  /**
5
9
  * Creates a mixed state that combines multiple states into a tuple.
package/dist/node.d.ts CHANGED
@@ -1,11 +1,41 @@
1
1
  import type { FieldValues } from './path';
2
2
  import type { State, StoreRoot } from './types';
3
3
  export { createNode, createRootNode, type Extension };
4
- /** Build a deep proxy for dynamic path access under a namespace. */
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
+ */
5
14
  declare function createRootNode<T extends FieldValues>(storeApi: StoreRoot<T>, initialPath?: string): State<T>;
15
+ /**
16
+ * Extension interface for adding custom getters/setters to proxy nodes.
17
+ * Used internally by form handling to add error-related methods.
18
+ */
6
19
  type Extension = {
20
+ /** Custom getter function */
7
21
  get?: () => any;
22
+ /** Custom setter function; returns true if the set was handled */
8
23
  set?: (value: any) => boolean;
9
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
+ */
10
40
  declare function createNode<T extends FieldValues>(storeApi: StoreRoot<any>, path: string, cache: Map<string, any>, extensions?: Record<string | symbol, Extension>, from?: typeof unchanged, to?: typeof unchanged): State<T>;
11
41
  declare function unchanged(value: any): any;
package/dist/node.js CHANGED
@@ -1,11 +1,35 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { useState } from 'react';
3
3
  export { createNode, createRootNode };
4
- /** Build a deep proxy for dynamic path access under a namespace. */
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
+ */
5
14
  function createRootNode(storeApi, initialPath = '') {
6
15
  const proxyCache = new Map();
7
16
  return createNode(storeApi, initialPath, proxyCache);
8
17
  }
18
+ /**
19
+ * Creates a proxy node for a specific path in the store.
20
+ *
21
+ * The proxy intercepts property access to provide state methods (use, set, value, etc.)
22
+ * and recursively creates child proxies for nested paths. Supports derived state
23
+ * transformations via the `from` and `to` parameters.
24
+ *
25
+ * @param storeApi - The underlying store API
26
+ * @param path - Dot-separated path to this node (e.g., "user.profile.name")
27
+ * @param cache - Shared cache to avoid recreating proxies for the same path
28
+ * @param extensions - Optional custom getters/setters (used by form handling)
29
+ * @param from - Transform function applied when reading values (for derived state)
30
+ * @param to - Transform function applied when writing values (for derived state)
31
+ * @returns A proxy implementing the State interface for the given path
32
+ */
9
33
  function createNode(storeApi, path, cache, extensions, from = unchanged, to = unchanged) {
10
34
  const isDerived = from !== unchanged || to !== unchanged;
11
35
  if (!isDerived && cache.has(path)) {
package/dist/root.d.ts CHANGED
@@ -1,7 +1,23 @@
1
1
  import type { FieldValues } from './path';
2
2
  import type { StoreRoot } from './types';
3
3
  export { createStoreRoot, type StoreOptions };
4
+ /**
5
+ * Configuration options for store creation.
6
+ */
4
7
  type StoreOptions = {
8
+ /** When true, the store only uses memory and does not persist to localStorage */
5
9
  memoryOnly?: boolean;
6
10
  };
11
+ /**
12
+ * Creates the core store API with path-based methods.
13
+ *
14
+ * This is an internal function that sets up the subscription system, persistence,
15
+ * and provides the base API that the proxy wraps. The returned object contains
16
+ * methods like `use`, `set`, `value`, etc. that accept path strings.
17
+ *
18
+ * @param namespace - Unique identifier for the store
19
+ * @param defaultValue - Initial state merged with any persisted data
20
+ * @param options - Configuration options
21
+ * @returns The store API object with path-based methods
22
+ */
7
23
  declare function createStoreRoot<T extends FieldValues>(namespace: string, defaultValue: T, options?: StoreOptions): StoreRoot<T>;
package/dist/root.js CHANGED
@@ -1,6 +1,18 @@
1
1
  import { useCallback, useRef } from 'react';
2
2
  import { getNestedValue, getSnapshot, joinPath, notifyListeners, produce, setLeaf, useDebounce, useObject, useSubscribe } from './impl';
3
3
  export { createStoreRoot };
4
+ /**
5
+ * Creates the core store API with path-based methods.
6
+ *
7
+ * This is an internal function that sets up the subscription system, persistence,
8
+ * and provides the base API that the proxy wraps. The returned object contains
9
+ * methods like `use`, `set`, `value`, etc. that accept path strings.
10
+ *
11
+ * @param namespace - Unique identifier for the store
12
+ * @param defaultValue - Initial state merged with any persisted data
13
+ * @param options - Configuration options
14
+ * @returns The store API object with path-based methods
15
+ */
4
16
  function createStoreRoot(namespace, defaultValue, options = {}) {
5
17
  const memoryOnly = options?.memoryOnly ?? false;
6
18
  if (memoryOnly) {
package/dist/store.d.ts CHANGED
@@ -16,4 +16,27 @@ export { createStore, type Store };
16
16
  type Store<T extends FieldValues> = StoreRoot<T> & {
17
17
  [K in keyof T]: NonNullable<T[K]> extends object ? DeepProxy<T[K]> : State<T[K]>;
18
18
  };
19
+ /**
20
+ * Creates a persistent, hierarchical store with localStorage backing and cross-tab synchronization.
21
+ *
22
+ * @param namespace - Unique identifier for the store, used as the localStorage key prefix
23
+ * @param defaultValue - Initial state shape; merged with any existing persisted data
24
+ * @param options - Configuration options
25
+ * @param options.memoryOnly - When true, disables localStorage persistence (default: false)
26
+ * @returns A proxy object providing both path-based and dynamic property access to the store
27
+ *
28
+ * @example
29
+ * const store = createStore('app', {
30
+ * user: { name: 'Guest' },
31
+ * todos: []
32
+ * })
33
+ *
34
+ * // Dynamic access
35
+ * store.user.name.use()
36
+ * store.todos.push({ text: 'New todo' })
37
+ *
38
+ * // Path-based access
39
+ * store.use('user.name')
40
+ * store.set('user.name', 'Alice')
41
+ */
19
42
  declare function createStore<T extends FieldValues>(namespace: string, defaultValue: T, options?: StoreOptions): Store<T>;
package/dist/store.js CHANGED
@@ -1,6 +1,29 @@
1
1
  import { createRootNode } from './node';
2
2
  import { createStoreRoot } from './root';
3
3
  export { createStore };
4
+ /**
5
+ * Creates a persistent, hierarchical store with localStorage backing and cross-tab synchronization.
6
+ *
7
+ * @param namespace - Unique identifier for the store, used as the localStorage key prefix
8
+ * @param defaultValue - Initial state shape; merged with any existing persisted data
9
+ * @param options - Configuration options
10
+ * @param options.memoryOnly - When true, disables localStorage persistence (default: false)
11
+ * @returns A proxy object providing both path-based and dynamic property access to the store
12
+ *
13
+ * @example
14
+ * const store = createStore('app', {
15
+ * user: { name: 'Guest' },
16
+ * todos: []
17
+ * })
18
+ *
19
+ * // Dynamic access
20
+ * store.user.name.use()
21
+ * store.todos.push({ text: 'New todo' })
22
+ *
23
+ * // Path-based access
24
+ * store.use('user.name')
25
+ * store.set('user.name', 'Alice')
26
+ */
4
27
  function createStore(namespace, defaultValue, options = {}) {
5
28
  const storeApi = createStoreRoot(namespace, defaultValue, options);
6
29
  return new Proxy(storeApi, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juststore",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A small, expressive, and type-safe state management library for React.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",