add: adding cache package
This commit is contained in:
parent
c1372244c4
commit
fd9e5c32c5
15 changed files with 865 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
|
12
packages/cache/AUTHORS.md
vendored
Normal file
12
packages/cache/AUTHORS.md
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
64
packages/cache/CHANGELOG.md
vendored
Normal file
64
packages/cache/CHANGELOG.md
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Change Log
|
||||
|
||||
## 8.2.0
|
||||
|
||||
* Require Dart >= 3.3
|
||||
* Updated `lints` to 4.0.0
|
||||
|
||||
## 8.1.1
|
||||
|
||||
* Updated repository link
|
||||
|
||||
## 8.1.0
|
||||
|
||||
* Updated `lints` to 3.0.0
|
||||
* Fixed linter warnings
|
||||
|
||||
## 8.0.0
|
||||
|
||||
* Require Dart >= 3.0
|
||||
|
||||
## 7.0.0
|
||||
|
||||
* Require Dart >= 2.17
|
||||
|
||||
## 6.0.0
|
||||
|
||||
* Require Dart >= 2.16
|
||||
|
||||
## 5.0.0
|
||||
|
||||
* Skipped release
|
||||
|
||||
## 4.0.3
|
||||
|
||||
* Updated linter to `package:lints`
|
||||
|
||||
## 4.0.2
|
||||
|
||||
* Updated README
|
||||
* Added home page link
|
||||
* All 7 unit tests passed
|
||||
|
||||
## 4.0.1
|
||||
|
||||
* Updated pubspec description
|
||||
* Fixed: Return `200` with cached data instead of `403`
|
||||
* Updated broken unit tests
|
||||
|
||||
## 4.0.0
|
||||
|
||||
* Migrated to support Dart >= 2.12 NNBD
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Migrated to work with Dart >= 2.12 Non NNBD
|
||||
|
||||
## 2.0.1
|
||||
|
||||
* Add `ignoreQueryAndFragment` to `ResponseCache`.
|
||||
* Rename `CacheService.ignoreQuery` to `ignoreParams`.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* First version
|
29
packages/cache/LICENSE
vendored
Normal file
29
packages/cache/LICENSE
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
91
packages/cache/README.md
vendored
Normal file
91
packages/cache/README.md
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
# Angel3 HTTP Cache
|
||||
|
||||
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/platform_cache?include_prereleases)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Discord](https://img.shields.io/discord/1060322353214660698)](https://discord.gg/3X6bxTUdCM)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/angel)](https://github.com/dart-backend/angel/tree/master/packages/cache/LICENSE)
|
||||
|
||||
A service that provides HTTP caching to the response data for [Angel3 framework](https://pub.dev/packages/angel3).
|
||||
|
||||
## `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 Memcache/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
|
||||
void main() async {
|
||||
var app = Angel()..lazyParseBodies = true;
|
||||
|
||||
app.use(
|
||||
'/api/todos',
|
||||
CacheService(
|
||||
database: AnonymousService(
|
||||
index: ([params]) {
|
||||
print('Fetched directly from the underlying service at ${DateTime.now()}!');
|
||||
return ['foo', 'bar', 'baz'];
|
||||
},
|
||||
read: (id, [params]) {
|
||||
return {id: '$id at ${DateTime.now()}'};
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## `ResponseCache`
|
||||
|
||||
A flexible response cache for Angel3.
|
||||
|
||||
Use this to improve real and perceived response of Web applications, as well as to memorize 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 = ResponseCache();
|
||||
|
||||
// You can also pass an invalidation timeout.
|
||||
var cache = 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',
|
||||
RegExp(r'\.(png|jpg|gif|txt)$'),
|
||||
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 AngelHttpException.forbidden();
|
||||
return cache.purge(req.uri.path);
|
||||
});
|
||||
}
|
||||
```
|
1
packages/cache/analysis_options.yaml
vendored
Normal file
1
packages/cache/analysis_options.yaml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
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:platform_cache/platform_cache.dart';
|
||||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
|
||||
void main() async {
|
||||
var app = Application();
|
||||
|
||||
app.use(
|
||||
'/api/todos',
|
||||
CacheService(
|
||||
cache: MapService(),
|
||||
database: AnonymousService(index: ([params]) {
|
||||
print(
|
||||
'Fetched directly from the underlying service at ${DateTime.now()}!');
|
||||
return ['foo', 'bar', 'baz'];
|
||||
}, read: (dynamic id, [params]) {
|
||||
return {id: '$id at ${DateTime.now()}'};
|
||||
})),
|
||||
);
|
||||
|
||||
var http = PlatformHttp(app);
|
||||
var server = await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening at http://${server.address.address}:${server.port}');
|
||||
}
|
37
packages/cache/example/main.dart
vendored
Normal file
37
packages/cache/example/main.dart
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
import 'package:platform_cache/platform_cache.dart';
|
||||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_foundation/http.dart';
|
||||
|
||||
void main() async {
|
||||
var app = Application();
|
||||
|
||||
// Cache a glob
|
||||
var cache = ResponseCache()
|
||||
..patterns.addAll([
|
||||
RegExp('^/?\\w+\\.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(DateTime.now().toIso8601String()));
|
||||
|
||||
// Support purging the cache.
|
||||
app.addRoute('PURGE', '*', (req, res) {
|
||||
if (req.ip != '127.0.0.1') {
|
||||
throw PlatformHttpException.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 = PlatformHttp(app);
|
||||
var server = await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening at http://${server.address.address}:${server.port}');
|
||||
}
|
3
packages/cache/lib/platform_cache.dart
vendored
Normal file
3
packages/cache/lib/platform_cache.dart
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
export 'src/cache.dart';
|
||||
export 'src/cache_service.dart';
|
||||
export 'src/serializer.dart';
|
203
packages/cache/lib/src/cache.dart
vendored
Normal file
203
packages/cache/lib/src/cache.dart
vendored
Normal file
|
@ -0,0 +1,203 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io' show HttpDate;
|
||||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:pool/pool.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// A flexible response cache for Angel.
|
||||
///
|
||||
/// Use this to improve real and perceived response of Web applications,
|
||||
/// as well as to memorize 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;
|
||||
|
||||
final log = Logger('ResponseCache');
|
||||
|
||||
ResponseCache(
|
||||
{this.timeout = const Duration(minutes: 10),
|
||||
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;
|
||||
}
|
||||
|
||||
var modifiedSince = req.headers?.ifModifiedSince;
|
||||
if (modifiedSince != null) {
|
||||
// Check if there is a cache entry.
|
||||
for (var pattern in patterns) {
|
||||
var reqPath = _getEffectivePath(req);
|
||||
|
||||
if (pattern.allMatches(reqPath).isNotEmpty &&
|
||||
_cache.containsKey(reqPath)) {
|
||||
var response = _cache[reqPath];
|
||||
|
||||
//log.info('timestamp ${response?.timestamp} vs since $modifiedSince');
|
||||
|
||||
if (response != null &&
|
||||
response.timestamp.compareTo(modifiedSince) <= 0) {
|
||||
// If the cache timeout has been met, don't send the cached response.
|
||||
var timeDiff =
|
||||
DateTime.now().toUtc().difference(response.timestamp);
|
||||
|
||||
//log.info(
|
||||
// 'Time Diff: ${timeDiff.inMilliseconds} >= ${timeout.inMilliseconds}');
|
||||
if (timeDiff.inMilliseconds >= timeout.inMilliseconds) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// 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 = DateTime.now().toUtc();
|
||||
|
||||
if (_cache.containsKey(_getEffectivePath(req))) {
|
||||
var response = _cache[_getEffectivePath(req)];
|
||||
|
||||
if (response == null ||
|
||||
now.difference(response.timestamp) >= timeout) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_setCachedHeaders(response.timestamp, req, res);
|
||||
res
|
||||
..headers.addAll(response.headers)
|
||||
..add(response.body);
|
||||
await res.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) {
|
||||
var reqPath = _getEffectivePath(req);
|
||||
|
||||
if (pattern.allMatches(reqPath).isNotEmpty) {
|
||||
var now = DateTime.now().toUtc();
|
||||
|
||||
// Invalidate the response, if need be.
|
||||
if (_cache.containsKey(reqPath)) {
|
||||
// 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[reqPath];
|
||||
if (response == null ||
|
||||
now.difference(response.timestamp) < timeout) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the cache entry should be invalidated, then invalidate it.
|
||||
purge(reqPath);
|
||||
}
|
||||
|
||||
// Save the response.
|
||||
var writeLock = _writeLocks.putIfAbsent(reqPath, () => Pool(1));
|
||||
await writeLock.withResource(() {
|
||||
if (res.buffer != null) {
|
||||
_cache[reqPath] = _CachedResponse(
|
||||
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}'
|
||||
..['last-modified'] = HttpDate.format(modified);
|
||||
|
||||
var expiry = 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);
|
||||
}
|
133
packages/cache/lib/src/cache_service.dart
vendored
Normal file
133
packages/cache/lib/src/cache_service.dart
vendored
Normal file
|
@ -0,0 +1,133 @@
|
|||
import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:platform_foundation/core.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.timeout = const Duration(minutes: 10),
|
||||
this.ignoreParams = false});
|
||||
|
||||
Future<T> _getCached<T>(
|
||||
Map<String, dynamic> params,
|
||||
_CachedItem? Function() get,
|
||||
FutureOr<T> Function() getFresh,
|
||||
FutureOr<T> Function() getCached,
|
||||
FutureOr<T> Function(T data, DateTime now) save) async {
|
||||
var cached = get();
|
||||
var now = DateTime.now().toUtc();
|
||||
|
||||
if (cached != null) {
|
||||
// If the entry has expired, don't send from the cache
|
||||
var expired = now.difference(cached.timestamp) >= timeout;
|
||||
|
||||
if (!expired) {
|
||||
// Read from the cache if necessary
|
||||
var queryEqual = ignoreParams == true ||
|
||||
(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 = _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] = _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 dynamic params;
|
||||
final DateTime timestamp;
|
||||
final Data? data;
|
||||
|
||||
_CachedItem(this.params, this.timestamp, [this.data]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$timestamp:$params:$data';
|
||||
}
|
||||
}
|
29
packages/cache/lib/src/serializer.dart
vendored
Normal file
29
packages/cache/lib/src/serializer.dart
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'dart:async';
|
||||
import 'package:platform_foundation/core.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;
|
||||
|
||||
// TODO: Commented out as it is not doing anything useful
|
||||
var cache = <dynamic, String>{};
|
||||
|
||||
res.serializer = (value) {
|
||||
if (shouldCache == null) {
|
||||
return cache.putIfAbsent(value, () => oldSerializer(value) as String);
|
||||
}
|
||||
|
||||
return oldSerializer(value);
|
||||
};
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
42
packages/cache/pubspec.yaml
vendored
Normal file
42
packages/cache/pubspec.yaml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
name: platform_cache
|
||||
version: 8.2.0
|
||||
description: A service that provides HTTP caching to the response data for Angel3
|
||||
homepage: https://angel3-framework.web.app/
|
||||
repository: https://github.com/dart-backend/angel/tree/master/packages/cache
|
||||
environment:
|
||||
sdk: '>=3.4.0 <4.0.0'
|
||||
dependencies:
|
||||
platform_foundation: ^8.0.0
|
||||
collection: ^1.17.0
|
||||
meta: ^1.9.0
|
||||
pool: ^1.5.0
|
||||
logging: ^1.2.0
|
||||
dev_dependencies:
|
||||
platform_testing: ^8.0.0
|
||||
glob: ^2.0.1
|
||||
http: ^1.0.0
|
||||
test: ^1.24.0
|
||||
lints: ^4.0.0
|
||||
# dependency_overrides:
|
||||
# angel3_container:
|
||||
# path: ../container/angel_container
|
||||
# angel3_framework:
|
||||
# path: ../framework
|
||||
# angel3_http_exception:
|
||||
# path: ../http_exception
|
||||
# angel3_model:
|
||||
# path: ../model
|
||||
# angel3_route:
|
||||
# path: ../route
|
||||
# angel3_mock_request:
|
||||
# path: ../mock_request
|
||||
# angel3_test:
|
||||
# path: ../test
|
||||
# platform_websocket:
|
||||
# path: ../websocket
|
||||
# platform_client:
|
||||
# path: ../client
|
||||
# angel3_auth:
|
||||
# path: ../auth
|
||||
# platform_validation:
|
||||
# path: ../validate
|
134
packages/cache/test/cache_test.dart
vendored
Normal file
134
packages/cache/test/cache_test.dart
vendored
Normal file
|
@ -0,0 +1,134 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:platform_cache/platform_cache.dart';
|
||||
import 'package:platform_foundation/core.dart';
|
||||
import 'package:platform_testing/testing.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
//import 'package:glob/glob.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
print(
|
||||
'${record.time}: ${record.level.name}: ${record.loggerName}: ${record.message}');
|
||||
});
|
||||
|
||||
group('no timeout', () {
|
||||
late TestClient client;
|
||||
DateTime? lastModified;
|
||||
late http.Response response1, response2;
|
||||
|
||||
setUp(() async {
|
||||
var app = Application();
|
||||
var cache = ResponseCache()
|
||||
..patterns.addAll([
|
||||
//Glob('/*.txt'), // Requires to create folders and files for testing
|
||||
RegExp('^/?\\w+\\.txt'),
|
||||
]);
|
||||
|
||||
app.fallback(cache.handleRequest);
|
||||
|
||||
app.get('/date.txt', (req, res) {
|
||||
var data = DateTime.now().toIso8601String();
|
||||
print('Res data: $data');
|
||||
res
|
||||
..useBuffer()
|
||||
..write(data);
|
||||
print('Generate results...');
|
||||
});
|
||||
|
||||
app.addRoute('PURGE', '*', (req, res) {
|
||||
if (req.uri != null) {
|
||||
cache.purge(req.uri!.path);
|
||||
print('Purged ${req.uri!.path}');
|
||||
} else {
|
||||
print('req.uri is null');
|
||||
}
|
||||
});
|
||||
|
||||
app.responseFinalizers.add(cache.responseFinalizer);
|
||||
|
||||
var oldHandler = app.errorHandler;
|
||||
app.errorHandler = (e, req, res) {
|
||||
if (e.error == null) {
|
||||
oldHandler(e, req, res);
|
||||
}
|
||||
return Zone.current
|
||||
.handleUncaughtError(e.error as Object, e.stackTrace!);
|
||||
};
|
||||
|
||||
client = await connectTo(app);
|
||||
response1 = await client.get(Uri.parse('/date.txt'));
|
||||
print('Response 1 status: ${response1.statusCode}');
|
||||
print('Response 1 headers: ${response1.headers}');
|
||||
print('Response 1 body: ${response1.body}');
|
||||
|
||||
response2 = await client.get(Uri.parse('/date.txt'));
|
||||
print('Response 2 status: ${response2.statusCode}');
|
||||
print('Response 2 headers: ${response2.headers}');
|
||||
print('Response 2 body: ${response2.body}');
|
||||
if (response2.headers['last-modified'] == null) {
|
||||
print('last-modified is null');
|
||||
} else {
|
||||
lastModified = HttpDate.parse(response2.headers['last-modified']!);
|
||||
}
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
test('saves content', () async {
|
||||
expect(response2.body, response1.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(Uri.parse('/date.txt'));
|
||||
print('Response after invalidation: ${response.body}');
|
||||
expect(response.body, isNot(response1.body));
|
||||
});
|
||||
|
||||
test('sends 304 on if-modified-since', () async {
|
||||
lastModified ??= DateTime.now();
|
||||
var headers = {
|
||||
'if-modified-since':
|
||||
HttpDate.format(lastModified!.add(const Duration(days: 1)))
|
||||
};
|
||||
var response = await client.get(Uri.parse('/date.txt'), headers: headers);
|
||||
print('Sending headers: $headers');
|
||||
print('Response status: ${response.statusCode})');
|
||||
print('Response headers: ${response.headers}');
|
||||
print('Response body: ${response.body}');
|
||||
//expect(response.statusCode, 304);
|
||||
expect(response.statusCode, 200);
|
||||
});
|
||||
|
||||
test('last-modified in the past', () async {
|
||||
lastModified ??= DateTime.now();
|
||||
var response = await client.get(Uri.parse('/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', () {});
|
||||
}
|
0
packages/cache/test/files/date.txt
vendored
Normal file
0
packages/cache/test/files/date.txt
vendored
Normal file
Loading…
Reference in a new issue