mirror of
https://github.com/vale981/emacs-jupyter
synced 2025-03-05 07:41:37 -05:00
342 lines
12 KiB
JavaScript
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);
|
|
});
|
|
}
|
|
}
|