juststore 0.3.2 → 0.3.4
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/README.md +37 -35
- package/dist/impl.d.ts +4 -3
- package/dist/impl.js +142 -29
- package/dist/index.d.ts +1 -1
- package/dist/mixed_state.d.ts +3 -8
- package/dist/mixed_state.js +3 -0
- package/dist/node.js +155 -69
- package/dist/root.js +30 -19
- package/dist/types.d.ts +19 -8
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -454,44 +454,46 @@ Creates a form store with validation support.
|
|
|
454
454
|
|
|
455
455
|
The store root provides path-based methods for dynamic access:
|
|
456
456
|
|
|
457
|
-
| Method
|
|
458
|
-
|
|
|
459
|
-
| `.state(path)`
|
|
460
|
-
| `.use(path)`
|
|
461
|
-
| `.useDebounce(path, ms)`
|
|
462
|
-
| `.useState(path)`
|
|
463
|
-
| `.value(path)`
|
|
464
|
-
| `.set(path, value)`
|
|
465
|
-
| `.set(path, fn)`
|
|
466
|
-
| `.reset(path)`
|
|
467
|
-
| `.rename(path, oldKey, newKey
|
|
468
|
-
| `.
|
|
469
|
-
| `.
|
|
470
|
-
| `.
|
|
471
|
-
| `.
|
|
472
|
-
| `.
|
|
457
|
+
| Method | Description |
|
|
458
|
+
| ------------------------------- | ------------------------------------------------------- |
|
|
459
|
+
| `.state(path)` | Get the state object for a path |
|
|
460
|
+
| `.use(path)` | Subscribe and read value (triggers re-render on change) |
|
|
461
|
+
| `.useDebounce(path, ms)` | Subscribe with debounced updates |
|
|
462
|
+
| `.useState(path)` | Returns `[value, setValue]` tuple |
|
|
463
|
+
| `.value(path)` | Read without subscribing |
|
|
464
|
+
| `.set(path, value)` | Update value |
|
|
465
|
+
| `.set(path, fn)` | Functional update |
|
|
466
|
+
| `.reset(path)` | Delete value at path |
|
|
467
|
+
| `.rename(path, oldKey, newKey)` | Rename a key in an object |
|
|
468
|
+
| `.keys(path)` | Get the readonly state of keys of an object |
|
|
469
|
+
| `.subscribe(path, fn)` | Subscribe to changes (for effects) |
|
|
470
|
+
| `.notify(path)` | Manually trigger subscribers |
|
|
471
|
+
| `.useCompute(path, fn)` | Derive a computed value |
|
|
472
|
+
| `.Render({ path, children })` | Render prop component |
|
|
473
|
+
| `.Show({ path, children, on })` | Conditional render component |
|
|
473
474
|
|
|
474
475
|
### State Methods
|
|
475
476
|
|
|
476
|
-
| Method
|
|
477
|
-
|
|
|
478
|
-
| `.use()`
|
|
479
|
-
| `.useDebounce(ms)`
|
|
480
|
-
| `.useState()`
|
|
481
|
-
| `.value`
|
|
482
|
-
| `.set(value)`
|
|
483
|
-
| `.set(fn)`
|
|
484
|
-
| `.reset()`
|
|
485
|
-
| `.subscribe(fn)`
|
|
486
|
-
| `.rename(oldKey, newKey
|
|
487
|
-
| `.
|
|
488
|
-
| `.
|
|
489
|
-
| `.
|
|
490
|
-
| `.
|
|
491
|
-
| `.
|
|
492
|
-
| `.
|
|
493
|
-
| `.
|
|
494
|
-
| `.
|
|
477
|
+
| Method | Description |
|
|
478
|
+
| ---------------------------- | ----------------------------------------------------------------------- |
|
|
479
|
+
| `.use()` | Subscribe and read value (triggers re-render on change) |
|
|
480
|
+
| `.useDebounce(ms)` | Subscribe with debounced updates |
|
|
481
|
+
| `.useState()` | Returns `[value, setValue]` tuple |
|
|
482
|
+
| `.value` | Read without subscribing |
|
|
483
|
+
| `.set(value)` | Update value |
|
|
484
|
+
| `.set(fn)` | Functional update |
|
|
485
|
+
| `.reset()` | Delete value at path |
|
|
486
|
+
| `.subscribe(fn)` | Subscribe to changes (for effects) |
|
|
487
|
+
| `.rename(oldKey, newKey)` | Rename a key in an object |
|
|
488
|
+
| `.keys()` | Get the readonly state of keys of an object |
|
|
489
|
+
| `.notify()` | Manually trigger subscribers |
|
|
490
|
+
| `.useCompute(fn)` | Derive a computed value |
|
|
491
|
+
| `.derived({ from, to })` | Create bidirectional transform |
|
|
492
|
+
| `.ensureArray()` | Ensure the value is an array |
|
|
493
|
+
| `.ensureObject()` | Ensure the value is an object |
|
|
494
|
+
| `.withDefault(defaultValue)` | Return a new state with a default value, and make the type non-nullable |
|
|
495
|
+
| `.Render({ children })` | Render prop component |
|
|
496
|
+
| `.Show({ children, on })` | Conditional render component |
|
|
495
497
|
|
|
496
498
|
## License
|
|
497
499
|
|
package/dist/impl.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { FieldPath, FieldPathValue, FieldValues } from './path';
|
|
2
|
-
export { getNestedValue, getSnapshot, isClass, isEqual, joinPath, notifyListeners, produce, rename, setLeaf, subscribe, useDebounce, useObject, useSubscribe };
|
|
2
|
+
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, subscribe, useDebounce, useObject, useSubscribe };
|
|
3
|
+
declare function setExternalKeyOrder(target: object, keys: string[]): void;
|
|
4
|
+
declare function getStableKeys(value: unknown): string[];
|
|
3
5
|
declare function isClass(value: unknown): boolean;
|
|
4
6
|
/** Compare two values for equality
|
|
5
7
|
* @description
|
|
@@ -70,9 +72,8 @@ declare function produce(key: string, value: unknown, skipUpdate?: boolean, memo
|
|
|
70
72
|
* @param path - The full key path to rename
|
|
71
73
|
* @param oldKey - The old key to rename
|
|
72
74
|
* @param newKey - The new key to rename to
|
|
73
|
-
* @param notifyObject - Whether to notify listeners to the object path
|
|
74
75
|
*/
|
|
75
|
-
declare function rename(path: string, oldKey: string, newKey: string
|
|
76
|
+
declare function rename(path: string, oldKey: string, newKey: string): void;
|
|
76
77
|
/**
|
|
77
78
|
* React hook that subscribes to and reads a value at a path.
|
|
78
79
|
*
|
package/dist/impl.js
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
2
|
import rfcIsEqual from 'react-fast-compare';
|
|
3
3
|
import { localStorageDelete, localStorageGet, localStorageSet } from './local_storage';
|
|
4
|
-
export { getNestedValue, getSnapshot, isClass, isEqual, joinPath, notifyListeners, produce, rename, setLeaf, subscribe, useDebounce, useObject, useSubscribe };
|
|
4
|
+
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, subscribe, useDebounce, useObject, useSubscribe };
|
|
5
5
|
const memoryStore = new Map();
|
|
6
6
|
const listeners = new Map();
|
|
7
7
|
const descendantListenerKeysByPrefix = new Map();
|
|
8
|
+
const virtualRevisions = new Map();
|
|
9
|
+
const externalKeyOrder = new WeakMap();
|
|
10
|
+
function isVirtualKey(key) {
|
|
11
|
+
return key.endsWith('.__juststore_keys') || key === '__juststore_keys';
|
|
12
|
+
}
|
|
13
|
+
function setExternalKeyOrder(target, keys) {
|
|
14
|
+
externalKeyOrder.set(target, keys);
|
|
15
|
+
}
|
|
16
|
+
function hasOwn(target, key) {
|
|
17
|
+
return Object.prototype.hasOwnProperty.call(target, key);
|
|
18
|
+
}
|
|
19
|
+
function getStableKeys(value) {
|
|
20
|
+
if (!isRecord(value))
|
|
21
|
+
return [];
|
|
22
|
+
const target = value;
|
|
23
|
+
const existing = externalKeyOrder.get(target);
|
|
24
|
+
if (existing) {
|
|
25
|
+
const next = existing.filter(k => hasOwn(target, k));
|
|
26
|
+
const nextSet = new Set(next);
|
|
27
|
+
for (const k of Object.keys(target)) {
|
|
28
|
+
if (nextSet.has(k))
|
|
29
|
+
continue;
|
|
30
|
+
next.push(k);
|
|
31
|
+
}
|
|
32
|
+
if (next.length !== existing.length) {
|
|
33
|
+
setExternalKeyOrder(target, next);
|
|
34
|
+
}
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
const keys = Object.keys(target);
|
|
38
|
+
setExternalKeyOrder(target, keys);
|
|
39
|
+
return keys;
|
|
40
|
+
}
|
|
8
41
|
// check if the value is a class instance
|
|
9
42
|
function isClass(value) {
|
|
10
43
|
if (value === null || value === undefined)
|
|
@@ -21,6 +54,13 @@ function isClass(value) {
|
|
|
21
54
|
}
|
|
22
55
|
return false;
|
|
23
56
|
}
|
|
57
|
+
function isRecord(value) {
|
|
58
|
+
if (value === null || value === undefined)
|
|
59
|
+
return false;
|
|
60
|
+
if (typeof value !== 'object')
|
|
61
|
+
return false;
|
|
62
|
+
return !Array.isArray(value) && !isClass(value);
|
|
63
|
+
}
|
|
24
64
|
/** Compare two values for equality
|
|
25
65
|
* @description
|
|
26
66
|
* - react-fast-compare for non-class instances
|
|
@@ -95,7 +135,14 @@ function setNestedValue(obj, path, value) {
|
|
|
95
135
|
? {}
|
|
96
136
|
: Array.isArray(obj)
|
|
97
137
|
? [...obj]
|
|
98
|
-
:
|
|
138
|
+
: (() => {
|
|
139
|
+
const existing = obj;
|
|
140
|
+
const next = { ...existing };
|
|
141
|
+
const order = externalKeyOrder.get(existing);
|
|
142
|
+
if (order)
|
|
143
|
+
setExternalKeyOrder(next, order);
|
|
144
|
+
return next;
|
|
145
|
+
})();
|
|
99
146
|
let current = result;
|
|
100
147
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
101
148
|
const segment = segments[i];
|
|
@@ -136,7 +183,11 @@ function setNestedValue(obj, path, value) {
|
|
|
136
183
|
next = [...existing];
|
|
137
184
|
}
|
|
138
185
|
else {
|
|
139
|
-
|
|
186
|
+
const existingObj = existing;
|
|
187
|
+
next = { ...existingObj };
|
|
188
|
+
const order = externalKeyOrder.get(existingObj);
|
|
189
|
+
if (order)
|
|
190
|
+
setExternalKeyOrder(next, order);
|
|
140
191
|
}
|
|
141
192
|
currentObj[segment] = next;
|
|
142
193
|
current = next;
|
|
@@ -156,11 +207,22 @@ function setNestedValue(obj, path, value) {
|
|
|
156
207
|
}
|
|
157
208
|
else if (typeof current === 'object' && current !== null) {
|
|
158
209
|
const currentObj = current;
|
|
210
|
+
const hadKey = hasOwn(currentObj, lastSegment);
|
|
159
211
|
if (value === undefined) {
|
|
160
212
|
delete currentObj[lastSegment];
|
|
213
|
+
if (hadKey) {
|
|
214
|
+
const order = externalKeyOrder.get(currentObj);
|
|
215
|
+
if (order)
|
|
216
|
+
setExternalKeyOrder(currentObj, order.filter(k => k !== lastSegment));
|
|
217
|
+
}
|
|
161
218
|
}
|
|
162
219
|
else {
|
|
163
220
|
currentObj[lastSegment] = value;
|
|
221
|
+
if (!hadKey) {
|
|
222
|
+
const order = externalKeyOrder.get(currentObj);
|
|
223
|
+
if (order)
|
|
224
|
+
setExternalKeyOrder(currentObj, [...order, lastSegment]);
|
|
225
|
+
}
|
|
164
226
|
}
|
|
165
227
|
}
|
|
166
228
|
return result;
|
|
@@ -209,6 +271,9 @@ function getKeyPrefixes(key) {
|
|
|
209
271
|
prefixes.unshift(parts[0]);
|
|
210
272
|
return prefixes;
|
|
211
273
|
}
|
|
274
|
+
function joinChildKey(parent, child) {
|
|
275
|
+
return parent ? `${parent}.${child}` : child;
|
|
276
|
+
}
|
|
212
277
|
/**
|
|
213
278
|
* Notifies all relevant listeners when a value changes.
|
|
214
279
|
*
|
|
@@ -221,6 +286,23 @@ function getKeyPrefixes(key) {
|
|
|
221
286
|
* determined by deep equality comparison.
|
|
222
287
|
*/
|
|
223
288
|
function notifyListeners(key, oldValue, newValue, { skipRoot = false, skipChildren = false, forceNotify = false } = {}) {
|
|
289
|
+
// Keep `state.xxx.keys()` in sync: any mutation under a path can change the set of
|
|
290
|
+
// keys for that path (or its ancestors). Keys are represented as virtual nodes at
|
|
291
|
+
// `${path}.__juststore_keys`, so we bump those virtual nodes here.
|
|
292
|
+
//
|
|
293
|
+
// Important: avoid recursion when *we* are notifying a virtual key.
|
|
294
|
+
if (!isVirtualKey(key)) {
|
|
295
|
+
const paths = [...getKeyPrefixes(key), key];
|
|
296
|
+
for (const p of paths) {
|
|
297
|
+
const virtualKey = joinChildKey(p, '__juststore_keys');
|
|
298
|
+
const listenerSet = listeners.get(virtualKey);
|
|
299
|
+
if (listenerSet && listenerSet.size > 0) {
|
|
300
|
+
// Only notify the virtual key subscribers; the current call will handle
|
|
301
|
+
// ancestors/children for the real key.
|
|
302
|
+
notifyVirtualKey(virtualKey);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
224
306
|
if (skipRoot && skipChildren) {
|
|
225
307
|
if (!forceNotify && isEqual(oldValue, newValue)) {
|
|
226
308
|
return;
|
|
@@ -260,6 +342,21 @@ function notifyListeners(key, oldValue, newValue, { skipRoot = false, skipChildr
|
|
|
260
342
|
const childKeys = descendantListenerKeysByPrefix.get(key);
|
|
261
343
|
if (childKeys) {
|
|
262
344
|
for (const childKey of childKeys) {
|
|
345
|
+
if (isVirtualKey(childKey)) {
|
|
346
|
+
const childPath = childKey.slice(key.length + 1);
|
|
347
|
+
const suffix = '.__juststore_keys';
|
|
348
|
+
const objectPath = childPath.endsWith(suffix) ? childPath.slice(0, -suffix.length) : '';
|
|
349
|
+
const getKeys = (root) => {
|
|
350
|
+
const obj = objectPath ? getNestedValue(root, objectPath) : root;
|
|
351
|
+
return getStableKeys(obj);
|
|
352
|
+
};
|
|
353
|
+
const oldKeys = getKeys(oldValue);
|
|
354
|
+
const newKeys = getKeys(newValue);
|
|
355
|
+
if (forceNotify || !isEqual(oldKeys, newKeys)) {
|
|
356
|
+
notifyVirtualKey(childKey);
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
263
360
|
const childPath = childKey.slice(key.length + 1);
|
|
264
361
|
const oldChildValue = getNestedValue(oldValue, childPath);
|
|
265
362
|
const newChildValue = getNestedValue(newValue, childPath);
|
|
@@ -273,8 +370,13 @@ function notifyListeners(key, oldValue, newValue, { skipRoot = false, skipChildr
|
|
|
273
370
|
}
|
|
274
371
|
}
|
|
275
372
|
}
|
|
276
|
-
function
|
|
277
|
-
|
|
373
|
+
function notifyVirtualKey(key) {
|
|
374
|
+
virtualRevisions.set(key, (virtualRevisions.get(key) ?? 0) + 1);
|
|
375
|
+
notifyListeners(key, undefined, undefined, {
|
|
376
|
+
skipRoot: true,
|
|
377
|
+
skipChildren: true,
|
|
378
|
+
forceNotify: true
|
|
379
|
+
});
|
|
278
380
|
}
|
|
279
381
|
// BroadcastChannel for cross-tab synchronization
|
|
280
382
|
const broadcastChannel = typeof window !== 'undefined' ? new BroadcastChannel('juststore') : null;
|
|
@@ -367,6 +469,9 @@ const store = {
|
|
|
367
469
|
};
|
|
368
470
|
/** Snapshot getter used by React's useSyncExternalStore. */
|
|
369
471
|
function getSnapshot(key) {
|
|
472
|
+
if (isVirtualKey(key)) {
|
|
473
|
+
return virtualRevisions.get(key) ?? 0;
|
|
474
|
+
}
|
|
370
475
|
return store.get(key);
|
|
371
476
|
}
|
|
372
477
|
// Cross-tab synchronization: keep memoryStore in sync with BroadcastChannel events
|
|
@@ -413,14 +518,14 @@ function subscribe(key, listener) {
|
|
|
413
518
|
keyListeners.delete(listener);
|
|
414
519
|
if (keyListeners.size === 0) {
|
|
415
520
|
listeners.delete(key);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
521
|
+
for (const prefix of prefixes) {
|
|
522
|
+
const prefixKeys = descendantListenerKeysByPrefix.get(prefix);
|
|
523
|
+
if (prefixKeys) {
|
|
524
|
+
prefixKeys.delete(key);
|
|
525
|
+
if (prefixKeys.size === 0) {
|
|
526
|
+
descendantListenerKeysByPrefix.delete(prefix);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
424
529
|
}
|
|
425
530
|
}
|
|
426
531
|
}
|
|
@@ -459,29 +564,37 @@ function produce(key, value, skipUpdate = false, memoryOnly = false) {
|
|
|
459
564
|
* @param path - The full key path to rename
|
|
460
565
|
* @param oldKey - The old key to rename
|
|
461
566
|
* @param newKey - The new key to rename to
|
|
462
|
-
* @param notifyObject - Whether to notify listeners to the object path
|
|
463
567
|
*/
|
|
464
|
-
function rename(path, oldKey, newKey
|
|
568
|
+
function rename(path, oldKey, newKey) {
|
|
465
569
|
const current = store.get(path);
|
|
466
570
|
if (current === undefined || current === null || typeof current !== 'object') {
|
|
467
571
|
// assign a new object with the new key
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
572
|
+
const next = { [newKey]: undefined };
|
|
573
|
+
store.set(path, next);
|
|
574
|
+
setExternalKeyOrder(next, [newKey]);
|
|
575
|
+
notifyListeners(path, current, next);
|
|
472
576
|
return;
|
|
473
577
|
}
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
578
|
+
const obj = current;
|
|
579
|
+
if (oldKey === newKey)
|
|
580
|
+
return;
|
|
581
|
+
if (!hasOwn(obj, oldKey))
|
|
582
|
+
return;
|
|
583
|
+
const keyOrder = getStableKeys(obj);
|
|
584
|
+
const entries = [];
|
|
585
|
+
for (const key of keyOrder) {
|
|
586
|
+
if (!hasOwn(obj, key))
|
|
587
|
+
continue;
|
|
588
|
+
if (key === oldKey) {
|
|
589
|
+
entries.push([newKey, obj[oldKey]]);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
entries.push([key, obj[key]]);
|
|
484
593
|
}
|
|
594
|
+
const newObject = Object.fromEntries(entries);
|
|
595
|
+
store.set(path, newObject);
|
|
596
|
+
setExternalKeyOrder(newObject, Array.from(new Set(entries.map(([k]) => k))));
|
|
597
|
+
notifyListeners(path, current, newObject);
|
|
485
598
|
}
|
|
486
599
|
/**
|
|
487
600
|
* React hook that subscribes to and reads a value at a path.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type * from './form';
|
|
2
2
|
export { useForm } from './form';
|
|
3
3
|
export { useMemoryStore, type MemoryStore } from './memory';
|
|
4
|
-
export { createMixedState
|
|
4
|
+
export { createMixedState } from './mixed_state';
|
|
5
5
|
export type * from './path';
|
|
6
6
|
export { createStore, type Store } from './store';
|
|
7
7
|
export type * from './types';
|
package/dist/mixed_state.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export { createMixedState
|
|
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
|
-
*/
|
|
7
|
-
type MixedState<T extends readonly unknown[]> = Prettify<Pick<ValueState<Readonly<Required<T>>>, 'value' | 'use' | 'Render' | 'Show'>>;
|
|
1
|
+
import type { ReadOnlyState, ValueState } from './types';
|
|
2
|
+
export { createMixedState };
|
|
8
3
|
/**
|
|
9
4
|
* Creates a mixed state that combines multiple states into a tuple.
|
|
10
5
|
*
|
|
@@ -22,4 +17,4 @@ type MixedState<T extends readonly unknown[]> = Prettify<Pick<ValueState<Readonl
|
|
|
22
17
|
*/
|
|
23
18
|
declare function createMixedState<T extends readonly unknown[]>(...states: {
|
|
24
19
|
[K in keyof T]-?: ValueState<T[K]>;
|
|
25
|
-
}):
|
|
20
|
+
}): ReadOnlyState<T>;
|
package/dist/mixed_state.js
CHANGED
|
@@ -21,6 +21,9 @@ function createMixedState(...states) {
|
|
|
21
21
|
return states.map(state => state.value);
|
|
22
22
|
},
|
|
23
23
|
use,
|
|
24
|
+
useCompute(fn) {
|
|
25
|
+
return states.map(state => state.useCompute(value => fn(value)));
|
|
26
|
+
},
|
|
24
27
|
Render({ children }) {
|
|
25
28
|
const value = use();
|
|
26
29
|
return children(value);
|
package/dist/node.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { getStableKeys } from './impl';
|
|
1
3
|
export { createNode, createRootNode };
|
|
2
4
|
/**
|
|
3
5
|
* Creates the root proxy node for dynamic path access.
|
|
@@ -40,60 +42,80 @@ function createNode(storeApi, path, cache, extensions, from = unchanged, to = un
|
|
|
40
42
|
return fieldName;
|
|
41
43
|
}
|
|
42
44
|
if (prop === 'use') {
|
|
43
|
-
return () => from(storeApi.use(path));
|
|
45
|
+
return (_target._use ??= () => from(storeApi.use(path)));
|
|
44
46
|
}
|
|
45
47
|
if (prop === 'useDebounce') {
|
|
46
|
-
return (delay) => from(storeApi.useDebounce(path, delay));
|
|
48
|
+
return (_target._useDebounce ??= (delay) => from(storeApi.useDebounce(path, delay)));
|
|
47
49
|
}
|
|
48
50
|
if (prop === 'useState') {
|
|
49
|
-
return () => {
|
|
51
|
+
return (_target._useState ??= () => {
|
|
50
52
|
const value = storeApi.use(path);
|
|
51
53
|
return [from(value), (next) => storeApi.set(path, to(next))];
|
|
52
|
-
};
|
|
54
|
+
});
|
|
53
55
|
}
|
|
54
56
|
if (prop === 'value') {
|
|
55
57
|
return from(storeApi.value(path));
|
|
56
58
|
}
|
|
57
59
|
if (prop === 'set') {
|
|
58
|
-
return (value, skipUpdate) => storeApi.set(path, to(value), skipUpdate);
|
|
60
|
+
return (_target._set ??= (value, skipUpdate) => storeApi.set(path, to(value), skipUpdate));
|
|
59
61
|
}
|
|
60
62
|
if (prop === 'reset') {
|
|
61
|
-
return () => storeApi.reset(path);
|
|
63
|
+
return (_target._reset ??= () => storeApi.reset(path));
|
|
62
64
|
}
|
|
63
65
|
if (prop === 'subscribe') {
|
|
64
|
-
return (listener) => storeApi.subscribe(path, value => listener(to(value)));
|
|
66
|
+
return (_target._subscribe ??= (listener) => storeApi.subscribe(path, value => listener(to(value))));
|
|
65
67
|
}
|
|
66
68
|
if (prop === 'Render') {
|
|
67
|
-
return ({ children }) => storeApi.Render({
|
|
69
|
+
return (_target._Render ??= ({ children }) => storeApi.Render({
|
|
68
70
|
path,
|
|
69
71
|
children: (value, update) => children(from(value), value => update(to(value)))
|
|
70
|
-
});
|
|
72
|
+
}));
|
|
71
73
|
}
|
|
72
74
|
if (prop === 'Show') {
|
|
73
|
-
return ({ children, on }) => storeApi.Show({ path, children, on: value => on(from(value)) });
|
|
75
|
+
return (_target._Show ??= ({ children, on }) => storeApi.Show({ path, children, on: value => on(from(value)) }));
|
|
74
76
|
}
|
|
75
77
|
if (prop === 'useCompute') {
|
|
76
|
-
return (fn) => {
|
|
78
|
+
return (_target._useCompute ??= (fn) => {
|
|
77
79
|
return storeApi.useCompute(path, value => fn(from(value)));
|
|
78
|
-
};
|
|
80
|
+
});
|
|
79
81
|
}
|
|
80
82
|
if (prop === 'derived') {
|
|
81
83
|
if (isDerived) {
|
|
82
84
|
throw new Error(`Derived method cannot be called on a derived node: ${path}`);
|
|
83
85
|
}
|
|
84
|
-
return ({ from, to }) => createNode(storeApi, path, cache, extensions, from, to);
|
|
86
|
+
return (_target._derived ??= ({ from, to }) => createNode(storeApi, path, cache, extensions, from, to));
|
|
85
87
|
}
|
|
86
88
|
if (prop === 'notify') {
|
|
87
|
-
return () => storeApi.notify(path);
|
|
89
|
+
return (_target._notify ??= () => storeApi.notify(path));
|
|
88
90
|
}
|
|
89
91
|
if (prop === 'ensureArray') {
|
|
90
|
-
return (
|
|
92
|
+
return (_target._ensureArray ??= () => {
|
|
93
|
+
const cacheKey = `${path}.__juststore_ensureArray`;
|
|
94
|
+
if (!isDerived && cache.has(cacheKey)) {
|
|
95
|
+
return cache.get(cacheKey);
|
|
96
|
+
}
|
|
97
|
+
const node = createNode(storeApi, path, cache, extensions, value => ensureArray(value, from), unchanged);
|
|
98
|
+
if (!isDerived) {
|
|
99
|
+
cache.set(cacheKey, node);
|
|
100
|
+
}
|
|
101
|
+
return node;
|
|
102
|
+
});
|
|
91
103
|
}
|
|
92
104
|
if (prop === 'ensureObject') {
|
|
93
|
-
return (
|
|
105
|
+
return (_target._ensureObject ??= () => {
|
|
106
|
+
const cacheKey = `${path}.__juststore_ensureObject`;
|
|
107
|
+
if (!isDerived && cache.has(cacheKey)) {
|
|
108
|
+
return cache.get(cacheKey);
|
|
109
|
+
}
|
|
110
|
+
const node = createNode(storeApi, path, cache, extensions, value => ensureObject(value, from), to);
|
|
111
|
+
if (!isDerived) {
|
|
112
|
+
cache.set(cacheKey, node);
|
|
113
|
+
}
|
|
114
|
+
return node;
|
|
115
|
+
});
|
|
94
116
|
}
|
|
95
117
|
if (prop === 'withDefault') {
|
|
96
|
-
return (defaultValue) => createNode(storeApi, path, cache, extensions, value => withDefault(value, defaultValue, from), to);
|
|
118
|
+
return (_target._withDefault ??= (defaultValue) => createNode(storeApi, path, cache, extensions, value => withDefault(value, defaultValue, from), to));
|
|
97
119
|
}
|
|
98
120
|
if (isObjectMethod(prop)) {
|
|
99
121
|
const derivedValue = from(storeApi.value(path));
|
|
@@ -101,9 +123,18 @@ function createNode(storeApi, path, cache, extensions, from = unchanged, to = un
|
|
|
101
123
|
throw new Error(`Expected object at path ${path}, got ${typeof derivedValue}`);
|
|
102
124
|
}
|
|
103
125
|
if (prop === 'rename') {
|
|
104
|
-
return (oldKey, newKey
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
return (_target._rename ??= (oldKey, newKey) => storeApi.rename(path, oldKey, newKey));
|
|
127
|
+
}
|
|
128
|
+
if (prop === 'keys') {
|
|
129
|
+
const cacheKey = `${path}.__juststore_keys`;
|
|
130
|
+
if (!isDerived && cache.has(cacheKey)) {
|
|
131
|
+
return cache.get(cacheKey);
|
|
132
|
+
}
|
|
133
|
+
const keysNode = createKeysNode(storeApi, path, value => ensureObject(value, from));
|
|
134
|
+
if (!isDerived) {
|
|
135
|
+
cache.set(cacheKey, keysNode);
|
|
136
|
+
}
|
|
137
|
+
return keysNode;
|
|
107
138
|
}
|
|
108
139
|
}
|
|
109
140
|
if (isArrayMethod(prop)) {
|
|
@@ -113,91 +144,115 @@ function createNode(storeApi, path, cache, extensions, from = unchanged, to = un
|
|
|
113
144
|
}
|
|
114
145
|
const currentArray = derivedValue ? [...derivedValue] : [];
|
|
115
146
|
if (prop === 'at') {
|
|
116
|
-
return (index) => {
|
|
147
|
+
return (_target._at ??= (index) => {
|
|
117
148
|
const nextPath = path ? `${path}.${index}` : String(index);
|
|
118
149
|
return createNode(storeApi, nextPath, cache, extensions);
|
|
119
|
-
};
|
|
150
|
+
});
|
|
120
151
|
}
|
|
121
152
|
if (prop === 'length') {
|
|
122
153
|
return currentArray.length;
|
|
123
154
|
}
|
|
124
155
|
// Array mutation methods
|
|
125
156
|
if (prop === 'push') {
|
|
126
|
-
return (...items) => {
|
|
127
|
-
|
|
157
|
+
return (_target._push ??= (...items) => {
|
|
158
|
+
// We need to fetch the current array at call time, not bind time
|
|
159
|
+
const arr = from(storeApi.value(path)) ?? [];
|
|
160
|
+
const newArray = [...arr, ...items];
|
|
128
161
|
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
129
162
|
return newArray.length;
|
|
130
|
-
};
|
|
163
|
+
});
|
|
131
164
|
}
|
|
132
165
|
if (prop === 'pop') {
|
|
133
|
-
return () => {
|
|
134
|
-
|
|
166
|
+
return (_target._pop ??= () => {
|
|
167
|
+
const arr = from(storeApi.value(path)) ?? [];
|
|
168
|
+
if (arr.length === 0)
|
|
135
169
|
return undefined;
|
|
136
|
-
const newArray =
|
|
137
|
-
const poppedItem =
|
|
170
|
+
const newArray = arr.slice(0, -1);
|
|
171
|
+
const poppedItem = arr[arr.length - 1];
|
|
138
172
|
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
139
173
|
return poppedItem;
|
|
140
|
-
};
|
|
174
|
+
});
|
|
141
175
|
}
|
|
142
176
|
if (prop === 'shift') {
|
|
143
|
-
return () => {
|
|
144
|
-
|
|
177
|
+
return (_target._shift ??= () => {
|
|
178
|
+
const arr = from(storeApi.value(path)) ?? [];
|
|
179
|
+
if (arr.length === 0)
|
|
145
180
|
return undefined;
|
|
146
|
-
const newArray =
|
|
147
|
-
const shiftedItem =
|
|
181
|
+
const newArray = arr.slice(1);
|
|
182
|
+
const shiftedItem = arr[0];
|
|
148
183
|
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
149
184
|
return shiftedItem;
|
|
150
|
-
};
|
|
185
|
+
});
|
|
151
186
|
}
|
|
152
187
|
if (prop === 'unshift') {
|
|
153
|
-
return (...items) => {
|
|
154
|
-
const
|
|
188
|
+
return (_target._unshift ??= (...items) => {
|
|
189
|
+
const arr = from(storeApi.value(path)) ?? [];
|
|
190
|
+
const newArray = [...items, ...arr];
|
|
155
191
|
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
156
192
|
return newArray.length;
|
|
157
|
-
};
|
|
193
|
+
});
|
|
158
194
|
}
|
|
159
195
|
if (prop === 'splice') {
|
|
160
|
-
return (start, deleteCount, ...items) => {
|
|
161
|
-
const
|
|
162
|
-
|
|
196
|
+
return (_target._splice ??= (start, deleteCount, ...items) => {
|
|
197
|
+
const arr = from(storeApi.value(path)) ?? [];
|
|
198
|
+
const newArray = [...arr];
|
|
199
|
+
const deletedItems = newArray.splice(start, deleteCount ?? 0, ...items);
|
|
200
|
+
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
163
201
|
return deletedItems;
|
|
164
|
-
};
|
|
202
|
+
});
|
|
165
203
|
}
|
|
166
204
|
if (prop === 'reverse') {
|
|
167
|
-
return () => {
|
|
168
|
-
|
|
205
|
+
return (_target._reverse ??= () => {
|
|
206
|
+
const arr = from(storeApi.value(path));
|
|
207
|
+
if (!Array.isArray(arr))
|
|
169
208
|
return [];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
209
|
+
const newArray = [...arr];
|
|
210
|
+
newArray.reverse();
|
|
211
|
+
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
212
|
+
return newArray;
|
|
213
|
+
});
|
|
174
214
|
}
|
|
175
215
|
if (prop === 'sort') {
|
|
176
|
-
return (compareFn) => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
216
|
+
return (_target._sort ??= (compareFn) => {
|
|
217
|
+
const arr = from(storeApi.value(path));
|
|
218
|
+
if (!Array.isArray(arr))
|
|
219
|
+
return [];
|
|
220
|
+
const newArray = [...arr];
|
|
221
|
+
newArray.sort(compareFn);
|
|
222
|
+
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
223
|
+
return newArray;
|
|
224
|
+
});
|
|
181
225
|
}
|
|
182
226
|
if (prop === 'fill') {
|
|
183
|
-
return (value, start, end) => {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
227
|
+
return (_target._fill ??= (value, start, end) => {
|
|
228
|
+
const arr = from(storeApi.value(path));
|
|
229
|
+
if (!Array.isArray(arr))
|
|
230
|
+
return [];
|
|
231
|
+
const newArray = [...arr];
|
|
232
|
+
newArray.fill(value, start, end);
|
|
233
|
+
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
234
|
+
return newArray;
|
|
235
|
+
});
|
|
188
236
|
}
|
|
189
237
|
if (prop === 'copyWithin') {
|
|
190
|
-
return (target, start, end) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
238
|
+
return (_target._copyWithin ??= (target, start, end) => {
|
|
239
|
+
const arr = from(storeApi.value(path));
|
|
240
|
+
if (!Array.isArray(arr))
|
|
241
|
+
return [];
|
|
242
|
+
const newArray = [...arr];
|
|
243
|
+
newArray.copyWithin(target, start, end);
|
|
244
|
+
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
245
|
+
return newArray;
|
|
246
|
+
});
|
|
195
247
|
}
|
|
196
248
|
if (prop === 'sortedInsert') {
|
|
197
|
-
return (cmp, ...items) => {
|
|
249
|
+
return (_target._sortedInsert ??= (cmp, ...items) => {
|
|
250
|
+
const arr = from(storeApi.value(path));
|
|
251
|
+
if (!Array.isArray(arr))
|
|
252
|
+
return [];
|
|
198
253
|
if (typeof cmp !== 'function')
|
|
199
|
-
return
|
|
200
|
-
const newArray = [...
|
|
254
|
+
return arr.length;
|
|
255
|
+
const newArray = [...arr];
|
|
201
256
|
for (const item of items) {
|
|
202
257
|
let left = 0;
|
|
203
258
|
let right = newArray.length;
|
|
@@ -216,7 +271,7 @@ function createNode(storeApi, path, cache, extensions, from = unchanged, to = un
|
|
|
216
271
|
}
|
|
217
272
|
storeApi.set(path, isDerived ? newArray.map(to) : newArray);
|
|
218
273
|
return newArray.length;
|
|
219
|
-
};
|
|
274
|
+
});
|
|
220
275
|
}
|
|
221
276
|
}
|
|
222
277
|
if (extensions?.[prop]?.get) {
|
|
@@ -260,13 +315,44 @@ function isArrayMethod(prop) {
|
|
|
260
315
|
prop === 'sortedInsert');
|
|
261
316
|
}
|
|
262
317
|
function isObjectMethod(prop) {
|
|
263
|
-
return prop === 'rename';
|
|
318
|
+
return prop === 'rename' || prop === 'keys';
|
|
264
319
|
}
|
|
265
320
|
function unchanged(value) {
|
|
266
321
|
return value;
|
|
267
322
|
}
|
|
268
323
|
const EMPTY_ARRAY = [];
|
|
269
324
|
const EMPTY_OBJECT = {};
|
|
325
|
+
function createKeysNode(storeApi, path, getObjectValue) {
|
|
326
|
+
const signalPath = path ? `${path}.__juststore_keys` : '__juststore_keys';
|
|
327
|
+
const computeKeys = () => {
|
|
328
|
+
return getStableKeys(getObjectValue(storeApi.value(path)));
|
|
329
|
+
};
|
|
330
|
+
return new Proxy({}, {
|
|
331
|
+
get(_target, prop) {
|
|
332
|
+
if (prop === 'use') {
|
|
333
|
+
return (_target._use ??= () => storeApi.useCompute(signalPath, computeKeys));
|
|
334
|
+
}
|
|
335
|
+
if (prop === 'value')
|
|
336
|
+
return computeKeys();
|
|
337
|
+
if (prop === 'useCompute') {
|
|
338
|
+
return (_target._useCompute ??= (fn) => {
|
|
339
|
+
return storeApi.useCompute(signalPath, () => fn(computeKeys()));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (prop === 'Render') {
|
|
343
|
+
return (_target._Render ??= ({ children }) => children(storeApi.useCompute(signalPath, computeKeys), () => { }));
|
|
344
|
+
}
|
|
345
|
+
if (prop === 'Show') {
|
|
346
|
+
// eslint-disable-next-line react/display-name
|
|
347
|
+
return (_target._Show ??= ({ children, on }) => {
|
|
348
|
+
const show = storeApi.useCompute(signalPath, () => on(computeKeys()), [on]);
|
|
349
|
+
return show ? children : null;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
270
356
|
function ensureArray(value, from) {
|
|
271
357
|
if (value === undefined || value === null)
|
|
272
358
|
return EMPTY_ARRAY;
|
|
@@ -279,7 +365,7 @@ function ensureObject(value, from) {
|
|
|
279
365
|
if (value === undefined || value === null)
|
|
280
366
|
return EMPTY_OBJECT;
|
|
281
367
|
const obj = from(value);
|
|
282
|
-
if (typeof obj === 'object')
|
|
368
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj))
|
|
283
369
|
return obj;
|
|
284
370
|
return EMPTY_OBJECT;
|
|
285
371
|
}
|
package/dist/root.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
2
|
-
import { getNestedValue, getSnapshot, joinPath, notifyListeners, produce, rename, setLeaf, subscribe, useDebounce, useObject, useSubscribe } from './impl';
|
|
2
|
+
import { getNestedValue, getSnapshot, isEqual, joinPath, notifyListeners, produce, rename, setLeaf, subscribe, useDebounce, useObject, useSubscribe } from './impl';
|
|
3
3
|
import { createRootNode } from './node';
|
|
4
4
|
export { createStoreRoot };
|
|
5
5
|
/**
|
|
@@ -33,28 +33,39 @@ function createStoreRoot(namespace, defaultValue, options = {}) {
|
|
|
33
33
|
},
|
|
34
34
|
value: (path) => getSnapshot(joinPath(namespace, path)),
|
|
35
35
|
reset: (path) => produce(joinPath(namespace, path), undefined, false, memoryOnly),
|
|
36
|
-
rename: (path, oldKey, newKey
|
|
36
|
+
rename: (path, oldKey, newKey) => rename(joinPath(namespace, path), oldKey, newKey),
|
|
37
37
|
subscribe: (path, listener) =>
|
|
38
38
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
39
39
|
useSubscribe(joinPath(namespace, path), listener),
|
|
40
|
-
useCompute: (path, fn) => {
|
|
40
|
+
useCompute: (path, fn, deps) => {
|
|
41
41
|
const fullPath = joinPath(namespace, path);
|
|
42
42
|
const fnRef = useRef(fn);
|
|
43
43
|
fnRef.current = fn;
|
|
44
|
-
// Cache to avoid infinite loops - only recompute when store value changes
|
|
45
44
|
const cacheRef = useRef(null);
|
|
46
45
|
const subscribeToPath = useCallback((onStoreChange) => subscribe(fullPath, onStoreChange), [fullPath]);
|
|
47
46
|
const getComputedSnapshot = useCallback(() => {
|
|
47
|
+
if (cacheRef.current && cacheRef.current.path !== fullPath) {
|
|
48
|
+
cacheRef.current = null;
|
|
49
|
+
}
|
|
50
|
+
if (cacheRef.current && !isEqual(cacheRef.current.deps, deps)) {
|
|
51
|
+
cacheRef.current = null;
|
|
52
|
+
}
|
|
48
53
|
const storeValue = getSnapshot(fullPath);
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
if (cacheRef.current && isEqual(cacheRef.current.storeValue, storeValue)) {
|
|
55
|
+
// same store value, return the same computed value
|
|
56
|
+
return cacheRef.current.computed;
|
|
57
|
+
}
|
|
58
|
+
const computedNext = fnRef.current(storeValue);
|
|
59
|
+
// Important: even if storeValue changed, we should avoid forcing a re-render
|
|
60
|
+
// when the computed result is logically unchanged. `useSyncExternalStore`
|
|
61
|
+
// uses `Object.is` on the snapshot; returning the same reference will bail out.
|
|
62
|
+
if (cacheRef.current && isEqual(cacheRef.current.computed, computedNext)) {
|
|
63
|
+
cacheRef.current.storeValue = storeValue;
|
|
51
64
|
return cacheRef.current.computed;
|
|
52
65
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return computed;
|
|
57
|
-
}, [fullPath]);
|
|
66
|
+
cacheRef.current = { path: fullPath, storeValue, computed: computedNext, deps };
|
|
67
|
+
return computedNext;
|
|
68
|
+
}, [fullPath, deps]);
|
|
58
69
|
return useSyncExternalStore(subscribeToPath, getComputedSnapshot, getComputedSnapshot);
|
|
59
70
|
},
|
|
60
71
|
notify: (path) => {
|
|
@@ -66,28 +77,28 @@ function createStoreRoot(namespace, defaultValue, options = {}) {
|
|
|
66
77
|
});
|
|
67
78
|
},
|
|
68
79
|
useState: (path) => {
|
|
69
|
-
const
|
|
80
|
+
const fullPath = joinPath(namespace, path);
|
|
70
81
|
const setValue = useCallback((value) => {
|
|
71
82
|
if (typeof value === 'function') {
|
|
72
|
-
const currentValue = getSnapshot(
|
|
83
|
+
const currentValue = getSnapshot(fullPath);
|
|
73
84
|
const newValue = value(currentValue);
|
|
74
85
|
return setLeaf(namespace, path, newValue, false, memoryOnly);
|
|
75
86
|
}
|
|
76
87
|
return setLeaf(namespace, path, value, false, memoryOnly);
|
|
77
|
-
}, [path]);
|
|
78
|
-
return [useObject(
|
|
88
|
+
}, [fullPath, path]);
|
|
89
|
+
return [useObject(namespace, path), setValue];
|
|
79
90
|
},
|
|
80
91
|
Render: ({ path, children }) => {
|
|
81
|
-
const
|
|
82
|
-
const value = useObject(
|
|
92
|
+
const fullPath = joinPath(namespace, path);
|
|
93
|
+
const value = useObject(namespace, path);
|
|
83
94
|
const update = useCallback((value) => {
|
|
84
95
|
if (typeof value === 'function') {
|
|
85
|
-
const currentValue = getSnapshot(
|
|
96
|
+
const currentValue = getSnapshot(fullPath);
|
|
86
97
|
const newValue = value(currentValue);
|
|
87
98
|
return setLeaf(namespace, path, newValue, false, memoryOnly);
|
|
88
99
|
}
|
|
89
100
|
return setLeaf(namespace, path, value, false, memoryOnly);
|
|
90
|
-
}, [path]);
|
|
101
|
+
}, [fullPath, path]);
|
|
91
102
|
return children(value, update);
|
|
92
103
|
},
|
|
93
104
|
Show: ({ path, children, on }) => {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FieldPath, FieldPathValue, FieldValues, IsEqual } from './path';
|
|
2
|
-
export type { AllowedKeys, ArrayProxy, ArrayState, DerivedStateProps, IsNullable, MaybeNullable, ObjectMutationMethods, ObjectState, Prettify, State, StoreRenderProps, StoreRoot, StoreSetStateAction, StoreShowProps, StoreUse, ValueState };
|
|
2
|
+
export type { AllowedKeys, ArrayProxy, ArrayState, DerivedStateProps, IsNullable, MaybeNullable, ObjectMutationMethods, ObjectState, Prettify, ReadOnlyState, State, StoreRenderProps, StoreRoot, StoreSetStateAction, StoreShowProps, StoreUse, ValueState };
|
|
3
3
|
type Prettify<T> = {
|
|
4
4
|
[K in keyof T]: T[K];
|
|
5
5
|
} & {};
|
|
@@ -23,9 +23,18 @@ type ArrayProxy<T, ElementState = State<T>> = ArrayMutationMethods<T> & {
|
|
|
23
23
|
/** Insert items into the array in sorted order using the provided comparison function. */
|
|
24
24
|
sortedInsert(cmp: (a: T, b: T) => number, ...items: T[]): number;
|
|
25
25
|
};
|
|
26
|
+
type ObjectProxy<T extends FieldValues> = {
|
|
27
|
+
/** Virtual state for the object's keys.
|
|
28
|
+
*
|
|
29
|
+
* This does NOT read from a real `keys` property on the stored object; it results in a stable array of keys.
|
|
30
|
+
*/
|
|
31
|
+
readonly keys: ReadOnlyState<FieldPath<T>[]>;
|
|
32
|
+
} & {
|
|
33
|
+
[K in keyof T]-?: State<T[K]>;
|
|
34
|
+
};
|
|
26
35
|
type ObjectMutationMethods = {
|
|
27
36
|
/** Rename a key in an object. */
|
|
28
|
-
rename: (oldKey: string, newKey: string
|
|
37
|
+
rename: (oldKey: string, newKey: string) => void;
|
|
29
38
|
};
|
|
30
39
|
/** Tuple returned by Store.use(path). */
|
|
31
40
|
type StoreUse<T> = Readonly<[T | undefined, (value: T | undefined) => void]>;
|
|
@@ -47,11 +56,11 @@ type StoreRoot<T extends FieldValues> = {
|
|
|
47
56
|
/** Delete value at path (for arrays, removes index; for objects, deletes key). */
|
|
48
57
|
reset: <P extends FieldPath<T>>(path: P) => void;
|
|
49
58
|
/** Rename a key in an object. */
|
|
50
|
-
rename: <P extends FieldPath<T>>(path: P, oldKey: string, newKey: string
|
|
59
|
+
rename: <P extends FieldPath<T>>(path: P, oldKey: string, newKey: string) => void;
|
|
51
60
|
/** Subscribe to changes at path and invoke listener with the new value. */
|
|
52
61
|
subscribe: <P extends FieldPath<T>>(path: P, listener: (value: FieldPathValue<T, P>) => void) => void;
|
|
53
62
|
/** Compute a derived value from the current value, similar to useState + useMemo */
|
|
54
|
-
useCompute: <P extends FieldPath<T>, R>(path: P, fn: (value: FieldPathValue<T, P>) => R) => R;
|
|
63
|
+
useCompute: <P extends FieldPath<T>, R>(path: P, fn: (value: FieldPathValue<T, P>) => R, deps?: readonly unknown[]) => R;
|
|
55
64
|
/** Notify listeners at path. */
|
|
56
65
|
notify: <P extends FieldPath<T>>(path: P) => void;
|
|
57
66
|
/** Render-prop helper for inline usage. */
|
|
@@ -78,7 +87,7 @@ type ValueState<T> = {
|
|
|
78
87
|
/** Subscribe to changes at path and invoke listener with the new value. */
|
|
79
88
|
subscribe(listener: (value: T) => void): void;
|
|
80
89
|
/** Compute a derived value from the current value, similar to useState + useMemo */
|
|
81
|
-
useCompute: <R>(fn: (value: T) => R) => R;
|
|
90
|
+
useCompute: <R>(fn: (value: T) => R, deps?: readonly unknown[]) => R;
|
|
82
91
|
/** Ensure the value is an array. */
|
|
83
92
|
ensureArray(): NonNullable<T> extends (infer U)[] ? ArrayState<U> : never;
|
|
84
93
|
/** Ensure the value is an object. */
|
|
@@ -125,13 +134,15 @@ type ValueState<T> = {
|
|
|
125
134
|
on: (value: T) => boolean;
|
|
126
135
|
}) => React.ReactNode;
|
|
127
136
|
};
|
|
137
|
+
/**
|
|
138
|
+
* A read-only state that provides access to the value, use, Render, and Show methods.
|
|
139
|
+
*/
|
|
140
|
+
type ReadOnlyState<T> = Prettify<Pick<ValueState<Readonly<Required<T>>>, 'value' | 'use' | 'useCompute' | 'Render' | 'Show'>>;
|
|
128
141
|
type MaybeNullable<T, Nullable extends boolean = false> = Nullable extends true ? T | undefined : T;
|
|
129
142
|
type IsNullable<T> = T extends undefined | null ? true : false;
|
|
130
143
|
type State<T> = IsEqual<T, unknown> extends true ? never : [NonNullable<T>] extends [readonly (infer U)[]] ? ArrayState<U, IsNullable<T>> : [NonNullable<T>] extends [FieldValues] ? ObjectState<NonNullable<T>, IsNullable<T>> : ValueState<T>;
|
|
131
144
|
type ArrayState<T, Nullable extends boolean = false> = IsEqual<T, unknown> extends true ? never : ValueState<MaybeNullable<T[], Nullable>> & ArrayProxy<T>;
|
|
132
|
-
type ObjectState<T extends FieldValues, Nullable extends boolean = false> =
|
|
133
|
-
[K in keyof T]-?: State<T[K]>;
|
|
134
|
-
} & ValueState<MaybeNullable<T, Nullable>> & ObjectMutationMethods;
|
|
145
|
+
type ObjectState<T extends FieldValues, Nullable extends boolean = false> = ObjectProxy<T> & ValueState<MaybeNullable<T, Nullable>> & ObjectMutationMethods;
|
|
135
146
|
/** Props for Store.Render helper. */
|
|
136
147
|
type StoreRenderProps<T extends FieldValues, P extends FieldPath<T>> = {
|
|
137
148
|
path: P;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "juststore",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "A small, expressive, and type-safe state management library for React.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,19 +52,19 @@
|
|
|
52
52
|
"react-fast-compare": "^3.2.2"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@eslint/js": "^9.39.
|
|
56
|
-
"@types/node": "^24.10.
|
|
57
|
-
"@types/react": "^19.2.
|
|
58
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
59
|
-
"@typescript-eslint/parser": "^8.
|
|
60
|
-
"eslint": "^9.39.
|
|
55
|
+
"@eslint/js": "^9.39.2",
|
|
56
|
+
"@types/node": "^24.10.4",
|
|
57
|
+
"@types/react": "^19.2.7",
|
|
58
|
+
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
59
|
+
"@typescript-eslint/parser": "^8.50.1",
|
|
60
|
+
"eslint": "^9.39.2",
|
|
61
61
|
"eslint-plugin-prettier": "^5.5.4",
|
|
62
62
|
"eslint-plugin-react": "^7.37.5",
|
|
63
63
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
64
|
-
"eslint-plugin-react-refresh": "^0.4.
|
|
64
|
+
"eslint-plugin-react-refresh": "^0.4.26",
|
|
65
65
|
"husky": "^9.1.7",
|
|
66
66
|
"prettier": "^3.7.4",
|
|
67
|
-
"react": "^19.2.
|
|
67
|
+
"react": "^19.2.3",
|
|
68
68
|
"typescript": "^5.9.3"
|
|
69
69
|
}
|
|
70
70
|
}
|