diff --git a/.idea/runConfigurations/cache_service_dart.xml b/.idea/runConfigurations/cache_service_dart.xml new file mode 100644 index 00000000..0e7d0ce4 --- /dev/null +++ b/.idea/runConfigurations/cache_service_dart.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index de2210c9..0ab8dc43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,3 @@ -language: dart \ No newline at end of file +language: dart +dart: + - dev \ No newline at end of file diff --git a/example/cache_service.dart b/example/cache_service.dart new file mode 100644 index 00000000..433ffb45 --- /dev/null +++ b/example/cache_service.dart @@ -0,0 +1,25 @@ +import 'package:angel_cache/angel_cache.dart'; +import 'package:angel_framework/angel_framework.dart'; + +main() async { + var app = new Angel()..lazyParseBodies = true; + + app.use( + '/api/todos', + new CacheService( + database: new AnonymousService( + index: ([params]) { + print('Fetched directly from the underlying service at ${new DateTime.now()}!'); + return ['foo', 'bar', 'baz']; + }, + read: (id, [params]) { + return {id: '$id at ${new DateTime.now()}'}; + } + ), + ), + ); + + var http = new AngelHttp(app); + var server = await http.startServer('127.0.0.1', 3000); + print('Listening at http://${server.address.address}:${server.port}'); +} diff --git a/lib/src/cache_service.dart b/lib/src/cache_service.dart index 9f79435f..78ca1fce 100644 --- a/lib/src/cache_service.dart +++ b/lib/src/cache_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:meta/meta.dart'; @@ -15,28 +16,118 @@ class CacheService extends Service { /// If not provided, this defaults to a [MapService]. final Service cache; - CacheService({@required this.database, Service cache}) + final bool ignoreQuery; + + final Duration timeout; + + final Map _cache = {}; + _CachedItem _indexed; + + CacheService( + {@required this.database, + Service cache, + this.ignoreQuery: false, + this.timeout}) : this.cache = cache ?? new MapService() { assert(database != null); } + Future _getCached(Map params, _CachedItem get(), Future getFresh(), + Future getCached(), Future save(data, DateTime now)) async { + var cached = get(); + //print('$params => $cached'); + 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 + var queryEqual = ignoreQuery == true || + (params != null && + cached.params != null && + const MapEquality() + .equals(params['query'], cached.params['query'])); + 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 + Future index([Map params]) { + return _getCached( + params, + () => _indexed, + () => database.index(params), + () => _indexed.data, + (data, now) async { + _indexed = new _CachedItem(params, now, data); + return data; + }, + ); + } + + @override + Future read(id, [Map params]) async { + return _getCached( + params, + () => _cache[id], + () => database.read(id, params), + () => cache.read(id), + (data, now) async { + _cache[id] = new _CachedItem(params, now); + return await cache.modify(id, data); + }, + ); + } + @override Future create(data, [Map params]) { + _indexed = null; return database.create(data, params); } @override Future modify(id, data, [Map params]) { + _indexed = null; + _cache.remove(id); return database.modify(id, data, params); } @override Future update(id, data, [Map params]) { + _indexed = null; + _cache.remove(id); return database.modify(id, data, params); } @override Future remove(id, [Map params]) { + _indexed = null; + _cache.remove(id); return database.remove(id, params); } } + +class _CachedItem { + final params; + final DateTime timestamp; + final data; + + _CachedItem(this.params, this.timestamp, [this.data]); + + @override + String toString() { + return '$timestamp:$params:$data'; + } +}