@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.mjs CHANGED
@@ -32,6 +32,52 @@ var ApiError = class _ApiError extends Error {
32
32
  Object.setPrototypeOf(this, _ApiError.prototype);
33
33
  }
34
34
  };
35
+ var isObject = (v) => typeof v === "object" && v !== null;
36
+ function toApiError(thrown) {
37
+ if (thrown instanceof ApiError) return thrown;
38
+ if (isObject(thrown) && "response" in thrown && isObject(thrown.response)) {
39
+ const r = thrown.response;
40
+ const status = typeof r.status === "number" ? r.status : 0;
41
+ const data = isObject(r.data) ? r.data : void 0;
42
+ const message = (data && typeof data.message === "string" ? data.message : void 0) ?? (thrown instanceof Error ? thrown.message : void 0) ?? `HTTP ${status}`;
43
+ return new ApiError({
44
+ message,
45
+ status,
46
+ code: data && typeof data.code === "string" ? data.code : void 0,
47
+ errors: data?.errors,
48
+ isNetworkError: status === 0,
49
+ isUnauthorized: status === 401,
50
+ isValidationError: status === 422 || data?.errors !== void 0,
51
+ raw: r.data ?? thrown
52
+ });
53
+ }
54
+ if (thrown instanceof Error) {
55
+ return new ApiError({
56
+ message: thrown.message,
57
+ status: 0,
58
+ isNetworkError: true,
59
+ raw: thrown
60
+ });
61
+ }
62
+ return new ApiError({
63
+ message: "Unknown error",
64
+ status: 0,
65
+ raw: thrown
66
+ });
67
+ }
68
+ function businessErrorToApiError(response) {
69
+ if (!isObject(response)) {
70
+ return new ApiError({ message: "Request failed", status: 200, raw: response });
71
+ }
72
+ return new ApiError({
73
+ message: typeof response.message === "string" ? response.message : "Request failed",
74
+ status: 200,
75
+ code: typeof response.code === "string" ? response.code : void 0,
76
+ errors: response.errors,
77
+ isValidationError: response.errors !== void 0,
78
+ raw: response
79
+ });
80
+ }
35
81
 
36
82
  // src/utils.ts
37
83
  function buildEndpoint(endpoint, params) {
@@ -40,7 +86,7 @@ function buildEndpoint(endpoint, params) {
40
86
  }
41
87
  return endpoint;
42
88
  }
43
- async function executeRequest(endpoint, fetchConfig, params) {
89
+ async function executeRequest(endpoint, fetchConfig, params, signal) {
44
90
  const url = buildEndpoint(endpoint, params);
45
91
  const config = getConfig();
46
92
  try {
@@ -51,7 +97,8 @@ async function executeRequest(endpoint, fetchConfig, params) {
51
97
  params: {
52
98
  ...requestConfig.requestParams,
53
99
  ...params
54
- }
100
+ },
101
+ signal
55
102
  });
56
103
  } else {
57
104
  response = await config.httpClient.request(url, {
@@ -59,38 +106,29 @@ async function executeRequest(endpoint, fetchConfig, params) {
59
106
  data: {
60
107
  ...requestConfig.requestParams,
61
108
  ...params
62
- }
109
+ },
110
+ signal
63
111
  });
64
112
  }
65
113
  const hasExplicitStatus = response && typeof response === "object" && "status" in response && typeof response.status === "boolean";
66
114
  if (config.throwOnError && hasExplicitStatus && response.status === false) {
67
- const body = response;
68
- throw new ApiError({
69
- message: body.message ?? "Request failed",
70
- status: 200,
71
- errors: body.errors,
72
- raw: response
73
- });
115
+ throw businessErrorToApiError(response);
74
116
  }
75
117
  return { ...response, status: true };
76
118
  } catch (e) {
77
- if (e instanceof ApiError) throw e;
119
+ if (e instanceof ApiError) {
120
+ if (e.status === 401 && config.onUnauthorized) {
121
+ await config.onUnauthorized();
122
+ }
123
+ throw e;
124
+ }
78
125
  const error = e;
79
126
  const httpStatus = error.response?.status;
80
127
  if (httpStatus === 401 && config.onUnauthorized) {
81
128
  await config.onUnauthorized();
82
129
  }
83
130
  if (config.throwOnError) {
84
- const data = error.response?.data;
85
- const isNetwork = httpStatus === void 0;
86
- throw new ApiError({
87
- message: data?.message ?? (e instanceof Error ? e.message : void 0) ?? (isNetwork ? "Network error" : "Request error"),
88
- status: httpStatus ?? 0,
89
- code: data?.code,
90
- errors: data,
91
- isNetworkError: isNetwork,
92
- raw: e
93
- });
131
+ throw toApiError(e);
94
132
  }
95
133
  if (error.response?.data) {
96
134
  return {
@@ -251,6 +289,21 @@ var DEFAULT_STALE_TIME = 0;
251
289
  var QueryCache = class {
252
290
  constructor() {
253
291
  this.entries = /* @__PURE__ */ new Map();
292
+ this.globalListeners = /* @__PURE__ */ new Set();
293
+ }
294
+ /**
295
+ * Подписка на любое изменение кэша: setData, invalidate, remove,
296
+ * успешный/ошибочный fetch. Используется persistQueryClient и
297
+ * devtools-подобными адаптерами. Не дублирует `subscribe(key, ...)`.
298
+ */
299
+ subscribeAll(listener) {
300
+ this.globalListeners.add(listener);
301
+ return () => {
302
+ this.globalListeners.delete(listener);
303
+ };
304
+ }
305
+ notifyGlobal() {
306
+ for (const listener of this.globalListeners) listener();
254
307
  }
255
308
  ensureEntry(key, staleTime, gcTime) {
256
309
  const hash = hashQueryKey(key);
@@ -267,6 +320,7 @@ var QueryCache = class {
267
320
  },
268
321
  subscribers: /* @__PURE__ */ new Set(),
269
322
  inflight: null,
323
+ inflightController: null,
270
324
  gcTimer: null,
271
325
  staleTime: staleTime ?? DEFAULT_STALE_TIME,
272
326
  gcTime: gcTime ?? DEFAULT_GC_TIME
@@ -315,6 +369,7 @@ var QueryCache = class {
315
369
  }
316
370
  notify(entry) {
317
371
  for (const listener of entry.subscribers) listener();
372
+ this.notifyGlobal();
318
373
  }
319
374
  scheduleGc(entry) {
320
375
  if (entry.gcTimer) clearTimeout(entry.gcTimer);
@@ -343,12 +398,14 @@ var QueryCache = class {
343
398
  if (entry.inflight) return entry.inflight;
344
399
  entry.state = { ...entry.state, status: "loading", error: null };
345
400
  this.notify(entry);
401
+ const controller = new AbortController();
346
402
  const token = Symbol("inflight");
347
403
  entry.inflightToken = token;
404
+ entry.inflightController = controller;
348
405
  const isCurrent = () => entry.inflightToken === token;
349
406
  const myPromise = (async () => {
350
407
  try {
351
- const data = await queryFn();
408
+ const data = await queryFn({ signal: controller.signal });
352
409
  if (!isCurrent()) return data;
353
410
  entry.state = {
354
411
  data,
@@ -369,7 +426,10 @@ var QueryCache = class {
369
426
  this.notify(entry);
370
427
  throw err;
371
428
  } finally {
372
- if (isCurrent()) entry.inflight = null;
429
+ if (isCurrent()) {
430
+ entry.inflight = null;
431
+ entry.inflightController = null;
432
+ }
373
433
  }
374
434
  })();
375
435
  entry.inflight = myPromise;
@@ -401,6 +461,8 @@ var QueryCache = class {
401
461
  const match = typeof predicate === "function" ? predicate : (k) => matchQueryKey(predicate, k);
402
462
  for (const entry of this.entries.values()) {
403
463
  if (match(entry.key) && entry.inflight) {
464
+ entry.inflightController?.abort();
465
+ entry.inflightController = null;
404
466
  entry.inflight = null;
405
467
  entry.inflightToken = void 0;
406
468
  if (entry.state.status === "loading") {
@@ -416,12 +478,15 @@ var QueryCache = class {
416
478
  */
417
479
  remove(predicate) {
418
480
  const match = typeof predicate === "function" ? predicate : (k) => matchQueryKey(predicate, k);
481
+ let removed = false;
419
482
  for (const [hash, entry] of [...this.entries]) {
420
483
  if (match(entry.key)) {
421
484
  if (entry.gcTimer) clearTimeout(entry.gcTimer);
422
485
  this.entries.delete(hash);
486
+ removed = true;
423
487
  }
424
488
  }
489
+ if (removed) this.notifyGlobal();
425
490
  }
426
491
  /** Только для тестов / DevTools. */
427
492
  _debugEntries() {
@@ -526,18 +591,26 @@ function createUseFetch(endpoint, fetchConfig) {
526
591
  }, [customKey ? hashQueryKey(customKey) : null, serializedParams]);
527
592
  const client = getQueryClient();
528
593
  const cache = client.cache;
529
- const queryFn = useCallback(() => {
530
- const parsedParams = serializedParams ? JSON.parse(serializedParams) : void 0;
531
- return executeRequest(endpoint, fetchConfig, parsedParams);
532
- }, [serializedParams]);
594
+ const queryFn = useCallback(
595
+ ({ signal }) => {
596
+ const parsedParams = serializedParams ? JSON.parse(serializedParams) : void 0;
597
+ return executeRequest(endpoint, fetchConfig, parsedParams, signal);
598
+ },
599
+ [serializedParams]
600
+ );
533
601
  const initialState = cache.getState(queryKey);
534
602
  const [, forceRender] = useState(0);
535
603
  const rerender = useCallback(() => forceRender((v) => v + 1), []);
536
604
  useEffect(() => {
537
- if (!enabled) return;
538
605
  const unsub = cache.subscribe(queryKey, rerender);
539
- return unsub;
540
- }, [cache, enabled, hashQueryKey(queryKey), rerender]);
606
+ return () => {
607
+ unsub();
608
+ const state2 = cache._debugEntries().get(hashQueryKey(queryKey));
609
+ if (state2 && state2.subscribers.size === 0 && state2.inflight) {
610
+ cache.cancelQueries(queryKey);
611
+ }
612
+ };
613
+ }, [cache, hashQueryKey(queryKey), rerender]);
541
614
  const lastNotifiedRef = useRef({});
542
615
  const runFetch = useCallback(
543
616
  async (force) => {
@@ -797,14 +870,14 @@ function createUsePaginate(endpoint, fetchConfig, options) {
797
870
  [keyPrefix, limit]
798
871
  );
799
872
  const pageQueryFn = useCallback(
800
- (page) => () => {
873
+ (page) => ({ signal }) => {
801
874
  const parsedParams = serializedParams ? JSON.parse(serializedParams) : {};
802
875
  const requestParams = {
803
876
  ...parsedParams,
804
877
  page,
805
878
  limit
806
879
  };
807
- return executeRequest(endpoint, fetchConfig, requestParams);
880
+ return executeRequest(endpoint, fetchConfig, requestParams, signal);
808
881
  },
809
882
  [serializedParams, limit]
810
883
  );
@@ -935,19 +1008,26 @@ function persistQueryClient(options) {
935
1008
  maxAge,
936
1009
  version
937
1010
  } = options;
938
- let timer = null;
1011
+ let pendingTimer = null;
939
1012
  let lastSerialized = null;
1013
+ let unsubscribed = false;
940
1014
  const persist = async () => {
941
1015
  const state = client.cache.dehydrate(allowList);
942
- if (state.queries.length === 0) {
943
- return;
944
- }
1016
+ if (state.queries.length === 0) return;
945
1017
  const snap = { version, savedAt: Date.now(), state };
946
1018
  const serialized = JSON.stringify(snap);
947
1019
  if (serialized === lastSerialized) return;
948
1020
  lastSerialized = serialized;
949
1021
  await storage.setItem(storageKey, serialized);
950
1022
  };
1023
+ const scheduleWrite = () => {
1024
+ if (unsubscribed) return;
1025
+ if (pendingTimer) return;
1026
+ pendingTimer = setTimeout(() => {
1027
+ pendingTimer = null;
1028
+ void persist();
1029
+ }, throttleMs);
1030
+ };
951
1031
  const restore = async () => {
952
1032
  const raw = await storage.getItem(storageKey);
953
1033
  if (!raw) return;
@@ -966,15 +1046,17 @@ function persistQueryClient(options) {
966
1046
  await storage.removeItem(storageKey);
967
1047
  }
968
1048
  };
969
- timer = setInterval(() => {
970
- void persist();
971
- }, throttleMs);
1049
+ const unsubscribeFromCache = client.cache.subscribeAll(scheduleWrite);
972
1050
  return {
973
1051
  restore,
974
1052
  persist,
975
1053
  unsubscribe: () => {
976
- if (timer) clearInterval(timer);
977
- timer = null;
1054
+ unsubscribed = true;
1055
+ unsubscribeFromCache();
1056
+ if (pendingTimer) {
1057
+ clearTimeout(pendingTimer);
1058
+ pendingTimer = null;
1059
+ }
978
1060
  }
979
1061
  };
980
1062
  }
@@ -1075,6 +1157,6 @@ function apiPaginate(endpoint, fetchConfig = {}, options) {
1075
1157
  }
1076
1158
  var index_default = apiClient;
1077
1159
 
1078
- export { ApiClientProvider, ApiError, QueryCache, QueryClient, apiMutation, apiPaginate, configureApiClient, index_default as default, focusManager, getConfig, getQueryClient, hashQueryKey, inspectCache, invalidateAll, isConfigured, matchQueryKey, onlineManager, persistQueryClient, setQueryClient, summarizeCache, useQueryClient };
1160
+ export { ApiClientProvider, ApiError, QueryCache, QueryClient, apiMutation, apiPaginate, businessErrorToApiError, configureApiClient, index_default as default, focusManager, getConfig, getQueryClient, hashQueryKey, inspectCache, invalidateAll, isConfigured, matchQueryKey, onlineManager, persistQueryClient, setQueryClient, summarizeCache, toApiError, useQueryClient };
1079
1161
  //# sourceMappingURL=index.mjs.map
1080
1162
  //# sourceMappingURL=index.mjs.map