Cache
This page shows the full API of dox_cache, the cache behind Cache() in Dox.
It has remember, tags, atomic locks and four drivers.
In a Dox app
dox_core uses dox_cache and exports it. There is nothing to install.
With no config, Dox has a file store (the default) and a memory store. Use it with Cache():
await Cache().put('foo', 'bar', duration: Duration(minutes: 10));
String? foo = await Cache().get('foo');
// store JSON values, so a model becomes a map with toJson()
List<dynamic> posts = await Cache().remember(
'posts',
Duration(minutes: 5),
() async => (await Post().all()).map((Post p) => p.toJson()).toList(),
);
The app template has the stores in lib/config/cache.dart.
lib/config/app.dart passes them to AppConfig(cache: cache).
Change this file to add a store, like Redis:
import 'package:dox_core/dox_core.dart';
import 'package:my_app/config/env.dart';
CacheConfig cache = CacheConfig(
defaultStore: env.cacheStore, // CACHE_STORE in .env, default 'file'
stores: <String, Map<String, dynamic>>{
'file': <String, dynamic>{'driver': 'file', 'path': 'storage/cache'},
'memory': <String, dynamic>{'driver': 'memory', 'max_size': 10000},
'redis': <String, dynamic>{'driver': 'redis', 'host': '127.0.0.1', 'prefix': 'app_cache:'},
},
);
Then set CACHE_STORE=redis in .env.
Cache().store('memory')uses another store.Dox().cacheis theCacheManagerof this page.Cache().repositoryis theCacheStore.- The full guide is at Cache.
Drivers
| Driver | What it is | Shared between isolates |
|---|---|---|
memory |
a map in this isolate. Fast. Optional LRU size limit | No |
file |
one JSON file per key in a folder | Yes (same machine) |
redis |
Redis, with the ioredis package | Yes |
null |
stores nothing. Use it to turn caching off | - |
Warning
Dox runs many isolates. Each isolate has its own memory cache.
Use redis when isolates or servers must see the same data.
Without Dox
Note
dox_core exports CacheConfig, CacheManager, CacheStore and CacheLock from this package.
They are the same classes, so importing both packages is fine.
Make a CacheManager with your stores:
CacheManager cache = CacheManager(CacheConfig(
defaultStore: 'redis',
stores: <String, Map<String, dynamic>>{
'memory': <String, dynamic>{'driver': 'memory', 'max_size': 1000},
'file': <String, dynamic>{'driver': 'file', 'path': 'storage/cache'},
'redis': <String, dynamic>{'driver': 'redis', 'host': '127.0.0.1', 'prefix': 'app:'},
},
));
CacheStore store = cache.store(); // the default store
await cache.store('file').get('a'); // a named store
| Driver | Options |
|---|---|
| every driver | driver, prefix (default dox_cache:) |
memory |
max_size: the most items. When full, the least recently used item is removed. No limit when not set |
file |
path (default storage/cache). Each prefix gets its own sub folder |
redis |
host (default 127.0.0.1), port (default 6379), username, password, db, tls, pool_size, max_value_bytes (values bigger than this throw. No limit by default) |
pool_size is the most Redis connections one store opens in one isolate (default 10). A store uses about one, so you rarely change it.
Basic use
await store.put('a', 1, Duration(minutes: 5));
await store.get('a'); // 1
await store.get('missing', 'none'); // 'none'
await store.has('a'); // true
await store.forget('a');
No ttl means forever
put without a ttl keeps the value forever.
A zero or negative ttl removes the key.
Cache() in dox_core takes the ttl as a named argument: Cache().put('a', 1, duration: Duration(minutes: 5)).
Remember
Get the cached value, or run the function and cache the result.
List<dynamic> posts = await store.remember('posts', Duration(minutes: 10), () async {
return await loadPostsAsJson();
});
A cached value comes back as JSON. A List is List<dynamic> and a Map is Map<String, dynamic>.
So without decode, the type must be a raw JSON type.
For other types, pass decode. It turns the cached JSON into your type:
List<String> names = await store.remember<List<String>>(
'names',
Duration(minutes: 10),
loadNames,
decode: (Object? json) => List<String>.from(json as List<dynamic>),
);
- Many calls for the same key at the same time run the function once in one isolate. The others wait for the first.
- With
lock: true, callers in other isolates and servers wait too. It uses a cache lock, so the driver must support locks.
Flexible
Stale while revalidate. The value is fresh for the first time. After that it is stale for the second time. A stale value is still returned right away, and one caller refreshes it in the background. After both times, the value is gone and the next call waits for the function.
int count = await store.flexible<int>(
'users',
Duration(minutes: 5), // fresh
Duration(minutes: 10), // stale
countUsers,
);
It takes decode like remember. The driver must support locks.
All methods
| Method | What it does |
|---|---|
get(key, [default]) |
the value, or the default when missing or expired. The default can be a function. It runs only on a miss |
getRaw(key) |
the stored JSON text, with no decode. Null when missing |
put(key, value, [ttl]) |
store it |
add(key, value, [ttl]) |
store only when missing. Returns true when stored |
has(key) |
true when the value is not null |
forget(key) |
remove it. Returns true when it was there |
flush() |
remove all items of this store |
many(keys) / putMany(map, [ttl]) |
many keys at once |
increment(key, [by]) / decrement(key, [by]) |
change a number. A missing key starts at 0. The ttl stays |
forever(key, value) |
store without expiry |
remember(key, ttl, compute, {decode, lock}) |
see above |
flexible(key, fresh, stale, compute, {decode}) |
see above |
touch(key, ttl) |
set a new ttl and keep the value. Returns true when the key was there |
rememberForever(key, compute, {decode, lock}) |
the same, without expiry |
pull(key, [default]) |
get it and remove it, in one atomic step |
tags([...]) |
a tagged view |
lock(name, [ttl, owner]) |
an atomic lock |
Values are JSON
Values must be JSON types: Map, List, String, num, bool, null. Other types throw.
- A
Mapcomes back asMap<String, dynamic>. AListcomes back asList<dynamic>. - A stored
nullreads as a miss.hasreturns false for it. - For a model, store
model.toJson(), not the model.
Tags
await store.tags(<String>['users']).put('u1', 'Ann');
await store.tags(<String>['users']).flush(); // removes only the 'users' items
Tags work with every driver. Old items are not deleted right away. They are just not found any more, and they go away when their ttl ends.
Locks
Only one worker runs the code at a time.
CacheLock lock = store.lock('report', Duration(seconds: 10));
if (await lock.get()) {
try {
// only one worker runs this
} finally {
await lock.release();
}
}
bool got = await lock.block(Duration(seconds: 5)); // wait up to 5 seconds
get()takes the lock and returnstrue, or returnsfalsewhen someone else has it.block(timeout)tries again every 50 ms until it gets the lock or the time is over.- The
ttlfrees the lock by itself if the worker crashes. Without a ttl, the lock stays untilrelease(). release()frees the lock only for the owner that took it.block(timeout, callback)runs the callback while it holds the lock, frees the lock and returns the result. It throwsLockTimeoutExceptionwhen the time is over.forceRelease()frees the lock, whoever holds it.memory,fileandredissupport locks. Thememorylock works inside one isolate only. Thefilelock is a file made withcreate(exclusive: true). It works for all isolates on one machine.nullthrowsUnsupportedError.
Prefix
Every store has a prefix (default dox_cache:). It is added to every key.
Use different prefixes when two apps share one Redis database.
On Redis, a prefix with no : at the end gets one, so app does not match the keys of app2.
Redis flush
flush deletes only the keys with the prefix. It never runs FLUSHDB.
An empty prefix makes flush throw.
File driver
- Each prefix has its own sub folder inside
path.flushdeletes only the files of that prefix. add,incrementandtouchtake a file lock on the key, so they are safe across isolates.- A read never deletes a file. An expired file stays until it is written again.
prune()deletes the expired files. Call it from time to time, eg. in a schedule:
- Or set
prune_everyon the store to runprune()in the background once every that many writes. It is off by default. Each run reads every file of the store, so pick a big number, eg.1000:
prune()takes the key lock thatadd,incrementandtouchuse, so it never deletes their new value. A plainputtakes no lock. If aputwrites a key at the same momentprune()deletes it, the new value is lost. It is only a cache miss.
Your own driver
class MyDriverFactory implements CacheDriverFactory {
@override
CacheDriver create(Map<String, dynamic> options) {
return MyDriver(options);
}
}
cache.extend('mine', MyDriverFactory()); // config: {'driver': 'mine'}
cache.addStore('ready', MemoryDriver()); // or add a ready driver
MyDriver extends CacheDriver. It must implement prefix, get, put, add, increment, forget and flush.
Override lock to support locks, and close to close its connections.
Override pull to make it atomic, and has and touch to make them faster. The defaults work, but pull is a get and then a forget.
Close
cache.close() closes the connections of every store that was built, eg. the Redis connections.
Memory, file and null stores have nothing to close. It is safe to call twice.
A store used after it is built again.
In a Dox app you do not call it. Dox closes Cache() stores on shutdown, in every isolate, after the open requests are done.