@@ -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