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