Bug 1394977 - Move L10nRegistry to use async iterators. r=mossop
authorZibi Braniecki <zbraniecki@mozilla.com>
Fri, 08 Sep 2017 23:19:49 -0700
changeset 429341 e0d88cd771d6791be4eaed4ada9315f4b26e2ad1
parent 429340 b8254b550772912407a04b3c771e42b688d338e9
child 429382 c71b01e993510268bab7d60154b2f80692fd507d
child 429388 0dadc40f8fd55e2a86eb444419550035ecf0ae8a
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmossop
bugs1394977
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1394977 - Move L10nRegistry to use async iterators. r=mossop MozReview-Commit-ID: 5szrQ1UWJ64
intl/l10n/L10nRegistry.jsm
intl/l10n/Localization.jsm
intl/l10n/test/dom/test_domloc.xul
intl/l10n/test/dom/test_domloc_connectRoot.html
intl/l10n/test/dom/test_domloc_disconnectRoot.html
intl/l10n/test/dom/test_domloc_getAttributes.html
intl/l10n/test/dom/test_domloc_mutations.html
intl/l10n/test/dom/test_domloc_overlay.html
intl/l10n/test/dom/test_domloc_overlay_missing_children.html
intl/l10n/test/dom/test_domloc_overlay_repeated.html
intl/l10n/test/dom/test_domloc_setAttributes.html
intl/l10n/test/dom/test_domloc_translateElement.html
intl/l10n/test/dom/test_domloc_translateFragment.html
intl/l10n/test/dom/test_domloc_translateRoots.html
intl/l10n/test/test_l10nregistry.js
intl/l10n/test/test_localization.js
--- a/intl/l10n/L10nRegistry.jsm
+++ b/intl/l10n/L10nRegistry.jsm
@@ -31,17 +31,17 @@ Components.utils.importGlobalProperties(
  *     ]
  *
  * If the user will request:
  *   L10nRegistry.generateContexts(['de', 'en-US'], [
  *     '/browser/menu.ftl',
  *     '/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',
  *     resources: [
  *       ['app', '/browser/menu.ftl'],
  *       ['app', '/platform/toolkit.ftl'],
  *     ]
  *   },
@@ -80,19 +80,19 @@ const L10nRegistry = {
   ctxCache: new Map(),
 
   /**
    * Based on the list of requested languages and resource Ids,
    * this function returns an lazy iterator over message context permutations.
    *
    * @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);
     }
   },
 
   /**
    * Adds a new resource source to the L10nRegistry.
@@ -174,19 +174,19 @@ function generateContextID(locale, sourc
  * This function is called recursively to generate all possible permutations
  * and uses the last, optional parameter, to pass the already resolved
  * sources order.
  *
  * @param {String} locale
  * @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;
 
   // Inside that loop we have a list of resources and the sources for them, like this:
   //   ['test.ftl', 'menu.ftl', 'foo.ftl']
   //   ['app', 'platform', 'app']
   for (const sourceName of sourcesOrder) {
     const order = resolvedOrder.concat(sourceName);
@@ -197,150 +197,202 @@ function* generateContextsForLocale(loca
     // have to perform the I/O to learn.
     if (L10nRegistry.sources.get(sourceName).hasFile(locale, resourceIds[resolvedOrder.length]) === false) {
       continue;
     }
 
     // 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.
       yield * generateContextsForLocale(locale, sourcesOrder, resourceIds, order);
     }
   }
 }
 
 /**
  * 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;
+  if (L10nRegistry.ctxCache.has(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);
       }
-      ctx.addMessages(data);
-    }
-    L10nRegistry.ctxCache.set(ctxId, ctx);
-  }
-  return L10nRegistry.ctxCache.get(ctxId);
+      return ctx;
+    },
+    () => null
+  );
+  L10nRegistry.ctxCache.set(ctxId, ctxPromise);
+  return ctxPromise;
 }
 
 /**
  * This is a basic Source for L10nRegistry.
  * It registers its own locales and a pre-path, and when asked for a file
  * it attempts to download and cache it.
  *
  * The Source caches the downloaded files so any consecutive loads will
  * 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 = {};
   }
 
   getPath(locale, path) {
     return (this.prePath + path).replace(/\{locale\}/g, locale);
   }
 
   hasFile(locale, path) {
     if (!this.locales.includes(locale)) {
       return false;
     }
 
     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);
+      }
+    );
   }
 }
 
 /**
  * This is an extension of the FileSource which should be used
  * for sources that can provide the list of files available in the source.
  *
  * This allows for a faster lookup in cases where the source does not
  * 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;
 this.FileSource = FileSource;
 this.IndexedFileSource = IndexedFileSource;
 
 this.EXPORTED_SYMBOLS = ['L10nRegistry', 'FileSource', 'IndexedFileSource'];
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -31,32 +31,32 @@ const ObserverService = Cc["@mozilla.org
 /**
  * CachedIterable caches the elements yielded by an iterable.
  *
  * It can be used to iterate over an iterable many times without depleting the
  * iterable.
  */
 class CachedIterable {
   constructor(iterable) {
-    if (!(Symbol.iterator in Object(iterable))) {
-      throw new TypeError('Argument must implement the iteration protocol.');
+    if (!(Symbol.asyncIterator in Object(iterable))) {
+      throw new TypeError('Argument must implement the async iteration protocol.');
     }
 
-    this.iterator = iterable[Symbol.iterator]();
+    this.iterator = iterable[Symbol.asyncIterator]();
     this.seen = [];
   }
 
-  [Symbol.iterator]() {
+  [Symbol.asyncIterator]() {
     const { seen, iterator } = this;
     let cur = 0;
 
     return {
-      next() {
+      async next() {
         if (seen.length <= cur) {
-          seen.push(iterator.next());
+          seen.push(await iterator.next());
         }
         return seen[cur++];
       }
     };
   }
 }
 
 /**
@@ -126,17 +126,17 @@ class Localization {
    *
    * @param   {Array<Array>}          keys    - Translation keys to format.
    * @param   {Function}              method  - Formatting function.
    * @returns {Promise<Array<string|Object>>}
    * @private
    */
   async formatWithFallback(keys, method) {
     const translations = [];
-    for (let ctx of this.ctxs) {
+    for await (let ctx of this.ctxs) {
       // This can operate on synchronous and asynchronous
       // contexts coming from the iterator.
       if (typeof ctx.then === 'function') {
         ctx = await ctx;
       }
       const errors = keysFromContext(method, ctx, keys, translations);
       if (!errors) {
         break;
--- a/intl/l10n/test/dom/test_domloc.xul
+++ b/intl/l10n/test/dom/test_domloc.xul
@@ -11,17 +11,17 @@
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
   <script type="application/javascript">
   <![CDATA[
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
 
-  function * generateMessages(locales, resourceIds) {
+  async function * generateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages(`
 file-menu
     .label = File
     .accesskey = F
 new-tab
     .label = New Tab
     .accesskey = N
--- a/intl/l10n/test/dom/test_domloc_connectRoot.html
+++ b/intl/l10n/test/dom/test_domloc_connectRoot.html
@@ -5,17 +5,17 @@
   <title>Test DOMLocalization.prototype.connectRoot</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {
+  async function * mockGenerateMessages(locales, resourceIds) {
   }
 
   window.onload = async function () {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       window,
       [],
--- a/intl/l10n/test/dom/test_domloc_disconnectRoot.html
+++ b/intl/l10n/test/dom/test_domloc_disconnectRoot.html
@@ -5,17 +5,17 @@
   <title>Test DOMLocalization.prototype.disconnectRoot</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {
+  async function * mockGenerateMessages(locales, resourceIds) {
   }
 
   window.onload = async function () {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       window,
       [],
--- a/intl/l10n/test/dom/test_domloc_getAttributes.html
+++ b/intl/l10n/test/dom/test_domloc_getAttributes.html
@@ -5,17 +5,17 @@
   <title>Test DOMLocalization.prototype.getAttributes</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {}
+  async function * mockGenerateMessages(locales, resourceIds) {}
 
   window.onload = function () {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       window,
       [],
       mockGenerateMessages
--- a/intl/l10n/test/dom/test_domloc_mutations.html
+++ b/intl/l10n/test/dom/test_domloc_mutations.html
@@ -7,17 +7,17 @@
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {
+  async function * mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages('title = Hello World');
     mc.addMessages('title2 = Hello Another World');
     yield mc;
   }
 
   window.onload = async function () {
     SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_overlay.html
+++ b/intl/l10n/test/dom/test_domloc_overlay.html
@@ -7,17 +7,17 @@
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {
+  async function * mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages('title = <strong>Hello</strong> World');
     mc.addMessages('title2 = This is <a>a link</a>!');
     yield mc;
   }
 
   window.onload = async function () {
     SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_overlay_missing_children.html
+++ b/intl/l10n/test/dom/test_domloc_overlay_missing_children.html
@@ -7,17 +7,17 @@
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {
+  async function * mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages('title = Visit <a>Mozilla</a> or <a>Firefox</a> website!');
     yield mc;
   }
 
   window.onload = async function () {
     SimpleTest.waitForExplicitFinish();
 
--- a/intl/l10n/test/dom/test_domloc_overlay_repeated.html
+++ b/intl/l10n/test/dom/test_domloc_overlay_repeated.html
@@ -7,17 +7,17 @@
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {
+  async function * mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages('title = Visit <a>Mozilla</a> or <a>Firefox</a> website!');
     yield mc;
   }
 
   window.onload = async function () {
     SimpleTest.waitForExplicitFinish();
 
--- a/intl/l10n/test/dom/test_domloc_setAttributes.html
+++ b/intl/l10n/test/dom/test_domloc_setAttributes.html
@@ -5,17 +5,17 @@
   <title>Test DOMLocalization.prototype.setAttributes</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {}
+  async function * mockGenerateMessages(locales, resourceIds) {}
 
   window.onload = function () {
     SimpleTest.waitForExplicitFinish();
 
     const domLoc = new DOMLocalization(
       window,
       [],
       mockGenerateMessages
--- a/intl/l10n/test/dom/test_domloc_translateElement.html
+++ b/intl/l10n/test/dom/test_domloc_translateElement.html
@@ -7,17 +7,17 @@
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {
+  async function * mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages('title = Hello World');
     mc.addMessages('link\n    .title = Click me');
     yield mc;
   }
 
   window.onload = async function () {
     SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_translateFragment.html
+++ b/intl/l10n/test/dom/test_domloc_translateFragment.html
@@ -7,17 +7,17 @@
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {
+  async function * mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages('title = Hello World');
     mc.addMessages('subtitle = Welcome to Fluent');
     yield mc;
   }
 
   window.onload = async function () {
     SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/dom/test_domloc_translateRoots.html
+++ b/intl/l10n/test/dom/test_domloc_translateRoots.html
@@ -7,17 +7,17 @@
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   <script type="application/javascript">
   "use strict";
   const { DOMLocalization } =
     Components.utils.import("resource://gre/modules/DOMLocalization.jsm", {});
   const { MessageContext } =
     Components.utils.import("resource://gre/modules/MessageContext.jsm", {});
 
-  function * mockGenerateMessages(locales, resourceIds) {
+  async function * mockGenerateMessages(locales, resourceIds) {
     const mc = new MessageContext(locales);
     mc.addMessages('title = Hello World');
     mc.addMessages('title2 = Hello Another World');
     yield mc;
   }
 
   window.onload = async function () {
     SimpleTest.waitForExplicitFinish();
--- a/intl/l10n/test/test_l10nregistry.js
+++ b/intl/l10n/test/test_l10nregistry.js
@@ -1,19 +1,23 @@
 /* Any copyrighequal dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const {
   L10nRegistry,
   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];
 }
 
 add_task(function test_methods_presence() {
   equal(typeof L10nRegistry.generateContexts, "function");
   equal(typeof L10nRegistry.getAvailableLocales, "function");
   equal(typeof L10nRegistry.registerSource, "function");
   equal(typeof L10nRegistry.updateSource, "function");
@@ -22,24 +26,23 @@ add_task(function test_methods_presence(
 /**
  * This test tests generation of a proper context for a single
  * source scenario
  */
 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);
 
   // cleanup
   L10nRegistry.sources.clear();
   L10nRegistry.ctxCache.clear();
 });
 
@@ -59,27 +62,27 @@ add_task(async function test_has_one_sou
 
   equal(L10nRegistry.sources.size, 1);
   equal(L10nRegistry.sources.has('app'), true);
 
 
   // 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();
   L10nRegistry.ctxCache.clear();
 });
 
 /**
  * This test verifies that public methods return expected values
@@ -102,41 +105,41 @@ add_task(async function test_has_two_sou
   equal(L10nRegistry.sources.size, 2);
   equal(L10nRegistry.sources.has('app'), true);
   equal(L10nRegistry.sources.has('platform'), true);
 
 
   // 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();
   L10nRegistry.ctxCache.clear();
 });
 
 /**
  * This test verifies that behavior specific to the IndexedFileSource
@@ -183,29 +186,29 @@ add_task(async function test_override() 
     '/app/data/locales/pl/test.ftl': 'key = value',
     '/data/locales/pl/test.ftl': 'key = addon value'
   };
 
   equal(L10nRegistry.sources.size, 2);
   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();
   L10nRegistry.ctxCache.clear();
 });
 
 /**
  * This test verifies that new contexts are returned
@@ -216,32 +219,32 @@ add_task(async function test_updating() 
     '/data/locales/pl/test.ftl',
   ]);
   L10nRegistry.registerSource(oneSource);
   fs = {
     '/data/locales/pl/test.ftl': 'key = value'
   };
 
   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), 'value');
 
 
   const newSource = new IndexedFileSource('langpack-pl', ['pl'], '/data/locales/{locale}/', [
     '/data/locales/pl/test.ftl'
   ]);
   fs['/data/locales/pl/test.ftl'] = 'key = new value';
   L10nRegistry.updateSource(newSource);
 
   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');
 
   // cleanup
   L10nRegistry.sources.clear();
   L10nRegistry.ctxCache.clear();
 });
 
@@ -262,51 +265,151 @@ add_task(async function test_removing() 
     '/app/data/locales/pl/test.ftl': 'key = value',
     '/data/locales/pl/test.ftl': 'key = addon value'
   };
 
   equal(L10nRegistry.sources.size, 2);
   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
 
   L10nRegistry.removeSource('langpack-pl');
 
   equal(L10nRegistry.sources.size, 1);
   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
 
   L10nRegistry.removeSource('app');
 
   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;
+});
--- a/intl/l10n/test/test_localization.js
+++ b/intl/l10n/test/test_localization.js
@@ -18,25 +18,25 @@ add_task(async function test_methods_cal
 
   const fs = {
     '/localization/de/browser/menu.ftl': 'key = [de] Value2',
     '/localization/en-US/browser/menu.ftl': 'key = [en] Value2\nkey2 = [en] Value3',
   };
   const originalLoad = L10nRegistry.load;
   const originalRequested = LocaleService.getRequestedLocales();
 
-  L10nRegistry.load = function(url) {
+  L10nRegistry.load = async function(url) {
     return fs[url];
   }
 
   const source = new FileSource('test', ['de', 'en-US'], '/localization/{locale}');
   L10nRegistry.registerSource(source);
 
-  function * generateMessages(resIds) {
-    yield * L10nRegistry.generateContexts(['de', 'en-US'], resIds);
+  async function * generateMessages(resIds) {
+    yield * await L10nRegistry.generateContexts(['de', 'en-US'], resIds);
   }
 
   const l10n = new Localization([
     '/browser/menu.ftl'
   ], generateMessages);
 
   let values = await l10n.formatValues([['key'], ['key2']]);