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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# 🧠 React Smart Query
|
|
2
|
+
|
|
3
|
+
**Production-grade, offline-first, normalized data orchestration for React Native and Web.**
|
|
4
|
+
|
|
5
|
+
React Smart Query is more than just a cache; it's a high-performance normalization engine designed to handle complex, paginated lists with predictable, O(log n) mutations — even when offline.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/react-smart-query)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 🚀 Why React Smart Query?
|
|
13
|
+
|
|
14
|
+
Handling mutations in paginated lists (Infinite Scroll) is notoriously difficult in modern UI frameworks. Standard tooling often results in:
|
|
15
|
+
1. **Data Duplication:** The same resource existing in multiple pages of a query response cache, leading to out-of-sync UI components.
|
|
16
|
+
2. **Inconsistent Ordering:** Newly added optimistic items appearing at the top/bottom rather than their correct sorted semantic position, causing visual "jumps" on next load.
|
|
17
|
+
3. **Complex Manual Cache Logic:** Forcing developers to write intricate, error-prone traversal logic just to update a single nested item deep inside a page array.
|
|
18
|
+
|
|
19
|
+
**React Smart Query solves this by adopting a Unified Normalized Storage model.**
|
|
20
|
+
|
|
21
|
+
Instead of storing data exactly as the API returns it (as a paginated chunk of arrays), React Smart Query intercepts the data, extracts individual entities (using your provided `getItemId`), and stores them in a single, perfectly sorted, globally accessible dictionary (`byId`) and array (`allIds`).
|
|
22
|
+
|
|
23
|
+
- **O(log n) Sorted Mutations**: Items are inserted into the single, global sorted list using binary search. Adding a new item guarantees it instantly lands in the mathematically correct position according to your `sortComparator`.
|
|
24
|
+
- **Automatic Deduplication**: Because items are normalized by ID, an item fetched in Page 1 and (erroneously) returned again in Page 3 only ever exists once in the engine.
|
|
25
|
+
- **Offline-First & Auto-Sync**: A built-in mutation queue automatically captures changes made while offline, persists them to local storage (MMKV/IndexedDB), and plays them back to your server seamlessly when the connection returns.
|
|
26
|
+
- **Derived Pagination**: Pagination is treated strictly as a "slice" view over your normalized data, not how the data is stored. Your UI always receives perfectly contiguous, sorted data.
|
|
27
|
+
- **Memory Protected**: Automatic "soft trimming" algorithm monitors list sizes and safely drops least-recently-seen items to prevent infinite scroll memory bloat on low-end devices.
|
|
28
|
+
|
|
29
|
+
### How it compares to TanStack Query
|
|
30
|
+
|
|
31
|
+
React Smart Query is **built on top of** TanStack Query. It uses TanStack Query for the networking, background refetching, and general request lifecycle, but completely replaces how the returned data is cached and mutated.
|
|
32
|
+
|
|
33
|
+
| Feature | TanStack Query | React Smart Query |
|
|
34
|
+
| :--- | :--- | :--- |
|
|
35
|
+
| **Primary Focus** | Server State Synchronization | Normalized Data Orchestration & Mutability |
|
|
36
|
+
| **Data Storage** | Raw API Responses (Arrays/Objects) | Unified Normalized Dictionary (`byId` & `allIds`) |
|
|
37
|
+
| **Infinite List Mutations** | Manual Cache Traversal (`setQueryData`) | `O(log n)` Binary Search Auto-Insertions |
|
|
38
|
+
| **Deduplication** | Across identical URL endpoints | Across entire application by Item ID |
|
|
39
|
+
| **Offline Mutations** | Difficult (Requires manual hydration/plugins) | Built-in Persistent Queue & Auto-Sync |
|
|
40
|
+
| **Memory Management** | Time-based garbage collection | Time-based + Soft trimming of large lists |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 📦 Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install react-smart-query @tanstack/react-query react-native-mmkv
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🛠️ Quick Start
|
|
53
|
+
|
|
54
|
+
### 1. Basic Query
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { useSmartQuery } from 'react-smart-query';
|
|
58
|
+
|
|
59
|
+
const { data, isLoading } = useSmartQuery({
|
|
60
|
+
queryKey: ['profile'],
|
|
61
|
+
queryFn: () => api.get('/me'),
|
|
62
|
+
select: (res) => res.user,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Infinite Scroll (The Real Magic)
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
const { data, addItem, fetchNextPage } = useInfiniteSmartQuery({
|
|
70
|
+
queryKey: ['expenses'],
|
|
71
|
+
queryFn: ({ pageParam }) => api.get('/expenses', { cursor: pageParam }),
|
|
72
|
+
getNextCursor: (res) => res.nextCursor,
|
|
73
|
+
select: (res) => res.items,
|
|
74
|
+
getItemId: (item) => item.id,
|
|
75
|
+
sortComparator: (a, b) => b.createdAt - a.createdAt, // Perfect sort across all pages
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Adding an item "just works" and inserts into the correct sorted position
|
|
79
|
+
const onAdd = () => addItem({ id: '123', description: 'Coffee', createdAt: Date.now() });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🏗️ Architecture
|
|
85
|
+
|
|
86
|
+
```mermaid
|
|
87
|
+
graph TD
|
|
88
|
+
A[Component] -->|useInfiniteSmartQuery| B(Normalization Engine)
|
|
89
|
+
B -->|Sorted Binary Insert| C[Unified Normalized List]
|
|
90
|
+
C -->|O1 Lookup| D[(byId Map)]
|
|
91
|
+
C -->|Sorted Order| E[(allIds Array)]
|
|
92
|
+
B -->|Persist| F[Storage Adapter]
|
|
93
|
+
F -->|Mobile| G[(MMKV)]
|
|
94
|
+
F -->|Web| H[(IndexedDB)]
|
|
95
|
+
I[Global Registry] -->|Mutate from Background| B
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🔥 Professional Features
|
|
101
|
+
|
|
102
|
+
- **Mutation Conflict Guards**: Prevents stale background updates from overwriting fresher local data.
|
|
103
|
+
- **Batch Updates**: Group multiple mutations into a single storage write and render.
|
|
104
|
+
- **Smart Diffing**: 5-tier hybrid comparison ensures components only re-render when data actually changes.
|
|
105
|
+
- **DevTools**: Inspect internal normalized state, cache hits/misses, and in-flight requests in development.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 💡 When to use?
|
|
110
|
+
|
|
111
|
+
✅ **Use if**:
|
|
112
|
+
- You have high-frequency updates in paginated lists (e.g., Chat, Feeds, Transactions).
|
|
113
|
+
- You need robust offline support with optimistic UI.
|
|
114
|
+
- You want to eliminate "flicker" when items change positions.
|
|
115
|
+
|
|
116
|
+
❌ **Avoid if**:
|
|
117
|
+
- Your data is small and non-relational.
|
|
118
|
+
- You don't need offline persistence or sorted lists.
|
|
119
|
+
|
|
120
|
+
## 📚 Full Documentation
|
|
121
|
+
|
|
122
|
+
For advanced usages, hooks APIs, and architectural guidelines, please see the full documentation:
|
|
123
|
+
- [API Reference](./docs/API_REFERENCE.md)
|
|
124
|
+
- [Guidelines & Best Practices](./docs/GUIDELINES.md)
|
|
125
|
+
- [Testing & Debugging](./docs/TESTING.md)
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 📄 License
|
|
130
|
+
MIT © 2024 React Smart Query Team
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { CURRENT_CACHE_VERSION, cacheKeyFor, deleteCache, getPartialCache, isCacheStale, readCache, setMaxCacheEntries, writeCache } from './chunk-KSLDOL27.mjs';
|
|
2
|
+
import './chunk-QRCVY7UR.mjs';
|
|
3
|
+
//# sourceMappingURL=cache.service-MR6EEYM4.mjs.map
|
|
4
|
+
//# sourceMappingURL=cache.service-MR6EEYM4.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"cache.service-MR6EEYM4.mjs"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { emit } from './chunk-KSLDOL27.mjs';
|
|
2
|
+
import { __export, __esm, getStorage } from './chunk-QRCVY7UR.mjs';
|
|
3
|
+
|
|
4
|
+
// src/services/requestLock.service.ts
|
|
5
|
+
var requestLock_service_exports = {};
|
|
6
|
+
__export(requestLock_service_exports, {
|
|
7
|
+
fetchWithLock: () => fetchWithLock,
|
|
8
|
+
inFlightCount: () => inFlightCount,
|
|
9
|
+
inFlightKeys: () => inFlightKeys
|
|
10
|
+
});
|
|
11
|
+
function fetchWithLock(key, fn) {
|
|
12
|
+
const existing = inFlight.get(key);
|
|
13
|
+
if (existing) return existing;
|
|
14
|
+
const promise = fn().finally(() => inFlight.delete(key));
|
|
15
|
+
inFlight.set(key, promise);
|
|
16
|
+
return promise;
|
|
17
|
+
}
|
|
18
|
+
var inFlight, inFlightCount, inFlightKeys;
|
|
19
|
+
var init_requestLock_service = __esm({
|
|
20
|
+
"src/services/requestLock.service.ts"() {
|
|
21
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
22
|
+
inFlightCount = () => inFlight.size;
|
|
23
|
+
inFlightKeys = () => Array.from(inFlight.keys());
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// src/services/queue.service.ts
|
|
28
|
+
var QUEUE_KEY = "sq2:mutation_queue";
|
|
29
|
+
var DEFAULT_MAX_RETRIES = 5;
|
|
30
|
+
var BACKOFF_BASE_MS = 1e3;
|
|
31
|
+
var BACKOFF_MAX_MS = 12e4;
|
|
32
|
+
var executors = /* @__PURE__ */ new Map();
|
|
33
|
+
function registerExecutor(type, fn) {
|
|
34
|
+
executors.set(type, fn);
|
|
35
|
+
}
|
|
36
|
+
function coalesceQueue(queue) {
|
|
37
|
+
const byEntityKey = /* @__PURE__ */ new Map();
|
|
38
|
+
const standalone = [];
|
|
39
|
+
for (const m of queue) {
|
|
40
|
+
if (!m.entityKey) {
|
|
41
|
+
standalone.push(m);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const group = byEntityKey.get(m.entityKey) ?? [];
|
|
45
|
+
group.push(m);
|
|
46
|
+
byEntityKey.set(m.entityKey, group);
|
|
47
|
+
}
|
|
48
|
+
const coalesced = [];
|
|
49
|
+
for (const group of byEntityKey.values()) {
|
|
50
|
+
group.sort((a, b) => a.enqueuedAt - b.enqueuedAt);
|
|
51
|
+
const hasRemove = group.some((m) => m.type === "REMOVE_ITEM");
|
|
52
|
+
if (hasRemove) {
|
|
53
|
+
const removeOp = [...group].reverse().find((m) => m.type === "REMOVE_ITEM");
|
|
54
|
+
coalesced.push(removeOp);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const addOp = group.find((m) => m.type === "ADD_ITEM");
|
|
58
|
+
const updateOps = group.filter((m) => m.type === "UPDATE_ITEM");
|
|
59
|
+
if (addOp && updateOps.length > 0) {
|
|
60
|
+
const latestUpdate = updateOps[updateOps.length - 1];
|
|
61
|
+
coalesced.push({ ...addOp, payload: latestUpdate.payload });
|
|
62
|
+
} else if (addOp) {
|
|
63
|
+
coalesced.push(addOp);
|
|
64
|
+
} else if (updateOps.length > 0) {
|
|
65
|
+
coalesced.push(updateOps[updateOps.length - 1]);
|
|
66
|
+
} else {
|
|
67
|
+
coalesced.push(...group);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...standalone, ...coalesced].sort((a, b) => a.enqueuedAt - b.enqueuedAt);
|
|
71
|
+
}
|
|
72
|
+
async function loadQueue() {
|
|
73
|
+
try {
|
|
74
|
+
const raw = await getStorage().get(QUEUE_KEY);
|
|
75
|
+
if (!raw) return [];
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function saveQueue(queue) {
|
|
82
|
+
try {
|
|
83
|
+
await getStorage().set(QUEUE_KEY, JSON.stringify(queue));
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function backoffMs(retryCount) {
|
|
88
|
+
const exp = Math.min(BACKOFF_BASE_MS * 2 ** retryCount, BACKOFF_MAX_MS);
|
|
89
|
+
return Math.random() * exp;
|
|
90
|
+
}
|
|
91
|
+
var isProcessing = false;
|
|
92
|
+
async function processQueue() {
|
|
93
|
+
if (isProcessing) return;
|
|
94
|
+
isProcessing = true;
|
|
95
|
+
try {
|
|
96
|
+
const raw = await loadQueue();
|
|
97
|
+
if (raw.length === 0) return;
|
|
98
|
+
const queue = coalesceQueue(raw);
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const remaining = [];
|
|
101
|
+
for (const mutation of queue) {
|
|
102
|
+
if (mutation.nextRetryAt > now) {
|
|
103
|
+
remaining.push(mutation);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const executor = executors.get(mutation.type);
|
|
107
|
+
if (!executor) {
|
|
108
|
+
if (__DEV__) {
|
|
109
|
+
console.warn(`[SmartQuery] No executor for "${mutation.type}"`);
|
|
110
|
+
}
|
|
111
|
+
remaining.push(mutation);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await executor(mutation);
|
|
116
|
+
emit({ type: "queue_success", mutationId: mutation.id });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const nextRetry = mutation.retryCount + 1;
|
|
119
|
+
emit({ type: "queue_failure", mutationId: mutation.id, retryCount: nextRetry });
|
|
120
|
+
if (nextRetry >= mutation.maxRetries) {
|
|
121
|
+
if (__DEV__) {
|
|
122
|
+
console.error(
|
|
123
|
+
`[SmartQuery] Mutation ${mutation.id} dropped after ${mutation.maxRetries} retries`,
|
|
124
|
+
err
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
remaining.push({
|
|
129
|
+
...mutation,
|
|
130
|
+
retryCount: nextRetry,
|
|
131
|
+
nextRetryAt: now + backoffMs(nextRetry)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
await saveQueue(remaining);
|
|
137
|
+
if (remaining.length === 0) emit({ type: "queue_drained" });
|
|
138
|
+
} finally {
|
|
139
|
+
isProcessing = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function enqueueMutation(options) {
|
|
143
|
+
const queue = await loadQueue();
|
|
144
|
+
const mutation = {
|
|
145
|
+
id: `mut_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
146
|
+
type: options.type,
|
|
147
|
+
entityKey: options.entityKey,
|
|
148
|
+
queryKey: options.queryKey,
|
|
149
|
+
payload: options.payload,
|
|
150
|
+
enqueuedAt: Date.now(),
|
|
151
|
+
retryCount: 0,
|
|
152
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
153
|
+
nextRetryAt: 0
|
|
154
|
+
};
|
|
155
|
+
queue.push(mutation);
|
|
156
|
+
await saveQueue(queue);
|
|
157
|
+
emit({ type: "queue_enqueue", mutationId: mutation.id, mutationType: mutation.type });
|
|
158
|
+
}
|
|
159
|
+
async function initQueue() {
|
|
160
|
+
await processQueue();
|
|
161
|
+
}
|
|
162
|
+
async function clearQueue() {
|
|
163
|
+
await saveQueue([]);
|
|
164
|
+
}
|
|
165
|
+
var getQueue = loadQueue;
|
|
166
|
+
var getQueueLength = async () => (await loadQueue()).length;
|
|
167
|
+
|
|
168
|
+
export { clearQueue, enqueueMutation, fetchWithLock, getQueue, getQueueLength, inFlightCount, inFlightKeys, initQueue, init_requestLock_service, processQueue, registerExecutor, requestLock_service_exports };
|
|
169
|
+
//# sourceMappingURL=chunk-KLJQATIV.mjs.map
|
|
170
|
+
//# sourceMappingURL=chunk-KLJQATIV.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/services/requestLock.service.ts","../src/services/queue.service.ts"],"names":[],"mappings":"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","file":"chunk-KLJQATIV.mjs","sourcesContent":["/**\n * src/services/requestLock.service.ts\n *\n * In-flight request deduplication.\n * Concurrent calls with the same key share one Promise — only one fetch fires.\n */\n\nconst inFlight = new Map<string, Promise<unknown>>();\n\nexport function fetchWithLock<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const existing = inFlight.get(key);\n if (existing) return existing as Promise<T>;\n\n const promise = fn().finally(() => inFlight.delete(key));\n inFlight.set(key, promise);\n return promise;\n}\n\nexport const inFlightCount = (): number => inFlight.size;\nexport const inFlightKeys = (): string[] => Array.from(inFlight.keys());\n","/**\n * src/services/queue.service.ts\n *\n * Offline Mutation Queue — persist-first, retry-on-reconnect, with coalescing.\n *\n * Key design decisions:\n * • Sequential FIFO processing preserves causal ordering\n * • Coalescing: two mutations with the same entityKey are merged before send\n * (prevents stale optimistic updates from racing each other)\n * • Exponential backoff with full jitter — no thundering herd on reconnect\n * • Observability events on enqueue / success / failure / drain\n * • clearQueue() on logout prevents cross-user mutation leakage\n */\n\nimport { getStorage } from \"./storage.adapter\";\nimport { emit } from \"./observer.service\";\nimport type { QueuedMutation, MutationType } from \"../types\";\n\nexport type { QueuedMutation, MutationType };\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst QUEUE_KEY = \"sq2:mutation_queue\";\nconst DEFAULT_MAX_RETRIES = 5;\nconst BACKOFF_BASE_MS = 1_000;\nconst BACKOFF_MAX_MS = 120_000;\n\n// ─── Executor registry ────────────────────────────────────────────────────────\n\ntype Executor<P = unknown> = (mutation: QueuedMutation<P>) => Promise<void>;\nconst executors = new Map<string, Executor>();\n\n/**\n * Register an executor for a mutation type.\n * Must be called at app startup before mutations are enqueued.\n *\n * @example\n * registerExecutor(\"ADD_ITEM\", async (m) => {\n * await api.post(\"/expenses\", m.payload);\n * });\n */\nexport function registerExecutor<TPayload>(\n type: string,\n fn: Executor<TPayload>\n): void {\n executors.set(type, fn as Executor);\n}\n\n// ─── Queue coalescing ─────────────────────────────────────────────────────────\n\n/**\n * Coalesce mutations with the same entityKey.\n *\n * Rules (applied in order for each entityKey group):\n * • REMOVE_ITEM after any other mutation → keep only REMOVE_ITEM\n * • Multiple UPDATE_ITEM → keep only the latest (highest enqueuedAt)\n * • Multiple ADD_ITEM → keep only the latest (shouldn't normally happen)\n * • ADD_ITEM then UPDATE_ITEM → merge into a single ADD_ITEM with latest payload\n *\n * Mutations without an entityKey are never coalesced.\n */\nfunction coalesceQueue(queue: QueuedMutation[]): QueuedMutation[] {\n // Separate coalesable (have entityKey) from non-coalesable\n const byEntityKey = new Map<string, QueuedMutation[]>();\n const standalone: QueuedMutation[] = [];\n\n for (const m of queue) {\n if (!m.entityKey) { standalone.push(m); continue; }\n const group = byEntityKey.get(m.entityKey) ?? [];\n group.push(m);\n byEntityKey.set(m.entityKey, group);\n }\n\n const coalesced: QueuedMutation[] = [];\n\n for (const group of byEntityKey.values()) {\n // Sort by enqueuedAt ascending within each group\n group.sort((a, b) => a.enqueuedAt - b.enqueuedAt);\n\n const hasRemove = group.some((m) => m.type === \"REMOVE_ITEM\");\n if (hasRemove) {\n // Keep only the last REMOVE_ITEM — all preceding mutations are superseded\n const removeOp = [...group].reverse().find((m) => m.type === \"REMOVE_ITEM\")!;\n coalesced.push(removeOp);\n continue;\n }\n\n const addOp = group.find((m) => m.type === \"ADD_ITEM\");\n const updateOps = group.filter((m) => m.type === \"UPDATE_ITEM\");\n\n if (addOp && updateOps.length > 0) {\n // ADD + UPDATE(s) → single ADD with the latest payload\n const latestUpdate = updateOps[updateOps.length - 1];\n coalesced.push({ ...addOp, payload: latestUpdate.payload });\n } else if (addOp) {\n coalesced.push(addOp);\n } else if (updateOps.length > 0) {\n // Multiple UPDATEs → keep latest\n coalesced.push(updateOps[updateOps.length - 1]);\n } else {\n // CUSTOM or mixed — keep all\n coalesced.push(...group);\n }\n }\n\n // Restore original ordering by enqueuedAt\n return [...standalone, ...coalesced].sort((a, b) => a.enqueuedAt - b.enqueuedAt);\n}\n\n// ─── Persistence ──────────────────────────────────────────────────────────────\n\nasync function loadQueue(): Promise<QueuedMutation[]> {\n try {\n const raw = await getStorage().get(QUEUE_KEY);\n if (!raw) return [];\n return JSON.parse(raw) as QueuedMutation[];\n } catch { return []; }\n}\n\nasync function saveQueue(queue: QueuedMutation[]): Promise<void> {\n try {\n await getStorage().set(QUEUE_KEY, JSON.stringify(queue));\n } catch {}\n}\n\n// ─── Backoff ──────────────────────────────────────────────────────────────────\n\nfunction backoffMs(retryCount: number): number {\n const exp = Math.min(BACKOFF_BASE_MS * 2 ** retryCount, BACKOFF_MAX_MS);\n return Math.random() * exp; // full jitter\n}\n\n// ─── Processing ───────────────────────────────────────────────────────────────\n\nlet isProcessing = false;\n\n/**\n * Process all pending mutations in FIFO order.\n * Coalesces before sending to minimize network calls.\n * Safe to call concurrently — guarded by isProcessing flag.\n */\nexport async function processQueue(): Promise<void> {\n if (isProcessing) return;\n isProcessing = true;\n\n try {\n const raw = await loadQueue();\n if (raw.length === 0) return;\n\n const queue = coalesceQueue(raw);\n const now = Date.now();\n const remaining: QueuedMutation[] = [];\n\n for (const mutation of queue) {\n if (mutation.nextRetryAt > now) { remaining.push(mutation); continue; }\n\n const executor = executors.get(mutation.type);\n if (!executor) {\n if (__DEV__) {\n console.warn(`[SmartQuery] No executor for \"${mutation.type}\"`);\n }\n remaining.push(mutation);\n continue;\n }\n\n try {\n await executor(mutation);\n emit({ type: \"queue_success\", mutationId: mutation.id });\n } catch (err) {\n const nextRetry = mutation.retryCount + 1;\n emit({ type: \"queue_failure\", mutationId: mutation.id, retryCount: nextRetry });\n\n if (nextRetry >= mutation.maxRetries) {\n if (__DEV__) {\n console.error(\n `[SmartQuery] Mutation ${mutation.id} dropped after ${mutation.maxRetries} retries`,\n err\n );\n }\n } else {\n remaining.push({\n ...mutation,\n retryCount: nextRetry,\n nextRetryAt: now + backoffMs(nextRetry),\n });\n }\n }\n }\n\n await saveQueue(remaining);\n if (remaining.length === 0) emit({ type: \"queue_drained\" });\n } finally {\n isProcessing = false;\n }\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Add a mutation to the persistent queue.\n *\n * @param entityKey Optional logical key for coalescing (e.g. \"expense:exp_123\").\n * Mutations with the same entityKey are merged before sending.\n */\nexport async function enqueueMutation<TPayload>(options: {\n type: MutationType | string;\n queryKey: readonly unknown[];\n payload: TPayload;\n entityKey?: string;\n maxRetries?: number;\n}): Promise<void> {\n const queue = await loadQueue();\n const mutation: QueuedMutation<TPayload> = {\n id: `mut_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,\n type: options.type as MutationType,\n entityKey: options.entityKey,\n queryKey: options.queryKey,\n payload: options.payload,\n enqueuedAt: Date.now(),\n retryCount: 0,\n maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,\n nextRetryAt: 0,\n };\n queue.push(mutation as QueuedMutation);\n await saveQueue(queue);\n emit({ type: \"queue_enqueue\", mutationId: mutation.id, mutationType: mutation.type });\n}\n\n/** Process queue on app startup */\nexport async function initQueue(): Promise<void> {\n await processQueue();\n}\n\n/** Clear all pending mutations — call on logout */\nexport async function clearQueue(): Promise<void> {\n await saveQueue([]);\n}\n\nexport const getQueue = loadQueue;\nexport const getQueueLength = async (): Promise<number> =>\n (await loadQueue()).length;\n"]}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { getStorage } from './chunk-QRCVY7UR.mjs';
|
|
2
|
+
|
|
3
|
+
// src/services/observer.service.ts
|
|
4
|
+
var observers = /* @__PURE__ */ new Set();
|
|
5
|
+
function addObserver(fn) {
|
|
6
|
+
observers.add(fn);
|
|
7
|
+
return () => observers.delete(fn);
|
|
8
|
+
}
|
|
9
|
+
function removeObserver(fn) {
|
|
10
|
+
observers.delete(fn);
|
|
11
|
+
}
|
|
12
|
+
function clearObservers() {
|
|
13
|
+
observers.clear();
|
|
14
|
+
}
|
|
15
|
+
function emit(event) {
|
|
16
|
+
if (observers.size === 0) return;
|
|
17
|
+
for (const fn of observers) {
|
|
18
|
+
try {
|
|
19
|
+
fn(event);
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/services/cache.service.ts
|
|
26
|
+
var CURRENT_CACHE_VERSION = 2;
|
|
27
|
+
var DEFAULT_MAX_ENTRIES = 200;
|
|
28
|
+
var _maxEntries = DEFAULT_MAX_ENTRIES;
|
|
29
|
+
function setMaxCacheEntries(n) {
|
|
30
|
+
_maxEntries = n;
|
|
31
|
+
}
|
|
32
|
+
function cacheKeyFor(queryKey) {
|
|
33
|
+
return `sq2:${JSON.stringify(queryKey)}`;
|
|
34
|
+
}
|
|
35
|
+
async function readCache(key, queryKey) {
|
|
36
|
+
try {
|
|
37
|
+
const storage = getStorage();
|
|
38
|
+
const raw = await storage.get(key);
|
|
39
|
+
if (!raw) {
|
|
40
|
+
if (queryKey) emit({ type: "cache_miss", queryKey });
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const entry = JSON.parse(raw);
|
|
44
|
+
if (entry.version !== CURRENT_CACHE_VERSION) {
|
|
45
|
+
void storage.delete(key);
|
|
46
|
+
if (queryKey) emit({ type: "cache_miss", queryKey });
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
void storage.set(
|
|
50
|
+
key,
|
|
51
|
+
JSON.stringify({ ...entry, lastAccessedAt: Date.now() })
|
|
52
|
+
);
|
|
53
|
+
if (queryKey) emit({ type: "cache_hit", queryKey, cachedAt: entry.cachedAt });
|
|
54
|
+
return entry;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function writeCache(key, data, queryKey) {
|
|
60
|
+
try {
|
|
61
|
+
const storage = getStorage();
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const entry = {
|
|
64
|
+
version: CURRENT_CACHE_VERSION,
|
|
65
|
+
data,
|
|
66
|
+
cachedAt: now,
|
|
67
|
+
lastAccessedAt: now
|
|
68
|
+
};
|
|
69
|
+
const serialized = JSON.stringify(entry);
|
|
70
|
+
try {
|
|
71
|
+
await storage.set(key, serialized);
|
|
72
|
+
if (queryKey) {
|
|
73
|
+
emit({ type: "cache_write", queryKey, dataSize: serialized.length });
|
|
74
|
+
}
|
|
75
|
+
} catch (quotaErr) {
|
|
76
|
+
emit({ type: "storage_quota_exceeded", key });
|
|
77
|
+
await evictLRUEntries();
|
|
78
|
+
await storage.set(key, serialized);
|
|
79
|
+
}
|
|
80
|
+
void checkAndEvict();
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function deleteCache(key) {
|
|
85
|
+
try {
|
|
86
|
+
await getStorage().delete(key);
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function checkAndEvict() {
|
|
91
|
+
const storage = getStorage();
|
|
92
|
+
const allKeys = await storage.keys();
|
|
93
|
+
const sqKeys = allKeys.filter((k) => k.startsWith("sq2:"));
|
|
94
|
+
if (sqKeys.length <= _maxEntries) return;
|
|
95
|
+
await evictLRUEntries(sqKeys);
|
|
96
|
+
}
|
|
97
|
+
async function evictLRUEntries(sqKeys) {
|
|
98
|
+
const storage = getStorage();
|
|
99
|
+
const keys = sqKeys ?? (await storage.keys()).filter((k) => k.startsWith("sq2:"));
|
|
100
|
+
const metas = [];
|
|
101
|
+
await Promise.all(
|
|
102
|
+
keys.map(async (key) => {
|
|
103
|
+
try {
|
|
104
|
+
const raw = await storage.get(key);
|
|
105
|
+
if (!raw) return;
|
|
106
|
+
const parsed = JSON.parse(raw);
|
|
107
|
+
metas.push({ key, lastAccessedAt: parsed.lastAccessedAt ?? 0 });
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
metas.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
|
|
113
|
+
const evictCount = Math.max(1, Math.floor(metas.length * 0.2));
|
|
114
|
+
const toEvict = metas.slice(0, evictCount);
|
|
115
|
+
await Promise.all(toEvict.map(({ key }) => storage.delete(key)));
|
|
116
|
+
}
|
|
117
|
+
async function getPartialCache(key, ids) {
|
|
118
|
+
const entry = await readCache(key);
|
|
119
|
+
if (!entry) return null;
|
|
120
|
+
const { byId } = entry.data;
|
|
121
|
+
const result = [];
|
|
122
|
+
for (const id of ids) {
|
|
123
|
+
if (id in byId) result.push(byId[id]);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
function isCacheStale(entry, ttlMs) {
|
|
128
|
+
return Date.now() - entry.cachedAt > ttlMs;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { CURRENT_CACHE_VERSION, addObserver, cacheKeyFor, clearObservers, deleteCache, emit, getPartialCache, isCacheStale, readCache, removeObserver, setMaxCacheEntries, writeCache };
|
|
132
|
+
//# sourceMappingURL=chunk-KSLDOL27.mjs.map
|
|
133
|
+
//# sourceMappingURL=chunk-KSLDOL27.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/services/observer.service.ts","../src/services/cache.service.ts"],"names":[],"mappings":"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","file":"chunk-KSLDOL27.mjs","sourcesContent":["/**\n * src/services/observer.service.ts\n *\n * Pluggable observability — emit structured events to any analytics backend.\n *\n * Zero coupling: the library emits; you decide where it goes.\n * Attach observers at app startup; they receive every internal event.\n *\n * @example\n * // Sentry breadcrumbs\n * addObserver((event) => {\n * if (event.type === \"fetch_error\") {\n * Sentry.addBreadcrumb({ message: event.type, data: event });\n * }\n * });\n *\n * @example\n * // Datadog / Mixpanel\n * addObserver((event) => {\n * analytics.track(event.type, event);\n * });\n *\n * @example\n * // Simple console logger in dev\n * if (__DEV__) addObserver(console.log);\n */\n\nimport type { ObservabilityEvent, ObserverFn } from \"../types\";\n\nconst observers = new Set<ObserverFn>();\n\n/**\n * Register an observer. Returns an unsubscribe function.\n *\n * @example\n * const unsub = addObserver(myLogger);\n * // Later:\n * unsub();\n */\nexport function addObserver(fn: ObserverFn): () => void {\n observers.add(fn);\n return () => observers.delete(fn);\n}\n\n/** Remove a specific observer */\nexport function removeObserver(fn: ObserverFn): void {\n observers.delete(fn);\n}\n\n/** Remove all observers */\nexport function clearObservers(): void {\n observers.clear();\n}\n\n/**\n * @internal — emit an event to all registered observers.\n * Called by cache.service, queue.service, and useSmartQuery.\n * Never throws — observer errors are swallowed to protect the data path.\n */\nexport function emit(event: ObservabilityEvent): void {\n if (observers.size === 0) return; // fast path — no observers registered\n for (const fn of observers) {\n try { fn(event); } catch { /* observer error must not crash the app */ }\n }\n}\n","/**\n * src/services/cache.service.ts\n *\n * Versioned, LRU-aware cache layer.\n *\n * Features:\n * • Schema versioning — auto-invalidates stale entries on version bump\n * • lastAccessedAt tracking — enables LRU eviction\n * • Configurable max entries per prefix — prevents unbounded growth\n * • Partial hydration — read a subset of a NormalizedList by ids\n * • Observability events on hit / miss / write / quota exceeded\n */\n\nimport { getStorage } from \"./storage.adapter\";\nimport { emit } from \"./observer.service\";\nimport type { CacheEntry, NormalizedList, AnyItem } from \"../types\";\n\n// ─── Versioning ───────────────────────────────────────────────────────────────\n\n/**\n * Bump when CacheEntry shape or NormalizedList schema changes in a\n * breaking way. Any stored entry with a lower version is silently discarded.\n */\nexport const CURRENT_CACHE_VERSION = 2;\n\n// ─── LRU config ───────────────────────────────────────────────────────────────\n\nconst DEFAULT_MAX_ENTRIES = 200;\nlet _maxEntries = DEFAULT_MAX_ENTRIES;\n\n/** Override the global max entries limit (call before any reads/writes) */\nexport function setMaxCacheEntries(n: number): void {\n _maxEntries = n;\n}\n\n// ─── Key derivation ───────────────────────────────────────────────────────────\n\nexport function cacheKeyFor(queryKey: readonly unknown[]): string {\n return `sq2:${JSON.stringify(queryKey)}`;\n}\n\n// ─── Read ─────────────────────────────────────────────────────────────────────\n\nexport async function readCache<T>(\n key: string,\n queryKey?: readonly unknown[]\n): Promise<CacheEntry<T> | null> {\n try {\n const storage = getStorage();\n const raw = await storage.get(key);\n\n if (!raw) {\n if (queryKey) emit({ type: \"cache_miss\", queryKey });\n return null;\n }\n\n const entry = JSON.parse(raw) as CacheEntry<T>;\n\n if (entry.version !== CURRENT_CACHE_VERSION) {\n void storage.delete(key);\n if (queryKey) emit({ type: \"cache_miss\", queryKey });\n return null;\n }\n\n // Touch lastAccessedAt for LRU — fire-and-forget, non-blocking\n void storage.set(\n key,\n JSON.stringify({ ...entry, lastAccessedAt: Date.now() })\n );\n\n if (queryKey) emit({ type: \"cache_hit\", queryKey, cachedAt: entry.cachedAt });\n return entry;\n } catch {\n return null;\n }\n}\n\n// ─── Write ────────────────────────────────────────────────────────────────────\n\nexport async function writeCache<T>(\n key: string,\n data: T,\n queryKey?: readonly unknown[]\n): Promise<void> {\n try {\n const storage = getStorage();\n const now = Date.now();\n const entry: CacheEntry<T> = {\n version: CURRENT_CACHE_VERSION,\n data,\n cachedAt: now,\n lastAccessedAt: now,\n };\n\n const serialized = JSON.stringify(entry);\n\n try {\n await storage.set(key, serialized);\n if (queryKey) {\n emit({ type: \"cache_write\", queryKey, dataSize: serialized.length });\n }\n } catch (quotaErr) {\n emit({ type: \"storage_quota_exceeded\", key });\n // Attempt LRU eviction then retry once\n await evictLRUEntries();\n await storage.set(key, serialized);\n }\n\n // Async LRU check — doesn't block the write\n void checkAndEvict();\n } catch {\n // Fail silently — a cache write failure must never crash the app\n }\n}\n\n// ─── Delete ───────────────────────────────────────────────────────────────────\n\nexport async function deleteCache(key: string): Promise<void> {\n try {\n await getStorage().delete(key);\n } catch {}\n}\n\n// ─── LRU eviction ─────────────────────────────────────────────────────────────\n\ninterface LRUMeta {\n key: string;\n lastAccessedAt: number;\n}\n\nasync function checkAndEvict(): Promise<void> {\n const storage = getStorage();\n const allKeys = await storage.keys();\n const sqKeys = allKeys.filter((k) => k.startsWith(\"sq2:\"));\n\n if (sqKeys.length <= _maxEntries) return;\n\n await evictLRUEntries(sqKeys);\n}\n\nasync function evictLRUEntries(sqKeys?: string[]): Promise<void> {\n const storage = getStorage();\n const keys = sqKeys ?? (await storage.keys()).filter((k) => k.startsWith(\"sq2:\"));\n\n // Read lastAccessedAt for each entry — lightweight parse\n const metas: LRUMeta[] = [];\n await Promise.all(\n keys.map(async (key) => {\n try {\n const raw = await storage.get(key);\n if (!raw) return;\n const parsed = JSON.parse(raw) as Partial<CacheEntry<unknown>>;\n metas.push({ key, lastAccessedAt: parsed.lastAccessedAt ?? 0 });\n } catch {}\n })\n );\n\n // Sort oldest-first and evict the bottom 20%\n metas.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n const evictCount = Math.max(1, Math.floor(metas.length * 0.2));\n const toEvict = metas.slice(0, evictCount);\n\n await Promise.all(toEvict.map(({ key }) => storage.delete(key)));\n}\n\n// ─── Partial hydration ────────────────────────────────────────────────────────\n\n/**\n * Read a subset of a NormalizedList cache entry by item ids.\n *\n * Use for pagination, lazy loading, or detail views that only need\n * a handful of items from a large cached list.\n *\n * @returns null if the cache entry doesn't exist.\n * Empty array if none of the requested ids are cached.\n *\n * @example\n * const items = await getPartialCache<Expense>(\n * cacheKeyFor([\"expenses\", tripId]),\n * [\"exp_1\", \"exp_2\"]\n * );\n */\nexport async function getPartialCache<T extends AnyItem>(\n key: string,\n ids: string[]\n): Promise<T[] | null> {\n const entry = await readCache<NormalizedList<T>>(key);\n if (!entry) return null;\n\n const { byId } = entry.data;\n const result: T[] = [];\n for (const id of ids) {\n if (id in byId) result.push(byId[id]);\n }\n return result;\n}\n\n// ─── TTL check ────────────────────────────────────────────────────────────────\n\nexport function isCacheStale(entry: CacheEntry<unknown>, ttlMs: number): boolean {\n return Date.now() - entry.cachedAt > ttlMs;\n}\n"]}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
13
|
+
var __esm = (fn, res) => function __init() {
|
|
14
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
|
+
};
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
function createMemoryStorage() {
|
|
30
|
+
const store = /* @__PURE__ */ new Map();
|
|
31
|
+
return {
|
|
32
|
+
get: (key) => Promise.resolve(store.get(key)),
|
|
33
|
+
set: (key, value) => {
|
|
34
|
+
store.set(key, value);
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
},
|
|
37
|
+
delete: (key) => {
|
|
38
|
+
store.delete(key);
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
},
|
|
41
|
+
clearAll: () => {
|
|
42
|
+
store.clear();
|
|
43
|
+
return Promise.resolve();
|
|
44
|
+
},
|
|
45
|
+
keys: () => Promise.resolve(Array.from(store.keys()))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function createNativeStorage() {
|
|
49
|
+
const MMKV = __require("react-native-mmkv").MMKV;
|
|
50
|
+
const mmkv = new MMKV({ id: "react-smart-query-v2" });
|
|
51
|
+
return {
|
|
52
|
+
get: (key) => Promise.resolve(mmkv.getString(key) ?? void 0),
|
|
53
|
+
set: (key, value) => Promise.resolve(void mmkv.set(key, value)),
|
|
54
|
+
delete: (key) => Promise.resolve(void mmkv.delete(key)),
|
|
55
|
+
clearAll: () => Promise.resolve(void mmkv.clearAll()),
|
|
56
|
+
keys: () => Promise.resolve(mmkv.getAllKeys())
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
var IDB_NAME = "SmartQueryV2";
|
|
60
|
+
var IDB_STORE = "entries";
|
|
61
|
+
var IDB_VERSION = 1;
|
|
62
|
+
function isIDBAvailable() {
|
|
63
|
+
return typeof globalThis !== "undefined" && typeof globalThis.indexedDB !== "undefined";
|
|
64
|
+
}
|
|
65
|
+
function openIDB() {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const req = indexedDB.open(IDB_NAME, IDB_VERSION);
|
|
68
|
+
req.onupgradeneeded = (e) => {
|
|
69
|
+
const db = e.target.result;
|
|
70
|
+
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
|
71
|
+
db.createObjectStore(IDB_STORE);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
req.onsuccess = (e) => resolve(e.target.result);
|
|
75
|
+
req.onerror = () => reject(req.error);
|
|
76
|
+
req.onblocked = () => reject(new Error("IDB blocked by another tab"));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
var _idb = null;
|
|
80
|
+
var getIDB = () => {
|
|
81
|
+
_idb ??= openIDB();
|
|
82
|
+
return _idb;
|
|
83
|
+
};
|
|
84
|
+
function idbWrap(req) {
|
|
85
|
+
return new Promise((res, rej) => {
|
|
86
|
+
req.onsuccess = () => res(req.result);
|
|
87
|
+
req.onerror = () => rej(req.error);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function createWebStorage() {
|
|
91
|
+
if (!isIDBAvailable()) return createMemoryStorage();
|
|
92
|
+
return {
|
|
93
|
+
async get(key) {
|
|
94
|
+
const db = await getIDB();
|
|
95
|
+
return idbWrap(
|
|
96
|
+
db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).get(key)
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
async set(key, value) {
|
|
100
|
+
const db = await getIDB();
|
|
101
|
+
await idbWrap(
|
|
102
|
+
db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).put(value, key)
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
async delete(key) {
|
|
106
|
+
const db = await getIDB();
|
|
107
|
+
await idbWrap(
|
|
108
|
+
db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).delete(key)
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
async clearAll() {
|
|
112
|
+
const db = await getIDB();
|
|
113
|
+
await idbWrap(
|
|
114
|
+
db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).clear()
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
async keys() {
|
|
118
|
+
const db = await getIDB();
|
|
119
|
+
const result = await idbWrap(
|
|
120
|
+
db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).getAllKeys()
|
|
121
|
+
);
|
|
122
|
+
return result.map(String);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
var storage = Platform.OS === "web" ? createWebStorage() : createNativeStorage();
|
|
127
|
+
var _overrideStorage = null;
|
|
128
|
+
function getStorage() {
|
|
129
|
+
return _overrideStorage ?? storage;
|
|
130
|
+
}
|
|
131
|
+
function _setStorageOverride(s) {
|
|
132
|
+
_overrideStorage = s;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { __esm, __export, __toCommonJS, _setStorageOverride, getStorage, storage };
|
|
136
|
+
//# sourceMappingURL=chunk-QRCVY7UR.mjs.map
|
|
137
|
+
//# sourceMappingURL=chunk-QRCVY7UR.mjs.map
|