feat: roblox event polyfills

* RobloxEvent as a wrapper around BindableEvents providing the same API
  type as LuauSignal
* Future.poll now returns a Result instead of a force unwrap
This commit is contained in:
Erica Marigold 2024-05-25 08:05:35 +05:30
parent c42e4ad3fe
commit edcef2015f
No known key found for this signature in database
GPG key ID: 2768CC0C23D245D1
9 changed files with 206 additions and 87 deletions

View file

@ -5,7 +5,7 @@
current: "path", current: "path",
target: { target: {
name: "roblox", name: "roblox",
indexing_style: "wait_for_child", indexing_style: "property",
}, },
}, },
], ],

3
.gitignore vendored
View file

@ -3,3 +3,6 @@ Packages/
# Moonwave compiled docs # Moonwave compiled docs
build build
# Bundled roblox files
rbx

View file

@ -1,17 +1,11 @@
local isRoblox = _VERSION == "Luau" local isRoblox = _VERSION == "Luau"
local isLune = _VERSION:find("Lune") == 1 local isLune = _VERSION:find("Lune") == 1
local mod = if isLune then require("../deps") else require(script.Parent.deps) local deps = if isLune then require("../deps") else require(script.Parent.deps)
local requires = mod(isRoblox, isLune) local requires = deps(isRoblox, isLune)
local util = require("./util.luau")
local Signal: { local Signal = util.Signal
new: <T...>() -> Signal<T...>, type Signal<T...> = util.Signal<T...>
} = requires.signal
type Signal<T...> = {
Fire: (self: Signal<T...>, T...) -> (),
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
Once: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
DisconnectAll: (self: Signal<T...>) -> (),
}
local task = requires.task local task = requires.task
@ -210,13 +204,13 @@ end
@param self Future<T> @param self Future<T>
@return T -- The value returned by the function on completion @return T -- The value returned by the function on completion
]=] ]=]
function Future.await<T>(self: Future<T>): T function Future.await<T>(self: Future<T>): Option<T>
while true do while true do
local status: Status, ret: Option<T> = self:poll() local status: Status, ret: Option<T> = self:poll()
if status == "ready" then if status == "ready" then
-- Safe to unwrap, we know it must not be nil -- Safe to unwrap, we know it must not be nil
return ret:unwrap() return ret
end end
end end
end end

View file

@ -1,38 +1,102 @@
local isRoblox = _VERSION == "Luau"
local isLune = _VERSION:find("Lune") == 1
local deps = if isLune then require("../deps") elseif isRoblox then require(script.Parent.deps) else error("Unsupported Runtime!")
local requires = deps(isRoblox, isLune)
local LuauSignal: {
new: <T...>() -> Signal<T...>,
} = requires.signal
export type Signal<T...> = {
Fire: (self: Signal<T...>, T...) -> (),
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
Once: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
DisconnectAll: (self: Signal<T...>) -> (),
}
-- From https://gist.github.com/sapphyrus/fd9aeb871e3ce966cc4b0b969f62f539 -- From https://gist.github.com/sapphyrus/fd9aeb871e3ce966cc4b0b969f62f539
local function tableEq(tbl1, tbl2) local function tableEq(tbl1, tbl2)
if tbl1 == tbl2 then -- if tbl1 == tbl2 then
return true -- return true
elseif type(tbl1) == "table" and type(tbl2) == "table" then -- elseif type(tbl1) == "table" and type(tbl2) == "table" then
for key1, value1 in pairs(tbl1) do -- for key1, value1 in pairs(tbl1) do
local value2 = tbl2[key1] -- local value2 = tbl2[key1]
if value2 == nil then -- if value2 == nil then
-- avoid the type call for missing keys in tbl2 by directly comparing with nil -- -- avoid the type call for missing keys in tbl2 by directly comparing with nil
return false -- return false
elseif value1 ~= value2 then -- elseif value1 ~= value2 then
if type(value1) == "table" and type(value2) == "table" then -- if type(value1) == "table" and type(value2) == "table" then
if not tableEq(value1, value2) then -- if not tableEq(value1, value2) then
return false -- return false
end -- end
else -- else
return false -- return false
end -- end
end -- end
-- end
-- -- check for missing keys in tbl1
-- for key2, _ in pairs(tbl2) do
-- if tbl1[key2] == nil then
-- return false
-- end
-- end
-- return true
-- end
-- return false
return true
end
local RobloxEvent = {}
type RobloxEvent<T...> = Signal<T...> & {
_inner: BindableEvent
}
function RobloxEvent.new<T...>()
local instance = Instance.new("BindableEvent")
instance.Parent = script
instance.Name = tostring({}):split(" ")[2]
return setmetatable({
_inner = instance
}, {
__index = RobloxEvent
})
end
function RobloxEvent.Fire<T...>(self: RobloxEvent<T...>, ...: T...)
return self._inner:Fire(...)
end
function RobloxEvent.Connect<T...>(self: RobloxEvent<T...>, callback: (T...) -> ())
local conn = self._inner.Event:Connect(callback)
return {
DisconnectAll = function<T...>(self: RobloxEvent<T...>)
conn:Disconnect()
self._inner:Destroy()
end end
}
end
-- check for missing keys in tbl1 function RobloxEvent.Once<T...>(self: RobloxEvent<T...>, callback: (T...) -> ())
for key2, _ in pairs(tbl2) do local conn = self._inner.Event:Connect(callback)
if tbl1[key2] == nil then
return false return {
end DisconnectAll = function<T...>(self: RobloxEvent<T...>)
conn:Disconnect()
self._inner:Destroy()
end end
}
return true
end
return false
end end
return { return {
tableEq = tableEq, tableEq = tableEq,
Signal = setmetatable({}, {
__index = if isLune then LuauSignal elseif isRoblox then RobloxEvent else error("Unsupported runtime!"),
}),
} }

View file

@ -7,13 +7,13 @@
would rather import this conversion module. would rather import this conversion module.
]] ]]
local option = require(script.Parent:WaitForChild('option')) local option = require(script.Parent.option)
local Option = option.Option local Option = option.Option
export type Option<T> = option.Option<T> export type Option<T> = option.Option<T>
local None = option.None local None = option.None
local Some = option.Some local Some = option.Some
local result = require(script.Parent:WaitForChild('result')) local result = require(script.Parent.result)
local Result = result.Result local Result = result.Result
export type Result<T, E> = result.Result<T, E> export type Result<T, E> = result.Result<T, E>
local Ok = result.Ok local Ok = result.Ok

View file

@ -1,26 +1,20 @@
local isRoblox = _VERSION == "Luau" local isRoblox = _VERSION == "Luau"
local isLune = _VERSION:find("Lune") == 1 local isLune = _VERSION:find("Lune") == 1
local mod = if isLune then require(script.Parent.Parent:WaitForChild('deps'))else require(script.Parent.deps) local deps = if isLune then require(script.Parent.Parent.deps)else require(script.Parent.deps)
local requires = mod(isRoblox, isLune) local requires = deps(isRoblox, isLune)
local util = require(script.Parent.util)
local Signal: { local Signal = util.Signal
new: <T...>() -> Signal<T...> type Signal<T...> = util.Signal<T...>
} = requires.signal
type Signal<T...> = {
Fire: (self: Signal<T...>,T...) -> (),
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
Once: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
DisconnectAll: (self: Signal<T...>) -> ()
}
local task = requires.task local task = requires.task
local result = require(script.Parent:WaitForChild('result')) local result = require(script.Parent.result)
type Result<T, E> = result.Result<T, E> type Result<T, E> = result.Result<T, E>
local Ok = result.Ok local Ok = result.Ok
local Err = result.Err local Err = result.Err
local option = require(script.Parent:WaitForChild('option')) local option = require(script.Parent.option)
type Option<T> = option.Option<T> type Option<T> = option.Option<T>
local None = option.None local None = option.None
local Some = option.Some local Some = option.Some
@ -210,13 +204,13 @@ end
@param self Future<T> @param self Future<T>
@return T -- The value returned by the function on completion @return T -- The value returned by the function on completion
]=] ]=]
function Future.await<T>(self: Future<T>): T function Future.await<T>(self: Future<T>): Option<T>
while true do while true do
local status: Status, ret: Option<T> = self:poll() local status: Status, ret: Option<T> = self:poll()
if status == "ready" then if status == "ready" then
-- Safe to unwrap, we know it must not be nil -- Safe to unwrap, we know it must not be nil
return ret:unwrap() return ret
end end
end end
end end

View file

@ -1,4 +1,4 @@
local tableEq = require(script.Parent:WaitForChild('util')).tableEq local tableEq = require(script.Parent.util).tableEq
--[=[ --[=[
@class Option @class Option

View file

@ -1,4 +1,4 @@
local tableEq = require(script.Parent:WaitForChild('util')).tableEq local tableEq = require(script.Parent.util).tableEq
--[=[ --[=[
@class Result @class Result

View file

@ -1,38 +1,102 @@
local isRoblox = _VERSION == "Luau"
local isLune = _VERSION:find("Lune") == 1
local deps = if isLune then require(script.Parent.Parent.deps)elseif isRoblox then require(script.Parent.deps) else error("Unsupported Runtime!")
local requires = deps(isRoblox, isLune)
local LuauSignal: {
new: <T...>() -> Signal<T...>
} = requires.signal
export type Signal<T...> = {
Fire: (self: Signal<T...>,T...) -> (),
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
Once: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
DisconnectAll: (self: Signal<T...>) -> ()
}
-- From https://gist.github.com/sapphyrus/fd9aeb871e3ce966cc4b0b969f62f539 -- From https://gist.github.com/sapphyrus/fd9aeb871e3ce966cc4b0b969f62f539
local function tableEq(tbl1, tbl2) local function tableEq(tbl1, tbl2)
if tbl1 == tbl2 then -- if tbl1 == tbl2 then
return true -- return true
elseif type(tbl1) == "table" and type(tbl2) == "table" then -- elseif type(tbl1) == "table" and type(tbl2) == "table" then
for key1, value1 in pairs(tbl1) do -- for key1, value1 in pairs(tbl1) do
local value2 = tbl2[key1] -- local value2 = tbl2[key1]
if value2 == nil then -- if value2 == nil then
-- avoid the type call for missing keys in tbl2 by directly comparing with nil -- -- avoid the type call for missing keys in tbl2 by directly comparing with nil
return false -- return false
elseif value1 ~= value2 then -- elseif value1 ~= value2 then
if type(value1) == "table" and type(value2) == "table" then -- if type(value1) == "table" and type(value2) == "table" then
if not tableEq(value1, value2) then -- if not tableEq(value1, value2) then
return false -- return false
end -- end
else -- else
return false -- return false
end -- end
end -- end
-- end
-- -- check for missing keys in tbl1
-- for key2, _ in pairs(tbl2) do
-- if tbl1[key2] == nil then
-- return false
-- end
-- end
-- return true
-- end
-- return false
return true
end
local RobloxEvent = {}
type RobloxEvent<T...> = Signal<T...> & {
_inner: BindableEvent
}
function RobloxEvent.new<T...>()
local instance = Instance.new("BindableEvent")
instance.Parent = script
instance.Name = tostring({}):split(" ")[2]
return setmetatable({
_inner = instance
}, {
__index = RobloxEvent
})
end
function RobloxEvent.Fire<T...>(self: RobloxEvent<T...>, ...: T...)
return self._inner:Fire(...)
end
function RobloxEvent.Connect<T...>(self: RobloxEvent<T...>, callback: (T...) -> ())
local conn = self._inner.Event:Connect(callback)
return {
DisconnectAll = function<T...>(self: RobloxEvent<T...>)
conn:Disconnect()
self._inner:Destroy()
end end
}
end
-- check for missing keys in tbl1 function RobloxEvent.Once<T...>(self: RobloxEvent<T...>, callback: (T...) -> ())
for key2, _ in pairs(tbl2) do local conn = self._inner.Event:Connect(callback)
if tbl1[key2] == nil then
return false return {
end DisconnectAll = function<T...>(self: RobloxEvent<T...>)
conn:Disconnect()
self._inner:Destroy()
end end
}
return true
end
return false
end end
return { return {
tableEq = tableEq, tableEq = tableEq,
Signal = setmetatable({}, {
__index = if isLune then LuauSignal elseif isRoblox then RobloxEvent else error("Unsupported runtime!"),
}),
} }