Source: lib/offline/indexeddb/base_storage_cell.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.offline.indexeddb.BaseStorageCell');
  7. goog.require('shaka.offline.indexeddb.DBConnection');
  8. goog.require('shaka.util.Error');
  9. goog.requireType('shaka.offline.indexeddb.DBOperation');
  10. /**
  11. * indexeddb.StorageCellBase is a base class for all stores that use IndexedDB.
  12. *
  13. * @implements {shaka.extern.StorageCell}
  14. */
  15. shaka.offline.indexeddb.BaseStorageCell = class {
  16. /**
  17. * @param {IDBDatabase} connection
  18. * @param {string} segmentStore
  19. * @param {string} manifestStore
  20. */
  21. constructor(connection, segmentStore, manifestStore) {
  22. /** @protected {!shaka.offline.indexeddb.DBConnection} */
  23. this.connection_ = new shaka.offline.indexeddb.DBConnection(connection);
  24. /** @protected {string} */
  25. this.segmentStore_ = segmentStore;
  26. /** @protected {string} */
  27. this.manifestStore_ = manifestStore;
  28. }
  29. /** @override */
  30. destroy() {
  31. return this.connection_.destroy();
  32. }
  33. /** @override */
  34. hasFixedKeySpace() {
  35. // By default, all IDB stores are read-only. The latest one will need to
  36. // override this default to be read-write.
  37. return true;
  38. }
  39. /** @override */
  40. addSegments(segments) {
  41. // By default, reject all additions.
  42. return this.rejectAdd(this.segmentStore_);
  43. }
  44. /** @override */
  45. removeSegments(keys, onRemove) {
  46. return this.remove_(this.segmentStore_, keys, onRemove);
  47. }
  48. /** @override */
  49. async getSegments(keys) {
  50. const rawSegments = await this.get_(this.segmentStore_, keys);
  51. return rawSegments.map((s) => this.convertSegmentData(s));
  52. }
  53. /** @override */
  54. addManifests(manifests) {
  55. // By default, reject all additions.
  56. return this.rejectAdd(this.manifestStore_);
  57. }
  58. /** @override */
  59. updateManifest(key, manifest) {
  60. // By default, reject all updates.
  61. return this.rejectUpdate(this.manifestStore_);
  62. }
  63. /**
  64. * @param {number} key
  65. * @param {!shaka.extern.ManifestDB} manifest
  66. * @return {!Promise}
  67. * @protected
  68. */
  69. updateManifestImplementation(key, manifest) {
  70. const op = this.connection_.startReadWriteOperation(this.manifestStore_);
  71. const store = op.store();
  72. store.get(key).onsuccess = (e) => {
  73. store.put(manifest, key);
  74. };
  75. return op.promise();
  76. }
  77. /** @override */
  78. updateManifestExpiration(key, newExpiration) {
  79. const op = this.connection_.startReadWriteOperation(this.manifestStore_);
  80. const store = op.store();
  81. store.get(key).onsuccess = (e) => {
  82. const manifest = e.target.result;
  83. // If we can't find the value, then there is nothing for us to update.
  84. if (manifest) {
  85. manifest.expiration = newExpiration;
  86. store.put(manifest, key);
  87. }
  88. };
  89. return op.promise();
  90. }
  91. /** @override */
  92. removeManifests(keys, onRemove) {
  93. return this.remove_(this.manifestStore_, keys, onRemove);
  94. }
  95. /** @override */
  96. async getManifests(keys) {
  97. const rawManifests = await this.get_(this.manifestStore_, keys);
  98. return Promise.all(rawManifests.map((m) => this.convertManifest(m)));
  99. }
  100. /** @override */
  101. async getAllManifests() {
  102. /** @type {!shaka.offline.indexeddb.DBOperation} */
  103. const op = this.connection_.startReadOnlyOperation(this.manifestStore_);
  104. /** @type {!Map<number, shaka.extern.ManifestDB>} */
  105. const values = new Map();
  106. await op.forEachEntry(async (key, value) => {
  107. const manifest = await this.convertManifest(value);
  108. values.set(/** @type {number} */(key), manifest);
  109. });
  110. await op.promise();
  111. return values;
  112. }
  113. /**
  114. * @param {?} old
  115. * @return {shaka.extern.SegmentDataDB}
  116. * @protected
  117. */
  118. convertSegmentData(old) {
  119. // Conversion is specific to each subclass. By default, do nothing.
  120. return /** @type {shaka.extern.SegmentDataDB} */(old);
  121. }
  122. /**
  123. * @param {?} old
  124. * @return {!Promise<shaka.extern.ManifestDB>}
  125. * @protected
  126. */
  127. convertManifest(old) {
  128. // Conversion is specific to each subclass. By default, do nothing.
  129. return Promise.resolve(/** @type {shaka.extern.ManifestDB} */(old));
  130. }
  131. /**
  132. * @param {string} storeName
  133. * @return {!Promise}
  134. * @protected
  135. */
  136. rejectAdd(storeName) {
  137. return Promise.reject(new shaka.util.Error(
  138. shaka.util.Error.Severity.CRITICAL,
  139. shaka.util.Error.Category.STORAGE,
  140. shaka.util.Error.Code.NEW_KEY_OPERATION_NOT_SUPPORTED,
  141. 'Cannot add new value to ' + storeName));
  142. }
  143. /**
  144. * @param {string} storeName
  145. * @return {!Promise}
  146. * @protected
  147. */
  148. rejectUpdate(storeName) {
  149. return Promise.reject(new shaka.util.Error(
  150. shaka.util.Error.Severity.CRITICAL,
  151. shaka.util.Error.Category.STORAGE,
  152. shaka.util.Error.Code.MODIFY_OPERATION_NOT_SUPPORTED,
  153. 'Cannot modify values in ' + storeName));
  154. }
  155. /**
  156. * @param {string} storeName
  157. * @param {!Array<T>} values
  158. * @return {!Promise<!Array<number>>}
  159. * @template T
  160. * @protected
  161. */
  162. async add(storeName, values) {
  163. const op = this.connection_.startReadWriteOperation(storeName);
  164. const store = op.store();
  165. /** @type {!Array<number>} */
  166. const keys = [];
  167. // Write each segment out. When each request completes, the key will
  168. // be in |request.result| as can be seen in
  169. // https://w3c.github.io/IndexedDB/#key-generator-construct.
  170. for (const value of values) {
  171. const request = store.add(value);
  172. request.onsuccess = (event) => {
  173. const key = request.result;
  174. keys.push(key);
  175. };
  176. }
  177. // Wait until the operation completes or else |keys| will not be fully
  178. // populated.
  179. await op.promise();
  180. return keys;
  181. }
  182. /**
  183. * @param {string} storeName
  184. * @param {!Array<number>} keys
  185. * @param {function(number)} onRemove
  186. * @return {!Promise}
  187. * @private
  188. */
  189. remove_(storeName, keys, onRemove) {
  190. const op = this.connection_.startReadWriteOperation(storeName);
  191. const store = op.store();
  192. for (const key of keys) {
  193. store.delete(key).onsuccess = () => onRemove(key);
  194. }
  195. return op.promise();
  196. }
  197. /**
  198. * @param {string} storeName
  199. * @param {!Array<number>} keys
  200. * @return {!Promise<!Array<T>>}
  201. * @template T
  202. * @private
  203. */
  204. async get_(storeName, keys) {
  205. const op = this.connection_.startReadOnlyOperation(storeName);
  206. const store = op.store();
  207. const values = {};
  208. /** @type {!Array<number>} */
  209. const missing = [];
  210. // Use a map to store the objects so that we can reorder the results to
  211. // match the order of |keys|.
  212. for (const key of keys) {
  213. const request = store.get(key);
  214. request.onsuccess = () => {
  215. // Make sure a defined value was found. Indexeddb treats no-value found
  216. // as a success with an undefined result.
  217. if (request.result == undefined) {
  218. missing.push(key);
  219. }
  220. values[key] = request.result;
  221. };
  222. }
  223. // Wait until the operation completes or else values may be missing from
  224. // |values|. Use the original key list to convert the map to a list so that
  225. // the order will match.
  226. await op.promise();
  227. if (missing.length) {
  228. throw new shaka.util.Error(
  229. shaka.util.Error.Severity.CRITICAL,
  230. shaka.util.Error.Category.STORAGE,
  231. shaka.util.Error.Code.KEY_NOT_FOUND,
  232. 'Could not find values for ' + missing);
  233. }
  234. return keys.map((key) => values[key]);
  235. }
  236. };