New package - jam:mongo-transactions - An easy way to use Mongo Transactions

jam:mongo-transactions is an easy way to use Mongo Transactions. Here are a few of the benefits:

  • Write with the standard Meteor collection methods you’re accustomed to. You don’t need to worry about using rawCollection().
  • Because it’s a low-level solution, ID generation works as expected.
  • You don’t need to worry about passing session around. This package takes care of that for you.
  • Works out-of-the-box with other packages that automatically validate on DB writes, like jam:easy-schema and aldeed:collection2.
  • One simple API to use. Mongo has made things complicated with two APIs for Transactions, the Callback API and the Core API. This package defaults to using the Callback API as recommended by Mongo, but allows you to use the Core API by passing in autoRetry: false.
  • Can be used isomorphically.
  • Compatible with Meteor 2.8.1 and up, including support for Meteor 3.0+

It was inspired by this Github discussion and builds on some of the ideas shared there.

Here’s a quick example of how to use it:

import { Mongo } from 'meteor/mongo';

async function purchase(purchaseData) {
  try {
    const { invoiceId } = await Mongo.withTransaction(async () => {
      const invoiceId = await Invoices.insertAsync(purchaseData);
      const changeQuantity = await Items.updateAsync(purchaseData.itemId, { $set: {...} });
      return { invoiceId, changeQuantity } // you can return whatever you'd like
    });
    return invoiceId;
  } catch (error) {
    // something went wrong with the transaction and it could not be automatically retried
    // handle the error as you see fit
  }
}

If you end up taking it for a spin, let me know! If you have ideas on how to make it better, feel free to post below or send me a DM.

10 Likes

Thanks for this, @jam.

Is there any reason why it must use a try-catch? Is there no way to handle errors explicitly where the error happens?

As a subjective choice, we only use try-catch in our team when we do not have control over the error handling, i.e. 3rd party code/api/library

The package doesn’t make any assumptions on how you might want to handle errors. If you throw from within the withTransaction callback, the transaction will be aborted and the error will propagate up. For example:

async function purchase(purchaseData) {
  const { invoiceId } = await Mongo.withTransaction(async () => {
    const invoiceId = await Invoices.insertAsync(purchaseData);
    const changeQuantity = await Items.updateAsync(purchaseData.itemId, { $set: {...} });
    
    // throw in here to abort the transaction
    // e.g. throw new Meteor.Error('...') or throw new Error(...)
    
    return { invoiceId, changeQuantity } // you can return whatever you'd like
  });

  return invoiceId;
}

You may want to catch errors on withTransaction in case you get a TransactionTooLargeForCache error. Mongo does not automatically retry these so you may want to handle this explicitly.

If, for some reason, you want to explicitly handle all errors and you want to prevent Mongo’s default of retrying for TransientTransactionError and UnknownTransactionCommitResult, then you’ll need to pass in { autoRetry: false } and handle errors explicitly with a catch. For example:

async function purchase(purchaseData) {
  try {
    const { invoiceId } = await Mongo.withTransaction(async () => {
      const invoiceId = await Invoices.insertAsync(purchaseData);
      const changeQuantity = await Items.updateAsync(purchaseData.itemId, { $set: {...} });
      return { invoiceId, changeQuantity } // you can return whatever you'd like
    }, { autoRetry: false }); // passing in false uses the Core API and you'll be responsible for handling all errors

    return invoiceId;
  } catch (error) {
    if (error instanceof MongoError && error.hasErrorLabel('UnknownTransactionCommitResult')) {
      // add your logic to retry or handle the error
    } else if (error instanceof MongoError && error.hasErrorLabel('TransientTransactionError')) {
      // add your logic to retry or handle the error
    } else {
      console.log('An error occured in the transaction, performing a data rollback:' + error);
    }
  }
}

Hope that answers your question. Let me know if you had something else in mind.

2 Likes

Really awesome package! Will def be using this in the future

1 Like

@jam I assume this is still an issue? : Mongo Transactions - insert is not being picked up by publication in production - #7 by jam

I also see you added a disclaimer on the package’s page. Tx.

The solution to that was to upgrade to a paid tier of Mongo Atlas. I guess they are artificially limiting the free tier to increase revenue.

As you mentioned, I noted that at the bottom of Readme.