eated prior to migration support.
      await this.#connection.executeTransaction(async () => {
        const createProfilesTable = `
            CREATE TABLE IF NOT EXISTS "Profiles" (
              id      INTEGER NOT NULL,
              path    TEXT NOT NULL UNIQUE,
              name    TEXT NOT NULL,
              avatar  TEXT NOT NULL,
              themeId TEXT NOT NULL,
              themeFg TEXT NOT NULL,
              themeBg TEXT NOT NULL,
              PRIMARY KEY(id)
            );`;

        await this.#connection.execute(createProfilesTable);

        const createSharedPrefsTable = `
            CREATE TABLE IF NOT EXISTS "SharedPrefs" (
              id        INTEGER NOT NULL,
              name      TEXT NOT NULL UNIQUE,
              value     BLOB,
              isBoolean INTEGER,
              PRIMARY KEY(id)
            );`;

        await this.#connection.execute(createSharedPrefsTable);
      });

      await this.#connection.setSchemaVersion(1);
    }

    if (currentVersion < 2) {
      await this.#connection.executeTransaction(async () => {
        // NB: The profileId field is actually TEXT (a string UUID).
        const createEnrollmentsTable = `
          CREATE TABLE IF NOT EXISTS "NimbusEnrollments" (
            id             INTEGER NOT NULL,
            profileId      INTEGER NOT NULL,
            slug           TEXT NOT NULL,
            branchSlug     TEXT NOT NULL,
            recipe         JSONB,
            active         BOOLEAN NOT NULL,
            unenrollReason TEXT,
            lastSeen       TEXT NOT NULL,
            setPrefs       JSONB,
            prefFlips      JSONB,
            source         TEXT NOT NULL,
            PRIMARY KEY(id),
            UNIQUE (profileId, slug) ON CONFLICT FAIL
          );
        `;

        await this.#connection.execute(createEnrollmentsTable);
      });

      await this.#connection.setSchemaVersion(2);
    }

    if (currentVersion < 3) {
      await this.#connection.executeTransaction(async () => {
        await this.#connection.execute("DELETE FROM NimbusEnrollments;");
      });
      await this.#connection.setSchemaVersion(3);
    }

    if (currentVersion < 4) {
      await this.#connection.executeTransaction(async () => {
        await this.#connection.execute("DELETE FROM NimbusEnrollments;");
      });
      await this.#connection.setSchemaVersion(4);
    }

    if (currentVersion < 5) {
      await this.#connection.executeTransaction(async () => {
        const createHeartbeatTable = `
            CREATE TABLE IF NOT EXISTS "Heartbeats" (
              id              INTEGER NOT NULL,
              recipeId        TEXT NOT NULL UNIQUE,
              lastShown       INTEGER,
              lastInteraction	INTEGER,
              PRIMARY KEY(id)
            );`;

        await this.#connection.execute(createHeartbeatTable);
      });

      await this.#connection.setSchemaVersion(5);
    }

    if (currentVersion < 6) {
      await this.#connection.executeTransaction(async () => {
        const createMessageImpressionsTable = `
          CREATE TABLE IF NOT EXISTS "MessagingSystemMessageImpressions" (
            id                  INTEGER PRIMARY KEY,
            messageId           TEXT UNIQUE NOT NULL,
            impressions         JSONB
          );
        `;

        const createMessageBlocklistTable = `
          CREATE TABLE IF NOT EXISTS "MessagingSystemMessageBlocklist" (
            id                  INTEGER PRIMARY KEY,
            messageId           TEXT UNIQUE NOT NULL
          );
        `;

        await this.#connection.execute(createMessageImpressionsTable);
        await this.#connection.execute(createMessageBlocklistTable);
      });

      await this.#connection.setSchemaVersion(6);
    }

    if (currentVersion < 7) {
      await this.#connection.executeTransaction(async () => {
        const createNimbusSyncTable = `
          CREATE TABLE IF NOT EXISTS "NimbusSyncTimestamps" (
            id                  INTEGER NOT NULL,
            profileId           TEXT NOT NULL,
            collection          TEXT NOT NULL,
            lastModified        INTEGER NOT NULL,
            PRIMARY KEY(id),
            UNIQUE (profileId, collection) ON CONFLICT FAIL
          );
        `;

        await this.#connection.execute(createNimbusSyncTable);
      });

      await this.#connection.setSchemaVersion(7);
    }
  }

  /**
   * Trigger an async cross-instance notification that the data in the
   * datastore has been changed.
   *
   * Two different nsIObserver events may be fired, depending on whether the
   * change originated in the current Firefox instance or in another instance.
   *
   * Changes to the datastore made by the current instance will trigger the
   * "pds-datastore-changed" nsIObserver event; see `#datastoreChanged` for
   * details. These events are fired whether or not the multiple profiles
   * feature is enabled.
   *
   * If the multiple profiles feature is enabled, then changes to the datastore
   * made either by the current instance or another instance in the profile
   * group will trigger the "sps-profiles-updated" nsIObserver event. See
   * `SelectableProfileService.databaseChanged` for details.
   */
  notify() {
    this.#notifyTask.arm();
  }

  /**
   * Notify datastore observers that the data has changed by firing
   * the "pds-datastore-changed" nsIObserver signal.
   *
   *@param {"local"|"shutdown"} source The source of the
   *   notification. Either "local" meaning that the change was made in this
   *   process and "shutdown" meaning we are closing the connection and
   *   shutting down.
   */
  #datastoreChanged(source) {
    Services.obs.notifyObservers(null, "pds-datastore-changed", source);
  }

  get storeID() {
    return new Promise(resolve => {
      this.init().then(() => {
        resolve(this.#storeID);
      });
    });
  }

  get initialized() {
    return this.#initialized;
  }

  static get PROFILE_GROUPS_DIR() {
    if (this.#dirSvc && "ProfileGroups" in this.#dirSvc) {
      return this.#dirSvc.ProfileGroups;
    }

    return PathUtils.join(
      ProfilesDatastoreServiceClass.getDirectory("UAppData").path,
      "Profile Groups"
    );
  }

  overrideDirectoryService(dirSvc) {
    if (!Cu.isInAutomation) {
      return;
    }

    ProfilesDatastoreServiceClass.#dirSvc = dirSvc;
  }

  static getDirectory(id) {
    if (this.#dirSvc) {
      if (id in this.#dirSvc) {
        return this.#dirSvc[id].clone();
      }
    }

    return Services.dirsvc.get(id, Ci.nsIFile);
  }

  /**
   * For use in testing only, override the profile service with a mock version
   * and reset state accordingly.
   *
   * @param {Ci.nsIToolkitProfileService} profileService The mock profile service
   */
  async resetProfileService(profileService) {
    if (!Cu.isInAutomation) {
      return;
    }

    await this.uninit();
    this.#profileService =
      profileService ??
      Cc["@mozilla.org/toolkit/profile-service;1"].getService(
        Ci.nsIToolkitProfileService
      );
    await this.init();
  }

  get toolkitProfileService() {
    return this.#profileService;
  }

  constructor() {
    this.#asyncShutdownBlocker = () => this.uninit();
    this.#asyncShutdownBarrier = new lazy.AsyncShutdown.Barrier(
      "ProfilesDatastoreService: waiting for clients to finish pending writes"
    );
    this.#profileService = Cc[
      "@mozilla.org/toolkit/profile-service;1"
    ].getService(Ci.nsIToolkitProfileService);
  }

  /**
   * At startup, get the groupDBPath from the prefs, and connect to it.
   *
   * @returns {Promise}
   */
  init() {
    if (!this.#initPromise) {
      this.#initPromise = this.#init().finally(
        () => (this.#initPromise = null)
      );
    }

    return this.#initPromise;
  }

  async #init() {
    if (this.#initialized) {
      return;
    }

    // We read the store ID from prefs but in early startup (for example in the profile selector)
    // this is not available so we get it from the current profile.
    this.#storeID = Services.startup.startingUp
      ? this.#profileService.currentProfile?.storeID
      : Services.prefs.getStringPref(STOREID_PREF_NAME, "");

    // This could fail if we're adding it during shutdown. In this case,
    // don't throw but don't continue initialization.
    try {
      lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
        "ProfilesDatastoreService uninit",
        this.#asyncShutdownBlocker
      );
    } catch (ex) {
      console.error(ex);
      return;
    }

    this.#notifyTask = new DeferredTask(async () => {
      this.#datastoreChanged("local");
    }, NOTIFY_TIMEOUT);

    try {
      await this.#initConnection();
    } catch (e) {
      console.error(e);

      await this.uninit();
      return;
    }

    this.#initialized = true;
  }

  async uninit() {
    if (this.#asyncShutdownBarrier) {
      await this.#asyncShutdownBarrier.wait();
    }

    lazy.AsyncShutdown.profileChangeTeardown.removeBlocker(
      this.#asyncShutdownBlocker
    );

    // During shutdown we don't need to notify ourselves, just other instances
    // so rather than finalizing the task just disarm it and do the notification
    // manually.
    if (this.#notifyTask.isArmed) {
      this.#notifyTask.disarm();
      this.#datastoreChanged("shutdown");
    }

    await this.closeConnection();

    this.#storeID = null;

    this.#initialized = false;
  }

  async #initConnection() {
    if (this.#connection) {
      return;
    }

    let path = await this.getProfilesStorePath();

    // TODO: (Bug 1902320) Handle exceptions on connection opening
    // This could fail if the store is corrupted.
    this.#connection = await lazy.Sqlite.openConnection({
      path,
      openNotExclusive: true,
    });

    await this.#connection.execute("PRAGMA journal_mode = WAL");
    await this.#connection.execute("PRAGMA wal_autocheckpoint = 16");

    await this.createTables();
  }

  async closeConnection() {
    if (!this.#connection) {
      return;
    }

    // An error could occur while closing the connection. We suppress the
    // error since it is not a critical part of the browser.
    try {
      await this.#connection.close();
    } catch (ex) {}
    this.#connection = null;
  }

  maybeCreateStoreID() {
    if (this.#storeID) {
      return;
    }

    const storageID = Services.uuid
      .generateUUID()
      .toString()
      .replace("{", "")
      .split("-")[0];

    this.#storeID = storageID;
    Services.prefs.setStringPref(STOREID_PREF_NAME, storageID);
  }

  async getProfilesStorePath() {
    this.maybeCreateStoreID();

    // If we are not running in a named nsIToolkitProfile, the datastore path
    // should be in the profile directory. This is true in a local build or a
    // CI test build, for example.
    if (!this.#profileService.currentProfile) {
      return PathUtils.join(
        ProfilesDatastoreServiceClass.getDirectory("ProfD").path,
        `${this.#storeID}.sqlite`
      );
    }

    await IOUtils.makeDirectory(
      ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR
    );

    return PathUtils.join(
      ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR,
      `${this.#storeID}.sqlite`
    );
  }
}

const ProfilesDatastoreService = new ProfilesDatastoreServiceClass();
export { ProfilesDatastoreService };
PK