Permalink
Browse files

Add multi-client support for HMR

Summary:
This diff builds on top of the refactor to use `async/await` and adds multi-client support to Hot Module Reloading.

Thanks to async/await it's been quite straightforward to add this logic, since the only thing that I've had to do is to create a `Set` with the currently connected clients and passed the specified client to each method that was using the global client before.

This closes #14334

Reviewed By: davidaurelio

Differential Revision: D5611176

fbshipit-source-id: ec29438887342877c372b61132efada16af58fa5
  • Loading branch information...
rafeca authored and facebook-github-bot committed Aug 11, 2017
1 parent f32d0ee commit 8b2975ad7b27ce34dcf56836685fd54ddd62086c
Showing with 94 additions and 74 deletions.
  1. +94 −74 local-cli/server/util/attachHMRServer.js
@@ -18,6 +18,7 @@ const url = require('url');
import type {ResolutionResponse} from './getInverseDependencies'; import type {ResolutionResponse} from './getInverseDependencies';
import type {Server as HTTPServer} from 'http'; import type {Server as HTTPServer} from 'http';
import type {Server as HTTPSServer} from 'https'; import type {Server as HTTPSServer} from 'https';
import type {Client as WebSocketClient} from 'ws';
const blacklist = [ const blacklist = [
'Libraries/Utilities/HMRClient.js', 'Libraries/Utilities/HMRClient.js',
@@ -77,11 +78,25 @@ type Moduleish = {
function attachHMRServer<TModule: Moduleish>( function attachHMRServer<TModule: Moduleish>(
{httpServer, path, packagerServer}: HMROptions<TModule>, {httpServer, path, packagerServer}: HMROptions<TModule>,
) { ) {
let client = null; type Client = {|
ws: WebSocketClient,
platform: string,
bundleEntry: string,
dependenciesCache: Array<string>,
dependenciesModulesCache: {[mixed]: TModule},
shallowDependencies: {[string]: Array<TModule>},
inverseDependenciesCache: mixed,
|};
function disconnect() { const clients: Set<Client> = new Set();
client = null;
packagerServer.setHMRFileChangeListener(null); function disconnect(client: Client) {
clients.delete(client);
// If there are no clients connected, stop listenig for file changes
if (clients.size === 0) {
packagerServer.setHMRFileChangeListener(null);
}
} }
// For the give platform and entry file, returns a promise with: // For the give platform and entry file, returns a promise with:
@@ -175,11 +190,14 @@ function attachHMRServer<TModule: Moduleish>(
}; };
} }
async function prepareResponse(filename): Object { async function prepareResponse(
client: Client,
filename: string,
): Promise<?Object> {
try { try {
const bundle = await generateBundle(filename); const bundle = await generateBundle(client, filename);
if (!client || !bundle || bundle.isEmpty()) { if (!bundle || bundle.isEmpty()) {
return; return;
} }
@@ -217,11 +235,10 @@ function attachHMRServer<TModule: Moduleish>(
} }
} }
async function generateBundle(filename) { async function generateBundle(
if (client === null) { client: Client,
return; filename: string,
} ): Promise<?HMRBundle> {
const deps = await packagerServer.getShallowDependencies({ const deps = await packagerServer.getShallowDependencies({
dev: true, dev: true,
minify: false, minify: false,
@@ -231,10 +248,6 @@ function attachHMRServer<TModule: Moduleish>(
recursive: true, recursive: true,
}); });
if (client === null) {
return;
}
// if the file dependencies have change we need to invalidate the // if the file dependencies have change we need to invalidate the
// dependencies caches because the list of files we need to send // dependencies caches because the list of files we need to send
// to the client may have changed // to the client may have changed
@@ -270,20 +283,12 @@ function attachHMRServer<TModule: Moduleish>(
resolutionResponse: myResolutionReponse, resolutionResponse: myResolutionReponse,
} = await getDependencies(client.platform, client.bundleEntry); } = await getDependencies(client.platform, client.bundleEntry);
if (client === null) {
return;
}
const moduleToUpdate = await packagerServer.getModuleForPath(filename); const moduleToUpdate = await packagerServer.getModuleForPath(filename);
if (client === null) {
return;
}
// build list of modules for which we'll send HMR updates // build list of modules for which we'll send HMR updates
const modulesToUpdate = [moduleToUpdate]; const modulesToUpdate = [moduleToUpdate];
Object.keys(depsModulesCache).forEach(module => { Object.keys(depsModulesCache).forEach(module => {
if (!client || !client.dependenciesModulesCache[module]) { if (!client.dependenciesModulesCache[module]) {
modulesToUpdate.push(depsModulesCache[module]); modulesToUpdate.push(depsModulesCache[module]);
} }
}); });
@@ -310,7 +315,7 @@ function attachHMRServer<TModule: Moduleish>(
} }
// make sure the file was modified is part of the bundle // make sure the file was modified is part of the bundle
if (!client || !client.shallowDependencies[filename]) { if (!client.shallowDependencies[filename]) {
return; return;
} }
@@ -337,6 +342,44 @@ function attachHMRServer<TModule: Moduleish>(
return bundle; return bundle;
} }
function handleFileChange(
type: string,
filename: string,
): void {
clients.forEach(
client => sendFileChangeToClient(client, type, filename),
);
}
async function sendFileChangeToClient(
client: Client,
type: string,
filename: string,
): Promise<mixed> {
const blacklisted = blacklist.find(
blacklistedPath => filename.indexOf(blacklistedPath) !== -1,
);
if (blacklisted) {
return;
}
if (clients.has(client)) {
client.ws.send(JSON.stringify({type: 'update-start'}));
}
if (type !== 'delete') {
const response = await prepareResponse(client, filename);
if (response && clients.has(client)) {
client.ws.send(JSON.stringify(response));
}
}
if (clients.has(client)) {
client.ws.send(JSON.stringify({type: 'update-done'}));
}
}
const WebSocketServer = require('ws').Server; const WebSocketServer = require('ws').Server;
const wss = new WebSocketServer({ const wss = new WebSocketServer({
server: httpServer, server: httpServer,
@@ -347,58 +390,35 @@ function attachHMRServer<TModule: Moduleish>(
/* $FlowFixMe: url might be null */ /* $FlowFixMe: url might be null */
const params = querystring.parse(url.parse(ws.upgradeReq.url).query); const params = querystring.parse(url.parse(ws.upgradeReq.url).query);
try { const {
const { dependenciesCache,
dependenciesCache, dependenciesModulesCache,
dependenciesModulesCache, shallowDependencies,
shallowDependencies, inverseDependenciesCache,
inverseDependenciesCache, } = await getDependencies(params.platform, params.bundleEntry);
} = await getDependencies(params.platform, params.bundleEntry);
client = {
ws,
platform: params.platform,
bundleEntry: params.bundleEntry,
dependenciesCache,
dependenciesModulesCache,
shallowDependencies,
inverseDependenciesCache,
};
packagerServer.setHMRFileChangeListener(async (type, filename) => {
if (client === null) {
return;
}
const blacklisted = blacklist.find(
blacklistedPath => filename.indexOf(blacklistedPath) !== -1,
);
if (blacklisted) {
return;
}
client.ws.send(JSON.stringify({type: 'update-start'}));
if (type !== 'delete') {
const response = await prepareResponse(filename);
if (client && response) { const client = {
client.ws.send(JSON.stringify(response)); ws,
} platform: params.platform,
} bundleEntry: params.bundleEntry,
dependenciesCache,
dependenciesModulesCache,
shallowDependencies,
inverseDependenciesCache,
};
clients.add(client);
client.ws.send(JSON.stringify({type: 'update-done'})); // If this is the first client connecting, start listening to file changes
}); if (clients.size === 1) {
packagerServer.setHMRFileChangeListener(handleFileChange);
}
client.ws.on('error', e => { client.ws.on('error', e => {
console.error('[Hot Module Replacement] Unexpected error', e); console.error('[Hot Module Replacement] Unexpected error', e);
disconnect(); disconnect(client);
}); });
client.ws.on('close', () => disconnect()); client.ws.on('close', () => disconnect(client));
} catch (err) {
throw err;
}
}); });
} }

1 comment on commit 8b2975a

@clentfort

This comment has been minimized.

Show comment
Hide comment
@clentfort

clentfort commented on 8b2975a Aug 14, 2017

Nice

Please sign in to comment.