diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/bus/static/src/js/crosstab_bus.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/bus/static/src/js/crosstab_bus.js')
| -rw-r--r-- | addons/bus/static/src/js/crosstab_bus.js | 363 |
1 files changed, 363 insertions, 0 deletions
diff --git a/addons/bus/static/src/js/crosstab_bus.js b/addons/bus/static/src/js/crosstab_bus.js new file mode 100644 index 00000000..bb2b2a00 --- /dev/null +++ b/addons/bus/static/src/js/crosstab_bus.js @@ -0,0 +1,363 @@ +odoo.define('bus.CrossTab', function (require) { +"use strict"; + +var Longpolling = require('bus.Longpolling'); + +var session = require('web.session'); + +/** + * 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. + * - 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: + * - window_focus : when the window is focused + * - 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) + */ +var CrossTabBus = Longpolling.extend({ + // constants + TAB_HEARTBEAT_PERIOD: 10000, // 10 seconds + MASTER_TAB_HEARTBEAT_PERIOD: 1500, // 1.5 seconds + HEARTBEAT_OUT_OF_DATE_PERIOD: 5000, // 5 seconds + HEARTBEAT_KILL_OLD_PERIOD: 15000, // 15 seconds + LOCAL_STORAGE_PREFIX: 'bus', + + // properties + _isMasterTab: false, + _isRegistered: false, + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + var now = new Date().getTime(); + // used to prefix localStorage keys + this._sanitizedOrigin = session.origin.replace(/:\/{0,2}/g, '_'); + // 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) { + this._callLocalStorage('removeItem', 'last'); + } + this._lastNotificationID = this._callLocalStorage('getItem', 'last', 0); + this.call('local_storage', 'onStorage', this, this._onStorage); + }, + destroy: function () { + this._super(); + clearTimeout(this._heartbeatTimeout); + }, + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + /** + * Share the bus channels with the others tab by the local storage + * + * @override + */ + addChannel: function () { + this._super.apply(this, arguments); + this._callLocalStorage('setItem', 'channels', this._channels); + }, + /** + * Share the bus channels with the others tab by the local storage + * + * @override + */ + deleteChannel: function () { + this._super.apply(this, arguments); + this._callLocalStorage('setItem', 'channels', this._channels); + }, + /** + * @return {string} + */ + getTabId: function () { + return this._id; + }, + /** + * Tells whether this bus is related to the master tab. + * + * @returns {boolean} + */ + isMasterTab: function () { + return this._isMasterTab; + }, + /** + * Use the local storage to share the long polling from the master tab. + * + * @override + */ + startPolling: function () { + 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) { + 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); + } + return; // startPolling will be called again on tab registration + } + + if (this._isMasterTab) { + this._super.apply(this, arguments); + } + }, + /** + * Share the option with the local storage + * + * @override + */ + updateOption: function () { + this._super.apply(this, arguments); + this._callLocalStorage('setItem', 'options', this._options); + }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * Call local_storage service + * + * @private + * @param {string} method (getItem, setItem, removeItem, on) + * @param {string} key + * @param {any} param + * @returns service information + */ + _callLocalStorage: function (method, key, param) { + return this.call('local_storage', method, this._generateKey(key), param); + }, + /** + * Generates localStorage keys prefixed by bus. (LOCAL_STORAGE_PREFIX = the name + * of this addon), and the sanitized origin, to prevent keys from + * conflicting when several bus instances (polling different origins) + * co-exist. + * + * @private + * @param {string} key + * @returns key prefixed with the origin + */ + _generateKey: function (key) { + return this.LOCAL_STORAGE_PREFIX + '.' + this._sanitizedOrigin + '.' + key; + }, + /** + * @override + * @returns {integer} number of milliseconds since 1 January 1970 00:00:00 + */ + _getLastPresence: function () { + return this._callLocalStorage('getItem', 'lastPresence') || this._super(); + }, + /** + * 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: function () { + 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); + } + + // Write lastPresence in local storage if it has been updated since last heartbeat + var hbPeriod = this._isMasterTab ? this.MASTER_TAB_HEARTBEAT_PERIOD : this.TAB_HEARTBEAT_PERIOD; + if (this._lastPresenceTime + hbPeriod > now) { + this._callLocalStorage('setItem', 'lastPresence', this._lastPresenceTime); + } + + this._heartbeatTimeout = setTimeout(this._heartbeat.bind(this), hbPeriod); + }, + /** + * @private + */ + _registerWindowUnload: function () { + $(window).on('unload.' + this._id, 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: function () { + 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); + } + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + /** + * @override + */ + _onFocusChange: function (params) { + this._super.apply(this, arguments); + this._callLocalStorage('setItem', 'focus', params.focus); + }, + /** + * If it's the master tab, the notifications ares broadcasted to other tabs by the + * local storage. + * + * @override + */ + _onPoll: function (notifications) { + var notifs = this._super(notifications); + if (this._isMasterTab && notifs.length) { + this._callLocalStorage('setItem', 'last', this._lastNotificationID); + this._callLocalStorage('setItem', 'last_ts', new Date().getTime()); + this._callLocalStorage('setItem', 'notification', notifs); + } + }, + /** + * Handler when the local storage is updated + * + * @private + * @param {OdooEvent} event + * @param {string} event.key + * @param {string} event.newValue + */ + _onStorage: function (e) { + 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) { + this.trigger("notification", value); + } + } + // update channels + else if (key === this._generateKey('channels')) { + this._channels = value; + } + // update options + else if (key === this._generateKey('options')) { + this._options = value; + } + // update focus + else if (key === this._generateKey('focus')) { + this._isOdooFocused = value; + this.trigger('window_focus', this._isOdooFocused); + } + }, + /** + * Handler when unload the window + * + * @private + */ + _onUnload: function () { + // unload peer + var peers = this._callLocalStorage('getItem', 'peers') || {}; + delete peers[this._id]; + this._callLocalStorage('setItem', 'peers', peers); + + // unload master + if (this._isMasterTab) { + this._callLocalStorage('removeItem', 'master'); + } + }, +}); + +return CrossTabBus; + +}); + |
