diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml
index c7ee0131..f73ff04c 100644
--- a/.idea/libraries/Dart_Packages.xml
+++ b/.idea/libraries/Dart_Packages.xml
@@ -26,14 +26,14 @@
-
+
-
+
@@ -86,6 +86,13 @@
+
+
+
+
+
+
+
@@ -114,13 +121,6 @@
-
-
-
-
-
-
-
@@ -271,7 +271,7 @@
-
+
@@ -442,8 +442,8 @@
-
-
+
+
@@ -451,11 +451,11 @@
+
-
@@ -477,7 +477,7 @@
-
+
diff --git a/.idea/runConfigurations/only_match_route_with_matching_method_in_routing_test_dart.xml b/.idea/runConfigurations/only_match_route_with_matching_method_in_routing_test_dart.xml
new file mode 100644
index 00000000..f030feb7
--- /dev/null
+++ b/.idea/runConfigurations/only_match_route_with_matching_method_in_routing_test_dart.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/tests_in_framework.xml b/.idea/runConfigurations/tests_in_framework.xml
new file mode 100644
index 00000000..4278d0e6
--- /dev/null
+++ b/.idea/runConfigurations/tests_in_framework.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 5a6d97d1..1a80a1ac 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -2,8 +2,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -26,36 +43,22 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
+
+
-
-
-
-
-
+
+
+
@@ -63,8 +66,8 @@
-
-
+
+
@@ -72,32 +75,12 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
+
@@ -105,37 +88,67 @@
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -152,36 +165,36 @@
- query
- value == n
- handleCont
- streamcontroller
- from(
- gua
- _buf
- sendRes
- Stopwatc
- toByt
- toBy
- listen
- this.
- close()
- dispose
- beforeAllStream
- handleRequest
- catchError
- dispose(
- putIf
- putif
- ANGEL_E
- custom(
- _sub?.c
- pipe
- pipeline
- <BR>
- extend(
handleReq
STARTS
+ list
+ sendRes
+ handleRequ
+ sendRespon
+ inject(
+ sendRe
+ sendResponse
+ addStream
+ Lo
+ useStr
+ _injections?
+ createRes
+ requestedUrl
+ cacheK
+ resolveAl
+ path
+ handleRequest
+ flatten
+ pipeline
+ pat
+ createR
+ print
+ dispos
+ handleRe
+ xhr
+ accepts
+ cacheKey
+ testing
_isClosed
@@ -200,14 +213,16 @@
JSON.decode
query
cookie
+ req.path
+ req.path`
C:\Users\thosa\Source\Angel\framework\lib
C:\Users\thosa\Source\Angel\framework\lib\src\http
C:\Users\thosa\Source\Angel\framework\test
$PROJECT_DIR$/lib/src
- $PROJECT_DIR$/test
$PROJECT_DIR$/lib
+ $PROJECT_DIR$/test
@@ -221,9 +236,6 @@
@@ -282,10 +297,10 @@
DEFINITION_ORDER
-
-
+
+
-
+
@@ -305,9 +320,9 @@
+
-
@@ -333,11 +348,16 @@
+
+
+
+
+
-
+
@@ -357,6 +377,7 @@
+
@@ -376,7 +397,7 @@
-
+
@@ -497,6 +518,12 @@
+
+
+
+
+
+
@@ -507,11 +534,6 @@
-
-
-
-
-
@@ -533,6 +555,10 @@
+
+
+
+
@@ -561,44 +587,52 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -657,7 +691,13 @@
-
+
+
+
+
+
+
+
1481237183504
@@ -939,43 +979,50 @@
1511026951368
-
+
+ 1511026990202
+
+
+
+ 1511026990202
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
@@ -987,32 +1034,33 @@
-
+
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
-
@@ -1021,7 +1069,6 @@
-
@@ -1031,7 +1078,6 @@
-
@@ -1056,131 +1102,14 @@
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1209,13 +1138,6 @@
-
-
-
-
-
-
-
@@ -1223,14 +1145,6 @@
-
-
-
-
-
-
-
-
@@ -1238,28 +1152,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1274,13 +1167,6 @@
-
-
-
-
-
-
-
@@ -1288,36 +1174,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1333,71 +1189,312 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
+
-
+
-
-
+
+
-
-
+
-
+
-
-
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -1405,22 +1502,14 @@
-
-
+
+
-
-
-
-
-
-
-
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dfad1d05..df66e68c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
* Added an `autoIdAndDateFields` flag to `MapService`. Finally.
diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart
index 1ea30dc9..730076bd 100644
--- a/lib/src/http/request_context.dart
+++ b/lib/src/http/request_context.dart
@@ -4,7 +4,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:mirrors';
import 'package:body_parser/body_parser.dart';
-import 'package:charcode/charcode.dart';
+import 'package:path/path.dart' as p;
import 'metadata.dart';
import 'response_context.dart';
import 'routable.dart';
@@ -13,7 +13,7 @@ part 'injection.dart';
/// A convenience wrapper around an incoming HTTP request.
class RequestContext {
- String _acceptHeaderCache;
+ String _acceptHeaderCache, _extensionCache;
bool _acceptsAllCache;
BodyParseResult _body;
ContentType _contentType;
@@ -39,9 +39,10 @@ class RequestContext {
String get hostname => io.headers.value(HttpHeaders.HOST);
final Map _injections = {};
+ Map _injectionsCache;
/// 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.
HttpRequest get io => _io;
@@ -135,8 +136,13 @@ class RequestContext {
io.headers.value("X-Requested-With")?.trim()?.toLowerCase() ==
'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].
- static Future from(HttpRequest request, Angel app) async {
+ static Future from(HttpRequest request, Angel app, String path) async {
RequestContext ctx = new RequestContext();
String override = request.method;
@@ -150,6 +156,7 @@ class RequestContext {
ctx._contentType = request.headers.contentType;
ctx._override = override;
+ /*
// Faster way to get path
List _path = [];
@@ -175,6 +182,9 @@ class RequestContext {
ctx._path = new String.fromCharCodes(_path.take(lastSlash));
else
ctx._path = new String.fromCharCodes(_path);
+ */
+
+ ctx._path = path;
ctx._io = request;
if (app.lazyParseBodies != true) {
@@ -219,17 +229,19 @@ class RequestContext {
}
_injections[type] = value;
+ _injectionsCache = null;
}
/// Returns `true` if the client's `Accept` header indicates that the given [contentType] is considered a valid response.
///
/// You cannot provide a `null` [contentType].
/// 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:
/// * 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.
- bool accepts(contentType) {
+ bool accepts(contentType, {bool strict: false}) {
var contentTypeString = contentType is ContentType
? contentType.mimeType
: contentType?.toString();
@@ -242,7 +254,7 @@ class RequestContext {
if (_acceptHeaderCache == null)
return false;
- else if (_acceptHeaderCache.contains('*/*'))
+ else if (strict != true && _acceptHeaderCache.contains('*/*'))
return true;
else
return _acceptHeaderCache.contains(contentTypeString);
diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart
index 0abe6747..d3f64103 100644
--- a/lib/src/http/response_context.dart
+++ b/lib/src/http/response_context.dart
@@ -6,6 +6,7 @@ import 'dart:io';
import 'package:angel_route/angel_route.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:mime/mime.dart';
+import 'package:pool/pool.dart';
import 'server.dart' show Angel;
import 'controller.dart';
import 'request_context.dart';
@@ -337,6 +338,31 @@ class ResponseContext implements StreamSink>, StringSink {
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.
///
/// This will also set [willCloseItself] to `true`, thus canceling out response finalizers.
@@ -346,24 +372,10 @@ class ResponseContext implements StreamSink>, StringSink {
@override
Future addStream(Stream> stream) {
if (_isClosed && !_useStream) throw _closed();
- bool firstStream = _useStream == false;
- willCloseItself = _useStream = _isClosed = true;
-
- if (_correspondingRequest?.injections?.containsKey(Stopwatch) == true) {
- (_correspondingRequest.injections[Stopwatch] as Stopwatch).stop();
- }
+ var firstStream = useStream();
Stream> 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) {
var allowedEncodings =
(correspondingRequest.headers[HttpHeaders.ACCEPT_ENCODING] ?? [])
diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart
index 44a01c0b..1b46fd2a 100644
--- a/lib/src/http/routable.dart
+++ b/lib/src/http/routable.dart
@@ -64,6 +64,7 @@ class Routable extends Router {
/// Assigns a middleware to a name for convenience.
@override
registerMiddleware(String name, @checked RequestHandler middleware) =>
+ // ignore: deprecated_member_use
super.registerMiddleware(name, middleware);
/// Retrieves the service assigned to the given path.
diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart
index 75975441..54237e16 100644
--- a/lib/src/http/server.dart
+++ b/lib/src/http/server.dart
@@ -1,16 +1,17 @@
library angel_framework.http.server;
import 'dart:async';
+import 'dart:collection' show HashMap;
import 'dart:convert';
import 'dart:io';
import 'package:angel_http_exception/angel_http_exception.dart';
-import 'package:angel_route/angel_route.dart' hide Extensible;
-import 'package:charcode/charcode.dart';
+import 'package:angel_route/angel_route.dart';
+import 'package:combinator/combinator.dart';
export 'package:container/container.dart';
-import 'package:flatten/flatten.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
+import 'package:pool/pool.dart';
import 'package:tuple/tuple.dart';
import 'angel_base.dart';
import 'controller.dart';
@@ -30,11 +31,13 @@ typedef Future AngelConfigurer(Angel app);
/// A powerful real-time/REST/MVC server class.
class Angel extends AngelBase {
final List _children = [];
- final Map> handlerCache = {};
+ final Map>>>
+ handlerCache = new HashMap();
Router _flattened;
- bool _isProduction = false;
+ bool _isProduction;
Angel _parent;
+ Pool _pool;
StreamSubscription _sub;
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
/// configuration at runtime!
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.
@@ -237,7 +241,7 @@ class Angel extends AngelBase {
String tab: ' ',
bool showMatchers: false}) {
if (isProduction) {
- if (_flattened == null) _flattened = flatten(this);
+ _flattened ??= flatten(this);
_flattened.dumpTree(
callback: callback,
@@ -246,8 +250,7 @@ class Angel extends AngelBase {
: (isProduction
? 'Dumping flattened route tree:'
: 'Dumping route tree:'),
- tab: tab ?? ' ',
- showMatchers: showMatchers == true);
+ tab: tab ?? ' ');
} else {
super.dumpTree(
callback: callback,
@@ -256,8 +259,7 @@ class Angel extends AngelBase {
: (isProduction
? 'Dumping flattened route tree:'
: 'Dumping route tree:'),
- tab: tab ?? ' ',
- showMatchers: showMatchers == true);
+ tab: tab ?? ' ');
}
}
@@ -308,7 +310,6 @@ class Angel extends AngelBase {
/// Runs some [handler]. Returns `true` if request execution should continue.
Future executeHandler(
handler, RequestContext req, ResponseContext res) async {
- if (handler == null) return false;
var result = await getHandlerResult(handler, req, res);
if (result == null)
@@ -325,8 +326,11 @@ class Angel extends AngelBase {
}
Future createRequestContext(HttpRequest request) {
- return RequestContext.from(request, this).then((req) {
- _injections.forEach(req.inject);
+ var path = request.uri.path.replaceAll(_straySlashes, '');
+ 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;
});
}
@@ -378,53 +382,32 @@ class Angel extends AngelBase {
Future handleRequest(HttpRequest request) async {
var req = await createRequestContext(request);
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 {
- String requestedUrl;
+ try {
+ var path = req.path;
+ if (path == '/') path = '';
- // Faster way to get path
- List _path = request.uri.path.codeUnits;
-
- // 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 resolveTuple() {
- Router r = isProduction ? (_flattened ??= flatten(this)) : this;
+ Tuple3>> resolveTuple() {
+ Router r = _flattened ?? this;
var resolved =
- r.resolveAll(requestedUrl, requestedUrl, method: req.method);
+ r.resolveAbsolute(path, method: req.method, strip: false);
return new Tuple3(
new MiddlewarePipeline(resolved).handlers,
resolved.fold