Skip to content

Commit

Permalink
Merge pull request #158 from rt2zz/immutable
Browse files Browse the repository at this point in the history
v4
  • Loading branch information
rt2zz authored Aug 28, 2016
2 parents e9503e3 + c7fc4dc commit 7ca410b
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 102 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# What about redux-persist?
This is a fork of redux-persist published as `redux-persist-2` until the PR https://github.com/rt2zz/redux-persist/pull/113 gets merged.

# Redux Persist
Persist and rehydrate a redux store.

Expand Down
59 changes: 33 additions & 26 deletions src/autoRehydrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { REHYDRATE } from './constants'
import isStatePlainEnough from './utils/isStatePlainEnough'

export default function autoRehydrate (config = {}) {
const stateReconciler = config._stateReconciler || defaultStateReconciler

return (next) => (reducer, initialState, enhancer) => {
return next(createRehydrationReducer(reducer), initialState, enhancer)
}
Expand All @@ -19,32 +21,8 @@ export default function autoRehydrate (config = {}) {

let inboundState = action.payload
let reducedState = reducer(state, action)
let newState = {...reducedState}

Object.keys(inboundState).forEach((key) => {
// if initialState does not have key, skip auto rehydration
if (!state.hasOwnProperty(key)) return

// if initial state is an object but inbound state is null/undefined, skip
if (typeof state[key] === 'object' && !inboundState[key]) {
if (config.log) console.log('redux-persist/autoRehydrate: sub state for key `%s` is falsy but initial state is an object, skipping autoRehydrate.', key)
return
}

// if reducer modifies substate, skip auto rehydration
if (state[key] !== reducedState[key]) {
if (config.log) console.log('redux-persist/autoRehydrate: sub state for key `%s` modified, skipping autoRehydrate.', key)
newState[key] = reducedState[key]
return
}

// otherwise take the inboundState
if (isStatePlainEnough(inboundState[key]) && isStatePlainEnough(state[key])) newState[key] = {...state[key], ...inboundState[key]} // shallow merge
else newState[key] = inboundState[key] // hard set

if (config.log) console.log('redux-persist/autoRehydrate: key `%s`, rehydrated to ', key, newState[key])
})
return newState

return stateReconciler(state, inboundState, reducedState, config.log)
}
}
}
Expand All @@ -59,3 +37,32 @@ function logPreRehydrate (preRehydrateActions) {
`, preRehydrateActions.length)
}
}

function defaultStateReconciler (state, inboundState, reducedState, log) {
let newState = {...reducedState}

Object.keys(inboundState).forEach((key) => {
// if initialState does not have key, skip auto rehydration
if (!state.hasOwnProperty(key)) return

// if initial state is an object but inbound state is null/undefined, skip
if (typeof state[key] === 'object' && !inboundState[key]) {
if (log) console.log('redux-persist/autoRehydrate: sub state for key `%s` is falsy but initial state is an object, skipping autoRehydrate.', key)
return
}

// if reducer modifies substate, skip auto rehydration
if (state[key] !== reducedState[key]) {
if (log) console.log('redux-persist/autoRehydrate: sub state for key `%s` modified, skipping autoRehydrate.', key)
newState[key] = reducedState[key]
return
}

// otherwise take the inboundState
if (isStatePlainEnough(inboundState[key]) && isStatePlainEnough(state[key])) newState[key] = {...state[key], ...inboundState[key]} // shallow merge
else newState[key] = inboundState[key] // hard set

if (log) console.log('redux-persist/autoRehydrate: key `%s`, rehydrated to ', key, newState[key])
})
return newState
}
70 changes: 33 additions & 37 deletions src/createPersistor.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { KEY_PREFIX, REHYDRATE } from './constants'
import createAsyncLocalStorage from './defaults/asyncLocalStorage'
import isStatePlainEnough from './utils/isStatePlainEnough'
import purgeStoredState from './purgeStoredState'
import stringify from 'json-stringify-safe'
import { forEach } from 'lodash'

Expand All @@ -13,13 +13,19 @@ export default function createPersistor (store, config) {
const transforms = config.transforms || []
const debounce = config.debounce || false
const keyPrefix = config.keyPrefix || KEY_PREFIX
let storage = config.storage || createAsyncLocalStorage('local')

// fallback getAllKeys to `keys` if present (LocalForage compatability)
// pluggable state shape (e.g. immutablejs)
const stateInit = config._stateInit || {}
const stateIterator = config._stateIterator || defaultStateIterator
const stateGetter = config._stateGetter || defaultStateGetter
const stateSetter = config._stateSetter || defaultStateSetter

// storage with keys -> getAllKeys for localForage support
let storage = config.storage || createAsyncLocalStorage('local')
if (storage.keys && !storage.getAllKeys) storage = {...storage, getAllKeys: storage.keys}

// initialize stateful values
let lastState = {}
let lastState = stateInit
let paused = false
let storesToProcess = []
let timeIterator = null
Expand All @@ -28,13 +34,10 @@ export default function createPersistor (store, config) {
if (paused) return

let state = store.getState()
if (process.env.NODE_ENV !== 'production') {
if (!isStatePlainEnough(state)) console.warn('redux-persist: State is not plain enough to persist. Can only persist plain objects.')
}

forEach(state, (subState, key) => {
stateIterator(state, (subState, key) => {
if (!passWhitelistBlacklist(key)) return
if (lastState[key] === state[key]) return
if (stateGetter(lastState, key) === stateGetter(state, key)) return
if (storesToProcess.indexOf(key) !== -1) return
storesToProcess.push(key)
})
Expand All @@ -50,7 +53,7 @@ export default function createPersistor (store, config) {

let key = storesToProcess[0]
let storageKey = createStorageKey(key)
let endState = transforms.reduce((subState, transformer) => transformer.in(subState, key), store.getState()[storesToProcess[0]])
let endState = transforms.reduce((subState, transformer) => transformer.in(subState, key), stateGetter(store.getState(), key))
if (typeof endState !== 'undefined') storage.setItem(storageKey, serialize(endState), warnIfSetError(key))
storesToProcess.shift()
}, debounce)
Expand All @@ -71,9 +74,10 @@ export default function createPersistor (store, config) {
forEach(incoming, (subState, key) => {
try {
let data = deserialize(subState)
state[key] = transforms.reduceRight((interState, transformer) => {
let value = transforms.reduceRight((interState, transformer) => {
return transformer.out(interState, key)
}, data)
state = stateSetter(state, key, value)
} catch (err) {
if (process.env.NODE_ENV !== 'production') console.warn(`Error rehydrating data for key "${key}"`, subState, err)
}
Expand All @@ -84,24 +88,6 @@ export default function createPersistor (store, config) {
return state
}

function purge (keys) {
if (typeof keys === 'undefined') {
purgeAll()
} else {
forEach(keys, (key) => {
storage.removeItem(createStorageKey(key), warnIfRemoveError(key))
})
}
}

function purgeAll () {
// @TODO deprecate
storage.getAllKeys((err, allKeys) => {
if (err && process.env.NODE_ENV !== 'production') { console.warn('Error in storage.getAllKeys') }
purge(allKeys.filter((key) => key.indexOf(keyPrefix) === 0).map((key) => key.slice(keyPrefix.length)))
})
}

function createStorageKey (key) {
return `${keyPrefix}${key}`
}
Expand All @@ -111,14 +97,11 @@ export default function createPersistor (store, config) {
rehydrate: adhocRehydrate,
pause: () => { paused = true },
resume: () => { paused = false },
purge,
purgeAll
}
}

function warnIfRemoveError (key) {
return function removeError (err) {
if (err && process.env.NODE_ENV !== 'production') { console.warn('Error storing data for key:', key, err) }
purge: (keys) => purgeStoredState({storage, keyPrefix}, keys),
purgeAll: () => {
console.warn('redux-persist: purgeAll is deprecated. use `persistor.purge()` instead')
return purgeStoredState({storage, keyPrefix})
}
}
}

Expand Down Expand Up @@ -150,3 +133,16 @@ function rehydrateAction (data) {
payload: data
}
}

function defaultStateIterator (collection, callback) {
return forEach(collection, callback)
}

function defaultStateGetter (state, key) {
return state[key]
}

function defaultStateSetter (state, key, value) {
state[key] = value
return state
}
90 changes: 53 additions & 37 deletions src/defaults/asyncLocalStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,48 +35,64 @@ function getStorage (type) {
export default function (type) {
let storage = getStorage(type)
return {
getItem: function (key, cb) {
try {
var s = storage.getItem(key)
nextTick(() => {
cb(null, s)
})
} catch (e) {
cb(e)
}
getItem (key, cb) {
return new Promise((resolve, reject) => {
try {
var s = storage.getItem(key)
nextTick(() => {
cb && cb(null, s)
resolve(s)
})
} catch (e) {
cb && cb(e)
reject(e)
}
})
},
setItem: function (key, string, cb) {
try {
storage.setItem(key, string)
nextTick(() => {
cb(null)
})
} catch (e) {
cb(e)
}
setItem (key, string, cb) {
return new Promise((resolve, reject) => {
try {
storage.setItem(key, string)
nextTick(() => {
cb && cb(null)
resolve()
})
} catch (e) {
cb && cb(e)
reject(e)
}
})
},
removeItem: function (key, cb) {
try {
storage.removeItem(key)
nextTick(() => {
cb(null)
})
} catch (e) {
cb(e)
}
removeItem (key, cb) {
return new Promise((resolve, reject) => {
try {
storage.removeItem(key)
nextTick(() => {
cb && cb(null)
resolve()
})
} catch (e) {
cb && cb(e)
reject(e)
}
})
},
getAllKeys: function (cb) {
try {
var keys = []
for (var i = 0; i < storage.length; i++) {
keys.push(storage.key(i))
return new Promise((resolve, reject) => {
try {
var keys = []
for (var i = 0; i < storage.length; i++) {
keys.push(storage.key(i))
}
nextTick(() => {
cb && cb(null, keys)
resolve(keys)
})
} catch (e) {
cb && cb(e)
reject(e)
}
nextTick(() => {
cb(null, keys)
})
} catch (e) {
cb(e)
}
})
}
}
}
6 changes: 4 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import createAsyncLocalStorage from './defaults/asyncLocalStorage'

import autoRehydrate from './autoRehydrate'
import createPersistor from './createPersistor'
import createTransform from './createTransform'
import getStoredState from './getStoredState'
import persistStore from './persistStore'
import purgeStoredState from './purgeStoredState'

import createAsyncLocalStorage from './defaults/asyncLocalStorage'
const storages = {
asyncLocalStorage: createAsyncLocalStorage('local'),
asyncSessionStorage: createAsyncLocalStorage('session')
}

export { autoRehydrate, createPersistor, createTransform, getStoredState, persistStore, storages }
export { autoRehydrate, createPersistor, createTransform, getStoredState, persistStore, purgeStoredState, storages }
33 changes: 33 additions & 0 deletions src/purgeStoredState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { KEY_PREFIX } from './constants'

export default function purgeStoredState (config, keys) {
const storage = config.storage
const keyPrefix = config.keyPrefix || KEY_PREFIX

// basic validation
if (Array.isArray(config)) throw new Error('redux-persist: purgeStoredState requires config as a first argument (found array). An array of keys is the optional second argument.')
if (!storage) throw new Error('redux-persist: config.storage required in purgeStoredState')

if (typeof keys === 'undefined') { // if keys is not defined, purge all keys
return new Promise((resolve, reject) => {
storage.getAllKeys((err, allKeys) => {
if (err && process.env.NODE_ENV !== 'production') {
console.warn('redux-persist: error during purgeStoredState in storage.getAllKeys')
reject(err)
} else {
resolve(purgeStoredState(config, allKeys.filter((key) => key.indexOf(keyPrefix) === 0).map((key) => key.slice(keyPrefix.length))))
}
})
})
} else { // otherwise purge specified keys
return Promise.all(keys.map((key) => {
return storage.removeItem(`${keyPrefix}${key}`, warnIfRemoveError(key))
}))
}
}

function warnIfRemoveError (key) {
return function removeError (err) {
if (err && process.env.NODE_ENV !== 'production') { console.warn('Error storing data for key:', key, err) }
}
}
19 changes: 19 additions & 0 deletions test/purgeStoredState.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import test from 'ava'

import { purgeStoredState, storages } from '../src'

test('purgeStoredState (all) returns a promise', t => {
let purgeResult = purgeStoredState({ storage: storages.asyncLocalStorage })
t.true(isPromise(purgeResult))
return purgeResult
})

test('purgeStoredState (whitelist) returns a promise', t => {
let purgeResult = purgeStoredState({ storage: storages.asyncLocalStorage }, ['foo'])
t.true(isPromise(purgeResult))
return purgeResult
})

function isPromise (something) {
return typeof something === 'object' && typeof something.then === 'function'
}

0 comments on commit 7ca410b

Please sign in to comment.