juststore 0.0.1

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/form.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { FieldPath, FieldPathValue, FieldValues } from './path';
2
+ import type { ArrayProxy, State } from './types';
3
+ export { useForm, type CreateFormOptions, type DeepNonNullable, type FormArrayProxy, type FormDeepProxy, type FormState, type FormStore };
4
+ type FormCommon = {
5
+ /** Subscribe and read the error at path. Re-renders when the error changes. */
6
+ useError: () => string | undefined;
7
+ /** Read the error at path without subscribing. */
8
+ readonly error: string | undefined;
9
+ /** Set the error at path. */
10
+ setError: (error: string | undefined) => void;
11
+ };
12
+ type FormArrayProxy<T> = ArrayProxy<T> & FormCommon;
13
+ type FormState<T> = State<T> & FormCommon;
14
+ /** Type for nested objects with proxy methods */
15
+ type FormDeepProxy<T> = NonNullable<T> extends readonly (infer U)[] ? FormArrayProxy<U> & FormState<T> : NonNullable<T> extends FieldValues ? {
16
+ [K in keyof NonNullable<T>]-?: NonNullable<NonNullable<T>[K]> extends object ? FormDeepProxy<NonNullable<T>[K]> : FormState<NonNullable<T>[K]>;
17
+ } & FormState<T> : FormState<T>;
18
+ /** Type for nested objects with proxy methods */
19
+ type DeepNonNullable<T> = NonNullable<T> extends readonly (infer U)[] ? U[] : NonNullable<T> extends FieldValues ? {
20
+ [K in keyof NonNullable<T>]-?: DeepNonNullable<NonNullable<T>[K]>;
21
+ } : NonNullable<T>;
22
+ type FormStore<T extends FieldValues> = FormDeepProxy<T> & {
23
+ clearErrors(): void;
24
+ handleSubmit(onSubmit: (values: T) => void): (e: React.FormEvent) => void;
25
+ };
26
+ type NoEmptyValidator = 'not-empty';
27
+ type RegexValidator = RegExp;
28
+ type FunctionValidator<T extends FieldValues> = (value: FieldPathValue<T, FieldPath<T>> | undefined, state: FormStore<T>) => string | undefined;
29
+ type Validator<T extends FieldValues> = NoEmptyValidator | RegexValidator | FunctionValidator<T>;
30
+ type FieldConfig<T extends FieldValues> = {
31
+ validate?: Validator<T>;
32
+ };
33
+ type CreateFormOptions<T extends FieldValues> = Partial<Record<FieldPath<T>, FieldConfig<T>>>;
34
+ declare function useForm<T extends FieldValues>(defaultValue: T, fieldConfigs?: CreateFormOptions<T>): FormStore<T>;
package/dist/form.js ADDED
@@ -0,0 +1,111 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ 'use client';
3
+ import { pascalCase } from 'change-case';
4
+ import { useId } from 'react';
5
+ import { getSnapshot, produce } from './impl';
6
+ import { createNode } from './node';
7
+ import { createStoreRoot } from './root';
8
+ export { useForm };
9
+ function useForm(defaultValue, fieldConfigs = {}) {
10
+ const formId = useId();
11
+ const namespace = `form:${formId}`;
12
+ const errorNamespace = `errors.${namespace}`;
13
+ const storeApi = createStoreRoot(namespace, defaultValue, { memoryOnly: true });
14
+ const errorStore = createStoreRoot(errorNamespace, {}, { memoryOnly: true });
15
+ const formStore = {
16
+ clearErrors: () => produce(errorNamespace, undefined, false, true),
17
+ handleSubmit: (onSubmit) => (e) => {
18
+ e.preventDefault();
19
+ // disable submit if there are errors
20
+ if (Object.keys(getSnapshot(errorNamespace) ?? {}).length === 0) {
21
+ onSubmit(getSnapshot(namespace));
22
+ }
23
+ }
24
+ };
25
+ const store = new Proxy(storeApi, {
26
+ get(_target, prop) {
27
+ if (prop in formStore) {
28
+ return formStore[prop];
29
+ }
30
+ if (prop in storeApi) {
31
+ return storeApi[prop];
32
+ }
33
+ if (typeof prop === 'string') {
34
+ return createFormProxy(storeApi, errorStore, prop);
35
+ }
36
+ return undefined;
37
+ }
38
+ });
39
+ for (const entry of Object.entries(fieldConfigs)) {
40
+ const [path, config] = entry;
41
+ const validator = getValidator(path, config?.validate);
42
+ if (validator) {
43
+ storeApi.subscribe(path, (value) => {
44
+ const error = validator(value, store);
45
+ if (!error) {
46
+ errorStore.reset(path);
47
+ }
48
+ else {
49
+ errorStore.set(path, error);
50
+ }
51
+ });
52
+ }
53
+ }
54
+ return store;
55
+ }
56
+ const createFormProxy = (storeApi, errorStore, path) => {
57
+ const proxyCache = new Map();
58
+ const useError = () => errorStore.use(path);
59
+ const getError = () => errorStore.value(path);
60
+ const setError = (error) => {
61
+ errorStore.set(path, error);
62
+ return true;
63
+ };
64
+ return createNode(storeApi, path, proxyCache, {
65
+ useError: {
66
+ get: () => useError
67
+ },
68
+ error: {
69
+ get: getError
70
+ },
71
+ setError: {
72
+ get: () => setError
73
+ }
74
+ });
75
+ };
76
+ function getValidator(field, validator) {
77
+ if (!validator) {
78
+ return undefined;
79
+ }
80
+ if (validator === 'not-empty') {
81
+ return (value) => validateNoEmpty(field, value);
82
+ }
83
+ if (validator instanceof RegExp) {
84
+ return (value) => validateRegex(field, value, validator);
85
+ }
86
+ return validator;
87
+ }
88
+ function validateNoEmpty(field, value) {
89
+ if (!stringValue(value)) {
90
+ return `${pascalCase(field)} is required`;
91
+ }
92
+ return undefined;
93
+ }
94
+ function validateRegex(field, value, regex) {
95
+ if (!regex.test(stringValue(value))) {
96
+ return `${pascalCase(field)} is invalid`;
97
+ }
98
+ return undefined;
99
+ }
100
+ function stringValue(v) {
101
+ if (typeof v === 'string') {
102
+ return v;
103
+ }
104
+ if (typeof v === 'number') {
105
+ return String(v);
106
+ }
107
+ if (typeof v === 'boolean') {
108
+ return String(v);
109
+ }
110
+ return '';
111
+ }
package/dist/impl.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { FieldPath, FieldPathValue, FieldValues } from './path';
2
+ export { getNestedValue, getSnapshot, joinPath, notifyListeners, produce, setLeaf, useDebounce, useObject, useSubscribe };
3
+ declare function joinPath(namespace: string, path?: string): string;
4
+ /** Get a nested value from an object/array using a dot-separated path. */
5
+ declare function getNestedValue(obj: unknown, path: string): unknown;
6
+ /** Notify exact, root, and affected child listeners for a given key change. */
7
+ declare function notifyListeners(key: string, oldValue: unknown, newValue: unknown, skipRoot?: boolean, skipChildren?: boolean): void;
8
+ /** Snapshot getter used by React's useSyncExternalStore. */
9
+ declare function getSnapshot(key: string): unknown;
10
+ /** Core mutation function that updates store and notifies listeners. */
11
+ declare function produce(key: string, value: unknown, skipUpdate?: boolean, memoryOnly?: boolean): void;
12
+ /** React hook: subscribe to and read a namespaced path value. */
13
+ declare function useObject<T extends FieldValues, P extends FieldPath<T>>(key: string, path?: P): FieldPathValue<T, P> | undefined;
14
+ /** React hook: subscribe to and read a namespaced path debounced value. */
15
+ declare function useDebounce<T extends FieldValues, P extends FieldPath<T>>(key: string, path: P, delay: number): FieldPathValue<T, P> | undefined;
16
+ /** Effectful subscription helper that calls onChange with the latest value. */
17
+ declare function useSubscribe<T>(key: string, onChange: (value: T) => void): void;
18
+ /** Set a leaf value under namespace.path, optionally skipping notifications. */
19
+ declare function setLeaf<T extends FieldValues, P extends FieldPath<T>>(key: string, path: P, value: FieldPathValue<T, P> | undefined, skipUpdate?: boolean, memoryOnly?: boolean): void;
package/dist/impl.js ADDED
@@ -0,0 +1,378 @@
1
+ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
2
+ import isEqual from 'react-fast-compare';
3
+ import { localStorageDelete, localStorageGet, localStorageSet } from './local_storage';
4
+ export { getNestedValue, getSnapshot, joinPath, notifyListeners, produce, setLeaf, useDebounce, useObject, useSubscribe };
5
+ const memoryStore = new Map();
6
+ function joinPath(namespace, path) {
7
+ if (!path)
8
+ return namespace;
9
+ return namespace + '.' + path;
10
+ }
11
+ // Path traversal utilities
12
+ /** Get a nested value from an object/array using a dot-separated path. */
13
+ function getNestedValue(obj, path) {
14
+ if (!path)
15
+ return obj;
16
+ const segments = path.split('.');
17
+ let current = obj;
18
+ for (const segment of segments) {
19
+ if (current === null || current === undefined)
20
+ return undefined;
21
+ if (typeof current !== 'object')
22
+ return undefined;
23
+ if (Array.isArray(current)) {
24
+ const index = Number(segment);
25
+ if (Number.isNaN(index))
26
+ return undefined;
27
+ current = current[index];
28
+ }
29
+ else {
30
+ current = current[segment];
31
+ }
32
+ }
33
+ return current;
34
+ }
35
+ function tryStructuredClone(obj) {
36
+ if (obj === null || obj === undefined)
37
+ return null;
38
+ if (typeof obj !== 'object')
39
+ return obj;
40
+ // Fast path for array type
41
+ if (Array.isArray(obj)) {
42
+ // Check if array needs deep cloning (contains objects/arrays)
43
+ const needsDeepClone = obj.some(item => item !== null && typeof item === 'object');
44
+ if (!needsDeepClone) {
45
+ // Array of primitives - just return new array reference
46
+ return [...obj];
47
+ }
48
+ return obj.map(item => tryStructuredClone(item));
49
+ }
50
+ // Fallback to structuredClone for complex objects
51
+ try {
52
+ return structuredClone(obj);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ /**
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.
63
+ */
64
+ function setNestedValue(obj, path, value) {
65
+ if (!path)
66
+ return value;
67
+ const segments = path.split('.');
68
+ const result = tryStructuredClone(obj);
69
+ let current = result ?? {};
70
+ for (let i = 0; i < segments.length - 1; i++) {
71
+ const segment = segments[i];
72
+ const nextSegment = segments[i + 1];
73
+ const isNextIndex = !Number.isNaN(Number(nextSegment));
74
+ if (Array.isArray(current)) {
75
+ const index = Number(segment);
76
+ if (Number.isNaN(index))
77
+ break;
78
+ if (!current[index]) {
79
+ current[index] = isNextIndex ? [] : {};
80
+ }
81
+ current = current[index];
82
+ }
83
+ else if (typeof current === 'object' && current !== null) {
84
+ const currentObj = current;
85
+ if (!currentObj[segment]) {
86
+ currentObj[segment] = isNextIndex ? [] : {};
87
+ }
88
+ current = currentObj[segment];
89
+ }
90
+ }
91
+ const lastSegment = segments[segments.length - 1];
92
+ if (Array.isArray(current)) {
93
+ const index = Number(lastSegment);
94
+ if (!Number.isNaN(index)) {
95
+ if (value === undefined) {
96
+ current.splice(index, 1);
97
+ }
98
+ else {
99
+ current[index] = value;
100
+ }
101
+ }
102
+ }
103
+ else if (typeof current === 'object' && current !== null) {
104
+ const currentObj = current;
105
+ if (value === undefined) {
106
+ delete currentObj[lastSegment];
107
+ }
108
+ else {
109
+ currentObj[lastSegment] = value;
110
+ }
111
+ }
112
+ return result;
113
+ }
114
+ /** Extract the root namespace from a full key (e.g. "ns.a.b" => "ns"). */
115
+ function getRootKey(key) {
116
+ return key.split('.')[0];
117
+ }
118
+ /** Extract the nested path from a full key (e.g. "ns.a.b" => "a.b"). */
119
+ function getPath(key) {
120
+ const segments = key.split('.');
121
+ return segments.slice(1).join('.');
122
+ }
123
+ // Helper function to notify listeners hierarchically
124
+ /** Notify exact, root, and affected child listeners for a given key change. */
125
+ function notifyListeners(key, oldValue, newValue, skipRoot = false, skipChildren = false) {
126
+ const rootKey = skipRoot ? null : key.split('.').slice(0, 2).join('.');
127
+ const keyPrefix = skipChildren ? null : key + '.';
128
+ // Single pass: collect listeners to notify
129
+ const listenersToNotify = new Set();
130
+ for (const [listenerKey, listenerSet] of listeners.entries()) {
131
+ if (listenerKey === key) {
132
+ // Exact key match
133
+ listenerSet.forEach(listener => listenersToNotify.add(listener));
134
+ }
135
+ else if (rootKey && listenerKey === rootKey) {
136
+ // Root key match
137
+ listenerSet.forEach(listener => listenersToNotify.add(listener));
138
+ }
139
+ else if (keyPrefix && listenerKey.startsWith(keyPrefix)) {
140
+ // Child key match - check if value actually changed
141
+ const childPath = listenerKey.substring(key.length + 1);
142
+ const oldChildValue = getNestedValue(oldValue, childPath);
143
+ const newChildValue = getNestedValue(newValue, childPath);
144
+ if (!isEqual(oldChildValue, newChildValue)) {
145
+ listenerSet.forEach(listener => listenersToNotify.add(listener));
146
+ }
147
+ }
148
+ }
149
+ // Notify all collected listeners
150
+ listenersToNotify.forEach(listener => listener());
151
+ }
152
+ // BroadcastChannel for cross-tab synchronization
153
+ const broadcastChannel = typeof window !== 'undefined' ? new BroadcastChannel('godoxy-producer-consumer') : null;
154
+ /**
155
+ * Backing store providing in-memory data with localStorage persistence
156
+ * and cross-tab synchronization. All operations are namespaced at the root key
157
+ * (characters before the first dot).
158
+ */
159
+ const store = {
160
+ has(key) {
161
+ const rootKey = getRootKey(key);
162
+ return (memoryStore.has(rootKey) ||
163
+ (typeof window !== 'undefined' && localStorageGet(rootKey) !== undefined));
164
+ },
165
+ get(key) {
166
+ const rootKey = getRootKey(key);
167
+ const path = getPath(key);
168
+ // Get root object from memory or localStorage
169
+ let rootValue;
170
+ if (memoryStore.has(rootKey)) {
171
+ rootValue = memoryStore.get(rootKey);
172
+ }
173
+ else if (typeof window !== 'undefined') {
174
+ rootValue = localStorageGet(rootKey);
175
+ if (rootValue !== undefined) {
176
+ memoryStore.set(rootKey, rootValue);
177
+ }
178
+ }
179
+ // If no path, return root value
180
+ if (!path)
181
+ return rootValue;
182
+ // Traverse to nested value
183
+ return getNestedValue(rootValue, path);
184
+ },
185
+ set(key, value, memoryOnly = false) {
186
+ const rootKey = getRootKey(key);
187
+ const path = getPath(key);
188
+ let rootValue;
189
+ if (!path) {
190
+ // Setting root value directly
191
+ rootValue = value;
192
+ }
193
+ else {
194
+ // Setting nested value
195
+ const currentRoot = memoryStore.get(rootKey) ?? localStorageGet(rootKey) ?? {};
196
+ rootValue = setNestedValue(currentRoot, path, value);
197
+ }
198
+ // Update memory
199
+ if (rootValue === undefined) {
200
+ memoryStore.delete(rootKey);
201
+ }
202
+ else {
203
+ memoryStore.set(rootKey, rootValue);
204
+ }
205
+ // Persist to localStorage (unless memoryOnly)
206
+ if (!memoryOnly && typeof window !== 'undefined') {
207
+ localStorageSet(rootKey, rootValue);
208
+ // Broadcast change to other tabs
209
+ if (broadcastChannel) {
210
+ broadcastChannel.postMessage({ type: 'set', key: rootKey, value: rootValue });
211
+ }
212
+ }
213
+ },
214
+ delete(key, memoryOnly = false) {
215
+ const rootKey = getRootKey(key);
216
+ const path = getPath(key);
217
+ if (!path) {
218
+ // Deleting root key
219
+ memoryStore.delete(rootKey);
220
+ if (!memoryOnly && typeof window !== 'undefined') {
221
+ localStorageDelete(rootKey);
222
+ if (broadcastChannel) {
223
+ broadcastChannel.postMessage({ type: 'delete', key: rootKey });
224
+ }
225
+ }
226
+ }
227
+ else {
228
+ // Deleting nested value
229
+ const currentRoot = memoryStore.get(rootKey) ?? localStorageGet(rootKey);
230
+ if (currentRoot !== undefined) {
231
+ const updatedRoot = setNestedValue(currentRoot, path, undefined);
232
+ memoryStore.set(rootKey, updatedRoot);
233
+ if (!memoryOnly && typeof window !== 'undefined') {
234
+ localStorageSet(rootKey, updatedRoot);
235
+ if (broadcastChannel) {
236
+ broadcastChannel.postMessage({ type: 'set', key: rootKey, value: updatedRoot });
237
+ }
238
+ }
239
+ }
240
+ }
241
+ },
242
+ get size() {
243
+ return memoryStore.size;
244
+ }
245
+ };
246
+ /** Snapshot getter used by React's useSyncExternalStore. */
247
+ function getSnapshot(key) {
248
+ return store.get(key);
249
+ }
250
+ // Cross-tab synchronization: keep memoryStore in sync with BroadcastChannel events
251
+ if (broadcastChannel) {
252
+ broadcastChannel.addEventListener('message', event => {
253
+ const { type, key, value } = event.data;
254
+ if (!key)
255
+ return;
256
+ // Store old value before updating
257
+ const oldRootValue = memoryStore.get(key);
258
+ if (type === 'delete') {
259
+ memoryStore.delete(key);
260
+ }
261
+ else if (type === 'set') {
262
+ memoryStore.set(key, value);
263
+ }
264
+ // Notify all listeners that might be affected by this root key change
265
+ const newRootValue = type === 'delete' ? undefined : value;
266
+ for (const listenerKey of listeners.keys()) {
267
+ if (listenerKey === key) {
268
+ // Direct key match - notify with old and new values
269
+ notifyListeners(listenerKey, oldRootValue, newRootValue);
270
+ }
271
+ else if (listenerKey.startsWith(key + '.')) {
272
+ // Child key - check if its value actually changed
273
+ const childPath = listenerKey.substring(key.length + 1);
274
+ const oldChildValue = getNestedValue(oldRootValue, childPath);
275
+ const newChildValue = getNestedValue(newRootValue, childPath);
276
+ if (!isEqual(oldChildValue, newChildValue)) {
277
+ const childListeners = listeners.get(listenerKey);
278
+ childListeners?.forEach(listener => listener());
279
+ }
280
+ }
281
+ }
282
+ });
283
+ }
284
+ const listeners = new Map();
285
+ /** Subscribe to changes for a key; returns an unsubscribe function. */
286
+ function subscribe(key, listener) {
287
+ if (!listeners.has(key)) {
288
+ listeners.set(key, new Set());
289
+ }
290
+ listeners.get(key).add(listener);
291
+ return () => {
292
+ const keyListeners = listeners.get(key);
293
+ if (keyListeners) {
294
+ keyListeners.delete(listener);
295
+ if (keyListeners.size === 0) {
296
+ listeners.delete(key);
297
+ }
298
+ }
299
+ };
300
+ }
301
+ /** Core mutation function that updates store and notifies listeners. */
302
+ function produce(key, value, skipUpdate = false, memoryOnly = false) {
303
+ const current = store.get(key);
304
+ if (value === undefined) {
305
+ skipUpdate = current === undefined;
306
+ store.delete(key, memoryOnly);
307
+ }
308
+ else {
309
+ if (isEqual(current, value))
310
+ return;
311
+ store.set(key, value, memoryOnly);
312
+ }
313
+ if (skipUpdate)
314
+ return;
315
+ // Notify listeners hierarchically with old and new values
316
+ notifyListeners(key, current, value);
317
+ }
318
+ /** React hook: subscribe to and read a namespaced path value. */
319
+ function useObject(key, path) {
320
+ const fullKey = joinPath(key, path);
321
+ const value = useSyncExternalStore(listener => subscribe(fullKey, listener), () => getSnapshot(fullKey), () => getSnapshot(fullKey));
322
+ return value;
323
+ }
324
+ /** React hook: subscribe to and read a namespaced path debounced value. */
325
+ function useDebounce(key, path, delay) {
326
+ const fullKey = joinPath(key, path);
327
+ const currentValue = useSyncExternalStore(listener => subscribe(fullKey, listener), () => getSnapshot(fullKey), () => getSnapshot(fullKey));
328
+ const [debouncedValue, setDebouncedValue] = useState(currentValue);
329
+ const timeoutRef = useRef(undefined);
330
+ useEffect(() => {
331
+ if (timeoutRef.current) {
332
+ clearTimeout(timeoutRef.current);
333
+ }
334
+ timeoutRef.current = setTimeout(() => {
335
+ if (!isEqual(debouncedValue, currentValue)) {
336
+ setDebouncedValue(currentValue);
337
+ }
338
+ }, delay);
339
+ return () => {
340
+ if (timeoutRef.current) {
341
+ clearTimeout(timeoutRef.current);
342
+ }
343
+ };
344
+ }, [currentValue, delay, debouncedValue]);
345
+ return debouncedValue;
346
+ }
347
+ /** Effectful subscription helper that calls onChange with the latest value. */
348
+ function useSubscribe(key, onChange) {
349
+ const onChangeRef = useRef(onChange);
350
+ useEffect(() => {
351
+ onChangeRef.current = onChange;
352
+ }, [onChange]);
353
+ useEffect(() => {
354
+ const unsubscribe = subscribe(key, () => {
355
+ const value = getSnapshot(key);
356
+ onChangeRef.current(value);
357
+ });
358
+ return unsubscribe;
359
+ }, [key]);
360
+ }
361
+ /** Set a leaf value under namespace.path, optionally skipping notifications. */
362
+ function setLeaf(key, path, value, skipUpdate = false, memoryOnly = false) {
363
+ const fullKey = joinPath(key, path);
364
+ produce(fullKey, value, skipUpdate, memoryOnly);
365
+ }
366
+ // Debug helpers (dev only)
367
+ /** Development-only debug helpers exposed on window.__pc_debug in development. */
368
+ const __pc_debug = {
369
+ getStoreSize: () => store.size,
370
+ getListenerSize: () => listeners.size,
371
+ getStore: () => memoryStore,
372
+ getStoreValue: (key) => memoryStore.get(key)
373
+ };
374
+ // Expose debug in browser for quick inspection during development
375
+ if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
376
+ ;
377
+ window.__pc_debug = __pc_debug;
378
+ }
@@ -0,0 +1,5 @@
1
+ export { useForm } from './form';
2
+ export type { FormState, FormStore } from './form';
3
+ export { useMemoryStore, type MemoryStore } from './memory';
4
+ export { createStore, type Store } from './store';
5
+ export type { State, StoreSetStateAction } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { useForm } from './form';
2
+ export { useMemoryStore } from './memory';
3
+ export { createStore } from './store';
@@ -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,21 @@
1
+ import type { FieldValues } from './path';
2
+ import type { DeepProxy, State } from './types';
3
+ export { useMemoryStore, type MemoryStore };
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> = State<T> & {
14
+ [K in keyof T]: NonNullable<T[K]> extends object ? DeepProxy<T[K]> : State<T[K]>;
15
+ };
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')`.
20
+ */
21
+ declare function useMemoryStore<T extends FieldValues>(defaultValue: T): MemoryStore<T>;
package/dist/memory.js ADDED
@@ -0,0 +1,17 @@
1
+ import { useId } from 'react';
2
+ import { createRootNode } from './node';
3
+ import { createStoreRoot } from './root';
4
+ export { useMemoryStore };
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')`.
9
+ */
10
+ function useMemoryStore(defaultValue) {
11
+ const memoryStoreId = useId();
12
+ const namespace = `memory:${memoryStoreId}`;
13
+ const storeApi = createStoreRoot(namespace, defaultValue, {
14
+ memoryOnly: true
15
+ });
16
+ return createRootNode(storeApi);
17
+ }
@@ -0,0 +1,21 @@
1
+ import type { Prettify, State } from './types';
2
+ export { createMixedState, type MixedState };
3
+ type MixedState<T extends readonly unknown[]> = Prettify<Pick<State<Readonly<T>>, 'value' | 'use' | 'Render' | 'Show'>>;
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
+ * <mixedState.Render>
16
+ * {[addLoading, copyLoading, agent] => <SomeComponent />}
17
+ * </mixedState.Render>
18
+ */
19
+ declare function createMixedState<T extends readonly unknown[]>(...states: {
20
+ [K in keyof T]: State<T[K]>;
21
+ }): MixedState<T>;