npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@rbxts/tether

v1.4.0

Published

A message-based networking solution for Roblox with automatic binary serialization and type validation

Readme

Tether

A message-based networking solution for Roblox with automatic binary serialization and type validation.

This package uses Serio for binary serialization, so to find more info on schemas, check it out!

[!CAUTION] Depends on rbxts-transformer-flamework!

Usage

In shared/messaging.ts

import { MessageEmitter } from "@rbxts/tether";
import type { Packed, u8 } from "@rbxts/serio";

export const messaging = MessageEmitter.create<MessageData>();

export const enum Message {
  Test,
  Packed,
}

export interface MessageData {
  [Message.Test]: {
    readonly foo: string;
    readonly n: u8;
  };
  [Message.Packed]: Packed<{
    readonly boolean1: boolean;
    readonly boolean2: boolean;
    readonly boolean3: boolean;
    readonly boolean4: boolean;
    readonly boolean5: boolean;
    readonly boolean6: boolean;
    readonly boolean7: boolean;
    readonly boolean8: boolean;
  }>;
}

[!CAUTION] Every single message kind must implement an interface for it's data (in the example that would be the object types in MessageData). Message serialization (as well as your message itself) will not work if you don't do this.

Server

import { Message, messaging } from "shared/messaging";

messaging.server.on(Message.Test, (player, data) =>
  print(player, "sent data:", data)
);

Client

import { Message, messaging } from "shared/messaging";

messaging.server.emit(Message.Test, {
  foo: "bar",
  n: 69,
});

Simulated Remote Functions

Tether does not directly use RemoteFunctions since it's based on the MessageEmitter structure. However I have created a small framework to simulate remote functions, as shown below.

For each function you will need two messages. One to invoke the function, and one to send the return value back (which is done automatically).

In shared/messaging.ts

import { MessageEmitter } from "@rbxts/tether";
import type { u8 } from "@rbxts/serio";

export const messaging = MessageEmitter.create<MessageData>();

export const enum Message {
  Increment,
  IncrementReturn,
}

export interface MessageData {
  [Message.Increment]: u8;
  [Message.IncrementReturn]: u8;
}

Server

import { Message, messaging } from "shared/messaging";

messaging.server.setCallback(
  Message.Increment,
  Message.IncrementReturn,
  (_, n) => n + 1
);

Client

import { Message, messaging } from "shared/messaging";

messaging.server
  .invoke(Message.Increment, Message.IncrementReturn, 69)
  .then(print); // 70 - incremented by the server

// or use await style
async function main(): Promise<void> {
  const value = await messaging.server.invoke(
    Message.Increment,
    Message.IncrementReturn,
    69
  );
  print(value); // 70
}

main();

Middleware

Drop, delay, or modify requests

Creating middleware

Note: These client/server middlewares can be implemented as shared middlewares. This is strictly an example.

Client

import type { ClientMiddleware } from "@rbxts/tether";

export function logClient(): ClientMiddleware {
  return (player, ctx) =>
    print(
      `[LOG]: Sent message '${ctx.message}' to player ${player} with data:`,
      ctx.data
    );
}

Server

import type { ServerMiddleware } from "@rbxts/tether";

export function logServer(): ServerMiddleware {
  return (ctx) =>
    print(
      `[LOG]: Sent message '${ctx.message}' to server with data:`,
      ctx.data
    );
}

Shared

import { type SharedMiddleware, DropRequest } from "@rbxts/tether";

export function rateLimit(interval: number): SharedMiddleware {
  let lastRequest = 0;

  return () => {
    if (os.clock() - lastRequest < interval) return DropRequest;

    lastRequest = os.clock();
  };
}

Transforming data

import type { ServerMiddleware } from "@rbxts/tether";

export function incrementNumberData(): ServerMiddleware<number> {
  // sets the data to be used by the any subsequent middlewares as well as sent through the remote
  return (ctx) => ctx.data++;
}

Using middleware

import { MessageEmitter, BuiltinMiddlewares } from "@rbxts/tether";
import type { Packed, u8 } from "@rbxts/serio";

export const messaging = MessageEmitter.create<MessageData>();
messaging.middleware
  // only allows requests to the server every 5 seconds,
  // drops any requests that occur within 5 seconds of each other
  .useServer(Message.Test, BuiltinMiddlewares.rateLimit(5))
  // will be just one byte!
  .useShared(Message.Packed, ctx => print("Packed object size:", buffer.len(ctx.getRawData().buffer)));
  // logs every message fired
  .useServerGlobal(logServer())
  .useClientGlobal(logClient())
  .useSharedGlobal(BuiltinMiddlewares.debug()); // verbosely logs every packet sent
  .useServer(Message.Test, incrementNumberData()) // error! - data for Message.Test is not a number
  .useServerGlobal(incrementNumberData()); // error! - global data type is always 'unknown', we cannot guarantee a number

export const enum Message {
  Test,
  Packed
}

export interface MessageData {
  [Message.Test]: {
    readonly foo: string;
    readonly n: u8;
  };
  [Message.Packed]: Packed<{
    readonly boolean1: boolean;
    readonly boolean2: boolean;
    readonly boolean3: boolean;
    readonly boolean4: boolean;
    readonly boolean5: boolean;
    readonly boolean6: boolean;
    readonly boolean7: boolean;
    readonly boolean8: boolean;
  }>;
}