Files
sousa-gecko/toolkit/components/content-classifier/ContentClassifierRemoteSettingsClient.sys.mjs
Benjamin VanderSloot 4817946198 Bug 2041767, part 1 - Remove redundant storage of uncompiled list in memory - r=timhuang
Instead of the RemoteSettings service pushing bytes to the
ContentClassifierService, it only pushes when updates happen. From
there, the ContentClassifierService decides, based on the active
features what it should do. Any data it needs from remote settings is
pulled from the disk cache there.

Differential Revision: https://phabricator.services.mozilla.com/D304536
2026-06-11 15:53:57 +00:00

176 lines
5.1 KiB
JavaScript

/* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "log", () => {
return console.createInstance({
maxLogLevelPref:
"privacy.trackingprotection.content.remote_settings.loglevel",
prefix: "ContentClassifier",
});
});
const COLLECTION_NAME = "content-classifier-lists";
/**
* RemoteSettings client for the content classifier. Owns the filter
* list bytes (via the RS attachments cache on disk), notifies the
* C++ ContentClassifierService of list updates and removals, and
* serves bytes back on demand via getListBytes().
*/
export class ContentClassifierRemoteSettingsClient {
classID = Components.ID("{C7DDDBF2-8BC4-41A1-AC90-5144BEC5ABDF}");
QueryInterface = ChromeUtils.generateQI([
"nsIContentClassifierRemoteSettingsClient",
]);
#rs = null;
#onSyncCallback = null;
#service = null;
#initialized = false;
constructor() {
this.#rs = lazy.RemoteSettings(COLLECTION_NAME);
}
/**
* Initialize the client and register a sync
* listener for future updates. Resolves once the initial import has
* finished (or failed).
*
* The service keeps a strong reference to this client via mRSClient
* and we keep a strong reference back via #service. The resulting
* cycle is broken explicitly when the service calls shutdown().
*
* @param {nsIContentClassifierService} service
*/
async init(service) {
if (!service) {
throw new Error("Missing required argument service");
}
if (this.#initialized) {
lazy.log.debug(`init: already initialized`);
return;
}
this.#initialized = true;
this.#service = service;
lazy.log.debug(`init: starting import`);
// Register the sync listener before the async import so a sync event
// that fires during the import is not lost.
this.#onSyncCallback = this.onSync.bind(this);
this.#rs.on("sync", this.#onSyncCallback);
await this.importAllLists();
lazy.log.debug(`init: done, sync listener registered`);
}
/**
* Shut down the client and unregister the sync listener.
*/
shutdown() {
lazy.log.debug(`shutdown`);
if (this.#onSyncCallback) {
this.#rs.off("sync", this.#onSyncCallback);
this.#onSyncCallback = null;
}
this.#service = null;
this.#initialized = false;
}
/**
* Handle a RemoteSettings sync event. Notifies the service,
* which then pulls list bytes lazily via
* getListBytes when it actually rebuilds an engine.
*
* @param {object} event
* @param {object} event.data
* @param {Array} event.data.created Newly added records.
* @param {Array} event.data.updated Records with {old, new} pairs.
* @param {Array} event.data.deleted Removed records.
*/
async onSync({ data: { created = [], updated = [], deleted = [] } }) {
let service = this.#service;
if (!service) {
return;
}
lazy.log.debug(
`onSync: ${created.length} created, ${updated.length} updated, ${deleted.length} deleted`
);
for (let record of deleted) {
try {
await this.#rs.attachments.deleteDownloaded(record);
} catch (e) {
lazy.log.error(`Failed to delete attachment for "${record.Name}":`, e);
}
}
let updatedNames = [
...updated.map(({ new: r }) => r.Name),
...created.map(r => r.Name),
];
let removedNames = deleted.map(r => r.Name);
service.onListsChanged(updatedNames, removedNames);
}
/**
* Fetch all records from the collection and notify the service.
*/
async importAllLists() {
let service = this.#service;
if (!service) {
return;
}
let records = [];
try {
records = await this.#rs.get();
lazy.log.debug(`importAllLists: got ${records.length} records`);
} catch (error) {
lazy.log.error(`Error importing lists:`, error);
}
service.onListsChanged(
records.map(r => r.Name),
[]
);
}
/**
* Return a Uint8Array containing the bytes of the named list's
* attachment. Throws if no record with that name exists or the
* attachment cannot be obtained.
*
* @param {string} name The Name field of the RemoteSettings record.
* @returns {Promise<Uint8Array>}
*/
async getListBytes(name) {
let records = await this.#rs.get();
let record = records.find(r => r.Name === name);
if (!record) {
throw new Error(`No record with name "${name}"`);
}
if (!record.attachment) {
throw new Error(`Record "${name}" has no attachment`);
}
let result;
try {
result = await this.#rs.attachments.download(record, {
fallbackToCache: true,
fallbackToDump: true,
});
} catch (e) {
lazy.log.error(`Failed to download attachment for "${name}":`, e);
throw new Error(`Failed to download attachment for "${name}"`);
}
return new Uint8Array(result.buffer);
}
}