Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 67 additions & 15 deletions src/hot.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
/**
* @typedef {object} EventStream
* @property {(req: IncomingMessage, res: ServerResponse) => void} handler attach a new client
* @property {() => boolean} hasClients true when at least one client is connected
* @property {(payload: Payload | { action: string }) => void} publish publish a payload to every client
* @property {(res: ServerResponse, payload: Payload | { action: string }) => void} publishTo publish a payload to a single client
* @property {() => void} close end every client and stop the heartbeat
Expand Down Expand Up @@ -79,27 +80,48 @@ function createEventStream(heartbeat, logger) {
}
};

const interval = setInterval(() => {
everyClient((client) => {
client.write("data: 💓\n\n");
});
}, heartbeat);
// Runs only while clients are connected: started with the first client,
// stopped with the last one.
/** @type {ReturnType<typeof setInterval> | null} */
let interval = null;

// Don't block process exit on the heartbeat timer.
if (typeof interval.unref === "function") {
interval.unref();
}
const startHeartbeat = () => {
if (interval !== null) {
return;
}

interval = setInterval(() => {
everyClient((client) => {
client.write("data: 💓\n\n");
});
}, heartbeat);

// Don't block process exit on the heartbeat timer.
if (typeof interval.unref === "function") {
interval.unref();
}
};

const stopHeartbeat = () => {
if (interval !== null) {
clearInterval(interval);
interval = null;
}
};

return {
close() {
clearInterval(interval);
stopHeartbeat();
everyClient((client) => {
if (!client.writableEnded) {
client.end();
}
});
clients = new Map();
},
hasClients() {
return clients.size > 0;
},
handler(req, res) {
// A response another middleware already started can no longer become an
// SSE stream — end it instead of crashing on writeHead.
Expand Down Expand Up @@ -135,17 +157,28 @@ function createEventStream(heartbeat, logger) {

const id = clientId++;
clients.set(id, res);
startHeartbeat();
logger.log(`Client connected (${clients.size} active)`);

req.on("close", () => {
if (!res.writableEnded) {
res.end();
}
clients.delete(id);

if (clients.size === 0) {
stopHeartbeat();
}

logger.log(`Client disconnected (${clients.size} active)`);
});
},
publish(payload) {
// With no clients connected there is nothing to serialize for.
if (clients.size === 0) {
return;
}

const frame = `data: ${JSON.stringify(payload)}\n\n`;

everyClient((client) => {
Expand Down Expand Up @@ -261,6 +294,23 @@ function bundlePayload(stats, action) {
* @param {EventStream} eventStream event stream
*/
function publishBundles(bundles, previousBundles, eventStream) {
// Grouped once up front so pairing stays linear with many child compilers.
/** @type {Map<string, StatsCompilation[]>} */
const previousByName = new Map();

if (previousBundles !== null) {
for (const bundle of previousBundles) {
const name = bundle.name || "";
const group = previousByName.get(name);

if (group) {
group.push(bundle);
} else {
previousByName.set(name, [bundle]);
}
}
}

/** @type {Map<string, number>} */
const occurrences = new Map();

Expand All @@ -277,10 +327,8 @@ function publishBundles(bundles, previousBundles, eventStream) {
if (name) {
const occurrence = occurrences.get(name) || 0;
occurrences.set(name, occurrence + 1);
previous =
previousBundles.filter((bundle) => (bundle.name || "") === name)[
occurrence
] || null;
const group = previousByName.get(name);
previous = (group && group[occurrence]) || null;
} else {
previous = previousBundles[index] || null;
}
Expand Down Expand Up @@ -331,9 +379,13 @@ function createHot(compiler, userOptions) {

// Published only when the rounded percent changes to keep the stream small.
new webpack.ProgressPlugin((percent, message) => {
if (closed || !eventStream.hasClients()) {
return;
}

const rounded = Math.round(percent * 100);

if (closed || rounded === lastProgressPercent) {
if (rounded === lastProgressPercent) {
return;
}

Expand Down