[483] | 1 | define("dojox/rpc/OfflineRest", ["dojo", "dojox", "dojox/data/ClientFilter", "dojox/rpc/Rest", "dojox/storage"], function(dojo, dojox) { |
---|
| 2 | |
---|
| 3 | var Rest = dojox.rpc.Rest; |
---|
| 4 | var namespace = "dojox_rpc_OfflineRest"; |
---|
| 5 | var loaded; |
---|
| 6 | var index = Rest._index; |
---|
| 7 | dojox.storage.manager.addOnLoad(function(){ |
---|
| 8 | // now that we are loaded we need to save everything in the index |
---|
| 9 | loaded = dojox.storage.manager.available; |
---|
| 10 | for(var i in index){ |
---|
| 11 | saveObject(index[i], i); |
---|
| 12 | } |
---|
| 13 | }); |
---|
| 14 | var dontSave; |
---|
| 15 | function getStorageKey(key){ |
---|
| 16 | // returns a key that is safe to use in storage |
---|
| 17 | return key.replace(/[^0-9A-Za-z_]/g,'_'); |
---|
| 18 | } |
---|
| 19 | function saveObject(object,id){ |
---|
| 20 | // save the object into local storage |
---|
| 21 | |
---|
| 22 | if(loaded && !dontSave && (id || (object && object.__id))){ |
---|
| 23 | dojox.storage.put( |
---|
| 24 | getStorageKey(id||object.__id), |
---|
| 25 | typeof object=='object'?dojox.json.ref.toJson(object):object, // makeshift technique to determine if the object is json object or not |
---|
| 26 | function(){}, |
---|
| 27 | namespace); |
---|
| 28 | } |
---|
| 29 | } |
---|
| 30 | function isNetworkError(error){ |
---|
| 31 | // determine if the error was a network error and should be saved offline |
---|
| 32 | // or if it was a server error and not a result of offline-ness |
---|
| 33 | return error instanceof Error && (error.status == 503 || error.status > 12000 || !error.status); // TODO: Make the right error determination |
---|
| 34 | } |
---|
| 35 | function sendChanges(){ |
---|
| 36 | // periodical try to save our dirty data |
---|
| 37 | if(loaded){ |
---|
| 38 | var dirty = dojox.storage.get("dirty",namespace); |
---|
| 39 | if(dirty){ |
---|
| 40 | for (var dirtyId in dirty){ |
---|
| 41 | commitDirty(dirtyId,dirty); |
---|
| 42 | } |
---|
| 43 | } |
---|
| 44 | } |
---|
| 45 | } |
---|
| 46 | var OfflineRest; |
---|
| 47 | function sync(){ |
---|
| 48 | OfflineRest.sendChanges(); |
---|
| 49 | OfflineRest.downloadChanges(); |
---|
| 50 | } |
---|
| 51 | var syncId = setInterval(sync,15000); |
---|
| 52 | dojo.connect(document, "ononline", sync); |
---|
| 53 | OfflineRest = dojox.rpc.OfflineRest = { |
---|
| 54 | // summary: |
---|
| 55 | // Makes the REST service be able to store changes in local |
---|
| 56 | // storage so it can be used offline automatically. |
---|
| 57 | |
---|
| 58 | turnOffAutoSync: function(){ |
---|
| 59 | clearInterval(syncId); |
---|
| 60 | }, |
---|
| 61 | sync: sync, |
---|
| 62 | sendChanges: sendChanges, |
---|
| 63 | downloadChanges: function(){ |
---|
| 64 | |
---|
| 65 | }, |
---|
| 66 | addStore: function(/*data-store*/store,/*query?*/baseQuery){ |
---|
| 67 | // summary: |
---|
| 68 | // Adds a store to the monitored store for local storage |
---|
| 69 | // store: |
---|
| 70 | // Store to add |
---|
| 71 | // baseQuery: |
---|
| 72 | // This is the base query to should be used to load the items for |
---|
| 73 | // the store. Generally you want to load all the items that should be |
---|
| 74 | // available when offline. |
---|
| 75 | OfflineRest.stores.push(store); |
---|
| 76 | store.fetch({queryOptions:{cache:true},query:baseQuery,onComplete:function(results,args){ |
---|
| 77 | store._localBaseResults = results; |
---|
| 78 | store._localBaseFetch = args; |
---|
| 79 | }}); |
---|
| 80 | |
---|
| 81 | } |
---|
| 82 | }; |
---|
| 83 | OfflineRest.stores = []; |
---|
| 84 | var defaultGet = Rest._get; |
---|
| 85 | Rest._get = function(service, id){ |
---|
| 86 | // We specifically do NOT want the paging information to be used by the default handler, |
---|
| 87 | // this is because online apps want to minimize the data transfer, |
---|
| 88 | // but an offline app wants the opposite, as much data as possible transferred to |
---|
| 89 | // the client side |
---|
| 90 | try{ |
---|
| 91 | // if we are reloading the application with local dirty data in an online environment |
---|
| 92 | // we want to make sure we save the changes first, so that we get up-to-date |
---|
| 93 | // information from the server |
---|
| 94 | sendChanges(); |
---|
| 95 | if(window.navigator && navigator.onLine===false){ |
---|
| 96 | // we force an error if we are offline in firefox, otherwise it will silently load it from the cache |
---|
| 97 | throw new Error(); |
---|
| 98 | } |
---|
| 99 | var dfd = defaultGet(service, id); |
---|
| 100 | }catch(e){ |
---|
| 101 | dfd = new dojo.Deferred(); |
---|
| 102 | dfd.errback(e); |
---|
| 103 | } |
---|
| 104 | var sync = dojox.rpc._sync; |
---|
| 105 | dfd.addCallback(function(result){ |
---|
| 106 | saveObject(result, service._getRequest(id).url); |
---|
| 107 | return result; |
---|
| 108 | }); |
---|
| 109 | dfd.addErrback(function(error){ |
---|
| 110 | if(loaded){ |
---|
| 111 | // if the storage is loaded, we can go ahead and get the object out of storage |
---|
| 112 | if(isNetworkError(error)){ |
---|
| 113 | var loadedObjects = {}; |
---|
| 114 | // network error, load from local storage |
---|
| 115 | var byId = function(id,backup){ |
---|
| 116 | if(loadedObjects[id]){ |
---|
| 117 | return backup; |
---|
| 118 | } |
---|
| 119 | var result = dojo.fromJson(dojox.storage.get(getStorageKey(id),namespace)) || backup; |
---|
| 120 | |
---|
| 121 | loadedObjects[id] = result; |
---|
| 122 | for(var i in result){ |
---|
| 123 | var val = result[i]; // resolve references if we can |
---|
| 124 | id = val && val.$ref; |
---|
| 125 | if (id){ |
---|
| 126 | if(id.substring && id.substring(0,4) == "cid:"){ |
---|
| 127 | // strip the cid scheme, we should be able to resolve it locally |
---|
| 128 | id = id.substring(4); |
---|
| 129 | } |
---|
| 130 | result[i] = byId(id,val); |
---|
| 131 | } |
---|
| 132 | } |
---|
| 133 | if (result instanceof Array){ |
---|
| 134 | //remove any deleted items |
---|
| 135 | for (i = 0;i<result.length;i++){ |
---|
| 136 | if (result[i]===undefined){ |
---|
| 137 | result.splice(i--,1); |
---|
| 138 | } |
---|
| 139 | } |
---|
| 140 | } |
---|
| 141 | return result; |
---|
| 142 | }; |
---|
| 143 | dontSave = true; // we don't want to be resaving objects when loading from local storage |
---|
| 144 | //TODO: Should this reuse something from dojox.rpc.Rest |
---|
| 145 | var result = byId(service._getRequest(id).url); |
---|
| 146 | |
---|
| 147 | if(!result){// if it is not found we have to just return the error |
---|
| 148 | return error; |
---|
| 149 | } |
---|
| 150 | dontSave = false; |
---|
| 151 | return result; |
---|
| 152 | } |
---|
| 153 | else{ |
---|
| 154 | return error; // server error, let the error propagate |
---|
| 155 | } |
---|
| 156 | } |
---|
| 157 | else{ |
---|
| 158 | if(sync){ |
---|
| 159 | return new Error("Storage manager not loaded, can not continue"); |
---|
| 160 | } |
---|
| 161 | // we are not loaded, so we need to defer until we are loaded |
---|
| 162 | dfd = new dojo.Deferred(); |
---|
| 163 | dfd.addCallback(arguments.callee); |
---|
| 164 | dojox.storage.manager.addOnLoad(function(){ |
---|
| 165 | dfd.callback(); |
---|
| 166 | }); |
---|
| 167 | return dfd; |
---|
| 168 | } |
---|
| 169 | }); |
---|
| 170 | return dfd; |
---|
| 171 | }; |
---|
| 172 | function changeOccurred(method, absoluteId, contentId, serializedContent, service){ |
---|
| 173 | if(method=='delete'){ |
---|
| 174 | dojox.storage.remove(getStorageKey(absoluteId),namespace); |
---|
| 175 | } |
---|
| 176 | else{ |
---|
| 177 | // both put and post should store the actual object |
---|
| 178 | dojox.storage.put(getStorageKey(contentId), serializedContent, function(){ |
---|
| 179 | },namespace); |
---|
| 180 | } |
---|
| 181 | var store = service && service._store; |
---|
| 182 | // record all the updated queries |
---|
| 183 | if(store){ |
---|
| 184 | store.updateResultSet(store._localBaseResults, store._localBaseFetch); |
---|
| 185 | dojox.storage.put(getStorageKey(service._getRequest(store._localBaseFetch.query).url),dojox.json.ref.toJson(store._localBaseResults),function(){ |
---|
| 186 | },namespace); |
---|
| 187 | |
---|
| 188 | } |
---|
| 189 | |
---|
| 190 | } |
---|
| 191 | dojo.addOnLoad(function(){ |
---|
| 192 | dojo.connect(dojox.data, "restListener", function(message){ |
---|
| 193 | var channel = message.channel; |
---|
| 194 | var method = message.event.toLowerCase(); |
---|
| 195 | var service = dojox.rpc.JsonRest && dojox.rpc.JsonRest.getServiceAndId(channel).service; |
---|
| 196 | changeOccurred( |
---|
| 197 | method, |
---|
| 198 | channel, |
---|
| 199 | method == "post" ? channel + message.result.id : channel, |
---|
| 200 | dojo.toJson(message.result), |
---|
| 201 | service |
---|
| 202 | ); |
---|
| 203 | |
---|
| 204 | }); |
---|
| 205 | }); |
---|
| 206 | //FIXME: Should we make changes after a commit to see if the server rejected the change |
---|
| 207 | // or should we come up with a revert mechanism? |
---|
| 208 | var defaultChange = Rest._change; |
---|
| 209 | Rest._change = function(method,service,id,serializedContent){ |
---|
| 210 | if(!loaded){ |
---|
| 211 | return defaultChange.apply(this,arguments); |
---|
| 212 | } |
---|
| 213 | var absoluteId = service._getRequest(id).url; |
---|
| 214 | changeOccurred(method, absoluteId, dojox.rpc.JsonRest._contentId, serializedContent, service); |
---|
| 215 | var dirty = dojox.storage.get("dirty",namespace) || {}; |
---|
| 216 | if (method=='put' || method=='delete'){ |
---|
| 217 | // these supersede so we can overwrite anything using this id |
---|
| 218 | var dirtyId = absoluteId; |
---|
| 219 | } |
---|
| 220 | else{ |
---|
| 221 | dirtyId = 0; |
---|
| 222 | for (var i in dirty){ |
---|
| 223 | if(!isNaN(parseInt(i))){ |
---|
| 224 | dirtyId = i; |
---|
| 225 | } |
---|
| 226 | } // get the last dirtyId to make a unique id for non-idempotent methods |
---|
| 227 | dirtyId++; |
---|
| 228 | } |
---|
| 229 | dirty[dirtyId] = {method:method,id:absoluteId,content:serializedContent}; |
---|
| 230 | return commitDirty(dirtyId,dirty); |
---|
| 231 | }; |
---|
| 232 | function commitDirty(dirtyId, dirty){ |
---|
| 233 | var dirtyItem = dirty[dirtyId]; |
---|
| 234 | var serviceAndId = dojox.rpc.JsonRest.getServiceAndId(dirtyItem.id); |
---|
| 235 | var deferred = defaultChange(dirtyItem.method,serviceAndId.service,serviceAndId.id,dirtyItem.content); |
---|
| 236 | // add it to our list of dirty objects |
---|
| 237 | dirty[dirtyId] = dirtyItem; |
---|
| 238 | dojox.storage.put("dirty",dirty,function(){},namespace); |
---|
| 239 | deferred.addBoth(function(result){ |
---|
| 240 | if (isNetworkError(result)){ |
---|
| 241 | // if a network error (offlineness) was the problem, we leave it |
---|
| 242 | // dirty, and return to indicate successfulness |
---|
| 243 | return null; |
---|
| 244 | } |
---|
| 245 | // it was successful or the server rejected it, we remove it from the dirty list |
---|
| 246 | var dirty = dojox.storage.get("dirty",namespace) || {}; |
---|
| 247 | delete dirty[dirtyId]; |
---|
| 248 | dojox.storage.put("dirty",dirty,function(){},namespace); |
---|
| 249 | return result; |
---|
| 250 | }); |
---|
| 251 | return deferred; |
---|
| 252 | } |
---|
| 253 | |
---|
| 254 | dojo.connect(index,"onLoad",saveObject); |
---|
| 255 | dojo.connect(index,"onUpdate",saveObject); |
---|
| 256 | |
---|
| 257 | return OfflineRest; |
---|
| 258 | }); |
---|