karna_network 0.1.0
karna_network: ^0.1.0 copied to clipboard
A stale-while-revalidate (SWR) caching layer for Dio: serves cached GET responses instantly, revalidates in the background, dedupes concurrent requests, and falls back to stale data on network failure.
import 'package:dio/dio.dart';
import 'package:karna_network/karna_network.dart';
late final Dio dio;
late final SwrInterceptor swr;
late final OfflineMutationQueue offlineQueue;
/// Stub — wire this to your real auth/session storage.
class MyTokenProvider implements TokenProvider {
String? _accessToken = 'initial-token';
@override
Future<String?> getAccessToken() async => _accessToken;
@override
Future<String?> refreshAccessToken() async {
// call your refresh endpoint here; return the new token or null.
_accessToken = 'refreshed-token';
return _accessToken;
}
}
void setup() {
final circuitBreaker = CircuitBreaker(
failureThreshold: 3,
cooldown: const Duration(seconds: 30),
);
final auth = AuthInterceptor(
dioProvider: () => dio,
tokenProvider: MyTokenProvider(),
);
final retry = RetryInterceptor(
dioProvider: () => dio,
circuitBreaker: circuitBreaker,
maxRetries: 2,
);
swr = SwrInterceptor(
dioProvider: () => dio,
store: MemoryCacheStore(maxEntries: 300),
defaultOptions: const SwrOptions(
freshDuration: Duration(seconds: 30),
staleDuration: Duration(minutes: 5),
),
);
final errors = ErrorNormalizingInterceptor();
dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'))
..interceptors.addAll([
auth, // 1. attach token, refresh on 401
retry, // 2. retry/backoff + circuit breaker on real network attempts
swr, // 3. serve/refresh cache
errors, // 4. last: normalize whatever exception bubbles out
]);
offlineQueue = OfflineMutationQueue(
dio: dio,
store: MemoryMutationQueueStore(),
isOnline: () async => true, // replace with connectivity_plus or similar
);
swr.events.listen((event) {
switch (event) {
case SwrRevalidating():
break; // show a subtle "refreshing" indicator for event.key
case SwrRevalidated():
break; // event.data is the fresh payload
case SwrRevalidationFailed():
break; // silent by design; log if you want visibility
}
});
}
Future<void> fetchProfile() async {
try {
final res = await dio.get<dynamic>('/me');
print('fromCache=${res.extra['fromCache']} data=${res.data}');
} on DioException catch (e) {
final appError = e.error as AppError; // normalized regardless of upstream
print('failed: ${appError.kind} ${appError.message}');
}
}
Future<void> mutateThenInvalidate() async {
try {
await offlineQueue.send('POST', '/todos', data: {'title': 'write docs'});
} on MutationQueuedException {
print('offline — queued, will sync later');
}
final key = defaultCacheKeyBuilder(
RequestOptions(baseUrl: dio.options.baseUrl, path: '/todos'),
);
await swr.invalidate(key);
}
/// Batching example: several widgets each ask for one user by id within
/// the same frame; only fires one request to a batch endpoint you already
/// have on the backend.
final BatchCollector<Map<String, dynamic>> userBatcher = BatchCollector(
batchFetcher: (ids) async {
final res = await dio
.get<dynamic>('/users', queryParameters: {'ids': ids.join(',')});
final list = (res.data as List).cast<Map<String, dynamic>>();
return {for (final u in list) u['id'] as String: u};
},
);
Future<Map<String, dynamic>> fetchUser(String id) => userBatcher.load(id);
Future<void> aggregatedDashboard() async {
final aggregator = RequestAggregator(dio);
final result = await aggregator.fetchAll({
'profile': '/me',
'notifications': '/notifications',
'billing': '/billing/summary',
});
print(result['profile']?.data);
}