in backend/columnar.js [876:943]
function groupChangeOps(changes, ops) {
let changesByActor = {} // map from actorId to array of changes by that actor
for (let change of changes) {
change.ops = []
if (!changesByActor[change.actor]) changesByActor[change.actor] = []
if (change.seq !== changesByActor[change.actor].length + 1) {
throw new RangeError(`Expected seq = ${changesByActor[change.actor].length + 1}, got ${change.seq}`)
}
if (change.seq > 1 && changesByActor[change.actor][change.seq - 2].maxOp > change.maxOp) {
throw new RangeError('maxOp must increase monotonically per actor')
}
changesByActor[change.actor].push(change)
}
let opsById = {}
for (let op of ops) {
if (op.action === 'del') throw new RangeError('document should not contain del operations')
op.pred = opsById[op.id] ? opsById[op.id].pred : []
opsById[op.id] = op
for (let succ of op.succ) {
if (!opsById[succ]) {
if (op.elemId) {
const elemId = op.insert ? op.id : op.elemId
opsById[succ] = {id: succ, action: 'del', obj: op.obj, elemId, pred: []}
} else {
opsById[succ] = {id: succ, action: 'del', obj: op.obj, key: op.key, pred: []}
}
}
opsById[succ].pred.push(op.id)
}
delete op.succ
}
for (let op of Object.values(opsById)) {
if (op.action === 'del') ops.push(op)
}
for (let op of ops) {
const { counter, actorId } = parseOpId(op.id)
const actorChanges = changesByActor[actorId]
// Binary search to find the change that should contain this operation
let left = 0, right = actorChanges.length
while (left < right) {
const index = Math.floor((left + right) / 2)
if (actorChanges[index].maxOp < counter) {
left = index + 1
} else {
right = index
}
}
if (left >= actorChanges.length) {
throw new RangeError(`Operation ID ${op.id} outside of allowed range`)
}
actorChanges[left].ops.push(op)
}
for (let change of changes) {
change.ops.sort((op1, op2) => sortOpIds(op1.id, op2.id))
change.startOp = change.maxOp - change.ops.length + 1
delete change.maxOp
for (let i = 0; i < change.ops.length; i++) {
const op = change.ops[i], expectedId = `${change.startOp + i}@${change.actor}`
if (op.id !== expectedId) {
throw new RangeError(`Expected opId ${expectedId}, got ${op.id}`)
}
delete op.id
}
}
}