@krymskyimaksym/react-api-client 1.0.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 +406 -9
- package/dist/index.d.ts +406 -9
- package/dist/index.js +917 -147
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +902 -149
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createContext, useMemo, createElement, useContext, useCallback, useState, useEffect, useRef } from 'react';
|
|
2
2
|
|
|
3
3
|
// src/config.ts
|
|
4
4
|
var globalConfig = null;
|
|
@@ -17,6 +17,68 @@ function isConfigured() {
|
|
|
17
17
|
return globalConfig !== null;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// src/errors.ts
|
|
21
|
+
var ApiError = class _ApiError extends Error {
|
|
22
|
+
constructor(init) {
|
|
23
|
+
super(init.message);
|
|
24
|
+
this.name = "ApiError";
|
|
25
|
+
this.status = init.status;
|
|
26
|
+
this.code = init.code;
|
|
27
|
+
this.errors = init.errors;
|
|
28
|
+
this.isNetworkError = init.isNetworkError ?? false;
|
|
29
|
+
this.isUnauthorized = init.isUnauthorized ?? init.status === 401;
|
|
30
|
+
this.isValidationError = init.isValidationError ?? (init.status === 422 || init.errors !== void 0 && init.errors !== null);
|
|
31
|
+
this.raw = init.raw;
|
|
32
|
+
Object.setPrototypeOf(this, _ApiError.prototype);
|
|
33
|
+
}
|
|
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
|
+
}
|
|
81
|
+
|
|
20
82
|
// src/utils.ts
|
|
21
83
|
function buildEndpoint(endpoint, params) {
|
|
22
84
|
if (typeof endpoint === "function" && typeof params !== "undefined") {
|
|
@@ -24,7 +86,7 @@ function buildEndpoint(endpoint, params) {
|
|
|
24
86
|
}
|
|
25
87
|
return endpoint;
|
|
26
88
|
}
|
|
27
|
-
async function executeRequest(endpoint, fetchConfig, params) {
|
|
89
|
+
async function executeRequest(endpoint, fetchConfig, params, signal) {
|
|
28
90
|
const url = buildEndpoint(endpoint, params);
|
|
29
91
|
const config = getConfig();
|
|
30
92
|
try {
|
|
@@ -35,7 +97,8 @@ async function executeRequest(endpoint, fetchConfig, params) {
|
|
|
35
97
|
params: {
|
|
36
98
|
...requestConfig.requestParams,
|
|
37
99
|
...params
|
|
38
|
-
}
|
|
100
|
+
},
|
|
101
|
+
signal
|
|
39
102
|
});
|
|
40
103
|
} else {
|
|
41
104
|
response = await config.httpClient.request(url, {
|
|
@@ -43,15 +106,30 @@ async function executeRequest(endpoint, fetchConfig, params) {
|
|
|
43
106
|
data: {
|
|
44
107
|
...requestConfig.requestParams,
|
|
45
108
|
...params
|
|
46
|
-
}
|
|
109
|
+
},
|
|
110
|
+
signal
|
|
47
111
|
});
|
|
48
112
|
}
|
|
113
|
+
const hasExplicitStatus = response && typeof response === "object" && "status" in response && typeof response.status === "boolean";
|
|
114
|
+
if (config.throwOnError && hasExplicitStatus && response.status === false) {
|
|
115
|
+
throw businessErrorToApiError(response);
|
|
116
|
+
}
|
|
49
117
|
return { ...response, status: true };
|
|
50
118
|
} catch (e) {
|
|
119
|
+
if (e instanceof ApiError) {
|
|
120
|
+
if (e.status === 401 && config.onUnauthorized) {
|
|
121
|
+
await config.onUnauthorized();
|
|
122
|
+
}
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
51
125
|
const error = e;
|
|
52
|
-
|
|
126
|
+
const httpStatus = error.response?.status;
|
|
127
|
+
if (httpStatus === 401 && config.onUnauthorized) {
|
|
53
128
|
await config.onUnauthorized();
|
|
54
129
|
}
|
|
130
|
+
if (config.throwOnError) {
|
|
131
|
+
throw toApiError(e);
|
|
132
|
+
}
|
|
55
133
|
if (error.response?.data) {
|
|
56
134
|
return {
|
|
57
135
|
...error.response.data,
|
|
@@ -60,7 +138,7 @@ async function executeRequest(endpoint, fetchConfig, params) {
|
|
|
60
138
|
}
|
|
61
139
|
return {
|
|
62
140
|
status: false,
|
|
63
|
-
message:
|
|
141
|
+
message: e instanceof Error ? e.message : "Request error"
|
|
64
142
|
};
|
|
65
143
|
}
|
|
66
144
|
}
|
|
@@ -72,78 +150,562 @@ function handleResponse(result, onSuccess, onError) {
|
|
|
72
150
|
onError(err);
|
|
73
151
|
}
|
|
74
152
|
}
|
|
153
|
+
|
|
154
|
+
// src/query/focus-manager.ts
|
|
155
|
+
var FocusManager = class {
|
|
156
|
+
constructor() {
|
|
157
|
+
this.focused = true;
|
|
158
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
159
|
+
this.cleanup = null;
|
|
160
|
+
}
|
|
161
|
+
subscribe(listener) {
|
|
162
|
+
this.listeners.add(listener);
|
|
163
|
+
this.setupBrowserListeners();
|
|
164
|
+
return () => {
|
|
165
|
+
this.listeners.delete(listener);
|
|
166
|
+
if (this.listeners.size === 0) this.teardownBrowserListeners();
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
isFocused() {
|
|
170
|
+
return this.focused;
|
|
171
|
+
}
|
|
172
|
+
setFocused(focused) {
|
|
173
|
+
if (this.focused === focused) return;
|
|
174
|
+
this.focused = focused;
|
|
175
|
+
for (const l of this.listeners) l(focused);
|
|
176
|
+
}
|
|
177
|
+
setupBrowserListeners() {
|
|
178
|
+
if (this.cleanup) return;
|
|
179
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const onFocus = () => this.setFocused(true);
|
|
183
|
+
const onVisibility = () => {
|
|
184
|
+
if (typeof document !== "undefined") {
|
|
185
|
+
this.setFocused(document.visibilityState !== "hidden");
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
window.addEventListener("focus", onFocus);
|
|
189
|
+
if (typeof document !== "undefined") {
|
|
190
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
191
|
+
}
|
|
192
|
+
this.cleanup = () => {
|
|
193
|
+
window.removeEventListener("focus", onFocus);
|
|
194
|
+
if (typeof document !== "undefined") {
|
|
195
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
teardownBrowserListeners() {
|
|
200
|
+
this.cleanup?.();
|
|
201
|
+
this.cleanup = null;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var focusManager = new FocusManager();
|
|
205
|
+
|
|
206
|
+
// src/query/online-manager.ts
|
|
207
|
+
var OnlineManager = class {
|
|
208
|
+
constructor() {
|
|
209
|
+
this.online = true;
|
|
210
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
211
|
+
this.cleanup = null;
|
|
212
|
+
}
|
|
213
|
+
subscribe(listener) {
|
|
214
|
+
this.listeners.add(listener);
|
|
215
|
+
this.setupBrowserListeners();
|
|
216
|
+
return () => {
|
|
217
|
+
this.listeners.delete(listener);
|
|
218
|
+
if (this.listeners.size === 0) this.teardownBrowserListeners();
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
isOnline() {
|
|
222
|
+
return this.online;
|
|
223
|
+
}
|
|
224
|
+
setOnline(online) {
|
|
225
|
+
if (this.online === online) return;
|
|
226
|
+
this.online = online;
|
|
227
|
+
for (const l of this.listeners) l(online);
|
|
228
|
+
}
|
|
229
|
+
setupBrowserListeners() {
|
|
230
|
+
if (this.cleanup) return;
|
|
231
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (typeof navigator !== "undefined" && "onLine" in navigator) {
|
|
235
|
+
this.online = navigator.onLine;
|
|
236
|
+
}
|
|
237
|
+
const onOnline = () => this.setOnline(true);
|
|
238
|
+
const onOffline = () => this.setOnline(false);
|
|
239
|
+
window.addEventListener("online", onOnline);
|
|
240
|
+
window.addEventListener("offline", onOffline);
|
|
241
|
+
this.cleanup = () => {
|
|
242
|
+
window.removeEventListener("online", onOnline);
|
|
243
|
+
window.removeEventListener("offline", onOffline);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
teardownBrowserListeners() {
|
|
247
|
+
this.cleanup?.();
|
|
248
|
+
this.cleanup = null;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
var onlineManager = new OnlineManager();
|
|
252
|
+
|
|
253
|
+
// src/query/key.ts
|
|
254
|
+
function hashQueryKey(key) {
|
|
255
|
+
return JSON.stringify(key, (_, value) => {
|
|
256
|
+
if (typeof value === "function") {
|
|
257
|
+
throw new Error(
|
|
258
|
+
"react-api-client: \u0444\u0443\u043D\u043A\u0446\u0438\u0438 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u044B \u0432 queryKey \u2014 \u043A\u043B\u044E\u0447 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0441\u0435\u0440\u0438\u0430\u043B\u0438\u0437\u0443\u0435\u043C"
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (typeof value === "symbol") {
|
|
262
|
+
throw new Error(
|
|
263
|
+
"react-api-client: symbol \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u044B \u0432 queryKey \u2014 \u043A\u043B\u044E\u0447 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0441\u0435\u0440\u0438\u0430\u043B\u0438\u0437\u0443\u0435\u043C"
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
267
|
+
const sortedKeys = Object.keys(value).sort();
|
|
268
|
+
const result = {};
|
|
269
|
+
for (const k of sortedKeys) {
|
|
270
|
+
const v = value[k];
|
|
271
|
+
if (v !== void 0) result[k] = v;
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function matchQueryKey(prefix, key) {
|
|
279
|
+
if (prefix.length > key.length) return false;
|
|
280
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
281
|
+
if (hashQueryKey([prefix[i]]) !== hashQueryKey([key[i]])) return false;
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/query/cache.ts
|
|
287
|
+
var DEFAULT_GC_TIME = 5 * 60 * 1e3;
|
|
288
|
+
var DEFAULT_STALE_TIME = 0;
|
|
289
|
+
var QueryCache = class {
|
|
290
|
+
constructor() {
|
|
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();
|
|
307
|
+
}
|
|
308
|
+
ensureEntry(key, staleTime, gcTime) {
|
|
309
|
+
const hash = hashQueryKey(key);
|
|
310
|
+
let entry = this.entries.get(hash);
|
|
311
|
+
if (!entry) {
|
|
312
|
+
entry = {
|
|
313
|
+
key,
|
|
314
|
+
state: {
|
|
315
|
+
data: void 0,
|
|
316
|
+
error: null,
|
|
317
|
+
status: "idle",
|
|
318
|
+
updatedAt: 0,
|
|
319
|
+
isStale: true
|
|
320
|
+
},
|
|
321
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
322
|
+
inflight: null,
|
|
323
|
+
inflightController: null,
|
|
324
|
+
gcTimer: null,
|
|
325
|
+
staleTime: staleTime ?? DEFAULT_STALE_TIME,
|
|
326
|
+
gcTime: gcTime ?? DEFAULT_GC_TIME
|
|
327
|
+
};
|
|
328
|
+
this.entries.set(hash, entry);
|
|
329
|
+
} else {
|
|
330
|
+
if (staleTime !== void 0) entry.staleTime = staleTime;
|
|
331
|
+
if (gcTime !== void 0) entry.gcTime = gcTime;
|
|
332
|
+
}
|
|
333
|
+
return entry;
|
|
334
|
+
}
|
|
335
|
+
getState(key) {
|
|
336
|
+
const entry = this.entries.get(hashQueryKey(key));
|
|
337
|
+
return entry?.state;
|
|
338
|
+
}
|
|
339
|
+
getData(key) {
|
|
340
|
+
return this.getState(key)?.data;
|
|
341
|
+
}
|
|
342
|
+
setData(key, updater) {
|
|
343
|
+
const entry = this.ensureEntry(key);
|
|
344
|
+
const next = typeof updater === "function" ? updater(entry.state.data) : updater;
|
|
345
|
+
entry.state = {
|
|
346
|
+
data: next,
|
|
347
|
+
error: null,
|
|
348
|
+
status: "success",
|
|
349
|
+
updatedAt: Date.now(),
|
|
350
|
+
isStale: false
|
|
351
|
+
};
|
|
352
|
+
this.notify(entry);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Подписка на изменения ключа. Возвращает unsubscribe.
|
|
356
|
+
* Подписка останавливает GC таймер; отписка — запускает его обратно.
|
|
357
|
+
*/
|
|
358
|
+
subscribe(key, listener) {
|
|
359
|
+
const entry = this.ensureEntry(key);
|
|
360
|
+
entry.subscribers.add(listener);
|
|
361
|
+
if (entry.gcTimer) {
|
|
362
|
+
clearTimeout(entry.gcTimer);
|
|
363
|
+
entry.gcTimer = null;
|
|
364
|
+
}
|
|
365
|
+
return () => {
|
|
366
|
+
entry.subscribers.delete(listener);
|
|
367
|
+
if (entry.subscribers.size === 0) this.scheduleGc(entry);
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
notify(entry) {
|
|
371
|
+
for (const listener of entry.subscribers) listener();
|
|
372
|
+
this.notifyGlobal();
|
|
373
|
+
}
|
|
374
|
+
scheduleGc(entry) {
|
|
375
|
+
if (entry.gcTimer) clearTimeout(entry.gcTimer);
|
|
376
|
+
if (entry.gcTime <= 0) {
|
|
377
|
+
this.entries.delete(hashQueryKey(entry.key));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
entry.gcTimer = setTimeout(() => {
|
|
381
|
+
if (entry.subscribers.size === 0 && !entry.inflight) {
|
|
382
|
+
this.entries.delete(hashQueryKey(entry.key));
|
|
383
|
+
}
|
|
384
|
+
}, entry.gcTime);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Запускает или присоединяется к inflight-запросу.
|
|
388
|
+
* Если данные свежие (не stale) и не force — отдаёт кэш без запроса.
|
|
389
|
+
*/
|
|
390
|
+
async fetch(key, queryFn, options = {}) {
|
|
391
|
+
const staleTime = options.staleTime ?? DEFAULT_STALE_TIME;
|
|
392
|
+
const gcTime = options.gcTime ?? DEFAULT_GC_TIME;
|
|
393
|
+
const entry = this.ensureEntry(key, staleTime, gcTime);
|
|
394
|
+
const isFresh = entry.state.status === "success" && !entry.state.isStale && Date.now() - entry.state.updatedAt < staleTime;
|
|
395
|
+
if (!options.force && isFresh && entry.state.data !== void 0) {
|
|
396
|
+
return entry.state.data;
|
|
397
|
+
}
|
|
398
|
+
if (entry.inflight) return entry.inflight;
|
|
399
|
+
entry.state = { ...entry.state, status: "loading", error: null };
|
|
400
|
+
this.notify(entry);
|
|
401
|
+
const controller = new AbortController();
|
|
402
|
+
const token = Symbol("inflight");
|
|
403
|
+
entry.inflightToken = token;
|
|
404
|
+
entry.inflightController = controller;
|
|
405
|
+
const isCurrent = () => entry.inflightToken === token;
|
|
406
|
+
const myPromise = (async () => {
|
|
407
|
+
try {
|
|
408
|
+
const data = await queryFn({ signal: controller.signal });
|
|
409
|
+
if (!isCurrent()) return data;
|
|
410
|
+
entry.state = {
|
|
411
|
+
data,
|
|
412
|
+
error: null,
|
|
413
|
+
status: "success",
|
|
414
|
+
updatedAt: Date.now(),
|
|
415
|
+
isStale: false
|
|
416
|
+
};
|
|
417
|
+
this.notify(entry);
|
|
418
|
+
return data;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (!isCurrent()) throw err;
|
|
421
|
+
entry.state = {
|
|
422
|
+
...entry.state,
|
|
423
|
+
status: "error",
|
|
424
|
+
error: err
|
|
425
|
+
};
|
|
426
|
+
this.notify(entry);
|
|
427
|
+
throw err;
|
|
428
|
+
} finally {
|
|
429
|
+
if (isCurrent()) {
|
|
430
|
+
entry.inflight = null;
|
|
431
|
+
entry.inflightController = null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
})();
|
|
435
|
+
entry.inflight = myPromise;
|
|
436
|
+
return myPromise;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Помечает запись как stale. Сами по себе данные не удаляются.
|
|
440
|
+
* Если есть подписчики — они получат уведомление, чтобы инициировать refetch.
|
|
441
|
+
*/
|
|
442
|
+
invalidate(predicate) {
|
|
443
|
+
const invalidated = [];
|
|
444
|
+
const match = typeof predicate === "function" ? predicate : (k) => matchQueryKey(predicate, k);
|
|
445
|
+
for (const [hash, entry] of this.entries) {
|
|
446
|
+
if (match(entry.key)) {
|
|
447
|
+
entry.state = { ...entry.state, isStale: true };
|
|
448
|
+
this.notify(entry);
|
|
449
|
+
invalidated.push(hash);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return invalidated;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Отменяет «привязку» inflight-промиса к ключу. Сам HTTP-запрос
|
|
456
|
+
* продолжит исполняться (executeRequest не использует AbortSignal),
|
|
457
|
+
* но его результат больше не попадёт в кэш и не уведомит подписчиков.
|
|
458
|
+
* Полезно при размонтировании / при переключении страниц.
|
|
459
|
+
*/
|
|
460
|
+
cancelQueries(predicate) {
|
|
461
|
+
const match = typeof predicate === "function" ? predicate : (k) => matchQueryKey(predicate, k);
|
|
462
|
+
for (const entry of this.entries.values()) {
|
|
463
|
+
if (match(entry.key) && entry.inflight) {
|
|
464
|
+
entry.inflightController?.abort();
|
|
465
|
+
entry.inflightController = null;
|
|
466
|
+
entry.inflight = null;
|
|
467
|
+
entry.inflightToken = void 0;
|
|
468
|
+
if (entry.state.status === "loading") {
|
|
469
|
+
entry.state = { ...entry.state, status: "idle" };
|
|
470
|
+
this.notify(entry);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Полностью удаляет записи (даже с активными подписчиками).
|
|
477
|
+
* Используется редко — обычно достаточно invalidate.
|
|
478
|
+
*/
|
|
479
|
+
remove(predicate) {
|
|
480
|
+
const match = typeof predicate === "function" ? predicate : (k) => matchQueryKey(predicate, k);
|
|
481
|
+
let removed = false;
|
|
482
|
+
for (const [hash, entry] of [...this.entries]) {
|
|
483
|
+
if (match(entry.key)) {
|
|
484
|
+
if (entry.gcTimer) clearTimeout(entry.gcTimer);
|
|
485
|
+
this.entries.delete(hash);
|
|
486
|
+
removed = true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (removed) this.notifyGlobal();
|
|
490
|
+
}
|
|
491
|
+
/** Только для тестов / DevTools. */
|
|
492
|
+
_debugEntries() {
|
|
493
|
+
return this.entries;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Сериализует записи со статусом success — для persistence.
|
|
497
|
+
* inflight / loading / error не сохраняются, чтобы не гидратировать
|
|
498
|
+
* приложение в полу-загруженном состоянии.
|
|
499
|
+
*/
|
|
500
|
+
dehydrate(filter) {
|
|
501
|
+
const queries = [];
|
|
502
|
+
for (const entry of this.entries.values()) {
|
|
503
|
+
if (entry.state.status !== "success") continue;
|
|
504
|
+
if (entry.state.data === void 0) continue;
|
|
505
|
+
if (filter && !filter(entry.key)) continue;
|
|
506
|
+
queries.push({
|
|
507
|
+
key: entry.key,
|
|
508
|
+
data: entry.state.data,
|
|
509
|
+
updatedAt: entry.state.updatedAt
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return { queries };
|
|
513
|
+
}
|
|
514
|
+
hydrate(state) {
|
|
515
|
+
for (const q of state.queries) {
|
|
516
|
+
const entry = this.ensureEntry(q.key);
|
|
517
|
+
if (entry.state.updatedAt >= q.updatedAt && entry.state.data !== void 0) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
entry.state = {
|
|
521
|
+
data: q.data,
|
|
522
|
+
error: null,
|
|
523
|
+
status: "success",
|
|
524
|
+
updatedAt: q.updatedAt,
|
|
525
|
+
isStale: true
|
|
526
|
+
// гидратированные данные сразу stale → фоновый refetch
|
|
527
|
+
};
|
|
528
|
+
this.notify(entry);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// src/query/client.ts
|
|
534
|
+
var QueryClient = class {
|
|
535
|
+
constructor(cache) {
|
|
536
|
+
this.cache = cache ?? new QueryCache();
|
|
537
|
+
}
|
|
538
|
+
getQueryData(key) {
|
|
539
|
+
return this.cache.getData(key);
|
|
540
|
+
}
|
|
541
|
+
setQueryData(key, updater) {
|
|
542
|
+
this.cache.setData(key, updater);
|
|
543
|
+
}
|
|
544
|
+
fetchQuery(key, queryFn, options) {
|
|
545
|
+
return this.cache.fetch(key, queryFn, options);
|
|
546
|
+
}
|
|
547
|
+
invalidateQueries(predicate) {
|
|
548
|
+
this.cache.invalidate(predicate);
|
|
549
|
+
}
|
|
550
|
+
removeQueries(predicate) {
|
|
551
|
+
this.cache.remove(predicate);
|
|
552
|
+
}
|
|
553
|
+
cancelQueries(predicate) {
|
|
554
|
+
this.cache.cancelQueries(predicate);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
var globalClient = null;
|
|
558
|
+
function getQueryClient() {
|
|
559
|
+
if (!globalClient) globalClient = new QueryClient();
|
|
560
|
+
return globalClient;
|
|
561
|
+
}
|
|
562
|
+
function setQueryClient(client) {
|
|
563
|
+
globalClient = client;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/hooks/use-fetch.ts
|
|
75
567
|
function createUseFetch(endpoint, fetchConfig) {
|
|
76
568
|
return (params, options = {}) => {
|
|
77
569
|
const {
|
|
78
570
|
enabled = true,
|
|
79
571
|
refetchOnMount = true,
|
|
572
|
+
refetchOnFocus = false,
|
|
573
|
+
refetchOnAppActive = false,
|
|
574
|
+
refetchOnReconnect = false,
|
|
575
|
+
staleTime = 0,
|
|
576
|
+
gcTime,
|
|
577
|
+
pollingInterval,
|
|
578
|
+
queryKey: customKey,
|
|
579
|
+
select,
|
|
80
580
|
onSuccess,
|
|
81
581
|
onError
|
|
82
582
|
} = options;
|
|
83
|
-
const [data, setData] = useState(null);
|
|
84
|
-
const [isLoading, setIsLoading] = useState(enabled && refetchOnMount);
|
|
85
|
-
const [isRefetching, setIsRefetching] = useState(false);
|
|
86
|
-
const [error, setError] = useState(null);
|
|
87
|
-
const isMountedRef = useRef(true);
|
|
88
583
|
const serializedParams = useMemo(
|
|
89
|
-
() => params ? JSON.stringify(params)
|
|
584
|
+
() => params === void 0 ? null : JSON.stringify(params),
|
|
90
585
|
[params]
|
|
91
586
|
);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
587
|
+
const queryKey = useMemo(() => {
|
|
588
|
+
if (customKey) return customKey;
|
|
589
|
+
const endpointId = typeof endpoint === "function" ? buildEndpoint(endpoint, params) : endpoint;
|
|
590
|
+
return ["__endpoint__", endpointId, params ?? null];
|
|
591
|
+
}, [customKey ? hashQueryKey(customKey) : null, serializedParams]);
|
|
592
|
+
const client = getQueryClient();
|
|
593
|
+
const cache = client.cache;
|
|
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
|
+
);
|
|
601
|
+
const initialState = cache.getState(queryKey);
|
|
602
|
+
const [, forceRender] = useState(0);
|
|
603
|
+
const rerender = useCallback(() => forceRender((v) => v + 1), []);
|
|
604
|
+
useEffect(() => {
|
|
605
|
+
const unsub = cache.subscribe(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]);
|
|
614
|
+
const lastNotifiedRef = useRef({});
|
|
615
|
+
const runFetch = useCallback(
|
|
616
|
+
async (force) => {
|
|
95
617
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const result = await executeRequest(endpoint, fetchConfig, parsedParams);
|
|
104
|
-
if (isMountedRef.current) {
|
|
105
|
-
setData(result);
|
|
618
|
+
const data = await cache.fetch(queryKey, queryFn, {
|
|
619
|
+
staleTime,
|
|
620
|
+
gcTime,
|
|
621
|
+
force
|
|
622
|
+
});
|
|
623
|
+
if (lastNotifiedRef.current.success !== data) {
|
|
624
|
+
lastNotifiedRef.current.success = data;
|
|
106
625
|
handleResponse(
|
|
107
|
-
|
|
626
|
+
data,
|
|
108
627
|
onSuccess,
|
|
109
628
|
onError
|
|
110
629
|
);
|
|
111
630
|
}
|
|
112
631
|
} catch (err) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
} finally {
|
|
121
|
-
if (isMountedRef.current) {
|
|
122
|
-
if (isRefetch) {
|
|
123
|
-
setIsRefetching(false);
|
|
124
|
-
} else {
|
|
125
|
-
setIsLoading(false);
|
|
126
|
-
}
|
|
632
|
+
const e = err;
|
|
633
|
+
const hash = `${e.name}:${e.message}`;
|
|
634
|
+
if (lastNotifiedRef.current.errorHash !== hash) {
|
|
635
|
+
lastNotifiedRef.current.errorHash = hash;
|
|
636
|
+
onError?.(e);
|
|
127
637
|
}
|
|
128
638
|
}
|
|
129
639
|
},
|
|
130
|
-
[
|
|
640
|
+
[cache, hashQueryKey(queryKey), queryFn, staleTime, gcTime, onSuccess, onError]
|
|
131
641
|
);
|
|
132
|
-
const refetch = useCallback(async () => {
|
|
133
|
-
await fetchData(true);
|
|
134
|
-
}, [fetchData]);
|
|
135
642
|
useEffect(() => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
643
|
+
if (!enabled || !refetchOnMount) return;
|
|
644
|
+
void runFetch(false);
|
|
645
|
+
}, [enabled, refetchOnMount, runFetch]);
|
|
646
|
+
const stateForEffect = cache.getState(queryKey);
|
|
647
|
+
const isStale = stateForEffect?.isStale ?? false;
|
|
648
|
+
const hasFetchedData = stateForEffect?.data !== void 0;
|
|
649
|
+
useEffect(() => {
|
|
650
|
+
if (!enabled) return;
|
|
651
|
+
if (isStale && hasFetchedData) void runFetch(true);
|
|
652
|
+
}, [enabled, isStale, hasFetchedData, runFetch]);
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
if (!enabled) return;
|
|
655
|
+
if (!refetchOnFocus && !refetchOnAppActive) return;
|
|
656
|
+
const unsub = focusManager.subscribe((focused) => {
|
|
657
|
+
if (focused) void runFetch(false);
|
|
658
|
+
});
|
|
659
|
+
return unsub;
|
|
660
|
+
}, [enabled, refetchOnFocus, refetchOnAppActive, runFetch]);
|
|
661
|
+
useEffect(() => {
|
|
662
|
+
if (!enabled || !refetchOnReconnect) return;
|
|
663
|
+
const unsub = onlineManager.subscribe((online) => {
|
|
664
|
+
if (online) void runFetch(false);
|
|
665
|
+
});
|
|
666
|
+
return unsub;
|
|
667
|
+
}, [enabled, refetchOnReconnect, runFetch]);
|
|
668
|
+
useEffect(() => {
|
|
669
|
+
if (!enabled || !pollingInterval || pollingInterval <= 0) return;
|
|
670
|
+
let timer = null;
|
|
671
|
+
const start = () => {
|
|
672
|
+
if (timer) return;
|
|
673
|
+
timer = setInterval(() => {
|
|
674
|
+
if (focusManager.isFocused()) void runFetch(true);
|
|
675
|
+
}, pollingInterval);
|
|
676
|
+
};
|
|
677
|
+
const stop = () => {
|
|
678
|
+
if (timer) clearInterval(timer);
|
|
679
|
+
timer = null;
|
|
680
|
+
};
|
|
681
|
+
start();
|
|
682
|
+
const unsub = focusManager.subscribe((focused) => {
|
|
683
|
+
if (focused) start();
|
|
684
|
+
else stop();
|
|
685
|
+
});
|
|
140
686
|
return () => {
|
|
141
|
-
|
|
687
|
+
stop();
|
|
688
|
+
unsub();
|
|
142
689
|
};
|
|
143
|
-
}, [enabled,
|
|
690
|
+
}, [enabled, pollingInterval, runFetch]);
|
|
691
|
+
const state = cache.getState(queryKey) ?? initialState;
|
|
692
|
+
const rawData = state?.data ?? null;
|
|
693
|
+
const selectedData = useMemo(() => {
|
|
694
|
+
if (rawData === null) return null;
|
|
695
|
+
if (!select) return rawData;
|
|
696
|
+
return select(rawData);
|
|
697
|
+
}, [rawData, select]);
|
|
698
|
+
const status = state?.status ?? "idle";
|
|
699
|
+
const hasData = rawData !== null;
|
|
700
|
+
const isLoading = status === "loading" && !hasData;
|
|
701
|
+
const isRefetching = status === "loading" && hasData;
|
|
702
|
+
const error = state?.error ?? null;
|
|
703
|
+
const refetch = useCallback(async () => {
|
|
704
|
+
await runFetch(true);
|
|
705
|
+
}, [runFetch]);
|
|
144
706
|
return {
|
|
145
|
-
data,
|
|
146
|
-
isLoading,
|
|
707
|
+
data: selectedData,
|
|
708
|
+
isLoading: enabled ? isLoading : false,
|
|
147
709
|
isRefetching,
|
|
148
710
|
error,
|
|
149
711
|
refetch
|
|
@@ -152,7 +714,14 @@ function createUseFetch(endpoint, fetchConfig) {
|
|
|
152
714
|
}
|
|
153
715
|
function createUseMutation(endpoint, fetchConfig) {
|
|
154
716
|
return (options = {}) => {
|
|
155
|
-
const {
|
|
717
|
+
const {
|
|
718
|
+
onMutate,
|
|
719
|
+
onSuccess,
|
|
720
|
+
onError,
|
|
721
|
+
onSettled,
|
|
722
|
+
invalidateKeys,
|
|
723
|
+
setQueryData
|
|
724
|
+
} = options;
|
|
156
725
|
const [data, setData] = useState(null);
|
|
157
726
|
const [error, setError] = useState(null);
|
|
158
727
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -165,33 +734,51 @@ function createUseMutation(endpoint, fetchConfig) {
|
|
|
165
734
|
setIsSuccess(false);
|
|
166
735
|
setIsError(false);
|
|
167
736
|
}, []);
|
|
737
|
+
const applyInvalidate = useCallback(
|
|
738
|
+
(vars, result) => {
|
|
739
|
+
if (!invalidateKeys) return;
|
|
740
|
+
const client = getQueryClient();
|
|
741
|
+
const keys = typeof invalidateKeys === "function" ? invalidateKeys(vars, result) : invalidateKeys;
|
|
742
|
+
if (keys.length === 0) return;
|
|
743
|
+
client.invalidateQueries(
|
|
744
|
+
(k) => keys.some((prefix) => matchQueryKey(prefix, k))
|
|
745
|
+
);
|
|
746
|
+
},
|
|
747
|
+
[invalidateKeys]
|
|
748
|
+
);
|
|
168
749
|
const mutateAsync = useCallback(
|
|
169
750
|
async (variables) => {
|
|
170
751
|
setIsLoading(true);
|
|
171
752
|
setIsSuccess(false);
|
|
172
753
|
setIsError(false);
|
|
173
754
|
setError(null);
|
|
755
|
+
let context;
|
|
174
756
|
try {
|
|
175
757
|
if (onMutate) {
|
|
176
|
-
await onMutate(variables);
|
|
758
|
+
const ctx = await onMutate(variables);
|
|
759
|
+
context = ctx;
|
|
177
760
|
}
|
|
178
761
|
const result = await executeRequest(endpoint, fetchConfig, variables);
|
|
179
762
|
setData(result);
|
|
180
763
|
if (result.status) {
|
|
181
764
|
setIsSuccess(true);
|
|
765
|
+
if (setQueryData) {
|
|
766
|
+
setQueryData(getQueryClient(), variables, result);
|
|
767
|
+
}
|
|
768
|
+
applyInvalidate(variables, result);
|
|
182
769
|
if (onSuccess) {
|
|
183
|
-
await onSuccess(result, variables);
|
|
770
|
+
await onSuccess(result, variables, context);
|
|
184
771
|
}
|
|
185
772
|
} else {
|
|
186
773
|
const err = new Error(result.message ?? "Mutation failed");
|
|
187
774
|
setIsError(true);
|
|
188
775
|
setError(err);
|
|
189
776
|
if (onError) {
|
|
190
|
-
await onError(err, variables);
|
|
777
|
+
await onError(err, variables, context);
|
|
191
778
|
}
|
|
192
779
|
}
|
|
193
780
|
if (onSettled) {
|
|
194
|
-
await onSettled(result, null, variables);
|
|
781
|
+
await onSettled(result, null, variables, context);
|
|
195
782
|
}
|
|
196
783
|
return result;
|
|
197
784
|
} catch (err) {
|
|
@@ -200,17 +787,24 @@ function createUseMutation(endpoint, fetchConfig) {
|
|
|
200
787
|
setIsError(true);
|
|
201
788
|
setIsSuccess(false);
|
|
202
789
|
if (onError) {
|
|
203
|
-
await onError(error2, variables);
|
|
790
|
+
await onError(error2, variables, context);
|
|
204
791
|
}
|
|
205
792
|
if (onSettled) {
|
|
206
|
-
await onSettled(null, error2, variables);
|
|
793
|
+
await onSettled(null, error2, variables, context);
|
|
207
794
|
}
|
|
208
795
|
throw error2;
|
|
209
796
|
} finally {
|
|
210
797
|
setIsLoading(false);
|
|
211
798
|
}
|
|
212
799
|
},
|
|
213
|
-
[
|
|
800
|
+
[
|
|
801
|
+
onMutate,
|
|
802
|
+
onSuccess,
|
|
803
|
+
onError,
|
|
804
|
+
onSettled,
|
|
805
|
+
setQueryData,
|
|
806
|
+
applyInvalidate
|
|
807
|
+
]
|
|
214
808
|
);
|
|
215
809
|
const mutateSync = useCallback(
|
|
216
810
|
(variables) => {
|
|
@@ -236,127 +830,153 @@ function createUsePaginate(endpoint, fetchConfig, options) {
|
|
|
236
830
|
enabled = true,
|
|
237
831
|
initialPage = 1,
|
|
238
832
|
initialLimit = 20,
|
|
833
|
+
staleTime = 0,
|
|
834
|
+
gcTime,
|
|
835
|
+
keepPreviousData = false,
|
|
836
|
+
queryKey: customKey,
|
|
239
837
|
onSuccess,
|
|
240
838
|
onError
|
|
241
839
|
} = hookOptions;
|
|
242
|
-
const
|
|
840
|
+
const client = getQueryClient();
|
|
841
|
+
const cache = client.cache;
|
|
842
|
+
const limit = initialLimit;
|
|
243
843
|
const [currentPage, setCurrentPage] = useState(initialPage);
|
|
244
|
-
const [totalPages, setTotalPages] = useState(null);
|
|
245
|
-
const [total, setTotal] = useState(null);
|
|
246
|
-
const [isLoading, setIsLoading] = useState(enabled);
|
|
247
844
|
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
|
|
248
845
|
const [error, setError] = useState(null);
|
|
249
|
-
const isMountedRef = useRef(true);
|
|
250
|
-
const limit = initialLimit;
|
|
251
846
|
const dataExtractor = useMemo(
|
|
252
847
|
() => options?.dataExtractor || ((response) => response.data),
|
|
848
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
253
849
|
[]
|
|
254
850
|
);
|
|
255
851
|
const totalExtractor = useMemo(
|
|
256
852
|
() => options?.totalExtractor || ((response) => response.total ?? 0),
|
|
853
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
257
854
|
[]
|
|
258
855
|
);
|
|
259
856
|
const serializedParams = useMemo(
|
|
260
|
-
() => params ? JSON.stringify(params)
|
|
857
|
+
() => params === void 0 ? null : JSON.stringify(params),
|
|
261
858
|
[params]
|
|
262
859
|
);
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
860
|
+
const keyPrefix = useMemo(() => {
|
|
861
|
+
if (customKey) return customKey;
|
|
862
|
+
const endpointId = typeof endpoint === "function" ? buildEndpoint(
|
|
863
|
+
endpoint,
|
|
864
|
+
params
|
|
865
|
+
) : endpoint;
|
|
866
|
+
return ["__paginate__", endpointId, params ?? null];
|
|
867
|
+
}, [customKey ? hashQueryKey(customKey) : null, serializedParams]);
|
|
868
|
+
const pageKey = useCallback(
|
|
869
|
+
(page) => [...keyPrefix, { page, limit }],
|
|
870
|
+
[keyPrefix, limit]
|
|
871
|
+
);
|
|
872
|
+
const pageQueryFn = useCallback(
|
|
873
|
+
(page) => ({ signal }) => {
|
|
874
|
+
const parsedParams = serializedParams ? JSON.parse(serializedParams) : {};
|
|
875
|
+
const requestParams = {
|
|
876
|
+
...parsedParams,
|
|
877
|
+
page,
|
|
878
|
+
limit
|
|
879
|
+
};
|
|
880
|
+
return executeRequest(endpoint, fetchConfig, requestParams, signal);
|
|
881
|
+
},
|
|
882
|
+
[serializedParams, limit]
|
|
883
|
+
);
|
|
884
|
+
const [, forceRender] = useState(0);
|
|
885
|
+
const rerender = useCallback(() => forceRender((v) => v + 1), []);
|
|
886
|
+
useEffect(() => {
|
|
887
|
+
if (!enabled) return;
|
|
888
|
+
const unsub = cache.subscribe(pageKey(currentPage), rerender);
|
|
889
|
+
return unsub;
|
|
890
|
+
}, [cache, enabled, hashQueryKey(pageKey(currentPage)), rerender]);
|
|
891
|
+
const previousPageKeyRef = useRef(null);
|
|
892
|
+
const runFetchPage = useCallback(
|
|
893
|
+
async (page, isNextPage) => {
|
|
266
894
|
try {
|
|
267
|
-
if (
|
|
268
|
-
setIsFetchingNextPage(true);
|
|
269
|
-
} else {
|
|
270
|
-
setIsLoading(true);
|
|
271
|
-
}
|
|
895
|
+
if (isNextPage) setIsFetchingNextPage(true);
|
|
272
896
|
setError(null);
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
286
|
-
if (!result.status) {
|
|
287
|
-
const err = new Error(result.message ?? "Request failed");
|
|
288
|
-
setError(err);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
const newData = dataExtractor(result);
|
|
292
|
-
const totalCount = totalExtractor(result);
|
|
293
|
-
if (append) {
|
|
294
|
-
setData((prevData) => [...prevData, ...newData]);
|
|
295
|
-
} else {
|
|
296
|
-
setData(newData);
|
|
297
|
-
}
|
|
298
|
-
setCurrentPage(page);
|
|
299
|
-
setTotal(totalCount);
|
|
300
|
-
setTotalPages(Math.ceil(totalCount / limit));
|
|
897
|
+
const result = await cache.fetch(pageKey(page), pageQueryFn(page), {
|
|
898
|
+
staleTime,
|
|
899
|
+
gcTime
|
|
900
|
+
});
|
|
901
|
+
handleResponse(
|
|
902
|
+
result,
|
|
903
|
+
onSuccess,
|
|
904
|
+
onError
|
|
905
|
+
);
|
|
906
|
+
if (!result.status) {
|
|
907
|
+
setError(new Error(result.message ?? "Request failed"));
|
|
908
|
+
return;
|
|
301
909
|
}
|
|
910
|
+
previousPageKeyRef.current = pageKey(page);
|
|
911
|
+
setCurrentPage(page);
|
|
302
912
|
} catch (err) {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (onError) {
|
|
307
|
-
onError(error2);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
913
|
+
const e = err;
|
|
914
|
+
setError(e);
|
|
915
|
+
onError?.(e);
|
|
310
916
|
} finally {
|
|
311
|
-
if (
|
|
312
|
-
if (append) {
|
|
313
|
-
setIsFetchingNextPage(false);
|
|
314
|
-
} else {
|
|
315
|
-
setIsLoading(false);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
917
|
+
if (isNextPage) setIsFetchingNextPage(false);
|
|
318
918
|
}
|
|
319
919
|
},
|
|
320
|
-
[
|
|
321
|
-
enabled,
|
|
322
|
-
serializedParams,
|
|
323
|
-
limit,
|
|
324
|
-
onSuccess,
|
|
325
|
-
onError,
|
|
326
|
-
dataExtractor,
|
|
327
|
-
totalExtractor
|
|
328
|
-
]
|
|
920
|
+
[cache, pageKey, pageQueryFn, staleTime, gcTime, onSuccess, onError]
|
|
329
921
|
);
|
|
922
|
+
useEffect(() => {
|
|
923
|
+
if (!enabled) return;
|
|
924
|
+
void runFetchPage(initialPage, false);
|
|
925
|
+
setCurrentPage(initialPage);
|
|
926
|
+
}, [enabled, serializedParams, initialPage]);
|
|
927
|
+
const currentState = cache.getState(pageKey(currentPage));
|
|
928
|
+
const isStale = currentState?.isStale ?? false;
|
|
929
|
+
const hasFetchedData = currentState?.data !== void 0;
|
|
930
|
+
useEffect(() => {
|
|
931
|
+
if (!enabled) return;
|
|
932
|
+
if (isStale && hasFetchedData) void runFetchPage(currentPage, false);
|
|
933
|
+
}, [enabled, isStale, hasFetchedData, currentPage, runFetchPage]);
|
|
934
|
+
const currentResult = currentState?.data;
|
|
935
|
+
const usingPlaceholder = keepPreviousData && !currentResult && previousPageKeyRef.current !== null;
|
|
936
|
+
const effectiveResult = usingPlaceholder ? cache.getData(previousPageKeyRef.current) : currentResult;
|
|
937
|
+
const data = effectiveResult && effectiveResult.status ? dataExtractor(effectiveResult) : [];
|
|
938
|
+
const totalCount = effectiveResult && effectiveResult.status ? totalExtractor(effectiveResult) : null;
|
|
939
|
+
const total = totalCount;
|
|
940
|
+
const totalPages = totalCount !== null ? Math.ceil(totalCount / limit) : null;
|
|
330
941
|
const hasNextPage = totalPages !== null && currentPage < totalPages;
|
|
331
942
|
const hasPreviousPage = currentPage > 1;
|
|
943
|
+
const status = currentState?.status ?? "idle";
|
|
944
|
+
const isLoading = status === "loading" && !hasFetchedData && !usingPlaceholder;
|
|
332
945
|
const fetchNextPage = useCallback(async () => {
|
|
333
946
|
if (!hasNextPage) return;
|
|
334
|
-
await
|
|
335
|
-
}, [hasNextPage, currentPage,
|
|
947
|
+
await runFetchPage(currentPage + 1, true);
|
|
948
|
+
}, [hasNextPage, currentPage, runFetchPage]);
|
|
336
949
|
const fetchPreviousPage = useCallback(async () => {
|
|
337
950
|
if (!hasPreviousPage) return;
|
|
338
|
-
await
|
|
339
|
-
}, [hasPreviousPage, currentPage,
|
|
951
|
+
await runFetchPage(currentPage - 1, false);
|
|
952
|
+
}, [hasPreviousPage, currentPage, runFetchPage]);
|
|
953
|
+
const prefetchNextPage = useCallback(async () => {
|
|
954
|
+
if (!hasNextPage) return;
|
|
955
|
+
await cache.fetch(
|
|
956
|
+
pageKey(currentPage + 1),
|
|
957
|
+
pageQueryFn(currentPage + 1),
|
|
958
|
+
{ staleTime, gcTime }
|
|
959
|
+
);
|
|
960
|
+
}, [hasNextPage, currentPage, cache, pageKey, pageQueryFn, staleTime, gcTime]);
|
|
340
961
|
const refetch = useCallback(async () => {
|
|
341
|
-
await
|
|
342
|
-
|
|
962
|
+
await cache.fetch(pageKey(currentPage), pageQueryFn(currentPage), {
|
|
963
|
+
staleTime: 0,
|
|
964
|
+
gcTime,
|
|
965
|
+
force: true
|
|
966
|
+
});
|
|
967
|
+
}, [cache, pageKey, pageQueryFn, currentPage, gcTime]);
|
|
343
968
|
const reset = useCallback(() => {
|
|
344
|
-
|
|
969
|
+
cache.remove((k) => {
|
|
970
|
+
if (k.length < keyPrefix.length) return false;
|
|
971
|
+
for (let i = 0; i < keyPrefix.length; i++) {
|
|
972
|
+
if (hashQueryKey([k[i]]) !== hashQueryKey([keyPrefix[i]])) return false;
|
|
973
|
+
}
|
|
974
|
+
return true;
|
|
975
|
+
});
|
|
976
|
+
previousPageKeyRef.current = null;
|
|
345
977
|
setCurrentPage(initialPage);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
setError(null);
|
|
349
|
-
void fetchPage(initialPage, false);
|
|
350
|
-
}, [initialPage, fetchPage]);
|
|
351
|
-
useEffect(() => {
|
|
352
|
-
isMountedRef.current = true;
|
|
353
|
-
if (enabled) {
|
|
354
|
-
void fetchPage(initialPage, false);
|
|
355
|
-
}
|
|
356
|
-
return () => {
|
|
357
|
-
isMountedRef.current = false;
|
|
358
|
-
};
|
|
359
|
-
}, [enabled, serializedParams, fetchPage, initialPage]);
|
|
978
|
+
void runFetchPage(initialPage, false);
|
|
979
|
+
}, [cache, keyPrefix, initialPage, runFetchPage]);
|
|
360
980
|
return {
|
|
361
981
|
data,
|
|
362
982
|
currentPage,
|
|
@@ -364,17 +984,150 @@ function createUsePaginate(endpoint, fetchConfig, options) {
|
|
|
364
984
|
total,
|
|
365
985
|
hasNextPage,
|
|
366
986
|
hasPreviousPage,
|
|
367
|
-
isLoading,
|
|
987
|
+
isLoading: enabled ? isLoading : false,
|
|
368
988
|
isFetchingNextPage,
|
|
989
|
+
isPlaceholderData: usingPlaceholder,
|
|
369
990
|
error,
|
|
370
991
|
fetchNextPage,
|
|
371
992
|
fetchPreviousPage,
|
|
993
|
+
prefetchNextPage,
|
|
372
994
|
refetch,
|
|
373
995
|
reset
|
|
374
996
|
};
|
|
375
997
|
};
|
|
376
998
|
}
|
|
377
999
|
|
|
1000
|
+
// src/query/persist.ts
|
|
1001
|
+
function persistQueryClient(options) {
|
|
1002
|
+
const {
|
|
1003
|
+
client,
|
|
1004
|
+
storage,
|
|
1005
|
+
storageKey = "react-api-client:cache",
|
|
1006
|
+
throttleMs = 1e3,
|
|
1007
|
+
allowList,
|
|
1008
|
+
maxAge,
|
|
1009
|
+
version
|
|
1010
|
+
} = options;
|
|
1011
|
+
let pendingTimer = null;
|
|
1012
|
+
let lastSerialized = null;
|
|
1013
|
+
let unsubscribed = false;
|
|
1014
|
+
const persist = async () => {
|
|
1015
|
+
const state = client.cache.dehydrate(allowList);
|
|
1016
|
+
if (state.queries.length === 0) return;
|
|
1017
|
+
const snap = { version, savedAt: Date.now(), state };
|
|
1018
|
+
const serialized = JSON.stringify(snap);
|
|
1019
|
+
if (serialized === lastSerialized) return;
|
|
1020
|
+
lastSerialized = serialized;
|
|
1021
|
+
await storage.setItem(storageKey, serialized);
|
|
1022
|
+
};
|
|
1023
|
+
const scheduleWrite = () => {
|
|
1024
|
+
if (unsubscribed) return;
|
|
1025
|
+
if (pendingTimer) return;
|
|
1026
|
+
pendingTimer = setTimeout(() => {
|
|
1027
|
+
pendingTimer = null;
|
|
1028
|
+
void persist();
|
|
1029
|
+
}, throttleMs);
|
|
1030
|
+
};
|
|
1031
|
+
const restore = async () => {
|
|
1032
|
+
const raw = await storage.getItem(storageKey);
|
|
1033
|
+
if (!raw) return;
|
|
1034
|
+
try {
|
|
1035
|
+
const snap = JSON.parse(raw);
|
|
1036
|
+
if (version !== void 0 && snap.version !== version) {
|
|
1037
|
+
await storage.removeItem(storageKey);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (maxAge && Date.now() - snap.savedAt > maxAge) {
|
|
1041
|
+
await storage.removeItem(storageKey);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
client.cache.hydrate(snap.state);
|
|
1045
|
+
} catch {
|
|
1046
|
+
await storage.removeItem(storageKey);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
const unsubscribeFromCache = client.cache.subscribeAll(scheduleWrite);
|
|
1050
|
+
return {
|
|
1051
|
+
restore,
|
|
1052
|
+
persist,
|
|
1053
|
+
unsubscribe: () => {
|
|
1054
|
+
unsubscribed = true;
|
|
1055
|
+
unsubscribeFromCache();
|
|
1056
|
+
if (pendingTimer) {
|
|
1057
|
+
clearTimeout(pendingTimer);
|
|
1058
|
+
pendingTimer = null;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/query/devtools.ts
|
|
1065
|
+
function inspectCache(cache) {
|
|
1066
|
+
const entries = cache._debugEntries();
|
|
1067
|
+
const snapshot = [];
|
|
1068
|
+
for (const [hash, entry] of entries) {
|
|
1069
|
+
snapshot.push({
|
|
1070
|
+
key: entry.key,
|
|
1071
|
+
hash,
|
|
1072
|
+
status: entry.state.status,
|
|
1073
|
+
isStale: entry.state.isStale,
|
|
1074
|
+
updatedAt: entry.state.updatedAt,
|
|
1075
|
+
hasData: entry.state.data !== void 0,
|
|
1076
|
+
subscribers: entry.subscribers.size,
|
|
1077
|
+
hasInflight: entry.inflight !== null,
|
|
1078
|
+
errorMessage: entry.state.error?.message ?? null
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
return snapshot;
|
|
1082
|
+
}
|
|
1083
|
+
function invalidateAll(client) {
|
|
1084
|
+
client.invalidateQueries(() => true);
|
|
1085
|
+
}
|
|
1086
|
+
function summarizeCache(cache) {
|
|
1087
|
+
const byStatus = {
|
|
1088
|
+
idle: 0,
|
|
1089
|
+
loading: 0,
|
|
1090
|
+
success: 0,
|
|
1091
|
+
error: 0
|
|
1092
|
+
};
|
|
1093
|
+
let withSubscribers = 0;
|
|
1094
|
+
let inflight = 0;
|
|
1095
|
+
let stale = 0;
|
|
1096
|
+
const entries = cache._debugEntries();
|
|
1097
|
+
for (const entry of entries.values()) {
|
|
1098
|
+
byStatus[entry.state.status]++;
|
|
1099
|
+
if (entry.subscribers.size > 0) withSubscribers++;
|
|
1100
|
+
if (entry.inflight) inflight++;
|
|
1101
|
+
if (entry.state.isStale) stale++;
|
|
1102
|
+
}
|
|
1103
|
+
return { total: entries.size, withSubscribers, inflight, stale, byStatus };
|
|
1104
|
+
}
|
|
1105
|
+
var QueryClientContext = createContext(null);
|
|
1106
|
+
function ApiClientProvider({
|
|
1107
|
+
client,
|
|
1108
|
+
children
|
|
1109
|
+
}) {
|
|
1110
|
+
const value = useMemo(() => {
|
|
1111
|
+
const instance = client ?? new QueryClient();
|
|
1112
|
+
setQueryClient(instance);
|
|
1113
|
+
return instance;
|
|
1114
|
+
}, [client]);
|
|
1115
|
+
return createElement(
|
|
1116
|
+
QueryClientContext.Provider,
|
|
1117
|
+
{ value },
|
|
1118
|
+
children
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
function useQueryClient() {
|
|
1122
|
+
const ctx = useContext(QueryClientContext);
|
|
1123
|
+
if (!ctx) {
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
"useQueryClient: \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D ApiClientProvider. \u041E\u0431\u0435\u0440\u043D\u0438 \u043A\u043E\u0440\u0435\u043D\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0432 <ApiClientProvider>."
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
return ctx;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
378
1131
|
// src/index.ts
|
|
379
1132
|
function apiClient(endpoint, fetchConfig = {}) {
|
|
380
1133
|
const fetch = async (params) => {
|
|
@@ -404,6 +1157,6 @@ function apiPaginate(endpoint, fetchConfig = {}, options) {
|
|
|
404
1157
|
}
|
|
405
1158
|
var index_default = apiClient;
|
|
406
1159
|
|
|
407
|
-
export { apiMutation, apiPaginate, configureApiClient, index_default as default, getConfig, isConfigured };
|
|
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 };
|
|
408
1161
|
//# sourceMappingURL=index.mjs.map
|
|
409
1162
|
//# sourceMappingURL=index.mjs.map
|