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