Skip to content

🌐 A library designed to facilitate the maintenance of networking code in monorepos

License

Notifications You must be signed in to change notification settings

CoconutGoodie/monorepo-networker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

60 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

A library designed to facilitate the maintenance of networking code in monorepos


🧢 What is monorepo-networker?

Consider a scenario where you maintain a codebase following the monorepo pattern, with an IPC-esque communication mechanism between sidesβ€”similar to FiveM's Scripting SDK or Figma's Plugin API. In such cases, you may encounter excessive boilerplate code just to ensure that the correct data is sent under the appropriate title. This library aims to simplify that process by abstracting transport strategies between sides, thereby standardizing communication.

🎁 Examples

πŸ’» How to use it?

This library assumes your codebase is a monorepo that has distinct sides sharing some code. For simplicity, tutorial ahead will use a folder structure like so:

|- common
|- packages
|  |- ui
|  |- client
|  |- server

1. Define the Sides

Start by creating sides and defining the events they can receive.

// ./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.)

// ./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<void>((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.)
// ./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();
// ./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(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

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 <main>{/* ... Omitted for simplicity */}</main>;
}

⭐ Special Thanks to

  • @thediaval: For his endless support and awesome memes.

πŸ“œ License

Β© 2024 Taha AnΔ±lcan Metinyurt (iGoodie)

For any part of this work for which the license is applicable, this work is licensed under the Attribution-ShareAlike 4.0 International license. (See LICENSE).

Creative Commons License

About

🌐 A library designed to facilitate the maintenance of networking code in monorepos

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published