An active standalone execution terminal that imperatively controls the lifecycle of an Effect.
While declarative Effect nodes are typically tethered to reactive tracking scopes via BloomVia.tether, an EffectRunner enables executing effects imperatively outside reactive scopes (e.g. in domain services, ViewModels, or background workers).
System Architectural Flow
┌────────────────────────────────────────────────────────┐
│ Declarative Effect Node │
└───────────────────────────┬────────────────────────────┘
│ (Active Subscription Edge)
▼
┌────────────────────────────────────────────────────────┐
│ EffectRunner │
├────────────────────────────────────────────────────────┤
│ • start() ──> Attaches edge, resumes runs │
│ • stop() ──> Relinquishes edge (Dormancy)│
│ • isActive ──> Dual-state health check │
└────────────────────────────────────────────────────────┘
Key Invariants & Architectural Benefits
- Zero-Leak Inactive Dormancy: When stopped via stop, immediately relinquishes its subscription edge, guaranteeing dormant upstream subgraphs when unobserved.
- Imperative Standalone Lifecycle: Controls reactive side-effect execution in services, view models, or CLI tasks outside widget lifecycles.
- Subscriber-Driven Dormancy Invariant: If the same Effect instance is shared between
a tracking scope (
via.tether) and an EffectRunner, stopping the runner leaves the effect active as long as the tethering scope remains active. The underlying effect enters true dormancy only when all observing terminals and tethers have detached. - Dual-State Health Invariant: isActive reflects both the runner's lifecycle state
and the underlying effect terminal's status. If the underlying effect deactivates due to
exhausting its
taketicket quota (Early Teardown), isActive transitions tofalseautomatically.
Warning (Anti-Pattern Prevention):
- Do NOT call
.start()repeatedly inside reactive derivations or request handlers without stopping; unmanaged runners stay attached to upstream effects and cause memory leaks.
BAD:
// [Anti-Pattern] Spawning unmanaged effect runners inside a computed derivation
final activeQuery = computed((via) {
searchEffect.start(); // Leaks a new runner on every evaluation!
return via(query.bloom);
});
GOOD:
// [Recommended] Manage runner in a long-lived service lifecycle or tether via via.tether()
class SearchService {
late final EffectRunner _runner;
void init() {
_runner = searchEffect.start();
}
void dispose() => _runner.stop();
}
Examples
final query = blooming('');
final searchEffect = Effect.debounce(
[query.bloom],
const Duration(milliseconds: 300),
() => apiService.search(untracked(query.bloom)),
);
// Start standalone execution:
final runner = searchEffect.start();
// Pause observation:
runner.stop();
// Resume observation:
runner.start();
- Implemented types
Constructors
- EffectRunner(Effect effect)
-
Creates an inactive, dormant EffectRunner managing execution of
effect.factory
Properties
- effect → Effect
-
The underlying Effect executed by this runner.
no setter
- hashCode → int
-
The hash code for this object.
no setterinherited
- isActive → bool
-
Whether both this runner and its underlying effect terminal are currently active.
no setteroverride
- runtimeType → Type
-
A representation of the runtime type of the object.
no setterinherited
Methods
-
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
start(
) → void - Starts or resumes observation and execution of the underlying effect.
-
stop(
) → void - Stops or pauses observation and execution, relinquishing this runner's subscription edge.
-
toString(
) → String -
A string representation of this object.
inherited
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited