Blame | Last modification | View Log | RSS feed
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 applicationsdojo.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 onlineactions: 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 sessionif(!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 whyif(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 downloadingdojo.connect(this.actions, "onReplayFinished", this, this.download);// replay the actions logthis.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 whyreasonHalted: null,// isReplaying: boolean// If true, we are in the middle of replaying a command log; if false,// then we are notisReplaying: 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// storageif(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 logreturn 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 savingif(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 ranthis.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 savingif(this.autoSave){var self = this;this._save(function(){self.isReplaying = false;self.onReplayFinished();});return;}else{this.isReplaying = false;this.onReplayFinished();return;}}// get the next actionvar nextAction = this.entries[0];this.onReplay(nextAction, this);},clear: function(){ /* void */// summary:// Completely clears this action log of its entriesif(this.isReplaying){return;}this.entries = [];// save our updated state into persistent// storageif(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 haveif(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();}