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');