UNPKG

3.72 kBJavaScriptView Raw
1import WebSocket from 'ws';
2export class EsmHmrEngine {
3 constructor(options = {}) {
4 this.clients = new Set();
5 this.dependencyTree = new Map();
6 const wss = options.server
7 ? new WebSocket.Server({ noServer: true })
8 : new WebSocket.Server({ port: 12321 });
9 if (options.server) {
10 options.server.on('upgrade', (req, socket, head) => {
11 // Only handle upgrades to ESM-HMR requests, ignore others.
12 if (req.headers['sec-websocket-protocol'] !== 'esm-hmr') {
13 return;
14 }
15 wss.handleUpgrade(req, socket, head, (client) => {
16 wss.emit('connection', client, req);
17 });
18 });
19 }
20 wss.on('connection', (client) => {
21 this.connectClient(client);
22 this.registerListener(client);
23 });
24 }
25 registerListener(client) {
26 client.on('message', (data) => {
27 const message = JSON.parse(data.toString());
28 if (message.type === 'hotAccept') {
29 const entry = this.getEntry(message.id, true);
30 entry.isHmrAccepted = true;
31 entry.isHmrEnabled = true;
32 }
33 });
34 }
35 createEntry(sourceUrl) {
36 const newEntry = {
37 dependencies: new Set(),
38 dependents: new Set(),
39 needsReplacement: false,
40 isHmrEnabled: false,
41 isHmrAccepted: false,
42 };
43 this.dependencyTree.set(sourceUrl, newEntry);
44 return newEntry;
45 }
46 getEntry(sourceUrl, createIfNotFound = false) {
47 const result = this.dependencyTree.get(sourceUrl);
48 if (result) {
49 return result;
50 }
51 if (createIfNotFound) {
52 return this.createEntry(sourceUrl);
53 }
54 return null;
55 }
56 setEntry(sourceUrl, imports, isHmrEnabled = false) {
57 const result = this.getEntry(sourceUrl, true);
58 const outdatedDependencies = new Set(result.dependencies);
59 result.isHmrEnabled = isHmrEnabled;
60 for (const importUrl of imports) {
61 this.addRelationship(sourceUrl, importUrl);
62 outdatedDependencies.delete(importUrl);
63 }
64 for (const importUrl of outdatedDependencies) {
65 this.removeRelationship(sourceUrl, importUrl);
66 }
67 }
68 removeRelationship(sourceUrl, importUrl) {
69 let importResult = this.getEntry(importUrl);
70 importResult && importResult.dependents.delete(sourceUrl);
71 const sourceResult = this.getEntry(sourceUrl);
72 sourceResult && sourceResult.dependencies.delete(importUrl);
73 }
74 addRelationship(sourceUrl, importUrl) {
75 if (importUrl !== sourceUrl) {
76 let importResult = this.getEntry(importUrl, true);
77 importResult.dependents.add(sourceUrl);
78 const sourceResult = this.getEntry(sourceUrl, true);
79 sourceResult.dependencies.add(importUrl);
80 }
81 }
82 markEntryForReplacement(entry, state) {
83 entry.needsReplacement = state;
84 }
85 broadcastMessage(data) {
86 this.clients.forEach((client) => {
87 if (client.readyState === WebSocket.OPEN) {
88 client.send(JSON.stringify(data));
89 }
90 else {
91 this.disconnectClient(client);
92 }
93 });
94 }
95 connectClient(client) {
96 this.clients.add(client);
97 }
98 disconnectClient(client) {
99 client.terminate();
100 this.clients.delete(client);
101 }
102 disconnectAllClients() {
103 for (const client of this.clients) {
104 this.disconnectClient(client);
105 }
106 }
107}