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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -34,11 +34,23 @@ 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;
40
41
  };
41
- type QueryFn<T> = () => Promise<T>;
42
+ type QueryFnContext = {
43
+ signal: AbortSignal;
44
+ };
45
+ /**
46
+ * queryFn принимает контекст с AbortSignal. Если HTTP-клиент его
47
+ * использует — отмена будет реальной; если игнорирует — поведение
48
+ * деградирует до текущего (запрос идёт до конца, но кэш игнорирует результат).
49
+ *
50
+ * Для обратной совместимости старая сигнатура `() => Promise<T>` тоже
51
+ * принимается — пакет просто не передаст signal внутрь.
52
+ */
53
+ type QueryFn<T> = (ctx: QueryFnContext) => Promise<T>;
42
54
  type FetchOptions = {
43
55
  staleTime?: number;
44
56
  gcTime?: number;
@@ -51,6 +63,14 @@ type FetchOptions = {
51
63
  */
52
64
  declare class QueryCache {
53
65
  private entries;
66
+ private globalListeners;
67
+ /**
68
+ * Подписка на любое изменение кэша: setData, invalidate, remove,
69
+ * успешный/ошибочный fetch. Используется persistQueryClient и
70
+ * devtools-подобными адаптерами. Не дублирует `subscribe(key, ...)`.
71
+ */
72
+ subscribeAll(listener: Listener$2): () => void;
73
+ private notifyGlobal;
54
74
  private ensureEntry;
55
75
  getState<T>(key: QueryKey): QueryState<T> | undefined;
56
76
  getData<T>(key: QueryKey): T | undefined;
@@ -136,6 +156,18 @@ type ResponseWrapper<DataType, ErrorsType = unknown> = {
136
156
  errors?: ErrorsType;
137
157
  } & DataType;
138
158
  type UseFetchOptions<T, TSelected = T> = {
159
+ /**
160
+ * При `false`:
161
+ * - запрос НЕ инициируется (ни на mount, ни на focus/reconnect/poll);
162
+ * - но хук **подписан на ключ кэша** и перерисуется, если данные
163
+ * обновит другой источник (другой `useFetch` с тем же ключом,
164
+ * `setQueryData` / `invalidateQueries`, мутация, push-handler).
165
+ *
166
+ * То есть `enabled: false` превращает хук в read-only слушателя.
167
+ * Если нужно полностью «потушить» хук — просто не вызывай его.
168
+ *
169
+ * По умолчанию `true`.
170
+ */
139
171
  enabled?: boolean;
140
172
  refetchOnMount?: boolean;
141
173
  /** Refetch при возврате на экран / в браузерное окно. */
@@ -240,10 +272,12 @@ type UseMutationResult<TData, TVariables> = {
240
272
  interface IHttpClient {
241
273
  get<T>(url: string, config?: {
242
274
  params?: Record<string, unknown>;
275
+ signal?: AbortSignal;
243
276
  }): Promise<T>;
244
277
  request<T>(url: string, config: {
245
278
  method?: string;
246
279
  data?: Record<string, unknown>;
280
+ signal?: AbortSignal;
247
281
  }): Promise<T>;
248
282
  }
249
283
  type ApiClientConfig = {
@@ -260,7 +294,7 @@ type ApiClientConfig = {
260
294
  };
261
295
  type ApiClientReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
262
296
  fetch: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
263
- useFetch: (params?: RequestParamsType, options?: UseFetchOptions<ResponseWrapper<ResponseType, ErrorResponseType>>) => UseFetchResult<ResponseWrapper<ResponseType, ErrorResponseType>>;
297
+ useFetch: <TSelected = ResponseWrapper<ResponseType, ErrorResponseType>>(params?: RequestParamsType, options?: UseFetchOptions<ResponseWrapper<ResponseType, ErrorResponseType>, TSelected>) => UseFetchResult<TSelected>;
264
298
  };
265
299
  type ApiMutationReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
266
300
  mutate: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
@@ -320,6 +354,21 @@ declare class ApiError<E = unknown> extends Error {
320
354
  readonly raw: unknown;
321
355
  constructor(init: ApiErrorInit<E>);
322
356
  }
357
+ /**
358
+ * Конвертация произвольного throw'нутого значения в `ApiError`.
359
+ * Покрывает 4 кейса:
360
+ * - `ApiError` → passthrough
361
+ * - axios-like `{ response: { status, data } }` → ApiError с полями из data
362
+ * - `Error` (сетевая ошибка, таймаут) → ApiError(isNetworkError, status: 0)
363
+ * - неизвестный объект → ApiError(message: 'Unknown error', status: 0)
364
+ */
365
+ declare function toApiError(thrown: unknown): ApiError;
366
+ /**
367
+ * Для 2xx ответа, в теле которого Laravel-style `{ status: false }`.
368
+ * Возвращает ApiError(status: 200, ...). Вызывается из executeRequest
369
+ * только при `throwOnError: true`.
370
+ */
371
+ declare function businessErrorToApiError(response: unknown): ApiError;
323
372
 
324
373
  /**
325
374
  * Storage-адаптер. Совместим с AsyncStorage и expo-secure-store:
@@ -350,14 +399,17 @@ type PersistOptions = {
350
399
  };
351
400
  /**
352
401
  * Подключает QueryClient к persistent storage.
353
- * Возвращает { restore, persist, unsubscribe }:
354
- * - `restore()`гидратирует кэш из storage (вызывать на старте приложения).
402
+ *
403
+ * С версии 2.0.0 подписан на `cache.subscribeAll`, поэтому
404
+ * автоматически сохраняет состояние через `throttleMs` после любого
405
+ * изменения (setData, успешный fetch, invalidate, remove). Ручной
406
+ * `persist()` остаётся доступным для критичных моментов (logout,
407
+ * shutdown), но в обычном потоке не нужен.
408
+ *
409
+ * Возвращает:
410
+ * - `restore()` — гидратирует кэш из storage. Вызывать на старте.
355
411
  * - `persist()` — форс-запись текущего состояния.
356
412
  * - `unsubscribe()` — отключить авто-сохранение.
357
- *
358
- * Простая модель: на каждое изменение через client.cache._debugEntries
359
- * нет подписки, поэтому persist вызываем вручную из мутаций / по таймеру.
360
- * Здесь — таймер по throttleMs. Этого хватает для чатов/архива.
361
413
  */
362
414
  declare function persistQueryClient(options: PersistOptions): {
363
415
  restore: () => Promise<void>;
@@ -494,4 +546,4 @@ declare function apiPaginate<ResponseType extends {
494
546
  totalExtractor?: (response: ResponseType) => number;
495
547
  }): ApiPaginateReturn<ResponseType, RequestParamsType, TData, ErrorResponseType>;
496
548
 
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 };
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 };
package/dist/index.d.ts CHANGED
@@ -34,11 +34,23 @@ 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;
40
41
  };
41
- type QueryFn<T> = () => Promise<T>;
42
+ type QueryFnContext = {
43
+ signal: AbortSignal;
44
+ };
45
+ /**
46
+ * queryFn принимает контекст с AbortSignal. Если HTTP-клиент его
47
+ * использует — отмена будет реальной; если игнорирует — поведение
48
+ * деградирует до текущего (запрос идёт до конца, но кэш игнорирует результат).
49
+ *
50
+ * Для обратной совместимости старая сигнатура `() => Promise<T>` тоже
51
+ * принимается — пакет просто не передаст signal внутрь.
52
+ */
53
+ type QueryFn<T> = (ctx: QueryFnContext) => Promise<T>;
42
54
  type FetchOptions = {
43
55
  staleTime?: number;
44
56
  gcTime?: number;
@@ -51,6 +63,14 @@ type FetchOptions = {
51
63
  */
52
64
  declare class QueryCache {
53
65
  private entries;
66
+ private globalListeners;
67
+ /**
68
+ * Подписка на любое изменение кэша: setData, invalidate, remove,
69
+ * успешный/ошибочный fetch. Используется persistQueryClient и
70
+ * devtools-подобными адаптерами. Не дублирует `subscribe(key, ...)`.
71
+ */
72
+ subscribeAll(listener: Listener$2): () => void;
73
+ private notifyGlobal;
54
74
  private ensureEntry;
55
75
  getState<T>(key: QueryKey): QueryState<T> | undefined;
56
76
  getData<T>(key: QueryKey): T | undefined;
@@ -136,6 +156,18 @@ type ResponseWrapper<DataType, ErrorsType = unknown> = {
136
156
  errors?: ErrorsType;
137
157
  } & DataType;
138
158
  type UseFetchOptions<T, TSelected = T> = {
159
+ /**
160
+ * При `false`:
161
+ * - запрос НЕ инициируется (ни на mount, ни на focus/reconnect/poll);
162
+ * - но хук **подписан на ключ кэша** и перерисуется, если данные
163
+ * обновит другой источник (другой `useFetch` с тем же ключом,
164
+ * `setQueryData` / `invalidateQueries`, мутация, push-handler).
165
+ *
166
+ * То есть `enabled: false` превращает хук в read-only слушателя.
167
+ * Если нужно полностью «потушить» хук — просто не вызывай его.
168
+ *
169
+ * По умолчанию `true`.
170
+ */
139
171
  enabled?: boolean;
140
172
  refetchOnMount?: boolean;
141
173
  /** Refetch при возврате на экран / в браузерное окно. */
@@ -240,10 +272,12 @@ type UseMutationResult<TData, TVariables> = {
240
272
  interface IHttpClient {
241
273
  get<T>(url: string, config?: {
242
274
  params?: Record<string, unknown>;
275
+ signal?: AbortSignal;
243
276
  }): Promise<T>;
244
277
  request<T>(url: string, config: {
245
278
  method?: string;
246
279
  data?: Record<string, unknown>;
280
+ signal?: AbortSignal;
247
281
  }): Promise<T>;
248
282
  }
249
283
  type ApiClientConfig = {
@@ -260,7 +294,7 @@ type ApiClientConfig = {
260
294
  };
261
295
  type ApiClientReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
262
296
  fetch: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
263
- useFetch: (params?: RequestParamsType, options?: UseFetchOptions<ResponseWrapper<ResponseType, ErrorResponseType>>) => UseFetchResult<ResponseWrapper<ResponseType, ErrorResponseType>>;
297
+ useFetch: <TSelected = ResponseWrapper<ResponseType, ErrorResponseType>>(params?: RequestParamsType, options?: UseFetchOptions<ResponseWrapper<ResponseType, ErrorResponseType>, TSelected>) => UseFetchResult<TSelected>;
264
298
  };
265
299
  type ApiMutationReturn<ResponseType, RequestParamsType, ErrorResponseType = unknown> = {
266
300
  mutate: (params?: RequestParamsType) => Promise<ResponseWrapper<ResponseType, ErrorResponseType>>;
@@ -320,6 +354,21 @@ declare class ApiError<E = unknown> extends Error {
320
354
  readonly raw: unknown;
321
355
  constructor(init: ApiErrorInit<E>);
322
356
  }
357
+ /**
358
+ * Конвертация произвольного throw'нутого значения в `ApiError`.
359
+ * Покрывает 4 кейса:
360
+ * - `ApiError` → passthrough
361
+ * - axios-like `{ response: { status, data } }` → ApiError с полями из data
362
+ * - `Error` (сетевая ошибка, таймаут) → ApiError(isNetworkError, status: 0)
363
+ * - неизвестный объект → ApiError(message: 'Unknown error', status: 0)
364
+ */
365
+ declare function toApiError(thrown: unknown): ApiError;
366
+ /**
367
+ * Для 2xx ответа, в теле которого Laravel-style `{ status: false }`.
368
+ * Возвращает ApiError(status: 200, ...). Вызывается из executeRequest
369
+ * только при `throwOnError: true`.
370
+ */
371
+ declare function businessErrorToApiError(response: unknown): ApiError;
323
372
 
324
373
  /**
325
374
  * Storage-адаптер. Совместим с AsyncStorage и expo-secure-store:
@@ -350,14 +399,17 @@ type PersistOptions = {
350
399
  };
351
400
  /**
352
401
  * Подключает QueryClient к persistent storage.
353
- * Возвращает { restore, persist, unsubscribe }:
354
- * - `restore()`гидратирует кэш из storage (вызывать на старте приложения).
402
+ *
403
+ * С версии 2.0.0 подписан на `cache.subscribeAll`, поэтому
404
+ * автоматически сохраняет состояние через `throttleMs` после любого
405
+ * изменения (setData, успешный fetch, invalidate, remove). Ручной
406
+ * `persist()` остаётся доступным для критичных моментов (logout,
407
+ * shutdown), но в обычном потоке не нужен.
408
+ *
409
+ * Возвращает:
410
+ * - `restore()` — гидратирует кэш из storage. Вызывать на старте.
355
411
  * - `persist()` — форс-запись текущего состояния.
356
412
  * - `unsubscribe()` — отключить авто-сохранение.
357
- *
358
- * Простая модель: на каждое изменение через client.cache._debugEntries
359
- * нет подписки, поэтому persist вызываем вручную из мутаций / по таймеру.
360
- * Здесь — таймер по throttleMs. Этого хватает для чатов/архива.
361
413
  */
362
414
  declare function persistQueryClient(options: PersistOptions): {
363
415
  restore: () => Promise<void>;
@@ -494,4 +546,4 @@ declare function apiPaginate<ResponseType extends {
494
546
  totalExtractor?: (response: ResponseType) => number;
495
547
  }): ApiPaginateReturn<ResponseType, RequestParamsType, TData, ErrorResponseType>;
496
548
 
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 };
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 };
package/dist/index.js CHANGED
@@ -36,6 +36,52 @@ var ApiError = class _ApiError extends Error {
36
36
  Object.setPrototypeOf(this, _ApiError.prototype);
37
37
  }
38
38
  };
39
+ var isObject = (v) => typeof v === "object" && v !== null;
40
+ function toApiError(thrown) {
41
+ if (thrown instanceof ApiError) return thrown;
42
+ if (isObject(thrown) && "response" in thrown && isObject(thrown.response)) {
43
+ const r = thrown.response;
44
+ const status = typeof r.status === "number" ? r.status : 0;
45
+ const data = isObject(r.data) ? r.data : void 0;
46
+ const message = (data && typeof data.message === "string" ? data.message : void 0) ?? (thrown instanceof Error ? thrown.message : void 0) ?? `HTTP ${status}`;
47
+ return new ApiError({
48
+ message,
49
+ status,
50
+ code: data && typeof data.code === "string" ? data.code : void 0,
51
+ errors: data?.errors,
52
+ isNetworkError: status === 0,
53
+ isUnauthorized: status === 401,
54
+ isValidationError: status === 422 || data?.errors !== void 0,
55
+ raw: r.data ?? thrown
56
+ });
57
+ }
58
+ if (thrown instanceof Error) {
59
+ return new ApiError({
60
+ message: thrown.message,
61
+ status: 0,
62
+ isNetworkError: true,
63
+ raw: thrown
64
+ });
65
+ }
66
+ return new ApiError({
67
+ message: "Unknown error",
68
+ status: 0,
69
+ raw: thrown
70
+ });
71
+ }
72
+ function businessErrorToApiError(response) {
73
+ if (!isObject(response)) {
74
+ return new ApiError({ message: "Request failed", status: 200, raw: response });
75
+ }
76
+ return new ApiError({
77
+ message: typeof response.message === "string" ? response.message : "Request failed",
78
+ status: 200,
79
+ code: typeof response.code === "string" ? response.code : void 0,
80
+ errors: response.errors,
81
+ isValidationError: response.errors !== void 0,
82
+ raw: response
83
+ });
84
+ }
39
85
 
40
86
  // src/utils.ts
41
87
  function buildEndpoint(endpoint, params) {
@@ -44,7 +90,7 @@ function buildEndpoint(endpoint, params) {
44
90
  }
45
91
  return endpoint;
46
92
  }
47
- async function executeRequest(endpoint, fetchConfig, params) {
93
+ async function executeRequest(endpoint, fetchConfig, params, signal) {
48
94
  const url = buildEndpoint(endpoint, params);
49
95
  const config = getConfig();
50
96
  try {
@@ -55,7 +101,8 @@ async function executeRequest(endpoint, fetchConfig, params) {
55
101
  params: {
56
102
  ...requestConfig.requestParams,
57
103
  ...params
58
- }
104
+ },
105
+ signal
59
106
  });
60
107
  } else {
61
108
  response = await config.httpClient.request(url, {
@@ -63,38 +110,29 @@ async function executeRequest(endpoint, fetchConfig, params) {
63
110
  data: {
64
111
  ...requestConfig.requestParams,
65
112
  ...params
66
- }
113
+ },
114
+ signal
67
115
  });
68
116
  }
69
117
  const hasExplicitStatus = response && typeof response === "object" && "status" in response && typeof response.status === "boolean";
70
118
  if (config.throwOnError && hasExplicitStatus && response.status === false) {
71
- const body = response;
72
- throw new ApiError({
73
- message: body.message ?? "Request failed",
74
- status: 200,
75
- errors: body.errors,
76
- raw: response
77
- });
119
+ throw businessErrorToApiError(response);
78
120
  }
79
121
  return { ...response, status: true };
80
122
  } catch (e) {
81
- if (e instanceof ApiError) throw e;
123
+ if (e instanceof ApiError) {
124
+ if (e.status === 401 && config.onUnauthorized) {
125
+ await config.onUnauthorized();
126
+ }
127
+ throw e;
128
+ }
82
129
  const error = e;
83
130
  const httpStatus = error.response?.status;
84
131
  if (httpStatus === 401 && config.onUnauthorized) {
85
132
  await config.onUnauthorized();
86
133
  }
87
134
  if (config.throwOnError) {
88
- const data = error.response?.data;
89
- const isNetwork = httpStatus === void 0;
90
- throw new ApiError({
91
- message: data?.message ?? (e instanceof Error ? e.message : void 0) ?? (isNetwork ? "Network error" : "Request error"),
92
- status: httpStatus ?? 0,
93
- code: data?.code,
94
- errors: data,
95
- isNetworkError: isNetwork,
96
- raw: e
97
- });
135
+ throw toApiError(e);
98
136
  }
99
137
  if (error.response?.data) {
100
138
  return {
@@ -255,6 +293,21 @@ var DEFAULT_STALE_TIME = 0;
255
293
  var QueryCache = class {
256
294
  constructor() {
257
295
  this.entries = /* @__PURE__ */ new Map();
296
+ this.globalListeners = /* @__PURE__ */ new Set();
297
+ }
298
+ /**
299
+ * Подписка на любое изменение кэша: setData, invalidate, remove,
300
+ * успешный/ошибочный fetch. Используется persistQueryClient и
301
+ * devtools-подобными адаптерами. Не дублирует `subscribe(key, ...)`.
302
+ */
303
+ subscribeAll(listener) {
304
+ this.globalListeners.add(listener);
305
+ return () => {
306
+ this.globalListeners.delete(listener);
307
+ };
308
+ }
309
+ notifyGlobal() {
310
+ for (const listener of this.globalListeners) listener();
258
311
  }
259
312
  ensureEntry(key, staleTime, gcTime) {
260
313
  const hash = hashQueryKey(key);
@@ -271,6 +324,7 @@ var QueryCache = class {
271
324
  },
272
325
  subscribers: /* @__PURE__ */ new Set(),
273
326
  inflight: null,
327
+ inflightController: null,
274
328
  gcTimer: null,
275
329
  staleTime: staleTime ?? DEFAULT_STALE_TIME,
276
330
  gcTime: gcTime ?? DEFAULT_GC_TIME
@@ -319,6 +373,7 @@ var QueryCache = class {
319
373
  }
320
374
  notify(entry) {
321
375
  for (const listener of entry.subscribers) listener();
376
+ this.notifyGlobal();
322
377
  }
323
378
  scheduleGc(entry) {
324
379
  if (entry.gcTimer) clearTimeout(entry.gcTimer);
@@ -347,12 +402,14 @@ var QueryCache = class {
347
402
  if (entry.inflight) return entry.inflight;
348
403
  entry.state = { ...entry.state, status: "loading", error: null };
349
404
  this.notify(entry);
405
+ const controller = new AbortController();
350
406
  const token = Symbol("inflight");
351
407
  entry.inflightToken = token;
408
+ entry.inflightController = controller;
352
409
  const isCurrent = () => entry.inflightToken === token;
353
410
  const myPromise = (async () => {
354
411
  try {
355
- const data = await queryFn();
412
+ const data = await queryFn({ signal: controller.signal });
356
413
  if (!isCurrent()) return data;
357
414
  entry.state = {
358
415
  data,
@@ -373,7 +430,10 @@ var QueryCache = class {
373
430
  this.notify(entry);
374
431
  throw err;
375
432
  } finally {
376
- if (isCurrent()) entry.inflight = null;
433
+ if (isCurrent()) {
434
+ entry.inflight = null;
435
+ entry.inflightController = null;
436
+ }
377
437
  }
378
438
  })();
379
439
  entry.inflight = myPromise;
@@ -405,6 +465,8 @@ var QueryCache = class {
405
465
  const match = typeof predicate === "function" ? predicate : (k) => matchQueryKey(predicate, k);
406
466
  for (const entry of this.entries.values()) {
407
467
  if (match(entry.key) && entry.inflight) {
468
+ entry.inflightController?.abort();
469
+ entry.inflightController = null;
408
470
  entry.inflight = null;
409
471
  entry.inflightToken = void 0;
410
472
  if (entry.state.status === "loading") {
@@ -420,12 +482,15 @@ var QueryCache = class {
420
482
  */
421
483
  remove(predicate) {
422
484
  const match = typeof predicate === "function" ? predicate : (k) => matchQueryKey(predicate, k);
485
+ let removed = false;
423
486
  for (const [hash, entry] of [...this.entries]) {
424
487
  if (match(entry.key)) {
425
488
  if (entry.gcTimer) clearTimeout(entry.gcTimer);
426
489
  this.entries.delete(hash);
490
+ removed = true;
427
491
  }
428
492
  }
493
+ if (removed) this.notifyGlobal();
429
494
  }
430
495
  /** Только для тестов / DevTools. */
431
496
  _debugEntries() {
@@ -530,18 +595,26 @@ function createUseFetch(endpoint, fetchConfig) {
530
595
  }, [customKey ? hashQueryKey(customKey) : null, serializedParams]);
531
596
  const client = getQueryClient();
532
597
  const cache = client.cache;
533
- const queryFn = react.useCallback(() => {
534
- const parsedParams = serializedParams ? JSON.parse(serializedParams) : void 0;
535
- return executeRequest(endpoint, fetchConfig, parsedParams);
536
- }, [serializedParams]);
598
+ const queryFn = react.useCallback(
599
+ ({ signal }) => {
600
+ const parsedParams = serializedParams ? JSON.parse(serializedParams) : void 0;
601
+ return executeRequest(endpoint, fetchConfig, parsedParams, signal);
602
+ },
603
+ [serializedParams]
604
+ );
537
605
  const initialState = cache.getState(queryKey);
538
606
  const [, forceRender] = react.useState(0);
539
607
  const rerender = react.useCallback(() => forceRender((v) => v + 1), []);
540
608
  react.useEffect(() => {
541
- if (!enabled) return;
542
609
  const unsub = cache.subscribe(queryKey, rerender);
543
- return unsub;
544
- }, [cache, enabled, hashQueryKey(queryKey), rerender]);
610
+ return () => {
611
+ unsub();
612
+ const state2 = cache._debugEntries().get(hashQueryKey(queryKey));
613
+ if (state2 && state2.subscribers.size === 0 && state2.inflight) {
614
+ cache.cancelQueries(queryKey);
615
+ }
616
+ };
617
+ }, [cache, hashQueryKey(queryKey), rerender]);
545
618
  const lastNotifiedRef = react.useRef({});
546
619
  const runFetch = react.useCallback(
547
620
  async (force) => {
@@ -801,14 +874,14 @@ function createUsePaginate(endpoint, fetchConfig, options) {
801
874
  [keyPrefix, limit]
802
875
  );
803
876
  const pageQueryFn = react.useCallback(
804
- (page) => () => {
877
+ (page) => ({ signal }) => {
805
878
  const parsedParams = serializedParams ? JSON.parse(serializedParams) : {};
806
879
  const requestParams = {
807
880
  ...parsedParams,
808
881
  page,
809
882
  limit
810
883
  };
811
- return executeRequest(endpoint, fetchConfig, requestParams);
884
+ return executeRequest(endpoint, fetchConfig, requestParams, signal);
812
885
  },
813
886
  [serializedParams, limit]
814
887
  );
@@ -939,19 +1012,26 @@ function persistQueryClient(options) {
939
1012
  maxAge,
940
1013
  version
941
1014
  } = options;
942
- let timer = null;
1015
+ let pendingTimer = null;
943
1016
  let lastSerialized = null;
1017
+ let unsubscribed = false;
944
1018
  const persist = async () => {
945
1019
  const state = client.cache.dehydrate(allowList);
946
- if (state.queries.length === 0) {
947
- return;
948
- }
1020
+ if (state.queries.length === 0) return;
949
1021
  const snap = { version, savedAt: Date.now(), state };
950
1022
  const serialized = JSON.stringify(snap);
951
1023
  if (serialized === lastSerialized) return;
952
1024
  lastSerialized = serialized;
953
1025
  await storage.setItem(storageKey, serialized);
954
1026
  };
1027
+ const scheduleWrite = () => {
1028
+ if (unsubscribed) return;
1029
+ if (pendingTimer) return;
1030
+ pendingTimer = setTimeout(() => {
1031
+ pendingTimer = null;
1032
+ void persist();
1033
+ }, throttleMs);
1034
+ };
955
1035
  const restore = async () => {
956
1036
  const raw = await storage.getItem(storageKey);
957
1037
  if (!raw) return;
@@ -970,15 +1050,17 @@ function persistQueryClient(options) {
970
1050
  await storage.removeItem(storageKey);
971
1051
  }
972
1052
  };
973
- timer = setInterval(() => {
974
- void persist();
975
- }, throttleMs);
1053
+ const unsubscribeFromCache = client.cache.subscribeAll(scheduleWrite);
976
1054
  return {
977
1055
  restore,
978
1056
  persist,
979
1057
  unsubscribe: () => {
980
- if (timer) clearInterval(timer);
981
- timer = null;
1058
+ unsubscribed = true;
1059
+ unsubscribeFromCache();
1060
+ if (pendingTimer) {
1061
+ clearTimeout(pendingTimer);
1062
+ pendingTimer = null;
1063
+ }
982
1064
  }
983
1065
  };
984
1066
  }
@@ -1085,6 +1167,7 @@ exports.QueryCache = QueryCache;
1085
1167
  exports.QueryClient = QueryClient;
1086
1168
  exports.apiMutation = apiMutation;
1087
1169
  exports.apiPaginate = apiPaginate;
1170
+ exports.businessErrorToApiError = businessErrorToApiError;
1088
1171
  exports.configureApiClient = configureApiClient;
1089
1172
  exports.default = index_default;
1090
1173
  exports.focusManager = focusManager;
@@ -1099,6 +1182,7 @@ exports.onlineManager = onlineManager;
1099
1182
  exports.persistQueryClient = persistQueryClient;
1100
1183
  exports.setQueryClient = setQueryClient;
1101
1184
  exports.summarizeCache = summarizeCache;
1185
+ exports.toApiError = toApiError;
1102
1186
  exports.useQueryClient = useQueryClient;
1103
1187
  //# sourceMappingURL=index.js.map
1104
1188
  //# sourceMappingURL=index.js.map