Skip to content

Commit 48f4c5b

Browse files
committed
Add deleted marker
1 parent 6149da1 commit 48f4c5b

File tree

7 files changed

+38
-14
lines changed

7 files changed

+38
-14
lines changed

@types/automerge/index.d.ts

Lines changed: 1 addition & 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

backend/new.js

Lines changed: 17 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,7 +1021,7 @@ 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-
const removeOpId = `${op[succCtrIdx]}@${docState.actorIds[op[idActorIdx]]}`
1024+
const removeOpId = `${op[succCtrIdx]}@${docState.actorIds[op[succActorIdx]]}`
10251025
appendEdit(patch.edits, {action: 'remove', index: listIndex, count: 1, opId: removeOpId})
10261026
if (newBlock && newBlock.lastObjectActor === op[objActorIdx] && newBlock.lastObjectCtr === op[objCtrIdx]) {
10271027
newBlock.numVisible -= 1
@@ -1030,7 +1030,21 @@ function updatePatchProperty(patches, newBlock, objectId, op, docState, propStat
10301030

10311031
} else if (patchValue || !isWholeDoc) {
10321032
// Updating a map or table (with string key)
1033-
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+
}
10341048
if (patchValue) patch.props[op[keyStrIdx]][patchKey] = patchValue
10351049
}
10361050
}

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: 5 additions & 4 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,
@@ -518,7 +519,7 @@ describe('Automerge.Backend', () => {
518519
done: {[`4@${actor1}`]: {type: 'value', value: false}}
519520
}
520521
}},
521-
{action: 'remove', index: 0, count: 1, opId: `5@${actor1}`}
522+
{action: 'remove', index: 0, count: 1, opId: `5@${actor2}`}
522523
]
523524
}}}}
524525
})

test/new_backend_test.js

Lines changed: 2 additions & 1 deletion
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) {
@@ -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)

0 commit comments

Comments
 (0)