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 };
|