floppy-disk 3.0.0-alpha.4 → 3.0.0-alpha.6
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 +373 -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 +1 -1
- 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/esm/react.mjs
CHANGED
|
@@ -1,30 +1,75 @@
|
|
|
1
|
-
import { useLayoutEffect, useEffect, useState, useRef } from 'react';
|
|
2
|
-
import { isClient,
|
|
1
|
+
import { useLayoutEffect, useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
|
2
|
+
import { isClient, initStore, getHash, noop } from 'floppy-disk/vanilla';
|
|
3
3
|
|
|
4
4
|
const useIsomorphicLayoutEffect = isClient ? useLayoutEffect : useEffect;
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
const getValueByPath = (obj, path) => path.reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
|
|
7
|
+
const isPrefixPath = (candidatePrefix, targetPath) => {
|
|
8
|
+
if (candidatePrefix.length >= targetPath.length) return false;
|
|
9
|
+
for (let i = 0; i < candidatePrefix.length; i++) {
|
|
10
|
+
if (candidatePrefix[i] !== targetPath[i]) return false;
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
};
|
|
14
|
+
const compressPaths = (paths) => {
|
|
15
|
+
const result = [];
|
|
16
|
+
let prev = null;
|
|
17
|
+
for (let i = paths.length - 1; i >= 0; i--) {
|
|
18
|
+
const current = paths[i];
|
|
19
|
+
if (!prev || !isPrefixPath(current, prev)) result.push(current);
|
|
20
|
+
prev = current;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
19
23
|
};
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const useStoreStateProxy = (storeState) => {
|
|
25
|
+
const usedPathsRef = useRef([]);
|
|
26
|
+
usedPathsRef.current = [];
|
|
27
|
+
const trackedState = useMemo(() => {
|
|
28
|
+
const track = (path) => usedPathsRef.current.push(path);
|
|
29
|
+
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
const createDeepProxy = (target, path = []) => {
|
|
31
|
+
if (typeof target !== "object" || target === null) {
|
|
32
|
+
return target;
|
|
33
|
+
}
|
|
34
|
+
if (proxyCache.has(target)) {
|
|
35
|
+
return proxyCache.get(target);
|
|
36
|
+
}
|
|
37
|
+
const proxy = new Proxy(target, {
|
|
38
|
+
get(obj, key) {
|
|
39
|
+
const newPath = [...path, key];
|
|
40
|
+
track(newPath);
|
|
41
|
+
const value = obj[key];
|
|
42
|
+
return createDeepProxy(value, newPath);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
proxyCache.set(target, proxy);
|
|
46
|
+
return proxy;
|
|
47
|
+
};
|
|
48
|
+
return createDeepProxy(storeState);
|
|
49
|
+
}, [storeState]);
|
|
50
|
+
return [trackedState, usedPathsRef];
|
|
51
|
+
};
|
|
52
|
+
const useStoreState = (storeState, subscribe) => {
|
|
53
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(storeState);
|
|
54
|
+
const [, reRender] = useState({});
|
|
55
|
+
useIsomorphicLayoutEffect(() => {
|
|
56
|
+
return subscribe((nextState, prevState, changedKeys) => {
|
|
57
|
+
const paths = compressPaths(usedPathsRef.current);
|
|
58
|
+
for (const path of paths) {
|
|
59
|
+
const rootKey = path[0];
|
|
60
|
+
if (!changedKeys.includes(rootKey)) continue;
|
|
61
|
+
const prevVal = getValueByPath(prevState, path);
|
|
62
|
+
const nextVal = getValueByPath(nextState, path);
|
|
63
|
+
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}, [subscribe]);
|
|
67
|
+
return trackedState;
|
|
23
68
|
};
|
|
24
69
|
|
|
25
70
|
const createStore = (initialState, options) => {
|
|
26
71
|
const store = initStore(initialState, options);
|
|
27
|
-
const useStore = (
|
|
72
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
28
73
|
return Object.assign(useStore, store);
|
|
29
74
|
};
|
|
30
75
|
|
|
@@ -39,9 +84,7 @@ const createStores = (initialState, options) => {
|
|
|
39
84
|
store = initStore(initialState, options);
|
|
40
85
|
stores.set(keyHash, store);
|
|
41
86
|
}
|
|
42
|
-
const useStore = (
|
|
43
|
-
return useStoreState(store, selector);
|
|
44
|
-
};
|
|
87
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
45
88
|
return Object.assign(useStore, {
|
|
46
89
|
...store,
|
|
47
90
|
delete: () => {
|
|
@@ -85,7 +128,7 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
85
128
|
onSettled = noop,
|
|
86
129
|
shouldRetry: shouldRetryFn = (_, s) => s.retryCount === 0 ? [true, 1500] : [false]
|
|
87
130
|
} = options;
|
|
88
|
-
const initialState = INITIAL_STATE$1;
|
|
131
|
+
const initialState = { ...INITIAL_STATE$1 };
|
|
89
132
|
const stores = /* @__PURE__ */ new Map();
|
|
90
133
|
const configureStoreEvents = () => ({
|
|
91
134
|
...options,
|
|
@@ -323,30 +366,51 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
323
366
|
stores.set(variableHash, store);
|
|
324
367
|
internals.set(store, configureInternals(store, variable, variableHash));
|
|
325
368
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
let options2;
|
|
329
|
-
if (typeof optionsOrSelector === "function") {
|
|
330
|
-
options2 = {};
|
|
331
|
-
selector = optionsOrSelector;
|
|
332
|
-
} else {
|
|
333
|
-
options2 = optionsOrSelector;
|
|
334
|
-
selector = maybeSelector || identity;
|
|
335
|
-
}
|
|
336
|
-
useStoreUpdateNotifier(store, selector);
|
|
337
|
-
useIsomorphicLayoutEffect(() => {
|
|
338
|
-
if (options2.enabled !== false) revalidate(store, variable, false);
|
|
339
|
-
}, [store, options2.enabled]);
|
|
369
|
+
const useStore = (options2 = {}) => {
|
|
370
|
+
const { revalidateOnMount = true, keepPreviousData } = options2;
|
|
340
371
|
const storeState = store.getState();
|
|
341
|
-
let storeStateToBeUsed = storeState;
|
|
342
372
|
const prevState = useRef({});
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
373
|
+
let storeStateToBeUsed = storeState;
|
|
374
|
+
if (storeState.state !== "INITIAL") {
|
|
375
|
+
prevState.current = {
|
|
376
|
+
data: storeState.data,
|
|
377
|
+
dataUpdatedAt: storeState.dataUpdatedAt
|
|
378
|
+
};
|
|
379
|
+
} else if (keepPreviousData) {
|
|
346
380
|
storeStateToBeUsed = { ...storeState, ...prevState.current };
|
|
347
381
|
}
|
|
348
|
-
|
|
349
|
-
|
|
382
|
+
const [trackedState, usedPathsRef] = useStoreStateProxy(
|
|
383
|
+
revalidateOnMount && storeState.state === "INITIAL" ? (
|
|
384
|
+
// Optimize rendering on initial state
|
|
385
|
+
// Do { isPending: true } → result
|
|
386
|
+
// instead of { isPending: false } → { isPending: true } → result
|
|
387
|
+
{ ...storeStateToBeUsed, isPending: true }
|
|
388
|
+
) : storeStateToBeUsed
|
|
389
|
+
);
|
|
390
|
+
const [, reRender] = useState({});
|
|
391
|
+
useIsomorphicLayoutEffect(() => {
|
|
392
|
+
return store.subscribe((nextState, prevState2, changedKeys) => {
|
|
393
|
+
if (prevState2.state === "INITIAL" && !prevState2.isPending && nextState.isPending) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const paths = compressPaths(usedPathsRef.current);
|
|
397
|
+
for (const path of paths) {
|
|
398
|
+
const rootKey = path[0];
|
|
399
|
+
if (!changedKeys.includes(rootKey)) continue;
|
|
400
|
+
const prevVal = getValueByPath(prevState2, path);
|
|
401
|
+
const nextVal = getValueByPath(nextState, path);
|
|
402
|
+
if (!Object.is(prevVal, nextVal)) return reRender({});
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}, [store]);
|
|
406
|
+
useIsomorphicLayoutEffect(() => {
|
|
407
|
+
if (revalidateOnMount !== false) revalidate(store, variable, false);
|
|
408
|
+
}, [store, revalidateOnMount]);
|
|
409
|
+
if (keepPreviousData) {
|
|
410
|
+
!!trackedState.error;
|
|
411
|
+
}
|
|
412
|
+
return trackedState;
|
|
413
|
+
};
|
|
350
414
|
return Object.assign(useStore, {
|
|
351
415
|
subscribe: store.subscribe,
|
|
352
416
|
getSubscribers: store.getSubscribers,
|
|
@@ -415,19 +479,26 @@ const INITIAL_STATE = {
|
|
|
415
479
|
};
|
|
416
480
|
const createMutation = (mutationFn, options = {}) => {
|
|
417
481
|
const { onSuccess = noop, onError, onSettled = noop } = options;
|
|
418
|
-
const initialState = INITIAL_STATE;
|
|
482
|
+
const initialState = { ...INITIAL_STATE };
|
|
483
|
+
let ongoingPromise;
|
|
484
|
+
const resolveFns = /* @__PURE__ */ new Set([]);
|
|
419
485
|
const store = initStore(initialState, options);
|
|
420
|
-
const useStore = (
|
|
486
|
+
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
421
487
|
const execute = (variable) => {
|
|
488
|
+
let currentResolveFn;
|
|
422
489
|
const stateBeforeExecute = store.getState();
|
|
423
490
|
if (stateBeforeExecute.isPending) {
|
|
424
491
|
console.warn(
|
|
425
|
-
"
|
|
492
|
+
"A mutation was executed while a previous execution is still pending. The previous execution will be ignored (latest execution wins)."
|
|
426
493
|
);
|
|
427
494
|
}
|
|
428
495
|
store.setState({ isPending: true });
|
|
429
|
-
|
|
496
|
+
const promise = new Promise((resolve) => {
|
|
497
|
+
currentResolveFn = resolve;
|
|
430
498
|
mutationFn(variable, stateBeforeExecute).then((data) => {
|
|
499
|
+
if (promise !== ongoingPromise) {
|
|
500
|
+
return resolve({ data, variable });
|
|
501
|
+
}
|
|
431
502
|
store.setState({
|
|
432
503
|
state: "SUCCESS",
|
|
433
504
|
isPending: false,
|
|
@@ -440,8 +511,12 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
440
511
|
errorUpdatedAt: void 0
|
|
441
512
|
});
|
|
442
513
|
resolve({ data, variable });
|
|
514
|
+
resolveFns.clear();
|
|
443
515
|
onSuccess(data, variable, stateBeforeExecute);
|
|
444
516
|
}).catch((error) => {
|
|
517
|
+
if (promise !== ongoingPromise) {
|
|
518
|
+
return resolve({ error, variable });
|
|
519
|
+
}
|
|
445
520
|
store.setState({
|
|
446
521
|
state: "ERROR",
|
|
447
522
|
isPending: false,
|
|
@@ -454,12 +529,19 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
454
529
|
errorUpdatedAt: Date.now()
|
|
455
530
|
});
|
|
456
531
|
resolve({ error, variable });
|
|
532
|
+
resolveFns.clear();
|
|
457
533
|
if (onError) onError(error, variable, stateBeforeExecute);
|
|
458
534
|
else console.error(store.getState());
|
|
459
535
|
}).finally(() => {
|
|
536
|
+
if (promise !== ongoingPromise) return;
|
|
460
537
|
onSettled(variable, stateBeforeExecute);
|
|
538
|
+
ongoingPromise = void 0;
|
|
461
539
|
});
|
|
462
540
|
});
|
|
541
|
+
if (ongoingPromise) resolveFns.forEach((resolveFn) => resolveFn(promise));
|
|
542
|
+
resolveFns.add(currentResolveFn);
|
|
543
|
+
ongoingPromise = promise;
|
|
544
|
+
return promise;
|
|
463
545
|
};
|
|
464
546
|
return Object.assign(useStore, {
|
|
465
547
|
subscribe: store.subscribe,
|
|
@@ -486,17 +568,17 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
486
568
|
* - `{ error, variable }` on failure
|
|
487
569
|
*
|
|
488
570
|
* @remarks
|
|
489
|
-
* - If a mutation is already in progress, a warning is logged.
|
|
490
|
-
* - Concurrent executions are allowed but may lead to race conditions.
|
|
491
571
|
* - The promise never rejects to simplify async handling.
|
|
572
|
+
* - If a mutation is already in progress, a warning is logged.
|
|
573
|
+
* - When a new execution starts, all previous pending executions will resolve with the result of the latest execution.
|
|
492
574
|
*/
|
|
493
575
|
execute,
|
|
494
576
|
/**
|
|
495
577
|
* Resets the mutation state back to its initial state.
|
|
496
578
|
*
|
|
497
579
|
* @remarks
|
|
498
|
-
* - Does not cancel any ongoing
|
|
499
|
-
* - If
|
|
580
|
+
* - Does not cancel any ongoing execution.
|
|
581
|
+
* - If an execution is still pending, its result may override the reset state.
|
|
500
582
|
*/
|
|
501
583
|
reset: () => {
|
|
502
584
|
if (store.getState().isPending) {
|
|
@@ -509,4 +591,107 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
509
591
|
});
|
|
510
592
|
};
|
|
511
593
|
|
|
512
|
-
|
|
594
|
+
const useMutation = (mutationFn, options = {}) => {
|
|
595
|
+
const { onSuccess = noop, onError, onSettled = noop } = options;
|
|
596
|
+
const callbackRef = useRef({ onSuccess, onError, onSettled });
|
|
597
|
+
callbackRef.current.onSuccess = onSuccess;
|
|
598
|
+
callbackRef.current.onError = onError;
|
|
599
|
+
callbackRef.current.onSettled = onSettled;
|
|
600
|
+
const stateRef = useRef({ ...INITIAL_STATE });
|
|
601
|
+
const [, reRender] = useState({});
|
|
602
|
+
const refs = useRef({
|
|
603
|
+
mutationFn,
|
|
604
|
+
ongoingPromise: void 0,
|
|
605
|
+
resolveFns: /* @__PURE__ */ new Set()
|
|
606
|
+
});
|
|
607
|
+
refs.current.mutationFn = mutationFn;
|
|
608
|
+
const execute = useCallback((variable) => {
|
|
609
|
+
let currentResolveFn;
|
|
610
|
+
const stateBeforeExecute = stateRef.current;
|
|
611
|
+
if (stateBeforeExecute.isPending) {
|
|
612
|
+
console.warn(
|
|
613
|
+
"A mutation was executed while a previous execution is still pending. The previous execution will be ignored (latest execution wins)."
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
stateRef.current.isPending = true;
|
|
617
|
+
reRender({});
|
|
618
|
+
const promise = new Promise(
|
|
619
|
+
(resolve) => {
|
|
620
|
+
currentResolveFn = resolve;
|
|
621
|
+
refs.current.mutationFn(variable, stateBeforeExecute).then((data) => {
|
|
622
|
+
if (promise !== refs.current.ongoingPromise) {
|
|
623
|
+
return resolve({ data, variable });
|
|
624
|
+
}
|
|
625
|
+
stateRef.current = {
|
|
626
|
+
state: "SUCCESS",
|
|
627
|
+
isPending: false,
|
|
628
|
+
isSuccess: true,
|
|
629
|
+
isError: false,
|
|
630
|
+
variable,
|
|
631
|
+
data,
|
|
632
|
+
dataUpdatedAt: Date.now(),
|
|
633
|
+
error: void 0,
|
|
634
|
+
errorUpdatedAt: void 0
|
|
635
|
+
};
|
|
636
|
+
reRender({});
|
|
637
|
+
resolve({ data, variable });
|
|
638
|
+
refs.current.resolveFns.clear();
|
|
639
|
+
callbackRef.current.onSuccess(data, variable, stateBeforeExecute);
|
|
640
|
+
}).catch((error) => {
|
|
641
|
+
if (promise !== refs.current.ongoingPromise) {
|
|
642
|
+
return resolve({ error, variable });
|
|
643
|
+
}
|
|
644
|
+
stateRef.current = {
|
|
645
|
+
state: "ERROR",
|
|
646
|
+
isPending: false,
|
|
647
|
+
isSuccess: false,
|
|
648
|
+
isError: true,
|
|
649
|
+
variable,
|
|
650
|
+
data: void 0,
|
|
651
|
+
dataUpdatedAt: void 0,
|
|
652
|
+
error,
|
|
653
|
+
errorUpdatedAt: Date.now()
|
|
654
|
+
};
|
|
655
|
+
reRender({});
|
|
656
|
+
resolve({ error, variable });
|
|
657
|
+
refs.current.resolveFns.clear();
|
|
658
|
+
if (callbackRef.current.onError) {
|
|
659
|
+
callbackRef.current.onError(error, variable, stateBeforeExecute);
|
|
660
|
+
} else {
|
|
661
|
+
console.error(stateRef.current);
|
|
662
|
+
}
|
|
663
|
+
}).finally(() => {
|
|
664
|
+
if (promise !== refs.current.ongoingPromise) return;
|
|
665
|
+
callbackRef.current.onSettled(variable, stateBeforeExecute);
|
|
666
|
+
refs.current.ongoingPromise = void 0;
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
);
|
|
670
|
+
if (refs.current.ongoingPromise) {
|
|
671
|
+
refs.current.resolveFns.forEach((resolveFn) => resolveFn(promise));
|
|
672
|
+
}
|
|
673
|
+
refs.current.resolveFns.add(currentResolveFn);
|
|
674
|
+
refs.current.ongoingPromise = promise;
|
|
675
|
+
return promise;
|
|
676
|
+
}, []);
|
|
677
|
+
const reset = useCallback(() => {
|
|
678
|
+
if (stateRef.current.isPending) {
|
|
679
|
+
console.warn(
|
|
680
|
+
"Mutation state was reset while a request is still pending. The request will continue, but its result may override the reset state."
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
stateRef.current = { ...INITIAL_STATE };
|
|
684
|
+
reRender({});
|
|
685
|
+
}, []);
|
|
686
|
+
const r = [
|
|
687
|
+
stateRef.current,
|
|
688
|
+
{
|
|
689
|
+
execute,
|
|
690
|
+
reset,
|
|
691
|
+
getLatestState: () => stateRef.current
|
|
692
|
+
}
|
|
693
|
+
];
|
|
694
|
+
return r;
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
export { createMutation, createQuery, createStore, createStores, useIsomorphicLayoutEffect, useMutation, useStoreState };
|
package/esm/vanilla/basic.d.mts
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/esm/vanilla/store.d.mts
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/esm/vanilla.d.mts
CHANGED
package/esm/vanilla.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const isClient = typeof window !== "undefined" && !("Deno" in window);
|
|
2
2
|
const noop = () => {
|
|
3
3
|
};
|
|
4
|
-
const identity = (value) => value;
|
|
5
4
|
const getValue = (valueOrComputeValueFn, ...params) => {
|
|
6
5
|
if (typeof valueOrComputeValueFn === "function") {
|
|
7
6
|
return valueOrComputeValueFn(...params);
|
|
@@ -9,41 +8,6 @@ const getValue = (valueOrComputeValueFn, ...params) => {
|
|
|
9
8
|
return valueOrComputeValueFn;
|
|
10
9
|
};
|
|
11
10
|
|
|
12
|
-
const shallow = (a, b) => {
|
|
13
|
-
if (Object.is(a, b)) {
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
if (a instanceof Map && b instanceof Map) {
|
|
20
|
-
if (a.size !== b.size) return false;
|
|
21
|
-
for (const [key, value] of a) {
|
|
22
|
-
if (!Object.is(value, b.get(key))) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
if (a instanceof Set && b instanceof Set) {
|
|
29
|
-
if (a.size !== b.size) return false;
|
|
30
|
-
for (const value of a) {
|
|
31
|
-
if (!b.has(value)) return false;
|
|
32
|
-
}
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
const keysA = Object.keys(a);
|
|
36
|
-
if (keysA.length !== Object.keys(b).length) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
for (let i = 0; i < keysA.length; i++) {
|
|
40
|
-
if (!Object.prototype.hasOwnProperty.call(b, keysA[i]) || !Object.is(a[keysA[i]], b[keysA[i]])) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return true;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
11
|
const hasObjectPrototype = (value) => {
|
|
48
12
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
49
13
|
};
|
|
@@ -96,13 +60,15 @@ const initStore = (initialState, options = {}) => {
|
|
|
96
60
|
}
|
|
97
61
|
const prevState = state;
|
|
98
62
|
const newValue = getValue(value, state);
|
|
63
|
+
const changedKeys = [];
|
|
99
64
|
for (const key in newValue) {
|
|
100
65
|
if (!Object.is(prevState[key], newValue[key])) {
|
|
101
|
-
|
|
102
|
-
[...subscribers].forEach((subscriber) => subscriber(state, prevState));
|
|
103
|
-
return;
|
|
66
|
+
changedKeys.push(key);
|
|
104
67
|
}
|
|
105
68
|
}
|
|
69
|
+
if (changedKeys.length === 0) return;
|
|
70
|
+
state = { ...prevState, ...newValue };
|
|
71
|
+
[...subscribers].forEach((subscriber) => subscriber(state, prevState, changedKeys));
|
|
106
72
|
};
|
|
107
73
|
const storeApi = {
|
|
108
74
|
getState,
|
|
@@ -113,4 +79,4 @@ const initStore = (initialState, options = {}) => {
|
|
|
113
79
|
return storeApi;
|
|
114
80
|
};
|
|
115
81
|
|
|
116
|
-
export { getHash, getValue,
|
|
82
|
+
export { getHash, getValue, initStore, isClient, isPlainObject, noop };
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
|
|
|
14
14
|
* - No retry mechanism
|
|
15
15
|
* - No caching across executions
|
|
16
16
|
*/
|
|
17
|
-
export type MutationState<TData, TVariable> = {
|
|
17
|
+
export type MutationState<TData, TVariable, TError> = {
|
|
18
18
|
isPending: boolean;
|
|
19
19
|
} & ({
|
|
20
20
|
state: 'INITIAL';
|
|
@@ -41,28 +41,42 @@ export type MutationState<TData, TVariable> = {
|
|
|
41
41
|
variable: TVariable;
|
|
42
42
|
data: undefined;
|
|
43
43
|
dataUpdatedAt: undefined;
|
|
44
|
-
error:
|
|
44
|
+
error: TError;
|
|
45
45
|
errorUpdatedAt: number;
|
|
46
46
|
});
|
|
47
|
+
export declare const INITIAL_STATE: {
|
|
48
|
+
state: string;
|
|
49
|
+
isPending: boolean;
|
|
50
|
+
isSuccess: boolean;
|
|
51
|
+
isError: boolean;
|
|
52
|
+
variable: undefined;
|
|
53
|
+
data: undefined;
|
|
54
|
+
dataUpdatedAt: undefined;
|
|
55
|
+
error: undefined;
|
|
56
|
+
errorUpdatedAt: undefined;
|
|
57
|
+
};
|
|
47
58
|
/**
|
|
48
59
|
* Configuration options for a mutation.
|
|
49
60
|
*
|
|
50
61
|
* @remarks
|
|
51
62
|
* Lifecycle callbacks are triggered for each execution.
|
|
52
63
|
*/
|
|
53
|
-
export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<TData, TVariable>> & {
|
|
64
|
+
export type MutationOptions<TData, TVariable, TError = Error> = InitStoreOptions<MutationState<TData, TVariable, TError>> & {
|
|
54
65
|
/**
|
|
55
|
-
* Called when the mutation succeeds
|
|
66
|
+
* Called when the mutation succeeds.\
|
|
67
|
+
* If multiple concurrent executions happened, only the latest execution triggers this callback.
|
|
56
68
|
*/
|
|
57
|
-
onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => void;
|
|
69
|
+
onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
|
|
58
70
|
/**
|
|
59
|
-
* Called when the mutation fails
|
|
71
|
+
* Called when the mutation fails.\
|
|
72
|
+
* If multiple concurrent executions happened, only the latest execution triggers this callback.
|
|
60
73
|
*/
|
|
61
|
-
onError?: (error:
|
|
74
|
+
onError?: (error: TError, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
|
|
62
75
|
/**
|
|
63
|
-
* Called after the mutation settles (either success or error)
|
|
76
|
+
* Called after the mutation settles (either success or error).\
|
|
77
|
+
* If multiple concurrent executions happened, only the latest execution triggers this callback.
|
|
64
78
|
*/
|
|
65
|
-
onSettled?: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => void;
|
|
79
|
+
onSettled?: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => void;
|
|
66
80
|
};
|
|
67
81
|
/**
|
|
68
82
|
* Creates a mutation store for handling async operations that modify data.
|
|
@@ -78,8 +92,10 @@ export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<T
|
|
|
78
92
|
* - Mutations are **not cached** and only track the latest execution.
|
|
79
93
|
* - Designed for operations that change data (e.g. create, update, delete).
|
|
80
94
|
* - No retry mechanism is provided by default.
|
|
81
|
-
* - Each execution overwrites the previous state.
|
|
82
95
|
* - The mutation always resolves (never throws): the result contains either `data` or `error`.
|
|
96
|
+
* - If multiple executions triggered at the same time:
|
|
97
|
+
* - Only the latest execution is allowed to update the state.
|
|
98
|
+
* - Results from previous executions are ignored if a newer one exists.
|
|
83
99
|
*
|
|
84
100
|
* @example
|
|
85
101
|
* const useCreateUser = createMutation(async (input) => {
|
|
@@ -89,10 +105,10 @@ export type MutationOptions<TData, TVariable> = InitStoreOptions<MutationState<T
|
|
|
89
105
|
* const { isPending } = useCreateUser();
|
|
90
106
|
* const result = await useCreateUser.execute({ name: 'John' });
|
|
91
107
|
*/
|
|
92
|
-
export declare const createMutation: <TData, TVariable = undefined>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => Promise<TData>, options?: MutationOptions<TData, TVariable>) => (
|
|
93
|
-
subscribe: (subscriber: import("../vanilla.ts").Subscriber<MutationState<TData, TVariable>>) => () => void;
|
|
94
|
-
getSubscribers: () => Set<import("../vanilla.ts").Subscriber<MutationState<TData, TVariable>>>;
|
|
95
|
-
getState: () => MutationState<TData, TVariable>;
|
|
108
|
+
export declare const createMutation: <TData, TVariable = undefined, TError = Error>(mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => Promise<TData>, options?: MutationOptions<TData, TVariable, TError>) => (() => MutationState<TData, TVariable, TError>) & {
|
|
109
|
+
subscribe: (subscriber: import("../vanilla.ts").Subscriber<MutationState<TData, TVariable, TError>>) => () => void;
|
|
110
|
+
getSubscribers: () => Set<import("../vanilla.ts").Subscriber<MutationState<TData, TVariable, TError>>>;
|
|
111
|
+
getState: () => MutationState<TData, TVariable, TError>;
|
|
96
112
|
/**
|
|
97
113
|
* Manually updates the mutation state.
|
|
98
114
|
*
|
|
@@ -100,7 +116,7 @@ export declare const createMutation: <TData, TVariable = undefined>(mutationFn:
|
|
|
100
116
|
* - Intended for advanced use cases.
|
|
101
117
|
* - Prefer using provided mutation actions (`execute`, `reset`) instead.
|
|
102
118
|
*/
|
|
103
|
-
setState: (value: SetState<MutationState<TData, TVariable>>) => void;
|
|
119
|
+
setState: (value: SetState<MutationState<TData, TVariable, TError>>) => void;
|
|
104
120
|
/**
|
|
105
121
|
* Executes the mutation.
|
|
106
122
|
*
|
|
@@ -111,25 +127,25 @@ export declare const createMutation: <TData, TVariable = undefined>(mutationFn:
|
|
|
111
127
|
* - `{ error, variable }` on failure
|
|
112
128
|
*
|
|
113
129
|
* @remarks
|
|
114
|
-
* - If a mutation is already in progress, a warning is logged.
|
|
115
|
-
* - Concurrent executions are allowed but may lead to race conditions.
|
|
116
130
|
* - The promise never rejects to simplify async handling.
|
|
131
|
+
* - If a mutation is already in progress, a warning is logged.
|
|
132
|
+
* - When a new execution starts, all previous pending executions will resolve with the result of the latest execution.
|
|
117
133
|
*/
|
|
118
134
|
execute: TVariable extends undefined ? () => Promise<{
|
|
119
135
|
variable: undefined;
|
|
120
136
|
data?: TData;
|
|
121
|
-
error?:
|
|
137
|
+
error?: TError;
|
|
122
138
|
}> : (variable: TVariable) => Promise<{
|
|
123
139
|
variable: TVariable;
|
|
124
140
|
data?: TData;
|
|
125
|
-
error?:
|
|
141
|
+
error?: TError;
|
|
126
142
|
}>;
|
|
127
143
|
/**
|
|
128
144
|
* Resets the mutation state back to its initial state.
|
|
129
145
|
*
|
|
130
146
|
* @remarks
|
|
131
|
-
* - Does not cancel any ongoing
|
|
132
|
-
* - If
|
|
147
|
+
* - Does not cancel any ongoing execution.
|
|
148
|
+
* - If an execution is still pending, its result may override the reset state.
|
|
133
149
|
*/
|
|
134
150
|
reset: () => void;
|
|
135
151
|
};
|