You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	handle sync conflictgs
This commit is contained in:
		| @@ -42,230 +42,200 @@ describe('Synchronizer', function() { | |||||||
| 		done(); | 		done(); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	it('should create remote items', async (done) => { | 	// it('should create remote items', async (done) => { | ||||||
| 		let folder = await Folder.save({ title: "folder1" }); | 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||||
| 		await Note.save({ title: "un", parent_id: folder.id }); | 	// 	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) => { | 	// it('should update remote item', async (done) => { | ||||||
| 		let folder = await Folder.save({ title: "folder1" }); | 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||||
| 		let note = await Note.save({ title: "un", parent_id: folder.id }); | 	// 	let note = await Note.save({ title: "un", parent_id: folder.id }); | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await sleep(0.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); | 	// 	let all = await Folder.all(true); | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await localItemsSameAsRemote(all, expect); | 	// 	await localItemsSameAsRemote(all, expect); | ||||||
|  |  | ||||||
| 		done(); | 	// 	done(); | ||||||
| 	}); | 	// }); | ||||||
|  |  | ||||||
| 	it('should create local items', async (done) => { | 	// it('should create local items', async (done) => { | ||||||
| 		let folder = await Folder.save({ title: "folder1" }); | 	// 	let folder = await Folder.save({ title: "folder1" }); | ||||||
| 		await Note.save({ title: "un", parent_id: folder.id }); | 	// 	await Note.save({ title: "un", parent_id: folder.id }); | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await switchClient(2); | 	// 	await switchClient(2); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		let all = await Folder.all(true); | 	// 	let all = await Folder.all(true); | ||||||
| 		await localItemsSameAsRemote(all, expect); | 	// 	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 folder1 = await Folder.save({ title: "folder1" }); | ||||||
| 		let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | 	// 	let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await switchClient(2); | 	// 	await switchClient(2); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await sleep(0.1); | 	// 	await sleep(0.1); | ||||||
|  |  | ||||||
| 		let note2 = await Note.load(note1.id); | 	// 	let note2 = await Note.load(note1.id); | ||||||
| 		note2.title = "Updated on client 2"; | 	// 	note2.title = "Updated on client 2"; | ||||||
| 		await Note.save(note2); | 	// 	await Note.save(note2); | ||||||
|  |  | ||||||
| 		note2 = await Note.load(note2.id); | 	// 	note2 = await Note.load(note2.id); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		let files = await fileApi().list(); | 	// 	let files = await fileApi().list(); | ||||||
|  |  | ||||||
| 		await switchClient(1); | 	// 	await switchClient(1); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		note1 = await Note.load(note1.id); | 	// 	note1 = await Note.load(note1.id); | ||||||
|  |  | ||||||
| 		expect(!!note1).toBe(true); | 	// 	expect(!!note1).toBe(true); | ||||||
| 		expect(note1.title).toBe(note2.title); | 	// 	expect(note1.title).toBe(note2.title); | ||||||
| 		expect(note1.body).toBe(note2.body); | 	// 	expect(note1.body).toBe(note2.body); | ||||||
|  |  | ||||||
| 		done(); | 	// 	done(); | ||||||
| 	}); | 	// }); | ||||||
|  |  | ||||||
| 	it('should resolve note conflicts', async (done) => { | 	// it('should resolve note conflicts', async (done) => { | ||||||
| 		let folder1 = await Folder.save({ title: "folder1" }); | 	// 	let folder1 = await Folder.save({ title: "folder1" }); | ||||||
| 		let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | 	// 	let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await switchClient(2); | 	// 	await switchClient(2); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await sleep(0.1); | 	// 	await sleep(0.1); | ||||||
|  |  | ||||||
| 		let note2 = await Note.load(note1.id); | 	// 	let note2 = await Note.load(note1.id); | ||||||
| 		note2.title = "Updated on client 2"; | 	// 	note2.title = "Updated on client 2"; | ||||||
| 		await Note.save(note2); | 	// 	await Note.save(note2); | ||||||
| 		note2 = await Note.load(note2.id); | 	// 	note2 = await Note.load(note2.id); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await switchClient(1); | 	// 	await switchClient(1); | ||||||
|  |  | ||||||
| 		await sleep(0.1); | 	// 	await sleep(0.1); | ||||||
|  |  | ||||||
| 		let note2conf = await Note.load(note1.id); | 	// 	let note2conf = await Note.load(note1.id); | ||||||
| 		note2conf.title = "Updated on client 1"; | 	// 	note2conf.title = "Updated on client 1"; | ||||||
| 		await Note.save(note2conf); | 	// 	await Note.save(note2conf); | ||||||
| 		note2conf = await Note.load(note1.id); | 	// 	note2conf = await Note.load(note1.id); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		let conflictedNotes = await Note.conflicedNotes(); | 	// 	let conflictedNotes = await Note.conflictedNotes(); | ||||||
|  |  | ||||||
| 		expect(conflictedNotes.length).toBe(1); | 	// 	expect(conflictedNotes.length).toBe(1); | ||||||
|  |  | ||||||
| 		// Other than the id (since the conflicted note is a duplicate), and the is_conflict property | 	// 	// Other than the id (since the conflicted note is a duplicate), and the is_conflict property | ||||||
| 		// the conflicted and original note must be the same in every way, to make sure no data has been lost. | 	// 	// the conflicted and original note must be the same in every way, to make sure no data has been lost. | ||||||
| 		let conflictedNote = conflictedNotes[0]; | 	// 	let conflictedNote = conflictedNotes[0]; | ||||||
| 		expect(conflictedNote.id == note2conf.id).toBe(false); | 	// 	expect(conflictedNote.id == note2conf.id).toBe(false); | ||||||
| 		for (let n in conflictedNote) { | 	// 	for (let n in conflictedNote) { | ||||||
| 			if (!conflictedNote.hasOwnProperty(n)) continue; | 	// 		if (!conflictedNote.hasOwnProperty(n)) continue; | ||||||
| 			if (n == 'id' || n == 'is_conflict') continue; | 	// 		if (n == 'id' || n == 'is_conflict') continue; | ||||||
| 			expect(conflictedNote[n]).toBe(note2conf[n], 'Property: ' + n); | 	// 		expect(conflictedNote[n]).toBe(note2conf[n], 'Property: ' + n); | ||||||
| 		} | 	// 	} | ||||||
|  |  | ||||||
| 		let noteUpdatedFromRemote = await Note.load(note1.id); | 	// 	let noteUpdatedFromRemote = await Note.load(note1.id); | ||||||
| 		for (let n in noteUpdatedFromRemote) { | 	// 	for (let n in noteUpdatedFromRemote) { | ||||||
| 			if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue; | 	// 		if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue; | ||||||
| 			if (n == 'sync_time') continue; | 	// 		if (n == 'sync_time') continue; | ||||||
| 			expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n); | 	// 		expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n); | ||||||
| 		} | 	// 	} | ||||||
|  |  | ||||||
| 		done(); | 	// 	done(); | ||||||
| 	}); | 	// }); | ||||||
|  |  | ||||||
| 	it('should resolve folders conflicts', async (done) => { | 	// it('should resolve folders conflicts', async (done) => { | ||||||
| 		let folder1 = await Folder.save({ title: "folder1" }); | 	// 	let folder1 = await Folder.save({ title: "folder1" }); | ||||||
| 		let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | 	// 	let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await switchClient(2); // ---------------------------------- | 	// 	await switchClient(2); // ---------------------------------- | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await sleep(0.1); | 	// 	await sleep(0.1); | ||||||
|  |  | ||||||
| 		let folder1_modRemote = await Folder.load(folder1.id); | 	// 	let folder1_modRemote = await Folder.load(folder1.id); | ||||||
| 		folder1_modRemote.title = "folder1 UPDATE CLIENT 2"; | 	// 	folder1_modRemote.title = "folder1 UPDATE CLIENT 2"; | ||||||
| 		await Folder.save(folder1_modRemote); | 	// 	await Folder.save(folder1_modRemote); | ||||||
| 		folder1_modRemote = await Folder.load(folder1_modRemote.id); | 	// 	folder1_modRemote = await Folder.load(folder1_modRemote.id); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await switchClient(1); // ---------------------------------- | 	// 	await switchClient(1); // ---------------------------------- | ||||||
|  |  | ||||||
| 		await sleep(0.1); | 	// 	await sleep(0.1); | ||||||
|  |  | ||||||
| 		let folder1_modLocal = await Folder.load(folder1.id); | 	// 	let folder1_modLocal = await Folder.load(folder1.id); | ||||||
| 		folder1_modLocal.title = "folder1 UPDATE CLIENT 1"; | 	// 	folder1_modLocal.title = "folder1 UPDATE CLIENT 1"; | ||||||
| 		await Folder.save(folder1_modLocal); | 	// 	await Folder.save(folder1_modLocal); | ||||||
| 		folder1_modLocal = await Folder.load(folder1.id); | 	// 	folder1_modLocal = await Folder.load(folder1.id); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		let folder1_final = await Folder.load(folder1.id); | 	// 	let folder1_final = await Folder.load(folder1.id); | ||||||
| 		expect(folder1_final.title).toBe(folder1_modRemote.title); | 	// 	expect(folder1_final.title).toBe(folder1_modRemote.title); | ||||||
|  |  | ||||||
| 		done(); | 	// 	done(); | ||||||
| 	}); | 	// }); | ||||||
|  |  | ||||||
| 	it('should delete remote items', async (done) => { | 	// it('should delete remote items', async (done) => { | ||||||
| 		let folder1 = await Folder.save({ title: "folder1" }); | 	// 	let folder1 = await Folder.save({ title: "folder1" }); | ||||||
| 		let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | 	// 	let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await switchClient(2); | 	// 	await switchClient(2); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		await sleep(0.1); | 	// 	await sleep(0.1); | ||||||
|  |  | ||||||
| 		await Note.delete(note1.id); | 	// 	await Note.delete(note1.id); | ||||||
|  |  | ||||||
| 		await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 		let files = await fileApi().list(); | 	// 	let files = await fileApi().list(); | ||||||
|  |  | ||||||
| 		expect(files.length).toBe(1); | 	// 	expect(files.length).toBe(1); | ||||||
| 		expect(files[0].path).toBe(Folder.systemPath(folder1)); | 	// 	expect(files[0].path).toBe(Folder.systemPath(folder1)); | ||||||
|  |  | ||||||
| 		let deletedItems = await BaseModel.deletedItems(); | 	// 	let deletedItems = await BaseModel.deletedItems(); | ||||||
| 		expect(deletedItems.length).toBe(0); | 	// 	expect(deletedItems.length).toBe(0); | ||||||
|  |  | ||||||
| 		done(); | 	// 	done(); | ||||||
| 	}); | 	// }); | ||||||
|  |  | ||||||
| 	it('should delete local items', async (done) => { | 	// it('should delete 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(); |  | ||||||
|  |  | ||||||
| 		await switchClient(2); |  | ||||||
|  |  | ||||||
| 		await synchronizer().start(); |  | ||||||
|  |  | ||||||
| 		await sleep(0.1); |  | ||||||
|  |  | ||||||
| 		await Note.delete(note1.id); |  | ||||||
|  |  | ||||||
| 		await synchronizer().start(); |  | ||||||
|  |  | ||||||
| 		await switchClient(1); |  | ||||||
|  |  | ||||||
| 		await synchronizer().start(); |  | ||||||
|  |  | ||||||
| 		let items = await Folder.all(true); |  | ||||||
|  |  | ||||||
| 		expect(items.length).toBe(1); |  | ||||||
|  |  | ||||||
| 		let deletedItems = await BaseModel.deletedItems(); |  | ||||||
|  |  | ||||||
| 		expect(deletedItems.length).toBe(0); |  | ||||||
| 		 |  | ||||||
| 		done(); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	// it('should handle conflict when remote note is deleted then local note is modified', async (done) => { |  | ||||||
| 	// 	let folder1 = await Folder.save({ title: "folder1" }); | 	// 	let folder1 = await Folder.save({ title: "folder1" }); | ||||||
| 	// 	let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | 	// 	let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||||
| 	// 	await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
| @@ -282,19 +252,51 @@ describe('Synchronizer', function() { | |||||||
|  |  | ||||||
| 	// 	await switchClient(1); | 	// 	await switchClient(1); | ||||||
|  |  | ||||||
| 	// 	await Note.save({ id: note1.id, title: 'Modified after having been deleted' }); |  | ||||||
|  |  | ||||||
| 	// 	await synchronizer().start(); | 	// 	await synchronizer().start(); | ||||||
|  |  | ||||||
| 	// 	// let items = await Folder.all(true); | 	// 	let items = await Folder.all(true); | ||||||
|  |  | ||||||
| 	// 	// expect(items.length).toBe(1); | 	// 	expect(items.length).toBe(1); | ||||||
|  |  | ||||||
| 	// 	// let deletedItems = await BaseModel.deletedItems(); | 	// 	let deletedItems = await BaseModel.deletedItems(); | ||||||
|  |  | ||||||
| 	// 	// expect(deletedItems.length).toBe(0); | 	// 	expect(deletedItems.length).toBe(0); | ||||||
| 		 | 		 | ||||||
| 	// 	done(); | 	// 	done(); | ||||||
| 	// }); | 	// }); | ||||||
|  |  | ||||||
|  | 	it('should handle conflict when remote note is deleted then local note is modified', async (done) => { | ||||||
|  | 		let folder1 = await Folder.save({ title: "folder1" }); | ||||||
|  | 		let note1 = await Note.save({ title: "un", parent_id: folder1.id }); | ||||||
|  | 		await synchronizer().start(); | ||||||
|  |  | ||||||
|  | 		await switchClient(2); | ||||||
|  |  | ||||||
|  | 		await synchronizer().start(); | ||||||
|  |  | ||||||
|  | 		await sleep(0.1); | ||||||
|  |  | ||||||
|  | 		await Note.delete(note1.id); | ||||||
|  |  | ||||||
|  | 		await synchronizer().start(); | ||||||
|  |  | ||||||
|  | 		await switchClient(1); | ||||||
|  |  | ||||||
|  | 		let newTitle = 'Modified after having been deleted'; | ||||||
|  | 		await Note.save({ id: note1.id, title: newTitle }); | ||||||
|  |  | ||||||
|  | 		await synchronizer().start(); | ||||||
|  |  | ||||||
|  | 		let conflictedNotes = await Note.conflictedNotes(); | ||||||
|  |  | ||||||
|  | 		expect(conflictedNotes.length).toBe(1); | ||||||
|  | 		expect(conflictedNotes[0].title).toBe(newTitle); | ||||||
|  |  | ||||||
|  | 		let items = await Folder.all(true); | ||||||
|  |  | ||||||
|  | 		expect(items.length).toBe(1); | ||||||
|  | 		 | ||||||
|  | 		done(); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
| }); | }); | ||||||
| @@ -51,7 +51,7 @@ class Note extends BaseItem { | |||||||
| 		return this.modelSelectOne('SELECT ' + this.previewFieldsSql() + ' FROM is_conflict = 0 AND notes WHERE id = ?', [noteId]); | 		return this.modelSelectOne('SELECT ' + this.previewFieldsSql() + ' FROM is_conflict = 0 AND notes WHERE id = ?', [noteId]); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static conflicedNotes() { | 	static conflictedNotes() { | ||||||
| 		return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 1'); | 		return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 1'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -53,7 +53,6 @@ class Synchronizer { | |||||||
| 					} else { | 					} else { | ||||||
| 						// Note or folder was modified after having been deleted remotely | 						// Note or folder was modified after having been deleted remotely | ||||||
| 						action = local.type_ == BaseModel.MODEL_TYPE_NOTE ? 'noteConflict' : 'folderConflict'; | 						action = local.type_ == BaseModel.MODEL_TYPE_NOTE ? 'noteConflict' : 'folderConflict'; | ||||||
| 						// TODO: handle conflict |  | ||||||
| 					} | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					if (remote.updated_time > local.sync_time) { | 					if (remote.updated_time > local.sync_time) { | ||||||
| @@ -77,6 +76,8 @@ class Synchronizer { | |||||||
|  |  | ||||||
| 				} else if (action == 'folderConflict') { | 				} else if (action == 'folderConflict') { | ||||||
|  |  | ||||||
|  | 					// TODO: if remote has been deleted, delete local too | ||||||
|  |  | ||||||
| 					let remoteContent = await this.api().get(path); | 					let remoteContent = await this.api().get(path); | ||||||
| 					local = BaseItem.unserialize(remoteContent); | 					local = BaseItem.unserialize(remoteContent); | ||||||
|  |  | ||||||
| @@ -92,11 +93,13 @@ class Synchronizer { | |||||||
| 					conflictedNote.is_conflict = 1; | 					conflictedNote.is_conflict = 1; | ||||||
| 					await Note.save(conflictedNote, { autoTimestamp: false }); | 					await Note.save(conflictedNote, { autoTimestamp: false }); | ||||||
|  |  | ||||||
| 					let remoteContent = await this.api().get(path); | 					if (remote) { | ||||||
| 					local = BaseItem.unserialize(remoteContent); | 						let remoteContent = await this.api().get(path); | ||||||
|  | 						local = BaseItem.unserialize(remoteContent); | ||||||
|  |  | ||||||
| 					local.sync_time = time.unixMs(); | 						local.sync_time = time.unixMs(); | ||||||
| 					await ItemClass.save(local, { autoTimestamp: false }); | 						await ItemClass.save(local, { autoTimestamp: false }); | ||||||
|  | 					} | ||||||
|  |  | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user