From b743fda90d82be399e2be1806fa519357e71b834 Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Thu, 2 Sep 2021 09:27:46 +0000 Subject: [PATCH] [IMP] bus: allow tabs to have differents channels This commit prepares the ground for the collaborative feature. Before this commit, it was impossible to have different channels for each tabs. In collaboration, multiples tabs could listen to differents set of channels. Example: One tab X could be subscribed to channel [a, b] and tab Y on channel [a, b] and a tab Z on [a, c]. Before this commit, only the channels of the master tab were listened. So in this case it was either [a, b] or [a, c] depending on which tab is the master. So either channel b or channel c were not listened depending on which tab is the master. Now, each time a tab listens or stops listens to a channel, the master tab listen all channels for all tabs. task-2497931 odoo pr: 75768 Part-of: odoo/odoo#75768 --- addons/bus/static/src/js/crosstab_bus.js | 59 ++++++++++++++++++--- addons/bus/static/src/js/longpolling_bus.js | 18 +++++-- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/addons/bus/static/src/js/crosstab_bus.js b/addons/bus/static/src/js/crosstab_bus.js index bb2b2a00d50..ad90bdb217b 100644 --- a/addons/bus/static/src/js/crosstab_bus.js +++ b/addons/bus/static/src/js/crosstab_bus.js @@ -46,6 +46,7 @@ var CrossTabBus = Longpolling.extend({ var now = new Date().getTime(); // used to prefix localStorage keys this._sanitizedOrigin = session.origin.replace(/:\/{0,2}/g, '_'); + this._currentTabChannels = new Set(); // prevents collisions between different tabs and in tests this._id = _.uniqueId(this.LOCAL_STORAGE_PREFIX) + ':' + now; if (this._callLocalStorage('getItem', 'last_ts', 0) + 50000 < now) { @@ -66,18 +67,20 @@ var CrossTabBus = Longpolling.extend({ * * @override */ - addChannel: function () { + addChannel: function (channel) { + this._currentTabChannels.add(channel); this._super.apply(this, arguments); - this._callLocalStorage('setItem', 'channels', this._channels); + this._updateChannels(); }, /** * Share the bus channels with the others tab by the local storage * * @override */ - deleteChannel: function () { + deleteChannel: function (channel) { + this._currentTabChannels.delete(channel); this._super.apply(this, arguments); - this._callLocalStorage('setItem', 'channels', this._channels); + this._updateChannels(); }, /** * @return {string} @@ -118,12 +121,11 @@ var CrossTabBus = Longpolling.extend({ this._heartbeat(); if (this._isMasterTab) { - this._callLocalStorage('setItem', 'channels', this._channels); this._callLocalStorage('setItem', 'options', this._options); } else { - this._channels = this._callLocalStorage('getItem', 'channels', this._channels); this._options = this._callLocalStorage('getItem', 'options', this._options); } + this._updateChannels(); return; // startPolling will be called again on tab registration } @@ -274,6 +276,45 @@ var CrossTabBus = Longpolling.extend({ this._callLocalStorage('setItem', 'peers', peers); } }, + /** + * Update localstorage channels of with the channels of this tab. + * + * @private + * @return {boolean} true if the aggregated channels has changed. + */ + _updateChannels: function () { + const currentPeerIds = new Set(Object.keys(this._callLocalStorage('getItem', 'peers') || {})); + const peerChannels = this._callLocalStorage('getItem', 'channels') || {}; + const peerChannelsBefore = JSON.stringify(peerChannels); + peerChannels[this._id] = Array.from(this._currentTabChannels); + + // Clean outdated channels. + for (const channelPeerId of Object.keys(peerChannels)) { + if (!currentPeerIds.has(channelPeerId)) { + delete peerChannels[channelPeerId]; + } + } + + const peerChannelsAfter = JSON.stringify(peerChannels); + if (peerChannelsBefore === peerChannelsAfter) { + return false; + } + this._callLocalStorage('setItem', 'channels', peerChannels); + + const allChannels = new Set(); + for (const channels of Object.values(peerChannels)) { + for (const channel of channels) { + allChannels.add(channel); + } + } + // Insure the current tab channels are always in the aggregated channels + // in case this tab is not in the currentPeerIds nor peerChannels. + for (const channel of this._currentTabChannels) { + allChannels.add(channel); + } + this._channels = Array.from(allChannels); + return true; + }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -327,7 +368,9 @@ var CrossTabBus = Longpolling.extend({ } // update channels else if (key === this._generateKey('channels')) { - this._channels = value; + if (this._updateChannels()) { + this._restartPolling(); + }; } // update options else if (key === this._generateKey('options')) { @@ -349,6 +392,8 @@ var CrossTabBus = Longpolling.extend({ 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) { diff --git a/addons/bus/static/src/js/longpolling_bus.js b/addons/bus/static/src/js/longpolling_bus.js index 9c8df877c9a..b9580574ee8 100644 --- a/addons/bus/static/src/js/longpolling_bus.js +++ b/addons/bus/static/src/js/longpolling_bus.js @@ -75,11 +75,7 @@ var LongpollingBus = Bus.extend(ServicesMixin, { addChannel: function (channel) { if (this._channels.indexOf(channel) === -1) { this._channels.push(channel); - if (this._pollRpc) { - this._pollRpc.abort(); - } else { - this.startPolling(); - } + this._restartPolling(); } }, /** @@ -255,6 +251,18 @@ var LongpollingBus = Bus.extend(ServicesMixin, { _onPresence: function () { this._lastPresenceTime = new Date().getTime(); }, + /** + * Restart polling. + * + * @private + */ + _restartPolling() { + if (this._pollRpc) { + this._pollRpc.abort(); + } else { + this.startPolling(); + } + }, }); return LongpollingBus;