blob: 248c6cb60971dc140c352d380a53112e346f92a5 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Contains definition for modules and the module loader.
* The Media Router extension is logically separated into modules. Each module
* and their corresponding bundle path are registered at startup
* time. When a module is required, they will be loaded on-demand.
*/
goog.provide('mr.Module');
goog.provide('mr.ModuleId');
goog.require('mr.Logger');
goog.require('mr.PromiseResolver');
/**
* Identifier for each module.
* @enum {string}
*/
mr.ModuleId = {
CAST_CHANNEL_SERVICE: 'mr.cast.ChannelService',
CAST_SINK_DISCOVERY_SERVICE: 'mr.cast.SinkDiscoveryService',
CAST_STREAMING_SERVICE: 'mr.mirror.cast.Service',
SLARTI_SINK_DISCOVERY_SERVICE: 'mr.cloud.slarti.SinkDiscoveryService',
WEAVE_SINK_DISCOVERY_SERVICE: 'mr.cloud.discovery.WeaveSinkDiscoveryService',
HANGOUTS_SERVICE: 'mr.mirror.hangouts.HangoutsService',
MEETINGS_SERVICE: 'mr.mirror.hangouts.MeetingsService',
PROVIDER_MANAGER: 'mr.ProviderManager',
WEBRTC_STREAMING_SERVICE: 'mr.mirror.webrtc.WebRtcService'
};
/**
* Identifier for bundles. A bundle is a js file containing a collection of
* modules.
* @enum {string}
*/
mr.Bundle = {
MAIN: 'background_script.js',
MIRRORING_CAST_STREAMING: 'mirroring_cast_streaming.js',
MIRRORING_HANGOUTS: 'mirroring_hangouts.js',
MIRRORING_WEBRTC: 'mirroring_webrtc.js'
};
/**
* Base class for a module. When a module is loaded and initialized, it should
* call mr.Module.onModuleLoaded to inform its dependencies that it is ready.
*/
mr.Module = class {
/**
* Returns the module with the given ID if it is already initialized, null
* otherwise.
* @param {mr.ModuleId} moduleId
* @return {?mr.Module}
*/
static get(moduleId) {
return mr.Module.moduleById_.get(moduleId) || null;
}
/**
* Loads the module with the given ID. If the module is already loaded, the
* Promise is resolved immediately.
* @param {mr.ModuleId} moduleId
* @return {!Promise<!mr.Module>} Resolved with the module when
* it is loaded.
*/
static load(moduleId) {
const module = mr.Module.get(moduleId);
if (module) {
return Promise.resolve(module);
}
let resolver = mr.Module.resolverByModuleId_.get(moduleId);
if (!resolver) {
resolver = new mr.PromiseResolver();
mr.Module.resolverByModuleId_.set(moduleId, resolver);
mr.Module.loadBundleForModule_(moduleId, resolver);
}
return resolver.promise;
}
/**
* Loads the bundle corresponding to the given module. Called the first time
* a module is requested.
* @param {mr.ModuleId} moduleId
* @param {!mr.PromiseResolver<!mr.Module>} resolver Rejected if the bundle
* associated with the module won't be loaded due to permanent error.
* @private
*/
static loadBundleForModule_(moduleId, resolver) {
const bundle = mr.Module.getBundle_(moduleId);
if (!bundle) {
resolver.reject(new Error(`No corresponding bundle for ${moduleId}`));
return;
}
if (mr.Module.DEFAULT_LOAD_BUNDLES_.has(bundle)) {
return;
}
// Check if the bundle has been not requested previously.
let bundlePromise = mr.Module.bundlePromises_.get(bundle);
if (!bundlePromise) {
mr.Module.logger_.info(`Loading bundle ${bundle} for module ${moduleId}`);
bundlePromise = mr.Module.doLoadBundle_(bundle);
mr.Module.bundlePromises_.set(bundle, bundlePromise);
}
// Add an error handler to reject the module load request.
bundlePromise.catch(e => {
resolver.reject(e);
});
}
/**
* Returns the bundle associated with a module.
* @param {mr.ModuleId} moduleId
* @return {?mr.Bundle}
* @private
*/
static getBundle_(moduleId) {
return mr.Module.MODULE_TO_BUNDLE_MAPPING_.get(moduleId) || null;
}
/**
* Called when a bundle needs to be loaded.
* @param {mr.Bundle} bundle Name of bundle to load.
* @return {!Promise<void>} Resolves when the bundle is loaded or rejected if
* it failed to load.
* @private
*/
static doLoadBundle_(bundle) {
let resolver = new mr.PromiseResolver();
resolver.promise.then(
() => {
mr.Module.logger_.info(`Bundle ${bundle} loaded`);
},
e => {
mr.Module.logger_.error(`Failed to load bundle ${bundle}`);
throw e;
});
let script = document.createElement('script');
script.src = chrome.extension.getURL(bundle);
script.setAttribute('type', 'text/javascript');
script.async = true;
script.onload = () => resolver.resolve(undefined);
script.onerror = () =>
resolver.reject(new Error(`Failed to load bundle ${bundle}`));
document.head.appendChild(script);
return resolver.promise;
}
/**
* Called by a module when it is done loading and initializing. Registers the
* module and resolves the outstanding promise returned by |load(moduleId)|.
* @param {mr.ModuleId} moduleId Identifier of the module. No two modules can
* have the same identifier.
* @param {!mr.Module} module The module that is ready.
*/
static onModuleLoaded(moduleId, module) {
if (mr.Module.moduleById_.has(moduleId)) {
throw new Error('Duplicate module ' + moduleId);
}
mr.Module.moduleById_.set(moduleId, module);
const resolver = mr.Module.resolverByModuleId_.get(moduleId);
if (resolver) {
resolver.resolve(module);
}
}
/**
* Used for testing only.
*/
static clearForTest() {
mr.Module.moduleById_.clear();
mr.Module.resolverByModuleId_.clear();
mr.Module.bundlePromises_.clear();
}
/**
* Subclasses should override this if a mr.EventListener designated this
* module to forward the events to.
* @param {*} event The event delivered to the handler. It is the handler's
* responsibility to verify that it can handle the event.
* @param {...*} args Arguments for the event.
*/
handleEvent(event, ...args) {
throw new Error('Not implemented');
}
};
/**
* Maps a module ID to a bundle ID. Used for loading the bundle that contains
* a required module.
* @private @const {!Map<mr.ModuleId, mr.Bundle>}
*/
mr.Module.MODULE_TO_BUNDLE_MAPPING_ = new Map([
[mr.ModuleId.CAST_CHANNEL_SERVICE, mr.Bundle.MAIN],
[mr.ModuleId.CAST_SINK_DISCOVERY_SERVICE, mr.Bundle.MAIN],
[mr.ModuleId.CAST_STREAMING_SERVICE, mr.Bundle.MIRRORING_CAST_STREAMING],
[mr.ModuleId.SLARTI_SINK_DISCOVERY_SERVICE, mr.Bundle.MAIN],
[mr.ModuleId.WEAVE_SINK_DISCOVERY_SERVICE, mr.Bundle.MAIN],
[mr.ModuleId.HANGOUTS_SERVICE, mr.Bundle.MIRRORING_HANGOUTS],
[mr.ModuleId.MEETINGS_SERVICE, mr.Bundle.MIRRORING_HANGOUTS],
[mr.ModuleId.PROVIDER_MANAGER, mr.Bundle.MAIN],
[mr.ModuleId.WEBRTC_STREAMING_SERVICE, mr.Bundle.MIRRORING_WEBRTC]
]);
/**
* Set of bundles that are loaded by default.
* @private @const {!Set<mr.Bundle>}
*/
mr.Module.DEFAULT_LOAD_BUNDLES_ = new Set([mr.Bundle.MAIN]);
/** @private {mr.Logger} */
mr.Module.logger_ = mr.Logger.getInstance('mr.Module');
/**
* The set of modules currently loaded and initialized in the extension, keyed
* by their IDs.
* @private {!Map<mr.ModuleId, !mr.Module>}
*/
mr.Module.moduleById_ = new Map();
/**
* Holds the outstanding promise while a module is being loaded.
* @private {!Map<mr.ModuleId, !mr.PromiseResolver<!mr.Module>>}
*/
mr.Module.resolverByModuleId_ = new Map();
/**
* Holds the outstanding promise while a bundle is being loaded.
* @private {!Map<mr.Bundle, !Promise<void>>}
*/
mr.Module.bundlePromises_ = new Map();