Almost
This commit is contained in:
parent
7957a4beee
commit
7d1f0a9c65
20 changed files with 610 additions and 714 deletions
4
.anaylsis-options.yaml
Normal file
4
.anaylsis-options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode: true
|
||||||
|
exclude:
|
||||||
|
- .scripts-bin/**/*.dart
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -26,3 +26,5 @@ doc/api/
|
||||||
# (Library packages only! Remove pattern if developing an application package)
|
# (Library packages only! Remove pattern if developing an application package)
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
log.txt
|
1
.travis.yml
Normal file
1
.travis.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
language: dart
|
|
@ -1,17 +1,73 @@
|
||||||
|
/// WebSocket plugin for Angel.
|
||||||
library angel_websocket;
|
library angel_websocket;
|
||||||
|
|
||||||
|
const String ACTION_INDEX = 'index';
|
||||||
|
const String ACTION_READ = 'read';
|
||||||
|
const String ACTION_CREATE = 'create';
|
||||||
|
const String ACTION_MODIFY = 'modify';
|
||||||
|
const String ACTION_UPDATE = 'update';
|
||||||
|
const String ACTION_REMOVE = 'remove';
|
||||||
|
|
||||||
|
const String EVENT_ERROR = 'error';
|
||||||
|
const String EVENT_INDEXED = 'indexed';
|
||||||
|
const String EVENT_READ = 'read';
|
||||||
|
const String EVENT_CREATED = 'created';
|
||||||
|
const String EVENT_MODIFIED = 'modified';
|
||||||
|
const String EVENT_UPDATED = 'updated';
|
||||||
|
const String EVENT_REMOVED = 'removed';
|
||||||
|
|
||||||
|
/// The standard Angel service actions.
|
||||||
|
const List<String> ACTIONS = const [
|
||||||
|
ACTION_INDEX,
|
||||||
|
ACTION_READ,
|
||||||
|
ACTION_CREATE,
|
||||||
|
ACTION_MODIFY,
|
||||||
|
ACTION_UPDATE,
|
||||||
|
ACTION_REMOVE
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The standard Angel service events.
|
||||||
|
const List<String> EVENTS = const [
|
||||||
|
EVENT_INDEXED,
|
||||||
|
EVENT_READ,
|
||||||
|
EVENT_CREATED,
|
||||||
|
EVENT_MODIFIED,
|
||||||
|
EVENT_UPDATED,
|
||||||
|
EVENT_REMOVED
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A notification from the server that something has occurred.
|
||||||
class WebSocketEvent {
|
class WebSocketEvent {
|
||||||
String eventName;
|
String eventName;
|
||||||
var data;
|
var data;
|
||||||
|
|
||||||
WebSocketEvent({String this.eventName, this.data});
|
WebSocketEvent({String this.eventName, this.data});
|
||||||
|
|
||||||
|
factory WebSocketEvent.fromJson(Map data) =>
|
||||||
|
new WebSocketEvent(eventName: data['eventName'], data: data['data']);
|
||||||
|
|
||||||
|
Map toJson() {
|
||||||
|
return {'eventName': eventName, 'data': data};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A command sent to the server, usually corresponding to a service method.
|
||||||
class WebSocketAction {
|
class WebSocketAction {
|
||||||
String id;
|
String id;
|
||||||
String eventName;
|
String eventName;
|
||||||
var data;
|
var data;
|
||||||
var params;
|
var params;
|
||||||
|
|
||||||
WebSocketAction({String this.id, String this.eventName, this.data, this.params});
|
WebSocketAction(
|
||||||
|
{String this.id, String this.eventName, this.data, this.params});
|
||||||
|
|
||||||
|
factory WebSocketAction.fromJson(Map data) => new WebSocketAction(
|
||||||
|
id: data['id'],
|
||||||
|
eventName: data['eventName'],
|
||||||
|
data: data['data'],
|
||||||
|
params: data['params']);
|
||||||
|
|
||||||
|
Map toJson() {
|
||||||
|
return {'id': id, 'eventName': eventName, 'data': data, 'params': params};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,140 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:angel_client/angel_client.dart';
|
import 'package:angel_client/angel_client.dart';
|
||||||
import 'package:http/src/base_client.dart' as http;
|
import 'package:http/src/base_client.dart' as http;
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
import 'package:web_socket_channel/status.dart';
|
import 'package:web_socket_channel/status.dart' as status;
|
||||||
import 'angel_websocket.dart';
|
import 'angel_websocket.dart';
|
||||||
export 'package:angel_client/angel_client.dart';
|
export 'package:angel_client/angel_client.dart';
|
||||||
import 'package:angel_client/base_angel_client.dart';
|
import 'package:angel_client/base_angel_client.dart';
|
||||||
|
|
||||||
final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
|
final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
|
||||||
|
|
||||||
|
/// An [Angel] client that operates across WebSockets.
|
||||||
abstract class BaseWebSocketClient extends BaseAngelClient {
|
abstract class BaseWebSocketClient extends BaseAngelClient {
|
||||||
WebSocketChannel _socket;
|
WebSocketChannel _socket;
|
||||||
|
|
||||||
|
final StreamController _onData = new StreamController();
|
||||||
|
final StreamController<WebSocketEvent> _onMessage =
|
||||||
|
new StreamController<WebSocketEvent>();
|
||||||
|
final StreamController<AngelHttpException> _onError =
|
||||||
|
new StreamController<AngelHttpException>();
|
||||||
|
final StreamController<Map<String, WebSocketEvent>> _onServiceEvent =
|
||||||
|
new StreamController<Map<String, WebSocketEvent>>.broadcast();
|
||||||
|
final StreamController<WebSocketChannelException>
|
||||||
|
_onWebSocketChannelException =
|
||||||
|
new StreamController<WebSocketChannelException>();
|
||||||
|
|
||||||
|
/// A broadcast stream of data coming from the [socket].
|
||||||
|
///
|
||||||
|
/// Mostly just for internal use.
|
||||||
|
Stream get onData => _onData.stream;
|
||||||
|
|
||||||
|
/// Fired on errors.
|
||||||
|
Stream<AngelHttpException> get onError => _onError.stream;
|
||||||
|
|
||||||
|
/// Fired on all events.
|
||||||
|
Stream<WebSocketEvent> get onMessage => _onMessage.stream;
|
||||||
|
|
||||||
|
/// Fired whenever an event is fired by a service.
|
||||||
|
Stream<Map<String, WebSocketEvent>> get onServiceEvent =>
|
||||||
|
_onServiceEvent.stream;
|
||||||
|
|
||||||
|
/// Fired on [WebSocketChannelException]s.
|
||||||
|
Stream<WebSocketChannelException> get onWebSocketChannelException =>
|
||||||
|
_onWebSocketChannelException.stream;
|
||||||
|
|
||||||
/// The [WebSocketChannel] underneath this instance.
|
/// The [WebSocketChannel] underneath this instance.
|
||||||
WebSocketChannel get socket => _socket;
|
WebSocketChannel get socket => _socket;
|
||||||
|
|
||||||
BaseWebSocketClient(http.BaseClient client, String basePath)
|
BaseWebSocketClient(http.BaseClient client, String basePath)
|
||||||
: super(client, basePath);
|
: super(client, basePath) {}
|
||||||
|
|
||||||
Future<WebSocketChannel> connect();
|
@override
|
||||||
|
Future close() async => _socket.sink.close(status.goingAway);
|
||||||
|
|
||||||
|
/// Connects the WebSocket.
|
||||||
|
Future<WebSocketChannel> connect() async {
|
||||||
|
_socket = await getConnectedWebSocket();
|
||||||
|
listen();
|
||||||
|
return _socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [WebSocketChannel], ready to be listened on.
|
||||||
|
///
|
||||||
|
/// This should be overriden by child classes, **NOT** [connect].
|
||||||
|
Future<WebSocketChannel> getConnectedWebSocket();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
BaseWebSocketService service<T>(String path,
|
BaseWebSocketService service<T>(String path,
|
||||||
{Type type, AngelDeserializer deserializer}) {
|
{Type type, AngelDeserializer deserializer}) {
|
||||||
String uri = path.toString().replaceAll(_straySlashes, '');
|
String uri = path.toString().replaceAll(_straySlashes, '');
|
||||||
return new BaseWebSocketService(socket, this, uri,
|
return new BaseWebSocketService(socket, this, uri,
|
||||||
deserializer: deserializer)..listen();
|
deserializer: deserializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts listening for data.
|
||||||
|
void listen() {
|
||||||
|
_socket.stream.listen((data) {
|
||||||
|
_onData.add(data);
|
||||||
|
|
||||||
|
if (data is WebSocketChannelException) {
|
||||||
|
_onWebSocketChannelException.add(data);
|
||||||
|
} else if (data is String) {
|
||||||
|
var json = JSON.decode(data);
|
||||||
|
|
||||||
|
if (json is Map) {
|
||||||
|
var event = new WebSocketEvent.fromJson(json);
|
||||||
|
_onMessage.add(event);
|
||||||
|
|
||||||
|
if (event.eventName == EVENT_ERROR) {
|
||||||
|
var error = new AngelHttpException.fromMap(event.data ?? {});
|
||||||
|
_onError.add(error);
|
||||||
|
} else if (event.eventName?.isNotEmpty == true) {
|
||||||
|
var split = event.eventName
|
||||||
|
.split("::")
|
||||||
|
.where((str) => str.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (split.length >= 2) {
|
||||||
|
var serviceName = split[0], eventName = split[1];
|
||||||
|
_onServiceEvent.add({serviceName: event..eventName = eventName});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes data to JSON.
|
||||||
|
serialize(x) => JSON.encode(x);
|
||||||
|
|
||||||
|
/// Alternative form of [send]ing an action.
|
||||||
|
void send(String eventName, WebSocketAction action) =>
|
||||||
|
sendAction(action..eventName = eventName);
|
||||||
|
|
||||||
|
/// Sends the given [action] on the [socket].
|
||||||
|
void sendAction(WebSocketAction action) {
|
||||||
|
socket.sink.add(serialize(action));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A [Service] that asynchronously interacts with the server.
|
||||||
class BaseWebSocketService extends Service {
|
class BaseWebSocketService extends Service {
|
||||||
|
/// The [BaseWebSocketClient] that spawned this service.
|
||||||
@override
|
@override
|
||||||
final Angel app;
|
final BaseWebSocketClient app;
|
||||||
final AngelDeserializer deserializer;
|
|
||||||
final WebSocketChannel socket;
|
|
||||||
final String uri;
|
|
||||||
|
|
||||||
final StreamController<WebSocketEvent> _onMessage =
|
/// Used to deserialize JSON into typed data.
|
||||||
new StreamController<WebSocketEvent>();
|
final AngelDeserializer deserializer;
|
||||||
final StreamController<WebSocketEvent> _onError =
|
|
||||||
|
/// The [WebSocketChannel] to listen to, and send data across.
|
||||||
|
final WebSocketChannel socket;
|
||||||
|
|
||||||
|
/// The service path to listen to.
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
final StreamController<WebSocketEvent> _onAllEvents =
|
||||||
new StreamController<WebSocketEvent>();
|
new StreamController<WebSocketEvent>();
|
||||||
final StreamController<WebSocketEvent> _onIndexed =
|
final StreamController<WebSocketEvent> _onIndexed =
|
||||||
new StreamController<WebSocketEvent>();
|
new StreamController<WebSocketEvent>();
|
||||||
|
@ -52,17 +148,13 @@ class BaseWebSocketService extends Service {
|
||||||
new StreamController<WebSocketEvent>();
|
new StreamController<WebSocketEvent>();
|
||||||
final StreamController<WebSocketEvent> _onRemoved =
|
final StreamController<WebSocketEvent> _onRemoved =
|
||||||
new StreamController<WebSocketEvent>();
|
new StreamController<WebSocketEvent>();
|
||||||
final WebSocketExtraneousEventHandler _on =
|
|
||||||
new WebSocketExtraneousEventHandler();
|
|
||||||
|
|
||||||
/// Use this to handle events that are not standard.
|
/// Use this to handle events that are not standard.
|
||||||
WebSocketExtraneousEventHandler get on => _on;
|
final WebSocketExtraneousEventHandler on =
|
||||||
|
new WebSocketExtraneousEventHandler();
|
||||||
|
|
||||||
/// Fired on all events.
|
/// Fired on all events.
|
||||||
Stream<WebSocketEvent> get onMessage => _onMessage.stream;
|
Stream<WebSocketEvent> get onAllEvents => _onAllEvents.stream;
|
||||||
|
|
||||||
/// Fired on errors.
|
|
||||||
Stream<WebSocketEvent> get onError => _onError.stream;
|
|
||||||
|
|
||||||
/// Fired on `index` events.
|
/// Fired on `index` events.
|
||||||
Stream<WebSocketEvent> get onIndexed => _onIndexed.stream;
|
Stream<WebSocketEvent> get onIndexed => _onIndexed.stream;
|
||||||
|
@ -82,48 +174,116 @@ class BaseWebSocketService extends Service {
|
||||||
/// Fired on `removed` events.
|
/// Fired on `removed` events.
|
||||||
Stream<WebSocketEvent> get onRemoved => _onRemoved.stream;
|
Stream<WebSocketEvent> get onRemoved => _onRemoved.stream;
|
||||||
|
|
||||||
BaseWebSocketService(this.socket, this.app, this.uri, {this.deserializer});
|
BaseWebSocketService(this.socket, this.app, this.path, {this.deserializer}) {
|
||||||
|
listen();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes an [action] to be sent over a WebSocket.
|
||||||
|
serialize(WebSocketAction action) => JSON.encode(action);
|
||||||
|
|
||||||
|
/// Deserializes data from a [WebSocketEvent].
|
||||||
|
deserialize(x) {
|
||||||
|
return deserializer != null ? deserializer(x) : x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes the contents of an [event].
|
||||||
|
WebSocketEvent transformEvent(WebSocketEvent event) {
|
||||||
|
return event..data = deserialize(event.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts listening for events.
|
||||||
void listen() {
|
void listen() {
|
||||||
socket.stream.listen((message) {
|
app.onServiceEvent.listen((map) {
|
||||||
print('Message: ${message.runtimeType}');
|
if (map.containsKey(path)) {
|
||||||
|
var event = map[path];
|
||||||
|
var transformed = transformEvent(event);
|
||||||
|
|
||||||
|
_onAllEvents.add(event);
|
||||||
|
on._getStream(event.eventName).add(event);
|
||||||
|
|
||||||
|
switch (event.eventName) {
|
||||||
|
case EVENT_INDEXED:
|
||||||
|
_onIndexed.add(transformed);
|
||||||
|
break;
|
||||||
|
case EVENT_READ:
|
||||||
|
_onRead.add(transformed);
|
||||||
|
break;
|
||||||
|
case EVENT_CREATED:
|
||||||
|
_onCreated.add(transformed);
|
||||||
|
break;
|
||||||
|
case EVENT_MODIFIED:
|
||||||
|
_onModified.add(transformed);
|
||||||
|
break;
|
||||||
|
case EVENT_UPDATED:
|
||||||
|
_onUpdated.add(transformed);
|
||||||
|
break;
|
||||||
|
case EVENT_REMOVED:
|
||||||
|
_onRemoved.add(transformed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
/// Sends the given [action] on the [socket].
|
||||||
Future<List> index([Map params]) {
|
void send(WebSocketAction action) {
|
||||||
// TODO: implement index
|
socket.sink.add(serialize(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future read(id, [Map params]) {
|
Future<List> index([Map params]) async {
|
||||||
// TODO: implement read
|
socket.sink.add(serialize(new WebSocketAction(
|
||||||
|
eventName: '$path::${ACTION_INDEX}', params: params ?? {})));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future create(data, [Map params]) {
|
Future read(id, [Map params]) async {
|
||||||
// TODO: implement create
|
socket.sink
|
||||||
|
.add(serialize(new WebSocketAction(id: id, params: params ?? {})));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future modify(id, data, [Map params]) {
|
Future create(data, [Map params]) async {
|
||||||
// TODO: implement modify
|
socket.sink
|
||||||
|
.add(serialize(new WebSocketAction(data: data, params: params ?? {})));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future update(id, data, [Map params]) {
|
Future modify(id, data, [Map params]) async {
|
||||||
// TODO: implement update
|
socket.sink.add(serialize(
|
||||||
|
new WebSocketAction(id: id, data: data, params: params ?? {})));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future remove(id, [Map params]) {
|
Future update(id, data, [Map params]) async {
|
||||||
// TODO: implement remove
|
socket.sink.add(serialize(
|
||||||
|
new WebSocketAction(id: id, data: data, params: params ?? {})));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future remove(id, [Map params]) async {
|
||||||
|
socket.sink
|
||||||
|
.add(serialize(new WebSocketAction(id: id, params: params ?? {})));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Contains a dynamic Map of [WebSocketEvent] streams.
|
||||||
class WebSocketExtraneousEventHandler {
|
class WebSocketExtraneousEventHandler {
|
||||||
Map<String, StreamController<WebSocketEvent>> _events = {};
|
Map<String, StreamController<WebSocketEvent>> _events = {};
|
||||||
|
|
||||||
|
StreamController<WebSocketEvent> _getStream(String index) {
|
||||||
|
if (_events[index] == null)
|
||||||
|
_events[index] = new StreamController<WebSocketEvent>();
|
||||||
|
|
||||||
|
return _events[index];
|
||||||
|
}
|
||||||
|
|
||||||
operator [](String index) {
|
operator [](String index) {
|
||||||
if (_events[index] == null)
|
if (_events[index] == null)
|
||||||
_events[index] = new StreamController<WebSocketEvent>();
|
_events[index] = new StreamController<WebSocketEvent>();
|
||||||
|
|
226
lib/browser.dart
226
lib/browser.dart
|
@ -1,212 +1,50 @@
|
||||||
|
/// Browser WebSocket client library for the Angel framework.
|
||||||
|
library angel_websocket.browser;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:html';
|
import 'dart:html';
|
||||||
import 'package:angel_client/angel_client.dart';
|
import 'package:angel_client/angel_client.dart';
|
||||||
import 'package:angel_websocket/angel_websocket.dart';
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
import 'package:web_socket_channel/html.dart';
|
||||||
|
import 'base_websocket_client.dart';
|
||||||
export 'package:angel_client/angel_client.dart';
|
export 'package:angel_client/angel_client.dart';
|
||||||
export 'package:angel_websocket/angel_websocket.dart';
|
export 'angel_websocket.dart';
|
||||||
|
|
||||||
class WebSocketClient extends Angel {
|
final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
|
||||||
WebSocket _socket;
|
|
||||||
Map<Pattern, List<WebSocketService>> _services = {};
|
|
||||||
WebSocket get _underlyingSocket => _socket;
|
|
||||||
|
|
||||||
WebSocketClient(String wsEndpoint) : super(wsEndpoint) {
|
/// Queries an Angel server via WebSockets.
|
||||||
_socket = new WebSocket(wsEndpoint);
|
class WebSockets extends BaseWebSocketClient {
|
||||||
_connect();
|
WebSockets(String path) : super(new http.Client(), path);
|
||||||
}
|
|
||||||
|
|
||||||
onData(data) {
|
|
||||||
var fromJson = JSON.decode(data);
|
|
||||||
var e = new WebSocketEvent(
|
|
||||||
eventName: fromJson['eventName'], data: fromJson['data']);
|
|
||||||
var split = e.eventName.split("::");
|
|
||||||
var serviceName = split[0];
|
|
||||||
var services = _services[serviceName];
|
|
||||||
|
|
||||||
if (serviceName == "error") {
|
|
||||||
throw new Exception("Server-side error.");
|
|
||||||
} else if (services != null) {
|
|
||||||
e.eventName = split[1];
|
|
||||||
|
|
||||||
for (WebSocketService service in services) {
|
|
||||||
service._onAllEvents.add(e);
|
|
||||||
switch (e.eventName) {
|
|
||||||
case "indexed":
|
|
||||||
service._onIndexed.add(e);
|
|
||||||
break;
|
|
||||||
case "read":
|
|
||||||
service._onRead.add(e);
|
|
||||||
break;
|
|
||||||
case "created":
|
|
||||||
service._onCreated.add(e);
|
|
||||||
break;
|
|
||||||
case "modified":
|
|
||||||
service._onModified.add(e);
|
|
||||||
break;
|
|
||||||
case "updated":
|
|
||||||
service._onUpdated.add(e);
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
service._onRemoved.add(e);
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
service._onError.add(e);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (service._on._events.containsKey(e.eventName))
|
|
||||||
service._on._events[e.eventName].add(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _connect() {
|
|
||||||
_socket.onMessage.listen((MessageEvent event) {
|
|
||||||
onData(event.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void send(String eventName, data) {
|
|
||||||
_socket.send(JSON.encode({"eventName": eventName, "data": data}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Service service(Pattern path, {Type type}) {
|
Future<WebSocketChannel> getConnectedWebSocket() {
|
||||||
var service =
|
var socket = new WebSocket(basePath);
|
||||||
new WebSocketService._base(path.toString(), this, _socket, type);
|
var completer = new Completer<WebSocketChannel>();
|
||||||
if (_services[path.toString()] == null) _services[path.toString()] = [];
|
|
||||||
|
|
||||||
_services[path.toString()].add(service);
|
socket
|
||||||
return service;
|
..onOpen.listen((_) {
|
||||||
}
|
if (!completer.isCompleted)
|
||||||
}
|
return completer.complete(new HtmlWebSocketChannel(socket));
|
||||||
|
})
|
||||||
class WebSocketExtraneousEventHandler {
|
..onError.listen((ErrorEvent e) {
|
||||||
Map<String, StreamController<WebSocketEvent>> _events = {};
|
if (!completer.isCompleted) return completer.completeError(e.error);
|
||||||
|
|
||||||
operator [](String index) {
|
|
||||||
if (_events[index] == null)
|
|
||||||
_events[index] = new StreamController<WebSocketEvent>();
|
|
||||||
|
|
||||||
return _events[index].stream;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WebSocketServiceTransformer
|
|
||||||
implements StreamTransformer<WebSocketEvent, WebSocketEvent> {
|
|
||||||
Type _outputType;
|
|
||||||
|
|
||||||
_WebSocketServiceTransformer.base(this._outputType);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<WebSocketEvent> bind(Stream<WebSocketEvent> stream) {
|
|
||||||
var _stream = new StreamController<WebSocketEvent>();
|
|
||||||
|
|
||||||
stream.listen((WebSocketEvent e) {
|
|
||||||
/* if (_outputType != null && e.eventName != "error")
|
|
||||||
e.data = god.deserialize(god.serialize(e.data), outputType: _outputType);
|
|
||||||
*/
|
|
||||||
_stream.add(e);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return _stream.stream;
|
return completer.future;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebSocketService extends Service {
|
|
||||||
Type _outputType;
|
|
||||||
String _path;
|
|
||||||
_WebSocketServiceTransformer _transformer;
|
|
||||||
WebSocket connection;
|
|
||||||
|
|
||||||
WebSocketExtraneousEventHandler _on = new WebSocketExtraneousEventHandler();
|
|
||||||
var _onAllEvents = new StreamController<WebSocketEvent>();
|
|
||||||
var _onError = new StreamController<WebSocketEvent>();
|
|
||||||
var _onIndexed = new StreamController<WebSocketEvent>();
|
|
||||||
var _onRead = new StreamController<WebSocketEvent>();
|
|
||||||
var _onCreated = new StreamController<WebSocketEvent>();
|
|
||||||
var _onModified = new StreamController<WebSocketEvent>();
|
|
||||||
var _onUpdated = new StreamController<WebSocketEvent>();
|
|
||||||
var _onRemoved = new StreamController<WebSocketEvent>();
|
|
||||||
|
|
||||||
WebSocketExtraneousEventHandler get on => _on;
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onAllEvents =>
|
|
||||||
_onAllEvents.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onError => _onError.stream;
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onIndexed =>
|
|
||||||
_onIndexed.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onRead => _onRead.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onCreated =>
|
|
||||||
_onCreated.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onModified =>
|
|
||||||
_onModified.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onUpdated =>
|
|
||||||
_onUpdated.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onRemoved =>
|
|
||||||
_onRemoved.stream.transform(_transformer);
|
|
||||||
|
|
||||||
WebSocketService._base(
|
|
||||||
String path, Angel app, WebSocket this.connection, Type _outputType) {
|
|
||||||
this._path = path;
|
|
||||||
this.app = app;
|
|
||||||
this._outputType = _outputType;
|
|
||||||
_transformer = new _WebSocketServiceTransformer.base(this._outputType);
|
|
||||||
}
|
|
||||||
|
|
||||||
_serialize(WebSocketAction action) {
|
|
||||||
var data = {"id": action.id, "eventName": action.eventName};
|
|
||||||
|
|
||||||
if (action.data != null) data["data"] = action.data;
|
|
||||||
|
|
||||||
if (action.params != null) data["params"] = action.params;
|
|
||||||
|
|
||||||
return JSON.encode(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List> index([Map params]) async {
|
WebSocketsService service<T>(String path,
|
||||||
connection.send(_serialize(
|
{Type type, AngelDeserializer deserializer}) {
|
||||||
new WebSocketAction(eventName: "$_path::index", params: params)));
|
String uri = path.replaceAll(_straySlashes, '');
|
||||||
return null;
|
return new WebSocketsService(socket, this, uri, T != dynamic ? T : type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
class WebSocketsService extends BaseWebSocketService {
|
||||||
Future read(id, [Map params]) async {
|
final Type type;
|
||||||
connection.send(_serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::read", id: id, params: params)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
WebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type)
|
||||||
Future create(data, [Map params]) async {
|
: super(socket, app, uri);
|
||||||
connection.send(_serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::create", data: data, params: params)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future modify(id, data, [Map params]) async {
|
|
||||||
connection.send(_serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::modify", id: id, data: data, params: params)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future update(id, data, [Map params]) async {
|
|
||||||
connection.send(_serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::update", id: id, data: data, params: params)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future remove(id, [Map params]) async {
|
|
||||||
connection.send(_serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::remove", id: id, params: params)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
224
lib/cli.dart
224
lib/cli.dart
|
@ -1,224 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_client/angel_client.dart';
|
|
||||||
import 'package:angel_framework/angel_framework.dart' as srv;
|
|
||||||
import 'package:angel_websocket/angel_websocket.dart';
|
|
||||||
import 'package:json_god/json_god.dart' as god;
|
|
||||||
export 'package:angel_client/angel_client.dart';
|
|
||||||
export 'package:angel_websocket/angel_websocket.dart';
|
|
||||||
|
|
||||||
class WebSocketClient extends Angel {
|
|
||||||
WebSocket _socket;
|
|
||||||
Map<Pattern, List<WebSocketService>> _services = {};
|
|
||||||
WebSocket get underlyingSocket => _socket;
|
|
||||||
_WebSocketEventTable on = new _WebSocketEventTable();
|
|
||||||
|
|
||||||
WebSocketClient(String wsEndpoint) : super(wsEndpoint);
|
|
||||||
|
|
||||||
onData(data) async {
|
|
||||||
var fromJson = JSON.decode(data);
|
|
||||||
print("a: $fromJson");
|
|
||||||
var e = new WebSocketEvent(
|
|
||||||
eventName: fromJson['eventName'], data: fromJson['data']);
|
|
||||||
print("b: $e");
|
|
||||||
var split = e.eventName.split("::");
|
|
||||||
var serviceName = split[0];
|
|
||||||
var services = _services[serviceName];
|
|
||||||
|
|
||||||
if (serviceName == "error") {
|
|
||||||
var exc = new srv.AngelHttpException(new Exception("Server-side error."));
|
|
||||||
exc.statusCode = e.data['statusCode'];
|
|
||||||
exc.message = e.data['message'];
|
|
||||||
exc.errors = exc.errors ?? [];
|
|
||||||
exc.errors.addAll(e.data['errors'] ?? []);
|
|
||||||
throw exc;
|
|
||||||
} else {
|
|
||||||
on._getStreamForEvent(serviceName).add(e.data);
|
|
||||||
|
|
||||||
if (services != null) {
|
|
||||||
e.eventName = split[1];
|
|
||||||
|
|
||||||
for (WebSocketService service in services) {
|
|
||||||
service._onAllEvents.add(e);
|
|
||||||
switch (e.eventName) {
|
|
||||||
case srv.HookedServiceEvent.INDEXED:
|
|
||||||
service._onIndexed.add(e);
|
|
||||||
break;
|
|
||||||
case srv.HookedServiceEvent.READ:
|
|
||||||
service._onRead.add(e);
|
|
||||||
break;
|
|
||||||
case srv.HookedServiceEvent.CREATED:
|
|
||||||
service._onCreated.add(e);
|
|
||||||
break;
|
|
||||||
case srv.HookedServiceEvent.MODIFIED:
|
|
||||||
service._onModified.add(e);
|
|
||||||
break;
|
|
||||||
case srv.HookedServiceEvent.UPDATED:
|
|
||||||
service._onUpdated.add(e);
|
|
||||||
break;
|
|
||||||
case srv.HookedServiceEvent.REMOVED:
|
|
||||||
service._onRemoved.add(e);
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
service._onError.add(e);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (service._on._events.containsKey(e.eventName))
|
|
||||||
service._on._events[e.eventName].add(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future connect() async {
|
|
||||||
_socket = await WebSocket.connect(basePath);
|
|
||||||
_socket.listen(onData);
|
|
||||||
}
|
|
||||||
|
|
||||||
void send(String eventName, data) {
|
|
||||||
_socket.add(JSON.encode({"eventName": eventName, "data": data}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Service service(Pattern path, {Type type}) {
|
|
||||||
var service =
|
|
||||||
new WebSocketService._base(path.toString(), this, _socket, type);
|
|
||||||
if (_services[path.toString()] == null) _services[path.toString()] = [];
|
|
||||||
|
|
||||||
_services[path.toString()].add(service);
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebSocketExtraneousEventHandler {
|
|
||||||
Map<String, StreamController<WebSocketEvent>> _events = {};
|
|
||||||
|
|
||||||
operator [](String index) {
|
|
||||||
if (_events[index] == null)
|
|
||||||
_events[index] = new StreamController<WebSocketEvent>();
|
|
||||||
|
|
||||||
return _events[index].stream;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WebSocketServiceTransformer
|
|
||||||
implements StreamTransformer<WebSocketEvent, WebSocketEvent> {
|
|
||||||
Type _outputType;
|
|
||||||
|
|
||||||
_WebSocketServiceTransformer.base(this._outputType);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<WebSocketEvent> bind(Stream<WebSocketEvent> stream) {
|
|
||||||
var _stream = new StreamController<WebSocketEvent>();
|
|
||||||
|
|
||||||
stream.listen((WebSocketEvent e) {
|
|
||||||
if (_outputType != null && e.eventName != "error")
|
|
||||||
e.data =
|
|
||||||
god.deserialize(god.serialize(e.data), outputType: _outputType);
|
|
||||||
_stream.add(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
return _stream.stream;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebSocketService extends Service {
|
|
||||||
Type _outputType;
|
|
||||||
String _path;
|
|
||||||
_WebSocketServiceTransformer _transformer;
|
|
||||||
WebSocket connection;
|
|
||||||
|
|
||||||
WebSocketExtraneousEventHandler _on = new WebSocketExtraneousEventHandler();
|
|
||||||
var _onAllEvents = new StreamController<WebSocketEvent>();
|
|
||||||
var _onError = new StreamController<WebSocketEvent>();
|
|
||||||
var _onIndexed = new StreamController<WebSocketEvent>();
|
|
||||||
var _onRead = new StreamController<WebSocketEvent>();
|
|
||||||
var _onCreated = new StreamController<WebSocketEvent>();
|
|
||||||
var _onModified = new StreamController<WebSocketEvent>();
|
|
||||||
var _onUpdated = new StreamController<WebSocketEvent>();
|
|
||||||
var _onRemoved = new StreamController<WebSocketEvent>();
|
|
||||||
|
|
||||||
WebSocketExtraneousEventHandler get on => _on;
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onAllEvents =>
|
|
||||||
_onAllEvents.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onError => _onError.stream;
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onIndexed =>
|
|
||||||
_onIndexed.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onRead => _onRead.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onCreated =>
|
|
||||||
_onCreated.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onModified =>
|
|
||||||
_onModified.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onUpdated =>
|
|
||||||
_onUpdated.stream.transform(_transformer);
|
|
||||||
|
|
||||||
Stream<WebSocketEvent> get onRemoved =>
|
|
||||||
_onRemoved.stream.transform(_transformer);
|
|
||||||
|
|
||||||
WebSocketService._base(
|
|
||||||
String path, Angel app, WebSocket this.connection, Type _outputType) {
|
|
||||||
this._path = path;
|
|
||||||
this.app = app;
|
|
||||||
this._outputType = _outputType;
|
|
||||||
_transformer = new _WebSocketServiceTransformer.base(this._outputType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List> index([Map params]) async {
|
|
||||||
connection.add(god.serialize(
|
|
||||||
new WebSocketAction(eventName: "$_path::index", params: params)));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future read(id, [Map params]) async {
|
|
||||||
connection.add(god.serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::read", id: id, params: params)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future create(data, [Map params]) async {
|
|
||||||
connection.add(god.serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::create", data: data, params: params)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future modify(id, data, [Map params]) async {
|
|
||||||
connection.add(god.serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::modify", id: id, data: data, params: params)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future update(id, data, [Map params]) async {
|
|
||||||
connection.add(god.serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::update", id: id, data: data, params: params)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future remove(id, [Map params]) async {
|
|
||||||
connection.add(god.serialize(new WebSocketAction(
|
|
||||||
eventName: "$_path::remove", id: id, params: params)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WebSocketEventTable {
|
|
||||||
Map<String, StreamController<Map>> _handlers = {};
|
|
||||||
|
|
||||||
StreamController<Map> _getStreamForEvent(eventName) {
|
|
||||||
if (!_handlers.containsKey(eventName))
|
|
||||||
_handlers[eventName] = new StreamController<Map>.broadcast();
|
|
||||||
return _handlers[eventName];
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<Map> operator [](String key) => _getStreamForEvent(key).stream;
|
|
||||||
}
|
|
25
lib/io.dart
25
lib/io.dart
|
@ -1,12 +1,14 @@
|
||||||
/// Command-line WebSocket client library for the Angel framework.
|
/// Command-line WebSocket client library for the Angel framework.
|
||||||
library angel_client.cli;
|
library angel_websocket.io;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:angel_client/angel_client.dart';
|
import 'package:angel_client/angel_client.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:json_god/json_god.dart' as god;
|
import 'package:json_god/json_god.dart' as god;
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
import 'package:web_socket_channel/io.dart';
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
import 'angel_websocket.dart';
|
||||||
import 'base_websocket_client.dart';
|
import 'base_websocket_client.dart';
|
||||||
export 'package:angel_client/angel_client.dart';
|
export 'package:angel_client/angel_client.dart';
|
||||||
export 'angel_websocket.dart';
|
export 'angel_websocket.dart';
|
||||||
|
@ -18,16 +20,20 @@ class WebSockets extends BaseWebSocketClient {
|
||||||
WebSockets(String path) : super(new http.Client(), path);
|
WebSockets(String path) : super(new http.Client(), path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<WebSocketChannel> connect() async {
|
Future<WebSocketChannel> getConnectedWebSocket() async {
|
||||||
return new IOWebSocketChannel.connect(basePath);
|
var socket = await WebSocket.connect(basePath);
|
||||||
|
return new IOWebSocketChannel(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
WebSocketsService service<T>(String path,
|
WebSocketsService service<T>(String path,
|
||||||
{Type type, AngelDeserializer deserializer}) {
|
{Type type, AngelDeserializer deserializer}) {
|
||||||
String uri = path.replaceAll(_straySlashes, "");
|
String uri = path.replaceAll(_straySlashes, '');
|
||||||
return new WebSocketsService(socket, this, uri, T != dynamic ? T : type);
|
return new WebSocketsService(socket, this, uri, T != dynamic ? T : type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
serialize(x) => god.serialize(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketsService extends BaseWebSocketService {
|
class WebSocketsService extends BaseWebSocketService {
|
||||||
|
@ -35,4 +41,15 @@ class WebSocketsService extends BaseWebSocketService {
|
||||||
|
|
||||||
WebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type)
|
WebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type)
|
||||||
: super(socket, app, uri);
|
: super(socket, app, uri);
|
||||||
|
|
||||||
|
@override
|
||||||
|
serialize(WebSocketAction action) => god.serialize(action);
|
||||||
|
|
||||||
|
@override
|
||||||
|
deserialize(x) {
|
||||||
|
if (type != null && type != dynamic) {
|
||||||
|
return god.deserializeDatum(x, outputType: type);
|
||||||
|
} else
|
||||||
|
return super.deserialize(x);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
123
lib/server.dart
123
lib/server.dart
|
@ -1,3 +1,4 @@
|
||||||
|
/// Server-side support for WebSockets.
|
||||||
library angel_websocket.server;
|
library angel_websocket.server;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
@ -13,30 +14,46 @@ export 'angel_websocket.dart';
|
||||||
part 'websocket_context.dart';
|
part 'websocket_context.dart';
|
||||||
part 'websocket_controller.dart';
|
part 'websocket_controller.dart';
|
||||||
|
|
||||||
|
/// Broadcasts events from [HookedService]s, and handles incoming [WebSocketAction]s.
|
||||||
class AngelWebSocket extends AngelPlugin {
|
class AngelWebSocket extends AngelPlugin {
|
||||||
Angel _app;
|
Angel _app;
|
||||||
List<WebSocket> _clients = [];
|
List<WebSocket> _clients = [];
|
||||||
StreamController<WebSocketContext> _onConnection =
|
|
||||||
new StreamController<WebSocketContext>();
|
|
||||||
StreamController<WebSocketContext> _onDisconnect =
|
|
||||||
new StreamController<WebSocketContext>();
|
|
||||||
final List<String> _servicesAlreadyWired = [];
|
final List<String> _servicesAlreadyWired = [];
|
||||||
|
|
||||||
|
final StreamController<WebSocketAction> _onAction =
|
||||||
|
new StreamController<WebSocketAction>();
|
||||||
|
final StreamController _onData = new StreamController();
|
||||||
|
final StreamController<WebSocketContext> _onConnection =
|
||||||
|
new StreamController<WebSocketContext>.broadcast();
|
||||||
|
final StreamController<WebSocketContext> _onDisconnect =
|
||||||
|
new StreamController<WebSocketContext>.broadcast();
|
||||||
|
|
||||||
|
/// Include debug information, and send error information across WebSockets.
|
||||||
|
final bool debug;
|
||||||
|
|
||||||
/// A list of clients currently connected to this server via WebSockets.
|
/// A list of clients currently connected to this server via WebSockets.
|
||||||
List<WebSocket> get clients => new List.unmodifiable(_clients);
|
List<WebSocket> get clients => new List.unmodifiable(_clients);
|
||||||
|
|
||||||
/// Services that have already been hooked to fire socket events.
|
/// Services that have already been hooked to fire socket events.
|
||||||
List<String> get servicesAlreadyWired => new List.unmodifiable(_servicesAlreadyWired);
|
List<String> get servicesAlreadyWired =>
|
||||||
|
new List.unmodifiable(_servicesAlreadyWired);
|
||||||
|
|
||||||
|
/// The endpoint that users should connect a WebSocket to.
|
||||||
final String endpoint;
|
final String endpoint;
|
||||||
|
|
||||||
|
/// Fired on any [WebSocketAction].
|
||||||
|
Stream<WebSocketAction> get onAction => _onAction.stream;
|
||||||
|
|
||||||
|
/// Fired whenever a WebSocket sends data.
|
||||||
|
Stream get onData => _onData.stream;
|
||||||
|
|
||||||
/// Fired on incoming connections.
|
/// Fired on incoming connections.
|
||||||
Stream<WebSocketContext> get onConnection => _onConnection.stream;
|
Stream<WebSocketContext> get onConnection => _onConnection.stream;
|
||||||
|
|
||||||
/// Fired when a user disconnects.
|
/// Fired when a user disconnects.
|
||||||
Stream<WebSocketContext> get onDisconnection => _onDisconnect.stream;
|
Stream<WebSocketContext> get onDisconnection => _onDisconnect.stream;
|
||||||
|
|
||||||
AngelWebSocket(String this.endpoint);
|
AngelWebSocket({this.endpoint: '/ws', this.debug: false});
|
||||||
|
|
||||||
_batchEvent(String path) {
|
_batchEvent(String path) {
|
||||||
return (HookedServiceEvent e) async {
|
return (HookedServiceEvent e) async {
|
||||||
|
@ -46,6 +63,7 @@ class AngelWebSocket extends AngelPlugin {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Slates an event to be dispatched.
|
||||||
Future batchEvent(WebSocketEvent event) async {
|
Future batchEvent(WebSocketEvent event) async {
|
||||||
// Default implementation will just immediately fire events
|
// Default implementation will just immediately fire events
|
||||||
_clients.forEach((client) {
|
_clients.forEach((client) {
|
||||||
|
@ -53,8 +71,10 @@ class AngelWebSocket extends AngelPlugin {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a list of events yet to be sent.
|
||||||
Future<List<WebSocketEvent>> getBatchedEvents() async => [];
|
Future<List<WebSocketEvent>> getBatchedEvents() async => [];
|
||||||
|
|
||||||
|
/// Responds to an incoming action on a WebSocket.
|
||||||
Future handleAction(WebSocketAction action, WebSocketContext socket) async {
|
Future handleAction(WebSocketAction action, WebSocketContext socket) async {
|
||||||
var split = action.eventName.split("::");
|
var split = action.eventName.split("::");
|
||||||
|
|
||||||
|
@ -67,7 +87,7 @@ class AngelWebSocket extends AngelPlugin {
|
||||||
return socket.sendError(new AngelHttpException.NotFound(
|
return socket.sendError(new AngelHttpException.NotFound(
|
||||||
message: "No service \"${split[0]}\" exists."));
|
message: "No service \"${split[0]}\" exists."));
|
||||||
|
|
||||||
var eventName = split[1];
|
var actionName = split[1];
|
||||||
|
|
||||||
var params = mergeMap([
|
var params = mergeMap([
|
||||||
god.deserializeDatum(action.params),
|
god.deserializeDatum(action.params),
|
||||||
|
@ -75,39 +95,44 @@ class AngelWebSocket extends AngelPlugin {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (eventName == "index") {
|
if (actionName == ACTION_INDEX) {
|
||||||
return socket.send("${split[0]}::" + HookedServiceEvent.INDEXED,
|
return socket.send(
|
||||||
await service.index(params));
|
"${split[0]}::" + EVENT_INDEXED, await service.index(params));
|
||||||
} else if (eventName == "read") {
|
} else if (actionName == ACTION_READ) {
|
||||||
return socket.send("${split[0]}::" + HookedServiceEvent.READ,
|
return socket.send("${split[0]}::" + EVENT_READ,
|
||||||
await service.read(action.id, params));
|
await service.read(action.id, params));
|
||||||
} else if (eventName == "create") {
|
} else if (actionName == ACTION_CREATE) {
|
||||||
return new WebSocketEvent(
|
return new WebSocketEvent(
|
||||||
eventName: "${split[0]}::" + HookedServiceEvent.CREATED,
|
eventName: "${split[0]}::" + EVENT_CREATED,
|
||||||
data: await service.create(action.data, params));
|
data: await service.create(action.data, params));
|
||||||
} else if (eventName == "modify") {
|
} else if (actionName == ACTION_MODIFY) {
|
||||||
return new WebSocketEvent(
|
return new WebSocketEvent(
|
||||||
eventName: "${split[0]}::" + HookedServiceEvent.MODIFIED,
|
eventName: "${split[0]}::" + EVENT_MODIFIED,
|
||||||
data: await service.modify(action.id, action.data, params));
|
data: await service.modify(action.id, action.data, params));
|
||||||
} else if (eventName == "update") {
|
} else if (actionName == ACTION_UPDATE) {
|
||||||
return new WebSocketEvent(
|
return new WebSocketEvent(
|
||||||
eventName: "${split[0]}::" + HookedServiceEvent.UPDATED,
|
eventName: "${split[0]}::" + EVENT_UPDATED,
|
||||||
data: await service.update(action.id, action.data, params));
|
data: await service.update(action.id, action.data, params));
|
||||||
} else if (eventName == "remove") {
|
} else if (actionName == ACTION_REMOVE) {
|
||||||
return new WebSocketEvent(
|
return new WebSocketEvent(
|
||||||
eventName: "${split[0]}::" + HookedServiceEvent.REMOVED,
|
eventName: "${split[0]}::" + EVENT_REMOVED,
|
||||||
data: await service.remove(action.id, params));
|
data: await service.remove(action.id, params));
|
||||||
} else {
|
} else {
|
||||||
return socket.sendError(new AngelHttpException.MethodNotAllowed(
|
return socket.sendError(new AngelHttpException.MethodNotAllowed(
|
||||||
message: "Method Not Allowed: \"$eventName\""));
|
message: "Method Not Allowed: \"$actionName\""));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, st) {
|
||||||
if (e is AngelHttpException) return socket.sendError(e);
|
if (e is AngelHttpException)
|
||||||
|
return socket.sendError(e);
|
||||||
return socket.sendError(new AngelHttpException(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) {
|
hookupService(Pattern _path, HookedService service) {
|
||||||
String path = _path.toString();
|
String path = _path.toString();
|
||||||
var batch = _batchEvent(path);
|
var batch = _batchEvent(path);
|
||||||
|
@ -121,17 +146,16 @@ class AngelWebSocket extends AngelPlugin {
|
||||||
_servicesAlreadyWired.add(path);
|
_servicesAlreadyWired.add(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future onConnect(WebSocketContext socket) async {}
|
/// Runs before firing [onConnection].
|
||||||
|
Future handleConnect(WebSocketContext socket) async {}
|
||||||
|
|
||||||
onData(WebSocketContext socket, data) async {
|
/// Handles incoming data from a WebSocket.
|
||||||
|
handleData(WebSocketContext socket, data) async {
|
||||||
try {
|
try {
|
||||||
socket._onData.add(data);
|
socket._onData.add(data);
|
||||||
var fromJson = JSON.decode(data);
|
var fromJson = JSON.decode(data);
|
||||||
var action = new WebSocketAction(
|
var action = new WebSocketAction.fromJson(fromJson);
|
||||||
id: fromJson['id'],
|
_onAction.add(action);
|
||||||
eventName: fromJson['eventName'],
|
|
||||||
data: fromJson['data'],
|
|
||||||
params: fromJson['params']);
|
|
||||||
|
|
||||||
if (action.eventName == null ||
|
if (action.eventName == null ||
|
||||||
action.eventName is! String ||
|
action.eventName is! String ||
|
||||||
|
@ -140,22 +164,17 @@ class AngelWebSocket extends AngelPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fromJson is Map && fromJson.containsKey("eventName")) {
|
if (fromJson is Map && fromJson.containsKey("eventName")) {
|
||||||
socket._onAll.add(fromJson);
|
socket._onAction.add(new WebSocketAction.fromJson(fromJson));
|
||||||
socket.on._getStreamForEvent(fromJson["eventName"].toString()).add(fromJson["data"]);
|
socket.on
|
||||||
|
._getStreamForEvent(fromJson["eventName"].toString())
|
||||||
|
.add(fromJson["data"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.eventName.contains("::")) {
|
if (action.eventName.contains("::")) {
|
||||||
var split = action.eventName.split("::");
|
var split = action.eventName.split("::");
|
||||||
|
|
||||||
if (split.length >= 2) {
|
if (split.length >= 2) {
|
||||||
if ([
|
if (ACTIONS.contains(split[1])) {
|
||||||
"index",
|
|
||||||
"read",
|
|
||||||
"create",
|
|
||||||
"modify",
|
|
||||||
"update",
|
|
||||||
"remove"
|
|
||||||
].contains(split[1])) {
|
|
||||||
var event = handleAction(action, socket);
|
var event = handleAction(action, socket);
|
||||||
if (event is Future) event = await event;
|
if (event is Future) event = await event;
|
||||||
|
|
||||||
|
@ -165,19 +184,24 @@ class AngelWebSocket extends AngelPlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, st) {
|
||||||
// Send an error
|
// Send an error
|
||||||
if (e is AngelHttpException)
|
if (e is AngelHttpException)
|
||||||
socket.sendError(e);
|
socket.sendError(e);
|
||||||
|
else if (debug == true)
|
||||||
|
socket.sendError(new AngelHttpException(e,
|
||||||
|
message: e.toString(), stackTrace: st, errors: [st.toString()]));
|
||||||
else
|
else
|
||||||
socket.sendError(new AngelHttpException(e));
|
socket.sendError(new AngelHttpException(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transforms a [HookedServiceEvent], so that it can be broadcasted.
|
||||||
Future<WebSocketEvent> transformEvent(HookedServiceEvent event) async {
|
Future<WebSocketEvent> transformEvent(HookedServiceEvent event) async {
|
||||||
return new WebSocketEvent(eventName: event.eventName, data: event.result);
|
return new WebSocketEvent(eventName: event.eventName, data: event.result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hooks any [HookedService]s that are not being broadcasted yet.
|
||||||
wireAllServices(Angel app) {
|
wireAllServices(Angel app) {
|
||||||
for (Pattern key in app.services.keys.where((x) {
|
for (Pattern key in app.services.keys.where((x) {
|
||||||
return !_servicesAlreadyWired.contains(x) &&
|
return !_servicesAlreadyWired.contains(x) &&
|
||||||
|
@ -189,7 +213,7 @@ class AngelWebSocket extends AngelPlugin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future call(Angel app) async {
|
Future call(Angel app) async {
|
||||||
this._app = app..container.singleton(this);
|
_app = app..container.singleton(this);
|
||||||
|
|
||||||
if (runtimeType != AngelWebSocket)
|
if (runtimeType != AngelWebSocket)
|
||||||
app.container.singleton(this, as: AngelWebSocket);
|
app.container.singleton(this, as: AngelWebSocket);
|
||||||
|
@ -202,24 +226,25 @@ class AngelWebSocket extends AngelPlugin {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get(endpoint, (RequestContext req, ResponseContext res) async {
|
app.get(endpoint, (RequestContext req, ResponseContext res) async {
|
||||||
if (!WebSocketTransformer.isUpgradeRequest(req.underlyingRequest))
|
if (!WebSocketTransformer.isUpgradeRequest(req.io))
|
||||||
throw new AngelHttpException.BadRequest();
|
throw new AngelHttpException.BadRequest();
|
||||||
|
|
||||||
res
|
res
|
||||||
..willCloseItself = true
|
..willCloseItself = true
|
||||||
..end();
|
..end();
|
||||||
|
|
||||||
var ws = await WebSocketTransformer.upgrade(req.underlyingRequest);
|
var ws = await WebSocketTransformer.upgrade(req.io);
|
||||||
_clients.add(ws);
|
_clients.add(ws);
|
||||||
|
|
||||||
var socket = new WebSocketContext(ws, req, res);
|
var socket = new WebSocketContext(ws, req, res);
|
||||||
await onConnect(socket);
|
await handleConnect(socket);
|
||||||
|
|
||||||
_onConnection.add(socket);
|
_onConnection.add(socket);
|
||||||
req.params['socket'] = socket;
|
req.properties['socket'] = socket;
|
||||||
|
|
||||||
ws.listen((data) {
|
ws.listen((data) {
|
||||||
onData(socket, data);
|
_onData.add(data);
|
||||||
|
handleData(socket, data);
|
||||||
}, onDone: () {
|
}, onDone: () {
|
||||||
_onDisconnect.add(socket);
|
_onDisconnect.add(socket);
|
||||||
_clients.remove(ws);
|
_clients.remove(ws);
|
||||||
|
|
|
@ -1,24 +1,40 @@
|
||||||
part of angel_websocket.server;
|
part of angel_websocket.server;
|
||||||
|
|
||||||
|
/// Represents a WebSocket session, with the original
|
||||||
|
/// [RequestContext] and [ResponseContext] attached.
|
||||||
class WebSocketContext {
|
class WebSocketContext {
|
||||||
StreamController<Map> _onAll = new StreamController<Map>.broadcast();
|
/// Use this to listen for events.
|
||||||
StreamController _onData = new StreamController.broadcast();
|
|
||||||
_WebSocketEventTable on = new _WebSocketEventTable();
|
_WebSocketEventTable on = new _WebSocketEventTable();
|
||||||
Stream<Map> get onAll => _onAll.stream;
|
|
||||||
|
/// The underlying [WebSocket] instance.
|
||||||
|
final WebSocket io;
|
||||||
|
|
||||||
|
/// The original [RequestContext].
|
||||||
|
final RequestContext request;
|
||||||
|
|
||||||
|
/// The original [ResponseContext].
|
||||||
|
final ResponseContext response;
|
||||||
|
|
||||||
|
StreamController<WebSocketAction> _onAction =
|
||||||
|
new StreamController<WebSocketAction>();
|
||||||
|
StreamController _onData = new StreamController();
|
||||||
|
|
||||||
|
/// Fired on any [WebSocketAction];
|
||||||
|
Stream<WebSocketAction> get onAction => _onAction.stream;
|
||||||
|
|
||||||
|
/// Fired when any data is sent through [io].
|
||||||
Stream get onData => _onData.stream;
|
Stream get onData => _onData.stream;
|
||||||
WebSocket underlyingSocket;
|
|
||||||
RequestContext requestContext;
|
|
||||||
ResponseContext responseContext;
|
|
||||||
|
|
||||||
WebSocketContext(WebSocket this.underlyingSocket,
|
WebSocketContext(WebSocket this.io, RequestContext this.request,
|
||||||
RequestContext this.requestContext, ResponseContext this.responseContext);
|
ResponseContext this.response);
|
||||||
|
|
||||||
send(String eventName, data) {
|
/// Sends an arbitrary [WebSocketEvent];
|
||||||
underlyingSocket.add(
|
void send(String eventName, data) {
|
||||||
god.serialize(new WebSocketEvent(eventName: eventName, data: data)));
|
io.add(god.serialize(new WebSocketEvent(eventName: eventName, data: data)));
|
||||||
}
|
}
|
||||||
|
|
||||||
sendError(AngelHttpException error) => send("error", error.toJson());
|
/// Sends an error event.
|
||||||
|
void sendError(AngelHttpException error) => send(EVENT_ERROR, error.toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
class _WebSocketEventTable {
|
class _WebSocketEventTable {
|
||||||
|
|
|
@ -9,17 +9,27 @@ class ExposeWs {
|
||||||
class WebSocketController extends Controller {
|
class WebSocketController extends Controller {
|
||||||
Map<String, MethodMirror> _handlers = {};
|
Map<String, MethodMirror> _handlers = {};
|
||||||
Map<String, Symbol> _handlerSymbols = {};
|
Map<String, Symbol> _handlerSymbols = {};
|
||||||
InstanceMirror _instanceMirror;
|
|
||||||
AngelWebSocket ws;
|
AngelWebSocket ws;
|
||||||
|
|
||||||
WebSocketController():super() {
|
WebSocketController() : super();
|
||||||
_instanceMirror = reflect(this);
|
|
||||||
|
void broadcast(String eventName, data) {
|
||||||
|
ws.batchEvent(new WebSocketEvent(eventName: eventName, data: data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onConnect(WebSocketContext socket) {}
|
||||||
|
|
||||||
|
onDisconnect(WebSocketContext socket) {}
|
||||||
|
|
||||||
|
onAction(WebSocketAction action, WebSocketContext socket) async {}
|
||||||
|
|
||||||
|
onData(data, WebSocketContext socket) {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future call(Angel app) async {
|
Future call(Angel app) async {
|
||||||
await super.call(app);
|
await super.call(app);
|
||||||
|
|
||||||
|
InstanceMirror instanceMirror = reflect(this);
|
||||||
ClassMirror classMirror = reflectClass(this.runtimeType);
|
ClassMirror classMirror = reflectClass(this.runtimeType);
|
||||||
classMirror.instanceMembers.forEach((sym, mirror) {
|
classMirror.instanceMembers.forEach((sym, mirror) {
|
||||||
if (mirror.isRegularMethod) {
|
if (mirror.isRegularMethod) {
|
||||||
|
@ -38,51 +48,32 @@ class WebSocketController extends Controller {
|
||||||
AngelWebSocket ws = app.container.make(AngelWebSocket);
|
AngelWebSocket ws = app.container.make(AngelWebSocket);
|
||||||
|
|
||||||
ws.onConnection.listen((socket) async {
|
ws.onConnection.listen((socket) async {
|
||||||
|
socket.request
|
||||||
|
..inject('socket', socket)
|
||||||
|
..inject(WebSocketContext, socket);
|
||||||
|
|
||||||
await onConnect(socket);
|
await onConnect(socket);
|
||||||
|
|
||||||
socket.onData.listen(onData);
|
socket.onData.listen((data) => onData(data, socket));
|
||||||
|
|
||||||
socket.onAll.listen((Map data) async {
|
socket.onAction.listen((WebSocketAction action) async {
|
||||||
await onAllEvents(data);
|
await onAction(action, socket);
|
||||||
|
|
||||||
if (_handlers.containsKey(data["eventName"])) {
|
if (_handlers.containsKey(action.eventName)) {
|
||||||
var methodMirror = _handlers[data["eventName"]];
|
|
||||||
try {
|
try {
|
||||||
// Load parameters, and execute
|
var methodMirror = _handlers[action.eventName];
|
||||||
List args = [];
|
var fn = instanceMirror.getField(methodMirror.simpleName).reflectee;
|
||||||
|
|
||||||
for (int i = 0; i < methodMirror.parameters.length; i++) {
|
return app.runContained(fn, socket.request, socket.response);
|
||||||
ParameterMirror parameter = methodMirror.parameters[i];
|
} catch (e, st) {
|
||||||
String name = MirrorSystem.getName(parameter.simpleName);
|
|
||||||
|
|
||||||
if (parameter.type.reflectedType == RequestContext ||
|
|
||||||
name == "req")
|
|
||||||
args.add(socket.requestContext);
|
|
||||||
else if (parameter.type.reflectedType == ResponseContext ||
|
|
||||||
name == "res")
|
|
||||||
args.add(socket.responseContext);
|
|
||||||
else if (parameter.type == AngelWebSocket)
|
|
||||||
args.add(socket);
|
|
||||||
else {
|
|
||||||
if (socket.requestContext.params.containsKey(name)) {
|
|
||||||
args.add(socket.requestContext.params[name]);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
args.add(app.container.make(parameter.type.reflectedType));
|
|
||||||
continue;
|
|
||||||
} catch (e) {
|
|
||||||
throw new AngelHttpException.BadRequest(
|
|
||||||
message: "Missing parameter '$name'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _instanceMirror.invoke(_handlerSymbols[data["eventName"]], args);
|
|
||||||
} catch (e) {
|
|
||||||
// Send an error
|
// Send an error
|
||||||
if (e is AngelHttpException)
|
if (e is AngelHttpException)
|
||||||
socket.sendError(e);
|
socket.sendError(e);
|
||||||
|
else if (ws.debug == true)
|
||||||
|
socket.sendError(new AngelHttpException(e,
|
||||||
|
message: e.toString(),
|
||||||
|
stackTrace: st,
|
||||||
|
errors: [st.toString()]));
|
||||||
else
|
else
|
||||||
socket.sendError(new AngelHttpException(e));
|
socket.sendError(new AngelHttpException(e));
|
||||||
}
|
}
|
||||||
|
@ -92,16 +83,4 @@ class WebSocketController extends Controller {
|
||||||
|
|
||||||
ws.onDisconnection.listen(onDisconnect);
|
ws.onDisconnection.listen(onDisconnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
void broadcast(String eventName, data) {
|
|
||||||
ws.batchEvent(new WebSocketEvent(eventName: eventName, data: data));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future onConnect(WebSocketContext socket) async {}
|
|
||||||
|
|
||||||
Future onDisconnect(WebSocketContext socket) async {}
|
|
||||||
|
|
||||||
Future onAllEvents(Map data) async {}
|
|
||||||
|
|
||||||
void onData(data) {}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,5 +12,6 @@ dependencies:
|
||||||
uuid: "^0.5.3"
|
uuid: "^0.5.3"
|
||||||
web_socket_channel: "^1.0.0"
|
web_socket_channel: "^1.0.0"
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
angel_diagnostics: "^1.0.0-dev"
|
||||||
http: "^0.11.3"
|
http: "^0.11.3"
|
||||||
test: "^0.12.15"
|
test: "^0.12.15"
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'server.dart' as server;
|
|
||||||
|
|
||||||
main() async {
|
|
||||||
group("server", server.main);
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_framework/src/defs.dart';
|
|
||||||
|
|
||||||
class Todo extends MemoryModel {
|
|
||||||
String text;
|
|
||||||
String when;
|
|
||||||
|
|
||||||
Todo({String this.text, String this.when});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future startTestServer(Angel app) async {
|
|
||||||
var host = InternetAddress.LOOPBACK_IP_V4;
|
|
||||||
var port = 3000;
|
|
||||||
|
|
||||||
await app.startServer(host, port);
|
|
||||||
app.properties["ws_url"] = "ws://${host.address}:$port/ws";
|
|
||||||
print("Test server listening on ${host.address}:$port");
|
|
||||||
}
|
|
17
test/controller/common.dart
Normal file
17
test/controller/common.dart
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_websocket/server.dart';
|
||||||
|
|
||||||
|
class Game {
|
||||||
|
final String playerOne, playerTwo;
|
||||||
|
|
||||||
|
const Game({this.playerOne, this.playerTwo});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Expose('/game')
|
||||||
|
class GameController extends WebSocketController {
|
||||||
|
@ExposeWs('search')
|
||||||
|
search(WebSocketContext socket) async {
|
||||||
|
print('OMG ok');
|
||||||
|
socket.send('searched', 'poop');
|
||||||
|
}
|
||||||
|
}
|
62
test/controller/io_test.dart
Normal file
62
test/controller/io_test.dart
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:angel_diagnostics/angel_diagnostics.dart' as srv;
|
||||||
|
import 'package:angel_framework/angel_framework.dart' as srv;
|
||||||
|
import 'package:angel_websocket/io.dart' as ws;
|
||||||
|
import 'package:angel_websocket/server.dart' as srv;
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'common.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
srv.Angel app;
|
||||||
|
ws.WebSockets client;
|
||||||
|
srv.AngelWebSocket websockets;
|
||||||
|
HttpServer server;
|
||||||
|
String url;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
app = new srv.Angel();
|
||||||
|
|
||||||
|
websockets = new srv.AngelWebSocket(debug: true)
|
||||||
|
..onData.listen((data) {
|
||||||
|
print('Received by server: $data');
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.configure(websockets);
|
||||||
|
await app.configure(new GameController());
|
||||||
|
|
||||||
|
server =
|
||||||
|
await new srv.DiagnosticsServer(app, new File('log.txt')).startServer();
|
||||||
|
url = 'ws://${server.address.address}:${server.port}/ws';
|
||||||
|
|
||||||
|
client = new ws.WebSockets(url);
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
client
|
||||||
|
..onData.listen((data) {
|
||||||
|
print('Received by client: $data');
|
||||||
|
})
|
||||||
|
..onError.listen((error) {
|
||||||
|
// Auto-fail tests on errors ;)
|
||||||
|
stderr.writeln(error);
|
||||||
|
error.errors.forEach(stderr.writeln);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await client.close();
|
||||||
|
await server.close(force: true);
|
||||||
|
app = null;
|
||||||
|
client = null;
|
||||||
|
server = null;
|
||||||
|
url = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
group('controller.io', () {
|
||||||
|
test('search', () async {
|
||||||
|
client.send('search', new ws.WebSocketAction());
|
||||||
|
var search = await client.onData.first;
|
||||||
|
print('First: $search');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
117
test/server.dart
117
test/server.dart
|
@ -1,117 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart' as server;
|
|
||||||
import 'package:angel_websocket/cli.dart' as client;
|
|
||||||
import 'package:angel_websocket/server.dart';
|
|
||||||
import 'package:json_god/json_god.dart' as god;
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'common.dart';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
server.Angel app;
|
|
||||||
client.WebSocketClient clientApp;
|
|
||||||
client.WebSocketService clientTodos;
|
|
||||||
Stream<Map> customEventStream;
|
|
||||||
Stream<Map> customEventStream2;
|
|
||||||
WebSocket socket;
|
|
||||||
AngelWebSocket webSocket = new AngelWebSocket("/ws");
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
app = new server.Angel();
|
|
||||||
|
|
||||||
app.use("/real", new FakeService(), hooked: false);
|
|
||||||
app.use("/api/todos", new server.MemoryService<Todo>());
|
|
||||||
await app
|
|
||||||
.service("api/todos")
|
|
||||||
.create(new Todo(text: "Clean your room", when: "now"));
|
|
||||||
|
|
||||||
await app.configure(webSocket);
|
|
||||||
await app.configure((server.Angel app) async {
|
|
||||||
AngelWebSocket ws = app.container.make(AngelWebSocket);
|
|
||||||
|
|
||||||
ws.onConnection.listen((WebSocketContext socket) {
|
|
||||||
socket.onData.listen((data) {
|
|
||||||
print("Data: $data");
|
|
||||||
});
|
|
||||||
|
|
||||||
customEventStream = socket.on["custom"];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await app.configure(new Custom2Controller());
|
|
||||||
await app.configure(startTestServer);
|
|
||||||
|
|
||||||
socket = await WebSocket.connect(app.properties["ws_url"]);
|
|
||||||
clientApp = new client.WebSocketClient(app.properties["ws_url"]);
|
|
||||||
await clientApp.connect();
|
|
||||||
customEventStream2 = clientApp.on["custom2"];
|
|
||||||
|
|
||||||
clientTodos = clientApp.service("api/todos", type: Todo);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async {
|
|
||||||
await app.httpServer.close(force: true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("find all real-time services", () {
|
|
||||||
print(webSocket.servicesAlreadyWired);
|
|
||||||
expect(webSocket.servicesAlreadyWired, equals(["api/todos"]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("index", () async {
|
|
||||||
var action = new WebSocketAction(eventName: "api/todos::index");
|
|
||||||
socket.add(god.serialize(action));
|
|
||||||
|
|
||||||
String json = await socket.first;
|
|
||||||
print(json);
|
|
||||||
|
|
||||||
WebSocketEvent e = god.deserialize(json, outputType: WebSocketEvent);
|
|
||||||
expect(e.eventName, equals("api/todos::indexed"));
|
|
||||||
expect(e.data[0]["when"], equals("now"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("create", () async {
|
|
||||||
var todo = new Todo(text: "Finish the Angel framework", when: "2016");
|
|
||||||
clientTodos.create(todo);
|
|
||||||
|
|
||||||
var all = await clientTodos.onAllEvents.first;
|
|
||||||
var e = await clientTodos.onCreated.first;
|
|
||||||
print(god.serialize(e));
|
|
||||||
|
|
||||||
expect(all, equals(e));
|
|
||||||
expect(e.eventName, equals("created"));
|
|
||||||
expect(e.data is Todo, equals(true));
|
|
||||||
expect(e.data.text, equals(todo.text));
|
|
||||||
expect(e.data.when, equals(todo.when));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("custom event via controller", () async {
|
|
||||||
clientApp.send("custom", {"hello": "world"});
|
|
||||||
|
|
||||||
var data = await customEventStream.first;
|
|
||||||
|
|
||||||
expect(data["hello"], equals("world"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("custom event via ws controller", () async {
|
|
||||||
clientApp.send("custom2", {"hello": "world"});
|
|
||||||
|
|
||||||
var data = customEventStream2.first;
|
|
||||||
print("Received data from server: $data");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeService extends server.Service {}
|
|
||||||
|
|
||||||
@server.Expose("/custom2")
|
|
||||||
class Custom2Controller extends WebSocketController {
|
|
||||||
@override
|
|
||||||
Future onConnect(WebSocketContext socket) async {
|
|
||||||
print(
|
|
||||||
"Got a WS connection from session #${socket.requestContext.session.id}!");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExposeWs("custom2")
|
|
||||||
void sayFoo(WebSocketContext socket, server.RequestContext req, AngelWebSocket ws) {
|
|
||||||
socket.send("custom2", {"franken": "stein"});
|
|
||||||
}
|
|
||||||
}
|
|
5
test/service/browser_test.dart
Normal file
5
test/service/browser_test.dart
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
group('service.browser', () {});
|
||||||
|
}
|
24
test/service/common.dart
Normal file
24
test/service/common.dart
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/src/defs.dart';
|
||||||
|
import 'package:angel_websocket/base_websocket_client.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class Todo extends MemoryModel {
|
||||||
|
String text;
|
||||||
|
String when;
|
||||||
|
|
||||||
|
Todo({String this.text, String this.when});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoService extends MemoryService<Todo> {}
|
||||||
|
|
||||||
|
testIndex(BaseWebSocketClient client) async {
|
||||||
|
var Todos = client.service('api/todos');
|
||||||
|
Todos.index();
|
||||||
|
|
||||||
|
var indexed = await Todos.onIndexed.first;
|
||||||
|
print('indexed: ${indexed.toJson()}');
|
||||||
|
|
||||||
|
expect(indexed.data, isList);
|
||||||
|
expect(indexed.data, isEmpty);
|
||||||
|
}
|
56
test/service/io_test.dart
Normal file
56
test/service/io_test.dart
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:angel_diagnostics/angel_diagnostics.dart' as srv;
|
||||||
|
import 'package:angel_framework/angel_framework.dart' as srv;
|
||||||
|
import 'package:angel_websocket/io.dart' as ws;
|
||||||
|
import 'package:angel_websocket/server.dart' as srv;
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'common.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
srv.Angel app;
|
||||||
|
ws.WebSockets client;
|
||||||
|
srv.AngelWebSocket websockets;
|
||||||
|
HttpServer server;
|
||||||
|
String url;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
app = new srv.Angel()..use('/api/todos', new TodoService());
|
||||||
|
|
||||||
|
websockets = new srv.AngelWebSocket(debug: true)
|
||||||
|
..onData.listen((data) {
|
||||||
|
print('Received by server: $data');
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.configure(websockets);
|
||||||
|
server =
|
||||||
|
await new srv.DiagnosticsServer(app, new File('log.txt')).startServer();
|
||||||
|
url = 'ws://${server.address.address}:${server.port}/ws';
|
||||||
|
|
||||||
|
client = new ws.WebSockets(url);
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
client
|
||||||
|
..onData.listen((data) {
|
||||||
|
print('Received by client: $data');
|
||||||
|
})
|
||||||
|
..onError.listen((error) {
|
||||||
|
// Auto-fail tests on errors ;)
|
||||||
|
stderr.writeln(error);
|
||||||
|
error.errors.forEach(stderr.writeln);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await client.close();
|
||||||
|
await server.close(force: true);
|
||||||
|
app = null;
|
||||||
|
client = null;
|
||||||
|
server = null;
|
||||||
|
url = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
group('service.io', () {
|
||||||
|
test('index', () => testIndex(client));
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue