This commit is contained in:
thosakwe 2017-02-28 09:15:34 -05:00
parent fe1f85bfa8
commit 01db4aa9eb
8 changed files with 205 additions and 42 deletions

View file

@ -1,5 +1,5 @@
# angel_websocket
[![1.0.1](https://img.shields.io/badge/pub-1.0.1-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket)
[![1.0.2](https://img.shields.io/badge/pub-1.0.2-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket)
[![build status](https://travis-ci.org/angel-dart/websocket.svg)](https://travis-ci.org/angel-dart/websocket)
WebSocket plugin for Angel.
@ -84,6 +84,9 @@ main() async {
// Happens asynchronously
Cars.create({"brand": "Toyota"});
// Authenticate a WebSocket, if you were not already authenticated...
app.authenticateViaJwt('<some-jwt>');
// Listen for arbitrary events
app.on['custom_event'].listen((event) {
// For example, this might be sent by a
@ -127,5 +130,8 @@ main() async {
// Happens asynchronously
Cars.create({"year": 2016, "brand": "Toyota", "make": "Camry"});
// Authenticate a WebSocket, if you were not already authenticated...
app.authenticateViaJwt('<some-jwt>');
}
```

View file

@ -1,6 +1,7 @@
/// WebSocket plugin for Angel.
library angel_websocket;
const String ACTION_AUTHENTICATE = 'authenticate';
const String ACTION_INDEX = 'index';
const String ACTION_READ = 'read';
const String ACTION_CREATE = 'create';
@ -8,6 +9,7 @@ const String ACTION_MODIFY = 'modify';
const String ACTION_UPDATE = 'update';
const String ACTION_REMOVE = 'remove';
const String EVENT_AUTHENTICATED = 'authenticated';
const String EVENT_ERROR = 'error';
const String EVENT_INDEXED = 'indexed';
const String EVENT_READ = 'read';

View file

@ -12,11 +12,14 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
/// An [Angel] client that operates across WebSockets.
abstract class BaseWebSocketClient extends BaseAngelClient {
Duration _reconnectInterval;
WebSocketChannel _socket;
final StreamController _onData = new StreamController();
final StreamController<WebSocketEvent> _onAllEvents =
new StreamController<WebSocketEvent>();
final StreamController<AngelAuthResult> _onAuthenticated =
new StreamController<AngelAuthResult>();
final StreamController<AngelHttpException> _onError =
new StreamController<AngelHttpException>();
final StreamController<Map<String, WebSocketEvent>> _onServiceEvent =
@ -32,6 +35,9 @@ abstract class BaseWebSocketClient extends BaseAngelClient {
/// Fired on all events.
Stream<WebSocketEvent> get onAllEvents => _onAllEvents.stream;
/// Fired whenever a WebSocket is successfully authenticated.
Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream;
/// A broadcast stream of data coming from the [socket].
///
/// Mostly just for internal use.
@ -51,18 +57,67 @@ abstract class BaseWebSocketClient extends BaseAngelClient {
/// The [WebSocketChannel] underneath this instance.
WebSocketChannel get socket => _socket;
BaseWebSocketClient(http.BaseClient client, String basePath)
: super(client, basePath) {}
/// If `true` (default), then the client will automatically try to reconnect to the server
/// if the socket closes.
final bool reconnectOnClose;
/// The amount of time to wait between reconnect attempts. Default: 10 seconds.
Duration get reconnectInterval => _reconnectInterval;
BaseWebSocketClient(http.BaseClient client, String basePath,
{this.reconnectOnClose: true, Duration reconnectInterval})
: super(client, basePath) {
_reconnectInterval = reconnectInterval ?? new Duration(seconds: 10);
}
@override
Future close() async => _socket.sink.close(status.goingAway);
Future close() async {
await _socket.sink.close(status.goingAway);
_onData.close();
_onAllEvents.close();
_onAuthenticated.close();
_onError.close();
_onServiceEvent.close();
_onWebSocketChannelException.close();
}
/// Connects the WebSocket.
Future<WebSocketChannel> connect() async {
/// Connects the WebSocket. [timeout] is optional.
Future<WebSocketChannel> connect({Duration timeout}) async {
if (timeout != null) {
var c = new Completer<WebSocketChannel>();
Timer timer;
timer = new Timer(timeout, () {
if (!c.isCompleted) {
if (timer.isActive) timer.cancel();
c.completeError(new TimeoutException(
'WebSocket connection exceeded timeout of ${timeout.inMilliseconds} ms',
timeout));
}
});
getConnectedWebSocket().then((socket) {
if (!c.isCompleted) {
if (timer.isActive) timer.cancel();
c.complete(socket);
}
}).catchError((e, st) {
if (!c.isCompleted) {
if (timer.isActive) timer.cancel();
c.completeError(e, st);
}
});
return await c.future.then((socket) {
_socket = socket;
listen();
});
} else {
_socket = await getConnectedWebSocket();
listen();
return _socket;
}
}
/// Returns a new [WebSocketChannel], ready to be listened on.
///
@ -79,7 +134,8 @@ abstract class BaseWebSocketClient extends BaseAngelClient {
/// Starts listening for data.
void listen() {
_socket.stream.listen((data) {
_socket?.stream?.listen(
(data) {
_onData.add(data);
if (data is WebSocketChannelException) {
@ -98,6 +154,9 @@ abstract class BaseWebSocketClient extends BaseAngelClient {
if (event.eventName == EVENT_ERROR) {
var error = new AngelHttpException.fromMap(event.data ?? {});
_onError.add(error);
} else if (event.eventName == EVENT_AUTHENTICATED) {
var authResult = new AngelAuthResult.fromMap(event.data);
_onAuthenticated.add(authResult);
} else if (event.eventName?.isNotEmpty == true) {
var split = event.eventName
.split("::")
@ -106,11 +165,28 @@ abstract class BaseWebSocketClient extends BaseAngelClient {
if (split.length >= 2) {
var serviceName = split[0], eventName = split[1];
_onServiceEvent.add({serviceName: event..eventName = eventName});
_onServiceEvent
.add({serviceName: event..eventName = eventName});
}
}
}
}
},
cancelOnError: true,
onDone: () {
if (reconnectOnClose == true) {
new Timer.periodic(reconnectInterval, (Timer timer) async {
var result;
try {
result = await connect(timeout: reconnectInterval);
} catch (e) {
//
}
if (result != null) timer.cancel();
});
}
});
}
@ -125,6 +201,11 @@ abstract class BaseWebSocketClient extends BaseAngelClient {
void sendAction(WebSocketAction action) {
socket.sink.add(serialize(action));
}
/// Attempts to authenticate a WebSocket, using a valid JWT.
void authenticateViaJwt(String jwt) {
send(ACTION_AUTHENTICATE, new WebSocketAction(params: {'jwt': jwt}));
}
}
/// A [Service] that asynchronously interacts with the server.

View file

@ -5,6 +5,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:mirrors';
import 'package:angel_auth/angel_auth.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:merge_map/merge_map.dart';
@ -35,6 +36,9 @@ class AngelWebSocket extends AngelPlugin {
/// discarded, other than `params['query']`.
final bool allowClientParams;
/// If `true`, then clients can authenticate their WebSockets by sending a valid JWT.
final bool allowAuth;
/// Include debug information, and send error information across WebSockets.
final bool debug;
@ -51,6 +55,9 @@ class AngelWebSocket extends AngelPlugin {
/// The endpoint that users should connect a WebSocket to.
final String endpoint;
/// Used to notify other nodes of an event's firing. Good for scaled applications.
final WebSocketSynchronizer synchronizer;
/// Fired on any [WebSocketAction].
Stream<WebSocketAction> get onAction => _onAction.stream;
@ -67,7 +74,9 @@ class AngelWebSocket extends AngelPlugin {
{this.endpoint: '/ws',
this.debug: false,
this.allowClientParams: false,
this.register});
this.allowAuth: true,
this.register,
this.synchronizer});
serviceHook(String path) {
return (HookedServiceEvent e) async {
@ -93,7 +102,7 @@ class AngelWebSocket extends AngelPlugin {
/// Slates an event to be dispatched.
Future batchEvent(WebSocketEvent event,
{filter(WebSocketContext socket)}) async {
{filter(WebSocketContext socket), bool notify: true}) async {
// Default implementation will just immediately fire events
_clients.forEach((client) async {
var result = true;
@ -105,6 +114,9 @@ class AngelWebSocket extends AngelPlugin {
client.io.add(god.serialize(event.toJson()));
}
});
if (synchronizer != null && notify != false)
synchronizer.notifyOthers(event);
}
/// Returns a list of events yet to be sent.
@ -112,6 +124,9 @@ class AngelWebSocket extends AngelPlugin {
/// Responds to an incoming action on a WebSocket.
Future handleAction(WebSocketAction action, WebSocketContext socket) async {
if (action.eventName == ACTION_AUTHENTICATE)
return await handleAuth(action, socket);
var split = action.eventName.split("::");
if (split.length < 2)
@ -181,6 +196,37 @@ class AngelWebSocket extends AngelPlugin {
}
}
/// Authenticates a [WebSocketContext].
Future handleAuth(WebSocketAction action, WebSocketContext socket) async {
if (allowAuth != false &&
action.eventName == ACTION_AUTHENTICATE &&
action.params['jwt'] is String) {
try {
var auth = socket.request.grab<AngelAuth>(AngelAuth);
var jwt = action.params['jwt'] as String;
AuthToken token;
token = new AuthToken.validate(jwt, auth.hmac);
var user = await auth.deserializer(token.userId);
var req = socket.request;
req
..inject(AuthToken, req.properties['token'] = token)
..inject(user.runtimeType, req.properties["user"] = user);
socket.send(EVENT_AUTHENTICATED,
{'token': token.serialize(auth.hmac), 'data': user});
} catch (e, st) {
// Send an error
if (e is AngelHttpException)
socket.sendError(e);
else if (debug == true)
socket.sendError(new AngelHttpException(e,
message: e.toString(), stackTrace: st, errors: [st.toString()]));
else
socket.sendError(new AngelHttpException(e));
}
}
}
/// Hooks a service up to have its events broadcasted.
hookupService(Pattern _path, HookedService service) {
String path = _path.toString();
@ -309,5 +355,16 @@ class AngelWebSocket extends AngelPlugin {
}
await _register();
if (synchronizer != null) {
synchronizer.stream.listen((e) => batchEvent(e, notify: false));
}
}
}
/// Notifies other nodes of outgoing WWebSocket events, and listens for
/// notifications from other nodes.
abstract class WebSocketSynchronizer {
Stream<WebSocketEvent> get stream;
void notifyOthers(WebSocketEvent e);
}

View file

@ -17,17 +17,32 @@ class WebSocketContext {
StreamController<WebSocketAction> _onAction =
new StreamController<WebSocketAction>();
StreamController<Null> _onClose = new StreamController<Null>();
StreamController _onData = new StreamController();
/// Fired on any [WebSocketAction];
Stream<WebSocketAction> get onAction => _onAction.stream;
/// Fired once the underlying [WebSocket] closes.
Stream<Null> get onClose => _onClose.stream;
/// Fired when any data is sent through [io].
Stream get onData => _onData.stream;
WebSocketContext(WebSocket this.io, RequestContext this.request,
ResponseContext this.response);
/// Closes the underlying [WebSocket].
Future close([int code, String reason]) async {
await io.close(code, reason);
_onAction.close();
_onData.close();
_onClose.add(null);
_onClose.close();
}
/// Sends an arbitrary [WebSocketEvent];
void send(String eventName, data) {
io.add(god.serialize(new WebSocketEvent(eventName: eventName, data: data)));
@ -42,7 +57,7 @@ class _WebSocketEventTable {
StreamController<Map> _getStreamForEvent(eventName) {
if (!_handlers.containsKey(eventName))
_handlers[eventName] = new StreamController<Map>.broadcast();
_handlers[eventName] = new StreamController<Map>();
return _handlers[eventName];
}

View file

@ -2,12 +2,12 @@ name: angel_websocket
description: WebSocket plugin for Angel.
environment:
sdk: ">=1.19.0"
version: 1.0.1
version: 1.0.2
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_websocket
dependencies:
angel_auth: "^1.0.0-dev"
angel_client: "^1.0.0-dev"
angel_client: "^1.0.0"
angel_framework: "^1.0.0-dev"
uuid: "^0.5.3"
web_socket_channel: "^1.0.0"

View file

@ -29,7 +29,7 @@ main() {
url = 'ws://${server.address.address}:${server.port}/ws';
client = new ws.WebSockets(url);
await client.connect();
await client.connect(timeout: new Duration(seconds: 3));
client
..onData.listen((data) {

View file

@ -10,7 +10,9 @@ class Todo extends Model {
Todo({String this.text, String this.when});
}
class TodoService extends MemoryService<Todo> {}
class TodoService extends TypedService<Todo> {
TodoService() : super(new MapService());
}
testIndex(BaseWebSocketClient client) async {
var Todos = client.service('api/todos');