sanger/sequencescape

View on GitHub
app/frontend/plate-picks/store/actions.js

Summary

Maintainability
B
6 hrs
Test Coverage
// Provides backward support for fetch
// If this ends up causing problems in future its probably safe to lose:
// https://caniuse.com/#search=fetch
// Latest versions of Chrome (49) and Firefox (52.9.0) on windows XP have
// native support, which covers out main need for legacy support.
// So this polyfill will only really be useful for anyone attempting to use IE.
import "whatwg-fetch";

const PLATE_PATH = "/plate_picks/plates/";
const BATCH_PATH = "/plate_picks/batches/";

// Private:
// Extracts pick information from batch and adds it to the plate
async function processPicks({ commit, dispatch }, batch) {
  const pick_ids = [];
  for (const pick of batch.picks) {
    const id = (await dispatch("nextPickId")).toString();
    const new_pick = { ...pick, id, short: `${id}` };
    pick_ids.push(id);
    commit("updatePick", new_pick);
    for (const plate of pick.plates) {
      commit("addPickToPlate", { plate: plate, batch: batch.id, pick: { id } });
    }
  }
  return pick_ids;
}

/**
 * Requests plate_barcode from the server.
 * Note: The catch mainly handles cases where the network or server is
 * inaccessible. If the server responds, even with a status of 5**, fetch
 * treats it as a success.
 * CORS issues also end up getting caught here.
 *
 * @param {String} plate_barcode The barcode of the plate to fetch.
 */
const plateRequest = async (plate_barcode) => {
  try {
    return await window.fetch(PLATE_PATH + encodeURIComponent(plate_barcode), {});
  } catch (error) {
    console.error(error);
    throw `Network Error: ${error.message}`;
  }
};

/**
 * Requests batch_id from the server.
 * Note: The catch mainly handles cases where the network or server is
 * inaccessible. If the server responds, even with a status of 5**, fetch
 * treats it as a success.
 * CORS issues also end up getting caught here.
 *
 * @param {String} batch_id The id of the batch to fetch. A string, rather than a number as that plays nicer with some
 *                          of the expectations of the libraries we're using.
 */
const batchRequest = async (batch_id) => {
  try {
    return await window.fetch(BATCH_PATH + encodeURIComponent(batch_id), {});
  } catch (error) {
    console.error(error);
    throw `Network Error: ${error.message}`;
  }
};

/**
 * Extracts the payload from he response, updates the plate,
 * and triggers requests for any batches associated with the plate.
 * Catch here will deal with cases where the payload from the server isn't
 * json, or doesn't match the format we're expecting.
 *
 * @param {Response} response The response to extract data from
 * @param {Function} commit A Vuex commit function
 * @param {Function} dispatch A Vuex dispatch function
 * @param {String} plate_barcode the barcode of the plate as scanned. Ensures that
 *                               handle cases where the barcode scanned was NOT the
 *                               primary barcode.
 */
const extractPlateJson = async (response) => {
  try {
    const json = await response.json();
    return json.plate;
  } catch (e) {
    console.error(e);
    throw "Unexpected response from the server. Contact support.";
  }
};

/**
 * Extract errors from a response object if available
 * and return an error message
 *
 * @param {Response} response The response to extract errors from
 */
const extractErrors = async (response) => {
  try {
    const json = await response.json();
    console.error("Error response with json", json);
    return `${response.statusText}: ${json.errors}`;
  } catch (e) {
    console.error("Error response without json", e);
    return response.statusText;
  }
};

// Actions handle asynchronous update of state, such as via calls to external apis
// Exported for easy testing. See https://vuex.vuejs.org/guide/testing.html
export default {
  plateBarcodeScan: async ({ commit, dispatch, state }, plate_barcode) => {
    // Firstly we record that the plate has been scanned.
    commit("scanPlate", { barcode: plate_barcode });
    const scanned_plate_id = state.scanStore[`_${plate_barcode}`].id;

    // If we have an id, we don't need to fetch more plate info but we may have
    // batches to fetch
    if (scanned_plate_id) {
      const scanned_plate = state.plates[scanned_plate_id];
      // Don't fetch batches from control plates
      if (!scanned_plate.control) {
        dispatch("fetchBatches", { ids: scanned_plate.batches });
      }
    } else {
      // We've not loaded the plate yet, so go fetch it.
      try {
        const response = await plateRequest(plate_barcode);

        if (response.status === 200) {
          const plate = await extractPlateJson(response);
          commit("updatePlate", { ...plate, scanned: true });
          commit("updateScanPlate", { barcode: plate_barcode, id: plate.id });
          if (!plate.control) {
            dispatch("fetchBatches", { ids: plate.batches });
          }
        } else {
          const error_message = await extractErrors(response);
          throw error_message;
        }
      } catch (error) {
        // We catch and log most expected errors where they occur, and re-throw with
        // a user-friendly error message. This handles displaying that
        commit("updateScanPlate", { barcode: plate_barcode, errorMessage: error });
      }
    }
  },
  fetchBatches({ dispatch }, batch_details) {
    for (const id of batch_details.ids) {
      dispatch("fetchBatch", { id: `${id}` });
    }
  },
  fetchBatch: async ({ commit, state, dispatch }, batch_attributes) => {
    // Add the batch to the list so that we can show a spinner
    const batch_id = batch_attributes.id;
    const existingBatch = state.batches[`b${batch_id}`];
    // We only want to trigger a lookup once, so back out if we already exist.
    if (existingBatch) {
      return;
    }

    // Add the batch to the list so that we can show a spinner
    commit("updateBatch", { id: batch_id });

    try {
      const response = await batchRequest(batch_id);

      if (response.status === 200) {
        const json = await response.json();
        const picks = await processPicks({ commit, dispatch }, json.batch);

        commit("updateBatch", { ...json.batch, picks });
      } else {
        const error_message = await extractErrors(response);
        throw error_message;
      }
    } catch (error) {
      console.error(error);
      commit("updateBatch", { id: batch_id, errorMessage: error });
    }
  },
  nextPickId({ commit, state }) {
    commit("incrementPick");
    return state.pickCount;
  },
};