Skip to content
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
29,647 changes: 29,647 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions src/components/CommunicationQueue/CommunicationQueue.actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
QUEUE_EVENT,
REMOVE_EVENTS,
INCREMENT_RETRY,
CLEAR_QUEUE
} from './CommunicationQueue.constants';

export const queueEvent = payload => ({
type: QUEUE_EVENT,
payload
});

export const removeEvents = eventIds => ({
type: REMOVE_EVENTS,
payload: eventIds
});

export const incrementRetry = eventId => ({
type: INCREMENT_RETRY,
payload: eventId
});

export const clearQueue = () => ({
type: CLEAR_QUEUE
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const QUEUE_EVENT = 'QUEUE_EVENT';
export const REMOVE_EVENTS = 'REMOVE_EVENTS';
export const INCREMENT_RETRY = 'INCREMENT_RETRY';
export const CLEAR_QUEUE = 'CLEAR_QUEUE';
60 changes: 60 additions & 0 deletions src/components/CommunicationQueue/CommunicationQueue.reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
QUEUE_EVENT,
REMOVE_EVENTS,
INCREMENT_RETRY,
CLEAR_QUEUE
} from './CommunicationQueue.constants';

const MAX_QUEUE_SIZE = 500;

const initialState = {
events: []
};

function communicationQueueReducer(state = initialState, action) {
switch (action.type) {
case QUEUE_EVENT: {
const updatedEvents = [...state.events, action.payload];

return {
...state,
events: updatedEvents.slice(-MAX_QUEUE_SIZE)
};
}

case REMOVE_EVENTS:
return {
...state,
events: state.events.filter(
event => !action.payload.includes(event.id)
)
};

case INCREMENT_RETRY:
return {
...state,
events: state.events.map(event =>
event.id === action.payload
? {
...event,
retryCount: (event.retryCount || 0) + 1,
nextRetryAt:
Date.now() +
Math.min(1000 * 2 ** ((event.retryCount || 0) + 1), 60000)
}
: event
)
};

case CLEAR_QUEUE:
return {
...state,
events: []
};

default:
return state;
}
}

export default communicationQueueReducer;
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import configureStore, { getStore } from './store';
import SubscriptionProvider from './providers/SubscriptionProvider';
import { PAYPAL_CLIENT_ID } from './constants';
import { initializeAppInsights } from './appInsights';
import { startCommunicationQueueService } from './services/communicationQueue.service';

initializeAppInsights();
const { persistor } = configureStore();
const store = getStore();

const dndOptions = {
enableTouchEvents: true,
enableMouseEvents: true,
Expand All @@ -45,6 +47,7 @@ const renderApp = () => {
if (isCordova()) {
initCordovaPlugins();
}
startCommunicationQueueService();
ReactDOM.render(
<Provider store={store}>
<PersistGate persistor={persistor}>
Expand Down
3 changes: 3 additions & 0 deletions src/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import boardReducer from './components/Board/Board.reducer';
import communicatorReducer from './components/Communicator/Communicator.reducer';
import notificationsReducer from './components/Notifications/Notifications.reducer';
import subscriptionProviderReducer from './providers/SubscriptionProvider/SubscriptionProvider.reducer';
import communicationQueueReducer from './components/CommunicationQueue/CommunicationQueue.reducer';
import { DEFAULT_BOARDS } from '../src/helpers';

localForage.config({
Expand Down Expand Up @@ -151,8 +152,10 @@ export default function createReducer() {
speech: speechProviderReducer,
board: boardReducer,
communicator: communicatorReducer,
communicationQueue: communicationQueueReducer,
scanner: scannerProviderReducer,
notification: notificationsReducer,
subscription: subscriptionProviderReducer

});
}
80 changes: 80 additions & 0 deletions src/services/communicationQueue.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { getStore } from '../store';

import {
removeEvents,
incrementRetry
} from '../components/CommunicationQueue/CommunicationQueue.actions';

const FLUSH_INTERVAL = 15000;
const BATCH_SIZE = 25;
const MAX_RETRIES = 5;

let flushInterval = null;

export function startCommunicationQueueService() {
if (flushInterval) {
return;
}

flushInterval = setInterval(() => {
flushQueue();
}, FLUSH_INTERVAL);

window.addEventListener('online', flushQueue);

document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
flushQueue();
}
});

window.addEventListener('pagehide', flushQueue);
}

export async function flushQueue() {
if (!navigator.onLine) {
return;
}

const store = getStore();

if (!store) {
return;
}

const state = store.getState();

const queuedEvents = state.communicationQueue?.events || [];

if (!queuedEvents.length) {
return;
}

const now = Date.now();

const retryableEvents = queuedEvents.filter(
event => !event.nextRetryAt || event.nextRetryAt <= now
);

const batch = retryableEvents.slice(0, BATCH_SIZE);

if (!batch.length) {
return;
}

try {
console.log('Flushing communication events:', batch);

// API request will go here later

store.dispatch(removeEvents(batch.map(event => event.id)));
} catch (error) {
console.error('Failed to flush communication queue', error);

batch.forEach(event => {
if ((event.retryCount || 0) < MAX_RETRIES) {
store.dispatch(incrementRetry(event.id));
}
});
}
}
Loading