@zeix/cause-effect
Version:
Cause & Effect - reactive state management primitives library for TypeScript.
70 lines (52 loc) • 3.1 kB
Markdown
# ADR 0014: Two-Path Access Pattern for Composite Signals
## Status
✅ Accepted
## Context
Composite signal types (Store, List, Collection) have a dual role:
1. **They are signals** — can be read by consumers
2. **They manage child signals** — their value is composed from child signal values
When a consumer reads a composite signal, we need to:
- Return the current value (composed from child signals)
- Establish dependency edges from child signals to the composite
- Track which child signals exist (for structural reactivity)
However, **establishing edges on every read is expensive** — it would rebuild the entire dependency structure repeatedly, even when nothing has changed.
## Decision
Implement a **two-path access pattern** with a **fast path** and a **tracked path**:
### Fast Path (Subsequent Reads)
```typescript
if (node.sources && !(node.flags & FLAG_RELINK)) {
return untrack(buildValue); // Rebuild without re-linking
}
```
When edges already exist and no structural changes have occurred (`FLAG_RELINK` not set), use `untrack(buildValue)` to rebuild the value without creating dependency edges.
### Tracked Path (First Read or Structural Changes)
```typescript
// FLAG_RELINK is set, or first subscriber
refresh(node); // via recomputeMemo()
```
When `FLAG_RELINK` is set (indicating structural changes) or this is the first subscriber (no `node.sources` yet), force a tracked `refresh()` that:
1. Sets `activeSink = node`
2. Calls `buildValue()` which reads all child signals
3. Each `.get()` call creates edges via `link(child, node)`
4. Sets `sourcesTail` to the last accessed child
5. Clears `FLAG_RELINK`
### Structural Change Handling
On structural mutations (add/remove in List, property changes in Store):
```typescript
node.flags |= FLAG_DIRTY | FLAG_RELINK;
```
This ensures the next `get()` takes the tracked path to re-establish edges.
## Alternatives Considered
- **(a) Always track on read**: Rejected — O(n) overhead on every read for large composites
- **(b) Only track on first read**: Rejected — doesn't handle structural changes
- **(c) Manual edge management**: Rejected — error-prone, more complex API
## Consequences
- ✅ **Performance**: Fast path is O(1) for value rebuilding without edge overhead
- ✅ **Correctness**: Tracked path ensures edges are always up-to-date after structural changes
- ✅ **Unified pattern**: Store, List, Collection, deriveCollection all use the same pattern
- ✅ **Lazy edge establishment**: Edges only created when needed (on first read)
- ⚠️ **Slight complexity**: Requires careful flag management
## Related
- Requirements: [Performance Constraints](REQUIREMENTS.md#performance), [Unified Graph](REQUIREMENTS.md#unified-graph)
- Architecture: [Store](ARCHITECTURE.md#store-srcnodesstorets), [List](ARCHITECTURE.md#list-srcnodeslistts), [Collection](ARCHITECTURE.md#collection-srcnodescollectionts)
- Dependencies: [FLAG_RELINK Mechanism](0010-flag-relink-mechanism-for-structural-reactivity.md), [activeSink Protocol](0009-activeSink-protocol-for-automatic-dependency-tracking.md)