lucent-pack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lucent_pack/__init__.py +3 -0
- lucent_pack/__main__.py +3 -0
- lucent_pack/build.py +244 -0
- lucent_pack/cli.py +119 -0
- lucent_pack/serve.py +41 -0
- lucent_pack/static/lucent/dist/httpuv-sw.d.ts +2 -0
- lucent_pack/static/lucent/dist/httpuv-sw.js +1478 -0
- lucent_pack/static/lucent/dist/httpuv-sw.js.map +1 -0
- lucent_pack/static/lucent/dist/httpuv-web.d.ts +263 -0
- lucent_pack/static/lucent/dist/httpuv-web.js +794 -0
- lucent_pack/static/lucent/dist/httpuv-web.js.map +1 -0
- lucent_pack/static/lucent/dist/rWasmWorker.d.ts +2 -0
- lucent_pack/static/lucent/dist/rWasmWorker.js +1662 -0
- lucent_pack/static/lucent/dist/rWasmWorker.js.map +1 -0
- lucent_pack/static/lucent/dist/runApp.d.ts +8 -0
- lucent_pack/static/lucent/dist/runApp.js +1056 -0
- lucent_pack/static/lucent/dist/runApp.js.map +1 -0
- lucent_pack/static/lucent/dist/shiny-socket.d.ts +53 -0
- lucent_pack/static/lucent/dist/shiny-socket.js +388 -0
- lucent_pack/static/lucent/dist/shiny-socket.js.map +1 -0
- lucent_pack/templates/favicon.svg +7 -0
- lucent_pack/templates/index.html +96 -0
- lucent_pack/templates/style.css +73 -0
- lucent_pack-0.1.0.dist-info/METADATA +90 -0
- lucent_pack-0.1.0.dist-info/RECORD +28 -0
- lucent_pack-0.1.0.dist-info/WHEEL +4 -0
- lucent_pack-0.1.0.dist-info/entry_points.txt +2 -0
- lucent_pack-0.1.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
// src/httpuv/constants.ts
|
|
2
|
+
var WS_FRAME = {
|
|
3
|
+
SEND: "websocket.send",
|
|
4
|
+
CLOSE: "websocket.close"
|
|
5
|
+
};
|
|
6
|
+
var WASM_R_HOME = "/lib/R";
|
|
7
|
+
var REQUEST_TIMEOUT_MS = 18e4;
|
|
8
|
+
var MSG = {
|
|
9
|
+
REGISTER_HOST: "httpuv_register_host",
|
|
10
|
+
HTTP_REQUEST: "httpuv_http_request",
|
|
11
|
+
HTTP_RESPONSE: "httpuv_http_response",
|
|
12
|
+
WS_PUSH: "httpuv_ws_push",
|
|
13
|
+
/** Iframe → SW (on session port): frame delivered to Shiny.createSocket. */
|
|
14
|
+
WS_PUSH_ACK: "httpuv_ws_push_ack",
|
|
15
|
+
STOP: "httpuv_stop",
|
|
16
|
+
/** Drop cached GET /shiny/ without tearing down the R worker (app restart). */
|
|
17
|
+
CLEAR_APP_CACHE: "httpuv_clear_app_cache",
|
|
18
|
+
/** Host -> SW: full shiny::resourcePaths() map (warmup). */
|
|
19
|
+
REGISTER_RESOURCE_PATHS: "httpuv_register_resource_paths",
|
|
20
|
+
/** Host -> SW: upsert or remove one addResourcePath prefix (empty dir removes). */
|
|
21
|
+
UPDATE_RESOURCE_PATH: "httpuv_update_resource_path",
|
|
22
|
+
/** SW -> host: Comlink to the R worker was lost (e.g. after SW update). */
|
|
23
|
+
REQUEST_COMLINK: "httpuv_request_comlink",
|
|
24
|
+
/** Iframe → SW: transfer a MessagePort for session WS push delivery. */
|
|
25
|
+
REGISTER_SESSION: "httpuv_register_session",
|
|
26
|
+
/** SW → iframe (on session port): registration accepted. */
|
|
27
|
+
SESSION_ACK: "httpuv_session_ack",
|
|
28
|
+
/** SW → iframe client: session MessagePort was lost (e.g. SW idle restart); re-REGISTER_SESSION. */
|
|
29
|
+
REQUEST_SESSION_PORT: "httpuv_request_session_port"
|
|
30
|
+
};
|
|
31
|
+
var WARMUP_REQUEST_HEADER = "X-Shiny-Forge-Warmup";
|
|
32
|
+
var CHANNEL = {
|
|
33
|
+
HTTP_REQUEST: "httpuv_http_request",
|
|
34
|
+
TCP_RESPONSE: "httpuv_tcp_response",
|
|
35
|
+
WS_OPEN: "httpuv_ws_open",
|
|
36
|
+
WS_MESSAGE: "httpuv_ws_message",
|
|
37
|
+
WS_CLOSE: "httpuv_ws_close",
|
|
38
|
+
WS_RESPONSE: "httpuv_ws_response",
|
|
39
|
+
STDIN: "stdin"
|
|
40
|
+
};
|
|
41
|
+
var HTTPUV_OPTIONS = {
|
|
42
|
+
ON_REQUEST: "httpuv_onRequest",
|
|
43
|
+
ON_WS_OPEN: "httpuv_onWSOpen",
|
|
44
|
+
ON_WS_MESSAGE: "httpuv_onWSMessage",
|
|
45
|
+
ON_WS_CLOSE: "httpuv_onWSClose"
|
|
46
|
+
};
|
|
47
|
+
var COMLINK = {
|
|
48
|
+
PORT_HANDOFF: "httpuv_comlink_port",
|
|
49
|
+
/**
|
|
50
|
+
* SW -> host: the handoff port arrived. Sent before the Comlink round-trip so
|
|
51
|
+
* the host can distinguish a dropped handoff (worker evicted mid-boot) from an
|
|
52
|
+
* R worker that never finished setup.
|
|
53
|
+
*/
|
|
54
|
+
PORT_HANDOFF_ACK: "httpuv_comlink_port_ack"
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/httpuv/debug.ts
|
|
58
|
+
function isHttpuvDebug() {
|
|
59
|
+
if (globalThis.__HTTPUV_DEBUG__) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
if (typeof location !== "undefined") {
|
|
64
|
+
const params = new URLSearchParams(location.search);
|
|
65
|
+
if (params.has("httpuvDebug") || params.get("debug") === "httpuv") {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem("shinyForgeDebug") === "1") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (typeof self !== "undefined" && self.location?.href) {
|
|
73
|
+
const params = new URL(self.location.href).searchParams;
|
|
74
|
+
if (params.has("httpuvDebug") || params.get("debug") === "httpuv") {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function httpuvDebugLog(stage, ...args) {
|
|
83
|
+
if (!isHttpuvDebug()) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.info(`[httpuv-debug:${stage}]`, ...args);
|
|
87
|
+
}
|
|
88
|
+
function enableHttpuvDebug() {
|
|
89
|
+
globalThis.__HTTPUV_DEBUG__ = true;
|
|
90
|
+
try {
|
|
91
|
+
localStorage.setItem("shinyForgeDebug", "1");
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
console.info(
|
|
95
|
+
"[httpuv-debug] enabled - reload or run shinyForge.enableHttpuvDebug() then re-run app"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/httpuv/prefix.ts
|
|
100
|
+
var shinyPrefix = null;
|
|
101
|
+
function resolveShinyPrefix(fromUrl) {
|
|
102
|
+
const prefix = new URL("shiny/", fromUrl).pathname;
|
|
103
|
+
return prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
104
|
+
}
|
|
105
|
+
function setShinyPrefix(prefix) {
|
|
106
|
+
shinyPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
107
|
+
}
|
|
108
|
+
function getShinyPrefix() {
|
|
109
|
+
if (!shinyPrefix) {
|
|
110
|
+
throw new Error("Shiny prefix not initialized");
|
|
111
|
+
}
|
|
112
|
+
return shinyPrefix;
|
|
113
|
+
}
|
|
114
|
+
var SESSION_DIR = "__session__";
|
|
115
|
+
var HOST_DIR = "__host__";
|
|
116
|
+
function normalizeSessionHandle(handle) {
|
|
117
|
+
let s = String(handle ?? "").trim();
|
|
118
|
+
while (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
119
|
+
s = s.slice(1, -1);
|
|
120
|
+
}
|
|
121
|
+
return s;
|
|
122
|
+
}
|
|
123
|
+
function resolveSessionPrefix(fromUrl) {
|
|
124
|
+
return `${resolveShinyPrefix(fromUrl)}${SESSION_DIR}/`;
|
|
125
|
+
}
|
|
126
|
+
function getSessionPrefix() {
|
|
127
|
+
return `${getShinyPrefix()}${SESSION_DIR}/`;
|
|
128
|
+
}
|
|
129
|
+
function parseSessionAction(urlString, prefix) {
|
|
130
|
+
const url = new URL(urlString);
|
|
131
|
+
const sessionPrefix = `${prefix}${SESSION_DIR}/`;
|
|
132
|
+
if (!url.pathname.startsWith(sessionPrefix)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const action = url.pathname.slice(sessionPrefix.length).replace(/\/$/, "");
|
|
136
|
+
if (!["open", "send", "recv", "close"].includes(action)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const rawHandle = url.searchParams.get("handle");
|
|
140
|
+
return {
|
|
141
|
+
action,
|
|
142
|
+
handle: rawHandle ? normalizeSessionHandle(rawHandle) : null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function isSessionHttpRequest(urlString, prefix) {
|
|
146
|
+
if (prefix) {
|
|
147
|
+
const session = parseSessionAction(urlString, prefix);
|
|
148
|
+
return session !== null && session.action !== "recv";
|
|
149
|
+
}
|
|
150
|
+
return /\/__session__\/(open|send|close)(?:\?|$|\/)/.test(urlString);
|
|
151
|
+
}
|
|
152
|
+
function isHostPushUrl(urlString, prefix) {
|
|
153
|
+
const url = new URL(urlString);
|
|
154
|
+
return url.pathname === `${prefix}${HOST_DIR}/push`;
|
|
155
|
+
}
|
|
156
|
+
function shinyAppUrl(subpath = "", fromUrl = import.meta.url) {
|
|
157
|
+
const base = new URL("shiny/", fromUrl);
|
|
158
|
+
return new URL(subpath.replace(/^\//, ""), base).href;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/httpuv/bridge.ts
|
|
162
|
+
var invokeROption = null;
|
|
163
|
+
var pushToR = null;
|
|
164
|
+
function decodeBase64Body(data) {
|
|
165
|
+
const text = String(data ?? "");
|
|
166
|
+
const binary = atob(text);
|
|
167
|
+
const bytes = new Uint8Array(binary.length);
|
|
168
|
+
for (let i = 0; i < binary.length; i++) {
|
|
169
|
+
bytes[i] = binary.charCodeAt(i);
|
|
170
|
+
}
|
|
171
|
+
return bytes.buffer;
|
|
172
|
+
}
|
|
173
|
+
function encodeResponseBody(body) {
|
|
174
|
+
if (body == null) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (typeof body === "object" && !Array.isArray(body) && body.httpuvRaw === "base64") {
|
|
178
|
+
return decodeBase64Body(body.data);
|
|
179
|
+
}
|
|
180
|
+
if (typeof body === "string") {
|
|
181
|
+
return body;
|
|
182
|
+
}
|
|
183
|
+
if (body instanceof ArrayBuffer) {
|
|
184
|
+
return body;
|
|
185
|
+
}
|
|
186
|
+
if (ArrayBuffer.isView(body)) {
|
|
187
|
+
return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(body)) {
|
|
190
|
+
return new Uint8Array(body).buffer;
|
|
191
|
+
}
|
|
192
|
+
return String(body);
|
|
193
|
+
}
|
|
194
|
+
function normalizeHeaders(headers) {
|
|
195
|
+
if (!headers || typeof headers !== "object") {
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
const out = {};
|
|
199
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
200
|
+
if (value != null) {
|
|
201
|
+
out[key] = String(value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
function shinyPathInfo(pathname) {
|
|
207
|
+
const shinyPrefix2 = getShinyPrefix();
|
|
208
|
+
if (!pathname.startsWith(shinyPrefix2)) {
|
|
209
|
+
return pathname || "/";
|
|
210
|
+
}
|
|
211
|
+
const rest = pathname.slice(shinyPrefix2.length).replace(/^\/+/, "");
|
|
212
|
+
return rest ? `/${rest}` : "/";
|
|
213
|
+
}
|
|
214
|
+
function buildReq(msg) {
|
|
215
|
+
const url = new URL(msg.url);
|
|
216
|
+
const pathInfo = shinyPathInfo(url.pathname);
|
|
217
|
+
const queryString = url.search.length > 1 ? url.search.slice(1) : "";
|
|
218
|
+
const shinyPrefix2 = getShinyPrefix();
|
|
219
|
+
const req = {
|
|
220
|
+
UUID: msg.uuid,
|
|
221
|
+
REQUEST_METHOD: msg.method,
|
|
222
|
+
SCRIPT_NAME: shinyPrefix2.replace(/\/$/, ""),
|
|
223
|
+
PATH_INFO: pathInfo,
|
|
224
|
+
QUERY_STRING: queryString,
|
|
225
|
+
"rook.version": "1.1-0",
|
|
226
|
+
"rook.url_scheme": url.protocol === "https:" ? "https" : "http",
|
|
227
|
+
SERVER_NAME: url.hostname,
|
|
228
|
+
SERVER_PORT: url.port || (url.protocol === "https:" ? "443" : "80"),
|
|
229
|
+
REMOTE_ADDR: "127.0.0.1",
|
|
230
|
+
REMOTE_PORT: "0",
|
|
231
|
+
HEADERS: { ...msg.headers ?? {} }
|
|
232
|
+
};
|
|
233
|
+
for (const [key, value] of Object.entries(msg.headers ?? {})) {
|
|
234
|
+
req[`HTTP_${key.toUpperCase().replace(/-/g, "_")}`] = value;
|
|
235
|
+
}
|
|
236
|
+
if (msg.body) {
|
|
237
|
+
req.body = msg.body;
|
|
238
|
+
req.CONTENT_LENGTH = String(msg.body.byteLength);
|
|
239
|
+
}
|
|
240
|
+
return req;
|
|
241
|
+
}
|
|
242
|
+
function shinySocketScriptUrl() {
|
|
243
|
+
return new URL("./shiny-socket.js", import.meta.url).pathname;
|
|
244
|
+
}
|
|
245
|
+
function injectShinySocketBootstrap(html) {
|
|
246
|
+
const prefix = getShinyPrefix();
|
|
247
|
+
const baseTag = html.includes("<base") ? "" : `<base href="${prefix}">`;
|
|
248
|
+
const tag = `<script type="module" src="${shinySocketScriptUrl()}"><\/script>`;
|
|
249
|
+
const injection = [baseTag, tag].filter(Boolean).join("\n ");
|
|
250
|
+
if (html.includes("<head")) {
|
|
251
|
+
return html.replace(/<head([^>]*)>/, `<head$1>
|
|
252
|
+
${injection}`);
|
|
253
|
+
}
|
|
254
|
+
if (html.includes("<body")) {
|
|
255
|
+
return html.replace(/<body([^>]*)>/, `<body$1>
|
|
256
|
+
${tag}`);
|
|
257
|
+
}
|
|
258
|
+
return `${tag}
|
|
259
|
+
${html}`;
|
|
260
|
+
}
|
|
261
|
+
function bodyToText(body) {
|
|
262
|
+
if (body == null) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (typeof body === "string") {
|
|
266
|
+
return body;
|
|
267
|
+
}
|
|
268
|
+
if (body instanceof ArrayBuffer) {
|
|
269
|
+
return new TextDecoder().decode(body);
|
|
270
|
+
}
|
|
271
|
+
if (ArrayBuffer.isView(body)) {
|
|
272
|
+
return new TextDecoder().decode(body);
|
|
273
|
+
}
|
|
274
|
+
if (Array.isArray(body)) {
|
|
275
|
+
return new TextDecoder().decode(new Uint8Array(body));
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
function maybeInjectShinySocketBootstrap(body, headers) {
|
|
280
|
+
const contentType = Object.entries(headers).find(([key]) => key.toLowerCase() === "content-type")?.[1] ?? "";
|
|
281
|
+
const normalized = encodeResponseBody(body);
|
|
282
|
+
if (!contentType.includes("text/html")) {
|
|
283
|
+
return normalized;
|
|
284
|
+
}
|
|
285
|
+
const html = bodyToText(normalized);
|
|
286
|
+
if (!html) {
|
|
287
|
+
return normalized;
|
|
288
|
+
}
|
|
289
|
+
return injectShinySocketBootstrap(html);
|
|
290
|
+
}
|
|
291
|
+
function sessionMessageFromBody(body) {
|
|
292
|
+
if (!body || body.byteLength === 0) {
|
|
293
|
+
return { binary: false, message: "" };
|
|
294
|
+
}
|
|
295
|
+
const bytes = new Uint8Array(body);
|
|
296
|
+
const isText = bytes.every((b) => b === 9 || b === 10 || b === 13 || b >= 32 && b <= 126);
|
|
297
|
+
if (isText) {
|
|
298
|
+
return { binary: false, message: new TextDecoder().decode(body) };
|
|
299
|
+
}
|
|
300
|
+
return { binary: true, message: body };
|
|
301
|
+
}
|
|
302
|
+
function handleSessionHttp(msg, session) {
|
|
303
|
+
switch (session.action) {
|
|
304
|
+
case "open": {
|
|
305
|
+
const handle = normalizeSessionHandle(crypto.randomUUID());
|
|
306
|
+
const req = buildReq(msg);
|
|
307
|
+
pushInboundChannelMessage({ type: CHANNEL.WS_OPEN, handle, req });
|
|
308
|
+
sendTcpResponse(
|
|
309
|
+
msg.uuid ?? "",
|
|
310
|
+
200,
|
|
311
|
+
{ "Content-Type": "application/json" },
|
|
312
|
+
JSON.stringify({ handle })
|
|
313
|
+
);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "send": {
|
|
317
|
+
if (!session.handle) {
|
|
318
|
+
sendTcpResponse(msg.uuid ?? "", 400, { "Content-Type": "text/plain" }, "missing handle");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const { binary, message } = sessionMessageFromBody(msg.body);
|
|
322
|
+
pushInboundChannelMessage({
|
|
323
|
+
type: CHANNEL.WS_MESSAGE,
|
|
324
|
+
handle: normalizeSessionHandle(session.handle),
|
|
325
|
+
binary,
|
|
326
|
+
message
|
|
327
|
+
});
|
|
328
|
+
sendTcpResponse(msg.uuid ?? "", 204, {}, null);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
case "close": {
|
|
332
|
+
if (!session.handle) {
|
|
333
|
+
sendTcpResponse(msg.uuid ?? "", 400, { "Content-Type": "text/plain" }, "missing handle");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
pushInboundChannelMessage({
|
|
337
|
+
type: CHANNEL.WS_CLOSE,
|
|
338
|
+
handle: normalizeSessionHandle(session.handle)
|
|
339
|
+
});
|
|
340
|
+
sendTcpResponse(msg.uuid ?? "", 204, {}, null);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
default:
|
|
344
|
+
sendTcpResponse(
|
|
345
|
+
msg.uuid ?? "",
|
|
346
|
+
404,
|
|
347
|
+
{ "Content-Type": "text/plain" },
|
|
348
|
+
"unknown session action"
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function sendTcpResponse(uuid, status, headers, body) {
|
|
353
|
+
getChannel().write({
|
|
354
|
+
type: CHANNEL.TCP_RESPONSE,
|
|
355
|
+
uuid,
|
|
356
|
+
data: {
|
|
357
|
+
status,
|
|
358
|
+
headers,
|
|
359
|
+
body
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function getServiceWorkerController() {
|
|
364
|
+
return navigator.serviceWorker.controller;
|
|
365
|
+
}
|
|
366
|
+
function serializeBodyForChannel(body) {
|
|
367
|
+
if (!body) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
return Array.from(new Uint8Array(body));
|
|
371
|
+
}
|
|
372
|
+
function formatOutboundForHost(msg, deliver) {
|
|
373
|
+
if (msg.type === CHANNEL.TCP_RESPONSE) {
|
|
374
|
+
const headers = normalizeHeaders(msg.data?.headers);
|
|
375
|
+
let body2;
|
|
376
|
+
try {
|
|
377
|
+
body2 = maybeInjectShinySocketBootstrap(msg.data?.body, headers);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
console.warn("[httpuv-bridge] shiny socket bootstrap injection failed", err);
|
|
380
|
+
body2 = encodeResponseBody(msg.data?.body);
|
|
381
|
+
}
|
|
382
|
+
const transfer2 = body2 instanceof ArrayBuffer ? [body2] : [];
|
|
383
|
+
deliver(
|
|
384
|
+
{
|
|
385
|
+
type: MSG.HTTP_RESPONSE,
|
|
386
|
+
uuid: msg.uuid,
|
|
387
|
+
status: msg.data?.status ?? 500,
|
|
388
|
+
headers,
|
|
389
|
+
body: body2
|
|
390
|
+
},
|
|
391
|
+
transfer2
|
|
392
|
+
);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const body = encodeResponseBody(msg.data?.message);
|
|
396
|
+
const transfer = body instanceof ArrayBuffer ? [body] : [];
|
|
397
|
+
if (msg.type === CHANNEL.WS_RESPONSE) {
|
|
398
|
+
deliver(
|
|
399
|
+
{
|
|
400
|
+
type: MSG.WS_PUSH,
|
|
401
|
+
handle: normalizeSessionHandle(msg.data?.handle),
|
|
402
|
+
binary: msg.data?.binary ?? false,
|
|
403
|
+
wsType: msg.data?.type ?? WS_FRAME.SEND,
|
|
404
|
+
message: body
|
|
405
|
+
},
|
|
406
|
+
transfer
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function postToServiceWorker(msg) {
|
|
411
|
+
const controller = getServiceWorkerController();
|
|
412
|
+
formatOutboundForHost(msg, (outbound, transfer = []) => {
|
|
413
|
+
if (msg.type === CHANNEL.TCP_RESPONSE) {
|
|
414
|
+
if (controller) {
|
|
415
|
+
controller.postMessage(outbound, transfer);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
void fetch(shinyAppUrl("__host__/push"), {
|
|
419
|
+
method: "POST",
|
|
420
|
+
headers: { "Content-Type": "application/json" },
|
|
421
|
+
body: JSON.stringify(outbound)
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (msg.type === CHANNEL.WS_RESPONSE) {
|
|
426
|
+
if (controller) {
|
|
427
|
+
controller.postMessage(outbound, transfer);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const outboundMsg = outbound;
|
|
431
|
+
void fetch(shinyAppUrl("__host__/push"), {
|
|
432
|
+
method: "POST",
|
|
433
|
+
headers: { "Content-Type": "application/json" },
|
|
434
|
+
body: JSON.stringify({
|
|
435
|
+
...outbound,
|
|
436
|
+
message: typeof outboundMsg.message === "string" ? outboundMsg.message : void 0
|
|
437
|
+
})
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
var postOutbound = null;
|
|
443
|
+
var deferredOutbound = [];
|
|
444
|
+
function flushDeferredOutbound() {
|
|
445
|
+
if (deferredOutbound.length === 0) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const batch = deferredOutbound.splice(0, deferredOutbound.length);
|
|
449
|
+
for (const msg of batch) {
|
|
450
|
+
postOutboundSync(msg);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function postOutboundMaybeDefer(msg) {
|
|
454
|
+
const depth = globalThis.Module?._rWasmEvalDepth ?? 0;
|
|
455
|
+
if (depth > 0) {
|
|
456
|
+
deferredOutbound.push(msg);
|
|
457
|
+
if (msg.type === CHANNEL.TCP_RESPONSE) {
|
|
458
|
+
httpuvDebugLog("channel-tcp-response-deferred", {
|
|
459
|
+
uuid: msg.uuid,
|
|
460
|
+
status: msg.data?.status,
|
|
461
|
+
depth
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (msg.type === CHANNEL.TCP_RESPONSE) {
|
|
467
|
+
httpuvDebugLog("channel-tcp-response", {
|
|
468
|
+
uuid: msg.uuid,
|
|
469
|
+
status: msg.data?.status
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
postOutboundSync(msg);
|
|
473
|
+
}
|
|
474
|
+
function postOutboundSync(msg) {
|
|
475
|
+
if (postOutbound) {
|
|
476
|
+
formatOutboundForHost(msg, (outbound, transfer = []) => {
|
|
477
|
+
postOutbound?.(outbound, transfer);
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
void postToServiceWorker(msg);
|
|
482
|
+
}
|
|
483
|
+
function createChannel() {
|
|
484
|
+
const inbox = [];
|
|
485
|
+
return {
|
|
486
|
+
inbox,
|
|
487
|
+
hasMessage() {
|
|
488
|
+
return inbox.length > 0;
|
|
489
|
+
},
|
|
490
|
+
read() {
|
|
491
|
+
return inbox.shift() ?? { type: CHANNEL.STDIN };
|
|
492
|
+
},
|
|
493
|
+
write(msg) {
|
|
494
|
+
if (msg.type === CHANNEL.TCP_RESPONSE || msg.type === CHANNEL.WS_RESPONSE) {
|
|
495
|
+
postOutboundMaybeDefer(msg);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
console.warn(
|
|
499
|
+
"[httpuv-bridge] inbound channel.write is deprecated; use pushInboundChannelMessage",
|
|
500
|
+
msg.type
|
|
501
|
+
);
|
|
502
|
+
pushInboundChannelMessage(msg);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function dispatch(msg) {
|
|
507
|
+
if (!msg || typeof msg !== "object" || typeof msg.type !== "string") {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
switch (msg.type) {
|
|
511
|
+
case CHANNEL.HTTP_REQUEST: {
|
|
512
|
+
const req = buildReq(msg);
|
|
513
|
+
const handled = invokeROption?.(HTTPUV_OPTIONS.ON_REQUEST, req) ?? false;
|
|
514
|
+
if (!handled) {
|
|
515
|
+
sendTcpResponse(
|
|
516
|
+
msg.uuid ?? "",
|
|
517
|
+
503,
|
|
518
|
+
{ "Content-Type": "text/plain" },
|
|
519
|
+
"httpuv: no R handler registered"
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case CHANNEL.WS_OPEN: {
|
|
525
|
+
const handled = invokeROption?.(HTTPUV_OPTIONS.ON_WS_OPEN, msg.handle, msg.req) ?? false;
|
|
526
|
+
if (!handled) {
|
|
527
|
+
httpuvDebugLog("bridge-ws-open-no-handler", msg.handle);
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case CHANNEL.WS_MESSAGE: {
|
|
532
|
+
const handled = invokeROption?.(
|
|
533
|
+
HTTPUV_OPTIONS.ON_WS_MESSAGE,
|
|
534
|
+
msg.handle,
|
|
535
|
+
msg.binary,
|
|
536
|
+
msg.message
|
|
537
|
+
) ?? false;
|
|
538
|
+
if (!handled) {
|
|
539
|
+
httpuvDebugLog("bridge-ws-message-no-handler", msg.handle);
|
|
540
|
+
getChannel().write({
|
|
541
|
+
type: CHANNEL.WS_RESPONSE,
|
|
542
|
+
data: {
|
|
543
|
+
handle: msg.handle,
|
|
544
|
+
binary: msg.binary,
|
|
545
|
+
type: WS_FRAME.SEND,
|
|
546
|
+
message: msg.message
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
case CHANNEL.WS_CLOSE: {
|
|
553
|
+
invokeROption?.(HTTPUV_OPTIONS.ON_WS_CLOSE, msg.handle);
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
default:
|
|
557
|
+
console.warn("[httpuv-bridge] unhandled channel message", msg.type);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function drainInboundChannel() {
|
|
561
|
+
const channel = getChannel();
|
|
562
|
+
while (channel.hasMessage()) {
|
|
563
|
+
const msg = channel.read();
|
|
564
|
+
if (msg.type === CHANNEL.STDIN) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
dispatch(msg);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function getChannel() {
|
|
571
|
+
const httpuv = ensureModuleHttpuv();
|
|
572
|
+
if (!httpuv.channel) {
|
|
573
|
+
httpuv.channel = createChannel();
|
|
574
|
+
}
|
|
575
|
+
return httpuv.channel;
|
|
576
|
+
}
|
|
577
|
+
function ensureModuleHttpuv() {
|
|
578
|
+
globalThis.Module = globalThis.Module ?? {};
|
|
579
|
+
globalThis.Module.httpuv = globalThis.Module.httpuv ?? {};
|
|
580
|
+
return globalThis.Module.httpuv;
|
|
581
|
+
}
|
|
582
|
+
function pushInboundChannelMessage(msg) {
|
|
583
|
+
if (!pushToR) {
|
|
584
|
+
console.warn("[httpuv-bridge] pushToR not configured");
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
pushToR(msg);
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
function pushInboundHostMessage(msg) {
|
|
591
|
+
if (!msg || typeof msg !== "object") {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
switch (msg.type) {
|
|
595
|
+
case MSG.HTTP_REQUEST: {
|
|
596
|
+
const session = parseSessionAction(msg.url ?? "", getShinyPrefix());
|
|
597
|
+
if (session && session.action !== "recv") {
|
|
598
|
+
httpuvDebugLog("bridge-inbound-session", {
|
|
599
|
+
uuid: msg.uuid,
|
|
600
|
+
action: session.action,
|
|
601
|
+
handle: session.handle
|
|
602
|
+
});
|
|
603
|
+
handleSessionHttp(msg, session);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
httpuvDebugLog("bridge-push-http", {
|
|
607
|
+
uuid: msg.uuid,
|
|
608
|
+
method: msg.method,
|
|
609
|
+
url: msg.url
|
|
610
|
+
});
|
|
611
|
+
pushInboundChannelMessage({
|
|
612
|
+
type: CHANNEL.HTTP_REQUEST,
|
|
613
|
+
uuid: msg.uuid,
|
|
614
|
+
method: msg.method,
|
|
615
|
+
url: msg.url,
|
|
616
|
+
headers: msg.headers ?? {},
|
|
617
|
+
body: serializeBodyForChannel(msg.body),
|
|
618
|
+
clientId: msg.clientId
|
|
619
|
+
});
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case MSG.STOP:
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function handleInboundHostMessage(msg) {
|
|
627
|
+
pushInboundHostMessage(msg);
|
|
628
|
+
}
|
|
629
|
+
function installServiceWorkerListener() {
|
|
630
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
631
|
+
handleInboundHostMessage(event.data);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function installHttpuvBridge(options = {}) {
|
|
635
|
+
if (options.postOutbound) {
|
|
636
|
+
postOutbound = options.postOutbound;
|
|
637
|
+
}
|
|
638
|
+
if (options.pushToR) {
|
|
639
|
+
pushToR = options.pushToR;
|
|
640
|
+
}
|
|
641
|
+
const httpuv = ensureModuleHttpuv();
|
|
642
|
+
if (!httpuv.channel) {
|
|
643
|
+
httpuv.channel = createChannel();
|
|
644
|
+
}
|
|
645
|
+
httpuv.dispatch = dispatch;
|
|
646
|
+
httpuv.drainInboundChannel = drainInboundChannel;
|
|
647
|
+
httpuv.pushInboundChannelMessage = pushInboundChannelMessage;
|
|
648
|
+
httpuv.pushInboundHostMessage = pushInboundHostMessage;
|
|
649
|
+
httpuv.buildReq = buildReq;
|
|
650
|
+
httpuv.injectShinySocketBootstrap = injectShinySocketBootstrap;
|
|
651
|
+
httpuv.shinySocketScriptUrl = shinySocketScriptUrl;
|
|
652
|
+
httpuv.shinyPrefix = getShinyPrefix();
|
|
653
|
+
if (options.requestHostService) {
|
|
654
|
+
httpuv.requestHostService = options.requestHostService;
|
|
655
|
+
}
|
|
656
|
+
if (options.scheduleHostDelay) {
|
|
657
|
+
httpuv.scheduleHostDelay = options.scheduleHostDelay;
|
|
658
|
+
}
|
|
659
|
+
httpuv.pushWsMessage = (handle, message, opts = {}) => {
|
|
660
|
+
getChannel().write({
|
|
661
|
+
type: CHANNEL.WS_RESPONSE,
|
|
662
|
+
data: {
|
|
663
|
+
handle,
|
|
664
|
+
binary: opts.binary ?? false,
|
|
665
|
+
type: opts.wsType ?? WS_FRAME.SEND,
|
|
666
|
+
message
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
httpuv.bindInvokeROption = (fn) => {
|
|
671
|
+
invokeROption = fn;
|
|
672
|
+
};
|
|
673
|
+
const installSwListener = options.installSwListener ?? true;
|
|
674
|
+
if (installSwListener && !httpuv._swListenerInstalled) {
|
|
675
|
+
installServiceWorkerListener();
|
|
676
|
+
httpuv._swListenerInstalled = true;
|
|
677
|
+
}
|
|
678
|
+
httpuvDebugLog("bridge-installed");
|
|
679
|
+
return httpuv;
|
|
680
|
+
}
|
|
681
|
+
function setInvokeROption(fn) {
|
|
682
|
+
invokeROption = fn;
|
|
683
|
+
}
|
|
684
|
+
function setPushToR(fn) {
|
|
685
|
+
pushToR = fn;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/httpuv/r-push.ts
|
|
689
|
+
function jsonForR(value) {
|
|
690
|
+
return JSON.stringify(JSON.stringify(value));
|
|
691
|
+
}
|
|
692
|
+
function serializeReqForR(req) {
|
|
693
|
+
const out = { ...req };
|
|
694
|
+
const body = out.body;
|
|
695
|
+
if (body instanceof ArrayBuffer) {
|
|
696
|
+
out.body = Array.from(new Uint8Array(body));
|
|
697
|
+
} else if (ArrayBuffer.isView(body)) {
|
|
698
|
+
out.body = Array.from(
|
|
699
|
+
new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
function encodeWsPayloadBytes(message, binary) {
|
|
705
|
+
if (binary) {
|
|
706
|
+
if (message instanceof ArrayBuffer) {
|
|
707
|
+
return Array.from(new Uint8Array(message));
|
|
708
|
+
}
|
|
709
|
+
if (ArrayBuffer.isView(message)) {
|
|
710
|
+
return Array.from(new Uint8Array(message.buffer, message.byteOffset, message.byteLength));
|
|
711
|
+
}
|
|
712
|
+
if (Array.isArray(message)) {
|
|
713
|
+
return message;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return Array.from(new TextEncoder().encode(String(message ?? "")));
|
|
717
|
+
}
|
|
718
|
+
function isLikelyStaticAsset(url) {
|
|
719
|
+
return /\.(js|css|png|jpe?g|gif|svg|woff2?|ico|map)(\?|$)/i.test(url) || /\/shiny\/(shared|jquery|bootstrap|htmltools|shiny-)/i.test(url);
|
|
720
|
+
}
|
|
721
|
+
function channelMessageToRExpr(msg) {
|
|
722
|
+
switch (msg.type) {
|
|
723
|
+
case CHANNEL.HTTP_REQUEST: {
|
|
724
|
+
const payload = {
|
|
725
|
+
uuid: msg.uuid,
|
|
726
|
+
method: msg.method,
|
|
727
|
+
url: msg.url,
|
|
728
|
+
headers: msg.headers ?? {},
|
|
729
|
+
body: msg.body ?? null
|
|
730
|
+
};
|
|
731
|
+
const msgJson = jsonForR(payload);
|
|
732
|
+
return `local({
|
|
733
|
+
msg <- jsonlite::fromJSON(${msgJson}, simplifyVector=FALSE)
|
|
734
|
+
wrapper <- get("active_app_wrapper", envir=httpuv:::.globals)
|
|
735
|
+
if (is.null(wrapper)) {
|
|
736
|
+
if (!is.null(msg$uuid)) {
|
|
737
|
+
httpuv:::httpuv_write_tcp_response(
|
|
738
|
+
msg$uuid,
|
|
739
|
+
list(
|
|
740
|
+
status = 503L,
|
|
741
|
+
headers = list(\`Content-Type\` = "text/plain"),
|
|
742
|
+
body = "httpuv: no server running"
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
httpuv:::httpuv_handle_http_request(wrapper, msg)
|
|
748
|
+
}
|
|
749
|
+
invisible(TRUE)
|
|
750
|
+
})`;
|
|
751
|
+
}
|
|
752
|
+
case CHANNEL.WS_OPEN: {
|
|
753
|
+
const reqPart = msg.req ? `jsonlite::fromJSON(${jsonForR(serializeReqForR(msg.req))}, simplifyVector=FALSE)` : "NULL";
|
|
754
|
+
return `local({
|
|
755
|
+
wrapper <- get("active_app_wrapper", envir=httpuv:::.globals)
|
|
756
|
+
if (!is.null(wrapper)) {
|
|
757
|
+
req <- ${reqPart}
|
|
758
|
+
wrapper$onWSOpen(${jsonForR(msg.handle)}, httpuv:::httpuv_js_req_to_rook(req))
|
|
759
|
+
}
|
|
760
|
+
invisible(TRUE)
|
|
761
|
+
})`;
|
|
762
|
+
}
|
|
763
|
+
case CHANNEL.WS_MESSAGE: {
|
|
764
|
+
const bytesJson = jsonForR(encodeWsPayloadBytes(msg.message, Boolean(msg.binary)));
|
|
765
|
+
return `local({
|
|
766
|
+
wrapper <- get("active_app_wrapper", envir=httpuv:::.globals)
|
|
767
|
+
if (!is.null(wrapper)) {
|
|
768
|
+
msg_raw <- httpuv:::httpuv_bytes_to_raw(
|
|
769
|
+
jsonlite::fromJSON(${bytesJson}, simplifyVector=FALSE)
|
|
770
|
+
)
|
|
771
|
+
wrapper$onWSMessage(${jsonForR(msg.handle)}, TRUE, msg_raw)
|
|
772
|
+
}
|
|
773
|
+
invisible(TRUE)
|
|
774
|
+
})`;
|
|
775
|
+
}
|
|
776
|
+
case CHANNEL.WS_CLOSE:
|
|
777
|
+
return `local({
|
|
778
|
+
wrapper <- get("active_app_wrapper", envir=httpuv:::.globals)
|
|
779
|
+
if (!is.null(wrapper)) {
|
|
780
|
+
wrapper$onWSClose(${jsonForR(msg.handle)})
|
|
781
|
+
}
|
|
782
|
+
invisible(TRUE)
|
|
783
|
+
})`;
|
|
784
|
+
default:
|
|
785
|
+
throw new Error(`unsupported inbound channel message type: ${msg.type}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/httpuv/index.ts
|
|
790
|
+
var HTTPUV_TRANSPORT_VERSION = "0.0.0";
|
|
791
|
+
|
|
792
|
+
export { CHANNEL, COMLINK, HOST_DIR, HTTPUV_OPTIONS, HTTPUV_TRANSPORT_VERSION, MSG, REQUEST_TIMEOUT_MS, SESSION_DIR, WARMUP_REQUEST_HEADER, WASM_R_HOME, WS_FRAME, buildReq, channelMessageToRExpr, dispatch, drainInboundChannel, enableHttpuvDebug, flushDeferredOutbound, getSessionPrefix, getShinyPrefix, handleInboundHostMessage, httpuvDebugLog, injectShinySocketBootstrap, installHttpuvBridge, isHostPushUrl, isHttpuvDebug, isLikelyStaticAsset, isSessionHttpRequest, jsonForR, normalizeSessionHandle, parseSessionAction, pushInboundChannelMessage, pushInboundHostMessage, resolveSessionPrefix, resolveShinyPrefix, serializeReqForR, setInvokeROption, setPushToR, setShinyPrefix, shinyAppUrl, shinySocketScriptUrl };
|
|
793
|
+
//# sourceMappingURL=httpuv-web.js.map
|
|
794
|
+
//# sourceMappingURL=httpuv-web.js.map
|