effect library
Reactive side-effect pipelines and temporal rate limiters.
package:blooming/effect.dart provides an expressive suite of non-propagating reactive
terminals: declarative Effect, time-domain rate limiters (Effect.throttle, Effect.debounce),
finite execution bounds with reactive guards (guard, take), and composite grouping (Effect.group).
Architectural Overview
In the Bloom DAG architecture, reactive state nodes (Bloom) adhere strictly to the Push-Dirty / Lazy-Pull paradigm. Side-effects (such as analytics, logging, audio/haptic feedback, or external I/O sync) MUST NOT mutate intermediate DAG nodes to produce business state.
Instead, side-effects are modeled as Terminal Sinks:
- Terminal Non-Propagation: Effects do not propagate dirty pulses downstream; they consume signals at graph terminal boundaries without creating cascading DAG cycles.
- Declarative Blueprints: Effect instances can be defined
declaratively and attached to tracking scopes via
via.tether(). - Zero-Glitch Invariant: Effects are flushed only after state transactions settle, guaranteeing that multi-node updates are read consistently without transient tearing.
System Architectural Flow
┌────────────────────────────────────────────────────────┐
│ Bloom Reactive Topology │
├───────────────────────────┬────────────────────────────┤
│ State Pipeline Domain │ Side-Effect Domain │
│ (package:blooming/blooming) │ (package:blooming/effect) │
├───────────────────────────┼────────────────────────────┤
│ • Push-Dirty / Lazy-Pull │ • Terminal Sinks │
│ • Bloom / Computed / FSM │ • Non-propagating actions │
│ • State Synchronous SSOT │ • Debounce / Throttle │
│ • 1 node = 1 allocation │ • Scope-bound via via.tether│
└───────────────────────────┴────────────────────────────┘
Architectural Benefits
- Cycle-Free Terminal Boundaries: Effects never act as reactive sources for upstream nodes, guaranteeing that side-effect executions cannot create infinite topological update cycles.
- Transaction-Safe Flush: Effects fire only after state transactions settle, eliminating transient tearing or intermediate state reads.
- Declarative Lifecycle Management: Tethering via
via.tether()automatically aligns effect lifetimes with active subscription scopes, preventing background memory and listener leaks.
Warning (Anti-Pattern Prevention):
- Do NOT mutate upstream BloomController nodes inside an Effect without an explicit guard or exit condition; uncontrolled mutations induce reentrant event loops.
- Do NOT instantiate new effects repeatedly inside Flutter build methods or dynamic loop passes without persisting them in a field or binding them to a lifecycle scope.
BAD:
// [Anti-Pattern] Uncontrolled state mutation inside side-effect creates reentrancy loop
final counter = blooming(0);
final e = counter.bloom.effect(() {
counter.set(untracked(counter.bloom) + 1); // Mutates source node without guard!
});
GOOD:
// [Recommended] One-way terminal sink for external interactions
final counter = blooming(0);
final e = counter.bloom.effect(() {
analyticsService.logCount(untracked(counter.bloom)); // Pure side-effect sink
});
Examples
1. Fluent Single-Node Debounce:
final searchQuery = blooming('');
// Debounce searches to execute 300ms after user stops typing
final searchRunner = searchQuery.bloom.effect
.debounce(
const Duration(milliseconds: 300),
() => fetchSearchResults(untracked(searchQuery.bloom)),
)
.start();
2. Scope-Bound Lifecycle with via.tether():
final user = blooming(User(id: 'u1'));
final settings = blooming(Settings());
// Inside a computation scope or terminal:
final term = RawViaTerminal(
(via) {
via.tether((user.bloom, settings.bloom).effect(() {
analytics.track('User updated: ${untracked(user.bloom).id}');
}));
},
onInvalidated: () {},
);
Classes
- Effect
- A declarative lifecycle-bound side-effect node that executes an untracked action upon upstream mutations.
- EffectDebounceFunctor
- Sub-factory providing declarative debounce rate-limiting Effect creation.
- EffectRunner
- An active standalone execution terminal that imperatively controls the lifecycle of an Effect.
- EffectScope
- An active execution context scope for a scoped Effect run.
- EffectScopedFunctor
- Sub-factory providing declarative Effect creation with scoped lifecycle context (EffectScope).
- EffectThrottleFunctor
- Sub-factory providing declarative throttle rate-limiting Effect creation.
- Tether
- Top-level declarative lifecycle-bound entity in the Bloom reactive DAG.
Extension Types
- BloomEffectScope
- Canonical scoped namespace for declarative reactive Effect creation targeting multiple Bloom nodes.
- UnaryBloomEffectScope
- Specialized scoped namespace for declarative reactive Effect creation targeting a single Bloom.
Extensions
- BloomEffectExtension on Bloom
- Extension granting a single Bloom node direct access to reactive effect namespaces.
-
BloomIterableEffectExtension
on Iterable<
Bloom> - Extension granting collections of Bloom instances access to reactive effect namespaces.
- BloomRecord2EffectExtension on (Bloom, Bloom)
- Extension granting a 2-tuple of Bloom instances access to reactive effect namespaces.
- BloomRecord3EffectExtension on (Bloom, Bloom, Bloom)
- Extension granting a 3-tuple of Bloom instances access to reactive effect namespaces.
- BloomRecord4EffectExtension on (Bloom, Bloom, Bloom, Bloom)
- Extension granting a 4-tuple of Bloom instances access to reactive effect namespaces.
- BloomRecord5EffectExtension on (Bloom, Bloom, Bloom, Bloom, Bloom)
- Extension granting a 5-tuple of Bloom instances access to reactive effect namespaces.
-
EffectIterableExtension
on Iterable<
Effect> - Extension granting collections of Effect instances direct grouping into a composite Effect.
- EffectRecord2Extension on (Effect, Effect)
- Extension granting a 2-tuple of Effect instances direct grouping into a composite Effect.
- EffectRecord3Extension on (Effect, Effect, Effect)
- Extension granting a 3-tuple of Effect instances direct grouping into a composite Effect.
- EffectRecord4Extension on (Effect, Effect, Effect, Effect)
- Extension granting a 4-tuple of Effect instances direct grouping into a composite Effect.
- EffectRecord5Extension on (Effect, Effect, Effect, Effect, Effect)
- Extension granting a 5-tuple of Effect instances direct grouping into a composite Effect.
- EffectStartExtension on Effect
- Extension granting declarative Effect instances direct invocation of an EffectRunner.