untilExactlyK<T extends Object> method
Waits until a dependency with the exact typeEntity is registered.
The result is cast to T.
Note: Requires enableUntilExactlyK: true during registration.
If typeEntity doesn't match an existing or future registration exactly,
this will not resolve.
The completer captures a registration epoch at creation. If the
dependency is unregistered between the time this caller starts waiting
and the time its continuation runs, the epoch advances and the
continuation re-waits for the next registration rather than returning a
stale value (or unwrap-ping a now-missing dependency).
Implementation
Resolvable<T> untilExactlyK<T extends Object>(
Entity typeEntity, {
Entity groupEntity = const DefaultEntity(),
bool traverse = true,
}) {
final g = groupEntity.preferOverDefault(focusGroup);
if (getK<T>(typeEntity, groupEntity: g, traverse: traverse)
case Some(value: final r)) {
return r;
}
final myEpoch = _epochForK(g, typeEntity);
// Look for an existing completer in THIS container OR any ancestor
// (so concurrent waiters from different containers share one).
ReservedSafeCompleter? completer;
final searchScope = traverse ? _allAncestorsK() : <DI>[this as DI];
for (final di in searchScope) {
final found = (di as SupportsMixinK).completersK[g]?.firstWhereOrNull(
(e) => e.typeEntity == typeEntity,
);
if (found != null) {
completer = found;
break;
}
}
if (completer == null) {
completer = ReservedSafeCompleter(typeEntity);
(completersK[g] ??= []).add(completer);
// Seed the completer into every ancestor so an ancestor's
// `register<...>(..., enableUntilExactlyK: true)` (which only walks
// its OWN `completersK`, not transitively into children) still
// fires this waiter. Mirrors the `until` directional-asymmetry fix.
if (traverse) {
for (final ancestor in _allAncestorsK().skip(1)) {
final mixinAncestor = ancestor as SupportsMixinK;
(mixinAncestor.completersK[g] ??= []).add(completer);
}
}
}
return completer.resolvable().then((_) {
// Remove the completer from THIS container AND every ancestor we
// seeded above. Use identity-comparison so we don't accidentally
// drop a different waiter that happens to share the same
// typeEntity (e.g. a sibling waiter).
final cleanupScope = traverse ? _allAncestorsK() : <DI>[this as DI];
for (final di in cleanupScope) {
(di as SupportsMixinK)
.completersK[g]
?.removeWhere((e) => identical(e, completer));
}
if (_epochForK(g, typeEntity) != myEpoch) {
return untilExactlyK<T>(
typeEntity,
groupEntity: g,
traverse: traverse,
);
}
// Completer resolved → matching register must have happened. If a
// concurrent unregister wiped the slot between completion and now,
// surface an Err Resolvable rather than throwing — callers chained off
// the outer `.flatten()` then see a normal Err on their pipeline.
return switch (getK<T>(typeEntity, groupEntity: g, traverse: traverse)) {
Some(value: final r) => r,
None() => Sync<T>.err(
Err('untilExactlyK<$T>: completer resolved but post-resolution '
'lookup returned None (raced with unregister).'),
),
};
}).flatten();
}