floppy-disk 3.0.0-beta.1 → 3.0.0-beta.3
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 +371 -1
- package/esm/react/create-mutation.d.mts +37 -21
- package/esm/react/create-query.d.mts +60 -57
- package/esm/react/create-store.d.mts +7 -4
- package/esm/react/create-stores.d.mts +11 -4
- package/esm/react/use-mutation.d.mts +82 -0
- package/esm/react/use-store.d.mts +18 -8
- package/esm/react.d.mts +3 -2
- package/esm/react.mjs +237 -52
- package/esm/vanilla/basic.d.mts +0 -8
- package/esm/vanilla/store.d.mts +17 -8
- package/esm/vanilla.d.mts +0 -1
- package/esm/vanilla.mjs +6 -40
- package/package.json +2 -2
- package/react/create-mutation.d.ts +37 -21
- package/react/create-query.d.ts +60 -57
- package/react/create-store.d.ts +7 -4
- package/react/create-stores.d.ts +11 -4
- package/react/use-mutation.d.ts +82 -0
- package/react/use-store.d.ts +18 -8
- package/react.d.ts +3 -2
- package/react.js +235 -50
- package/vanilla/basic.d.ts +0 -8
- package/vanilla/store.d.ts +17 -8
- package/vanilla.d.ts +0 -1
- package/vanilla.js +5 -41
- package/esm/vanilla/shallow.d.mts +0 -6
- package/vanilla/shallow.d.ts +0 -6
package/react.js
CHANGED
|
@@ -5,28 +5,73 @@ var vanilla = require('floppy-disk/vanilla');
|
|
|
5
5
|
|
|
6
6
|
const useIsomorphicLayoutEffect = vanilla.isClient ? react.useLayoutEffect : react.useEffect;
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
const getValueByPath = (obj, path) => path.reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
|
|
9
|
+
const isPrefixPath = (candidatePrefix, targetPath) => {
|
|
10
|
+
if (candidatePrefix.length >= targetPath.length) return false;
|
|
11
|
+
for (let i = 0; i < candidatePrefix.length; i++) {
|
|
12
|
+
if (candidatePrefix[i] !== targetPath[i]) return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
const compressPaths = (paths) => {
|
|
17
|
+
const result = [];
|
|
18
|
+
let prev = null;
|
|
19
|
+
for (let i = paths.length - 1; i >= 0; i--) {
|
|
20
|
+
const current = paths[i];
|
|
21
|
+
if (!prev || !isPrefixPath(current, prev)) result.push(current);
|
|
22
|
+
prev = current;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
21
25
|
};
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
const useStoreStateProxy = (storeState) => {
|
|
27
|
+
const usedPathsRef = react.useRef([]);
|
|
28
|
+
usedPathsRef.current = [];
|
|
29
|
+
const trackedState = react.useMemo(() => {
|
|
30
|
+
const track = (path) => usedPathsRef.current.push(path);
|
|
31
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
32
|
+
const createDeepProxy = (target, path = []) => {
|
|
33
|
+
if (typeof target !== "object" || target === null) {
|
|
34
|
+
return target;
|
|
35
|
+
}
|
|
36
|
+
if (proxyCache.has(target)) {
|
|
37
|
+
return proxyCache.get(target);
|
|
38
|
+
}
|
|
39
|
+
const proxy = new Proxy(target, {
|
|
40
|
+
get(obj, key) {
|
|
41
|
+
const newPath = [...path, key];
|
|
42
|
+
track(newPath);
|
|
43
|
+
const value = obj[key];
|
|
44
|
+
return createDeepProxy(value, newPath);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
proxyCache.set(target, proxy);
|
|
48
|
+
return proxy;
|
|
49
|
+
};
|
|
50
|
+
return createDeepProxy(storeState);
|
|
51
|
+
}, [storeState]);
|
|
52
|
+
return [trackedState, usedPathsRef];
|
|
53
|
+
};
|
|
54
|
+
const useStoreState = (storeState, subscribe) => {
|
|
55
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(storeState);
|
|
56
|
+
const [, reRender] = react.useState({});
|
|
57
|
+
useIsomorphicLayoutEffect(() => {
|
|
58
|
+
return subscribe((nextState, prevState, changedKeys) => {
|
|
59
|
+
const paths = compressPaths(usedPathsRef.current);
|
|
60
|
+
for (const path of paths) {
|
|
61
|
+
const rootKey = path[0];
|
|
62
|
+
if (!changedKeys.includes(rootKey)) continue;
|
|
63
|
+
const prevVal = getValueByPath(prevState, path);
|
|
64
|
+
const nextVal = getValueByPath(nextState, path);
|
|
65
|
+
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}, [subscribe]);
|
|
69
|
+
return trackedState;
|
|
25
70
|
};
|
|
26
71
|
|
|
27
72
|
const createStore = (initialState, options) => {
|
|
28
73
|
const store = vanilla.initStore(initialState, options);
|
|
29
|
-
const useStore = (
|
|
74
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
30
75
|
return Object.assign(useStore, store);
|
|
31
76
|
};
|
|
32
77
|
|
|
@@ -41,9 +86,7 @@ const createStores = (initialState, options) => {
|
|
|
41
86
|
store = vanilla.initStore(initialState, options);
|
|
42
87
|
stores.set(keyHash, store);
|
|
43
88
|
}
|
|
44
|
-
const useStore = (
|
|
45
|
-
return useStoreState(store, selector);
|
|
46
|
-
};
|
|
89
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
47
90
|
return Object.assign(useStore, {
|
|
48
91
|
...store,
|
|
49
92
|
delete: () => {
|
|
@@ -87,7 +130,7 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
87
130
|
onSettled = vanilla.noop,
|
|
88
131
|
shouldRetry: shouldRetryFn = (_, s) => s.retryCount === 0 ? [true, 1500] : [false]
|
|
89
132
|
} = options;
|
|
90
|
-
const initialState = INITIAL_STATE$1;
|
|
133
|
+
const initialState = { ...INITIAL_STATE$1 };
|
|
91
134
|
const stores = /* @__PURE__ */ new Map();
|
|
92
135
|
const configureStoreEvents = () => ({
|
|
93
136
|
...options,
|
|
@@ -325,30 +368,51 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
325
368
|
stores.set(variableHash, store);
|
|
326
369
|
internals.set(store, configureInternals(store, variable, variableHash));
|
|
327
370
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
let options2;
|
|
331
|
-
if (typeof optionsOrSelector === "function") {
|
|
332
|
-
options2 = {};
|
|
333
|
-
selector = optionsOrSelector;
|
|
334
|
-
} else {
|
|
335
|
-
options2 = optionsOrSelector;
|
|
336
|
-
selector = maybeSelector || vanilla.identity;
|
|
337
|
-
}
|
|
338
|
-
useStoreUpdateNotifier(store, selector);
|
|
339
|
-
useIsomorphicLayoutEffect(() => {
|
|
340
|
-
if (options2.enabled !== false) revalidate(store, variable, false);
|
|
341
|
-
}, [store, options2.enabled]);
|
|
371
|
+
const useStore = (options2 = {}) => {
|
|
372
|
+
const { revalidateOnMount = true, keepPreviousData } = options2;
|
|
342
373
|
const storeState = store.getState();
|
|
343
|
-
let storeStateToBeUsed = storeState;
|
|
344
374
|
const prevState = react.useRef({});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
375
|
+
let storeStateToBeUsed = storeState;
|
|
376
|
+
if (storeState.state !== "INITIAL") {
|
|
377
|
+
prevState.current = {
|
|
378
|
+
data: storeState.data,
|
|
379
|
+
dataUpdatedAt: storeState.dataUpdatedAt
|
|
380
|
+
};
|
|
381
|
+
} else if (keepPreviousData) {
|
|
348
382
|
storeStateToBeUsed = { ...storeState, ...prevState.current };
|
|
349
383
|
}
|
|
350
|
-
|
|
351
|
-
|
|
384
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(
|
|
385
|
+
revalidateOnMount && storeState.state === "INITIAL" ? (
|
|
386
|
+
// Optimize rendering on initial state
|
|
387
|
+
// Do { isPending: true } → result
|
|
388
|
+
// instead of { isPending: false } → { isPending: true } → result
|
|
389
|
+
{ ...storeStateToBeUsed, isPending: true }
|
|
390
|
+
) : storeStateToBeUsed
|
|
391
|
+
);
|
|
392
|
+
const [, reRender] = react.useState({});
|
|
393
|
+
useIsomorphicLayoutEffect(() => {
|
|
394
|
+
return store.subscribe((nextState, prevState2, changedKeys) => {
|
|
395
|
+
if (prevState2.state === "INITIAL" && !prevState2.isPending && nextState.isPending) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const paths = compressPaths(usedPathsRef.current);
|
|
399
|
+
for (const path of paths) {
|
|
400
|
+
const rootKey = path[0];
|
|
401
|
+
if (!changedKeys.includes(rootKey)) continue;
|
|
402
|
+
const prevVal = getValueByPath(prevState2, path);
|
|
403
|
+
const nextVal = getValueByPath(nextState, path);
|
|
404
|
+
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}, [store]);
|
|
408
|
+
useIsomorphicLayoutEffect(() => {
|
|
409
|
+
if (revalidateOnMount !== false) revalidate(store, variable, false);
|
|
410
|
+
}, [store, revalidateOnMount]);
|
|
411
|
+
if (keepPreviousData) {
|
|
412
|
+
!!trackedState.error;
|
|
413
|
+
}
|
|
414
|
+
return trackedState;
|
|
415
|
+
};
|
|
352
416
|
return Object.assign(useStore, {
|
|
353
417
|
subscribe: store.subscribe,
|
|
354
418
|
getSubscribers: store.getSubscribers,
|
|
@@ -417,19 +481,26 @@ const INITIAL_STATE = {
|
|
|
417
481
|
};
|
|
418
482
|
const createMutation = (mutationFn, options = {}) => {
|
|
419
483
|
const { onSuccess = vanilla.noop, onError, onSettled = vanilla.noop } = options;
|
|
420
|
-
const initialState = INITIAL_STATE;
|
|
484
|
+
const initialState = { ...INITIAL_STATE };
|
|
485
|
+
let ongoingPromise;
|
|
486
|
+
const resolveFns = /* @__PURE__ */ new Set([]);
|
|
421
487
|
const store = vanilla.initStore(initialState, options);
|
|
422
|
-
const useStore = (
|
|
488
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
423
489
|
const execute = (variable) => {
|
|
490
|
+
let currentResolveFn;
|
|
424
491
|
const stateBeforeExecute = store.getState();
|
|
425
492
|
if (stateBeforeExecute.isPending) {
|
|
426
493
|
console.warn(
|
|
427
|
-
"
|
|
494
|
+
"A mutation was executed while a previous execution is still pending. The previous execution will be ignored (latest execution wins)."
|
|
428
495
|
);
|
|
429
496
|
}
|
|
430
497
|
store.setState({ isPending: true });
|
|
431
|
-
|
|
498
|
+
const promise = new Promise((resolve) => {
|
|
499
|
+
currentResolveFn = resolve;
|
|
432
500
|
mutationFn(variable, stateBeforeExecute).then((data) => {
|
|
501
|
+
if (promise !== ongoingPromise) {
|
|
502
|
+
return resolve({ data, variable });
|
|
503
|
+
}
|
|
433
504
|
store.setState({
|
|
434
505
|
state: "SUCCESS",
|
|
435
506
|
isPending: false,
|
|
@@ -442,8 +513,12 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
442
513
|
errorUpdatedAt: void 0
|
|
443
514
|
});
|
|
444
515
|
resolve({ data, variable });
|
|
516
|
+
resolveFns.clear();
|
|
445
517
|
onSuccess(data, variable, stateBeforeExecute);
|
|
446
518
|
}).catch((error) => {
|
|
519
|
+
if (promise !== ongoingPromise) {
|
|
520
|
+
return resolve({ error, variable });
|
|
521
|
+
}
|
|
447
522
|
store.setState({
|
|
448
523
|
state: "ERROR",
|
|
449
524
|
isPending: false,
|
|
@@ -456,12 +531,19 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
456
531
|
errorUpdatedAt: Date.now()
|
|
457
532
|
});
|
|
458
533
|
resolve({ error, variable });
|
|
534
|
+
resolveFns.clear();
|
|
459
535
|
if (onError) onError(error, variable, stateBeforeExecute);
|
|
460
536
|
else console.error(store.getState());
|
|
461
537
|
}).finally(() => {
|
|
538
|
+
if (promise !== ongoingPromise) return;
|
|
462
539
|
onSettled(variable, stateBeforeExecute);
|
|
540
|
+
ongoingPromise = void 0;
|
|
463
541
|
});
|
|
464
542
|
});
|
|
543
|
+
if (ongoingPromise) resolveFns.forEach((resolveFn) => resolveFn(promise));
|
|
544
|
+
resolveFns.add(currentResolveFn);
|
|
545
|
+
ongoingPromise = promise;
|
|
546
|
+
return promise;
|
|
465
547
|
};
|
|
466
548
|
return Object.assign(useStore, {
|
|
467
549
|
subscribe: store.subscribe,
|
|
@@ -488,17 +570,17 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
488
570
|
* - `{ error, variable }` on failure
|
|
489
571
|
*
|
|
490
572
|
* @remarks
|
|
491
|
-
* - If a mutation is already in progress, a warning is logged.
|
|
492
|
-
* - Concurrent executions are allowed but may lead to race conditions.
|
|
493
573
|
* - The promise never rejects to simplify async handling.
|
|
574
|
+
* - If a mutation is already in progress, a warning is logged.
|
|
575
|
+
* - When a new execution starts, all previous pending executions will resolve with the result of the latest execution.
|
|
494
576
|
*/
|
|
495
577
|
execute,
|
|
496
578
|
/**
|
|
497
579
|
* Resets the mutation state back to its initial state.
|
|
498
580
|
*
|
|
499
581
|
* @remarks
|
|
500
|
-
* - Does not cancel any ongoing
|
|
501
|
-
* - If
|
|
582
|
+
* - Does not cancel any ongoing execution.
|
|
583
|
+
* - If an execution is still pending, its result may override the reset state.
|
|
502
584
|
*/
|
|
503
585
|
reset: () => {
|
|
504
586
|
if (store.getState().isPending) {
|
|
@@ -511,10 +593,113 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
511
593
|
});
|
|
512
594
|
};
|
|
513
595
|
|
|
596
|
+
const useMutation = (mutationFn, options = {}) => {
|
|
597
|
+
const { onSuccess = vanilla.noop, onError, onSettled = vanilla.noop } = options;
|
|
598
|
+
const callbackRef = react.useRef({ onSuccess, onError, onSettled });
|
|
599
|
+
callbackRef.current.onSuccess = onSuccess;
|
|
600
|
+
callbackRef.current.onError = onError;
|
|
601
|
+
callbackRef.current.onSettled = onSettled;
|
|
602
|
+
const stateRef = react.useRef({ ...INITIAL_STATE });
|
|
603
|
+
const [, reRender] = react.useState({});
|
|
604
|
+
const refs = react.useRef({
|
|
605
|
+
mutationFn,
|
|
606
|
+
ongoingPromise: void 0,
|
|
607
|
+
resolveFns: /* @__PURE__ */ new Set()
|
|
608
|
+
});
|
|
609
|
+
refs.current.mutationFn = mutationFn;
|
|
610
|
+
const execute = react.useCallback((variable) => {
|
|
611
|
+
let currentResolveFn;
|
|
612
|
+
const stateBeforeExecute = stateRef.current;
|
|
613
|
+
if (stateBeforeExecute.isPending) {
|
|
614
|
+
console.warn(
|
|
615
|
+
"A mutation was executed while a previous execution is still pending. The previous execution will be ignored (latest execution wins)."
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
stateRef.current.isPending = true;
|
|
619
|
+
reRender({});
|
|
620
|
+
const promise = new Promise(
|
|
621
|
+
(resolve) => {
|
|
622
|
+
currentResolveFn = resolve;
|
|
623
|
+
refs.current.mutationFn(variable, stateBeforeExecute).then((data) => {
|
|
624
|
+
if (promise !== refs.current.ongoingPromise) {
|
|
625
|
+
return resolve({ data, variable });
|
|
626
|
+
}
|
|
627
|
+
stateRef.current = {
|
|
628
|
+
state: "SUCCESS",
|
|
629
|
+
isPending: false,
|
|
630
|
+
isSuccess: true,
|
|
631
|
+
isError: false,
|
|
632
|
+
variable,
|
|
633
|
+
data,
|
|
634
|
+
dataUpdatedAt: Date.now(),
|
|
635
|
+
error: void 0,
|
|
636
|
+
errorUpdatedAt: void 0
|
|
637
|
+
};
|
|
638
|
+
reRender({});
|
|
639
|
+
resolve({ data, variable });
|
|
640
|
+
refs.current.resolveFns.clear();
|
|
641
|
+
callbackRef.current.onSuccess(data, variable, stateBeforeExecute);
|
|
642
|
+
}).catch((error) => {
|
|
643
|
+
if (promise !== refs.current.ongoingPromise) {
|
|
644
|
+
return resolve({ error, variable });
|
|
645
|
+
}
|
|
646
|
+
stateRef.current = {
|
|
647
|
+
state: "ERROR",
|
|
648
|
+
isPending: false,
|
|
649
|
+
isSuccess: false,
|
|
650
|
+
isError: true,
|
|
651
|
+
variable,
|
|
652
|
+
data: void 0,
|
|
653
|
+
dataUpdatedAt: void 0,
|
|
654
|
+
error,
|
|
655
|
+
errorUpdatedAt: Date.now()
|
|
656
|
+
};
|
|
657
|
+
reRender({});
|
|
658
|
+
resolve({ error, variable });
|
|
659
|
+
refs.current.resolveFns.clear();
|
|
660
|
+
if (callbackRef.current.onError) {
|
|
661
|
+
callbackRef.current.onError(error, variable, stateBeforeExecute);
|
|
662
|
+
} else {
|
|
663
|
+
console.error(stateRef.current);
|
|
664
|
+
}
|
|
665
|
+
}).finally(() => {
|
|
666
|
+
if (promise !== refs.current.ongoingPromise) return;
|
|
667
|
+
callbackRef.current.onSettled(variable, stateBeforeExecute);
|
|
668
|
+
refs.current.ongoingPromise = void 0;
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
if (refs.current.ongoingPromise) {
|
|
673
|
+
refs.current.resolveFns.forEach((resolveFn) => resolveFn(promise));
|
|
674
|
+
}
|
|
675
|
+
refs.current.resolveFns.add(currentResolveFn);
|
|
676
|
+
refs.current.ongoingPromise = promise;
|
|
677
|
+
return promise;
|
|
678
|
+
}, []);
|
|
679
|
+
const reset = react.useCallback(() => {
|
|
680
|
+
if (stateRef.current.isPending) {
|
|
681
|
+
console.warn(
|
|
682
|
+
"Mutation state was reset while a request is still pending. The request will continue, but its result may override the reset state."
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
stateRef.current = { ...INITIAL_STATE };
|
|
686
|
+
reRender({});
|
|
687
|
+
}, []);
|
|
688
|
+
const r = [
|
|
689
|
+
stateRef.current,
|
|
690
|
+
{
|
|
691
|
+
execute,
|
|
692
|
+
reset,
|
|
693
|
+
getLatestState: () => stateRef.current
|
|
694
|
+
}
|
|
695
|
+
];
|
|
696
|
+
return r;
|
|
697
|
+
};
|
|
698
|
+
|
|
514
699
|
exports.createMutation = createMutation;
|
|
515
700
|
exports.createQuery = createQuery;
|
|
516
701
|
exports.createStore = createStore;
|
|
517
702
|
exports.createStores = createStores;
|
|
518
703
|
exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
|
|
704
|
+
exports.useMutation = useMutation;
|
|
519
705
|
exports.useStoreState = useStoreState;
|
|
520
|
-
exports.useStoreUpdateNotifier = useStoreUpdateNotifier;
|
package/vanilla/basic.d.ts
CHANGED
|
@@ -6,14 +6,6 @@ export declare const isClient: boolean;
|
|
|
6
6
|
* Empty function.
|
|
7
7
|
*/
|
|
8
8
|
export declare const noop: () => void;
|
|
9
|
-
/**
|
|
10
|
-
* Identity function.
|
|
11
|
-
*
|
|
12
|
-
* It accepts 1 argument, and simply return it.
|
|
13
|
-
*
|
|
14
|
-
* `const identity = value => value`
|
|
15
|
-
*/
|
|
16
|
-
export declare const identity: <T>(value: T) => T;
|
|
17
9
|
/**
|
|
18
10
|
* If the value is a function, it will invoke the function.\
|
|
19
11
|
* If the value is not a function, it will just return it.
|
package/vanilla/store.d.ts
CHANGED
|
@@ -11,18 +11,21 @@ export type SetState<TState> = Partial<TState> | ((state: TState) => Partial<TSt
|
|
|
11
11
|
*
|
|
12
12
|
* @param state - The latest state
|
|
13
13
|
* @param prevState - The previous state before the update
|
|
14
|
+
* @param changedKeys - The top-level keys that changed (shallow diff)
|
|
14
15
|
*
|
|
15
16
|
* @remarks
|
|
16
|
-
* - Subscribers are only called when
|
|
17
|
+
* - Subscribers are only called when at least one field changes.
|
|
17
18
|
* - Change detection is performed per key using `Object.is`.
|
|
19
|
+
* - `changedKeys` only includes top-level keys; nested changes must be inferred by the consumer.
|
|
18
20
|
*/
|
|
19
|
-
export type Subscriber<TState> = (state: TState, prevState: TState) => void;
|
|
21
|
+
export type Subscriber<TState> = (state: TState, prevState: TState, changedKeys: Array<keyof TState>) => void;
|
|
20
22
|
/**
|
|
21
23
|
* Core store API for managing state.
|
|
22
24
|
*
|
|
23
25
|
* @remarks
|
|
24
26
|
* - The store performs **shallow change detection per key** before notifying subscribers.
|
|
25
27
|
* - Subscribers are only notified when at least one field changes.
|
|
28
|
+
* - State is treated as **immutable**. Mutating nested values directly will not trigger updates.
|
|
26
29
|
* - Designed to be framework-agnostic (React bindings are built separately).
|
|
27
30
|
* - By default, `setState` is **disabled on the server** to prevent accidental shared state between requests.
|
|
28
31
|
*/
|
|
@@ -48,13 +51,17 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
48
51
|
onSubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
49
52
|
onUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
50
53
|
onLastUnsubscribe?: (state: TState, store: StoreApi<TState>) => void;
|
|
54
|
+
/**
|
|
55
|
+
* By default, calling `setState` on the server is disallowed to prevent shared state across requests.
|
|
56
|
+
* Set this to `true` only if you explicitly intend to mutate state during server execution.
|
|
57
|
+
*/
|
|
51
58
|
allowSetStateServerSide?: boolean;
|
|
52
59
|
};
|
|
53
60
|
/**
|
|
54
61
|
* Creates a vanilla store with pub-sub capabilities.
|
|
55
62
|
*
|
|
56
|
-
* The store state
|
|
57
|
-
* Updates are applied as
|
|
63
|
+
* The store state must be an **object**.\
|
|
64
|
+
* Updates are applied as shallow merges, so non-object states are not supported.
|
|
58
65
|
*
|
|
59
66
|
* @param initialState - The initial state of the store
|
|
60
67
|
* @param options - Optional lifecycle hooks
|
|
@@ -64,11 +71,13 @@ export type InitStoreOptions<TState extends Record<string, any>> = {
|
|
|
64
71
|
* @remarks
|
|
65
72
|
* - State updates are **shallowly compared per key** before notifying subscribers.
|
|
66
73
|
* - Subscribers are only notified when at least one updated field changes (using `Object.is` comparison).
|
|
67
|
-
* - Subscribers receive
|
|
74
|
+
* - Subscribers receive the new state, previous state, and changed top-level keys.
|
|
75
|
+
* - State is expected to be treated as **immutable**.
|
|
76
|
+
* - Mutating nested values directly will not trigger updates.
|
|
68
77
|
* - Lifecycle hooks allow side-effect management tied to subscription count.
|
|
69
|
-
* - By default, `setState` is **
|
|
70
|
-
* - This
|
|
71
|
-
* -
|
|
78
|
+
* - By default, `setState` is **not allowed on the server** to prevent accidental shared state between requests.
|
|
79
|
+
* - This helps avoid leaking data between users in server environments.
|
|
80
|
+
* - If you intentionally want to allow this behavior, set `allowSetStateServerSide: true`.
|
|
72
81
|
*
|
|
73
82
|
* @example
|
|
74
83
|
* const store = initStore({ count: 0 });
|
package/vanilla.d.ts
CHANGED
package/vanilla.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const isClient = typeof window !== "undefined" && !("Deno" in window);
|
|
4
4
|
const noop = () => {
|
|
5
5
|
};
|
|
6
|
-
const identity = (value) => value;
|
|
7
6
|
const getValue = (valueOrComputeValueFn, ...params) => {
|
|
8
7
|
if (typeof valueOrComputeValueFn === "function") {
|
|
9
8
|
return valueOrComputeValueFn(...params);
|
|
@@ -11,41 +10,6 @@ const getValue = (valueOrComputeValueFn, ...params) => {
|
|
|
11
10
|
return valueOrComputeValueFn;
|
|
12
11
|
};
|
|
13
12
|
|
|
14
|
-
const shallow = (a, b) => {
|
|
15
|
-
if (Object.is(a, b)) {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
if (a instanceof Map && b instanceof Map) {
|
|
22
|
-
if (a.size !== b.size) return false;
|
|
23
|
-
for (const [key, value] of a) {
|
|
24
|
-
if (!Object.is(value, b.get(key))) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
if (a instanceof Set && b instanceof Set) {
|
|
31
|
-
if (a.size !== b.size) return false;
|
|
32
|
-
for (const value of a) {
|
|
33
|
-
if (!b.has(value)) return false;
|
|
34
|
-
}
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
const keysA = Object.keys(a);
|
|
38
|
-
if (keysA.length !== Object.keys(b).length) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
for (let i = 0; i < keysA.length; i++) {
|
|
42
|
-
if (!Object.prototype.hasOwnProperty.call(b, keysA[i]) || !Object.is(a[keysA[i]], b[keysA[i]])) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
13
|
const hasObjectPrototype = (value) => {
|
|
50
14
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
51
15
|
};
|
|
@@ -98,13 +62,15 @@ const initStore = (initialState, options = {}) => {
|
|
|
98
62
|
}
|
|
99
63
|
const prevState = state;
|
|
100
64
|
const newValue = getValue(value, state);
|
|
65
|
+
const changedKeys = [];
|
|
101
66
|
for (const key in newValue) {
|
|
102
67
|
if (!Object.is(prevState[key], newValue[key])) {
|
|
103
|
-
|
|
104
|
-
[...subscribers].forEach((subscriber) => subscriber(state, prevState));
|
|
105
|
-
return;
|
|
68
|
+
changedKeys.push(key);
|
|
106
69
|
}
|
|
107
70
|
}
|
|
71
|
+
if (changedKeys.length === 0) return;
|
|
72
|
+
state = { ...prevState, ...newValue };
|
|
73
|
+
[...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
|
|
108
74
|
};
|
|
109
75
|
const storeApi = {
|
|
110
76
|
getState,
|
|
@@ -117,9 +83,7 @@ const initStore = (initialState, options = {}) => {
|
|
|
117
83
|
|
|
118
84
|
exports.getHash = getHash;
|
|
119
85
|
exports.getValue = getValue;
|
|
120
|
-
exports.identity = identity;
|
|
121
86
|
exports.initStore = initStore;
|
|
122
87
|
exports.isClient = isClient;
|
|
123
88
|
exports.isPlainObject = isPlainObject;
|
|
124
89
|
exports.noop = noop;
|
|
125
|
-
exports.shallow = shallow;
|