ript
export interface SortStepEvent {
type: 'compare' | 'swap' | 'partition' | 'merge' | 'complete';
indices: number[];
metrics: { comparisons: number; swaps: number };
stateSnapshot: number[]; // Optional: for immutable rendering pipelines
}
export type SortGenerator = Generator<SortStepEvent, void, unknown>;
### Step 2: Implement Algorithms as Generators
Algorithms must never touch the DOM, `setTimeout`, or `requestAnimationFrame`. They operate on a mutable array reference and yield structured events.
**Bubble Sort Implementation**
```typescript
export function* bubbleSortEngine(
data: number[],
metrics: { comparisons: number; swaps: number }
): SortGenerator {
const n = data.length;
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - 1 - i; j++) {
metrics.comparisons++;
yield {
type: 'compare',
indices: [j, j + 1],
metrics: { ...metrics },
stateSnapshot: [...data]
};
if (data[j] > data[j + 1]) {
metrics.swaps++;
[data[j], data[j + 1]] = [data[j + 1], data[j]];
yield {
type: 'swap',
indices: [j, j + 1],
metrics: { ...metrics },
stateSnapshot: [...data]
};
}
}
}
yield {
type: 'complete',
indices: [],
metrics: { ...metrics },
stateSnapshot: [...data]
};
}
Merge Sort with Recursive Delegation
Recursive algorithms require yield* to propagate yields from sub-generators up to the driver.
function* mergeStep(
data: number[],
metrics: { comparisons: number; swaps: number },
left: number,
mid: number,
right: number
): SortGenerator {
const leftSlice = data.slice(left, mid + 1);
const rightSlice = data.slice(mid + 1, right + 1);
let i = 0, j = 0, k = left;
while (i < leftSlice.length && j < rightSlice.length) {
metrics.comparisons++;
yield { type: 'compare', indices: [k], metrics: { ...metrics }, stateSnapshot: [...data] };
if (leftSlice[i] <= rightSlice[j]) {
data[k] = leftSlice[i++];
} else {
metrics.swaps++;
data[k] = rightSlice[j++];
}
yield { type: 'merge', indices: [k], metrics: { ...metrics }, stateSnapshot: [...data] };
k++;
}
while (i < leftSlice.length) {
data[k] = leftSlice[i++];
yield { type: 'merge', indices: [k], metrics: { ...metrics }, stateSnapshot: [...data] };
k++;
}
while (j < rightSlice.length) {
data[k] = rightSlice[j++];
yield { type: 'merge', indices: [k], metrics: { ...metrics }, stateSnapshot: [...data] };
k++;
}
}
export function* mergeSortEngine(
data: number[],
metrics: { comparisons: number; swaps: number },
left = 0,
right = data.length - 1
): SortGenerator {
if (left >= right) return;
const mid = (left + right) >> 1;
yield* mergeSortEngine(data, metrics, left, mid);
yield* mergeSortEngine(data, metrics, mid + 1, right);
yield* mergeStep(data, metrics, left, mid, right);
}
Step 3: Build the Animation Driver
The driver consumes the generator, schedules execution, and handles runtime configuration. It reads timing parameters on each tick, enabling immediate speed adjustments.
export class SortAnimationDriver {
private generator: SortGenerator | null = null;
private timerId: ReturnType<typeof setTimeout> | null = null;
private isRunning = false;
private getDelay: () => number;
constructor(delayProvider: () => number) {
this.getDelay = delayProvider;
}
start(gen: SortGenerator, onStep: (event: SortStepEvent) => void, onComplete: () => void): void {
if (this.isRunning) this.stop();
this.generator = gen;
this.isRunning = true;
this.tick(onStep, onComplete);
}
private tick(onStep: (event: SortStepEvent) => void, onComplete: () => void): void {
if (!this.generator || !this.isRunning) return;
const { value, done } = this.generator.next();
if (!done && value) {
onStep(value);
this.timerId = setTimeout(() => this.tick(onStep, onComplete), this.getDelay());
} else {
this.isRunning = false;
onComplete();
}
}
stop(): void {
if (this.timerId) clearTimeout(this.timerId);
this.isRunning = false;
this.generator = null;
}
}
Architecture Decisions & Rationale
- Mutable Array + Snapshot Yields: Algorithms mutate the input array directly for performance, but yield a shallow copy (
stateSnapshot) for rendering. This avoids deep cloning overhead while providing immutable data to UI frameworks that rely on change detection.
- Dynamic Delay Resolution:
this.getDelay() is invoked inside setTimeout callback, not cached. This allows the UI slider to change animation speed instantly without restarting the generator.
- Explicit Lifecycle Management:
stop() clears the timer and nullifies the generator reference. This prevents memory leaks and ensures clean unmounting in component-based architectures.
yield* for Recursion: Merge and Quick sort delegate to sub-generators. yield* flattens the call stack, ensuring the driver receives every step sequentially without manual iteration management.
Pitfall Guide
1. Forgetting yield* in Recursive Algorithms
Explanation: Using a regular function call inside a generator (mergeSortEngine(...)) executes the sub-routine synchronously without yielding control. The driver receives no intermediate steps, and the animation appears to freeze or skip entirely.
Fix: Always use yield* when calling sub-generators. It propagates each yield up the call chain, maintaining step-by-step execution.
2. Over-Yielding Trivial Operations
Explanation: Yielding on every loop iteration or index increment bloats the step count, causing performance degradation and visual noise. The animation becomes sluggish, and metrics become meaningless.
Fix: Only yield at semantically significant events: comparisons, swaps, partitions, or merge boundaries. Skip loop counters and index increments.
3. State Desynchronization in Shift-Based Sorts
Explanation: Insertion sort and similar algorithms shift elements rightward before placing the key. If you mark individual indices as "sorted" during the shift phase, the UI displays incorrect final states because those positions haven't received their definitive values yet.
Fix: Apply sorted markers to the entire processed prefix (0..i) only after the key is placed. Re-render the prefix state explicitly to maintain visual accuracy.
4. Blocking the Main Thread with Tight Loops
Explanation: Generators are synchronous iterators. If the driver uses a while loop to drain the generator without yielding to the event loop, the UI freezes completely.
Fix: Always schedule the next step via setTimeout or requestAnimationFrame. This yields control back to the browser, keeping the UI responsive and allowing user interaction during execution.
5. Assuming Generators Handle Async Operations
Explanation: Developers sometimes try to await inside a generator or mix async/await with yield. This breaks the synchronous iteration contract and causes undefined returns or unhandled promise rejections.
Fix: Keep generators strictly synchronous. Timing, delays, and async side effects belong exclusively in the driver layer.
6. Memory Leaks from Orphaned Generators
Explanation: When a component unmounts or the user navigates away, the generator reference may persist in memory if not explicitly cleared. This causes stale timers to fire or memory bloat in long-running applications.
Fix: Implement a destroy() or stop() method that calls clearTimeout, sets the generator reference to null, and removes event listeners. Integrate this with framework lifecycle hooks (useEffect cleanup, componentWillUnmount).
7. Inconsistent Metric Tracking
Explanation: Comparisons and swaps are often counted inside conditional branches or after yields, leading to inaccurate statistics. Some implementations count a comparison twice or miss swaps during partitioning.
Fix: Increment metrics immediately before the yield that represents the operation. Pass metrics by reference or return them in the step event to ensure atomic updates.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Educational visualizer / prototyping | Generator + DOM renderer | Fastest implementation, zero dependencies, easy to debug | Low (development time) |
| High-frequency trading simulation / 60fps UI | Generator + Canvas/WebGL driver | DOM manipulation becomes bottleneck at >1000 elements | Medium (requires rendering expertise) |
| Background algorithm benchmarking | Generator + Web Worker | Keeps main thread free, enables parallel testing | High (serialization overhead, worker setup) |
| Real-time collaborative debugging | Generator + CRDT/State Sync | Deterministic steps enable time-travel debugging and multi-user sync | High (infrastructure complexity) |
Configuration Template
// config.ts
export interface VisualizationConfig {
algorithm: 'bubble' | 'insertion' | 'merge' | 'quick';
arraySize: number;
baseDelay: number; // milliseconds
speedMultiplier: number; // 0.1 to 5.0
renderMode: 'dom' | 'canvas';
}
// driver-factory.ts
import { SortAnimationDriver } from './driver';
import { bubbleSortEngine, mergeSortEngine } from './algorithms';
export function createSortDriver(config: VisualizationConfig) {
const delayProvider = () => config.baseDelay / config.speedMultiplier;
const driver = new SortAnimationDriver(delayProvider);
const algorithmMap = {
bubble: bubbleSortEngine,
merge: mergeSortEngine,
// quick: quickSortEngine,
// insertion: insertionSortEngine
};
return {
driver,
run: (data: number[]) => {
const metrics = { comparisons: 0, swaps: 0 };
const gen = algorithmMap[config.algorithm](data, metrics);
return { gen, metrics };
}
};
}
Quick Start Guide
- Initialize the driver: Instantiate
SortAnimationDriver with a delay provider function that reads your UI speed control.
- Generate algorithm instance: Call your chosen generator function with a mutable array and a metrics object.
- Bind step handler: Pass a callback to
driver.start() that receives SortStepEvent objects and updates your UI state.
- Handle completion: Provide an
onComplete callback to reset controls, display final metrics, or trigger next steps.
- Test headlessly: Create a test runner that calls
generator.next() in a loop until done === true, then assert array order and metric counts without any browser environment.