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