title: Is there a GOOD way to use Angular.js and Indexed DB?
body: <p>So, I'm developing a javascript + api forum software. My point in this is so that you can read a forum while offline -- this involves HTML5's offline-storage. In particular, I would like to use IndexedDB as it seems to be the most promising for the future. I've gotten a good service/factory for fetching / temporarily storing the data, but IDDB is broken majorly. Does anybody have advice on how to go about this?</p>
  
  <p><em>edit</em> Also, for anybody who would like a hosted version, here it is on <a href="https://c9.io/tyler-elric/phoenix/workspace/index.html" rel="nofollow">cloud9</a>.</p>
  
  <pre><code>var angular = angular || {};
  (function(w){
      w.localStorage = w.localStorage||{};
      w.indexedDB = w.indexedDB || w.mozIndexedDB || w.webkitIndexedDB || w.msIndexedDB || null;
      w.IDBTransaction = w.IDBTransaction || w.webkitIDBTransaction || w.msIDBTransaction || null;
      w.IDBKeyRange = w.IDBKeyRange || w.webkitIDBKeyRange || w.msIDBKeyRange || null;
  })(this);
  
  angular.module("JSForumServices",[],function($provide){
      $provide.factory('ForumStorage',(function(){
          var service = {
              post:null,
              thread:null,
              board:null,
              cache:{},
              pending:{}
          };
          var fetch = (function(baseFunction,path,callback){
              if(path in service.pending)
                  return service.pending[path];
              var r=baseFunction();
              service.pending[path] = r;
              var ajaxRequest = new XMLHttpRequest();
              var cancelled = false;
              var dateRegex =
                /^(?:(Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s+)?(0[1-9]|[1-2]?[0-9]|3[01])\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(19[0-9]{2}|[2-9][0-9]{3})\s+(2[0-3]|[0-1][0-9]):([0-5][0-9])(?::(60|[0-5][0-9]))?\s+([-\+][0-9]{2}[0-5][0-9]|(?:UT|GMT|(?:E|C|M|P)(?:ST|DT)|[A-IK-Z]))(\s+|\(([^\(\)]+|\\\(|\\\))*\))*$/;
              ajaxRequest.onreadystatechange = (function(){
                  var readyState = ajaxRequest.readyState;
                  if(readyState==4&amp;&amp;(!cancelled)){
                      // Store the copy locally!
                      // Also, initiate the callback.
                      // This way if the storage fails,
                      // The application continues to work
                      // As expected.
                      var data = JSON.parse(ajaxRequest.responseText);
                      for(var k in data)
                          r[k] = data[k];
                      service.cache[path]={obj:r,modified:new Date()};
                      delete service.pending[path];
                      callback(r);
                  }
                  else if((path in service.cache)&amp;&amp;readyState&gt;=2){
                      var oldDate = service.cache[path].modified;
                      console.log("Cache'd copy for",path,"exists.",oldDate.toString());
                      var isMoreRecent = false;//Is the server-copy more recent?
                      var serverModifiedString = ajaxRequest.getResponseHeader("Last-Modified");
                      console.log(serverModifiedString);
                      var match = dateRegex.exec(serverModifiedString);
                      var serverModified = new Date();
                      serverModified.setDate(parseInt(match[2],10));
                      serverModified.setMonth({
                          "jan":0,
                          "feb":1,
                          "mar":2,
                          "apr":3,
                          "may":4,
                          "jun":5,
                          "jul":6,
                          "aug":7,
                          "sep":8,
                          "oct":9,
                          "nov":10,
                          "dec":11
                      }[match[3].toLowerCase()]);
                      serverModified.setYear(parseInt(match[4],10));
                      serverModified.setHours(parseInt(match[5],10));
                      serverModified.setMinutes(parseInt(match[6],10));
                      serverModified.setMilliseconds(parseInt(match[7],10));
                      isMoreRecent = serverModified &gt; oldDate;
                      if(!isMoreRecent&amp;&amp;(path in service.pending)){//sometimes this code may be slower than network speeds? Just to be safe, I guess.
                          cancelled=true;
                          var oldObject = service.cache[path].obj;
                          for(var key in oldObject)
                              r[key]=oldObject[key];
                          console.log("using a cache'd value.",r);
                          callback(r);
                          delete service.pending[path];
                          ajaxRequest.abort();//No need to waste more bandwidth!
                      }
                  }
              });
              ajaxRequest.open("GET",path,true);
              ajaxRequest.send();
              return r;
          });
          var ObjectFetch = (function(base,constr){
              var ret = (function(id,cb){
                  cb = cb||(function(){});
                  return fetch(constr,base+id+".json",cb);
              });
              return ret;
          });
          service.post = ObjectFetch("./post/",(function(){
              return {
                  id:"???",
                  author:"???",
                  content:"",
                  date:""
              };
          }));
          service.thread = ObjectFetch("./thread/",(function(){
              return {
                  id:"???",
                  title:"???",
                  posts:[],
                  tags:""
              };
          }));
          service.board = ObjectFetch("./board/",(function(){
              return {
                  id:"???",
                  title:"???",
                  threads:[],
                  tags:""
              };
          }));
          service.forum = ObjectFetch("./",(function(){
              return {
                  name:"Javascript Forum Software.",
                  boards:[]
              };
          }));
          if(window.indexedDB!==null){
              var postFetch = service.post;
              var threadFetch = service.thread;
              var boardFetch = service.board;
              (function(dbengine){
                  var boardsLoaded = false;
                  var threadsLoaded = false;
                  var postsLoaded = false;
                  var req = dbengine.open("forum",1);
                  req.onupgradeneeded = (function(e){
                     var db = e.target.result;
                     db.createObjectStore("post",{
                         keyPath:"id"
                     });
                     db.createObjectStore("thread",{
                         keyPath:"id"
                     });
                     db.createObjectStore("board",{
                         keyPath:"id"
                     });
                  });
                  req.onsuccess = (function(e){
                      service.database = e.target.result;
                      var loadData = service.database.transaction([
                          'board',
                          'thread',
                          'post'],'readwrite');
                      loadData.onsuccess = (function(ee){
                         var transaction = ee.target.result;
                         transaction.objectStore("board").openCursor().onsuccess=(function(e){
                             var cursor = e.target.result;
                             if(cursor===null){
                                 boardsLoaded = true;
                                 return;
                             }
                             var id = cursor.key;
                             var obj = cursor.value;
                             var lastModified = new Date();
                             if("lastModified" in obj){
                                 lastModified = obj.lastModified;
                             }
                             service.cache["./board/"+id.toString().toLowerCase()+".json"]={
                                 obj:obj,
                                 modified:lastModified
                             };
                         });
                         transaction.objectStore("thread").openCursor().onsuccess=(function(e){
                             var cursor = e.target.result;
                             if(cursor===null){
                                 threadsLoaded = true;
                                 return;
                             }
                             var id = cursor.key;
                             var obj = cursor.value;
                             var lastModified = new Date();
                             if("lastModified" in obj){
                                 lastModified = obj.lastModified;
                             }
                             service.cache["./thread/"+id.toString().toLowerCase()+".json"]={
                                 obj:obj,
                                 modified:lastModified
                             };
                         });
                         transaction.objectStore("post").openCursor().onsuccess=(function(e){
                             var cursor = e.target.result;
                             if(cursor===null){
                                 postsLoaded = true;
                                 return;
                             }
                             var id = cursor.key;
                             var obj = cursor.value;
                             var lastModified = new Date();
                             if("lastModified" in obj){
                                 lastModified = obj.lastModified;
                             }
                             service.cache["./post/"+id.toString().toLowerCase()+".json"]={
                                 obj:obj,
                                 modified:lastModified
                             };
                         });
                         service.post = (function(id,cb){
                              console.log("DDDDDAFF");
                              var trans = service.database.transaction(["post"],"readwrite");
                              trans.onsuccess = (function(e){
                                  var req = e.target.result.objectStore("post").get(id);
                                  req.onsuccess = (function(ee){
                                      cb(req.result);
                                  });
                                  req.onerror = (function(ee){
                                      console.log("HAAAA?!");
                                      postFetch(id,(function(post){
                                          e.target.result.objcetStore.save(post);
                                          cb(post);
                                      }));
                                  });
                              });
                              trans.onerror = (function(e){
                                  console.log("Error with IDDB:",e);
                                  threadFetch(id,cb);
                              });
                          });
                         service.thread = (function(id,cb){
                              var trans = service.database.transaction(["thread"],"readwrite");
                              trans.onsuccess = (function(e){
                                  var req = e.target.result.objectStore("thread").get(id);
                                  req.onsuccess = (function(ee){
                                      cb(req.result);
                                  });
                                  req.onerror = (function(ee){
                                      threadFetch(id,(function(post){
                                          e.target.result.objcetStore.save(post);
                                          cb(post);
                                      }));
                                  });
                              });
                              trans.onerror = (function(e){
                                  console.log("Error with IDDB:",e);
                                  postFetch(id,cb);
                              });
                          });
                         service.board = (function(id,cb){
                              var trans = service.database.transaction(["board"],"readwrite");
                              trans.onsuccess = (function(e){
                                  var req = e.target.result.objectStore("board").get(id);
                                  req.onsuccess = (function(ee){
                                      cb(req.result);
                                  });
                                  req.onerror = (function(ee){
                                      boardFetch(id,(function(post){
                                          e.target.result.objcetStore.save(post);
                                          cb(post);
                                      }));
                                  });
                              });
                              trans.onerror = (function(e){
                                  console.log("Error with IDDB:",e);
                                  boardFetch(id,cb);
                              });
                          });
                      });
                  });
              })(window.indexedDB);
          }
          return service;
      }));
  });
  
  angular.module('JSForum',["JSForumServices"]).config(
      ['$routeProvider',
      function($routeProvider){
      $routeProvider.when('/',{
          templateUrl:"forum.html",
          controller:ForumController
      });
      $routeProvider.when('/board/:id',{
          templateUrl:"board.html",
          controller:BoardController
      });
      $routeProvider.when('/thread/:id',{
          templateUrl:"thread.html",
          controller:ThreadController
      });
      $routeProvider.otherwise({redirectTo:"/"});
  }]);
  
  function ThreadController($scope,$routeParams,ForumStorage){
      $scope.id = $routeParams.id.toString().toLowerCase();
      $scope.thread = null;
      ForumStorage.thread($scope.id,(function(thread){
          $scope.thread = thread;
          $scope.$apply();
          var callback = (function(p){
              return (function(post){
                  $scope.thread.posts[p] = post;
                  $scope.$apply();
              });
          });
          for(var i=0;i&lt;thread.posts.length;i++)
              if(typeof(thread.posts[i].id) == 'undefined')
                  ForumStorage.post(thread.posts[i],callback(i));
          $scope.$apply();
      }));
  }
  
  function BoardController($scope,$routeParams,ForumStorage){
      $scope.id = $routeParams.id.toString().toLowerCase();
      $scope.board = null;
      ForumStorage.board($scope.id,(function(board){
          var callback = (function(p){
              return (function(thread){
                  $scope.board.threads[p] = thread;
                  $scope.$apply();
              });
          });
          $scope.board = board;
          console.log("Using board:",$scope.board);
          for(var i=0;i&lt;board.threads.length;i++)
              if(typeof(board.threads[i].id)=='undefined')
                  ForumStorage.thread(board.threads[i],callback(i));
          $scope.$apply();
      }));
  }
  
  function ForumController($scope,ForumStorage){
      $scope.name = localStorage.forumName||"Forum";
      $scope.boards = [];
      ForumStorage.forum("forum",(function(forum){
          document.title = $scope.name = localStorage.forumName = forum.name;
          $scope.boards = forum.boards;
          var callback=(function(p){
              return (function(o){
                  $scope.boards[p] = o;
                  $scope.$apply();
              });
          });
          for(var i=0;i&lt;$scope.boards.length;i++){
              if(typeof($scope.boards[i].id) == 'undefined')
                  ForumStorage.board(forum.boards[i],callback(i));
          }
          $scope.$apply();
      }));
  }
  </code></pre>