start method
Resolves all features in dependency order and starts the container.
Idempotent and queue-friendly with stop:
- Calling start while a start is already in flight returns the same future (concurrent calls coalesce).
- Calling start when the container is already in ContainerStatus.working (and no stop is pending) is a no-op.
- Calling start while a stop is in flight queues the start — it begins after the stop completes, on the next cycle's fresh stores.
Throws ContainerUsageError if invoked from within a feature lifecycle callback (would self-deadlock).
Implementation
Future<void> start() {
// Re-entrance guard runs first — before coalesce — because the
// in-flight start IS the caller (a feature callback re-entering
// start()). Coalescing onto the in-flight future would have the
// callback await its own parent and deadlock.
if (Zone.current[userCallbackZoneKey] == true) {
return Future.error(
ContainerUsageError(
'AppContainer.start() cannot be called from within a feature '
'lifecycle callback. Schedule the start outside of it '
'(e.g. unawaited(Future.microtask(container.start))).',
),
);
}
// Coalesce: an in-flight (or queued-after-stop) start returns the
// same future. Two consecutive `container.start()` calls share one
// result — the second doesn't kick off a second cycle.
final inFlight = _startFuture;
if (inFlight != null) return inFlight;
// No-op: container is already up and no stop is tearing it down.
// Returns an already-completed future so callers can `await` without
// ceremony.
if (_status == ContainerStatus.working && _stopping == null) {
return Future.value();
}
assert(() {
// Re-attach the debug finalizer for this cycle. `detach` first
// is defensive — if a previous cycle didn't detach (e.g. because
// it was never stopped), we'd otherwise stack attach calls.
_debugFinalizer.detach(this);
_debugFinalizer.attach(
this,
'AppContainer#${identityHashCode(this)}',
detach: this,
);
return true;
}());
return _startFuture = _runStart();
}