@livetl/svelte-webext-stores
Version:
Svelte stores that synchronizes to WebExtension storage.
344 lines (333 loc) • 11 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var store = require('svelte/store');
function initWebExtStorage(type, area) {
const listeners = [];
// @ts-expect-error Ignore browser namespace error
const storage = type === 'webExt' ? browser.storage : chrome.storage;
if (storage == null) {
throw new TypeError('storage is undefined. Perhaps the storage permission is not granted?');
}
const storageArea = storage[area];
function addOnChangedListener(callback) {
const listener = (changes, areaName) => {
if (areaName !== area)
return;
callback(changes);
};
storage.onChanged.addListener(listener);
listeners.push(listener);
}
function cleanUp() {
listeners.forEach((l) => storage.onChanged.removeListener(l));
}
return { storageArea, addOnChangedListener, cleanUp };
}
function storageWebExtShared(type, area) {
const { storageArea, addOnChangedListener, cleanUp } = type === 'webExt'
? initWebExtStorage('webExt', area)
: initWebExtStorage('chrome', area);
async function get(key) {
return await storageArea.get(key).then((result) => result[key]);
}
async function set(key, value) {
return await storageArea.set({ [key]: value });
}
async function remove(key) {
return await storageArea.remove(key);
}
async function clear() {
return await storageArea.clear();
}
return {
get,
set,
addOnChangedListener,
cleanUp,
remove,
clear
};
}
/**
* Create storage backend for Mozilla WebExtension (browser API).
* @param area `'local'` | `'sync'` | `'managed'`. Default: `'local'`
*/
function storageWebExt(area = 'local') {
return storageWebExtShared('webExt', area);
}
/**
* Create storage backend for Chrome Manifest Version 2 (callback API).
* @param area `'local'` | `'sync'` | `'managed'`. Default: `'local'`
*/
function storageMV2(area = 'local') {
const { storageArea, addOnChangedListener, cleanUp } = initWebExtStorage('chrome', area);
function resolveCallback(value, resolve, reject) {
const error = chrome.runtime.lastError;
if (error != null) {
reject(error);
return;
}
resolve(value);
}
async function get(key) {
return await new Promise((resolve, reject) => storageArea.get(key, (result) => resolveCallback(result[key], resolve, reject)));
}
async function set(key, value) {
return await new Promise((resolve, reject) => storageArea.set({ [key]: value }, () => resolveCallback(undefined, resolve, reject)));
}
async function remove(key) {
return await new Promise((resolve, reject) => storageArea.remove(key, () => resolveCallback(undefined, resolve, reject)));
}
async function clear() {
return await new Promise((resolve, reject) => storageArea.clear(() => resolveCallback(undefined, resolve, reject)));
}
return {
get,
set,
remove,
clear,
addOnChangedListener,
cleanUp
};
}
/**
* Create storage backend for Chrome Manifest Version 3 (Promise API).
* @param area `'local'` | `'sync'` | `'managed'`. Default: `'local'`
*/
function storageMV3(area = 'local') {
return storageWebExtShared('chrome', area);
}
/**
* Create storage backend for legacy/non-WebExtension applications.
* @param area `'session'` | `'local'`. Default: `'session'`.
*/
function storageLegacy(area = 'session') {
const storage = area === 'session' ? window.sessionStorage : window.localStorage;
let callbacks = [];
const listeners = [];
async function get(key) {
const result = storage.getItem(key);
if (result == null)
return undefined;
return JSON.parse(result);
}
async function set(key, value) {
const oldValue = await get(key);
storage.setItem(key, JSON.stringify(value));
callbacks.forEach((callback) => {
const changes = {
[key]: { oldValue, newValue: value }
};
callback(changes);
});
}
function addOnChangedListener(callback) {
const listener = (event) => {
if (event.key == null)
return;
const changes = {
[event.key]: { oldValue: event.oldValue, newValue: event.newValue }
};
callback(changes);
};
window.addEventListener('storage', listener);
callbacks.push(callback);
listeners.push(listener);
}
function cleanUp() {
callbacks = [];
listeners.forEach((l) => window.removeEventListener('storage', l));
}
async function remove(key) {
storage.removeItem(key);
}
async function clear() {
storage.clear();
}
return {
get,
set,
addOnChangedListener,
cleanUp,
remove,
clear
};
}
/**
* Create a store that is synchronized to the storage backend.
* @param key Storage key.
* @param defaultValue Item default value.
* @param backend Storage backend.
* @param syncFromExternal Whether store should be updated when storage value
* is updated externally.
* @param versionedOptions Enables options for migrating storage values from an
* older version to a newer version.
*/
function syncStore(key, defaultValue, backend, syncFromExternal, versionedOptions) {
let currentValue = defaultValue;
let isReady = false;
const store$1 = store.writable(defaultValue);
const keyPure = key;
if (versionedOptions != null) {
key = `${keyPure}${versionedOptions.seperator}${versionedOptions.version}`;
}
async function ready() {
if (isReady)
return;
await updateFromBackend();
if ((versionedOptions === null || versionedOptions === void 0 ? void 0 : versionedOptions.migrations) != null) {
for (const [oldVersion, migrate] of versionedOptions.migrations.entries()) {
const oldKey = oldVersion === -1
? keyPure
: `${keyPure}${versionedOptions.seperator}${oldVersion}`;
const oldValue = await backend.get(oldKey);
if (oldValue === undefined)
continue;
const newValue = migrate(oldValue);
await set(newValue);
await backend.remove(oldKey);
}
}
isReady = true;
}
function setStore(value) {
store$1.set(value);
currentValue = value;
}
async function get() {
await ready();
return currentValue;
}
function getCurrent() {
return currentValue;
}
function setRaw(value) {
if (value === currentValue)
return;
setStore(value);
}
async function set(value) {
if (value === currentValue)
return;
setStore(value);
await backend.set(key, value);
}
async function updateFromBackend() {
const value = await backend.get(key);
if (value === undefined) {
await backend.set(key, defaultValue);
return;
}
setStore(value);
}
/** Reset store value to default value. */
async function reset() {
await set(defaultValue);
}
function subscribe(run) {
ready()
.then(() => {
if (typeof run !== 'function')
console.log(run);
run(currentValue);
})
.catch((e) => console.error(e));
return store$1.subscribe(run);
}
async function update(updater) {
return await set(updater(currentValue));
}
return {
subscribe,
get,
set,
setRaw,
getCurrent,
reset,
ready,
syncFromExternal,
key,
update
};
}
function addLookupMethods(store) {
async function getItem(key) {
const storeValue = await store.get();
return storeValue[key];
}
async function setItem(key, value) {
const storeValue = Object.assign({}, await store.get());
storeValue[key] = value;
await store.set(storeValue);
}
return Object.assign(Object.assign({}, store), { getItem,
setItem });
}
/**
* Create handler for registering stores that are synced to storage.
* @param backend Storage backend.
*/
function webExtStores(backend = storageMV2()) {
const stores = new Map();
backend.addOnChangedListener((changes) => {
Object.keys(changes).forEach((key) => {
const change = changes[key];
const result = stores.get(key);
if (result == null ||
!result.syncFromExternal)
return;
result.setRaw(change.newValue);
});
});
function addSyncStore(key, defaultValue, syncFromExternal = true, versionedOptions) {
const store = syncStore(key, defaultValue, backend, syncFromExternal, versionedOptions);
stores.set(key, store);
return store;
}
function addLookupStore(key, defaultValue, syncFromExternal = true, versionedOptions) {
const store = addLookupMethods(addSyncStore(key, defaultValue, syncFromExternal, versionedOptions));
return store;
}
function addCustomStore(getStore) {
const store = getStore(backend);
stores.set(store.key, store);
return store;
}
async function _clear() {
for (const key of stores.keys()) {
await backend.remove(key);
}
stores.clear();
}
async function exportJson() {
const result = {};
for (const [key, store] of stores) {
result[key] = await store.get();
}
return JSON.stringify(result);
}
async function importJson(json) {
const data = JSON.parse(json);
for (const [key, value] of Object.entries(data)) {
const store = stores.get(key);
if (store == null)
continue;
await store.set(value);
}
}
return {
addSyncStore,
addLookupStore,
addCustomStore,
_clear,
exportJson,
importJson
};
}
exports.addLookupMethods = addLookupMethods;
exports.storageLegacy = storageLegacy;
exports.storageMV2 = storageMV2;
exports.storageMV3 = storageMV3;
exports.storageWebExt = storageWebExt;
exports.syncStore = syncStore;
exports.webExtStores = webExtStores;