You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-03 23:50:33 +02:00
Synchronizer
This commit is contained in:
@ -58,13 +58,17 @@ class BaseModel {
|
||||
return options;
|
||||
}
|
||||
|
||||
static load(id) {
|
||||
return this.db().selectOne('SELECT * FROM ' + this.tableName() + ' WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
static saveQuery(o, isNew = 'auto') {
|
||||
if (isNew == 'auto') isNew = !o.id;
|
||||
let query = '';
|
||||
let itemId = o.id;
|
||||
|
||||
if (isNew) {
|
||||
if (this.useUuid()) {
|
||||
if (this.useUuid() && !o.id) {
|
||||
o = Object.assign({}, o);
|
||||
itemId = uuid.create();
|
||||
o.id = itemId;
|
||||
@ -96,15 +100,28 @@ class BaseModel {
|
||||
// which are not handled by React Native.
|
||||
const { Change } = require('src/models/change.js');
|
||||
|
||||
let change = Change.newChange();
|
||||
change.type = isNew ? Change.TYPE_CREATE : Change.TYPE_UPDATE;
|
||||
change.item_id = query.id;
|
||||
change.item_type = this.itemType();
|
||||
if (isNew) {
|
||||
let change = Change.newChange();
|
||||
change.type = Change.TYPE_CREATE;
|
||||
change.item_id = query.id;
|
||||
change.item_type = this.itemType();
|
||||
|
||||
let changeQuery = Change.saveQuery(change);
|
||||
tx.executeSql(changeQuery.sql, changeQuery.params);
|
||||
let changeQuery = Change.saveQuery(change);
|
||||
tx.executeSql(changeQuery.sql, changeQuery.params);
|
||||
} else {
|
||||
for (let n in o) {
|
||||
if (!o.hasOwnProperty(n)) continue;
|
||||
|
||||
// TODO: item field for UPDATE
|
||||
let change = Change.newChange();
|
||||
change.type = Change.TYPE_UPDATE;
|
||||
change.item_id = query.id;
|
||||
change.item_type = this.itemType();
|
||||
change.item_field = n;
|
||||
|
||||
let changeQuery = Change.saveQuery(change);
|
||||
tx.executeSql(changeQuery.sql, changeQuery.params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
o = Object.assign({}, o);
|
||||
@ -122,16 +139,16 @@ class BaseModel {
|
||||
}
|
||||
|
||||
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]).then(() => {
|
||||
// if (options.trackChanges && this.trackChanges()) {
|
||||
// const { Change } = require('src/models/change.js');
|
||||
if (options.trackChanges && this.trackChanges()) {
|
||||
const { Change } = require('src/models/change.js');
|
||||
|
||||
// let change = Change.newChange();
|
||||
// change.type = Change.TYPE_DELETE;
|
||||
// change.item_id = id;
|
||||
// change.item_type = this.itemType();
|
||||
let change = Change.newChange();
|
||||
change.type = Change.TYPE_DELETE;
|
||||
change.item_id = id;
|
||||
change.item_type = this.itemType();
|
||||
|
||||
// return Change.save(change);
|
||||
// }
|
||||
return Change.save(change);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ class ItemListComponent extends Component {
|
||||
<TouchableHighlight onPress={onPress} onLongPress={onLongPress}>
|
||||
<View>
|
||||
{ isEditable && <Checkbox label={item.title} ></Checkbox> }
|
||||
{ !isEditable && <Text>{item.title}</Text> }
|
||||
{ !isEditable && <Text>{item.title} [{item.id}]</Text> }
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
|
@ -110,7 +110,7 @@ class Database {
|
||||
}
|
||||
|
||||
open() {
|
||||
this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-11.sqlite' }, (db) => {
|
||||
this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-12.sqlite' }, (db) => {
|
||||
Log.info('Database was open successfully');
|
||||
}, (error) => {
|
||||
Log.error('Cannot open database: ', error);
|
||||
|
@ -15,6 +15,7 @@ function main() {
|
||||
Registry.setDebugMode(true);
|
||||
AppRegistry.registerComponent('AwesomeProject', () => Root);
|
||||
Log.setLevel(Registry.debugMode() ? Log.LEVEL_DEBUG : Log.LEVEL_WARN);
|
||||
Log.info('START ======================================================================================================');
|
||||
// Note: The final part of the initialization process is in
|
||||
// AppComponent.componentDidMount(), when the application is ready.
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Log } from 'src/log.js';
|
||||
|
||||
class Change extends BaseModel {
|
||||
|
||||
static TYPE_UNKNOWN = 0;
|
||||
static TYPE_NOOP = 0;
|
||||
static TYPE_CREATE = 1;
|
||||
static TYPE_UPDATE = 2;
|
||||
static TYPE_DELETE = 3;
|
||||
@ -22,15 +22,84 @@ class Change extends BaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
// static all() {
|
||||
// return this.db().selectAll('SELECT * FROM folders').then((r) => {
|
||||
// let output = [];
|
||||
// for (let i = 0; i < r.rows.length; i++) {
|
||||
// output.push(r.rows.item(i));
|
||||
// }
|
||||
// return output;
|
||||
// });
|
||||
// }
|
||||
static all() {
|
||||
return this.db().selectAll('SELECT * FROM changes').then((r) => {
|
||||
let output = [];
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
output.push(r.rows.item(i));
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
static deleteMultiple(ids) {
|
||||
if (ids.length == 0) return Promise.resolve();
|
||||
|
||||
return this.db().transaction((tx) => {
|
||||
let sql = '';
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
tx.executeSql('DELETE FROM changes WHERE id = ?', [ids[i]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static mergeChanges(changes) {
|
||||
let createdItems = [];
|
||||
let deletedItems = [];
|
||||
let itemChanges = {};
|
||||
|
||||
for (let i = 0; i < changes.length; i++) {
|
||||
let change = changes[i];
|
||||
|
||||
if (itemChanges[change.item_id]) {
|
||||
mergedChange = itemChanges[change.item_id];
|
||||
} else {
|
||||
mergedChange = {
|
||||
item_id: change.item_id,
|
||||
item_type: change.item_type,
|
||||
fields: [],
|
||||
ids: [],
|
||||
type: change.type,
|
||||
}
|
||||
}
|
||||
|
||||
if (change.type == this.TYPE_CREATE) {
|
||||
createdItems.push(change.item_id);
|
||||
} else if (change.type == this.TYPE_DELETE) {
|
||||
deletedItems.push(change.item_id);
|
||||
} else if (change.type == this.TYPE_UPDATE) {
|
||||
if (mergedChange.fields.indexOf(change.item_field) < 0) {
|
||||
mergedChange.fields.push(change.item_field);
|
||||
}
|
||||
}
|
||||
|
||||
mergedChange.ids.push(change.id);
|
||||
|
||||
itemChanges[change.item_id] = mergedChange;
|
||||
}
|
||||
|
||||
let output = [];
|
||||
|
||||
for (let itemId in itemChanges) {
|
||||
if (!itemChanges.hasOwnProperty(itemId)) continue;
|
||||
let change = itemChanges[itemId];
|
||||
|
||||
if (createdItems.indexOf(itemId) >= 0 && deletedItems.indexOf(itemId) >= 0) {
|
||||
// Item both created then deleted - skip
|
||||
change.type = this.TYPE_NOOP;
|
||||
} else if (deletedItems.indexOf(itemId) >= 0) {
|
||||
// Item was deleted at some point - just return one 'delete' event
|
||||
change.type = this.TYPE_DELETE;
|
||||
} else if (createdItems.indexOf(itemId) >= 0) {
|
||||
// Item was created then updated - just return one 'create' event with the latest changes
|
||||
change.type = this.TYPE_CREATE;
|
||||
}
|
||||
|
||||
output.push(change);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -43,15 +43,11 @@ class Setting extends BaseModel {
|
||||
}
|
||||
|
||||
static setValue(key, value) {
|
||||
// if (value !== null && typeof value === 'object') {
|
||||
// return this.setObject(key, value);
|
||||
// }
|
||||
|
||||
this.scheduleUpdate();
|
||||
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
if (this.cache_[i].key == key) {
|
||||
if (this.cache_[i].value === value) return;
|
||||
this.cache_[i].value = value;
|
||||
this.scheduleUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -59,19 +55,9 @@ class Setting extends BaseModel {
|
||||
let s = this.defaultSetting(key);
|
||||
s.value = value;
|
||||
this.cache_.push(s);
|
||||
this.scheduleUpdate();
|
||||
}
|
||||
|
||||
// static del(key) {
|
||||
// this.scheduleUpdate();
|
||||
|
||||
// for (let i = 0; i < this.cache_.length; i++) {
|
||||
// if (this.cache_[i].key == key) {
|
||||
// this.cache_[i].value = value;
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
static value(key) {
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
if (this.cache_[i].key == key) {
|
||||
@ -104,23 +90,32 @@ class Setting extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
static scheduleUpdate() {
|
||||
if (this.updateTimeoutId) clearTimeout(this.updateTimeoutId);
|
||||
static saveAll() {
|
||||
if (!this.updateTimeoutId_) return Promise.resolve();
|
||||
|
||||
this.updateTimeoutId = setTimeout(() => {
|
||||
Log.info('Saving settings...');
|
||||
this.updateTimeoutId = null;
|
||||
BaseModel.db().transaction((tx) => {
|
||||
tx.executeSql('DELETE FROM settings');
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
let q = Database.insertQuery(this.tableName(), this.cache_[i]);
|
||||
tx.executeSql(q.sql, q.params);
|
||||
}
|
||||
}).then(() => {
|
||||
Log.info('Settings have been saved.');
|
||||
}).catch((error) => {
|
||||
Log.warn('Could not update settings:', error);
|
||||
});
|
||||
Log.info('Saving settings...');
|
||||
clearTimeout(this.updateTimeoutId_);
|
||||
this.updateTimeoutId_ = null;
|
||||
|
||||
return BaseModel.db().transaction((tx) => {
|
||||
tx.executeSql('DELETE FROM settings');
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
let q = Database.insertQuery(this.tableName(), this.cache_[i]);
|
||||
tx.executeSql(q.sql, q.params);
|
||||
}
|
||||
}).then(() => {
|
||||
Log.info('Settings have been saved.');
|
||||
}).catch((error) => {
|
||||
Log.warn('Could not save settings', error);
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
static scheduleUpdate() {
|
||||
if (this.updateTimeoutId_) clearTimeout(this.updateTimeoutId_);
|
||||
|
||||
this.updateTimeoutId_ = setTimeout(() => {
|
||||
this.saveAll();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ let defaultState = {
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
Log.info('Reducer action', action);
|
||||
Log.info('Reducer action', action.type);
|
||||
|
||||
let newState = state;
|
||||
|
||||
@ -163,8 +163,8 @@ class AppComponent extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
let db = new Database();
|
||||
db.setDebugEnabled(Registry.debugMode());
|
||||
|
||||
//db.setDebugEnabled(Registry.debugMode());
|
||||
db.setDebugEnabled(false);
|
||||
BaseModel.dispatch = this.props.dispatch;
|
||||
|
||||
db.open().then(() => {
|
||||
@ -200,8 +200,8 @@ class AppComponent extends React.Component {
|
||||
Log.warn('Cannot load folders', error);
|
||||
});
|
||||
}).then(() => {
|
||||
// let synchronizer = new Synchronizer();
|
||||
// synchronizer.start();
|
||||
let synchronizer = new Synchronizer(db, Registry.api());
|
||||
synchronizer.start();
|
||||
}).catch((error) => {
|
||||
Log.error('Initialization error:', error);
|
||||
});
|
||||
|
@ -3,11 +3,14 @@ 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';
|
||||
import { promiseChain } from 'src/promise-chain.js';
|
||||
|
||||
class Synchronizer {
|
||||
|
||||
constructor() {
|
||||
constructor(db, api) {
|
||||
this.state_ = 'idle';
|
||||
this.db_ = db;
|
||||
this.api_ = api;
|
||||
}
|
||||
|
||||
state() {
|
||||
@ -15,52 +18,105 @@ class Synchronizer {
|
||||
}
|
||||
|
||||
db() {
|
||||
return Registry.db();
|
||||
return this.db_;
|
||||
}
|
||||
|
||||
api() {
|
||||
return Registry.api();
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
switchState(state) {
|
||||
Log.info('Sync: switching state to: ' + state);
|
||||
|
||||
if (state == 'downloadChanges') {
|
||||
let maxRevId = null;
|
||||
this.api().get('synchronizer', { last_id: Setting.value('sync.lastRevId') }).then((syncOperations) => {
|
||||
let promise = new Promise((resolve, reject) => { resolve(); });
|
||||
let chain = [];
|
||||
for (let i = 0; i < syncOperations.items.length; i++) {
|
||||
let syncOp = syncOperations.items[i];
|
||||
if (syncOp.id > maxRevId) maxRevId = syncOp.id;
|
||||
if (syncOp.item_type == 'folder') {
|
||||
|
||||
if (syncOp.type == 'create') {
|
||||
promise = promise.then(() => {
|
||||
chain.push(() => {
|
||||
let folder = Folder.fromApiResult(syncOp.item);
|
||||
// TODO: automatically handle NULL fields by checking type and default value of field
|
||||
if (!folder.parent_id) folder.parent_id = '';
|
||||
return Folder.save(folder, { isNew: true });
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: update
|
||||
// TODO: delete
|
||||
}
|
||||
}
|
||||
return promiseChain(chain);
|
||||
}).then(() => {
|
||||
Log.info('All items synced.');
|
||||
if (maxRevId) {
|
||||
Setting.setValue('sync.lastRevId', maxRevId);
|
||||
return Setting.saveAll();
|
||||
}
|
||||
}).then(() => {
|
||||
this.switchState('uploadingChanges');
|
||||
}).catch((error) => {
|
||||
Log.warn('Sync error', error);
|
||||
});
|
||||
} else if (state == 'uploadingChanges') {
|
||||
Change.all().then((changes) => {
|
||||
let mergedChanges = Change.mergeChanges(changes);
|
||||
// Log.info(mergedChanges);
|
||||
let chain = [];
|
||||
let processedChangeIds = [];
|
||||
for (let i = 0; i < mergedChanges.length; i++) {
|
||||
let c = mergedChanges[i];
|
||||
chain.push(() => {
|
||||
let p = null;
|
||||
|
||||
promise.then(() => {
|
||||
Log.info('All items synced.');
|
||||
}).catch((error) => {
|
||||
Log.warn('Sync error', error);
|
||||
Log.info(this.api());
|
||||
|
||||
if (c.type == Change.TYPE_NOOP) {
|
||||
p = Promise.resolve();
|
||||
} else if (c.type == Change.TYPE_CREATE) {
|
||||
p = Folder.load(c.item_id).then((folder) => {
|
||||
return this.api().put('folders/' + folder.id, null, folder);
|
||||
});
|
||||
} else if (c.type == Change.TYPE_UPDATE) {
|
||||
p = Folder.load(c.item_id).then((folder) => {
|
||||
return this.api().patch('folders/' + folder.id, null, folder);
|
||||
});
|
||||
} else if (c.type == Change.TYPE_DELETE) {
|
||||
p = Folder.load(c.item_id).then((folder) => {
|
||||
return this.api().delete('folders/' + folder.id);
|
||||
});
|
||||
}
|
||||
|
||||
return p.then(() => {
|
||||
processedChangeIds = processedChangeIds.concat(c.ids);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
promiseChain(chain).then(() => {
|
||||
Log.info('IDs to delete: ', processedChangeIds);
|
||||
Change.deleteMultiple(processedChangeIds);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
Log.info('Sync: start');
|
||||
|
||||
if (this.state() != 'idle') {
|
||||
Log.info("Sync: cannot start synchronizer because synchronization already in progress. State: " + this.state());
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info('Sync: start');
|
||||
if (!this.api().session()) {
|
||||
Log.info("Sync: cannot start synchronizer because user is not logged in.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.switchState('downloadChanges');
|
||||
}
|
||||
|
@ -23,11 +23,18 @@ class WebApi {
|
||||
let options = {};
|
||||
options.method = method.toUpperCase();
|
||||
if (data) {
|
||||
var formData = new FormData();
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) continue;
|
||||
formData.append(key, data[key]);
|
||||
let formData = null;
|
||||
if (method == 'POST') {
|
||||
formData = new FormData();
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) continue;
|
||||
formData.append(key, data[key]);
|
||||
}
|
||||
} else {
|
||||
options.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||
formData = stringify(data);
|
||||
}
|
||||
|
||||
options.body = formData;
|
||||
}
|
||||
|
||||
@ -91,6 +98,14 @@ class WebApi {
|
||||
return this.exec('POST', path, query, data);
|
||||
}
|
||||
|
||||
put(path, query, data) {
|
||||
return this.exec('PUT', path, query, data);
|
||||
}
|
||||
|
||||
patch(path, query, data) {
|
||||
return this.exec('PATCH', path, query, data);
|
||||
}
|
||||
|
||||
delete(path, query) {
|
||||
return this.exec('DELETE', path, query);
|
||||
}
|
||||
|
Reference in New Issue
Block a user