-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathshiny.js
172 lines (156 loc) · 5.34 KB
/
shiny.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// Each client session has a unique ID
let clientId;
// Register service worker
navigator.serviceWorker.register('./httpuv-serviceworker.js').then(async (registration)=>{
await navigator.serviceWorker.ready;
// If the service worker is ready, but not in control, reload the main page
if (!navigator.serviceWorker.controller) {
window.location.reload();
}
// Register with the service worker and get our client ID
clientId = await new Promise((resolve) => {
navigator.serviceWorker.addEventListener('message', function listener(event) {
if (event.data.type === 'registration-successful') {
navigator.serviceWorker.removeEventListener('message', listener);
resolve(event.data.clientId);
}
});
registration.active.postMessage({type: "register-client"});
});
console.log('I am client: ', clientId);
console.log("serviceworker proxy is ready");
});
import('https://webr.r-wasm.org/v0.2.2/webr.mjs').then(async ({ WebR }) => {
let webSocketHandleCounter = 0;
let webSocketRefs = {};
// Create a proxy WebSocket class to intercept WebSocket API calls inside the
// Shiny iframe and forward the messages to webR over the communication channel.
class WebSocketProxy {
url;
handle;
bufferedAmount;
readyState;
constructor(_url) {
this.url = _url
this.handle = webSocketHandleCounter++;
this.bufferedAmount = 0;
this.shelter = null;
webSocketRefs[this.handle] = this;
// Trigger the WS onOpen callbacks
webR.evalRVoid(`
onWSOpen <- options('webr_httpuv_onWSOpen')[[1]]
if (!is.null(onWSOpen)) {
onWSOpen(
${this.handle},
list(
handle = ${this.handle}
)
)
}
`)
setTimeout(() => {
this.readyState = 1;
this.onopen()
}, 0);
}
async send(msg) {
// Intercept WS message and send it via the webR channel
webR.evalRVoid(`
onWSMessage <- options('webr_httpuv_onWSMessage')[[1]]
if (!is.null(onWSMessage)) {
onWSMessage(${this.handle}, FALSE, '${msg}')
}
`)
}
}
// Initialise webR with a local package repo
const webR = new WebR();
await webR.init();
console.log("webR init OK");
// Read webR channel for events
(async () => {
for (;;) {
const output = await webR.read();
switch (output.type) {
case 'stdout':
document.getElementById('out').append(output.data + '\n');
document.getElementById('console').scrollTop =
document.getElementById('console').scrollHeight;
break;
case 'stderr':
document.getElementById('out').append(output.data + '\n');
document.getElementById('console').scrollTop =
document.getElementById('console').scrollHeight;
break;
case '_webR_httpuv_TcpResponse':
const registration = await navigator.serviceWorker.getRegistration();
registration.active.postMessage({
type: "wasm-http-response",
uuid: output.uuid,
response: output.data,
});
break;
case '_webR_httpuv_WSResponse':
const event = { data: output.data.message };
webSocketRefs[output.data.handle].onmessage(event);
break;
}
}
})();
// Upload file to webR filesystem
async function fetchToWebR(url, path) {
const req = await fetch(url);
const data = await req.arrayBuffer();
await webR.FS.writeFile(path, new Uint8Array(data));
}
// Setup shiny app on webR VFS
await webR.FS.mkdir('/home/web_user/app');
await webR.FS.mkdir('/home/web_user/app/www');
await fetchToWebR('app/ui.R', '/home/web_user/app/ui.R');
await fetchToWebR('app/server.R', '/home/web_user/app/server.R');
// Install and run shiny
await webR.evalRVoid(`webr::mount("/shiny", "${window.location.href}/image/library.data")`);
webR.writeConsole(`
.libPaths(c("/shiny", .libPaths()))
library(shiny)
options(shiny.trace = TRUE)
runApp('app', display.mode = 'showcase', launch.browser = FALSE)
`);
// Setup listener for service worker messages
navigator.serviceWorker.addEventListener('message', async (event) => {
if (event.data.type === 'wasm-http-fetch') {
var url = new URL(event.data.url);
var pathname = url.pathname.replace(/.*\/__wasm__\/([0-9a-fA-F-]{36})/,"");
var query = url.search.replace(/^\?/, '');
webR.evalRVoid(`
onRequest <- options("webr_httpuv_onRequest")[[1]]
if (!is.null(onRequest)) {
onRequest(
list(
PATH_INFO = "${pathname}",
REQUEST_METHOD = "${event.data.method}",
UUID = "${event.data.uuid}",
QUERY_STRING = "${query}"
)
)
}
`);
}
});
// Load the WASM httpuv hosted page in an iframe
let iframe = document.createElement('iframe');
iframe.id = 'app';
iframe.src = `./__wasm__/${clientId}/`;
iframe.frameBorder = '0';
iframe.style.position = 'fixed';
iframe.style.top = 0;
iframe.style.left = 0;
iframe.style.right = 0;
iframe.style.width = '100%';
iframe.style.height = '80%';
document.body.appendChild(iframe);
// Install the websocket proxy for chatting to httpuv
iframe.contentWindow.WebSocket = WebSocketProxy;
// Hide the loading div
document.getElementById('loading').style.display = "none";
});