diff --git a/src/apply.luau b/src/apply.luau index b0408ae..828f193 100644 --- a/src/apply.luau +++ b/src/apply.luau @@ -1,12 +1,13 @@ +local typeof = typeof + if not game then - script = (require :: any) "test/wrap-require" - typeof = require "test/mock".typeof + script = require "test/wrap-require" + typeof = require "test/mock".typeof :: any end local graph = require(script.Parent.graph) type Node = graph.Node -local throw = require(script.Parent.throw) local bind = require(script.Parent.bind) local _, is_action = require(script.Parent.action)() diff --git a/src/bind.luau b/src/bind.luau index 6f65990..4a36a34 100644 --- a/src/bind.luau +++ b/src/bind.luau @@ -1,13 +1,12 @@ -local warn = warn -- todo +local warn = warn if not game then - script = (require :: any) "test/wrap-require" + script = require "test/wrap-require" warn = print end local graph = require(script.Parent.graph) type Node = graph.Node -local get = graph.get local set_effect = graph.set_effect local capture = graph.capture diff --git a/src/create.luau b/src/create.luau index baf2b0f..06ded51 100644 --- a/src/create.luau +++ b/src/create.luau @@ -1,5 +1,8 @@ +local Instance = Instance +local typeof = typeof + if not game then - script = (require :: any) "test/wrap-require" + script = require "test/wrap-require" Instance = require("test/mock").Instance typeof = require("test/mock").typeof end diff --git a/src/defaults.luau b/src/defaults.luau index abc3e10..b1049d6 100644 --- a/src/defaults.luau +++ b/src/defaults.luau @@ -1,14 +1,12 @@ - --- todo local Enum = Enum local Color3 = Color3 local Vector3 = Vector3 if not game then local mock = require "test/mock" - Enum = mock.Enum :: any - Color3 = mock.Color3 :: any - Vector3 = mock.Vector3 :: any + Enum = mock.Enum + Color3 = mock.Color3 + Vector3 = mock.Vector3 end return { diff --git a/src/derive.luau b/src/derive.luau index 7a05384..ca44b43 100644 --- a/src/derive.luau +++ b/src/derive.luau @@ -1,8 +1,7 @@ -if not game then script = (require :: any) "test/wrap-require" end +if not game then script = require "test/wrap-require" end local graph = require(script.Parent.graph) local create = graph.create -local get = graph.get local capture_and_link = graph.capture_and_link local function derive(fn: () -> T): () -> T diff --git a/src/graph.luau b/src/graph.luau index eb79d8f..60e1158 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -1,4 +1,4 @@ -if not game then script = (require :: any) "test/wrap-require" end +if not game then script = require "test/wrap-require" end local flags = require(script.Parent.flags) diff --git a/src/init.luau b/src/init.luau index 1bf027b..cabd2f2 100644 --- a/src/init.luau +++ b/src/init.luau @@ -3,7 +3,7 @@ -- v0.1.0 -------------------------------------------------------------------------------- -if not game then script = (require :: any) "test/wrap-require" end +if not game then script = require "test/wrap-require" end local create = require(script.create) local source = require(script.source) diff --git a/src/maps.luau b/src/maps.luau index dd87536..553b495 100644 --- a/src/maps.luau +++ b/src/maps.luau @@ -1,4 +1,4 @@ -if not game then script = (require :: any) "test/wrap-require" end +if not game then script = require "test/wrap-require" end local graph = require(script.Parent.graph) type Node = graph.Node diff --git a/src/spring.luau b/src/spring.luau index 1049d5c..2326e51 100644 --- a/src/spring.luau +++ b/src/spring.luau @@ -1,4 +1,4 @@ -if not game then script = (require :: any) "test/wrap-require" end +if not game then script = require "test/wrap-require" end --[[ diff --git a/src/watch.luau b/src/watch.luau index a50b51a..e18a871 100644 --- a/src/watch.luau +++ b/src/watch.luau @@ -1,4 +1,4 @@ -if not game then script = (require :: any) "test/wrap-require" end +if not game then script = require "test/wrap-require" end local graph = require(script.Parent.graph) local set_effect = graph.set_effect diff --git a/test/goodsignal.luau b/test/goodsignal.luau deleted file mode 100644 index 6aa1fb4..0000000 --- a/test/goodsignal.luau +++ /dev/null @@ -1,184 +0,0 @@ ---!nocheck - --- modified for use in pure luau - -local task = { spawn = function(thread, ...) - local ok, err = coroutine.resume(thread, ...) - if not ok then error(err, 3) end -end } - -export type Type = RBXScriptSignal & { Fire: (Type, ...any)-> () } - ----------------------------------------------------------------------------------------------------- --- Batched Yield-Safe Signal Implementation -- --- This is a Signal class which has effectively identical behavior to a -- --- normal RBXScriptSignal, with the only difference being a couple extra -- --- stack frames at the bottom of the stack trace when an error is thrown. -- --- This implementation caches runner coroutines, so the ability to yield in -- --- the signal handlers comes at minimal extra cost over a naive signal -- --- implementation that either always or never spawns a thread. -- --- -- --- API: -- --- local Signal = require(THIS MODULE) -- --- local sig = Signal.new() -- --- local connection = sig:Connect(function(arg1, arg2, ...) ... end) -- --- sig:Fire(arg1, arg2, ...) -- --- connection:Disconnect() -- --- sig:DisconnectAll() -- --- local arg1, arg2, ... = sig:Wait() -- --- -- --- Licence: -- --- Licenced under the MIT licence. -- --- -- --- Authors: -- --- stravant - July 31st, 2021 - Created the file. -- ----------------------------------------------------------------------------------------------------- - --- The currently idle thread to run the next handler on -local freeRunnerThread = nil - --- Function which acquires the currently idle handler runner thread, runs the --- function fn on it, and then releases the thread, returning it to being the --- currently idle one. --- If there was a currently idle runner thread already, that's okay, that old --- one will just get thrown and eventually GCed. -local function acquireRunnerThreadAndCallEventHandler(fn, ...) - local acquiredRunnerThread = freeRunnerThread - freeRunnerThread = nil - fn(...) - -- The handler finished running, this runner thread is free again. - freeRunnerThread = acquiredRunnerThread -end - --- Coroutine runner that we create coroutines of. The coroutine can be --- repeatedly resumed with functions to run followed by the argument to run --- them with. -local function runEventHandlerInFreeThread() - -- Note: We cannot use the initial set of arguments passed to - -- runEventHandlerInFreeThread for a call to the handler, because those - -- arguments would stay on the stack for the duration of the thread's - -- existence, temporarily leaking references. Without access to raw bytecode - -- there's no way for us to clear the "..." references from the stack. - while true do - acquireRunnerThreadAndCallEventHandler(coroutine.yield()) - end -end - --- Connection class -local Connection = {} -Connection.__index = Connection - -function Connection.new(signal, fn) - return setmetatable({ - _connected = true, - _signal = signal, - _fn = fn, - _next = false, - }, Connection) -end - -function Connection:Disconnect() - self._connected = false - - -- Unhook the node, but DON'T clear it. That way any fire calls that are - -- currently sitting on this node will be able to iterate forwards off of - -- it, but any subsequent fire calls will not hit it, and it will be GCed - -- when no more fire calls are sitting on it. - if self._signal._handlerListHead == self then - self._signal._handlerListHead = self._next - else - local prev = self._signal._handlerListHead - while prev and prev._next ~= self do - prev = prev._next - end - if prev then - prev._next = self._next - end - end -end - --- Make Connection strict -setmetatable(Connection, { - __index = function(tb, key) - error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2) - end, - __newindex = function(tb, key, value) - error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2) - end -}) - --- Signal class -local Signal = {} -Signal.__index = Signal - -Signal.__type = "RBXScriptSignal" - -function Signal.new(): Type - return setmetatable({ - _handlerListHead = false, - }, Signal) :: any -end - -function Signal:Connect(fn) - if type(fn) ~= "function" then error(`attempt to connect non function (got { type(fn) })`, 2) end - local connection = Connection.new(self, fn) - if self._handlerListHead then - connection._next = self._handlerListHead - self._handlerListHead = connection - else - self._handlerListHead = connection - end - return connection -end - --- Disconnect all handlers. Since we use a linked list it suffices to clear the --- reference to the head handler. -function Signal:DisconnectAll() - self._handlerListHead = false -end - --- Signal:Fire(...) implemented by running the handler functions on the --- coRunnerThread, and any time the resulting thread yielded without returning --- to us, that means that it yielded to the Roblox scheduler and has been taken --- over by Roblox scheduling, meaning we have to make a new coroutine runner. -function Signal:Fire(...) - local item = self._handlerListHead - while item do - if item._connected then - if not freeRunnerThread then - freeRunnerThread = coroutine.create(runEventHandlerInFreeThread) - -- Get the freeRunnerThread to the first yield - coroutine.resume(freeRunnerThread) - end - task.spawn(freeRunnerThread, item._fn, ...) - end - item = item._next - end -end - --- Implement Signal:Wait() in terms of a temporary connection using --- a Signal:Connect() which disconnects itself. -function Signal:Wait() - local waitingCoroutine = coroutine.running() - local cn; - cn = self:Connect(function(...) - cn:Disconnect() - task.spawn(waitingCoroutine, ...) - end) - return coroutine.yield() -end - --- Implement Signal:Once() in terms of a connection which disconnects --- itself before running the handler. -function Signal:Once(fn) - local cn; - cn = self:Connect(function(...) - if cn._connected then - cn:Disconnect() - end - fn(...) - end) - return cn -end - -return Signal diff --git a/test/mock.luau b/test/mock.luau index e943a25..e21b872 100644 --- a/test/mock.luau +++ b/test/mock.luau @@ -1,22 +1,58 @@ -local Instance = {} do - local Signal = require "test/goodsignal" - type Signal = Signal.Type +--[[ - type userdata = { __USERDATA: true } +contains mock Roblox API interfaces for unit testing +]] + +local Signal = {} :: any do + Signal.__index = Signal + Signal.__type = "RBXScriptSignal" + + local function new_connection(signal, fn) + return { + signal = signal, + fn = fn, + + Disconnect = function(self) + local i = table.find(signal.connections, self) + if not i then return end + table.remove(signal.connections, i) + end + } + end + + function Signal.new() + return setmetatable({ + connections = {} + }, Signal) + end + + function Signal.Connect(self, fn) + local con = new_connection(self, fn) + table.insert(self.connections, con) + return con + end + + function Signal.fire(self, ...) + for i = #self.connections, 1, -1 do + self.connections[i].fn(...) + end + end +end + +local Instance = {} :: any do --[[ - attempt to mimic roblox engine's method of userdata proxy to actual instance data + attempt to mimic roblox engine's method of userdata reflection proxy can gc independantly of actual instance data proxy prevents gc of actual instance data - luau code never has direct access to actual instance data, only to proxy - - proxy knows data - data does not know proxy - separate weak map kept data -> proxy + user code never has direct access to actual instance data, only to proxy + internal weak map kept data -> proxy ]] + type userdata = { __USERDATA: true } + type ProxyMT = { proxy: userdata, data: Data, @@ -28,9 +64,9 @@ local Instance = {} do name: string, parent: Data?, children: { Data }, - changed: { [string]: Signal }, + changed: { [string]: RBXScriptSignal }, properties: { [string]: unknown }, - destroying: Signal, + destroying: RBXScriptSignal, class: string, type: "Instance" } @@ -47,8 +83,6 @@ local Instance = {} do return t :: T & {} end - - local proxies = {} :: { [Data]: userdata? } setmetatable(proxies :: any, { __mode = "v" }) @@ -96,7 +130,7 @@ local Instance = {} do end if data.changed[property] then - data.changed[property]:Fire() + Signal.fire(data.changed[property]) end end @@ -175,17 +209,17 @@ local Instance = {} do function methods.Destroy(userdata: userdata) local data = get_data(userdata); - data.destroying:Fire() + Signal.fire(data.destroying) data.parent = nil if data.changed["Parent"] then - data.changed["Parent"]:Fire() + Signal.fire(data.changed["Parent"]) end end end -local Color3 = {} do - function Color3.new(r, g, b): Color3 - return setmetatable({ r = r, g = g, b = b}, Color3) :: any +local Color3 = {} :: any do + function Color3.new(r, g, b) + return setmetatable({ r = r, g = g, b = b}, Color3) end function Color3.__eq(a, b) @@ -193,9 +227,9 @@ local Color3 = {} do end end -local Vector3 = {} do - function Vector3.new(x, y, z): Vector3 - return setmetatable({ x = x, y = y, z = z}, Vector3) :: any +local Vector3 = {} :: any do + function Vector3.new(x, y, z) + return setmetatable({ x = x, y = y, z = z}, Vector3) end function Vector3.__eq(a, b) @@ -203,9 +237,9 @@ local Vector3 = {} do end end -local Vector2 = {} do - function Vector2.new(x, y): Vector2 - return setmetatable({ x = x, y = y }, Vector2) :: any +local Vector2 = {} :: any do + function Vector2.new(x, y) + return setmetatable({ x = x, y = y }, Vector2) end function Vector2.__eq(a, b) @@ -213,9 +247,9 @@ local Vector2 = {} do end end -local UDim2 = {} do - function UDim2.fromScale(x, y): UDim2 - return setmetatable({ x = { scale = x, offset = 0 }, y = { scale = y, offset = 0 } }, UDim2) :: any +local UDim2 = {} :: any do + function UDim2.fromScale(x, y) + return setmetatable({ x = { scale = x, offset = 0 }, y = { scale = y, offset = 0 } }, UDim2) end function UDim2.__eq(a, b) @@ -237,18 +271,19 @@ local Enum = {} :: any do end}) end -local function typeof(v): string +local typeof = function(v) return if Instance.is_instance(v) then "Instance" elseif getmetatable(v) and getmetatable(v).__type then getmetatable(v).__type else type(v) -end +end :: any return { - Instance = Instance, - Color3 = Color3, - Vector3 = Vector3, - Vector2 = Vector2, - UDim2 = UDim2, - Enum = Enum, - typeof = typeof + Signal = Signal, + Instance = Instance :: typeof(Instance), + Color3 = Color3 :: typeof(Color3), + Vector3 = Vector3 :: typeof(Vector3), + Vector2 = Vector2 :: typeof(Vector2), + UDim2 = UDim2 :: typeof(UDim2), + Enum = Enum :: typeof(Enum), + typeof = typeof :: typeof(typeof) } diff --git a/test/tests.luau b/test/tests.luau index becea69..1de2f7e 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,12 +1,9 @@ local testkit = require("test/testkit") local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() -local Signal = require "test/goodsignal" local mock = require "test/mock" - -local Instance, Vector3, Color3, Vector2, UDim2 = - mock.Instance, mock.Vector3, mock.Color3, mock.Vector2, mock.UDim2 +local Instance, Signal = mock.Instance, mock.Signal local vide = require "src/init" @@ -1194,7 +1191,7 @@ TEST("Events", function() -- testkit.print2(getmetatable(val)) CHECK(not connected) - val.Value = 1; val.Signal:Fire(val.Value) + val.Value = 1; Signal.fire(val.Signal, val.Value) CHECK(connected) end end)