Skip to content

Commit d60d792

Browse files
authored
Merge pull request #4 from thinkgrid-labs/dev
remove dependencies
2 parents 23709d2 + 7572a67 commit d60d792

9 files changed

Lines changed: 163 additions & 42 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ jobs:
2525
- name: Build Library
2626
run: npm run build
2727

28+
- name: Lint Library
29+
run: npm run lint
30+
2831
- name: Run Library Tests
2932
run: npm test
3033

README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
# @thinkgrid/react-local-fetch
22

3-
A lightweight, resilient, and secure data fetching layer for React and Next.js. It implements a **Hydrate-and-Sync (SWR)** pattern using **IndexedDB** for persistence and the **Web Crypto API** for optional hardware-accelerated encryption.
3+
[![CI](https://github.com/thinkgrid-labs/react-local-fetch/actions/workflows/ci.yml/badge.svg)](https://github.com/thinkgrid-labs/react-local-fetch/actions/workflows/ci.yml)
4+
[![npm version](https://img.shields.io/npm/v/@thinkgrid/react-local-fetch.svg)](https://www.npmjs.com/package/@thinkgrid/react-local-fetch)
5+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
46

5-
## 🚀 Key Features
7+
**Ultra-lightweight, resilient, and secure data fetching for React and Next.js.**
68

7-
- **Instant UI**: Returns cached data immediately while syncing with the API in the background.
8-
- **Hardware Encryption**: Optional AES-GCM 256-bit encryption for sensitive local data.
9-
- **Version-based Invalidation**: Force-clear stale cache when your data schema changes.
10-
- **Next.js Optimized**: Automatically bypasses cache on the server to prevent hydration mismatches and build errors.
11-
- **Resilient**: Gracefully falls back to stale data if the network is down or the API returns an error.
12-
- **Tiny**: < 5KB bundled with zero external dependencies (aside from `idb-keyval`).
9+
`react-local-fetch` is a **zero-dependency** library that implements a powerful **Hydrate-and-Sync (SWR)** pattern. It uses **native IndexedDB** for persistent local caching and the **Web Crypto API** for optional, high-performance AES-GCM 256-bit encryption. Perfect for building **offline-first**, **local-first**, and privacy-conscious web applications.
10+
11+
## ✨ Why react-local-fetch?
12+
13+
- **⚡ Zero Latency**: Return cached data instantly while refreshing from your API in the background.
14+
- **🛡️ Secure by Design**: Optional hardware-accelerated encryption for sensitive local data.
15+
- **📦 Zero Dependencies**: Pure ESM/CJS build using only browser native APIs.
16+
- **🔄 Smart Invalidation**: Version-based cache busting for schema changes and deployments.
17+
- **🌐 Next.js & SSR Ready**: Automatically bypasses client-side storage during server rendering.
18+
- **🔌 Resilient**: Gracefully returns stale data if the network is down or the API fails.
19+
- **🚀 Tiny Footprint**: < 4KB gzipped.
1320

1421
## 📦 Installation
1522

examples/vite-example/package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/vite-example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"dependencies": {
1313
"react": "^19.0.0",
1414
"react-dom": "^19.0.0",
15-
"react-local-fetch": "file:../../",
15+
"@thinkgrid/react-local-fetch": "file:../../",
1616
"@tanstack/react-query": "^5.0.0"
1717
},
1818
"devDependencies": {

examples/vite-example/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useLocalFetch, localFetch } from 'react-local-fetch'
1+
import { useLocalFetch, localFetch } from '@thinkgrid/react-local-fetch'
22
import { useQuery } from '@tanstack/react-query'
33
import './App.css'
44

package-lock.json

Lines changed: 2 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@thinkgrid/react-local-fetch",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "Resilient, encrypted, local-first fetching for React and Next.js using IndexedDB.",
55
"main": "dist/index.js",
66
"module": "dist/index.mjs",
@@ -16,13 +16,27 @@
1616
"test": "vitest",
1717
"lint": "tsc --noEmit"
1818
},
19+
"keywords": [
20+
"react",
21+
"nextjs",
22+
"swr",
23+
"indexeddb",
24+
"local-first",
25+
"encryption",
26+
"aes-gcm",
27+
"data-fetching",
28+
"caching",
29+
"offline-first",
30+
"react-hooks",
31+
"crypto",
32+
"secure-storage",
33+
"thinkgrid"
34+
],
1935
"peerDependencies": {
2036
"react": ">=16.8",
2137
"react-dom": ">=16.8"
2238
},
23-
"dependencies": {
24-
"idb-keyval": "^6.2.2"
25-
},
39+
"dependencies": {},
2640
"devDependencies": {
2741
"@testing-library/jest-dom": "^6.9.1",
2842
"@testing-library/react": "^16.3.2",

src/storage-error.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import 'fake-indexeddb/auto';
3+
import { saveToStorage, getFromStorage } from './storage';
4+
5+
describe('storage advanced error handling', () => {
6+
const testKey = 'error-test';
7+
const testEntry = {
8+
data: { msg: 'test' },
9+
metadata: { timestamp: Date.now(), version: 1, isEncrypted: false },
10+
};
11+
12+
beforeEach(() => {
13+
vi.clearAllMocks();
14+
});
15+
16+
it('should handle IndexedDB open errors gracefully', async () => {
17+
// Mock indexedDB.open to fail
18+
const originalOpen = indexedDB.open;
19+
indexedDB.open = vi.fn().mockImplementation(() => {
20+
const request = {
21+
onerror: null as any,
22+
onsuccess: null as any,
23+
result: null,
24+
error: new Error('Failed to open database'),
25+
};
26+
setTimeout(() => {
27+
if (request.onerror) request.onerror();
28+
}, 0);
29+
return request;
30+
});
31+
32+
// In storage.ts, getDB() rejects on onerror
33+
await expect(saveToStorage(testKey, testEntry)).rejects.toThrow('Failed to open database');
34+
35+
// Restore
36+
indexedDB.open = originalOpen;
37+
});
38+
39+
it('should handle transaction errors gracefully', async () => {
40+
// 1. Open the DB normally first
41+
const db = await new Promise<IDBDatabase>((resolve, reject) => {
42+
const request = indexedDB.open('react-local-fetch', 1);
43+
request.onsuccess = () => resolve(request.result);
44+
request.onerror = () => reject(request.error);
45+
});
46+
47+
const originalTransaction = IDBDatabase.prototype.transaction;
48+
IDBDatabase.prototype.transaction = vi.fn().mockImplementation(() => {
49+
throw new Error('Transaction failed');
50+
});
51+
52+
await expect(getFromStorage(testKey)).rejects.toThrow('Transaction failed');
53+
54+
// Restore
55+
IDBDatabase.prototype.transaction = originalTransaction;
56+
db.close();
57+
});
58+
});

src/storage.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,84 @@
1-
import { get, set, del, clear } from 'idb-keyval';
21
import { CacheEntry } from './types';
32

3+
const DB_NAME = 'react-local-fetch';
4+
const STORE_NAME = 'keyval';
5+
const DB_VERSION = 1;
6+
7+
/**
8+
* Promise wrapper for IndexedDB.
9+
*/
10+
function getDB(): Promise<IDBDatabase> {
11+
return new Promise((resolve, reject) => {
12+
const request = indexedDB.open(DB_NAME, DB_VERSION);
13+
14+
request.onupgradeneeded = () => {
15+
const db = request.result;
16+
if (!db.objectStoreNames.contains(STORE_NAME)) {
17+
db.createObjectStore(STORE_NAME);
18+
}
19+
};
20+
21+
request.onsuccess = () => resolve(request.result);
22+
request.onerror = () => reject(request.error);
23+
});
24+
}
25+
426
/**
527
* Saves data to IndexedDB.
628
*/
729
export async function saveToStorage<T>(key: string, entry: CacheEntry<T>): Promise<void> {
830
if (typeof window === 'undefined') return;
9-
await set(`rlf_${key}`, entry);
31+
const db = await getDB();
32+
return new Promise((resolve, reject) => {
33+
const transaction = db.transaction(STORE_NAME, 'readwrite');
34+
const store = transaction.objectStore(STORE_NAME);
35+
const request = store.put(entry, `rlf_${key}`);
36+
request.onsuccess = () => resolve();
37+
request.onerror = () => reject(request.error);
38+
});
1039
}
1140

1241
/**
1342
* Retrieves data from IndexedDB.
1443
*/
1544
export async function getFromStorage<T>(key: string): Promise<CacheEntry<T> | undefined> {
1645
if (typeof window === 'undefined') return undefined;
17-
return await get(`rlf_${key}`);
46+
const db = await getDB();
47+
return new Promise((resolve, reject) => {
48+
const transaction = db.transaction(STORE_NAME, 'readonly');
49+
const store = transaction.objectStore(STORE_NAME);
50+
const request = store.get(`rlf_${key}`);
51+
request.onsuccess = () => resolve(request.result);
52+
request.onerror = () => reject(request.error);
53+
});
1854
}
1955

2056
/**
2157
* Deletes a specific key from IndexedDB.
2258
*/
2359
export async function removeFromStorage(key: string): Promise<void> {
2460
if (typeof window === 'undefined') return;
25-
await del(`rlf_${key}`);
61+
const db = await getDB();
62+
return new Promise((resolve, reject) => {
63+
const transaction = db.transaction(STORE_NAME, 'readwrite');
64+
const store = transaction.objectStore(STORE_NAME);
65+
const request = store.delete(`rlf_${key}`);
66+
request.onsuccess = () => resolve();
67+
request.onerror = () => reject(request.error);
68+
});
2669
}
2770

2871
/**
2972
* Clears all react-local-fetch data from IndexedDB.
3073
*/
3174
export async function clearAllStorage(): Promise<void> {
3275
if (typeof window === 'undefined') return;
33-
// This is a bit aggressive, but we could filter keys if needed.
34-
// For now, let's use a simple clear or filtered clear.
35-
await clear();
76+
const db = await getDB();
77+
return new Promise((resolve, reject) => {
78+
const transaction = db.transaction(STORE_NAME, 'readwrite');
79+
const store = transaction.objectStore(STORE_NAME);
80+
const request = store.clear();
81+
request.onsuccess = () => resolve();
82+
request.onerror = () => reject(request.error);
83+
});
3684
}

0 commit comments

Comments
 (0)