1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-02-16 19:47:40 +02:00
joplin/ReactNativeClient/src/synchronizer.js

181 lines
4.8 KiB
JavaScript
Raw Normal View History

2017-05-18 19:58:01 +00:00
import { Log } from 'src/log.js';
import { Setting } from 'src/models/setting.js';
import { Change } from 'src/models/change.js';
import { Folder } from 'src/models/folder.js';
2017-05-20 00:16:50 +02:00
import { Note } from 'src/models/note.js';
import { BaseModel } from 'src/base-model.js';
2017-05-19 19:12:09 +00:00
import { promiseChain } from 'src/promise-chain.js';
2017-05-18 19:58:01 +00:00
class Synchronizer {
2017-05-19 19:12:09 +00:00
constructor(db, api) {
2017-05-18 19:58:01 +00:00
this.state_ = 'idle';
2017-05-19 19:12:09 +00:00
this.db_ = db;
this.api_ = api;
2017-05-18 19:58:01 +00:00
}
state() {
return this.state_;
}
db() {
2017-05-19 19:12:09 +00:00
return this.db_;
2017-05-18 19:58:01 +00:00
}
api() {
2017-05-19 19:12:09 +00:00
return this.api_;
2017-05-18 19:58:01 +00:00
}
2017-06-03 17:20:17 +01:00
processState_uploadChanges() {
Change.all().then((changes) => {
let mergedChanges = Change.mergeChanges(changes);
let chain = [];
let processedChangeIds = [];
for (let i = 0; i < mergedChanges.length; i++) {
let c = mergedChanges[i];
chain.push(() => {
let p = null;
2017-05-20 00:16:50 +02:00
let ItemClass = null;
2017-06-03 17:20:17 +01:00
let path = null;
if (c.item_type == BaseModel.ITEM_TYPE_FOLDER) {
2017-05-20 00:16:50 +02:00
ItemClass = Folder;
2017-06-03 17:20:17 +01:00
path = 'folders';
} else if (c.item_type == BaseModel.ITEM_TYPE_NOTE) {
2017-05-20 00:16:50 +02:00
ItemClass = Note;
2017-06-03 17:20:17 +01:00
path = 'notes';
2017-05-20 00:16:50 +02:00
}
2017-05-19 19:12:09 +00:00
2017-06-03 17:20:17 +01:00
if (c.type == Change.TYPE_NOOP) {
p = Promise.resolve();
} else if (c.type == Change.TYPE_CREATE) {
p = ItemClass.load(c.item_id).then((item) => {
return this.api().put(path + '/' + item.id, null, item);
2017-05-20 00:16:50 +02:00
});
2017-06-03 17:20:17 +01:00
} else if (c.type == Change.TYPE_UPDATE) {
p = ItemClass.load(c.item_id).then((item) => {
return this.api().patch(path + '/' + item.id, null, item);
2017-05-20 00:16:50 +02:00
});
2017-06-03 17:20:17 +01:00
} else if (c.type == Change.TYPE_DELETE) {
p = this.api().delete(path + '/' + c.item_id);
2017-05-20 00:16:50 +02:00
}
2017-05-19 19:32:49 +00:00
2017-06-03 17:20:17 +01:00
return p.then(() => {
processedChangeIds = processedChangeIds.concat(c.ids);
}).catch((error) => {
Log.warn('Failed applying changes', c.ids, error.message, error.type);
// This is fine - trying to apply changes to an object that has been deleted
if (error.type == 'NotFoundException') {
processedChangeIds = processedChangeIds.concat(c.ids);
} else {
throw error;
}
});
});
}
return promiseChain(chain).catch((error) => {
Log.warn('Synchronization was interrupted due to an error:', error);
2017-05-19 19:12:09 +00:00
}).then(() => {
2017-06-03 17:20:17 +01:00
Log.info('IDs to delete: ', processedChangeIds);
Change.deleteMultiple(processedChangeIds);
2017-05-19 19:12:09 +00:00
});
2017-06-03 17:20:17 +01:00
}).then(() => {
this.processState('downloadChanges');
});
}
2017-05-20 00:16:50 +02:00
2017-06-03 17:20:17 +01:00
processState_downloadChanges() {
let maxRevId = null;
let hasMore = false;
2017-06-03 23:33:46 +01:00
this.api().get('synchronizer', { rev_id: Setting.value('sync.lastRevId') }).then((syncOperations) => {
2017-06-03 17:20:17 +01:00
hasMore = syncOperations.has_more;
let chain = [];
for (let i = 0; i < syncOperations.items.length; i++) {
let syncOp = syncOperations.items[i];
if (syncOp.id > maxRevId) maxRevId = syncOp.id;
let ItemClass = null;
if (syncOp.item_type == 'folder') {
ItemClass = Folder;
} else if (syncOp.item_type == 'note') {
ItemClass = Note;
}
2017-05-19 19:12:09 +00:00
2017-06-03 17:20:17 +01:00
if (syncOp.type == 'create') {
chain.push(() => {
let item = ItemClass.fromApiResult(syncOp.item);
// TODO: automatically handle NULL fields by checking type and default value of field
if ('parent_id' in item && !item.parent_id) item.parent_id = '';
return ItemClass.save(item, { isNew: true, trackChanges: false });
});
}
if (syncOp.type == 'update') {
chain.push(() => {
return ItemClass.load(syncOp.item_id).then((item) => {
if (!item) return;
item = ItemClass.applyPatch(item, syncOp.item);
return ItemClass.save(item, { trackChanges: false });
2017-05-19 19:12:09 +00:00
});
});
}
2017-06-03 17:20:17 +01:00
if (syncOp.type == 'delete') {
chain.push(() => {
return ItemClass.delete(syncOp.item_id, { trackChanges: false });
});
}
}
return promiseChain(chain);
}).then(() => {
Log.info('All items synced. has_more = ', hasMore);
if (maxRevId) {
Setting.setValue('sync.lastRevId', maxRevId);
return Setting.saveAll();
}
}).then(() => {
if (hasMore) {
this.processState('downloadChanges');
} else {
2017-05-23 19:58:12 +00:00
this.processState('idle');
2017-06-03 17:20:17 +01:00
}
}).catch((error) => {
Log.warn('Sync error', error);
});
}
processState(state) {
Log.info('Sync: processing: ' + state);
this.state_ = state;
if (state == 'uploadChanges') {
processState_uploadChanges();
} else if (state == 'downloadChanges') {
processState_downloadChanges();
} else if (state == 'idle') {
// Nothing
} else {
throw new Error('Invalid state: ' . state);
2017-05-18 19:58:01 +00:00
}
}
start() {
2017-05-19 19:12:09 +00:00
Log.info('Sync: start');
2017-05-18 19:58:01 +00:00
if (this.state() != 'idle') {
Log.info("Sync: cannot start synchronizer because synchronization already in progress. State: " + this.state());
return;
}
2017-05-19 19:12:09 +00:00
if (!this.api().session()) {
Log.info("Sync: cannot start synchronizer because user is not logged in.");
return;
}
2017-05-18 19:58:01 +00:00
2017-06-03 17:20:17 +01:00
this.processState('uploadChanges');
2017-05-18 19:58:01 +00:00
}
}
export { Synchronizer };