UNPKG

@amo-tm/wsc

Version:

The amo WSC component of the amo JS SDK

576 lines (561 loc) 20.3 kB
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