@krymskyimaksym/react-api-client 2.0.0-beta.1 → 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 +64 -0
- package/dist/index.d.mts +82 -6
- package/dist/index.d.ts +82 -6
- package/dist/index.js +160 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +160 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
*
|
|
253
|
-
*
|
|
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
|
-
*
|
|
253
|
-
*
|
|
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
|
-
|
|
746
|
-
|
|
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) =>
|
|
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
|