Skip to content

Commit

Permalink
Add message channel
Browse files Browse the repository at this point in the history
  • Loading branch information
ije committed Jan 7, 2024
1 parent 131fead commit be21be2
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 30 deletions.
41 changes: 34 additions & 7 deletions hot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
HotCore,
ImportMap,
Loader,
MessageChannel,
Plugin,
URLTest,
VFSRecord,
Expand Down Expand Up @@ -142,13 +143,39 @@ class Hot implements HotCore {
return this;
}

on(event: string, handler: (data: any) => void): () => void {
// TODO
return () => {};
}

send(event: string, data?: any) {
// TODO
openMessageChannel(name: string): Promise<MessageChannel> {
const conn = new EventSource(this.basePath + "@hot-events?channel=" + name);
return new Promise((resolve, reject) => {
const mc: MessageChannel = {
postMessage: (data) => {
fetch(
this.basePath + "@hot-events?channel=" + name,
{
method: "POST",
body: stringify(data),
},
);
},
onMessage: (handler) => {
const msgHandler = (evt: MessageEvent) => {
handler(parse(evt.data));
};
conn.addEventListener("message", msgHandler);
return () => {
conn.removeEventListener("message", msgHandler);
};
},
close: () => {
conn.close();
},
};
conn.onopen = () => {
resolve(mc);
};
conn.onerror = () => {
reject(new Error("Failed to open message channel."));
};
});
}

waitUntil(promise: Promise<void>) {
Expand Down
6 changes: 4 additions & 2 deletions hot/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function setup(hot: Hot) {

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

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

es.addEventListener("open-devtools", async () => {
const { render } = await import(new URL("./devtools", import.meta.url).href);
const { render } = await import(
new URL("./devtools", import.meta.url).href
);
render(hot);
});

Expand Down
69 changes: 50 additions & 19 deletions packages/esm.sh/src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const serveHot = (options) => {
const env = typeof Deno === "object" ? Deno.env.toObject() : process.env;
const onFsNotify = fs.watch(root);
const contentCache = new Map(); // todo: use worker `caches` api if possible
const hotClients = new Map();

/**
* Fetcher handles requests for hot applications.
Expand Down Expand Up @@ -193,7 +194,9 @@ export const serveHot = (options) => {
return new Response("[]", { headers });
}
const entries = await fs.ls(root);
const matched = entries.filter((entry) => glob.includes(entry) || entry.match(globToRegExp(glob)));
const matched = entries.filter((entry) =>
glob.includes(entry) || entry.match(globToRegExp(glob))
);
if (!matched.length) {
return new Response("[]", { headers });
}
Expand Down Expand Up @@ -241,23 +244,47 @@ export const serveHot = (options) => {

/** Event stream for HMR */
case "/@hot-events": {
const channelName = url.searchParams.get("channel");
const devChannel = channelName === "dev";
const disposes = [];
if (req.method === "POST") {
const data = await req.json();
const clients = hotClients.get(channelName)
if (!clients) {
return new Response("Channel not found", { status: 404 });
}
clients.forEach(({ sentEvent }) => sentEvent("message", data));
return new Response("Ok");
}
return new Response(
new ReadableStream({
start(controller) {
const sendEvent = (eventName, data) => {
controller.enqueue("event: " + eventName + "\ndata: " + JSON.stringify(data) + "\n\n");
const sentEvent = (eventName, data) => {
controller.enqueue(
"event: " + eventName + "\ndata: " + JSON.stringify(data) +
"\n\n",
);
};
disposes.push(onFsNotify((type, name) => {
sendEvent("fs-notify", { type, name });
}));
controller.enqueue(": hot notify stream\n\n");
if (isLocalHost(url)) {
sendEvent("open-devtools", null);
controller.enqueue(": hot events stream\n\n");
if (devChannel) {
disposes.push(onFsNotify((type, name) => {
sentEvent("fs-notify", { type, name });
}));
if (isLocalHost(url)) {
sentEvent("open-devtools", null);
}
} else {
const map = hotClients.get(channelName) ??
hotClients.set(channelName, new Map()).get(channelName);
map.set(req, { sentEvent });
}
},
cancel() {
disposes.forEach((dispose) => dispose());
if (devChannel) {
disposes.forEach((dispose) => dispose());
} else {
hotClients.get(channelName)?.delete(req);
}
},
}),
{
Expand Down Expand Up @@ -299,12 +326,10 @@ export const serveHot = (options) => {
);
}
default: {
const htmls = [
pathname !== "/" ? pathname + ".html" : null,
pathname !== "/" ? pathname + "/index.html" : null,
"/404.html",
"/index.html",
].filter(Boolean);
const htmls = ["/404.html", "/index.html"];
if (pathname !== "/") {
htmls.unshift(pathname + ".html", pathname + "/index.html");
}
for (const path of htmls) {
filepath = path;
file = await fs.open(root + filepath);
Expand Down Expand Up @@ -376,7 +401,9 @@ export const serveHot = (options) => {
const { pathname } = new URL(content, url.origin + filepath);
const index = await fs.ls(root + pathname);
el.replace(
`<script type="applicatin/json" id="@hot/router">${JSON.stringify({ index })}</script>`,
`<script type="applicatin/json" id="@hot/router">${
JSON.stringify({ index })
}</script>`,
{ html: true },
);
},
Expand Down Expand Up @@ -417,7 +444,9 @@ export const serveHot = (options) => {
async element(el) {
if (contentMap) {
try {
const { contents = {} } = isNEString(contentMap) ? (contentMap = JSON.parse(contentMap)) : contentMap;
const { contents = {} } = isNEString(contentMap)
? (contentMap = JSON.parse(contentMap))
: contentMap;
const name = el.getAttribute("from");
let content = contents[name];
let asterisk = undefined;
Expand Down Expand Up @@ -454,7 +483,9 @@ export const serveHot = (options) => {
value = new Function("return this." + expr).call(data);
}
}
return !isNullish(value) ? value.toString?.() ?? stringify(value) : "";
return !isNullish(value)
? value.toString?.() ?? stringify(value)
: "";
};
const render = (data) => {
el.setInnerContent(process(data), { html: true });
Expand Down
8 changes: 8 additions & 0 deletions packages/esm.sh/templates/react/with-unocss/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
<react-root src="./App.jsx"></react-root>
<script type="module">
import hot from "http://localhost:8080/v135/hot?plugins=react";
hot.onFire(() => {
hot.openMessageChannel("demo").then(channel => {
channel.onMessage(message => {
console.log(message);
})
channel.postMessage("Hello world!");
})
})
hot.fire();
</script>
</body>
Expand Down
9 changes: 7 additions & 2 deletions server/embed/types/hot.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,17 @@ export interface HotCore {
fetch?: Loader["fetch"],
priority?: "eager",
): this;
on(event: string, handler: (data: any) => void): () => void;
send(event: string, data?: any): void;
openMessageChannel(name: string): Promise<MessageChannel>;
waitUntil(promise: Promise<any>): void;
use(...plugins: Plugin[]): this;
}

export interface MessageChannel {
onMessage(handler: (data: any) => void): () => void;
postMessage(data?: any): void;
close(): void;
}

export interface CallbackMap<T extends Function> {
readonly map: Map<string, Set<T>>;
add: (path: string, callback: T) => void;
Expand Down

0 comments on commit be21be2

Please sign in to comment.