All files / cli/src/dev-server hmr-server.js

73.91% Statements 17/23
33.33% Branches 2/6
71.42% Functions 5/7
80% Lines 16/20

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                                                                          8x   8x         5x 5x 5x 5x                 8x       8x   4x 4x 3x 3x 3x               9x 5x   9x                  
/**
 * 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';
 
/**
 * @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.
 *
 * @param {import('node:http').Server} httpServer - HTTP server to attach to.
 * @returns {HmrServer}
 */
export function createHmrServer(httpServer) {
  const wss = new WebSocketServer({ server: httpServer });
 
  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(() => {
      Eif (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() {
      for (const client of wss.clients) {
        try { client.close(); } catch { /* ignore */ }
      }
      wss.close();
    },
    clientCount() {
      let n = 0;
      for (const c of wss.clients) if (c.readyState === c.OPEN) n++;
      return n;
    },
  };
}