2018-04-02 01:05:35 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
|
|
|
import 'package:intl/intl.dart';
|
2018-04-02 01:37:34 +00:00
|
|
|
import 'package:pool/pool.dart';
|
2018-04-02 01:05:35 +00:00
|
|
|
|
|
|
|
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';
|
|
|
|
|
2018-04-02 01:49:35 +00:00
|
|
|
/// A flexible response cache for Angel.
|
|
|
|
///
|
|
|
|
/// Use this to improve real and perceived response of Web applications,
|
|
|
|
/// as well as to memoize expensive responses.
|
2018-04-02 01:05:35 +00:00
|
|
|
class ResponseCache {
|
2018-04-02 01:49:35 +00:00
|
|
|
/// A set of [Patterns] for which responses will be cached.
|
|
|
|
///
|
|
|
|
/// For example, you can pass a `Glob` matching `**/*.png` files to catch all PNG images.
|
2018-04-02 01:05:35 +00:00
|
|
|
final List<Pattern> patterns = [];
|
2018-04-02 01:49:35 +00:00
|
|
|
|
|
|
|
/// An optional timeout, after which a given response will be removed from the cache, and the contents refreshed.
|
2018-04-02 01:05:35 +00:00
|
|
|
final Duration timeout;
|
2018-04-02 01:49:35 +00:00
|
|
|
|
2018-04-02 01:05:35 +00:00
|
|
|
final Map<String, _CachedResponse> _cache = {};
|
2018-04-02 01:49:35 +00:00
|
|
|
final Map<String, Pool> _writeLocks = {};
|
2018-04-02 01:05:35 +00:00
|
|
|
|
|
|
|
ResponseCache({this.timeout});
|
|
|
|
|
2018-04-02 01:49:35 +00:00
|
|
|
/// Closes all internal write-locks, and closes the cache.
|
|
|
|
Future close() async {
|
|
|
|
_writeLocks.forEach((_, p) => p.close());
|
|
|
|
}
|
|
|
|
|
2018-04-02 01:37:34 +00:00
|
|
|
/// Removes an entry from the response cache.
|
2018-04-02 01:59:46 +00:00
|
|
|
void purge(String path) => _cache.remove(path);
|
2018-04-02 01:37:34 +00:00
|
|
|
|
2018-04-02 01:24:13 +00:00
|
|
|
/// A middleware that handles requests with an `If-Modified-Since` header.
|
|
|
|
///
|
|
|
|
/// This prevents the server from even having to access the cache, and plays very well with static assets.
|
|
|
|
Future<bool> ifModifiedSince(RequestContext req, ResponseContext res) async {
|
|
|
|
if (req.headers.value('if-modified-since') != null) {
|
2018-04-02 01:37:34 +00:00
|
|
|
var modifiedSince = _fmt
|
|
|
|
.parse(req.headers.value('if-modified-since').replaceAll('GMT', ''));
|
2018-04-02 01:05:35 +00:00
|
|
|
|
2018-04-02 01:24:13 +00:00
|
|
|
// Check if there is a cache entry.
|
|
|
|
for (var pattern in patterns) {
|
|
|
|
if (pattern.allMatches(req.uri.path).isNotEmpty &&
|
|
|
|
_cache.containsKey(req.uri.path)) {
|
|
|
|
var response = _cache[req.uri.path];
|
2018-04-02 01:05:35 +00:00
|
|
|
|
2018-04-02 01:27:56 +00:00
|
|
|
if (response.timestamp.compareTo(modifiedSince) <= 0) {
|
2018-04-02 01:24:13 +00:00
|
|
|
res.statusCode = 304;
|
|
|
|
return false;
|
|
|
|
}
|
2018-04-02 01:05:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-04-02 01:45:45 +00:00
|
|
|
/// Serves content from the cache, if applicable.
|
|
|
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
|
2018-04-02 01:59:46 +00:00
|
|
|
if (!await ifModifiedSince(req, res))
|
|
|
|
return false;
|
2018-04-02 01:45:45 +00:00
|
|
|
|
|
|
|
// Check if there is a cache entry.
|
|
|
|
for (var pattern in patterns) {
|
|
|
|
if (pattern.allMatches(req.uri.path).isNotEmpty) {
|
|
|
|
var now = new DateTime.now().toUtc();
|
|
|
|
|
|
|
|
if (_cache.containsKey(req.uri.path)) {
|
|
|
|
// If the cache timeout has been met, don't send the cached response.
|
|
|
|
var response = _cache[req.uri.path];
|
|
|
|
if (now.difference(response.timestamp) >= timeout) return true;
|
|
|
|
_setCachedHeaders(response.timestamp, req, res);
|
|
|
|
res
|
|
|
|
..headers.addAll(response.headers)
|
|
|
|
..buffer.add(response.body)
|
|
|
|
..end();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-04-02 01:37:34 +00:00
|
|
|
/// A response finalizer that saves responses to the cache.
|
2018-04-02 01:24:13 +00:00
|
|
|
Future<bool> responseFinalizer(
|
|
|
|
RequestContext req, ResponseContext res) async {
|
|
|
|
if (res.statusCode == 304) return true;
|
|
|
|
|
2018-04-02 01:37:34 +00:00
|
|
|
// Check if there is a cache entry.
|
|
|
|
for (var pattern in patterns) {
|
|
|
|
if (pattern.allMatches(req.uri.path).isNotEmpty) {
|
|
|
|
var now = new DateTime.now().toUtc();
|
|
|
|
|
|
|
|
// Invalidate the response, if need be.
|
|
|
|
if (_cache.containsKey(req.uri.path)) {
|
|
|
|
// If there is no timeout, don't invalidate.
|
|
|
|
if (timeout == null) return true;
|
|
|
|
|
|
|
|
// Otherwise, don't invalidate unless the timeout has been exceeded.
|
|
|
|
var response = _cache[req.uri.path];
|
|
|
|
if (now.difference(response.timestamp) < timeout) return true;
|
|
|
|
|
|
|
|
// If the cache entry should be invalidated, then invalidate it.
|
2018-04-02 01:59:46 +00:00
|
|
|
purge(req.uri.path);
|
2018-04-02 01:37:34 +00:00
|
|
|
}
|
2018-04-02 01:40:08 +00:00
|
|
|
|
|
|
|
// Save the response.
|
2018-04-02 01:50:38 +00:00
|
|
|
var writeLock = _writeLocks.putIfAbsent(req.uri.path, () => new Pool(1));
|
|
|
|
await writeLock.withResource(() {
|
|
|
|
_cache[req.uri.path] = new _CachedResponse(
|
|
|
|
new Map.from(res.headers), res.buffer.toBytes(), now);
|
|
|
|
});
|
|
|
|
|
2018-04-02 01:45:45 +00:00
|
|
|
_setCachedHeaders(now, req, res);
|
2018-04-02 01:37:34 +00:00
|
|
|
}
|
|
|
|
}
|
2018-04-02 01:40:28 +00:00
|
|
|
|
|
|
|
return true;
|
2018-04-02 01:05:35 +00:00
|
|
|
}
|
|
|
|
|
2018-04-02 01:45:45 +00:00
|
|
|
void _setCachedHeaders(
|
2018-04-02 01:05:35 +00:00
|
|
|
DateTime modified, RequestContext req, ResponseContext res) {
|
|
|
|
var privacy = 'public';
|
|
|
|
|
|
|
|
res.headers
|
|
|
|
..['cache-control'] = '$privacy, max-age=${timeout?.inSeconds ?? 0}'
|
|
|
|
..['last-modified'] = _formatDateForHttp(modified);
|
|
|
|
|
|
|
|
if (timeout != null) {
|
|
|
|
var expiry = new DateTime.now().add(timeout);
|
|
|
|
res.headers['expires'] = _formatDateForHttp(expiry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _CachedResponse {
|
|
|
|
final Map<String, String> headers;
|
|
|
|
final List<int> body;
|
|
|
|
final DateTime timestamp;
|
|
|
|
|
2018-04-02 01:40:08 +00:00
|
|
|
_CachedResponse(this.headers, this.body, this.timestamp);
|
2018-04-02 01:24:13 +00:00
|
|
|
}
|