platform/packages/cache/lib/src/cache.dart

204 lines
6.4 KiB
Dart
Raw Normal View History

2018-04-02 01:05:35 +00:00
import 'dart:async';
2018-10-21 09:35:04 +00:00
import 'dart:io' show HttpDate;
import 'package:angel3_framework/angel3_framework.dart';
2018-04-02 01:37:34 +00:00
import 'package:pool/pool.dart';
2021-06-26 10:02:41 +00:00
import 'package:logging/logging.dart';
2018-04-02 01:05:35 +00:00
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,
2022-01-23 05:10:50 +00:00
/// as well as to memorize 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.
2021-06-22 10:42:26 +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
2018-11-10 16:59:07 +00:00
/// If `true` (default: `false`), then caching of results will discard URI query parameters and fragments.
final bool ignoreQueryAndFragment;
2021-06-26 10:02:41 +00:00
final log = Logger('ResponseCache');
2021-06-22 10:42:26 +00:00
ResponseCache(
{this.timeout = const Duration(minutes: 10),
this.ignoreQueryAndFragment = false});
2018-04-02 01:05:35 +00:00
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 {
2021-06-26 10:02:41 +00:00
if (req.method != 'GET' && req.method != 'HEAD') {
return true;
}
2018-04-02 01:05:35 +00:00
2021-06-26 10:02:41 +00:00
var modifiedSince = req.headers?.ifModifiedSince;
if (modifiedSince != null) {
2018-04-02 01:24:13 +00:00
// Check if there is a cache entry.
for (var pattern in patterns) {
2021-06-26 10:02:41 +00:00
var reqPath = _getEffectivePath(req);
if (pattern.allMatches(reqPath).isNotEmpty &&
_cache.containsKey(reqPath)) {
var response = _cache[reqPath];
//log.info('timestamp ${response?.timestamp} vs since $modifiedSince');
2018-04-02 02:43:00 +00:00
2021-06-26 10:02:41 +00:00
if (response != null &&
response.timestamp.compareTo(modifiedSince) <= 0) {
2021-06-22 10:42:26 +00:00
// If the cache timeout has been met, don't send the cached response.
2021-06-26 10:02:41 +00:00
var timeDiff =
DateTime.now().toUtc().difference(response.timestamp);
//log.info(
// 'Time Diff: ${timeDiff.inMilliseconds} >= ${timeout.inMilliseconds}');
if (timeDiff.inMilliseconds >= timeout.inMilliseconds) {
2021-06-22 10:42:26 +00:00
return true;
2018-04-02 02:43:00 +00:00
}
2018-04-02 01:05:35 +00:00
2021-06-26 10:02:41 +00:00
// Old code: res.statusCode = 304;
// Return the response stored in the cache
_setCachedHeaders(response.timestamp, req, res);
res
..headers.addAll(response.headers)
..add(response.body);
await res.close();
2018-04-02 01:24:13 +00:00
return false;
}
2018-04-02 01:05:35 +00:00
}
}
}
return true;
}
2021-06-26 10:02:41 +00:00
String _getEffectivePath(RequestContext req) {
if (req.uri == null) {
log.severe('Request URI is null');
throw ArgumentError('Request URI is null');
}
return ignoreQueryAndFragment == true ? req.uri!.path : req.uri.toString();
}
2018-11-10 16:59:07 +00:00
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 02:29:36 +00:00
if (!await ifModifiedSince(req, res)) return false;
2018-04-02 02:30:21 +00:00
if (req.method != 'GET' && req.method != 'HEAD') return true;
2018-04-02 02:50:13 +00:00
if (!res.isOpen) return true;
2018-04-02 01:45:45 +00:00
// Check if there is a cache entry.
2018-04-02 02:43:00 +00:00
//
// If `if-modified-since` is present, this check has already been performed.
2021-06-22 10:42:26 +00:00
if (req.headers?.ifModifiedSince == null) {
2018-04-02 02:43:00 +00:00
for (var pattern in patterns) {
2018-11-10 16:59:07 +00:00
if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty) {
2021-06-22 10:42:26 +00:00
var now = DateTime.now().toUtc();
2018-04-02 02:43:00 +00:00
2018-11-10 16:59:07 +00:00
if (_cache.containsKey(_getEffectivePath(req))) {
var response = _cache[_getEffectivePath(req)];
2018-04-02 02:43:00 +00:00
2021-06-22 10:42:26 +00:00
if (response == null ||
now.difference(response.timestamp) >= timeout) {
return true;
2018-04-02 02:43:00 +00:00
}
2021-06-22 10:42:26 +00:00
_setCachedHeaders(response.timestamp, req, res);
2018-04-02 02:43:00 +00:00
res
..headers.addAll(response.headers)
2021-06-22 10:42:26 +00:00
..add(response.body);
await res.close();
2018-04-02 02:43:00 +00:00
return false;
2018-10-21 09:35:04 +00:00
} else {
_setCachedHeaders(now, req, res);
2018-04-02 02:29:36 +00:00
}
2018-04-02 01:45:45 +00:00
}
}
}
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 {
2021-06-22 10:42:26 +00:00
if (res.statusCode == 304) {
return true;
}
2021-06-26 10:02:41 +00:00
2021-06-22 10:42:26 +00:00
if (req.method != 'GET' && req.method != 'HEAD') {
return true;
}
2018-04-02 01:24:13 +00:00
2018-04-02 01:37:34 +00:00
// Check if there is a cache entry.
for (var pattern in patterns) {
2021-06-26 10:02:41 +00:00
var reqPath = _getEffectivePath(req);
if (pattern.allMatches(reqPath).isNotEmpty) {
2021-06-22 10:42:26 +00:00
var now = DateTime.now().toUtc();
2018-04-02 01:37:34 +00:00
// Invalidate the response, if need be.
2021-06-26 10:02:41 +00:00
if (_cache.containsKey(reqPath)) {
2018-04-02 01:37:34 +00:00
// If there is no timeout, don't invalidate.
2021-06-22 10:42:26 +00:00
//if (timeout == null) return true;
2018-04-02 01:37:34 +00:00
// Otherwise, don't invalidate unless the timeout has been exceeded.
2021-06-26 10:02:41 +00:00
var response = _cache[reqPath];
2021-06-22 10:42:26 +00:00
if (response == null ||
now.difference(response.timestamp) < timeout) {
return true;
}
2018-04-02 01:37:34 +00:00
// If the cache entry should be invalidated, then invalidate it.
2021-06-26 10:02:41 +00:00
purge(reqPath);
2018-04-02 01:37:34 +00:00
}
2018-04-02 01:40:08 +00:00
// Save the response.
2021-06-26 10:02:41 +00:00
var writeLock = _writeLocks.putIfAbsent(reqPath, () => Pool(1));
2018-04-02 01:50:38 +00:00
await writeLock.withResource(() {
2021-06-26 10:02:41 +00:00
if (res.buffer != null) {
_cache[reqPath] = _CachedResponse(
Map.from(res.headers), res.buffer!.toBytes(), now);
}
2018-04-02 01:50:38 +00:00
});
2018-10-21 09:35:04 +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
2021-06-22 10:42:26 +00:00
..['cache-control'] = '$privacy, max-age=${timeout.inSeconds}'
2018-10-21 09:35:04 +00:00
..['last-modified'] = HttpDate.format(modified);
2018-04-02 01:05:35 +00:00
2021-06-22 10:42:26 +00:00
var expiry = DateTime.now().add(timeout);
res.headers['expires'] = HttpDate.format(expiry);
2018-04-02 01:05:35 +00:00
}
}
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
}