OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library json_rpc_2.client; |
| 6 |
| 7 import 'dart:async'; |
| 8 |
| 9 import 'package:stack_trace/stack_trace.dart'; |
| 10 |
| 11 import 'exception.dart'; |
| 12 import 'two_way_stream.dart'; |
| 13 import 'utils.dart'; |
| 14 |
| 15 /// A JSON-RPC 2.0 client. |
| 16 /// |
| 17 /// A client calls methods on a server and handles the server's responses to |
| 18 /// those method calls. Methods can be called with [sendRequest], or with |
| 19 /// [sendNotification] if no response is expected. |
| 20 class Client { |
| 21 final TwoWayStream _streams; |
| 22 |
| 23 /// The next request id. |
| 24 var _id = 0; |
| 25 |
| 26 /// The current batch of requests to be sent together. |
| 27 /// |
| 28 /// Each element is a JSON-serializable object. |
| 29 List _batch; |
| 30 |
| 31 /// The map of request ids for pending requests to [Completer]s that will be |
| 32 /// completed with those requests' responses. |
| 33 final _pendingRequests = new Map<int, Completer>(); |
| 34 |
| 35 /// Creates a [Client] that writes requests to [requests] and reads responses |
| 36 /// from [responses]. |
| 37 /// |
| 38 /// If [responses] is a [StreamSink] as well as a [Stream] (for example, a |
| 39 /// `WebSocket`), [requests] may be omitted. |
| 40 /// |
| 41 /// Note that the client won't begin listening to [responses] until |
| 42 /// [Client.listen] is called. |
| 43 Client(Stream<String> responses, [StreamSink<String> requests]) |
| 44 : _streams = new TwoWayStream( |
| 45 "Client", responses, "responses", requests, "requests"); |
| 46 |
| 47 /// Creates a [Client] that writes decoded responses to [responses] and reads |
| 48 /// decoded requests from [requests]. |
| 49 /// |
| 50 /// Unlike [new Client], this doesn't read or write JSON strings. Instead, it |
| 51 /// reads and writes decoded maps or lists. |
| 52 /// |
| 53 /// If [responses] is a [StreamSink] as well as a [Stream], [requests] may be |
| 54 /// omitted. |
| 55 /// |
| 56 /// Note that the client won't begin listening to [responses] until |
| 57 /// [Client.listen] is called. |
| 58 Client.withoutJson(Stream responses, [StreamSink requests]) |
| 59 : _streams = new TwoWayStream.withoutJson( |
| 60 "Client", responses, "responses", requests, "requests"); |
| 61 |
| 62 /// Users of the library should not use this constructor. |
| 63 Client.internal(this._streams); |
| 64 |
| 65 /// Starts listening to the underlying stream. |
| 66 /// |
| 67 /// Returns a [Future] that will complete when the stream is closed or when it |
| 68 /// has an error. |
| 69 /// |
| 70 /// [listen] may only be called once. |
| 71 Future listen() => _streams.listen(_handleResponse); |
| 72 |
| 73 /// Closes the server's request sink and response subscription. |
| 74 /// |
| 75 /// Returns a [Future] that completes when all resources have been released. |
| 76 /// |
| 77 /// A client can't be closed before [listen] has been called. |
| 78 Future close() => _streams.close(); |
| 79 |
| 80 /// Sends a JSON-RPC 2 request to invoke the given [method]. |
| 81 /// |
| 82 /// If passed, [parameters] is the parameters for the method. This must be |
| 83 /// either an [Iterable] (to pass parameters by position) or a [Map] with |
| 84 /// [String] keys (to pass parameters by name). Either way, it must be |
| 85 /// JSON-serializable. |
| 86 /// |
| 87 /// If the request succeeds, this returns the response result as a decoded |
| 88 /// JSON-serializable object. If it fails, it throws an [RpcException] |
| 89 /// describing the failure. |
| 90 Future sendRequest(String method, [parameters]) { |
| 91 var id = _id++; |
| 92 _send(method, parameters, id); |
| 93 |
| 94 var completer = new Completer.sync(); |
| 95 _pendingRequests[id] = completer; |
| 96 return completer.future; |
| 97 } |
| 98 |
| 99 /// Sends a JSON-RPC 2 request to invoke the given [method] without expecting |
| 100 /// a response. |
| 101 /// |
| 102 /// If passed, [parameters] is the parameters for the method. This must be |
| 103 /// either an [Iterable] (to pass parameters by position) or a [Map] with |
| 104 /// [String] keys (to pass parameters by name). Either way, it must be |
| 105 /// JSON-serializable. |
| 106 /// |
| 107 /// Since this is just a notification to which the server isn't expected to |
| 108 /// send a response, it has no return value. |
| 109 void sendNotification(String method, [parameters]) => |
| 110 _send(method, parameters); |
| 111 |
| 112 /// A helper method for [sendRequest] and [sendNotification]. |
| 113 /// |
| 114 /// Sends a request to invoke [method] with [parameters]. If [id] is given, |
| 115 /// the request uses that id. |
| 116 void _send(String method, parameters, [int id]) { |
| 117 if (parameters is Iterable) parameters = parameters.toList(); |
| 118 if (parameters is! Map && parameters is! List && parameters != null) { |
| 119 throw new ArgumentError('Only maps and lists may be used as JSON-RPC ' |
| 120 'parameters, was "$parameters".'); |
| 121 } |
| 122 |
| 123 var message = { |
| 124 "jsonrpc": "2.0", |
| 125 "method": method |
| 126 }; |
| 127 if (id != null) message["id"] = id; |
| 128 if (parameters != null) message["params"] = parameters; |
| 129 |
| 130 if (_batch != null) { |
| 131 _batch.add(message); |
| 132 } else { |
| 133 _streams.add(message); |
| 134 } |
| 135 } |
| 136 |
| 137 /// Runs [callback] and batches any requests sent until it returns. |
| 138 /// |
| 139 /// A batch of requests is sent in a single message on the underlying stream, |
| 140 /// and the responses are likewise sent back in a single message. |
| 141 /// |
| 142 /// [callback] may be synchronous or asynchronous. If it returns a [Future], |
| 143 /// requests will be batched until that Future returns; otherwise, requests |
| 144 /// will only be batched while synchronously executing [callback]. |
| 145 /// |
| 146 /// If this is called in the context of another [withBatch] call, it just |
| 147 /// invokes [callback] without creating another batch. This means that |
| 148 /// responses are batched until the first batch ends. |
| 149 withBatch(callback()) { |
| 150 if (_batch != null) return callback(); |
| 151 |
| 152 _batch = []; |
| 153 return tryFinally(callback, () { |
| 154 _streams.add(_batch); |
| 155 _batch = null; |
| 156 }); |
| 157 } |
| 158 |
| 159 /// Handles a decoded response from the server. |
| 160 void _handleResponse(response) { |
| 161 if (response is List) { |
| 162 response.forEach(_handleSingleResponse); |
| 163 } else { |
| 164 _handleSingleResponse(response); |
| 165 } |
| 166 } |
| 167 |
| 168 /// Handles a decoded response from the server after batches have been |
| 169 /// resolved. |
| 170 void _handleSingleResponse(response) { |
| 171 if (!_isResponseValid(response)) return; |
| 172 var completer = _pendingRequests.remove(response["id"]); |
| 173 if (response.containsKey("result")) { |
| 174 completer.complete(response["result"]); |
| 175 } else { |
| 176 completer.completeError(new RpcException( |
| 177 response["error"]["code"], |
| 178 response["error"]["message"], |
| 179 data: response["error"]["data"]), |
| 180 new Chain.current()); |
| 181 } |
| 182 } |
| 183 |
| 184 /// Determines whether the server's response is valid per the spec. |
| 185 bool _isResponseValid(response) { |
| 186 if (response is! Map) return false; |
| 187 if (response["jsonrpc"] != "2.0") return false; |
| 188 if (!_pendingRequests.containsKey(response["id"])) return false; |
| 189 if (response.containsKey("result")) return true; |
| 190 |
| 191 if (!response.containsKey("error")) return false; |
| 192 var error = response["error"]; |
| 193 if (error is! Map) return false; |
| 194 if (error["code"] is! int) return false; |
| 195 if (error["message"] is! String) return false; |
| 196 return true; |
| 197 } |
| 198 } |
OLD | NEW |