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,1662 @@
|
|
|
1
|
+
// node_modules/comlink/dist/esm/comlink.mjs
|
|
2
|
+
var proxyMarker = /* @__PURE__ */ Symbol("Comlink.proxy");
|
|
3
|
+
var createEndpoint = /* @__PURE__ */ Symbol("Comlink.endpoint");
|
|
4
|
+
var releaseProxy = /* @__PURE__ */ Symbol("Comlink.releaseProxy");
|
|
5
|
+
var finalizer = /* @__PURE__ */ Symbol("Comlink.finalizer");
|
|
6
|
+
var throwMarker = /* @__PURE__ */ Symbol("Comlink.thrown");
|
|
7
|
+
var isObject = (val) => typeof val === "object" && val !== null || typeof val === "function";
|
|
8
|
+
var proxyTransferHandler = {
|
|
9
|
+
canHandle: (val) => isObject(val) && val[proxyMarker],
|
|
10
|
+
serialize(obj) {
|
|
11
|
+
const { port1, port2 } = new MessageChannel();
|
|
12
|
+
expose(obj, port1);
|
|
13
|
+
return [port2, [port2]];
|
|
14
|
+
},
|
|
15
|
+
deserialize(port) {
|
|
16
|
+
port.start();
|
|
17
|
+
return wrap(port);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var throwTransferHandler = {
|
|
21
|
+
canHandle: (value) => isObject(value) && throwMarker in value,
|
|
22
|
+
serialize({ value }) {
|
|
23
|
+
let serialized;
|
|
24
|
+
if (value instanceof Error) {
|
|
25
|
+
serialized = {
|
|
26
|
+
isError: true,
|
|
27
|
+
value: {
|
|
28
|
+
message: value.message,
|
|
29
|
+
name: value.name,
|
|
30
|
+
stack: value.stack
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
} else {
|
|
34
|
+
serialized = { isError: false, value };
|
|
35
|
+
}
|
|
36
|
+
return [serialized, []];
|
|
37
|
+
},
|
|
38
|
+
deserialize(serialized) {
|
|
39
|
+
if (serialized.isError) {
|
|
40
|
+
throw Object.assign(new Error(serialized.value.message), serialized.value);
|
|
41
|
+
}
|
|
42
|
+
throw serialized.value;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var transferHandlers = /* @__PURE__ */ new Map([
|
|
46
|
+
["proxy", proxyTransferHandler],
|
|
47
|
+
["throw", throwTransferHandler]
|
|
48
|
+
]);
|
|
49
|
+
function isAllowedOrigin(allowedOrigins, origin) {
|
|
50
|
+
for (const allowedOrigin of allowedOrigins) {
|
|
51
|
+
if (origin === allowedOrigin || allowedOrigin === "*") {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function expose(obj, ep = globalThis, allowedOrigins = ["*"]) {
|
|
61
|
+
ep.addEventListener("message", function callback(ev) {
|
|
62
|
+
if (!ev || !ev.data) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!isAllowedOrigin(allowedOrigins, ev.origin)) {
|
|
66
|
+
console.warn(`Invalid origin '${ev.origin}' for comlink proxy`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { id, type, path } = Object.assign({ path: [] }, ev.data);
|
|
70
|
+
const argumentList = (ev.data.argumentList || []).map(fromWireValue);
|
|
71
|
+
let returnValue;
|
|
72
|
+
try {
|
|
73
|
+
const parent = path.slice(0, -1).reduce((obj2, prop) => obj2[prop], obj);
|
|
74
|
+
const rawValue = path.reduce((obj2, prop) => obj2[prop], obj);
|
|
75
|
+
switch (type) {
|
|
76
|
+
case "GET":
|
|
77
|
+
{
|
|
78
|
+
returnValue = rawValue;
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case "SET":
|
|
82
|
+
{
|
|
83
|
+
parent[path.slice(-1)[0]] = fromWireValue(ev.data.value);
|
|
84
|
+
returnValue = true;
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
case "APPLY":
|
|
88
|
+
{
|
|
89
|
+
returnValue = rawValue.apply(parent, argumentList);
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case "CONSTRUCT":
|
|
93
|
+
{
|
|
94
|
+
const value = new rawValue(...argumentList);
|
|
95
|
+
returnValue = proxy(value);
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
case "ENDPOINT":
|
|
99
|
+
{
|
|
100
|
+
const { port1, port2 } = new MessageChannel();
|
|
101
|
+
expose(obj, port2);
|
|
102
|
+
returnValue = transfer(port1, [port1]);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case "RELEASE":
|
|
106
|
+
{
|
|
107
|
+
returnValue = void 0;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
} catch (value) {
|
|
114
|
+
returnValue = { value, [throwMarker]: 0 };
|
|
115
|
+
}
|
|
116
|
+
Promise.resolve(returnValue).catch((value) => {
|
|
117
|
+
return { value, [throwMarker]: 0 };
|
|
118
|
+
}).then((returnValue2) => {
|
|
119
|
+
const [wireValue, transferables] = toWireValue(returnValue2);
|
|
120
|
+
ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables);
|
|
121
|
+
if (type === "RELEASE") {
|
|
122
|
+
ep.removeEventListener("message", callback);
|
|
123
|
+
closeEndPoint(ep);
|
|
124
|
+
if (finalizer in obj && typeof obj[finalizer] === "function") {
|
|
125
|
+
obj[finalizer]();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}).catch((error) => {
|
|
129
|
+
const [wireValue, transferables] = toWireValue({
|
|
130
|
+
value: new TypeError("Unserializable return value"),
|
|
131
|
+
[throwMarker]: 0
|
|
132
|
+
});
|
|
133
|
+
ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
if (ep.start) {
|
|
137
|
+
ep.start();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function isMessagePort(endpoint) {
|
|
141
|
+
return endpoint.constructor.name === "MessagePort";
|
|
142
|
+
}
|
|
143
|
+
function closeEndPoint(endpoint) {
|
|
144
|
+
if (isMessagePort(endpoint))
|
|
145
|
+
endpoint.close();
|
|
146
|
+
}
|
|
147
|
+
function wrap(ep, target) {
|
|
148
|
+
const pendingListeners = /* @__PURE__ */ new Map();
|
|
149
|
+
ep.addEventListener("message", function handleMessage(ev) {
|
|
150
|
+
const { data } = ev;
|
|
151
|
+
if (!data || !data.id) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const resolver = pendingListeners.get(data.id);
|
|
155
|
+
if (!resolver) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
resolver(data);
|
|
160
|
+
} finally {
|
|
161
|
+
pendingListeners.delete(data.id);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return createProxy(ep, pendingListeners, [], target);
|
|
165
|
+
}
|
|
166
|
+
function throwIfProxyReleased(isReleased) {
|
|
167
|
+
if (isReleased) {
|
|
168
|
+
throw new Error("Proxy has been released and is not useable");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function releaseEndpoint(ep) {
|
|
172
|
+
return requestResponseMessage(ep, /* @__PURE__ */ new Map(), {
|
|
173
|
+
type: "RELEASE"
|
|
174
|
+
}).then(() => {
|
|
175
|
+
closeEndPoint(ep);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
var proxyCounter = /* @__PURE__ */ new WeakMap();
|
|
179
|
+
var proxyFinalizers = "FinalizationRegistry" in globalThis && new FinalizationRegistry((ep) => {
|
|
180
|
+
const newCount = (proxyCounter.get(ep) || 0) - 1;
|
|
181
|
+
proxyCounter.set(ep, newCount);
|
|
182
|
+
if (newCount === 0) {
|
|
183
|
+
releaseEndpoint(ep);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
function registerProxy(proxy2, ep) {
|
|
187
|
+
const newCount = (proxyCounter.get(ep) || 0) + 1;
|
|
188
|
+
proxyCounter.set(ep, newCount);
|
|
189
|
+
if (proxyFinalizers) {
|
|
190
|
+
proxyFinalizers.register(proxy2, ep, proxy2);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function unregisterProxy(proxy2) {
|
|
194
|
+
if (proxyFinalizers) {
|
|
195
|
+
proxyFinalizers.unregister(proxy2);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function createProxy(ep, pendingListeners, path = [], target = function() {
|
|
199
|
+
}) {
|
|
200
|
+
let isProxyReleased = false;
|
|
201
|
+
const proxy2 = new Proxy(target, {
|
|
202
|
+
get(_target, prop) {
|
|
203
|
+
throwIfProxyReleased(isProxyReleased);
|
|
204
|
+
if (prop === releaseProxy) {
|
|
205
|
+
return () => {
|
|
206
|
+
unregisterProxy(proxy2);
|
|
207
|
+
releaseEndpoint(ep);
|
|
208
|
+
pendingListeners.clear();
|
|
209
|
+
isProxyReleased = true;
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (prop === "then") {
|
|
213
|
+
if (path.length === 0) {
|
|
214
|
+
return { then: () => proxy2 };
|
|
215
|
+
}
|
|
216
|
+
const r = requestResponseMessage(ep, pendingListeners, {
|
|
217
|
+
type: "GET",
|
|
218
|
+
path: path.map((p) => p.toString())
|
|
219
|
+
}).then(fromWireValue);
|
|
220
|
+
return r.then.bind(r);
|
|
221
|
+
}
|
|
222
|
+
return createProxy(ep, pendingListeners, [...path, prop]);
|
|
223
|
+
},
|
|
224
|
+
set(_target, prop, rawValue) {
|
|
225
|
+
throwIfProxyReleased(isProxyReleased);
|
|
226
|
+
const [value, transferables] = toWireValue(rawValue);
|
|
227
|
+
return requestResponseMessage(ep, pendingListeners, {
|
|
228
|
+
type: "SET",
|
|
229
|
+
path: [...path, prop].map((p) => p.toString()),
|
|
230
|
+
value
|
|
231
|
+
}, transferables).then(fromWireValue);
|
|
232
|
+
},
|
|
233
|
+
apply(_target, _thisArg, rawArgumentList) {
|
|
234
|
+
throwIfProxyReleased(isProxyReleased);
|
|
235
|
+
const last = path[path.length - 1];
|
|
236
|
+
if (last === createEndpoint) {
|
|
237
|
+
return requestResponseMessage(ep, pendingListeners, {
|
|
238
|
+
type: "ENDPOINT"
|
|
239
|
+
}).then(fromWireValue);
|
|
240
|
+
}
|
|
241
|
+
if (last === "bind") {
|
|
242
|
+
return createProxy(ep, pendingListeners, path.slice(0, -1));
|
|
243
|
+
}
|
|
244
|
+
const [argumentList, transferables] = processArguments(rawArgumentList);
|
|
245
|
+
return requestResponseMessage(ep, pendingListeners, {
|
|
246
|
+
type: "APPLY",
|
|
247
|
+
path: path.map((p) => p.toString()),
|
|
248
|
+
argumentList
|
|
249
|
+
}, transferables).then(fromWireValue);
|
|
250
|
+
},
|
|
251
|
+
construct(_target, rawArgumentList) {
|
|
252
|
+
throwIfProxyReleased(isProxyReleased);
|
|
253
|
+
const [argumentList, transferables] = processArguments(rawArgumentList);
|
|
254
|
+
return requestResponseMessage(ep, pendingListeners, {
|
|
255
|
+
type: "CONSTRUCT",
|
|
256
|
+
path: path.map((p) => p.toString()),
|
|
257
|
+
argumentList
|
|
258
|
+
}, transferables).then(fromWireValue);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
registerProxy(proxy2, ep);
|
|
262
|
+
return proxy2;
|
|
263
|
+
}
|
|
264
|
+
function myFlat(arr) {
|
|
265
|
+
return Array.prototype.concat.apply([], arr);
|
|
266
|
+
}
|
|
267
|
+
function processArguments(argumentList) {
|
|
268
|
+
const processed = argumentList.map(toWireValue);
|
|
269
|
+
return [processed.map((v) => v[0]), myFlat(processed.map((v) => v[1]))];
|
|
270
|
+
}
|
|
271
|
+
var transferCache = /* @__PURE__ */ new WeakMap();
|
|
272
|
+
function transfer(obj, transfers) {
|
|
273
|
+
transferCache.set(obj, transfers);
|
|
274
|
+
return obj;
|
|
275
|
+
}
|
|
276
|
+
function proxy(obj) {
|
|
277
|
+
return Object.assign(obj, { [proxyMarker]: true });
|
|
278
|
+
}
|
|
279
|
+
function toWireValue(value) {
|
|
280
|
+
for (const [name, handler] of transferHandlers) {
|
|
281
|
+
if (handler.canHandle(value)) {
|
|
282
|
+
const [serializedValue, transferables] = handler.serialize(value);
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
type: "HANDLER",
|
|
286
|
+
name,
|
|
287
|
+
value: serializedValue
|
|
288
|
+
},
|
|
289
|
+
transferables
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return [
|
|
294
|
+
{
|
|
295
|
+
type: "RAW",
|
|
296
|
+
value
|
|
297
|
+
},
|
|
298
|
+
transferCache.get(value) || []
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
function fromWireValue(value) {
|
|
302
|
+
switch (value.type) {
|
|
303
|
+
case "HANDLER":
|
|
304
|
+
return transferHandlers.get(value.name).deserialize(value.value);
|
|
305
|
+
case "RAW":
|
|
306
|
+
return value.value;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function requestResponseMessage(ep, pendingListeners, msg, transfers) {
|
|
310
|
+
return new Promise((resolve) => {
|
|
311
|
+
const id = generateUUID();
|
|
312
|
+
pendingListeners.set(id, resolve);
|
|
313
|
+
if (ep.start) {
|
|
314
|
+
ep.start();
|
|
315
|
+
}
|
|
316
|
+
ep.postMessage(Object.assign({ id }, msg), transfers);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function generateUUID() {
|
|
320
|
+
return new Array(4).fill(0).map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16)).join("-");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/config.ts
|
|
324
|
+
var DEFAULT_TRANSPORT_BASE_URL = "/lucent/dist/";
|
|
325
|
+
var DEFAULT_SERVICE_WORKER_URL = "/httpuv-sw.js";
|
|
326
|
+
var DEFAULT_R_RUNTIME_BASE_URL = "/runtime/";
|
|
327
|
+
var DEFAULT_EMPACK_META_URL = "/packages/empack_env_meta.json";
|
|
328
|
+
var DEFAULT_SHINY_BASE_URL = "/";
|
|
329
|
+
var LUCENT_CONFIG_PARAM = "lucentConfig";
|
|
330
|
+
function configOverridesFromUrl() {
|
|
331
|
+
try {
|
|
332
|
+
const search = globalThis.location?.search;
|
|
333
|
+
if (!search) {
|
|
334
|
+
return {};
|
|
335
|
+
}
|
|
336
|
+
const raw = new URLSearchParams(search).get(LUCENT_CONFIG_PARAM);
|
|
337
|
+
if (!raw) {
|
|
338
|
+
return {};
|
|
339
|
+
}
|
|
340
|
+
const parsed = JSON.parse(raw);
|
|
341
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
342
|
+
return parsed;
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
return {};
|
|
347
|
+
}
|
|
348
|
+
function resolveAgainstLocation(url) {
|
|
349
|
+
const base = typeof self !== "undefined" && self.location?.href ? self.location.href : "http://localhost/";
|
|
350
|
+
return new URL(url, base);
|
|
351
|
+
}
|
|
352
|
+
function resolveLucentConfig(overrides = {}) {
|
|
353
|
+
const fromGlobal = globalThis.__LUCENT__ ?? {};
|
|
354
|
+
const fromUrl = configOverridesFromUrl();
|
|
355
|
+
const pick = (key) => overrides[key] ?? fromUrl[key] ?? fromGlobal[key];
|
|
356
|
+
const empackMetaUrl = pick("empackMetaUrl") ?? DEFAULT_EMPACK_META_URL;
|
|
357
|
+
const empackPackagesBaseUrl = pick("empackPackagesBaseUrl") ?? new URL(".", resolveAgainstLocation(empackMetaUrl)).href;
|
|
358
|
+
return {
|
|
359
|
+
transportBaseUrl: pick("transportBaseUrl") ?? DEFAULT_TRANSPORT_BASE_URL,
|
|
360
|
+
serviceWorkerUrl: pick("serviceWorkerUrl") ?? DEFAULT_SERVICE_WORKER_URL,
|
|
361
|
+
rRuntimeBaseUrl: pick("rRuntimeBaseUrl") ?? DEFAULT_R_RUNTIME_BASE_URL,
|
|
362
|
+
empackMetaUrl,
|
|
363
|
+
empackPackagesBaseUrl,
|
|
364
|
+
shinyBaseUrl: pick("shinyBaseUrl") ?? DEFAULT_SHINY_BASE_URL
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/debug.ts
|
|
369
|
+
function isLucentDebug() {
|
|
370
|
+
if (globalThis.__HTTPUV_DEBUG__) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem("shinyForgeDebug") === "1") {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
const loc = typeof location !== "undefined" ? location : typeof self !== "undefined" && "location" in self ? self.location : void 0;
|
|
378
|
+
if (loc?.href) {
|
|
379
|
+
const params = new URL(loc.href).searchParams;
|
|
380
|
+
if (params.has("httpuvDebug") || params.get("debug") === "httpuv") {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
function lucentInfo(...args) {
|
|
389
|
+
if (!isLucentDebug()) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
console.info(...args);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/rwasm-constants.ts
|
|
396
|
+
var WASM_R_HOME = "/lib/R";
|
|
397
|
+
var WEB_APP_DIR = "/webApp";
|
|
398
|
+
var RWASM = {
|
|
399
|
+
READY: "rwasm_ready",
|
|
400
|
+
EVAL: "rwasm_eval",
|
|
401
|
+
EVAL_RESULT: "rwasm_eval_result",
|
|
402
|
+
STOP_APP: "rwasm_stop_app",
|
|
403
|
+
REMOUNT_R_HOME: "rwasm_remount_r_home",
|
|
404
|
+
STOPPED: "rwasm_stopped",
|
|
405
|
+
COMLINK_PORT: "rwasm_comlink_port",
|
|
406
|
+
COMLINK_READY: "rwasm_comlink_ready",
|
|
407
|
+
GET_RESOURCE_PATHS: "rwasm_get_resource_paths",
|
|
408
|
+
RESOURCE_PATHS: "rwasm_resource_paths",
|
|
409
|
+
LOG: "rwasm_log",
|
|
410
|
+
/** Worker → main: empack package download progress. */
|
|
411
|
+
DOWNLOAD_PROGRESS: "rwasm_download_progress",
|
|
412
|
+
/** Worker → main: coarse boot phase updates after / around downloads. */
|
|
413
|
+
BOOT_STATUS: "rwasm_boot_status",
|
|
414
|
+
ERROR: "rwasm_error",
|
|
415
|
+
/** Main → worker: run one shiny::serviceOnce tick. */
|
|
416
|
+
SERVICE_TICK: "rwasm_service_tick",
|
|
417
|
+
/** Worker → main: result of a SERVICE_TICK (`hadWork` / `nextDelayMs`). */
|
|
418
|
+
SERVICE_STATUS: "rwasm_service_status",
|
|
419
|
+
/** Worker → main: schedule an immediate MessageChannel wake. */
|
|
420
|
+
NEED_SERVICE: "rwasm_need_service",
|
|
421
|
+
/** Worker → main: delayed wake (`delayMs`) via setTimeout then MessageChannel.
|
|
422
|
+
* Used when scheduleTask runs outside a SERVICE_TICK; idle delays after a tick
|
|
423
|
+
* go through SERVICE_STATUS.nextDelayMs instead. */
|
|
424
|
+
SCHEDULE_DELAY: "rwasm_schedule_delay"
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/rWasmBootstrap.ts
|
|
428
|
+
var evalRPostFlush = null;
|
|
429
|
+
function setEvalRPostFlush(fn) {
|
|
430
|
+
evalRPostFlush = fn;
|
|
431
|
+
}
|
|
432
|
+
function asDirectoryUrl(url) {
|
|
433
|
+
const resolved = new URL(url, self.location.href);
|
|
434
|
+
if (!resolved.pathname.endsWith("/")) {
|
|
435
|
+
resolved.pathname += "/";
|
|
436
|
+
}
|
|
437
|
+
return resolved;
|
|
438
|
+
}
|
|
439
|
+
function createRuntimeAssetUrls(runtimeBaseUrl) {
|
|
440
|
+
const base = asDirectoryUrl(runtimeBaseUrl);
|
|
441
|
+
return {
|
|
442
|
+
glue: new URL("bin/Rmain.js", base),
|
|
443
|
+
wasm: new URL("bin/Rmain.wasm", base),
|
|
444
|
+
/** Host mirror of `$PREFIX/lib/R/lib` for MAIN_MODULE dynlink before VFS populate. */
|
|
445
|
+
rLib: new URL("lib/R/lib/", base)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
var vfsBlobUrls = /* @__PURE__ */ new Map();
|
|
449
|
+
function clearVfsBlobUrlCache() {
|
|
450
|
+
for (const url of vfsBlobUrls.values()) {
|
|
451
|
+
URL.revokeObjectURL(url);
|
|
452
|
+
}
|
|
453
|
+
vfsBlobUrls.clear();
|
|
454
|
+
}
|
|
455
|
+
function vfsFileToBlobUrl(module, vfsPath) {
|
|
456
|
+
const cached2 = vfsBlobUrls.get(vfsPath);
|
|
457
|
+
if (cached2) {
|
|
458
|
+
return cached2;
|
|
459
|
+
}
|
|
460
|
+
const data = module.FS.readFile(vfsPath, { encoding: "binary" });
|
|
461
|
+
const copy = new Uint8Array(data.byteLength);
|
|
462
|
+
copy.set(data);
|
|
463
|
+
const url = URL.createObjectURL(new Blob([copy.buffer], { type: "application/wasm" }));
|
|
464
|
+
vfsBlobUrls.set(vfsPath, url);
|
|
465
|
+
return url;
|
|
466
|
+
}
|
|
467
|
+
function vfsExists(module, path) {
|
|
468
|
+
try {
|
|
469
|
+
return module.FS.analyzePath(path).exists;
|
|
470
|
+
} catch {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function resolveSideModuleVfsPath(module, file) {
|
|
475
|
+
const fileBase = file.split("/").pop() ?? file;
|
|
476
|
+
const candidates = [];
|
|
477
|
+
if (file.startsWith("/")) {
|
|
478
|
+
candidates.push(file);
|
|
479
|
+
}
|
|
480
|
+
const pkgMatch = file.match(/(?:^|\/)library\/([^/]+)\/libs\/([^/]+)$/);
|
|
481
|
+
if (pkgMatch) {
|
|
482
|
+
candidates.push(`${WASM_R_HOME}/library/${pkgMatch[1]}/libs/${pkgMatch[2]}`);
|
|
483
|
+
}
|
|
484
|
+
candidates.push(`${WASM_R_HOME}/lib/${fileBase}`);
|
|
485
|
+
candidates.push(`/lib/${fileBase}`);
|
|
486
|
+
if (!file.startsWith("/")) {
|
|
487
|
+
candidates.push(`/${file}`);
|
|
488
|
+
candidates.push(`${WASM_R_HOME}/${file}`);
|
|
489
|
+
}
|
|
490
|
+
for (const path of candidates) {
|
|
491
|
+
if (vfsExists(module, path)) {
|
|
492
|
+
return path;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
function createFsBackedLocateFile(moduleRef, runtimeBaseUrl) {
|
|
498
|
+
const { wasm, rLib } = createRuntimeAssetUrls(runtimeBaseUrl);
|
|
499
|
+
return function locateFile(file) {
|
|
500
|
+
const fileBase = file.split("/").pop() ?? file;
|
|
501
|
+
if (fileBase.endsWith(".wasm")) {
|
|
502
|
+
return wasm.href;
|
|
503
|
+
}
|
|
504
|
+
const module = moduleRef.current;
|
|
505
|
+
if (module?.FS) {
|
|
506
|
+
const vfsPath = resolveSideModuleVfsPath(module, file);
|
|
507
|
+
if (vfsPath) {
|
|
508
|
+
return vfsFileToBlobUrl(module, vfsPath);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return new URL(fileBase, rLib).href;
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function mountRHomeLibToSlashLib(module) {
|
|
515
|
+
const libDir = `${WASM_R_HOME}/lib`;
|
|
516
|
+
if (!vfsExists(module, libDir)) {
|
|
517
|
+
lucentInfo("[rWasm] No", libDir, "to mirror into /lib");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
module.FS.mkdirTree("/lib");
|
|
521
|
+
let mirrored = 0;
|
|
522
|
+
for (const name of module.FS.readdir(libDir)) {
|
|
523
|
+
if (name === "." || name === ".." || !name.endsWith(".so")) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const src = `${libDir}/${name}`;
|
|
527
|
+
const data = module.FS.readFile(src, { encoding: "binary" });
|
|
528
|
+
module.FS.writeFile(`/lib/${name}`, data);
|
|
529
|
+
mirrored += 1;
|
|
530
|
+
}
|
|
531
|
+
lucentInfo("[rWasm] Mirrored", mirrored, "*.so from", libDir, "\u2192 /lib");
|
|
532
|
+
}
|
|
533
|
+
function verifyMountedTree(module) {
|
|
534
|
+
const methodsSo = `${WASM_R_HOME}/library/methods/libs/methods.so`;
|
|
535
|
+
if (!vfsExists(module, methodsSo)) {
|
|
536
|
+
throw new Error(`Mounted FS is missing ${methodsSo}`);
|
|
537
|
+
}
|
|
538
|
+
const data = module.FS.readFile(methodsSo, { encoding: "binary" });
|
|
539
|
+
if (!(data[0] === 0 && data[1] === 97 && data[2] === 115 && data[3] === 109)) {
|
|
540
|
+
throw new Error(`${methodsSo} is not a wasm module (bad magic bytes)`);
|
|
541
|
+
}
|
|
542
|
+
if (!vfsExists(module, WEB_APP_DIR)) {
|
|
543
|
+
throw new Error(`Mounted FS is missing ${WEB_APP_DIR} (empack pack dir/append webApp?)`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function loadRmainFactory(runtimeBaseUrl) {
|
|
547
|
+
const { glue: glueUrl } = createRuntimeAssetUrls(runtimeBaseUrl);
|
|
548
|
+
const mod = await import(
|
|
549
|
+
/* webpackIgnore: true */
|
|
550
|
+
glueUrl.href
|
|
551
|
+
);
|
|
552
|
+
const factory = mod.default ?? mod.Rmain;
|
|
553
|
+
if (typeof factory !== "function") {
|
|
554
|
+
throw new Error(
|
|
555
|
+
"Rmain factory missing; rebuild r-main with -sMODULARIZE=1 -sEXPORT_NAME=Rmain -sEXPORT_ES6=1"
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
return factory;
|
|
559
|
+
}
|
|
560
|
+
function evalR(Module, code) {
|
|
561
|
+
if (Module._rWasmEvalDepth > 0) {
|
|
562
|
+
throw new Error("reentrant evalR");
|
|
563
|
+
}
|
|
564
|
+
Module._rWasmEvalDepth = 1;
|
|
565
|
+
try {
|
|
566
|
+
return Module.evalR(code);
|
|
567
|
+
} finally {
|
|
568
|
+
Module._rWasmEvalDepth = 0;
|
|
569
|
+
evalRPostFlush?.();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async function populateFromEmpack(module, {
|
|
573
|
+
empackMetaUrl,
|
|
574
|
+
empackPackagesBaseUrl,
|
|
575
|
+
onDownloadProgress: onDownloadProgress2,
|
|
576
|
+
onBootStatus: onBootStatus2
|
|
577
|
+
}) {
|
|
578
|
+
if (typeof module.populateFilesystem !== "function") {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"Module.populateFilesystem missing; rebuild r-main with the parameterized rmain_post.js"
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
onBootStatus2?.({
|
|
584
|
+
phase: "download",
|
|
585
|
+
message: "Loading packages\u2026",
|
|
586
|
+
percent: 5
|
|
587
|
+
});
|
|
588
|
+
lucentInfo("[rWasm] populateFilesystem", empackMetaUrl);
|
|
589
|
+
await module.populateFilesystem({
|
|
590
|
+
metaUrl: empackMetaUrl,
|
|
591
|
+
packagesBaseUrl: empackPackagesBaseUrl,
|
|
592
|
+
onProgress: onDownloadProgress2
|
|
593
|
+
});
|
|
594
|
+
await extractEmpackMountsIfNeeded(module, {
|
|
595
|
+
empackMetaUrl,
|
|
596
|
+
empackPackagesBaseUrl,
|
|
597
|
+
onDownloadProgress: onDownloadProgress2
|
|
598
|
+
});
|
|
599
|
+
onBootStatus2?.({
|
|
600
|
+
phase: "filesystem",
|
|
601
|
+
message: "Preparing filesystem\u2026",
|
|
602
|
+
percent: 62
|
|
603
|
+
});
|
|
604
|
+
clearVfsBlobUrlCache();
|
|
605
|
+
mountRHomeLibToSlashLib(module);
|
|
606
|
+
verifyMountedTree(module);
|
|
607
|
+
}
|
|
608
|
+
async function extractEmpackMountsIfNeeded(module, {
|
|
609
|
+
empackMetaUrl,
|
|
610
|
+
empackPackagesBaseUrl,
|
|
611
|
+
onDownloadProgress: onDownloadProgress2
|
|
612
|
+
}) {
|
|
613
|
+
if (vfsExists(module, WEB_APP_DIR)) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const fetchUrls = module.fetchUrls;
|
|
617
|
+
if (typeof fetchUrls !== "function" || typeof module.extractArchiveFromMemory !== "function") {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const metaHref = new URL(empackMetaUrl, self.location.href).href;
|
|
621
|
+
const metaRes = await fetch(metaHref);
|
|
622
|
+
if (!metaRes.ok) {
|
|
623
|
+
throw new Error(`Failed to fetch ${metaHref}: HTTP ${metaRes.status}`);
|
|
624
|
+
}
|
|
625
|
+
const meta = await metaRes.json();
|
|
626
|
+
const mounts = Array.isArray(meta.mounts) ? meta.mounts : [];
|
|
627
|
+
if (mounts.length === 0) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const baseHref = new URL(empackPackagesBaseUrl, self.location.href).href;
|
|
631
|
+
const urls = mounts.map((m) => {
|
|
632
|
+
if (!m?.filename) {
|
|
633
|
+
throw new Error(`mount entry missing filename in ${metaHref}`);
|
|
634
|
+
}
|
|
635
|
+
return new URL(m.filename, baseHref).href;
|
|
636
|
+
});
|
|
637
|
+
lucentInfo("[rWasm] extracting", urls.length, "empack mount archive(s)");
|
|
638
|
+
const errors = [];
|
|
639
|
+
await fetchUrls(urls, {
|
|
640
|
+
concurrency: 5,
|
|
641
|
+
onProgress: onDownloadProgress2,
|
|
642
|
+
onDone: async ({ url, arrayBuffer, error }) => {
|
|
643
|
+
if (error || !arrayBuffer) {
|
|
644
|
+
errors.push({ url, error: error ?? new Error("empty archive") });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
module.extractArchiveFromMemory(new Uint8Array(arrayBuffer));
|
|
649
|
+
} catch (extractError) {
|
|
650
|
+
errors.push({ url, error: extractError });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
if (errors.length > 0) {
|
|
655
|
+
const detail = errors.map((e) => `${e.url}: ${e.error instanceof Error ? e.error.message : String(e.error)}`).join("; ");
|
|
656
|
+
throw new Error(`Failed to extract empack mounts: ${detail}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function unloadTransportPackages(Module) {
|
|
660
|
+
evalR(Module, `tryCatch({
|
|
661
|
+
for (pkg in c("shiny", "httpuv")) {
|
|
662
|
+
if (pkg %in% loadedNamespaces()) {
|
|
663
|
+
tryCatch(unloadNamespace(pkg), error = function(e) {
|
|
664
|
+
cat("[rWasm] unloadNamespace ", pkg, ": ", conditionMessage(e), "\\n", sep = "")
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}, error = function(e) NULL)`);
|
|
669
|
+
}
|
|
670
|
+
async function remountRHome(Module, {
|
|
671
|
+
empackMetaUrl,
|
|
672
|
+
empackPackagesBaseUrl,
|
|
673
|
+
onDownloadProgress: onDownloadProgress2,
|
|
674
|
+
onBootStatus: onBootStatus2
|
|
675
|
+
}) {
|
|
676
|
+
await populateFromEmpack(Module, {
|
|
677
|
+
empackMetaUrl,
|
|
678
|
+
empackPackagesBaseUrl,
|
|
679
|
+
onDownloadProgress: onDownloadProgress2,
|
|
680
|
+
onBootStatus: onBootStatus2
|
|
681
|
+
});
|
|
682
|
+
unloadTransportPackages(Module);
|
|
683
|
+
lucentInfo("[rWasm] Remounted empack env from", empackMetaUrl);
|
|
684
|
+
}
|
|
685
|
+
async function bootstrapRSession(Module, onBootStatus2) {
|
|
686
|
+
onBootStatus2?.({
|
|
687
|
+
phase: "init-r",
|
|
688
|
+
message: "Initializing R\u2026",
|
|
689
|
+
percent: 68
|
|
690
|
+
});
|
|
691
|
+
const status = Module.initR(["--no-restore", "--no-save", "--vanilla"]);
|
|
692
|
+
if (status !== 0) {
|
|
693
|
+
throw new Error(`R init failed with status ${status}`);
|
|
694
|
+
}
|
|
695
|
+
onBootStatus2?.({
|
|
696
|
+
phase: "load-httpuv",
|
|
697
|
+
message: "Loading httpuv\u2026",
|
|
698
|
+
percent: 78
|
|
699
|
+
});
|
|
700
|
+
evalR(Module, "suppressPackageStartupMessages(library(httpuv))");
|
|
701
|
+
evalR(Module, 'setwd("/")');
|
|
702
|
+
lucentInfo("[rWasm] R session ready");
|
|
703
|
+
}
|
|
704
|
+
async function initRModule({
|
|
705
|
+
runtimeBaseUrl,
|
|
706
|
+
empackMetaUrl,
|
|
707
|
+
empackPackagesBaseUrl,
|
|
708
|
+
print,
|
|
709
|
+
printErr,
|
|
710
|
+
onDownloadProgress: onDownloadProgress2,
|
|
711
|
+
onBootStatus: onBootStatus2
|
|
712
|
+
}) {
|
|
713
|
+
const moduleRef = { current: null };
|
|
714
|
+
const locateFile = createFsBackedLocateFile(moduleRef, runtimeBaseUrl);
|
|
715
|
+
onBootStatus2?.({
|
|
716
|
+
phase: "load-runtime",
|
|
717
|
+
message: "Loading R.wasm runtime\u2026",
|
|
718
|
+
percent: 2
|
|
719
|
+
});
|
|
720
|
+
const createRmain = await loadRmainFactory(runtimeBaseUrl);
|
|
721
|
+
const module = {
|
|
722
|
+
noInitialRun: true,
|
|
723
|
+
_rWasmEvalDepth: 0,
|
|
724
|
+
locateFile,
|
|
725
|
+
onAbort(reason) {
|
|
726
|
+
throw new Error(`Rmain aborted: ${String(reason)}`);
|
|
727
|
+
},
|
|
728
|
+
print(text) {
|
|
729
|
+
(print ?? console.log)(String(text));
|
|
730
|
+
},
|
|
731
|
+
printErr(text) {
|
|
732
|
+
(printErr ?? console.error)(String(text));
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
moduleRef.current = module;
|
|
736
|
+
try {
|
|
737
|
+
await createRmain(module);
|
|
738
|
+
} catch (err) {
|
|
739
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
740
|
+
}
|
|
741
|
+
await populateFromEmpack(module, {
|
|
742
|
+
empackMetaUrl,
|
|
743
|
+
empackPackagesBaseUrl,
|
|
744
|
+
onDownloadProgress: onDownloadProgress2,
|
|
745
|
+
onBootStatus: onBootStatus2
|
|
746
|
+
});
|
|
747
|
+
await bootstrapRSession(module, onBootStatus2);
|
|
748
|
+
globalThis.Module = module;
|
|
749
|
+
return module;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/rWasmTasks.ts
|
|
753
|
+
var SHINY_HOST = {
|
|
754
|
+
stop: `tryCatch({
|
|
755
|
+
if ("shiny" %in% loadedNamespaces() && shiny::isRunning()) {
|
|
756
|
+
shiny::stopApp()
|
|
757
|
+
}
|
|
758
|
+
}, error=function(e) NULL)`,
|
|
759
|
+
suspend: `tryCatch(shiny::suspendServiceLoop(), error=function(e) NULL)`,
|
|
760
|
+
resume: `tryCatch(shiny::resumeServiceLoop(), error=function(e) NULL)`,
|
|
761
|
+
serviceOnce: `tryCatch(shiny::serviceOnce(), error=function(e) NULL)`,
|
|
762
|
+
/**
|
|
763
|
+
* Run serviceOnce and write status JSON to a VFS flag file.
|
|
764
|
+
* Module.evalR returns an SEXP pointer, not the R value — callers must read
|
|
765
|
+
* the flag file for hadWork / nextMs. The host arms delays from nextMs on
|
|
766
|
+
* SERVICE_STATUS (serviceOnce itself does not scheduleHostDelay).
|
|
767
|
+
*
|
|
768
|
+
* Before the app starts this reports idle in plain base R: a service tick must
|
|
769
|
+
* never be the thing that loads shiny.
|
|
770
|
+
*/
|
|
771
|
+
serviceOnceHadWork: `tryCatch({
|
|
772
|
+
if (!("shiny" %in% loadedNamespaces())) {
|
|
773
|
+
cat("0", file = "/tmp/lucent-service-had-work")
|
|
774
|
+
invisible(FALSE)
|
|
775
|
+
} else {
|
|
776
|
+
had <- isTRUE(shiny::serviceOnce())
|
|
777
|
+
pending_flush <- tryCatch(shiny:::appsNeedingFlush$size() > 0, error=function(e) FALSE)
|
|
778
|
+
if (isTRUE(pending_flush)) {
|
|
779
|
+
had <- TRUE
|
|
780
|
+
}
|
|
781
|
+
next_ms <- tryCatch(shiny:::timerCallbacks$timeToNextEvent(), error=function(e) NA_real_)
|
|
782
|
+
jsonlite::write_json(list(
|
|
783
|
+
had = isTRUE(had),
|
|
784
|
+
nextMs = if (is.finite(next_ms)) next_ms else -1
|
|
785
|
+
), "/tmp/lucent-service-had-work", auto_unbox=TRUE)
|
|
786
|
+
invisible(had)
|
|
787
|
+
}
|
|
788
|
+
}, error=function(e) {
|
|
789
|
+
tryCatch(jsonlite::write_json(list(had=FALSE, nextMs=-1, err=conditionMessage(e)), "/tmp/lucent-service-had-work", auto_unbox=TRUE), error=function(e2) NULL)
|
|
790
|
+
invisible(FALSE)
|
|
791
|
+
})`
|
|
792
|
+
};
|
|
793
|
+
function createRTaskQueue(rt) {
|
|
794
|
+
const macrotaskChannel = new MessageChannel();
|
|
795
|
+
const macrotaskQueue = [];
|
|
796
|
+
macrotaskChannel.port1.onmessage = () => {
|
|
797
|
+
const cb = macrotaskQueue.shift();
|
|
798
|
+
if (cb) {
|
|
799
|
+
cb();
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
function scheduleMacrotask(cb) {
|
|
803
|
+
macrotaskQueue.push(cb);
|
|
804
|
+
macrotaskChannel.port2.postMessage(0);
|
|
805
|
+
}
|
|
806
|
+
const rTaskQueue = [];
|
|
807
|
+
let rDrainScheduled = false;
|
|
808
|
+
let rLocked = false;
|
|
809
|
+
function scheduleRDrain() {
|
|
810
|
+
if (rDrainScheduled) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
rDrainScheduled = true;
|
|
814
|
+
scheduleMacrotask(drainRTaskQueue);
|
|
815
|
+
}
|
|
816
|
+
function drainRTaskQueue() {
|
|
817
|
+
rDrainScheduled = false;
|
|
818
|
+
if (!rt.getModule() || rTaskQueue.length === 0) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const task = rTaskQueue.shift();
|
|
822
|
+
if (!task) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
rLocked = true;
|
|
826
|
+
try {
|
|
827
|
+
task.work();
|
|
828
|
+
task.resolve();
|
|
829
|
+
} catch (err) {
|
|
830
|
+
task.reject(err);
|
|
831
|
+
} finally {
|
|
832
|
+
rLocked = false;
|
|
833
|
+
rt.getTransport()?.flushDeferredOutbound();
|
|
834
|
+
}
|
|
835
|
+
if (rTaskQueue.length > 0) {
|
|
836
|
+
scheduleRDrain();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function enqueueRTask(work) {
|
|
840
|
+
return new Promise((resolve, reject) => {
|
|
841
|
+
rTaskQueue.push({ work, resolve, reject });
|
|
842
|
+
scheduleRDrain();
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function evalRNow(code) {
|
|
846
|
+
const module = rt.getModule();
|
|
847
|
+
if (!module) {
|
|
848
|
+
throw new Error("[lucent] R module not initialized yet");
|
|
849
|
+
}
|
|
850
|
+
return evalR(module, code);
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
scheduleMacrotask,
|
|
854
|
+
enqueueRTask,
|
|
855
|
+
evalRNow,
|
|
856
|
+
isRLocked: () => rLocked,
|
|
857
|
+
hasPendingRTasks: () => rTaskQueue.length > 0
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/rWasmHttpDelivery.ts
|
|
862
|
+
var HTTP_PUSH_DRAIN_ROUNDS = 128;
|
|
863
|
+
function createHttpDelivery(deps) {
|
|
864
|
+
let httpIdleMaxMs = 3e4;
|
|
865
|
+
let httpDeliveryActive = false;
|
|
866
|
+
let httpDeliveryDraining = false;
|
|
867
|
+
let shinyLoopSuspended = false;
|
|
868
|
+
let httpDeliveryInflight = 0;
|
|
869
|
+
const httpDeliveryQueue = [];
|
|
870
|
+
const httpDrainByUuid = /* @__PURE__ */ new Map();
|
|
871
|
+
async function idleWaitForHttpResponse(state, uuid, maxMs = httpIdleMaxMs) {
|
|
872
|
+
const start = Date.now();
|
|
873
|
+
if (!state.resolved) {
|
|
874
|
+
await new Promise((resolve) => {
|
|
875
|
+
let settled = false;
|
|
876
|
+
const done = () => {
|
|
877
|
+
if (settled) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
settled = true;
|
|
881
|
+
clearTimeout(timer);
|
|
882
|
+
if (state.notify === done) {
|
|
883
|
+
state.notify = void 0;
|
|
884
|
+
}
|
|
885
|
+
resolve();
|
|
886
|
+
};
|
|
887
|
+
state.notify = done;
|
|
888
|
+
const timer = setTimeout(done, maxMs);
|
|
889
|
+
if (state.resolved) {
|
|
890
|
+
done();
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
deps.dbg("worker-push-idle-done", {
|
|
895
|
+
uuid,
|
|
896
|
+
resolved: state.resolved,
|
|
897
|
+
waitedMs: Date.now() - start
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
function isStaticAssetUrl(url) {
|
|
901
|
+
return deps.requireTransport().isLikelyStaticAsset(url);
|
|
902
|
+
}
|
|
903
|
+
function isSessionHttpUrl(url) {
|
|
904
|
+
const t = deps.requireTransport();
|
|
905
|
+
try {
|
|
906
|
+
return t.isSessionHttpRequest(url, t.getShinyPrefix());
|
|
907
|
+
} catch {
|
|
908
|
+
return t.isSessionHttpRequest(url);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async function ensureShinyLoopSuspended() {
|
|
912
|
+
if (shinyLoopSuspended || !deps.getModule()) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
await deps.tasks.enqueueRTask(() => {
|
|
916
|
+
deps.tasks.evalRNow(SHINY_HOST.suspend);
|
|
917
|
+
});
|
|
918
|
+
shinyLoopSuspended = true;
|
|
919
|
+
deps.dbg("worker-shiny-loop-suspend", {});
|
|
920
|
+
}
|
|
921
|
+
async function ensureShinyLoopResumed() {
|
|
922
|
+
if (!shinyLoopSuspended || !deps.getModule()) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
await deps.tasks.enqueueRTask(() => {
|
|
926
|
+
deps.tasks.evalRNow(SHINY_HOST.resume);
|
|
927
|
+
});
|
|
928
|
+
shinyLoopSuspended = false;
|
|
929
|
+
deps.dbg("worker-shiny-loop-resume", {});
|
|
930
|
+
}
|
|
931
|
+
async function maybeFinishHttpDeliveryBatch() {
|
|
932
|
+
if (httpDeliveryQueue.length > 0 || httpDeliveryInflight > 0) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
await ensureShinyLoopResumed();
|
|
936
|
+
httpDeliveryActive = false;
|
|
937
|
+
httpDeliveryDraining = false;
|
|
938
|
+
}
|
|
939
|
+
function drainAfterHttpPush(roundsLeft, uuid, state) {
|
|
940
|
+
if (roundsLeft <= 0 || !deps.getModule() || state.resolved) {
|
|
941
|
+
if (!state.resolved) {
|
|
942
|
+
deps.dbg("worker-push-drain-exhausted", { uuid, roundsLeft });
|
|
943
|
+
}
|
|
944
|
+
return Promise.resolve();
|
|
945
|
+
}
|
|
946
|
+
return new Promise((resolve) => {
|
|
947
|
+
deps.tasks.scheduleMacrotask(() => {
|
|
948
|
+
void deps.tasks.enqueueRTask(() => {
|
|
949
|
+
deps.tasks.evalRNow(SHINY_HOST.serviceOnce);
|
|
950
|
+
}).then(() => drainAfterHttpPush(roundsLeft - 1, uuid, state)).then(resolve);
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function yieldMacrotask() {
|
|
955
|
+
return new Promise((resolve) => {
|
|
956
|
+
deps.tasks.scheduleMacrotask(() => resolve());
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
async function deliverOneHttpRequest(req) {
|
|
960
|
+
const uuid = req.uuid ?? "";
|
|
961
|
+
const url = req.url ?? "";
|
|
962
|
+
const state = { resolved: false };
|
|
963
|
+
httpDrainByUuid.set(uuid, state);
|
|
964
|
+
httpDeliveryInflight++;
|
|
965
|
+
const staticAsset = isStaticAssetUrl(url);
|
|
966
|
+
const sessionHttp = isSessionHttpUrl(url);
|
|
967
|
+
try {
|
|
968
|
+
await deps.tasks.enqueueRTask(() => {
|
|
969
|
+
deps.requireTransport().pushInboundHostMessage(req);
|
|
970
|
+
});
|
|
971
|
+
if (!staticAsset) {
|
|
972
|
+
await idleWaitForHttpResponse(state, uuid, httpIdleMaxMs);
|
|
973
|
+
if (!state.resolved) {
|
|
974
|
+
await drainAfterHttpPush(HTTP_PUSH_DRAIN_ROUNDS, uuid, state);
|
|
975
|
+
} else if (sessionHttp) {
|
|
976
|
+
deps.dbg("worker-session-skip-drain", { uuid });
|
|
977
|
+
}
|
|
978
|
+
} else if (!state.resolved) {
|
|
979
|
+
await yieldMacrotask();
|
|
980
|
+
}
|
|
981
|
+
} finally {
|
|
982
|
+
httpDrainByUuid.delete(uuid);
|
|
983
|
+
httpDeliveryInflight--;
|
|
984
|
+
if (sessionHttp) {
|
|
985
|
+
deps.requestService();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async function drainHttpDeliveryQueue() {
|
|
990
|
+
while (httpDeliveryQueue.length > 0) {
|
|
991
|
+
const item = httpDeliveryQueue.shift();
|
|
992
|
+
if (!item) {
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
const itemUrl = item.req.url ?? "";
|
|
996
|
+
const needsSuspend = !isStaticAssetUrl(itemUrl) && !isSessionHttpUrl(itemUrl);
|
|
997
|
+
if (needsSuspend && !shinyLoopSuspended) {
|
|
998
|
+
await ensureShinyLoopSuspended();
|
|
999
|
+
}
|
|
1000
|
+
if (needsSuspend) {
|
|
1001
|
+
httpDeliveryActive = true;
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
await deliverOneHttpRequest(item.req);
|
|
1005
|
+
item.resolve();
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
deps.logError(err, itemUrl);
|
|
1008
|
+
item.reject(err);
|
|
1009
|
+
} finally {
|
|
1010
|
+
if (needsSuspend) {
|
|
1011
|
+
httpDeliveryActive = false;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const next = httpDeliveryQueue[0];
|
|
1015
|
+
const nextUrl = next?.req.url ?? "";
|
|
1016
|
+
const nextNeedsSuspend = Boolean(
|
|
1017
|
+
next && !isStaticAssetUrl(nextUrl) && !isSessionHttpUrl(nextUrl)
|
|
1018
|
+
);
|
|
1019
|
+
if (shinyLoopSuspended && !nextNeedsSuspend) {
|
|
1020
|
+
await ensureShinyLoopResumed();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
await maybeFinishHttpDeliveryBatch();
|
|
1024
|
+
if (httpDeliveryQueue.length > 0 && !httpDeliveryDraining) {
|
|
1025
|
+
httpDeliveryDraining = true;
|
|
1026
|
+
void drainHttpDeliveryQueue();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function enqueueHttpDelivery(req) {
|
|
1030
|
+
deps.requestService();
|
|
1031
|
+
deps.dbg("worker-push", { uuid: req.uuid, method: req.method, url: req.url });
|
|
1032
|
+
return new Promise((resolve, reject) => {
|
|
1033
|
+
httpDeliveryQueue.push({ req, resolve, reject });
|
|
1034
|
+
if (!httpDeliveryDraining) {
|
|
1035
|
+
httpDeliveryDraining = true;
|
|
1036
|
+
void drainHttpDeliveryQueue().catch((err) => {
|
|
1037
|
+
httpDeliveryDraining = false;
|
|
1038
|
+
httpDeliveryActive = false;
|
|
1039
|
+
deps.logError(err);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
return {
|
|
1045
|
+
setIdleMaxMs(ms) {
|
|
1046
|
+
httpIdleMaxMs = ms;
|
|
1047
|
+
},
|
|
1048
|
+
isHttpDeliveryActive: () => httpDeliveryActive,
|
|
1049
|
+
hasHttpWork: () => httpDeliveryInflight > 0 || httpDeliveryQueue.length > 0,
|
|
1050
|
+
noteHttpResponse(uuid) {
|
|
1051
|
+
const drainState = uuid ? httpDrainByUuid.get(uuid) : void 0;
|
|
1052
|
+
if (!drainState || drainState.resolved) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
drainState.resolved = true;
|
|
1056
|
+
drainState.notify?.();
|
|
1057
|
+
},
|
|
1058
|
+
enqueueHttpDelivery
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/transport.ts
|
|
1063
|
+
var cached = null;
|
|
1064
|
+
function transportUrl(baseUrl) {
|
|
1065
|
+
const base = new URL(baseUrl, self.location.href);
|
|
1066
|
+
return new URL("httpuv-web.js", base).href;
|
|
1067
|
+
}
|
|
1068
|
+
function loadTransport(baseUrl) {
|
|
1069
|
+
if (!cached) {
|
|
1070
|
+
const url = transportUrl(baseUrl);
|
|
1071
|
+
cached = import(
|
|
1072
|
+
/* @vite-ignore */
|
|
1073
|
+
/* webpackIgnore: true */
|
|
1074
|
+
url
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
return cached;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/wiring.ts
|
|
1081
|
+
function createRHostApi(httpRequestType, handlers) {
|
|
1082
|
+
const { onHttpRequest, onStop, getResourcePaths, registerSwDelivery, readVfsFile: readVfsFile2, readVfsFileAt: readVfsFileAt2 } = handlers;
|
|
1083
|
+
return {
|
|
1084
|
+
registerSwDelivery(port, wsPushPort2) {
|
|
1085
|
+
registerSwDelivery(port, wsPushPort2);
|
|
1086
|
+
},
|
|
1087
|
+
deliverHttpRequest(req) {
|
|
1088
|
+
return onHttpRequest({
|
|
1089
|
+
type: httpRequestType,
|
|
1090
|
+
uuid: req.uuid,
|
|
1091
|
+
method: req.method,
|
|
1092
|
+
url: req.url,
|
|
1093
|
+
headers: req.headers ?? {},
|
|
1094
|
+
body: req.body ?? null,
|
|
1095
|
+
clientId: req.clientId ?? null
|
|
1096
|
+
});
|
|
1097
|
+
},
|
|
1098
|
+
getShinyResourcePaths() {
|
|
1099
|
+
return getResourcePaths();
|
|
1100
|
+
},
|
|
1101
|
+
readVfsFile(vfsDir, suffix) {
|
|
1102
|
+
return readVfsFile2(vfsDir, suffix);
|
|
1103
|
+
},
|
|
1104
|
+
readVfsFileAt(vfsPath) {
|
|
1105
|
+
return readVfsFileAt2(vfsPath);
|
|
1106
|
+
},
|
|
1107
|
+
stop() {
|
|
1108
|
+
onStop();
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/rWasmWorker.ts
|
|
1114
|
+
var workerSelf = self;
|
|
1115
|
+
var config = resolveLucentConfig();
|
|
1116
|
+
var rAssetBaseUrl = null;
|
|
1117
|
+
var mountedRuntimeKey = null;
|
|
1118
|
+
var transport = null;
|
|
1119
|
+
var rModule = null;
|
|
1120
|
+
var rModulePromise = null;
|
|
1121
|
+
var swDelivery = null;
|
|
1122
|
+
var wsPushPort = null;
|
|
1123
|
+
var rHostPortReady = false;
|
|
1124
|
+
var swDeliveryPortReady = false;
|
|
1125
|
+
var tasks = createRTaskQueue({
|
|
1126
|
+
getModule: () => rModule,
|
|
1127
|
+
getTransport: () => transport
|
|
1128
|
+
});
|
|
1129
|
+
function postToHost(payload, transfer2 = []) {
|
|
1130
|
+
workerSelf.postMessage(payload, transfer2);
|
|
1131
|
+
}
|
|
1132
|
+
function requestService() {
|
|
1133
|
+
postToHost({ type: RWASM.NEED_SERVICE });
|
|
1134
|
+
}
|
|
1135
|
+
function requireTransport() {
|
|
1136
|
+
if (!transport) {
|
|
1137
|
+
throw new Error("[lucent] transport not loaded yet");
|
|
1138
|
+
}
|
|
1139
|
+
return transport;
|
|
1140
|
+
}
|
|
1141
|
+
function requireRModule() {
|
|
1142
|
+
if (!rModule) {
|
|
1143
|
+
throw new Error("[lucent] R module not initialized yet");
|
|
1144
|
+
}
|
|
1145
|
+
return rModule;
|
|
1146
|
+
}
|
|
1147
|
+
function dbg(stage, ...args) {
|
|
1148
|
+
transport?.httpuvDebugLog(stage, ...args);
|
|
1149
|
+
}
|
|
1150
|
+
var http = createHttpDelivery({
|
|
1151
|
+
tasks,
|
|
1152
|
+
getModule: () => rModule,
|
|
1153
|
+
requireTransport,
|
|
1154
|
+
requestService,
|
|
1155
|
+
dbg,
|
|
1156
|
+
formatError: formatRWasmError,
|
|
1157
|
+
logError: logHttpDeliveryError
|
|
1158
|
+
});
|
|
1159
|
+
function log(level, text) {
|
|
1160
|
+
const msg = String(text);
|
|
1161
|
+
if (level === "error" && msg.startsWith("Error")) {
|
|
1162
|
+
postToHost({ type: RWASM.LOG, level: "error", text: msg });
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
postToHost({ type: RWASM.LOG, level: "log", text: msg });
|
|
1166
|
+
}
|
|
1167
|
+
function maybeAnnounceComlinkReady() {
|
|
1168
|
+
if (rHostPortReady && swDeliveryPortReady) {
|
|
1169
|
+
postToHost({ type: RWASM.COMLINK_READY });
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function messageBodyLength(message) {
|
|
1173
|
+
if (typeof message === "string") {
|
|
1174
|
+
return message.length;
|
|
1175
|
+
}
|
|
1176
|
+
if (message instanceof ArrayBuffer) {
|
|
1177
|
+
return message.byteLength;
|
|
1178
|
+
}
|
|
1179
|
+
if (ArrayBuffer.isView(message)) {
|
|
1180
|
+
return message.byteLength;
|
|
1181
|
+
}
|
|
1182
|
+
return 0;
|
|
1183
|
+
}
|
|
1184
|
+
function formatRWasmError(err) {
|
|
1185
|
+
if (err instanceof Error) {
|
|
1186
|
+
return err.message || err.name;
|
|
1187
|
+
}
|
|
1188
|
+
const WasmException = WebAssembly.Exception;
|
|
1189
|
+
if (typeof WasmException !== "undefined" && err instanceof WasmException) {
|
|
1190
|
+
const message = err.message;
|
|
1191
|
+
if (typeof message === "string" && message) {
|
|
1192
|
+
return message;
|
|
1193
|
+
}
|
|
1194
|
+
return "WebAssembly.Exception (likely WASM trap during R eval)";
|
|
1195
|
+
}
|
|
1196
|
+
if (typeof err === "object" && err !== null) {
|
|
1197
|
+
const message = err.message;
|
|
1198
|
+
if (typeof message === "string" && message) {
|
|
1199
|
+
return message;
|
|
1200
|
+
}
|
|
1201
|
+
const text = String(err);
|
|
1202
|
+
if (text !== "[object Object]" && text !== "[object WebAssembly.Exception]") {
|
|
1203
|
+
return text;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return String(err);
|
|
1207
|
+
}
|
|
1208
|
+
function replyOk(id) {
|
|
1209
|
+
if (id == null) {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
postToHost({ type: RWASM.EVAL_RESULT, id, ok: true });
|
|
1213
|
+
}
|
|
1214
|
+
function replyErr(id, err) {
|
|
1215
|
+
if (id == null) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
postToHost({
|
|
1219
|
+
type: RWASM.EVAL_RESULT,
|
|
1220
|
+
id,
|
|
1221
|
+
ok: false,
|
|
1222
|
+
error: formatRWasmError(err)
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
function logHttpDeliveryError(err, url) {
|
|
1226
|
+
const where = url ? ` ${url}` : "";
|
|
1227
|
+
console.error(`[rWasmWorker] deliverHttpRequest failed:${where}`, formatRWasmError(err), err);
|
|
1228
|
+
}
|
|
1229
|
+
function deliverToServiceWorker(outbound, transfer2 = []) {
|
|
1230
|
+
const t = requireTransport();
|
|
1231
|
+
if (!swDelivery) {
|
|
1232
|
+
console.warn("[rWasmWorker] Service worker delivery API not connected");
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
requestService();
|
|
1236
|
+
if (outbound.type === t.MSG.HTTP_RESPONSE) {
|
|
1237
|
+
dbg("comlink-deliver-http", { uuid: outbound.uuid, status: outbound.status });
|
|
1238
|
+
http.noteHttpResponse(outbound.uuid);
|
|
1239
|
+
const resp = {
|
|
1240
|
+
uuid: outbound.uuid,
|
|
1241
|
+
status: outbound.status,
|
|
1242
|
+
headers: outbound.headers,
|
|
1243
|
+
body: outbound.body
|
|
1244
|
+
};
|
|
1245
|
+
void swDelivery.deliverHttpResponse(transfer2.length > 0 ? transfer(resp, transfer2) : resp).catch((err) => {
|
|
1246
|
+
console.error("[rWasmWorker] deliverHttpResponse failed:", formatRWasmError(err), err);
|
|
1247
|
+
});
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (outbound.type === t.MSG.WS_PUSH) {
|
|
1251
|
+
const handle = t.normalizeSessionHandle(outbound.handle);
|
|
1252
|
+
dbg("ws-push", {
|
|
1253
|
+
handle,
|
|
1254
|
+
wsType: outbound.wsType,
|
|
1255
|
+
binary: outbound.binary,
|
|
1256
|
+
messageLen: messageBodyLength(outbound.message)
|
|
1257
|
+
});
|
|
1258
|
+
if (!wsPushPort) {
|
|
1259
|
+
console.warn("[rWasmWorker] WS push port not connected; dropping frame");
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const fullMsg = {
|
|
1263
|
+
type: t.MSG.WS_PUSH,
|
|
1264
|
+
handle,
|
|
1265
|
+
binary: outbound.binary,
|
|
1266
|
+
wsType: outbound.wsType,
|
|
1267
|
+
message: outbound.message
|
|
1268
|
+
};
|
|
1269
|
+
try {
|
|
1270
|
+
if (transfer2.length > 0) {
|
|
1271
|
+
wsPushPort.postMessage(fullMsg, transfer2);
|
|
1272
|
+
} else {
|
|
1273
|
+
wsPushPort.postMessage(fullMsg);
|
|
1274
|
+
}
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
console.error("[rWasmWorker] wsPushPort.postMessage failed:", formatRWasmError(err), err);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
function pushToR(msg) {
|
|
1281
|
+
const t = requireTransport();
|
|
1282
|
+
if (msg?.uuid) {
|
|
1283
|
+
dbg("worker-push-evalR-begin", { uuid: msg.uuid });
|
|
1284
|
+
}
|
|
1285
|
+
const isWs = msg?.type === t.CHANNEL.WS_OPEN || msg?.type === t.CHANNEL.WS_MESSAGE || msg?.type === t.CHANNEL.WS_CLOSE;
|
|
1286
|
+
if (isWs) {
|
|
1287
|
+
dbg("worker-push-ws", {
|
|
1288
|
+
type: msg.type,
|
|
1289
|
+
handle: t.normalizeSessionHandle(msg.handle),
|
|
1290
|
+
binary: msg.binary,
|
|
1291
|
+
messageLen: messageBodyLength(msg.message)
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
let pushOk = false;
|
|
1295
|
+
try {
|
|
1296
|
+
tasks.evalRNow(`tryCatch({
|
|
1297
|
+
${t.channelMessageToRExpr(msg)}
|
|
1298
|
+
}, error=function(e) {
|
|
1299
|
+
msg <- paste0("[httpuv] push failed (", ${JSON.stringify(String(msg.url ?? ""))}, "): ", conditionMessage(e))
|
|
1300
|
+
message(msg)
|
|
1301
|
+
stop(msg)
|
|
1302
|
+
})`);
|
|
1303
|
+
pushOk = true;
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
throw err;
|
|
1306
|
+
} finally {
|
|
1307
|
+
if (msg?.uuid) {
|
|
1308
|
+
dbg("worker-push-evalR-finish", { uuid: msg.uuid, ok: pushOk });
|
|
1309
|
+
}
|
|
1310
|
+
void tasks.enqueueRTask(() => {
|
|
1311
|
+
if (!rModule) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
tasks.evalRNow(SHINY_HOST.serviceOnceHadWork);
|
|
1315
|
+
}).catch((err) => {
|
|
1316
|
+
const error = formatRWasmError(err);
|
|
1317
|
+
if (/Maximum call stack size exceeded/i.test(error) && rModule) {
|
|
1318
|
+
try {
|
|
1319
|
+
tasks.evalRNow(`tryCatch({
|
|
1320
|
+
for (s in shiny:::appsByToken$values()) {
|
|
1321
|
+
priv <- s$.__enclos_env__$private
|
|
1322
|
+
while (isTRUE(priv$busyCount > 0L)) {
|
|
1323
|
+
s$decrementBusyCount()
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
invisible(NULL)
|
|
1327
|
+
}, error=function(e) NULL)`);
|
|
1328
|
+
} catch {
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
console.warn("[rWasmWorker] post-push serviceOnce failed:", error);
|
|
1332
|
+
});
|
|
1333
|
+
requestService();
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function installBridge(t) {
|
|
1337
|
+
const shinyBase = new URL(config.shinyBaseUrl, self.location.href).href;
|
|
1338
|
+
t.setShinyPrefix(t.resolveShinyPrefix(shinyBase));
|
|
1339
|
+
setEvalRPostFlush(() => t.flushDeferredOutbound());
|
|
1340
|
+
t.installHttpuvBridge({
|
|
1341
|
+
installSwListener: false,
|
|
1342
|
+
postOutbound: deliverToServiceWorker,
|
|
1343
|
+
pushToR: (msg) => {
|
|
1344
|
+
pushToR(msg);
|
|
1345
|
+
},
|
|
1346
|
+
requestHostService: () => {
|
|
1347
|
+
requestService();
|
|
1348
|
+
},
|
|
1349
|
+
scheduleHostDelay: (delayMs) => {
|
|
1350
|
+
postToHost({
|
|
1351
|
+
type: RWASM.SCHEDULE_DELAY,
|
|
1352
|
+
delayMs: Math.max(0, Number(delayMs) || 0)
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
function empackMountKey() {
|
|
1358
|
+
return [
|
|
1359
|
+
config.rRuntimeBaseUrl,
|
|
1360
|
+
config.empackMetaUrl,
|
|
1361
|
+
config.empackPackagesBaseUrl
|
|
1362
|
+
].join("\0");
|
|
1363
|
+
}
|
|
1364
|
+
function onDownloadProgress(progress) {
|
|
1365
|
+
const pct = progress.percent == null ? "?" : `${progress.percent.toFixed(1)}%`;
|
|
1366
|
+
lucentInfo(
|
|
1367
|
+
`[rWasm] download ${pct} (${progress.downloadedBytes}/${progress.totalBytes})`
|
|
1368
|
+
);
|
|
1369
|
+
postToHost({
|
|
1370
|
+
type: RWASM.DOWNLOAD_PROGRESS,
|
|
1371
|
+
percent: progress.percent,
|
|
1372
|
+
downloadedBytes: progress.downloadedBytes,
|
|
1373
|
+
totalBytes: progress.totalBytes
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
function onBootStatus(status) {
|
|
1377
|
+
lucentInfo(`[rWasm] boot [${status.phase}] ${status.message}`);
|
|
1378
|
+
postToHost({
|
|
1379
|
+
type: RWASM.BOOT_STATUS,
|
|
1380
|
+
phase: status.phase,
|
|
1381
|
+
message: status.message,
|
|
1382
|
+
percent: status.percent
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
async function initEverything() {
|
|
1386
|
+
transport = await loadTransport(config.transportBaseUrl);
|
|
1387
|
+
http.setIdleMaxMs(transport.REQUEST_TIMEOUT_MS);
|
|
1388
|
+
const runtimeBaseUrl = new URL(config.rRuntimeBaseUrl, self.location.href).href;
|
|
1389
|
+
const empackMetaUrl = new URL(config.empackMetaUrl, self.location.href).href;
|
|
1390
|
+
const empackPackagesBaseUrl = new URL(
|
|
1391
|
+
config.empackPackagesBaseUrl,
|
|
1392
|
+
self.location.href
|
|
1393
|
+
).href;
|
|
1394
|
+
rAssetBaseUrl = runtimeBaseUrl;
|
|
1395
|
+
const module = await initRModule({
|
|
1396
|
+
runtimeBaseUrl,
|
|
1397
|
+
empackMetaUrl,
|
|
1398
|
+
empackPackagesBaseUrl,
|
|
1399
|
+
print: (text) => log("log", text),
|
|
1400
|
+
printErr: (text) => log("error", text),
|
|
1401
|
+
onDownloadProgress,
|
|
1402
|
+
onBootStatus
|
|
1403
|
+
});
|
|
1404
|
+
rModule = module;
|
|
1405
|
+
mountedRuntimeKey = empackMountKey();
|
|
1406
|
+
installBridge(transport);
|
|
1407
|
+
return module;
|
|
1408
|
+
}
|
|
1409
|
+
function ensureRModule() {
|
|
1410
|
+
if (!rModulePromise) {
|
|
1411
|
+
rModulePromise = initEverything();
|
|
1412
|
+
}
|
|
1413
|
+
return rModulePromise;
|
|
1414
|
+
}
|
|
1415
|
+
function readVfsFileAt(vfsPath) {
|
|
1416
|
+
const module = requireRModule();
|
|
1417
|
+
const path = vfsPath.replace(/\/+$/, "");
|
|
1418
|
+
if (!path.startsWith("/") || path.includes("..")) {
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
try {
|
|
1422
|
+
const data = module.FS.readFile(path, { encoding: "binary" });
|
|
1423
|
+
return data.buffer.slice(
|
|
1424
|
+
data.byteOffset,
|
|
1425
|
+
data.byteOffset + data.byteLength
|
|
1426
|
+
);
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
log("error", `[rWasmWorker] readVfsFileAt failed ${path}: ${formatRWasmError(err)}`);
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
function readVfsFile(vfsDir, suffix) {
|
|
1433
|
+
return ensureRModule().then(() => {
|
|
1434
|
+
const rel = suffix.replace(/^\/+/, "");
|
|
1435
|
+
if (!rel || rel.includes("..")) {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
const path = `${vfsDir.replace(/\/$/, "")}/${rel}`;
|
|
1439
|
+
return readVfsFileAt(path);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
function readVfsFileAtAsync(vfsPath) {
|
|
1443
|
+
return ensureRModule().then(() => readVfsFileAt(vfsPath));
|
|
1444
|
+
}
|
|
1445
|
+
function readShinyResourcePathsFromR() {
|
|
1446
|
+
const module = requireRModule();
|
|
1447
|
+
tasks.evalRNow(`tryCatch({
|
|
1448
|
+
paths <- if (requireNamespace("shiny", quietly=TRUE)) as.list(shiny::resourcePaths()) else list()
|
|
1449
|
+
jsonlite::write_json(paths, "/resourcePaths.json", auto_unbox=TRUE)
|
|
1450
|
+
}, error=function(e) {
|
|
1451
|
+
jsonlite::write_json(list(), "/resourcePaths.json", auto_unbox=TRUE)
|
|
1452
|
+
})`);
|
|
1453
|
+
const text = module.FS.readFile("/resourcePaths.json", { encoding: "utf8" });
|
|
1454
|
+
try {
|
|
1455
|
+
module.FS.unlink("/resourcePaths.json");
|
|
1456
|
+
} catch {
|
|
1457
|
+
}
|
|
1458
|
+
const parsed = JSON.parse(text);
|
|
1459
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1460
|
+
}
|
|
1461
|
+
function getShinyResourcePaths() {
|
|
1462
|
+
return ensureRModule().then(() => {
|
|
1463
|
+
let result = {};
|
|
1464
|
+
return tasks.enqueueRTask(() => {
|
|
1465
|
+
result = readShinyResourcePathsFromR();
|
|
1466
|
+
}).then(() => result);
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
function exposeRHost(port) {
|
|
1470
|
+
const t = requireTransport();
|
|
1471
|
+
const api = createRHostApi(t.MSG.HTTP_REQUEST, {
|
|
1472
|
+
onHttpRequest: (req) => ensureRModule().then(() => http.enqueueHttpDelivery(req)).catch((err) => {
|
|
1473
|
+
logHttpDeliveryError(err);
|
|
1474
|
+
throw err;
|
|
1475
|
+
}),
|
|
1476
|
+
onStop: () => {
|
|
1477
|
+
void ensureRModule().then(() => {
|
|
1478
|
+
void tasks.enqueueRTask(() => {
|
|
1479
|
+
requireTransport().pushInboundHostMessage({ type: t.MSG.STOP });
|
|
1480
|
+
tasks.evalRNow(SHINY_HOST.stop);
|
|
1481
|
+
});
|
|
1482
|
+
}).catch((err) => {
|
|
1483
|
+
log("error", `[rWasmWorker] stop failed: ${formatRWasmError(err)}`);
|
|
1484
|
+
});
|
|
1485
|
+
},
|
|
1486
|
+
getResourcePaths: () => getShinyResourcePaths(),
|
|
1487
|
+
readVfsFile: (vfsDir, suffix) => readVfsFile(vfsDir, suffix),
|
|
1488
|
+
readVfsFileAt: (vfsPath) => readVfsFileAtAsync(vfsPath),
|
|
1489
|
+
registerSwDelivery: (deliveryPort, pushPort) => {
|
|
1490
|
+
connectSwDelivery(deliveryPort, pushPort);
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
expose(api, port);
|
|
1494
|
+
rHostPortReady = true;
|
|
1495
|
+
lucentInfo("[rWasmWorker] Comlink: exposing unified host API");
|
|
1496
|
+
}
|
|
1497
|
+
function connectSwDelivery(port, pushPort) {
|
|
1498
|
+
swDelivery = wrap(port);
|
|
1499
|
+
wsPushPort = pushPort;
|
|
1500
|
+
wsPushPort.start();
|
|
1501
|
+
swDeliveryPortReady = true;
|
|
1502
|
+
lucentInfo("[rWasmWorker] Comlink: connected to SW delivery API (+ WS push port)");
|
|
1503
|
+
maybeAnnounceComlinkReady();
|
|
1504
|
+
}
|
|
1505
|
+
function stopRunningApp() {
|
|
1506
|
+
if (!rModule) {
|
|
1507
|
+
return Promise.resolve();
|
|
1508
|
+
}
|
|
1509
|
+
return tasks.enqueueRTask(() => {
|
|
1510
|
+
tasks.evalRNow(SHINY_HOST.stop);
|
|
1511
|
+
}).catch((err) => {
|
|
1512
|
+
log("error", `[rWasmWorker] stopApp failed: ${formatRWasmError(err)}`);
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
async function onMessage(event) {
|
|
1516
|
+
const data = event.data;
|
|
1517
|
+
if (data?.type === RWASM.COMLINK_PORT && event.ports[0]) {
|
|
1518
|
+
const port = event.ports[0];
|
|
1519
|
+
port.start();
|
|
1520
|
+
rHostPortReady = false;
|
|
1521
|
+
swDeliveryPortReady = false;
|
|
1522
|
+
swDelivery = null;
|
|
1523
|
+
wsPushPort = null;
|
|
1524
|
+
await ensureRModule();
|
|
1525
|
+
exposeRHost(port);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (!data || typeof data.type !== "string") {
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
switch (data.type) {
|
|
1532
|
+
case RWASM.EVAL: {
|
|
1533
|
+
try {
|
|
1534
|
+
await ensureRModule();
|
|
1535
|
+
await tasks.enqueueRTask(() => {
|
|
1536
|
+
tasks.evalRNow(String(data.code ?? ""));
|
|
1537
|
+
});
|
|
1538
|
+
replyOk(data.id);
|
|
1539
|
+
requestService();
|
|
1540
|
+
} catch (err) {
|
|
1541
|
+
if (data.id != null) {
|
|
1542
|
+
replyErr(data.id, err);
|
|
1543
|
+
} else {
|
|
1544
|
+
log("error", `[rWasmWorker] eval failed: ${formatRWasmError(err)}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
case RWASM.REMOUNT_R_HOME: {
|
|
1550
|
+
try {
|
|
1551
|
+
await ensureRModule();
|
|
1552
|
+
if (!rAssetBaseUrl) {
|
|
1553
|
+
throw new Error("R runtime base URL is not set");
|
|
1554
|
+
}
|
|
1555
|
+
const force = Boolean(data.force);
|
|
1556
|
+
const key = empackMountKey();
|
|
1557
|
+
if (!force && mountedRuntimeKey === key) {
|
|
1558
|
+
lucentInfo("[rWasmWorker] remount skipped (empack env unchanged)");
|
|
1559
|
+
replyOk(data.id);
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
const empackMetaUrl = new URL(config.empackMetaUrl, self.location.href).href;
|
|
1563
|
+
const empackPackagesBaseUrl = new URL(
|
|
1564
|
+
config.empackPackagesBaseUrl,
|
|
1565
|
+
self.location.href
|
|
1566
|
+
).href;
|
|
1567
|
+
await remountRHome(requireRModule(), {
|
|
1568
|
+
empackMetaUrl,
|
|
1569
|
+
empackPackagesBaseUrl,
|
|
1570
|
+
onDownloadProgress,
|
|
1571
|
+
onBootStatus
|
|
1572
|
+
});
|
|
1573
|
+
mountedRuntimeKey = key;
|
|
1574
|
+
replyOk(data.id);
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
log("error", `[rWasmWorker] remount R_HOME failed: ${formatRWasmError(err)}`);
|
|
1577
|
+
replyErr(data.id, err);
|
|
1578
|
+
}
|
|
1579
|
+
break;
|
|
1580
|
+
}
|
|
1581
|
+
case RWASM.STOP_APP: {
|
|
1582
|
+
try {
|
|
1583
|
+
await stopRunningApp();
|
|
1584
|
+
if (data.id != null) {
|
|
1585
|
+
postToHost({ type: RWASM.STOPPED, id: data.id });
|
|
1586
|
+
}
|
|
1587
|
+
} catch (err) {
|
|
1588
|
+
log("error", `[rWasmWorker] stop failed: ${formatRWasmError(err)}`);
|
|
1589
|
+
}
|
|
1590
|
+
break;
|
|
1591
|
+
}
|
|
1592
|
+
case RWASM.SERVICE_TICK: {
|
|
1593
|
+
try {
|
|
1594
|
+
await ensureRModule();
|
|
1595
|
+
let hadWork = false;
|
|
1596
|
+
let nextDelayMs = -1;
|
|
1597
|
+
await tasks.enqueueRTask(() => {
|
|
1598
|
+
if (!rModule) {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
tasks.evalRNow(SHINY_HOST.serviceOnceHadWork);
|
|
1602
|
+
try {
|
|
1603
|
+
const raw = rModule.FS.readFile("/tmp/lucent-service-had-work", {
|
|
1604
|
+
encoding: "utf8"
|
|
1605
|
+
}).trim();
|
|
1606
|
+
if (raw === "1" || raw === "0") {
|
|
1607
|
+
hadWork = raw === "1";
|
|
1608
|
+
nextDelayMs = -1;
|
|
1609
|
+
} else {
|
|
1610
|
+
const status = JSON.parse(raw);
|
|
1611
|
+
hadWork = Boolean(status.had);
|
|
1612
|
+
const ms = Number(status.nextMs);
|
|
1613
|
+
nextDelayMs = Number.isFinite(ms) && ms >= 0 ? ms : -1;
|
|
1614
|
+
}
|
|
1615
|
+
} catch {
|
|
1616
|
+
hadWork = false;
|
|
1617
|
+
nextDelayMs = -1;
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
postToHost({ type: RWASM.SERVICE_STATUS, hadWork, nextDelayMs });
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
log("error", `[rWasmWorker] service tick failed: ${formatRWasmError(err)}`);
|
|
1623
|
+
postToHost({ type: RWASM.SERVICE_STATUS, hadWork: false, nextDelayMs: -1 });
|
|
1624
|
+
}
|
|
1625
|
+
break;
|
|
1626
|
+
}
|
|
1627
|
+
case RWASM.GET_RESOURCE_PATHS: {
|
|
1628
|
+
try {
|
|
1629
|
+
const paths = await getShinyResourcePaths();
|
|
1630
|
+
postToHost({ type: RWASM.RESOURCE_PATHS, id: data.id, paths });
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
postToHost({
|
|
1633
|
+
type: RWASM.RESOURCE_PATHS,
|
|
1634
|
+
id: data.id,
|
|
1635
|
+
paths: {},
|
|
1636
|
+
error: formatRWasmError(err)
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
workerSelf.addEventListener("message", (event) => {
|
|
1644
|
+
void onMessage(event);
|
|
1645
|
+
});
|
|
1646
|
+
void ensureRModule().then(() => postToHost({ type: RWASM.READY })).catch((err) => {
|
|
1647
|
+
postToHost({
|
|
1648
|
+
type: RWASM.ERROR,
|
|
1649
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
/*! Bundled license information:
|
|
1653
|
+
|
|
1654
|
+
comlink/dist/esm/comlink.mjs:
|
|
1655
|
+
(**
|
|
1656
|
+
* @license
|
|
1657
|
+
* Copyright 2019 Google LLC
|
|
1658
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
1659
|
+
*)
|
|
1660
|
+
*/
|
|
1661
|
+
//# sourceMappingURL=rWasmWorker.js.map
|
|
1662
|
+
//# sourceMappingURL=rWasmWorker.js.map
|