@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 +61 -9
- package/dist/index.d.ts +61 -9
- package/dist/index.js +125 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +124 -42
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
*
|
|
354
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
354
|
-
*
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|
544
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
981
|
-
|
|
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
|