/
createEventsSocketEndpoint.ts
195 lines (182 loc) · 5.86 KB
/
createEventsSocketEndpoint.ts
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import {Server as WebSocketServer} from 'ws';
import {logger} from '@react-native-community/cli-tools';
import prettyFormat from 'pretty-format';
/**
* The eventsSocket websocket listens at the 'events/` for websocket
* connections, on which all Metro reports will be emitted.
*
* This is mostly useful for developer tools (clients) that wants to monitor Metro,
* and the apps connected to Metro.
*
* The eventsSocket provides the following features:
* - it reports any Metro event (that is reported through a reporter) to all clients
* - it reports any console.log's (and friends) from the connected app to all clients
* (as client_log event)
* - it allows connected clients to send commands through Metro to the connected app.
* This reuses the generic command mechanism.
* Two useful commands are 'reload' and 'devmenu'.
*/
type Command = {
version: number;
type: 'command';
command: string;
params?: any;
};
/**
* This number is used to version the communication protocol between
* Dev tooling like Flipper and Metro, so that in the future we can recognize
* messages coming from old clients, so that it will be simpler to implement
* backward compatibility.
*
* We start at 2 as the protocol is currently the same as used internally at FB,
* which happens to be at version 2 as well.
*/
const PROTOCOL_VERSION = 2;
function parseMessage<T extends Object>(data: string): T | undefined {
try {
const message = JSON.parse(data);
if (message.version === PROTOCOL_VERSION) {
return message;
}
logger.error(
'Received message had wrong protocol version: ' + message.version,
);
} catch {
logger.error('Failed to parse the message as JSON:\n' + data);
}
return undefined;
}
/**
* Two types of messages will arrive in this function,
* 1) messages generated by Metro itself (through the reporter abstraction)
* those are yet to be serialized, and can contain any kind of data structure
* 2) a specific event generated by Metro is `client_log`, which describes
* console.* calls in the app.
* The arguments send to the console are pretty printed so that they can be
* displayed in a nicer way in dev tools
*
* @param message
*/
function serializeMessage(message: any) {
// We do want to send Metro report messages, but their contents is not guaranteed to be serializable.
// For some known types we will pretty print otherwise not serializable parts first:
let toSerialize = message;
if (message && message.error && message.error instanceof Error) {
toSerialize = {
...message,
error: prettyFormat(message.error, {
escapeString: true,
highlight: true,
maxDepth: 3,
min: true,
}),
};
} else if (message && message.type === 'client_log') {
toSerialize = {
...message,
data: message.data.map((item: any) =>
typeof item === 'string'
? item
: prettyFormat(item, {
escapeString: true,
highlight: true,
maxDepth: 3,
min: true,
plugins: [prettyFormat.plugins.ReactElement],
}),
),
};
}
try {
return JSON.stringify(toSerialize);
} catch (e) {
logger.error('Failed to serialize: ' + e);
return null;
}
}
/**
* Starts the eventsSocket at the given path
*
*/
export default function createEventsSocketEndpoint(
broadcast: (method: string, params?: Record<string, any>) => void,
): {
server: WebSocketServer;
reportEvent: (event: any) => void;
} {
const wss = new WebSocketServer({
noServer: true,
verifyClient({origin}: {origin: string}) {
// This exposes the full JS logs and enables issuing commands like reload
// so let's make sure only locally running stuff can connect to it
// origin is only checked if it is set, e.g. when the request is made from a (CORS) browser
// any 'back-end' connection isn't CORS at all, and has full control over the origin header,
// so there is no point in checking it security wise
return (
!origin ||
origin.startsWith('http://localhost:') ||
origin.startsWith('file:')
);
},
});
const clients = new Map();
let nextClientId = 0;
/**
* broadCastEvent is called by reportEvent (below), which is called by the
* default reporter of this server, to make sure that all Metro events are
* broadcasted to all connected clients
* (that is, all devtools such as Flipper, _not_: connected apps)
*
* @param message
*/
function broadCastEvent(message: any) {
if (!clients.size) {
return;
}
const serialized = serializeMessage(message);
if (!serialized) {
return;
}
for (const ws of clients.values()) {
try {
ws.send(serialized);
} catch (e) {
logger.error(
`Failed to send broadcast to client due to:\n ${(e as any).toString()}`,
);
}
}
}
wss.on('connection', function (clientWs) {
const clientId = `client#${nextClientId++}`;
clients.set(clientId, clientWs);
clientWs.onclose = clientWs.onerror = () => {
clients.delete(clientId);
};
clientWs.onmessage = (event) => {
const message: Command | undefined = parseMessage(event.data.toString());
if (message == null) {
return;
}
if (message.type === 'command') {
try {
/**
* messageSocket.broadcast (not to be confused with our own broadcast above)
* forwards a command to all connected React Native applications.
*/
broadcast(message.command, message.params);
} catch (e) {
logger.error('Failed to forward message to clients: ', e as any);
}
} else {
logger.error('Unknown message type: ', message.type);
}
};
});
return {
server: wss,
reportEvent: (event: any) => {
broadCastEvent(event);
},
};
}