typed_cache_json 0.3.0
typed_cache_json: ^0.3.0 copied to clipboard
A typed cache implementation for Flutter that uses JSON serialization.
typed_cache_json #
A JSON-based cache backend for the typed_cache package. Provides a simple, type-safe, and persistent solution for storing data in a single JSON file, ideal for Flutter and Dart applications that need lightweight persistence.
📚 Complete Documentation: The entire codebase is fully documented with DartDoc comments. Use your IDE's autocomplete or generate documentation with
dart docto explore the complete API.
Features #
- Type-Safe Caching: Store and retrieve objects with type safety using
CacheCodec. - JSON Persistence: All data is saved in a single local JSON file.
- Atomic Writes: Uses temporary (
.tmp) and backup (.bak) files to prevent data corruption during writes. - Automatic Recovery: Attempts to recover data from backups if the main file is corrupted.
- Expiration Support (TTL): Set a time-to-live for your cache entries.
- Tag-Based Indexing: Organize and bulk-remove cache entries using tags.
- Flutter Integration: Easy path resolution (
ApplicationSupport,Documents,Temporary) viapath_provider. - Thread-Safe: Operations protected by an async mutex, ensuring safety in concurrent environments.
- Complete Documentation: Fully documented API with examples and detailed explanations.
Getting Started #
Add the dependency to your pubspec.yaml:
dependencies:
typed_cache_json:
git:
url: https://github.com/saulogatti/typed_cache_json.git
Usage #
Basic Setup (Flutter) #
The easiest way to get started with Flutter is using the create function:
import 'package:typed_cache_json/typed_cache_json.dart';
void main() async {
// Initialize the cache pointing to the application support directory
final cache = await create(
location: CacheLocation.support,
subdir: 'my_app_cache',
fileName: 'cache.json',
);
// Now you can use the cache!
}
Available Locations
The CacheLocation enum defines where the cache file will be stored:
CacheLocation.support(Recommended): Internal application files not exposed to the userCacheLocation.temporary: Temporary cache; the OS may clean it when neededCacheLocation.documents: User documents (avoid for cache)
Advanced Configuration #
If you need more control, you can create the backend directly:
import 'package:typed_cache_json/typed_cache_json.dart';
import 'package:typed_cache_json/src/json_file_cache_backend.dart';
void main() async {
// Create the backend with custom settings
final backend = await JsonFileCacheBackend.fromLocation(
location: CacheLocation.support,
subdir: 'my_app_cache',
fileName: 'cache.json',
enableRecovery: true, // Enable automatic recovery (default: true)
);
// Create the cache with the backend
final cache = createTypedCache(
backend: backend,
deleteCorruptedEntries: true, // Automatically remove corrupted entries
);
}
Storing and Retrieving Data #
To use the cache, you need to define a CacheCodec for your data type:
import 'package:typed_cache/typed_cache.dart';
class User {
final String name;
final int age;
User(this.name, this.age);
}
class UserCodec extends CacheCodec<User, Map<String, dynamic>> {
@override
String get typeId => 'user';
@override
User decode(Map<String, dynamic> data) {
return User(data['name'] as String, data['age'] as int);
}
@override
Map<String, dynamic> encode(User value) {
return {'name': value.name, 'age': value.age};
}
}
// Using the cache
void main() async {
final cache = await create(
location: CacheLocation.support,
fileName: 'cache.json',
);
final user = User('Saulo', 30);
final codec = UserCodec();
// Save
await cache.put('user_1', user, codec: codec);
// Retrieve
final cachedUser = await cache.get('user_1', codec: codec);
print('Name: ${cachedUser?.name}, Age: ${cachedUser?.age}');
}
Using the Built-in JSON Codec #
For simple data in Map format, you can use the included CacheJsonCodec:
import 'package:typed_cache_json/typed_cache_json.dart';
void main() async {
final cache = await create(
location: CacheLocation.support,
fileName: 'cache.json',
);
final codec = CacheJsonCodec();
// Save a Map directly
await cache.put('config', {'theme': 'dark', 'version': 2}, codec: codec);
// Retrieve
final config = await cache.get('config', codec: codec);
print('Theme: ${config?['theme']}');
}
Using Tags and TTL #
// Save with 1-hour expiration and tags
await cache.put(
'session_data',
sessionData,
codec: myCodec,
ttl: Duration(hours: 1),
tags: {'session', 'auth'},
);
// Invalidate everything with the 'session' tag
await cache.invalidateByTag('session');
// Get all keys with a specific tag
final sessionKeys = await cache.keysByTag('session');
print('Session keys: $sessionKeys');
Cleaning Expired Cache #
The cache does not automatically remove expired entries from disk (except when you try to read an expired key). To clean the file:
// Remove all expired entries from the JSON file
final count = await cache.purgeExpired();
print('$count entries removed');
Complete Cache Clear #
To remove all cache data:
// Clear all cache
await cache.clear();
File Structure #
The backend maintains a JSON file with the following structure:
{
"schemaVersion": 1,
"entries": {
"key1": {
"key": "key1",
"typeId": "user",
"payload": { "name": "Saulo", "age": 30 },
"createdAt": 1700000000000,
"expiresAt": 1700003600000,
"tags": ["session"]
}
},
"tagIndex": {
"session": ["key1"]
}
}
Safety Files #
During write operations, the backend creates auxiliary files:
cache.json.tmp: Temporary file used during write operationscache.json.bak: Backup of the previous file, used for recovery in case of corruption
These files are managed automatically and ensure data integrity.
Data Recovery #
The package includes a robust data recovery system:
- If the main file is corrupted, it attempts to load from
.bak - If the
.bakis also corrupted, it tries the.tmpfile - If none work, it initializes an empty cache
You can disable automatic recovery when creating the backend:
final backend = await JsonFileCacheBackend.fromLocation(
location: CacheLocation.support,
fileName: 'cache.json',
enableRecovery: false, // Disable recovery
);
Logging #
For debugging and monitoring, you can enable logs when creating the cache:
final cache = await create(
location: CacheLocation.support,
fileName: 'cache.json',
logger: (message) => print('[Cache] $message'),
);
Architecture and Internal Workings #
Main Components #
The package is organized into specialized components:
1. JsonFileCacheBackend
Main backend that implements CacheBackend from typed_cache. Responsible for:
- Atomic read/write operations
- Entry lifecycle management
- Tag index maintenance
- Automatic failure recovery
2. AsyncMutex
Async mutex that serializes concurrent operations. Ensures that:
- I/O operations don't overlap
- Internal state remains consistent
- Errors in one operation don't block others
3. JsonCacheFile
Data model that represents the JSON file structure in memory:
- Stores all cache entries
- Maintains reverse tag index for efficient searches
- Serializes/deserializes the JSON file
4. CacheJsonCodec
Pre-built codec for simple JSON data (Map<String, dynamic>):
- Facilitates storage of configurations and structured data
- No need to create custom codecs for simple data
Operation Flow #
Write Operation
TypedCache.put()
→ JsonFileCacheBackend.write()
→ _mutex.synchronized()
→ _load() (load file)
→ _upsertEntry() (update entry and tag index)
→ _save()
→ _atomicWrite() (write .tmp → rename → backup .bak)
Read Operation
TypedCache.get()
→ JsonFileCacheBackend.read()
→ _mutex.synchronized()
→ _load() (load and cache in memory during operation)
→ return entry or null
Failure Recovery
_load() fails
→ _recoverOrEmpty() (if enableRecovery = true)
→ try .bak
→ try .tmp
→ return empty if all fail
Thread-Safety Guarantees #
All public operations are protected by AsyncMutex, ensuring:
- Serialization: Operations execute one at a time, in submission order
- Consistency: File and index state always synchronized
- Isolation: Errors in one operation don't affect others
Durability Guarantees #
The atomic write protocol ensures:
- Atomicity: Complete write or no write (no partial corruption)
- Automatic Backup: Previous version always preserved in
.bak - Recovery: System tries multiple paths before giving up
Additional Information #
Compatibility #
- Dart SDK: ^3.10.4
- Flutter: Compatible
- Platforms: iOS, Android, macOS, Windows, Linux
API Documentation #
All code in this package is fully documented with DartDoc comments. The documentation includes:
- Detailed Descriptions: Each class, method, and property has a clear description
- Usage Examples: Practical examples for main features
- Parameters and Returns: Complete documentation of all parameters and return values
- Exceptions: Information about possible errors and how to handle them
- Implementation Notes: Details about internal behavior and thread-safety guarantees
How to Access Documentation
- Via IDE: Use autocomplete (Ctrl+Space / Cmd+Space) and hover over any symbol to see inline documentation
- Generate HTML: Run
dart docin the project directory to generate navigable HTML documentation - Read the Code: DartDoc comments are visible directly in source files
Main Documented Classes
JsonFileCacheBackend: Main backend with atomic operations and automatic recoveryAsyncMutex: Async mutex implementation for operation serializationCacheJsonCodec: Pre-built codec for simple JSON dataJsonCacheFile: Internal cache file modelCacheLocation: Enum for choosing cache file location
Useful Links #
- typed_cache - Base caching package
- Repository
Advanced Resources #
For more details about:
- Creating complex codecs
- Custom TTL policies
- Invalidation strategies
- Performance optimizations
See the typed_cache documentation.
Best Practices #
Choosing Storage Location #
- Use
CacheLocation.supportfor most cases - it's the recommended location for cache - Use
CacheLocation.temporaryonly for truly disposable cache that can be cleared by the OS - Avoid
CacheLocation.documentsfor cache - it's for user-visible files
Tag Management #
// Organize related entries with tags
await cache.put('user_123', userData, codec: codec, tags: {'user', 'session'});
await cache.put('config_123', configData, codec: codec, tags: {'config', 'session'});
// Clear everything related to session at once
await cache.invalidateByTag('session');
Periodic Cleanup #
// Run periodically to keep the file optimized
Future<void> performCacheMaintenance() async {
final removed = await cache.purgeExpired();
print('Removed $removed expired entries');
}
// Example: run on app startup
void main() async {
final cache = await create(/*...*/);
await performCacheMaintenance();
runApp(MyApp());
}
Custom Codecs #
// For complex objects, create specific codecs
class UserCodec extends CacheCodec<User, Map<String, dynamic>> {
@override
String get typeId => 'user:v1'; // Include version in typeId
@override
User decode(Map<String, dynamic> data) {
return User.fromJson(data);
}
@override
Map<String, dynamic> encode(User value) {
return value.toJson();
}
}
Error Handling #
try {
final data = await cache.get('key', codec: codec);
if (data == null) {
// Key doesn't exist or has expired
print('Cache miss');
}
} catch (e) {
// I/O error or corruption
print('Error accessing cache: $e');
// The cache attempts automatic recovery
}
Troubleshooting #
Cache doesn't persist between runs #
Cause: Possible use of CacheLocation.temporary on device with low space.
Solution: Use CacheLocation.support for data that should persist.
File gets corrupted repeatedly #
Cause: Possible write failure (e.g., app terminated abruptly). Solution: The automatic recovery system should resolve it. If it persists:
// Force a complete cleanup
await cache.clear();
Slow performance with many entries #
Cause: Large JSON file being loaded/saved on every operation. Solutions:
- Run
purgeExpired()periodically - Use TTL to limit entry lifespan
- Consider splitting into multiple cache files by context
- Use tags to organize and clean groups of entries
"Permission Denied" error #
Cause: Attempting to access directory without appropriate permissions.
Solution: Use CacheLocation.support which always has proper permissions.
Data disappears on iOS #
Cause: Use of CacheLocation.temporary - iOS aggressively clears this folder.
Solution: Use CacheLocation.support for data that should persist.
Contributing #
Contributions are welcome! Feel free to open issues or pull requests.
License #
This project is open source. Check the LICENSE file for more details.