[IMP] bus, *: introduce multi tab service
*: calendar, survey. In order to ease the PR introducing websockets in Odoo, introduce the multi service. Indeed, the cross tab bus won't be necessary anymore but there will still be a need to elect a main tab: some actions should only be triggered once. To achieve this, the code electing the main tab has been split with the one handling the longpolling. The crosstab bus now relies on the multi tab service to known whether or not the current tab is the main tab. task-2053917 closes odoo/odoo#96174 Related: odoo/enterprise#29573 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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/**/*',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user