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