<NME> contents.ts
<BEF> import querystring from "querystring";
import { Observable } from "rxjs";
import { ajax, AjaxResponse } from "rxjs/ajax";
import urljoin from "url-join";
import { ServerConfig, IGetParams, IContent, FileType, IContentProvider } from "@nteract/types";
import { createAJAXSettings, JupyterAjaxResponse } from "./base";
import { mergeMap } from "rxjs/operators";

const formURI = (path: string) => urljoin("/api/contents/", path);

const formCheckpointURI = (path: string, checkpointID: string) =>
  urljoin("/api/contents/", path, "checkpoints", checkpointID);

/*********************************************
 * Contents API request and response payloads
 *********************************************/

/**
 * Creates an AjaxObservable for removing content.
 *
 * @param serverConfig The server configuration
 * @param path The path to the content
 *
 * @returns An Observable with the request response
 */
export const remove = (serverConfig: ServerConfig, path: string) =>
  ajax(
    createAJAXSettings(serverConfig, formURI(path), {
      method: "DELETE"
    })
  );

/**
 * Creates an AjaxObservable for getting content at a path
 *
 * @param serverConfig The server configuration
 * @param path The content to fetch
 * @param params type, format, content
 * @param params.type file type, one of 'file', 'directory', 'notebook'
 * @param params.format How file content should be returned, e.g. 'text', 'base64'
 * @param params.content Return content or not (0 => no content, 1 => content please)
 *
 * @returns An Observable with the request response
 */
export function get(
  serverConfig: ServerConfig,
  path: string,
  params: Partial<IGetParams> = {}
) {
  let uri = formURI(path);
  const query = querystring.stringify(params);
  if (query.length > 0) {
    uri = `${uri}?${query}`;
  }

  // NOTE: If the user requests with params.content === 0
  // Then the response is IEmptyContent
  return ajax(
    createAJAXSettings(serverConfig, uri, { cache: false })
  ) as Observable<JupyterAjaxResponse<IContent<FileType>>>;
}

/**
 * Creates an AjaxObservable for renaming a file.
 *
 * @param serverConfig The server configuration
 * @param path The content to rename.
 * @param model The data to send in the server request
 *
 * @returns An Observable with the request response
 */
export function update<FT extends FileType>(
  serverConfig: ServerConfig,
  path: string,
  model: Partial<IContent<FT>>
) {
  return ajax(
    createAJAXSettings(serverConfig, formURI(path), {
      body: model,
      headers: {
        "Content-Type": "application/json"
      },
      method: "PATCH"
    })
  );
}

/**
 * Creates an AjaxObservable for creating content
 *
 * @param serverConfig The server configuration
 * @param path The path to the content
 * @param model The data to send in the server request
 *
 * @returns An Observable with the request response
 */
export function create<FT extends FileType>(
  serverConfig: ServerConfig,
  path: string,
  model: Partial<IContent<FT>> & { type: FT }
): Observable<AjaxResponse> {
  return ajax(
    createAJAXSettings(serverConfig, formURI(path), {
      body: model,
      headers: {
        "Content-Type": "application/json"
      },
      method: "POST"
    })
  );
}

/**
 * Creates an AjaxObservable for saving the file in the location specified by
 * name and path in the model.
 *
 * @param serverConfig  The server configuration
 * @param path The path to the content
 * @param model The data to send in the server request
 *
 * @returns An Observable with the request response
 */
export function save<FT extends FileType>(
  serverConfig: ServerConfig,
  path: string,
  model: Partial<IContent<FT>>
) {
  return ajax(
    createAJAXSettings(serverConfig, formURI(path), {
      body: model,
      headers: {
        "Content-Type": "application/json"
      },
      method: "PUT"
    })
  ) as Observable<
    JupyterAjaxResponse<{ path: string; [property: string]: string }>
  >;
}

/**
 * Creates an AjaxObservable for listing checkpoints for a given file.
 *
 * @param serverConfig The server configuration
 * @param path The content containing checkpoints to be listed.
 *
 * @returns An Observable with the request response
 */
export const listCheckpoints = (serverConfig: ServerConfig, path: string) =>
  ajax(
    createAJAXSettings(serverConfig, formCheckpointURI(path, ""), {
      cache: false,
      method: "GET"
    })
  );

/**
 * Creates an AjaxObservable for creating a new checkpoint with the current state of a file.
 * With the default Jupyter FileContentsManager, only one checkpoint is supported,
 * so creating new checkpoints clobbers existing ones.
export function create<FT extends FileType>(
  serverConfig: ServerConfig,
  path: string,
  model: IContent<FT>
) {
  return ajax(
    createAJAXSettings(serverConfig, formURI(path), {
  ajax(
    createAJAXSettings(serverConfig, formCheckpointURI(path, ""), {
      method: "POST"
    })
  );

/**
 * Creates an AjaxObservable for deleting a checkpoint for a given file.
 *
 * @param  serverConfig The server configuration
 * @param  path The content containing the checkpoint to be deleted
 * @param  checkpointID ID of checkpoint to be deleted
 *
 * @returns An Observable with the request response
 */
export const deleteCheckpoint = (
  serverConfig: ServerConfig,
  path: string,
  checkpointID: string
) =>
  ajax(
    createAJAXSettings(serverConfig, formCheckpointURI(path, checkpointID), {
      method: "DELETE"
    })
  );

/**
 * Creates an AjaxObservable for restoring a file to a specified checkpoint.
 *
      },
      method: "PUT"
    })
  );
}

/**
  checkpointID: string
) =>
  ajax(
    createAJAXSettings(serverConfig, formCheckpointURI(path, checkpointID), {
      method: "POST"
    })
  );

/**
 * IContentProvider implementation for Jupyter content server.
 */
export const JupyterContentProvider: IContentProvider = {
  remove(serverConfig: ServerConfig, path: string) {
    return remove(serverConfig, path);
  },
  
  get(serverConfig: ServerConfig, path: string, params: Partial<IGetParams>) {
    return get(serverConfig, path, params);
  },

  update<FT extends FileType>(serverConfig: ServerConfig, path: string, model: Partial<IContent<FT>>) {
    return update(serverConfig, path, model);
  },

  create<FT extends FileType>(serverConfig: ServerConfig, path: string, model: Partial<IContent<FT>> & { type: FT }) {
    return create(serverConfig, path, model);
  },

  save<FT extends FileType>(serverConfig: ServerConfig, path: string, model: Partial<IContent<FT>>) {
    return save(serverConfig, path, model)
      .pipe(
        mergeMap(async (saveXhr: AjaxResponse) => {
          if (saveXhr.response.errno) {
            return saveXhr;
          }

          const pollIntervalMs = 500;
          const maxPollNb = 4;

          // Last_modified value from jupyter server is unreliable:
          // https://github.com/nteract/nteract/issues/4583
          // Check last-modified until value is stable.
          for (let i = 0; i < maxPollNb; ++i) {
            await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
            const getXhr: AjaxResponse = await get(serverConfig, path, { content: 0 }).toPromise();
            if (
              getXhr.status === 200 &&
              typeof getXhr.response !== "string" &&
              getXhr.response.last_modified &&
              Date.parse(getXhr.response.last_modified) > Date.parse(saveXhr.response.last_modified)
            ) {
              // Found a newer last_modified
              saveXhr.response.last_modified = getXhr.response.last_modified;
              return saveXhr;
            }
          }
          return saveXhr;
        })
      );
  },

  listCheckpoints(serverConfig: ServerConfig, path: string) {
    return listCheckpoints(serverConfig, path);
  },

  createCheckpoint(serverConfig: ServerConfig, path: string) {
    return createCheckpoint(serverConfig, path);
  },

  deleteCheckpoint(serverConfig: ServerConfig, path: string, checkpointID: string) {
    return deleteCheckpoint(serverConfig, path, checkpointID);
  },

  restoreFromCheckpoint(serverConfig: ServerConfig, path: string, checkpointID: string) {
    return restoreFromCheckpoint(serverConfig, path, checkpointID);
  }
}

<MSG> Merge pull request #3929 from nteract/kyle/dec/even-more-ts-for-jext

Even more ts for jext
<DFF> @@ -161,7 +161,7 @@ export function update<FT extends FileType>(
 export function create<FT extends FileType>(
   serverConfig: ServerConfig,
   path: string,
-  model: IContent<FT>
+  model: Partial<IContent<FT>> & { type: FT }
 ) {
   return ajax(
     createAJAXSettings(serverConfig, formURI(path), {
@@ -197,7 +197,9 @@ export function save<FT extends FileType>(
       },
       method: "PUT"
     })
-  );
+  ) as Observable<
+    JupyterAjaxResponse<{ path: string; [property: string]: string }>
+  >;
 }
 
 /**

{'commit': 'diff --git a/applications/jupyter-extension/nteract_on_jupyter/app/contents/directory.tsx b/applications/jupyter-extension/nteract_on_jupyter/app/contents/directory.tsx\nindex 87d3dc53c..5d16611bd 100644\n--- a/applications/jupyter-extension/nteract_on_jupyter/app/contents/directory.tsx\n+++ b/applications/jupyter-extension/nteract_on_jupyter/app/contents/directory.tsx\n@@ -1,6 +1,5 @@\n import * as React from "react";\n import styled from "styled-components";\n-import { selectors } from "@nteract/core";\n import { NewNotebookNavigation } from "@nteract/connected-components";\n import {\n   Entry,\n@@ -10,6 +9,7 @@ import {\n   LastSaved\n } from "@nteract/directory-listing";\n import {\n+  selectors,\n   AppState,\n   KernelspecRecord,\n   KernelspecProps,\n@@ -23,7 +23,7 @@ import { Nav, NavSection } from "../components/nav";\n import { openNotebook } from "../triggers/open-notebook";\n import { ThemedLogo } from "../components/themed-logo";\n \n-const urljoin = require("url-join");\n+import urljoin from "url-join";\n \n const ListingRoot = styled.div`\n   margin-top: 2rem;\n@@ -31,26 +31,31 @@ const ListingRoot = styled.div`\n   padding-right: 2rem;\n `;\n \n+type LightDirectoryEntry = {\n+  last_modified: Date | null;\n+  name: string;\n+  path: string;\n+  type: "notebook" | "dummy" | "directory" | "file" | "unknown";\n+};\n+\n+type LightDirectoryEntries = LightDirectoryEntry[];\n+\n type DirectoryProps = {\n-  content: DirectoryContentRecord,\n-  host: JupyterHostRecord,\n-  appVersion: string,\n-  contentRef: ContentRef,\n-  contents: Array<{\n-    path: string,\n-    type: NotebookTypes,\n-    name: string,\n-    last_modified?: Date\n-  }>\n+  appBase: string;\n+  content: DirectoryContentRecord;\n+  host: JupyterHostRecord;\n+  appVersion: string;\n+  contentRef: ContentRef;\n+  contents: LightDirectoryEntries;\n };\n \n export class DirectoryApp extends React.PureComponent<DirectoryProps> {\n   openNotebook = (ks: KernelspecRecord | KernelspecProps) => {\n     openNotebook(this.props.host, ks, {\n+      appBase: this.props.appBase,\n       appVersion: this.props.appVersion,\n       // Since we\'re looking at a directory, the base dir is the directory we are in\n-      baseDir: this.props.content.filepath,\n-      appPath: this.props.host.basePath\n+      baseDir: this.props.content.filepath\n     });\n   };\n \n@@ -58,20 +63,20 @@ export class DirectoryApp extends React.PureComponent<DirectoryProps> {\n     const atRoot = this.props.content.filepath === "/";\n \n     const dotdothref = urljoin(\n-      this.props.host.basePath,\n-      "/nteract/edit/",\n+      this.props.appBase,\n+      // Make sure leading / and .. don\'t navigate outside of the appBase\n       urljoin(this.props.content.filepath, "..")\n     );\n-    const basePath = this.props.host.basePath;\n-    const dotdotlink = <a href={dotdothref}>{".."}</a>;\n+    const dotdotlink = (\n+      <a href={dotdothref} title="Navigate down a directory" role="button">\n+        {".."}\n+      </a>\n+    );\n     return (\n       <React.Fragment>\n         <Nav contentRef={this.props.contentRef}>\n           <NavSection>\n-            <a\n-              href={urljoin(this.props.host.basePath, "/nteract/edit")}\n-              title="Home"\n-            >\n+            <a href={urljoin(this.props.appBase)} title="Home" role="button">\n               <ThemedLogo />\n             </a>\n             <span>{this.props.content.filepath.split("/").pop()}</span>\n@@ -91,7 +96,7 @@ export class DirectoryApp extends React.PureComponent<DirectoryProps> {\n             {this.props.contents.map((entry, index) => {\n               const link = (\n                 <a\n-                  href={urljoin(basePath, "/nteract/edit/", entry.path)}\n+                  href={urljoin(this.props.appBase, entry.path)}\n                   // When it\'s a notebook, we open a new tab\n                   target={entry.type === "notebook" ? "_blank" : undefined}\n                 >\n@@ -115,11 +120,12 @@ export class DirectoryApp extends React.PureComponent<DirectoryProps> {\n \n const mapStateToDirectoryProps = (\n   state: AppState,\n-  ownProps: { contentRef: ContentRef }\n+  ownProps: { contentRef: ContentRef; appBase: string }\n ): DirectoryProps => {\n+  const { contentRef, appBase } = ownProps;\n+\n   const host = selectors.currentHost(state);\n   const content = selectors.content(state, ownProps);\n-  const contentRef = ownProps.contentRef;\n \n   if (host.type !== "jupyter") {\n     throw new Error("This component only works with jupyter servers");\n@@ -131,9 +137,9 @@ const mapStateToDirectoryProps = (\n     );\n   }\n \n-  let contents = [];\n-  content.model.items.map(x => {\n-    const row = selectors.content(state, { contentRef: x });\n+  const contents: LightDirectoryEntry[] = [];\n+  content.model.items.map(entryRef => {\n+    const row = selectors.content(state, { contentRef: entryRef });\n     if (!row) {\n       return {\n         last_modified: new Date(),\n@@ -156,11 +162,12 @@ const mapStateToDirectoryProps = (\n   });\n \n   return {\n+    appBase,\n     appVersion: selectors.appVersion(state),\n     content,\n     contentRef,\n-    host,\n-    contents\n+    contents,\n+    host\n   };\n };\n \ndiff --git a/applications/jupyter-extension/nteract_on_jupyter/app/contents/index.tsx b/applications/jupyter-extension/nteract_on_jupyter/app/contents/index.tsx\nindex ea74acb5e..d6e94ae30 100644\n--- a/applications/jupyter-extension/nteract_on_jupyter/app/contents/index.tsx\n+++ b/applications/jupyter-extension/nteract_on_jupyter/app/contents/index.tsx\n@@ -3,22 +3,20 @@\n // in production and the footprint is minimal.\n import { hot } from "react-hot-loader";\n import * as React from "react";\n-import { selectors } from "@nteract/core";\n-import { AppState, ContentRef } from "@nteract/core";\n+import { selectors, AppState, ContentRef } from "@nteract/core";\n import { connect } from "react-redux";\n \n import { ThemedLogo } from "../components/themed-logo";\n import { Nav, NavSection } from "../components/nav";\n \n-import { default as Directory } from "./directory";\n+import { ConnectedDirectory } from "./directory";\n import { default as File } from "./file";\n-\n-const urljoin = require("url-join");\n+import urljoin from "url-join";\n \n type ContentsProps = {\n-  contentType: "dummy" | "notebook" | "directory" | "file",\n-  contentRef: ContentRef,\n-  appBase: string\n+  contentType: "dummy" | "notebook" | "directory" | "file";\n+  contentRef: ContentRef;\n+  appBase: string;\n };\n \n const mapStateToProps = (\n@@ -47,7 +45,7 @@ const mapStateToProps = (\n   };\n };\n \n-class Contents extends React.Component<ContentsProps> {\n+class Contents extends React.PureComponent<ContentsProps> {\n   render() {\n     const appBase = this.props.appBase;\n \n@@ -58,7 +56,10 @@ class Contents extends React.Component<ContentsProps> {\n         return <File contentRef={this.props.contentRef} appBase={appBase} />;\n       case "directory":\n         return (\n-          <Directory contentRef={this.props.contentRef} appBase={appBase} />\n+          <ConnectedDirectory\n+            contentRef={this.props.contentRef}\n+            appBase={appBase}\n+          />\n         );\n       default:\n         return (\n@@ -71,7 +72,7 @@ class Contents extends React.Component<ContentsProps> {\n               </NavSection>\n             </Nav>\n             <div>\n-              {`content type ${ this.props.contentType } not implemented`}\n+              {`content type ${this.props.contentType} not implemented`}\n             </div>\n           </React.Fragment>\n         );\ndiff --git a/applications/jupyter-extension/nteract_on_jupyter/app/triggers/open-notebook.ts b/applications/jupyter-extension/nteract_on_jupyter/app/triggers/open-notebook.ts\nindex f8bee8ae8..4eff5e236 100644\n--- a/applications/jupyter-extension/nteract_on_jupyter/app/triggers/open-notebook.ts\n+++ b/applications/jupyter-extension/nteract_on_jupyter/app/triggers/open-notebook.ts\n@@ -8,25 +8,30 @@ import { selectors } from "@nteract/core";\n import { contents, sessions } from "rx-jupyter";\n import { first, map, mergeMap } from "rxjs/operators";\n import { forkJoin } from "rxjs";\n+import { Notebook } from "@nteract/commutable";\n \n const urljoin = require("url-join");\n \n export function openNotebook(\n   host: JupyterHostRecord,\n   ks: KernelspecRecord | KernelspecProps,\n-  props: { appVersion: string, baseDir: string, appPath: string }\n+  props: {\n+    appVersion: string;\n+    baseDir: string;\n+    appBase: string;\n+  }\n ) {\n   const serverConfig = selectors.serverConfig(host);\n \n   // The notebook they get to start with\n-  const notebook = {\n+  const notebook: Notebook = {\n     cells: [\n       {\n         cell_type: "code",\n         execution_count: null,\n         metadata: {},\n         outputs: [],\n-        source: []\n+        source: [""]\n       }\n     ],\n     metadata: {\n@@ -47,7 +52,7 @@ export function openNotebook(\n   //       happening here instead of an epic\n   contents\n     // Create UntitledXYZ.ipynb by letting the server do it\n-    .create(serverConfig, props.baseDir, {\n+    .create<"notebook">(serverConfig, props.baseDir, {\n       type: "notebook"\n       // NOTE: The contents API appears to ignore the content field for new\n       // notebook creation.\n@@ -81,8 +86,8 @@ export function openNotebook(\n           sessions.create(serverConfig, sessionPayload),\n           // Save the initial notebook document\n           contents.save(serverConfig, filepath, {\n-            type: "notebook",\n-            content: notebook\n+            content: notebook,\n+            type: "notebook"\n           })\n         );\n       }),\n@@ -91,11 +96,24 @@ export function openNotebook(\n       map(([session, content]) => {\n         const { response } = content;\n \n+        if (content.status > 299 || typeof response === "string") {\n+          // hack around this old hack around for creating a notebook from the directory\n+          // ideally this would be in a proper epic instead of leaky async code here\n+          const message = ["Failed to create notebook due to: "];\n+\n+          if (typeof response === "string") {\n+            message.push(response);\n+          } else {\n+            message.push(JSON.stringify(response));\n+          }\n+\n+          alert(message.join(""));\n+\n+          return;\n+        }\n+\n         const url = urljoin(\n-          // User path\n-          props.appPath,\n-          // nteract edit path\n-          "/nteract/edit",\n+          props.appBase,\n           // Actual file\n           response.path\n         );\ndiff --git a/packages/rx-jupyter/src/contents.ts b/packages/rx-jupyter/src/contents.ts\nindex 3b575c48e..ffdf3fdbc 100644\n--- a/packages/rx-jupyter/src/contents.ts\n+++ b/packages/rx-jupyter/src/contents.ts\n@@ -161,7 +161,7 @@ export function update<FT extends FileType>(\n export function create<FT extends FileType>(\n   serverConfig: ServerConfig,\n   path: string,\n-  model: IContent<FT>\n+  model: Partial<IContent<FT>> & { type: FT }\n ) {\n   return ajax(\n     createAJAXSettings(serverConfig, formURI(path), {\n@@ -197,7 +197,9 @@ export function save<FT extends FileType>(\n       },\n       method: "PUT"\n     })\n-  );\n+  ) as Observable<\n+    JupyterAjaxResponse<{ path: string; [property: string]: string }>\n+  >;\n }\n \n /**\n', 'commit_subject': 'Merge pull request #3929 from nteract/kyle/dec/even-more-ts-for-jext', 'repo_name': 'nteract/nteract', 'committer': {'name': 'GitHub', 'email': '1505422b2465e9a84f6fdfaa161078890c593f06@github.com', 'time_sec': '1546278121', 'tz_offset': '-480', 'date': {'seconds': '1546278121'}}, 'commit_tree': 'b9206ee33cf763364839bb0c225ad538e436dfd9', 'commit_message': 'Merge pull request #3929 from nteract/kyle/dec/even-more-ts-for-jext\n\nEven more ts for jext', 'language_name': 'JavaScript', 'license': 'bsd-3-clause', 'hunks': '@@ -161,7 +161,7 @@ export function update<FT extends FileType>(\n export function create<FT extends FileType>(\n   serverConfig: ServerConfig,\n   path: string,\n-  model: IContent<FT>\n+  model: Partial<IContent<FT>> & { type: FT }\n ) {\n   return ajax(\n     createAJAXSettings(serverConfig, formURI(path), {\n@@ -197,7 +197,9 @@ export function save<FT extends FileType>(\n       },\n       method: "PUT"\n     })\n-  );\n+  ) as Observable<\n+    JupyterAjaxResponse<{ path: string; [property: string]: string }>\n+  >;\n }\n \n /**\n', 'addition_count': 4, 'deletion_count': 2, 'src_file': 'a/packages/rx-jupyter/src/contents.ts', 'tgt_file': 'b/packages/rx-jupyter/src/contents.ts', 'file_extension': '.ts', 'before_file': 'import querystring from "querystring";\nimport { Observable } from "rxjs";\nimport { ajax, AjaxResponse } from "rxjs/ajax";\nimport urljoin from "url-join";\nimport { ServerConfig, IGetParams, IContent, FileType, IContentProvider } from "@nteract/types";\nimport { createAJAXSettings, JupyterAjaxResponse } from "./base";\nimport { mergeMap } from "rxjs/operators";\n\nconst formURI = (path: string) => urljoin("/api/contents/", path);\n\nconst formCheckpointURI = (path: string, checkpointID: string) =>\n  urljoin("/api/contents/", path, "checkpoints", checkpointID);\n\n/*********************************************\n * Contents API request and response payloads\n *********************************************/\n\n/**\n * Creates an AjaxObservable for removing content.\n *\n * @param serverConfig The server configuration\n * @param path The path to the content\n *\n * @returns An Observable with the request response\n */\nexport const remove = (serverConfig: ServerConfig, path: string) =>\n  ajax(\n    createAJAXSettings(serverConfig, formURI(path), {\n      method: "DELETE"\n    })\n  );\n\n/**\n * Creates an AjaxObservable for getting content at a path\n *\n * @param serverConfig The server configuration\n * @param path The content to fetch\n * @param params type, format, content\n * @param params.type file type, one of \'file\', \'directory\', \'notebook\'\n * @param params.format How file content should be returned, e.g. \'text\', \'base64\'\n * @param params.content Return content or not (0 => no content, 1 => content please)\n *\n * @returns An Observable with the request response\n */\nexport function get(\n  serverConfig: ServerConfig,\n  path: string,\n  params: Partial<IGetParams> = {}\n) {\n  let uri = formURI(path);\n  const query = querystring.stringify(params);\n  if (query.length > 0) {\n    uri = `${uri}?${query}`;\n  }\n\n  // NOTE: If the user requests with params.content === 0\n  // Then the response is IEmptyContent\n  return ajax(\n    createAJAXSettings(serverConfig, uri, { cache: false })\n  ) as Observable<JupyterAjaxResponse<IContent<FileType>>>;\n}\n\n/**\n * Creates an AjaxObservable for renaming a file.\n *\n * @param serverConfig The server configuration\n * @param path The content to rename.\n * @param model The data to send in the server request\n *\n * @returns An Observable with the request response\n */\nexport function update<FT extends FileType>(\n  serverConfig: ServerConfig,\n  path: string,\n  model: Partial<IContent<FT>>\n) {\n  return ajax(\n    createAJAXSettings(serverConfig, formURI(path), {\n      body: model,\n      headers: {\n        "Content-Type": "application/json"\n      },\n      method: "PATCH"\n    })\n  );\n}\n\n/**\n * Creates an AjaxObservable for creating content\n *\n * @param serverConfig The server configuration\n * @param path The path to the content\n * @param model The data to send in the server request\n *\n * @returns An Observable with the request response\n */\nexport function create<FT extends FileType>(\n  serverConfig: ServerConfig,\n  path: string,\n  model: Partial<IContent<FT>> & { type: FT }\n): Observable<AjaxResponse> {\n  return ajax(\n    createAJAXSettings(serverConfig, formURI(path), {\n      body: model,\n      headers: {\n        "Content-Type": "application/json"\n      },\n      method: "POST"\n    })\n  );\n}\n\n/**\n * Creates an AjaxObservable for saving the file in the location specified by\n * name and path in the model.\n *\n * @param serverConfig  The server configuration\n * @param path The path to the content\n * @param model The data to send in the server request\n *\n * @returns An Observable with the request response\n */\nexport function save<FT extends FileType>(\n  serverConfig: ServerConfig,\n  path: string,\n  model: Partial<IContent<FT>>\n) {\n  return ajax(\n    createAJAXSettings(serverConfig, formURI(path), {\n      body: model,\n      headers: {\n        "Content-Type": "application/json"\n      },\n      method: "PUT"\n    })\n  ) as Observable<\n    JupyterAjaxResponse<{ path: string; [property: string]: string }>\n  >;\n}\n\n/**\n * Creates an AjaxObservable for listing checkpoints for a given file.\n *\n * @param serverConfig The server configuration\n * @param path The content containing checkpoints to be listed.\n *\n * @returns An Observable with the request response\n */\nexport const listCheckpoints = (serverConfig: ServerConfig, path: string) =>\n  ajax(\n    createAJAXSettings(serverConfig, formCheckpointURI(path, ""), {\n      cache: false,\n      method: "GET"\n    })\n  );\n\n/**\n * Creates an AjaxObservable for creating a new checkpoint with the current state of a file.\n * With the default Jupyter FileContentsManager, only one checkpoint is supported,\n * so creating new checkpoints clobbers existing ones.\nexport function create<FT extends FileType>(\n  serverConfig: ServerConfig,\n  path: string,\n  model: IContent<FT>\n) {\n  return ajax(\n    createAJAXSettings(serverConfig, formURI(path), {\n  ajax(\n    createAJAXSettings(serverConfig, formCheckpointURI(path, ""), {\n      method: "POST"\n    })\n  );\n\n/**\n * Creates an AjaxObservable for deleting a checkpoint for a given file.\n *\n * @param  serverConfig The server configuration\n * @param  path The content containing the checkpoint to be deleted\n * @param  checkpointID ID of checkpoint to be deleted\n *\n * @returns An Observable with the request response\n */\nexport const deleteCheckpoint = (\n  serverConfig: ServerConfig,\n  path: string,\n  checkpointID: string\n) =>\n  ajax(\n    createAJAXSettings(serverConfig, formCheckpointURI(path, checkpointID), {\n      method: "DELETE"\n    })\n  );\n\n/**\n * Creates an AjaxObservable for restoring a file to a specified checkpoint.\n *\n      },\n      method: "PUT"\n    })\n  );\n}\n\n/**\n  checkpointID: string\n) =>\n  ajax(\n    createAJAXSettings(serverConfig, formCheckpointURI(path, checkpointID), {\n      method: "POST"\n    })\n  );\n\n/**\n * IContentProvider implementation for Jupyter content server.\n */\nexport const JupyterContentProvider: IContentProvider = {\n  remove(serverConfig: ServerConfig, path: string) {\n    return remove(serverConfig, path);\n  },\n  \n  get(serverConfig: ServerConfig, path: string, params: Partial<IGetParams>) {\n    return get(serverConfig, path, params);\n  },\n\n  update<FT extends FileType>(serverConfig: ServerConfig, path: string, model: Partial<IContent<FT>>) {\n    return update(serverConfig, path, model);\n  },\n\n  create<FT extends FileType>(serverConfig: ServerConfig, path: string, model: Partial<IContent<FT>> & { type: FT }) {\n    return create(serverConfig, path, model);\n  },\n\n  save<FT extends FileType>(serverConfig: ServerConfig, path: string, model: Partial<IContent<FT>>) {\n    return save(serverConfig, path, model)\n      .pipe(\n        mergeMap(async (saveXhr: AjaxResponse) => {\n          if (saveXhr.response.errno) {\n            return saveXhr;\n          }\n\n          const pollIntervalMs = 500;\n          const maxPollNb = 4;\n\n          // Last_modified value from jupyter server is unreliable:\n          // https://github.com/nteract/nteract/issues/4583\n          // Check last-modified until value is stable.\n          for (let i = 0; i < maxPollNb; ++i) {\n            await new Promise(resolve => setTimeout(resolve, pollIntervalMs));\n            const getXhr: AjaxResponse = await get(serverConfig, path, { content: 0 }).toPromise();\n            if (\n              getXhr.status === 200 &&\n              typeof getXhr.response !== "string" &&\n              getXhr.response.last_modified &&\n              Date.parse(getXhr.response.last_modified) > Date.parse(saveXhr.response.last_modified)\n            ) {\n              // Found a newer last_modified\n              saveXhr.response.last_modified = getXhr.response.last_modified;\n              return saveXhr;\n            }\n          }\n          return saveXhr;\n        })\n      );\n  },\n\n  listCheckpoints(serverConfig: ServerConfig, path: string) {\n    return listCheckpoints(serverConfig, path);\n  },\n\n  createCheckpoint(serverConfig: ServerConfig, path: string) {\n    return createCheckpoint(serverConfig, path);\n  },\n\n  deleteCheckpoint(serverConfig: ServerConfig, path: string, checkpointID: string) {\n    return deleteCheckpoint(serverConfig, path, checkpointID);\n  },\n\n  restoreFromCheckpoint(serverConfig: ServerConfig, path: string, checkpointID: string) {\n    return restoreFromCheckpoint(serverConfig, path, checkpointID);\n  }\n}\n', 'message': 'Merge pull request #3929 from nteract/kyle/dec/even-more-ts-for-jext\n\nEven more ts for jext', 'author_name': 'GitHub', 'lang': 'ts'}