@@ -11,10 +11,22 @@ ChromeUtils.defineESModuleGetters(
11
11
lazy ,
12
12
{
13
13
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" ,
14
16
} ,
15
17
ES_MODULES_OPTIONS
16
18
) ;
17
19
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
+
18
30
/**
19
31
* Enumeration for the progress status text.
20
32
*/
@@ -782,6 +794,56 @@ export class BlockListManager {
782
794
} ) ;
783
795
}
784
796
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
+
785
847
/**
786
848
* Decode a base64 encoded string to its original representation.
787
849
*
@@ -903,3 +965,127 @@ export class BlockListManager {
903
965
return false ;
904
966
}
905
967
}
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