Source: lib/offline/db_engine.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.offline.DBEngine');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.offline.IStorageEngine');
  21. goog.require('shaka.util.Error');
  22. goog.require('shaka.util.Functional');
  23. goog.require('shaka.util.PublicPromise');
  24. /**
  25. * This manages all operations on an IndexedDB. This wraps all operations
  26. * in Promises. All Promises will resolve once the transaction has completed.
  27. * Depending on the browser, this may or may not be after the data is flushed
  28. * to disk. https://goo.gl/zMOeJc
  29. *
  30. * @struct
  31. * @constructor
  32. * @implements {shaka.offline.IStorageEngine}
  33. */
  34. shaka.offline.DBEngine = function() {
  35. goog.asserts.assert(
  36. shaka.offline.DBEngine.isSupported(),
  37. 'DBEngine should not be called when DBEngine is not supported');
  38. /** @private {IDBDatabase} */
  39. this.db_ = null;
  40. /** @private {!Array.<shaka.offline.DBEngine.Operation>} */
  41. this.operations_ = [];
  42. /** @private {!Object.<string, number>} */
  43. this.currentIdMap_ = {};
  44. };
  45. /**
  46. * @typedef {{
  47. * transaction: !IDBTransaction,
  48. * promise: !shaka.util.PublicPromise
  49. * }}
  50. *
  51. * @property {!IDBTransaction} transaction
  52. * The transaction that this operation is using.
  53. * @property {!shaka.util.PublicPromise} promise
  54. * The promise associated with the operation.
  55. */
  56. shaka.offline.DBEngine.Operation;
  57. /** @private {string} */
  58. shaka.offline.DBEngine.DB_NAME_ = 'shaka_offline_db';
  59. /** @private @const {number} */
  60. shaka.offline.DBEngine.DB_VERSION_ = 1;
  61. /**
  62. * Determines if the browsers supports IndexedDB.
  63. * @return {boolean}
  64. */
  65. shaka.offline.DBEngine.isSupported = function() {
  66. return window.indexedDB != null;
  67. };
  68. /**
  69. * Delete the database. There must be no open connections to the database.
  70. * @return {!Promise}
  71. */
  72. shaka.offline.DBEngine.deleteDatabase = function() {
  73. if (!window.indexedDB)
  74. return Promise.resolve();
  75. var request =
  76. window.indexedDB.deleteDatabase(shaka.offline.DBEngine.DB_NAME_);
  77. var p = new shaka.util.PublicPromise();
  78. request.onsuccess = function(event) {
  79. goog.asserts.assert(event.newVersion == null, 'Unexpected database update');
  80. p.resolve();
  81. };
  82. request.onerror = shaka.offline.DBEngine.onError_.bind(null, request, p);
  83. return p;
  84. };
  85. /** @override */
  86. shaka.offline.DBEngine.prototype.initialized = function() {
  87. return this.db_ != null;
  88. };
  89. /** @override */
  90. shaka.offline.DBEngine.prototype.init = function(storeMap, opt_retryCount) {
  91. goog.asserts.assert(!this.db_, 'Already initialized');
  92. return this.createConnection_(storeMap, opt_retryCount).then(function() {
  93. // For each store, get the next ID and store in the map.
  94. var stores = Object.keys(storeMap);
  95. return Promise.all(stores.map(function(store) {
  96. return this.getNextId_(store).then(function(id) {
  97. this.currentIdMap_[store] = id;
  98. }.bind(this));
  99. }.bind(this)));
  100. }.bind(this));
  101. };
  102. /** @override */
  103. shaka.offline.DBEngine.prototype.destroy = function() {
  104. return Promise.all(this.operations_.map(function(op) {
  105. try {
  106. // If the transaction is considered finished but has not called the
  107. // callbacks yet, it will still be in the list and this call will fail.
  108. // Simply ignore errors.
  109. op.transaction.abort();
  110. } catch (e) {}
  111. var Functional = shaka.util.Functional;
  112. return op.promise.catch(Functional.noop);
  113. })).then(function() {
  114. goog.asserts.assert(this.operations_.length == 0,
  115. 'All operations should have been closed');
  116. if (this.db_) {
  117. this.db_.close();
  118. this.db_ = null;
  119. }
  120. }.bind(this));
  121. };
  122. /** @override */
  123. shaka.offline.DBEngine.prototype.get = function(storeName, key) {
  124. /** @type {!IDBRequest} */
  125. var request;
  126. return this.createTransaction_(storeName, 'readonly', function(store) {
  127. request = store.get(key);
  128. }).then(function() { return request.result; });
  129. };
  130. /** @override */
  131. shaka.offline.DBEngine.prototype.forEach = function(storeName, callback) {
  132. return this.createTransaction_(storeName, 'readonly', function(store) {
  133. var request = store.openCursor();
  134. request.onsuccess = function(event) {
  135. var cursor = event.target.result;
  136. if (cursor) {
  137. callback(cursor.value);
  138. cursor.continue();
  139. }
  140. };
  141. });
  142. };
  143. /** @override */
  144. shaka.offline.DBEngine.prototype.insert = function(storeName, value) {
  145. return this.createTransaction_(storeName, 'readwrite', function(store) {
  146. store.put(value);
  147. });
  148. };
  149. /** @override */
  150. shaka.offline.DBEngine.prototype.remove = function(storeName, key) {
  151. return this.createTransaction_(storeName, 'readwrite', function(store) {
  152. store.delete(key);
  153. });
  154. };
  155. /** @override */
  156. shaka.offline.DBEngine.prototype.removeKeys = function(storeName,
  157. keys,
  158. opt_onKeyRemoved) {
  159. return this.createTransaction_(storeName, 'readwrite', function(store) {
  160. for (var i = 0; i < keys.length; i++) {
  161. var request = store.delete(keys[i]);
  162. request.onsuccess = opt_onKeyRemoved || function(event) { };
  163. }
  164. });
  165. };
  166. /** @override */
  167. shaka.offline.DBEngine.prototype.reserveId = function(storeName) {
  168. goog.asserts.assert(storeName in this.currentIdMap_,
  169. 'Store name must be passed to init()');
  170. return this.currentIdMap_[storeName]++;
  171. };
  172. /**
  173. * Gets the ID to start at.
  174. *
  175. * @param {string} storeName
  176. * @return {!Promise.<number>}
  177. * @private
  178. */
  179. shaka.offline.DBEngine.prototype.getNextId_ = function(storeName) {
  180. var id = 0;
  181. return this.createTransaction_(storeName, 'readonly', function(store) {
  182. var request = store.openCursor(null, 'prev');
  183. request.onsuccess = function(event) {
  184. var cursor = event.target.result;
  185. if (cursor) {
  186. id = cursor.key + 1;
  187. }
  188. };
  189. }).then(function() { return id; });
  190. };
  191. /**
  192. * Creates a new transaction for the given store name and calls |action| to
  193. * modify the store. The transaction will resolve or reject the promise
  194. * returned by this function.
  195. *
  196. * @param {string} storeName
  197. * @param {string} type
  198. * @param {!function(IDBObjectStore)} action
  199. *
  200. * @return {!Promise}
  201. * @private
  202. */
  203. shaka.offline.DBEngine.prototype.createTransaction_ = function(storeName,
  204. type,
  205. action) {
  206. goog.asserts.assert(this.db_, 'DBEngine must not be destroyed');
  207. goog.asserts.assert(type == 'readonly' || type == 'readwrite',
  208. 'Type must be "readonly" or "readwrite"');
  209. var op = {
  210. transaction: this.db_.transaction([storeName], type),
  211. promise: new shaka.util.PublicPromise()
  212. };
  213. op.transaction.oncomplete = (function(event) {
  214. this.closeOperation_(op);
  215. op.promise.resolve();
  216. }.bind(this));
  217. // We will see an onabort call via:
  218. // 1. request error -> transaction error -> transaction abort
  219. // 2. transaction commit fail -> transaction abort
  220. // As any transaction error will result in an abort, it is better to listen
  221. // for an abort so that we will catch all failed transaction operations.
  222. op.transaction.onabort = (function(event) {
  223. this.closeOperation_(op);
  224. shaka.offline.DBEngine.onError_(op.transaction, op.promise, event);
  225. }.bind(this));
  226. // We need to prevent default on the onerror event or else Firefox will
  227. // raise an error which will cause a karma failure. This will not stop the
  228. // onabort callback from firing.
  229. op.transaction.onerror = (function(event) {
  230. event.preventDefault();
  231. }.bind(this));
  232. var store = op.transaction.objectStore(storeName);
  233. action(store);
  234. this.operations_.push(op);
  235. return op.promise;
  236. };
  237. /**
  238. * Close an open operation.
  239. *
  240. * @param {!shaka.offline.DBEngine.Operation} op
  241. * @private
  242. */
  243. shaka.offline.DBEngine.prototype.closeOperation_ = function(op) {
  244. var i = this.operations_.indexOf(op);
  245. goog.asserts.assert(i >= 0, 'Operation must be in the list.');
  246. this.operations_.splice(i, 1);
  247. };
  248. /**
  249. * Creates a new connection to the database.
  250. *
  251. * On IE/Edge, it is possible for the database to not be deleted when the
  252. * success callback is fired. This means that when we delete the database and
  253. * immediately create a new connection, we will connect to the old database.
  254. * If this happens, we need to close the connection and retry.
  255. *
  256. * @see https://goo.gl/hOYJvN
  257. *
  258. * @param {!Object.<string, string>} storeMap
  259. * @param {number=} opt_retryCount
  260. * @return {!Promise}
  261. * @private
  262. */
  263. shaka.offline.DBEngine.prototype.createConnection_ = function(
  264. storeMap, opt_retryCount) {
  265. var DBEngine = shaka.offline.DBEngine;
  266. var indexedDB = window.indexedDB;
  267. var request = indexedDB.open(DBEngine.DB_NAME_, DBEngine.DB_VERSION_);
  268. var upgraded = false;
  269. var createPromise = new shaka.util.PublicPromise();
  270. request.onupgradeneeded = function(event) {
  271. upgraded = true;
  272. var db = event.target.result;
  273. goog.asserts.assert(event.oldVersion == 0,
  274. 'Must be upgrading from version 0');
  275. goog.asserts.assert(db.objectStoreNames.length == 0,
  276. 'Version 0 database should be empty');
  277. for (var name in storeMap) {
  278. db.createObjectStore(name, {keyPath: storeMap[name]});
  279. }
  280. };
  281. request.onsuccess = (function(event) {
  282. if (opt_retryCount && !upgraded) {
  283. event.target.result.close();
  284. shaka.log.info('Didn\'t get an upgrade event... trying again.');
  285. setTimeout(function() {
  286. var p = this.createConnection_(storeMap, opt_retryCount - 1);
  287. p.then(createPromise.resolve, createPromise.reject);
  288. }.bind(this), 1000);
  289. return;
  290. }
  291. goog.asserts.assert(opt_retryCount == undefined || upgraded,
  292. 'Should get upgrade event');
  293. this.db_ = event.target.result;
  294. createPromise.resolve();
  295. }.bind(this));
  296. request.onerror = DBEngine.onError_.bind(null, request, createPromise);
  297. return createPromise;
  298. };
  299. /**
  300. * Rejects the given Promise using the error fromt the transaction.
  301. *
  302. * @param {!IDBTransaction|!IDBRequest} errorSource
  303. * @param {!shaka.util.PublicPromise} promise
  304. * @param {Event} event
  305. * @private
  306. */
  307. shaka.offline.DBEngine.onError_ = function(errorSource, promise, event) {
  308. if (errorSource.error) {
  309. promise.reject(new shaka.util.Error(
  310. shaka.util.Error.Severity.CRITICAL,
  311. shaka.util.Error.Category.STORAGE,
  312. shaka.util.Error.Code.INDEXED_DB_ERROR, errorSource.error));
  313. } else {
  314. promise.reject(new shaka.util.Error(
  315. shaka.util.Error.Severity.CRITICAL,
  316. shaka.util.Error.Category.STORAGE,
  317. shaka.util.Error.Code.OPERATION_ABORTED));
  318. }
  319. // Firefox will raise an error which will cause a karma failure.
  320. event.preventDefault();
  321. };