diff --git a/package.json b/package.json index 32beeba..c9c50de 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,8 @@ "typescript": "^2.1.4", "zone.js": "^0.7.7" }, - "typings": "./dist/index.d.ts" + "typings": "./dist/index.d.ts", + "dependencies": { + "lodash.merge": "^4.6.1" + } } diff --git a/spec/index_spec.ts b/spec/index_spec.ts index 35fcb58..c59ba51 100644 --- a/spec/index_spec.ts +++ b/spec/index_spec.ts @@ -167,6 +167,30 @@ describe('ngrxLocalStorage', () => { expect(finalState.state instanceof TypeA).toBeFalsy(); }); + it('filtered - multiple keys at root - should properly revive partial state', function () { + const s = new MockStorage(); + const skr = mockStorageKeySerializer; + + // state at any given moment, subject to sync selectively + const nestedState = { + app: { app1: true, app2: [1, 2], app3: { any: 'thing' } }, + feature1: { slice11: true, slice12: [1, 2], slice13: { any: 'thing' } }, + feature2: { slice21: true, slice22: [1, 2], slice23: { any: 'thing' } }, + }; + + // test selective write to storage + syncStateUpdate(nestedState, [ + { 'feature1': ['slice11', 'slice12'] }, + { 'feature2': ['slice21', 'slice22'] }, + ], s, skr, false); + + const raw1 = s.getItem('feature1'); + expect(raw1).toEqual(jasmine.arrayContaining(['slice11', 'slice12'])); + + const raw2 = s.getItem('feature2'); + expect(raw2).toEqual(jasmine.arrayContaining(['slice21', 'slice22'])); + }); + it('reviver', () => { // Use the reviver option to restore including classes @@ -433,4 +457,33 @@ describe('ngrxLocalStorage', () => { const finalState = metaReducer(reducer)(initialState, action); expect(finalState.state.astring).toEqual(initialState.state.astring); }); -}); \ No newline at end of file + + it('should merge selectively saved state and rehydrated state', () => { + const initialState = { + app: { app1: false, app2: [], app3: {} }, + feature1: { slice11: false, slice12: [], slice13: {} }, + feature2: { slice21: false, slice22: [], slice23: {} }, + }; + + // A legit case where state is saved in chunks rather than as a single object + localStorage.setItem('feature1', JSON.stringify({ slice11: true, slice12: [1, 2] })); + localStorage.setItem('feature2', JSON.stringify({ slice21: true, slice22: [1, 2] })); + + // Set up reducers + const reducer = (state = initialState, action) => state; + const metaReducer = localStorageSync({keys: [ + {'feature1': ['slice11', 'slice12']}, + {'feature2': ['slice21', 'slice22']}, + ], rehydrate: true}); + + const action = {type: INIT_ACTION}; + + // Resultant state should merge the rehydrated partial state and our initial state + const finalState = metaReducer(reducer)(initialState, action); + expect(finalState).toEqual({ + app: { app1: false, app2: [], app3: {} }, + feature1: { slice11: true, slice12: [1, 2], slice13: {} }, + feature2: { slice21: true, slice22: [1, 2], slice23: {} }, + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index ade9c61..6a8d9bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +import * as merge from 'lodash.merge'; + const INIT_ACTION = '@ngrx/store/init'; const UPDATE_ACTION = '@ngrx/store/update-reducers'; const detectDate = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/; @@ -237,39 +239,34 @@ export const localStorageSync = (config: LocalStorageConfig) => ( ) : undefined; - return function(state = rehydratedState, action: any) { - /* - Handle case where state is rehydrated AND initial state is supplied. - Any additional state supplied will override rehydrated state for the given key. - */ - if ( - (action.type === INIT_ACTION || action.type === UPDATE_ACTION) && - rehydratedState - ) { - if (state) { - Object.keys(state).forEach(function (key) { - if (state[key] instanceof Array && rehydratedState[key] instanceof Array) { - state[key] = rehydratedState[key]; - } else if (typeof state[key] === 'object' - && typeof rehydratedState[key] === 'object') { - state[key] = Object.assign({}, state[key], rehydratedState[key]); - } else { - state[key] = rehydratedState[key]; - } - }); - } else { - state = Object.assign({}, state, rehydratedState); - } + return function (state, action: any) { + let nextState; + + // If state arrives undefined, we need to let it through the supplied reducer + // in order to get a complete state as defined by user + if ((action.type === INIT_ACTION) && !state) { + nextState = reducer(state, action); + } else { + nextState = { ...state }; + } + + if ((action.type === INIT_ACTION || action.type === UPDATE_ACTION) && rehydratedState) { + nextState = merge({}, nextState, rehydratedState); } - const nextState = reducer(state, action); - syncStateUpdate( - nextState, - stateKeys, - config.storage, - config.storageKeySerializer, - config.removeOnUndefined, - config.syncCondition - ); + + nextState = reducer(nextState, action); + + if (action.type !== INIT_ACTION) { + syncStateUpdate( + nextState, + stateKeys, + config.storage, + config.storageKeySerializer, + config.removeOnUndefined, + config.syncCondition, + ); + } + return nextState; }; };