@amo-tm/wsc
Version:
The amo WSC component of the amo JS SDK
576 lines (561 loc) • 20.3 kB
JavaScript
class Deferred {
constructor() {
this.reject = () => { };
this.resolve = () => { };
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
const DEFAULT_INSTANCE_NAME = '[DEFAULT_INSTANCE_NAME]';
/**
* Provider for instance for service name T, e.g. 'wsc', 'wsc-connector-internal'
* NameServiceMapping[T] is an alias for the type of the instance
*/
class Provider {
constructor(name, container) {
this.name = name;
this.container = container;
this.component = null;
this.instances = new Map();
this.instancesDeferred = new Map();
}
get() {
const normalizedIdentifier = this.normalizeInstanceIdentifier();
if (this.instances.has(normalizedIdentifier)) {
return Promise.resolve(this.instances.get(normalizedIdentifier));
}
if (!this.instancesDeferred.has(normalizedIdentifier)) {
if (this.isInitialized(normalizedIdentifier) || this.shouldAutoInitialize()) {
// initialize the service if it can be auto-initialized
try {
void this.getOrInitializeService({
instanceIdentifier: normalizedIdentifier,
});
}
catch (e) {
// when the instance factory throws an exception during get(), it should not cause
// a fatal error. We just return the unresolved promise in this case.
}
}
else {
const deferred = new Deferred();
this.instancesDeferred.set(normalizedIdentifier, deferred);
}
}
return this.instancesDeferred.get(normalizedIdentifier).promise;
}
getImmediate(options) {
var _a;
const normalizedIdentifier = this.normalizeInstanceIdentifier();
const optional = (_a = options === null || options === void 0 ? void 0 : options.optional) !== null && _a !== void 0 ? _a : false;
if (this.isInitialized(normalizedIdentifier) || this.shouldAutoInitialize()) {
try {
const instanceOrInstancePromise = this.getOrInitializeService({
instanceIdentifier: normalizedIdentifier,
});
if (instanceOrInstancePromise instanceof Promise) {
if (optional) {
return null;
}
else {
throw Error(`Service ${this.name} is not ready.`);
}
}
return instanceOrInstancePromise;
}
catch (e) {
if (optional) {
return null;
}
else {
throw e;
}
}
}
else {
// In case a component is not initialized and should/can not be auto-initialized at the moment, return null if the optional flag is set, or throw
if (optional) {
return null;
}
else {
throw Error(`Service ${this.name} is not available.`);
}
}
}
setComponent(component) {
if (component.name !== this.name) {
throw Error(`Mismatching Component ${component.name} for Provider ${this.name}.`);
}
if (this.component) {
throw Error(`Component for ${this.name} has already been provided`);
}
this.component = component;
// return early without attempting to initialize the component
if (!this.shouldAutoInitialize()) {
return;
}
// if the service is eager, initialize the default instance
if (isComponentEager(component)) {
try {
void this.getOrInitializeService({ instanceIdentifier: DEFAULT_INSTANCE_NAME });
}
catch (e) {
// when the instance factory for an eager Component throws an exception during the eager
// initialization, it should not cause a fatal error.
}
}
}
isComponentSet() {
return this.component !== null;
}
isInitialized(identifier = DEFAULT_INSTANCE_NAME) {
return this.instances.has(identifier);
}
getOrInitializeService({ instanceIdentifier, options = {}, }) {
let instance = this.instances.get(instanceIdentifier);
let instanceDeferred = this.instancesDeferred.get(instanceIdentifier);
if (this.component && !(instance || instanceDeferred)) {
const instanceOrInstancePromise = this.component.instanceFactory(this.container, {
options,
});
if (instanceOrInstancePromise instanceof Promise) {
instanceDeferred = instanceDeferred || new Deferred();
this.instancesDeferred.set(instanceIdentifier, instanceDeferred);
instanceOrInstancePromise.then((instance) => {
const currentInstanceDeferred = this.instancesDeferred.get(instanceIdentifier);
if (!currentInstanceDeferred || currentInstanceDeferred !== instanceDeferred) {
return;
}
this.instances.set(instanceIdentifier, instance);
this.instancesDeferred.delete(instanceIdentifier);
currentInstanceDeferred.resolve(instance);
}, instanceDeferred.reject);
}
else {
instance = instanceOrInstancePromise;
this.instances.set(instanceIdentifier, instance);
}
}
return instance || (instanceDeferred === null || instanceDeferred === void 0 ? void 0 : instanceDeferred.promise) || null;
}
normalizeInstanceIdentifier() {
return DEFAULT_INSTANCE_NAME;
}
shouldAutoInitialize() {
return Boolean(this.component);
}
}
function isComponentEager(component) {
return component.instantiationMode === "EAGER" /* EAGER */;
}
/**
* ComponentContainer that provides Providers for service name T, e.g. `wsc`, `wsc-connector-internal`
*/
class ComponentContainer {
constructor(name) {
this.name = name;
this.providers = new Map();
}
/**
* @param component Component being added
* @description When a component with the same name has already been registered throw an exception
*/
addComponent(component) {
const provider = this.getProvider(component.name);
if (provider.isComponentSet()) {
throw new Error(`Component ${component.name} has already been registered with ${this.name}`);
}
provider.setComponent(component);
}
/**
* getProvider provides a type safe interface where it can only be called with a field name
* present in NameServiceMapping interface.
*
* Amo SDKs providing services should extend NameServiceMapping interface to register
* themselves.
*/
getProvider(name) {
if (this.providers.has(name)) {
return this.providers.get(name);
}
// create a Provider for a service that hasn't registered with Amo
const provider = new Provider(name, this);
this.providers.set(name, provider);
return provider;
}
}
/**
* A container for all of the Logger instances
*/
/**
* The JS SDK supports 5 log levels and also allows a user the ability to
* silence the logs altogether.
*
* The order is a follows:
* DEBUG < VERBOSE < INFO < WARN < ERROR
*
* All of the log types above the current log level will be captured (i.e. if
* you set the log level to `INFO`, errors will still be logged, but `DEBUG` and
* `VERBOSE` logs will not)
*/
var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
LogLevel[LogLevel["VERBOSE"] = 1] = "VERBOSE";
LogLevel[LogLevel["INFO"] = 2] = "INFO";
LogLevel[LogLevel["WARN"] = 3] = "WARN";
LogLevel[LogLevel["ERROR"] = 4] = "ERROR";
LogLevel[LogLevel["SILENT"] = 5] = "SILENT";
})(LogLevel || (LogLevel = {}));
const levelStringToEnum = {
debug: LogLevel.DEBUG,
verbose: LogLevel.VERBOSE,
info: LogLevel.INFO,
warn: LogLevel.WARN,
error: LogLevel.ERROR,
silent: LogLevel.SILENT,
};
/**
* The default log level
*/
const defaultLogLevel = LogLevel.INFO;
/**
* By default, `console.debug` is not displayed in the developer console (in
* chrome). To avoid forcing users to have to opt-in to these logs twice
* (i.e. once for amo, and once in the console), we are sending `DEBUG`
* logs to the `console.log` function.
*/
const ConsoleMethod = {
[LogLevel.DEBUG]: 'log',
[LogLevel.VERBOSE]: 'log',
[LogLevel.INFO]: 'info',
[LogLevel.WARN]: 'warn',
[LogLevel.ERROR]: 'error',
};
/**
* The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR
* messages on to their corresponding console counterparts (if the log method
* is supported by the current log level)
*/
const defaultLogHandler = (instance, logType, ...args) => {
if (logType < instance.logLevel) {
return;
}
const now = new Date().toISOString();
const method = ConsoleMethod[logType];
if (method) {
console[method](`[${now}] ${instance.name}:`, ...args);
}
else {
throw new Error(`Attempted to log a message with an invalid logType (value: ${logType})`);
}
};
class Logger {
/**
* Gives you an instance of a Logger to capture messages according to
* Amo's logging scheme.
*
* @param name The name that the logs will be associated with
*/
constructor(name) {
this.name = name;
/**
* The log level of the given Logger instance.
*/
this._logLevel = defaultLogLevel;
/**
* The main (internal) log handler for the Logger instance.
* Can be set to a new function in internal package code but not by user.
*/
this._logHandler = defaultLogHandler;
}
get logLevel() {
return this._logLevel;
}
set logLevel(val) {
if (!(val in LogLevel)) {
throw new TypeError(`Invalid value "${val}" assigned to \`logLevel\``);
}
this._logLevel = val;
}
// Workaround for setter/getter having to be the same type.
setLogLevel(val) {
this._logLevel = typeof val === 'string' ? levelStringToEnum[val] : val;
}
get logHandler() {
return this._logHandler;
}
set logHandler(val) {
if (typeof val !== 'function') {
throw new TypeError('Value assigned to `logHandler` must be a function');
}
this._logHandler = val;
}
/**
* The functions below are all based on the `console` interface
*/
debug(...args) {
this._logHandler(this, LogLevel.DEBUG, ...args);
}
log(...args) {
this._logHandler(this, LogLevel.VERBOSE, ...args);
}
info(...args) {
this._logHandler(this, LogLevel.INFO, ...args);
}
warn(...args) {
this._logHandler(this, LogLevel.WARN, ...args);
}
error(...args) {
this._logHandler(this, LogLevel.ERROR, ...args);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instanceFactoriesDeferred = new Map();
const _registerRemoteInstanceFactory = (instanceFactory) => {
const currentScript = document.currentScript;
if (!currentScript || !(currentScript instanceof HTMLScriptElement)) {
return;
}
const remoteInstanceUrl = currentScript.src;
const instanceFactoryDeferred = instanceFactoriesDeferred.get(remoteInstanceUrl);
if (!instanceFactoryDeferred) {
return;
}
instanceFactoryDeferred.resolve(instanceFactory);
};
class RemoteInstanceLoader {
constructor(url) {
this.url = url;
}
getInstanceFactory() {
return (container, options) => {
let instanceFactoryDeferred = instanceFactoriesDeferred.get(this.url);
if (!instanceFactoryDeferred) {
instanceFactoryDeferred = new Deferred();
instanceFactoriesDeferred.set(this.url, instanceFactoryDeferred);
this.initializeRemoteScript(this.url);
}
return instanceFactoryDeferred.promise.then((instanceFactory) => {
return instanceFactory(container, options);
});
};
}
initializeRemoteScript(url) {
const scriptElement = document.createElement('script');
scriptElement.type = 'text/javascript';
scriptElement.async = true;
scriptElement.src = url;
const anchorElement = document.getElementsByTagName('script')[0];
anchorElement.parentNode.insertBefore(scriptElement, anchorElement);
}
}
/**
* The default container name
*
* @internal
*/
const DEFAULT_CONTAINER_NAME = '[DEFAULT_CONTAINER_NAME]';
const container = new ComponentContainer(DEFAULT_CONTAINER_NAME);
const logger = new Logger('app');
window.AmoJsSdk = Object.assign(Object.assign({}, (window.AmoJsSdk || {})), { _registerInstanceFactory: _registerRemoteInstanceFactory });
/**
* Registered components.
*
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _components = new Map();
/**
* @param component - the component being added to the default container
* @internal
*/
function _addComponent(component) {
try {
container.addComponent(component);
}
catch (e) {
logger.debug(`Component ${component.name} failed to register with ComponentContainer ${container.name}`, e);
}
}
/**
*
* @param component - the component to register
* @returns whether or not the component is registered successfully
*
* @internal
*/
function _registerComponent(component) {
const componentName = component.name;
if (_components.has(componentName)) {
logger.debug(`There were multiple attempts to register component ${componentName}.`);
return false;
}
_components.set(componentName, component);
// add the component to existing container instances
_addComponent(component);
return true;
}
/**
* @param name - service name
*
* @returns the provider for the service with the matching name
*
* @internal
*/
function _getProvider(name) {
return container.getProvider(name);
}
/**
* Component for service name T, e.g. `wsc`
*/
class Component {
/**
*
* @param name The public service name, e.g. wsc, wsc-connector-internal
* @param instanceFactory Service factory responsible for creating the public interface
*/
constructor(name, instanceFactory) {
this.name = name;
this.instanceFactory = instanceFactory;
this.instantiationMode = "LAZY" /* LAZY */;
}
setInstantiationMode(mode) {
this.instantiationMode = mode;
return this;
}
}
class WscService {
constructor(wscConnectorInnerProvider) {
this.wscConnectorInnerProvider = wscConnectorInnerProvider;
this.currentWscParams = null;
this.iframeElementDeffered = null;
this.connectingPromise = null;
}
updateParams(wscParams) {
// Preload wscConnectorInner
void this.wscConnectorInnerProvider.get();
this.currentWscParams = wscParams;
this.connectingPromise = null;
this.iframeElementDeffered = null;
}
getCurrentWscParams() {
return this.currentWscParams;
}
async connectIframe(wscParams) {
if (!this.connectingPromise) {
const connectingPromise = new Promise(async (resolve) => {
const [iframeElement, wscConnectorInner] = await Promise.all([
this.getIframeElement(),
this.wscConnectorInnerProvider.get(),
]);
if (connectingPromise !== this.connectingPromise) {
return;
}
if (iframeElement instanceof Error) {
resolve(iframeElement);
return;
}
if (wscParams !== this.currentWscParams) {
resolve(new Error('The Wsc was reinitialized with other params.'));
return;
}
await wscConnectorInner.initializeIframe(iframeElement, wscParams);
if (connectingPromise !== this.connectingPromise) {
return;
}
resolve();
});
this.connectingPromise = connectingPromise;
}
return this.connectingPromise;
}
async getIframeElement() {
if (!this.currentWscParams) {
return new Error('The Wsc should be initialized before.');
}
if (!this.iframeElementDeffered) {
const deffered = new Deferred();
this.iframeElementDeffered = deffered;
void this.wscConnectorInnerProvider
.get()
.then((wscConnectorInner) => {
return wscConnectorInner.createIframeElement();
})
.then((iframeElement) => {
if (deffered === this.iframeElementDeffered) {
deffered.resolve(iframeElement);
}
});
}
return this.iframeElementDeffered.promise;
}
}
const WscFactory = (container) => {
return new WscService(container.getProvider('wsc-connector-inner'));
};
function registerWsc() {
_registerComponent(new Component('wsc-connector-inner', new RemoteInstanceLoader('https://js.amo.tm/v1.3/wsc/connector.js').getInstanceFactory()));
_registerComponent(new Component('wsc', WscFactory));
}
const initializeWsc$1 = (wscParams) => {
const wsc = _getProvider('wsc').getImmediate();
wsc.updateParams(wscParams);
};
const mountWsc$1 = async (options) => {
const wsc = _getProvider('wsc').getImmediate();
const wscParams = wsc.getCurrentWscParams();
if (!wscParams) {
if (options.onError) {
options.onError(new Error('The Wsc should be initialized before mount.'));
}
return;
}
let containerElement = null;
if (options.container instanceof HTMLElement) {
containerElement = options.container;
}
else {
containerElement = document.querySelector(options.container);
}
if (!containerElement) {
if (options.onError) {
options.onError(new Error('The container element is not found.'));
}
return;
}
const iframeElement = await wsc.getIframeElement();
const wscParamsAfterIframeElementGet = wsc.getCurrentWscParams();
if (wscParamsAfterIframeElementGet !== wscParams) {
return;
}
if (iframeElement instanceof Error) {
if (options.onError) {
options.onError(iframeElement);
}
return;
}
containerElement.innerHTML = '';
containerElement.append(iframeElement);
const connectIframeResult = await wsc.connectIframe(wscParams);
if (connectIframeResult instanceof Error) {
if (options.onError) {
options.onError(connectIframeResult);
}
return;
}
if (options.onSuccess) {
options.onSuccess();
}
};
const initializeWsc = (wscParams) => {
initializeWsc$1(wscParams);
};
const mountWsc = (options) => {
void mountWsc$1(options);
};
registerWsc();
export { initializeWsc, mountWsc };
//# sourceMappingURL=index.esm2017.js.map