Skip to content

Commit 353f8dc

Browse files
committed
fix(MergeSyncProcess): reconcileDiffs was outdated
Signed-off-by: Marcel Klehr <[email protected]>
1 parent 9b3fbb2 commit 353f8dc

File tree

2 files changed

+97
-46
lines changed

2 files changed

+97
-46
lines changed

src/lib/strategies/Default.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,7 @@ export default class SyncProcess {
605605
return {localScanResult, serverScanResult}
606606
}
607607

608+
// Note: Parts of this are duplicated to MergeSyncProcess!
608609
async reconcileDiffs<L1 extends TItemLocation, L2 extends TItemLocation, L3 extends TItemLocation>(
609610
sourceScanResult:ScanResult<L1, L2>,
610611
targetScanResult:ScanResult<TOppositeLocation<L1>, L3>,

src/lib/strategies/Merge.ts

Lines changed: 96 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Folder, ItemLocation, TItem, TItemLocation, TOppositeLocation } from '../Tree'
2-
import Diff, { CreateAction, MoveAction, PlanStage1, PlanStage3, ReorderAction } from '../Diff'
1+
import { Folder, ItemLocation, ItemType, TItem, TItemLocation, TOppositeLocation } from '../Tree'
2+
import Diff, { CreateAction, MoveAction, PlanStage1 } from '../Diff'
33
import Scanner, { ScanResult } from '../Scanner'
44
import * as Parallel from 'async-parallel'
55
import DefaultSyncProcess, { ISerializedSyncProcess } from './Default'
6-
import Mappings, { MappingSnapshot } from '../Mappings'
6+
import Mappings from '../Mappings'
77
import Logger from '../Logger'
88

99
const ACTION_CONCURRENCY = 12
@@ -49,27 +49,34 @@ export default class MergeSyncProcess extends DefaultSyncProcess {
4949
return {localScanResult, serverScanResult}
5050
}
5151

52+
// This is a copy of DefaultSyncProcess#reconcileDiffs without anything involving REMOVEs
5253
async reconcileDiffs<L1 extends TItemLocation, L2 extends TItemLocation, L3 extends TItemLocation>(
5354
sourceScanResult:ScanResult<L1, L2>,
5455
targetScanResult:ScanResult<TOppositeLocation<L1>, L3>,
5556
targetLocation: TOppositeLocation<L1>
5657
): Promise<PlanStage1<L1, L2>> {
58+
Logger.log('Reconciling diffs to prepare a plan for ' + targetLocation)
5759
const mappingsSnapshot = this.mappings.getSnapshot()
5860

5961
const targetCreations = targetScanResult.CREATE.getActions()
62+
const targetRemovals = targetScanResult.REMOVE.getActions()
6063
const targetMoves = targetScanResult.MOVE.getActions()
64+
const targetUpdates = targetScanResult.UPDATE.getActions()
65+
const targetReorders = targetScanResult.REORDER.getActions()
6166

67+
const sourceRemovals = sourceScanResult.REMOVE.getActions()
6268
const sourceMoves = sourceScanResult.MOVE.getActions()
63-
const sourceUpdates = sourceScanResult.UPDATE.getActions()
6469

65-
const targetTree = this.getTargetTree(targetLocation)
66-
const sourceTree = this.getTargetTree(targetLocation === ItemLocation.LOCAL ? ItemLocation.SERVER : ItemLocation.LOCAL) as Folder<L1>
70+
const targetTree : Folder<TOppositeLocation<L1>> = (targetLocation === ItemLocation.LOCAL ? this.localTreeRoot : this.serverTreeRoot) as Folder<TOppositeLocation<L1>>
71+
const sourceTree : Folder<L1> = (targetLocation === ItemLocation.LOCAL ? this.serverTreeRoot : this.localTreeRoot) as Folder<L1>
6772

6873
const allCreateAndMoveActions = (sourceScanResult.CREATE.getActions() as Array<CreateAction<L1, L2> | MoveAction<L1, L2> | CreateAction<TOppositeLocation<L1>, L3> | MoveAction<TOppositeLocation<L1>, L3>>)
6974
.concat(sourceScanResult.MOVE.getActions())
7075
.concat(targetScanResult.CREATE.getActions())
7176
.concat(targetScanResult.MOVE.getActions())
7277

78+
const avoidTargetReorders = {}
79+
7380
// Prepare target plan
7481
const targetPlan: PlanStage1<L1, L2> = {
7582
CREATE: new Diff(),
@@ -79,16 +86,18 @@ export default class MergeSyncProcess extends DefaultSyncProcess {
7986
REORDER: new Diff(),
8087
}
8188

89+
const findChainCacheForCreations = {}
8290
await Parallel.each(sourceScanResult.CREATE.getActions(), async(action) => {
83-
const concurrentCreation = targetCreations.find(a =>
84-
a.payload.parentId === Mappings.mapParentId(mappingsSnapshot, action.payload, a.payload.location) &&
85-
action.payload.canMergeWith(a.payload))
91+
const concurrentCreation = targetCreations.find(a => (
92+
action.payload.parentId === Mappings.mapParentId(mappingsSnapshot, a.payload, action.payload.location) &&
93+
action.payload.canMergeWith(a.payload)
94+
))
8695
if (concurrentCreation) {
87-
// created on both the server and locally, try to reconcile
96+
// created on both the target and sourcely, try to reconcile
8897
const newMappings = []
8998
const subScanner = new Scanner(
90-
concurrentCreation.payload, // server tree
91-
action.payload, // local tree
99+
concurrentCreation.payload, // target tree
100+
action.payload, // source tree
92101
(oldItem, newItem) => {
93102
if (oldItem.type === newItem.type && oldItem.canMergeWith(newItem)) {
94103
// if two items can be merged, we'll add mappings here directly
@@ -98,7 +107,6 @@ export default class MergeSyncProcess extends DefaultSyncProcess {
98107
return false
99108
},
100109
this.hashSettings,
101-
false,
102110
false
103111
)
104112
await subScanner.run()
@@ -110,76 +118,118 @@ export default class MergeSyncProcess extends DefaultSyncProcess {
110118
return
111119
}
112120

121+
const concurrentRemoval = targetScanResult.REMOVE.getActions().find(targetRemoval =>
122+
// target removal removed this creation's target (via some chain)
123+
Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, sourceTree, action.payload, targetRemoval, findChainCacheForCreations)
124+
)
125+
if (concurrentRemoval) {
126+
avoidTargetReorders[action.payload.parentId] = true
127+
// Already deleted on target, do nothing.
128+
return
129+
}
130+
113131
targetPlan.CREATE.commit(action)
114132
}, ACTION_CONCURRENCY)
115133

116134
await Parallel.each(sourceScanResult.MOVE.getActions(), async(action) => {
117-
if (targetLocation === ItemLocation.LOCAL) {
118-
const concurrentMove = sourceMoves.find(a =>
119-
(action.payload.type === a.payload.type && Mappings.mappable(mappingsSnapshot, action.payload, a.payload)) ||
120-
(action.payload.type === 'bookmark' && action.payload.canMergeWith(a.payload))
121-
)
135+
if (targetLocation === this.masterLocation) {
136+
const concurrentMove = targetMoves.find(a =>
137+
action.payload.type === a.payload.type && Mappings.mappable(mappingsSnapshot, action.payload, a.payload))
122138
if (concurrentMove) {
123-
// Moved both on server and locally, local has precedence: do nothing locally
139+
// Moved both on target and sourcely, master has precedence: do nothing on master
124140
return
125141
}
126142
}
143+
144+
let findChainCache1 = {}, findChainCache2 = {}
127145
// Find concurrent moves that form a hierarchy reversal together with this one
128146
const concurrentHierarchyReversals = targetMoves.filter(targetMove => {
129-
return Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, sourceTree, action.payload, targetMove) &&
130-
Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, targetTree, targetMove.payload, action)
147+
return Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, sourceTree, action.payload, targetMove, findChainCache1) &&
148+
Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, targetTree, targetMove.payload, action, findChainCache2)
131149
})
132150
if (concurrentHierarchyReversals.length) {
133-
if (targetLocation === ItemLocation.SERVER) {
151+
if (targetLocation !== this.masterLocation) {
152+
targetPlan.MOVE.commit(action)
153+
154+
findChainCache1 = {}
155+
findChainCache2 = {}
134156
concurrentHierarchyReversals.forEach(a => {
135-
// moved locally but moved in reverse hierarchical order on server
136-
const payload = a.oldItem.copyWithLocation(false, action.payload.location) // we don't map here as we want this to look like a local action
157+
// moved sourcely but moved in reverse hierarchical order on target
158+
const payload = a.oldItem.copyWithLocation(false, action.payload.location)
137159
const oldItem = a.payload.copyWithLocation(false, action.oldItem.location)
138-
oldItem.id = Mappings.mapId(mappingsSnapshot, oldItem, action.payload.location)
139-
oldItem.parentId = Mappings.mapParentId(mappingsSnapshot, oldItem, action.payload.location)
160+
oldItem.id = Mappings.mapId(mappingsSnapshot, a.payload, action.oldItem.location)
161+
oldItem.parentId = Mappings.mapParentId(mappingsSnapshot, a.payload, action.oldItem.location)
140162

141163
if (
164+
// Don't create duplicates!
142165
targetPlan.MOVE.getActions().find(move => String(move.payload.id) === String(payload.id)) ||
143-
sourceMoves.find(move => String(move.payload.id) === String(payload.id))
166+
sourceMoves.find(move => String(move.payload.id) === String(payload.id)) ||
167+
// Don't move back into removed territory
168+
targetRemovals.find(remove => Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, sourceTree, action.payload, remove, findChainCache1)) ||
169+
sourceRemovals.find(remove => Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, targetTree, action.payload, remove, findChainCache2))
144170
) {
145-
// Don't create duplicates!
146171
return
147172
}
148173

149-
// revert server move
174+
// revert target move
150175
targetPlan.MOVE.commit({ ...a, payload, oldItem })
176+
DefaultSyncProcess.removeItemFromReorders(mappingsSnapshot, sourceScanResult.REORDER, payload)
177+
DefaultSyncProcess.removeItemFromReorders(mappingsSnapshot, sourceScanResult.REORDER, oldItem)
151178
})
152-
targetPlan.MOVE.commit(action)
179+
} else {
180+
DefaultSyncProcess.removeItemFromReorders(mappingsSnapshot, sourceScanResult.REORDER, action.oldItem)
181+
DefaultSyncProcess.removeItemFromReorders(mappingsSnapshot, sourceScanResult.REORDER, action.payload)
153182
}
154-
155-
// if target === LOCAL: Moved locally and in reverse hierarchical order on server. local has precedence: do nothing locally
156183
return
157184
}
158185

159186
targetPlan.MOVE.commit(action)
160-
}, ACTION_CONCURRENCY)
187+
}, 1)
161188

162189
await Parallel.each(sourceScanResult.UPDATE.getActions(), async(action) => {
163-
const concurrentUpdate = sourceUpdates.find(a =>
190+
const concurrentUpdate = targetUpdates.find(a =>
164191
action.payload.type === a.payload.type && Mappings.mappable(mappingsSnapshot, action.payload, a.payload))
165-
if (concurrentUpdate && targetLocation === ItemLocation.LOCAL) {
166-
// Updated both on server and locally, local has precedence: do nothing locally
192+
if (concurrentUpdate && targetLocation === this.masterLocation) {
193+
// Updated both on target and sourcely, source has precedence: do nothing sourcely
194+
return
195+
}
196+
const concurrentRemoval = targetRemovals.find(a =>
197+
a.payload.findItem(action.payload.type, Mappings.mapId(mappingsSnapshot, action.payload, a.payload.location)) ||
198+
a.payload.findItem(ItemType.FOLDER, Mappings.mapParentId(mappingsSnapshot, action.payload, a.payload.location)))
199+
if (concurrentRemoval) {
200+
// Already deleted on target, do nothing.
167201
return
168202
}
169203

170204
targetPlan.UPDATE.commit(action)
171-
}, ACTION_CONCURRENCY)
205+
})
172206

173-
return targetPlan
174-
}
207+
await Parallel.each(sourceScanResult.REORDER.getActions(), async(action) => {
208+
if (avoidTargetReorders[action.payload.id]) {
209+
return
210+
}
211+
212+
if (targetLocation !== this.masterLocation) {
213+
const concurrentReorder = targetReorders.find(a =>
214+
action.payload.type === a.payload.type && Mappings.mappable(mappingsSnapshot, action.payload, a.payload))
215+
if (concurrentReorder) {
216+
return
217+
}
218+
}
175219

176-
reconcileReorderings<L1 extends TItemLocation, L2 extends TItemLocation>(
177-
targetReorders: Diff<L2, TItemLocation, ReorderAction<L2, TItemLocation>>,
178-
targetOrSourceDonePlan: PlanStage3<TItemLocation, TItemLocation, TItemLocation>,
179-
targetLocation: L1,
180-
mappingSnapshot: MappingSnapshot
181-
) : Diff<L2, TItemLocation, ReorderAction<L2, TItemLocation>> {
182-
return super.reconcileReorderings(targetReorders, targetOrSourceDonePlan, targetLocation, mappingSnapshot)
220+
const findChainCache = {}
221+
const concurrentRemoval = targetRemovals.find(targetRemoval =>
222+
Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, sourceTree, action.payload, targetRemoval, findChainCache)
223+
)
224+
if (concurrentRemoval) {
225+
// Already deleted on target, do nothing.
226+
return
227+
}
228+
229+
targetPlan.REORDER.commit(action)
230+
})
231+
232+
return targetPlan
183233
}
184234

185235
async loadChildren(serverTreeRoot: Folder<typeof ItemLocation.SERVER>):Promise<void> {

0 commit comments

Comments
 (0)