// @ts-nocheck import Engine from 'lib/engine' import { sourceTypes } from 'ui/utils/seamly-utils' const isActionObject = (actionObj) => typeof actionObj === 'object' && 'action' in actionObj const fixActionObjectArgs = (actionObj) => ({ ...actionObj, args: Array.isArray(actionObj.args) ? actionObj.args : [actionObj.args], }) class ExternalApi { constructor(appConfig) { this._waitingActions = [] this._instances = {} this.timeouts = {} this.appConfig = appConfig this.context = {} } push(...actionObjects) { actionObjects = actionObjects .filter(isActionObject) .map(fixActionObjectArgs) if (actionObjects.length) { this._waitingActions.push(...actionObjects) this.handleActions() } } handleActions() { const actions = this._waitingActions this._waitingActions = [] actions.forEach((actionObj) => { switch (actionObj.action) { case 'init': this.handleInit(actionObj) break case 'destroy': this.handleDestroy(actionObj) break default: if ( !this.handleAction(actionObj) && // Store context properties for the next instance that will be created !this.setContext(actionObj.action, ...actionObj.args) ) { this._waitingActions.push(actionObj) } break } }) } instanceInitializing(namespace) { const instances = Object.keys(this._instances) return (!namespace && instances.length > 0) || instances.includes(namespace) } setContext(action, args) { const { instance: namespace } = args // Do not set this action in context if we are already initializing. // If we do, it will not be sent to the server anymore (because it gets marked as 'handled'). if (this.instanceInitializing(namespace)) { return false } switch (action) { case 'setTopic': { const { name } = args if (name) { this.context.topic = name } return true } // Deprecated. case 'setTranslation': { const { enabled, locale } = args if (!!enabled && locale) { this.context.userLocale = locale this.context.source = sourceTypes.windowApi } return true } case 'setContext': { const { userLocale, contentLocale } = args this.context.userLocale = userLocale this.context.contentLocale = contentLocale if (userLocale && userLocale != this.appConfig?.userLocale) { this.context.source = sourceTypes.windowApi } return true } case 'setVariables': if (Object.keys(args).length > 0) { this.context.variables = { ...args } } return true default: return false } } handleInit(actionObj) { const userConfig = this.getUserConfig(...actionObj.args) const config = this.getCombinedConfig(userConfig) // if this.appConfig is a function, it might return an invalid configuration (false, null, undefined) const { parentElement, namespace } = config || userConfig if (!namespace) { this.destroy() } else { Object.values(this._instances).forEach((instance) => { if ( instance.parentElement === parentElement || instance.namespace === namespace ) { this.destroy(instance) } }) } if (config) { const instance = this.createInstance(config) this._instances[config.namespace] = instance // Clear the context after creating the instance, so we do not reuse it for the next this.context = {} this.debouncedRender(instance, config.namespace) } } handleDestroy(actionObj) { this.destroy(actionObj.instance) } handleAction(actionObj) { const { action, instance: namespace, args } = actionObj const instances = Object.values(this._instances) if (!namespace && instances.length > 1) { console.warn( `Multiple instances detected. Due to potential race conditions, it is recommended to target a specific instance with the action: ${action}`, ) } // results will be an array containing the results of wether an instance has // handled the action or not const results = instances.map((instance) => !namespace || instance.namespace === namespace ? instance.execFunction(action, ...args) : false, ) // test if any of the instances has handled the action return results.some(Boolean) } createInstance(config) { return new Engine(config, this) } /** * @param {Engine} instance * @param {string} namespace */ debouncedRender(instance, namespace) { window.clearTimeout(this.timeouts[namespace]) this.timeouts[namespace] = window.setTimeout(() => { instance.render() }, 100) } destroy(instance) { if (!instance) { Object.entries(this._instances).forEach(([namespace, _instance]) => { _instance.destroy() delete this._instances[namespace] }) } else { if (typeof instance === 'string') { instance = this._instances[instance] } if (instance) { instance.destroy() delete this._instances[instance.namespace] } } } getUserConfig(userConfig = {}) { return userConfig } getCombinedConfig(userConfig) { const context = { ...(userConfig.context || this.appConfig.context), ...this.context, } // Key `variables` should only be set if there are any variables const variables = this.getMergedVariables(userConfig) if (Object.keys(variables).length > 0) { context.variables = variables } const defaults = { ...this.appConfig.defaults, ...userConfig.defaults, } const combinedConfig = { ...this.appConfig, ...userConfig, api: { ...this.appConfig.api, ...userConfig.api, }, context: Object.keys(context).length ? context : undefined, defaults: Object.keys(defaults).length ? defaults : undefined, } return typeof this.appConfig === 'function' ? this.appConfig(combinedConfig) : combinedConfig } getMergedVariables(userConfig) { return { ...(this.appConfig.context?.variables || {}), ...(userConfig.context?.variables || {}), ...(this.context.variables || {}), } } } export default ExternalApi