Using WebSockets with Angel Services

Tobe O
The Angel Framework
4 min readFeb 12, 2017

WebSockets are a common way to implement real-time data transfer between clients and servers, and Angel has tailored support for it. Today, we will build a simple application where users drop bombs (really just send an action to a service over WebSockets), and are notified later when the bombs explode.

Let’s get started, by creating our backend. The easiest way to bootstrap an Angel application is to use the Angel CLI:

$ pub global activate angel_cli
$ angel init explode
# Optional: Rename the project from 'angel' to 'explode'
$ cd explode && angel rename explode

Next, head to lib/src/routes/routes.dart, and remove the route that serves a default index page:

// Delete this:
app.get('/', (req, ResponseContext res) => res.render('hello'));

Let’s create two services: Bomb and Explosion. Optionally, you may also create corresponding data model classes.

First is the BombService.

class BombService extends Service {
final List<Bomb> bombs = [];
@override
index([params]) async => bombs;
@override
create(data, [params]) async {
// We're actually going to ignore the input data
// in every case, and just spawn a new bomb.
var bomb = new Bomb(id: bombs.length.toString());
bombs.add(bomb);
return bomb;
}
}

Next, explosions.

AngelConfigurer configureServer() {
return (Angel app) async {
// The `AnonymousService` class is a shortcut
// you can use to create a service with minimal
// functionality, without creating a whole new
// class.
app.use('/explosions', new AnonymousService(create: (Explosion data, [params]) {
// This service will simply echo whatever we give to it.
//
// We expect `data` to be a Map with a `bombId`.
// Validation will be covered in a later tutorial.
return {'bombId': data.bombId};
}));
// Use service hooks to add additional functionality to
// services.
//
// This pattern is preferred, as hooks can be applied to any
// service, regardless of what data store (if any) is being
// used.
var service = app.service('explosions') as HookedService;
service.beforeAll(hooks.disable()); // Disable for all clients
};
}

In real life, bombs set off after a defined period of time elapses. We will mimic this functionality by firing an explosion event 10 seconds after every bomb is spawned. We can do this with a service hook:

AngelConfigurer configureServer() {
return (Angel app) async {
app.use('/bombs', new BombService());
var service = app.service('bombs') as HookedService;service.afterCreated.listen((HookedServiceEvent e) {
// Bombs should explode 10 seconds after creation.
var explosionService = e.service.app.service('explosions');
var bomb = e.result as Bomb;
new Future.delayed(new Duration(seconds: 10)).then((_) {
print('KABOOM!');
explosionService.create(new Explosion(bombId: bomb.id));
});
});
};
}

Now, we have two (very simple) services that can interact with each other. We want to notify our clients in real-time whenever these interactions occur; WebSockets are a perfect medium to do so. Install angel_websocket:

dependencies:
angel_websocket: ^1.0.0

Finally, we run the AngelWebSocket plug-in just after mounting our services (it actually doesn’t matter whether you do this before or after). It listens to every HookedService we mount, and broadcasts events to all connected clients.

configureServer(Angel app) async {
Db db = app.container.make(Db);
await app.configure(new AngelWebSocket(debug: true));
await app.configure(Bomb.configureServer());
await app.configure(Explosion.configureServer());
await app.configure(User.configureServer(db));
}

If we setdebug to true, then when errors occur within our services, the stack traces will be sent over WebSockets, along with the error message. This makes debugging easier. :)

Next, a simple front-end:

import 'dart:html';
import 'package:angel/src/models/models.dart';
import 'package:angel_websocket/browser.dart';
// `WebSockets` extends the main `Angel` class from `angel_client` ;)
final WebSockets app = new WebSockets('ws://${window.location.host}/ws');
final UListElement $explosions = querySelector('#explosions');
final ButtonElement $btn = querySelector('button');
main() async {
// You MUST connect your WebSocket to the server!
await app.connect();
WebSocketsService bombService = app.service('bombs'),
explosionService = app.service('explosions',
deserializer: (Map data) => new Explosion.fromJson(data));
app.onError.listen((e) {
window.alert('Whoops: $e');
// If `debug` is true in our server-side WebSocket plug-in,
// it will send us stack traces, which makes it easier to debug.
window.console.error(e);
e.errors.map(window.console.error);
});
explosionService.onCreated.listen((e) {
var explosion = e.data as Explosion;
$explosions.children
.add(new LIElement()..text = 'Bomb #${explosion.bombId} exploded!');
});
$btn.onClick.listen((_) => bombService.create({}).catchError((e) {
window.alert('Bomb creation error: $e');
}));
}

The most important things to note here:

  • You must call connect() on a WebSocket client. Don’t forget!
  • Client-side services can use a deserializer function to deserialize JSON into typed data, without having to include dart:mirrors in compiled code.
  • Error events are received by the central client only. They are automatically decoded as AngelHttpException instances, the same class used on the server side.
  • WebSocketsService methods return no values, as they function entirely asynchronously. You must use events, such as onCreated, to capture results.
  • The model classes we made above do not depend on any dart:io code, and thus can be used anywhere.

Now, you have a working server that fires events over WebSockets, and a client that handles these events using a very similar API. You can use this pattern for notifications, etc. as well. In addition, the angel_websocket package contains a WebSocketController class to build hybrid controllers that serve both plain HTTP and real-time WebSockets.

You’ve probably noticed just by looking at the boilerplate that there is much more to the Angel framework than just WebSockets. Go ahead and explore it for yourself. Feedback is greatly appreciated, as the library is far from perfect.

--

--