Версия:

Фрагменты кода для Роспатент и объем занимаемой памяти.

Введение

Для регистрации продукта требуется предоставить фрагменты кода программного продукта, а также размер приложения.

Размер приложения

Из формы не ясно, какой именно размер приложения должен быть указан: общий объем занимаемой памяти исходников или скомпилированных/собранных файлов.

ПО на языке JavaScript не предполагает компиляции, а собрано может быть по разному, с различной степенью оптимизации.

Поэтому предоставим объем занимаемой памяти всех модулей при скачивании из репозиториев в GitFlic в zip архивах на текущий момент.

Основной проект: 141 MB
Модуль регистрации: 1,53 MB
Модуль консультанта: 12 MB
Модуль Виджета: 6,57 MB

ИТОГО, СУММАРНЫЙ РАЗМЕР: 161,1 MB 

Фрагменты кода

Основной проект

Блоки кода ядра GoCore, на котором построено приложение
// MySQLModel

var MySQLModel = function (obj) {
    if (debug) console.log('creating new MySQLModel ....', obj.name, obj.client_object || 'no_CO')
    var _t = this
    if (typeof obj !== 'object') {
        throw new MyError('Не верно вызвана функция конструктор в MySQLModel.js')
    }
    _t.tableName = obj.name.toLowerCase()
    _t.user = obj.user || {sid: '0'}
    _t.name = obj.name
    _t.data_cache_alias = obj.name + '_' + (obj.client_object || '0') + '_' + _t.user.sid
    _t.client_object = obj.client_object
    _t.cache = {}
    _t.columns = []
    _t.uniqueColumns = []
    _t.required_fields = []
    _t.is_inherit_fields = {} // Все которые нужны в конечном запросе (columns)
    _t.is_inherit_fields_real_value = {} // Только те которые проверяются на значения. Отдельный объект, чтобы был короче цикл
    _t.not_insertable = ['created']
    _t.validation = {}
    _t.init_params = obj.params || {}
    _t.dynamic_field_input_arr = []
    _t.dynamic_field_tables_for_clear_cache = {}
    _t.current_dynamic_field_alias = ''
    _t.dynamic_field_pair = []
    _t.history_fields = []
    _t.always_save_log_fields = []

    this.validationFormats = {
        notNull: {
            format: '<значение>',
            example: 'строка, число, дата...'
        },
        number: {
            format: '<число>',
            example: '10'
        },
        url: {
            format: '<Протокол>://<адрес>',
            example: 'http://example.ru'
        },
        email: {
            format: '<Имя>@<домен>',
            example: 'user@example.ru'
        }
    }
    this.beforeFunction = {
        get: function (obj, cb) {
            cb(null, null)
        },
        add: function (obj, cb) {
            cb(null, null)
        },
        modify: function (obj, cb) {
            cb(null, null)
        },
        remove: function (obj, cb) {
            cb(null, null)
        }
    }
    this.prepareResult = function (rows, profile) {
        for (var i in rows) {
            // Берем тип данных для этого поля и преобразуем null -> '', 1/0 --> true/false
            if (typeof profile[i] != 'object') continue
            if (rows[i] === 'NULL') rows[i] = null
            if (rows[i] === null && profile[i].type !== 'tinyint') {
                rows[i] = ''
                continue
            }

            if (profile[i].type == 'tinyint' && profile[i].field_length == 1) rows[i] = !!rows[i]
        }
        return rows
    }
    this.getFormatingFunc = function (rows, params) {
        if (!Array.isArray(rows)) {
            var isSingleValue = true
            rows = [rows]
        }
        var formatData = function (field, field_type, profile) {
            var formatFuncName = profile.get_formating || (function () {
                switch (field_type) {
                    case "date":
                        return 'userFriendlyDate'
                        break
                    case "datetime":
                        return 'userFriendlyDateTime'
                        break
                    case "blob":
                    case "mediumblob":
                    case "longblob":
                        return 'parseBlob'
                        break
                    case "tinyint":
                        return (profile.field_length == 1) ? 'parseBool' : null
                        break
                    case "DECIMAL(10, 2)":
                    case "DECIMAL(15, 2)":
                    case "DECIMAL(50, 2)":
                        return 'formatMoney'
                        break
                    case "DECIMAL(3, 2)":
                        return 'formatPercent'
                        break
                    default :
                        return null
                        break
                }
            })()
            if (formatFuncName) {
                if (typeof _t[formatFuncName] == 'function') return _t[formatFuncName](field, {
                    params: params,
                    profile: profile
                })
                if (typeof funcs[formatFuncName] == 'function') return funcs[formatFuncName](field)
            }
            return field
        }
        for (var i in rows) {
            /*if (rows[i].name.indexOf('MySQLvariable')!==-1) {
             delete rows[i];
             continue;
             }*/
            var row = rows[i]
            for (var j in row) {
                if (row[j] === null) row[j] = ''
                var field = row[j]
                if (j.indexOf('MySQLvariable') > -1) {
                    delete rows[i][j]
                    continue
                }
                var profile = _t.class_fields_profile[j]
                if (!profile) continue
                var field_type = profile.type
                rows[i][j] = formatData(field, field_type, profile)
            }
        }
        return (isSingleValue) ? (rows.length) ? rows[0] : [] : rows
    }
    this.setFormatingFunc = function (row) {
        for (var i in row) {
            var field = row[i]
            if (field === null) continue
            var profile = _t.class_fields_profile[i]
            var field_type = profile.type.replace(/\W|[0-9]/ig, '').toLowerCase()
            var formatFuncName = profile.set_formating || (function () {

                switch (field_type) {
                    case "date":
                        return 'getDateMySQL'
                        break
                    case "datetime":
                        return 'getDateTimeMySQL'
                        break
                    case "int":
                    case "bigint":
                    case "decimal":
                        if (field === '') row[i] = null
                        return null
                        break
                    default :
                        return null
                        break
                }
            })()
            if (formatFuncName) {
                if (typeof _t[formatFuncName] == 'function') {
                    row[i] = [formatFuncName](field)
                } else if (typeof funcs[formatFuncName] == 'function') {
                    row[i] = funcs[formatFuncName](field)
                }
            }
        }
        return row
    }
    this.loadDefaultValues = function (obj, cb, additional_params) {

        if (typeof additional_params !== 'object') additional_params = {}
        var standart = additional_params.standart
        var is_virtual = additional_params.is_virtual || true
        if (standart) {
            // загрузим стандартные значения
            for (var i in _t.class_fields_profile) {
                var colProfile = _t.class_fields_profile[i]
                var columnName = colProfile.column_name
                if (typeof colProfile !== 'object') continue
                var colValue = obj[columnName]
                if (typeof colValue === 'undefined' && colProfile.default_value && !colProfile.is_virtual) {
                    obj[columnName] = colProfile.default_value
                }
            }
        }
        if (!is_virtual) return cb(null, obj)
        // Загрузим значения по умолчанию для is_virtual полей
        async.eachSeries(_t.class_fields_profile, function (item, cb) {
            var columnName = item.column_name
            if (typeof item !== 'object') {
                if (excludeCheckExistFields.includes(i)) return cb(null)
                return cb(new MyError('Не удалось получить профайл колонки...', i))
            }
            var colValue = obj[columnName]
            if (typeof colValue === 'undefined' && item.is_virtual && item.default_value) {
                // Загрузим необходимый default
                if (typeof obj[item.keyword] !== 'undefined') return cb(null)
                var o = {
                    command: 'getPrototype',
                    object: item.from_table,
                    params: {
                        columns: ['id'],
                        collapseData: false,
                        where: [
                            {
                                key: 'sysname',
                                val1: item.default_value
                            }
                        ]
                    }
                }
                _t.api(o, function (err, res) {

                    if (err || typeof res !== 'object') {
                        console.log('Не удалось получить значение по умолчанию для поля ' + columnName, err, res)
                        return cb(null)
                    }
                    if (!res.length) {
                        console.log('Нет записей в ' + item.from_table + 'с sysname = ' + item.default_value + ' для поля ' + columnName)
                        return cb(null)
                    }
                    obj[item.keyword] = res[0].id
                    // Сохраним значение и виртуального поля, для возможности проверки ролевой модели например
                    obj[columnName] = item.default_value
                    cb(null)
                })
            } else if (typeof colValue === 'undefined' && item.default_value) {
                obj[columnName] = item.default_value
                return cb(null)
            } else {
                cb(null)
            }
        }, function (err) {
            return cb(err, obj)
        })
    }
}

// КОНЕЦ ФРАГМЕНТА

MySQLModel.prototype.get = function (params, cb) {
    if (arguments.length == 1) {
        cb = arguments[0]
        params = {}
    }

    if (typeof cb !== 'function') throw new MyError('В метод не передан cb')
    if (typeof params !== 'object') return cb(new MyError('В метод не переданы params'))
    var _t = this
    var doNotLog = params.doNotLog || !debugSQL
    delete params.doNotLog

    let cacheAlias = _t.data_cache_alias // Уже содержит User sid
    let cacheAliasHash, realSQL


    var use_cache = (typeof params.use_cache !== 'undefined') ? params.use_cache : _t.use_cache

    if (use_cache) {

        cacheAlias += hashObj(params)
        cacheAlias += _t.user.user_data ? ('---' + _t.user.user_data.id) : ''

        cacheAliasHash = cacheAlias

        if (!global.classesCache[_t.name]) global.classesCache[_t.name] = {}

        if (global.classesCache[_t.name][cacheAliasHash]) {
            // if (!doNotLog) console.log('\n========USE_CACHE=======', global.classesCache[_t.name][cacheAliasHash].alias)
            if (!doNotLog) console.log('USE_CACHE:', _t.name)
            // if (!doNotLog) console.error('\n========USE_CACHE=======', global.classesCache[_t.name][cacheAliasHash].alias)
            let results = global.classesCache[_t.name][cacheAliasHash].results
            && Array.isArray(global.classesCache[_t.name][cacheAliasHash].results)
                ? [...global.classesCache[_t.name][cacheAliasHash].results]
                : global.classesCache[_t.name][cacheAliasHash].results
            if (results) results = funcs.cloneObj(results)

            let additionalDataRes = global.classesCache[_t.name][cacheAliasHash].additionalData
            if (additionalDataRes) additionalDataRes = funcs.cloneObj(additionalDataRes)


            return cb(null, results, additionalDataRes)
        }

    }

    var fromClient = params.fromClient
    delete params.fromClient

    var doNotUseDefaultWhere = typeof params.doNotUseDefaultWhere !== 'undefined' && !fromClient
        ? params.doNotUseDefaultWhere
        : null
    delete params.doNotUseDefaultWhere

    var list_of_access_res_ids = []
    var doNotCheckList = fromClient ? false : params.doNotCheckList
    var skipCheckRoleModel = fromClient ? false : params.skipCheckRoleModel
    var skipCheckBlocked = fromClient ? false : params.skipCheckBlocked

    var skipCheckRoleModelAdmin = (_t.user.authorized && _t.user.user_data.user_type_sysname === 'ADMIN')

    var role_model_where = []
    delete params.doNotCheckList
    delete params.skipCheckRoleModel
    delete params.skipCheckBlocked

    // Будет использоваться для подзапросов типа "Select ... from .... where some_table_id in (select id from some_table)
    const prepareSQL = params.prepareSQL
    const prepareSQLField = params.prepareSQLField || 'id' // Поле, которое запрашивается при подготовки подзапроса


    const countOnly = params.countOnly

    if (fromClient && params.specColumns) {
        return cb(new MyError('specColumns is not available for client request'))
    }
    if (fromClient && params.groupBy) {
        return cb(new MyError('groupBy is not available for client request'))
    }
    if (fromClient && params.prepareSQL) {
        return cb(new MyError('prepareSQL is not available for client request'))
    }

    var multi_value_separator = '-|-';

// КОНЕЦ ФРАГМЕНТА

    async.series({
        // ...

        formatWhere: (cb) => {
            let new_where = []
            for (var ii0 in where) {
                if (typeof where[ii0] === 'object') new_where.push(where[ii0])
            }
            where = new_where
            for (var ii in where) {
                if (where[ii].group === 'Access_by_list_SYSTEM') where[ii].group = 'Access_by_list_SYSTEM_replaced'
            }

            if (list_of_access_res_ids.length) {
                where.push(
                    {
                        key: 'id',
                        type: 'in',
                        group: 'Access_by_list_SYSTEM',
                        val1: list_of_access_res_ids
                    }
                )
            }

            where = where.sort(function (a, b) {
                if (!a.group) a.group = a.key + '_serverAutoGroup'
                if (!b.group) b.group = b.key + '_serverAutoGroup'
                a.groupTrim = a.group.replace(/\.\w+/ig, '')
                b.groupTrim = b.group.replace(/\.\w+/ig, '')
                if (a.groupTrim > b.groupTrim) return 1
                if (a.groupTrim < b.groupTrim) return -1
                if (a.role_group > b.role_group) return 1
                if (a.role_group < b.role_group) return -1
                return 0
            })


            var p = {
                tableName: tableName,
                class_fields_profile: _t.class_fields_profile,
                columns: columns,
                where: where,
                ready_columns_by_colName: ready_columns_by_colName
            }
            getWhereStrCb(p, (err, res) => {
                if (err) {
                    console.error('getWhereStr', err)
                    return cb(err)
                }
                whereStr = res.str
                return cb(null)
            })

        },
        
        // ...
        query: (cb) => {
            pool.getConn((err, conn) => {
                if (err) return cb(new MyError('Не удалось получить подключение к БД', {err: err}))
                const t1 = Date.now()
                conn.query(realSQL, [], function (err, res) {
                    conn.release()

                    const cnt = !isNaN(+debugSQL) ? +debugSQL : false
                    const diff = Date.now() - t1
                    if (!cnt || String(diff).length >= cnt) {
                        // если debugSQL указан как число, то будут выводиться только те,
                        // у кого длительность больше или равна Х символов
                        if (diff > debugSQLConsoleErrTime) {
                            console.error('LONG realSQL. SQL_TIME:', diff, realSQL)
                        } else {
                            if (!doNotLog) console.log('realSQL. SQL_TIME:', diff, realSQL)
                        }

                    }

                    if (err) {
                        err.msg = err.message
                        if (err.code === 'ER_BAD_FIELD_ERROR') {
                            var err2 = err
                            err = new MyError('ER_BAD_FIELD_ERROR. Возможно, Вы добавили поле, которое использует для своего построения другие поля, ' +
                                'но его sort_no меньше чем у них. Проверьте что все используемые им поля имеют sort_no меньше. ' +
                                'Внимание! SORT_NO не синхронизируется! ' +
                                'Если изначально проставлено не верно, то надо залесть в class_profile и установить sort_no (функция в контекстном меню).'
                                , {
                                    name: _t.name,
                                    err: err2,
                                    realSQL
                                })
                        }
                        console.error('GET ERROR', err, realSQL)
                        return cb(err)
                    }
                    rows = res
                    cb(null)
                })
            })
        },
    }, cb);
    
}
// Внутренний api

var api = function (obj, cb, user) {
    // if (obj.object == 'tangibles') {
    //     console.log(obj)
    //     debugger
    // }
    if (typeof cb !== 'function') throw new MyError('The API did not pass the callback function')
    if (typeof obj !== 'object') return cb(new MyError('The API is not passed to obj'))
    obj = funcs.cloneObj(obj)
    const objOrig = funcs.cloneObj(obj)
    obj.params = obj.params || {}
    var _t = this
    if (!user) throw new MyError('API not passed to user')
    let sid = user.sid

    var command, object
    var object_params = obj.object_params || {}
    var params = obj.params || {}
    var paramsConsoleJSON
    var client_object = obj.client_object || params.client_object
    var fromClient = obj.params.fromClient
    //if (client_object) params.client_object = client_object;
    var t1 = moment()
    if (obj.command == '_CLEAR') {
        global.classes = {}
        return cb(null, new UserOk('Cache dropped.'))
    }

    var apiRID = funcs.guidShort() // API Request ID
    var fromClientConsole = (fromClient) ? '==> ' : ''
    var now_time = moment()
    var is_access_important // Определяет был ли дан доступ или требуются доп проверки
    var is_access_by_list // Определяет что доступ выдан но требеется проверить по списку

    var noToastr = params.noToastr
    delete params.noToastr
    var skipCheckRoleModel = (!fromClient) ? params.skipCheckRoleModel : false


    // var checkAccess = (fromClient)? false : params.checkAccess;
    var checkAccess = params.checkAccess

    var skipCheckRoleModelAdmin = (user.authorized && user.user_data.user_type_sysname === 'ADMIN')
    // if (user.authorized && user.user_data.user_type_sysname === 'ADMIN'){
    //     skipCheckRoleModel = true;
    // }


    delete params.checkAccess
    delete params.noToastr

    var accesses = []
    var denies = []

    var _class

    async.series({
        checkServicesProblem:async cb => {
            // ...
        },
        checkArgs: cb => {
            // Проверим аргументы
            // ...
        },
        logInCosole: cb => {
            // log in console all requests

            var excludedParams = ['password']
            var paramsConsole = funcs.cloneObj(params)
            // paramsConsole
            for (var i in excludedParams) {
                if (typeof paramsConsole[excludedParams[i]]!== 'undefined') paramsConsole[excludedParams[i]] = '***'
            }
            if (paramsConsole)
                paramsConsoleJSON = JSON.stringify(paramsConsole).substr(0, 5000)
            if (debug) console.info(moment().format('DD.MM.YYYY HH:mm:ss'),fromClientConsole + apiRID, ' → ', checkAccess ? 'checkAccess': '', now_time.format('DD.MM.YYYY HH:mm:ss'), 'API LOG (' + sid + '):', command, object, paramsConsoleJSON)
            if (debug) global.times.log_time += moment().diff(now_time)
            cb(null)
        },
        checkAccess: cb => {
            //checkAccess
            // ...
        },
        prepareAlias: cb => {
            // Подготовим инстанс класса (из кэша или новый)
            // ...
        },
        // ...
        checkAccessByList: cb => {
            // Проверка доступа по спискам для команд не 'get'. Для 'get' отдельный механизм
            // ...
        },
        // ...
        checkRoleModel: cb => {
            if (!useRoleModel) return cb(null)

            // ...
        },
        do: cb => {
            if (checkAccess) return cb(null, new UserOk('noToastr'))
            // Выполнить действие или вернуть класс
            if (command == '_getClass') return cb(null, _class)
            if (command == '_clearCache') {
                for (var key in global.classes) {
                    if (key.indexOf(object + '_-_') === 0) {
                        global.classesCache[object] = {}
                        if (client_object) delete global.classes[key][client_object]
                        else global.classes[key] = {}
                    }
                }
                return cb(null, new UserOk('The class / client cache has been successfully cleared.'))
            }
            if (command == '_clearCacheAll') {
                for (var key2 in global.classes) {
                    if (key2.indexOf(object) === 0) {
                        global.classesCache[object] = {}
                        global.classes[key2] = {}
                        delete global.classes[key2]
                    }
                }
                return cb(null, new UserOk('Class cache successfully cleared for all client objects.')) //Кеш класса успешно очищен для всех клиентских объектов
            }
            if (command == '_getTimes') {
                return cb(null, new UserOk('See console', {times: global.times}))
            }
            if (typeof _class !== 'object') return cb(new MyError('The class is not an object.'))
            if (typeof _class[command] !== 'function') return cb(new MyError('The class does not have this method.', {
                method: command,
                object: object
            }))
            _class.is_busy = true

            if (fromClient) delete params.doNotCheckList
            if (!is_access_by_list) {
                params.doNotCheckList = true
            } // Доступ выдан на высоком уровне, проверять список не нужно.

            _class[command](params, function (err, res, additionalData) {
                if (err) {
                    console.log(moment().format('DD.MM.YYYY HH:mm:ss'), err)
                }
                delete _class.is_busy
                cb(err, res)
                // cb(err, res, additionalData);
            })
        }
    }, (err, resAll, additionalData) => {
        if (err instanceof UserOk && err.message === 'goNext') {
            resAll.do = err
            err = null
        }

        // Проверить на ошибки
        let res = resAll.do
        var end_time = moment()
        var request_time = end_time.diff(t1)
        if (debug) console.info(fromClientConsole + apiRID, ' ← Time:', checkAccess ? 'checkAccess' : '', request_time, end_time.format('DD.MM.YYYY HH:mm:ss'), 'API LOG END(' + sid + '):', command, object, paramsConsoleJSON)

        if (err) {

            if (config.get('debugMyError')){
                if (err instanceof MyError){
                    console.error('ERR:MyError', err)
                }
            }
            if (config.get('debugUserError')){
                if (err instanceof UserError){
                    console.error('ERR:UserError', err)
                }
            }

            if (err instanceof UserOk) {
                // ...
                return
                // return cb(null, getCode('ok', err), request_time);
            }
            if (err instanceof UserError && !!fromClient) {
                getCode({name: err.message, params: err.data, user: user}, function (code_err, code_res) {
                    if (code_err) return cb(code_err, request_time)
                    return cb(null, code_res, request_time)
                })
                return
            } else {
                // ...
                return
            }
        }
        // выполнить форматирование результата
        if (res instanceof UserOk) {
            // ...
        }
        if (additionalData) return cb(null, res, additionalData, request_time)
        cb(null, res, request_time)
    })
}
Блоки кода специфичные для проекта Voda
// Widget.js (бэк)

/**
 * Функция будет отслеживать состояние подключения WebRTC соединения и отсылать соответствующие команды
 * Здесь же будут выполняться подписки/отписки сокетов для обмена
 * @param info
 */
const processWebRTC = (info) => {

    let _t = this

    // отправим консультанту инфу необходимую для начала инициализации webRTC
    const consultant = info.consultant
    const widget = info.widget

    if (!widget.connected || !consultant.connected) {
        if (!widget.processWebRTCCounter) widget.processWebRTCCounter = 0
        if (widget.processWebRTCCounter >= 50) return
        widget.processWebRTCCounter++
        setTimeout(() => {
            processWebRTC(info)
        }, 200)
        return
    } else {
        widget.processWebRTCCounter = 0
    }

    if (info.communication_session?.is_finished) {
        return
    }

    if (consultant.local.webRTC?.data?.connected || consultant.local.webRTC?.data?.failed) {

        // ...
    }
    if (widget.local.webRTC?.data?.connected || widget.local.webRTC?.data?.failed) {
        if (typeof widget.unsubscribeWebRTC === "function") {
            // ...

        }
        if (widget.local.webRTC?.data?.failed) {
            console.error('ERR:processWebRTC: widget.failed', {webRTC:widget.local.webRTC?.data})
            widget.endWebRTC()
        }
    }

    const route = getWebRTCAction(consultant, widget)

    // ...

    if (route.isStart){
        const id = info?.widget?.remote?.communicationSessions?.data?.id
        // ...

    }

    let targetInstance
    if (route.target === CONSULTANT) targetInstance = consultant
    else if (route.target === WIDGET) targetInstance = widget

    const session = info.communication_session

    const targetWebRTC = targetInstance.local.webRTC?.data || {}
    // const confCounterKey = `${route.toState}_counter`

    if (!session.connectWebRTCProcess) session.connectWebRTCProcess = []
    if (!route.isStart) session.connectWebRTCProcess = []
    session.connectWebRTCProcess.push(`${route.target}.${route.toState}`)

    let data_
    if (route.target === CONSULTANT) {
        // Определим данные для передачи в зависимости от того в какое состояние переводим
        switch (route.toState) {
            case 'start':
                if (!targetWebRTC[route.toState]) {

                }
                data_ = {
                    start: true,
                    createOffer: true,
                    session: {is_share:session.is_share, is_video: session.is_video, is_audio: session.is_audio},
                    iceServers
                }
                break
            
            // ...
                
            case 'reanswer_confirmed':
                widget.setData('webRTC', {reoffer:null, reanswer:null, reoffer_confirmed:null, reanswer_confirmed:null}, null, true, true)
                consultant.setData('webRTC', {reoffer:null, reanswer:null, reoffer_confirmed:null, reanswer_confirmed:null}, null, true, true)
            default:
                break
        }
    } else if (route.target === WIDGET) {
        // Определим данные для передачи в зависимости от того в какое состояние переводим
        switch (route.toState) {

            // ...
                
            case 'offer':
                if (!consultant.local.webRTC.data.offer) break // Оффера еще нет. Придем сюда же при получении оффера
                data_ = {offer: consultant.local.webRTC.data.offer}
                break
            case 'consultantReadyToIce':
                data_ = {consultantReadyToIce: true}
                break
            // ...
            default:
                break
        }
    }
    // Если есть что установить - установим
    if (data_) {
        targetInstance.setData('webRTC', data_, null, true, true)
    }
}


Model.prototype.call = function (obj, cb) {
    if (arguments.length === 1) {
        cb = arguments[0]
        obj = {}
    }
    var _t = this
    var rollback_key = obj.rollback_key || rollback.create()
    var doNotSaveRollback = obj.doNotSaveRollback || !!obj.rollback_key

    // Создадим crm
    // Создадим communication_session,
    // Синхронизируем консультантов

    let crm_user_id = this.user.sessionData ? this.user.sessionData.crm_user_id : null
    const uuid = this.user.sessionData?.uuid
    const aMacAddr = this.user?.mac
    const aDOM = this.user?.DOM ?? ""

    const onlyAudio = obj.onlyAudio
    const oSConsultant_id = obj.consultant_id

    let widget, communication_session, communication_session_id

    const users = getAuthorizedUsers()
    const admins = users.filter(one => one.user_data.user_type_sysname === 'ADMIN')
    let tokens = null
    let FCMtokens = null
    let user_id

    let consultants_for_sites, consultants, stack

    let device, deviceId, macAddr, is_exist_user

    async.series({
        createCrm: cb => {
            async.series({
                checkExist: cb => {
                    if (!crm_user_id) return cb(null)
                    // ...
                },
                add: cb => {
                    if (crm_user_id) return cb(null)
                    // ...
                },
            }, cb)
        },
        getOrCreateCommunicationSession: cb => {
            // ...
        },
        syncCommunicationSession: cb => {
            const o = {
                command: 'syncCommunicationSessionLocal',
                object: 'Widget',
                params: {
                    widgetKey: widget.widgetKey,
                    communication_session,
                    syncCSConsultantStack: true
                }
            }
            _t.apiSys(o, (err, res) => {
                if (err) console.error(new MyError('Не удалось syncCommunicationSessionLocal', {o, err}))
                consultants = res.consultants
                stack = res.stack
                cb(null) // Если не выполняем в фоне
            })
        },
        syncConsultants: cb => {
            // ...
        },
        // ...
        sendCallNotification: async cb => {
            const storage = getConsultantStorage()
            // ...
        },
        sendMailNotification: cb => {
            // ...
        },
    }, function (err, res) {
        // ...
    })
}

// Org_site_consultant.js (бэк)

Model.prototype.initConsultant = function (obj, cb) {
    if (arguments.length === 1) {
        cb = arguments[0]
        obj = {}
    }
    var _t = this
    var rollback_key = obj.rollback_key || rollback.create()
    var doNotSaveRollback = obj.doNotSaveRollback || !!obj.rollback_key
    let consultant = this.getConsultant()

    let consultantInfo

    const disconnectHandler = e => {
        // запустим таймер и если не произойдет переподключения того же сокета
        //, остановим сеанс и webRTC
        consultant.disconnected = Date.now()
        consultant.connected = null

        const i = setInterval(() => {
            if (consultant.socket.connected || !consultant.disconnected) {
                // Успешно переподключились. Остановим
                clearInterval(i)
                return
            }
            if (Date.now() - consultant.disconnected >= 120000) {

                // if (!consultant.connected) consultant.connected = Date.now()
                clearInterval(i)

                async.series({
                    endSession: cb => {
                        // Проверим что надо завершать
                        if (!consultant.communication_id) return cb(null)

                        const [err, info] =
                            this.getInfoByCommunicationId(consultant.communication_id)
                        if (err) {
                            console.error('Не удалось получить communication_session по консультанту (1054)', err)
                            return cb(null)
                        }

                        const communication_session = info.communication_session
                        if (finished_communication_session_statuses.includes(communication_session.status_sysname)) {
                            return cb(null)
                        }
                        communication_session.to_status = 'CONSULTANT_LEFT'
                        const o = {
                            command: 'syncConsultantStack',
                            object: 'Communication_session',
                            params: {
                                communication_session
                            }
                        }
                        _t.apiSys(o, (err, res) => {
                            consultant.endWebRTC()
                            if (err) console.error(new MyError('Не удалось syncConsultantStack', {o, err}))
                            cb(null)
                        })
                    },
                }, err => {
                    // Ничего не надо
                })

            }
        }, 100)

        // ...
    }

    const pushConfirmHandler = data => {
        const hash = data.hash
        if (!hash) {
            console.error('Consultant. pushConfirm. hash not passed')
            return
        }
        const key = data.key
        if (!key) {
            console.error('Consultant. pushConfirm. key not passed')
            return
        }
        if (consultant.remote[key]) {
            if (consultant.remote[key].hash === hash) {
                if (!consultant.remote[key].confirm) consultant.remote[key].confirm = true

                consultantOnConfirm.call(this, consultant, key)
            }
        }
    }

    const handleWebRTCConsultant = data => {
        consultant.setData('webRTC', {...data}, null, true, true)
    }


    if (consultant && !obj.useHere) {
        if (consultant.socket.id !== this.user.socket.id && consultant.socket.connected) {
            // Если это новый сокет, а старый при этом подключен (то есть вкладка не закрыта).
            this.user.socket.removeAllListeners('disconnect')
            // this.user.socket.removeListener('disconnect', disconnectHandler)
            return cb(new UserError('Модуль используется в другой вкладке..',
                {
                    usedElsewhere: true,
                    anotherSocketId: consultant.socket?.id
                }))
        }
    } else if (consultant && obj.useHere) {
        // Старому сокету нужно все завершить и перевести его в режим usedElsewhere
        // Возможно в дальнейшем с запросом к пользователю. Перейти на другую вкладку? Да! Нет!
        // ...
    }

    async.series({
        createConsultant: cb => {
            if (consultant) return cb(null)
            _t.createConsultant({}, (err, res) => {
                if (err) return cb(err)
                consultant = res.consultant
                cb(null)
            })
        },
        addListenersAndResetRemoteAndSocket: cb => {
            // ...
        },

        loadPrevFiles: async cb => {
            // ...
        },
        syncLocal: cb => {

            const user_ids = [consultant.user_id]
            const params = {
                user_ids,
            }

            skipConsultantRemote(params)

            async.series({
                sites: cb => {
                    // cb(null)

                    _t.syncConsultantSitesLocal(params, (err, res) => {
                        if (err) console.error(new MyError('Не удалось syncConsultantSitesLocal', {params, err}))
                        cb(null)
                    })
                },
                stack: cb => {
                    // ...
                },
            }, err => {
                if (err) console.error(new MyError('Не удалось syncConsultantLocal', {o, err}))
                cb(null)
            })

        },
        getOrganizationOptions: cb => {
            // ...
        },

        getSitesSettings: cb => {
            // ...
        },
        // ...
        syncRemote_:cb => {
            cb(null)
        },

    }, function (err, res) {
        // ...
    })
}

Модуль консультанта

// CallScreen.js

const CallScreen = () => {

    const selfMedia = 'consultMedia'
    const serverClassName = 'Org_site_consultant'

    const isCamExist = true // Мы отдельно заранее не проверяем наличие, как это сделано в виджете.

    const {state, isIOS} = useApp()

    const {
        webRTC,
        emitWebRTC,
        answerSession,
        chats,
        chatId,
        setChatId,
        setCrmUserId,
        options
    } = useApp()

    const [activeCamera, setActiveCamera] = useState(false)
    // ...

    const communicationSessionData = useRef()
    useEffect(() => {

        // if (!state.communications?.data) return
        // const activeSessionMedia = state.communications?.data[state.communications?.data.length - 1]?.[selfMedia]?.is_audio
        const activeSessionMedia = state.communications?.data
            ? state.communications?.data
                .filter(session => ['ACCEPTED', 'IN_PROGRESS'].includes(session.status_sysname))
            : null
        communicationSessionData.current = activeSessionMedia?.length ? activeSessionMedia[0] : null
    }, [state.communications?.data])

    const getConstrainsSettingsImportantRef = useRef(null)

    const isDisplayStreamedRef = useRef(false)
    const [isDisplayStreamed, setIsDisplayStreamed] = useState(false)
    const isVideoStreamedRef = useRef(communicationSessionData.current?.[selfMedia]?.is_video)
    const [isVideoStreamed, setIsVideoStreamed] = useState(communicationSessionData.current?.[selfMedia]?.is_video)
    // ...

    //=================Set WebTRC Connecntion=================

    const [crmUserIdFromComSession, setCrmUserIdFromComSession] = useState([])

    const currentSessionIdRef = useRef()
    const oldSessStatusRef = useRef()
    // ...

    const webRTCLocalRef = useRef({})
    const clearWebRTCLocalTimerRef = useRef(null)

    const [retryState, setRetryState] = useState({})

    const webRTCData = webRTC?.data // webRTC?.data  // !webRTC ? false : webRTC.data
    const webRTCDataRef = useRef(webRTCData)
    useEffect(() => {
        webRTCDataRef.current = webRTCData
    }, [webRTCData])

    const endWebRTC = () => {

        if (peerConnection.current) {

            peerConnection.current.getSenders()?.forEach(sender => sender?.track?.stop())

            stopShareScreen()

            peerConnection.current.close()
            peerConnection.current = null
        }

        if (localMediaStream.current) {
            console.log('endWebRTC: CLEAR localMediaStream')
            localMediaStream.current.getTracks().forEach(track => track.stop())
            localMediaStream.current = null
        }

        if (localMediaElem.current) {
            console.log('endWebRTC: CLEAR localMediaElem srcObject')
            localMediaElem.current.srcObject = null
        }

        if (shareScreenStreamRef.current) {
            shareScreenStreamRef.current.getTracks().forEach(track => track.stop())
            shareScreenStreamRef.current = null
        }

        if (localMediaElemHandlersRef.current) {
            localMediaElemHandlersRef.current.forEach(item => {
                if (localMediaElem.current) localMediaElem.current?.removeEventListener(item.type, item.handler)
            })
            localMediaElemHandlersRef.current = []
        }
        if (remoteMediaElemHandlersRef.current) {
            remoteMediaElemHandlersRef.current.forEach(item => {
                if (remoteMediaElem.current) remoteMediaElem.current?.removeEventListener(item.type, item.handler)
            })
            remoteMediaElemHandlersRef.current = []
        }

        // setShareScreen(false)

        if (remoteMediaElem.current) {
            remoteMediaElem.current.srcObject = null
        }


        console.log('endWebRTC: webRTCLocalRef (before clear):', webRTCLocalRef.current)
        if (clearWebRTCLocalTimerRef.current) clearTimeout(clearWebRTCLocalTimerRef.current)
        webRTCLocalRef.current = {}

        if (rebootWebRTCTimerRef.current) clearTimeout(rebootWebRTCTimerRef.current)
        if (waitConnectionRestoreTimeoutRef.current) clearTimeout(waitConnectionRestoreTimeoutRef.current)
        if (waitConnectionRestoreBeforeFinishTimeoutRef.current) {
            clearTimeout(waitConnectionRestoreBeforeFinishTimeoutRef.current)
        }

        if (canvasAnimationIdRef.current) {
            cancelAnimationFrame(canvasAnimationIdRef.current)
            canvasAnimationIdRef.current = null
        }

        if (!communicationSessionData.current) stopRecording()
    }

    const rebootWebRTCHandler = async () => {
        const o = {
            command: 'rebootWebRTC',
            object: 'Org_site_consultant'
        }
        const res = await socketQuery(o)
    }

    // Завершение webRTC по unmount и Скрытие карточки клиента
    useEffect(() => {
        // Реакция на unmount компонента.
        return () => {
            // Завершим webRTC
            endWebRTC()
            // Отдельно завершим все потоки, так как не всегда при endWebRTC надо завершать потоки
            if (shareScreenStreamRef.current) {
                shareScreenStreamRef.current.getTracks().forEach(track => track.stop())
                shareScreenStreamRef.current = null
            }

            if (localMediaStream.current) {
                localMediaStream.current.getTracks().forEach(track => track.stop())
                localMediaStream.current = null
            }

            setCrmUserId(null)
        }
    }, [])


    const defineDevices = async (options) => {
        // Выполним заполнение справочников устройств
        // ...

        // logD('DEBUG:Devices==>', devices_)
        audioinput_ = devices_.filter(d => d.kind === 'audioinput')
        audiooutput_ = devices_.filter(d => d.kind === 'audiooutput')
        videoinput_ = devices_.filter(d => d.kind === 'videoinput')
        // Почему это так, см описание выше
        audioComplexDevices_ = isMobile ? audioinput_ : audiooutput_
        setDevices(devices_)
        setAudioinput(audioinput_)
        setAudiooutput(audiooutput_)
        setVideoinput(videoinput_)
        setAudioComplexDevices(audioComplexDevices_)
    }

    const getConstrains = (constrains = {}) => {
        // const isVideo = constrains.is_video ?? webRTCData.session?.is_video
        // ...
    }

    /**
     * Попробует получить доступ к медиа ресурсам. Попробует несколько раз
     * Если выйдет, то вернет поток, если нет, то залогирует и вернет null
     * @param constrains
     * @param params
     * @return {Promise<ok|err>}
     */
    async function getLocalMedia(constrains, params = {}) {
        let stream
        if (!constrains) {
            return err('Не переданы defauldConstrains')
        }

        try {

            stream = await navigator.mediaDevices.getUserMedia(constrains)
        } catch (e) {
            // Повторим попытку, спустя X.
            // Если не выйдет попробуем взять только часть аудио (если было и то и то)
            // ...
        }

        await defineDevices(options)

        return ok('ok', {stream})

    }

    const waitConnectionRestoreTimeoutRef = useRef()
    const waitConnectionRestoreBeforeFinishTimeoutRef = useRef()

    function handleConnectionLoss() {
        setLoaderText('Собеседник ушел. Подождем возвращения...')
        waitConnectionRestoreTimeoutRef.current = setTimeout(() => {
            setLoaderText('Собеседник не вернулся. Завершаем сеанс...')
            waitConnectionRestoreBeforeFinishTimeoutRef.current = setTimeout(()=>{
                if (state.currentVideoSession) finishCurrentCall(state.currentVideoSession)
            }, 5000)
        }, 12000)

    }

    function handleConnectionRestored() {
        setLoaderText(null)
        if (canvasAnimationIdRef.current) {
            cancelAnimationFrame(canvasAnimationIdRef.current)
            canvasAnimationIdRef.current = null
        }
        if (waitConnectionRestoreTimeoutRef.current) clearTimeout(waitConnectionRestoreTimeoutRef.current)
        if (waitConnectionRestoreBeforeFinishTimeoutRef.current) {
            clearTimeout(waitConnectionRestoreBeforeFinishTimeoutRef.current)
        }
        void rebuildMixer();

    }

    /**
     * Создаст новый peer и установит подписки на события получения системных данных и треков от WebRTC
     * Если передан офер, то создаст answer
     * @return {Promise<{msg: string, code: number, data: {}, type: string}>}
     */
    async function handleNewPeer({createOffer}) {
        if (peerConnection.current) {
            return console.warn(`Already connected`)
        }

        peerConnection.current = new RTCPeerConnection({
            iceServers: webRTCData.iceServers
        })
        // ...

    }

    const firstTimeCamOrScreenOnForIOSRef = useRef(true)


    /**
     * Отправит новый дескрипшн.
     * @return {Promise<{msg: string, code: number, data: {}, type: string}>}
     */
    async function handleUpdatePeer() {

        if (!peerConnection.current) {
            console.warn('Connection is not set')
            return
        }

        await peerConnection.current.setLocalDescription() // сбрасываем предыдущее описание
        const offer = await peerConnection.current.createOffer()
        await peerConnection.current.setLocalDescription(offer)

        // ...
    }



    //=======================ON WebRTCData REACTIONS=================================

    // реакция на start
    useEffect(() => {
        // Завершение webRTC Если пришел пустой
        if (!webRTCData || !Object.keys(webRTCData).length) {
            endWebRTC()
            return
        }

        if (!webRTCData) return
        if (webRTCData.connected) return

        if ((webRTCData.start && webRTCLocalRef.current.started
                && webRTCLocalRef.current.started !== currentSessionIdRef.current)
            || (webRTCData.start && !currentSessionIdRef.current)) {
            // ...

        }

        // if (webRTCData.start && !webRTCLocal.started) {
        if (webRTCData.start && !webRTCLocalRef.current.started
            && !webRTCLocalRef.current.reoffer && !webRTCLocalRef.current.reanswer) {
            console.log('СТАРТУЕМ', webRTCData, webRTCLocalRef.current, currentSessionIdRef.current)
            // ...

            startCapture()
                .then(() => {
                    handleNewPeer({createOffer: webRTCData.createOffer})
                        .then(() => {
                            emitWebRTC({start_confirmed: Date.now()}) // Отправим на сервер подтверждение

                        })
                        .catch(e => {
                            logD('ERROR handleNewPeer', e)
                            endWebRTC()

                        })
                })
                .catch(e => {
                    logD('Error getting user media', e)
                    setHint(true)
                })
            // } else if (webRTCLocal.started) {
        }
        // }, [webRTC, reconnectWebRTCCounter])
    }, [webRTCData, retryState])


    // реакция на answer
    useEffect(() => {

        if (!webRTCData) return
        if (webRTCData.connected) return
// ...

    }, [webRTC])
// ...

    // реакция на нового iceCandidate
    useEffect(() => {

        // ...
    }, [])


    //===================ANOTHER REACTIONS=======================================


    // ...


    /**
     * Позволяет включить камеру или демонстрацию экрана заменив или добавив поток
     * @param newTrack
     * @return boolean true if added
     */
    const addOrReplaceTrack = (newTrack) => {
        if (!peerConnection.current) return false

        const sender = peerConnection.current.getSenders().find(sender =>
            sender.track?.kind === newTrack.kind
        )

        if (!sender) {
            console.log('DEBUG:addOrReplaceTrack: peerConnection.current.addTrack. newTrack=', newTrack)
            peerConnection.current.addTrack(newTrack, localMediaStream.current)
            setLocalMediaFromPeer()

            return true
        }

        // В будущем можно заменять только однотипные (экран/камера) треки, разделяя их например по label 'window...'
        // Тогда можно будет передавать сразу оба трека и камера и экран

        sender.replaceTrack(newTrack).then()
        setLocalMediaFromPeer()
        return false

    }

    


    //=======================SHARE============================

    const shareScreenStreamRef = useRef(null)

    // const endedDisplayMediaListener = async () => {
    //     // здесь обрабатываем случай отключения
    //     console.log('Screen sharing stopped')
    //     setIsDisplayStreamed(false)
    // }


    const isCamBeforeShare = useRef(false) // Была ли вкл камера, до демонстрации
// ...


    //====================ANOTHER===========================


    const finishCurrentCall = async id => {
        if (!id) return

        setFinishLoader(true)

        const o = {
            command: 'finishCommunication',
            object: serverClassName,
            params: {
                communication_id: id,
                noToastr: true
            }
        }

        const res = await socketQuery(o)

        answerSession(null)

    }

// ...

    const [consultantMedia, setConsultantMedia] = useState({})

// ...
   
    useEffect(() => {
        if (state.currentVideoSession === null) setFinishLoader(false)
    }, [state.currentVideoSession])

    // ...




    // ================CHAT=========================
    

    // ================RENDER======================

    // Вывод экрана активной видео сессии
    const displayCurrentVideoSession = (state.currentVideoSession === null)
        ? false
        : <div className="callscreen__wrapper">
            
        </div>
    return (
        <>
            {displayCurrentVideoSession}

            {(!state.warnMessage) ? false : <Warning/>}
        </>
    )
}

Модуль виджета

// ConsultantPreview.js


const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream

const ConsultantPreview = ({consult, startCallHandler, isCurrent, getActiveConsultantId, isWidgetResized, showWidgetHandler, isSmallWidth, callBtnColor, callBtnText}) => {

    const serverParams = getServerParams()

    if (!isCurrent) {
        return (
            <div className="slide" style={{minWidth: isWidgetResized ? 'auto' : '326px'}}>
                <p className="slide-nickname"
                   style={{opacity: '0', display: isWidgetResized ? "none" : "block"}}>{consult.nickname}</p>
                <img src={`${serverParams.url}/upload/${consult.user_image}`} alt={'preview'}/>
            </div>
        )
    }

    const previewImg = useRef(null)
    // ...
    const setNowPlay = (toPlay) => {
        nowPlay.current = toPlay
        setShowEl(toPlay ? toPlay.videoEl : previewImg)
    }


    const LOADER_PAUSE = 'LOADER_PAUSE'
    const LOADER_LOAD_VIDEO = 'LOADER_LOAD_VIDEO'

    const loader = useRef(false)
    // ...
    const [videoVolume, setVideoVolume] = useState(0)
    const [videoBtnUnmute, setVideoBtnUnmute] = useState(false)
    const [videoAutoUnmute, setVideoAutoUnmute] = useState(false)

    useEffect(()=> {
        setVideoVolume(parsedOptions?.video_volume)
        setVideoBtnUnmute(parsedOptions?.video_btn_unmute)
        setVideoAutoUnmute(parsedOptions?.video_auto_unmute)
        setShowCalendar(parsedOptions?.calendar_init)
    }, [ parsedOptions ])


    let previews = null

    const allVideoElements = [videoEl1]

    const lockEls = useRef([]) // Заблокировать элементы, когда его уже выбрали но еще не использовали

    const unlockEl = el => {
        lockEls.current = lockEls.current.filter(one => one !== el)
    }

    const getNextVideoEl = () => {

        const busyEls = [...lockEls.current]

        const lockEl = () => {
            const freeEls = allVideoElements.filter(one => !busyEls.includes(one))
            const el = freeEls[0]
            if (!el) return null
            lockEls.current.push(el)
            return el
        }

        if (!nowPlay.current) {
            return lockEl()
        }

        // Найдем video которое сейчас играет. Исключим его DOM элемент и элементы всех последующих
        let start = videosRef.current.indexOf(nowPlay.current)
        if (start < 0) start = 0
        for (let i = start; i < videosRef.current.length - 1; i++) {
            const item = videosRef.current[i]
            if (!busyEls.includes(item.videoEl)) {
                busyEls.push(item.videoEl)
            }
        }

        return lockEl()
    }

    const processPlay = () => {

        if (!videosRef.current.length) return

        const playing = videosRef.current.find(one => one.playing)
        if (playing) return

        let toPlay

        for (const item of videosRef.current) {
            if (item.played || item.error) continue
            toPlay = item
            break
        }

        // Нет видео в очереди готовых к воспроизведению.
        // Выставим лоадер и запустим через две сек.
        if (!toPlay) {
            // Если лоадер === Pause то значит мы уже ждали пару сек и можно старотовать последнее
            // ...
        }

        if (toPlay) {

            if (!toPlay.loaded) return

            // ...
        }
    }

    const prepareVideo = (video) => {


        const found = videosRef.current.find(one => one.video.filename === video.filename)
        if (found) return true

        // Если пока нет свобдных видеоэлементов, то тоже выходим
        const videoEl = getNextVideoEl()

        // ...
        function onloadHandler(e) {
            // ...
        }

        function onendHandler(e) {
            // ...
        }

        videoEl.current.addEventListener('loadeddata', onloadHandler)

        videoEl.current.addEventListener('ended', onendHandler)


        try {
            videoEl.current.src = video.url
            videoEl.current.load()
        } catch (e) {
            console.warn('Не удалось загрузить файл', video.url)
            videosRef.current = videosRef.current.map(one => {
                if (one === videoObj) {
                    one.error = true
                }
                return one
            })
            processPlay()
            if (previews && Array.isArray(previews)) processPreview(previews)
        }


    }

    const processPreview = (previews) => {
        // ...
    }

    useEffect(() => {
        getActiveConsultantId(consult.id)
    }, [isCurrent])


    const muteRef = useRef(null)

    // ...

    return (
        <div
            className="slide"
            style={{minWidth: isWidgetResized ? 'auto' : '326px'}}
            key={consult.id}
            id={consult.id}
            onClick={(e) => {
                if (isWidgetResized) {
                    if (options.data?.muted_preview) setMuted(false)
                    widgetResizeHandler(isWidgetResized)
                    return
                }

                startCallHandler(consult.id, consult)
            }}>

            {isWidgetResized && isSmallWidth
                ?
                <span></span>
                :
                <p className={widgetColor !== ""
                    ? `voda-slide-nickname voda-slide-nickname-color-${widgetColor}`
                    : "voda-slide-nickname"}
                >
                    {
                        ["IN_PROGRESS", "ACCEPTED"].includes(consult.call_status) ?
                            <svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <circle cx="4" cy="4" r="4" fill="#ffb300"/>
                            </svg>
                            :
                            <svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <circle cx="4" cy="4" r="4" fill="#2CE829"/>
                            </svg>
                    }
                    <span>{consult.nickname}</span>
                </p>
            }
            {
                ["IN_PROGRESS", "ACCEPTED"].includes(consult.call_status) ?
                    <div className="voda-slide-busy-note" style={{bottom: busy_note_bottom}}>
                        <span>{contentSettings.consultant_preview[widgetLanguage].title} </span>
                        <span>{contentSettings.consultant_preview[widgetLanguage].note}</span>
                    </div>
                    : ''
            }

            <LiveMessage liveMsgs={consult?.liveMsgs}/>

            <div className="voda-priview-btn-container" style={{bottom: consult_small_bottom}}>
                <CalendarBtn addClass="preview" show={showCalendar} />
                {
                    isWidgetResized && isSmallWidth
                        ?
                        <div
                            style={ callBtnColor ? {backgroundColor: callBtnColor} : {} }
                            className={
                                consultVideo?.isYoutube
                                    ? "voda-preview-call-btn voda-preview-call-btn-is-youtube voda-is-small-width"
                                    : "voda-preview-call-btn voda-is-small-width"
                            }>
                            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <path fillRule="evenodd" clipRule="evenodd" d="M14.062 5.62001C11.6967 5.37156 9.31285 5.35314 6.94398 5.56501L5.36198 5.70601C4.72717 5.76295 4.13007 6.03213 3.66704 6.47012C3.20401 6.90811 2.90208 7.48934 2.80998 8.12001C2.43378 10.693 2.43378 13.307 2.80998 15.88C2.9023 16.5108 3.20442 17.092 3.66762 17.53C4.13081 17.968 4.72805 18.2371 5.36298 18.294L6.94498 18.434C9.31381 18.6463 11.6976 18.6282 14.063 18.38L14.671 18.316C15.2745 18.2527 15.842 17.9981 16.2905 17.5893C16.739 17.1805 17.0451 16.6391 17.164 16.044L20.211 17.662C20.3204 17.7201 20.4425 17.7502 20.5663 17.7497C20.6902 17.7491 20.812 17.7178 20.9208 17.6587C21.0297 17.5996 21.1222 17.5144 21.1901 17.4108C21.258 17.3072 21.2992 17.1884 21.31 17.065L21.335 16.781C21.6139 13.5994 21.6139 10.3996 21.335 7.21801L21.31 6.93401C21.2991 6.81056 21.2578 6.69172 21.1898 6.58813C21.1218 6.48454 21.0291 6.39942 20.9202 6.34038C20.8112 6.28134 20.6893 6.25022 20.5654 6.24981C20.4414 6.24939 20.3193 6.2797 20.21 6.33801L17.164 7.95601C17.0451 7.36094 16.739 6.81948 16.2905 6.41072C15.842 6.00196 15.2745 5.74733 14.671 5.68401L14.062 5.62001ZM7.07798 7.05901C9.35001 6.8559 11.6364 6.87365 13.905 7.11201L14.513 7.17601C14.8101 7.20747 15.0882 7.33685 15.3037 7.54379C15.5191 7.75072 15.6596 8.02345 15.703 8.31901C16.061 10.76 16.061 13.239 15.703 15.681C15.6596 15.9766 15.5191 16.2493 15.3037 16.4562C15.0882 16.6632 14.8101 16.7926 14.513 16.824L13.905 16.888C11.6364 17.1264 9.35001 17.1441 7.07798 16.941L5.49598 16.799C5.197 16.7723 4.91571 16.6457 4.69746 16.4397C4.47921 16.2336 4.33673 15.96 4.29298 15.663C3.93782 13.2339 3.93782 10.7661 4.29298 8.33701C4.33636 8.03979 4.4787 7.76588 4.69699 7.55956C4.91529 7.35324 5.19679 7.22656 5.49598 7.20001L7.07798 7.05901ZM17.36 9.55001C17.509 11.18 17.509 12.82 17.36 14.45L19.907 15.803C20.0883 13.2709 20.0883 10.7291 19.907 8.19701L17.36 9.55001Z" fill="black"/>
                            </svg>
                        </div>
                        :
                        <div className={
                            consultVideo?.isYoutube ? "voda-preview-call-btn voda-preview-call-btn-is-youtube" : "voda-preview-call-btn"
                        }
                             style={
                                 callBtnColor
                                     ? {bottom: consult_small_bottom, backgroundColor: callBtnColor}
                                     : {bottom: consult_small_bottom}
                             }
                        >
                            {
                                callBtnText ? callBtnText : contentSettings.preview_call_btn[widgetLanguage].text
                            }
                        </div>

                }
            </div>

            {!consultVideo
                ? <img
                    alt={'preview'}
                    src={`${serverParams.url}/upload/${consult.user_image}`}
                    className="voda-img_preview"
                    ref={previewImg}
                />
                : <>
                    {!options.data?.muted_preview || consultVideo?.isYoutube
                        ? false
                        : <MuteBtn onClick={(e)=>{
                            if (isWidgetResized) return
                            e.stopPropagation()
                            setMuted(!muted)
                        }} muted={muted} isWidgetResized={isWidgetResized} isSmallWidth={isSmallWidth} />}

                    {consultVideo?.isYoutube && (() => {
                        // Это для отладки
                        console.log('RENDER:', {isYoutube: consultVideo?.isYoutube, consultYoutubeVideoIsLoading})
                        return true
                    })()
                        ?
                        <div className="voda-iframe-video-holder">
                            <div className="voda-iframe-video-overlay">
                                {
                                    consultYoutubeVideoIsLoading
                                        ? <img
                                            alt={'preview'}
                                            src={`${serverParams.url}/upload/${consult.user_image}`}
                                            className="voda-img_preview"
                                            ref={previewImg}
                                        />
                                        : false
                                }
                            </div>
                            <div className="voda-iframe-video-holder-inner" id="voda-iframe-video-holder-inner"></div>
                        </div>
                        :
                        <video
                            preload="none" playsInline autoPlay="autoplay" muted={muted} loop="loop"
                            id={`voda-video-layer-${1}`}>
                            <source src={consultVideo.url} type="video/mp4"/>

                        </video>
                    }
                </>}
            <span style={{
                position: 'absolute',
                width: '1px',
                height: '1px',
                background: '#ff0000',
                left: '50%',
                bottom: 0,
                zIndex: 1000,
            }}></span>

            {options.data?.is_tablet ? <div className="voda-tablet-call-btn">Связаться с консультантом</div> : false}


        </div>
    )
}

export default ConsultantPreview


Модуль регистрации

// Registration.js

const REQUEST_TIMEOUT = 70000;

const Registration = () => {
    const [currentSection, setCurrentSection] = useState(0)
    const [isLoading, setIsLoading] = useState(false);
    const [remainingTime, setRemainingTime] = useState(null);
    const [serverMessage, setServerMessage] = useState('');


    const [formData, setFormData] = useState({
        greetings: {
            name: '',
            // phone: '',
            email: '',
            about_web: ''
        },

        platform: 'welcome'
    })



    const nextStepHandler = async () => {
        const cfg = window.WELCOME_CONFIG || {}
        if (currentSection === regStructure.length - 1) return;

        // ...
        const apiRequest = socketQuery({
            command: 'createDemoCrmUser',
            object: 'Quiz_registration',
            params: { ...formData }
        });

        try {
            // ...
        } catch (err) {
            clearInterval(countdownInterval);
            setIsLoading(false);
            setRemainingTime(null);
            if (err.message === 'timeout') {
                setHint(true);
                setServerMessage('Сервер не ответил вовремя, попробуйте позже.');
                setTimeout(() => {
                    setHint(false);
                    setServerMessage('');
                }, 10000);
            }
            // Если таймаут, просто гасим лоадер, ответ игнорируется
        }

        if (currentScreen.screen === 'consultants') {
            addConsultantHandler()
        }
    }

    // ...

    const validateData = (inputData, screen) => {
        // ...
    }

    // ...

    return (
        <div className="voda-registration">
            <Logo />
            {/*// ...*/}
        </div>


    )
}

export default Registration