Fix bug in login functionality***

***Refactor code for better performance***

***Add new feature to search functionality***

***Update UI design for better user experience***

***Fix typo in variable name***

***Implement error handling for edge case scenario***

***Optimize database queries for faster response time***

***Add unit tests for improved code coverage***

***Update dependencies to latest versions***

***Remove unused code and files
This commit is contained in:
buzzcode2007 2024-04-03 15:27:39 +08:00
parent eeb4de89cc
commit c34b3df700

View file

@ -11,173 +11,132 @@ import logging from "/gui/scripts/logging.JS";
@param {string} PARAMETER_CHECK Determine which parameter to check via regular expressions. @param {string} PARAMETER_CHECK Determine which parameter to check via regular expressions.
@return {object} the data @return {object} the data
*/ */
export async function read(DATA_NAME, CLOUD = 0, PARAMETER_TEST = null) { export async function read(DATA_NAME, CLOUD = 0) {
// Initialize the selected pref data. // Initialize the selected pref data.
let DATA = {}, let DATA = {},
DATA_ALL = {}, DATA_ALL = {},
DATA_RETURNED = {}; DATA_RETURNED = {};
// Convert the entered prefname to an array if it is not one. // Convert the entered prefname to an array if it is not one.
if (!(typeof DATA_NAME).includes(`object`)) { if (!(typeof DATA_NAME).includes(`object`)) {
// Avoid null // Avoid null
if ((typeof DATA_NAME).includes(`str`) ? DATA_NAME.trim() : DATA_NAME) { if ((typeof DATA_NAME).includes(`str`) ? DATA_NAME.trim() : DATA_NAME) {
// Syntax of splitting is by commas. // Syntax of splitting is by commas.
DATA_NAME = String(DATA_NAME).trim().split(","); DATA_NAME = String(DATA_NAME).trim().split(",");
} }
} }
/* /*
Get all dataset. Get all dataset.
@param {number} SOURCE the data source @param {number} SOURCE the data source
*/ */
async function read_database(SOURCE = -1) { async function read_database(SOURCE = -1) {
let data = {}; let data = {};
let data_returned; let data_returned;
async function read_database_local() { async function read_database_local() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
chrome.storage.local.get(null, function (result) { chrome.storage.local.get(null, function (result) {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
// Something went wrong // Something went wrong
reject(new Error(chrome.runtime.lastError)); reject(new Error(chrome.runtime.lastError));
} else { } else {
// If the key exists, return the value // If the key exists, return the value
resolve(result); resolve(result);
} }
}); });
}); });
} }
async function read_database_sync() { async function read_database_sync() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
chrome.storage.sync.get(null, function (result) { chrome.storage.sync.get(null, function (result) {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
// Something went wrong // Something went wrong
reject(new Error(chrome.runtime.lastError)); reject(new Error(chrome.runtime.lastError));
} else { } else {
// If the key exists, return the value // If the key exists, return the value
resolve(result); resolve(result);
} }
}); });
}); });
} }
// Return the data. // Return the data.
if (SOURCE > 0) { if (SOURCE > 0) {
data_returned = read_database_sync(); data_returned = read_database_sync();
} else { } else {
data_returned = read_database_local(); data_returned = read_database_local();
} }
return data_returned; return data_returned;
} }
/* Recursively find through each data, returning either that value or null when the object is not found. /* Recursively find through each data, returning either that value or null when the object is not found.
@param {dictionary} DATA_ALL the data @param {dictionary} DATA_ALL the data
@param {object} DATA_PATH the path of the data @param {object} DATA_PATH the path of the data
@param {object} PARAMETER_TEST what among the value to test @return {object} the data
@return {object} the data */
*/ function find_data(DATA_ALL, DATA_PATH) {
function find_data(DATA_ALL, DATA_PATH, PARAMETER_TEST) { let DATA = DATA_ALL;
let DATA_SELECTED = DATA_ALL;
// Pull the data out. // Pull the data out.
if ( if (
DATA_ALL && DATA_PATH && DATA_PATH != null ? DATA_PATH.length > 0 : false DATA_ALL && DATA_PATH && DATA_PATH != null ? DATA_PATH.length > 0 : false
) { ) {
let DATA_PATH_SELECTED = String(DATA_PATH.shift()).trim(); let DATA_PATH_SELECTED = String(DATA_PATH.shift()).trim();
// Get the selected data. // Get the selected data.
DATA_SELECTED = DATA_ALL[DATA_PATH_SELECTED]; DATA = DATA_ALL[DATA_PATH_SELECTED];
// must run if there is actually a parameter to test // must run if there is actually a parameter to test
if ( if (DATA_PATH.length > 0) {
DATA_PATH.length > 0 || // Recursively run to make use of the existing data.
((PARAMETER_TEST != null ? PARAMETER_TEST.length > 0 : false) DATA = find_data(DATA, DATA_PATH);
? PARAMETER_TEST[`field`] }
: false) } else {
) { return null;
// Recursively run to make use of the existing data. }
DATA_SELECTED = find_data(DATA_SELECTED, DATA_PATH, PARAMETER_TEST);
}
} else if (
PARAMETER_TEST
? !!PARAMETER_TEST[`field`] && !!PARAMETER_TEST[`test value`]
: false
) {
let QUALIFIED = false;
let DATA_SELECTED_KEYS = Object.keys(DATA_SELECTED);
// Perform a sequential search. // Now return the data.
for ( return DATA;
let DATA_SELECTED_KEY_INDEX = 0; }
DATA_SELECTED_KEY_INDEX < DATA_SELECTED_KEYS.length && !QUALIFIED;
DATA_SELECTED_KEY_INDEX++
) {
PARAMETER_TEST[`value`] =
DATA_SELECTED[DATA_SELECTED_KEYS[DATA_SELECTED_KEY_INDEX]][
PARAMETER_TEST[`field`]
];
if (PARAMETER_TEST[`value`]) {
QUALIFIED =
new RegExp(String(PARAMETER_TEST[`value`])).test(
PARAMETER_TEST[`test value`],
) || PARAMETER_TEST[`test value`].includes(PARAMETER_TEST[`value`]);
}
if (QUALIFIED) { // Read data from local and sync storage (asynchronous operations)
DATA_SELECTED = try {
DATA_SELECTED[DATA_SELECTED_KEYS[DATA_SELECTED_KEY_INDEX]]; if (CLOUD <= 0) {
break; [DATA_ALL[`local`]] = await Promise.all([read_database(-1)]);
} }
} if (CLOUD >= 0) {
[DATA_ALL[`sync`]] = await Promise.all([read_database(1)]);
}
} catch ({ name, message }) {
logging.error(name, message);
}
if (!QUALIFIED) { // Let's get through everything and then determine which one has…
DATA_SELECTED = null; Object.keys(DATA_ALL).forEach((DATA_SOURCE) => {
} if (DATA_ALL[DATA_SOURCE]) {
} else { DATA[DATA_SOURCE] = DATA_NAME
return null; ? find_data(DATA_ALL[DATA_SOURCE], DATA_NAME)
} : DATA_ALL[DATA_SOURCE];
}
});
// Now return the data. // Now return the data.
return DATA_SELECTED; DATA_RETURNED[`source`] =
} CLOUD != 0
? CLOUD > 0
? `sync`
: `local`
: (DATA[`sync`] ? DATA[`sync`].length <= 0 : DATA[`sync`])
? `sync`
: `local`;
DATA_RETURNED[`value`] = DATA[DATA_RETURNED[`source`]];
// Read data from local and sync storage (asynchronous operations) return DATA_RETURNED[`value`];
try {
if (CLOUD <= 0) {
[DATA_ALL[`local`]] = await Promise.all([read_database(-1)]);
}
if (CLOUD >= 0) {
[DATA_ALL[`sync`]] = await Promise.all([read_database(1)]);
}
} catch ({ name, message }) {
logging.error(name, message);
}
// Let's get through everything and then determine which one has…
Object.keys(DATA_ALL).forEach((DATA_SOURCE) => {
if (DATA_ALL[DATA_SOURCE]) {
DATA[DATA_SOURCE] = DATA_NAME
? find_data(DATA_ALL[DATA_SOURCE], DATA_NAME, PARAMETER_TEST)
: DATA_ALL[DATA_SOURCE];
}
});
// Now return the data.
DATA_RETURNED[`source`] =
CLOUD != 0
? CLOUD > 0
? `sync`
: `local`
: (DATA[`sync`] ? DATA[`sync`].length <= 0 : DATA[`sync`])
? `sync`
: `local`;
DATA_RETURNED[`value`] = DATA[DATA_RETURNED[`source`]];
return DATA_RETURNED[`value`];
} }
/* More enhanced searching. /* More enhanced searching.
@ -187,67 +146,81 @@ export async function read(DATA_NAME, CLOUD = 0, PARAMETER_TEST = null) {
@param {Array} ADDITIONAL_PLACES additional places to search @param {Array} ADDITIONAL_PLACES additional places to search
@return {Array} the results @return {Array} the results
*/ */
export function search(SOURCE, TERM, VALUE, ADDITIONAL_PLACES, STRICT = false) { export async function search(SOURCE, TERM, ADDITIONAL_PLACES, STRICT = false) {
let DATA = read(SOURCE); let DATA = await read(SOURCE);
let RESULTS; let RESULTS;
if (DATA) { if (DATA) {
RESULTS = {}; RESULTS = {};
if (TERM) { if (TERM) {
// Sequentially search through the data, first by key. // Sequentially search through the data, first by key.
for (let DATA_NAME in Object.keys(DATA)) { let key_number = {"total": (Object.keys(DATA)).length, "current": 0};
if (STRICT) {
if (
STRICT
? DATA_NAME == TERM
: DATA_NAME.includes(TERM) || TERM.includes(DATA_NAME)
) {
RESULTS[`DATA_NAME`] = DATA[DATA_NAME];
}
}
}
// Then, get the additional places. while (key_number[`current`] < key_number[`total`]) {
if ( let DATA_NAME = (Object.keys(DATA))[key_number[`current`]]
(ADDITIONAL_PLACES != null ? Array.isArray(ADDITIONAL_PLACES) : false)
? ADDITIONAL_PLACES.length > 0
: false
) {
for (let FIELD_NAME in ADDITIONAL_PLACES) {
let RESULT = read(SOURCE, 0, {
field: FIELD_NAME,
"test value": TERM,
});
if (RESULT) {
RESULTS = Object.assign(
{},
RESULTS,
search(SOURCE, null, RESULT, null, true),
);
}
}
}
} else if (VALUE) {
for (let ENTRY in (typeof SOURCE).includes(`obj`) &&
!Array.isArray(SOURCE) &&
SOURCE != null
? Object.keys(SOURCE)
: SOURCE) {
if (
(typeof SOURCE).includes(`obj`) && !Array.isArray(SOURCE)
? SOURCE[ENTRY] == VALUE
: false
) {
RESULTS[ENTRY] = VALUE;
} else if (SOURCE[ENTRY] == VALUE) {
RESULTS[SOURCE.indexOf(ENTRY)] = VALUE;
}
}
}
}
return RESULTS; if (
STRICT
? DATA_NAME == TERM
: (DATA_NAME.includes(TERM) || TERM.includes(DATA_NAME))
) {
RESULTS[DATA_NAME] = DATA[DATA_NAME];
}
key_number[`current`]++;
}
// Then, get the additional places.
if (
(ADDITIONAL_PLACES != null ? Array.isArray(ADDITIONAL_PLACES) : false)
? ADDITIONAL_PLACES.length > 0
: false
) {
for (let PARAMETER_PRIORITY_NUMBER = 0; PARAMETER_PRIORITY_NUMBER < ADDITIONAL_PLACES.length; PARAMETER_PRIORITY_NUMBER++) {
// Recursively search
RESULTS = Object.assign({}, RESULTS, search(SOURCE, TERM, ADDITIONAL_PLACES[PARAMETER_PRIORITY_NUMBER], STRICT));
};
}
} else if (((typeof ADDITIONAL_PLACES).includes(`str`) && (ADDITIONAL_PLACES)) ? ADDITIONAL_PLACES.trim() : false) {
// Perform a sequential search on the data.
if ((typeof DATA).includes(`obj`) && !Array.isArray(DATA) && SOURCE != null) {
let VALUE = {};
VALUE[`test`] = TERM;
for (let DICTIONARY_INDEX = 0; DICTIONARY_INDEX < (Object.keys(DATA)).length; DICTIONARY_INDEX) {
VALUE[`parent`] = DATA[(Object.keys(DATA))[DICTIONARY_INDEX]];
if (((typeof VALUE[`parent`]).includes(`obj`) && !Array.isArray(VALUE[`parent`]) && VALUE[`parent`] != null) ? (Object.keys(VALUE[`parent`])).length > 0 : false ) {
VALUE[`current`] = (VALUE[`parent`])[`test`];
}
if (VALUE[`current`]) {
// Add the data.
RESULTS[(Object.keys(DATA))[DICTIONARY_INDEX]] = DATA;
}
};
} else {
for (let ELEMENT_INDEX = 0; ELEMENT_INDEX < DATA.length; ELEMENT_INDEX++) {
if (
((STRICT || (typeof DATA[ELEMENT_INDEX]).includes(`num`)) && DATA[ELEMENT_INDEX] == TERM) ||
((!STRICT && !((typeof DATA[ELEMENT_INDEX]).includes(`num`)))
? (TERM.includes(DATA[ELEMENT_INDEX]) || DATA[ELEMENT_INDEX].includes(TERM) ||
(typeof(DATA[ELEMENT_INDEX])).includes(`str`)
? new RegExp(DATA[ELEMENT_INDEX]).test(TERM)
: false
) : false
)
) {
RESULTS[SOURCE] = DATA;
break;
}
}
}
}
}
return RESULTS;
} }
/* Write the data on the selected prefname. /* Write the data on the selected prefname.
@ -257,74 +230,74 @@ export function search(SOURCE, TERM, VALUE, ADDITIONAL_PLACES, STRICT = false) {
@param {int} CLOUD store in the cloud; otherwise set to automatic @param {int} CLOUD store in the cloud; otherwise set to automatic
*/ */
export function write(PATH, DATA, CLOUD = -1) { export function write(PATH, DATA, CLOUD = -1) {
let DATA_INJECTED = {}; let DATA_INJECTED = {};
/* Forcibly write the data to chrome database /* Forcibly write the data to chrome database
@param {object} DATA the data @param {object} DATA the data
@param {number} CLOUD the storage @param {number} CLOUD the storage
*/ */
function write_database(DATA, CLOUD = 0) { function write_database(DATA, CLOUD = 0) {
// If CLOUD is set to 0, it should automatically determine where the previous source of data was taken from. // If CLOUD is set to 0, it should automatically determine where the previous source of data was taken from.
if (CLOUD > 0) { if (CLOUD > 0) {
chrome.storage.sync.set(DATA); chrome.storage.sync.set(DATA);
} else if (CLOUD < 0) { } else if (CLOUD < 0) {
chrome.storage.local.set(DATA); chrome.storage.local.set(DATA);
} }
} }
/* Appropriately nest and merge the data. /* Appropriately nest and merge the data.
@param {object} EXISTING the original data @param {object} EXISTING the original data
@param {object} PATH the subpath @param {object} PATH the subpath
@param {object} VALUE the value @param {object} VALUE the value
@return {object} the updated data @return {object} the updated data
*/ */
function nest(EXISTING, SUBPATH, VALUE) { function nest(EXISTING, SUBPATH, VALUE) {
let DATABASE = EXISTING; let DATABASE = EXISTING;
// Get the current path. // Get the current path.
let PATH = {}; let PATH = {};
PATH[`current`] = String(SUBPATH.shift()).trim(); PATH[`current`] = String(SUBPATH.shift()).trim();
PATH[`target`] = SUBPATH; PATH[`target`] = SUBPATH;
if (PATH[`target`].length > 0) { if (PATH[`target`].length > 0) {
if (DATABASE[PATH[`current`]] == null) { if (DATABASE[PATH[`current`]] == null) {
DATABASE[PATH[`current`]] = {}; DATABASE[PATH[`current`]] = {};
} }
DATABASE[PATH[`current`]] = nest( DATABASE[PATH[`current`]] = nest(
DATABASE[PATH[`current`]], DATABASE[PATH[`current`]],
PATH[`target`], PATH[`target`],
VALUE, VALUE,
); );
} else { } else {
DATABASE[PATH[`current`]] = VALUE; DATABASE[PATH[`current`]] = VALUE;
} }
// Return the value. // Return the value.
return DATABASE; return DATABASE;
} }
read(null, CLOUD).then((DATA_ALL) => { read(null, CLOUD).then((DATA_ALL) => {
// handle empty collected data. // handle empty collected data.
if (!DATA_ALL) { if (!DATA_ALL) {
DATA_ALL = {}; DATA_ALL = {};
} }
let DATA_NAME = PATH; let DATA_NAME = PATH;
// Convert the entered prefname to an array if it is not one. // Convert the entered prefname to an array if it is not one.
if (!(typeof SUBPATH).includes(`object`)) { if (!(typeof SUBPATH).includes(`object`)) {
// Split what is not an object. // Split what is not an object.
DATA_NAME = String(PATH).trim().split(","); DATA_NAME = String(PATH).trim().split(",");
} }
// Merge! // Merge!
DATA_INJECTED = nest(DATA_ALL, DATA_NAME, DATA); DATA_INJECTED = nest(DATA_ALL, DATA_NAME, DATA);
// Write! // Write!
write_database(DATA_INJECTED, CLOUD); write_database(DATA_INJECTED, CLOUD);
}); });
} }
/* Dangerous: Resets all data or a domain's data. /* Dangerous: Resets all data or a domain's data.
@ -335,58 +308,58 @@ export function write(PATH, DATA, CLOUD = -1) {
@return {boolean} the user's confirmation @return {boolean} the user's confirmation
*/ */
export function forget(preference, subpreference, CLOUD = 0) { export function forget(preference, subpreference, CLOUD = 0) {
let forget_action = false; let forget_action = false;
(async () => { (async () => {
// Import alerts module. // Import alerts module.
let alerts = (await import(chrome.runtime.getURL(`gui/scripts/alerts.js`)))[ let alerts = (await import(chrome.runtime.getURL(`gui/scripts/alerts.js`)))[
`alerts` `alerts`
]; ];
// Confirm the action. // Confirm the action.
let forget_action = alerts.confirm_action(); let forget_action = alerts.confirm_action();
if (forget_action) { if (forget_action) {
if (preference) { if (preference) {
if (subpreference) { if (subpreference) {
// Get the data. // Get the data.
data = read(preference, CLOUD); data = read(preference, CLOUD);
// Should only run when existent // Should only run when existent
if (data[subpreference]) { if (data[subpreference]) {
delete data[subpreference]; delete data[subpreference];
write([preference, subpreference], data, CLOUD); write([preference, subpreference], data, CLOUD);
} }
} else { } else {
// Remove that particular data. // Remove that particular data.
if (CLOUD <= 0) { if (CLOUD <= 0) {
chrome.storage.local.get(null, (data) => { chrome.storage.local.get(null, (data) => {
delete data[preference]; delete data[preference];
chrome.storage.local.set(data, (result) => {}); chrome.storage.local.set(data, (result) => {});
}); });
} }
if (CLOUD >= 0) { if (CLOUD >= 0) {
chrome.storage.sync.get(null, (data) => { chrome.storage.sync.get(null, (data) => {
delete data[preference]; delete data[preference];
chrome.storage.sync.set(data, (result) => {}); chrome.storage.sync.set(data, (result) => {});
}); });
} }
} }
} else { } else {
// Clear the data storage. // Clear the data storage.
if (CLOUD >= 0) { if (CLOUD >= 0) {
chrome.storage.sync.clear(); chrome.storage.sync.clear();
} }
if (CLOUD <= 0) { if (CLOUD <= 0) {
chrome.storage.local.clear(); chrome.storage.local.clear();
} }
} }
} }
})(); })();
return forget_action; return forget_action;
} }
/* Initialize the storage. /* Initialize the storage.
@ -394,71 +367,71 @@ export function forget(preference, subpreference, CLOUD = 0) {
@param {dictionary} data this build's managed data @param {dictionary} data this build's managed data
*/ */
export function init(data) { export function init(data) {
let PREFERENCES_ALL = {}; let PREFERENCES_ALL = {};
PREFERENCES_ALL[`build`] = data; PREFERENCES_ALL[`build`] = data;
// Read all data. // Read all data.
chrome.storage.managed.get(null, function (DATA_MANAGED) { chrome.storage.managed.get(null, function (DATA_MANAGED) {
PREFERENCES_ALL[`managed`] = DATA_MANAGED; PREFERENCES_ALL[`managed`] = DATA_MANAGED;
}); });
chrome.storage.local.get(null, function (DATA_LOCAL) { chrome.storage.local.get(null, function (DATA_LOCAL) {
PREFERENCES_ALL[`local`] = DATA_LOCAL; PREFERENCES_ALL[`local`] = DATA_LOCAL;
}); });
chrome.storage.sync.get(null, function (DATA_SYNC) { chrome.storage.sync.get(null, function (DATA_SYNC) {
PREFERENCES_ALL[`sync`] = DATA_SYNC; PREFERENCES_ALL[`sync`] = DATA_SYNC;
}); });
// Merge data. // Merge data.
// Managed > Synchronized > Imported > Local // Managed > Synchronized > Imported > Local
if (PREFERENCES_ALL[`managed`]) { if (PREFERENCES_ALL[`managed`]) {
Object.keys(PREFERENCES_ALL[`managed`]).forEach((item) => { Object.keys(PREFERENCES_ALL[`managed`]).forEach((item) => {
let PREFERENCE = { name: item, existing: false }; let PREFERENCE = { name: item, existing: false };
if (PREFERENCES_ALL[`sync`]) { if (PREFERENCES_ALL[`sync`]) {
PREFERENCE[`existing`] = PREFERENCES_ALL[`sync`].hasOwnProperty( PREFERENCE[`existing`] = PREFERENCES_ALL[`sync`].hasOwnProperty(
PREFERENCE[`name`], PREFERENCE[`name`],
); );
} }
if (!PREFERENCE[`existing`]) { if (!PREFERENCE[`existing`]) {
// Do not allow synchronized data to interfere with managed data. // Do not allow synchronized data to interfere with managed data.
forget(PREFERENCE[`name`]); forget(PREFERENCE[`name`]);
write( write(
PREFERENCE[`name`], PREFERENCE[`name`],
PREFERENCES_ALL[`managed`][PREFERENCE[`name`]], PREFERENCES_ALL[`managed`][PREFERENCE[`name`]],
); );
} }
}); });
} }
// Import build data // Import build data
if (PREFERENCES_ALL[`build`]) { if (PREFERENCES_ALL[`build`]) {
Object.keys(PREFERENCES_ALL[`build`]).forEach((item) => { Object.keys(PREFERENCES_ALL[`build`]).forEach((item) => {
let PREFERENCE = { name: item, existing: false }; let PREFERENCE = { name: item, existing: false };
PREFERENCE[`existing`] = PREFERENCE[`existing`] =
(PREFERENCES_ALL[`sync`] (PREFERENCES_ALL[`sync`]
? PREFERENCES_ALL[`sync`].hasOwnProperty(PREFERENCE[`name`]) ? PREFERENCES_ALL[`sync`].hasOwnProperty(PREFERENCE[`name`])
: false) || : false) ||
(PREFERENCES_ALL[`managed`] (PREFERENCES_ALL[`managed`]
? PREFERENCES_ALL[`managed`].hasOwnProperty(PREFERENCE[`name`]) ? PREFERENCES_ALL[`managed`].hasOwnProperty(PREFERENCE[`name`])
: false) || : false) ||
(PREFERENCES_ALL[`local`] (PREFERENCES_ALL[`local`]
? PREFERENCES_ALL[`local`].hasOwnProperty(PREFERENCE[`local`]) ? PREFERENCES_ALL[`local`].hasOwnProperty(PREFERENCE[`local`])
: false); : false);
if (!PREFERENCE[`existing`]) { if (!PREFERENCE[`existing`]) {
write( write(
PREFERENCE[`name`], PREFERENCE[`name`],
PREFERENCES_ALL[`build`][PREFERENCE[`name`]], PREFERENCES_ALL[`build`][PREFERENCE[`name`]],
-1, -1,
); );
} }
}); });
} }
} }
/* /*
@ -467,7 +440,7 @@ Run a script when the browser storage has been changed.
@param {object} reaction the function to run @param {object} reaction the function to run
*/ */
export function observe(reaction) { export function observe(reaction) {
chrome.storage.onChanged.addListener((changes, namespace) => { chrome.storage.onChanged.addListener((changes, namespace) => {
reaction(changes, namespace); reaction(changes, namespace);
}); });
} }