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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,131 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ type QueryKey = readonly unknown[];
5
+ /**
6
+ * Стабильная сериализация ключа кэша.
7
+ * - Объекты сериализуются с сортировкой ключей.
8
+ * - Массивы сохраняют порядок.
9
+ * - Функции и symbol — недопустимы (как в TanStack Query), кидаем.
10
+ * - undefined в массиве становится null, в объекте — поле пропускается.
11
+ *
12
+ * Пример: ['orders', { sort: 'date', dir: 'asc' }] и
13
+ * ['orders', { dir: 'asc', sort: 'date' }] дают одинаковый hash.
14
+ */
15
+ declare function hashQueryKey(key: QueryKey): string;
16
+ /**
17
+ * Проверяет, начинается ли `key` с `prefix`.
18
+ * Используется для invalidateQueries по префиксу: ['orders'] матчит
19
+ * и ['orders', 'manager'], и ['orders', 960].
20
+ */
21
+ declare function matchQueryKey(prefix: QueryKey, key: QueryKey): boolean;
22
+
23
+ type QueryStatus = 'idle' | 'loading' | 'success' | 'error';
24
+ type QueryState<T> = {
25
+ data: T | undefined;
26
+ error: Error | null;
27
+ status: QueryStatus;
28
+ updatedAt: number;
29
+ isStale: boolean;
30
+ };
31
+ type Listener$2 = () => void;
32
+ type QueryEntry<T> = {
33
+ key: QueryKey;
34
+ state: QueryState<T>;
35
+ subscribers: Set<Listener$2>;
36
+ inflight: Promise<T> | null;
37
+ gcTimer: ReturnType<typeof setTimeout> | null;
38
+ staleTime: number;
39
+ gcTime: number;
40
+ };
41
+ type QueryFn<T> = () => Promise<T>;
42
+ type FetchOptions = {
43
+ staleTime?: number;
44
+ gcTime?: number;
45
+ /** Если true — игнорируем staleTime и форсим запрос */
46
+ force?: boolean;
47
+ };
48
+ /**
49
+ * In-memory кэш запросов с подпиской по ключу, dedupe inflight-промисов
50
+ * и сборкой мусора через gcTime.
51
+ */
52
+ declare class QueryCache {
53
+ private entries;
54
+ private ensureEntry;
55
+ getState<T>(key: QueryKey): QueryState<T> | undefined;
56
+ getData<T>(key: QueryKey): T | undefined;
57
+ setData<T>(key: QueryKey, updater: T | ((prev: T | undefined) => T)): void;
58
+ /**
59
+ * Подписка на изменения ключа. Возвращает unsubscribe.
60
+ * Подписка останавливает GC таймер; отписка — запускает его обратно.
61
+ */
62
+ subscribe(key: QueryKey, listener: Listener$2): () => void;
63
+ private notify;
64
+ private scheduleGc;
65
+ /**
66
+ * Запускает или присоединяется к inflight-запросу.
67
+ * Если данные свежие (не stale) и не force — отдаёт кэш без запроса.
68
+ */
69
+ fetch<T>(key: QueryKey, queryFn: QueryFn<T>, options?: FetchOptions): Promise<T>;
70
+ /**
71
+ * Помечает запись как stale. Сами по себе данные не удаляются.
72
+ * Если есть подписчики — они получат уведомление, чтобы инициировать refetch.
73
+ */
74
+ invalidate(predicate: QueryKey | ((key: QueryKey) => boolean)): string[];
75
+ /**
76
+ * Отменяет «привязку» inflight-промиса к ключу. Сам HTTP-запрос
77
+ * продолжит исполняться (executeRequest не использует AbortSignal),
78
+ * но его результат больше не попадёт в кэш и не уведомит подписчиков.
79
+ * Полезно при размонтировании / при переключении страниц.
80
+ */
81
+ cancelQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
82
+ /**
83
+ * Полностью удаляет записи (даже с активными подписчиками).
84
+ * Используется редко — обычно достаточно invalidate.
85
+ */
86
+ remove(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
87
+ /** Только для тестов / DevTools. */
88
+ _debugEntries(): ReadonlyMap<string, QueryEntry<unknown>>;
89
+ /**
90
+ * Сериализует записи со статусом success — для persistence.
91
+ * inflight / loading / error не сохраняются, чтобы не гидратировать
92
+ * приложение в полу-загруженном состоянии.
93
+ */
94
+ dehydrate(filter?: (key: QueryKey) => boolean): DehydratedState;
95
+ hydrate(state: DehydratedState): void;
96
+ }
97
+ type DehydratedQuery = {
98
+ key: unknown[];
99
+ data: unknown;
100
+ updatedAt: number;
101
+ };
102
+ type DehydratedState = {
103
+ queries: DehydratedQuery[];
104
+ };
105
+
106
+ /**
107
+ * Высокоуровневый фасад над QueryCache: единый объект, который удобно
108
+ * прокидывать через Provider и дергать из push-handler'ов / эффектов.
109
+ *
110
+ * Сами queryFn-ы здесь не регистрируются — refetchQueries требует, чтобы
111
+ * либо подписчик сам перезапустил запрос (через subscribe), либо чтобы
112
+ * вызвали fetchQuery с queryFn вручную. Это намеренно: хук React сам
113
+ * знает, как собрать свой queryFn из endpoint/params.
114
+ */
115
+ declare class QueryClient {
116
+ readonly cache: QueryCache;
117
+ constructor(cache?: QueryCache);
118
+ getQueryData<T>(key: QueryKey): T | undefined;
119
+ setQueryData<T>(key: QueryKey, updater: T | ((prev: T | undefined) => T)): void;
120
+ fetchQuery<T>(key: QueryKey, queryFn: QueryFn<T>, options?: FetchOptions): Promise<T>;
121
+ invalidateQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
122
+ removeQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
123
+ cancelQueries(predicate: QueryKey | ((key: QueryKey) => boolean)): void;
124
+ }
125
+ /** Singleton-доступ для тех мест, где Provider недоступен (push-handler и т.п.). */
126
+ declare function getQueryClient(): QueryClient;
127
+ declare function setQueryClient(client: QueryClient): void;
128
+
1
129
  type RequestConfig = {
2
130
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
3
131
  requestParams?: Record<string, string>;
@@ -7,9 +135,28 @@ type ResponseWrapper<DataType, ErrorsType = unknown> = {
7
135
  message?: string;
8
136
  errors?: ErrorsType;
9
137
  } & DataType;
10
- type UseFetchOptions<T> = {
138
+ type UseFetchOptions<T, TSelected = T> = {
11
139
  enabled?: boolean;
12
140
  refetchOnMount?: boolean;
141
+ /** Refetch при возврате на экран / в браузерное окно. */
142
+ refetchOnFocus?: boolean;
143
+ /** Refetch при возврате приложения в активное состояние (RN AppState). */
144
+ refetchOnAppActive?: boolean;
145
+ /** Refetch при восстановлении сетевого соединения (offline → online). */
146
+ refetchOnReconnect?: boolean;
147
+ /** Время свежести данных в ms. Пока не истечёт — повторный mount берёт из кэша без сети. */
148
+ staleTime?: number;
149
+ /** Сколько держать запись в кэше после ухода последнего подписчика. По умолчанию 5 мин. */
150
+ gcTime?: number;
151
+ /** Интервал поллинга в ms. Поллинг автоматически останавливается, если экран не виден. */
152
+ pollingInterval?: number;
153
+ /**
154
+ * Кастомный queryKey. По умолчанию собирается как
155
+ * `['__endpoint__', endpointString, params]`.
156
+ */
157
+ queryKey?: readonly unknown[];
158
+ /** Селектор результата — пересчитывается мемоизированно. */
159
+ select?: (data: T) => TSelected;
13
160
  onSuccess?: (data: T) => void;
14
161
  onError?: (error: Error) => void;
15
162
  };
@@ -24,6 +171,16 @@ type UsePaginateOptions<T> = {
24
171
  enabled?: boolean;
25
172
  initialPage?: number;
26
173
  initialLimit?: number;
174
+ /** ms — пока страница свежая, повторный mount берёт её из кэша. */
175
+ staleTime?: number;
176
+ gcTime?: number;
177
+ /**
178
+ * Если true — при смене страницы (или params) предыдущие данные остаются
179
+ * на экране до прихода новых. Полезно для плавной пагинации.
180
+ */
181
+ keepPreviousData?: boolean;
182
+ /** Кастомный префикс ключа кэша. По умолчанию — endpoint + serialized params. */
183
+ queryKey?: readonly unknown[];
27
184
  onSuccess?: (data: T) => void;
28
185
  onError?: (error: Error) => void;
29
186
  };
@@ -36,17 +193,39 @@ type UsePaginateResult<TData extends unknown[]> = {
36
193
  hasPreviousPage: boolean;
37
194
  isLoading: boolean;
38
195
  isFetchingNextPage: boolean;
196
+ /** true если данные показываются из предыдущей страницы (keepPreviousData). */
197
+ isPlaceholderData: boolean;
39
198
  error: Error | null;
40
199
  fetchNextPage: () => Promise<void>;
41
200
  fetchPreviousPage: () => Promise<void>;
201
+ /** Префетчит следующую страницу в кэш, не меняя UI. */
202
+ prefetchNextPage: () => Promise<void>;
42
203
  refetch: () => Promise<void>;
43
204
  reset: () => void;
44
205
  };
45
- type UseMutationOptions<TData, TVariables> = {
46
- onMutate?: (variables: TVariables) => void | Promise<void>;
47
- onSuccess?: (data: TData, variables: TVariables) => void | Promise<void>;
48
- onError?: (error: Error, variables: TVariables) => void | Promise<void>;
49
- onSettled?: (data: TData | null, error: Error | null, variables: TVariables) => void | Promise<void>;
206
+
207
+ type UseMutationOptions<TData, TVariables, TContext = unknown> = {
208
+ /**
209
+ * Вызывается ДО запроса. Может вернуть context он попадёт в onError
210
+ * (для rollback) и в onSettled. Используется для optimistic updates:
211
+ * сохранить снепшот → применить оптимистичный setQueryData → в onError
212
+ * вернуть снепшот обратно.
213
+ */
214
+ onMutate?: (variables: TVariables) => void | TContext | Promise<void | TContext>;
215
+ onSuccess?: (data: TData, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
216
+ onError?: (error: Error, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
217
+ onSettled?: (data: TData | null, error: Error | null, variables: TVariables, context: TContext | undefined) => void | Promise<void>;
218
+ /**
219
+ * Ключи кэша, которые надо инвалидировать после успеха.
220
+ * Может быть массивом ключей или функцией, считающей их по vars/data.
221
+ * Каждый ключ матчится по префиксу (см. matchQueryKey).
222
+ */
223
+ invalidateKeys?: QueryKey[] | ((vars: TVariables, data: TData) => QueryKey[]);
224
+ /**
225
+ * Точечно патчит кэш после успеха — до invalidate. Удобно для
226
+ * «сервер вернул свежий объект, положим его прямо в ['orders', id]».
227
+ */
228
+ setQueryData?: (client: QueryClient, variables: TVariables, data: TData) => void;
50
229
  };
51
230
  type UseMutationResult<TData, TVariables> = {
52
231
  data: TData | null;
@@ -70,6 +249,14 @@ interface IHttpClient {
70
249
  type ApiClientConfig = {
71
250
  httpClient: IHttpClient;
72
251
  onUnauthorized?: () => void | Promise<void>;
252
+ /**
253
+ * Когда true — любая ошибка (HTTP >= 400, сеть, тело с `{ status: false }`)
254
+ * приводит к `throw new ApiError(...)`. По умолчанию false — пакет
255
+ * сохраняет старое поведение (ошибки приходят как `{ status: false }`).
256
+ * Включать только после того, как все вызовы `await *.mutate()` /
257
+ * `await *.fetch()` обёрнуты в try/catch.
258
+ */
259
+ throwOnError?: boolean;
73
260
  };
74
261
  type ApiClientReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
75
262
  fetch: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
@@ -77,7 +264,7 @@ type ApiClientReturn<ResponseType, RequestParamsType, ErrorResponseType = unknow
77
264
  };
78
265
  type ApiMutationReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
79
266
  mutate: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
80
- useMutation: (options?: UseMutationOptions<ResponseWrapper<ResponseType, ErrorResponseType>, RequestParamsType>) => UseMutationResult<ResponseWrapper<ResponseType, ErrorResponseType>, RequestParamsType>;
267
+ useMutation: <TContext = unknown>(options?: UseMutationOptions<ResponseWrapper<ResponseType, ErrorResponseType>, RequestParamsType, TContext>) => UseMutationResult<ResponseWrapper<ResponseType, ErrorResponseType>, RequestParamsType>;
81
268
  };
82
269
  type ApiPaginateReturn<ResponseType, RequestParamsType, DataArrayType extends unknown[], ErrorResponseType = unknown> = {
83
270
  usePaginate: (params?: Omit<RequestParamsType, 'page' | 'limit'>, options?: UsePaginateOptions<ResponseWrapper<ResponseType, ErrorResponseType>>) => UsePaginateResult<DataArrayType>;
@@ -107,6 +294,164 @@ declare function getConfig(): ApiClientConfig;
107
294
  */
108
295
  declare function isConfigured(): boolean;
109
296
 
297
+ /**
298
+ * Унифицированная ошибка API. Кидается из `executeRequest` (а значит,
299
+ * из `fetch`/`mutate`/хуков) только когда в `configureApiClient` включён
300
+ * `throwOnError: true`. По умолчанию поведение пакета не меняется —
301
+ * ошибки приходят как `{ status: false }` (см. CHANGELOG, Фаза 3.5).
302
+ */
303
+ type ApiErrorInit<E = unknown> = {
304
+ message: string;
305
+ status: number;
306
+ code?: string;
307
+ errors?: E;
308
+ isNetworkError?: boolean;
309
+ isUnauthorized?: boolean;
310
+ isValidationError?: boolean;
311
+ raw?: unknown;
312
+ };
313
+ declare class ApiError<E = unknown> extends Error {
314
+ readonly status: number;
315
+ readonly code?: string;
316
+ readonly errors?: E;
317
+ readonly isNetworkError: boolean;
318
+ readonly isUnauthorized: boolean;
319
+ readonly isValidationError: boolean;
320
+ readonly raw: unknown;
321
+ constructor(init: ApiErrorInit<E>);
322
+ }
323
+
324
+ /**
325
+ * Storage-адаптер. Совместим с AsyncStorage и expo-secure-store:
326
+ * { getItem(key): Promise<string|null>, setItem(key, value): Promise<void>,
327
+ * removeItem(key): Promise<void> }.
328
+ */
329
+ interface PersistStorage {
330
+ getItem(key: string): Promise<string | null>;
331
+ setItem(key: string, value: string): Promise<void>;
332
+ removeItem(key: string): Promise<void>;
333
+ }
334
+ type PersistOptions = {
335
+ client: QueryClient;
336
+ storage: PersistStorage;
337
+ /** Ключ в storage. По умолчанию 'react-api-client:cache'. */
338
+ storageKey?: string;
339
+ /** Дросселирование записи (ms). По умолчанию 1000. */
340
+ throttleMs?: number;
341
+ /** Фильтр ключей: что вообще сохранять. По умолчанию — всё. */
342
+ allowList?: (key: QueryKey) => boolean;
343
+ /** Максимальный возраст сохранённого снэпшота (ms). Старее — выбрасываем. */
344
+ maxAge?: number;
345
+ /**
346
+ * Версия снэпшота. При несовпадении — снэпшот игнорируется и удаляется.
347
+ * Поднимай при изменении формата данных в кэше.
348
+ */
349
+ version?: string | number;
350
+ };
351
+ /**
352
+ * Подключает QueryClient к persistent storage.
353
+ * Возвращает { restore, persist, unsubscribe }:
354
+ * - `restore()` — гидратирует кэш из storage (вызывать на старте приложения).
355
+ * - `persist()` — форс-запись текущего состояния.
356
+ * - `unsubscribe()` — отключить авто-сохранение.
357
+ *
358
+ * Простая модель: на каждое изменение через client.cache._debugEntries
359
+ * нет подписки, поэтому persist вызываем вручную из мутаций / по таймеру.
360
+ * Здесь — таймер по throttleMs. Этого хватает для чатов/архива.
361
+ */
362
+ declare function persistQueryClient(options: PersistOptions): {
363
+ restore: () => Promise<void>;
364
+ persist: () => Promise<void>;
365
+ unsubscribe: () => void;
366
+ };
367
+
368
+ type CacheEntrySnapshot = {
369
+ key: QueryKey;
370
+ hash: string;
371
+ status: QueryStatus;
372
+ isStale: boolean;
373
+ updatedAt: number;
374
+ hasData: boolean;
375
+ subscribers: number;
376
+ hasInflight: boolean;
377
+ errorMessage: string | null;
378
+ };
379
+ /**
380
+ * Снимок состояния всего кэша — без приватных полей и без данных.
381
+ * Удобно выводить в debug-экране или логировать.
382
+ */
383
+ declare function inspectCache(cache: QueryCache): CacheEntrySnapshot[];
384
+ /** «invalidate all» — пометить весь кэш как stale. */
385
+ declare function invalidateAll(client: QueryClient): void;
386
+ /** Краткая сводка для логов: сколько записей, активных подписчиков, inflight. */
387
+ declare function summarizeCache(cache: QueryCache): {
388
+ total: number;
389
+ withSubscribers: number;
390
+ inflight: number;
391
+ stale: number;
392
+ byStatus: Record<QueryStatus, number>;
393
+ };
394
+
395
+ /**
396
+ * Глобальный менеджер «фокуса» приложения.
397
+ * - В вебе автоматически подключается к window focus/visibilitychange.
398
+ * - В React Native интеграцию ставит сам потребитель через
399
+ * `focusManager.setFocused(state === 'active')` из AppState.
400
+ *
401
+ * Подписчики (useFetch с refetchOnFocus / refetchOnAppActive) получают
402
+ * уведомление при переходе в focused == true.
403
+ */
404
+ type Listener$1 = (focused: boolean) => void;
405
+ declare class FocusManager {
406
+ private focused;
407
+ private listeners;
408
+ private cleanup;
409
+ subscribe(listener: Listener$1): () => void;
410
+ isFocused(): boolean;
411
+ setFocused(focused: boolean): void;
412
+ private setupBrowserListeners;
413
+ private teardownBrowserListeners;
414
+ }
415
+ declare const focusManager: FocusManager;
416
+
417
+ /**
418
+ * Глобальный менеджер сетевого статуса.
419
+ * - В браузере подключается к window online/offline.
420
+ * - В React Native интеграция — через NetInfo (опционально):
421
+ * `NetInfo.addEventListener(s => onlineManager.setOnline(!!s.isConnected))`.
422
+ * Жёсткой зависимости от NetInfo нет — без него по умолчанию online: true.
423
+ *
424
+ * Подписчики (useFetch с refetchOnReconnect) получают уведомление при
425
+ * переходе offline → online.
426
+ */
427
+ type Listener = (online: boolean) => void;
428
+ declare class OnlineManager {
429
+ private online;
430
+ private listeners;
431
+ private cleanup;
432
+ subscribe(listener: Listener): () => void;
433
+ isOnline(): boolean;
434
+ setOnline(online: boolean): void;
435
+ private setupBrowserListeners;
436
+ private teardownBrowserListeners;
437
+ }
438
+ declare const onlineManager: OnlineManager;
439
+
440
+ type ApiClientProviderProps = {
441
+ client?: QueryClient;
442
+ children: ReactNode;
443
+ };
444
+ /**
445
+ * Корневой Provider пакета. Создаёт (или принимает) QueryClient и кладёт
446
+ * его в React Context. Хуки достают клиент через useQueryClient().
447
+ *
448
+ * Также синхронизирует переданный client с singleton'ом
449
+ * getQueryClient() — чтобы push-handler'ы вне React-дерева могли дергать
450
+ * `getQueryClient().invalidateQueries(...)`.
451
+ */
452
+ declare function ApiClientProvider({ client, children, }: ApiClientProviderProps): react.FunctionComponentElement<react.ProviderProps<QueryClient | null>>;
453
+ declare function useQueryClient(): QueryClient;
454
+
110
455
  /**
111
456
  * Creates an API client for GET requests
112
457
  * @param endpoint - URL string or function that generates URL from params
@@ -149,4 +494,4 @@ declare function apiPaginate<ResponseType extends {
149
494
  totalExtractor?: (response: ResponseType) => number;
150
495
  }): ApiPaginateReturn<ResponseType, RequestParamsType, TData, ErrorResponseType>;
151
496
 
152
- export { type ApiClientConfig, type IHttpClient, type ResponseWrapper, type UseFetchOptions, type UseFetchResult, type UseMutationOptions, type UseMutationResult, type UsePaginateOptions, type UsePaginateResult, apiMutation, apiPaginate, configureApiClient, apiClient as default, getConfig, isConfigured };
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 };