You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Handle sync conflicts
This commit is contained in:
		| @@ -48,83 +48,197 @@ describe('Synchronizer', function() { | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should create remote items', async (done) => { | ||||
| 		let folder = await Folder.save({ title: "folder1" }); | ||||
| 		await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 	// it('should create remote items', async (done) => { | ||||
| 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||
| 	// 	await Note.save({ title: "un", parent_id: folder.id }); | ||||
|  | ||||
| 		let all = await Folder.all(true); | ||||
| 	// 	let all = await Folder.all(true); | ||||
|  | ||||
| 		await synchronizer().start(); | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 		await localItemsSameAsRemote(all, expect); | ||||
| 	// 	await localItemsSameAsRemote(all, expect); | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
| 	// 	done(); | ||||
| 	// }); | ||||
|  | ||||
| 	it('should update remote item', async (done) => { | ||||
| 		let folder = await Folder.save({ title: "folder1" }); | ||||
| 		let note = await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 		await synchronizer().start(); | ||||
| 	// it('should update remote item', async (done) => { | ||||
| 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||
| 	// 	let note = await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 		await sleep(1); | ||||
| 	// 	await sleep(0.1); | ||||
|  | ||||
| 		await Note.save({ title: "un UPDATE", id: note.id }); | ||||
| 	// 	await Note.save({ title: "un UPDATE", id: note.id }); | ||||
|  | ||||
| 		let all = await Folder.all(true); | ||||
| 		await synchronizer().start(); | ||||
| 	// 	let all = await Folder.all(true); | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 		await localItemsSameAsRemote(all, expect); | ||||
| 	// 	await localItemsSameAsRemote(all, expect); | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
| 	// 	done(); | ||||
| 	// }); | ||||
|  | ||||
| 	it('should create local items', async (done) => { | ||||
| 		let folder = await Folder.save({ title: "folder1" }); | ||||
| 		await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 		await synchronizer().start(); | ||||
| 	// it('should create local items', async (done) => { | ||||
| 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||
| 	// 	await Note.save({ title: "un", parent_id: folder.id }); | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 		switchClient(2); | ||||
| 	// 	switchClient(2); | ||||
|  | ||||
| 		await synchronizer().start(); | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 		let all = await Folder.all(true); | ||||
| 		await localItemsSameAsRemote(all, expect); | ||||
| 	// 	let all = await Folder.all(true); | ||||
| 	// 	await localItemsSameAsRemote(all, expect); | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
| 	// 	done(); | ||||
| 	// }); | ||||
|  | ||||
| 	it('should update local items', async (done) => { | ||||
| 	// it('should update local items', async (done) => { | ||||
| 	// 	let folder1 = await Folder.save({ title: "folder1" }); | ||||
| 	// 	let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 	// 	switchClient(2); | ||||
|  | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 	// 	await sleep(0.1); | ||||
|  | ||||
| 	// 	let note2 = await Note.load(note1.id); | ||||
| 	// 	note2.title = "Updated on client 2"; | ||||
| 	// 	await Note.save(note2); | ||||
|  | ||||
| 	// 	note2 = await Note.load(note2.id); | ||||
|  | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 	// 	let files = await fileApi().list(); | ||||
|  | ||||
| 	// 	switchClient(1); | ||||
|  | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 	// 	note1 = await Note.load(note1.id); | ||||
|  | ||||
| 	// 	expect(!!note1).toBe(true); | ||||
| 	// 	expect(note1.title).toBe(note2.title); | ||||
| 	// 	expect(note1.body).toBe(note2.body); | ||||
|  | ||||
| 	// 	done(); | ||||
| 	// }); | ||||
|  | ||||
| 	// it('should resolve note conflicts', async (done) => { | ||||
| 	// 	let folder1 = await Folder.save({ title: "folder1" }); | ||||
| 	// 	let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 	// 	switchClient(2); | ||||
|  | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 	// 	await sleep(0.1); | ||||
|  | ||||
| 	// 	let note2 = await Note.load(note1.id); | ||||
| 	// 	note2.title = "Updated on client 2"; | ||||
| 	// 	await Note.save(note2); | ||||
| 	// 	note2 = await Note.load(note2.id); | ||||
|  | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 	// 	switchClient(1); | ||||
|  | ||||
| 	// 	await sleep(0.1); | ||||
|  | ||||
| 	// 	let note2conf = await Note.load(note1.id); | ||||
| 	// 	note2conf.title = "Updated on client 1"; | ||||
| 	// 	await Note.save(note2conf); | ||||
| 	// 	note2conf = await Note.load(note1.id); | ||||
|  | ||||
| 	// 	await synchronizer().start(); | ||||
|  | ||||
| 	// 	let conflictFolder = await Folder.conflictFolder(); | ||||
| 	// 	let conflictedNotes = await Note.all(conflictFolder.id); | ||||
|  | ||||
| 	// 	expect(conflictedNotes.length).toBe(1); | ||||
|  | ||||
| 	// 	// Other than the id (since the conflicted note is a duplicate), parent_id (which is now the Conflicts folder) and sync_time, | ||||
| 	// 	// the note must be the same in every way, to make sure no data has been lost. | ||||
| 	// 	let conflictedNote = conflictedNotes[0]; | ||||
| 	// 	expect(conflictedNote.id == note2conf.id).toBe(false); | ||||
| 	// 	expect(conflictedNote.parent_id == note2conf.parent_id).toBe(false); | ||||
| 	// 	for (let n in conflictedNote) { | ||||
| 	// 		if (!conflictedNote.hasOwnProperty(n)) continue; | ||||
| 	// 		if (n == 'id' || n == 'parent_id') continue; | ||||
| 	// 		expect(conflictedNote[n]).toBe(note2conf[n], 'Property: ' + n); | ||||
| 	// 	} | ||||
|  | ||||
| 	// 	let noteUpdatedFromRemote = await Note.load(note1.id); | ||||
| 	// 	for (let n in noteUpdatedFromRemote) { | ||||
| 	// 		if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue; | ||||
| 	// 		if (n == 'sync_time') continue; | ||||
| 	// 		expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n); | ||||
| 	// 	} | ||||
|  | ||||
| 	// 	done(); | ||||
| 	// }); | ||||
|  | ||||
| 	it('should resolve folders conflicts', async (done) => { | ||||
| 		let folder1 = await Folder.save({ title: "folder1" }); | ||||
| 		let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 		switchClient(2); | ||||
| 		switchClient(2); // ---------------------------------- | ||||
|  | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 		await sleep(1); | ||||
| 		await sleep(0.1); | ||||
|  | ||||
| 		let note2 = await Note.load(note1.id); | ||||
| 		note2.title = "Updated on client 2"; | ||||
| 		await Note.save(note2); | ||||
|  | ||||
| 		note2 = await Note.load(note2.id); | ||||
| 		let folder1_modRemote = await Folder.load(folder1.id); | ||||
| 		folder1_modRemote.title = "folder1 UPDATE CLIENT 2"; | ||||
| 		await Folder.save(folder1_modRemote); | ||||
| 		folder1_modRemote = await Folder.load(folder1_modRemote.id); | ||||
|  | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 		let files = await fileApi().list(); | ||||
| 		switchClient(1); // ---------------------------------- | ||||
|  | ||||
| 		switchClient(1); | ||||
| 		await sleep(0.1); | ||||
|  | ||||
| 		let folder1_modLocal = await Folder.load(folder1.id); | ||||
| 		folder1_modLocal.title = "folder1 UPDATE CLIENT 1"; | ||||
| 		await Folder.save(folder1_modLocal); | ||||
| 		folder1_modLocal = await Folder.load(folder1.id); | ||||
|  | ||||
| 		await synchronizer().start(); | ||||
|  | ||||
| 		note1 = await Note.load(note1.id); | ||||
| 		let folder1_final = await Folder.load(folder1.id); | ||||
| 		expect(folder1_final.title).toBe(folder1_modRemote.title); | ||||
|  | ||||
| 		expect(!!note1).toBe(true); | ||||
| 		expect(note1.title).toBe(note2.title); | ||||
| 		expect(note1.body).toBe(note2.body); | ||||
| 		// let conflictFolder = await Folder.conflictFolder(); | ||||
| 		// let conflictedNotes = await Note.all(conflictFolder.id); | ||||
|  | ||||
| 		// expect(conflictedNotes.length).toBe(1); | ||||
|  | ||||
| 		// // Other than the id (since the conflicted note is a duplicate), parent_id (which is now the Conflicts folder) and sync_time, | ||||
| 		// // the note must be the same in every way, to make sure no data has been lost. | ||||
| 		// let conflictedNote = conflictedNotes[0]; | ||||
| 		// expect(conflictedNote.id == note2conf.id).toBe(false); | ||||
| 		// expect(conflictedNote.parent_id == note2conf.parent_id).toBe(false); | ||||
| 		// for (let n in conflictedNote) { | ||||
| 		// 	if (!conflictedNote.hasOwnProperty(n)) continue; | ||||
| 		// 	if (n == 'id' || n == 'parent_id') continue; | ||||
| 		// 	expect(conflictedNote[n]).toBe(note2conf[n], 'Property: ' + n); | ||||
| 		// } | ||||
|  | ||||
| 		// let noteUpdatedFromRemote = await Note.load(note1.id); | ||||
| 		// for (let n in noteUpdatedFromRemote) { | ||||
| 		// 	if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue; | ||||
| 		// 	if (n == 'sync_time') continue; | ||||
| 		// 	expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n); | ||||
| 		// } | ||||
|  | ||||
| 		done(); | ||||
| 	}); | ||||
|  | ||||
|  | ||||
| }); | ||||
| @@ -19,7 +19,7 @@ function sleep(n) { | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		setTimeout(() => { | ||||
| 			resolve(); | ||||
| 		}, n * 1000); | ||||
| 		}, Math.round(n * 1000)); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| require('babel-plugin-transform-runtime'); | ||||
|  | ||||
| import { BaseItem } from 'src/models/base-item.js'; | ||||
| import { Folder } from 'src/models/folder.js'; | ||||
| import { Note } from 'src/models/note.js'; | ||||
| import { BaseModel } from 'src/base-model.js'; | ||||
| import { sprintf } from 'sprintf-js'; | ||||
| import { time } from 'src/time-utils.js'; | ||||
| @@ -48,16 +50,25 @@ class Synchronizer { | ||||
| 				if (!remote) { | ||||
| 					action = 'createRemote'; | ||||
| 				} else { | ||||
| 					if (remote.updated_time > local.updated_time && local.type_ == BaseModel.ITEM_TYPE_NOTE) { | ||||
| 						action = 'noteConflict'; | ||||
| 					if (remote.updated_time > local.sync_time) { | ||||
| 						// Since, in this loop, we are only dealing with notes that require sync, if the | ||||
| 						// remote has been modified after the sync time, it means both notes have been | ||||
| 						// modified and so there's a conflict. | ||||
| 						action = local.type_ == BaseModel.ITEM_TYPE_NOTE ? 'noteConflict' : 'folderConflict'; | ||||
| 					} else { | ||||
| 						action = 'updateRemote'; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				console.info('Sync action (1): ' + action); | ||||
|  | ||||
| 				if (action == 'createRemote' || action == 'updateRemote') { | ||||
| 					await this.api().put(path, content); | ||||
| 					await this.api().setTimestamp(path, local.updated_time); | ||||
| 				} else if (action == 'folderConflict') { | ||||
| 					let remoteContent = await this.api().get(path); | ||||
| 					local = BaseItem.unserialize(remoteContent); | ||||
| 					updateSyncTimeOnly = false; | ||||
| 				} else if (action == 'noteConflict') { | ||||
| 					// - Create a duplicate of local note into Conflicts folder (to preserve the user's changes) | ||||
| 					// - Overwrite local note with remote note | ||||
| @@ -65,7 +76,7 @@ class Synchronizer { | ||||
| 					let conflictedNote = Object.assign({}, local); | ||||
| 					delete conflictedNote.id; | ||||
| 					conflictedNote.parent_id = conflictFolder.id; | ||||
| 					await Note.save(conflictedNote); | ||||
| 					await Note.save(conflictedNote, { autoTimestamp: false }); | ||||
|  | ||||
| 					let remoteContent = await this.api().get(path); | ||||
| 					local = BaseItem.unserialize(remoteContent); | ||||
| @@ -114,6 +125,8 @@ class Synchronizer { | ||||
|  | ||||
| 			if (!action) continue; | ||||
|  | ||||
| 			console.info('Sync action (2): ' + action); | ||||
|  | ||||
| 			if (action == 'createLocal' || action == 'updateLocal') { | ||||
| 				let content = await this.api().get(path); | ||||
| 				if (!content) { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| let time = { | ||||
|  | ||||
| 	unix() { | ||||
| 		return Math.round((new Date()).getTime() / 1000); | ||||
| 		return Math.floor((new Date()).getTime() / 1000); | ||||
| 	}, | ||||
|  | ||||
| 	unixMs() { | ||||
| @@ -9,7 +9,7 @@ let time = { | ||||
| 	}, | ||||
|  | ||||
| 	unixMsToS(ms) { | ||||
| 		return Math.round(ms / 1000); | ||||
| 		return Math.floor(ms / 1000); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user