in backend/sync.js [327:393]
function generateSyncMessage(backend, syncState) {
if (!backend) {
throw new Error("generateSyncMessage called with no Automerge document")
}
if (!syncState) {
throw new Error("generateSyncMessage requires a syncState, which can be created with initSyncState()")
}
let { sharedHeads, lastSentHeads, theirHeads, theirNeed, theirHave, sentHashes } = syncState
const ourHeads = Backend.getHeads(backend)
// Hashes to explicitly request from the remote peer: any missing dependencies of unapplied
// changes, and any of the remote peer's heads that we don't know about
const ourNeed = Backend.getMissingDeps(backend, theirHeads || [])
// There are two reasons why ourNeed may be nonempty: 1. we might be missing dependencies due to
// Bloom filter false positives; 2. we might be missing heads that the other peer mentioned
// because they (intentionally) only sent us a subset of changes. In case 1, we leave the `have`
// field of the message empty because we just want to fill in the missing dependencies for now.
// In case 2, or if ourNeed is empty, we send a Bloom filter to request any unsent changes.
let ourHave = []
if (!theirHeads || ourNeed.every(hash => theirHeads.includes(hash))) {
ourHave = [makeBloomFilter(backend, sharedHeads)]
}
// Fall back to a full re-sync if the sender's last sync state includes hashes
// that we don't know. This could happen if we crashed after the last sync and
// failed to persist changes that the other node already sent us.
if (theirHave && theirHave.length > 0) {
const lastSync = theirHave[0].lastSync
if (!lastSync.every(hash => Backend.getChangeByHash(backend, hash))) {
// we need to queue them to send us a fresh sync message, the one they sent is uninteligible so we don't know what they need
const resetMsg = {heads: ourHeads, need: [], have: [{ lastSync: [], bloom: new Uint8Array(0) }], changes: []}
return [syncState, encodeSyncMessage(resetMsg)]
}
}
// XXX: we should limit ourselves to only sending a subset of all the messages, probably limited by a total message size
// these changes should ideally be RLE encoded but we haven't implemented that yet.
let changesToSend = Array.isArray(theirHave) && Array.isArray(theirNeed) ? getChangesToSend(backend, theirHave, theirNeed) : []
// If the heads are equal, we're in sync and don't need to do anything further
const headsUnchanged = Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)
const headsEqual = Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)
if (headsUnchanged && headsEqual && changesToSend.length === 0) {
// no need to send a sync message if we know we're synced!
return [syncState, null]
}
// TODO: this recomputes the SHA-256 hash of each change; we should restructure this to avoid the
// unnecessary recomputation
changesToSend = changesToSend.filter(change => !sentHashes[decodeChangeMeta(change, true).hash])
// Regular response to a sync message: send any changes that the other node
// doesn't have. We leave the "have" field empty because the previous message
// generated by `syncStart` already indicated what changes we have.
const syncMessage = {heads: ourHeads, have: ourHave, need: ourNeed, changes: changesToSend}
if (changesToSend.length > 0) {
sentHashes = copyObject(sentHashes)
for (const change of changesToSend) {
sentHashes[decodeChangeMeta(change, true).hash] = true
}
}
syncState = Object.assign({}, syncState, {lastSentHeads: ourHeads, sentHashes})
return [syncState, encodeSyncMessage(syncMessage)]
}