Add 'packages/cache/' from commit 'aea408b2f7bda86f28b7ea8c8f8d2b43f47915e7'
git-subtree-dir: packages/cache git-subtree-mainline:24d8c0515d
git-subtree-split:aea408b2f7
This commit is contained in:
commit
e271a0eafd
21 changed files with 764 additions and 0 deletions
63
packages/cache/.gitignore
vendored
Normal file
63
packages/cache/.gitignore
vendored
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# See https://www.dartlang.org/tools/private-files.html
|
||||||
|
|
||||||
|
# Files and directories created by pub
|
||||||
|
.packages
|
||||||
|
.pub/
|
||||||
|
build/
|
||||||
|
# If you're building an application, you may want to check-in your pubspec.lock
|
||||||
|
pubspec.lock
|
||||||
|
|
||||||
|
# Directory created by dartdoc
|
||||||
|
# If you don't generate documentation locally you can remove this line.
|
||||||
|
doc/api/
|
||||||
|
### JetBrains template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff:
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
|
||||||
|
# Sensitive or high-churn files:
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.xml
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
|
||||||
|
# Gradle:
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-debug/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin:
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
## File-based project format:
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
## Plugin-specific files:
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
.dart_tool
|
14
packages/cache/.idea/cache.iml
vendored
Normal file
14
packages/cache/.idea/cache.iml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||||
|
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||||
|
</component>
|
||||||
|
</module>
|
8
packages/cache/.idea/modules.xml
vendored
Normal file
8
packages/cache/.idea/modules.xml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/cache.iml" filepath="$PROJECT_DIR$/.idea/cache.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
7
packages/cache/.idea/runConfigurations/cache_service_dart.xml
vendored
Normal file
7
packages/cache/.idea/runConfigurations/cache_service_dart.xml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="cache_service.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/example/cache_service.dart" />
|
||||||
|
<option name="workingDirectory" value="$PROJECT_DIR$" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
7
packages/cache/.idea/runConfigurations/main_dart.xml
vendored
Normal file
7
packages/cache/.idea/runConfigurations/main_dart.xml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="main.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/example/main.dart" />
|
||||||
|
<option name="workingDirectory" value="$PROJECT_DIR$" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
8
packages/cache/.idea/runConfigurations/sends_304_on_if_modified_since_in_cache_test_dart.xml
vendored
Normal file
8
packages/cache/.idea/runConfigurations/sends_304_on_if_modified_since_in_cache_test_dart.xml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="sends 304 on if-modified-since in cache_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/test/cache_test.dart" />
|
||||||
|
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
|
||||||
|
<option name="testName" value="sends 304 on if-modified-since" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
6
packages/cache/.idea/runConfigurations/tests_in_cache_test_dart.xml
vendored
Normal file
6
packages/cache/.idea/runConfigurations/tests_in_cache_test_dart.xml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="tests in cache_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/test/cache_test.dart" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
6
packages/cache/.idea/vcs.xml
vendored
Normal file
6
packages/cache/.idea/vcs.xml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
4
packages/cache/.travis.yml
vendored
Normal file
4
packages/cache/.travis.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
language: dart
|
||||||
|
dart:
|
||||||
|
- stable
|
||||||
|
- dev
|
6
packages/cache/CHANGELOG.md
vendored
Normal file
6
packages/cache/CHANGELOG.md
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# 2.0.1
|
||||||
|
* Add `ignoreQueryAndFragment` to `ResponseCache`.
|
||||||
|
* Rename `CacheService.ignoreQuery` to `ignoreParams`.
|
||||||
|
|
||||||
|
# 1.0.0
|
||||||
|
* First version
|
21
packages/cache/LICENSE
vendored
Normal file
21
packages/cache/LICENSE
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 The Angel Framework
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
92
packages/cache/README.md
vendored
Normal file
92
packages/cache/README.md
vendored
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
# cache
|
||||||
|
[](https://pub.dartlang.org/packages/angel_cache)
|
||||||
|
[](https://travis-ci.org/angel-dart/cache)
|
||||||
|
|
||||||
|
Support for server-side caching in [Angel](https://angel-dart.github.io).
|
||||||
|
|
||||||
|
## `CacheService`
|
||||||
|
|
||||||
|
A `Service` class that caches data from one service, storing it in another.
|
||||||
|
An imaginable use case is storing results from MongoDB or another database in
|
||||||
|
MemcacheD/Redis.
|
||||||
|
|
||||||
|
## `cacheSerializationResults`
|
||||||
|
A middleware that enables the caching of response serialization.
|
||||||
|
|
||||||
|
This can improve the performance of sending objects that are complex to serialize.
|
||||||
|
You can pass a [shouldCache] callback to determine which values should be cached.
|
||||||
|
|
||||||
|
```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()}'};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `ResponseCache`
|
||||||
|
A flexible response cache for Angel.
|
||||||
|
|
||||||
|
Use this to improve real and perceived response of Web applications,
|
||||||
|
as well as to memoize expensive responses.
|
||||||
|
|
||||||
|
Supports the `If-Modified-Since` header, as well as storing the contents of
|
||||||
|
response buffers in memory.
|
||||||
|
|
||||||
|
To initialize a simple cache:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future configureServer(Angel app) async {
|
||||||
|
// Simple instance.
|
||||||
|
var cache = new ResponseCache();
|
||||||
|
|
||||||
|
// You can also pass an invalidation timeout.
|
||||||
|
var cache = new ResponseCache(timeout: const Duration(days: 2));
|
||||||
|
|
||||||
|
// Close the cache when the application closes.
|
||||||
|
app.shutdownHooks.add((_) => cache.close());
|
||||||
|
|
||||||
|
// Use `patterns` to specify which resources should be cached.
|
||||||
|
cache.patterns.addAll([
|
||||||
|
'robots.txt',
|
||||||
|
new RegExp(r'\.(png|jpg|gif|txt)$'),
|
||||||
|
new Glob('public/**/*'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// REQUIRED: The middleware that serves cached responses
|
||||||
|
app.use(cache.handleRequest);
|
||||||
|
|
||||||
|
// REQUIRED: The response finalizer that saves responses to the cache
|
||||||
|
app.responseFinalizers.add(cache.responseFinalizer);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Purging the Cache
|
||||||
|
Call `invalidate` to remove a resource from a `ResponseCache`.
|
||||||
|
|
||||||
|
Some servers expect a reverse proxy or caching layer to support `PURGE` requests.
|
||||||
|
If this is your case, make sure to include some sort of validation (maybe IP-based)
|
||||||
|
to ensure no arbitrary attacker can hack your cache:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future configureServer(Angel app) async {
|
||||||
|
app.addRoute('PURGE', '*', (req, res) {
|
||||||
|
if (req.ip != '127.0.0.1')
|
||||||
|
throw new AngelHttpException.forbidden();
|
||||||
|
return cache.purge(req.uri.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
3
packages/cache/analysis_options.yaml
vendored
Normal file
3
packages/cache/analysis_options.yaml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
24
packages/cache/example/cache_service.dart
vendored
Normal file
24
packages/cache/example/cache_service.dart
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:angel_cache/angel_cache.dart';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
var app = new Angel();
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
'/api/todos',
|
||||||
|
new CacheService(
|
||||||
|
cache: new MapService(),
|
||||||
|
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}');
|
||||||
|
}
|
35
packages/cache/example/main.dart
vendored
Normal file
35
packages/cache/example/main.dart
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:angel_cache/angel_cache.dart';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:glob/glob.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
var app = new Angel();
|
||||||
|
|
||||||
|
// Cache a glob
|
||||||
|
var cache = new ResponseCache()
|
||||||
|
..patterns.addAll([
|
||||||
|
new Glob('/*.txt'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Handle `if-modified-since` header, and also send cached content
|
||||||
|
app.fallback(cache.handleRequest);
|
||||||
|
|
||||||
|
// A simple handler that returns a different result every time.
|
||||||
|
app.get('/date.txt',
|
||||||
|
(req, res) => res.write(new DateTime.now().toIso8601String()));
|
||||||
|
|
||||||
|
// Support purging the cache.
|
||||||
|
app.addRoute('PURGE', '*', (req, res) {
|
||||||
|
if (req.ip != '127.0.0.1') throw new AngelHttpException.forbidden();
|
||||||
|
|
||||||
|
cache.purge(req.uri.path);
|
||||||
|
print('Purged ${req.uri.path}');
|
||||||
|
});
|
||||||
|
|
||||||
|
// The response finalizer that actually saves the content
|
||||||
|
app.responseFinalizers.add(cache.responseFinalizer);
|
||||||
|
|
||||||
|
var http = new AngelHttp(app);
|
||||||
|
var server = await http.startServer('127.0.0.1', 3000);
|
||||||
|
print('Listening at http://${server.address.address}:${server.port}');
|
||||||
|
}
|
3
packages/cache/lib/angel_cache.dart
vendored
Normal file
3
packages/cache/lib/angel_cache.dart
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export 'src/cache.dart';
|
||||||
|
export 'src/cache_service.dart';
|
||||||
|
export 'src/serializer.dart';
|
169
packages/cache/lib/src/cache.dart
vendored
Normal file
169
packages/cache/lib/src/cache.dart
vendored
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io' show HttpDate;
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:pool/pool.dart';
|
||||||
|
|
||||||
|
/// A flexible response cache for Angel.
|
||||||
|
///
|
||||||
|
/// Use this to improve real and perceived response of Web applications,
|
||||||
|
/// as well as to memoize expensive responses.
|
||||||
|
class ResponseCache {
|
||||||
|
/// 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.
|
||||||
|
final List<Pattern> patterns = [];
|
||||||
|
|
||||||
|
/// An optional timeout, after which a given response will be removed from the cache, and the contents refreshed.
|
||||||
|
final Duration timeout;
|
||||||
|
|
||||||
|
final Map<String, _CachedResponse> _cache = {};
|
||||||
|
final Map<String, Pool> _writeLocks = {};
|
||||||
|
|
||||||
|
/// If `true` (default: `false`), then caching of results will discard URI query parameters and fragments.
|
||||||
|
final bool ignoreQueryAndFragment;
|
||||||
|
|
||||||
|
ResponseCache({this.timeout, this.ignoreQueryAndFragment: false});
|
||||||
|
|
||||||
|
/// Closes all internal write-locks, and closes the cache.
|
||||||
|
Future close() async {
|
||||||
|
_writeLocks.forEach((_, p) => p.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an entry from the response cache.
|
||||||
|
void purge(String path) => _cache.remove(path);
|
||||||
|
|
||||||
|
/// 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.method != 'GET' && req.method != 'HEAD') return true;
|
||||||
|
|
||||||
|
if (req.headers.ifModifiedSince != null) {
|
||||||
|
var modifiedSince = req.headers.ifModifiedSince;
|
||||||
|
|
||||||
|
// Check if there is a cache entry.
|
||||||
|
for (var pattern in patterns) {
|
||||||
|
if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty &&
|
||||||
|
_cache.containsKey(_getEffectivePath(req))) {
|
||||||
|
var response = _cache[_getEffectivePath(req)];
|
||||||
|
//print('timestamp ${response.timestamp} vs since ${modifiedSince}');
|
||||||
|
|
||||||
|
if (response.timestamp.compareTo(modifiedSince) <= 0) {
|
||||||
|
if (timeout != null) {
|
||||||
|
// If the cache timeout has been met, don't send the cached response.
|
||||||
|
if (new DateTime.now().toUtc().difference(response.timestamp) >=
|
||||||
|
timeout) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.statusCode = 304;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getEffectivePath(RequestContext req) =>
|
||||||
|
ignoreQueryAndFragment == true ? req.uri.path : req.uri.toString();
|
||||||
|
|
||||||
|
/// Serves content from the cache, if applicable.
|
||||||
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
|
||||||
|
if (!await ifModifiedSince(req, res)) return false;
|
||||||
|
if (req.method != 'GET' && req.method != 'HEAD') return true;
|
||||||
|
if (!res.isOpen) return true;
|
||||||
|
|
||||||
|
// Check if there is a cache entry.
|
||||||
|
//
|
||||||
|
// If `if-modified-since` is present, this check has already been performed.
|
||||||
|
if (req.headers.ifModifiedSince == null) {
|
||||||
|
for (var pattern in patterns) {
|
||||||
|
if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty) {
|
||||||
|
var now = new DateTime.now().toUtc();
|
||||||
|
|
||||||
|
if (_cache.containsKey(_getEffectivePath(req))) {
|
||||||
|
var response = _cache[_getEffectivePath(req)];
|
||||||
|
|
||||||
|
if (timeout != null) {
|
||||||
|
// If the cache timeout has been met, don't send the cached response.
|
||||||
|
if (now.difference(response.timestamp) >= timeout) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setCachedHeaders(response.timestamp, req, res);
|
||||||
|
res
|
||||||
|
..headers.addAll(response.headers)
|
||||||
|
..add(response.body)
|
||||||
|
..close();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
_setCachedHeaders(now, req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A response finalizer that saves responses to the cache.
|
||||||
|
Future<bool> responseFinalizer(
|
||||||
|
RequestContext req, ResponseContext res) async {
|
||||||
|
if (res.statusCode == 304) return true;
|
||||||
|
if (req.method != 'GET' && req.method != 'HEAD') return true;
|
||||||
|
|
||||||
|
// Check if there is a cache entry.
|
||||||
|
for (var pattern in patterns) {
|
||||||
|
if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty) {
|
||||||
|
var now = new DateTime.now().toUtc();
|
||||||
|
|
||||||
|
// Invalidate the response, if need be.
|
||||||
|
if (_cache.containsKey(_getEffectivePath(req))) {
|
||||||
|
// 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[_getEffectivePath(req)];
|
||||||
|
if (now.difference(response.timestamp) < timeout) return true;
|
||||||
|
|
||||||
|
// If the cache entry should be invalidated, then invalidate it.
|
||||||
|
purge(_getEffectivePath(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the response.
|
||||||
|
var writeLock =
|
||||||
|
_writeLocks.putIfAbsent(_getEffectivePath(req), () => new Pool(1));
|
||||||
|
await writeLock.withResource(() {
|
||||||
|
_cache[_getEffectivePath(req)] = new _CachedResponse(
|
||||||
|
new Map.from(res.headers), res.buffer.toBytes(), now);
|
||||||
|
});
|
||||||
|
|
||||||
|
_setCachedHeaders(now, req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setCachedHeaders(
|
||||||
|
DateTime modified, RequestContext req, ResponseContext res) {
|
||||||
|
var privacy = 'public';
|
||||||
|
|
||||||
|
res.headers
|
||||||
|
..['cache-control'] = '$privacy, max-age=${timeout?.inSeconds ?? 86400}'
|
||||||
|
..['last-modified'] = HttpDate.format(modified);
|
||||||
|
|
||||||
|
if (timeout != null) {
|
||||||
|
var expiry = new DateTime.now().add(timeout);
|
||||||
|
res.headers['expires'] = HttpDate.format(expiry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CachedResponse {
|
||||||
|
final Map<String, String> headers;
|
||||||
|
final List<int> body;
|
||||||
|
final DateTime timestamp;
|
||||||
|
|
||||||
|
_CachedResponse(this.headers, this.body, this.timestamp);
|
||||||
|
}
|
138
packages/cache/lib/src/cache_service.dart
vendored
Normal file
138
packages/cache/lib/src/cache_service.dart
vendored
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
/// An Angel [Service] that caches data from another service.
|
||||||
|
///
|
||||||
|
/// This is useful for applications of scale, where network latency
|
||||||
|
/// can have real implications on application performance.
|
||||||
|
class CacheService<Id, Data> extends Service<Id, Data> {
|
||||||
|
/// The underlying [Service] that represents the original data store.
|
||||||
|
final Service<Id, Data> database;
|
||||||
|
|
||||||
|
/// The [Service] used to interface with a caching layer.
|
||||||
|
///
|
||||||
|
/// If not provided, this defaults to a [MapService].
|
||||||
|
final Service<Id, Data> cache;
|
||||||
|
|
||||||
|
/// If `true` (default: `false`), then result caching will discard parameters passed to service methods.
|
||||||
|
///
|
||||||
|
/// If you want to return a cached result more-often-than-not, you may want to enable this.
|
||||||
|
final bool ignoreParams;
|
||||||
|
|
||||||
|
final Duration timeout;
|
||||||
|
|
||||||
|
final Map<Id, _CachedItem<Data>> _cache = {};
|
||||||
|
_CachedItem<List<Data>> _indexed;
|
||||||
|
|
||||||
|
CacheService(
|
||||||
|
{@required this.database,
|
||||||
|
@required this.cache,
|
||||||
|
this.ignoreParams: false,
|
||||||
|
this.timeout}) {
|
||||||
|
assert(database != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _getCached<T>(
|
||||||
|
Map<String, dynamic> params,
|
||||||
|
_CachedItem get(),
|
||||||
|
FutureOr<T> getFresh(),
|
||||||
|
FutureOr<T> getCached(),
|
||||||
|
FutureOr<T> save(T data, DateTime now)) async {
|
||||||
|
var cached = get();
|
||||||
|
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 = ignoreParams == true ||
|
||||||
|
(params != null &&
|
||||||
|
cached.params != null &&
|
||||||
|
const MapEquality().equals(
|
||||||
|
params['query'] as Map, cached.params['query'] as Map));
|
||||||
|
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<List<Data>> index([Map<String, dynamic> params]) {
|
||||||
|
return _getCached(
|
||||||
|
params,
|
||||||
|
() => _indexed,
|
||||||
|
() => database.index(params),
|
||||||
|
() => _indexed?.data ?? [],
|
||||||
|
(data, now) async {
|
||||||
|
_indexed = new _CachedItem(params, now, data);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Data> read(Id id, [Map<String, dynamic> params]) async {
|
||||||
|
return _getCached<Data>(
|
||||||
|
params,
|
||||||
|
() => _cache[id],
|
||||||
|
() => database.read(id, params),
|
||||||
|
() => cache.read(id),
|
||||||
|
(data, now) async {
|
||||||
|
_cache[id] = new _CachedItem(params, now, data);
|
||||||
|
return await cache.modify(id, data);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Data> create(data, [Map<String, dynamic> params]) {
|
||||||
|
_indexed = null;
|
||||||
|
return database.create(data, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]) {
|
||||||
|
_indexed = null;
|
||||||
|
_cache.remove(id);
|
||||||
|
return database.modify(id, data, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Data> update(Id id, Data data, [Map<String, dynamic> params]) {
|
||||||
|
_indexed = null;
|
||||||
|
_cache.remove(id);
|
||||||
|
return database.modify(id, data, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Data> remove(Id id, [Map<String, dynamic> params]) {
|
||||||
|
_indexed = null;
|
||||||
|
_cache.remove(id);
|
||||||
|
return database.remove(id, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CachedItem<Data> {
|
||||||
|
final params;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final Data data;
|
||||||
|
|
||||||
|
_CachedItem(this.params, this.timestamp, [this.data]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '$timestamp:$params:$data';
|
||||||
|
}
|
||||||
|
}
|
26
packages/cache/lib/src/serializer.dart
vendored
Normal file
26
packages/cache/lib/src/serializer.dart
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
|
||||||
|
/// A middleware that enables the caching of response serialization.
|
||||||
|
///
|
||||||
|
/// This can improve the performance of sending objects that are complex to serialize.
|
||||||
|
///
|
||||||
|
/// You can pass a [shouldCache] callback to determine which values should be cached.
|
||||||
|
RequestHandler cacheSerializationResults(
|
||||||
|
{Duration timeout,
|
||||||
|
FutureOr<bool> Function(RequestContext, ResponseContext, Object)
|
||||||
|
shouldCache}) {
|
||||||
|
return (RequestContext req, ResponseContext res) async {
|
||||||
|
var oldSerializer = res.serializer;
|
||||||
|
var cache = <dynamic, String>{};
|
||||||
|
res.serializer = (value) {
|
||||||
|
if (shouldCache == null) {
|
||||||
|
return cache.putIfAbsent(value, () => oldSerializer(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldSerializer(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
17
packages/cache/pubspec.yaml
vendored
Normal file
17
packages/cache/pubspec.yaml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: angel_cache
|
||||||
|
version: 2.0.1
|
||||||
|
homepage: https://github.com/angel-dart/cache
|
||||||
|
description: Support for server-side caching in Angel.
|
||||||
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.0.0-dev <3.0.0"
|
||||||
|
dependencies:
|
||||||
|
angel_framework: ^2.0.0-alpha
|
||||||
|
collection: ^1.0.0
|
||||||
|
meta: ^1.0.0
|
||||||
|
pool: ^1.0.0
|
||||||
|
dev_dependencies:
|
||||||
|
angel_test: ^2.0.0-alpha
|
||||||
|
glob: ^1.0.0
|
||||||
|
http: any
|
||||||
|
test: ^1.0.0
|
107
packages/cache/test/cache_test.dart
vendored
Normal file
107
packages/cache/test/cache_test.dart
vendored
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:angel_cache/angel_cache.dart';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_test/angel_test.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:glob/glob.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
group('no timeout', () {
|
||||||
|
TestClient client;
|
||||||
|
DateTime lastModified;
|
||||||
|
http.Response response1, response2;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
var app = new Angel();
|
||||||
|
var cache = new ResponseCache()
|
||||||
|
..patterns.addAll([
|
||||||
|
new Glob('/*.txt'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
app.fallback(cache.handleRequest);
|
||||||
|
|
||||||
|
app.get('/date.txt', (req, res) {
|
||||||
|
res
|
||||||
|
..useBuffer()
|
||||||
|
..write(new DateTime.now().toIso8601String());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.addRoute('PURGE', '*', (req, res) {
|
||||||
|
cache.purge(req.uri.path);
|
||||||
|
print('Purged ${req.uri.path}');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.responseFinalizers.add(cache.responseFinalizer);
|
||||||
|
|
||||||
|
var oldHandler = app.errorHandler;
|
||||||
|
app.errorHandler = (e, req, res) {
|
||||||
|
if (e.error == null) return oldHandler(e, req, res);
|
||||||
|
return Zone.current.handleUncaughtError(e.error, e.stackTrace);
|
||||||
|
};
|
||||||
|
|
||||||
|
client = await connectTo(app);
|
||||||
|
response1 = await client.get('/date.txt');
|
||||||
|
response2 = await client.get('/date.txt');
|
||||||
|
print(response2.headers);
|
||||||
|
lastModified = HttpDate.parse(response2.headers['last-modified']);
|
||||||
|
print('Response 1 status: ${response1.statusCode}');
|
||||||
|
print('Response 2 status: ${response2.statusCode}');
|
||||||
|
print('Response 1 body: ${response1.body}');
|
||||||
|
print('Response 2 body: ${response2.body}');
|
||||||
|
print('Response 1 headers: ${response1.headers}');
|
||||||
|
print('Response 2 headers: ${response2.headers}');
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() => client.close());
|
||||||
|
|
||||||
|
test('saves content', () async {
|
||||||
|
expect(response1.body, response2.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saves headers', () async {
|
||||||
|
response1.headers.forEach((k, v) {
|
||||||
|
expect(response2.headers, containsPair(k, v));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('first response is normal', () {
|
||||||
|
expect(response1.statusCode, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends last-modified', () {
|
||||||
|
expect(response2.headers.keys, contains('last-modified'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalidate', () async {
|
||||||
|
await client.sendUnstreamed('PURGE', '/date.txt', {});
|
||||||
|
var response = await client.get('/date.txt');
|
||||||
|
print('Response after invalidation: ${response.body}');
|
||||||
|
expect(response.body, isNot(response1.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends 304 on if-modified-since', () async {
|
||||||
|
var headers = {
|
||||||
|
'if-modified-since':
|
||||||
|
HttpDate.format(lastModified.add(const Duration(days: 1)))
|
||||||
|
};
|
||||||
|
var response = await client.get('/date.txt', headers: headers);
|
||||||
|
print('Sending headers: $headers');
|
||||||
|
print('Response (${response.statusCode}): ${response.headers}');
|
||||||
|
expect(response.statusCode, 304);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('last-modified in the past', () async {
|
||||||
|
var response = await client.get('/date.txt', headers: {
|
||||||
|
'if-modified-since':
|
||||||
|
HttpDate.format(lastModified.subtract(const Duration(days: 10)))
|
||||||
|
});
|
||||||
|
print('Response: ${response.body}');
|
||||||
|
expect(response.statusCode, 200);
|
||||||
|
expect(response.body, isNot(response1.body));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('with timeout', () {});
|
||||||
|
}
|
Loading…
Reference in a new issue