UNPKG

@livetl/svelte-webext-stores

Version:

Svelte stores that synchronizes to WebExtension storage.

344 lines (333 loc) 11 kB
'use strict'; 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;