@webjsdev/server 0.8.46 → 0.8.47
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.
- package/package.json +1 -1
- package/src/dev-reload-worker.js +53 -0
- package/src/dev.js +90 -7
package/package.json
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev live-reload SharedWorker relay (#887), the BROWSER half. Kept as a
|
|
3
|
+
* standalone browser-safe module (no node imports) so the served worker inlines
|
|
4
|
+
* the EXACT source a browser test drives, with no drift, the same pattern as
|
|
5
|
+
* `dev-overlay.js` (#264). `reloadWorkerJs` in dev.js reads this file, strips
|
|
6
|
+
* the `export` keyword, and appends a
|
|
7
|
+
* `startReloadWorker(self, EventSource, '<eventsUrl>')` call.
|
|
8
|
+
*
|
|
9
|
+
* One SharedWorker is shared across every tab of the origin (a SharedWorker is
|
|
10
|
+
* keyed by its script URL), so it holds the ONE `EventSource` to
|
|
11
|
+
* `/__webjs/events` and fans each `reload` / `webjs-error` out to every tab over
|
|
12
|
+
* its `MessagePort`. Tab count never touches the browser's per-host HTTP/1.1
|
|
13
|
+
* connection cap, which the per-tab `EventSource` it replaces used to exhaust.
|
|
14
|
+
*
|
|
15
|
+
* @param {{ onconnect: any }} scope the worker global (`self`)
|
|
16
|
+
* @param {new (url: string) => any} EventSourceCtor the `EventSource` constructor
|
|
17
|
+
* @param {string} eventsUrl the base-path-aware `/__webjs/events` URL
|
|
18
|
+
*/
|
|
19
|
+
export function startReloadWorker(scope, EventSourceCtor, eventsUrl) {
|
|
20
|
+
/** @type {Set<any>} */
|
|
21
|
+
const ports = new Set();
|
|
22
|
+
/** @type {string | null} the last error frame, cached for late-joining tabs */
|
|
23
|
+
let lastError = null;
|
|
24
|
+
|
|
25
|
+
// A MessagePort has no reliable close event, so prune a port when a post to it
|
|
26
|
+
// throws (a closed tab). Some browsers silently no-op instead of throwing,
|
|
27
|
+
// leaving a dead port in the set, but that is a harmless dev-only no-op and
|
|
28
|
+
// the set is bounded by the tabs opened in one session.
|
|
29
|
+
function fanout(msg) {
|
|
30
|
+
for (const p of ports) {
|
|
31
|
+
try { p.postMessage(msg); } catch (_) { ports.delete(p); }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const es = new EventSourceCtor(eventsUrl);
|
|
36
|
+
es.addEventListener('reload', () => { lastError = null; fanout({ type: 'reload' }); });
|
|
37
|
+
es.addEventListener('webjs-error', (e) => { lastError = e.data; fanout({ type: 'webjs-error', data: e.data }); });
|
|
38
|
+
|
|
39
|
+
scope.onconnect = (e) => {
|
|
40
|
+
const port = e.ports[0];
|
|
41
|
+
ports.add(port);
|
|
42
|
+
port.start();
|
|
43
|
+
// A tab that connects AFTER a breaking edit still needs the current overlay.
|
|
44
|
+
// The single shared EventSource already consumed the server's replay (#264),
|
|
45
|
+
// so the worker caches the last error and hands it to each new tab itself.
|
|
46
|
+
if (lastError != null) {
|
|
47
|
+
try { port.postMessage({ type: 'webjs-error', data: lastError }); } catch (_) { ports.delete(port); }
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Returned for tests; the served worker ignores it.
|
|
52
|
+
return { ports, es };
|
|
53
|
+
}
|
package/src/dev.js
CHANGED
|
@@ -1745,6 +1745,14 @@ async function tryServeFrameworkStatic(path, method, ctx) {
|
|
|
1745
1745
|
});
|
|
1746
1746
|
}
|
|
1747
1747
|
|
|
1748
|
+
// Dev live-reload SharedWorker: one shared connection for all tabs (#887).
|
|
1749
|
+
if (path === '/__webjs/reload-worker.js') {
|
|
1750
|
+
if (!dev) return new Response('Not found', { status: 404 });
|
|
1751
|
+
return new Response(reloadWorkerJs(basePath()), {
|
|
1752
|
+
headers: { 'content-type': 'application/javascript; charset=utf-8' },
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1748
1756
|
// Core module: /__webjs/core/*
|
|
1749
1757
|
//
|
|
1750
1758
|
// ETag + ~1h max-age, NOT immutable. The URL path is un-versioned
|
|
@@ -1822,7 +1830,8 @@ async function handleCore(req, ctx) {
|
|
|
1822
1830
|
|
|
1823
1831
|
// Health / readiness probes (`/__webjs/health`, `/__webjs/ready`) and the
|
|
1824
1832
|
// framework-internal static assets (`/__webjs/core/*`, `/__webjs/reload.js`,
|
|
1825
|
-
// downloaded `/__webjs/vendor/*`) are served in
|
|
1833
|
+
// `/__webjs/reload-worker.js`, downloaded `/__webjs/vendor/*`) are served in
|
|
1834
|
+
// `handle()` BEFORE ensureReady,
|
|
1826
1835
|
// so they are not repeated here. This fallback covers the (currently
|
|
1827
1836
|
// unreachable) case of handleCore being entered for one of those assets, so
|
|
1828
1837
|
// the routing stays correct if a future caller bypasses the early path.
|
|
@@ -2259,7 +2268,25 @@ async function sendWebResponse(res, webRes, req, opts) {
|
|
|
2259
2268
|
*/
|
|
2260
2269
|
async function fileResponse(abs, opts) {
|
|
2261
2270
|
try {
|
|
2262
|
-
|
|
2271
|
+
let data = await readFile(abs);
|
|
2272
|
+
// In dev an external watcher (tailwindcss --watch, esbuild, ...) rewrites a
|
|
2273
|
+
// public asset with truncate-then-write, so the file is 0 bytes for a short
|
|
2274
|
+
// window during a rebuild. A hot reload that lands in that window would
|
|
2275
|
+
// serve empty CSS / JS and the page paints unstyled (#891). Close that
|
|
2276
|
+
// 0-byte window: when an empty read comes from a file that was JUST modified
|
|
2277
|
+
// (the mid-rewrite signal), re-read a few times so the truncated read does
|
|
2278
|
+
// not reach the browser. A genuinely empty, untouched asset is served
|
|
2279
|
+
// immediately (no delay), and a non-zero-but-partial write is a smaller
|
|
2280
|
+
// residual gap this does not cover. Prod has no such watcher and is
|
|
2281
|
+
// left untouched.
|
|
2282
|
+
if (opts.dev && data.length === 0) {
|
|
2283
|
+
let midRewrite = false;
|
|
2284
|
+
try { midRewrite = Date.now() - (await stat(abs)).mtimeMs < 500; } catch { /* gone */ }
|
|
2285
|
+
for (let i = 0; midRewrite && i < 12 && data.length === 0; i++) {
|
|
2286
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
2287
|
+
try { data = await readFile(abs); } catch { break; }
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2263
2290
|
const type = MIME[extname(abs).toLowerCase()] || 'application/octet-stream';
|
|
2264
2291
|
// The body is fully buffered (read into `data`), so opt it into the
|
|
2265
2292
|
// conditional-GET funnel, which is the single place that hashes the bytes
|
|
@@ -2609,18 +2636,74 @@ function locatePackageDir(appDir, pkgName) {
|
|
|
2609
2636
|
const DEV_OVERLAY_SRC = readFileSync(new URL('./dev-overlay.js', import.meta.url), 'utf8')
|
|
2610
2637
|
.replace(/^export /gm, '');
|
|
2611
2638
|
|
|
2639
|
+
// The reload SharedWorker relay source (#887), read once + `export`-stripped so
|
|
2640
|
+
// it inlines into the served worker script. Sharing the one source
|
|
2641
|
+
// (`dev-reload-worker.js`, which the browser test imports directly) means the
|
|
2642
|
+
// test drives the EXACT relay code that ships, with no drift (same as #264).
|
|
2643
|
+
const RELOAD_WORKER_SRC = readFileSync(new URL('./dev-reload-worker.js', import.meta.url), 'utf8')
|
|
2644
|
+
.replace(/^export /gm, '');
|
|
2645
|
+
|
|
2612
2646
|
function reloadClientJs(bp) {
|
|
2613
2647
|
// The overlay renderer uses textContent throughout (never innerHTML), so the
|
|
2614
2648
|
// error message / code frame can never inject markup (#264). Served only in
|
|
2615
2649
|
// dev (the /__webjs/reload.js branch 404s in prod), so it never reaches a
|
|
2616
2650
|
// production page.
|
|
2651
|
+
//
|
|
2652
|
+
// Every tab shares ONE live-reload connection through a SharedWorker (#887).
|
|
2653
|
+
// The old client opened an `EventSource` per tab, and because the dev server
|
|
2654
|
+
// is HTTP/1.1 and browsers cap concurrent connections per host at ~6, a
|
|
2655
|
+
// handful of open tabs would hold every connection slot with idle SSE streams
|
|
2656
|
+
// and later tabs could not even fetch their HTML. The SharedWorker holds the
|
|
2657
|
+
// single `EventSource` (see reloadWorkerJs) and relays each `reload` /
|
|
2658
|
+
// `webjs-error` to every tab. The overlay still renders on the main thread
|
|
2659
|
+
// (a worker has no DOM), so the worker forwards only the raw frame data.
|
|
2660
|
+
//
|
|
2661
|
+
// Fallback: where `SharedWorker` is missing (some mobile browsers) or its
|
|
2662
|
+
// construction throws (a strict dev CSP without `worker-src`), each tab opens
|
|
2663
|
+
// its own `EventSource`, the original behaviour. Correct, just not shared.
|
|
2664
|
+
const eventsUrl = JSON.stringify(withBasePath('/__webjs/events', bp));
|
|
2665
|
+
const workerUrl = JSON.stringify(withBasePath('/__webjs/reload-worker.js', bp));
|
|
2617
2666
|
return `// webjs dev reload client
|
|
2618
2667
|
${DEV_OVERLAY_SRC}
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
es.addEventListener('webjs-error', (e) => {
|
|
2622
|
-
let f; try { f = JSON.parse(e.data); } catch (_) { return; }
|
|
2668
|
+
function __webjsApplyError(data) {
|
|
2669
|
+
let f; try { f = JSON.parse(data); } catch (_) { return; }
|
|
2623
2670
|
renderDevOverlay(f);
|
|
2624
|
-
}
|
|
2671
|
+
}
|
|
2672
|
+
function __webjsDirectEvents() {
|
|
2673
|
+
const es = new EventSource(${eventsUrl});
|
|
2674
|
+
es.addEventListener('reload', () => location.reload());
|
|
2675
|
+
es.addEventListener('webjs-error', (e) => __webjsApplyError(e.data));
|
|
2676
|
+
}
|
|
2677
|
+
try {
|
|
2678
|
+
if (typeof SharedWorker !== 'undefined') {
|
|
2679
|
+
const w = new SharedWorker(${workerUrl});
|
|
2680
|
+
w.port.onmessage = (e) => {
|
|
2681
|
+
const m = e.data || {};
|
|
2682
|
+
if (m.type === 'reload') location.reload();
|
|
2683
|
+
else if (m.type === 'webjs-error') __webjsApplyError(m.data);
|
|
2684
|
+
};
|
|
2685
|
+
w.port.start();
|
|
2686
|
+
} else {
|
|
2687
|
+
__webjsDirectEvents();
|
|
2688
|
+
}
|
|
2689
|
+
} catch (_) {
|
|
2690
|
+
__webjsDirectEvents();
|
|
2691
|
+
}
|
|
2692
|
+
`;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
/**
|
|
2696
|
+
* The dev live-reload SharedWorker. One instance is shared across every tab of
|
|
2697
|
+
* the same origin (a SharedWorker is keyed by its script URL), so it holds the
|
|
2698
|
+
* ONE `EventSource` to `/__webjs/events` and fans each event out to all tabs
|
|
2699
|
+
* over their `MessagePort`s (#887). The `EventSource` URL carries the base path
|
|
2700
|
+
* the same way the client's does (#256). Served only in dev.
|
|
2701
|
+
* @param {string} bp the normalized base path (`''` = no-op)
|
|
2702
|
+
* @returns {string}
|
|
2703
|
+
*/
|
|
2704
|
+
function reloadWorkerJs(bp) {
|
|
2705
|
+
return `// webjs dev reload worker (one shared connection for all tabs)
|
|
2706
|
+
${RELOAD_WORKER_SRC}
|
|
2707
|
+
startReloadWorker(self, EventSource, ${JSON.stringify(withBasePath('/__webjs/events', bp))});
|
|
2625
2708
|
`;
|
|
2626
2709
|
}
|