123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- /*
- Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
- Available via Academic Free License >= 2.1 OR the modified BSD license.
- see: http://dojotoolkit.org/license for details
- */
- if(!dojo._hasResource["dojox.rpc.OfflineRest"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
- dojo._hasResource["dojox.rpc.OfflineRest"] = true;
- dojo.provide("dojox.rpc.OfflineRest");
- dojo.require("dojox.data.ClientFilter");
- dojo.require("dojox.rpc.Rest");
- dojo.require("dojox.storage");
- // summary:
- // Makes the REST service be able to store changes in local
- // storage so it can be used offline automatically.
- var Rest = dojox.rpc.Rest;
- var namespace = "dojox_rpc_OfflineRest";
- var loaded;
- var index = Rest._index;
- dojox.storage.manager.addOnLoad(function(){
- // now that we are loaded we need to save everything in the index
- loaded = dojox.storage.manager.available;
- for(var i in index){
- saveObject(index[i], i);
- }
- });
- var dontSave;
- function getStorageKey(key){
- // returns a key that is safe to use in storage
- return key.replace(/[^0-9A-Za-z_]/g,'_');
- }
- function saveObject(object,id){
- // save the object into local storage
-
- if(loaded && !dontSave && (id || (object && object.__id))){
- dojox.storage.put(
- getStorageKey(id||object.__id),
- typeof object=='object'?dojox.json.ref.toJson(object):object, // makeshift technique to determine if the object is json object or not
- function(){},
- namespace);
- }
- }
- function isNetworkError(error){
- // determine if the error was a network error and should be saved offline
- // or if it was a server error and not a result of offline-ness
- return error instanceof Error && (error.status == 503 || error.status > 12000 || !error.status); // TODO: Make the right error determination
- }
- function sendChanges(){
- // periodical try to save our dirty data
- if(loaded){
- var dirty = dojox.storage.get("dirty",namespace);
- if(dirty){
- for (var dirtyId in dirty){
- commitDirty(dirtyId,dirty);
- }
- }
- }
- }
- var OfflineRest;
- function sync(){
- OfflineRest.sendChanges();
- OfflineRest.downloadChanges();
- }
- var syncId = setInterval(sync,15000);
- dojo.connect(document, "ononline", sync);
- OfflineRest = dojox.rpc.OfflineRest = {
- turnOffAutoSync: function(){
- clearInterval(syncId);
- },
- sync: sync,
- sendChanges: sendChanges,
- downloadChanges: function(){
-
- },
- addStore: function(/*data-store*/store,/*query?*/baseQuery){
- // summary:
- // Adds a store to the monitored store for local storage
- // store:
- // Store to add
- // baseQuery:
- // This is the base query to should be used to load the items for
- // the store. Generally you want to load all the items that should be
- // available when offline.
- OfflineRest.stores.push(store);
- store.fetch({queryOptions:{cache:true},query:baseQuery,onComplete:function(results,args){
- store._localBaseResults = results;
- store._localBaseFetch = args;
- }});
-
- }
- };
- OfflineRest.stores = [];
- var defaultGet = Rest._get;
- Rest._get = function(service, id){
- // We specifically do NOT want the paging information to be used by the default handler,
- // this is because online apps want to minimize the data transfer,
- // but an offline app wants the opposite, as much data as possible transferred to
- // the client side
- try{
- // if we are reloading the application with local dirty data in an online environment
- // we want to make sure we save the changes first, so that we get up-to-date
- // information from the server
- sendChanges();
- if(window.navigator && navigator.onLine===false){
- // we force an error if we are offline in firefox, otherwise it will silently load it from the cache
- throw new Error();
- }
- var dfd = defaultGet(service, id);
- }catch(e){
- dfd = new dojo.Deferred();
- dfd.errback(e);
- }
- var sync = dojox.rpc._sync;
- dfd.addCallback(function(result){
- saveObject(result, service._getRequest(id).url);
- return result;
- });
- dfd.addErrback(function(error){
- if(loaded){
- // if the storage is loaded, we can go ahead and get the object out of storage
- if(isNetworkError(error)){
- var loadedObjects = {};
- // network error, load from local storage
- var byId = function(id,backup){
- if(loadedObjects[id]){
- return backup;
- }
- var result = dojo.fromJson(dojox.storage.get(getStorageKey(id),namespace)) || backup;
-
- loadedObjects[id] = result;
- for(var i in result){
- var val = result[i]; // resolve references if we can
- id = val && val.$ref;
- if (id){
- if(id.substring && id.substring(0,4) == "cid:"){
- // strip the cid scheme, we should be able to resolve it locally
- id = id.substring(4);
- }
- result[i] = byId(id,val);
- }
- }
- if (result instanceof Array){
- //remove any deleted items
- for (i = 0;i<result.length;i++){
- if (result[i]===undefined){
- result.splice(i--,1);
- }
- }
- }
- return result;
- };
- dontSave = true; // we don't want to be resaving objects when loading from local storage
- //TODO: Should this reuse something from dojox.rpc.Rest
- var result = byId(service._getRequest(id).url);
-
- if(!result){// if it is not found we have to just return the error
- return error;
- }
- dontSave = false;
- return result;
- }
- else{
- return error; // server error, let the error propagate
- }
- }
- else{
- if(sync){
- return new Error("Storage manager not loaded, can not continue");
- }
- // we are not loaded, so we need to defer until we are loaded
- dfd = new dojo.Deferred();
- dfd.addCallback(arguments.callee);
- dojox.storage.manager.addOnLoad(function(){
- dfd.callback();
- });
- return dfd;
- }
- });
- return dfd;
- };
- function changeOccurred(method, absoluteId, contentId, serializedContent, service){
- if(method=='delete'){
- dojox.storage.remove(getStorageKey(absoluteId),namespace);
- }
- else{
- // both put and post should store the actual object
- dojox.storage.put(getStorageKey(contentId), serializedContent, function(){
- },namespace);
- }
- var store = service && service._store;
- // record all the updated queries
- if(store){
- store.updateResultSet(store._localBaseResults, store._localBaseFetch);
- dojox.storage.put(getStorageKey(service._getRequest(store._localBaseFetch.query).url),dojox.json.ref.toJson(store._localBaseResults),function(){
- },namespace);
-
- }
-
- }
- dojo.addOnLoad(function(){
- dojo.connect(dojox.data, "restListener", function(message){
- var channel = message.channel;
- var method = message.event.toLowerCase();
- var service = dojox.rpc.JsonRest && dojox.rpc.JsonRest.getServiceAndId(channel).service;
- changeOccurred(
- method,
- channel,
- method == "post" ? channel + message.result.id : channel,
- dojo.toJson(message.result),
- service
- );
-
- });
- });
- //FIXME: Should we make changes after a commit to see if the server rejected the change
- // or should we come up with a revert mechanism?
- var defaultChange = Rest._change;
- Rest._change = function(method,service,id,serializedContent){
- if(!loaded){
- return defaultChange.apply(this,arguments);
- }
- var absoluteId = service._getRequest(id).url;
- changeOccurred(method, absoluteId, dojox.rpc.JsonRest._contentId, serializedContent, service);
- var dirty = dojox.storage.get("dirty",namespace) || {};
- if (method=='put' || method=='delete'){
- // these supersede so we can overwrite anything using this id
- var dirtyId = absoluteId;
- }
- else{
- dirtyId = 0;
- for (var i in dirty){
- if(!isNaN(parseInt(i))){
- dirtyId = i;
- }
- } // get the last dirtyId to make a unique id for non-idempotent methods
- dirtyId++;
- }
- dirty[dirtyId] = {method:method,id:absoluteId,content:serializedContent};
- return commitDirty(dirtyId,dirty);
- };
- function commitDirty(dirtyId, dirty){
- var dirtyItem = dirty[dirtyId];
- var serviceAndId = dojox.rpc.JsonRest.getServiceAndId(dirtyItem.id);
- var deferred = defaultChange(dirtyItem.method,serviceAndId.service,serviceAndId.id,dirtyItem.content);
- // add it to our list of dirty objects
- dirty[dirtyId] = dirtyItem;
- dojox.storage.put("dirty",dirty,function(){},namespace);
- deferred.addBoth(function(result){
- if (isNetworkError(result)){
- // if a network error (offlineness) was the problem, we leave it
- // dirty, and return to indicate successfulness
- return null;
- }
- // it was successful or the server rejected it, we remove it from the dirty list
- var dirty = dojox.storage.get("dirty",namespace) || {};
- delete dirty[dirtyId];
- dojox.storage.put("dirty",dirty,function(){},namespace);
- return result;
- });
- return deferred;
- }
-
- dojo.connect(index,"onLoad",saveObject);
- dojo.connect(index,"onUpdate",saveObject);
- }
|