From 6fc976813c8c0d319f3e1e303987f549cbf5dd26 Mon Sep 17 00:00:00 2001 From: jackdotink Date: Tue, 3 Oct 2023 21:41:59 -0500 Subject: [PATCH] release 2.1.0 --- Test/Client/Client.client.luau | 21 +++++++- Test/Server/Server.server.luau | 28 ++++++++++- Test/Shared/ComplexFunction.luau | 13 +++++ Test/Shared/EmptyFunction.luau | 9 ++++ Test/Shared/SimpleFunction.luau | 10 ++++ docs/.vitepress/config.ts | 14 +++--- docs/2.0/Function.md | 45 +++++++++++++++++ docs/2.0/Red.md | 36 ++++++++++++++ docs/guide/functions.md | 83 ++++++++++++++++++++++++++++++++ lib/Event/init.luau | 2 +- lib/Function.luau | 55 +++++++++++++++++++++ lib/init.luau | 1 + wally.toml | 2 +- 13 files changed, 308 insertions(+), 11 deletions(-) create mode 100644 Test/Shared/ComplexFunction.luau create mode 100644 Test/Shared/EmptyFunction.luau create mode 100644 Test/Shared/SimpleFunction.luau create mode 100644 docs/2.0/Function.md create mode 100644 docs/guide/functions.md create mode 100644 lib/Function.luau diff --git a/Test/Client/Client.client.luau b/Test/Client/Client.client.luau index e2b11a0..ac89343 100644 --- a/Test/Client/Client.client.luau +++ b/Test/Client/Client.client.luau @@ -1,11 +1,18 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ComplexFunction = require(ReplicatedStorage.ComplexFunction) +local EmptyFunction = require(ReplicatedStorage.EmptyFunction) +local SimpleFunction = require(ReplicatedStorage.SimpleFunction) + local ComplexEvent = require(ReplicatedStorage.ComplexEvent):Client() local SimpleEvent = require(ReplicatedStorage.SimpleEvent):Client() local EmptyEvent = require(ReplicatedStorage.EmptyEvent):Client() local ReadyEvent = require(ReplicatedStorage.ReadyEvent):Client() +task.wait(0.5) + print("Registering Listeners") + ComplexEvent:On(function(Value1, Value2, Value3) print("ComplexEvent", Value1, Value2, Value3) end) @@ -18,12 +25,24 @@ EmptyEvent:On(function() print("EmptyEvent") end) +task.wait(0.5) + print("Firing Events") + ComplexEvent:Fire({ one = { "String Literal", 123 }, two = { 123, "String Literal" } }, "hello world again", 123) SimpleEvent:Fire(123) EmptyEvent:Fire() -task.wait(1) +task.wait(0.5) + +print("Calling Functions") + +print(ComplexFunction:Call({ one = { "String Literal", 123 }, two = { 123, "String Literal" } }, "hi", 12):Await()) +print(SimpleFunction:Call(123):Await()) +print(EmptyFunction:Call():Await()) + +task.wait(0.5) print("Firing Ready") + ReadyEvent:Fire() diff --git a/Test/Server/Server.server.luau b/Test/Server/Server.server.luau index b9a1320..c3c0092 100644 --- a/Test/Server/Server.server.luau +++ b/Test/Server/Server.server.luau @@ -1,11 +1,37 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ComplexFunction = require(ReplicatedStorage.ComplexFunction) +local SimpleFunction = require(ReplicatedStorage.SimpleFunction) +local EmptyFunction = require(ReplicatedStorage.EmptyFunction) + local ComplexEvent = require(ReplicatedStorage.ComplexEvent):Server() local SimpleEvent = require(ReplicatedStorage.SimpleEvent):Server() local EmptyEvent = require(ReplicatedStorage.EmptyEvent):Server() local ReadyEvent = require(ReplicatedStorage.ReadyEvent):Server() -print("Registering Listeners") +print("Registering Function Callbacks") + +ComplexFunction:SetCallback(function(Player, Value1, Value2, Value3) + print("ComplexFunction", Player, Value1, Value2, Value3) + task.wait(0.2) + + return Value1, Value2, Value3 +end) + +SimpleFunction:SetCallback(function(Player, Value) + print("SimpleFunction", Player, Value) + + return tostring(Value) +end) + +EmptyFunction:SetCallback(function(Player) + print("EmptyFunction", Player) + + return +end) + +print("Registering Event Listeners") + ComplexEvent:On(function(Player, Value1, Value2, Value3) print("ComplexEvent", Player, Value1, Value2, Value3) end) diff --git a/Test/Shared/ComplexFunction.luau b/Test/Shared/ComplexFunction.luau new file mode 100644 index 0000000..82edef6 --- /dev/null +++ b/Test/Shared/ComplexFunction.luau @@ -0,0 +1,13 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Red = require(ReplicatedStorage.Packages.Red) +local Guard = require(ReplicatedStorage.Packages.Guard) + +local ValueCheck = + Guard.Optional(Guard.Map(Guard.String, Guard.List(Guard.Or(Guard.Literal("String Literal"), Guard.Number)))) + +return Red.Function("ComplexFunction", function(Value1, Value2, Value3) + return ValueCheck(Value1), Guard.String(Value2), Guard.Number(Value3) +end, function(Value1, Value2, Value3) + return ValueCheck(Value1), Guard.String(Value2), Guard.Number(Value3) +end) diff --git a/Test/Shared/EmptyFunction.luau b/Test/Shared/EmptyFunction.luau new file mode 100644 index 0000000..baa7e15 --- /dev/null +++ b/Test/Shared/EmptyFunction.luau @@ -0,0 +1,9 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Red = require(ReplicatedStorage.Packages.Red) + +return Red.Function("EmptyFunction", function() + return +end, function() + return +end) diff --git a/Test/Shared/SimpleFunction.luau b/Test/Shared/SimpleFunction.luau new file mode 100644 index 0000000..3edd2cc --- /dev/null +++ b/Test/Shared/SimpleFunction.luau @@ -0,0 +1,10 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Red = require(ReplicatedStorage.Packages.Red) +local Guard = require(ReplicatedStorage.Packages.Guard) + +return Red.Function("SimpleFunction", function(Value) + return Guard.Number(Value) +end, function(Value) + return Guard.String(Value) +end) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index fe1ae40..45a86c7 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -4,12 +4,7 @@ function nav() { return [ { text: 'FAQ', link: '/faq' }, { text: 'Guide', link: '/guide/introduction/what-is-red' }, - { - text: 'API Reference', - items: [ - { text: '2.0', link: '/2.0/Red' }, - ] - }, + { text: 'API Reference', link: '/2.0/Red' }, ] } @@ -32,6 +27,10 @@ function sidebar() { { text: 'Client Usage', link: '/guide/events/client' }, ] }, + { + text: 'Functions', + link: '/guide/functions', + }, ], '/2.0/': [ @@ -46,7 +45,8 @@ function sidebar() { { text: 'Server', link: '/2.0/Event/Server' }, { text: 'Client', link: '/2.0/Event/Client' }, ] - } + }, + { text: 'Function', link: '/2.0/Function' }, ], } ], diff --git a/docs/2.0/Function.md b/docs/2.0/Function.md new file mode 100644 index 0000000..f40b724 --- /dev/null +++ b/docs/2.0/Function.md @@ -0,0 +1,45 @@ +# Function + +Functions allow RemoteFunction-like behavior in Red for calling client -> server. + +## SetCallback + +Sets the function's callback. + +```lua +( + Callback: (Player, A...) -> R... -- The callback to set. +) +``` + +A singular callback must be set on the server to allow clients to call the function. This callback is given the arguments and must return the expected return values. This callback may only be set on the server, attempting to set it on the client will result in an error. + +```lua +local Function = require(Path.To.Function) + +Function:SetCallback(function(Player, Arg1, Arg2, Arg3) + return Arg1, Arg2, Arg3 +end) +``` + +::: danger +If the callback errors the client will never recieve any value and will yield forever. **Doing this is a memory leak!** Do not rely on erroring not sending back values. +::: + +## Call + +Calls the function on the server. + +```lua +( + ...A: any -- The arguments to pass to the function. +): Future +``` + +A function is called on the client to call the function on the server. This method returns a [Future](https://util.redblox.dev/future) which can be used to await the return values or connect a function to be called when the return values are received. + +```lua +local Function = require(Path.To.Function) + +local Ret1, Ret2, Ret3 = Function:Call(Arg1, Arg2, Arg3):Await() +``` diff --git a/docs/2.0/Red.md b/docs/2.0/Red.md index a8cb3da..c6dd2ca 100644 --- a/docs/2.0/Red.md +++ b/docs/2.0/Red.md @@ -34,3 +34,39 @@ end) ::: tip It is recommended to use [Guard](https://util.redblox.dev/guard) to typecheck payloads as it also narrows types. ::: + +## Function + +Creates a new [function](./Function) object. + +```lua +( + Name: string, -- The name of the function + ValidateArg: (...unknown) -> A..., -- Validates function arguments + ValidateRet: (...unknown) -> R..., -- Validates function returns +) -> Function +``` + +This will create a function with the passed name. + +::: danger +The name of the function must be unique, using the same name twice will result in an error. +::: + +The validation functions are used to validate the type of the arguments and returns. The functions have the same three rules as event validation functions: + +1. The callback returns the arguments in the same order they were passed in. +2. The callback must error if the arguments are invalid. +3. The callback must narrow the types of the arguments. + +```lua +return Red.Function("FunctionName", function(Number, String) + return Guard.Number(Number), Guard.String(String) +end, function(Number, String) + return Guard.Number(Number), Guard.String(String) +end) +``` + +::: tip +It is recommended to use [Guard](https://util.redblox.dev/guard) to typecheck as it also narrows types. +::: diff --git a/docs/guide/functions.md b/docs/guide/functions.md new file mode 100644 index 0000000..4b22ee8 --- /dev/null +++ b/docs/guide/functions.md @@ -0,0 +1,83 @@ +# Functions + +Functions are Red's version of RemoteFunctions. If you don't know what they are, it's a way to call code on the server from the client and get return values. Red only allows clients to call the server, not the server to call clients. + +## Declaring + +Functions are declared similarly to events, they take two validation callbacks: one for the arguments and one for the return value. + +```lua +local Red = require(Path.To.Red) + +return Red.Function("FunctionName", function(Arg1, Arg2, Arg3) + assert(type(Arg1) == "string") + assert(type(Arg2) == "number") + assert(type(Arg3) == "boolean") + + return Arg1, Arg2, Arg3 +end, function(Ret1, Ret2, Ret3) + assert(type(Ret1) == "string") + assert(type(Ret2) == "number") + assert(type(Ret3) == "boolean") + + return Ret1, Ret2, Ret3 +end) +``` + +These callbacks must follow the same three rules that event validation callbacks do: + +1. The callback returns the arguments in the same order they were passed in. +2. The callback must error if the arguments are invalid. +3. The callback must narrow the types of the arguments. + +These callbacks are only called in specific circumstances. Do not use these callbacks as middleware, logging, or other side effects. + +::: tip +I once again suggest using [Guard](https://util.redblox.dev/guard) to both narrow and check types at the same time. + +```lua +local Red = require(Path.To.Red) +local Guard = require(Path.To.Guard) + +local CheckArg1 = Guard.Map(Guard.String, Guard.Number) +local CheckArg2 = Guard.List(Guard.Vector3) +local CheckArg3 = Guard.Boolean + +local CheckRet1 = Guard.String +local CheckRet2 = Guard.Number +local CheckRet3 = Guard.Set(Guard.String) + +return Red.Function("FunctionName", function(Arg1, Arg2, Arg3) + return CheckArg1(Arg1), CheckArg2(Arg2), CheckArg3(Arg3) +end, function(Ret1, Ret2, Ret3) + return CheckRet1(Ret1), CheckRet2(Ret2), CheckRet3(Ret3) +end) +``` + +::: + +## Set Callback + +A singular callback must be set on the server to allow clients to call the event. This callback is given the arguments and must return the expected return values. + +```lua +local Function = require(Path.To.Function) + +Function:SetCallback(function(Player, Arg1, Arg2, Arg3) + return Arg1, Arg2, Arg3 +end) +``` + +::: danger +If the callback errors then the client will never recieve any value and will yield forever. **Doing this is a memory leak!** Do not rely on erroring not sending back values. +::: + +## Calling + +Functions can only be called from the client. The client must pass valid arguments to the function, and will be given back a [Future](https://util.redblox.dev/future) that completes with the returned values. + +```lua +local Function = require(Path.To.Function) + +local Ret1, Ret2, Ret3 = Function:Call("Hello", 1, true):Await() +``` diff --git a/lib/Event/init.luau b/lib/Event/init.luau index 61eef6b..dcdf607 100644 --- a/lib/Event/init.luau +++ b/lib/Event/init.luau @@ -37,7 +37,7 @@ local function Client(self: Event): ClientEvent.Client end local function Event(Name: string, Validate: (...unknown) -> T...): Event - assert(not Identifier.Exists(Name), "Cannot create event with duplicate name!") + assert(not Identifier.Exists(Name), "Cannot use same name twice") return { Id = Identifier.Shared(Name):Await(), diff --git a/lib/Function.luau b/lib/Function.luau new file mode 100644 index 0000000..423d0a9 --- /dev/null +++ b/lib/Function.luau @@ -0,0 +1,55 @@ +local RunService = game:GetService("RunService") + +local Future = require(script.Parent.Parent.Future) + +local ServerEvent = require(script.Parent.ServerEvent) +local ClientEvent = require(script.Parent.ClientEvent) +local Identifier = require(script.Parent.Identifier) + +local function PackArgs(...: any) + return { ... } +end + +export type Function = { + Id: string, + Validate: (...unknown) -> A..., + Listening: boolean, + + SetCallback: (self: Function, Callback: (Player, A...) -> R...) -> (), + Call: (self: Function, A...) -> typeof(Future.new(function(): R... end)), +} + +local function SetCallback(self: Function, Callback: (Player, A...) -> R...) + assert(RunService:IsServer(), "Cannot set callback to function on client") + assert(not self.Listening, "Cannot set callback to function multiple times") + + self.Listening = true + ServerEvent.Listen(self.Id, function(Player, ...) + if pcall(self.Validate, ...) then + return Callback(Player, ...) + end + end) +end + +local function Call(self: Function, ...: A...) + return ClientEvent.Call(self.Id, PackArgs(...)) +end + +local function Function( + Name: string, + ValidateArg: (...unknown) -> A..., + ValidateRet: (...unknown) -> R... +): Function + assert(not Identifier.Exists(Name), "Cannot use same name twice") + + return { + Id = Identifier.Shared(Name):Await(), + Validate = ValidateArg, + Listening = false, + + SetCallback = SetCallback, + Call = Call, + } :: any +end + +return Function diff --git a/lib/init.luau b/lib/init.luau index 6d67583..b79642b 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -40,4 +40,5 @@ end return { Event = require(script.Event), + Function = require(script.Function), } diff --git a/wally.toml b/wally.toml index 236d3e1..bd301eb 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "red-blox/red" -version = "2.0.0" +version = "2.1.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" license = "MIT"