Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
73eb542
redux-persist [nfc]: Stop using `config.debounce`, since we don't set…
chrisbobbe Apr 2, 2021
51321e8
redux-persist [nfc]: Inline `debounce` variable in createPersistor.
chrisbobbe Apr 2, 2021
2861123
redux-persist [nfc]: Use 0 instead of `false` in `setInterval`.
chrisbobbe Apr 2, 2021
0fd6884
redux-persist [nfc]: Stop using `config.transforms`, since we don't s…
chrisbobbe Apr 2, 2021
c0f2b91
redux-persist [nfc]: Inline `transforms` variables.
chrisbobbe Apr 2, 2021
87a4a73
createPersistor [nfc]: Stop using more config values that we don't set.
chrisbobbe Jul 7, 2021
248e53e
createPersistor [nfc]: Declare three functions more directly.
chrisbobbe Jul 7, 2021
3b06472
createPersistor [nfc]: Simplify callback requirement for `stateIterat…
chrisbobbe Jul 7, 2021
0176553
createPersistor [nfc]: Simplify `stateIterator` further.
chrisbobbe Jul 7, 2021
6b5704d
createPersistor [nfc]: Inline `stateIterator`.
chrisbobbe Jul 7, 2021
3aae30d
createPersistor [nfc]: Inline `stateGetter`.
chrisbobbe Jul 7, 2021
cb4e34c
createPersistor [nfc]: Inline `stateSetter`.
chrisbobbe Jul 7, 2021
8abe7bd
createPersistor [nfc]: Inline `stateInit`.
chrisbobbe Jul 7, 2021
390324e
createPersistor [nfc]: Use `const`, not `let`, for some local variables.
chrisbobbe Jul 7, 2021
dc397b2
redux-persist: Switch `console.log` and `console.warn` to `logging.wa…
chrisbobbe Jun 3, 2021
e4f84c7
redux-persist: Remove potentially a lot of data from a log.
chrisbobbe Jun 18, 2021
815fc23
redux-persist [nfc]: Remove a noisy log line in `autoRehydrate`.
chrisbobbe Jun 3, 2021
bf39f17
redux-persist: Start sending some bug-tracking logs in production.
chrisbobbe Jun 3, 2021
adac929
redux-persist: Remove unnecessary conditional in `createPersistor`.
chrisbobbe Jul 7, 2021
bb48e24
ZulipAsyncStorage [nfc]: Make `setItem` more compact with a ternary.
chrisbobbe Apr 22, 2021
9027646
ZulipAsyncStorage: Implement `multiSet`.
chrisbobbe Apr 22, 2021
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
5 changes: 5 additions & 0 deletions flow-typed/@react-native-community/async-storage_v1.x.x.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ declare module '@react-native-community/async-storage' {

setItem(key: string, value: string, callback?: ?(error: ?Error) => void): Promise<null>,

multiSet(
keyValuePairs: Array<Array<string>>,
callback?: ?(errors: ?$ReadOnlyArray<?Error>) => void,
): Promise<null>,

removeItem(key: string, callback?: ?(error: ?Error) => void): Promise<null>,

getAllKeys(
Expand Down
23 changes: 19 additions & 4 deletions src/boot/ZulipAsyncStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,25 @@ export default class ZulipAsyncStorage {
}

static async setItem(key: string, value: string) {
if (!NativeModules.TextCompressionModule) {
return AsyncStorage.setItem(key, value);
}
return AsyncStorage.setItem(key, await NativeModules.TextCompressionModule.compress(value));
return AsyncStorage.setItem(
key,
NativeModules.TextCompressionModule
? await NativeModules.TextCompressionModule.compress(value)
: value,
);
}

static async multiSet(keyValuePairs: Array<Array<string>>) {
return AsyncStorage.multiSet(
NativeModules.TextCompressionModule
? await Promise.all(
keyValuePairs.map(async ([key, value]) => [
key,
await NativeModules.TextCompressionModule.compress(value),
]),
)
: keyValuePairs,
);
}

static removeItem = AsyncStorage.removeItem;
Expand Down
78 changes: 73 additions & 5 deletions src/boot/__tests__/ZulipAsyncStorage-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,61 @@ describe('setItem', () => {
});
});

describe('multiSet', () => {
const keyValuePairs = [
['foo', 'bar'],
['food', 'bard'],
];

// For checking that AsyncStorage.multiSet is called in ways we expect.
const asyncStorageMultiSetSpy = jest.spyOn(AsyncStorage, 'multiSet');
beforeEach(() => asyncStorageMultiSetSpy.mockClear());

const run = async () => ZulipAsyncStorage.multiSet(keyValuePairs);

describe('success', () => {
// AsyncStorage provides its own mock for `.multiSet`, which gives
// success every time. So, no need to mock that behavior
// ourselves.

test('resolves correctly', async () => {
await run();
expect(asyncStorageMultiSetSpy).toHaveBeenCalledTimes(1);
expect(asyncStorageMultiSetSpy).toHaveBeenCalledWith(
Platform.OS === 'ios'
? keyValuePairs
: await Promise.all(
keyValuePairs.map(async ([key, value]) => [
key,
await NativeModules.TextCompressionModule.compress(value),
]),
),
);
});
});

describe('failure', () => {
// AsyncStorage provides its own mock for `.multiSet`, but it's
// not set up to simulate failure. So, mock that behavior
// ourselves, and reset to the global mock when we're done.
const globalMock = AsyncStorage.multiSet;
beforeEach(() => {
// $FlowFixMe[cannot-write] Make Flow understand about mocking.
AsyncStorage.multiSet = jest.fn(async (p: string[][]): Promise<null> => {
throw new Error();
});
});
afterAll(() => {
// $FlowFixMe[cannot-write] Make Flow understand about mocking.
AsyncStorage.multiSet = globalMock;
});

test('rejects correctly', async () => {
await expect(run()).rejects.toThrow(Error);
});
});
});

describe('getItem', () => {
const key = 'foo!';
const value = '123!';
Expand Down Expand Up @@ -135,14 +190,27 @@ describe('getItem', () => {
}
});

describe('setItem/getItem together', () => {
test('round-tripping works', async () => {
describe('set/get together', () => {
// AsyncStorage provides its own mocks for `.getItem`, `.setItem`, and
// `.multiSet`; it writes to a variable instead of storage.

test('round-tripping of single key-value pair works', async () => {
const key = eg.randString();
const value = eg.randString();

// AsyncStorage provides its own mocks for `.getItem` and
// `.setItem`; it writes to a variable instead of storage.
await ZulipAsyncStorage.setItem(key, value);
expect(await ZulipAsyncStorage.getItem(key)).toEqual(value);
});

test('round-tripping of multiple key-value pairs works', async () => {
const keyValuePairs = [
[eg.randString(), eg.randString()],
[eg.randString(), eg.randString()],
];
await ZulipAsyncStorage.multiSet(keyValuePairs);
expect(
await Promise.all(
keyValuePairs.map(async ([key, _]) => [key, await ZulipAsyncStorage.getItem(key)]),
),
).toEqual(keyValuePairs);
});
});
2 changes: 1 addition & 1 deletion src/boot/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ const store: Store<GlobalState, Action> = createStore(
// where the live state just gets filled in with the corresponding parts
// of the just-loaded state from disk. See upstream docs:
// https://github.com/rt2zz/redux-persist/tree/v4.10.2#autorehydrateconfig
autoRehydrate(),
autoRehydrate({ log: true }),
),
);

Expand Down
9 changes: 9 additions & 0 deletions src/reduxTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,15 @@ export type GlobalState = $ReadOnly<{|
users: UsersState,
|}>;

// No substate should allow `undefined`; our use of AsyncStorage
// depends on it. (This check will also complain on `null`, which I
// don't think we'd have a problem with. We could try to write this
// differently if we want to allow `null`.)
type NonMaybeProperties<O: { ... }> = $ObjMap<O, <V>(V) => $NonMaybeType<V>>;
type NonMaybeGlobalState = NonMaybeProperties<GlobalState>;
// This function definition will fail typechecking if GlobalState is wrong.
(s: GlobalState): NonMaybeGlobalState => s; // eslint-disable-line no-unused-expressions

/** A selector returning TResult, with extra parameter TParam. */
// Seems like this should be OutputSelector... but for whatever reason,
// putting that on a selector doesn't cause the result type to propagate to
Expand Down
16 changes: 8 additions & 8 deletions src/third/redux-persist/autoRehydrate.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as logging from '../../utils/logging';

import { REHYDRATE } from './constants'
import isStatePlainEnough from './utils/isStatePlainEnough'

Expand Down Expand Up @@ -37,11 +39,11 @@ export default function autoRehydrate (config = {}) {
function logPreRehydrate (preRehydrateActions) {
const concernedActions = preRehydrateActions.slice(1)
if (concernedActions.length > 0) {
console.log(`
redux-persist/autoRehydrate: %d actions were fired before rehydration completed. This can be a symptom of a race
logging.warn(`
redux-persist/autoRehydrate: ${concernedActions.length} actions were fired before rehydration completed. This can be a symptom of a race
condition where the rehydrate action may overwrite the previously affected state. Consider running these actions
after rehydration:
`, concernedActions.length, concernedActions)
after rehydration.
`, { concernedActionTypes: concernedActions.map(a => a.type) })
}
}

Expand All @@ -54,22 +56,20 @@ function defaultStateReconciler (state, inboundState, reducedState, log) {

// 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)
if (log) logging.warn('redux-persist/autoRehydrate: sub state for a key 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)
if (log) logging.warn('redux-persist/autoRehydrate: sub state for a key 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
}
54 changes: 17 additions & 37 deletions src/third/redux-persist/createPersistor.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as logging from '../../utils/logging';

import { KEY_PREFIX, REHYDRATE } from './constants'
import purgeStoredState from './purgeStoredState'
import stringify from 'json-stringify-safe'
Expand All @@ -23,32 +25,24 @@ export default function createPersistor (store, config) {
}
const blacklist = config.blacklist || []
const whitelist = config.whitelist || false
const transforms = config.transforms || []
const debounce = config.debounce || false
const keyPrefix = config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX

// 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

const storage = config.storage;

// initialize stateful values
let lastState = stateInit
let lastState = {}
let paused = false
let storesToProcess = []
let timeIterator = null

store.subscribe(() => {
if (paused) return

let state = store.getState()
const state = store.getState()

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

let key = storesToProcess.shift()
let storageKey = createStorageKey(key)
let endState = transforms.reduce((subState, transformer) => transformer.in(subState, key), stateGetter(store.getState(), key))
if (typeof endState !== 'undefined') storage.setItem(storageKey, serializer(endState)).catch(warnIfSetError(key))
}, debounce)
const key = storesToProcess.shift()
const storageKey = createStorageKey(key)
const endState = store.getState()[key]
storage.setItem(storageKey, serializer(endState)).catch(warnIfSetError(key))
}, 0)
}

lastState = state
Expand All @@ -83,15 +77,14 @@ export default function createPersistor (store, config) {
function adhocRehydrate (incoming, options = {}) {
let state = {}
if (options.serial) {
stateIterator(incoming, (subState, key) => {
Object.keys(incoming).forEach((key) => {
const subState = incoming[key]
try {
let data = deserializer(subState)
let value = transforms.reduceRight((interState, transformer) => {
return transformer.out(interState, key)
}, data)
state = stateSetter(state, key, value)
let value = data
state[key] = value
} catch (err) {
if (process.env.NODE_ENV !== 'production') console.warn(`Error rehydrating data for key "${key}"`, subState, err)
logging.warn('Error rehydrating data for a key', { key, err })
}
})
} else state = incoming
Expand Down Expand Up @@ -120,7 +113,7 @@ export default function createPersistor (store, config) {

function warnIfSetError (key) {
return function setError (err) {
if (err && process.env.NODE_ENV !== 'production') { console.warn('Error storing data for key:', key, err) }
if (err) { logging.warn('Error storing data for key:', key, err) }
}
}

Expand All @@ -146,16 +139,3 @@ function rehydrateAction (data) {
payload: data
}
}

function defaultStateIterator (collection, callback) {
return Object.keys(collection).forEach((key) => callback(collection[key], key))
}

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

function defaultStateSetter (state, key, value) {
state[key] = value
return state
}
13 changes: 6 additions & 7 deletions src/third/redux-persist/getStoredState.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as logging from '../../utils/logging';

import { KEY_PREFIX } from './constants'

export default function getStoredState (config, onComplete) {
Expand All @@ -12,7 +14,6 @@ export default function getStoredState (config, onComplete) {
}
const blacklist = config.blacklist || []
const whitelist = config.whitelist || false
const transforms = config.transforms || []
const keyPrefix = config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX

let restoredState = {}
Expand All @@ -28,7 +29,7 @@ export default function getStoredState (config, onComplete) {
}

if (err) {
if (process.env.NODE_ENV !== 'production') console.warn('redux-persist/getStoredState: Error in storage.getAllKeys')
logging.warn('redux-persist/getStoredState: Error in storage.getAllKeys')
complete(err)
return
}
Expand All @@ -47,7 +48,7 @@ export default function getStoredState (config, onComplete) {
} catch (e) {
err = e
}
if (err && process.env.NODE_ENV !== 'production') console.warn('redux-persist/getStoredState: Error restoring data for key:', key, err)
if (err) logging.warn('redux-persist/getStoredState: Error restoring data for a key.', { key, err })
else restoredState[key] = rehydrate(key, serialized)
completionCount += 1
if (completionCount === restoreCount) complete(null, restoredState)
Expand All @@ -60,11 +61,9 @@ export default function getStoredState (config, onComplete) {

try {
let data = deserializer(serialized)
state = transforms.reduceRight((subState, transformer) => {
return transformer.out(subState, key)
}, data)
state = data
} catch (err) {
if (process.env.NODE_ENV !== 'production') console.warn('redux-persist/getStoredState: Error restoring data for key:', key, err)
logging.warn('redux-persist/getStoredState: Error restoring data for a key.', { key, err })
}

return state
Expand Down
2 changes: 0 additions & 2 deletions src/third/redux-persist/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ declare type Config = {|
blacklist?: Blacklist,
whitelist?: Whitelist,
storage?: Storage,
transforms?: Array<Object>,
debounce?: number,
serialize?: boolean | Function,
deserialize?: boolean | Function,
keyPrefix?: string,
Expand Down
3 changes: 2 additions & 1 deletion src/third/redux-persist/persistStore.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as logging from '../../utils/logging';
import { cacheKeys } from '../../boot/store';

import { REHYDRATE } from './constants'
Expand All @@ -9,7 +10,7 @@ export default function persistStore (store, config = {}, onComplete) {
// defaults
// @TODO remove shouldRestore
const shouldRestore = !config.skipRestore
if (process.env.NODE_ENV !== 'production' && config.skipRestore) console.warn('redux-persist: config.skipRestore has been deprecated. If you want to skip restoration use `createPersistor` instead')
if (config.skipRestore) logging.warn('redux-persist: config.skipRestore has been deprecated. If you want to skip restoration use `createPersistor` instead')

let purgeKeys = null

Expand Down
Loading