    });

    this.#rsValueSet ??= new Set();

    // Remove items in sitesToRemove
    for (const site of deleted) {
      this.#rsValueSet.delete(site.serialize());
    }

    // Add items from sitesToAdd
    for (const site of created) {
      this.#rsValueSet.add(site.serialize());
    }

    this.#handleExceptionChange(created, deleted);
  }

  onPrefChange() {
    let newExceptions = Services.prefs.getStringPref(PREF_NAME, "").split(";");

    // Convert the exception strings to exception entries.
    newExceptions = newExceptions
      .map(ex => ThirdPartyCookieExceptionEntry.fromString(ex))
      .filter(Boolean);

    // If this is the first time we're initializing from pref, we can directly
    // call handleExceptionChange to create the exceptions.
    if (!this.#prefValueSet) {
      this.#handleExceptionChange({
        data: { created: newExceptions },
        prefUpdate: true,
      });
      // Serialize the exception entries to the string format and store in the
      // pref set.
      this.#prefValueSet = new Set(newExceptions.map(ex => ex.serialize()));
      return;
    }

    // Otherwise, we need to check for changes in the pref.

    // Find added items
    let created = [...newExceptions].filter(
      ex => !this.#prefValueSet.has(ex.serialize())
    );

    // Convert the new exceptions to the string format to check against the pref
    // set.
    let newExceptionStringSet = new Set(
      newExceptions.map(ex => ex.serialize())
    );

    // Find removed items
    let deleted = Array.from(this.#prefValueSet)
      .filter(item => !newExceptionStringSet.has(item))
      .map(ex => ThirdPartyCookieExceptionEntry.fromString(ex));

    // We shouldn't remove the exceptions in the remote settings list.
    if (this.#rsValueSet) {
      deleted = deleted.filter(ex => !this.#rsValueSet.has(ex.serialize()));
    }

    this.#prefValueSet = newExceptionStringSet;

    // Calling handleExceptionChange to handle the changes.
    this.#handleExceptionChange(created, deleted);
  }

  observe(subject, topic, data) {
    if (topic != "nsPref:changed" || data != PREF_NAME) {
      throw new Error(`Unexpected event ${topic} with ${data}`);
    }

    this.onPrefChange();
  }

  async importAllExceptions() {
    try {
      let exceptions = await this.#rs.get();
      if (!exceptions.length) {
        return;
      }
      this.onSync({ data: { created: exceptions } });
    } catch (error) {
      console.error(
        "Error while importing 3pcb exceptions from RemoteSettings",
        error
      );
    }
  }
}

export class ThirdPartyCookieExceptionEntry {
  classId = Components.ID("{8200e12c-416c-42eb-8af5-db9745d2e527}");
  QueryInterface = ChromeUtils.generateQI([
    "nsIThirdPartyCookieExceptionEntry",
  ]);

  constructor(fpSite, tpSite) {
    this.firstPartySite = fpSite;
    this.thirdPartySite = tpSite;
  }

  // Serialize the exception entry into a string. This is used for keying the
  // exception in the pref and RemoteSettings set.
  serialize() {
    return `${this.firstPartySite},${this.thirdPartySite}`;
  }

  equals(other) {
    return (
      this.firstPartySite === other.firstPartySite &&
      this.thirdPartySite === other.thirdPartySite
    );
  }

  static fromString(exStr) {
    if (!exStr) {
      return null;
    }

    let [fpSite, tpSite] = exStr.split(",");
    try {
      fpSite = this.#sanitizeSite(fpSite, true);
      tpSite = this.#sanitizeSite(tpSite);

      return new ThirdPartyCookieExceptionEntry(fpSite, tpSite);
    } catch (e) {
      console.error(
        `Error while constructing 3pcd exception entry from string`,
        exStr
      );
      return null;
    }
  }

  static fromRemoteSettingsRecord(record) {
    try {
      let fpSite = this.#sanitizeSite(record.fpSite, true);
      let tpSite = this.#sanitizeSite(record.tpSite);

      return new ThirdPartyCookieExceptionEntry(fpSite, tpSite);
    } catch (e) {
      console.error(
        `Error while constructing 3pcd exception entry from RemoteSettings record`,
        record
      );
      return null;
    }
  }

  // A helper function to sanitize the site using the eTLD service.
  static #sanitizeSite(site, acceptWildcard = false) {
    if (acceptWildcard && site === "*") {
      return "*";
    }

    let uri = Services.io.newURI(site);
    return Services.eTLD.getSite(uri);
  }
}
PK