diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml
index 06615469..264fd455 100644
--- a/.idea/libraries/Dart_Packages.xml
+++ b/.idea/libraries/Dart_Packages.xml
@@ -19,7 +19,7 @@
-
+
@@ -399,7 +399,7 @@
-
+
diff --git a/.idea/runConfigurations/performance__hello.xml b/.idea/runConfigurations/performance__hello.xml
new file mode 100644
index 00000000..910062a6
--- /dev/null
+++ b/.idea/runConfigurations/performance__hello.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 0aa163ff..b7eb8194 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -2,10 +2,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -29,70 +47,14 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -100,38 +62,20 @@
-
-
-
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -148,36 +92,36 @@
- status
- createdAt
- preInj
- p
- printDebug
- d
- debug
- _printDebug
- use(
- _injec
- writeln
- write(
- _
- index
- typed
- x
- _afterP
- _beforeP
- optim
- ??=
dump
void li
_onError
Unhandled exc
handleAn
Tom Fo
- handleRequest(
transform(
grab(
json(
+ cons
+ const
+ call
+ use(
+ value
+ whenI
+ before
+ afterProcessed
+ onco
+ StreamC
+ onCo
+ fatalErrorStream
+ handleReq
+ handleRequest(
+ _Lockabl
+ takeBy
+ from
+ createR
+ sendRes
+ replaceAll
+ noSuch
_isClosed
@@ -187,9 +131,14 @@
update
remove
data
+ event
+ after
+ fatalErrorStream
+ onController
C:\Users\thosa\Source\Angel\framework\lib
+ C:\Users\thosa\Source\Angel\framework\lib\src\http
@@ -203,37 +152,43 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
@@ -267,8 +222,6 @@
-
-
@@ -291,21 +244,11 @@
-
-
-
-
-
-
-
-
-
-
-
+
-
+
@@ -363,15 +306,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -379,6 +338,7 @@
+
-
+
@@ -435,6 +395,12 @@
+
+
+
+
+
+
@@ -473,6 +439,25 @@
false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -515,24 +500,28 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -574,7 +563,8 @@
-
+
+
1481237183504
@@ -751,43 +741,50 @@
1499806099923
-
+
+ 1500824472581
+
+
+
+ 1500824472581
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
@@ -802,28 +799,28 @@
-
+
-
-
+
-
+
+
+
+
+
-
-
-
@@ -857,119 +854,14 @@
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -998,13 +890,6 @@
-
-
-
-
-
-
-
@@ -1033,13 +918,6 @@
-
-
-
-
-
-
-
@@ -1047,13 +925,6 @@
-
-
-
-
-
-
-
@@ -1061,13 +932,6 @@
-
-
-
-
-
-
-
@@ -1089,20 +953,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1124,21 +974,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1146,14 +981,6 @@
-
-
-
-
-
-
-
-
@@ -1162,27 +989,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1191,13 +997,6 @@
-
-
-
-
-
-
-
@@ -1206,10 +1005,203 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1217,18 +1209,72 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6719ced..db0ebd82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 00000000..518eb901
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,2 @@
+analyzer:
+ strong-mode: true
\ No newline at end of file
diff --git a/lib/hooks.dart b/lib/hooks.dart
index 19eca064..4e165c47 100644
--- a/lib/hooks.dart
+++ b/lib/hooks.dart
@@ -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);
diff --git a/lib/src/extensible.dart b/lib/src/extensible.dart
deleted file mode 100644
index 45ad12d4..00000000
--- a/lib/src/extensible.dart
+++ /dev/null
@@ -1,3 +0,0 @@
-library angel_framework.extensible;
-
-export 'package:angel_route/src/extensible.dart';
\ No newline at end of file
diff --git a/lib/src/fast_name_from_symbol.dart b/lib/src/fast_name_from_symbol.dart
new file mode 100644
index 00000000..1460117d
--- /dev/null
+++ b/lib/src/fast_name_from_symbol.dart
@@ -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);
+}
\ No newline at end of file
diff --git a/lib/src/http/angel_base.dart b/lib/src/http/angel_base.dart
index 2c869965..297f1356 100644
--- a/lib/src/http/angel_base.dart
+++ b/lib/src/http/angel_base.dart
@@ -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 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);
+ }
}
\ No newline at end of file
diff --git a/lib/src/http/controller.dart b/lib/src/http/controller.dart
index 32b5c1bd..beb760dd 100644
--- a/lib/src/http/controller.dart
+++ b/lib/src/http/controller.dart
@@ -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 named = {};
+ final Map 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 optional = [];
+ final List 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 {
diff --git a/lib/src/http/hooked_service.dart b/lib/src/http/hooked_service.dart
index 2e2cc77c..3e3fa0d5 100644
--- a/lib/src/http/hooked_service.dart
+++ b/lib/src/http/hooked_service.dart
@@ -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 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 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 beforeAllStream() {
var ctrl = new StreamController();
_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 afterAllStream() {
var ctrl = new StreamController();
_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 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 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 ALL = all;
/// Use this to end processing of an event.
void cancel([result]) {
diff --git a/lib/src/http/request_context.dart b/lib/src/http/request_context.dart
index 73dcbd5e..086ed951 100644
--- a/lib/src/http/request_context.dart
+++ b/lib/src/http/request_context.dart
@@ -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 _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);
+ }
}
diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart
index 9ce80e0b..08bd6972 100644
--- a/lib/src/http/response_context.dart
+++ b/lib/src/http/response_context.dart
@@ -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 _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 _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 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.from(_data);
+ var r = new Uint8List.fromList(_data);
clear();
return r;
}
diff --git a/lib/src/http/routable.dart b/lib/src/http/routable.dart
index c01cfd61..6102b503 100644
--- a/lib/src/http/routable.dart
+++ b/lib/src/http/routable.dart
@@ -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 _controllers = {};
final Map _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);
+ }
}
}
diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart
index 357e3eaa..8a85cd53 100644
--- a/lib/src/http/server.dart
+++ b/lib/src/http/server.dart
@@ -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 _afterProcessed =
- new StreamController.broadcast();
- StreamController _beforeProcessed =
- new StreamController.broadcast();
- StreamController _fatalErrorStream =
- new StreamController.broadcast();
- StreamController _onController =
- new StreamController.broadcast();
+ SafeCtrl _afterProcessed = new SafeCtrl.broadcast();
+ SafeCtrl _beforeProcessed =
+ new SafeCtrl.broadcast();
+ SafeCtrl _fatalErrorStream =
+ new SafeCtrl.broadcast();
+ SafeCtrl _onController = new SafeCtrl.broadcast();
+
final List _children = [];
Router _flattened;
bool _isProduction;
Angel _parent;
+ final Map _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 createResponseContext(HttpResponse response) async =>
- new ResponseContext(response, this)
- ..serializer = (_serializer ?? god.serialize);
+ Future createResponseContext(HttpResponse response) =>
+ new Future.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 _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(
+ 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);
diff --git a/lib/src/http/service.dart b/lib/src/http/service.dart
index 9a27d3f4..ab58c037 100644
--- a/lib/src/http/service.dart
+++ b/lib/src/http/service.dart
@@ -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 SPECIAL_QUERY_KEYS = const [
+ static const List specialQueryKeys = const [
r'$limit',
r'$sort',
'page',
'token'
];
+ /// Use [specialQueryKeys] instead.
+ @deprecated
+ static const List 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());
diff --git a/lib/src/safe_stream_controller.dart b/lib/src/safe_stream_controller.dart
new file mode 100644
index 00000000..558d1082
--- /dev/null
+++ b/lib/src/safe_stream_controller.dart
@@ -0,0 +1,123 @@
+import 'dart:async';
+
+typedef void _InitCallback();
+
+/// A [StreamController] boilerplate that prevents memory leaks.
+abstract class SafeCtrl {
+ factory SafeCtrl() => new _SingleSafeCtrl();
+
+ factory SafeCtrl.broadcast() => new _BroadcastSafeCtrl();
+
+ Stream get stream;
+
+ void add(T event);
+
+ void addError(error, [StackTrace stackTrace]);
+
+ Future close();
+
+ void whenInitialized(void callback());
+}
+
+class _SingleSafeCtrl implements SafeCtrl {
+ StreamController _stream;
+ bool _hasListener = false, _initialized = false;
+ _InitCallback _initializer;
+
+ _SingleSafeCtrl() {
+ _stream = new StreamController(onListen: () {
+ _hasListener = true;
+
+ if (!_initialized && _initializer != null) {
+ _initializer();
+ _initialized = true;
+ }
+ }, onPause: () {
+ _hasListener = false;
+ }, onResume: () {
+ _hasListener = true;
+ }, onCancel: () {
+ _hasListener = false;
+ });
+ }
+
+ @override
+ Stream 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 implements SafeCtrl {
+ StreamController _stream;
+ int _listeners = 0;
+ bool _initialized = false;
+ _InitCallback _initializer;
+
+ _BroadcastSafeCtrl() {
+ _stream = new StreamController.broadcast(onListen: () {
+ _listeners++;
+
+ if (!_initialized && _initializer != null) {
+ _initializer();
+ _initialized = true;
+ }
+ }, onCancel: () {
+ _listeners--;
+ });
+ }
+
+ @override
+ Stream 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;
+ }
+ }
+ }
+}
diff --git a/performance/hello/main.dart b/performance/hello/main.dart
new file mode 100644
index 00000000..405c6cde
--- /dev/null
+++ b/performance/hello/main.dart
@@ -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}');
+ });
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 267e1702..344c0dd4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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
diff --git a/test/exception_test.dart b/test/exception_test.dart
index f1db47ff..ae00dc5d 100644
--- a/test/exception_test.dart
+++ b/test/exception_test.dart
@@ -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;
}
}
diff --git a/test/routing_test.dart b/test/routing_test.dart
index 183ac2f6..e6c955de 100644
--- a/test/routing_test.dart
+++ b/test/routing_test.dart
@@ -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() {
diff --git a/test/services_test.dart b/test/services_test.dart
index fa402f0a..bfdc4595 100644
--- a/test/services_test.dart
+++ b/test/services_test.dart
@@ -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));
});
});
}