// NOTE: Info on widgets http://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Low%20Level.html var disposable = require('@phosphor/disposable'); var coreutils = require('@jupyterlab/coreutils'); // The KernelFutureHandler allows comms to register their callbacks to be // called when messages are received in response to a request sent to the // kernel. var KernelFutureHandler = require('@jupyterlab/services/kernel/future').KernelFutureHandler; // The CommHandler object handles comm interaction to/from the kernel. It takes // a target_name, usually jupyter.widget, and a comm_id. It takes care of // sending comm messages to the kernel and calls the callback methods of a Comm // when a comm_msg is received from the kernel. // // A Comm object is essentially a wrapper around a CommHandler that updates the // CommHandler callbacks and registers callbacks on the futures created when a // Comm sends a message on the shell channel. var CommHandler = require('@jupyterlab/services/kernel/comm').CommHandler; // A CommManager takes care of registering new comm targets and creating new // comms and holding a list of all the live comms. // It looks like I just ned to implement the IKernel interface and pass the // object that implements it to CommManager, this way I can create new comms // with CommManager.new_comm when handling comm_open messages. In the IKernel // interface, I'll just redirect all the message sending functions to Emacs. // It looks like widgets send messages through the callbacks of a // KernelFutureHandler so I will have to redirect all received messages that // originated from a request generated by skewer.postJSON back to the // JavaScript environment. Emacs then acts as an intermediary, capturing kernel // messages and re-packaging them to send to the Javascript environment. // // It looks like whenever the kernel receives a message it accesse the correct // future object using this.futures.get and calls handleMsg function of the // future. // // The flow of message with respect to Comm objects is that Comm object send // shell messages, then widgets register callbacks on the future. var EmacsJupyter = function(options, port) { var _this = this; this.username = options.username || ''; // This is the Jupyter session id this.clientId = options.clientId; this.isDisposed = false; // A mapping from comm_id's to promises that resolve to their open Comm // objects. this.commPromises = new Map(); // The targetRegistry is a dictionary mapping target names to target // functions that are called whenever a new Comm is requested to be open by // the kernel. The target function gets called with the initial comm_open // message data and a comm handler for the new Comm. this.targetRegistry = {}; // A mapping of msg_id's for messages sent to the kernel and their // KernelFutureHandler objects. this.futures = new Map(); // The WidgetManager that connects comms to their corresponding widget // models, construct widget views, load widget modules, and get the current // widget state. this.widgetManager = null; this.widgetState = null; // The CommManager that registers the target names and their target // functions handles opening and closing comms for a particular // target name. this.commManager = null; this.messagePromise = new Promise(function (resolve) { resolve(); }); window.addEventListener("unload", function(event) { // TODO: Send widget state }); // Localhost this.wsPort = port; this.ws = new WebSocket("ws://127.0.0.1:" + port); this.ws.onopen = function () { // Ensure that Emacs knows which websocket connection corresponds to // each kernel client _this.ws.send(JSON.stringify({ header: { msg_type: "connect", session: _this.clientId } })); }; this.ws.onmessage = function(event) { if(_this.isDisposed) { return; } var msg = JSON.parse(event.data); _this.messagePromise = _this.messagePromise.then(function () { if(msg.buffers && msg.buffers.length > 0) { for(var i = 0; i < msg.buffers.length; i++) { var bin = atob(msg.buffers[i]); var len = bin.length; var buf = new Uint8Array(len); for(var j = 0; j < len; j++) { buf[j] = bin.charCodeAt(j); } msg.buffers[i] = buf.buffer; } } _this.handleMessage(msg); }); }; }; exports.EmacsJupyter = EmacsJupyter; EmacsJupyter.prototype.dispose = function () { if (this.isDisposed) { return; } this.isDisposed = true; this.commPromises.forEach(function (promise, key) { promise.then(function (comm) { comm.dispose(); }); }); }; EmacsJupyter.prototype.registerCommTarget = function(targetName, callback) { var _this = this; this.targetRegistry[targetName] = callback; return new disposable.DisposableDelegate(function () { if (!_this.isDisposed) { delete _this.targetRegistry[targetName]; } }); }; EmacsJupyter.prototype.connectToComm = function (targetName, commId) { var _this = this; var id = commId || coreutils.uuid(); if (this.commPromises.has(id)) { return this.commPromises.get(id); } var promise = Promise.resolve(void 0).then(function () { return new CommHandler(targetName, id, _this, function () { _this._unregisterComm(id); }); }); this.commPromises.set(id, promise); return promise; }; EmacsJupyter.prototype.handleCommOpen = function (msg) { var _this = this; var content = msg.content; if (this.isDisposed) { return; } var promise = this.loadObject(content.target_name, content.target_module, this.targetRegistry) .then(function (target) { var comm = new CommHandler(content.target_name, content.comm_id, _this, function () { _this._unregisterComm(content.comm_id); }); var response; try { response = target(comm, msg); } catch (e) { comm.close(); console.error('Exception opening new comm'); throw (e); } return Promise.resolve(response).then(function () { if (_this.isDisposed) { return null; } return comm; }); }); this.commPromises.set(content.comm_id, promise); }; EmacsJupyter.prototype.handleCommClose = function (msg) { var _this = this; var content = msg.content; var promise = this.commPromises.get(content.comm_id); if (!promise) { console.error('Comm not found for comm id ' + content.comm_id); return; } promise.then(function (comm) { if (!comm) { return; } _this._unregisterComm(comm.commId); try { var onClose = comm.onClose; if (onClose) { onClose(msg); } comm.dispose(); } catch (e) { console.error('Exception closing comm: ', e, e.stack, msg); } }); }; EmacsJupyter.prototype.handleCommMsg = function (msg) { var promise = this.commPromises.get(msg.content.comm_id); if (!promise) { // We do have a registered comm for this comm id, ignore. return; } promise.then(function (comm) { if (!comm) { return; } try { var onMsg = comm.onMsg; if (onMsg) { onMsg(msg); } } catch (e) { console.error('Exception handling comm msg: ', e, e.stack, msg); } }); }; EmacsJupyter.prototype.loadObject = function(name, moduleName, registry) { return new Promise(function (resolve, reject) { // Try loading the view module using require.js if (moduleName) { if (typeof window.require === 'undefined') { throw new Error('requirejs not found'); } window.require([moduleName], function (mod) { if (mod[name] === void 0) { var msg = "Object '" + name + "' not found in module '" + moduleName + "'"; reject(new Error(msg)); } else { resolve(mod[name]); } }, reject); } else { if (registry && registry[name]) { resolve(registry[name]); } else { reject(new Error("Object '" + name + "' not found in registry")); } } }); } EmacsJupyter.prototype._unregisterComm = function (commId) { this.commPromises.delete(commId); }; EmacsJupyter.prototype.sendShellMessage = function(msg, expectReply, disposeOnDone) { var _this = this; if (expectReply === void 0) { expectReply = false; } if (disposeOnDone === void 0) { disposeOnDone = true; } var future = new KernelFutureHandler(function () { var msgId = msg.header.msg_id; _this.futures.delete(msgId); }, msg, expectReply, disposeOnDone, this); this.ws.send(JSON.stringify(msg)); this.futures.set(msg.header.msg_id, future); return future; }; EmacsJupyter.prototype.requestCommInfo = function(targetName) { var msg = { channel: 'shell', msg_type: 'comm_info_request', // A message ID will be added by Emacs anyway header: {msg_id: ''}, content: {target_name: targetName} }; var future = this.sendShellMessage(msg, true); return new Promise(function (resolve) { future.onReply = resolve; }); }; EmacsJupyter.prototype.handleMessage = function(msg) { var _this = this; var parentHeader = msg.parent_header; var future = parentHeader && this.futures && this.futures.get(parentHeader.msg_id); if (future) { return new Promise(function (resolve, reject) { try { future.handleMsg(msg); resolve(msg); } catch(err) { reject(err); } }); } else { return new Promise(function (resolve, reject) { switch(msg.msg_type) { // Special messages not really a Jupyter message case 'display_model': _this.widgetManager.get_model(msg.content.model_id).then(function (model) { _this.widgetManager.display_model(undefined, model); }); break; case 'clear_display': var widget = _this.widgetManager.area; while(widget.firstChild) { widget.removeChild(widget.firstChild); } break; // Regular Jupyter messages case 'comm_open': _this.handleCommOpen(msg); // Periodically get the state of the widgetManager, this gets // sent to the browser when its unloaded. // _this.widgetManager.get_state({}).then(function (state) { // _this.widgetState = state; // }); break; case 'comm_close': _this.handleCommClose(msg); break; case 'comm_msg': _this.handleCommMsg(msg); break; case 'status': // Comes from the comm info messages break; default: reject(new Error('Unhandled message', msg)); }; resolve(msg); }); } }