|
| 1 | +# ngx-fetcher-with-etag |
| 2 | + |
| 3 | +**A smart, ETag-powered data fetching library for modern Angular applications.** |
| 4 | + |
| 5 | +Stop re-fetching unchanged data. `ngx-fetcher-with-etag` simplifies your data layer with automatic caching, smart polling, and utilities for handling local data. |
| 6 | + |
| 7 | +----- |
| 8 | + |
| 9 | +## 🤔 Why ngx-fetcher-with-etag? |
| 10 | + |
| 11 | +Modern web apps often make redundant API calls, fetching the same data repeatedly. This library solves that problem by leveraging browser caching and HTTP ETag headers. The server tells you when data *hasn't* changed, saving bandwidth and making your app feel faster. |
| 12 | + |
| 13 | +This library automates that entire process and packages it into a clean, reactive, RxJS-powered API. |
| 14 | + |
| 15 | +----- |
| 16 | + |
| 17 | +## ✨ Features |
| 18 | + |
| 19 | + - ⚡ **ETag**: Seamlessly uses `ETag` / `If-None-Match` headers to avoid re-downloading unchanged data. |
| 20 | + - 🔄 **Polling**: Automatically refreshes data based on server `Expires` headers or custom intervals. |
| 21 | + - 🔍 **Local Lookups**: A service for fetching a list and then accessing individual items synchronously by ID. |
| 22 | + - 🛡️ **Extensible Authentication**: A simple `AuthProvider` system to inject authentication headers into HTTP requests. |
| 23 | + - 👁️ **Fully Reactive**: Built from the ground up with RxJS Observables. |
| 24 | + |
| 25 | +----- |
| 26 | + |
| 27 | +## 🚀 Installation |
| 28 | + |
| 29 | +```bash |
| 30 | +npm install ngx-fetcher-with-etag |
| 31 | +``` |
| 32 | + |
| 33 | +----- |
| 34 | + |
| 35 | +## Core Concepts |
| 36 | + |
| 37 | +The library is built around a few key components that work together: |
| 38 | + |
| 39 | +1. **`FetcherWithEtag`**: The core engine. It handles a single API endpoint, managing ETag caching and polling. |
| 40 | +3. **`LocalDataLoaderEtagService`**: The most advanced utility. It uses `FetcherWithEtag` to fetch an array of items and then indexes them for instant, synchronous access by ID. |
| 41 | +4. **`AuthProvider`**: A simple class you extend to provide authentication headers for your HTTP requests. |
| 42 | + |
| 43 | +----- |
| 44 | + |
| 45 | +## 📖 API and Usage |
| 46 | + |
| 47 | +### FetcherWithEtag |
| 48 | + |
| 49 | +This is the main class for fetching data from a single endpoint. It handles all the ETag, caching, and polling logic for you. |
| 50 | + |
| 51 | +#### Basic Usage |
| 52 | + |
| 53 | +```typescript |
| 54 | +import { FetcherWithEtag } from 'ngx-fetcher-with-etag'; |
| 55 | + |
| 56 | +// Create a new fetcher instance |
| 57 | +const fetcher = new FetcherWithEtag( |
| 58 | + httpClient, |
| 59 | + authProvider, |
| 60 | + 'https://api.example.com/data', |
| 61 | + (raw: RawData) => new Data(raw), // Converter function |
| 62 | + (old: Data, new: Data) => old.id === new.id // Equality check |
| 63 | +); |
| 64 | + |
| 65 | +// Subscribe to data updates (only emits when data has actually changed) |
| 66 | +fetcher.data$.subscribe(data => console.log('Data updated:', data)); |
| 67 | + |
| 68 | +// Subscribe to the loading state |
| 69 | +fetcher.loading$.subscribe(loading => console.log('Is loading:', loading)); |
| 70 | +``` |
| 71 | + |
| 72 | +#### Constructor Parameters Explained |
| 73 | + |
| 74 | +**Converter Function** `(raw: RawT) => T` |
| 75 | + |
| 76 | +This function's job is to transform the raw, plain JSON response from your API into a new type or interface that your application can use. This is the perfect place to map fields, add computed properties, or parse dates. |
| 77 | + |
| 78 | +```typescript |
| 79 | +// Example: Convert a raw API user into a clean User model |
| 80 | +type RawUser = { user_id: string; full_name: string; }; |
| 81 | +type User = { |
| 82 | + id: string; |
| 83 | + name: string; |
| 84 | + initial: string; |
| 85 | +}; |
| 86 | + |
| 87 | +function userConverter(raw: RawUser): User { |
| 88 | + return { |
| 89 | + id: raw.user_id, |
| 90 | + name: raw.full_name, |
| 91 | + initial: raw.full_name.charAt(0) |
| 92 | + }; |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +**Equality Check Function** `(old: T, new: T) => boolean` |
| 97 | + |
| 98 | +This function prevents your `data$` observable from emitting a new value if the data is effectively the same. This is crucial for performance and preventing unnecessary re-renders in your UI. You can define what "equal" means—whether it's a simple ID check or a deep object comparison. |
| 99 | + |
| 100 | +```typescript |
| 101 | +// Only emit if the user's name has changed |
| 102 | +function userEqualityCheck(old: User, new: User): boolean { |
| 103 | + return old.name === new.name; |
| 104 | +} |
| 105 | + |
| 106 | +// Only emit if something has changed |
| 107 | +type ItemWithEtag = { |
| 108 | + id: string; |
| 109 | + etag: string; |
| 110 | +}; |
| 111 | + |
| 112 | +function userEtagCheck(old: ItemWithEtag, new: ItemWithEtag): boolean { |
| 113 | + return old.etag === new.etag; |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +----- |
| 118 | + |
| 119 | +### LocalDataLoaderEtagService |
| 120 | + |
| 121 | +This is a powerful abstract service for a common pattern: fetching a list of items and then needing to look up individual items from that list by their ID. It combines the caching of `FetcherWithEtag` with an in-memory, indexed store for O(1) lookups. |
| 122 | + |
| 123 | +#### When to use it |
| 124 | + |
| 125 | +Use this when you have a `users` or `products` endpoint and your app frequently needs to ask, "What's the name of the user with ID `user-123`?" without making another API call. |
| 126 | + |
| 127 | +#### Example Implementation |
| 128 | + |
| 129 | +```typescript |
| 130 | +import { LocalDataLoaderEtagService } from 'ngx-fetcher-with-etag'; |
| 131 | + |
| 132 | +// 1. Define your data structures |
| 133 | +type User = { id: string; name: string; email: string; }; |
| 134 | +type RawUsersResponse = { |
| 135 | + etag: string; // ETag is required for comparison |
| 136 | + items: User[]; |
| 137 | +}; |
| 138 | +type ProcessedUsersData = RawUsersResponse & { |
| 139 | + // This will be added automatically: a map for fast lookups |
| 140 | + byId: Record<string, User>; |
| 141 | +}; |
| 142 | + |
| 143 | +// 2. Create a service that extends the base class |
| 144 | +@Injectable({ providedIn: 'root' }) |
| 145 | +export class UsersService extends LocalDataLoaderEtagService< |
| 146 | + 'items', // The key in the response containing the array |
| 147 | + 'id', // The key on each item to use as its ID |
| 148 | + User, // The type of a single item |
| 149 | + ProcessedUsersData, // The final processed data structure (with `byId`) |
| 150 | + RawUsersResponse // The raw API response |
| 151 | +> { |
| 152 | + constructor(http: HttpClient, auth: AuthProvider) { |
| 153 | + super( |
| 154 | + // Provide a function that creates the underlying FetcherWithEtag |
| 155 | + (converter, isEqual) => new FetcherWithEtag( |
| 156 | + http, |
| 157 | + auth, |
| 158 | + 'https://api.example.com/users', |
| 159 | + converter, |
| 160 | + isEqual |
| 161 | + ), |
| 162 | + 'id', // Tell it the ID field is named 'id' |
| 163 | + 'items' // Tell it the array is in the 'items' field |
| 164 | + ); |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +// 3. Use the service in your component |
| 169 | +constructor(private usersService: UsersService) {} |
| 170 | + |
| 171 | +this.usersService.dataLoader$.subscribe((dataLoader) => { |
| 172 | + // dataLoader is an object with a `load` method for synchronous access |
| 173 | + const user = dataLoader.load('user-123'); // Instant local lookup! |
| 174 | + if (user) { |
| 175 | + console.log('User found locally:', user.name); |
| 176 | + } |
| 177 | +}); |
| 178 | +``` |
| 179 | + |
| 180 | +----- |
| 181 | + |
| 182 | +### Authentication |
| 183 | + |
| 184 | +Provide authentication headers to your requests by creating a simple `AuthProvider`. |
| 185 | + |
| 186 | +#### Example: JWT Authentication |
| 187 | + |
| 188 | +```typescript |
| 189 | +import { AuthProvider } from 'ngx-fetcher-with-etag'; |
| 190 | +import { HttpHeaders } from '@angular/common/http'; |
| 191 | + |
| 192 | +@Injectable({ providedIn: 'root' }) |
| 193 | +export class JwtAuthProvider extends AuthProvider { |
| 194 | + override getAuthHeaders(): HttpHeaders { |
| 195 | + const token = localStorage.getItem('jwt_token'); |
| 196 | + if (token) { |
| 197 | + return new HttpHeaders({ |
| 198 | + Authorization: `Bearer ${token}` |
| 199 | + }); |
| 200 | + } |
| 201 | + return new HttpHeaders(); |
| 202 | + } |
| 203 | +} |
| 204 | +``` |
0 commit comments