stunk 2.8.1 → 3.0.0-beta.1

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/dist/index.d.cts CHANGED
@@ -4,51 +4,249 @@ type NamedMiddleware<T> = {
4
4
  name?: string;
5
5
  fn: Middleware<T>;
6
6
  };
7
+ interface ChunkConfig<T> {
8
+ name?: string;
9
+ middleware?: (Middleware<T> | NamedMiddleware<T>)[];
10
+ /**
11
+ * When true, setting a value with keys not present in the initial
12
+ * shape throws an error in development. Useful for catching accidental
13
+ * shape mutations early. Has no effect in production. (default: false)
14
+ */
15
+ strict?: boolean;
16
+ }
7
17
  interface Chunk<T> {
8
18
  /** Get the current value of the chunk. */
9
19
  get: () => T;
20
+ /** Peek at the current value without tracking dependencies. */
21
+ peek: () => T;
10
22
  /** Set a new value for the chunk & Update existing value efficiently. */
11
23
  set: (newValueOrUpdater: T | ((currentValue: T) => T)) => void;
12
24
  /** Subscribe to changes in the chunk. Returns an unsubscribe function. */
13
25
  subscribe: (callback: Subscriber<T>) => () => void;
14
26
  /** Create a derived chunk based on this chunk's value. */
15
- derive: <D>(fn: (value: T) => D) => Chunk<D>;
27
+ derive: <D>(fn: (value: T) => D) => ReadOnlyChunk<D>;
16
28
  /** Reset the chunk to its initial value. */
17
29
  reset: () => void;
18
30
  /** Destroy the chunk and all its subscribers. */
19
31
  destroy: () => void;
20
32
  }
33
+ interface ReadOnlyChunk<T> extends Omit<Chunk<T>, 'set' | 'reset'> {
34
+ derive: <D>(fn: (value: T) => D) => ReadOnlyChunk<D>;
35
+ }
21
36
  /**
22
- * Batch multiple chunk updates into a single re-render.
23
- * Useful for updating multiple chunks at once without causing multiple re-renders.
37
+ * Groups multiple chunk updates into a single notification pass.
38
+ *
39
+ * Without batching, each `set()` call notifies subscribers immediately.
40
+ * Inside a `batch()`, all updates are collected and subscribers are notified
41
+ * once after the callback completes — even if multiple chunks were updated.
42
+ *
43
+ * Batches can be nested safely. Notifications only flush when the outermost
44
+ * batch completes.
45
+ *
46
+ * @param callback - A function containing one or more chunk `set()` calls.
47
+ *
48
+ * @example
49
+ * const x = chunk(0);
50
+ * const y = chunk(0);
51
+ *
52
+ * batch(() => {
53
+ * x.set(1);
54
+ * y.set(2);
55
+ * });
56
+ * // Subscribers of x and y are each notified once, not twice.
24
57
  */
25
58
  declare function batch(callback: () => void): void;
26
- declare function chunk<T>(initialValue: T, middleware?: (Middleware<T> | NamedMiddleware<T>)[]): Chunk<T>;
59
+ /**
60
+ * Creates a reactive state unit — the core primitive of Stunk.
61
+ *
62
+ * A chunk holds a single value and notifies all subscribers whenever that
63
+ * value changes. Values are compared by reference (`===`) for primitives and
64
+ * by the result of middleware processing for objects — so setting the same
65
+ * value twice does not trigger subscribers.
66
+ *
67
+ * @param initialValue - The starting value. Cannot be `undefined`.
68
+ * @param config - Optional configuration for naming, middleware, and strict mode.
69
+ * @returns A `Chunk<T>` with `get()`, `set()`, `peek()`, `subscribe()`, `derive()`, `reset()`, and `destroy()`.
70
+ */
71
+ declare function chunk<T>(initialValue: T, config?: ChunkConfig<T>): Chunk<T>;
27
72
 
28
- type AsyncChunkOpt<T, E extends Error> = {
29
- initialData?: T | null;
30
- onError?: (error: E) => void;
31
- retryCount?: number;
32
- retryDelay?: number;
33
- };
34
- type InferAsyncData<T> = T extends AsyncChunk<infer U, Error> ? U : never;
35
- type CombinedData<T extends Record<string, AsyncChunk<any>>> = {
36
- [K in keyof T]: InferAsyncData<T[K]> | null;
37
- };
38
- type CombinedState<T extends Record<string, AsyncChunk<any>>> = {
39
- loading: boolean;
40
- error: Error | null;
41
- errors: Partial<{
42
- [K in keyof T]: Error;
43
- }>;
44
- data: CombinedData<T>;
45
- };
73
+ interface ChunkMeta {
74
+ name: string;
75
+ id: number;
76
+ }
77
+ declare function isValidChunkValue(value: unknown): boolean;
78
+ declare function isChunk<T>(value: unknown): value is Chunk<T>;
79
+ declare function getChunkMeta<T>(chunk: Chunk<T>): ChunkMeta | undefined;
80
+
81
+ interface Computed<T> extends ReadOnlyChunk<T> {
82
+ /** Checks if the computed value needs to be recalculated due to dependency changes. */
83
+ isDirty: () => boolean;
84
+ /** Manually forces recalculation of the computed value from its dependencies. */
85
+ recompute: () => void;
86
+ }
87
+ /**
88
+ * Creates a derived value that automatically tracks its dependencies and
89
+ * recomputes lazily when any of them change.
90
+ *
91
+ * Dependencies are discovered automatically — any chunk whose `.get()` is
92
+ * called inside `computeFn` is tracked. Use `.peek()` inside the function
93
+ * to read a value without tracking it as a dependency.
94
+ *
95
+ * The computed value is cached and only recalculated when a dependency
96
+ * changes and the value is accessed (lazy) or when active subscribers exist
97
+ * (eager). Object values are compared with shallow equality to prevent
98
+ * unnecessary subscriber notifications.
99
+ *
100
+ * @param computeFn - A pure function that derives the computed value.
101
+ * @returns A read-only `Computed<T>` with `isDirty()`, `recompute()`, `derive()`, `subscribe()`, `peek()`, and `destroy()`.
102
+ */
103
+ declare function computed<T>(computeFn: () => T): Computed<T>;
104
+
105
+ interface SelectOptions {
106
+ /**
107
+ * When `true`, uses shallow equality to compare selected values.
108
+ * Prevents unnecessary subscriber notifications when the selected
109
+ * object is a new reference but has the same property values.
110
+ */
111
+ useShallowEqual?: boolean;
112
+ }
113
+ /**
114
+ * Creates a read-only derived chunk that tracks a slice of a source chunk.
115
+ *
116
+ * Only notifies subscribers when the selected value actually changes —
117
+ * updates to unselected parts of the source are ignored.
118
+ *
119
+ * @param sourceChunk - The chunk to select from.
120
+ * @param selector - A function that extracts the desired slice.
121
+ * @param options.useShallowEqual - When `true`, uses shallow equality to
122
+ * compare selected values — prevents unnecessary updates for objects.
123
+ * @returns A `ReadOnlyChunk<S>` that updates only when the selected value changes.
124
+ *
125
+ * @example
126
+ * const user = chunk({ name: 'Alice', age: 30 });
127
+ * const name = select(user, u => u.name);
128
+ * name.get(); // 'Alice'
129
+ *
130
+ * // age changes — name subscribers are NOT notified
131
+ * user.set({ name: 'Alice', age: 31 });
132
+ *
133
+ * @example
134
+ * // Shallow equality — prevents updates when object values are the same
135
+ * const details = select(user, u => u.details, { useShallowEqual: true });
136
+ */
137
+ declare function select<T, S>(sourceChunk: Chunk<T> | ReadOnlyChunk<T>, selector: (value: T) => S, options?: SelectOptions): ReadOnlyChunk<S>;
138
+
139
+ /**
140
+ * Middleware that logs every value passed to `set()` to the console.
141
+ *
142
+ * @example
143
+ * const count = chunk(0, { middleware: [logger()] });
144
+ * count.set(5); // logs: "Setting value: 5"
145
+ */
146
+ declare function logger<T>(): Middleware<T>;
147
+
148
+ /**
149
+ * Middleware that throws if a numeric value is set below zero.
150
+ *
151
+ * @example
152
+ * const balance = chunk(100, { middleware: [nonNegativeValidator] });
153
+ * balance.set(-1); // throws: "Value must be non-negative!"
154
+ */
155
+ declare const nonNegativeValidator: Middleware<number>;
156
+
157
+ interface ChunkWithHistory<T> extends Chunk<T> {
158
+ /** Reverts to the previous state (if available). */
159
+ undo: () => void;
160
+ /** Moves to the next state (if available). */
161
+ redo: () => void;
162
+ /** Returns true if there is a previous state to revert to. */
163
+ canUndo: () => boolean;
164
+ /** Returns true if there is a next state to move to. */
165
+ canRedo: () => boolean;
166
+ /** Returns an array of all the values in the history. */
167
+ getHistory: () => T[];
168
+ /** Clears the history, keeping only the current value. */
169
+ clearHistory: () => void;
170
+ }
171
+ /**
172
+ * Wraps a chunk with undo/redo history tracking.
173
+ *
174
+ * Every `set()` call is recorded. `undo()` and `redo()` move through the stack.
175
+ * Branching is supported — calling `set()` after `undo()` discards forward history.
176
+ *
177
+ * @param baseChunk - The chunk to wrap.
178
+ * @param options.maxHistory - Max entries to keep (default: 100).
179
+ * @param options.skipDuplicates - `true` skips strictly equal values.
180
+ * `'shallow'` also skips shallowly equal objects.
181
+ *
182
+ * @example
183
+ * const count = chunk(0);
184
+ * const tracked = history(count);
185
+ * tracked.set(1); tracked.set(2);
186
+ * tracked.undo(); // 1
187
+ * tracked.redo(); // 2
188
+ */
189
+ declare function history<T>(baseChunk: Chunk<T>, options?: {
190
+ maxHistory?: number;
191
+ /**
192
+ * true — skip entries that are strictly equal (===) to the current value.
193
+ * 'shallow' — also skip entries that are shallowly equal to the current value.
194
+ */
195
+ skipDuplicates?: boolean | "shallow";
196
+ }): ChunkWithHistory<T>;
197
+
198
+ interface PersistOptions<T> {
199
+ /** Storage key (required). */
200
+ key: string;
201
+ /** Storage engine (default: localStorage). */
202
+ storage?: Storage;
203
+ /** Serialize value to string (default: JSON.stringify). */
204
+ serialize?: (value: T) => string;
205
+ /** Deserialize string to value (default: JSON.parse). */
206
+ deserialize?: (value: string) => T;
207
+ /** Called on load/save errors and type mismatches. */
208
+ onError?: (error: Error, operation: 'load' | 'save') => void;
209
+ }
210
+ interface PersistedChunk<T> extends Chunk<T> {
211
+ /** Remove the persisted key from storage without destroying the chunk. */
212
+ clearStorage: () => void;
213
+ }
214
+ /**
215
+ * Wraps a chunk with automatic persistence to a storage engine.
216
+ *
217
+ * Loads any saved value on creation. Saves on every `set()`.
218
+ * Gracefully disabled in SSR when no storage is available.
219
+ *
220
+ * @param baseChunk - The chunk to wrap.
221
+ * @param options.key - Storage key (required).
222
+ * @param options.storage - Storage engine (default: `localStorage`).
223
+ * @param options.serialize - Custom serializer (default: `JSON.stringify`).
224
+ * @param options.deserialize - Custom deserializer (default: `JSON.parse`).
225
+ * @param options.onError - Called on load/save errors or type mismatches.
226
+ *
227
+ * @example
228
+ * const user = chunk({ name: 'Alice' });
229
+ * const persisted = persist(user, { key: 'user' });
230
+ * persisted.set({ name: 'Bob' }); // saved to localStorage
231
+ * persisted.clearStorage(); // removes the key
232
+ */
233
+ declare function persist<T>(baseChunk: Chunk<T>, options: PersistOptions<T>): PersistedChunk<T>;
234
+
235
+ declare const index$1_history: typeof history;
236
+ declare const index$1_logger: typeof logger;
237
+ declare const index$1_nonNegativeValidator: typeof nonNegativeValidator;
238
+ declare const index$1_persist: typeof persist;
239
+ declare namespace index$1 {
240
+ export { index$1_history as history, index$1_logger as logger, index$1_nonNegativeValidator as nonNegativeValidator, index$1_persist as persist };
241
+ }
46
242
 
47
243
  interface AsyncState<T, E extends Error> {
48
244
  loading: boolean;
49
245
  error: E | null;
50
246
  data: T | null;
51
247
  lastFetched?: number;
248
+ /** True when showing previous data while new data is loading (keepPreviousData: true) */
249
+ isPlaceholderData?: boolean;
52
250
  }
53
251
  interface PaginationState {
54
252
  page: number;
@@ -59,82 +257,111 @@ interface PaginationState {
59
257
  interface AsyncStateWithPagination<T, E extends Error> extends AsyncState<T, E> {
60
258
  pagination?: PaginationState;
61
259
  }
62
- interface RefreshConfig {
63
- /** Time in ms after which data becomes stale */
64
- staleTime?: number;
65
- /** Time in ms to cache data */
66
- cacheTime?: number;
67
- /** Auto-refresh interval in ms */
68
- refetchInterval?: number;
69
- }
70
- interface PaginationConfig {
71
- /** Initial page number (default: 1) */
72
- initialPage?: number;
73
- /** Items per page (default: 10) */
74
- pageSize?: number;
75
- /** Whether to accumulate pages (infinite scroll) or replace */
76
- mode?: 'replace' | 'accumulate';
77
- }
78
- interface AsyncChunkOptExtended<T, E extends Error> extends AsyncChunkOpt<T, E> {
79
- refresh?: RefreshConfig;
80
- pagination?: PaginationConfig;
81
- /** Enable/disable the fetcher */
82
- enabled?: boolean;
83
- }
84
260
  interface FetcherResponse<T> {
85
261
  data: T;
86
262
  total?: number;
87
263
  hasMore?: boolean;
88
264
  }
265
+ interface AsyncChunkOptions<T, E extends Error = Error> {
266
+ /** Deduplication key — concurrent calls with the same key share one in-flight request */
267
+ key?: string;
268
+ /** Seed data shown before the first fetch completes */
269
+ initialData?: T | null;
270
+ /** Disable fetching until ready — pass a function for dynamic evaluation */
271
+ enabled?: boolean | (() => boolean);
272
+ /** Called after every successful fetch */
273
+ onSuccess?: (data: T) => void;
274
+ /** Called when all retries are exhausted */
275
+ onError?: (error: E) => void;
276
+ /** Number of retries on failure (default: 0) */
277
+ retryCount?: number;
278
+ /** Delay in ms between retries (default: 1000) */
279
+ retryDelay?: number;
280
+ /** Show previous data while refetching — prevents UI flicker on param changes (default: false) */
281
+ keepPreviousData?: boolean;
282
+ /** Time in ms before data is considered stale (default: 0) */
283
+ staleTime?: number;
284
+ /** Time in ms to cache data after last subscriber leaves (default: 300_000) */
285
+ cacheTime?: number;
286
+ /** Auto-refetch interval in ms */
287
+ refetchInterval?: number;
288
+ /** Refetch when window regains focus (default: false) */
289
+ refetchOnWindowFocus?: boolean;
290
+ pagination?: {
291
+ /** Initial page number (default: 1) */
292
+ initialPage?: number;
293
+ /** Items per page (default: 10) */
294
+ pageSize?: number;
295
+ /** Replace data on each page load, or accumulate for infinite scroll (default: 'replace') */
296
+ mode?: 'replace' | 'accumulate';
297
+ };
298
+ }
89
299
  interface AsyncChunk<T, E extends Error = Error> extends Chunk<AsyncStateWithPagination<T, E>> {
90
- /** Force reload data */
300
+ /** Force a fresh fetch, ignoring stale time */
91
301
  reload: (params?: any) => Promise<void>;
92
- /** Smart refresh - respects stale time */
302
+ /** Fetch only if data is stale respects staleTime */
93
303
  refresh: (params?: any) => Promise<void>;
94
- /** Mutate data directly */
304
+ /** Update data directly without a network request */
95
305
  mutate: (mutator: (currentData: T | null) => T) => void;
96
- /** Reset to initial state */
306
+ /** Reset to initial state and re-fetch */
97
307
  reset: () => void;
98
- /** Clean up intervals */
308
+ /** Safe cleanup only tears down if no active subscribers remain */
99
309
  cleanup: () => void;
310
+ /** Force cleanup regardless of subscriber count */
311
+ forceCleanup: () => void;
312
+ /** Clear all current params and refetch */
313
+ clearParams: () => void;
100
314
  }
101
315
  interface PaginatedAsyncChunk<T, E extends Error = Error> extends AsyncChunk<T, E> {
102
- /** Load next page */
316
+ /** Load the next page */
103
317
  nextPage: () => Promise<void>;
104
- /** Load previous page */
318
+ /** Load the previous page */
105
319
  prevPage: () => Promise<void>;
106
- /** Go to specific page */
320
+ /** Jump to a specific page */
107
321
  goToPage: (page: number) => Promise<void>;
108
- /** Reset pagination to first page */
322
+ /** Reset pagination to page 1 and re-fetch */
109
323
  resetPagination: () => Promise<void>;
110
324
  }
111
- declare function asyncChunk<T, E extends Error = Error>(fetcher: () => Promise<T | FetcherResponse<T>>, options?: AsyncChunkOptExtended<T, E>): AsyncChunk<T, E> | PaginatedAsyncChunk<T, E>;
325
+ declare function asyncChunk<T, E extends Error = Error>(fetcher: () => Promise<T | FetcherResponse<T>>, options?: AsyncChunkOptions<T, E>): AsyncChunk<T, E> | PaginatedAsyncChunk<T, E>;
112
326
  declare function asyncChunk<T, E extends Error = Error, P extends Record<string, any> = {}>(fetcher: (params: P & {
113
327
  page?: number;
114
328
  pageSize?: number;
115
- }) => Promise<T | FetcherResponse<T>>, options?: AsyncChunkOptExtended<T, E>): (AsyncChunk<T, E> | PaginatedAsyncChunk<T, E>) & {
329
+ }) => Promise<T | FetcherResponse<T>>, options?: AsyncChunkOptions<T, E>): (AsyncChunk<T, E> | PaginatedAsyncChunk<T, E>) & {
116
330
  setParams: (params: Partial<P>) => void;
117
331
  reload: (params?: Partial<P>) => Promise<void>;
118
332
  refresh: (params?: Partial<P>) => Promise<void>;
119
333
  };
120
334
 
121
- interface InfiniteAsyncChunkOptions<T, E extends Error> {
122
- /** Initial page size (default: 10) */
335
+ type InfiniteAsyncChunkOptions<T, E extends Error = Error> = Omit<AsyncChunkOptions<T[], E>, 'pagination'> & {
336
+ /** Items per page (default: 10) */
123
337
  pageSize?: number;
124
- /** Time in ms after which data becomes stale */
125
- staleTime?: number;
126
- /** Time in ms to cache data */
127
- cacheTime?: number;
128
- /** Retry count on error */
129
- retryCount?: number;
130
- /** Delay between retries in ms */
131
- retryDelay?: number;
132
- /** Error callback */
133
- onError?: (error: E) => void;
134
- }
338
+ };
339
+ type InfiniteAsyncChunk<T, E extends Error = Error, P extends Record<string, any> = {}> = PaginatedAsyncChunk<T[], E> & {
340
+ setParams: (params: Partial<Record<keyof P, P[keyof P] | null>>) => void;
341
+ clearParams: () => void;
342
+ reload: (params?: Partial<P>) => Promise<void>;
343
+ refresh: (params?: Partial<P>) => Promise<void>;
344
+ forceCleanup: () => void;
345
+ };
135
346
  /**
136
- * Create an infinite scroll async chunk with accumulate mode.
137
- * Automatically handles pagination in accumulate mode for infinite scrolling.
347
+ * Creates an infinite scroll async chunk that accumulates pages.
348
+ *
349
+ * A convenience wrapper around `asyncChunk` with `pagination.mode: 'accumulate'`
350
+ * pre-configured. Each `nextPage()` appends to the existing data array.
351
+ *
352
+ * @param fetcher - Async function receiving `{ page, pageSize, ...params }`,
353
+ * returning `{ data: T[], hasMore?, total? }`.
354
+ * @param options.pageSize - Items per page (default: 10).
355
+ * @param options.key - Deduplication key.
356
+ * @param options.onSuccess - Called with the full accumulated array after each fetch.
357
+ *
358
+ * @example
359
+ * const posts = infiniteAsyncChunk(
360
+ * async ({ page, pageSize }) => fetchPosts({ page, pageSize }),
361
+ * { pageSize: 20 }
362
+ * );
363
+ * posts.reload(); // page 1
364
+ * posts.nextPage(); // page 2 appended
138
365
  */
139
366
  declare function infiniteAsyncChunk<T, E extends Error = Error, P extends Record<string, any> = {}>(fetcher: (params: P & {
140
367
  page: number;
@@ -143,101 +370,208 @@ declare function infiniteAsyncChunk<T, E extends Error = Error, P extends Record
143
370
  data: T[];
144
371
  hasMore?: boolean;
145
372
  total?: number;
146
- }>, options?: InfiniteAsyncChunkOptions<T, E>): PaginatedAsyncChunk<T[], E> & {
147
- setParams: (params: Partial<P>) => void;
148
- reload: (params?: Partial<P>) => Promise<void>;
149
- refresh: (params?: Partial<P>) => Promise<void>;
150
- };
373
+ }>, options?: InfiniteAsyncChunkOptions<T, E>): InfiniteAsyncChunk<T, E, P>;
151
374
 
152
- type ChunkValue<T> = T extends Chunk<infer U> ? U : never;
153
- type DependencyValues<T extends Chunk<any>[]> = {
154
- [K in keyof T]: T[K] extends Chunk<any> ? ChunkValue<T[K]> : never;
375
+ type InferAsyncData<T> = T extends AsyncChunk<infer U, Error> ? U : never;
376
+ type CombinedData<T extends Record<string, AsyncChunk<any>>> = {
377
+ [K in keyof T]: InferAsyncData<T[K]> | null;
378
+ };
379
+ type CombinedState<T extends Record<string, AsyncChunk<any>>> = {
380
+ loading: boolean;
381
+ error: Error | null;
382
+ errors: Partial<{
383
+ [K in keyof T]: Error;
384
+ }>;
385
+ data: CombinedData<T>;
155
386
  };
156
- interface Computed<T> extends Chunk<T> {
157
- /**
158
- * Checks if the computed value needs to be recalculated due to dependency changes.
159
- * @returns True if the computed value is dirty, false otherwise.
160
- */
161
- isDirty: () => boolean;
162
- /** Manually forces recalculation of the computed value from its dependencies. */
163
- recompute: () => void;
164
- }
165
- declare function computed<TDeps extends Chunk<any>[], TResult>(dependencies: [...TDeps], computeFn: (...args: DependencyValues<TDeps>) => TResult): Computed<TResult>;
166
-
167
- interface SelectOptions {
168
- /**
169
- * Configuration options for selector functions.
170
- * @property {boolean} [useShallowEqual] - When true, performs a shallow equality check
171
- * on the derived selector results to prevent unnecessary updates.
172
- */
173
- useShallowEqual?: boolean;
174
- }
175
- /**
176
- * Creates a derived read-only chunk based on a selector function.
177
- * @param sourceChunk The source chunk to derive from.
178
- * @param selector A function that extracts part of the source value.
179
- * @param options Optional settings for shallow equality comparison.
180
- * @returns A read-only derived chunk.
181
- */
182
- declare function select<T, S>(sourceChunk: Chunk<T>, selector: (value: T) => S, options?: SelectOptions): Chunk<S>;
183
387
 
184
- declare function isValidChunkValue(value: unknown): boolean;
185
- declare function isChunk<T>(value: unknown): value is Chunk<T>;
186
- declare function once<T>(fn: () => T): () => T;
187
388
  /**
188
- * Combines multiple async chunks into a single chunk.
189
- * The combined chunk tracks loading, error, and data states from all source chunks.
389
+ * Combines multiple async chunks into a single unified state chunk.
390
+ *
391
+ * The result tracks `loading` (true if any chunk is loading), `error` (first
392
+ * error encountered), `errors` (per-chunk errors), and `data` (per-chunk data).
393
+ *
394
+ * @param chunks - A record of named `AsyncChunk` instances.
395
+ * @returns A `Chunk<CombinedState<T>>` that reflects the live state of all inputs.
396
+ *
397
+ * @example
398
+ * const combined = combineAsyncChunks({ user: userChunk, posts: postsChunk });
399
+ * combined.get(); // { loading, error, errors, data: { user, posts } }
190
400
  */
191
401
  declare function combineAsyncChunks<T extends Record<string, AsyncChunk<any>>>(chunks: T): Chunk<CombinedState<T>>;
192
402
 
193
- declare const logger: Middleware<any>;
194
-
195
- declare const nonNegativeValidator: Middleware<number>;
196
-
197
- interface ChunkWithHistory<T> extends Chunk<T> {
198
- /**
199
- * Reverts to the previous state (if available).
200
- */
201
- undo: () => void;
202
- /**
203
- * Moves to the next state (if available).
204
- */
205
- redo: () => void;
206
- /**
207
- * Returns true if there is a previous state to revert to.
208
- */
209
- canUndo: () => boolean;
403
+ interface GlobalQueryConfig {
210
404
  /**
211
- * Returns true if there is a next state to move to.
405
+ * Default configuration applied to all asyncChunk instances.
406
+ * Per-chunk options always override these defaults.
212
407
  */
213
- canRedo: () => boolean;
408
+ query?: {
409
+ /** Time in ms before data is considered stale (default: 0) */
410
+ staleTime?: number;
411
+ /** Time in ms to cache data after last subscriber leaves (default: 300_000) */
412
+ cacheTime?: number;
413
+ /** Auto-refetch interval in ms */
414
+ refetchInterval?: number;
415
+ /** Refetch when window regains focus (default: false) */
416
+ refetchOnWindowFocus?: boolean;
417
+ /** Number of retries on failure (default: 0) */
418
+ retryCount?: number;
419
+ /** Delay in ms between retries (default: 1000) */
420
+ retryDelay?: number;
421
+ /** Global error handler — called when all retries are exhausted */
422
+ onError?: (error: Error) => void;
423
+ /** Global success handler — called after every successful fetch */
424
+ onSuccess?: (data: unknown) => void;
425
+ };
214
426
  /**
215
- * Returns an array of all the values in the history.
427
+ * Default configuration applied to all mutation instances.
428
+ * Reserved for when mutation() ships — per-mutation options always override.
216
429
  */
217
- getHistory: () => T[];
218
- /**
219
- * Clears the history, keeping only the current value.
220
- */
221
- clearHistory: () => void;
430
+ mutation?: {
431
+ /** Global error handler for mutations */
432
+ onError?: (error: Error) => void;
433
+ /** Global success handler for mutations */
434
+ onSuccess?: (data: unknown) => void;
435
+ };
222
436
  }
223
- declare function withHistory<T>(baseChunk: Chunk<T>, options?: {
224
- maxHistory?: number;
225
- }): ChunkWithHistory<T>;
437
+ /**
438
+ * Configures global defaults for all `asyncChunk` and `mutation` instances.
439
+ *
440
+ * Call this once at app entry — before any `asyncChunk` is created.
441
+ * Per-chunk options always take precedence over these defaults.
442
+ *
443
+ * @param config.query - Defaults for all async chunks (staleTime, retryCount, onError, etc.)
444
+ * @param config.mutation - Defaults for all mutations (onError, onSuccess)
445
+ *
446
+ * @example
447
+ * import { configureQuery } from "stunk/query";
448
+ *
449
+ * configureQuery({
450
+ * query: {
451
+ * staleTime: 30_000,
452
+ * retryCount: 3,
453
+ * refetchOnWindowFocus: true,
454
+ * onError: (err) => toast.error(err.message),
455
+ * },
456
+ * mutation: {
457
+ * onError: (err) => toast.error(err.message),
458
+ * onSuccess: () => toast.success("Done!"),
459
+ * },
460
+ * });
461
+ */
462
+ declare function configureQuery(config: GlobalQueryConfig): void;
463
+ /**
464
+ * Returns the current global query config.
465
+ * Used internally by asyncChunk and mutation to read defaults.
466
+ */
467
+ declare function getGlobalQueryConfig(): GlobalQueryConfig;
468
+ /**
469
+ * Resets the global config back to defaults.
470
+ * Primarily useful in tests to avoid config bleed between test cases.
471
+ *
472
+ * @example
473
+ * afterEach(() => resetQueryConfig());
474
+ */
475
+ declare function resetQueryConfig(): void;
226
476
 
227
- interface PersistOptions<T> {
228
- key: string;
229
- storage?: Storage;
230
- serialize?: (value: T) => string;
231
- deserialize?: (value: string) => T;
477
+ type MutationFn<TData, TVariables> = (variables: TVariables) => Promise<TData>;
478
+ interface MutationOptions<TData, TError extends Error = Error, TVariables = void> {
479
+ /** Chunks to automatically reload after a successful mutation */
480
+ invalidates?: AsyncChunk<any, any>[];
481
+ /** Called after a successful mutation with the returned data and original variables */
482
+ onSuccess?: (data: TData, variables: TVariables) => void;
483
+ /** Called when the mutation fails with the error and original variables */
484
+ onError?: (error: TError, variables: TVariables) => void;
485
+ /** Called after every attempt — success or failure — useful for unconditional cleanup */
486
+ onSettled?: (data: TData | null, error: TError | null, variables: TVariables) => void;
487
+ }
488
+ interface MutationState<TData, TError extends Error = Error> {
489
+ /** True while the mutation is in progress */
490
+ loading: boolean;
491
+ /** The data returned from the last successful mutation, or null */
492
+ data: TData | null;
493
+ /** The error from the last failed mutation, or null */
494
+ error: TError | null;
495
+ /** True after a successful mutation — distinct from data since data can be null on success */
496
+ isSuccess: boolean;
497
+ }
498
+ interface MutationResult<TData, TError extends Error = Error> {
499
+ /** The returned data on success, or null on failure */
500
+ data: TData | null;
501
+ /** The error on failure, or null on success */
502
+ error: TError | null;
503
+ }
504
+ interface Mutation<TData, TError extends Error = Error, TVariables = void> {
505
+ /**
506
+ * Execute the mutation. Always resolves — never throws.
507
+ * Returns `{ data, error }` so you can await it or fire and forget safely.
508
+ *
509
+ * @example
510
+ * // Fire and forget — safe
511
+ * createPost.mutate({ title: 'Hello' });
512
+ *
513
+ * // Await for local UI control — no try/catch needed
514
+ * const { data, error } = await createPost.mutate({ title: 'Hello' });
515
+ * if (!error) router.push('/posts');
516
+ */
517
+ mutate: (...args: TVariables extends void ? [] : [variables: TVariables]) => Promise<MutationResult<TData, TError>>;
518
+ /** Returns the current mutation state */
519
+ get: () => MutationState<TData, TError>;
520
+ /** Subscribe to state changes. Returns an unsubscribe function. */
521
+ subscribe: (callback: (state: MutationState<TData, TError>) => void) => () => void;
522
+ /** Reset state back to initial — clears data, error, isSuccess */
523
+ reset: () => void;
232
524
  }
233
- declare function withPersistence<T>(baseChunk: Chunk<T>, options: PersistOptions<T>): Chunk<T>;
525
+ /**
526
+ * Creates a reactive mutation for POST, PUT, DELETE, or any async side effect.
527
+ *
528
+ * Always returns a promise that resolves — never throws.
529
+ * On success, automatically reloads any chunks listed in `invalidates`.
530
+ *
531
+ * @param mutationFn - Async function that performs the side effect.
532
+ * @param options.invalidates - Chunks to reload after a successful mutation.
533
+ * @param options.onSuccess - Called with data and variables on success.
534
+ * @param options.onError - Called with error and variables on failure.
535
+ * @param options.onSettled - Called after every attempt regardless of outcome.
536
+ *
537
+ * @example
538
+ * const createPost = mutation(
539
+ * async (data: NewPost) => fetchAPI('/posts', { method: 'POST', body: data }),
540
+ * {
541
+ * invalidates: [postsChunk],
542
+ * onSuccess: (data) => toast.success('Post created!'),
543
+ * onError: (err) => toast.error(err.message),
544
+ * }
545
+ * );
546
+ *
547
+ * // Fire and forget
548
+ * createPost.mutate({ title: 'Hello' });
549
+ *
550
+ * // Await for local control
551
+ * const { data, error } = await createPost.mutate({ title: 'Hello' });
552
+ * if (!error) router.push('/posts');
553
+ */
554
+ declare function mutation<TData, TError extends Error = Error, TVariables = void>(mutationFn: MutationFn<TData, TVariables>, options?: MutationOptions<TData, TError, TVariables>): Mutation<TData, TError, TVariables>;
234
555
 
235
- declare const index_logger: typeof logger;
236
- declare const index_nonNegativeValidator: typeof nonNegativeValidator;
237
- declare const index_withHistory: typeof withHistory;
238
- declare const index_withPersistence: typeof withPersistence;
556
+ type index_AsyncChunk<T, E extends Error = Error> = AsyncChunk<T, E>;
557
+ type index_AsyncState<T, E extends Error> = AsyncState<T, E>;
558
+ type index_AsyncStateWithPagination<T, E extends Error> = AsyncStateWithPagination<T, E>;
559
+ type index_GlobalQueryConfig = GlobalQueryConfig;
560
+ type index_Mutation<TData, TError extends Error = Error, TVariables = void> = Mutation<TData, TError, TVariables>;
561
+ type index_MutationFn<TData, TVariables> = MutationFn<TData, TVariables>;
562
+ type index_MutationOptions<TData, TError extends Error = Error, TVariables = void> = MutationOptions<TData, TError, TVariables>;
563
+ type index_MutationResult<TData, TError extends Error = Error> = MutationResult<TData, TError>;
564
+ type index_MutationState<TData, TError extends Error = Error> = MutationState<TData, TError>;
565
+ type index_PaginatedAsyncChunk<T, E extends Error = Error> = PaginatedAsyncChunk<T, E>;
566
+ declare const index_asyncChunk: typeof asyncChunk;
567
+ declare const index_combineAsyncChunks: typeof combineAsyncChunks;
568
+ declare const index_configureQuery: typeof configureQuery;
569
+ declare const index_getGlobalQueryConfig: typeof getGlobalQueryConfig;
570
+ declare const index_infiniteAsyncChunk: typeof infiniteAsyncChunk;
571
+ declare const index_mutation: typeof mutation;
572
+ declare const index_resetQueryConfig: typeof resetQueryConfig;
239
573
  declare namespace index {
240
- export { index_logger as logger, index_nonNegativeValidator as nonNegativeValidator, index_withHistory as withHistory, index_withPersistence as withPersistence };
574
+ export { type index_AsyncChunk as AsyncChunk, type index_AsyncState as AsyncState, type index_AsyncStateWithPagination as AsyncStateWithPagination, type index_GlobalQueryConfig as GlobalQueryConfig, type index_Mutation as Mutation, type index_MutationFn as MutationFn, type index_MutationOptions as MutationOptions, type index_MutationResult as MutationResult, type index_MutationState as MutationState, type index_PaginatedAsyncChunk as PaginatedAsyncChunk, index_asyncChunk as asyncChunk, index_combineAsyncChunks as combineAsyncChunks, index_configureQuery as configureQuery, index_getGlobalQueryConfig as getGlobalQueryConfig, index_infiniteAsyncChunk as infiniteAsyncChunk, index_mutation as mutation, index_resetQueryConfig as resetQueryConfig };
241
575
  }
242
576
 
243
- export { type AsyncChunk, type AsyncState, type AsyncStateWithPagination, type Chunk, type Middleware, type PaginatedAsyncChunk, asyncChunk, batch, chunk, combineAsyncChunks, computed, infiniteAsyncChunk, isChunk, isValidChunkValue, index as middleware, once, select };
577
+ export { type Chunk, type Middleware, batch, chunk, computed, getChunkMeta, isChunk, isValidChunkValue, index$1 as middleware, index as query, select };