OfflineRest.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dojox.rpc.OfflineRest"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.rpc.OfflineRest"] = true;
  8. dojo.provide("dojox.rpc.OfflineRest");
  9. dojo.require("dojox.data.ClientFilter");
  10. dojo.require("dojox.rpc.Rest");
  11. dojo.require("dojox.storage");
  12. // summary:
  13. // Makes the REST service be able to store changes in local
  14. // storage so it can be used offline automatically.
  15. var Rest = dojox.rpc.Rest;
  16. var namespace = "dojox_rpc_OfflineRest";
  17. var loaded;
  18. var index = Rest._index;
  19. dojox.storage.manager.addOnLoad(function(){
  20. // now that we are loaded we need to save everything in the index
  21. loaded = dojox.storage.manager.available;
  22. for(var i in index){
  23. saveObject(index[i], i);
  24. }
  25. });
  26. var dontSave;
  27. function getStorageKey(key){
  28. // returns a key that is safe to use in storage
  29. return key.replace(/[^0-9A-Za-z_]/g,'_');
  30. }
  31. function saveObject(object,id){
  32. // save the object into local storage
  33. if(loaded && !dontSave && (id || (object && object.__id))){
  34. dojox.storage.put(
  35. getStorageKey(id||object.__id),
  36. typeof object=='object'?dojox.json.ref.toJson(object):object, // makeshift technique to determine if the object is json object or not
  37. function(){},
  38. namespace);
  39. }
  40. }
  41. function isNetworkError(error){
  42. // determine if the error was a network error and should be saved offline
  43. // or if it was a server error and not a result of offline-ness
  44. return error instanceof Error && (error.status == 503 || error.status > 12000 || !error.status); // TODO: Make the right error determination
  45. }
  46. function sendChanges(){
  47. // periodical try to save our dirty data
  48. if(loaded){
  49. var dirty = dojox.storage.get("dirty",namespace);
  50. if(dirty){
  51. for (var dirtyId in dirty){
  52. commitDirty(dirtyId,dirty);
  53. }
  54. }
  55. }
  56. }
  57. var OfflineRest;
  58. function sync(){
  59. OfflineRest.sendChanges();
  60. OfflineRest.downloadChanges();
  61. }
  62. var syncId = setInterval(sync,15000);
  63. dojo.connect(document, "ononline", sync);
  64. OfflineRest = dojox.rpc.OfflineRest = {
  65. turnOffAutoSync: function(){
  66. clearInterval(syncId);
  67. },
  68. sync: sync,
  69. sendChanges: sendChanges,
  70. downloadChanges: function(){
  71. },
  72. addStore: function(/*data-store*/store,/*query?*/baseQuery){
  73. // summary:
  74. // Adds a store to the monitored store for local storage
  75. // store:
  76. // Store to add
  77. // baseQuery:
  78. // This is the base query to should be used to load the items for
  79. // the store. Generally you want to load all the items that should be
  80. // available when offline.
  81. OfflineRest.stores.push(store);
  82. store.fetch({queryOptions:{cache:true},query:baseQuery,onComplete:function(results,args){
  83. store._localBaseResults = results;
  84. store._localBaseFetch = args;
  85. }});
  86. }
  87. };
  88. OfflineRest.stores = [];
  89. var defaultGet = Rest._get;
  90. Rest._get = function(service, id){
  91. // We specifically do NOT want the paging information to be used by the default handler,
  92. // this is because online apps want to minimize the data transfer,
  93. // but an offline app wants the opposite, as much data as possible transferred to
  94. // the client side
  95. try{
  96. // if we are reloading the application with local dirty data in an online environment
  97. // we want to make sure we save the changes first, so that we get up-to-date
  98. // information from the server
  99. sendChanges();
  100. if(window.navigator && navigator.onLine===false){
  101. // we force an error if we are offline in firefox, otherwise it will silently load it from the cache
  102. throw new Error();
  103. }
  104. var dfd = defaultGet(service, id);
  105. }catch(e){
  106. dfd = new dojo.Deferred();
  107. dfd.errback(e);
  108. }
  109. var sync = dojox.rpc._sync;
  110. dfd.addCallback(function(result){
  111. saveObject(result, service._getRequest(id).url);
  112. return result;
  113. });
  114. dfd.addErrback(function(error){
  115. if(loaded){
  116. // if the storage is loaded, we can go ahead and get the object out of storage
  117. if(isNetworkError(error)){
  118. var loadedObjects = {};
  119. // network error, load from local storage
  120. var byId = function(id,backup){
  121. if(loadedObjects[id]){
  122. return backup;
  123. }
  124. var result = dojo.fromJson(dojox.storage.get(getStorageKey(id),namespace)) || backup;
  125. loadedObjects[id] = result;
  126. for(var i in result){
  127. var val = result[i]; // resolve references if we can
  128. id = val && val.$ref;
  129. if (id){
  130. if(id.substring && id.substring(0,4) == "cid:"){
  131. // strip the cid scheme, we should be able to resolve it locally
  132. id = id.substring(4);
  133. }
  134. result[i] = byId(id,val);
  135. }
  136. }
  137. if (result instanceof Array){
  138. //remove any deleted items
  139. for (i = 0;i<result.length;i++){
  140. if (result[i]===undefined){
  141. result.splice(i--,1);
  142. }
  143. }
  144. }
  145. return result;
  146. };
  147. dontSave = true; // we don't want to be resaving objects when loading from local storage
  148. //TODO: Should this reuse something from dojox.rpc.Rest
  149. var result = byId(service._getRequest(id).url);
  150. if(!result){// if it is not found we have to just return the error
  151. return error;
  152. }
  153. dontSave = false;
  154. return result;
  155. }
  156. else{
  157. return error; // server error, let the error propagate
  158. }
  159. }
  160. else{
  161. if(sync){
  162. return new Error("Storage manager not loaded, can not continue");
  163. }
  164. // we are not loaded, so we need to defer until we are loaded
  165. dfd = new dojo.Deferred();
  166. dfd.addCallback(arguments.callee);
  167. dojox.storage.manager.addOnLoad(function(){
  168. dfd.callback();
  169. });
  170. return dfd;
  171. }
  172. });
  173. return dfd;
  174. };
  175. function changeOccurred(method, absoluteId, contentId, serializedContent, service){
  176. if(method=='delete'){
  177. dojox.storage.remove(getStorageKey(absoluteId),namespace);
  178. }
  179. else{
  180. // both put and post should store the actual object
  181. dojox.storage.put(getStorageKey(contentId), serializedContent, function(){
  182. },namespace);
  183. }
  184. var store = service && service._store;
  185. // record all the updated queries
  186. if(store){
  187. store.updateResultSet(store._localBaseResults, store._localBaseFetch);
  188. dojox.storage.put(getStorageKey(service._getRequest(store._localBaseFetch.query).url),dojox.json.ref.toJson(store._localBaseResults),function(){
  189. },namespace);
  190. }
  191. }
  192. dojo.addOnLoad(function(){
  193. dojo.connect(dojox.data, "restListener", function(message){
  194. var channel = message.channel;
  195. var method = message.event.toLowerCase();
  196. var service = dojox.rpc.JsonRest && dojox.rpc.JsonRest.getServiceAndId(channel).service;
  197. changeOccurred(
  198. method,
  199. channel,
  200. method == "post" ? channel + message.result.id : channel,
  201. dojo.toJson(message.result),
  202. service
  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. dojo.connect(index,"onLoad",saveObject);
  254. dojo.connect(index,"onUpdate",saveObject);
  255. }