UNPKG

@farmfe/core

Version:

Farm is a extremely fast web build tool written in Rust. Farm can start a project in milliseconds and perform HMR within 10ms, making it much faster than similar tools like webpack and vite.

221 lines 7.56 kB
import { WebSocket, WebSocketServer as WebSocketServerRaw } from 'ws'; import { Logger, red } from '../index.js'; const HMR_HEADER = 'farm_hmr'; const wsServerEvents = [ 'connection', 'error', 'headers', 'listening', 'message' ]; export default class WsServer { constructor(httpServer, config, hmrEngine, logger) { this.httpServer = httpServer; this.config = config; this.hmrEngine = hmrEngine; this.customListeners = new Map(); this.clientsMap = new WeakMap(); this.bufferedError = null; this.logger = logger ?? new Logger(); this.createWebSocketServer(); } createWebSocketServer() { try { const WebSocketServer = process.versions.bun ? // @ts-expect-error: Bun defines `import.meta.require` import.meta.require('ws').WebSocketServer : WebSocketServerRaw; this.wss = new WebSocketServer({ noServer: true }); this.connection(); // TODO IF not have httpServer this.httpServer.on('upgrade', this.upgradeWsServer.bind(this)); } catch (err) { this.handleSocketError(err); } } upgradeWsServer(request, socket, head) { if (this.isHMRRequest(request)) { this.handleHMRUpgrade(request, socket, head); } } listen() { // TODO alone with use httpServer we need start this function // Start listening for WebSocket connections } // Farm uses the `sendMessage` method in hmr and // the send method is reserved for migration vite send(...args) { let payload; if (typeof args[0] === 'string') { payload = { type: 'custom', event: args[0], data: args[1] }; } else { payload = args[0]; } if (payload.type === 'error' && !this.wss.clients.size) { this.bufferedError = payload; return; } const stringified = JSON.stringify(payload); this.wss.clients.forEach((client) => { // readyState 1 means the connection is open if (client.readyState === 1) { client.send(stringified); } }); } isHMRRequest(request) { return (request.url === this.config.hmr.path && request.headers['sec-websocket-protocol'] === HMR_HEADER); } handleHMRUpgrade(request, socket, head) { this.wss.handleUpgrade(request, socket, head, (ws) => { this.wss.emit('connection', ws, request); }); } get clients() { return new Set(Array.from(this.wss.clients).map(this.getSocketClient.bind(this))); } // a custom method defined by farm to send custom events sendCustomEvent(event, payload) { // Send a custom event to all clients this.send({ type: 'custom', event, data: payload }); } on(event, listener) { if (wsServerEvents.includes(event)) { this.wss.on(event, listener); } else { this.addCustomEventListener(event, listener); } } off(event, listener) { if (wsServerEvents.includes(event)) { this.wss.off(event, listener); } else { this.removeCustomEventListener(event, listener); } } connection() { this.wss.on('connection', (socket) => { socket.on('message', (raw) => { if (!this.customListeners.size) return; let parsed; try { parsed = JSON.parse(String(raw)); } catch { this.logger.error('Failed to parse WebSocket message: ' + raw); } // transform vite js-update to farm update if (parsed?.type === 'js-update' && parsed?.path) { this.hmrEngine.hmrUpdate(parsed.path, true); return; } if (!parsed || parsed.type !== 'custom' || !parsed.event) return; const listeners = this.customListeners.get(parsed.event); if (!listeners?.size) return; const client = this.getSocketClient(socket); listeners.forEach((listener) => listener(parsed.data, client)); }); socket.on('error', (err) => { return this.handleSocketError(err); }); socket.send(JSON.stringify({ type: 'connected' })); if (this.bufferedError) { socket.send(JSON.stringify(this.bufferedError)); this.bufferedError = null; } }); } async close() { if (this.upgradeWsServer && this.httpServer) { this.httpServer.off('upgrade', this.upgradeWsServer); } await this.terminateAllClients(); await this.closeWebSocketServer(); // TODO if not have httpServer we need close httpServer } terminateAllClients() { const terminatePromises = Array.from(this.wss.clients).map((client) => { return new Promise((resolve) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ type: 'closing' })); client.close(1000, 'Server shutdown'); } // Temporarily remove the direct shutdown of ws // client.terminate(); client.once('close', () => resolve(true)); }); }); return Promise.all(terminatePromises); } closeWebSocketServer() { return new Promise((resolve, reject) => { this.wss.close((err) => { if (err) { reject(err); } else { // TODO if not have httpServer resolve(true); } }); }); } addCustomEventListener(event, listener) { if (!this.customListeners.has(event)) { this.customListeners.set(event, new Set()); } this.customListeners.get(event).add(listener); } removeCustomEventListener(event, listener) { this.customListeners.get(event)?.delete(listener); } getSocketClient(socket) { if (!this.clientsMap.has(socket)) { this.clientsMap.set(socket, { send: (...args) => this.sendMessage(socket, ...args), socket, rawSend: (str) => socket.send(str) }); } return this.clientsMap.get(socket); } sendMessage(socket, ...args) { let payload; if (typeof args[0] === 'string') { payload = { type: 'custom', event: args[0], data: args[1] }; } else { payload = args[0]; } socket.send(JSON.stringify(payload)); } handleSocketError(err) { if (err.code === 'EADDRINUSE') { this.logger.error(red(`WebSocket server error: Port is already in use`), { error: err }); } else { this.logger.error(red(`WebSocket server error:\n${err.stack || err.message}`), { error: err }); } } } //# sourceMappingURL=ws.js.map