mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 11:25:00 +00:00
Bug 1394977 - Move L10nRegistry to use async iterators. r=mossop
MozReview-Commit-ID: I6KuXyoItOQ --HG-- extra : rebase_source : 97c1ada528e3a1536ea64905023e5fa41311a9a6
This commit is contained in:
parent
66109d5421
commit
b45802f003
@ -36,7 +36,7 @@ Components.utils.importGlobalProperties(["fetch"]); /* globals fetch */
|
||||
* '/platform/toolkit.ftl'
|
||||
* ]);
|
||||
*
|
||||
* the generator will return an iterator over the following contexts:
|
||||
* the generator will return an async iterator over the following contexts:
|
||||
*
|
||||
* {
|
||||
* locale: 'de',
|
||||
@ -85,9 +85,9 @@ const L10nRegistry = {
|
||||
*
|
||||
* @param {Array} requestedLangs
|
||||
* @param {Array} resourceIds
|
||||
* @returns {Iterator<MessageContext>}
|
||||
* @returns {AsyncIterator<MessageContext>}
|
||||
*/
|
||||
* generateContexts(requestedLangs, resourceIds) {
|
||||
async * generateContexts(requestedLangs, resourceIds) {
|
||||
const sourcesOrder = Array.from(this.sources.keys()).reverse();
|
||||
for (const locale of requestedLangs) {
|
||||
yield * generateContextsForLocale(locale, sourcesOrder, resourceIds);
|
||||
@ -179,9 +179,9 @@ function generateContextID(locale, sourcesOrder, resourceIds) {
|
||||
* @param {Array} sourcesOrder
|
||||
* @param {Array} resourceIds
|
||||
* @param {Array} [resolvedOrder]
|
||||
* @returns {Iterator<MessageContext>}
|
||||
* @returns {AsyncIterator<MessageContext>}
|
||||
*/
|
||||
function* generateContextsForLocale(locale, sourcesOrder, resourceIds, resolvedOrder = []) {
|
||||
async function* generateContextsForLocale(locale, sourcesOrder, resourceIds, resolvedOrder = []) {
|
||||
const resolvedLength = resolvedOrder.length;
|
||||
const resourcesLength = resourceIds.length;
|
||||
|
||||
@ -202,7 +202,10 @@ function* generateContextsForLocale(locale, sourcesOrder, resourceIds, resolvedO
|
||||
// If the number of resolved sources equals the number of resources,
|
||||
// create the right context and return it if it loads.
|
||||
if (resolvedLength + 1 === resourcesLength) {
|
||||
yield generateContext(locale, order, resourceIds);
|
||||
const ctx = await generateContext(locale, order, resourceIds);
|
||||
if (ctx !== null) {
|
||||
yield ctx;
|
||||
}
|
||||
} else {
|
||||
// otherwise recursively load another generator that walks over the
|
||||
// partially resolved list of sources.
|
||||
@ -215,25 +218,41 @@ function* generateContextsForLocale(locale, sourcesOrder, resourceIds, resolvedO
|
||||
* Generates a single MessageContext by loading all resources
|
||||
* from the listed sources for a given locale.
|
||||
*
|
||||
* The function casts all error cases into a Promise that resolves with
|
||||
* value `null`.
|
||||
* This allows the caller to be an async generator without using
|
||||
* try/catch clauses.
|
||||
*
|
||||
* @param {String} locale
|
||||
* @param {Array} sourcesOrder
|
||||
* @param {Array} resourceIds
|
||||
* @returns {Promise<MessageContext>}
|
||||
*/
|
||||
async function generateContext(locale, sourcesOrder, resourceIds) {
|
||||
function generateContext(locale, sourcesOrder, resourceIds) {
|
||||
const ctxId = generateContextID(locale, sourcesOrder, resourceIds);
|
||||
if (!L10nRegistry.ctxCache.has(ctxId)) {
|
||||
const ctx = new MessageContext(locale);
|
||||
for (let i = 0; i < resourceIds.length; i++) {
|
||||
const data = await L10nRegistry.sources.get(sourcesOrder[i]).fetchFile(locale, resourceIds[i]);
|
||||
if (data === null) {
|
||||
return false;
|
||||
}
|
||||
ctx.addMessages(data);
|
||||
}
|
||||
L10nRegistry.ctxCache.set(ctxId, ctx);
|
||||
if (L10nRegistry.ctxCache.has(ctxId)) {
|
||||
return L10nRegistry.ctxCache.get(ctxId);
|
||||
}
|
||||
return L10nRegistry.ctxCache.get(ctxId);
|
||||
|
||||
const fetchPromises = resourceIds.map((resourceId, i) => {
|
||||
return L10nRegistry.sources.get(sourcesOrder[i]).fetchFile(locale, resourceId);
|
||||
});
|
||||
|
||||
const ctxPromise = Promise.all(fetchPromises).then(
|
||||
dataSets => {
|
||||
const ctx = new MessageContext(locale);
|
||||
for (const data of dataSets) {
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
ctx.addMessages(data);
|
||||
}
|
||||
return ctx;
|
||||
},
|
||||
() => null
|
||||
);
|
||||
L10nRegistry.ctxCache.set(ctxId, ctxPromise);
|
||||
return ctxPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -245,10 +264,33 @@ async function generateContext(locale, sourcesOrder, resourceIds) {
|
||||
* come from the cache.
|
||||
**/
|
||||
class FileSource {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Array<string>} locales
|
||||
* @param {string} prePath
|
||||
*
|
||||
* @returns {IndexedFileSource}
|
||||
*/
|
||||
constructor(name, locales, prePath) {
|
||||
this.name = name;
|
||||
this.locales = locales;
|
||||
this.prePath = prePath;
|
||||
this.indexed = false;
|
||||
|
||||
// The cache object stores information about the resources available
|
||||
// in the Source.
|
||||
//
|
||||
// It can take one of three states:
|
||||
// * true - the resource is available but not fetched yet
|
||||
// * false - the resource is not available
|
||||
// * Promise - the resource has been fetched
|
||||
//
|
||||
// If the cache has no entry for a given path, that means that there
|
||||
// is no information available about whether the resource is available.
|
||||
//
|
||||
// If the `indexed` property is set to `true` it will be treated as the
|
||||
// resource not being available. Otherwise, the resource may be
|
||||
// available and we do not have any information about it yet.
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
@ -263,31 +305,45 @@ class FileSource {
|
||||
|
||||
const fullPath = this.getPath(locale, path);
|
||||
if (!this.cache.hasOwnProperty(fullPath)) {
|
||||
return undefined;
|
||||
return this.indexed ? false : undefined;
|
||||
}
|
||||
|
||||
if (this.cache[fullPath] === null) {
|
||||
if (this.cache[fullPath] === false) {
|
||||
return false;
|
||||
}
|
||||
if (this.cache[fullPath].then) {
|
||||
return undefined;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async fetchFile(locale, path) {
|
||||
fetchFile(locale, path) {
|
||||
if (!this.locales.includes(locale)) {
|
||||
return null;
|
||||
return Promise.reject(`The source has no resources for locale "${locale}"`);
|
||||
}
|
||||
|
||||
const fullPath = this.getPath(locale, path);
|
||||
if (this.hasFile(locale, path) === undefined) {
|
||||
let file = await L10nRegistry.load(fullPath);
|
||||
|
||||
if (file === undefined) {
|
||||
this.cache[fullPath] = null;
|
||||
} else {
|
||||
this.cache[fullPath] = file;
|
||||
if (this.cache.hasOwnProperty(fullPath)) {
|
||||
if (this.cache[fullPath] === false) {
|
||||
return Promise.reject(`The source has no resources for path "${fullPath}"`);
|
||||
}
|
||||
if (this.cache[fullPath].then) {
|
||||
return this.cache[fullPath];
|
||||
}
|
||||
} else {
|
||||
if (this.indexed) {
|
||||
return Promise.reject(`The source has no resources for path "${fullPath}"`);
|
||||
}
|
||||
}
|
||||
return this.cache[fullPath];
|
||||
return this.cache[fullPath] = L10nRegistry.load(fullPath).then(
|
||||
data => {
|
||||
return this.cache[fullPath] = data;
|
||||
},
|
||||
err => {
|
||||
this.cache[fullPath] = false;
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,44 +355,40 @@ class FileSource {
|
||||
* contain most of the files that the app will request for (e.g. an addon).
|
||||
**/
|
||||
class IndexedFileSource extends FileSource {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Array<string>} locales
|
||||
* @param {string} prePath
|
||||
* @param {Array<string>} paths
|
||||
*
|
||||
* @returns {IndexedFileSource}
|
||||
*/
|
||||
constructor(name, locales, prePath, paths) {
|
||||
super(name, locales, prePath);
|
||||
this.paths = paths;
|
||||
}
|
||||
|
||||
hasFile(locale, path) {
|
||||
if (!this.locales.includes(locale)) {
|
||||
return false;
|
||||
}
|
||||
const fullPath = this.getPath(locale, path);
|
||||
return this.paths.includes(fullPath);
|
||||
}
|
||||
|
||||
async fetchFile(locale, path) {
|
||||
if (!this.locales.includes(locale)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fullPath = this.getPath(locale, path);
|
||||
if (this.paths.includes(fullPath)) {
|
||||
let file = await L10nRegistry.load(fullPath);
|
||||
|
||||
if (file === undefined) {
|
||||
return null;
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
this.indexed = true;
|
||||
for (const path of paths) {
|
||||
this.cache[path] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The low level wrapper around Fetch API. It unifies the error scenarios to
|
||||
* always produce a promise rejection.
|
||||
*
|
||||
* We keep it as a method to make it easier to override for testing purposes.
|
||||
**/
|
||||
*
|
||||
* @param {string} url
|
||||
*
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
L10nRegistry.load = function(url) {
|
||||
return fetch(url).then(data => data.text()).catch(() => undefined);
|
||||
return fetch(url).then(response => {
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.statusText);
|
||||
}
|
||||
return response.text()
|
||||
});
|
||||
};
|
||||
|
||||
this.L10nRegistry = L10nRegistry;
|
||||
|
@ -6,9 +6,13 @@ const {
|
||||
FileSource,
|
||||
IndexedFileSource
|
||||
} = Components.utils.import("resource://gre/modules/L10nRegistry.jsm", {});
|
||||
Components.utils.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
let fs;
|
||||
L10nRegistry.load = async function(url) {
|
||||
if (!fs.hasOwnProperty(url)) {
|
||||
return Promise.reject('Resource unavailable');
|
||||
}
|
||||
return fs[url];
|
||||
}
|
||||
|
||||
@ -27,14 +31,13 @@ add_task(async function test_methods_calling() {
|
||||
fs = {
|
||||
'/localization/en-US/browser/menu.ftl': 'key = Value',
|
||||
};
|
||||
const originalLoad = L10nRegistry.load;
|
||||
|
||||
const source = new FileSource('test', ['en-US'], '/localization/{locale}');
|
||||
L10nRegistry.registerSource(source);
|
||||
|
||||
const ctxs = L10nRegistry.generateContexts(['en-US'], ['/browser/menu.ftl']);
|
||||
|
||||
const ctx = await ctxs.next().value;
|
||||
const ctx = (await ctxs.next()).value;
|
||||
|
||||
equal(ctx.hasMessage('key'), true);
|
||||
|
||||
@ -64,17 +67,17 @@ add_task(async function test_has_one_source() {
|
||||
// returns a single context
|
||||
|
||||
let ctxs = L10nRegistry.generateContexts(['en-US'], ['test.ftl']);
|
||||
let ctx0 = await ctxs.next().value;
|
||||
let ctx0 = (await ctxs.next()).value;
|
||||
equal(ctx0.hasMessage('key'), true);
|
||||
|
||||
equal(ctxs.next().done, true);
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
|
||||
// returns no contexts for missing locale
|
||||
|
||||
ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
|
||||
|
||||
equal(ctxs.next().done, true);
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
// cleanup
|
||||
L10nRegistry.sources.clear();
|
||||
@ -107,31 +110,31 @@ add_task(async function test_has_two_sources() {
|
||||
// returns correct contexts for en-US
|
||||
|
||||
let ctxs = L10nRegistry.generateContexts(['en-US'], ['test.ftl']);
|
||||
let ctx0 = await ctxs.next().value;
|
||||
let ctx0 = (await ctxs.next()).value;
|
||||
|
||||
equal(ctx0.hasMessage('key'), true);
|
||||
let msg = ctx0.getMessage('key');
|
||||
equal(ctx0.format(msg), 'platform value');
|
||||
|
||||
equal(ctxs.next().done, true);
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
|
||||
// returns correct contexts for [pl, en-US]
|
||||
|
||||
ctxs = L10nRegistry.generateContexts(['pl', 'en-US'], ['test.ftl']);
|
||||
ctx0 = await ctxs.next().value;
|
||||
ctx0 = (await ctxs.next()).value;
|
||||
equal(ctx0.locales[0], 'pl');
|
||||
equal(ctx0.hasMessage('key'), true);
|
||||
let msg0 = ctx0.getMessage('key');
|
||||
equal(ctx0.format(msg0), 'app value');
|
||||
|
||||
let ctx1 = await ctxs.next().value;
|
||||
let ctx1 = (await ctxs.next()).value;
|
||||
equal(ctx1.locales[0], 'en-US');
|
||||
equal(ctx1.hasMessage('key'), true);
|
||||
let msg1 = ctx1.getMessage('key');
|
||||
equal(ctx1.format(msg1), 'platform value');
|
||||
|
||||
equal(ctxs.next().done, true);
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
// cleanup
|
||||
L10nRegistry.sources.clear();
|
||||
@ -188,19 +191,19 @@ add_task(async function test_override() {
|
||||
equal(L10nRegistry.sources.has('langpack-pl'), true);
|
||||
|
||||
let ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
|
||||
let ctx0 = await ctxs.next().value;
|
||||
let ctx0 = (await ctxs.next()).value;
|
||||
equal(ctx0.locales[0], 'pl');
|
||||
equal(ctx0.hasMessage('key'), true);
|
||||
let msg0 = ctx0.getMessage('key');
|
||||
equal(ctx0.format(msg0), 'addon value');
|
||||
|
||||
let ctx1 = await ctxs.next().value;
|
||||
let ctx1 = (await ctxs.next()).value;
|
||||
equal(ctx1.locales[0], 'pl');
|
||||
equal(ctx1.hasMessage('key'), true);
|
||||
let msg1 = ctx1.getMessage('key');
|
||||
equal(ctx1.format(msg1), 'value');
|
||||
|
||||
equal(ctxs.next().done, true);
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
// cleanup
|
||||
L10nRegistry.sources.clear();
|
||||
@ -221,7 +224,7 @@ add_task(async function test_updating() {
|
||||
};
|
||||
|
||||
let ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
|
||||
let ctx0 = await ctxs.next().value;
|
||||
let ctx0 = (await ctxs.next()).value;
|
||||
equal(ctx0.locales[0], 'pl');
|
||||
equal(ctx0.hasMessage('key'), true);
|
||||
let msg0 = ctx0.getMessage('key');
|
||||
@ -236,7 +239,7 @@ add_task(async function test_updating() {
|
||||
|
||||
equal(L10nRegistry.sources.size, 1);
|
||||
ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
|
||||
ctx0 = await ctxs.next().value;
|
||||
ctx0 = (await ctxs.next()).value;
|
||||
msg0 = ctx0.getMessage('key');
|
||||
equal(ctx0.format(msg0), 'new value');
|
||||
|
||||
@ -267,19 +270,19 @@ add_task(async function test_removing() {
|
||||
equal(L10nRegistry.sources.has('langpack-pl'), true);
|
||||
|
||||
let ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
|
||||
let ctx0 = await ctxs.next().value;
|
||||
let ctx0 = (await ctxs.next()).value;
|
||||
equal(ctx0.locales[0], 'pl');
|
||||
equal(ctx0.hasMessage('key'), true);
|
||||
let msg0 = ctx0.getMessage('key');
|
||||
equal(ctx0.format(msg0), 'addon value');
|
||||
|
||||
let ctx1 = await ctxs.next().value;
|
||||
let ctx1 = (await ctxs.next()).value;
|
||||
equal(ctx1.locales[0], 'pl');
|
||||
equal(ctx1.hasMessage('key'), true);
|
||||
let msg1 = ctx1.getMessage('key');
|
||||
equal(ctx1.format(msg1), 'value');
|
||||
|
||||
equal(ctxs.next().done, true);
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
// Remove langpack
|
||||
|
||||
@ -289,13 +292,13 @@ add_task(async function test_removing() {
|
||||
equal(L10nRegistry.sources.has('langpack-pl'), false);
|
||||
|
||||
ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
|
||||
ctx0 = await ctxs.next().value;
|
||||
ctx0 = (await ctxs.next()).value;
|
||||
equal(ctx0.locales[0], 'pl');
|
||||
equal(ctx0.hasMessage('key'), true);
|
||||
msg0 = ctx0.getMessage('key');
|
||||
equal(ctx0.format(msg0), 'value');
|
||||
|
||||
equal(ctxs.next().done, true);
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
// Remove app source
|
||||
|
||||
@ -304,9 +307,109 @@ add_task(async function test_removing() {
|
||||
equal(L10nRegistry.sources.size, 0);
|
||||
|
||||
ctxs = L10nRegistry.generateContexts(['pl'], ['test.ftl']);
|
||||
equal(ctxs.next().done, true);
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
// cleanup
|
||||
L10nRegistry.sources.clear();
|
||||
L10nRegistry.ctxCache.clear();
|
||||
});
|
||||
|
||||
/**
|
||||
* This test verifies that the logic works correctly when there's a missing
|
||||
* file in the FileSource scenario.
|
||||
*/
|
||||
add_task(async function test_missing_file() {
|
||||
let oneSource = new FileSource('app', ['en-US'], './app/data/locales/{locale}/');
|
||||
L10nRegistry.registerSource(oneSource);
|
||||
let twoSource = new FileSource('platform', ['en-US'], './platform/data/locales/{locale}/');
|
||||
L10nRegistry.registerSource(twoSource);
|
||||
|
||||
fs = {
|
||||
'./app/data/locales/en-US/test.ftl': 'key = value en-US',
|
||||
'./platform/data/locales/en-US/test.ftl': 'key = value en-US',
|
||||
'./platform/data/locales/en-US/test2.ftl': 'key2 = value2 en-US'
|
||||
};
|
||||
|
||||
|
||||
// has two sources
|
||||
|
||||
equal(L10nRegistry.sources.size, 2);
|
||||
equal(L10nRegistry.sources.has('app'), true);
|
||||
equal(L10nRegistry.sources.has('platform'), true);
|
||||
|
||||
|
||||
// returns a single context
|
||||
|
||||
let ctxs = L10nRegistry.generateContexts(['en-US'], ['test.ftl', 'test2.ftl']);
|
||||
let ctx0 = (await ctxs.next()).value;
|
||||
let ctx1 = (await ctxs.next()).value;
|
||||
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
|
||||
// cleanup
|
||||
L10nRegistry.sources.clear();
|
||||
L10nRegistry.ctxCache.clear();
|
||||
});
|
||||
|
||||
/**
|
||||
* This test verifies that each file is that all files requested
|
||||
* by a single context are fetched at the same time, even
|
||||
* if one I/O is slow.
|
||||
*/
|
||||
add_task(async function test_parallel_io() {
|
||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||
let originalLoad = L10nRegistry.load;
|
||||
let fetchIndex = new Map();
|
||||
|
||||
L10nRegistry.load = function(url) {
|
||||
if (!fetchIndex.has(url)) {
|
||||
fetchIndex.set(url, 0);
|
||||
}
|
||||
fetchIndex.set(url, fetchIndex.get(url) + 1);
|
||||
|
||||
if (url === '/en-US/slow-file.ftl') {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
// Despite slow-file being the first on the list,
|
||||
// by the time the it finishes loading, the other
|
||||
// two files are already fetched.
|
||||
equal(fetchIndex.get('/en-US/test.ftl'), 1);
|
||||
equal(fetchIndex.get('/en-US/test2.ftl'), 1);
|
||||
|
||||
resolve('');
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
return Promise.resolve('');
|
||||
}
|
||||
let oneSource = new FileSource('app', ['en-US'], '/{locale}/');
|
||||
L10nRegistry.registerSource(oneSource);
|
||||
|
||||
fs = {
|
||||
'/en-US/test.ftl': 'key = value en-US',
|
||||
'/en-US/test2.ftl': 'key2 = value2 en-US',
|
||||
'/en-US/slow-file.ftl': 'key-slow = value slow en-US',
|
||||
};
|
||||
|
||||
// returns a single context
|
||||
|
||||
let ctxs = L10nRegistry.generateContexts(['en-US'], ['slow-file.ftl', 'test.ftl', 'test2.ftl']);
|
||||
|
||||
equal(fetchIndex.size, 0);
|
||||
|
||||
let ctx0 = await ctxs.next();
|
||||
|
||||
equal(ctx0.done, false);
|
||||
|
||||
equal((await ctxs.next()).done, true);
|
||||
|
||||
// When requested again, the cache should make the load operation not
|
||||
// increase the fetchedIndex count
|
||||
let ctxs2= L10nRegistry.generateContexts(['en-US'], ['test.ftl', 'test2.ftl', 'slow-file.ftl']);
|
||||
|
||||
// cleanup
|
||||
L10nRegistry.sources.clear();
|
||||
L10nRegistry.ctxCache.clear();
|
||||
L10nRegistry.load = originalLoad;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user