As we’ve shown in the Rhom section, adding synchronized data via RhoConnect to your Rhodes application is as simple as generating a model and enabling a :sync
flag. This triggers the internal Rhodes sync system called the SyncEngine
to synchronize data for the model and transparently handle bi-directional updates between the Rhodes application and the RhoConnect server.
Since version 4.0 SyncEngine API is available through ‘rhoconnect-client’ extension, which is bundled as part of RhoMobileSuite in ‘rhoconnect-client’ gem. All new-generated Rhodes applications will include rhoconnect-client extension by default. For existing applications that use SyncEngine API you should add ‘rhoconnect-client’ to the extensions section of build.yml.
This section covers in detail how the SyncEngine
works in Rhodes and how you can use its flexible APIs to build data-rich native applications.
The SyncEngine
interacts with RhoConnect over http(s) using JSON as a data exchange format. With the exception of bulk sync, pages of synchronized data, or “sync pages” as we will refer to them here, are sent as JSON from RhoConnect to the SyncEngine
.
Below is a simplified diagram of the SyncEngine
workflow:
This workflow consists of the following steps:
SyncEngine
sends authentication request to RhoConnect via SyncEngine.login
. RhoConnect calls Application.authenticate
with supplied credentials and returns true
or false
.
If this is a new client (i.e. fresh install or reset), the SyncEngine
will initialize with RhoConnect:
It requests a new unique id (client id) from RhoConnect. This id will be referenced throughout the sync process.
It will register platform information with RhoConnect. If this is a push-enabled application application, the SyncEngine
will send additional information like device push pin.
SyncEngine
requests sync pages from RhoConnect, one model(or Rhom model) at a time. The order the models are synchronized is determined by the model’s :sync_priority
, or determined automatically by the SyncEngine
.
When you generate a Rhodes application, you’ll notice there is an included directory called app/Settings
. This contains a default settings_controller.rb
and some views to manage authentication with RhoConnect.
login
In settings_controller.rb#do_login
, the SyncEngine.login
method is called:
SyncEngine.login( @params['login'], @params['password'], url_for(:action => :login_callback) )
Here login is called with the login
and password
provided by the login.erb
form. A :login_callback
action is declared to handle the asynchronous result of the SyncEngine.login
request.
login_callback
When SyncEngine.login
completes, the callback declared is executed and receives parameters including success or failure and error messages (if any).
def login_callback error_code = @params['error_code'].to_i if error_code == 0 # run sync if we were successful WebView.navigate Rho::RhoConfig.options_path SyncEngine.dosync else if error_code == Rho::RhoError::ERR_CUSTOMSYNCSERVER @msg = @params['error_message'] end if not @msg or @msg.length == 0 @msg = Rho::RhoError.new(error_code).message end WebView.navigate( url_for(:action => :login, :query => {:msg => @msg}) ) end end
This sample checks the login error_code
, if it is 0
, perform a full sync and render the settings page. Otherwise, it sets up an error message and re-displays the login page with an error.
application.rb#on_sync_user_changed
If the SyncEngine
already knows about a logged-in user and a new user logs in, then the on_sync_user_changed
hook is called (if it exists) before the login_callback
. This is useful, for example, if you want to re-initialize personalized settings for a new user.
require 'rho/rhoapplication' class AppApplication < Rho::RhoApplication def initialize super end def on_sync_user_changed super MyCoolApp.reset_user_preferences! end end
If
on_sync_user_changed
, data for all sync-enabled models will be removed. To remove data for all local models as well:
def on_sync_user_changed super Rhom::Rhom.database_local_reset end
Other auth-related methods are described in the SyncEngine
API section.
The SyncEngine
system uses notifications to provide information about the sync process to a Rhodes application. Notifications can be setup once for the duration of runtime or each time a sync is triggered. One a sync is processing for a model, notifications are called with parameters containing sync process state. Your application can use this information to display different wait pages, progress bars, etc.
To set a notification for a model, you can use the following method:
SyncEngine.set_notification( Account.get_source_id, url_for(:action => :sync_notify), "sync_complete=true" )
Which is the same as:
Account.set_notification( url_for(:action => :sync_notify), "sync_complete=true" )
In this example, once the sync process for the Account
model is complete, the view will be directed to the sync_notify
action (with params ‘sync_complete=true’) if user is on the same page.
In these examples, after the sync is complete the notifications are removed.
You can also set a notification for all models:
SyncEngine.set_notification( -1, url_for(:action => :sync_notify), "sync_complete=true" )
This notification will not be removed automatically.
When the notification is called, it will receive a variable called @params
, just like a normal Rhodes controller action.
These parameters are included in all notifications.
@params["source_id"]
- The id of the current model that is synchronizing.@params["source_name"]
- Name of the model (i.e. “Product”)@params["sync_type"]
- Type of sync used for this model: “incremental” or “bulk”@params["status"]
- Status of the current sync process: “in_progress”, “error”, “ok”, “complete”, “schema-changed”@params["total_count"]
- Total number of records that exist for this RhoConnect source.@params["processed_count"]
- Number of records included in the sync page.@params["cumulative_count"]
- Number of records the SyncEngine
has processed so far for this source.@params["bulk_status"]
- The state of the bulk sync process:“start”: when bulk sync start and when specific partition is start syncing
“download”: when client start downloading database from server
“change_db”: when client start applying new database
“blobs”: when client start downloading remote blob files
“ok”: when sync of partition finished without error
“complete”: when bulk sync finished for all partitions without errors
@params["partition"]
- Current bulk sync partition.@params["error_code"]
- HTTP response code of the RhoConnect server error: 401, 500, 404, etc.@params["error_message"]
- Response body (if any)@params["server_errors"]
- Hash of Type objects of RhoConnect adapter error (if exists): “login-error”, “query-error”, “create-error”, “update-error”, “delete-error”, “logoff-error”For “login-error”, “query-error”, “logoff-error”: Type object is hash contains ‘message’ from server: @params[“server_errors”][“query-error”][‘message’]
For “create-error”, “update-error”, “delete-error”: Type object is hash each containing an “object” as a key (that failed to create) and a corresponding “message” and “attributes”: @params[“server_errors”][“create-error”][object][‘message’], @params[“server_errors”][“create-error”][object][‘attributes’]
“create-error” has to be handled in sync callback. Otherwise sync will stop on this model. To fix create errors you should call Model.on_sync_create_error or SyncEngine.on_sync_create_error
@params["total_count"]
- Total number of records that exist for this RhoConnect source.@params["processed_count"]
- Number of records included in the last sync page.@params["cumulative_count"]
- Number of records the SyncEngine
has processed so far for this source.This status returns only when the SyncEngine
process is complete.
This status returns for bulk-sync models that use FixedSchema
when the schema has changed in the RhoConnect server.
In this scenario the sync callback should notify the user with a wait screen and start the bulk sync process.
has to be handled in sync callback. Otherwise sync will stop on this model. To fix create errors you should call Model.on_sync_create_error or SyncEngine.on_sync_create_error:
SyncEngine.on_sync_create_error( src_name, objects, action ) Model.on_sync_create_error( objects, action ) * objects - One or more error objects * action - May be :delete or :recreate. :delete just remove object from client, :recreate will push this object to server again at next sync.
If not handled, local modifications, which were failing on server, will never sync to server again. So sync will work fine, but nobody will know about these changes.
SyncEngine.on_sync_update_error( src_name, objects, action, rollback_objects = nil ) Model.on_sync_update_error( objects, action, rollback_objects = nil) * objects - One or more error objects * action - May be :retry or :rollback. :retry will push update object operation to server again at next sync, :rollback will write rollback_objects to client database. * rollback_objects - contains objects attributes before failed update and sends by server. should be specified for :rollback action.
If not handled, local modifications, which were failing on server, will never sync to server again. So sync will work fine, but nobody will know about these changes.
SyncEngine.on_sync_delete_error( src_name, objects, action ) Model.on_sync_delete_error( objects, action ) * objects - One or more error objects * action - May be :retry - will push delete object operation to server again at next sync.
For example: :::ruby SyncEngine.on_sync_create_error( @params[‘source_name’], @params[‘server_errors’][‘create-error’], :delete)
SyncEngine.on_sync_update_error( @params['source_name'], @params['server_errors']['update-error'], :retry) SyncEngine.on_sync_update_error( @params['source_name'], @params['server_errors']['update-error'], :rollback, @params['server_errors']['update-rollback'] ) SyncEngine.on_sync_delete_error( @params['source_name'], @params['server_errors']['delete-error'], :retry)
Unknown client error return by server after resetting server database, removing particular client id from database or any other cases when server cannot find client id(sync server unique id of device). Note that login session may still exist on server, so in this case client does not have to login again, just create new client id. Processing of this error contain 2 steps:
When unknown client error is come from server, client should call database_client_reset and start new sync, to register new client id:
rho_error = Rho::RhoError.new(err_code) if err_code == Rho::RhoError::ERR_CUSTOMSYNCSERVER @msg = @params['error_message'] end @msg = rho_error.message unless @msg and @msg.length > 0 if rho_error.unknown_client?(@params['error_message']) Rhom::Rhom.database_client_reset SyncEngine.dosync end
If login session also deleted or expired on the server, then customer has to login again:
rho_error = Rho::RhoError.new(err_code) if err_code == Rho::RhoError::ERR_CUSTOMSYNCSERVER @msg = @params['error_message'] end @msg = rho_error.message unless @msg and @msg.length > 0 if err_code == Rho::RhoError::ERR_UNATHORIZED WebView.navigate( url_for( :action => :login, :query => { :msg => "Server credentials expired!" } ) ) end
Here is a simple example of a sync notification method that uses some of the parameters described above:
def sync_notify status = @params['status'] ? @params['status'] : "" bulk_sync? = @params['sync_type'] == 'bulk' if status == "in_progress" # do nothing elsif status == "complete" or status == "ok" WebView.navigate Rho::RhoConfig.start_path elsif status == "error" if @params['server_errors'] && @params['server_errors']['create-error'] SyncEngine.on_sync_create_error( @params['source_name'], @params['server_errors']['create-error'], :delete) end err_code = @params['error_code'].to_i rho_error = Rho::RhoError.new(err_code) if err_code == Rho::RhoError::ERR_CUSTOMSYNCSERVER @msg = @params['error_message'] end @msg = rho_error.message unless @msg and @msg.length > 0 if rho_error.unknown_client?(@params['error_message']) Rhom::Rhom.database_client_reset SyncEngine.dosync elsif err_code == Rho::RhoError::ERR_UNATHORIZED WebView.navigate( url_for( :action => :login, :query => { :msg => "Server credentials expired!" } ) ) else WebView.navigate( url_for( :action => :err_sync, :query => { :msg => @msg } ) ) end end end
If the view was updated using AJAX calls, this mechanism may not work correctly as the view location will not change from one AJAX call to another. Therefore, you might need to specify the
:controller
option in WebView.navigate.
The SyncEngine
can also send a notification when a specific object on the current page has been modified. This is useful if you have frequently-changing data like feeds or timelines in your application and want them to update without the user taking any action.
To use object notifications, first set the notification callback in application.rb#initialize
:
class AppApplication < Rho::RhoApplication def initialize super SyncEngine.set_objectnotify_url( url_for( :controller => "Product", :action => :sync_object_notify ) ) end end
Next, in your controller action that displays the object(s), add the object notification by passing in a record or collection of records:
class ProductController < Rho::RhoController # GET /Product def index @products = Product.find(:all) add_objectnotify(@products) render end # ... def sync_object_notify #... do something with notification data ... # refresh the current page WebView.refresh # or call System.execute_js to call JavaScript function which will update list end end
The object notification callback receives three arrays of hashes: “deleted”, “updated” and “created”. Each hash contains values for the keys “object” and “source_id” so you can display which records were changed.
Synchronizing images or binary objects between RhoConnect and the SyncEngine
is declared by having a ‘blob attribute’ on the Rhom model. Please see the blob sync section for more information.
If you have a large dataset in your backend service, you don’t have to synchronize everything with the SyncEngine
. Instead you can filter the synchronized dataset using the SyncEngine
’s search
function.
Like everything else with the SyncEngine
, search
requires a defined callback which is executed when the search
results are retrieved from RhoConnect.
First, call search
from your controller action:
def search page = @params['page'] || 0 page_size = @params['page_size'] || 10 Contact.search( :from => 'search', :search_params => { :FirstName => @params['FirstName'], :LastName => @params['LastName'], :Company => @params['Company'] }, :offset => page * page_size, :max_results => page_size, :callback => url_for(:action => :search_callback), :callback_param => "" ) render :action => :search_wait end
Your callback might look like:
def search_callback status = @params["status"] if (status and status == "ok") WebView.navigate( url_for( :action => :show_page, :query => @params['search_params'] ) ) else render :action => :search_error end end
Typically you want to forward the original search query
@params['search_params']
to your view that displays the results so you can perform the same query locally.
Next, the resulting action :show_page
will be called. Here we demonstrate using Rhom’s advanced find query syntax since we are filtering a very large dataset:
def show_page @contacts = Contact.find( :all, :conditions => { { :func => 'LOWER', :name => 'FirstName', :op => 'LIKE' } => @params[:FirstName], { :func => 'LOWER', :name=>'LastName', :op=>'LIKE' } => @params[:LastName], { :func=>'LOWER', :name=>'Company', :op=>'LIKE' } => @params[:Company], }, :op => 'OR', :select => ['FirstName','LastName', 'Company'], :per_page => page_size, :offset => page * page_size ) render :action => :show_page end
If you want to stop or cancel the search, return “stop” in your callback:
def search_callback if(status and status == 'ok') WebView.navigate( url_for :action => :show_page ) else 'stop' end end
Finally, you will need to implement the search
method in your source adapter. See the RhoConnect search method for more details.
Below is the full list of methods available on the SyncEngine
:
login(login, password, callback)
Authenticates the user with RhoConnect. The callback will be executed when it is finished. See the authentication section for details.
SyncEngine.login( @params['login'], @params['password'], url_for(:action => :login_callback) )
logout
Logout the user from the RhoConnect server. This removes the local user session. See the authentication section for details.
SyncEngine.logout
logged_in
Returns 1 if the SyncEngine
currently has a user session, 0 if not.
if SyncEngine::logged_in == 1 render :action => :index else render :action => :login end
dosync(show_sync_status = true, query_params = "", sync_only_sources_with_local_changes = false )
Start the SyncEngine
process and display an optional status popup (defaults to true).
query_params will pass to sync server. sync_only_sources_with_local_changes indicates that only sources that have local changes will be synced.
SyncEngine.dosync(false) #=> no status popups are displayed SyncEngine.dosync(false, "param1=12¶m2=abc") #=> no status popups are displayed and parameters will pass to sync server SyncEngine.dosync(false, "", true) #=> no status popups are displayed and synchronization will be performed only for sources with local changes.
dosync_source(source_id_or_name, show_sync_status = true, query_params = "")
Star the SyncEngine
process for a given source id or source name and display an optional status popup (defaults to true).
query_params will pass to sync server
SyncEngine.dosync_source(Product.get_source_id.to_i, false) #sync by source id SyncEngine.dosync_source(Product.get_source_name, false) #sync by source name
lock_sync_mutex
Blocking call to wait for SyncEngine
lock (useful for performing batch operations).
SyncEngine.lock_sync_mutex #... perform blocking tasks... SyncEngine.unlock_sync_mutex
unlock_sync_mutex
Release the acquired SyncEngine
lock (make sure you do this if you call lock_sync_mutex
!).
stop_sync
Stops any sync operations currently in progress.
SyncEngine.stop_sync #=> no callback is called
set_notification(source_id, callback_url, params = nil)
See the sync notification section.
set_notification(-1, callback_url, params = nil)
Set notification callback for all models. This callback is not removed after the sync process completes. See the sync notification section.
clear_notification(source_id)
Clears the sync notification for a given source id.
SyncEngine.clear_notification(Product.get_source_id)
on_sync_create_error( src_name, objects, action )
“create-error” has to be handled in sync callback. Otherwise sync will stop on this model. To fix create errors you should call Model.on_sync_create_error or SyncEngine.on_sync_create_error.
SyncEngine.on_sync_create_error( @params['source_name'], @params['server_errors']['create-error'], :delete)
on_sync_update_error( src_name, objects, action, rollback_objects = nil )
SyncEngine.on_sync_update_error( @params['source_name'], @params['server_errors']['update-error'], :retry) SyncEngine.on_sync_update_error( @params['source_name'], @params['server_errors']['update-error'], :rollback, @params['server_errors']['update-rollback'] )
on_sync_delete_error( src_name, objects, action )
SyncEngine.on_sync_delete_error( @params['source_name'], @params['server_errors']['delete-error'], :retry)
set_pollinterval(interval)
Update the SyncEngine
poll interval. Setting this to 0 will disable polling-based sync. However, you may still use push-based-sync.
SyncEngine.set_pollinterval(20)' #=> now polls every 20 seconds
set_syncserver(server_url)
Sets the RhoConnect server address and stores it in rhoconfig.txt
.
SyncEngine.set_syncserver("http://myapp.com/application") #=> don't forget the '/application' path
set_objectnotify_url(url)
See the sync notification section.
set_pagesize(size)
Set the sync page size for the SyncEngine
. Default size is 2000. See the SyncEngine
workflow for how this is used.
SyncEngine.set_pagesize(5000)
get_pagesize
Get the current sync page size for the SyncEngine
. See the SyncEngine
workflow for how this is used.
SyncEngine.get_pagesize #=> 2000 SyncEngine.set_pagesize(5000) SyncEngine.get_pagesize #=> 5000
enable_status_popup(false)
Enable or disable show status popup. True by default for Blackberry, false for other platforms.
SyncEngine.enable_status_popup(true)
set_ssl_verify_peer(true)
Enable or disable verification of RhoConnect ssl certificates, true by default.
# using a self-signed cert SyncEngine.set_ssl_verify_peer(false)
get_user_name
Returns current username of the SyncEngine
session if logged_in
is true, otherwise returns the last logged in username.
SyncEngine.get_user_name #=> "testuser"
search(*args)
Call search on the RhoConnect application with given parameters. See the search section for more details.
# :from Sets the RhoConnect path that records # will be fetched with (optional). # Default is 'search'. # # :search_params Hash containing key/value search items. # # :offset Starting record to be returned. # # :max_results Max number of records to be returned. # # :callback Callback to be executed after search # is completed. # # :callback_param (optional) Parameters passed to callback. # # :progress_step (optional) Define how often search callback # will be executed with 'in_progress' state. # :sync_changes (optional) - true or false(default). Define should client changes send to server before search. Contact.search( :from => 'search', :search_params => { :FirstName => @params['FirstName'], :LastName => @params['LastName'], :Company => @params['Company'] }, :offset => page * page_size, :max_results => page_size, :callback => url_for(:action => :search_callback), :callback_param => "", :sync_changes => false )
search(*args) (multiple sources)
Call search on the RhoConnect application with multiple source names. This is useful if your search
spans across multiple models.
For example:
SyncEngine.search( :source_names => ['Product', 'Customer'], :from => 'search', :search_params => { :FirstName => @params['FirstName'], :LastName => @params['LastName'], :Company => @params['Company'] }, :offset => page * page_size, :max_results => page_size, :callback => url_for(:action => :search_callback), :callback_param => "", :sync_changes => false )
Parameters are the same as for ModelName.search with an additional parameter:
:source_names
- Sends a list of source adapter names to RhoConnect to search across.Sync engine AJAX API has been implemented to provide access to low-level control on synchronization process right from plain HTML/javascript UI pages.
It isn’t intended to be used in every application. It requires deep knowledge of SyncEngine functionality and operations. It may broke your application if used improperly so use it with care please.
On iOS, if application is put to background, it will be suspended. To allow application finish sync after application goes to background, you can use ‘finish_sync_in_background’ parameter in rhoconfig.txt
. When this parameter is set to ‘1’, if sync is active in the time of background transition ( e.g. started from app_deactivate handler ), application will not be suspended until sync is finished.