This commit is contained in:
Tobe O 2017-11-28 13:14:50 -05:00
parent 2c846ae449
commit 2d87631e8d
19 changed files with 808 additions and 586 deletions

View file

@ -26,14 +26,14 @@
<entry key="angel_route"> <entry key="angel_route">
<value> <value>
<list> <list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-1.0.8/lib" /> <option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-2.0.3+2/lib" />
</list> </list>
</value> </value>
</entry> </entry>
<entry key="args"> <entry key="args">
<value> <value>
<list> <list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.0.2/lib" /> <option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.1.0/lib" />
</list> </list>
</value> </value>
</entry> </entry>
@ -86,6 +86,13 @@
</list> </list>
</value> </value>
</entry> </entry>
<entry key="combinator">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/combinator-1.0.0-beta+7/lib" />
</list>
</value>
</entry>
<entry key="container"> <entry key="container">
<value> <value>
<list> <list>
@ -114,13 +121,6 @@
</list> </list>
</value> </value>
</entry> </entry>
<entry key="flatten">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/flatten-1.0.0/lib" />
</list>
</value>
</entry>
<entry key="front_end"> <entry key="front_end">
<value> <value>
<list> <list>
@ -271,7 +271,7 @@
<entry key="path"> <entry key="path">
<value> <value>
<list> <list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.5.0/lib" /> <option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.5.1/lib" />
</list> </list>
</value> </value>
</entry> </entry>
@ -442,8 +442,8 @@
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/analyzer-0.30.0+4/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/analyzer-0.30.0+4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_http_exception-1.0.0/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_http_exception-1.0.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_model-1.0.0/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_model-1.0.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-1.0.8/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-2.0.3+2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.0.2/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.1.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/async-1.13.3/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/async-1.13.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/barback-0.15.2+13/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/barback-0.15.2+13/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/body_parser-1.0.3/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/body_parser-1.0.3/lib" />
@ -451,11 +451,11 @@
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/charcode-1.1.1/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/charcode-1.1.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/cli_util-0.1.2+1/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/cli_util-0.1.2+1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/collection-1.14.3/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/collection-1.14.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/combinator-1.0.0-beta+7/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/container-0.1.2/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/container-0.1.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/convert-2.0.1/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/convert-2.0.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/crypto-2.0.2+1/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/crypto-2.0.2+1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/csslib-0.14.1/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/csslib-0.14.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/flatten-1.0.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/front_end-0.1.0-alpha.4.1/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/front_end-0.1.0-alpha.4.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/glob-1.1.5/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/glob-1.1.5/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/html-0.13.2/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/html-0.13.2/lib" />
@ -477,7 +477,7 @@
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/node_preamble-1.4.0/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/node_preamble-1.4.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_config-1.0.3/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_config-1.0.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_resolver-1.0.2/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_resolver-1.0.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.5.0/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.5.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/plugin-0.2.0+2/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/plugin-0.2.0+2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pool-1.3.3/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pool-1.3.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pub_semver-1.3.2/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pub_semver-1.3.2/lib" />

View file

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="only match route with matching method in routing_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="envs">
<entry key="ANGEL_ENV" value="production" />
</option>
<option name="filePath" value="$PROJECT_DIR$/test/routing_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="only match route with matching method" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in framework" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,15 @@
# 1.1.0-alpha+9
* Fixed a bug that prevented `isProduction` from ever returning `true`.
* This enabled caching, which greatly improved performance.
* Requests no longer have independent zones, which greatly improved performance.
* `FormatException`, when caught, is automatically transformed in a `400` error response.
* Added `extension` to `RequestContext`.
* Added `strict` to `RequestContext#accepts`.
* Added a `toString` override for the `Providers` class.
* Returned to `RegExp` for stripping stray slashes.
* The request path is now only parsed once.
* Optimized the parsing of the `ACCEPT_ENCODING` header.
# 1.1.0-alpha+8 # 1.1.0-alpha+8
* Added an `autoIdAndDateFields` flag to `MapService`. Finally. * Added an `autoIdAndDateFields` flag to `MapService`. Finally.

View file

@ -4,7 +4,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:mirrors'; import 'dart:mirrors';
import 'package:body_parser/body_parser.dart'; import 'package:body_parser/body_parser.dart';
import 'package:charcode/charcode.dart'; import 'package:path/path.dart' as p;
import 'metadata.dart'; import 'metadata.dart';
import 'response_context.dart'; import 'response_context.dart';
import 'routable.dart'; import 'routable.dart';
@ -13,7 +13,7 @@ part 'injection.dart';
/// A convenience wrapper around an incoming HTTP request. /// A convenience wrapper around an incoming HTTP request.
class RequestContext { class RequestContext {
String _acceptHeaderCache; String _acceptHeaderCache, _extensionCache;
bool _acceptsAllCache; bool _acceptsAllCache;
BodyParseResult _body; BodyParseResult _body;
ContentType _contentType; ContentType _contentType;
@ -39,9 +39,10 @@ class RequestContext {
String get hostname => io.headers.value(HttpHeaders.HOST); String get hostname => io.headers.value(HttpHeaders.HOST);
final Map _injections = {}; final Map _injections = {};
Map _injectionsCache;
/// A [Map] of singletons injected via [inject]. *Read-only*. /// A [Map] of singletons injected via [inject]. *Read-only*.
Map get injections => new Map.unmodifiable(_injections); Map get injections => _injectionsCache ??= new Map.unmodifiable(_injections);
/// The underlying [HttpRequest] instance underneath this context. /// The underlying [HttpRequest] instance underneath this context.
HttpRequest get io => _io; HttpRequest get io => _io;
@ -135,8 +136,13 @@ class RequestContext {
io.headers.value("X-Requested-With")?.trim()?.toLowerCase() == io.headers.value("X-Requested-With")?.trim()?.toLowerCase() ==
'xmlhttprequest'; 'xmlhttprequest';
/// Returns the file extension of the requested path, if any.
///
/// Includes the leading `.`, if there is one.
String get extension => _extensionCache ??= p.extension(uri.path);
/// Magically transforms an [HttpRequest] into a [RequestContext]. /// Magically transforms an [HttpRequest] into a [RequestContext].
static Future<RequestContext> from(HttpRequest request, Angel app) async { static Future<RequestContext> from(HttpRequest request, Angel app, String path) async {
RequestContext ctx = new RequestContext(); RequestContext ctx = new RequestContext();
String override = request.method; String override = request.method;
@ -150,6 +156,7 @@ class RequestContext {
ctx._contentType = request.headers.contentType; ctx._contentType = request.headers.contentType;
ctx._override = override; ctx._override = override;
/*
// Faster way to get path // Faster way to get path
List<int> _path = []; List<int> _path = [];
@ -175,6 +182,9 @@ class RequestContext {
ctx._path = new String.fromCharCodes(_path.take(lastSlash)); ctx._path = new String.fromCharCodes(_path.take(lastSlash));
else else
ctx._path = new String.fromCharCodes(_path); ctx._path = new String.fromCharCodes(_path);
*/
ctx._path = path;
ctx._io = request; ctx._io = request;
if (app.lazyParseBodies != true) { if (app.lazyParseBodies != true) {
@ -219,17 +229,19 @@ class RequestContext {
} }
_injections[type] = value; _injections[type] = value;
_injectionsCache = null;
} }
/// Returns `true` if the client's `Accept` header indicates that the given [contentType] is considered a valid response. /// Returns `true` if the client's `Accept` header indicates that the given [contentType] is considered a valid response.
/// ///
/// You cannot provide a `null` [contentType]. /// You cannot provide a `null` [contentType].
/// If the `Accept` header's value is `*/*`, this method will always return `true`. /// If the `Accept` header's value is `*/*`, this method will always return `true`.
/// To ignore the wildcard (`*/*`), pass [strict] as `true`.
/// ///
/// [contentType] can be either of the following: /// [contentType] can be either of the following:
/// * A [ContentType], in which case the `Accept` header will be compared against its `mimeType` property. /// * A [ContentType], in which case the `Accept` header will be compared against its `mimeType` property.
/// * Any other Dart value, in which case the `Accept` header will be compared against the result of a `toString()` call. /// * Any other Dart value, in which case the `Accept` header will be compared against the result of a `toString()` call.
bool accepts(contentType) { bool accepts(contentType, {bool strict: false}) {
var contentTypeString = contentType is ContentType var contentTypeString = contentType is ContentType
? contentType.mimeType ? contentType.mimeType
: contentType?.toString(); : contentType?.toString();
@ -242,7 +254,7 @@ class RequestContext {
if (_acceptHeaderCache == null) if (_acceptHeaderCache == null)
return false; return false;
else if (_acceptHeaderCache.contains('*/*')) else if (strict != true && _acceptHeaderCache.contains('*/*'))
return true; return true;
else else
return _acceptHeaderCache.contains(contentTypeString); return _acceptHeaderCache.contains(contentTypeString);

View file

@ -6,6 +6,7 @@ import 'dart:io';
import 'package:angel_route/angel_route.dart'; import 'package:angel_route/angel_route.dart';
import 'package:json_god/json_god.dart' as god; import 'package:json_god/json_god.dart' as god;
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:pool/pool.dart';
import 'server.dart' show Angel; import 'server.dart' show Angel;
import 'controller.dart'; import 'controller.dart';
import 'request_context.dart'; import 'request_context.dart';
@ -337,6 +338,31 @@ class ResponseContext implements StreamSink<List<int>>, StringSink {
buffer.add(data); buffer.add(data);
} }
/// Configure the response to write directly to the output stream, instead of buffering.
bool useStream() {
if (!_useStream) {
// If this is the first stream added to this response,
// then add headers, status code, etc.
io
..statusCode = statusCode
..cookies.addAll(cookies);
headers.forEach(io.headers.set);
willCloseItself = _useStream = _isClosed = true;
if (_correspondingRequest?.injections?.containsKey(Stopwatch) == true) {
(_correspondingRequest.injections[Stopwatch] as Stopwatch).stop();
}
if (_correspondingRequest?.injections?.containsKey(PoolResource) == true) {
(_correspondingRequest.injections[PoolResource] as PoolResource).release();
}
return true;
}
return false;
}
/// Adds a stream directly the underlying dart:[io] response. /// Adds a stream directly the underlying dart:[io] response.
/// ///
/// This will also set [willCloseItself] to `true`, thus canceling out response finalizers. /// This will also set [willCloseItself] to `true`, thus canceling out response finalizers.
@ -346,24 +372,10 @@ class ResponseContext implements StreamSink<List<int>>, StringSink {
@override @override
Future addStream(Stream<List<int>> stream) { Future addStream(Stream<List<int>> stream) {
if (_isClosed && !_useStream) throw _closed(); if (_isClosed && !_useStream) throw _closed();
bool firstStream = _useStream == false; var firstStream = useStream();
willCloseItself = _useStream = _isClosed = true;
if (_correspondingRequest?.injections?.containsKey(Stopwatch) == true) {
(_correspondingRequest.injections[Stopwatch] as Stopwatch).stop();
}
Stream<List<int>> output = stream; Stream<List<int>> output = stream;
if (firstStream) {
// If this is the first stream added to this response,
// then add headers, status code, etc.
io
..statusCode = statusCode
..cookies.addAll(cookies);
headers.forEach(io.headers.set);
}
if (encoders.isNotEmpty && correspondingRequest != null) { if (encoders.isNotEmpty && correspondingRequest != null) {
var allowedEncodings = var allowedEncodings =
(correspondingRequest.headers[HttpHeaders.ACCEPT_ENCODING] ?? []) (correspondingRequest.headers[HttpHeaders.ACCEPT_ENCODING] ?? [])

View file

@ -64,6 +64,7 @@ class Routable extends Router {
/// Assigns a middleware to a name for convenience. /// Assigns a middleware to a name for convenience.
@override @override
registerMiddleware(String name, @checked RequestHandler middleware) => registerMiddleware(String name, @checked RequestHandler middleware) =>
// ignore: deprecated_member_use
super.registerMiddleware(name, middleware); super.registerMiddleware(name, middleware);
/// Retrieves the service assigned to the given path. /// Retrieves the service assigned to the given path.

View file

@ -1,16 +1,17 @@
library angel_framework.http.server; library angel_framework.http.server;
import 'dart:async'; import 'dart:async';
import 'dart:collection' show HashMap;
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:angel_route/angel_route.dart' hide Extensible; import 'package:angel_route/angel_route.dart';
import 'package:charcode/charcode.dart'; import 'package:combinator/combinator.dart';
export 'package:container/container.dart'; export 'package:container/container.dart';
import 'package:flatten/flatten.dart';
import 'package:json_god/json_god.dart' as god; import 'package:json_god/json_god.dart' as god;
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:pool/pool.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'angel_base.dart'; import 'angel_base.dart';
import 'controller.dart'; import 'controller.dart';
@ -30,11 +31,13 @@ typedef Future AngelConfigurer(Angel app);
/// A powerful real-time/REST/MVC server class. /// A powerful real-time/REST/MVC server class.
class Angel extends AngelBase { class Angel extends AngelBase {
final List<Angel> _children = []; final List<Angel> _children = [];
final Map<String, Tuple3<List, Map, Match>> handlerCache = {}; final Map<String, Tuple3<List, Map, ParseResult<Map<String, String>>>>
handlerCache = new HashMap();
Router _flattened; Router _flattened;
bool _isProduction = false; bool _isProduction;
Angel _parent; Angel _parent;
Pool _pool;
StreamSubscription<HttpRequest> _sub; StreamSubscription<HttpRequest> _sub;
ServerGenerator _serverGenerator = HttpServer.bind; ServerGenerator _serverGenerator = HttpServer.bind;
@ -75,7 +78,8 @@ class Angel extends AngelBase {
/// This value is memoized the first time you call it, so do not change environment /// This value is memoized the first time you call it, so do not change environment
/// configuration at runtime! /// configuration at runtime!
bool get isProduction { bool get isProduction {
return _isProduction ??= (Platform.environment['ANGEL_ENV'] == 'production'); return _isProduction ??=
(Platform.environment['ANGEL_ENV'] == 'production');
} }
/// The function used to bind this instance to an HTTP server. /// The function used to bind this instance to an HTTP server.
@ -237,7 +241,7 @@ class Angel extends AngelBase {
String tab: ' ', String tab: ' ',
bool showMatchers: false}) { bool showMatchers: false}) {
if (isProduction) { if (isProduction) {
if (_flattened == null) _flattened = flatten(this); _flattened ??= flatten(this);
_flattened.dumpTree( _flattened.dumpTree(
callback: callback, callback: callback,
@ -246,8 +250,7 @@ class Angel extends AngelBase {
: (isProduction : (isProduction
? 'Dumping flattened route tree:' ? 'Dumping flattened route tree:'
: 'Dumping route tree:'), : 'Dumping route tree:'),
tab: tab ?? ' ', tab: tab ?? ' ');
showMatchers: showMatchers == true);
} else { } else {
super.dumpTree( super.dumpTree(
callback: callback, callback: callback,
@ -256,8 +259,7 @@ class Angel extends AngelBase {
: (isProduction : (isProduction
? 'Dumping flattened route tree:' ? 'Dumping flattened route tree:'
: 'Dumping route tree:'), : 'Dumping route tree:'),
tab: tab ?? ' ', tab: tab ?? ' ');
showMatchers: showMatchers == true);
} }
} }
@ -308,7 +310,6 @@ class Angel extends AngelBase {
/// Runs some [handler]. Returns `true` if request execution should continue. /// Runs some [handler]. Returns `true` if request execution should continue.
Future<bool> executeHandler( Future<bool> executeHandler(
handler, RequestContext req, ResponseContext res) async { handler, RequestContext req, ResponseContext res) async {
if (handler == null) return false;
var result = await getHandlerResult(handler, req, res); var result = await getHandlerResult(handler, req, res);
if (result == null) if (result == null)
@ -325,8 +326,11 @@ class Angel extends AngelBase {
} }
Future<RequestContext> createRequestContext(HttpRequest request) { Future<RequestContext> createRequestContext(HttpRequest request) {
return RequestContext.from(request, this).then((req) { var path = request.uri.path.replaceAll(_straySlashes, '');
_injections.forEach(req.inject); if (path.length == 0) path = '/';
return RequestContext.from(request, this, path).then((req) async {
if (_pool != null) req.inject(PoolResource, await _pool.request());
if (_injections.isNotEmpty) _injections.forEach(req.inject);
return req; return req;
}); });
} }
@ -378,53 +382,32 @@ class Angel extends AngelBase {
Future handleRequest(HttpRequest request) async { Future handleRequest(HttpRequest request) async {
var req = await createRequestContext(request); var req = await createRequestContext(request);
var res = await createResponseContext(request.response, req); var res = await createResponseContext(request.response, req);
var zoneSpec = await createZoneForRequest(request, req, res);
var zone = Zone.current.fork(specification: zoneSpec);
return zone.runGuarded(() async { try {
String requestedUrl; var path = req.path;
if (path == '/') path = '';
// Faster way to get path Tuple3<List, Map, ParseResult<Map<String, String>>> resolveTuple() {
List<int> _path = request.uri.path.codeUnits; Router r = _flattened ?? this;
// Remove trailing slashes
int lastSlash = -1;
for (int i = _path.length - 1; i >= 0; i--) {
if (_path[i] == $slash)
lastSlash = i;
else
break;
}
if (lastSlash > -1)
requestedUrl = new String.fromCharCodes(_path.take(lastSlash));
else
requestedUrl = new String.fromCharCodes(_path);
if (requestedUrl.isEmpty) requestedUrl = '/';
Tuple3<List, Map, Match> resolveTuple() {
Router r = isProduction ? (_flattened ??= flatten(this)) : this;
var resolved = var resolved =
r.resolveAll(requestedUrl, requestedUrl, method: req.method); r.resolveAbsolute(path, method: req.method, strip: false);
return new Tuple3( return new Tuple3(
new MiddlewarePipeline(resolved).handlers, new MiddlewarePipeline(resolved).handlers,
resolved.fold<Map>({}, (out, r) => out..addAll(r.allParams)), resolved.fold<Map>({}, (out, r) => out..addAll(r.allParams)),
resolved.isEmpty ? null : resolved.first.route.match(requestedUrl), resolved.isEmpty ? null : resolved.first.parseResult,
); );
} }
var cacheKey = req.method + path;
var tuple = isProduction var tuple = isProduction
? handlerCache.putIfAbsent( ? handlerCache.putIfAbsent(cacheKey, resolveTuple)
'${req.method}:$requestedUrl', resolveTuple)
: resolveTuple(); : resolveTuple();
req.inject(Zone, zone); //req.inject(Zone, zone);
req.inject(ZoneSpecification, zoneSpec); //req.inject(ZoneSpecification, zoneSpec);
req.params.addAll(tuple.item2); req.params.addAll(tuple.item2);
req.inject(Match, tuple.item3); req.inject(ParseResult, tuple.item3);
if (logger != null) req.inject(Stopwatch, new Stopwatch()..start()); if (logger != null) req.inject(Stopwatch, new Stopwatch()..start());
@ -453,7 +436,15 @@ class Angel extends AngelBase {
ignoreFinalizers: true, ignoreFinalizers: true,
); );
} }
}).catchError((error, stackTrace) { } on FormatException catch (error, stackTrace) {
var e = new AngelHttpException.badRequest(message: error.message);
if (logger != null) {
logger.severe(e.message ?? e.toString(), error, stackTrace);
}
return await handleAngelHttpException(e, stackTrace, req, res, request);
} catch (error, stackTrace) {
var e = new AngelHttpException(error, var e = new AngelHttpException(error,
stackTrace: stackTrace, message: error?.toString()); stackTrace: stackTrace, message: error?.toString());
@ -461,13 +452,10 @@ class Angel extends AngelBase {
logger.severe(e.message ?? e.toString(), error, stackTrace); logger.severe(e.message ?? e.toString(), error, stackTrace);
} }
return handleAngelHttpException(e, stackTrace, req, res, request); return await handleAngelHttpException(e, stackTrace, req, res, request);
}).whenComplete(() { } finally {
scheduleMicrotask(() {
req.close();
res.dispose(); res.dispose();
}); }
});
} }
/// Runs several optimizations, *if* [isProduction] is `true`. /// Runs several optimizations, *if* [isProduction] is `true`.
@ -495,7 +483,7 @@ class Angel extends AngelBase {
}); });
} }
if (_flattened == null) _flattened = flatten(this); _flattened ??= flatten(this);
_walk(_flattened); _walk(_flattened);
@ -528,9 +516,8 @@ class Angel extends AngelBase {
Future sendResponse( Future sendResponse(
HttpRequest request, RequestContext req, ResponseContext res, HttpRequest request, RequestContext req, ResponseContext res,
{bool ignoreFinalizers: false}) { {bool ignoreFinalizers: false}) {
if (res.willCloseItself) { if (res.willCloseItself) return new Future.value();
return new Future.value();
} else {
Future finalizers = ignoreFinalizers == true Future finalizers = ignoreFinalizers == true
? new Future.value() ? new Future.value()
: responseFinalizers.fold<Future>( : responseFinalizers.fold<Future>(
@ -549,13 +536,14 @@ class Angel extends AngelBase {
if (res.encoders.isNotEmpty) { if (res.encoders.isNotEmpty) {
var allowedEncodings = var allowedEncodings =
(req.headers[HttpHeaders.ACCEPT_ENCODING] ?? []).map((str) { req.headers[HttpHeaders.ACCEPT_ENCODING]?.map((str) {
// Ignore quality specifications in accept-encoding // Ignore quality specifications in accept-encoding
// ex. gzip;q=0.8 // ex. gzip;q=0.8
if (!str.contains(';')) return str; if (!str.contains(';')) return str;
return str.split(';')[0]; return str.split(';')[0];
}); });
if (allowedEncodings != null) {
for (var encodingName in allowedEncodings) { for (var encodingName in allowedEncodings) {
Converter<List<int>, List<int>> encoder; Converter<List<int>, List<int>> encoder;
String key = encodingName; String key = encodingName;
@ -574,13 +562,20 @@ class Angel extends AngelBase {
} }
} }
} }
}
request.response request.response
..statusCode = res.statusCode ..statusCode = res.statusCode
..cookies.addAll(res.cookies) ..cookies.addAll(res.cookies)
..add(outputBuffer); ..add(outputBuffer);
return finalizers.then((_) => request.response.close()).then((_) { return finalizers.then((_) async {
request.response.close();
if (req.injections.containsKey(PoolResource)) {
req.injections[PoolResource].release();
}
if (logger != null) { if (logger != null) {
var sw = req.grab<Stopwatch>(Stopwatch); var sw = req.grab<Stopwatch>(Stopwatch);
@ -592,6 +587,13 @@ class Angel extends AngelBase {
} }
}); });
} }
/// Limits the maximum number of requests to be handled concurrently by this instance.
///
/// You can optionally provide a [timeout] to limit the amount of time a request can be
/// handled before.
void throttle(int maxConcurrentRequests, {Duration timeout}) {
_pool = new Pool(maxConcurrentRequests, timeout: timeout);
} }
/// Applies an [AngelConfigurer] to this instance. /// Applies an [AngelConfigurer] to this instance.
@ -651,8 +653,10 @@ class Angel extends AngelBase {
/// Default constructor. ;) /// Default constructor. ;)
Angel() : super() { Angel() : super() {
bootstrapContainer(); bootstrapContainer();
createZoneForRequest = defaultZoneCreator;
}
createZoneForRequest = (request, req, res) async { Future<ZoneSpecification> defaultZoneCreator(request, req, res) async {
return new ZoneSpecification( return new ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) { print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
if (logger != null) { if (logger != null) {
@ -662,7 +666,6 @@ class Angel extends AngelBase {
} }
}, },
); );
};
} }
/// An instance mounted on a server started by the [serverGenerator]. /// An instance mounted on a server started by the [serverGenerator].

View file

@ -35,6 +35,11 @@ class Providers {
@override @override
bool operator ==(other) => other is Providers && other.via == via; bool operator ==(other) => other is Providers && other.via == via;
@override
String toString() {
return 'via:$via';
}
} }
/// A front-facing interface that can present data to and operate on data on behalf of the user. /// A front-facing interface that can present data to and operate on data on behalf of the user.

View file

@ -22,7 +22,8 @@ class AngelMetrics extends Angel {
res.contentType = ContentType.HTML; res.contentType = ContentType.HTML;
var rows = stats.all.map((stat) { var rows = stats.all.map((stat) {
return '''<tr> return '''
<tr>
<td>${stat.name}</td> <td>${stat.name}</td>
<td>${stat.iterations}</td> <td>${stat.iterations}</td>
<td>${stat.sum}ms</td> <td>${stat.sum}ms</td>
@ -107,6 +108,13 @@ class AngelMetrics extends Angel {
() => super.createResponseContext(response, correspondingRequest)); () => super.createResponseContext(response, correspondingRequest));
} }
@override
Iterable<RoutingResult> resolveAll(String absolute, String relative,
{String method: 'GET', bool strip: true}) {
return stats.resolveAll
.run(() => super.resolveAll(absolute, relative, method: method, strip: strip));
}
@override @override
Future handleRequest(HttpRequest request) { Future handleRequest(HttpRequest request) {
return stats.handleRequest.run(() async { return stats.handleRequest.run(() async {
@ -146,6 +154,7 @@ class AngelMetricsStats {
all = [ all = [
createRequestContext, createRequestContext,
createResponseContext, createResponseContext,
resolveAll,
executeHandler, executeHandler,
getHandlerResult, getHandlerResult,
runContained, runContained,
@ -156,6 +165,7 @@ class AngelMetricsStats {
final Stats createRequestContext = new Stats('createRequestContext'); final Stats createRequestContext = new Stats('createRequestContext');
final Stats createResponseContext = new Stats('createResponseContext'); final Stats createResponseContext = new Stats('createResponseContext');
final Stats resolveAll = new Stats('resolveAll');
final Stats handleRequest = new Stats('handleRequest'); final Stats handleRequest = new Stats('handleRequest');
final Stats executeHandler = new Stats('executeHandler'); final Stats executeHandler = new Stats('executeHandler');
final Stats getHandlerResult = new Stats('getHandlerResult'); final Stats getHandlerResult = new Stats('getHandlerResult');

View file

@ -1,6 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:mirrors'; import 'dart:mirrors';
final RegExp straySlashes = new RegExp(r'(^/+)|(/+$)');
matchingAnnotation(List<InstanceMirror> metadata, Type T) { matchingAnnotation(List<InstanceMirror> metadata, Type T) {
for (InstanceMirror metaDatum in metadata) { for (InstanceMirror metaDatum in metadata) {
if (metaDatum.hasReflectee) { if (metaDatum.hasReflectee) {

View file

@ -5,20 +5,39 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/metrics.dart';
main() { main() async {
var isolates = <Isolate>[];
for (int i = 0; i < Platform.numberOfProcessors; i++) { for (int i = 0; i < Platform.numberOfProcessors; i++) {
Isolate.spawn(start, i + 1); isolates.add(await Isolate.spawn(start, i + 1));
} }
start(0); await Future.wait(isolates.map((i) {
var rcv = new ReceivePort();
i.addOnExitListener(rcv.sendPort);
return rcv.first;
}));
//start(0);
} }
void start(int id) { void start(int id) {
var app = new AngelMetrics.custom(startShared) var app = new Angel.custom(startShared)..lazyParseBodies = true;
..lazyParseBodies = true
..get('/', (req, res) => res.write('Hello, world!')); if (true) {
app.get('/', (req, ResponseContext res) {
res.willCloseItself = true;
res.io
..write('Hello, world!')
..close();
return false;
});
} else {
app.get('/', (req, ResponseContext res) {
res.useStream();
res.write('Hello, world!');
});
}
var oldHandler = app.errorHandler; var oldHandler = app.errorHandler;
app.errorHandler = (e, req, res) { app.errorHandler = (e, req, res) {

View file

@ -1,5 +1,5 @@
name: angel_framework name: angel_framework
version: 1.1.0-alpha+8 version: 1.1.0-alpha+9
description: A high-powered HTTP server with DI, routing and more. description: A high-powered HTTP server with DI, routing and more.
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_framework homepage: https://github.com/angel-dart/angel_framework
@ -8,17 +8,17 @@ environment:
dependencies: dependencies:
angel_http_exception: ^1.0.0 angel_http_exception: ^1.0.0
angel_model: ^1.0.0 angel_model: ^1.0.0
angel_route: ">=1.0.5 <2.0.0" angel_route: ^2.0.0
body_parser: ^1.0.0-dev body_parser: ^1.0.0-dev
charcode: ^1.0.0 charcode: ^1.0.0
container: ^0.1.2 container: ^0.1.2
flatten: ^1.0.0
json_god: ^2.0.0-beta json_god: ^2.0.0-beta
logging: ">=0.11.3 <1.0.0" logging: ">=0.11.3 <1.0.0"
matcher: ^0.12.0 matcher: ^0.12.0
merge_map: ^1.0.0 merge_map: ^1.0.0
meta: ^1.0.0 meta: ^1.0.0
mime: ^0.9.3 mime: ^0.9.3
pool: ^1.0.0
random_string: ^0.0.1 random_string: ^0.0.1
tuple: ^1.0.0 tuple: ^1.0.0
dev_dependencies: dev_dependencies:

View file

@ -34,6 +34,12 @@ main() {
expect(req.accepts('text/html'), isTrue); expect(req.accepts('text/html'), isTrue);
}); });
test('strict', () async {
var req = await acceptContentTypes(['text/html', "*/*"]);
expect(req.accepts(ContentType.HTML), isTrue);
expect(req.accepts(ContentType.JSON, strict: true), isFalse);
});
group('disallow null', () { group('disallow null', () {
RequestContext req; RequestContext req;

View file

@ -4,6 +4,7 @@ import 'controller_test.dart' as controller;
import 'di_test.dart' as di; import 'di_test.dart' as di;
import 'encoders_buffer_test.dart' as encoders_buffer; import 'encoders_buffer_test.dart' as encoders_buffer;
import 'exception_test.dart' as exception; import 'exception_test.dart' as exception;
import 'extension_test.dart' as extension;
import 'general_test.dart' as general; import 'general_test.dart' as general;
import 'hooked_test.dart' as hooked; import 'hooked_test.dart' as hooked;
import 'parameter_meta_test.dart' as parameter_meta; import 'parameter_meta_test.dart' as parameter_meta;
@ -27,6 +28,7 @@ main() {
group('di', di.main); group('di', di.main);
group('encoders_buffer', encoders_buffer.main); group('encoders_buffer', encoders_buffer.main);
group('exception', exception.main); group('exception', exception.main);
group('extension', extension.main);
group('general', general.main); group('general', general.main);
group('hooked', hooked.main); group('hooked', hooked.main);
group('parameter_meta', parameter_meta.main); group('parameter_meta', parameter_meta.main);

View file

@ -1,7 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_route/angel_route.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'common.dart'; import 'common.dart';

29
test/extension_test.dart Normal file
View file

@ -0,0 +1,29 @@
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart';
final Uri ENDPOINT = Uri.parse('http://example.com');
main() {
test('single extension', () async {
var req = await makeRequest('foo.js');
expect(req.extension, '.js');
});
test('multiple extensions', () async {
var req = await makeRequest('foo.min.js');
expect(req.extension, '.js');
});
test('no extension', () async {
var req = await makeRequest('foo');
expect(req.extension, '');
});
}
Future<RequestContext> makeRequest(String path) {
var rq = new MockHttpRequest('GET', ENDPOINT.replace(path: path))..close();
var app = new Angel();
return app.createRequestContext(rq);
}

View file

@ -49,8 +49,8 @@ main() {
ted = nested.post('/ted/:route', (RequestContext req, res) { ted = nested.post('/ted/:route', (RequestContext req, res) {
print('Params: ${req.params}'); print('Params: ${req.params}');
print( print('Path: ${ted.path}, uri: ${req.path}');
'Path: ${ted.path}, matcher: ${ted.matcher.pattern}, uri: ${req.path}'); print('matcher: ${ted.parser}');
return req.params; return req.params;
}); });
@ -65,7 +65,7 @@ main() {
app app
.get('/greet/:name', .get('/greet/:name',
(RequestContext req, res) async => "Hello ${req.params['name']}") (RequestContext req, res) async => "Hello ${req.params['name']}")
.as('Named routes'); .name = 'Named routes';
app.get('/named', (req, ResponseContext res) async { app.get('/named', (req, ResponseContext res) async {
res.redirectTo('Named routes', {'name': 'tests'}); res.redirectTo('Named routes', {'name': 'tests'});
}); });
@ -86,7 +86,9 @@ main() {
}; };
} }
app.chain(write('a')).chain([write('b'), write('c')]).get('/chained', () => false); app
.chain(write('a'))
.chain([write('b'), write('c')]).get('/chained', () => false);
app.use('MJ'); app.use('MJ');
@ -165,7 +167,7 @@ main() {
}); });
test('Can name routes', () { test('Can name routes', () {
Route foo = new Route('/framework/:id', name: 'frm'); Route foo = app.get('/framework/:id', [])..name = 'frm';
print('Foo: $foo'); print('Foo: $foo');
String uri = foo.makeUri({'id': 'angel'}); String uri = foo.makeUri({'id': 'angel'});
print(uri); print(uri);