@callstack/repack-dev-server
Version:
A bundler-agnostic development server for React Native applications as part of @callstack/repack.
152 lines (151 loc) • 5.01 kB
JavaScript
import * as prettyFormat from 'pretty-format';
import { WebSocketServer } from '../WebSocketServer.js';
/**
* Class for creating a WebSocket server to process events and reports.
*
* Based on: https://github.com/react-native-community/cli/blob/v4.14.0/packages/cli-server-api/src/websocket/eventsSocketServer.ts
*
* @category Development server
*/
export class WebSocketEventsServer extends WebSocketServer {
/**
* Create new instance of WebSocketHMRServer and attach it to the given Fastify instance.
* Any logging information, will be passed through standard `fastify.log` API.
*
* @param fastify Fastify instance to attach the WebSocket server to.
* @param config Configuration object.
*/
constructor(fastify, config) {
super(fastify, {
name: 'Events',
path: '/events',
wss: {
verifyClient: (({ origin }) => {
return /^(https?:\/\/localhost|file:\/\/)/.test(origin);
}),
},
});
this.config = config;
}
/**
* Parse received command message from connected client.
*
* @param data Stringified command message to parse.
* @returns Parsed command or `undefined` if parsing failed.
*/
parseMessage(data) {
try {
const message = JSON.parse(data);
if (message.version === WebSocketEventsServer.PROTOCOL_VERSION) {
return message;
}
this.fastify.log.error({
msg: 'Received message had wrong protocol version',
message,
});
}
catch {
this.fastify.log.error({
msg: 'Failed to parse the message as JSON',
data,
});
}
return undefined;
}
/**
* Stringify `message` into a format that can be transported as a `string`.
*
* @param message Message to serialize.
* @returns String representation of a `message` or `undefined` if serialization failed.
*/
serializeMessage(message) {
let toSerialize = message;
if (message.error && message.error instanceof Error) {
toSerialize = {
...message,
error: prettyFormat.default.default(message.error, {
escapeString: true,
highlight: true,
maxDepth: 3,
min: true,
}),
};
}
else if (message && message.type === 'client_log') {
toSerialize = {
...message,
data: message.data.map((item) => typeof item === 'string'
? item
: prettyFormat.default.default(item, {
escapeString: true,
highlight: true,
maxDepth: 3,
min: true,
plugins: [prettyFormat.plugins.ReactElement],
})),
};
}
try {
return JSON.stringify(toSerialize);
}
catch (error) {
this.fastify.log.error({ msg: 'Failed to serialize', error });
return undefined;
}
}
/**
* Broadcast event to all connected clients.
*
* @param event Event message to broadcast.
*/
broadcastEvent(event) {
if (!this.clients.size) {
return;
}
const serialized = this.serializeMessage(event);
if (!serialized) {
return;
}
for (const [clientId, socket] of this.clients.entries()) {
try {
socket.send(serialized);
}
catch (error) {
this.fastify.log.error({
msg: 'Failed to send broadcast to client',
clientId,
error,
_skipBroadcast: true,
});
}
}
}
onConnection(socket, request) {
const clientId = super.onConnection(socket, request);
socket.addEventListener('message', (event) => {
const message = this.parseMessage(event.data.toString());
if (!message) {
return;
}
if (message.type === 'command') {
try {
this.config.webSocketMessageServer.broadcast(message.command, message.params);
}
catch (error) {
this.fastify.log.error({
msg: 'Failed to forward message to clients',
error,
});
}
}
else {
this.fastify.log.error({
msg: 'Unknown message type',
message,
});
}
});
return clientId;
}
}
WebSocketEventsServer.PROTOCOL_VERSION = 2;