UNPKG

thebe-core

Version:

Typescript based core functionality for Thebe

520 lines 25.3 kB
"use strict"; 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