Refresh credentials once (#73)

* Made sure refreshCredentials is only running once when multiple calls are made

* Added unit test

* Added line to changelog

* Updated version in pubspec
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f2809d..21b211c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 1.6.1
+
+* Added fix to make sure that credentials are only refreshed once when multiple calls are made.
+
 # 1.6.0
 
 * Added PKCE support to `AuthorizationCodeGrant`.
diff --git a/lib/src/client.dart b/lib/src/client.dart
index d33aaa0..7a78aa3 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -135,6 +135,9 @@
         params['error_uri'] == null ? null : Uri.parse(params['error_uri']));
   }
 
+  /// A [Future] used to track whether [refreshCredentials] is running.
+  Future<Credentials> _refreshingFuture;
+
   /// Explicitly refreshes this client's credentials. Returns this client.
   ///
   /// This will throw a [StateError] if the [Credentials] can't be refreshed, an
@@ -151,14 +154,26 @@
       throw StateError("$prefix can't be refreshed.");
     }
 
-    _credentials = await credentials.refresh(
-        identifier: identifier,
-        secret: secret,
-        newScopes: newScopes,
-        basicAuth: _basicAuth,
-        httpClient: _httpClient);
-
-    if (_onCredentialsRefreshed != null) _onCredentialsRefreshed(_credentials);
+    // To make sure that only one refresh happens when credentials are expired
+    // we track it using the [_refreshingFuture]. And also make sure that the
+    // _onCredentialsRefreshed callback is only called once.
+    if (_refreshingFuture == null) {
+      try {
+        _refreshingFuture = credentials.refresh(
+          identifier: identifier,
+          secret: secret,
+          newScopes: newScopes,
+          basicAuth: _basicAuth,
+          httpClient: _httpClient,
+        );
+        _credentials = await _refreshingFuture;
+        _onCredentialsRefreshed?.call(_credentials);
+      } finally {
+        _refreshingFuture = null;
+      }
+    } else {
+      await _refreshingFuture;
+    }
 
     return this;
   }
diff --git a/pubspec.yaml b/pubspec.yaml
index db168e3..4648628 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: oauth2
-version: 1.6.0
+version: 1.6.1
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/oauth2
 description: >-
diff --git a/test/client_test.dart b/test/client_test.dart
index 11b725c..5960cbd 100644
--- a/test/client_test.dart
+++ b/test/client_test.dart
@@ -65,6 +65,46 @@
       expect(client.credentials.accessToken, equals('new access token'));
     });
 
+    test(
+        'that can be refreshed refreshes only once if multiple requests are made',
+        () async {
+      var expiration = DateTime.now().subtract(Duration(hours: 1));
+      var credentials = oauth2.Credentials('access token',
+          refreshToken: 'refresh token',
+          tokenEndpoint: tokenEndpoint,
+          expiration: expiration);
+      var client = oauth2.Client(credentials,
+          identifier: 'identifier', secret: 'secret', httpClient: httpClient);
+
+      httpClient.expectRequest((request) {
+        expect(request.method, equals('POST'));
+        expect(request.url.toString(), equals(tokenEndpoint.toString()));
+        return Future.value(http.Response(
+            jsonEncode(
+                {'access_token': 'new access token', 'token_type': 'bearer'}),
+            200,
+            headers: {'content-type': 'application/json'}));
+      });
+
+      final numCalls = 2;
+
+      for (var i = 0; i < numCalls; i++) {
+        httpClient.expectRequest((request) {
+          expect(request.method, equals('GET'));
+          expect(request.url.toString(), equals(requestUri.toString()));
+          expect(request.headers['authorization'],
+              equals('Bearer new access token'));
+
+          return Future.value(http.Response('good job', 200));
+        });
+      }
+
+      await Future.wait(
+          List<Future>.generate(numCalls, (_) => client.read(requestUri)));
+
+      expect(client.credentials.accessToken, equals('new access token'));
+    });
+
     test('that onCredentialsRefreshed is called', () async {
       var callbackCalled = false;