Status: Experimental
(Java flavored)
The Client API I propose bridges Datastores, Pipes, and Push technologies.
Currently, the user has to manually signal an intent to push his changes to a remote service. However, the push subsystem takes care of receiving updates from a server.
This strawman is biased towards Java, but as we discuss it we can move toward a more generic document.
This straw man is still very limited. Implementation specific limitations are described with their respective classes, but in general I have not yet proposed solutions for offline support, conflict resolution and detection, etc.
Before we get into the APIs I would like to demonstrate a usage example for two way settings sync.
TwoWaySqlSynchronizer<UserCalendar> userCalendarSync;
TwoWaySqlSynchronizerConfig syncConfig = new TwoWaySqlSynchronizer.TwoWaySqlSynchronizerConfig();
syncConfig.setKlass(UserCalendar.class);
syncConfig.setPipeConfig(userCalendarPipeConfig);
syncConfig.setStoreConfig(userCalendarStoreConfig);
userCalendarSync = new TwoWaySqlSynchronizer<UserCalendar>(syncConfig);
userCalendarSync.beginSync(this, callback);
userCalendarSync.resetToRemoteState(new Callback<List<UserCalendar>>() {
@Override
public void onSuccess(final List<UserCalendar> schedules) {
mainLoopHandler.post(new Runnable() {
@Override
public void run() {
adapter.update(schedules);
}
});
}
@Override
public void onFailure(Exception e) {
Log.e("LOAD_CAL", e.getMessage(), e);
}
});
userCalendarSync.sync();
//Note, the class these methods are members of extends Fragment and implements SynchronizeEventListener<UserCalendar>
@Override
public void onResume() {
super.onResume();
userCalendarSync.addListener(this);
}
@Override
public void onPause() {
super.onPause();
userCalendarSync.removeListener(this);
}
@Override
public void dataUpdated(Collection<UserCalendar> newData) {
adapter.update(new ArrayList<UserCalendar>(newData));
adapter.notifyDataSetChanged();
}
<receiver
android:name="org.devnexus.sync.AeroGearGCMSyncMessageReceiver"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="org.devnexus" />
</intent-filter>
<meta-data android:name="SYNC_PROVIDER_KEY" android:value="org.devnexus.sync.DevnexusCalendarSyncProvider"/>
<meta-data android:name="SYNC_MESSAGE_KEY" android:value="org.devnexus.sync.UserCalendar"/>
</receiver>
The following classes should be applicable over all implementations.
Objects which implement this pattern will be notified by the sync subsystem when a change has occurred.
Objects which implement this pattern will be responsible for scheduling synchronization and listeners.
This Synchronizer will examine a local data store and send changes to the server. It does this by maintaining a reference to an external SQLStore and an internal reference to a Shadow SQL Store. The Shadow store is updated when synchronization happens to generate a change list to send to the server or apply to the local data store.
One of the “neat” things about this synchronizer is that since GCM handles offline state, we will only receive messages when the device is online. Thus we don’t have to worry about online/offline detection. We still have to worry about conflict detection and resolution however.
This synchronizer may store data in any store and refreshes its data from the server on a schedule provided by its config object.
Properties on this object are used to configure Synchronizers. Individual synchronizers may extends this class to include their own properties.
The following classes are specific to Android but may serve as a referenc for others.
This class listens for GCM messages from Google and sends signals to a configured Synchronizer to load the remote changes.
This class listens for Alarms invoked by the Android Alarm manager. It will call inform a sychronizer to synchronize based on a clock.
Use cases where caching data is useful and where it should be transparent and long lived are covered by this.
PipeConfig pipeConfig = buildConfig()
SyncConfig syncConfig = new SyncConfig();
syncConfig.setType(SyncTypes.PeriodicReadOnly);
syncConfig.setExpiresIn(SyncConfig.24_HOURS);
pipeConfig.setSync(syncConfig);
Pipe<Schedule> schedulePipe = pipeline.pipe(pipeConfig);
schedulePipe.read(/*call back*/);
/*
This will check a SQL Store, notice there is no data, fetch the data, and store metadata about when it was fetched.
*/
//12 Hours later
schedulePipe.read(/*call back*/);
/*
This will check a SQL Store, notice there is data, notice it is inside of the expires time, and return it.
No call to the remote source will be made.
*/
//36 Hours later
schedulePipe.read(/*call back*/);
/*
This will check a SQL Store, notice there is data, notice it is stale, and refetch and refresh the data.
*/
// [option 1 fully automatic we create a pipe and add the posibilty to add a store for failover and sync just happens at on- and offline events]
// and because merging can fail users can add a conflict handlers
Builder builder = Builder.createPipe(pipeConfig).addFailoverStore(storeConfig);
Pipe<Car> pipe = builder.pipe(Car.class);
pipe.addConfictHandler(new ConflictHandler() {
public void conflict(Field originalField, Field newField) {
// user interaction
}
});
// [option 2 explicit let the user specify when to sync and what to sync]
SyncedPipe pipe = SyncPipeBuilder.build(options); // SyncPipe Store and Pipe togheter
// or we only use a store to sync and tell the sync manager where to sync to
SyncManager syncManager = new SyncManger();
syncManger.filter(readFilter); // maybe we don't want to sync all data but just some part
syncManager.addConnectionHandler(new ConnectionHandler() {
public void onConnection() {
syncManger.sync(pipe);
}
});
syncManger.addConfictHandler(new ConflictHandler() {
public void conflict(Field original, Field new) {
// user interaction
}
});