Working on 1.0.8, including performance tuning

This commit is contained in:
thosakwe 2017-08-03 12:40:21 -04:00
parent 3d12297316
commit f6695080b8
22 changed files with 995 additions and 619 deletions

View file

@ -19,7 +19,7 @@
<entry key="angel_route">
<value>
<list>
<option value="$USER_HOME$/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/angel_route-1.0.3/lib" />
<option value="$USER_HOME$/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/angel_route-1.0.5/lib" />
</list>
</value>
</entry>
@ -399,7 +399,7 @@
<CLASSES>
<root url="file://$USER_HOME$/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/analyzer-0.30.0+2/lib" />
<root url="file://$USER_HOME$/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/angel_model-1.0.0/lib" />
<root url="file://$USER_HOME$/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/angel_route-1.0.3/lib" />
<root url="file://$USER_HOME$/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/angel_route-1.0.5/lib" />
<root url="file://$USER_HOME$/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/args-0.13.7/lib" />
<root url="file://$USER_HOME$/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/async-1.13.3/lib" />
<root url="file://$USER_HOME$/AppData/Roaming/Pub/Cache/hosted/pub.dartlang.org/barback-0.15.2+11/lib" />

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="performance::hello" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="checkedMode" value="false" />
<option name="filePath" value="$PROJECT_DIR$/performance/hello/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,20 @@
# 1.0.8
* Changed `req.query` to use a modifiable Map if the body has not parsed. Resolves
[#157](https://github.com/angel-dart/framework/issues/157).
* Changed all constants to `camelCase`, and deprecated their `CONSTANT_CASE` counterparts. Resolves
[#155](https://github.com/angel-dart/framework/issues/155).
* Resolved [#156](https://github.com/angel-dart/framework/issues/156) by adding a `graphql` provider.
* Added an `analysis-options.yaml` enabling strong mode. Preparing for Dart 2.0.
* Added a dependency on `package:meta`, resolving [#154](https://github.com/angel-dart/framework/issues/154),
and added corresponding annotations to make extending Angel easier.
* Resolved [#158](https://github.com/angel-dart/framework/issues/158) by using proper `StreamController`
patterns, to prevent memory leaks.
* Route handler sequences are now cached in a Map, so repeat requests will be resolved faster.
* A message is no longer printed in production mode.
* Removed the inheritance on `Extensible` in many classes, and removed it from `angel_route`.
Now, only `Angel` and `RequestContext` have `@proxy` annotations.
* Deprecated passing `debug` to Angel.
# 1.0.7+2
Changed `ResponseContext.serialize`. The `contentType` is now set *before* serialization.

2
analysis_options.yaml Normal file
View file

@ -0,0 +1,2 @@
analyzer:
strong-mode: true

View file

@ -143,8 +143,6 @@ HookedServiceEventListener remove(key, [remover(key, obj)]) {
return obj.where((k) => !key);
else if (obj is Map)
return obj..remove(key);
else if (obj is Extensible)
return obj..properties.remove(key);
else {
try {
reflect(obj).setField(new Symbol(key), null);
@ -231,8 +229,6 @@ HookedServiceEventListener addCreatedAt(
return assign(obj, now);
else if (obj is Map)
obj[name] = now;
else if (obj is Extensible)
obj..properties[name] = now;
else {
try {
reflect(obj).setField(new Symbol(name), now);
@ -282,8 +278,6 @@ HookedServiceEventListener addUpdatedAt(
return assign(obj, now);
else if (obj is Map)
obj[name] = now;
else if (obj is Extensible)
obj..properties[name] = now;
else {
try {
reflect(obj).setField(new Symbol(name), now);

View file

@ -1,3 +0,0 @@
library angel_framework.extensible;
export 'package:angel_route/src/extensible.dart';

View file

@ -0,0 +1,6 @@
String fastNameFromSymbol(Symbol s) {
String str = s.toString();
int open = str.indexOf('"');
int close = str.lastIndexOf('"');
return str.substring(open + 1, close);
}

View file

@ -2,17 +2,19 @@ library angel_framework.http.angel_base;
import 'dart:async';
import 'package:container/container.dart';
import '../fast_name_from_symbol.dart';
import 'routable.dart';
/// A function that asynchronously generates a view from the given path and data.
typedef Future<String> ViewGenerator(String path, [Map data]);
/// Base class for Angel servers. Do not bother extending this.
@proxy
class AngelBase extends Routable {
AngelBase({bool debug: false}):super(debug: debug);
Container _container = new Container();
final Map properties = {};
/// When set to true, the request body will not be parsed
/// automatically. You can call `req.parse()` manually,
/// or use `lazyBody()`.
@ -29,4 +31,22 @@ class AngelBase extends Routable {
///
/// Called by [ResponseContext]@`render`.
ViewGenerator viewGenerator = (String view, [Map data]) async => "No view engine has been configured yet.";
operator [](key) => properties[key];
operator []=(key, value) => properties[key] = value;
noSuchMethod(Invocation invocation) {
if (invocation.memberName != null) {
String name = fastNameFromSymbol(invocation.memberName);
if (invocation.isMethod) {
return Function.apply(properties[name], invocation.positionalArguments,
invocation.namedArguments);
} else if (invocation.isGetter) {
return properties[name];
}
}
return super.noSuchMethod(invocation);
}
}

View file

@ -3,6 +3,7 @@ library angel_framework.http.controller;
import 'dart:async';
import 'dart:mirrors';
import 'package:angel_route/angel_route.dart';
import 'package:meta/meta.dart';
import 'metadata.dart';
import 'request_context.dart';
import 'response_context.dart';
@ -18,13 +19,20 @@ import 'server.dart' show Angel, preInject;
/// and memory use.
class InjectionRequest {
/// Optional, typed data that can be passed to a DI-enabled method.
Map<String, Type> named = {};
final Map<String, Type> named;
/// A list of the arguments required for a DI-enabled method to run.
final List required = [];
final List required;
/// A list of the arguments that can be null in a DI-enabled method.
final List<String> optional = [];
final List<String> optional;
const InjectionRequest.constant({this.named, this.required, this.optional});
InjectionRequest()
: named = {},
required = [],
optional = [];
}
/// Supports grouping routes with shared functionality.
@ -47,6 +55,7 @@ class Controller {
Controller({this.debug: false, this.injectSingleton: true});
@mustCallSuper
Future call(Angel app) async {
_app = app;
@ -143,6 +152,7 @@ RequestHandler createDynamicHandler(handler,
injection.optional.addAll(optional ?? []);
return handleContained(handler, injection);
}
/// Handles a request with a DI-enabled handler.
RequestHandler handleContained(handler, InjectionRequest injection) {
return (RequestContext req, ResponseContext res) async {

View file

@ -108,27 +108,27 @@ class HookedService extends Service {
applyListeners(inner.index, beforeIndexed);
applyListeners(inner.read, beforeRead);
applyListeners(inner.created, beforeCreated);
applyListeners(inner.create, beforeCreated);
applyListeners(inner.modify, beforeModified);
applyListeners(inner.updated, beforeUpdated);
applyListeners(inner.removed, beforeRemoved);
applyListeners(inner.update, beforeUpdated);
applyListeners(inner.remove, beforeRemoved);
applyListeners(inner.index, afterIndexed, true);
applyListeners(inner.read, afterRead, true);
applyListeners(inner.created, afterCreated, true);
applyListeners(inner.create, afterCreated, true);
applyListeners(inner.modify, afterModified, true);
applyListeners(inner.updated, afterUpdated, true);
applyListeners(inner.removed, afterRemoved, true);
applyListeners(inner.update, afterUpdated, true);
applyListeners(inner.remove, afterRemoved, true);
}
/// Adds routes to this instance.
@override
void addRoutes() {
// Set up our routes. We still need to copy middleware from inner service
Map restProvider = {'provider': Providers.REST};
Map restProvider = {'provider': Providers.rest};
// Add global middleware if declared on the instance itself
Middleware before = getAnnotation(inner, Middleware);
final handlers = [
List handlers = [
(RequestContext req, ResponseContext res) async {
req.serviceParams
..['__requestctx'] = req
@ -270,17 +270,17 @@ class HookedService extends Service {
Iterable<String> eventNames, HookedServiceEventListener listener) {
eventNames.map((name) {
switch (name) {
case HookedServiceEvent.INDEXED:
case HookedServiceEvent.indexed:
return beforeIndexed;
case HookedServiceEvent.READ:
case HookedServiceEvent.read:
return beforeRead;
case HookedServiceEvent.CREATED:
case HookedServiceEvent.created:
return beforeCreated;
case HookedServiceEvent.MODIFIED:
case HookedServiceEvent.modified:
return beforeModified;
case HookedServiceEvent.UPDATED:
case HookedServiceEvent.updated:
return beforeUpdated;
case HookedServiceEvent.REMOVED:
case HookedServiceEvent.removed:
return beforeRemoved;
default:
throw new ArgumentError('Invalid service method: ${name}');
@ -293,17 +293,17 @@ class HookedService extends Service {
void after(Iterable<String> eventNames, HookedServiceEventListener listener) {
eventNames.map((name) {
switch (name) {
case HookedServiceEvent.INDEXED:
case HookedServiceEvent.indexed:
return afterIndexed;
case HookedServiceEvent.READ:
case HookedServiceEvent.read:
return afterRead;
case HookedServiceEvent.CREATED:
case HookedServiceEvent.created:
return afterCreated;
case HookedServiceEvent.MODIFIED:
case HookedServiceEvent.modified:
return afterModified;
case HookedServiceEvent.UPDATED:
case HookedServiceEvent.updated:
return afterUpdated;
case HookedServiceEvent.REMOVED:
case HookedServiceEvent.removed:
return afterRemoved;
default:
throw new ArgumentError('Invalid service method: ${name}');
@ -340,7 +340,7 @@ class HookedService extends Service {
Stream<HookedServiceEvent> beforeAllStream() {
var ctrl = new StreamController<HookedServiceEvent>();
_ctrl.add(ctrl);
before(HookedServiceEvent.ALL, ctrl.add);
before(HookedServiceEvent.all, ctrl.add);
return ctrl.stream;
}
@ -352,7 +352,7 @@ class HookedService extends Service {
Stream<HookedServiceEvent> afterAllStream() {
var ctrl = new StreamController<HookedServiceEvent>();
_ctrl.add(ctrl);
before(HookedServiceEvent.ALL, ctrl.add);
before(HookedServiceEvent.all, ctrl.add);
return ctrl.stream;
}
@ -392,12 +392,12 @@ class HookedService extends Service {
var params = _stripReq(_params);
HookedServiceEvent before = await beforeIndexed._emit(
new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.INDEXED,
_getResponse(_params), inner, HookedServiceEvent.indexed,
params: params));
if (before._canceled) {
HookedServiceEvent after = await beforeIndexed._emit(
new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.INDEXED,
_getResponse(_params), inner, HookedServiceEvent.indexed,
params: params, result: before.result));
return after.result;
}
@ -408,7 +408,7 @@ class HookedService extends Service {
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.INDEXED,
HookedServiceEvent.indexed,
params: params,
result: result));
return after.result;
@ -422,7 +422,7 @@ class HookedService extends Service {
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.READ,
HookedServiceEvent.indexed,
id: id,
params: params));
@ -432,7 +432,7 @@ class HookedService extends Service {
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.READ,
HookedServiceEvent.read,
id: id,
params: params,
result: before.result));
@ -445,7 +445,7 @@ class HookedService extends Service {
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.READ,
HookedServiceEvent.read,
id: id,
params: params,
result: result));
@ -457,13 +457,13 @@ class HookedService extends Service {
var params = _stripReq(_params);
HookedServiceEvent before = await beforeCreated._emit(
new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.CREATED,
_getResponse(_params), inner, HookedServiceEvent.created,
data: data, params: params));
if (before._canceled) {
HookedServiceEvent after = await afterCreated._emit(
new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.CREATED,
_getResponse(_params), inner, HookedServiceEvent.created,
data: data, params: params, result: before.result));
return after.result;
}
@ -474,7 +474,7 @@ class HookedService extends Service {
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.CREATED,
HookedServiceEvent.created,
data: data,
params: params,
result: result));
@ -486,13 +486,13 @@ class HookedService extends Service {
var params = _stripReq(_params);
HookedServiceEvent before = await beforeModified._emit(
new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.MODIFIED,
_getResponse(_params), inner, HookedServiceEvent.modified,
id: id, data: data, params: params));
if (before._canceled) {
HookedServiceEvent after = await afterModified._emit(
new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.MODIFIED,
_getResponse(_params), inner, HookedServiceEvent.modified,
id: id, data: data, params: params, result: before.result));
return after.result;
}
@ -503,7 +503,7 @@ class HookedService extends Service {
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.MODIFIED,
HookedServiceEvent.modified,
id: id,
data: data,
params: params,
@ -516,13 +516,13 @@ class HookedService extends Service {
var params = _stripReq(_params);
HookedServiceEvent before = await beforeUpdated._emit(
new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.UPDATED,
_getResponse(_params), inner, HookedServiceEvent.updated,
id: id, data: data, params: params));
if (before._canceled) {
HookedServiceEvent after = await afterUpdated._emit(
new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.UPDATED,
_getResponse(_params), inner, HookedServiceEvent.updated,
id: id, data: data, params: params, result: before.result));
return after.result;
}
@ -533,7 +533,7 @@ class HookedService extends Service {
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.UPDATED,
HookedServiceEvent.updated,
id: id,
data: data,
params: params,
@ -546,13 +546,13 @@ class HookedService extends Service {
var params = _stripReq(_params);
HookedServiceEvent before = await beforeRemoved._emit(
new HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.REMOVED,
_getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params));
if (before._canceled) {
HookedServiceEvent after = await afterRemoved._emit(
new HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.REMOVED,
_getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params, result: before.result));
return after.result;
}
@ -563,7 +563,7 @@ class HookedService extends Service {
_getRequest(_params),
_getResponse(_params),
inner,
HookedServiceEvent.REMOVED,
HookedServiceEvent.removed,
id: id,
params: params,
result: result));
@ -577,22 +577,22 @@ class HookedService extends Service {
HookedServiceEventDispatcher dispatcher;
switch (eventName) {
case HookedServiceEvent.INDEXED:
case HookedServiceEvent.indexed:
dispatcher = afterIndexed;
break;
case HookedServiceEvent.READ:
case HookedServiceEvent.read:
dispatcher = afterRead;
break;
case HookedServiceEvent.CREATED:
case HookedServiceEvent.created:
dispatcher = afterCreated;
break;
case HookedServiceEvent.MODIFIED:
case HookedServiceEvent.modified:
dispatcher = afterModified;
break;
case HookedServiceEvent.UPDATED:
case HookedServiceEvent.updated:
dispatcher = afterUpdated;
break;
case HookedServiceEvent.REMOVED:
case HookedServiceEvent.removed:
dispatcher = afterRemoved;
break;
default:
@ -614,20 +614,44 @@ class HookedService extends Service {
/// Fired when a hooked service is invoked.
class HookedServiceEvent {
static const String INDEXED = "indexed";
static const String READ = "read";
static const String CREATED = "created";
static const String MODIFIED = "modified";
static const String UPDATED = "updated";
static const String REMOVED = "removed";
static const List<String> ALL = const [
INDEXED,
READ,
CREATED,
MODIFIED,
UPDATED,
REMOVED
static const String indexed = 'indexed';
static const String read = 'read';
static const String created = 'created';
static const String modified = 'modified';
static const String updated = 'updated';
static const String removed = 'removed';
static const List<String> all = const [
indexed, read, created, modified, updated, removed
];
/// Use [indexed] instead.
@deprecated
static const String INDEXED = indexed;
/// Use [read] instead.
@deprecated
static const String READ = read;
/// Use [created] instead.
@deprecated
static const String CREATED = created;
/// Use [modified] instead.
@deprecated
static const String MODIFIED = modified;
/// Use [updated] instead.
@deprecated
static const String UPDATED = updated;
/// Use [removed] instead.
@deprecated
static const String REMOVED = removed;
/// Use [all] instead.
@deprecated
static const List<String> ALL = all;
/// Use this to end processing of an event.
void cancel([result]) {

View file

@ -2,18 +2,23 @@ library angel_framework.http.request_context;
import 'dart:async';
import 'dart:io';
import 'package:angel_route/src/extensible.dart';
import 'package:body_parser/body_parser.dart';
import 'package:charcode/charcode.dart';
import '../fast_name_from_symbol.dart';
import 'server.dart' show Angel;
/// A convenience wrapper around an incoming HTTP request.
class RequestContext extends Extensible {
@proxy
class RequestContext {
String _acceptHeaderCache;
bool _acceptsAllCache;
BodyParseResult _body;
ContentType _contentType;
HttpRequest _io;
String _override, _path;
Map _provisionalQuery;
final Map properties = {};
/// Additional params to be passed to services.
final Map serviceParams = {};
@ -106,7 +111,7 @@ class RequestContext extends Extensible {
/// **If you are writing a plug-in, consider using [lazyQuery] instead.**
Map get query {
if (_body == null)
return uri.queryParameters;
return _provisionalQuery ??= new Map.from(uri.queryParameters);
else
return _body.query;
}
@ -145,16 +150,36 @@ class RequestContext extends Extensible {
ctx.app = app;
ctx._contentType = request.headers.contentType;
ctx._override = override;
ctx._path = request.uri
.toString()
.replaceAll("?" + request.uri.query, "")
.replaceAll(new RegExp(r'/+$'), '');
// Faster way to get path
List<int> _path = [];
// Go up until we reach a ?
for (int ch in request.uri.toString().codeUnits) {
if (ch != $question)
_path.add(ch);
else break;
}
// 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)
ctx._path = new String.fromCharCodes(_path.take(lastSlash));
else ctx._path = new String.fromCharCodes(_path);
ctx._io = request;
if (app.lazyParseBodies != true)
if (app.lazyParseBodies != true) {
ctx._body = (await parseBody(request,
storeOriginalBuffer: app.storeOriginalBuffer == true)) ??
storeOriginalBuffer: app.storeOriginalBuffer == true)) ??
{};
}
return ctx;
}
@ -246,7 +271,26 @@ class RequestContext extends Extensible {
if (_body != null)
return _body;
else
_provisionalQuery = null;
return _body = await parseBody(io,
storeOriginalBuffer: app.storeOriginalBuffer == true);
}
operator [](key) => properties[key];
operator []=(key, value) => properties[key] = value;
noSuchMethod(Invocation invocation) {
if (invocation.memberName != null) {
String name = fastNameFromSymbol(invocation.memberName);
if (invocation.isMethod) {
return Function.apply(properties[name], invocation.positionalArguments,
invocation.namedArguments);
} else if (invocation.isGetter) {
return properties[name];
}
}
return super.noSuchMethod(invocation);
}
}

View file

@ -3,10 +3,10 @@ library angel_framework.http.response_context;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:angel_route/angel_route.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:mime/mime.dart';
import '../extensible.dart';
import 'server.dart' show Angel;
import 'controller.dart';
@ -19,9 +19,10 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
typedef String ResponseSerializer(data);
/// A convenience wrapper around an outgoing HTTP request.
class ResponseContext extends Extensible implements StringSink {
class ResponseContext implements StringSink {
final _LockableBytesBuilder _buffer = new _LockableBytesBuilder();
final Map<String, String> _headers = {HttpHeaders.SERVER: 'angel'};
final Map properties = {};
bool _isOpen = true, _isClosed = false;
int _statusCode = 200;
@ -366,7 +367,7 @@ abstract class _LockableBytesBuilder extends BytesBuilder {
class _LockableBytesBuilderImpl implements _LockableBytesBuilder {
bool _closed = false;
final List<int> _data = [];
Uint8List _data = new Uint8List(0);
StateError _deny() =>
new StateError('Cannot modified a closed response\'s buffer.');
@ -385,8 +386,19 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder {
void add(List<int> bytes) {
if (_closed)
throw _deny();
else {
_data.addAll(bytes);
else if (bytes.isNotEmpty) {
int len = _data.length + bytes.length;
var d = new Uint8List(len);
for (int i = 0; i < _data.length; i++) {
d[i] = _data[i];
}
for (int i = 0; i < bytes.length; i++) {
d[i + _data.length] = bytes[i];
}
_data = d;
}
}
@ -395,7 +407,15 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder {
if (_closed)
throw _deny();
else {
_data.add(byte);
int len = _data.length + 1;
var d = new Uint8List(len);
for (int i = 0; i < _data.length; i++) {
d[i] = _data[i];
}
d[_data.length] = byte;
_data = d;
}
}
@ -404,7 +424,7 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder {
if (_closed)
throw _deny();
else {
_data.clear();
for (int i = 0; i < _data.length; i++) _data[i] = 0;
}
}
@ -422,7 +442,7 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder {
if (_closed)
return toBytes();
else {
var r = new List<int>.from(_data);
var r = new Uint8List.fromList(_data);
clear();
return r;
}

View file

@ -2,6 +2,7 @@ library angel_framework.http.routable;
import 'dart:async';
import 'package:angel_route/angel_route.dart';
import 'package:meta/meta.dart';
import '../util.dart';
import 'angel_base.dart';
import 'controller.dart';
@ -36,6 +37,7 @@ RequestMiddleware waterfall(List handlers) {
class Routable extends Router {
final Map<Pattern, Controller> _controllers = {};
final Map<Pattern, Service> _services = {};
final Map properties = {};
Routable({bool debug: false}) : super(debug: debug);
@ -59,7 +61,7 @@ class Routable extends Router {
/// Assigns a middleware to a name for convenience.
@override
registerMiddleware(String name, RequestMiddleware middleware) =>
registerMiddleware(String name, @checked RequestHandler middleware) =>
super.registerMiddleware(name, middleware);
/// Retrieves the service assigned to the given path.
@ -129,17 +131,19 @@ class Routable extends Router {
}
// Also copy properties...
Map copiedProperties = new Map.from(router.properties);
for (String propertyName in copiedProperties.keys) {
properties.putIfAbsent("$middlewarePrefix$propertyName",
() => copiedMiddleware[propertyName]);
if (router is Routable) {
Map copiedProperties = new Map.from(router.properties);
for (String propertyName in copiedProperties.keys) {
properties.putIfAbsent("$middlewarePrefix$propertyName",
() => copiedMiddleware[propertyName]);
}
}
// _router.dumpTree(header: 'Mounting on "$path":');
// root.child(path, debug: debug, handlers: handlers).addChild(router.root);
mount(path, _router);
if (router is Routable) {
if (_router is Routable) {
// Copy services, too. :)
for (Pattern servicePath in _router._services.keys) {
String newServicePath =
@ -149,6 +153,8 @@ class Routable extends Router {
}
}
if (service != null) _onService.add(service);
if (service != null) {
if (_onService.hasListener) _onService.add(service);
}
}
}

View file

@ -3,10 +3,13 @@ library angel_framework.http.server;
import 'dart:async';
import 'dart:io';
import 'dart:mirrors';
import 'package:angel_route/angel_route.dart';
import 'package:angel_route/angel_route.dart' hide Extensible;
import 'package:charcode/charcode.dart';
export 'package:container/container.dart';
import 'package:flatten/flatten.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:meta/meta.dart';
import '../safe_stream_controller.dart';
import 'angel_base.dart';
import 'angel_http_exception.dart';
import 'controller.dart';
@ -31,18 +34,18 @@ typedef Future AngelConfigurer(Angel app);
/// A powerful real-time/REST/MVC server class.
class Angel extends AngelBase {
StreamController<HttpRequest> _afterProcessed =
new StreamController<HttpRequest>.broadcast();
StreamController<HttpRequest> _beforeProcessed =
new StreamController<HttpRequest>.broadcast();
StreamController<AngelFatalError> _fatalErrorStream =
new StreamController<AngelFatalError>.broadcast();
StreamController<Controller> _onController =
new StreamController<Controller>.broadcast();
SafeCtrl<HttpRequest> _afterProcessed = new SafeCtrl<HttpRequest>.broadcast();
SafeCtrl<HttpRequest> _beforeProcessed =
new SafeCtrl<HttpRequest>.broadcast();
SafeCtrl<AngelFatalError> _fatalErrorStream =
new SafeCtrl<AngelFatalError>.broadcast();
SafeCtrl<Controller> _onController = new SafeCtrl<Controller>.broadcast();
final List<Angel> _children = [];
Router _flattened;
bool _isProduction;
Angel _parent;
final Map<String, List> _handlerCache = {};
ServerGenerator _serverGenerator = HttpServer.bind;
/// A global Map of manual injections. You usually will not want to touch this.
@ -152,7 +155,7 @@ class Angel extends AngelBase {
}
@override
Route addRoute(String method, String path, Object handler,
Route addRoute(String method, Pattern path, Object handler,
{List middleware: const []}) {
if (_flattened != null) {
print(
@ -308,9 +311,9 @@ class Angel extends AngelBase {
});
}
Future<ResponseContext> createResponseContext(HttpResponse response) async =>
new ResponseContext(response, this)
..serializer = (_serializer ?? god.serialize);
Future<ResponseContext> createResponseContext(HttpResponse response) =>
new Future<ResponseContext>.value(new ResponseContext(response, this)
..serializer = (_serializer ?? god.serialize));
/// Attempts to find a middleware by the given name within this application.
findMiddleware(key) {
@ -361,42 +364,50 @@ class Angel extends AngelBase {
try {
var req = await createRequestContext(request);
var res = await createResponseContext(request.response);
String requestedUrl = request.uri.path.replaceAll(_straySlashes, '');
String requestedUrl;
// Faster way to get path
List<int> _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 = '/';
Router r =
isProduction ? (_flattened ?? (_flattened = flatten(this))) : this;
var resolved =
r.resolveAll(requestedUrl, requestedUrl, method: req.method);
var pipeline = _handlerCache.putIfAbsent(requestedUrl, () {
Router r =
isProduction ? (_flattened ?? (_flattened = flatten(this))) : this;
var resolved =
r.resolveAll(requestedUrl, requestedUrl, method: req.method);
for (var result in resolved) req.params.addAll(result.allParams);
for (var result in resolved) req.params.addAll(result.allParams);
if (resolved.isNotEmpty) {
var route = resolved.first.route;
req.inject(Match, route.match(requestedUrl));
}
if (resolved.isNotEmpty) {
var route = resolved.first.route;
req.inject(Match, route.match(requestedUrl));
}
var m = new MiddlewarePipeline(resolved);
req.inject(MiddlewarePipeline, m);
var m = new MiddlewarePipeline(resolved);
req.inject(MiddlewarePipeline, m);
var pipeline = []..addAll(before)..addAll(m.handlers)..addAll(after);
// _printDebug('Handler sequence on $requestedUrl: $pipeline');
return new List.from(before)..addAll(m.handlers)..addAll(after);
});
for (var handler in pipeline) {
try {
// _printDebug('Executing handler: $handler');
var result = await executeHandler(handler, req, res);
// _printDebug('Result: $result');
if (!result) {
// _printDebug('Last executed handler: $handler');
break;
} else {
// _printDebug(
// 'Handler completed successfully, did not terminate response: $handler');
}
if (!await executeHandler(handler, req, res)) break;
} on AngelHttpException catch (e, st) {
e.stackTrace ??= st;
return await handleAngelHttpException(e, st, req, res, request);
@ -448,7 +459,8 @@ class Angel extends AngelBase {
if (_flattened == null) _flattened = flatten(this);
_walk(_flattened);
print('Angel is running in production mode.');
//if (silent != true) print('Angel is running in production mode.');
}
}
@ -482,15 +494,16 @@ class Angel extends AngelBase {
/// Sends a response.
Future sendResponse(
HttpRequest request, RequestContext req, ResponseContext res,
{bool ignoreFinalizers: false}) async {
{bool ignoreFinalizers: false}) {
_afterProcessed.add(request);
if (!res.willCloseItself) {
if (ignoreFinalizers != true) {
for (var finalizer in responseFinalizers) {
await finalizer(req, res);
}
}
if (res.willCloseItself) {
return new Future.value();
} else {
Future finalizers = ignoreFinalizers == true
? new Future.value()
: responseFinalizers.fold<Future>(
new Future.value(), (out, f) => out.then((_) => f(req, res)));
if (res.isOpen) res.end();
@ -505,8 +518,9 @@ class Angel extends AngelBase {
request.response
..statusCode = res.statusCode
..cookies.addAll(res.cookies)
..add(res.buffer.takeBytes());
await request.response.close();
..add(res.buffer.toBytes());
return finalizers.then((_) => request.response.close());
}
}
@ -541,7 +555,7 @@ class Angel extends AngelBase {
/// NOTE: The above will not be properly copied if [path] is
/// a [RegExp].
@override
use(Pattern path, Routable routable,
use(Pattern path, @checked Routable routable,
{bool hooked: true, String namespace: null}) {
var head = path.toString().replaceAll(_straySlashes, '');
@ -581,6 +595,22 @@ class Angel extends AngelBase {
var tail = k.toString().replaceAll(_straySlashes, '');
services['$head/$tail'.replaceAll(_straySlashes, '')] = v;
});
_beforeProcessed.whenInitialized(() {
routable.beforeProcessed.listen(_beforeProcessed.add);
});
_afterProcessed.whenInitialized(() {
routable.afterProcessed.listen(_afterProcessed.add);
});
_fatalErrorStream.whenInitialized(() {
routable.fatalErrorStream.listen(_fatalErrorStream.add);
});
_onController.whenInitialized(() {
routable.onController.listen(_onController.add);
});
}
if (routable is Service) {
@ -597,17 +627,18 @@ class Angel extends AngelBase {
}
/// Default constructor. ;)
Angel({bool debug: false}) : super(debug: debug == true) {
Angel({@deprecated bool debug: false}) : super() {
bootstrapContainer();
}
/// An instance mounted on a server started by the [serverGenerator].
factory Angel.custom(ServerGenerator serverGenerator, {bool debug: false}) =>
new Angel(debug: debug == true).._serverGenerator = serverGenerator;
factory Angel.custom(ServerGenerator serverGenerator,
{@deprecated bool debug: false}) =>
new Angel().._serverGenerator = serverGenerator;
factory Angel.fromSecurityContext(SecurityContext context,
{bool debug: false}) {
var app = new Angel(debug: debug == true);
{@deprecated bool debug: false}) {
var app = new Angel();
app._serverGenerator = (InternetAddress address, int port) async {
return await HttpServer.bindSecure(address, port, context);

View file

@ -20,14 +20,34 @@ class Providers {
const Providers(String this.via);
static const String viaRest = "rest";
static const String viaWebsocket = "websocket";
static const String viaGraphQL = "graphql";
/// Use [viaRest] instead.
@deprecated
static const String VIA_REST = "rest";
/// Use [viaWebSocket] instead.
@deprecated
static const String VIA_WEBSOCKET = "websocket";
/// Use [rest] instead.
@deprecated
static const Providers REST = const Providers(viaRest);
/// Use [websocket] instead.
@deprecated
static const Providers WEBSOCKET = const Providers(viaWebsocket);
/// Represents a request via REST.
static final Providers REST = const Providers(VIA_REST);
static const Providers rest = const Providers(viaRest);
/// Represents a request over WebSockets.
static final Providers WEBSOCKET = const Providers(VIA_WEBSOCKET);
static const Providers websocket = const Providers(viaWebsocket);
/// Represents a request parsed from GraphQL.
static const Providers graphql = const Providers(viaGraphQL);
@override
bool operator ==(other) => other is Providers && other.via == via;
@ -38,13 +58,17 @@ class Providers {
/// Heavily inspired by FeathersJS. <3
class Service extends Routable {
/// A [List] of keys that services should ignore, should they see them in the query.
static const List<String> SPECIAL_QUERY_KEYS = const [
static const List<String> specialQueryKeys = const [
r'$limit',
r'$sort',
'page',
'token'
];
/// Use [specialQueryKeys] instead.
@deprecated
static const List<String> SPECIAL_QUERY_KEYS = specialQueryKeys;
/// The [Angel] app powering this service.
AngelBase app;
@ -91,7 +115,7 @@ class Service extends Routable {
/// Generates RESTful routes pointing to this class's methods.
void addRoutes() {
Map restProvider = {'provider': Providers.REST};
Map restProvider = {'provider': Providers.rest};
// Add global middleware if declared on the instance itself
Middleware before = getAnnotation(this, Middleware);
@ -107,8 +131,9 @@ class Service extends Routable {
req.serviceParams
]));
},
middleware: []..addAll(handlers)..addAll(
(indexMiddleware == null) ? [] : indexMiddleware.handlers));
middleware: []
..addAll(handlers)
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
Middleware createMiddleware = getAnnotation(this.create, Middleware);
post('/', (req, ResponseContext res) async {
@ -122,29 +147,30 @@ class Service extends Routable {
res.statusCode = 201;
return r;
},
middleware: []..addAll(handlers)..addAll(
(createMiddleware == null) ? [] : createMiddleware.handlers));
middleware: []
..addAll(handlers)
..addAll(
(createMiddleware == null) ? [] : createMiddleware.handlers));
Middleware readMiddleware = getAnnotation(this.read, Middleware);
get(
'/:id',
(req, res) async =>
await this.read(
(req, res) async => await this.read(
toId(req.params['id']),
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
middleware: []..addAll(handlers)..addAll(
(readMiddleware == null) ? [] : readMiddleware.handlers));
middleware: []
..addAll(handlers)
..addAll((readMiddleware == null) ? [] : readMiddleware.handlers));
Middleware modifyMiddleware = getAnnotation(this.modify, Middleware);
patch(
'/:id',
(req, res) async =>
await this.modify(
(req, res) async => await this.modify(
toId(req.params['id']),
await req.lazyBody(),
mergeMap([
@ -152,14 +178,15 @@ class Service extends Routable {
restProvider,
req.serviceParams
])),
middleware: []..addAll(handlers)..addAll(
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
middleware: []
..addAll(handlers)
..addAll(
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
Middleware updateMiddleware = getAnnotation(this.update, Middleware);
post(
'/:id',
(req, res) async =>
await this.update(
(req, res) async => await this.update(
toId(req.params['id']),
await req.lazyBody(),
mergeMap([
@ -167,12 +194,13 @@ class Service extends Routable {
restProvider,
req.serviceParams
])),
middleware: []..addAll(handlers)..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
middleware: []
..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
put(
'/:id',
(req, res) async =>
await this.update(
(req, res) async => await this.update(
toId(req.params['id']),
await req.lazyBody(),
mergeMap([
@ -180,34 +208,38 @@ class Service extends Routable {
restProvider,
req.serviceParams
])),
middleware: []..addAll(handlers)..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
middleware: []
..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
Middleware removeMiddleware = getAnnotation(this.remove, Middleware);
delete(
'/',
(req, res) async =>
await this.remove(
(req, res) async => await this.remove(
null,
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
middleware: []..addAll(handlers)..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
middleware: []
..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
delete(
'/:id',
(req, res) async =>
await this.remove(
(req, res) async => await this.remove(
toId(req.params['id']),
mergeMap([
{'query': req.query},
restProvider,
req.serviceParams
])),
middleware: []..addAll(handlers)..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
middleware: []
..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
// REST compliance
put('/', () => throw new AngelHttpException.notFound());

View file

@ -0,0 +1,123 @@
import 'dart:async';
typedef void _InitCallback();
/// A [StreamController] boilerplate that prevents memory leaks.
abstract class SafeCtrl<T> {
factory SafeCtrl() => new _SingleSafeCtrl();
factory SafeCtrl.broadcast() => new _BroadcastSafeCtrl();
Stream<T> get stream;
void add(T event);
void addError(error, [StackTrace stackTrace]);
Future close();
void whenInitialized(void callback());
}
class _SingleSafeCtrl<T> implements SafeCtrl<T> {
StreamController<T> _stream;
bool _hasListener = false, _initialized = false;
_InitCallback _initializer;
_SingleSafeCtrl() {
_stream = new StreamController<T>(onListen: () {
_hasListener = true;
if (!_initialized && _initializer != null) {
_initializer();
_initialized = true;
}
}, onPause: () {
_hasListener = false;
}, onResume: () {
_hasListener = true;
}, onCancel: () {
_hasListener = false;
});
}
@override
Stream<T> get stream => _stream.stream;
@override
void add(T event) {
if (_hasListener) _stream.add(event);
}
@override
void addError(error, [StackTrace stackTrace]) {
if (_hasListener) _stream.addError(error, stackTrace);
}
@override
Future close() {
return _stream.close();
}
@override
void whenInitialized(void callback()) {
if (!_initialized) {
if (!_hasListener)
_initializer = callback;
else {
_initializer();
_initialized = true;
}
}
}
}
class _BroadcastSafeCtrl<T> implements SafeCtrl<T> {
StreamController<T> _stream;
int _listeners = 0;
bool _initialized = false;
_InitCallback _initializer;
_BroadcastSafeCtrl() {
_stream = new StreamController<T>.broadcast(onListen: () {
_listeners++;
if (!_initialized && _initializer != null) {
_initializer();
_initialized = true;
}
}, onCancel: () {
_listeners--;
});
}
@override
Stream<T> get stream => _stream.stream;
@override
void add(T event) {
if (_listeners > 0) _stream.add(event);
}
@override
void addError(error, [StackTrace stackTrace]) {
if (_listeners > 0) _stream.addError(error, stackTrace);
}
@override
Future close() {
return _stream.close();
}
@override
void whenInitialized(void callback()) {
if (!_initialized) {
if (_listeners <= 0)
_initializer = callback;
else {
_initializer();
_initialized = true;
}
}
}
}

View file

@ -0,0 +1,27 @@
/// A basic server that prints "Hello, world!"
library performance.hello;
import 'dart:io';
import 'dart:isolate';
import 'package:angel_framework/angel_framework.dart';
main() {
for (int i = 0; i < Platform.numberOfProcessors - 1; i++)
Isolate.spawn(start, i + 1);
start(0);
}
void start(int id) {
var app = new Angel.custom(startShared)
..lazyParseBodies = true
..get('/', (req, res) => res.write('Hello, world!'))
..optimizeForProduction(force: true)
..fatalErrorStream.listen((e) {
print('Oops: ${e.error}');
print(e.stack);
});
app.startServer(InternetAddress.LOOPBACK_IP_V4, 3000).then((server) {
print(
'Instance #$id listening at http://${server.address.address}:${server.port}');
});
}

View file

@ -7,14 +7,15 @@ environment:
sdk: ">=1.19.0"
dependencies:
angel_model: ^1.0.0
angel_route: ^1.0.0-dev
angel_route: ">=1.0.5 <2.0.0"
body_parser: ^1.0.0-dev
container: ^0.1.2
flatten: ^1.0.0
json_god: ^2.0.0-beta
merge_map: ^1.0.0
random_string: ^0.0.1
meta: ^1.0.0
mime: ^0.9.3
random_string: ^0.0.1
dev_dependencies:
mock_request: ^1.0.0
http: ^0.11.3

View file

@ -2,58 +2,35 @@ import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:matcher/matcher.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart';
main() {
test('named constructors', () {
expect(new AngelHttpException.badRequest(),
isException(HttpStatus.BAD_REQUEST, '400 Bad Request'));
expect(new AngelHttpException.BadRequest(),
isException(HttpStatus.BAD_REQUEST, '400 Bad Request'));
expect(new AngelHttpException.notAuthenticated(),
isException(HttpStatus.UNAUTHORIZED, '401 Not Authenticated'));
expect(new AngelHttpException.NotAuthenticated(),
isException(HttpStatus.UNAUTHORIZED, '401 Not Authenticated'));
expect(new AngelHttpException.paymentRequired(),
isException(HttpStatus.PAYMENT_REQUIRED, '402 Payment Required'));
expect(new AngelHttpException.PaymentRequired(),
isException(HttpStatus.PAYMENT_REQUIRED, '402 Payment Required'));
expect(new AngelHttpException.forbidden(),
isException(HttpStatus.FORBIDDEN, '403 Forbidden'));
expect(new AngelHttpException.Forbidden(),
isException(HttpStatus.FORBIDDEN, '403 Forbidden'));
expect(new AngelHttpException.notFound(),
isException(HttpStatus.NOT_FOUND, '404 Not Found'));
expect(new AngelHttpException.NotFound(),
isException(HttpStatus.NOT_FOUND, '404 Not Found'));
expect(new AngelHttpException.methodNotAllowed(),
isException(HttpStatus.METHOD_NOT_ALLOWED, '405 Method Not Allowed'));
expect(new AngelHttpException.MethodNotAllowed(),
isException(HttpStatus.METHOD_NOT_ALLOWED, '405 Method Not Allowed'));
expect(new AngelHttpException.notAcceptable(),
isException(HttpStatus.NOT_ACCEPTABLE, '406 Not Acceptable'));
expect(new AngelHttpException.NotAcceptable(),
isException(HttpStatus.NOT_ACCEPTABLE, '406 Not Acceptable'));
expect(new AngelHttpException.methodTimeout(),
isException(HttpStatus.REQUEST_TIMEOUT, '408 Timeout'));
expect(new AngelHttpException.MethodTimeout(),
isException(HttpStatus.REQUEST_TIMEOUT, '408 Timeout'));
expect(new AngelHttpException.conflict(),
isException(HttpStatus.CONFLICT, '409 Conflict'));
expect(new AngelHttpException.Conflict(),
isException(HttpStatus.CONFLICT, '409 Conflict'));
expect(new AngelHttpException.notProcessable(),
isException(422, '422 Not Processable'));
expect(new AngelHttpException.NotProcessable(),
isException(422, '422 Not Processable'));
expect(new AngelHttpException.notImplemented(),
isException(HttpStatus.NOT_IMPLEMENTED, '501 Not Implemented'));
expect(new AngelHttpException.NotImplemented(),
isException(HttpStatus.NOT_IMPLEMENTED, '501 Not Implemented'));
expect(new AngelHttpException.unavailable(),
isException(HttpStatus.SERVICE_UNAVAILABLE, '503 Unavailable'));
expect(new AngelHttpException.Unavailable(),
isException(HttpStatus.SERVICE_UNAVAILABLE, '503 Unavailable'));
});
test('fromMap', () {
@ -91,7 +68,7 @@ class _IsException extends Matcher {
description.add('has status code $statusCode and message "$message"');
@override
bool matches(AngelHttpException item, Map matchState) {
bool matches(@checked AngelHttpException item, Map matchState) {
return item.statusCode == statusCode && item.message == message;
}
}

View file

@ -13,9 +13,7 @@ testMiddlewareMetadata(RequestContext req, ResponseContext res) async {
class QueryService extends Service {
@override
@Middleware(const ['interceptor'])
read(id, [Map params]) {
return params;
}
read(id, [Map params]) async => params;
}
main() {

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:angel_framework/src/defs.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:http/http.dart' as http;
@ -44,13 +45,8 @@ main() {
var response = await client.get("$url/todos/");
print(response.body);
expect(response.body, equals('[]'));
for (int i = 0; i < 3; i++) {
String postData = god.serialize({'text': 'Hello, world!'});
await client.post("$url/todos", headers: headers, body: postData);
}
response = await client.get("$url/todos");
print(response.body);
expect(god.deserialize(response.body).length, equals(3));
expect(JSON.decode(response.body).length, 0);
});
test('can create data', () async {
@ -100,15 +96,12 @@ main() {
test('can delete data', () async {
String postData = god.serialize({'text': 'Hello, world!'});
await client.post("$url/todos", headers: headers, body: postData);
var response = await client.delete("$url/todos/0");
var created = await client.post("$url/todos", headers: headers, body: postData).then((r) => JSON.decode(r.body));
var response = await client.delete("$url/todos/${created['id']}");
expect(response.statusCode, 200);
var json = god.deserialize(response.body);
print(json);
expect(json['text'], equals('Hello, world!'));
response = await client.get("$url/todos");
print(response.body);
expect(god.deserialize(response.body).length, equals(0));
});
});
}