/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "IDBTransaction.h"

#include "BackgroundChildImpl.h"
#include "IDBDatabase.h"
#include "IDBEvents.h"
#include "IDBObjectStore.h"
#include "IDBRequest.h"
#include "ProfilerHelpers.h"
#include "ReportInternalError.h"
#include "ThreadLocal.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/DOMStringList.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerRef.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "nsPIDOMWindow.h"
#include "nsQueryObject.h"
#include "nsServiceManagerUtils.h"
#include "nsTHashtable.h"

// Include this last to avoid path problems on Windows.
#include "ActorsChild.h"

namespace {
using namespace mozilla::dom::indexedDB;
using namespace mozilla::ipc;

// TODO: Move this to xpcom/ds.
template <typename T, typename Range, typename Transformation>
nsTHashtable<T> TransformToHashtable(const Range& aRange,
                                     const Transformation& aTransformation) {
  // TODO: Determining the size of the range is not syntactically necessary (and
  // requires random access iterators if expressed this way). It is a
  // performance optimization. We could resort to std::distance to support any
  // iterator category, but this would lead to a double iteration of the range
  // in case of non-random-access iterators. It is hard to determine in general
  // if double iteration or reallocation is worse.
  auto res = nsTHashtable<T>(aRange.cend() - aRange.cbegin());
  // TOOD: std::transform could be used if nsTHashtable had an insert_iterator,
  // and this would also allow a more generic version not depending on
  // nsTHashtable at all.
  for (const auto& item : aRange) {
    res.PutEntry(aTransformation(item));
  }
  return res;
}

ThreadLocal* GetIndexedDBThreadLocal() {
  BackgroundChildImpl::ThreadLocal* const threadLocal =
      BackgroundChildImpl::GetThreadLocalForCurrentThread();
  MOZ_ASSERT(threadLocal);

  ThreadLocal* idbThreadLocal = threadLocal->mIndexedDBThreadLocal.get();
  MOZ_ASSERT(idbThreadLocal);

  return idbThreadLocal;
}
}  // namespace

namespace mozilla::dom {

using namespace mozilla::dom::indexedDB;
using namespace mozilla::ipc;

bool IDBTransaction::HasTransactionChild() const {
  return (mMode == Mode::VersionChange
              ? static_cast<void*>(
                    mBackgroundActor.mVersionChangeBackgroundActor)
              : mBackgroundActor.mNormalBackgroundActor) != nullptr;
}

template <typename Func>
auto IDBTransaction::DoWithTransactionChild(const Func& aFunc) const {
  MOZ_ASSERT(HasTransactionChild());
  return mMode == Mode::VersionChange
             ? aFunc(*mBackgroundActor.mVersionChangeBackgroundActor)
             : aFunc(*mBackgroundActor.mNormalBackgroundActor);
}

IDBTransaction::IDBTransaction(IDBDatabase* const aDatabase,
                               const nsTArray<nsString>& aObjectStoreNames,
                               const Mode aMode, const Durability aDurability,
                               JSCallingLocation&& aCallerLocation,
                               CreatedFromFactoryFunction /*aDummy*/)
    : DOMEventTargetHelper(aDatabase),
      mDatabase(aDatabase),
      mObjectStoreNames(aObjectStoreNames.Clone()),
      mLoggingSerialNumber(GetIndexedDBThreadLocal()->NextTransactionSN(aMode)),
      mNextObjectStoreId(0),
      mNextIndexId(0),
      mNextRequestId(0),
      mAbortCode(NS_OK),
      mPendingRequestCount(0),
      mCallerLocation(std::move(aCallerLocation)),
      mMode(aMode),
      mDurability(aDurability),
      mRegistered(false),
      mNotedActiveTransaction(false) {
  MOZ_ASSERT(aDatabase);
  aDatabase->AssertIsOnOwningThread();

  // This also nulls mBackgroundActor.mVersionChangeBackgroundActor, so this is
  // valid also for mMode == Mode::VersionChange.
  mBackgroundActor.mNormalBackgroundActor = nullptr;

#ifdef DEBUG
  if (!aObjectStoreNames.IsEmpty()) {
    // Make sure the array is properly sorted.
    MOZ_ASSERT(
        std::is_sorted(aObjectStoreNames.cbegin(), aObjectStoreNames.cend()));

    // Make sure there are no duplicates in our objectStore names.
    MOZ_ASSERT(aObjectStoreNames.cend() ==
               std::adjacent_find(aObjectStoreNames.cbegin(),
                                  aObjectStoreNames.cend()));
  }
#endif

  mozilla::HoldJSObjects(this);
}

IDBTransaction::~IDBTransaction() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!mPendingRequestCount);
  MOZ_ASSERT(mReadyState != ReadyState::Active);
  MOZ_ASSERT(mReadyState != ReadyState::Inactive);
  MOZ_ASSERT(mReadyState != ReadyState::Committing);
  MOZ_ASSERT(!mNotedActiveTransaction);
  MOZ_ASSERT(mSentCommitOrAbort);
  MOZ_ASSERT_IF(HasTransactionChild(), mFiredCompleteOrAbort);

  if (mRegistered) {
    mDatabase->UnregisterTransaction(*this);
#ifdef DEBUG
    mRegistered = false;
#endif
  }

  if (HasTransactionChild()) {
    if (mMode == Mode::VersionChange) {
      mBackgroundActor.mVersionChangeBackgroundActor->SendDeleteMeInternal(
          /* aFailedConstructor */ false);
    } else {
      mBackgroundActor.mNormalBackgroundActor->SendDeleteMeInternal();
    }
  }
  MOZ_ASSERT(!HasTransactionChild(),
             "SendDeleteMeInternal should have cleared!");

  mozilla::DropJSObjects(this);
}

// static
SafeRefPtr<IDBTransaction> IDBTransaction::CreateVersionChange(
    IDBDatabase* const aDatabase,
    BackgroundVersionChangeTransactionChild* const aActor,
    const NotNull<IDBOpenDBRequest*> aOpenRequest,
    const int64_t aNextObjectStoreId, const int64_t aNextIndexId) {
  MOZ_ASSERT(aDatabase);
  aDatabase->AssertIsOnOwningThread();
  MOZ_ASSERT(aActor);
  MOZ_ASSERT(aNextObjectStoreId > 0);
  MOZ_ASSERT(aNextIndexId > 0);

  const nsTArray<nsString> emptyObjectStoreNames;

  // XXX: What should we have as durability hint here?
  auto transaction = MakeSafeRefPtr<IDBTransaction>(
      aDatabase, emptyObjectStoreNames, Mode::VersionChange,
      Durability::Default, JSCallingLocation(aOpenRequest->GetCallerLocation()),
      CreatedFromFactoryFunction{});

  transaction->NoteActiveTransaction();

  transaction->mBackgroundActor.mVersionChangeBackgroundActor = aActor;
  transaction->mNextObjectStoreId = aNextObjectStoreId;
  transaction->mNextIndexId = aNextIndexId;

  aDatabase->RegisterTransaction(*transaction);
  transaction->mRegistered = true;

  return transaction;
}

// static
SafeRefPtr<IDBTransaction> IDBTransaction::Create(
    JSContext* const aCx, IDBDatabase* const aDatabase,
    const nsTArray<nsString>& aObjectStoreNames, const Mode aMode,
    const Durability aDurability) {
  MOZ_ASSERT(aDatabase);
  aDatabase->AssertIsOnOwningThread();
  MOZ_ASSERT(!aObjectStoreNames.IsEmpty());
  MOZ_ASSERT(aMode == Mode::ReadOnly || aMode == Mode::ReadWrite ||
             aMode == Mode::ReadWriteFlush || aMode == Mode::Cleanup);

  auto transaction = MakeSafeRefPtr<IDBTransaction>(
      aDatabase, aObjectStoreNames, aMode, aDurability,
      JSCallingLocation::Get(aCx), CreatedFromFactoryFunction{});

  if (!NS_IsMainThread()) {
    WorkerPrivate* const workerPrivate = GetCurrentThreadWorkerPrivate();
    MOZ_ASSERT(workerPrivate);

    workerPrivate->AssertIsOnWorkerThread();

    RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create(
        workerPrivate, "IDBTransaction",
        [transaction = AsRefPtr(transaction.clonePtr())]() {
          transaction->AssertIsOnOwningThread();
          if (!transaction->IsCommittingOrFinished()) {
            IDB_REPORT_INTERNAL_ERR();
            transaction->AbortInternal(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR,
                                       nullptr);
          }
        });
    if (NS_WARN_IF(!workerRef)) {
#ifdef DEBUG
      // Silence the destructor assertions if we never made this object live.
      transaction->mReadyState = ReadyState::Finished;
      transaction->mSentCommitOrAbort.Flip();
#endif
      return nullptr;
    }

    transaction->mWorkerRef = std::move(workerRef);
  }

  nsCOMPtr<nsIRunnable> runnable =
      do_QueryObject(transaction.unsafeGetRawPtr());
  nsContentUtils::AddPendingIDBTransaction(runnable.forget());

  aDatabase->RegisterTransaction(*transaction);
  transaction->mRegistered = true;

  return transaction;
}

// static
Maybe<IDBTransaction&> IDBTransaction::MaybeCurrent() {
  using namespace mozilla::ipc;

  MOZ_ASSERT(BackgroundChild::GetForCurrentThread());

  return GetIndexedDBThreadLocal()->MaybeCurrentTransactionRef();
}

#ifdef DEBUG

void IDBTransaction::AssertIsOnOwningThread() const {
  MOZ_ASSERT(mDatabase);
  mDatabase->AssertIsOnOwningThread();
}

#endif  // DEBUG

void IDBTransaction::SetBackgroundActor(
    indexedDB::BackgroundTransactionChild* const aBackgroundActor) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aBackgroundActor);
  MOZ_ASSERT(!mBackgroundActor.mNormalBackgroundActor);
  MOZ_ASSERT(mMode != Mode::VersionChange);

  NoteActiveTransaction();

  mBackgroundActor.mNormalBackgroundActor = aBackgroundActor;
}

BackgroundRequestChild* IDBTransaction::StartRequest(
    MovingNotNull<RefPtr<mozilla::dom::IDBRequest>> aRequest,
    const RequestParams& aParams) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aParams.type() != RequestParams::T__None);

  BackgroundRequestChild* const actor =
      new BackgroundRequestChild(std::move(aRequest));

  DoWithTransactionChild([this, actor, &aParams](auto& transactionChild) {
    transactionChild.SendPBackgroundIDBRequestConstructor(
        actor, NextRequestId(), aParams);
  });

  // Balanced in BackgroundRequestChild::Recv__delete__().
  OnNewRequest();

  return actor;
}

void IDBTransaction::OpenCursor(PBackgroundIDBCursorChild& aBackgroundActor,
                                const OpenCursorParams& aParams) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None);

  DoWithTransactionChild([this, &aBackgroundActor, &aParams](auto& actor) {
    actor.SendPBackgroundIDBCursorConstructor(&aBackgroundActor,
                                              NextRequestId(), aParams);
  });

  // Balanced in BackgroundCursorChild::RecvResponse().
  OnNewRequest();
}

void IDBTransaction::RefreshSpec(const bool aMayDelete) {
  AssertIsOnOwningThread();

  for (auto& objectStore : mObjectStores) {
    objectStore->RefreshSpec(aMayDelete);
  }

  for (auto& objectStore : mDeletedObjectStores) {
    objectStore->RefreshSpec(false);
  }
}

void IDBTransaction::OnNewRequest() {
  AssertIsOnOwningThread();

  if (!mPendingRequestCount) {
    MOZ_ASSERT(ReadyState::Active == mReadyState);
    mStarted.Flip();
  }

  ++mPendingRequestCount;
}

void IDBTransaction::TransitionToActive() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mReadyState == ReadyState::Inactive);

  if (!mDeferralActive) {
    MOZ_DIAGNOSTIC_ASSERT(mDeferredRunnables.IsEmpty());

    mReadyState = ReadyState::Active;
    return;
  }

  mDeferralActive = false;

  DrainDeferredResponses();

  // Draining can run script that aborts the transaction; only become
  // Active again if we're still Inactive so post-abort guards stay effective.
  MOZ_DIAGNOSTIC_ASSERT(mReadyState == ReadyState::Inactive ||
                        mReadyState == ReadyState::Finished);
  if (mReadyState == ReadyState::Inactive) {
    mReadyState = ReadyState::Active;
  }
}

void IDBTransaction::TransitionToInactiveWithDeferral() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mReadyState == ReadyState::Active);

  mReadyState = ReadyState::Inactive;
  mDeferralActive = true;
}

void IDBTransaction::QueueDeferredResponse(
    already_AddRefed<nsIRunnable> aRunnable) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mDeferralActive);

  nsCOMPtr<nsIRunnable> runnable(aRunnable);
  mDeferredRunnables.AppendElement(std::move(runnable));
}

void IDBTransaction::DrainDeferredResponses() {
  AssertIsOnOwningThread();

  while (!mDeferredRunnables.IsEmpty()) {
    nsTArray<nsCOMPtr<nsIRunnable>> responses;
    responses.SwapElements(mDeferredRunnables);
    for (const auto& runnable : responses) {
      runnable->Run();
      MOZ_ASSERT(mPendingRequestCount);
      --mPendingRequestCount;
    }
  }
}

void IDBTransaction::OnRequestFinished(
    const bool aRequestCompletedSuccessfully) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(mReadyState != ReadyState::Active);
  MOZ_ASSERT_IF(mReadyState == ReadyState::Finished,
                NS_FAILED(mAbortCode) || mFiredCompleteOrAbort);
  MOZ_ASSERT(mPendingRequestCount);

  --mPendingRequestCount;

  if (!mPendingRequestCount) {
    MOZ_ASSERT(mDeferredRunnables.IsEmpty());
    if (mSentCommitOrAbort) {
      return;
    }

    if (aRequestCompletedSuccessfully) {
      if (mReadyState == ReadyState::Inactive) {
        mReadyState = ReadyState::Committing;
      }

      if (NS_SUCCEEDED(mAbortCode)) {
        SendCommit(true);
      } else {
        SendAbort(mAbortCode);
      }
    } else {
      // Don't try to send any more messages to the parent if the request actor
      // was killed. Set our state accordingly to Finished.
      mReadyState = ReadyState::Finished;
      mSentCommitOrAbort.Flip();
      IDB_LOG_MARK_CHILD_TRANSACTION(
          "Request actor was killed, transaction will be aborted",
          "IDBTransaction abort", LoggingSerialNumber());
    }
  }
}

void IDBTransaction::SendCommit(const bool aAutoCommit) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(NS_SUCCEEDED(mAbortCode));
  MOZ_ASSERT(IsCommittingOrFinished());

  // Don't do this in the macro because we always need to increment the serial
  // number to keep in sync with the parent.
  const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber();

  IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
      "Committing transaction (%s)", "IDBTransaction commit (%s)",
      LoggingSerialNumber(), requestSerialNumber,
      aAutoCommit ? "automatically" : "explicitly");

  const int64_t requestId = NextRequestId();

  const auto lastRequestId = [this, aAutoCommit,
                              requestId]() -> Maybe<decltype(requestId)> {
    if (aAutoCommit) {
      return Nothing();
    }

    // In case of an explicit commit, we need to note the id of the last
    // request to check if a request submitted before the commit request
    // failed. If we are currently in an event handler for a request on this
    // transaction, ignore this request. This is used to synchronize the
    // transaction's committing state with the parent side, to abort the
    // transaction in case of a request resulting in an error (see
    // https://w3c.github.io/IndexedDB/#async-execute-request, step 5.3.). With
    // automatic commit, this is not necessary, as the transaction's state will
    // only be set to committing after the last request completed.
    const auto maybeCurrentTransaction =
        BackgroundChildImpl::GetThreadLocalForCurrentThread()
            ->mIndexedDBThreadLocal->MaybeCurrentTransactionRef();
    const bool dispatchingEventForThisTransaction =
        maybeCurrentTransaction && &maybeCurrentTransaction.ref() == this;

    return Some(requestId
                    ? (requestId - (dispatchingEventForThisTransaction ? 0 : 1))
                    : 0);
  }();

  DoWithTransactionChild(
      [lastRequestId](auto& actor) { actor.SendCommit(lastRequestId); });

  mSentCommitOrAbort.Flip();
}

void IDBTransaction::SendAbort(const nsresult aResultCode) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(NS_FAILED(aResultCode));
  MOZ_ASSERT(IsCommittingOrFinished());

  // Don't do this in the macro because we always need to increment the serial
  // number to keep in sync with the parent.
  const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber();

  IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(
      "Aborting transaction with result 0x%" PRIx32,
      "IDBTransaction abort (0x%" PRIx32 ")", LoggingSerialNumber(),
      requestSerialNumber, static_cast<uint32_t>(aResultCode));

  DoWithTransactionChild(
      [aResultCode](auto& actor) { actor.SendAbort(aResultCode); });

  mSentCommitOrAbort.Flip();
}

void IDBTransaction::NoteActiveTransaction() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!mNotedActiveTransaction);

  mDatabase->NoteActiveTransaction();
  mNotedActiveTransaction = true;
}

void IDBTransaction::MaybeNoteInactiveTransaction() {
  AssertIsOnOwningThread();

  if (mNotedActiveTransaction) {
    mDatabase->NoteInactiveTransaction();
    mNotedActiveTransaction = false;
  }
}

RefPtr<IDBObjectStore> IDBTransaction::CreateObjectStore(
    ObjectStoreSpec& aSpec) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aSpec.metadata().id());
  MOZ_ASSERT(Mode::VersionChange == mMode);
  MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor);
  MOZ_ASSERT(IsActive());

#ifdef DEBUG
  {
    // TODO: Bind name outside of lambda capture as a workaround for GCC 7 bug
    // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66735.
    const auto& name = aSpec.metadata().name();
    // TODO: Use #ifdef and local variable as a workaround for Bug 1583449.
    const bool objectStoreNameDoesNotYetExist =
        std::all_of(mObjectStores.cbegin(), mObjectStores.cend(),
                    [&name](const auto& objectStore) {
                      return objectStore->Name() != name;
                    });
    MOZ_ASSERT(objectStoreNameDoesNotYetExist);
  }
#endif

  MOZ_ALWAYS_TRUE(
      mBackgroundActor.mVersionChangeBackgroundActor->SendCreateObjectStore(
          aSpec.metadata()));

  RefPtr<IDBObjectStore> objectStore = IDBObjectStore::Create(
      SafeRefPtr{this, AcquireStrongRefFromRawPtr{}}, aSpec);
  MOZ_ASSERT(objectStore);

  mObjectStores.AppendElement(objectStore);

  return objectStore;
}

void IDBTransaction::DeleteObjectStore(const int64_t aObjectStoreId) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObjectStoreId);
  MOZ_ASSERT(Mode::VersionChange == mMode);
  MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor);
  MOZ_ASSERT(IsActive());

  MOZ_ALWAYS_TRUE(
      mBackgroundActor.mVersionChangeBackgroundActor->SendDeleteObjectStore(
          aObjectStoreId));

  const auto foundIt =
      std::find_if(mObjectStores.begin(), mObjectStores.end(),
                   [aObjectStoreId](const auto& objectStore) {
                     return objectStore->Id() == aObjectStoreId;
                   });
  if (foundIt != mObjectStores.end()) {
    auto& objectStore = *foundIt;
    objectStore->NoteDeletion();

    RefPtr<IDBObjectStore>* deletedObjectStore =
        mDeletedObjectStores.AppendElement();
    deletedObjectStore->swap(objectStore);

    mObjectStores.RemoveElementAt(foundIt);
  }
}

void IDBTransaction::RenameObjectStore(const int64_t aObjectStoreId,
                                       const nsAString& aName) const {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObjectStoreId);
  MOZ_ASSERT(Mode::VersionChange == mMode);
  MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor);
  MOZ_ASSERT(IsActive());

  MOZ_ALWAYS_TRUE(
      mBackgroundActor.mVersionChangeBackgroundActor->SendRenameObjectStore(
          aObjectStoreId, nsString(aName)));
}

void IDBTransaction::CreateIndex(
    IDBObjectStore* const aObjectStore,
    const indexedDB::IndexMetadata& aMetadata) const {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObjectStore);
  MOZ_ASSERT(aMetadata.id());
  MOZ_ASSERT(Mode::VersionChange == mMode);
  MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor);
  MOZ_ASSERT(IsActive());

  MOZ_ALWAYS_TRUE(
      mBackgroundActor.mVersionChangeBackgroundActor->SendCreateIndex(
          aObjectStore->Id(), aMetadata));
}

void IDBTransaction::DeleteIndex(IDBObjectStore* const aObjectStore,
                                 const int64_t aIndexId) const {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObjectStore);
  MOZ_ASSERT(aIndexId);
  MOZ_ASSERT(Mode::VersionChange == mMode);
  MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor);
  MOZ_ASSERT(IsActive());

  MOZ_ALWAYS_TRUE(
      mBackgroundActor.mVersionChangeBackgroundActor->SendDeleteIndex(
          aObjectStore->Id(), aIndexId));
}

void IDBTransaction::RenameIndex(IDBObjectStore* const aObjectStore,
                                 const int64_t aIndexId,
                                 const nsAString& aName) const {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aObjectStore);
  MOZ_ASSERT(aIndexId);
  MOZ_ASSERT(Mode::VersionChange == mMode);
  MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor);
  MOZ_ASSERT(IsActive());

  MOZ_ALWAYS_TRUE(
      mBackgroundActor.mVersionChangeBackgroundActor->SendRenameIndex(
          aObjectStore->Id(), aIndexId, nsString(aName)));
}

void IDBTransaction::AbortInternal(const nsresult aAbortCode,
                                   RefPtr<DOMException> aError) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(NS_FAILED(aAbortCode));
  MOZ_ASSERT(!IsCommittingOrFinished());

  const bool isVersionChange = mMode == Mode::VersionChange;
  const bool needToSendAbort = !mStarted;

  mAbortCode = aAbortCode;
  mReadyState = ReadyState::Finished;
  mError = std::move(aError);

  if (isVersionChange) {
    // If a version change transaction is aborted, we must revert the world
    // back to its previous state unless we're being invalidated after the
    // transaction already completed.
    if (!mDatabase->IsInvalidated()) {
      mDatabase->RevertToPreviousState();
    }

    // We do the reversion only for the mObjectStores/mDeletedObjectStores but
    // not for the mIndexes/mDeletedIndexes of each IDBObjectStore because it's
    // time-consuming(O(m*n)) and mIndexes/mDeletedIndexes won't be used anymore
    // in IDBObjectStore::(Create|Delete)Index() and IDBObjectStore::Index() in
    // which all the executions are returned earlier by
    // !transaction->IsActive().

    const nsTArray<ObjectStoreSpec>& specArray =
        mDatabase->Spec()->objectStores();

    if (specArray.IsEmpty()) {
      // This case is specially handled as a performance optimization, it is
      // equivalent to the else block.
      mObjectStores.Clear();
    } else {
      const auto validIds = TransformToHashtable<nsUint64HashKey>(
          specArray, [](const auto& spec) {
            const int64_t objectStoreId = spec.metadata().id();
            MOZ_ASSERT(objectStoreId);
            return static_cast<uint64_t>(objectStoreId);
          });

      mObjectStores.RemoveLastElements(
          mObjectStores.end() -
          std::remove_if(mObjectStores.begin(), mObjectStores.end(),
                         [&validIds](const auto& objectStore) {
                           return !validIds.Contains(
                               uint64_t(objectStore->Id()));
                         }));

      std::copy_if(std::make_move_iterator(mDeletedObjectStores.begin()),
                   std::make_move_iterator(mDeletedObjectStores.end()),
                   MakeBackInserter(mObjectStores),
                   [&validIds](const auto& deletedObjectStore) {
                     const int64_t objectStoreId = deletedObjectStore->Id();
                     MOZ_ASSERT(objectStoreId);
                     return validIds.Contains(uint64_t(objectStoreId));
                   });
    }
    mDeletedObjectStores.Clear();
  }

  // Fire the abort event if there are no outstanding requests. Otherwise the
  // abort event will be fired when all outstanding requests finish.
  if (needToSendAbort) {
    SendAbort(aAbortCode);
  }

  if (isVersionChange) {
    mDatabase->Close();
  }
}

void IDBTransaction::Abort(IDBRequest* const aRequest) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aRequest);

  if (IsCommittingOrFinished()) {
    // Already started (and maybe finished) the commit or abort so there is
    // nothing to do here.
    return;
  }

  ErrorResult rv;
  RefPtr<DOMException> error = aRequest->GetError(rv);

  // TODO: Do we deliberately ignore rv here? Isn't there a static analysis that
  // prevents that?

  AbortInternal(aRequest->GetErrorCode(), std::move(error));
}

void IDBTransaction::Abort(const nsresult aErrorCode) {
  AssertIsOnOwningThread();

  if (IsCommittingOrFinished()) {
    // Already started (and maybe finished) the commit or abort so there is
    // nothing to do here.
    return;
  }

  AbortInternal(aErrorCode, DOMException::Create(aErrorCode));
}

// Specified by https://w3c.github.io/IndexedDB/#dom-idbtransaction-abort.
void IDBTransaction::Abort(ErrorResult& aRv) {
  AssertIsOnOwningThread();

  if (IsCommittingOrFinished()) {
    aRv = NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR;
    return;
  }

  mReadyState = ReadyState::Inactive;

  AbortInternal(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR, nullptr);

  mAbortedByScript.Flip();
}

// Specified by https://w3c.github.io/IndexedDB/#dom-idbtransaction-commit.
void IDBTransaction::Commit(ErrorResult& aRv) {
  AssertIsOnOwningThread();

  if (mReadyState != ReadyState::Active || !mNotedActiveTransaction) {
    aRv = NS_ERROR_DOM_INVALID_STATE_ERR;
    return;
  }

  MOZ_ASSERT(!mSentCommitOrAbort);

  MOZ_ASSERT(mReadyState == ReadyState::Active);
  mReadyState = ReadyState::Committing;
  if (NS_WARN_IF(NS_FAILED(mAbortCode))) {
    SendAbort(mAbortCode);
    aRv = mAbortCode;
    return;
  }

#ifdef DEBUG
  mWasExplicitlyCommitted.Flip();
#endif

  SendCommit(false);
}

void IDBTransaction::FireCompleteOrAbortEvents(const nsresult aResult) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!mFiredCompleteOrAbort);
  MOZ_ASSERT(mDeferredRunnables.IsEmpty());

  mReadyState = ReadyState::Finished;

#ifdef DEBUG
  mFiredCompleteOrAbort.Flip();
#endif

  // Make sure we drop the WorkerRef when this function completes.
  const auto scopeExit = MakeScopeExit([&] { mWorkerRef = nullptr; });

  RefPtr<Event> event;
  if (NS_SUCCEEDED(aResult)) {
    event = CreateGenericEvent(this, nsDependentString(kCompleteEventType),
                               eDoesNotBubble, eNotCancelable);
    MOZ_ASSERT(event);

    // If we hit this assertion, it probably means transaction object on the
    // parent process doesn't propagate error properly.
    MOZ_ASSERT(NS_SUCCEEDED(mAbortCode));
  } else {
    if (aResult == NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR) {
      mDatabase->SetQuotaExceeded();
    }

    if (!mError && !mAbortedByScript) {
      mError = DOMException::Create(aResult);
    }

    event = CreateGenericEvent(this, nsDependentString(kAbortEventType),
                               eDoesBubble, eNotCancelable);
    MOZ_ASSERT(event);

    if (NS_SUCCEEDED(mAbortCode)) {
      mAbortCode = aResult;
    }
  }

  if (NS_SUCCEEDED(mAbortCode)) {
    IDB_LOG_MARK_CHILD_TRANSACTION("Firing 'complete' event",
                                   "IDBTransaction 'complete' event",
                                   mLoggingSerialNumber);
  } else {
    IDB_LOG_MARK_CHILD_TRANSACTION(
        "Firing 'abort' event with error 0x%" PRIx32,
        "IDBTransaction 'abort' event (0x%" PRIx32 ")", mLoggingSerialNumber,
        static_cast<uint32_t>(mAbortCode));
  }

  IgnoredErrorResult rv;
  DispatchEvent(*event, rv);
  if (rv.Failed()) {
    NS_WARNING("DispatchEvent failed!");
  }

  // Normally, we note inactive transaction here instead of
  // IDBTransaction::ClearBackgroundActor() because here is the earliest place
  // to know that it becomes non-blocking to allow the scheduler to start the
  // preemption as soon as it can.
  // Note: If the IDBTransaction object is held by the script,
  // ClearBackgroundActor() will be done in ~IDBTransaction() until garbage
  // collected after its window is closed which prevents us to preempt its
  // window immediately after committed.
  MaybeNoteInactiveTransaction();
}

int64_t IDBTransaction::NextObjectStoreId() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(Mode::VersionChange == mMode);

  return mNextObjectStoreId++;
}

int64_t IDBTransaction::NextIndexId() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(Mode::VersionChange == mMode);

  return mNextIndexId++;
}

int64_t IDBTransaction::NextRequestId() {
  AssertIsOnOwningThread();

  return mNextRequestId++;
}

void IDBTransaction::InvalidateCursorCaches() {
  AssertIsOnOwningThread();

  for (const auto& cursor : mCursors) {
    cursor->InvalidateCachedResponses();
  }
}

void IDBTransaction::RegisterCursor(IDBCursor& aCursor) {
  AssertIsOnOwningThread();

  mCursors.AppendElement(WrapNotNullUnchecked(&aCursor));
}

void IDBTransaction::UnregisterCursor(IDBCursor& aCursor) {
  AssertIsOnOwningThread();

  DebugOnly<bool> removed = mCursors.RemoveElement(&aCursor);
  MOZ_ASSERT(removed);
}

nsIGlobalObject* IDBTransaction::GetParentObject() const {
  AssertIsOnOwningThread();

  return mDatabase->GetParentObject();
}

IDBTransactionMode IDBTransaction::GetMode(ErrorResult& aRv) const {
  AssertIsOnOwningThread();

  switch (mMode) {
    case Mode::ReadOnly:
      return IDBTransactionMode::Readonly;

    case Mode::ReadWrite:
      return IDBTransactionMode::Readwrite;

    case Mode::ReadWriteFlush:
      return IDBTransactionMode::Readwriteflush;

    case Mode::Cleanup:
      return IDBTransactionMode::Cleanup;

    case Mode::VersionChange:
      return IDBTransactionMode::Versionchange;

    case Mode::Invalid:
    default:
      MOZ_CRASH("Bad mode!");
  }
}

IDBTransactionDurability IDBTransaction::GetDurability(ErrorResult& aRv) const {
  AssertIsOnOwningThread();

  switch (mDurability) {
    case Durability::Default:
      return IDBTransactionDurability::Default;

    case Durability::Strict:
      return IDBTransactionDurability::Strict;

    case Durability::Relaxed:
      return IDBTransactionDurability::Relaxed;

    default:
      MOZ_CRASH("Bad mode!");
  }
}

DOMException* IDBTransaction::GetError() const {
  AssertIsOnOwningThread();

  return mError;
}

RefPtr<DOMStringList> IDBTransaction::ObjectStoreNames() const {
  AssertIsOnOwningThread();

  if (mMode == Mode::VersionChange) {
    return mDatabase->ObjectStoreNames();
  }

  auto list = MakeRefPtr<DOMStringList>();
  list->StringArray() = mObjectStoreNames.Clone();
  return list;
}

RefPtr<IDBObjectStore> IDBTransaction::ObjectStore(const nsAString& aName,
                                                   ErrorResult& aRv) {
  AssertIsOnOwningThread();

  if (IsCommittingOrFinished()) {
    aRv.ThrowInvalidStateError("Transaction is already committing or done.");
    return nullptr;
  }

  auto* const spec = [this, &aName]() -> ObjectStoreSpec* {
    if (IDBTransaction::Mode::VersionChange == mMode ||
        mObjectStoreNames.Contains(aName)) {
      return mDatabase->LookupModifiableObjectStoreSpec(
          [&aName](const auto& objectStore) {
            return objectStore.metadata().name() == aName;
          });
    }
    return nullptr;
  }();

  if (!spec) {
    aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR);
    return nullptr;
  }

  RefPtr<IDBObjectStore> objectStore;

  const auto foundIt = std::find_if(
      mObjectStores.cbegin(), mObjectStores.cend(),
      [desiredId = spec->metadata().id()](const auto& existingObjectStore) {
        return existingObjectStore->Id() == desiredId;
      });
  if (foundIt != mObjectStores.cend()) {
    objectStore = *foundIt;
  } else {
    objectStore = IDBObjectStore::Create(
        SafeRefPtr{this, AcquireStrongRefFromRawPtr{}}, *spec);
    MOZ_ASSERT(objectStore);

    mObjectStores.AppendElement(objectStore);
  }

  return objectStore;
}

NS_IMPL_ADDREF_INHERITED(IDBTransaction, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(IDBTransaction, DOMEventTargetHelper)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBTransaction)
  NS_INTERFACE_MAP_ENTRY(nsIRunnable)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

NS_IMPL_CYCLE_COLLECTION_CLASS(IDBTransaction)

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBTransaction,
                                                  DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDatabase)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObjectStores)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDeletedObjectStores)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBTransaction,
                                                DOMEventTargetHelper)
  // Don't unlink mDatabase!
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mError)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mObjectStores)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDeletedObjectStores)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

JSObject* IDBTransaction::WrapObject(JSContext* const aCx,
                                     JS::Handle<JSObject*> aGivenProto) {
  AssertIsOnOwningThread();

  return IDBTransaction_Binding::Wrap(aCx, this, std::move(aGivenProto));
}

void IDBTransaction::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
  AssertIsOnOwningThread();

  aVisitor.mCanHandle = true;
  aVisitor.SetParentTarget(mDatabase, false);
}

NS_IMETHODIMP
IDBTransaction::Run() {
  AssertIsOnOwningThread();

  // TODO: Instead of checking for Finished and Committing states here, we could
  // remove the transaction from the pending IDB transactions list on
  // abort/commit.

  if (ReadyState::Finished == mReadyState) {
    // There are three cases where mReadyState is set to Finished: In
    // FileCompleteOrAbortEvents, AbortInternal and in CommitIfNotStarted. We
    // shouldn't get here after CommitIfNotStarted again.
    MOZ_ASSERT(mFiredCompleteOrAbort || IsAborted());
    return NS_OK;
  }

  if (ReadyState::Committing == mReadyState) {
    MOZ_ASSERT(mSentCommitOrAbort);
    return NS_OK;
  }
  // We're back at the event loop, no longer newborn, so
  // return to Inactive state:
  // https://w3c.github.io/IndexedDB/#cleanup-indexed-database-transactions.
  MOZ_ASSERT(ReadyState::Active == mReadyState);
  mReadyState = ReadyState::Inactive;

  CommitIfNotStarted();

  return NS_OK;
}

void IDBTransaction::CommitIfNotStarted() {
  AssertIsOnOwningThread();

  MOZ_ASSERT(ReadyState::Inactive == mReadyState);

  // Maybe commit if there were no requests generated.
  if (!mStarted) {
    MOZ_ASSERT(!mPendingRequestCount);
    mReadyState = ReadyState::Finished;

    SendCommit(true);
  }
}

}  // namespace mozilla::dom
