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

Convert the store to using promises #6

Merged
merged 12 commits into from
Aug 8, 2020
2 changes: 1 addition & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Expensify from './Expensify.js';
import Expensify from './Expensify';
import React from 'react';

export default () => <Expensify />;
15 changes: 15 additions & 0 deletions src/CONFIG.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// TODO: Figure out how to determine prod/dev on mobile, etc.
const IS_IN_PRODUCTION = false;

export default {
PUSHER: {
APP_KEY: IS_IN_PRODUCTION ? '268df511a204fbb60884' : 'ac6d22b891daae55283a',
AUTH_URL: IS_IN_PRODUCTION ? 'https://www.expensify.com' : 'https://www.expensify.com.dev',
CLUSTER: 'mt1',
},
EXPENSIFY: {
PARTNER_NAME: IS_IN_PRODUCTION ? 'chat-expensify-com' : 'android',
PARTNER_PASSWORD: IS_IN_PRODUCTION ? 'e21965746fd75f82bb66' : 'c3a9ac418ea3f152aae2',
API_ROOT: IS_IN_PRODUCTION ? 'https://www.expensify.com/api?' : 'https://www.expensify.com.dev/api?',
}
};
14 changes: 7 additions & 7 deletions src/Expensify.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {init as StoreInit} from './store/Store.js';
import SignInPage from './page/SignInPage.js';
import HomePage from './page/HomePage/HomePage.js';
import * as Store from './store/Store.js';
import * as ActiveClientManager from './lib/ActiveClientManager.js';
import {verifyAuthToken} from './store/actions/SessionActions.js';
import STOREKEYS from './store/STOREKEYS.js';
import {init as StoreInit} from './store/Store';
import SignInPage from './page/SignInPage';
import HomePage from './page/HomePage/HomePage';
import * as Store from './store/Store';
import * as ActiveClientManager from './lib/ActiveClientManager';
import {verifyAuthToken} from './store/actions/SessionActions';
import STOREKEYS from './store/STOREKEYS';
import React, {Component} from 'react';
import {Route, Router, Redirect, Switch} from './lib/Router';

Expand Down
6 changes: 0 additions & 6 deletions src/config.js

This file was deleted.

6 changes: 3 additions & 3 deletions src/lib/ActiveClientManager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Guid from './Guid.js';
import * as Store from '../store/Store.js';
import STOREKEYS from '../store/STOREKEYS.js';
import Guid from './Guid';
import * as Store from '../store/Store';
import STOREKEYS from '../store/STOREKEYS';

const clientID = Guid();

Expand Down
2 changes: 1 addition & 1 deletion src/lib/DateUtils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import moment from 'moment';
import Str from './Str.js';
import Str from './Str';

// Non-Deprecated Methods

Expand Down
2 changes: 1 addition & 1 deletion src/lib/ExpensiMark.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Str from './Str.js';
import Str from './Str';

export default class ExpensiMark {
constructor() {
Expand Down
84 changes: 41 additions & 43 deletions src/lib/Network.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as $ from 'jquery';
import * as Store from '../store/Store.js';
import * as Store from '../store/Store';
import CONFIG from '../CONFIG';
import STOREKEYS from '../store/STOREKEYS';

let isAppOffline = false;

Expand All @@ -9,27 +11,29 @@ let isAppOffline = false;
* @param {string} command
* @param {mixed} data
* @param {string} [type]
* @returns {$.Deferred}
* @returns {Promise}
*/
async function request(command, data, type = 'post') {
console.debug(`Making "${command}" ${type} request`);
const formData = new FormData();
formData.append('authToken', await Store.get('session', 'authToken'));
for (const property in data) {
formData.append(property, data[property]);
}
try {
let response = await fetch(
`https://www.expensify.com.dev/api?command=${command}`,
{
method: type,
body: formData,
},
);
return await response.json();
} catch (error) {
isAppOffline = true;
}
function request(command, data, type = 'post') {
return Store.get(STOREKEYS.SESSION, 'authToken')
.then((authToken) => {
const formData = new FormData();
formData.append('authToken', authToken);
_.each(data, (val, key) => {
formData.append(key, val);
});
return formData;
})
.then((formData) => {
return fetch(
`${CONFIG.EXPENSIFY.API_ROOT}command=${command}`,
{
method: type,
body: formData,
},
)
})
.then(response => response.json())
.catch(() => isAppOffline = true);
}

// Holds a queue of all the write requests that need to happen
Expand All @@ -39,20 +43,18 @@ const delayedWriteQueue = [];
* A method to write data to the API in a delayed fashion that supports the app being offline
*
* @param {string} command
* @param {midex} data
* @returns {$.Deferred}
* @param {mixed} data
* @returns {Promise}
*/
function delayedWrite(command, data, cb) {
const promise = $.Deferred();

// Add the write request to a queue of actions to perform
delayedWriteQueue.push({
command,
data,
promise,
function delayedWrite(command, data) {
return new Promise((resolve) => {
// Add the write request to a queue of actions to perform
delayedWriteQueue.push({
command,
data,
callback: resolve,
});
});

return promise;
}

/**
Expand All @@ -61,27 +63,23 @@ function delayedWrite(command, data, cb) {
function processWriteQueue() {
if (isAppOffline) {
// Make a simple request to see if we're online again
request('Get', null, 'get').done(() => {
isAppOffline = false;
});
request('Get', null, 'get')
.then(() => isAppOffline = false);
return;
}

if (delayedWriteQueue.length === 0) {
return;
}

for (let i = 0; i < delayedWriteQueue.length; i++) {
// Take the request object out of the queue and make the request
const delayedWriteRequest = delayedWriteQueue.shift();

_.each(delayedWriteQueue, (delayedWriteRequest) => {
request(delayedWriteRequest.command, delayedWriteRequest.data)
.done(delayedWriteRequest.promise.resolve)
.fail(() => {
.then(delayedWriteRequest.callback)
.catch(() => {
// If the request failed, we need to put the request object back into the queue
delayedWriteQueue.push(delayedWriteRequest);
});
}
});
}

// TODO: Figure out setInterval
Expand Down
53 changes: 53 additions & 0 deletions src/lib/PersistentStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ function get(key) {
});
};

/**
* Get the data for multiple keys
*
* @param {string[]} keys
* @returns {Promise}
*/
function multiGet(keys) {
// AsyncStorage returns the data in an array format like:
// [ ['@MyApp_user', 'myUserValue'], ['@MyApp_key', 'myKeyValue'] ]
// This method will transform the data into a better JSON format like:
// {'@MyApp_user': 'myUserValue', '@MyApp_key': 'myKeyValue'}
return AsyncStorage.multiGet(keys)
.then(arrayOfData => _.reduce(arrayOfData, (finalData, keyValuePair) => ({
...finalData,
[keyValuePair[0]]: JSON.parse(keyValuePair[1]),
}), {}))
.catch((err) => {
console.error(`Unable to get item from persistent storage. Keys: ${JSON.stringify(keys)} Error: ${err}`);
});
}

/**
* Write a key to storage
*
Expand All @@ -32,6 +53,24 @@ function set(key, val) {
return AsyncStorage.setItem(key, JSON.stringify(val));
};

/**
* Set multiple keys at once
*
* @param {object} data where the keys and values will be stored
* @returns {Promise|Promise<void>|*}
*/
function multiSet(data) {
// AsyncStorage expenses the data in an array like:
// [["@MyApp_user", "value_1"], ["@MyApp_key", "value_2"]]
// This method will transform the params from a better JSON format like:
// {'@MyApp_user': 'myUserValue', '@MyApp_key': 'myKeyValue'}
const keyValuePairs = _.reduce(data, (finalArray, val, key) => ([
...finalArray,
[key, JSON.stringify(val)],
]), []);
return AsyncStorage.multiSet(keyValuePairs);
}

/**
* Empty out the storage (like when the user signs out)
*
Expand All @@ -41,8 +80,22 @@ function clear() {
return AsyncStorage.clear();
};

/**
* Merges `val` into an existing key. Best used when updating an existing object
*
* @param {string} key
* @param {mixed} val
* @returns {Promise}
*/
function merge(key, val) {
return AsyncStorage.mergeItem(key, val);
}

export {
get,
multiGet,
set,
multiSet,
merge,
clear,
};
6 changes: 3 additions & 3 deletions src/page/SignInPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
TextInput,
View,
} from 'react-native';
import * as Store from '../store/Store.js';
import {signIn} from '../store/actions/SessionActions.js';
import STOREKEYS from '../store/STOREKEYS.js';
import * as Store from '../store/Store';
import {signIn} from '../store/actions/SessionActions';
import STOREKEYS from '../store/STOREKEYS';

export default class App extends Component {
constructor(props) {
Expand Down
73 changes: 59 additions & 14 deletions src/store/Store.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as _ from 'lodash';
import * as PersistentStorage from '../lib/PersistentStorage.js';
import {get as lodashGet} from 'lodash';
import * as PersistentStorage from '../lib/PersistentStorage';

// Holds all of the callbacks that have registered for a specific key pattern
const callbackMapping = {};
Expand Down Expand Up @@ -54,7 +54,7 @@ function unsubscribe(keyPattern, cb) {
* @param {mixed} data
*/
function keyChanged(key, data) {
for (const [keyPattern, callbacks] of Object.entries(callbackMapping)) {
_.each(callbackMapping, (callbacks, keyPattern) => {
const regex = RegExp(keyPattern);

// If there is a callback whose regex matches the key that was changed, then the callback for that regex
Expand All @@ -65,22 +65,23 @@ function keyChanged(key, data) {
callback(data);
}
}
}
});
}

/**
* Write a value to our store with the given key
*
* @param {string} key
* @param {mixed} val
* @returns {Promise}
*/
function set(key, val) {
// Write the thing to local storage, which will trigger a storage event for any other tabs open on this domain
PersistentStorage.set(key, val);

// The storage event doesn't trigger for the current window, so just call keyChanged() manually to mimic
// the storage event
keyChanged(key, val);

// Write the thing to persistent storage, which will trigger a storage event for any other tabs open on this domain
return PersistentStorage.set(key, val);
}

/**
Expand All @@ -92,12 +93,56 @@ function set(key, val) {
* we are looking for doesn't exist in the object yet
* @returns {*}
*/
const get = async (key, extraPath, defaultValue) => {
const val = await PersistentStorage.get(key);
if (extraPath) {
return _.get(val, extraPath, defaultValue);
}
return val;
function get(key, extraPath, defaultValue) {
return PersistentStorage.get(key)
.then((val) => {
if (extraPath) {
return lodashGet(val, extraPath, defaultValue);
}
return val;
});
};

export {subscribe, unsubscribe, set, get, init};
/**
* Get multiple keys of data
*
* @param {string[]} keys
* @returns {Promise}
*/
function multiGet(keys) {
return PersistentStorage.multiGet(keys);
}

/**
* Sets multiple keys and values. Example
* Store.multiSet({'key1': 'a', 'key2': 'b'});
*
* @param {object} data
* @returns {Promise}
*/
function multiSet(data) {
return PersistentStorage.multiSet(data)
.then(() => {
_.each(data, (val, key) => keyChanged(key, val));
});
}

/**
* Clear out all the data in the store
*
* @returns {Promise}
*/
function clear() {
return PersistentStorage.clear();
}

export {
subscribe,
unsubscribe,
set,
multiSet,
get,
multiGet,
clear,
init
};
Loading