/
storage.ts
180 lines (159 loc) · 5.7 KB
/
storage.ts
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
/* 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/. */
import log, { LoggingLevel } from "../../../core/log.js";
import type { StorageIndex } from "../../../core/storage/index.js";
import type Store from "../../../core/storage/index.js";
import {
updateNestedObject,
getValueFromNestedObject,
deleteKeyFromNestedObject
} from "../../../core/storage/utils.js";
import type { JSONArray, JSONObject, JSONPrimitive, JSONValue } from "../../../core/utils.js";
import { isJSONValue, isObject } from "../../../core/utils.js";
const LOG_TAG = "platform.webext.Storage";
type WebExtStoreQuery = { [x: string]: WebExtStoreQuery | JSONPrimitive | JSONArray | null; };
/**
* Strips all properties whose values are `null` from a WebExtStoreQuery.
*
* The `null` values are the ones which were not found,
* thus can be safely removed.
*
* # Important
*
* This modifies the original object.
*
* @param query The query we want to strip of null values.
*/
function stripNulls(query: WebExtStoreQuery) {
for (const key in query) {
const curr = query[key];
if (curr === null) {
delete query[key];
}
if (isObject(curr)) {
if (Object.keys(curr).length === 0) {
delete query[key];
} else {
stripNulls(curr);
}
}
}
}
/**
* Persistent storage implementation based on the Promise-based
* [storage API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage)
* for web extensions.
*
* To make sure this implementation works on Chromium based browsers, the user must install the peer dependency
* [`mozilla/webextension-polyfill`](https://github.com/mozilla/webextension-polyfill).
*/
class WebExtStore implements Store {
private store;
private logTag: string;
// The main key under which all other entries will be recorded for this store instance.
private rootKey: string;
constructor(rootKey: string) {
if (typeof browser === "undefined") {
throw Error(
`The web extensions store should only be user in a browser extension context.
If running is a browser different from Firefox, make sure you have installed
the webextension-polyfill peer dependency. To do so, run \`npm i webextension-polyfill\`.`
);
}
if (typeof browser.storage.local === "undefined") {
throw Error(
`Unable to access web extension storage.
This is probably happening due to missing \`storage\` API permissions.
Make sure this permission was set on the manifest.json file.`
);
}
this.store = browser.storage.local;
this.rootKey = rootKey;
this.logTag = `${LOG_TAG}.${rootKey}`;
}
private async _getWholeStore(): Promise<JSONObject> {
const result = await this.store.get({ [this.rootKey]: {} });
return result[this.rootKey];
}
/**
* Build a query object to retrieve / update a given entry from the storage.
*
* @param index The index to the given entry on the storage.
* @returns The query object.
*/
private _buildQuery(index: StorageIndex): WebExtStoreQuery {
let query = null;
for (const key of [ this.rootKey, ...index ].reverse()) {
query = { [key]: query };
}
return <WebExtStoreQuery>query;
}
/**
* Retrieves the full store and builds a query object on top of it.
*
* @param transformFn The transformation function to apply to the store.
* @returns The query object with the modified store.
*/
private async _buildQueryFromStore(transformFn: (s: JSONObject) => JSONObject): Promise<JSONObject> {
const store = await this._getWholeStore();
return { [this.rootKey]: transformFn(store) };
}
async get(index: StorageIndex = []): Promise<JSONValue | undefined> {
const query = this._buildQuery(index);
const response = await this.store.get(query);
stripNulls(response);
if (!response) {
return;
}
if (isJSONValue(response)) {
if (isObject(response)) {
return getValueFromNestedObject(<JSONObject>response, [ this.rootKey, ...index ]);
} else {
return response;
}
}
log(
this.logTag,
[
`Unexpected value found in storage for index ${JSON.stringify(index)}. Ignoring.
${JSON.stringify(response, null, 2)}`
],
LoggingLevel.Warn
);
}
async update(
index: StorageIndex,
transformFn: (v?: JSONValue) => JSONValue
): Promise<void> {
if (index.length === 0) {
throw Error("The index must contain at least one property to update.");
}
// We need to get the full store object here, change it as requested and then re-save.
// This is necessary, because if we try to set a key to an inside object on the storage,
// it will erase any sibling keys that are not mentioned.
const query = await this._buildQueryFromStore(
store => updateNestedObject(store, index, transformFn)
);
return this.store.set(query);
}
async delete(index: StorageIndex): Promise<void> {
// The `remove API`[https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/StorageArea/remove]
// doesn't expose a way for us to delete nested keys.
// This means we need to get the whole store,
// make the necessary changes to it locally and then reset it.
try {
const query = await this._buildQueryFromStore(
store => deleteKeyFromNestedObject(store, index)
);
return this.store.set(query);
} catch(e) {
log(
this.logTag,
[`Error attempting to delete key ${index.toString()} from storage. Ignoring.`, e],
LoggingLevel.Warn
);
}
}
}
export default WebExtStore;