@cook-step/dirty-tracker
v0.1.0
Published
High-performance dirty tracking system with dependency propagation for reactive systems
Maintainers
Readme
@cook-step/dirty-tracker
High-performance dirty tracking system with dependency propagation for reactive systems, DAG execution engines, and UI frameworks.
Features
- 🚀 Efficient propagation - Smart dependency tracking with cycle detection
- 🎯 Multiple strategies - Immediate, lazy, smart, and batch propagation
- 🔄 Batch updates - Coalescing and transaction support
- 📊 Smart tracking - Only propagate when values actually change
- 🧮 Value comparison - Hash-based and custom comparators
- 📈 Statistics - Comprehensive tracking metrics
- ⚡ Performance - Optimized for large dependency graphs
- ✅ Well tested - Comprehensive test coverage
Installation
npm install @cook-step/dirty-tracker
# or
pnpm add @cook-step/dirty-tracker
# or
yarn add @cook-step/dirty-trackerQuick Start
import { DirtyTracker, DirtyReason } from "@cook-step/dirty-tracker";
// Create tracker
const tracker = new DirtyTracker();
// Set up dependencies
tracker.setDependencies("node2", ["node1"]);
tracker.setDependencies("node3", ["node2"]);
// Mark node as dirty - propagates to dependents
tracker.markDirty("node1", DirtyReason.PARAMETER_CHANGE);
console.log(tracker.isDirty("node1")); // true
console.log(tracker.isDirty("node2")); // true (propagated)
console.log(tracker.isDirty("node3")); // true (propagated)
// Clear dirty state
tracker.clearDirty("node1");Core Concepts
Dependency Graph
The tracker maintains a directed graph of dependencies between entities:
// node3 depends on node1 and node2
tracker.setDependencies("node3", ["node1", "node2"]);
// When node1 changes, node3 is marked dirty
tracker.markDirty("node1");
console.log(tracker.isDirty("node3")); // truePropagation Strategies
// Immediate propagation (default)
const immediate = new DirtyTracker({
strategy: PropagationStrategy.IMMEDIATE,
});
// Lazy propagation - manual trigger
const lazy = new DirtyTracker({
strategy: PropagationStrategy.LAZY,
});
lazy.markDirty("node1");
lazy.processPendingPropagations(); // Trigger propagationDirty Reasons
Track why entities became dirty:
enum DirtyReason {
INITIAL = "initial",
PARAMETER_CHANGE = "parameter_change",
INPUT_CHANGE = "input_change",
DEPENDENCY_CHANGE = "dependency_change",
FORCED_UPDATE = "forced_update",
EXTERNAL_CHANGE = "external_change",
}
tracker.markDirty("node1", DirtyReason.PARAMETER_CHANGE);Advanced Usage
Batch Updates
Process multiple updates efficiently:
import { BatchDirtyTracker } from "@cook-step/dirty-tracker";
const tracker = new BatchDirtyTracker();
// Batch mode
tracker.beginBatch();
tracker.markDirty("node1");
tracker.markDirty("node2");
tracker.markDirty("node3");
tracker.endBatch(); // All propagations happen here
// Mark multiple at once
tracker.markMultipleDirty([
{ id: "node1", reason: DirtyReason.PARAMETER_CHANGE },
{ id: "node2", reason: DirtyReason.INPUT_CHANGE },
]);Coalescing
Debounce rapid updates:
const tracker = new BatchDirtyTracker({
coalescingDelay: 16, // One frame at 60fps
});
// Rapid updates are coalesced
tracker.markDirtyCoalesced("node1");
tracker.markDirtyCoalesced("node1"); // Deduped
tracker.markDirtyCoalesced("node2");
// Processed after delay
setTimeout(() => {
console.log(tracker.isDirty("node1")); // true
}, 20);Transactions
Atomic updates with rollback:
try {
const result = await tracker.transaction(async (t) => {
t.markDirty("node1");
t.markDirty("node2");
if (someCondition) {
throw new Error("Rollback");
}
return "success";
});
} catch (error) {
// All changes rolled back
}Smart Tracking
Only propagate when values actually change:
import { SmartDirtyTracker } from "@cook-step/dirty-tracker";
const tracker = new SmartDirtyTracker();
// Track value changes
tracker.setValue("node1", 10);
console.log(tracker.isDirty("node1")); // true
tracker.clearDirty("node1");
// Same value - no dirty mark
tracker.setValue("node1", 10);
console.log(tracker.isDirty("node1")); // false
// Different value - marks dirty
tracker.setValue("node1", 20);
console.log(tracker.isDirty("node1")); // trueCustom Comparators
Define how values are compared:
const tracker = new SmartDirtyTracker({
valueComparator: {
equals: (a, b) => {
// Custom comparison logic
return a?.id === b?.id;
},
},
hashFunction: (value) => {
// Custom hash for efficient comparison
return JSON.stringify(value?.id || null);
},
});
// Only marks dirty if id changes
tracker.setValue("node1", { id: 1, name: "A" });
tracker.setValue("node1", { id: 1, name: "B" }); // Not dirty
tracker.setValue("node1", { id: 2, name: "B" }); // DirtySmart Propagation
Stop propagation when values don't change:
tracker.setDependencies("node2", ["node1"]);
// Propagates only if value actually changed
tracker.smartPropagate("node1", newValue);
// If newValue equals old value, propagation stopsStatistics
Monitor tracker performance:
const stats = tracker.getStats();
console.log({
totalEntities: stats.totalEntities,
dirtyEntities: stats.dirtyEntities,
dirtyPercentage: stats.dirtyPercentage,
propagationCount: stats.propagationCount,
maxDepth: stats.maxPropagationDepth,
});
// Reset statistics
tracker.resetStats();Integration Examples
With DAG Execution Engine
class ExecutionEngine {
private tracker = new SmartDirtyTracker();
executeNode(nodeId: string) {
if (!this.tracker.isDirty(nodeId)) {
return this.cache.get(nodeId);
}
const result = this.compute(nodeId);
this.tracker.clearDirty(nodeId);
this.tracker.smartPropagate(nodeId, result);
return result;
}
onParameterChange(nodeId: string, params: any) {
if (this.tracker.setValue(nodeId, params)) {
// Value changed, mark dependents
this.executeDirtyNodes();
}
}
}With React
function useTracker() {
const [tracker] = useState(() => new SmartDirtyTracker());
const [, forceUpdate] = useReducer((x) => x + 1, 0);
useEffect(() => {
const handler = () => forceUpdate();
tracker.options.onDirtyChange = handler;
return () => {
tracker.options.onDirtyChange = undefined;
};
}, [tracker]);
return tracker;
}With UI Forms
class FormValidator {
private tracker = new BatchDirtyTracker();
constructor() {
// Set up field dependencies
this.tracker.setDependencies("total", ["price", "quantity"]);
this.tracker.setDependencies("submit", ["email", "password"]);
}
onFieldChange(fieldName: string, value: any) {
this.tracker.markDirtyCoalesced(fieldName);
// Validate dirty fields after coalescing
setTimeout(() => this.validateDirtyFields(), 100);
}
validateDirtyFields() {
const dirty = this.tracker.getStats().dirtyEntities;
// Validate only changed fields
}
}API Reference
DirtyTracker
class DirtyTracker<T extends EntityId = EntityId> {
constructor(options?: DirtyTrackerOptions);
// Basic operations
markDirty(id: T, reason?: DirtyReason, source?: T): void;
isDirty(id: T): boolean;
clearDirty(id: T): void;
clearAll(): void;
// Dependencies
setDependencies(id: T, dependencies: T[]): void;
getDependencies(id: T): T[];
getDependents(id: T): T[];
// Entity management
remove(id: T): void;
getDependencyInfo(id: T): DependencyInfo;
// Propagation
processPendingPropagations(): void;
// Statistics
getStats(): DirtyStats;
resetStats(): void;
}Types
interface DirtyTrackerOptions {
strategy?: PropagationStrategy;
maxPropagationDepth?: number;
enableDebug?: boolean;
onDirtyChange?: (id: EntityId, state: DirtyState) => void;
coalescingDelay?: number;
}
interface DirtyState {
isDirty: boolean;
reason: DirtyReason;
timestamp: number;
propagationDepth: number;
source?: EntityId;
}
interface DirtyStats {
totalEntities: number;
dirtyEntities: number;
cleanEntities: number;
dirtyPercentage: number;
propagationCount: number;
lastDirtyTime: Date | null;
maxPropagationDepth: number;
}Performance
| Operation | Time | Complexity | | ------------------- | ------------- | ---------- | | Mark dirty | ~0.001ms | O(1) | | Check dirty | ~0.0001ms | O(1) | | Propagation | ~0.01ms/level | O(d) | | Set dependencies | ~0.01ms | O(n) | | Batch update (1000) | ~1ms | O(n) | | Smart comparison | ~0.02ms | O(k) |
Testing
# Run tests
pnpm test
# Run tests with coverage
pnpm test:coverage
# Watch mode
pnpm test:watchContributing
Contributions are welcome! Please ensure:
- All tests pass (
pnpm test) - Type checking passes (
pnpm typecheck) - Follow existing code style
License
MIT
