Skip to content

Commit 3ae7df3

Browse files
committed
Bug 1957299 - Enable Initializing blockList from Remote Settings - r=txia
Differential Revision: https://phabricator.services.mozilla.com/D243609
1 parent 45f7a13 commit 3ae7df3

File tree

2 files changed

+461
-0
lines changed

2 files changed

+461
-0
lines changed

toolkit/components/ml/content/Utils.sys.mjs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,22 @@ ChromeUtils.defineESModuleGetters(
1111
lazy,
1212
{
1313
BLOCK_WORDS_ENCODED: "chrome://global/content/ml/BlockWords.sys.mjs",
14+
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
15+
TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
1416
},
1517
ES_MODULES_OPTIONS
1618
);
1719

20+
ChromeUtils.defineLazyGetter(lazy, "console", () => {
21+
return console.createInstance({
22+
maxLogLevelPref: IN_WORKER ? "Error" : "browser.ml.logLevel",
23+
prefix: "ML:Utils",
24+
});
25+
});
26+
27+
/** The name of the remote settings collection holding block list */
28+
const RS_BLOCK_LIST_COLLECTION = "ml-inference-words-block-list";
29+
1830
/**
1931
* Enumeration for the progress status text.
2032
*/
@@ -782,6 +794,56 @@ export class BlockListManager {
782794
});
783795
}
784796

797+
/**
798+
* Initialize the block list manager from remote settings
799+
*
800+
* @param {object} options - Configuration object.
801+
* @param {string} options.blockListName - Name of the block list within the remote setting collection.
802+
* @param {string} options.language - A string with a BCP 47 language tag for the language of the blocked n-grams.
803+
* Example: "en" for English, "fr" for French.
804+
* See https://en.wikipedia.org/wiki/IETF_language_tag.
805+
* @param {boolean} options.fallbackToDefault - Whether to fall back to the default block list if the remote settings retrieval fails.
806+
* @param {number} options.majorVersion - The target version of the block list in remote settings.
807+
* @param {number} options.collectionName - The remote settings collection holding the block list.
808+
*
809+
* @returns {Promise<BlockListManager>} A promise to a new BlockListManager instance.
810+
*/
811+
static async initializeFromRemoteSettings({
812+
blockListName,
813+
language = "en",
814+
fallbackToDefault = true,
815+
majorVersion = 1,
816+
collectionName = RS_BLOCK_LIST_COLLECTION,
817+
} = {}) {
818+
try {
819+
const record = await RemoteSettingsManager.getRemoteData({
820+
collectionName,
821+
filters: { name: blockListName, language },
822+
majorVersion,
823+
});
824+
825+
if (!record) {
826+
throw new Error(
827+
`No block list record found for ${JSON.stringify({ language, majorVersion, blockListName })}`
828+
);
829+
}
830+
831+
return new BlockListManager({
832+
blockNgrams: record.blockList,
833+
language,
834+
});
835+
} catch (error) {
836+
if (fallbackToDefault) {
837+
lazy.console.debug(
838+
"Error when retrieving list from remote settings. Falling back to in-source list"
839+
);
840+
return BlockListManager.initializeFromDefault({ language });
841+
}
842+
843+
throw error;
844+
}
845+
}
846+
785847
/**
786848
* Decode a base64 encoded string to its original representation.
787849
*
@@ -903,3 +965,127 @@ export class BlockListManager {
903965
return false;
904966
}
905967
}
968+
969+
/**
970+
* A class to retrieve data from remote setting
971+
*
972+
*/
973+
export class RemoteSettingsManager {
974+
/**
975+
* The cached remote settings clients that downloads the data.
976+
*
977+
* @type {Record<string, RemoteSettingsClient>}
978+
*/
979+
static #remoteClients = {};
980+
981+
/**
982+
* Remote settings isn't available in tests, so provide mocked clients.
983+
*
984+
* @param {Record<string, RemoteSettingsClient>} remoteClients
985+
*/
986+
static mockRemoteSettings(remoteClients) {
987+
lazy.console.log("Mocking remote settings in RemoteSettingsManager.");
988+
RemoteSettingsManager.#remoteClients = remoteClients;
989+
}
990+
991+
/**
992+
* Remove anything that could have been mocked.
993+
*/
994+
static removeMocks() {
995+
lazy.console.log("Removing mocked remote client in RemoteSettingsManager.");
996+
RemoteSettingsManager.#remoteClients = {};
997+
}
998+
999+
/**
1000+
* Lazily initialize the remote settings client responsible for downloading the data.
1001+
*
1002+
* @param {string} collectionName - The name of the collection to use.
1003+
* @returns {RemoteSettingsClient}
1004+
*/
1005+
static getRemoteClient(collectionName) {
1006+
if (RemoteSettingsManager.#remoteClients[collectionName]) {
1007+
return RemoteSettingsManager.#remoteClients[collectionName];
1008+
}
1009+
1010+
/** @type {RemoteSettingsClient} */
1011+
const client = lazy.RemoteSettings(collectionName, {
1012+
bucketName: "main",
1013+
});
1014+
1015+
RemoteSettingsManager.#remoteClients[collectionName] = client;
1016+
1017+
client.on("sync", async ({ data: { created, updated, deleted } }) => {
1018+
lazy.console.debug(`"sync" event for ${collectionName}`, {
1019+
created,
1020+
updated,
1021+
deleted,
1022+
});
1023+
1024+
// Remove all the deleted records.
1025+
for (const record of deleted) {
1026+
await client.attachments.deleteDownloaded(record);
1027+
}
1028+
1029+
// Remove any updated records, and download the new ones.
1030+
for (const { old: oldRecord } of updated) {
1031+
await client.attachments.deleteDownloaded(oldRecord);
1032+
}
1033+
1034+
// Do nothing for the created records.
1035+
});
1036+
1037+
return client;
1038+
}
1039+
1040+
/**
1041+
* Gets data from remote settings.
1042+
*
1043+
* @param {object} options - Configuration object
1044+
* @param {string} options.collectionName - The name of the remote settings collection.
1045+
* @param {object} options.filters - The filters to use where key should match the schema in remote settings.
1046+
* @param {number|null} options.majorVersion - The target version or null if no version is supported.
1047+
* @param {Function} [options.lookupKey=(record => record.name)]
1048+
* The function to use to extract a lookup key from each record when versionning is supported..
1049+
* This function should take a record as input and return a string that represents the lookup key for the record.
1050+
* @returns {Promise<object|null>}
1051+
*/
1052+
1053+
static async getRemoteData({
1054+
collectionName,
1055+
filters,
1056+
majorVersion,
1057+
lookupKey = record => record.name,
1058+
} = {}) {
1059+
const client = RemoteSettingsManager.getRemoteClient(collectionName);
1060+
1061+
let records = [];
1062+
1063+
if (majorVersion) {
1064+
records = await lazy.TranslationsParent.getMaxSupportedVersionRecords(
1065+
client,
1066+
{
1067+
filters,
1068+
minSupportedMajorVersion: majorVersion,
1069+
maxSupportedMajorVersion: majorVersion,
1070+
lookupKey,
1071+
}
1072+
);
1073+
} else {
1074+
records = await client.get({ filters });
1075+
}
1076+
1077+
// Handle case where multiple records exist
1078+
if (records.length > 1) {
1079+
throw new Error(
1080+
`Found more than one record in '${collectionName}' for filters ${JSON.stringify(filters)}. Double-check your filters.`
1081+
);
1082+
}
1083+
1084+
// If still no records, return null
1085+
if (records.length === 0) {
1086+
return null;
1087+
}
1088+
1089+
return records[0];
1090+
}
1091+
}

0 commit comments

Comments
 (0)