From fe277e0cac6e0d195c3ebc87ba58ff2a4c1d73bb Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sun, 21 May 2017 21:55:01 +0200 Subject: [PATCH] synchronizer --- ReactNativeClient/src/base-model.js | 4 ++- .../src/components/screens/login.js | 4 +++ ReactNativeClient/src/database.js | 25 ++++++++++++++++- ReactNativeClient/src/registry.js | 9 +++++++ ReactNativeClient/src/root.js | 3 ++- ReactNativeClient/src/synchronizer.js | 27 ++++++++++++++----- .../Controller/FoldersController.php | 4 ++- src/AppBundle/Controller/NotesController.php | 4 ++- src/AppBundle/Model/Change.php | 27 +++++++++++++++---- 9 files changed, 91 insertions(+), 16 deletions(-) diff --git a/ReactNativeClient/src/base-model.js b/ReactNativeClient/src/base-model.js index 1953c5bd8..01b18ead4 100644 --- a/ReactNativeClient/src/base-model.js +++ b/ReactNativeClient/src/base-model.js @@ -171,10 +171,12 @@ class BaseModel { } } } - }).then(() => { + }).then((r) => { o = Object.assign({}, o); o.id = query.id; return o; + }).catch((error) => { + Log.error('Cannot save model', error); }); } diff --git a/ReactNativeClient/src/components/screens/login.js b/ReactNativeClient/src/components/screens/login.js index 4d3d4aa7c..c908b08e1 100644 --- a/ReactNativeClient/src/components/screens/login.js +++ b/ReactNativeClient/src/components/screens/login.js @@ -58,6 +58,10 @@ class LoginScreenComponent extends React.Component { this.props.dispatch({ type: 'Navigation/BACK', }); + + Registry.api().setSession(session.id); + + Registry.synchronizer().start(); }).catch((error) => { this.setState({ errorMessage: _('Could not login: %s)', error.message) }); }); diff --git a/ReactNativeClient/src/database.js b/ReactNativeClient/src/database.js index ca946424e..9bcf5caa0 100644 --- a/ReactNativeClient/src/database.js +++ b/ReactNativeClient/src/database.js @@ -117,7 +117,7 @@ class Database { } open() { - this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-21.sqlite' }, (db) => { + this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-26.sqlite' }, (db) => { Log.info('Database was open successfully'); }, (error) => { Log.error('Cannot open database: ', error); @@ -333,6 +333,29 @@ class Database { Log.info(this.tableFields_); }); + + + + // }).then(() => { + // let p = this.exec('DELETE FROM notes').then(() => { + // return this.exec('DELETE FROM folders'); + // }).then(() => { + // return this.exec('DELETE FROM changes'); + // }).then(() => { + // return this.exec('DELETE FROM settings WHERE `key` = "sync.lastRevId"'); + // }); + + // return p.then(() => { + // return this.exec('UPDATE settings SET `value` = "' + uuid.create() + '" WHERE `key` = "clientId"'); + // }).then(() => { + // return this.exec('DELETE FROM settings WHERE `key` != "clientId"'); + // }); + + // return p; + + + + }).catch((error) => { if (error && error.code != 0) { Log.error(error); diff --git a/ReactNativeClient/src/registry.js b/ReactNativeClient/src/registry.js index 7025e9435..1898009fb 100644 --- a/ReactNativeClient/src/registry.js +++ b/ReactNativeClient/src/registry.js @@ -33,6 +33,15 @@ class Registry { return this.db_; } + static setSynchronizer(s) { + this.synchronizer_ = s; + } + + static synchronizer() { + if (!this.synchronizer_) throw new Error('Accessing synchronizer before it has been initialised'); + return this.synchronizer_; + } + } export { Registry }; \ No newline at end of file diff --git a/ReactNativeClient/src/root.js b/ReactNativeClient/src/root.js index 828a02ff5..95721a25a 100644 --- a/ReactNativeClient/src/root.js +++ b/ReactNativeClient/src/root.js @@ -164,7 +164,7 @@ class AppComponent extends React.Component { componentDidMount() { let db = new Database(); //db.setDebugEnabled(Registry.debugMode()); - db.setDebugEnabled(false); + db.setDebugEnabled(true); BaseModel.dispatch = this.props.dispatch; BaseModel.db_ = db; @@ -199,6 +199,7 @@ class AppComponent extends React.Component { }); }).then(() => { let synchronizer = new Synchronizer(db, Registry.api()); + Registry.setSynchronizer(synchronizer); synchronizer.start(); }).catch((error) => { Log.error('Initialization error:', error); diff --git a/ReactNativeClient/src/synchronizer.js b/ReactNativeClient/src/synchronizer.js index 0c3feca73..74739d87d 100644 --- a/ReactNativeClient/src/synchronizer.js +++ b/ReactNativeClient/src/synchronizer.js @@ -27,12 +27,20 @@ class Synchronizer { return this.api_; } - switchState(state) { - Log.info('Sync: switching state to: ' + state); + processState(state) { + // if (this.state() == state) { + // Log.info('Sync: cannot switch to same state: ' + state); + // return; + // } + + Log.info('Sync: processing: ' + state); + this.state_ = state; if (state == 'downloadChanges') { let maxRevId = null; + let hasMore = false; this.api().get('synchronizer', { last_id: Setting.value('sync.lastRevId') }).then((syncOperations) => { + hasMore = syncOperations.has_more; let chain = []; for (let i = 0; i < syncOperations.items.length; i++) { let syncOp = syncOperations.items[i]; @@ -57,6 +65,7 @@ class Synchronizer { 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 }); }); @@ -71,13 +80,17 @@ class Synchronizer { } return promiseChain(chain); }).then(() => { - Log.info('All items synced.'); + Log.info('All items synced. has_more = ', hasMore); if (maxRevId) { Setting.setValue('sync.lastRevId', maxRevId); return Setting.saveAll(); } }).then(() => { - this.switchState('uploadingChanges'); + if (hasMore) { + this.processState('downloadChanges'); + } else { + this.processState('uploadingChanges'); + } }).catch((error) => { Log.warn('Sync error', error); }); @@ -112,11 +125,13 @@ class Synchronizer { return this.api().patch(path + '/' + item.id, null, item); }); } else if (c.type == Change.TYPE_DELETE) { - return this.api().delete(path + '/' + c.item_id); + p = this.api().delete(path + '/' + c.item_id); } return p.then(() => { processedChangeIds = processedChangeIds.concat(c.ids); + }).catch((error) => { + Log.warn('Failed applying changes', c.ids); }); }); } @@ -142,7 +157,7 @@ class Synchronizer { return; } - this.switchState('downloadChanges'); + this.processState('downloadChanges'); } } diff --git a/src/AppBundle/Controller/FoldersController.php b/src/AppBundle/Controller/FoldersController.php index e82b2a58b..4389e3ced 100755 --- a/src/AppBundle/Controller/FoldersController.php +++ b/src/AppBundle/Controller/FoldersController.php @@ -45,10 +45,12 @@ class FoldersController extends ApiController { } if ($request->isMethod('PUT')) { - if (!$folder) $folder = new Folder(); + $isNew = !$folder; + if ($isNew) $folder = new Folder(); $folder->fromPublicArray($this->putParameters()); $folder->id = Folder::unhex($id); $folder->owner_id = $this->user()->id; + $folder->setIsNew($isNew); $folder->save(); return static::successResponse($folder); } diff --git a/src/AppBundle/Controller/NotesController.php b/src/AppBundle/Controller/NotesController.php index 189867dc7..6c56c6aee 100755 --- a/src/AppBundle/Controller/NotesController.php +++ b/src/AppBundle/Controller/NotesController.php @@ -37,10 +37,12 @@ class NotesController extends ApiController { } if ($request->isMethod('PUT')) { - if (!$note) $note = new Note(); + $isNew = !$note; + if ($isNew) $note = new Note(); $note->fromPublicArray($this->putParameters()); $note->id = Note::unhex($id); $note->owner_id = $this->user()->id; + $note->setIsNew($isNew); $note->save(); return static::successResponse($note); } diff --git a/src/AppBundle/Model/Change.php b/src/AppBundle/Model/Change.php index 7dd3ad8ac..6196f8ed0 100755 --- a/src/AppBundle/Model/Change.php +++ b/src/AppBundle/Model/Change.php @@ -18,6 +18,7 @@ class Change extends BaseModel { // - If update, update, delete, update => return 'delete' only // - If update, update, update => return last + // $limit = 10000; $limit = 100; $changes = self::where('id', '>', $fromChangeId) ->where('user_id', '=', $userId) @@ -46,8 +47,12 @@ class Change extends BaseModel { } $itemIdToChange[$change->item_id] = $change; + + // echo BaseModel::hex($change->item_id) . ' ' . $change->id . ' ' . Change::enumName('type', $change->type) . "\n"; } + // die(); + $output = array(); foreach ($itemIdToChange as $itemId => $change) { @@ -68,11 +73,23 @@ class Change extends BaseModel { $syncItem['type'] = 'delete'; } else if (in_array($itemId, $createdItems)) { // Item was created then updated - just return one 'create' event with the latest changes - $syncItem['type'] = 'create'; - $syncItem['item'] = self::requireItemById($change->item_type, $change->item_id); + + // If $item is null it can mean two things: + // - The item has been deleted by the client requesting the sync items (which means the "delete" event is not included in the batch) + // - The item has been deleted in the next batch - for example, if we're requesting sync items 0 to 100, the "delete" event is in range 101 to 200. + // In the both cases we don't need to do anything. In the first case, the client already knows that the item has been deleted. + // In the second case, the "delete" event will be sent later on. + $item = BaseItem::byTypeAndId($change->item_type, $change->item_id); + if ($item) { + $syncItem['type'] = 'create'; + $syncItem['item'] = $item; + } } else { - $syncItem['item_fields'] = $itemIdToChangedFields[$change->item_id]; - $syncItem['item'] = self::requireItemById($change->item_type, $change->item_id); + $item = BaseItem::byTypeAndId($change->item_type, $change->item_id); + if ($item) { + $syncItem['item_fields'] = $itemIdToChangedFields[$change->item_id]; + $syncItem['item'] = $item; + } } $output[] = $syncItem; @@ -108,7 +125,7 @@ class Change extends BaseModel { static private function requireItemById($itemTypeId, $itemId) { $item = BaseItem::byTypeAndId($itemTypeId, $itemId); - if (!$item) throw new \Exception('No such item: ' . $itemTypeId . ' ' . $itemId); + if (!$item) throw new \Exception('No such item: ' . $itemTypeId . ' ' . BaseModel::hex($itemId)); return $item; }