roblox-aurora / rbx-net Goto Github PK
View Code? Open in Web Editor NEWAdvanced multi-language networking framework for Roblox
Home Page: https://rbxnet.australis.dev/
License: MIT License
Advanced multi-language networking framework for Roblox
Home Page: https://rbxnet.australis.dev/
License: MIT License
node_modules/@rbxts/net/out/definitions/Types.d.ts:62:85 - error TS2536: Type '"Definitions"' cannot be used to index type 'T'.
62 export declare type InferGroupDeclaration = T extends DeclarationNamespaceLike ? T["Definitions"] : {};
Not sure exactly why Typedoc wants to error out like that but hopefully can be resolved
I feel like the definitions API could be cleaner, perhaps instead of doing something like:
[InventoryClientFunctionId.MoveEquippedItemToInventorySlot]: ServerAsyncFunction<
(
request: MoveEquippedItemToInventorySlotRequest,
) => Serializer.NetworkResult<MoveEquippedItemToInventorySlotResponse, MoveEquippedItemToInventorySlotError>
>([
Net.Middleware.TypeChecking(
t.interface({
InventoryId: t.numberMin(0),
SlotId: t.numberMin(0),
EquipSlot: t.string,
}),
),
]),
We could do
[InventoryClientFunctionId.MoveEquippedItemToInventorySlot]: ServerAsyncFunction<
(
request: MoveEquippedItemToInventorySlotRequest,
) => Serializer.NetworkResult<MoveEquippedItemToInventorySlotResponse, MoveEquippedItemToInventorySlotError>
>().WithMiddleware([
Net.Middleware.TypeChecking(
t.interface({
InventoryId: t.numberMin(0),
SlotId: t.numberMin(0),
EquipSlot: t.string,
}),
),
]),
Could simplify this further for built-in middleware, such as .WithTypeChecking(...types)
and .WithRateLimit(options)
[InventoryClientFunctionId.MoveEquippedItemToInventorySlot]: ServerAsyncFunction<
(
request: MoveEquippedItemToInventorySlotRequest,
) => Serializer.NetworkResult<MoveEquippedItemToInventorySlotResponse, MoveEquippedItemToInventorySlotError>
>().WithTypeChecking(
t.interface({
InventoryId: t.numberMin(0),
SlotId: t.numberMin(0),
EquipSlot: t.string,
}),
),
This could play well with #62.
When a game is running and calls Net.CreateDefinitions
then Event
s are created under the net.out._NetManaged
Instance
When starting a rojo server using a "$path"
to load dependencies including rbx-net, the rojo server creates a DataModel
with a net.out
Instance
without the events, as it only knows about what is on the filesystem.
If I attempt to connect a running game to that running rojo server using the rojo plugin, the plugin(?) will overwrite the runtime Instance
s currently in the game with the ones the server knows about. This removes the Event
s created by Net.CreateDefinitions
.
Then event handlers in the game no longer work.
When go into my dependency tree and add a file net/out/init.meta.json
with contents
{"ignoreUnknownInstances": true}
the previous setup will keep the Event
s created by Net.CreateDefinitions
, as rojo will ignore the _NetManaged
Instance
when updating. Then I can use the rewire library to hot reload code as desired.
Adding this file to the repo might be a solution, but I have not given proper consideration to the ramifications of leaving old events around, and how such a choice could conflict with the intentional design decision mentioned in #78.
Here's a quick example:
// main.client.ts
import { Remotes } from "shared/remotes";
const PrintMessage = Remotes.Client.Get("PrintMessage");
PrintMessage.SendToServer("Hello there!");
// remotes.ts
import Net from "@rbxts/net";
export const Remotes = Net.Definitions.Create({
PrintMessage: Net.Definitions.ClientToServerEvent<[message: string]>(),
});
// main.client.ts
import { Remotes } from "shared/remotes";
const PrintMessage = Remotes.Server.Create("PrintMessage");
PrintMessage.Connect((player, message) => {
print(`Server recieved message: ${message} from player: ${player}`);
});
With Workspace.SignalBehavior
set to Immediate
, the server prints Server recieved message: Hello there! from player: Player
as expected. However, with Workspace.SignalBehavior
set to Deferred
, the client might throw the following error:
ReplicatedStorage.rbxts_include.node_modules.net.out.internal:188: Could not find Remote of type RemoteEvent called "PrintMessage"
I am not able to get my definitions type correctly and since the usage is missing for rbxts(only luau) in the docs I'm unsure on how to fix it.
Remotes.ts
import Net, { Definitions } from '@rbxts/net';
import type { SerializedCharacter } from '../types/Serialized/SerializedCharacter';
const Remotes = Net.CreateDefinitions({
GetCharacters: Definitions.ServerFunction<(player: Player) => SerializedCharacter[]>()
});
export = Remotes;
main.server.ts
import Remotes from 'shared/Remotes';
import type { SerializedCharacter } from '../types/Serialized/SerializedCharacter';
Remotes.Server.OnFunction('GetCharacters',() => {
})
My types are not resolving and if I'm meant to build the types myself I did it like this though I could of done it wrong.
type CharSelectRemoteDeclarations = {
[key: string]: DeclarationLike | DeclarationNamespaceLike;
GetCharacters: FunctionDeclaration<[player: Player],SerializedCharacter[]>
};
const Remotes = Net.CreateDefinitions<CharSelectRemoteDeclarations>({
GetCharacters: Definitions.ServerFunction<(player: Player) => SerializedCharacter[]>()
});
At the moment remotes need to be manually created on the server for them to be accessible on the client.
This may lead to an issue where you can see that a remote is accessible on a client, but it hasn't been created on the server. This would fix said issue by ensuring the remote is at least created, even if it's not hooked into yet.
Even though the rbx-net way of doing it is asynchronous, it may never actually resolve or reject the promise (yield forever) due to how RemoteFunctions work. This can be done by exploiters to wreck havoc on your game. Enough of these promises being created could create unnecessary memory usage and that's an issue that needs to be addressed.
What kinds of use-cases are there for ServerFunction.CallPlayerAsync
that people have? I wish to know before I consider actually removing this function.
It will otherwise be removed soon in the next update.
I've noticed you cannot create ServerFunctions within namespaces. It'll throw a type error:
TypeError: Type '{ SpawnUnit: ServerFunctionDefinition }' could not be converted into '{| [string]: ClientAsyncFunctionDefinition | ClientToServerEventDefinition | ServerAsyncFunctionDefinition | ServerToClientEventDefinition |}'
caused by:
Property 'SpawnUnit' is not compatible. Type 'ClientAsyncFunctionDefinition | ClientToServerEventDefinition | ServerAsyncFunctionDefinition | ServerToClientEventDefinition' could not be converted into 'ServerFunctionDefinition'
caused by:
Not all union options are compatible. Table type 'ServerToClientEventDefinition' not compatible with type 'ServerFunctionDefinition' because the former has extra field '__nominal_ServerToClientEventDefinition'
Missing documentation for how to use namespaces. Preferred over duplicate def files.
Serialization/Deserialization of objects in RbxNet is an important use case I want to eventually cover.
This would probably involve a slight overhaul of the remote objects used by Net to accompany the ability for functions to serialize values.
The idea would be to have a member Serialize()
, then a static deserialize()
on the object. It's possible Net could implicitly convert this before sending and when receiving by checking for objects.
Net.Serialization.AddSerializableClass(MyClass)
which:MyClass
- should have member Serialize
and static deserialize
.Serialized representation would be something akin to {"ClassName": "MyClass", "Value": SerializedObjectValue}
.
This would also probably be something that gets only enabled if there are actually serializable classes added.
Would involve the remote objects themselves. I am currently thinking up the API idea for this.
I can attach an ErrorHandler for Middleware.RateLimit but not for Middleware.TypeChecking
Even if the remote already exists, it will wait one heartbeat.
https://github.com/roblox-aurora/rbx-net/blob/main/src/internal/index.ts#L133-L138
This is an easy fix- first check if remote exists and resolve immediately if so. Otherwise, go into the polling loop.
Since both NetServerEvent and NetServerFunction both already have custom callback mechanisms to handle the type checker, the throttling can now be part of that.
Simply put for example
const myThrottledEvent = new Net.ServerThrottledEvent("Test", 10);
would be replaced with
const myThrottledEvent = new Net.ServerEvent("Test");
myThrottledEvent.SetRateLimit(10);
Eventually in the future they will be removed. For now I will be putting warnings in place.
this is my Remotes moduleScript in replicated storage
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Net = require(ReplicatedStorage:WaitForChild("build.rbxnet.luau"))
local Remotes = Net.CreateDefinitions({
Ping = Net.Definitions.ServerFunction({
Net.Middleware.RateLimit({
MaxRequestsPerMinute = 30
})
}),
})
return Remotes
The error says it all, line 5 attempt to call a nil value
this is the module in replicated storage
Deprecate Net.Definitions.Event
for Net.Definitions.ServerEvent
, Net.Definitions.ClientEvent
.
The difference here is
ServerEvent
- An event fired by the server and received by a client.
ClientEvent
- An event fired by the client and recieved by the server.
The typings are updated so
A ServerEvent
grabbed on the server can only fire, and on the client can only recieve.
A ClientEvent
on the server can only recieve and on the client can only fire.
This is only a typing difference, behaviorally for Luau it would not matter.
I'm not sure on the naming schemes here though. Are they too ambiguous? what would work better?
basic:
import { Definitions, Middleware } from "@rbxts/net";
export const RateLimitSet = new Set<number>();
export const RateLimit: Middleware = (nextMiddleware) => {
return (sender, ...args) => {
if (!RateLimitSet.has(sender.UserId)) {
RateLimitSet.add(sender.UserId);
return nextMiddleware(sender, ...args);
}
};
};
const def = Definitions.Create({
Test: Definitions.ServerAsyncFunction([RateLimit]),
})
[compiled]
wrapped:
import { Definitions, Middleware } from "@rbxts/net";
export const RateLimitSet = new Set<number>();
export function createRateLimiter(): Middleware {
return (nextMiddleware) => {
return (sender, ...args) => {
if (!RateLimitSet.has(sender.UserId)) {
RateLimitSet.add(sender.UserId);
return nextMiddleware(sender, ...args);
}
};
};
}
const def = Definitions.Create({
Test: Definitions.ServerAsyncFunction([createRateLimiter()]),
})
[compiled]
Both use cases need to be covered, so that there's no confusion around this. Second example should show an example of passing your own options to change the mw behaviour.
Remotes.ts:
import Net, { Definitions } from '@rbxts/net';
import type { SerializedCharacter } from '../types/Serialized/SerializedCharacter';
const Remotes = Net.CreateDefinitions({
GetCharacters: Definitions.ServerFunction<(player: Player) => SerializedCharacter[]>()
});
export = Remotes;
main.server.ts:
import Remotes from 'shared/Remotes';
import type { SerializedCharacter } from '../types/Serialized/SerializedCharacter';
Remotes.Server.OnFunction('GetCharacters',(player: Player) => {
return [];
});
Trying to solve the problem described in #78.
Currently, all networking objects are created when code is run from the definitions object
Not sure this can be done with typescript - to create files from definitions object with typescript transformer in compile time.
But surely we can create separate files and add transform to that?
For example(not actual implementation):
fileName: toggleCampaignStartButton.ts
@Definitions.ServerToClientEvent
interface {
show: boolean;
}
Version: 3.0.0
Code:
import Net, { Definitions as def } from "@rbxts/net";
export = Net.CreateDefinitions({
initialize: def.ServerAsyncFunction<() => boolean>(),
realm: def.Namespace({
subscribe: def.ServerToClientEvent<[realm: string]>(),
unsubscribe: def.ServerToClientEvent<[realm: string]>(),
}),
});
Currently something like Flamework has an advantage over RbxNet with automatically generating runtime type checking.
We could
Experiencing a lockup when using this lib in the following scenario:
shared/netMessages.ts has a defintion for a ServerToClientEvent
client/homeGui.client.ts has a Remotes.Client.Get()
for that same event
I forgot to implement any server side Remotes.Server.Create()
for this event. So that was my mistake.
But in this scenario, my app silently fails to load, never throws an error, and sits there doing nothing.
To track it down I had to add print() statements in all the chain of imports until I facepalmed when I discovered the unconnected event.
Previously, I have seen an error message from rbx-net in a similar situation, but I think it was actually the opposite, where the Server had tried to send an event, and Client code was not yet written for that. It has a timeout that waits a number of seconds before displaying a warning. Similar behavior in the above-mentioned opposite bug would have been very much appreciated.
Clean up all the definition types, remove all the excess properties etc.
This will unfortunately mean the removal of Event
, Function
and AsyncFunction
but will mean there isn't weird "fake" props on definition function returned values.
The updated definition types should just have the readonly unique symbols to keep them nominal.
I have an idea for the Net.Definitions
. For a large project, there may be a crap-load of remotes in a definition.
The idea would be you'd be able to group remotes by feature, which would
A) Visually organize the definitions in larger projects
B) Categorize related remotes in the API
Example API
const Remotes = Net.Definitions.Create({
FeatureA: Net.Definitions.Namespace({
DoSomethingWithFeatureA: Net.Definitions.Event(),
DoAnotherThingWithFeatureA: Net.Definitions.AsyncFunction()
}),
FeatureB: Net.Definitions.Namespace({
DoSomethingWithFeatureB: Net.Definitions.Function()
DoAnotherThingWithFeatureB: Net.Definitions.Event()
})
})
Then if I wanted all the remotes relating to FeatureA
on the server -
const FeatureA = Remotes.Server.GetNamespace("FeatureA");
const DoSomethingWithFeatureA = FeatureA.Create("DoSomethingWithFeatureA");
const DoAnotherThingWithFeatureA = FeatureA.Create("DoAnotherThingWithFeatureA");
Then if I wanted all the remotes relating to FeatureB
on the client -
const FeatureB = Remotes.Client.GetNamespace("FeatureB");
const DoSomethingWithFeatureB = FeatureB.Get("DoSomethingWithFeatureB");
const DoAnotherThingWithFeatureB = FeatureB.Get("DoAnotherThingWithFeatureB");
RbxNet version: 1.2.6
export function SetConfiguration<K extends keyof RbxNetConfigItem>(key: K, value: RbxNetConfigItem[K]) {
assert(IS_SERVER, "Cannot set configuration on client!");
if (key === "ServerThrottleMessage") { // should be "ServerThrottleResetTimer"
throttleResetTimer = value as number;
} else if (key === "ServerThrottleMessage") {
rateLimitReachedMessage = value as string;
} else if (key === "EnableDebugMessages") {
DebugEnabled = value as boolean;
}
}
As you can see, ServerThrottleMessage is duplicated twice and sets both the rateLimitReachedMessage and the throttleResetTimer to the value (string) passed.
For requests that use data stores or other APIs with rate limits, it would be useful to be able to implement throttles and rate limits on the events and functions using those APIs. For example, you could rate limit a RemoteFunction that gets profile data from data stores so that there's only 2 requests per 10 seconds, and other requests can be queued or dropped.
Background info: I'm using rbx-net with roblox-ts to create a turn-based game. A network design pattern that feels idiomatic for the game is to have a RemoteFunction named "AskPlay" that waits for the player to give input and then resolves a Promise. On the server, this is easy to do, as CallPlayerAsync
returns a Promise.
However, I can't set the client callback to be an async
function, as async
functions return a Promise
that rbx-net
doesn't wait for to be resolved. As a result, I can't await
for the player to make their choice. So ideally, I would like my code to look like this:
askPlay.setCallback(async () => {
let localPlayer = state.LocalPlayer()
let playedCards = await this.AskPlay()
localPlayer.Hand!.RemoveCards(playedCards.Cards)
return playedCards.Cards.map((card) => localPlayer.Hand!.Cards.indexOf(card))
})
Here this.AskPlay()
returns a Promise that is resolved upon user input. Could it be that this design isn't in line with how rbx-net is designed to work?
Using v1.1.0
import Net from "@rbxts/net";
let exampleOne = Net.CreateEvent("NameOfEvent");
Yields
21:31:28.973 - Infinite yield possible on 'ReplicatedStorage.rbxts_include.node_modules.net.out:WaitForChild("Instance")'
21:31:28.974 - Stack Begin
21:31:28.974 - Script 'ReplicatedStorage.rbxts_include.RuntimeLib', Line 91 - field import
21:31:28.974 - Script 'ReplicatedStorage.rbxts_include.node_modules.net.out', Line 7
21:31:28.975 - Stack End
Roblox studio 0.401.0.338533 (Mac)
ps: version 1.1.2 does say "Loaded rbx-net v1.0.13"
RbxNet version: 2.0.0-alpha.5
RobloxTS version: 1.0.0-beta.11
Describe the bug
Throttle counters are not cleared every minute so the RateLimiter middleware will never reset its throttle counters making the maxRequestsPerMinute
a max requests limit where players cannot call the event at all after calling it above the maxRequestsPerMinute
limit.
Expected behavior
Throttle counters should be cleared every minute.
BidirectionalEvent
, OnEvent
and OnFunction
given this
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Net = require(ReplicatedStorage.Packages.Net)
return Net.CreateDefinitions({
Cuffs = Net.Definitions.Namespace({}), --> error here
})
TypeError: Type '{ Cuffs: {ClientAsyncFunctionDefinition | ClientToServerEventDefinition | ServerAsyncFunctionDefinition | ServerFunctionDefinition | ServerToClientEventDefinition} }' could not be converted into 'RemoteDefinitions'
caused by:
Property 'Cuffs' is not compatible. Type 'ClientAsyncFunctionDefinition | ClientToServerEventDefinition | ServerAsyncFunctionDefinition | ServerFunctionDefinition | ServerToClientEventDefinition' could not be converted into '{ClientAsyncFunctionDefinition | ClientToServerEventDefinition | ServerAsyncFunctionDefinition | ServerFunctionDefinition | ServerToClientEventDefinition}'
caused by:
Not all union options are compatible. Table type 'ServerToClientEventDefinition' not compatible with type '{ClientAsyncFunctionDefinition | ClientToServerEventDefinition | ServerAsyncFunctionDefinition | ServerFunctionDefinition | ServerToClientEventDefinition}' because the former has extra field '__nominal_ServerToClientEventDefinition'Luau[1000](https://luau-lang.org/typecheck)
currently using luau-lsp
node_modules/@rbxts/net/out/server/ServerFunction.d.ts:5:22 - error TS2515: Non-abstract class 'ServerFunction<CallbackArgs, Returns>' does not implement inherited abstract member 'GetInstance' from class 'MiddlewareFunction'.
5 export default class ServerFunction<CallbackArgs extends ReadonlyArray = Array, Returns extends unknown = unknown> extends MiddlewareFunction {
Not sure exactly why Typedoc wants to error out like that but hopefully can be resolved.
When calling the server with a ServerAsyncFunction, there's a possibility that your middleware may not propagate your request and finish it. For a client that is awaiting the promise's fate, the promise never settles.
This can be reproduced by making a middleware that rejects all requests and hooking a :finally() method to your promise. It will never reach it.
Unable to browse the documentation for this lib due to doc site being unreachable.
This is due to how roblox-ts
API changed. Will fix.
That way, the connection may be disconnected in Server/ClientEvents.
I sent a message from Server to client with array of point data. It's just a normal array with integer keys.
When client received the array, the keys are turned into string. It causes bad problems with iteration.
Here's what server sent:
16:35:35.624 firing AllP with ▼ {
[1] = ▼ {
["bs"] = ▼ {
["ct"] = 32,
["fi"] = -1,
["li"] = -1,
["pts"] = ▼ {
[0] = ▼ {
["idx"] = -1,
["px"] = -184,
["pz"] = -149,
["stamp"] = 1643733334.642958,
["vx"] = 34.1944465637207,
["vz"] = 27.69006729125977
}
}
},
["color"] = 0.709804, 0, 0.0784314,
["id"] = 2031724732,
["initCFrame"] = -184, 8, -149, -0.629319727, -0, -0.777146518, -0, 1, -0, 0.777146518, 0, -0.629319727,
["wormLength"] = 1
}
} - Server - serverPlayerManager:102
Notice the path to the first player's first point is [1].bs.pts[0]
- and the 0
is an integer.
Here's what client received:
16:35:35.643 client: AllP recd is ▼ {
[1] = ▼ {
["bs"] = ▼ {
["ct"] = 32,
["fi"] = -1,
["li"] = -1,
["pts"] = ▼ {
["0"] = ▼ {
["idx"] = -1,
["px"] = -184,
["pz"] = -149,
["stamp"] = 1643733334.642958,
["vx"] = 34.1944465637207,
["vz"] = 27.69006729125977
}
}
},
["color"] = 0.709804, 0, 0.0784314,
["id"] = 2031724732,
["initCFrame"] = -184, 8, -149, -0.62928617, 0, -0.777173758, 0, 1, 0, 0.777173758, 0, -0.62928617,
["wormLength"] = 1
}
} - Client - clientPlayerManager:80
the path to the first player's first point is now [1].bs.pts["0"]
- and the "0"
has turned to a string.
The topmost level was recognized as an array and interpreted correctly. The nested array was the one with the problem.
Here is corresponding type definition of IPlayerInit
structure. Payload is array of this. Nested inside is bs: IBufferState
with array of IBufferPoint
pts
export interface IBufferPoint {
idx: number;
stamp: number;
px: number;
pz: number;
vx: number;
vz: number;
}
export interface IBufferState {
fi: number;
li: number;
ct: number;
pts: IBufferPoint[];
}
export interface IPlayerInit {
id: number;
color: Color3;
initCFrame: CFrame;
wormLength: number;
bs: IBufferState;
}
Currently, definitions are created into this library source code:
Can we make Parent configurable?
Line 88 in 5cde004
Would like that _NetManaged folder would be directly under ReplicatedStorage
ReplicatedStorage->_NetManaged->toggleCampaignStartButton
Little bit of background:
I want to implement code Hot Reloading and current problem is that _NetManaged folders gets removed, if I run Rojo and files are synced.
ServerMessagingEvent
would allow sending messages cross-server to the specified player clients in any server with the matching user id.
This technically existed in 2.1.x, however as it was an undocumented (broken) API I want to revisit it.
SendToAllServers
and SendToServer
is essentially the same in ExperienceBroadcastEvent
.SendToPlayer(userId, ...)
and `SendToPlayers(userIds[], ...)ClientMessagingEvent
would be the client counter-part listening to this.
I am not sure if this API is useful? It could be for something like guild chat, perhaps :?
There's still the issue here of the client not being able to send these messages directly though so maybe it's better to just leave this sort of stuff as more manual. I don't know if I feel like adding that capability unless there's an explicit handler for that the user must specify somehow.
The current package version on Wally is on 3.0.1
. This is two releases behind main
, which is currently at 3.0.3
.
Furthermore, since namespaces are broken on 3.0.1
(see #80 and #67)- this should probably be a high priority.
I'm using luau and it throws a type error, seems like isn't published to wally
current version in wally: 3.0.1
current project version: 3.0.3
Right now there is no way to make a piece of middleware apply to all remotes in a Definition
. This would be useful for applications such as logging or analytics; I want to track how much data is going through remotes and at what frequency in production.
Maybe when creating a Definition
you could also pass in an array of Middleware
as a second parameter. Something like the following:
Net.Definitions.Create({
SomeEvent = Net.Definitions.Event()
}, [
Net.Definitions.ReadonlyMiddleware((remoteName, remoteData) => {
// Do something
})
])
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.