)]}' {"version": 3, "sources": ["/web/static/src/module_loader.js", "/bus/static/src/workers/websocket_worker.js", "/bus/static/src/workers/websocket_worker_script.js", "/bus/static/src/workers/websocket_worker_utils.js"], "mappings": "AAAA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AClOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1dA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "sourcesContent": ["/**\n *------------------------------------------------------------------------------\n * Odoo Web Boostrap Code\n *------------------------------------------------------------------------------\n */\n(function () {\n \"use strict\";\n\n class ModuleLoader {\n /** @type {Map} mapping name => deps/fn */\n factories = new Map();\n /** @type {Set} names of modules waiting to be started */\n jobs = new Set();\n /** @type {Set} names of failed modules */\n failed = new Set();\n\n /** @type {Map} mapping name => value */\n modules = new Map();\n\n bus = new EventTarget();\n\n checkErrorProm = null;\n\n /**\n * @param {string} name\n * @param {string[]} deps\n * @param {Function} factory\n */\n define(name, deps, factory) {\n if (typeof name !== \"string\") {\n throw new Error(`Invalid name definition: ${name} (should be a string)\"`);\n }\n if (!(deps instanceof Array)) {\n throw new Error(`Dependencies should be defined by an array: ${deps}`);\n }\n if (typeof factory !== \"function\") {\n throw new Error(`Factory should be defined by a function ${factory}`);\n }\n if (!this.factories.has(name)) {\n this.factories.set(name, {\n deps,\n fn: factory,\n ignoreMissingDeps: globalThis.__odooIgnoreMissingDependencies,\n });\n this.addJob(name);\n this.checkErrorProm ||= Promise.resolve().then(() => {\n this.checkAndReportErrors();\n this.checkErrorProm = null;\n });\n }\n }\n\n addJob(name) {\n this.jobs.add(name);\n this.startModules();\n }\n\n findJob() {\n for (const job of this.jobs) {\n if (this.factories.get(job).deps.every((dep) => this.modules.has(dep))) {\n return job;\n }\n }\n return null;\n }\n\n startModules() {\n let job;\n while ((job = this.findJob())) {\n this.startModule(job);\n }\n }\n\n startModule(name) {\n const require = (name) => this.modules.get(name);\n this.jobs.delete(name);\n const factory = this.factories.get(name);\n let value = null;\n try {\n value = factory.fn(require);\n } catch (error) {\n this.failed.add(name);\n throw new Error(`Error while loading \"${name}\":\\n${error}`);\n }\n this.modules.set(name, value);\n this.bus.dispatchEvent(\n new CustomEvent(\"module-started\", { detail: { moduleName: name, module: value } })\n );\n }\n\n findErrors() {\n // cycle detection\n const dependencyGraph = new Map();\n for (const job of this.jobs) {\n dependencyGraph.set(job, this.factories.get(job).deps);\n }\n function visitJobs(jobs, visited = new Set()) {\n for (const job of jobs) {\n const result = visitJob(job, visited);\n if (result) {\n return result;\n }\n }\n return null;\n }\n\n function visitJob(job, visited) {\n if (visited.has(job)) {\n const jobs = Array.from(visited).concat([job]);\n const index = jobs.indexOf(job);\n return jobs\n .slice(index)\n .map((j) => `\"${j}\"`)\n .join(\" => \");\n }\n const deps = dependencyGraph.get(job);\n return deps ? visitJobs(deps, new Set(visited).add(job)) : null;\n }\n\n // missing dependencies\n const missing = new Set();\n for (const job of this.jobs) {\n const factory = this.factories.get(job);\n if (factory.ignoreMissingDeps) {\n continue;\n }\n for (const dep of factory.deps) {\n if (!this.factories.has(dep)) {\n missing.add(dep);\n }\n }\n }\n\n return {\n failed: [...this.failed],\n cycle: visitJobs(this.jobs),\n missing: [...missing],\n unloaded: [...this.jobs].filter((j) => !this.factories.get(j).ignoreMissingDeps),\n };\n }\n\n async checkAndReportErrors() {\n const { failed, cycle, missing, unloaded } = this.findErrors();\n if (!failed.length && !unloaded.length) {\n return;\n }\n\n function domReady(cb) {\n if (document.readyState === \"complete\") {\n cb();\n } else {\n document.addEventListener(\"DOMContentLoaded\", cb);\n }\n }\n\n function list(heading, names) {\n const frag = document.createDocumentFragment();\n if (!names || !names.length) {\n return frag;\n }\n frag.textContent = heading;\n const ul = document.createElement(\"ul\");\n for (const el of names) {\n const li = document.createElement(\"li\");\n li.textContent = el;\n ul.append(li);\n }\n frag.appendChild(ul);\n return frag;\n }\n\n domReady(() => {\n // Empty body\n while (document.body.childNodes.length) {\n document.body.childNodes[0].remove();\n }\n const container = document.createElement(\"div\");\n container.className =\n \"o_module_error position-fixed w-100 h-100 d-flex align-items-center flex-column bg-white overflow-auto modal\";\n container.style.zIndex = \"10000\";\n const alert = document.createElement(\"div\");\n alert.className = \"alert alert-danger o_error_detail fw-bold m-auto\";\n container.appendChild(alert);\n alert.appendChild(\n list(\n \"The following modules failed to load because of an error, you may find more information in the devtools console:\",\n failed\n )\n );\n alert.appendChild(\n list(\n \"The following modules could not be loaded because they form a dependency cycle:\",\n cycle && [cycle]\n )\n );\n alert.appendChild(\n list(\n \"The following modules are needed by other modules but have not been defined, they may not be present in the correct asset bundle:\",\n missing\n )\n );\n alert.appendChild(\n list(\n \"The following modules could not be loaded because they have unmet dependencies, this is a secondary error which is likely caused by one of the above problems:\",\n unloaded\n )\n );\n document.body.appendChild(container);\n });\n }\n }\n\n if (!globalThis.odoo) {\n globalThis.odoo = {};\n }\n const odoo = globalThis.odoo;\n if (odoo.debug && !new URLSearchParams(location.search).has(\"debug\")) {\n // remove debug mode if not explicitely set in url\n odoo.debug = \"\";\n }\n\n const loader = new ModuleLoader();\n odoo.define = loader.define.bind(loader);\n\n odoo.loader = loader;\n})();\n", "/** @odoo-module **/\n\nimport { debounce } from \"@bus/workers/websocket_worker_utils\";\n\n/**\n * Type of events that can be sent from the worker to its clients.\n *\n * @typedef { 'connect' | 'reconnect' | 'disconnect' | 'reconnecting' | 'notification' | 'initialized' } WorkerEvent\n */\n\n/**\n * Type of action that can be sent from the client to the worker.\n *\n * @typedef {'add_channel' | 'delete_channel' | 'force_update_channels' | 'initialize_connection' | 'send' | 'leave' | 'stop' | 'start'} WorkerAction\n */\n\nexport const WEBSOCKET_CLOSE_CODES = Object.freeze({\n CLEAN: 1000,\n GOING_AWAY: 1001,\n PROTOCOL_ERROR: 1002,\n INCORRECT_DATA: 1003,\n ABNORMAL_CLOSURE: 1006,\n INCONSISTENT_DATA: 1007,\n MESSAGE_VIOLATING_POLICY: 1008,\n MESSAGE_TOO_BIG: 1009,\n EXTENSION_NEGOTIATION_FAILED: 1010,\n SERVER_ERROR: 1011,\n RESTART: 1012,\n TRY_LATER: 1013,\n BAD_GATEWAY: 1014,\n SESSION_EXPIRED: 4001,\n KEEP_ALIVE_TIMEOUT: 4002,\n RECONNECTING: 4003,\n});\n// Should be incremented on every worker update in order to force\n// update of the worker in browser cache.\nexport const WORKER_VERSION = \"1.0.7\";\nconst MAXIMUM_RECONNECT_DELAY = 60000;\n\n/**\n * This class regroups the logic necessary in order for the\n * SharedWorker/Worker to work. Indeed, Safari and some minor browsers\n * do not support SharedWorker. In order to solve this issue, a Worker\n * is used in this case. The logic is almost the same than the one used\n * for SharedWorker and this class implements it.\n */\nexport class WebsocketWorker {\n INITIAL_RECONNECT_DELAY = 1000;\n RECONNECT_JITTER = 1000;\n\n constructor() {\n // Timestamp of start of most recent bus service sender\n this.newestStartTs = undefined;\n this.websocketURL = \"\";\n this.currentUID = null;\n this.currentDB = null;\n this.isWaitingForNewUID = true;\n this.channelsByClient = new Map();\n this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n this.connectTimeout = null;\n this.debugModeByClient = new Map();\n this.isDebug = false;\n this.isReconnecting = false;\n this.lastChannelSubscription = null;\n this.lastNotificationId = 0;\n this.messageWaitQueue = [];\n this._forceUpdateChannels = debounce(this._forceUpdateChannels, 300);\n this._updateChannels = debounce(this._updateChannels, 0);\n\n this._onWebsocketClose = this._onWebsocketClose.bind(this);\n this._onWebsocketError = this._onWebsocketError.bind(this);\n this._onWebsocketMessage = this._onWebsocketMessage.bind(this);\n this._onWebsocketOpen = this._onWebsocketOpen.bind(this);\n }\n\n //--------------------------------------------------------------------------\n // Public\n //--------------------------------------------------------------------------\n\n /**\n * Send the message to all the clients that are connected to the\n * worker.\n *\n * @param {WorkerEvent} type Event to broadcast to connected\n * clients.\n * @param {Object} data\n */\n broadcast(type, data) {\n for (const client of this.channelsByClient.keys()) {\n client.postMessage({ type, data });\n }\n }\n\n /**\n * Register a client handled by this worker.\n *\n * @param {MessagePort} messagePort\n */\n registerClient(messagePort) {\n messagePort.onmessage = (ev) => {\n this._onClientMessage(messagePort, ev.data);\n };\n this.channelsByClient.set(messagePort, []);\n }\n\n /**\n * Send message to the given client.\n *\n * @param {number} client\n * @param {WorkerEvent} type\n * @param {Object} data\n */\n sendToClient(client, type, data) {\n client.postMessage({ type, data });\n }\n\n //--------------------------------------------------------------------------\n // PRIVATE\n //--------------------------------------------------------------------------\n\n /**\n * Called when a message is posted to the worker by a client (i.e. a\n * MessagePort connected to this worker).\n *\n * @param {MessagePort} client\n * @param {Object} message\n * @param {WorkerAction} [message.action]\n * Action to execute.\n * @param {Object|undefined} [message.data] Data required by the\n * action.\n */\n _onClientMessage(client, { action, data }) {\n switch (action) {\n case \"send\":\n return this._sendToServer(data);\n case \"start\":\n return this._start();\n case \"stop\":\n return this._stop();\n case \"leave\":\n return this._unregisterClient(client);\n case \"add_channel\":\n return this._addChannel(client, data);\n case \"delete_channel\":\n return this._deleteChannel(client, data);\n case \"force_update_channels\":\n return this._forceUpdateChannels();\n case \"initialize_connection\":\n return this._initializeConnection(client, data);\n }\n }\n\n /**\n * Add a channel for the given client. If this channel is not yet\n * known, update the subscription on the server.\n *\n * @param {MessagePort} client\n * @param {string} channel\n */\n _addChannel(client, channel) {\n const clientChannels = this.channelsByClient.get(client);\n if (!clientChannels.includes(channel)) {\n clientChannels.push(channel);\n this.channelsByClient.set(client, clientChannels);\n this._updateChannels();\n }\n }\n\n /**\n * Remove a channel for the given client. If this channel is not\n * used anymore, update the subscription on the server.\n *\n * @param {MessagePort} client\n * @param {string} channel\n */\n _deleteChannel(client, channel) {\n const clientChannels = this.channelsByClient.get(client);\n if (!clientChannels) {\n return;\n }\n const channelIndex = clientChannels.indexOf(channel);\n if (channelIndex !== -1) {\n clientChannels.splice(channelIndex, 1);\n this._updateChannels();\n }\n }\n\n /**\n * Update the channels on the server side even if the channels on\n * the client side are the same than the last time we subscribed.\n */\n _forceUpdateChannels() {\n this._updateChannels({ force: true });\n }\n\n /**\n * Remove the given client from this worker client list as well as\n * its channels. If some of its channels are not used anymore,\n * update the subscription on the server.\n *\n * @param {MessagePort} client\n */\n _unregisterClient(client) {\n this.channelsByClient.delete(client);\n this.debugModeByClient.delete(client);\n this.isDebug = Object.values(this.debugModeByClient).some(\n (debugValue) => debugValue !== \"\"\n );\n this._updateChannels();\n }\n\n /**\n * Initialize a client connection to this worker.\n *\n * @param {Object} param0\n * @param {string} [param0.db] Database name.\n * @param {String} [param0.debug] Current debugging mode for the\n * given client.\n * @param {Number} [param0.lastNotificationId] Last notification id\n * known by the client.\n * @param {String} [param0.websocketURL] URL of the websocket endpoint.\n * @param {Number|false|undefined} [param0.uid] Current user id\n * - Number: user is logged whether on the frontend/backend.\n * - false: user is not logged.\n * - undefined: not available (e.g. livechat support page)\n * @param {Number} param0.startTs Timestamp of start of bus service sender.\n */\n _initializeConnection(client, { db, debug, lastNotificationId, uid, websocketURL, startTs }) {\n if (this.newestStartTs && this.newestStartTs > startTs) {\n this.debugModeByClient[client] = debug;\n this.isDebug = Object.values(this.debugModeByClient).some(\n (debugValue) => debugValue !== \"\"\n );\n this.sendToClient(client, \"initialized\");\n return;\n }\n this.newestStartTs = startTs;\n this.websocketURL = websocketURL;\n this.lastNotificationId = lastNotificationId;\n this.debugModeByClient[client] = debug;\n this.isDebug = Object.values(this.debugModeByClient).some(\n (debugValue) => debugValue !== \"\"\n );\n const isCurrentUserKnown = uid !== undefined;\n if (this.isWaitingForNewUID && isCurrentUserKnown) {\n this.isWaitingForNewUID = false;\n this.currentUID = uid;\n }\n if ((this.currentUID !== uid && isCurrentUserKnown) || this.currentDB !== db) {\n this.currentUID = uid;\n this.currentDB = db;\n if (this.websocket) {\n this.websocket.close(WEBSOCKET_CLOSE_CODES.CLEAN);\n }\n this.channelsByClient.forEach((_, key) => this.channelsByClient.set(key, []));\n }\n this.sendToClient(client, \"initialized\");\n }\n\n /**\n * Determine whether or not the websocket associated to this worker\n * is connected.\n *\n * @returns {boolean}\n */\n _isWebsocketConnected() {\n return this.websocket && this.websocket.readyState === 1;\n }\n\n /**\n * Determine whether or not the websocket associated to this worker\n * is connecting.\n *\n * @returns {boolean}\n */\n _isWebsocketConnecting() {\n return this.websocket && this.websocket.readyState === 0;\n }\n\n /**\n * Determine whether or not the websocket associated to this worker\n * is in the closing state.\n *\n * @returns {boolean}\n */\n _isWebsocketClosing() {\n return this.websocket && this.websocket.readyState === 2;\n }\n\n /**\n * Triggered when a connection is closed. If closure was not clean ,\n * try to reconnect after indicating to the clients that the\n * connection was closed.\n *\n * @param {CloseEvent} ev\n * @param {number} code close code indicating why the connection\n * was closed.\n * @param {string} reason reason indicating why the connection was\n * closed.\n */\n _onWebsocketClose({ code, reason }) {\n if (this.isDebug) {\n console.debug(\n `%c${new Date().toLocaleString()} - [onClose]`,\n \"color: #c6e; font-weight: bold;\",\n code,\n reason\n );\n }\n this.lastChannelSubscription = null;\n if (this.isReconnecting) {\n // Connection was not established but the close event was\n // triggered anyway. Let the onWebsocketError method handle\n // this case.\n return;\n }\n this.broadcast(\"disconnect\", { code, reason });\n if (code === WEBSOCKET_CLOSE_CODES.CLEAN) {\n // WebSocket was closed on purpose, do not try to reconnect.\n return;\n }\n // WebSocket was not closed cleanly, let's try to reconnect.\n this.broadcast(\"reconnecting\", { closeCode: code });\n this.isReconnecting = true;\n if (code === WEBSOCKET_CLOSE_CODES.KEEP_ALIVE_TIMEOUT) {\n // Don't wait to reconnect on keep alive timeout.\n this.connectRetryDelay = 0;\n }\n if (code === WEBSOCKET_CLOSE_CODES.SESSION_EXPIRED) {\n this.isWaitingForNewUID = true;\n }\n this._retryConnectionWithDelay();\n }\n\n /**\n * Triggered when a connection failed or failed to established.\n */\n _onWebsocketError() {\n if (this.isDebug) {\n console.debug(\n `%c${new Date().toLocaleString()} - [onError]`,\n \"color: #c6e; font-weight: bold;\"\n );\n }\n this._retryConnectionWithDelay();\n }\n\n /**\n * Handle data received from the bus.\n *\n * @param {MessageEvent} messageEv\n */\n _onWebsocketMessage(messageEv) {\n const notifications = JSON.parse(messageEv.data);\n if (this.isDebug) {\n console.debug(\n `%c${new Date().toLocaleString()} - [onMessage]`,\n \"color: #c6e; font-weight: bold;\",\n notifications\n );\n }\n this.lastNotificationId = notifications[notifications.length - 1].id;\n this.broadcast(\"notification\", notifications);\n }\n\n /**\n * Triggered on websocket open. Send message that were waiting for\n * the connection to open.\n */\n _onWebsocketOpen() {\n if (this.isDebug) {\n console.debug(\n `%c${new Date().toLocaleString()} - [onOpen]`,\n \"color: #c6e; font-weight: bold;\"\n );\n }\n this._updateChannels();\n this.messageWaitQueue.forEach((msg) => this.websocket.send(msg));\n this.messageWaitQueue = [];\n this.broadcast(this.isReconnecting ? \"reconnect\" : \"connect\");\n this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n this.connectTimeout = null;\n this.isReconnecting = false;\n }\n\n /**\n * Try to reconnect to the server, an exponential back off is\n * applied to the reconnect attempts.\n */\n _retryConnectionWithDelay() {\n this.connectRetryDelay =\n Math.min(this.connectRetryDelay * 1.5, MAXIMUM_RECONNECT_DELAY) +\n this.RECONNECT_JITTER * Math.random();\n this.connectTimeout = setTimeout(this._start.bind(this), this.connectRetryDelay);\n }\n\n /**\n * Send a message to the server through the websocket connection.\n * If the websocket is not open, enqueue the message and send it\n * upon the next reconnection.\n *\n * @param {{event_name: string, data: any }} message Message to send to the server.\n */\n _sendToServer(message) {\n const payload = JSON.stringify(message);\n if (!this._isWebsocketConnected()) {\n this.messageWaitQueue.push(payload);\n } else {\n this.websocket.send(payload);\n }\n }\n\n /**\n * Start the worker by opening a websocket connection.\n */\n _start() {\n if (this._isWebsocketConnected() || this._isWebsocketConnecting()) {\n return;\n }\n if (this.websocket) {\n this.websocket.removeEventListener(\"open\", this._onWebsocketOpen);\n this.websocket.removeEventListener(\"message\", this._onWebsocketMessage);\n this.websocket.removeEventListener(\"error\", this._onWebsocketError);\n this.websocket.removeEventListener(\"close\", this._onWebsocketClose);\n }\n if (this._isWebsocketClosing()) {\n // close event was not triggered and will never be, broadcast the\n // disconnect event for consistency sake.\n this.lastChannelSubscription = null;\n this.broadcast(\"disconnect\", { code: WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE });\n }\n this.websocket = new WebSocket(this.websocketURL);\n this.websocket.addEventListener(\"open\", this._onWebsocketOpen);\n this.websocket.addEventListener(\"error\", this._onWebsocketError);\n this.websocket.addEventListener(\"message\", this._onWebsocketMessage);\n this.websocket.addEventListener(\"close\", this._onWebsocketClose);\n }\n\n /**\n * Stop the worker.\n */\n _stop() {\n clearTimeout(this.connectTimeout);\n this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n this.isReconnecting = false;\n this.lastChannelSubscription = null;\n if (this.websocket) {\n this.websocket.close();\n }\n }\n\n /**\n * Update the channel subscription on the server. Ignore if the channels\n * did not change since the last subscription.\n *\n * @param {boolean} force Whether or not we should update the subscription\n * event if the channels haven't change since last subscription.\n */\n _updateChannels({ force = false } = {}) {\n const allTabsChannels = [\n ...new Set([].concat.apply([], [...this.channelsByClient.values()])),\n ].sort();\n const allTabsChannelsString = JSON.stringify(allTabsChannels);\n const shouldUpdateChannelSubscription =\n allTabsChannelsString !== this.lastChannelSubscription;\n if (force || shouldUpdateChannelSubscription) {\n this.lastChannelSubscription = allTabsChannelsString;\n this._sendToServer({\n event_name: \"subscribe\",\n data: { channels: allTabsChannels, last: this.lastNotificationId },\n });\n }\n }\n}\n", "/** @odoo-module **/\n/* eslint-env worker */\n/* eslint-disable no-restricted-globals */\n\nimport { WebsocketWorker } from \"./websocket_worker\";\n\n(function () {\n const websocketWorker = new WebsocketWorker();\n\n if (self.name.includes(\"shared\")) {\n // The script is running in a shared worker: let's register every\n // tab connection to the worker in order to relay notifications\n // coming from the websocket.\n onconnect = function (ev) {\n const currentClient = ev.ports[0];\n websocketWorker.registerClient(currentClient);\n };\n } else {\n // The script is running in a simple web worker.\n websocketWorker.registerClient(self);\n }\n})();\n", "/** @odoo-module **/\n\n/**\n * Returns a function, that, as long as it continues to be invoked, will not\n * be triggered. The function will be called after it stops being called for\n * N milliseconds. If `immediate` is passed, trigger the function on the\n * leading edge, instead of the trailing.\n *\n * Inspired by https://davidwalsh.name/javascript-debounce-function\n */\nexport function debounce(func, wait, immediate) {\n let timeout;\n return function () {\n const context = this;\n const args = arguments;\n function later() {\n timeout = null;\n if (!immediate) {\n func.apply(context, args);\n }\n }\n const callNow = immediate && !timeout;\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n if (callNow) {\n func.apply(context, args);\n }\n };\n}\n"], "file": "/web/assets/8dcae3b/bus.websocket_worker_assets.js", "sourceRoot": "../../../"}