Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FR: API for making the transaction object globally accessible #8083

Open
sebastiangon11 opened this issue Mar 19, 2024 · 3 comments
Open

FR: API for making the transaction object globally accessible #8083

sebastiangon11 opened this issue Mar 19, 2024 · 3 comments

Comments

@sebastiangon11
Copy link

sebastiangon11 commented Mar 19, 2024

Operating System

N/A

Browser Version

N/A

Firebase SDK Version

N/A

Firebase SDK Product:

Firestore

Describe your project's tooling

N/A

Describe the problem

Feature request

Would it be possible to provide an API for begginning/commiting a transaction without having to perform all the transaction operantions within a single closure?

Also it would be extremely helpful to not have to use another set of functions to manipulate the data when using a transaction and when not. When inside a transaction, operations have to be done through the transaction object (transaction.set) and when there is no transaction they are done through the firestore modular functions (setDoc).

Problem description

The fact that firebase only provides the runTransaction() api is very limiting for architecturing an app using different architectural patterns and abstracting functionality as the application scales.

For instance

In the case of implementing a service/repository pattern, where the service handles all the business logic and the repositories encapsulates all the database queries, the issue is clearly visible when I need to start a transaction from the service and call multiple repositories involved in the operation.

Now I'm forced to:

  • Create a new layer of abstraction for sharing the transaction object between the service and the repositories.
  • Inject this new layer in all my services for initializing transactions.
  • Inject this new layer in all my repositories for accessing the transaction object in each operation.
  • Duplicate the code in each operation for checking if I'm inside a transaction or not.
    • This is because the code is different depending if there is a transaction or not, and the repositories should still work event if they are not inside a transaction.
    • For example setDoc vs transaction.set

Code example

  // TransactionManager

  import { runTransaction as fsRunTransaction } from 'firebase/firestore'

  let openTransaction: Transaction | null = null

  const transactionManager = {
  const db = getFirestore(getFirebaseApp())

  const runTransaction = (updateFn) => {
    return fsRunTransaction(
      db,
      async (transaction) => {
        // Keep a reference to the open transaction so I can make different operations across different repositories.
        openTransaction = transaction
        await updateFn(transaction)
        openTransaction = null
      }
    )
  }

  return { transaction: openTransaction, runTransaction }
  // Service

  const create = async (snapshot) => {
       transactionManager.runTransaction(async () => {
          const account = await accountsRepository.get(snapshot.accountId)

          if (!account) throw new Error('Account not found')

         await snapshotsRepository.create(snapshot)
         await accountsRepository.update({ ...account, balance: snapshot.balance })
    })
  }
  // Repository

  const create = async (snapshot) => {
    const snapshotsRef = collection(db, 'accounts', snapshot.accountId, 'snapshots')
    const snapshotRef = doc(snapshotsRef)

    if (transactionManager.transaction) {
      transactionManager.transaction.set(snapshotRef, snapshot)
    } else {
      await setDoc(snapshotRef, snapshot)
    }
  }

Steps and code to reproduce issue

N/A

@sebastiangon11 sebastiangon11 added new A new issue that hasn't be categoirzed as question, bug or feature request question labels Mar 19, 2024
@jbalidiong jbalidiong added needs-attention api: firestore and removed question new A new issue that hasn't be categoirzed as question, bug or feature request labels Mar 19, 2024
@ehsannas ehsannas self-assigned this Mar 19, 2024
@tom-andersen
Copy link
Contributor

tom-andersen commented Mar 19, 2024

Some things to consider:

Transactions on web/mobile clients use optimistic concurrency to avoid locking data. This comes at the cost of having to retry transaction when data is changed from elsewhere, before the commit is done. Understanding what commit errors can be retried and which are permanent errors is a complexity that is abstracted away by using the update function closure. The client SDK handles retry, with backoff, responding differently according to the error code. By exposing a transaction object with begin/commit, the application developer will now have to take responsibility for this.

The transaction object (transaction.set) and the firestore modular functions (setDoc) are quite different. When reading/writing through modular functions, you can work offline and have your writes queued for when you go back online. The changes you make will be visible immediately, by reading through the cache.

Transactions on the other hand, only work when you are online and do not become visible until they are written to Firestore, and then retrieved again. The transaction read and write path does not go through the cache.

Are you running in a safe trusted environment with reliable internet connection? If so, the feature you describe might be better aligned for the Server SDK. Transactions is an area we want to improve, so understanding your use case better will be helpful.

@sebastiangon11
Copy link
Author

Thanks for the detailed explanation @tom-andersen

I didn't know there were that many differences between transactions and modular operations. It there any place I can read more about this? I'd like to learn more about how this is intended to be used and how it works.

I understand that is not possible to add the begin/commit transaction API because the update closure abstracts a lot of functionality from end users.

My end goal in this case was to build a full client side application as a POC for a product and only involve a server if the POC is successfull. That's the primary reason of why I choose firestore as my storage... because it gives me the possibility to delegate authentication and storage in a very straighforward way and without involving any server side code.

Answering to your question: Yes, I'm running in a safe trusted environment and I could use the server SDK, but that would be a next step in the product lifecycle.

I think that the example I provided shows a very common pattern for separating concerns in a web app. If adding the begin/commit transaction is not possible, then do you think that this would be the way to go? Or do you know about any ressources that provide guidance on how to architect an app using firestore (JS SDK) transactions without having everything in the same file?

@tom-andersen
Copy link
Contributor

We have a page describing transactions and how they differ here:
https://firebase.google.com/docs/firestore/transaction-data-contention

Here is some documentation cache provided by Web/Mobile SDKs:
https://firebase.google.com/docs/firestore/manage-data/enable-offline

Note that the write guarantees of a transaction are much higher and therefore do not align with offline data. We cannot guarantee preconditions are still true when the client goes back online. Nor do we know whether the client will ever go online again.

I think your feature request is better implemented in the Server SDK that uses pessimistic concurrency controls. Please feel free to create an issue there as well, since customer feature requests help drive priority in development.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants