| 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 /// Starts listening to the underlying stream. | |
| 63 /// | |
| 64 /// Returns a [Future] that will complete when the stream is closed or when it | |
| 65 /// has an error. | |
| 66 /// | |
| 67 /// [listen] may only be called once. | |
| 68 Future listen() => _streams.listen(_handleResponse); | |
| 69 | |
| 70 /// Closes the server's request sink and response subscription. | |
| 71 /// | |
| 72 /// Returns a [Future] that completes when all resources have been released. | |
| 73 /// | |
| 74 /// A client can't be closed before [listen] has been called. | |
| 75 Future close() => _streams.close(); | |
| 76 | |
| 77 /// Sends a JSON-RPC 2 request to invoke the given [method]. | |
| 78 /// | |
| 79 /// If passed, [parameters] is the parameters for the method. This must be | |
| 80 /// either an [Iterable] (to pass parameters by position) or a [Map] with | |
| 81 /// [String] keys (to pass parameters by name). Either way, it must be | |
| 82 /// JSON-serializable. | |
| 83 /// | |
| 84 /// If the request succeeds, this returns the response result as a decoded | |
| 85 /// JSON-serializable object. If it fails, it throws an [RpcException] | |
| 86 /// describing the failure. | |
| 87 Future sendRequest(String method, [parameters]) { | |
| 88 var id = _id++; | |
| 89 _send(method, parameters, id); | |
| 90 | |
| 91 var completer = new Completer.sync(); | |
| 92 _pendingRequests[id] = completer; | |
| 93 return completer.future; | |
| 94 } | |
| 95 | |
| 96 /// Sends a JSON-RPC 2 request to invoke the given [method] without expecting | |
| 97 /// a response. | |
| 98 /// | |
| 99 /// If passed, [parameters] is the parameters for the method. This must be | |
| 100 /// either an [Iterable] (to pass parameters by position) or a [Map] with | |
| 101 /// [String] keys (to pass parameters by name). Either way, it must be | |
| 102 /// JSON-serializable. | |
| 103 /// | |
| 104 /// Since this is just a notification to which the server isn't expected to | |
| 105 /// send a response, it has no return value. | |
| 106 void sendNotification(String method, [parameters]) => | |
| 107 _send(method, parameters); | |
| 108 | |
| 109 /// A helper method for [sendRequest] and [sendNotification]. | |
| 110 /// | |
| 111 /// Sends a request to invoke [method] with [parameters]. If [id] is given, | |
| 112 /// the request uses that id. | |
| 113 void _send(String method, parameters, [int id]) { | |
| 114 if (parameters is Iterable) parameters = parameters.toList(); | |
| 115 if (parameters is! Map && parameters is! List && parameters != null) { | |
| 116 throw new ArgumentError('Only maps and lists may be used as JSON-RPC ' | |
| 117 'parameters, was "$parameters".'); | |
| 118 } | |
| 119 | |
| 120 var message = { | |
| 121 "jsonrpc": "2.0", | |
| 122 "method": method | |
| 123 }; | |
| 124 if (id != null) message["id"] = id; | |
| 125 if (parameters != null) message["params"] = parameters; | |
| 126 | |
| 127 if (_batch != null) { | |
| 128 _batch.add(message); | |
| 129 } else { | |
| 130 _streams.add(message); | |
| 131 } | |
| 132 } | |
| 133 | |
| 134 /// Runs [callback] and batches any requests sent until it returns. | |
| 135 /// | |
| 136 /// A batch of requests is sent in a single message on the underlying stream, | |
| 137 /// and the responses are likewise sent back in a single message. | |
| 138 /// | |
| 139 /// [callback] may be synchronous or asynchronous. If it returns a [Future], | |
| 140 /// requests will be batched until that Future returns; otherwise, requests | |
| 141 /// will only be batched while synchronously executing [callback]. | |
| 142 /// | |
| 143 /// If this is called in the context of another [withBatch] call, it just | |
| 144 /// invokes [callback] without creating another batch. This means that | |
| 145 /// responses are batched until the first batch ends. | |
| 146 withBatch(callback()) { | |
| 147 if (_batch != null) return callback(); | |
| 148 | |
| 149 _batch = []; | |
| 150 return tryFinally(callback, () { | |
| 151 _streams.add(_batch); | |
| 152 _batch = null; | |
| 153 }); | |
| 154 } | |
| 155 | |
| 156 /// Handles a decoded response from the server. | |
| 157 void _handleResponse(response) { | |
| 158 if (response is List) { | |
| 159 response.forEach(_handleSingleResponse); | |
| 160 } else { | |
| 161 _handleSingleResponse(response); | |
| 162 } | |
| 163 } | |
| 164 | |
| 165 /// Handles a decoded response from the server after batches have been | |
| 166 /// resolved. | |
| 167 void _handleSingleResponse(response) { | |
| 168 if (!_isResponseValid(response)) return; | |
| 169 var completer = _pendingRequests.remove(response["id"]); | |
| 170 if (response.containsKey("result")) { | |
| 171 completer.complete(response["result"]); | |
| 172 } else { | |
| 173 completer.completeError(new RpcException( | |
| 174 response["error"]["code"], | |
| 175 response["error"]["message"], | |
| 176 data: response["error"]["data"]), | |
| 177 new Chain.current()); | |
| 178 } | |
| 179 } | |
| 180 | |
| 181 /// Determines whether the server's response is valid per the spec. | |
| 182 bool _isResponseValid(response) { | |
| 183 if (response is! Map) return false; | |
| 184 if (response["jsonrpc"] != "2.0") return false; | |
| 185 if (!_pendingRequests.containsKey(response["id"])) return false; | |
| 186 if (response.containsKey("result")) return true; | |
| 187 | |
| 188 if (!response.containsKey("error")) return false; | |
| 189 var error = response["error"]; | |
| 190 if (error is! Map) return false; | |
| 191 if (error["code"] is! int) return false; | |
| 192 if (error["message"] is! String) return false; | |
| 193 return true; | |
| 194 } | |
| 195 } | |
| OLD | NEW |