4 * Class: RemoteStorage.IndexedDB
10 * This file exposes a get/put/delete interface, accessing data in an indexedDB.
12 * There are multiple parts to this interface:
14 * The RemoteStorage integration:
15 * - RemoteStorage.IndexedDB._rs_supported() determines if indexedDB support
16 * is available. If it isn't, RemoteStorage won't initialize the feature.
17 * - RemoteStorage.IndexedDB._rs_init() initializes the feature. It returns
18 * a promise that is fulfilled as soon as the database has been opened and
21 * The storage interface (RemoteStorage.IndexedDB object):
22 * - Usually this is accessible via "remoteStorage.local"
23 * - #get() takes a path and returns a promise.
24 * - #put() takes a path, body and contentType and also returns a promise.
25 * In addition it also takes a 'incoming' flag, which indicates that the
26 * change is not fresh, but synchronized from remote.
27 * - #delete() takes a path and also returns a promise. It also supports
28 * the 'incoming' flag described for #put().
29 * - #on('change', ...) events, being fired whenever something changes in
30 * the storage. Change events roughly follow the StorageEvent pattern.
31 * They have "oldValue" and "newValue" properties, which can be used to
32 * distinguish create/update/delete operations and analyze changes in
33 * change handlers. In addition they carry a "origin" property, which
34 * is either "window", "local", or "remote". "remote" events are fired
35 * whenever the "incoming" flag is passed to #put() or #delete(). This
36 * is usually done by RemoteStorage.Sync.
38 * The revision interface (also on RemoteStorage.IndexedDB object):
39 * - #setRevision(path, revision) sets the current revision for the given
40 * path. Revisions are only generated by the remotestorage server, so
41 * this is usually done from RemoteStorage.Sync once a pending change
42 * has been pushed out.
43 * - #setRevisions(revisions) takes path/revision pairs in the form:
44 * [[path1, rev1], [path2, rev2], ...] and updates all revisions in a
46 * - #getRevision(path) returns the currently stored revision for the given
49 * The changes interface (also on RemoteStorage.IndexedDB object):
50 * - Used to record local changes between sync cycles.
51 * - Changes are stored in a separate ObjectStore called "changes".
52 * - #_recordChange() records a change and is called by #put() and #delete(),
53 * given the "incoming" flag evaluates to false. It is private and should
54 * never be used from the outside.
55 * - #changesBelow() takes a path and returns a promise that will be fulfilled
56 * with an Array of changes that are pending for the given path or below.
57 * This is usually done in a sync cycle to push out pending changes.
58 * - #clearChange removes the change for a given path. This is usually done
59 * RemoteStorage.Sync once a change has successfully been pushed out.
60 * - #setConflict sets conflict attributes on a change. It also fires the
62 * - #on('conflict', ...) event. Conflict events usually have the following
63 * attributes: path, localAction and remoteAction. Both actions are either
64 * "PUT" or "DELETE". They also bring a "resolve" method, which can be
65 * called with either of the strings "remote" and "local" to mark the
66 * conflict as resolved. The actual resolution will usually take place in
67 * the next sync cycle.
70 var RS = RemoteStorage;
72 var DEFAULT_DB_NAME = 'remotestorage';
75 function keepDirNode(node) {
76 return Object.keys(node.body).length > 0 ||
77 Object.keys(node.cached).length > 0;
80 function removeFromParent(nodes, path, key) {
81 var parts = path.match(/^(.*\/)([^\/]+\/?)$/);
83 var dirname = parts[1], basename = parts[2];
84 nodes.get(dirname).onsuccess = function(evt) {
85 var node = evt.target.result;
86 if (!node) {//attempt to remove something from a non-existing directory
89 delete node[key][basename];
90 if (keepDirNode(node)) {
93 nodes.delete(node.path).onsuccess = function() {
94 if (dirname !== '/') {
95 removeFromParent(nodes, dirname, key);
103 function makeNode(path) {
104 var node = { path: path };
105 if (path[path.length - 1] === '/') {
108 node.contentType = 'application/json';
113 function addToParent(nodes, path, key, revision) {
114 var parts = path.match(/^(.*\/)([^\/]+\/?)$/);
116 var dirname = parts[1], basename = parts[2];
117 nodes.get(dirname).onsuccess = function(evt) {
118 var node = evt.target.result || makeNode(dirname);
119 node[key][basename] = revision || true;
120 nodes.put(node).onsuccess = function() {
121 if (dirname !== '/') {
122 addToParent(nodes, dirname, key, true);
129 RS.IndexedDB = function(database) {
130 this.db = database || DEFAULT_DB;
132 RemoteStorage.log("Failed to open indexedDB");
135 RS.cachingLayer(this);
136 RS.eventHandling(this, 'change', 'conflict');
139 RS.IndexedDB.prototype = {
141 get: function(path) {
142 var promise = promising();
143 var transaction = this.db.transaction(['nodes'], 'readonly');
144 var nodes = transaction.objectStore('nodes');
145 var nodeReq = nodes.get(path);
148 nodeReq.onsuccess = function() {
149 node = nodeReq.result;
152 transaction.oncomplete = function() {
154 promise.fulfill(200, node.body, node.contentType, node.revision);
156 promise.fulfill(404);
160 transaction.onerror = transaction.onabort = promise.reject;
164 put: function(path, body, contentType, incoming, revision) {
165 var promise = promising();
166 if (path[path.length - 1] === '/') { throw "Bad: don't PUT folders"; }
167 var transaction = this.db.transaction(['nodes'], 'readwrite');
168 var nodes = transaction.objectStore('nodes');
172 nodes.get(path).onsuccess = function(evt) {
174 oldNode = evt.target.result;
177 contentType: contentType,
180 nodes.put(node).onsuccess = function() {
182 addToParent(nodes, path, 'body', revision);
184 if (typeof(done) === 'undefined') {
191 if (typeof(done) === 'undefined') {
198 transaction.oncomplete = function() {
199 this._emit('change', {
201 origin: incoming ? 'remote' : 'window',
202 oldValue: oldNode ? oldNode.body : undefined,
206 this._recordChange(path, { action: 'PUT', revision: oldNode ? oldNode.revision : undefined });
208 if (typeof(done) === 'undefined') {
210 promise.fulfill(200);
214 transaction.onerror = transaction.onabort = promise.reject;
218 delete: function(path, incoming) {
219 var promise = promising();
220 if (path[path.length - 1] === '/') { throw "Bad: don't DELETE folders"; }
221 var transaction = this.db.transaction(['nodes'], 'readwrite');
222 var nodes = transaction.objectStore('nodes');
225 nodes.get(path).onsuccess = function(evt) {
226 oldNode = evt.target.result;
227 nodes.delete(path).onsuccess = function() {
228 removeFromParent(nodes, path, 'body', incoming);
232 transaction.oncomplete = function() {
234 this._emit('change', {
236 origin: incoming ? 'remote' : 'window',
237 oldValue: oldNode.body,
242 this._recordChange(path, { action: 'DELETE', revision: oldNode ? oldNode.revision : undefined });
244 promise.fulfill(200);
247 transaction.onerror = transaction.onabort = promise.reject;
251 setRevision: function(path, revision) {
252 return this.setRevisions([[path, revision]]);
255 setRevisions: function(revs) {
256 var promise = promising();
257 var transaction = this.db.transaction(['nodes'], 'readwrite');
259 revs.forEach(function(rev) {
260 var nodes = transaction.objectStore('nodes');
261 nodes.get(rev[0]).onsuccess = function(event) {
262 var node = event.target.result || makeNode(rev[0]);
263 node.revision = rev[1];
264 nodes.put(node).onsuccess = function() {
265 addToParent(nodes, rev[0], 'cached', rev[1]);
270 transaction.oncomplete = function() {
274 transaction.onerror = transaction.onabort = promise.reject;
278 getRevision: function(path) {
279 var promise = promising();
280 var transaction = this.db.transaction(['nodes'], 'readonly');
283 transaction.objectStore('nodes').
284 get(path).onsuccess = function(evt) {
285 if (evt.target.result) {
286 rev = evt.target.result.revision;
290 transaction.oncomplete = function() {
291 promise.fulfill(rev);
294 transaction.onerror = transaction.onabort = promise.reject;
298 getCached: function(path) {
299 if (path[path.length - 1] !== '/') {
300 return this.get(path);
302 var promise = promising();
303 var transaction = this.db.transaction(['nodes'], 'readonly');
304 var nodes = transaction.objectStore('nodes');
306 nodes.get(path).onsuccess = function(evt) {
307 var node = evt.target.result || {};
308 promise.fulfill(200, node.cached, node.contentType, node.revision);
314 reset: function(callback) {
315 var dbName = this.db.name;
318 RS.IndexedDB.clean(this.db.name, function() {
319 RS.IndexedDB.open(dbName, function(other) {
327 fireInitial: function() {
328 var transaction = this.db.transaction(['nodes'], 'readonly');
329 var cursorReq = transaction.objectStore('nodes').openCursor();
330 cursorReq.onsuccess = function(evt) {
331 var cursor = evt.target.result;
333 var path = cursor.key;
334 if (path.substr(-1) !== '/') {
335 this._emit('change', {
339 newValue: cursor.value.body
347 _recordChange: function(path, attributes) {
348 var promise = promising();
349 var transaction = this.db.transaction(['changes'], 'readwrite');
350 var changes = transaction.objectStore('changes');
353 changes.get(path).onsuccess = function(evt) {
354 change = evt.target.result || {};
356 for(var key in attributes) {
357 change[key] = attributes[key];
362 transaction.oncomplete = promise.fulfill;
363 transaction.onerror = transaction.onabort = promise.reject;
367 clearChange: function(path) {
368 var promise = promising();
369 var transaction = this.db.transaction(['changes'], 'readwrite');
370 var changes = transaction.objectStore('changes');
371 changes.delete(path);
373 transaction.oncomplete = function() {
380 changesBelow: function(path) {
381 var promise = promising();
382 var transaction = this.db.transaction(['changes'], 'readonly');
383 var cursorReq = transaction.objectStore('changes').
384 openCursor(IDBKeyRange.lowerBound(path));
385 var pl = path.length;
388 cursorReq.onsuccess = function() {
389 var cursor = cursorReq.result;
391 if (cursor.key.substr(0, pl) === path) {
392 changes.push(cursor.value);
398 transaction.oncomplete = function() {
399 promise.fulfill(changes);
405 setConflict: function(path, attributes) {
406 var event = this._createConflictEvent(path, attributes);
407 this._recordChange(path, { conflict: attributes }).
409 // fire conflict once conflict has been recorded.
410 if (this._handlers.conflict.length > 0) {
411 this._emit('conflict', event);
413 setTimeout(function() { event.resolve('remote'); }, 0);
418 closeDB: function() {
426 RS.IndexedDB.open = function(name, callback) {
427 var timer = setTimeout(function() {
428 callback("timeout trying to open db");
431 var dbOpen = indexedDB.open(name, DB_VERSION);
433 dbOpen.onerror = function() {
434 RemoteStorage.log('opening db failed', dbOpen);
436 callback(dbOpen.error);
439 dbOpen.onupgradeneeded = function(event) {
440 RemoteStorage.log("[IndexedDB] Upgrade: from ", event.oldVersion, " to ", event.newVersion);
441 var db = dbOpen.result;
442 if (event.oldVersion !== 1) {
443 RemoteStorage.log("[IndexedDB] Creating object store: nodes");
444 db.createObjectStore('nodes', { keyPath: 'path' });
446 RemoteStorage.log("[IndexedDB] Creating object store: changes");
447 db.createObjectStore('changes', { keyPath: 'path' });
450 dbOpen.onsuccess = function() {
452 callback(null, dbOpen.result);
456 RS.IndexedDB.clean = function(databaseName, callback) {
457 var req = indexedDB.deleteDatabase(databaseName);
458 req.onsuccess = function() {
459 RemoteStorage.log('done removing db');
462 req.onerror = req.onabort = function(evt) {
463 console.error('failed to remove database "' + databaseName + '"', evt);
467 RS.IndexedDB._rs_init = function(remoteStorage) {
468 var promise = promising();
469 RS.IndexedDB.open(DEFAULT_DB_NAME, function(err, db) {
474 db.onerror = function() { remoteStorage._emit('error', err); };
482 RS.IndexedDB._rs_supported = function() {
483 return 'indexedDB' in global;
486 RS.IndexedDB._rs_cleanup = function(remoteStorage) {
487 if (remoteStorage.local) {
488 remoteStorage.local.closeDB();
490 var promise = promising();
491 RS.IndexedDB.clean(DEFAULT_DB_NAME, function() {
497 })(typeof(window) !== 'undefined' ? window : global);