emacs-jupyter/js/emacs-jupyter.js
2018-08-30 18:12:18 -05:00

342 lines
12 KiB
JavaScript

// 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);
});
}
}