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 +34 -0
- package/dist/form.js +111 -0
- package/dist/impl.d.ts +19 -0
- package/dist/impl.js +378 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/local_storage.d.ts +7 -0
- package/dist/local_storage.js +43 -0
- package/dist/memory.d.ts +21 -0
- package/dist/memory.js +17 -0
- package/dist/mixed_state.d.ts +21 -0
- package/dist/mixed_state.js +34 -0
- package/dist/node.d.ts +11 -0
- package/dist/node.js +218 -0
- package/dist/path.d.ts +135 -0
- package/dist/path.js +2 -0
- package/dist/root.d.ts +7 -0
- package/dist/root.js +63 -0
- package/dist/store.d.ts +19 -0
- package/dist/store.js +17 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.js +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { createMixedState };
|
|
2
|
+
/**
|
|
3
|
+
* Creates a mixed state that combines multiple states into a tuple.
|
|
4
|
+
*
|
|
5
|
+
* If one of the states is changed, the mixed state will be updated.
|
|
6
|
+
*
|
|
7
|
+
* @param states - Array of states to combine
|
|
8
|
+
* @returns A new state that provides all values as a tuple
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const mixedState = createMixedState(states.addLoading, states.copyLoading, state.agent)
|
|
12
|
+
*
|
|
13
|
+
* <mixedState.Render>
|
|
14
|
+
* {[addLoading, copyLoading, agent] => <SomeComponent />}
|
|
15
|
+
* </mixedState.Render>
|
|
16
|
+
*/
|
|
17
|
+
function createMixedState(...states) {
|
|
18
|
+
const use = () => states.map(state => state.use());
|
|
19
|
+
const mixedState = {
|
|
20
|
+
get value() {
|
|
21
|
+
return states.map(state => state.value);
|
|
22
|
+
},
|
|
23
|
+
use,
|
|
24
|
+
Render({ children }) {
|
|
25
|
+
const value = use();
|
|
26
|
+
return children(value);
|
|
27
|
+
},
|
|
28
|
+
Show({ children, on }) {
|
|
29
|
+
const value = use();
|
|
30
|
+
return on(value) ? children : null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return mixedState;
|
|
34
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FieldValues } from './path';
|
|
2
|
+
import type { State, StoreRoot } from './types';
|
|
3
|
+
export { createNode, createRootNode, type Extension };
|
|
4
|
+
/** Build a deep proxy for dynamic path access under a namespace. */
|
|
5
|
+
declare function createRootNode<T extends FieldValues>(storeApi: StoreRoot<T>, initialPath?: string): State<T>;
|
|
6
|
+
type Extension = {
|
|
7
|
+
get?: () => any;
|
|
8
|
+
set?: (value: any) => boolean;
|
|
9
|
+
};
|
|
10
|
+
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
|
+
declare function unchanged(value: any): any;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
export { createNode, createRootNode };
|
|
4
|
+
/** Build a deep proxy for dynamic path access under a namespace. */
|
|
5
|
+
function createRootNode(storeApi, initialPath = '') {
|
|
6
|
+
const proxyCache = new Map();
|
|
7
|
+
return createNode(storeApi, initialPath, proxyCache);
|
|
8
|
+
}
|
|
9
|
+
function createNode(storeApi, path, cache, extensions, from = unchanged, to = unchanged) {
|
|
10
|
+
const isDerived = from !== unchanged || to !== unchanged;
|
|
11
|
+
if (!isDerived && cache.has(path)) {
|
|
12
|
+
return cache.get(path);
|
|
13
|
+
}
|
|
14
|
+
const proxy = new Proxy({}, {
|
|
15
|
+
get(_target, prop) {
|
|
16
|
+
if (prop === 'field') {
|
|
17
|
+
return path.split('.').pop();
|
|
18
|
+
}
|
|
19
|
+
if (prop === 'use') {
|
|
20
|
+
return () => from(storeApi.use(path));
|
|
21
|
+
}
|
|
22
|
+
if (prop === 'useDebounce') {
|
|
23
|
+
return (delay) => from(storeApi.useDebounce(path, delay));
|
|
24
|
+
}
|
|
25
|
+
if (prop === 'useState') {
|
|
26
|
+
return () => [from(storeApi.use(path)), (value) => storeApi.set(path, to(value))];
|
|
27
|
+
}
|
|
28
|
+
if (prop === 'value') {
|
|
29
|
+
return from(storeApi.value(path));
|
|
30
|
+
}
|
|
31
|
+
if (prop === 'set') {
|
|
32
|
+
return (value, skipUpdate) => storeApi.set(path, to(value), skipUpdate);
|
|
33
|
+
}
|
|
34
|
+
if (prop === 'reset') {
|
|
35
|
+
return () => storeApi.reset(path);
|
|
36
|
+
}
|
|
37
|
+
if (prop === 'subscribe') {
|
|
38
|
+
return (listener) => storeApi.subscribe(path, value => listener(to(value)));
|
|
39
|
+
}
|
|
40
|
+
if (prop === 'Render') {
|
|
41
|
+
return ({ children }) => storeApi.Render({
|
|
42
|
+
path,
|
|
43
|
+
children: (value, update) => children(from(value), value => update(to(value)))
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (prop === 'Show') {
|
|
47
|
+
return ({ children, on }) => storeApi.Show({ path, children, on: value => on(from(value)) });
|
|
48
|
+
}
|
|
49
|
+
if (prop === 'compute') {
|
|
50
|
+
return (fn) => {
|
|
51
|
+
const initialValue = from(storeApi.value(path));
|
|
52
|
+
const [computedValue, setComputedValue] = useState(() => fn(initialValue));
|
|
53
|
+
storeApi.subscribe(path, value => setComputedValue(fn(from(value))));
|
|
54
|
+
return computedValue;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (prop === 'derived') {
|
|
58
|
+
if (isDerived) {
|
|
59
|
+
throw new Error('Derived method cannot be called on a derived node');
|
|
60
|
+
}
|
|
61
|
+
return ({ from, to }) => createNode(storeApi, path, cache, extensions, from, to);
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(storeApi.value(path))) {
|
|
64
|
+
if (prop === 'at') {
|
|
65
|
+
return (index) => {
|
|
66
|
+
const nextPath = path ? `${path}.${index}` : String(index);
|
|
67
|
+
return createNode(storeApi, nextPath, cache, extensions, from, to);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (prop === 'length') {
|
|
71
|
+
const value = from(storeApi.value(path));
|
|
72
|
+
return Array.isArray(value) ? value.length : undefined;
|
|
73
|
+
}
|
|
74
|
+
// Array mutation methods
|
|
75
|
+
if (prop === 'push') {
|
|
76
|
+
return (...items) => {
|
|
77
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
78
|
+
const transformedItems = isDerived ? items.map(to) : items;
|
|
79
|
+
const newArray = [...currentArray, ...transformedItems];
|
|
80
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
81
|
+
return newArray.length;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (prop === 'pop') {
|
|
85
|
+
return () => {
|
|
86
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
87
|
+
if (currentArray.length === 0)
|
|
88
|
+
return undefined;
|
|
89
|
+
const newArray = currentArray.slice(0, -1);
|
|
90
|
+
const poppedItem = currentArray[currentArray.length - 1];
|
|
91
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
92
|
+
return poppedItem;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (prop === 'shift') {
|
|
96
|
+
return () => {
|
|
97
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
98
|
+
if (currentArray.length === 0)
|
|
99
|
+
return undefined;
|
|
100
|
+
const newArray = currentArray.slice(1);
|
|
101
|
+
const shiftedItem = currentArray[0];
|
|
102
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
103
|
+
return shiftedItem;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (prop === 'unshift') {
|
|
107
|
+
return (...items) => {
|
|
108
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
109
|
+
const transformedItems = isDerived ? items.map(to) : items;
|
|
110
|
+
const newArray = [...transformedItems, ...currentArray];
|
|
111
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
112
|
+
return newArray.length;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (prop === 'splice') {
|
|
116
|
+
return (start, deleteCount, ...items) => {
|
|
117
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
118
|
+
const newArray = [...currentArray];
|
|
119
|
+
const transformedItems = isDerived ? items.map(to) : items;
|
|
120
|
+
const deletedItems = newArray.splice(start, deleteCount ?? 0, ...transformedItems);
|
|
121
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
122
|
+
return deletedItems;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (prop === 'reverse') {
|
|
126
|
+
return () => {
|
|
127
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
128
|
+
if (!Array.isArray(currentArray))
|
|
129
|
+
return [];
|
|
130
|
+
const newArray = [...currentArray].reverse();
|
|
131
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
132
|
+
return newArray;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (prop === 'sort') {
|
|
136
|
+
return (compareFn) => {
|
|
137
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
138
|
+
const newArray = [...currentArray].sort(compareFn);
|
|
139
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
140
|
+
return newArray;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (prop === 'fill') {
|
|
144
|
+
return (value, start, end) => {
|
|
145
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
146
|
+
const newArray = [...currentArray].fill(value, start, end);
|
|
147
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
148
|
+
return newArray;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (prop === 'copyWithin') {
|
|
152
|
+
return (target, start, end) => {
|
|
153
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
154
|
+
const newArray = [...currentArray].copyWithin(target, start, end);
|
|
155
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
156
|
+
return newArray;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (prop === 'sortedInsert') {
|
|
160
|
+
return (cmp, ...items) => {
|
|
161
|
+
const currentArray = from(storeApi.value(path)) ?? [];
|
|
162
|
+
if (typeof cmp !== 'function')
|
|
163
|
+
return isDerived ? currentArray.map(from).length : currentArray.length;
|
|
164
|
+
// Create a copy of the current array
|
|
165
|
+
const newArray = isDerived ? currentArray.map(from) : [...currentArray];
|
|
166
|
+
const transformedItems = isDerived ? items.map(to) : items;
|
|
167
|
+
// Insert each item in sorted order using binary search
|
|
168
|
+
for (const item of transformedItems) {
|
|
169
|
+
let left = 0;
|
|
170
|
+
let right = newArray.length;
|
|
171
|
+
// Binary search to find insertion point
|
|
172
|
+
while (left < right) {
|
|
173
|
+
const mid = (left + right) >>> 1;
|
|
174
|
+
if (cmp(newArray[mid], item) <= 0) {
|
|
175
|
+
left = mid + 1;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
right = mid;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Insert at the found position
|
|
182
|
+
newArray.splice(left, 0, item);
|
|
183
|
+
}
|
|
184
|
+
storeApi.set(path, isDerived ? newArray.map(from) : newArray);
|
|
185
|
+
return newArray.length;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (extensions?.[prop]?.get) {
|
|
190
|
+
return extensions[prop]?.get();
|
|
191
|
+
}
|
|
192
|
+
if (typeof prop === 'string' || typeof prop === 'number') {
|
|
193
|
+
const nextPath = path ? `${path}.${prop}` : String(prop);
|
|
194
|
+
// Always return a proxy
|
|
195
|
+
return createNode(storeApi, nextPath, cache, extensions, from, to);
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
},
|
|
199
|
+
set(_target, prop, value) {
|
|
200
|
+
if (extensions?.[prop]?.set) {
|
|
201
|
+
return extensions[prop]?.set(value);
|
|
202
|
+
}
|
|
203
|
+
if (typeof prop === 'string' || typeof prop === 'number') {
|
|
204
|
+
const nextPath = path ? `${path}.${prop}` : prop;
|
|
205
|
+
storeApi.set(nextPath, to(value));
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
if (!isDerived) {
|
|
212
|
+
cache.set(path, proxy);
|
|
213
|
+
}
|
|
214
|
+
return proxy;
|
|
215
|
+
}
|
|
216
|
+
function unchanged(value) {
|
|
217
|
+
return value;
|
|
218
|
+
}
|
package/dist/path.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export type Primitive = null | undefined | string | number | boolean | symbol | bigint;
|
|
2
|
+
export type BrowserNativeObject = Date | FileList | File;
|
|
3
|
+
/**
|
|
4
|
+
* Type which given a tuple type returns its own keys, i.e. only its indices.
|
|
5
|
+
* @typeParam T - tuple type
|
|
6
|
+
* @example
|
|
7
|
+
* ```
|
|
8
|
+
* TupleKeys<[number, string]> = '0' | '1'
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export type TupleKeys<T extends ReadonlyArray<any>> = Exclude<keyof T, keyof any[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Helper type for recursively constructing paths through a type.
|
|
14
|
+
* This actually constructs the strings and recurses into nested
|
|
15
|
+
* object types.
|
|
16
|
+
*
|
|
17
|
+
* See {@link ArrayPath}
|
|
18
|
+
*/
|
|
19
|
+
type ArrayPathImpl<K extends string | number, V, TraversedTypes> = V extends Primitive | BrowserNativeObject ? IsAny<V> extends true ? string : never : V extends ReadonlyArray<infer U> ? U extends Primitive | BrowserNativeObject ? IsAny<V> extends true ? string : never : true extends AnyIsEqual<TraversedTypes, V> ? never : `${K}` | `${K}.${ArrayPathInternal<V, TraversedTypes | V>}` : true extends AnyIsEqual<TraversedTypes, V> ? never : `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`;
|
|
20
|
+
/**
|
|
21
|
+
* Checks whether the type is any
|
|
22
|
+
* See {@link https://stackoverflow.com/a/49928360/3406963}
|
|
23
|
+
* @typeParam T - type which may be any
|
|
24
|
+
* ```
|
|
25
|
+
* IsAny<any> = true
|
|
26
|
+
* IsAny<string> = false
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
30
|
+
/**
|
|
31
|
+
* Checks whether T1 can be exactly (mutually) assigned to T2
|
|
32
|
+
* @typeParam T1 - type to check
|
|
33
|
+
* @typeParam T2 - type to check against
|
|
34
|
+
* ```
|
|
35
|
+
* IsEqual<string, string> = true
|
|
36
|
+
* IsEqual<'foo', 'foo'> = true
|
|
37
|
+
* IsEqual<string, number> = false
|
|
38
|
+
* IsEqual<string, number> = false
|
|
39
|
+
* IsEqual<string, 'foo'> = false
|
|
40
|
+
* IsEqual<'foo', string> = false
|
|
41
|
+
* IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export type IsEqual<T1, T2> = T1 extends T2 ? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2 ? true : false : false;
|
|
45
|
+
/**
|
|
46
|
+
* Helper function to break apart T1 and check if any are equal to T2
|
|
47
|
+
*
|
|
48
|
+
* See {@link IsEqual}
|
|
49
|
+
*/
|
|
50
|
+
type AnyIsEqual<T1, T2> = T1 extends T2 ? (IsEqual<T1, T2> extends true ? true : never) : never;
|
|
51
|
+
/**
|
|
52
|
+
* Type to query whether an array type T is a tuple type.
|
|
53
|
+
* @typeParam T - type which may be an array or tuple
|
|
54
|
+
* @example
|
|
55
|
+
* ```
|
|
56
|
+
* IsTuple<[number]> = true
|
|
57
|
+
* IsTuple<number[]> = false
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export type IsTuple<T extends ReadonlyArray<any>> = number extends T['length'] ? false : true;
|
|
61
|
+
/**
|
|
62
|
+
* Type which can be used to index an array or tuple type.
|
|
63
|
+
*/
|
|
64
|
+
export type ArrayKey = number;
|
|
65
|
+
/**
|
|
66
|
+
* Helper type for recursively constructing paths through a type.
|
|
67
|
+
* This obscures the internal type param TraversedTypes from exported contract.
|
|
68
|
+
*
|
|
69
|
+
* See {@link ArrayPath}
|
|
70
|
+
*/
|
|
71
|
+
type ArrayPathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V> ? IsTuple<T> extends true ? {
|
|
72
|
+
[K in TupleKeys<T>]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>;
|
|
73
|
+
}[TupleKeys<T>] : ArrayPathImpl<ArrayKey, V, TraversedTypes> : {
|
|
74
|
+
[K in keyof T]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>;
|
|
75
|
+
}[keyof T];
|
|
76
|
+
/**
|
|
77
|
+
* Type which eagerly collects all paths through a type which point to an array
|
|
78
|
+
* type.
|
|
79
|
+
* @typeParam T - type which should be introspected.
|
|
80
|
+
* @example
|
|
81
|
+
* ```
|
|
82
|
+
* Path<{foo: {bar: string[], baz: number[]}}> = 'foo.bar' | 'foo.baz'
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export type ArrayPath<T> = T extends any ? ArrayPathInternal<T> : never;
|
|
86
|
+
/**
|
|
87
|
+
* Type which eagerly collects all paths through a type
|
|
88
|
+
* @typeParam T - type which should be introspected
|
|
89
|
+
* @example
|
|
90
|
+
* ```
|
|
91
|
+
* Path<{foo: {bar: string}}> = 'foo' | 'foo.bar'
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export type Path<T> = T extends any ? PathInternal<T> : never;
|
|
95
|
+
/**
|
|
96
|
+
* See {@link Path}
|
|
97
|
+
*/
|
|
98
|
+
export type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>;
|
|
99
|
+
/**
|
|
100
|
+
* Helper type for recursively constructing paths through a type.
|
|
101
|
+
* This actually constructs the strings and recurses into nested
|
|
102
|
+
* object types.
|
|
103
|
+
*
|
|
104
|
+
* See {@link Path}
|
|
105
|
+
*/
|
|
106
|
+
type PathImpl<K extends string | number, V, TraversedTypes> = V extends Primitive | BrowserNativeObject ? `${K}` : true extends AnyIsEqual<TraversedTypes, V> ? `${K}` : `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`;
|
|
107
|
+
/**
|
|
108
|
+
* Helper type for recursively constructing paths through a type.
|
|
109
|
+
* This obscures the internal type param TraversedTypes from exported contract.
|
|
110
|
+
*
|
|
111
|
+
* See {@link Path}
|
|
112
|
+
*/
|
|
113
|
+
type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V> ? IsTuple<T> extends true ? {
|
|
114
|
+
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>;
|
|
115
|
+
}[TupleKeys<T>] : PathImpl<ArrayKey, V, TraversedTypes> : {
|
|
116
|
+
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>;
|
|
117
|
+
}[keyof T];
|
|
118
|
+
/**
|
|
119
|
+
* Type to evaluate the type which the given path points to.
|
|
120
|
+
* @typeParam T - deeply nested type which is indexed by the path
|
|
121
|
+
* @typeParam P - path into the deeply nested type
|
|
122
|
+
* @example
|
|
123
|
+
* ```
|
|
124
|
+
* PathValue<{foo: {bar: string}}, 'foo.bar'> = string
|
|
125
|
+
* PathValue<[number, string], '1'> = string
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export type PathValue<T, P extends Path<T> | ArrayPath<T>> = PathValueImpl<T, P>;
|
|
129
|
+
type PathValueImpl<T, P extends string> = T extends any ? P extends `${infer K}.${infer R}` ? K extends keyof T ? undefined extends T[K] ? PathValueImpl<T[K], R> | undefined : PathValueImpl<T[K], R> : K extends `${ArrayKey}` ? T extends ReadonlyArray<infer V> ? PathValueImpl<V, R> : never : never : P extends keyof T ? T[P] : P extends `${ArrayKey}` ? T extends ReadonlyArray<infer V> ? V : undefined extends T ? undefined : never : never : never;
|
|
130
|
+
/**
|
|
131
|
+
* See {@link PathValue}
|
|
132
|
+
*/
|
|
133
|
+
export type FieldPathValue<TFieldValues extends FieldValues, TFieldPath extends FieldPath<TFieldValues>> = Exclude<PathValue<TFieldValues, TFieldPath>, Function>;
|
|
134
|
+
export type FieldValues = Record<string, any>;
|
|
135
|
+
export {};
|
package/dist/path.js
ADDED
package/dist/root.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FieldValues } from './path';
|
|
2
|
+
import type { StoreRoot } from './types';
|
|
3
|
+
export { createStoreRoot, type StoreOptions };
|
|
4
|
+
type StoreOptions = {
|
|
5
|
+
memoryOnly?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare function createStoreRoot<T extends FieldValues>(namespace: string, defaultValue: T, options?: StoreOptions): StoreRoot<T>;
|
package/dist/root.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
import { getNestedValue, getSnapshot, joinPath, notifyListeners, produce, setLeaf, useDebounce, useObject, useSubscribe } from './impl';
|
|
3
|
+
export { createStoreRoot };
|
|
4
|
+
function createStoreRoot(namespace, defaultValue, options = {}) {
|
|
5
|
+
const memoryOnly = options?.memoryOnly ?? false;
|
|
6
|
+
if (memoryOnly) {
|
|
7
|
+
produce(namespace, undefined, true, false); // clear localStorage value
|
|
8
|
+
}
|
|
9
|
+
produce(namespace, { ...defaultValue, ...(getSnapshot(namespace) ?? {}) }, true, true);
|
|
10
|
+
const storeApi = {
|
|
11
|
+
use: (path) => useObject(namespace, path),
|
|
12
|
+
useDebounce: (path, delay) => useDebounce(namespace, path, delay),
|
|
13
|
+
set: (path, value, skipUpdate = false) => {
|
|
14
|
+
const currentValue = storeApi.value(path);
|
|
15
|
+
const newValue = typeof value === 'function'
|
|
16
|
+
? value(currentValue)
|
|
17
|
+
: value;
|
|
18
|
+
return setLeaf(namespace, path, newValue, skipUpdate, memoryOnly);
|
|
19
|
+
},
|
|
20
|
+
value: (path) => getSnapshot(joinPath(namespace, path)),
|
|
21
|
+
reset: (path) => produce(joinPath(namespace, path), undefined, false, memoryOnly),
|
|
22
|
+
subscribe: (path, listener) =>
|
|
23
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
24
|
+
useSubscribe(joinPath(namespace, path), listener),
|
|
25
|
+
notify: (path) => {
|
|
26
|
+
const value = getNestedValue(getSnapshot(namespace), path);
|
|
27
|
+
return notifyListeners(joinPath(namespace, path), value, value, true, true);
|
|
28
|
+
},
|
|
29
|
+
useState: (path) => {
|
|
30
|
+
const fullPathRef = useRef(joinPath(namespace, path));
|
|
31
|
+
const setValue = useCallback((value) => {
|
|
32
|
+
if (typeof value === 'function') {
|
|
33
|
+
const currentValue = getSnapshot(fullPathRef.current);
|
|
34
|
+
const newValue = value(currentValue);
|
|
35
|
+
return setLeaf(namespace, path, newValue, false, memoryOnly);
|
|
36
|
+
}
|
|
37
|
+
return setLeaf(namespace, path, value, false, memoryOnly);
|
|
38
|
+
}, [path]);
|
|
39
|
+
return [useObject(fullPathRef.current), setValue];
|
|
40
|
+
},
|
|
41
|
+
Render: ({ path, children }) => {
|
|
42
|
+
const fullPathRef = useRef(joinPath(namespace, path));
|
|
43
|
+
const value = useObject(fullPathRef.current);
|
|
44
|
+
const update = useCallback((value) => {
|
|
45
|
+
if (typeof value === 'function') {
|
|
46
|
+
const currentValue = getSnapshot(fullPathRef.current);
|
|
47
|
+
const newValue = value(currentValue);
|
|
48
|
+
return setLeaf(namespace, path, newValue, false, memoryOnly);
|
|
49
|
+
}
|
|
50
|
+
return setLeaf(namespace, path, value, false, memoryOnly);
|
|
51
|
+
}, [path]);
|
|
52
|
+
return children(value, update);
|
|
53
|
+
},
|
|
54
|
+
Show: ({ path, children, on }) => {
|
|
55
|
+
const value = useObject(namespace, path);
|
|
56
|
+
if (!on(value)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return children;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return storeApi;
|
|
63
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FieldValues } from './path';
|
|
2
|
+
import { type StoreOptions } from './root';
|
|
3
|
+
import type { DeepProxy, State, StoreRoot } from './types';
|
|
4
|
+
export { createStore, type Store };
|
|
5
|
+
/**
|
|
6
|
+
* A persistent, hierarchical, cross-tab synchronized key-value store with React bindings.
|
|
7
|
+
*
|
|
8
|
+
* - Dot-path addressing for nested values (e.g. "config.ui.theme").
|
|
9
|
+
* - Immutable partial updates with automatic object/array creation.
|
|
10
|
+
* - Persists root namespaces to localStorage with an in-memory mirror (no localStorage on SSR).
|
|
11
|
+
* - Cross-tab synchronization via BroadcastChannel (no-ops on SSR).
|
|
12
|
+
* - Fine-grained subscriptions built on useSyncExternalStore.
|
|
13
|
+
* - Type-safe paths using FieldPath.
|
|
14
|
+
* - Dynamic deep access via Proxy for ergonomic usage like `store.a.b.c.use()` and `store.a.b.c.set(v)`.
|
|
15
|
+
*/
|
|
16
|
+
type Store<T extends FieldValues> = StoreRoot<T> & {
|
|
17
|
+
[K in keyof T]: NonNullable<T[K]> extends object ? DeepProxy<T[K]> : State<T[K]>;
|
|
18
|
+
};
|
|
19
|
+
declare function createStore<T extends FieldValues>(namespace: string, defaultValue: T, options?: StoreOptions): Store<T>;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createRootNode } from './node';
|
|
2
|
+
import { createStoreRoot } from './root';
|
|
3
|
+
export { createStore };
|
|
4
|
+
function createStore(namespace, defaultValue, options = {}) {
|
|
5
|
+
const storeApi = createStoreRoot(namespace, defaultValue, options);
|
|
6
|
+
return new Proxy(storeApi, {
|
|
7
|
+
get(target, prop) {
|
|
8
|
+
if (prop in target) {
|
|
9
|
+
return target[prop];
|
|
10
|
+
}
|
|
11
|
+
if (typeof prop === 'string' || typeof prop === 'number') {
|
|
12
|
+
return createRootNode(target, prop);
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { FieldPath, FieldPathValue, FieldValues } from './path';
|
|
2
|
+
export type { ArrayProxy, DeepProxy, DerivedStateProps, Prettify, State, StoreRenderProps, StoreRoot, StoreSetStateAction, StoreShowProps, StoreUse };
|
|
3
|
+
type Prettify<T> = {
|
|
4
|
+
[K in keyof T]: T[K];
|
|
5
|
+
} & {};
|
|
6
|
+
/** Type for nested objects with proxy methods */
|
|
7
|
+
type DeepProxy<T> = NonNullable<T> extends readonly (infer U)[] ? ArrayProxy<U> & State<T> : NonNullable<T> extends FieldValues ? {
|
|
8
|
+
[K in keyof NonNullable<T>]-?: NonNullable<NonNullable<T>[K]> extends object ? DeepProxy<NonNullable<T>[K]> : State<NonNullable<T>[K]>;
|
|
9
|
+
} & State<T> : State<T>;
|
|
10
|
+
type ArrayMutationMethods<T> = Pick<Array<T>, 'push' | 'pop' | 'shift' | 'unshift' | 'splice' | 'reverse' | 'sort' | 'fill' | 'copyWithin'>;
|
|
11
|
+
/** Type for array proxy with index access */
|
|
12
|
+
type ArrayProxy<T> = Prettify<ArrayMutationMethods<T>> & {
|
|
13
|
+
/** Read without subscribing. Returns array or undefined for missing paths. */
|
|
14
|
+
readonly value: T[] | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Length of the underlying array. Runtime may return undefined when the
|
|
17
|
+
* current value is not an array at the path. Prefer `Array.isArray(x) && x.length` when unsure.
|
|
18
|
+
*/
|
|
19
|
+
readonly length: number;
|
|
20
|
+
/** Numeric index access never returns undefined at the type level because
|
|
21
|
+
* the proxy always returns another proxy object, even if the underlying value doesn't exist.
|
|
22
|
+
*/
|
|
23
|
+
[K: number]: T extends object ? DeepProxy<T> : State<T>;
|
|
24
|
+
/** Safe accessor that never returns undefined at the type level */
|
|
25
|
+
at(index: number): T extends object ? DeepProxy<T> : State<T>;
|
|
26
|
+
/** Insert items into the array in sorted order using the provided comparison function. */
|
|
27
|
+
sortedInsert(cmp: (a: T, b: T) => number, ...items: T[]): number;
|
|
28
|
+
};
|
|
29
|
+
/** Tuple returned by Store.use(path). */
|
|
30
|
+
type StoreUse<T> = Readonly<[T | undefined, (value: T | undefined) => void]>;
|
|
31
|
+
type StoreSetStateAction<T> = (value: T | undefined | ((prev: T) => T), skipUpdate?: boolean) => void;
|
|
32
|
+
/** Public API returned by createStore(namespace, defaultValue). */
|
|
33
|
+
type StoreRoot<T extends FieldValues> = {
|
|
34
|
+
/** Subscribe and read the value at path. Re-renders when the value changes. */
|
|
35
|
+
use: <P extends FieldPath<T>>(path: P) => FieldPathValue<T, P> | undefined;
|
|
36
|
+
/** Subscribe and read the debounced value at path. Re-renders when the value changes. */
|
|
37
|
+
useDebounce: <P extends FieldPath<T>>(path: P, delay: number) => FieldPathValue<T, P> | undefined;
|
|
38
|
+
/** Set value at path (creates intermediate nodes as needed). */
|
|
39
|
+
set: <P extends FieldPath<T>>(path: P, value: FieldPathValue<T, P> | ((prev: FieldPathValue<T, P> | undefined) => FieldPathValue<T, P>), skipUpdate?: boolean) => void;
|
|
40
|
+
/** Read without subscribing. */
|
|
41
|
+
value: <P extends FieldPath<T>>(path: P) => FieldPathValue<T, P> | undefined;
|
|
42
|
+
/** Delete value at path (for arrays, removes index; for objects, deletes key). */
|
|
43
|
+
reset: <P extends FieldPath<T>>(path: P) => void;
|
|
44
|
+
/** Subscribe to changes at path and invoke listener with the new value. */
|
|
45
|
+
subscribe: <P extends FieldPath<T>>(path: P, listener: (value: FieldPathValue<T, P>) => void) => void;
|
|
46
|
+
/** Notify listeners at path. */
|
|
47
|
+
notify: <P extends FieldPath<T>>(path: P) => void;
|
|
48
|
+
/** Convenience hook returning [value, setValue] for the path. */
|
|
49
|
+
useState: <P extends FieldPath<T>>(path: P) => StoreUse<FieldPathValue<T, P>>;
|
|
50
|
+
/** Render-prop helper for inline usage. */
|
|
51
|
+
Render: <P extends FieldPath<T>>(props: FieldPathValue<T, P> extends undefined ? never : StoreRenderProps<T, P>) => React.ReactNode;
|
|
52
|
+
/** Show or hide children based on the value at the path. */
|
|
53
|
+
Show: <P extends FieldPath<T>>(props: FieldPathValue<T, P> extends undefined ? never : StoreShowProps<T, P>) => React.ReactNode;
|
|
54
|
+
};
|
|
55
|
+
/** Common methods available on any deep proxy node */
|
|
56
|
+
type State<T> = {
|
|
57
|
+
/** Read without subscribing. */
|
|
58
|
+
readonly value: T;
|
|
59
|
+
/** The field name for the proxy. */
|
|
60
|
+
readonly field: string;
|
|
61
|
+
/** Subscribe and read the value at path. Re-renders when the value changes. */
|
|
62
|
+
use(): T;
|
|
63
|
+
/** Subscribe and read the debounced value at path. Re-renders when the value changes. */
|
|
64
|
+
useDebounce(delay: number): T;
|
|
65
|
+
/** Convenience hook returning [value, setValue] for the path. */
|
|
66
|
+
useState(): readonly [T, (value: T | undefined) => void];
|
|
67
|
+
/** Set value at path (creates intermediate nodes as needed). */
|
|
68
|
+
set(value: T | undefined | ((prev: T) => T), skipUpdate?: boolean): void;
|
|
69
|
+
/** Delete value at path (for arrays, removes index; for objects, deletes key). */
|
|
70
|
+
reset(): void;
|
|
71
|
+
/** Subscribe to changes at path and invoke listener with the new value. */
|
|
72
|
+
subscribe(listener: (value: T) => void): void;
|
|
73
|
+
/** Compute a derived value from the current value, similar to useState + useMemo */
|
|
74
|
+
compute: <R>(fn: (value: T) => R) => R;
|
|
75
|
+
/** Virtual state derived from the current value. */
|
|
76
|
+
derived: <R>({ from, to }: {
|
|
77
|
+
from: (value: T | undefined) => R;
|
|
78
|
+
to: (value: R) => T | undefined;
|
|
79
|
+
}) => State<R>;
|
|
80
|
+
/** Notify listener of current value. */
|
|
81
|
+
notify(): void;
|
|
82
|
+
/** Render-prop helper for inline usage.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* <store.a.b.c.Render>
|
|
86
|
+
* {(value, update) => <button onClick={() => update('new value')}>{value}</button>}
|
|
87
|
+
* </store.a.b.c.Render>
|
|
88
|
+
*/
|
|
89
|
+
Render: (props: {
|
|
90
|
+
children: (value: T, update: (value: T | undefined) => void) => React.ReactNode;
|
|
91
|
+
}) => React.ReactNode;
|
|
92
|
+
/** Show or hide children based on the value at the path.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* <store.a.b.c.Show on={value => value === 'show'}>
|
|
96
|
+
* <div>Show</div>
|
|
97
|
+
* </store.a.b.c.Show>
|
|
98
|
+
*/
|
|
99
|
+
Show: (props: {
|
|
100
|
+
children: React.ReactNode;
|
|
101
|
+
on: (value: T) => boolean;
|
|
102
|
+
}) => React.ReactNode;
|
|
103
|
+
};
|
|
104
|
+
/** Props for Store.Render helper. */
|
|
105
|
+
type StoreRenderProps<T extends FieldValues, P extends FieldPath<T>> = {
|
|
106
|
+
path: P;
|
|
107
|
+
children: (value: FieldPathValue<T, P> | undefined, update: (value: FieldPathValue<T, P> | undefined) => void) => React.ReactNode;
|
|
108
|
+
};
|
|
109
|
+
/** Props for Store.Show helper. */
|
|
110
|
+
type StoreShowProps<T extends FieldValues, P extends FieldPath<T>> = {
|
|
111
|
+
path: P;
|
|
112
|
+
children: React.ReactNode;
|
|
113
|
+
on: (value: FieldPathValue<T, P> | undefined) => boolean;
|
|
114
|
+
};
|
|
115
|
+
type DerivedStateProps<T, R> = {
|
|
116
|
+
from: (value: T | undefined) => R;
|
|
117
|
+
to: (value: R) => T | undefined;
|
|
118
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|