Skip to content

Commit

Permalink
Merge pull request #6 from AndrewGable/tgolen-async-promise
Browse files Browse the repository at this point in the history
Convert the store to using promises
  • Loading branch information
AndrewGable authored Aug 8, 2020
2 parents f72386e + 8944ef2 commit e70f822
Show file tree
Hide file tree
Showing 13 changed files with 494 additions and 246 deletions.
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

0 comments on commit e70f822

Please sign in to comment.