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. updateManifestExpiration(key, newExpiration) {
  60. const op = this.connection_.startReadWriteOperation(this.manifestStore_);
  61. const store = op.store();
  62. store.get(key).onsuccess = (e) => {
  63. const manifest = e.target.result;
  64. // If we can't find the value, then there is nothing for us to update.
  65. if (manifest) {
  66. manifest.expiration = newExpiration;
  67. store.put(manifest, key);
  68. }
  69. };
  70. return op.promise();
  71. }
  72. /** @override */
  73. removeManifests(keys, onRemove) {
  74. return this.remove_(this.manifestStore_, keys, onRemove);
  75. }
  76. /** @override */
  77. async getManifests(keys) {
  78. const rawManifests = await this.get_(this.manifestStore_, keys);
  79. return Promise.all(rawManifests.map((m) => this.convertManifest(m)));
  80. }
  81. /** @override */
  82. async getAllManifests() {
  83. /** @type {!shaka.offline.indexeddb.DBOperation} */
  84. const op = this.connection_.startReadOnlyOperation(this.manifestStore_);
  85. /** @type {!Map.<number, shaka.extern.ManifestDB>} */
  86. const values = new Map();
  87. await op.forEachEntry(async (key, value) => {
  88. const manifest = await this.convertManifest(value);
  89. values.set(/** @type {number} */(key), manifest);
  90. });
  91. await op.promise();
  92. return values;
  93. }
  94. /**
  95. * @param {?} old
  96. * @return {shaka.extern.SegmentDataDB}
  97. * @protected
  98. */
  99. convertSegmentData(old) {
  100. // Conversion is specific to each subclass. By default, do nothing.
  101. return /** @type {shaka.extern.SegmentDataDB} */(old);
  102. }
  103. /**
  104. * @param {?} old
  105. * @return {!Promise.<shaka.extern.ManifestDB>}
  106. * @protected
  107. */
  108. convertManifest(old) {
  109. // Conversion is specific to each subclass. By default, do nothing.
  110. return Promise.resolve(/** @type {shaka.extern.ManifestDB} */(old));
  111. }
  112. /**
  113. * @param {string} storeName
  114. * @return {!Promise}
  115. * @protected
  116. */
  117. rejectAdd(storeName) {
  118. return Promise.reject(new shaka.util.Error(
  119. shaka.util.Error.Severity.CRITICAL,
  120. shaka.util.Error.Category.STORAGE,
  121. shaka.util.Error.Code.NEW_KEY_OPERATION_NOT_SUPPORTED,
  122. 'Cannot add new value to ' + storeName));
  123. }
  124. /**
  125. * @param {string} storeName
  126. * @param {!Array.<T>} values
  127. * @return {!Promise.<!Array.<number>>}
  128. * @template T
  129. * @protected
  130. */
  131. async add(storeName, values) {
  132. const op = this.connection_.startReadWriteOperation(storeName);
  133. const store = op.store();
  134. /** @type {!Array.<number>} */
  135. const keys = [];
  136. // Write each segment out. When each request completes, the key will
  137. // be in |request.result| as can be seen in
  138. // https://w3c.github.io/IndexedDB/#key-generator-construct.
  139. for (const value of values) {
  140. const request = store.add(value);
  141. request.onsuccess = (event) => {
  142. const key = request.result;
  143. keys.push(key);
  144. };
  145. }
  146. // Wait until the operation completes or else |keys| will not be fully
  147. // populated.
  148. await op.promise();
  149. return keys;
  150. }
  151. /**
  152. * @param {string} storeName
  153. * @param {!Array.<number>} keys
  154. * @param {function(number)} onRemove
  155. * @return {!Promise}
  156. * @private
  157. */
  158. remove_(storeName, keys, onRemove) {
  159. const op = this.connection_.startReadWriteOperation(storeName);
  160. const store = op.store();
  161. for (const key of keys) {
  162. store.delete(key).onsuccess = () => onRemove(key);
  163. }
  164. return op.promise();
  165. }
  166. /**
  167. * @param {string} storeName
  168. * @param {!Array.<number>} keys
  169. * @return {!Promise.<!Array.<T>>}
  170. * @template T
  171. * @private
  172. */
  173. async get_(storeName, keys) {
  174. const op = this.connection_.startReadOnlyOperation(storeName);
  175. const store = op.store();
  176. const values = {};
  177. /** @type {!Array.<number>} */
  178. const missing = [];
  179. // Use a map to store the objects so that we can reorder the results to
  180. // match the order of |keys|.
  181. for (const key of keys) {
  182. const request = store.get(key);
  183. request.onsuccess = () => {
  184. // Make sure a defined value was found. Indexeddb treats no-value found
  185. // as a success with an undefined result.
  186. if (request.result == undefined) {
  187. missing.push(key);
  188. }
  189. values[key] = request.result;
  190. };
  191. }
  192. // Wait until the operation completes or else values may be missing from
  193. // |values|. Use the original key list to convert the map to a list so that
  194. // the order will match.
  195. await op.promise();
  196. if (missing.length) {
  197. throw new shaka.util.Error(
  198. shaka.util.Error.Severity.CRITICAL,
  199. shaka.util.Error.Category.STORAGE,
  200. shaka.util.Error.Code.KEY_NOT_FOUND,
  201. 'Could not find values for ' + missing);
  202. }
  203. return keys.map((key) => values[key]);
  204. }
  205. };