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.
@@ -19,7 +19,7 @@ import { type InitStoreOptions, type SetState } from 'floppy-disk/vanilla';
19
19
  * @remarks
20
20
  * - Data and error are mutually exclusive except in `SUCCESS_BUT_REVALIDATION_ERROR`.
21
21
  */
22
- export type QueryState<TData> = {
22
+ export type QueryState<TData, TError> = {
23
23
  isPending: boolean;
24
24
  isRevalidating: boolean;
25
25
  isRetrying: boolean;
@@ -46,7 +46,7 @@ export type QueryState<TData> = {
46
46
  isError: true;
47
47
  data: undefined;
48
48
  dataUpdatedAt: undefined;
49
- error: any;
49
+ error: TError;
50
50
  errorUpdatedAt: number;
51
51
  } | {
52
52
  state: 'SUCCESS_BUT_REVALIDATION_ERROR';
@@ -54,7 +54,7 @@ export type QueryState<TData> = {
54
54
  isError: false;
55
55
  data: TData;
56
56
  dataUpdatedAt: number;
57
- error: any;
57
+ error: TError;
58
58
  errorUpdatedAt: number;
59
59
  });
60
60
  /**
@@ -63,13 +63,13 @@ export type QueryState<TData> = {
63
63
  * @remarks
64
64
  * Controls caching, retry behavior, lifecycle, and side effects of an async operation.
65
65
  */
66
- export type QueryOptions<TData, TVariable extends Record<string, any>> = InitStoreOptions<QueryState<TData>> & {
66
+ export type QueryOptions<TData, TVariable extends Record<string, any>, TError = Error> = InitStoreOptions<QueryState<TData, TError>> & {
67
67
  /**
68
68
  * Time (in milliseconds) that data is considered fresh.
69
69
  *
70
- * While fresh, revalidation will be skipped.
70
+ * While fresh, revalidation will be skipped unless explicitly invalidated.
71
71
  *
72
- * @default 2500 ms (2.5 minutes)
72
+ * @default 2500 ms (2.5 seconds)
73
73
  */
74
74
  staleTime?: number;
75
75
  /**
@@ -95,15 +95,15 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
95
95
  /**
96
96
  * Called when the query succeeds.
97
97
  */
98
- onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: QueryState<TData>) => void;
98
+ onSuccess?: (data: TData, variable: TVariable, stateBeforeExecute: QueryState<TData, TError>) => void;
99
99
  /**
100
100
  * Called when the query fails and will not retry.
101
101
  */
102
- onError?: (error: any, variable: TVariable, stateBeforeExecute: QueryState<TData>) => void;
102
+ onError?: (error: TError, variable: TVariable, stateBeforeExecute: QueryState<TData, TError>) => void;
103
103
  /**
104
104
  * Called after the query settles (success or final failure).
105
105
  */
106
- onSettled?: (variable: TVariable, stateBeforeExecute: QueryState<TData>) => void;
106
+ onSettled?: (variable: TVariable, stateBeforeExecute: QueryState<TData, TError>) => void;
107
107
  /**
108
108
  * Determines whether a failed query should retry.
109
109
  *
@@ -120,7 +120,7 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
120
120
  * return [false];
121
121
  * }
122
122
  */
123
- shouldRetry?: (error: any, currentState: QueryState<TData>) => [true, number] | [false];
123
+ shouldRetry?: (error: TError, currentState: QueryState<TData, TError>) => [true, number] | [false];
124
124
  };
125
125
  /**
126
126
  * Creates a query factory for managing cached async operations.
@@ -131,16 +131,20 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
131
131
  * @returns A function to retrieve or create a query instance by variable
132
132
  *
133
133
  * @remarks
134
- * - Queries are cached by a deterministic key derived from `variable`.
134
+ * - Queries are **keyed by variable** (via deterministic hashing).
135
135
  * - Each unique variable maps to its own store instance.
136
- * - Queries support:
137
- * - Caching with `staleTime`
138
- * - Explicit invalidation independent of freshness
139
- * - Automatic garbage collection (`gcTime`)
140
- * - Retry logic via `shouldRetry`
141
- * - Background revalidation (focus / reconnect)
142
- * - Execution is deduplicated: multiple calls share the same in-flight promise.
143
- * - Ongoing executions can be optionally overwritten.
136
+ *
137
+ * Core features:
138
+ * - Caching via `staleTime`
139
+ * - Explicit invalidation (independent of freshness)
140
+ * - Retry logic via `shouldRetry`
141
+ * - Background revalidation (focus / reconnect)
142
+ * - Garbage collection via `gcTime`
143
+ *
144
+ * Execution behavior:
145
+ * - By default, executions **overwrite ongoing executions**
146
+ * - Set `overwriteOngoingExecution: false` to enable deduplication
147
+ * - Internal revalidation (focus/reconnect) uses deduplication by default
144
148
  *
145
149
  * @example
146
150
  * const userQuery = createQuery<UserDetail, { id: string }>(async ({ id }) => {
@@ -153,39 +157,38 @@ export type QueryOptions<TData, TVariable extends Record<string, any>> = InitSto
153
157
  * // ...
154
158
  * }
155
159
  */
156
- export declare const createQuery: <TData, TVariable extends Record<string, any> = never>(queryFn: (variable: TVariable, currentState: QueryState<TData>) => Promise<TData>, options?: QueryOptions<TData, TVariable>) => ((variable?: TVariable) => {
157
- <TStateSlice = QueryState<TData>>(options?: {
158
- /**
159
- * Whether the query should execute automatically on mount.
160
- *
161
- * @default true
162
- */
163
- enabled?: boolean;
164
- /**
165
- * Whether to keep previous successful data while a new variable is loading.
166
- *
167
- * @remarks
168
- * - Only applies when the query is in the `INITIAL` state (no data & no error).
169
- * - Intended for variable changes:
170
- * when switching from one variable to another, the previous data is temporarily shown
171
- * while the new execution is in progress.
172
- * - Once the new execution resolves (success or error), the previous data is no longer used.
173
- * - Prevents UI flicker (e.g. empty/loading state) during transitions.
174
- *
175
- * @example
176
- * // Switching from userId=1 → userId=2
177
- * // While loading userId=2, still show userId=1 data
178
- * useQuery({ id: userId }, { keepPreviousData: true });
179
- */ keepPreviousData?: boolean;
180
- }, selector?: (state: QueryState<TData>) => TStateSlice): TStateSlice;
181
- <TStateSlice = QueryState<TData>>(selector?: (state: QueryState<TData>) => TStateSlice): TStateSlice;
182
- } & {
160
+ export declare const createQuery: <TData, TVariable extends Record<string, any> = never, TError = Error>(queryFn: (variable: TVariable, currentState: QueryState<TData, TError>) => Promise<TData>, options?: QueryOptions<TData, TVariable, TError>) => ((variable?: TVariable) => ((options?: {
161
+ /**
162
+ * Whether the query should be ravalidated automatically on mount.
163
+ *
164
+ * Revalidate means execute the queryFn **if stale/invalidated**.
165
+ *
166
+ * @default true
167
+ */
168
+ revalidateOnMount?: boolean;
169
+ /**
170
+ * Whether to keep previous successful data while a new variable is loading.
171
+ *
172
+ * @remarks
173
+ * - Only applies when the query is in the `INITIAL` state (no data & no error).
174
+ * - Intended for variable changes:
175
+ * when switching from one variable to another, the previous data is temporarily shown
176
+ * while the new execution is in progress.
177
+ * - Once the new execution resolves (success or error), the previous data is no longer used.
178
+ * - Prevents UI flicker (e.g. empty/loading state) during transitions.
179
+ *
180
+ * @example
181
+ * // Switching from userId=1 userId=2
182
+ * // While loading userId=2, still show userId=1 data
183
+ * useQuery({ id: userId }, { keepPreviousData: true });
184
+ */ keepPreviousData?: boolean;
185
+ }) => QueryState<TData, TError>) & {
183
186
  metadata: {
184
187
  isInvalidated?: boolean;
185
- promise?: Promise<QueryState<TData>> | undefined;
186
- promiseResolver?: ((value: QueryState<TData> | PromiseLike<QueryState<TData>>) => void) | undefined;
188
+ promise?: Promise<QueryState<TData, TError>> | undefined;
189
+ promiseResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
187
190
  retryTimeoutId?: number;
188
- retryResolver?: ((value: QueryState<TData> | PromiseLike<QueryState<TData>>) => void) | undefined;
191
+ retryResolver?: ((value: QueryState<TData, TError> | PromiseLike<QueryState<TData, TError>>) => void) | undefined;
189
192
  garbageCollectionTimeoutId?: number;
190
193
  rollbackData?: TData | undefined;
191
194
  };
@@ -211,7 +214,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
211
214
  * @returns A promise resolving to the latest query state
212
215
  *
213
216
  * @remarks
214
- * - By default, each call starts a new execution even if one is already in progress.
217
+ * - By default, each call **starts a new execution** even if one is already in progress.
215
218
  * - Set `overwriteOngoingExecution: false` to reuse an ongoing execution (deduplication).
216
219
  * - Handles:
217
220
  * - Pending state
@@ -221,7 +224,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
221
224
  */
222
225
  execute: (options?: {
223
226
  overwriteOngoingExecution?: boolean;
224
- }) => Promise<QueryState<TData>>;
227
+ }) => Promise<QueryState<TData, TError>>;
225
228
  /**
226
229
  * Re-executes the query if needed based on freshness or invalidation.
227
230
  *
@@ -237,7 +240,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
237
240
  */
238
241
  revalidate: (options?: {
239
242
  overwriteOngoingExecution?: boolean;
240
- }) => Promise<QueryState<TData>>;
243
+ }) => Promise<QueryState<TData, TError>>;
241
244
  /**
242
245
  * Marks the query as invalidated and optionally triggers re-execution.
243
246
  *
@@ -289,7 +292,7 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
289
292
  * const { rollback, revalidate } = query.optimisticUpdate(newData);
290
293
  */
291
294
  optimisticUpdate: (data: TData) => {
292
- revalidate: () => Promise<QueryState<TData>>;
295
+ revalidate: () => Promise<QueryState<TData, TError>>;
293
296
  rollback: () => TData;
294
297
  };
295
298
  /**
@@ -301,10 +304,10 @@ export declare const createQuery: <TData, TVariable extends Record<string, any>
301
304
  * - Should be used if an optimistic update fails.
302
305
  */
303
306
  rollbackOptimisticUpdate: () => TData;
304
- subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<QueryState<TData>>) => () => void;
305
- getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<QueryState<TData>>>;
306
- getState: () => QueryState<TData>;
307
- setState: (value: SetState<QueryState<TData>>) => void;
307
+ subscribe: (subscriber: import("../vanilla.d.mts").Subscriber<QueryState<TData, TError>>) => () => void;
308
+ getSubscribers: () => Set<import("../vanilla.d.mts").Subscriber<QueryState<TData, TError>>>;
309
+ getState: () => QueryState<TData, TError>;
310
+ setState: (value: SetState<QueryState<TData, TError>>) => void;
308
311
  }) & {
309
312
  /**
310
313
  * Executes all query instances.
@@ -12,14 +12,17 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
12
12
  * @remarks
13
13
  * - Combines the vanilla store with React integration.
14
14
  * - The returned function can be used directly as a hook.
15
+ * - The hook uses Proxy-based tracking to automatically detect which state fields are used.
16
+ * - Components will only re-render when the accessed values change.
15
17
  *
16
18
  * @example
17
- * const useCounter = createStore({ count: 0 });
19
+ * const useMyStore = createStore({ foo: 1, bar: 2 });
18
20
  *
19
21
  * function Component() {
20
- * const count = useCounter((s) => s.count);
22
+ * const state = useMyStore();
23
+ * return <div>{state.foo}</div>;
21
24
  * }
22
25
  *
23
- * useCounter.setState({ count: 1 });
26
+ * useMyStore.setState({ foo: 2 }); // only components using foo will re-render
24
27
  */
25
- export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & import("../vanilla.d.mts").StoreApi<TState>;
28
+ export declare const createStore: <TState extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (() => TState) & import("../vanilla.d.mts").StoreApi<TState>;
@@ -12,18 +12,25 @@ import { type InitStoreOptions } from 'floppy-disk/vanilla';
12
12
  * - Keys are deterministically hashed, ensuring stable identity.
13
13
  * - Stores are lazily created and cached.
14
14
  * - Each store has its own state, subscribers, and lifecycle.
15
+ * - Each returned store includes:
16
+ * - React hook (with Proxy-based tracking)
17
+ * - Store API methods
18
+ * - `delete()` for manual cleanup
15
19
  * - Useful for scenarios like:
16
20
  * - Query caches
17
21
  * - Entity-based state
18
22
  * - Dynamic instances
19
23
  *
20
24
  * @example
21
- * const getUserStore = createStores({ name: '' });
25
+ * const userStore = createStores<{ name: string }, { id: number }>({ name: '' });
22
26
  *
23
- * const userStore = getUserStore({ id: 1 });
24
- * const name = userStore((s) => s.name);
27
+ * function Component() {
28
+ * const useUserStore = userStore({ id: 1 });
29
+ * const state = useUserStore();
30
+ * return <div>{state.name}</div>;
31
+ * }
25
32
  */
26
- export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => (<TStateSlice = TState>(selector?: (state: TState) => TStateSlice) => TStateSlice) & {
33
+ export declare const createStores: <TState extends Record<string, any>, TKey extends Record<string, any>>(initialState: TState, options?: InitStoreOptions<TState>) => (key?: TKey) => (() => TState) & {
27
34
  delete: () => boolean;
28
35
  setState: (value: import("../vanilla.d.mts").SetState<TState>) => void;
29
36
  getState: () => TState;
@@ -0,0 +1,82 @@
1
+ import { type MutationOptions, type MutationState } from './create-mutation.mjs';
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
+ }];
@@ -1,18 +1,28 @@
1
1
  import { type StoreApi } from 'floppy-disk/vanilla';
2
- export declare const useStoreUpdateNotifier: <TState extends Record<string, any>, TStateSlice = TState>(store: StoreApi<TState>, selector: (state: TState) => TStateSlice) => void;
2
+ type Path = Array<string | number | symbol>;
3
+ export declare const getValueByPath: (obj: any, path: Path) => any;
4
+ export declare const isPrefixPath: (candidatePrefix: Path, targetPath: Path) => boolean;
5
+ export declare const compressPaths: (paths: Path[]) => Path[];
6
+ export declare const useStoreStateProxy: <TState extends Record<string, any>>(storeState: TState) => readonly [TState, import("react").RefObject<Path[]>];
3
7
  /**
4
- * React hook for subscribing to a store with optional state selection.
8
+ * React hook for subscribing to a store using automatic dependency tracking.
5
9
  *
6
10
  * @param store - The store instance to subscribe to
7
- * @param selector - Optional selector to derive a slice of state
8
11
  *
9
- * @returns The selected state slice (or full state if no selector is provided)
12
+ * @returns A proxied version of the store state
10
13
  *
11
14
  * @remarks
12
- * - The selector does **not** need to be memoized.
13
- * - The hook internally keeps the latest selector reference to avoid re-subscription.
15
+ * - This hook uses a **Proxy-based tracking mechanism** to detect which parts of the state are accessed during render.
16
+ * - The component will only re-render when the **accessed values actually change**.
17
+ * - State must be treated as **immutable**:
18
+ * - Updates must replace objects rather than mutate them
19
+ * - Otherwise, changes may not be detected
20
+ * - No selector or memoization is needed.
14
21
  *
15
22
  * @example
16
- * const count = useStoreState(store, (s) => s.count);
23
+ * const state = useStoreState(store);
24
+ * return <div>{state.user.name}</div>;
25
+ * // Component will only re-render if `user.name` changes
17
26
  */
18
- export declare const useStoreState: <TState extends Record<string, any>, TStateSlice = TState>(store: StoreApi<TState>, selector?: (state: TState) => TStateSlice) => TStateSlice;
27
+ export declare const useStoreState: <TState extends Record<string, any>>(storeState: TState, subscribe: StoreApi<TState>["subscribe"]) => TState;
28
+ export {};
package/esm/react.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './react/use-isomorphic-layout-effect.mjs';
2
- export * from './react/use-store.mjs';
2
+ export { useStoreState } from './react/use-store.mjs';
3
3
  export * from './react/create-store.mjs';
4
4
  export * from './react/create-stores.mjs';
5
5
  export * from './react/create-query.mjs';
6
- export * from './react/create-mutation.mjs';
6
+ export { createMutation, type MutationOptions, type MutationState, } from './react/create-mutation.mjs';
7
+ export * from './react/use-mutation.mjs';