@nteract/epics
Version:
Redux-Observable epics for nteract apps
333 lines • 15.3 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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.closeNotebookEpic = exports.saveAsContentEpic = exports.saveContentEpic = exports.autoSaveCurrentContentEpic = exports.downloadString = exports.fetchContentEpic = exports.updateContentEpic = void 0;
const actions = __importStar(require("@nteract/actions"));
const commutable_1 = require("@nteract/commutable");
const mythic_configuration_1 = require("@nteract/mythic-configuration");
const mythic_notifications_1 = require("@nteract/mythic-notifications");
const selectors = __importStar(require("@nteract/selectors"));
const types_1 = require("@nteract/types");
const file_saver_1 = __importDefault(require("file-saver"));
const path = __importStar(require("path"));
const redux_observable_1 = require("redux-observable");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const url_join_1 = __importDefault(require("url-join"));
function updateContentEpic(action$, state$, dependencies) {
return action$.pipe(redux_observable_1.ofType(actions.CHANGE_CONTENT_NAME), operators_1.switchMap(action => {
const state = state$.value;
const { filepath, prevFilePath } = action.payload;
const host = selectors.currentHost(state);
const serverConfig = selectors.serverConfig(host);
return dependencies.contentProvider
.update(serverConfig, prevFilePath, { path: filepath.slice(1) })
.pipe(operators_1.tap(xhr => {
if (xhr.status !== 200) {
throw new Error(xhr.response);
}
}), operators_1.map(() => {
/*
* Modifying the url's file name in the browser.
* This effects back button behavior.
* Is there a better way to accomplish this?
*/
window.history.replaceState({}, filepath, url_join_1.default(host.basePath, `/nteract/edit${filepath}`));
return actions.changeContentNameFulfilled({
contentRef: action.payload.contentRef,
filepath: action.payload.filepath,
prevFilePath
});
}), operators_1.catchError((xhrError) => rxjs_1.of(actions.changeContentNameFailed({
basepath: host.basePath,
filepath: action.payload.filepath,
prevFilePath,
error: xhrError,
contentRef: action.payload.contentRef
}))));
}));
}
exports.updateContentEpic = updateContentEpic;
function fetchContentEpic(action$, state$, dependencies) {
return action$.pipe(redux_observable_1.ofType(actions.FETCH_CONTENT), operators_1.switchMap(action => {
const state = state$.value;
const host = selectors.currentHost(state);
const serverConfig = selectors.serverConfig(host);
return dependencies.contentProvider
.get(serverConfig, action.payload.filepath, action.payload.params)
.pipe(operators_1.tap(xhr => {
if (xhr.status !== 200) {
throw new Error(xhr.response.toString());
}
}), operators_1.map(xhr => {
if (typeof xhr.response === "string") {
throw new Error(`Invalid API response: ${xhr.response}`);
}
return actions.fetchContentFulfilled({
filepath: action.payload.filepath,
model: xhr.response,
kernelRef: action.payload.kernelRef,
contentRef: action.payload.contentRef
});
}), operators_1.catchError((xhrError) => rxjs_1.of(actions.fetchContentFailed({
filepath: action.payload.filepath,
error: xhrError,
kernelRef: action.payload.kernelRef,
contentRef: action.payload.contentRef
}))));
}));
}
exports.fetchContentEpic = fetchContentEpic;
function downloadString(fileContents, filepath, contentType) {
const filename = filepath.split("/").pop();
const blob = new Blob([fileContents], { type: contentType });
// NOTE: There is no callback for this, we have to rely on the browser
// to do this well, so we assume it worked
file_saver_1.default.saveAs(blob, filename);
}
exports.downloadString = downloadString;
const { selector: autoSaveInterval } = mythic_configuration_1.defineConfigOption({
key: "autoSaveInterval",
label: "Auto-save interval",
defaultValue: 120000,
});
function autoSaveCurrentContentEpic(action$, state$) {
return state$.pipe(operators_1.map(state => autoSaveInterval(state)), operators_1.switchMap(time => rxjs_1.interval(time)), operators_1.mergeMap(() => {
const state = state$.value;
return rxjs_1.from(selectors
.contentByRef(state)
.filter(
/*
* Only save contents that are files or notebooks with
* a filepath already set.
*/
content => (content.type === "file" || content.type === "notebook") &&
content.filepath !== "")
.keys());
}), operators_1.filter((contentRef) => {
const model = selectors.model(state$.value, { contentRef });
if (model && model.type === "notebook") {
return selectors.notebook.isDirty(model);
}
return false;
}), operators_1.map((contentRef) => actions.save({ contentRef })));
}
exports.autoSaveCurrentContentEpic = autoSaveCurrentContentEpic;
function serializeContent(state, content) {
// This could be object for notebook, or string for files
let serializedData;
let saveModel = {};
if (content.type === "notebook") {
const appVersion = selectors.appVersion(state);
// contents API takes notebook as raw JSON whereas downloading takes
// a string
serializedData = commutable_1.toJS(content.model.notebook.setIn(["metadata", "nteract", "version"], appVersion));
saveModel = {
content: serializedData,
type: content.type
};
}
else if (content.type === "file") {
serializedData = content.model.text;
saveModel = {
content: serializedData,
type: content.type,
format: "text"
};
}
else {
return { saveModel: null, serializedData: null };
}
return { saveModel, serializedData };
}
function saveContentEpic(action$, state$, dependencies) {
return action$.pipe(redux_observable_1.ofType(actions.SAVE, actions.DOWNLOAD_CONTENT), operators_1.mergeMap((action) => {
const state = state$.value;
const contentRef = action.payload.contentRef;
const content = selectors.content(state, { contentRef });
// NOTE: This could save by having selectors for each model type
// have toDisk() selectors
// It will need to be cased off when we have more than one type
// of content we actually save
if (!content) {
const errorPayload = {
error: new Error("Content was not set."),
contentRef: action.payload.contentRef
};
if (action.type === actions.DOWNLOAD_CONTENT) {
return rxjs_1.of(actions.downloadContentFailed(errorPayload));
}
return rxjs_1.of(actions.saveFailed(errorPayload));
}
if (content.type === "directory") {
return rxjs_1.of(actions.saveFailed({
error: new Error("Cannot save directories."),
contentRef: action.payload.contentRef
}));
}
const filepath = content.filepath;
const { serializedData, saveModel } = serializeContent(state, content);
if (!saveModel || !serializedData) {
return rxjs_1.of(actions.saveFailed({
error: new Error("No serialized model created for this content."),
contentRef: action.payload.contentRef
}));
}
switch (action.type) {
case actions.DOWNLOAD_CONTENT: {
// FIXME: Convert this to downloadString, so it works for
// both files & notebooks
if (content.type === "notebook" &&
typeof serializedData === "object") {
downloadString(commutable_1.stringifyNotebook(serializedData), filepath || "notebook.ipynb", "application/json");
}
else if (content.type === "file" &&
typeof serializedData === "string") {
downloadString(serializedData, filepath, content.mimetype || "application/octet-stream");
}
else {
// This shouldn't happen, is here for safety
return rxjs_1.EMPTY;
}
return rxjs_1.of(actions.downloadContentFulfilled({
contentRef: action.payload.contentRef
}));
}
case actions.SAVE: {
const host = selectors.currentHost(state);
const serverConfig = selectors.serverConfig(host);
return dependencies.contentProvider
.save(serverConfig, filepath, saveModel)
.pipe(operators_1.mergeMap((saveXhr) => {
if (saveXhr.response.errno) {
return rxjs_1.of(actions.saveFailed({
contentRef: action.payload.contentRef,
error: saveXhr.response
}));
}
return rxjs_1.of(actions.saveFulfilled({
contentRef: action.payload.contentRef,
model: saveXhr.response
}));
}), operators_1.catchError((error) => rxjs_1.of(actions.saveFailed({
error,
contentRef: action.payload.contentRef
}))));
}
default:
// NOTE: Our ofType should prevent reaching here, this
// is here merely as safety
return rxjs_1.EMPTY;
}
}));
}
exports.saveContentEpic = saveContentEpic;
function saveAsContentEpic(action$, state$, dependencies) {
return action$.pipe(redux_observable_1.ofType(actions.SAVE_AS), operators_1.mergeMap((action) => {
const state = state$.value;
const contentRef = action.payload.contentRef;
const content = selectors.content(state, { contentRef });
if (!content) {
const errorPayload = {
error: new Error("Content was not set."),
contentRef: action.payload.contentRef
};
return rxjs_1.of(actions.saveAsFailed(errorPayload));
}
if (content.type === "directory") {
return rxjs_1.of(actions.saveAsFailed({
error: new Error("Cannot save directories."),
contentRef: action.payload.contentRef
}));
}
const filepath = action.payload.filepath;
const { saveModel } = serializeContent(state, content);
if (!saveModel) {
return rxjs_1.of(actions.saveAsFailed({
error: new Error("No serialized model created for this content."),
contentRef: action.payload.contentRef
}));
}
const host = selectors.currentHost(state);
const serverConfig = selectors.serverConfig(host);
const kernelRef = selectors.kernelRefByContentRef(state, { contentRef });
const alertKernelChanged = [];
if (kernelRef) {
const kernel = selectors.kernel(state, { kernelRef });
if (kernel) {
const cwd = path.dirname(path.resolve(filepath));
if (cwd !== kernel.cwd) {
alertKernelChanged.push(mythic_notifications_1.sendNotification.create({
title: "Notebook folder changed",
message: "The kernel executing your code thinks your notebook is still " +
"in the old folder. Would you like to launch a new kernel in " +
"the new folder?",
level: "warning",
action: {
label: "Launch new kernel",
callback() {
if (window && window.store) {
window.store.dispatch(actions.launchKernelByName({
kernelSpecName: kernel.kernelSpecName,
cwd,
kernelRef: types_1.createKernelRef(),
selectNextKernel: true,
contentRef
}));
}
}
}
}));
}
}
}
return dependencies.contentProvider
.save(serverConfig, filepath, saveModel)
.pipe(operators_1.mergeMap((xhr) => {
return rxjs_1.of(actions.changeFilename({
contentRef: action.payload.contentRef,
filepath
}), actions.saveAsFulfilled({
contentRef: action.payload.contentRef,
model: xhr.response
}), ...alertKernelChanged);
}), operators_1.catchError((error) => rxjs_1.of(actions.saveAsFailed({
error,
contentRef: action.payload.contentRef
}))));
}));
}
exports.saveAsContentEpic = saveAsContentEpic;
function closeNotebookEpic(action$, state$) {
return action$.pipe(redux_observable_1.ofType(actions.CLOSE_NOTEBOOK), operators_1.mergeMap((action) => {
const state = state$.value;
const contentRef = action.payload.contentRef;
const kernelRef = selectors.kernelRefByContentRef(state, {
contentRef
});
return rxjs_1.of(actions.disposeContent({ contentRef }), actions.killKernel({ kernelRef, restarting: false, dispose: true }));
}));
}
exports.closeNotebookEpic = closeNotebookEpic;
//# sourceMappingURL=contents.js.map