syncache_drift
Drift (SQLite) storage backend for Syncache — durable persistent caching with a real relational tag index and a mutation outbox sharing one SQL connection.
Features
- Persistent storage using Drift (SQLite)
- Full support for tag-based cache invalidation — tags live in a dedicated
cache_tagstable, so tag lookups are indexed SQL queries (no scans) - Pattern-based key matching and deletion with the same glob (
*/?) semantics assyncache_hive - Durable mutation outbox (
DriftMutationOutbox) withoptimisticSnapshotpersistence for exact-replay recovery - One shared database: cache and outbox share a single SQLite
connection, so
clearnever destroys unsynced writes and cache + outbox writes can run inside one real SQL transaction - Automatic serialization via JSON (
fromJson/toJson, same shape asHiveStore) - Corrupted rows are handled gracefully: reads skip/delete them instead of crashing, outbox loads skip and report them
Installation
Add syncache_drift to your pubspec.yaml:
dependencies:
syncache: ^0.6.0
syncache_drift: ^0.6.0
For Flutter apps, also add the SQLite binaries:
dependencies:
sqlite3_flutter_libs: ^0.5.0
Backend dependency stance
This package depends on drift only. It never depends on hive,
hive_ce, or syncache_hive — backend packages for Syncache are
independent and never reference each other. Use syncache_drift instead
of syncache_hive (or alongside it, e.g. Hive cache + drift outbox, if
you like, though there is rarely a reason to). You never need both for one
backend's features: the drift backend covers everything the Hive backend
does, plus the shared-connection transaction bonus.
Usage
Basic Setup
import 'package:syncache/syncache.dart';
import 'package:syncache_drift/syncache_drift.dart';
class User {
final int id;
final String name;
const User({required this.id, required this.name});
Map<String, dynamic> toJson() => {'id': id, 'name': name};
static User fromJson(Map<String, dynamic> json) =>
User(id: json['id'] as int, name: json['name'] as String);
}
final api = Api();
final fetchUser = (_) async => api.getUser(123);
class Api {
Future<User> getUser(int id) async => User(id: id, name: 'User');
}
Future<void> main() async {
// Open ONE database shared by the store and the outbox …
final db = SyncacheDriftDatabase.openFile('path/to/app.sqlite');
final store = DriftStore<User>(
db: db,
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
);
final outbox = DriftMutationOutbox(db: db);
// … and wire both into Syncache.
final cache = Syncache<User>(store: store, outbox: outbox);
// Fetch and cache data
final user = await cache.get(
key: 'user:123',
fetch: fetchUser,
);
print(user);
}
Cache-only usage can skip the outbox with the one-call DriftStore.open:
final store = await DriftStore.open<User>(
path: 'path/to/cache.sqlite',
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
);
Table schema
One database, three tables (see SyncacheDriftDatabase):
| Table | Columns | Purpose |
|---|---|---|
cache |
key TEXT PRIMARY KEY, value TEXT, version INTEGER, stored_at INTEGER, ttl_ms INTEGER NULL, etag TEXT NULL, last_modified INTEGER NULL |
cached entries (values as JSON text) |
cache_tags |
key TEXT, tag TEXT, PRIMARY KEY(key, tag) + index on tag |
relational tag index (no scans) |
mutation_outbox |
id TEXT PRIMARY KEY, key TEXT, seq INTEGER (indexed), payload TEXT, queued_at INTEGER, optimistic_snapshot TEXT NULL |
durable pending-mutation records |
The shared-connection bonus
Because cache and outbox live in one database:
store.clear()never deletes outbox records. Clearing the cache removes only thecacheandcache_tagstables — unsynced offline writes inmutation_outboxare untouched and recovered on next startup.- Cache writes and outbox writes are atomically composable. Wrap both in a real SQL transaction; either both commit, or neither does:
await db.transaction(() async {
await store.writeWithTags(key, entry, ['users']);
await outbox.save(record);
});
This is impossible with engines that store cache and outbox in separate files (e.g. Hive boxes).
Using Tags
// Store with tags for grouped invalidation
await cache.get(
key: 'calendar:events:2024-03',
fetch: fetchEvents,
tags: ['calendar', 'events', 'workspace:123'],
);
// Invalidate all entries with 'calendar' tag
await cache.invalidateTag('calendar');
The tag lifecycle contract from the core package holds:
- plain
writepreserves existing tags (tags live incache_tags, a plain write only touchescache), writeWithTagsreplaces tags entirely,delete/clearremove tag mappings together with the entries.
Pattern support
getKeysByPattern/deleteByPattern support the glob subset * (any
characters) and ? (one character), translated identically to
syncache_hive (regex escaping included), so pattern semantics are
portable across backends.
Closing
Close the store (or the outbox) when done; either closes the shared
database. close() waits for in-flight operations before closing, so
pending writes are never truncated:
await store.close();
Serialization
DriftStore requires fromJson and toJson functions to serialize your
data type — values are stored as JSON text:
import 'package:syncache_drift/syncache_drift.dart';
class User {
final String name;
final String email;
User({required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
name: json['name'] as String,
email: json['email'] as String,
);
}
Map<String, dynamic> toJson() => {
'name': name,
'email': email,
};
}
final store = DriftStore.open<User>(
path: 'cache.sqlite',
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
);
API Reference
DriftStore
class DriftStore<T> implements TaggableStore<T>, IterableStore<T> {
DriftStore({required SyncacheDriftDatabase db, required fromJson, required toJson});
static Future<DriftStore<T>> open<T>({required String path, required fromJson, required toJson});
Future<void> close();
}
DriftMutationOutbox
class DriftMutationOutbox implements MutationOutbox {
DriftMutationOutbox({required SyncacheDriftDatabase db, void Function(Object error)? onError});
Future<void> close();
}
Inherited from TaggableStore
write(key, entry)- Store a value (preserves tags)writeWithTags(key, entry, tags)- Store a value with tagsread(key)- Retrieve a valuedelete(key)- Delete a valueclear()- Clear all values (outbox records survive)getTags(key)- Get tags for a keygetKeysByTag(tag)- Get all keys with a tagdeleteByTag(tag)- Delete all entries with a tagdeleteByTags(tags, {matchAll})- Delete entries matching tagsgetKeysByPattern(pattern)- Get keys matching a glob patterndeleteByPattern(pattern)- Delete keys matching a glob patternkeys()- All keys (no value deserialization)
Example
See example/syncache_drift_example.dart
for a runnable end-to-end example (cache, tags, patterns, offline mutation,
recovery, shared transaction).
License
MIT License - see LICENSE file for details.
Libraries
- syncache_drift
- Drift (SQLite) storage backend for Syncache.