thebe-core
Version:
Typescript based core functionality for Thebe
520 lines • 25.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const url_1 = require("./url");
const sessions_1 = require("./sessions");
const services_1 = require("@jupyterlab/services");
const session_1 = tslib_1.__importDefault(require("./session"));
const utils_1 = require("./utils");
const events_1 = require("./events");
const emitter_1 = require("./emitter");
function responseToJson(res) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
if (!res.ok)
throw Error(`${res.status} - ${res.statusText}`);
const json = yield res.json();
return json;
});
}
function errorAsString(errorLike) {
if (typeof errorLike === 'string')
return errorLike;
if (errorLike.message)
return errorLike.message;
if (errorLike.status && errorLike.statusText)
return `${errorLike.status} - ${errorLike.statusText}`;
return JSON.stringify(errorLike);
}
class ThebeServer {
constructor(config) {
this.id = (0, utils_1.shortId)();
this.config = config;
this.events = new emitter_1.EventEmitter(this.id, config, events_1.EventSubject.server, this);
this.ready = new Promise((resolve, reject) => {
this.resolveReadyFn = resolve;
this.rejectReadyFn = reject;
});
this._isDisposed = false;
}
get isBinder() {
return !!this.binderUrls;
}
get isReady() {
var _a, _b;
return (_b = (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.isReady) !== null && _b !== void 0 ? _b : false;
}
get isDisposed() {
return this._isDisposed;
}
get settings() {
var _a;
return (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.serverSettings;
}
shutdownSession(id) {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
return (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.shutdown(id);
});
}
shutdownAllSessions() {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
return (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.shutdownAll();
});
}
check() {
var _a, _b;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const resp = yield ThebeServer.status((_b = (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.serverSettings) !== null && _b !== void 0 ? _b : this.config.serverSettings);
return resp.ok;
});
}
dispose() {
var _a, _b, _c, _d;
if (this._isDisposed)
return;
if (!((_a = this.serviceManager) === null || _a === void 0 ? void 0 : _a.isDisposed))
(_b = this.serviceManager) === null || _b === void 0 ? void 0 : _b.dispose();
if (!((_c = this.sessionManager) === null || _c === void 0 ? void 0 : _c.isDisposed))
(_d = this.sessionManager) === null || _d === void 0 ? void 0 : _d.dispose();
// Implementing the flag at this level as
this._isDisposed = true;
}
startNewSession(rendermime, kernelOptions) {
var _a, _b, _c;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
yield this.ready;
if (!this.sessionManager) {
throw Error('Requesting session from a server, with no SessionManager available');
}
yield this.sessionManager.ready;
// name is assumed to be a non empty string but is otherwise not required
// if a notebook name has been supplied on the path, use that otherwise use a default
// https://jupyterlab.readthedocs.io/en/3.4.x/api/modules/services.session.html#isessionoptions
let path = (_a = kernelOptions === null || kernelOptions === void 0 ? void 0 : kernelOptions.path) !== null && _a !== void 0 ? _a : this.config.kernels.path;
let name = 'thebe.ipynb';
const match = path.match(/\/*([a-zA-Z0-9-]+.ipynb)$/);
if (match) {
name = match[1];
}
const kernelName = (_b = kernelOptions === null || kernelOptions === void 0 ? void 0 : kernelOptions.kernelName) !== null && _b !== void 0 ? _b : this.config.kernels.kernelName;
console.debug('thebe:api:startNewSession server', this);
console.debug('thebe:api:startNewSession', { name, path, kernelName });
if (this.serviceManager) {
// Temporary Fix: thebe-lite does not yet support filesystem based resouces fully,
// so we can't use a path that points to a sub folder.
path = path.slice(1).replace(/\//g, '-');
}
const connection = yield ((_c = this.sessionManager) === null || _c === void 0 ? void 0 : _c.startNew({
name,
path,
type: 'notebook',
kernel: {
name: kernelName,
},
}));
return new session_1.default(this, connection, rendermime);
});
}
listRunningSessions() {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
yield this.ready;
const iter = (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.running();
const models = [];
let result = iter === null || iter === void 0 ? void 0 : iter.next();
while (result && !result.done) {
models.push(result.value);
result = iter === null || iter === void 0 ? void 0 : iter.next();
}
return models;
});
}
refreshRunningSessions() {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
yield this.ready;
yield ((_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.refreshRunning());
return this.listRunningSessions();
});
}
connectToExistingSession(model, rendermime) {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
yield this.ready;
if (!this.sessionManager) {
throw Error('Requesting session from a server, with no SessionManager available');
}
yield this.sessionManager.ready;
const connection = (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.connectTo({ model });
return new session_1.default(this, connection, rendermime);
});
}
clearSavedBinderSessions() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const urls = this.makeBinderUrls();
window.localStorage.removeItem(urls.storageKey);
});
}
/**
* Connect to a Jupyter server directly
*
*/
connectToJupyterServer() {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
console.debug('thebe:api:connectToJupyterServer:serverSettings:', this.config.serverSettings);
const serverSettings = services_1.ServerConnection.makeSettings(this.config.serverSettings);
// ping the server to check it is alive before trying to
// hook up services
try {
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Checking server url`,
});
yield ThebeServer.status(serverSettings);
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Server reachable`,
});
// eslint-disable-next-line no-empty
}
catch (err) {
const message = `Server not reachable (${serverSettings.baseUrl}) - ${err}`;
this.events.triggerError({
status: events_1.ErrorStatusEvent.error,
message,
});
(_a = this.rejectReadyFn) === null || _a === void 0 ? void 0 : _a.call(this, message);
return;
}
const kernelManager = new services_1.KernelManager({ serverSettings });
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Created KernelManager`,
});
this.sessionManager = new services_1.SessionManager({
kernelManager,
serverSettings,
});
this.sessionManager.connectionFailure.connect((_, err) => {
this.events.triggerError({
status: events_1.ErrorStatusEvent.server,
message: `connection failure: ${err}`,
});
});
this.sessionManager.runningChanged.connect((_, models) => {
this.events.triggerStatus({
status: events_1.ServerStatusEvent.ready,
message: `${models.length} running sessions changed: ${models
.map((m) => m.name)
.join(',')}`,
});
});
this.events.triggerStatus({
status: events_1.ServerStatusEvent.ready,
message: `Created SessionManager`,
});
// Resolve the ready promise
return this.sessionManager.ready.then(() => {
var _a;
this.userServerUrl = `${serverSettings.baseUrl}?token=${serverSettings.token}`;
this.events.triggerStatus({
status: events_1.ServerStatusEvent.ready,
message: `Server connection ready`,
});
(_a = this.resolveReadyFn) === null || _a === void 0 ? void 0 : _a.call(this, this);
}, (err) => { var _a; return (_a = this.rejectReadyFn) === null || _a === void 0 ? void 0 : _a.call(this, errorAsString(err)); });
});
}
/**
* Connect to Jupyterlite Server
*/
connectToJupyterLiteServer(config) {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Connecting to JupyterLite`,
});
if (!window.thebeLite)
throw new Error(`thebe-lite is not available at window.thebeLite - load this onto your page before loading thebe or thebe-core.`);
this.serviceManager = yield window.thebeLite.startJupyterLiteServer(config);
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Started JupyterLite server`,
});
console.debug('thebe:api:connectToJupyterLiteServer:serverSettings:', this.serviceManager.serverSettings);
this.sessionManager = this.serviceManager.sessions;
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Received SessionMananger from JupyterLite`,
});
return (_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.ready.then(() => {
var _a;
this.userServerUrl = '/'; // TODO bundle jlite UI `${this.serviceManager?.serverSettings.baseUrl}?token=${this.serviceManager?.serverSettings.token}`;
this.events.triggerStatus({
status: events_1.ServerStatusEvent.ready,
message: `Server connection established`,
});
(_a = this.resolveReadyFn) === null || _a === void 0 ? void 0 : _a.call(this, this);
}, (err) => { var _a; return (_a = this.rejectReadyFn) === null || _a === void 0 ? void 0 : _a.call(this, errorAsString(err)); });
});
}
makeBinderUrls() {
var _a;
return (0, url_1.makeBinderUrls)(this.config, (_a = this.repoProviders) !== null && _a !== void 0 ? _a : url_1.WELL_KNOWN_REPO_PROVIDERS);
}
checkForSavedBinderSession() {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
const { storageKey } = (0, url_1.makeBinderUrls)(this.config, (_a = this.repoProviders) !== null && _a !== void 0 ? _a : url_1.WELL_KNOWN_REPO_PROVIDERS);
return (0, sessions_1.getExistingServer)(this.config.savedSessions, storageKey);
}
catch (err) {
this.events.triggerError({
status: events_1.ErrorStatusEvent.error,
message: `${err} - Failed to check for saved session.`,
});
return null;
}
});
}
/**
* Connect to a Binder instance in order to
* access a Jupyter server that can provide kernels
*
* @param ctx
* @param opts
* @returns
*/
connectToServerViaBinder(customProviders) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
// request new server
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Connecting to binderhub at ${this.config.binder.binderUrl}`,
});
// TODO binder connection setup probably better as a a factory independent of the server
this.repoProviders = [...url_1.WELL_KNOWN_REPO_PROVIDERS, ...(customProviders !== null && customProviders !== void 0 ? customProviders : [])];
try {
this.binderUrls = (0, url_1.makeBinderUrls)(this.config, this.repoProviders);
}
catch (err) {
this.events.triggerError({
status: events_1.ErrorStatusEvent.error,
message: `${err} - Failed to connect to binderhub at ${this.config.binder.binderUrl}`,
});
return;
}
const urls = this.binderUrls;
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Binder build url is ${urls.build}`,
});
if (this.config.savedSessions.enabled) {
console.debug('thebe:server:connectToServerViaBinder Checking for saved session...');
// the follow function will ping the server based on the settings and only return
// non-null if the server is still alive. So highly likely that the remainder of
// the connection calls below, work.
const existingSettings = yield this.checkForSavedBinderSession();
if (existingSettings) {
// Connect to the existing session
const serverSettings = services_1.ServerConnection.makeSettings(existingSettings);
const kernelManager = new services_1.KernelManager({ serverSettings });
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Created KernelManager`,
});
this.sessionManager = new services_1.SessionManager({
kernelManager,
serverSettings,
});
this.events.triggerStatus({
status: events_1.ServerStatusEvent.launching,
message: `Created KernelManager`,
});
return this.sessionManager.ready.then(() => {
var _a;
this.userServerUrl = `${serverSettings.baseUrl}?token=${serverSettings.token}`;
this.events.triggerStatus({
status: events_1.ServerStatusEvent.ready,
message: `Re-connected to binder server`,
});
(_a = this.resolveReadyFn) === null || _a === void 0 ? void 0 : _a.call(this, this);
}, (err) => { var _a; return (_a = this.rejectReadyFn) === null || _a === void 0 ? void 0 : _a.call(this, errorAsString(err)); });
// else drop out of this block and request a new session
}
}
// TODO we can get rid of one level of promise here?
// Talk to the binder server
const state = {
status: events_1.ServerStatusEvent.launching,
};
const es = new EventSource(urls.build);
this.events.triggerStatus({
status: state.status,
message: `Opened connection to binder: ${urls.build}`,
});
// handle errors
es.onerror = (evt) => {
var _a;
console.error(`Lost connection to binder: ${urls.build}`, evt);
es === null || es === void 0 ? void 0 : es.close();
state.status = events_1.ErrorStatusEvent.error;
const data = evt === null || evt === void 0 ? void 0 : evt.data;
const phase = data ? data.phase : 'unknown';
const message = `Lost connection to binder: ${urls.build}\nphase: ${phase} - ${data ? data.message : 'no message'}`;
this.events.triggerError({
status: events_1.ErrorStatusEvent.error,
message,
});
(_a = this.rejectReadyFn) === null || _a === void 0 ? void 0 : _a.call(this, message);
};
es.onmessage = (evt) => tslib_1.__awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
const msg = JSON.parse(evt.data);
const phase = (_b = (_a = msg.phase) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '';
switch (phase) {
case 'failed':
es === null || es === void 0 ? void 0 : es.close();
state.status = events_1.ErrorStatusEvent.error;
this.events.triggerError({
status: events_1.ErrorStatusEvent.error,
message: `Binder: failed to build - ${urls.build} - ${msg.message}`,
});
(_c = this.rejectReadyFn) === null || _c === void 0 ? void 0 : _c.call(this, msg.message);
break;
case 'ready':
{
es === null || es === void 0 ? void 0 : es.close();
const settings = {
baseUrl: msg.url,
wsUrl: 'ws' + msg.url.slice(4),
token: msg.token,
appendToken: true,
};
const serverSettings = services_1.ServerConnection.makeSettings(settings);
const kernelManager = new services_1.KernelManager({ serverSettings });
this.sessionManager = new services_1.SessionManager({
kernelManager,
serverSettings,
});
if (this.config.savedSessions.enabled) {
(0, sessions_1.saveServerInfo)(urls.storageKey, this.id, serverSettings);
console.debug(`thebe:server:connectToServerViaBinder Saved session for ${this.id} at ${urls.build}`);
}
// promise has already been returned to the caller
// so we can await here
yield this.sessionManager.ready;
this.userServerUrl = `${msg.url}?token=${msg.token}`;
state.status = events_1.ServerStatusEvent.ready;
this.events.triggerStatus({
status: state.status,
message: `Binder server is ready: ${msg.message}`,
});
(_d = this.resolveReadyFn) === null || _d === void 0 ? void 0 : _d.call(this, this);
}
break;
default:
this.events.triggerStatus({
status: state.status,
message: `Binder is: ${phase} - ${msg.message}`,
});
}
});
});
}
//
// ServerRestAPI Implementation
//
getFetchUrl(relativeUrl) {
var _a, _b;
// TODO use ServerConnection.makeRequest - this willadd the token
// and use any internal fetch overrides
// TODO BUG this is the wrong serverSetting, they should be for the active connection
if (!this.sessionManager)
throw new Error('Must connect to a server before requesting KernelSpecs');
if (!((_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.serverSettings))
throw new Error('No server settings available in session manager');
const settings = (_b = this.sessionManager) === null || _b === void 0 ? void 0 : _b.serverSettings;
const baseUrl = new URL(settings.baseUrl);
const url = new URL(`${baseUrl.pathname}${relativeUrl}`.replace('//', '/'), baseUrl.origin);
url.searchParams.append('token', settings.token);
return url;
}
static status(serverSettings) {
return services_1.ServerConnection.makeRequest(`${serverSettings.baseUrl}api/status`, {}, services_1.ServerConnection.makeSettings(serverSettings));
}
getKernelSpecs() {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
if (!this.sessionManager)
throw new Error('Must connect to a server before requesting KernelSpecs');
return services_1.KernelSpecAPI.getSpecs(services_1.ServerConnection.makeSettings((_a = this.sessionManager) === null || _a === void 0 ? void 0 : _a.serverSettings));
});
}
getContents(opts) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const url = this.getFetchUrl(`/api/contents/${opts.path}`);
if (opts.type)
url.searchParams.append('type', opts.type);
if (opts.format)
url.searchParams.append('format', opts.format);
url.searchParams.append('content', opts.returnContent ? '1' : '0');
return responseToJson(yield fetch(url));
});
}
duplicateFile(opts) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const url = this.getFetchUrl(`/api/contents/${opts.path}`);
const { copy_from, ext, type } = opts;
return responseToJson(yield fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ copy_from, ext, type }),
}));
});
}
createDirectory(opts) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const url = this.getFetchUrl(`/api/contents/${opts.path}`);
return responseToJson(yield fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'directory' }),
}));
});
}
renameContents(opts) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const { path, newPath } = opts;
const url = this.getFetchUrl(`/api/contents/${path}`);
return responseToJson(yield fetch(url, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: newPath }),
}));
});
}
uploadFile(opts) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const { path, content, format, type } = opts;
const url = this.getFetchUrl(`/api/contents/${path}`);
console.debug('thebe:api:server:uploadFile', url);
return responseToJson(yield fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path,
content,
format: format !== null && format !== void 0 ? format : 'json',
type: type !== null && type !== void 0 ? type : 'notebook',
}),
}));
});
}
}
exports.default = ThebeServer;
//# sourceMappingURL=server.js.map