Add an http_throttle package.
BUG=20058
R=alanknight@google.com
Review URL: https://codereview.chromium.org//418103004
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/http_throttle@38560 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5c60afe
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2014, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fcbacdb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+`http_throttle` is middleware for the [http package][] that throttles the number
+of concurrent requests that an HTTP client can make.
+
+```dart
+// This client allows 32 concurrent requests.
+final client = new ThrottleClient(32);
+
+Future<List<String>> readAllUrls(Iterable<Uri> urls) {
+ return Future.wait(urls.map((url) {
+ // You can safely call as many client methods as you want concurrently, and
+ // ThrottleClient will ensure that only 32 underlying HTTP requests will be
+ // open at once.
+ return client.read(url);
+ }));
+}
+```
+
+[http package]: pub.dartlang.org/packages/http
diff --git a/lib/http_throttle.dart b/lib/http_throttle.dart
new file mode 100644
index 0000000..7cd95a6
--- /dev/null
+++ b/lib/http_throttle.dart
@@ -0,0 +1,50 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library http_throttle;
+
+import 'dart:async';
+
+import 'package:http/http.dart';
+import 'package:pool/pool.dart';
+
+/// A middleware client that throttles the number of concurrent requests.
+///
+/// As long as the number of requests is within the limit, this works just like
+/// a normal client. If a request is made beyond the limit, the underlying HTTP
+/// request won't be sent until other requests have completed.
+class ThrottleClient extends BaseClient {
+ final Pool _pool;
+ final Client _inner;
+
+ /// Creates a new client that allows no more than [maxActiveRequests]
+ /// concurrent requests.
+ ///
+ /// If [inner] is passed, it's used as the inner client for sending HTTP
+ /// requests. It defaults to `new http.Client()`.
+ ThrottleClient(int maxActiveRequests, [Client inner])
+ : _pool = new Pool(maxActiveRequests),
+ _inner = inner == null ? new Client() : inner;
+
+ Future<StreamedResponse> send(BaseRequest request) {
+ return _pool.request().then((resource) {
+ return _inner.send(request).then((response) {
+ var stream = response.stream.transform(
+ new StreamTransformer.fromHandlers(handleDone: (sink) {
+ resource.release();
+ sink.close();
+ }));
+ return new StreamedResponse(stream, response.statusCode,
+ contentLength: response.contentLength,
+ request: response.request,
+ headers: response.headers,
+ isRedirect: response.isRedirect,
+ persistentConnection: response.persistentConnection,
+ reasonPhrase: response.reasonPhrase);
+ });
+ });
+ }
+
+ void close() => _inner.close();
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..f7cc6b9
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,8 @@
+name: http_throttle
+version: 1.0.0
+description: HTTP client middleware that throttles requests.
+dependencies:
+ http: ">=0.9.0 <0.12.0"
+ pool: ">=1.0.0 <2.0.0"
+dev_dependencies:
+ unittest: ">=0.11.0 <0.12.0"
diff --git a/test/http_throttle_test.dart b/test/http_throttle_test.dart
new file mode 100644
index 0000000..13fe058
--- /dev/null
+++ b/test/http_throttle_test.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart';
+import 'package:http_throttle/http_throttle.dart';
+import 'package:unittest/unittest.dart';
+
+void main() {
+ test("makes requests until the limit is hit", () {
+ var pendingResponses = [];
+ var client = new ThrottleClient(10, new MockClient((request) {
+ var completer = new Completer();
+ pendingResponses.add(completer);
+ return completer.future.then((response) {
+ pendingResponses.remove(completer);
+ return response;
+ });
+ }));
+
+ // Make the first batch of requests. All of these should be sent
+ // immediately.
+ for (var i = 0; i < 10; i++) {
+ client.get('/');
+ }
+
+ return pumpEventQueue().then((_) {
+ // All ten of the requests should have responses pending.
+ expect(pendingResponses, hasLength(10));
+
+ // Make the second batch of requests. None of these should be sent
+ // until the previous batch has finished.
+ for (var i = 0; i < 5; i++) {
+ client.get('/');
+ }
+
+ return pumpEventQueue();
+ }).then((_) {
+ // Only the original ten requests should have responses pending.
+ expect(pendingResponses, hasLength(10));
+
+ // Send the first ten responses, allowing the next batch of requests to
+ // fire.
+ for (var completer in pendingResponses) {
+ completer.complete(new http.Response("done", 200));
+ }
+
+ return pumpEventQueue();
+ }).then((_) {
+ // Now the second batch of responses should be pending.
+ expect(pendingResponses, hasLength(5));
+ });
+ });
+}
+
+/// Returns a [Future] that completes after pumping the event queue [times]
+/// times. By default, this should pump the event queue enough times to allow
+/// any code to run, as long as it's not waiting on some external event.
+Future pumpEventQueue([int times = 20]) {
+ if (times == 0) return new Future.value();
+ // We use a delayed future to allow microtask events to finish. The
+ // Future.value or Future() constructors use scheduleMicrotask themselves and
+ // would therefore not wait for microtask callbacks that are scheduled after
+ // invoking this method.
+ return new Future.delayed(Duration.ZERO, () => pumpEventQueue(times - 1));
+}