Skip to content

Commit be21be2

Browse files
committed
Add message channel
1 parent 131fead commit be21be2

File tree

5 files changed

+103
-30
lines changed

5 files changed

+103
-30
lines changed

hot.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
HotCore,
1212
ImportMap,
1313
Loader,
14+
MessageChannel,
1415
Plugin,
1516
URLTest,
1617
VFSRecord,
@@ -142,13 +143,39 @@ class Hot implements HotCore {
142143
return this;
143144
}
144145

145-
on(event: string, handler: (data: any) => void): () => void {
146-
// TODO
147-
return () => {};
148-
}
149-
150-
send(event: string, data?: any) {
151-
// TODO
146+
openMessageChannel(name: string): Promise<MessageChannel> {
147+
const conn = new EventSource(this.basePath + "@hot-events?channel=" + name);
148+
return new Promise((resolve, reject) => {
149+
const mc: MessageChannel = {
150+
postMessage: (data) => {
151+
fetch(
152+
this.basePath + "@hot-events?channel=" + name,
153+
{
154+
method: "POST",
155+
body: stringify(data),
156+
},
157+
);
158+
},
159+
onMessage: (handler) => {
160+
const msgHandler = (evt: MessageEvent) => {
161+
handler(parse(evt.data));
162+
};
163+
conn.addEventListener("message", msgHandler);
164+
return () => {
165+
conn.removeEventListener("message", msgHandler);
166+
};
167+
},
168+
close: () => {
169+
conn.close();
170+
},
171+
};
172+
conn.onopen = () => {
173+
resolve(mc);
174+
};
175+
conn.onerror = () => {
176+
reject(new Error("Failed to open message channel."));
177+
};
178+
});
152179
}
153180

154181
waitUntil(promise: Promise<void>) {

hot/dev.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function setup(hot: Hot) {
105105

106106
let connected = false;
107107
const es = new EventSource(
108-
new URL(hot.basePath + "@hot-events", location.href),
108+
new URL(hot.basePath + "@hot-events?channel=dev", location.href),
109109
);
110110

111111
es.addEventListener("fs-notify", async (evt) => {
@@ -143,7 +143,9 @@ export function setup(hot: Hot) {
143143
});
144144

145145
es.addEventListener("open-devtools", async () => {
146-
const { render } = await import(new URL("./devtools", import.meta.url).href);
146+
const { render } = await import(
147+
new URL("./devtools", import.meta.url).href
148+
);
147149
render(hot);
148150
});
149151

packages/esm.sh/src/index.mjs

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const serveHot = (options) => {
2323
const env = typeof Deno === "object" ? Deno.env.toObject() : process.env;
2424
const onFsNotify = fs.watch(root);
2525
const contentCache = new Map(); // todo: use worker `caches` api if possible
26+
const hotClients = new Map();
2627

2728
/**
2829
* Fetcher handles requests for hot applications.
@@ -193,7 +194,9 @@ export const serveHot = (options) => {
193194
return new Response("[]", { headers });
194195
}
195196
const entries = await fs.ls(root);
196-
const matched = entries.filter((entry) => glob.includes(entry) || entry.match(globToRegExp(glob)));
197+
const matched = entries.filter((entry) =>
198+
glob.includes(entry) || entry.match(globToRegExp(glob))
199+
);
197200
if (!matched.length) {
198201
return new Response("[]", { headers });
199202
}
@@ -241,23 +244,47 @@ export const serveHot = (options) => {
241244

242245
/** Event stream for HMR */
243246
case "/@hot-events": {
247+
const channelName = url.searchParams.get("channel");
248+
const devChannel = channelName === "dev";
244249
const disposes = [];
250+
if (req.method === "POST") {
251+
const data = await req.json();
252+
const clients = hotClients.get(channelName)
253+
if (!clients) {
254+
return new Response("Channel not found", { status: 404 });
255+
}
256+
clients.forEach(({ sentEvent }) => sentEvent("message", data));
257+
return new Response("Ok");
258+
}
245259
return new Response(
246260
new ReadableStream({
247261
start(controller) {
248-
const sendEvent = (eventName, data) => {
249-
controller.enqueue("event: " + eventName + "\ndata: " + JSON.stringify(data) + "\n\n");
262+
const sentEvent = (eventName, data) => {
263+
controller.enqueue(
264+
"event: " + eventName + "\ndata: " + JSON.stringify(data) +
265+
"\n\n",
266+
);
250267
};
251-
disposes.push(onFsNotify((type, name) => {
252-
sendEvent("fs-notify", { type, name });
253-
}));
254-
controller.enqueue(": hot notify stream\n\n");
255-
if (isLocalHost(url)) {
256-
sendEvent("open-devtools", null);
268+
controller.enqueue(": hot events stream\n\n");
269+
if (devChannel) {
270+
disposes.push(onFsNotify((type, name) => {
271+
sentEvent("fs-notify", { type, name });
272+
}));
273+
if (isLocalHost(url)) {
274+
sentEvent("open-devtools", null);
275+
}
276+
} else {
277+
const map = hotClients.get(channelName) ??
278+
hotClients.set(channelName, new Map()).get(channelName);
279+
map.set(req, { sentEvent });
257280
}
258281
},
259282
cancel() {
260-
disposes.forEach((dispose) => dispose());
283+
if (devChannel) {
284+
disposes.forEach((dispose) => dispose());
285+
} else {
286+
hotClients.get(channelName)?.delete(req);
287+
}
261288
},
262289
}),
263290
{
@@ -299,12 +326,10 @@ export const serveHot = (options) => {
299326
);
300327
}
301328
default: {
302-
const htmls = [
303-
pathname !== "/" ? pathname + ".html" : null,
304-
pathname !== "/" ? pathname + "/index.html" : null,
305-
"/404.html",
306-
"/index.html",
307-
].filter(Boolean);
329+
const htmls = ["/404.html", "/index.html"];
330+
if (pathname !== "/") {
331+
htmls.unshift(pathname + ".html", pathname + "/index.html");
332+
}
308333
for (const path of htmls) {
309334
filepath = path;
310335
file = await fs.open(root + filepath);
@@ -376,7 +401,9 @@ export const serveHot = (options) => {
376401
const { pathname } = new URL(content, url.origin + filepath);
377402
const index = await fs.ls(root + pathname);
378403
el.replace(
379-
`<script type="applicatin/json" id="@hot/router">${JSON.stringify({ index })}</script>`,
404+
`<script type="applicatin/json" id="@hot/router">${
405+
JSON.stringify({ index })
406+
}</script>`,
380407
{ html: true },
381408
);
382409
},
@@ -417,7 +444,9 @@ export const serveHot = (options) => {
417444
async element(el) {
418445
if (contentMap) {
419446
try {
420-
const { contents = {} } = isNEString(contentMap) ? (contentMap = JSON.parse(contentMap)) : contentMap;
447+
const { contents = {} } = isNEString(contentMap)
448+
? (contentMap = JSON.parse(contentMap))
449+
: contentMap;
421450
const name = el.getAttribute("from");
422451
let content = contents[name];
423452
let asterisk = undefined;
@@ -454,7 +483,9 @@ export const serveHot = (options) => {
454483
value = new Function("return this." + expr).call(data);
455484
}
456485
}
457-
return !isNullish(value) ? value.toString?.() ?? stringify(value) : "";
486+
return !isNullish(value)
487+
? value.toString?.() ?? stringify(value)
488+
: "";
458489
};
459490
const render = (data) => {
460491
el.setInnerContent(process(data), { html: true });

packages/esm.sh/templates/react/with-unocss/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
<react-root src="./App.jsx"></react-root>
2323
<script type="module">
2424
import hot from "http://localhost:8080/v135/hot?plugins=react";
25+
hot.onFire(() => {
26+
hot.openMessageChannel("demo").then(channel => {
27+
channel.onMessage(message => {
28+
console.log(message);
29+
})
30+
channel.postMessage("Hello world!");
31+
})
32+
})
2533
hot.fire();
2634
</script>
2735
</body>

server/embed/types/hot.d.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,17 @@ export interface HotCore {
118118
fetch?: Loader["fetch"],
119119
priority?: "eager",
120120
): this;
121-
on(event: string, handler: (data: any) => void): () => void;
122-
send(event: string, data?: any): void;
121+
openMessageChannel(name: string): Promise<MessageChannel>;
123122
waitUntil(promise: Promise<any>): void;
124123
use(...plugins: Plugin[]): this;
125124
}
126125

126+
export interface MessageChannel {
127+
onMessage(handler: (data: any) => void): () => void;
128+
postMessage(data?: any): void;
129+
close(): void;
130+
}
131+
127132
export interface CallbackMap<T extends Function> {
128133
readonly map: Map<string, Set<T>>;
129134
add: (path: string, callback: T) => void;

0 commit comments

Comments
 (0)