2018-04-02 02:58:16 +00:00
|
|
|
import 'dart:async';
|
2018-04-02 03:41:43 +00:00
|
|
|
import 'package:collection/collection.dart';
|
2018-04-02 01:05:35 +00:00
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
|
|
|
import 'package:meta/meta.dart';
|
|
|
|
|
|
|
|
/// An Angel [Service] that caches data from another service.
|
|
|
|
///
|
|
|
|
/// This is useful for applications of scale, where network latency
|
|
|
|
/// can have real implications on application performance.
|
2018-10-21 09:35:04 +00:00
|
|
|
class CacheService<Id, Data> extends Service<Id, Data> {
|
2018-04-02 01:05:35 +00:00
|
|
|
/// The underlying [Service] that represents the original data store.
|
2018-10-21 09:35:04 +00:00
|
|
|
final Service<Id, Data> database;
|
2018-04-02 01:05:35 +00:00
|
|
|
|
|
|
|
/// The [Service] used to interface with a caching layer.
|
|
|
|
///
|
|
|
|
/// If not provided, this defaults to a [MapService].
|
2018-10-21 09:35:04 +00:00
|
|
|
final Service<Id, Data> cache;
|
2018-04-02 01:05:35 +00:00
|
|
|
|
2018-11-10 16:59:07 +00:00
|
|
|
/// If `true` (default: `false`), then result caching will discard parameters passed to service methods.
|
|
|
|
///
|
|
|
|
/// If you want to return a cached result more-often-than-not, you may want to enable this.
|
|
|
|
final bool ignoreParams;
|
2018-04-02 03:41:43 +00:00
|
|
|
|
|
|
|
final Duration timeout;
|
|
|
|
|
2018-10-21 09:35:04 +00:00
|
|
|
final Map<Id, _CachedItem<Data>> _cache = {};
|
|
|
|
_CachedItem<List<Data>> _indexed;
|
2018-04-02 03:41:43 +00:00
|
|
|
|
|
|
|
CacheService(
|
|
|
|
{@required this.database,
|
2018-10-21 09:35:04 +00:00
|
|
|
@required this.cache,
|
2018-11-10 16:59:07 +00:00
|
|
|
this.ignoreParams: false,
|
2018-10-21 09:35:04 +00:00
|
|
|
this.timeout}) {
|
2018-04-02 01:05:35 +00:00
|
|
|
assert(database != null);
|
|
|
|
}
|
2018-04-02 02:58:16 +00:00
|
|
|
|
2018-10-21 09:35:04 +00:00
|
|
|
Future<T> _getCached<T>(
|
|
|
|
Map<String, dynamic> params,
|
|
|
|
_CachedItem get(),
|
|
|
|
FutureOr<T> getFresh(),
|
|
|
|
FutureOr<T> getCached(),
|
|
|
|
FutureOr<T> save(T data, DateTime now)) async {
|
2018-04-02 03:41:43 +00:00
|
|
|
var cached = get();
|
|
|
|
var now = new DateTime.now().toUtc();
|
|
|
|
|
|
|
|
if (cached != null) {
|
|
|
|
// If the entry has expired, don't send from the cache
|
|
|
|
var expired =
|
|
|
|
timeout != null && now.difference(cached.timestamp) >= timeout;
|
|
|
|
|
|
|
|
if (timeout == null || !expired) {
|
|
|
|
// Read from the cache if necessary
|
2018-11-10 16:59:07 +00:00
|
|
|
var queryEqual = ignoreParams == true ||
|
2018-04-02 03:41:43 +00:00
|
|
|
(params != null &&
|
|
|
|
cached.params != null &&
|
2018-10-21 09:35:04 +00:00
|
|
|
const MapEquality().equals(
|
|
|
|
params['query'] as Map, cached.params['query'] as Map));
|
2018-04-02 03:41:43 +00:00
|
|
|
if (queryEqual) {
|
|
|
|
return await getCached();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we haven't fetched from the cache by this point,
|
|
|
|
// let's fetch from the database.
|
|
|
|
var data = await getFresh();
|
|
|
|
await save(data, now);
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2018-10-21 09:35:04 +00:00
|
|
|
Future<List<Data>> index([Map<String, dynamic> params]) {
|
2018-04-02 03:41:43 +00:00
|
|
|
return _getCached(
|
|
|
|
params,
|
|
|
|
() => _indexed,
|
|
|
|
() => database.index(params),
|
2018-10-21 09:35:04 +00:00
|
|
|
() => _indexed?.data ?? [],
|
2018-04-02 03:41:43 +00:00
|
|
|
(data, now) async {
|
|
|
|
_indexed = new _CachedItem(params, now, data);
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2018-10-21 09:35:04 +00:00
|
|
|
Future<Data> read(Id id, [Map<String, dynamic> params]) async {
|
|
|
|
return _getCached<Data>(
|
2018-04-02 03:41:43 +00:00
|
|
|
params,
|
|
|
|
() => _cache[id],
|
|
|
|
() => database.read(id, params),
|
|
|
|
() => cache.read(id),
|
|
|
|
(data, now) async {
|
2018-05-16 03:54:34 +00:00
|
|
|
_cache[id] = new _CachedItem(params, now, data);
|
2018-04-02 03:41:43 +00:00
|
|
|
return await cache.modify(id, data);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-04-02 02:58:16 +00:00
|
|
|
@override
|
2018-10-21 09:35:04 +00:00
|
|
|
Future<Data> create(data, [Map<String, dynamic> params]) {
|
2018-04-02 03:41:43 +00:00
|
|
|
_indexed = null;
|
2018-04-02 02:58:16 +00:00
|
|
|
return database.create(data, params);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2018-10-21 09:35:04 +00:00
|
|
|
Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]) {
|
2018-04-02 03:41:43 +00:00
|
|
|
_indexed = null;
|
|
|
|
_cache.remove(id);
|
2018-04-02 02:58:16 +00:00
|
|
|
return database.modify(id, data, params);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2018-10-21 09:35:04 +00:00
|
|
|
Future<Data> update(Id id, Data data, [Map<String, dynamic> params]) {
|
2018-04-02 03:41:43 +00:00
|
|
|
_indexed = null;
|
|
|
|
_cache.remove(id);
|
2018-04-02 02:58:16 +00:00
|
|
|
return database.modify(id, data, params);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2018-10-21 09:35:04 +00:00
|
|
|
Future<Data> remove(Id id, [Map<String, dynamic> params]) {
|
2018-04-02 03:41:43 +00:00
|
|
|
_indexed = null;
|
|
|
|
_cache.remove(id);
|
2018-04-02 02:58:16 +00:00
|
|
|
return database.remove(id, params);
|
|
|
|
}
|
2018-04-02 01:05:35 +00:00
|
|
|
}
|
2018-04-02 03:41:43 +00:00
|
|
|
|
2018-10-21 09:35:04 +00:00
|
|
|
class _CachedItem<Data> {
|
2018-04-02 03:41:43 +00:00
|
|
|
final params;
|
|
|
|
final DateTime timestamp;
|
2018-10-21 09:35:04 +00:00
|
|
|
final Data data;
|
2018-04-02 03:41:43 +00:00
|
|
|
|
|
|
|
_CachedItem(this.params, this.timestamp, [this.data]);
|
|
|
|
|
|
|
|
@override
|
|
|
|
String toString() {
|
|
|
|
return '$timestamp:$params:$data';
|
|
|
|
}
|
|
|
|
}
|