diff --git a/.idea/runConfigurations/Load_Balanced_Server__PRODUCTION_.xml b/.idea/runConfigurations/Load_Balanced_Server__PRODUCTION_.xml
new file mode 100644
index 0000000..d6a3e0f
--- /dev/null
+++ b/.idea/runConfigurations/Load_Balanced_Server__PRODUCTION_.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Multithreaded_Server__PRODUCTION_.xml b/.idea/runConfigurations/Multithreaded_Server__PRODUCTION_.xml
new file mode 100644
index 0000000..a70ff59
--- /dev/null
+++ b/.idea/runConfigurations/Multithreaded_Server__PRODUCTION_.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bin/cluster.dart b/bin/cluster.dart
index d9e5e93..14090b8 100644
--- a/bin/cluster.dart
+++ b/bin/cluster.dart
@@ -7,6 +7,5 @@ import 'common.dart';
import 'dart:isolate';
main(args, SendPort sendPort) async {
- runZoned(startServer(args, clustered: true, sendPort: sendPort),
- onError: onError);
+ runZoned(startServer(args, sendPort: sendPort), onError: onError);
}
diff --git a/bin/common.dart b/bin/common.dart
index c4dba88..87f1135 100644
--- a/bin/common.dart
+++ b/bin/common.dart
@@ -5,7 +5,11 @@ import 'package:angel_diagnostics/angel_diagnostics.dart';
import 'package:angel_hot/angel_hot.dart';
import 'package:intl/intl.dart';
-startServer(args, {bool clustered: false, SendPort sendPort}) {
+/// Start a single instance of this application.
+///
+/// If a [sendPort] is provided, then the URL of the mounted server will be sent through the port.
+/// Use this if you are starting multiple instances of your server.
+startServer(args, {SendPort sendPort}) {
return () async {
var app = await createServer();
var dateFormat = new DateFormat("y-MM-dd");
@@ -13,26 +17,36 @@ startServer(args, {bool clustered: false, SendPort sendPort}) {
InternetAddress host;
int port;
- if (!clustered) {
- host = new InternetAddress(app.properties['host']);
- port = app.properties['port'];
- } else {
- host = InternetAddress.LOOPBACK_IP_V4;
- port = 0;
- }
+ // Load the right host and port from application config.
+ host = new InternetAddress(app.properties['host']);
+ // Listen on port 0 if we are using the load balancer.
+ port = sendPort != null ? 0 : app.properties['port'];
+
+ // Log requests and errors to a log file.
await app.configure(logRequests(logFile));
HttpServer server;
// Use `package:angel_hot` in any case, EXCEPT if starting in production mode.
+ //
+ // With hot-reloading, our server will automatically reload in-place on file changes,
+ // for a faster development cycle. :)
if (Platform.environment['ANGEL_ENV'] == 'production')
server = await app.startServer(host, port);
else {
var hot = new HotReloader(() async {
+ // If we are hot-reloading, we need to provide a callback
+ // to use to start a fresh instance on-the-fly.
var app = await createServer();
await app.configure(logRequests(logFile));
return app;
- }, [new Directory('config'), new Directory('lib')]);
+ },
+ // Paths we might want to listen for changes on...
+ [
+ new Directory('config'),
+ new Directory('lib'),
+ new Directory('views')
+ ]);
server = await hot.startServer(host, port);
}
diff --git a/bin/multi_server.dart b/bin/multi_server.dart
index 65deaeb..9ccc5de 100644
--- a/bin/multi_server.dart
+++ b/bin/multi_server.dart
@@ -2,7 +2,9 @@
/// This is intended to replace Nginx in your web stack.
/// Either use this or another reverse proxy, but there is no real
/// reason to use them together.
-library angel.multiserver;
+///
+/// In most cases, you should run `scaled_server.dart` instead of this file.
+library angel.multi_server;
import 'dart:io';
import 'package:angel_compress/angel_compress.dart';
@@ -10,6 +12,10 @@ import 'package:angel_multiserver/angel_multiserver.dart';
final Uri cluster = Platform.script.resolve('cluster.dart');
+/// The number of isolates to spawn. You might consider starting one instance
+/// per processor core on your machine.
+final int nInstances = Platform.numberOfProcessors;
+
main() async {
var app = new LoadBalancer();
// Or, for SSL:
@@ -21,8 +27,8 @@ main() async {
// Cache static assets - just to lower response time
await app.configure(cacheResponses(filters: [new RegExp(r'images/\.*')]));
- // Start up 5 instances of our main application
- await app.spawnIsolates(cluster, count: 5);
+ // Start up multiple instances of our main application.
+ await app.spawnIsolates(cluster, count: nInstances);
app.onCrash.listen((_) async {
// Boot up a new instance on crash
@@ -33,4 +39,5 @@ main() async {
var port = 3000;
var server = await app.startServer(host, port);
print('Listening at http://${server.address.address}:${server.port}');
+ print('Load-balancing $nInstances instance(s)');
}
diff --git a/bin/scaled_server.dart b/bin/scaled_server.dart
new file mode 100644
index 0000000..b42c840
--- /dev/null
+++ b/bin/scaled_server.dart
@@ -0,0 +1,67 @@
+#!/usr/bin/env dart
+
+/// Most Angel applications will not need to use the load balancer.
+/// Instead, you can start up a multi-threaded cluster.
+library angel.scaled_server;
+
+import 'dart:io';
+import 'dart:isolate';
+import 'package:angel_compress/angel_compress.dart';
+import 'package:angel_multiserver/angel_multiserver.dart';
+import 'package:angel/angel.dart';
+
+/// The number of isolates to spawn. You might consider starting one instance
+/// per processor core on your machine.
+final int nInstances = Platform.numberOfProcessors;
+
+main() {
+ var startupPort = new ReceivePort();
+ List startupMessages = [];
+
+ // Start up multiple instances of our application.
+ for (int i = 0; i < nInstances; i++) {
+ Isolate.spawn(isolateMain, [i, startupPort.sendPort]);
+ }
+
+ int nStarted = 0;
+
+ // Listen for notifications of application startup...
+ startupPort.listen((String startupMessage) {
+ startupMessages.add(startupMessage);
+
+ if (++nStarted == nInstances) {
+ // Keep track of how many instances successfully started up,
+ // and print a success message when they all boot.
+ startupMessages.forEach(print);
+ print('Spawned $nInstances instance(s) of Angel.');
+ }
+ });
+}
+
+void isolateMain(List args) {
+ int instanceId = args[0];
+ SendPort startupPort = args[1];
+
+ createServer().then((app) async {
+ // Response compression via GZIP.
+ //
+ // See the documentation here:
+ // https://github.com/angel-dart/compress
+ app.responseFinalizers.add(gzip());
+
+ // Cache static assets - just to lower response time.
+ //
+ // See the documentation here:
+ // https://github.com/angel-dart/multiserver
+ //
+ // Here is an example of response caching:
+ // https://github.com/angel-dart/multiserver/blob/master/example/cache.dart
+ await app.configure(cacheResponses(filters: [new RegExp(r'images/\.*')]));
+
+ var server = await app.startServer(
+ InternetAddress.ANY_IP_V4, app.properties['port'] ?? 3000);
+
+ // Send a notification back to the main isolate
+ startupPort.send('Instance #$instanceId listening at http://${server.address.address}:${server.port}');
+ });
+}
diff --git a/lib/angel.dart b/lib/angel.dart
index 2a8a5b2..28668ab 100644
--- a/lib/angel.dart
+++ b/lib/angel.dart
@@ -9,8 +9,13 @@ import 'src/services/services.dart' as services;
/// Creates and configures the server instance.
Future createServer() async {
- Angel app = new Angel();
+ /// Passing `startShared` to the constructor allows us to start multiple
+ /// instances of our application concurrently, listening on a single port.
+ ///
+ /// This effectively lets us multi-thread the application.
+ var app = new Angel.custom(startShared);
+ /// Set up our application, using three plug-ins defined with this project.
await app.configure(configuration.configureServer);
await app.configure(services.configureServer);
await app.configure(routes.configureServer);
diff --git a/lib/src/config/config.dart b/lib/src/config/config.dart
index e2b4e29..810f40d 100644
--- a/lib/src/config/config.dart
+++ b/lib/src/config/config.dart
@@ -3,26 +3,27 @@ library angel.config;
import 'dart:io';
import 'package:angel_common/angel_common.dart';
-// import 'package:angel_multiserver/angel_multiserver.dart';
-import 'package:mongo_dart/mongo_dart.dart';
import 'plugins/plugins.dart' as plugins;
/// This is a perfect place to include configuration and load plug-ins.
configureServer(Angel app) async {
- await app.configure(loadConfigurationFile());
- var db = new Db(app.mongo_db);
- await db.open();
- app.container.singleton(db);
-
- await app.configure(mustache(new Directory('views')));
- await plugins.configureServer(app);
-
-
- // Uncomment this to enable session synchronization across instances.
- // This will add the overhead of querying a database at the beginning
- // and end of every request. Thus, it should only be activated if necessary.
+ // Load configuration from the `config/` directory.
//
- // For applications of scale, it is better to steer clear of session use
- // entirely.
- // await app.configure(new MongoSessionSynchronizer(db.collection('sessions')));
+ // See: https://github.com/angel-dart/configuration
+ await app.configure(loadConfigurationFile());
+
+ // Configure our application to render Mustache templates from the `views/` directory.
+ //
+ // See: https://github.com/angel-dart/mustache
+ await app.configure(mustache(new Directory('views')));
+
+ // Apply another plug-ins, i.e. ones that *you* have written.
+ //
+ // Typically, the plugins in `lib/src/config/plugins/plugins.dart` are plug-ins
+ // that add functionality specific to your application.
+ //
+ // If you write a plug-in that you plan to use again, or are
+ // using one created by the community, include it in
+ // `lib/src/config/config.dart`.
+ await plugins.configureServer(app);
}
diff --git a/lib/src/models/user.dart b/lib/src/models/user.dart
index bf97c1d..52f4e11 100644
--- a/lib/src/models/user.dart
+++ b/lib/src/models/user.dart
@@ -2,6 +2,26 @@ library angel.models.user;
import 'package:angel_framework/common.dart';
+// Model classes in Angel, as a convention, should extend the `Model`
+// class.
+//
+// Angel doesn't box you into using a specific ORM. In fact, you might not
+// need one at all.
+//
+// The out-of-the-box configuration for Angel is to assume
+// all data you handle is a Map. You might consider leaving it this way, and just
+// (de)serializing data when you need typing support.
+//
+// If you use a `TypedService`, then Angel will perform (de)serialization for you automatically:
+// https://github.com/angel-dart/angel/wiki/TypedService
+//
+// You also have the option of using a source-generated serialization library.
+// Consider `package:owl` (not affiliated with Angel):
+// https://github.com/agilord/owl
+//
+// The `Model` class has no server-side dependency, and thus you can use it as-is, cross-platform.
+// This is good for full-stack applications, as you do not have to maintain duplicate class files. ;)
+
class User extends Model {
@override
String id;
diff --git a/lib/src/routes/routes.dart b/lib/src/routes/routes.dart
index 251b5ed..3a80b03 100644
--- a/lib/src/routes/routes.dart
+++ b/lib/src/routes/routes.dart
@@ -1,43 +1,92 @@
/// This app's route configuration.
library angel.routes;
+import 'dart:io';
import 'package:angel_common/angel_common.dart';
import 'controllers/controllers.dart' as controllers;
+/// Adds global middleware to the application.
+///
+/// Use these to apply functionality to requests before business logic is run.
+///
+/// More on the request lifecycle:
+/// https://github.com/angel-dart/angel/wiki/Request-Lifecycle
configureBefore(Angel app) async {
app.before.add(cors());
}
/// Put your app routes here!
+///
+/// See the wiki for information about routing, requests, and responses:
+/// * https://github.com/angel-dart/angel/wiki/Basic-Routing
+/// * https://github.com/angel-dart/angel/wiki/Requests-&-Responses
configureRoutes(Angel app) async {
+ // Render `views/hello.mustache` when a user visits the application root.
app.get('/', (req, ResponseContext res) => res.render('hello'));
}
+/// Configures fallback middleware.
+///
+/// Use these to run generic actions on requests that were not terminated by
+/// earlier request handlers.
+///
+/// Note that these middleware might not always run.
+///
+/// More on the request lifecycle: https://github.com/angel-dart/angel/wiki/Request-Lifecycle
configureAfter(Angel app) async {
- // Uncomment this to proxy over pub serve while in development:
- // await app.configure(new PubServeLayer());
-
- // Static server at /web or /build/web, depending on if in production
+ // Uncomment this to proxy over `pub serve` while in development.
+ // This is a useful feature for full-stack applications, especially if you
+ // are using Angular2.
//
- // In production, `Cache-Control` headers will also be enabled.
+ // For documentation, see `package:angel_proxy`:
+ // https://github.com/angel-dart/proxy
+ //
+ // await app.configure(new PubServeLayer());
+
+ // Mount static server at /web or /build/web, depending on if
+ // you are running in production mode. `Cache-Control` headers will also be enabled.
+ //
+ // Read the following two sources for documentation:
+ // * https://medium.com/the-angel-framework/serving-static-files-with-the-angel-framework-2ddc7a2b84ae
+ // * https://github.com/angel-dart/static
await app.configure(new CachingVirtualDirectory());
// Set our application up to handle different errors.
+ //
+ // Read the following two sources for documentation:
+ // * https://github.com/angel-dart/angel/wiki/Error-Handling
+ // * https://github.com/angel-dart/errors
var errors = new ErrorHandler(handlers: {
+ // Display a 404 page if no resource is found.
404: (req, res) async =>
res.render('error', {'message': 'No file exists at ${req.path}.'}),
+
+ // On generic errors, give information about why the application failed.
+ //
+ // An `AngelHttpException` instance will be present in `req.properties`
+ // as `error`.
500: (req, res) async => res.render('error', {'message': req.error.message})
});
+ // Use a fatal error handler to attempt to resolve any issues that
+ // result in Angel not being able to send the user a response.
errors.fatalErrorHandler = (AngelFatalError e) async {
- var req = await RequestContext.from(e.request, app);
- var res = new ResponseContext(e.request.response, app);
- res.render('error', {'message': 'Internal Server Error: ${e.error}'});
- await app.sendResponse(e.request, req, res);
+ try {
+ // Manually create a request and response context.
+ var req = await RequestContext.from(e.request, app);
+ var res = new ResponseContext(e.request.response, app);
+
+ // *Attempt* to render an error page.
+ res.render('error', {'message': 'Internal Server Error: ${e.error}'});
+ await app.sendResponse(e.request, req, res);
+ } catch (_) {
+ // If execution fails here, there is nothing we can do.
+ stderr..writeln('Fatal error: ${e.error}')..writeln(e.stack);
+ }
};
- // Throw a 404 if no route matched the request
- app.after.add(errors.throwError());
+ // Throw a 404 if no route matched the request.
+ app.after.add(() => throw new AngelHttpException.notFound());
// Handle errors when they occur, based on outgoing status code.
// By default, requests will go through the 500 handler, unless
@@ -45,19 +94,45 @@ configureAfter(Angel app) async {
// registered.
app.after.add(errors.middleware());
- // Pass AngelHttpExceptions through handler as well
- await app.configure(errors);
-
- // Compress via GZIP
- // Ideally you'll run this on a `multiserver` instance, but if not,
- // feel free to knock yourself out!
+ // Pass AngelHttpExceptions through handler as well.
//
- // app.responseFinalizers.add(gzip());
+ // Again, here is the error handling documentation:
+ // * https://github.com/angel-dart/angel/wiki/Error-Handling
+ // * https://github.com/angel-dart/errors
+ await app.configure(errors);
}
+/// Adds response finalizers to the application.
+///
+/// These run after every request.
+///
+/// See more on the request lifecycle here:
+/// https://github.com/angel-dart/angel/wiki/Request-Lifecycle
+configureFinalizers(Angel app) async {}
+
+/// Adds routes to our application.
+///
+/// See the wiki for information about routing, requests, and responses:
+/// * https://github.com/angel-dart/angel/wiki/Basic-Routing
+/// * https://github.com/angel-dart/angel/wiki/Requests-&-Responses
configureServer(Angel app) async {
+ // The order in which we run these plug-ins is relatively significant.
+ // Try not to change it.
+
+ // Add global middleware.
await configureBefore(app);
+
+ // Typically, you want to mount controllers first, after any global middleware.
await app.configure(controllers.configureServer);
+
+ // Next, you can add any supplemental routes.
await configureRoutes(app);
+
+ // Add handlers to run after requests are handled.
+ //
+ // See the request lifecycle docs to find out why these two
+ // are separate:
+ // https://github.com/angel-dart/angel/wiki/Request-Lifecycle
await configureAfter(app);
+ await configureFinalizers(app);
}
diff --git a/lib/src/services/services.dart b/lib/src/services/services.dart
index 9c29925..6230710 100644
--- a/lib/src/services/services.dart
+++ b/lib/src/services/services.dart
@@ -2,10 +2,16 @@
library angel.services;
import 'package:angel_common/angel_common.dart';
-import 'package:mongo_dart/mongo_dart.dart';
import 'user.dart' as user;
+/// Configure our application to use *services*.
+/// Services must be wired to the app via `app.use`.
+///
+/// They provide many benefits, such as instant REST API generation,
+/// and respond to both REST and WebSockets.
+///
+/// Read more here:
+/// https://github.com/angel-dart/angel/wiki/Service-Basics
configureServer(Angel app) async {
- Db db = app.container.make(Db);
- await app.configure(user.configureServer(db));
+ await app.configure(user.configureServer());
}
diff --git a/lib/src/services/user.dart b/lib/src/services/user.dart
index b5927ab..45f1526 100644
--- a/lib/src/services/user.dart
+++ b/lib/src/services/user.dart
@@ -1,16 +1,29 @@
import 'package:angel_common/angel_common.dart';
import 'package:angel_framework/hooks.dart' as hooks;
import 'package:crypto/crypto.dart' show sha256;
-import 'package:mongo_dart/mongo_dart.dart';
import 'package:random_string/random_string.dart' as rs;
import '../models/user.dart';
import '../validators/user.dart';
export '../models/user.dart';
-configureServer(Db db) {
+/// Sets up a service mounted at `api/users`.
+///
+/// In the real world, you will want to hook this up to a database.
+/// However, for the sake of the boilerplate, an in-memory service is used,
+/// so that users are not tied into using just one database. :)
+configureServer() {
return (Angel app) async {
- app.use('/api/users',
- new TypedService(new MongoService(db.collection('users'))));
+ // A TypedService can be used to serialize and deserialize data to a class, somewhat like an ORM.
+ //
+ // See here: https://github.com/angel-dart/angel/wiki/TypedService
+ app.use('/api/users', new TypedService(new MapService()));
+
+ // Configure hooks for the user service.
+ // Hooks can be used to add additional functionality, or change the behavior
+ // of services, and run on any service, regardless of which database you are using.
+ //
+ // If you have not already, *definitely* read the service hook documentation:
+ // https://github.com/angel-dart/angel/wiki/Hooks
var service = app.service('api/users') as HookedService;
diff --git a/lib/src/validators/user.dart b/lib/src/validators/user.dart
index 1d8bf2f..4cb72ae 100644
--- a/lib/src/validators/user.dart
+++ b/lib/src/validators/user.dart
@@ -1,5 +1,9 @@
import 'package:angel_validate/angel_validate.dart';
+// Validators can be used on the server, in the browser, and even in Flutter.
+//
+// It is highly recommended that you read the documentation:
+// https://github.com/angel-dart/validate
final Validator USER = new Validator({
'email': [isString, isNotEmpty, isEmail],
'username': [isString, isNotEmpty],
diff --git a/pubspec.yaml b/pubspec.yaml
index d0854e1..8b226af 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,17 +1,21 @@
name: angel
description: An easily-extensible web server framework in Dart.
-publish_to: none
+publish_to: none # Ensure we don't accidentally publish our private code! ;)
environment:
sdk: ">=1.19.0"
homepage: https://github.com/angel-dart/angel
dependencies:
- angel_common: ^1.0.0
- angel_configuration: ^1.0.0
- angel_hot: ^1.0.0-rc.1
- angel_multiserver: ^1.0.0
+ angel_common: ^1.0.0 # Bundles the most commonly-used Angel packages.
+ angel_configuration: ^1.0.0 # Included in `angel_common`, but also exposes a transformer
+ angel_hot: ^1.0.0-rc.1 # Hot-reloading support. :)
+ angel_multiserver: ^1.0.0 # Helpful for applications running a scale.
dev_dependencies:
grinder: ^0.8.0
http: ^0.11.3
test: ^0.12.13
-transformers:
+transformers:
+ # Injects data from application configuration into Dart code.
+ #
+ # Documentation:
+ # https://github.com/angel-dart/configuration
- angel_configuration
diff --git a/test/services/users_test.dart b/test/services/users_test.dart
index dbc627e..0ffdcd2 100644
--- a/test/services/users_test.dart
+++ b/test/services/users_test.dart
@@ -1,27 +1,42 @@
import 'dart:io';
import 'package:angel/angel.dart';
-import 'package:angel_framework/angel_framework.dart';
import 'package:angel_test/angel_test.dart';
import 'package:test/test.dart';
+// Angel also includes facilities to make testing easier.
+//
+// `package:angel_test` ships a client that can test
+// both plain HTTP and WebSockets.
+//
+// Tests do not require your server to actually be mounted on a port,
+// so they will run faster than they would in other frameworks, where you
+// would have to first bind a socket, and then account for network latency.
+//
+// See the documentation here:
+// https://github.com/angel-dart/test
+//
+// If you are unfamiliar with Dart's advanced testing library, you can read up
+// here:
+// https://github.com/dart-lang/test
+
main() async {
- Angel app;
TestClient client;
setUp(() async {
- app = await createServer();
+ var app = await createServer();
client = await connectTo(app);
});
tearDown(() async {
await client.close();
- app = null;
});
test('index users', () async {
- final response = await client.get('/api/users');
+ // Request a resource at the given path.
+ var response = await client.get('/api/users');
// By default, we locked this away from the Internet...
+ // Expect a 403 response.
expect(response, hasStatus(HttpStatus.FORBIDDEN));
});
}
diff --git a/tool/grind.dart b/tool/grind.dart
index 1451802..d26a26a 100644
--- a/tool/grind.dart
+++ b/tool/grind.dart
@@ -1,3 +1,9 @@
+// Grinder is not part of Angel, but you may consider using it
+// to run tasks, a la Gulp.
+//
+// See its documentation here:
+// https://github.com/google/grinder.dart
+
import 'package:grinder/grinder.dart';
main(args) => grind(args);