This commit is contained in:
Tobe O 2018-10-21 05:35:04 -04:00
parent 5295ea57eb
commit 466c784d03
10 changed files with 75 additions and 71 deletions

View file

@ -1,2 +1,3 @@
analyzer: analyzer:
strong-mode: true strong-mode:
implicit-casts: false

View file

@ -2,20 +2,19 @@ import 'package:angel_cache/angel_cache.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
main() async { main() async {
var app = new Angel()..lazyParseBodies = true; var app = new Angel();
app.use( app.use(
'/api/todos', '/api/todos',
new CacheService( new CacheService(
database: new AnonymousService( cache: new MapService(),
index: ([params]) { database: new AnonymousService(index: ([params]) {
print('Fetched directly from the underlying service at ${new DateTime.now()}!'); print(
'Fetched directly from the underlying service at ${new DateTime.now()}!');
return ['foo', 'bar', 'baz']; return ['foo', 'bar', 'baz'];
}, }, read: (id, [params]) {
read: (id, [params]) {
return {id: '$id at ${new DateTime.now()}'}; return {id: '$id at ${new DateTime.now()}'};
} }),
),
), ),
); );

View file

@ -3,7 +3,7 @@ import 'package:angel_framework/angel_framework.dart';
import 'package:glob/glob.dart'; import 'package:glob/glob.dart';
main() async { main() async {
var app = new Angel()..lazyParseBodies = true; var app = new Angel();
// Cache a glob // Cache a glob
var cache = new ResponseCache() var cache = new ResponseCache()
@ -12,14 +12,14 @@ main() async {
]); ]);
// Handle `if-modified-since` header, and also send cached content // Handle `if-modified-since` header, and also send cached content
app.use(cache.handleRequest); app.fallback(cache.handleRequest);
// A simple handler that returns a different result every time. // A simple handler that returns a different result every time.
app.get('/date.txt', app.get('/date.txt',
(ResponseContext res) => res.write(new DateTime.now().toIso8601String())); (req, res) => res.write(new DateTime.now().toIso8601String()));
// Support purging the cache. // Support purging the cache.
app.addRoute('PURGE', '*', (RequestContext req) { app.addRoute('PURGE', '*', (req, res) {
if (req.ip != '127.0.0.1') throw new AngelHttpException.forbidden(); if (req.ip != '127.0.0.1') throw new AngelHttpException.forbidden();
cache.purge(req.uri.path); cache.purge(req.uri.path);

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io' show HttpDate;
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:pool/pool.dart'; import 'package:pool/pool.dart';
import 'util.dart';
/// A flexible response cache for Angel. /// A flexible response cache for Angel.
/// ///
@ -35,9 +35,8 @@ class ResponseCache {
Future<bool> ifModifiedSince(RequestContext req, ResponseContext res) async { Future<bool> ifModifiedSince(RequestContext req, ResponseContext res) async {
if (req.method != 'GET' && req.method != 'HEAD') return true; if (req.method != 'GET' && req.method != 'HEAD') return true;
if (req.headers.value('if-modified-since') != null) { if (req.headers.ifModifiedSince != null) {
var modifiedSince = fmt var modifiedSince = req.headers.ifModifiedSince;
.parse(req.headers.value('if-modified-since').replaceAll('GMT', ''));
// Check if there is a cache entry. // Check if there is a cache entry.
for (var pattern in patterns) { for (var pattern in patterns) {
@ -72,7 +71,7 @@ class ResponseCache {
// Check if there is a cache entry. // Check if there is a cache entry.
// //
// If `if-modified-since` is present, this check has already been performed. // If `if-modified-since` is present, this check has already been performed.
if (req.headers.value('if-modified-since') == null) { if (req.headers.ifModifiedSince == null) {
for (var pattern in patterns) { for (var pattern in patterns) {
if (pattern.allMatches(req.uri.path).isNotEmpty) { if (pattern.allMatches(req.uri.path).isNotEmpty) {
var now = new DateTime.now().toUtc(); var now = new DateTime.now().toUtc();
@ -88,9 +87,11 @@ class ResponseCache {
_setCachedHeaders(response.timestamp, req, res); _setCachedHeaders(response.timestamp, req, res);
res res
..headers.addAll(response.headers) ..headers.addAll(response.headers)
..buffer.add(response.body) ..add(response.body)
..end(); ..close();
return false; return false;
} else {
_setCachedHeaders(now, req, res);
} }
} }
} }
@ -131,7 +132,7 @@ class ResponseCache {
new Map.from(res.headers), res.buffer.toBytes(), now); new Map.from(res.headers), res.buffer.toBytes(), now);
}); });
// _setCachedHeaders(now, req, res); _setCachedHeaders(now, req, res);
} }
} }
@ -144,11 +145,11 @@ class ResponseCache {
res.headers res.headers
..['cache-control'] = '$privacy, max-age=${timeout?.inSeconds ?? 86400}' ..['cache-control'] = '$privacy, max-age=${timeout?.inSeconds ?? 86400}'
..['last-modified'] = formatDateForHttp(modified); ..['last-modified'] = HttpDate.format(modified);
if (timeout != null) { if (timeout != null) {
var expiry = new DateTime.now().add(timeout); var expiry = new DateTime.now().add(timeout);
res.headers['expires'] = formatDateForHttp(expiry); res.headers['expires'] = HttpDate.format(expiry);
} }
} }
} }

View file

@ -7,33 +7,36 @@ import 'package:meta/meta.dart';
/// ///
/// This is useful for applications of scale, where network latency /// This is useful for applications of scale, where network latency
/// can have real implications on application performance. /// can have real implications on application performance.
class CacheService extends Service { class CacheService<Id, Data> extends Service<Id, Data> {
/// The underlying [Service] that represents the original data store. /// The underlying [Service] that represents the original data store.
final Service database; final Service<Id, Data> database;
/// The [Service] used to interface with a caching layer. /// The [Service] used to interface with a caching layer.
/// ///
/// If not provided, this defaults to a [MapService]. /// If not provided, this defaults to a [MapService].
final Service cache; final Service<Id, Data> cache;
final bool ignoreQuery; final bool ignoreQuery;
final Duration timeout; final Duration timeout;
final Map<dynamic, _CachedItem> _cache = {}; final Map<Id, _CachedItem<Data>> _cache = {};
_CachedItem _indexed; _CachedItem<List<Data>> _indexed;
CacheService( CacheService(
{@required this.database, {@required this.database,
Service cache, @required this.cache,
this.ignoreQuery: false, this.ignoreQuery: false,
this.timeout}) this.timeout}) {
: this.cache = cache ?? new MapService() {
assert(database != null); assert(database != null);
} }
Future _getCached(Map params, _CachedItem get(), Future getFresh(), Future<T> _getCached<T>(
Future getCached(), Future save(data, DateTime now)) async { Map<String, dynamic> params,
_CachedItem get(),
FutureOr<T> getFresh(),
FutureOr<T> getCached(),
FutureOr<T> save(T data, DateTime now)) async {
var cached = get(); var cached = get();
var now = new DateTime.now().toUtc(); var now = new DateTime.now().toUtc();
@ -47,8 +50,8 @@ class CacheService extends Service {
var queryEqual = ignoreQuery == true || var queryEqual = ignoreQuery == true ||
(params != null && (params != null &&
cached.params != null && cached.params != null &&
const MapEquality() const MapEquality().equals(
.equals(params['query'], cached.params['query'])); params['query'] as Map, cached.params['query'] as Map));
if (queryEqual) { if (queryEqual) {
return await getCached(); return await getCached();
} }
@ -63,12 +66,12 @@ class CacheService extends Service {
} }
@override @override
Future index([Map params]) { Future<List<Data>> index([Map<String, dynamic> params]) {
return _getCached( return _getCached(
params, params,
() => _indexed, () => _indexed,
() => database.index(params), () => database.index(params),
() => _indexed.data, () => _indexed?.data ?? [],
(data, now) async { (data, now) async {
_indexed = new _CachedItem(params, now, data); _indexed = new _CachedItem(params, now, data);
return data; return data;
@ -77,8 +80,8 @@ class CacheService extends Service {
} }
@override @override
Future read(id, [Map params]) async { Future<Data> read(Id id, [Map<String, dynamic> params]) async {
return _getCached( return _getCached<Data>(
params, params,
() => _cache[id], () => _cache[id],
() => database.read(id, params), () => database.read(id, params),
@ -91,37 +94,37 @@ class CacheService extends Service {
} }
@override @override
Future create(data, [Map params]) { Future<Data> create(data, [Map<String, dynamic> params]) {
_indexed = null; _indexed = null;
return database.create(data, params); return database.create(data, params);
} }
@override @override
Future modify(id, data, [Map params]) { Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]) {
_indexed = null; _indexed = null;
_cache.remove(id); _cache.remove(id);
return database.modify(id, data, params); return database.modify(id, data, params);
} }
@override @override
Future update(id, data, [Map params]) { Future<Data> update(Id id, Data data, [Map<String, dynamic> params]) {
_indexed = null; _indexed = null;
_cache.remove(id); _cache.remove(id);
return database.modify(id, data, params); return database.modify(id, data, params);
} }
@override @override
Future remove(id, [Map params]) { Future<Data> remove(Id id, [Map<String, dynamic> params]) {
_indexed = null; _indexed = null;
_cache.remove(id); _cache.remove(id);
return database.remove(id, params); return database.remove(id, params);
} }
} }
class _CachedItem { class _CachedItem<Data> {
final params; final params;
final DateTime timestamp; final DateTime timestamp;
final data; final Data data;
_CachedItem(this.params, this.timestamp, [this.data]); _CachedItem(this.params, this.timestamp, [this.data]);

View file

@ -7,7 +7,9 @@ import 'package:angel_framework/angel_framework.dart';
/// ///
/// You can pass a [shouldCache] callback to determine which values should be cached. /// You can pass a [shouldCache] callback to determine which values should be cached.
RequestHandler cacheSerializationResults( RequestHandler cacheSerializationResults(
{Duration timeout, FutureOr<bool> Function(RequestContext, ResponseContext, Object) shouldCache}) { {Duration timeout,
FutureOr<bool> Function(RequestContext, ResponseContext, Object)
shouldCache}) {
return (RequestContext req, ResponseContext res) async { return (RequestContext req, ResponseContext res) async {
var oldSerializer = res.serializer; var oldSerializer = res.serializer;
var cache = <dynamic, String>{}; var cache = <dynamic, String>{};

View file

@ -1,6 +0,0 @@
import 'package:intl/intl.dart';
final DateFormat fmt = new DateFormat('EEE, d MMM yyyy HH:mm:ss');
/// Formats a date (converted to UTC), ex: `Sun, 03 May 2015 23:02:37 GMT`.
String formatDateForHttp(DateTime dt) => fmt.format(dt.toUtc()) + ' GMT';

View file

@ -1,16 +1,15 @@
name: angel_cache name: angel_cache
version: 1.0.0 version: 2.0.0
homepage: https://github.com/angel-dart/cache homepage: https://github.com/angel-dart/cache
description: Support for server-side caching in Angel. description: Support for server-side caching in Angel.
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
environment: environment:
sdk: ">=1.19.0 <3.0.0" sdk: ">=2.0.0-dev <3.0.0"
dependencies: dependencies:
angel_framework: ">=1.0.0-dev <2.0.0" angel_framework: ^2.0.0-alpha
intl: ^0.15.0
meta: ^1.0.0 meta: ^1.0.0
pool: ^1.0.0 pool: ^1.0.0
dev_dependencies: dev_dependencies:
angel_test: ^1.1.0 angel_test: ^2.0.0-alpha
glob: ^1.0.0 glob: ^1.0.0
test: ^0.12.0 test: ^1.0.0

View file

@ -1,5 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_cache/src/util.dart'; import 'dart:io';
import 'package:angel_cache/angel_cache.dart'; import 'package:angel_cache/angel_cache.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel_test/angel_test.dart';
@ -20,14 +20,15 @@ main() async {
new Glob('/*.txt'), new Glob('/*.txt'),
]); ]);
app.use(cache.handleRequest); app.fallback(cache.handleRequest);
app.get( app.get('/date.txt', (req, res) {
'/date.txt', res
(ResponseContext res) => ..useBuffer()
res.write(new DateTime.now().toIso8601String())); ..write(new DateTime.now().toIso8601String());
});
app.addRoute('PURGE', '*', (RequestContext req) { app.addRoute('PURGE', '*', (req, res) {
cache.purge(req.uri.path); cache.purge(req.uri.path);
print('Purged ${req.uri.path}'); print('Purged ${req.uri.path}');
}); });
@ -43,7 +44,8 @@ main() async {
client = await connectTo(app); client = await connectTo(app);
response1 = await client.get('/date.txt'); response1 = await client.get('/date.txt');
response2 = await client.get('/date.txt'); response2 = await client.get('/date.txt');
lastModified = fmt.parse(response2.headers['last-modified']); print(response2.headers);
lastModified = HttpDate.parse(response2.headers['last-modified']);
print('Response 1 status: ${response1.statusCode}'); print('Response 1 status: ${response1.statusCode}');
print('Response 2 status: ${response2.statusCode}'); print('Response 2 status: ${response2.statusCode}');
print('Response 1 body: ${response1.body}'); print('Response 1 body: ${response1.body}');
@ -80,7 +82,10 @@ main() async {
}); });
test('sends 304 on if-modified-since', () async { test('sends 304 on if-modified-since', () async {
var headers = {'if-modified-since': formatDateForHttp(lastModified.add(const Duration(days: 1)))}; var headers = {
'if-modified-since':
HttpDate.format(lastModified.add(const Duration(days: 1)))
};
var response = await client.get('/date.txt', headers: headers); var response = await client.get('/date.txt', headers: headers);
print('Sending headers: $headers'); print('Sending headers: $headers');
print('Response (${response.statusCode}): ${response.headers}'); print('Response (${response.statusCode}): ${response.headers}');
@ -90,7 +95,7 @@ main() async {
test('last-modified in the past', () async { test('last-modified in the past', () async {
var response = await client.get('/date.txt', headers: { var response = await client.get('/date.txt', headers: {
'if-modified-since': 'if-modified-since':
formatDateForHttp(lastModified.subtract(const Duration(days: 10))) HttpDate.format(lastModified.subtract(const Duration(days: 10)))
}); });
print('Response: ${response.body}'); print('Response: ${response.body}');
expect(response.statusCode, 200); expect(response.statusCode, 200);