@krymskyimaksym/react-api-client 2.0.0-beta.0 → 2.0.0-beta.2

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 CHANGED
@@ -290,6 +290,70 @@ const httpClient = {
290
290
  };
291
291
  ```
292
292
 
293
+ ## `mutate` vs `mutateAsync`
294
+
295
+ `useMutation` возвращает оба варианта вызова — намеренно с разными типами.
296
+
297
+ ```ts
298
+ const { mutate, mutateAsync } = api.useMutation();
299
+
300
+ // ❌ Анти-паттерн: mutate возвращает void, не Promise.
301
+ // `await` отдаст undefined; try/catch не сработает.
302
+ await mutate({ id: 1 });
303
+
304
+ // ✅ Правильно: для последовательной логики или try/catch — mutateAsync.
305
+ try {
306
+ const result = await mutateAsync({ id: 1 });
307
+ } catch (e) {
308
+ // обработка
309
+ }
310
+
311
+ // ✅ Правильно: для fire-and-forget (кнопка с onClick) — mutate.
312
+ <Button onPress={() => mutate({ id: 1 })} />;
313
+ ```
314
+
315
+ Если включён `throwOnError: true` — для критичных мутаций используй
316
+ `mutateAsync` и обёртку `try/catch`, иначе ошибка не будет
317
+ поймана в caller'е.
318
+
319
+ ## SSR / hydrate
320
+
321
+ Кэш сериализуется через `cache.dehydrate()` и восстанавливается через
322
+ `cache.hydrate(state)`. Стандартный SSR-паттерн:
323
+
324
+ ```ts
325
+ // На сервере
326
+ const client = new QueryClient();
327
+ await client.fetchQuery(['orders'], () => fetchOrders());
328
+ const dehydratedState = client.cache.dehydrate();
329
+
330
+ // Встраиваем в HTML
331
+ res.send(`
332
+ <html>
333
+ <body>
334
+ <div id="root">${renderToString(<App />)}</div>
335
+ <script>
336
+ window.__APP_DATA__ = ${JSON.stringify(dehydratedState)};
337
+ </script>
338
+ </body>
339
+ </html>
340
+ `);
341
+
342
+ // На клиенте
343
+ const client = new QueryClient();
344
+ client.cache.hydrate(window.__APP_DATA__);
345
+ ReactDOM.hydrateRoot(
346
+ document.getElementById('root'),
347
+ <ApiClientProvider client={client}>
348
+ <App />
349
+ </ApiClientProvider>,
350
+ );
351
+ ```
352
+
353
+ Гидратированные данные сразу помечаются как `isStale: true` → подписанные
354
+ `useFetch` отдают серверные данные мгновенно и в фоне делают refetch для
355
+ проверки актуальности.
356
+
293
357
  ## License
294
358
 
295
359
  MIT
package/dist/index.d.mts CHANGED
@@ -34,11 +34,29 @@ type QueryEntry<T> = {
34
34
  state: QueryState<T>;
35
35
  subscribers: Set<Listener$2>;
36
36
  inflight: Promise<T> | null;
37
+ inflightController: AbortController | null;
37
38
  gcTimer: ReturnType<typeof setTimeout> | null;
38
39
  staleTime: number;
39
40
  gcTime: number;
41
+ /**
42
+ * Последний queryFn, переданный в fetch для этого ключа.
43
+ * Используется refetchQueries: позволяет перезапустить запрос,
44
+ * не зная queryFn из caller'а (например, push-handler).
45
+ */
46
+ lastQueryFn: QueryFn<T> | null;
47
+ };
48
+ type QueryFnContext = {
49
+ signal: AbortSignal;
40
50
  };
41
- type QueryFn<T> = () => Promise<T>;
51
+ /**
52
+ * queryFn принимает контекст с AbortSignal. Если HTTP-клиент его
53
+ * использует — отмена будет реальной; если игнорирует — поведение
54
+ * деградирует до текущего (запрос идёт до конца, но кэш игнорирует результат).
55
+ *
56
+ * Для обратной совместимости старая сигнатура `() => Promise<T>` тоже
57
+ * принимается — пакет просто не передаст signal внутрь.
58
+ */
59
+ type QueryFn<T> = (ctx: QueryFnContext) => Promise<T>;
42
60
  type FetchOptions = {
43
61
  staleTime?: number;
44
62
  gcTime?: number;
@@ -51,6 +69,14 @@ type FetchOptions = {
51
69
  */
52
70
  declare class QueryCache {
53
71
  private entries;
72
+ private globalListeners;
73
+ /**
74
+ * Подписка на любое изменение кэша: setData, invalidate, remove,
75
+ * успешный/ошибочный fetch. Используется persistQueryClient и
76
+ * devtools-подобными адаптерами. Не дублирует `subscribe(key, ...)`.
77
+ */
78
+ subscribeAll(listener: Listener$2): () => void;
79
+ private notifyGlobal;
54
80
  private ensureEntry;
55
81
  getState<T>(key: QueryKey): QueryState<T> | undefined;
56
82
  getData<T>(key: QueryKey): T | undefined;
@@ -72,6 +98,13 @@ declare class QueryCache {
72
98
  * Если есть подписчики — они получат уведомление, чтобы инициировать refetch.
73
99
  */
74
100
  invalidate(predicate: QueryKey | ((key: QueryKey) => boolean)): string[];
101
+ /**
102
+ * Перезапускает все записи, матчинг predicate, у которых сохранён
103
+ * `lastQueryFn` (т.е. их хоть раз кто-то загрузил через `fetch`).
104
+ * Возвращает promise, который резолвится когда все запросы завершились.
105
+ * Ошибки отдельных запросов проглатываются — общий promise успешный.
106
+ */
107
+ refetchQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): Promise<void>;
75
108
  /**
76
109
  * Отменяет «привязку» inflight-промиса к ключу. Сам HTTP-запрос
77
110
  * продолжит исполняться (executeRequest не использует AbortSignal),
@@ -84,6 +117,12 @@ declare class QueryCache {
84
117
  * Используется редко — обычно достаточно invalidate.
85
118
  */
86
119
  remove(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
120
+ /**
121
+ * Количество inflight-запросов в кэше, опционально отфильтрованных
122
+ * по predicate. Используется `useIsFetching()` для глобального
123
+ * индикатора загрузки.
124
+ */
125
+ countFetching(predicate?: QueryKey | ((key: QueryKey) => boolean)): number;
87
126
  /** Только для тестов / DevTools. */
88
127
  _debugEntries(): ReadonlyMap<string, QueryEntry<unknown>>;
89
128
  /**
@@ -121,6 +160,12 @@ declare class QueryClient {
121
160
  invalidateQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
122
161
  removeQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
123
162
  cancelQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
163
+ refetchQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): Promise<void>;
164
+ /**
165
+ * Кладёт запрос в кэш, не пробрасывая ошибки. Подходит для оптимистичной
166
+ * подгрузки следующего экрана при наведении / долгом тапе.
167
+ */
168
+ prefetchQuery<T>(key: QueryKey, queryFn: QueryFn<T>, options?: FetchOptions): Promise<void>;
124
169
  }
125
170
  /** Singleton-доступ для тех мест, где Provider недоступен (push-handler и т.п.). */
126
171
  declare function getQueryClient(): QueryClient;
@@ -136,6 +181,18 @@ type ResponseWrapper<DataType, ErrorsType = unknown> = {
136
181
  errors?: ErrorsType;
137
182
  } & DataType;
138
183
  type UseFetchOptions<T, TSelected = T> = {
184
+ /**
185
+ * При `false`:
186
+ * - запрос НЕ инициируется (ни на mount, ни на focus/reconnect/poll);
187
+ * - но хук **подписан на ключ кэша** и перерисуется, если данные
188
+ * обновит другой источник (другой `useFetch` с тем же ключом,
189
+ * `setQueryData` / `invalidateQueries`, мутация, push-handler).
190
+ *
191
+ * То есть `enabled: false` превращает хук в read-only слушателя.
192
+ * Если нужно полностью «потушить» хук — просто не вызывай его.
193
+ *
194
+ * По умолчанию `true`.
195
+ */
139
196
  enabled?: boolean;
140
197
  refetchOnMount?: boolean;
141
198
  /** Refetch при возврате на экран / в браузерное окно. */
@@ -216,11 +273,16 @@ type UseMutationOptions<TData, TVariables, TContext = unknown> = {
216
273
  onError?: (error: Error, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
217
274
  onSettled?: (data: TData | null, error: Error | null, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
218
275
  /**
219
- * Ключи кэша, которые надо инвалидировать после успеха.
220
- * Может быть массивом ключей или функцией, считающей их по vars/data.
221
- * Каждый ключ матчится по префиксу (см. matchQueryKey).
276
+ * Что инвалидировать в кэше после успешной мутации.
277
+ *
278
+ * Три формы:
279
+ * - `QueryKey[]` — массив префиксов; матч по `matchQueryKey`
280
+ * - `(vars, data) => QueryKey[]` — динамический список префиксов
281
+ * - `(vars, data) => (key: QueryKey) => boolean` — произвольный предикат
282
+ * по каждому ключу кэша (например «инвалидируй всё, где встречается
283
+ * этот orderId, в любой позиции ключа»).
222
284
  */
223
- invalidateKeys?: QueryKey[] | ((vars: TVariables, data: TData) => QueryKey[]);
285
+ invalidateKeys?: QueryKey[] | ((vars: TVariables, data: TData) => QueryKey[]) | ((vars: TVariables, data: TData) => (key: QueryKey) => boolean);
224
286
  /**
225
287
  * Точечно патчит кэш после успеха — до invalidate. Удобно для
226
288
  * «сервер вернул свежий объект, положим его прямо в ['orders', id]».
@@ -240,12 +302,29 @@ type UseMutationResult<TData, TVariables> = {
240
302
  interface IHttpClient {
241
303
  get<T>(url: string, config?: {
242
304
  params?: Record<string, unknown>;
305
+ signal?: AbortSignal;
243
306
  }): Promise<T>;
244
307
  request<T>(url: string, config: {
245
308
  method?: string;
246
309
  data?: Record<string, unknown>;
310
+ signal?: AbortSignal;
247
311
  }): Promise<T>;
248
312
  }
313
+ /**
314
+ * Колбэки для логирования / отладки. Все опциональны.
315
+ * Пробрасываются в Reactotron / Flipper или просто в console.log.
316
+ * Ошибки в самих колбэках проглатываются — логгер не должен ломать
317
+ * приложение.
318
+ */
319
+ type ApiClientLogger = {
320
+ onFetchStart?: (key: readonly unknown[]) => void;
321
+ onFetchSuccess?: (key: readonly unknown[], data: unknown) => void;
322
+ onFetchError?: (key: readonly unknown[], error: unknown) => void;
323
+ onInvalidate?: (invalidatedHashes: string[]) => void;
324
+ onMutationStart?: (endpoint: string, vars: unknown) => void;
325
+ onMutationSuccess?: (endpoint: string, vars: unknown, data: unknown) => void;
326
+ onMutationError?: (endpoint: string, vars: unknown, error: unknown) => void;
327
+ };
249
328
  type ApiClientConfig = {
250
329
  httpClient: IHttpClient;
251
330
  onUnauthorized?: () => void | Promise<void>;
@@ -257,10 +336,12 @@ type ApiClientConfig = {
257
336
  * `await *.fetch()` обёрнуты в try/catch.
258
337
  */
259
338
  throwOnError?: boolean;
339
+ /** Опциональный логгер для отладки. */
340
+ logger?: ApiClientLogger;
260
341
  };
261
342
  type ApiClientReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
262
343
  fetch: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
263
- useFetch: (params?: RequestParamsType, options?: UseFetchOptions<ResponseWrapper<ResponseType, ErrorResponseType>>) => UseFetchResult<ResponseWrapper<ResponseType, ErrorResponseType>>;
344
+ useFetch: <TSelected = ResponseWrapper<ResponseType, ErrorResponseType>>(params?: RequestParamsType, options?: UseFetchOptions<ResponseWrapper<ResponseType, ErrorResponseType>, TSelected>) => UseFetchResult<TSelected>;
264
345
  };
265
346
  type ApiMutationReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
266
347
  mutate: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
@@ -320,6 +401,36 @@ declare class ApiError<E = unknown> extends Error {
320
401
  readonly raw: unknown;
321
402
  constructor(init: ApiErrorInit<E>);
322
403
  }
404
+ /**
405
+ * Конвертация произвольного throw'нутого значения в `ApiError`.
406
+ * Покрывает 4 кейса:
407
+ * - `ApiError` → passthrough
408
+ * - axios-like `{ response: { status, data } }` → ApiError с полями из data
409
+ * - `Error` (сетевая ошибка, таймаут) → ApiError(isNetworkError, status: 0)
410
+ * - неизвестный объект → ApiError(message: 'Unknown error', status: 0)
411
+ */
412
+ declare function toApiError(thrown: unknown): ApiError;
413
+ /**
414
+ * Для 2xx ответа, в теле которого Laravel-style `{ status: false }`.
415
+ * Возвращает ApiError(status: 200, ...). Вызывается из executeRequest
416
+ * только при `throwOnError: true`.
417
+ */
418
+ declare function businessErrorToApiError(response: unknown): ApiError;
419
+
420
+ /**
421
+ * Возвращает число активных (inflight) запросов в кэше.
422
+ * Полезно для глобального индикатора загрузки в шапке.
423
+ *
424
+ * С опциональным predicate — считает только матчинг запросы:
425
+ * `useIsFetching(['orders'])` — сколько inflight'ов под префиксом ['orders'].
426
+ */
427
+ declare function useIsFetching(predicate?: QueryKey | ((key: QueryKey) => boolean)): number;
428
+ /**
429
+ * Возвращает число активных мутаций. С predicate — только матчинг.
430
+ * Скоуп мутации = первый ключ из её `invalidateKeys`, если задан массив;
431
+ * иначе — без скоупа (попадает только в безусловный счётчик).
432
+ */
433
+ declare function useIsMutating(predicate?: QueryKey | ((scope: QueryKey | undefined) => boolean)): number;
323
434
 
324
435
  /**
325
436
  * Storage-адаптер. Совместим с AsyncStorage и expo-secure-store:
@@ -350,14 +461,17 @@ type PersistOptions = {
350
461
  };
351
462
  /**
352
463
  * Подключает QueryClient к persistent storage.
353
- * Возвращает { restore, persist, unsubscribe }:
354
- * - `restore()`гидратирует кэш из storage (вызывать на старте приложения).
464
+ *
465
+ * С версии 2.0.0 подписан на `cache.subscribeAll`, поэтому
466
+ * автоматически сохраняет состояние через `throttleMs` после любого
467
+ * изменения (setData, успешный fetch, invalidate, remove). Ручной
468
+ * `persist()` остаётся доступным для критичных моментов (logout,
469
+ * shutdown), но в обычном потоке не нужен.
470
+ *
471
+ * Возвращает:
472
+ * - `restore()` — гидратирует кэш из storage. Вызывать на старте.
355
473
  * - `persist()` — форс-запись текущего состояния.
356
474
  * - `unsubscribe()` — отключить авто-сохранение.
357
- *
358
- * Простая модель: на каждое изменение через client.cache._debugEntries
359
- * нет подписки, поэтому persist вызываем вручную из мутаций / по таймеру.
360
- * Здесь — таймер по throttleMs. Этого хватает для чатов/архива.
361
475
  */
362
476
  declare function persistQueryClient(options: PersistOptions): {
363
477
  restore: () => Promise<void>;
@@ -475,7 +589,21 @@ declare function apiClient<ResponseType = void, RequestParamsType = void, ErrorR
475
589
  */
476
590
  declare function apiMutation<ResponseType = void, RequestParamsType = void, ErrorResponseType = unknown>(endpoint: string | ((arg0: RequestParamsType) => string), fetchConfig?: RequestConfig): ApiMutationReturn<ResponseType, RequestParamsType, ErrorResponseType>;
477
591
  /**
478
- * Creates an API client for paginated requests
592
+ * Creates an API client for paginated requests.
593
+ *
594
+ * Каждая страница хранится в кэше под собственным подключом
595
+ * `['__paginate__', endpoint, params, { page, limit }]`. Это значит, что
596
+ * мутация может одной операцией пометить stale **все страницы** списка:
597
+ *
598
+ * ```ts
599
+ * confirmOrder.useMutation({
600
+ * invalidateKeys: [['__paginate__', '/orders/archive']],
601
+ * });
602
+ * ```
603
+ *
604
+ * Префикс матчится по `matchQueryKey` → подключи каждой страницы тоже
605
+ * считаются совпадающими.
606
+ *
479
607
  * @param endpoint - URL string or function that generates URL from params
480
608
  * @param fetchConfig - Request configuration
481
609
  * @param options - Pagination options (data/total extractors)
@@ -494,4 +622,4 @@ declare function apiPaginate<ResponseType extends {
494
622
  totalExtractor?: (response: ResponseType) => number;
495
623
  }): ApiPaginateReturn<ResponseType, RequestParamsType, TData, ErrorResponseType>;
496
624
 
497
- export { type ApiClientConfig, ApiClientProvider, type ApiClientProviderProps, ApiError, type ApiErrorInit, type CacheEntrySnapshot, type DehydratedQuery, type DehydratedState, type FetchOptions, type IHttpClient, type PersistOptions, type PersistStorage, QueryCache, QueryClient, type QueryFn, type QueryKey, type QueryState, type QueryStatus, type ResponseWrapper, type UseFetchOptions, type UseFetchResult, type UseMutationOptions, type UseMutationResult, type UsePaginateOptions, type UsePaginateResult, apiMutation, apiPaginate, configureApiClient, apiClient as default, focusManager, getConfig, getQueryClient, hashQueryKey, inspectCache, invalidateAll, isConfigured, matchQueryKey, onlineManager, persistQueryClient, setQueryClient, summarizeCache, useQueryClient };
625
+ export { type ApiClientConfig, type ApiClientLogger, ApiClientProvider, type ApiClientProviderProps, ApiError, type ApiErrorInit, type CacheEntrySnapshot, type DehydratedQuery, type DehydratedState, type FetchOptions, type IHttpClient, type PersistOptions, type PersistStorage, QueryCache, QueryClient, type QueryFn, type QueryKey, type QueryState, type QueryStatus, type ResponseWrapper, type UseFetchOptions, type UseFetchResult, type UseMutationOptions, type UseMutationResult, type UsePaginateOptions, type UsePaginateResult, apiMutation, apiPaginate, businessErrorToApiError, configureApiClient, apiClient as default, focusManager, getConfig, getQueryClient, hashQueryKey, inspectCache, invalidateAll, isConfigured, matchQueryKey, onlineManager, persistQueryClient, setQueryClient, summarizeCache, toApiError, useIsFetching, useIsMutating, useQueryClient };
package/dist/index.d.ts CHANGED
@@ -34,11 +34,29 @@ type QueryEntry<T> = {
34
34
  state: QueryState<T>;
35
35
  subscribers: Set<Listener$2>;
36
36
  inflight: Promise<T> | null;
37
+ inflightController: AbortController | null;
37
38
  gcTimer: ReturnType<typeof setTimeout> | null;
38
39
  staleTime: number;
39
40
  gcTime: number;
41
+ /**
42
+ * Последний queryFn, переданный в fetch для этого ключа.
43
+ * Используется refetchQueries: позволяет перезапустить запрос,
44
+ * не зная queryFn из caller'а (например, push-handler).
45
+ */
46
+ lastQueryFn: QueryFn<T> | null;
47
+ };
48
+ type QueryFnContext = {
49
+ signal: AbortSignal;
40
50
  };
41
- type QueryFn<T> = () => Promise<T>;
51
+ /**
52
+ * queryFn принимает контекст с AbortSignal. Если HTTP-клиент его
53
+ * использует — отмена будет реальной; если игнорирует — поведение
54
+ * деградирует до текущего (запрос идёт до конца, но кэш игнорирует результат).
55
+ *
56
+ * Для обратной совместимости старая сигнатура `() => Promise<T>` тоже
57
+ * принимается — пакет просто не передаст signal внутрь.
58
+ */
59
+ type QueryFn<T> = (ctx: QueryFnContext) => Promise<T>;
42
60
  type FetchOptions = {
43
61
  staleTime?: number;
44
62
  gcTime?: number;
@@ -51,6 +69,14 @@ type FetchOptions = {
51
69
  */
52
70
  declare class QueryCache {
53
71
  private entries;
72
+ private globalListeners;
73
+ /**
74
+ * Подписка на любое изменение кэша: setData, invalidate, remove,
75
+ * успешный/ошибочный fetch. Используется persistQueryClient и
76
+ * devtools-подобными адаптерами. Не дублирует `subscribe(key, ...)`.
77
+ */
78
+ subscribeAll(listener: Listener$2): () => void;
79
+ private notifyGlobal;
54
80
  private ensureEntry;
55
81
  getState<T>(key: QueryKey): QueryState<T> | undefined;
56
82
  getData<T>(key: QueryKey): T | undefined;
@@ -72,6 +98,13 @@ declare class QueryCache {
72
98
  * Если есть подписчики — они получат уведомление, чтобы инициировать refetch.
73
99
  */
74
100
  invalidate(predicate: QueryKey | ((key: QueryKey) => boolean)): string[];
101
+ /**
102
+ * Перезапускает все записи, матчинг predicate, у которых сохранён
103
+ * `lastQueryFn` (т.е. их хоть раз кто-то загрузил через `fetch`).
104
+ * Возвращает promise, который резолвится когда все запросы завершились.
105
+ * Ошибки отдельных запросов проглатываются — общий promise успешный.
106
+ */
107
+ refetchQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): Promise<void>;
75
108
  /**
76
109
  * Отменяет «привязку» inflight-промиса к ключу. Сам HTTP-запрос
77
110
  * продолжит исполняться (executeRequest не использует AbortSignal),
@@ -84,6 +117,12 @@ declare class QueryCache {
84
117
  * Используется редко — обычно достаточно invalidate.
85
118
  */
86
119
  remove(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
120
+ /**
121
+ * Количество inflight-запросов в кэше, опционально отфильтрованных
122
+ * по predicate. Используется `useIsFetching()` для глобального
123
+ * индикатора загрузки.
124
+ */
125
+ countFetching(predicate?: QueryKey | ((key: QueryKey) => boolean)): number;
87
126
  /** Только для тестов / DevTools. */
88
127
  _debugEntries(): ReadonlyMap<string, QueryEntry<unknown>>;
89
128
  /**
@@ -121,6 +160,12 @@ declare class QueryClient {
121
160
  invalidateQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
122
161
  removeQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
123
162
  cancelQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
163
+ refetchQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): Promise<void>;
164
+ /**
165
+ * Кладёт запрос в кэш, не пробрасывая ошибки. Подходит для оптимистичной
166
+ * подгрузки следующего экрана при наведении / долгом тапе.
167
+ */
168
+ prefetchQuery<T>(key: QueryKey, queryFn: QueryFn<T>, options?: FetchOptions): Promise<void>;
124
169
  }
125
170
  /** Singleton-доступ для тех мест, где Provider недоступен (push-handler и т.п.). */
126
171
  declare function getQueryClient(): QueryClient;
@@ -136,6 +181,18 @@ type ResponseWrapper<DataType, ErrorsType = unknown> = {
136
181
  errors?: ErrorsType;
137
182
  } & DataType;
138
183
  type UseFetchOptions<T, TSelected = T> = {
184
+ /**
185
+ * При `false`:
186
+ * - запрос НЕ инициируется (ни на mount, ни на focus/reconnect/poll);
187
+ * - но хук **подписан на ключ кэша** и перерисуется, если данные
188
+ * обновит другой источник (другой `useFetch` с тем же ключом,
189
+ * `setQueryData` / `invalidateQueries`, мутация, push-handler).
190
+ *
191
+ * То есть `enabled: false` превращает хук в read-only слушателя.
192
+ * Если нужно полностью «потушить» хук — просто не вызывай его.
193
+ *
194
+ * По умолчанию `true`.
195
+ */
139
196
  enabled?: boolean;
140
197
  refetchOnMount?: boolean;
141
198
  /** Refetch при возврате на экран / в браузерное окно. */
@@ -216,11 +273,16 @@ type UseMutationOptions<TData, TVariables, TContext = unknown> = {
216
273
  onError?: (error: Error, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
217
274
  onSettled?: (data: TData | null, error: Error | null, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
218
275
  /**
219
- * Ключи кэша, которые надо инвалидировать после успеха.
220
- * Может быть массивом ключей или функцией, считающей их по vars/data.
221
- * Каждый ключ матчится по префиксу (см. matchQueryKey).
276
+ * Что инвалидировать в кэше после успешной мутации.
277
+ *
278
+ * Три формы:
279
+ * - `QueryKey[]` — массив префиксов; матч по `matchQueryKey`
280
+ * - `(vars, data) => QueryKey[]` — динамический список префиксов
281
+ * - `(vars, data) => (key: QueryKey) => boolean` — произвольный предикат
282
+ * по каждому ключу кэша (например «инвалидируй всё, где встречается
283
+ * этот orderId, в любой позиции ключа»).
222
284
  */
223
- invalidateKeys?: QueryKey[] | ((vars: TVariables, data: TData) => QueryKey[]);
285
+ invalidateKeys?: QueryKey[] | ((vars: TVariables, data: TData) => QueryKey[]) | ((vars: TVariables, data: TData) => (key: QueryKey) => boolean);
224
286
  /**
225
287
  * Точечно патчит кэш после успеха — до invalidate. Удобно для
226
288
  * «сервер вернул свежий объект, положим его прямо в ['orders', id]».
@@ -240,12 +302,29 @@ type UseMutationResult<TData, TVariables> = {
240
302
  interface IHttpClient {
241
303
  get<T>(url: string, config?: {
242
304
  params?: Record<string, unknown>;
305
+ signal?: AbortSignal;
243
306
  }): Promise<T>;
244
307
  request<T>(url: string, config: {
245
308
  method?: string;
246
309
  data?: Record<string, unknown>;
310
+ signal?: AbortSignal;
247
311
  }): Promise<T>;
248
312
  }
313
+ /**
314
+ * Колбэки для логирования / отладки. Все опциональны.
315
+ * Пробрасываются в Reactotron / Flipper или просто в console.log.
316
+ * Ошибки в самих колбэках проглатываются — логгер не должен ломать
317
+ * приложение.
318
+ */
319
+ type ApiClientLogger = {
320
+ onFetchStart?: (key: readonly unknown[]) => void;
321
+ onFetchSuccess?: (key: readonly unknown[], data: unknown) => void;
322
+ onFetchError?: (key: readonly unknown[], error: unknown) => void;
323
+ onInvalidate?: (invalidatedHashes: string[]) => void;
324
+ onMutationStart?: (endpoint: string, vars: unknown) => void;
325
+ onMutationSuccess?: (endpoint: string, vars: unknown, data: unknown) => void;
326
+ onMutationError?: (endpoint: string, vars: unknown, error: unknown) => void;
327
+ };
249
328
  type ApiClientConfig = {
250
329
  httpClient: IHttpClient;
251
330
  onUnauthorized?: () => void | Promise<void>;
@@ -257,10 +336,12 @@ type ApiClientConfig = {
257
336
  * `await *.fetch()` обёрнуты в try/catch.
258
337
  */
259
338
  throwOnError?: boolean;
339
+ /** Опциональный логгер для отладки. */
340
+ logger?: ApiClientLogger;
260
341
  };
261
342
  type ApiClientReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
262
343
  fetch: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
263
- useFetch: (params?: RequestParamsType, options?: UseFetchOptions<ResponseWrapper<ResponseType, ErrorResponseType>>) => UseFetchResult<ResponseWrapper<ResponseType, ErrorResponseType>>;
344
+ useFetch: <TSelected = ResponseWrapper<ResponseType, ErrorResponseType>>(params?: RequestParamsType, options?: UseFetchOptions<ResponseWrapper<ResponseType, ErrorResponseType>, TSelected>) => UseFetchResult<TSelected>;
264
345
  };
265
346
  type ApiMutationReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
266
347
  mutate: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
@@ -320,6 +401,36 @@ declare class ApiError<E = unknown> extends Error {
320
401
  readonly raw: unknown;
321
402
  constructor(init: ApiErrorInit<E>);
322
403
  }
404
+ /**
405
+ * Конвертация произвольного throw'нутого значения в `ApiError`.
406
+ * Покрывает 4 кейса:
407
+ * - `ApiError` → passthrough
408
+ * - axios-like `{ response: { status, data } }` → ApiError с полями из data
409
+ * - `Error` (сетевая ошибка, таймаут) → ApiError(isNetworkError, status: 0)
410
+ * - неизвестный объект → ApiError(message: 'Unknown error', status: 0)
411
+ */
412
+ declare function toApiError(thrown: unknown): ApiError;
413
+ /**
414
+ * Для 2xx ответа, в теле которого Laravel-style `{ status: false }`.
415
+ * Возвращает ApiError(status: 200, ...). Вызывается из executeRequest
416
+ * только при `throwOnError: true`.
417
+ */
418
+ declare function businessErrorToApiError(response: unknown): ApiError;
419
+
420
+ /**
421
+ * Возвращает число активных (inflight) запросов в кэше.
422
+ * Полезно для глобального индикатора загрузки в шапке.
423
+ *
424
+ * С опциональным predicate — считает только матчинг запросы:
425
+ * `useIsFetching(['orders'])` — сколько inflight'ов под префиксом ['orders'].
426
+ */
427
+ declare function useIsFetching(predicate?: QueryKey | ((key: QueryKey) => boolean)): number;
428
+ /**
429
+ * Возвращает число активных мутаций. С predicate — только матчинг.
430
+ * Скоуп мутации = первый ключ из её `invalidateKeys`, если задан массив;
431
+ * иначе — без скоупа (попадает только в безусловный счётчик).
432
+ */
433
+ declare function useIsMutating(predicate?: QueryKey | ((scope: QueryKey | undefined) => boolean)): number;
323
434
 
324
435
  /**
325
436
  * Storage-адаптер. Совместим с AsyncStorage и expo-secure-store:
@@ -350,14 +461,17 @@ type PersistOptions = {
350
461
  };
351
462
  /**
352
463
  * Подключает QueryClient к persistent storage.
353
- * Возвращает { restore, persist, unsubscribe }:
354
- * - `restore()`гидратирует кэш из storage (вызывать на старте приложения).
464
+ *
465
+ * С версии 2.0.0 подписан на `cache.subscribeAll`, поэтому
466
+ * автоматически сохраняет состояние через `throttleMs` после любого
467
+ * изменения (setData, успешный fetch, invalidate, remove). Ручной
468
+ * `persist()` остаётся доступным для критичных моментов (logout,
469
+ * shutdown), но в обычном потоке не нужен.
470
+ *
471
+ * Возвращает:
472
+ * - `restore()` — гидратирует кэш из storage. Вызывать на старте.
355
473
  * - `persist()` — форс-запись текущего состояния.
356
474
  * - `unsubscribe()` — отключить авто-сохранение.
357
- *
358
- * Простая модель: на каждое изменение через client.cache._debugEntries
359
- * нет подписки, поэтому persist вызываем вручную из мутаций / по таймеру.
360
- * Здесь — таймер по throttleMs. Этого хватает для чатов/архива.
361
475
  */
362
476
  declare function persistQueryClient(options: PersistOptions): {
363
477
  restore: () => Promise<void>;
@@ -475,7 +589,21 @@ declare function apiClient<ResponseType = void, RequestParamsType = void, ErrorR
475
589
  */
476
590
  declare function apiMutation<ResponseType = void, RequestParamsType = void, ErrorResponseType = unknown>(endpoint: string | ((arg0: RequestParamsType) => string), fetchConfig?: RequestConfig): ApiMutationReturn<ResponseType, RequestParamsType, ErrorResponseType>;
477
591
  /**
478
- * Creates an API client for paginated requests
592
+ * Creates an API client for paginated requests.
593
+ *
594
+ * Каждая страница хранится в кэше под собственным подключом
595
+ * `['__paginate__', endpoint, params, { page, limit }]`. Это значит, что
596
+ * мутация может одной операцией пометить stale **все страницы** списка:
597
+ *
598
+ * ```ts
599
+ * confirmOrder.useMutation({
600
+ * invalidateKeys: [['__paginate__', '/orders/archive']],
601
+ * });
602
+ * ```
603
+ *
604
+ * Префикс матчится по `matchQueryKey` → подключи каждой страницы тоже
605
+ * считаются совпадающими.
606
+ *
479
607
  * @param endpoint - URL string or function that generates URL from params
480
608
  * @param fetchConfig - Request configuration
481
609
  * @param options - Pagination options (data/total extractors)
@@ -494,4 +622,4 @@ declare function apiPaginate<ResponseType extends {
494
622
  totalExtractor?: (response: ResponseType) => number;
495
623
  }): ApiPaginateReturn<ResponseType, RequestParamsType, TData, ErrorResponseType>;
496
624
 
497
- export { type ApiClientConfig, ApiClientProvider, type ApiClientProviderProps, ApiError, type ApiErrorInit, type CacheEntrySnapshot, type DehydratedQuery, type DehydratedState, type FetchOptions, type IHttpClient, type PersistOptions, type PersistStorage, QueryCache, QueryClient, type QueryFn, type QueryKey, type QueryState, type QueryStatus, type ResponseWrapper, type UseFetchOptions, type UseFetchResult, type UseMutationOptions, type UseMutationResult, type UsePaginateOptions, type UsePaginateResult, apiMutation, apiPaginate, configureApiClient, apiClient as default, focusManager, getConfig, getQueryClient, hashQueryKey, inspectCache, invalidateAll, isConfigured, matchQueryKey, onlineManager, persistQueryClient, setQueryClient, summarizeCache, useQueryClient };
625
+ export { type ApiClientConfig, type ApiClientLogger, ApiClientProvider, type ApiClientProviderProps, ApiError, type ApiErrorInit, type CacheEntrySnapshot, type DehydratedQuery, type DehydratedState, type FetchOptions, type IHttpClient, type PersistOptions, type PersistStorage, QueryCache, QueryClient, type QueryFn, type QueryKey, type QueryState, type QueryStatus, type ResponseWrapper, type UseFetchOptions, type UseFetchResult, type UseMutationOptions, type UseMutationResult, type UsePaginateOptions, type UsePaginateResult, apiMutation, apiPaginate, businessErrorToApiError, configureApiClient, apiClient as default, focusManager, getConfig, getQueryClient, hashQueryKey, inspectCache, invalidateAll, isConfigured, matchQueryKey, onlineManager, persistQueryClient, setQueryClient, summarizeCache, toApiError, useIsFetching, useIsMutating, useQueryClient };