Syncache
An offline-first cache and sync engine for Dart applications.
Syncache provides a unified API for caching data with multiple strategies, reactive updates via streams, and optimistic mutations with automatic background sync.
Installation
dependencies:
syncache: ^0.1.0
Quick Start
import 'package:syncache/syncache.dart';
// Create a cache instance
final cache = Syncache<User>(
store: MemoryStore<User>(),
);
// Fetch data with offline-first policy
final user = await cache.get(
key: 'user:123',
fetch: (request) => api.getUser(123),
);
// Watch for reactive updates
cache.watch(
key: 'user:123',
fetch: (request) => api.getUser(123),
).listen((user) {
print('User updated: ${user.name}');
});
Features
Caching Policies
Control how data is fetched and cached with five built-in policies:
| Policy | Behavior |
|---|---|
offlineFirst |
Returns cached data if valid; fetches if expired or missing (default) |
cacheOnly |
Returns only cached data; never makes network requests |
networkOnly |
Always fetches from network; ignores cache |
refresh |
Fetches from network if online; falls back to cache if offline |
staleWhileRefresh |
Returns cache immediately; refreshes in background if expired |
final user = await cache.get(
key: 'user:123',
fetch: fetchUser,
policy: Policy.staleWhileRefresh,
ttl: Duration(minutes: 5),
);
Reactive Streams
Subscribe to cache updates with watch():
cache.watch(key: 'user:123', fetch: fetchUser).listen((user) {
// Called on initial fetch and every subsequent update
});
// With metadata (cache status, staleness, age)
cache.watchWithMeta(key: 'user:123', fetch: fetchUser).listen((result) {
print('From cache: ${result.meta.isFromCache}');
print('Is stale: ${result.meta.isStale}');
});
Optimistic Mutations
Update the UI immediately while syncing to the server in the background:
await cache.mutate(
key: 'user:123',
mutation: Mutation<User>(
apply: (user) => user.copyWith(name: 'New Name'),
send: (user) => api.updateUser(user),
),
);
Failed mutations are automatically queued and retried when the network becomes available.
Tag-Based Invalidation
Group related cache entries with tags for bulk invalidation:
// Store with tags
await cache.get(
key: 'user:123',
fetch: fetchUser,
tags: ['users', 'account:456'],
);
// Invalidate all entries with a tag
await cache.invalidateTag('users');
// Invalidate by pattern
await cache.invalidatePattern('user:*');
Request Deduplication
Concurrent requests for the same key are automatically deduplicated:
// These three calls result in only one network request
final results = await Future.wait([
cache.get(key: 'user:123', fetch: fetchUser),
cache.get(key: 'user:123', fetch: fetchUser),
cache.get(key: 'user:123', fetch: fetchUser),
]);
Retry with Exponential Backoff
Configure automatic retries for transient failures:
final user = await cache.get(
key: 'user:123',
fetch: fetchUser,
retry: RetryConfig(
maxAttempts: 3,
delay: Duration(seconds: 1),
multiplier: 2.0,
retryIf: (error) => error is SocketException,
),
);
Cancellation
Cancel in-flight requests with cancellation tokens:
final token = CancellationToken();
// Start fetch
final future = cache.get(
key: 'user:123',
fetch: fetchUser,
cancel: token,
);
// Cancel if needed
token.cancel();
Scoped Caches
Isolate cache entries with key prefixes for multi-tenant scenarios:
final tenantCache = cache.scoped('tenant:abc');
// All operations are prefixed with 'tenant:abc:'
await tenantCache.get(key: 'user:123', fetch: fetchUser);
// Actually stored as 'tenant:abc:user:123'
Prefetching
Prefetch multiple items in parallel or with dependency ordering:
// Parallel prefetch
final results = await cache.prefetch([
PrefetchRequest(key: 'user:1', fetch: fetchUser1),
PrefetchRequest(key: 'user:2', fetch: fetchUser2),
]);
// Dependency graph prefetch
final results = await cache.prefetchGraph([
PrefetchNode(key: 'config', fetch: fetchConfig),
PrefetchNode(key: 'user', fetch: fetchUser, dependsOn: ['config']),
PrefetchNode(key: 'posts', fetch: fetchPosts, dependsOn: ['user']),
]);
Observability
Monitor cache operations for logging and analytics:
final cache = Syncache<User>(
store: MemoryStore<User>(),
observers: [LoggingObserver()],
);
// Or implement your own
class AnalyticsObserver extends SyncacheObserver {
@override
void onCacheHit(String key) => analytics.track('cache_hit', {'key': key});
@override
void onCacheMiss(String key) => analytics.track('cache_miss', {'key': key});
}
Storage Backends
MemoryStore
In-memory storage with optional tag support:
final cache = Syncache<User>(
store: MemoryStore<User>(),
);
SharedMemoryStore
Share cache data across multiple Syncache instances:
final cache1 = Syncache<User>(store: SharedMemoryStore<User>('users'));
final cache2 = Syncache<User>(store: SharedMemoryStore<User>('users'));
// Both instances share the same underlying data
Custom Store
Implement the Store interface for custom backends (SQLite, Hive, etc.):
class SqliteStore<T> implements Store<T> {
@override
Future<void> write(String key, Stored<T> entry) async { ... }
@override
Future<Stored<T>?> read(String key) async { ... }
@override
Future<void> delete(String key) async { ... }
@override
Future<void> clear() async { ... }
}
Network Awareness
Provide a custom Network implementation for offline detection:
class ConnectivityNetwork implements Network {
@override
Future<bool> get isOnline async {
final result = await Connectivity().checkConnectivity();
return result != ConnectivityResult.none;
}
}
final cache = Syncache<User>(
store: MemoryStore<User>(),
network: ConnectivityNetwork(),
);
Related Packages
Looking for additional functionality? Check out these companion packages:
| Package | Description |
|---|---|
| syncache_flutter | Flutter integration with widgets (CacheBuilder, CacheConsumer), lifecycle management, and connectivity detection via FlutterNetwork |
| syncache_hive | Persistent storage backend using Hive for cross-platform caching (iOS, Android, Web, Desktop) |
License
MIT License. See LICENSE for details.