UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

286 lines 11.9 kB
import { ClientReconnected, ConnectionLost } from "../core/bokeh_events"; import { logger } from "../core/logging"; import { Document } from "../document"; import { Message } from "../protocol/message"; import { Receiver } from "../protocol/receiver"; import { ClientSession } from "./session"; import { assert } from "../core/util/assert"; export const DEFAULT_SERVER_WEBSOCKET_URL = "ws://localhost:5006/ws"; export const DEFAULT_TOKEN = "eyJzZXNzaW9uX2lkIjogImRlZmF1bHQifQ"; const MAX_RECONNECTION_ATTEMPTS = 5; const RECONNECT_BASE_DELAY = 1000; let _connection_count = 0; export function parse_token(token) { let payload = token.split(".")[0]; const mod = payload.length % 4; if (mod != 0) { payload = payload + "=".repeat(4 - mod); } return JSON.parse(atob(payload.replace(/_/g, "/").replace(/-/g, "+"))); } // WebSocket close event is emitted before page is destroyed, resulting in an // unnecessary reconnect attempt and a UI notification just before page reloads. let _prevent_reconnect = false; addEventListener("beforeunload", () => { _prevent_reconnect = true; }); export class ClientConnection { url; token; args_string; static __name__ = "ClientConnection"; _number = _connection_count++; socket = null; session = null; closed_permanently = false; id; _reconnection_attempts_left = MAX_RECONNECTION_ATTEMPTS; get reconnection_attempts() { return MAX_RECONNECTION_ATTEMPTS - this._reconnection_attempts_left; } _current_handler = null; _pending_replies = new Map(); _pending_messages = []; _receiver = new Receiver(); constructor(url = DEFAULT_SERVER_WEBSOCKET_URL, token = DEFAULT_TOKEN, args_string = null) { this.url = url; this.token = token; this.args_string = args_string; this.id = parse_token(token).session_id.split(".")[0]; logger.debug(`Creating websocket ${this._number} to '${this.url}' session '${this.id}'`); } async reconnect() { this._try_reconnect(true); } async connect() { if (this.closed_permanently) { throw new Error("Cannot connect() a closed ClientConnection"); } if (this.socket != null) { throw new Error("Already connected"); } this._current_handler = null; this._pending_replies.clear(); this._pending_messages = []; try { let versioned_url = `${this.url}`; if (this.args_string != null && this.args_string.length > 0) { versioned_url += `?${this.args_string}`; } this.socket = new WebSocket(versioned_url, ["bokeh", this.token]); return new Promise((resolve, reject) => { assert(this.socket != null); // "arraybuffer" gives us binary data we can look at; // if we just needed an opaque blob we could use "blob" this.socket.binaryType = "arraybuffer"; this.socket.onopen = () => this._on_open(resolve, reject); this.socket.onmessage = (event) => this._on_message(event); this.socket.onclose = (event) => this._on_close(event, reject); this.socket.onerror = () => this._on_error(reject); }); } catch (error) { logger.error(`websocket creation failed to url: ${this.url}`); logger.error(` - ${error}`); throw error; } } close() { if (!this.closed_permanently) { logger.debug(`Permanently closing websocket connection ${this._number}`); this.closed_permanently = true; if (this.socket != null) { this.socket.close(1000, `close method called on ClientConnection ${this._number}`); } this.session._connection_closed(); } } _try_reconnect(force = false) { if (this.closed_permanently) { logger.info(`Websocket connection ${this._number} permanently disconnected, will not attempt to reconnect`); } else if (!force && this._reconnection_attempts_left <= 0) { logger.info(`Websocket connection ${this._number} disconnected, will not attempt to automatically reconnect`); } else { if (this.socket?.readyState !== WebSocket.OPEN && this.socket?.readyState !== WebSocket.CONNECTING) { this._reconnection_attempts_left -= 1; logger.debug(`Attempting to reconnect websocket ${this._number}, ${this._reconnection_attempts_left} attempts left`); this.connect().then(() => { logger.info(`Reconnected websocket ${this._number}`); this._reconnection_attempts_left = MAX_RECONNECTION_ATTEMPTS; this.session?.document.event_manager.send_event(new ClientReconnected()); }).catch(err => { logger.debug(`Could not reconnect ${this._number}, ${err}`); }); } } } _schedule_reconnect(milliseconds) { if (this.session == null) { return; } const { document } = this.session; const should_reconnect = document.config.reconnect_session && this._reconnection_attempts_left > 0; const timeout = should_reconnect ? milliseconds : null; const event = new ConnectionLost(new WeakRef(this), this.reconnection_attempts, timeout); document.event_manager.send_event(event); if (should_reconnect) { setTimeout(() => this._try_reconnect(), milliseconds); } } send(message) { if (this.socket != null) { message.send(this.socket); } else { logger.error("not connected so cannot send", message); } } async send_with_reply(message) { const reply = await new Promise((resolve, reject) => { this._pending_replies.set(message.msgid(), { resolve, reject }); this.send(message); }); if (reply.msgtype() == "ERROR") { throw new Error(`Error reply ${reply.content.text}`); } else { return reply; } } async _pull_doc_json() { const message = Message.create("PULL-DOC-REQ", {}, {}); const reply = await this.send_with_reply(message); if (!("doc" in reply.content)) { throw new Error("No 'doc' field in PULL-DOC-REPLY"); } return { doc_json: reply.content.doc, buffers: reply.buffers }; } async _repull_session_doc(resolve, reject) { logger.debug(this.session != null ? "Repulling session" : "Pulling session for first time"); try { const { doc_json, buffers } = await this._pull_doc_json(); if (this.session == null) { if (this.closed_permanently) { logger.debug("Got new document after connection was already closed"); reject(new Error("The connection has been closed")); } else { const events = []; const document = Document.from_json(doc_json, events, buffers); this.session = new ClientSession(this, document); // Send back change events that happened during model initialization. for (const event of events) { document._trigger_on_change(event); } for (const msg of this._pending_messages) { this.session.handle(msg); } this._pending_messages = []; logger.debug("Created a new session from new pulled doc"); resolve(this.session); } } else { this.session.document.replace_with_json(doc_json); logger.debug("Updated existing session with new pulled doc"); resolve(this.session); } } catch (error) { console.trace(error); logger.error(`Failed to repull session ${error}`); reject(error instanceof Error ? error : `${error}`); } } _on_open(resolve, reject) { logger.info(`Websocket connection ${this._number} is now open`); this._current_handler = (message) => { this._awaiting_ack_handler(message, resolve, reject); }; } _on_message(event) { if (this._current_handler == null) { logger.error("Got a message with no current handler set"); } try { this._receiver.consume(event.data); } catch (e) { this._close_bad_protocol(`${e}`); } const msg = this._receiver.message; if (msg != null) { const problem = msg.problem(); if (problem != null) { this._close_bad_protocol(problem); } this._current_handler(msg); } } /** * The reconnect delay exponentially increases after each attempt. The * first attempt is done immediately. */ _reconnect_delay() { const retries = MAX_RECONNECTION_ATTEMPTS - this._reconnection_attempts_left; return retries == 0 ? 0 : RECONNECT_BASE_DELAY * 2 ** retries; } _on_close(event, reject) { logger.info(`Lost websocket ${this._number} connection, ${event.code} (${event.reason})`); this.socket = null; this._pending_replies.forEach((pr) => pr.reject("Disconnected")); this._pending_replies.clear(); if (!this.closed_permanently && !_prevent_reconnect) { logger.debug(`Pending schedule_reconnect for ${this._number}`); this._schedule_reconnect(this._reconnect_delay()); } reject(new Error(`Lost websocket connection, ${event.code} (${event.reason})`)); } _on_error(reject) { logger.debug(`Websocket error on socket ${this._number}`); const msg = "Could not open websocket"; logger.error(`Failed to connect to Bokeh server: ${msg}`); reject(new Error(msg)); } _close_bad_protocol(detail) { logger.error(`Closing connection: ${detail}`); if (this.socket != null) { this.socket.close(1002, detail); } // 1002 = protocol error } _awaiting_ack_handler(message, resolve, reject) { if (message.msgtype() === "ACK") { this._current_handler = (message) => this._steady_state_handler(message); // Reload any sessions void this._repull_session_doc(resolve, reject); } else { this._close_bad_protocol("First message was not an ACK"); } } _steady_state_handler(message) { const reqid = message.reqid(); const pr = this._pending_replies.get(reqid); if (pr != null) { this._pending_replies.delete(reqid); pr.resolve(message); } else if (this.session != null) { this.session.handle(message); } else if (message.msgtype() != "PATCH-DOC") { // This branch can be executed only before we get the document. // When we get the document, all of the patches will already be incorporated. // In general, it's not possible to apply patches received before the document, // since they may change some models that were removed before serving the document. this._pending_messages.push(message); } } } export function pull_session(url, token, args_string) { const connection = new ClientConnection(url, token, args_string); return connection.connect(); } //# sourceMappingURL=connection.js.map