dom/indexedDB/ActorsParent.cpp
author Simon Giesecke <sgiesecke@mozilla.com>
Wed, 18 Nov 2020 08:58:37 +0000
changeset 557797 785624cc9edfc68ebc6428f8a3cac45fdeb74383
parent 557796 84bf4201587c74c3d16bf30f87417149713c40ed
child 557969 9e8572b4f56fdcc53364e951694045924fadc1b3
permissions -rw-r--r--
Bug 1663924 - Use IDB_TRY in Maintenance::Run. r=dom-workers-and-storage-reviewers,ttung Differential Revision: https://phabricator.services.mozilla.com/D93990

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "ActorsParent.h"

#include <inttypes.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <iterator>
#include <new>
#include <numeric>
#include <tuple>
#include <type_traits>
#include <utility>
#include "ActorsParentCommon.h"
#include "CrashAnnotations.h"
#include "DBSchema.h"
#include "ErrorList.h"
#include "FileInfoFwd.h"
#include "FileInfoT.h"
#include "FileManager.h"
#include "FileManagerBase.h"
#include "GeckoProfiler.h"
#include "IDBCursorType.h"
#include "IDBObjectStore.h"
#include "IDBTransaction.h"
#include "IndexedDBCommon.h"
#include "IndexedDatabaseInlines.h"
#include "IndexedDatabaseManager.h"
#include "KeyPath.h"
#include "MainThreadUtils.h"
#include "PermissionRequestBase.h"
#include "ProfilerHelpers.h"
#include "ReportInternalError.h"
#include "SafeRefPtr.h"
#include "SchemaUpgrades.h"
#include "chrome/common/ipc_channel.h"
#include "ipc/IPCMessageUtils.h"
#include "js/RootingAPI.h"
#include "js/StructuredClone.h"
#include "js/Value.h"
#include "jsapi.h"
#include "mozIStorageAsyncConnection.h"
#include "mozIStorageConnection.h"
#include "mozIStorageFunction.h"
#include "mozIStorageProgressHandler.h"
#include "mozIStorageService.h"
#include "mozIStorageStatement.h"
#include "mozIStorageValueArray.h"
#include "mozStorageCID.h"
#include "mozStorageHelper.h"
#include "mozilla/Algorithm.h"
#include "mozilla/ArrayAlgorithm.h"
#include "mozilla/ArrayIterator.h"
#include "mozilla/Assertions.h"
#include "mozilla/Atomics.h"
#include "mozilla/Attributes.h"
#include "mozilla/Casting.h"
#include "mozilla/CondVar.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/InitializedOnce.h"
#include "mozilla/Logging.h"
#include "mozilla/MacroForEach.h"
#include "mozilla/Maybe.h"
#include "mozilla/Monitor.h"
#include "mozilla/Mutex.h"
#include "mozilla/NotNull.h"
#include "mozilla/Preferences.h"
#include "mozilla/RefCountType.h"
#include "mozilla/RefCounted.h"
#include "mozilla/RemoteLazyInputStreamParent.h"
#include "mozilla/Result.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/SchedulerGroup.h"
#include "mozilla/Scoped.h"
#include "mozilla/SnappyCompressOutputStream.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/TaskCategory.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/Variant.h"
#include "mozilla/dom/BlobImpl.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/FileBlobImpl.h"
#include "mozilla/dom/FileHandleStorage.h"
#include "mozilla/dom/FlippedOnce.h"
#include "mozilla/dom/IDBCursorBinding.h"
#include "mozilla/dom/IPCBlob.h"
#include "mozilla/dom/IPCBlobUtils.h"
#include "mozilla/dom/IndexedDatabase.h"
#include "mozilla/dom/Nullable.h"
#include "mozilla/dom/PBackgroundMutableFileParent.h"
#include "mozilla/dom/PContentParent.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/filehandle/ActorsParent.h"
#include "mozilla/dom/indexedDB/IDBResult.h"
#include "mozilla/dom/indexedDB/Key.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBCursor.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBCursorParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabase.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseRequestParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBFactory.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryRequestParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBRequest.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBRequestParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBTransactionParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBVersionChangeTransactionParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIndexedDBUtilsParent.h"
#include "mozilla/dom/ipc/IdType.h"
#include "mozilla/dom/quota/CheckedUnsafePtr.h"
#include "mozilla/dom/quota/Client.h"
#include "mozilla/dom/quota/DecryptingInputStream_impl.h"
#include "mozilla/dom/quota/EncryptingOutputStream_impl.h"
#include "mozilla/dom/quota/FileStreams.h"
#include "mozilla/dom/quota/OriginScope.h"
#include "mozilla/dom/quota/PersistenceType.h"
#include "mozilla/dom/quota/QuotaCommon.h"
#include "mozilla/dom/quota/QuotaManager.h"
#include "mozilla/dom/quota/QuotaObject.h"
#include "mozilla/dom/quota/UsageInfo.h"
#include "mozilla/fallible.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/ipc/InputStreamParams.h"
#include "mozilla/ipc/PBackgroundParent.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozilla/ipc/ProtocolUtils.h"
#include "mozilla/mozalloc.h"
#include "mozilla/PRemoteLazyInputStreamParent.h"
#include "mozilla/storage/Variant.h"
#include "nsBaseHashtable.h"
#include "nsCOMPtr.h"
#include "nsClassHashtable.h"
#include "nsContentUtils.h"
#include "nsDataHashtable.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsEscape.h"
#include "nsExceptionHandler.h"
#include "nsHashKeys.h"
#include "nsIAsyncInputStream.h"
#include "nsID.h"
#include "nsIDirectoryEnumerator.h"
#include "nsIEventTarget.h"
#include "nsIFile.h"
#include "nsIFileProtocolHandler.h"
#include "nsIFileStreams.h"
#include "nsIFileURL.h"
#include "nsIInputStream.h"
#include "nsIOutputStream.h"
#include "nsIProtocolHandler.h"
#include "nsIRunnable.h"
#include "nsISupports.h"
#include "nsISupportsPriority.h"
#include "nsISupportsUtils.h"
#include "nsIThread.h"
#include "nsIThreadInternal.h"
#include "nsITimer.h"
#include "nsIURIMutator.h"
#include "nsIVariant.h"
#include "nsInterfaceHashtable.h"
#include "nsLiteralString.h"
#include "nsNetCID.h"
#include "nsPrintfCString.h"
#include "nsProxyRelease.h"
#include "nsRefPtrHashtable.h"
#include "nsServiceManagerUtils.h"
#include "nsStreamUtils.h"
#include "nsString.h"
#include "nsStringFlags.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTHashtable.h"
#include "nsTLiteralString.h"
#include "nsTStringRepr.h"
#include "nsThreadPool.h"
#include "nsThreadUtils.h"
#include "nscore.h"
#include "prinrval.h"
#include "prio.h"
#include "prsystem.h"
#include "prthread.h"
#include "prtime.h"
#include "prtypes.h"
#include "snappy/snappy.h"

struct JSContext;
class JSObject;
template <class T>
class nsPtrHashKey;

#define DISABLE_ASSERTS_FOR_FUZZING 0

#if DISABLE_ASSERTS_FOR_FUZZING
#  define ASSERT_UNLESS_FUZZING(...) \
    do {                             \
    } while (0)
#else
#  define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__)
#endif

#define IDB_DEBUG_LOG(_args) \
  MOZ_LOG(IndexedDatabaseManager::GetLoggingModule(), LogLevel::Debug, _args)

#if defined(MOZ_WIDGET_ANDROID)
#  define IDB_MOBILE
#endif

namespace mozilla {

MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedPRFileDesc, PRFileDesc,
                                          PR_Close);

namespace dom::indexedDB {

using namespace mozilla::dom::quota;
using namespace mozilla::ipc;
using mozilla::dom::quota::Client;

namespace {

class ConnectionPool;
class Database;
struct DatabaseActorInfo;
class DatabaseFile;
class DatabaseLoggingInfo;
class DatabaseMaintenance;
class Factory;
class Maintenance;
class MutableFile;
class OpenDatabaseOp;
class TransactionBase;
class TransactionDatabaseOperationBase;
class VersionChangeTransaction;
template <bool StatementHasIndexKeyBindings>
struct ValuePopulateResponseHelper;

/*******************************************************************************
 * Constants
 ******************************************************************************/

const int32_t kStorageProgressGranularity = 1000;

// Changing the value here will override the page size of new databases only.
// A journal mode change and VACUUM are needed to change existing databases, so
// the best way to do that is to use the schema version upgrade mechanism.
const uint32_t kSQLitePageSizeOverride =
#ifdef IDB_MOBILE
    2048;
#else
    4096;
#endif

static_assert(kSQLitePageSizeOverride == /* mozStorage default */ 0 ||
                  (kSQLitePageSizeOverride % 2 == 0 &&
                   kSQLitePageSizeOverride >= 512 &&
                   kSQLitePageSizeOverride <= 65536),
              "Must be 0 (disabled) or a power of 2 between 512 and 65536!");

// Set to -1 to use SQLite's default, 0 to disable, or some positive number to
// enforce a custom limit.
const int32_t kMaxWALPages = 5000;  // 20MB on desktop, 10MB on mobile.

// Set to some multiple of the page size to grow the database in larger chunks.
const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2;

static_assert(kSQLiteGrowthIncrement >= 0 &&
                  kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 &&
                  kSQLiteGrowthIncrement < uint32_t(INT32_MAX),
              "Must be 0 (disabled) or a positive multiple of the page size!");

// The maximum number of threads that can be used for database activity at a
// single time.
const uint32_t kMaxConnectionThreadCount = 20;

static_assert(kMaxConnectionThreadCount, "Must have at least one thread!");

// The maximum number of threads to keep when idle. Threads that become idle in
// excess of this number will be shut down immediately.
const uint32_t kMaxIdleConnectionThreadCount = 2;

static_assert(kMaxConnectionThreadCount >= kMaxIdleConnectionThreadCount,
              "Idle thread limit must be less than total thread limit!");

// The length of time that database connections will be held open after all
// transactions have completed before doing idle maintenance.
const uint32_t kConnectionIdleMaintenanceMS = 2 * 1000;  // 2 seconds

// The length of time that database connections will be held open after all
// transactions and maintenance have completed.
const uint32_t kConnectionIdleCloseMS = 10 * 1000;  // 10 seconds

// The length of time that idle threads will stay alive before being shut down.
const uint32_t kConnectionThreadIdleMS = 30 * 1000;  // 30 seconds

#define SAVEPOINT_CLAUSE "SAVEPOINT sp;"_ns

// For efficiency reasons, kEncryptedStreamBlockSize must be a multiple of large
// 4k disk sectors.
static_assert(kEncryptedStreamBlockSize % 4096 == 0);
// Similarly, the file copy buffer size must be a multiple of the encrypted
// block size.
static_assert(kFileCopyBufferSize % kEncryptedStreamBlockSize == 0);

constexpr auto kJournalDirectoryName = u"journals"_ns;

constexpr auto kFileManagerDirectoryNameSuffix = u".files"_ns;
constexpr auto kSQLiteSuffix = u".sqlite"_ns;
constexpr auto kSQLiteJournalSuffix = u".sqlite-journal"_ns;
constexpr auto kSQLiteSHMSuffix = u".sqlite-shm"_ns;
constexpr auto kSQLiteWALSuffix = u".sqlite-wal"_ns;

const char kPrefFileHandleEnabled[] = "dom.fileHandle.enabled";

constexpr auto kPermissionStringBase = "indexedDB-chrome-"_ns;
constexpr auto kPermissionReadSuffix = "-read"_ns;
constexpr auto kPermissionWriteSuffix = "-write"_ns;

// The following constants define all names of binding parameters in statements,
// where they are bound by name. This should include all parameter names which
// are bound by name. Binding may be done by index when the statement definition
// and binding are done in the same local scope, and no other reasons prevent
// using the indexes (e.g. multiple statement variants with differing number or
// order of parameters). Neither the styles of specifying parameter names
// (literally vs. via these constants) nor the binding styles (by index vs. by
// name) should not be mixed for the same statement. The decision must be made
// for each statement based on the proximity of statement and binding calls.
constexpr auto kStmtParamNameCurrentKey = "current_key"_ns;
constexpr auto kStmtParamNameRangeBound = "range_bound"_ns;
constexpr auto kStmtParamNameObjectStorePosition = "object_store_position"_ns;
constexpr auto kStmtParamNameLowerKey = "lower_key"_ns;
constexpr auto kStmtParamNameUpperKey = "upper_key"_ns;
constexpr auto kStmtParamNameKey = "key"_ns;
constexpr auto kStmtParamNameObjectStoreId = "object_store_id"_ns;
constexpr auto kStmtParamNameIndexId = "index_id"_ns;
// TODO: Maybe the uses of kStmtParamNameId should be replaced by more
// specific constants such as kStmtParamNameObjectStoreId.
constexpr auto kStmtParamNameId = "id"_ns;
constexpr auto kStmtParamNameValue = "value"_ns;
constexpr auto kStmtParamNameObjectDataKey = "object_data_key"_ns;
constexpr auto kStmtParamNameIndexDataValues = "index_data_values"_ns;
constexpr auto kStmtParamNameData = "data"_ns;
constexpr auto kStmtParamNameFileIds = "file_ids"_ns;
constexpr auto kStmtParamNameValueLocale = "value_locale"_ns;
constexpr auto kStmtParamNameLimit = "limit"_ns;

// The following constants define some names of columns in tables, which are
// referred to in remote locations, e.g. in calls to
// GetBindingClauseForKeyRange.
constexpr auto kColumnNameKey = "key"_ns;
constexpr auto kColumnNameValue = "value"_ns;
constexpr auto kColumnNameAliasSortKey = "sort_column"_ns;

// SQL fragments used at multiple locations.
constexpr auto kOpenLimit = " LIMIT "_ns;

// The deletion marker file is created before RemoveDatabaseFilesAndDirectory
// begins deleting a database. It is removed as the last step of deletion. If a
// deletion marker file is found when initializing the origin, the deletion
// routine is run again to ensure that the database and all of its related files
// are removed. The primary goal of this mechanism is to avoid situations where
// a database has been partially deleted, leading to inconsistent state for the
// origin.
constexpr auto kIdbDeletionMarkerFilePrefix = u"idb-deleting-"_ns;

const uint32_t kDeleteTimeoutMs = 1000;

/**
 * Automatically crash the browser if IndexedDB shutdown takes this long.  We've
 * chosen a value that is longer than the value for QuotaManager shutdown timer
 * which is currently set to 30 seconds.  We've also chosen a value that is long
 * long enough that it is unlikely for the problem to be falsely triggered by
 * slow system I/O.  We've also chosen a value long enough so that automated
 * tests should time out and fail if IndexedDB shutdown hangs.  Also, this value
 * is long enough so that testers can notice the IndexedDB shutdown hang; we
 * want to know about the hangs, not hide them.  On the other hand this value is
 * less than 60 seconds which is used by nsTerminator to crash a hung main
 * process.
 */
#define SHUTDOWN_TIMEOUT_MS 50000

#ifdef DEBUG

const int32_t kDEBUGThreadPriority = nsISupportsPriority::PRIORITY_NORMAL;
const uint32_t kDEBUGThreadSleepMS = 0;

const int32_t kDEBUGTransactionThreadPriority =
    nsISupportsPriority::PRIORITY_NORMAL;
const uint32_t kDEBUGTransactionThreadSleepMS = 0;

#endif

/*******************************************************************************
 * Metadata classes
 ******************************************************************************/

// Can be instantiated either on the QuotaManager IO thread or on a
// versionchange transaction thread. These threads can never race so this is
// totally safe.
struct FullIndexMetadata {
  IndexMetadata mCommonMetadata = {0,     nsString(), KeyPath(0), nsCString(),
                                   false, false,      false};

  FlippedOnce<false> mDeleted;

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullIndexMetadata)

 private:
  ~FullIndexMetadata() = default;
};

typedef nsRefPtrHashtable<nsUint64HashKey, FullIndexMetadata> IndexTable;

// Can be instantiated either on the QuotaManager IO thread or on a
// versionchange transaction thread. These threads can never race so this is
// totally safe.
struct FullObjectStoreMetadata {
  ObjectStoreMetadata mCommonMetadata = {0, nsString(), KeyPath(0), false};
  IndexTable mIndexes;

  // These two members are only ever touched on a transaction thread!
  int64_t mNextAutoIncrementId = 0;
  int64_t mCommittedAutoIncrementId = 0;

  FlippedOnce<false> mDeleted;

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullObjectStoreMetadata);

  bool HasLiveIndexes() const;

 private:
  ~FullObjectStoreMetadata() = default;
};

typedef nsRefPtrHashtable<nsUint64HashKey, FullObjectStoreMetadata>
    ObjectStoreTable;

static_assert(
    std::is_same_v<
        IndexOrObjectStoreId,
        std::remove_cv_t<std::remove_reference_t<decltype(
            std::declval<const ObjectStoreGetParams&>().objectStoreId())>>>);
static_assert(std::is_same_v<
              IndexOrObjectStoreId,
              std::remove_cv_t<std::remove_reference_t<decltype(
                  std::declval<const IndexGetParams&>().objectStoreId())>>>);

struct FullDatabaseMetadata final : AtomicSafeRefCounted<FullDatabaseMetadata> {
  DatabaseMetadata mCommonMetadata;
  nsCString mDatabaseId;
  nsString mFilePath;
  ObjectStoreTable mObjectStores;

  IndexOrObjectStoreId mNextObjectStoreId = 0;
  IndexOrObjectStoreId mNextIndexId = 0;

 public:
  explicit FullDatabaseMetadata(const DatabaseMetadata& aCommonMetadata)
      : mCommonMetadata(aCommonMetadata) {
    AssertIsOnBackgroundThread();
  }

  [[nodiscard]] SafeRefPtr<FullDatabaseMetadata> Duplicate() const;

  MOZ_DECLARE_REFCOUNTED_TYPENAME(FullDatabaseMetadata)
};

template <class Enumerable>
auto MatchMetadataNameOrId(const Enumerable& aEnumerable,
                           IndexOrObjectStoreId aId,
                           Maybe<const nsAString&> aName = Nothing()) {
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(aId);

  const auto it = std::find_if(
      aEnumerable.cbegin(), aEnumerable.cend(),
      [aId, aName](const auto& entry) {
        MOZ_ASSERT(entry.GetKey() != 0);

        const auto& value = entry.GetData();
        MOZ_ASSERT(value);

        return !value->mDeleted &&
               (aId == value->mCommonMetadata.id() ||
                (aName && *aName == value->mCommonMetadata.name()));
      });

  return it != aEnumerable.cend() ? SomeRef(*it->GetData()) : Nothing();
}

/*******************************************************************************
 * SQLite functions
 ******************************************************************************/

// WARNING: the hash function used for the database name must not change.
// That's why this function exists separately from mozilla::HashString(), even
// though it is (at the time of writing) equivalent. See bug 780408 and bug
// 940315 for details.
uint32_t HashName(const nsAString& aName) {
  struct Helper {
    static uint32_t RotateBitsLeft32(uint32_t aValue, uint8_t aBits) {
      MOZ_ASSERT(aBits < 32);
      return (aValue << aBits) | (aValue >> (32 - aBits));
    }
  };

  static const uint32_t kGoldenRatioU32 = 0x9e3779b9u;

  return std::accumulate(aName.BeginReading(), aName.EndReading(), uint32_t(0),
                         [](uint32_t hash, char16_t ch) {
                           return kGoldenRatioU32 *
                                  (Helper::RotateBitsLeft32(hash, 5) ^ ch);
                         });
}

Result<Ok, nsresult> AssertExists(const nsCOMPtr<nsIFile>& aDirectory) {
#ifdef DEBUG
  IDB_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(aDirectory, Exists));

  MOZ_ASSERT(exists);
#endif

  return Ok{};
};

nsresult ClampResultCode(nsresult aResultCode) {
  if (NS_SUCCEEDED(aResultCode) ||
      NS_ERROR_GET_MODULE(aResultCode) == NS_ERROR_MODULE_DOM_INDEXEDDB) {
    return aResultCode;
  }

  switch (aResultCode) {
    case NS_ERROR_FILE_NO_DEVICE_SPACE:
      return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
    case NS_ERROR_STORAGE_CONSTRAINT:
      return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR;
    default:
#ifdef DEBUG
      nsPrintfCString message("Converting non-IndexedDB error code (0x%" PRIX32
                              ") to "
                              "NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR",
                              static_cast<uint32_t>(aResultCode));
      NS_WARNING(message.get());
#else
        ;
#endif
  }

  IDB_REPORT_INTERNAL_ERR();
  return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}

nsAutoString GetDatabaseFilenameBase(const nsAString& aDatabaseName) {
  nsAutoString databaseFilenameBase;

  // WARNING: do not change this hash function. See the comment in HashName()
  // for details.
  databaseFilenameBase.AppendInt(HashName(aDatabaseName));

  nsAutoCString escapedName;
  if (!NS_Escape(NS_ConvertUTF16toUTF8(aDatabaseName), escapedName,
                 url_XPAlphas)) {
    MOZ_CRASH("Can't escape database name!");
  }

  const char* forwardIter = escapedName.BeginReading();
  const char* backwardIter = escapedName.EndReading() - 1;

  nsAutoCString substring;
  while (forwardIter <= backwardIter && substring.Length() < 21) {
    if (substring.Length() % 2) {
      substring.Append(*backwardIter--);
    } else {
      substring.Append(*forwardIter++);
    }
  }

  databaseFilenameBase.AppendASCII(substring.get(), substring.Length());

  return databaseFilenameBase;
}

Result<nsCOMPtr<nsIFileURL>, nsresult> GetDatabaseFileURL(
    nsIFile& aDatabaseFile, const int64_t aDirectoryLockId,
    const Maybe<CipherKey>& aMaybeKey) {
  MOZ_ASSERT(aDirectoryLockId >= -1);

  IDB_TRY_INSPECT(const auto& protocolHandler,
                  ToResultGet<nsCOMPtr<nsIProtocolHandler>>(
                      MOZ_SELECT_OVERLOAD(do_GetService),
                      NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file"));

  IDB_TRY_INSPECT(const auto& fileHandler,
                  ToResultGet<nsCOMPtr<nsIFileProtocolHandler>>(
                      MOZ_SELECT_OVERLOAD(do_QueryInterface), protocolHandler));

  IDB_TRY_INSPECT(const auto& mutator, MOZ_TO_RESULT_INVOKE_TYPED(
                                           nsCOMPtr<nsIURIMutator>, fileHandler,
                                           NewFileURIMutator, &aDatabaseFile));

  // aDirectoryLockId should only be -1 when we are called
  // - from FileManager::InitDirectory when the temporary storage hasn't been
  //    initialized yet. At that time, the in-memory objects (e.g. OriginInfo)
  //    are only being created so it doesn't make sense to tunnel quota
  //    information to TelemetryVFS to get corresponding QuotaObject instances
  //    for SQLite files.
  // - from DeleteDatabaseOp::LoadPreviousVersion, since this might require
  //   temporarily exceeding the quota limit before the database can be deleted.
  const auto directoryLockIdClause =
      aDirectoryLockId >= 0
          ? "&directoryLockId="_ns + IntToCString(aDirectoryLockId)
          : EmptyCString();

  const auto keyClause = [&aMaybeKey] {
    nsAutoCString keyClause;
    if (aMaybeKey) {
      keyClause.AssignLiteral("&key=");
      for (uint8_t byte : IndexedDBCipherStrategy::SerializeKey(*aMaybeKey)) {
        keyClause.AppendPrintf("%02x", byte);
      }
    }
    return keyClause;
  }();

  IDB_TRY_UNWRAP(
      auto result, ([&mutator, &directoryLockIdClause, &keyClause] {
        nsCOMPtr<nsIFileURL> result;
        nsresult rv = NS_MutateURI(mutator)
                          .SetQuery("cache=private"_ns + directoryLockIdClause +
                                    keyClause)
                          .Finalize(result);
        return NS_SUCCEEDED(rv) ? Result<nsCOMPtr<nsIFileURL>, nsresult>{result}
                                : Err(rv);
      }()));

  return result;
}

nsresult SetDefaultPragmas(mozIStorageConnection& aConnection) {
  MOZ_ASSERT(!NS_IsMainThread());

  static constexpr auto kBuiltInPragmas =
      // We use foreign keys in DEBUG builds only because there is a performance
      // cost to using them.
      "PRAGMA foreign_keys = "
#ifdef DEBUG
      "ON"
#else
      "OFF"
#endif
      ";"

      // The "INSERT OR REPLACE" statement doesn't fire the update trigger,
      // instead it fires only the insert trigger. This confuses the update
      // refcount function. This behavior changes with enabled recursive
      // triggers, so the statement fires the delete trigger first and then the
      // insert trigger.
      "PRAGMA recursive_triggers = ON;"

      // We aggressively truncate the database file when idle so don't bother
      // overwriting the WAL with 0 during active periods.
      "PRAGMA secure_delete = OFF;"_ns;

  IDB_TRY(aConnection.ExecuteSimpleSQL(kBuiltInPragmas));

  IDB_TRY(aConnection.ExecuteSimpleSQL(nsAutoCString{
      "PRAGMA synchronous = "_ns +
      (IndexedDatabaseManager::FullSynchronous() ? "FULL"_ns : "NORMAL"_ns) +
      ";"_ns}));

#ifndef IDB_MOBILE
  if (kSQLiteGrowthIncrement) {
    // This is just an optimization so ignore the failure if the disk is
    // currently too full.
    IDB_TRY(
        ToResult(aConnection.SetGrowthIncrement(kSQLiteGrowthIncrement, ""_ns))
            .mapErr([](const nsresult rv) {
              return rv == NS_ERROR_FILE_TOO_BIG ? NS_OK : rv;
            }));
  }
#endif  // IDB_MOBILE

  return NS_OK;
}

Result<nsCOMPtr<mozIStorageStatement>, nsresult>
CreateAndExecuteSingleStepStatement(mozIStorageConnection& aConnection,
                                    const nsACString& aStatementString) {
  IDB_TRY_UNWRAP(auto stmt, MOZ_TO_RESULT_INVOKE_TYPED(
                                nsCOMPtr<mozIStorageStatement>, aConnection,
                                CreateStatement, aStatementString));

  IDB_TRY_UNWRAP(const DebugOnly<bool> hasResult,
                 MOZ_TO_RESULT_INVOKE(stmt, ExecuteStep));
  MOZ_ASSERT(hasResult);

  return stmt;
}

template <typename StepFunc>
Result<Ok, nsresult> CollectWhileHasResult(mozIStorageStatement& aStmt,
                                           StepFunc&& aStepFunc) {
  return CollectWhile(
      [&aStmt] { IDB_TRY_RETURN(MOZ_TO_RESULT_INVOKE(aStmt, ExecuteStep)); },
      [&aStmt, &aStepFunc] { return aStepFunc(aStmt); });
}

nsresult SetJournalMode(mozIStorageConnection& aConnection) {
  MOZ_ASSERT(!NS_IsMainThread());

  // Try enabling WAL mode. This can fail in various circumstances so we have to
  // check the results here.
  constexpr auto journalModeQueryStart = "PRAGMA journal_mode = "_ns;
  constexpr auto journalModeWAL = "wal"_ns;

  IDB_TRY_INSPECT(const auto& stmt,
                  CreateAndExecuteSingleStepStatement(
                      aConnection, journalModeQueryStart + journalModeWAL));

  IDB_TRY_INSPECT(
      const auto& journalMode,
      MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 0));

  if (journalMode.Equals(journalModeWAL)) {
    // WAL mode successfully enabled. Maybe set limits on its size here.
    if (kMaxWALPages >= 0) {
      IDB_TRY(aConnection.ExecuteSimpleSQL("PRAGMA wal_autocheckpoint = "_ns +
                                           IntToCString(kMaxWALPages)));
    }
  } else {
    NS_WARNING("Failed to set WAL mode, falling back to normal journal mode.");
#ifdef IDB_MOBILE
    IDB_TRY(
        aConnection.ExecuteSimpleSQL(journalModeQueryStart + "truncate"_ns));
#endif
  }

  return NS_OK;
}

Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult> OpenDatabase(
    mozIStorageService& aStorageService, nsIFileURL& aFileURL,
    const uint32_t aTelemetryId = 0) {
  const nsAutoCString telemetryFilename =
      aTelemetryId ? "indexedDB-"_ns + IntToCString(aTelemetryId) +
                         NS_ConvertUTF16toUTF8(kSQLiteSuffix)
                   : nsAutoCString();

  IDB_TRY_UNWRAP(auto connection,
                 MOZ_TO_RESULT_INVOKE_TYPED(
                     nsCOMPtr<mozIStorageConnection>, aStorageService,
                     OpenDatabaseWithFileURL, &aFileURL, telemetryFilename));

  return WrapMovingNotNull(std::move(connection));
}

Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
OpenDatabaseAndHandleBusy(mozIStorageService& aStorageService,
                          nsIFileURL& aFileURL,
                          const uint32_t aTelemetryId = 0) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());

  using ConnectionType = Maybe<MovingNotNull<nsCOMPtr<mozIStorageConnection>>>;

  IDB_TRY_UNWRAP(
      auto connection,
      OpenDatabase(aStorageService, aFileURL, aTelemetryId)
          .map([](auto connection) -> ConnectionType {
            return Some(std::move(connection));
          })
          .orElse(ErrToDefaultOkOrErr<NS_ERROR_STORAGE_BUSY, ConnectionType>));

  if (connection.isNothing()) {
#ifdef DEBUG
    {
      nsCString path;
      MOZ_ALWAYS_SUCCEEDS(aFileURL.GetFileName(path));

      nsPrintfCString message(
          "Received NS_ERROR_STORAGE_BUSY when attempting to open database "
          "'%s', retrying for up to 10 seconds",
          path.get());
      NS_WARNING(message.get());
    }
#endif

    // Another thread must be checkpointing the WAL. Wait up to 10 seconds for
    // that to complete.
    const TimeStamp start = TimeStamp::NowLoRes();

    do {
      PR_Sleep(PR_MillisecondsToInterval(100));

      IDB_TRY_UNWRAP(
          connection,
          OpenDatabase(aStorageService, aFileURL, aTelemetryId)
              .map([](auto connection) -> ConnectionType {
                return Some(std::move(connection));
              })
              .orElse([&start](
                          nsresult aValue) -> Result<ConnectionType, nsresult> {
                if (aValue != NS_ERROR_STORAGE_BUSY ||
                    TimeStamp::NowLoRes() - start >
                        TimeDuration::FromSeconds(10)) {
                  return Err(aValue);
                }

                return ConnectionType();
              }));
    } while (connection.isNothing());
  }

  return connection.extract();
}

// Returns true if a given nsIFile exists and is a directory. Returns false if
// it doesn't exist. Returns an error if it exists, but is not a directory, or
// any other error occurs.
Result<bool, nsresult> ExistsAsDirectory(nsIFile& aDirectory) {
  IDB_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(aDirectory, Exists));

  if (exists) {
    IDB_TRY_INSPECT(const bool& isDirectory,
                    MOZ_TO_RESULT_INVOKE(aDirectory, IsDirectory));

    IDB_TRY(OkIf(isDirectory), Err(NS_ERROR_FAILURE));
  }

  return exists;
}

constexpr nsresult mapNoDeviceSpaceError(nsresult aRv) {
  if (aRv == NS_ERROR_FILE_NO_DEVICE_SPACE) {
    // mozstorage translates SQLITE_FULL to
    // NS_ERROR_FILE_NO_DEVICE_SPACE, which we know better as
    // NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR.
    return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
  }
  return aRv;
}

Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
CreateStorageConnection(nsIFile& aDBFile, nsIFile& aFMDirectory,
                        const nsAString& aName, const nsACString& aOrigin,
                        const int64_t aDirectoryLockId,
                        const uint32_t aTelemetryId,
                        const Maybe<CipherKey>& aMaybeKey) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aDirectoryLockId >= -1);

  AUTO_PROFILER_LABEL("CreateStorageConnection", DOM);

  IDB_TRY_INSPECT(const auto& dbFileUrl,
                  GetDatabaseFileURL(aDBFile, aDirectoryLockId, aMaybeKey));

  IDB_TRY_INSPECT(
      const auto& storageService,
      ToResultGet<nsCOMPtr<mozIStorageService>>(
          MOZ_SELECT_OVERLOAD(do_GetService), MOZ_STORAGE_SERVICE_CONTRACTID));

  IDB_TRY_UNWRAP(
      auto connection,
      OpenDatabaseAndHandleBusy(*storageService, *dbFileUrl, aTelemetryId)
          .map([](auto connection) -> nsCOMPtr<mozIStorageConnection> {
            return std::move(connection).unwrapBasePtr();
          })
          .orElse([&aName](nsresult aValue)
                      -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
            // If we're just opening the database during origin initialization,
            // then we don't want to erase any files. The failure here will fail
            // origin initialization too.
            if (aValue != NS_ERROR_FILE_CORRUPTED || aName.IsVoid()) {
              return Err(aValue);
            }

            return nsCOMPtr<mozIStorageConnection>();
          }));

  if (!connection) {
    // XXX Shouldn't we also update quota usage?

    // Nuke the database file.
    IDB_TRY(aDBFile.Remove(false));
    IDB_TRY_INSPECT(const bool& existsAsDirectory,
                    ExistsAsDirectory(aFMDirectory));

    if (existsAsDirectory) {
      IDB_TRY(aFMDirectory.Remove(true));
    }

    IDB_TRY_UNWRAP(connection, OpenDatabaseAndHandleBusy(
                                   *storageService, *dbFileUrl, aTelemetryId));
  }

  IDB_TRY(SetDefaultPragmas(*connection));
  IDB_TRY(connection->EnableModule("filesystem"_ns));

  // Check to make sure that the database schema is correct.
  IDB_TRY_INSPECT(const int32_t& schemaVersion,
                  MOZ_TO_RESULT_INVOKE(connection, GetSchemaVersion));

  // Unknown schema will fail origin initialization too.
  IDB_TRY(
      OkIf(schemaVersion || !aName.IsVoid()),
      Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), [](const auto&) {
        IDB_WARNING("Unable to open IndexedDB database, schema is not set!");
      });

  IDB_TRY(
      OkIf(schemaVersion <= kSQLiteSchemaVersion),
      Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), [](const auto&) {
        IDB_WARNING("Unable to open IndexedDB database, schema is too high!");
      });

  bool journalModeSet = false;

  if (schemaVersion != kSQLiteSchemaVersion) {
    const bool newDatabase = !schemaVersion;

    if (newDatabase) {
      // Set the page size first.
      const auto sqlitePageSizeOverride =
          aMaybeKey ? 8192 : kSQLitePageSizeOverride;
      if (sqlitePageSizeOverride) {
        IDB_TRY(connection->ExecuteSimpleSQL(nsPrintfCString(
            "PRAGMA page_size = %" PRIu32 ";", sqlitePageSizeOverride)));
      }

      // We have to set the auto_vacuum mode before opening a transaction.
      IDB_TRY((MOZ_TO_RESULT_INVOKE(
                   connection, ExecuteSimpleSQL,
#ifdef IDB_MOBILE
                   // Turn on full auto_vacuum mode to reclaim disk space on
                   // mobile devices (at the cost of some COMMIT speed).
                   "PRAGMA auto_vacuum = FULL;"_ns
#else
                   // Turn on incremental auto_vacuum mode on desktop builds.
                   "PRAGMA auto_vacuum = INCREMENTAL;"_ns
#endif
                   )
                   .mapErr(mapNoDeviceSpaceError)));

      IDB_TRY(SetJournalMode(*connection));

      journalModeSet = true;
    } else {
#ifdef DEBUG
      // Disable foreign key support while upgrading. This has to be done before
      // starting a transaction.
      MOZ_ALWAYS_SUCCEEDS(
          connection->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns));
#endif
    }

    bool vacuumNeeded = false;

    mozStorageTransaction transaction(
        connection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);

    if (newDatabase) {
      IDB_TRY(CreateTables(*connection));

#ifdef DEBUG
      {
        IDB_TRY_INSPECT(const int32_t& schemaVersion,
                        MOZ_TO_RESULT_INVOKE(connection, GetSchemaVersion),
                        QM_ASSERT_UNREACHABLE);
        MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
      }
#endif

      // The parameter names are not used, parameters are bound by index only
      // locally in the same function.
      IDB_TRY_INSPECT(
          const auto& stmt,
          MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection,
                                     CreateStatement,
                                     "INSERT INTO database (name, origin) "
                                     "VALUES (:name, :origin)"_ns));

      IDB_TRY(stmt->BindStringByIndex(0, aName));
      IDB_TRY(stmt->BindUTF8StringByIndex(1, aOrigin));
      IDB_TRY(stmt->Execute());
    } else {
      IDB_TRY_UNWRAP(vacuumNeeded,
                     MaybeUpgradeSchema(*connection, schemaVersion,
                                        aFMDirectory, aOrigin));
    }

    IDB_TRY(MOZ_TO_RESULT_INVOKE(transaction, Commit)
                .mapErr(mapNoDeviceSpaceError));

#ifdef DEBUG
    if (!newDatabase) {
      // Re-enable foreign key support after doing a foreign key check.
      nsCOMPtr<mozIStorageStatement> checkStmt;
      MOZ_ALWAYS_SUCCEEDS(connection->CreateStatement(
          "PRAGMA foreign_key_check;"_ns, getter_AddRefs(checkStmt)));

      bool hasResult;
      MOZ_ALWAYS_SUCCEEDS(checkStmt->ExecuteStep(&hasResult));
      MOZ_ASSERT(!hasResult, "Database has inconsisistent foreign keys!");

      MOZ_ALWAYS_SUCCEEDS(
          connection->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns));
    }
#endif

    if (kSQLitePageSizeOverride && !newDatabase) {
      IDB_TRY_INSPECT(const auto& stmt,
                      CreateAndExecuteSingleStepStatement(
                          *connection, "PRAGMA page_size;"_ns));

      IDB_TRY_INSPECT(const int32_t& pageSize,
                      MOZ_TO_RESULT_INVOKE(stmt, GetInt32, 0));
      MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536);

      if (kSQLitePageSizeOverride != uint32_t(pageSize)) {
        // We must not be in WAL journal mode to change the page size.
        IDB_TRY(
            connection->ExecuteSimpleSQL("PRAGMA journal_mode = DELETE;"_ns));

        IDB_TRY_INSPECT(const auto& stmt,
                        CreateAndExecuteSingleStepStatement(
                            *connection, "PRAGMA journal_mode;"_ns));

        IDB_TRY_INSPECT(
            const auto& journalMode,
            MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 0));

        if (journalMode.EqualsLiteral("delete")) {
          // Successfully set to rollback journal mode so changing the page size
          // is possible with a VACUUM.
          IDB_TRY(connection->ExecuteSimpleSQL(nsPrintfCString(
              "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride)));

          // We will need to VACUUM in order to change the page size.
          vacuumNeeded = true;
        } else {
          NS_WARNING(
              "Failed to set journal_mode for database, unable to "
              "change the page size!");
        }
      }
    }

    if (vacuumNeeded) {
      IDB_TRY(connection->ExecuteSimpleSQL("VACUUM;"_ns));
    }

    if (newDatabase || vacuumNeeded) {
      if (journalModeSet) {
        // Make sure we checkpoint to get an accurate file size.
        IDB_TRY(
            connection->ExecuteSimpleSQL("PRAGMA wal_checkpoint(FULL);"_ns));
      }

      IDB_TRY_INSPECT(const int64_t& fileSize,
                      MOZ_TO_RESULT_INVOKE(aDBFile, GetFileSize));
      MOZ_ASSERT(fileSize > 0);

      PRTime vacuumTime = PR_Now();
      MOZ_ASSERT(vacuumTime);

      // The parameter names are not used, parameters are bound by index only
      // locally in the same function.
      IDB_TRY_INSPECT(
          const auto& vacuumTimeStmt,
          MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection,
                                     CreateStatement,
                                     "UPDATE database "
                                     "SET last_vacuum_time = :time"
                                     ", last_vacuum_size = :size;"_ns));

      IDB_TRY(vacuumTimeStmt->BindInt64ByIndex(0, vacuumTime));
      IDB_TRY(vacuumTimeStmt->BindInt64ByIndex(1, fileSize));
      IDB_TRY(vacuumTimeStmt->Execute());
    }
  }

  if (!journalModeSet) {
    IDB_TRY(SetJournalMode(*connection));
  }

  return WrapMovingNotNullUnchecked(std::move(connection));
}

nsCOMPtr<nsIFile> GetFileForPath(const nsAString& aPath) {
  MOZ_ASSERT(!aPath.IsEmpty());

  IDB_TRY_RETURN(QM_NewLocalFile(aPath), nullptr);
}

Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
GetStorageConnection(nsIFile& aDatabaseFile, const int64_t aDirectoryLockId,
                     const uint32_t aTelemetryId,
                     const Maybe<CipherKey>& aMaybeKey) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(aDirectoryLockId >= 0);

  AUTO_PROFILER_LABEL("GetStorageConnection", DOM);

  IDB_TRY_INSPECT(const bool& exists,
                  MOZ_TO_RESULT_INVOKE(aDatabaseFile, Exists));

  IDB_TRY(OkIf(exists), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR),
          IDB_REPORT_INTERNAL_ERR_LAMBDA);

  IDB_TRY_INSPECT(
      const auto& dbFileUrl,
      GetDatabaseFileURL(aDatabaseFile, aDirectoryLockId, aMaybeKey));

  IDB_TRY_INSPECT(
      const auto& storageService,
      ToResultGet<nsCOMPtr<mozIStorageService>>(
          MOZ_SELECT_OVERLOAD(do_GetService), MOZ_STORAGE_SERVICE_CONTRACTID));

  IDB_TRY_UNWRAP(
      nsCOMPtr<mozIStorageConnection> connection,
      OpenDatabaseAndHandleBusy(*storageService, *dbFileUrl, aTelemetryId));

  IDB_TRY(SetDefaultPragmas(*connection));

  IDB_TRY(SetJournalMode(*connection));

  return WrapMovingNotNullUnchecked(std::move(connection));
}

Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
GetStorageConnection(const nsAString& aDatabaseFilePath,
                     const int64_t aDirectoryLockId,
                     const uint32_t aTelemetryId,
                     const Maybe<CipherKey>& aMaybeKey) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(!aDatabaseFilePath.IsEmpty());
  MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, kSQLiteSuffix));
  MOZ_ASSERT(aDirectoryLockId >= 0);

  nsCOMPtr<nsIFile> dbFile = GetFileForPath(aDatabaseFilePath);

  IDB_TRY(OkIf(dbFile), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR),
          IDB_REPORT_INTERNAL_ERR_LAMBDA);

  return GetStorageConnection(*dbFile, aDirectoryLockId, aTelemetryId,
                              aMaybeKey);
}

/*******************************************************************************
 * ConnectionPool declarations
 ******************************************************************************/

class DatabaseConnection final {
  friend class ConnectionPool;

  enum class CheckpointMode { Full, Restart, Truncate };

 public:
  class AutoSavepoint;
  class CachedStatement;
  class UpdateRefcountFunction;

  class MOZ_STACK_CLASS BorrowedStatement : mozStorageStatementScoper {
   public:
    mozIStorageStatement& operator*() const;

    MOZ_NONNULL_RETURN mozIStorageStatement* operator->() const
        MOZ_NO_ADDREF_RELEASE_ON_RETURN;

    BorrowedStatement(BorrowedStatement&& aOther) = default;

    // No funny business allowed.
    BorrowedStatement& operator=(BorrowedStatement&&) = delete;
    BorrowedStatement(const BorrowedStatement&) = delete;
    BorrowedStatement& operator=(const BorrowedStatement&) = delete;

   private:
    friend class CachedStatement;

#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
    BorrowedStatement(NotNull<mozIStorageStatement*> aStatement,
                      const nsACString& aQuery)
        : mozStorageStatementScoper(aStatement),
          mExtraInfo{ScopedLogExtraInfo::kTagQuery, aQuery} {}

    ScopedLogExtraInfo mExtraInfo;
#else
    MOZ_IMPLICIT BorrowedStatement(NotNull<mozIStorageStatement*> aStatement)
        : mozStorageStatementScoper(aStatement) {}
#endif
  };

 private:
  InitializedOnce<const NotNull<nsCOMPtr<mozIStorageConnection>>>
      mStorageConnection;
  InitializedOnce<const NotNull<SafeRefPtr<FileManager>>> mFileManager;
  nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement>
      mCachedStatements;
  RefPtr<UpdateRefcountFunction> mUpdateRefcountFunction;
  RefPtr<QuotaObject> mQuotaObject;
  RefPtr<QuotaObject> mJournalQuotaObject;
  bool mInReadTransaction;
  bool mInWriteTransaction;

#ifdef DEBUG
  uint32_t mDEBUGSavepointCount;
#endif

  NS_DECL_OWNINGTHREAD

 public:
  void AssertIsOnConnectionThread() const {
    NS_ASSERT_OWNINGTHREAD(DatabaseConnection);
  }

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DatabaseConnection)

  bool HasStorageConnection() const {
    return static_cast<bool>(mStorageConnection);
  }

  mozIStorageConnection& MutableStorageConnection() const {
    AssertIsOnConnectionThread();
    MOZ_ASSERT(mStorageConnection);

    return **mStorageConnection;
  }

  UpdateRefcountFunction* GetUpdateRefcountFunction() const {
    AssertIsOnConnectionThread();

    return mUpdateRefcountFunction;
  }

  Result<CachedStatement, nsresult> GetCachedStatement(
      const nsACString& aQuery);

  Result<BorrowedStatement, nsresult> BorrowCachedStatement(
      const nsACString& aQuery);

  template <typename BindFunctor>
  nsresult ExecuteCachedStatement(const nsACString& aQuery,
                                  const BindFunctor& aBindFunctor);

  nsresult ExecuteCachedStatement(const nsACString& aQuery);

  nsresult BeginWriteTransaction();

  nsresult CommitWriteTransaction();

  void RollbackWriteTransaction();

  void FinishWriteTransaction();

  nsresult StartSavepoint();

  nsresult ReleaseSavepoint();

  nsresult RollbackSavepoint();

  nsresult Checkpoint() {
    AssertIsOnConnectionThread();

    return CheckpointInternal(CheckpointMode::Full);
  }

  void DoIdleProcessing(bool aNeedsCheckpoint);

  void Close();

  nsresult DisableQuotaChecks();

  void EnableQuotaChecks();

 private:
  DatabaseConnection(
      MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection,
      MovingNotNull<SafeRefPtr<FileManager>> aFileManager);

  ~DatabaseConnection();

  nsresult Init();

  nsresult CheckpointInternal(CheckpointMode aMode);

  Result<uint32_t, nsresult> GetFreelistCount(
      CachedStatement& aCachedStatement);

  /**
   * On success, returns whether some pages were freed.
   */
  Result<bool, nsresult> ReclaimFreePagesWhileIdle(
      CachedStatement& aFreelistStatement, CachedStatement& aRollbackStatement,
      uint32_t aFreelistCount, bool aNeedsCheckpoint);

  Result<int64_t, nsresult> GetFileSize(const nsAString& aPath);
};

class MOZ_STACK_CLASS DatabaseConnection::AutoSavepoint final {
  DatabaseConnection* mConnection;
#ifdef DEBUG
  const TransactionBase* mDEBUGTransaction;
#endif

 public:
  AutoSavepoint();
  ~AutoSavepoint();

  nsresult Start(const TransactionBase& aTransaction);

  nsresult Commit();
};

class DatabaseConnection::CachedStatement final {
  friend class DatabaseConnection;

  nsCOMPtr<mozIStorageStatement> mStatement;

#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
  nsCString mQuery;
#endif

#ifdef DEBUG
  DatabaseConnection* mDEBUGConnection;
#endif

 public:
#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING)
  CachedStatement();
  ~CachedStatement();
#else
  CachedStatement() = default;
#endif

  void AssertIsOnConnectionThread() const {
#ifdef DEBUG
    if (mDEBUGConnection) {
      mDEBUGConnection->AssertIsOnConnectionThread();
    }
    MOZ_ASSERT(!NS_IsMainThread());
    MOZ_ASSERT(!IsOnBackgroundThread());
#endif
  }

  explicit operator bool() const;

  BorrowedStatement Borrow() const;

 private:
  // Only called by DatabaseConnection.
  CachedStatement(DatabaseConnection* aConnection,
                  nsCOMPtr<mozIStorageStatement> aStatement,
                  const nsACString& aQuery);

 public:
#if defined(NS_BUILD_REFCNT_LOGGING)
  CachedStatement(CachedStatement&& aOther)
      : mStatement(std::move(aOther.mStatement))
#  if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
        ,
        mQuery(std::move(aOther.mQuery))
#  endif
#  ifdef DEBUG
        ,
        mDEBUGConnection(aOther.mDEBUGConnection)
#  endif
  {
    MOZ_COUNT_CTOR(DatabaseConnection::CachedStatement);
  }
#else
  CachedStatement(CachedStatement&&) = default;
#endif

  CachedStatement& operator=(CachedStatement&&) = default;

  // No funny business allowed.
  CachedStatement(const CachedStatement&) = delete;
  CachedStatement& operator=(const CachedStatement&) = delete;
};

class DatabaseConnection::UpdateRefcountFunction final
    : public mozIStorageFunction {
  class FileInfoEntry;

  enum class UpdateType { Increment, Decrement };

  DatabaseConnection* const mConnection;
  FileManager& mFileManager;
  nsClassHashtable<nsUint64HashKey, FileInfoEntry> mFileInfoEntries;
  nsDataHashtable<nsUint64HashKey, FileInfoEntry*> mSavepointEntriesIndex;

  nsTArray<int64_t> mJournalsToCreateBeforeCommit;
  nsTArray<int64_t> mJournalsToRemoveAfterCommit;
  nsTArray<int64_t> mJournalsToRemoveAfterAbort;

  bool mInSavepoint;

 public:
  NS_DECL_ISUPPORTS
  NS_DECL_MOZISTORAGEFUNCTION

  UpdateRefcountFunction(DatabaseConnection* aConnection,
                         FileManager& aFileManager);

  nsresult WillCommit();

  void DidCommit();

  void DidAbort();

  void StartSavepoint();

  void ReleaseSavepoint();

  void RollbackSavepoint();

  void Reset();

 private:
  ~UpdateRefcountFunction() = default;

  nsresult ProcessValue(mozIStorageValueArray* aValues, int32_t aIndex,
                        UpdateType aUpdateType);

  nsresult CreateJournals();

  nsresult RemoveJournals(const nsTArray<int64_t>& aJournals);
};

class DatabaseConnection::UpdateRefcountFunction::FileInfoEntry final {
  SafeRefPtr<FileInfo> mFileInfo;
  int32_t mDelta;
  int32_t mSavepointDelta;

 public:
  explicit FileInfoEntry(SafeRefPtr<FileInfo> aFileInfo)
      : mFileInfo(std::move(aFileInfo)), mDelta(0), mSavepointDelta(0) {
    MOZ_COUNT_CTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry);
  }

  void IncDeltas(bool aUpdateSavepointDelta) {
    ++mDelta;
    if (aUpdateSavepointDelta) {
      ++mSavepointDelta;
    }
  }
  void DecDeltas(bool aUpdateSavepointDelta) {
    --mDelta;
    if (aUpdateSavepointDelta) {
      --mSavepointDelta;
    }
  }
  void DecBySavepointDelta() { mDelta -= mSavepointDelta; }
  SafeRefPtr<FileInfo> ReleaseFileInfo() { return std::move(mFileInfo); }
  void MaybeUpdateDBRefs() {
    if (mDelta) {
      mFileInfo->UpdateDBRefs(mDelta);
    }
  }

  int32_t Delta() const { return mDelta; }
  int32_t SavepointDelta() const { return mSavepointDelta; }

  ~FileInfoEntry() {
    MOZ_COUNT_DTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry);
  }
};

class ConnectionPool final {
 public:
  class FinishCallback;

 private:
  class ConnectionRunnable;
  class CloseConnectionRunnable;
  struct DatabaseInfo;
  struct DatabasesCompleteCallback;
  class FinishCallbackWrapper;
  class IdleConnectionRunnable;

  class ThreadRunnable;
  class TransactionInfo;
  struct TransactionInfoPair;

  struct IdleResource {
    TimeStamp mIdleTime;

    IdleResource(const IdleResource& aOther) = delete;
    IdleResource(IdleResource&& aOther) noexcept
        : IdleResource(aOther.mIdleTime) {}
    IdleResource& operator=(const IdleResource& aOther) = delete;
    IdleResource& operator=(IdleResource&& aOther) = delete;

   protected:
    explicit IdleResource(const TimeStamp& aIdleTime);

    ~IdleResource();
  };

  struct IdleDatabaseInfo final : public IdleResource {
    InitializedOnce<const NotNull<DatabaseInfo*>> mDatabaseInfo;

   public:
    explicit IdleDatabaseInfo(DatabaseInfo& aDatabaseInfo);

    IdleDatabaseInfo(const IdleDatabaseInfo& aOther) = delete;
    IdleDatabaseInfo(IdleDatabaseInfo&& aOther) noexcept
        : IdleResource(std::move(aOther)),
          mDatabaseInfo{std::move(aOther.mDatabaseInfo)} {
      MOZ_ASSERT(mDatabaseInfo);

      MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo);
    }
    IdleDatabaseInfo& operator=(const IdleDatabaseInfo& aOther) = delete;
    IdleDatabaseInfo& operator=(IdleDatabaseInfo&& aOther) = delete;

    ~IdleDatabaseInfo();

    bool operator==(const IdleDatabaseInfo& aOther) const {
      return *mDatabaseInfo == *aOther.mDatabaseInfo;
    }

    bool operator==(const DatabaseInfo* aDatabaseInfo) const {
      return *mDatabaseInfo == aDatabaseInfo;
    }

    bool operator<(const IdleDatabaseInfo& aOther) const {
      return mIdleTime < aOther.mIdleTime;
    }
  };

  class ThreadInfo {
   public:
    ThreadInfo();

    ThreadInfo(nsCOMPtr<nsIThread> aThread, RefPtr<ThreadRunnable> aRunnable)
        : mThread{std::move(aThread)}, mRunnable{std::move(aRunnable)} {
      AssertIsOnBackgroundThread();
      AssertValid();

      MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo);
    }

    ThreadInfo(const ThreadInfo& aOther) = delete;
    ThreadInfo& operator=(const ThreadInfo& aOther) = delete;

    ThreadInfo(ThreadInfo&& aOther) noexcept;
    ThreadInfo& operator=(ThreadInfo&& aOther) = default;

    bool IsValid() const {
      const bool res = mThread;
      if (res) {
        AssertValid();
      } else {
        AssertEmpty();
      }
      return res;
    }

    void AssertValid() const {
      MOZ_ASSERT(mThread);
      MOZ_ASSERT(mRunnable);
    }

    void AssertEmpty() const {
      MOZ_ASSERT(!mThread);
      MOZ_ASSERT(!mRunnable);
    }

    nsIThread& ThreadRef() {
      AssertValid();
      return *mThread;
    }

    std::tuple<nsCOMPtr<nsIThread>, RefPtr<ThreadRunnable>> Forget() {
      AssertValid();

      return {std::move(mThread), std::move(mRunnable)};
    }

    ~ThreadInfo();

    bool operator==(const ThreadInfo& aOther) const {
      return mThread == aOther.mThread && mRunnable == aOther.mRunnable;
    }

   private:
    nsCOMPtr<nsIThread> mThread;
    RefPtr<ThreadRunnable> mRunnable;
  };

  struct IdleThreadInfo final : public IdleResource {
    ThreadInfo mThreadInfo;

    explicit IdleThreadInfo(ThreadInfo aThreadInfo);

    IdleThreadInfo(const IdleThreadInfo& aOther) = delete;
    IdleThreadInfo(IdleThreadInfo&& aOther) noexcept
        : IdleResource(std::move(aOther)),
          mThreadInfo(std::move(aOther.mThreadInfo)) {
      AssertIsOnBackgroundThread();
      mThreadInfo.AssertValid();

      MOZ_COUNT_CTOR(ConnectionPool::IdleThreadInfo);
    }
    IdleThreadInfo& operator=(const IdleThreadInfo& aOther) = delete;
    IdleThreadInfo& operator=(IdleThreadInfo&& aOther) = delete;

    ~IdleThreadInfo();

    bool operator==(const IdleThreadInfo& aOther) const {
      return mThreadInfo == aOther.mThreadInfo;
    }

    bool operator<(const IdleThreadInfo& aOther) const {
      return mIdleTime < aOther.mIdleTime;
    }
  };

  // This mutex guards mDatabases, see below.
  Mutex mDatabasesMutex;

  nsTArray<IdleThreadInfo> mIdleThreads;
  nsTArray<IdleDatabaseInfo> mIdleDatabases;
  nsTArray<NotNull<DatabaseInfo*>> mDatabasesPerformingIdleMaintenance;
  nsCOMPtr<nsITimer> mIdleTimer;
  TimeStamp mTargetIdleTime;

  // Only modifed on the owning thread, but read on multiple threads. Therefore
  // all modifications and all reads off the owning thread must be protected by
  // mDatabasesMutex.
  nsClassHashtable<nsCStringHashKey, DatabaseInfo> mDatabases;

  nsClassHashtable<nsUint64HashKey, TransactionInfo> mTransactions;
  nsTArray<NotNull<TransactionInfo*>> mQueuedTransactions;

  nsTArray<UniquePtr<DatabasesCompleteCallback>> mCompleteCallbacks;

  uint64_t mNextTransactionId;
  uint32_t mTotalThreadCount;
  FlippedOnce<false> mShutdownRequested;
  FlippedOnce<false> mShutdownComplete;

 public:
  ConnectionPool();

  void AssertIsOnOwningThread() const {
    NS_ASSERT_OWNINGTHREAD(ConnectionPool);
  }

  Result<RefPtr<DatabaseConnection>, nsresult> GetOrCreateConnection(
      const Database& aDatabase);

  uint64_t Start(const nsID& aBackgroundChildLoggingId,
                 const nsACString& aDatabaseId, int64_t aLoggingSerialNumber,
                 const nsTArray<nsString>& aObjectStoreNames,
                 bool aIsWriteTransaction,
                 TransactionDatabaseOperationBase* aTransactionOp);

  void Dispatch(uint64_t aTransactionId, nsIRunnable* aRunnable);

  void Finish(uint64_t aTransactionId, FinishCallback* aCallback);

  void CloseDatabaseWhenIdle(const nsACString& aDatabaseId) {
    Unused << CloseDatabaseWhenIdleInternal(aDatabaseId);
  }

  void WaitForDatabasesToComplete(nsTArray<nsCString>&& aDatabaseIds,
                                  nsIRunnable* aCallback);

  void Shutdown();

  NS_INLINE_DECL_REFCOUNTING(ConnectionPool)

 private:
  ~ConnectionPool();

  static void IdleTimerCallback(nsITimer* aTimer, void* aClosure);

  void Cleanup();

  void AdjustIdleTimer();

  void CancelIdleTimer();

  void ShutdownThread(ThreadInfo aThreadInfo);

  void CloseIdleDatabases();

  void ShutdownIdleThreads();

  bool ScheduleTransaction(TransactionInfo& aTransactionInfo,
                           bool aFromQueuedTransactions);

  void NoteFinishedTransaction(uint64_t aTransactionId);

  void ScheduleQueuedTransactions(ThreadInfo aThreadInfo);

  void NoteIdleDatabase(DatabaseInfo& aDatabaseInfo);

  void NoteClosedDatabase(DatabaseInfo& aDatabaseInfo);

  bool MaybeFireCallback(DatabasesCompleteCallback* aCallback);

  void PerformIdleDatabaseMaintenance(DatabaseInfo& aDatabaseInfo);

  void CloseDatabase(DatabaseInfo& aDatabaseInfo);

  bool CloseDatabaseWhenIdleInternal(const nsACString& aDatabaseId);
};

class ConnectionPool::ConnectionRunnable : public Runnable {
 protected:
  DatabaseInfo& mDatabaseInfo;
  nsCOMPtr<nsIEventTarget> mOwningEventTarget;

  explicit ConnectionRunnable(DatabaseInfo& aDatabaseInfo);

  ~ConnectionRunnable() override = default;
};

class ConnectionPool::IdleConnectionRunnable final : public ConnectionRunnable {
  const bool mNeedsCheckpoint;

 public:
  IdleConnectionRunnable(DatabaseInfo& aDatabaseInfo, bool aNeedsCheckpoint)
      : ConnectionRunnable(aDatabaseInfo), mNeedsCheckpoint(aNeedsCheckpoint) {}

  NS_INLINE_DECL_REFCOUNTING_INHERITED(IdleConnectionRunnable,
                                       ConnectionRunnable)

 private:
  ~IdleConnectionRunnable() override = default;

  NS_DECL_NSIRUNNABLE
};

class ConnectionPool::CloseConnectionRunnable final
    : public ConnectionRunnable {
 public:
  explicit CloseConnectionRunnable(DatabaseInfo& aDatabaseInfo)
      : ConnectionRunnable(aDatabaseInfo) {}

  NS_INLINE_DECL_REFCOUNTING_INHERITED(CloseConnectionRunnable,
                                       ConnectionRunnable)

 private:
  ~CloseConnectionRunnable() override = default;

  NS_DECL_NSIRUNNABLE
};

struct ConnectionPool::DatabaseInfo final {
  friend class mozilla::DefaultDelete<DatabaseInfo>;

  RefPtr<ConnectionPool> mConnectionPool;
  const nsCString mDatabaseId;
  RefPtr<DatabaseConnection> mConnection;
  nsClassHashtable<nsStringHashKey, TransactionInfoPair> mBlockingTransactions;
  nsTArray<NotNull<TransactionInfo*>> mTransactionsScheduledDuringClose;
  nsTArray<NotNull<TransactionInfo*>> mScheduledWriteTransactions;
  Maybe<TransactionInfo&> mRunningWriteTransaction;
  ThreadInfo mThreadInfo;
  uint32_t mReadTransactionCount;
  uint32_t mWriteTransactionCount;
  bool mNeedsCheckpoint;
  bool mIdle;
  FlippedOnce<false> mCloseOnIdle;
  bool mClosing;

#ifdef DEBUG
  PRThread* mDEBUGConnectionThread;
#endif

  DatabaseInfo(ConnectionPool* aConnectionPool, const nsACString& aDatabaseId);

  void AssertIsOnConnectionThread() const {
    MOZ_ASSERT(mDEBUGConnectionThread);
    MOZ_ASSERT(PR_GetCurrentThread() == mDEBUGConnectionThread);
  }

  uint64_t TotalTransactionCount() const {
    return mReadTransactionCount + mWriteTransactionCount;
  }

 private:
  ~DatabaseInfo();

  DatabaseInfo(const DatabaseInfo&) = delete;
  DatabaseInfo& operator=(const DatabaseInfo&) = delete;
};

struct ConnectionPool::DatabasesCompleteCallback final {
  friend class DefaultDelete<DatabasesCompleteCallback>;

  nsTArray<nsCString> mDatabaseIds;
  nsCOMPtr<nsIRunnable> mCallback;

  DatabasesCompleteCallback(nsTArray<nsCString>&& aDatabaseIds,
                            nsIRunnable* aCallback);

 private:
  ~DatabasesCompleteCallback();
};

class NS_NO_VTABLE ConnectionPool::FinishCallback : public nsIRunnable {
 public:
  // Called on the owning thread before any additional transactions are
  // unblocked.
  virtual void TransactionFinishedBeforeUnblock() = 0;

  // Called on the owning thread after additional transactions may have been
  // unblocked.
  virtual void TransactionFinishedAfterUnblock() = 0;

 protected:
  FinishCallback() = default;

  virtual ~FinishCallback() = default;
};

class ConnectionPool::FinishCallbackWrapper final : public Runnable {
  RefPtr<ConnectionPool> mConnectionPool;
  RefPtr<FinishCallback> mCallback;
  nsCOMPtr<nsIEventTarget> mOwningEventTarget;
  uint64_t mTransactionId;
  bool mHasRunOnce;

 public:
  FinishCallbackWrapper(ConnectionPool* aConnectionPool,
                        uint64_t aTransactionId, FinishCallback* aCallback);

  NS_INLINE_DECL_REFCOUNTING_INHERITED(FinishCallbackWrapper, Runnable)

 private:
  ~FinishCallbackWrapper() override;

  NS_DECL_NSIRUNNABLE
};

class ConnectionPool::ThreadRunnable final : public Runnable {
  // Only touched on the background thread.
  static uint32_t sNextSerialNumber;

  // Set at construction for logging.
  const uint32_t mSerialNumber;

  // These two values are only modified on the connection thread.
  FlippedOnce<true> mFirstRun;
  FlippedOnce<true> mContinueRunning;

 public:
  ThreadRunnable();

  NS_INLINE_DECL_REFCOUNTING_INHERITED(ThreadRunnable, Runnable)

  uint32_t SerialNumber() const { return mSerialNumber; }

  nsCString GetThreadName() const {
    return nsPrintfCString("IndexedDB #%" PRIu32, mSerialNumber);
  }

 private:
  ~ThreadRunnable() override;

  NS_DECL_NSIRUNNABLE
};

class ConnectionPool::TransactionInfo final {
  friend class mozilla::DefaultDelete<TransactionInfo>;

  nsTHashtable<nsPtrHashKey<TransactionInfo>> mBlocking;
  nsTArray<NotNull<TransactionInfo*>> mBlockingOrdered;

 public:
  DatabaseInfo& mDatabaseInfo;
  const nsID mBackgroundChildLoggingId;
  const nsCString mDatabaseId;
  const uint64_t mTransactionId;
  const int64_t mLoggingSerialNumber;
  const nsTArray<nsString> mObjectStoreNames;
  nsTHashtable<nsPtrHashKey<TransactionInfo>> mBlockedOn;
  nsTArray<nsCOMPtr<nsIRunnable>> mQueuedRunnables;
  const bool mIsWriteTransaction;
  bool mRunning;

#ifdef DEBUG
  FlippedOnce<false> mFinished;
#endif

  TransactionInfo(DatabaseInfo& aDatabaseInfo,
                  const nsID& aBackgroundChildLoggingId,
                  const nsACString& aDatabaseId, uint64_t aTransactionId,
                  int64_t aLoggingSerialNumber,
                  const nsTArray<nsString>& aObjectStoreNames,
                  bool aIsWriteTransaction,
                  TransactionDatabaseOperationBase* aTransactionOp);

  void AddBlockingTransaction(TransactionInfo& aTransactionInfo);

  void RemoveBlockingTransactions();

 private:
  ~TransactionInfo();

  void MaybeUnblock(TransactionInfo& aTransactionInfo);
};

struct ConnectionPool::TransactionInfoPair final {
  // Multiple reading transactions can block future writes.
  nsTArray<NotNull<TransactionInfo*>> mLastBlockingWrites;
  // But only a single writing transaction can block future reads.
  Maybe<TransactionInfo&> mLastBlockingReads;

#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING)
  TransactionInfoPair();
  ~TransactionInfoPair();
#endif
};

/*******************************************************************************
 * Actor class declarations
 ******************************************************************************/

template <IDBCursorType CursorType>
class CommonOpenOpHelper;
template <IDBCursorType CursorType>
class IndexOpenOpHelper;
template <IDBCursorType CursorType>
class ObjectStoreOpenOpHelper;
template <IDBCursorType CursorType>
class OpenOpHelper;

class DatabaseOperationBase : public Runnable,
                              public mozIStorageProgressHandler {
  template <IDBCursorType CursorType>
  friend class OpenOpHelper;

 protected:
  class AutoSetProgressHandler;

  typedef nsDataHashtable<nsUint64HashKey, bool> UniqueIndexTable;

  const nsCOMPtr<nsIEventTarget> mOwningEventTarget;
  const nsID mBackgroundChildLoggingId;
  const uint64_t mLoggingSerialNumber;

 private:
  nsresult mResultCode = NS_OK;
  Atomic<bool> mOperationMayProceed;
  FlippedOnce<false> mActorDestroyed;

 public:
  NS_DECL_ISUPPORTS_INHERITED

  bool IsOnOwningThread() const {
    MOZ_ASSERT(mOwningEventTarget);

    bool current;
    return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(&current)) &&
           current;
  }

  void AssertIsOnOwningThread() const {
    MOZ_ASSERT(IsOnBackgroundThread());
    MOZ_ASSERT(IsOnOwningThread());
  }

  void NoteActorDestroyed() {
    AssertIsOnOwningThread();

    mActorDestroyed.Flip();
    mOperationMayProceed = false;
  }

  bool IsActorDestroyed() const {
    AssertIsOnOwningThread();

    return mActorDestroyed;
  }

  // May be called on any thread, but you should call IsActorDestroyed() if
  // you know you're on the background thread because it is slightly faster.
  bool OperationMayProceed() const { return mOperationMayProceed; }

  const nsID& BackgroundChildLoggingId() const {
    return mBackgroundChildLoggingId;
  }

  uint64_t LoggingSerialNumber() const { return mLoggingSerialNumber; }

  nsresult ResultCode() const { return mResultCode; }

  void SetFailureCode(nsresult aFailureCode) {
    MOZ_ASSERT(NS_SUCCEEDED(mResultCode));
    OverrideFailureCode(aFailureCode);
  }

  void SetFailureCodeIfUnset(nsresult aFailureCode) {
    if (NS_SUCCEEDED(mResultCode)) {
      OverrideFailureCode(aFailureCode);
    }
  }

  bool HasFailed() const { return NS_FAILED(mResultCode); }

 protected:
  DatabaseOperationBase(const nsID& aBackgroundChildLoggingId,
                        uint64_t aLoggingSerialNumber)
      : Runnable("dom::indexedDB::DatabaseOperationBase"),
        mOwningEventTarget(GetCurrentEventTarget()),
        mBackgroundChildLoggingId(aBackgroundChildLoggingId),
        mLoggingSerialNumber(aLoggingSerialNumber),
        mOperationMayProceed(true) {
    AssertIsOnOwningThread();
  }

  ~DatabaseOperationBase() override { MOZ_ASSERT(mActorDestroyed); }

  void OverrideFailureCode(nsresult aFailureCode) {
    MOZ_ASSERT(NS_FAILED(aFailureCode));

    mResultCode = aFailureCode;
  }

  static nsAutoCString MaybeGetBindingClauseForKeyRange(
      const Maybe<SerializedKeyRange>& aOptionalKeyRange,
      const nsACString& aKeyColumnName);

  static nsAutoCString GetBindingClauseForKeyRange(
      const SerializedKeyRange& aKeyRange, const nsACString& aKeyColumnName);

  static uint64_t ReinterpretDoubleAsUInt64(double aDouble);

  static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange,
                                          mozIStorageStatement* aStatement);

  static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange,
                                          mozIStorageStatement* aStatement,
                                          const nsCString& aLocale);

  static Result<IndexDataValuesAutoArray, nsresult>
  IndexDataValuesFromUpdateInfos(const nsTArray<IndexUpdateInfo>& aUpdateInfos,
                                 const UniqueIndexTable& aUniqueIndexTable);

  static nsresult InsertIndexTableRows(
      DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId,
      const Key& aObjectStoreKey, const nsTArray<IndexDataValue>& aIndexValues);

  static nsresult DeleteIndexDataTableRows(
      DatabaseConnection* aConnection, const Key& aObjectStoreKey,
      const nsTArray<IndexDataValue>& aIndexValues);

  static nsresult DeleteObjectStoreDataTableRowsWithIndexes(
      DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId,
      const Maybe<SerializedKeyRange>& aKeyRange);

  static nsresult UpdateIndexValues(
      DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId,
      const Key& aObjectStoreKey, const nsTArray<IndexDataValue>& aIndexValues);

  static Result<bool, nsresult> ObjectStoreHasIndexes(
      DatabaseConnection& aConnection, IndexOrObjectStoreId aObjectStoreId);

 private:
  template <typename KeyTransformation>
  static nsresult MaybeBindKeyToStatement(
      const Key& aKey, mozIStorageStatement* aStatement,
      const nsCString& aParameterName,
      const KeyTransformation& aKeyTransformation);

  template <typename KeyTransformation>
  static nsresult BindTransformedKeyRangeToStatement(
      const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement,
      const KeyTransformation& aKeyTransformation);

  // Not to be overridden by subclasses.
  NS_DECL_MOZISTORAGEPROGRESSHANDLER
};

class MOZ_STACK_CLASS DatabaseOperationBase::AutoSetProgressHandler final {
  Maybe<mozIStorageConnection&> mConnection;
#ifdef DEBUG
  DatabaseOperationBase* mDEBUGDatabaseOp;
#endif

 public:
  AutoSetProgressHandler();

  ~AutoSetProgressHandler();

  nsresult Register(mozIStorageConnection& aConnection,
                    DatabaseOperationBase* aDatabaseOp);

  void Unregister();
};

class TransactionDatabaseOperationBase : public DatabaseOperationBase {
  enum class InternalState {
    Initial,
    DatabaseWork,
    SendingPreprocess,
    WaitingForContinue,
    SendingResults,
    Completed
  };

  InitializedOnce<const NotNull<SafeRefPtr<TransactionBase>>> mTransaction;
  InternalState mInternalState = InternalState::Initial;
  bool mWaitingForContinue = false;
  const bool mTransactionIsAborted;

 protected:
  const int64_t mTransactionLoggingSerialNumber;

#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
 protected:
  // A check only enables when the diagnostic assert turns on. It assumes the
  // mUpdateRefcountFunction is a nullptr because the previous
  // StartTransactionOp failed on the connection thread and the next write
  // operation (e.g. ObjectstoreAddOrPutRequestOp) doesn't have enough time to
  // catch up the failure information.
  bool mAssumingPreviousOperationFail = false;
#endif

 public:
  void AssertIsOnConnectionThread() const
#ifdef DEBUG
      ;
#else
  {
  }
#endif

  uint64_t StartOnConnectionPool(const nsID& aBackgroundChildLoggingId,
                                 const nsACString& aDatabaseId,
                                 int64_t aLoggingSerialNumber,
                                 const nsTArray<nsString>& aObjectStoreNames,
                                 bool aIsWriteTransaction);

  void DispatchToConnectionPool();

  TransactionBase& Transaction() { return **mTransaction; }

  const TransactionBase& Transaction() const { return **mTransaction; }

  bool IsWaitingForContinue() const {
    AssertIsOnOwningThread();

    return mWaitingForContinue;
  }

  void NoteContinueReceived();

  int64_t TransactionLoggingSerialNumber() const {
    return mTransactionLoggingSerialNumber;
  }

  // May be overridden by subclasses if they need to perform work on the
  // background thread before being dispatched. Returning false will kill the
  // child actors and prevent dispatch.
  virtual bool Init(TransactionBase& aTransaction);

  // This callback will be called on the background thread before releasing the
  // final reference to this request object. Subclasses may perform any
  // additional cleanup here but must always call the base class implementation.
  virtual void Cleanup();

 protected:
  explicit TransactionDatabaseOperationBase(
      SafeRefPtr<TransactionBase> aTransaction);

  TransactionDatabaseOperationBase(SafeRefPtr<TransactionBase> aTransaction,
                                   uint64_t aLoggingSerialNumber);

  ~TransactionDatabaseOperationBase() override;

  virtual void RunOnConnectionThread();

  // Must be overridden in subclasses. Called on the target thread to allow the
  // subclass to perform necessary database or file operations. A successful
  // return value will trigger a SendSuccessResult callback on the background
  // thread while a failure value will trigger a SendFailureResult callback.
  virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) = 0;

  // May be overriden in subclasses. Called on the background thread to decide
  // if the subclass needs to send any preprocess info to the child actor.
  virtual bool HasPreprocessInfo();

  // May be overriden in subclasses. Called on the background thread to allow
  // the subclass to serialize its preprocess info and send it to the child
  // actor. A successful return value will trigger a wait for a
  // NoteContinueReceived callback on the background thread while a failure
  // value will trigger a SendFailureResult callback.
  virtual nsresult SendPreprocessInfo();

  // Must be overridden in subclasses. Called on the background thread to allow
  // the subclass to serialize its results and send them to the child actor. A
  // failed return value will trigger a SendFailureResult callback.
  virtual nsresult SendSuccessResult() = 0;

  // Must be overridden in subclasses. Called on the background thread to allow
  // the subclass to send its failure code. Returning false will cause the
  // transaction to be aborted with aResultCode. Returning true will not cause
  // the transaction to be aborted.
  virtual bool SendFailureResult(nsresult aResultCode) = 0;

#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
  auto MakeAutoSavepointCleanupHandler(DatabaseConnection& aConnection) {
    return [this, &aConnection](const auto) {
      if (!aConnection.GetUpdateRefcountFunction()) {
        mAssumingPreviousOperationFail = true;
      }
    };
  }
#endif

 private:
  void SendToConnectionPool();

  void SendPreprocess();

  void SendResults();

  void SendPreprocessInfoOrResults(bool aSendPreprocessInfo);

  // Not to be overridden by subclasses.
  NS_DECL_NSIRUNNABLE
};

class Factory final : public PBackgroundIDBFactoryParent,
                      public AtomicSafeRefCounted<Factory> {
  RefPtr<DatabaseLoggingInfo> mLoggingInfo;

#ifdef DEBUG
  bool mActorDestroyed;
#endif

  // Reference counted.
  ~Factory() override;

 public:
  [[nodiscard]] static SafeRefPtr<Factory> Create(
      const LoggingInfo& aLoggingInfo);

  DatabaseLoggingInfo* GetLoggingInfo() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mLoggingInfo);

    return mLoggingInfo;
  }

  MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::Factory)
  MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(Factory, AtomicSafeRefCounted)

  // Only constructed in Create().
  explicit Factory(RefPtr<DatabaseLoggingInfo> aLoggingInfo);

  // IPDL methods are only called by IPDL.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  PBackgroundIDBFactoryRequestParent* AllocPBackgroundIDBFactoryRequestParent(
      const FactoryRequestParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBFactoryRequestConstructor(
      PBackgroundIDBFactoryRequestParent* aActor,
      const FactoryRequestParams& aParams) override;

  bool DeallocPBackgroundIDBFactoryRequestParent(
      PBackgroundIDBFactoryRequestParent* aActor) override;

  PBackgroundIDBDatabaseParent* AllocPBackgroundIDBDatabaseParent(
      const DatabaseSpec& aSpec,
      PBackgroundIDBFactoryRequestParent* aRequest) override;

  bool DeallocPBackgroundIDBDatabaseParent(
      PBackgroundIDBDatabaseParent* aActor) override;
};

class WaitForTransactionsHelper final : public Runnable {
  const nsCString mDatabaseId;
  nsCOMPtr<nsIRunnable> mCallback;

  enum class State {
    Initial = 0,
    WaitingForTransactions,
    WaitingForFileHandles,
    Complete
  } mState;

 public:
  WaitForTransactionsHelper(const nsCString& aDatabaseId,
                            nsIRunnable* aCallback)
      : Runnable("dom::indexedDB::WaitForTransactionsHelper"),
        mDatabaseId(aDatabaseId),
        mCallback(aCallback),
        mState(State::Initial) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(!aDatabaseId.IsEmpty());
    MOZ_ASSERT(aCallback);
  }

  void WaitForTransactions();

  NS_INLINE_DECL_REFCOUNTING_INHERITED(WaitForTransactionsHelper, Runnable)

 private:
  ~WaitForTransactionsHelper() override {
    MOZ_ASSERT(!mCallback);
    MOZ_ASSERT(mState == State::Complete);
  }

  void MaybeWaitForTransactions();

  void MaybeWaitForFileHandles();

  void CallCallback();

  NS_DECL_NSIRUNNABLE
};

class Database final
    : public PBackgroundIDBDatabaseParent,
      public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>>,
      public AtomicSafeRefCounted<Database> {
  friend class VersionChangeTransaction;

  class StartTransactionOp;
  class UnmapBlobCallback;

 private:
  SafeRefPtr<Factory> mFactory;
  SafeRefPtr<FullDatabaseMetadata> mMetadata;
  SafeRefPtr<FileManager> mFileManager;
  RefPtr<DirectoryLock> mDirectoryLock;
  nsTHashtable<nsPtrHashKey<TransactionBase>> mTransactions;
  nsTHashtable<nsPtrHashKey<MutableFile>> mMutableFiles;
  nsRefPtrHashtable<nsIDHashKey, FileInfo> mMappedBlobs;
  RefPtr<DatabaseConnection> mConnection;
  const PrincipalInfo mPrincipalInfo;
  const Maybe<ContentParentId> mOptionalContentParentId;
  const quota::GroupAndOrigin mGroupAndOrigin;
  const nsCString mId;
  const nsString mFilePath;
  const Maybe<const CipherKey> mKey;
  uint32_t mActiveMutableFileCount;
  uint32_t mPendingCreateFileOpCount;
  int64_t mDirectoryLockId;
  const uint32_t mTelemetryId;
  const PersistenceType mPersistenceType;
  const bool mFileHandleDisabled;
  const bool mChromeWriteAccessAllowed;
  const bool mInPrivateBrowsing;
  FlippedOnce<false> mClosed;
  FlippedOnce<false> mInvalidated;
  FlippedOnce<false> mActorWasAlive;
  FlippedOnce<false> mActorDestroyed;
  nsCOMPtr<nsIEventTarget> mBackgroundThread;
#ifdef DEBUG
  bool mAllBlobsUnmapped;
#endif

 public:
  // Created by OpenDatabaseOp.
  Database(SafeRefPtr<Factory> aFactory, const PrincipalInfo& aPrincipalInfo,
           const Maybe<ContentParentId>& aOptionalContentParentId,
           const quota::GroupAndOrigin& aGroupAndOrigin, uint32_t aTelemetryId,
           SafeRefPtr<FullDatabaseMetadata> aMetadata,
           SafeRefPtr<FileManager> aFileManager,
           RefPtr<DirectoryLock> aDirectoryLock, bool aFileHandleDisabled,
           bool aChromeWriteAccessAllowed, bool aInPrivateBrowsing,
           const Maybe<const CipherKey>& aMaybeKey);

  void AssertIsOnConnectionThread() const {
#ifdef DEBUG
    if (mConnection) {
      MOZ_ASSERT(mConnection);
      mConnection->AssertIsOnConnectionThread();
    } else {
      MOZ_ASSERT(!NS_IsMainThread());
      MOZ_ASSERT(!IsOnBackgroundThread());
      MOZ_ASSERT(mInvalidated);
    }
#endif
  }

  MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::Database)

  void Invalidate();

  bool IsOwnedByProcess(ContentParentId aContentParentId) const {
    return mOptionalContentParentId &&
           mOptionalContentParentId.value() == aContentParentId;
  }

  const quota::GroupAndOrigin& GroupAndOrigin() const {
    return mGroupAndOrigin;
  }

  const nsCString& Id() const { return mId; }

  int64_t DirectoryLockId() const { return mDirectoryLockId; }

  uint32_t TelemetryId() const { return mTelemetryId; }

  PersistenceType Type() const { return mPersistenceType; }

  const nsString& FilePath() const { return mFilePath; }

  FileManager& GetFileManager() const { return *mFileManager; }

  MovingNotNull<SafeRefPtr<FileManager>> GetFileManagerPtr() const {
    return WrapMovingNotNull(mFileManager.clonePtr());
  }

  const FullDatabaseMetadata& Metadata() const {
    MOZ_ASSERT(mMetadata);
    return *mMetadata;
  }

  SafeRefPtr<FullDatabaseMetadata> MetadataPtr() const {
    MOZ_ASSERT(mMetadata);
    return mMetadata.clonePtr();
  }

  PBackgroundParent* GetBackgroundParent() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(!IsActorDestroyed());

    return Manager()->Manager();
  }

  DatabaseLoggingInfo* GetLoggingInfo() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mFactory);

    return mFactory->GetLoggingInfo();
  }

  bool RegisterTransaction(TransactionBase& aTransaction);

  void UnregisterTransaction(TransactionBase& aTransaction);

  bool IsFileHandleDisabled() const { return mFileHandleDisabled; }

  bool RegisterMutableFile(MutableFile* aMutableFile);

  void UnregisterMutableFile(MutableFile* aMutableFile);

  void NoteActiveMutableFile();

  void NoteInactiveMutableFile();

  void NotePendingCreateFileOp();

  void NoteCompletedCreateFileOp();

  void SetActorAlive();

  void MapBlob(const IPCBlob& aIPCBlob, SafeRefPtr<FileInfo> aFileInfo);

  bool IsActorAlive() const {
    AssertIsOnBackgroundThread();

    return mActorWasAlive && !mActorDestroyed;
  }

  bool IsActorDestroyed() const {
    AssertIsOnBackgroundThread();

    return mActorWasAlive && mActorDestroyed;
  }

  bool IsClosed() const {
    AssertIsOnBackgroundThread();

    return mClosed;
  }

  bool IsInvalidated() const {
    AssertIsOnBackgroundThread();

    return mInvalidated;
  }

  nsresult EnsureConnection();

  DatabaseConnection* GetConnection() const {
#ifdef DEBUG
    if (mConnection) {
      mConnection->AssertIsOnConnectionThread();
    }
#endif

    return mConnection;
  }

  void Stringify(nsACString& aResult) const;

  bool IsInPrivateBrowsing() const {
    AssertIsOnBackgroundThread();

    return mInPrivateBrowsing;
  }

  const Maybe<const CipherKey>& MaybeKeyRef() const {
    // This can be called on any thread, as it is const.
    MOZ_ASSERT(mKey.isSome() == mInPrivateBrowsing);
    return mKey;
  }

  ~Database() override {
    MOZ_ASSERT(mClosed);
    MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed);

    NS_ProxyRelease("ReleaseIDBFactory", mBackgroundThread.get(),
                    mFactory.forget());
  }

 private:
  [[nodiscard]] SafeRefPtr<FileInfo> GetBlob(const IPCBlob& aID);

  void UnmapBlob(const nsID& aID);

  void UnmapAllBlobs();

  bool CloseInternal();

  void MaybeCloseConnection();

  void ConnectionClosedCallback();

  void CleanupMetadata();

  bool VerifyRequestParams(const DatabaseRequestParams& aParams) const;

  // IPDL methods are only called by IPDL.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  PBackgroundIDBDatabaseFileParent* AllocPBackgroundIDBDatabaseFileParent(
      const IPCBlob& aIPCBlob) override;

  bool DeallocPBackgroundIDBDatabaseFileParent(
      PBackgroundIDBDatabaseFileParent* aActor) override;

  PBackgroundIDBDatabaseRequestParent* AllocPBackgroundIDBDatabaseRequestParent(
      const DatabaseRequestParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBDatabaseRequestConstructor(
      PBackgroundIDBDatabaseRequestParent* aActor,
      const DatabaseRequestParams& aParams) override;

  bool DeallocPBackgroundIDBDatabaseRequestParent(
      PBackgroundIDBDatabaseRequestParent* aActor) override;

  already_AddRefed<PBackgroundIDBTransactionParent>
  AllocPBackgroundIDBTransactionParent(
      const nsTArray<nsString>& aObjectStoreNames, const Mode& aMode) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBTransactionConstructor(
      PBackgroundIDBTransactionParent* aActor,
      nsTArray<nsString>&& aObjectStoreNames, const Mode& aMode) override;

  PBackgroundMutableFileParent* AllocPBackgroundMutableFileParent(
      const nsString& aName, const nsString& aType) override;

  bool DeallocPBackgroundMutableFileParent(
      PBackgroundMutableFileParent* aActor) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  mozilla::ipc::IPCResult RecvBlocked() override;

  mozilla::ipc::IPCResult RecvClose() override;

  template <typename T>
  static bool InvalidateAll(const nsTHashtable<nsPtrHashKey<T>>& aTable);
};

class Database::StartTransactionOp final
    : public TransactionDatabaseOperationBase {
  friend class Database;

 private:
  explicit StartTransactionOp(SafeRefPtr<TransactionBase> aTransaction)
      : TransactionDatabaseOperationBase(std::move(aTransaction),
                                         /* aLoggingSerialNumber */ 0) {}

  ~StartTransactionOp() override = default;

  void RunOnConnectionThread() override;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  nsresult SendSuccessResult() override;

  bool SendFailureResult(nsresult aResultCode) override;

  void Cleanup() override;
};

class Database::UnmapBlobCallback final
    : public RemoteLazyInputStreamParentCallback {
  SafeRefPtr<Database> mDatabase;

 public:
  explicit UnmapBlobCallback(SafeRefPtr<Database> aDatabase)
      : mDatabase(std::move(aDatabase)) {
    AssertIsOnBackgroundThread();
  }

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Database::UnmapBlobCallback, override)

  void ActorDestroyed(const nsID& aID) override {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mDatabase);

    const SafeRefPtr<Database> database = std::move(mDatabase);
    database->UnmapBlob(aID);
  }

 private:
  ~UnmapBlobCallback() = default;
};

/**
 * In coordination with IDBDatabase's mFileActors weak-map on the child side, a
 * long-lived mapping from a child process's live Blobs to their corresponding
 * FileInfo in our owning database.  Assists in avoiding redundant IPC traffic
 * and disk storage.  This includes both:
 * - Blobs retrieved from this database and sent to the child that do not need
 *   to be written to disk because they already exist on disk in this database's
 *   files directory.
 * - Blobs retrieved from other databases or from anywhere else that will need
 *   to be written to this database's files directory.  In this case we will
 *   hold a reference to its BlobImpl in mBlobImpl until we have successfully
 *   written the Blob to disk.
 *
 * Relevant Blob context: Blobs sent from the parent process to child processes
 * are automatically linked back to their source BlobImpl when the child process
 * references the Blob via IPC. This is done using the internal IPCBlob
 * inputStream actor ID to FileInfo mapping. However, when getting an actor
 * in the child process for sending an in-child-created Blob to the parent
 * process, there is (currently) no Blob machinery to automatically establish
 * and reuse a long-lived Actor.  As a result, without IDB's weak-map
 * cleverness, a memory-backed Blob repeatedly sent from the child to the parent
 * would appear as a different Blob each time, requiring the Blob data to be
 * sent over IPC each time as well as potentially needing to be written to disk
 * each time.
 *
 * This object remains alive as long as there is an active child actor or an
 * ObjectStoreAddOrPutRequestOp::StoredFileInfo for a queued or active add/put
 * op is holding a reference to us.
 */
class DatabaseFile final : public PBackgroundIDBDatabaseFileParent {
  // mBlobImpl's ownership lifecycle:
  // - Initialized on the background thread at creation time.  Then
  //   responsibility is handed off to the connection thread.
  // - Checked and used by the connection thread to generate a stream to write
  //   the blob to disk by an add/put operation.
  // - Cleared on the connection thread once the file has successfully been
  //   written to disk.
  InitializedOnce<const RefPtr<BlobImpl>> mBlobImpl;
  const SafeRefPtr<FileInfo> mFileInfo;

 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::DatabaseFile);

  const FileInfo& GetFileInfo() const {
    AssertIsOnBackgroundThread();

    return *mFileInfo;
  }

  SafeRefPtr<FileInfo> GetFileInfoPtr() const {
    AssertIsOnBackgroundThread();

    return mFileInfo.clonePtr();
  }

  /**
   * If mBlobImpl is non-null (implying the contents of this file have not yet
   * been written to disk), then return an input stream. Otherwise, if mBlobImpl
   * is null (because the contents have been written to disk), returns null.
   */
  [[nodiscard]] nsCOMPtr<nsIInputStream> GetInputStream(ErrorResult& rv) const;

  /**
   * To be called upon successful copying of the stream GetInputStream()
   * returned so that we won't try and redundantly write the file to disk in the
   * future.  This is a separate step from GetInputStream() because
   * the write could fail due to quota errors that happen now but that might
   * not happen in a future attempt.
   */
  void WriteSucceededClearBlobImpl() {
    MOZ_ASSERT(!IsOnBackgroundThread());

    MOZ_ASSERT(*mBlobImpl);
    mBlobImpl.destroy();
  }

 public:
  // Called when sending to the child.
  explicit DatabaseFile(SafeRefPtr<FileInfo> aFileInfo)
      : mBlobImpl{nullptr}, mFileInfo(std::move(aFileInfo)) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mFileInfo);
  }

  // Called when receiving from the child.
  DatabaseFile(RefPtr<BlobImpl> aBlobImpl, SafeRefPtr<FileInfo> aFileInfo)
      : mBlobImpl(std::move(aBlobImpl)), mFileInfo(std::move(aFileInfo)) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(*mBlobImpl);
    MOZ_ASSERT(mFileInfo);
  }

 private:
  ~DatabaseFile() override = default;

  void ActorDestroy(ActorDestroyReason aWhy) override {
    AssertIsOnBackgroundThread();
  }
};

nsCOMPtr<nsIInputStream> DatabaseFile::GetInputStream(ErrorResult& rv) const {
  // We should only be called from our DB connection thread, not the background
  // thread.
  MOZ_ASSERT(!IsOnBackgroundThread());

  // If we were constructed without a BlobImpl, or WriteSucceededClearBlobImpl
  // was already called, return nullptr.
  if (!mBlobImpl || !*mBlobImpl) {
    return nullptr;
  }

  nsCOMPtr<nsIInputStream> inputStream;
  (*mBlobImpl)->CreateInputStream(getter_AddRefs(inputStream), rv);
  if (rv.Failed()) {
    return nullptr;
  }

  return inputStream;
}

class TransactionBase : public AtomicSafeRefCounted<TransactionBase> {
  friend class CursorBase;

  template <IDBCursorType CursorType>
  friend class Cursor;

  class CommitOp;

 protected:
  typedef IDBTransaction::Mode Mode;

 private:
  const SafeRefPtr<Database> mDatabase;
  nsTArray<RefPtr<FullObjectStoreMetadata>>
      mModifiedAutoIncrementObjectStoreMetadataArray;
  LazyInitializedOnceNotNull<const uint64_t> mTransactionId;
  const nsCString mDatabaseId;
  const int64_t mLoggingSerialNumber;
  uint64_t mActiveRequestCount;
  Atomic<bool> mInvalidatedOnAnyThread;
  const Mode mMode;
  FlippedOnce<false> mInitialized;
  FlippedOnce<false> mHasBeenActiveOnConnectionThread;
  FlippedOnce<false> mActorDestroyed;
  FlippedOnce<false> mInvalidated;

 protected:
  nsresult mResultCode;
  FlippedOnce<false> mCommitOrAbortReceived;
  FlippedOnce<false> mCommittedOrAborted;
  FlippedOnce<false> mForceAborted;
  LazyInitializedOnce<const Maybe<int64_t>> mLastRequestBeforeCommit;
  Maybe<int64_t> mLastFailedRequest;

 public:
  void AssertIsOnConnectionThread() const {
    MOZ_ASSERT(mDatabase);
    mDatabase->AssertIsOnConnectionThread();
  }

  bool IsActorDestroyed() const {
    AssertIsOnBackgroundThread();

    return mActorDestroyed;
  }

  // Must be called on the background thread.
  bool IsInvalidated() const {
    MOZ_ASSERT(IsOnBackgroundThread(), "Use IsInvalidatedOnAnyThread()");
    MOZ_ASSERT_IF(mInvalidated, NS_FAILED(mResultCode));

    return mInvalidated;
  }

  // May be called on any thread, but is more expensive than IsInvalidated().
  bool IsInvalidatedOnAnyThread() const { return mInvalidatedOnAnyThread; }

  void Init(const uint64_t aTransactionId) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(aTransactionId);

    mTransactionId.init(aTransactionId);
    mInitialized.Flip();
  }

  void SetActiveOnConnectionThread() {
    AssertIsOnConnectionThread();
    mHasBeenActiveOnConnectionThread.Flip();
  }

  MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::TransactionBase)

  void Abort(nsresult aResultCode, bool aForce);

  uint64_t TransactionId() const { return *mTransactionId; }

  const nsCString& DatabaseId() const { return mDatabaseId; }

  Mode GetMode() const { return mMode; }

  const Database& GetDatabase() const {
    MOZ_ASSERT(mDatabase);

    return *mDatabase;
  }

  Database& GetMutableDatabase() const {
    MOZ_ASSERT(mDatabase);

    return *mDatabase;
  }

  SafeRefPtr<Database> GetDatabasePtr() const {
    MOZ_ASSERT(mDatabase);

    return mDatabase.clonePtr();
  }

  DatabaseLoggingInfo* GetLoggingInfo() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mDatabase);

    return mDatabase->GetLoggingInfo();
  }

  int64_t LoggingSerialNumber() const { return mLoggingSerialNumber; }

  bool IsAborted() const {
    AssertIsOnBackgroundThread();

    return NS_FAILED(mResultCode);
  }

  [[nodiscard]] RefPtr<FullObjectStoreMetadata> GetMetadataForObjectStoreId(
      IndexOrObjectStoreId aObjectStoreId) const;

  [[nodiscard]] RefPtr<FullIndexMetadata> GetMetadataForIndexId(
      FullObjectStoreMetadata* const aObjectStoreMetadata,
      IndexOrObjectStoreId aIndexId) const;

  PBackgroundParent* GetBackgroundParent() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(!IsActorDestroyed());

    return GetDatabase().GetBackgroundParent();
  }

  void NoteModifiedAutoIncrementObjectStore(FullObjectStoreMetadata* aMetadata);

  void ForgetModifiedAutoIncrementObjectStore(
      FullObjectStoreMetadata* aMetadata);

  void NoteActiveRequest();

  void NoteFinishedRequest(int64_t aRequestId, nsresult aResultCode);

  void Invalidate();

  virtual ~TransactionBase();

 protected:
  TransactionBase(SafeRefPtr<Database> aDatabase, Mode aMode);

  void NoteActorDestroyed() {
    AssertIsOnBackgroundThread();

    mActorDestroyed.Flip();
  }

#ifdef DEBUG
  // Only called by VersionChangeTransaction.
  void FakeActorDestroyed() { mActorDestroyed.EnsureFlipped(); }
#endif

  bool RecvCommit(Maybe<int64_t> aLastRequest);

  bool RecvAbort(nsresult aResultCode);

  void MaybeCommitOrAbort() {
    AssertIsOnBackgroundThread();

    // If we've already committed or aborted then there's nothing else to do.
    if (mCommittedOrAborted) {
      return;
    }

    // If there are active requests then we have to wait for those requests to
    // complete (see NoteFinishedRequest).
    if (mActiveRequestCount) {
      return;
    }

    // If we haven't yet received a commit or abort message then there could be
    // additional requests coming so we should wait unless we're being forced to
    // abort.
    if (!mCommitOrAbortReceived && !mForceAborted) {
      return;
    }

    CommitOrAbort();
  }

  PBackgroundIDBRequestParent* AllocRequest(RequestParams&& aParams,
                                            bool aTrustParams);

  bool StartRequest(PBackgroundIDBRequestParent* aActor);

  bool DeallocRequest(PBackgroundIDBRequestParent* aActor);

  already_AddRefed<PBackgroundIDBCursorParent> AllocCursor(
      const OpenCursorParams& aParams, bool aTrustParams);

  bool StartCursor(PBackgroundIDBCursorParent* aActor,
                   const OpenCursorParams& aParams);

  virtual void UpdateMetadata(nsresult aResult) {}

  virtual void SendCompleteNotification(nsresult aResult) = 0;

 private:
  bool VerifyRequestParams(const RequestParams& aParams) const;

  bool VerifyRequestParams(const SerializedKeyRange& aKeyRange) const;

  bool VerifyRequestParams(const ObjectStoreAddPutParams& aParams) const;

  bool VerifyRequestParams(const Maybe<SerializedKeyRange>& aKeyRange) const;

  void CommitOrAbort();
};

class TransactionBase::CommitOp final : public DatabaseOperationBase,
                                        public ConnectionPool::FinishCallback {
  friend class TransactionBase;

  SafeRefPtr<TransactionBase> mTransaction;
  nsresult mResultCode;  ///< TODO: There is also a mResultCode in
                         ///< DatabaseOperationBase. Is there a reason not to
                         ///< use that? At least a more specific name should be
                         ///< given to this one.

 private:
  CommitOp(SafeRefPtr<TransactionBase> aTransaction, nsresult aResultCode);

  ~CommitOp() override = default;

  // Writes new autoIncrement counts to database.
  nsresult WriteAutoIncrementCounts();

  // Updates counts after a database activity has finished.
  void CommitOrRollbackAutoIncrementCounts();

  void AssertForeignKeyConsistency(DatabaseConnection* aConnection)
#ifdef DEBUG
      ;
#else
  {
  }
#endif

  NS_DECL_NSIRUNNABLE

  void TransactionFinishedBeforeUnblock() override;

  void TransactionFinishedAfterUnblock() override;

 public:
  // We need to declare all of nsISupports, because FinishCallback has
  // a pure-virtual nsISupports declaration.
  NS_DECL_ISUPPORTS_INHERITED
};

class NormalTransaction final : public TransactionBase,
                                public PBackgroundIDBTransactionParent {
  nsTArray<RefPtr<FullObjectStoreMetadata>> mObjectStores;

  // Reference counted.
  ~NormalTransaction() override = default;

  bool IsSameProcessActor();

  // Only called by TransactionBase.
  void SendCompleteNotification(nsresult aResult) override;

  // IPDL methods are only called by IPDL.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  mozilla::ipc::IPCResult RecvCommit(
      const Maybe<int64_t>& aLastRequest) override;

  mozilla::ipc::IPCResult RecvAbort(const nsresult& aResultCode) override;

  PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent(
      const RequestParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor(
      PBackgroundIDBRequestParent* aActor,
      const RequestParams& aParams) override;

  bool DeallocPBackgroundIDBRequestParent(
      PBackgroundIDBRequestParent* aActor) override;

  already_AddRefed<PBackgroundIDBCursorParent> AllocPBackgroundIDBCursorParent(
      const OpenCursorParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor(
      PBackgroundIDBCursorParent* aActor,
      const OpenCursorParams& aParams) override;

 public:
  // This constructor is only called by Database.
  NormalTransaction(SafeRefPtr<Database> aDatabase, TransactionBase::Mode aMode,
                    nsTArray<RefPtr<FullObjectStoreMetadata>>&& aObjectStores);

  MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(NormalTransaction, TransactionBase)
};

class VersionChangeTransaction final
    : public TransactionBase,
      public PBackgroundIDBVersionChangeTransactionParent {
  friend class OpenDatabaseOp;

  RefPtr<OpenDatabaseOp> mOpenDatabaseOp;
  SafeRefPtr<FullDatabaseMetadata> mOldMetadata;

  FlippedOnce<false> mActorWasAlive;

 public:
  // Only called by OpenDatabaseOp.
  explicit VersionChangeTransaction(OpenDatabaseOp* aOpenDatabaseOp);

  MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(VersionChangeTransaction,
                                            TransactionBase)

 private:
  // Reference counted.
  ~VersionChangeTransaction() override;

  bool IsSameProcessActor();

  // Only called by OpenDatabaseOp.
  bool CopyDatabaseMetadata();

  void SetActorAlive();

  // Only called by TransactionBase.
  void UpdateMetadata(nsresult aResult) override;

  // Only called by TransactionBase.
  void SendCompleteNotification(nsresult aResult) override;

  // IPDL methods are only called by IPDL.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  mozilla::ipc::IPCResult RecvCommit(
      const Maybe<int64_t>& aLastRequest) override;

  mozilla::ipc::IPCResult RecvAbort(const nsresult& aResultCode) override;

  mozilla::ipc::IPCResult RecvCreateObjectStore(
      const ObjectStoreMetadata& aMetadata) override;

  mozilla::ipc::IPCResult RecvDeleteObjectStore(
      const IndexOrObjectStoreId& aObjectStoreId) override;

  mozilla::ipc::IPCResult RecvRenameObjectStore(
      const IndexOrObjectStoreId& aObjectStoreId,
      const nsString& aName) override;

  mozilla::ipc::IPCResult RecvCreateIndex(
      const IndexOrObjectStoreId& aObjectStoreId,
      const IndexMetadata& aMetadata) override;

  mozilla::ipc::IPCResult RecvDeleteIndex(
      const IndexOrObjectStoreId& aObjectStoreId,
      const IndexOrObjectStoreId& aIndexId) override;

  mozilla::ipc::IPCResult RecvRenameIndex(
      const IndexOrObjectStoreId& aObjectStoreId,
      const IndexOrObjectStoreId& aIndexId, const nsString& aName) override;

  PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent(
      const RequestParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor(
      PBackgroundIDBRequestParent* aActor,
      const RequestParams& aParams) override;

  bool DeallocPBackgroundIDBRequestParent(
      PBackgroundIDBRequestParent* aActor) override;

  already_AddRefed<PBackgroundIDBCursorParent> AllocPBackgroundIDBCursorParent(
      const OpenCursorParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor(
      PBackgroundIDBCursorParent* aActor,
      const OpenCursorParams& aParams) override;
};

class MutableFile : public BackgroundMutableFileParentBase {
  const SafeRefPtr<Database> mDatabase;
  const SafeRefPtr<FileInfo> mFileInfo;

 public:
  [[nodiscard]] static RefPtr<MutableFile> Create(
      nsIFile* aFile, SafeRefPtr<Database> aDatabase,
      SafeRefPtr<FileInfo> aFileInfo);

  const Database& GetDatabase() const {
    AssertIsOnBackgroundThread();

    return *mDatabase;
  }

  SafeRefPtr<FileInfo> GetFileInfoPtr() const {
    AssertIsOnBackgroundThread();

    return mFileInfo.clonePtr();
  }

  void NoteActiveState() override;

  void NoteInactiveState() override;

  PBackgroundParent* GetBackgroundParent() const override;

  already_AddRefed<nsISupports> CreateStream(bool aReadOnly) override;

  already_AddRefed<BlobImpl> CreateBlobImpl() override;

 private:
  MutableFile(nsIFile* aFile, SafeRefPtr<Database> aDatabase,
              SafeRefPtr<FileInfo> aFileInfo);

  ~MutableFile() override;

  PBackgroundFileHandleParent* AllocPBackgroundFileHandleParent(
      const FileMode& aMode) final;

  mozilla::ipc::IPCResult RecvPBackgroundFileHandleConstructor(
      PBackgroundFileHandleParent* aActor, const FileMode& aMode) final;

  mozilla::ipc::IPCResult RecvGetFileId(int64_t* aFileId) override;
};

class FactoryOp
    : public DatabaseOperationBase,
      public OpenDirectoryListener,
      public PBackgroundIDBFactoryRequestParent,
      public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
 public:
  struct MaybeBlockedDatabaseInfo final {
    SafeRefPtr<Database> mDatabase;
    bool mBlocked;

    MaybeBlockedDatabaseInfo(MaybeBlockedDatabaseInfo&&) = default;
    MaybeBlockedDatabaseInfo& operator=(MaybeBlockedDatabaseInfo&&) = default;

    MOZ_IMPLICIT MaybeBlockedDatabaseInfo(SafeRefPtr<Database> aDatabase)
        : mDatabase(std::move(aDatabase)), mBlocked(false) {
      MOZ_ASSERT(mDatabase);

      MOZ_COUNT_CTOR(FactoryOp::MaybeBlockedDatabaseInfo);
    }

    ~MaybeBlockedDatabaseInfo() {
      MOZ_COUNT_DTOR(FactoryOp::MaybeBlockedDatabaseInfo);
    }

    bool operator==(const Database* aOther) const {
      return mDatabase == aOther;
    }

    Database* operator->() const& MOZ_NO_ADDREF_RELEASE_ON_RETURN {
      return mDatabase.unsafeGetRawPtr();
    }
  };

 protected:
  enum class State {
    // Just created on the PBackground thread, dispatched to the main thread.
    // Next step is either SendingResults if permission is denied,
    // PermissionChallenge if the permission is unknown, or FinishOpen
    // if permission is granted.
    Initial,

    // Sending a permission challenge message to the child on the PBackground
    // thread. Next step is PermissionRetry.
    PermissionChallenge,

    // Retrying permission check after a challenge on the main thread. Next step
    // is either SendingResults if permission is denied or FinishOpen
    // if permission is granted.
    PermissionRetry,

    // Opening directory or initializing quota manager on the PBackground
    // thread. Next step is either DirectoryOpenPending if quota manager is
    // already initialized or QuotaManagerPending if quota manager needs to be
    // initialized.
    FinishOpen,

    // Waiting for quota manager initialization to complete on the PBackground
    // thread. Next step is either SendingResults if initialization failed or
    // DirectoryOpenPending if initialization succeeded.
    QuotaManagerPending,

    // Waiting for directory open allowed on the PBackground thread. The next
    // step is either SendingResults if directory lock failed to acquire, or
    // DatabaseOpenPending if directory lock is acquired.
    DirectoryOpenPending,

    // Waiting for database open allowed on the PBackground thread. The next
    // step is DatabaseWorkOpen.
    DatabaseOpenPending,

    // Waiting to do/doing work on the QuotaManager IO thread. Its next step is
    // either BeginVersionChange if the requested version doesn't match the
    // existing database version or SendingResults if the versions match.
    DatabaseWorkOpen,

    // Starting a version change transaction or deleting a database on the
    // PBackground thread. We need to notify other databases that a version
    // change is about to happen, and maybe tell the request that a version
    // change has been blocked. If databases are notified then the next step is
    // WaitingForOtherDatabasesToClose. Otherwise the next step is
    // WaitingForTransactionsToComplete.
    BeginVersionChange,

    // Waiting for other databases to close on the PBackground thread. This
    // state may persist until all databases are closed. The next state is
    // WaitingForTransactionsToComplete.
    WaitingForOtherDatabasesToClose,

    // Waiting for all transactions that could interfere with this operation to
    // complete on the PBackground thread. Next state is
    // DatabaseWorkVersionChange.
    WaitingForTransactionsToComplete,

    // Waiting to do/doing work on the "work thread". This involves waiting for
    // the VersionChangeOp (OpenDatabaseOp and DeleteDatabaseOp each have a
    // different implementation) to do its work. Eventually the state will
    // transition to SendingResults.
    DatabaseWorkVersionChange,

    // Waiting to send/sending results on the PBackground thread. Next step is
    // Completed.
    SendingResults,

    // All done.
    Completed
  };

  // Must be released on the background thread!
  SafeRefPtr<Factory> mFactory;

  // Must be released on the main thread!
  RefPtr<ContentParent> mContentParent;

  // Must be released on the main thread!
  RefPtr<DirectoryLock> mDirectoryLock;

  RefPtr<FactoryOp> mDelayedOp;
  nsTArray<MaybeBlockedDatabaseInfo> mMaybeBlockedDatabases;

  const CommonFactoryRequestParams mCommonParams;
  QuotaInfo mQuotaInfo;
  nsCString mDatabaseId;
  nsString mDatabaseFilePath;
  int64_t mDirectoryLockId;
  State mState;
  bool mWaitingForPermissionRetry;
  bool mEnforcingQuota;
  const bool mDeleting;
  bool mChromeWriteAccessAllowed;
  bool mFileHandleDisabled;
  FlippedOnce<false> mInPrivateBrowsing;

 public:
  const nsCString& Origin() const {
    AssertIsOnOwningThread();

    return mQuotaInfo.mOrigin;
  }

  bool DatabaseFilePathIsKnown() const {
    AssertIsOnOwningThread();

    return !mDatabaseFilePath.IsEmpty();
  }

  const nsString& DatabaseFilePath() const {
    AssertIsOnOwningThread();
    MOZ_ASSERT(!mDatabaseFilePath.IsEmpty());

    return mDatabaseFilePath;
  }

  void NoteDatabaseBlocked(Database* aDatabase);

  void NoteDatabaseClosed(Database* aDatabase);

#ifdef DEBUG
  bool HasBlockedDatabases() const { return !mMaybeBlockedDatabases.IsEmpty(); }
#endif

  void StringifyState(nsACString& aResult) const;

  void Stringify(nsACString& aResult) const;

 protected:
  FactoryOp(SafeRefPtr<Factory> aFactory, RefPtr<ContentParent> aContentParent,
            const CommonFactoryRequestParams& aCommonParams, bool aDeleting);

  ~FactoryOp() override {
    // Normally this would be out-of-line since it is a virtual function but
    // MSVC 2010 fails to link for some reason if it is not inlined here...
    MOZ_ASSERT_IF(OperationMayProceed(),
                  mState == State::Initial || mState == State::Completed);
  }

  nsresult Open();

  nsresult ChallengePermission();

  void PermissionRetry();

  nsresult RetryCheckPermission();

  nsresult DirectoryOpen();

  nsresult SendToIOThread();

  void WaitForTransactions();

  void CleanupMetadata();

  void FinishSendResults();

  nsresult SendVersionChangeMessages(DatabaseActorInfo* aDatabaseActorInfo,
                                     Maybe<Database&> aOpeningDatabase,
                                     uint64_t aOldVersion,
                                     const Maybe<uint64_t>& aNewVersion);

  // Methods that subclasses must implement.
  virtual nsresult DatabaseOpen() = 0;

  virtual nsresult DoDatabaseWork() = 0;

  virtual nsresult BeginVersionChange() = 0;

  virtual bool AreActorsAlive() = 0;

  virtual nsresult DispatchToWorkThread() = 0;

  // Should only be called by Run().
  virtual void SendResults() = 0;

  // We need to declare refcounting unconditionally, because
  // OpenDirectoryListener has pure-virtual refcounting.
  NS_DECL_ISUPPORTS_INHERITED

  // Common nsIRunnable implementation that subclasses may not override.
  NS_IMETHOD
  Run() final;

  // OpenDirectoryListener overrides.
  void DirectoryLockAcquired(DirectoryLock* aLock) override;

  void DirectoryLockFailed() override;

  // IPDL methods.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvPermissionRetry() final;

  virtual void SendBlockedNotification() = 0;

 private:
  mozilla::Result<PermissionRequestBase::PermissionValue, nsresult>
  CheckPermission(ContentParent* aContentParent);

  static bool CheckAtLeastOneAppHasPermission(
      ContentParent* aContentParent, const nsACString& aPermissionString);

  nsresult FinishOpen();

  nsresult QuotaManagerOpen();

  nsresult OpenDirectory();

  // Test whether this FactoryOp needs to wait for the given op.
  bool MustWaitFor(const FactoryOp& aExistingOp);
};

class OpenDatabaseOp final : public FactoryOp {
  friend class Database;
  friend class VersionChangeTransaction;

  class VersionChangeOp;

  Maybe<ContentParentId> mOptionalContentParentId;

  SafeRefPtr<FullDatabaseMetadata> mMetadata;

  uint64_t mRequestedVersion;
  SafeRefPtr<FileManager> mFileManager;

  SafeRefPtr<Database> mDatabase;
  SafeRefPtr<VersionChangeTransaction> mVersionChangeTransaction;

  // This is only set while a VersionChangeOp is live. It holds a strong
  // reference to its OpenDatabaseOp object so this is a weak pointer to avoid
  // cycles.
  VersionChangeOp* mVersionChangeOp;

  uint32_t mTelemetryId;

 public:
  OpenDatabaseOp(SafeRefPtr<Factory> aFactory,
                 RefPtr<ContentParent> aContentParent,
                 const CommonFactoryRequestParams& aParams);

 private:
  ~OpenDatabaseOp() override { MOZ_ASSERT(!mVersionChangeOp); }

  nsresult LoadDatabaseInformation(mozIStorageConnection& aConnection);

  nsresult SendUpgradeNeeded();

  void EnsureDatabaseActor();

  nsresult EnsureDatabaseActorIsAlive();

  mozilla::Result<DatabaseSpec, nsresult> MetadataToSpec() const;

  void AssertMetadataConsistency(const FullDatabaseMetadata& aMetadata)
#ifdef DEBUG
      ;
#else
  {
  }
#endif

  void ConnectionClosedCallback();

  void ActorDestroy(ActorDestroyReason aWhy) override;

  nsresult DatabaseOpen() override;

  nsresult DoDatabaseWork() override;

  nsresult BeginVersionChange() override;

  bool AreActorsAlive() override;

  void SendBlockedNotification() override;

  nsresult DispatchToWorkThread() override;

  void SendResults() override;

  static nsresult UpdateLocaleAwareIndex(mozIStorageConnection& aConnection,
                                         const IndexMetadata& aIndexMetadata,
                                         const nsCString& aLocale);
};

class OpenDatabaseOp::VersionChangeOp final
    : public TransactionDatabaseOperationBase {
  friend class OpenDatabaseOp;

  RefPtr<OpenDatabaseOp> mOpenDatabaseOp;
  const uint64_t mRequestedVersion;
  uint64_t mPreviousVersion;

 private:
  explicit VersionChangeOp(OpenDatabaseOp* aOpenDatabaseOp)
      : TransactionDatabaseOperationBase(
            aOpenDatabaseOp->mVersionChangeTransaction.clonePtr(),
            aOpenDatabaseOp->LoggingSerialNumber()),
        mOpenDatabaseOp(aOpenDatabaseOp),
        mRequestedVersion(aOpenDatabaseOp->mRequestedVersion),
        mPreviousVersion(
            aOpenDatabaseOp->mMetadata->mCommonMetadata.version()) {
    MOZ_ASSERT(aOpenDatabaseOp);
    MOZ_ASSERT(mRequestedVersion);
  }

  ~VersionChangeOp() override { MOZ_ASSERT(!mOpenDatabaseOp); }

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  nsresult SendSuccessResult() override;

  bool SendFailureResult(nsresult aResultCode) override;

  void Cleanup() override;
};

class DeleteDatabaseOp final : public FactoryOp {
  class VersionChangeOp;

  nsString mDatabaseDirectoryPath;
  nsString mDatabaseFilenameBase;
  uint64_t mPreviousVersion;

 public:
  DeleteDatabaseOp(SafeRefPtr<Factory> aFactory,
                   RefPtr<ContentParent> aContentParent,
                   const CommonFactoryRequestParams& aParams)
      : FactoryOp(std::move(aFactory), std::move(aContentParent), aParams,
                  /* aDeleting */ true),
        mPreviousVersion(0) {}

 private:
  ~DeleteDatabaseOp() override = default;

  void LoadPreviousVersion(nsIFile& aDatabaseFile);

  nsresult DatabaseOpen() override;

  nsresult DoDatabaseWork() override;

  nsresult BeginVersionChange() override;

  bool AreActorsAlive() override;

  void SendBlockedNotification() override;

  nsresult DispatchToWorkThread() override;

  void SendResults() override;
};

class DeleteDatabaseOp::VersionChangeOp final : public DatabaseOperationBase {
  friend class DeleteDatabaseOp;

  RefPtr<DeleteDatabaseOp> mDeleteDatabaseOp;

 private:
  explicit VersionChangeOp(DeleteDatabaseOp* aDeleteDatabaseOp)
      : DatabaseOperationBase(aDeleteDatabaseOp->BackgroundChildLoggingId(),
                              aDeleteDatabaseOp->LoggingSerialNumber()),
        mDeleteDatabaseOp(aDeleteDatabaseOp) {
    MOZ_ASSERT(aDeleteDatabaseOp);
    MOZ_ASSERT(!aDeleteDatabaseOp->mDatabaseDirectoryPath.IsEmpty());
  }

  ~VersionChangeOp() override = default;

  nsresult RunOnIOThread();

  void RunOnOwningThread();

  NS_DECL_NSIRUNNABLE
};

class DatabaseOp : public DatabaseOperationBase,
                   public PBackgroundIDBDatabaseRequestParent {
 protected:
  SafeRefPtr<Database> mDatabase;

  enum class State {
    // Just created on the PBackground thread, dispatched to the main thread.
    // Next step is DatabaseWork.
    Initial,

    // Waiting to do/doing work on the QuotaManager IO thread. Next step is
    // SendingResults.
    DatabaseWork,

    // Waiting to send/sending results on the PBackground thread. Next step is
    // Completed.
    SendingResults,

    // All done.
    Completed
  };

  State mState;

 public:
  void RunImmediately() {
    MOZ_ASSERT(mState == State::Initial);

    Unused << this->Run();
  }

 protected:
  DatabaseOp(SafeRefPtr<Database> aDatabase);

  ~DatabaseOp() override {
    MOZ_ASSERT_IF(OperationMayProceed(),
                  mState == State::Initial || mState == State::Completed);
  }

  nsresult SendToIOThread();

  // Methods that subclasses must implement.
  virtual nsresult DoDatabaseWork() = 0;

  virtual void SendResults() = 0;

  // Common nsIRunnable implementation that subclasses may not override.
  NS_IMETHOD
  Run() final;

  // IPDL methods.
  void ActorDestroy(ActorDestroyReason aWhy) override;
};

class CreateFileOp final : public DatabaseOp {
  const CreateFileParams mParams;

  LazyInitializedOnce<const SafeRefPtr<FileInfo>> mFileInfo;

 public:
  CreateFileOp(SafeRefPtr<Database> aDatabase,
               const DatabaseRequestParams& aParams);

 private:
  ~CreateFileOp() override = default;

  mozilla::Result<RefPtr<MutableFile>, nsresult> CreateMutableFile();

  nsresult DoDatabaseWork() override;

  void SendResults() override;
};

class VersionChangeTransactionOp : public TransactionDatabaseOperationBase {
 public:
  void Cleanup() override;

 protected:
  explicit VersionChangeTransactionOp(
      SafeRefPtr<VersionChangeTransaction> aTransaction)
      : TransactionDatabaseOperationBase(std::move(aTransaction)) {}

  ~VersionChangeTransactionOp() override = default;

 private:
  nsresult SendSuccessResult() override;

  bool SendFailureResult(nsresult aResultCode) override;
};

class CreateObjectStoreOp final : public VersionChangeTransactionOp {
  friend class VersionChangeTransaction;

  const ObjectStoreMetadata mMetadata;

 private:
  // Only created by VersionChangeTransaction.
  CreateObjectStoreOp(SafeRefPtr<VersionChangeTransaction> aTransaction,
                      const ObjectStoreMetadata& aMetadata)
      : VersionChangeTransactionOp(std::move(aTransaction)),
        mMetadata(aMetadata) {
    MOZ_ASSERT(aMetadata.id());
  }

  ~CreateObjectStoreOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;
};

class DeleteObjectStoreOp final : public VersionChangeTransactionOp {
  friend class VersionChangeTransaction;

  const RefPtr<FullObjectStoreMetadata> mMetadata;
  const bool mIsLastObjectStore;

 private:
  // Only created by VersionChangeTransaction.
  DeleteObjectStoreOp(SafeRefPtr<VersionChangeTransaction> aTransaction,
                      FullObjectStoreMetadata* const aMetadata,
                      const bool aIsLastObjectStore)
      : VersionChangeTransactionOp(std::move(aTransaction)),
        mMetadata(aMetadata),
        mIsLastObjectStore(aIsLastObjectStore) {
    MOZ_ASSERT(aMetadata->mCommonMetadata.id());
  }

  ~DeleteObjectStoreOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;
};

class RenameObjectStoreOp final : public VersionChangeTransactionOp {
  friend class VersionChangeTransaction;

  const int64_t mId;
  const nsString mNewName;

 private:
  // Only created by VersionChangeTransaction.
  RenameObjectStoreOp(SafeRefPtr<VersionChangeTransaction> aTransaction,
                      FullObjectStoreMetadata* const aMetadata)
      : VersionChangeTransactionOp(std::move(aTransaction)),
        mId(aMetadata->mCommonMetadata.id()),
        mNewName(aMetadata->mCommonMetadata.name()) {
    MOZ_ASSERT(mId);
  }

  ~RenameObjectStoreOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;
};

class CreateIndexOp final : public VersionChangeTransactionOp {
  friend class VersionChangeTransaction;

  class UpdateIndexDataValuesFunction;

  const IndexMetadata mMetadata;
  Maybe<UniqueIndexTable> mMaybeUniqueIndexTable;
  const SafeRefPtr<FileManager> mFileManager;
  const nsCString mDatabaseId;
  const IndexOrObjectStoreId mObjectStoreId;

 private:
  // Only created by VersionChangeTransaction.
  CreateIndexOp(SafeRefPtr<VersionChangeTransaction> aTransaction,
                IndexOrObjectStoreId aObjectStoreId,
                const IndexMetadata& aMetadata);

  ~CreateIndexOp() override = default;

  nsresult InsertDataFromObjectStore(DatabaseConnection* aConnection);

  nsresult InsertDataFromObjectStoreInternal(DatabaseConnection* aConnection);

  bool Init(TransactionBase& aTransaction) override;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;
};

class CreateIndexOp::UpdateIndexDataValuesFunction final
    : public mozIStorageFunction {
  RefPtr<CreateIndexOp> mOp;
  RefPtr<DatabaseConnection> mConnection;
  const NotNull<SafeRefPtr<Database>> mDatabase;

 public:
  UpdateIndexDataValuesFunction(CreateIndexOp* aOp,
                                DatabaseConnection* aConnection,
                                SafeRefPtr<Database> aDatabase)
      : mOp(aOp),
        mConnection(aConnection),
        mDatabase(WrapNotNull(std::move(aDatabase))) {
    MOZ_ASSERT(aOp);
    MOZ_ASSERT(aConnection);
    aConnection->AssertIsOnConnectionThread();
  }

  NS_DECL_ISUPPORTS

 private:
  ~UpdateIndexDataValuesFunction() = default;

  NS_DECL_MOZISTORAGEFUNCTION
};

class DeleteIndexOp final : public VersionChangeTransactionOp {
  friend class VersionChangeTransaction;

  const IndexOrObjectStoreId mObjectStoreId;
  const IndexOrObjectStoreId mIndexId;
  const bool mUnique;
  const bool mIsLastIndex;

 private:
  // Only created by VersionChangeTransaction.
  DeleteIndexOp(SafeRefPtr<VersionChangeTransaction> aTransaction,
                IndexOrObjectStoreId aObjectStoreId,
                IndexOrObjectStoreId aIndexId, const bool aUnique,
                const bool aIsLastIndex);

  ~DeleteIndexOp() override = default;

  nsresult RemoveReferencesToIndex(DatabaseConnection* aConnection,
                                   const Key& aObjectDataKey,
                                   nsTArray<IndexDataValue>& aIndexValues);

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;
};

class RenameIndexOp final : public VersionChangeTransactionOp {
  friend class VersionChangeTransaction;

  const IndexOrObjectStoreId mObjectStoreId;
  const IndexOrObjectStoreId mIndexId;
  const nsString mNewName;

 private:
  // Only created by VersionChangeTransaction.
  RenameIndexOp(SafeRefPtr<VersionChangeTransaction> aTransaction,
                FullIndexMetadata* const aMetadata,
                IndexOrObjectStoreId aObjectStoreId)
      : VersionChangeTransactionOp(std::move(aTransaction)),
        mObjectStoreId(aObjectStoreId),
        mIndexId(aMetadata->mCommonMetadata.id()),
        mNewName(aMetadata->mCommonMetadata.name()) {
    MOZ_ASSERT(mIndexId);
  }

  ~RenameIndexOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;
};

class NormalTransactionOp : public TransactionDatabaseOperationBase,
                            public PBackgroundIDBRequestParent {
#ifdef DEBUG
  bool mResponseSent;
#endif

 public:
  void Cleanup() override;

 protected:
  explicit NormalTransactionOp(SafeRefPtr<TransactionBase> aTransaction)
      : TransactionDatabaseOperationBase(std::move(aTransaction))
#ifdef DEBUG
        ,
        mResponseSent(false)
#endif
  {
  }

  ~NormalTransactionOp() override = default;

  // An overload of DatabaseOperationBase's function that can avoid doing extra
  // work on non-versionchange transactions.
  mozilla::Result<bool, nsresult> ObjectStoreHasIndexes(
      DatabaseConnection& aConnection, IndexOrObjectStoreId aObjectStoreId,
      bool aMayHaveIndexes);

  virtual mozilla::Result<PreprocessParams, nsresult> GetPreprocessParams();

  // Subclasses use this override to set the IPDL response value.
  virtual void GetResponse(RequestResponse& aResponse,
                           size_t* aResponseSize) = 0;

 private:
  nsresult SendPreprocessInfo() override;

  nsresult SendSuccessResult() override;

  bool SendFailureResult(nsresult aResultCode) override;

  // IPDL methods.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvContinue(
      const PreprocessResponse& aResponse) final;
};

class ObjectStoreAddOrPutRequestOp final : public NormalTransactionOp {
  friend class TransactionBase;

  typedef mozilla::dom::quota::PersistenceType PersistenceType;

  class StoredFileInfo final {
    InitializedOnce<const NotNull<SafeRefPtr<FileInfo>>> mFileInfo;
    // Either nothing, a file actor or a non-Blob-backed inputstream to write to
    // disk.
    using FileActorOrInputStream =
        Variant<Nothing, RefPtr<DatabaseFile>, nsCOMPtr<nsIInputStream>>;
    InitializedOnce<const FileActorOrInputStream> mFileActorOrInputStream;
#ifdef DEBUG
    const StructuredCloneFileBase::FileType mType;
#endif

    void AssertInvariants() const;

    explicit StoredFileInfo(SafeRefPtr<FileInfo> aFileInfo);

    StoredFileInfo(SafeRefPtr<FileInfo> aFileInfo,
                   RefPtr<DatabaseFile> aFileActor);

    StoredFileInfo(SafeRefPtr<FileInfo> aFileInfo,
                   nsCOMPtr<nsIInputStream> aInputStream);

   public:
#if defined(NS_BUILD_REFCNT_LOGGING)
    // Only for MOZ_COUNT_CTOR.
    StoredFileInfo(StoredFileInfo&& aOther)
        : mFileInfo{std::move(aOther.mFileInfo)}, mFileActorOrInputStream {
      std::move(aOther.mFileActorOrInputStream)
    }
#  ifdef DEBUG
    , mType { aOther.mType }
#  endif
    { MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); }
#else
    StoredFileInfo(StoredFileInfo&&) = default;
#endif

    static StoredFileInfo CreateForMutableFile(SafeRefPtr<FileInfo> aFileInfo);
    static StoredFileInfo CreateForBlob(SafeRefPtr<FileInfo> aFileInfo,
                                        RefPtr<DatabaseFile> aFileActor);
    static StoredFileInfo CreateForStructuredClone(
        SafeRefPtr<FileInfo> aFileInfo, nsCOMPtr<nsIInputStream> aInputStream);

#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING)
    ~StoredFileInfo() {
      AssertIsOnBackgroundThread();
      AssertInvariants();

      MOZ_COUNT_DTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo);
    }
#endif

    bool IsValid() const { return static_cast<bool>(mFileInfo); }

    const FileInfo& GetFileInfo() const { return **mFileInfo; }

    bool ShouldCompress() const;

    void NotifyWriteSucceeded() const;

    using InputStreamResult =
        mozilla::Result<nsCOMPtr<nsIInputStream>, nsresult>;
    InputStreamResult GetInputStream();

    void Serialize(nsString& aText) const;
  };
  class SCInputStream;

  const ObjectStoreAddPutParams mParams;
  Maybe<UniqueIndexTable> mUniqueIndexTable;

  // This must be non-const so that we can update the mNextAutoIncrementId field
  // if we are modifying an autoIncrement objectStore.
  RefPtr<FullObjectStoreMetadata> mMetadata;

  nsTArray<StoredFileInfo> mStoredFileInfos;

  Key mResponse;
  const GroupAndOrigin mGroupAndOrigin;
  const PersistenceType mPersistenceType;
  const bool mOverwrite;
  bool mObjectStoreMayHaveIndexes;
  bool mDataOverThreshold;

 private:
  // Only created by TransactionBase.
  ObjectStoreAddOrPutRequestOp(SafeRefPtr<TransactionBase> aTransaction,
                               RequestParams&& aParams);

  ~ObjectStoreAddOrPutRequestOp() override = default;

  nsresult RemoveOldIndexDataValues(DatabaseConnection* aConnection);

  bool Init(TransactionBase& aTransaction) override;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override;

  void Cleanup() override;
};

void ObjectStoreAddOrPutRequestOp::StoredFileInfo::AssertInvariants() const {
  // The only allowed types are eStructuredClone, eBlob and eMutableFile.
  MOZ_ASSERT(StructuredCloneFileBase::eStructuredClone == mType ||
             StructuredCloneFileBase::eBlob == mType ||
             StructuredCloneFileBase::eMutableFile == mType);

  // mFileInfo and a file actor in mFileActorOrInputStream are present until
  // the object is moved away, but an inputStream in mFileActorOrInputStream
  // can be released early.
  MOZ_ASSERT_IF(static_cast<bool>(mFileActorOrInputStream) &&
                    mFileActorOrInputStream->is<RefPtr<DatabaseFile>>(),
                static_cast<bool>(mFileInfo));

  if (mFileInfo) {
    // In a non-moved StoredFileInfo, one of the following is true:
    // - This was an overflow structured clone (eStructuredClone) and
    //   storedFileInfo.mFileActorOrInputStream CAN be a non-nullptr input
    //   stream (but that might have been release by ReleaseInputStream).
    MOZ_ASSERT_IF(
        StructuredCloneFileBase::eStructuredClone == mType,
        !mFileActorOrInputStream ||
            (mFileActorOrInputStream->is<nsCOMPtr<nsIInputStream>>() &&
             mFileActorOrInputStream->as<nsCOMPtr<nsIInputStream>>()));

    // - This is a reference to a Blob (eBlob) that may or may not have
    //   already been written to disk.  storedFileInfo.mFileActorOrInputStream
    //   MUST be a non-null file actor, but its GetInputStream may return
    //   nullptr (so don't assert on that).
    MOZ_ASSERT_IF(StructuredCloneFileBase::eBlob == mType,
                  mFileActorOrInputStream->is<RefPtr<DatabaseFile>>() &&
                      mFileActorOrInputStream->as<RefPtr<DatabaseFile>>());

    // - It's a mutable file (eMutableFile).  No writing will be performed,
    // and storedFileInfo.mFileActorOrInputStream is Nothing.
    MOZ_ASSERT_IF(StructuredCloneFileBase::eMutableFile == mType,
                  mFileActorOrInputStream->is<Nothing>());
  }
}

ObjectStoreAddOrPutRequestOp::StoredFileInfo::StoredFileInfo(
    SafeRefPtr<FileInfo> aFileInfo)
    : mFileInfo{WrapNotNull(std::move(aFileInfo))}, mFileActorOrInputStream {
  Nothing {}
}
#ifdef DEBUG
, mType { StructuredCloneFileBase::eMutableFile }
#endif
{
  AssertIsOnBackgroundThread();
  AssertInvariants();

  MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo);
}

ObjectStoreAddOrPutRequestOp::StoredFileInfo::StoredFileInfo(
    SafeRefPtr<FileInfo> aFileInfo, RefPtr<DatabaseFile> aFileActor)
    : mFileInfo{WrapNotNull(std::move(aFileInfo))}, mFileActorOrInputStream {
  std::move(aFileActor)
}
#ifdef DEBUG
, mType { StructuredCloneFileBase::eBlob }
#endif
{
  AssertIsOnBackgroundThread();
  AssertInvariants();

  MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo);
}

ObjectStoreAddOrPutRequestOp::StoredFileInfo::StoredFileInfo(
    SafeRefPtr<FileInfo> aFileInfo, nsCOMPtr<nsIInputStream> aInputStream)
    : mFileInfo{WrapNotNull(std::move(aFileInfo))}, mFileActorOrInputStream {
  std::move(aInputStream)
}
#ifdef DEBUG
, mType { StructuredCloneFileBase::eStructuredClone }
#endif
{
  AssertIsOnBackgroundThread();
  AssertInvariants();

  MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo);
}

ObjectStoreAddOrPutRequestOp::StoredFileInfo
ObjectStoreAddOrPutRequestOp::StoredFileInfo::CreateForMutableFile(
    SafeRefPtr<FileInfo> aFileInfo) {
  return StoredFileInfo{std::move(aFileInfo)};
}

ObjectStoreAddOrPutRequestOp::StoredFileInfo
ObjectStoreAddOrPutRequestOp::StoredFileInfo::CreateForBlob(
    SafeRefPtr<FileInfo> aFileInfo, RefPtr<DatabaseFile> aFileActor) {
  return {std::move(aFileInfo), std::move(aFileActor)};
}

ObjectStoreAddOrPutRequestOp::StoredFileInfo
ObjectStoreAddOrPutRequestOp::StoredFileInfo::CreateForStructuredClone(
    SafeRefPtr<FileInfo> aFileInfo, nsCOMPtr<nsIInputStream> aInputStream) {
  return {std::move(aFileInfo), std::move(aInputStream)};
}

bool ObjectStoreAddOrPutRequestOp::StoredFileInfo::ShouldCompress() const {
  // Must not be called after moving.
  MOZ_ASSERT(IsValid());

  // Compression is only necessary for eStructuredClone, i.e. when
  // mFileActorOrInputStream stored an input stream. However, this is only
  // called after GetInputStream, when mFileActorOrInputStream has been
  // cleared, which is only possible for this type.
  const bool res = !mFileActorOrInputStream;
  MOZ_ASSERT(res == (StructuredCloneFileBase::eStructuredClone == mType));
  return res;
}

void ObjectStoreAddOrPutRequestOp::StoredFileInfo::NotifyWriteSucceeded()
    const {
  MOZ_ASSERT(IsValid());

  // For eBlob, clear the blob implementation.
  if (mFileActorOrInputStream &&
      mFileActorOrInputStream->is<RefPtr<DatabaseFile>>()) {
    mFileActorOrInputStream->as<RefPtr<DatabaseFile>>()
        ->WriteSucceededClearBlobImpl();
  }

  // For the other types, no action is necessary.
}

ObjectStoreAddOrPutRequestOp::StoredFileInfo::InputStreamResult
ObjectStoreAddOrPutRequestOp::StoredFileInfo::GetInputStream() {
  if (!mFileActorOrInputStream) {
    MOZ_ASSERT(StructuredCloneFileBase::eStructuredClone == mType);
    return nsCOMPtr<nsIInputStream>{};
  }

  // For the different cases, see also the comments in AssertInvariants.
  return mFileActorOrInputStream->match(
      [](const Nothing&) -> InputStreamResult {
        return nsCOMPtr<nsIInputStream>{};
      },
      [](const RefPtr<DatabaseFile>& databaseActor) -> InputStreamResult {
        ErrorResult rv;
        auto inputStream = databaseActor->GetInputStream(rv);
        if (NS_WARN_IF(rv.Failed())) {
          return Err(rv.StealNSResult());
        }

        return inputStream;
      },
      [this](const nsCOMPtr<nsIInputStream>& inputStream) -> InputStreamResult {
        auto res = inputStream;
        // destroy() clears the inputStream parameter, so we needed to make a
        // copy before
        mFileActorOrInputStream.destroy();
        AssertInvariants();
        return res;
      });
}

void ObjectStoreAddOrPutRequestOp::StoredFileInfo::Serialize(
    nsString& aText) const {
  AssertInvariants();
  MOZ_ASSERT(IsValid());

  const int64_t id = (*mFileInfo)->Id();

  auto structuredCloneHandler = [&aText, id](const nsCOMPtr<nsIInputStream>&) {
    // eStructuredClone
    aText.Append('.');
    aText.AppendInt(id);
  };

  // If mFileActorOrInputStream was moved, we had an inputStream before.
  if (!mFileActorOrInputStream) {
    structuredCloneHandler(nullptr);
    return;
  }

  // This encoding is parsed in DeserializeStructuredCloneFile.
  mFileActorOrInputStream->match(
      [&aText, id](const Nothing&) {
        // eMutableFile
        aText.AppendInt(-id);
      },
      [&aText, id](const RefPtr<DatabaseFile>&) {
        // eBlob
        aText.AppendInt(id);
      },
      structuredCloneHandler);
}

class ObjectStoreAddOrPutRequestOp::SCInputStream final
    : public nsIInputStream {
  const JSStructuredCloneData& mData;
  JSStructuredCloneData::Iterator mIter;

 public:
  explicit SCInputStream(const JSStructuredCloneData& aData)
      : mData(aData), mIter(aData.Start()) {}

 private:
  virtual ~SCInputStream() = default;

  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIINPUTSTREAM
};

class ObjectStoreGetRequestOp final : public NormalTransactionOp {
  friend class TransactionBase;

  const IndexOrObjectStoreId mObjectStoreId;
  SafeRefPtr<Database> mDatabase;
  const Maybe<SerializedKeyRange> mOptionalKeyRange;
  AutoTArray<StructuredCloneReadInfoParent, 1> mResponse;
  PBackgroundParent* mBackgroundParent;
  uint32_t mPreprocessInfoCount;
  const uint32_t mLimit;
  const bool mGetAll;

 private:
  // Only created by TransactionBase.
  ObjectStoreGetRequestOp(SafeRefPtr<TransactionBase> aTransaction,
                          const RequestParams& aParams, bool aGetAll);

  ~ObjectStoreGetRequestOp() override = default;

  template <typename T>
  mozilla::Result<T, nsresult> ConvertResponse(
      StructuredCloneReadInfoParent&& aInfo);

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  bool HasPreprocessInfo() override;

  mozilla::Result<PreprocessParams, nsresult> GetPreprocessParams() override;

  void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override;
};

class ObjectStoreGetKeyRequestOp final : public NormalTransactionOp {
  friend class TransactionBase;

  const IndexOrObjectStoreId mObjectStoreId;
  const Maybe<SerializedKeyRange> mOptionalKeyRange;
  const uint32_t mLimit;
  const bool mGetAll;
  nsTArray<Key> mResponse;

 private:
  // Only created by TransactionBase.
  ObjectStoreGetKeyRequestOp(SafeRefPtr<TransactionBase> aTransaction,
                             const RequestParams& aParams, bool aGetAll);

  ~ObjectStoreGetKeyRequestOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override;
};

class ObjectStoreDeleteRequestOp final : public NormalTransactionOp {
  friend class TransactionBase;

  const ObjectStoreDeleteParams mParams;
  ObjectStoreDeleteResponse mResponse;
  bool mObjectStoreMayHaveIndexes;

 private:
  ObjectStoreDeleteRequestOp(SafeRefPtr<TransactionBase> aTransaction,
                             const ObjectStoreDeleteParams& aParams);

  ~ObjectStoreDeleteRequestOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override {
    aResponse = std::move(mResponse);
    *aResponseSize = 0;
  }
};

class ObjectStoreClearRequestOp final : public NormalTransactionOp {
  friend class TransactionBase;

  const ObjectStoreClearParams mParams;
  ObjectStoreClearResponse mResponse;
  bool mObjectStoreMayHaveIndexes;

 private:
  ObjectStoreClearRequestOp(SafeRefPtr<TransactionBase> aTransaction,
                            const ObjectStoreClearParams& aParams);

  ~ObjectStoreClearRequestOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override {
    aResponse = std::move(mResponse);
    *aResponseSize = 0;
  }
};

class ObjectStoreCountRequestOp final : public NormalTransactionOp {
  friend class TransactionBase;

  const ObjectStoreCountParams mParams;
  ObjectStoreCountResponse mResponse;

 private:
  ObjectStoreCountRequestOp(SafeRefPtr<TransactionBase> aTransaction,
                            const ObjectStoreCountParams& aParams)
      : NormalTransactionOp(std::move(aTransaction)), mParams(aParams) {}

  ~ObjectStoreCountRequestOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override {
    aResponse = std::move(mResponse);
    *aResponseSize = sizeof(uint64_t);
  }
};

class IndexRequestOpBase : public NormalTransactionOp {
 protected:
  const RefPtr<FullIndexMetadata> mMetadata;

 protected:
  IndexRequestOpBase(SafeRefPtr<TransactionBase> aTransaction,
                     const RequestParams& aParams)
      : NormalTransactionOp(std::move(aTransaction)),
        mMetadata(IndexMetadataForParams(Transaction(), aParams)) {}

  ~IndexRequestOpBase() override = default;

 private:
  static RefPtr<FullIndexMetadata> IndexMetadataForParams(
      const TransactionBase& aTransaction, const RequestParams& aParams);
};

class IndexGetRequestOp final : public IndexRequestOpBase {
  friend class TransactionBase;

  SafeRefPtr<Database> mDatabase;
  const Maybe<SerializedKeyRange> mOptionalKeyRange;
  AutoTArray<StructuredCloneReadInfoParent, 1> mResponse;
  PBackgroundParent* mBackgroundParent;
  const uint32_t mLimit;
  const bool mGetAll;

 private:
  // Only created by TransactionBase.
  IndexGetRequestOp(SafeRefPtr<TransactionBase> aTransaction,
                    const RequestParams& aParams, bool aGetAll);

  ~IndexGetRequestOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override;
};

class IndexGetKeyRequestOp final : public IndexRequestOpBase {
  friend class TransactionBase;

  const Maybe<SerializedKeyRange> mOptionalKeyRange;
  AutoTArray<Key, 1> mResponse;
  const uint32_t mLimit;
  const bool mGetAll;

 private:
  // Only created by TransactionBase.
  IndexGetKeyRequestOp(SafeRefPtr<TransactionBase> aTransaction,
                       const RequestParams& aParams, bool aGetAll);

  ~IndexGetKeyRequestOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override;
};

class IndexCountRequestOp final : public IndexRequestOpBase {
  friend class TransactionBase;

  const IndexCountParams mParams;
  IndexCountResponse mResponse;

 private:
  // Only created by TransactionBase.
  IndexCountRequestOp(SafeRefPtr<TransactionBase> aTransaction,
                      const RequestParams& aParams)
      : IndexRequestOpBase(std::move(aTransaction), aParams),
        mParams(aParams.get_IndexCountParams()) {}

  ~IndexCountRequestOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override {
    aResponse = std::move(mResponse);
    *aResponseSize = sizeof(uint64_t);
  }
};

template <IDBCursorType CursorType>
class Cursor;

constexpr IDBCursorType ToKeyOnlyType(const IDBCursorType aType) {
  MOZ_ASSERT(aType == IDBCursorType::ObjectStore ||
             aType == IDBCursorType::ObjectStoreKey ||
             aType == IDBCursorType::Index || aType == IDBCursorType::IndexKey);
  switch (aType) {
    case IDBCursorType::ObjectStore:
    case IDBCursorType::ObjectStoreKey:
      return IDBCursorType::ObjectStoreKey;
    case IDBCursorType::Index:
    case IDBCursorType::IndexKey:
      return IDBCursorType::IndexKey;
  }
}

template <IDBCursorType CursorType>
using CursorPosition = CursorData<ToKeyOnlyType(CursorType)>;

#ifdef DEBUG
constexpr indexedDB::OpenCursorParams::Type ToOpenCursorParamsType(
    const IDBCursorType aType) {
  MOZ_ASSERT(aType == IDBCursorType::ObjectStore ||
             aType == IDBCursorType::ObjectStoreKey ||
             aType == IDBCursorType::Index || aType == IDBCursorType::IndexKey);
  switch (aType) {
    case IDBCursorType::ObjectStore:
      return indexedDB::OpenCursorParams::TObjectStoreOpenCursorParams;
    case IDBCursorType::ObjectStoreKey:
      return indexedDB::OpenCursorParams::TObjectStoreOpenKeyCursorParams;
    case IDBCursorType::Index:
      return indexedDB::OpenCursorParams::TIndexOpenCursorParams;
    case IDBCursorType::IndexKey:
      return indexedDB::OpenCursorParams::TIndexOpenKeyCursorParams;
  }
}
#endif

class CursorBase : public PBackgroundIDBCursorParent {
  friend class TransactionBase;
  template <IDBCursorType CursorType>
  friend class CommonOpenOpHelper;

 protected:
  const SafeRefPtr<TransactionBase> mTransaction;

  // This should only be touched on the PBackground thread to check whether
  // the objectStore has been deleted. Holding these saves a hash lookup for
  // every call to continue()/advance().
  InitializedOnce<const NotNull<RefPtr<FullObjectStoreMetadata>>>
      mObjectStoreMetadata;

  const IndexOrObjectStoreId mObjectStoreId;

  LazyInitializedOnce<const Key>
      mLocaleAwareRangeBound;  ///< If the cursor is based on a key range, the
                               ///< bound in the direction of iteration (e.g.
                               ///< the upper bound in case of mDirection ==
                               ///< NEXT). If the cursor is based on a key, it
                               ///< is unset. If mLocale is set, this was
                               ///< converted to mLocale.

  const Direction mDirection;

  const int32_t mMaxExtraCount;

  const bool mIsSameProcessActor;

  struct ConstructFromTransactionBase {};

 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::CursorBase,
                                        final)

  CursorBase(SafeRefPtr<TransactionBase> aTransaction,
             RefPtr<FullObjectStoreMetadata> aObjectStoreMetadata,
             Direction aDirection,
             ConstructFromTransactionBase aConstructionTag);

 protected:
  // Reference counted.
  ~CursorBase() override { MOZ_ASSERT(!mObjectStoreMetadata); }

 private:
  virtual bool Start(const OpenCursorParams& aParams) = 0;
};

class IndexCursorBase : public CursorBase {
 public:
  bool IsLocaleAware() const { return !mLocale.IsEmpty(); }

  IndexCursorBase(SafeRefPtr<TransactionBase> aTransaction,
                  RefPtr<FullObjectStoreMetadata> aObjectStoreMetadata,
                  RefPtr<FullIndexMetadata> aIndexMetadata,
                  Direction aDirection,
                  ConstructFromTransactionBase aConstructionTag)
      : CursorBase{std::move(aTransaction), std::move(aObjectStoreMetadata),
                   aDirection, aConstructionTag},
        mIndexMetadata(WrapNotNull(std::move(aIndexMetadata))),
        mIndexId((*mIndexMetadata)->mCommonMetadata.id()),
        mUniqueIndex((*mIndexMetadata)->mCommonMetadata.unique()),
        mLocale((*mIndexMetadata)->mCommonMetadata.locale()) {}

 protected:
  IndexOrObjectStoreId Id() const { return mIndexId; }

  // This should only be touched on the PBackground thread to check whether
  // the index has been deleted. Holding these saves a hash lookup for every
  // call to continue()/advance().
  InitializedOnce<const NotNull<RefPtr<FullIndexMetadata>>> mIndexMetadata;
  const IndexOrObjectStoreId mIndexId;
  const bool mUniqueIndex;
  const nsCString
      mLocale;  ///< The locale if the cursor is locale-aware, otherwise empty.

  struct ContinueQueries {
    nsCString mContinueQuery;
    nsCString mContinueToQuery;
    nsCString mContinuePrimaryKeyQuery;

    const nsCString& GetContinueQuery(const bool hasContinueKey,
                                      const bool hasContinuePrimaryKey) const {
      return hasContinuePrimaryKey ? mContinuePrimaryKeyQuery
             : hasContinueKey      ? mContinueToQuery
                                   : mContinueQuery;
    }
  };
};

class ObjectStoreCursorBase : public CursorBase {
 public:
  using CursorBase::CursorBase;

  static constexpr bool IsLocaleAware() { return false; }

 protected:
  IndexOrObjectStoreId Id() const { return mObjectStoreId; }

  struct ContinueQueries {
    nsCString mContinueQuery;
    nsCString mContinueToQuery;

    const nsCString& GetContinueQuery(const bool hasContinueKey,
                                      const bool hasContinuePrimaryKey) const {
      MOZ_ASSERT(!hasContinuePrimaryKey);
      return hasContinueKey ? mContinueToQuery : mContinueQuery;
    }
  };
};

using FilesArray = nsTArray<nsTArray<StructuredCloneFileParent>>;

struct PseudoFilesArray {
  static constexpr bool IsEmpty() { return true; }

  static constexpr void Clear() {}
};

template <IDBCursorType CursorType>
using FilesArrayT =
    std::conditional_t<!CursorTypeTraits<CursorType>::IsKeyOnlyCursor,
                       FilesArray, PseudoFilesArray>;

class ValueCursorBase {
  friend struct ValuePopulateResponseHelper<true>;
  friend struct ValuePopulateResponseHelper<false>;

 protected:
  explicit ValueCursorBase(TransactionBase* const aTransaction)
      : mDatabase(aTransaction->GetDatabasePtr()),
        mFileManager(mDatabase->GetFileManagerPtr()),
        mBackgroundParent(WrapNotNull(aTransaction->GetBackgroundParent())) {
    MOZ_ASSERT(mDatabase);
  }

  void ProcessFiles(CursorResponse& aResponse, const FilesArray& aFiles);

  ~ValueCursorBase() { MOZ_ASSERT(!mBackgroundParent); }

  const SafeRefPtr<Database> mDatabase;
  const NotNull<SafeRefPtr<FileManager>> mFileManager;

  InitializedOnce<const NotNull<PBackgroundParent*>> mBackgroundParent;
};

class KeyCursorBase {
 protected:
  explicit KeyCursorBase(TransactionBase* const /*aTransaction*/) {}

  static constexpr void ProcessFiles(CursorResponse& aResponse,
                                     const PseudoFilesArray& aFiles) {}
};

template <IDBCursorType CursorType>
class CursorOpBaseHelperBase;

template <IDBCursorType CursorType>
class Cursor final
    : public std::conditional_t<
          CursorTypeTraits<CursorType>::IsObjectStoreCursor,
          ObjectStoreCursorBase, IndexCursorBase>,
      public std::conditional_t<CursorTypeTraits<CursorType>::IsKeyOnlyCursor,
                                KeyCursorBase, ValueCursorBase> {
  using Base =
      std::conditional_t<CursorTypeTraits<CursorType>::IsObjectStoreCursor,
                         ObjectStoreCursorBase, IndexCursorBase>;

  using KeyValueBase =
      std::conditional_t<CursorTypeTraits<CursorType>::IsKeyOnlyCursor,
                         KeyCursorBase, ValueCursorBase>;

  static constexpr bool IsIndexCursor =
      !CursorTypeTraits<CursorType>::IsObjectStoreCursor;

  static constexpr bool IsValueCursor =
      !CursorTypeTraits<CursorType>::IsKeyOnlyCursor;

  class CursorOpBase;
  class OpenOp;
  class ContinueOp;

  using Base::Id;
  using CursorBase::Manager;
  using CursorBase::mDirection;
  using CursorBase::mObjectStoreId;
  using CursorBase::mTransaction;
  using typename CursorBase::ActorDestroyReason;

  using TypedOpenOpHelper =
      std::conditional_t<IsIndexCursor, IndexOpenOpHelper<CursorType>,
                         ObjectStoreOpenOpHelper<CursorType>>;

  friend class CursorOpBaseHelperBase<CursorType>;
  friend class CommonOpenOpHelper<CursorType>;
  friend TypedOpenOpHelper;
  friend class OpenOpHelper<CursorType>;

  CursorOpBase* mCurrentlyRunningOp = nullptr;

  LazyInitializedOnce<const typename Base::ContinueQueries> mContinueQueries;

  // Only called by TransactionBase.
  bool Start(const OpenCursorParams& aParams) final;

  void SendResponseInternal(CursorResponse& aResponse,
                            const FilesArrayT<CursorType>& aFiles);

  // Must call SendResponseInternal!
  bool SendResponse(const CursorResponse& aResponse) = delete;

  // IPDL methods.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  mozilla::ipc::IPCResult RecvContinue(
      const CursorRequestParams& aParams, const Key& aCurrentKey,
      const Key& aCurrentObjectStoreKey) override;

 public:
  Cursor(SafeRefPtr<TransactionBase> aTransaction,
         RefPtr<FullObjectStoreMetadata> aObjectStoreMetadata,
         RefPtr<FullIndexMetadata> aIndexMetadata,
         typename Base::Direction aDirection,
         typename Base::ConstructFromTransactionBase aConstructionTag)
      : Base{std::move(aTransaction), std::move(aObjectStoreMetadata),
             std::move(aIndexMetadata), aDirection, aConstructionTag},
        KeyValueBase{this->mTransaction.unsafeGetRawPtr()} {}

  Cursor(SafeRefPtr<TransactionBase> aTransaction,
         RefPtr<FullObjectStoreMetadata> aObjectStoreMetadata,
         typename Base::Direction aDirection,
         typename Base::ConstructFromTransactionBase aConstructionTag)
      : Base{std::move(aTransaction), std::move(aObjectStoreMetadata),
             aDirection, aConstructionTag},
        KeyValueBase{this->mTransaction.unsafeGetRawPtr()} {}

 private:
  void SetOptionalKeyRange(const Maybe<SerializedKeyRange>& aOptionalKeyRange,
                           bool* aOpen);

  bool VerifyRequestParams(const CursorRequestParams& aParams,
                           const CursorPosition<CursorType>& aPosition) const;

  ~Cursor() final = default;
};

template <IDBCursorType CursorType>
class Cursor<CursorType>::CursorOpBase
    : public TransactionDatabaseOperationBase {
  friend class CursorOpBaseHelperBase<CursorType>;

 protected:
  RefPtr<Cursor> mCursor;
  FilesArrayT<CursorType> mFiles;  // TODO: Consider removing this member
                                   // entirely if we are no value cursor.

  CursorResponse mResponse;

#ifdef DEBUG
  bool mResponseSent;
#endif

 protected:
  explicit CursorOpBase(Cursor* aCursor)
      : TransactionDatabaseOperationBase(aCursor->mTransaction.clonePtr()),
        mCursor(aCursor)
#ifdef DEBUG
        ,
        mResponseSent(false)
#endif
  {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(aCursor);
  }

  ~CursorOpBase() override = default;

  bool SendFailureResult(nsresult aResultCode) final;
  nsresult SendSuccessResult() final;

  void Cleanup() override;
};

template <IDBCursorType CursorType>
class OpenOpHelper;

using ResponseSizeOrError = Result<size_t, nsresult>;

template <IDBCursorType CursorType>
class CursorOpBaseHelperBase {
 public:
  explicit CursorOpBaseHelperBase(
      typename Cursor<CursorType>::CursorOpBase& aOp)
      : mOp{aOp} {}

  ResponseSizeOrError PopulateResponseFromStatement(mozIStorageStatement* aStmt,
                                                    bool aInitializeResponse,
                                                    Key* const aOptOutSortKey);

  void PopulateExtraResponses(mozIStorageStatement* aStmt,
                              uint32_t aMaxExtraCount,
                              const size_t aInitialResponseSize,
                              const nsCString& aOperation,
                              Key* const aOptPreviousSortKey);

 protected:
  Cursor<CursorType>& GetCursor() {
    MOZ_ASSERT(mOp.mCursor);
    return *mOp.mCursor;
  }

  void SetResponse(CursorResponse aResponse) {
    mOp.mResponse = std::move(aResponse);
  }

 protected:
  typename Cursor<CursorType>::CursorOpBase& mOp;
};

class CommonOpenOpHelperBase {
 protected:
  static void AppendConditionClause(const nsACString& aColumnName,
                                    const nsACString& aStatementParameterName,
                                    bool aLessThan, bool aEquals,
                                    nsCString& aResult);
};

template <IDBCursorType CursorType>
class CommonOpenOpHelper : public CursorOpBaseHelperBase<CursorType>,
                           protected CommonOpenOpHelperBase {
 public:
  explicit CommonOpenOpHelper(typename Cursor<CursorType>::OpenOp& aOp)
      : CursorOpBaseHelperBase<CursorType>{aOp} {}

 protected:
  using CursorOpBaseHelperBase<CursorType>::GetCursor;
  using CursorOpBaseHelperBase<CursorType>::PopulateExtraResponses;
  using CursorOpBaseHelperBase<CursorType>::PopulateResponseFromStatement;
  using CursorOpBaseHelperBase<CursorType>::SetResponse;

  const Maybe<SerializedKeyRange>& GetOptionalKeyRange() const {
    // This downcast is safe, since we initialized mOp from an OpenOp in the
    // ctor.
    return static_cast<typename Cursor<CursorType>::OpenOp&>(this->mOp)
        .mOptionalKeyRange;
  }

  nsresult ProcessStatementSteps(mozIStorageStatement* aStmt);
};

template <IDBCursorType CursorType>
class ObjectStoreOpenOpHelper : protected CommonOpenOpHelper<CursorType> {
 public:
  using CommonOpenOpHelper<CursorType>::CommonOpenOpHelper;

 protected:
  using CommonOpenOpHelper<CursorType>::GetCursor;
  using CommonOpenOpHelper<CursorType>::GetOptionalKeyRange;
  using CommonOpenOpHelper<CursorType>::AppendConditionClause;

  void PrepareKeyConditionClauses(const nsACString& aDirectionClause,
                                  const nsCString& aQueryStart);
};

template <IDBCursorType CursorType>
class IndexOpenOpHelper : protected CommonOpenOpHelper<CursorType> {
 public:
  using CommonOpenOpHelper<CursorType>::CommonOpenOpHelper;

 protected:
  using CommonOpenOpHelper<CursorType>::GetCursor;
  using CommonOpenOpHelper<CursorType>::GetOptionalKeyRange;
  using CommonOpenOpHelper<CursorType>::AppendConditionClause;

  void PrepareIndexKeyConditionClause(
      const nsACString& aDirectionClause,
      const nsLiteralCString& aObjectDataKeyPrefix, nsAutoCString aQueryStart);
};

template <>
class OpenOpHelper<IDBCursorType::ObjectStore>
    : public ObjectStoreOpenOpHelper<IDBCursorType::ObjectStore> {
 public:
  using ObjectStoreOpenOpHelper<
      IDBCursorType::ObjectStore>::ObjectStoreOpenOpHelper;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection);
};

template <>
class OpenOpHelper<IDBCursorType::ObjectStoreKey>
    : public ObjectStoreOpenOpHelper<IDBCursorType::ObjectStoreKey> {
 public:
  using ObjectStoreOpenOpHelper<
      IDBCursorType::ObjectStoreKey>::ObjectStoreOpenOpHelper;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection);
};

template <>
class OpenOpHelper<IDBCursorType::Index>
    : IndexOpenOpHelper<IDBCursorType::Index> {
 private:
  void PrepareKeyConditionClauses(const nsACString& aDirectionClause,
                                  nsAutoCString aQueryStart) {
    PrepareIndexKeyConditionClause(aDirectionClause, "index_table."_ns,
                                   std::move(aQueryStart));
  }

 public:
  using IndexOpenOpHelper<IDBCursorType::Index>::IndexOpenOpHelper;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection);
};

template <>
class OpenOpHelper<IDBCursorType::IndexKey>
    : IndexOpenOpHelper<IDBCursorType::IndexKey> {
 private:
  void PrepareKeyConditionClauses(const nsACString& aDirectionClause,
                                  nsAutoCString aQueryStart) {
    PrepareIndexKeyConditionClause(aDirectionClause, ""_ns,
                                   std::move(aQueryStart));
  }

 public:
  using IndexOpenOpHelper<IDBCursorType::IndexKey>::IndexOpenOpHelper;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection);
};

template <IDBCursorType CursorType>
class Cursor<CursorType>::OpenOp final : public CursorOpBase {
  friend class Cursor<CursorType>;
  friend class CommonOpenOpHelper<CursorType>;

  const Maybe<SerializedKeyRange> mOptionalKeyRange;

  using CursorOpBase::mCursor;
  using CursorOpBase::mResponse;

  // Only created by Cursor.
  OpenOp(Cursor* const aCursor,
         const Maybe<SerializedKeyRange>& aOptionalKeyRange)
      : CursorOpBase(aCursor), mOptionalKeyRange(aOptionalKeyRange) {}

  // Reference counted.
  ~OpenOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;
};

template <IDBCursorType CursorType>
class Cursor<CursorType>::ContinueOp final
    : public Cursor<CursorType>::CursorOpBase {
  friend class Cursor<CursorType>;

  using CursorOpBase::mCursor;
  using CursorOpBase::mResponse;
  const CursorRequestParams mParams;

  // Only created by Cursor.
  ContinueOp(Cursor* const aCursor, CursorRequestParams aParams,
             CursorPosition<CursorType> aPosition)
      : CursorOpBase(aCursor),
        mParams(std::move(aParams)),
        mCurrentPosition{std::move(aPosition)} {
    MOZ_ASSERT(mParams.type() != CursorRequestParams::T__None);
  }

  // Reference counted.
  ~ContinueOp() override = default;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  const CursorPosition<CursorType> mCurrentPosition;
};

class Utils final : public PBackgroundIndexedDBUtilsParent {
#ifdef DEBUG
  bool mActorDestroyed;
#endif

 public:
  Utils();

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Utils)

 private:
  // Reference counted.
  ~Utils() override;

  // IPDL methods are only called by IPDL.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  mozilla::ipc::IPCResult RecvGetFileReferences(
      const PersistenceType& aPersistenceType, const nsCString& aOrigin,
      const nsString& aDatabaseName, const int64_t& aFileId, int32_t* aRefCnt,
      int32_t* aDBRefCnt, bool* aResult) override;
};

/*******************************************************************************
 * Other class declarations
 ******************************************************************************/

struct DatabaseActorInfo final {
  friend class mozilla::DefaultDelete<DatabaseActorInfo>;

  SafeRefPtr<FullDatabaseMetadata> mMetadata;
  nsTArray<CheckedUnsafePtr<Database>> mLiveDatabases;
  RefPtr<FactoryOp> mWaitingFactoryOp;

  DatabaseActorInfo(SafeRefPtr<FullDatabaseMetadata> aMetadata,
                    Database* aDatabase)
      : mMetadata(std::move(aMetadata)) {
    MOZ_ASSERT(aDatabase);

    MOZ_COUNT_CTOR(DatabaseActorInfo);

    mLiveDatabases.AppendElement(aDatabase);
  }

 private:
  ~DatabaseActorInfo() {
    MOZ_ASSERT(mLiveDatabases.IsEmpty());
    MOZ_ASSERT(!mWaitingFactoryOp || !mWaitingFactoryOp->HasBlockedDatabases());

    MOZ_COUNT_DTOR(DatabaseActorInfo);
  }
};

class DatabaseLoggingInfo final {
#ifdef DEBUG
  // Just for potential warnings.
  friend class Factory;
#endif

  LoggingInfo mLoggingInfo;

 public:
  explicit DatabaseLoggingInfo(const LoggingInfo& aLoggingInfo)
      : mLoggingInfo(aLoggingInfo) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(aLoggingInfo.nextTransactionSerialNumber());
    MOZ_ASSERT(aLoggingInfo.nextVersionChangeTransactionSerialNumber());
    MOZ_ASSERT(aLoggingInfo.nextRequestSerialNumber());
  }

  const nsID& Id() const {
    AssertIsOnBackgroundThread();

    return mLoggingInfo.backgroundChildLoggingId();
  }

  int64_t NextTransactionSN(IDBTransaction::Mode aMode) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mLoggingInfo.nextTransactionSerialNumber() < INT64_MAX);
    MOZ_ASSERT(mLoggingInfo.nextVersionChangeTransactionSerialNumber() >
               INT64_MIN);

    if (aMode == IDBTransaction::Mode::VersionChange) {
      return mLoggingInfo.nextVersionChangeTransactionSerialNumber()--;
    }

    return mLoggingInfo.nextTransactionSerialNumber()++;
  }

  uint64_t NextRequestSN() {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mLoggingInfo.nextRequestSerialNumber() < UINT64_MAX);

    return mLoggingInfo.nextRequestSerialNumber()++;
  }

  NS_INLINE_DECL_REFCOUNTING(DatabaseLoggingInfo)

 private:
  ~DatabaseLoggingInfo();
};

class QuotaClient final : public mozilla::dom::quota::Client {
  static QuotaClient* sInstance;

  nsCOMPtr<nsIEventTarget> mBackgroundThread;
  nsCOMPtr<nsITimer> mDeleteTimer;
  nsTArray<RefPtr<Maintenance>> mMaintenanceQueue;
  RefPtr<Maintenance> mCurrentMaintenance;
  RefPtr<nsThreadPool> mMaintenanceThreadPool;
  nsClassHashtable<nsRefPtrHashKey<FileManager>, nsTArray<int64_t>>
      mPendingDeleteInfos;
  FlippedOnce<false> mShutdownRequested;

 public:
  QuotaClient();

  static QuotaClient* GetInstance() {
    AssertIsOnBackgroundThread();

    return sInstance;
  }

  static bool IsShuttingDownOnBackgroundThread() {
    AssertIsOnBackgroundThread();

    if (sInstance) {
      return sInstance->IsShuttingDown();
    }

    return QuotaManager::IsShuttingDown();
  }

  static bool IsShuttingDownOnNonBackgroundThread() {
    MOZ_ASSERT(!IsOnBackgroundThread());

    return QuotaManager::IsShuttingDown();
  }

  nsIEventTarget* BackgroundThread() const {
    MOZ_ASSERT(mBackgroundThread);
    return mBackgroundThread;
  }

  bool IsShuttingDown() const {
    AssertIsOnBackgroundThread();

    return mShutdownRequested;
  }

  nsresult AsyncDeleteFile(FileManager* aFileManager, int64_t aFileId);

  nsresult FlushPendingFileDeletions();

  RefPtr<Maintenance> GetCurrentMaintenance() const {
    return mCurrentMaintenance;
  }

  void NoteFinishedMaintenance(Maintenance* aMaintenance) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(aMaintenance);
    MOZ_ASSERT(mCurrentMaintenance == aMaintenance);

    mCurrentMaintenance = nullptr;
    ProcessMaintenanceQueue();
  }

  nsThreadPool* GetOrCreateThreadPool();

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::QuotaClient,
                                        override)

  mozilla::dom::quota::Client::Type GetType() override;

  nsresult UpgradeStorageFrom1_0To2_0(nsIFile* aDirectory) override;

  nsresult UpgradeStorageFrom2_1To2_2(nsIFile* aDirectory) override;

  Result<UsageInfo, nsresult> InitOrigin(PersistenceType aPersistenceType,
                                         const GroupAndOrigin& aGroupAndOrigin,
                                         const AtomicBool& aCanceled) override;

  nsresult InitOriginWithoutTracking(PersistenceType aPersistenceType,
                                     const GroupAndOrigin& aGroupAndOrigin,
                                     const AtomicBool& aCanceled) override;

  Result<UsageInfo, nsresult> GetUsageForOrigin(
      PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin,
      const AtomicBool& aCanceled) override;

  void OnOriginClearCompleted(PersistenceType aPersistenceType,
                              const nsACString& aOrigin) override;

  void ReleaseIOThreadObjects() override;

  void AbortOperations(const nsACString& aOrigin) override;

  void AbortOperationsForProcess(ContentParentId aContentParentId) override;

  void StartIdleMaintenance() override;

  void StopIdleMaintenance() override;

  void ShutdownWorkThreads() override;

 private:
  ~QuotaClient() override;

  void ShutdownTimedOut();

  static void DeleteTimerCallback(nsITimer* aTimer, void* aClosure);

  Result<nsCOMPtr<nsIFile>, nsresult> GetDirectory(
      PersistenceType aPersistenceType, const nsACString& aOrigin);

  struct SubdirectoriesToProcessAndDatabaseFilenames {
    AutoTArray<nsString, 20> subdirsToProcess;
    nsTHashtable<nsStringHashKey> databaseFilenames{20};
  };

  struct SubdirectoriesToProcessAndDatabaseFilenamesAndObsoleteFilenames {
    AutoTArray<nsString, 20> subdirsToProcess;
    nsTHashtable<nsStringHashKey> databaseFilenames{20};
    nsTHashtable<nsStringHashKey> obsoleteFilenames{20};
  };

  enum class ObsoleteFilenamesHandling { Include, Omit };

  template <ObsoleteFilenamesHandling ObsoleteFilenames>
  using GetDatabaseFilenamesResult = std::conditional_t<
      ObsoleteFilenames == ObsoleteFilenamesHandling::Include,
      SubdirectoriesToProcessAndDatabaseFilenamesAndObsoleteFilenames,
      SubdirectoriesToProcessAndDatabaseFilenames>;

  // Returns a two-part or three-part structure:
  //
  // The first part is an array of subdirectories to process.
  //
  // The second part is a hashtable of database filenames.
  //
  // When ObsoleteFilenames is ObsoleteFilenamesHandling::Include, will also
  // collect files based on the marker files. For now,
  // GetUsageForOriginInternal() is the only consumer of this result because it
  // checks those unfinished deletion and clean them up after that.
  template <ObsoleteFilenamesHandling ObsoleteFilenames =
                ObsoleteFilenamesHandling::Omit>
  Result<GetDatabaseFilenamesResult<ObsoleteFilenames>, nsresult>
  GetDatabaseFilenames(nsIFile& aDirectory, const AtomicBool& aCanceled);

  nsresult GetUsageForOriginInternal(PersistenceType aPersistenceType,
                                     const GroupAndOrigin& aGroupAndOrigin,
                                     const AtomicBool& aCanceled,
                                     bool aInitializing, UsageInfo* aUsageInfo);

  // Runs on the PBackground thread. Checks to see if there's a queued
  // Maintenance to run.
  void ProcessMaintenanceQueue();

  template <typename Condition>
  static void InvalidateLiveDatabasesMatching(const Condition& aCondition);
};

class DeleteFilesRunnable final : public Runnable,
                                  public OpenDirectoryListener {
  typedef mozilla::dom::quota::DirectoryLock DirectoryLock;

  enum State {
    // Just created on the PBackground thread. Next step is
    // State_DirectoryOpenPending.
    State_Initial,

    // Waiting for directory open allowed on the main thread. The next step is
    // State_DatabaseWorkOpen.
    State_DirectoryOpenPending,

    // Waiting to do/doing work on the QuotaManager IO thread. The next step is
    // State_UnblockingOpen.
    State_DatabaseWorkOpen,

    // Notifying the QuotaManager that it can proceed to the next operation on
    // the main thread. Next step is State_Completed.
    State_UnblockingOpen,

    // All done.
    State_Completed
  };

  nsCOMPtr<nsIEventTarget> mOwningEventTarget;
  SafeRefPtr<FileManager> mFileManager;
  RefPtr<DirectoryLock> mDirectoryLock;
  nsTArray<int64_t> mFileIds;
  State mState;

 public:
  DeleteFilesRunnable(SafeRefPtr<FileManager> aFileManager,
                      nsTArray<int64_t>&& aFileIds);

  void RunImmediately();

 private:
  ~DeleteFilesRunnable() = default;

  void Open();

  void DoDatabaseWork();

  void Finish();

  void UnblockOpen();

  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_NSIRUNNABLE

  // OpenDirectoryListener overrides.
  virtual void DirectoryLockAcquired(DirectoryLock* aLock) override;

  virtual void DirectoryLockFailed() override;
};

class Maintenance final : public Runnable, public OpenDirectoryListener {
  struct DirectoryInfo final {
    InitializedOnce<const GroupAndOrigin> mGroupAndOrigin;
    InitializedOnce<const nsTArray<nsString>> mDatabasePaths;
    const PersistenceType mPersistenceType;

    DirectoryInfo(PersistenceType aPersistenceType,
                  GroupAndOrigin aGroupAndOrigin,
                  nsTArray<nsString>&& aDatabasePaths);

    DirectoryInfo(const DirectoryInfo& aOther) = delete;
    DirectoryInfo(DirectoryInfo&& aOther) = delete;

    ~DirectoryInfo() { MOZ_COUNT_DTOR(Maintenance::DirectoryInfo); }
  };

  enum class State {
    // Newly created on the PBackground thread. Will proceed immediately or be
    // added to the maintenance queue. The next step is either
    // DirectoryOpenPending if IndexedDatabaseManager is running, or
    // CreateIndexedDatabaseManager if not.
    Initial = 0,

    // Create IndexedDatabaseManager on the main thread. The next step is either
    // Finishing if IndexedDatabaseManager initialization fails, or
    // IndexedDatabaseManagerOpen if initialization succeeds.
    CreateIndexedDatabaseManager,

    // Call OpenDirectory() on the PBackground thread. The next step is
    // DirectoryOpenPending.
    IndexedDatabaseManagerOpen,

    // Waiting for directory open allowed on the PBackground thread. The next
    // step is either Finishing if directory lock failed to acquire, or
    // DirectoryWorkOpen if directory lock is acquired.
    DirectoryOpenPending,

    // Waiting to do/doing work on the QuotaManager IO thread. The next step is
    // BeginDatabaseMaintenance.
    DirectoryWorkOpen,

    // Dispatching a runnable for each database on the PBackground thread. The
    // next state is either WaitingForDatabaseMaintenancesToComplete if at least
    // one runnable has been dispatched, or Finishing otherwise.
    BeginDatabaseMaintenance,

    // Waiting for DatabaseMaintenance to finish on maintenance thread pool.
    // The next state is Finishing if the last runnable has finished.
    WaitingForDatabaseMaintenancesToComplete,

    // Waiting to finish/finishing on the PBackground thread. The next step is
    // Completed.
    Finishing,

    // All done.
    Complete
  };

  RefPtr<QuotaClient> mQuotaClient;
  PRTime mStartTime;
  RefPtr<DirectoryLock> mDirectoryLock;
  nsTArray<DirectoryInfo> mDirectoryInfos;
  nsDataHashtable<nsStringHashKey, DatabaseMaintenance*> mDatabaseMaintenances;
  nsresult mResultCode;
  Atomic<bool> mAborted;
  State mState;

 public:
  explicit Maintenance(QuotaClient* aQuotaClient)
      : Runnable("dom::indexedDB::Maintenance"),
        mQuotaClient(aQuotaClient),
        mStartTime(PR_Now()),
        mResultCode(NS_OK),
        mAborted(false),
        mState(State::Initial) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(aQuotaClient);
    MOZ_ASSERT(QuotaClient::GetInstance() == aQuotaClient);
    MOZ_ASSERT(mStartTime);
  }

  nsIEventTarget* BackgroundThread() const {
    MOZ_ASSERT(mQuotaClient);
    return mQuotaClient->BackgroundThread();
  }

  PRTime StartTime() const { return mStartTime; }

  bool IsAborted() const { return mAborted; }

  void RunImmediately() {
    MOZ_ASSERT(mState == State::Initial);

    Unused << this->Run();
  }

  void Abort() {
    AssertIsOnBackgroundThread();

    mAborted = true;
  }

  void RegisterDatabaseMaintenance(DatabaseMaintenance* aDatabaseMaintenance);

  void UnregisterDatabaseMaintenance(DatabaseMaintenance* aDatabaseMaintenance);

  RefPtr<DatabaseMaintenance> GetDatabaseMaintenance(
      const nsAString& aDatabasePath) const {
    AssertIsOnBackgroundThread();

    return mDatabaseMaintenances.Get(aDatabasePath);
  }

  void Stringify(nsACString& aResult) const;

 private:
  ~Maintenance() override {
    MOZ_ASSERT(mState == State::Complete);
    MOZ_ASSERT(!mDatabaseMaintenances.Count());
  }

  // Runs on the PBackground thread. Checks if IndexedDatabaseManager is
  // running. Calls OpenDirectory() or dispatches to the main thread on which
  // CreateIndexedDatabaseManager() is called.
  nsresult Start();

  // Runs on the main thread. Once IndexedDatabaseManager is created it will
  // dispatch to the PBackground thread on which OpenDirectory() is called.
  nsresult CreateIndexedDatabaseManager();

  // Runs on the PBackground thread. Once QuotaManager has given a lock it will
  // call DirectoryOpen().
  nsresult OpenDirectory();

  // Runs on the PBackground thread. Dispatches to the QuotaManager I/O thread.
  nsresult DirectoryOpen();

  // Runs on the QuotaManager I/O thread. Once it finds databases it will
  // dispatch to the PBackground thread on which BeginDatabaseMaintenance()
  // is called.
  nsresult DirectoryWork();

  // Runs on the PBackground thread. It dispatches a runnable for each database.
  nsresult BeginDatabaseMaintenance();

  // Runs on the PBackground thread. Called when the maintenance is finished or
  // if any of above methods fails.
  void Finish();

  // We need to declare refcounting unconditionally, because
  // OpenDirectoryListener has pure-virtual refcounting.
  NS_DECL_ISUPPORTS_INHERITED

  NS_DECL_NSIRUNNABLE

  // OpenDirectoryListener overrides.
  void DirectoryLockAcquired(DirectoryLock* aLock) override;

  void DirectoryLockFailed() override;
};

Maintenance::DirectoryInfo::DirectoryInfo(PersistenceType aPersistenceType,
                                          GroupAndOrigin aGroupAndOrigin,
                                          nsTArray<nsString>&& aDatabasePaths)
    : mGroupAndOrigin(std::move(aGroupAndOrigin)),
      mDatabasePaths(std::move(aDatabasePaths)),
      mPersistenceType(aPersistenceType) {
  MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID);
  MOZ_ASSERT(!mGroupAndOrigin->mGroup.IsEmpty());
  MOZ_ASSERT(!mGroupAndOrigin->mOrigin.IsEmpty());
#ifdef DEBUG
  MOZ_ASSERT(!mDatabasePaths->IsEmpty());
  for (const nsString& databasePath : *mDatabasePaths) {
    MOZ_ASSERT(!databasePath.IsEmpty());
  }
#endif

  MOZ_COUNT_CTOR(Maintenance::DirectoryInfo);
}

class DatabaseMaintenance final : public Runnable {
  // The minimum amount of time that has passed since the last vacuum before we
  // will attempt to analyze the database for fragmentation.
  static const PRTime kMinVacuumAge =
      PRTime(PR_USEC_PER_SEC) * 60 * 60 * 24 * 7;

  // If the percent of database pages that are not in contiguous order is higher
  // than this percentage we will attempt a vacuum.
  static const int32_t kPercentUnorderedThreshold = 30;

  // If the percent of file size growth since the last vacuum is higher than
  // this percentage we will attempt a vacuum.
  static const int32_t kPercentFileSizeGrowthThreshold = 10;

  // The number of freelist pages beyond which we will favor an incremental
  // vacuum over a full vacuum.
  static const int32_t kMaxFreelistThreshold = 5;

  // If the percent of unused file bytes in the database exceeds this percentage
  // then we will attempt a full vacuum.
  static const int32_t kPercentUnusedThreshold = 20;

  class AutoProgressHandler;

  enum class MaintenanceAction { Nothing = 0, IncrementalVacuum, FullVacuum };

  RefPtr<Maintenance> mMaintenance;
  RefPtr<DirectoryLock> mDirectoryLock;
  const GroupAndOrigin mGroupAndOrigin;
  const nsString mDatabasePath;
  int64_t mDirectoryLockId;
  nsCOMPtr<nsIRunnable> mCompleteCallback;
  const PersistenceType mPersistenceType;
  const Maybe<CipherKey> mMaybeKey;

 public:
  DatabaseMaintenance(Maintenance* aMaintenance, DirectoryLock* aDirectoryLock,
                      PersistenceType aPersistenceType,
                      const GroupAndOrigin& aGroupAndOrigin,
                      const nsString& aDatabasePath,
                      const Maybe<CipherKey>& aMaybeKey)
      : Runnable("dom::indexedDB::DatabaseMaintenance"),
        mMaintenance(aMaintenance),
        mDirectoryLock(aDirectoryLock),
        mGroupAndOrigin(aGroupAndOrigin),
        mDatabasePath(aDatabasePath),
        mPersistenceType(aPersistenceType),
        mMaybeKey{aMaybeKey} {
    MOZ_ASSERT(aDirectoryLock);

    MOZ_ASSERT(mDirectoryLock->Id() >= 0);
    mDirectoryLockId = mDirectoryLock->Id();
  }

  const nsString& DatabasePath() const { return mDatabasePath; }

  void WaitForCompletion(nsIRunnable* aCallback) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(!mCompleteCallback);

    mCompleteCallback = aCallback;
  }

  void Stringify(nsACString& aResult) const;

 private:
  ~DatabaseMaintenance() override = default;

  // Runs on maintenance thread pool. Does maintenance on the database.
  void PerformMaintenanceOnDatabase();

  // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase.
  nsresult CheckIntegrity(mozIStorageConnection& aConnection, bool* aOk);

  // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase.
  nsresult DetermineMaintenanceAction(mozIStorageConnection& aConnection,
                                      nsIFile* aDatabaseFile,
                                      MaintenanceAction* aMaintenanceAction);

  // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase.
  void IncrementalVacuum(mozIStorageConnection& aConnection);

  // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase.
  void FullVacuum(mozIStorageConnection& aConnection, nsIFile* aDatabaseFile);

  // Runs on the PBackground thread. It dispatches a complete callback and
  // unregisters from Maintenance.
  void RunOnOwningThread();

  // Runs on maintenance thread pool. Once it performs database maintenance
  // it will dispatch to the PBackground thread on which RunOnOwningThread()
  // is called.
  void RunOnConnectionThread();

  NS_DECL_NSIRUNNABLE
};

class MOZ_STACK_CLASS DatabaseMaintenance::AutoProgressHandler final
    : public mozIStorageProgressHandler {
  Maintenance* mMaintenance;
  LazyInitializedOnce<const NotNull<mozIStorageConnection*>> mConnection;

  NS_DECL_OWNINGTHREAD

#ifdef DEBUG
  // This class is stack-based so we never actually allow AddRef/Release to do
  // anything. But we need to know if any consumer *thinks* that they have a
  // reference to this object so we track the reference countin DEBUG builds.
  nsrefcnt mDEBUGRefCnt;
#endif

 public:
  explicit AutoProgressHandler(Maintenance* aMaintenance)
      : mMaintenance(aMaintenance),
        mConnection()
#ifdef DEBUG
        ,
        mDEBUGRefCnt(0)
#endif
  {
    MOZ_ASSERT(!NS_IsMainThread());
    MOZ_ASSERT(!IsOnBackgroundThread());
    NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler);
    MOZ_ASSERT(aMaintenance);
  }

  ~AutoProgressHandler() {
    NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler);

    if (mConnection) {
      Unregister();
    }

    MOZ_ASSERT(!mDEBUGRefCnt);
  }

  nsresult Register(NotNull<mozIStorageConnection*> aConnection);

  // We don't want the mRefCnt member but this class does not "inherit"
  // nsISupports.
  NS_DECL_ISUPPORTS_INHERITED

 private:
  void Unregister();

  NS_DECL_MOZISTORAGEPROGRESSHANDLER

  // Not available for the heap!
  void* operator new(size_t) = delete;
  void* operator new[](size_t) = delete;
  void operator delete(void*) = delete;
  void operator delete[](void*) = delete;
};

#ifdef DEBUG

class DEBUGThreadSlower final : public nsIThreadObserver {
 public:
  DEBUGThreadSlower() {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(kDEBUGThreadSleepMS);
  }

  NS_DECL_ISUPPORTS

 private:
  ~DEBUGThreadSlower() { AssertIsOnBackgroundThread(); }

  NS_DECL_NSITHREADOBSERVER
};

#endif  // DEBUG

/*******************************************************************************
 * Helper classes
 ******************************************************************************/

// XXX Get rid of FileHelper and move the functions into FileManager.
// Then, FileManager::Get(Journal)Directory and FileManager::GetFileForId might
// eventually be made private.
class MOZ_STACK_CLASS FileHelper final {
  const SafeRefPtr<FileManager> mFileManager;

  LazyInitializedOnce<const NotNull<nsCOMPtr<nsIFile>>> mFileDirectory;
  LazyInitializedOnce<const NotNull<nsCOMPtr<nsIFile>>> mJournalDirectory;

  class ReadCallback;
  LazyInitializedOnce<const NotNull<RefPtr<ReadCallback>>> mReadCallback;

 public:
  explicit FileHelper(SafeRefPtr<FileManager>&& aFileManager)
      : mFileManager(std::move(aFileManager)) {
    MOZ_ASSERT(mFileManager);
  }

  nsresult Init();

  [[nodiscard]] nsCOMPtr<nsIFile> GetFile(const FileInfo& aFileInfo);

  [[nodiscard]] nsCOMPtr<nsIFile> GetJournalFile(const FileInfo& aFileInfo);

  nsresult CreateFileFromStream(nsIFile& aFile, nsIFile& aJournalFile,
                                nsIInputStream& aInputStream, bool aCompress,
                                const Maybe<CipherKey>& aMaybeKey);

 private:
  nsresult SyncCopy(nsIInputStream& aInputStream,
                    nsIOutputStream& aOutputStream, char* aBuffer,
                    uint32_t aBufferSize);

  nsresult SyncRead(nsIInputStream& aInputStream, char* aBuffer,
                    uint32_t aBufferSize, uint32_t* aRead);
};

/*******************************************************************************
 * Helper Functions
 ******************************************************************************/

bool GetFilenameBase(const nsAString& aFilename, const nsAString& aSuffix,
                     nsDependentSubstring& aFilenameBase) {
  MOZ_ASSERT(!aFilename.IsEmpty());
  MOZ_ASSERT(aFilenameBase.IsEmpty());

  if (!StringEndsWith(aFilename, aSuffix) ||
      aFilename.Length() == aSuffix.Length()) {
    return false;
  }

  MOZ_ASSERT(aFilename.Length() > aSuffix.Length());

  aFilenameBase.Rebind(aFilename, 0, aFilename.Length() - aSuffix.Length());
  return true;
}

class EncryptedFileBlobImpl final : public FileBlobImpl {
 public:
  EncryptedFileBlobImpl(const nsCOMPtr<nsIFile>& aNativeFile,
                        const FileInfo::IdType aId, const CipherKey& aKey)
      : FileBlobImpl{aNativeFile}, mKey{aKey} {
    SetFileId(aId);
  }

  uint64_t GetSize(ErrorResult& aRv) override {
    nsCOMPtr<nsIInputStream> inputStream;
    CreateInputStream(getter_AddRefs(inputStream), aRv);

    if (aRv.Failed()) {
      return 0;
    }

    MOZ_ASSERT(inputStream);

    IDB_TRY_RETURN(MOZ_TO_RESULT_INVOKE(inputStream, Available), 0,
                   [&aRv](const nsresult rv) { aRv = rv; });
  }

  void CreateInputStream(nsIInputStream** aInputStream,
                         ErrorResult& aRv) override {
    nsCOMPtr<nsIInputStream> baseInputStream;
    FileBlobImpl::CreateInputStream(getter_AddRefs(baseInputStream), aRv);
    if (NS_WARN_IF(aRv.Failed())) {
      return;
    }

    *aInputStream =
        MakeAndAddRef<DecryptingInputStream<IndexedDBCipherStrategy>>(
            WrapNotNull(std::move(baseInputStream)), kEncryptedStreamBlockSize,
            mKey)
            .take();
  }

  void GetBlobImplType(nsAString& aBlobImplType) const override {
    aBlobImplType = u"EncryptedFileBlobImpl"_ns;
  }

  already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength,
                                         const nsAString& aContentType,
                                         ErrorResult& aRv) override {
    MOZ_CRASH("Not implemented because this should be unreachable.");
  }

 private:
  const CipherKey& mKey;
};

RefPtr<BlobImpl> CreateFileBlobImpl(const Database& aDatabase,
                                    const nsCOMPtr<nsIFile>& aNativeFile,
                                    const FileInfo::IdType aId) {
  const auto& maybeKey = aDatabase.MaybeKeyRef();
  if (maybeKey) {
    return MakeRefPtr<EncryptedFileBlobImpl>(aNativeFile, aId, *maybeKey);
  }

  auto impl = MakeRefPtr<FileBlobImpl>(aNativeFile);
  impl->SetFileId(aId);
  return impl;
}

Result<nsTArray<SerializedStructuredCloneFile>, nsresult>
SerializeStructuredCloneFiles(PBackgroundParent* aBackgroundActor,
                              const SafeRefPtr<Database>& aDatabase,
                              const nsTArray<StructuredCloneFileParent>& aFiles,
                              bool aForPreprocess) {
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(aBackgroundActor);
  MOZ_ASSERT(aDatabase);

  if (aFiles.IsEmpty()) {
    return nsTArray<SerializedStructuredCloneFile>{};
  }

  const nsCOMPtr<nsIFile> directory =
      aDatabase->GetFileManager().GetCheckedDirectory();
  IDB_TRY(OkIf(directory), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR),
          IDB_REPORT_INTERNAL_ERR_LAMBDA);

  nsTArray<SerializedStructuredCloneFile> serializedStructuredCloneFiles;
  IDB_TRY(OkIf(serializedStructuredCloneFiles.SetCapacity(aFiles.Length(),
                                                          fallible)),
          Err(NS_ERROR_OUT_OF_MEMORY));

  IDB_TRY(TransformIfAbortOnErr(
      aFiles, MakeBackInserter(serializedStructuredCloneFiles),
      [aForPreprocess](const auto& file) {
        return !aForPreprocess ||
               file.Type() == StructuredCloneFileBase::eStructuredClone;
      },
      [&directory, &aDatabase, aBackgroundActor, aForPreprocess](
          const auto& file) -> Result<SerializedStructuredCloneFile, nsresult> {
        const int64_t fileId = file.FileInfo().Id();
        MOZ_ASSERT(fileId > 0);

        const nsCOMPtr<nsIFile> nativeFile =
            mozilla::dom::indexedDB::FileManager::GetCheckedFileForId(directory,
                                                                      fileId);
        IDB_TRY(OkIf(nativeFile), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR),
                IDB_REPORT_INTERNAL_ERR_LAMBDA);

        switch (file.Type()) {
          case StructuredCloneFileBase::eStructuredClone:
            if (!aForPreprocess) {
              return SerializedStructuredCloneFile{
                  null_t(), StructuredCloneFileBase::eStructuredClone};
            }

            [[fallthrough]];

          case StructuredCloneFileBase::eBlob: {
            const auto impl = CreateFileBlobImpl(*aDatabase, nativeFile,
                                                 file.FileInfo().Id());

            IPCBlob ipcBlob;

            // This can only fail if the child has crashed.
            IDB_TRY(IPCBlobUtils::Serialize(impl, aBackgroundActor, ipcBlob),
                    Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR),
                    IDB_REPORT_INTERNAL_ERR_LAMBDA);

            aDatabase->MapBlob(ipcBlob, file.FileInfoPtr());

            return SerializedStructuredCloneFile{ipcBlob, file.Type()};
          }

          case StructuredCloneFileBase::eMutableFile: {
            if (aDatabase->IsFileHandleDisabled()) {
              return SerializedStructuredCloneFile{
                  null_t(), StructuredCloneFileBase::eMutableFile};
            }

            const RefPtr<MutableFile> actor = MutableFile::Create(
                nativeFile, aDatabase.clonePtr(), file.FileInfoPtr());
            IDB_TRY(OkIf(actor), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR),
                    IDB_REPORT_INTERNAL_ERR_LAMBDA);

            // Transfer ownership to IPDL.
            actor->SetActorAlive();

            if (!aDatabase->SendPBackgroundMutableFileConstructor(actor, u""_ns,
                                                                  u""_ns)) {
              // This can only fail if the child has crashed.
              IDB_REPORT_INTERNAL_ERR();
              return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
            }

            return SerializedStructuredCloneFile{
                actor.get(), StructuredCloneFileBase::eMutableFile};
          }

          case StructuredCloneFileBase::eWasmBytecode:
          case StructuredCloneFileBase::eWasmCompiled: {
            // Set file() to null, support for storing WebAssembly.Modules has
            // been removed in bug 1469395. Support for de-serialization of
            // WebAssembly.Modules modules has been removed in bug 1561876. Full
            // removal is tracked in bug 1487479.

            return SerializedStructuredCloneFile{null_t(), file.Type()};
          }

          default:
            MOZ_CRASH("Should never get here!");
        }
      }));

  return std::move(serializedStructuredCloneFiles);
}

enum struct Idempotency { Yes, No };

template <typename R>
Result<Maybe<R>, nsresult> IdempotentFilter(const nsresult aRv) {
  if (aRv == NS_ERROR_FILE_NOT_FOUND ||
      aRv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
    return Maybe<R>{};
  }

  return Err(aRv);
}

template <typename R>
auto MakeMaybeIdempotentFilter(const Idempotency aIdempotent)
    -> Result<Maybe<R>, nsresult> (*)(nsresult) {
  if (aIdempotent == Idempotency::Yes) {
    return IdempotentFilter<R>;
  }

  return [](const nsresult rv) { return Result<Maybe<R>, nsresult>{Err(rv)}; };
}

// Delete a file, decreasing the quota usage as appropriate. If the file no
// longer exists but aIdempotent is true, success is returned, although quota
// usage can't be decreased. (With the assumption being that the file was
// already deleted prior to this logic running, and the non-existent file was no
// longer tracked by quota because it didn't exist at initialization time or a
// previous deletion call updated the usage.)
nsresult DeleteFile(nsIFile& aFile, QuotaManager* const aQuotaManager,
                    const PersistenceType aPersistenceType,
                    const GroupAndOrigin& aGroupAndOrigin,
                    const Idempotency aIdempotent) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());

  IDB_TRY_INSPECT(
      const auto& fileSize,
      ([aQuotaManager, &aFile,
        aIdempotent]() -> Result<Maybe<int64_t>, nsresult> {
        if (aQuotaManager) {
          IDB_TRY_INSPECT(
              const Maybe<int64_t>& fileSize,
              MOZ_TO_RESULT_INVOKE(aFile, GetFileSize)
                  .map([](const int64_t val) { return Some(val); })
                  .orElse(MakeMaybeIdempotentFilter<int64_t>(aIdempotent)));

          // XXX Can we really assert that the file size is not 0 if
          // it existed? This might be violated by external
          // influences.
          MOZ_ASSERT(!fileSize || fileSize.value() >= 0);

          return fileSize;
        }

        return Some(int64_t(0));
      }()));

  if (!fileSize) {
    return NS_OK;
  }

  IDB_TRY_INSPECT(const auto& didExist,
                  ToResult(aFile.Remove(false))
                      .map(Some<Ok>)
                      .orElse(MakeMaybeIdempotentFilter<Ok>(aIdempotent)));

  if (!didExist) {
    // XXX If we get here, this means that the file still existed when we
    // queried its size, but no longer when we tried to remove it. Not sure if
    // this should really be silently accepted in idempotent mode.
    return NS_OK;
  }

  if (fileSize.value() > 0) {
    MOZ_ASSERT(aQuotaManager);

    aQuotaManager->DecreaseUsageForOrigin(aPersistenceType, aGroupAndOrigin,
                                          Client::IDB, fileSize.value());
  }

  return NS_OK;
}

nsresult DeleteFile(nsIFile& aDirectory, const nsAString& aFilename,
                    QuotaManager* const aQuotaManager,
                    const PersistenceType aPersistenceType,
                    const GroupAndOrigin& aGroupAndOrigin,
                    const Idempotency aIdempotent) {
  AssertIsOnIOThread();
  MOZ_ASSERT(!aFilename.IsEmpty());

  IDB_TRY_INSPECT(const auto& file, CloneFileAndAppend(aDirectory, aFilename));

  return DeleteFile(*file, aQuotaManager, aPersistenceType, aGroupAndOrigin,
                    aIdempotent);
}

nsresult DeleteFilesNoQuota(nsIFile* aDirectory, const nsAString& aFilename) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aDirectory);
  MOZ_ASSERT(!aFilename.IsEmpty());

  // The current using function hasn't initialized the origin, so in here we
  // don't update the size of origin. Adding this assertion for preventing from
  // misusing.
  DebugOnly<QuotaManager*> quotaManager = QuotaManager::Get();
  MOZ_ASSERT(!quotaManager->IsTemporaryStorageInitialized());

  IDB_TRY_INSPECT(const auto& file, CloneFileAndAppend(*aDirectory, aFilename));

  IDB_TRY_INSPECT(
      const auto& didExist,
      ToResult(file->Remove(true)).map(Some<Ok>).orElse(IdempotentFilter<Ok>));

  Unused << didExist;

  return NS_OK;
}

// CreateMarkerFile and RemoveMarkerFile are a pair of functions to indicate
// whether having removed all the files successfully. The marker file should
// be checked before executing the next operation or initialization.
Result<nsCOMPtr<nsIFile>, nsresult> CreateMarkerFile(
    nsIFile& aBaseDirectory, const nsAString& aDatabaseNameBase) {
  AssertIsOnIOThread();
  MOZ_ASSERT(!aDatabaseNameBase.IsEmpty());

  IDB_TRY_INSPECT(
      const auto& markerFile,
      CloneFileAndAppend(aBaseDirectory,
                         kIdbDeletionMarkerFilePrefix + aDatabaseNameBase));

  IDB_TRY(
      MOZ_TO_RESULT_INVOKE(markerFile, Create, nsIFile::NORMAL_FILE_TYPE, 0644)
          .orElse(ErrToDefaultOkOrErr<NS_ERROR_FILE_ALREADY_EXISTS, Ok>));

  return markerFile;
}

nsresult RemoveMarkerFile(nsIFile* aMarkerFile) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aMarkerFile);

  DebugOnly<bool> exists;
  MOZ_ASSERT(NS_SUCCEEDED(aMarkerFile->Exists(&exists)));
  MOZ_ASSERT(exists);

  IDB_TRY(aMarkerFile->Remove(false));

  return NS_OK;
}

Result<Ok, nsresult> DeleteFileManagerDirectory(
    nsIFile& aFileManagerDirectory, QuotaManager* aQuotaManager,
    const PersistenceType aPersistenceType,
    const GroupAndOrigin& aGroupAndOrigin) {
  if (!aQuotaManager) {
    IDB_TRY(aFileManagerDirectory.Remove(true));

    return Ok{};
  }

  IDB_TRY_UNWRAP(auto fileUsage, FileManager::GetUsage(&aFileManagerDirectory));

  uint64_t usageValue = fileUsage.GetValue().valueOr(0);

  auto res =
      MOZ_TO_RESULT_INVOKE(aFileManagerDirectory, Remove, true)
          .orElse([&usageValue, &aFileManagerDirectory](nsresult rv) {
            // We may have deleted some files, try to update quota
            // information before returning the error.

            // failures of GetUsage are intentionally ignored
            Unused << FileManager::GetUsage(&aFileManagerDirectory)
                          .andThen([&usageValue](const auto& newFileUsage) {
                            const auto newFileUsageValue =
                                newFileUsage.GetValue().valueOr(0);
                            MOZ_ASSERT(newFileUsageValue <= usageValue);
                            usageValue -= newFileUsageValue;

                            // XXX andThen does not support void return
                            // values right now, we must return a Result
                            return Result<Ok, nsresult>{Ok{}};
                          });

            return Result<Ok, nsresult>{Err(rv)};
          });

  if (usageValue) {
    aQuotaManager->DecreaseUsageForOrigin(aPersistenceType, aGroupAndOrigin,
                                          Client::IDB, usageValue);
  }

  return res;
}

// Idempotently delete all the parts of an IndexedDB database including its
// SQLite database file, its WAL journal, it's shared-memory file, and its
// Blob/Files sub-directory. A marker file is created prior to performing the
// deletion so that in the event we crash or fail to successfully delete the
// database and its files, we will re-attempt the deletion the next time the
// origin is initialized using this method. Because this means the method may be
// called on a partially deleted database, this method uses DeleteFile which
// succeeds when the file we ask it to delete does not actually exist. The
// marker file is removed once deletion has successfully completed.
nsresult RemoveDatabaseFilesAndDirectory(nsIFile& aBaseDirectory,
                                         const nsAString& aDatabaseFilenameBase,
                                         QuotaManager* aQuotaManager,
                                         const PersistenceType aPersistenceType,
                                         const GroupAndOrigin& aGroupAndOrigin,
                                         const nsAString& aDatabaseName) {
  AssertIsOnIOThread();
  MOZ_ASSERT(!aDatabaseFilenameBase.IsEmpty());

  AUTO_PROFILER_LABEL("RemoveDatabaseFilesAndDirectory", DOM);

  IDB_TRY_UNWRAP(auto markerFile,
                 CreateMarkerFile(aBaseDirectory, aDatabaseFilenameBase));

  // The database file counts towards quota.
  IDB_TRY(DeleteFile(aBaseDirectory, aDatabaseFilenameBase + kSQLiteSuffix,
                     aQuotaManager, aPersistenceType, aGroupAndOrigin,
                     Idempotency::Yes));

  // .sqlite-journal files don't count towards quota.
  IDB_TRY(DeleteFile(aBaseDirectory,
                     aDatabaseFilenameBase + kSQLiteJournalSuffix,
                     /* doesn't count */ nullptr, aPersistenceType,
                     aGroupAndOrigin, Idempotency::Yes));

  // .sqlite-shm files don't count towards quota.
  IDB_TRY(DeleteFile(aBaseDirectory, aDatabaseFilenameBase + kSQLiteSHMSuffix,
                     /* doesn't count */ nullptr, aPersistenceType,
                     aGroupAndOrigin, Idempotency::Yes));

  // .sqlite-wal files do count towards quota.
  IDB_TRY(DeleteFile(aBaseDirectory, aDatabaseFilenameBase + kSQLiteWALSuffix,
                     aQuotaManager, aPersistenceType, aGroupAndOrigin,
                     Idempotency::Yes));

  // The files directory counts towards quota.
  IDB_TRY_INSPECT(
      const auto& fmDirectory,
      CloneFileAndAppend(aBaseDirectory, aDatabaseFilenameBase +
                                             kFileManagerDirectoryNameSuffix));

  IDB_TRY_INSPECT(const bool& exists,
                  MOZ_TO_RESULT_INVOKE(fmDirectory, Exists));

  if (exists) {
    IDB_TRY_INSPECT(const bool& isDirectory,
                    MOZ_TO_RESULT_INVOKE(fmDirectory, IsDirectory));

    IDB_TRY(OkIf(isDirectory), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);

    IDB_TRY(DeleteFileManagerDirectory(*fmDirectory, aQuotaManager,
                                       aPersistenceType, aGroupAndOrigin));
  }

  IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get();
  MOZ_ASSERT_IF(aQuotaManager, mgr);

  if (mgr) {
    mgr->InvalidateFileManager(aPersistenceType, aGroupAndOrigin.mOrigin,
                               aDatabaseName);
  }

  IDB_TRY(RemoveMarkerFile(markerFile));

  return NS_OK;
}

/*******************************************************************************
 * Globals
 ******************************************************************************/

// Counts the number of "live" Factory, FactoryOp and Database instances.
uint64_t gBusyCount = 0;

typedef nsTArray<CheckedUnsafePtr<FactoryOp>> FactoryOpArray;

StaticAutoPtr<FactoryOpArray> gFactoryOps;

// Maps a database id to information about live database actors.
typedef nsClassHashtable<nsCStringHashKey, DatabaseActorInfo>
    DatabaseActorHashtable;

StaticAutoPtr<DatabaseActorHashtable> gLiveDatabaseHashtable;

using PrivateBrowsingInfoHashtable =
    nsDataHashtable<nsCStringHashKey, CipherKey>;
// XXX Maybe we can avoid a mutex here by moving all accesses to the background
// thread.
StaticAutoPtr<DataMutex<PrivateBrowsingInfoHashtable>>
    gPrivateBrowsingInfoHashtable;

StaticRefPtr<ConnectionPool> gConnectionPool;

StaticRefPtr<FileHandleThreadPool> gFileHandleThreadPool;

typedef nsDataHashtable<nsIDHashKey, DatabaseLoggingInfo*>
    DatabaseLoggingInfoHashtable;

StaticAutoPtr<DatabaseLoggingInfoHashtable> gLoggingInfoHashtable;

typedef nsDataHashtable<nsUint32HashKey, uint32_t> TelemetryIdHashtable;

StaticAutoPtr<TelemetryIdHashtable> gTelemetryIdHashtable;

// Protects all reads and writes to gTelemetryIdHashtable.
StaticAutoPtr<Mutex> gTelemetryIdMutex;

#ifdef DEBUG

StaticRefPtr<DEBUGThreadSlower> gDEBUGThreadSlower;

#endif  // DEBUG

void IncreaseBusyCount() {
  AssertIsOnBackgroundThread();

  // If this is the first instance then we need to do some initialization.
  if (!gBusyCount) {
    MOZ_ASSERT(!gFactoryOps);
    gFactoryOps = new FactoryOpArray();

    MOZ_ASSERT(!gLiveDatabaseHashtable);
    gLiveDatabaseHashtable = new DatabaseActorHashtable();

    MOZ_ASSERT(!gPrivateBrowsingInfoHashtable);
    gPrivateBrowsingInfoHashtable = new DataMutex<PrivateBrowsingInfoHashtable>(
        "gPrivateBrowsingInfoHashtable");

    MOZ_ASSERT(!gLoggingInfoHashtable);
    gLoggingInfoHashtable = new DatabaseLoggingInfoHashtable();

#ifdef DEBUG
    if (kDEBUGThreadPriority != nsISupportsPriority::PRIORITY_NORMAL) {
      NS_WARNING(
          "PBackground thread debugging enabled, priority has been "
          "modified!");
      nsCOMPtr<nsISupportsPriority> thread =
          do_QueryInterface(NS_GetCurrentThread());
      MOZ_ASSERT(thread);

      MOZ_ALWAYS_SUCCEEDS(thread->SetPriority(kDEBUGThreadPriority));
    }

    if (kDEBUGThreadSleepMS) {
      NS_WARNING(
          "PBackground thread debugging enabled, sleeping after every "
          "event!");
      nsCOMPtr<nsIThreadInternal> thread =
          do_QueryInterface(NS_GetCurrentThread());
      MOZ_ASSERT(thread);

      gDEBUGThreadSlower = new DEBUGThreadSlower();

      MOZ_ALWAYS_SUCCEEDS(thread->AddObserver(gDEBUGThreadSlower));
    }
#endif  // DEBUG
  }

  gBusyCount++;
}

void DecreaseBusyCount() {
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(gBusyCount);

  // Clean up if there are no more instances.
  if (--gBusyCount == 0) {
    MOZ_ASSERT(gLoggingInfoHashtable);
    gLoggingInfoHashtable = nullptr;

    MOZ_ASSERT(gLiveDatabaseHashtable);
    MOZ_ASSERT(!gLiveDatabaseHashtable->Count());
    gLiveDatabaseHashtable = nullptr;

    MOZ_ASSERT(gPrivateBrowsingInfoHashtable);
    // XXX After we add the private browsing session end listener, we can assert
    // this.
    // MOZ_ASSERT(!gPrivateBrowsingInfoHashtable->Count());
    gPrivateBrowsingInfoHashtable = nullptr;

    MOZ_ASSERT(gFactoryOps);
    MOZ_ASSERT(gFactoryOps->IsEmpty());
    gFactoryOps = nullptr;

#ifdef DEBUG
    if (kDEBUGThreadPriority != nsISupportsPriority::PRIORITY_NORMAL) {
      nsCOMPtr<nsISupportsPriority> thread =
          do_QueryInterface(NS_GetCurrentThread());
      MOZ_ASSERT(thread);

      MOZ_ALWAYS_SUCCEEDS(
          thread->SetPriority(nsISupportsPriority::PRIORITY_NORMAL));
    }

    if (kDEBUGThreadSleepMS) {
      MOZ_ASSERT(gDEBUGThreadSlower);

      nsCOMPtr<nsIThreadInternal> thread =
          do_QueryInterface(NS_GetCurrentThread());
      MOZ_ASSERT(thread);

      MOZ_ALWAYS_SUCCEEDS(thread->RemoveObserver(gDEBUGThreadSlower));

      gDEBUGThreadSlower = nullptr;
    }
#endif  // DEBUG
  }
}

uint32_t TelemetryIdForFile(nsIFile* aFile) {
  // May be called on any thread!

  MOZ_ASSERT(aFile);
  MOZ_ASSERT(gTelemetryIdMutex);

  // The storage directory is structured like this:
  //
  //   <profile>/storage/<persistence>/<origin>/idb/<filename>.sqlite
  //
  // For the purposes of this function we're only concerned with the
  // <persistence>, <origin>, and <filename> pieces.

  nsString filename;
  MOZ_ALWAYS_SUCCEEDS(aFile->GetLeafName(filename));

  // Make sure we were given a database file.
  MOZ_ASSERT(StringEndsWith(filename, kSQLiteSuffix));

  filename.Truncate(filename.Length() - kSQLiteSuffix.Length());

  // Get the "idb" directory.
  nsCOMPtr<nsIFile> idbDirectory;
  MOZ_ALWAYS_SUCCEEDS(aFile->GetParent(getter_AddRefs(idbDirectory)));

  DebugOnly<nsString> idbLeafName;
  MOZ_ASSERT(NS_SUCCEEDED(idbDirectory->GetLeafName(idbLeafName)));
  MOZ_ASSERT(static_cast<nsString&>(idbLeafName).EqualsLiteral("idb"));

  // Get the <origin> directory.
  nsCOMPtr<nsIFile> originDirectory;
  MOZ_ALWAYS_SUCCEEDS(idbDirectory->GetParent(getter_AddRefs(originDirectory)));

  nsString origin;
  MOZ_ALWAYS_SUCCEEDS(originDirectory->GetLeafName(origin));

  // Any databases in these directories are owned by the application and should
  // not have their filenames masked. Hopefully they also appear in the
  // Telemetry.cpp whitelist.
  if (origin.EqualsLiteral("chrome") ||
      origin.EqualsLiteral("moz-safe-about+home")) {
    return 0;
  }

  // Get the <persistence> directory.
  nsCOMPtr<nsIFile> persistenceDirectory;
  MOZ_ALWAYS_SUCCEEDS(
      originDirectory->GetParent(getter_AddRefs(persistenceDirectory)));

  nsString persistence;
  MOZ_ALWAYS_SUCCEEDS(persistenceDirectory->GetLeafName(persistence));

  constexpr auto separator = u"*"_ns;

  uint32_t hashValue =
      HashString(persistence + separator + origin + separator + filename);

  MutexAutoLock lock(*gTelemetryIdMutex);

  if (!gTelemetryIdHashtable) {
    gTelemetryIdHashtable = new TelemetryIdHashtable();
  }

  uint32_t id;
  if (!gTelemetryIdHashtable->Get(hashValue, &id)) {
    static uint32_t sNextId = 1;

    // We're locked, no need for atomics.
    id = sNextId++;

    gTelemetryIdHashtable->Put(hashValue, id);
  }

  return id;
}

const CommonIndexOpenCursorParams& GetCommonIndexOpenCursorParams(
    const OpenCursorParams& aParams) {
  switch (aParams.type()) {
    case OpenCursorParams::TIndexOpenCursorParams:
      return aParams.get_IndexOpenCursorParams().commonIndexParams();
    case OpenCursorParams::TIndexOpenKeyCursorParams:
      return aParams.get_IndexOpenKeyCursorParams().commonIndexParams();
    default:
      MOZ_CRASH("Should never get here!");
  }
}

const CommonOpenCursorParams& GetCommonOpenCursorParams(
    const OpenCursorParams& aParams) {
  switch (aParams.type()) {
    case OpenCursorParams::TObjectStoreOpenCursorParams:
      return aParams.get_ObjectStoreOpenCursorParams().commonParams();
    case OpenCursorParams::TObjectStoreOpenKeyCursorParams:
      return aParams.get_ObjectStoreOpenKeyCursorParams().commonParams();
    case OpenCursorParams::TIndexOpenCursorParams:
    case OpenCursorParams::TIndexOpenKeyCursorParams:
      return GetCommonIndexOpenCursorParams(aParams).commonParams();
    default:
      MOZ_CRASH("Should never get here!");
  }
}

// TODO: Using nsCString as a return type here seems to lead to a dependency on
// some temporaries, which I did not expect. Is it a good idea that the default
// operator+ behaviour constructs such strings? It is certainly useful as an
// optimization, but this should be better done via an appropriately named
// function rather than an operator.
nsAutoCString MakeColumnPairSelectionList(
    const nsLiteralCString& aPlainColumnName,
    const nsLiteralCString& aLocaleAwareColumnName,
    const nsLiteralCString& aSortColumnAlias, const bool aIsLocaleAware) {
  return aPlainColumnName +
         (aIsLocaleAware ? EmptyCString() : " as "_ns + aSortColumnAlias) +
         ", "_ns + aLocaleAwareColumnName +
         (aIsLocaleAware ? " as "_ns + aSortColumnAlias : EmptyCString());
}

constexpr bool IsIncreasingOrder(const IDBCursorDirection aDirection) {
  MOZ_ASSERT(aDirection == IDBCursorDirection::Next ||
             aDirection == IDBCursorDirection::Nextunique ||
             aDirection == IDBCursorDirection::Prev ||
             aDirection == IDBCursorDirection::Prevunique);

  return aDirection == IDBCursorDirection::Next ||
         aDirection == IDBCursorDirection::Nextunique;
}

constexpr bool IsUnique(const IDBCursorDirection aDirection) {
  MOZ_ASSERT(aDirection == IDBCursorDirection::Next ||
             aDirection == IDBCursorDirection::Nextunique ||
             aDirection == IDBCursorDirection::Prev ||
             aDirection == IDBCursorDirection::Prevunique);

  return aDirection == IDBCursorDirection::Nextunique ||
         aDirection == IDBCursorDirection::Prevunique;
}

// TODO: In principle, this could be constexpr, if operator+(nsLiteralCString,
// nsLiteralCString) were constexpr and returned a literal type.
nsAutoCString MakeDirectionClause(const IDBCursorDirection aDirection) {
  return " ORDER BY "_ns + kColumnNameKey +
         (IsIncreasingOrder(aDirection) ? " ASC"_ns : " DESC"_ns);
}

enum struct ComparisonOperator {
  LessThan,
  LessOrEquals,
  Equals,
  GreaterThan,
  GreaterOrEquals,
};

constexpr nsLiteralCString GetComparisonOperatorString(
    const ComparisonOperator aComparisonOperator) {
  switch (aComparisonOperator) {
    case ComparisonOperator::LessThan:
      return "<"_ns;
    case ComparisonOperator::LessOrEquals:
      return "<="_ns;
    case ComparisonOperator::Equals:
      return "=="_ns;
    case ComparisonOperator::GreaterThan:
      return ">"_ns;
    case ComparisonOperator::GreaterOrEquals:
      return ">="_ns;
  }

  // TODO: This is just to silence the "control reaches end of non-void
  // function" warning. Cannot use MOZ_CRASH in a constexpr function,
  // unfortunately.
  return ""_ns;
}

nsAutoCString GetKeyClause(const nsCString& aColumnName,
                           const ComparisonOperator aComparisonOperator,
                           const nsLiteralCString& aStmtParamName) {
  return aColumnName + " "_ns +
         GetComparisonOperatorString(aComparisonOperator) + " :"_ns +
         aStmtParamName;
}

nsAutoCString GetSortKeyClause(const ComparisonOperator aComparisonOperator,
                               const nsLiteralCString& aStmtParamName) {
  return GetKeyClause(kColumnNameAliasSortKey, aComparisonOperator,
                      aStmtParamName);
}

template <IDBCursorType CursorType>
struct PopulateResponseHelper;

struct CommonPopulateResponseHelper {
  explicit CommonPopulateResponseHelper(
      const TransactionDatabaseOperationBase& aOp)
      : mOp{aOp} {}

  nsresult GetKeys(mozIStorageStatement* const aStmt,
                   Key* const aOptOutSortKey) {
    IDB_TRY(GetCommonKeys(aStmt));

    if (aOptOutSortKey) {
      *aOptOutSortKey = mPosition;
    }

    return NS_OK;
  }

  nsresult GetCommonKeys(mozIStorageStatement* const aStmt) {
    MOZ_ASSERT(mPosition.IsUnset());

    IDB_TRY(mPosition.SetFromStatement(aStmt, 0));

    IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST(
        "PRELOAD: Populating response with key %s", "Populating%.0s",
        IDB_LOG_ID_STRING(mOp.BackgroundChildLoggingId()),
        mOp.TransactionLoggingSerialNumber(), mOp.LoggingSerialNumber(),
        mPosition.GetBuffer().get());

    return NS_OK;
  }

  template <typename Response>
  void FillKeys(Response& aResponse) {
    MOZ_ASSERT(!mPosition.IsUnset());
    aResponse.key() = std::move(mPosition);
  }

  template <typename Response>
  static size_t GetKeySize(const Response& aResponse) {
    return aResponse.key().GetBuffer().Length();
  }

 protected:
  const Key& GetPosition() const { return mPosition; }

 private:
  const TransactionDatabaseOperationBase& mOp;
  Key mPosition;
};

struct IndexPopulateResponseHelper : CommonPopulateResponseHelper {
  using CommonPopulateResponseHelper::CommonPopulateResponseHelper;

  nsresult GetKeys(mozIStorageStatement* const aStmt,
                   Key* const aOptOutSortKey) {
    MOZ_ASSERT(mLocaleAwarePosition.IsUnset());
    MOZ_ASSERT(mObjectStorePosition.IsUnset());

    IDB_TRY(CommonPopulateResponseHelper::GetCommonKeys(aStmt));

    IDB_TRY(mLocaleAwarePosition.SetFromStatement(aStmt, 1));

    IDB_TRY(mObjectStorePosition.SetFromStatement(aStmt, 2));

    if (aOptOutSortKey) {
      *aOptOutSortKey =
          mLocaleAwarePosition.IsUnset() ? GetPosition() : mLocaleAwarePosition;
    }

    return NS_OK;
  }

  template <typename Response>
  void FillKeys(Response& aResponse) {
    MOZ_ASSERT(!mLocaleAwarePosition.IsUnset());
    MOZ_ASSERT(!mObjectStorePosition.IsUnset());

    CommonPopulateResponseHelper::FillKeys(aResponse);
    aResponse.sortKey() = std::move(mLocaleAwarePosition);
    aResponse.objectKey() = std::move(mObjectStorePosition);
  }

  template <typename Response>
  static size_t GetKeySize(Response& aResponse) {
    return CommonPopulateResponseHelper::GetKeySize(aResponse) +
           aResponse.sortKey().GetBuffer().Length() +
           aResponse.objectKey().GetBuffer().Length();
  }

 private:
  Key mLocaleAwarePosition, mObjectStorePosition;
};

struct KeyPopulateResponseHelper {
  static constexpr nsresult MaybeGetCloneInfo(
      mozIStorageStatement* const /*aStmt*/, const CursorBase& /*aCursor*/) {
    return NS_OK;
  }

  template <typename Response>
  static constexpr void MaybeFillCloneInfo(Response& /*aResponse*/,
                                           FilesArray* const /*aFiles*/) {}

  template <typename Response>
  static constexpr size_t MaybeGetCloneInfoSize(const Response& /*aResponse*/) {
    return 0;
  }
};

template <bool StatementHasIndexKeyBindings>
struct ValuePopulateResponseHelper {
  nsresult MaybeGetCloneInfo