Separate state for sources (#4811)
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index a0e2ab2..4183294 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -331,7 +331,7 @@
 
   Pubspec(
     String name, {
-    Version? version,
+    super.version,
     Iterable<PackageRange>? dependencies,
     Iterable<PackageRange>? devDependencies,
     Iterable<PackageRange>? dependencyOverrides,
@@ -364,11 +364,7 @@
        // This is a dummy value. Dependencies should already be resolved, so we
        // never need to do relative resolutions.
        _containingDescription = ResolvedRootDescription.fromDir('.'),
-       super(
-         fields == null ? YamlMap() : YamlMap.wrap(fields),
-         name: name,
-         version: version,
-       );
+       super(fields == null ? YamlMap() : YamlMap.wrap(fields), name: name);
 
   /// Returns a Pubspec object for an already-parsed map representing its
   /// contents.
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart
index 1293b9a..e8d2b69 100644
--- a/lib/src/solver/version_solver.dart
+++ b/lib/src/solver/version_solver.dart
@@ -126,7 +126,7 @@
     );
 
     try {
-      return await _systemCache.hosted.withPrefetching(() async {
+      return await _systemCache.hosted.withPrefetching(_systemCache, () async {
         String? next = _root.name;
         while (next != null) {
           _propagate(next);
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index a74e306..9dd19fe 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -266,21 +266,6 @@
     return p.url.normalize(parsed.toString());
   }
 
-  /// Limit the number of concurrent git operations to 1.
-  // TODO(sigurdm): Use RateLimitedScheduler.
-  final Pool _pool = Pool(1);
-
-  /// A map from revision cache locations to futures that will complete once
-  /// they're finished being cloned.
-  ///
-  /// This lets us avoid race conditions when getting multiple different
-  /// packages from the same repository.
-  final _revisionCacheClones = <String, Future<void>>{};
-
-  /// The paths to the canonical clones of repositories for which "git fetch"
-  /// has already been run during this run of pub.
-  final _updatedRepos = <String>{};
-
   /// Given a Git repo that contains a pub package, gets the name of the pub
   /// package.
   ///
@@ -305,7 +290,7 @@
       containingDir: relativeTo,
       tagPattern: tagPattern,
     );
-    return await _pool.withResource(() async {
+    return await cache.gitCache.pool.withResource(() async {
       await _ensureRepoCache(description, cache);
       final path = _repoCachePath(description, cache);
 
@@ -370,7 +355,7 @@
     if (description is! GitDescription) {
       throw StateError('Called with wrong ref');
     }
-    return await _pool.withResource(() async {
+    return await cache.gitCache.pool.withResource(() async {
       await _ensureRepoCache(description, cache);
       final path = _repoCachePath(description, cache);
       final result = <PackageId>[];
@@ -417,7 +402,7 @@
     if (description is! ResolvedGitDescription) {
       throw StateError('Called with wrong ref');
     }
-    final pubspec = await _pool.withResource(
+    final pubspec = await cache.gitCache.pool.withResource(
       () => _describeUncached(id.toRef(), description.resolvedRef, cache),
     );
     if (pubspec.version != id.version) {
@@ -430,8 +415,6 @@
     return pubspec;
   }
 
-  final Map<(PackageRef, String), Pubspec> _pubspecAtRevisionCache = {};
-
   /// Like [describeUncached], but takes a separate [ref] and Git [revision]
   /// rather than a single ID.
   Future<Pubspec> _describeUncached(
@@ -443,7 +426,10 @@
     if (description is! GitDescription) {
       throw ArgumentError('Wrong source');
     }
-    return _pubspecAtRevisionCache[(ref, revision)] ??= await () async {
+    return cache.gitCache.pubspecAtRevisionCache[(
+      ref,
+      revision,
+    )] ??= await () async {
       await _ensureRevision(description, revision, cache);
       final resolvedDescription = ResolvedGitDescription(description, revision);
       return Pubspec.parse(
@@ -473,7 +459,7 @@
     PackageId id,
     SystemCache cache,
   ) async {
-    return await _pool.withResource(() async {
+    return await cache.gitCache.pool.withResource(() async {
       var didUpdate = false;
       final ref = id.toRef();
       final description = ref.description;
@@ -495,25 +481,28 @@
 
       final revisionCachePath = _revisionCachePath(id, cache);
       final path = description.path;
-      await _revisionCacheClones.putIfAbsent(revisionCachePath, () async {
-        if (!entryExists(revisionCachePath)) {
-          await _cloneViaTemp(
-            _repoCachePath(description, cache),
-            revisionCachePath,
-            cache,
-          );
-          await git.run([
-            'config',
-            'remote.origin.lfsurl',
-            description.url,
-          ], workingDir: revisionCachePath);
-          await _checkOut(revisionCachePath, resolvedRef);
-          _writePackageList(revisionCachePath, [path]);
-          didUpdate = true;
-        } else {
-          didUpdate |= _updatePackageList(revisionCachePath, path);
-        }
-      });
+      await cache.gitCache.revisionCacheClones.putIfAbsent(
+        revisionCachePath,
+        () async {
+          if (!entryExists(revisionCachePath)) {
+            await _cloneViaTemp(
+              _repoCachePath(description, cache),
+              revisionCachePath,
+              cache,
+            );
+            await git.run([
+              'config',
+              'remote.origin.lfsurl',
+              description.url,
+            ], workingDir: revisionCachePath);
+            await _checkOut(revisionCachePath, resolvedRef);
+            _writePackageList(revisionCachePath, [path]);
+            didUpdate = true;
+          } else {
+            didUpdate |= _updatePackageList(revisionCachePath, path);
+          }
+        },
+      );
       return DownloadPackageResult(id, didUpdate: didUpdate);
     });
   }
@@ -646,7 +635,7 @@
     SystemCache cache,
   ) async {
     final path = _repoCachePath(description, cache);
-    if (_updatedRepos.contains(path)) return false;
+    if (cache.gitCache.updatedRepos.contains(path)) return false;
 
     await _deleteGitRepoIfInvalid(path);
 
@@ -672,7 +661,7 @@
     SystemCache cache,
   ) async {
     final path = _repoCachePath(description, cache);
-    if (_updatedRepos.contains(path)) return false;
+    if (cache.gitCache.updatedRepos.contains(path)) return false;
 
     await _deleteGitRepoIfInvalid(path);
 
@@ -693,14 +682,14 @@
     SystemCache cache,
   ) async {
     final path = _repoCachePath(description, cache);
-    assert(!_updatedRepos.contains(path));
+    assert(!cache.gitCache.updatedRepos.contains(path));
     try {
       await _cloneViaTemp(description.url, path, cache, mirror: true);
     } catch (_) {
       await _deleteGitRepoIfInvalid(path);
       rethrow;
     }
-    _updatedRepos.add(path);
+    cache.gitCache.updatedRepos.add(path);
   }
 
   /// Runs "git fetch" in the canonical clone of the repository referred to by
@@ -714,9 +703,9 @@
     SystemCache cache,
   ) async {
     final path = _repoCachePath(description, cache);
-    if (_updatedRepos.contains(path)) return false;
+    if (cache.gitCache.updatedRepos.contains(path)) return false;
     await git.run([_gitDirArg(path), 'fetch'], workingDir: path);
-    _updatedRepos.add(path);
+    cache.gitCache.updatedRepos.add(path);
     return true;
   }
 
@@ -1226,3 +1215,22 @@
     r'$',
   );
 }
+
+/// State that is cached for the GitSource.
+final class GitSourceCache {
+  /// Limit the number of concurrent git operations to 1.
+  final pool = Pool(1);
+
+  /// A map from revision cache locations to futures that will complete once
+  /// they're finished being cloned.
+  ///
+  /// This lets us avoid race conditions when getting multiple different
+  /// packages from the same repository.
+  final revisionCacheClones = <String, Future<void>>{};
+
+  /// The paths to the canonical clones of repositories for which "git fetch"
+  /// has already been run during this run of pub.
+  final updatedRepos = <String>{};
+
+  final pubspecAtRevisionCache = <(PackageRef, String), Pubspec>{};
+}
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index d13db32..09dc7ff 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -8,7 +8,7 @@
 import 'dart:io';
 import 'dart:typed_data';
 
-import 'package:collection/collection.dart' show IterableExtension, maxBy;
+import 'package:collection/collection.dart' show IterableExtension;
 import 'package:crypto/crypto.dart';
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
@@ -372,10 +372,7 @@
     r'^[a-zA-Z_]+[a-zA-Z0-9_]*$',
   );
 
-  late final RateLimitedScheduler<_RefAndCache, List<_VersionInfo>> _scheduler =
-      RateLimitedScheduler(_fetchVersions, maxConcurrentOperations: 10);
-
-  List<_VersionInfo> _versionInfoFromPackageListing(
+  List<HostedVersionInfo> _versionInfoFromPackageListing(
     Map body,
     PackageRef ref,
     Uri location,
@@ -443,7 +440,7 @@
         isRetracted: retracted,
         advisoriesUpdated: advisoriesDate,
       );
-      return _VersionInfo(
+      return HostedVersionInfo(
         pubspec.version,
         pubspec,
         Uri.parse(archiveUrl),
@@ -453,7 +450,7 @@
     }).toList();
   }
 
-  Future<List<_VersionInfo>> _fetchVersionsNoPrefetching(
+  Future<List<HostedVersionInfo>> _fetchVersionsNoPrefetching(
     PackageRef ref,
     SystemCache cache,
   ) async {
@@ -469,7 +466,7 @@
 
     final String bodyText;
     final dynamic body;
-    final List<_VersionInfo> result;
+    final List<HostedVersionInfo> result;
     try {
       // TODO(sigurdm): Implement cancellation of requests. This probably
       // requires resolution of: https://github.com/dart-lang/http/issues/424.
@@ -509,29 +506,35 @@
     return result;
   }
 
-  Future<List<_VersionInfo>> _fetchVersions(_RefAndCache refAndCache) async {
+  Future<List<HostedVersionInfo>> fetchVersions(
+    HostedRefAndCache refAndCache,
+  ) async {
     final ref = refAndCache.ref;
     final description = ref.description;
     if (description is! HostedDescription) {
       throw ArgumentError('Wrong source');
     }
     final preschedule =
-        Zone.current[_prefetchingKey] as void Function(_RefAndCache)?;
+        Zone.current[_prefetchingKey] as void Function(HostedRefAndCache)?;
 
     /// Prefetch the dependencies of the latest version, we are likely to need
     /// them later.
     void prescheduleDependenciesOfLatest(
-      List<_VersionInfo>? listing,
+      List<HostedVersionInfo>? listing,
       SystemCache cache,
     ) {
       if (listing == null || listing.isEmpty) return;
-      final latestVersion =
-          maxBy<_VersionInfo, Version>(listing, (e) => e.version)!;
+      var latestVersion = listing.first;
+      for (final e in listing.skip(1)) {
+        if (e.version > latestVersion.version) {
+          latestVersion = e;
+        }
+      }
       final dependencies = latestVersion.pubspec.dependencies.values;
       unawaited(() async {
         for (final packageRange in dependencies) {
           if (packageRange.source is HostedSource) {
-            preschedule!(_RefAndCache(packageRange.toRef(), cache));
+            preschedule!(HostedRefAndCache(packageRange.toRef(), cache));
           }
         }
       }());
@@ -823,7 +826,8 @@
   /// Invariant: Entries in this cache are the parsed version of the exact same
   /// information cached on disk. I.e. if the entry is present in this cache,
   /// there will not be a newer version on disk.
-  final Map<PackageRef, (DateTime, List<_VersionInfo>)> _responseCache = {};
+  final Map<PackageRef, (DateTime, List<HostedVersionInfo>)> _responseCache =
+      {};
 
   /// If a cached version listing response for [ref] exists on disk and is less
   /// than [maxAge] old it is parsed and returned.
@@ -832,7 +836,7 @@
   ///
   /// If [maxAge] is not given, we will try to get the cached version no matter
   /// how old it is.
-  Future<List<_VersionInfo>?> _cachedVersionListingResponse(
+  Future<List<HostedVersionInfo>?> _cachedVersionListingResponse(
     PackageRef ref,
     SystemCache cache, {
     Duration? maxAge,
@@ -954,7 +958,7 @@
         PackageStatus();
   }
 
-  Future<_VersionInfo?> _versionInfo(
+  Future<HostedVersionInfo?> _versionInfo(
     PackageRef ref,
     Version version,
     SystemCache cache, {
@@ -967,10 +971,14 @@
       if (versionListing == null) {
         return null;
       }
-      return versionListing.firstWhereOrNull((l) => l.version == version);
+      return versionListing.firstWhereOrNull(
+        (HostedVersionInfo l) => l.version == version,
+      );
     }
     // Did we already get info for this package?
-    var versionListing = _scheduler.peek(_RefAndCache(ref, cache));
+    var versionListing = cache.hostedCache.scheduler.peek(
+      HostedRefAndCache(ref, cache),
+    );
     if (maxAge != null) {
       // Do we have a cached version response on disk?
       versionListing ??= await _cachedVersionListingResponse(
@@ -980,11 +988,11 @@
       );
     }
     // Otherwise retrieve the info from the host.
-    versionListing ??= await _scheduler
-        .schedule(_RefAndCache(ref, cache))
+    versionListing ??= await cache.hostedCache.scheduler
+        .schedule(HostedRefAndCache(ref, cache))
         // Failures retrieving the listing here should just be ignored.
         .catchError(
-          (_) async => <_VersionInfo>[],
+          (_) async => <HostedVersionInfo>[],
           test: (error) => error is Exception,
         );
 
@@ -1056,7 +1064,9 @@
 
       return offlineVersions;
     }
-    var versionListing = _scheduler.peek(_RefAndCache(ref, cache));
+    var versionListing = cache.hostedCache.scheduler.peek(
+      HostedRefAndCache(ref, cache),
+    );
     if (maxAge != null) {
       // Do we have a cached version response on disk?
       versionListing ??= await _cachedVersionListingResponse(
@@ -1065,7 +1075,9 @@
         maxAge: maxAge,
       );
     }
-    versionListing ??= await _scheduler.schedule(_RefAndCache(ref, cache));
+    versionListing ??= await cache.hostedCache.scheduler.schedule(
+      HostedRefAndCache(ref, cache),
+    );
     return versionListing
         .map(
           (i) => PackageId(
@@ -1143,7 +1155,9 @@
         hint: 'Try again without --offline!',
       );
     }
-    final versions = await _scheduler.schedule(_RefAndCache(id.toRef(), cache));
+    final versions = await cache.hostedCache.scheduler.schedule(
+      HostedRefAndCache(id.toRef(), cache),
+    );
     final url = _listVersionsUrl(id.toRef());
     return versions.firstWhereOrNull((i) => i.version == id.version)?.pubspec ??
         (throw PackageNotFoundException('Could not find package $id at $url'));
@@ -1508,7 +1522,9 @@
     // a custom package server may include a temporary signature in the
     // query-string as is the case with signed S3 URLs. And we wish to allow for
     // such URLs to be used.
-    final versions = await _scheduler.schedule(_RefAndCache(id.toRef(), cache));
+    final versions = await cache.hostedCache.scheduler.schedule(
+      HostedRefAndCache(id.toRef(), cache),
+    );
     final versionInfo = versions.firstWhereOrNull(
       (i) => i.version == id.version,
     );
@@ -1830,8 +1846,13 @@
 
   /// Enables speculative prefetching of dependencies of packages queried with
   /// [doGetVersions].
-  Future<T> withPrefetching<T>(Future<T> Function() callback) async {
-    return await _scheduler.withPrescheduling((preschedule) async {
+  Future<T> withPrefetching<T>(
+    SystemCache cache,
+    Future<T> Function() callback,
+  ) async {
+    return await cache.hostedCache.scheduler.withPrescheduling((
+      preschedule,
+    ) async {
       return await runZoned(
         callback,
         zoneValues: {_prefetchingKey: preschedule},
@@ -1948,7 +1969,7 @@
 }
 
 /// Information about a package version retrieved from /api/packages/$package<
-class _VersionInfo {
+class HostedVersionInfo {
   final Pubspec pubspec;
   final Uri archiveUrl;
   final Version version;
@@ -1957,7 +1978,7 @@
   final Uint8List? archiveSha256;
   final PackageStatus status;
 
-  _VersionInfo(
+  HostedVersionInfo(
     this.version,
     this.pubspec,
     this.archiveUrl,
@@ -2046,15 +2067,16 @@
 }
 
 // TODO(sigurdm): This is quite inelegant.
-class _RefAndCache {
+class HostedRefAndCache {
   final PackageRef ref;
   final SystemCache cache;
-  _RefAndCache(this.ref, this.cache);
+  HostedRefAndCache(this.ref, this.cache);
 
   @override
   int get hashCode => ref.hashCode;
   @override
-  bool operator ==(Object other) => other is _RefAndCache && other.ref == ref;
+  bool operator ==(Object other) =>
+      other is HostedRefAndCache && other.ref == ref;
 }
 
 /// A sink that can only have `add` called once, and that can retrieve the
@@ -2157,3 +2179,19 @@
 
   return null;
 }
+
+/// State that is cached for the HostedSource.
+final class HostedSourceCache {
+  /// The scheduler used to fetch version information for packages.
+  ///
+  /// This allows rate-limiting requests and speculative pre-fetching of
+  /// dependencies.
+  final RateLimitedScheduler<HostedRefAndCache, List<HostedVersionInfo>>
+  scheduler;
+
+  HostedSourceCache(HostedSource hosted)
+    : scheduler = RateLimitedScheduler(
+        (HostedRefAndCache refAndCache) => hosted.fetchVersions(refAndCache),
+        maxConcurrentOperations: 10,
+      );
+}
diff --git a/lib/src/system_cache.dart b/lib/src/system_cache.dart
index 3eacc07..58a53ad 100644
--- a/lib/src/system_cache.dart
+++ b/lib/src/system_cache.dart
@@ -6,6 +6,7 @@
 import 'dart:io';
 
 import 'package:crypto/crypto.dart';
+
 import 'package:pub_semver/pub_semver.dart';
 
 import 'authentication/token_store.dart';
@@ -18,6 +19,7 @@
 import 'path.dart';
 import 'platform_info.dart';
 import 'pubspec.dart';
+
 import 'source.dart';
 import 'source/cached.dart';
 import 'source/git.dart';
@@ -37,6 +39,12 @@
   String get rootDir => _rootDir ??= defaultDir;
   String? _rootDir;
 
+  /// Cache for HostedSource.
+  late final hostedCache = HostedSourceCache(hosted);
+
+  /// Cache for GitSource.
+  final gitCache = GitSourceCache();
+
   String rootDirForSource(CachedSource source) => p.join(rootDir, source.name);
 
   String get tempDir => p.join(rootDir, '_temp');
diff --git a/pubspec.lock b/pubspec.lock
index 52755d8..65fe612 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -466,4 +466,4 @@
     source: hosted
     version: "2.2.2"
 sdks:
-  dart: ">=3.11.0 <4.0.0"
+  dart: ">=3.9.0 <4.0.0"