computeHashGraph() {
    const binaryDoc = this.save()
    this.haveHashGraph = true
    this.changes = []
    this.changeIndexByHash = {}
    this.dependenciesByHash = {}
    this.dependentsByHash = {}
    this.hashesByActor = {}
    this.clock = {}

    for (let change of decodeChanges([binaryDoc])) {
      const binaryChange = encodeChange(change) // TODO: avoid decoding and re-encoding again
      this.changes.push(binaryChange)
      this.changeIndexByHash[change.hash] = this.changes.length - 1
      this.dependenciesByHash[change.hash] = change.deps
      this.dependentsByHash[change.hash] = []
      for (let dep of change.deps) this.dependentsByHash[dep].push(change.hash)
      if (change.seq === 1) this.hashesByActor[change.actor] = []
      this.hashesByActor[change.actor].push(change.hash)
      const expectedSeq = (this.clock[change.actor] || 0) + 1
      if (change.seq !== expectedSeq) {
        throw new RangeError(`Expected seq ${expectedSeq}, got seq ${change.seq} from actor ${change.actor}`)
      }
      this.clock[change.actor] = change.seq
    }
  }
Reconstructs the full change history of a document, and initialises the variables that allow us
to traverse the hash graph of changes and their dependencies. When a compressed document is
loaded we defer the computation of this hash graph to make loading faster, but if the hash
graph is later needed (e.g. for the sync protocol), this function fills it in.