New file |
0,0 → 1,690 |
if(!dojo._hasResource["dojox.off.sync"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. |
dojo._hasResource["dojox.off.sync"] = true; |
dojo.provide("dojox.off.sync"); |
|
dojo.require("dojox.storage.GearsStorageProvider"); |
dojo.require("dojox.off._common"); |
dojo.require("dojox.off.files"); |
|
// Author: Brad Neuberg, bkn3@columbia.edu, http://codinginparadise.org |
|
// summary: |
// Exposes syncing functionality to offline applications |
dojo.mixin(dojox.off.sync, { |
// isSyncing: boolean |
// Whether we are in the middle of a syncing session. |
isSyncing: false, |
|
// cancelled: boolean |
// Whether we were cancelled during our last sync request or not. If |
// we are cancelled, then successful will be false. |
cancelled: false, |
|
// successful: boolean |
// Whether the last sync was successful or not. If false, an error |
// occurred. |
successful: true, |
|
// details: String[] |
// Details on the sync. If the sync was successful, this will carry |
// any conflict or merging messages that might be available; if the |
// sync was unsuccessful, this will have an error message. For both |
// of these, this should be an array of Strings, where each string |
// carries details on the sync. |
// Example: |
// dojox.off.sync.details = ["The document 'foobar' had conflicts - yours one", |
// "The document 'hello world' was automatically merged"]; |
details: [], |
|
// error: boolean |
// Whether an error occurred during the syncing process. |
error: false, |
|
// actions: dojox.off.sync.ActionLog |
// Our ActionLog that we store offline actions into for later |
// replaying when we go online |
actions: null, |
|
// autoSync: boolean |
// For advanced usage; most developers can ignore this. |
// Whether we do automatically sync on page load or when we go online. |
// If true we do, if false syncing must be manually initiated. |
// Defaults to true. |
autoSync: true, |
|
// summary: |
// An event handler that is called during the syncing process with |
// the state of syncing. It is important that you connect to this |
// method and respond to certain sync events, especially the |
// "download" event. |
// description: |
// This event handler is called during the syncing process. You can |
// do a dojo.connect to receive sync feedback: |
// |
// dojo.connect(dojox.off.sync, "onSync", someFunc); |
// |
// You will receive one argument, which is the type of the event |
// and which can have the following values. |
// |
// The most common two types that you need to care about are "download" |
// and "finished", especially if you are using the default |
// Dojo Offline UI widget that does the hard work of informing |
// the user through the UI about what is occuring during syncing. |
// |
// If you receive the "download" event, you should make a network call |
// to retrieve and store your data somehow for offline access. The |
// "finished" event indicates that syncing is done. An example: |
// |
// dojo.connect(dojox.off.sync, "onSync", function(type){ |
// if(type == "download"){ |
// // make a network call to download some data |
// // for use offline |
// dojo.xhrGet({ |
// url: "downloadData.php", |
// handleAs: "javascript", |
// error: function(err){ |
// dojox.off.sync.finishedDownloading(false, "Can't download data"); |
// }, |
// load: function(data){ |
// // store our data |
// dojox.storage.put("myData", data); |
// |
// // indicate we are finished downloading |
// dojox.off.sync.finishedDownloading(true); |
// } |
// }); |
// }else if(type == "finished"){ |
// // update UI somehow to indicate we are finished, |
// // such as using the download data to change the |
// // available data |
// } |
// }) |
// |
// Here is the full list of event types if you want to do deep |
// customization, such as updating your UI to display the progress |
// of syncing (note that the default Dojo Offline UI widget does |
// this for you if you choose to pull that in). Most of these |
// are only appropriate for advanced usage and can be safely |
// ignored: |
// |
// * "start" |
// syncing has started |
// * "refreshFiles" |
// syncing will begin refreshing |
// our offline file cache |
// * "upload" |
// syncing will begin uploading |
// any local data changes we have on the client. |
// This event is fired before we fire |
// the dojox.off.sync.actions.onReplay event for |
// each action to replay; use it to completely |
// over-ride the replaying behavior and prevent |
// it entirely, perhaps rolling your own sync |
// protocol if needed. |
// * "download" |
// syncing will begin downloading any new data that is |
// needed into persistent storage. Applications are required to |
// implement this themselves, storing the required data into |
// persistent local storage using Dojo Storage. |
// * "finished" |
// syncing is finished; this |
// will be called whether an error ocurred or not; check |
// dojox.off.sync.successful and dojox.off.sync.error for sync details |
// * "cancel" |
// Fired when canceling has been initiated; canceling will be |
// attempted, followed by the sync event "finished". |
onSync: function(/* String */ type){}, |
|
synchronize: function(){ /* void */ |
// summary: Starts synchronizing |
|
//dojo.debug("synchronize"); |
if(this.isSyncing || dojox.off.goingOnline || (!dojox.off.isOnline)){ |
return; |
} |
|
this.isSyncing = true; |
this.successful = false; |
this.details = []; |
this.cancelled = false; |
|
this.start(); |
}, |
|
cancel: function(){ /* void */ |
// summary: |
// Attempts to cancel this sync session |
|
if(!this.isSyncing){ return; } |
|
this.cancelled = true; |
if(dojox.off.files.refreshing){ |
dojox.off.files.abortRefresh(); |
} |
|
this.onSync("cancel"); |
}, |
|
finishedDownloading: function(successful /* boolean? */, |
errorMessage /* String? */){ |
// summary: |
// Applications call this method from their |
// after getting a "download" event in |
// dojox.off.sync.onSync to signal that |
// they are finished downloading any data |
// that should be available offline |
// successful: boolean? |
// Whether our downloading was successful or not. |
// If not present, defaults to true. |
// errorMessage: String? |
// If unsuccessful, a message explaining why |
if(typeof successful == "undefined"){ |
successful = true; |
} |
|
if(!successful){ |
this.successful = false; |
this.details.push(errorMessage); |
this.error = true; |
} |
|
this.finished(); |
}, |
|
start: function(){ /* void */ |
// summary: |
// For advanced usage; most developers can ignore this. |
// Called at the start of the syncing process. Advanced |
// developers can over-ride this method to use their |
// own sync mechanism to start syncing. |
|
if(this.cancelled){ |
this.finished(); |
return; |
} |
this.onSync("start"); |
this.refreshFiles(); |
}, |
|
refreshFiles: function(){ /* void */ |
// summary: |
// For advanced usage; most developers can ignore this. |
// Called when we are going to refresh our list |
// of offline files during syncing. Advanced developers |
// can over-ride this method to do some advanced magic related to |
// refreshing files. |
|
//dojo.debug("refreshFiles"); |
if(this.cancelled){ |
this.finished(); |
return; |
} |
|
this.onSync("refreshFiles"); |
|
dojox.off.files.refresh(dojo.hitch(this, function(error, errorMessages){ |
if(error){ |
this.error = true; |
this.successful = false; |
for(var i = 0; i < errorMessages.length; i++){ |
this.details.push(errorMessages[i]); |
} |
|
// even if we get an error while syncing files, |
// keep syncing so we can upload and download |
// data |
} |
|
this.upload(); |
})); |
}, |
|
upload: function(){ /* void */ |
// summary: |
// For advanced usage; most developers can ignore this. |
// Called when syncing wants to upload data. Advanced |
// developers can over-ride this method to completely |
// throw away the Action Log and replaying system |
// and roll their own advanced sync mechanism if needed. |
|
if(this.cancelled){ |
this.finished(); |
return; |
} |
|
this.onSync("upload"); |
|
// when we are done uploading start downloading |
dojo.connect(this.actions, "onReplayFinished", this, this.download); |
|
// replay the actions log |
this.actions.replay(); |
}, |
|
download: function(){ /* void */ |
// summary: |
// For advanced usage; most developers can ignore this. |
// Called when syncing wants to download data. Advanced |
// developers can over-ride this method to use their |
// own sync mechanism. |
|
if(this.cancelled){ |
this.finished(); |
return; |
} |
|
// apps should respond to the "download" |
// event to download their data; when done |
// they must call dojox.off.sync.finishedDownloading() |
this.onSync("download"); |
}, |
|
finished: function(){ /* void */ |
// summary: |
// For advanced usage; most developers can ignore this. |
// Called when syncing is finished. Advanced |
// developers can over-ride this method to clean |
// up after finishing their own sync |
// mechanism they might have rolled. |
this.isSyncing = false; |
|
this.successful = (!this.cancelled && !this.error); |
|
this.onSync("finished"); |
}, |
|
_save: function(callback){ |
this.actions._save(function(){ |
callback(); |
}); |
}, |
|
_load: function(callback){ |
this.actions._load(function(){ |
callback(); |
}); |
} |
}); |
|
|
// summary: |
// A class that records actions taken by a user when they are offline, |
// suitable for replaying when the network reappears. |
// description: |
// The basic idea behind this method is to record user actions that would |
// normally have to contact a server into an action log when we are |
// offline, so that later when we are online we can simply replay this log |
// in the order user actions happened so that they can be executed against |
// the server, causing synchronization to happen. |
// |
// When we replay, for each of the actions that were added, we call a |
// method named onReplay that applications should connect to and |
// which will be called over and over for each of our actions -- |
// applications should take the offline action |
// information and use it to talk to a server to have this action |
// actually happen online, 'syncing' themselves with the server. |
// |
// For example, if the action was "update" with the item that was updated, we |
// might call some RESTian server API that exists for updating an item in |
// our application. The server could either then do sophisticated merging |
// and conflict resolution on the server side, for example, allowing you |
// to pop up a custom merge UI, or could do automatic merging or nothing |
// of the sort. When you are finished with this particular action, your |
// application is then required to call continueReplay() on the actionLog object |
// passed to onReplay() to continue replaying the action log, or haltReplay() |
// with the reason for halting to completely stop the syncing/replaying |
// process. |
// |
// For example, imagine that we have a web application that allows us to add |
// contacts. If we are offline, and we update a contact, we would add an action; |
// imagine that the user has to click an Update button after changing the values |
// for a given contact: |
// |
// dojox.off.whenOffline(dojo.byId("updateButton"), "onclick", function(evt){ |
// // get the updated customer values |
// var customer = getCustomerValues(); |
// |
// // we are offline -- just record this action |
// var action = {name: "update", customer: customer}; |
// dojox.off.sync.actions.add(action) |
// |
// // persist this customer data into local storage as well |
// dojox.storage.put(customer.name, customer); |
// }) |
// |
// Then, when we go back online, the dojox.off.sync.actions.onReplay event |
// will fire over and over, once for each action that was recorded while offline: |
// |
// dojo.connect(dojox.off.sync.actions, "onReplay", function(action, actionLog){ |
// // called once for each action we added while offline, in the order |
// // they were added |
// if(action.name == "update"){ |
// var customer = action.customer; |
// |
// // call some network service to update this customer |
// dojo.xhrPost({ |
// url: "updateCustomer.php", |
// content: {customer: dojo.toJson(customer)}, |
// error: function(err){ |
// actionLog.haltReplay(err); |
// }, |
// load: function(data){ |
// actionLog.continueReplay(); |
// } |
// }) |
// } |
// }) |
// |
// Note that the actions log is always automatically persisted locally while using it, so |
// that if the user closes the browser or it crashes the actions will safely be stored |
// for later replaying. |
dojo.declare("dojox.off.sync.ActionLog", null, { |
// entries: Array |
// An array of our action entries, where each one is simply a custom |
// object literal that were passed to add() when this action entry |
// was added. |
entries: [], |
|
// reasonHalted: String |
// If we halted, the reason why |
reasonHalted: null, |
|
// isReplaying: boolean |
// If true, we are in the middle of replaying a command log; if false, |
// then we are not |
isReplaying: false, |
|
// autoSave: boolean |
// Whether we automatically save the action log after each call to |
// add(); defaults to true. For applications that are rapidly adding |
// many action log entries in a short period of time, it can be |
// useful to set this to false and simply call save() yourself when |
// you are ready to persist your command log -- otherwise performance |
// could be slow as the default action is to attempt to persist the |
// actions log constantly with calls to add(). |
autoSave: true, |
|
add: function(action /* Object */){ /* void */ |
// summary: |
// Adds an action to our action log |
// description: |
// This method will add an action to our |
// action log, later to be replayed when we |
// go from offline to online. 'action' |
// will be available when this action is |
// replayed and will be passed to onReplay. |
// |
// Example usage: |
// |
// dojox.off.sync.log.add({actionName: "create", itemType: "document", |
// {title: "Message", content: "Hello World"}}); |
// |
// The object literal is simply a custom object appropriate |
// for our application -- it can be anything that preserves the state |
// of a user action that will be executed when we go back online |
// and replay this log. In the above example, |
// "create" is the name of this action; "documents" is the |
// type of item this command is operating on, such as documents, contacts, |
// tasks, etc.; and the final argument is the document that was created. |
|
if(this.isReplaying){ |
throw "Programming error: you can not call " |
+ "dojox.off.sync.actions.add() while " |
+ "we are replaying an action log"; |
} |
|
this.entries.push(action); |
|
// save our updated state into persistent |
// storage |
if(this.autoSave){ |
this._save(); |
} |
}, |
|
onReplay: function(action /* Object */, |
actionLog /* dojox.off.sync.ActionLog */){ /* void */ |
// summary: |
// Called when we replay our log, for each of our action |
// entries. |
// action: Object |
// A custom object literal representing an action for this |
// application, such as |
// {actionName: "create", item: {title: "message", content: "hello world"}} |
// actionLog: dojox.off.sync.ActionLog |
// A reference to the dojox.off.sync.actions log so that developers |
// can easily call actionLog.continueReplay() or actionLog.haltReplay(). |
// description: |
// This callback should be connected to by applications so that |
// they can sync themselves when we go back online: |
// |
// dojo.connect(dojox.off.sync.actions, "onReplay", function(action, actionLog){ |
// // do something |
// }) |
// |
// When we replay our action log, this callback is called for each |
// of our action entries in the order they were added. The |
// 'action' entry that was passed to add() for this action will |
// also be passed in to onReplay, so that applications can use this information |
// to do their syncing, such as contacting a server web-service |
// to create a new item, for example. |
// |
// Inside the method you connected to onReplay, you should either call |
// actionLog.haltReplay(reason) if an error occurred and you would like to halt |
// action replaying or actionLog.continueReplay() to have the action log |
// continue replaying its log and proceed to the next action; |
// the reason you must call these is the action you execute inside of |
// onAction will probably be asynchronous, since it will be talking on |
// the network, and you should call one of these two methods based on |
// the result of your network call. |
}, |
|
length: function(){ /* Number */ |
// summary: |
// Returns the length of this |
// action log |
return this.entries.length; |
}, |
|
haltReplay: function(reason /* String */){ /* void */ |
// summary: Halts replaying this command log. |
// reason: String |
// The reason we halted. |
// description: |
// This method is called as we are replaying an action log; it |
// can be called from dojox.off.sync.actions.onReplay, for |
// example, for an application to indicate an error occurred |
// while replaying this action, halting further processing of |
// the action log. Note that any action log entries that |
// were processed before have their effects retained (i.e. |
// they are not rolled back), while the action entry that was |
// halted stays in our list of actions to later be replayed. |
if(!this.isReplaying){ |
return; |
} |
|
if(reason){ |
this.reasonHalted = reason.toString(); |
} |
|
// save the state of our action log, then |
// tell anyone who is interested that we are |
// done when we are finished saving |
if(this.autoSave){ |
var self = this; |
this._save(function(){ |
self.isReplaying = false; |
self.onReplayFinished(); |
}); |
}else{ |
this.isReplaying = false; |
this.onReplayFinished(); |
} |
}, |
|
continueReplay: function(){ /* void */ |
// summary: |
// Indicates that we should continue processing out list of |
// actions. |
// description: |
// This method is called by applications that have overridden |
// dojox.off.sync.actions.onReplay() to continue replaying our |
// action log after the application has finished handling the |
// current action. |
if(!this.isReplaying){ |
return; |
} |
|
// shift off the old action we just ran |
this.entries.shift(); |
|
// are we done? |
if(!this.entries.length){ |
// save the state of our action log, then |
// tell anyone who is interested that we are |
// done when we are finished saving |
if(this.autoSave){ |
var self = this; |
this._save(function(){ |
self.isReplaying = false; |
self.onReplayFinished(); |
}); |
return; |
}else{ |
this.isReplaying = false; |
this.onReplayFinished(); |
return; |
} |
} |
|
// get the next action |
var nextAction = this.entries[0]; |
this.onReplay(nextAction, this); |
}, |
|
clear: function(){ /* void */ |
// summary: |
// Completely clears this action log of its entries |
|
if(this.isReplaying){ |
return; |
} |
|
this.entries = []; |
|
// save our updated state into persistent |
// storage |
if(this.autoSave){ |
this._save(); |
} |
}, |
|
replay: function(){ /* void */ |
// summary: |
// For advanced usage; most developers can ignore this. |
// Replays all of the commands that have been |
// cached in this command log when we go back online; |
// onCommand will be called for each command we have |
|
if(this.isReplaying){ |
return; |
} |
|
this.reasonHalted = null; |
|
if(!this.entries.length){ |
this.onReplayFinished(); |
return; |
} |
|
this.isReplaying = true; |
|
var nextAction = this.entries[0]; |
this.onReplay(nextAction, this); |
}, |
|
// onReplayFinished: Function |
// For advanced usage; most developers can ignore this. |
// Called when we are finished replaying our commands; |
// called if we have successfully exhausted all of our |
// commands, or if an error occurred during replaying. |
// The default implementation simply continues the |
// synchronization process. Connect to this to register |
// for the event: |
// |
// dojo.connect(dojox.off.sync.actions, "onReplayFinished", |
// someFunc) |
onReplayFinished: function(){ |
}, |
|
toString: function(){ |
var results = ""; |
results += "["; |
|
for(var i = 0; i < this.entries.length; i++){ |
results += "{"; |
for(var j in this.entries[i]){ |
results += j + ": \"" + this.entries[i][j] + "\""; |
results += ", "; |
} |
results += "}, "; |
} |
|
results += "]"; |
|
return results; |
}, |
|
_save: function(callback){ |
if(!callback){ |
callback = function(){}; |
} |
|
try{ |
var self = this; |
var resultsHandler = function(status, key, message){ |
//console.debug("resultsHandler, status="+status+", key="+key+", message="+message); |
if(status == dojox.storage.FAILED){ |
dojox.off.onFrameworkEvent("save", |
{status: dojox.storage.FAILED, |
isCoreSave: true, |
key: key, |
value: message, |
namespace: dojox.off.STORAGE_NAMESPACE}); |
callback(); |
}else if(status == dojox.storage.SUCCESS){ |
callback(); |
} |
}; |
|
dojox.storage.put("actionlog", this.entries, resultsHandler, |
dojox.off.STORAGE_NAMESPACE); |
}catch(exp){ |
console.debug("dojox.off.sync._save: " + exp.message||exp); |
dojox.off.onFrameworkEvent("save", |
{status: dojox.storage.FAILED, |
isCoreSave: true, |
key: "actionlog", |
value: this.entries, |
namespace: dojox.off.STORAGE_NAMESPACE}); |
callback(); |
} |
}, |
|
_load: function(callback){ |
var entries = dojox.storage.get("actionlog", dojox.off.STORAGE_NAMESPACE); |
|
if(!entries){ |
entries = []; |
} |
|
this.entries = entries; |
|
callback(); |
} |
} |
); |
|
dojox.off.sync.actions = new dojox.off.sync.ActionLog(); |
|
} |