@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.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
|
-
|
|
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)
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
|
540
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
977
|
-
|
|
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
|