Skip to content

Commit fc55ebd

Browse files
committed
Add OpId for deletions in Patch
1 parent 08a456c commit fc55ebd

File tree

7 files changed

+52
-25
lines changed

7 files changed

+52
-25
lines changed

@types/automerge/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ declare module 'automerge' {
258258
// (properties that are not changing are not listed). The nested object is
259259
// empty if the property is being deleted, contains one opId if it is set to
260260
// a single value, and contains multiple opIds if there is a conflict.
261-
props: {[propName: string]: {[opId: string]: MapDiff | ListDiff | ValueDiff }}
261+
props: {[propName: string]: {[opId: string]: MapDiff | ListDiff | ValueDiff | '___DELETED___' }}
262262
}
263263

264264
// Describes changes to a list or Automerge.Text object, in which each element
@@ -309,6 +309,7 @@ declare module 'automerge' {
309309
// text object.
310310
interface RemoveEdit {
311311
action: 'remove'
312+
opId: OpId // ID of the operation that removed this value. NOTE: if count > 1 opId is the first operation.
312313
index: number // index of the first list element to remove
313314
count: number // number of list elements to remove
314315
}

backend/new.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { parseOpId, copyObject } = require('../src/common')
1+
const { parseOpId, copyObject, DELETED_MARKER } = require('../src/common')
22
const { COLUMN_TYPE, VALUE_TYPE, ACTIONS, OBJECT_TYPE, DOC_OPS_COLUMNS, CHANGE_COLUMNS, DOCUMENT_COLUMNS,
33
encoderByColumnId, decoderByColumnId, makeDecoders, decodeValue,
44
encodeChange, decodeChangeColumns, decodeChangeMeta, decodeChanges, decodeDocumentHeader, encodeDocumentHeader } = require('./columnar')
@@ -1021,15 +1021,30 @@ function updatePatchProperty(patches, newBlock, objectId, op, docState, propStat
10211021
} else if (oldSuccNum === 0 && !propState[elemId].action) {
10221022
// If the property used to have a non-overwritten/non-deleted value, but no longer, it's a remove
10231023
propState[elemId].action = 'remove'
1024-
appendEdit(patch.edits, {action: 'remove', index: listIndex, count: 1})
1024+
const removeOpId = `${op[succCtrIdx]}@${docState.actorIds[op[succActorIdx]]}`
1025+
appendEdit(patch.edits, {action: 'remove', index: listIndex, count: 1, opId: removeOpId})
10251026
if (newBlock && newBlock.lastObjectActor === op[objActorIdx] && newBlock.lastObjectCtr === op[objCtrIdx]) {
10261027
newBlock.numVisible -= 1
10271028
}
10281029
}
10291030

10301031
} else if (patchValue || !isWholeDoc) {
10311032
// Updating a map or table (with string key)
1032-
if (firstOp || !patch.props[op[keyStrIdx]]) patch.props[op[keyStrIdx]] = {}
1033+
if (firstOp || !patch.props[op[keyStrIdx]]) {
1034+
patch.props[op[keyStrIdx]] = {}
1035+
// Go over succ (which are successors to this operations) and consider them as deletion operation.
1036+
// If a succ operation is really a deletion, it'll stay on patch.
1037+
// If a succ operation isn't a deletion, we'll see it in this function and delete from patch (see below).
1038+
for (let i = 0; i < op[succNumIdx]; i++) {
1039+
const succOp = `${op[succCtrIdx][i]}@${docState.actorIds[op[succActorIdx][i]]}`
1040+
patch.props[op[keyStrIdx]][succOp] = DELETED_MARKER
1041+
}
1042+
}
1043+
// If we look at operation, we know it's not a deletion (we don't store deletions in history).
1044+
// That means we can delete it from props if earlier we added it when considering it as a delete.
1045+
if (patch.props[op[keyStrIdx]] !== null && patch.props[op[keyStrIdx]][opId] === DELETED_MARKER) {
1046+
delete patch.props[op[keyStrIdx]][opId]
1047+
}
10331048
if (patchValue) patch.props[op[keyStrIdx]][patchKey] = patchValue
10341049
}
10351050
}

frontend/apply_patch.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { isObject, copyObject, parseOpId } = require('../src/common')
1+
const { isObject, copyObject, parseOpId, DELETED_MARKER } = require('../src/common')
22
const { OBJECT_ID, CONFLICTS, ELEM_IDS } = require('./constants')
33
const { instantiateText } = require('./text')
44
const { instantiateTable } = require('./table')
@@ -20,6 +20,8 @@ function getValue(patch, object, updated) {
2020
return new Date(patch.value)
2121
} else if (patch.datatype === 'counter') {
2222
return new Counter(patch.value)
23+
} else if (patch === DELETED_MARKER) {
24+
return DELETED_MARKER;
2325
} else {
2426
// Primitive value (int, uint, float64, string, boolean, or null)
2527
return patch.value
@@ -59,20 +61,25 @@ function applyProperties(props, object, conflicts, updated) {
5961

6062
for (let key of Object.keys(props)) {
6163
const values = {}, opIds = Object.keys(props[key]).sort(lamportCompare).reverse()
64+
const appliedOpIds = []
6265
for (let opId of opIds) {
6366
const subpatch = props[key][opId]
67+
if (subpatch === DELETED_MARKER) {
68+
continue
69+
}
70+
appliedOpIds.push(opId)
6471
if (conflicts[key] && conflicts[key][opId]) {
6572
values[opId] = getValue(subpatch, conflicts[key][opId], updated)
6673
} else {
6774
values[opId] = getValue(subpatch, undefined, updated)
6875
}
6976
}
7077

71-
if (opIds.length === 0) {
78+
if (appliedOpIds.length === 0) {
7279
delete object[key]
7380
delete conflicts[key]
7481
} else {
75-
object[key] = values[opIds[0]]
82+
object[key] = values[appliedOpIds[0]]
7683
conflicts[key] = values
7784
}
7885
}

frontend/context.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ class Context {
167167
subpatch = values[nextOpId]
168168
object = this.getPropertyValue(object, pathElem.key, nextOpId)
169169
}
170-
171170
return subpatch
172171
}
173172

src/common.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ function createArrayOfNulls(length) {
5050
return array
5151
}
5252

53+
const DELETED_MARKER = '___DELETED___'
54+
5355
module.exports = {
54-
isObject, copyObject, parseOpId, equalBytes, createArrayOfNulls
56+
isObject, copyObject, parseOpId, equalBytes, createArrayOfNulls, DELETED_MARKER
5557
}

test/backend_test.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') :
44
const Backend = Automerge.Backend
55
const { encodeChange, decodeChange } = require('../backend/columnar')
66
const uuid = require('../src/uuid')
7+
const { DELETED_MARKER } = require("../src/common")
78

89
function hash(change) {
910
return decodeChange(encodeChange(change)).hash
@@ -79,7 +80,7 @@ describe('Automerge.Backend', () => {
7980
const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])
8081
assert.deepStrictEqual(patch2, {
8182
clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 2, pendingChanges: 0,
82-
diffs: {objectId: '_root', type: 'map', props: {bird: {}}}
83+
diffs: {objectId: '_root', type: 'map', props: {bird: {[`2@${actor}`]: DELETED_MARKER}}}
8384
})
8485
})
8586

@@ -132,7 +133,7 @@ describe('Automerge.Backend', () => {
132133
const [s1, patch1] = Backend.applyChanges(s0, [change1, change2].map(encodeChange))
133134
assert.deepStrictEqual(patch1, {
134135
clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 3, pendingChanges: 0,
135-
diffs: {objectId: '_root', type: 'map', props: {birds: {}}}
136+
diffs: {objectId: '_root', type: 'map', props: {birds: {[`3@${actor}`]: DELETED_MARKER}}}
136137
})
137138
})
138139

@@ -209,7 +210,7 @@ describe('Automerge.Backend', () => {
209210
const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change3)])
210211
assert.deepStrictEqual(patch1, {
211212
clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change2)], maxOp: 3, pendingChanges: 0,
212-
diffs: {objectId: '_root', type: 'map', props: {birds: {}}}
213+
diffs: {objectId: '_root', type: 'map', props: {birds: {[`3@${actor2}`]: DELETED_MARKER}}}
213214
})
214215
assert.deepStrictEqual(patch2, {
215216
clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), maxOp: 3, pendingChanges: 0,
@@ -380,7 +381,7 @@ describe('Automerge.Backend', () => {
380381
clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 3, pendingChanges: 0,
381382
diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {
382383
objectId: `1@${actor}`, type: 'list', edits: [
383-
{action: 'remove', index: 0, count: 1}
384+
{action: 'remove', index: 0, count: 1, opId: `3@${actor}`}
384385
]
385386
}}}}
386387
})
@@ -403,7 +404,7 @@ describe('Automerge.Backend', () => {
403404
diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {
404405
objectId: `1@${actor}`, type: 'list', edits: [
405406
{action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'chaffinch'}},
406-
{action: 'remove', index: 0, count: 1}
407+
{action: 'remove', index: 0, count: 1, opId: `3@${actor}`}
407408
]
408409
}}}}
409410
})
@@ -518,7 +519,7 @@ describe('Automerge.Backend', () => {
518519
done: {[`4@${actor1}`]: {type: 'value', value: false}}
519520
}
520521
}},
521-
{action: 'remove', index: 0, count: 1}
522+
{action: 'remove', index: 0, count: 1, opId: `5@${actor2}`}
522523
]
523524
}}}}
524525
})
@@ -710,7 +711,7 @@ describe('Automerge.Backend', () => {
710711
clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 9, pendingChanges: 0,
711712
diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {
712713
objectId: `1@${actor}`, type: 'list', edits: [
713-
{action: 'remove', index: 1, count: 3}
714+
{action: 'remove', index: 1, count: 3, opId: `7@${actor}`}
714715
]
715716
}}}}
716717
})
@@ -882,7 +883,7 @@ describe('Automerge.Backend', () => {
882883
birds: {'1@111111': {objectId: '1@111111', type: 'list',
883884
edits: [
884885
{action: 'insert', index: 0, elemId: '2@111111', opId: '2@111111', value: {type: 'value', value: 'magpie'}},
885-
{action: 'remove', index: 0, count: 1}
886+
{action: 'remove', index: 0, count: 1, opId: '3@111111'}
886887
]}}
887888
}}
888889
})
@@ -971,7 +972,7 @@ describe('Automerge.Backend', () => {
971972
clock: {[actor]: 2}, deps: [], maxOp: 9, actor, seq: 2, pendingChanges: 0,
972973
diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {
973974
objectId: `1@${actor}`, type: 'list', edits: [
974-
{action: 'remove', index: 1, count: 3}
975+
{action: 'remove', index: 1, count: 3, opId: `7@${actor}`}
975976
]
976977
}}}}
977978
})

test/new_backend_test.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { checkEncoded } = require('./helpers')
33
const { DOC_OPS_COLUMNS, encodeChange, decodeChange } = require('../backend/columnar')
44
const { MAX_BLOCK_SIZE, BackendDoc, bloomFilterContains } = require('../backend/new')
55
const uuid = require('../src/uuid')
6+
const { DELETED_MARKER } = require("../src/common")
67

78
function checkColumns(block, expectedCols) {
89
for (let actual of block.columns) {
@@ -625,7 +626,7 @@ describe('BackendDoc applying changes', () => {
625626
assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {
626627
maxOp: 3, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,
627628
diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {
628-
objectId: `1@${actor}`, type: 'text', edits: [{action: 'remove', index: 0, count: 1}]
629+
objectId: `1@${actor}`, type: 'text', edits: [{action: 'remove', index: 0, count: 1, opId: `3@${actor}`}]
629630
}}}}
630631
})
631632
checkColumns(backend.blocks[0], {
@@ -678,7 +679,7 @@ describe('BackendDoc applying changes', () => {
678679
assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {
679680
maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,
680681
diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {
681-
objectId: `1@${actor}`, type: 'text', edits: [{action: 'remove', index: 1, count: 1}]
682+
objectId: `1@${actor}`, type: 'text', edits: [{action: 'remove', index: 1, count: 1, opId: `5@${actor}`}]
682683
}}}}
683684
})
684685
checkColumns(backend.blocks[0], {
@@ -1271,7 +1272,7 @@ describe('BackendDoc applying changes', () => {
12711272
})
12721273
assert.deepStrictEqual(backend.applyChanges([encodeChange(change3)]), {
12731274
maxOp: 3, clock: {[actor]: 3}, deps: [hash(change3)], pendingChanges: 0,
1274-
diffs: {objectId: '_root', type: 'map', props: {counter: {}}}
1275+
diffs: {objectId: '_root', type: 'map', props: {counter: {[`3@${actor}`]: DELETED_MARKER}}}
12751276
})
12761277
assert.strictEqual(backend.blocks[0].lastKey, 'counter')
12771278
assert.strictEqual(backend.blocks[0].numOps, 2)
@@ -1618,7 +1619,7 @@ describe('BackendDoc applying changes', () => {
16181619
diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {
16191620
objectId: `1@${actor}`, type: 'text', edits: [
16201621
{action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'b']},
1621-
{action: 'remove', index: 0, count: 1},
1622+
{action: 'remove', index: 0, count: 1, opId: `4@${actor}`},
16221623
{action: 'update', index: 0, opId: `5@${actor}`, value: {type: 'value', value: 'x'}}
16231624
]
16241625
}}}}
@@ -1670,7 +1671,7 @@ describe('BackendDoc applying changes', () => {
16701671
{action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {
16711672
type: 'value', value: 1, datatype: 'uint'
16721673
}},
1673-
{action: 'remove', index: 0, count: 1}
1674+
{action: 'remove', index: 0, count: 1, opId: `3@${actor1}`}
16741675
]
16751676
}}}}
16761677
})
@@ -2069,14 +2070,15 @@ describe('BackendDoc applying changes', () => {
20692070
for (let i = 2; i <= MAX_BLOCK_SIZE; i++) {
20702071
change1.ops.push({action: 'set', obj: `1@${actor}`, elemId: `${i}@${actor}`, insert: true, value: 'a', pred: []})
20712072
}
2072-
const change2 = {actor, seq: 2, startOp: MAX_BLOCK_SIZE + 3, time: 0, deps: [], ops: []}
2073+
const delStartOp = MAX_BLOCK_SIZE + 3
2074+
const change2 = {actor, seq: 2, startOp: delStartOp, time: 0, deps: [], ops: []}
20732075
for (let i = 2; i <= MAX_BLOCK_SIZE + 1; i++) {
20742076
change2.ops.push({action: 'del', obj: `1@${actor}`, elemId: `${i}@${actor}`, insert: false, pred: [`${i}@${actor}`]})
20752077
}
20762078
const backend = new BackendDoc()
20772079
backend.applyChanges([encodeChange(change1)])
20782080
const patch = backend.applyChanges([encodeChange(change2)])
2079-
assert.deepStrictEqual(patch.diffs.props.text[`1@${actor}`].edits, [{action: 'remove', index: 0, count: MAX_BLOCK_SIZE}])
2081+
assert.deepStrictEqual(patch.diffs.props.text[`1@${actor}`].edits, [{action: 'remove', index: 0, count: MAX_BLOCK_SIZE, opId: `${delStartOp}@${actor}`}])
20802082
assert.strictEqual(backend.blocks.length, 2)
20812083
const sizeByte1 = 0x80 | 0x7f & (MAX_BLOCK_SIZE / 2), sizeByte2 = (MAX_BLOCK_SIZE / 2) >>> 7
20822084
const firstSucc = MAX_BLOCK_SIZE + 3, secondSucc = MAX_BLOCK_SIZE + 3 + MAX_BLOCK_SIZE / 2

0 commit comments

Comments
 (0)