Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding async support for localstorage (#68) #69

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 64 additions & 19 deletions spec/index_spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
declare var beforeEachProviders, it, describe, expect, inject;
require('es6-shim');
import { syncStateUpdate, rehydrateApplicationState, dateReviver } from '../src/index';
import { syncStateUpdate, rehydrateApplicationState, dateReviver, AsyncOperations } from '../src/index';
import * as CryptoJS from 'crypto-js';

// Very simple classes to test serialization options. They cover string, number, date, and nested classes
Expand Down Expand Up @@ -109,6 +109,11 @@ describe('ngrxLocalStorage', () => {
const primitiveStr = 'string is not an object';
const initialStatePrimitiveStr = { state: primitiveStr };

const dumbAsyncOperations: AsyncOperations = {
resolveOnUpdate: () => { },
catchOnUpdate: () => { }
};

it('simple', () => {
// This tests a very simple state object syncing to mock Storage
// Since we're not specifiying anything for rehydration, the roundtrip
Expand All @@ -117,7 +122,7 @@ describe('ngrxLocalStorage', () => {
let s = new MockStorage();
let skr = mockStorageKeySerializer;

syncStateUpdate(initialState, ['state'], s, skr, false);
syncStateUpdate(initialState, ['state'], s, skr, false, dumbAsyncOperations);

let raw = s.getItem('state');
expect(raw).toEqual(t1Json);
Expand All @@ -133,7 +138,7 @@ describe('ngrxLocalStorage', () => {
const s = new MockStorage();
const skr = mockStorageKeySerializer;

syncStateUpdate(initialStatePrimitiveStr, ['state'], s, skr, false);
syncStateUpdate(initialStatePrimitiveStr, ['state'], s, skr, false, dumbAsyncOperations);

const raw = s.getItem('state');
expect(raw).toEqual(primitiveStr);
Expand All @@ -153,7 +158,7 @@ describe('ngrxLocalStorage', () => {
let initialState = { state: t1 };
let keys = [{ state: ['astring', 'aclass'] }];

syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);

let raw = s.getItem('state');
expect(raw).toEqual(JSON.stringify(t1Filtered));
Expand All @@ -173,7 +178,7 @@ describe('ngrxLocalStorage', () => {
let initialState = { state: t1 };
let keys = [{ state: TypeA.reviver }];

syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);

let finalState: any = rehydrateApplicationState(keys, s, skr);
expect(JSON.stringify(finalState)).toEqual(JSON.stringify(initialState));
Expand All @@ -189,7 +194,7 @@ describe('ngrxLocalStorage', () => {
let initialState = { state: t1 };
let keys = [{ state: { reviver: TypeA.reviver } }];

syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);

let finalState: any = rehydrateApplicationState(keys, s, skr);
expect(JSON.stringify(finalState)).toEqual(JSON.stringify(initialState));
Expand All @@ -206,7 +211,7 @@ describe('ngrxLocalStorage', () => {
let initialState = { filtered: t1 };
let keys = [{ filtered: { filter: ['astring', 'aclass'] } }];

syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);

let raw = s.getItem('filtered');
expect(raw).toEqual(JSON.stringify(t1Filtered));
Expand All @@ -228,7 +233,7 @@ describe('ngrxLocalStorage', () => {
let initialState = { replacer: t1 };
let keys = [{ replacer: { reviver: TypeA.replacer } }];

syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);

let finalState: any = rehydrateApplicationState(keys, s, skr);
expect(JSON.stringify(finalState)).toEqual(JSON.stringify({ replacer: t1Filtered }));
Expand All @@ -247,7 +252,7 @@ describe('ngrxLocalStorage', () => {
let initialState = { replacer: t1 };
let keys = [{ replacer: { replacer: ['astring', 'adate', 'anumber'], space: 2 } }];

syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);

// We want to validate the space parameter, but don't want to trip up on OS specific newlines, so filter the newlines out and
// compare against the literal string.
Expand All @@ -270,7 +275,7 @@ describe('ngrxLocalStorage', () => {
let initialState = { state: t1 };
let keys = [{ state: { serialize: TypeA.serialize, deserialize: TypeA.deserialize } }];

syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);

let finalState: any = rehydrateApplicationState(keys, s, skr);
expect(JSON.stringify(finalState)).toEqual(initialStateJson);
Expand All @@ -282,14 +287,14 @@ describe('ngrxLocalStorage', () => {
// This tests that the state slice is removed when the state it's undefined
let s = new MockStorage();
let skr = mockStorageKeySerializer;
syncStateUpdate(initialState, ['state'], s, skr, true);
syncStateUpdate(initialState, ['state'], s, skr, true, dumbAsyncOperations);

// do update
let raw = s.getItem('state');
expect(raw).toEqual(t1Json);

// ensure that it's erased
syncStateUpdate(undefinedState, ['state'], s, skr, true);
syncStateUpdate(undefinedState, ['state'], s, skr, true, dumbAsyncOperations);
raw = s.getItem('state');
expect(raw).toBeFalsy();
});
Expand All @@ -298,14 +303,14 @@ describe('ngrxLocalStorage', () => {
// This tests that the state slice is keeped when the state it's undefined
let s = new MockStorage();
let skr = mockStorageKeySerializer;
syncStateUpdate(initialState, ['state'], s, skr, false);
syncStateUpdate(initialState, ['state'], s, skr, false, dumbAsyncOperations);

// do update
let raw = s.getItem('state');
expect(raw).toEqual(t1Json);

// test update doesn't erase when it's undefined
syncStateUpdate(undefinedState, ['state'], s, skr, false);
syncStateUpdate(undefinedState, ['state'], s, skr, false, dumbAsyncOperations);
raw = s.getItem('state');
expect(raw).toEqual(t1Json);
});
Expand All @@ -316,7 +321,7 @@ describe('ngrxLocalStorage', () => {
let initialState = { state: t1 };
let keys = [{ state: { encrypt: TypeC.encrypt, decrypt: TypeC.decrypt } }];

syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);
// Decript stored value and compare with the on-memory state
let raw = s.getItem('state');
expect(TypeC.decrypt(raw)).toEqual(JSON.stringify(initialState.state));
Expand All @@ -333,14 +338,14 @@ describe('ngrxLocalStorage', () => {
let keys;
keys = [{ state: { encrypt: TypeC.encrypt } }];

syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);
// Stored value must not be encripted due to decrypt function is not present, so must be equal to the on-memory state
let raw = s.getItem('state');
expect(raw).toEqual(JSON.stringify(initialState.state));

// Stored value must not be encripted, if one of the encryption functions are not present
keys = [{ state: { decrypt: TypeC.decrypt } }];
syncStateUpdate(initialState, keys, s, skr, false);
syncStateUpdate(initialState, keys, s, skr, false, dumbAsyncOperations);
raw = s.getItem('state');
expect(raw).toEqual(JSON.stringify(initialState.state));
});
Expand All @@ -349,8 +354,7 @@ describe('ngrxLocalStorage', () => {
// This tests that storage key serializer are working.
let s = new MockStorage();
let skr = (key) => `this_key` + key;
console.log(skr('a'));
syncStateUpdate(initialState, ['state'], s, skr, false);
syncStateUpdate(initialState, ['state'], s, skr, false, dumbAsyncOperations);

let raw = s.getItem('1232342');
expect(raw).toBeNull();
Expand All @@ -361,4 +365,45 @@ describe('ngrxLocalStorage', () => {
expect(t1 instanceof TypeA).toBeTruthy();
expect(finalState.simple instanceof TypeA).toBeFalsy();
});

it('simple async string', async (done) => {
const s = new MockStorage();
const skr = mockStorageKeySerializer;

const asyncOperations: AsyncOperations = {
resolveOnUpdate: (resolveObject) => {
expect(resolveObject.key).toEqual('state');
expect(resolveObject.stateSlice).toEqual(primitiveStr);
done();
},
catchOnUpdate: () => {
// Shouldn't reach here
expect(true).toBeFalsy();
done();
}
};
syncStateUpdate(initialStatePrimitiveStr, ['state'], s, skr, false, asyncOperations);
});

it('simple async string throw error', async (done) => {
const s = new MockStorage();
const skr = mockStorageKeySerializer;

s.setItem = (s: string, v: string) => {
throw('async error');
};

const asyncOperations: AsyncOperations = {
resolveOnUpdate: (resolveObject) => {
// Shouldn't reach here
expect(true).toBeFalsy();
done();
},
catchOnUpdate: (error) => {
expect(error.toString()).toBe('async error');
done();
}
};
syncStateUpdate(initialStatePrimitiveStr, ['state'], s, skr, false, asyncOperations);
});
});
55 changes: 38 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const rehydrateApplicationState = (keys: any[], storage: Storage, storage
}, {});
};

export const syncStateUpdate = (state: any, keys: any[], storage: Storage, storageKeySerializer: (key: string) => string, removeOnUndefined: boolean) => {
export const syncStateUpdate = (state: any, keys: any[], storage: Storage, storageKeySerializer: (key: string) => string, removeOnUndefined: boolean, asyncOperations: AsyncOperations) => {
keys.forEach(key => {

let stateSlice = state[key];
Expand Down Expand Up @@ -139,23 +139,33 @@ export const syncStateUpdate = (state: any, keys: any[], storage: Storage, stora
key = name;
}

if (typeof (stateSlice) !== 'undefined') {
try {
if (encrypt) {
// ensure that a string message is passed
stateSlice = encrypt(typeof stateSlice === 'string' ? stateSlice : JSON.stringify(stateSlice, replacer, space));
new Promise((resolve, reject) => {
const serializedKey = storageKeySerializer(key);

if (typeof (stateSlice) !== 'undefined') {
try {
if (encrypt) {
// ensure that a string message is passed
stateSlice = encrypt(typeof stateSlice === 'string' ? stateSlice : JSON.stringify(stateSlice, replacer, space));
}
storage.setItem(serializedKey, typeof stateSlice === 'string' ? stateSlice : JSON.stringify(stateSlice, replacer, space));
resolve({ key, stateSlice });
} catch (e) {
console.warn('Unable to save state to localStorage:', e);
reject(e);
}
} else if (typeof (stateSlice) === 'undefined' && removeOnUndefined) {
try {
storage.removeItem(serializedKey);
resolve({ key, stateSlice });
} catch (e) {
console.warn(`Exception on removing/cleaning undefined '${key}' state`, e);
reject(e);
}
storage.setItem(storageKeySerializer(key), typeof stateSlice === 'string' ? stateSlice : JSON.stringify(stateSlice, replacer, space));
} catch (e) {
console.warn('Unable to save state to localStorage:', e);
}
} else if (typeof (stateSlice) === 'undefined' && removeOnUndefined) {
try {
storage.removeItem(storageKeySerializer(key));
} catch (e) {
console.warn(`Exception on removing/cleaning undefined '${key}' state`, e);
}
}
})
.then(asyncOperations.resolveOnUpdate)
.catch(asyncOperations.catchOnUpdate);
});
};

Expand All @@ -169,6 +179,11 @@ export const localStorageSync = (config: LocalStorageConfig) => (reducer: any) =
config.storageKeySerializer = (key) => key;
}

config.asyncOperations = Object.assign({}, {
resolveOnUpdate: () => { },
catchOnUpdate: () => { }
}, config.asyncOperations);

const stateKeys = validateStateKeys(config.keys);
const rehydratedState = config.rehydrate ? rehydrateApplicationState(stateKeys, config.storage, config.storageKeySerializer) : undefined;

Expand All @@ -181,7 +196,7 @@ export const localStorageSync = (config: LocalStorageConfig) => (reducer: any) =
state = Object.assign({}, state, rehydratedState);
}
const nextState = reducer(state, action);
syncStateUpdate(nextState, stateKeys, config.storage, config.storageKeySerializer, config.removeOnUndefined);
syncStateUpdate(nextState, stateKeys, config.storage, config.storageKeySerializer, config.removeOnUndefined, config.asyncOperations);
return nextState;
};
};
Expand Down Expand Up @@ -211,4 +226,10 @@ export interface LocalStorageConfig {
storage?: Storage;
removeOnUndefined?: boolean;
storageKeySerializer?: (key: string) => string;
asyncOperations?: AsyncOperations;
}

export interface AsyncOperations {
resolveOnUpdate: (resolveObject: any) => any;
catchOnUpdate: (error) => any;
}