@quenty/signal 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [2.4.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/signal@2.3.0...@quenty/signal@2.4.0) (2023-05-26)
7
+
8
+
9
+ ### Features
10
+
11
+ * Initial refactor of guis to use ValueObject instead of ValueObject ([723aba0](https://github.com/Quenty/NevermoreEngine/commit/723aba0208cae7e06c9d8bf2d8f0092d042d70ea))
12
+
13
+
14
+
15
+
16
+
6
17
  # [2.3.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/signal@2.2.0...@quenty/signal@2.3.0) (2022-08-22)
7
18
 
8
19
 
package/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2014-2022 Quenty
3
+ Copyright (c) 2014-2023 James Onnen (Quenty)
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/signal",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "A simple signal implementation for Roblox",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -29,5 +29,5 @@
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
- "gitHead": "4efdac5a9e3433adc7903782e233d805dba568ac"
32
+ "gitHead": "11058e90e51ea83d3dad6ae9abe59cc19c36b94b"
33
33
  }
@@ -0,0 +1,184 @@
1
+ --------------------------------------------------------------------------------
2
+ -- Batched Yield-Safe Signal Implementation --
3
+ -- This is a Signal class which has effectively identical behavior to a --
4
+ -- normal RBXScriptSignal, with the only difference being a couple extra --
5
+ -- stack frames at the bottom of the stack trace when an error is thrown. --
6
+ -- This implementation caches runner coroutines, so the ability to yield in --
7
+ -- the signal handlers comes at minimal extra cost over a naive signal --
8
+ -- implementation that either always or never spawns a thread. --
9
+ -- --
10
+ -- API: --
11
+ -- local Signal = require(THIS MODULE) --
12
+ -- local sig = Signal.new() --
13
+ -- local connection = sig:Connect(function(arg1, arg2, ...) ... end) --
14
+ -- sig:Fire(arg1, arg2, ...) --
15
+ -- connection:Disconnect() --
16
+ -- sig:DisconnectAll() --
17
+ -- local arg1, arg2, ... = sig:Wait() --
18
+ -- --
19
+ -- Licence: --
20
+ -- Licenced under the MIT licence. --
21
+ -- --
22
+ -- Authors: --
23
+ -- stravant - July 31st, 2021 - Created the file. --
24
+ --------------------------------------------------------------------------------
25
+
26
+ -- The currently idle thread to run the next handler on
27
+ local freeRunnerThread = nil
28
+
29
+ -- Function which acquires the currently idle handler runner thread, runs the
30
+ -- function fn on it, and then releases the thread, returning it to being the
31
+ -- currently idle one.
32
+ -- If there was a currently idle runner thread already, that's okay, that old
33
+ -- one will just get thrown and eventually GCed.
34
+ local function acquireRunnerThreadAndCallEventHandler(fn, ...)
35
+ local acquiredRunnerThread = freeRunnerThread
36
+ freeRunnerThread = nil
37
+ fn(...)
38
+ -- The handler finished running, this runner thread is free again.
39
+ freeRunnerThread = acquiredRunnerThread
40
+ end
41
+
42
+ -- Coroutine runner that we create coroutines of. The coroutine can be
43
+ -- repeatedly resumed with functions to run followed by the argument to run
44
+ -- them with.
45
+ local function runEventHandlerInFreeThread()
46
+ -- Note: We cannot use the initial set of arguments passed to
47
+ -- runEventHandlerInFreeThread for a call to the handler, because those
48
+ -- arguments would stay on the stack for the duration of the thread's
49
+ -- existence, temporarily leaking references. Without access to raw bytecode
50
+ -- there's no way for us to clear the "..." references from the stack.
51
+ while true do
52
+ acquireRunnerThreadAndCallEventHandler(coroutine.yield())
53
+ end
54
+ end
55
+
56
+ -- Connection class
57
+ local Connection = {}
58
+ Connection.__index = Connection
59
+
60
+ function Connection.new(signal, fn)
61
+ return setmetatable({
62
+ _connected = true,
63
+ _signal = signal,
64
+ _fn = fn,
65
+ _next = false,
66
+ }, Connection)
67
+ end
68
+
69
+ function Connection:Disconnect()
70
+ self._connected = false
71
+
72
+ -- Unhook the node, but DON'T clear it. That way any fire calls that are
73
+ -- currently sitting on this node will be able to iterate forwards off of
74
+ -- it, but any subsequent fire calls will not hit it, and it will be GCed
75
+ -- when no more fire calls are sitting on it.
76
+ if self._signal._handlerListHead == self then
77
+ self._signal._handlerListHead = self._next
78
+ else
79
+ local prev = self._signal._handlerListHead
80
+ while prev and prev._next ~= self do
81
+ prev = prev._next
82
+ end
83
+ if prev then
84
+ prev._next = self._next
85
+ end
86
+ end
87
+ end
88
+
89
+ Connection.Destroy = Connection.Disconnect
90
+
91
+ -- Make Connection strict
92
+ setmetatable(Connection, {
93
+ __index = function(_, key)
94
+ error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
95
+ end,
96
+ __newindex = function(_, key, _)
97
+ error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
98
+ end
99
+ })
100
+
101
+ -- Signal class
102
+ local Signal = {}
103
+ Signal.__index = Signal
104
+
105
+ function Signal.new()
106
+ return setmetatable({
107
+ _handlerListHead = false,
108
+ }, Signal)
109
+ end
110
+
111
+ function Signal:Connect(fn)
112
+ local connection = Connection.new(self, fn)
113
+ if self._handlerListHead then
114
+ connection._next = self._handlerListHead
115
+ self._handlerListHead = connection
116
+ else
117
+ self._handlerListHead = connection
118
+ end
119
+ return connection
120
+ end
121
+
122
+ -- Disconnect all handlers. Since we use a linked list it suffices to clear the
123
+ -- reference to the head handler.
124
+ function Signal:DisconnectAll()
125
+ self._handlerListHead = false
126
+ end
127
+
128
+ -- Signal:Fire(...) implemented by running the handler functions on the
129
+ -- coRunnerThread, and any time the resulting thread yielded without returning
130
+ -- to us, that means that it yielded to the Roblox scheduler and has been taken
131
+ -- over by Roblox scheduling, meaning we have to make a new coroutine runner.
132
+ function Signal:Fire(...)
133
+ local item = self._handlerListHead
134
+ while item do
135
+ if item._connected then
136
+ if not freeRunnerThread then
137
+ freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
138
+ -- Get the freeRunnerThread to the first yield
139
+ coroutine.resume(freeRunnerThread)
140
+ end
141
+ task.spawn(freeRunnerThread, item._fn, ...)
142
+ end
143
+ item = item._next
144
+ end
145
+ end
146
+
147
+ -- Implement Signal:Wait() in terms of a temporary connection using
148
+ -- a Signal:Connect() which disconnects itself.
149
+ function Signal:Wait()
150
+ local waitingCoroutine = coroutine.running()
151
+ local cn;
152
+ cn = self:Connect(function(...)
153
+ cn:Disconnect()
154
+ task.spawn(waitingCoroutine, ...)
155
+ end)
156
+ return coroutine.yield()
157
+ end
158
+
159
+ -- Implement Signal:Once() in terms of a connection which disconnects
160
+ -- itself before running the handler.
161
+ function Signal:Once(fn)
162
+ local cn;
163
+ cn = self:Connect(function(...)
164
+ if cn._connected then
165
+ cn:Disconnect()
166
+ end
167
+ fn(...)
168
+ end)
169
+ return cn
170
+ end
171
+
172
+ Signal.Destroy = Signal.DisconnectAll
173
+
174
+ -- Make signal strict
175
+ setmetatable(Signal, {
176
+ __index = function(_, key)
177
+ error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
178
+ end,
179
+ __newindex = function(_, key, _)
180
+ error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
181
+ end
182
+ })
183
+
184
+ return Signal