register<T extends Object> method

Resolvable<T> register<T extends Object>(
  1. FutureOr<T> value, {
  2. Option<TOnRegisterCallback<T>> onRegister = const None(),
  3. Option<TOnUnregisterCallback<T>> onUnregister = const None(),
  4. Entity groupEntity = const DefaultEntity(),
  5. bool enableUntilExactlyK = false,
})

Registers a dependency with the container.

Implementation

Resolvable<T> register<T extends Object>(
  FutureOr<T> value, {
  Option<TOnRegisterCallback<T>> onRegister = const None(),
  Option<TOnUnregisterCallback<T>> onUnregister = const None(),
  Entity groupEntity = const DefaultEntity(),
  bool enableUntilExactlyK = false,
}) {
  assert(T != Object, 'T must be specified and cannot be Object.');
  assert(
    value is! Future || T != FutureOr,
    'register<$T>: registering a Future where T is FutureOr is ambiguous. '
    'Use Resolvable<T> or unwrap the Future before registering.',
  );
  final g = groupEntity.preferOverDefault(focusGroup);

  // Existence check FIRST. If a slot for [T] already exists in this
  // container's [g] group, reject the registration BEFORE constructing
  // the wrapping Resolvable — building it runs the `onRegister` side
  // effects (init() etc.), and we must NOT run those for a registration
  // we'll reject (audit-pass-3 finding: previously a duplicate-register
  // call fired init() on a service that would never be reachable,
  // leaking the resource).
  switch (getDependency<T>(groupEntity: g, traverse: false)) {
    case Some():
      return Sync<T>.err(Err('Dependency already registered.'));
    case None():
      break;
  }

  final metadata = DependencyMetadata(
    index: Some(_indexIncrementer++),
    groupEntity: g,
    onUnregister: onUnregister.map((cb) => (e) => cb(e.transf())),
  );
  // Wrap the onRegister invocation so that a synchronous throw is
  // captured into the Resolvable instead of escaping out of `register()`.
  // Without this, a sync-throwing onRegister blows the call stack
  // mid-`register` and the caller has no Resolvable-shaped handle to the
  // failure — unacceptable for medical-grade code where every callback
  // site must be uniformly catchable.
  final a = Resolvable(
    () => consec(value, (e) {
      return consec(_safeOnRegister<T>(onRegister, e), (_) => e);
    }),
  );
  final b = registerDependency<T>(
    dependency: Dependency(a, metadata: Some(metadata)),
    // Slot was already free-checked above; skip the redundant probe.
    checkExisting: false,
  );
  switch (b) {
    case Err<Dependency<T>> err:
      return Sync.err(err.transfErr<T>());
    case Ok():
      break;
  }
  if (value is! ReservedSafeCompleter<T>) {
    // Used for until*. Walks ANY pending ReservedSafeCompleter (regardless
    // of its type parameter) and lets each completer's own captured
    // type-check decide whether `value` matches — see `_maybeFinish` for
    // why filtering by `<T>` here is unsafe under dart2js release.
    _maybeFinish<Object>(value: value, g: g);

    // Used for untilT and untilK. Disabled by default to improve performance.
    if (enableUntilExactlyK) {
      (this as SupportsMixinK).maybeFinishK<T>(g: g);
    }
  }
  // We just succeeded at `registerDependency` above, so `get<T>` MUST find
  // the slot we wrote. Pattern-match instead of `.unwrap()` so the
  // theoretical "should never happen" branch is explicit: if a concurrent
  // unregister somehow raced and wiped the slot, return an Err Resolvable
  // instead of throwing — callers awaiting the returned Resolvable then
  // see a normal Err on their chain.
  return switch (get<T>(groupEntity: groupEntity)) {
    Some(value: final r) => r,
    None() => Sync<T>.err(
        Err('register<$T>: post-register lookup returned None '
            '(slot was concurrently removed).'),
      ),
  };
}