untilExactlyK<T extends Object> method

Resolvable<T> untilExactlyK<T extends Object>(
  1. Entity typeEntity, {
  2. Entity groupEntity = const DefaultEntity(),
  3. bool traverse = true,
})
inherited

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();
}