Skip to content

Repository files navigation

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";importtype{Packed,u8}from"@rbxts/serio";exportconstmessaging=MessageEmitter.create<MessageData>();exportconstenumMessage{Test,Packed,}exportinterfaceMessageData{[Message.Test]: {readonlyfoo: string;readonlyn: u8;};[Message.Packed]: Packed<{readonlyboolean1: boolean;readonlyboolean2: boolean;readonlyboolean3: boolean;readonlyboolean4: boolean;readonlyboolean5: boolean;readonlyboolean6: boolean;readonlyboolean7: boolean;readonlyboolean8: 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,});

Function Messages

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";importtype{u8}from"@rbxts/serio";exportconstmessaging=MessageEmitter.create<MessageData>();exportconstenumMessage{Increment,IncrementReturn,}exportinterfaceMessageData{[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 styleasyncfunctionmain(): Promise<void>{constvalue=awaitmessaging.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

importtype{ClientMiddleware}from"@rbxts/tether";exportfunctionlogClient(): ClientMiddleware{return(player,ctx)=>print(`[LOG]: Sent message '${ctx.message}' to player ${player} with data:`,ctx.data);}

Server

importtype{ServerMiddleware}from"@rbxts/tether";exportfunctionlogServer(): ServerMiddleware{return(ctx)=>print(`[LOG]: Sent message '${ctx.message}' to server with data:`,ctx.data);}

Shared

import{typeSharedMiddleware,DropRequest}from"@rbxts/tether";exportfunctionrateLimit(interval: number): SharedMiddleware{letlastRequest=0;return()=>{if(os.clock()-lastRequest<interval)returnDropRequest;lastRequest=os.clock();};}

Transforming data

importtype{ServerMiddleware}from"@rbxts/tether";exportfunctionincrementNumberData(): ServerMiddleware<number>{// sets the data to be used by the any subsequent middlewares as well as sent through the remotereturn(ctx)=>ctx.data++;}

Using middleware

import{MessageEmitter,BuiltinMiddlewares}from"@rbxts/tether";importtype{Packed,u8}from"@rbxts/serio";exportconstmessaging=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 numberexportconstenumMessage{Test,Packed}exportinterfaceMessageData{[Message.Test]: {readonlyfoo: string;readonlyn: u8;};[Message.Packed]: Packed<{readonlyboolean1: boolean;readonlyboolean2: boolean;readonlyboolean3: boolean;readonlyboolean4: boolean;readonlyboolean5: boolean;readonlyboolean6: boolean;readonlyboolean7: boolean;readonlyboolean8: boolean;}>;}

About

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

Topics

Resources

Stars

12 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages