8398c9048d
code was modified slightly, so the code differs from the original downloadable 1.9.5 version
686 lines
No EOL
21 KiB
JavaScript
686 lines
No EOL
21 KiB
JavaScript
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(); |