UNPKG

reduxed-chrome-storage

Version:

Redux interface to chrome.storage (browser.storage). A unified way to use Redux in all modern browser extensions. The only way to get Redux working in Manifest V3 Chrome extensions

639 lines (583 loc) 25.9 kB
/** * @license * ReduxedChromeStorage v3.0.10 * https://github.com/hindmost/reduxed-chrome-storage * Copyright (c) Savr Goryaev aka hindmost * * This source code is licensed under the MIT license * https://github.com/hindmost/reduxed-chrome-storage/blob/master/LICENSE * * Dependencies: * * uuid v8.3.2 */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.reduxedChromeStorage = {})); })(this, (function (exports) { 'use strict'; // Unique ID creation requires a high quality random # generator. In the browser we therefore // require the crypto API and do not support built-in fallback to lower quality random number // generators (like Math.random()). var getRandomValues; var rnds8 = new Uint8Array(16); function rng() { // lazy load so that environments that need to polyfill have a chance to do so if (!getRandomValues) { // getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. Also, // find the complete implementation of crypto (msCrypto) on IE11. getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto !== 'undefined' && typeof msCrypto.getRandomValues === 'function' && msCrypto.getRandomValues.bind(msCrypto); if (!getRandomValues) { throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported'); } } return getRandomValues(rnds8); } var REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; function validate(uuid) { return typeof uuid === 'string' && REGEX.test(uuid); } /** * Convert array of 16 byte values to UUID string format of the form: * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX */ var byteToHex = []; for (var i = 0; i < 256; ++i) { byteToHex.push((i + 0x100).toString(16).substr(1)); } function stringify(arr) { var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; // Note: Be careful editing this code! It's been tuned for performance // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 var uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one // of the following: // - One or more input array values don't map to a hex octet (leading to // "undefined" in the uuid) // - Invalid input values for the RFC `version` or `variant` fields if (!validate(uuid)) { throw TypeError('Stringified UUID is invalid'); } return uuid; } function v4(options, buf, offset) { options = options || {}; var rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` rnds[6] = rnds[6] & 0x0f | 0x40; rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided if (buf) { offset = offset || 0; for (var i = 0; i < 16; ++i) { buf[offset + i] = rnds[i]; } return buf; } return stringify(rnds); } /** * Utility function: returns a deep copy of its argument */ function cloneDeep(o) { return o == null || typeof o !== 'object' ? o : JSON.parse(JSON.stringify(o)); } /** * Utility function: checks deeply if its two arguments are equal */ function isEqual(a, b) { if (a === b) { return true; } if (a == null || typeof a !== 'object' || b == null || typeof b !== 'object' || Array.isArray(a) !== Array.isArray(b)) { return false; } var keysA = Object.keys(a), keysB = Object.keys(b); if (keysA.length !== keysB.length) { return false; } for (var i = 0, list = keysA; i < list.length; i += 1) { var key = list[i]; if (keysB.indexOf(key) <= -1 || !isEqual(a[key], b[key])) { return false; } } return true; } /** * Utility function: returns the deep difference between its two arguments */ function diffDeep(a, b) { if (a === b) { return undefined; } if (a == null || typeof a !== 'object' || b == null || typeof b !== 'object') { return a; } if (Array.isArray(a) || Array.isArray(b)) { return isEqual(a, b) ? undefined : a; } var keysA = Object.keys(a), keysB = Object.keys(b); var eq = true; var ret = keysA.reduce(function (acc, key) { var diff = keysB.indexOf(key) > -1 ? diffDeep(a[key], b[key]) : a[key]; if (typeof diff === 'undefined') { return acc; } eq = false; acc[key] = diff; return acc; }, {}); keysB.forEach(function (key) { if (keysA.indexOf(key) > -1) { return; } eq = false; ret[key] = undefined; }); return eq ? undefined : ret; } function mergeOrReplace(a, b, withReduction) { if (Array.isArray(b)) { return cloneDeep(b); } if (a == null || typeof a !== 'object' || Array.isArray(a) || b == null || typeof b !== 'object') { return cloneDeep(typeof b !== 'undefined' ? b : a); } var ret = Object.keys(a).concat(Object.keys(b).filter(function (key) { return !(key in a); })).reduce(function (acc, key) { return acc[key] = mergeOrReplace(a[key], b[key], withReduction), acc; }, {}); if (!withReduction) { return ret; } var keysB = Object.keys(b); Object.keys(a).forEach(function (key) { keysB.indexOf(key) > -1 || delete ret[key]; }); return ret; } var packState = function (state, id, ts) { return [id, ts, state]; }; var unpackState = function (data) { if (typeof data === 'undefined' || !Array.isArray(data) || data.length !== 3) { return [data, '', 0]; } var id = data[0]; var ts = data[1]; var state = data[2]; return typeof id === 'string' && typeof ts === 'number' ? [state, id, ts] : [data, '', 0]; }; var ReduxedStorage = function ReduxedStorage(container, storage, isolated, plainActions, outdatedTimeout, localChangeListener, resetState) { this.container = container; this.storage = storage; this.isolated = isolated; this.plain = plainActions; this.timeout = outdatedTimeout ? Math.max(outdatedTimeout, 500) : 1000; this.resetState = resetState; this.store = this._instantiateStore(); this.state = null; this.id = v4(); this.tmstamp = 0; this.outdted = []; if (typeof localChangeListener === 'function') { this.lisner = localChangeListener; } this.lisners = []; this.getState = this.getState.bind(this); this.subscribe = this.subscribe.bind(this); this.dispatch = this.dispatch.bind(this); this.replaceReducer = this.replaceReducer.bind(this); this[Symbol.observable] = this[Symbol.observable].bind(this); }; ReduxedStorage.prototype.init = function init () { var this$1$1 = this; this.tmstamp || this.isolated || this.storage.subscribe(function (data, oldData) { var ref = unpackState(data); var state = ref[0]; var id = ref[1]; var timestamp = ref[2]; if (id === this$1$1.id || isEqual(state, this$1$1.state)) { return; } var newTime = timestamp >= this$1$1.tmstamp; var newState = newTime ? mergeOrReplace(this$1$1.state, state, true) : mergeOrReplace(state, this$1$1.state, true); if (!newTime && isEqual(newState, this$1$1.state)) { return; } this$1$1._setState(newState, timestamp); this$1$1._renewStore(); if (!isEqual(newState, state)) { this$1$1._send2Storage(); } this$1$1._callListeners(); }); var defaultState = this.store.getState(); // return a promise to be resolved when the last state (if any) // is restored from chrome.storage return new Promise(function (resolve) { this$1$1.storage.load(function (data) { var ref = unpackState(data); var storedState = ref[0]; var timestamp = ref[2]; var newState = storedState ? mergeOrReplace(defaultState, storedState) : defaultState; if (this$1$1.resetState) { newState = mergeOrReplace(newState, this$1$1.resetState); } this$1$1._setState(newState, timestamp); this$1$1._renewStore(); isEqual(newState, storedState) || this$1$1._send2Storage(); resolve(this$1$1); }); }); }; ReduxedStorage.prototype.initFrom = function initFrom (state) { this._setState(state, 0); this._renewStore(); return this; }; ReduxedStorage.prototype._setState = function _setState (data, timestamp) { this.state = cloneDeep(data); timestamp = typeof timestamp !== 'undefined' ? timestamp : Date.now(); if (timestamp > this.tmstamp) { this.tmstamp = timestamp; } }; ReduxedStorage.prototype._renewStore = function _renewStore () { var this$1$1 = this; this.plain ? this.unsub && this.unsub() : this._clean(); var store = this.store = this._instantiateStore(this.state); var now = Date.now(); var n = this.outdted.length; this.outdted = this.outdted.map(function (ref, i) { var t = ref[0]; var u = ref[1]; return t || i >= n - 1 ? [t, u] : [now, u]; }); var state0 = cloneDeep(this.state); var unsubscribe = this.store.subscribe(function () { var state = store && store.getState(); var sameStore = this$1$1.store === store; this$1$1._clean(); if (isEqual(state, this$1$1.state)) { return; } if (sameStore) { this$1$1._setState(state); } else { var diff = diffDeep(state, state0); if (typeof diff === 'undefined') { return; } this$1$1._setState(mergeOrReplace(this$1$1.state, diff)); this$1$1._renewStore(); } this$1$1._send2Storage(); this$1$1._callListeners(true, state0); state0 = cloneDeep(state); }); if (this.plain) { this.unsub = unsubscribe; } else { this.outdted.push([0, unsubscribe]); } }; ReduxedStorage.prototype._clean = function _clean () { var this$1$1 = this; if (this.plain) { return; } var now = Date.now(); var n = this.outdted.length; this.outdted.forEach(function (ref, i) { var timestamp = ref[0]; var unsubscribe = ref[1]; if (i >= n - 1 || now - timestamp < this$1$1.timeout) { return; } unsubscribe(); delete this$1$1.outdted[i]; }); }; ReduxedStorage.prototype._instantiateStore = function _instantiateStore (state) { var store = this.container(state); if (typeof store !== 'object' || typeof store.getState !== 'function') { throw new Error("Invalid 'storeCreatorContainer' supplied"); } return store; }; ReduxedStorage.prototype._send2Storage = function _send2Storage () { this.storage.save(packState(this.state, this.id, this.tmstamp)); }; ReduxedStorage.prototype._callListeners = function _callListeners (local, oldState) { local && this.lisner && this.lisner(this, oldState); for (var i = 0, list = this.lisners; i < list.length; i += 1) { var fn = list[i]; fn(); } }; ReduxedStorage.prototype.getState = function getState () { return this.state; }; ReduxedStorage.prototype.subscribe = function subscribe (fn) { var this$1$1 = this; typeof fn === 'function' && this.lisners.push(fn); return function () { if (typeof fn === 'function') { this$1$1.lisners = this$1$1.lisners.filter(function (v) { return v !== fn; }); } }; }; ReduxedStorage.prototype.dispatch = function dispatch (action) { return this.store.dispatch(action); }; ReduxedStorage.prototype.replaceReducer = function replaceReducer (nextReducer) { if (typeof nextReducer === 'function') { this.store.replaceReducer(nextReducer); } return this; }; ReduxedStorage.prototype[Symbol.observable] = function () { var obj; var getState = this.getState; var subscribe = this.subscribe; return ( obj = { subscribe: function subscribe$1(observer) { if (typeof observer !== 'object' || observer === null) { throw new TypeError('Expected the observer to be an object.'); } function observeState() { var observerAsObserver = observer; observerAsObserver.next && observerAsObserver.next(getState()); } observeState(); var unsubscribe = subscribe(observeState); return { unsubscribe: unsubscribe }; } }, obj[Symbol.observable] = function () { return this; }, obj ); }; var StorageAreaName; (function (StorageAreaName) { StorageAreaName["local"] = "local"; StorageAreaName["sync"] = "sync"; })(StorageAreaName || (StorageAreaName = {})); var usageSize = function (data) { return new TextEncoder().encode(Object.entries(data).map(function (ref) { var key = ref[0]; var val = ref[1]; return key + JSON.stringify(val); }).join('')).length; }; var WrappedStorage = function WrappedStorage(ref) { var namespace = ref.namespace; var area = ref.area; var key = ref.key; this.ns = namespace; this.areaName = area === StorageAreaName.sync ? StorageAreaName.sync : StorageAreaName.local; this.key = key || 'reduxed'; this.listeners = []; this.errListeners = []; }; WrappedStorage.prototype.regShared = function regShared () { var this$1$1 = this; this.regListener(function (newValue, oldValue) { for (var i = 0, list = this$1$1.listeners; i < list.length; i += 1) { var listener = list[i]; listener(newValue, oldValue); } }); }; WrappedStorage.prototype.regListener = function regListener (listener) { var this$1$1 = this; this.ns.storage.onChanged.addListener(function (changes, area) { if (area !== this$1$1.areaName || !(this$1$1.key in changes)) { return; } var ref = changes[this$1$1.key]; var newValue = ref.newValue; var oldValue = ref.oldValue; newValue && listener(newValue, oldValue); }); }; WrappedStorage.prototype.subscribe = function subscribe (listener) { typeof listener === 'function' && this.listeners.push(listener); }; WrappedStorage.prototype.subscribeForError = function subscribeForError (listener) { typeof listener === 'function' && this.errListeners.push(listener); }; WrappedStorage.prototype.fireErrorListeners = function fireErrorListeners (message, exceeded) { for (var i = 0, list = this.errListeners; i < list.length; i += 1) { var listener = list[i]; listener(message, exceeded); } }; WrappedStorage.prototype.callbackOnLoad = function callbackOnLoad (data, callback, all) { callback(!this.ns.runtime.lastError && (all ? data : data && data[this.key])); }; WrappedStorage.prototype.callbackOnSave = function callbackOnSave (data, area) { var this$1$1 = this; var obj; if (!this.ns.runtime.lastError) { return; } var ref = this.ns.runtime.lastError; var message = ref.message; if (!message || !data || !area) { this.fireErrorListeners(message || '', false); return; } var b = this.areaName === StorageAreaName.sync && area.QUOTA_BYTES_PER_ITEM && usageSize(( obj = {}, obj[this.key] = data, obj )) > area.QUOTA_BYTES_PER_ITEM; if (b) { this.fireErrorListeners(message, true); return; } this.load(function (allData) { var obj; var b = typeof allData === 'object' && area.QUOTA_BYTES > 0 && usageSize(Object.assign(Object.assign({}, allData), ( obj = {}, obj[this$1$1.key] = data, obj ))) > area.QUOTA_BYTES; this$1$1.fireErrorListeners(message, b); }, true); }; var WrappedChromeStorage = /*@__PURE__*/(function (WrappedStorage) { function WrappedChromeStorage(ref) { var namespace = ref.namespace; var area = ref.area; var key = ref.key; WrappedStorage.call(this, { namespace: namespace, area: area, key: key }); this.areaApi = this.ns.storage[this.areaName]; } if ( WrappedStorage ) WrappedChromeStorage.__proto__ = WrappedStorage; WrappedChromeStorage.prototype = Object.create( WrappedStorage && WrappedStorage.prototype ); WrappedChromeStorage.prototype.constructor = WrappedChromeStorage; WrappedChromeStorage.prototype.load = function load (fn, all) { var this$1$1 = this; typeof fn === 'function' && this.areaApi.get(all ? null : this.key, function (data) { this$1$1.callbackOnLoad(data, fn, all); }); }; WrappedChromeStorage.prototype.save = function save (data) { var this$1$1 = this; var obj; this.areaApi.set(( obj = {}, obj[this.key] = data, obj ), function () { this$1$1.callbackOnSave(data, this$1$1.areaApi); }); }; return WrappedChromeStorage; }(WrappedStorage)); var WrappedBrowserStorage = /*@__PURE__*/(function (WrappedStorage) { function WrappedBrowserStorage(ref) { var namespace = ref.namespace; var area = ref.area; var key = ref.key; WrappedStorage.call(this, { namespace: namespace, area: area, key: key }); this.areaApi = this.ns.storage[this.areaName]; } if ( WrappedStorage ) WrappedBrowserStorage.__proto__ = WrappedStorage; WrappedBrowserStorage.prototype = Object.create( WrappedStorage && WrappedStorage.prototype ); WrappedBrowserStorage.prototype.constructor = WrappedBrowserStorage; WrappedBrowserStorage.prototype.load = function load (fn, all) { var this$1$1 = this; typeof fn === 'function' && this.areaApi.get(all ? null : this.key).then(function (data) { this$1$1.callbackOnLoad(data, fn); }); }; WrappedBrowserStorage.prototype.save = function save (data) { var this$1$1 = this; var obj; this.areaApi.set(( obj = {}, obj[this.key] = data, obj )).then(function () { this$1$1.callbackOnSave(data, this$1$1.areaApi); }); }; return WrappedBrowserStorage; }(WrappedStorage)); var Namespace; (function (Namespace) { Namespace["chrome"] = "chrome"; Namespace["browser"] = "browser"; })(Namespace || (Namespace = {})); /** * Sets up Reduxed Chrome Storage * @param storeCreatorContainer a function that calls a store creator * and returns the created Redux store. * Receives one argument to be passed as the preloadedState argument * into the store creator. Store creator is either the Redux's createStore() * or any function that wraps the createStore(), e.g. RTK's configureStore() * @param options * @param options.namespace string to identify the APIs namespace to be used, * either 'chrome' or 'browser'. If this and the next two options are missing, * the chrome namespace is used by default * @param options.chromeNs the chrome namespace within Manifest V2 extension. * If this option is supplied, the previous one is ignored * @param options.browserNs the browser namespace within Firefox extension, * or the chrome namespace within Manifest V3 chrome extension. * If this option is supplied, the previous two are ignored * @param options.storageArea the name of chrome.storage area to be used, * either 'local' or 'sync'. Defaults to 'local' * @param options.storageKey the key under which the state will be * stored/tracked in chrome.storage. Defaults to 'reduxed' * @param options.isolated check this option if your store in this specific * extension component isn't supposed to receive state changes from other * extension components. Defaults to false * @param options.plainActions check this option if your store is only supposed * to dispatch plain object actions. Defaults to false * @param options.outdatedTimeout max. time (in ms) to wait for outdated (async) * actions to be completed. Defaults to 1000. This option is ignored * if at least one of the previous two is checked * @param listeners * @param listeners.onGlobalChange a function to be called whenever the state * changes that may be caused by any extension component (popup etc.). * Receives two arguments: * 1) a temporary store representing the current state; * 2) the previous state * @param listeners.onLocalChange a function to be called whenever a store in * this specific extension component causes a change in the state. * Receives two arguments: * 1) reference to the store that caused this change in the state; * 2) the previous state * @param listeners.onError a function to be called whenever an error * occurs during chrome.storage update. Receives two arguments: * 1) an error message defined by storage API; * 2) a boolean indicating if the limit for the used storage area is exceeded * @returns a function that creates asynchronously a Redux store replacement * connected to the state stored in chrome.storage. * Receives one optional argument: some value to which the state * will be reset entirely or partially upon the store replacement creation. * Returns a Promise to be resolved when the created store replacement is ready */ function setupReduxed(storeCreatorContainer, options, listeners) { var ref = options || {}; var namespace = ref.namespace; var chromeNs = ref.chromeNs; var browserNs = ref.browserNs; var storageArea = ref.storageArea; var storageKey = ref.storageKey; var isolated = ref.isolated; var plainActions = ref.plainActions; var outdatedTimeout = ref.outdatedTimeout; var ref$1 = listeners || {}; var onGlobalChange = ref$1.onGlobalChange; var onLocalChange = ref$1.onLocalChange; var onError = ref$1.onError; if (typeof storeCreatorContainer !== 'function') { throw new Error("Missing argument for 'storeCreatorContainer'"); } var storage = browserNs || namespace === Namespace.browser ? new WrappedBrowserStorage({ namespace: browserNs || browser, area: storageArea, key: storageKey }) : new WrappedChromeStorage({ namespace: chromeNs || chrome, area: storageArea, key: storageKey }); typeof onGlobalChange === 'function' && storage.regListener(function (data, oldData) { var store = new ReduxedStorage(storeCreatorContainer, storage, true, plainActions); var ref = unpackState(data); var state = ref[0]; var ref$1 = unpackState(oldData); var oldState = ref$1[0]; onGlobalChange(store.initFrom(state), oldState); }); isolated || storage.regShared(); var instantiate = function (resetState) { onError && storage.subscribeForError(onError); var store = new ReduxedStorage(storeCreatorContainer, storage, isolated, plainActions, outdatedTimeout, onLocalChange, resetState); return store.init(); }; return instantiate; } exports.cloneDeep = cloneDeep; exports.diffDeep = diffDeep; exports.isEqual = isEqual; exports.mergeOrReplace = mergeOrReplace; exports.setupReduxed = setupReduxed; Object.defineProperty(exports, '__esModule', { value: true }); }));