Offline Meteor/React Native app and offline actions

Hello,

I usually find a solution to a problem with Google/Stackoverflow/Meteor forums or find someone to fix it. However, this time I’m completely stuck. Here is my issue:

I’ve created a field service management app with Meteor/React/React Native. Managers can create tasks/work orders on the web app, the field team can create reports/work logs on the mobile app.

However, the region I’m in has a poor internet connection in some areas. So the offline feature is essential for both viewing tasks/reports but also creating the reports (not the tasks). To solve the offline data access, I’ve followed Spencer Carli’s excellent tutorial https://hackernoon.com/offline-first-react-native-meteor-apps-2bee8e976ec7

It’s working very well.

Things go wrong for creating offline reports. Basically, I have created an action queue in redux store to handle offline reports creation. When connection is back online, actions are being mapped, reports are created on the server, actions are then deleted, and offline created reports are being deleted from mini mongo and redux because anyway, once created on the server, it’s auto synced again.

It’s working very well BUT sometimes, especially when internet connection is slow, duplicates are created. And like, 50+ duplicates of the same report sometimes.

Here is the action queue syncing:

  async handleSync(props) {
    const data = Meteor.getData();
    const db = data && data.db;

    if (props.loading === false && props.connectionStatus === 'connected' && Meteor.userId() && db && props.actionQueue && props.actionQueue.length > 0) {
      for (let action of props.actionQueue) {
        if (action.msg === 'createReport') {
          const report = {
            organizationId: action.doc.organizationId,
            taskId: action.doc.taskId,
            status: action.doc.status,
            startedAt: action.doc.startedAt,
          };
          const result = await Meteor.call('Reports.create', report, (error, res) => {
            if (error) {
              Alert.alert(I18n.t('main.error'), `${I18n.t('main.errorSyncReport')} ${error.reason}`);
            } else {
              props.dispatch({ type: 'REMOVE_ACTION_QUEUE', payload: action.actionId });
              props.dispatch({ type: 'REMOVE_OFFLINE_REPORT', payload: action.doc._id });
              db['reports'].del(action.doc._id);
              const task = {
                organizationId: action.doc.organizationId,
                taskId: action.doc.taskId,
              };
              Meteor.call('Tasks.updateTaskStatus', task);
            }
          });
          return result;
        } 
        else if (action.msg === 'completeReport') {
          // this action is for completion of reports that have been created online
          const report = {
            organizationId: action.doc.organizationId,
            reportId: action.doc._id,
            comments: action.doc.comments,
            isTaskCompleted: action.doc.isTaskCompleted,
            completedAt: action.doc.completedAt,
            fields: action.doc.fields,
          };
          const result = await Meteor.call('Reports.completeReport', report, (error, res) => {
            if (error) {
              Alert.alert(I18n.t('main.error'), `${I18n.t('main.errorSyncReport')} ${error.reason}`);
            } else {
              props.dispatch({ type: 'REMOVE_ACTION_QUEUE', payload: action.actionId });
              const task = {
                organizationId: action.doc.organizationId,
                taskId: action.doc.taskId,
              };
              Meteor.call('Tasks.updateTaskStatus', task);
            }
          });
          return result;
        }
        else if (action.msg === 'createCompletedReport') {
          // this action is for completion of reports that have been created offline to avoid _id problems
          // so a new completed report is created and the offline report is deleted
          const report = {
            organizationId: action.doc.organizationId,
            taskId: action.doc.taskId,
            comments: action.doc.comments,
            isTaskCompleted: action.doc.isTaskCompleted,
            fields: action.doc.fields,
            status: action.doc.status,
            startedAt: action.doc.startedAt,
            completedAt: action.doc.completedAt,
          };
          const result = await Meteor.call('Reports.create', report, (error, res) => {
            if (error) {
              Alert.alert(I18n.t('main.error'), `${I18n.t('main.errorSyncReport')} ${error.reason}`);
            } else {
              props.dispatch({ type: 'REMOVE_ACTION_QUEUE', payload: action.actionId });
              props.dispatch({ type: 'REMOVE_OFFLINE_REPORT', payload: action.doc._id });
              db['reports'].del(action.doc._id);
              const task = {
                organizationId: action.doc.organizationId,
                taskId: action.doc.taskId,
              };
              Meteor.call('Tasks.updateTaskStatus', task);
            }
          });
          return result;
        }
      }
    }
  }

Here is the offline initialization based on Spencer’s tutorial:

const onRehydration = (store) => {
  const data = Meteor.getData();
  const db = data && data.db;
  if (db) {
    _.each(store.getState(), (collectionData, collectionName) => {
      if (collectionName !== 'offlineUser' && collectionName !== 'offlineOrg' && collectionName !== 'actionQueue' && collectionName !== 'clipboard') {
        if (!db[collectionName]) {
          db.addCollection(collectionName);
        }
        const collectionArr = _.map(collectionData, (doc, _id) => {
          doc._id = _id;
          return doc;
        });
        db[collectionName].upsert(collectionArr);
      }
    });
  }
  store.dispatch({type: 'CACHING', caching: false})
};

export const initializeOffline = (opts = {}) => {
  let debug = false;
  const logger = createLogger({ predicate: () => debug&&opts.log || false });
  const store = createStore(reducers, applyMiddleware(logger), autoRehydrate());
  persistStore(store, {
    storage: AsyncStorage,
    keyPrefix: 'offline:',
    debounce: opts.debounce || 2000,
  }, () => onRehydration(store));
  store.dispatch({type: 'CACHING', caching: true})

  Meteor.ddp.on('added', (payload) => {
    store.dispatch({ type: 'DDP_ADDED', payload });
  });

  Meteor.ddp.on('changed', (payload) => {
    store.dispatch({ type: 'DDP_CHANGED', payload });
  });

  Meteor.ddp.on('removed', (payload) => {
    store.dispatch({ type: 'DDP_REMOVED', payload });
  });
  return store;
};

If someone has an idea of the problem or ever encountered a similar issue, I’d be grateful if you could share your solution :slight_smile: