0

I'm trying to perform a fetch request within a transaction but when the code executes I receive the following error.

Error: Cannot modify a WriteBatch that has been committed.

The steps the function is performing are the following:

  1. Compute document references (taken from an external source)
  2. Query the documents available in Firestore
  3. Verify if document exists
  4. Fetch for further details (lazy loading mechanism)
  5. Start populating first level collection
  6. Start populating second level collection

Below the code I'm using.

await firestore.runTransaction(async (transaction) => {

  // 1. Compute document references
  const docRefs = computeDocRefs(colName, itemsDict);
  // 2. Query the documents available in Firestore
  const snapshots = await transaction.getAll(...docRefs);
  snapshots.forEach(async (snapshot) => {
    // 3. Verify if document exists
    if (!snapshot.exists) {
      console.log(snapshot.id + " does not exists");

      const item = itemsDict[snapshot.id];
      if (item) {
        // 4. Fetch for further details
        const response = await fetchData(item.detailUrl);
        const detailItemsDict = prepareDetailPageData(response);

        // 5. Start populating first level collection
        transaction.set(snapshot.ref, {
          index: item.index,
          detailUrl: item.detailUrl,
          title: item.title,
        });

        // 6. Start populating second level collection
        const subColRef = colRef.doc(snapshot.id).collection(subColName);
        detailItemsDict.detailItems.forEach((detailItem) => {
          const subColDocRef = subColRef.doc();

          transaction.set(subColDocRef, {
            title: detailItem.title,
            pdfUrl: detailItem.pdfUrl,
          });
        });
      }
    } else {
      console.log(snapshot.id + " exists");
    }
  });
});

computeDocRefs is described below

function computeDocRefs(colName, itemsDict) {
  const identifiers = Object.keys(itemsDict);
  const docRefs = identifiers.map((identifier) => {
    const docId = `${colName}/${identifier}`
    return firestore.doc(docId);
  });
  return docRefs;
}

while fetchData uses axios under the hood

async function fetchData(url) {
  const response = await axios(url);
  if (response.status !== 200) {
    throw new Error('Fetched data failed!');
  }
  return response;
}

prepareMainPageData and prepareDetailPageData are functions that prepare the data normalizing them.

If I comment the await fetchData(item.detailUrl), the first level collection with all the documents associated to it are stored correctly.

On the contrary with await fetchData(item.detailUrl) the errors happens below the following comment: // 5. Start populating first level collection.

The order of the operation are important since I do now want to make the second call if not necessary.

Are you able to guide me towards the correct solution?

Lorenzo B
  • 33,216
  • 24
  • 116
  • 190
  • Does your code use batched writes anywhere ? – Dharmaraj Nov 25 '22 at 16:53
  • @Dharmaraj no, I moved from batched writes to transaction. I'm going to double check for sure. I've also updated the question. – Lorenzo B Nov 25 '22 at 16:59
  • 1
    You really shouldn't do network calls inside a transaction. Transactions need to be as fast as possible by operating only on data in memory and from the database. Do any network calls ahead of time, and if that means you have to also fetch documents multiple times, that's a small price to pay. – Doug Stevenson Nov 25 '22 at 17:40
  • @DougStevenson Do you mean that I should break the logic into different transactions right? Thanks! – Lorenzo B Nov 25 '22 at 17:42
  • 1
    No, all I am saying is that the network calls should not occur during a transaction. – Doug Stevenson Nov 25 '22 at 18:17

1 Answers1

1

The problem is due to the fact that forEach and async/await do not work well together. For example: Using async/await with a forEach loop.

Now I've completely changed the approach I'm following and now it works smoothly.

The code now is like the following:

// Read transaction to retrieve the items that are not yet available in Firestore
const itemsToFetch = await readItemsToFetch(itemsDict, colName);
// Merge the items previously retrieved to grab additional details through fetch network calls
const fetchedItems = await aggregateItemsToFetch(itemsToFetch);
// Write transaction (Batched Write) to save items into Firestore
const result = await writeFetchedItems(fetchedItems, colName, subColName);

A big thanks goes to Doug Stevenson and Renaud Tarnec.

Lorenzo B
  • 33,216
  • 24
  • 116
  • 190