Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for R Shiny with webR #51

Merged
merged 21 commits into from
Sep 5, 2023
Merged

Conversation

georgestagg
Copy link
Collaborator

@georgestagg georgestagg commented Aug 22, 2023

This is a fairly large PR with several moving parts. My apologies that it is not broken up into more manageable pieces.

This set of commits extends Shinylive's infrastructure to include running R Shiny apps using webR. The addition of the webR Wasm engine is based heavily on the Pyodide implementation, to the point of some duplication. In the future we might want to abstract shared pieces out, but hopefully this is OK for now.

The new code for loading webR into the page, executing R code, and setting things up for running Shiny is contained in the files hooks/useWebR.tsx and webr-proxy.ts.


New (but similar to existing) functions have been added to messageporthttp.ts and messageportwebsocket-channel.ts for handling traffic for R Shiny apps separately to Python Shiny apps.

When webR loads a Shiny app, a httpuv app object is created and stored in a global registry. When HTTP requests are received from the service worker, they are converted into rook-style objects and the correct abbObj$call() is invoked directly for a response. When a WebSocket connection is initialised, traffic is handled via callbacks from R. Proxy R object references to the callback functions are stored and used to send WS traffic from the client to the Shiny server.

Going the other way, WS traffic from the server to the client is first serialised then sent via a standard webR output channel to be handled by Shinylive. A map of Shiny app names to specfic app's toClient() functions is maintained and then used to forward the WS output message content onto the correct Shiny client.


When a Shiny client launches in an iframe and opens a WebSocket, an openChannel message is fired to the containing window. This happens for both Python and R and causes cross-talk if both engines are running at the same time. The handlers for these messages have been tweaked so that a communication channel is only opened with a specific Wasm engine if that Wasm engine recognises the app name in its .shiny_app_registry (for webR) or _shiny_app_registry for (Pyodide).


The selection of which Wasm engine will be used is handled in App.tsx. The App component has a new attribute appEngine, of type AppEngine = 'python' | 'r'. The function runApp() now also takes the Wasm engine as an optional argument. The chosen Wasm engine is loaded into the page by using the argument's value to select which of the engine's hooks are used.

The new appEngine argument is optional and defaults to a defined constant in build.ts. By default, build.ts sets the default appEngine to "python". The intention here is that current scripts and CI set up for building the Shinylive website and assets pack will hopefully be unaffected by these changes, by defaulting to using "python" as before.

However, when build.ts is invoked with --r as one of its arguments, the behavior is changed so that the default value for the appEngine argument is 'r' instead. With this, a version of the Shinylive website that loads Shiny for R, running on webR, can be built and served at some other URL, perhaps something like shinylive.io/r/.

In any case, for either type of build both R and/or Python are available to be used in a web page by invoking runApp() with either r or python as the appEngine argument. The same Shinylive asset pack and service worker script can be used to launch both engines. This will be useful later when loading multiple apps in the same page, such as with quarto-shinylive.


Several make recipes have been added to Makefile:

  • make webr - Copy the webR distribution from node_modules into the shinylive assets tree. This step has also been added to make all.
  • make serve-r, make serve-prod-r - Build and serve a version of the Shinylive website for R Shiny.
  • make buildjs-prod-r - Build a version of the Shinylive website for R Shiny.

These commands are useful for building versions of the Shinylive editor and examples website that defaults to using webR and loading R examples, rather than Python.

The previous make serve, make buildjs etc. recipes have been left as-is. They should continue to build the Shinylive website with Python as the default engine, as before.


The examples directory has been reorganised into python and r subdirectories, and I have added some of the examples from https://github.com/rstudio/shiny-examples. The Shinylive website checks the appEngine argument provided to the root App component and filters out the examples that don't match.


The Editor and other React components have been updated to detect Shiny for R apps, by extending the search to include app.R and server.R.

The "Format code" button is hidden when running under webR. Probably more work can be done later to add a full LSP server for R, running under webR. But right now, the editor just doesn't handle any of those extra features when running with the webR engine.

The Terminal component is extended to support webR. The component is tweaked a little so that the correct prompt string is shown (>, or other prompts from R, rather than a fixed >>>), and a Ctrl-C handler has been added so that running R code can be interrupted.


WebR requires that the containing web page is Cross-Origin Isolated (COI) to work when running with Shinylive. This happens when the page is served with certain HTTP headers set. The following changes have been made to assist in loading a webR Shinylive app under COI:

The build.ts dev server has been tweaked to serve its content with the required headers, but only when the --r argument is supplied.

The shinylive service worker is extended so that it can inject the required HTTP headers if they are missing. The service worker has not been set up to do this by default, because it can break Shinylive for Python examples that rely on loading cross-origin assets into the page (e.g. the Map example). So, in this PR the service worker has been set up to only inject the COI headers for webR if it has been asked to, by setting the URL of the loading page (or the referrer URL) to contain the query parameter "coi=1". Servers where files are already served with COI headers don't need to include the query.

The runApp() function has been tweaked so that if the engine argument is 'r', and the page is not already COI, and the query parameter is not in the URL already, then the coi=1 query is added to the page URL and the page refreshed. By adding this, if a webR Shinylive app is used in a web page (e.g. part of a quarto-shinylive document) and the page is not already COI, then the page will refresh after the first load and the service worker injects the required headers. This fixes running Shinylive apps in non-COI contexts, such as GitHub pages.


Note that loading webR's Shiny takes a fair while longer than Pyodide's Shiny for Python. This is due to package downloading. We might still be able to improve this in time by reducing Shiny's prerequisite R package download sizes.

For this initial introduction, the standard webr npm package is bundled with Shinylive. However, in the future I'd like to instead use a different version of the package built specially for Shinylive that already includes all the R packages required to launch R Shiny on its virtual filesystem. That should improve startup time quite a lot.


I think that's pretty much all the major changes. Please let me know if you have any questions or see anything that looks broken or like it won't work.

Example Shinylive for R deployment: https://georgestagg.github.io/shinylive/examples/
Example Quarto doc with Shiny for R & Shiny for Python examples: https://georgestagg.github.io/quarto-shinylive-demo/

@CLAassistant
Copy link

CLAassistant commented Aug 22, 2023

CLA assistant check
All committers have signed the CLA.

Copy link
Collaborator

@wch wch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this looks great! I had a couple small questions/comments but I'm fine with it if you want to get this merged and address those things later.

examples/index.json Show resolved Hide resolved
@@ -458,7 +466,7 @@ export default function Editor({
// ===========================================================================
// Buttons
// ===========================================================================
const formatCodeButton = (
const formatCodeButton = appEngine === 'python' ? (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we could use the styler package to reformat R code, but we can save that for a later PR. (It's probably also a good idea to see how heavy the dependencies are for styler.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, we should definitely look into integrating something like styler. For the moment the idea was mostly just so that a broken format button does not appear when using the R engine.

I think it makes sense to experiment with this in a later PR, as this one is already pretty large.

@@ -400,10 +402,15 @@ let channelListenerRegistered = false;
function ensureOpenChannelListener(pyodideProxy: PyodideProxy): void {
if (channelListenerRegistered) return;

window.addEventListener("message", (event) => {
window.addEventListener("message", async (event) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure it's safe to make this an async function? I'm not saying that I think it's not safe -- I just want to make sure.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like pyodideProxy.openChannel() is itself async, so even without this change I don’t think there’s any guarantee that the channel will be opened by the time the event handler returns.

Add COOP & COEP headers to esbuild dev server for cross-origin
isolation. This enables use of SharedArrayBuffer for Wasm.

Add CORP header to esbuild dev server to permit embedding shinylive
into an outer window.

Use the shinylive service worker to add CORP/COOP/COEP headers to
responses. Allows shinylive to work with webR when running in
environments where it is not possible to change the HTTP headers, e.g.
under GitHub pages.
A new argument `engine` is added to runApp() that selects webR or
Pyodide as the underlying engine to use.

The main app selects a React hook `useWebR()` or `usePyodide()` based on
this argument.

The proxy handle variable `proxyHandle` is typed as a union,
`ProxyHandle = PyodideProxyHandle | WebRProxyHandle`, and the property
`engine` is added as a way to discriminate between which Wasm engine is
currently loaded.

Hooks relying on Pyodide are disabled through early return when the
`proxyHandle.engine` property is not equal to "pyodide".
* Update Terminal component to use a general `proxyHandle`.
* Tweak the `runCode` Ref to return the next prompt as a string when
  running with the webR engine. This allows for the debuging `Browse[1]>`
  and input `readline()` prompts when using R.
* Maintain default ">>> " prompt when running under Pyodide.
The `useEffect()` hook setting up the `runApp()` and `stopApp()` methods
is tweaked so that the current hook runs only when the Wasm engine is
Pyodide.

A second `useEffect()` hook is added for setting up the methods to start
an R shiny server with webR.
Three Makefile targets are added to build and serve with webR
as the default engine:
 * make serve-r
 * make serve-prod-r
 * make buildjs-prod-r
With this commit the Service Worker will only inject the HTTP
headers required for Cross-Origin Isolation if the URL or referrer
URL contains the parameter `coi=1`.

Now, Shinylive for Python is not served with COI headers injected
by default. This fixes the Map example when running under Pyodide.

If an app using the webR engine is run, and the page is not
Cross-Origin Isolated, the parameter `coi=1` is added to the current
location URL and the page refreshed. With this, using webR without
the required COI headers from the server is handled automatically.
@georgestagg
Copy link
Collaborator Author

Minor update: I've rebased on main and updated Shiny et. al. in the webR public Wasm package repo. There was an issue with one of the extra R examples that I added, so I've removed that commit from this PR for now.

So, the included R examples are now just the basic ones that ship with the R shiny package. We can revisit adding more complex R shiny examples after this PR has been merged.

@georgestagg georgestagg merged commit 4193b38 into posit-dev:main Sep 5, 2023
2 checks passed
@georgestagg georgestagg deleted the webr2 branch September 6, 2023 08:18
georgestagg added a commit to georgestagg/shinylive that referenced this pull request Feb 10, 2024
With this, cross-origin requests for embedded content are sent without
credentials and the responses are allowed without an explicit permission
via the CORP header in the response. This fixes embedding certain types
of content in webR Shinylive apps, while still allowing the option of
using cross-origin isolation and SharedArrayBuffer with webR.

Fixes posit-dev#49, posit-dev#51.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants