Socket
Socket
Sign inDemoInstall

reduxed-chrome-storage

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

reduxed-chrome-storage - npm Package Compare versions

Comparing version 2.7.0 to 3.0.0

dist/utils/index.d.ts

106

dist/index.d.ts

@@ -1,57 +0,73 @@

import { StoreCreator, StoreEnhancer, Reducer } from 'redux';
import { ExtendedStore } from './types/store';
import { ExtendedStore, StoreCreatorContainer } from './types/store';
import { ChromeNamespace, BrowserNamespace } from './types/apis';
import { ChangeListener, ErrorListener } from './types/listeners';
import { cloneDeep, isEqual, diffDeep, mergeOrReplace } from './utils';
export { ChromeNamespace, BrowserNamespace } from './types/apis';
export { ChangeListener, ErrorListener } from './types/listeners';
export { ExtendedDispatch, ExtendedStore, StoreCreatorContainer } from './types/store';
export interface ReduxedSetupOptions {
namespace?: string;
chromeNs?: ChromeNamespace;
browserNs?: BrowserNamespace;
storageArea?: string;
storageKey?: string;
isolated?: boolean;
plainActions?: boolean;
outdatedTimeout?: number;
}
export interface ReduxedSetupListeners {
onGlobalChange?: ChangeListener;
onLocalChange?: ChangeListener;
onError?: ErrorListener;
}
/**
* ReduxedChromeStorage creator factory.
* Returns an async store creator that's supposed to replace
* the original Redux's createStore function.
* Unlike the original createStore() that immediately returns a store,
* async store creator returns a Promise to be resolved
* when the created store is ready
* @param obj
* @param obj.createStore the original Redux's createStore function.
* The only mandatory property/option
* @param obj.namespace string to identify the APIs namespace to be used,
* either 'chrome' or 'browser'.
* If this and the next two options are missing,
* 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 obj.chromeNs the chrome namespace within Manifest V2 extension.
* @param options.chromeNs the chrome namespace within Manifest V2 extension.
* If this option is supplied, the previous one is ignored
* @param obj.browserNs the browser namespace within Firefox extension,
* @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 obj.changeListener a function to be called whenever the state changes,
* receives two parameters:
* 1) a one-time store - container of the current state;
* 2) the previous state.
* If this option is supplied, the async store creator returned by the factory
* is not supposed to be immediately used for store creation
* @param obj.errorListener a function to be called whenever an error occurs
* during chrome.storage update, receives two parameters:
* @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
* @param obj.storageArea the name of chrome.storage area to be used,
* either 'local' or 'sync', defaults to 'local'
* @param obj.storageKey key under which the state will be stored/tracked
* in chrome.storage, defaults to 'reduxed'
* @param obj.bufferLife lifetime of the bulk actions buffer (in ms),
* defaults to 100
* @returns an async store creator to replace the original createStore function
* @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
*/
export default function reduxedStorageCreatorFactory({ createStore, namespace, chromeNs, browserNs, changeListener, errorListener, storageArea, storageKey, bufferLife }: {
createStore: StoreCreator;
namespace?: string;
chromeNs?: ChromeNamespace;
browserNs?: BrowserNamespace;
changeListener?: ChangeListener;
errorListener?: ErrorListener;
storageArea?: string;
storageKey?: string;
bufferLife?: number;
}): {
(reducer: Reducer, enhancer?: StoreEnhancer<{}, {}> | undefined): Promise<ExtendedStore>;
(reducer: Reducer, initialState?: any, enhancer?: StoreEnhancer<{}, {}> | undefined): Promise<ExtendedStore>;
};
declare function setupReduxed(storeCreatorContainer: StoreCreatorContainer, options?: ReduxedSetupOptions, listeners?: ReduxedSetupListeners): (resetState?: any) => Promise<ExtendedStore>;
export { setupReduxed, cloneDeep, isEqual, diffDeep, mergeOrReplace };
/**
* @license
* ReduxedChromeStorage v2.7.0
* ReduxedChromeStorage v3.0.0
* https://github.com/hindmost/reduxed-chrome-storage

@@ -9,4 +9,85 @@ * Copyright (c) Savr Goryaev aka hindmost

* https://github.com/hindmost/reduxed-chrome-storage/blob/master/LICENSE
*
* Dependencies:
*
* uuid v8.3.2
*/
// 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) {

@@ -17,2 +98,5 @@ return o == null || typeof o !== 'object' ?

}
/**
* Utility function: checks deeply if its two arguments are equal
*/
function isEqual(a, b) {

@@ -34,2 +118,25 @@ if (a === b)

}
/**
* 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;
const keysB = Object.keys(b);
let eq = true;
const ret = Object.keys(a).reduce((acc, key) => {
const 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;
}, {});
return eq ? undefined : ret;
}
function mergeOrReplace(a, b) {

@@ -46,50 +153,62 @@ if (Array.isArray(b))

const packState = (state, id, ts) => [id, ts, state];
const unpackState = (data) => {
if (typeof data === 'undefined' || !Array.isArray(data) || data.length !== 3)
return [data, '', 0];
const [id, ts, state] = data;
return typeof id === 'string' && typeof ts === 'number' ?
[state, id, ts] :
[data, '', 0];
};
class ReduxedStorage {
constructor({ createStore, reducer, storage, bufferLife, initialState, enhancer }) {
this.createStore = createStore;
constructor(container, storage, isolated, plainActions, outdatedTimeout, localChangeListener, resetState) {
this.container = container;
this.storage = storage;
this.reducer = reducer;
this.enhancer = enhancer;
this.buffLife = bufferLife ? Math.min(Math.max(bufferLife, 0), 2000) : 100;
this.state0 = initialState;
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.buffStore = null;
this.lastState = null;
this.listeners = [];
this.inited = false;
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.subscribe = this.subscribe.bind(this);
this.replaceReducer = this.replaceReducer.bind(this);
this[Symbol.observable] = this[Symbol.observable].bind(this);
this.replaceReducer = this.replaceReducer.bind(this);
}
init() {
if (this.inited)
return new Promise(resolve => {
resolve(this);
this.tmstamp || this.isolated ||
this.storage.subscribe((data, oldData) => {
const [state, id, timestamp] = unpackState(data);
if (id === this.id || isEqual(state, this.state))
return;
const newTime = timestamp >= this.tmstamp;
const newState = newTime ?
mergeOrReplace(this.state, state) : mergeOrReplace(state, this.state);
!newTime && isEqual(newState, this.state) ||
(this._setState(newState, timestamp), this._renewStore());
newTime && isEqual(newState, state) ||
this._send2Storage();
this._callListeners();
});
const defaultState = this._createStore().getState();
// subscribe for changes in chrome.storage
this.storage.subscribe((data, oldData) => {
if (isEqual(data, this.state))
return;
this._setState(data);
for (const fn of this.listeners) {
fn(oldData);
}
});
this.inited = true;
// return a promise to be resolved when the last state (if any) is restored from chrome.storage
const defaultState = this.store.getState();
// return a promise to be resolved when the last state (if any)
// is restored from chrome.storage
return new Promise(resolve => {
// try to restore the last state stored in chrome.storage, if any
this.storage.load(storedState => {
let state = storedState ?
this.storage.load(data => {
const [storedState, id, timestamp] = unpackState(data);
let newState = storedState ?
mergeOrReplace(defaultState, storedState) : defaultState;
if (this.state0) {
state = mergeOrReplace(state, this.state0);
if (this.resetState) {
newState = mergeOrReplace(newState, this.resetState);
}
this._setState(state);
if (!isEqual(state, storedState)) {
this._send2Storage(state);
}
this._setState(newState, timestamp);
this._renewStore();
isEqual(newState, storedState) || this._send2Storage();
resolve(this);

@@ -100,15 +219,69 @@ });

initFrom(state) {
this._setState(state);
this.inited = true;
this._setState(state, 0);
this._renewStore();
return this;
}
_createStore(initialState) {
return this.createStore(this.reducer, initialState, this.enhancer);
_setState(data, timestamp) {
this.state = cloneDeep(data);
timestamp = typeof timestamp !== 'undefined' ? timestamp : Date.now();
if (timestamp > this.tmstamp) {
this.tmstamp = timestamp;
}
}
_send2Storage(data) {
this.storage.save(data);
_renewStore() {
this.plain ? this.unsub && this.unsub() : this._clean();
const store = this.store = this._instantiateStore(this.state);
const timestamp = Date.now();
this.outdted.map(([t, u]) => t ? [t, u] : [timestamp, u]);
let state0 = cloneDeep(this.state);
const unsubscribe = this.store.subscribe(() => {
const state = store && store.getState();
const sameStore = this.store === store;
this._clean();
if (isEqual(state, this.state))
return;
if (sameStore) {
this._setState(state);
}
else {
const diff = diffDeep(state, state0);
if (typeof diff === 'undefined')
return;
this._setState(mergeOrReplace(this.state, diff));
this._renewStore();
}
this._send2Storage();
this._callListeners(true, state0);
state0 = cloneDeep(state);
});
if (this.plain)
this.unsub = unsubscribe;
else
this.outdted.push([0, unsubscribe]);
}
_setState(data) {
if (data) {
this.state = cloneDeep(data);
_clean() {
if (this.plain)
return;
const now = Date.now();
const nOld = this.outdted.length;
this.outdted.forEach(([timestamp, unsubscribe], i) => {
if (i >= nOld - 1 || now - timestamp < this.timeout)
return;
unsubscribe();
delete this.outdted[i];
});
}
_instantiateStore(state) {
const store = this.container(state);
if (typeof store !== 'object' || typeof store.getState !== 'function')
throw new Error(`Invalid 'storeCreatorContainer' supplied`);
return store;
}
_send2Storage() {
this.storage.save(packState(this.state, this.id, this.tmstamp));
}
_callListeners(local, oldState) {
local && this.lisner && this.lisner(this, oldState);
for (const fn of this.lisners) {
fn();
}

@@ -120,6 +293,6 @@ }

subscribe(fn) {
typeof fn === 'function' && this.listeners.push(fn);
typeof fn === 'function' && this.lisners.push(fn);
return () => {
if (typeof fn === 'function') {
this.listeners = this.listeners.filter(v => v !== fn);
this.lisners = this.lisners.filter(v => v !== fn);
}

@@ -129,37 +302,7 @@ };

dispatch(action) {
if (!this.buffStore) {
// this.buffStore is to be used with sync actions
this.buffStore = this._createStore(this.state);
// this.lastState is shared by both sync and async actions
this.lastState = this.buffStore.getState();
setTimeout(() => {
this.buffStore = null;
}, this.buffLife);
}
// lastStore, holding an extra reference to the last created store, is to be used with async actions (e.g. via Redux Thunk);
// then when this.buffStore is reset to null this variable should still refer to the same store
let lastStore = this.buffStore;
// set up a one-time state change listener
const unsubscribe = lastStore.subscribe(() => {
// if this.buffStore is non-empty, use it for getting the current state,
// otherwise an async action is implied, so use lastStore instead
const store = this.buffStore || lastStore;
const state = store && store.getState();
// we need a state change to be effective, so the current state should differ from the last saved one
if (isEqual(state, this.lastState))
return;
// send the current state to chrome.storage & update this.lastState
this._send2Storage(state);
this.lastState = state;
// unsubscribe this listener and reset the lastStore in order to release the related resources
setTimeout(() => {
unsubscribe();
lastStore = null;
}, this.buffLife);
});
return lastStore.dispatch(action);
return this.store.dispatch(action);
}
replaceReducer(nextReducer) {
if (typeof nextReducer === 'function') {
this.reducer = nextReducer;
this.store.replaceReducer(nextReducer);
}

@@ -205,6 +348,12 @@ return this;

this.listeners = [];
this.errorListeners = [];
this.errListeners = [];
}
init() {
// Setup internal (shared) listener for chrome.storage.onChanged
regShared() {
this.regListener((newValue, oldValue) => {
for (const listener of this.listeners) {
listener(newValue, oldValue);
}
});
}
regListener(listener) {
this.ns.storage.onChanged.addListener((changes, area) => {

@@ -214,19 +363,14 @@ if (area !== this.areaName || !(this.key in changes))

const { newValue, oldValue } = changes[this.key];
if (!newValue)
return;
// call external chrome.storage.onChanged listeners
for (const fn of this.listeners) {
fn(newValue, oldValue);
}
newValue && listener(newValue, oldValue);
});
}
subscribe(fn) {
typeof fn === 'function' && this.listeners.push(fn);
subscribe(listener) {
typeof listener === 'function' && this.listeners.push(listener);
}
subscribeForError(fn) {
typeof fn === 'function' && this.errorListeners.push(fn);
subscribeForError(listener) {
typeof listener === 'function' && this.errListeners.push(listener);
}
fireErrorListeners(message, exceeded) {
for (const fn of this.errorListeners) {
fn(message, exceeded);
for (const listener of this.errListeners) {
listener(message, exceeded);
}

@@ -303,41 +447,55 @@ }

/**
* ReduxedChromeStorage creator factory.
* Returns an async store creator that's supposed to replace
* the original Redux's createStore function.
* Unlike the original createStore() that immediately returns a store,
* async store creator returns a Promise to be resolved
* when the created store is ready
* @param obj
* @param obj.createStore the original Redux's createStore function.
* The only mandatory property/option
* @param obj.namespace string to identify the APIs namespace to be used,
* either 'chrome' or 'browser'.
* If this and the next two options are missing,
* 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 obj.chromeNs the chrome namespace within Manifest V2 extension.
* @param options.chromeNs the chrome namespace within Manifest V2 extension.
* If this option is supplied, the previous one is ignored
* @param obj.browserNs the browser namespace within Firefox extension,
* @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 obj.changeListener a function to be called whenever the state changes,
* receives two parameters:
* 1) a one-time store - container of the current state;
* 2) the previous state.
* If this option is supplied, the async store creator returned by the factory
* is not supposed to be immediately used for store creation
* @param obj.errorListener a function to be called whenever an error occurs
* during chrome.storage update, receives two parameters:
* @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
* @param obj.storageArea the name of chrome.storage area to be used,
* either 'local' or 'sync', defaults to 'local'
* @param obj.storageKey key under which the state will be stored/tracked
* in chrome.storage, defaults to 'reduxed'
* @param obj.bufferLife lifetime of the bulk actions buffer (in ms),
* defaults to 100
* @returns an async store creator to replace the original createStore function
* @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 reduxedStorageCreatorFactory({ createStore, namespace, chromeNs, browserNs, changeListener, errorListener, storageArea, storageKey, bufferLife }) {
if (typeof createStore !== 'function')
throw new Error(`Missing 'createStore' property/option`);
function setupReduxed(storeCreatorContainer, options, listeners) {
const { namespace, chromeNs, browserNs, storageArea, storageKey, isolated, plainActions, outdatedTimeout } = options || {};
const { onGlobalChange, onLocalChange, onError } = listeners || {};
if (typeof storeCreatorContainer !== 'function')
throw new Error(`Missing argument for 'storeCreatorContainer'`);
const storage = browserNs || namespace === Namespace.browser ?

@@ -350,32 +508,18 @@ new WrappedBrowserStorage({

});
storage.init();
typeof errorListener === 'function' &&
storage.subscribeForError(errorListener);
function asyncStoreCreator(reducer, initialState, enhancer) {
if (typeof reducer !== 'function')
throw new Error(`Missing 'reducer' parameter`);
if (typeof initialState === 'function' && typeof enhancer === 'function')
throw new Error(`Multiple 'enhancer' parameters unallowed`);
if (typeof initialState === 'function' && typeof enhancer === 'undefined') {
enhancer = initialState;
initialState = undefined;
}
const opts = {
createStore, storage, bufferLife, reducer, initialState, enhancer
};
if (typeof changeListener === 'function') {
storage.subscribe((data, oldData) => {
const store = new ReduxedStorage(opts);
changeListener(store.initFrom(data), oldData);
});
return new Promise(resolve => {
resolve(createStore(state => state));
});
}
const store = new ReduxedStorage(opts);
typeof onGlobalChange === 'function' &&
storage.regListener((data, oldData) => {
const store = new ReduxedStorage(storeCreatorContainer, storage, true, plainActions);
const [state] = unpackState(data);
const [oldState] = unpackState(oldData);
onGlobalChange(store.initFrom(state), oldState);
});
isolated || storage.regShared();
const instantiate = (resetState) => {
onError && storage.subscribeForError(onError);
const store = new ReduxedStorage(storeCreatorContainer, storage, isolated, plainActions, outdatedTimeout, onLocalChange, resetState);
return store.init();
}
return asyncStoreCreator;
};
return instantiate;
}
export { reduxedStorageCreatorFactory as default };
export { cloneDeep, diffDeep, isEqual, mergeOrReplace, setupReduxed };
/**
* @license
* ReduxedChromeStorage v2.7.0
* ReduxedChromeStorage v3.0.0
* https://github.com/hindmost/reduxed-chrome-storage

@@ -9,10 +9,91 @@ * Copyright (c) Savr Goryaev aka hindmost

* https://github.com/hindmost/reduxed-chrome-storage/blob/master/LICENSE
*
* Dependencies:
*
* uuid v8.3.2
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.reduxedChromeStorage = factory());
}(this, (function () { 'use strict';
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) {

@@ -23,2 +104,5 @@ return o == null || typeof o !== 'object' ?

}
/**
* Utility function: checks deeply if its two arguments are equal
*/
function isEqual(a, b) {

@@ -42,2 +126,25 @@ if (a === b)

}
/**
* 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 keysB = Object.keys(b);
var eq = true;
var ret = Object.keys(a).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;
}, {});
return eq ? undefined : ret;
}
function mergeOrReplace(a, b) {

@@ -54,26 +161,33 @@ if (Array.isArray(b))

var ReduxedStorage = function ReduxedStorage(ref) {
var createStore = ref.createStore;
var reducer = ref.reducer;
var storage = ref.storage;
var bufferLife = ref.bufferLife;
var initialState = ref.initialState;
var enhancer = ref.enhancer;
this.createStore = createStore;
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.reducer = reducer;
this.enhancer = enhancer;
this.buffLife = bufferLife ? Math.min(Math.max(bufferLife, 0), 2000) : 100;
this.state0 = initialState;
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.buffStore = null;
this.lastState = null;
this.listeners = [];
this.inited = false;
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.subscribe = this.subscribe.bind(this);
this.replaceReducer = this.replaceReducer.bind(this);
this[Symbol.observable] = this[Symbol.observable].bind(this);
this.replaceReducer = this.replaceReducer.bind(this);
};

@@ -83,32 +197,36 @@ ReduxedStorage.prototype.init = function init () {

if (this.inited)
{ return new Promise(function (resolve) {
resolve(this$1$1);
}); }
var defaultState = this._createStore().getState();
// subscribe for changes in chrome.storage
this.storage.subscribe(function (data, oldData) {
if (isEqual(data, this$1$1.state))
{ return; }
this$1$1._setState(data);
for (var i = 0, list = this$1$1.listeners; i < list.length; i += 1) {
var fn = list[i];
fn(oldData);
}
});
this.inited = true;
// return a promise to be resolved when the last state (if any) is restored from chrome.storage
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) : mergeOrReplace(state, this$1$1.state);
!newTime && isEqual(newState, this$1$1.state) ||
(this$1$1._setState(newState, timestamp), this$1$1._renewStore());
newTime && 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) {
// try to restore the last state stored in chrome.storage, if any
this$1$1.storage.load(function (storedState) {
var state = storedState ?
this$1$1.storage.load(function (data) {
var ref = unpackState(data);
var storedState = ref[0];
ref[1];
var timestamp = ref[2];
var newState = storedState ?
mergeOrReplace(defaultState, storedState) : defaultState;
if (this$1$1.state0) {
state = mergeOrReplace(state, this$1$1.state0);
if (this$1$1.resetState) {
newState = mergeOrReplace(newState, this$1$1.resetState);
}
this$1$1._setState(state);
if (!isEqual(state, storedState)) {
this$1$1._send2Storage(state);
}
this$1$1._setState(newState, timestamp);
this$1$1._renewStore();
isEqual(newState, storedState) || this$1$1._send2Storage();
resolve(this$1$1);

@@ -119,15 +237,83 @@ });

ReduxedStorage.prototype.initFrom = function initFrom (state) {
this._setState(state);
this.inited = true;
this._setState(state, 0);
this._renewStore();
return this;
};
ReduxedStorage.prototype._createStore = function _createStore (initialState) {
return this.createStore(this.reducer, initialState, this.enhancer);
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._send2Storage = function _send2Storage (data) {
this.storage.save(data);
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 timestamp = Date.now();
this.outdted.map(function (ref) {
var t = ref[0];
var u = ref[1];
return t ? [t, u] : [timestamp, 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._setState = function _setState (data) {
if (data) {
this.state = cloneDeep(data);
ReduxedStorage.prototype._clean = function _clean () {
var this$1$1 = this;
if (this.plain)
{ return; }
var now = Date.now();
var nOld = this.outdted.length;
this.outdted.forEach(function (ref, i) {
var timestamp = ref[0];
var unsubscribe = ref[1];
if (i >= nOld - 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();
}

@@ -141,6 +327,6 @@ };

typeof fn === 'function' && this.listeners.push(fn);
typeof fn === 'function' && this.lisners.push(fn);
return function () {
if (typeof fn === 'function') {
this$1$1.listeners = this$1$1.listeners.filter(function (v) { return v !== fn; });
this$1$1.lisners = this$1$1.lisners.filter(function (v) { return v !== fn; });
}

@@ -150,39 +336,7 @@ };

ReduxedStorage.prototype.dispatch = function dispatch (action) {
var this$1$1 = this;
if (!this.buffStore) {
// this.buffStore is to be used with sync actions
this.buffStore = this._createStore(this.state);
// this.lastState is shared by both sync and async actions
this.lastState = this.buffStore.getState();
setTimeout(function () {
this$1$1.buffStore = null;
}, this.buffLife);
}
// lastStore, holding an extra reference to the last created store, is to be used with async actions (e.g. via Redux Thunk);
// then when this.buffStore is reset to null this variable should still refer to the same store
var lastStore = this.buffStore;
// set up a one-time state change listener
var unsubscribe = lastStore.subscribe(function () {
// if this.buffStore is non-empty, use it for getting the current state,
// otherwise an async action is implied, so use lastStore instead
var store = this$1$1.buffStore || lastStore;
var state = store && store.getState();
// we need a state change to be effective, so the current state should differ from the last saved one
if (isEqual(state, this$1$1.lastState))
{ return; }
// send the current state to chrome.storage & update this.lastState
this$1$1._send2Storage(state);
this$1$1.lastState = state;
// unsubscribe this listener and reset the lastStore in order to release the related resources
setTimeout(function () {
unsubscribe();
lastStore = null;
}, this$1$1.buffLife);
});
return lastStore.dispatch(action);
return this.store.dispatch(action);
};
ReduxedStorage.prototype.replaceReducer = function replaceReducer (nextReducer) {
if (typeof nextReducer === 'function') {
this.reducer = nextReducer;
this.store.replaceReducer(nextReducer);
}

@@ -236,8 +390,18 @@ return this;

this.listeners = [];
this.errorListeners = [];
this.errListeners = [];
};
WrappedStorage.prototype.init = function init () {
WrappedStorage.prototype.regShared = function regShared () {
var this$1$1 = this;
// Setup internal (shared) listener for chrome.storage.onChanged
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) {

@@ -249,23 +413,16 @@ if (area !== this$1$1.areaName || !(this$1$1.key in changes))

var oldValue = ref.oldValue;
if (!newValue)
{ return; }
// call external chrome.storage.onChanged listeners
for (var i = 0, list = this$1$1.listeners; i < list.length; i += 1) {
var fn = list[i];
fn(newValue, oldValue);
}
newValue && listener(newValue, oldValue);
});
};
WrappedStorage.prototype.subscribe = function subscribe (fn) {
typeof fn === 'function' && this.listeners.push(fn);
WrappedStorage.prototype.subscribe = function subscribe (listener) {
typeof listener === 'function' && this.listeners.push(listener);
};
WrappedStorage.prototype.subscribeForError = function subscribeForError (fn) {
typeof fn === 'function' && this.errorListeners.push(fn);
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.errorListeners; i < list.length; i += 1) {
var fn = list[i];
for (var i = 0, list = this.errListeners; i < list.length; i += 1) {
var listener = list[i];
fn(message, exceeded);
listener(message, exceeded);
}

@@ -377,51 +534,66 @@ };

/**
* ReduxedChromeStorage creator factory.
* Returns an async store creator that's supposed to replace
* the original Redux's createStore function.
* Unlike the original createStore() that immediately returns a store,
* async store creator returns a Promise to be resolved
* when the created store is ready
* @param obj
* @param obj.createStore the original Redux's createStore function.
* The only mandatory property/option
* @param obj.namespace string to identify the APIs namespace to be used,
* either 'chrome' or 'browser'.
* If this and the next two options are missing,
* 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 obj.chromeNs the chrome namespace within Manifest V2 extension.
* @param options.chromeNs the chrome namespace within Manifest V2 extension.
* If this option is supplied, the previous one is ignored
* @param obj.browserNs the browser namespace within Firefox extension,
* @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 obj.changeListener a function to be called whenever the state changes,
* receives two parameters:
* 1) a one-time store - container of the current state;
* 2) the previous state.
* If this option is supplied, the async store creator returned by the factory
* is not supposed to be immediately used for store creation
* @param obj.errorListener a function to be called whenever an error occurs
* during chrome.storage update, receives two parameters:
* @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
* @param obj.storageArea the name of chrome.storage area to be used,
* either 'local' or 'sync', defaults to 'local'
* @param obj.storageKey key under which the state will be stored/tracked
* in chrome.storage, defaults to 'reduxed'
* @param obj.bufferLife lifetime of the bulk actions buffer (in ms),
* defaults to 100
* @returns an async store creator to replace the original createStore function
* @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 reduxedStorageCreatorFactory(ref) {
var createStore = ref.createStore;
function setupReduxed(storeCreatorContainer, options, listeners) {
var ref = options || {};
var namespace = ref.namespace;
var chromeNs = ref.chromeNs;
var browserNs = ref.browserNs;
var changeListener = ref.changeListener;
var errorListener = ref.errorListener;
var storageArea = ref.storageArea;
var storageKey = ref.storageKey;
var bufferLife = ref.bufferLife;
if (typeof createStore !== 'function')
{ throw new Error("Missing 'createStore' property/option"); }
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 ?

@@ -434,34 +606,28 @@ new WrappedBrowserStorage({

});
storage.init();
typeof errorListener === 'function' &&
storage.subscribeForError(errorListener);
function asyncStoreCreator(reducer, initialState, enhancer) {
if (typeof reducer !== 'function')
{ throw new Error("Missing 'reducer' parameter"); }
if (typeof initialState === 'function' && typeof enhancer === 'function')
{ throw new Error("Multiple 'enhancer' parameters unallowed"); }
if (typeof initialState === 'function' && typeof enhancer === 'undefined') {
enhancer = initialState;
initialState = undefined;
}
var opts = {
createStore: createStore, storage: storage, bufferLife: bufferLife, reducer: reducer, initialState: initialState, enhancer: enhancer
};
if (typeof changeListener === 'function') {
storage.subscribe(function (data, oldData) {
var store = new ReduxedStorage(opts);
changeListener(store.initFrom(data), oldData);
});
return new Promise(function (resolve) {
resolve(createStore(function (state) { return state; }));
});
}
var store = new ReduxedStorage(opts);
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 asyncStoreCreator;
};
return instantiate;
}
return reduxedStorageCreatorFactory;
exports.cloneDeep = cloneDeep;
exports.diffDeep = diffDeep;
exports.isEqual = isEqual;
exports.mergeOrReplace = mergeOrReplace;
exports.setupReduxed = setupReduxed;
})));
Object.defineProperty(exports, '__esModule', { value: true });
}));
/**
* @license
* ReduxedChromeStorage v2.7.0
* ReduxedChromeStorage v3.0.0
* https://github.com/hindmost/reduxed-chrome-storage

@@ -9,4 +9,8 @@ * Copyright (c) Savr Goryaev aka hindmost

* https://github.com/hindmost/reduxed-chrome-storage/blob/master/LICENSE
*
* Dependencies:
*
* uuid v8.3.2
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).reduxedChromeStorage=e()}(this,(function(){"use strict";function t(t){return null==t||"object"!=typeof t?t:JSON.parse(JSON.stringify(t))}function e(t,r){if(t===r)return!0;if(null==t||"object"!=typeof t||null==r||"object"!=typeof r||Array.isArray(t)!==Array.isArray(r))return!1;var n=Object.keys(t),i=Object.keys(r);if(n.length!==i.length)return!1;for(var o=0,s=n;s.length>o;o+=1){var a=s[o];if(-1>=i.indexOf(a)||!e(t[a],r[a]))return!1}return!0}function r(e,n){return Array.isArray(n)?t(n):null==e||"object"!=typeof e||Array.isArray(e)||null==n||"object"!=typeof n?void 0!==n?n:e:Object.keys(e).concat(Object.keys(n).filter((function(t){return!(t in e)}))).reduce((function(t,i){return t[i]=r(e[i],n[i]),t}),{})}var n,i=function(t){var e=t.reducer,r=t.storage,n=t.bufferLife,i=t.initialState,o=t.enhancer;this.createStore=t.createStore,this.storage=r,this.reducer=e,this.enhancer=o,this.buffLife=n?Math.min(Math.max(n,0),2e3):100,this.state0=i,this.state=null,this.buffStore=null,this.lastState=null,this.listeners=[],this.inited=!1,this.getState=this.getState.bind(this),this.dispatch=this.dispatch.bind(this),this.subscribe=this.subscribe.bind(this),this[Symbol.observable]=this[Symbol.observable].bind(this),this.replaceReducer=this.replaceReducer.bind(this)};i.prototype.init=function(){var t=this;if(this.inited)return new Promise((function(e){e(t)}));var n=this._createStore().getState();return this.storage.subscribe((function(r,n){if(!e(r,t.state)){t._setState(r);for(var i=0,o=t.listeners;o.length>i;i+=1){(0,o[i])(n)}}})),this.inited=!0,new Promise((function(i){t.storage.load((function(o){var s=o?r(n,o):n;t.state0&&(s=r(s,t.state0)),t._setState(s),e(s,o)||t._send2Storage(s),i(t)}))}))},i.prototype.initFrom=function(t){return this._setState(t),this.inited=!0,this},i.prototype._createStore=function(t){return this.createStore(this.reducer,t,this.enhancer)},i.prototype._send2Storage=function(t){this.storage.save(t)},i.prototype._setState=function(e){e&&(this.state=t(e))},i.prototype.getState=function(){return this.state},i.prototype.subscribe=function(t){var e=this;return"function"==typeof t&&this.listeners.push(t),function(){"function"==typeof t&&(e.listeners=e.listeners.filter((function(e){return e!==t})))}},i.prototype.dispatch=function(t){var r=this;this.buffStore||(this.buffStore=this._createStore(this.state),this.lastState=this.buffStore.getState(),setTimeout((function(){r.buffStore=null}),this.buffLife));var n=this.buffStore,i=n.subscribe((function(){var t=r.buffStore||n,o=t&&t.getState();e(o,r.lastState)||(r._send2Storage(o),r.lastState=o,setTimeout((function(){i(),n=null}),r.buffLife))}));return n.dispatch(t)},i.prototype.replaceReducer=function(t){return"function"==typeof t&&(this.reducer=t),this},i.prototype[Symbol.observable]=function(){var t,e=this.getState,r=this.subscribe;return(t={subscribe:function(t){if("object"!=typeof t||null===t)throw new TypeError("Expected the observer to be an object.");function n(){t.next&&t.next(e())}return n(),{unsubscribe:r(n)}}})[Symbol.observable]=function(){return this},t},function(t){t.local="local",t.sync="sync"}(n||(n={}));var o=function(t){return(new TextEncoder).encode(Object.entries(t).map((function(t){return t[0]+JSON.stringify(t[1])})).join("")).length},s=function(t){var e=t.area,r=t.key;this.ns=t.namespace,this.areaName=e===n.sync?n.sync:n.local,this.key=r||"reduxed",this.listeners=[],this.errorListeners=[]};s.prototype.init=function(){var t=this;this.ns.storage.onChanged.addListener((function(e,r){if(r===t.areaName&&t.key in e){var n=e[t.key],i=n.newValue,o=n.oldValue;if(i)for(var s=0,a=t.listeners;a.length>s;s+=1){(0,a[s])(i,o)}}}))},s.prototype.subscribe=function(t){"function"==typeof t&&this.listeners.push(t)},s.prototype.subscribeForError=function(t){"function"==typeof t&&this.errorListeners.push(t)},s.prototype.fireErrorListeners=function(t,e){for(var r=0,n=this.errorListeners;n.length>r;r+=1){(0,n[r])(t,e)}},s.prototype.callbackOnLoad=function(t,e,r){e(!this.ns.runtime.lastError&&(r?t:t&&t[this.key]))},s.prototype.callbackOnSave=function(t,e){var r,i=this;if(this.ns.runtime.lastError){var s=this.ns.runtime.lastError.message;if(s&&t&&e)this.areaName===n.sync&&e.QUOTA_BYTES_PER_ITEM&&o(((r={})[this.key]=t,r))>e.QUOTA_BYTES_PER_ITEM?this.fireErrorListeners(s,!0):this.load((function(r){var n,a="object"==typeof r&&e.QUOTA_BYTES>0&&o(Object.assign(Object.assign({},r),((n={})[i.key]=t,n)))>e.QUOTA_BYTES;i.fireErrorListeners(s,a)}),!0);else this.fireErrorListeners(s||"",!1)}};var a,c=function(t){function e(e){t.call(this,{namespace:e.namespace,area:e.area,key:e.key}),this.areaApi=this.ns.storage[this.areaName]}return t&&(e.__proto__=t),(e.prototype=Object.create(t&&t.prototype)).constructor=e,e.prototype.load=function(t,e){var r=this;"function"==typeof t&&this.areaApi.get(e?null:this.key,(function(n){r.callbackOnLoad(n,t,e)}))},e.prototype.save=function(t){var e,r=this;this.areaApi.set(((e={})[this.key]=t,e),(function(){r.callbackOnSave(t,r.areaApi)}))},e}(s),u=function(t){function e(e){t.call(this,{namespace:e.namespace,area:e.area,key:e.key}),this.areaApi=this.ns.storage[this.areaName]}return t&&(e.__proto__=t),(e.prototype=Object.create(t&&t.prototype)).constructor=e,e.prototype.load=function(t,e){var r=this;"function"==typeof t&&this.areaApi.get(e?null:this.key).then((function(e){r.callbackOnLoad(e,t)}))},e.prototype.save=function(t){var e,r=this;this.areaApi.set((e={},e[this.key]=t,e)).then((function(){r.callbackOnSave(t,r.areaApi)}))},e}(s);return function(t){t.chrome="chrome",t.browser="browser"}(a||(a={})),function(t){var e=t.createStore,r=t.namespace,n=t.chromeNs,o=t.browserNs,s=t.changeListener,f=t.errorListener,h=t.storageArea,p=t.storageKey,l=t.bufferLife;if("function"!=typeof e)throw Error("Missing 'createStore' property/option");var y=o||r===a.browser?new u({namespace:o||browser,area:h,key:p}):new c({namespace:n||chrome,area:h,key:p});return y.init(),"function"==typeof f&&y.subscribeForError(f),function(t,r,n){if("function"!=typeof t)throw Error("Missing 'reducer' parameter");if("function"==typeof r&&"function"==typeof n)throw Error("Multiple 'enhancer' parameters unallowed");"function"==typeof r&&void 0===n&&(n=r,r=void 0);var o={createStore:e,storage:y,bufferLife:l,reducer:t,initialState:r,enhancer:n};return"function"==typeof s?(y.subscribe((function(t,e){var r=new i(o);s(r.initFrom(t),e)})),new Promise((function(t){t(e((function(t){return t})))}))):new i(o).init()}}}));
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).reduxedChromeStorage={})}(this,(function(t){"use strict";var e,r=new Uint8Array(16);function n(){if(!e&&!(e="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return e(r)}var i=/^(?:[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 o(t){return"string"==typeof t&&i.test(t)}for(var s=[],a=0;256>a;++a)s.push((a+256).toString(16).substr(1));function u(t,e,r){var i=(t=t||{}).random||(t.rng||n)();if(i[6]=15&i[6]|64,i[8]=63&i[8]|128,e){r=r||0;for(var a=0;16>a;++a)e[r+a]=i[a];return e}return function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,r=(s[t[e+0]]+s[t[e+1]]+s[t[e+2]]+s[t[e+3]]+"-"+s[t[e+4]]+s[t[e+5]]+"-"+s[t[e+6]]+s[t[e+7]]+"-"+s[t[e+8]]+s[t[e+9]]+"-"+s[t[e+10]]+s[t[e+11]]+s[t[e+12]]+s[t[e+13]]+s[t[e+14]]+s[t[e+15]]).toLowerCase();if(!o(r))throw TypeError("Stringified UUID is invalid");return r}(i)}function c(t){return null==t||"object"!=typeof t?t:JSON.parse(JSON.stringify(t))}function f(t,e){if(t===e)return!0;if(null==t||"object"!=typeof t||null==e||"object"!=typeof e||Array.isArray(t)!==Array.isArray(e))return!1;var r=Object.keys(t),n=Object.keys(e);if(r.length!==n.length)return!1;for(var i=0,o=r;o.length>i;i+=1){var s=o[i];if(-1>=n.indexOf(s)||!f(t[s],e[s]))return!1}return!0}function h(t,e){if(t!==e){if(null==t||"object"!=typeof t||null==e||"object"!=typeof e)return t;if(Array.isArray(t)||Array.isArray(e))return f(t,e)?void 0:t;var r=Object.keys(e),n=!0,i=Object.keys(t).reduce((function(i,o){var s=r.indexOf(o)>-1?h(t[o],e[o]):t[o];return void 0===s||(n=!1,i[o]=s),i}),{});return n?void 0:i}}function p(t,e){return Array.isArray(e)?c(e):null==t||"object"!=typeof t||Array.isArray(t)||null==e||"object"!=typeof e?void 0!==e?e:t:Object.keys(t).concat(Object.keys(e).filter((function(e){return!(e in t)}))).reduce((function(r,n){return r[n]=p(t[n],e[n]),r}),{})}var l,y=function(t){if(void 0===t||!Array.isArray(t)||3!==t.length)return[t,"",0];var e=t[0],r=t[1];return"string"==typeof e&&"number"==typeof r?[t[2],e,r]:[t,"",0]},d=function(t,e,r,n,i,o,s){this.container=t,this.storage=e,this.isolated=r,this.plain=n,this.timeout=i?Math.max(i,500):1e3,this.resetState=s,this.store=this._instantiateStore(),this.state=null,this.id=u(),this.tmstamp=0,this.outdted=[],"function"==typeof o&&(this.lisner=o),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)};d.prototype.init=function(){var t=this;this.tmstamp||this.isolated||this.storage.subscribe((function(e,r){var n=y(e),i=n[0],o=n[2];if(n[1]!==t.id&&!f(i,t.state)){var s=o>=t.tmstamp,a=s?p(t.state,i):p(i,t.state);!s&&f(a,t.state)||(t._setState(a,o),t._renewStore()),s&&f(a,i)||t._send2Storage(),t._callListeners()}}));var e=this.store.getState();return new Promise((function(r){t.storage.load((function(n){var i=y(n),o=i[0],s=i[2],a=o?p(e,o):e;t.resetState&&(a=p(a,t.resetState)),t._setState(a,s),t._renewStore(),f(a,o)||t._send2Storage(),r(t)}))}))},d.prototype.initFrom=function(t){return this._setState(t,0),this._renewStore(),this},d.prototype._setState=function(t,e){this.state=c(t),(e=void 0!==e?e:Date.now())>this.tmstamp&&(this.tmstamp=e)},d.prototype._renewStore=function(){var t=this;this.plain?this.unsub&&this.unsub():this._clean();var e=this.store=this._instantiateStore(this.state),r=Date.now();this.outdted.map((function(t){var e=t[0],n=t[1];return e?[e,n]:[r,n]}));var n=c(this.state),i=this.store.subscribe((function(){var r=e&&e.getState(),i=t.store===e;if(t._clean(),!f(r,t.state)){if(i)t._setState(r);else{var o=h(r,n);if(void 0===o)return;t._setState(p(t.state,o)),t._renewStore()}t._send2Storage(),t._callListeners(!0,n),n=c(r)}}));this.plain?this.unsub=i:this.outdted.push([0,i])},d.prototype._clean=function(){var t=this;if(!this.plain){var e=Date.now(),r=this.outdted.length;this.outdted.forEach((function(n,i){r-1>i&&e-n[0]>=t.timeout&&((0,n[1])(),delete t.outdted[i])}))}},d.prototype._instantiateStore=function(t){var e=this.container(t);if("object"!=typeof e||"function"!=typeof e.getState)throw Error("Invalid 'storeCreatorContainer' supplied");return e},d.prototype._send2Storage=function(){this.storage.save([this.id,this.tmstamp,this.state])},d.prototype._callListeners=function(t,e){t&&this.lisner&&this.lisner(this,e);for(var r=0,n=this.lisners;n.length>r;r+=1){(0,n[r])()}},d.prototype.getState=function(){return this.state},d.prototype.subscribe=function(t){var e=this;return"function"==typeof t&&this.lisners.push(t),function(){"function"==typeof t&&(e.lisners=e.lisners.filter((function(e){return e!==t})))}},d.prototype.dispatch=function(t){return this.store.dispatch(t)},d.prototype.replaceReducer=function(t){return"function"==typeof t&&this.store.replaceReducer(t),this},d.prototype[Symbol.observable]=function(){var t,e=this.getState,r=this.subscribe;return(t={subscribe:function(t){if("object"!=typeof t||null===t)throw new TypeError("Expected the observer to be an object.");function n(){t.next&&t.next(e())}return n(),{unsubscribe:r(n)}}})[Symbol.observable]=function(){return this},t},function(t){t.local="local",t.sync="sync"}(l||(l={}));var b=function(t){return(new TextEncoder).encode(Object.entries(t).map((function(t){return t[0]+JSON.stringify(t[1])})).join("")).length},v=function(t){var e=t.area,r=t.key;this.ns=t.namespace,this.areaName=e===l.sync?l.sync:l.local,this.key=r||"reduxed",this.listeners=[],this.errListeners=[]};v.prototype.regShared=function(){var t=this;this.regListener((function(e,r){for(var n=0,i=t.listeners;i.length>n;n+=1){(0,i[n])(e,r)}}))},v.prototype.regListener=function(t){var e=this;this.ns.storage.onChanged.addListener((function(r,n){if(n===e.areaName&&e.key in r){var i=r[e.key],o=i.newValue;o&&t(o,i.oldValue)}}))},v.prototype.subscribe=function(t){"function"==typeof t&&this.listeners.push(t)},v.prototype.subscribeForError=function(t){"function"==typeof t&&this.errListeners.push(t)},v.prototype.fireErrorListeners=function(t,e){for(var r=0,n=this.errListeners;n.length>r;r+=1){(0,n[r])(t,e)}},v.prototype.callbackOnLoad=function(t,e,r){e(!this.ns.runtime.lastError&&(r?t:t&&t[this.key]))},v.prototype.callbackOnSave=function(t,e){var r,n=this;if(this.ns.runtime.lastError){var i=this.ns.runtime.lastError.message;if(i&&t&&e)this.areaName===l.sync&&e.QUOTA_BYTES_PER_ITEM&&b(((r={})[this.key]=t,r))>e.QUOTA_BYTES_PER_ITEM?this.fireErrorListeners(i,!0):this.load((function(r){var o,s="object"==typeof r&&e.QUOTA_BYTES>0&&b(Object.assign(Object.assign({},r),((o={})[n.key]=t,o)))>e.QUOTA_BYTES;n.fireErrorListeners(i,s)}),!0);else this.fireErrorListeners(i||"",!1)}};var m,g=function(t){function e(e){t.call(this,{namespace:e.namespace,area:e.area,key:e.key}),this.areaApi=this.ns.storage[this.areaName]}return t&&(e.__proto__=t),(e.prototype=Object.create(t&&t.prototype)).constructor=e,e.prototype.load=function(t,e){var r=this;"function"==typeof t&&this.areaApi.get(e?null:this.key,(function(n){r.callbackOnLoad(n,t,e)}))},e.prototype.save=function(t){var e,r=this;this.areaApi.set(((e={})[this.key]=t,e),(function(){r.callbackOnSave(t,r.areaApi)}))},e}(v),S=function(t){function e(e){t.call(this,{namespace:e.namespace,area:e.area,key:e.key}),this.areaApi=this.ns.storage[this.areaName]}return t&&(e.__proto__=t),(e.prototype=Object.create(t&&t.prototype)).constructor=e,e.prototype.load=function(t,e){var r=this;"function"==typeof t&&this.areaApi.get(e?null:this.key).then((function(e){r.callbackOnLoad(e,t)}))},e.prototype.save=function(t){var e,r=this;this.areaApi.set((e={},e[this.key]=t,e)).then((function(){r.callbackOnSave(t,r.areaApi)}))},e}(v);!function(t){t.chrome="chrome",t.browser="browser"}(m||(m={})),t.cloneDeep=c,t.diffDeep=h,t.isEqual=f,t.mergeOrReplace=p,t.setupReduxed=function(t,e,r){var n=e||{},i=n.namespace,o=n.chromeNs,s=n.browserNs,a=n.storageArea,u=n.storageKey,c=n.isolated,f=n.plainActions,h=n.outdatedTimeout,p=r||{},l=p.onGlobalChange,b=p.onLocalChange,v=p.onError;if("function"!=typeof t)throw Error("Missing argument for 'storeCreatorContainer'");var _=s||i===m.browser?new S({namespace:s||browser,area:a,key:u}):new g({namespace:o||chrome,area:a,key:u});return"function"==typeof l&&_.regListener((function(e,r){var n=new d(t,_,!0,f),i=y(e)[0],o=y(r)[0];l(n.initFrom(i),o)})),c||_.regShared(),function(e){return v&&_.subscribeForError(v),new d(t,_,c,f,h,b,e).init()}},Object.defineProperty(t,"__esModule",{value:!0})}));

@@ -9,1 +9,4 @@ import { Store, Action, AnyAction, compose } from 'redux';

}
export interface StoreCreatorContainer {
(preloadedState?: any): Store;
}
{
"name": "reduxed-chrome-storage",
"version": "2.7.0",
"description": "Redux interface to chrome.storage. Unified way to use Redux in all modern browser extensions. The only way to get Redux working in Manifest V3 Chrome extensions",
"version": "3.0.0",
"description": "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",
"license": "MIT",
"author": "Savr Goryaev",
"author": "Savr Goryaev aka hindmost",
"repository": "github:hindmost/reduxed-chrome-storage",

@@ -28,2 +28,5 @@ "bugs": "https://github.com/hindmost/reduxed-chrome-storage/issues",

],
"dependencies": {
"uuid": "^8.3"
},
"peerDependencies": {

@@ -33,24 +36,26 @@ "redux": "^4"

"devDependencies": {
"chai": "^4",
"eslint": "^7",
"eslint-import-resolver-typescript": "^2.4",
"eslint-plugin-import": "^2.24",
"mocha": "^8",
"redux": "^4",
"redux-thunk": "^2",
"rollup": "^2.56",
"chai": "^4.3",
"eslint": "^8.23",
"eslint-import-resolver-typescript": "^3.5",
"eslint-plugin-import": "^2.26",
"mocha": "^10.0",
"redux": "^4.2",
"@reduxjs/toolkit": "1.8",
"rollup": "^2.79",
"@rollup/plugin-buble": "^0.21",
"rollup-plugin-delete": "^2",
"rollup-plugin-license": "^2.5",
"rollup-plugin-terser": "^7",
"rollup-plugin-typescript2": "^0.30",
"sinon": "^9",
"ts-mocha": "^8",
"typescript": "^4.3",
"@types/chai": "^4",
"@types/mocha": "^8",
"@types/node": "^16.4",
"@types/sinon": "^9",
"@typescript-eslint/eslint-plugin": "^4.29",
"@typescript-eslint/parser": "^4.29"
"rollup-plugin-delete": "^2.0",
"rollup-plugin-license": "^2.8",
"@rollup/plugin-node-resolve": "13.3",
"rollup-plugin-terser": "^7.0",
"rollup-plugin-typescript2": "^0.33",
"sinon": "^14.0",
"ts-mocha": "^10.0",
"typescript": "^4.8",
"@types/chai": "^4.3",
"@types/mocha": "^9.1",
"@types/node": "^18.7",
"@types/sinon": "^10.0",
"@types/uuid": "^8.3",
"@typescript-eslint/eslint-plugin": "^5.36",
"@typescript-eslint/parser": "^5.36"
},

@@ -57,0 +62,0 @@ "scripts": {

# Reduxed Chrome Storage
Redux interface to [`chrome.storage`](https://developer.chrome.com/extensions/storage). Unified way to use Redux in all modern browser extensions. The only way to get Redux working in [Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/) Chrome extensions (aside from full reproduction of the Redux code).
Redux interface to [`chrome.storage`](https://developer.chrome.com/extensions/storage) ([`browser.storage`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage)). A unified way to use Redux in all modern browser extensions. The only way to get Redux working in [Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/) Chrome extensions.
[Related article](https://levelup.gitconnected.com/using-redux-in-event-driven-chrome-extensions-problem-solution-30eed1207a42)
Table of contents
=================
* [Installation](#installation)
* [How To Use](#how-to-use)
* [Common use case](#common-use-case)
* [Special use case: Manifest V3 service worker](#special-use-case-manifest-v3-service-worker)
* [Tracking errors](#tracking-errors)
* [setupReduxed() function](#setupreduxed()-function)
* [Store creator container](#store-creator-container)
* [Options](#options)
* [Listeners](#listeners)
* [The returning function](#the-returning-function)
* [Utility functions](#utility-functions)
* [How It Works / Caveats](#how-it-works-caveats)
* [JSON stringification of the state](#json-stringification-of-the-state)
* [Internal store, external state updates and outdated actions](#internal-store-external-state-updates-and-outdated-actions)
* [License](#license)
## Installation

@@ -14,107 +33,125 @@

## Usage
## How To Use
### Standard way (Promises):
### Common use case
```js
import { createStore } from 'redux';
import storeCreatorFactory from 'reduxed-chrome-storage';
```typescript
import {
setupReduxed, ReduxedSetupOptions
} from 'reduxed-chrome-storage';
import { configureStore } from '@reduxjs/toolkit';
import reducer from './reducer';
const options = {
createStore: createStore,
namespace?: ...,
chromeNs?: ...,
browserNs?: ...,
errorListener?: ...,
storageArea?: ...,
storageKey?: ...,
bufferLife?: ...
};
const asyncStoreCreator = storeCreatorFactory(options);
asyncStoreCreator(reducer).then(store => {
const storeCreatorContainer = (preloadedState?: any) =>
configureStore({reducer, preloadedState});
const options: ReduxedSetupOptions = { ... };
const instantiate = setupReduxed(storeCreatorContainer, options);
// Obtain a replacement store instance
// Option #1: Promise style
instantiate().then(store => {
const state = store.getState();
...
});
```
### Advanced way (`async/await`):
```js
...
// Option #2: async/await style
async () => {
const asyncStoreCreator = storeCreatorFactory({ createStore });
const store = await asyncStoreCreator(reducer);
const store = await instantiate();
const state = store.getState();
...
}
...
```
#### One-liner:
### Special use case: Manifest V3 service worker
```js
```typescript
import {
setupReduxed, diffDeep, isEqual,
ReduxedSetupOptions, ReduxedSetupListeners, ChangeListener
} from 'reduxed-chrome-storage';
...
async () => {
const store = await storeCreatorFactory({ createStore })( reducer );
...
}
...
```
### Storage API errors listening along with size/limits control:
// In order to track state changes set onGlobalChange and(or) onLocalChange
// properties within 3rd argument of setupReduxed like below
// instead of calling store.subscribe() in each API event listener
```js
import { createStore } from 'redux';
import storeCreatorFactory from 'reduxed-chrome-storage';
import reducer from './reducer';
const globalChangeListener: ChangeListener = (store, previousState) => {
const state = store.getState();
// Below is an example how to filter down state changes
// by comparing current state with previous one
const diff = diffDeep(state, previousState);
if (diff && ['someKey', 'anotherKey'].some(key => key in diff)) {
...
}
};
const errorListener = (message: string, exceeded: boolean) => {
...
const localChangeListener: ChangeListener = (store, previousState) => {
const state = store.getState();
// Another (a simpler) example how to filter down state changes
if (isEqual(state.someKey, previousState.someKey)) {
...
}
};
const options = {
createStore: createStore,
errorListener: errorListener,
namespace?: ...,
chromeNs?: ...,
browserNs?: ...,
storageArea?: ...,
storageKey?: ...,
bufferLife?: ...
const storeCreatorContainer = ...;
const options: ReduxedSetupOptions = { ... };
const listeners: ReduxedSetupListeners = {
onGlobalChange: globalChangeListener,
onLocalChange: localChangeListener
};
storeCreatorFactory(options)(reducer);
const instantiate = setupReduxed(storeCreatorContainer, options, listeners);
// General pattern
chrome.{API}.on{Event}.addListener(async () => {
// Obtain a store instance
const store = await instantiate();
const state = store.getState();
...
});
// Specific example
chrome.runtime.onStartup.addListener(async () => {
// Obtain a store instance
const store = await instantiate();
const state = store.getState();
...
});
...
```
### State change listening (special case - only makes sense in Manifest V3 service workers):
### Tracking errors
```js
import { createStore } from 'redux';
import storeCreatorFactory from 'reduxed-chrome-storage';
import reducer from './reducer';
```typescript
import {
setupReduxed, ReduxedSetupListeners, ErrorListener
} from 'reduxed-chrome-storage';
...
const changeListener = (store, oldState) => {
const currentState = store.getState();
// errorListener will be called whenever an error occurs
// during chrome.storage update
const errorListener: ErrorListener = (message, exceeded) => {
...
};
const options = {
createStore: createStore,
changeListener: changeListener,
namespace?: ...,
chromeNs?: ...,
browserNs?: ...,
errorListener?: ...,
storageArea?: ...,
storageKey?: ...,
bufferLife?: ...
const storeCreatorContainer = ...;
const listeners: ReduxedSetupListeners = {
onError: errorListener
};
storeCreatorFactory(options)(reducer);
const instantiate = setupReduxed(storeCreatorContainer, { ... }, listeners);
...
```
## Options
### createStore
Type: `function`
## setupReduxed() function
The original Redux's `createStore` function. The only mandatory option.
This library exports four named functions: `setupReduxed()` as well as three utility functions to be described later. As its name suggests, `setupReduxed()` function sets up Reduxed Chrome Storage allowing to get a Redux store replacement connected to the state in `chrome.storage` (here and below `chrome.storage` means both `chrome.storage` itself and Webextensions' `browser.storage`). `setupReduxed()` receives three arguments (to be described below) and returns an async function (TBD below too).
### namespace
Note: `setupReduxed()` must only be called once per extension component (popup, content script etc.).
### Store creator container
`setupReduxed()` expects its first argument (the only mandatory one) to be a _store creator container_. _Store creator container_ is a function that calls a _store creator_ and returns the result that is a Redux store. It receives one argument to be passed as the `preloadedState` argument into the store creator. _Store creator_ is either the Redux's [`createStore()`](https://redux.js.org/api/createstore) or any function that wraps the `createStore()`, e.g. [`configureStore()`](https://redux-toolkit.js.org/api/configureStore) of Redux Toolkit.
### Options
`setupReduxed()` allows to customize the setup via _options_. _Options_ are specified as named properties within optional second argument. Below is the list of available options.
#### namespace
Type: `string`<br>

@@ -125,3 +162,3 @@ Default: `'chrome'`

### chromeNs
#### chromeNs
Type: `host object` (`ChromeNamespace` in Typescript definition)

@@ -131,3 +168,3 @@

### browserNs
#### browserNs
Type: `host object` (`BrowserNamespace` in Typescript definition)

@@ -137,16 +174,40 @@

### changeListener
Type: `function` (`ChangeListener` in Typescript definition)<br>
#### storageArea
Type: `string`<br>
Default: `'local'`
A function to be called whenever the state changes, receives two parameters:
The name of `chrome.storage` area to be used, either `'sync'` or `'local'`. Note: it is not recommended to use `sync` area for immediately storing the state of extension. Use `local` area instead - it has less strict limits than `sync`. If you need to sync the state (entirely or partially) to a user's account, create a temporary store of `sync` area, then copy the needed data to (or from) the main store (of `local` area).
1. one-time store - container of the current state;
2. the previous state.
#### storageKey
Type: `string`<br>
Default: `'reduxed'`
This option only makes sense in Manifest V3 service workers or event-driven background scripts. However it works in the same way in persistent scripts too, which may be useful for cross-browser development. Note: if this option is supplied, the async store creator returned by the factory is not supposed to be immediately used for store creation; its only purpose in this case is to hold the arguments to be passed to the original `createStore` upon a one-time store creation.
Key under which the state will be stored/tracked in `chrome.storage`.
### errorListener
#### isolated
Type: `boolean`<br>
Default: `false`
Check this option if your store in this specific extension component isn't supposed to receive state changes from other extension components. It is recommended to always check this option in Manifest V3 service worker and all extension-related pages (e.g. options page etc.) except popup page.
#### plainActions
Type: `boolean`<br>
Default: `false`
Check this option if your store is only supposed to dispatch plain object actions.
#### outdatedTimeout
Type: `number`<br>
Default: `1000`
Max. time (in ms) to wait for _outdated_ (async) actions to be completed (see [How It Works](#internal-store-external-state-updates-and-outdated-actions) section for details). This option is ignored if at least one of the previous two (`isolated`/`plainActions`) options is checked.
### Listeners
`setupReduxed()` also allows to specify an error listener as well as two kinds of state change listeners. These listeners are specified as named properties within optional third argument. Below are their descriptions.
#### onError
Type: `function` (`ErrorListener` in Typescript definition)<br>
A function to be called whenever an error occurs during `chrome.storage` update, receives two parameters:
A function to be called whenever an error occurs during `chrome.storage` update. Receives two arguments:

@@ -156,31 +217,76 @@ 1. an error message defined by storage API;

### storageArea
Type: `string`<br>
Default: `'local'`
#### onGlobalChange
Type: `function` (`ChangeListener` in Typescript definition)<br>
The name of `chrome.storage` area to be used, either `'sync'` or `'local'`. Note: it is not recommended to use `sync` area for immediately storing the state of extension. Use `local` area instead - it has less strict limits than `sync`. If you need to sync the state (entirely or partially) to a user's account, create a temporary store of `sync` area, then copy the needed data to (or from) the main store (of `local` area).
A function to be called whenever the state changes that may be caused by any extension component (popup etc.). Receives two arguments:
### storageKey
Type: `string`<br>
Default: `'reduxed'`
1. a temporary store representing the current state;
2. the previous state.
Key under which the state will be stored/tracked in `chrome.storage`.
#### onLocalChange
Type: `function` (`ChangeListener` in Typescript definition)<br>
### bufferLife
Type: `number`<br>
Default: `100`
A function to be called whenever a store in this specific extension component (obviously a service worker) causes a change in the state. Receives two arguments:
Lifetime of the bulk actions buffer (in ms).
1. reference to the store that caused this change in the state;
2. the previous state.
`onGlobalChange` (that gets state updates from all extension components) has a larger coverage than `onLocalChange` (that only gets state updates from specific extension component). However `onLocalChange` has the advantage that it gets state updates _immediately_ once they're done, unlike `onGlobalChange` that only gets state updates after they're passed through `chrome.storage` update cycle (`storage.set` call, then `storage.onChanged` event firing) which takes some time.
## Notes
`onGlobalChange`/`onLocalChange` listeners only make sense in Manifest V3 service workers where they're to be used as a replacement of `store.subscribe`. In all other extension components (MV2 background scripts, popups etc.) the standard [`store.subscribe`](https://redux.js.org/api/store#subscribelistener) is supposed to be used (mostly indirectly) for tracking state changes.
The usage is the _**same**_ for _**any**_ extension component (background or content script or popup - no matter).
### The returning function
`asyncStoreCreator` function returned by `storeCreatorFactory` is similar to the original Redux's `createStore` function except that unlike the latter `asyncStoreCreator` runs in async way returning a promise instead of a new store (which is due to asynchronous nature of `chrome.storage` API). `asyncStoreCreator` has the same syntax as its Redux's counterpart, though its 2nd parameter has a slightly different meaning. Unlike Redux, this library features state persistence through extension's activity periods (browser sessions in the case of persistent extension). With Reduxed Chrome Storage the current state is always persisted in `chrome.storage` by default. So there is no need to specify a previosly (somewhere) serialized state upon store creation/instantiation. However there may be a need to reset some parts (properties) of the state (e.g. user session variables) to their initial values upon store instantiation. And this is how the 2nd parameter is supposed to be used in Reduxed Chrome Storage: as initial values for some specific state properties. To be more specific, when a new store is created by `asyncStoreCreator`, first it tries to restore its last state from `chrome.storage`, then the result is merged with the 2nd parameter (if supplied).
`setupReduxed()` returns a function that creates asynchronously a Redux store replacement connected to the state stored in `chrome.storage`.
If you're developing a Manifest V3 Chrome extension or a Manifest V2 extension with non-persistent background script, you have to keep in mind that your background script must comply with requirements of [event-based model](https://developer.chrome.com/extensions/background_pages). In the context of usage of this library it means that `asyncStoreCreator`'s promise callback should not contain any extension event listener (e.g. `chrome.runtime.onStartup` etc). If `async/await` syntax is used, there should not be any event listener after first `await` occurrence. Furthermore, `storeCreatorFactory` should not be called inside any event listener (as it implicitly sets up `chrome.storage.onChanged` event listener).
**_Receives_** one optional argument of any type: 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 replacement store is ready.
Note: Like `setupReduxed()`, the returning function must only be called once per extension component. The only exception is a Manifest V3 service worker, where the returning function is to be called in each API event listener (see [How To Use](#special-use-case-manifest-v3-service-worker) section).
## Utility functions
Aside from `setupReduxed()`, this library also exports three utility functions optimised to work with JSON data: `isEqual`, `diffDeep` and `cloneDeep`. One may use these functions to compare/copy state-related data (see [How To Use](#special-use-case-manifest-v3-service-worker) section).
### isEqual
Checks deeply if two supplied values are equal.
**_Receives_**: two arguments - values to be compared.
**_Returns_**: a `boolean`.
### diffDeep
Finds the deep difference between two supplied values.
**_Receives_**: two arguments - values to be compared.
**_Returns_**: the found deep difference.
### cloneDeep
Creates a deep copy of supplied value using JSON stringification.
**_Receives_**: one argument - a value to copy.
**_Returns_**: the created deep copy.
## How It Works / Caveats
### JSON stringification of the state
With this library all the state is stored in `chrome.storage`. As known, all data stored in `chrome.storage` are JSON-stringified. This means that the state should not contain non-JSON values as they are to be lost anyway (to be removed or replaced with JSON primitive values). Using non-JSON values in the state may cause unwanted side effects. This is especially true for object properties explicitly set to `undefined` ( `{key: undefined, ...}` ).
### Internal store, external state updates and outdated actions
In order to ensure full compatibility with Redux API this library uses an internal Redux store - true Redux store created by the supplied store creator container upon initialisation of the replacement Redux store. Every Redux action dispatched on a replacement Redux store (returned by the `setupReduxed()` returning function) is forwarded to its internal Redux store that dispatches this action. As a result the current state in the replacement store is updated (synced) with the result state in the internal store. If a dispatching action is a plain object, the respective state update/change comes to the replacement store immediately. Otherwise, i.e. if this is an async action, it may take some time for the replacement store to receive the respective state update.
Such state updates as above (caused and received within the same store / extension component) are called _local_. But there can also be _external_ state updates - caused by stores in other extension components. Whenever a replacement store receives an external state update its internal Redux store is re-created (renewed) with a new state that is the result of merging the current state with the state resulted from the external state update (note that only object literals are actually merged, all other values incl. arrays are replaced). Such re-creation is the only way to address external state updates as they come via `chrome.storage` API that doesn't provide info (action or series of actions) how to reproduce this state update/change, only the result state. Once the re-creation is done, the former current state is lost (replaced with a new one). But the former internal Redux store isn't necessarily lost. If there are uncompleted (async) actions belonging to (initiated by) this store, they all (incl. the store) are stored in a sort of temporary memory. Such uncompleted actions, belonging to internal Redux store that is no longer actual, are called _outdated_. Outdated actions belonging to the same store are stored in the temporary memory until the timeout specified by `outdatedTimeout` option is exceeded (the time is counted from the moment the store was re-created i.e. became outdated). If an outdated action is completed before `outdatedTimeout` period has run out, the respective result state is to be merged with the current state of the replacement store. Otherwise this (still uncompleted) action is to be lost, along with the store it belongs to (and all other still uncompleted actions within this store).
## License
Licensed under the MIT license.
Licensed under the [MIT license](LICENSE).
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc