mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
Handle sync conflicts
This commit is contained in:
parent
baa1a01ed0
commit
cb7067ddf4
@ -48,83 +48,197 @@ 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(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();
|
||||||
|
|
||||||
switchClient(2);
|
// 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 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 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();
|
||||||
|
|
||||||
switchClient(2);
|
switchClient(2); // ----------------------------------
|
||||||
|
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
|
|
||||||
await sleep(1);
|
await sleep(0.1);
|
||||||
|
|
||||||
let note2 = await Note.load(note1.id);
|
let folder1_modRemote = await Folder.load(folder1.id);
|
||||||
note2.title = "Updated on client 2";
|
folder1_modRemote.title = "folder1 UPDATE CLIENT 2";
|
||||||
await Note.save(note2);
|
await Folder.save(folder1_modRemote);
|
||||||
|
folder1_modRemote = await Folder.load(folder1_modRemote.id);
|
||||||
note2 = await Note.load(note2.id);
|
|
||||||
|
|
||||||
await synchronizer().start();
|
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();
|
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);
|
// let conflictFolder = await Folder.conflictFolder();
|
||||||
expect(note1.title).toBe(note2.title);
|
// let conflictedNotes = await Note.all(conflictFolder.id);
|
||||||
expect(note1.body).toBe(note2.body);
|
|
||||||
|
// 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();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
@ -19,7 +19,7 @@ function sleep(n) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
}, n * 1000);
|
}, Math.round(n * 1000));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
require('babel-plugin-transform-runtime');
|
require('babel-plugin-transform-runtime');
|
||||||
|
|
||||||
import { BaseItem } from 'src/models/base-item.js';
|
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 { BaseModel } from 'src/base-model.js';
|
||||||
import { sprintf } from 'sprintf-js';
|
import { sprintf } from 'sprintf-js';
|
||||||
import { time } from 'src/time-utils.js';
|
import { time } from 'src/time-utils.js';
|
||||||
@ -48,16 +50,25 @@ class Synchronizer {
|
|||||||
if (!remote) {
|
if (!remote) {
|
||||||
action = 'createRemote';
|
action = 'createRemote';
|
||||||
} else {
|
} else {
|
||||||
if (remote.updated_time > local.updated_time && local.type_ == BaseModel.ITEM_TYPE_NOTE) {
|
if (remote.updated_time > local.sync_time) {
|
||||||
action = 'noteConflict';
|
// 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 {
|
} else {
|
||||||
action = 'updateRemote';
|
action = 'updateRemote';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info('Sync action (1): ' + action);
|
||||||
|
|
||||||
if (action == 'createRemote' || action == 'updateRemote') {
|
if (action == 'createRemote' || action == 'updateRemote') {
|
||||||
await this.api().put(path, content);
|
await this.api().put(path, content);
|
||||||
await this.api().setTimestamp(path, local.updated_time);
|
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') {
|
} else if (action == 'noteConflict') {
|
||||||
// - Create a duplicate of local note into Conflicts folder (to preserve the user's changes)
|
// - Create a duplicate of local note into Conflicts folder (to preserve the user's changes)
|
||||||
// - Overwrite local note with remote note
|
// - Overwrite local note with remote note
|
||||||
@ -65,7 +76,7 @@ class Synchronizer {
|
|||||||
let conflictedNote = Object.assign({}, local);
|
let conflictedNote = Object.assign({}, local);
|
||||||
delete conflictedNote.id;
|
delete conflictedNote.id;
|
||||||
conflictedNote.parent_id = conflictFolder.id;
|
conflictedNote.parent_id = conflictFolder.id;
|
||||||
await Note.save(conflictedNote);
|
await Note.save(conflictedNote, { autoTimestamp: false });
|
||||||
|
|
||||||
let remoteContent = await this.api().get(path);
|
let remoteContent = await this.api().get(path);
|
||||||
local = BaseItem.unserialize(remoteContent);
|
local = BaseItem.unserialize(remoteContent);
|
||||||
@ -114,6 +125,8 @@ class Synchronizer {
|
|||||||
|
|
||||||
if (!action) continue;
|
if (!action) continue;
|
||||||
|
|
||||||
|
console.info('Sync action (2): ' + action);
|
||||||
|
|
||||||
if (action == 'createLocal' || action == 'updateLocal') {
|
if (action == 'createLocal' || action == 'updateLocal') {
|
||||||
let content = await this.api().get(path);
|
let content = await this.api().get(path);
|
||||||
if (!content) {
|
if (!content) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
let time = {
|
let time = {
|
||||||
|
|
||||||
unix() {
|
unix() {
|
||||||
return Math.round((new Date()).getTime() / 1000);
|
return Math.floor((new Date()).getTime() / 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
unixMs() {
|
unixMs() {
|
||||||
@ -9,7 +9,7 @@ let time = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
unixMsToS(ms) {
|
unixMsToS(ms) {
|
||||||
return Math.round(ms / 1000);
|
return Math.floor(ms / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user