reduxed-chrome-storage
Advanced tools
Comparing version 2.7.0 to 3.0.0
@@ -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": { |
310
README.md
# 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). |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
79772
11
1270
288
2
24
1
+ Addeduuid@^8.3
+ Addeduuid@8.3.2(transitive)