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,1056 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var DEFAULT_TRANSPORT_BASE_URL = "/lucent/dist/";
|
|
3
|
+
var DEFAULT_SERVICE_WORKER_URL = "/httpuv-sw.js";
|
|
4
|
+
var DEFAULT_R_RUNTIME_BASE_URL = "/runtime/";
|
|
5
|
+
var DEFAULT_EMPACK_META_URL = "/packages/empack_env_meta.json";
|
|
6
|
+
var DEFAULT_SHINY_BASE_URL = "/";
|
|
7
|
+
var LUCENT_CONFIG_PARAM = "lucentConfig";
|
|
8
|
+
function configOverridesFromUrl() {
|
|
9
|
+
try {
|
|
10
|
+
const search = globalThis.location?.search;
|
|
11
|
+
if (!search) {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
const raw = new URLSearchParams(search).get(LUCENT_CONFIG_PARAM);
|
|
15
|
+
if (!raw) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
function resolveAgainstLocation(url) {
|
|
27
|
+
const base = typeof self !== "undefined" && self.location?.href ? self.location.href : "http://localhost/";
|
|
28
|
+
return new URL(url, base);
|
|
29
|
+
}
|
|
30
|
+
function resolveLucentConfig(overrides = {}) {
|
|
31
|
+
const fromGlobal = globalThis.__LUCENT__ ?? {};
|
|
32
|
+
const fromUrl = configOverridesFromUrl();
|
|
33
|
+
const pick = (key) => overrides[key] ?? fromUrl[key] ?? fromGlobal[key];
|
|
34
|
+
const empackMetaUrl = pick("empackMetaUrl") ?? DEFAULT_EMPACK_META_URL;
|
|
35
|
+
const empackPackagesBaseUrl = pick("empackPackagesBaseUrl") ?? new URL(".", resolveAgainstLocation(empackMetaUrl)).href;
|
|
36
|
+
return {
|
|
37
|
+
transportBaseUrl: pick("transportBaseUrl") ?? DEFAULT_TRANSPORT_BASE_URL,
|
|
38
|
+
serviceWorkerUrl: pick("serviceWorkerUrl") ?? DEFAULT_SERVICE_WORKER_URL,
|
|
39
|
+
rRuntimeBaseUrl: pick("rRuntimeBaseUrl") ?? DEFAULT_R_RUNTIME_BASE_URL,
|
|
40
|
+
empackMetaUrl,
|
|
41
|
+
empackPackagesBaseUrl,
|
|
42
|
+
shinyBaseUrl: pick("shinyBaseUrl") ?? DEFAULT_SHINY_BASE_URL
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/debug.ts
|
|
47
|
+
function isLucentDebug() {
|
|
48
|
+
if (globalThis.__HTTPUV_DEBUG__) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem("shinyForgeDebug") === "1") {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const loc = typeof location !== "undefined" ? location : typeof self !== "undefined" && "location" in self ? self.location : void 0;
|
|
56
|
+
if (loc?.href) {
|
|
57
|
+
const params = new URL(loc.href).searchParams;
|
|
58
|
+
if (params.has("httpuvDebug") || params.get("debug") === "httpuv") {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function lucentInfo(...args) {
|
|
67
|
+
if (!isLucentDebug()) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.info(...args);
|
|
71
|
+
}
|
|
72
|
+
function forwardRWorkerLog(level, text) {
|
|
73
|
+
if (level === "error") {
|
|
74
|
+
console.error(`[rWasmWorker] ${text}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(`[rWasmWorker] ${text}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/rwasm-constants.ts
|
|
81
|
+
var RWASM = {
|
|
82
|
+
READY: "rwasm_ready",
|
|
83
|
+
EVAL: "rwasm_eval",
|
|
84
|
+
EVAL_RESULT: "rwasm_eval_result",
|
|
85
|
+
STOP_APP: "rwasm_stop_app",
|
|
86
|
+
REMOUNT_R_HOME: "rwasm_remount_r_home",
|
|
87
|
+
STOPPED: "rwasm_stopped",
|
|
88
|
+
COMLINK_PORT: "rwasm_comlink_port",
|
|
89
|
+
COMLINK_READY: "rwasm_comlink_ready",
|
|
90
|
+
GET_RESOURCE_PATHS: "rwasm_get_resource_paths",
|
|
91
|
+
RESOURCE_PATHS: "rwasm_resource_paths",
|
|
92
|
+
LOG: "rwasm_log",
|
|
93
|
+
/** Worker → main: empack package download progress. */
|
|
94
|
+
DOWNLOAD_PROGRESS: "rwasm_download_progress",
|
|
95
|
+
/** Worker → main: coarse boot phase updates after / around downloads. */
|
|
96
|
+
BOOT_STATUS: "rwasm_boot_status",
|
|
97
|
+
ERROR: "rwasm_error",
|
|
98
|
+
/** Main → worker: run one shiny::serviceOnce tick. */
|
|
99
|
+
SERVICE_TICK: "rwasm_service_tick",
|
|
100
|
+
/** Worker → main: result of a SERVICE_TICK (`hadWork` / `nextDelayMs`). */
|
|
101
|
+
SERVICE_STATUS: "rwasm_service_status",
|
|
102
|
+
/** Worker → main: schedule an immediate MessageChannel wake. */
|
|
103
|
+
NEED_SERVICE: "rwasm_need_service",
|
|
104
|
+
/** Worker → main: delayed wake (`delayMs`) via setTimeout then MessageChannel.
|
|
105
|
+
* Used when scheduleTask runs outside a SERVICE_TICK; idle delays after a tick
|
|
106
|
+
* go through SERVICE_STATUS.nextDelayMs instead. */
|
|
107
|
+
SCHEDULE_DELAY: "rwasm_schedule_delay"
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/rWasmServiceDriver.ts
|
|
111
|
+
function createServiceDriver(options = {}) {
|
|
112
|
+
let worker = null;
|
|
113
|
+
let rafPending = false;
|
|
114
|
+
let rafId = 0;
|
|
115
|
+
let tickInFlight = false;
|
|
116
|
+
let wakeQueued = false;
|
|
117
|
+
let soonPending = false;
|
|
118
|
+
let stopped = true;
|
|
119
|
+
const delayTimers = /* @__PURE__ */ new Set();
|
|
120
|
+
let delayDeadline = Infinity;
|
|
121
|
+
const soonChannel = new MessageChannel();
|
|
122
|
+
soonChannel.port1.onmessage = () => {
|
|
123
|
+
if (!soonPending) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
soonPending = false;
|
|
127
|
+
if (stopped || !worker) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
postTick();
|
|
131
|
+
};
|
|
132
|
+
function clearSoon() {
|
|
133
|
+
soonPending = false;
|
|
134
|
+
}
|
|
135
|
+
function clearDelayOnly() {
|
|
136
|
+
for (const id of delayTimers) {
|
|
137
|
+
clearTimeout(id);
|
|
138
|
+
}
|
|
139
|
+
delayTimers.clear();
|
|
140
|
+
delayDeadline = Infinity;
|
|
141
|
+
}
|
|
142
|
+
function clearDelayTimers() {
|
|
143
|
+
clearDelayOnly();
|
|
144
|
+
clearSoon();
|
|
145
|
+
}
|
|
146
|
+
function cancelRaf() {
|
|
147
|
+
if (rafPending && rafId) {
|
|
148
|
+
cancelAnimationFrame(rafId);
|
|
149
|
+
}
|
|
150
|
+
rafPending = false;
|
|
151
|
+
rafId = 0;
|
|
152
|
+
}
|
|
153
|
+
function postTick() {
|
|
154
|
+
if (!worker || stopped) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (tickInFlight) {
|
|
158
|
+
wakeQueued = true;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
tickInFlight = true;
|
|
162
|
+
wakeQueued = false;
|
|
163
|
+
worker.postMessage({ type: RWASM.SERVICE_TICK });
|
|
164
|
+
}
|
|
165
|
+
function onFrame() {
|
|
166
|
+
rafPending = false;
|
|
167
|
+
rafId = 0;
|
|
168
|
+
if (stopped || !worker) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
postTick();
|
|
172
|
+
}
|
|
173
|
+
function wake() {
|
|
174
|
+
if (stopped || !worker) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (document.visibilityState !== "visible") {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (tickInFlight) {
|
|
181
|
+
wakeQueued = true;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (rafPending) {
|
|
185
|
+
wakeQueued = true;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
rafPending = true;
|
|
189
|
+
rafId = requestAnimationFrame(onFrame);
|
|
190
|
+
}
|
|
191
|
+
function wakeSoon() {
|
|
192
|
+
if (stopped || !worker) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (tickInFlight) {
|
|
196
|
+
wakeQueued = true;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (soonPending) {
|
|
200
|
+
wakeQueued = true;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
soonPending = true;
|
|
204
|
+
soonChannel.port2.postMessage(0);
|
|
205
|
+
}
|
|
206
|
+
function scheduleFollowUp() {
|
|
207
|
+
wakeSoon();
|
|
208
|
+
wake();
|
|
209
|
+
}
|
|
210
|
+
function scheduleDelay(delayMs) {
|
|
211
|
+
if (stopped || !worker) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const ms = Math.max(0, Number(delayMs) || 0);
|
|
215
|
+
if (ms <= 0) {
|
|
216
|
+
scheduleFollowUp();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const deadline = performance.now() + ms;
|
|
220
|
+
if (deadline >= delayDeadline) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
clearDelayOnly();
|
|
224
|
+
delayDeadline = deadline;
|
|
225
|
+
const id = setTimeout(() => {
|
|
226
|
+
delayTimers.delete(id);
|
|
227
|
+
delayDeadline = Infinity;
|
|
228
|
+
scheduleFollowUp();
|
|
229
|
+
}, ms);
|
|
230
|
+
delayTimers.add(id);
|
|
231
|
+
}
|
|
232
|
+
function onWorkerMessage(event) {
|
|
233
|
+
const data = event.data;
|
|
234
|
+
if (!data || typeof data.type !== "string") {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (data.type === RWASM.SERVICE_STATUS) {
|
|
238
|
+
tickInFlight = false;
|
|
239
|
+
const status = data;
|
|
240
|
+
const hadWork = Boolean(status.hadWork);
|
|
241
|
+
const nextDelayMs = Number(status.nextDelayMs);
|
|
242
|
+
if (stopped) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (hadWork || wakeQueued) {
|
|
246
|
+
if (hadWork) {
|
|
247
|
+
options.onHadWork?.();
|
|
248
|
+
}
|
|
249
|
+
wakeQueued = false;
|
|
250
|
+
scheduleFollowUp();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (Number.isFinite(nextDelayMs) && nextDelayMs >= 0) {
|
|
254
|
+
scheduleDelay(nextDelayMs);
|
|
255
|
+
} else {
|
|
256
|
+
clearDelayOnly();
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (data.type === RWASM.NEED_SERVICE) {
|
|
261
|
+
scheduleFollowUp();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (data.type === RWASM.SCHEDULE_DELAY) {
|
|
265
|
+
scheduleDelay(Number(data.delayMs) || 0);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function onVisibilityChange() {
|
|
269
|
+
if (stopped) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (document.visibilityState === "visible") {
|
|
273
|
+
scheduleFollowUp();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function attach(next) {
|
|
277
|
+
if (worker === next && !stopped) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
stop();
|
|
281
|
+
worker = next;
|
|
282
|
+
stopped = false;
|
|
283
|
+
worker.addEventListener("message", onWorkerMessage);
|
|
284
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
285
|
+
}
|
|
286
|
+
function stop() {
|
|
287
|
+
stopped = true;
|
|
288
|
+
tickInFlight = false;
|
|
289
|
+
wakeQueued = false;
|
|
290
|
+
cancelRaf();
|
|
291
|
+
clearDelayTimers();
|
|
292
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
293
|
+
if (worker) {
|
|
294
|
+
worker.removeEventListener("message", onWorkerMessage);
|
|
295
|
+
worker = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { wake, scheduleDelay, attach, stop };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/transport.ts
|
|
302
|
+
var cached = null;
|
|
303
|
+
function transportUrl(baseUrl) {
|
|
304
|
+
const base = new URL(baseUrl, self.location.href);
|
|
305
|
+
return new URL("httpuv-web.js", base).href;
|
|
306
|
+
}
|
|
307
|
+
function loadTransport(baseUrl) {
|
|
308
|
+
if (!cached) {
|
|
309
|
+
const url = transportUrl(baseUrl);
|
|
310
|
+
cached = import(
|
|
311
|
+
/* @vite-ignore */
|
|
312
|
+
/* webpackIgnore: true */
|
|
313
|
+
url
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return cached;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/wiring.ts
|
|
320
|
+
var HANDOFF_ATTEMPTS = 3;
|
|
321
|
+
var HANDOFF_ACK_TIMEOUT_MS = 5e3;
|
|
322
|
+
var COMLINK_READY_TIMEOUT_MS = 3e4;
|
|
323
|
+
async function connectHttpuvComlink(rWorker2, portHandoffType, options = {}) {
|
|
324
|
+
const {
|
|
325
|
+
ackType,
|
|
326
|
+
attempts = HANDOFF_ATTEMPTS,
|
|
327
|
+
ackTimeoutMs = HANDOFF_ACK_TIMEOUT_MS,
|
|
328
|
+
readyTimeoutMs = COMLINK_READY_TIMEOUT_MS
|
|
329
|
+
} = options;
|
|
330
|
+
const ready = watchWorkerComlinkReady(rWorker2);
|
|
331
|
+
const deadline = Date.now() + readyTimeoutMs;
|
|
332
|
+
const maxAttempts = ackType ? Math.max(1, attempts) : 1;
|
|
333
|
+
let connected = false;
|
|
334
|
+
let acked = false;
|
|
335
|
+
try {
|
|
336
|
+
for (let attempt = 1; attempt <= maxAttempts && !connected && !acked; attempt += 1) {
|
|
337
|
+
const ack = ackType ? watchServiceWorkerMessage(ackType) : null;
|
|
338
|
+
try {
|
|
339
|
+
postComlinkPorts(rWorker2, portHandoffType);
|
|
340
|
+
const outcome = await raceHandshake(ready.promise, ack?.promise, ackTimeoutMs);
|
|
341
|
+
connected = outcome === "ready";
|
|
342
|
+
acked = outcome === "acked";
|
|
343
|
+
if (outcome === "pending") {
|
|
344
|
+
lucentInfo(
|
|
345
|
+
`[lucent] service worker did not confirm the Comlink port handoff (attempt ${attempt}/${maxAttempts})`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
} finally {
|
|
349
|
+
ack?.cancel();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (!connected) {
|
|
353
|
+
await waitFor(
|
|
354
|
+
ready.promise,
|
|
355
|
+
Math.max(0, deadline - Date.now()),
|
|
356
|
+
() => comlinkTimeoutError(Boolean(ackType), acked)
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
lucentInfo("[lucent] service worker <-> R worker connected (unified port)");
|
|
360
|
+
} finally {
|
|
361
|
+
ready.cancel();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function postComlinkPorts(rWorker2, portHandoffType) {
|
|
365
|
+
const controller = navigator.serviceWorker.controller;
|
|
366
|
+
if (!controller) {
|
|
367
|
+
throw new Error("Service worker controller is not available for Comlink setup");
|
|
368
|
+
}
|
|
369
|
+
const channel = new MessageChannel();
|
|
370
|
+
controller.postMessage({ type: portHandoffType }, [channel.port1]);
|
|
371
|
+
rWorker2.postMessage({ type: RWASM.COMLINK_PORT }, [channel.port2]);
|
|
372
|
+
}
|
|
373
|
+
function comlinkTimeoutError(canAck, acked) {
|
|
374
|
+
if (!canAck) {
|
|
375
|
+
return new Error("Comlink setup timed out");
|
|
376
|
+
}
|
|
377
|
+
if (acked) {
|
|
378
|
+
return new Error(
|
|
379
|
+
"Comlink setup timed out: the service worker took the port but the R worker never completed setup"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return new Error(
|
|
383
|
+
"Comlink setup timed out: the service worker never acknowledged the port handoff"
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
async function raceHandshake(ready, ack, timeoutMs) {
|
|
387
|
+
const timer = timeoutWatcher(timeoutMs);
|
|
388
|
+
try {
|
|
389
|
+
return await Promise.race([
|
|
390
|
+
ready.then(() => "ready"),
|
|
391
|
+
...ack ? [ack.then(() => "acked")] : [],
|
|
392
|
+
timer.promise.then(() => "pending")
|
|
393
|
+
]);
|
|
394
|
+
} finally {
|
|
395
|
+
timer.cancel();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function waitFor(promise, timeoutMs, onTimeout) {
|
|
399
|
+
const timer = timeoutWatcher(timeoutMs);
|
|
400
|
+
try {
|
|
401
|
+
const timedOut = await Promise.race([
|
|
402
|
+
promise.then(() => false),
|
|
403
|
+
timer.promise.then(() => true)
|
|
404
|
+
]);
|
|
405
|
+
if (timedOut) {
|
|
406
|
+
throw onTimeout();
|
|
407
|
+
}
|
|
408
|
+
} finally {
|
|
409
|
+
timer.cancel();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function timeoutWatcher(ms) {
|
|
413
|
+
let timer;
|
|
414
|
+
const promise = new Promise((resolve) => {
|
|
415
|
+
timer = setTimeout(resolve, ms);
|
|
416
|
+
});
|
|
417
|
+
return { promise, cancel: () => clearTimeout(timer) };
|
|
418
|
+
}
|
|
419
|
+
function watchWorkerComlinkReady(rWorker2) {
|
|
420
|
+
let cleanup = () => {
|
|
421
|
+
};
|
|
422
|
+
const promise = new Promise((resolve, reject) => {
|
|
423
|
+
const onMessage = (event) => {
|
|
424
|
+
if (event.data?.type === RWASM.COMLINK_READY) {
|
|
425
|
+
cleanup();
|
|
426
|
+
resolve();
|
|
427
|
+
}
|
|
428
|
+
if (event.data?.type === RWASM.ERROR) {
|
|
429
|
+
cleanup();
|
|
430
|
+
reject(new Error(event.data.message ?? "R worker Comlink setup failed"));
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
cleanup = () => rWorker2.removeEventListener("message", onMessage);
|
|
434
|
+
rWorker2.addEventListener("message", onMessage);
|
|
435
|
+
});
|
|
436
|
+
return { promise, cancel: () => cleanup() };
|
|
437
|
+
}
|
|
438
|
+
function watchServiceWorkerMessage(type) {
|
|
439
|
+
let cleanup = () => {
|
|
440
|
+
};
|
|
441
|
+
const promise = new Promise((resolve) => {
|
|
442
|
+
const onMessage = (event) => {
|
|
443
|
+
if (event.data?.type === type) {
|
|
444
|
+
cleanup();
|
|
445
|
+
resolve();
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
cleanup = () => navigator.serviceWorker.removeEventListener("message", onMessage);
|
|
449
|
+
navigator.serviceWorker.addEventListener("message", onMessage);
|
|
450
|
+
});
|
|
451
|
+
return { promise, cancel: () => cleanup() };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/runApp.ts
|
|
455
|
+
var RUN_WEB_APP_R = `
|
|
456
|
+
local({
|
|
457
|
+
loadNamespace("shiny")
|
|
458
|
+
shiny::startApp(appDir = "webApp", port = 3838L, host = "127.0.0.1", launch.browser = FALSE, quiet = TRUE)
|
|
459
|
+
})
|
|
460
|
+
`;
|
|
461
|
+
var config = resolveLucentConfig();
|
|
462
|
+
var transport = null;
|
|
463
|
+
var rWorker = null;
|
|
464
|
+
var rWorkerPromise = null;
|
|
465
|
+
var comlinkConnected = false;
|
|
466
|
+
var comlinkPromise = null;
|
|
467
|
+
var comlinkHandshakeInFlight = false;
|
|
468
|
+
var swRegistrationPromise = null;
|
|
469
|
+
var httpuvReadyPromise = null;
|
|
470
|
+
var evalSeq = 0;
|
|
471
|
+
var serviceDriver = null;
|
|
472
|
+
var HTTPUV_SW_RELOAD_KEY = "httpuv-sw-reload";
|
|
473
|
+
function requireTransport() {
|
|
474
|
+
if (!transport) {
|
|
475
|
+
throw new Error("[lucent] transport not loaded yet");
|
|
476
|
+
}
|
|
477
|
+
return transport;
|
|
478
|
+
}
|
|
479
|
+
function shinyBaseUrl() {
|
|
480
|
+
return new URL(config.shinyBaseUrl, self.location.href).href;
|
|
481
|
+
}
|
|
482
|
+
function shinyPrefix() {
|
|
483
|
+
return requireTransport().resolveShinyPrefix(shinyBaseUrl());
|
|
484
|
+
}
|
|
485
|
+
function appUrl(subpath = "") {
|
|
486
|
+
return requireTransport().shinyAppUrl(subpath, shinyBaseUrl());
|
|
487
|
+
}
|
|
488
|
+
function serviceWorkerScriptUrl() {
|
|
489
|
+
const url = new URL(config.serviceWorkerUrl, self.location.href);
|
|
490
|
+
url.searchParams.set("shinyPrefix", shinyPrefix());
|
|
491
|
+
return url;
|
|
492
|
+
}
|
|
493
|
+
function serviceWorkerScope() {
|
|
494
|
+
return new URL("./", serviceWorkerScriptUrl()).pathname;
|
|
495
|
+
}
|
|
496
|
+
function announceHostToServiceWorker() {
|
|
497
|
+
const t = requireTransport();
|
|
498
|
+
const prefix = shinyPrefix();
|
|
499
|
+
const msg = {
|
|
500
|
+
type: t.MSG.REGISTER_HOST,
|
|
501
|
+
shinyPrefix: prefix
|
|
502
|
+
};
|
|
503
|
+
const controller = navigator.serviceWorker.controller;
|
|
504
|
+
if (controller) {
|
|
505
|
+
controller.postMessage(msg);
|
|
506
|
+
lucentInfo("[httpuv] Announced host to service worker");
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
async function waitForServiceWorkerController(timeoutMs = 3e3) {
|
|
512
|
+
if (navigator.serviceWorker.controller) {
|
|
513
|
+
return navigator.serviceWorker.controller;
|
|
514
|
+
}
|
|
515
|
+
await navigator.serviceWorker.ready;
|
|
516
|
+
if (navigator.serviceWorker.controller) {
|
|
517
|
+
return navigator.serviceWorker.controller;
|
|
518
|
+
}
|
|
519
|
+
return new Promise((resolve, reject) => {
|
|
520
|
+
const deadline = setTimeout(() => {
|
|
521
|
+
navigator.serviceWorker.removeEventListener("controllerchange", onController);
|
|
522
|
+
reject(new Error("timeout"));
|
|
523
|
+
}, timeoutMs);
|
|
524
|
+
const onController = () => {
|
|
525
|
+
if (navigator.serviceWorker.controller) {
|
|
526
|
+
clearTimeout(deadline);
|
|
527
|
+
navigator.serviceWorker.removeEventListener("controllerchange", onController);
|
|
528
|
+
resolve(navigator.serviceWorker.controller);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
navigator.serviceWorker.addEventListener("controllerchange", onController);
|
|
532
|
+
onController();
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async function waitForServiceWorkerInstall(reg, timeoutMs = 15e3) {
|
|
536
|
+
const worker = reg.waiting ?? reg.installing ?? reg.active;
|
|
537
|
+
if (!worker) {
|
|
538
|
+
await navigator.serviceWorker.ready;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (worker.state === "activated" || worker.state === "installed") {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
await new Promise((resolve, reject) => {
|
|
545
|
+
const onStateChange = () => {
|
|
546
|
+
if (worker.state === "activated" || worker.state === "installed" || worker.state === "redundant") {
|
|
547
|
+
cleanup();
|
|
548
|
+
resolve();
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
const deadline = setTimeout(() => {
|
|
552
|
+
cleanup();
|
|
553
|
+
if (worker.state === "installed") {
|
|
554
|
+
resolve();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
reject(new Error(`Service worker activation timed out (state: ${worker.state})`));
|
|
558
|
+
}, timeoutMs);
|
|
559
|
+
const cleanup = () => {
|
|
560
|
+
clearTimeout(deadline);
|
|
561
|
+
worker.removeEventListener("statechange", onStateChange);
|
|
562
|
+
};
|
|
563
|
+
worker.addEventListener("statechange", onStateChange);
|
|
564
|
+
onStateChange();
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function serviceWorkerScriptPath() {
|
|
568
|
+
return serviceWorkerScriptUrl().href.split("?")[0];
|
|
569
|
+
}
|
|
570
|
+
function controllerMatchesExpectedScript() {
|
|
571
|
+
const expected = serviceWorkerScriptPath();
|
|
572
|
+
const current = navigator.serviceWorker.controller?.scriptURL.split("?")[0];
|
|
573
|
+
return current === expected;
|
|
574
|
+
}
|
|
575
|
+
async function cleanupStaleHttpuvServiceWorkers() {
|
|
576
|
+
const expected = serviceWorkerScriptPath();
|
|
577
|
+
for (const reg of await navigator.serviceWorker.getRegistrations()) {
|
|
578
|
+
const scriptUrls = [reg.active, reg.waiting, reg.installing].filter((worker) => worker != null).map((worker) => worker.scriptURL.split("?")[0]);
|
|
579
|
+
const isHttpuv = scriptUrls.some((url) => url.endsWith("/httpuv-sw.js"));
|
|
580
|
+
if (!isHttpuv) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (!scriptUrls.includes(expected)) {
|
|
584
|
+
lucentInfo("[httpuv] Unregistering stale service worker", scriptUrls);
|
|
585
|
+
await reg.unregister();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function registerHttpuvServiceWorker() {
|
|
590
|
+
if (!("serviceWorker" in navigator)) {
|
|
591
|
+
console.warn("[httpuv] Service workers are not supported in this browser");
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
await cleanupStaleHttpuvServiceWorkers();
|
|
596
|
+
const reg = await navigator.serviceWorker.register(serviceWorkerScriptUrl(), {
|
|
597
|
+
scope: serviceWorkerScope(),
|
|
598
|
+
updateViaCache: "none"
|
|
599
|
+
});
|
|
600
|
+
await waitForServiceWorkerInstall(reg);
|
|
601
|
+
if (reg.waiting) {
|
|
602
|
+
reg.waiting.postMessage({ type: "SKIP_WAITING" });
|
|
603
|
+
await waitForServiceWorkerInstall(reg, 5e3).catch(() => void 0);
|
|
604
|
+
}
|
|
605
|
+
if (!navigator.serviceWorker.controller || !controllerMatchesExpectedScript()) {
|
|
606
|
+
const reloaded = sessionStorage.getItem(HTTPUV_SW_RELOAD_KEY);
|
|
607
|
+
if (!reloaded) {
|
|
608
|
+
sessionStorage.setItem(HTTPUV_SW_RELOAD_KEY, "1");
|
|
609
|
+
lucentInfo("[httpuv] Service worker installed \u2014 reloading once to activate");
|
|
610
|
+
window.location.reload();
|
|
611
|
+
await new Promise(() => {
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
console.warn(
|
|
615
|
+
"[httpuv] Page still not controlled by the expected worker; check Application \u2192 Service Workers for httpuv-sw.js errors"
|
|
616
|
+
);
|
|
617
|
+
} else {
|
|
618
|
+
sessionStorage.removeItem(HTTPUV_SW_RELOAD_KEY);
|
|
619
|
+
}
|
|
620
|
+
await waitForServiceWorkerController().catch(() => void 0);
|
|
621
|
+
announceHostToServiceWorker();
|
|
622
|
+
lucentInfo("[httpuv] Service worker registered", {
|
|
623
|
+
scope: reg.scope,
|
|
624
|
+
shinyPrefix: shinyPrefix(),
|
|
625
|
+
controller: Boolean(navigator.serviceWorker.controller)
|
|
626
|
+
});
|
|
627
|
+
return reg;
|
|
628
|
+
} catch (err) {
|
|
629
|
+
console.error("[httpuv] Service worker registration failed:", err);
|
|
630
|
+
throw err;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function ensureHttpuvServiceWorker() {
|
|
634
|
+
if (!swRegistrationPromise) {
|
|
635
|
+
swRegistrationPromise = registerHttpuvServiceWorker();
|
|
636
|
+
}
|
|
637
|
+
return swRegistrationPromise;
|
|
638
|
+
}
|
|
639
|
+
function postToRWorker(worker, msg, transfer = []) {
|
|
640
|
+
const id = msg.id ?? `m${++evalSeq}`;
|
|
641
|
+
const payload = { ...msg, id };
|
|
642
|
+
return new Promise((resolve, reject) => {
|
|
643
|
+
const onMessage = (event) => {
|
|
644
|
+
const data = event.data;
|
|
645
|
+
if (!data || data.id !== id) {
|
|
646
|
+
if (data?.type === RWASM.ERROR && !msg.id) {
|
|
647
|
+
worker.removeEventListener("message", onMessage);
|
|
648
|
+
reject(new Error(data.message ?? "R worker failed"));
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (data.type === RWASM.EVAL_RESULT || data.type === RWASM.STOPPED || data.type === RWASM.RESOURCE_PATHS) {
|
|
653
|
+
worker.removeEventListener("message", onMessage);
|
|
654
|
+
if (data.type === RWASM.STOPPED || data.type === RWASM.RESOURCE_PATHS || data.ok) {
|
|
655
|
+
resolve(data);
|
|
656
|
+
} else {
|
|
657
|
+
reject(new Error(data.error ?? "eval failed"));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
worker.addEventListener("message", onMessage);
|
|
662
|
+
worker.postMessage(payload, transfer);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function workerConfigParam() {
|
|
666
|
+
const base = self.location.href;
|
|
667
|
+
const abs = (u) => u == null ? void 0 : new URL(u, base).href;
|
|
668
|
+
const payload = {
|
|
669
|
+
transportBaseUrl: abs(config.transportBaseUrl),
|
|
670
|
+
serviceWorkerUrl: abs(config.serviceWorkerUrl),
|
|
671
|
+
rRuntimeBaseUrl: abs(config.rRuntimeBaseUrl),
|
|
672
|
+
empackMetaUrl: abs(config.empackMetaUrl),
|
|
673
|
+
empackPackagesBaseUrl: abs(config.empackPackagesBaseUrl),
|
|
674
|
+
shinyBaseUrl: abs(config.shinyBaseUrl)
|
|
675
|
+
};
|
|
676
|
+
return JSON.stringify(payload);
|
|
677
|
+
}
|
|
678
|
+
function dispatchLucentEvent(name, detail) {
|
|
679
|
+
window.dispatchEvent(new CustomEvent(name, { detail }));
|
|
680
|
+
}
|
|
681
|
+
function reportBootStatus(phase, message, percent) {
|
|
682
|
+
dispatchLucentEvent("lucent:boot-status", { phase, message, percent });
|
|
683
|
+
}
|
|
684
|
+
function installWorkerUiEvents(worker) {
|
|
685
|
+
worker.addEventListener("message", (event) => {
|
|
686
|
+
const data = event.data;
|
|
687
|
+
if (data?.type === RWASM.LOG) {
|
|
688
|
+
forwardRWorkerLog(data.level === "error" ? "error" : "log", data.text ?? "");
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (data?.type === RWASM.DOWNLOAD_PROGRESS) {
|
|
692
|
+
dispatchLucentEvent("lucent:download-progress", {
|
|
693
|
+
percent: data.percent ?? null,
|
|
694
|
+
downloadedBytes: data.downloadedBytes ?? 0,
|
|
695
|
+
totalBytes: data.totalBytes ?? 0
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (data?.type === RWASM.BOOT_STATUS) {
|
|
700
|
+
dispatchLucentEvent("lucent:boot-status", {
|
|
701
|
+
phase: data.phase ?? "",
|
|
702
|
+
message: data.message ?? "",
|
|
703
|
+
percent: data.percent ?? null
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function createRWorker() {
|
|
709
|
+
const workerUrl = new URL("./rWasmWorker.js", import.meta.url);
|
|
710
|
+
workerUrl.searchParams.set(LUCENT_CONFIG_PARAM, workerConfigParam());
|
|
711
|
+
if (requireTransport().isHttpuvDebug()) {
|
|
712
|
+
workerUrl.searchParams.set("httpuvDebug", "1");
|
|
713
|
+
}
|
|
714
|
+
const worker = new Worker(workerUrl, { type: "module" });
|
|
715
|
+
installWorkerUiEvents(worker);
|
|
716
|
+
return new Promise((resolve, reject) => {
|
|
717
|
+
const onBoot = (event) => {
|
|
718
|
+
const data = event.data;
|
|
719
|
+
if (data?.type === RWASM.LOG || data?.type === RWASM.DOWNLOAD_PROGRESS || data?.type === RWASM.BOOT_STATUS) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (data?.type === RWASM.READY) {
|
|
723
|
+
worker.removeEventListener("message", onBoot);
|
|
724
|
+
lucentInfo("[runApp] R.wasm worker ready");
|
|
725
|
+
resolve(worker);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (data?.type === RWASM.ERROR) {
|
|
729
|
+
worker.removeEventListener("message", onBoot);
|
|
730
|
+
reject(new Error(data.message ?? "R worker bootstrap failed"));
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
worker.addEventListener("message", onBoot);
|
|
734
|
+
worker.addEventListener("error", (event) => {
|
|
735
|
+
worker.removeEventListener("message", onBoot);
|
|
736
|
+
const detail = [event.message, event.filename, event.lineno].filter(Boolean).join(" ");
|
|
737
|
+
reject(new Error(detail ? `R worker failed to load: ${detail}` : "R worker failed to load"));
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
async function ensureRWorker() {
|
|
742
|
+
if (rWorker) {
|
|
743
|
+
return rWorker;
|
|
744
|
+
}
|
|
745
|
+
if (!rWorkerPromise) {
|
|
746
|
+
rWorkerPromise = createRWorker().then((worker) => {
|
|
747
|
+
rWorker = worker;
|
|
748
|
+
if (!serviceDriver) {
|
|
749
|
+
serviceDriver = createServiceDriver({
|
|
750
|
+
onHadWork: scheduleDebouncedResourcePathSync
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
serviceDriver.attach(worker);
|
|
754
|
+
return worker;
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
return rWorkerPromise;
|
|
758
|
+
}
|
|
759
|
+
function ensureComlinkConnected() {
|
|
760
|
+
if (comlinkPromise && (comlinkConnected || comlinkHandshakeInFlight)) {
|
|
761
|
+
return comlinkPromise;
|
|
762
|
+
}
|
|
763
|
+
comlinkHandshakeInFlight = true;
|
|
764
|
+
const handshake = (async () => {
|
|
765
|
+
lucentInfo("[runApp] Waiting for R worker and service worker\u2026");
|
|
766
|
+
const [worker] = await Promise.all([ensureRWorker(), ensureHttpuvServiceWorker()]);
|
|
767
|
+
if (!navigator.serviceWorker.controller || !controllerMatchesExpectedScript()) {
|
|
768
|
+
throw new Error("Service worker controller is not available");
|
|
769
|
+
}
|
|
770
|
+
lucentInfo("[runApp] Connecting Comlink\u2026");
|
|
771
|
+
const { COMLINK } = requireTransport();
|
|
772
|
+
await connectHttpuvComlink(worker, COMLINK.PORT_HANDOFF, {
|
|
773
|
+
ackType: COMLINK.PORT_HANDOFF_ACK
|
|
774
|
+
});
|
|
775
|
+
comlinkConnected = true;
|
|
776
|
+
})();
|
|
777
|
+
const clearInFlight = () => {
|
|
778
|
+
comlinkHandshakeInFlight = false;
|
|
779
|
+
};
|
|
780
|
+
handshake.then(clearInFlight, clearInFlight);
|
|
781
|
+
comlinkPromise = handshake;
|
|
782
|
+
return handshake;
|
|
783
|
+
}
|
|
784
|
+
function invalidateComlink() {
|
|
785
|
+
comlinkConnected = false;
|
|
786
|
+
httpuvReadyPromise = null;
|
|
787
|
+
if (!comlinkHandshakeInFlight) {
|
|
788
|
+
comlinkPromise = null;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function reconnectComlinkAfterServiceWorkerUpdate() {
|
|
792
|
+
invalidateComlink();
|
|
793
|
+
announceHostToServiceWorker();
|
|
794
|
+
void ensureComlinkConnected().catch((err) => {
|
|
795
|
+
console.warn("[httpuv] Comlink reconnect after service worker update failed:", err);
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
async function ensureHttpuvReady() {
|
|
799
|
+
const t = requireTransport();
|
|
800
|
+
t.setShinyPrefix(shinyPrefix());
|
|
801
|
+
if (comlinkConnected && comlinkPromise) {
|
|
802
|
+
return comlinkPromise;
|
|
803
|
+
}
|
|
804
|
+
httpuvReadyPromise = ensureComlinkConnected();
|
|
805
|
+
return httpuvReadyPromise;
|
|
806
|
+
}
|
|
807
|
+
function stopRunningApp() {
|
|
808
|
+
navigator.serviceWorker.controller?.postMessage({ type: requireTransport().MSG.STOP });
|
|
809
|
+
serviceDriver?.stop();
|
|
810
|
+
if (rWorker) {
|
|
811
|
+
rWorker.postMessage({ type: RWASM.STOP_APP });
|
|
812
|
+
}
|
|
813
|
+
lucentInfo("[runApp] App stopped");
|
|
814
|
+
}
|
|
815
|
+
function loadViewerFrame() {
|
|
816
|
+
const frame = document.getElementById("app-frame");
|
|
817
|
+
const url = appUrl();
|
|
818
|
+
if (frame) {
|
|
819
|
+
frame.src = url;
|
|
820
|
+
}
|
|
821
|
+
lucentInfo("[runApp] Viewer iframe \u2192", url);
|
|
822
|
+
}
|
|
823
|
+
function clearAppDocumentCache() {
|
|
824
|
+
lastResourcePaths = {};
|
|
825
|
+
navigator.serviceWorker.controller?.postMessage({
|
|
826
|
+
type: requireTransport().MSG.CLEAR_APP_CACHE
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
var RESOURCE_PATH_SYNC_DEBOUNCE_MS = 100;
|
|
830
|
+
var resourcePathSyncTimer = null;
|
|
831
|
+
var lastResourcePaths = {};
|
|
832
|
+
var resourcePathSyncInFlight = false;
|
|
833
|
+
function scheduleDebouncedResourcePathSync() {
|
|
834
|
+
if (resourcePathSyncTimer) {
|
|
835
|
+
clearTimeout(resourcePathSyncTimer);
|
|
836
|
+
}
|
|
837
|
+
resourcePathSyncTimer = setTimeout(() => {
|
|
838
|
+
resourcePathSyncTimer = null;
|
|
839
|
+
if (rWorker) {
|
|
840
|
+
void syncResourcePathsIncremental(rWorker);
|
|
841
|
+
}
|
|
842
|
+
}, RESOURCE_PATH_SYNC_DEBOUNCE_MS);
|
|
843
|
+
}
|
|
844
|
+
function rememberResourcePaths(paths) {
|
|
845
|
+
lastResourcePaths = { ...paths };
|
|
846
|
+
}
|
|
847
|
+
async function syncResourcePathsToServiceWorker(worker) {
|
|
848
|
+
const controller = navigator.serviceWorker.controller;
|
|
849
|
+
if (!controller) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
const data = await postToRWorker(worker, { type: RWASM.GET_RESOURCE_PATHS });
|
|
854
|
+
const paths = data.paths ?? {};
|
|
855
|
+
rememberResourcePaths(paths);
|
|
856
|
+
controller.postMessage({ type: requireTransport().MSG.REGISTER_RESOURCE_PATHS, paths });
|
|
857
|
+
if (Object.keys(paths).length > 0) {
|
|
858
|
+
lucentInfo("[runApp] synced", Object.keys(paths).length, "resource path(s) to SW");
|
|
859
|
+
}
|
|
860
|
+
} catch (err) {
|
|
861
|
+
console.warn("[runApp] resource path sync failed; SW will fall through to R", err);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async function syncResourcePathsIncremental(worker) {
|
|
865
|
+
if (resourcePathSyncInFlight) {
|
|
866
|
+
scheduleDebouncedResourcePathSync();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const controller = navigator.serviceWorker.controller;
|
|
870
|
+
if (!controller) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
resourcePathSyncInFlight = true;
|
|
874
|
+
try {
|
|
875
|
+
const data = await postToRWorker(worker, { type: RWASM.GET_RESOURCE_PATHS });
|
|
876
|
+
const paths = data.paths ?? {};
|
|
877
|
+
const updateType = requireTransport().MSG.UPDATE_RESOURCE_PATH;
|
|
878
|
+
let changed = 0;
|
|
879
|
+
for (const prefix of Object.keys(paths)) {
|
|
880
|
+
if (lastResourcePaths[prefix] !== paths[prefix]) {
|
|
881
|
+
controller.postMessage({ type: updateType, prefix, dir: paths[prefix] });
|
|
882
|
+
changed += 1;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
for (const prefix of Object.keys(lastResourcePaths)) {
|
|
886
|
+
if (!(prefix in paths)) {
|
|
887
|
+
controller.postMessage({ type: updateType, prefix, dir: "" });
|
|
888
|
+
changed += 1;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
rememberResourcePaths(paths);
|
|
892
|
+
if (changed > 0) {
|
|
893
|
+
lucentInfo("[runApp] updated", changed, "resource path(s) on SW");
|
|
894
|
+
}
|
|
895
|
+
} catch (err) {
|
|
896
|
+
console.warn("[runApp] incremental resource path sync failed", err);
|
|
897
|
+
} finally {
|
|
898
|
+
resourcePathSyncInFlight = false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async function waitForShinyHttpReady(worker) {
|
|
902
|
+
const t = requireTransport();
|
|
903
|
+
const url = appUrl();
|
|
904
|
+
reportBootStatus("warmup", "Warming up Shiny\u2026", 92);
|
|
905
|
+
lucentInfo("[runApp] Warming up Shiny (may take a minute on first load)\u2026", url);
|
|
906
|
+
const res = await fetch(url, {
|
|
907
|
+
cache: "no-store",
|
|
908
|
+
headers: { [t.WARMUP_REQUEST_HEADER]: "1" },
|
|
909
|
+
signal: AbortSignal.timeout(t.REQUEST_TIMEOUT_MS)
|
|
910
|
+
});
|
|
911
|
+
if (!res.ok) {
|
|
912
|
+
throw new Error(`Shiny warmup GET ${url} failed: HTTP ${res.status}`);
|
|
913
|
+
}
|
|
914
|
+
lucentInfo("[runApp] Shiny warmup OK (HTTP", res.status + ")");
|
|
915
|
+
await syncResourcePathsToServiceWorker(worker);
|
|
916
|
+
}
|
|
917
|
+
async function runApp() {
|
|
918
|
+
const worker = await ensureRWorker();
|
|
919
|
+
clearAppDocumentCache();
|
|
920
|
+
await postToRWorker(worker, { type: RWASM.STOP_APP });
|
|
921
|
+
const forceRemount = new URLSearchParams(self.location.search).get("remountRHome") === "1";
|
|
922
|
+
await postToRWorker(worker, { type: RWASM.REMOUNT_R_HOME, force: forceRemount });
|
|
923
|
+
lucentInfo("[runApp] worker eval", RUN_WEB_APP_R);
|
|
924
|
+
reportBootStatus("start-app", "Starting Shiny\u2026", 85);
|
|
925
|
+
await postToRWorker(worker, {
|
|
926
|
+
type: RWASM.EVAL,
|
|
927
|
+
code: RUN_WEB_APP_R
|
|
928
|
+
});
|
|
929
|
+
serviceDriver?.attach(worker);
|
|
930
|
+
serviceDriver?.wake();
|
|
931
|
+
return 0;
|
|
932
|
+
}
|
|
933
|
+
async function startShinyApp() {
|
|
934
|
+
await ensureHttpuvReady();
|
|
935
|
+
const worker = await ensureRWorker();
|
|
936
|
+
await runApp();
|
|
937
|
+
invalidateComlink();
|
|
938
|
+
await ensureHttpuvReady();
|
|
939
|
+
await waitForShinyHttpReady(worker);
|
|
940
|
+
loadViewerFrame();
|
|
941
|
+
dispatchLucentEvent("lucent:ready");
|
|
942
|
+
}
|
|
943
|
+
function installHostServiceWorkerListeners() {
|
|
944
|
+
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
|
945
|
+
reconnectComlinkAfterServiceWorkerUpdate();
|
|
946
|
+
});
|
|
947
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
948
|
+
if (event.data?.type === requireTransport().MSG.REQUEST_COMLINK) {
|
|
949
|
+
reconnectComlinkAfterServiceWorkerUpdate();
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
const reassertHost = () => {
|
|
953
|
+
announceHostToServiceWorker();
|
|
954
|
+
if (!comlinkConnected) {
|
|
955
|
+
reconnectComlinkAfterServiceWorkerUpdate();
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
document.addEventListener("visibilitychange", () => {
|
|
959
|
+
if (document.visibilityState === "visible") {
|
|
960
|
+
reassertHost();
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
window.addEventListener("pageshow", reassertHost);
|
|
964
|
+
window.addEventListener("focus", () => {
|
|
965
|
+
announceHostToServiceWorker();
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
function installGlobalHelpers() {
|
|
969
|
+
const lucent = {
|
|
970
|
+
shinyUrl: (subpath = "") => appUrl(subpath),
|
|
971
|
+
ensureHttpuvReady,
|
|
972
|
+
stopRunningApp,
|
|
973
|
+
enableHttpuvDebug: () => requireTransport().enableHttpuvDebug(),
|
|
974
|
+
/** Quick check that the worker responds. */
|
|
975
|
+
ping: () => lucent.evalR("1+1"),
|
|
976
|
+
async evalR(code) {
|
|
977
|
+
const worker = await ensureRWorker();
|
|
978
|
+
await postToRWorker(worker, {
|
|
979
|
+
type: RWASM.EVAL,
|
|
980
|
+
code
|
|
981
|
+
});
|
|
982
|
+
},
|
|
983
|
+
startShinyApp: () => startShinyApp(),
|
|
984
|
+
async testVirtualSocket(message = '{"method":"ping"}') {
|
|
985
|
+
await ensureHttpuvReady();
|
|
986
|
+
const controller = navigator.serviceWorker.controller;
|
|
987
|
+
if (!controller) {
|
|
988
|
+
console.warn(
|
|
989
|
+
"[lucent] No service worker controller \u2014 fetch may not be intercepted; unregister old workers and hard-refresh"
|
|
990
|
+
);
|
|
991
|
+
throw new Error("no service worker controller");
|
|
992
|
+
}
|
|
993
|
+
const openUrl = new URL("__session__/open", appUrl());
|
|
994
|
+
lucentInfo("[lucent] testVirtualSocket: open", openUrl.href);
|
|
995
|
+
const openRes = await fetch(openUrl, { method: "POST" });
|
|
996
|
+
if (!openRes.ok) {
|
|
997
|
+
throw new Error(`session open failed: HTTP ${openRes.status} ${await openRes.text()}`);
|
|
998
|
+
}
|
|
999
|
+
const { handle } = await openRes.json();
|
|
1000
|
+
lucentInfo("[lucent] testVirtualSocket: handle", handle);
|
|
1001
|
+
const channel = new MessageChannel();
|
|
1002
|
+
const ack = new Promise((resolve, reject) => {
|
|
1003
|
+
const timer = setTimeout(() => reject(new Error("session ACK timed out")), 5e3);
|
|
1004
|
+
channel.port1.onmessage = (event) => {
|
|
1005
|
+
if (event.data?.type === requireTransport().MSG.SESSION_ACK) {
|
|
1006
|
+
clearTimeout(timer);
|
|
1007
|
+
resolve();
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
});
|
|
1011
|
+
controller.postMessage(
|
|
1012
|
+
{ type: requireTransport().MSG.REGISTER_SESSION, handle },
|
|
1013
|
+
[channel.port2]
|
|
1014
|
+
);
|
|
1015
|
+
await ack;
|
|
1016
|
+
lucentInfo("[lucent] testVirtualSocket: session port registered");
|
|
1017
|
+
const sendUrl = new URL(`__session__/send?handle=${encodeURIComponent(handle)}`, appUrl());
|
|
1018
|
+
const sendRes = await fetch(sendUrl, {
|
|
1019
|
+
method: "POST",
|
|
1020
|
+
headers: { "Content-Type": "text/plain" },
|
|
1021
|
+
body: message
|
|
1022
|
+
});
|
|
1023
|
+
if (!sendRes.ok && sendRes.status !== 204) {
|
|
1024
|
+
throw new Error(`session send failed: HTTP ${sendRes.status}`);
|
|
1025
|
+
}
|
|
1026
|
+
channel.port1.close();
|
|
1027
|
+
const result = { handle, registered: true, sendStatus: sendRes.status };
|
|
1028
|
+
lucentInfo("[lucent] testVirtualSocket: result", result);
|
|
1029
|
+
return result;
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
globalThis.__lucent = lucent;
|
|
1033
|
+
}
|
|
1034
|
+
async function main() {
|
|
1035
|
+
transport = await loadTransport(config.transportBaseUrl);
|
|
1036
|
+
transport.setShinyPrefix(shinyPrefix());
|
|
1037
|
+
installHostServiceWorkerListeners();
|
|
1038
|
+
installGlobalHelpers();
|
|
1039
|
+
if (isLucentDebug()) {
|
|
1040
|
+
lucentInfo("[runApp] httpuv debug tracing enabled (?httpuvDebug=1)");
|
|
1041
|
+
}
|
|
1042
|
+
void ensureHttpuvServiceWorker().catch((err) => {
|
|
1043
|
+
console.error("[httpuv] Service worker setup failed:", err);
|
|
1044
|
+
});
|
|
1045
|
+
await startShinyApp();
|
|
1046
|
+
}
|
|
1047
|
+
void main().catch((err) => {
|
|
1048
|
+
console.error("[runApp] Failed to start:", err);
|
|
1049
|
+
dispatchLucentEvent("lucent:error", {
|
|
1050
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
export { ensureHttpuvReady, runApp };
|
|
1055
|
+
//# sourceMappingURL=runApp.js.map
|
|
1056
|
+
//# sourceMappingURL=runApp.js.map
|