in backend/new.js [865:1016]
function updatePatchProperty(patches, newBlock, objectId, op, docState, propState, listIndex, oldSuccNum) {
const isWholeDoc = !newBlock
const type = op[actionIdx] < ACTIONS.length ? OBJECT_TYPE[ACTIONS[op[actionIdx]]] : null
const opId = `${op[idCtrIdx]}@${docState.actorIds[op[idActorIdx]]}`
const elemIdActor = op[insertIdx] ? op[idActorIdx] : op[keyActorIdx]
const elemIdCtr = op[insertIdx] ? op[idCtrIdx] : op[keyCtrIdx]
const elemId = op[keyStrIdx] ? op[keyStrIdx] : `${elemIdCtr}@${docState.actorIds[elemIdActor]}`
// When the change contains a new make* operation (i.e. with an even-numbered action), record the
// new parent-child relationship in objectMeta. TODO: also handle link/move operations.
if (op[actionIdx] % 2 === 0 && !docState.objectMeta[opId]) {
docState.objectMeta[opId] = {parentObj: objectId, parentKey: elemId, opId, type, children: {}}
deepCopyUpdate(docState.objectMeta, [objectId, 'children', elemId, opId], {objectId: opId, type, props: {}})
}
// firstOp is true if the current operation is the first of a sequence of ops for the same key
const firstOp = !propState[elemId]
if (!propState[elemId]) propState[elemId] = {visibleOps: [], hasChild: false}
// An operation is overwritten if it is a document operation that has at least one successor
const isOverwritten = (oldSuccNum !== undefined && op[succNumIdx] > 0)
// Record all visible values for the property, and whether it has any child object
if (!isOverwritten) {
propState[elemId].visibleOps.push(op)
propState[elemId].hasChild = propState[elemId].hasChild || (op[actionIdx] % 2) === 0 // even-numbered action == make* operation
}
// If one or more of the values of the property is a child object, we update objectMeta to store
// all of the visible values of the property (even the non-child-object values). Then, when we
// subsequently process an update within that child object, we can construct the patch to
// contain the conflicting values.
const prevChildren = docState.objectMeta[objectId].children[elemId]
if (propState[elemId].hasChild || (prevChildren && Object.keys(prevChildren).length > 0)) {
let values = {}
for (let visible of propState[elemId].visibleOps) {
const opId = `${visible[idCtrIdx]}@${docState.actorIds[visible[idActorIdx]]}`
if (ACTIONS[visible[actionIdx]] === 'set') {
values[opId] = Object.assign({type: 'value'}, decodeValue(visible[valLenIdx], visible[valRawIdx]))
} else if (visible[actionIdx] % 2 === 0) {
const objType = visible[actionIdx] < ACTIONS.length ? OBJECT_TYPE[ACTIONS[visible[actionIdx]]] : null
values[opId] = emptyObjectPatch(opId, objType)
}
}
// Copy so that objectMeta is not modified if an exception is thrown while applying change
deepCopyUpdate(docState.objectMeta, [objectId, 'children', elemId], values)
}
let patchKey, patchValue
// For counters, increment operations are succs to the set operation that created the counter,
// but in this case we want to add the values rather than overwriting them.
if (isOverwritten && ACTIONS[op[actionIdx]] === 'set' && (op[valLenIdx] & 0x0f) === VALUE_TYPE.COUNTER) {
// This is the initial set operation that creates a counter. Initialise the counter state
// to contain all successors of the set operation. Only if we later find that each of these
// successor operations is an increment, we make the counter visible in the patch.
if (!propState[elemId]) propState[elemId] = {visibleOps: [], hasChild: false}
if (!propState[elemId].counterStates) propState[elemId].counterStates = {}
let counterStates = propState[elemId].counterStates
let counterState = {opId, value: decodeValue(op[valLenIdx], op[valRawIdx]).value, succs: {}}
for (let i = 0; i < op[succNumIdx]; i++) {
const succOp = `${op[succCtrIdx][i]}@${docState.actorIds[op[succActorIdx][i]]}`
counterStates[succOp] = counterState
counterState.succs[succOp] = true
}
} else if (ACTIONS[op[actionIdx]] === 'inc') {
// Incrementing a previously created counter.
if (!propState[elemId] || !propState[elemId].counterStates || !propState[elemId].counterStates[opId]) {
throw new RangeError(`increment operation ${opId} for unknown counter`)
}
let counterState = propState[elemId].counterStates[opId]
counterState.value += decodeValue(op[valLenIdx], op[valRawIdx]).value
delete counterState.succs[opId]
if (Object.keys(counterState.succs).length === 0) {
patchKey = counterState.opId
patchValue = {type: 'value', datatype: 'counter', value: counterState.value}
// TODO if the counter is in a list element, we need to add a 'remove' action when deleted
}
} else if (!isOverwritten) {
// Add the value to the patch if it is not overwritten (i.e. if it has no succs).
if (ACTIONS[op[actionIdx]] === 'set') {
patchKey = opId
patchValue = Object.assign({type: 'value'}, decodeValue(op[valLenIdx], op[valRawIdx]))
} else if (op[actionIdx] % 2 === 0) { // even-numbered action == make* operation
if (!patches[opId]) patches[opId] = emptyObjectPatch(opId, type)
patchKey = opId
patchValue = patches[opId]
}
}
if (!patches[objectId]) patches[objectId] = emptyObjectPatch(objectId, docState.objectMeta[objectId].type)
const patch = patches[objectId]
// Updating a list or text object (with elemId key)
if (op[keyStrIdx] === null) {
// If we come across any document op that was previously non-overwritten/non-deleted, that
// means the current list element already had a value before this change was applied, and
// therefore the current element cannot be an insert. If we already registered an insert, we
// have to convert it into an update.
if (oldSuccNum === 0 && !isWholeDoc && propState[elemId].action === 'insert') {
propState[elemId].action = 'update'
convertInsertToUpdate(patch.edits, listIndex, elemId)
if (newBlock) newBlock.numVisible[objectId] -= 1
}
if (patchValue) {
// If the op has a non-overwritten value and it came from the change, it's an insert.
// (It's not necessarily the case that op[insertIdx] is true: if a list element is concurrently
// deleted and updated, the node that first processes the deletion and then the update will
// observe the update as a re-insertion of the deleted list element.)
if (!propState[elemId].action && (oldSuccNum === undefined || isWholeDoc)) {
propState[elemId].action = 'insert'
appendEdit(patch.edits, {action: 'insert', index: listIndex, elemId, opId: patchKey, value: patchValue})
if (newBlock) {
if (newBlock.numVisible[objectId] === undefined) newBlock.numVisible[objectId] = 0
newBlock.numVisible[objectId] += 1
}
// If the property has a value and it's not an insert, then it must be an update.
// We might have previously registered it as a remove, in which case we convert it to update.
} else if (propState[elemId].action === 'remove') {
let lastEdit = patch.edits[patch.edits.length - 1]
if (lastEdit.action !== 'remove') throw new RangeError('last edit has unexpected type')
if (lastEdit.count > 1) lastEdit.count -= 1; else patch.edits.pop()
propState[elemId].action = 'update'
appendUpdate(patch.edits, listIndex, elemId, patchKey, patchValue, true)
if (newBlock) newBlock.numVisible[objectId] += 1
} else {
// A 'normal' update
appendUpdate(patch.edits, listIndex, elemId, patchKey, patchValue, !propState[elemId].action)
if (!propState[elemId].action) propState[elemId].action = 'update'
}
} else if (oldSuccNum === 0 && !propState[elemId].action) {
// If the property used to have a non-overwritten/non-deleted value, but no longer, it's a remove
propState[elemId].action = 'remove'
appendEdit(patch.edits, {action: 'remove', index: listIndex, count: 1})
if (newBlock) newBlock.numVisible[objectId] -= 1
}
} else if (patchValue || !isWholeDoc) {
// Updating a map or table (with string key)
if (firstOp || !patch.props[op[keyStrIdx]]) patch.props[op[keyStrIdx]] = {}
if (patchValue) patch.props[op[keyStrIdx]][patchKey] = patchValue
}
}