From e7587cc361d49fc0cade490810e28ee97925d38c Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Sat, 7 Apr 2018 19:06:24 -0700 Subject: [PATCH] Add a test util for dumping and restoring from IDB This makes it easy to dump and restore databases from IndexedDB. This, in turn, makes it easy for us to test that all database versions can be read. Before we close the backward compatibility issue that has plagued v2.3, we will add tests that use this utility to load DB snapshots from various older schemas and prove that they can still be read. This also adds dumps of all database versions to the assets folder, including a snapshot of a what our broken upgrade in v2.3.0 did to the v2 database schema. Issue #1248 Change-Id: If7e8995f50abbdee67e3fa93e79f07a49582c5e8 --- build/conformance.textproto | 3 + lib/util/string_utils.js | 17 +- lib/util/uint8array_utils.js | 4 +- test/test/assets/db-dump-v1.json | 1 + test/test/assets/db-dump-v2-broken.json | 1 + test/test/assets/db-dump-v2-clean.json | 1 + test/test/assets/db-dump-v3.json | 1 + test/test/util/canned_idb.js | 398 ++++++++++++++++++++++++ 8 files changed, 418 insertions(+), 8 deletions(-) create mode 100644 test/test/assets/db-dump-v1.json create mode 100644 test/test/assets/db-dump-v2-broken.json create mode 100644 test/test/assets/db-dump-v2-clean.json create mode 100644 test/test/assets/db-dump-v3.json create mode 100644 test/test/util/canned_idb.js diff --git a/build/conformance.textproto b/build/conformance.textproto index c91013695..e17083f5a 100644 --- a/build/conformance.textproto +++ b/build/conformance.textproto @@ -49,6 +49,9 @@ requirement { java_class: 'com.google.javascript.jscomp.ConformanceRules$BanUnknownThis' error_message: 'Failed to infer type of "this"; ' 'please be explicit or use bind!' + # Until we can get this rule updated for ES6 static methods + # (https://github.com/google/closure-compiler/issues/2880): + whitelist_regexp: 'test/test/util/canned_idb.js' } diff --git a/lib/util/string_utils.js b/lib/util/string_utils.js index 7a5d423e7..2cc58fe29 100644 --- a/lib/util/string_utils.js +++ b/lib/util/string_utils.js @@ -46,7 +46,7 @@ shaka.util.StringUtils.fromUTF8 = function(data) { } // http://stackoverflow.com/a/13691499 - let utf8 = shaka.util.StringUtils.fromCharCode_(uint8); + let utf8 = shaka.util.StringUtils.fromCharCode(uint8); // This converts each character in the string to an escape sequence. If the // character is in the ASCII range, it is not converted; otherwise it is // converted to a URI escape sequence. @@ -107,7 +107,7 @@ shaka.util.StringUtils.fromUTF16 = function(data, littleEndian, opt_noThrow) { for (let i = 0; i < length; i++) { arr[i] = dataView.getUint16(i * 2, littleEndian); } - return shaka.util.StringUtils.fromCharCode_(arr); + return shaka.util.StringUtils.fromCharCode(arr); }; @@ -185,15 +185,18 @@ shaka.util.StringUtils.toUTF8 = function(str) { /** * Creates a new string from the given array of char codes. * - * @param {!TypedArray} args + * Using String.fromCharCode.apply is risky because you can trigger stack errors + * on very large arrays. This breaks up the array into several pieces to avoid + * this. + * + * @param {!TypedArray} array * @return {string} - * @private */ -shaka.util.StringUtils.fromCharCode_ = function(args) { +shaka.util.StringUtils.fromCharCode = function(array) { let max = 16000; let ret = ''; - for (let i = 0; i < args.length; i += max) { - let subArray = args.subarray(i, i + max); + for (let i = 0; i < array.length; i += max) { + let subArray = array.subarray(i, i + max); ret += String.fromCharCode.apply(null, subArray); } diff --git a/lib/util/uint8array_utils.js b/lib/util/uint8array_utils.js index 7b7e9f48b..74e24f5a5 100644 --- a/lib/util/uint8array_utils.js +++ b/lib/util/uint8array_utils.js @@ -17,6 +17,8 @@ goog.provide('shaka.util.Uint8ArrayUtils'); +goog.require('shaka.util.StringUtils'); + /** * @namespace shaka.util.Uint8ArrayUtils @@ -36,7 +38,7 @@ goog.provide('shaka.util.Uint8ArrayUtils'); */ shaka.util.Uint8ArrayUtils.toBase64 = function(arr, opt_padding) { // btoa expects a "raw string" where each character is interpreted as a byte. - let bytes = String.fromCharCode.apply(null, arr); + let bytes = shaka.util.StringUtils.fromCharCode(arr); let padding = (opt_padding == undefined) ? true : opt_padding; let base64 = window.btoa(bytes).replace(/\+/g, '-').replace(/\//g, '_'); return padding ? base64 : base64.replace(/=*$/, ''); diff --git a/test/test/assets/db-dump-v1.json b/test/test/assets/db-dump-v1.json new file mode 100644 index 000000000..98abb8a31 --- /dev/null +++ b/test/test/assets/db-dump-v1.json @@ -0,0 +1 @@ +{"version":1,"stores":{"manifest":{"parameters":{"keyPath":"key","autoIncrement":false},"data":[{"value":{"key":0,"originalManifestUri":"//storage.googleapis.com/shaka-demo-assets/heliocentrism/heliocentrism.mpd","duration":4.904831,"size":456654,"expiration":null,"periods":[{"startTime":0,"streams":[{"id":5,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/mp4","codecs":"avc1.4d401f","frameRate":29.97,"language":"und","label":null,"width":640,"height":480,"initSegmentUri":"offline:0/5/1","encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":2.06874,"uri":"offline:0/5/0"}],"variantIds":[13]}]},{"startTime":2.06874,"streams":[{"id":24,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/mp4","codecs":"avc1.4d401f","frameRate":29.97,"language":"und","label":null,"width":640,"height":480,"initSegmentUri":"offline:0/24/3","encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":2.13539,"uri":"offline:0/24/2"}],"variantIds":[32]}]},{"startTime":4.20413,"streams":[{"id":35,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/webm","codecs":"vp9","frameRate":30.303030303030305,"language":"und","label":null,"width":320,"height":240,"initSegmentUri":"offline:0/35/5","encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":0.700701,"uri":"offline:0/35/4"}],"variantIds":[39]}]}],"sessionIds":[],"drmInfo":null,"appMetadata":{"name":"[OFFLINE] Heliocentrism (multicodec, multiperiod)"}}}]},"segment":{"parameters":{"keyPath":"key","autoIncrement":false},"data":[{"value":{"key":0,"data":{"__type__":"ArrayBuffer","__value__":""},"manifestKey":0,"streamNumber":5,"segmentNumber":0}},{"value":{"key":1,"data":{"__type__":"ArrayBuffer","__value__":""},"manifestKey":0,"streamNumber":5,"segmentNumber":-1}},{"value":{"key":2,"data":{"__type__":"ArrayBuffer","__value__":""},"manifestKey":0,"streamNumber":24,"segmentNumber":2}},{"value":{"key":3,"data":{"__type__":"ArrayBuffer","__value__":""},"manifestKey":0,"streamNumber":24,"segmentNumber":-1}},{"value":{"key":4,"data":{"__type__":"ArrayBuffer","__value__":""},"manifestKey":0,"streamNumber":35,"segmentNumber":4}},{"value":{"key":5,"data":{"__type__":"ArrayBuffer","__value__":""},"manifestKey":0,"streamNumber":35,"segmentNumber":-1}}]}}} diff --git a/test/test/assets/db-dump-v2-broken.json b/test/test/assets/db-dump-v2-broken.json new file mode 100644 index 000000000..44ddbaea1 --- /dev/null +++ b/test/test/assets/db-dump-v2-broken.json @@ -0,0 +1 @@ +{"version":2,"stores":{"manifest":{"parameters":{"keyPath":"key","autoIncrement":false},"data":[]},"segment":{"parameters":{"keyPath":"key","autoIncrement":false},"data":[]},"manifest-v2":{"parameters":{"keyPath":null,"autoIncrement":false},"data":[{"value":{"originalManifestUri":"//storage.googleapis.com/shaka-demo-assets/heliocentrism/heliocentrism.mpd","duration":4.904831,"size":456654,"expiration":null,"periods":[{"startTime":0,"streams":[{"id":5,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/mp4","codecs":"avc1.4d401f","frameRate":29.97,"language":"und","label":null,"width":640,"height":480,"initSegmentKey":1,"encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":2.06874,"dataKey":0}],"variantIds":[13]}]},{"startTime":2.06874,"streams":[{"id":24,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/mp4","codecs":"avc1.4d401f","frameRate":29.97,"language":"und","label":null,"width":640,"height":480,"initSegmentKey":3,"encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":2.13539,"dataKey":2}],"variantIds":[32]}]},{"startTime":4.20413,"streams":[{"id":35,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/webm","codecs":"vp9","frameRate":30.303030303030305,"language":"und","label":null,"width":320,"height":240,"initSegmentKey":5,"encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":0.700701,"dataKey":4}],"variantIds":[39]}]}],"sessionIds":[],"drmInfo":null,"appMetadata":{"name":"[OFFLINE] Heliocentrism (multicodec, multiperiod)"}},"key":0}]},"segment-v2":{"parameters":{"keyPath":null,"autoIncrement":false},"data":[{"value":{"data":{"__type__":"ArrayBuffer","__value__":""}},"key":0},{"value":{"data":{"__type__":"ArrayBuffer","__value__":""}},"key":1},{"value":{"data":{"__type__":"ArrayBuffer","__value__":""}},"key":2},{"value":{"data":{"__type__":"ArrayBuffer","__value__":""}},"key":3},{"value":{"data":{"__type__":"ArrayBuffer","__value__":""}},"key":4},{"value":{"data":{"__type__":"ArrayBuffer","__value__":""}},"key":5}]}}} diff --git a/test/test/assets/db-dump-v2-clean.json b/test/test/assets/db-dump-v2-clean.json new file mode 100644 index 000000000..463ecfbda --- /dev/null +++ b/test/test/assets/db-dump-v2-clean.json @@ -0,0 +1 @@ +{"version":2,"stores":{"manifest-v2":{"parameters":{"keyPath":null,"autoIncrement":true},"data":[{"key":1,"value":{"originalManifestUri":"//storage.googleapis.com/shaka-demo-assets/heliocentrism/heliocentrism.mpd","duration":4.904831,"size":456654,"expiration":null,"periods":[{"startTime":0,"streams":[{"id":5,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/mp4","codecs":"avc1.4d401f","frameRate":29.97,"language":"und","label":null,"width":640,"height":480,"initSegmentKey":1,"encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":2.06874,"dataKey":3}],"variantIds":[13]}]},{"startTime":2.06874,"streams":[{"id":24,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/mp4","codecs":"avc1.4d401f","frameRate":29.97,"language":"und","label":null,"width":640,"height":480,"initSegmentKey":2,"encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":2.13539,"dataKey":5}],"variantIds":[32]}]},{"startTime":4.20413,"streams":[{"id":35,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/webm","codecs":"vp9","frameRate":30.303030303030305,"language":"und","label":null,"width":320,"height":240,"initSegmentKey":4,"encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":0.700701,"dataKey":6}],"variantIds":[39]}]}],"sessionIds":[],"drmInfo":null,"appMetadata":{"name":"[OFFLINE] Heliocentrism (multicodec, multiperiod)"}}}]},"segment-v2":{"parameters":{"keyPath":null,"autoIncrement":true},"data":[{"key":1,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":2,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":3,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":4,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":5,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":6,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}}]}}} diff --git a/test/test/assets/db-dump-v3.json b/test/test/assets/db-dump-v3.json new file mode 100644 index 000000000..2730d9a9d --- /dev/null +++ b/test/test/assets/db-dump-v3.json @@ -0,0 +1 @@ +{"version":3,"stores":{"manifest-v3":{"parameters":{"keyPath":null,"autoIncrement":true},"data":[{"key":1,"value":{"originalManifestUri":"//storage.googleapis.com/shaka-demo-assets/heliocentrism/heliocentrism.mpd","duration":4.904831,"size":456654,"expiration":null,"periods":[{"startTime":0,"streams":[{"id":5,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/mp4","codecs":"avc1.4d401f","frameRate":29.97,"language":"und","label":null,"width":640,"height":480,"initSegmentKey":1,"encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":2.06874,"dataKey":3}],"variantIds":[13]}]},{"startTime":2.06874,"streams":[{"id":24,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/mp4","codecs":"avc1.4d401f","frameRate":29.97,"language":"und","label":null,"width":640,"height":480,"initSegmentKey":2,"encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":2.13539,"dataKey":5}],"variantIds":[32]}]},{"startTime":4.20413,"streams":[{"id":35,"primary":false,"presentationTimeOffset":0,"contentType":"video","mimeType":"video/webm","codecs":"vp9","frameRate":30.303030303030305,"language":"und","label":null,"width":320,"height":240,"initSegmentKey":4,"encrypted":false,"keyId":null,"segments":[{"startTime":0,"endTime":0.700701,"dataKey":6}],"variantIds":[39]}]}],"sessionIds":[],"drmInfo":null,"appMetadata":{"name":"[OFFLINE] Heliocentrism (multicodec, multiperiod)"}}}]},"segment-v3":{"parameters":{"keyPath":null,"autoIncrement":true},"data":[{"key":1,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":2,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":3,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":4,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":5,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}},{"key":6,"value":{"data":{"__type__":"ArrayBuffer","__value__":""}}}]}}} diff --git a/test/test/util/canned_idb.js b/test/test/util/canned_idb.js new file mode 100644 index 000000000..a75686163 --- /dev/null +++ b/test/test/util/canned_idb.js @@ -0,0 +1,398 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('shaka.test.CannedIDB'); + +/** + * A testing utility that can be used to dump and restore entire IndexedDB + * databases. This can be inserted into a running app to snapshot databases + * for testing. + * + * @example + * shaka.test = shaka.test || {}; + * s = document.createElement('script'); + * s.src = '/shaka/test/test/util/canned_idb.js'; + * document.head.appendChild(s); + * dump = await shaka.test.CannedIDB.dumpJSON('shaka_offline_db', true); + */ +shaka.test.CannedIDB = class { + /** + * @param {string} name The name of the database to dump. + * @param {boolean=} dummyArrayBuffers If true, replace array buffer data with + * dummy data, as you might want for generating unit test data. Defaults to + * false. + * @return {!Promise.} A JSON string that can be used to recreate the + * database later in a call to restoreJSON(). + */ + static async dumpJSON(name, dummyArrayBuffers) { + const savedDatabase = await this.dump(name, dummyArrayBuffers); + const replacer = this.replacer_.bind(null, dummyArrayBuffers); + return JSON.stringify(savedDatabase, replacer); + } + + /** + * @param {string} name The name of the database to dump. + * @return {!Promise.} An object that can + * be used to recreate the database later in a call to restore(). + */ + static async dump(name) { + // Open the database, which should exist already. + const db = await this.openDatabase_(name); + + try { + const savedDatabase = { + version: db.version, + stores: {}, + }; + + // For each store, dump the store parameters and data. + const dumpOperations = []; + for (const storeName of db.objectStoreNames) { + dumpOperations.push(this.dumpStore_(db, storeName, savedDatabase)); + } + await Promise.all(dumpOperations); + + return savedDatabase; + } finally { + // Make sure the DB gets closed no matter what. + db.close(); + } + } + + /** + * @param {string} name The name of the database to restore. + * @param {string} savedDatabaseJson A JSON string containing the database + * definition and the data to populate it with. + * @param {boolean=} wipeDatabase If true, wipe the database before loading + * the saved data. If false, add stores and data, but keep any existing + * stores and data. Defaults to true. + * @return {!Promise} Resolved when the operation is complete. + */ + static async restoreJSON(name, savedDatabaseJson, wipeDatabase) { + const savedDatabase = JSON.parse(savedDatabaseJson, this.reviver_); + await this.restore(name, savedDatabase, wipeDatabase); + } + + /** + * @param {string} name The name of the database to restore. + * @param {shaka.test.CannedIDB.SavedDatabase} savedDatabase An object + * containing the database definition and the data to populate it with. + * @param {boolean=} wipeDatabase If true, wipe the database before loading + * the saved data. If false, add stores and data, but keep any existing + * stores and data. Defaults to true. + * @return {!Promise} Resolved when the operation is complete. + */ + static async restore(name, savedDatabase, wipeDatabase) { + wipeDatabase = (wipeDatabase == undefined) ? true : wipeDatabase; + + if (wipeDatabase) { + // Wipe out any existing data. + await this.deleteDatabase_(name); + } + + // Create a new DB, or open an existing one, and add the stores we need. + const db = await this.createDatabase_(name, savedDatabase); + + try { + // Populate it with data. + await this.populateDatabase_(db, savedDatabase); + } finally { + // Make sure the DB gets closed no matter what. + db.close(); + } + } + + /** + * A replacer callback for JSON.stringify. It creates special objects to + * represent types that can't be directly stringified into JSON, such as + * ArrayBuffer. Should be used with bind() to supply the dummyArrayBuffers + * argument. + * + * @param {boolean} dummyArrayBuffers + * @param {string} key + * @param {?} value + * @return {?} + * @private + */ + static replacer_(dummyArrayBuffers, key, value) { + if (value instanceof ArrayBuffer) { + /** @type {string} */ + let data; + if (dummyArrayBuffers) { + data = ''; + } else { + data = shaka.util.Uint8ArrayUtils.toBase64(new Uint8Array(value)); + } + return { + __type__: 'ArrayBuffer', + __value__: data, + }; + } + return value; + } + + /** + * A reviver callback for JSON.parse. It recognizes special objects from + * replacer_ and turns them back into their original format. + * + * @param {string} key + * @param {?} value + * @return {?} + * @private + */ + static reviver_(key, value) { + if (value && value.__type__ == 'ArrayBuffer') { + return shaka.util.Uint8ArrayUtils.fromBase64(value.__value__).buffer; + } + return value; + } + + /** + * @param {string} name The name of the database to open. + * @return {!Promise.} Resolved when the named DB has been + * opened. + * @private + */ + static openDatabase_(name) { + return new Promise((resolve, reject) => { + const request = indexedDB.open(name); + + request.onupgradeneeded = (event) => { + reject(new Error('DB did not exist!')); + const transaction = event.target.transaction; + transaction.abort(); + }; + + request.onsuccess = (event) => { + /** @type {IDBDatabase} */ + const db = event.target.result; + resolve(db); + }; + + request.onerror = (event) => { + event.preventDefault(); + reject(request.error); + }; + }); + } + + /** + * @param {IDBDatabase} db An open database connection. + * @param {string} name The store name to dump. + * @param {shaka.test.CannedIDB.SavedDatabase} savedDatabase An object where + * we write the store parameters and the data from the named store. + * @return {!Promise} Resolved when the store has been written to + * savedDatabase.stores. + * @private + */ + static dumpStore_(db, name, savedDatabase) { + return new Promise((resolve, reject) => { + const transactionType = 'readonly'; + const transaction = db.transaction([name], transactionType); + const store = transaction.objectStore(name); + shaka.log.debug('Dumping store', name); + + /** @type {shaka.test.CannedIDB.SavedStore} */ + const savedStore = { + parameters: { + keyPath: store.keyPath, + autoIncrement: store.autoIncrement, + }, + data: [], + }; + + const request = store.openCursor(); + + request.onsuccess = (event) => { + /** @type {IDBCursorWithValue} */ + const cursor = event.target.result; + if (!cursor) { + // No more data. + return; + } + + // Only store the key if there is no explicit keyPath for this store. + const data = {value: cursor.value}; + if (!store.keyPath) { + data.key = cursor.key; + } + savedStore.data.push(data); + + cursor.continue(); + }; + + transaction.oncomplete = (event) => { + shaka.log.debug('Dumped', savedStore.data.length, 'entries from store', + name); + savedDatabase.stores[name] = savedStore; + resolve(); + }; + + transaction.onerror = (event) => { + reject(event.error); + event.preventDefault(); + }; + }); + } + + /** + * @param {string} name The name of the database to delete. + * @return {!Promise} Resolved when the named DB has been deleted. + * @private + */ + static deleteDatabase_(name) { + return new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(name); + + request.onsuccess = (event) => { + resolve(); + }; + + request.onerror = (event) => { + event.preventDefault(); + resolve(); + }; + }); + } + + /** + * Creates a database. Does not populate it. + * + * @param {string} name The name of the database to create. + * @param {shaka.test.CannedIDB.SavedDatabase} savedDatabase An object + * containing the database definition and the data to populate it with. + * @return {!Promise.} Resolved when the named DB has been + * created. + * @private + */ + static createDatabase_(name, savedDatabase) { + return new Promise((resolve, reject) => { + const request = indexedDB.open(name, savedDatabase.version); + + request.onupgradeneeded = (event) => { + shaka.log.debug('DB upgrade from', event.oldVersion, 'to', + savedDatabase.version); + const transaction = event.target.transaction; + const db = transaction.db; + + // We will ignore existing stores, so first make a map of them. + const existingStoreMap = {}; + for (const storeName of db.objectStoreNames) { + existingStoreMap[storeName] = true; + } + + for (const storeName in savedDatabase.stores) { + if (storeName in existingStoreMap) { + shaka.log.debug('Ignoring existing store', storeName); + } else { + shaka.log.debug('Creating store', storeName); + const storeInfo = savedDatabase.stores[storeName]; + db.createObjectStore(storeName, storeInfo.parameters); + } + } + + // If there isn't an oncomplete on transaction, we seem not to get a + // call to request.onsuccess. Perhaps the browser blocks the + // transaction or request until there is an oncomplete handler. + transaction.oncomplete = (event) => {}; + }; + + request.onsuccess = (event) => { + /** @type {IDBDatabase} */ + const db = event.target.result; + resolve(db); + }; + + request.onerror = (event) => { + event.preventDefault(); + reject(request.error); + }; + }); + } + + /** + * @param {IDBDatabase} db An open database connection. + * @param {shaka.test.CannedIDB.SavedDatabase} savedDatabase An object + * containing the database definition and the data to populate it with. + * @return {!Promise} Resolved when the named DB has been populated with data. + * @private + */ + static populateDatabase_(db, savedDatabase) { + return new Promise((resolve, reject) => { + const transactionType = 'readwrite'; + const storeNames = Object.keys(savedDatabase.stores); + const transaction = db.transaction(storeNames, transactionType); + + for (const storeName in savedDatabase.stores) { + const store = transaction.objectStore(storeName); + const storeInfo = savedDatabase.stores[storeName]; + + shaka.log.debug('Populating store', storeName, 'with', + storeInfo.data.length, 'entries'); + storeInfo.data.forEach((item) => { + // If this store uses an explicit keyPath, we can't specify a key. + if (storeInfo.parameters.keyPath) { + store.add(item.value); + } else { + store.add(item.value, item.key); + } + }); + } + + transaction.oncomplete = (event) => { + resolve(); + }; + + transaction.onerror = (event) => { + reject(event.error); + event.preventDefault(); + }; + }); + } +}; + +/** + * @typedef {{ + * keyPath: ?, + * autoIncrement: boolean, + * }} + */ +shaka.test.CannedIDB.SavedStoreParameters; + +/** + * @typedef {{ + * key: ?, + * value: ?, + * }} + */ +shaka.test.CannedIDB.SavedStoreDataItem; + +/** + * @typedef {{ + * parameters: shaka.test.CannedIDB.SavedStoreParameters, + * data: !Array., + * }} + */ +shaka.test.CannedIDB.SavedStore; + +/** + * @typedef {{ + * version: number, + * stores: !Object., + * }} + */ +shaka.test.CannedIDB.SavedDatabase;