platform/lib/base_websocket_client.dart

415 lines
12 KiB
Dart
Raw Normal View History

2016-12-23 10:47:21 +00:00
import 'dart:async';
2017-04-17 11:03:42 +00:00
import 'dart:collection';
2018-10-02 15:32:06 +00:00
import 'dart:convert';
2016-12-23 10:47:21 +00:00
import 'package:angel_client/angel_client.dart';
2017-09-24 04:37:58 +00:00
import 'package:angel_client/base_angel_client.dart';
import 'package:angel_http_exception/angel_http_exception.dart';
2016-12-23 10:47:21 +00:00
import 'package:http/src/base_client.dart' as http;
import 'package:web_socket_channel/web_socket_channel.dart';
2016-12-23 20:57:46 +00:00
import 'package:web_socket_channel/status.dart' as status;
2016-12-23 10:47:21 +00:00
import 'angel_websocket.dart';
final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)");
2016-12-23 20:57:46 +00:00
/// An [Angel] client that operates across WebSockets.
2016-12-23 10:47:21 +00:00
abstract class BaseWebSocketClient extends BaseAngelClient {
2017-02-28 14:15:34 +00:00
Duration _reconnectInterval;
2016-12-23 10:47:21 +00:00
WebSocketChannel _socket;
2017-04-17 11:03:42 +00:00
final Queue<WebSocketAction> _queue = new Queue<WebSocketAction>();
2016-12-23 10:47:21 +00:00
2016-12-23 20:57:46 +00:00
final StreamController _onData = new StreamController();
2016-12-24 01:45:52 +00:00
final StreamController<WebSocketEvent> _onAllEvents =
2016-12-23 20:57:46 +00:00
new StreamController<WebSocketEvent>();
2017-02-28 14:15:34 +00:00
final StreamController<AngelAuthResult> _onAuthenticated =
new StreamController<AngelAuthResult>();
2016-12-23 20:57:46 +00:00
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>();
2016-12-24 01:45:52 +00:00
/// Use this to handle events that are not standard.
final WebSocketExtraneousEventHandler on =
new WebSocketExtraneousEventHandler();
/// Fired on all events.
Stream<WebSocketEvent> get onAllEvents => _onAllEvents.stream;
2017-02-28 14:15:34 +00:00
/// Fired whenever a WebSocket is successfully authenticated.
Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream;
2016-12-23 20:57:46 +00:00
/// 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 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;
2016-12-23 10:47:21 +00:00
/// The [WebSocketChannel] underneath this instance.
WebSocketChannel get socket => _socket;
2017-02-28 14:15:34 +00:00
/// 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);
}
2016-12-23 20:57:46 +00:00
@override
2017-02-28 14:15:34 +00:00
Future close() async {
await _socket.sink.close(status.goingAway);
_onData.close();
_onAllEvents.close();
_onAuthenticated.close();
_onError.close();
_onServiceEvent.close();
_onWebSocketChannelException.close();
}
2016-12-23 10:47:21 +00:00
2017-02-28 14:15:34 +00:00
/// 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();
2017-04-17 11:03:42 +00:00
while (_queue.isNotEmpty) {
var action = _queue.removeFirst();
socket.sink.add(serialize(action));
}
2017-02-28 14:15:34 +00:00
c.complete(socket);
}
2018-07-10 16:54:55 +00:00
}).catchError((e, StackTrace st) {
2017-02-28 14:15:34 +00:00
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;
}
2016-12-23 20:57:46 +00:00
}
/// Returns a new [WebSocketChannel], ready to be listened on.
///
/// This should be overriden by child classes, **NOT** [connect].
Future<WebSocketChannel> getConnectedWebSocket();
2016-12-23 10:47:21 +00:00
@override
2018-10-21 08:15:51 +00:00
WebSocketsService<Id, Data> service<Id, Data>(String path,
{Type type, AngelDeserializer<Data> deserializer}) {
2016-12-23 10:47:21 +00:00
String uri = path.toString().replaceAll(_straySlashes, '');
2018-10-21 08:15:51 +00:00
return new WebSocketsService<Id, Data>(socket, this, uri,
deserializer: deserializer);
2016-12-23 20:57:46 +00:00
}
/// Starts listening for data.
void listen() {
2017-02-28 14:15:34 +00:00
_socket?.stream?.listen(
(data) {
_onData.add(data);
if (data is WebSocketChannelException) {
_onWebSocketChannelException.add(data);
} else if (data is String) {
2018-07-10 16:54:55 +00:00
var jsons = json.decode(data);
2017-02-28 14:15:34 +00:00
2018-07-10 16:54:55 +00:00
if (jsons is Map) {
var event = new WebSocketEvent.fromJson(jsons);
2017-02-28 14:15:34 +00:00
if (event.eventName?.isNotEmpty == true) {
_onAllEvents.add(event);
on._getStream(event.eventName).add(event);
}
if (event.eventName == EVENT_ERROR) {
2018-10-02 15:32:06 +00:00
var error =
new AngelHttpException.fromMap((event.data ?? {}) as Map);
2017-02-28 14:15:34 +00:00
_onError.add(error);
} else if (event.eventName == EVENT_AUTHENTICATED) {
2018-07-10 16:54:55 +00:00
var authResult = new AngelAuthResult.fromMap(event.data as Map);
2017-02-28 14:15:34 +00:00
_onAuthenticated.add(authResult);
} 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});
}
}
2016-12-23 20:57:46 +00:00
}
}
2017-02-28 14:15:34 +00:00
},
cancelOnError: true,
onDone: () {
2017-04-17 11:03:42 +00:00
_socket = null;
2017-02-28 14:15:34 +00:00
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();
});
}
});
2016-12-23 20:57:46 +00:00
}
/// Serializes data to JSON.
2018-07-10 16:54:55 +00:00
serialize(x) => json.encode(x);
2016-12-23 20:57:46 +00:00
/// 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) {
2017-04-17 11:03:42 +00:00
if (_socket == null)
_queue.addLast(action);
else
socket.sink.add(serialize(action));
2016-12-23 10:47:21 +00:00
}
2017-02-28 14:15:34 +00:00
/// Attempts to authenticate a WebSocket, using a valid JWT.
void authenticateViaJwt(String jwt) {
2017-04-23 18:53:12 +00:00
send(
ACTION_AUTHENTICATE,
new WebSocketAction(params: {
'query': {'jwt': jwt}
}));
2017-02-28 14:15:34 +00:00
}
2016-12-23 10:47:21 +00:00
}
2016-12-23 20:57:46 +00:00
/// A [Service] that asynchronously interacts with the server.
2018-10-21 08:15:51 +00:00
class WebSocketsService<Id, Data> extends Service<Id, Data> {
2016-12-23 20:57:46 +00:00
/// The [BaseWebSocketClient] that spawned this service.
2016-12-23 10:47:21 +00:00
@override
2016-12-23 20:57:46 +00:00
final BaseWebSocketClient app;
/// Used to deserialize JSON into typed data.
2018-10-21 08:15:51 +00:00
final AngelDeserializer<Data> deserializer;
2016-12-23 20:57:46 +00:00
/// The [WebSocketChannel] to listen to, and send data across.
2016-12-23 10:47:21 +00:00
final WebSocketChannel socket;
2016-12-23 20:57:46 +00:00
/// The service path to listen to.
final String path;
final StreamController<WebSocketEvent> _onAllEvents =
2016-12-23 10:47:21 +00:00
new StreamController<WebSocketEvent>();
2018-10-02 15:32:06 +00:00
final StreamController _onIndexed = new StreamController();
2018-10-21 08:15:51 +00:00
final StreamController<Data> _onRead = new StreamController<Data>();
final StreamController<Data> _onCreated = new StreamController<Data>();
final StreamController<Data> _onModified = new StreamController<Data>();
final StreamController<Data> _onUpdated = new StreamController<Data>();
final StreamController<Data> _onRemoved = new StreamController<Data>();
2016-12-23 10:47:21 +00:00
/// Fired on all events.
2016-12-23 20:57:46 +00:00
Stream<WebSocketEvent> get onAllEvents => _onAllEvents.stream;
2016-12-23 10:47:21 +00:00
/// Fired on `index` events.
2017-12-21 20:15:47 +00:00
Stream get onIndexed => _onIndexed.stream;
2016-12-23 10:47:21 +00:00
/// Fired on `read` events.
2018-10-21 08:15:51 +00:00
Stream<Data> get onRead => _onRead.stream;
2016-12-23 10:47:21 +00:00
/// Fired on `created` events.
2018-10-21 08:15:51 +00:00
Stream<Data> get onCreated => _onCreated.stream;
2016-12-23 10:47:21 +00:00
/// Fired on `modified` events.
2018-10-21 08:15:51 +00:00
Stream<Data> get onModified => _onModified.stream;
2016-12-23 10:47:21 +00:00
/// Fired on `updated` events.
2018-10-21 08:15:51 +00:00
Stream<Data> get onUpdated => _onUpdated.stream;
2016-12-23 10:47:21 +00:00
/// Fired on `removed` events.
2018-10-21 08:15:51 +00:00
Stream<Data> get onRemoved => _onRemoved.stream;
2016-12-23 10:47:21 +00:00
2017-06-30 23:09:03 +00:00
WebSocketsService(this.socket, this.app, this.path, {this.deserializer}) {
2016-12-23 20:57:46 +00:00
listen();
}
2017-03-02 03:54:58 +00:00
Future close() async {
_onAllEvents.close();
_onCreated.close();
_onIndexed.close();
_onModified.close();
_onRead.close();
_onRemoved.close();
_onUpdated.close();
}
2016-12-23 20:57:46 +00:00
/// Serializes an [action] to be sent over a WebSocket.
2018-07-10 16:54:55 +00:00
serialize(WebSocketAction action) => json.encode(action);
2016-12-23 10:47:21 +00:00
2016-12-23 20:57:46 +00:00
/// Deserializes data from a [WebSocketEvent].
2018-10-21 08:15:51 +00:00
Data deserialize(x) {
return deserializer != null ? deserializer(x) : x as Data;
2016-12-23 20:57:46 +00:00
}
/// Deserializes the contents of an [event].
2018-10-21 08:15:51 +00:00
WebSocketEvent<Data> transformEvent(WebSocketEvent<Data> event) {
2016-12-23 20:57:46 +00:00
return event..data = deserialize(event.data);
}
/// Starts listening for events.
2016-12-23 10:47:21 +00:00
void listen() {
2016-12-23 20:57:46 +00:00
app.onServiceEvent.listen((map) {
if (map.containsKey(path)) {
2018-10-21 08:15:51 +00:00
var event = map[path].cast<Data>();
2017-12-21 20:15:47 +00:00
var transformed = transformEvent(event).data;
2016-12-23 20:57:46 +00:00
_onAllEvents.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;
}
}
2016-12-23 10:47:21 +00:00
});
}
2016-12-23 20:57:46 +00:00
/// Sends the given [action] on the [socket].
void send(WebSocketAction action) {
2017-04-17 11:03:42 +00:00
app.sendAction(action);
2016-12-23 20:57:46 +00:00
}
2016-12-23 10:47:21 +00:00
@override
2018-10-02 15:32:06 +00:00
Future index([Map<String, dynamic> params]) async {
2017-04-17 11:03:42 +00:00
app.sendAction(new WebSocketAction(
eventName: '$path::${ACTION_INDEX}', params: params ?? {}));
2016-12-23 20:57:46 +00:00
return null;
2016-12-23 10:47:21 +00:00
}
@override
2018-10-02 15:32:06 +00:00
Future read(id, [Map<String, dynamic> params]) async {
2017-04-17 11:03:42 +00:00
app.sendAction(new WebSocketAction(
2018-10-02 15:32:06 +00:00
eventName: '$path::${ACTION_READ}',
id: id.toString(),
params: params ?? {}));
2016-12-23 20:57:46 +00:00
return null;
2016-12-23 10:47:21 +00:00
}
@override
2018-10-02 15:32:06 +00:00
Future create(data, [Map<String, dynamic> params]) async {
2017-04-17 11:03:42 +00:00
app.sendAction(new WebSocketAction(
2017-02-12 03:12:20 +00:00
eventName: '$path::${ACTION_CREATE}',
data: data,
2017-04-17 11:03:42 +00:00
params: params ?? {}));
2016-12-23 20:57:46 +00:00
return null;
2016-12-23 10:47:21 +00:00
}
@override
2018-10-02 15:32:06 +00:00
Future modify(id, data, [Map<String, dynamic> params]) async {
2017-04-17 11:03:42 +00:00
app.sendAction(new WebSocketAction(
2017-02-12 03:12:20 +00:00
eventName: '$path::${ACTION_MODIFY}',
2018-07-10 16:54:55 +00:00
id: id.toString(),
2017-02-12 03:12:20 +00:00
data: data,
2017-04-17 11:03:42 +00:00
params: params ?? {}));
2016-12-23 20:57:46 +00:00
return null;
2016-12-23 10:47:21 +00:00
}
@override
2018-10-02 15:32:06 +00:00
Future update(id, data, [Map<String, dynamic> params]) async {
2017-04-17 11:03:42 +00:00
app.sendAction(new WebSocketAction(
2017-02-12 03:12:20 +00:00
eventName: '$path::${ACTION_UPDATE}',
2018-07-10 16:54:55 +00:00
id: id.toString(),
2017-02-12 03:12:20 +00:00
data: data,
2017-04-17 11:03:42 +00:00
params: params ?? {}));
2016-12-23 20:57:46 +00:00
return null;
2016-12-23 10:47:21 +00:00
}
@override
2018-10-02 15:32:06 +00:00
Future remove(id, [Map<String, dynamic> params]) async {
2017-04-17 11:03:42 +00:00
app.sendAction(new WebSocketAction(
2018-10-02 15:32:06 +00:00
eventName: '$path::${ACTION_REMOVE}',
id: id.toString(),
params: params ?? {}));
2016-12-23 20:57:46 +00:00
return null;
2016-12-23 10:47:21 +00:00
}
2017-12-10 05:31:34 +00:00
2017-12-21 20:15:47 +00:00
/// No longer necessary.
@deprecated
Service unwrap() => this;
2016-12-23 10:47:21 +00:00
}
2016-12-23 20:57:46 +00:00
/// Contains a dynamic Map of [WebSocketEvent] streams.
2016-12-23 10:47:21 +00:00
class WebSocketExtraneousEventHandler {
Map<String, StreamController<WebSocketEvent>> _events = {};
2016-12-23 20:57:46 +00:00
StreamController<WebSocketEvent> _getStream(String index) {
if (_events[index] == null)
_events[index] = new StreamController<WebSocketEvent>();
return _events[index];
}
2016-12-24 01:45:52 +00:00
Stream<WebSocketEvent> operator [](String index) {
2016-12-23 10:47:21 +00:00
if (_events[index] == null)
_events[index] = new StreamController<WebSocketEvent>();
return _events[index].stream;
}
2018-10-02 15:32:06 +00:00
}