Skip to content

Commit 19290dc

Browse files
committed
V9.2.0: Cache allReady() future for watch_it scope detection
- allReady() now caches its Future and returns the same instance on repeated calls - Cache is invalidated when new async singletons are registered - Pending async singletons complete their ready completer when removed via unregister() or scope reset
1 parent 294ae0f commit 19290dc

File tree

4 files changed

+120
-5
lines changed

4 files changed

+120
-5
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [9.2.0] - 2025-12-03
2+
3+
* `allReady()` now caches its Future and returns the same instance on repeated calls
4+
* Cache is automatically invalidated when new async singletons are registered
5+
* This enables watch_it to detect when async registrations change (e.g., after `pushNewScope`)
6+
* Fixed: pending async singletons now complete their ready completer when removed via `unregister()` or scope reset, allowing `allReady()` to complete properly
7+
18
## [9.1.1] - 2025-11-25
29

310
* Updated example project

lib/get_it_impl.dart

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,10 @@ class _Scope {
447447
..sort((a, b) => b.registrationNumber.compareTo(a.registrationNumber));
448448

449449
for (final registration in registrations) {
450+
// Complete pending completers so allReady() can complete
451+
if (!registration.isReady) {
452+
registration._readyCompleter.complete();
453+
}
450454
await registration.dispose();
451455
}
452456
}
@@ -604,6 +608,9 @@ class _GetItImplementation implements GetIt {
604608
/// Completer.
605609
final _globalReadyCompleter = Completer();
606610

611+
/// Cached allReady future - invalidated when new async singletons are registered
612+
Future<void>? _cachedAllReadyFuture;
613+
607614
/// By default it's not allowed to register a type a second time.
608615
/// If you really need to you can disable the asserts by setting[allowReassignment]= true
609616
@override
@@ -1430,6 +1437,12 @@ class _GetItImplementation implements GetIt {
14301437
registrationToRemove._referenceCount--;
14311438
return;
14321439
}
1440+
1441+
// Complete pending completer so allReady() can complete
1442+
if (!registrationToRemove.isReady) {
1443+
registrationToRemove._readyCompleter.complete();
1444+
}
1445+
14331446
final typeRegistration = registrationToRemove.registeredIn;
14341447

14351448
if (registrationToRemove.isNamedRegistration) {
@@ -1659,6 +1672,7 @@ class _GetItImplementation implements GetIt {
16591672
}
16601673
_scopes.removeRange(1, _scopes.length);
16611674
await resetScope(dispose: dispose);
1675+
_cachedAllReadyFuture = null;
16621676
assert(() {
16631677
_fireDevToolEvent('reset', {'dispose': dispose});
16641678
return true;
@@ -2069,6 +2083,9 @@ class _GetItImplementation implements GetIt {
20692083
// it is dependent on other registered Singletons.
20702084
if ((isAsync || (dependsOn?.isNotEmpty ?? false)) &&
20712085
type == ObjectRegistrationType.constant) {
2086+
// Invalidate allReady cache since a new async singleton affects allReady()
2087+
_cachedAllReadyFuture = null;
2088+
20722089
/// Any client awaiting the completion of this Singleton
20732090
/// Has to wait for the completion of the Singleton itself as well
20742091
/// as for the completion of all the Singletons this one depends on
@@ -2305,6 +2322,17 @@ class _GetItImplementation implements GetIt {
23052322
Duration? timeout,
23062323
bool ignorePendingAsyncCreation = false,
23072324
}) {
2325+
// Return cached future if available
2326+
if (_cachedAllReadyFuture != null) {
2327+
if (timeout != null) {
2328+
return _cachedAllReadyFuture!.timeout(
2329+
timeout,
2330+
onTimeout: () => throw _createTimeoutError(),
2331+
);
2332+
}
2333+
return _cachedAllReadyFuture!;
2334+
}
2335+
23082336
final futures = FutureGroup();
23092337
_allRegistrations
23102338
.where(
@@ -2331,14 +2359,16 @@ class _GetItImplementation implements GetIt {
23312359
}
23322360
});
23332361
futures.close();
2362+
2363+
_cachedAllReadyFuture = futures.future;
2364+
23342365
if (timeout != null) {
2335-
return futures.future.timeout(
2366+
return _cachedAllReadyFuture!.timeout(
23362367
timeout,
2337-
onTimeout: () async => throw _createTimeoutError(),
2368+
onTimeout: () => throw _createTimeoutError(),
23382369
);
2339-
} else {
2340-
return futures.future;
23412370
}
2371+
return _cachedAllReadyFuture!;
23422372
}
23432373

23442374
/// Returns if all async Singletons are ready without waiting

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: get_it
22
description: Simple direct Service Locator that allows to decouple the interface from a concrete implementation and to access the concrete implementation from everywhere in your App"
3-
version: 9.1.1
3+
version: 9.2.0
44
maintainer: Thomas Burkhart (@escamoteur)
55
homepage: https://github.com/flutter-it/get_it
66
repository: https://github.com/flutter-it/get_it

test/async_test.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,84 @@ void main() {
946946
expect(instance.initCompleted, isTrue);
947947
});
948948
});
949+
950+
group('allReady caching', () {
951+
test('allReady returns same Future on repeated calls', () async {
952+
final getIt = GetIt.instance;
953+
await getIt.reset();
954+
955+
getIt.registerSingletonAsync<TestClass>(
956+
() => Future.delayed(const Duration(milliseconds: 10))
957+
.then((_) => TestClass(internalCompletion: false)),
958+
);
959+
960+
final future1 = getIt.allReady();
961+
final future2 = getIt.allReady();
962+
963+
expect(identical(future1, future2), isTrue);
964+
965+
await future1;
966+
});
967+
968+
test('allReady returns new Future after registerSingletonAsync', () async {
969+
final getIt = GetIt.instance;
970+
await getIt.reset();
971+
972+
getIt.registerSingletonAsync<TestClass>(
973+
() => Future.delayed(const Duration(milliseconds: 10))
974+
.then((_) => TestClass(internalCompletion: false)),
975+
);
976+
977+
final future1 = getIt.allReady();
978+
await future1;
979+
980+
// Register another async singleton
981+
getIt.registerSingletonAsync<TestClass2>(
982+
() => Future.delayed(const Duration(milliseconds: 10))
983+
.then((_) => TestClass2(internalCompletion: false)),
984+
);
985+
986+
final future2 = getIt.allReady();
987+
988+
// Should be a new future since we registered a new async singleton
989+
expect(identical(future1, future2), isFalse);
990+
991+
await future2;
992+
});
993+
994+
test(
995+
'allReady returns new Future after pushNewScope with async registration',
996+
() async {
997+
final getIt = GetIt.instance;
998+
await getIt.reset();
999+
1000+
getIt.registerSingletonAsync<TestClass>(
1001+
() => Future.delayed(const Duration(milliseconds: 10))
1002+
.then((_) => TestClass(internalCompletion: false)),
1003+
);
1004+
1005+
final future1 = getIt.allReady();
1006+
await future1;
1007+
1008+
// Push a new scope with an async registration
1009+
getIt.pushNewScope(
1010+
init: (getIt) {
1011+
getIt.registerSingletonAsync<TestClass2>(
1012+
() => Future.delayed(const Duration(milliseconds: 10))
1013+
.then((_) => TestClass2(internalCompletion: false)),
1014+
);
1015+
},
1016+
);
1017+
1018+
final future2 = getIt.allReady();
1019+
1020+
// Should be a new future since we registered a new async singleton in the scope
1021+
expect(identical(future1, future2), isFalse);
1022+
1023+
await future2;
1024+
await getIt.popScope();
1025+
});
1026+
});
9491027
}
9501028

9511029
abstract class Service1 {}

0 commit comments

Comments
 (0)