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/esm/react.mjs CHANGED
@@ -1,30 +1,75 @@
1
- import { useLayoutEffect, useEffect, useState, useRef } from 'react';
2
- import { isClient, identity, shallow, initStore, getHash, noop } from 'floppy-disk/vanilla';
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 useStoreUpdateNotifier = (store, selector) => {
7
- const [, reRender] = useState({});
8
- const selectorRef = useRef(selector);
9
- selectorRef.current = selector;
10
- useIsomorphicLayoutEffect(
11
- () => store.subscribe((state, prevState) => {
12
- if (selectorRef.current === identity) return reRender({});
13
- const prevSlice = selectorRef.current(prevState);
14
- const nextSlice = selectorRef.current(state);
15
- if (!shallow(prevSlice, nextSlice)) reRender({});
16
- }),
17
- [store]
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 useStoreState = (store, selector = identity) => {
21
- useStoreUpdateNotifier(store, selector);
22
- return selector(store.getState());
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 = (selector) => useStoreState(store, selector);
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 = (selector) => {
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
- function useStore(optionsOrSelector = {}, maybeSelector) {
327
- let selector;
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
- if (storeState.isSuccess) {
344
- prevState.current = { data: storeState.data, dataUpdatedAt: storeState.dataUpdatedAt };
345
- } else if (storeState.state === "INITIAL" && options2.keepPreviousData) {
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
- return selector(storeStateToBeUsed);
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 = (selector) => useStoreState(store, selector);
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
- "Mutation executed while a previous execution is still pending. This may cause race conditions or unexpected state updates."
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
- return new Promise((resolve) => {
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 request.
499
- * - If a request is still pending, its result may override the reset state.
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
- export { createMutation, createQuery, createStore, createStores, useIsomorphicLayoutEffect, useStoreState, useStoreUpdateNotifier };
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 };
@@ -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.
@@ -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 the state actually changes.
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 is expected to be an **object**.\
57
- * Updates are applied as partial merges, so non-object states are not supported.
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 both the new state and the previous state.
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 **disabled on the server** to prevent accidental shared state between requests.
70
- * - This avoids leaking data between users in server environments.
71
- * - You can override this by setting `allowSetStateServerSide: true`.
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
@@ -1,4 +1,3 @@
1
1
  export * from './vanilla/basic.mjs';
2
- export * from './vanilla/shallow.mjs';
3
2
  export * from './vanilla/hash.mjs';
4
3
  export * from './vanilla/store.mjs';
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
- state = { ...prevState, ...newValue };
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, identity, initStore, isClient, isPlainObject, noop, shallow };
82
+ export { getHash, getValue, initStore, isClient, isPlainObject, noop };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "floppy-disk",
3
3
  "description": "Lightweight, simple, and powerful state management library",
4
4
  "private": false,
5
- "version": "3.0.0-alpha.4",
5
+ "version": "3.0.0-alpha.6",
6
6
  "publishConfig": {
7
7
  "tag": "alpha"
8
8
  },
@@ -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: any;
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: any, variable: TVariable, stateBeforeExecute: MutationState<TData, TVariable>) => void;
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>) => (<TStateSlice = MutationState<TData, TVariable>>(selector?: (state: MutationState<TData, TVariable>) => TStateSlice) => TStateSlice) & {
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?: any;
137
+ error?: TError;
122
138
  }> : (variable: TVariable) => Promise<{
123
139
  variable: TVariable;
124
140
  data?: TData;
125
- error?: any;
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 request.
132
- * - If a request is still pending, its result may override the reset state.
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
  };