├── .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 |
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
--------------------------------------------------------------------------------