Merge pull request dart-lang/json_rpc_2#28 from bcko/patch-1

Update .gitignore to new `dart_tool` pub cache
diff --git a/pkgs/json_rpc_2/CHANGELOG.md b/pkgs/json_rpc_2/CHANGELOG.md
index 3e2d662..878e1f0 100644
--- a/pkgs/json_rpc_2/CHANGELOG.md
+++ b/pkgs/json_rpc_2/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 2.1.0
+
+* `Server` and related classes can now take an `onUnhandledError` callback to
+  notify callers of unhandled exceptions.
+
+## 2.0.10
+
+* Allow `stream_channel` version 2.x
+
 ## 2.0.8
 
 * Updated SDK version to 2.0.0-dev.17.0
diff --git a/pkgs/json_rpc_2/README.md b/pkgs/json_rpc_2/README.md
index e495264..0567d20 100644
--- a/pkgs/json_rpc_2/README.md
+++ b/pkgs/json_rpc_2/README.md
@@ -14,7 +14,11 @@
 
 main() async {
   var socket = IOWebSocketChannel.connect('ws://localhost:4321');
-  var server = new json_rpc.Server(socket);
+
+  // The socket is a StreamChannel<dynamic> because it might emit binary
+  // List<int>s, but JSON RPC 2 only works with Strings so we assert it only
+  // emits those by casting it.
+  var server = new json_rpc.Server(socket.cast<String>());
 
   // Any string may be used as a method name. JSON-RPC 2.0 methods are
   // case-sensitive.
diff --git a/pkgs/json_rpc_2/analysis_options.yaml b/pkgs/json_rpc_2/analysis_options.yaml
deleted file mode 100644
index a10d4c5..0000000
--- a/pkgs/json_rpc_2/analysis_options.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-analyzer:
-  strong-mode: true
diff --git a/pkgs/json_rpc_2/lib/src/peer.dart b/pkgs/json_rpc_2/lib/src/peer.dart
index 18cac07..7176763 100644
--- a/pkgs/json_rpc_2/lib/src/peer.dart
+++ b/pkgs/json_rpc_2/lib/src/peer.dart
@@ -39,13 +39,20 @@
   Future get done => _manager.done;
   bool get isClosed => _manager.isClosed;
 
+  @override
+  ErrorCallback get onUnhandledError => _server?.onUnhandledError;
+
   /// Creates a [Peer] that communicates over [channel].
   ///
   /// Note that the peer won't begin listening to [channel] until [Peer.listen]
   /// is called.
-  Peer(StreamChannel<String> channel)
+  ///
+  /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
+  /// If this is not provided, unhandled exceptions will be swallowed.
+  Peer(StreamChannel<String> channel, {ErrorCallback onUnhandledError})
       : this.withoutJson(
-            jsonDocument.bind(channel).transform(respondToFormatExceptions));
+            jsonDocument.bind(channel).transform(respondToFormatExceptions),
+            onUnhandledError: onUnhandledError);
 
   /// Creates a [Peer] that communicates using decoded messages over [channel].
   ///
@@ -54,10 +61,14 @@
   ///
   /// Note that the peer won't begin listening to [channel] until
   /// [Peer.listen] is called.
-  Peer.withoutJson(StreamChannel channel)
+  ///
+  /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
+  /// If this is not provided, unhandled exceptions will be swallowed.
+  Peer.withoutJson(StreamChannel channel, {ErrorCallback onUnhandledError})
       : _manager = new ChannelManager("Peer", channel) {
     _server = new Server.withoutJson(
-        new StreamChannel(_serverIncomingForwarder.stream, channel.sink));
+        new StreamChannel(_serverIncomingForwarder.stream, channel.sink),
+        onUnhandledError: onUnhandledError);
     _client = new Client.withoutJson(
         new StreamChannel(_clientIncomingForwarder.stream, channel.sink));
   }
diff --git a/pkgs/json_rpc_2/lib/src/server.dart b/pkgs/json_rpc_2/lib/src/server.dart
index 0aa4c2f..5c3b132 100644
--- a/pkgs/json_rpc_2/lib/src/server.dart
+++ b/pkgs/json_rpc_2/lib/src/server.dart
@@ -15,6 +15,9 @@
 import 'parameters.dart';
 import 'utils.dart';
 
+/// A callback for unhandled exceptions.
+typedef ErrorCallback = void Function(dynamic error, dynamic stackTrace);
+
 /// A JSON-RPC 2.0 server.
 ///
 /// A server exposes methods that are called by requests, to which it provides
@@ -51,13 +54,24 @@
   /// endpoint closes the connection.
   bool get isClosed => _manager.isClosed;
 
+  /// A callback that is fired on unhandled exceptions.
+  ///
+  /// In the case where a user provided callback results in an exception that
+  /// cannot be properly routed back to the client, this handler will be
+  /// invoked. If it is not set, the exception will be swallowed.
+  final ErrorCallback onUnhandledError;
+
   /// Creates a [Server] that communicates over [channel].
   ///
   /// Note that the server won't begin listening to [requests] until
   /// [Server.listen] is called.
-  Server(StreamChannel<String> channel)
+  ///
+  /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
+  /// If this is not provided, unhandled exceptions will be swallowed.
+  Server(StreamChannel<String> channel, {ErrorCallback onUnhandledError})
       : this.withoutJson(
-            jsonDocument.bind(channel).transform(respondToFormatExceptions));
+            jsonDocument.bind(channel).transform(respondToFormatExceptions),
+            onUnhandledError: onUnhandledError);
 
   /// Creates a [Server] that communicates using decoded messages over
   /// [channel].
@@ -67,7 +81,10 @@
   ///
   /// Note that the server won't begin listening to [requests] until
   /// [Server.listen] is called.
-  Server.withoutJson(StreamChannel channel)
+  ///
+  /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
+  /// If this is not provided, unhandled exceptions will be swallowed.
+  Server.withoutJson(StreamChannel channel, {this.onUnhandledError})
       : _manager = new ChannelManager("Server", channel);
 
   /// Starts listening to the underlying stream.
@@ -175,9 +192,11 @@
             request.containsKey('id')) {
           return error.serialize(request);
         } else {
+          onUnhandledError?.call(error, stackTrace);
           return null;
         }
       } else if (!request.containsKey('id')) {
+        onUnhandledError?.call(error, stackTrace);
         return null;
       }
       final chain = new Chain.forTrace(stackTrace);
diff --git a/pkgs/json_rpc_2/pubspec.yaml b/pkgs/json_rpc_2/pubspec.yaml
index 77b52c0..40c0293 100644
--- a/pkgs/json_rpc_2/pubspec.yaml
+++ b/pkgs/json_rpc_2/pubspec.yaml
@@ -1,12 +1,16 @@
 name: json_rpc_2
-version: 2.0.8
+version: 2.1.0
 author: Dart Team <misc@dartlang.org>
-description: An implementation of the JSON-RPC 2.0 spec.
-homepage: http://github.com/dart-lang/json_rpc_2
-dependencies:
-  stack_trace: '>=0.9.1 <2.0.0'
-  stream_channel: '^1.1.0'
-dev_dependencies:
-  test: "^0.12.28"
+description: >-
+  Utilities to write a client or server using the JSON-RPC 2.0 spec.
+homepage: https://github.com/dart-lang/json_rpc_2
+
 environment:
-  sdk: ">=2.0.0-dev.17.0 <2.0.0"
+  sdk: ">=2.0.0 <3.0.0"
+
+dependencies:
+  stack_trace: ^1.0.0
+  stream_channel: ">=1.1.0 <3.0.0"
+
+dev_dependencies:
+  test: ^1.0.0
diff --git a/pkgs/json_rpc_2/test/client/client_test.dart b/pkgs/json_rpc_2/test/client/client_test.dart
index 1648df0..358247d 100644
--- a/pkgs/json_rpc_2/test/client/client_test.dart
+++ b/pkgs/json_rpc_2/test/client/client_test.dart
@@ -69,7 +69,7 @@
 
   test("sends a synchronous batch of requests", () {
     controller.expectRequest((request) {
-      expect(request, new isInstanceOf<List>());
+      expect(request, new TypeMatcher<List>());
       expect(request, hasLength(3));
       expect(request[0], equals({'jsonrpc': '2.0', 'method': 'foo'}));
       expect(
@@ -101,7 +101,7 @@
 
   test("sends an asynchronous batch of requests", () {
     controller.expectRequest((request) {
-      expect(request, new isInstanceOf<List>());
+      expect(request, new TypeMatcher<List>());
       expect(request, hasLength(3));
       expect(request[0], equals({'jsonrpc': '2.0', 'method': 'foo'}));
       expect(
@@ -157,7 +157,7 @@
 
     expect(controller.client.sendRequest("foo", {'param': 'value'}),
         throwsA(predicate((exception) {
-      expect(exception, new isInstanceOf<json_rpc.RpcException>());
+      expect(exception, new TypeMatcher<json_rpc.RpcException>());
       expect(exception.code, equals(error_code.SERVER_ERROR));
       expect(exception.message, equals('you are bad at requests'));
       expect(exception.data, equals('some junk'));
diff --git a/pkgs/json_rpc_2/test/peer_test.dart b/pkgs/json_rpc_2/test/peer_test.dart
index 94c49e6..491526f 100644
--- a/pkgs/json_rpc_2/test/peer_test.dart
+++ b/pkgs/json_rpc_2/test/peer_test.dart
@@ -184,4 +184,24 @@
       incoming.add({"completely": "wrong"});
     });
   });
+
+  test("can notify on unhandled errors for if the method throws", () async {
+    Exception exception = Exception('test exception');
+    var incomingController = new StreamController();
+    var outgoingController = new StreamController();
+    final Completer<Exception> completer = Completer<Exception>();
+    peer = new json_rpc.Peer.withoutJson(
+      new StreamChannel(incomingController.stream, outgoingController),
+      onUnhandledError: (error, stack) {
+        completer.complete(error);
+      },
+    );
+    peer
+      ..registerMethod('foo', () => throw exception)
+      ..listen();
+
+    incomingController.add({'jsonrpc': '2.0', 'method': 'foo'});
+    Exception receivedException = await completer.future;
+    expect(receivedException, equals(exception));
+  });
 }
diff --git a/pkgs/json_rpc_2/test/server/server_test.dart b/pkgs/json_rpc_2/test/server/server_test.dart
index c2f87ab..b9ba504 100644
--- a/pkgs/json_rpc_2/test/server/server_test.dart
+++ b/pkgs/json_rpc_2/test/server/server_test.dart
@@ -70,7 +70,7 @@
             'data': {
               'request': {'jsonrpc': '2.0', 'method': 'foo', 'id': 1234},
               'full': 'FormatException: bad format',
-              'stack': new isInstanceOf<String>()
+              'stack': new TypeMatcher<String>()
             }
           }
         }));
@@ -168,7 +168,7 @@
               'data': {
                 'request': {'jsonrpc': '2.0', 'method': 'foo', 'id': 1234},
                 'full': 'FormatException: bad format',
-                'stack': new isInstanceOf<String>()
+                'stack': new TypeMatcher<String>()
               }
             }
           }));
diff --git a/pkgs/json_rpc_2/test/server/utils.dart b/pkgs/json_rpc_2/test/server/utils.dart
index f383f89..ef089ad 100644
--- a/pkgs/json_rpc_2/test/server/utils.dart
+++ b/pkgs/json_rpc_2/test/server/utils.dart
@@ -23,9 +23,10 @@
   json_rpc.Server get server => _server;
   json_rpc.Server _server;
 
-  ServerController() {
+  ServerController({json_rpc.ErrorCallback onUnhandledError}) {
     _server = new json_rpc.Server(
-        new StreamChannel(_requestController.stream, _responseController.sink));
+        new StreamChannel(_requestController.stream, _responseController.sink),
+        onUnhandledError: onUnhandledError);
     _server.listen();
   }
 
@@ -64,7 +65,7 @@
 /// `invalid_params` error code.
 Matcher throwsInvalidParams(String message) {
   return throwsA(predicate((error) {
-    expect(error, new isInstanceOf<json_rpc.RpcException>());
+    expect(error, new TypeMatcher<json_rpc.RpcException>());
     expect(error.code, equals(error_code.INVALID_PARAMS));
     expect(error.message, equals(message));
     return true;