UNPKG

@nteract/epics

Version:
308 lines 13.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.sendExecuteRequestEpic = exports.createExecuteCellStream = exports.executeCellStream = void 0; const messaging_1 = require("@nteract/messaging"); const redux_observable_1 = require("redux-observable"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const actions = __importStar(require("@nteract/actions")); const selectors = __importStar(require("@nteract/selectors")); const types_1 = require("@nteract/types"); const EXECUTE_CANCEL_ALL = "all"; /** * Observe all the reactions to running code for cell with id. * * @param {Subject} channels - The standard channels specified in the Jupyter * specification. * @param {String} id - Universally Unique Identifier of cell to be executed. * @param {String} code - Source code to be executed. * @return {Observable<Action>} updatedOutputs - It returns an observable with * a stream of events that need to happen after a cell has been executed. */ function executeCellStream(channels, id, message, contentRef) { const executeRequest = message; // All the streams intended for all frontends const cellMessages = channels.pipe(messaging_1.childOf(executeRequest), operators_1.share()); // All the payload streams, intended for one user const payloadStream = cellMessages.pipe(messaging_1.payloads()); const cellAction$ = rxjs_1.merge(payloadStream.pipe(operators_1.map((payload) => actions.acceptPayloadMessage({ id, payload, contentRef }))), /** * Set the ISO datetime when the execute_input message * was broadcast from the kernel, per nbformat. */ cellMessages.pipe(messaging_1.ofMessageType("execute_input"), operators_1.map(() => actions.setInCell({ id, contentRef, path: ["metadata", "execution", "iopub.execute_input"], value: new Date().toISOString() }))), /** * Set the ISO datetime when the execute_reply message * was broadcast from the kernel, per nbformat. */ cellMessages.pipe(messaging_1.ofMessageType("execute_reply"), operators_1.map(() => actions.setInCell({ id, contentRef, path: ["metadata", "execution", "shell.execute_reply"], value: new Date().toISOString() }))), /** * Set the ISO datetime when the status associated with the * cell execution was sent from the kernel, per nbformat. */ cellMessages.pipe(messaging_1.kernelStatuses(), operators_1.map((status) => actions.setInCell({ id, contentRef, path: ["metadata", "execution", `iopub.status.${status}`], value: new Date().toISOString() }))), // All actions for updating cell status cellMessages.pipe(messaging_1.kernelStatuses(), operators_1.map((status) => actions.updateCellStatus({ id, status, contentRef }))), // Update the input numbering: `[ ]` cellMessages.pipe(messaging_1.executionCounts(), operators_1.map((ct) => actions.updateCellExecutionCount({ id, value: ct, contentRef }))), // All actions for new outputs cellMessages.pipe(messaging_1.outputs(), operators_1.map((output) => actions.appendOutput({ id, output, contentRef }))), cellMessages.pipe(messaging_1.ofMessageType("error"), operators_1.map(() => actions.executeCanceled({ contentRef, id: EXECUTE_CANCEL_ALL }))), // clear_output display message cellMessages.pipe(messaging_1.ofMessageType("clear_output"), operators_1.map(() => actions.clearOutputs({ id, contentRef }))), // Prompt the user for input cellMessages.pipe(messaging_1.inputRequests(), operators_1.map((inputRequest) => { return actions.promptInputRequest({ id, contentRef, prompt: inputRequest.prompt, password: inputRequest.password }); }))); /** * When someone subscribes, dispatch the messge to the kernel * by calling `channels.next` then process the responses by proxying * to the inner Observable (cellAction$). */ return rxjs_1.Observable.create((observer) => { const subscription = cellAction$.subscribe(observer); channels.next(executeRequest); return subscription; }); } exports.executeCellStream = executeCellStream; function createExecuteCellStream(action$, channels, message, id, contentRef) { /** * Execute the individual cell, but stop if the execution is cancelled or the * cell is deleted. * * Also stop if a kernel is: * - launched * - interrupted * - killed * - restarted */ const cellStream = executeCellStream(channels, id, message, contentRef).pipe(operators_1.takeUntil(rxjs_1.merge(action$.pipe(redux_observable_1.ofType(actions.EXECUTE_CANCELED, actions.DELETE_CELL), operators_1.filter((action) => action.payload.id === id || action.payload.id === EXECUTE_CANCEL_ALL)), action$.pipe(redux_observable_1.ofType(actions.LAUNCH_KERNEL, actions.LAUNCH_KERNEL_BY_NAME, actions.KILL_KERNEL, actions.INTERRUPT_KERNEL, actions.RESTART_KERNEL), operators_1.filter((action) => action.payload.contentRef === contentRef))))); /** * Begin the execution... */ return cellStream.pipe( /** * But first dispatch some actions to... */ operators_1.startWith( /** * clear the existing contents of the cell */ actions.clearOutputs({ id, contentRef }), /** * update the cell-status to queued */ actions.updateCellStatus({ id, status: "queued", contentRef }))); } exports.createExecuteCellStream = createExecuteCellStream; /** * the send execute request epic processes execute requests for all cells, * creating inner observable streams of the running execution responses */ function sendExecuteRequestEpic(action$, state$) { return action$.pipe(redux_observable_1.ofType(actions.SEND_EXECUTE_REQUEST), /** * Split the stream of SendExecuteRequests that are being dispatched * globally on the Redux store to a seperate stream for each cell. * * This allows us to process each cell's execution lifecycle seperately * from other cells. */ operators_1.groupBy((action) => action.payload.id), /** * We work (map) on each cell's stream individually and merge them * back together into a single stream where the per-cell grouping * is maintained. */ operators_1.mergeMap((cellAction$) => cellAction$.pipe( /** * When a new SendExecuteRequest comes for the same cell, the * switchMap allows us to stop executing the stream assocaited * with the previous execution request and start working on the * new one. */ operators_1.switchMap((action) => { const { id } = action.payload; const state = state$.value; const contentRef = action.payload.contentRef; const model = selectors.model(state, { contentRef }); /** * Currently, only notebooks can send execute requests * because the SendExecuteRequest passes a ContentRef and * a CellId. In the future, we can make this epic applicable * on all content-types by adding a `source` property to the * SendExecuteRequest action. */ if (!model || model.type !== "notebook") { return rxjs_1.of(actions.executeFailed({ error: new Error("Cannot send execute requests from non-notebook files."), code: types_1.errors.EXEC_NOT_A_NOTEBOOK, contentRef })); } /** * Retrieve the cell that we are targetting for execution. * * If it does not exist in the content, then throw an error * because something has gone wrong. * * This might mean that the wrong ContentRef and cellId pair * were passed or that the CellId doesn't exist in the notebook. */ const cell = selectors.notebook.cellById(model, { id }); if (!cell) { return rxjs_1.of(actions.executeFailed({ error: new Error("Could not find the cell with the given CellId."), code: types_1.errors.EXEC_NO_CELL_WITH_ID, contentRef, id })); } /** * Only code cells can be execute so we throw an error * if an attempt to execute a non-code cell is made. */ if (cell.get("cell_type", null) != "code") { return rxjs_1.of(actions.executeCanceled({ code: types_1.errors.EXEC_INVALID_CELL_TYPE, contentRef, id })); } /** * We cannot execute cells with no content, so * we through an error action if this is the case. */ const source = cell.get("source", ""); if (source === "") { return rxjs_1.of(actions.executeCanceled({ code: types_1.errors.EXEC_NO_SOURCE_ERROR, contentRef, id })); } /** * Get the kernel associated with the content model that * we are aexecuting from and its channels. `channels` is * a WebSocketSubject that maintains a WebSocket connection * to the kernel via the /channels WebSocket endpoint on the * Jupyter server. */ const kernel = selectors.kernelByContentRef(state, { contentRef }); const channels = kernel ? kernel.channels : null; const kernelConnected = kernel && !(kernel.status === types_1.KernelStatus.Starting || kernel.status === types_1.KernelStatus.NotConnected); /** * If there is no kernel object for this content or the * kernel is in a processing state, then throw an error * action. */ if (!kernelConnected) { return rxjs_1.of(actions.executeFailed({ error: new Error("There is no connected kernel for this content."), code: types_1.errors.EXEC_NO_KERNEL_ERROR, contentRef })); } /** * If the channels WebSocketSubject doesn't look right, then * throw an error action. */ if (!channels || !channels.pipe || !channels.next) { return rxjs_1.of(actions.executeFailed({ error: new Error("The WebSocket associated with the target kernel is in a bad state."), code: types_1.errors.EXEC_WEBSOCKET_ERROR, contentRef })); } const message = messaging_1.executeRequest(source); return createExecuteCellStream(action$, channels, message, id, action.payload.contentRef).pipe( /** * Catch uncaught exceptions that occur on * each cell's execution flow and dispatch * an action to the user. * * We do not subscribe back to the source stream * and restart the cell execution to avoid getting * caught in loops where the execution keeps failing. * * It's safe to say that if an error is raised here and * none of the guards above caught it, then we cannot * recover from it. * * We can continue adding specific guards above as we * discover new classes of errors. */ operators_1.catchError((error) => rxjs_1.merge(rxjs_1.of(actions.executeFailed({ error, code: types_1.errors.EXEC_ERROR_IN_CELL_STREAM, contentRef: action.payload.contentRef }))))); }))), operators_1.catchError((error, source) => { /** * If any uncaught excpetions bubble here, then throw an error. * Note that we don't have access to the contentRef in this scope. * * When possible, it is best to throw executeFailed errors where * CellId and ContentRef information is present to avoid throwing * generic errors here. * * This catchError returns the source Observable to reset the * sendExecuteRequest epic in the event of unexpected failures. * * Note that SendExecuteRequest actions dispatched during the * reset will not be processed. */ return rxjs_1.merge(rxjs_1.of(actions.executeFailed({ error, code: types_1.errors.EXEC_EPIC_ERROR })), source); })); } exports.sendExecuteRequestEpic = sendExecuteRequestEpic; //# sourceMappingURL=execute.js.map