function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
  if (!backend) {
    throw new Error("generateSyncMessage called with no Automerge document")
  }
  if (!oldSyncState) {
    throw new Error(
      "generateSyncMessage requires a syncState, which can be created with initSyncState()",
    )
  }

  let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState,
    patch = null
  const message = decodeSyncMessage(binaryMessage)
  const beforeHeads = Backend.getHeads(backend)

  // If we received changes, we try to apply them to the document. There may still be missing
  // dependencies due to Bloom filter false positives, in which case the backend will enqueue the
  // changes without applying them. The set of changes may also be incomplete if the sender decided
  // to break a large set of changes into chunks.
  if (message.changes.length > 0) {
    ;[backend, patch] = Backend.applyChanges(backend, message.changes)
    sharedHeads = advanceHeads(
      beforeHeads,
      Backend.getHeads(backend),
      sharedHeads,
    )
  }

  // If heads are equal, indicate we don't need to send a response message
  if (
    message.changes.length === 0 &&
    compareArrays(message.heads, beforeHeads)
  ) {
    lastSentHeads = message.heads
  }

  // If all of the remote heads are known to us, that means either our heads are equal, or we are
  // ahead of the remote peer. In this case, take the remote heads to be our shared heads.
  const knownHeads = message.heads.filter(head =>
    Backend.getChangeByHash(backend, head),
  )
  if (knownHeads.length === message.heads.length) {
    sharedHeads = message.heads
    // If the remote peer has lost all its data, reset our state to perform a full resync
    if (message.heads.length === 0) {
      lastSentHeads = []
      sentHashes = []
    }
  } else {
    // If some remote heads are unknown to us, we add all the remote heads we know to
    // sharedHeads, but don't remove anything from sharedHeads. This might cause sharedHeads to
    // contain some redundant hashes (where one hash is actually a transitive dependency of
    // another), but this will be cleared up as soon as we know all the remote heads.
    sharedHeads = [...new Set(knownHeads.concat(sharedHeads))].sort()
  }

  const syncState = {
    sharedHeads, // what we have in common to generate an efficient bloom filter
    lastSentHeads,
    theirHave: message.have, // the information we need to calculate the changes they need
    theirHeads: message.heads,
    theirNeed: message.need,
    sentHashes,
  }
  return [backend, syncState, patch]
}
Given a backend, a message message and the state of our peer, apply any changes, update what
we believe about the peer, and (if there were applied changes) produce a patch for the frontend