@krymskyimaksym/react-api-client 2.0.0-beta.1 → 2.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.
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
@@ -38,6 +38,12 @@ type QueryEntry<T> = {
38
38
  gcTimer: ReturnType<typeof setTimeout> | null;
39
39
  staleTime: number;
40
40
  gcTime: number;
41
+ /**
42
+ * Последний queryFn, переданный в fetch для этого ключа.
43
+ * Используется refetchQueries: позволяет перезапустить запрос,
44
+ * не зная queryFn из caller'а (например, push-handler).
45
+ */
46
+ lastQueryFn: QueryFn<T> | null;
41
47
  };
42
48
  type QueryFnContext = {
43
49
  signal: AbortSignal;
@@ -92,6 +98,13 @@ declare class QueryCache {
92
98
  * Если есть подписчики — они получат уведомление, чтобы инициировать refetch.
93
99
  */
94
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>;
95
108
  /**
96
109
  * Отменяет «привязку» inflight-промиса к ключу. Сам HTTP-запрос
97
110
  * продолжит исполняться (executeRequest не использует AbortSignal),
@@ -104,6 +117,12 @@ declare class QueryCache {
104
117
  * Используется редко — обычно достаточно invalidate.
105
118
  */
106
119
  remove(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
120
+ /**
121
+ * Количество inflight-запросов в кэше, опционально отфильтрованных
122
+ * по predicate. Используется `useIsFetching()` для глобального
123
+ * индикатора загрузки.
124
+ */
125
+ countFetching(predicate?: QueryKey | ((key: QueryKey) => boolean)): number;
107
126
  /** Только для тестов / DevTools. */
108
127
  _debugEntries(): ReadonlyMap<string, QueryEntry<unknown>>;
109
128
  /**
@@ -141,6 +160,12 @@ declare class QueryClient {
141
160
  invalidateQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
142
161
  removeQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
143
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>;
144
169
  }
145
170
  /** Singleton-доступ для тех мест, где Provider недоступен (push-handler и т.п.). */
146
171
  declare function getQueryClient(): QueryClient;
@@ -248,11 +273,16 @@ type UseMutationOptions<TData, TVariables, TContext = unknown> = {
248
273
  onError?: (error: Error, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
249
274
  onSettled?: (data: TData | null, error: Error | null, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
250
275
  /**
251
- * Ключи кэша, которые надо инвалидировать после успеха.
252
- * Может быть массивом ключей или функцией, считающей их по vars/data.
253
- * Каждый ключ матчится по префиксу (см. matchQueryKey).
276
+ * Что инвалидировать в кэше после успешной мутации.
277
+ *
278
+ * Три формы:
279
+ * - `QueryKey[]` — массив префиксов; матч по `matchQueryKey`
280
+ * - `(vars, data) => QueryKey[]` — динамический список префиксов
281
+ * - `(vars, data) => (key: QueryKey) => boolean` — произвольный предикат
282
+ * по каждому ключу кэша (например «инвалидируй всё, где встречается
283
+ * этот orderId, в любой позиции ключа»).
254
284
  */
255
- invalidateKeys?: QueryKey[] | ((vars: TVariables, data: TData) => QueryKey[]);
285
+ invalidateKeys?: QueryKey[] | ((vars: TVariables, data: TData) => QueryKey[]) | ((vars: TVariables, data: TData) => (key: QueryKey) => boolean);
256
286
  /**
257
287
  * Точечно патчит кэш после успеха — до invalidate. Удобно для
258
288
  * «сервер вернул свежий объект, положим его прямо в ['orders', id]».
@@ -280,6 +310,21 @@ interface IHttpClient {
280
310
  signal?: AbortSignal;
281
311
  }): Promise<T>;
282
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
+ };
283
328
  type ApiClientConfig = {
284
329
  httpClient: IHttpClient;
285
330
  onUnauthorized?: () => void | Promise<void>;
@@ -291,6 +336,8 @@ type ApiClientConfig = {
291
336
  * `await *.fetch()` обёрнуты в try/catch.
292
337
  */
293
338
  throwOnError?: boolean;
339
+ /** Опциональный логгер для отладки. */
340
+ logger?: ApiClientLogger;
294
341
  };
295
342
  type ApiClientReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
296
343
  fetch: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
@@ -370,6 +417,21 @@ declare function toApiError(thrown: unknown): ApiError;
370
417
  */
371
418
  declare function businessErrorToApiError(response: unknown): ApiError;
372
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;
434
+
373
435
  /**
374
436
  * Storage-адаптер. Совместим с AsyncStorage и expo-secure-store:
375
437
  * { getItem(key): Promise<string|null>, setItem(key, value): Promise<void>,
@@ -527,7 +589,21 @@ declare function apiClient<ResponseType = void, RequestParamsType = void, ErrorR
527
589
  */
528
590
  declare function apiMutation<ResponseType = void, RequestParamsType = void, ErrorResponseType = unknown>(endpoint: string | ((arg0: RequestParamsType) => string), fetchConfig?: RequestConfig): ApiMutationReturn<ResponseType, RequestParamsType, ErrorResponseType>;
529
591
  /**
530
- * 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
+ *
531
607
  * @param endpoint - URL string or function that generates URL from params
532
608
  * @param fetchConfig - Request configuration
533
609
  * @param options - Pagination options (data/total extractors)
@@ -546,4 +622,4 @@ declare function apiPaginate<ResponseType extends {
546
622
  totalExtractor?: (response: ResponseType) => number;
547
623
  }): ApiPaginateReturn<ResponseType, RequestParamsType, TData, ErrorResponseType>;
548
624
 
549
- 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, businessErrorToApiError, configureApiClient, apiClient as default, focusManager, getConfig, getQueryClient, hashQueryKey, inspectCache, invalidateAll, isConfigured, matchQueryKey, onlineManager, persistQueryClient, setQueryClient, summarizeCache, toApiError, 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
@@ -38,6 +38,12 @@ type QueryEntry<T> = {
38
38
  gcTimer: ReturnType<typeof setTimeout> | null;
39
39
  staleTime: number;
40
40
  gcTime: number;
41
+ /**
42
+ * Последний queryFn, переданный в fetch для этого ключа.
43
+ * Используется refetchQueries: позволяет перезапустить запрос,
44
+ * не зная queryFn из caller'а (например, push-handler).
45
+ */
46
+ lastQueryFn: QueryFn<T> | null;
41
47
  };
42
48
  type QueryFnContext = {
43
49
  signal: AbortSignal;
@@ -92,6 +98,13 @@ declare class QueryCache {
92
98
  * Если есть подписчики — они получат уведомление, чтобы инициировать refetch.
93
99
  */
94
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>;
95
108
  /**
96
109
  * Отменяет «привязку» inflight-промиса к ключу. Сам HTTP-запрос
97
110
  * продолжит исполняться (executeRequest не использует AbortSignal),
@@ -104,6 +117,12 @@ declare class QueryCache {
104
117
  * Используется редко — обычно достаточно invalidate.
105
118
  */
106
119
  remove(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
120
+ /**
121
+ * Количество inflight-запросов в кэше, опционально отфильтрованных
122
+ * по predicate. Используется `useIsFetching()` для глобального
123
+ * индикатора загрузки.
124
+ */
125
+ countFetching(predicate?: QueryKey | ((key: QueryKey) => boolean)): number;
107
126
  /** Только для тестов / DevTools. */
108
127
  _debugEntries(): ReadonlyMap<string, QueryEntry<unknown>>;
109
128
  /**
@@ -141,6 +160,12 @@ declare class QueryClient {
141
160
  invalidateQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
142
161
  removeQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
143
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>;
144
169
  }
145
170
  /** Singleton-доступ для тех мест, где Provider недоступен (push-handler и т.п.). */
146
171
  declare function getQueryClient(): QueryClient;
@@ -248,11 +273,16 @@ type UseMutationOptions<TData, TVariables, TContext = unknown> = {
248
273
  onError?: (error: Error, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
249
274
  onSettled?: (data: TData | null, error: Error | null, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
250
275
  /**
251
- * Ключи кэша, которые надо инвалидировать после успеха.
252
- * Может быть массивом ключей или функцией, считающей их по vars/data.
253
- * Каждый ключ матчится по префиксу (см. matchQueryKey).
276
+ * Что инвалидировать в кэше после успешной мутации.
277
+ *
278
+ * Три формы:
279
+ * - `QueryKey[]` — массив префиксов; матч по `matchQueryKey`
280
+ * - `(vars, data) => QueryKey[]` — динамический список префиксов
281
+ * - `(vars, data) => (key: QueryKey) => boolean` — произвольный предикат
282
+ * по каждому ключу кэша (например «инвалидируй всё, где встречается
283
+ * этот orderId, в любой позиции ключа»).
254
284
  */
255
- invalidateKeys?: QueryKey[] | ((vars: TVariables, data: TData) => QueryKey[]);
285
+ invalidateKeys?: QueryKey[] | ((vars: TVariables, data: TData) => QueryKey[]) | ((vars: TVariables, data: TData) => (key: QueryKey) => boolean);
256
286
  /**
257
287
  * Точечно патчит кэш после успеха — до invalidate. Удобно для
258
288
  * «сервер вернул свежий объект, положим его прямо в ['orders', id]».
@@ -280,6 +310,21 @@ interface IHttpClient {
280
310
  signal?: AbortSignal;
281
311
  }): Promise<T>;
282
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
+ };
283
328
  type ApiClientConfig = {
284
329
  httpClient: IHttpClient;
285
330
  onUnauthorized?: () => void | Promise<void>;
@@ -291,6 +336,8 @@ type ApiClientConfig = {
291
336
  * `await *.fetch()` обёрнуты в try/catch.
292
337
  */
293
338
  throwOnError?: boolean;
339
+ /** Опциональный логгер для отладки. */
340
+ logger?: ApiClientLogger;
294
341
  };
295
342
  type ApiClientReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
296
343
  fetch: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
@@ -370,6 +417,21 @@ declare function toApiError(thrown: unknown): ApiError;
370
417
  */
371
418
  declare function businessErrorToApiError(response: unknown): ApiError;
372
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;
434
+
373
435
  /**
374
436
  * Storage-адаптер. Совместим с AsyncStorage и expo-secure-store:
375
437
  * { getItem(key): Promise<string|null>, setItem(key, value): Promise<void>,
@@ -527,7 +589,21 @@ declare function apiClient<ResponseType = void, RequestParamsType = void, ErrorR
527
589
  */
528
590
  declare function apiMutation<ResponseType = void, RequestParamsType = void, ErrorResponseType = unknown>(endpoint: string | ((arg0: RequestParamsType) => string), fetchConfig?: RequestConfig): ApiMutationReturn<ResponseType, RequestParamsType, ErrorResponseType>;
529
591
  /**
530
- * 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
+ *
531
607
  * @param endpoint - URL string or function that generates URL from params
532
608
  * @param fetchConfig - Request configuration
533
609
  * @param options - Pagination options (data/total extractors)
@@ -546,4 +622,4 @@ declare function apiPaginate<ResponseType extends {
546
622
  totalExtractor?: (response: ResponseType) => number;
547
623
  }): ApiPaginateReturn<ResponseType, RequestParamsType, TData, ErrorResponseType>;
548
624
 
549
- 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, businessErrorToApiError, configureApiClient, apiClient as default, focusManager, getConfig, getQueryClient, hashQueryKey, inspectCache, invalidateAll, isConfigured, matchQueryKey, onlineManager, persistQueryClient, setQueryClient, summarizeCache, toApiError, 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.js CHANGED
@@ -155,6 +155,21 @@ function handleResponse(result, onSuccess, onError) {
155
155
  }
156
156
  }
157
157
 
158
+ // src/logger.ts
159
+ function getLogger() {
160
+ if (!isConfigured()) return void 0;
161
+ return getConfig().logger;
162
+ }
163
+ function callLogger(method, ...args) {
164
+ const logger = getLogger();
165
+ const fn = logger?.[method];
166
+ if (!fn) return;
167
+ try {
168
+ fn(...args);
169
+ } catch {
170
+ }
171
+ }
172
+
158
173
  // src/query/focus-manager.ts
159
174
  var FocusManager = class {
160
175
  constructor() {
@@ -326,6 +341,7 @@ var QueryCache = class {
326
341
  inflight: null,
327
342
  inflightController: null,
328
343
  gcTimer: null,
344
+ lastQueryFn: null,
329
345
  staleTime: staleTime ?? DEFAULT_STALE_TIME,
330
346
  gcTime: gcTime ?? DEFAULT_GC_TIME
331
347
  };
@@ -396,6 +412,7 @@ var QueryCache = class {
396
412
  const gcTime = options.gcTime ?? DEFAULT_GC_TIME;
397
413
  const entry = this.ensureEntry(key, staleTime, gcTime);
398
414
  const isFresh = entry.state.status === "success" && !entry.state.isStale && Date.now() - entry.state.updatedAt < staleTime;
415
+ entry.lastQueryFn = queryFn;
399
416
  if (!options.force && isFresh && entry.state.data !== void 0) {
400
417
  return entry.state.data;
401
418
  }
@@ -455,6 +472,26 @@ var QueryCache = class {
455
472
  }
456
473
  return invalidated;
457
474
  }
475
+ /**
476
+ * Перезапускает все записи, матчинг predicate, у которых сохранён
477
+ * `lastQueryFn` (т.е. их хоть раз кто-то загрузил через `fetch`).
478
+ * Возвращает promise, который резолвится когда все запросы завершились.
479
+ * Ошибки отдельных запросов проглатываются — общий promise успешный.
480
+ */
481
+ refetchQueries(predicate) {
482
+ const match = typeof predicate === "function" ? predicate : (k) => matchQueryKey(predicate, k);
483
+ const promises = [];
484
+ for (const entry of this.entries.values()) {
485
+ if (!match(entry.key)) continue;
486
+ if (!entry.lastQueryFn) continue;
487
+ promises.push(
488
+ this.fetch(entry.key, entry.lastQueryFn, { force: true }).catch(
489
+ () => void 0
490
+ )
491
+ );
492
+ }
493
+ return Promise.all(promises).then(() => void 0);
494
+ }
458
495
  /**
459
496
  * Отменяет «привязку» inflight-промиса к ключу. Сам HTTP-запрос
460
497
  * продолжит исполняться (executeRequest не использует AbortSignal),
@@ -492,6 +529,19 @@ var QueryCache = class {
492
529
  }
493
530
  if (removed) this.notifyGlobal();
494
531
  }
532
+ /**
533
+ * Количество inflight-запросов в кэше, опционально отфильтрованных
534
+ * по predicate. Используется `useIsFetching()` для глобального
535
+ * индикатора загрузки.
536
+ */
537
+ countFetching(predicate) {
538
+ const match = !predicate ? () => true : typeof predicate === "function" ? predicate : (k) => matchQueryKey(predicate, k);
539
+ let n = 0;
540
+ for (const entry of this.entries.values()) {
541
+ if (entry.state.status === "loading" && match(entry.key)) n++;
542
+ }
543
+ return n;
544
+ }
495
545
  /** Только для тестов / DevTools. */
496
546
  _debugEntries() {
497
547
  return this.entries;
@@ -549,7 +599,8 @@ var QueryClient = class {
549
599
  return this.cache.fetch(key, queryFn, options);
550
600
  }
551
601
  invalidateQueries(predicate) {
552
- this.cache.invalidate(predicate);
602
+ const hashes = this.cache.invalidate(predicate);
603
+ if (hashes.length > 0) callLogger("onInvalidate", hashes);
553
604
  }
554
605
  removeQueries(predicate) {
555
606
  this.cache.remove(predicate);
@@ -557,6 +608,19 @@ var QueryClient = class {
557
608
  cancelQueries(predicate) {
558
609
  this.cache.cancelQueries(predicate);
559
610
  }
611
+ refetchQueries(predicate) {
612
+ return this.cache.refetchQueries(predicate);
613
+ }
614
+ /**
615
+ * Кладёт запрос в кэш, не пробрасывая ошибки. Подходит для оптимистичной
616
+ * подгрузки следующего экрана при наведении / долгом тапе.
617
+ */
618
+ prefetchQuery(key, queryFn, options) {
619
+ return this.cache.fetch(key, queryFn, options).then(
620
+ () => void 0,
621
+ () => void 0
622
+ );
623
+ }
560
624
  };
561
625
  var globalClient = null;
562
626
  function getQueryClient() {
@@ -618,12 +682,14 @@ function createUseFetch(endpoint, fetchConfig) {
618
682
  const lastNotifiedRef = react.useRef({});
619
683
  const runFetch = react.useCallback(
620
684
  async (force) => {
685
+ callLogger("onFetchStart", queryKey);
621
686
  try {
622
687
  const data = await cache.fetch(queryKey, queryFn, {
623
688
  staleTime,
624
689
  gcTime,
625
690
  force
626
691
  });
692
+ callLogger("onFetchSuccess", queryKey, data);
627
693
  if (lastNotifiedRef.current.success !== data) {
628
694
  lastNotifiedRef.current.success = data;
629
695
  handleResponse(
@@ -633,6 +699,7 @@ function createUseFetch(endpoint, fetchConfig) {
633
699
  );
634
700
  }
635
701
  } catch (err) {
702
+ callLogger("onFetchError", queryKey, err);
636
703
  const e = err;
637
704
  const hash = `${e.name}:${e.message}`;
638
705
  if (lastNotifiedRef.current.errorHash !== hash) {
@@ -716,6 +783,47 @@ function createUseFetch(endpoint, fetchConfig) {
716
783
  };
717
784
  };
718
785
  }
786
+
787
+ // src/query/mutation-counter.ts
788
+ var MutationCounter = class {
789
+ constructor() {
790
+ this.active = /* @__PURE__ */ new Map();
791
+ this.listeners = /* @__PURE__ */ new Set();
792
+ }
793
+ start(scope) {
794
+ const id = Symbol("mutation");
795
+ this.active.set(id, scope);
796
+ this.notify();
797
+ return id;
798
+ }
799
+ stop(id) {
800
+ if (this.active.delete(id)) this.notify();
801
+ }
802
+ count(predicate) {
803
+ if (!predicate) return this.active.size;
804
+ let n = 0;
805
+ for (const scope of this.active.values()) {
806
+ if (typeof predicate === "function") {
807
+ if (predicate(scope)) n++;
808
+ } else {
809
+ if (scope && matchQueryKey(predicate, scope)) n++;
810
+ }
811
+ }
812
+ return n;
813
+ }
814
+ subscribe(listener) {
815
+ this.listeners.add(listener);
816
+ return () => {
817
+ this.listeners.delete(listener);
818
+ };
819
+ }
820
+ notify() {
821
+ for (const l of this.listeners) l();
822
+ }
823
+ };
824
+ var mutationCounter = new MutationCounter();
825
+
826
+ // src/hooks/use-mutation.ts
719
827
  function createUseMutation(endpoint, fetchConfig) {
720
828
  return (options = {}) => {
721
829
  const {
@@ -742,10 +850,21 @@ function createUseMutation(endpoint, fetchConfig) {
742
850
  (vars, result) => {
743
851
  if (!invalidateKeys) return;
744
852
  const client = getQueryClient();
745
- const keys = typeof invalidateKeys === "function" ? invalidateKeys(vars, result) : invalidateKeys;
746
- if (keys.length === 0) return;
853
+ if (Array.isArray(invalidateKeys)) {
854
+ if (invalidateKeys.length === 0) return;
855
+ client.invalidateQueries(
856
+ (k) => invalidateKeys.some((prefix) => matchQueryKey(prefix, k))
857
+ );
858
+ return;
859
+ }
860
+ const out = invalidateKeys(vars, result);
861
+ if (typeof out === "function") {
862
+ client.invalidateQueries(out);
863
+ return;
864
+ }
865
+ if (out.length === 0) return;
747
866
  client.invalidateQueries(
748
- (k) => keys.some((prefix) => matchQueryKey(prefix, k))
867
+ (k) => out.some((prefix) => matchQueryKey(prefix, k))
749
868
  );
750
869
  },
751
870
  [invalidateKeys]
@@ -756,6 +875,10 @@ function createUseMutation(endpoint, fetchConfig) {
756
875
  setIsSuccess(false);
757
876
  setIsError(false);
758
877
  setError(null);
878
+ const scope = Array.isArray(invalidateKeys) && invalidateKeys.length > 0 ? invalidateKeys[0] : void 0;
879
+ const mutationId = mutationCounter.start(scope);
880
+ const endpointId = buildEndpoint(endpoint, variables);
881
+ callLogger("onMutationStart", endpointId, variables);
759
882
  let context;
760
883
  try {
761
884
  if (onMutate) {
@@ -766,6 +889,7 @@ function createUseMutation(endpoint, fetchConfig) {
766
889
  setData(result);
767
890
  if (result.status) {
768
891
  setIsSuccess(true);
892
+ callLogger("onMutationSuccess", endpointId, variables, result);
769
893
  if (setQueryData) {
770
894
  setQueryData(getQueryClient(), variables, result);
771
895
  }
@@ -777,6 +901,7 @@ function createUseMutation(endpoint, fetchConfig) {
777
901
  const err = new Error(result.message ?? "Mutation failed");
778
902
  setIsError(true);
779
903
  setError(err);
904
+ callLogger("onMutationError", endpointId, variables, err);
780
905
  if (onError) {
781
906
  await onError(err, variables, context);
782
907
  }
@@ -790,6 +915,7 @@ function createUseMutation(endpoint, fetchConfig) {
790
915
  setError(error2);
791
916
  setIsError(true);
792
917
  setIsSuccess(false);
918
+ callLogger("onMutationError", endpointId, variables, error2);
793
919
  if (onError) {
794
920
  await onError(error2, variables, context);
795
921
  }
@@ -798,6 +924,7 @@ function createUseMutation(endpoint, fetchConfig) {
798
924
  }
799
925
  throw error2;
800
926
  } finally {
927
+ mutationCounter.stop(mutationId);
801
928
  setIsLoading(false);
802
929
  }
803
930
  },
@@ -1000,6 +1127,33 @@ function createUsePaginate(endpoint, fetchConfig, options) {
1000
1127
  };
1001
1128
  };
1002
1129
  }
1130
+ function useIsFetching(predicate) {
1131
+ const client = getQueryClient();
1132
+ const get = react.useCallback(
1133
+ () => client.cache.countFetching(predicate),
1134
+ [client.cache, predicate]
1135
+ );
1136
+ const [count, setCount] = react.useState(get);
1137
+ react.useEffect(() => {
1138
+ setCount(get());
1139
+ const unsub = client.cache.subscribeAll(() => setCount(get()));
1140
+ return unsub;
1141
+ }, [client.cache, get]);
1142
+ return count;
1143
+ }
1144
+ function useIsMutating(predicate) {
1145
+ const get = react.useCallback(
1146
+ () => mutationCounter.count(predicate),
1147
+ [predicate]
1148
+ );
1149
+ const [count, setCount] = react.useState(get);
1150
+ react.useEffect(() => {
1151
+ setCount(get());
1152
+ const unsub = mutationCounter.subscribe(() => setCount(get()));
1153
+ return unsub;
1154
+ }, [get]);
1155
+ return count;
1156
+ }
1003
1157
 
1004
1158
  // src/query/persist.ts
1005
1159
  function persistQueryClient(options) {
@@ -1183,6 +1337,8 @@ exports.persistQueryClient = persistQueryClient;
1183
1337
  exports.setQueryClient = setQueryClient;
1184
1338
  exports.summarizeCache = summarizeCache;
1185
1339
  exports.toApiError = toApiError;
1340
+ exports.useIsFetching = useIsFetching;
1341
+ exports.useIsMutating = useIsMutating;
1186
1342
  exports.useQueryClient = useQueryClient;
1187
1343
  //# sourceMappingURL=index.js.map
1188
1344
  //# sourceMappingURL=index.js.map