2.0.0
This commit is contained in:
parent
5295ea57eb
commit
466c784d03
10 changed files with 75 additions and 71 deletions
|
@ -1,2 +1,3 @@
|
||||||
analyzer:
|
analyzer:
|
||||||
strong-mode: true
|
strong-mode:
|
||||||
|
implicit-casts: false
|
|
@ -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()}'};
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
@ -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>{};
|
||||||
|
|
|
@ -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';
|
|
11
pubspec.yaml
11
pubspec.yaml
|
@ -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
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue