Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 19x 19x 12x 9x 3x 19x 9x 9x 7x 7x 19x 19x 5x 5x 4x 4x 4x 20x 9x 20x | /**
* HMR WebSocket Server
*
* Attaches a WebSocket server to an existing HTTP server (sharing the
* same port), tracks connected dev clients, and exposes a broadcast()
* helper that serializes a message once and fan-outs to every live
* client. Used by the Coherent dev server to push hot-update events
* to browser-side HMR clients.
*
* Wire protocol matches packages/client/src/hmr/client.js — server
* sends `{type, filePath?, webPath?, error?, updateType?}` objects;
* client switches on `type` and handles updates, reloads, errors.
*
* @module @coherent.js/cli/dev-server/hmr-server
*/
import { WebSocketServer } from 'ws';
import { isHostAllowed, isOriginAllowed } from './access.js';
/**
* @typedef {Object} HmrServer
* @property {(message: object) => void} broadcast - Serialize and send a JSON message to every live client.
* @property {() => void} close - Close the WebSocket server and drop all clients.
* @property {() => number} clientCount - Current number of live clients (for tests / diagnostics).
*/
/**
* Create and attach an HMR WebSocket server to an existing HTTP server.
*
* The WS server shares the HTTP server's port — clients connect to
* `ws://host:port` (no separate port to manage). New clients receive
* a `{type: 'connected'}` ack on open. Dead clients are pruned on
* the next broadcast.
*
* Upgrades are refused (403) when the Host header does not name this
* server or when a browser Origin belongs to another site: without that,
* any page the developer visits could subscribe to the change feed.
*
* @param {import('node:http').Server} httpServer - HTTP server to attach to.
* @param {import('./access.js').HostOptions} [options] - Accepted hosts (see access.js).
* @returns {HmrServer}
*/
export function createHmrServer(httpServer, options = {}) {
const hostOptions = { host: options.host, allowedHosts: options.allowedHosts ?? [] };
const wss = new WebSocketServer({
server: httpServer,
verifyClient: ({ origin, req }, done) => {
if (isHostAllowed(req.headers.host, hostOptions) && isOriginAllowed(origin, hostOptions)) {
done(true);
} else {
done(false, 403, 'Forbidden');
}
},
});
wss.on('connection', (socket) => {
// Defer the initial ack by one event-loop turn so the client-side
// 'open' event has time to resolve before the message arrives.
// Without this, the 'message' event fires synchronously during the
// WS handshake — before the client can attach its first listener.
setTimeout(() => {
if (socket.readyState === socket.OPEN) {
try {
socket.send(JSON.stringify({ type: 'connected' }));
} catch {
// Client may have disconnected mid-handshake; ignore.
}
}
}, 0);
});
// Surface listener errors instead of crashing the dev server.
wss.on('error', (err) => {
console.warn('[coherent dev] HMR server error:', err.message);
});
return {
broadcast(message) {
const frame = JSON.stringify(message); // throws on circular — surfaces caller bug loudly
for (const client of wss.clients) {
Eif (client.readyState === client.OPEN) {
try {
client.send(frame);
} catch {
// Dead socket — let `ws` clean it up on its own close event.
}
}
}
},
close() {
// terminate(), not close(): close() waits up to 30s for the browser to
// answer the closing handshake, and a tab that doesn't (busy, or
// already reconnecting) held the dev server's shutdown that long.
for (const client of wss.clients) {
try { client.terminate(); } catch { /* ignore */ }
}
wss.close();
},
clientCount() {
let n = 0;
for (const c of wss.clients) if (c.readyState === c.OPEN) n++;
return n;
},
};
}
|