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.
@@ -0,0 +1,272 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var reactQuery = require('@tanstack/react-query');
5
+ var reactNative = require('react-native');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var React__default = /*#__PURE__*/_interopDefault(React);
10
+
11
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
12
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
13
+ }) : x)(function(x) {
14
+ if (typeof require !== "undefined") return require.apply(this, arguments);
15
+ throw Error('Dynamic require of "' + x + '" is not supported');
16
+ });
17
+ function createMemoryStorage() {
18
+ const store = /* @__PURE__ */ new Map();
19
+ return {
20
+ get: (key) => Promise.resolve(store.get(key)),
21
+ set: (key, value) => {
22
+ store.set(key, value);
23
+ return Promise.resolve();
24
+ },
25
+ delete: (key) => {
26
+ store.delete(key);
27
+ return Promise.resolve();
28
+ },
29
+ clearAll: () => {
30
+ store.clear();
31
+ return Promise.resolve();
32
+ },
33
+ keys: () => Promise.resolve(Array.from(store.keys()))
34
+ };
35
+ }
36
+ function createNativeStorage() {
37
+ const MMKV = __require("react-native-mmkv").MMKV;
38
+ const mmkv = new MMKV({ id: "react-smart-query-v2" });
39
+ return {
40
+ get: (key) => Promise.resolve(mmkv.getString(key) ?? void 0),
41
+ set: (key, value) => Promise.resolve(void mmkv.set(key, value)),
42
+ delete: (key) => Promise.resolve(void mmkv.delete(key)),
43
+ clearAll: () => Promise.resolve(void mmkv.clearAll()),
44
+ keys: () => Promise.resolve(mmkv.getAllKeys())
45
+ };
46
+ }
47
+ var IDB_NAME = "SmartQueryV2";
48
+ var IDB_STORE = "entries";
49
+ var IDB_VERSION = 1;
50
+ function isIDBAvailable() {
51
+ return typeof globalThis !== "undefined" && typeof globalThis.indexedDB !== "undefined";
52
+ }
53
+ function openIDB() {
54
+ return new Promise((resolve, reject) => {
55
+ const req = indexedDB.open(IDB_NAME, IDB_VERSION);
56
+ req.onupgradeneeded = (e) => {
57
+ const db = e.target.result;
58
+ if (!db.objectStoreNames.contains(IDB_STORE)) {
59
+ db.createObjectStore(IDB_STORE);
60
+ }
61
+ };
62
+ req.onsuccess = (e) => resolve(e.target.result);
63
+ req.onerror = () => reject(req.error);
64
+ req.onblocked = () => reject(new Error("IDB blocked by another tab"));
65
+ });
66
+ }
67
+ var _idb = null;
68
+ var getIDB = () => {
69
+ _idb ??= openIDB();
70
+ return _idb;
71
+ };
72
+ function idbWrap(req) {
73
+ return new Promise((res, rej) => {
74
+ req.onsuccess = () => res(req.result);
75
+ req.onerror = () => rej(req.error);
76
+ });
77
+ }
78
+ function createWebStorage() {
79
+ if (!isIDBAvailable()) return createMemoryStorage();
80
+ return {
81
+ async get(key) {
82
+ const db = await getIDB();
83
+ return idbWrap(
84
+ db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).get(key)
85
+ );
86
+ },
87
+ async set(key, value) {
88
+ const db = await getIDB();
89
+ await idbWrap(
90
+ db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).put(value, key)
91
+ );
92
+ },
93
+ async delete(key) {
94
+ const db = await getIDB();
95
+ await idbWrap(
96
+ db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).delete(key)
97
+ );
98
+ },
99
+ async clearAll() {
100
+ const db = await getIDB();
101
+ await idbWrap(
102
+ db.transaction(IDB_STORE, "readwrite").objectStore(IDB_STORE).clear()
103
+ );
104
+ },
105
+ async keys() {
106
+ const db = await getIDB();
107
+ const result = await idbWrap(
108
+ db.transaction(IDB_STORE, "readonly").objectStore(IDB_STORE).getAllKeys()
109
+ );
110
+ return result.map(String);
111
+ }
112
+ };
113
+ }
114
+ var storage = reactNative.Platform.OS === "web" ? createWebStorage() : createNativeStorage();
115
+ var _overrideStorage = null;
116
+ function getStorage() {
117
+ return _overrideStorage ?? storage;
118
+ }
119
+ function _setStorageOverride(s) {
120
+ _overrideStorage = s;
121
+ }
122
+
123
+ // src/services/observer.service.ts
124
+ var observers = /* @__PURE__ */ new Set();
125
+ function emit(event) {
126
+ if (observers.size === 0) return;
127
+ for (const fn of observers) {
128
+ try {
129
+ fn(event);
130
+ } catch {
131
+ }
132
+ }
133
+ }
134
+
135
+ // src/services/cache.service.ts
136
+ var CURRENT_CACHE_VERSION = 2;
137
+ var DEFAULT_MAX_ENTRIES = 200;
138
+ var _maxEntries = DEFAULT_MAX_ENTRIES;
139
+ function cacheKeyFor(queryKey) {
140
+ return `sq2:${JSON.stringify(queryKey)}`;
141
+ }
142
+ async function writeCache(key, data, queryKey) {
143
+ try {
144
+ const storage2 = getStorage();
145
+ const now = Date.now();
146
+ const entry = {
147
+ version: CURRENT_CACHE_VERSION,
148
+ data,
149
+ cachedAt: now,
150
+ lastAccessedAt: now
151
+ };
152
+ const serialized = JSON.stringify(entry);
153
+ try {
154
+ await storage2.set(key, serialized);
155
+ if (queryKey) ;
156
+ } catch (quotaErr) {
157
+ emit({ type: "storage_quota_exceeded", key });
158
+ await evictLRUEntries();
159
+ await storage2.set(key, serialized);
160
+ }
161
+ void checkAndEvict();
162
+ } catch {
163
+ }
164
+ }
165
+ async function checkAndEvict() {
166
+ const storage2 = getStorage();
167
+ const allKeys = await storage2.keys();
168
+ const sqKeys = allKeys.filter((k) => k.startsWith("sq2:"));
169
+ if (sqKeys.length <= _maxEntries) return;
170
+ await evictLRUEntries(sqKeys);
171
+ }
172
+ async function evictLRUEntries(sqKeys) {
173
+ const storage2 = getStorage();
174
+ const keys = sqKeys ?? (await storage2.keys()).filter((k) => k.startsWith("sq2:"));
175
+ const metas = [];
176
+ await Promise.all(
177
+ keys.map(async (key) => {
178
+ try {
179
+ const raw = await storage2.get(key);
180
+ if (!raw) return;
181
+ const parsed = JSON.parse(raw);
182
+ metas.push({ key, lastAccessedAt: parsed.lastAccessedAt ?? 0 });
183
+ } catch {
184
+ }
185
+ })
186
+ );
187
+ metas.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
188
+ const evictCount = Math.max(1, Math.floor(metas.length * 0.2));
189
+ const toEvict = metas.slice(0, evictCount);
190
+ await Promise.all(toEvict.map(({ key }) => storage2.delete(key)));
191
+ }
192
+
193
+ // src/testing.ts
194
+ function createMockStorage() {
195
+ const store = /* @__PURE__ */ new Map();
196
+ return {
197
+ get: (key) => Promise.resolve(store.get(key)),
198
+ set: (key, value) => {
199
+ store.set(key, value);
200
+ return Promise.resolve();
201
+ },
202
+ delete: (key) => {
203
+ store.delete(key);
204
+ return Promise.resolve();
205
+ },
206
+ clearAll: () => {
207
+ store.clear();
208
+ return Promise.resolve();
209
+ },
210
+ keys: () => Promise.resolve(Array.from(store.keys()))
211
+ };
212
+ }
213
+ var _mockStorage = null;
214
+ function ensureMockStorage() {
215
+ if (!_mockStorage) {
216
+ _mockStorage = createMockStorage();
217
+ _setStorageOverride(_mockStorage);
218
+ }
219
+ return _mockStorage;
220
+ }
221
+ function SmartQueryTestProvider({
222
+ children,
223
+ queryClient
224
+ }) {
225
+ const client = queryClient ?? new reactQuery.QueryClient({
226
+ defaultOptions: {
227
+ queries: {
228
+ retry: false,
229
+ gcTime: Infinity
230
+ }
231
+ }
232
+ });
233
+ React.useEffect(() => {
234
+ ensureMockStorage();
235
+ return () => {
236
+ _setStorageOverride(null);
237
+ _mockStorage = null;
238
+ };
239
+ }, []);
240
+ return React__default.default.createElement(
241
+ reactQuery.QueryClientProvider,
242
+ { client },
243
+ children
244
+ );
245
+ }
246
+ async function seedCache(queryKey, data) {
247
+ ensureMockStorage();
248
+ const key = cacheKeyFor(queryKey);
249
+ await writeCache(key, data);
250
+ }
251
+ async function clearTestCache() {
252
+ await ensureMockStorage().clearAll();
253
+ }
254
+ function waitForCacheLoad(ms = 50) {
255
+ return new Promise((resolve) => setTimeout(resolve, ms));
256
+ }
257
+ function mockQueryFn(data, delayMs = 0) {
258
+ return () => new Promise((resolve) => setTimeout(() => resolve(data), delayMs));
259
+ }
260
+ function mockErrorFn(error, delayMs = 0) {
261
+ return () => new Promise((_, reject) => setTimeout(() => reject(error), delayMs));
262
+ }
263
+
264
+ exports.SmartQueryTestProvider = SmartQueryTestProvider;
265
+ exports.clearTestCache = clearTestCache;
266
+ exports.createMockStorage = createMockStorage;
267
+ exports.mockErrorFn = mockErrorFn;
268
+ exports.mockQueryFn = mockQueryFn;
269
+ exports.seedCache = seedCache;
270
+ exports.waitForCacheLoad = waitForCacheLoad;
271
+ //# sourceMappingURL=testing.js.map
272
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/services/storage.adapter.ts","../src/services/observer.service.ts","../src/services/cache.service.ts","../src/testing.ts"],"names":["Platform","storage","QueryClient","useEffect","React","QueryClientProvider"],"mappings":"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","file":"testing.js","sourcesContent":["/**\n * src/services/storage.adapter.ts\n *\n * Platform-aware storage with SSR safety.\n *\n * iOS / Android → react-native-mmkv\n * Web (browser) → IndexedDB\n * SSR / Node.js → in-memory Map (safe no-op, hydrates on client)\n *\n * The SSR guard prevents \"indexedDB is not defined\" crashes in\n * Next.js / Expo Router SSR builds. Data written during SSR is discarded;\n * the client hydrates from real IDB on first mount.\n */\n\nimport { Platform } from \"react-native\";\nimport type { AsyncStorage } from \"../types\";\n\nexport type { AsyncStorage };\n\n// ─── SSR / Node.js in-memory adapter ─────────────────────────────────────────\n\nfunction createMemoryStorage(): AsyncStorage {\n const store = new Map<string, string>();\n return {\n get: (key) => Promise.resolve(store.get(key)),\n set: (key, value) => { store.set(key, value); return Promise.resolve(); },\n delete: (key) => { store.delete(key); return Promise.resolve(); },\n clearAll: () => { store.clear(); return Promise.resolve(); },\n keys: () => Promise.resolve(Array.from(store.keys())),\n };\n}\n\n// ─── Native adapter (iOS + Android) ──────────────────────────────────────────\n\nfunction createNativeStorage(): AsyncStorage {\n const MMKV = require(\"react-native-mmkv\").MMKV;\n const mmkv = new MMKV({ id: \"react-smart-query-v2\" });\n return {\n get: (key) => Promise.resolve(mmkv.getString(key) ?? undefined),\n set: (key, value) => Promise.resolve(void mmkv.set(key, value)),\n delete: (key) => Promise.resolve(void mmkv.delete(key)),\n clearAll: () => Promise.resolve(void mmkv.clearAll()),\n keys: () => Promise.resolve(mmkv.getAllKeys()),\n };\n}\n\n// ─── Web adapter (IndexedDB) with SSR guard ───────────────────────────────────\n\nconst IDB_NAME = \"SmartQueryV2\";\nconst IDB_STORE = \"entries\";\nconst IDB_VERSION = 1;\n\nfunction isIDBAvailable(): boolean {\n return typeof globalThis !== \"undefined\" &&\n typeof (globalThis as unknown as Record<string, unknown>).indexedDB !== \"undefined\";\n}\n\nfunction openIDB(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(IDB_NAME, IDB_VERSION);\n req.onupgradeneeded = (e) => {\n const db = (e.target as IDBOpenDBRequest).result;\n if (!db.objectStoreNames.contains(IDB_STORE)) {\n db.createObjectStore(IDB_STORE);\n }\n };\n req.onsuccess = (e) => resolve((e.target as IDBOpenDBRequest).result);\n req.onerror = () => reject(req.error);\n req.onblocked = () => reject(new Error(\"IDB blocked by another tab\"));\n });\n}\n\nlet _idb: Promise<IDBDatabase> | null = null;\nconst getIDB = () => { _idb ??= openIDB(); return _idb; };\n\nfunction idbWrap<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise((res, rej) => {\n req.onsuccess = () => res(req.result);\n req.onerror = () => rej(req.error);\n });\n}\n\nfunction createWebStorage(): AsyncStorage {\n // SSR guard — return memory storage when IDB is unavailable\n if (!isIDBAvailable()) return createMemoryStorage();\n\n return {\n async get(key) {\n const db = await getIDB();\n return idbWrap<string | undefined>(\n db.transaction(IDB_STORE, \"readonly\").objectStore(IDB_STORE).get(key)\n );\n },\n async set(key, value) {\n const db = await getIDB();\n await idbWrap(\n db.transaction(IDB_STORE, \"readwrite\").objectStore(IDB_STORE).put(value, key)\n );\n },\n async delete(key) {\n const db = await getIDB();\n await idbWrap(\n db.transaction(IDB_STORE, \"readwrite\").objectStore(IDB_STORE).delete(key)\n );\n },\n async clearAll() {\n const db = await getIDB();\n await idbWrap(\n db.transaction(IDB_STORE, \"readwrite\").objectStore(IDB_STORE).clear()\n );\n },\n async keys() {\n const db = await getIDB();\n const result = await idbWrap<IDBValidKey[]>(\n db.transaction(IDB_STORE, \"readonly\").objectStore(IDB_STORE).getAllKeys()\n );\n return result.map(String);\n },\n };\n}\n\n// ─── Singleton ────────────────────────────────────────────────────────────────\n\nexport const storage: AsyncStorage =\n Platform.OS === \"web\" ? createWebStorage() : createNativeStorage();\n\n/** Exposed for test utilities to inject a custom adapter */\nlet _overrideStorage: AsyncStorage | null = null;\n\nexport function getStorage(): AsyncStorage {\n return _overrideStorage ?? storage;\n}\n\n/** @internal — used by SmartQueryTestProvider only */\nexport function _setStorageOverride(s: AsyncStorage | null): void {\n _overrideStorage = s;\n}\n","/**\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","/**\n * src/testing.ts\n *\n * Test utilities for SmartQuery.\n *\n * Provides:\n * • SmartQueryTestProvider — wraps components with all required context\n * • createMockStorage — in-memory AsyncStorage (no MMKV, no IDB)\n * • seedCache — pre-populate cache before rendering\n * • clearTestCache — reset between tests\n * • waitForCacheLoad — await the initial async cache read\n *\n * Works with Jest, Vitest, and React Native Testing Library.\n *\n * @example\n * import { render } from \"@testing-library/react-native\";\n * import { SmartQueryTestProvider, seedCache } from \"react-smart-query/testing\";\n *\n * beforeEach(() => seedCache([\"expenses\", \"trip_1\"], mockExpenses));\n *\n * it(\"renders cached expenses\", async () => {\n * const { findAllByTestId } = render(\n * <SmartQueryTestProvider>\n * <ExpenseList tripId=\"trip_1\" />\n * </SmartQueryTestProvider>\n * );\n * const rows = await findAllByTestId(\"expense-row\");\n * expect(rows).toHaveLength(mockExpenses.length);\n * });\n */\n\nimport React, { ReactNode, useEffect } from \"react\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { _setStorageOverride } from \"./services/storage.adapter\";\nimport { writeCache, cacheKeyFor } from \"./services/cache.service\";\nimport type { AsyncStorage, AnyItem } from \"./types\";\n\n// ─── In-memory storage ────────────────────────────────────────────────────────\n\nexport function createMockStorage(): AsyncStorage {\n const store = new Map<string, string>();\n return {\n get: (key) => Promise.resolve(store.get(key)),\n set: (key, value) => { store.set(key, value); return Promise.resolve(); },\n delete: (key) => { store.delete(key); return Promise.resolve(); },\n clearAll: () => { store.clear(); return Promise.resolve(); },\n keys: () => Promise.resolve(Array.from(store.keys())),\n };\n}\n\n// Module-level mock storage used by all test utilities\nlet _mockStorage: AsyncStorage | null = null;\n\nfunction ensureMockStorage(): AsyncStorage {\n if (!_mockStorage) {\n _mockStorage = createMockStorage();\n _setStorageOverride(_mockStorage);\n }\n return _mockStorage;\n}\n\n// ─── SmartQueryTestProvider ───────────────────────────────────────────────────\n\ninterface TestProviderProps {\n children: ReactNode;\n /** Override the QueryClient (e.g. to set custom defaults) */\n queryClient?: QueryClient;\n}\n\n/**\n * Wrap your component tree in tests.\n * Automatically uses in-memory storage — no MMKV, no IndexedDB required.\n */\nexport function SmartQueryTestProvider({\n children,\n queryClient,\n}: TestProviderProps): React.ReactElement {\n const client = queryClient ?? new QueryClient({\n defaultOptions: {\n queries: {\n retry: false,\n gcTime: Infinity,\n },\n },\n });\n\n useEffect(() => {\n ensureMockStorage();\n return () => {\n // Clean up override after the test\n _setStorageOverride(null);\n _mockStorage = null;\n };\n }, []);\n\n return React.createElement(\n QueryClientProvider,\n { client },\n children\n );\n}\n\n// ─── Cache seeding ────────────────────────────────────────────────────────────\n\n/**\n * Pre-populate the cache before rendering a component.\n * Must be called before rendering (not inside a component).\n *\n * @example\n * beforeEach(() => seedCache([\"expenses\", \"trip_1\"], mockExpenses));\n */\nexport async function seedCache<T>(\n queryKey: readonly unknown[],\n data: T\n): Promise<void> {\n ensureMockStorage();\n const key = cacheKeyFor(queryKey);\n await writeCache(key, data);\n}\n\n/**\n * Clear all test cache entries between tests.\n *\n * @example\n * afterEach(() => clearTestCache());\n */\nexport async function clearTestCache(): Promise<void> {\n await ensureMockStorage().clearAll();\n}\n\n/**\n * Wait for the initial cache read to complete.\n * Useful when you need to assert on cached data immediately after render.\n *\n * @example\n * const { getByText } = render(<MyComponent />);\n * await waitForCacheLoad();\n * expect(getByText(\"Expense 1\")).toBeTruthy();\n */\nexport function waitForCacheLoad(ms = 50): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ─── Mock query factory ───────────────────────────────────────────────────────\n\n/**\n * Create a mock queryFn that resolves with the provided data.\n * Use to test loading → success transitions.\n *\n * @example\n * queryFn: mockQueryFn(mockExpenses, 100) // resolves after 100ms\n */\nexport function mockQueryFn<T>(data: T, delayMs = 0): () => Promise<T> {\n return () =>\n new Promise((resolve) => setTimeout(() => resolve(data), delayMs));\n}\n\n/**\n * Create a mock queryFn that rejects with the provided error.\n * Use to test error states.\n *\n * @example\n * queryFn: mockErrorFn(new Error(\"Network error\"))\n */\nexport function mockErrorFn(error: Error, delayMs = 0): () => Promise<never> {\n return () =>\n new Promise((_, reject) => setTimeout(() => reject(error), delayMs));\n}\n"]}
@@ -0,0 +1,78 @@
1
+ import { cacheKeyFor, writeCache } from './chunk-KSLDOL27.mjs';
2
+ import { _setStorageOverride } from './chunk-QRCVY7UR.mjs';
3
+ import React, { useEffect } from 'react';
4
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
+
6
+ function createMockStorage() {
7
+ const store = /* @__PURE__ */ new Map();
8
+ return {
9
+ get: (key) => Promise.resolve(store.get(key)),
10
+ set: (key, value) => {
11
+ store.set(key, value);
12
+ return Promise.resolve();
13
+ },
14
+ delete: (key) => {
15
+ store.delete(key);
16
+ return Promise.resolve();
17
+ },
18
+ clearAll: () => {
19
+ store.clear();
20
+ return Promise.resolve();
21
+ },
22
+ keys: () => Promise.resolve(Array.from(store.keys()))
23
+ };
24
+ }
25
+ var _mockStorage = null;
26
+ function ensureMockStorage() {
27
+ if (!_mockStorage) {
28
+ _mockStorage = createMockStorage();
29
+ _setStorageOverride(_mockStorage);
30
+ }
31
+ return _mockStorage;
32
+ }
33
+ function SmartQueryTestProvider({
34
+ children,
35
+ queryClient
36
+ }) {
37
+ const client = queryClient ?? new QueryClient({
38
+ defaultOptions: {
39
+ queries: {
40
+ retry: false,
41
+ gcTime: Infinity
42
+ }
43
+ }
44
+ });
45
+ useEffect(() => {
46
+ ensureMockStorage();
47
+ return () => {
48
+ _setStorageOverride(null);
49
+ _mockStorage = null;
50
+ };
51
+ }, []);
52
+ return React.createElement(
53
+ QueryClientProvider,
54
+ { client },
55
+ children
56
+ );
57
+ }
58
+ async function seedCache(queryKey, data) {
59
+ ensureMockStorage();
60
+ const key = cacheKeyFor(queryKey);
61
+ await writeCache(key, data);
62
+ }
63
+ async function clearTestCache() {
64
+ await ensureMockStorage().clearAll();
65
+ }
66
+ function waitForCacheLoad(ms = 50) {
67
+ return new Promise((resolve) => setTimeout(resolve, ms));
68
+ }
69
+ function mockQueryFn(data, delayMs = 0) {
70
+ return () => new Promise((resolve) => setTimeout(() => resolve(data), delayMs));
71
+ }
72
+ function mockErrorFn(error, delayMs = 0) {
73
+ return () => new Promise((_, reject) => setTimeout(() => reject(error), delayMs));
74
+ }
75
+
76
+ export { SmartQueryTestProvider, clearTestCache, createMockStorage, mockErrorFn, mockQueryFn, seedCache, waitForCacheLoad };
77
+ //# sourceMappingURL=testing.mjs.map
78
+ //# sourceMappingURL=testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing.ts"],"names":[],"mappings":";;;;;AAuCO,SAAS,iBAAA,GAAkC;AAChD,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAoB;AACtC,EAAA,OAAO;AAAA,IACL,GAAA,EAAK,CAAC,GAAA,KAAQ,OAAA,CAAQ,QAAQ,KAAA,CAAM,GAAA,CAAI,GAAG,CAAC,CAAA;AAAA,IAC5C,GAAA,EAAK,CAAC,GAAA,EAAK,KAAA,KAAU;AAAE,MAAA,KAAA,CAAM,GAAA,CAAI,KAAK,KAAK,CAAA;AAAG,MAAA,OAAO,QAAQ,OAAA,EAAQ;AAAA,IAAG,CAAA;AAAA,IACxE,MAAA,EAAQ,CAAC,GAAA,KAAQ;AAAE,MAAA,KAAA,CAAM,OAAO,GAAG,CAAA;AAAG,MAAA,OAAO,QAAQ,OAAA,EAAQ;AAAA,IAAG,CAAA;AAAA,IAChE,UAAU,MAAM;AAAE,MAAA,KAAA,CAAM,KAAA,EAAM;AAAG,MAAA,OAAO,QAAQ,OAAA,EAAQ;AAAA,IAAG,CAAA;AAAA,IAC3D,IAAA,EAAM,MAAM,OAAA,CAAQ,OAAA,CAAQ,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,CAAC;AAAA,GACtD;AACF;AAGA,IAAI,YAAA,GAAoC,IAAA;AAExC,SAAS,iBAAA,GAAkC;AACzC,EAAA,IAAI,CAAC,YAAA,EAAc;AACjB,IAAA,YAAA,GAAe,iBAAA,EAAkB;AACjC,IAAA,mBAAA,CAAoB,YAAY,CAAA;AAAA,EAClC;AACA,EAAA,OAAO,YAAA;AACT;AAcO,SAAS,sBAAA,CAAuB;AAAA,EACrC,QAAA;AAAA,EACA;AACF,CAAA,EAA0C;AACxC,EAAA,MAAM,MAAA,GAAS,WAAA,IAAe,IAAI,WAAA,CAAY;AAAA,IAC5C,cAAA,EAAgB;AAAA,MACd,OAAA,EAAS;AAAA,QACP,KAAA,EAAO,KAAA;AAAA,QACP,MAAA,EAAQ;AAAA;AACV;AACF,GACD,CAAA;AAED,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,iBAAA,EAAkB;AAClB,IAAA,OAAO,MAAM;AAEX,MAAA,mBAAA,CAAoB,IAAI,CAAA;AACxB,MAAA,YAAA,GAAe,IAAA;AAAA,IACjB,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,KAAA,CAAM,aAAA;AAAA,IACX,mBAAA;AAAA,IACA,EAAE,MAAA,EAAO;AAAA,IACT;AAAA,GACF;AACF;AAWA,eAAsB,SAAA,CACpB,UACA,IAAA,EACe;AACf,EAAA,iBAAA,EAAkB;AAClB,EAAA,MAAM,GAAA,GAAM,YAAY,QAAQ,CAAA;AAChC,EAAA,MAAM,UAAA,CAAW,KAAK,IAAI,CAAA;AAC5B;AAQA,eAAsB,cAAA,GAAgC;AACpD,EAAA,MAAM,iBAAA,GAAoB,QAAA,EAAS;AACrC;AAWO,SAAS,gBAAA,CAAiB,KAAK,EAAA,EAAmB;AACvD,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AACzD;AAWO,SAAS,WAAA,CAAe,IAAA,EAAS,OAAA,GAAU,CAAA,EAAqB;AACrE,EAAA,OAAO,MACL,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY,UAAA,CAAW,MAAM,OAAA,CAAQ,IAAI,CAAA,EAAG,OAAO,CAAC,CAAA;AACrE;AASO,SAAS,WAAA,CAAY,KAAA,EAAc,OAAA,GAAU,CAAA,EAAyB;AAC3E,EAAA,OAAO,MACL,IAAI,OAAA,CAAQ,CAAC,CAAA,EAAG,MAAA,KAAW,UAAA,CAAW,MAAM,MAAA,CAAO,KAAK,CAAA,EAAG,OAAO,CAAC,CAAA;AACvE","file":"testing.mjs","sourcesContent":["/**\n * src/testing.ts\n *\n * Test utilities for SmartQuery.\n *\n * Provides:\n * • SmartQueryTestProvider — wraps components with all required context\n * • createMockStorage — in-memory AsyncStorage (no MMKV, no IDB)\n * • seedCache — pre-populate cache before rendering\n * • clearTestCache — reset between tests\n * • waitForCacheLoad — await the initial async cache read\n *\n * Works with Jest, Vitest, and React Native Testing Library.\n *\n * @example\n * import { render } from \"@testing-library/react-native\";\n * import { SmartQueryTestProvider, seedCache } from \"react-smart-query/testing\";\n *\n * beforeEach(() => seedCache([\"expenses\", \"trip_1\"], mockExpenses));\n *\n * it(\"renders cached expenses\", async () => {\n * const { findAllByTestId } = render(\n * <SmartQueryTestProvider>\n * <ExpenseList tripId=\"trip_1\" />\n * </SmartQueryTestProvider>\n * );\n * const rows = await findAllByTestId(\"expense-row\");\n * expect(rows).toHaveLength(mockExpenses.length);\n * });\n */\n\nimport React, { ReactNode, useEffect } from \"react\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { _setStorageOverride } from \"./services/storage.adapter\";\nimport { writeCache, cacheKeyFor } from \"./services/cache.service\";\nimport type { AsyncStorage, AnyItem } from \"./types\";\n\n// ─── In-memory storage ────────────────────────────────────────────────────────\n\nexport function createMockStorage(): AsyncStorage {\n const store = new Map<string, string>();\n return {\n get: (key) => Promise.resolve(store.get(key)),\n set: (key, value) => { store.set(key, value); return Promise.resolve(); },\n delete: (key) => { store.delete(key); return Promise.resolve(); },\n clearAll: () => { store.clear(); return Promise.resolve(); },\n keys: () => Promise.resolve(Array.from(store.keys())),\n };\n}\n\n// Module-level mock storage used by all test utilities\nlet _mockStorage: AsyncStorage | null = null;\n\nfunction ensureMockStorage(): AsyncStorage {\n if (!_mockStorage) {\n _mockStorage = createMockStorage();\n _setStorageOverride(_mockStorage);\n }\n return _mockStorage;\n}\n\n// ─── SmartQueryTestProvider ───────────────────────────────────────────────────\n\ninterface TestProviderProps {\n children: ReactNode;\n /** Override the QueryClient (e.g. to set custom defaults) */\n queryClient?: QueryClient;\n}\n\n/**\n * Wrap your component tree in tests.\n * Automatically uses in-memory storage — no MMKV, no IndexedDB required.\n */\nexport function SmartQueryTestProvider({\n children,\n queryClient,\n}: TestProviderProps): React.ReactElement {\n const client = queryClient ?? new QueryClient({\n defaultOptions: {\n queries: {\n retry: false,\n gcTime: Infinity,\n },\n },\n });\n\n useEffect(() => {\n ensureMockStorage();\n return () => {\n // Clean up override after the test\n _setStorageOverride(null);\n _mockStorage = null;\n };\n }, []);\n\n return React.createElement(\n QueryClientProvider,\n { client },\n children\n );\n}\n\n// ─── Cache seeding ────────────────────────────────────────────────────────────\n\n/**\n * Pre-populate the cache before rendering a component.\n * Must be called before rendering (not inside a component).\n *\n * @example\n * beforeEach(() => seedCache([\"expenses\", \"trip_1\"], mockExpenses));\n */\nexport async function seedCache<T>(\n queryKey: readonly unknown[],\n data: T\n): Promise<void> {\n ensureMockStorage();\n const key = cacheKeyFor(queryKey);\n await writeCache(key, data);\n}\n\n/**\n * Clear all test cache entries between tests.\n *\n * @example\n * afterEach(() => clearTestCache());\n */\nexport async function clearTestCache(): Promise<void> {\n await ensureMockStorage().clearAll();\n}\n\n/**\n * Wait for the initial cache read to complete.\n * Useful when you need to assert on cached data immediately after render.\n *\n * @example\n * const { getByText } = render(<MyComponent />);\n * await waitForCacheLoad();\n * expect(getByText(\"Expense 1\")).toBeTruthy();\n */\nexport function waitForCacheLoad(ms = 50): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ─── Mock query factory ───────────────────────────────────────────────────────\n\n/**\n * Create a mock queryFn that resolves with the provided data.\n * Use to test loading → success transitions.\n *\n * @example\n * queryFn: mockQueryFn(mockExpenses, 100) // resolves after 100ms\n */\nexport function mockQueryFn<T>(data: T, delayMs = 0): () => Promise<T> {\n return () =>\n new Promise((resolve) => setTimeout(() => resolve(data), delayMs));\n}\n\n/**\n * Create a mock queryFn that rejects with the provided error.\n * Use to test error states.\n *\n * @example\n * queryFn: mockErrorFn(new Error(\"Network error\"))\n */\nexport function mockErrorFn(error: Error, delayMs = 0): () => Promise<never> {\n return () =>\n new Promise((_, reject) => setTimeout(() => reject(error), delayMs));\n}\n"]}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * src/types.ts
3
+ *
4
+ * Single source of truth for all shared public types.
5
+ * No implementation logic — pure TypeScript interfaces and type aliases.
6
+ */
7
+ /**
8
+ * Minimum shape required for list items.
9
+ * The actual id field name is configured via `getItemId` — this type
10
+ * only enforces that items are plain objects.
11
+ */
12
+ type AnyItem = Record<string, unknown>;
13
+ /**
14
+ * Extracts the id of an item. Configurable so APIs using `_id`, `uuid`,
15
+ * numeric ids, or composite keys all work without data transformation.
16
+ *
17
+ * @example
18
+ * getItemId: (item) => item.id // "id: string"
19
+ * getItemId: (item) => String(item._id) // MongoDB ObjectId
20
+ * getItemId: (item) => String(item.id) // numeric id
21
+ */
22
+ type GetItemId<T extends AnyItem> = (item: T) => string;
23
+ /**
24
+ * Comparator for sort order. Same contract as Array.sort comparator.
25
+ * Return negative if a < b, positive if a > b, 0 if equal.
26
+ */
27
+ type SortComparator<T extends AnyItem> = (a: T, b: T) => number;
28
+ /**
29
+ * Extracts a version sticker (timestamp or counter) from an item.
30
+ * Used to prevent stale updates from overwriting fresher data.
31
+ */
32
+ type GetItemVersion<T extends AnyItem> = (item: T) => number | string;
33
+ interface CacheEntry<T> {
34
+ readonly version: number;
35
+ readonly data: T;
36
+ readonly cachedAt: number;
37
+ readonly lastAccessedAt: number;
38
+ }
39
+ interface NormalizedList<T extends AnyItem> {
40
+ byId: Record<string, T>;
41
+ allIds: string[];
42
+ }
43
+ type PaginationMode = "normalized" | "pages";
44
+ interface UnifiedNormalizedInfiniteData<T extends AnyItem> {
45
+ data: NormalizedList<T>;
46
+ meta: {
47
+ nextCursor: unknown | null;
48
+ pageParams: unknown[];
49
+ lastFetchedAt?: number;
50
+ };
51
+ }
52
+ /** Legacy support - will be used internally or for migration if needed */
53
+ interface InfinitePagedData<T extends AnyItem> {
54
+ pages: NormalizedList<T>[];
55
+ pageParams: unknown[];
56
+ nextCursor: unknown | null;
57
+ }
58
+ type ObservabilityEvent = {
59
+ type: "cache_hit";
60
+ queryKey: readonly unknown[];
61
+ cachedAt: number;
62
+ } | {
63
+ type: "cache_miss";
64
+ queryKey: readonly unknown[];
65
+ } | {
66
+ type: "cache_write";
67
+ queryKey: readonly unknown[];
68
+ dataSize: number;
69
+ } | {
70
+ type: "fetch_start";
71
+ queryKey: readonly unknown[];
72
+ } | {
73
+ type: "fetch_success";
74
+ queryKey: readonly unknown[];
75
+ durationMs: number;
76
+ } | {
77
+ type: "fetch_error";
78
+ queryKey: readonly unknown[];
79
+ error: unknown;
80
+ } | {
81
+ type: "queue_enqueue";
82
+ mutationId: string;
83
+ mutationType: string;
84
+ } | {
85
+ type: "queue_success";
86
+ mutationId: string;
87
+ } | {
88
+ type: "queue_failure";
89
+ mutationId: string;
90
+ retryCount: number;
91
+ } | {
92
+ type: "queue_drained";
93
+ } | {
94
+ type: "sync_conflict";
95
+ queryKey: readonly unknown[];
96
+ localVersion: unknown;
97
+ serverVersion: unknown;
98
+ } | {
99
+ type: "storage_quota_exceeded";
100
+ key: string;
101
+ };
102
+ type ObserverFn = (event: ObservabilityEvent) => void;
103
+ type MutationType = "ADD_ITEM" | "UPDATE_ITEM" | "REMOVE_ITEM" | "CUSTOM";
104
+ interface QueuedMutation<TPayload = unknown> {
105
+ id: string;
106
+ type: MutationType | string;
107
+ /** Logical entity key for coalescing — e.g. "expense:exp_123" */
108
+ entityKey?: string;
109
+ queryKey: readonly unknown[];
110
+ payload: TPayload;
111
+ enqueuedAt: number;
112
+ retryCount: number;
113
+ maxRetries: number;
114
+ nextRetryAt: number;
115
+ }
116
+ interface SmartQueryError {
117
+ /** Original error from the API / network */
118
+ cause: unknown;
119
+ /** Human-readable message */
120
+ message: string;
121
+ /** Whether this error is retryable */
122
+ retryable: boolean;
123
+ /** HTTP status code if available */
124
+ statusCode?: number;
125
+ }
126
+ interface AsyncStorage {
127
+ get(key: string): Promise<string | undefined>;
128
+ set(key: string, value: string): Promise<void>;
129
+ delete(key: string): Promise<void>;
130
+ clearAll(): Promise<void>;
131
+ keys(): Promise<string[]>;
132
+ }
133
+
134
+ export type { AsyncStorage as A, CacheEntry as C, GetItemId as G, InfinitePagedData as I, MutationType as M, NormalizedList as N, ObserverFn as O, PaginationMode as P, QueuedMutation as Q, SortComparator as S, UnifiedNormalizedInfiniteData as U, AnyItem as a, GetItemVersion as b, SmartQueryError as c, ObservabilityEvent as d };