floppy-disk 3.0.0-alpha.5 → 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 +24 -22
- package/esm/react/use-mutation.d.mts +82 -0
- package/esm/react.d.mts +2 -1
- package/esm/react.mjs +135 -14
- package/package.json +1 -1
- package/react/create-mutation.d.ts +37 -21
- package/react/create-query.d.ts +24 -22
- package/react/use-mutation.d.ts +82 -0
- package/react.d.ts +2 -1
- package/react.js +134 -12
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type MutationOptions, type MutationState } from './create-mutation';
|
|
2
|
+
/**
|
|
3
|
+
* A hook for managing async mutation state.
|
|
4
|
+
*
|
|
5
|
+
* @param mutationFn - Async function that performs the mutation.
|
|
6
|
+
* Receives the input variable and the state snapshot before execution.
|
|
7
|
+
*
|
|
8
|
+
* @param options - Optional lifecycle callbacks:
|
|
9
|
+
* - `onSuccess(data, variable, stateBeforeExecute)`
|
|
10
|
+
* - `onError(error, variable, stateBeforeExecute)`
|
|
11
|
+
* - `onSettled(variable, stateBeforeExecute)`
|
|
12
|
+
*
|
|
13
|
+
* @returns A tuple containing:
|
|
14
|
+
* - state: The current mutation state (render snapshot)
|
|
15
|
+
* - controls: An object with mutation actions and helpers
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* - No retry mechanism is provided by default.
|
|
19
|
+
* - The mutation always resolves (never throws): the result contains either `data` or `error`.
|
|
20
|
+
* - If multiple executions triggered at the same time:
|
|
21
|
+
* - Only the latest execution is allowed to update the state.
|
|
22
|
+
* - Results from previous executions are ignored if a newer one exists.
|
|
23
|
+
*/
|
|
24
|
+
export declare const useMutation: <TData, TVariable = undefined, TError = Error>(
|
|
25
|
+
/**
|
|
26
|
+
* Async function that performs the mutation.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* - Does NOT need to be memoized (e.g. `useCallback`).
|
|
30
|
+
* - The latest function reference is always used internally.
|
|
31
|
+
*/
|
|
32
|
+
mutationFn: (variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable, TError>) => Promise<TData>,
|
|
33
|
+
/**
|
|
34
|
+
* Optional lifecycle callbacks.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* - Callbacks do NOT need to be memoized.
|
|
38
|
+
* - The latest callbacks are always used internally.
|
|
39
|
+
*/
|
|
40
|
+
options?: MutationOptions<TData, TVariable, TError>) => [MutationState<TData, TVariable, TError>, {
|
|
41
|
+
/**
|
|
42
|
+
* Executes the mutation.
|
|
43
|
+
*
|
|
44
|
+
* @param variable - Input passed to the mutation function
|
|
45
|
+
*
|
|
46
|
+
* @returns A promise that always resolves with:
|
|
47
|
+
* - `{ data, variable }` on success
|
|
48
|
+
* - `{ error, variable }` on failure
|
|
49
|
+
*
|
|
50
|
+
* @remarks
|
|
51
|
+
* - The promise never rejects to simplify async handling.
|
|
52
|
+
* - If a mutation is already in progress, a warning is logged.
|
|
53
|
+
* - When a new execution starts, all previous pending executions will resolve with the result of the latest execution.
|
|
54
|
+
*/
|
|
55
|
+
execute: TVariable extends undefined ? () => Promise<{
|
|
56
|
+
variable: undefined;
|
|
57
|
+
data?: TData;
|
|
58
|
+
error?: TError;
|
|
59
|
+
}> : (variable: TVariable) => Promise<{
|
|
60
|
+
variable: TVariable;
|
|
61
|
+
data?: TData;
|
|
62
|
+
error?: TError;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Resets the mutation state back to its initial state.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* - Does not cancel any ongoing execution.
|
|
69
|
+
* - If an execution is still pending, its result may override the reset state.
|
|
70
|
+
*/
|
|
71
|
+
reset: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Returns the latest mutation state directly from the internal ref.
|
|
74
|
+
*
|
|
75
|
+
* @returns The most up-to-date mutation state.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* - Unlike the `state` returned by the hook, this value is not tied to React render cycles.
|
|
79
|
+
* - Use this inside async flows or event handlers to avoid stale reads.
|
|
80
|
+
*/
|
|
81
|
+
getLatestState: () => MutationState<TData, TVariable, TError>;
|
|
82
|
+
}];
|
package/react.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { useStoreState } from './react/use-store';
|
|
|
3
3
|
export * from './react/create-store';
|
|
4
4
|
export * from './react/create-stores';
|
|
5
5
|
export * from './react/create-query';
|
|
6
|
-
export
|
|
6
|
+
export { createMutation, type MutationOptions, type MutationState, } from './react/create-mutation';
|
|
7
|
+
export * from './react/use-mutation';
|
package/react.js
CHANGED
|
@@ -130,7 +130,7 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
130
130
|
onSettled = vanilla.noop,
|
|
131
131
|
shouldRetry: shouldRetryFn = (_, s) => s.retryCount === 0 ? [true, 1500] : [false]
|
|
132
132
|
} = options;
|
|
133
|
-
const initialState = INITIAL_STATE$1;
|
|
133
|
+
const initialState = { ...INITIAL_STATE$1 };
|
|
134
134
|
const stores = /* @__PURE__ */ new Map();
|
|
135
135
|
const configureStoreEvents = () => ({
|
|
136
136
|
...options,
|
|
@@ -369,7 +369,7 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
369
369
|
internals.set(store, configureInternals(store, variable, variableHash));
|
|
370
370
|
}
|
|
371
371
|
const useStore = (options2 = {}) => {
|
|
372
|
-
const {
|
|
372
|
+
const { revalidateOnMount = true, keepPreviousData } = options2;
|
|
373
373
|
const storeState = store.getState();
|
|
374
374
|
const prevState = react.useRef({});
|
|
375
375
|
let storeStateToBeUsed = storeState;
|
|
@@ -382,7 +382,7 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
382
382
|
storeStateToBeUsed = { ...storeState, ...prevState.current };
|
|
383
383
|
}
|
|
384
384
|
const [trackedState, usedPathsRef] = useStoreStateProxy(
|
|
385
|
-
|
|
385
|
+
revalidateOnMount && storeState.state === "INITIAL" ? (
|
|
386
386
|
// Optimize rendering on initial state
|
|
387
387
|
// Do { isPending: true } → result
|
|
388
388
|
// instead of { isPending: false } → { isPending: true } → result
|
|
@@ -406,8 +406,8 @@ const createQuery = (queryFn, options = {}) => {
|
|
|
406
406
|
});
|
|
407
407
|
}, [store]);
|
|
408
408
|
useIsomorphicLayoutEffect(() => {
|
|
409
|
-
if (
|
|
410
|
-
}, [store,
|
|
409
|
+
if (revalidateOnMount !== false) revalidate(store, variable, false);
|
|
410
|
+
}, [store, revalidateOnMount]);
|
|
411
411
|
if (keepPreviousData) {
|
|
412
412
|
!!trackedState.error;
|
|
413
413
|
}
|
|
@@ -481,19 +481,26 @@ const INITIAL_STATE = {
|
|
|
481
481
|
};
|
|
482
482
|
const createMutation = (mutationFn, options = {}) => {
|
|
483
483
|
const { onSuccess = vanilla.noop, onError, onSettled = vanilla.noop } = options;
|
|
484
|
-
const initialState = INITIAL_STATE;
|
|
484
|
+
const initialState = { ...INITIAL_STATE };
|
|
485
|
+
let ongoingPromise;
|
|
486
|
+
const resolveFns = /* @__PURE__ */ new Set([]);
|
|
485
487
|
const store = vanilla.initStore(initialState, options);
|
|
486
488
|
const useStore = () => useStoreState(store.getState(), store.subscribe);
|
|
487
489
|
const execute = (variable) => {
|
|
490
|
+
let currentResolveFn;
|
|
488
491
|
const stateBeforeExecute = store.getState();
|
|
489
492
|
if (stateBeforeExecute.isPending) {
|
|
490
493
|
console.warn(
|
|
491
|
-
"
|
|
494
|
+
"A mutation was executed while a previous execution is still pending. The previous execution will be ignored (latest execution wins)."
|
|
492
495
|
);
|
|
493
496
|
}
|
|
494
497
|
store.setState({ isPending: true });
|
|
495
|
-
|
|
498
|
+
const promise = new Promise((resolve) => {
|
|
499
|
+
currentResolveFn = resolve;
|
|
496
500
|
mutationFn(variable, stateBeforeExecute).then((data) => {
|
|
501
|
+
if (promise !== ongoingPromise) {
|
|
502
|
+
return resolve({ data, variable });
|
|
503
|
+
}
|
|
497
504
|
store.setState({
|
|
498
505
|
state: "SUCCESS",
|
|
499
506
|
isPending: false,
|
|
@@ -506,8 +513,12 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
506
513
|
errorUpdatedAt: void 0
|
|
507
514
|
});
|
|
508
515
|
resolve({ data, variable });
|
|
516
|
+
resolveFns.clear();
|
|
509
517
|
onSuccess(data, variable, stateBeforeExecute);
|
|
510
518
|
}).catch((error) => {
|
|
519
|
+
if (promise !== ongoingPromise) {
|
|
520
|
+
return resolve({ error, variable });
|
|
521
|
+
}
|
|
511
522
|
store.setState({
|
|
512
523
|
state: "ERROR",
|
|
513
524
|
isPending: false,
|
|
@@ -520,12 +531,19 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
520
531
|
errorUpdatedAt: Date.now()
|
|
521
532
|
});
|
|
522
533
|
resolve({ error, variable });
|
|
534
|
+
resolveFns.clear();
|
|
523
535
|
if (onError) onError(error, variable, stateBeforeExecute);
|
|
524
536
|
else console.error(store.getState());
|
|
525
537
|
}).finally(() => {
|
|
538
|
+
if (promise !== ongoingPromise) return;
|
|
526
539
|
onSettled(variable, stateBeforeExecute);
|
|
540
|
+
ongoingPromise = void 0;
|
|
527
541
|
});
|
|
528
542
|
});
|
|
543
|
+
if (ongoingPromise) resolveFns.forEach((resolveFn) => resolveFn(promise));
|
|
544
|
+
resolveFns.add(currentResolveFn);
|
|
545
|
+
ongoingPromise = promise;
|
|
546
|
+
return promise;
|
|
529
547
|
};
|
|
530
548
|
return Object.assign(useStore, {
|
|
531
549
|
subscribe: store.subscribe,
|
|
@@ -552,17 +570,17 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
552
570
|
* - `{ error, variable }` on failure
|
|
553
571
|
*
|
|
554
572
|
* @remarks
|
|
555
|
-
* - If a mutation is already in progress, a warning is logged.
|
|
556
|
-
* - Concurrent executions are allowed but may lead to race conditions.
|
|
557
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.
|
|
558
576
|
*/
|
|
559
577
|
execute,
|
|
560
578
|
/**
|
|
561
579
|
* Resets the mutation state back to its initial state.
|
|
562
580
|
*
|
|
563
581
|
* @remarks
|
|
564
|
-
* - Does not cancel any ongoing
|
|
565
|
-
* - If
|
|
582
|
+
* - Does not cancel any ongoing execution.
|
|
583
|
+
* - If an execution is still pending, its result may override the reset state.
|
|
566
584
|
*/
|
|
567
585
|
reset: () => {
|
|
568
586
|
if (store.getState().isPending) {
|
|
@@ -575,9 +593,113 @@ const createMutation = (mutationFn, options = {}) => {
|
|
|
575
593
|
});
|
|
576
594
|
};
|
|
577
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
|
+
|
|
578
699
|
exports.createMutation = createMutation;
|
|
579
700
|
exports.createQuery = createQuery;
|
|
580
701
|
exports.createStore = createStore;
|
|
581
702
|
exports.createStores = createStores;
|
|
582
703
|
exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
|
|
704
|
+
exports.useMutation = useMutation;
|
|
583
705
|
exports.useStoreState = useStoreState;
|