blob: e11c235779bd548c9a4b6829b88bac097dab8236 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Class to handle accessing/storing/caching local storage data.
*/
export class LocalStorage {
/** @param {function(LocalStorage)} onInit */
constructor(onInit) {
/** @private {?Object} */
this.values_ = null;
chrome.storage.local.get(
null /* get all values */,
values => this.onInitialGet_(values, onInit));
chrome.storage.local.onChanged.addListener(
updates => this.update_(updates));
}
// ========== Static methods ==========
static async init() {
if (LocalStorage.instance) {
throw new Error(
'LocalStorage.init() should be called at most once in each ' +
'browser context.');
}
LocalStorage.instance =
await new Promise(resolve => new LocalStorage(resolve));
LocalStorage.migrateFromLocalStorage_();
return;
}
/** @param {string} key */
static get(key) {
LocalStorage.assertReady_();
return LocalStorage.instance.values_[key];
}
/** @param {string} key */
static remove(key) {
LocalStorage.assertReady_();
chrome.storage.local.remove(key);
delete LocalStorage.instance.values_[key];
}
/**
* @param {string} key
* @param {*} val
*/
static set(key, val) {
LocalStorage.assertReady_();
chrome.storage.local.set({[key]: val});
LocalStorage.instance.values_[key] = val;
}
/** @param {string} key */
static toggle(key) {
LocalStorage.assertReady_();
const val = LocalStorage.get(key);
if (typeof val !== 'boolean') {
throw new Error('Cannot toggle value of non-boolean setting');
}
LocalStorage.set(key, !val);
}
// ========= Private Methods ==========
/**
* @param {!Object} values
* @param {function(LocalStorage)} onInit
* @private
*/
onInitialGet_(values, onInit) {
this.values_ = values;
onInit(this);
}
/**
* @param {!Object<string, {newValue: *}>} updates
* @private
*/
update_(updates) {
for (const key in updates) {
this.values_[key] = updates[key].newValue;
}
}
/** @private */
static migrateFromLocalStorage_() {
// Save the keys, because otherwise the values are shifting under us as we
// iterate.
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
keys.push(localStorage.key(i));
}
for (const key of keys) {
let val = localStorage[key];
delete localStorage[key];
if (val === String(true)) {
val = true;
} else if (val === String(false)) {
val = false;
} else if (/^\d+$/.test(val)) {
// A string that with at least one digit and no other characters is an
// integer.
val = parseInt(val, 10);
} else if (/^[\d]+[.][\d]+$/.test(val)) {
// A string with at least one digit, followed by a dot, followed by at
// least one digit is a floating point number.
//
// When converting floats to strings, v8 adds the leading 0 if there
// were no digits before the decimal. E.g. String(.2) === "0.2"
//
// Similarly, integer values followed by a dot and any number of zeroes
// are stored without a decimal and will be handled by the above case.
// E.g. String(1.0) === "1"
val = parseFloat(val);
} else if (/^{.*}$/.test(val) || /^\[.*]$/.test(val)) {
// If a string begins and ends with curly or square brackets, try to
// convert it to an object/array. JSON.parse() will throw an error if
// the string is not valid JSON syntax. In that case, the variable value
// will remain unchanged (with a type of 'string').
try {
val = JSON.parse(val);
} catch (syntaxError) {
}
}
// We cannot call LocalStorage.set() because assertReady will fail.
chrome.storage.local.set({[key]: val});
LocalStorage.instance.values_[key] = val;
}
}
/** @private */
static assertReady_() {
if (!LocalStorage.instance || !LocalStorage.instance.values_) {
throw new Error(
'LocalStorage should not be accessed until initialization is ' +
'complete.');
}
}
}
/** @private {!LocalStorage} */
LocalStorage.instance;