Avoid reloading credentials file on each getter access (#4344)

* Avoid reloading credentials file on each getter access

* Check for tokensfile when attempting mutation
diff --git a/lib/src/authentication/credential.dart b/lib/src/authentication/credential.dart
index 7e03a4d..232ca5c 100644
--- a/lib/src/authentication/credential.dart
+++ b/lib/src/authentication/credential.dart
@@ -8,12 +8,15 @@
 import '../source/hosted.dart';
 import '../utils.dart';
 
-/// Token is a structure for storing authentication credentials for third-party
-/// pub registries. A token holds registry [url], credential [kind] and [token]
-/// itself.
+/// [Credential] is a structure for storing authentication credentials for
+/// third-party pub registries.
 ///
-/// Token could be serialized into and from JSON format structured like
-/// this:
+/// A [Credential] holds a registry [url], and either the [token] itself or the
+/// name of an environment variable [env] for looking up the token when
+/// authenticating.
+///
+/// For storing in the pub-tokens.json configuration, a [Credential] can be
+/// serialized into and from JSON format structured like this:
 ///
 /// ```json
 /// {
@@ -21,6 +24,17 @@
 ///   "token": "gjrjo7Tm2F0u64cTsECDq4jBNZYhco"
 /// }
 /// ```
+///
+/// or
+///
+/// /// ```json
+/// {
+///   "url": "https://example.com/",
+///   "env": "TOKEN_ENV_VAR"
+/// }
+/// ```
+///
+/// Unknown JSON properties will be preserved when reencoding.
 class Credential {
   /// Internal constructor that's only used by [Credential.fromJson].
   Credential._internal({
diff --git a/lib/src/authentication/token_store.dart b/lib/src/authentication/token_store.dart
index 104dbd1..d11f63c 100644
--- a/lib/src/authentication/token_store.dart
+++ b/lib/src/authentication/token_store.dart
@@ -19,11 +19,13 @@
   /// Cache directory.
   final String? configDir;
 
-  /// List of saved authentication tokens.
+  /// Enumeration of saved authentication tokens.
   ///
-  /// Modifying this field will not write changes to the disk. You have to call
-  /// [flush] to save changes.
-  Iterable<Credential> get credentials => _loadCredentials();
+  /// Call [addCredential] and [removeCredential] to update the credentials
+  /// while saving changes to disk.
+  Iterable<Credential> get credentials => _credentials;
+
+  late final List<Credential> _credentials = _loadCredentials();
 
   /// Reads "pub-tokens.json" and parses / deserializes it into list of
   /// [Credential].
@@ -103,7 +105,7 @@
   void _saveCredentials(List<Credential> credentials) {
     final tokensFile = this.tokensFile;
     if (tokensFile == null) {
-      missingConfigDir();
+      throw AssertionError('Bad state');
     }
     ensureDir(p.dirname(tokensFile));
     writeTextFile(
@@ -117,6 +119,9 @@
 
   /// Adds [token] into store and writes into disk.
   void addCredential(Credential token) {
+    if (tokensFile == null) {
+      missingConfigDir();
+    }
     final credentials = _loadCredentials();
 
     // Remove duplicate tokens
@@ -128,20 +133,23 @@
   /// Removes tokens with matching [hostedUrl] from store. Returns whether or
   /// not there's a stored token with matching url.
   bool removeCredential(Uri hostedUrl) {
-    final credentials = _loadCredentials();
-
+    if (tokensFile == null) {
+      missingConfigDir();
+    }
     var i = 0;
     var found = false;
-    while (i < credentials.length) {
-      if (credentials[i].url == hostedUrl) {
-        credentials.removeAt(i);
+    while (i < _credentials.length) {
+      if (_credentials[i].url == hostedUrl) {
+        _credentials.removeAt(i);
         found = true;
       } else {
         i++;
       }
     }
 
-    _saveCredentials(credentials);
+    if (found) {
+      _saveCredentials(_credentials);
+    }
 
     return found;
   }
@@ -150,7 +158,7 @@
   /// matching credential is found.
   Credential? findCredential(Uri hostedUrl) {
     Credential? matchedCredential;
-    for (final credential in credentials) {
+    for (final credential in _credentials) {
       if (credential.url == hostedUrl && credential.isValid()) {
         if (matchedCredential == null) {
           matchedCredential = credential;
@@ -170,7 +178,7 @@
   /// Returns whether or not store contains a token that could be used for
   /// authenticating given [url].
   bool hasCredential(Uri url) {
-    return credentials.any((it) => it.url == url && it.isValid());
+    return _credentials.any((it) => it.url == url && it.isValid());
   }
 
   /// Deletes pub-tokens.json file from the disk.