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));
+}