react-smart-query 1.0.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/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/cache.service-MR6EEYM4.mjs +4 -0
- package/dist/cache.service-MR6EEYM4.mjs.map +1 -0
- package/dist/chunk-KLJQATIV.mjs +170 -0
- package/dist/chunk-KLJQATIV.mjs.map +1 -0
- package/dist/chunk-KSLDOL27.mjs +133 -0
- package/dist/chunk-KSLDOL27.mjs.map +1 -0
- package/dist/chunk-QRCVY7UR.mjs +137 -0
- package/dist/chunk-QRCVY7UR.mjs.map +1 -0
- package/dist/index.d.mts +545 -0
- package/dist/index.d.ts +545 -0
- package/dist/index.js +1533 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1018 -0
- package/dist/index.mjs.map +1 -0
- package/dist/storage.adapter-PJCVI4DE.mjs +3 -0
- package/dist/storage.adapter-PJCVI4DE.mjs.map +1 -0
- package/dist/testing.d.mts +89 -0
- package/dist/testing.d.ts +89 -0
- package/dist/testing.js +272 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +78 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-XXiTKLnh.d.mts +134 -0
- package/dist/types-XXiTKLnh.d.ts +134 -0
- package/dist/utils/debug.d.mts +2 -0
- package/dist/utils/debug.d.ts +2 -0
- package/dist/utils/debug.js +208 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/debug.mjs +40 -0
- package/dist/utils/debug.mjs.map +1 -0
- package/docs/API_REFERENCE.md +149 -0
- package/docs/GUIDELINES.md +23 -0
- package/docs/TESTING.md +61 -0
- package/package.json +136 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1533 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var reactNative = require('react-native');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var reactQuery = require('@tanstack/react-query');
|
|
6
|
+
var equal = require('fast-deep-equal');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var equal__default = /*#__PURE__*/_interopDefault(equal);
|
|
11
|
+
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
14
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
17
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
18
|
+
}) : x)(function(x) {
|
|
19
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
20
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
21
|
+
});
|
|
22
|
+
var __esm = (fn, res) => function __init() {
|
|
23
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
24
|
+
};
|
|
25
|
+
var __export = (target, all) => {
|
|
26
|
+
for (var name in all)
|
|
27
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
28
|
+
};
|
|
29
|
+
var __copyProps = (to, from, except, desc) => {
|
|
30
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
31
|
+
for (let key of __getOwnPropNames(from))
|
|
32
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
33
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
34
|
+
}
|
|
35
|
+
return to;
|
|
36
|
+
};
|
|
37
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
38
|
+
|
|
39
|
+
// src/services/storage.adapter.ts
|
|
40
|
+
var storage_adapter_exports = {};
|
|
41
|
+
__export(storage_adapter_exports, {
|
|
42
|
+
_setStorageOverride: () => _setStorageOverride,
|
|
43
|
+
getStorage: () => getStorage,
|
|
44
|
+
storage: () => storage
|
|
45
|
+
});
|
|
46
|
+
function createMemoryStorage() {
|
|
47
|
+
const store = /* @__PURE__ */ new Map();
|
|
48
|
+
return {
|
|
49
|
+
get: (key) => Promise.resolve(store.get(key)),
|
|
50
|
+
set: (key, value) => {
|
|
51
|
+
store.set(key, value);
|
|
52
|
+
return Promise.resolve();
|
|
53
|
+
},
|
|
54
|
+
delete: (key) => {
|
|
55
|
+
store.delete(key);
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
},
|
|
58
|
+
clearAll: () => {
|
|
59
|
+
store.clear();
|
|
60
|
+
return Promise.resolve();
|
|
61
|
+
},
|
|
62
|
+
keys: () => Promise.resolve(Array.from(store.keys()))
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function createNativeStorage() {
|
|
66
|
+
const MMKV = __require("react-native-mmkv").MMKV;
|
|
67
|
+
const mmkv = new MMKV({ id: "react-smart-query-v2" });
|
|
68
|
+
return {
|
|
69
|
+
get: (key) => Promise.resolve(mmkv.getString(key) ?? void 0),
|
|
70
|
+
set: (key, value) => Promise.resolve(void mmkv.set(key, value)),
|
|
71
|
+
delete: (key) => Promise.resolve(void mmkv.delete(key)),
|
|
72
|
+
clearAll: () => Promise.resolve(void mmkv.clearAll()),
|
|
73
|
+
keys: () => Promise.resolve(mmkv.getAllKeys())
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function isIDBAvailable() {
|
|
77
|
+
return typeof globalThis !== "undefined" && typeof globalThis.indexedDB !== "undefined";
|
|
78
|
+
}
|
|
79
|
+
function openIDB() {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const req = indexedDB.open(IDB_NAME, IDB_VERSION);
|
|
82
|
+
req.onupgradeneeded = (e) => {
|
|
83
|
+
const db = e.target.result;
|
|
84
|
+
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
|
85
|
+
db.createObjectStore(IDB_STORE);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
req.onsuccess = (e) => resolve(e.target.result);
|
|
89
|
+
req.onerror = () => reject(req.error);
|
|
90
|
+
req.onblocked = () => reject(new Error("IDB blocked by another tab"));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function idbWrap(req) {
|
|
94
|
+
return new Promise((res, rej) => {
|
|
95
|
+
req.onsuccess = () => res(req.result);
|
|
96
|
+
req.onerror = () => rej(req.error);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function createWebStorage() {
|
|
100
|
+
if (!isIDBAvailable()) return createMemoryStorage();
|
|
101
|
+
return {
|
|
102
|
+
async get(key) {
|
|
103
|
+
const db = await getIDB();
|
|
104
|
+
return idbWrap(
|
|
105
|
+
db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).get(key)
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
async set(key, value) {
|
|
109
|
+
const db = await getIDB();
|
|
110
|
+
await idbWrap(
|
|
111
|
+
db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).put(value, key)
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
async delete(key) {
|
|
115
|
+
const db = await getIDB();
|
|
116
|
+
await idbWrap(
|
|
117
|
+
db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).delete(key)
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
async clearAll() {
|
|
121
|
+
const db = await getIDB();
|
|
122
|
+
await idbWrap(
|
|
123
|
+
db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).clear()
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
async keys() {
|
|
127
|
+
const db = await getIDB();
|
|
128
|
+
const result = await idbWrap(
|
|
129
|
+
db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).getAllKeys()
|
|
130
|
+
);
|
|
131
|
+
return result.map(String);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function getStorage() {
|
|
136
|
+
return _overrideStorage ?? storage;
|
|
137
|
+
}
|
|
138
|
+
function _setStorageOverride(s) {
|
|
139
|
+
_overrideStorage = s;
|
|
140
|
+
}
|
|
141
|
+
var IDB_NAME, IDB_STORE, IDB_VERSION, _idb, getIDB, storage, _overrideStorage;
|
|
142
|
+
var init_storage_adapter = __esm({
|
|
143
|
+
"src/services/storage.adapter.ts"() {
|
|
144
|
+
IDB_NAME = "SmartQueryV2";
|
|
145
|
+
IDB_STORE = "entries";
|
|
146
|
+
IDB_VERSION = 1;
|
|
147
|
+
_idb = null;
|
|
148
|
+
getIDB = () => {
|
|
149
|
+
_idb ??= openIDB();
|
|
150
|
+
return _idb;
|
|
151
|
+
};
|
|
152
|
+
storage = reactNative.Platform.OS === "web" ? createWebStorage() : createNativeStorage();
|
|
153
|
+
_overrideStorage = null;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// src/services/observer.service.ts
|
|
158
|
+
function addObserver(fn) {
|
|
159
|
+
observers.add(fn);
|
|
160
|
+
return () => observers.delete(fn);
|
|
161
|
+
}
|
|
162
|
+
function removeObserver(fn) {
|
|
163
|
+
observers.delete(fn);
|
|
164
|
+
}
|
|
165
|
+
function clearObservers() {
|
|
166
|
+
observers.clear();
|
|
167
|
+
}
|
|
168
|
+
function emit(event) {
|
|
169
|
+
if (observers.size === 0) return;
|
|
170
|
+
for (const fn of observers) {
|
|
171
|
+
try {
|
|
172
|
+
fn(event);
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
var observers;
|
|
178
|
+
var init_observer_service = __esm({
|
|
179
|
+
"src/services/observer.service.ts"() {
|
|
180
|
+
observers = /* @__PURE__ */ new Set();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// src/services/cache.service.ts
|
|
185
|
+
var cache_service_exports = {};
|
|
186
|
+
__export(cache_service_exports, {
|
|
187
|
+
CURRENT_CACHE_VERSION: () => exports.CURRENT_CACHE_VERSION,
|
|
188
|
+
cacheKeyFor: () => cacheKeyFor,
|
|
189
|
+
deleteCache: () => deleteCache,
|
|
190
|
+
getPartialCache: () => getPartialCache,
|
|
191
|
+
isCacheStale: () => isCacheStale,
|
|
192
|
+
readCache: () => readCache,
|
|
193
|
+
setMaxCacheEntries: () => setMaxCacheEntries,
|
|
194
|
+
writeCache: () => writeCache
|
|
195
|
+
});
|
|
196
|
+
function setMaxCacheEntries(n) {
|
|
197
|
+
_maxEntries = n;
|
|
198
|
+
}
|
|
199
|
+
function cacheKeyFor(queryKey) {
|
|
200
|
+
return `sq2:${JSON.stringify(queryKey)}`;
|
|
201
|
+
}
|
|
202
|
+
async function readCache(key, queryKey) {
|
|
203
|
+
try {
|
|
204
|
+
const storage2 = getStorage();
|
|
205
|
+
const raw = await storage2.get(key);
|
|
206
|
+
if (!raw) {
|
|
207
|
+
if (queryKey) emit({ type: "cache_miss", queryKey });
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const entry = JSON.parse(raw);
|
|
211
|
+
if (entry.version !== exports.CURRENT_CACHE_VERSION) {
|
|
212
|
+
void storage2.delete(key);
|
|
213
|
+
if (queryKey) emit({ type: "cache_miss", queryKey });
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
void storage2.set(
|
|
217
|
+
key,
|
|
218
|
+
JSON.stringify({ ...entry, lastAccessedAt: Date.now() })
|
|
219
|
+
);
|
|
220
|
+
if (queryKey) emit({ type: "cache_hit", queryKey, cachedAt: entry.cachedAt });
|
|
221
|
+
return entry;
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function writeCache(key, data, queryKey) {
|
|
227
|
+
try {
|
|
228
|
+
const storage2 = getStorage();
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const entry = {
|
|
231
|
+
version: exports.CURRENT_CACHE_VERSION,
|
|
232
|
+
data,
|
|
233
|
+
cachedAt: now,
|
|
234
|
+
lastAccessedAt: now
|
|
235
|
+
};
|
|
236
|
+
const serialized = JSON.stringify(entry);
|
|
237
|
+
try {
|
|
238
|
+
await storage2.set(key, serialized);
|
|
239
|
+
if (queryKey) {
|
|
240
|
+
emit({ type: "cache_write", queryKey, dataSize: serialized.length });
|
|
241
|
+
}
|
|
242
|
+
} catch (quotaErr) {
|
|
243
|
+
emit({ type: "storage_quota_exceeded", key });
|
|
244
|
+
await evictLRUEntries();
|
|
245
|
+
await storage2.set(key, serialized);
|
|
246
|
+
}
|
|
247
|
+
void checkAndEvict();
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function deleteCache(key) {
|
|
252
|
+
try {
|
|
253
|
+
await getStorage().delete(key);
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function checkAndEvict() {
|
|
258
|
+
const storage2 = getStorage();
|
|
259
|
+
const allKeys = await storage2.keys();
|
|
260
|
+
const sqKeys = allKeys.filter((k) => k.startsWith("sq2:"));
|
|
261
|
+
if (sqKeys.length <= _maxEntries) return;
|
|
262
|
+
await evictLRUEntries(sqKeys);
|
|
263
|
+
}
|
|
264
|
+
async function evictLRUEntries(sqKeys) {
|
|
265
|
+
const storage2 = getStorage();
|
|
266
|
+
const keys = sqKeys ?? (await storage2.keys()).filter((k) => k.startsWith("sq2:"));
|
|
267
|
+
const metas = [];
|
|
268
|
+
await Promise.all(
|
|
269
|
+
keys.map(async (key) => {
|
|
270
|
+
try {
|
|
271
|
+
const raw = await storage2.get(key);
|
|
272
|
+
if (!raw) return;
|
|
273
|
+
const parsed = JSON.parse(raw);
|
|
274
|
+
metas.push({ key, lastAccessedAt: parsed.lastAccessedAt ?? 0 });
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
metas.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
|
|
280
|
+
const evictCount = Math.max(1, Math.floor(metas.length * 0.2));
|
|
281
|
+
const toEvict = metas.slice(0, evictCount);
|
|
282
|
+
await Promise.all(toEvict.map(({ key }) => storage2.delete(key)));
|
|
283
|
+
}
|
|
284
|
+
async function getPartialCache(key, ids) {
|
|
285
|
+
const entry = await readCache(key);
|
|
286
|
+
if (!entry) return null;
|
|
287
|
+
const { byId } = entry.data;
|
|
288
|
+
const result = [];
|
|
289
|
+
for (const id of ids) {
|
|
290
|
+
if (id in byId) result.push(byId[id]);
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
function isCacheStale(entry, ttlMs) {
|
|
295
|
+
return Date.now() - entry.cachedAt > ttlMs;
|
|
296
|
+
}
|
|
297
|
+
exports.CURRENT_CACHE_VERSION = void 0; var DEFAULT_MAX_ENTRIES, _maxEntries;
|
|
298
|
+
var init_cache_service = __esm({
|
|
299
|
+
"src/services/cache.service.ts"() {
|
|
300
|
+
init_storage_adapter();
|
|
301
|
+
init_observer_service();
|
|
302
|
+
exports.CURRENT_CACHE_VERSION = 2;
|
|
303
|
+
DEFAULT_MAX_ENTRIES = 200;
|
|
304
|
+
_maxEntries = DEFAULT_MAX_ENTRIES;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// src/services/requestLock.service.ts
|
|
309
|
+
var requestLock_service_exports = {};
|
|
310
|
+
__export(requestLock_service_exports, {
|
|
311
|
+
fetchWithLock: () => fetchWithLock,
|
|
312
|
+
inFlightCount: () => exports.inFlightCount,
|
|
313
|
+
inFlightKeys: () => exports.inFlightKeys
|
|
314
|
+
});
|
|
315
|
+
function fetchWithLock(key, fn) {
|
|
316
|
+
const existing = inFlight.get(key);
|
|
317
|
+
if (existing) return existing;
|
|
318
|
+
const promise = fn().finally(() => inFlight.delete(key));
|
|
319
|
+
inFlight.set(key, promise);
|
|
320
|
+
return promise;
|
|
321
|
+
}
|
|
322
|
+
var inFlight; exports.inFlightCount = void 0; exports.inFlightKeys = void 0;
|
|
323
|
+
var init_requestLock_service = __esm({
|
|
324
|
+
"src/services/requestLock.service.ts"() {
|
|
325
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
326
|
+
exports.inFlightCount = () => inFlight.size;
|
|
327
|
+
exports.inFlightKeys = () => Array.from(inFlight.keys());
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// src/hooks/useSmartQuery.ts
|
|
332
|
+
init_cache_service();
|
|
333
|
+
init_requestLock_service();
|
|
334
|
+
init_storage_adapter();
|
|
335
|
+
init_observer_service();
|
|
336
|
+
function smartCompare(oldData, newData, options = {}) {
|
|
337
|
+
const idField = options.idField ?? "id";
|
|
338
|
+
const versionField = options.versionField === void 0 ? "updatedAt" : options.versionField;
|
|
339
|
+
if (oldData === newData) return { isEqual: true, tier: 1 };
|
|
340
|
+
const oldIsArr = Array.isArray(oldData);
|
|
341
|
+
const newIsArr = Array.isArray(newData);
|
|
342
|
+
if (oldIsArr !== newIsArr) return { isEqual: false, tier: 2 };
|
|
343
|
+
if (!oldIsArr) return { isEqual: equal__default.default(oldData, newData), tier: 5 };
|
|
344
|
+
const o = oldData;
|
|
345
|
+
const n = newData;
|
|
346
|
+
if (o.length !== n.length) return { isEqual: false, tier: 2 };
|
|
347
|
+
if (o.length === 0) return { isEqual: true, tier: 2 };
|
|
348
|
+
let oldIds = "";
|
|
349
|
+
let newIds = "";
|
|
350
|
+
for (let i = 0; i < o.length; i++) {
|
|
351
|
+
oldIds += String(o[i][idField] ?? i) + "|";
|
|
352
|
+
newIds += String(n[i][idField] ?? i) + "|";
|
|
353
|
+
}
|
|
354
|
+
if (oldIds !== newIds) return { isEqual: false, tier: 3 };
|
|
355
|
+
if (versionField !== null) {
|
|
356
|
+
let xorOld = 0;
|
|
357
|
+
let xorNew = 0;
|
|
358
|
+
for (let i = 0; i < o.length; i++) {
|
|
359
|
+
xorOld ^= Number(o[i][versionField] ?? 0) ^ i;
|
|
360
|
+
xorNew ^= Number(n[i][versionField] ?? 0) ^ i;
|
|
361
|
+
}
|
|
362
|
+
if (xorOld !== xorNew) return { isEqual: false, tier: 4 };
|
|
363
|
+
}
|
|
364
|
+
return { isEqual: equal__default.default(oldData, newData), tier: 5 };
|
|
365
|
+
}
|
|
366
|
+
function isDataEqual(oldData, newData, options) {
|
|
367
|
+
return smartCompare(oldData, newData, options).isEqual;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/utils/normalize.ts
|
|
371
|
+
function emptyList() {
|
|
372
|
+
return { byId: /* @__PURE__ */ Object.create(null), allIds: [] };
|
|
373
|
+
}
|
|
374
|
+
function fromArray(items, getItemId, comparator) {
|
|
375
|
+
const byId = /* @__PURE__ */ Object.create(null);
|
|
376
|
+
const allIds = new Array(items.length);
|
|
377
|
+
for (let i = 0; i < items.length; i++) {
|
|
378
|
+
const item = items[i];
|
|
379
|
+
const id = getItemId(item);
|
|
380
|
+
byId[id] = item;
|
|
381
|
+
allIds[i] = id;
|
|
382
|
+
}
|
|
383
|
+
if (comparator) allIds.sort((a, b) => comparator(byId[a], byId[b]));
|
|
384
|
+
return { byId, allIds };
|
|
385
|
+
}
|
|
386
|
+
function toArray(list) {
|
|
387
|
+
const out = new Array(list.allIds.length);
|
|
388
|
+
for (let i = 0; i < list.allIds.length; i++) out[i] = list.byId[list.allIds[i]];
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
function binaryIdx(allIds, byId, item, getItemId, cmp) {
|
|
392
|
+
const itemId = getItemId(item);
|
|
393
|
+
let lo = 0, hi = allIds.length;
|
|
394
|
+
while (lo < hi) {
|
|
395
|
+
const mid = lo + hi >>> 1;
|
|
396
|
+
const midItem = byId[allIds[mid]];
|
|
397
|
+
let res = cmp(midItem, item);
|
|
398
|
+
if (res === 0) {
|
|
399
|
+
res = allIds[mid].localeCompare(itemId);
|
|
400
|
+
}
|
|
401
|
+
res <= 0 ? lo = mid + 1 : hi = mid;
|
|
402
|
+
}
|
|
403
|
+
return lo;
|
|
404
|
+
}
|
|
405
|
+
function normalizedAdd(list, item, getItemId, comparator, getItemVersion) {
|
|
406
|
+
const id = getItemId(item);
|
|
407
|
+
const existing = list.byId[id];
|
|
408
|
+
if (existing && getItemVersion) {
|
|
409
|
+
const vExisting = getItemVersion(existing);
|
|
410
|
+
const vNew = getItemVersion(item);
|
|
411
|
+
if (vNew < vExisting) return list;
|
|
412
|
+
}
|
|
413
|
+
const newById = { ...list.byId, [id]: item };
|
|
414
|
+
const existingIdx = list.allIds.indexOf(id);
|
|
415
|
+
const workingIds = list.allIds.slice();
|
|
416
|
+
if (existingIdx !== -1) workingIds.splice(existingIdx, 1);
|
|
417
|
+
const insertIdx = binaryIdx(workingIds, newById, item, getItemId, comparator);
|
|
418
|
+
workingIds.splice(insertIdx, 0, id);
|
|
419
|
+
return { byId: newById, allIds: workingIds };
|
|
420
|
+
}
|
|
421
|
+
function normalizedUpdate(list, item, getItemId, comparator, getItemVersion) {
|
|
422
|
+
const id = getItemId(item);
|
|
423
|
+
const oldItem = list.byId[id];
|
|
424
|
+
if (!oldItem) return list;
|
|
425
|
+
if (getItemVersion) {
|
|
426
|
+
const vExisting = getItemVersion(oldItem);
|
|
427
|
+
const vNew = getItemVersion(item);
|
|
428
|
+
if (vNew < vExisting) return list;
|
|
429
|
+
}
|
|
430
|
+
if (comparator(oldItem, item) === 0) {
|
|
431
|
+
return {
|
|
432
|
+
allIds: list.allIds,
|
|
433
|
+
// Keep same reference if possible, but immutable is safer
|
|
434
|
+
byId: { ...list.byId, [id]: item }
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return normalizedAdd(list, item, getItemId, comparator, getItemVersion);
|
|
438
|
+
}
|
|
439
|
+
function normalizedRemove(list, id) {
|
|
440
|
+
if (!(id in list.byId)) return list;
|
|
441
|
+
const newById = { ...list.byId };
|
|
442
|
+
delete newById[id];
|
|
443
|
+
const idx = list.allIds.indexOf(id);
|
|
444
|
+
const newAllIds = list.allIds.slice();
|
|
445
|
+
if (idx !== -1) newAllIds.splice(idx, 1);
|
|
446
|
+
return { byId: newById, allIds: newAllIds };
|
|
447
|
+
}
|
|
448
|
+
function mergeNormalizedData(existing, incomingIds, incomingById, getItemId, comparator) {
|
|
449
|
+
const newById = { ...existing.byId, ...incomingById };
|
|
450
|
+
const allIds = [...existing.allIds];
|
|
451
|
+
const seen = new Set(existing.allIds);
|
|
452
|
+
for (const id of incomingIds) {
|
|
453
|
+
if (seen.has(id)) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
seen.add(id);
|
|
457
|
+
if (comparator) {
|
|
458
|
+
const item = incomingById[id];
|
|
459
|
+
const insertIdx = binaryIdx(allIds, newById, item, getItemId, comparator);
|
|
460
|
+
allIds.splice(insertIdx, 0, id);
|
|
461
|
+
} else {
|
|
462
|
+
allIds.push(id);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (comparator && incomingIds.some((id) => existing.byId[id])) {
|
|
466
|
+
allIds.sort((a, b) => comparator(newById[a], newById[b]));
|
|
467
|
+
}
|
|
468
|
+
return { byId: newById, allIds };
|
|
469
|
+
}
|
|
470
|
+
function getPage(allIds, byId, pageIndex, pageSize) {
|
|
471
|
+
const start = pageIndex * pageSize;
|
|
472
|
+
const ids = allIds.slice(start, start + pageSize);
|
|
473
|
+
return ids.map((id) => byId[id]);
|
|
474
|
+
}
|
|
475
|
+
function derivePages(allIds, byId, pageSize) {
|
|
476
|
+
const totalItems = allIds.length;
|
|
477
|
+
const totalPages = Math.ceil(totalItems / pageSize);
|
|
478
|
+
const pages = [];
|
|
479
|
+
for (let i = 0; i < totalPages; i++) {
|
|
480
|
+
pages.push(getPage(allIds, byId, i, pageSize));
|
|
481
|
+
}
|
|
482
|
+
return pages;
|
|
483
|
+
}
|
|
484
|
+
function isNormalizedEmpty(list) {
|
|
485
|
+
return list.allIds.length === 0;
|
|
486
|
+
}
|
|
487
|
+
function trimNormalizedList(list, maxItems) {
|
|
488
|
+
if (list.allIds.length <= maxItems) return list;
|
|
489
|
+
const removeCount = Math.max(
|
|
490
|
+
Math.ceil(list.allIds.length * 0.2),
|
|
491
|
+
list.allIds.length - maxItems
|
|
492
|
+
);
|
|
493
|
+
const newSize = list.allIds.length - removeCount;
|
|
494
|
+
const newAllIds = list.allIds.slice(0, newSize);
|
|
495
|
+
const newById = {};
|
|
496
|
+
for (const id of newAllIds) {
|
|
497
|
+
newById[id] = list.byId[id];
|
|
498
|
+
}
|
|
499
|
+
return { byId: newById, allIds: newAllIds };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/registry/smartQueryRegistry.ts
|
|
503
|
+
init_cache_service();
|
|
504
|
+
var liveRegistry = /* @__PURE__ */ new Map();
|
|
505
|
+
var sortConfigRegistry = /* @__PURE__ */ new Map();
|
|
506
|
+
function _registerUpdater(storageKey, updater, config) {
|
|
507
|
+
liveRegistry.set(storageKey, updater);
|
|
508
|
+
if (config) sortConfigRegistry.set(storageKey, config);
|
|
509
|
+
}
|
|
510
|
+
function _unregisterUpdater(storageKey) {
|
|
511
|
+
liveRegistry.delete(storageKey);
|
|
512
|
+
}
|
|
513
|
+
async function mutateStorageOnly(storageKey, queryKey, fn) {
|
|
514
|
+
const config = sortConfigRegistry.get(storageKey);
|
|
515
|
+
if (!config) return;
|
|
516
|
+
const entry = await readCache(storageKey, queryKey);
|
|
517
|
+
if (!entry) return;
|
|
518
|
+
const rawData = entry.data;
|
|
519
|
+
const isUnified = "data" in rawData && "meta" in rawData;
|
|
520
|
+
const currentList = isUnified ? rawData.data : rawData;
|
|
521
|
+
const nextList = fn(currentList, config);
|
|
522
|
+
const nextData = isUnified ? { ...rawData, data: nextList } : nextList;
|
|
523
|
+
await writeCache(storageKey, nextData, queryKey);
|
|
524
|
+
}
|
|
525
|
+
function getSmartQueryActions(queryKey) {
|
|
526
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
527
|
+
return {
|
|
528
|
+
isActive: () => liveRegistry.has(storageKey),
|
|
529
|
+
addItem: async (item) => {
|
|
530
|
+
const live = liveRegistry.get(storageKey);
|
|
531
|
+
if (live) {
|
|
532
|
+
live.add(item);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
await mutateStorageOnly(
|
|
536
|
+
storageKey,
|
|
537
|
+
queryKey,
|
|
538
|
+
(list, { comparator, getItemId }) => normalizedAdd(list, item, getItemId, comparator)
|
|
539
|
+
);
|
|
540
|
+
},
|
|
541
|
+
updateItem: async (item) => {
|
|
542
|
+
const live = liveRegistry.get(storageKey);
|
|
543
|
+
if (live) {
|
|
544
|
+
live.update(item);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
await mutateStorageOnly(
|
|
548
|
+
storageKey,
|
|
549
|
+
queryKey,
|
|
550
|
+
(list, { comparator, getItemId }) => normalizedUpdate(list, item, getItemId, comparator)
|
|
551
|
+
);
|
|
552
|
+
},
|
|
553
|
+
removeItem: async (id) => {
|
|
554
|
+
const live = liveRegistry.get(storageKey);
|
|
555
|
+
if (live) {
|
|
556
|
+
live.remove(id);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
await mutateStorageOnly(
|
|
560
|
+
storageKey,
|
|
561
|
+
queryKey,
|
|
562
|
+
(list) => normalizedRemove(list, id)
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
var smartQueryDebug = {
|
|
568
|
+
/** Get the current normalized state for a query key. */
|
|
569
|
+
getNormalizedState: async (queryKey) => {
|
|
570
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return null;
|
|
571
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
572
|
+
const entry = await readCache(storageKey, queryKey);
|
|
573
|
+
return entry ? entry.data : null;
|
|
574
|
+
},
|
|
575
|
+
/** Log a detailed summary of the cache entry to the console. */
|
|
576
|
+
inspectCache: async (queryKey) => {
|
|
577
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return;
|
|
578
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
579
|
+
const entry = await readCache(storageKey, queryKey);
|
|
580
|
+
if (!entry) {
|
|
581
|
+
console.log(`[SmartQuery Debug] Cache MISS for:`, queryKey);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
console.log(`[SmartQuery Debug] Cache HIT for:`, queryKey, {
|
|
585
|
+
cachedAt: new Date(entry.cachedAt).toISOString(),
|
|
586
|
+
size: JSON.stringify(entry.data).length,
|
|
587
|
+
data: entry.data
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
/** Clear the cache entry for a query key. */
|
|
591
|
+
clearCache: async (queryKey) => {
|
|
592
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return;
|
|
593
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
594
|
+
const { deleteCache: deleteCache2 } = await Promise.resolve().then(() => (init_cache_service(), cache_service_exports));
|
|
595
|
+
await deleteCache2(storageKey);
|
|
596
|
+
},
|
|
597
|
+
/** Get the current state of the offline mutation queue. */
|
|
598
|
+
getQueue: async () => {
|
|
599
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return [];
|
|
600
|
+
try {
|
|
601
|
+
const { getStorage: getStorage2 } = await Promise.resolve().then(() => (init_storage_adapter(), storage_adapter_exports));
|
|
602
|
+
const raw = await getStorage2().get("sq_mutation_queue");
|
|
603
|
+
return raw ? JSON.parse(raw) : [];
|
|
604
|
+
} catch {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
/** Get list of all storage keys currently being fetched. */
|
|
609
|
+
inFlightRequests: () => {
|
|
610
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return [];
|
|
611
|
+
try {
|
|
612
|
+
const { inFlightKeys: inFlightKeys2 } = (init_requestLock_service(), __toCommonJS(requestLock_service_exports));
|
|
613
|
+
return inFlightKeys2();
|
|
614
|
+
} catch {
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
async function batchUpdate(queryKey, fn) {
|
|
620
|
+
const storageKey = cacheKeyFor(queryKey);
|
|
621
|
+
const actions = getSmartQueryActions(queryKey);
|
|
622
|
+
const live = liveRegistry.has(storageKey);
|
|
623
|
+
if (live) {
|
|
624
|
+
await fn(actions);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const config = sortConfigRegistry.get(storageKey);
|
|
628
|
+
if (!config) return;
|
|
629
|
+
const entry = await readCache(storageKey, queryKey);
|
|
630
|
+
if (!entry) return;
|
|
631
|
+
let currentData = entry.data;
|
|
632
|
+
const isUnified = "data" in currentData && "meta" in currentData;
|
|
633
|
+
let currentList = isUnified ? currentData.data : currentData;
|
|
634
|
+
const batchActions = {
|
|
635
|
+
isActive: () => false,
|
|
636
|
+
addItem: async (item) => {
|
|
637
|
+
currentList = normalizedAdd(currentList, item, config.getItemId, config.comparator);
|
|
638
|
+
},
|
|
639
|
+
updateItem: async (item) => {
|
|
640
|
+
currentList = normalizedUpdate(currentList, item, config.getItemId, config.comparator);
|
|
641
|
+
},
|
|
642
|
+
removeItem: async (id) => {
|
|
643
|
+
currentList = normalizedRemove(currentList, id);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
await fn(batchActions);
|
|
647
|
+
const nextData = isUnified ? { ...currentData, data: currentList } : currentList;
|
|
648
|
+
await writeCache(storageKey, nextData, queryKey);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/hooks/useSmartQuery.ts
|
|
652
|
+
function normalizeError(cause) {
|
|
653
|
+
if (cause instanceof Error) {
|
|
654
|
+
const err = cause;
|
|
655
|
+
const status = err.status;
|
|
656
|
+
return {
|
|
657
|
+
cause,
|
|
658
|
+
message: cause.message,
|
|
659
|
+
retryable: !status || status >= 500 || status === 429,
|
|
660
|
+
statusCode: status
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
return { cause, message: String(cause), retryable: true };
|
|
664
|
+
}
|
|
665
|
+
var DEFAULT_TTL = 5 * 6e4;
|
|
666
|
+
function useSmartQuery(options) {
|
|
667
|
+
const {
|
|
668
|
+
queryKey,
|
|
669
|
+
queryFn,
|
|
670
|
+
select,
|
|
671
|
+
getItemId,
|
|
672
|
+
sortComparator,
|
|
673
|
+
cacheTtl = DEFAULT_TTL,
|
|
674
|
+
maxItems = 1e3,
|
|
675
|
+
getItemVersion,
|
|
676
|
+
strictFreshness = false,
|
|
677
|
+
fallbackData,
|
|
678
|
+
compareOptions,
|
|
679
|
+
onSuccess,
|
|
680
|
+
onError,
|
|
681
|
+
queryOptions = {}
|
|
682
|
+
} = options;
|
|
683
|
+
const queryClient = reactQuery.useQueryClient();
|
|
684
|
+
const storageKey = react.useMemo(
|
|
685
|
+
() => cacheKeyFor(queryKey),
|
|
686
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
687
|
+
[JSON.stringify(queryKey)]
|
|
688
|
+
);
|
|
689
|
+
const listMode = typeof sortComparator === "function" && typeof getItemId === "function";
|
|
690
|
+
const [viewData, setViewData] = react.useState(void 0);
|
|
691
|
+
const [isCacheLoading, setIsCacheLoading] = react.useState(true);
|
|
692
|
+
const [isFromCache, setIsFromCache] = react.useState(false);
|
|
693
|
+
const [shouldFetch, setShouldFetch] = react.useState(false);
|
|
694
|
+
const [smartError, setSmartError] = react.useState(null);
|
|
695
|
+
const prevRawRef = react.useRef(void 0);
|
|
696
|
+
const normalizedRef = react.useRef(emptyList());
|
|
697
|
+
react.useEffect(() => {
|
|
698
|
+
let cancelled = false;
|
|
699
|
+
setIsCacheLoading(true);
|
|
700
|
+
readCache(storageKey, queryKey).then((entry) => {
|
|
701
|
+
if (cancelled) return;
|
|
702
|
+
if (entry !== null) {
|
|
703
|
+
const isStale = isCacheStale(entry, cacheTtl);
|
|
704
|
+
const usable = !strictFreshness || !isStale;
|
|
705
|
+
if (usable) {
|
|
706
|
+
if (listMode) {
|
|
707
|
+
let normalized;
|
|
708
|
+
if (Array.isArray(entry.data)) {
|
|
709
|
+
normalized = fromArray(
|
|
710
|
+
entry.data,
|
|
711
|
+
getItemId,
|
|
712
|
+
sortComparator
|
|
713
|
+
);
|
|
714
|
+
} else {
|
|
715
|
+
normalized = entry.data;
|
|
716
|
+
}
|
|
717
|
+
normalizedRef.current = normalized;
|
|
718
|
+
const view = toArray(normalized);
|
|
719
|
+
prevRawRef.current = normalized;
|
|
720
|
+
queryClient.setQueryData(queryKey, view);
|
|
721
|
+
setViewData(view);
|
|
722
|
+
} else {
|
|
723
|
+
prevRawRef.current = entry.data;
|
|
724
|
+
queryClient.setQueryData(queryKey, entry.data);
|
|
725
|
+
setViewData(entry.data);
|
|
726
|
+
}
|
|
727
|
+
setIsFromCache(true);
|
|
728
|
+
setShouldFetch(isStale);
|
|
729
|
+
} else {
|
|
730
|
+
setShouldFetch(true);
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
setShouldFetch(true);
|
|
734
|
+
}
|
|
735
|
+
setIsCacheLoading(false);
|
|
736
|
+
});
|
|
737
|
+
return () => {
|
|
738
|
+
cancelled = true;
|
|
739
|
+
};
|
|
740
|
+
}, [storageKey]);
|
|
741
|
+
const wrappedQueryFn = react.useCallback(
|
|
742
|
+
async (ctx) => {
|
|
743
|
+
emit({ type: "fetch_start", queryKey });
|
|
744
|
+
const start = Date.now();
|
|
745
|
+
try {
|
|
746
|
+
const raw = await fetchWithLock(storageKey, () => queryFn(ctx));
|
|
747
|
+
const transformed = select ? select(raw) : raw;
|
|
748
|
+
emit({
|
|
749
|
+
type: "fetch_success",
|
|
750
|
+
queryKey,
|
|
751
|
+
durationMs: Date.now() - start
|
|
752
|
+
});
|
|
753
|
+
return transformed;
|
|
754
|
+
} catch (err) {
|
|
755
|
+
emit({ type: "fetch_error", queryKey, error: err });
|
|
756
|
+
throw err;
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
760
|
+
[storageKey, queryFn, select]
|
|
761
|
+
);
|
|
762
|
+
const {
|
|
763
|
+
data: freshData,
|
|
764
|
+
isFetching,
|
|
765
|
+
error: tqError,
|
|
766
|
+
refetch: tqRefetch
|
|
767
|
+
} = reactQuery.useQuery({
|
|
768
|
+
queryKey,
|
|
769
|
+
queryFn: wrappedQueryFn,
|
|
770
|
+
enabled: !isCacheLoading && shouldFetch,
|
|
771
|
+
staleTime: cacheTtl,
|
|
772
|
+
gcTime: cacheTtl * 2,
|
|
773
|
+
refetchOnWindowFocus: false,
|
|
774
|
+
refetchOnReconnect: true,
|
|
775
|
+
networkMode: "offlineFirst",
|
|
776
|
+
...queryOptions
|
|
777
|
+
});
|
|
778
|
+
react.useEffect(() => {
|
|
779
|
+
if (!tqError) {
|
|
780
|
+
setSmartError(null);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const structured = normalizeError(tqError);
|
|
784
|
+
const suppressed = onError?.(structured);
|
|
785
|
+
if (!suppressed) setSmartError(structured);
|
|
786
|
+
}, [tqError, onError]);
|
|
787
|
+
react.useEffect(() => {
|
|
788
|
+
if (freshData === void 0 || isFetching) return;
|
|
789
|
+
if (listMode) {
|
|
790
|
+
const freshNormalized = fromArray(
|
|
791
|
+
freshData,
|
|
792
|
+
getItemId,
|
|
793
|
+
sortComparator
|
|
794
|
+
);
|
|
795
|
+
if (!isDataEqual(prevRawRef.current, freshNormalized, compareOptions)) {
|
|
796
|
+
prevRawRef.current = freshNormalized;
|
|
797
|
+
normalizedRef.current = freshNormalized;
|
|
798
|
+
const view = toArray(freshNormalized);
|
|
799
|
+
queryClient.setQueryData(queryKey, view);
|
|
800
|
+
setViewData(() => view);
|
|
801
|
+
setIsFromCache(false);
|
|
802
|
+
const trimmed = trimNormalizedList(freshNormalized, maxItems);
|
|
803
|
+
void writeCache(storageKey, trimmed, queryKey);
|
|
804
|
+
onSuccess?.(view);
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
if (!isDataEqual(prevRawRef.current, freshData, compareOptions)) {
|
|
808
|
+
prevRawRef.current = freshData;
|
|
809
|
+
queryClient.setQueryData(queryKey, freshData);
|
|
810
|
+
setViewData(() => freshData);
|
|
811
|
+
setIsFromCache(false);
|
|
812
|
+
void writeCache(storageKey, freshData, queryKey);
|
|
813
|
+
onSuccess?.(freshData);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}, [freshData, isFetching]);
|
|
817
|
+
const applyMutation = react.useCallback(
|
|
818
|
+
(fn) => {
|
|
819
|
+
const next = trimNormalizedList(fn(normalizedRef.current), maxItems);
|
|
820
|
+
normalizedRef.current = next;
|
|
821
|
+
prevRawRef.current = next;
|
|
822
|
+
const view = toArray(next);
|
|
823
|
+
queryClient.setQueryData(queryKey, view);
|
|
824
|
+
setViewData(view);
|
|
825
|
+
void writeCache(storageKey, next, queryKey);
|
|
826
|
+
},
|
|
827
|
+
[queryClient, queryKey, storageKey, maxItems]
|
|
828
|
+
);
|
|
829
|
+
const addItem = react.useCallback(
|
|
830
|
+
(item) => {
|
|
831
|
+
if (!listMode) return;
|
|
832
|
+
applyMutation(
|
|
833
|
+
(cur) => normalizedAdd(
|
|
834
|
+
cur,
|
|
835
|
+
item,
|
|
836
|
+
getItemId,
|
|
837
|
+
sortComparator,
|
|
838
|
+
getItemVersion
|
|
839
|
+
)
|
|
840
|
+
);
|
|
841
|
+
},
|
|
842
|
+
[listMode, getItemId, sortComparator, getItemVersion, applyMutation]
|
|
843
|
+
);
|
|
844
|
+
const updateItem = react.useCallback(
|
|
845
|
+
(item) => {
|
|
846
|
+
if (!listMode) return;
|
|
847
|
+
applyMutation(
|
|
848
|
+
(cur) => normalizedUpdate(
|
|
849
|
+
cur,
|
|
850
|
+
item,
|
|
851
|
+
getItemId,
|
|
852
|
+
sortComparator,
|
|
853
|
+
getItemVersion
|
|
854
|
+
)
|
|
855
|
+
);
|
|
856
|
+
},
|
|
857
|
+
[listMode, getItemId, sortComparator, getItemVersion, applyMutation]
|
|
858
|
+
);
|
|
859
|
+
const removeItem = react.useCallback(
|
|
860
|
+
(id) => {
|
|
861
|
+
if (!listMode) return;
|
|
862
|
+
applyMutation((cur) => normalizedRemove(cur, id));
|
|
863
|
+
},
|
|
864
|
+
[listMode, applyMutation]
|
|
865
|
+
);
|
|
866
|
+
const addRef = react.useRef(addItem);
|
|
867
|
+
const updateRef = react.useRef(updateItem);
|
|
868
|
+
const removeRef = react.useRef(removeItem);
|
|
869
|
+
react.useEffect(() => {
|
|
870
|
+
addRef.current = addItem;
|
|
871
|
+
}, [addItem]);
|
|
872
|
+
react.useEffect(() => {
|
|
873
|
+
updateRef.current = updateItem;
|
|
874
|
+
}, [updateItem]);
|
|
875
|
+
react.useEffect(() => {
|
|
876
|
+
removeRef.current = removeItem;
|
|
877
|
+
}, [removeItem]);
|
|
878
|
+
react.useEffect(() => {
|
|
879
|
+
_registerUpdater(
|
|
880
|
+
storageKey,
|
|
881
|
+
{
|
|
882
|
+
add: (item) => addRef.current(item),
|
|
883
|
+
update: (item) => updateRef.current(item),
|
|
884
|
+
remove: (id) => removeRef.current(id)
|
|
885
|
+
},
|
|
886
|
+
listMode && !!sortComparator && !!getItemId ? {
|
|
887
|
+
comparator: sortComparator,
|
|
888
|
+
getItemId
|
|
889
|
+
} : null
|
|
890
|
+
);
|
|
891
|
+
return () => _unregisterUpdater(storageKey);
|
|
892
|
+
}, [storageKey]);
|
|
893
|
+
const data = viewData ?? (tqError ? fallbackData : void 0);
|
|
894
|
+
const isLoading = isCacheLoading || data === void 0 && isFetching;
|
|
895
|
+
const refetch = react.useCallback(() => {
|
|
896
|
+
setShouldFetch(true);
|
|
897
|
+
tqRefetch();
|
|
898
|
+
}, [tqRefetch]);
|
|
899
|
+
return {
|
|
900
|
+
data,
|
|
901
|
+
isLoading,
|
|
902
|
+
isFetching,
|
|
903
|
+
isFromCache,
|
|
904
|
+
isCacheLoading,
|
|
905
|
+
error: smartError,
|
|
906
|
+
refetch,
|
|
907
|
+
addItem,
|
|
908
|
+
updateItem,
|
|
909
|
+
removeItem
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
var invalidateSmartCache = (queryKey) => deleteCache(cacheKeyFor(queryKey));
|
|
913
|
+
var clearAllSmartCache = () => storage.clearAll();
|
|
914
|
+
|
|
915
|
+
// src/services/queue.service.ts
|
|
916
|
+
init_storage_adapter();
|
|
917
|
+
init_observer_service();
|
|
918
|
+
var QUEUE_KEY = "sq2:mutation_queue";
|
|
919
|
+
var DEFAULT_MAX_RETRIES = 5;
|
|
920
|
+
var BACKOFF_BASE_MS = 1e3;
|
|
921
|
+
var BACKOFF_MAX_MS = 12e4;
|
|
922
|
+
var executors = /* @__PURE__ */ new Map();
|
|
923
|
+
function registerExecutor(type, fn) {
|
|
924
|
+
executors.set(type, fn);
|
|
925
|
+
}
|
|
926
|
+
function coalesceQueue(queue) {
|
|
927
|
+
const byEntityKey = /* @__PURE__ */ new Map();
|
|
928
|
+
const standalone = [];
|
|
929
|
+
for (const m of queue) {
|
|
930
|
+
if (!m.entityKey) {
|
|
931
|
+
standalone.push(m);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const group = byEntityKey.get(m.entityKey) ?? [];
|
|
935
|
+
group.push(m);
|
|
936
|
+
byEntityKey.set(m.entityKey, group);
|
|
937
|
+
}
|
|
938
|
+
const coalesced = [];
|
|
939
|
+
for (const group of byEntityKey.values()) {
|
|
940
|
+
group.sort((a, b) => a.enqueuedAt - b.enqueuedAt);
|
|
941
|
+
const hasRemove = group.some((m) => m.type === "REMOVE_ITEM");
|
|
942
|
+
if (hasRemove) {
|
|
943
|
+
const removeOp = [...group].reverse().find((m) => m.type === "REMOVE_ITEM");
|
|
944
|
+
coalesced.push(removeOp);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const addOp = group.find((m) => m.type === "ADD_ITEM");
|
|
948
|
+
const updateOps = group.filter((m) => m.type === "UPDATE_ITEM");
|
|
949
|
+
if (addOp && updateOps.length > 0) {
|
|
950
|
+
const latestUpdate = updateOps[updateOps.length - 1];
|
|
951
|
+
coalesced.push({ ...addOp, payload: latestUpdate.payload });
|
|
952
|
+
} else if (addOp) {
|
|
953
|
+
coalesced.push(addOp);
|
|
954
|
+
} else if (updateOps.length > 0) {
|
|
955
|
+
coalesced.push(updateOps[updateOps.length - 1]);
|
|
956
|
+
} else {
|
|
957
|
+
coalesced.push(...group);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return [...standalone, ...coalesced].sort((a, b) => a.enqueuedAt - b.enqueuedAt);
|
|
961
|
+
}
|
|
962
|
+
async function loadQueue() {
|
|
963
|
+
try {
|
|
964
|
+
const raw = await getStorage().get(QUEUE_KEY);
|
|
965
|
+
if (!raw) return [];
|
|
966
|
+
return JSON.parse(raw);
|
|
967
|
+
} catch {
|
|
968
|
+
return [];
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
async function saveQueue(queue) {
|
|
972
|
+
try {
|
|
973
|
+
await getStorage().set(QUEUE_KEY, JSON.stringify(queue));
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function backoffMs(retryCount) {
|
|
978
|
+
const exp = Math.min(BACKOFF_BASE_MS * 2 ** retryCount, BACKOFF_MAX_MS);
|
|
979
|
+
return Math.random() * exp;
|
|
980
|
+
}
|
|
981
|
+
var isProcessing = false;
|
|
982
|
+
async function processQueue() {
|
|
983
|
+
if (isProcessing) return;
|
|
984
|
+
isProcessing = true;
|
|
985
|
+
try {
|
|
986
|
+
const raw = await loadQueue();
|
|
987
|
+
if (raw.length === 0) return;
|
|
988
|
+
const queue = coalesceQueue(raw);
|
|
989
|
+
const now = Date.now();
|
|
990
|
+
const remaining = [];
|
|
991
|
+
for (const mutation of queue) {
|
|
992
|
+
if (mutation.nextRetryAt > now) {
|
|
993
|
+
remaining.push(mutation);
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
const executor = executors.get(mutation.type);
|
|
997
|
+
if (!executor) {
|
|
998
|
+
if (__DEV__) {
|
|
999
|
+
console.warn(`[SmartQuery] No executor for "${mutation.type}"`);
|
|
1000
|
+
}
|
|
1001
|
+
remaining.push(mutation);
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
try {
|
|
1005
|
+
await executor(mutation);
|
|
1006
|
+
emit({ type: "queue_success", mutationId: mutation.id });
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
const nextRetry = mutation.retryCount + 1;
|
|
1009
|
+
emit({ type: "queue_failure", mutationId: mutation.id, retryCount: nextRetry });
|
|
1010
|
+
if (nextRetry >= mutation.maxRetries) {
|
|
1011
|
+
if (__DEV__) {
|
|
1012
|
+
console.error(
|
|
1013
|
+
`[SmartQuery] Mutation ${mutation.id} dropped after ${mutation.maxRetries} retries`,
|
|
1014
|
+
err
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
} else {
|
|
1018
|
+
remaining.push({
|
|
1019
|
+
...mutation,
|
|
1020
|
+
retryCount: nextRetry,
|
|
1021
|
+
nextRetryAt: now + backoffMs(nextRetry)
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
await saveQueue(remaining);
|
|
1027
|
+
if (remaining.length === 0) emit({ type: "queue_drained" });
|
|
1028
|
+
} finally {
|
|
1029
|
+
isProcessing = false;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async function enqueueMutation(options) {
|
|
1033
|
+
const queue = await loadQueue();
|
|
1034
|
+
const mutation = {
|
|
1035
|
+
id: `mut_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1036
|
+
type: options.type,
|
|
1037
|
+
entityKey: options.entityKey,
|
|
1038
|
+
queryKey: options.queryKey,
|
|
1039
|
+
payload: options.payload,
|
|
1040
|
+
enqueuedAt: Date.now(),
|
|
1041
|
+
retryCount: 0,
|
|
1042
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
1043
|
+
nextRetryAt: 0
|
|
1044
|
+
};
|
|
1045
|
+
queue.push(mutation);
|
|
1046
|
+
await saveQueue(queue);
|
|
1047
|
+
emit({ type: "queue_enqueue", mutationId: mutation.id, mutationType: mutation.type });
|
|
1048
|
+
}
|
|
1049
|
+
async function initQueue() {
|
|
1050
|
+
await processQueue();
|
|
1051
|
+
}
|
|
1052
|
+
async function clearQueue() {
|
|
1053
|
+
await saveQueue([]);
|
|
1054
|
+
}
|
|
1055
|
+
var getQueue = loadQueue;
|
|
1056
|
+
var getQueueLength = async () => (await loadQueue()).length;
|
|
1057
|
+
|
|
1058
|
+
// src/hooks/useSmartMutation.ts
|
|
1059
|
+
init_observer_service();
|
|
1060
|
+
function defaultIsOnline() {
|
|
1061
|
+
if (typeof navigator !== "undefined" && "onLine" in navigator) {
|
|
1062
|
+
return navigator.onLine;
|
|
1063
|
+
}
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
function normalizeError2(cause) {
|
|
1067
|
+
if (cause instanceof Error) {
|
|
1068
|
+
const err = cause;
|
|
1069
|
+
const status = err.status;
|
|
1070
|
+
return {
|
|
1071
|
+
cause,
|
|
1072
|
+
message: cause.message,
|
|
1073
|
+
retryable: !status || status >= 500 || status === 429,
|
|
1074
|
+
statusCode: status
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
return { cause, message: String(cause), retryable: true };
|
|
1078
|
+
}
|
|
1079
|
+
function useSmartMutation(options) {
|
|
1080
|
+
const {
|
|
1081
|
+
queryKey,
|
|
1082
|
+
mutationType,
|
|
1083
|
+
mutationFn,
|
|
1084
|
+
getItemId,
|
|
1085
|
+
toItem,
|
|
1086
|
+
enableOfflineQueue = true,
|
|
1087
|
+
getEntityKey,
|
|
1088
|
+
onSuccess,
|
|
1089
|
+
onError,
|
|
1090
|
+
isOnline = defaultIsOnline
|
|
1091
|
+
} = options;
|
|
1092
|
+
const [isPending, setIsPending] = react.useState(false);
|
|
1093
|
+
const [error, setError] = react.useState(null);
|
|
1094
|
+
const isMounted = react.useRef(true);
|
|
1095
|
+
const getActions = react.useCallback(
|
|
1096
|
+
() => getSmartQueryActions(queryKey),
|
|
1097
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1098
|
+
[JSON.stringify(queryKey)]
|
|
1099
|
+
);
|
|
1100
|
+
const mutateAsync = react.useCallback(
|
|
1101
|
+
async (item) => {
|
|
1102
|
+
const actions = getActions();
|
|
1103
|
+
const itemId = getItemId(item);
|
|
1104
|
+
if (isMounted.current) {
|
|
1105
|
+
setIsPending(true);
|
|
1106
|
+
setError(null);
|
|
1107
|
+
}
|
|
1108
|
+
if (mutationType === "ADD_ITEM" || mutationType === "CUSTOM") {
|
|
1109
|
+
await actions.addItem(item);
|
|
1110
|
+
} else if (mutationType === "UPDATE_ITEM") {
|
|
1111
|
+
await actions.updateItem(item);
|
|
1112
|
+
} else if (mutationType === "REMOVE_ITEM") {
|
|
1113
|
+
await actions.removeItem(itemId);
|
|
1114
|
+
}
|
|
1115
|
+
if (!isOnline()) {
|
|
1116
|
+
if (enableOfflineQueue) {
|
|
1117
|
+
await enqueueMutation({
|
|
1118
|
+
type: mutationType,
|
|
1119
|
+
queryKey,
|
|
1120
|
+
payload: item,
|
|
1121
|
+
entityKey: getEntityKey?.(item)
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
if (isMounted.current) setIsPending(false);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
try {
|
|
1128
|
+
const response = await mutationFn(item);
|
|
1129
|
+
const confirmedItem = toItem ? toItem(response) : response;
|
|
1130
|
+
if (mutationType !== "REMOVE_ITEM") {
|
|
1131
|
+
await actions.updateItem(confirmedItem);
|
|
1132
|
+
}
|
|
1133
|
+
emit({
|
|
1134
|
+
type: "queue_success",
|
|
1135
|
+
mutationId: `${mutationType}:${itemId}`
|
|
1136
|
+
});
|
|
1137
|
+
onSuccess?.(response, item);
|
|
1138
|
+
void processQueue();
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
if (mutationType === "ADD_ITEM" || mutationType === "CUSTOM") {
|
|
1141
|
+
await actions.removeItem(itemId);
|
|
1142
|
+
} else if (mutationType === "UPDATE_ITEM") ; else if (mutationType === "REMOVE_ITEM") {
|
|
1143
|
+
await actions.addItem(item);
|
|
1144
|
+
}
|
|
1145
|
+
const structured = normalizeError2(err);
|
|
1146
|
+
if (isMounted.current) setError(structured);
|
|
1147
|
+
onError?.(structured, item);
|
|
1148
|
+
} finally {
|
|
1149
|
+
if (isMounted.current) setIsPending(false);
|
|
1150
|
+
}
|
|
1151
|
+
},
|
|
1152
|
+
[
|
|
1153
|
+
getActions,
|
|
1154
|
+
getItemId,
|
|
1155
|
+
mutationType,
|
|
1156
|
+
mutationFn,
|
|
1157
|
+
toItem,
|
|
1158
|
+
enableOfflineQueue,
|
|
1159
|
+
getEntityKey,
|
|
1160
|
+
isOnline,
|
|
1161
|
+
onSuccess,
|
|
1162
|
+
onError,
|
|
1163
|
+
queryKey
|
|
1164
|
+
]
|
|
1165
|
+
);
|
|
1166
|
+
const mutate = react.useCallback(
|
|
1167
|
+
(item) => {
|
|
1168
|
+
void mutateAsync(item);
|
|
1169
|
+
},
|
|
1170
|
+
[mutateAsync]
|
|
1171
|
+
);
|
|
1172
|
+
const reset = react.useCallback(() => {
|
|
1173
|
+
setError(null);
|
|
1174
|
+
setIsPending(false);
|
|
1175
|
+
}, []);
|
|
1176
|
+
return { mutate, mutateAsync, isPending, error, reset };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/hooks/useInfiniteSmartQuery.ts
|
|
1180
|
+
init_cache_service();
|
|
1181
|
+
init_requestLock_service();
|
|
1182
|
+
init_observer_service();
|
|
1183
|
+
function normalizeError3(cause) {
|
|
1184
|
+
if (cause instanceof Error) {
|
|
1185
|
+
const err = cause;
|
|
1186
|
+
const status = err.status;
|
|
1187
|
+
return {
|
|
1188
|
+
cause,
|
|
1189
|
+
message: cause.message,
|
|
1190
|
+
retryable: !status || status >= 500 || status === 429,
|
|
1191
|
+
statusCode: status
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
return { cause, message: String(cause), retryable: true };
|
|
1195
|
+
}
|
|
1196
|
+
var DEFAULT_TTL2 = 5 * 6e4;
|
|
1197
|
+
function useInfiniteSmartQuery(options) {
|
|
1198
|
+
const {
|
|
1199
|
+
queryKey,
|
|
1200
|
+
queryFn,
|
|
1201
|
+
getNextCursor,
|
|
1202
|
+
select,
|
|
1203
|
+
getItemId,
|
|
1204
|
+
getItemVersion,
|
|
1205
|
+
sortComparator,
|
|
1206
|
+
initialPageParam = void 0,
|
|
1207
|
+
paginationMode = "normalized",
|
|
1208
|
+
pageSize: pageSizeProp,
|
|
1209
|
+
maxItems = 1e3,
|
|
1210
|
+
cacheTtl = DEFAULT_TTL2,
|
|
1211
|
+
strictFreshness = false,
|
|
1212
|
+
onError
|
|
1213
|
+
} = options;
|
|
1214
|
+
const storageKey = react.useMemo(
|
|
1215
|
+
() => cacheKeyFor(queryKey),
|
|
1216
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1217
|
+
[JSON.stringify(queryKey)]
|
|
1218
|
+
);
|
|
1219
|
+
const [infiniteData, setInfiniteData] = react.useState({
|
|
1220
|
+
data: emptyList(),
|
|
1221
|
+
meta: {
|
|
1222
|
+
nextCursor: initialPageParam ?? null,
|
|
1223
|
+
pageParams: []
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
const [pageSize, setPageSize] = react.useState(pageSizeProp);
|
|
1227
|
+
const [isCacheLoading, setIsCacheLoading] = react.useState(true);
|
|
1228
|
+
const [isFetchingNextPage, setIsFetchingNextPage] = react.useState(false);
|
|
1229
|
+
const [isFetching, setIsFetching] = react.useState(false);
|
|
1230
|
+
const [error, setError] = react.useState(null);
|
|
1231
|
+
const infiniteRef = react.useRef(infiniteData);
|
|
1232
|
+
function setAndNotify(next) {
|
|
1233
|
+
infiniteRef.current = next;
|
|
1234
|
+
setInfiniteData(next);
|
|
1235
|
+
}
|
|
1236
|
+
react.useEffect(() => {
|
|
1237
|
+
let cancelled = false;
|
|
1238
|
+
setIsCacheLoading(true);
|
|
1239
|
+
readCache(storageKey, queryKey).then((entry) => {
|
|
1240
|
+
if (cancelled) return;
|
|
1241
|
+
if (entry !== null) {
|
|
1242
|
+
const isStale = isCacheStale(entry, cacheTtl);
|
|
1243
|
+
if (!strictFreshness || !isStale) {
|
|
1244
|
+
setAndNotify(entry.data);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
setIsCacheLoading(false);
|
|
1248
|
+
});
|
|
1249
|
+
return () => {
|
|
1250
|
+
cancelled = true;
|
|
1251
|
+
};
|
|
1252
|
+
}, [storageKey]);
|
|
1253
|
+
const fetchPage = react.useCallback(
|
|
1254
|
+
async (cursor) => {
|
|
1255
|
+
const lockKey = `${storageKey}:${JSON.stringify(cursor)}`;
|
|
1256
|
+
emit({ type: "fetch_start", queryKey });
|
|
1257
|
+
try {
|
|
1258
|
+
const raw = await fetchWithLock(lockKey, () => queryFn({ pageParam: cursor }));
|
|
1259
|
+
const items = select(raw);
|
|
1260
|
+
const nextCursor = getNextCursor(raw);
|
|
1261
|
+
const pageIds = items.map(getItemId);
|
|
1262
|
+
const pageById = fromArray(items, getItemId).byId;
|
|
1263
|
+
const now = Date.now();
|
|
1264
|
+
emit({ type: "fetch_success", queryKey, durationMs: 0 });
|
|
1265
|
+
const current = infiniteRef.current;
|
|
1266
|
+
const alreadyFetched = current.meta.pageParams.includes(cursor);
|
|
1267
|
+
if (!pageSize && items.length > 0) {
|
|
1268
|
+
setPageSize(items.length);
|
|
1269
|
+
}
|
|
1270
|
+
let mergedList = mergeNormalizedData(
|
|
1271
|
+
current.data,
|
|
1272
|
+
pageIds,
|
|
1273
|
+
pageById,
|
|
1274
|
+
getItemId,
|
|
1275
|
+
sortComparator
|
|
1276
|
+
);
|
|
1277
|
+
mergedList = trimNormalizedList(mergedList, maxItems);
|
|
1278
|
+
const next = {
|
|
1279
|
+
data: mergedList,
|
|
1280
|
+
meta: {
|
|
1281
|
+
pageParams: alreadyFetched ? current.meta.pageParams : [...current.meta.pageParams, cursor],
|
|
1282
|
+
nextCursor,
|
|
1283
|
+
lastFetchedAt: now
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
setAndNotify(next);
|
|
1287
|
+
void writeCache(storageKey, next, queryKey);
|
|
1288
|
+
setError(null);
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
emit({ type: "fetch_error", queryKey, error: err });
|
|
1291
|
+
const structured = normalizeError3(err);
|
|
1292
|
+
setError(structured);
|
|
1293
|
+
onError?.(structured);
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1297
|
+
[storageKey, queryFn, select, getNextCursor, getItemId, sortComparator, maxItems]
|
|
1298
|
+
);
|
|
1299
|
+
react.useEffect(() => {
|
|
1300
|
+
if (isCacheLoading) return;
|
|
1301
|
+
void (async () => {
|
|
1302
|
+
setIsFetching(true);
|
|
1303
|
+
await fetchPage(initialPageParam);
|
|
1304
|
+
setIsFetching(false);
|
|
1305
|
+
})();
|
|
1306
|
+
}, [isCacheLoading]);
|
|
1307
|
+
const inFlightRef = react.useRef(/* @__PURE__ */ new Set());
|
|
1308
|
+
const fetchNextPage = react.useCallback(() => {
|
|
1309
|
+
const { nextCursor, lastFetchedAt } = infiniteRef.current.meta;
|
|
1310
|
+
if (nextCursor === null || isFetchingNextPage) return;
|
|
1311
|
+
const cursorKey = JSON.stringify(nextCursor);
|
|
1312
|
+
if (inFlightRef.current.has(cursorKey)) return;
|
|
1313
|
+
if (lastFetchedAt && Date.now() - lastFetchedAt < 500) return;
|
|
1314
|
+
void (async () => {
|
|
1315
|
+
inFlightRef.current.add(cursorKey);
|
|
1316
|
+
setIsFetchingNextPage(true);
|
|
1317
|
+
await fetchPage(nextCursor);
|
|
1318
|
+
setIsFetchingNextPage(false);
|
|
1319
|
+
inFlightRef.current.delete(cursorKey);
|
|
1320
|
+
})();
|
|
1321
|
+
}, [fetchPage, isFetchingNextPage]);
|
|
1322
|
+
const refetch = react.useCallback(() => {
|
|
1323
|
+
void (async () => {
|
|
1324
|
+
setIsFetching(true);
|
|
1325
|
+
await fetchPage(initialPageParam);
|
|
1326
|
+
setIsFetching(false);
|
|
1327
|
+
})();
|
|
1328
|
+
}, [fetchPage, initialPageParam]);
|
|
1329
|
+
const applyItemMutation = react.useCallback(
|
|
1330
|
+
(fn) => {
|
|
1331
|
+
const current = infiniteRef.current;
|
|
1332
|
+
const next = {
|
|
1333
|
+
...current,
|
|
1334
|
+
data: fn(current.data)
|
|
1335
|
+
};
|
|
1336
|
+
setAndNotify(next);
|
|
1337
|
+
void writeCache(storageKey, next, queryKey);
|
|
1338
|
+
},
|
|
1339
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1340
|
+
[storageKey]
|
|
1341
|
+
);
|
|
1342
|
+
const updateItem = react.useCallback(
|
|
1343
|
+
(item) => {
|
|
1344
|
+
applyItemMutation(
|
|
1345
|
+
(data) => normalizedUpdate(
|
|
1346
|
+
data,
|
|
1347
|
+
item,
|
|
1348
|
+
getItemId,
|
|
1349
|
+
sortComparator ?? ((a, b) => 0),
|
|
1350
|
+
getItemVersion
|
|
1351
|
+
)
|
|
1352
|
+
);
|
|
1353
|
+
},
|
|
1354
|
+
[getItemId, sortComparator, getItemVersion, applyItemMutation]
|
|
1355
|
+
);
|
|
1356
|
+
const addItem = react.useCallback(
|
|
1357
|
+
(item) => {
|
|
1358
|
+
const id = getItemId(item);
|
|
1359
|
+
const current = infiniteRef.current;
|
|
1360
|
+
if (id in current.data.byId) {
|
|
1361
|
+
updateItem(item);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
applyItemMutation((data) => {
|
|
1365
|
+
const next = normalizedAdd(
|
|
1366
|
+
data,
|
|
1367
|
+
item,
|
|
1368
|
+
getItemId,
|
|
1369
|
+
sortComparator ?? ((a, b) => 0),
|
|
1370
|
+
getItemVersion
|
|
1371
|
+
);
|
|
1372
|
+
return trimNormalizedList(next, maxItems);
|
|
1373
|
+
});
|
|
1374
|
+
},
|
|
1375
|
+
[getItemId, sortComparator, getItemVersion, applyItemMutation, updateItem, maxItems]
|
|
1376
|
+
);
|
|
1377
|
+
const removeItem = react.useCallback(
|
|
1378
|
+
(id) => {
|
|
1379
|
+
applyItemMutation((data) => normalizedRemove(data, id));
|
|
1380
|
+
},
|
|
1381
|
+
[applyItemMutation]
|
|
1382
|
+
);
|
|
1383
|
+
const flatData = react.useMemo(() => toArray(infiniteData.data), [infiniteData.data]);
|
|
1384
|
+
const isLoading = isCacheLoading || flatData.length === 0 && isFetching;
|
|
1385
|
+
const isRefreshing = !!(flatData.length > 0 && isFetching && !isFetchingNextPage);
|
|
1386
|
+
const hasNextPage = infiniteRef.current.meta.nextCursor !== null;
|
|
1387
|
+
const totalCount = infiniteData.data.allIds.length;
|
|
1388
|
+
const derivedData = react.useMemo(() => {
|
|
1389
|
+
if (paginationMode === "pages") {
|
|
1390
|
+
return {
|
|
1391
|
+
pages: derivePages(
|
|
1392
|
+
infiniteData.data.allIds,
|
|
1393
|
+
infiniteData.data.byId,
|
|
1394
|
+
pageSize ?? flatData.length
|
|
1395
|
+
)
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
return flatData;
|
|
1399
|
+
}, [paginationMode, infiniteData.data.allIds, infiniteData.data.byId, pageSize, flatData]);
|
|
1400
|
+
return {
|
|
1401
|
+
data: derivedData,
|
|
1402
|
+
isLoading,
|
|
1403
|
+
isFetchingNextPage,
|
|
1404
|
+
isFetching,
|
|
1405
|
+
isRefreshing,
|
|
1406
|
+
hasNextPage,
|
|
1407
|
+
error,
|
|
1408
|
+
totalCount,
|
|
1409
|
+
fetchNextPage,
|
|
1410
|
+
refetch,
|
|
1411
|
+
addItem,
|
|
1412
|
+
updateItem,
|
|
1413
|
+
removeItem
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function useSmartQuerySelector(queryKey, selector, equalityFn = equal__default.default) {
|
|
1417
|
+
const queryClient = reactQuery.useQueryClient();
|
|
1418
|
+
const selectedRef = react.useRef(selector(queryClient.getQueryData(queryKey)));
|
|
1419
|
+
const subscribe = react.useCallback(
|
|
1420
|
+
(onStoreChange) => {
|
|
1421
|
+
return queryClient.getQueryCache().subscribe((event) => {
|
|
1422
|
+
if (event.type !== "updated" && event.type !== "added" && event.type !== "removed") return;
|
|
1423
|
+
const cacheKey = JSON.stringify(queryKey);
|
|
1424
|
+
const eventKey = JSON.stringify(event.query.queryKey);
|
|
1425
|
+
if (cacheKey !== eventKey) return;
|
|
1426
|
+
const newSelected = selector(queryClient.getQueryData(queryKey));
|
|
1427
|
+
if (!equalityFn(selectedRef.current, newSelected)) {
|
|
1428
|
+
selectedRef.current = newSelected;
|
|
1429
|
+
onStoreChange();
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
},
|
|
1433
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1434
|
+
[queryClient, JSON.stringify(queryKey), selector, equalityFn]
|
|
1435
|
+
);
|
|
1436
|
+
const getSnapshot = react.useCallback(
|
|
1437
|
+
() => selectedRef.current,
|
|
1438
|
+
[]
|
|
1439
|
+
);
|
|
1440
|
+
const getServerSnapshot = react.useCallback(
|
|
1441
|
+
() => selector(void 0),
|
|
1442
|
+
[selector]
|
|
1443
|
+
);
|
|
1444
|
+
return react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/factory/createTypedQuery.ts
|
|
1448
|
+
function createTypedQuery(config) {
|
|
1449
|
+
return {
|
|
1450
|
+
useQuery(...args) {
|
|
1451
|
+
const qk = config.queryKey(...args);
|
|
1452
|
+
return useSmartQuery({
|
|
1453
|
+
queryKey: qk,
|
|
1454
|
+
queryFn: () => config.queryFn(...args),
|
|
1455
|
+
select: config.select,
|
|
1456
|
+
getItemId: config.getItemId,
|
|
1457
|
+
sortComparator: config.sortComparator,
|
|
1458
|
+
cacheTtl: config.cacheTtl,
|
|
1459
|
+
strictFreshness: config.strictFreshness,
|
|
1460
|
+
...config.defaultOptions
|
|
1461
|
+
});
|
|
1462
|
+
},
|
|
1463
|
+
useMutation(...argsAndOptions) {
|
|
1464
|
+
const mutationOptions = argsAndOptions[argsAndOptions.length - 1];
|
|
1465
|
+
const args = argsAndOptions.slice(0, -1);
|
|
1466
|
+
const qk = config.queryKey(...args);
|
|
1467
|
+
if (!config.getItemId) {
|
|
1468
|
+
throw new Error(
|
|
1469
|
+
"[SmartQuery] useMutation requires getItemId to be defined in createTypedQuery"
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
return useSmartMutation({
|
|
1473
|
+
queryKey: qk,
|
|
1474
|
+
getItemId: config.getItemId,
|
|
1475
|
+
...mutationOptions
|
|
1476
|
+
});
|
|
1477
|
+
},
|
|
1478
|
+
getActions(...args) {
|
|
1479
|
+
const qk = config.queryKey(...args);
|
|
1480
|
+
return getSmartQueryActions(qk);
|
|
1481
|
+
},
|
|
1482
|
+
invalidate(...args) {
|
|
1483
|
+
const qk = config.queryKey(...args);
|
|
1484
|
+
return invalidateSmartCache(qk);
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/index.ts
|
|
1490
|
+
init_observer_service();
|
|
1491
|
+
init_cache_service();
|
|
1492
|
+
init_requestLock_service();
|
|
1493
|
+
|
|
1494
|
+
exports.addObserver = addObserver;
|
|
1495
|
+
exports.batchUpdate = batchUpdate;
|
|
1496
|
+
exports.cacheKeyFor = cacheKeyFor;
|
|
1497
|
+
exports.clearAllSmartCache = clearAllSmartCache;
|
|
1498
|
+
exports.clearObservers = clearObservers;
|
|
1499
|
+
exports.clearQueue = clearQueue;
|
|
1500
|
+
exports.createTypedQuery = createTypedQuery;
|
|
1501
|
+
exports.deleteCache = deleteCache;
|
|
1502
|
+
exports.emptyList = emptyList;
|
|
1503
|
+
exports.enqueueMutation = enqueueMutation;
|
|
1504
|
+
exports.fetchWithLock = fetchWithLock;
|
|
1505
|
+
exports.fromArray = fromArray;
|
|
1506
|
+
exports.getPartialCache = getPartialCache;
|
|
1507
|
+
exports.getQueue = getQueue;
|
|
1508
|
+
exports.getQueueLength = getQueueLength;
|
|
1509
|
+
exports.getSmartQueryActions = getSmartQueryActions;
|
|
1510
|
+
exports.initQueue = initQueue;
|
|
1511
|
+
exports.invalidateSmartCache = invalidateSmartCache;
|
|
1512
|
+
exports.isCacheStale = isCacheStale;
|
|
1513
|
+
exports.isDataEqual = isDataEqual;
|
|
1514
|
+
exports.isNormalizedEmpty = isNormalizedEmpty;
|
|
1515
|
+
exports.normalizedAdd = normalizedAdd;
|
|
1516
|
+
exports.normalizedRemove = normalizedRemove;
|
|
1517
|
+
exports.normalizedUpdate = normalizedUpdate;
|
|
1518
|
+
exports.processQueue = processQueue;
|
|
1519
|
+
exports.readCache = readCache;
|
|
1520
|
+
exports.registerExecutor = registerExecutor;
|
|
1521
|
+
exports.removeObserver = removeObserver;
|
|
1522
|
+
exports.setMaxCacheEntries = setMaxCacheEntries;
|
|
1523
|
+
exports.smartCompare = smartCompare;
|
|
1524
|
+
exports.smartQueryDebug = smartQueryDebug;
|
|
1525
|
+
exports.toArray = toArray;
|
|
1526
|
+
exports.trimNormalizedList = trimNormalizedList;
|
|
1527
|
+
exports.useInfiniteSmartQuery = useInfiniteSmartQuery;
|
|
1528
|
+
exports.useSmartMutation = useSmartMutation;
|
|
1529
|
+
exports.useSmartQuery = useSmartQuery;
|
|
1530
|
+
exports.useSmartQuerySelector = useSmartQuerySelector;
|
|
1531
|
+
exports.writeCache = writeCache;
|
|
1532
|
+
//# sourceMappingURL=index.js.map
|
|
1533
|
+
//# sourceMappingURL=index.js.map
|