1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5ChromeUtils.defineModuleGetter(
6  this,
7  "IndexedDB",
8  "resource://gre/modules/IndexedDB.jsm"
9);
10
11this.ActivityStreamStorage = class ActivityStreamStorage {
12  /**
13   * @param storeNames Array of strings used to create all the required stores
14   */
15  constructor({ storeNames, telemetry }) {
16    if (!storeNames) {
17      throw new Error("storeNames required");
18    }
19
20    this.dbName = "ActivityStream";
21    this.dbVersion = 3;
22    this.storeNames = storeNames;
23    this.telemetry = telemetry;
24  }
25
26  get db() {
27    return this._db || (this._db = this.createOrOpenDb());
28  }
29
30  /**
31   * Public method that binds the store required by the consumer and exposes
32   * the private db getters and setters.
33   *
34   * @param storeName String name of desired store
35   */
36  getDbTable(storeName) {
37    if (this.storeNames.includes(storeName)) {
38      return {
39        get: this._get.bind(this, storeName),
40        getAll: this._getAll.bind(this, storeName),
41        set: this._set.bind(this, storeName),
42      };
43    }
44
45    throw new Error(`Store name ${storeName} does not exist.`);
46  }
47
48  async _getStore(storeName) {
49    return (await this.db).objectStore(storeName, "readwrite");
50  }
51
52  _get(storeName, key) {
53    return this._requestWrapper(async () =>
54      (await this._getStore(storeName)).get(key)
55    );
56  }
57
58  _getAll(storeName) {
59    return this._requestWrapper(async () =>
60      (await this._getStore(storeName)).getAll()
61    );
62  }
63
64  _set(storeName, key, value) {
65    return this._requestWrapper(async () =>
66      (await this._getStore(storeName)).put(value, key)
67    );
68  }
69
70  _openDatabase() {
71    return IndexedDB.open(this.dbName, { version: this.dbVersion }, db => {
72      // If provided with array of objectStore names we need to create all the
73      // individual stores
74      this.storeNames.forEach(store => {
75        if (!db.objectStoreNames.contains(store)) {
76          this._requestWrapper(() => db.createObjectStore(store));
77        }
78      });
79    });
80  }
81
82  /**
83   * createOrOpenDb - Open a db (with this.dbName) if it exists.
84   *                  If it does not exist, create it.
85   *                  If an error occurs, deleted the db and attempt to
86   *                  re-create it.
87   * @returns Promise that resolves with a db instance
88   */
89  async createOrOpenDb() {
90    try {
91      const db = await this._openDatabase();
92      return db;
93    } catch (e) {
94      if (this.telemetry) {
95        this.telemetry.handleUndesiredEvent({ event: "INDEXEDDB_OPEN_FAILED" });
96      }
97      await IndexedDB.deleteDatabase(this.dbName);
98      return this._openDatabase();
99    }
100  }
101
102  async _requestWrapper(request) {
103    let result = null;
104    try {
105      result = await request();
106    } catch (e) {
107      if (this.telemetry) {
108        this.telemetry.handleUndesiredEvent({ event: "TRANSACTION_FAILED" });
109      }
110      throw e;
111    }
112
113    return result;
114  }
115};
116
117function getDefaultOptions(options) {
118  return { collapsed: !!options.collapsed };
119}
120
121const EXPORTED_SYMBOLS = ["ActivityStreamStorage", "getDefaultOptions"];
122