@zeix/cause-effect
Version:
Cause & Effect - reactive state management primitives library for TypeScript.
64 lines (46 loc) • 2.91 kB
Markdown
# ADR 0008: Doubly-Linked List Edge Structure
## Status
✅ Accepted
## Context
The signal graph is a directed acyclic graph (DAG) where edges connect source nodes to sink nodes. Edges must support:
1. **Fast traversal** from source to sinks (for propagation)
2. **Fast traversal** from sink to sources (for dependency tracking)
3. **O(1) removal** when dependencies change (for graph adaptation)
4. **Minimal memory overhead** (for bundle size and runtime performance)
A naive approach using separate edge objects stored in arrays or Sets would:
- Require additional memory allocation per edge
- Have O(n) removal complexity
- Not meet the "Minimal memory" design goal
## Decision
Store edges as **doubly-linked lists embedded directly in nodes**, with each edge participating in two linked lists simultaneously:
```typescript
type Edge = {
source: SourceNode // the node being depended on
sink: SinkNode // the node that depends on source
nextSource: Edge | null // next edge in the sink's source list
prevSink: Edge | null // previous edge in the source's sink list
nextSink: Edge | null // next edge in the source's sink list
}
```
**Structure:**
- Each **source** maintains a singly-linked list of its sinks (`sinks` → `sinksTail`), threaded through `nextSink`/`prevSink`
- Each **sink** maintains a singly-linked list of its sources (`sources` → `sourcesTail`), threaded through `nextSource`
- The `prevSink` pointer enables O(1) removal from the source's sink list
This design means:
- No separate edge storage data structures
- Edges are allocated only when needed
- All operations (addition, removal, traversal) are O(1)
## Alternatives Considered
- **(a) Separate Edge objects in Sets**: Rejected — O(n) removal, higher memory overhead from object allocation
- **(b) Adjacency lists with arrays**: Rejected — O(n) removal for unlinking
- **(c) WeakMap-based edge tracking**: Rejected — Higher memory overhead, GC complexity
## Consequences
- ✅ **O(1) edge operations**: Addition, removal, and traversal are all constant time
- ✅ **Minimal memory**: Edges stored inline, no separate data structures
- ✅ **Bundle size efficient**: No external dependencies or complex data structures
- ✅ **Cache-friendly**: Sequential memory access patterns during traversal
- ⚠️ **Manual pointer management**: More error-prone to implement correctly
- ⚠️ **No built-in iteration**: Must use explicit traversal loops
## Related
- Requirements: [Bundle Size Constraints](REQUIREMENTS.md#size-and-performance-constraints), [Minimal Surface, Maximum Coverage](REQUIREMENTS.md#minimal-surface-maximum-coverage)
- Architecture: [Edges](ARCHITECTURE.md#edges), [Edge Creation: link(source, sink)](ARCHITECTURE.md#edge-creation-linksource-sink), [Edge Removal: trimSources(node) and unlink(edge)](ARCHITECTURE.md#edge-removal-trimsourcesnode-and-unlinkedge)