)]}' {"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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACpPA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvgBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "sourcesContent": ["// @odoo-module ignore\n\n//-----------------------------------------------------------------------------\n// Odoo Web Boostrap Code\n//-----------------------------------------------------------------------------\n\n(function (odoo) {\n \"use strict\";\n\n if (odoo.loader) {\n // Allows for duplicate calls to `module_loader`: only the first one is\n // executed.\n return;\n }\n\n class ModuleLoader {\n /** @type {OdooModuleLoader[\"bus\"]} */\n bus = new EventTarget();\n /** @type {OdooModuleLoader[\"checkErrorProm\"]} */\n checkErrorProm = null;\n /** @type {OdooModuleLoader[\"factories\"]} */\n factories = new Map();\n /** @type {OdooModuleLoader[\"failed\"]} */\n failed = new Set();\n /** @type {OdooModuleLoader[\"jobs\"]} */\n jobs = new Set();\n /** @type {OdooModuleLoader[\"modules\"]} */\n modules = new Map();\n\n /**\n * @param {HTMLElement} [root]\n */\n constructor(root) {\n this.root = root;\n }\n\n /** @type {OdooModuleLoader[\"addJob\"]} */\n addJob(name) {\n this.jobs.add(name);\n this.startModules();\n }\n\n /** @type {OdooModuleLoader[\"define\"]} */\n define(name, deps, factory, lazy = false) {\n if (typeof name !== \"string\") {\n throw new Error(`Module name should be a string, got: ${String(name)}`);\n }\n if (!Array.isArray(deps)) {\n throw new Error(\n `Module dependencies should be a list of strings, got: ${String(deps)}`\n );\n }\n if (typeof factory !== \"function\") {\n throw new Error(`Module factory should be a function, got: ${String(factory)}`);\n }\n if (this.factories.has(name)) {\n return; // Ignore duplicate modules\n }\n this.factories.set(name, {\n deps,\n fn: factory,\n ignoreMissingDeps: globalThis.__odooIgnoreMissingDependencies,\n });\n if (!lazy) {\n this.addJob(name);\n this.checkErrorProm ||= Promise.resolve().then(() => {\n this.checkErrorProm = null;\n this.reportErrors(this.findErrors());\n });\n }\n }\n\n /** @type {OdooModuleLoader[\"findErrors\"]} */\n findErrors(moduleNames) {\n /**\n * @param {Iterable} currentModuleNames\n * @param {Set} visited\n * @returns {string | null}\n */\n const findCycle = (currentModuleNames, visited) => {\n for (const name of currentModuleNames || []) {\n if (visited.has(name)) {\n const cycleModuleNames = [...visited, name];\n return cycleModuleNames\n .slice(cycleModuleNames.indexOf(name))\n .map((j) => `\"${j}\"`)\n .join(\" => \");\n }\n const cycle = findCycle(dependencyGraph[name], new Set(visited).add(name));\n if (cycle) {\n return cycle;\n }\n }\n return null;\n };\n\n moduleNames ||= this.jobs;\n\n /** @type {Record>} */\n const dependencyGraph = Object.create(null);\n /** @type {Set} */\n const missing = new Set();\n /** @type {Set} */\n const unloaded = new Set();\n\n for (const moduleName of moduleNames) {\n const { deps, ignoreMissingDeps } = this.factories.get(moduleName);\n\n dependencyGraph[moduleName] = deps;\n\n if (ignoreMissingDeps) {\n continue;\n }\n\n unloaded.add(moduleName);\n for (const dep of deps) {\n if (!this.factories.has(dep)) {\n missing.add(dep);\n }\n }\n }\n\n const cycle = findCycle(moduleNames, new Set());\n const errors = {};\n if (cycle) {\n errors.cycle = cycle;\n }\n if (this.failed.size) {\n errors.failed = this.failed;\n }\n if (missing.size) {\n errors.missing = missing;\n }\n if (unloaded.size) {\n errors.unloaded = unloaded;\n }\n return errors;\n }\n\n /** @type {OdooModuleLoader[\"findJob\"]} */\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 /** @type {OdooModuleLoader[\"reportErrors\"]} */\n async reportErrors(errors) {\n if (!Object.keys(errors).length) {\n return;\n }\n\n if (errors.failed) {\n console.error(\"The following modules failed to load because of an error:\", [\n ...errors.failed,\n ]);\n }\n if (errors.missing) {\n console.error(\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 [...errors.missing]\n );\n }\n if (errors.cycle) {\n console.error(\n \"The following modules could not be loaded because they form a dependency cycle:\",\n errors.cycle\n );\n }\n if (errors.unloaded) {\n console.error(\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 [...errors.unloaded]\n );\n }\n\n const document = this.root?.ownerDocument || globalThis.document;\n if (document.readyState === \"loading\") {\n await new Promise((resolve) =>\n document.addEventListener(\"DOMContentLoaded\", resolve)\n );\n }\n\n const style = document.createElement(\"style\");\n style.className = \"o_module_error_banner\";\n style.textContent = `\n body::before {\n font-weight: bold;\n content: \"An error occurred while loading javascript modules, you may find more information in the devtools console\";\n position: fixed;\n left: 0;\n bottom: 0;\n z-index: 100000000000;\n background-color: #C00;\n color: #DDD;\n }\n `;\n document.head.appendChild(style);\n }\n\n /** @type {OdooModuleLoader[\"startModules\"]} */\n startModules() {\n let job;\n while ((job = this.findJob())) {\n this.startModule(job);\n }\n }\n\n /** @type {OdooModuleLoader[\"startModule\"]} */\n startModule(name) {\n /** @type {(dependency: string) => OdooModule} */\n const require = (dependency) => this.modules.get(dependency);\n this.jobs.delete(name);\n const factory = this.factories.get(name);\n /** @type {OdooModule | null} */\n let module = null;\n try {\n module = 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, module);\n this.bus.dispatchEvent(\n new CustomEvent(\"module-started\", {\n detail: { moduleName: name, module },\n })\n );\n return module;\n }\n }\n\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 odoo.loader = loader;\n})((globalThis.odoo ||= {}));\n", "/** @odoo-module **/\n\nimport { debounce, Deferred } 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' | 'outdated'| 'worker_state_updated' | 'log_debug' } 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});\nexport const WORKER_STATE = Object.freeze({\n CONNECTED: \"CONNECTED\",\n DISCONNECTED: \"DISCONNECTED\",\n IDLE: \"IDLE\",\n CONNECTING: \"CONNECTING\",\n});\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.active = true;\n this.state = WORKER_STATE.IDLE;\n this.isReconnecting = false;\n this.lastChannelSubscription = null;\n this.firstSubscribeDeferred = new Deferred();\n this.lastNotificationId = 0;\n this.messageWaitQueue = [];\n this._forceUpdateChannels = debounce(this._forceUpdateChannels, 300);\n this._debouncedUpdateChannels = debounce(this._updateChannels, 300);\n this._debouncedSendToServer = debounce(this._sendToServer, 300);\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 this._logDebug(\"broadcast\", type, data);\n for (const client of this.channelsByClient.keys()) {\n client.postMessage({ type, data: data ? JSON.parse(JSON.stringify(data)) : undefined });\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 this._logDebug(\"sendToClient\", type, data);\n client.postMessage({ type, data: data ? JSON.parse(JSON.stringify(data)) : undefined });\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 this._logDebug(\"_onClientMessage\", action, data);\n switch (action) {\n case \"send\": {\n if (data[\"event_name\"] === \"update_presence\") {\n this._debouncedSendToServer(data);\n } else {\n this._sendToServer(data);\n }\n return;\n }\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._debouncedUpdateChannels();\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._debouncedUpdateChannels();\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 = [...this.debugModeByClient.values()].some(Boolean);\n this._debouncedUpdateChannels();\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.set(client, debug);\n this.isDebug = [...this.debugModeByClient.values()].some(Boolean);\n this.sendToClient(client, \"update_state\", this.state);\n this.sendToClient(client, \"initialized\");\n return;\n }\n this.newestStartTs = startTs;\n this.websocketURL = websocketURL;\n this.lastNotificationId = lastNotificationId;\n this.debugModeByClient.set(client, debug);\n this.isDebug = [...this.debugModeByClient.values()].some(Boolean);\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, \"update_state\", this.state);\n this.sendToClient(client, \"initialized\");\n if (!this.active) {\n this.sendToClient(client, \"outdated\");\n }\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 this._logDebug(\"_onWebsocketClose\", code, reason);\n this._updateState(WORKER_STATE.DISCONNECTED);\n this.lastChannelSubscription = null;\n this.firstSubscribeDeferred = new Deferred();\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 if (reason === \"OUTDATED_VERSION\") {\n console.warn(\"Worker deactivated due to an outdated version.\");\n this.active = false;\n this.broadcast(\"outdated\");\n }\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 this._logDebug(\"_onWebsocketError\");\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 this._logDebug(\"_onWebsocketMessage\", notifications);\n this.lastNotificationId = notifications[notifications.length - 1].id;\n this.broadcast(\"notification\", notifications);\n }\n\n _logDebug(title, ...args) {\n const clientsInDebug = [...this.debugModeByClient.keys()].filter((client) =>\n this.debugModeByClient.get(client)\n );\n for (const client of clientsInDebug) {\n client.postMessage({\n type: \"log_debug\",\n data: [\n `%c${new Date().toLocaleString()} - [${title}]`,\n \"color: #c6e; font-weight: bold;\",\n ...args,\n ],\n });\n }\n }\n\n /**\n * Triggered on websocket open. Send message that were waiting for\n * the connection to open.\n */\n _onWebsocketOpen() {\n this._logDebug(\"_onWebsocketOpen\");\n this._updateState(WORKER_STATE.CONNECTED);\n this.broadcast(this.isReconnecting ? \"reconnect\" : \"connect\");\n this._debouncedUpdateChannels();\n this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n this.connectTimeout = null;\n this.isReconnecting = false;\n this.firstSubscribeDeferred.then(() => {\n this.messageWaitQueue.forEach((msg) => this.websocket.send(msg));\n this.messageWaitQueue = [];\n });\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._logDebug(\"_retryConnectionWithDelay\", this.connectRetryDelay);\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 this._logDebug(\"_sendToServer\", message);\n const payload = JSON.stringify(message);\n if (!this._isWebsocketConnected()) {\n if (message[\"event_name\"] === \"subscribe\") {\n this.messageWaitQueue = this.messageWaitQueue.filter(\n (msg) => JSON.parse(msg).event_name !== \"subscribe\"\n );\n this.messageWaitQueue.unshift(payload);\n } else {\n this.messageWaitQueue.push(payload);\n }\n } else {\n if (message[\"event_name\"] === \"subscribe\") {\n this.websocket.send(payload);\n } else {\n this.firstSubscribeDeferred.then(() => this.websocket.send(payload));\n }\n }\n }\n\n _removeWebsocketListeners() {\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\n /**\n * Start the worker by opening a websocket connection.\n */\n _start() {\n this._logDebug(\"_start\");\n if (!this.active || this._isWebsocketConnected() || this._isWebsocketConnecting()) {\n return;\n }\n this._removeWebsocketListeners();\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._updateState(WORKER_STATE.CONNECTING);\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 this._logDebug(\"_stop\");\n clearTimeout(this.connectTimeout);\n this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n this.isReconnecting = false;\n this.lastChannelSubscription = null;\n this.websocket?.close();\n this._removeWebsocketListeners();\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 this.firstSubscribeDeferred.resolve();\n }\n }\n /**\n * Update the worker state and broadcast the new state to its clients.\n *\n * @param {WORKER_STATE[keyof WORKER_STATE]} newState\n */\n _updateState(newState) {\n this.state = newState;\n this.broadcast(\"worker_state_updated\", newState);\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\n/**\n * Deferred is basically a resolvable/rejectable extension of Promise.\n */\nexport class Deferred extends Promise {\n constructor() {\n let resolve;\n let reject;\n const prom = new Promise((res, rej) => {\n resolve = res;\n reject = rej;\n });\n return Object.assign(prom, { resolve, reject });\n }\n}\n"], "file": "/web/assets/55b7a9a/bus.websocket_worker_assets.js", "sourceRoot": "../../../"}