diff --git a/addons/bus/static/src/crosstab_bus.js b/addons/bus/static/src/crosstab_bus.js index 50e6ba5aeb5..1574e245428 100644 --- a/addons/bus/static/src/crosstab_bus.js +++ b/addons/bus/static/src/crosstab_bus.js @@ -9,34 +9,24 @@ import { Longpolling } from '@bus/longpolling_bus'; * CrossTab * * This is an extension of the longpolling bus with browser cross-tab synchronization. - * It uses a Master/Slaves with Leader Election architecture: - * - a single tab handles longpolling. + * It uses the multiTab service. + * - only the main tab handles longpolling. * - tabs are synchronized by means of the local storage. * * localStorage used keys are: * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.channels : shared public channel list to listen during the poll * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.options : shared options * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.notification : the received notifications from the last poll - * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.tab_list : list of opened tab ids - * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.tab_master : generated id of the master tab * * trigger: * - notification : when a notification is receive from the long polling - * - become_master : when this tab became the master - * - no_longer_master : when this tab is not longer the master (the user swith tab) */ export class CrossTab extends Longpolling { constructor(env, services) { super(env, services); - // constants - this.TAB_HEARTBEAT_PERIOD = 10000; // 10 seconds - this.MASTER_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 seconds - this.HEARTBEAT_OUT_OF_DATE_PERIOD = 5000; // 5 seconds - this.HEARTBEAT_KILL_OLD_PERIOD = 15000; // 15 seconds this.LOCAL_STORAGE_PREFIX = 'bus'; // properties - this._isMasterTab = false; this._isRegistered = false; var now = new Date().getTime(); @@ -49,7 +39,11 @@ export class CrossTab extends Longpolling { this._callLocalStorage('removeItem', 'last'); } this._lastNotificationID = this._callLocalStorage('getItem', 'last', 0); + this._registerWindowUnload(); browser.addEventListener('storage', this._onStorage.bind(this)); + + env.bus.addEventListener('no_longer_main_tab', () => this.stopPolling()); + env.bus.addEventListener('become_main_tab', () => this.startPolling()); } //-------------------------------------------------------------------------- @@ -85,49 +79,24 @@ export class CrossTab extends Longpolling { return this._id; } - /** - * Tells whether this bus is related to the master tab. - * - * @returns {boolean} - */ - isMasterTab() { - return this._isMasterTab; - } - /** * Use the local storage to share the long polling from the master tab. * * @override */ startPolling() { - if (this._isActive === null) { - this._heartbeat = this._heartbeat.bind(this); - } if (!this._isRegistered) { this._isRegistered = true; - var peers = this._callLocalStorage('getItem', 'peers', {}); - peers[this._id] = new Date().getTime(); - this._callLocalStorage('setItem', 'peers', peers); - - this._registerWindowUnload(); - - if (!this._callLocalStorage('getItem', 'master')) { - this._startElection(); - } - - this._heartbeat(); - - if (this._isMasterTab) { + if (this.env.services['multiTab'].isOnMainTab()) { this._callLocalStorage('setItem', 'options', this._options); } else { this._options = this._callLocalStorage('getItem', 'options', this._options); } this._updateChannels(); - return; // startPolling will be called again on tab registration } - if (this._isMasterTab) { + if (this.env.services['multiTab'].isOnMainTab()) { super.startPolling(); } } @@ -180,57 +149,6 @@ export class CrossTab extends Longpolling { return this.LOCAL_STORAGE_PREFIX + '.' + this._sanitizedOrigin + '.' + key; } - /** - * Check all the time (according to the constants) if the tab is the master tab and - * check if it is active. Use the local storage for this checks. - * - * @private - * @see _startElection method - */ - _heartbeat() { - var now = new Date().getTime(); - var heartbeatValue = parseInt(this._callLocalStorage('getItem', 'heartbeat', 0)); - var peers = this._callLocalStorage('getItem', 'peers', {}); - - if ((heartbeatValue + this.HEARTBEAT_OUT_OF_DATE_PERIOD) < now) { - // Heartbeat is out of date. Electing new master - this._startElection(); - heartbeatValue = parseInt(this._callLocalStorage('getItem', 'heartbeat', 0)); - } - - if (this._isMasterTab) { - //walk through all peers and kill old - var cleanedPeers = {}; - for (var peerName in peers) { - if (peers[peerName] + this.HEARTBEAT_KILL_OLD_PERIOD > now) { - cleanedPeers[peerName] = peers[peerName]; - } - } - - if (heartbeatValue !== this.lastHeartbeat) { - // someone else is also master... - // it should not happen, except in some race condition situation. - this._isMasterTab = false; - this.lastHeartbeat = 0; - peers[this._id] = now; - this._callLocalStorage('setItem', 'peers', peers); - this.stopPolling(); - this.trigger('no_longer_master'); - } else { - this.lastHeartbeat = now; - this._callLocalStorage('setItem', 'heartbeat', now); - this._callLocalStorage('setItem', 'peers', cleanedPeers); - } - } else { - //update own heartbeat - peers[this._id] = now; - this._callLocalStorage('setItem', 'peers', peers); - } - - var hbPeriod = this._isMasterTab ? this.MASTER_TAB_HEARTBEAT_PERIOD : this.TAB_HEARTBEAT_PERIOD; - this._heartbeatTimeout = browser.setTimeout(this._heartbeat.bind(this), hbPeriod); - } - /** * @private */ @@ -238,45 +156,6 @@ export class CrossTab extends Longpolling { browser.addEventListener('unload', this._onUnload.bind(this)); } - /** - * Check with the local storage if the current tab is the master tab. - * If this tab became the master, trigger 'become_master' event - * - * @private - */ - _startElection() { - if (this._isMasterTab) { - return; - } - //check who's next - var now = new Date().getTime(); - var peers = this._callLocalStorage('getItem', 'peers', {}); - var heartbeatKillOld = now - this.HEARTBEAT_KILL_OLD_PERIOD; - var newMaster; - for (var peerName in peers) { - //check for dead peers - if (peers[peerName] < heartbeatKillOld) { - continue; - } - newMaster = peerName; - break; - } - - if (newMaster === this._id) { - //we're next in queue. Electing as master - this.lastHeartbeat = now; - this._callLocalStorage('setItem', 'heartbeat', this.lastHeartbeat); - this._callLocalStorage('setItem', 'master', true); - this._isMasterTab = true; - this.startPolling(); - this.trigger('become_master'); - - //removing master peer from queue - delete peers[newMaster]; - this._callLocalStorage('setItem', 'peers', peers); - } - } - /** * Update localstorage channels of with the channels of this tab. * @@ -284,7 +163,7 @@ export class CrossTab extends Longpolling { * @return {boolean} true if the aggregated channels has changed. */ _updateChannels() { - const currentPeerIds = new Set(Object.keys(this._callLocalStorage('getItem', 'peers') || {})); + const currentPeerIds = new Set(this.env.services['multiTab'].getAllTabIds()); const peerChannels = this._callLocalStorage('getItem', 'channels') || {}; const peerChannelsBefore = JSON.stringify(peerChannels); peerChannels[this._id] = Array.from(this._currentTabChannels); @@ -329,7 +208,7 @@ export class CrossTab extends Longpolling { */ _onPoll(notifications) { var notifs = super._onPoll(notifications); - if (this._isMasterTab && notifs.length) { + if (this.env.services['multiTab'].isOnMainTab() && notifs.length) { this._callLocalStorage('setItem', 'last', this._lastNotificationID); this._callLocalStorage('setItem', 'last_ts', new Date().getTime()); this._callLocalStorage('setItem', 'notification', notifs); @@ -351,18 +230,13 @@ export class CrossTab extends Longpolling { var value = JSON.parse(e.newValue); var key = e.key; - if (this._isRegistered && key === this._generateKey('master') && !value) { - //master was unloaded - this._startElection(); - } - // last notification id changed if (key === this._generateKey('last')) { this._lastNotificationID = value || 0; } // notifications changed else if (key === this._generateKey('notification')) { - if (!this._isMasterTab) { + if (!this.env.services['multiTab'].isOnMainTab()) { this.trigger("notification", value); } } @@ -384,16 +258,7 @@ export class CrossTab extends Longpolling { * @private */ _onUnload() { - // unload peer - var peers = this._callLocalStorage('getItem', 'peers') || {}; - delete peers[this._id]; - this._callLocalStorage('setItem', 'peers', peers); this._currentTabChannels.clear(); this._updateChannels(); - - // unload master - if (this._isMasterTab) { - this._callLocalStorage('removeItem', 'master'); - } } } diff --git a/addons/bus/static/src/services/bus_service.js b/addons/bus/static/src/services/bus_service.js index d0497f15cb8..1a94dd10b2b 100644 --- a/addons/bus/static/src/services/bus_service.js +++ b/addons/bus/static/src/services/bus_service.js @@ -28,7 +28,7 @@ export class BusService extends CrossTab { */ sendNotification(options, callback) { if (window.Notification && Notification.permission === "granted") { - if (this.isMasterTab()) { + if (this.env.services['multiTab'].isOnMainTab()) { try { this._sendNativeNotification(options.title, options.message, callback); } catch (error) { @@ -45,7 +45,7 @@ export class BusService extends CrossTab { } } else { this.env.services['notification'].add(options.message, options); - if (this.isMasterTab()) { + if (this.env.services['multiTab'].isOnMainTab()) { this._beep(); } } @@ -121,7 +121,7 @@ export class BusService extends CrossTab { } export const busService = { - dependencies: ['notification', 'presence', 'rpc'], + dependencies: ['notification', 'presence', 'rpc', 'multiTab'], start(env, services) { return new BusService(env, services); }, diff --git a/addons/bus/static/src/services/legacy/make_multi_tab_to_legacy_env.js b/addons/bus/static/src/services/legacy/make_multi_tab_to_legacy_env.js new file mode 100644 index 00000000000..7cdac6ae64a --- /dev/null +++ b/addons/bus/static/src/services/legacy/make_multi_tab_to_legacy_env.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { registry } from '@web/core/registry'; + +export function makeMultiTabToLegacyEnv(legacyEnv) { + return { + dependencies: ['multiTab'], + start(_, { multiTab }) { + legacyEnv.services['multiTab'] = multiTab; + }, + }; +} + +registry.category('wowlToLegacyServiceMappers').add('multi_tab_to_legacy_env', makeMultiTabToLegacyEnv); diff --git a/addons/bus/static/src/services/multi_tab_service.js b/addons/bus/static/src/services/multi_tab_service.js new file mode 100644 index 00000000000..170d3bce543 --- /dev/null +++ b/addons/bus/static/src/services/multi_tab_service.js @@ -0,0 +1,213 @@ +/** @odoo-module **/ + +import { registry } from '@web/core/registry'; +import { browser } from '@web/core/browser/browser'; +import session from 'web.session'; + +/** + * This class uses a Master/Slaves with Leader Election architecture in + * order to keep track of the main tab. Tabs are synchronized thanks to the + * localStorage. + * + * localStorage used keys are: + * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.lastPresenceByTab: + * mapping of tab ids to their last recorded presence. + * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.main : id of the current + * main tab. + * - {LOCAL_STORAGE_PREFIX}.{sanitizedOrigin}.heartbeat : last main tab + * heartbeat time. + * + * trigger on env.bus: + * - become_main_tab : when this tab became the main. + * - no_longer_main_tab : when this tab is no longer the main. + */ +export class MultiTab { + constructor(env) { + this.env = env; + + // CONSTANTS + this.TAB_HEARTBEAT_PERIOD = 10000; // 10 seconds + this.MAIN_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 seconds + this.HEARTBEAT_OUT_OF_DATE_PERIOD = 5000; // 5 seconds + this.HEARTBEAT_KILL_OLD_PERIOD = 15000; // 15 seconds + this.LOCAL_STORAGE_PREFIX = 'multiTabService'; + + // PROPERTIES + this._isOnMainTab = false; + this._sanitizedOrigin = session.origin.replace(/:\/{0,2}/g, '_'); + + const now = new Date().getTime(); + this._id = _.uniqueId(this.LOCAL_STORAGE_PREFIX) + ':' + now; + browser.addEventListener('unload', this._onUnload.bind(this)); + browser.addEventListener('storage', this._onStorage.bind(this)); + // REGISTER THIS TAB + const lastPresenceByTab = this._callLocalStorage('getItem', 'lastPresenceByTab', {}); + lastPresenceByTab[this._id] = now; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + + if (!this._callLocalStorage('getItem', 'main')) { + this._startElection(); + } + this._heartbeat(); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + isOnMainTab() { + return this._isOnMainTab; + } + + getAllTabIds() { + return Object.keys(this._callLocalStorage('getItem', 'lastPresenceByTab', {})); + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Call browser localStorage. + * + * @private + * @param {string} method (getItem, setItem, removeItem) + * @param {string} key + * @param {any} param + * @returns Result of the called method, parsed. + */ + _callLocalStorage(method, key, param) { + if (method === 'setItem') { + param = JSON.stringify(param); + } + const result = browser.localStorage[method](this._generateKey(key), param); + if (method === 'getItem') { + return result ? JSON.parse(result) : param; + } + } + + /** + * Generates localStorage keys prefixed by LOCAL_STORAGE_PREFIX and + * the sanitized origin, to prevent keys from conflicting when + * several multi tab services co-exist. + * + * @private + * @param {string} key + * @returns Key prefixed with the origin. + */ + _generateKey(key) { + return this.LOCAL_STORAGE_PREFIX + '.' + this._sanitizedOrigin + '.' + key; + } + + /** + * Check all the time (according to the constants) if the tab is the main tab and + * check if it is active. Use the local storage for this checks. + * + * @private + * @see _startElection method + */ + _heartbeat() { + const now = new Date().getTime(); + let heartbeatValue = this._callLocalStorage('getItem', 'heartbeat', 0); + const lastPresenceByTab = this._callLocalStorage('getItem', 'lastPresenceByTab', {}); + if (heartbeatValue + this.HEARTBEAT_OUT_OF_DATE_PERIOD < now) { + // Heartbeat is out of date. Electing new main. + this._startElection(); + heartbeatValue = this._callLocalStorage('getItem', 'heartbeat', 0); + } + if (this._isOnMainTab) { + // Walk through all tabs and kill old ones. + const cleanedTabs = {}; + for (const [tabId, lastPresence] of Object.entries(lastPresenceByTab)) { + if (lastPresence + this.HEARTBEAT_KILL_OLD_PERIOD > now) { + cleanedTabs[tabId] = lastPresence; + } + } + if (heartbeatValue !== this.lastHeartbeat) { + // Someone else is also main... + // It should not happen, except in some race condition situation. + this._isOnMainTab = false; + this.lastHeartbeat = 0; + lastPresenceByTab[this._id] = now; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + this.env.bus.trigger('no_longer_main_tab'); + } else { + this.lastHeartbeat = now; + this._callLocalStorage('setItem', 'heartbeat', now); + this._callLocalStorage('setItem', 'lastPresenceByTab', cleanedTabs); + } + } else { + // Update own heartbeat. + lastPresenceByTab[this._id] = now; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + } + const hbPeriod = this._isOnMainTab ? this.MAIN_TAB_HEARTBEAT_PERIOD : this.TAB_HEARTBEAT_PERIOD; + this._heartbeatTimeout = browser.setTimeout(this._heartbeat.bind(this), hbPeriod); + } + + /** + * Check with the local storage if the current tab is the main tab. + * If this tab became the main, trigger 'become_main_tab' event. + * + * @private + */ + _startElection() { + if (this._isOnMainTab) { + return; + } + // Check who's next. + const now = new Date().getTime(); + const lastPresenceByTab = this._callLocalStorage('getItem', 'lastPresenceByTab', {}); + const heartbeatKillOld = now - this.HEARTBEAT_KILL_OLD_PERIOD; + let newMain; + for (const [tab, lastPresence] of Object.entries(lastPresenceByTab)) { + // Check for dead tabs. + if (lastPresence < heartbeatKillOld) { + continue; + } + newMain = tab; + break; + } + if (newMain === this._id) { + // We're next in queue. Electing as main. + this.lastHeartbeat = now; + this._callLocalStorage('setItem', 'heartbeat', this.lastHeartbeat); + this._callLocalStorage('setItem', 'main', true); + this._isOnMainTab = true; + this.env.bus.trigger('become_main_tab'); + // Removing main peer from queue. + delete lastPresenceByTab[newMain]; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + } + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onStorage({ key, newValue }) { + if (key === this._generateKey('main') && !newValue) { + // Main was unloaded. + this._startElection(); + } + } + + _onUnload() { + const lastPresenceByTab = this._callLocalStorage('getItem', 'lastPresenceByTab', {}); + delete lastPresenceByTab[this._id]; + this._callLocalStorage('setItem', 'lastPresenceByTab', lastPresenceByTab); + + // Unload main. + if (this._isOnMainTab) { + this._callLocalStorage('removeItem', 'main'); + } + } +} + +export const multiTabService = { + start(env) { + return new MultiTab(env); + }, +}; + +registry.category('services').add('multiTab', multiTabService); diff --git a/addons/bus/static/tests/assets_watchdog_tests.js b/addons/bus/static/tests/assets_watchdog_tests.js index 853794191f5..5189d8c39e9 100644 --- a/addons/bus/static/tests/assets_watchdog_tests.js +++ b/addons/bus/static/tests/assets_watchdog_tests.js @@ -2,6 +2,7 @@ import { busService } from "@bus/services/bus_service"; import { presenceService } from "@bus/services/presence_service"; +import { multiTabService } from "@bus/services/multi_tab_service"; import { createWebClient } from "@web/../tests/webclient/helpers"; import { assetsWatchdogService } from "@bus/services/assets_watchdog_service"; @@ -17,6 +18,7 @@ QUnit.module("Bus Assets WatchDog", (hooks) => { serviceRegistry.add("assetsWatchdog", assetsWatchdogService); serviceRegistry.add("bus_service", busService); serviceRegistry.add("presence", presenceService); + serviceRegistry.add("multiTab", multiTabService); patchWithCleanup(browser, { setTimeout(fn) { return this._super(fn, 0); diff --git a/addons/bus/static/tests/bus_tests.js b/addons/bus/static/tests/bus_tests.js index 97ac8c15e80..7f22e6361d4 100644 --- a/addons/bus/static/tests/bus_tests.js +++ b/addons/bus/static/tests/bus_tests.js @@ -3,6 +3,7 @@ odoo.define('web.bus_tests', function (require) { var { busService } = require('@bus/services/bus_service'); const { presenceService } = require('@bus/services/presence_service'); +const { multiTabService } = require('@bus/services/multi_tab_service'); var { CrossTab } = require('@bus/crosstab_bus'); var testUtils = require('web.test_utils'); @@ -14,17 +15,18 @@ const { makeTestEnv } = require('@web/../tests/helpers/mock_env'); QUnit.module('Bus', { beforeEach: function () { - const customBusService = { - ...busService, + const customMultiTabService = { + ...multiTabService, start() { - const originalBusService = busService.start(...arguments); - originalBusService.TAB_HEARTBEAT_PERIOD = 10; - originalBusService.MASTER_TAB_HEARTBEAT_PERIOD = 1; - return originalBusService; + const originalMultiTabService = multiTabService.start(...arguments); + originalMultiTabService.TAB_HEARTBEAT_PERIOD = 10; + originalMultiTabService.MAIN_TAB_HEARTBEAT_PERIOD = 1; + return originalMultiTabService; }, }; - registry.category('services').add('bus_service', customBusService); + registry.category('services').add('bus_service', busService); registry.category('services').add('presence', presenceService); + registry.category('services').add('multiTab', customMultiTabService); }, }, function () { QUnit.test('notifications received from the longpolling channel', async function (assert) { @@ -194,7 +196,7 @@ QUnit.module('Bus', { ]); }); - QUnit.test('cross tab bus elect new master on master unload', async function (assert) { + QUnit.test('multi tab service elects new master on master unload', async function (assert) { assert.expect(8); // master @@ -240,7 +242,7 @@ QUnit.module('Bus', { await testUtils.nextTick(); // simulate unloading master - masterEnv.services['bus_service']._onUnload(); + masterEnv.services['multiTab']._onUnload(); pollPromiseSlave.resolve([{ id: 2, diff --git a/addons/calendar/static/tests/calendar_notification_tests.js b/addons/calendar/static/tests/calendar_notification_tests.js index 9bd7af0618e..0bc10ea58a3 100644 --- a/addons/calendar/static/tests/calendar_notification_tests.js +++ b/addons/calendar/static/tests/calendar_notification_tests.js @@ -2,6 +2,7 @@ import { busService } from "@bus/services/bus_service"; import { presenceService } from "@bus/services/presence_service"; +import { multiTabService } from "@bus/services/multi_tab_service"; import { createWebClient } from "@web/../tests/webclient/helpers"; import { calendarNotificationService } from "@calendar/js/services/calendar_notification_service"; @@ -20,6 +21,7 @@ QUnit.module("Calendar Notification", (hooks) => { serviceRegistry.add("calendarNotification", calendarNotificationService); serviceRegistry.add("bus_service", busService); serviceRegistry.add("presence", presenceService); + serviceRegistry.add("multiTab", multiTabService); patchWithCleanup(browser, { setTimeout(fn) { this._super(fn, 0); diff --git a/addons/im_livechat/__manifest__.py b/addons/im_livechat/__manifest__.py index 36d6b6baf3c..2fa02db7da9 100644 --- a/addons/im_livechat/__manifest__.py +++ b/addons/im_livechat/__manifest__.py @@ -185,6 +185,7 @@ Help your customers with this chat, and analyse their feedback. 'bus/static/src/longpolling_bus.js', 'bus/static/src/crosstab_bus.js', 'bus/static/src/services/bus_service.js', + 'bus/static/src/services/multi_tab_service.js', 'mail/static/src/js/utils.js', 'im_livechat/static/src/legacy/public_livechat_history_tracking.js', 'im_livechat/static/src/legacy/public_livechat_chatbot.js', diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 1af73dac56b..adadf33d654 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -98,6 +98,7 @@ 'bus/static/src/*.js', 'bus/static/src/services/bus_service.js', 'bus/static/src/services/presence_service.js', + 'bus/static/src/services/multi_tab_service.js', 'bus/static/src/services/legacy/make_bus_service_to_legacy_env.js', 'web/static/lib/luxon/luxon.js', 'web/static/src/core/**/*', diff --git a/addons/mail/static/tests/helpers/webclient_setup.js b/addons/mail/static/tests/helpers/webclient_setup.js index c17ad188947..974ccd7f57d 100644 --- a/addons/mail/static/tests/helpers/webclient_setup.js +++ b/addons/mail/static/tests/helpers/webclient_setup.js @@ -1,7 +1,9 @@ /** @odoo-module **/ import { busService } from '@bus/services/bus_service'; +import { multiTabService } from '@bus/services/multi_tab_service'; import { makeBusServiceToLegacyEnv } from '@bus/services/legacy/make_bus_service_to_legacy_env'; +import { makeMultiTabToLegacyEnv } from '@bus/services/legacy/make_multi_tab_to_legacy_env'; import { makeFakePresenceService } from '@bus/../tests/helpers/mock_services'; import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container'; @@ -86,6 +88,7 @@ function setupMessagingServiceRegistries({ }; const customBusService = { + ...busService, start() { const originalService = busService.start(...arguments); Object.assign(originalService, { @@ -106,6 +109,7 @@ function setupMessagingServiceRegistries({ isOdooFocused: () => true, }), systrayService, + multiTab: multiTabService, ...services, }; @@ -114,6 +118,7 @@ function setupMessagingServiceRegistries({ }); registry.category('wowlToLegacyServiceMappers').add('bus_service_to_legacy_env', makeBusServiceToLegacyEnv); + registry.category('wowlToLegacyServiceMappers').add('multi_tab_to_legacy_env', makeMultiTabToLegacyEnv); registry.category('wowlToLegacyServiceMappers').add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv); } diff --git a/addons/survey/static/src/js/survey_form.js b/addons/survey/static/src/js/survey_form.js index d20f0da0462..2890badd11f 100644 --- a/addons/survey/static/src/js/survey_form.js +++ b/addons/survey/static/src/js/survey_form.js @@ -934,10 +934,10 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa this.call('bus_service', 'addChannel', this.options.surveyToken); this.call('bus_service', 'startPolling'); - if (!this._checkIsMasterTab()) { + if (!this._checkisOnMainTab()) { this.shouldReloadMasterTab = true; this.masterTabCheckInterval = setInterval(function () { - if (self._checkIsMasterTab()) { + if (self._checkisOnMainTab()) { clearInterval(self.masterTabCheckInterval); } }, 2000); @@ -1077,10 +1077,10 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa * * @private */ - _checkIsMasterTab: function () { - var isMasterTab = this.call('bus_service', 'isMasterTab'); + _checkisOnMainTab: function () { + var isOnMainTab = this.call('multiTab', 'isOnMainTab'); var $errorModal = this.$('#MasterTabErrorModal'); - if (isMasterTab) { + if (isOnMainTab) { // Force reload the page when survey is ready to be followed, to force restart long polling if (this.shouldReloadMasterTab) { window.location.reload();