Skip to content

Commit

Permalink
Channel type feature (#32)
Browse files Browse the repository at this point in the history
* Add ChannelType communication channel option.

Closes Consider allowing use of `ChannelType.PostMessage` in webR config #3

* Update README documentation to document the new `channel-type` option.

* Add a new piece of documentation describing webR communication channels within the quarto-webr scope.

* Switch channel type
  • Loading branch information
coatless authored Sep 16, 2023
1 parent 5f46e98 commit 00c3132
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 18 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ The `quarto-webr` extension supports specifying the following `WebROptions` opti

- `home-dir`: The WebAssembly user’s home directory and initial working directory ([`Documentation`](https://docs.r-wasm.org/webr/latest/api/js/interfaces/WebR.WebROptions.html#homedir)). Default: `'/home/web_user'`.
- `base-url`: The base URL used for downloading R WebAssembly binaries ([`Documentation`](https://docs.r-wasm.org/webr/latest/api/js/interfaces/WebR.WebROptions.html#baseurl)). Default: `'https://webr.r-wasm.org/[version]/'`.
- `channel-type`: The communication channel type to interact with webR ([`Documentation`](https://docs.r-wasm.org/webr/latest/api/js/interfaces/WebR.WebROptions.html#channeltype)). Default: `"automatic"` (0). Alternative options are: `"shared-array-buffer"` (1), `"service-worker"` (2), `"post-message"` (3).
- We recommend using `"post-message"` option if GitHub Pages or Quarto Pub are serving the webR-enabled document.
- However, this option prevents the interruption of running _R_ code and prevents the use of nested R REPLs (`readline()`, `menu()`, `browser()`, etc.)
- `service-worker-url`: The base URL from where to load JavaScript worker scripts when loading webR with the ServiceWorker communication channel mode ([`Documentation`](https://docs.r-wasm.org/webr/latest/api/js/interfaces/WebR.WebROptions.html#serviceworkerurl)). Default: `''`.

The extension also has native options for:
Expand Down
5 changes: 3 additions & 2 deletions _extensions/webr/webr-init.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,15 @@
}

// Retrieve the webr.mjs
import { WebR } from "https://webr.r-wasm.org/v0.2.1/webr.mjs";
import { WebR, ChannelType } from "https://webr.r-wasm.org/v0.2.1/webr.mjs";

// Populate WebR options with defaults or new values based on
// webr meta
globalThis.webR = new WebR({
"baseURL": "{{BASEURL}}",
"serviceWorkerUrl": "{{SERVICEWORKERURL}}",
"homedir": "{{HOMEDIR}}"
"homedir": "{{HOMEDIR}}",
"channelType": {{CHANNELTYPE}}
});

// Initialization WebR
Expand Down
74 changes: 58 additions & 16 deletions _extensions/webr/webr.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ local baseVersionWebR = "0.2.1"
local baseUrl = ""
local serviceWorkerUrl = ""

-- Define the webR communication protocol
local channelType = "ChannelType.Automatic"

-- Define a variable to suppress exporting service workers if not required.
-- (e.g. skipped for PostMessage or SharedArrayBuffer)
local hasServiceWorkerFiles = true

-- Define user directory
local homeDir = "/home/web_user"

Expand All @@ -41,6 +48,25 @@ function is_variable_empty(s)
return s == nil or s == ''
end

-- Convert the communication channel meta option into a WebROptions.channelType option
function convertMetaChannelTypeToWebROption(input)
-- Create a table of conditions
local conditions = {
["automatic"] = "ChannelType.Automatic",
[0] = "ChannelType.Automatic",
["shared-array-buffer"] = "ChannelType.SharedArrayBuffer",
[1] = "ChannelType.SharedArrayBuffer",
["service-worker"] = "ChannelType.ServiceWorker",
[2] = "ChannelType.ServiceWorker",
["post-message"] = "ChannelType.PostMessage",
[3] = "ChannelType.PostMessage",
}
-- Subset the table to obtain the communication channel.
-- If the option isn't found, return automatic.
return conditions[input] or "ChannelType.Automatic"
end


-- Parse the different webr options set in the YAML frontmatter, e.g.
--
-- ```yaml
Expand Down Expand Up @@ -73,6 +99,17 @@ function setWebRInitializationOptions(meta)
baseUrl = pandoc.utils.stringify(webr["base-url"])
end

-- The communication channel mode webR uses to connect R with the web browser
-- Default: "ChannelType.Automatic"
-- Documentation:
-- https://docs.r-wasm.org/webr/latest/api/js/interfaces/WebR.WebROptions.html#channeltype
if not is_variable_empty(webr["channel-type"]) then
channelType = convertMetaChannelTypeToWebROption(pandoc.utils.stringify(webr["channel-type"]))
if not (channelType == "ChannelType.Automatic" and channelType == "ChannelType.ServiceWorker") then
hasServiceWorkerFiles = false
end
end

-- The base URL from where to load JavaScript worker scripts when loading webR
-- with the ServiceWorker communication channel mode.
-- Documentation:
Expand Down Expand Up @@ -245,6 +282,7 @@ function initializationWebR()
["SHOWSTARTUPMESSAGE"] = showStartUpMessage, -- tostring()
["SHOWHEADERMESSAGE"] = showHeaderMessage,
["BASEURL"] = baseUrl,
["CHANNELTYPE"] = channelType,
["SERVICEWORKERURL"] = serviceWorkerUrl,
["HOMEDIR"] = homeDir,
["INSTALLRPACKAGESLIST"] = installRPackagesList
Expand Down Expand Up @@ -280,22 +318,26 @@ function ensureWebRSetup()
-- Insert the monaco editor initialization
quarto.doc.include_file("before-body", "monaco-editor-init.html")

-- Copy the two web workers into the directory
-- https://quarto.org/docs/extensions/lua-api.html#dependencies

quarto.doc.add_html_dependency({
name = "webr-worker",
version = baseVersionWebR,
--seviceworkers = {"webr-worker.js"}, -- Kept to avoid error text.
serviceworkers = {"webr-worker.js"}
})

quarto.doc.add_html_dependency({
name = "webr-serviceworker",
version = baseVersionWebR,
--seviceworkers = {"webr-serviceworker.js"}, -- Kept to avoid error text.
serviceworkers = {"webr-serviceworker.js"}
})
-- If the ChannelType requires service workers, register and copy them into the
-- output directory.
if hasServiceWorkerFiles then
-- Copy the two web workers into the directory
-- https://quarto.org/docs/extensions/lua-api.html#dependencies
quarto.doc.add_html_dependency({
name = "webr-worker",
version = baseVersionWebR,
seviceworkers = {"webr-worker.js"}, -- Kept to avoid error text.
serviceworkers = {"webr-worker.js"}
})

quarto.doc.add_html_dependency({
name = "webr-serviceworker",
version = baseVersionWebR,
seviceworkers = {"webr-serviceworker.js"}, -- Kept to avoid error text.
serviceworkers = {"webr-serviceworker.js"}
})
end

end

-- Define a function to replace keywords given by {{ WORD }}
Expand Down
8 changes: 8 additions & 0 deletions _publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,11 @@
quarto-pub:
- id: 2ae0a31d-b315-4617-a8a4-cda96a9abd3c
url: 'https://coatless.quarto.pub/webr-enabled-code-cells'
- source: webr-channel-type.qmd
quarto-pub:
- id: 9a6782cc-7f9a-4773-a43b-ddeae9944dfb
url: 'https://coatless.quarto.pub/webr-communication-channel-options'
- source: webr-internal-cell.qmd
quarto-pub:
- id: 9a086f67-3a62-4ac6-bd63-a5987751855a
url: 'https://coatless.quarto.pub/hidden-webr-code-cells'
197 changes: 197 additions & 0 deletions webr-channel-type.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
---
title: "webR Communication Channel Options"
subtitle: "Using the `channel-type` meta variable of the `webr` filter"
format:
html:
toc: true
engine: knitr
webr:
channel-type: "post-message"
filters:
- webr
---

# Making R Computations Run Smoothly

As you dive deeper into using webR, it's crucial to grasp how webR manages communication between R and your web browser. Think of it as a conversation between two active workers or "threads":

1. **Web Browser**: This is your web browser, like Chrome or Firefox, which you use to surf the internet and interact with web pages.
2. **webR**: This is like a dedicated helper that specializes in running R code. It operates separately from your main web browser.

Now, here's why this separation is so important: webR's special version of R can tackle complicated and time-consuming calculations without causing your web browser to freeze or become unresponsive when you're on a web page that uses webR.

Imagine trying to watch a video online while your computer is running a heavy software update. Without separating them, your video might start buffering, freeze, or even crash. But by putting the update in the background (on a separate worker, like webR), your video can continue to play smoothly. It's the same concept with webR and your web browser – keeping things running smoothly without hiccups.

For more details, please see the [official webR documentation](https://docs.r-wasm.org/webr/latest/) on [Worker Communication](https://docs.r-wasm.org/webr/latest/communication.html#webr-channels) and [Serving Web Pages with webR](https://docs.r-wasm.org/webr/latest/serving.html).

# Communication Channels: How Web Browser and webR Talk

Now, let's explore how your main web browser and the webR worker thread communicate with each other. They use what we call "communication channels" to swap information. Think of it like like passing notes between two people, but in a high-tech way.

There are a few different types of communication channels available:

1. **"automatic" or `0` (Default):**
- **Requirements:** Your setup should be prepared for either `"shared-array-buffer"` or `"service-worker"`.
- **Limitations:** It's not clear which option is being used.

2. **"shared-array-buffer" or `1`:**
- **Requirements:** This option requires something called "cross-origin isolation," which you can read more about [here](https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated).
- **Limitations:** There aren't any significant limitations to worry about.

3. **"service-worker" or `2`:**
- **Requirements:** You need to have JavaScript service workers set up, like `webr-serviceworker.js` and `webr-worker.js`.
- **Limitations:** The script for the service worker must be on the same website as the one using WebR.

4. **"post-message" or `3`:**
- **Requirements:** No special requirements are needed for this option.
- **Limitations:** R code can't be interrupted easily. Some advanced R features, like nested R REPLs, might not work as expected.

When you start using WebR, it usually picks the best communication channel for you. However, you can manually choose a channel type if needed. Just remember that each option comes with its own set of requirements and limitations, so pick the one that suits your project best.

In simpler terms, it's like choosing the right tool for the job. Depending on what you're doing, you might use different methods to pass messages between your web browser and webR.

## Choosing How WebR Communicates

In a Quarto document's YAML header, you can tell webR which communication channel to use by setting the `channel-type` option. It's like telling webR how you want it to talk with your web browser. For example, if you want to use the `"post-message"` channel, you can do it like this:

```yaml
---
title: "Setting Up webR to use the PostMessage Channel"
format: html
webr:
channel-type: "post-message"
filters:
- webr
---
```

# Your Communication Channel Choices

The remainder of the document describes the different communication methods alongside ways to satisfy the requirements.

## Using "automatic" for Communication (Default)

By default, the `quarto-webr` extension guides webR to use the `"automatic"` option for `channel-type`, if you don't specify the `channel-type` in your document's YAML header. Let's break down how this default setting works:

1. **Communication Attempts:** webR will try two different communication channels in order:
- First, it attempts to establish a communication channel using `"shared-array-buffer"`.
- If that doesn't work, it will then try to use `"service-worker"`.
2. **Fallback Behavior:** If both of these attempts are unsuccessful, webR code cells in your document will be shown in a deactivated state. This means they won't run or execute any R code.

:::callout-note
It's important to note that the `"automatic"` option doesn't try to use the `"post-message"` option for communication.
:::

:::callout-warning
One thing to be aware of is that when the `"automatic"` option is used, it adds two additional files, `webr-serviceworker.js` and `webr-worker.js`, into the output directory. These files must be present alongside the rendered HTML document if you intend to use the `"service-worker"` option. For more details on this, please refer to the `"service-worker"` section of the documentation.
:::

In summary, when you leave the `channel-type` unspecified, webR will follow the `"automatic"` option, attempting to use `"shared-array-buffer"` and then `"service-worker"`. If both attempts fail, your webR code cells will be inactive.

## Using "shared-array-buffer" for Communication

Now, let's explore the `"shared-array-buffer"` option for communication. When you specify `channel-type` as `"shared-array-buffer"`, webR aims to use something called [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer). This choice, however, comes with some specific requirements and benefits:

**Requirements:**
- Your web server needs to send web pages with webR using specific HTTP headers. This is to ensure that the page is [cross-origin isolated](https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated). In simple terms, your server should be set up to allow this kind of communication.

**Benefits:**
- The benefit of using this approach is that webR runs notably faster. It's like giving webR a high-speed lane for its operations.

::: callout-note
It's important to note that the `"shared-array-buffer"` option isn't currently available on platforms like [GitHub Pages](https://pages.github.com/) or [Quarto Pub](https://quartopub.com/). If you're using these services, we recommend using the `channel-type: "post-message"` option instead. There's a possibility that GitHub Pages may offer the option to set the necessary headers in the future, as discussed [here](https://github.com/community/community/discussions/13309).
:::

To help you set up the necessary headers for cross-origin isolation, we provide some guidance for both Netlify and nginx web server administrators:

**For Netlify Configuration:**

If you're hosting your website with [Netlify](https://www.netlify.com/), you can add the following code to your [netlify.toml configuration file](https://docs.netlify.com/routing/headers/#syntax-for-the-netlify-configuration-file):

```toml
[[headers]]
for = "/directory/with/webr/content/*"

[headers.values]
Cross-Origin-Opener-Policy = "same-origin"
Cross-Origin-Embedder-Policy = "require-corp"
```

**For nginx Web Server Administrators:**

If you're managing a server with `nginx`, you can use the [`add_header`](http://nginx.org/en/docs/http/ngx_http_headers_module.html) directive in your server's configuration file, which is usually found at `/etc/nginx/nginx.conf`. Here's an example:

```nginx
server {
# Enable headers for the webr directory
location ^~ /directory/with/webr/content {
add_header "Cross-Origin-Opener-Policy" "same-origin";
add_header "Cross-Origin-Embedder-Policy" "require-corp";
}
}
```

By following these instructions, you'll ensure that your web server is set up to display web pages with a cross-origin isolated status, allowing you to use the `"shared-array-buffer"` option effectively.

## Using "service-worker" for Communication

Here, we'll dive into the `"service-worker"` option for communication.

:::callout-warning
The `"service-worker"` option doesn't work with [Quarto Pub](https://quartopub.com/). If you're hosting documents with Quarto Pub, please use the `channel-type: "post-message"` option instead. There's an ongoing effort to address the service worker upload issue with the Quarto team, which you can track [here](https://github.com/quarto-dev/quarto-cli/issues/6828).
:::

When you set `channel-type` to `"service-worker"`, webR changes its communication channel to use the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). This means you need two worker scripts, `webr-worker.js` and `webr-serviceworker.js`, hosted on the same website as the page using webR.

Here's what you need to ensure:

**1. Worker Scripts:** The `quarto-webr` extension will automatically create and register these workers when your Quarto document is rendered.

**2. Directory Structure:** Your initial directory structure should include these files:

```sh
.
├── _extensions/coatless/quarto-webr
└── demo-quarto-webr.qmd
```

After rendering the Quarto document with the `"service-worker"` option, your directory will look like this:

```sh
.
├── _extensions/coatless/quarto-webr
├── demo-quarto-webr.qmd
├── demo-quarto-webr.html # Rendered document
├── webr-serviceworker.js # Service workers
└── webr-worker.js
```

**3. Hosting:** When hosting your rendered document, you need to make sure the rendered HTML document and the service worker files (`webr-serviceworker.js` and `webr-worker.js`) are present on the server. This is important for everything to work correctly:

```sh
.
├── demo-quarto-webr.html # Rendered document
├── webr-serviceworker.js # Service workers
└── webr-worker.js
```

If you want to change where the service workers are located, you can set the `service-worker-url` option in the document YAML. By default, the rendered document will search for the service workers in its current directory.

In a nutshell, the "service-worker" option is a powerful choice for communication with webR, but you need to ensure the correct setup and hosting to make it work smoothly.


## Using "post-message" for Communication

Now, let's delve into the `"post-message"` option. This sets up a communication channel using something called [Post Message](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage). It's a good choice when you can't use cross-isolation or service workers. This option is simpler to set up and works in more places, like RStudio's preview pane or the built-in browser in VSCode.

However, there are some trade-offs. While it's easier to get started, certain R features that need to pause and wait for input (like `readline()`, `menu()`, `browser()`, etc.) won't work as expected. Instead, they'll return empty values like `""` or `c('', '')`. Also, you won't have the option to stop or interrupt long-running R code. In those cases, you'll need to refresh the page to regain control.

Feel free to experiment with the `"post-message"` interface in this webR code cell:

```{webr-r}
# Attempt to used an input-blocked function
x = readline("What is your favorite color?")
## View the value of x
# print(x)
```
2 changes: 2 additions & 0 deletions webr-internal-cell.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ format:
html:
toc: true
engine: knitr
webr:
channel-type: "post-message"
filters:
- webr
---
Expand Down

0 comments on commit 00c3132

Please sign in to comment.