A library designed to facilitate the maintenance of networking code in monorepos
npm install monorepo-networker
height="150px"
srcset="https://raw.githubusercontent.com/CoconutGoodie/monorepo-networker/master/.github/assets/light-logo.svg" />
src ="https://raw.githubusercontent.com/CoconutGoodie/monorepo-networker/master/.github/assets/dark-logo.svg"
height="150px"
alt =""/>
A library designed to facilitate the maintenance of networking code in monorepos
|- common
|- packages
| |- ui
| |- client
| |- server
`
1. Define the Sides
Start by creating sides and defining the events they can receive.
`ts
// ./common/networkSides.ts
import { Networker } from "monorepo-networker";
export const UI = Networker.createSide("UI-side").listens<{
focusOnSelected(): void;
focusOnElement(elementId: string): void;
}>();
export const CLIENT = Networker.createSide("Client-side").listens<{
hello(text: string): void;
getClientTime(): number;
createRectangle(width: number, height: number): void;
execute(script: string): void;
}>();
export const SERVER = Networker.createSide("Server-side").listens<{
hello(text: string): void;
getServerTime(): number;
fetchUser(userId: string): { id: string; name: string };
markPresence(online: boolean): void;
}>();
`
> [!CAUTION]
> Side objects created here are supposed to be used across different side runtimes.
> Make sure NOT to use anything side-dependent in here.
2. Create the Channels
Create the channels for each side. Channels are responsible of communicating with other sides and listening to incoming messages using the registered strategies.
(Only the code for CLIENT side is shown, for simplicity.)
`ts
// ./packages/client/networkChannel.ts
import { CLIENT, SERVER, UI } from "@common/networkSides";
export const CLIENT_CHANNEL = CLIENT.channelBuilder()
.emitsTo(UI, (message) => {
// We're declaring how CLIENT sends a message to UI
parent.postMessage({ pluginMessage: message }, "*");
})
.emitsTo(SERVER, (message) => {
// We're declaring how CLIENT sends a message to SERVER
fetch("server://", { method: "POST", body: JSON.stringify(message) });
})
.receivesFrom(UI, (next) => {
// We're declaring how CLIENT receives a message from UI
const listener = (event: MessageEvent) => {
if (event.data?.pluginId == null) return;
next(event.data.pluginMessage);
};
window.addEventListener("message", listener);
return () => {
window.removeEventListener("message", listener);
};
})
.startListening();
// ----------- Declare how an incoming message is handled
CLIENT_CHANNEL.registerMessageHandler("hello", (text, from) => {
console.log(from.name, "said:", text);
});
CLIENT_CHANNEL.registerMessageHandler("getClientTime", () => {
// Returning a value will make this event "request-able"
return Date.now();
});
CLIENT_CHANNEL.registerMessageHandler("execute", async (script) => {
// It also supports Async handlers!
return new Promise((resolve) => {
setTimeout(() => resolve(eval(script)), 5000);
});
});
`
3. Initialize & Invoke
Initialize each side in their entry point. And enjoy the standardized messaging api!
- Channel::emit will emit given event to the given side
- Channel::request will emit given event to the given side, and wait for a response from the target side.
- Channel::subscribe will subscribe a listener for incoming messages on this side. (Note: subscribed listener cannot "respond" to them. Use Channel::registerMessageHandler to create a proper responder.)
`ts
// ./packages/server/main.ts
import { Networker } from "monorepo-networker";
import { SERVER, CLIENT } from "@common/networkSides";
import { SERVER_CHANNEL } from "@server/networkChannel";
async function bootstrap() {
Networker.initialize(SERVER, SERVER_CHANNEL);
console.log("We are at", Networker.getCurrentSide().name);
// ... Omitted code that bootstraps the server
SERVER_CHANNEL.emit(CLIENT, "hello", ["Hi there, client!"]);
// Event though CLIENT's createRectangle returns void, we can still await on its acknowledgement.
await SERVER_CHANNEL.request(CLIENT, "createRectangle", [100, 200]);
}
bootstrap();
`
`tsx
// ./packages/client/main.ts
import { Networker, NetworkError } from "monorepo-networker";
import { CLIENT, SERVER } from "@common/networkSides";
import { CLIENT_CHANNEL } from "@client/networkChannel";
import React, { useEffect, useRef } from "react";
import ReactDOM from "react-dom/client";
Networker.initialize(CLIENT, CLIENT_CHANNEL);
console.log("We are @", Networker.getCurrentSide().name);
CLIENT_CHANNEL.emit(SERVER, "hello", ["Hi there, server!"]);
// This one corresponds to SERVER's getServerTime(): number; event
CLIENT_CHANNEL.request(SERVER, "getServerTime", [])
.then((serverTime) => {
console.log('Server responded with "' + serverTime + '" !');
})
.catch((err) => {
if (err instanceof NetworkError) {
console.log("Server failed to respond..", { message: err.message });
}
});
const rootElement = document.getElementById("root") as HTMLElement;
const root = ReactDOM.createRoot(rootElement);
root.render(
);
function App() {
const rectangles = useRef<{ w: number; h: number }[]>([]);
useEffect(() => {
const unsubscribe = CLIENT_CHANNEL.subscribe(
"createRectangle",
(width, height, from) => {
console.log(from.name, "asked for a rectangle!");
rectangles.current.push({ w: width, h: height });
}
);
return () => unsubscribe();
}, []);
return {/ ... Omitted for simplicity /} ;
}
``