summaryrefslogtreecommitdiffstats
path: root/toolkit/components/kvstore/kvstore.sys.mjs
blob: 838f68a5dfeb1c7a047536712bb26f2fd7cf8505 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const gKeyValueService = Cc["@mozilla.org/key-value-service;1"].getService(
  Ci.nsIKeyValueService
);

function promisify(fn, ...args) {
  return new Promise((resolve, reject) => {
    fn({ resolve, reject }, ...args);
  });
}

/**
 * This module wraps the nsIKeyValue* interfaces in a Promise-based API.
 * To use it, import it, then call the KeyValueService.getOrCreate() method
 * with a database's path and (optionally) its name:
 *
 * ```
 *     ChromeUtils.import("resource://gre/modules/kvstore.jsm");
 *     let database = await KeyValueService.getOrCreate(path, name);
 * ```
 *
 * See the documentation in nsIKeyValue.idl for more information about the API
 * for key/value storage.
 */

export class KeyValueService {
  static async getOrCreate(dir, name) {
    return new KeyValueDatabase(
      await promisify(gKeyValueService.getOrCreate, dir, name)
    );
  }
}

/**
 * A class that wraps an nsIKeyValueDatabase in a Promise-based API.
 *
 * This class isn't exported, so you can't instantiate it directly, but you
 * can retrieve an instance of this class via KeyValueService.getOrCreate():
 *
 * ```
 *     const database = await KeyValueService.getOrCreate(path, name);
 * ```
 *
 * You can then call its put(), get(), has(), and delete() methods to access
 * and manipulate key/value pairs:
 *
 * ```
 *     await database.put("foo", 1);
 *     await database.get("foo") === 1; // true
 *     await database.has("foo"); // true
 *     await database.delete("foo");
 *     await database.has("foo"); // false
 * ```
 *
 * You can also call writeMany() to put/delete multiple key/value pairs:
 *
 * ```
 *     await database.writeMany({
 *       key1: "value1",
 *       key2: "value2",
 *       key3: "value3",
 *       key4: null, // delete
 *     });
 * ```
 *
 * And you can call its enumerate() method to retrieve a KeyValueEnumerator,
 * which is described below.
 */
class KeyValueDatabase {
  constructor(database) {
    this.database = database;
  }

  put(key, value) {
    return promisify(this.database.put, key, value);
  }

  /**
   * Writes multiple key/value pairs to the database.
   *
   * Note:
   *   * Each write could be either put or delete.
   *   * Given multiple values with the same key, only the last value will be stored.
   *   * If the same key gets put and deleted for multiple times, the final state
   *     of that key is subject to the ordering of the put(s) and delete(s).
   *
   * @param pairs Pairs could be any of following types:
   *        * An Object, all its properties and the corresponding values will
   *          be used as key value pairs. A property with null or undefined indicating
   *          a deletion.
   *        * An Array or an iterable whose elements are key-value pairs. such as
   *          [["key1", "value1"], ["key2", "value2"]]. Use a pair with value null
   *          to delete a key-value pair, e.g. ["delete-key", null].
   *        * A Map. A key with null or undefined value indicating a deletion.
   * @return A promise that is fulfilled when all the key/value pairs are written
   *         to the database.
   */
  writeMany(pairs) {
    if (!pairs) {
      throw new Error("writeMany(): unexpected argument.");
    }

    let entries;

    if (
      pairs instanceof Map ||
      pairs instanceof Array ||
      typeof pairs[Symbol.iterator] === "function"
    ) {
      try {
        // Let Map constructor validate the argument. Note that Map remembers
        // the original insertion order of the keys, which satisfies the ordering
        // premise of this function.
        const map = pairs instanceof Map ? pairs : new Map(pairs);
        entries = Array.from(map, ([key, value]) => ({ key, value }));
      } catch (error) {
        throw new Error("writeMany(): unexpected argument.");
      }
    } else if (typeof pairs === "object") {
      entries = Array.from(Object.entries(pairs), ([key, value]) => ({
        key,
        value,
      }));
    } else {
      throw new Error("writeMany(): unexpected argument.");
    }

    if (entries.length) {
      return promisify(this.database.writeMany, entries);
    }
    return Promise.resolve();
  }

  has(key) {
    return promisify(this.database.has, key);
  }

  get(key, defaultValue) {
    return promisify(this.database.get, key, defaultValue);
  }

  delete(key) {
    return promisify(this.database.delete, key);
  }

  clear() {
    return promisify(this.database.clear);
  }

  async enumerate(from_key, to_key) {
    return new KeyValueEnumerator(
      await promisify(this.database.enumerate, from_key, to_key)
    );
  }
}

/**
 * A class that wraps an nsIKeyValueEnumerator in a Promise-based API.
 *
 * This class isn't exported, so you can't instantiate it directly, but you
 * can retrieve an instance of this class by calling enumerate() on an instance
 * of KeyValueDatabase:
 *
 * ```
 *     const database = await KeyValueService.getOrCreate(path, name);
 *     const enumerator = await database.enumerate();
 * ```
 *
 * And then iterate pairs via its hasMoreElements() and getNext() methods:
 *
 * ```
 *     while (enumerator.hasMoreElements()) {
 *       const { key, value } = enumerator.getNext();
 *       …
 *     }
 * ```
 *
 * Or with a `for...of` statement:
 *
 * ```
 *     for (const { key, value } of enumerator) {
 *         …
 *     }
 * ```
 */
class KeyValueEnumerator {
  constructor(enumerator) {
    this.enumerator = enumerator;
  }

  hasMoreElements() {
    return this.enumerator.hasMoreElements();
  }

  getNext() {
    return this.enumerator.getNext();
  }

  *[Symbol.iterator]() {
    while (this.enumerator.hasMoreElements()) {
      yield this.enumerator.getNext();
    }
  }
}