@nteract/epics
Version:
Redux-Observable epics for nteract apps
308 lines • 13.5 kB
JavaScript
;
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