Skip to content

Dependency Graph

The Dependency Graph tracks relationships between notes, enabling O(1) lookup for both dependencies and dependents.

Overview

When a note's expression references another note, a dependency is created:

javascript
// Note 5 depends on Note 1
note5.frequency = module.getNoteById(1).getVariable('frequency').mul(...)

The dependency graph maintains:

  • Forward index: What does a note depend on?
  • Inverse index: What depends on a note?

Location: src/dependency-graph.js

Data Structures

Forward Dependencies

javascript
// dependencies: Map<noteId, Set<noteId>>
// "Note X depends on these notes"

dependencies = {
  5: Set([1, 2]),    // Note 5 depends on Notes 1 and 2
  3: Set([1]),       // Note 3 depends on Note 1
  4: Set([3]),       // Note 4 depends on Note 3
}

Inverse Dependencies (Dependents)

javascript
// dependents: Map<noteId, Set<noteId>>
// "These notes depend on Note X"

dependents = {
  1: Set([3, 5]),    // Notes 3 and 5 depend on Note 1
  2: Set([5]),       // Note 5 depends on Note 2
  3: Set([4]),       // Note 4 depends on Note 3
}

Property-Specific Dependencies

The graph also tracks dependencies by property type:

javascript
// Which notes' startTime depends on Note X's startTime
startTimeOnStartTimeDeps = {
  1: Set([2, 3]),
}

// Which notes' startTime depends on Note X's duration
startTimeOnDurationDeps = {
  1: Set([2]),
}

// Which notes' frequency depends on Note X's frequency
frequencyOnFrequencyDeps = {
  1: Set([2, 3, 4]),
}

Class Structure

javascript
class DependencyGraph {
  constructor() {
    // General dependencies
    this.dependencies = new Map();
    this.dependents = new Map();

    // Property-specific
    this.startTimeOnStartTime = new Map();
    this.startTimeOnDuration = new Map();
    this.frequencyOnFrequency = new Map();
    this.durationOnDuration = new Map();

    // BaseNote dependents
    this.baseNoteDependents = {
      startTime: new Set(),
      duration: new Set(),
      frequency: new Set(),
      tempo: new Set(),
    };
  }
}

Building the Graph

From Expression Bytecode

javascript
buildFromModule(module) {
  this.clear();

  for (const note of module.notes) {
    this.analyzeNote(note);
  }
}

analyzeNote(note) {
  for (const varName of ['startTime', 'duration', 'frequency']) {
    const expr = note.getExpression(varName);
    const refs = this.extractReferences(expr);

    for (const ref of refs) {
      this.addDependency(note.id, ref.noteId, varName, ref.varName);
    }
  }
}

Extracting References from Bytecode

javascript
extractReferences(expr) {
  const refs = [];
  let pc = 0;

  while (pc < expr.length) {
    const op = expr[pc++];

    if (op === OP.LOAD_REF) {
      const noteId = this.readUint16(expr, pc); pc += 2;
      const varIdx = expr[pc++];
      refs.push({ noteId, varName: VAR_NAMES[varIdx] });
    } else if (op === OP.LOAD_BASE) {
      const varIdx = expr[pc++];
      refs.push({ noteId: 0, varName: VAR_NAMES[varIdx] });
    } else {
      pc += OPCODE_SIZES[op] - 1;
    }
  }

  return refs;
}

Adding Dependencies

javascript
addDependency(fromNoteId, toNoteId, fromVar, toVar) {
  // Forward
  if (!this.dependencies.has(fromNoteId)) {
    this.dependencies.set(fromNoteId, new Set());
  }
  this.dependencies.get(fromNoteId).add(toNoteId);

  // Inverse
  if (!this.dependents.has(toNoteId)) {
    this.dependents.set(toNoteId, new Set());
  }
  this.dependents.get(toNoteId).add(fromNoteId);

  // Property-specific (e.g., startTime on duration)
  const key = `${fromVar}On${capitalize(toVar)}`;
  if (this[key]) {
    if (!this[key].has(toNoteId)) {
      this[key].set(toNoteId, new Set());
    }
    this[key].get(toNoteId).add(fromNoteId);
  }
}

Query Operations

Get Dependencies

javascript
// What does Note 5 depend on?
getDependencies(noteId) {
  return this.dependencies.get(noteId) || new Set();
}
// Returns: Set([1, 2])

Get Dependents

javascript
// What depends on Note 1?
getDependents(noteId) {
  return this.dependents.get(noteId) || new Set();
}
// Returns: Set([3, 5])

Get Cascade (Transitive Dependents)

javascript
// All notes affected if Note 1 changes
getCascade(noteId) {
  const affected = new Set();
  const queue = [noteId];

  while (queue.length > 0) {
    const current = queue.shift();
    const deps = this.getDependents(current);

    for (const dep of deps) {
      if (!affected.has(dep)) {
        affected.add(dep);
        queue.push(dep);
      }
    }
  }

  return affected;
}

Property-Specific Query

javascript
// Which notes' startTime depends on Note 2's duration?
// (Used for drag previews)
getStartTimeDependentsOnDuration(noteId) {
  return this.startTimeOnDuration.get(noteId) || new Set();
}

Use Cases

Smart Drag Preview

When dragging Note 1:

javascript
// Only move notes whose startTime actually depends on Note 1
const affectedNotes = graph.getStartTimeDependentsOnDuration(1);

for (const noteId of affectedNotes) {
  // Show preview position for this note
  previewNote(noteId, newPosition);
}

Notes with only frequency dependencies don't move in the preview.

Incremental Evaluation

When Note 3 changes:

javascript
// Mark Note 3 and all dependents as dirty
const dirtySet = graph.getCascade(3);
dirtySet.add(3);

// Evaluate only dirty notes
for (const noteId of topologicalSort(dirtySet)) {
  evaluate(noteId);
}

Circular Dependency Detection

javascript
wouldCreateCycle(fromNoteId, toNoteId) {
  // Check if toNoteId eventually depends on fromNoteId
  const reachable = this.getCascade(toNoteId);
  return reachable.has(fromNoteId);
}

// Before adding a new reference:
if (graph.wouldCreateCycle(noteA, noteB)) {
  throw new Error('Circular dependency detected');
}

Dependency Visualization

javascript
// For UI: get all lines to draw
getVisualizationData(selectedNoteId) {
  return {
    // Blue lines: what this note depends on
    dependencies: this.getDependencies(selectedNoteId),
    // Red lines: what depends on this note
    dependents: this.getDependents(selectedNoteId),
  };
}

Topological Sort

For correct evaluation order:

javascript
topologicalSort(notes) {
  const sorted = [];
  const visited = new Set();
  const visiting = new Set();

  const visit = (noteId) => {
    if (visited.has(noteId)) return;
    if (visiting.has(noteId)) {
      throw new Error('Circular dependency');
    }

    visiting.add(noteId);

    // Visit dependencies first
    for (const dep of this.getDependencies(noteId)) {
      visit(dep);
    }

    visiting.delete(noteId);
    visited.add(noteId);
    sorted.push(noteId);
  };

  for (const noteId of notes) {
    visit(noteId);
  }

  return sorted;
}

Performance

O(1) Operations

OperationTime
getDependencies(id)O(1)
getDependents(id)O(1)
addDependency()O(1)
removeDependency()O(1)

Space Complexity

StorageSize
Forward indexO(edges)
Inverse indexO(edges)
Property-specificO(edges)

Total: ~3× the number of dependency edges.

Comparison

Without inverted index:

javascript
// Slow: O(n) scan
getDependents(targetId) {
  const result = [];
  for (const note of allNotes) {
    if (note.references(targetId)) {
      result.push(note);
    }
  }
  return result;
}

With inverted index:

javascript
// Fast: O(1) lookup
getDependents(targetId) {
  return this.dependents.get(targetId) || new Set();
}

Maintaining the Graph

On Note Update

javascript
onNoteUpdated(noteId, oldExpr, newExpr) {
  // Remove old dependencies
  const oldRefs = this.extractReferences(oldExpr);
  for (const ref of oldRefs) {
    this.removeDependency(noteId, ref.noteId);
  }

  // Add new dependencies
  const newRefs = this.extractReferences(newExpr);
  for (const ref of newRefs) {
    this.addDependency(noteId, ref.noteId);
  }
}

On Note Delete

javascript
onNoteDeleted(noteId) {
  // Remove from all indices
  this.dependencies.delete(noteId);

  // Remove from inverse index
  for (const [depId, dependents] of this.dependents) {
    dependents.delete(noteId);
  }
  this.dependents.delete(noteId);

  // Update property-specific indices similarly
}

See Also

Released under the RMT Personal Non-Commercial License