From 14f7fa70bba690c3bd902450701ab733cbcfb0e1 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 28 Apr 2016 20:11:13 -0400 Subject: [PATCH 01/73] Initial commit --- .gitignore | 27 +++++++++++++++++++++++++++ LICENSE | 21 +++++++++++++++++++++ README.md | 2 ++ 3 files changed, 50 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7c280441 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.buildlog +.packages +.project +.pub/ +build/ +**/packages/ + +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) +*.dart.js +*.part.js +*.js.deps +*.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..eb4ce33e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 angel-dart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..b9c64b4f --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# angel_websocket +WebSocket plugin for Angel. From 28be7342b9fa9366421763692ca29ed7aa2b2ff0 Mon Sep 17 00:00:00 2001 From: regiostech Date: Thu, 28 Apr 2016 21:19:09 -0400 Subject: [PATCH 02/73] Will finish later --- .idea/libraries/Dart_Packages.xml | 347 ++++++++++++++++++++++++++++++ .idea/vcs.xml | 6 + lib/client.dart | 30 +++ lib/server.dart | 86 ++++++++ lib/shared.dart | 53 +++++ pubspec.yaml | 10 + 6 files changed, 532 insertions(+) create mode 100644 .idea/libraries/Dart_Packages.xml create mode 100644 .idea/vcs.xml create mode 100644 lib/client.dart create mode 100644 lib/server.dart create mode 100644 lib/shared.dart create mode 100644 pubspec.yaml diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml new file mode 100644 index 00000000..7cf9e265 --- /dev/null +++ b/.idea/libraries/Dart_Packages.xml @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/lib/client.dart b/lib/client.dart new file mode 100644 index 00000000..e4fe74c4 --- /dev/null +++ b/lib/client.dart @@ -0,0 +1,30 @@ +library angel_websocket.client; + +import 'dart:convert' show JSON; +import 'dart:html'; +import 'shared.dart'; + +class Angel { + String wsEndPoint; + WebSocket _socket; + + Angel(String this.wsEndPoint) { + _socket = new WebSocket(wsEndPoint); + } + + AngelService service(String path) { + return new AngelService._base(_socket, path.trim().replaceAll(new RegExp(r'(^\/+)|(\/+$)'), '')); + } +} + +class AngelService { + WebSocket _socket; + String path; + + AngelService._base(WebSocket this._socket, path) {} + + index([Map params]) { + AngelMessage request = new AngelMessage(path, 'index', body: params); + _socket.send(JSON.encode(request.toMap())); + } +} \ No newline at end of file diff --git a/lib/server.dart b/lib/server.dart new file mode 100644 index 00000000..dece693e --- /dev/null +++ b/lib/server.dart @@ -0,0 +1,86 @@ +///Exposes WebSocket functionality to Angel. +library angel_websocket.server; + +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'shared.dart'; + +_respond(AngelMessage message, Service service, Angel app) async { + if (message.method == 'index') { + return await service.index(message.body['query']); + } + + else if (message.method == 'read') { + return await service.read(message.body['id'], message.body['query']); + } + + else if (message.method == 'modify') { + return await service.modify( + message.body['id'], message.body['data'] ?? {}, message.body['query']); + } + + else if (message.method == 'update') { + return await service.update( + message.body['id'], message.body['data'] ?? {}, message.body['query']); + } + + else if (message.method == 'remove') { + return await service.remove(message.body['id'], message.body['query']); + } + + else throw new AngelHttpException.NotImplemented( + message: "This service does not support a \"${message + .method}\" method."); +} + +_handleMsg(WebSocket socket, Angel app) { + return (msg) async { + String text = msg.toString(); + try { + AngelMessage incoming = new AngelMessage.fromMap( + app.god.serializeToMap(text)); + try { + Service service = app.service(incoming.service); + if (service == null) { + throw new AngelHttpException.NotFound( + message: 'The requested service does not exist.'); + } + + // Now, let's respond. :) + var result = await _respond(incoming, service, app); + AngelMessage response = new AngelMessage( + incoming.service, incoming.method, body: {'result': result}); + socket.add(app.god.serialize(response)); + } catch (e) { + AngelHttpException err = (e is AngelHttpException) + ? e + : new AngelHttpException(e); + AngelMessage response = new AngelMessage( + incoming.service, incoming.method, body: err.toMap()); + socket.add(app.god.serialize(response)); + } + } catch (e) { + // If we are sent invalid data, we're not even going to + // bother responding. :) + } + }; +} + +websocket({String endPoint: '/ws'}) { + return (Angel app) async { + app.get(endPoint, (RequestContext req, ResponseContext res) async { + if (WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) { + res + ..end() + ..willCloseItself = true; + WebSocket socket = await WebSocketTransformer.upgrade( + req.underlyingRequest); + + socket.listen(_handleMsg(socket, app)); + } else { + throw new AngelHttpException.BadRequest( + message: 'This endpoint is only available via WebSockets.'); + } + }); + }; +} \ No newline at end of file diff --git a/lib/shared.dart b/lib/shared.dart new file mode 100644 index 00000000..7ce9b229 --- /dev/null +++ b/lib/shared.dart @@ -0,0 +1,53 @@ +library angel_websocket; +import 'dart:math'; + +String _randomString(int length) { + var rand = new Random(); + var codeUnits = new List.generate( + length, + (index){ + return rand.nextInt(33)+89; + } + ); + + return new String.fromCharCodes(codeUnits); +} + +/// A WebSocket message sent from server to client, or vice-versa. +class AngelMessage { + String id; + String service; + String method; + Map body; + + AngelMessage(String this.service, String this.method, + {Map this.body: const {}}) { + id = _randomString(32); + } + + /// Parses a Map into an AngelMessage. + AngelMessage.fromMap(Map msg) { + bool invalid = !(msg['service'] is String) || + (msg['service'] is String && msg['service'].isEmpty); + invalid = invalid || !(msg['method'] is String) || + (msg['method'] is String && msg['method'].isEmpty); + + if (invalid) { + throw new Exception("Invalid message supplied."); + } else { + this.id = _randomString(32); + this.service = msg['service']; + this.method = msg['method']; + this.body = msg['body'] ?? {}; + } + } + + Map toMap() { + return { + 'id': id, + 'service': service, + 'method': method, + 'body': body + }; + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..35516d53 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,10 @@ +name: angel_websocket +description: WebSocket plugin for Angel +version: 0.0.0-dev.0 +author: thosakwe +homepage: https://github.com/angel-dart/angel_websocket +dependencies: + angel_framework: ">=0.0.0-dev < 0.1.0" +dev_dependencies: + http: ">= 0.11.3 < 0.12.0" + test: ">= 0.12.13 < 0.13.0" \ No newline at end of file From 4b6ceec17e16c1d2814c9077031a2a84e2037f87 Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 3 May 2016 19:42:06 -0400 Subject: [PATCH 03/73] Make this event-based --- .idea/libraries/Dart_Packages.xml | 24 ++++++++++----------- lib/server.dart | 36 +++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index 7cf9e265..3c9bfe42 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -12,7 +12,7 @@ - @@ -61,21 +61,21 @@ - - - @@ -180,7 +180,7 @@ - @@ -278,7 +278,7 @@ - @@ -300,16 +300,16 @@ - + - - - + + + @@ -324,7 +324,7 @@ - + @@ -338,7 +338,7 @@ - + diff --git a/lib/server.dart b/lib/server.dart index dece693e..87169e82 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -5,6 +5,8 @@ import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'shared.dart'; +List sockets = []; + _respond(AngelMessage message, Service service, Angel app) async { if (message.method == 'index') { return await service.index(message.body['query']); @@ -20,12 +22,12 @@ _respond(AngelMessage message, Service service, Angel app) async { } else if (message.method == 'update') { - return await service.update( + await service.update( message.body['id'], message.body['data'] ?? {}, message.body['query']); } else if (message.method == 'remove') { - return await service.remove(message.body['id'], message.body['query']); + await service.remove(message.body['id'], message.body['query']); } else throw new AngelHttpException.NotImplemented( @@ -66,7 +68,27 @@ _handleMsg(WebSocket socket, Angel app) { }; } -websocket({String endPoint: '/ws'}) { +_wireHooks(bool hookAll) { + return (Angel app) async { + for (Pattern path in app.services.keys) { + Service _service = app.services[path]; + + // Hook any unhooked services + if (!(_service is HookedService) && hookAll) { + app.services[path] = new HookedService(_service); + } + + Service service = app.services[path]; + if (service is HookedService) { + service.onIndexed.listen((List items) { + + }); + } + } + }; +} + +websocket({String endPoint: '/ws', bool hookAll: true}) { return (Angel app) async { app.get(endPoint, (RequestContext req, ResponseContext res) async { if (WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) { @@ -76,11 +98,17 @@ websocket({String endPoint: '/ws'}) { WebSocket socket = await WebSocketTransformer.upgrade( req.underlyingRequest); - socket.listen(_handleMsg(socket, app)); + sockets.add(socket); + socket.listen(_handleMsg(socket, app), onDone: () { + // Remove from cache on disconnect + sockets.remove(socket); + }); } else { throw new AngelHttpException.BadRequest( message: 'This endpoint is only available via WebSockets.'); } }); + + await app.configure(_wireHooks(hookAll)); }; } \ No newline at end of file From 098411def7a61cf30f29f753688ab6249ea54d1d Mon Sep 17 00:00:00 2001 From: regiostech Date: Sun, 26 Jun 2016 20:42:21 -0400 Subject: [PATCH 04/73] Still needs a LOT of work. --- .idea/libraries/Dart_Packages.xml | 132 ++++++++++++--------- README.md | 2 +- lib/angel_websocket.dart | 16 +++ lib/client.dart | 30 ----- lib/server.dart | 188 ++++++++++++++++-------------- lib/shared.dart | 53 --------- pubspec.yaml | 7 +- 7 files changed, 198 insertions(+), 230 deletions(-) create mode 100644 lib/angel_websocket.dart delete mode 100644 lib/client.dart delete mode 100644 lib/shared.dart diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index 3c9bfe42..c3dbf8d7 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -12,28 +12,28 @@ - - - - @@ -47,7 +47,7 @@ - @@ -61,35 +61,35 @@ - - - - - @@ -103,28 +103,42 @@ - - - - + + + + + + + + + + + + @@ -159,7 +173,7 @@ - @@ -187,28 +201,28 @@ - - - - @@ -229,42 +243,42 @@ - - - - - - @@ -275,6 +289,13 @@ + + + + + + @@ -285,14 +306,14 @@ - - @@ -300,47 +321,50 @@ - - - - + + + + - + - - - - - + + + + + - - - - + + + + + + - + - - - - + + + + - - - - - - + + + + + + + - - + + diff --git a/README.md b/README.md index b9c64b4f..06ed0eb9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # angel_websocket -WebSocket plugin for Angel. +WebSocket plugin for Angel. Features JWT support. diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart new file mode 100644 index 00000000..dca59e7b --- /dev/null +++ b/lib/angel_websocket.dart @@ -0,0 +1,16 @@ +library angel_websocket; + +class WebSocketEvent { + String id; + String eventName; + var data; + + WebSocketEvent({String this.id, String this.eventName, this.data}); +} + +class WebSocketAction { + String id; + String eventName; + var data; + var params; +} \ No newline at end of file diff --git a/lib/client.dart b/lib/client.dart deleted file mode 100644 index e4fe74c4..00000000 --- a/lib/client.dart +++ /dev/null @@ -1,30 +0,0 @@ -library angel_websocket.client; - -import 'dart:convert' show JSON; -import 'dart:html'; -import 'shared.dart'; - -class Angel { - String wsEndPoint; - WebSocket _socket; - - Angel(String this.wsEndPoint) { - _socket = new WebSocket(wsEndPoint); - } - - AngelService service(String path) { - return new AngelService._base(_socket, path.trim().replaceAll(new RegExp(r'(^\/+)|(\/+$)'), '')); - } -} - -class AngelService { - WebSocket _socket; - String path; - - AngelService._base(WebSocket this._socket, path) {} - - index([Map params]) { - AngelMessage request = new AngelMessage(path, 'index', body: params); - _socket.send(JSON.encode(request.toMap())); - } -} \ No newline at end of file diff --git a/lib/server.dart b/lib/server.dart index 87169e82..5969d053 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -1,114 +1,122 @@ -///Exposes WebSocket functionality to Angel. library angel_websocket.server; +import 'dart:async'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; -import 'shared.dart'; +import 'package:json_god/json_god.dart' as god; +import 'package:uuid/uuid.dart'; +import 'angel_websocket.dart'; -List sockets = []; +typedef Future WebSocketFilter(WebsocketContext context); -_respond(AngelMessage message, Service service, Angel app) async { - if (message.method == 'index') { - return await service.index(message.body['query']); - } +List _clients = []; +Uuid _uuid = new Uuid(); - else if (message.method == 'read') { - return await service.read(message.body['id'], message.body['query']); - } +class WebsocketContext { + WebSocket socket; + RequestContext request; + ResponseContext response; - else if (message.method == 'modify') { - return await service.modify( - message.body['id'], message.body['data'] ?? {}, message.body['query']); - } - - else if (message.method == 'update') { - await service.update( - message.body['id'], message.body['data'] ?? {}, message.body['query']); - } - - else if (message.method == 'remove') { - await service.remove(message.body['id'], message.body['query']); - } - - else throw new AngelHttpException.NotImplemented( - message: "This service does not support a \"${message - .method}\" method."); + WebsocketContext(WebSocket this.socket, RequestContext this.request, + ResponseContext this.response); } -_handleMsg(WebSocket socket, Angel app) { - return (msg) async { - String text = msg.toString(); - try { - AngelMessage incoming = new AngelMessage.fromMap( - app.god.serializeToMap(text)); - try { - Service service = app.service(incoming.service); - if (service == null) { - throw new AngelHttpException.NotFound( - message: 'The requested service does not exist.'); - } +_broadcast(WebSocketEvent event) { + String json = god.serialize(event); + _clients.forEach((WebsocketContext client) { + client.socket.add(json); + }); +} - // Now, let's respond. :) - var result = await _respond(incoming, service, app); - AngelMessage response = new AngelMessage( - incoming.service, incoming.method, body: {'result': result}); - socket.add(app.god.serialize(response)); - } catch (e) { - AngelHttpException err = (e is AngelHttpException) - ? e - : new AngelHttpException(e); - AngelMessage response = new AngelMessage( - incoming.service, incoming.method, body: err.toMap()); - socket.add(app.god.serialize(response)); +_onData(Angel app) { + return (data) { + try { + WebSocketAction action = god.deserialize( + data, outputType: WebSocketAction); + + List split = action.eventName.split("::"); + + if (split.length >= 2) { + Service service = app.service(split[0]); + + if (service != null) { + String event = split[1]; + + if (event == "index") { + + } + } } } catch (e) { - // If we are sent invalid data, we're not even going to - // bother responding. :) + } }; } -_wireHooks(bool hookAll) { - return (Angel app) async { - for (Pattern path in app.services.keys) { - Service _service = app.services[path]; +_onError(e) { - // Hook any unhooked services - if (!(_service is HookedService) && hookAll) { - app.services[path] = new HookedService(_service); - } - - Service service = app.services[path]; - if (service is HookedService) { - service.onIndexed.listen((List items) { - - }); - } - } - }; } -websocket({String endPoint: '/ws', bool hookAll: true}) { - return (Angel app) async { - app.get(endPoint, (RequestContext req, ResponseContext res) async { - if (WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) { - res - ..end() - ..willCloseItself = true; - WebSocket socket = await WebSocketTransformer.upgrade( - req.underlyingRequest); +class websocket { + static Map filters = {}; - sockets.add(socket); - socket.listen(_handleMsg(socket, app), onDone: () { - // Remove from cache on disconnect - sockets.remove(socket); + call({List endPoints: const['/ws']}) { + return (Angel app) async { + for (Pattern endPoint in endPoints) { + app.all(endPoint, (RequestContext req, ResponseContext res) async { + if (!WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) { + res.write("This endpoint is only accessible via WebSockets."); + res.end(); + } else { + res + ..willCloseItself = true + ..end(); + WebSocket socket = await WebSocketTransformer.upgrade( + req.underlyingRequest); + WebsocketContext context = new WebsocketContext(socket, req, res); + _clients.add(context); + + socket.listen(_onData(app), onError: _onError, onDone: () { + _clients.remove(context); + }); + } }); - } else { - throw new AngelHttpException.BadRequest( - message: 'This endpoint is only available via WebSockets.'); - } - }); - await app.configure(_wireHooks(hookAll)); - }; + app.services.forEach((Pattern path, Service service) { + if (service is HookedService) { + String pathName = (path is RegExp) ? path.pattern : path; + List dispatchers = [ + service.afterIndexed, + service.afterCreated, + service.afterRead, + service.afterModified, + service.afterUpdated, + service.afterRemoved + ]; + + for (HookedServiceEventDispatcher dispatcher in dispatchers) { + dispatcher.listen((HookedServiceEvent event) async { + bool canContinue = true; + String filterName = "$pathName::${event.eventName}"; + WebSocketFilter filter = filters[filterName]; + + for (WebsocketContext client in _clients) { + if (filter != null) + canContinue = await filter(client); + } + + if (canContinue) { + WebSocketEvent socketEvent = new WebSocketEvent( + id: _uuid.v4(), + eventName: filterName, + data: event.result); + _broadcast(socketEvent); + } + }); + } + } + }); + } + }; + } } \ No newline at end of file diff --git a/lib/shared.dart b/lib/shared.dart deleted file mode 100644 index 7ce9b229..00000000 --- a/lib/shared.dart +++ /dev/null @@ -1,53 +0,0 @@ -library angel_websocket; -import 'dart:math'; - -String _randomString(int length) { - var rand = new Random(); - var codeUnits = new List.generate( - length, - (index){ - return rand.nextInt(33)+89; - } - ); - - return new String.fromCharCodes(codeUnits); -} - -/// A WebSocket message sent from server to client, or vice-versa. -class AngelMessage { - String id; - String service; - String method; - Map body; - - AngelMessage(String this.service, String this.method, - {Map this.body: const {}}) { - id = _randomString(32); - } - - /// Parses a Map into an AngelMessage. - AngelMessage.fromMap(Map msg) { - bool invalid = !(msg['service'] is String) || - (msg['service'] is String && msg['service'].isEmpty); - invalid = invalid || !(msg['method'] is String) || - (msg['method'] is String && msg['method'].isEmpty); - - if (invalid) { - throw new Exception("Invalid message supplied."); - } else { - this.id = _randomString(32); - this.service = msg['service']; - this.method = msg['method']; - this.body = msg['body'] ?? {}; - } - } - - Map toMap() { - return { - 'id': id, - 'service': service, - 'method': method, - 'body': body - }; - } -} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 35516d53..35334229 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,13 @@ name: angel_websocket description: WebSocket plugin for Angel -version: 0.0.0-dev.0 +version: 1.0.0-dev author: thosakwe homepage: https://github.com/angel-dart/angel_websocket dependencies: - angel_framework: ">=0.0.0-dev < 0.1.0" + angel_framework: ">=1.0.0-dev < 2.0.0" + json_god: ">=2.0.0-beta <3.0.0" + jwt: ">=0.1.4 <1.0.0" + uuid: ">=0.5.3 <1.0.0" dev_dependencies: http: ">= 0.11.3 < 0.12.0" test: ">= 0.12.13 < 0.13.0" \ No newline at end of file From 5be1b8b208ab1a916e9b78a67435d72e6f3f5464 Mon Sep 17 00:00:00 2001 From: regiostech Date: Tue, 5 Jul 2016 21:28:00 -0400 Subject: [PATCH 05/73] Redesigned it. Still needs a LOT of work, but now there is a clear direction. --- .idea/libraries/Dart_Packages.xml | 12 +- .idea/runConfigurations/All_Tests.xml | 6 + .idea/runConfigurations/Server_Tests.xml | 6 + lib/angel_websocket.dart | 5 +- lib/server.dart | 256 +++++++++++++---------- lib/server_old.dart | 120 +++++++++++ lib/websocket_context.dart | 17 ++ pubspec.yaml | 1 + test/all_tests.dart | 6 + test/common.dart | 20 ++ test/server.dart | 44 ++++ 11 files changed, 384 insertions(+), 109 deletions(-) create mode 100644 .idea/runConfigurations/All_Tests.xml create mode 100644 .idea/runConfigurations/Server_Tests.xml create mode 100644 lib/server_old.dart create mode 100644 lib/websocket_context.dart create mode 100644 test/all_tests.dart create mode 100644 test/common.dart create mode 100644 test/server.dart diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index c3dbf8d7..edbf2248 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -9,10 +9,17 @@ + + + + + + - @@ -321,7 +328,8 @@ - + + diff --git a/.idea/runConfigurations/All_Tests.xml b/.idea/runConfigurations/All_Tests.xml new file mode 100644 index 00000000..a824b209 --- /dev/null +++ b/.idea/runConfigurations/All_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Server_Tests.xml b/.idea/runConfigurations/Server_Tests.xml new file mode 100644 index 00000000..0e54f842 --- /dev/null +++ b/.idea/runConfigurations/Server_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart index dca59e7b..1690486f 100644 --- a/lib/angel_websocket.dart +++ b/lib/angel_websocket.dart @@ -1,11 +1,10 @@ library angel_websocket; class WebSocketEvent { - String id; String eventName; var data; - WebSocketEvent({String this.id, String this.eventName, this.data}); + WebSocketEvent({String this.eventName, this.data}); } class WebSocketAction { @@ -13,4 +12,6 @@ class WebSocketAction { String eventName; var data; var params; + + WebSocketAction({String this.id, String this.eventName, this.data, this.params}); } \ No newline at end of file diff --git a/lib/server.dart b/lib/server.dart index 5969d053..d0dbbaaf 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -2,121 +2,167 @@ library angel_websocket.server; import 'dart:async'; import 'dart:io'; +import 'dart:mirrors'; import 'package:angel_framework/angel_framework.dart'; import 'package:json_god/json_god.dart' as god; +import 'package:merge_map/merge_map.dart'; import 'package:uuid/uuid.dart'; import 'angel_websocket.dart'; -typedef Future WebSocketFilter(WebsocketContext context); +part 'websocket_context.dart'; -List _clients = []; -Uuid _uuid = new Uuid(); +final AngelWebSocket websocket = new AngelWebSocket("/ws"); -class WebsocketContext { - WebSocket socket; - RequestContext request; - ResponseContext response; - - WebsocketContext(WebSocket this.socket, RequestContext this.request, - ResponseContext this.response); +class Realtime { + const Realtime(); } -_broadcast(WebSocketEvent event) { - String json = god.serialize(event); - _clients.forEach((WebsocketContext client) { - client.socket.add(json); - }); -} +class AngelWebSocket { + Angel _app; + List _clients = []; + List servicesAlreadyWired = []; + String endpoint; -_onData(Angel app) { - return (data) { - try { - WebSocketAction action = god.deserialize( - data, outputType: WebSocketAction); + AngelWebSocket(String this.endpoint); - List split = action.eventName.split("::"); - - if (split.length >= 2) { - Service service = app.service(split[0]); - - if (service != null) { - String event = split[1]; - - if (event == "index") { - - } - } - } - } catch (e) { - - } - }; -} - -_onError(e) { - -} - -class websocket { - static Map filters = {}; - - call({List endPoints: const['/ws']}) { - return (Angel app) async { - for (Pattern endPoint in endPoints) { - app.all(endPoint, (RequestContext req, ResponseContext res) async { - if (!WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) { - res.write("This endpoint is only accessible via WebSockets."); - res.end(); - } else { - res - ..willCloseItself = true - ..end(); - WebSocket socket = await WebSocketTransformer.upgrade( - req.underlyingRequest); - WebsocketContext context = new WebsocketContext(socket, req, res); - _clients.add(context); - - socket.listen(_onData(app), onError: _onError, onDone: () { - _clients.remove(context); - }); - } - }); - - app.services.forEach((Pattern path, Service service) { - if (service is HookedService) { - String pathName = (path is RegExp) ? path.pattern : path; - List dispatchers = [ - service.afterIndexed, - service.afterCreated, - service.afterRead, - service.afterModified, - service.afterUpdated, - service.afterRemoved - ]; - - for (HookedServiceEventDispatcher dispatcher in dispatchers) { - dispatcher.listen((HookedServiceEvent event) async { - bool canContinue = true; - String filterName = "$pathName::${event.eventName}"; - WebSocketFilter filter = filters[filterName]; - - for (WebsocketContext client in _clients) { - if (filter != null) - canContinue = await filter(client); - } - - if (canContinue) { - WebSocketEvent socketEvent = new WebSocketEvent( - id: _uuid.v4(), - eventName: filterName, - data: event.result); - _broadcast(socketEvent); - } - }); - } - } - }); - } + _batchEvent(String path) { + return (HookedServiceEvent e) async { + var event = await transformEvent(e); + event.eventName = "$path::${event.eventName}"; + await batchEvent(event); }; } -} \ No newline at end of file + + Future batchEvent(WebSocketEvent event) async { + // Default implementation will just immediately fire events + _clients.forEach((client) { + client.add(god.serialize(event)); + }); + } + + Future> getBatchedEvents() async => []; + + Future handleAction(WebSocketAction action, WebSocketContext socket) async { + var split = action.eventName.split("::"); + + if (split.length < 2) + return socket.sendError(new AngelHttpException.BadRequest()); + + var service = _app.service(split[0]); + + if (service == null) + return socket.sendError(new AngelHttpException.NotFound( + message: "No service \"${split[0]}\" exists.")); + + var eventName = split[1]; + + var params = mergeMap([ + god.deserializeDatum(action.params), + {"provider": Providers.WEBSOCKET} + ]); + try { + if (eventName == "index") { + return socket.send("${split[0]}::" + HookedServiceEvent.INDEXED, + await service.index(params)); + } else if (eventName == "read") { + return socket.send("${split[0]}::" + HookedServiceEvent.READ, + await service.read(action.id, params)); + } else if (eventName == "create") { + return new WebSocketEvent( + eventName: "${split[0]}::" + HookedServiceEvent.CREATED, + data: await service.create(action.data, params)); + } else if (eventName == "modify") { + return new WebSocketEvent( + eventName: "${split[0]}::" + HookedServiceEvent.MODIFIED, + data: await service.modify(action.id, action.data, params)); + } else if (eventName == "update") { + return new WebSocketEvent( + eventName: "${split[0]}::" + HookedServiceEvent.UPDATED, + data: await service.update(action.id, action.data, params)); + } else if (eventName == "remove") { + return new WebSocketEvent( + eventName: "${split[0]}::" + HookedServiceEvent.REMOVED, + data: await service.remove(action.id, params)); + } else { + return socket.sendError(new AngelHttpException.MethodNotAllowed( + message: "Method Not Allowed: \"$eventName\"")); + } + } catch (e) { + if (e is AngelHttpException) return socket.sendError(e); + + return socket.sendError(new AngelHttpException(e)); + } + } + + hookupService(Pattern _path, HookedService service) { + String path = _path.toString(); + var batch = _batchEvent(path); + + service + ..afterCreated.listen(batch) + ..afterModified.listen(batch) + ..afterUpdated.listen(batch) + ..afterRemoved.listen(batch); + + servicesAlreadyWired.add(path); + } + + onData(WebSocketContext socket, data) { + try { + WebSocketAction action = + god.deserialize(data, outputType: WebSocketAction); + + if (action.eventName == null || + action.eventName is! String || + action.eventName.isEmpty) throw new AngelHttpException.BadRequest(); + + var event = handleAction(action, socket); + if (event is WebSocketEvent) { + batchEvent(event); + } + } catch (e) { + // Send an error + socket.sendError(new AngelHttpException(e)); + } + } + + Future transformEvent(HookedServiceEvent event) async { + return new WebSocketEvent(eventName: event.eventName, data: event.result); + } + + wireAllServices(Angel app) { + for (Pattern key in app.services.keys.where((x) { + return !servicesAlreadyWired.contains(x) && + app.services[x] is HookedService; + })) { + hookupService(key, app.services[key]); + } + } + + Future call(Angel app) async { + this._app = app; + + // Set up services + wireAllServices(app); + + app.onService.listen((_) { + wireAllServices(app); + }); + + app.get(endpoint, (RequestContext req, ResponseContext res) async { + if (!WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) + throw new AngelHttpException.BadRequest(); + + var ws = await WebSocketTransformer.upgrade(req.underlyingRequest); + var socket = new WebSocketContext(ws, req, res); + + ws.listen((data) { + onData(socket, data); + }, onDone: () { + _clients.remove(ws); + }, onError: (e) { + _clients.remove(ws); + }, cancelOnError: true); + }); + } +} diff --git a/lib/server_old.dart b/lib/server_old.dart new file mode 100644 index 00000000..cf391caf --- /dev/null +++ b/lib/server_old.dart @@ -0,0 +1,120 @@ +library angel_websocket.server; + +import 'dart:async'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:json_god/json_god.dart' as god; +import 'package:uuid/uuid.dart'; +import 'angel_websocket.dart'; + +typedef Future WebSocketFilter(WebsocketContext context); + +List _clients = []; +Uuid _uuid = new Uuid(); + +class WebsocketContext { + WebSocket socket; + RequestContext request; + ResponseContext response; + + WebsocketContext(WebSocket this.socket, RequestContext this.request, + ResponseContext this.response); +} + +_broadcast(WebSocketEvent event) { + String json = god.serialize(event); + _clients.forEach((WebsocketContext client) { + client.socket.add(json); + }); +} + +_onData(Angel app) { + return (data) { + try { + WebSocketAction action = god.deserialize( + data, outputType: WebSocketAction); + + List split = action.eventName.split("::"); + + if (split.length >= 2) { + Service service = app.service(split[0]); + + if (service != null) { + String event = split[1]; + + if (event == "index") { + + } + } + } + } catch (e) { + + } + }; +} + +_onError(e) { + +} + +class websocket { + static Map filters = {}; + + call({List endPoints: const['/ws']}) { + return (Angel app) async { + for (Pattern endPoint in endPoints) { + app.all(endPoint, (RequestContext req, ResponseContext res) async { + if (!WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) { + res.write("This endpoint is only accessible via WebSockets."); + res.end(); + } else { + res + ..willCloseItself = true + ..end(); + WebSocket socket = await WebSocketTransformer.upgrade( + req.underlyingRequest); + WebsocketContext context = new WebsocketContext(socket, req, res); + _clients.add(context); + + socket.listen(_onData(app), onError: _onError, onDone: () { + _clients.remove(context); + }); + } + }); + + app.services.forEach((Pattern path, Service service) { + if (service is HookedService) { + String pathName = (path is RegExp) ? path.pattern : path; + List dispatchers = [ + service.afterIndexed, + service.afterCreated, + service.afterRead, + service.afterModified, + service.afterUpdated, + service.afterRemoved + ]; + + for (HookedServiceEventDispatcher dispatcher in dispatchers) { + dispatcher.listen((HookedServiceEvent event) async { + bool canContinue = true; + String filterName = "$pathName::${event.eventName}"; + WebSocketFilter filter = filters[filterName]; + + for (WebsocketContext client in _clients) { + if (filter != null) + canContinue = await filter(client); + } + + if (canContinue) { + WebSocketEvent socketEvent = new WebSocketEvent(eventName: filterName, + data: event.result); + _broadcast(socketEvent); + } + }); + } + } + }); + } + }; + } +} \ No newline at end of file diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart new file mode 100644 index 00000000..d9c92529 --- /dev/null +++ b/lib/websocket_context.dart @@ -0,0 +1,17 @@ +part of angel_websocket.server; + +class WebSocketContext { + WebSocket underlyingSocket; + RequestContext requestContext; + ResponseContext responseContext; + + WebSocketContext(WebSocket this.underlyingSocket, + RequestContext this.requestContext, ResponseContext this.responseContext); + + send(String eventName, data) { + underlyingSocket.add( + god.serialize(new WebSocketEvent(eventName: eventName, data: data))); + } + + sendError(AngelHttpException error) => send("error", error); +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 35334229..c8b5abde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,6 +4,7 @@ version: 1.0.0-dev author: thosakwe homepage: https://github.com/angel-dart/angel_websocket dependencies: + angel_client: ">=1.0.0-dev <2.0.0" angel_framework: ">=1.0.0-dev < 2.0.0" json_god: ">=2.0.0-beta <3.0.0" jwt: ">=0.1.4 <1.0.0" diff --git a/test/all_tests.dart b/test/all_tests.dart new file mode 100644 index 00000000..b9d49b66 --- /dev/null +++ b/test/all_tests.dart @@ -0,0 +1,6 @@ +import 'package:test/test.dart'; +import 'server.dart' as server; + +main() async { + group("server", server.main); +} \ No newline at end of file diff --git a/test/common.dart b/test/common.dart new file mode 100644 index 00000000..cb09c5c0 --- /dev/null +++ b/test/common.dart @@ -0,0 +1,20 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/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"); +} diff --git a/test/server.dart b/test/server.dart new file mode 100644 index 00000000..e51fb462 --- /dev/null +++ b/test/server.dart @@ -0,0 +1,44 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_websocket/angel_websocket.dart'; +import 'package:angel_websocket/server.dart'; +import 'package:json_god/json_god.dart' as god; +import 'package:test/test.dart'; +import 'common.dart'; + +main() { + Angel app; + WebSocket socket; + + setUp(() async { + app = new Angel(); + + app.use("/real", new FakeService(), hooked: false); + app.use("/api/todos", new MemoryService()); + + await app.configure(websocket); + await app.configure(startTestServer); + + socket = await WebSocket.connect(app.properties["ws_url"]); + }); + + 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)); + + print(await socket.first); + }); +} + +@Realtime() +class FakeService extends Service {} \ No newline at end of file From 7b1f8bf1a705056011e81126652034d8e95cc341 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 6 Jul 2016 09:33:40 -0400 Subject: [PATCH 06/73] Tests are slowly coming --- .gitignore | 1 + .idea/libraries/Dart_Packages.xml | 184 +++++------ .idea/workspace.xml | 508 ++++++++++++++++++++++++++++++ lib/client.dart | 197 ++++++++++++ lib/server.dart | 30 +- test/packages | 1 + test/server.dart | 43 ++- 7 files changed, 861 insertions(+), 103 deletions(-) create mode 100644 .idea/workspace.xml create mode 100644 lib/client.dart create mode 120000 test/packages diff --git a/.gitignore b/.gitignore index 7c280441..ea89ccf0 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ doc/api/ # Don't commit pubspec lock file # (Library packages only! Remove pattern if developing an application package) pubspec.lock +.idea \ No newline at end of file diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index edbf2248..acaad618 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -5,374 +5,374 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 00000000..7b1a505f --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + project + + + + + + + + + + + + + + + + project + + + true + + bdd + + DIRECTORY + + false + + + + + + + + + + + + + 1467772535835 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/client.dart b/lib/client.dart new file mode 100644 index 00000000..a5b74af9 --- /dev/null +++ b/lib/client.dart @@ -0,0 +1,197 @@ +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; + +class WebSocketClient extends Angel { + WebSocket _socket; + Map> _services = {}; + + WebSocketClient(String wsEndpoint) : super(wsEndpoint); + + 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") { + 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 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); + } + + @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> _events = {}; + + operator [](String index) { + if (_events[index] == null) + _events[index] = new StreamController(); + + return _events[index].stream; + } +} + +class _WebSocketServiceTransformer + implements StreamTransformer { + Type _outputType; + + _WebSocketServiceTransformer.base(this._outputType); + + @override + Stream bind(Stream stream) { + var _stream = new StreamController(); + + 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(); + var _onError = new StreamController(); + var _onIndexed = new StreamController(); + var _onRead = new StreamController(); + var _onCreated = new StreamController(); + var _onModified = new StreamController(); + var _onUpdated = new StreamController(); + var _onRemoved = new StreamController(); + + WebSocketExtraneousEventHandler get on => _on; + + Stream get onAllEvents => + _onAllEvents.stream.transform(_transformer); + + Stream get onError => _onError.stream; + + Stream get onIndexed => + _onIndexed.stream.transform(_transformer); + + Stream get onRead => _onRead.stream.transform(_transformer); + + Stream get onCreated => + _onCreated.stream.transform(_transformer); + + Stream get onModified => + _onModified.stream.transform(_transformer); + + Stream get onUpdated => + _onUpdated.stream.transform(_transformer); + + Stream 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 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))); + } +} diff --git a/lib/server.dart b/lib/server.dart index d0dbbaaf..43c306e9 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -1,6 +1,7 @@ library angel_websocket.server; import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:mirrors'; import 'package:angel_framework/angel_framework.dart'; @@ -60,6 +61,7 @@ class AngelWebSocket { god.deserializeDatum(action.params), {"provider": Providers.WEBSOCKET} ]); + try { if (eventName == "index") { return socket.send("${split[0]}::" + HookedServiceEvent.INDEXED, @@ -107,22 +109,37 @@ class AngelWebSocket { servicesAlreadyWired.add(path); } - onData(WebSocketContext socket, data) { + Future onConnect(WebSocketContext socket) async {} + + onData(WebSocketContext socket, data) async { try { - WebSocketAction action = - god.deserialize(data, outputType: WebSocketAction); + var fromJson = JSON.decode(data); + var action = new WebSocketAction( + id: fromJson['id'], + eventName: fromJson['eventName'], + data: fromJson['data'], + params: fromJson['params']); if (action.eventName == null || action.eventName is! String || - action.eventName.isEmpty) throw new AngelHttpException.BadRequest(); + action.eventName.isEmpty) { + throw new AngelHttpException.BadRequest(); + } var event = handleAction(action, socket); + if (event is Future) + event = await event; + + if (event is WebSocketEvent) { batchEvent(event); } } catch (e) { // Send an error - socket.sendError(new AngelHttpException(e)); + if (e is AngelHttpException) + socket.sendError(e); + else + socket.sendError(new AngelHttpException(e)); } } @@ -154,7 +171,10 @@ class AngelWebSocket { throw new AngelHttpException.BadRequest(); var ws = await WebSocketTransformer.upgrade(req.underlyingRequest); + _clients.add(ws); + var socket = new WebSocketContext(ws, req, res); + await onConnect(socket); ws.listen((data) { onData(socket, data); diff --git a/test/packages b/test/packages new file mode 120000 index 00000000..a16c4050 --- /dev/null +++ b/test/packages @@ -0,0 +1 @@ +../packages \ No newline at end of file diff --git a/test/server.dart b/test/server.dart index e51fb462..1ff313bd 100644 --- a/test/server.dart +++ b/test/server.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/angel_framework.dart' as server; +import 'package:angel_websocket/client.dart' as client; import 'package:angel_websocket/angel_websocket.dart'; import 'package:angel_websocket/server.dart'; import 'package:json_god/json_god.dart' as god; @@ -8,19 +9,28 @@ import 'package:test/test.dart'; import 'common.dart'; main() { - Angel app; + server.Angel app; + client.WebSocketClient clientApp; + client.WebSocketService clientTodos; WebSocket socket; setUp(() async { - app = new Angel(); + app = new server.Angel(); app.use("/real", new FakeService(), hooked: false); - app.use("/api/todos", new MemoryService()); + app.use("/api/todos", new server.MemoryService()); + await app + .service("api/todos") + .create(new Todo(text: "Clean your room", when: "now")); await app.configure(websocket); await app.configure(startTestServer); socket = await WebSocket.connect(app.properties["ws_url"]); + clientApp = new client.WebSocketClient(app.properties["ws_url"]); + await clientApp.connect(); + + clientTodos = clientApp.service("api/todos", type: Todo); }); tearDown(() async { @@ -36,9 +46,30 @@ main() { var action = new WebSocketAction(eventName: "api/todos::index"); socket.add(god.serialize(action)); - print(await socket.first); + 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)); }); } @Realtime() -class FakeService extends Service {} \ No newline at end of file +class FakeService extends server.Service {} From e282b402c9a429adf7d91cb60fae569b93fd5a25 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 20 Jul 2016 21:36:16 -0400 Subject: [PATCH 07/73] ok --- lib/browser.dart | 193 +++++++++++++++++++++++++++++++++++++++++++++++ test/server.dart | 1 - 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 lib/browser.dart diff --git a/lib/browser.dart b/lib/browser.dart new file mode 100644 index 00000000..d9e89109 --- /dev/null +++ b/lib/browser.dart @@ -0,0 +1,193 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:html'; +import 'package:angel_client/angel_client.dart'; +import 'package:angel_websocket/angel_websocket.dart'; + +class WebSocketClient extends Angel { + WebSocket _socket; + Map> _services = {}; + + WebSocketClient(String wsEndpoint) : super(wsEndpoint) { + _socket = new WebSocket(wsEndpoint); + } + + 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; + } + } + } + } + + Future connect() async { + _socket.onMessage.listen((MessageEvent event) { + onData(event.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> _events = {}; + + operator [](String index) { + if (_events[index] == null) + _events[index] = new StreamController(); + + return _events[index].stream; + } +} + +class _WebSocketServiceTransformer + implements StreamTransformer { + Type _outputType; + + _WebSocketServiceTransformer.base(this._outputType); + + @override + Stream bind(Stream stream) { + var _stream = new StreamController(); + + 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(); + var _onError = new StreamController(); + var _onIndexed = new StreamController(); + var _onRead = new StreamController(); + var _onCreated = new StreamController(); + var _onModified = new StreamController(); + var _onUpdated = new StreamController(); + var _onRemoved = new StreamController(); + + WebSocketExtraneousEventHandler get on => _on; + + Stream get onAllEvents => + _onAllEvents.stream.transform(_transformer); + + Stream get onError => _onError.stream; + + Stream get onIndexed => + _onIndexed.stream.transform(_transformer); + + Stream get onRead => _onRead.stream.transform(_transformer); + + Stream get onCreated => + _onCreated.stream.transform(_transformer); + + Stream get onModified => + _onModified.stream.transform(_transformer); + + Stream get onUpdated => + _onUpdated.stream.transform(_transformer); + + Stream 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 index([Map params]) async { + connection.send(god.serialize( + new WebSocketAction(eventName: "$_path::index", params: params))); + return null; + } + + @override + Future read(id, [Map params]) async { + connection.send(god.serialize(new WebSocketAction( + eventName: "$_path::read", id: id, params: params))); + } + + @override + Future create(data, [Map params]) async { + connection.send(god.serialize(new WebSocketAction( + eventName: "$_path::create", data: data, params: params))); + } + + @override + Future modify(id, data, [Map params]) async { + connection.send(god.serialize(new WebSocketAction( + eventName: "$_path::modify", id: id, data: data, params: params))); + } + + @override + Future update(id, data, [Map params]) async { + connection.send(god.serialize(new WebSocketAction( + eventName: "$_path::update", id: id, data: data, params: params))); + } + + @override + Future remove(id, [Map params]) async { + connection.send(god.serialize(new WebSocketAction( + eventName: "$_path::remove", id: id, params: params))); + } +} diff --git a/test/server.dart b/test/server.dart index 1ff313bd..d74b44bf 100644 --- a/test/server.dart +++ b/test/server.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart' as server; import 'package:angel_websocket/client.dart' as client; From 778f0868ffad8043ba337fd00ea9cb0f8c2c80d1 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 3 Sep 2016 07:24:58 -0400 Subject: [PATCH 08/73] Bye IDEA --- .idea/libraries/Dart_Packages.xml | 379 ----------------- .idea/runConfigurations/All_Tests.xml | 6 - .idea/runConfigurations/Server_Tests.xml | 6 - .idea/vcs.xml | 6 - .idea/workspace.xml | 508 ----------------------- 5 files changed, 905 deletions(-) delete mode 100644 .idea/libraries/Dart_Packages.xml delete mode 100644 .idea/runConfigurations/All_Tests.xml delete mode 100644 .idea/runConfigurations/Server_Tests.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/workspace.xml diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml deleted file mode 100644 index acaad618..00000000 --- a/.idea/libraries/Dart_Packages.xml +++ /dev/null @@ -1,379 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/All_Tests.xml b/.idea/runConfigurations/All_Tests.xml deleted file mode 100644 index a824b209..00000000 --- a/.idea/runConfigurations/All_Tests.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Server_Tests.xml b/.idea/runConfigurations/Server_Tests.xml deleted file mode 100644 index 0e54f842..00000000 --- a/.idea/runConfigurations/Server_Tests.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 7b1a505f..00000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,508 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - project - - - - - - - - - - - - - - - - project - - - true - - bdd - - DIRECTORY - - false - - - - - - - - - - - - - 1467772535835 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From bdb9ad4fcd78f7a6124d66608e4ce8227d74bdf3 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 3 Sep 2016 07:33:59 -0400 Subject: [PATCH 09/73] Controller is missing??? --- lib/browser.dart | 31 ++++++++++++++++++++++++------- lib/server.dart | 2 -- pubspec.yaml | 4 ++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/browser.dart b/lib/browser.dart index d9e89109..56d1fea2 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:html'; import 'package:angel_client/angel_client.dart'; import 'package:angel_websocket/angel_websocket.dart'; +export 'package:angel_websocket/angel_websocket.dart'; class WebSocketClient extends Angel { WebSocket _socket; @@ -97,8 +98,9 @@ class _WebSocketServiceTransformer var _stream = new StreamController(); stream.listen((WebSocketEvent e) { - if (_outputType != null && e.eventName != "error") + /* if (_outputType != null && e.eventName != "error") e.data = god.deserialize(god.serialize(e.data), outputType: _outputType); + */ _stream.add(e); }); @@ -154,40 +156,55 @@ class WebSocketService extends Service { _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 Future index([Map params]) async { - connection.send(god.serialize( + connection.send(_serialize( new WebSocketAction(eventName: "$_path::index", params: params))); return null; } @override Future read(id, [Map params]) async { - connection.send(god.serialize(new WebSocketAction( + connection.send(_serialize(new WebSocketAction( eventName: "$_path::read", id: id, params: params))); } @override Future create(data, [Map params]) async { - connection.send(god.serialize(new WebSocketAction( + connection.send(_serialize(new WebSocketAction( eventName: "$_path::create", data: data, params: params))); } @override Future modify(id, data, [Map params]) async { - connection.send(god.serialize(new WebSocketAction( + connection.send(_serialize(new WebSocketAction( eventName: "$_path::modify", id: id, data: data, params: params))); } @override Future update(id, data, [Map params]) async { - connection.send(god.serialize(new WebSocketAction( + connection.send(_serialize(new WebSocketAction( eventName: "$_path::update", id: id, data: data, params: params))); } @override Future remove(id, [Map params]) async { - connection.send(god.serialize(new WebSocketAction( + connection.send(_serialize(new WebSocketAction( eventName: "$_path::remove", id: id, params: params))); } } diff --git a/lib/server.dart b/lib/server.dart index 43c306e9..f52d0805 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -3,11 +3,9 @@ library angel_websocket.server; import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:mirrors'; import 'package:angel_framework/angel_framework.dart'; import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; -import 'package:uuid/uuid.dart'; import 'angel_websocket.dart'; part 'websocket_context.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index c8b5abde..b5e0a63a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_websocket description: WebSocket plugin for Angel -version: 1.0.0-dev +version: 1.0.0-dev+1 author: thosakwe homepage: https://github.com/angel-dart/angel_websocket dependencies: @@ -11,4 +11,4 @@ dependencies: uuid: ">=0.5.3 <1.0.0" dev_dependencies: http: ">= 0.11.3 < 0.12.0" - test: ">= 0.12.13 < 0.13.0" \ No newline at end of file + test: ">= 0.12.13 < 0.13.0" From 29a682cce8126c16e49723c67e93d24f382cf5e9 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 3 Sep 2016 08:34:01 -0400 Subject: [PATCH 10/73] Readme --- README.md | 65 ++++++++++++++++++ lib/angel_websocket.dart | 2 +- lib/browser.dart | 1 + lib/{client.dart => cli.dart} | 2 + lib/server.dart | 1 + lib/server_old.dart | 120 ---------------------------------- lib/websocket_context.dart | 2 +- pubspec.yaml | 2 +- test/server.dart | 3 +- 9 files changed, 73 insertions(+), 125 deletions(-) rename lib/{client.dart => cli.dart} (98%) delete mode 100644 lib/server_old.dart diff --git a/README.md b/README.md index 06ed0eb9..015720eb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,67 @@ # angel_websocket WebSocket plugin for Angel. Features JWT support. + + +# Usage + +**Server-side** + +```dart +import "package:angel_framework/angel_framework.dart"; +import "package:angel_websocket/server.dart"; + +main() async { + var app = new Angel(); + await app.configure(new AngelWebSocket("/ws")); +} + +``` + +**In the Browser** + +```dart +import "package:angel_websocket/browser.dart"; + +main() async { + Angel app = new WebSocketClient("/ws"); + var Cars = app.service("api/cars"); + + Cars.onCreated.listen((e) => print("New car: ${e.data}")); + + // Happens asynchronously + Cars.create({"brand": "Toyota"}); +} +``` + +**CLI Client** + +```dart +import "package:angel_framework/angel_framework" as srv; +import "package:angel_websocket/browser.dart"; + +// You can include these in a shared file and access on both client and server +class Car extends srv.Model { + int year; + String brand, make; + + Car({this.year, this.brand, this.make}); + + @override String toString() => "$year $brand $make"; +} + +main() async { + Angel app = new WebSocketClient("/ws"); + var Cars = app.service("api/cars", type: Car); + + Cars.onCreated.listen((e) { + // Automatically deserialized into a car :) + Car car = e.data; + + // I just bought a new 2016 Toyota Camry! + print("I just bought a new $car!"); + }); + + // Happens asynchronously + Cars.create({"year": 2016, "brand": "Toyota", "make": "Camry"}); +} +``` diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart index 1690486f..fc058014 100644 --- a/lib/angel_websocket.dart +++ b/lib/angel_websocket.dart @@ -14,4 +14,4 @@ class WebSocketAction { var params; WebSocketAction({String this.id, String this.eventName, this.data, this.params}); -} \ No newline at end of file +} diff --git a/lib/browser.dart b/lib/browser.dart index 56d1fea2..5e208a92 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:html'; import 'package:angel_client/angel_client.dart'; import 'package:angel_websocket/angel_websocket.dart'; +export 'package:angel_client/angel_client.dart'; export 'package:angel_websocket/angel_websocket.dart'; class WebSocketClient extends Angel { diff --git a/lib/client.dart b/lib/cli.dart similarity index 98% rename from lib/client.dart rename to lib/cli.dart index a5b74af9..7802d4db 100644 --- a/lib/client.dart +++ b/lib/cli.dart @@ -5,6 +5,8 @@ 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; diff --git a/lib/server.dart b/lib/server.dart index f52d0805..6d06b6d1 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -7,6 +7,7 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; import 'angel_websocket.dart'; +export 'angel_websocket.dart'; part 'websocket_context.dart'; diff --git a/lib/server_old.dart b/lib/server_old.dart deleted file mode 100644 index cf391caf..00000000 --- a/lib/server_old.dart +++ /dev/null @@ -1,120 +0,0 @@ -library angel_websocket.server; - -import 'dart:async'; -import 'dart:io'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:json_god/json_god.dart' as god; -import 'package:uuid/uuid.dart'; -import 'angel_websocket.dart'; - -typedef Future WebSocketFilter(WebsocketContext context); - -List _clients = []; -Uuid _uuid = new Uuid(); - -class WebsocketContext { - WebSocket socket; - RequestContext request; - ResponseContext response; - - WebsocketContext(WebSocket this.socket, RequestContext this.request, - ResponseContext this.response); -} - -_broadcast(WebSocketEvent event) { - String json = god.serialize(event); - _clients.forEach((WebsocketContext client) { - client.socket.add(json); - }); -} - -_onData(Angel app) { - return (data) { - try { - WebSocketAction action = god.deserialize( - data, outputType: WebSocketAction); - - List split = action.eventName.split("::"); - - if (split.length >= 2) { - Service service = app.service(split[0]); - - if (service != null) { - String event = split[1]; - - if (event == "index") { - - } - } - } - } catch (e) { - - } - }; -} - -_onError(e) { - -} - -class websocket { - static Map filters = {}; - - call({List endPoints: const['/ws']}) { - return (Angel app) async { - for (Pattern endPoint in endPoints) { - app.all(endPoint, (RequestContext req, ResponseContext res) async { - if (!WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) { - res.write("This endpoint is only accessible via WebSockets."); - res.end(); - } else { - res - ..willCloseItself = true - ..end(); - WebSocket socket = await WebSocketTransformer.upgrade( - req.underlyingRequest); - WebsocketContext context = new WebsocketContext(socket, req, res); - _clients.add(context); - - socket.listen(_onData(app), onError: _onError, onDone: () { - _clients.remove(context); - }); - } - }); - - app.services.forEach((Pattern path, Service service) { - if (service is HookedService) { - String pathName = (path is RegExp) ? path.pattern : path; - List dispatchers = [ - service.afterIndexed, - service.afterCreated, - service.afterRead, - service.afterModified, - service.afterUpdated, - service.afterRemoved - ]; - - for (HookedServiceEventDispatcher dispatcher in dispatchers) { - dispatcher.listen((HookedServiceEvent event) async { - bool canContinue = true; - String filterName = "$pathName::${event.eventName}"; - WebSocketFilter filter = filters[filterName]; - - for (WebsocketContext client in _clients) { - if (filter != null) - canContinue = await filter(client); - } - - if (canContinue) { - WebSocketEvent socketEvent = new WebSocketEvent(eventName: filterName, - data: event.result); - _broadcast(socketEvent); - } - }); - } - } - }); - } - }; - } -} \ No newline at end of file diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index d9c92529..82e88b91 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -14,4 +14,4 @@ class WebSocketContext { } sendError(AngelHttpException error) => send("error", error); -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index b5e0a63a..c3cd1f0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_websocket description: WebSocket plugin for Angel -version: 1.0.0-dev+1 +version: 1.0.0-dev+2 author: thosakwe homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/server.dart b/test/server.dart index d74b44bf..f398198c 100644 --- a/test/server.dart +++ b/test/server.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:angel_framework/angel_framework.dart' as server; -import 'package:angel_websocket/client.dart' as client; -import 'package:angel_websocket/angel_websocket.dart'; +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'; From ab9926e1d2b279462817cf90e57fe6ed54715779 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 3 Sep 2016 08:43:34 -0400 Subject: [PATCH 11/73] close requests --- lib/server.dart | 8 +++++--- pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/server.dart b/lib/server.dart index 6d06b6d1..ad2e3559 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -126,9 +126,7 @@ class AngelWebSocket { } var event = handleAction(action, socket); - if (event is Future) - event = await event; - + if (event is Future) event = await event; if (event is WebSocketEvent) { batchEvent(event); @@ -169,6 +167,10 @@ class AngelWebSocket { if (!WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) throw new AngelHttpException.BadRequest(); + res + ..willCloseItself = true + ..end(); + var ws = await WebSocketTransformer.upgrade(req.underlyingRequest); _clients.add(ws); diff --git a/pubspec.yaml b/pubspec.yaml index c3cd1f0d..6ac23378 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_websocket description: WebSocket plugin for Angel -version: 1.0.0-dev+2 +version: 1.0.0-dev+3 author: thosakwe homepage: https://github.com/angel-dart/angel_websocket dependencies: From fb33a992818718b6b8a7ee9598237837d961830d Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 16:00:17 -0400 Subject: [PATCH 12/73] No controllers yet --- README.md | 12 +++++++++++- lib/browser.dart | 3 ++- lib/server.dart | 11 +++++++++-- test/common.dart | 2 +- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 015720eb..af853612 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # angel_websocket -WebSocket plugin for Angel. Features JWT support. +WebSocket plugin for Angel. + +This plugin broadcasts events from hooked services via WebSockets. + +In addition, +it adds itself to the app's IoC container as `AngelWebSocket`, so that it can be used +in controllers as well. + +WebSocket contexts are add to `req.params` as `'socket'`. # Usage @@ -51,6 +59,8 @@ class Car extends srv.Model { main() async { Angel app = new WebSocketClient("/ws"); + // Wait for WebSocket connection... + await app.connect(); var Cars = app.service("api/cars", type: Car); Cars.onCreated.listen((e) { diff --git a/lib/browser.dart b/lib/browser.dart index 5e208a92..70c64bcc 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -12,6 +12,7 @@ class WebSocketClient extends Angel { WebSocketClient(String wsEndpoint) : super(wsEndpoint) { _socket = new WebSocket(wsEndpoint); + _connect(); } onData(data) { @@ -60,7 +61,7 @@ class WebSocketClient extends Angel { } } - Future connect() async { + void _connect() { _socket.onMessage.listen((MessageEvent event) { onData(event.data); }); diff --git a/lib/server.dart b/lib/server.dart index ad2e3559..5a42b98b 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -17,9 +17,10 @@ class Realtime { const Realtime(); } -class AngelWebSocket { +class AngelWebSocket extends AngelPlugin { Angel _app; List _clients = []; + List get clients => new List.from(_clients, growable: false); List servicesAlreadyWired = []; String endpoint; @@ -153,8 +154,12 @@ class AngelWebSocket { } } + @override Future call(Angel app) async { - this._app = app; + this._app = app..container.singleton(this); + + if (runtimeType != AngelWebSocket) + app.container.singleton(this, as: AngelWebSocket); // Set up services wireAllServices(app); @@ -177,6 +182,8 @@ class AngelWebSocket { var socket = new WebSocketContext(ws, req, res); await onConnect(socket); + req.params['socket'] = socket; + ws.listen((data) { onData(socket, data); }, onDone: () { diff --git a/test/common.dart b/test/common.dart index cb09c5c0..bf4492f1 100644 --- a/test/common.dart +++ b/test/common.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/defs.dart'; +import 'package:angel_framework/src/defs.dart'; class Todo extends MemoryModel { String text; From 876e998445264cd79de9e3b8a1ff52e94f83e058 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 21:35:16 -0400 Subject: [PATCH 13/73] Just needs more test --- README.md | 21 +++++++++++++++++++++ lib/browser.dart | 16 ++++++++-------- lib/cli.dart | 8 ++++++++ lib/server.dart | 35 +++++++++++++++++++++++++++++------ lib/websocket_context.dart | 17 +++++++++++++++++ test/server.dart | 29 ++++++++++++++++++++++++++--- 6 files changed, 109 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index af853612..17474398 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,27 @@ main() async { ``` +**Adding Handlers within a Controller** + +```dart +import 'dart:async'; +import "package:angel_framework/angel_framework.dart"; +import "package:angel_websocket/server.dart"; + +@Expose("/") +class MyController extends Controller { + @override + Future call(AngelBase app) async { + var ws = app.container.make(AngelWebSocket); + ws.onConnection.listen((WebSocketContext socket) { + socket.on["message"].listen((WebSocketEvent e) { + socket.send("new_message", { "text": e.data["text"] }); + }); + }); + } +} +``` + **In the Browser** ```dart diff --git a/lib/browser.dart b/lib/browser.dart index 70c64bcc..69252e56 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -9,6 +9,7 @@ export 'package:angel_websocket/angel_websocket.dart'; class WebSocketClient extends Angel { WebSocket _socket; Map> _services = {}; + WebSocket get _underlyingSocket => _socket; WebSocketClient(String wsEndpoint) : super(wsEndpoint) { _socket = new WebSocket(wsEndpoint); @@ -67,6 +68,10 @@ class WebSocketClient extends Angel { }); } + void send(String eventName, data) { + _socket.send(JSON.encode({"eventName": eventName, "data": data})); + } + @override Service service(Pattern path, {Type type}) { var service = @@ -159,16 +164,11 @@ class WebSocketService extends Service { } _serialize(WebSocketAction action) { - var data = { - "id": action.id, - "eventName": action.eventName - }; + var data = {"id": action.id, "eventName": action.eventName}; - if (action.data != null) - data["data"] = action.data; + if (action.data != null) data["data"] = action.data; - if (action.params != null) - data["params"] = action.params; + if (action.params != null) data["params"] = action.params; return JSON.encode(data); } diff --git a/lib/cli.dart b/lib/cli.dart index 7802d4db..8d429e3c 100644 --- a/lib/cli.dart +++ b/lib/cli.dart @@ -11,6 +11,7 @@ export 'package:angel_websocket/angel_websocket.dart'; class WebSocketClient extends Angel { WebSocket _socket; Map> _services = {}; + WebSocket get underlyingSocket => _socket; WebSocketClient(String wsEndpoint) : super(wsEndpoint); @@ -70,6 +71,13 @@ class WebSocketClient extends Angel { _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 = diff --git a/lib/server.dart b/lib/server.dart index 5a42b98b..1f309cc5 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -11,8 +11,6 @@ export 'angel_websocket.dart'; part 'websocket_context.dart'; -final AngelWebSocket websocket = new AngelWebSocket("/ws"); - class Realtime { const Realtime(); } @@ -20,9 +18,12 @@ class Realtime { class AngelWebSocket extends AngelPlugin { Angel _app; List _clients = []; + StreamController _onConnection = + new StreamController.broadcast(); List get clients => new List.from(_clients, growable: false); List servicesAlreadyWired = []; String endpoint; + Stream get onConnection => _onConnection.stream; AngelWebSocket(String this.endpoint); @@ -113,6 +114,7 @@ class AngelWebSocket extends AngelPlugin { onData(WebSocketContext socket, data) async { try { + socket._onData.add(data); var fromJson = JSON.decode(data); var action = new WebSocketAction( id: fromJson['id'], @@ -126,11 +128,31 @@ class AngelWebSocket extends AngelPlugin { throw new AngelHttpException.BadRequest(); } - var event = handleAction(action, socket); - if (event is Future) event = await event; + if (fromJson is Map && fromJson.containsKey("eventName")) { + socket._onAll.add(fromJson); + socket.on._getStreamForEvent(fromJson["eventName"].toString()).add(fromJson); + } - if (event is WebSocketEvent) { - batchEvent(event); + if (action.eventName.contains("::")) { + var split = action.eventName.split("::"); + + if (split.length >= 2) { + if ([ + "index", + "read", + "create", + "modify", + "update", + "remove" + ].contains(split[1])) { + var event = handleAction(action, socket); + if (event is Future) event = await event; + + if (event is WebSocketEvent) { + batchEvent(event); + } + } + } } } catch (e) { // Send an error @@ -182,6 +204,7 @@ class AngelWebSocket extends AngelPlugin { var socket = new WebSocketContext(ws, req, res); await onConnect(socket); + _onConnection.add(socket); req.params['socket'] = socket; ws.listen((data) { diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index 82e88b91..8cdb2302 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -1,6 +1,11 @@ part of angel_websocket.server; class WebSocketContext { + StreamController _onAll = new StreamController.broadcast(); + StreamController _onData = new StreamController.broadcast(); + _WebSocketEventTable on = new _WebSocketEventTable(); + Stream get onAll => _onAll.stream; + Stream get onData => _onData.stream; WebSocket underlyingSocket; RequestContext requestContext; ResponseContext responseContext; @@ -15,3 +20,15 @@ class WebSocketContext { sendError(AngelHttpException error) => send("error", error); } + +class _WebSocketEventTable { + Map> _handlers = {}; + + StreamController _getStreamForEvent(eventName) { + if (!_handlers.containsKey(eventName)) + _handlers[eventName] = new StreamController.broadcast(); + return _handlers[eventName]; + } + + Stream operator [](String key) => _getStreamForEvent(key).stream; +} diff --git a/test/server.dart b/test/server.dart index f398198c..26d71334 100644 --- a/test/server.dart +++ b/test/server.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart' as server; import 'package:angel_websocket/cli.dart' as client; @@ -10,7 +11,9 @@ main() { server.Angel app; client.WebSocketClient clientApp; client.WebSocketService clientTodos; + Stream customEventStream; WebSocket socket; + AngelWebSocket webSocket = new AngelWebSocket("/ws"); setUp(() async { app = new server.Angel(); @@ -21,7 +24,18 @@ main() { .service("api/todos") .create(new Todo(text: "Clean your room", when: "now")); - await app.configure(websocket); + 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(startTestServer); socket = await WebSocket.connect(app.properties["ws_url"]); @@ -36,8 +50,8 @@ main() { }); test("find all real-time services", () { - print(websocket.servicesAlreadyWired); - expect(websocket.servicesAlreadyWired, equals(["api/todos"])); + print(webSocket.servicesAlreadyWired); + expect(webSocket.servicesAlreadyWired, equals(["api/todos"])); }); test("index", () async { @@ -67,6 +81,15 @@ main() { 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["eventName"], equals("custom")); + expect(data["data"]["hello"], equals("world")); + }); } @Realtime() From 00b681f04b22337f77bbf8140f937e2ce7891e94 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 22:53:58 -0400 Subject: [PATCH 14/73] Client-side 'on' not working :( --- README.md | 19 ++++--- lib/cli.dart | 89 +++++++++++++++++------------ lib/server.dart | 8 +-- lib/websocket_controller.dart | 103 ++++++++++++++++++++++++++++++++++ test/server.dart | 31 ++++++++-- 5 files changed, 196 insertions(+), 54 deletions(-) create mode 100644 lib/websocket_controller.dart diff --git a/README.md b/README.md index 17474398..5dcef69e 100644 --- a/README.md +++ b/README.md @@ -27,21 +27,24 @@ main() async { **Adding Handlers within a Controller** +`WebSocketController` extends a normal `Controller`, but also listens to WebSockets. + ```dart import 'dart:async'; import "package:angel_framework/angel_framework.dart"; import "package:angel_websocket/server.dart"; @Expose("/") -class MyController extends Controller { +class MyController extends WebSocketController { @override - Future call(AngelBase app) async { - var ws = app.container.make(AngelWebSocket); - ws.onConnection.listen((WebSocketContext socket) { - socket.on["message"].listen((WebSocketEvent e) { - socket.send("new_message", { "text": e.data["text"] }); - }); - }); + void onConnect(WebSocketContext socket) { + // On connect... + } + + // Dependency injection works, too.. + @ExposeWs("read_message") + void sendMessage(WebSocketContext socket, Db db) async { + socket.send("found_message", db.collection("messages").findOne(where.id("..."))); } } ``` diff --git a/lib/cli.dart b/lib/cli.dart index 8d429e3c..ec48b16e 100644 --- a/lib/cli.dart +++ b/lib/cli.dart @@ -12,13 +12,16 @@ class WebSocketClient extends Angel { WebSocket _socket; Map> _services = {}; WebSocket get underlyingSocket => _socket; + _WebSocketEventTable on = new _WebSocketEventTable(); WebSocketClient(String wsEndpoint) : super(wsEndpoint); - onData(data) { + 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]; @@ -30,37 +33,41 @@ class WebSocketClient extends Angel { exc.errors = exc.errors ?? []; exc.errors.addAll(e.data['errors'] ?? []); throw exc; - } else if (services != null) { - e.eventName = split[1]; + } else { + on._getStreamForEvent(serviceName).add(e.data); - 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; + 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; + } } } } @@ -72,10 +79,7 @@ class WebSocketClient extends Angel { } void send(String eventName, data) { - _socket.add(JSON.encode({ - "eventName": eventName, - "data": data - })); + _socket.add(JSON.encode({"eventName": eventName, "data": data})); } @override @@ -112,7 +116,8 @@ class _WebSocketServiceTransformer stream.listen((WebSocketEvent e) { if (_outputType != null && e.eventName != "error") - e.data = god.deserialize(god.serialize(e.data), outputType: _outputType); + e.data = + god.deserialize(god.serialize(e.data), outputType: _outputType); _stream.add(e); }); @@ -205,3 +210,15 @@ class WebSocketService extends Service { eventName: "$_path::remove", id: id, params: params))); } } + +class _WebSocketEventTable { + Map> _handlers = {}; + + StreamController _getStreamForEvent(eventName) { + if (!_handlers.containsKey(eventName)) + _handlers[eventName] = new StreamController.broadcast(); + return _handlers[eventName]; + } + + Stream operator [](String key) => _getStreamForEvent(key).stream; +} diff --git a/lib/server.dart b/lib/server.dart index 1f309cc5..1dd208b7 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -3,6 +3,7 @@ library angel_websocket.server; import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:mirrors'; import 'package:angel_framework/angel_framework.dart'; import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; @@ -10,10 +11,7 @@ import 'angel_websocket.dart'; export 'angel_websocket.dart'; part 'websocket_context.dart'; - -class Realtime { - const Realtime(); -} +part 'websocket_controller.dart'; class AngelWebSocket extends AngelPlugin { Angel _app; @@ -130,7 +128,7 @@ class AngelWebSocket extends AngelPlugin { if (fromJson is Map && fromJson.containsKey("eventName")) { socket._onAll.add(fromJson); - socket.on._getStreamForEvent(fromJson["eventName"].toString()).add(fromJson); + socket.on._getStreamForEvent(fromJson["eventName"].toString()).add(fromJson["data"]); } if (action.eventName.contains("::")) { diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart new file mode 100644 index 00000000..702831d7 --- /dev/null +++ b/lib/websocket_controller.dart @@ -0,0 +1,103 @@ +part of angel_websocket.server; + +class ExposeWs { + final String eventName; + + const ExposeWs(this.eventName); +} + +class WebSocketController extends Controller { + Map _handlers = {}; + Map _handlerSymbols = {}; + InstanceMirror _instanceMirror; + AngelWebSocket ws; + + WebSocketController():super() { + _instanceMirror = reflect(this); + } + + @override + Future call(Angel app) async { + await super.call(app); + + ClassMirror classMirror = reflectClass(this.runtimeType); + classMirror.instanceMembers.forEach((sym, mirror) { + if (mirror.isRegularMethod) { + InstanceMirror exposeMirror = mirror.metadata.firstWhere( + (mirror) => mirror.reflectee is ExposeWs, + orElse: () => null); + + if (exposeMirror != null) { + ExposeWs exposeWs = exposeMirror.reflectee; + _handlers[exposeWs.eventName] = mirror; + _handlerSymbols[exposeWs.eventName] = sym; + } + } + }); + + AngelWebSocket ws = app.container.make(AngelWebSocket); + + ws.onConnection.listen((socket) async { + await onConnect(socket); + + socket.onData.listen(onData); + + socket.onAll.listen((Map data) async { + await onAllEvents(data); + + if (_handlers.containsKey(data["eventName"])) { + var methodMirror = _handlers[data["eventName"]]; + try { + // Load parameters, and execute + List args = []; + + for (int i = 0; i < methodMirror.parameters.length; i++) { + ParameterMirror parameter = methodMirror.parameters[i]; + 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 + if (e is AngelHttpException) + socket.sendError(e); + else + socket.sendError(new AngelHttpException(e)); + } + } + }); + }); + } + + void broadcast(String eventName, data) { + ws.batchEvent(new WebSocketEvent(eventName: eventName, data: data)); + } + + Future onConnect(WebSocketContext socket) async {} + + Future onAllEvents(Map data) async {} + + void onData(data) {} +} diff --git a/test/server.dart b/test/server.dart index 26d71334..79abb9ec 100644 --- a/test/server.dart +++ b/test/server.dart @@ -12,6 +12,7 @@ main() { client.WebSocketClient clientApp; client.WebSocketService clientTodos; Stream customEventStream; + Stream customEventStream2; WebSocket socket; AngelWebSocket webSocket = new AngelWebSocket("/ws"); @@ -36,11 +37,13 @@ main() { 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); }); @@ -61,8 +64,7 @@ main() { String json = await socket.first; print(json); - WebSocketEvent e = - god.deserialize(json, outputType: WebSocketEvent); + WebSocketEvent e = god.deserialize(json, outputType: WebSocketEvent); expect(e.eventName, equals("api/todos::indexed")); expect(e.data[0]["when"], equals("now")); }); @@ -87,10 +89,29 @@ main() { var data = await customEventStream.first; - expect(data["eventName"], equals("custom")); - expect(data["data"]["hello"], equals("world")); + 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"); }); } -@Realtime() 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"}); + } +} From f69d3e474ea08914bdbb50c22e1bbe8da3b26026 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 23:16:51 -0400 Subject: [PATCH 15/73] More changes --- lib/server.dart | 5 +++++ lib/websocket_controller.dart | 4 ++++ pubspec.yaml | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/server.dart b/lib/server.dart index 1dd208b7..a9a2b45a 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -18,10 +18,13 @@ class AngelWebSocket extends AngelPlugin { List _clients = []; StreamController _onConnection = new StreamController.broadcast(); + StreamController _onDisconnect = + new StreamController.broadcast(); List get clients => new List.from(_clients, growable: false); List servicesAlreadyWired = []; String endpoint; Stream get onConnection => _onConnection.stream; + Stream get onDisconnection => _onDisconnect.stream; AngelWebSocket(String this.endpoint); @@ -208,8 +211,10 @@ class AngelWebSocket extends AngelPlugin { ws.listen((data) { onData(socket, data); }, onDone: () { + _onDisconnect.add(socket); _clients.remove(ws); }, onError: (e) { + _onDisconnect.add(socket); _clients.remove(ws); }, cancelOnError: true); }); diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index 702831d7..4ea621c1 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -89,6 +89,8 @@ class WebSocketController extends Controller { } }); }); + + ws.onDisconnection.listen(onDisconnect); } void broadcast(String eventName, data) { @@ -97,6 +99,8 @@ class WebSocketController extends Controller { Future onConnect(WebSocketContext socket) async {} + Future onDisconnect(WebSocketContext socket) async {} + Future onAllEvents(Map data) async {} void onData(data) {} diff --git a/pubspec.yaml b/pubspec.yaml index 6ac23378..63532c92 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_websocket description: WebSocket plugin for Angel -version: 1.0.0-dev+3 +version: 1.0.0-dev+4 author: thosakwe homepage: https://github.com/angel-dart/angel_websocket dependencies: From 57e1516b0e760c1b91c0c4c17b8997cf915c3ebf Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 17 Sep 2016 23:21:22 -0400 Subject: [PATCH 16/73] pubspec author --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 63532c92..7969b5d5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: angel_websocket description: WebSocket plugin for Angel -version: 1.0.0-dev+4 -author: thosakwe +version: 1.0.0-dev+5 +author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: angel_client: ">=1.0.0-dev <2.0.0" From 7957a4beeeb2696e41b30d629a944c14f1871cd2 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Fri, 23 Dec 2016 05:47:21 -0500 Subject: [PATCH 17/73] :) --- lib/base_websocket_client.dart | 133 +++++++++++++++++++++++++++++++++ lib/io.dart | 38 ++++++++++ lib/server.dart | 24 ++++-- lib/websocket_context.dart | 2 +- pubspec.yaml | 16 ++-- test/packages | 1 - 6 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 lib/base_websocket_client.dart create mode 100644 lib/io.dart delete mode 120000 test/packages diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart new file mode 100644 index 00000000..98f45a67 --- /dev/null +++ b/lib/base_websocket_client.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'package:angel_client/angel_client.dart'; +import 'package:http/src/base_client.dart' as http; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:web_socket_channel/status.dart'; +import 'angel_websocket.dart'; +export 'package:angel_client/angel_client.dart'; +import 'package:angel_client/base_angel_client.dart'; + +final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); + +abstract class BaseWebSocketClient extends BaseAngelClient { + WebSocketChannel _socket; + + /// The [WebSocketChannel] underneath this instance. + WebSocketChannel get socket => _socket; + + BaseWebSocketClient(http.BaseClient client, String basePath) + : super(client, basePath); + + Future connect(); + + @override + BaseWebSocketService service(String path, + {Type type, AngelDeserializer deserializer}) { + String uri = path.toString().replaceAll(_straySlashes, ''); + return new BaseWebSocketService(socket, this, uri, + deserializer: deserializer)..listen(); + } +} + +class BaseWebSocketService extends Service { + @override + final Angel app; + final AngelDeserializer deserializer; + final WebSocketChannel socket; + final String uri; + + final StreamController _onMessage = + new StreamController(); + final StreamController _onError = + new StreamController(); + final StreamController _onIndexed = + new StreamController(); + final StreamController _onRead = + new StreamController(); + final StreamController _onCreated = + new StreamController(); + final StreamController _onModified = + new StreamController(); + final StreamController _onUpdated = + new StreamController(); + final StreamController _onRemoved = + new StreamController(); + final WebSocketExtraneousEventHandler _on = + new WebSocketExtraneousEventHandler(); + + /// Use this to handle events that are not standard. + WebSocketExtraneousEventHandler get on => _on; + + /// Fired on all events. + Stream get onMessage => _onMessage.stream; + + /// Fired on errors. + Stream get onError => _onError.stream; + + /// Fired on `index` events. + Stream get onIndexed => _onIndexed.stream; + + /// Fired on `read` events. + Stream get onRead => _onRead.stream; + + /// Fired on `created` events. + Stream get onCreated => _onCreated.stream; + + /// Fired on `modified` events. + Stream get onModified => _onModified.stream; + + /// Fired on `updated` events. + Stream get onUpdated => _onUpdated.stream; + + /// Fired on `removed` events. + Stream get onRemoved => _onRemoved.stream; + + BaseWebSocketService(this.socket, this.app, this.uri, {this.deserializer}); + + void listen() { + socket.stream.listen((message) { + print('Message: ${message.runtimeType}'); + }); + } + + @override + Future index([Map params]) { + // TODO: implement index + } + + @override + Future read(id, [Map params]) { + // TODO: implement read + } + + @override + Future create(data, [Map params]) { + // TODO: implement create + } + + @override + Future modify(id, data, [Map params]) { + // TODO: implement modify + } + + @override + Future update(id, data, [Map params]) { + // TODO: implement update + } + + @override + Future remove(id, [Map params]) { + // TODO: implement remove + } +} + +class WebSocketExtraneousEventHandler { + Map> _events = {}; + + operator [](String index) { + if (_events[index] == null) + _events[index] = new StreamController(); + + return _events[index].stream; + } +} diff --git a/lib/io.dart b/lib/io.dart new file mode 100644 index 00000000..f7becf8c --- /dev/null +++ b/lib/io.dart @@ -0,0 +1,38 @@ +/// Command-line WebSocket client library for the Angel framework. +library angel_client.cli; + +import 'dart:async'; +import 'package:angel_client/angel_client.dart'; +import 'package:http/http.dart' as http; +import 'package:json_god/json_god.dart' as god; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:web_socket_channel/io.dart'; +import 'base_websocket_client.dart'; +export 'package:angel_client/angel_client.dart'; +export 'angel_websocket.dart'; + +final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); + +/// Queries an Angel server via WebSockets. +class WebSockets extends BaseWebSocketClient { + WebSockets(String path) : super(new http.Client(), path); + + @override + Future connect() async { + return new IOWebSocketChannel.connect(basePath); + } + + @override + WebSocketsService service(String path, + {Type type, AngelDeserializer deserializer}) { + String uri = path.replaceAll(_straySlashes, ""); + return new WebSocketsService(socket, this, uri, T != dynamic ? T : type); + } +} + +class WebSocketsService extends BaseWebSocketService { + final Type type; + + WebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type) + : super(socket, app, uri); +} diff --git a/lib/server.dart b/lib/server.dart index a9a2b45a..c1397e01 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -17,13 +17,23 @@ class AngelWebSocket extends AngelPlugin { Angel _app; List _clients = []; StreamController _onConnection = - new StreamController.broadcast(); + new StreamController(); StreamController _onDisconnect = - new StreamController.broadcast(); - List get clients => new List.from(_clients, growable: false); - List servicesAlreadyWired = []; - String endpoint; + new StreamController(); + final List _servicesAlreadyWired = []; + + /// A list of clients currently connected to this server via WebSockets. + List get clients => new List.unmodifiable(_clients); + + /// Services that have already been hooked to fire socket events. + List get servicesAlreadyWired => new List.unmodifiable(_servicesAlreadyWired); + + final String endpoint; + + /// Fired on incoming connections. Stream get onConnection => _onConnection.stream; + + /// Fired when a user disconnects. Stream get onDisconnection => _onDisconnect.stream; AngelWebSocket(String this.endpoint); @@ -108,7 +118,7 @@ class AngelWebSocket extends AngelPlugin { ..afterUpdated.listen(batch) ..afterRemoved.listen(batch); - servicesAlreadyWired.add(path); + _servicesAlreadyWired.add(path); } Future onConnect(WebSocketContext socket) async {} @@ -170,7 +180,7 @@ class AngelWebSocket extends AngelPlugin { wireAllServices(Angel app) { for (Pattern key in app.services.keys.where((x) { - return !servicesAlreadyWired.contains(x) && + return !_servicesAlreadyWired.contains(x) && app.services[x] is HookedService; })) { hookupService(key, app.services[key]); diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index 8cdb2302..0151eda9 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -18,7 +18,7 @@ class WebSocketContext { god.serialize(new WebSocketEvent(eventName: eventName, data: data))); } - sendError(AngelHttpException error) => send("error", error); + sendError(AngelHttpException error) => send("error", error.toJson()); } class _WebSocketEventTable { diff --git a/pubspec.yaml b/pubspec.yaml index 7969b5d5..45326092 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,14 +1,16 @@ name: angel_websocket description: WebSocket plugin for Angel +environment: + sdk: ">=1.19.0" version: 1.0.0-dev+5 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: - angel_client: ">=1.0.0-dev <2.0.0" - angel_framework: ">=1.0.0-dev < 2.0.0" - json_god: ">=2.0.0-beta <3.0.0" - jwt: ">=0.1.4 <1.0.0" - uuid: ">=0.5.3 <1.0.0" + angel_auth: "^1.0.0-dev" + angel_client: "^1.0.0-dev" + angel_framework: "^1.0.0-dev" + uuid: "^0.5.3" + web_socket_channel: "^1.0.0" dev_dependencies: - http: ">= 0.11.3 < 0.12.0" - test: ">= 0.12.13 < 0.13.0" + http: "^0.11.3" + test: "^0.12.15" diff --git a/test/packages b/test/packages deleted file mode 120000 index a16c4050..00000000 --- a/test/packages +++ /dev/null @@ -1 +0,0 @@ -../packages \ No newline at end of file From 7d1f0a9c65e50c8a80289df879f90ec64f8ccc9c Mon Sep 17 00:00:00 2001 From: thosakwe Date: Fri, 23 Dec 2016 15:57:46 -0500 Subject: [PATCH 18/73] Almost --- .anaylsis-options.yaml | 4 + .gitignore | 4 +- .travis.yml | 1 + lib/angel_websocket.dart | 58 ++++++++- lib/base_websocket_client.dart | 228 ++++++++++++++++++++++++++++----- lib/browser.dart | 228 +++++---------------------------- lib/cli.dart | 224 -------------------------------- lib/io.dart | 25 +++- lib/server.dart | 123 +++++++++++------- lib/websocket_context.dart | 40 ++++-- lib/websocket_controller.dart | 81 +++++------- pubspec.yaml | 1 + test/all_tests.dart | 6 - test/common.dart | 20 --- test/controller/common.dart | 17 +++ test/controller/io_test.dart | 62 +++++++++ test/server.dart | 117 ----------------- test/service/browser_test.dart | 5 + test/service/common.dart | 24 ++++ test/service/io_test.dart | 56 ++++++++ 20 files changed, 610 insertions(+), 714 deletions(-) create mode 100644 .anaylsis-options.yaml create mode 100644 .travis.yml delete mode 100644 lib/cli.dart delete mode 100644 test/all_tests.dart delete mode 100644 test/common.dart create mode 100644 test/controller/common.dart create mode 100644 test/controller/io_test.dart delete mode 100644 test/server.dart create mode 100644 test/service/browser_test.dart create mode 100644 test/service/common.dart create mode 100644 test/service/io_test.dart diff --git a/.anaylsis-options.yaml b/.anaylsis-options.yaml new file mode 100644 index 00000000..716de123 --- /dev/null +++ b/.anaylsis-options.yaml @@ -0,0 +1,4 @@ +analyzer: + strong-mode: true + exclude: + - .scripts-bin/**/*.dart \ No newline at end of file diff --git a/.gitignore b/.gitignore index ea89ccf0..4191ae6d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ doc/api/ # Don't commit pubspec lock file # (Library packages only! Remove pattern if developing an application package) pubspec.lock -.idea \ No newline at end of file +.idea + +log.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart index fc058014..03b4bb4b 100644 --- a/lib/angel_websocket.dart +++ b/lib/angel_websocket.dart @@ -1,17 +1,73 @@ +/// WebSocket plugin for Angel. 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 ACTIONS = const [ + ACTION_INDEX, + ACTION_READ, + ACTION_CREATE, + ACTION_MODIFY, + ACTION_UPDATE, + ACTION_REMOVE +]; + +/// The standard Angel service events. +const List 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 { String eventName; var 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 { String id; String eventName; var data; 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}; + } } diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 98f45a67..9cfed9d6 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -1,44 +1,140 @@ import 'dart:async'; +import 'dart:convert'; import 'package:angel_client/angel_client.dart'; import 'package:http/src/base_client.dart' as http; 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'; export 'package:angel_client/angel_client.dart'; import 'package:angel_client/base_angel_client.dart'; final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); +/// An [Angel] client that operates across WebSockets. abstract class BaseWebSocketClient extends BaseAngelClient { WebSocketChannel _socket; + final StreamController _onData = new StreamController(); + final StreamController _onMessage = + new StreamController(); + final StreamController _onError = + new StreamController(); + final StreamController> _onServiceEvent = + new StreamController>.broadcast(); + final StreamController + _onWebSocketChannelException = + new StreamController(); + + /// A broadcast stream of data coming from the [socket]. + /// + /// Mostly just for internal use. + Stream get onData => _onData.stream; + + /// Fired on errors. + Stream get onError => _onError.stream; + + /// Fired on all events. + Stream get onMessage => _onMessage.stream; + + /// Fired whenever an event is fired by a service. + Stream> get onServiceEvent => + _onServiceEvent.stream; + + /// Fired on [WebSocketChannelException]s. + Stream get onWebSocketChannelException => + _onWebSocketChannelException.stream; + /// The [WebSocketChannel] underneath this instance. WebSocketChannel get socket => _socket; BaseWebSocketClient(http.BaseClient client, String basePath) - : super(client, basePath); + : super(client, basePath) {} - Future connect(); + @override + Future close() async => _socket.sink.close(status.goingAway); + + /// Connects the WebSocket. + Future 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 getConnectedWebSocket(); @override BaseWebSocketService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.toString().replaceAll(_straySlashes, ''); 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 { + /// The [BaseWebSocketClient] that spawned this service. @override - final Angel app; - final AngelDeserializer deserializer; - final WebSocketChannel socket; - final String uri; + final BaseWebSocketClient app; - final StreamController _onMessage = - new StreamController(); - final StreamController _onError = + /// Used to deserialize JSON into typed data. + final AngelDeserializer deserializer; + + /// The [WebSocketChannel] to listen to, and send data across. + final WebSocketChannel socket; + + /// The service path to listen to. + final String path; + + final StreamController _onAllEvents = new StreamController(); final StreamController _onIndexed = new StreamController(); @@ -52,17 +148,13 @@ class BaseWebSocketService extends Service { new StreamController(); final StreamController _onRemoved = new StreamController(); - final WebSocketExtraneousEventHandler _on = - new WebSocketExtraneousEventHandler(); /// Use this to handle events that are not standard. - WebSocketExtraneousEventHandler get on => _on; + final WebSocketExtraneousEventHandler on = + new WebSocketExtraneousEventHandler(); /// Fired on all events. - Stream get onMessage => _onMessage.stream; - - /// Fired on errors. - Stream get onError => _onError.stream; + Stream get onAllEvents => _onAllEvents.stream; /// Fired on `index` events. Stream get onIndexed => _onIndexed.stream; @@ -82,48 +174,116 @@ class BaseWebSocketService extends Service { /// Fired on `removed` events. Stream 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() { - socket.stream.listen((message) { - print('Message: ${message.runtimeType}'); + app.onServiceEvent.listen((map) { + 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 - Future index([Map params]) { - // TODO: implement index + /// Sends the given [action] on the [socket]. + void send(WebSocketAction action) { + socket.sink.add(serialize(action)); } @override - Future read(id, [Map params]) { - // TODO: implement read + Future index([Map params]) async { + socket.sink.add(serialize(new WebSocketAction( + eventName: '$path::${ACTION_INDEX}', params: params ?? {}))); + return null; } @override - Future create(data, [Map params]) { - // TODO: implement create + Future read(id, [Map params]) async { + socket.sink + .add(serialize(new WebSocketAction(id: id, params: params ?? {}))); + return null; } @override - Future modify(id, data, [Map params]) { - // TODO: implement modify + Future create(data, [Map params]) async { + socket.sink + .add(serialize(new WebSocketAction(data: data, params: params ?? {}))); + return null; } @override - Future update(id, data, [Map params]) { - // TODO: implement update + Future modify(id, data, [Map params]) async { + socket.sink.add(serialize( + new WebSocketAction(id: id, data: data, params: params ?? {}))); + return null; } @override - Future remove(id, [Map params]) { - // TODO: implement remove + Future update(id, data, [Map params]) async { + 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 { Map> _events = {}; + StreamController _getStream(String index) { + if (_events[index] == null) + _events[index] = new StreamController(); + + return _events[index]; + } + operator [](String index) { if (_events[index] == null) _events[index] = new StreamController(); diff --git a/lib/browser.dart b/lib/browser.dart index 69252e56..cbde5c23 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -1,212 +1,50 @@ +/// Browser WebSocket client library for the Angel framework. +library angel_websocket.browser; + import 'dart:async'; -import 'dart:convert'; import 'dart:html'; 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_websocket/angel_websocket.dart'; +export 'angel_websocket.dart'; -class WebSocketClient extends Angel { - WebSocket _socket; - Map> _services = {}; - WebSocket get _underlyingSocket => _socket; +final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); - WebSocketClient(String wsEndpoint) : super(wsEndpoint) { - _socket = new WebSocket(wsEndpoint); - _connect(); - } +/// Queries an Angel server via WebSockets. +class WebSockets extends BaseWebSocketClient { + 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]; + @override + Future getConnectedWebSocket() { + var socket = new WebSocket(basePath); + var completer = new Completer(); - if (serviceName == "error") { - throw new Exception("Server-side error."); - } else if (services != null) { - e.eventName = split[1]; + socket + ..onOpen.listen((_) { + if (!completer.isCompleted) + return completer.complete(new HtmlWebSocketChannel(socket)); + }) + ..onError.listen((ErrorEvent e) { + if (!completer.isCompleted) return completer.completeError(e.error); + }); - 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})); + return completer.future; } @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; + WebSocketsService service(String path, + {Type type, AngelDeserializer deserializer}) { + String uri = path.replaceAll(_straySlashes, ''); + return new WebSocketsService(socket, this, uri, T != dynamic ? T : type); } } -class WebSocketExtraneousEventHandler { - Map> _events = {}; +class WebSocketsService extends BaseWebSocketService { + final Type type; - operator [](String index) { - if (_events[index] == null) - _events[index] = new StreamController(); - - return _events[index].stream; - } -} - -class _WebSocketServiceTransformer - implements StreamTransformer { - Type _outputType; - - _WebSocketServiceTransformer.base(this._outputType); - - @override - Stream bind(Stream stream) { - var _stream = new StreamController(); - - 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(); - var _onError = new StreamController(); - var _onIndexed = new StreamController(); - var _onRead = new StreamController(); - var _onCreated = new StreamController(); - var _onModified = new StreamController(); - var _onUpdated = new StreamController(); - var _onRemoved = new StreamController(); - - WebSocketExtraneousEventHandler get on => _on; - - Stream get onAllEvents => - _onAllEvents.stream.transform(_transformer); - - Stream get onError => _onError.stream; - - Stream get onIndexed => - _onIndexed.stream.transform(_transformer); - - Stream get onRead => _onRead.stream.transform(_transformer); - - Stream get onCreated => - _onCreated.stream.transform(_transformer); - - Stream get onModified => - _onModified.stream.transform(_transformer); - - Stream get onUpdated => - _onUpdated.stream.transform(_transformer); - - Stream 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 - Future index([Map params]) async { - connection.send(_serialize( - new WebSocketAction(eventName: "$_path::index", params: params))); - return null; - } - - @override - Future read(id, [Map params]) async { - connection.send(_serialize(new WebSocketAction( - eventName: "$_path::read", id: id, params: params))); - } - - @override - Future create(data, [Map params]) async { - 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))); - } + WebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type) + : super(socket, app, uri); } diff --git a/lib/cli.dart b/lib/cli.dart deleted file mode 100644 index ec48b16e..00000000 --- a/lib/cli.dart +++ /dev/null @@ -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> _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> _events = {}; - - operator [](String index) { - if (_events[index] == null) - _events[index] = new StreamController(); - - return _events[index].stream; - } -} - -class _WebSocketServiceTransformer - implements StreamTransformer { - Type _outputType; - - _WebSocketServiceTransformer.base(this._outputType); - - @override - Stream bind(Stream stream) { - var _stream = new StreamController(); - - 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(); - var _onError = new StreamController(); - var _onIndexed = new StreamController(); - var _onRead = new StreamController(); - var _onCreated = new StreamController(); - var _onModified = new StreamController(); - var _onUpdated = new StreamController(); - var _onRemoved = new StreamController(); - - WebSocketExtraneousEventHandler get on => _on; - - Stream get onAllEvents => - _onAllEvents.stream.transform(_transformer); - - Stream get onError => _onError.stream; - - Stream get onIndexed => - _onIndexed.stream.transform(_transformer); - - Stream get onRead => _onRead.stream.transform(_transformer); - - Stream get onCreated => - _onCreated.stream.transform(_transformer); - - Stream get onModified => - _onModified.stream.transform(_transformer); - - Stream get onUpdated => - _onUpdated.stream.transform(_transformer); - - Stream 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 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> _handlers = {}; - - StreamController _getStreamForEvent(eventName) { - if (!_handlers.containsKey(eventName)) - _handlers[eventName] = new StreamController.broadcast(); - return _handlers[eventName]; - } - - Stream operator [](String key) => _getStreamForEvent(key).stream; -} diff --git a/lib/io.dart b/lib/io.dart index f7becf8c..6c111124 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -1,12 +1,14 @@ /// Command-line WebSocket client library for the Angel framework. -library angel_client.cli; +library angel_websocket.io; import 'dart:async'; +import 'dart:io'; import 'package:angel_client/angel_client.dart'; import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart' as god; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/io.dart'; +import 'angel_websocket.dart'; import 'base_websocket_client.dart'; export 'package:angel_client/angel_client.dart'; export 'angel_websocket.dart'; @@ -18,16 +20,20 @@ class WebSockets extends BaseWebSocketClient { WebSockets(String path) : super(new http.Client(), path); @override - Future connect() async { - return new IOWebSocketChannel.connect(basePath); + Future getConnectedWebSocket() async { + var socket = await WebSocket.connect(basePath); + return new IOWebSocketChannel(socket); } @override WebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { - String uri = path.replaceAll(_straySlashes, ""); + String uri = path.replaceAll(_straySlashes, ''); return new WebSocketsService(socket, this, uri, T != dynamic ? T : type); } + + @override + serialize(x) => god.serialize(x); } class WebSocketsService extends BaseWebSocketService { @@ -35,4 +41,15 @@ class WebSocketsService extends BaseWebSocketService { WebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type) : 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); + } } diff --git a/lib/server.dart b/lib/server.dart index c1397e01..f63fcaf5 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -1,3 +1,4 @@ +/// Server-side support for WebSockets. library angel_websocket.server; import 'dart:async'; @@ -13,30 +14,46 @@ export 'angel_websocket.dart'; part 'websocket_context.dart'; part 'websocket_controller.dart'; +/// Broadcasts events from [HookedService]s, and handles incoming [WebSocketAction]s. class AngelWebSocket extends AngelPlugin { Angel _app; List _clients = []; - StreamController _onConnection = - new StreamController(); - StreamController _onDisconnect = - new StreamController(); final List _servicesAlreadyWired = []; + final StreamController _onAction = + new StreamController(); + final StreamController _onData = new StreamController(); + final StreamController _onConnection = + new StreamController.broadcast(); + final StreamController _onDisconnect = + new StreamController.broadcast(); + + /// Include debug information, and send error information across WebSockets. + final bool debug; + /// A list of clients currently connected to this server via WebSockets. List get clients => new List.unmodifiable(_clients); /// Services that have already been hooked to fire socket events. - List get servicesAlreadyWired => new List.unmodifiable(_servicesAlreadyWired); + List get servicesAlreadyWired => + new List.unmodifiable(_servicesAlreadyWired); + /// The endpoint that users should connect a WebSocket to. final String endpoint; + /// Fired on any [WebSocketAction]. + Stream get onAction => _onAction.stream; + + /// Fired whenever a WebSocket sends data. + Stream get onData => _onData.stream; + /// Fired on incoming connections. Stream get onConnection => _onConnection.stream; /// Fired when a user disconnects. Stream get onDisconnection => _onDisconnect.stream; - AngelWebSocket(String this.endpoint); + AngelWebSocket({this.endpoint: '/ws', this.debug: false}); _batchEvent(String path) { return (HookedServiceEvent e) async { @@ -46,6 +63,7 @@ class AngelWebSocket extends AngelPlugin { }; } + /// Slates an event to be dispatched. Future batchEvent(WebSocketEvent event) async { // Default implementation will just immediately fire events _clients.forEach((client) { @@ -53,8 +71,10 @@ class AngelWebSocket extends AngelPlugin { }); } + /// Returns a list of events yet to be sent. Future> getBatchedEvents() async => []; + /// Responds to an incoming action on a WebSocket. Future handleAction(WebSocketAction action, WebSocketContext socket) async { var split = action.eventName.split("::"); @@ -67,7 +87,7 @@ class AngelWebSocket extends AngelPlugin { return socket.sendError(new AngelHttpException.NotFound( message: "No service \"${split[0]}\" exists.")); - var eventName = split[1]; + var actionName = split[1]; var params = mergeMap([ god.deserializeDatum(action.params), @@ -75,39 +95,44 @@ class AngelWebSocket extends AngelPlugin { ]); try { - if (eventName == "index") { - return socket.send("${split[0]}::" + HookedServiceEvent.INDEXED, - await service.index(params)); - } else if (eventName == "read") { - return socket.send("${split[0]}::" + HookedServiceEvent.READ, + if (actionName == ACTION_INDEX) { + return socket.send( + "${split[0]}::" + EVENT_INDEXED, await service.index(params)); + } else if (actionName == ACTION_READ) { + return socket.send("${split[0]}::" + EVENT_READ, await service.read(action.id, params)); - } else if (eventName == "create") { + } else if (actionName == ACTION_CREATE) { return new WebSocketEvent( - eventName: "${split[0]}::" + HookedServiceEvent.CREATED, + eventName: "${split[0]}::" + EVENT_CREATED, data: await service.create(action.data, params)); - } else if (eventName == "modify") { + } else if (actionName == ACTION_MODIFY) { return new WebSocketEvent( - eventName: "${split[0]}::" + HookedServiceEvent.MODIFIED, + eventName: "${split[0]}::" + EVENT_MODIFIED, data: await service.modify(action.id, action.data, params)); - } else if (eventName == "update") { + } else if (actionName == ACTION_UPDATE) { return new WebSocketEvent( - eventName: "${split[0]}::" + HookedServiceEvent.UPDATED, + eventName: "${split[0]}::" + EVENT_UPDATED, data: await service.update(action.id, action.data, params)); - } else if (eventName == "remove") { + } else if (actionName == ACTION_REMOVE) { return new WebSocketEvent( - eventName: "${split[0]}::" + HookedServiceEvent.REMOVED, + eventName: "${split[0]}::" + EVENT_REMOVED, data: await service.remove(action.id, params)); } else { return socket.sendError(new AngelHttpException.MethodNotAllowed( - message: "Method Not Allowed: \"$eventName\"")); + message: "Method Not Allowed: \"$actionName\"")); } - } catch (e) { - if (e is AngelHttpException) return socket.sendError(e); - - return socket.sendError(new AngelHttpException(e)); + } catch (e, st) { + if (e is AngelHttpException) + return 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(); var batch = _batchEvent(path); @@ -121,17 +146,16 @@ class AngelWebSocket extends AngelPlugin { _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 { socket._onData.add(data); var fromJson = JSON.decode(data); - var action = new WebSocketAction( - id: fromJson['id'], - eventName: fromJson['eventName'], - data: fromJson['data'], - params: fromJson['params']); + var action = new WebSocketAction.fromJson(fromJson); + _onAction.add(action); if (action.eventName == null || action.eventName is! String || @@ -140,22 +164,17 @@ class AngelWebSocket extends AngelPlugin { } if (fromJson is Map && fromJson.containsKey("eventName")) { - socket._onAll.add(fromJson); - socket.on._getStreamForEvent(fromJson["eventName"].toString()).add(fromJson["data"]); + socket._onAction.add(new WebSocketAction.fromJson(fromJson)); + socket.on + ._getStreamForEvent(fromJson["eventName"].toString()) + .add(fromJson["data"]); } if (action.eventName.contains("::")) { var split = action.eventName.split("::"); if (split.length >= 2) { - if ([ - "index", - "read", - "create", - "modify", - "update", - "remove" - ].contains(split[1])) { + if (ACTIONS.contains(split[1])) { var event = handleAction(action, socket); if (event is Future) event = await event; @@ -165,19 +184,24 @@ class AngelWebSocket extends AngelPlugin { } } } - } catch (e) { + } 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)); } } + /// Transforms a [HookedServiceEvent], so that it can be broadcasted. Future transformEvent(HookedServiceEvent event) async { return new WebSocketEvent(eventName: event.eventName, data: event.result); } + /// Hooks any [HookedService]s that are not being broadcasted yet. wireAllServices(Angel app) { for (Pattern key in app.services.keys.where((x) { return !_servicesAlreadyWired.contains(x) && @@ -189,7 +213,7 @@ class AngelWebSocket extends AngelPlugin { @override Future call(Angel app) async { - this._app = app..container.singleton(this); + _app = app..container.singleton(this); if (runtimeType != AngelWebSocket) app.container.singleton(this, as: AngelWebSocket); @@ -202,24 +226,25 @@ class AngelWebSocket extends AngelPlugin { }); app.get(endpoint, (RequestContext req, ResponseContext res) async { - if (!WebSocketTransformer.isUpgradeRequest(req.underlyingRequest)) + if (!WebSocketTransformer.isUpgradeRequest(req.io)) throw new AngelHttpException.BadRequest(); res ..willCloseItself = true ..end(); - var ws = await WebSocketTransformer.upgrade(req.underlyingRequest); + var ws = await WebSocketTransformer.upgrade(req.io); _clients.add(ws); var socket = new WebSocketContext(ws, req, res); - await onConnect(socket); + await handleConnect(socket); _onConnection.add(socket); - req.params['socket'] = socket; + req.properties['socket'] = socket; ws.listen((data) { - onData(socket, data); + _onData.add(data); + handleData(socket, data); }, onDone: () { _onDisconnect.add(socket); _clients.remove(ws); diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index 0151eda9..98a6c78a 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -1,24 +1,40 @@ part of angel_websocket.server; +/// Represents a WebSocket session, with the original +/// [RequestContext] and [ResponseContext] attached. class WebSocketContext { - StreamController _onAll = new StreamController.broadcast(); - StreamController _onData = new StreamController.broadcast(); + /// Use this to listen for events. _WebSocketEventTable on = new _WebSocketEventTable(); - Stream get onAll => _onAll.stream; + + /// The underlying [WebSocket] instance. + final WebSocket io; + + /// The original [RequestContext]. + final RequestContext request; + + /// The original [ResponseContext]. + final ResponseContext response; + + StreamController _onAction = + new StreamController(); + StreamController _onData = new StreamController(); + + /// Fired on any [WebSocketAction]; + Stream get onAction => _onAction.stream; + + /// Fired when any data is sent through [io]. Stream get onData => _onData.stream; - WebSocket underlyingSocket; - RequestContext requestContext; - ResponseContext responseContext; - WebSocketContext(WebSocket this.underlyingSocket, - RequestContext this.requestContext, ResponseContext this.responseContext); + WebSocketContext(WebSocket this.io, RequestContext this.request, + ResponseContext this.response); - send(String eventName, data) { - underlyingSocket.add( - god.serialize(new WebSocketEvent(eventName: eventName, data: data))); + /// Sends an arbitrary [WebSocketEvent]; + void send(String eventName, 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 { diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index 4ea621c1..50ebc0e5 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -9,17 +9,27 @@ class ExposeWs { class WebSocketController extends Controller { Map _handlers = {}; Map _handlerSymbols = {}; - InstanceMirror _instanceMirror; AngelWebSocket ws; - WebSocketController():super() { - _instanceMirror = reflect(this); + WebSocketController() : super(); + + 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 Future call(Angel app) async { await super.call(app); + InstanceMirror instanceMirror = reflect(this); ClassMirror classMirror = reflectClass(this.runtimeType); classMirror.instanceMembers.forEach((sym, mirror) { if (mirror.isRegularMethod) { @@ -38,51 +48,32 @@ class WebSocketController extends Controller { AngelWebSocket ws = app.container.make(AngelWebSocket); ws.onConnection.listen((socket) async { + socket.request + ..inject('socket', socket) + ..inject(WebSocketContext, socket); + await onConnect(socket); - socket.onData.listen(onData); + socket.onData.listen((data) => onData(data, socket)); - socket.onAll.listen((Map data) async { - await onAllEvents(data); + socket.onAction.listen((WebSocketAction action) async { + await onAction(action, socket); - if (_handlers.containsKey(data["eventName"])) { - var methodMirror = _handlers[data["eventName"]]; + if (_handlers.containsKey(action.eventName)) { try { - // Load parameters, and execute - List args = []; + var methodMirror = _handlers[action.eventName]; + var fn = instanceMirror.getField(methodMirror.simpleName).reflectee; - for (int i = 0; i < methodMirror.parameters.length; i++) { - ParameterMirror parameter = methodMirror.parameters[i]; - 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) { + return app.runContained(fn, socket.request, socket.response); + } catch (e, st) { // Send an error if (e is AngelHttpException) socket.sendError(e); + else if (ws.debug == true) + socket.sendError(new AngelHttpException(e, + message: e.toString(), + stackTrace: st, + errors: [st.toString()])); else socket.sendError(new AngelHttpException(e)); } @@ -92,16 +83,4 @@ class WebSocketController extends Controller { 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) {} } diff --git a/pubspec.yaml b/pubspec.yaml index 45326092..f1330363 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,5 +12,6 @@ dependencies: uuid: "^0.5.3" web_socket_channel: "^1.0.0" dev_dependencies: + angel_diagnostics: "^1.0.0-dev" http: "^0.11.3" test: "^0.12.15" diff --git a/test/all_tests.dart b/test/all_tests.dart deleted file mode 100644 index b9d49b66..00000000 --- a/test/all_tests.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:test/test.dart'; -import 'server.dart' as server; - -main() async { - group("server", server.main); -} \ No newline at end of file diff --git a/test/common.dart b/test/common.dart deleted file mode 100644 index bf4492f1..00000000 --- a/test/common.dart +++ /dev/null @@ -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"); -} diff --git a/test/controller/common.dart b/test/controller/common.dart new file mode 100644 index 00000000..4739f7d3 --- /dev/null +++ b/test/controller/common.dart @@ -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'); + } +} \ No newline at end of file diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart new file mode 100644 index 00000000..be211bbe --- /dev/null +++ b/test/controller/io_test.dart @@ -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'); + }); + }); +} diff --git a/test/server.dart b/test/server.dart deleted file mode 100644 index 79abb9ec..00000000 --- a/test/server.dart +++ /dev/null @@ -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 customEventStream; - Stream 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()); - 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"}); - } -} diff --git a/test/service/browser_test.dart b/test/service/browser_test.dart new file mode 100644 index 00000000..2a91c878 --- /dev/null +++ b/test/service/browser_test.dart @@ -0,0 +1,5 @@ +import 'package:test/test.dart'; + +main() { + group('service.browser', () {}); +} \ No newline at end of file diff --git a/test/service/common.dart b/test/service/common.dart new file mode 100644 index 00000000..fcd4ff6b --- /dev/null +++ b/test/service/common.dart @@ -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 {} + +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); +} diff --git a/test/service/io_test.dart b/test/service/io_test.dart new file mode 100644 index 00000000..5b8eb12b --- /dev/null +++ b/test/service/io_test.dart @@ -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)); + }); +} From 96f95f4d42863d5d1ad4587b37331a9f4dceaa0c Mon Sep 17 00:00:00 2001 From: thosakwe Date: Fri, 23 Dec 2016 20:45:52 -0500 Subject: [PATCH 19/73] 6 --- README.md | 25 +++++++++++++++++++------ lib/base_websocket_client.dart | 25 ++++++++++++++----------- pubspec.yaml | 2 +- test/controller/common.dart | 17 ++++++++++++++--- test/controller/io_test.dart | 5 +++-- 5 files changed, 51 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5dcef69e..f8999930 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # angel_websocket +[![1.0.0-dev+6](https://img.shields.io/badge/version-1.0.0--dev+6-red.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. This plugin broadcasts events from hooked services via WebSockets. -In addition, -it adds itself to the app's IoC container as `AngelWebSocket`, so that it can be used +In addition, it adds itself to the app's IoC container as `AngelWebSocket`, so that it can be used in controllers as well. -WebSocket contexts are add to `req.params` as `'socket'`. +WebSocket contexts are add to `req.properties` as `'socket'`. # Usage @@ -55,13 +57,22 @@ class MyController extends WebSocketController { import "package:angel_websocket/browser.dart"; main() async { - Angel app = new WebSocketClient("/ws"); + Angel app = new WebSockets("/ws"); + await app.connect(); + var Cars = app.service("api/cars"); Cars.onCreated.listen((e) => print("New car: ${e.data}")); // Happens asynchronously Cars.create({"brand": "Toyota"}); + + // Listen for arbitrary events + app.on['custom_event'].listen((event) { + // For example, this might be sent by a + // WebSocketController. + print('Hi!'); + }); } ``` @@ -69,7 +80,7 @@ main() async { ```dart import "package:angel_framework/angel_framework" as srv; -import "package:angel_websocket/browser.dart"; +import "package:angel_websocket/io.dart"; // You can include these in a shared file and access on both client and server class Car extends srv.Model { @@ -82,9 +93,11 @@ class Car extends srv.Model { } main() async { - Angel app = new WebSocketClient("/ws"); + Angel app = new WebSockets("/ws"); + // Wait for WebSocket connection... await app.connect(); + var Cars = app.service("api/cars", type: Car); Cars.onCreated.listen((e) { diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 9cfed9d6..068aeed9 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -15,7 +15,7 @@ abstract class BaseWebSocketClient extends BaseAngelClient { WebSocketChannel _socket; final StreamController _onData = new StreamController(); - final StreamController _onMessage = + final StreamController _onAllEvents = new StreamController(); final StreamController _onError = new StreamController(); @@ -25,6 +25,13 @@ abstract class BaseWebSocketClient extends BaseAngelClient { _onWebSocketChannelException = new StreamController(); + /// Use this to handle events that are not standard. + final WebSocketExtraneousEventHandler on = + new WebSocketExtraneousEventHandler(); + + /// Fired on all events. + Stream get onAllEvents => _onAllEvents.stream; + /// A broadcast stream of data coming from the [socket]. /// /// Mostly just for internal use. @@ -33,9 +40,6 @@ abstract class BaseWebSocketClient extends BaseAngelClient { /// Fired on errors. Stream get onError => _onError.stream; - /// Fired on all events. - Stream get onMessage => _onMessage.stream; - /// Fired whenever an event is fired by a service. Stream> get onServiceEvent => _onServiceEvent.stream; @@ -85,7 +89,11 @@ abstract class BaseWebSocketClient extends BaseAngelClient { if (json is Map) { var event = new WebSocketEvent.fromJson(json); - _onMessage.add(event); + + if (event.eventName?.isNotEmpty == true) { + _onAllEvents.add(event); + on._getStream(event.eventName).add(event); + } if (event.eventName == EVENT_ERROR) { var error = new AngelHttpException.fromMap(event.data ?? {}); @@ -149,10 +157,6 @@ class BaseWebSocketService extends Service { final StreamController _onRemoved = new StreamController(); - /// Use this to handle events that are not standard. - final WebSocketExtraneousEventHandler on = - new WebSocketExtraneousEventHandler(); - /// Fired on all events. Stream get onAllEvents => _onAllEvents.stream; @@ -199,7 +203,6 @@ class BaseWebSocketService extends Service { var transformed = transformEvent(event); _onAllEvents.add(event); - on._getStream(event.eventName).add(event); switch (event.eventName) { case EVENT_INDEXED: @@ -284,7 +287,7 @@ class WebSocketExtraneousEventHandler { return _events[index]; } - operator [](String index) { + Stream operator [](String index) { if (_events[index] == null) _events[index] = new StreamController(); diff --git a/pubspec.yaml b/pubspec.yaml index f1330363..0390db4f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel environment: sdk: ">=1.19.0" -version: 1.0.0-dev+5 +version: 1.0.0-dev+6 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/controller/common.dart b/test/controller/common.dart index 4739f7d3..d23bbee2 100644 --- a/test/controller/common.dart +++ b/test/controller/common.dart @@ -5,13 +5,24 @@ class Game { final String playerOne, playerTwo; const Game({this.playerOne, this.playerTwo}); + + factory Game.fromJson(Map data) => + new Game(playerOne: data['playerOne'], playerTwo: data['playerTwo']); + + @override + bool operator ==(other) => + other is Game && + other.playerOne == playerOne && + other.playerTwo == playerTwo; } +const Game JOHN_VS_BOB = const Game(playerOne: 'John', playerTwo: 'Bob'); + @Expose('/game') class GameController extends WebSocketController { @ExposeWs('search') search(WebSocketContext socket) async { - print('OMG ok'); - socket.send('searched', 'poop'); + print('User is searching for a game...'); + socket.send('searched', JOHN_VS_BOB); } -} \ No newline at end of file +} diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart index be211bbe..c80a4314 100644 --- a/test/controller/io_test.dart +++ b/test/controller/io_test.dart @@ -55,8 +55,9 @@ main() { group('controller.io', () { test('search', () async { client.send('search', new ws.WebSocketAction()); - var search = await client.onData.first; - print('First: $search'); + var search = await client.on['searched'].first; + print('Searched: ${search.data}'); + expect(new Game.fromJson(search.data), equals(JOHN_VS_BOB)); }); }); } From f69c7869c81f1f2304919b5d1776e48becaebc08 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Fri, 23 Dec 2016 20:54:43 -0500 Subject: [PATCH 20/73] ;) --- .anaylsis-options.yaml => .analysis-options.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .anaylsis-options.yaml => .analysis-options.yaml (100%) diff --git a/.anaylsis-options.yaml b/.analysis-options.yaml similarity index 100% rename from .anaylsis-options.yaml rename to .analysis-options.yaml From 64cf7a653485be0473cf4f6a608df80c4886c5ad Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 28 Jan 2017 16:38:26 -0500 Subject: [PATCH 21/73] +7 --- README.md | 2 +- lib/server.dart | 31 +++++++++++++++++++++++-------- lib/websocket_controller.dart | 1 - pubspec.yaml | 2 +- test/service/io_test.dart | 6 +++--- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f8999930..69cc7b87 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.0-dev+6](https://img.shields.io/badge/version-1.0.0--dev+6-red.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.0-dev+7](https://img.shields.io/badge/version-1.0.0--dev+7-red.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. diff --git a/lib/server.dart b/lib/server.dart index f63fcaf5..b82f2dd2 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -14,6 +14,9 @@ export 'angel_websocket.dart'; part 'websocket_context.dart'; part 'websocket_controller.dart'; +/// Used to assign routes to a given handler. +typedef AngelWebSocketRegisterer(Angel app, RequestHandler handler); + /// Broadcasts events from [HookedService]s, and handles incoming [WebSocketAction]s. class AngelWebSocket extends AngelPlugin { Angel _app; @@ -31,6 +34,9 @@ class AngelWebSocket extends AngelPlugin { /// Include debug information, and send error information across WebSockets. final bool debug; + /// Registers this instance as a route on the server. + final AngelWebSocketRegisterer register; + /// A list of clients currently connected to this server via WebSockets. List get clients => new List.unmodifiable(_clients); @@ -53,7 +59,7 @@ class AngelWebSocket extends AngelPlugin { /// Fired when a user disconnects. Stream get onDisconnection => _onDisconnect.stream; - AngelWebSocket({this.endpoint: '/ws', this.debug: false}); + AngelWebSocket({this.endpoint: '/ws', this.debug: false, this.register}); _batchEvent(String path) { return (HookedServiceEvent e) async { @@ -79,12 +85,12 @@ class AngelWebSocket extends AngelPlugin { var split = action.eventName.split("::"); if (split.length < 2) - return socket.sendError(new AngelHttpException.BadRequest()); + return socket.sendError(new AngelHttpException.badRequest()); var service = _app.service(split[0]); if (service == null) - return socket.sendError(new AngelHttpException.NotFound( + return socket.sendError(new AngelHttpException.notFound( message: "No service \"${split[0]}\" exists.")); var actionName = split[1]; @@ -118,7 +124,7 @@ class AngelWebSocket extends AngelPlugin { eventName: "${split[0]}::" + EVENT_REMOVED, data: await service.remove(action.id, params)); } else { - return socket.sendError(new AngelHttpException.MethodNotAllowed( + return socket.sendError(new AngelHttpException.methodNotAllowed( message: "Method Not Allowed: \"$actionName\"")); } } catch (e, st) { @@ -160,7 +166,7 @@ class AngelWebSocket extends AngelPlugin { if (action.eventName == null || action.eventName is! String || action.eventName.isEmpty) { - throw new AngelHttpException.BadRequest(); + throw new AngelHttpException.badRequest(); } if (fromJson is Map && fromJson.containsKey("eventName")) { @@ -225,9 +231,9 @@ class AngelWebSocket extends AngelPlugin { wireAllServices(app); }); - app.get(endpoint, (RequestContext req, ResponseContext res) async { + handler(RequestContext req, ResponseContext res) async { if (!WebSocketTransformer.isUpgradeRequest(req.io)) - throw new AngelHttpException.BadRequest(); + throw new AngelHttpException.badRequest(); res ..willCloseItself = true @@ -252,6 +258,15 @@ class AngelWebSocket extends AngelPlugin { _onDisconnect.add(socket); _clients.remove(ws); }, cancelOnError: true); - }); + } + + _register() { + if (register != null) + return register(app, handler); + else + app.get(endpoint, handler); + } + + await _register(); } } diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index 50ebc0e5..7b3e1361 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -63,7 +63,6 @@ class WebSocketController extends Controller { try { var methodMirror = _handlers[action.eventName]; var fn = instanceMirror.getField(methodMirror.simpleName).reflectee; - return app.runContained(fn, socket.request, socket.response); } catch (e, st) { // Send an error diff --git a/pubspec.yaml b/pubspec.yaml index 0390db4f..c3132635 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel environment: sdk: ">=1.19.0" -version: 1.0.0-dev+6 +version: 1.0.0-dev+7 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/service/io_test.dart b/test/service/io_test.dart index 5b8eb12b..e3d8f9d7 100644 --- a/test/service/io_test.dart +++ b/test/service/io_test.dart @@ -1,5 +1,5 @@ import 'dart:io'; -import 'package:angel_diagnostics/angel_diagnostics.dart' as srv; +import 'package:angel_diagnostics/angel_diagnostics.dart'; 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; @@ -22,8 +22,8 @@ main() { }); await app.configure(websockets); - server = - await new srv.DiagnosticsServer(app, new File('log.txt')).startServer(); + await app.configure(logRequests(new File('log.txt'))); + server = await app.startServer(); url = 'ws://${server.address.address}:${server.port}/ws'; client = new ws.WebSockets(url); From 23bdb20846d7abdff6bedb023f5ebe0e3791a927 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 29 Jan 2017 15:02:19 -0500 Subject: [PATCH 22/73] 8 --- README.md | 17 ++++++++++++++++- lib/server.dart | 31 ++++++++++++++++++++++--------- lib/websocket_controller.dart | 16 +++++++++++++--- pubspec.yaml | 2 +- test/controller/io_test.dart | 6 +++--- 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 69cc7b87..1247832c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.0-dev+7](https://img.shields.io/badge/version-1.0.0--dev+7-red.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.0-dev+8](https://img.shields.io/badge/pub-1.0.0--dev+8-red.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. @@ -27,6 +27,15 @@ main() async { ``` +Filtering events is easy with services. Just return a `bool`, whether +synchronously or asynchronously. + +```dart +myService.properties['ws:filter'] = (WebSocketContext socket) async { + return true; +} +``` + **Adding Handlers within a Controller** `WebSocketController` extends a normal `Controller`, but also listens to WebSockets. @@ -48,6 +57,12 @@ class MyController extends WebSocketController { void sendMessage(WebSocketContext socket, Db db) async { socket.send("found_message", db.collection("messages").findOne(where.id("..."))); } + + // Event filtering + @ExposeWs("foo") + void foo() { + broadcast(new WebSocketEvent(...), filter: (socket) async => ...); + } } ``` diff --git a/lib/server.dart b/lib/server.dart index b82f2dd2..75323e4d 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -20,7 +20,7 @@ typedef AngelWebSocketRegisterer(Angel app, RequestHandler handler); /// Broadcasts events from [HookedService]s, and handles incoming [WebSocketAction]s. class AngelWebSocket extends AngelPlugin { Angel _app; - List _clients = []; + List _clients = []; final List _servicesAlreadyWired = []; final StreamController _onAction = @@ -38,7 +38,7 @@ class AngelWebSocket extends AngelPlugin { final AngelWebSocketRegisterer register; /// A list of clients currently connected to this server via WebSockets. - List get clients => new List.unmodifiable(_clients); + List get clients => new List.unmodifiable(_clients); /// Services that have already been hooked to fire socket events. List get servicesAlreadyWired => @@ -65,15 +65,26 @@ class AngelWebSocket extends AngelPlugin { return (HookedServiceEvent e) async { var event = await transformEvent(e); event.eventName = "$path::${event.eventName}"; - await batchEvent(event); + + _filter(WebSocketContext socket) { + if (e.service.properties.containsKey('ws:filter')) + return e.service.properties['ws:filter'](socket); + else + return true; + } + + await batchEvent(event, filter: _filter); }; } /// Slates an event to be dispatched. - Future batchEvent(WebSocketEvent event) async { + Future batchEvent(WebSocketEvent event, + {filter(WebSocketContext socket)}) async { // Default implementation will just immediately fire events - _clients.forEach((client) { - client.add(god.serialize(event)); + _clients.forEach((client) async { + var result = true; + if (filter != null) result = await filter(client); + if (result == true) client.io.add(god.serialize(event)); }); } @@ -240,13 +251,15 @@ class AngelWebSocket extends AngelPlugin { ..end(); var ws = await WebSocketTransformer.upgrade(req.io); - _clients.add(ws); - var socket = new WebSocketContext(ws, req, res); + _clients.add(socket); await handleConnect(socket); _onConnection.add(socket); - req.properties['socket'] = socket; + + req + ..properties['socket'] = socket + ..inject(WebSocketContext, socket); ws.listen((data) { _onData.add(data); diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index 7b3e1361..eb0fbc75 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -1,28 +1,38 @@ part of angel_websocket.server; +/// Marks a method as available to WebSockets. class ExposeWs { final String eventName; const ExposeWs(this.eventName); } +/// A special controller that also supports WebSockets. class WebSocketController extends Controller { Map _handlers = {}; Map _handlerSymbols = {}; - AngelWebSocket ws; + + /// The plug-in instance powering this controller. + AngelWebSocket plugin; WebSocketController() : super(); - void broadcast(String eventName, data) { - ws.batchEvent(new WebSocketEvent(eventName: eventName, data: data)); + /// Sends an event to all clients. + void broadcast(String eventName, data, {filter(WebSocketContext socket)}) { + plugin.batchEvent(new WebSocketEvent(eventName: eventName, data: data), + filter: filter); } + /// Fired on new connections. onConnect(WebSocketContext socket) {} + /// Fired on disconnections. onDisconnect(WebSocketContext socket) {} + /// Fired on all incoming actions. onAction(WebSocketAction action, WebSocketContext socket) async {} + /// Fired on arbitrary incoming data. onData(data, WebSocketContext socket) {} @override diff --git a/pubspec.yaml b/pubspec.yaml index c3132635..d979743c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel environment: sdk: ">=1.19.0" -version: 1.0.0-dev+7 +version: 1.0.0-dev+8 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart index c80a4314..505c2582 100644 --- a/test/controller/io_test.dart +++ b/test/controller/io_test.dart @@ -1,5 +1,5 @@ import 'dart:io'; -import 'package:angel_diagnostics/angel_diagnostics.dart' as srv; +import 'package:angel_diagnostics/angel_diagnostics.dart'; 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; @@ -23,9 +23,9 @@ main() { await app.configure(websockets); await app.configure(new GameController()); + await app.configure(logRequests(new File('log.txt'))); - server = - await new srv.DiagnosticsServer(app, new File('log.txt')).startServer(); + server = await app.startServer(); url = 'ws://${server.address.address}:${server.port}/ws'; client = new ws.WebSockets(url); From c460d26a7a553f631b3208a9518c241826042ef2 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 11 Feb 2017 22:06:40 -0500 Subject: [PATCH 23/73] 9 --- README.md | 2 +- lib/browser.dart | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1247832c..99a824ed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.0-dev+8](https://img.shields.io/badge/pub-1.0.0--dev+8-red.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.0-dev+9](https://img.shields.io/badge/pub-1.0.0--dev+9-red.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. diff --git a/lib/browser.dart b/lib/browser.dart index cbde5c23..9533a745 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -4,7 +4,7 @@ library angel_websocket.browser; import 'dart:async'; import 'dart:html'; import 'package:angel_client/angel_client.dart'; -import 'package:http/http.dart' as http; +import 'package:http/browser_client.dart' as http; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/html.dart'; import 'base_websocket_client.dart'; @@ -15,7 +15,7 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); /// Queries an Angel server via WebSockets. class WebSockets extends BaseWebSocketClient { - WebSockets(String path) : super(new http.Client(), path); + WebSockets(String path) : super(new http.BrowserClient(), path); @override Future getConnectedWebSocket() { diff --git a/pubspec.yaml b/pubspec.yaml index d979743c..f1e160a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel environment: sdk: ">=1.19.0" -version: 1.0.0-dev+8 +version: 1.0.0-dev+9 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From ce2dda5e08e8657844d092b5918347fc2a42c130 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 11 Feb 2017 22:12:20 -0500 Subject: [PATCH 24/73] 10 --- README.md | 2 +- lib/base_websocket_client.dart | 28 ++++++++++++++++++---------- lib/browser.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 99a824ed..a8622c1a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.0-dev+9](https://img.shields.io/badge/pub-1.0.0--dev+9-red.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.0-dev+10](https://img.shields.io/badge/pub-1.0.0--dev+10-red.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. diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 068aeed9..23dc9f1f 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -242,36 +242,44 @@ class BaseWebSocketService extends Service { @override Future read(id, [Map params]) async { - socket.sink - .add(serialize(new WebSocketAction(id: id, params: params ?? {}))); + socket.sink.add(serialize(new WebSocketAction( + eventName: '$path::${ACTION_READ}', id: id, params: params ?? {}))); return null; } @override Future create(data, [Map params]) async { - socket.sink - .add(serialize(new WebSocketAction(data: data, params: params ?? {}))); + socket.sink.add(serialize(new WebSocketAction( + eventName: '$path::${ACTION_CREATE}', + data: data, + params: params ?? {}))); return null; } @override Future modify(id, data, [Map params]) async { - socket.sink.add(serialize( - new WebSocketAction(id: id, data: data, params: params ?? {}))); + socket.sink.add(serialize(new WebSocketAction( + eventName: '$path::${ACTION_MODIFY}', + id: id, + data: data, + params: params ?? {}))); return null; } @override Future update(id, data, [Map params]) async { - socket.sink.add(serialize( - new WebSocketAction(id: id, data: data, params: params ?? {}))); + socket.sink.add(serialize(new WebSocketAction( + eventName: '$path::${ACTION_UPDATE}', + 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 ?? {}))); + socket.sink.add(serialize(new WebSocketAction( + eventName: '$path::${ACTION_REMOVE}', id: id, params: params ?? {}))); return null; } } diff --git a/lib/browser.dart b/lib/browser.dart index 9533a745..32cd6581 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -38,7 +38,7 @@ class WebSockets extends BaseWebSocketClient { WebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.replaceAll(_straySlashes, ''); - return new WebSocketsService(socket, this, uri, T != dynamic ? T : type); + return new WebSocketsService(socket, this, uri, null); } } diff --git a/pubspec.yaml b/pubspec.yaml index f1e160a2..6bcc7358 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel environment: sdk: ">=1.19.0" -version: 1.0.0-dev+9 +version: 1.0.0-dev+10 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From fc137dac3179c29ec5c26c937b174356936abfb8 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 12 Feb 2017 00:05:57 -0500 Subject: [PATCH 25/73] Progress --- lib/server.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/server.dart b/lib/server.dart index 75323e4d..cb5fc37b 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -77,6 +77,10 @@ class AngelWebSocket extends AngelPlugin { }; } + void _printDebug(String msg) { + if (debug == true) print(msg); + } + /// Slates an event to be dispatched. Future batchEvent(WebSocketEvent event, {filter(WebSocketContext socket)}) async { @@ -84,7 +88,12 @@ class AngelWebSocket extends AngelPlugin { _clients.forEach((client) async { var result = true; if (filter != null) result = await filter(client); - if (result == true) client.io.add(god.serialize(event)); + if (result == true) { + var serialized = event.toJson(); + _printDebug('Batching this event: $serialized'); + print('Serialized: ' + JSON.encode(serialized)); + client.io.add(god.serialize(event.toJson())); + } }); } From cfae7cf99f552567801f1fd05c0aa7ee0f09ea9f Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 12 Feb 2017 15:06:54 -0500 Subject: [PATCH 26/73] 1.0.0 --- README.md | 2 +- lib/base_websocket_client.dart | 2 +- lib/browser.dart | 7 ++++--- lib/server.dart | 8 ++++++-- pubspec.yaml | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a8622c1a..150cb6ee 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.0-dev+10](https://img.shields.io/badge/pub-1.0.0--dev+10-red.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.0](https://img.shields.io/badge/pub-1.0.0-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. diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 23dc9f1f..351deeb4 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -234,7 +234,7 @@ class BaseWebSocketService extends Service { } @override - Future index([Map params]) async { + Future index([Map params]) async { socket.sink.add(serialize(new WebSocketAction( eventName: '$path::${ACTION_INDEX}', params: params ?? {}))); return null; diff --git a/lib/browser.dart b/lib/browser.dart index 32cd6581..8affcc95 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -38,13 +38,14 @@ class WebSockets extends BaseWebSocketClient { WebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.replaceAll(_straySlashes, ''); - return new WebSocketsService(socket, this, uri, null); + return new WebSocketsService(socket, this, uri, deserializer: deserializer); } } class WebSocketsService extends BaseWebSocketService { final Type type; - WebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type) - : super(socket, app, uri); + WebSocketsService(WebSocketChannel socket, Angel app, String uri, + {this.type, AngelDeserializer deserializer}) + : super(socket, app, uri, deserializer: deserializer); } diff --git a/lib/server.dart b/lib/server.dart index cb5fc37b..032b1c1f 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -91,7 +91,7 @@ class AngelWebSocket extends AngelPlugin { if (result == true) { var serialized = event.toJson(); _printDebug('Batching this event: $serialized'); - print('Serialized: ' + JSON.encode(serialized)); + // print('Serialized: ' + JSON.encode(serialized)); client.io.add(god.serialize(event.toJson())); } }); @@ -117,7 +117,11 @@ class AngelWebSocket extends AngelPlugin { var params = mergeMap([ god.deserializeDatum(action.params), - {"provider": Providers.WEBSOCKET} + { + "provider": Providers.WEBSOCKET, + '__requestctx': socket.request, + '__responsectx': socket.response + } ]); try { diff --git a/pubspec.yaml b/pubspec.yaml index 6bcc7358..b92a8a6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel environment: sdk: ">=1.19.0" -version: 1.0.0-dev+10 +version: 1.0.0 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From fe1f85bfa85a3e57546589549790a08eceb42c8d Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 22 Feb 2017 17:34:35 -0500 Subject: [PATCH 27/73] 1.0.1 --- README.md | 6 ++++-- lib/server.dart | 29 ++++++++++++++++++++++------- pubspec.yaml | 4 ++-- test/service/common.dart | 4 ++-- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 150cb6ee..fad0b8ef 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.0](https://img.shields.io/badge/pub-1.0.0-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.1](https://img.shields.io/badge/pub-1.0.1-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. @@ -22,7 +22,9 @@ import "package:angel_websocket/server.dart"; main() async { var app = new Angel(); - await app.configure(new AngelWebSocket("/ws")); + + // Ensure this runs after all our services are in-place + app.justBeforeStart.add(new AngelWebSocket("/ws")); } ``` diff --git a/lib/server.dart b/lib/server.dart index 032b1c1f..ee5f11e2 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -31,6 +31,10 @@ class AngelWebSocket extends AngelPlugin { final StreamController _onDisconnect = new StreamController.broadcast(); + /// If this is not `true`, then all client-side service parameters will be + /// discarded, other than `params['query']`. + final bool allowClientParams; + /// Include debug information, and send error information across WebSockets. final bool debug; @@ -59,10 +63,16 @@ class AngelWebSocket extends AngelPlugin { /// Fired when a user disconnects. Stream get onDisconnection => _onDisconnect.stream; - AngelWebSocket({this.endpoint: '/ws', this.debug: false, this.register}); + AngelWebSocket( + {this.endpoint: '/ws', + this.debug: false, + this.allowClientParams: false, + this.register}); - _batchEvent(String path) { + serviceHook(String path) { return (HookedServiceEvent e) async { + if (e.params != null && e.params['broadcast'] == false) return; + var event = await transformEvent(e); event.eventName = "$path::${event.eventName}"; @@ -115,6 +125,15 @@ class AngelWebSocket extends AngelPlugin { var actionName = split[1]; + if (action.params is! Map) action.params = {}; + + if (allowClientParams != true) { + if (action.params['query'] is Map) + action.params = {'query': action.params['query']}; + else + action.params = {}; + } + var params = mergeMap([ god.deserializeDatum(action.params), { @@ -165,7 +184,7 @@ class AngelWebSocket extends AngelPlugin { /// Hooks a service up to have its events broadcasted. hookupService(Pattern _path, HookedService service) { String path = _path.toString(); - var batch = _batchEvent(path); + var batch = serviceHook(path); service ..afterCreated.listen(batch) @@ -207,10 +226,6 @@ class AngelWebSocket extends AngelPlugin { if (ACTIONS.contains(split[1])) { var event = handleAction(action, socket); if (event is Future) event = await event; - - if (event is WebSocketEvent) { - batchEvent(event); - } } } } diff --git a/pubspec.yaml b/pubspec.yaml index b92a8a6c..e0f2ca3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: angel_websocket -description: WebSocket plugin for Angel +description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.0 +version: 1.0.1 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/service/common.dart b/test/service/common.dart index fcd4ff6b..e71f30b5 100644 --- a/test/service/common.dart +++ b/test/service/common.dart @@ -1,9 +1,9 @@ import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/src/defs.dart'; +import 'package:angel_framework/common.dart'; import 'package:angel_websocket/base_websocket_client.dart'; import 'package:test/test.dart'; -class Todo extends MemoryModel { +class Todo extends Model { String text; String when; From 01db4aa9eb406429ef058e91265472347ea1705c Mon Sep 17 00:00:00 2001 From: thosakwe Date: Tue, 28 Feb 2017 09:15:34 -0500 Subject: [PATCH 28/73] 1.0.2 --- README.md | 8 +- lib/angel_websocket.dart | 2 + lib/base_websocket_client.dart | 149 +++++++++++++++++++++++++-------- lib/server.dart | 61 +++++++++++++- lib/websocket_context.dart | 17 +++- pubspec.yaml | 4 +- test/controller/io_test.dart | 2 +- test/service/common.dart | 4 +- 8 files changed, 205 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index fad0b8ef..2373bcf5 100644 --- a/README.md +++ b/README.md @@ -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(''); + // 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(''); } ``` diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart index 03b4bb4b..9b47796c 100644 --- a/lib/angel_websocket.dart +++ b/lib/angel_websocket.dart @@ -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'; diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 351deeb4..6d0eb7f3 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -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 _onAllEvents = new StreamController(); + final StreamController _onAuthenticated = + new StreamController(); final StreamController _onError = new StreamController(); final StreamController> _onServiceEvent = @@ -32,6 +35,9 @@ abstract class BaseWebSocketClient extends BaseAngelClient { /// Fired on all events. Stream get onAllEvents => _onAllEvents.stream; + /// Fired whenever a WebSocket is successfully authenticated. + Stream get onAuthenticated => _onAuthenticated.stream; + /// A broadcast stream of data coming from the [socket]. /// /// Mostly just for internal use. @@ -51,17 +57,66 @@ 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 connect() async { - _socket = await getConnectedWebSocket(); - listen(); - return _socket; + /// Connects the WebSocket. [timeout] is optional. + Future connect({Duration timeout}) async { + if (timeout != null) { + var c = new Completer(); + 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,39 +134,60 @@ abstract class BaseWebSocketClient extends BaseAngelClient { /// Starts listening for data. void listen() { - _socket.stream.listen((data) { - _onData.add(data); + _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 (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); + if (json is Map) { + var event = new WebSocketEvent.fromJson(json); - if (event.eventName?.isNotEmpty == true) { - _onAllEvents.add(event); - on._getStream(event.eventName).add(event); - } + if (event.eventName?.isNotEmpty == true) { + _onAllEvents.add(event); + on._getStream(event.eventName).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 (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("::") + .where((str) => str.isNotEmpty) + .toList(); - if (split.length >= 2) { - var serviceName = split[0], eventName = split[1]; - _onServiceEvent.add({serviceName: event..eventName = eventName}); + if (split.length >= 2) { + var serviceName = split[0], eventName = split[1]; + _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(); + }); + } + }); } /// Serializes data to JSON. @@ -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. diff --git a/lib/server.dart b/lib/server.dart index ee5f11e2..132c8edb 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -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 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); + 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 get stream; + void notifyOthers(WebSocketEvent e); +} diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index 98a6c78a..7c97689f 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -17,17 +17,32 @@ class WebSocketContext { StreamController _onAction = new StreamController(); + + StreamController _onClose = new StreamController(); + StreamController _onData = new StreamController(); /// Fired on any [WebSocketAction]; Stream get onAction => _onAction.stream; + /// Fired once the underlying [WebSocket] closes. + Stream 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 _getStreamForEvent(eventName) { if (!_handlers.containsKey(eventName)) - _handlers[eventName] = new StreamController.broadcast(); + _handlers[eventName] = new StreamController(); return _handlers[eventName]; } diff --git a/pubspec.yaml b/pubspec.yaml index e0f2ca3c..5f18af65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 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" diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart index 505c2582..0be6243a 100644 --- a/test/controller/io_test.dart +++ b/test/controller/io_test.dart @@ -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) { diff --git a/test/service/common.dart b/test/service/common.dart index e71f30b5..250e94f9 100644 --- a/test/service/common.dart +++ b/test/service/common.dart @@ -10,7 +10,9 @@ class Todo extends Model { Todo({String this.text, String this.when}); } -class TodoService extends MemoryService {} +class TodoService extends TypedService { + TodoService() : super(new MapService()); +} testIndex(BaseWebSocketClient client) async { var Todos = client.service('api/todos'); From e42528ef421ac7dd58399751623d5c625805e029 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Wed, 1 Mar 2017 22:54:58 -0500 Subject: [PATCH 29/73] 1.0.3 --- README.md | 2 +- lib/base_websocket_client.dart | 10 ++++++++++ lib/browser.dart | 36 ++++++++++++++++++++++++++++++++++ lib/io.dart | 17 ++++++++++++++++ pubspec.yaml | 2 +- 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2373bcf5..5280a65e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.2](https://img.shields.io/badge/pub-1.0.2-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.3](https://img.shields.io/badge/pub-1.0.3-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. diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 6d0eb7f3..e420160d 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -263,6 +263,16 @@ class BaseWebSocketService extends Service { listen(); } + Future close() async { + _onAllEvents.close(); + _onCreated.close(); + _onIndexed.close(); + _onModified.close(); + _onRead.close(); + _onRemoved.close(); + _onUpdated.close(); + } + /// Serializes an [action] to be sent over a WebSocket. serialize(WebSocketAction action) => JSON.encode(action); diff --git a/lib/browser.dart b/lib/browser.dart index 8affcc95..2cec58d4 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -15,8 +15,44 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); /// Queries an Angel server via WebSockets. class WebSockets extends BaseWebSocketClient { + final List _services = []; + WebSockets(String path) : super(new http.BrowserClient(), path); + @override + Future close() { + for (var service in _services) { + service.close(); + } + + return super.close(); + } + + @override + Stream authenticateViaPopup(String url, + {String eventName: 'token', String errorMessage}) { + var ctrl = new StreamController(); + var wnd = window.open(url, 'angel_client_auth_popup'); + + wnd + ..on['beforeunload'].listen((_) { + if (!ctrl.isClosed) { + ctrl.addError(new AngelHttpException.notAuthenticated( + message: + errorMessage ?? 'Authentication via popup window failed.')); + ctrl.close(); + } + }) + ..on[eventName ?? 'token'].listen((CustomEvent e) { + if (!ctrl.isClosed) { + ctrl.add(e.detail); + ctrl.close(); + } + }); + + return ctrl.stream; + } + @override Future getConnectedWebSocket() { var socket = new WebSocket(basePath); diff --git a/lib/io.dart b/lib/io.dart index 6c111124..428b204b 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -17,8 +17,25 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); /// Queries an Angel server via WebSockets. class WebSockets extends BaseWebSocketClient { + final List _services = []; + WebSockets(String path) : super(new http.Client(), path); + @override + Stream authenticateViaPopup(String url, {String eventName: 'token'}) { + throw new UnimplementedError( + 'Opening popup windows is not supported in the `dart:io` client.'); + } + + @override + Future close() { + for (var service in _services) { + service.close(); + } + + return super.close(); + } + @override Future getConnectedWebSocket() async { var socket = await WebSocket.connect(basePath); diff --git a/pubspec.yaml b/pubspec.yaml index 5f18af65..d6b2dd65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.2 +version: 1.0.3 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From 019989e3b1d597f99edc33f04a710cf19ad89e3a Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 9 Apr 2017 21:45:45 -0400 Subject: [PATCH 30/73] 1.0.4 --- README.md | 6 +++--- lib/browser.dart | 2 +- lib/hooks.dart | 8 ++++++++ lib/io.dart | 5 ++++- lib/server.dart | 11 ++--------- pubspec.yaml | 2 +- test/service/common.dart | 8 +++++++- 7 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 lib/hooks.dart diff --git a/README.md b/README.md index 5280a65e..0f28fc14 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.3](https://img.shields.io/badge/pub-1.0.3-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.4](https://img.shields.io/badge/pub-1.0.4-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. @@ -29,11 +29,11 @@ main() async { ``` -Filtering events is easy with services. Just return a `bool`, whether +Filtering events is easy with hooked services. Just return a `bool`, whether synchronously or asynchronously. ```dart -myService.properties['ws:filter'] = (WebSocketContext socket) async { +myService.properties['ws:filter'] = (HookedServiceEvent e, WebSocketContext socket) async { return true; } ``` diff --git a/lib/browser.dart b/lib/browser.dart index 2cec58d4..e0527938 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -55,7 +55,7 @@ class WebSockets extends BaseWebSocketClient { @override Future getConnectedWebSocket() { - var socket = new WebSocket(basePath); + var socket = new WebSocket(authToken?.isNotEmpty == true ? basePath : '$basePath?token=$authToken'); var completer = new Completer(); socket diff --git a/lib/hooks.dart b/lib/hooks.dart new file mode 100644 index 00000000..961d547a --- /dev/null +++ b/lib/hooks.dart @@ -0,0 +1,8 @@ +import 'package:angel_framework/angel_framework.dart'; + +/// Prevents a WebSocket event from being broadcasted, to any client. +HookedServiceEventListener doNotBroadcast() { + return (HookedServiceEvent e) { + if (e.params != null) e.params['broadcast'] = false; + }; +} diff --git a/lib/io.dart b/lib/io.dart index 428b204b..254e76fd 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -38,7 +38,10 @@ class WebSockets extends BaseWebSocketClient { @override Future getConnectedWebSocket() async { - var socket = await WebSocket.connect(basePath); + var socket = await WebSocket.connect(basePath, + headers: authToken?.isNotEmpty == true + ? {'Authorization': 'Bearer $authToken'} + : {}); return new IOWebSocketChannel(socket); } diff --git a/lib/server.dart b/lib/server.dart index 132c8edb..49da7ce4 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -87,7 +87,7 @@ class AngelWebSocket extends AngelPlugin { _filter(WebSocketContext socket) { if (e.service.properties.containsKey('ws:filter')) - return e.service.properties['ws:filter'](socket); + return e.service.properties['ws:filter'](e, socket); else return true; } @@ -230,14 +230,7 @@ class AngelWebSocket extends AngelPlugin { /// Hooks a service up to have its events broadcasted. hookupService(Pattern _path, HookedService service) { String path = _path.toString(); - var batch = serviceHook(path); - - service - ..afterCreated.listen(batch) - ..afterModified.listen(batch) - ..afterUpdated.listen(batch) - ..afterRemoved.listen(batch); - + service.afterAll(serviceHook(path)); _servicesAlreadyWired.add(path); } diff --git a/pubspec.yaml b/pubspec.yaml index d6b2dd65..34518ce1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.3 +version: 1.0.4 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/service/common.dart b/test/service/common.dart index 250e94f9..1abca933 100644 --- a/test/service/common.dart +++ b/test/service/common.dart @@ -1,6 +1,7 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/common.dart'; import 'package:angel_websocket/base_websocket_client.dart'; +import 'package:angel_websocket/server.dart'; import 'package:test/test.dart'; class Todo extends Model { @@ -11,7 +12,12 @@ class Todo extends Model { } class TodoService extends TypedService { - TodoService() : super(new MapService()); + TodoService() : super(new MapService()) { + properties['ws:filter'] = (HookedServiceEvent e, WebSocketContext socket) { + print('Hello, service filter world!'); + return true; + }; + } } testIndex(BaseWebSocketClient client) async { From ab7526e6241a63ec0ed52b5fde2414e7a1dbf505 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 9 Apr 2017 21:50:35 -0400 Subject: [PATCH 31/73] 1.0.4+1 --- README.md | 2 +- lib/hooks.dart | 25 ++++++++++++++++++++++--- pubspec.yaml | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0f28fc14..f4083945 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.4](https://img.shields.io/badge/pub-1.0.4-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.4+1](https://img.shields.io/badge/pub-1.0.4+1-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. diff --git a/lib/hooks.dart b/lib/hooks.dart index 961d547a..32dea957 100644 --- a/lib/hooks.dart +++ b/lib/hooks.dart @@ -1,8 +1,27 @@ import 'package:angel_framework/angel_framework.dart'; -/// Prevents a WebSocket event from being broadcasted, to any client. -HookedServiceEventListener doNotBroadcast() { +/// Prevents a WebSocket event from being broadcasted, to any client from the given [provider]. +/// +/// [provider] can be a String, a [Provider], or an Iterable. +HookedServiceEventListener doNotBroadcast(provider) { return (HookedServiceEvent e) { - if (e.params != null) e.params['broadcast'] = false; + if (e.params != null && e.params.containsKey('provider')) { + bool deny = false; + Iterable providers = provider is Iterable ? provider : [provider]; + + for (var p in providers) { + if (deny) break; + + if (p is Providers) { + deny = deny || + p == e.params['provider'] || + e.params['provider'] == p.via; + } else + deny = + deny || (e.params['provider'] as Providers).via == p.toString(); + } + + e.params['broadcast'] = false; + } }; } diff --git a/pubspec.yaml b/pubspec.yaml index 34518ce1..0dc79adc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.4 +version: 1.0.4+1 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From ac94a2db9c6df219315b965c76356fe776b13b25 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 15 Apr 2017 15:02:17 -0400 Subject: [PATCH 32/73] Fixed --- .gitignore | 47 ++++++++++++++++++++++++++++++++++- README.md | 2 +- lib/browser.dart | 2 +- lib/websocket_controller.dart | 1 + pubspec.yaml | 2 +- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 4191ae6d..1403b417 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,49 @@ doc/api/ pubspec.lock .idea -log.txt \ No newline at end of file +log.txt +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + diff --git a/README.md b/README.md index f4083945..245e8eca 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.4+1](https://img.shields.io/badge/pub-1.0.4+1-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.4+2](https://img.shields.io/badge/pub-1.0.4+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. diff --git a/lib/browser.dart b/lib/browser.dart index e0527938..b7ab6910 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -55,7 +55,7 @@ class WebSockets extends BaseWebSocketClient { @override Future getConnectedWebSocket() { - var socket = new WebSocket(authToken?.isNotEmpty == true ? basePath : '$basePath?token=$authToken'); + var socket = new WebSocket(authToken?.isNotEmpty == true ? '$basePath?token=$authToken' : basePath ); var completer = new Completer(); socket diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index eb0fbc75..c0d3db12 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -67,6 +67,7 @@ class WebSocketController extends Controller { socket.onData.listen((data) => onData(data, socket)); socket.onAction.listen((WebSocketAction action) async { + socket.request.inject(WebSocketAction, action); await onAction(action, socket); if (_handlers.containsKey(action.eventName)) { diff --git a/pubspec.yaml b/pubspec.yaml index 0dc79adc..0f4f4bf8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.4+1 +version: 1.0.4+2 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From 7120dafc0c853babb743a9f034198f3005adc118 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 15 Apr 2017 15:03:56 -0400 Subject: [PATCH 33/73] pubspec --- pubspec.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0f4f4bf8..a55dca17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,9 +9,11 @@ dependencies: angel_auth: "^1.0.0-dev" angel_client: "^1.0.0" angel_framework: "^1.0.0-dev" + http: "^0.11.3" + json_god: ^2.0.0-beta + merge_map: ^1.0.0 uuid: "^0.5.3" web_socket_channel: "^1.0.0" dev_dependencies: angel_diagnostics: "^1.0.0-dev" - http: "^0.11.3" test: "^0.12.15" From 6e1c1f4191a9db3edecf3ca5305fb0b80b8f20af Mon Sep 17 00:00:00 2001 From: thosakwe Date: Mon, 17 Apr 2017 07:03:42 -0400 Subject: [PATCH 34/73] Fixes --- README.md | 2 +- lib/base_websocket_client.dart | 40 ++++++++++++++++++++++------------ pubspec.yaml | 2 +- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 245e8eca..7aec6abb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.4+2](https://img.shields.io/badge/pub-1.0.4+2-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.4+3](https://img.shields.io/badge/pub-1.0.4+3-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. diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index e420160d..18081022 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'dart:convert'; import 'package:angel_client/angel_client.dart'; import 'package:http/src/base_client.dart' as http; @@ -14,6 +15,7 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); abstract class BaseWebSocketClient extends BaseAngelClient { Duration _reconnectInterval; WebSocketChannel _socket; + final Queue _queue = new Queue(); final StreamController _onData = new StreamController(); final StreamController _onAllEvents = @@ -99,6 +101,12 @@ abstract class BaseWebSocketClient extends BaseAngelClient { getConnectedWebSocket().then((socket) { if (!c.isCompleted) { if (timer.isActive) timer.cancel(); + + while (_queue.isNotEmpty) { + var action = _queue.removeFirst(); + socket.sink.add(serialize(action)); + } + c.complete(socket); } }).catchError((e, st) { @@ -174,6 +182,7 @@ abstract class BaseWebSocketClient extends BaseAngelClient { }, cancelOnError: true, onDone: () { + _socket = null; if (reconnectOnClose == true) { new Timer.periodic(reconnectInterval, (Timer timer) async { var result; @@ -199,7 +208,10 @@ abstract class BaseWebSocketClient extends BaseAngelClient { /// Sends the given [action] on the [socket]. void sendAction(WebSocketAction action) { - socket.sink.add(serialize(action)); + if (_socket == null) + _queue.addLast(action); + else + socket.sink.add(serialize(action)); } /// Attempts to authenticate a WebSocket, using a valid JWT. @@ -321,56 +333,56 @@ class BaseWebSocketService extends Service { /// Sends the given [action] on the [socket]. void send(WebSocketAction action) { - socket.sink.add(serialize(action)); + app.sendAction(action); } @override Future index([Map params]) async { - socket.sink.add(serialize(new WebSocketAction( - eventName: '$path::${ACTION_INDEX}', params: params ?? {}))); + app.sendAction(new WebSocketAction( + eventName: '$path::${ACTION_INDEX}', params: params ?? {})); return null; } @override Future read(id, [Map params]) async { - socket.sink.add(serialize(new WebSocketAction( - eventName: '$path::${ACTION_READ}', id: id, params: params ?? {}))); + app.sendAction(new WebSocketAction( + eventName: '$path::${ACTION_READ}', id: id, params: params ?? {})); return null; } @override Future create(data, [Map params]) async { - socket.sink.add(serialize(new WebSocketAction( + app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_CREATE}', data: data, - params: params ?? {}))); + params: params ?? {})); return null; } @override Future modify(id, data, [Map params]) async { - socket.sink.add(serialize(new WebSocketAction( + app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_MODIFY}', id: id, data: data, - params: params ?? {}))); + params: params ?? {})); return null; } @override Future update(id, data, [Map params]) async { - socket.sink.add(serialize(new WebSocketAction( + app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_UPDATE}', id: id, data: data, - params: params ?? {}))); + params: params ?? {})); return null; } @override Future remove(id, [Map params]) async { - socket.sink.add(serialize(new WebSocketAction( - eventName: '$path::${ACTION_REMOVE}', id: id, params: params ?? {}))); + app.sendAction(new WebSocketAction( + eventName: '$path::${ACTION_REMOVE}', id: id, params: params ?? {})); return null; } } diff --git a/pubspec.yaml b/pubspec.yaml index a55dca17..e93b8104 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.4+2 +version: 1.0.4+3 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From e8f49bdf37cbef7655c0c6e2637dd67dbacf07fb Mon Sep 17 00:00:00 2001 From: thosakwe Date: Mon, 17 Apr 2017 08:37:17 -0400 Subject: [PATCH 35/73] 1.0.5 --- README.md | 24 +++++++++++++++++++----- lib/server.dart | 7 ++++++- pubspec.yaml | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7aec6abb..281c0bb9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.4+3](https://img.shields.io/badge/pub-1.0.4+3-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.5](https://img.shields.io/badge/pub-1.0.5+3-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. @@ -56,8 +56,10 @@ class MyController extends WebSocketController { // Dependency injection works, too.. @ExposeWs("read_message") - void sendMessage(WebSocketContext socket, Db db) async { - socket.send("found_message", db.collection("messages").findOne(where.id("..."))); + void sendMessage(WebSocketContext socket, WebSocketAction, Db db) async { + socket.send( + "found_message", + db.collection("messages").findOne(where.id(action.data['message_id']))); } // Event filtering @@ -68,6 +70,18 @@ class MyController extends WebSocketController { } ``` +**Client Use** +This repo also provides two client libraries `browser` and `io` that extend the base +`angel_client` interface, and allow you to use a very similar API on the client to that of +the server. + +The provided clients also automatically try to reconnect their WebSockets when disconnected, +which means you can restart your development server without having to reload browser windows. + +They also provide streams of data that pump out filtered data as it comes in from the server. + +Clients can even perform authentication over WebSockets. + **In the Browser** ```dart @@ -99,11 +113,11 @@ main() async { **CLI Client** ```dart -import "package:angel_framework/angel_framework" as srv; +import "package:angel_framework/common.dart"; import "package:angel_websocket/io.dart"; // You can include these in a shared file and access on both client and server -class Car extends srv.Model { +class Car extends Model { int year; String brand, make; diff --git a/lib/server.dart b/lib/server.dart index 49da7ce4..876ddc47 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -230,7 +230,12 @@ class AngelWebSocket extends AngelPlugin { /// Hooks a service up to have its events broadcasted. hookupService(Pattern _path, HookedService service) { String path = _path.toString(); - service.afterAll(serviceHook(path)); + service.after([ + HookedServiceEvent.CREATED, + HookedServiceEvent.MODIFIED, + HookedServiceEvent.UPDATED, + HookedServiceEvent.REMOVED + ], serviceHook(path)); _servicesAlreadyWired.add(path); } diff --git a/pubspec.yaml b/pubspec.yaml index e93b8104..82d64578 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.4+3 +version: 1.0.5 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From 804e695e58283f52710a2d03f7503287f0bccc06 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 17 Apr 2017 14:19:27 -0400 Subject: [PATCH 36/73] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 281c0bb9..307775a0 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ class MyController extends WebSocketController { // Dependency injection works, too.. @ExposeWs("read_message") - void sendMessage(WebSocketContext socket, WebSocketAction, Db db) async { + void sendMessage(WebSocketContext socket, WebSocketAction action, Db db) async { socket.send( "found_message", db.collection("messages").findOne(where.id(action.data['message_id']))); From 3b7f2ae5c2d4c79ef6993251da527f8414a98b9c Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 17 Apr 2017 14:20:32 -0400 Subject: [PATCH 37/73] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 307775a0..d3d89267 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ class MyController extends WebSocketController { ``` **Client Use** + This repo also provides two client libraries `browser` and `io` that extend the base `angel_client` interface, and allow you to use a very similar API on the client to that of the server. From 63daca458d0904373f9e277f55d726de1fdfe1d7 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 23 Apr 2017 14:40:30 -0400 Subject: [PATCH 38/73] 1.0.6 --- README.md | 2 +- lib/browser.dart | 28 +++++++++++++++++----------- lib/hooks.dart | 5 ++++- lib/server.dart | 33 +++++++++++++++++++++++++++------ lib/websocket_controller.dart | 29 +++++++++++++++-------------- pubspec.yaml | 2 +- 6 files changed, 65 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index d3d89267..89652e74 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.5](https://img.shields.io/badge/pub-1.0.5+3-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.6](https://img.shields.io/badge/pub-1.0.6-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. diff --git a/lib/browser.dart b/lib/browser.dart index b7ab6910..0cdee541 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -34,21 +34,27 @@ class WebSockets extends BaseWebSocketClient { var ctrl = new StreamController(); var wnd = window.open(url, 'angel_client_auth_popup'); - wnd - ..on['beforeunload'].listen((_) { - if (!ctrl.isClosed) { + Timer t; + t = new Timer.periodic(new Duration(milliseconds: 500), (timer) { + if (!ctrl.isClosed) { + if (wnd.closed) { ctrl.addError(new AngelHttpException.notAuthenticated( message: - errorMessage ?? 'Authentication via popup window failed.')); + errorMessage ?? 'Authentication via popup window failed.')); ctrl.close(); + timer.cancel(); } - }) - ..on[eventName ?? 'token'].listen((CustomEvent e) { - if (!ctrl.isClosed) { - ctrl.add(e.detail); - ctrl.close(); - } - }); + } else + timer.cancel(); + }); + + window.on[eventName ?? 'token'].listen((CustomEvent e) { + if (!ctrl.isClosed) { + ctrl.add(e.detail); + t.cancel(); + ctrl.close(); + } + }); return ctrl.stream; } diff --git a/lib/hooks.dart b/lib/hooks.dart index 32dea957..362e5d62 100644 --- a/lib/hooks.dart +++ b/lib/hooks.dart @@ -3,7 +3,8 @@ import 'package:angel_framework/angel_framework.dart'; /// Prevents a WebSocket event from being broadcasted, to any client from the given [provider]. /// /// [provider] can be a String, a [Provider], or an Iterable. -HookedServiceEventListener doNotBroadcast(provider) { +/// If [provider] is `null`, any provider will be blocked. +HookedServiceEventListener doNotBroadcast([provider]) { return (HookedServiceEvent e) { if (e.params != null && e.params.containsKey('provider')) { bool deny = false; @@ -16,6 +17,8 @@ HookedServiceEventListener doNotBroadcast(provider) { deny = deny || p == e.params['provider'] || e.params['provider'] == p.via; + } else if (p == null) { + deny = true; } else deny = deny || (e.params['provider'] as Providers).via == p.toString(); diff --git a/lib/server.dart b/lib/server.dart index 876ddc47..45777912 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -42,6 +42,11 @@ class AngelWebSocket extends AngelPlugin { /// Include debug information, and send error information across WebSockets. final bool debug; + bool _sendErrors; + + /// Send error information across WebSockets, without including [debug] information.. + bool get sendErrors => _sendErrors == true; + /// Registers this instance as a route on the server. final AngelWebSocketRegisterer register; @@ -70,13 +75,27 @@ class AngelWebSocket extends AngelPlugin { /// Fired when a user disconnects. Stream get onDisconnection => _onDisconnect.stream; + /// Serializes data to WebSockets. + ResponseSerializer serializer; + + /// Deserializes data from WebSockets. + Function deserializer; + AngelWebSocket( {this.endpoint: '/ws', this.debug: false, + bool sendErrors, this.allowClientParams: false, this.allowAuth: true, this.register, - this.synchronizer}); + this.synchronizer, + this.serializer, + this.deserializer}) { + _sendErrors = sendErrors; + + if (serializer == null) serializer = god.serialize; + if (deserializer == null) deserializer = (params) => params; + } serviceHook(String path) { return (HookedServiceEvent e) async { @@ -111,7 +130,7 @@ class AngelWebSocket extends AngelPlugin { var serialized = event.toJson(); _printDebug('Batching this event: $serialized'); // print('Serialized: ' + JSON.encode(serialized)); - client.io.add(god.serialize(event.toJson())); + client.io.add((serializer ?? god.serialize)(event.toJson())); } }); @@ -150,7 +169,7 @@ class AngelWebSocket extends AngelPlugin { } var params = mergeMap([ - god.deserializeDatum(action.params), + (deserializer ?? (params) => params)(action.params), { "provider": Providers.WEBSOCKET, '__requestctx': socket.request, @@ -188,7 +207,7 @@ class AngelWebSocket extends AngelPlugin { } catch (e, st) { if (e is AngelHttpException) return socket.sendError(e); - else if (debug == true) + else if (debug == true || _sendErrors == true) socket.sendError(new AngelHttpException(e, message: e.toString(), stackTrace: st, errors: [st.toString()])); else @@ -218,7 +237,7 @@ class AngelWebSocket extends AngelPlugin { // Send an error if (e is AngelHttpException) socket.sendError(e); - else if (debug == true) + else if (debug == true || _sendErrors == true) socket.sendError(new AngelHttpException(e, message: e.toString(), stackTrace: st, errors: [st.toString()])); else @@ -277,7 +296,7 @@ class AngelWebSocket extends AngelPlugin { // Send an error if (e is AngelHttpException) socket.sendError(e); - else if (debug == true) + else if (debug == true || _sendErrors == true) socket.sendError(new AngelHttpException(e, message: e.toString(), stackTrace: st, errors: [st.toString()])); else @@ -302,6 +321,8 @@ class AngelWebSocket extends AngelPlugin { @override Future call(Angel app) async { + if (_sendErrors == null) _sendErrors = app.isProduction; + _app = app..container.singleton(this); if (runtimeType != AngelWebSocket) diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index c0d3db12..c1da2ec9 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -68,25 +68,26 @@ class WebSocketController extends Controller { socket.onAction.listen((WebSocketAction action) async { socket.request.inject(WebSocketAction, action); - await onAction(action, socket); - if (_handlers.containsKey(action.eventName)) { - try { + try { + await onAction(action, socket); + + if (_handlers.containsKey(action.eventName)) { var methodMirror = _handlers[action.eventName]; var fn = instanceMirror.getField(methodMirror.simpleName).reflectee; return app.runContained(fn, socket.request, socket.response); - } catch (e, st) { - // Send an error - if (e is AngelHttpException) - socket.sendError(e); - else if (ws.debug == true) - socket.sendError(new AngelHttpException(e, - message: e.toString(), - stackTrace: st, - errors: [st.toString()])); - else - socket.sendError(new AngelHttpException(e)); } + } catch (e, st) { + // Send an error + if (e is AngelHttpException) + socket.sendError(e); + else if (ws.debug == true || ws.sendErrors == true) + socket.sendError(new AngelHttpException(e, + message: e.toString(), + stackTrace: st, + errors: [st.toString()])); + else + socket.sendError(new AngelHttpException(e)); } }); }); diff --git a/pubspec.yaml b/pubspec.yaml index 82d64578..b1270b0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.5 +version: 1.0.6 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From 3617f05b7b524f00e5ec5f71ffc84a64a706d7be Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 23 Apr 2017 14:53:12 -0400 Subject: [PATCH 39/73] 1.0.6+1 --- README.md | 2 +- lib/base_websocket_client.dart | 6 +++++- lib/server.dart | 8 ++++++-- pubspec.yaml | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 89652e74..8df23a03 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.6](https://img.shields.io/badge/pub-1.0.6-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.6+1](https://img.shields.io/badge/pub-1.0.6+1-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. diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 18081022..f1d46e0f 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -216,7 +216,11 @@ abstract class BaseWebSocketClient extends BaseAngelClient { /// Attempts to authenticate a WebSocket, using a valid JWT. void authenticateViaJwt(String jwt) { - send(ACTION_AUTHENTICATE, new WebSocketAction(params: {'jwt': jwt})); + send( + ACTION_AUTHENTICATE, + new WebSocketAction(params: { + 'query': {'jwt': jwt} + })); } } diff --git a/lib/server.dart b/lib/server.dart index 45777912..9c219580 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -219,10 +219,11 @@ class AngelWebSocket extends AngelPlugin { Future handleAuth(WebSocketAction action, WebSocketContext socket) async { if (allowAuth != false && action.eventName == ACTION_AUTHENTICATE && - action.params['jwt'] is String) { + action.params['query'] is Map && + action.params['query']['jwt'] is String) { try { var auth = socket.request.grab(AngelAuth); - var jwt = action.params['jwt'] as String; + var jwt = action.params['query']['jwt'] as String; AuthToken token; token = new AuthToken.validate(jwt, auth.hmac); @@ -243,6 +244,9 @@ class AngelWebSocket extends AngelPlugin { else socket.sendError(new AngelHttpException(e)); } + } else { + socket.sendError(new AngelHttpException.badRequest( + message: 'No JWT provided for authentication.')); } } diff --git a/pubspec.yaml b/pubspec.yaml index b1270b0d..cbf4defd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.6 +version: 1.0.6+1 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From ff5c2a50a3f2b5e96523325916b5c846f8fb1d9c Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 3 Jun 2017 14:13:38 -0400 Subject: [PATCH 40/73] Fixed auth --- README.md | 2 +- lib/browser.dart | 5 +++- lib/server.dart | 38 +++++++++++++++--------------- pubspec.yaml | 10 ++++---- test/auth_test.dart | 56 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 test/auth_test.dart diff --git a/README.md b/README.md index 8df23a03..eb21d1cf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.6+1](https://img.shields.io/badge/pub-1.0.6+1-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![1.0.7](https://img.shields.io/badge/pub-1.0.7-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. diff --git a/lib/browser.dart b/lib/browser.dart index 0cdee541..7f3afa6d 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -35,6 +35,7 @@ class WebSockets extends BaseWebSocketClient { var wnd = window.open(url, 'angel_client_auth_popup'); Timer t; + StreamSubscription sub; t = new Timer.periodic(new Duration(milliseconds: 500), (timer) { if (!ctrl.isClosed) { if (wnd.closed) { @@ -43,16 +44,18 @@ class WebSockets extends BaseWebSocketClient { errorMessage ?? 'Authentication via popup window failed.')); ctrl.close(); timer.cancel(); + sub?.cancel(); } } else timer.cancel(); }); - window.on[eventName ?? 'token'].listen((CustomEvent e) { + sub = window.on[eventName ?? 'token'].listen((CustomEvent e) { if (!ctrl.isClosed) { ctrl.add(e.detail); t.cancel(); ctrl.close(); + sub.cancel(); } }); diff --git a/lib/server.dart b/lib/server.dart index 9c219580..51670e04 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -13,6 +13,7 @@ import 'angel_websocket.dart'; export 'angel_websocket.dart'; part 'websocket_context.dart'; + part 'websocket_controller.dart'; /// Used to assign routes to a given handler. @@ -25,12 +26,12 @@ class AngelWebSocket extends AngelPlugin { final List _servicesAlreadyWired = []; final StreamController _onAction = - new StreamController(); + new StreamController(); final StreamController _onData = new StreamController(); final StreamController _onConnection = - new StreamController.broadcast(); + new StreamController.broadcast(); final StreamController _onDisconnect = - new StreamController.broadcast(); + new StreamController.broadcast(); /// If this is not `true`, then all client-side service parameters will be /// discarded, other than `params['query']`. @@ -81,16 +82,15 @@ class AngelWebSocket extends AngelPlugin { /// Deserializes data from WebSockets. Function deserializer; - AngelWebSocket( - {this.endpoint: '/ws', - this.debug: false, - bool sendErrors, - this.allowClientParams: false, - this.allowAuth: true, - this.register, - this.synchronizer, - this.serializer, - this.deserializer}) { + AngelWebSocket({this.endpoint: '/ws', + this.debug: false, + bool sendErrors, + this.allowClientParams: false, + this.allowAuth: true, + this.register, + this.synchronizer, + this.serializer, + this.deserializer}) { _sendErrors = sendErrors; if (serializer == null) serializer = god.serialize; @@ -143,9 +143,6 @@ 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) @@ -229,9 +226,8 @@ class AngelWebSocket extends AngelPlugin { 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); + 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) { @@ -286,6 +282,9 @@ class AngelWebSocket extends AngelPlugin { .add(fromJson["data"]); } + if (action.eventName == ACTION_AUTHENTICATE) + await handleAuth(action, socket); + if (action.eventName.contains("::")) { var split = action.eventName.split("::"); @@ -389,5 +388,6 @@ class AngelWebSocket extends AngelPlugin { /// notifications from other nodes. abstract class WebSocketSynchronizer { Stream get stream; + void notifyOthers(WebSocketEvent e); } diff --git a/pubspec.yaml b/pubspec.yaml index cbf4defd..d7b1ae4e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,18 +2,18 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.6+1 +version: 1.0.7 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: - angel_auth: "^1.0.0-dev" + angel_auth: ^1.0.0-dev angel_client: "^1.0.0" - angel_framework: "^1.0.0-dev" - http: "^0.11.3" + angel_framework: ^1.0.0-dev + http: ">=0.11.0 <0.12.0" json_god: ^2.0.0-beta merge_map: ^1.0.0 uuid: "^0.5.3" web_socket_channel: "^1.0.0" dev_dependencies: - angel_diagnostics: "^1.0.0-dev" + angel_diagnostics: "^1.0.0" test: "^0.12.15" diff --git a/test/auth_test.dart b/test/auth_test.dart new file mode 100644 index 00000000..1118affc --- /dev/null +++ b/test/auth_test.dart @@ -0,0 +1,56 @@ +import 'dart:async'; +import 'package:angel_auth/angel_auth.dart'; +import 'package:angel_client/io.dart' as c; +import 'package:angel_diagnostics/angel_diagnostics.dart'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_websocket/io.dart' as c; +import 'package:angel_websocket/server.dart'; +import 'package:test/test.dart'; + +const Map USER = const {'username': 'foo', 'password': 'bar'}; + +main() { + Angel app; + c.Angel client; + c.WebSockets ws; + + setUp(() async { + app = new Angel(); + var auth = new AngelAuth(); + + auth.serializer = (_) async => 'baz'; + auth.deserializer = (_) async => USER; + + auth.strategies.add(new LocalAuthStrategy((username, password) async { + if (username == 'foo' && password == 'bar') return USER; + })); + + app.post('/auth/local', auth.authenticate('local')); + + await app.configure(auth); + var sock = new AngelWebSocket(); + await app.configure(sock); + await app.configure(logRequests()); + + var server = await app.startServer(); + client = new c.Rest('http://${server.address.address}:${server.port}'); + ws = new c.WebSockets('ws://${server.address.address}:${server.port}/ws'); + await ws.connect(); + }); + + tearDown(() => + Future.wait([ + app.close(), + client.close(), + ws.close() + ])); + + test('auth event fires', () async { + var localAuth = await client.authenticate(type: 'local', credentials: USER); + print('JWT: ${localAuth.token}'); + + ws.authenticateViaJwt(localAuth.token); + var auth = await ws.onAuthenticated.first; + expect(auth.token, localAuth.token); + }); +} From 11089978170049d6078a7fbbee143891ce4eb9c3 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Fri, 30 Jun 2017 19:09:03 -0400 Subject: [PATCH 41/73] 1.0.8 --- README.md | 6 ++++- lib/base_websocket_client.dart | 8 +++--- lib/browser.dart | 10 ++++---- lib/flutter.dart | 46 ++++++++++++++++++++++++++++++++++ lib/io.dart | 10 ++++---- lib/server.dart | 2 ++ lib/websocket_controller.dart | 2 +- pubspec.yaml | 2 +- 8 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 lib/flutter.dart diff --git a/README.md b/README.md index eb21d1cf..646d0d69 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # angel_websocket -[![1.0.7](https://img.shields.io/badge/pub-1.0.7-brightgreen.svg)](https://pub.dartlang.org/packages/angel_websocket) +[![Pub](https://img.shields.io/pub/v/angel_websocket.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. @@ -36,6 +36,10 @@ synchronously or asynchronously. myService.properties['ws:filter'] = (HookedServiceEvent e, WebSocketContext socket) async { return true; } + +myService.index({ + 'ws:filter': (e, socket) => ...; +}); ``` **Adding Handlers within a Controller** diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index f1d46e0f..8711a33a 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -133,10 +133,10 @@ abstract class BaseWebSocketClient extends BaseAngelClient { Future getConnectedWebSocket(); @override - BaseWebSocketService service(String path, + WebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.toString().replaceAll(_straySlashes, ''); - return new BaseWebSocketService(socket, this, uri, + return new WebSocketsService(socket, this, uri, deserializer: deserializer); } @@ -225,7 +225,7 @@ abstract class BaseWebSocketClient extends BaseAngelClient { } /// A [Service] that asynchronously interacts with the server. -class BaseWebSocketService extends Service { +class WebSocketsService extends Service { /// The [BaseWebSocketClient] that spawned this service. @override final BaseWebSocketClient app; @@ -275,7 +275,7 @@ class BaseWebSocketService extends Service { /// Fired on `removed` events. Stream get onRemoved => _onRemoved.stream; - BaseWebSocketService(this.socket, this.app, this.path, {this.deserializer}) { + WebSocketsService(this.socket, this.app, this.path, {this.deserializer}) { listen(); } diff --git a/lib/browser.dart b/lib/browser.dart index 7f3afa6d..c0598cbd 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -15,7 +15,7 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); /// Queries an Angel server via WebSockets. class WebSockets extends BaseWebSocketClient { - final List _services = []; + final List _services = []; WebSockets(String path) : super(new http.BrowserClient(), path); @@ -80,17 +80,17 @@ class WebSockets extends BaseWebSocketClient { } @override - WebSocketsService service(String path, + BrowserWebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.replaceAll(_straySlashes, ''); - return new WebSocketsService(socket, this, uri, deserializer: deserializer); + return new BrowserWebSocketsService(socket, this, uri, deserializer: deserializer); } } -class WebSocketsService extends BaseWebSocketService { +class BrowserWebSocketsService extends WebSocketsService { final Type type; - WebSocketsService(WebSocketChannel socket, Angel app, String uri, + BrowserWebSocketsService(WebSocketChannel socket, Angel app, String uri, {this.type, AngelDeserializer deserializer}) : super(socket, app, uri, deserializer: deserializer); } diff --git a/lib/flutter.dart b/lib/flutter.dart new file mode 100644 index 00000000..1186dbe8 --- /dev/null +++ b/lib/flutter.dart @@ -0,0 +1,46 @@ +/// Flutter-compatible WebSocket client library for the Angel framework. +library angel_websocket.flutter; + +import 'dart:async'; +import 'dart:io'; +import 'package:angel_client/angel_client.dart'; +import 'package:http/http.dart' as http; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:web_socket_channel/io.dart'; +import 'angel_websocket.dart'; +import 'base_websocket_client.dart'; +export 'package:angel_client/angel_client.dart'; +export 'angel_websocket.dart'; + +final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); + +/// Queries an Angel server via WebSockets. +class WebSockets extends BaseWebSocketClient { + final List _services = []; + + WebSockets(String path) : super(new http.Client(), path); + + @override + Stream authenticateViaPopup(String url, {String eventName: 'token'}) { + throw new UnimplementedError( + 'Opening popup windows is not supported in the `dart:io` client.'); + } + + @override + Future close() { + for (var service in _services) { + service.close(); + } + + return super.close(); + } + + @override + Future getConnectedWebSocket() async { + var socket = await WebSocket.connect(basePath, + headers: authToken?.isNotEmpty == true + ? {'Authorization': 'Bearer $authToken'} + : {}); + return new IOWebSocketChannel(socket); + } +} \ No newline at end of file diff --git a/lib/io.dart b/lib/io.dart index 254e76fd..19f9e648 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -17,7 +17,7 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); /// Queries an Angel server via WebSockets. class WebSockets extends BaseWebSocketClient { - final List _services = []; + final List _services = []; WebSockets(String path) : super(new http.Client(), path); @@ -46,20 +46,20 @@ class WebSockets extends BaseWebSocketClient { } @override - WebSocketsService service(String path, + IoWebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.replaceAll(_straySlashes, ''); - return new WebSocketsService(socket, this, uri, T != dynamic ? T : type); + return new IoWebSocketsService(socket, this, uri, T != dynamic ? T : type); } @override serialize(x) => god.serialize(x); } -class WebSocketsService extends BaseWebSocketService { +class IoWebSocketsService extends WebSocketsService { final Type type; - WebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type) + IoWebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type) : super(socket, app, uri); @override diff --git a/lib/server.dart b/lib/server.dart index 51670e04..d48f87c1 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -107,6 +107,8 @@ class AngelWebSocket extends AngelPlugin { _filter(WebSocketContext socket) { if (e.service.properties.containsKey('ws:filter')) return e.service.properties['ws:filter'](e, socket); + else if (e.params != null && e.params.containsKey('ws:filter')) + return e.params['ws:filter'](e, socket); else return true; } diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index c1da2ec9..f692df71 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -37,7 +37,7 @@ class WebSocketController extends Controller { @override Future call(Angel app) async { - await super.call(app); + if (findExpose() != null) await super.call(app); InstanceMirror instanceMirror = reflect(this); ClassMirror classMirror = reflectClass(this.runtimeType); diff --git a/pubspec.yaml b/pubspec.yaml index d7b1ae4e..fe514dbe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.7 +version: 1.0.8 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From 3c01c4b3609d95d9d7a576355711491ca6f8dd03 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 24 Sep 2017 00:37:58 -0400 Subject: [PATCH 42/73] 1.1.0-alpha --- README.md | 9 +- ...ysis-options.yaml => analysis_options.yaml | 0 lib/base_websocket_client.dart | 6 +- lib/browser.dart | 22 +- lib/flutter.dart | 2 - lib/io.dart | 4 +- lib/server.dart | 205 ++++++++---------- lib/websocket_controller.dart | 11 +- pubspec.yaml | 10 +- test/auth_test.dart | 24 +- test/controller/io_test.dart | 9 +- test/service/io_test.dart | 9 +- 12 files changed, 143 insertions(+), 168 deletions(-) rename .analysis-options.yaml => analysis_options.yaml (100%) diff --git a/README.md b/README.md index 646d0d69..78ec9696 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,13 @@ import "package:angel_websocket/server.dart"; main() async { var app = new Angel(); - // Ensure this runs after all our services are in-place - app.justBeforeStart.add(new AngelWebSocket("/ws")); + var ws = new AngelWebSocket(); + + // Apply configuration + await app.configure(ws.configureServer); + + // Listen for requests at `/ws`. + app.all('/ws', ws.handleRequest); } ``` diff --git a/.analysis-options.yaml b/analysis_options.yaml similarity index 100% rename from .analysis-options.yaml rename to analysis_options.yaml diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 8711a33a..a44780b1 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'package:angel_client/angel_client.dart'; +import 'package:angel_client/base_angel_client.dart'; +import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:http/src/base_client.dart' as http; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; import 'angel_websocket.dart'; -export 'package:angel_client/angel_client.dart'; -import 'package:angel_client/base_angel_client.dart'; final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); @@ -133,7 +133,7 @@ abstract class BaseWebSocketClient extends BaseAngelClient { Future getConnectedWebSocket(); @override - WebSocketsService service(String path, + WebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.toString().replaceAll(_straySlashes, ''); return new WebSocketsService(socket, this, uri, diff --git a/lib/browser.dart b/lib/browser.dart index c0598cbd..f1380051 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -4,11 +4,11 @@ library angel_websocket.browser; import 'dart:async'; import 'dart:html'; import 'package:angel_client/angel_client.dart'; +import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:http/browser_client.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 'angel_websocket.dart'; final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); @@ -41,7 +41,7 @@ class WebSockets extends BaseWebSocketClient { if (wnd.closed) { ctrl.addError(new AngelHttpException.notAuthenticated( message: - errorMessage ?? 'Authentication via popup window failed.')); + errorMessage ?? 'Authentication via popup window failed.')); ctrl.close(); timer.cancel(); sub?.cancel(); @@ -50,9 +50,9 @@ class WebSockets extends BaseWebSocketClient { timer.cancel(); }); - sub = window.on[eventName ?? 'token'].listen((CustomEvent e) { + sub = window.on[eventName ?? 'token'].listen((e) { if (!ctrl.isClosed) { - ctrl.add(e.detail); + ctrl.add((e as CustomEvent).detail); t.cancel(); ctrl.close(); sub.cancel(); @@ -64,7 +64,9 @@ class WebSockets extends BaseWebSocketClient { @override Future getConnectedWebSocket() { - var socket = new WebSocket(authToken?.isNotEmpty == true ? '$basePath?token=$authToken' : basePath ); + var socket = new WebSocket(authToken?.isNotEmpty == true + ? '$basePath?token=$authToken' + : basePath); var completer = new Completer(); socket @@ -72,18 +74,20 @@ class WebSockets extends BaseWebSocketClient { if (!completer.isCompleted) return completer.complete(new HtmlWebSocketChannel(socket)); }) - ..onError.listen((ErrorEvent e) { - if (!completer.isCompleted) return completer.completeError(e.error); + ..onError.listen((e) { + var err = e as ErrorEvent; + if (!completer.isCompleted) return completer.completeError(err.error); }); return completer.future; } @override - BrowserWebSocketsService service(String path, + BrowserWebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.replaceAll(_straySlashes, ''); - return new BrowserWebSocketsService(socket, this, uri, deserializer: deserializer); + return new BrowserWebSocketsService(socket, this, uri, + deserializer: deserializer); } } diff --git a/lib/flutter.dart b/lib/flutter.dart index 1186dbe8..e4ab38ad 100644 --- a/lib/flutter.dart +++ b/lib/flutter.dart @@ -3,11 +3,9 @@ library angel_websocket.flutter; import 'dart:async'; import 'dart:io'; -import 'package:angel_client/angel_client.dart'; import 'package:http/http.dart' as http; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/io.dart'; -import 'angel_websocket.dart'; import 'base_websocket_client.dart'; export 'package:angel_client/angel_client.dart'; export 'angel_websocket.dart'; diff --git a/lib/io.dart b/lib/io.dart index 19f9e648..fa89368e 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -46,10 +46,10 @@ class WebSockets extends BaseWebSocketClient { } @override - IoWebSocketsService service(String path, + IoWebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.replaceAll(_straySlashes, ''); - return new IoWebSocketsService(socket, this, uri, T != dynamic ? T : type); + return new IoWebSocketsService(socket, this, uri, type); } @override diff --git a/lib/server.dart b/lib/server.dart index d48f87c1..8d6a8628 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -16,22 +16,20 @@ part 'websocket_context.dart'; part 'websocket_controller.dart'; -/// Used to assign routes to a given handler. -typedef AngelWebSocketRegisterer(Angel app, RequestHandler handler); - /// Broadcasts events from [HookedService]s, and handles incoming [WebSocketAction]s. -class AngelWebSocket extends AngelPlugin { - Angel _app; +class AngelWebSocket { List _clients = []; final List _servicesAlreadyWired = []; final StreamController _onAction = - new StreamController(); + new StreamController(); final StreamController _onData = new StreamController(); final StreamController _onConnection = - new StreamController.broadcast(); + new StreamController.broadcast(); final StreamController _onDisconnect = - new StreamController.broadcast(); + new StreamController.broadcast(); + + final Angel app; /// If this is not `true`, then all client-side service parameters will be /// discarded, other than `params['query']`. @@ -40,16 +38,8 @@ class AngelWebSocket extends AngelPlugin { /// 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; - - bool _sendErrors; - /// Send error information across WebSockets, without including [debug] information.. - bool get sendErrors => _sendErrors == true; - - /// Registers this instance as a route on the server. - final AngelWebSocketRegisterer register; + final bool sendErrors; /// A list of clients currently connected to this server via WebSockets. List get clients => new List.unmodifiable(_clients); @@ -58,9 +48,6 @@ class AngelWebSocket extends AngelPlugin { List get servicesAlreadyWired => new List.unmodifiable(_servicesAlreadyWired); - /// 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; @@ -82,17 +69,13 @@ class AngelWebSocket extends AngelPlugin { /// Deserializes data from WebSockets. Function deserializer; - AngelWebSocket({this.endpoint: '/ws', - this.debug: false, - bool sendErrors, - this.allowClientParams: false, - this.allowAuth: true, - this.register, - this.synchronizer, - this.serializer, - this.deserializer}) { - _sendErrors = sendErrors; - + AngelWebSocket(this.app, + {this.sendErrors: false, + this.allowClientParams: false, + this.allowAuth: true, + this.synchronizer, + this.serializer, + this.deserializer}) { if (serializer == null) serializer = god.serialize; if (deserializer == null) deserializer = (params) => params; } @@ -117,10 +100,6 @@ class AngelWebSocket extends AngelPlugin { }; } - void _printDebug(String msg) { - if (debug == true) print(msg); - } - /// Slates an event to be dispatched. Future batchEvent(WebSocketEvent event, {filter(WebSocketContext socket), bool notify: true}) async { @@ -129,9 +108,6 @@ class AngelWebSocket extends AngelPlugin { var result = true; if (filter != null) result = await filter(client); if (result == true) { - var serialized = event.toJson(); - _printDebug('Batching this event: $serialized'); - // print('Serialized: ' + JSON.encode(serialized)); client.io.add((serializer ?? god.serialize)(event.toJson())); } }); @@ -150,7 +126,7 @@ class AngelWebSocket extends AngelPlugin { if (split.length < 2) return socket.sendError(new AngelHttpException.badRequest()); - var service = _app.service(split[0]); + var service = app.service(split[0]); if (service == null) return socket.sendError(new AngelHttpException.notFound( @@ -170,7 +146,7 @@ class AngelWebSocket extends AngelPlugin { var params = mergeMap([ (deserializer ?? (params) => params)(action.params), { - "provider": Providers.WEBSOCKET, + "provider": Providers.websocket, '__requestctx': socket.request, '__responsectx': socket.response } @@ -204,13 +180,7 @@ class AngelWebSocket extends AngelPlugin { message: "Method Not Allowed: \"$actionName\"")); } } catch (e, st) { - if (e is AngelHttpException) - return socket.sendError(e); - else if (debug == true || _sendErrors == true) - socket.sendError(new AngelHttpException(e, - message: e.toString(), stackTrace: st, errors: [st.toString()])); - else - socket.sendError(new AngelHttpException(e)); + catchError(e, st, socket); } } @@ -228,19 +198,13 @@ class AngelWebSocket extends AngelPlugin { 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); + 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 || _sendErrors == true) - socket.sendError(new AngelHttpException(e, - message: e.toString(), stackTrace: st, errors: [st.toString()])); - else - socket.sendError(new AngelHttpException(e)); + catchError(e, st, socket); } } else { socket.sendError(new AngelHttpException.badRequest( @@ -251,12 +215,15 @@ class AngelWebSocket extends AngelPlugin { /// Hooks a service up to have its events broadcasted. hookupService(Pattern _path, HookedService service) { String path = _path.toString(); - service.after([ - HookedServiceEvent.CREATED, - HookedServiceEvent.MODIFIED, - HookedServiceEvent.UPDATED, - HookedServiceEvent.REMOVED - ], serviceHook(path)); + service.after( + [ + HookedServiceEvent.created, + HookedServiceEvent.modified, + HookedServiceEvent.updated, + HookedServiceEvent.removed + ], + serviceHook(path), + ); _servicesAlreadyWired.add(path); } @@ -298,14 +265,24 @@ class AngelWebSocket extends AngelPlugin { } } } catch (e, st) { - // Send an error - if (e is AngelHttpException) - socket.sendError(e); - else if (debug == true || _sendErrors == true) - socket.sendError(new AngelHttpException(e, - message: e.toString(), stackTrace: st, errors: [st.toString()])); - else - socket.sendError(new AngelHttpException(e)); + catchError(e, st, socket); + } + } + + void catchError(e, StackTrace st, WebSocketContext socket) { + // Send an error + if (e is AngelHttpException) { + socket.sendError(e); + app.logger?.severe(e.message, e.error ?? e, e.stackTrace); + } else if (sendErrors) { + var err = new AngelHttpException(e, + message: e.toString(), stackTrace: st, errors: [st.toString()]); + socket.sendError(err); + app.logger?.severe(err.message, e, st); + } else { + var err = new AngelHttpException(e); + socket.sendError(err); + app.logger?.severe(e.toString(), e, st); } } @@ -324,11 +301,9 @@ class AngelWebSocket extends AngelPlugin { } } - @override - Future call(Angel app) async { - if (_sendErrors == null) _sendErrors = app.isProduction; - - _app = app..container.singleton(this); + /// Configiures an [Angel] instance to listen for WebSocket connections. + Future configureServer(Angel app) async { + app..container.singleton(this); if (runtimeType != AngelWebSocket) app.container.singleton(this, as: AngelWebSocket); @@ -340,50 +315,48 @@ class AngelWebSocket extends AngelPlugin { wireAllServices(app); }); - handler(RequestContext req, ResponseContext res) async { - if (!WebSocketTransformer.isUpgradeRequest(req.io)) - throw new AngelHttpException.badRequest(); - - res - ..willCloseItself = true - ..end(); - - var ws = await WebSocketTransformer.upgrade(req.io); - var socket = new WebSocketContext(ws, req, res); - _clients.add(socket); - await handleConnect(socket); - - _onConnection.add(socket); - - req - ..properties['socket'] = socket - ..inject(WebSocketContext, socket); - - ws.listen((data) { - _onData.add(data); - handleData(socket, data); - }, onDone: () { - _onDisconnect.add(socket); - _clients.remove(ws); - }, onError: (e) { - _onDisconnect.add(socket); - _clients.remove(ws); - }, cancelOnError: true); - } - - _register() { - if (register != null) - return register(app, handler); - else - app.get(endpoint, handler); - } - - await _register(); - if (synchronizer != null) { synchronizer.stream.listen((e) => batchEvent(e, notify: false)); } } + + /// Handles an incoming HTTP request. + Future handleRequest(RequestContext req, ResponseContext res) async { + if (!WebSocketTransformer.isUpgradeRequest(req.io)) + throw new AngelHttpException.badRequest(); + + res + ..willCloseItself = true + ..end(); + + var ws = await WebSocketTransformer.upgrade(req.io); + var socket = new WebSocketContext(ws, req, res); + _clients.add(socket); + await handleConnect(socket); + + _onConnection.add(socket); + + req + ..properties['socket'] = socket + ..inject(WebSocketContext, socket); + + ws.listen( + (data) { + _onData.add(data); + handleData(socket, data); + }, + onDone: () { + _onDisconnect.add(socket); + _clients.remove(ws); + }, + onError: (e) { + _onDisconnect.add(socket); + _clients.remove(ws); + }, + cancelOnError: true, + ); + return false; + } } /// Notifies other nodes of outgoing WWebSocket events, and listens for diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index f692df71..4aad8d9c 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -78,16 +78,7 @@ class WebSocketController extends Controller { return app.runContained(fn, socket.request, socket.response); } } catch (e, st) { - // Send an error - if (e is AngelHttpException) - socket.sendError(e); - else if (ws.debug == true || ws.sendErrors == true) - socket.sendError(new AngelHttpException(e, - message: e.toString(), - stackTrace: st, - errors: [st.toString()])); - else - socket.sendError(new AngelHttpException(e)); + ws.catchError(e, st, socket); } }); }); diff --git a/pubspec.yaml b/pubspec.yaml index fe514dbe..828ce5d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,18 +2,18 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.0.8 +version: 1.1.0-alpha author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: - angel_auth: ^1.0.0-dev - angel_client: "^1.0.0" - angel_framework: ^1.0.0-dev + angel_auth: ^1.1.0-alpha + angel_client: ^1.1.0-alpha + angel_framework: ^1.1.0-alpha http: ">=0.11.0 <0.12.0" json_god: ^2.0.0-beta merge_map: ^1.0.0 + meta: ^1.0.0 uuid: "^0.5.3" web_socket_channel: "^1.0.0" dev_dependencies: - angel_diagnostics: "^1.0.0" test: "^0.12.15" diff --git a/test/auth_test.dart b/test/auth_test.dart index 1118affc..0a79c973 100644 --- a/test/auth_test.dart +++ b/test/auth_test.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:angel_auth/angel_auth.dart'; import 'package:angel_client/io.dart' as c; -import 'package:angel_diagnostics/angel_diagnostics.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_websocket/io.dart' as c; import 'package:angel_websocket/server.dart'; +import 'package:logging/logging.dart'; import 'package:test/test.dart'; const Map USER = const {'username': 'foo', 'password': 'bar'}; @@ -27,10 +27,11 @@ main() { app.post('/auth/local', auth.authenticate('local')); - await app.configure(auth); - var sock = new AngelWebSocket(); - await app.configure(sock); - await app.configure(logRequests()); + await app.configure(auth.configureServer); + var sock = new AngelWebSocket(app); + await app.configure(sock.configureServer); + app.all('/ws', sock.handleRequest); + app.logger = new Logger('angel_auth')..onRecord.listen(print); var server = await app.startServer(); client = new c.Rest('http://${server.address.address}:${server.port}'); @@ -38,12 +39,13 @@ main() { await ws.connect(); }); - tearDown(() => - Future.wait([ - app.close(), - client.close(), - ws.close() - ])); + tearDown(() { + return Future.wait([ + app.close(), + client.close(), + ws.close(), + ]); + }); test('auth event fires', () async { var localAuth = await client.authenticate(type: 'local', credentials: USER); diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart index 0be6243a..7e78b796 100644 --- a/test/controller/io_test.dart +++ b/test/controller/io_test.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:angel_diagnostics/angel_diagnostics.dart'; 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:logging/logging.dart'; import 'package:test/test.dart'; import 'common.dart'; @@ -16,14 +16,15 @@ main() { setUp(() async { app = new srv.Angel(); - websockets = new srv.AngelWebSocket(debug: true) + websockets = new srv.AngelWebSocket(app) ..onData.listen((data) { print('Received by server: $data'); }); - await app.configure(websockets); + await app.configure(websockets.configureServer); + app.all('/ws', websockets.handleRequest); await app.configure(new GameController()); - await app.configure(logRequests(new File('log.txt'))); + app.logger = new Logger('angel_auth')..onRecord.listen(print); server = await app.startServer(); url = 'ws://${server.address.address}:${server.port}/ws'; diff --git a/test/service/io_test.dart b/test/service/io_test.dart index e3d8f9d7..883a64f2 100644 --- a/test/service/io_test.dart +++ b/test/service/io_test.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:angel_diagnostics/angel_diagnostics.dart'; 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:logging/logging.dart'; import 'package:test/test.dart'; import 'common.dart'; @@ -16,13 +16,14 @@ main() { setUp(() async { app = new srv.Angel()..use('/api/todos', new TodoService()); - websockets = new srv.AngelWebSocket(debug: true) + websockets = new srv.AngelWebSocket(app) ..onData.listen((data) { print('Received by server: $data'); }); - await app.configure(websockets); - await app.configure(logRequests(new File('log.txt'))); + await app.configure(websockets.configureServer); + app.all('/ws', websockets.handleRequest); + app.logger = new Logger('angel_auth')..onRecord.listen(print); server = await app.startServer(); url = 'ws://${server.address.address}:${server.port}/ws'; From 6eed138927e20b1b98f2f0bf83bddec05631c8c8 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 24 Sep 2017 12:19:16 -0400 Subject: [PATCH 43/73] Moved to WebSocketChannel --- lib/server.dart | 9 +++++++-- lib/websocket_context.dart | 14 +++++++------- pubspec.yaml | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/server.dart b/lib/server.dart index 8d6a8628..f2374acc 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -9,6 +9,8 @@ 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'; +import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; import 'angel_websocket.dart'; export 'angel_websocket.dart'; @@ -108,7 +110,7 @@ class AngelWebSocket { var result = true; if (filter != null) result = await filter(client); if (result == true) { - client.io.add((serializer ?? god.serialize)(event.toJson())); + client.channel.sink.add((serializer ?? god.serialize)(event.toJson())); } }); @@ -330,7 +332,8 @@ class AngelWebSocket { ..end(); var ws = await WebSocketTransformer.upgrade(req.io); - var socket = new WebSocketContext(ws, req, res); + var channel = new IOWebSocketChannel(ws); + var socket = new WebSocketContext(channel, req, res); _clients.add(socket); await handleConnect(socket); @@ -364,5 +367,7 @@ class AngelWebSocket { abstract class WebSocketSynchronizer { Stream get stream; + Future close() => new Future.value(); + void notifyOthers(WebSocketEvent e); } diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index 7c97689f..fb4196dd 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -6,8 +6,8 @@ class WebSocketContext { /// Use this to listen for events. _WebSocketEventTable on = new _WebSocketEventTable(); - /// The underlying [WebSocket] instance. - final WebSocket io; + /// The underlying [WebSocketChannel]. + final WebSocketChannel channel; /// The original [RequestContext]. final RequestContext request; @@ -28,15 +28,14 @@ class WebSocketContext { /// Fired once the underlying [WebSocket] closes. Stream get onClose => _onClose.stream; - /// Fired when any data is sent through [io]. + /// Fired when any data is sent through [channel]. Stream get onData => _onData.stream; - WebSocketContext(WebSocket this.io, RequestContext this.request, - ResponseContext this.response); + WebSocketContext(this.channel, this.request, this.response); /// Closes the underlying [WebSocket]. Future close([int code, String reason]) async { - await io.close(code, reason); + await channel.sink.close(code, reason); _onAction.close(); _onData.close(); _onClose.add(null); @@ -45,7 +44,8 @@ class WebSocketContext { /// Sends an arbitrary [WebSocketEvent]; void send(String eventName, data) { - io.add(god.serialize(new WebSocketEvent(eventName: eventName, data: data))); + channel.sink.add( + god.serialize(new WebSocketEvent(eventName: eventName, data: data))); } /// Sends an error event. diff --git a/pubspec.yaml b/pubspec.yaml index 828ce5d3..fc4e576d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.1.0-alpha +version: 1.1.0-alpha+1 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From 75cdcc8853fabd2f50081370c3e292bc6314d3b5 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 19 Oct 2017 18:26:59 -0400 Subject: [PATCH 44/73] DDC --- lib/server.dart | 4 ++-- lib/websocket_controller.dart | 10 +++++----- pubspec.yaml | 2 +- test/controller/common.dart | 2 ++ test/controller/io_test.dart | 2 +- test/service/common.dart | 2 +- web/index.html | 9 +++++++++ web/main.dart | 12 ++++++++++++ 8 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 web/index.html create mode 100644 web/main.dart diff --git a/lib/server.dart b/lib/server.dart index f2374acc..9e58c020 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -90,8 +90,8 @@ class AngelWebSocket { event.eventName = "$path::${event.eventName}"; _filter(WebSocketContext socket) { - if (e.service.properties.containsKey('ws:filter')) - return e.service.properties['ws:filter'](e, socket); + if (e.service.configuration.containsKey('ws:filter')) + return e.service.configuration['ws:filter'](e, socket); else if (e.params != null && e.params.containsKey('ws:filter')) return e.params['ws:filter'](e, socket); else diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index 4aad8d9c..cfdf42ce 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -9,13 +9,15 @@ class ExposeWs { /// A special controller that also supports WebSockets. class WebSocketController extends Controller { + final AngelWebSocket ws; + Map _handlers = {}; Map _handlerSymbols = {}; /// The plug-in instance powering this controller. AngelWebSocket plugin; - WebSocketController() : super(); + WebSocketController(this.ws) : super(); /// Sends an event to all clients. void broadcast(String eventName, data, {filter(WebSocketContext socket)}) { @@ -36,8 +38,8 @@ class WebSocketController extends Controller { onData(data, WebSocketContext socket) {} @override - Future call(Angel app) async { - if (findExpose() != null) await super.call(app); + Future configureServer(Angel app) async { + if (findExpose() != null) await super.configureServer(app); InstanceMirror instanceMirror = reflect(this); ClassMirror classMirror = reflectClass(this.runtimeType); @@ -55,8 +57,6 @@ class WebSocketController extends Controller { } }); - AngelWebSocket ws = app.container.make(AngelWebSocket); - ws.onConnection.listen((socket) async { socket.request ..inject('socket', socket) diff --git a/pubspec.yaml b/pubspec.yaml index fc4e576d..7c1ddf51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.1.0-alpha+1 +version: 1.1.0-alpha+2 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/controller/common.dart b/test/controller/common.dart index d23bbee2..1a8f914a 100644 --- a/test/controller/common.dart +++ b/test/controller/common.dart @@ -20,6 +20,8 @@ const Game JOHN_VS_BOB = const Game(playerOne: 'John', playerTwo: 'Bob'); @Expose('/game') class GameController extends WebSocketController { + GameController(AngelWebSocket ws) : super(ws); + @ExposeWs('search') search(WebSocketContext socket) async { print('User is searching for a game...'); diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart index 7e78b796..2b334249 100644 --- a/test/controller/io_test.dart +++ b/test/controller/io_test.dart @@ -23,7 +23,7 @@ main() { await app.configure(websockets.configureServer); app.all('/ws', websockets.handleRequest); - await app.configure(new GameController()); + await app.configure(new GameController(websockets).configureServer); app.logger = new Logger('angel_auth')..onRecord.listen(print); server = await app.startServer(); diff --git a/test/service/common.dart b/test/service/common.dart index 1abca933..11d265f1 100644 --- a/test/service/common.dart +++ b/test/service/common.dart @@ -13,7 +13,7 @@ class Todo extends Model { class TodoService extends TypedService { TodoService() : super(new MapService()) { - properties['ws:filter'] = (HookedServiceEvent e, WebSocketContext socket) { + configuration['ws:filter'] = (HookedServiceEvent e, WebSocketContext socket) { print('Hello, service filter world!'); return true; }; diff --git a/web/index.html b/web/index.html new file mode 100644 index 00000000..7728ad7e --- /dev/null +++ b/web/index.html @@ -0,0 +1,9 @@ + + + + Client + + + + + \ No newline at end of file diff --git a/web/main.dart b/web/main.dart new file mode 100644 index 00000000..8cab522e --- /dev/null +++ b/web/main.dart @@ -0,0 +1,12 @@ +import 'dart:html'; +import 'package:angel_websocket/browser.dart'; + +/// Dummy app to ensure client works with DDC. +main() { + var app = new WebSockets(window.location.origin); + window.alert(app.basePath); + + app.connect().catchError((_) { + window.alert('no websocket'); + }); +} \ No newline at end of file From d35ebbd0de3e8e90010addc1aca439751b0b3217 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 18 Nov 2017 00:15:29 -0500 Subject: [PATCH 45/73] 1.1.0-alpha+3 --- README.md | 6 +++++- lib/browser.dart | 3 +-- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 78ec9696..19ffa147 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ main() async { var ws = new AngelWebSocket(); - // Apply configuration + // This is a plug-in. It hooks all your services, + // to automatically broadcast events. await app.configure(ws.configureServer); // Listen for requests at `/ws`. @@ -58,6 +59,9 @@ import "package:angel_websocket/server.dart"; @Expose("/") class MyController extends WebSocketController { + // A reference to the WebSocket plug-in is required. + MyController(AngelWebSocket ws):super(ws); + @override void onConnect(WebSocketContext socket) { // On connect... diff --git a/lib/browser.dart b/lib/browser.dart index f1380051..aaacc5aa 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -75,8 +75,7 @@ class WebSockets extends BaseWebSocketClient { return completer.complete(new HtmlWebSocketChannel(socket)); }) ..onError.listen((e) { - var err = e as ErrorEvent; - if (!completer.isCompleted) return completer.completeError(err.error); + if (!completer.isCompleted) return completer.completeError(e is ErrorEvent ? e.error : e); }); return completer.future; diff --git a/pubspec.yaml b/pubspec.yaml index 7c1ddf51..432a9157 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.1.0-alpha+2 +version: 1.1.0-alpha+3 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From d72870a2f1899d5408396c4bf2d1fd4099b3f005 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 25 Nov 2017 15:58:41 -0500 Subject: [PATCH 46/73] CustomEvent -> Event --- lib/browser.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/browser.dart b/lib/browser.dart index aaacc5aa..b9f21f52 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -35,7 +35,7 @@ class WebSockets extends BaseWebSocketClient { var wnd = window.open(url, 'angel_client_auth_popup'); Timer t; - StreamSubscription sub; + StreamSubscription sub; t = new Timer.periodic(new Duration(milliseconds: 500), (timer) { if (!ctrl.isClosed) { if (wnd.closed) { diff --git a/pubspec.yaml b/pubspec.yaml index 432a9157..ba7e2917 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.1.0-alpha+3 +version: 1.1.0-alpha+4 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From caf3dda1b7a31b488a3fb5525fd1c09667f22a02 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 10 Dec 2017 00:31:34 -0500 Subject: [PATCH 47/73] +1 --- CHANGELOG.md | 2 + README.md | 10 ++++ lib/base_websocket_client.dart | 84 +++++++++++++++++++++++++++++++++- pubspec.yaml | 2 +- test/service/common.dart | 8 ++-- 5 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..3daafa79 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +# 1.1.0+1 +* Added `unwrap`. \ No newline at end of file diff --git a/README.md b/README.md index 19ffa147..b97af397 100644 --- a/README.md +++ b/README.md @@ -163,3 +163,13 @@ main() async { app.authenticateViaJwt(''); } ``` +**Unwrapping Events** +In several cases, it may be cumbersome or inconsistent to handle events +as `WebSocketEvent`s. Call `unwrap` to receive a `Service` that returns the underlying `data` +objects. + +```dart +foo() async { + var unwrapped = app.service('api/todos').unwrap(); +} +``` \ No newline at end of file diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index a44780b1..5ff956d6 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -136,8 +136,7 @@ abstract class BaseWebSocketClient extends BaseAngelClient { WebSocketsService service(String path, {Type type, AngelDeserializer deserializer}) { String uri = path.toString().replaceAll(_straySlashes, ''); - return new WebSocketsService(socket, this, uri, - deserializer: deserializer); + return new WebSocketsService(socket, this, uri, deserializer: deserializer); } /// Starts listening for data. @@ -389,6 +388,9 @@ class WebSocketsService extends Service { eventName: '$path::${ACTION_REMOVE}', id: id, params: params ?? {})); return null; } + + /// Returns a wrapper that queries this service, but fires the `data` of `WebSocketEvent`s, rather than the events themselves. + Service unwrap() => new _WebSocketsDataService(this); } /// Contains a dynamic Map of [WebSocketEvent] streams. @@ -409,3 +411,81 @@ class WebSocketExtraneousEventHandler { return _events[index].stream; } } + +class _WebSocketsDataService extends Service { + final WebSocketsService service; + + Stream _onIndexed, _onRead, _onCreated, _onModified, _onUpdated, _onRemoved; + + _WebSocketsDataService(this.service); + + getData(WebSocketEvent e) => e.data; + + @override + Future remove(id, [Map params]) { + return service.remove(id, params); + } + + @override + Future update(id, data, [Map params]) { + return service.update(id, data, params); + } + + @override + Future modify(id, data, [Map params]) { + return service.modify(id, data, params); + } + + @override + Future create(data, [Map params]) { + return service.create(data, params); + } + + @override + Future read(id, [Map params]) { + return service.read(id, params); + } + + @override + Future index([Map params]) { + return service.index(params); + } + + @override + Future close() async {} + + @override + Angel get app { + return service.app; + } + + @override + Stream get onRemoved { + return _onRemoved ??= service.onRemoved.map(getData); + } + + @override + Stream get onUpdated { + return _onUpdated ??= service.onUpdated.map(getData); + } + + @override + Stream get onModified { + return _onModified ??= service.onModified.map(getData); + } + + @override + Stream get onCreated { + return _onCreated ??= service.onCreated.map(getData); + } + + @override + Stream get onRead { + return _onRead ??= service.onRead.map(getData); + } + + @override + Stream get onIndexed { + return _onIndexed ??= service.onIndexed.map(getData); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index ba7e2917..fd545daf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.1.0-alpha+4 +version: 1.1.0+1 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/service/common.dart b/test/service/common.dart index 11d265f1..9e30ab79 100644 --- a/test/service/common.dart +++ b/test/service/common.dart @@ -21,12 +21,12 @@ class TodoService extends TypedService { } testIndex(BaseWebSocketClient client) async { - var Todos = client.service('api/todos'); + var Todos = client.service('api/todos').unwrap(); Todos.index(); var indexed = await Todos.onIndexed.first; - print('indexed: ${indexed.toJson()}'); + print('indexed: $indexed'); - expect(indexed.data, isList); - expect(indexed.data, isEmpty); + expect(indexed, isList); + expect(indexed, isEmpty); } From 23d0ab2105218659f9881d266f87e6c72b668c34 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 21 Dec 2017 15:15:47 -0500 Subject: [PATCH 48/73] 1.1.1 --- CHANGELOG.md | 4 ++ README.md | 20 ++---- lib/base_websocket_client.dart | 123 ++++++--------------------------- pubspec.yaml | 2 +- test/service/common.dart | 2 +- 5 files changed, 33 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3daafa79..5440cb9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,6 @@ +# 1.1.1 +* Deprecated `unwrap`. +* Service streams now pump out `e.data`, rather than the actual event. + # 1.1.0+1 * Added `unwrap`. \ No newline at end of file diff --git a/README.md b/README.md index b97af397..3f09a41e 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ main() async { var Cars = app.service("api/cars"); - Cars.onCreated.listen((e) => print("New car: ${e.data}")); + Cars.onCreated.listen((car) => print("New car: $car")); // Happens asynchronously Cars.create({"brand": "Toyota"}); @@ -148,10 +148,9 @@ main() async { var Cars = app.service("api/cars", type: Car); - Cars.onCreated.listen((e) { + Cars.onCreated.listen((Car car) { // Automatically deserialized into a car :) - Car car = e.data; - + // // I just bought a new 2016 Toyota Camry! print("I just bought a new $car!"); }); @@ -161,15 +160,4 @@ main() async { // Authenticate a WebSocket, if you were not already authenticated... app.authenticateViaJwt(''); -} -``` -**Unwrapping Events** -In several cases, it may be cumbersome or inconsistent to handle events -as `WebSocketEvent`s. Call `unwrap` to receive a `Service` that returns the underlying `data` -objects. - -```dart -foo() async { - var unwrapped = app.service('api/todos').unwrap(); -} -``` \ No newline at end of file +} \ No newline at end of file diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 5ff956d6..4dd793fe 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -240,39 +240,39 @@ class WebSocketsService extends Service { final StreamController _onAllEvents = new StreamController(); - final StreamController _onIndexed = - new StreamController(); - final StreamController _onRead = - new StreamController(); - final StreamController _onCreated = - new StreamController(); - final StreamController _onModified = - new StreamController(); - final StreamController _onUpdated = - new StreamController(); - final StreamController _onRemoved = - new StreamController(); + final StreamController _onIndexed = + new StreamController(); + final StreamController _onRead = + new StreamController(); + final StreamController _onCreated = + new StreamController(); + final StreamController _onModified = + new StreamController(); + final StreamController _onUpdated = + new StreamController(); + final StreamController _onRemoved = + new StreamController(); /// Fired on all events. Stream get onAllEvents => _onAllEvents.stream; /// Fired on `index` events. - Stream get onIndexed => _onIndexed.stream; + Stream get onIndexed => _onIndexed.stream; /// Fired on `read` events. - Stream get onRead => _onRead.stream; + Stream get onRead => _onRead.stream; /// Fired on `created` events. - Stream get onCreated => _onCreated.stream; + Stream get onCreated => _onCreated.stream; /// Fired on `modified` events. - Stream get onModified => _onModified.stream; + Stream get onModified => _onModified.stream; /// Fired on `updated` events. - Stream get onUpdated => _onUpdated.stream; + Stream get onUpdated => _onUpdated.stream; /// Fired on `removed` events. - Stream get onRemoved => _onRemoved.stream; + Stream get onRemoved => _onRemoved.stream; WebSocketsService(this.socket, this.app, this.path, {this.deserializer}) { listen(); @@ -306,7 +306,7 @@ class WebSocketsService extends Service { app.onServiceEvent.listen((map) { if (map.containsKey(path)) { var event = map[path]; - var transformed = transformEvent(event); + var transformed = transformEvent(event).data; _onAllEvents.add(event); @@ -389,8 +389,9 @@ class WebSocketsService extends Service { return null; } - /// Returns a wrapper that queries this service, but fires the `data` of `WebSocketEvent`s, rather than the events themselves. - Service unwrap() => new _WebSocketsDataService(this); + /// No longer necessary. + @deprecated + Service unwrap() => this; } /// Contains a dynamic Map of [WebSocketEvent] streams. @@ -410,82 +411,4 @@ class WebSocketExtraneousEventHandler { return _events[index].stream; } -} - -class _WebSocketsDataService extends Service { - final WebSocketsService service; - - Stream _onIndexed, _onRead, _onCreated, _onModified, _onUpdated, _onRemoved; - - _WebSocketsDataService(this.service); - - getData(WebSocketEvent e) => e.data; - - @override - Future remove(id, [Map params]) { - return service.remove(id, params); - } - - @override - Future update(id, data, [Map params]) { - return service.update(id, data, params); - } - - @override - Future modify(id, data, [Map params]) { - return service.modify(id, data, params); - } - - @override - Future create(data, [Map params]) { - return service.create(data, params); - } - - @override - Future read(id, [Map params]) { - return service.read(id, params); - } - - @override - Future index([Map params]) { - return service.index(params); - } - - @override - Future close() async {} - - @override - Angel get app { - return service.app; - } - - @override - Stream get onRemoved { - return _onRemoved ??= service.onRemoved.map(getData); - } - - @override - Stream get onUpdated { - return _onUpdated ??= service.onUpdated.map(getData); - } - - @override - Stream get onModified { - return _onModified ??= service.onModified.map(getData); - } - - @override - Stream get onCreated { - return _onCreated ??= service.onCreated.map(getData); - } - - @override - Stream get onRead { - return _onRead ??= service.onRead.map(getData); - } - - @override - Stream get onIndexed { - return _onIndexed ??= service.onIndexed.map(getData); - } -} +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index fd545daf..60129c58 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=1.19.0" -version: 1.1.0+1 +version: 1.1.1 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/service/common.dart b/test/service/common.dart index 9e30ab79..4e633106 100644 --- a/test/service/common.dart +++ b/test/service/common.dart @@ -21,7 +21,7 @@ class TodoService extends TypedService { } testIndex(BaseWebSocketClient client) async { - var Todos = client.service('api/todos').unwrap(); + var Todos = client.service('api/todos'); Todos.index(); var indexed = await Todos.onIndexed.first; From 03aae5624bd56d587cdf7f221d6f7a6ce8cd6469 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 10 Jul 2018 12:54:55 -0400 Subject: [PATCH 49/73] 1.1.2 --- CHANGELOG.md | 5 ++ analysis_options.yaml | 5 +- example/main.dart | 18 +++++++ lib/angel_websocket.dart | 6 +-- lib/base_websocket_client.dart | 26 ++++----- lib/browser.dart | 4 +- lib/flutter.dart | 4 +- lib/io.dart | 4 +- lib/server.dart | 96 ++++++++++++++++++++-------------- lib/websocket_context.dart | 2 +- lib/websocket_controller.dart | 2 +- pubspec.yaml | 10 ++-- test/auth_test.dart | 6 ++- test/controller/common.dart | 9 ++-- test/controller/io_test.dart | 8 +-- test/service/io_test.dart | 6 ++- 16 files changed, 130 insertions(+), 81 deletions(-) create mode 100644 example/main.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 5440cb9c..0e12c86b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 1.1.2 +* Dart 2 updates. +* Added `handleClient`, which is nice for external implementations +that plug into `AngelWebSocket`. + # 1.1.1 * Deprecated `unwrap`. * Service streams now pump out `e.data`, rather than the actual event. diff --git a/analysis_options.yaml b/analysis_options.yaml index 716de123..d38a81f8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,3 @@ analyzer: - strong-mode: true - exclude: - - .scripts-bin/**/*.dart \ No newline at end of file + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 00000000..2d80f25b --- /dev/null +++ b/example/main.dart @@ -0,0 +1,18 @@ +import "package:angel_framework/angel_framework.dart"; +import "package:angel_websocket/server.dart"; + +main() async { + var app = new Angel(); + var http = new AngelHttp(app); + var ws = new AngelWebSocket(app); + + // This is a plug-in. It hooks all your services, + // to automatically broadcast events. + await app.configure(ws.configureServer); + + // Listen for requests at `/ws`. + app.all('/ws', ws.handleRequest); + + var server = await http.startServer('127.0.0.1', 3000); + print('Listening at http://${server.address.address}:${server.port}'); +} diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart index 9b47796c..b45078c4 100644 --- a/lib/angel_websocket.dart +++ b/lib/angel_websocket.dart @@ -46,7 +46,7 @@ class WebSocketEvent { WebSocketEvent({String this.eventName, this.data}); factory WebSocketEvent.fromJson(Map data) => - new WebSocketEvent(eventName: data['eventName'], data: data['data']); + new WebSocketEvent(eventName: data['eventName'].toString(), data: data['data']); Map toJson() { return {'eventName': eventName, 'data': data}; @@ -64,8 +64,8 @@ class WebSocketAction { {String this.id, String this.eventName, this.data, this.params}); factory WebSocketAction.fromJson(Map data) => new WebSocketAction( - id: data['id'], - eventName: data['eventName'], + id: data['id'].toString(), + eventName: data['eventName'].toString(), data: data['data'], params: data['params']); diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 4dd793fe..a4870ecb 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:convert'; import 'package:angel_client/angel_client.dart'; import 'package:angel_client/base_angel_client.dart'; import 'package:angel_http_exception/angel_http_exception.dart'; +import 'package:dart2_constant/convert.dart'; import 'package:http/src/base_client.dart' as http; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; @@ -109,7 +109,7 @@ abstract class BaseWebSocketClient extends BaseAngelClient { c.complete(socket); } - }).catchError((e, st) { + }).catchError((e, StackTrace st) { if (!c.isCompleted) { if (timer.isActive) timer.cancel(); c.completeError(e, st); @@ -148,10 +148,10 @@ abstract class BaseWebSocketClient extends BaseAngelClient { if (data is WebSocketChannelException) { _onWebSocketChannelException.add(data); } else if (data is String) { - var json = JSON.decode(data); + var jsons = json.decode(data); - if (json is Map) { - var event = new WebSocketEvent.fromJson(json); + if (jsons is Map) { + var event = new WebSocketEvent.fromJson(jsons); if (event.eventName?.isNotEmpty == true) { _onAllEvents.add(event); @@ -159,10 +159,10 @@ abstract class BaseWebSocketClient extends BaseAngelClient { } if (event.eventName == EVENT_ERROR) { - var error = new AngelHttpException.fromMap(event.data ?? {}); + var error = new AngelHttpException.fromMap((event.data ?? {}) as Map); _onError.add(error); } else if (event.eventName == EVENT_AUTHENTICATED) { - var authResult = new AngelAuthResult.fromMap(event.data); + var authResult = new AngelAuthResult.fromMap(event.data as Map); _onAuthenticated.add(authResult); } else if (event.eventName?.isNotEmpty == true) { var split = event.eventName @@ -199,7 +199,7 @@ abstract class BaseWebSocketClient extends BaseAngelClient { } /// Serializes data to JSON. - serialize(x) => JSON.encode(x); + serialize(x) => json.encode(x); /// Alternative form of [send]ing an action. void send(String eventName, WebSocketAction action) => @@ -289,7 +289,7 @@ class WebSocketsService extends Service { } /// Serializes an [action] to be sent over a WebSocket. - serialize(WebSocketAction action) => JSON.encode(action); + serialize(WebSocketAction action) => json.encode(action); /// Deserializes data from a [WebSocketEvent]. deserialize(x) { @@ -349,7 +349,7 @@ class WebSocketsService extends Service { @override Future read(id, [Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_READ}', id: id, params: params ?? {})); + eventName: '$path::${ACTION_READ}', id: id.toString(), params: params ?? {})); return null; } @@ -366,7 +366,7 @@ class WebSocketsService extends Service { Future modify(id, data, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_MODIFY}', - id: id, + id: id.toString(), data: data, params: params ?? {})); return null; @@ -376,7 +376,7 @@ class WebSocketsService extends Service { Future update(id, data, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_UPDATE}', - id: id, + id: id.toString(), data: data, params: params ?? {})); return null; @@ -385,7 +385,7 @@ class WebSocketsService extends Service { @override Future remove(id, [Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_REMOVE}', id: id, params: params ?? {})); + eventName: '$path::${ACTION_REMOVE}', id: id.toString(), params: params ?? {})); return null; } diff --git a/lib/browser.dart b/lib/browser.dart index b9f21f52..2a35596f 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -52,7 +52,7 @@ class WebSockets extends BaseWebSocketClient { sub = window.on[eventName ?? 'token'].listen((e) { if (!ctrl.isClosed) { - ctrl.add((e as CustomEvent).detail); + ctrl.add((e as CustomEvent).detail.toString()); t.cancel(); ctrl.close(); sub.cancel(); @@ -93,7 +93,7 @@ class WebSockets extends BaseWebSocketClient { class BrowserWebSocketsService extends WebSocketsService { final Type type; - BrowserWebSocketsService(WebSocketChannel socket, Angel app, String uri, + BrowserWebSocketsService(WebSocketChannel socket, WebSockets app, String uri, {this.type, AngelDeserializer deserializer}) : super(socket, app, uri, deserializer: deserializer); } diff --git a/lib/flutter.dart b/lib/flutter.dart index e4ab38ad..37d00350 100644 --- a/lib/flutter.dart +++ b/lib/flutter.dart @@ -10,13 +10,13 @@ import 'base_websocket_client.dart'; export 'package:angel_client/angel_client.dart'; export 'angel_websocket.dart'; -final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); +// final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); /// Queries an Angel server via WebSockets. class WebSockets extends BaseWebSocketClient { final List _services = []; - WebSockets(String path) : super(new http.Client(), path); + WebSockets(String path) : super(new http.IOClient(), path); @override Stream authenticateViaPopup(String url, {String eventName: 'token'}) { diff --git a/lib/io.dart b/lib/io.dart index fa89368e..d791f588 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -19,7 +19,7 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); class WebSockets extends BaseWebSocketClient { final List _services = []; - WebSockets(String path) : super(new http.Client(), path); + WebSockets(String path) : super(new http.IOClient(), path); @override Stream authenticateViaPopup(String url, {String eventName: 'token'}) { @@ -59,7 +59,7 @@ class WebSockets extends BaseWebSocketClient { class IoWebSocketsService extends WebSocketsService { final Type type; - IoWebSocketsService(WebSocketChannel socket, Angel app, String uri, this.type) + IoWebSocketsService(WebSocketChannel socket, WebSockets app, String uri, this.type) : super(socket, app, uri); @override diff --git a/lib/server.dart b/lib/server.dart index 9e58c020..1b9e9362 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -2,11 +2,11 @@ library angel_websocket.server; 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:dart2_constant/convert.dart'; import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; import 'package:web_socket_channel/io.dart'; @@ -18,9 +18,11 @@ part 'websocket_context.dart'; part 'websocket_controller.dart'; +typedef String WebSocketResponseSerializer(data); + /// Broadcasts events from [HookedService]s, and handles incoming [WebSocketAction]s. class AngelWebSocket { - List _clients = []; + List _clients = []; final List _servicesAlreadyWired = []; final StreamController _onAction = @@ -40,7 +42,7 @@ class AngelWebSocket { /// If `true`, then clients can authenticate their WebSockets by sending a valid JWT. final bool allowAuth; - /// Send error information across WebSockets, without including [debug] information.. + /// Send error information across WebSockets, without including debug information.. final bool sendErrors; /// A list of clients currently connected to this server via WebSockets. @@ -66,7 +68,7 @@ class AngelWebSocket { Stream get onDisconnection => _onDisconnect.stream; /// Serializes data to WebSockets. - ResponseSerializer serializer; + WebSocketResponseSerializer serializer; /// Deserializes data from WebSockets. Function deserializer; @@ -82,7 +84,7 @@ class AngelWebSocket { if (deserializer == null) deserializer = (params) => params; } - serviceHook(String path) { + HookedServiceEventListener serviceHook(String path) { return (HookedServiceEvent e) async { if (e.params != null && e.params['broadcast'] == false) return; @@ -107,7 +109,7 @@ class AngelWebSocket { {filter(WebSocketContext socket), bool notify: true}) async { // Default implementation will just immediately fire events _clients.forEach((client) async { - var result = true; + dynamic result = true; if (filter != null) result = await filter(client); if (result == true) { client.channel.sink.add((serializer ?? god.serialize)(event.toJson())); @@ -125,14 +127,18 @@ class AngelWebSocket { Future handleAction(WebSocketAction action, WebSocketContext socket) async { var split = action.eventName.split("::"); - if (split.length < 2) - return socket.sendError(new AngelHttpException.badRequest()); + if (split.length < 2) { + socket.sendError(new AngelHttpException.badRequest()); + return null; + } var service = app.service(split[0]); - if (service == null) - return socket.sendError(new AngelHttpException.notFound( + if (service == null) { + socket.sendError(new AngelHttpException.notFound( message: "No service \"${split[0]}\" exists.")); + return null; + } var actionName = split[1]; @@ -146,7 +152,7 @@ class AngelWebSocket { } var params = mergeMap([ - (deserializer ?? (params) => params)(action.params), + ((deserializer ?? (params) => params)(action.params)) as Map, { "provider": Providers.websocket, '__requestctx': socket.request, @@ -156,11 +162,13 @@ class AngelWebSocket { try { if (actionName == ACTION_INDEX) { - return socket.send( + socket.send( "${split[0]}::" + EVENT_INDEXED, await service.index(params)); + return null; } else if (actionName == ACTION_READ) { - return socket.send("${split[0]}::" + EVENT_READ, + socket.send("${split[0]}::" + EVENT_READ, await service.read(action.id, params)); + return null; } else if (actionName == ACTION_CREATE) { return new WebSocketEvent( eventName: "${split[0]}::" + EVENT_CREATED, @@ -178,8 +186,9 @@ class AngelWebSocket { eventName: "${split[0]}::" + EVENT_REMOVED, data: await service.remove(action.id, params)); } else { - return socket.sendError(new AngelHttpException.methodNotAllowed( + socket.sendError(new AngelHttpException.methodNotAllowed( message: "Method Not Allowed: \"$actionName\"")); + return null; } } catch (e, st) { catchError(e, st, socket); @@ -236,8 +245,8 @@ class AngelWebSocket { handleData(WebSocketContext socket, data) async { try { socket._onData.add(data); - var fromJson = JSON.decode(data); - var action = new WebSocketAction.fromJson(fromJson); + var fromJson = json.decode(data.toString()); + var action = new WebSocketAction.fromJson(fromJson as Map); _onAction.add(action); if (action.eventName == null || @@ -250,7 +259,7 @@ class AngelWebSocket { socket._onAction.add(new WebSocketAction.fromJson(fromJson)); socket.on ._getStreamForEvent(fromJson["eventName"].toString()) - .add(fromJson["data"]); + .add(fromJson["data"] as Map); } if (action.eventName == ACTION_AUTHENTICATE) @@ -261,7 +270,7 @@ class AngelWebSocket { if (split.length >= 2) { if (ACTIONS.contains(split[1])) { - var event = handleAction(action, socket); + var event = await handleAction(action, socket); if (event is Future) event = await event; } } @@ -299,11 +308,11 @@ class AngelWebSocket { return !_servicesAlreadyWired.contains(x) && app.services[x] is HookedService; })) { - hookupService(key, app.services[key]); + hookupService(key, app.services[key] as HookedService); } } - /// Configiures an [Angel] instance to listen for WebSocket connections. + /// Configures an [Angel] instance to listen for WebSocket connections. Future configureServer(Angel app) async { app..container.singleton(this); @@ -320,45 +329,56 @@ class AngelWebSocket { if (synchronizer != null) { synchronizer.stream.listen((e) => batchEvent(e, notify: false)); } + + app.shutdownHooks.add((_) => synchronizer?.close()); } - /// Handles an incoming HTTP request. - Future handleRequest(RequestContext req, ResponseContext res) async { - if (!WebSocketTransformer.isUpgradeRequest(req.io)) - throw new AngelHttpException.badRequest(); - - res - ..willCloseItself = true - ..end(); - - var ws = await WebSocketTransformer.upgrade(req.io); - var channel = new IOWebSocketChannel(ws); - var socket = new WebSocketContext(channel, req, res); + /// Handles an incoming [WebSocketContext]. + Future handleClient(WebSocketContext socket) async { _clients.add(socket); await handleConnect(socket); _onConnection.add(socket); - req + socket.request ..properties['socket'] = socket ..inject(WebSocketContext, socket); - ws.listen( - (data) { + socket.channel.stream.listen( + (data) { _onData.add(data); handleData(socket, data); }, onDone: () { _onDisconnect.add(socket); - _clients.remove(ws); + _clients.remove(socket); }, onError: (e) { _onDisconnect.add(socket); - _clients.remove(ws); + _clients.remove(socket); }, cancelOnError: true, ); - return false; + } + + /// Handles an incoming HTTP request. + Future handleRequest(RequestContext req, ResponseContext res) async { + if (req is HttpRequestContextImpl) { + if (!WebSocketTransformer.isUpgradeRequest(req.io)) + throw new AngelHttpException.badRequest(); + + res + ..willCloseItself = true + ..end(); + + var ws = await WebSocketTransformer.upgrade(req.io); + var channel = new IOWebSocketChannel(ws); + var socket = new WebSocketContext(channel, req, res); + handleClient(socket); + return false; + } else { + throw new ArgumentError('Not an HTTP/1.1 RequestContext: $req'); + } } } diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index fb4196dd..a766ae78 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -55,7 +55,7 @@ class WebSocketContext { class _WebSocketEventTable { Map> _handlers = {}; - StreamController _getStreamForEvent(eventName) { + StreamController _getStreamForEvent(String eventName) { if (!_handlers.containsKey(eventName)) _handlers[eventName] = new StreamController(); return _handlers[eventName]; diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index cfdf42ce..8056458c 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -75,7 +75,7 @@ class WebSocketController extends Controller { if (_handlers.containsKey(action.eventName)) { var methodMirror = _handlers[action.eventName]; var fn = instanceMirror.getField(methodMirror.simpleName).reflectee; - return app.runContained(fn, socket.request, socket.response); + return app.runContained(fn as Function, socket.request, socket.response); } } catch (e, st) { ws.catchError(e, st, socket); diff --git a/pubspec.yaml b/pubspec.yaml index 60129c58..68ab0ab2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,19 +1,19 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: - sdk: ">=1.19.0" -version: 1.1.1 + sdk: ">=1.8.0 <3.0.0" +version: 1.1.2 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: angel_auth: ^1.1.0-alpha angel_client: ^1.1.0-alpha angel_framework: ^1.1.0-alpha - http: ">=0.11.0 <0.12.0" + http: ^0.11.0 json_god: ^2.0.0-beta merge_map: ^1.0.0 meta: ^1.0.0 - uuid: "^0.5.3" - web_socket_channel: "^1.0.0" + uuid: ^0.5.3 + web_socket_channel: ^1.0.0 dev_dependencies: test: "^0.12.15" diff --git a/test/auth_test.dart b/test/auth_test.dart index 0a79c973..d4f6ba7d 100644 --- a/test/auth_test.dart +++ b/test/auth_test.dart @@ -11,11 +11,13 @@ const Map USER = const {'username': 'foo', 'password': 'bar'}; main() { Angel app; + AngelHttp http; c.Angel client; c.WebSockets ws; setUp(() async { app = new Angel(); + http = new AngelHttp(app, useZone: false); var auth = new AngelAuth(); auth.serializer = (_) async => 'baz'; @@ -33,7 +35,7 @@ main() { app.all('/ws', sock.handleRequest); app.logger = new Logger('angel_auth')..onRecord.listen(print); - var server = await app.startServer(); + var server = await http.startServer(); client = new c.Rest('http://${server.address.address}:${server.port}'); ws = new c.WebSockets('ws://${server.address.address}:${server.port}/ws'); await ws.connect(); @@ -41,7 +43,7 @@ main() { tearDown(() { return Future.wait([ - app.close(), + http.close(), client.close(), ws.close(), ]); diff --git a/test/controller/common.dart b/test/controller/common.dart index 1a8f914a..b35094f2 100644 --- a/test/controller/common.dart +++ b/test/controller/common.dart @@ -6,8 +6,9 @@ class Game { const Game({this.playerOne, this.playerTwo}); - factory Game.fromJson(Map data) => - new Game(playerOne: data['playerOne'], playerTwo: data['playerTwo']); + factory Game.fromJson(Map data) => new Game( + playerOne: data['playerOne'].toString(), + playerTwo: data['playerTwo'].toString()); @override bool operator ==(other) => @@ -16,7 +17,7 @@ class Game { other.playerTwo == playerTwo; } -const Game JOHN_VS_BOB = const Game(playerOne: 'John', playerTwo: 'Bob'); +const Game johnVsBob = const Game(playerOne: 'John', playerTwo: 'Bob'); @Expose('/game') class GameController extends WebSocketController { @@ -25,6 +26,6 @@ class GameController extends WebSocketController { @ExposeWs('search') search(WebSocketContext socket) async { print('User is searching for a game...'); - socket.send('searched', JOHN_VS_BOB); + socket.send('searched', johnVsBob); } } diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart index 2b334249..ac6c74ce 100644 --- a/test/controller/io_test.dart +++ b/test/controller/io_test.dart @@ -8,6 +8,7 @@ import 'common.dart'; main() { srv.Angel app; + srv.AngelHttp http; ws.WebSockets client; srv.AngelWebSocket websockets; HttpServer server; @@ -15,6 +16,7 @@ main() { setUp(() async { app = new srv.Angel(); + http = new srv.AngelHttp(app, useZone: false); websockets = new srv.AngelWebSocket(app) ..onData.listen((data) { @@ -26,7 +28,7 @@ main() { await app.configure(new GameController(websockets).configureServer); app.logger = new Logger('angel_auth')..onRecord.listen(print); - server = await app.startServer(); + server = await http.startServer(); url = 'ws://${server.address.address}:${server.port}/ws'; client = new ws.WebSockets(url); @@ -46,7 +48,7 @@ main() { tearDown(() async { await client.close(); - await server.close(force: true); + await http.close(); app = null; client = null; server = null; @@ -58,7 +60,7 @@ main() { client.send('search', new ws.WebSocketAction()); var search = await client.on['searched'].first; print('Searched: ${search.data}'); - expect(new Game.fromJson(search.data), equals(JOHN_VS_BOB)); + expect(new Game.fromJson(search.data as Map), equals(johnVsBob)); }); }); } diff --git a/test/service/io_test.dart b/test/service/io_test.dart index 883a64f2..a35bb5ee 100644 --- a/test/service/io_test.dart +++ b/test/service/io_test.dart @@ -8,6 +8,7 @@ import 'common.dart'; main() { srv.Angel app; + srv.AngelHttp http; ws.WebSockets client; srv.AngelWebSocket websockets; HttpServer server; @@ -15,6 +16,7 @@ main() { setUp(() async { app = new srv.Angel()..use('/api/todos', new TodoService()); + http = new srv.AngelHttp(app, useZone: false); websockets = new srv.AngelWebSocket(app) ..onData.listen((data) { @@ -24,7 +26,7 @@ main() { await app.configure(websockets.configureServer); app.all('/ws', websockets.handleRequest); app.logger = new Logger('angel_auth')..onRecord.listen(print); - server = await app.startServer(); + server = await http.startServer(); url = 'ws://${server.address.address}:${server.port}/ws'; client = new ws.WebSockets(url); @@ -44,7 +46,7 @@ main() { tearDown(() async { await client.close(); - await server.close(force: true); + await http.close(); app = null; client = null; server = null; From ba72a2cf248c54d4bc25318d6d71bd1cd821a67e Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 10 Jul 2018 12:56:18 -0400 Subject: [PATCH 50/73] Require dart2_constant --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 68ab0ab2..083a892a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: angel_auth: ^1.1.0-alpha angel_client: ^1.1.0-alpha angel_framework: ^1.1.0-alpha + dart2_constant: ^1.0.0 http: ^0.11.0 json_god: ^2.0.0-beta merge_map: ^1.0.0 From 0e4c8cc723c2ef4bdfecae75d29f9e33b0c4d4a5 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 10 Jul 2018 12:56:54 -0400 Subject: [PATCH 51/73] Require angel_http_exception --- pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index 083a892a..47f9a01b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: angel_auth: ^1.1.0-alpha angel_client: ^1.1.0-alpha angel_framework: ^1.1.0-alpha + angel_http_exception: ^1.0.0 dart2_constant: ^1.0.0 http: ^0.11.0 json_god: ^2.0.0-beta @@ -17,4 +18,5 @@ dependencies: uuid: ^0.5.3 web_socket_channel: ^1.0.0 dev_dependencies: + logging: any test: "^0.12.15" From 7406f37204bede3a538bb3ce4c9d472c681f7972 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 10 Jul 2018 17:00:56 -0400 Subject: [PATCH 52/73] Update .travis.yml --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index de2210c9..0eb6fac6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,4 @@ -language: dart \ No newline at end of file +language: dart +dart: + - dev + - stable From a1ddf4709858eb136f6ed1267165f7a158f8c837 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 28 Aug 2018 10:17:14 -0400 Subject: [PATCH 53/73] Bump to 2.0.0-alpha --- .gitignore | 1 + lib/server.dart | 30 ++++++++++++------------------ lib/websocket_controller.dart | 14 ++++++++------ pubspec.yaml | 16 +++++++--------- test/controller/io_test.dart | 5 ++++- test/service/common.dart | 12 ++++++------ 6 files changed, 38 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 1403b417..b37504a1 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ crashlytics.properties crashlytics-build.properties fabric.properties +.dart_tool \ No newline at end of file diff --git a/lib/server.dart b/lib/server.dart index 1b9e9362..17b89085 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -202,16 +202,15 @@ class AngelWebSocket { action.params['query'] is Map && action.params['query']['jwt'] is String) { try { - var auth = socket.request.grab(AngelAuth); + var auth = socket.request.container.make(); var jwt = action.params['query']['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.request + ..container.registerSingleton(token) + ..container.registerSingleton(user, as: user.runtimeType as Type); socket.send(EVENT_AUTHENTICATED, {'token': token.serialize(auth.hmac), 'data': user}); } catch (e, st) { @@ -314,10 +313,10 @@ class AngelWebSocket { /// Configures an [Angel] instance to listen for WebSocket connections. Future configureServer(Angel app) async { - app..container.singleton(this); + app..container.registerSingleton(this); if (runtimeType != AngelWebSocket) - app.container.singleton(this, as: AngelWebSocket); + app..container.registerSingleton(this); // Set up services wireAllServices(app); @@ -340,12 +339,10 @@ class AngelWebSocket { _onConnection.add(socket); - socket.request - ..properties['socket'] = socket - ..inject(WebSocketContext, socket); + socket.request.container.registerSingleton(socket); socket.channel.stream.listen( - (data) { + (data) { _onData.add(data); handleData(socket, data); }, @@ -363,15 +360,12 @@ class AngelWebSocket { /// Handles an incoming HTTP request. Future handleRequest(RequestContext req, ResponseContext res) async { - if (req is HttpRequestContextImpl) { - if (!WebSocketTransformer.isUpgradeRequest(req.io)) + if (req is HttpRequestContext && res is HttpResponseContext) { + if (!WebSocketTransformer.isUpgradeRequest(req.rawRequest)) throw new AngelHttpException.badRequest(); - res - ..willCloseItself = true - ..end(); - - var ws = await WebSocketTransformer.upgrade(req.io); + await res.detach(); + var ws = await WebSocketTransformer.upgrade(req.rawRequest); var channel = new IOWebSocketChannel(ws); var socket = new WebSocketContext(channel, req, res); handleClient(socket); diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index 8056458c..7d911fbc 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -39,7 +39,8 @@ class WebSocketController extends Controller { @override Future configureServer(Angel app) async { - if (findExpose() != null) await super.configureServer(app); + if (findExpose(app.container.reflector) != null) + await super.configureServer(app); InstanceMirror instanceMirror = reflect(this); ClassMirror classMirror = reflectClass(this.runtimeType); @@ -58,16 +59,16 @@ class WebSocketController extends Controller { }); ws.onConnection.listen((socket) async { - socket.request - ..inject('socket', socket) - ..inject(WebSocketContext, socket); + if (!socket.request.container.has()) { + socket.request.container.registerSingleton(socket); + } await onConnect(socket); socket.onData.listen((data) => onData(data, socket)); socket.onAction.listen((WebSocketAction action) async { - socket.request.inject(WebSocketAction, action); + socket.request.container.registerSingleton(action); try { await onAction(action, socket); @@ -75,7 +76,8 @@ class WebSocketController extends Controller { if (_handlers.containsKey(action.eventName)) { var methodMirror = _handlers[action.eventName]; var fn = instanceMirror.getField(methodMirror.simpleName).reflectee; - return app.runContained(fn as Function, socket.request, socket.response); + return app.runContained( + fn as Function, socket.request, socket.response); } } catch (e, st) { ws.catchError(e, st, socket); diff --git a/pubspec.yaml b/pubspec.yaml index 47f9a01b..5431df6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,22 +1,20 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: - sdk: ">=1.8.0 <3.0.0" -version: 1.1.2 + sdk: ">=2.0.0-dev <3.0.0" +version: 2.0.0-alpha author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: - angel_auth: ^1.1.0-alpha - angel_client: ^1.1.0-alpha - angel_framework: ^1.1.0-alpha + angel_auth: ^2.0.0-alpha + angel_client: ^2.0.0-alpha + angel_framework: ^2.0.0-alpha angel_http_exception: ^1.0.0 - dart2_constant: ^1.0.0 http: ^0.11.0 json_god: ^2.0.0-beta merge_map: ^1.0.0 meta: ^1.0.0 - uuid: ^0.5.3 web_socket_channel: ^1.0.0 dev_dependencies: - logging: any - test: "^0.12.15" + logging: ^0.11.0 + test: ^1.0.0 diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart index ac6c74ce..2a0d0d52 100644 --- a/test/controller/io_test.dart +++ b/test/controller/io_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:angel_container/mirrors.dart'; 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; @@ -15,7 +16,7 @@ main() { String url; setUp(() async { - app = new srv.Angel(); + app = new srv.Angel(reflector: const MirrorsReflector()); http = new srv.AngelHttp(app, useZone: false); websockets = new srv.AngelWebSocket(app) @@ -34,6 +35,8 @@ main() { client = new ws.WebSockets(url); await client.connect(timeout: new Duration(seconds: 3)); + print('Connected'); + client ..onData.listen((data) { print('Received by client: $data'); diff --git a/test/service/common.dart b/test/service/common.dart index 4e633106..b3aa11e4 100644 --- a/test/service/common.dart +++ b/test/service/common.dart @@ -1,5 +1,5 @@ import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/common.dart'; +import 'package:angel_model/angel_model.dart'; import 'package:angel_websocket/base_websocket_client.dart'; import 'package:angel_websocket/server.dart'; import 'package:test/test.dart'; @@ -11,8 +11,8 @@ class Todo extends Model { Todo({String this.text, String this.when}); } -class TodoService extends TypedService { - TodoService() : super(new MapService()) { +class TodoService extends MapService { + TodoService() : super() { configuration['ws:filter'] = (HookedServiceEvent e, WebSocketContext socket) { print('Hello, service filter world!'); return true; @@ -21,10 +21,10 @@ class TodoService extends TypedService { } testIndex(BaseWebSocketClient client) async { - var Todos = client.service('api/todos'); - Todos.index(); + var todoService = client.service('api/todos'); + todoService.index(); - var indexed = await Todos.onIndexed.first; + var indexed = await todoService.onIndexed.first; print('indexed: $indexed'); expect(indexed, isList); From d48ef88e876c2711f52965b223dfdf4f4f042525 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 28 Aug 2018 10:19:45 -0400 Subject: [PATCH 54/73] CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e12c86b..a4110e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.0-alpha +* Depend on Dart 2 and Angel 2. + # 1.1.2 * Dart 2 updates. * Added `handleClient`, which is nice for external implementations From 11b7b6159edfc12340296639b51bb69a79497abe Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 2 Oct 2018 11:32:06 -0400 Subject: [PATCH 55/73] 2.0.0-alpha.1 --- CHANGELOG.md | 4 +++ lib/angel_websocket.dart | 8 +++--- lib/base_websocket_client.dart | 45 +++++++++++++++++----------------- lib/browser.dart | 3 ++- lib/flutter.dart | 2 +- lib/io.dart | 3 ++- lib/server.dart | 11 +++++---- pubspec.yaml | 4 ++- test/auth_test.dart | 8 +++--- test/service/browser_test.dart | 2 +- test/service/common.dart | 3 ++- web/main.dart | 2 +- 12 files changed, 53 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4110e2b..dddbe931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.0.0-alpha.1 +* Refactorings for updated Angel 2 versions. +* Remove `package:dart2_constant`. + # 2.0.0-alpha * Depend on Dart 2 and Angel 2. diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart index b45078c4..0a3a652b 100644 --- a/lib/angel_websocket.dart +++ b/lib/angel_websocket.dart @@ -45,8 +45,8 @@ class WebSocketEvent { WebSocketEvent({String this.eventName, this.data}); - factory WebSocketEvent.fromJson(Map data) => - new WebSocketEvent(eventName: data['eventName'].toString(), data: data['data']); + factory WebSocketEvent.fromJson(Map data) => new WebSocketEvent( + eventName: data['eventName'].toString(), data: data['data']); Map toJson() { return {'eventName': eventName, 'data': data}; @@ -58,7 +58,7 @@ class WebSocketAction { String id; String eventName; var data; - var params; + Map params; WebSocketAction( {String this.id, String this.eventName, this.data, this.params}); @@ -67,7 +67,7 @@ class WebSocketAction { id: data['id'].toString(), eventName: data['eventName'].toString(), data: data['data'], - params: data['params']); + params: data['params'] as Map); Map toJson() { return {'id': id, 'eventName': eventName, 'data': data, 'params': params}; diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index a4870ecb..1f200b31 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:convert'; import 'package:angel_client/angel_client.dart'; import 'package:angel_client/base_angel_client.dart'; import 'package:angel_http_exception/angel_http_exception.dart'; -import 'package:dart2_constant/convert.dart'; import 'package:http/src/base_client.dart' as http; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; @@ -159,7 +159,8 @@ abstract class BaseWebSocketClient extends BaseAngelClient { } if (event.eventName == EVENT_ERROR) { - var error = new AngelHttpException.fromMap((event.data ?? {}) as Map); + var error = + new AngelHttpException.fromMap((event.data ?? {}) as Map); _onError.add(error); } else if (event.eventName == EVENT_AUTHENTICATED) { var authResult = new AngelAuthResult.fromMap(event.data as Map); @@ -240,18 +241,12 @@ class WebSocketsService extends Service { final StreamController _onAllEvents = new StreamController(); - final StreamController _onIndexed = - new StreamController(); - final StreamController _onRead = - new StreamController(); - final StreamController _onCreated = - new StreamController(); - final StreamController _onModified = - new StreamController(); - final StreamController _onUpdated = - new StreamController(); - final StreamController _onRemoved = - new StreamController(); + final StreamController _onIndexed = new StreamController(); + final StreamController _onRead = new StreamController(); + final StreamController _onCreated = new StreamController(); + final StreamController _onModified = new StreamController(); + final StreamController _onUpdated = new StreamController(); + final StreamController _onRemoved = new StreamController(); /// Fired on all events. Stream get onAllEvents => _onAllEvents.stream; @@ -340,21 +335,23 @@ class WebSocketsService extends Service { } @override - Future index([Map params]) async { + Future index([Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_INDEX}', params: params ?? {})); return null; } @override - Future read(id, [Map params]) async { + Future read(id, [Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_READ}', id: id.toString(), params: params ?? {})); + eventName: '$path::${ACTION_READ}', + id: id.toString(), + params: params ?? {})); return null; } @override - Future create(data, [Map params]) async { + Future create(data, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_CREATE}', data: data, @@ -363,7 +360,7 @@ class WebSocketsService extends Service { } @override - Future modify(id, data, [Map params]) async { + Future modify(id, data, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_MODIFY}', id: id.toString(), @@ -373,7 +370,7 @@ class WebSocketsService extends Service { } @override - Future update(id, data, [Map params]) async { + Future update(id, data, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_UPDATE}', id: id.toString(), @@ -383,9 +380,11 @@ class WebSocketsService extends Service { } @override - Future remove(id, [Map params]) async { + Future remove(id, [Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_REMOVE}', id: id.toString(), params: params ?? {})); + eventName: '$path::${ACTION_REMOVE}', + id: id.toString(), + params: params ?? {})); return null; } @@ -411,4 +410,4 @@ class WebSocketExtraneousEventHandler { return _events[index].stream; } -} \ No newline at end of file +} diff --git a/lib/browser.dart b/lib/browser.dart index 2a35596f..3e64fca2 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -75,7 +75,8 @@ class WebSockets extends BaseWebSocketClient { return completer.complete(new HtmlWebSocketChannel(socket)); }) ..onError.listen((e) { - if (!completer.isCompleted) return completer.completeError(e is ErrorEvent ? e.error : e); + if (!completer.isCompleted) + return completer.completeError(e is ErrorEvent ? e.error : e); }); return completer.future; diff --git a/lib/flutter.dart b/lib/flutter.dart index 37d00350..263e50ed 100644 --- a/lib/flutter.dart +++ b/lib/flutter.dart @@ -41,4 +41,4 @@ class WebSockets extends BaseWebSocketClient { : {}); return new IOWebSocketChannel(socket); } -} \ No newline at end of file +} diff --git a/lib/io.dart b/lib/io.dart index d791f588..02baef5d 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -59,7 +59,8 @@ class WebSockets extends BaseWebSocketClient { class IoWebSocketsService extends WebSocketsService { final Type type; - IoWebSocketsService(WebSocketChannel socket, WebSockets app, String uri, this.type) + IoWebSocketsService( + WebSocketChannel socket, WebSockets app, String uri, this.type) : super(socket, app, uri); @override diff --git a/lib/server.dart b/lib/server.dart index 17b89085..2dbcf198 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -2,11 +2,11 @@ library angel_websocket.server; 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:dart2_constant/convert.dart'; import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; import 'package:web_socket_channel/io.dart'; @@ -132,7 +132,7 @@ class AngelWebSocket { return null; } - var service = app.service(split[0]); + var service = app.findService(split[0]); if (service == null) { socket.sendError(new AngelHttpException.notFound( @@ -142,7 +142,7 @@ class AngelWebSocket { var actionName = split[1]; - if (action.params is! Map) action.params = {}; + if (action.params is! Map) action.params = {}; if (allowClientParams != true) { if (action.params['query'] is Map) @@ -151,8 +151,9 @@ class AngelWebSocket { action.params = {}; } - var params = mergeMap([ - ((deserializer ?? (params) => params)(action.params)) as Map, + var params = mergeMap([ + ((deserializer ?? (params) => params)(action.params)) + as Map, { "provider": Providers.websocket, '__requestctx': socket.request, diff --git a/pubspec.yaml b/pubspec.yaml index 5431df6a..1952d638 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0-alpha +version: 2.0.0-alpha.1 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: @@ -16,5 +16,7 @@ dependencies: meta: ^1.0.0 web_socket_channel: ^1.0.0 dev_dependencies: + angel_container: ^1.0.0-alpha + angel_model: ^1.0.0 logging: ^0.11.0 test: ^1.0.0 diff --git a/test/auth_test.dart b/test/auth_test.dart index d4f6ba7d..d7dab90a 100644 --- a/test/auth_test.dart +++ b/test/auth_test.dart @@ -23,9 +23,11 @@ main() { auth.serializer = (_) async => 'baz'; auth.deserializer = (_) async => USER; - auth.strategies.add(new LocalAuthStrategy((username, password) async { - if (username == 'foo' && password == 'bar') return USER; - })); + auth.strategies['local'] = new LocalAuthStrategy( + (username, password) async { + if (username == 'foo' && password == 'bar') return USER; + }, + ); app.post('/auth/local', auth.authenticate('local')); diff --git a/test/service/browser_test.dart b/test/service/browser_test.dart index 2a91c878..b11d0664 100644 --- a/test/service/browser_test.dart +++ b/test/service/browser_test.dart @@ -2,4 +2,4 @@ import 'package:test/test.dart'; main() { group('service.browser', () {}); -} \ No newline at end of file +} diff --git a/test/service/common.dart b/test/service/common.dart index b3aa11e4..daae99d1 100644 --- a/test/service/common.dart +++ b/test/service/common.dart @@ -13,7 +13,8 @@ class Todo extends Model { class TodoService extends MapService { TodoService() : super() { - configuration['ws:filter'] = (HookedServiceEvent e, WebSocketContext socket) { + configuration['ws:filter'] = + (HookedServiceEvent e, WebSocketContext socket) { print('Hello, service filter world!'); return true; }; diff --git a/web/main.dart b/web/main.dart index 8cab522e..48bd1e27 100644 --- a/web/main.dart +++ b/web/main.dart @@ -9,4 +9,4 @@ main() { app.connect().catchError((_) { window.alert('no websocket'); }); -} \ No newline at end of file +} From 6674353a53451da9fea192354bbf15f7fdf7169d Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 21 Oct 2018 04:15:51 -0400 Subject: [PATCH 56/73] 2.0.0-alpha.2 --- CHANGELOG.md | 3 +++ lib/angel_websocket.dart | 16 ++++++++++---- lib/base_websocket_client.dart | 39 +++++++++++++++++----------------- lib/browser.dart | 10 ++++----- lib/io.dart | 10 ++++----- lib/websocket_controller.dart | 5 +++-- pubspec.yaml | 2 +- 7 files changed, 49 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dddbe931..00ef5cc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.0-alpha.2 +* Updated for the next version of `angel_client`. + # 2.0.0-alpha.1 * Refactorings for updated Angel 2 versions. * Remove `package:dart2_constant`. diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart index 0a3a652b..b7dfc04c 100644 --- a/lib/angel_websocket.dart +++ b/lib/angel_websocket.dart @@ -39,16 +39,24 @@ const List EVENTS = const [ ]; /// A notification from the server that something has occurred. -class WebSocketEvent { +class WebSocketEvent { String eventName; - var data; + Data data; WebSocketEvent({String this.eventName, this.data}); factory WebSocketEvent.fromJson(Map data) => new WebSocketEvent( - eventName: data['eventName'].toString(), data: data['data']); + eventName: data['eventName'].toString(), data: data['data'] as Data); - Map toJson() { + WebSocketEvent cast() { + if (T == Data) { + return this as WebSocketEvent; + } else { + return new WebSocketEvent(eventName: eventName, data: data as T); + } + } + + Map toJson() { return {'eventName': eventName, 'data': data}; } } diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 1f200b31..fd4d4997 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -133,10 +133,11 @@ abstract class BaseWebSocketClient extends BaseAngelClient { Future getConnectedWebSocket(); @override - WebSocketsService service(String path, - {Type type, AngelDeserializer deserializer}) { + WebSocketsService service(String path, + {Type type, AngelDeserializer deserializer}) { String uri = path.toString().replaceAll(_straySlashes, ''); - return new WebSocketsService(socket, this, uri, deserializer: deserializer); + return new WebSocketsService(socket, this, uri, + deserializer: deserializer); } /// Starts listening for data. @@ -225,13 +226,13 @@ abstract class BaseWebSocketClient extends BaseAngelClient { } /// A [Service] that asynchronously interacts with the server. -class WebSocketsService extends Service { +class WebSocketsService extends Service { /// The [BaseWebSocketClient] that spawned this service. @override final BaseWebSocketClient app; /// Used to deserialize JSON into typed data. - final AngelDeserializer deserializer; + final AngelDeserializer deserializer; /// The [WebSocketChannel] to listen to, and send data across. final WebSocketChannel socket; @@ -242,11 +243,11 @@ class WebSocketsService extends Service { final StreamController _onAllEvents = new StreamController(); final StreamController _onIndexed = new StreamController(); - final StreamController _onRead = new StreamController(); - final StreamController _onCreated = new StreamController(); - final StreamController _onModified = new StreamController(); - final StreamController _onUpdated = new StreamController(); - final StreamController _onRemoved = new StreamController(); + final StreamController _onRead = new StreamController(); + final StreamController _onCreated = new StreamController(); + final StreamController _onModified = new StreamController(); + final StreamController _onUpdated = new StreamController(); + final StreamController _onRemoved = new StreamController(); /// Fired on all events. Stream get onAllEvents => _onAllEvents.stream; @@ -255,19 +256,19 @@ class WebSocketsService extends Service { Stream get onIndexed => _onIndexed.stream; /// Fired on `read` events. - Stream get onRead => _onRead.stream; + Stream get onRead => _onRead.stream; /// Fired on `created` events. - Stream get onCreated => _onCreated.stream; + Stream get onCreated => _onCreated.stream; /// Fired on `modified` events. - Stream get onModified => _onModified.stream; + Stream get onModified => _onModified.stream; /// Fired on `updated` events. - Stream get onUpdated => _onUpdated.stream; + Stream get onUpdated => _onUpdated.stream; /// Fired on `removed` events. - Stream get onRemoved => _onRemoved.stream; + Stream get onRemoved => _onRemoved.stream; WebSocketsService(this.socket, this.app, this.path, {this.deserializer}) { listen(); @@ -287,12 +288,12 @@ class WebSocketsService extends Service { serialize(WebSocketAction action) => json.encode(action); /// Deserializes data from a [WebSocketEvent]. - deserialize(x) { - return deserializer != null ? deserializer(x) : x; + Data deserialize(x) { + return deserializer != null ? deserializer(x) : x as Data; } /// Deserializes the contents of an [event]. - WebSocketEvent transformEvent(WebSocketEvent event) { + WebSocketEvent transformEvent(WebSocketEvent event) { return event..data = deserialize(event.data); } @@ -300,7 +301,7 @@ class WebSocketsService extends Service { void listen() { app.onServiceEvent.listen((map) { if (map.containsKey(path)) { - var event = map[path]; + var event = map[path].cast(); var transformed = transformEvent(event).data; _onAllEvents.add(event); diff --git a/lib/browser.dart b/lib/browser.dart index 3e64fca2..a04f8be1 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -83,18 +83,18 @@ class WebSockets extends BaseWebSocketClient { } @override - BrowserWebSocketsService service(String path, - {Type type, AngelDeserializer deserializer}) { + BrowserWebSocketsService service(String path, + {Type type, AngelDeserializer deserializer}) { String uri = path.replaceAll(_straySlashes, ''); - return new BrowserWebSocketsService(socket, this, uri, + return new BrowserWebSocketsService(socket, this, uri, deserializer: deserializer); } } -class BrowserWebSocketsService extends WebSocketsService { +class BrowserWebSocketsService extends WebSocketsService { final Type type; BrowserWebSocketsService(WebSocketChannel socket, WebSockets app, String uri, - {this.type, AngelDeserializer deserializer}) + {this.type, AngelDeserializer deserializer}) : super(socket, app, uri, deserializer: deserializer); } diff --git a/lib/io.dart b/lib/io.dart index 02baef5d..4c5a7c9c 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -46,17 +46,17 @@ class WebSockets extends BaseWebSocketClient { } @override - IoWebSocketsService service(String path, - {Type type, AngelDeserializer deserializer}) { + IoWebSocketsService service(String path, + {Type type, AngelDeserializer deserializer}) { String uri = path.replaceAll(_straySlashes, ''); - return new IoWebSocketsService(socket, this, uri, type); + return new IoWebSocketsService(socket, this, uri, type); } @override serialize(x) => god.serialize(x); } -class IoWebSocketsService extends WebSocketsService { +class IoWebSocketsService extends WebSocketsService { final Type type; IoWebSocketsService( @@ -69,7 +69,7 @@ class IoWebSocketsService extends WebSocketsService { @override deserialize(x) { if (type != null && type != dynamic) { - return god.deserializeDatum(x, outputType: type); + return god.deserializeDatum(x, outputType: type) as Data; } else return super.deserialize(x); } diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index 7d911fbc..b214acbd 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -68,7 +68,8 @@ class WebSocketController extends Controller { socket.onData.listen((data) => onData(data, socket)); socket.onAction.listen((WebSocketAction action) async { - socket.request.container.registerSingleton(action); + var container = socket.request.container.createChild(); + container.registerSingleton(action); try { await onAction(action, socket); @@ -77,7 +78,7 @@ class WebSocketController extends Controller { var methodMirror = _handlers[action.eventName]; var fn = instanceMirror.getField(methodMirror.simpleName).reflectee; return app.runContained( - fn as Function, socket.request, socket.response); + fn as Function, socket.request, socket.response, container); } } catch (e, st) { ws.catchError(e, st, socket); diff --git a/pubspec.yaml b/pubspec.yaml index 1952d638..4c1670f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0-alpha.1 +version: 2.0.0-alpha.2 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From 17ef138a282e344465e1ca4ca2a78e4be628f790 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 22 Oct 2018 19:01:24 -0400 Subject: [PATCH 57/73] bump --- CHANGELOG.md | 3 +++ example/main.dart | 1 + lib/server.dart | 1 + pubspec.yaml | 2 +- test/auth_test.dart | 1 + test/controller/io_test.dart | 1 + test/service/io_test.dart | 1 + 7 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00ef5cc7..ab3dbd84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.0-alpha.3 +* Directly import Angel HTTP. + # 2.0.0-alpha.2 * Updated for the next version of `angel_client`. diff --git a/example/main.dart b/example/main.dart index 2d80f25b..28ca4aca 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,4 +1,5 @@ import "package:angel_framework/angel_framework.dart"; +import "package:angel_framework/http.dart"; import "package:angel_websocket/server.dart"; main() async { diff --git a/lib/server.dart b/lib/server.dart index 2dbcf198..ea8897eb 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'dart:mirrors'; import 'package:angel_auth/angel_auth.dart'; import 'package:angel_framework/angel_framework.dart'; +import "package:angel_framework/http.dart"; import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; import 'package:web_socket_channel/io.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 4c1670f4..45f18c05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0-alpha.2 +version: 2.0.0-alpha.3 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: diff --git a/test/auth_test.dart b/test/auth_test.dart index d7dab90a..8a8567ca 100644 --- a/test/auth_test.dart +++ b/test/auth_test.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:angel_auth/angel_auth.dart'; import 'package:angel_client/io.dart' as c; import 'package:angel_framework/angel_framework.dart'; +import "package:angel_framework/http.dart"; import 'package:angel_websocket/io.dart' as c; import 'package:angel_websocket/server.dart'; import 'package:logging/logging.dart'; diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart index 2a0d0d52..6f53b980 100644 --- a/test/controller/io_test.dart +++ b/test/controller/io_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:angel_container/mirrors.dart'; import 'package:angel_framework/angel_framework.dart' as srv; +import "package:angel_framework/http.dart" as srv; import 'package:angel_websocket/io.dart' as ws; import 'package:angel_websocket/server.dart' as srv; import 'package:logging/logging.dart'; diff --git a/test/service/io_test.dart b/test/service/io_test.dart index a35bb5ee..318f1417 100644 --- a/test/service/io_test.dart +++ b/test/service/io_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:angel_framework/angel_framework.dart' as srv; +import "package:angel_framework/http.dart" as srv; import 'package:angel_websocket/io.dart' as ws; import 'package:angel_websocket/server.dart' as srv; import 'package:logging/logging.dart'; From 122bf171621c993f797b76203684c54fe4b60128 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 3 Nov 2018 22:04:50 -0400 Subject: [PATCH 58/73] Update --- lib/base_websocket_client.dart | 38 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index fd4d4997..8dd5c9e1 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -242,7 +242,7 @@ class WebSocketsService extends Service { final StreamController _onAllEvents = new StreamController(); - final StreamController _onIndexed = new StreamController(); + final StreamController> _onIndexed = new StreamController(); final StreamController _onRead = new StreamController(); final StreamController _onCreated = new StreamController(); final StreamController _onModified = new StreamController(); @@ -253,7 +253,7 @@ class WebSocketsService extends Service { Stream get onAllEvents => _onAllEvents.stream; /// Fired on `index` events. - Stream get onIndexed => _onIndexed.stream; + Stream> get onIndexed => _onIndexed.stream; /// Fired on `read` events. Stream get onRead => _onRead.stream; @@ -293,23 +293,31 @@ class WebSocketsService extends Service { } /// Deserializes the contents of an [event]. - WebSocketEvent transformEvent(WebSocketEvent event) { - return event..data = deserialize(event.data); + WebSocketEvent transformEvent(WebSocketEvent event) { + return new WebSocketEvent( + eventName: event.eventName, data: deserialize(event.data)); } /// Starts listening for events. void listen() { app.onServiceEvent.listen((map) { if (map.containsKey(path)) { - var event = map[path].cast(); - var transformed = transformEvent(event).data; + var event = map[path]; _onAllEvents.add(event); + if (event.eventName == EVENT_INDEXED) { + var d = event.data; + var transformed = new WebSocketEvent( + eventName: event.eventName, + data: d is Iterable ? d.map(deserialize).toList() : null); + if (transformed.data != null) _onIndexed.add(transformed.data); + return; + } + + var transformed = transformEvent(event).data; + switch (event.eventName) { - case EVENT_INDEXED: - _onIndexed.add(transformed); - break; case EVENT_READ: _onRead.add(transformed); break; @@ -336,14 +344,14 @@ class WebSocketsService extends Service { } @override - Future index([Map params]) async { + Future> index([Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_INDEX}', params: params ?? {})); return null; } @override - Future read(id, [Map params]) async { + Future read(id, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_READ}', id: id.toString(), @@ -352,7 +360,7 @@ class WebSocketsService extends Service { } @override - Future create(data, [Map params]) async { + Future create(data, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_CREATE}', data: data, @@ -361,7 +369,7 @@ class WebSocketsService extends Service { } @override - Future modify(id, data, [Map params]) async { + Future modify(id, data, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_MODIFY}', id: id.toString(), @@ -371,7 +379,7 @@ class WebSocketsService extends Service { } @override - Future update(id, data, [Map params]) async { + Future update(id, data, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_UPDATE}', id: id.toString(), @@ -381,7 +389,7 @@ class WebSocketsService extends Service { } @override - Future remove(id, [Map params]) async { + Future remove(id, [Map params]) async { app.sendAction(new WebSocketAction( eventName: '$path::${ACTION_REMOVE}', id: id.toString(), From ee93f87677a8a90c4d1a3226ad4f87a1f24d43a0 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 3 Nov 2018 22:05:10 -0400 Subject: [PATCH 59/73] changelog --- CHANGELOG.md | 3 +++ pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab3dbd84..9dbea851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.0-alpha.4 +* Strong typing updates. + # 2.0.0-alpha.3 * Directly import Angel HTTP. diff --git a/pubspec.yaml b/pubspec.yaml index 45f18c05..6595ee8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0-alpha.3 +version: 2.0.0-alpha.4 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From f27b4ab85a835dca217316e930291367ba6aa923 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 3 Nov 2018 22:11:52 -0400 Subject: [PATCH 60/73] Bump to 2.0.0-alpha.4 --- CHANGELOG.md | 2 ++ lib/server.dart | 8 +++----- lib/websocket_context.dart | 14 +++++++------- pubspec.yaml | 1 - 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dbea851..ddd950ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # 2.0.0-alpha.4 +* Remove `package:json_god`. +* Make `WebSocketContext` take any `StreamChannel`. * Strong typing updates. # 2.0.0-alpha.3 diff --git a/lib/server.dart b/lib/server.dart index ea8897eb..1fcde714 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -8,10 +8,9 @@ import 'dart:mirrors'; import 'package:angel_auth/angel_auth.dart'; import 'package:angel_framework/angel_framework.dart'; import "package:angel_framework/http.dart"; -import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; +import 'package:stream_channel/stream_channel.dart'; import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; import 'angel_websocket.dart'; export 'angel_websocket.dart'; @@ -81,7 +80,7 @@ class AngelWebSocket { this.synchronizer, this.serializer, this.deserializer}) { - if (serializer == null) serializer = god.serialize; + if (serializer == null) serializer = json.encode; if (deserializer == null) deserializer = (params) => params; } @@ -113,7 +112,7 @@ class AngelWebSocket { dynamic result = true; if (filter != null) result = await filter(client); if (result == true) { - client.channel.sink.add((serializer ?? god.serialize)(event.toJson())); + client.channel.sink.add((serializer ?? json.encode)(event.toJson())); } }); @@ -365,7 +364,6 @@ class AngelWebSocket { if (req is HttpRequestContext && res is HttpResponseContext) { if (!WebSocketTransformer.isUpgradeRequest(req.rawRequest)) throw new AngelHttpException.badRequest(); - await res.detach(); var ws = await WebSocketTransformer.upgrade(req.rawRequest); var channel = new IOWebSocketChannel(ws); diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index a766ae78..74310bcb 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -6,8 +6,8 @@ class WebSocketContext { /// Use this to listen for events. _WebSocketEventTable on = new _WebSocketEventTable(); - /// The underlying [WebSocketChannel]. - final WebSocketChannel channel; + /// The underlying [StreamChannel]. + final StreamChannel channel; /// The original [RequestContext]. final RequestContext request; @@ -33,9 +33,9 @@ class WebSocketContext { WebSocketContext(this.channel, this.request, this.response); - /// Closes the underlying [WebSocket]. - Future close([int code, String reason]) async { - await channel.sink.close(code, reason); + /// Closes the underlying [StreamChannel]. + Future close() async { + await channel.sink.close(); _onAction.close(); _onData.close(); _onClose.add(null); @@ -44,8 +44,8 @@ class WebSocketContext { /// Sends an arbitrary [WebSocketEvent]; void send(String eventName, data) { - channel.sink.add( - god.serialize(new WebSocketEvent(eventName: eventName, data: data))); + channel.sink + .add(json.encode(new WebSocketEvent(eventName: eventName, data: data))); } /// Sends an error event. diff --git a/pubspec.yaml b/pubspec.yaml index 6595ee8e..e3216137 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,6 @@ dependencies: angel_framework: ^2.0.0-alpha angel_http_exception: ^1.0.0 http: ^0.11.0 - json_god: ^2.0.0-beta merge_map: ^1.0.0 meta: ^1.0.0 web_socket_channel: ^1.0.0 From af001414702bf0f16cf669ae4afe3f0c59d80d3d Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 3 Nov 2018 22:17:33 -0400 Subject: [PATCH 61/73] Purge json_god --- lib/angel_websocket.dart | 2 +- lib/io.dart | 16 ---------------- lib/websocket_context.dart | 4 ++-- test/controller/common.dart | 4 ++++ 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart index b7dfc04c..1f9b6e94 100644 --- a/lib/angel_websocket.dart +++ b/lib/angel_websocket.dart @@ -77,7 +77,7 @@ class WebSocketAction { data: data['data'], params: data['params'] as Map); - Map toJson() { + Map toJson() { return {'id': id, 'eventName': eventName, 'data': data, 'params': params}; } } diff --git a/lib/io.dart b/lib/io.dart index 4c5a7c9c..efd95c21 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -5,10 +5,8 @@ import 'dart:async'; import 'dart:io'; import 'package:angel_client/angel_client.dart'; import 'package:http/http.dart' as http; -import 'package:json_god/json_god.dart' as god; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/io.dart'; -import 'angel_websocket.dart'; import 'base_websocket_client.dart'; export 'package:angel_client/angel_client.dart'; export 'angel_websocket.dart'; @@ -51,9 +49,6 @@ class WebSockets extends BaseWebSocketClient { String uri = path.replaceAll(_straySlashes, ''); return new IoWebSocketsService(socket, this, uri, type); } - - @override - serialize(x) => god.serialize(x); } class IoWebSocketsService extends WebSocketsService { @@ -62,15 +57,4 @@ class IoWebSocketsService extends WebSocketsService { IoWebSocketsService( WebSocketChannel socket, WebSockets app, String uri, this.type) : 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) as Data; - } else - return super.deserialize(x); - } } diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index 74310bcb..4630fd59 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -44,8 +44,8 @@ class WebSocketContext { /// Sends an arbitrary [WebSocketEvent]; void send(String eventName, data) { - channel.sink - .add(json.encode(new WebSocketEvent(eventName: eventName, data: data))); + channel.sink.add(json + .encode(new WebSocketEvent(eventName: eventName, data: data).toJson())); } /// Sends an error event. diff --git a/test/controller/common.dart b/test/controller/common.dart index b35094f2..8e624d73 100644 --- a/test/controller/common.dart +++ b/test/controller/common.dart @@ -10,6 +10,10 @@ class Game { playerOne: data['playerOne'].toString(), playerTwo: data['playerTwo'].toString()); + Map toJson() { + return {'playerOne': playerOne, 'playerTwo': playerTwo}; + } + @override bool operator ==(other) => other is Game && From cbafb091eb117c839d334c775a86e87eaaa2c6a6 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 3 Nov 2018 22:18:16 -0400 Subject: [PATCH 62/73] deps --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index e3216137..4db02e6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: http: ^0.11.0 merge_map: ^1.0.0 meta: ^1.0.0 + stream_channel: ^1.0.0 web_socket_channel: ^1.0.0 dev_dependencies: angel_container: ^1.0.0-alpha From 11877cc0d8cea6d9cd2a83abd4556a539aaa7f37 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 3 Nov 2018 22:45:37 -0400 Subject: [PATCH 63/73] http --- CHANGELOG.md | 3 +++ pubspec.yaml | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddd950ac..04dbb7ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.0-alpha.5 +* Update `http` dependency. + # 2.0.0-alpha.4 * Remove `package:json_god`. * Make `WebSocketContext` take any `StreamChannel`. diff --git a/pubspec.yaml b/pubspec.yaml index 4db02e6d..09684a33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0-alpha.4 +version: 2.0.0-alpha.5 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: @@ -10,7 +10,7 @@ dependencies: angel_client: ^2.0.0-alpha angel_framework: ^2.0.0-alpha angel_http_exception: ^1.0.0 - http: ^0.11.0 + http: ">=0.11.0 <2.0.0" merge_map: ^1.0.0 meta: ^1.0.0 stream_channel: ^1.0.0 From 7092cb764e52cd0f73c91faf17858913713ab2a4 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 8 Nov 2018 16:53:52 -0500 Subject: [PATCH 64/73] 2.0.0-alpha.6 - http fixes --- CHANGELOG.md | 3 +++ lib/flutter.dart | 1 + lib/io.dart | 1 + pubspec.yaml | 4 ++-- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04dbb7ce..aff35596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.0-alpha.6 +* Explicit import of `import 'package:http/io_client.dart' as http;` + # 2.0.0-alpha.5 * Update `http` dependency. diff --git a/lib/flutter.dart b/lib/flutter.dart index 263e50ed..96e7fdfd 100644 --- a/lib/flutter.dart +++ b/lib/flutter.dart @@ -4,6 +4,7 @@ library angel_websocket.flutter; import 'dart:async'; import 'dart:io'; import 'package:http/http.dart' as http; +import 'package:http/io_client.dart' as http; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/io.dart'; import 'base_websocket_client.dart'; diff --git a/lib/io.dart b/lib/io.dart index efd95c21..47f94166 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:io'; import 'package:angel_client/angel_client.dart'; import 'package:http/http.dart' as http; +import 'package:http/io_client.dart' as http; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/io.dart'; import 'base_websocket_client.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 09684a33..7205679a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0-alpha.5 +version: 2.0.0-alpha.6 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: @@ -10,7 +10,7 @@ dependencies: angel_client: ^2.0.0-alpha angel_framework: ^2.0.0-alpha angel_http_exception: ^1.0.0 - http: ">=0.11.0 <2.0.0" + http: ">=0.11.0 <0.13.0" merge_map: ^1.0.0 meta: ^1.0.0 stream_channel: ^1.0.0 From b767b3583f12eed00d5d41c3e23b92dd17720b8e Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 15 Nov 2018 11:43:51 -0500 Subject: [PATCH 65/73] sync -> streamchannel --- CHANGELOG.md | 3 +++ lib/server.dart | 24 +++++++----------------- pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aff35596..8958e40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.0-alpha.7 +* Replace `WebSocketSynchronizer` with `StreamChannel`. + # 2.0.0-alpha.6 * Explicit import of `import 'package:http/io_client.dart' as http;` diff --git a/lib/server.dart b/lib/server.dart index 1fcde714..75295e5f 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -53,7 +53,7 @@ class AngelWebSocket { new List.unmodifiable(_servicesAlreadyWired); /// Used to notify other nodes of an event's firing. Good for scaled applications. - final WebSocketSynchronizer synchronizer; + final StreamChannel synchronizationChannel; /// Fired on any [WebSocketAction]. Stream get onAction => _onAction.stream; @@ -77,7 +77,7 @@ class AngelWebSocket { {this.sendErrors: false, this.allowClientParams: false, this.allowAuth: true, - this.synchronizer, + this.synchronizationChannel, this.serializer, this.deserializer}) { if (serializer == null) serializer = json.encode; @@ -116,8 +116,8 @@ class AngelWebSocket { } }); - if (synchronizer != null && notify != false) - synchronizer.notifyOthers(event); + if (synchronizationChannel != null && notify != false) + synchronizationChannel.sink.add(event); } /// Returns a list of events yet to be sent. @@ -326,11 +326,11 @@ class AngelWebSocket { wireAllServices(app); }); - if (synchronizer != null) { - synchronizer.stream.listen((e) => batchEvent(e, notify: false)); + if (synchronizationChannel != null) { + synchronizationChannel.stream.listen((e) => batchEvent(e, notify: false)); } - app.shutdownHooks.add((_) => synchronizer?.close()); + app.shutdownHooks.add((_) => synchronizationChannel?.sink?.close()); } /// Handles an incoming [WebSocketContext]. @@ -375,13 +375,3 @@ class AngelWebSocket { } } } - -/// Notifies other nodes of outgoing WWebSocket events, and listens for -/// notifications from other nodes. -abstract class WebSocketSynchronizer { - Stream get stream; - - Future close() => new Future.value(); - - void notifyOthers(WebSocketEvent e); -} diff --git a/pubspec.yaml b/pubspec.yaml index 7205679a..582696c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0-alpha.6 +version: 2.0.0-alpha.7 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From 6a5a4c35ffcb383219ad7c13b41617d1eaf24537 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 9 Dec 2018 11:40:09 -0500 Subject: [PATCH 66/73] 2.0.0-alpha.8 --- CHANGELOG.md | 3 ++ dev.key | 29 ++++++++++++++++ dev.pem | 57 +++++++++++++++++++++++++++++++ example/index.html | 30 +++++++++++++++++ example/main.dart | 60 ++++++++++++++++++++++++++++----- lib/server.dart | 83 +++++++++++++++++++++++++++++++++++++++++++--- pubspec.yaml | 2 +- 7 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 dev.key create mode 100644 dev.pem create mode 100644 example/index.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 8958e40a..b77928ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.0-alpha.8 +* Support for WebSockets over HTTP/2 (though in practice this doesn't often happen, if ever). + # 2.0.0-alpha.7 * Replace `WebSocketSynchronizer` with `StreamChannel`. diff --git a/dev.key b/dev.key new file mode 100644 index 00000000..5d49ae7e --- /dev/null +++ b/dev.key @@ -0,0 +1,29 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP +xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE +ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5 +Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1 +qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc +gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU +0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF +gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS +oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn +oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ +kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh +zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa +J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe +d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX +TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76 +ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW +HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN +goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im +EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j +ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS +YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3 +q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT +Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z +Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH +QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE +xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w +AUukhVtTNn4= +-----END ENCRYPTED PRIVATE KEY----- \ No newline at end of file diff --git a/dev.pem b/dev.pem new file mode 100644 index 00000000..01756b25 --- /dev/null +++ b/dev.pem @@ -0,0 +1,57 @@ +-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV +BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa +MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq +Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu +EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki +we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb +N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI +7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg +hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O +BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS +YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd +AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4 +CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM +4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG +MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5 +V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV +BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw +WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx +EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP +DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE +YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu +MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7 +B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd +IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb +oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC +cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8 +x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ +e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX +NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4 +0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh +FKvRDxsW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV +BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw +WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv +dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw +siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj +kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2 +hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV +DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU +ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD +26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ +lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X +J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/ +uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE +4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k +t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W +r6AL284qtw== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/example/index.html b/example/index.html new file mode 100644 index 00000000..d2f4a4ca --- /dev/null +++ b/example/index.html @@ -0,0 +1,30 @@ + + + + + + + Angel WS + + + + + diff --git a/example/main.dart b/example/main.dart index 28ca4aca..0c843aee 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,19 +1,61 @@ -import "package:angel_framework/angel_framework.dart"; -import "package:angel_framework/http.dart"; -import "package:angel_websocket/server.dart"; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/http.dart'; +import 'package:angel_framework/http2.dart'; +import 'package:angel_websocket/server.dart'; +import 'package:file/local.dart'; +import 'package:logging/logging.dart'; -main() async { +main(List args) async { var app = new Angel(); var http = new AngelHttp(app); - var ws = new AngelWebSocket(app); + var ws = new AngelWebSocket(app, sendErrors: !app.isProduction); + var fs = const LocalFileSystem(); + app.logger = new Logger('angel_auth') + ..onRecord.listen((rec) { + print(rec); + if (rec.error != null) print(rec.error); + if (rec.stackTrace != null) print(rec.stackTrace); + }); // This is a plug-in. It hooks all your services, // to automatically broadcast events. await app.configure(ws.configureServer); - // Listen for requests at `/ws`. - app.all('/ws', ws.handleRequest); + app.get('/', (req, res) => res.streamFile(fs.file('example/index.html'))); - var server = await http.startServer('127.0.0.1', 3000); - print('Listening at http://${server.address.address}:${server.port}'); + // Listen for requests at `/ws`. + app.get('/ws', ws.handleRequest); + + app.fallback((req, res) => throw AngelHttpException.notFound()); + + ws.onConnection.listen((socket) { + socket.onData.listen((x) { + socket.send('pong', x); + }); + }); + + if (args.contains('http2')) { + var ctx = new SecurityContext() + ..useCertificateChain('dev.pem') + ..usePrivateKey('dev.key', password: 'dartdart'); + + try { + ctx.setAlpnProtocols(['h2'], true); + } catch (e, st) { + app.logger.severe( + 'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.', + e, + st, + ); + } + + var http2 = new AngelHttp2(app, ctx); + http2.onHttp1.forEach(http.handleRequest); + await http2.startServer('127.0.0.1', 3000); + print('Listening at ${http2.uri}'); + } else { + await http.startServer('127.0.0.1', 3000); + print('Listening at ${http.uri}'); + } } diff --git a/lib/server.dart b/lib/server.dart index 75295e5f..de675fd4 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -7,10 +7,12 @@ import 'dart:io'; import 'dart:mirrors'; import 'package:angel_auth/angel_auth.dart'; import 'package:angel_framework/angel_framework.dart'; -import "package:angel_framework/http.dart"; +import 'package:angel_framework/http.dart'; +import 'package:angel_framework/http2.dart'; import 'package:merge_map/merge_map.dart'; import 'package:stream_channel/stream_channel.dart'; import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; import 'angel_websocket.dart'; export 'angel_websocket.dart'; @@ -39,6 +41,12 @@ class AngelWebSocket { /// discarded, other than `params['query']`. final bool allowClientParams; + /// An optional whitelist of allowed client origins, or [:null:]. + final List allowedOrigins; + + /// An optional whitelist of allowed client protocols, or [:null:]. + final List allowedProtocols; + /// If `true`, then clients can authenticate their WebSockets by sending a valid JWT. final bool allowAuth; @@ -79,7 +87,9 @@ class AngelWebSocket { this.allowAuth: true, this.synchronizationChannel, this.serializer, - this.deserializer}) { + this.deserializer, + this.allowedOrigins, + this.allowedProtocols}) { if (serializer == null) serializer = json.encode; if (deserializer == null) deserializer = (params) => params; } @@ -334,7 +344,14 @@ class AngelWebSocket { } /// Handles an incoming [WebSocketContext]. - Future handleClient(WebSocketContext socket) async { + Future handleClient(WebSocketContext socket) async { + var origin = socket.request.headers.value('origin'); + if (allowedOrigins != null && !allowedOrigins.contains(origin)) { + throw new AngelHttpException.forbidden( + message: + 'WebSocket connections are not allowed from the origin "$origin".'); + } + _clients.add(socket); await handleConnect(socket); @@ -370,8 +387,66 @@ class AngelWebSocket { var socket = new WebSocketContext(channel, req, res); handleClient(socket); return false; + } else if (req is Http2RequestContext && res is Http2ResponseContext) { + var connection = + req.headers['connection']?.map((s) => s.toLowerCase().trim()); + var upgrade = req.headers.value('upgrade')?.toLowerCase(); + var version = req.headers.value('sec-websocket-version'); + var key = req.headers.value('sec-websocket-key'); + var protocol = req.headers.value('sec-websocket-protocol'); + + if (connection == null) { + throw new AngelHttpException.badRequest( + message: 'Missing `connection` header.'); + } else if (!connection.contains('upgrade')) { + throw new AngelHttpException.badRequest( + message: 'Missing "upgrade" in `connection` header.'); + } else if (upgrade != 'websocket') { + throw new AngelHttpException.badRequest( + message: 'The `upgrade` header must equal "websocket".'); + } else if (version != '13') { + throw new AngelHttpException.badRequest( + message: 'The `sec-websocket-version` header must equal "13".'); + } else if (key == null) { + throw new AngelHttpException.badRequest( + message: 'Missing `sec-websocket-key` header.'); + } else if (protocol != null && + allowedProtocols != null && + !allowedProtocols.contains(protocol)) { + throw new AngelHttpException.badRequest( + message: 'Disallowed `sec-websocket-protocol` header "$protocol".'); + } else { + var stream = res.detach(); + var ctrl = new StreamChannelController>(); + + ctrl.local.stream.listen((buf) { + stream.sendData(buf); + }, onDone: () { + stream.outgoingMessages.close(); + }); + + if (req.hasParsedBody) { + ctrl.local.sink.close(); + } else { + req.body.pipe(ctrl.local.sink); + } + + var sink = utf8.encoder.startChunkedConversion(ctrl.foreign.sink); + sink.add("HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: ${WebSocketChannel.signKey(key)}\r\n"); + if (protocol != null) sink.add("Sec-WebSocket-Protocol: $protocol\r\n"); + sink.add("\r\n"); + + var ws = new WebSocketChannel(ctrl.foreign); + var socket = new WebSocketContext(ws, req, res); + handleClient(socket); + return false; + } } else { - throw new ArgumentError('Not an HTTP/1.1 RequestContext: $req'); + throw new ArgumentError( + 'Not an HTTP/1.1 or HTTP/2 RequestContext+ResponseContext pair: $req, $res'); } } } diff --git a/pubspec.yaml b/pubspec.yaml index 582696c3..b1dc0674 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: WebSocket plugin for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0-alpha.7 +version: 2.0.0-alpha.8 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From ed71ebaaeb789b71bbda91acc032efceb725cc1d Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 5 Jan 2019 21:41:46 -0500 Subject: [PATCH 67/73] 2.0.0 --- CHANGELOG.md | 3 ++ analysis_options.yaml | 5 +- example/main.dart | 7 +-- lib/angel_websocket.dart | 37 --------------- lib/base_websocket_client.dart | 71 ++++++++++++++++----------- lib/browser.dart | 16 +++++-- lib/constants.dart | 87 ++++++++++++++++++++++++++++++++++ lib/flutter.dart | 5 +- lib/io.dart | 5 +- lib/server.dart | 45 +++++++++--------- lib/websocket_context.dart | 2 +- pubspec.yaml | 5 +- test/controller/io_test.dart | 2 +- web/main.dart | 2 +- 14 files changed, 184 insertions(+), 108 deletions(-) create mode 100644 lib/constants.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b77928ee..eef45d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.0 +* Update to work with `client@2.0.0`. + # 2.0.0-alpha.8 * Support for WebSockets over HTTP/2 (though in practice this doesn't often happen, if ever). diff --git a/analysis_options.yaml b/analysis_options.yaml index d38a81f8..70257d27 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1,6 @@ +include: package:pedantic/analysis_options.yaml analyzer: strong-mode: - implicit-casts: false \ No newline at end of file + implicit-casts: false + errors: + unawaited_futures: ignore \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index 0c843aee..e1383c9c 100644 --- a/example/main.dart +++ b/example/main.dart @@ -11,12 +11,7 @@ main(List args) async { var http = new AngelHttp(app); var ws = new AngelWebSocket(app, sendErrors: !app.isProduction); var fs = const LocalFileSystem(); - app.logger = new Logger('angel_auth') - ..onRecord.listen((rec) { - print(rec); - if (rec.error != null) print(rec.error); - if (rec.stackTrace != null) print(rec.stackTrace); - }); + app.logger = new Logger('angel_websocket'); // This is a plug-in. It hooks all your services, // to automatically broadcast events. diff --git a/lib/angel_websocket.dart b/lib/angel_websocket.dart index 1f9b6e94..e2a460c3 100644 --- a/lib/angel_websocket.dart +++ b/lib/angel_websocket.dart @@ -1,43 +1,6 @@ /// 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'; -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'; -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 ACTIONS = const [ - ACTION_INDEX, - ACTION_READ, - ACTION_CREATE, - ACTION_MODIFY, - ACTION_UPDATE, - ACTION_REMOVE -]; - -/// The standard Angel service events. -const List 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 { String eventName; diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index 8dd5c9e1..d8136211 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -8,6 +8,7 @@ import 'package:http/src/base_client.dart' as http; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; import 'angel_websocket.dart'; +import 'constants.dart'; final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); @@ -66,9 +67,28 @@ abstract class BaseWebSocketClient extends BaseAngelClient { /// 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) { + Uri _wsUri; + + /// The [Uri] to which a websocket should point. + Uri get websocketUri => _wsUri ??= _toWsUri(baseUrl); + + static Uri _toWsUri(Uri u) { + if (u.hasScheme) { + if (u.scheme == 'http') { + return u.replace(scheme: 'ws'); + } else if (u.scheme == 'https') { + return u.replace(scheme: 'wss'); + } else { + return u; + } + } else { + return _toWsUri(u.replace(scheme: Uri.base.scheme)); + } + } + + BaseWebSocketClient(http.BaseClient client, baseUrl, + {this.reconnectOnClose = true, Duration reconnectInterval}) + : super(client, baseUrl) { _reconnectInterval = reconnectInterval ?? new Duration(seconds: 10); } @@ -159,11 +179,11 @@ abstract class BaseWebSocketClient extends BaseAngelClient { on._getStream(event.eventName).add(event); } - if (event.eventName == EVENT_ERROR) { + if (event.eventName == errorEvent) { var error = new AngelHttpException.fromMap((event.data ?? {}) as Map); _onError.add(error); - } else if (event.eventName == EVENT_AUTHENTICATED) { + } else if (event.eventName == authenticatedEvent) { var authResult = new AngelAuthResult.fromMap(event.data as Map); _onAuthenticated.add(authResult); } else if (event.eventName?.isNotEmpty == true) { @@ -203,10 +223,6 @@ abstract class BaseWebSocketClient extends BaseAngelClient { /// 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) { if (_socket == null) @@ -217,11 +233,12 @@ abstract class BaseWebSocketClient extends BaseAngelClient { /// Attempts to authenticate a WebSocket, using a valid JWT. void authenticateViaJwt(String jwt) { - send( - ACTION_AUTHENTICATE, - new WebSocketAction(params: { - 'query': {'jwt': jwt} - })); + sendAction(new WebSocketAction( + eventName: authenticateAction, + params: { + 'query': {'jwt': jwt} + }, + )); } } @@ -306,7 +323,7 @@ class WebSocketsService extends Service { _onAllEvents.add(event); - if (event.eventName == EVENT_INDEXED) { + if (event.eventName == indexedEvent) { var d = event.data; var transformed = new WebSocketEvent( eventName: event.eventName, @@ -318,19 +335,19 @@ class WebSocketsService extends Service { var transformed = transformEvent(event).data; switch (event.eventName) { - case EVENT_READ: + case readEvent: _onRead.add(transformed); break; - case EVENT_CREATED: + case createdEvent: _onCreated.add(transformed); break; - case EVENT_MODIFIED: + case modifiedEvent: _onModified.add(transformed); break; - case EVENT_UPDATED: + case updatedEvent: _onUpdated.add(transformed); break; - case EVENT_REMOVED: + case removedEvent: _onRemoved.add(transformed); break; } @@ -346,14 +363,14 @@ class WebSocketsService extends Service { @override Future> index([Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_INDEX}', params: params ?? {})); + eventName: '$path::$indexAction', params: params ?? {})); return null; } @override Future read(id, [Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_READ}', + eventName: '$path::$readAction', id: id.toString(), params: params ?? {})); return null; @@ -362,16 +379,14 @@ class WebSocketsService extends Service { @override Future create(data, [Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_CREATE}', - data: data, - params: params ?? {})); + eventName: '$path::$createAction', data: data, params: params ?? {})); return null; } @override Future modify(id, data, [Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_MODIFY}', + eventName: '$path::$modifyAction', id: id.toString(), data: data, params: params ?? {})); @@ -381,7 +396,7 @@ class WebSocketsService extends Service { @override Future update(id, data, [Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_UPDATE}', + eventName: '$path::$updateAction', id: id.toString(), data: data, params: params ?? {})); @@ -391,7 +406,7 @@ class WebSocketsService extends Service { @override Future remove(id, [Map params]) async { app.sendAction(new WebSocketAction( - eventName: '$path::${ACTION_REMOVE}', + eventName: '$path::$removeAction', id: id.toString(), params: params ?? {})); return null; diff --git a/lib/browser.dart b/lib/browser.dart index a04f8be1..d2278521 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -17,7 +17,7 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); class WebSockets extends BaseWebSocketClient { final List _services = []; - WebSockets(String path) : super(new http.BrowserClient(), path); + WebSockets(path) : super(new http.BrowserClient(), path); @override Future close() { @@ -30,7 +30,7 @@ class WebSockets extends BaseWebSocketClient { @override Stream authenticateViaPopup(String url, - {String eventName: 'token', String errorMessage}) { + {String eventName = 'token', String errorMessage}) { var ctrl = new StreamController(); var wnd = window.open(url, 'angel_client_auth_popup'); @@ -64,9 +64,15 @@ class WebSockets extends BaseWebSocketClient { @override Future getConnectedWebSocket() { - var socket = new WebSocket(authToken?.isNotEmpty == true - ? '$basePath?token=$authToken' - : basePath); + var url = websocketUri; + + if (authToken?.isNotEmpty == true) { + url = url.replace( + queryParameters: new Map.from(url.queryParameters) + ..['token'] = authToken); + } + + var socket = new WebSocket(url.toString()); var completer = new Completer(); socket diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 00000000..25f75e28 --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,87 @@ +const String authenticateAction = 'authenticate'; +const String indexAction = 'index'; +const String readAction = 'read'; +const String createAction = 'create'; +const String modifyAction = 'modify'; +const String updateAction = 'update'; +const String removeAction = 'remove'; + +@deprecated +const String ACTION_AUTHENTICATE = authenticateAction; + +@deprecated +const String ACTION_INDEX = indexAction; + +@deprecated +const String ACTION_READ = readAction; + +@deprecated +const String ACTION_CREATE = createAction; + +@deprecated +const String ACTION_MODIFY = modifyAction; + +@deprecated +const String ACTION_UPDATE = updateAction; + +@deprecated +const String ACTION_REMOVE = removeAction; + +const String authenticatedEvent = 'authenticated'; +const String errorEvent = 'error'; +const String indexedEvent = 'indexed'; +const String readEvent = 'read'; +const String createdEvent = 'created'; +const String modifiedEvent = 'modified'; +const String updatedEvent = 'updated'; +const String removedEvent = 'removed'; + +@deprecated +const String EVENT_AUTHENTICATED = authenticatedEvent; + +@deprecated +const String EVENT_ERROR = errorEvent; + +@deprecated +const String EVENT_INDEXED = indexedEvent; + +@deprecated +const String EVENT_READ = readEvent; + +@deprecated +const String EVENT_CREATED = createdEvent; + +@deprecated +const String EVENT_MODIFIED = modifiedEvent; + +@deprecated +const String EVENT_UPDATED = updatedEvent; + +@deprecated +const String EVENT_REMOVED = removedEvent; + +/// The standard Angel service actions. +const List actions = const [ + indexAction, + readAction, + createAction, + modifyAction, + updateAction, + removeAction +]; + +@deprecated +const List ACTIONS = actions; + +/// The standard Angel service events. +const List events = const [ + indexedEvent, + readEvent, + createdEvent, + modifiedEvent, + updatedEvent, + removedEvent +]; + +@deprecated +const List EVENTS = events; diff --git a/lib/flutter.dart b/lib/flutter.dart index 96e7fdfd..3e6eee0b 100644 --- a/lib/flutter.dart +++ b/lib/flutter.dart @@ -20,7 +20,8 @@ class WebSockets extends BaseWebSocketClient { WebSockets(String path) : super(new http.IOClient(), path); @override - Stream authenticateViaPopup(String url, {String eventName: 'token'}) { + Stream authenticateViaPopup(String url, + {String eventName = 'token'}) { throw new UnimplementedError( 'Opening popup windows is not supported in the `dart:io` client.'); } @@ -36,7 +37,7 @@ class WebSockets extends BaseWebSocketClient { @override Future getConnectedWebSocket() async { - var socket = await WebSocket.connect(basePath, + var socket = await WebSocket.connect(websocketUri.toString(), headers: authToken?.isNotEmpty == true ? {'Authorization': 'Bearer $authToken'} : {}); diff --git a/lib/io.dart b/lib/io.dart index 47f94166..3d89406e 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -21,7 +21,8 @@ class WebSockets extends BaseWebSocketClient { WebSockets(String path) : super(new http.IOClient(), path); @override - Stream authenticateViaPopup(String url, {String eventName: 'token'}) { + Stream authenticateViaPopup(String url, + {String eventName = 'token'}) { throw new UnimplementedError( 'Opening popup windows is not supported in the `dart:io` client.'); } @@ -37,7 +38,7 @@ class WebSockets extends BaseWebSocketClient { @override Future getConnectedWebSocket() async { - var socket = await WebSocket.connect(basePath, + var socket = await WebSocket.connect(websocketUri.toString(), headers: authToken?.isNotEmpty == true ? {'Authorization': 'Bearer $authToken'} : {}); diff --git a/lib/server.dart b/lib/server.dart index de675fd4..4bf162b9 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -14,6 +14,7 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'angel_websocket.dart'; +import 'constants.dart'; export 'angel_websocket.dart'; part 'websocket_context.dart'; @@ -82,9 +83,9 @@ class AngelWebSocket { Function deserializer; AngelWebSocket(this.app, - {this.sendErrors: false, - this.allowClientParams: false, - this.allowAuth: true, + {this.sendErrors = false, + this.allowClientParams = false, + this.allowAuth = true, this.synchronizationChannel, this.serializer, this.deserializer, @@ -115,8 +116,8 @@ class AngelWebSocket { } /// Slates an event to be dispatched. - Future batchEvent(WebSocketEvent event, - {filter(WebSocketContext socket), bool notify: true}) async { + Future batchEvent(WebSocketEvent event, + {filter(WebSocketContext socket), bool notify = true}) async { // Default implementation will just immediately fire events _clients.forEach((client) async { dynamic result = true; @@ -172,29 +173,29 @@ class AngelWebSocket { ]); try { - if (actionName == ACTION_INDEX) { + if (actionName == indexAction) { socket.send( - "${split[0]}::" + EVENT_INDEXED, await service.index(params)); + "${split[0]}::" + indexedEvent, await service.index(params)); return null; - } else if (actionName == ACTION_READ) { - socket.send("${split[0]}::" + EVENT_READ, - await service.read(action.id, params)); + } else if (actionName == readAction) { + socket.send( + "${split[0]}::" + readEvent, await service.read(action.id, params)); return null; - } else if (actionName == ACTION_CREATE) { + } else if (actionName == createAction) { return new WebSocketEvent( - eventName: "${split[0]}::" + EVENT_CREATED, + eventName: "${split[0]}::" + createdEvent, data: await service.create(action.data, params)); - } else if (actionName == ACTION_MODIFY) { + } else if (actionName == modifyAction) { return new WebSocketEvent( - eventName: "${split[0]}::" + EVENT_MODIFIED, + eventName: "${split[0]}::" + modifiedEvent, data: await service.modify(action.id, action.data, params)); - } else if (actionName == ACTION_UPDATE) { + } else if (actionName == updateAction) { return new WebSocketEvent( - eventName: "${split[0]}::" + EVENT_UPDATED, + eventName: "${split[0]}::" + updatedEvent, data: await service.update(action.id, action.data, params)); - } else if (actionName == ACTION_REMOVE) { + } else if (actionName == removeAction) { return new WebSocketEvent( - eventName: "${split[0]}::" + EVENT_REMOVED, + eventName: "${split[0]}::" + removedEvent, data: await service.remove(action.id, params)); } else { socket.sendError(new AngelHttpException.methodNotAllowed( @@ -209,7 +210,7 @@ class AngelWebSocket { /// Authenticates a [WebSocketContext]. Future handleAuth(WebSocketAction action, WebSocketContext socket) async { if (allowAuth != false && - action.eventName == ACTION_AUTHENTICATE && + action.eventName == authenticateAction && action.params['query'] is Map && action.params['query']['jwt'] is String) { try { @@ -222,7 +223,7 @@ class AngelWebSocket { socket.request ..container.registerSingleton(token) ..container.registerSingleton(user, as: user.runtimeType as Type); - socket.send(EVENT_AUTHENTICATED, + socket.send(authenticatedEvent, {'token': token.serialize(auth.hmac), 'data': user}); } catch (e, st) { catchError(e, st, socket); @@ -272,14 +273,14 @@ class AngelWebSocket { .add(fromJson["data"] as Map); } - if (action.eventName == ACTION_AUTHENTICATE) + if (action.eventName == authenticateAction) await handleAuth(action, socket); if (action.eventName.contains("::")) { var split = action.eventName.split("::"); if (split.length >= 2) { - if (ACTIONS.contains(split[1])) { + if (actions.contains(split[1])) { var event = await handleAction(action, socket); if (event is Future) event = await event; } diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index 4630fd59..c4722540 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -49,7 +49,7 @@ class WebSocketContext { } /// Sends an error event. - void sendError(AngelHttpException error) => send(EVENT_ERROR, error.toJson()); + void sendError(AngelHttpException error) => send(errorEvent, error.toJson()); } class _WebSocketEventTable { diff --git a/pubspec.yaml b/pubspec.yaml index b1dc0674..bfaed81f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: angel_websocket -description: WebSocket plugin for Angel. +description: Support for using pkg:angel_client with WebSockets. Designed for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0-alpha.8 +version: 2.0.0 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: @@ -19,4 +19,5 @@ dev_dependencies: angel_container: ^1.0.0-alpha angel_model: ^1.0.0 logging: ^0.11.0 + pedantic: ^1.0.0 test: ^1.0.0 diff --git a/test/controller/io_test.dart b/test/controller/io_test.dart index 6f53b980..929e3eb8 100644 --- a/test/controller/io_test.dart +++ b/test/controller/io_test.dart @@ -61,7 +61,7 @@ main() { group('controller.io', () { test('search', () async { - client.send('search', new ws.WebSocketAction()); + client.sendAction(new ws.WebSocketAction(eventName: 'search')); var search = await client.on['searched'].first; print('Searched: ${search.data}'); expect(new Game.fromJson(search.data as Map), equals(johnVsBob)); diff --git a/web/main.dart b/web/main.dart index 48bd1e27..ffc04ed9 100644 --- a/web/main.dart +++ b/web/main.dart @@ -4,7 +4,7 @@ import 'package:angel_websocket/browser.dart'; /// Dummy app to ensure client works with DDC. main() { var app = new WebSockets(window.location.origin); - window.alert(app.basePath); + window.alert(app.baseUrl.toString()); app.connect().catchError((_) { window.alert('no websocket'); From 0dbdd96e8c8d3dc32f90f8212e8d36e534ec6c78 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 3 Feb 2019 14:34:00 -0500 Subject: [PATCH 68/73] * Add `reconnectOnClose` and `reconnectinterval` parameters in top-level `WebSockets` constructors. --- CHANGELOG.md | 3 +++ lib/browser.dart | 6 +++++- lib/flutter.dart | 6 +++++- lib/io.dart | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eef45d67..32e6e8ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.1 +* Add `reconnectOnClose` and `reconnectinterval` parameters in top-level `WebSockets` constructors. + # 2.0.0 * Update to work with `client@2.0.0`. diff --git a/lib/browser.dart b/lib/browser.dart index d2278521..3f84ebc3 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -17,7 +17,11 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); class WebSockets extends BaseWebSocketClient { final List _services = []; - WebSockets(path) : super(new http.BrowserClient(), path); + WebSockets(baseUrl, + {bool reconnectOnClose = true, Duration reconnectInterval}) + : super(new http.BrowserClient(), baseUrl, + reconnectOnClose: reconnectOnClose, + reconnectInterval: reconnectInterval); @override Future close() { diff --git a/lib/flutter.dart b/lib/flutter.dart index 3e6eee0b..e88b94be 100644 --- a/lib/flutter.dart +++ b/lib/flutter.dart @@ -17,7 +17,11 @@ export 'angel_websocket.dart'; class WebSockets extends BaseWebSocketClient { final List _services = []; - WebSockets(String path) : super(new http.IOClient(), path); + WebSockets(baseUrl, + {bool reconnectOnClose = true, Duration reconnectInterval}) + : super(new http.IOClient(), baseUrl, + reconnectOnClose: reconnectOnClose, + reconnectInterval: reconnectInterval); @override Stream authenticateViaPopup(String url, diff --git a/lib/io.dart b/lib/io.dart index 3d89406e..6bcfc0ed 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -18,7 +18,11 @@ final RegExp _straySlashes = new RegExp(r"(^/)|(/+$)"); class WebSockets extends BaseWebSocketClient { final List _services = []; - WebSockets(String path) : super(new http.IOClient(), path); + WebSockets(baseUrl, + {bool reconnectOnClose = true, Duration reconnectInterval}) + : super(new http.IOClient(), baseUrl, + reconnectOnClose: reconnectOnClose, + reconnectInterval: reconnectInterval); @override Stream authenticateViaPopup(String url, From 66d3fb2e044c4e3a3c66aa280507ae2c70a221c9 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 3 Feb 2019 14:35:33 -0500 Subject: [PATCH 69/73] Close `WebSocketExtraneousEventHandler`. --- CHANGELOG.md | 1 + lib/base_websocket_client.dart | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32e6e8ef..bc2702e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 2.0.1 * Add `reconnectOnClose` and `reconnectinterval` parameters in top-level `WebSockets` constructors. +* Close `WebSocketExtraneousEventHandler`. # 2.0.0 * Update to work with `client@2.0.0`. diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index d8136211..dca21459 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -94,6 +94,7 @@ abstract class BaseWebSocketClient extends BaseAngelClient { @override Future close() async { + on._close(); await _socket.sink.close(status.goingAway); _onData.close(); _onAllEvents.close(); @@ -434,4 +435,8 @@ class WebSocketExtraneousEventHandler { return _events[index].stream; } + + void _close() { + _events.values.forEach((s) => s.close()); + } } From 8e8271f16191b34f81a471a4f2a825e6e8b0ef55 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 3 Feb 2019 14:44:53 -0500 Subject: [PATCH 70/73] Add onAuthenticated to server-side --- CHANGELOG.md | 1 + lib/server.dart | 1 + lib/websocket_context.dart | 6 ++++++ pubspec.yaml | 2 +- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2702e2..b3713fb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 2.0.1 * Add `reconnectOnClose` and `reconnectinterval` parameters in top-level `WebSockets` constructors. * Close `WebSocketExtraneousEventHandler`. +* Add onAuthenticated to server-side. # 2.0.0 * Update to work with `client@2.0.0`. diff --git a/lib/server.dart b/lib/server.dart index 4bf162b9..5b7c953b 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -223,6 +223,7 @@ class AngelWebSocket { socket.request ..container.registerSingleton(token) ..container.registerSingleton(user, as: user.runtimeType as Type); + socket._onAuthenticated.add(null); socket.send(authenticatedEvent, {'token': token.serialize(auth.hmac), 'data': user}); } catch (e, st) { diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index c4722540..bcdca377 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -18,6 +18,8 @@ class WebSocketContext { StreamController _onAction = new StreamController(); + StreamController _onAuthenticated = StreamController(); + StreamController _onClose = new StreamController(); StreamController _onData = new StreamController(); @@ -25,6 +27,9 @@ class WebSocketContext { /// Fired on any [WebSocketAction]; Stream get onAction => _onAction.stream; + /// Fired when the user authenticates. + Stream get onAuthenticated => _onAuthenticated.stream; + /// Fired once the underlying [WebSocket] closes. Stream get onClose => _onClose.stream; @@ -37,6 +42,7 @@ class WebSocketContext { Future close() async { await channel.sink.close(); _onAction.close(); + _onAuthenticated.close(); _onData.close(); _onClose.add(null); _onClose.close(); diff --git a/pubspec.yaml b/pubspec.yaml index bfaed81f..374196fd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: Support for using pkg:angel_client with WebSockets. Designed for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.0 +version: 2.0.1 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From d9a234bf9bca6f9f0e39ac0f17145d981dc549d4 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 18 Apr 2019 11:43:17 -0400 Subject: [PATCH 71/73] Bump to 2.0.2 --- CHANGELOG.md | 4 ++++ example/main.dart | 2 +- lib/websocket_controller.dart | 2 +- pubspec.yaml | 6 +++--- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3713fb5..c7f4fe3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.0.2 +* Update `stream_channel` to `2.0.0`. +* Use `angel_framework^@2.0.0-rc.0`. + # 2.0.1 * Add `reconnectOnClose` and `reconnectinterval` parameters in top-level `WebSockets` constructors. * Close `WebSocketExtraneousEventHandler`. diff --git a/example/main.dart b/example/main.dart index e1383c9c..e6757831 100644 --- a/example/main.dart +++ b/example/main.dart @@ -9,7 +9,7 @@ import 'package:logging/logging.dart'; main(List args) async { var app = new Angel(); var http = new AngelHttp(app); - var ws = new AngelWebSocket(app, sendErrors: !app.isProduction); + var ws = new AngelWebSocket(app, sendErrors: !app.environment.isProduction); var fs = const LocalFileSystem(); app.logger = new Logger('angel_websocket'); diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index b214acbd..1a0b0eb6 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -51,7 +51,7 @@ class WebSocketController extends Controller { orElse: () => null); if (exposeMirror != null) { - ExposeWs exposeWs = exposeMirror.reflectee; + ExposeWs exposeWs = exposeMirror.reflectee as ExposeWs; _handlers[exposeWs.eventName] = mirror; _handlerSymbols[exposeWs.eventName] = sym; } diff --git a/pubspec.yaml b/pubspec.yaml index 374196fd..507e0f2e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,18 +2,18 @@ name: angel_websocket description: Support for using pkg:angel_client with WebSockets. Designed for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.1 +version: 2.0.2 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: angel_auth: ^2.0.0-alpha angel_client: ^2.0.0-alpha - angel_framework: ^2.0.0-alpha + angel_framework: ^2.0.0-rc.0 angel_http_exception: ^1.0.0 http: ">=0.11.0 <0.13.0" merge_map: ^1.0.0 meta: ^1.0.0 - stream_channel: ^1.0.0 + stream_channel: ^2.0.0 web_socket_channel: ^1.0.0 dev_dependencies: angel_container: ^1.0.0-alpha From 7f38d181a1cd3c63fe7aead7aa3bdb964a6f9f92 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 1 May 2019 18:53:30 -0400 Subject: [PATCH 72/73] 2.0.3 --- CHANGELOG.md | 3 +++ lib/websocket_controller.dart | 6 ++---- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f4fe3f..6361f7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.0.3 +* Remove `WebSocketController.plugin`. + # 2.0.2 * Update `stream_channel` to `2.0.0`. * Use `angel_framework^@2.0.0-rc.0`. diff --git a/lib/websocket_controller.dart b/lib/websocket_controller.dart index 1a0b0eb6..ad577484 100644 --- a/lib/websocket_controller.dart +++ b/lib/websocket_controller.dart @@ -9,19 +9,17 @@ class ExposeWs { /// A special controller that also supports WebSockets. class WebSocketController extends Controller { + /// The plug-in instance powering this controller. final AngelWebSocket ws; Map _handlers = {}; Map _handlerSymbols = {}; - /// The plug-in instance powering this controller. - AngelWebSocket plugin; - WebSocketController(this.ws) : super(); /// Sends an event to all clients. void broadcast(String eventName, data, {filter(WebSocketContext socket)}) { - plugin.batchEvent(new WebSocketEvent(eventName: eventName, data: data), + ws.batchEvent(new WebSocketEvent(eventName: eventName, data: data), filter: filter); } diff --git a/pubspec.yaml b/pubspec.yaml index 507e0f2e..91d16906 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: angel_websocket description: Support for using pkg:angel_client with WebSockets. Designed for Angel. environment: sdk: ">=2.0.0-dev <3.0.0" -version: 2.0.2 +version: 2.0.3 author: Tobe O homepage: https://github.com/angel-dart/angel_websocket dependencies: From 229b5e72058a644292d0f258bd0f7f06613b7b24 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 1 May 2019 18:58:47 -0400 Subject: [PATCH 73/73] Fix travis --- CHANGELOG.md | 1 + analysis_options.yaml | 4 +-- example/main.dart | 2 +- lib/base_websocket_client.dart | 60 ++++++++++++++++++---------------- lib/server.dart | 8 ++--- lib/websocket_context.dart | 14 ++++---- test/service/common.dart | 4 ++- 7 files changed, 50 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6361f7a3..9bd7428c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 2.0.3 * Remove `WebSocketController.plugin`. +* Remove any unawaited futures. # 2.0.2 * Update `stream_channel` to `2.0.0`. diff --git a/analysis_options.yaml b/analysis_options.yaml index 70257d27..380eebc8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,6 +1,4 @@ include: package:pedantic/analysis_options.yaml analyzer: strong-mode: - implicit-casts: false - errors: - unawaited_futures: ignore \ No newline at end of file + implicit-casts: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index e6757831..cf3746dc 100644 --- a/example/main.dart +++ b/example/main.dart @@ -46,7 +46,7 @@ main(List args) async { } var http2 = new AngelHttp2(app, ctx); - http2.onHttp1.forEach(http.handleRequest); + http2.onHttp1.listen(http.handleRequest); await http2.startServer('127.0.0.1', 3000); print('Listening at ${http2.uri}'); } else { diff --git a/lib/base_websocket_client.dart b/lib/base_websocket_client.dart index dca21459..143ce69e 100644 --- a/lib/base_websocket_client.dart +++ b/lib/base_websocket_client.dart @@ -95,13 +95,15 @@ abstract class BaseWebSocketClient extends BaseAngelClient { @override Future close() async { on._close(); - await _socket.sink.close(status.goingAway); - _onData.close(); - _onAllEvents.close(); - _onAuthenticated.close(); - _onError.close(); - _onServiceEvent.close(); - _onWebSocketChannelException.close(); + scheduleMicrotask(() async { + await _socket.sink.close(status.goingAway); + await _onData.close(); + await _onAllEvents.close(); + await _onAuthenticated.close(); + await _onError.close(); + await _onServiceEvent.close(); + await _onWebSocketChannelException.close(); + }); } /// Connects the WebSocket. [timeout] is optional. @@ -119,22 +121,24 @@ abstract class BaseWebSocketClient extends BaseAngelClient { } }); - getConnectedWebSocket().then((socket) { - if (!c.isCompleted) { - if (timer.isActive) timer.cancel(); + scheduleMicrotask(() { + return getConnectedWebSocket().then((socket) { + if (!c.isCompleted) { + if (timer.isActive) timer.cancel(); - while (_queue.isNotEmpty) { - var action = _queue.removeFirst(); - socket.sink.add(serialize(action)); + while (_queue.isNotEmpty) { + var action = _queue.removeFirst(); + socket.sink.add(serialize(action)); + } + + c.complete(socket); } - - c.complete(socket); - } - }).catchError((e, StackTrace st) { - if (!c.isCompleted) { - if (timer.isActive) timer.cancel(); - c.completeError(e, st); - } + }).catchError((e, StackTrace st) { + if (!c.isCompleted) { + if (timer.isActive) timer.cancel(); + c.completeError(e, st); + } + }); }); return await c.future.then((socket) { @@ -293,13 +297,13 @@ class WebSocketsService extends Service { } Future close() async { - _onAllEvents.close(); - _onCreated.close(); - _onIndexed.close(); - _onModified.close(); - _onRead.close(); - _onRemoved.close(); - _onUpdated.close(); + await _onAllEvents.close(); + await _onCreated.close(); + await _onIndexed.close(); + await _onModified.close(); + await _onRead.close(); + await _onRemoved.close(); + await _onUpdated.close(); } /// Serializes an [action] to be sent over a WebSocket. diff --git a/lib/server.dart b/lib/server.dart index 5b7c953b..734c2c35 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -387,7 +387,7 @@ class AngelWebSocket { var ws = await WebSocketTransformer.upgrade(req.rawRequest); var channel = new IOWebSocketChannel(ws); var socket = new WebSocketContext(channel, req, res); - handleClient(socket); + scheduleMicrotask(() => handleClient(socket)); return false; } else if (req is Http2RequestContext && res is Http2ResponseContext) { var connection = @@ -428,9 +428,9 @@ class AngelWebSocket { }); if (req.hasParsedBody) { - ctrl.local.sink.close(); + await ctrl.local.sink.close(); } else { - req.body.pipe(ctrl.local.sink); + await req.body.pipe(ctrl.local.sink); } var sink = utf8.encoder.startChunkedConversion(ctrl.foreign.sink); @@ -443,7 +443,7 @@ class AngelWebSocket { var ws = new WebSocketChannel(ctrl.foreign); var socket = new WebSocketContext(ws, req, res); - handleClient(socket); + scheduleMicrotask(() => handleClient(socket)); return false; } } else { diff --git a/lib/websocket_context.dart b/lib/websocket_context.dart index bcdca377..1876f8bd 100644 --- a/lib/websocket_context.dart +++ b/lib/websocket_context.dart @@ -40,12 +40,14 @@ class WebSocketContext { /// Closes the underlying [StreamChannel]. Future close() async { - await channel.sink.close(); - _onAction.close(); - _onAuthenticated.close(); - _onData.close(); - _onClose.add(null); - _onClose.close(); + scheduleMicrotask(() async { + await channel.sink.close(); + await _onAction.close(); + await _onAuthenticated.close(); + await _onData.close(); + await _onClose.add(null); + await _onClose.close(); + }); } /// Sends an arbitrary [WebSocketEvent]; diff --git a/test/service/common.dart b/test/service/common.dart index daae99d1..9d327860 100644 --- a/test/service/common.dart +++ b/test/service/common.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:angel_framework/angel_framework.dart'; import 'package:angel_model/angel_model.dart'; import 'package:angel_websocket/base_websocket_client.dart'; @@ -23,7 +25,7 @@ class TodoService extends MapService { testIndex(BaseWebSocketClient client) async { var todoService = client.service('api/todos'); - todoService.index(); + scheduleMicrotask(() => todoService.index()); var indexed = await todoService.onIndexed.first; print('indexed: $indexed');