├── .gitignore ├── LICENSE ├── README.md ├── aftman.toml ├── default.project.json ├── moonwave.toml ├── selene.toml └── src ├── PlayerScripts ├── Benchmarking │ ├── Connect.lua │ ├── Disconnect.lua │ ├── Fire.lua │ ├── MultipleDisconnect.lua │ └── init.client.lua ├── SignalTesting.client.lua └── TypeTesting.client.lua └── ReplicatedStorage ├── FastSignal ├── Deferred.lua ├── Docs.lua ├── Immediate.lua ├── Readme.lua ├── init.lua ├── wally.lock └── wally.toml ├── GoodSignal.lua ├── LemonSignal.lua └── RBXScriptSignal.lua /.gitignore: -------------------------------------------------------------------------------- 1 | /roblox.toml 2 | /build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 LucasMZ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [*Fast*Signal](https://github.com/RBLXUtils/FastSignal) 2 | 3 | *Fast*Signal is a Signal library made with consistency and expectable behavior in mind, it is efficient, easy to use, and widely compatible. 4 | 5 | ## What about GoodSignal? 6 | 7 | GoodSignal while being an interesting implementation (that even helped *Fast*Signal be developed), it suffers from some issues. 8 | 9 | * GoodSignal does not support .Connected. 10 | 11 | * GoodSignal is made only for Immediate mode, and does not have a Deferred mode option, while sleitnick's fork has `:FireDeferred`, it's not a very optimal solution as it's not a toggle, you have to go out of your way to use Deferred and it's inconvenient. 12 | 13 | * GoodSignal has no :Destroy, only :DisconnectAll which means you can’t stop new connections from being created. 14 | 15 | * GoodSignal’s :DisconnectAll does not call :Disconnect on connections, this causes an [inconsistency with RBXScriptSignals.](https://github.com/stravant/goodsignal/issues/4) 16 | 17 | * GoodSignal’s connections and linked list nodes are the same reference, which causes issues such as disconnected connections can leak the connection’s function, signal, and other connections if not cleared properly. 18 | 19 | * GoodSignal’s classes are strict, meaning you can index members that don’t exist, this is pretty useless, and means that empty fields in a class are false, and not nil, which is something that makes forking a bit harder and it takes a bit of time to process. 20 | 21 | * GoodSignal’s connections are not immediately compatible with Janitor, or Maid. 22 | 23 | * GoodSignal’s methods don’t have any type declaration at all, which would make it way nicer to use. 24 | 25 | *Fast*Signal fixes all these issues. 26 | *Fast*Signal's selling point is parity with RBXScriptSignal's API and *familiarity*. 27 | 28 | *Fast*Signal has a familiar API and behavior to RBXScriptSignals and other signal libraries, which help you work faster, these help you not have headaches while using *Fast*Signal. 29 | 30 | ## Installation 31 | 32 | ### From GitHub 33 | 34 | You can get a `.rbxmx` file from a release on GitHub, you can do that by visiting [FastSignal's releases.](https://github.com/RBLXUtils/FastSignal/releases) 35 | 36 | ### From Roblox 37 | 38 | You can get FastSignal directly from Roblox, via its Roblox Model. 39 | You can find it [here.](https://www.roblox.com/library/6532460357) 40 | 41 | ### From Wally 42 | 43 | You can get FastSignal as a dependency on Wally. 44 | Add `lucasmzreal/fastsignal` in your dependencies and you're done. 45 | 46 | ```toml 47 | Signal = "lucasmzreal/fastsignal@10.4.0" 48 | ``` 49 | 50 | 51 | Releases 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /aftman.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Aftman, a cross-platform toolchain manager. 2 | # For more information, see https://github.com/LPGhatguy/aftman 3 | 4 | # To add a new tool, add an entry to this table. 5 | [tools] 6 | rojo = "rojo-rbx/rojo@7.4.4" 7 | wally = "UpliftGames/wally@0.3.2" 8 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FastSignal", 3 | "tree": { 4 | "$className": "DataModel", 5 | 6 | "ReplicatedStorage": { 7 | "$className": "ReplicatedStorage", 8 | "$path": "src/ReplicatedStorage" 9 | }, 10 | 11 | "ServerScriptService": { 12 | "$className": "ServerScriptService", 13 | "$path": "src/ServerScriptService" 14 | }, 15 | 16 | "ServerStorage": { 17 | "$className": "ServerStorage", 18 | "$path": "src/ServerStorage" 19 | }, 20 | 21 | "StarterPlayer": { 22 | "$className": "StarterPlayer", 23 | 24 | "StarterPlayerScripts": { 25 | "$className": "StarterPlayerScripts", 26 | "$path": "src/PlayerScripts" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /moonwave.toml: -------------------------------------------------------------------------------- 1 | title = "FastSignal" # From Git 2 | gitRepoUrl = "https://github.com/RBLXUtils/FastSignal" # From Git 3 | 4 | gitSourceBranch = "main" 5 | changelog = true 6 | classOrder = ["ScriptSignal", "ScriptConnection"] 7 | 8 | [home] 9 | enabled = false 10 | includeReadme = true 11 | 12 | [docusaurus] 13 | onBrokenLinks = "throw" 14 | onBrokenMarkdownLinks = "warn" 15 | 16 | # From git: 17 | organizationName = "RBLXUtils" 18 | projectName = "FastSignal" 19 | url = "https://rblxutils.github.io" 20 | baseUrl = "/FastSignal" 21 | tagline = "Made with consistency and familiarity in mind" 22 | 23 | [footer] 24 | style = "dark" -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" 2 | 3 | [rules] 4 | undefined_variable = "warn" 5 | unused_variable = "allow" 6 | empty_if = "allow" 7 | incorrect_standard_library_use = "warn" 8 | parenthese_conditions = "allow" 9 | mismatched_arg_count = "allow" -------------------------------------------------------------------------------- /src/PlayerScripts/Benchmarking/Connect.lua: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!native 3 | 4 | --[[ 5 | Expectations: 6 | 7 | RBXScriptSignal loses by a long shot, while FastSignal loses slightly by GoodSignal. 8 | Why? I believe this is because RBXScriptSignals have to 9 | listen to the script that made that connection 10 | for when it is destroyed / deactivated, 11 | so it can disconnect it. 12 | 13 | This is expected as FastSignal uses two tables for connections and GoodSignal uses one. 14 | FastSignal does this for memory management reasons, it prevents accidental leaks. 15 | 16 | Results: 17 | 18 | Well, the expectations sometimes are accurate, sometimes not, and when they're not 19 | they can be a lot of times quite faster than GoodSignal. Don't know why. 20 | ]] 21 | 22 | local NumberOfIterations = 1000 23 | local NumberOfBenchmarks = 3 24 | 25 | local function singleBenchmark(event) 26 | local Connect = event.Connect 27 | -- Avoid indexing speeds, those are important, but it's nice to test it 28 | -- without them. 29 | 30 | local initialTime = os.clock() 31 | for _ = 1, NumberOfIterations do 32 | Connect(event, function() end) -- Luau has an optimization where equal functions are cached 33 | end 34 | 35 | return os.clock() - initialTime 36 | end 37 | 38 | return function(event) 39 | local results = table.create(NumberOfBenchmarks) 40 | for _ = 1, NumberOfBenchmarks do 41 | local result = singleBenchmark(event) 42 | table.insert(results, result) 43 | task.wait() 44 | end 45 | 46 | local average = 0 47 | for _, result in ipairs(results) do 48 | average += result 49 | end 50 | average = average / NumberOfBenchmarks 51 | 52 | return average 53 | end -------------------------------------------------------------------------------- /src/PlayerScripts/Benchmarking/Disconnect.lua: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!native 3 | 4 | --[[ 5 | Expectations: 6 | 7 | BindableEvent loses by a long shot. 8 | 9 | FastSignal loses to GoodSignal, GoodSignal doesn't have a previous reference 10 | in its connection nodes, FastSignal does, and that's for disconnecting older 11 | connections in an optimized manner, however, for single connections that means 12 | that it would need one more table search. 13 | 14 | FastSignal also has one extra table search for just finding the _node 15 | reference on the ScriptConnection object. 16 | 17 | Things are not looking good for FastSignal here. 18 | 19 | Results: 20 | 21 | Note: After a BUG FIX of all things, FastSignal seems to be winning in 22 | this benchmark sometimes, sometimes losing. As far as I tested, 23 | FastSignal seems to be winning more frequently at least in machine. 24 | This might not be the case for you. 25 | 26 | ]] 27 | 28 | -- This test only benchmark the speed of a signal 29 | -- that only has one node, where I expect FastSignal to lose, FastSignal 30 | -- is optimized for multiple connections and random access. 31 | 32 | local NumberOfIterations = 1000 33 | local NumberOfBenchmarks = 3 34 | 35 | local function singleBenchmark(Event) 36 | local Disconnect = Event:Connect(function() 37 | 38 | end) 39 | Disconnect = Disconnect:Disconnect() or Disconnect.Disconnect 40 | 41 | local totalTime = 0 42 | for _ = 1, NumberOfIterations do 43 | local connection = Event:Connect(function() 44 | 45 | end) 46 | 47 | local initialTime = os.clock() 48 | Disconnect(connection) 49 | totalTime += os.clock() - initialTime 50 | end 51 | 52 | return totalTime 53 | end 54 | 55 | return function(event) 56 | local results = table.create(NumberOfBenchmarks) 57 | for _ = 1, NumberOfBenchmarks do 58 | local result = singleBenchmark(event) 59 | table.insert(results, result) 60 | task.wait() 61 | end 62 | 63 | local average = 0 64 | for _, result in ipairs(results) do 65 | average += result 66 | end 67 | average = average / NumberOfBenchmarks 68 | 69 | return average 70 | end -------------------------------------------------------------------------------- /src/PlayerScripts/Benchmarking/Fire.lua: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!native 3 | 4 | --[[ 5 | Expectations: 6 | 7 | BindableEvent loses by a long shot. 8 | In all scenearios. 9 | 10 | NoConnections: 11 | 12 | A tie. 13 | 14 | ConnectionStress: 15 | 16 | A tie. 17 | 18 | Results: 19 | 20 | NoConnections: 21 | 22 | BindableEvents don't lose from all that much in here actually, still loses, 23 | but not as much as I expected. 24 | 25 | FastSignal loses slightly to GoodSignal, the only reason I can think of 26 | why that's the case is that GoodSignal doesn't use nil, it uses false. 27 | So a head always technically exists, so maybe that makes searching 28 | an index slightly slower. 29 | 30 | ConnectionStress: 31 | 32 | In this case, BindableEvents WIN. Yeah, even from GoodSignal. 33 | Not by much, but they seem to be consistently winning by 0.1 seconds. 34 | 35 | GoodSignal and FastSignal are staying in 0.57 seconds, while 36 | BindableEvents stay at a 0.43! 37 | 38 | FastSignal sometimes seems to win over GoodSignal in this case, by a small amount 39 | though. 40 | 41 | Note: 42 | 43 | Not recycling threads (and by extension, deferred mode) can cause 44 | the speed of firing to be 5x slower. 45 | 46 | However, this is under a sceneario where the connected handler is empty. 47 | Realistically, you might have some expensive connections. 48 | Sometimes, that trade-off might be worth it for you, as firing wouldn't slow down 49 | everything immediately. 50 | ]] 51 | 52 | local NumberOfBenchmarks = 3 53 | local Mode = "ConnectionStress" -- "NoConnections" / "ConnectionStress" 54 | 55 | local function ConnectionsSetup(Event) 56 | if Mode ~= "ConnectionStress" then 57 | return 58 | end 59 | 60 | for _ = 1, 1000 do 61 | Event:Connect(function() end) 62 | end 63 | end 64 | 65 | local function singleBenchmark(Event) 66 | ConnectionsSetup(Event.Event or Event) 67 | 68 | local Fire = Event.Fire 69 | 70 | local initialTime = os.clock() 71 | for _ = 1, 1000 do 72 | Fire(Event) 73 | end 74 | 75 | return os.clock() - initialTime 76 | end 77 | 78 | return function(event) 79 | local results = table.create(NumberOfBenchmarks) 80 | for _ = 1, NumberOfBenchmarks do 81 | local result = singleBenchmark(event) 82 | table.insert(results, result) 83 | task.wait() 84 | end 85 | 86 | local average = 0 87 | for _, result in ipairs(results) do 88 | average += result 89 | end 90 | average = average / NumberOfBenchmarks 91 | 92 | return average 93 | end -------------------------------------------------------------------------------- /src/PlayerScripts/Benchmarking/MultipleDisconnect.lua: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!native 3 | 4 | --[[ 5 | Conclusion: 6 | 7 | It has been a while since I messed with Signals and Roblox overall, but I wanted to test this. This test is not fully implemented and there is more to do. 8 | This test is meant to benchmark the benchmark of :Disconnect when indexes are chosen at random, e.g. with multiple connections on a single signal 9 | 10 | In this test, the conclusion is that GoodSignal and RBXScriptSignal fall behind. This most likely has to do with FastSignal implementing a linked list with a 11 | reference to the last node, while this adds a slight overhead for the previous benchmarks, it provides a better result for this. 12 | ]] 13 | 14 | local Mode = "Random" 15 | local NumberOfIterations = 1000 -- In this case, the number of connections 16 | local NumberOfBenchmarks = 3 17 | -- TODO: Implement Ordered 18 | 19 | local RandomizedIndexes = table.create(NumberOfIterations) do 20 | local allNumbers = table.create(NumberOfIterations) do 21 | for i = 1, NumberOfIterations do 22 | allNumbers[i] = i 23 | end 24 | end 25 | 26 | while #allNumbers ~= 0 do 27 | local randomNumber = math.random(1, #allNumbers) 28 | table.insert(RandomizedIndexes, allNumbers[randomNumber]) 29 | table.remove(allNumbers, randomNumber) 30 | end 31 | end 32 | 33 | local function singleBenchmark(Signal) 34 | local connections = table.create(NumberOfIterations) 35 | if Mode == "Random" then 36 | local connections = table.create(NumberOfIterations) 37 | for i = 1, NumberOfIterations do 38 | connections[ RandomizedIndexes[i] ] = Signal:Connect(function() 39 | 40 | end) 41 | end 42 | 43 | local initialTime = os.clock() 44 | for _, connection in ipairs(connections) do 45 | connection:Disconnect() 46 | end 47 | 48 | return os.clock() - initialTime 49 | end 50 | end 51 | 52 | return function(event) 53 | local results = table.create(NumberOfBenchmarks) 54 | for _ = 1, NumberOfBenchmarks do 55 | local result = singleBenchmark(event) 56 | table.insert(results, result) 57 | task.wait() 58 | end 59 | 60 | local average = 0 61 | for _, result in ipairs(results) do 62 | average += result 63 | end 64 | average = average / NumberOfBenchmarks 65 | 66 | return average 67 | end 68 | -------------------------------------------------------------------------------- /src/PlayerScripts/Benchmarking/init.client.lua: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!native 3 | 4 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 5 | 6 | local SignalTypes= { 7 | GoodSignal = require(ReplicatedStorage.GoodSignal), 8 | FastSignal = require(ReplicatedStorage.FastSignal), 9 | RBXScriptSignal = require(ReplicatedStorage.RBXScriptSignal), 10 | LemonSignal = require(ReplicatedStorage.LemonSignal) 11 | } :: { 12 | [string]: { 13 | new: () -> RBXScriptSignal, 14 | [any]: any 15 | } 16 | } 17 | 18 | local Benchmarks: {ModuleScript} = {} do 19 | local children = script:GetChildren() 20 | 21 | for _, child in ipairs(children) do 22 | if child:IsA("ModuleScript") then 23 | table.insert(Benchmarks, child) 24 | end 25 | end 26 | end 27 | 28 | local results: { 29 | [string]: { -- Benchmark 30 | [string]: number -- SignalClass: result 31 | } 32 | } = {} 33 | for _, benchmark in ipairs(Benchmarks) do 34 | local benchmarkName = benchmark.Name 35 | benchmark = require(benchmark) :: (RBXScriptSignal) -> (number) 36 | 37 | local benchmarkResults = {} 38 | results[benchmarkName] = benchmarkResults 39 | 40 | for signalName, signalClass in pairs(SignalTypes) do 41 | task.wait(1) 42 | benchmarkResults[signalName] = benchmark(signalClass.new()) 43 | end 44 | end 45 | 46 | local resultString do 47 | resultString = "Benchmark Results:\n" 48 | 49 | local indentation = {} 50 | 51 | local function getIndent() 52 | return table.concat(indentation) 53 | end 54 | 55 | local function indent() 56 | table.insert(indentation, " ") 57 | end 58 | 59 | local function unindent() 60 | indentation[#indentation] = nil 61 | end 62 | 63 | for benchmark, signalResult in pairs(results) do 64 | indent() 65 | local benchmarkSection = getIndent().. benchmark.. ":\n" 66 | for signalName, result in pairs(signalResult) do 67 | indent() 68 | benchmarkSection ..= getIndent().. signalName.. ": ".. result.. "\n" 69 | unindent() 70 | end 71 | 72 | resultString ..= benchmarkSection 73 | unindent() 74 | end 75 | end 76 | 77 | print(resultString) -------------------------------------------------------------------------------- /src/PlayerScripts/SignalTesting.client.lua: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 2 | local FastSignal = require(ReplicatedStorage.FastSignal) 3 | local GoodSignal = require(ReplicatedStorage.GoodSignal) 4 | 5 | task.wait(5) 6 | 7 | local function EmptyFunction() 8 | -- Empty 9 | end 10 | 11 | local IsDeferred do 12 | IsDeferred = false 13 | 14 | local Event = FastSignal.new() 15 | 16 | task.defer(function() 17 | local connection = Event:Connect(function() 18 | IsDeferred = true 19 | end) 20 | 21 | Event:Connect(function() 22 | connection:Disconnect() 23 | end) 24 | 25 | Event:Fire() 26 | end) 27 | 28 | Event:Wait() 29 | Event:Destroy() 30 | 31 | warn( 32 | "Is Signal Deferred: ".. ( 33 | IsDeferred 34 | and "Yes" 35 | or "No" 36 | ) 37 | ) 38 | end 39 | 40 | local IsReverseOrder do 41 | IsReverseOrder = true 42 | 43 | local Event = FastSignal.new() 44 | local firstConnectionFired = false 45 | 46 | task.defer(function() 47 | Event:Connect(function() 48 | firstConnectionFired = true 49 | end) 50 | 51 | Event:Connect(function() 52 | if firstConnectionFired then 53 | IsReverseOrder = false 54 | end 55 | end) 56 | 57 | Event:Fire() 58 | end) 59 | 60 | Event:Wait() 61 | Event:Destroy() 62 | 63 | warn( 64 | "Is Connect Order Reverse: ".. ( 65 | IsReverseOrder 66 | and "Yes" 67 | or "No" 68 | ) 69 | ) 70 | end 71 | 72 | local DisconnectTest do 73 | local Event = FastSignal.new() 74 | local connection = Event:Connect(EmptyFunction) 75 | 76 | connection:Disconnect() 77 | 78 | warn( 79 | "Does :Disconnect disconnect a connection properly: ".. ( 80 | connection.Connected == false and connection.__node == nil and Event._head == nil 81 | and "Yes" 82 | or "No" 83 | ) 84 | ) 85 | end 86 | 87 | local DestroyTest do 88 | local Event = FastSignal.new() 89 | local connection = Event:Connect(EmptyFunction) 90 | 91 | Event:Destroy() 92 | 93 | warn( 94 | "Does :Destroy disconnect connections properly: ".. ( 95 | connection.Connected == false and connection._node == nil and Event._head == nil 96 | and "Yes" 97 | or "No" 98 | ) 99 | ) 100 | 101 | warn( 102 | "Does :Destroy make Signal not connect future connections: ".. ( 103 | Event:Connect(EmptyFunction).Connected == false 104 | and "Yes" 105 | or "No" 106 | ) 107 | ) 108 | end 109 | 110 | local ErrorTest do 111 | warn("Error testing:") 112 | 113 | local BindableEvent = Instance.new("BindableEvent") 114 | BindableEvent.Event:Connect(function() 115 | error("FastSignal stacktrace test") 116 | end) 117 | 118 | BindableEvent:Fire() 119 | 120 | local Event = FastSignal.new() 121 | Event:Connect(function() 122 | error("FastSignal stacktrace test") 123 | end) 124 | 125 | Event:Fire() 126 | end -------------------------------------------------------------------------------- /src/PlayerScripts/TypeTesting.client.lua: -------------------------------------------------------------------------------- 1 | --!strict 2 | 3 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 4 | local ScriptSignal = require(ReplicatedStorage.FastSignal.Immediate) 5 | 6 | local AutomaticType do 7 | local Event: ScriptSignal.ScriptSignal<{number}> = ScriptSignal.new() 8 | 9 | Event:Connect(function(array) 10 | array[1] ..= "what" 11 | array[2] += 5 12 | end) 13 | end 14 | 15 | local GenericTypes do 16 | local Event: ScriptSignal.ScriptSignal<{ 17 | Member1: string, 18 | Member2: number 19 | }> = ScriptSignal.new() 20 | 21 | -- Roblox LSP seems to complain, IG they don't support generic types? 22 | Event:Connect(function(info) 23 | info.Member1 += 10 -- Should complain 24 | info.Member2 ..= "what" -- Should complain 25 | end) 26 | end -------------------------------------------------------------------------------- /src/ReplicatedStorage/FastSignal/Deferred.lua: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!native 3 | 4 | export type ScriptSignal = { 5 | IsActive: (self: ScriptSignal) -> boolean, 6 | Fire: (self: ScriptSignal, T...) -> (), 7 | Connect: (self: ScriptSignal, callback: (T...) -> ()) -> ScriptConnection, 8 | Once: (self: ScriptSignal, callback: (T...) -> ()) -> ScriptConnection, 9 | DisconnectAll: (self: ScriptSignal) -> (), 10 | Destroy: (self: ScriptSignal) -> (), 11 | Wait: (self: ScriptSignal) -> T..., 12 | } 13 | export type ScriptConnection = { 14 | Disconnect: (self: ScriptConnection) -> (), 15 | Connected: boolean, 16 | } 17 | 18 | -- Legacy type. Do not use in newer work. 19 | export type Class = ScriptSignal<...any> 20 | 21 | local ScriptSignal = {} 22 | ScriptSignal.__index = ScriptSignal 23 | 24 | local ScriptConnection = {} 25 | ScriptConnection.__index = ScriptConnection 26 | 27 | function ScriptSignal.new() 28 | return setmetatable({ 29 | _active = true, 30 | _head = nil 31 | }, ScriptSignal) 32 | end 33 | 34 | function ScriptSignal.Is(object) 35 | return typeof(object) == 'table' 36 | and getmetatable(object) == ScriptSignal 37 | end 38 | 39 | function ScriptSignal:IsActive() 40 | return self._active == true 41 | end 42 | 43 | function ScriptSignal:Connect(handler) 44 | assert( 45 | typeof(handler) == 'function', 46 | "Must be function" 47 | ) 48 | 49 | if self._active ~= true then 50 | return setmetatable({ 51 | Connected = false, 52 | _node = nil 53 | }, ScriptConnection) 54 | end 55 | 56 | local _head = self._head 57 | 58 | local node = { 59 | _signal = self, 60 | _connection = nil, 61 | _handler = handler, 62 | 63 | _next = _head, 64 | _prev = nil 65 | } 66 | 67 | if _head ~= nil then 68 | _head._prev = node 69 | end 70 | 71 | self._head = node 72 | 73 | local connection = setmetatable({ 74 | Connected = true, 75 | _node = node 76 | }, ScriptConnection) 77 | 78 | node._connection = connection 79 | 80 | return connection 81 | end 82 | 83 | function ScriptSignal:Once(handler) 84 | assert( 85 | typeof(handler) == 'function', 86 | "Must be function" 87 | ) 88 | 89 | local connection 90 | connection = self:Connect(function(...) 91 | if connection == nil then 92 | return 93 | end 94 | 95 | connection:Disconnect() 96 | connection = nil 97 | 98 | handler(...) 99 | end) 100 | 101 | return connection 102 | end 103 | ScriptSignal.ConnectOnce = ScriptSignal.Once 104 | 105 | function ScriptSignal:Wait() 106 | local thread do 107 | thread = coroutine.running() 108 | 109 | local connection 110 | connection = self:Connect(function(...) 111 | if connection == nil then 112 | return 113 | end 114 | 115 | connection:Disconnect() 116 | connection = nil 117 | if coroutine.status(thread) == "suspended" then 118 | task.spawn(thread, ...) 119 | end 120 | end) 121 | end 122 | 123 | return coroutine.yield() 124 | end 125 | 126 | function ScriptSignal:Fire(...) 127 | local node = self._head 128 | while node ~= nil do 129 | task.defer(node._handler, ...) 130 | 131 | node = node._next 132 | end 133 | end 134 | 135 | function ScriptSignal:DisconnectAll() 136 | local node = self._head 137 | while node ~= nil do 138 | local _connection = node._connection 139 | 140 | if _connection ~= nil then 141 | _connection.Connected = false 142 | _connection._node = nil 143 | node._connection = nil 144 | end 145 | 146 | node = node._next 147 | end 148 | 149 | self._head = nil 150 | end 151 | 152 | function ScriptSignal:Destroy() 153 | if self._active ~= true then 154 | return 155 | end 156 | 157 | self:DisconnectAll() 158 | self._active = false 159 | end 160 | 161 | function ScriptConnection:Disconnect() 162 | if self.Connected ~= true then 163 | return 164 | end 165 | 166 | self.Connected = false 167 | 168 | local _node = self._node 169 | local _prev = _node._prev 170 | local _next = _node._next 171 | 172 | if _next ~= nil then 173 | _next._prev = _prev 174 | end 175 | 176 | if _prev ~= nil then 177 | _prev._next = _next 178 | else 179 | -- _node == _signal._head 180 | 181 | _node._signal._head = _next 182 | end 183 | 184 | _node._connection = nil 185 | self._node = nil 186 | end 187 | ScriptConnection.Destroy = ScriptConnection.Disconnect 188 | 189 | return ScriptSignal :: typeof( require(script.Parent.Docs) ) -------------------------------------------------------------------------------- /src/ReplicatedStorage/FastSignal/Docs.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Meant to hold docs. Makes it easier to mess with them individually. 3 | ]] 4 | 5 | if true then 6 | error("This is not supposed to run!") 7 | end 8 | 9 | export type ScriptSignal = { 10 | IsActive: (self: ScriptSignal) -> boolean, 11 | Fire: (self: ScriptSignal, T...) -> (), 12 | Connect: (self: ScriptSignal, callback: (T...) -> ()) -> ScriptConnection, 13 | Once: (self: ScriptSignal, callback: (T...) -> ()) -> ScriptConnection, 14 | DisconnectAll: (self: ScriptSignal) -> (), 15 | Destroy: (self: ScriptSignal) -> (), 16 | Wait: (self: ScriptSignal) -> T..., 17 | } 18 | export type ScriptConnection = { 19 | Disconnect: (self: ScriptConnection) -> (), 20 | Connected: boolean, 21 | } 22 | 23 | -- Legacy type. Do not use in newer work. 24 | export type Class = ScriptSignal<...any> 25 | 26 | -- Methods: 27 | 28 | --[=[ 29 | A class which holds data and methods for ScriptSignals. 30 | 31 | @class ScriptSignal 32 | ]=] 33 | local ScriptSignal = {} 34 | ScriptSignal.__index = ScriptSignal 35 | 36 | --[=[ 37 | A class which holds data and methods for ScriptConnections. 38 | 39 | @class ScriptConnection 40 | ]=] 41 | local ScriptConnection = {} 42 | ScriptConnection.__index = ScriptConnection 43 | 44 | --[=[ 45 | A boolean which determines if a ScriptConnection is active or not. 46 | 47 | @prop Connected boolean 48 | @within ScriptConnection 49 | 50 | @readonly 51 | ]=] 52 | 53 | --[=[ 54 | Creates a ScriptSignal object. 55 | 56 | @return ScriptSignal 57 | ]=] 58 | function ScriptSignal.new() 59 | return {} 60 | end 61 | 62 | --[=[ 63 | Returns a boolean determining if the object is a ScriptSignal. 64 | 65 | ```lua 66 | local janitor = Janitor.new() 67 | local signal = ScriptSignal.new() 68 | 69 | ScriptSignal.Is(signal) -> true 70 | ScriptSignal.Is(janitor) -> false 71 | ``` 72 | 73 | @param object any 74 | @return boolean 75 | ]=] 76 | function ScriptSignal.Is(object) 77 | return true 78 | end 79 | 80 | --[=[ 81 | Returns a boolean which determines if a ScriptSignal object is active. 82 | 83 | ```lua 84 | ScriptSignal:IsActive() -> true 85 | ScriptSignal:Destroy() 86 | ScriptSignal:IsActive() -> false 87 | ``` 88 | 89 | @return boolean 90 | ]=] 91 | function ScriptSignal:IsActive() 92 | return true 93 | end 94 | 95 | --[=[ 96 | Connects a handler to a ScriptSignal object. 97 | 98 | ```lua 99 | ScriptSignal:Connect(function(text) 100 | print(text) 101 | end) 102 | 103 | ScriptSignal:Fire("Something") 104 | ScriptSignal:Fire("Something else") 105 | 106 | -- "Something" and then "Something else" are printed 107 | ``` 108 | 109 | @param handler (...: any) -> () 110 | @return ScriptConnection 111 | ]=] 112 | function ScriptSignal:Connect(handler) 113 | 114 | end 115 | 116 | --[=[ 117 | Connects a handler to a ScriptSignal object, but only allows that 118 | connection to run once. Any `:Fire` calls called afterwards won't trigger anything. 119 | 120 | ```lua 121 | ScriptSignal:Once(function() 122 | print("Connection fired") 123 | end) 124 | 125 | ScriptSignal:Fire() 126 | ScriptSignal:Fire() 127 | 128 | -- "Connection fired" is only fired once 129 | ``` 130 | 131 | @param handler (...: any) -> () 132 | @return ScriptConnection 133 | ]=] 134 | function ScriptSignal:Once(handler) 135 | 136 | end 137 | 138 | --[=[ 139 | Yields the thread until a `:Fire` call occurs, returns what the signal was fired with. 140 | 141 | ```lua 142 | task.spawn(function() 143 | print( 144 | ScriptSignal:Wait() 145 | ) 146 | end) 147 | 148 | ScriptSignal:Fire("Arg", nil, 1, 2, 3, nil) 149 | -- "Arg", nil, 1, 2, 3, nil are printed 150 | ``` 151 | 152 | @yields 153 | @return ...any 154 | ]=] 155 | function ScriptSignal:Wait() 156 | 157 | end 158 | 159 | --[=[ 160 | Fires a ScriptSignal object with the arguments passed. 161 | 162 | ```lua 163 | ScriptSignal:Connect(function(text) 164 | print(text) 165 | end) 166 | 167 | ScriptSignal:Fire("Some Text...") 168 | 169 | -- "Some Text..." is printed twice 170 | ``` 171 | 172 | @param ... any 173 | ]=] 174 | function ScriptSignal:Fire(...) 175 | 176 | end 177 | 178 | --[=[ 179 | Disconnects all connections from a ScriptSignal object without making it unusable. 180 | 181 | ```lua 182 | local connection = ScriptSignal:Connect(function() end) 183 | 184 | connection.Connected -> true 185 | ScriptSignal:DisconnectAll() 186 | connection.Connected -> false 187 | ``` 188 | ]=] 189 | function ScriptSignal:DisconnectAll() 190 | 191 | end 192 | 193 | --[=[ 194 | Destroys a ScriptSignal object, disconnecting all connections and making it unusable. 195 | 196 | ```lua 197 | ScriptSignal:Destroy() 198 | 199 | local connection = ScriptSignal:Connect(function() end) 200 | connection.Connected -> false 201 | ``` 202 | ]=] 203 | function ScriptSignal:Destroy() 204 | 205 | end 206 | 207 | --[=[ 208 | Disconnects a connection, any `:Fire` calls from now on will not 209 | invoke this connection's handler. 210 | 211 | ```lua 212 | local connection = ScriptSignal:Connect(function() end) 213 | 214 | connection.Connected -> true 215 | connection:Disconnect() 216 | connection.Connected -> false 217 | ``` 218 | ]=] 219 | function ScriptConnection:Disconnect() 220 | 221 | end 222 | 223 | -- Stricter type 224 | local returnType = {} 225 | 226 | function returnType.new(): ScriptSignal 227 | return ScriptSignal.new() 228 | end 229 | 230 | function returnType.Is(any): boolean 231 | return true 232 | end 233 | 234 | return returnType -------------------------------------------------------------------------------- /src/ReplicatedStorage/FastSignal/Immediate.lua: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!native 3 | 4 | export type ScriptSignal = { 5 | IsActive: (self: ScriptSignal) -> boolean, 6 | Fire: (self: ScriptSignal, T...) -> (), 7 | Connect: (self: ScriptSignal, callback: (T...) -> ()) -> ScriptConnection, 8 | Once: (self: ScriptSignal, callback: (T...) -> ()) -> ScriptConnection, 9 | DisconnectAll: (self: ScriptSignal) -> (), 10 | Destroy: (self: ScriptSignal) -> (), 11 | Wait: (self: ScriptSignal) -> T..., 12 | } 13 | export type ScriptConnection = { 14 | Disconnect: (self: ScriptConnection) -> (), 15 | Connected: boolean, 16 | } 17 | 18 | -- Legacy type. Do not use in newer work. 19 | export type Class = ScriptSignal<...any> 20 | 21 | local MainScriptSignal = require(script.Parent.Deferred) 22 | 23 | local ScriptSignal = {} do 24 | for methodName, method in pairs(MainScriptSignal) do 25 | ScriptSignal[methodName] = method 26 | end 27 | ScriptSignal.__index = ScriptSignal 28 | end 29 | 30 | local FreeThread: thread? = nil 31 | local function RunHandlerInFreeThread(handler, ...) 32 | local thread = FreeThread :: thread 33 | FreeThread = nil 34 | 35 | handler(...) 36 | 37 | FreeThread = thread 38 | end 39 | 40 | local function CreateFreeThread() 41 | FreeThread = coroutine.running() 42 | 43 | while true do 44 | RunHandlerInFreeThread( coroutine.yield() ) 45 | end 46 | end 47 | 48 | function ScriptSignal.new() 49 | return setmetatable({ 50 | _active = true, 51 | _head = nil 52 | }, ScriptSignal) 53 | end 54 | 55 | function ScriptSignal.Is(object) 56 | return typeof(object) == 'table' 57 | and getmetatable(object) == ScriptSignal 58 | end 59 | 60 | function ScriptSignal:Fire(...) 61 | local node = self._head 62 | while node ~= nil do 63 | if node._connection ~= nil then 64 | if FreeThread == nil then 65 | task.spawn(CreateFreeThread) 66 | end 67 | 68 | task.spawn( 69 | FreeThread :: thread, 70 | node._handler, ... 71 | ) 72 | end 73 | 74 | node = node._next 75 | end 76 | end 77 | 78 | return ScriptSignal :: typeof( require(script.Parent.Docs) ) -------------------------------------------------------------------------------- /src/ReplicatedStorage/FastSignal/Readme.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | FastSignal has 3 editions. 3 | 4 | * Adaptive 5 | * Deferred 6 | * Immediate 7 | 8 | Deferred FastSignal as you might have guessed, runs in deferred mode. 9 | It uses task.defer, and don't recycle threads, however they are 10 | more consistent with RBXScriptSignal behavior in the future. 11 | 12 | Immediate signals are immediate, they use task.spawn, behavior 13 | is a bit more undefined, and they recycle threads, this is what 14 | live roblox games currently can only use. 15 | 16 | Adaptive fixes the deciding, realistically you should have deferred 17 | event behavior enabled on your game, with code working both in deferred, 18 | and immediate mode, if you can't, that usually means you have structural problems. 19 | 20 | Adaptive works by detecting which mode your game is currently using, 21 | and then adapting to that. 22 | 23 | Mixing signal behavior is something extremely annoying to deal with, 24 | as order can't be relied upon anymore, so I highly recommend you stay 25 | away from having systems use both deferred and immediate, and staying with one. 26 | Preferably, use Deferred behavior. 27 | ]] 28 | 29 | return error("Not meant to run.") -------------------------------------------------------------------------------- /src/ReplicatedStorage/FastSignal/init.lua: -------------------------------------------------------------------------------- 1 | --!nocheck 2 | --!optimize 2 3 | --!native 4 | 5 | --[[ 6 | This script deals with typing and automatic choosing of the right variant depending on what your experience is currently running. 7 | ]] 8 | 9 | local IsDeferred: boolean do 10 | IsDeferred = false 11 | 12 | local bindable = Instance.new("BindableEvent") 13 | 14 | local handlerRun = false 15 | bindable.Event:Connect(function() 16 | handlerRun = true 17 | end) 18 | 19 | bindable:Fire() 20 | bindable:Destroy() 21 | 22 | if handlerRun == false then 23 | -- In Deferred mode, things run "later", we can take advantage of this to detect the mode active, 24 | -- by checking whether a :Fire call manages to change a variable right away, we are able to detect 25 | -- whether Immediate or Deferred mode is being used. 26 | 27 | IsDeferred = true 28 | end 29 | end 30 | 31 | -- These were copied and modified from sleitnick's fork of GoodSignal, thanks sleitnick! 32 | export type ScriptSignal = { 33 | IsActive: (self: ScriptSignal) -> boolean, 34 | Fire: (self: ScriptSignal, T...) -> (), 35 | Connect: (self: ScriptSignal, callback: (T...) -> ()) -> ScriptConnection, 36 | Once: (self: ScriptSignal, callback: (T...) -> ()) -> ScriptConnection, 37 | DisconnectAll: (self: ScriptSignal) -> (), 38 | Destroy: (self: ScriptSignal) -> (), 39 | Wait: (self: ScriptSignal) -> T..., 40 | } 41 | export type ScriptConnection = { 42 | Disconnect: (self: ScriptConnection) -> (), 43 | Connected: boolean, 44 | } 45 | 46 | -- Legacy type. Do not use in newer work. 47 | export type Class = ScriptSignal<...any> 48 | 49 | local ChosenSignal: typeof( require(script.Docs) ) = IsDeferred 50 | and require(script.Deferred) 51 | or require(script.Immediate) 52 | 53 | return ChosenSignal -------------------------------------------------------------------------------- /src/ReplicatedStorage/FastSignal/wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "lucasmzreal/fastsignal" 7 | version = "10.1.0" 8 | dependencies = [] 9 | -------------------------------------------------------------------------------- /src/ReplicatedStorage/FastSignal/wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "lucasmzreal/fastsignal" 4 | description = "FastSignal is a signal library made with consistency and expectable behaviour in mind, it is efficient, easy to use, and widely compatible." 5 | version = "10.4.0" 6 | license = "MIT" 7 | authors = ["RBLXUtils", "LucasMZReal"] 8 | 9 | registry = "https://github.com/UpliftGames/wally-index" 10 | realm = "shared" -------------------------------------------------------------------------------- /src/ReplicatedStorage/GoodSignal.lua: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!native 3 | 4 | -- Licensed under the MIT License by stravant 5 | -- Slight modifications by LucasMZ_RBX for compatibility with Benchmarking code 6 | 7 | -- The currently idle thread to run the next handler on 8 | local freeRunnerThread = nil 9 | 10 | -- Function which acquires the currently idle handler runner thread, runs the 11 | -- function fn on it, and then releases the thread, returning it to being the 12 | -- currently idle one. 13 | -- If there was a currently idle runner thread already, that's okay, that old 14 | -- one will just get thrown and eventually GCed. 15 | local function acquireRunnerThreadAndCallEventHandler(fn, ...) 16 | local acquiredRunnerThread = freeRunnerThread 17 | freeRunnerThread = nil 18 | fn(...) 19 | -- The handler finished running, this runner thread is free again. 20 | freeRunnerThread = acquiredRunnerThread 21 | end 22 | 23 | -- Coroutine runner that we create coroutines of. The coroutine can be 24 | -- repeatedly resumed with functions to run followed by the argument to run 25 | -- them with. 26 | local function runEventHandlerInFreeThread(...) 27 | acquireRunnerThreadAndCallEventHandler(...) 28 | while true do 29 | acquireRunnerThreadAndCallEventHandler(coroutine.yield()) 30 | end 31 | end 32 | 33 | -- Connection class 34 | local Connection = {} 35 | Connection.__index = Connection 36 | 37 | function Connection.new(signal, fn) 38 | return setmetatable({ 39 | Connected = true, 40 | _signal = signal, 41 | _fn = fn, 42 | _next = false, 43 | }, Connection) 44 | end 45 | 46 | function Connection:Disconnect() 47 | assert(self.Connected, "Can't disconnect a connection twice.") 48 | self.Connected = false 49 | 50 | -- Unhook the node, but DON'T clear it. That way any fire calls that are 51 | -- currently sitting on this node will be able to iterate forwards off of 52 | -- it, but any subsequent fire calls will not hit it, and it will be GCed 53 | -- when no more fire calls are sitting on it. 54 | if self._signal._handlerListHead == self then 55 | self._signal._handlerListHead = self._next 56 | else 57 | local prev = self._signal._handlerListHead 58 | while prev and prev._next ~= self do 59 | prev = prev._next 60 | end 61 | if prev then 62 | prev._next = self._next 63 | end 64 | end 65 | end 66 | 67 | -- Signal class 68 | local Signal = {} 69 | Signal.__index = Signal 70 | 71 | function Signal.new() 72 | return setmetatable({ 73 | _handlerListHead = false, 74 | }, Signal) 75 | end 76 | 77 | function Signal:Connect(fn) 78 | local connection = Connection.new(self, fn) 79 | if self._handlerListHead then 80 | connection._next = self._handlerListHead 81 | self._handlerListHead = connection 82 | else 83 | self._handlerListHead = connection 84 | end 85 | return connection 86 | end 87 | 88 | -- Disconnect all handlers. Since we use a linked list it suffices to clear the 89 | -- reference to the head handler. 90 | function Signal:DisconnectAll() 91 | local node = self._handlerListHead 92 | while node do 93 | node:Disconnect() 94 | 95 | node = node._next 96 | end 97 | end 98 | 99 | -- Signal:Fire(...) implemented by running the handler functions on the 100 | -- coRunnerThread, and any time the resulting thread yielded without returning 101 | -- to us, that means that it yielded to the Roblox scheduler and has been taken 102 | -- over by Roblox scheduling, meaning we have to make a new coroutine runner. 103 | function Signal:Fire(...) 104 | local item = self._handlerListHead 105 | while item do 106 | if item.Connected then 107 | if not freeRunnerThread then 108 | freeRunnerThread = coroutine.create(runEventHandlerInFreeThread) 109 | end 110 | 111 | task.spawn(freeRunnerThread, item._fn, ...) 112 | end 113 | item = item._next 114 | end 115 | end 116 | 117 | -- Implement Signal:Wait() in terms of a temporary connection using 118 | -- a Signal:Connect() which disconnects itself. 119 | function Signal:Wait() 120 | local waitingCoroutine = coroutine.running() 121 | local cn; 122 | cn = self:Connect(function(...) 123 | cn:Disconnect() 124 | task.spawn(waitingCoroutine, ...) 125 | end) 126 | return coroutine.yield() 127 | end 128 | 129 | return Signal -------------------------------------------------------------------------------- /src/ReplicatedStorage/LemonSignal.lua: -------------------------------------------------------------------------------- 1 | --!optimize 2 2 | --!nocheck 3 | --!native 4 | 5 | export type Connection = { 6 | Connected: boolean, 7 | 8 | Disconnect: (self: Connection) -> (), 9 | Reconnect: (self: Connection) -> (), 10 | } 11 | 12 | export type Signal = { 13 | RBXScriptConnection: RBXScriptConnection?, 14 | 15 | Connect: (self: Signal, fn: (...any) -> (), U...) -> Connection, 16 | Once: (self: Signal, fn: (...any) -> (), U...) -> Connection, 17 | Wait: (self: Signal) -> T..., 18 | Fire: (self: Signal, T...) -> (), 19 | DisconnectAll: (self: Signal) -> (), 20 | Destroy: (self: Signal) -> (), 21 | } 22 | 23 | local freeThreads: { thread } = {} 24 | 25 | local function runCallback(callback, thread, ...) 26 | callback(...) 27 | table.insert(freeThreads, thread) 28 | end 29 | 30 | local function yielder() 31 | while true do 32 | runCallback(coroutine.yield()) 33 | end 34 | end 35 | 36 | local Connection = {} 37 | Connection.__index = Connection 38 | 39 | local function disconnect(self: Connection) 40 | if not self.Connected then 41 | return 42 | end 43 | self.Connected = false 44 | 45 | local next = self._next 46 | local prev = self._prev 47 | 48 | if next then 49 | next._prev = prev 50 | end 51 | if prev then 52 | prev._next = next 53 | end 54 | 55 | local signal = self._signal 56 | if signal._head == self then 57 | signal._head = next 58 | end 59 | end 60 | 61 | local function reconnect(self: Connection) 62 | if self.Connected then 63 | return 64 | end 65 | self.Connected = true 66 | 67 | local signal = self._signal 68 | local head = signal._head 69 | if head then 70 | head._prev = self 71 | end 72 | signal._head = self 73 | 74 | self._next = head 75 | self._prev = false 76 | end 77 | 78 | Connection.Disconnect = disconnect 79 | Connection.Reconnect = reconnect 80 | 81 | --\\ Signal //-- 82 | local Signal = {} 83 | Signal.__index = Signal 84 | 85 | -- stylua: ignore 86 | local rbxConnect, rbxDisconnect do 87 | if task then 88 | local bindable = Instance.new("BindableEvent") 89 | rbxConnect = bindable.Event.Connect 90 | rbxDisconnect = bindable.Event:Connect(function() end).Disconnect 91 | bindable:Destroy() 92 | end 93 | end 94 | 95 | local function connect(self: Signal, fn: (...any) -> (), ...: U...): Connection 96 | local head = self._head 97 | local cn = setmetatable({ 98 | Connected = true, 99 | _signal = self, 100 | _fn = fn, 101 | _varargs = if not ... then false else { ... }, 102 | _next = head, 103 | _prev = false, 104 | }, Connection) 105 | 106 | if head then 107 | head._prev = cn 108 | end 109 | self._head = cn 110 | 111 | return cn 112 | end 113 | 114 | local function once(self: Signal, fn: (...any) -> (), ...: U...) 115 | local cn 116 | cn = connect(self, function(...) 117 | disconnect(cn) 118 | fn(...) 119 | end, ...) 120 | return cn 121 | end 122 | 123 | local wait = if task 124 | then function(self: Signal): ...any 125 | local thread = coroutine.running() 126 | local cn 127 | cn = connect(self, function(...) 128 | disconnect(cn) 129 | task.spawn(thread, ...) 130 | end) 131 | return coroutine.yield() 132 | end 133 | else function(self: Signal): ...any 134 | local thread = coroutine.running() 135 | local cn 136 | cn = connect(self, function(...) 137 | disconnect(cn) 138 | local passed, message = coroutine.resume(thread, ...) 139 | if not passed then 140 | error(message, 0) 141 | end 142 | end) 143 | return coroutine.yield() 144 | end 145 | 146 | local fire = if task 147 | then function(self: Signal, ...: any) 148 | local cn = self._head 149 | while cn do 150 | local thread 151 | if #freeThreads > 0 then 152 | thread = freeThreads[#freeThreads] 153 | freeThreads[#freeThreads] = nil 154 | else 155 | thread = coroutine.create(yielder) 156 | coroutine.resume(thread) 157 | end 158 | 159 | if not cn._varargs then 160 | task.spawn(thread, cn._fn, thread, ...) 161 | else 162 | local args = cn._varargs 163 | local len = #args 164 | local count = len 165 | for _, value in { ... } do 166 | count += 1 167 | args[count] = value 168 | end 169 | 170 | task.spawn(thread, cn._fn, thread, table.unpack(args)) 171 | 172 | for i = count, len + 1, -1 do 173 | args[i] = nil 174 | end 175 | end 176 | 177 | cn = cn._next 178 | end 179 | end 180 | else function(self: Signal, ...: any) 181 | local cn = self._head 182 | while cn do 183 | local thread 184 | if #freeThreads > 0 then 185 | thread = freeThreads[#freeThreads] 186 | freeThreads[#freeThreads] = nil 187 | else 188 | thread = coroutine.create(yielder) 189 | coroutine.resume(thread) 190 | end 191 | 192 | if not cn._varargs then 193 | local passed, message = coroutine.resume(thread, cn._fn, thread, ...) 194 | if not passed then 195 | print(string.format("%s\nstacktrace:\n%s", message, debug.traceback())) 196 | end 197 | else 198 | local args = cn._varargs 199 | local len = #args 200 | local count = len 201 | for _, value in { ... } do 202 | count += 1 203 | args[count] = value 204 | end 205 | 206 | local passed, message = coroutine.resume(thread, cn._fn, thread, table.unpack(args)) 207 | if not passed then 208 | print(string.format("%s\nstacktrace:\n%s", message, debug.traceback())) 209 | end 210 | 211 | for i = count, len + 1, -1 do 212 | args[i] = nil 213 | end 214 | end 215 | 216 | cn = cn._next 217 | end 218 | end 219 | 220 | local function disconnectAll(self: Signal) 221 | local cn = self._head 222 | while cn do 223 | disconnect(cn) 224 | cn = cn._next 225 | end 226 | end 227 | 228 | local function destroy(self: Signal) 229 | disconnectAll(self) 230 | local cn = self.RBXScriptConnection 231 | if cn then 232 | rbxDisconnect(cn) 233 | self.RBXScriptConnection = nil 234 | end 235 | end 236 | 237 | --\\ Constructors 238 | function Signal.new(): Signal 239 | return setmetatable({ _head = false }, Signal) 240 | end 241 | 242 | function Signal.wrap(signal: RBXScriptSignal): Signal 243 | local wrapper = setmetatable({ _head = false }, Signal) 244 | wrapper.RBXScriptConnection = rbxConnect(signal, function(...) 245 | fire(wrapper, ...) 246 | end) 247 | return wrapper 248 | end 249 | 250 | --\\ Methods 251 | Signal.Connect = connect 252 | Signal.Once = once 253 | Signal.Wait = wait 254 | Signal.Fire = fire 255 | Signal.DisconnectAll = disconnectAll 256 | Signal.Destroy = destroy 257 | 258 | return { new = Signal.new, wrap = Signal.wrap } 259 | -------------------------------------------------------------------------------- /src/ReplicatedStorage/RBXScriptSignal.lua: -------------------------------------------------------------------------------- 1 | -- Simple wrapper for RBXScriptSignals. Not recommended for use 2 | -- Does not include fixes for firing args etc 3 | -- Only exists for compatibility with benchmarking script 4 | 5 | local Signal = {} 6 | Signal.__index = Signal 7 | 8 | function Signal.new() 9 | return setmetatable({ 10 | _bindable = Instance.new("BindableEvent") 11 | }, Signal) 12 | end 13 | 14 | function Signal:Connect(callback) 15 | return self._bindable.Event:Connect(callback) 16 | end 17 | 18 | function Signal:Wait() 19 | return self._bindable.Event:Wait() 20 | end 21 | 22 | function Signal:Fire(...) 23 | self._bindable:Fire(...) 24 | end 25 | 26 | function Signal:Destroy() 27 | self._bindable:Destroy() 28 | end 29 | 30 | return Signal --------------------------------------------------------------------------------