@quenty/signal 7.5.0 → 7.7.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,38 @@
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
+ # [7.7.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/signal@7.6.0...@quenty/signal@7.7.0) (2024-10-04)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * GoodSignal now uses the connection memory category instead of the original items memory category, resulting in even more accurate tracking ([c18353d](https://github.com/Quenty/NevermoreEngine/commit/c18353d61a4b4966ad4025c3b7e58b895dcb16a8))
12
+
13
+
14
+ ### Performance Improvements
15
+
16
+ * Connection clears references, and avoids storing _connected and _next, which reduces memory usage of signal ([8738269](https://github.com/Quenty/NevermoreEngine/commit/8738269c457b8075b89dd18e7371a103413879d6))
17
+
18
+
19
+
20
+
21
+
22
+ # [7.6.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/signal@7.5.0...@quenty/signal@7.6.0) (2024-09-25)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * Selene doesn't know debug.getmemorycategory() ([0c613a3](https://github.com/Quenty/NevermoreEngine/commit/0c613a3ae0b6ba6a4cda511f572220bfa951c70d))
28
+
29
+
30
+ ### Features
31
+
32
+ * MemoryCategory is tracked properly in signal (Not sure what the perf implications of this are) ([4d43b0c](https://github.com/Quenty/NevermoreEngine/commit/4d43b0c0c07fd5d24335b1801ca96c58d37ba149))
33
+
34
+
35
+
36
+
37
+
6
38
  # [7.5.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/signal@7.4.0...@quenty/signal@7.5.0) (2024-09-25)
7
39
 
8
40
  **Note:** Version bump only for package @quenty/signal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/signal",
3
- "version": "7.5.0",
3
+ "version": "7.7.0",
4
4
  "description": "A simple signal implementation for Roblox",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -30,7 +30,7 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@quenty/loader": "^10.5.0"
33
+ "@quenty/loader": "^10.6.0"
34
34
  },
35
- "gitHead": "41715b15e2b48b2d22ff4f5277a8d4b7a0d32ef3"
35
+ "gitHead": "035abfa088c854a73e1c65b350267eaa17669646"
36
36
  }
@@ -44,25 +44,31 @@
44
44
  ]=]
45
45
 
46
46
  -- The currently idle thread to run the next handler on
47
- local freeRunnerThread = nil
47
+ local weakFreeRunnerThreadLookup = setmetatable({}, {__mode = "kv"})
48
48
 
49
49
  -- Function which acquires the currently idle handler runner thread, runs the
50
50
  -- function fn on it, and then releases the thread, returning it to being the
51
51
  -- currently idle one.
52
52
  -- If there was a currently idle runner thread already, that's okay, that old
53
53
  -- one will just get thrown and eventually GCed.
54
- local function acquireRunnerThreadAndCallEventHandler(fn, ...)
55
- local acquiredRunnerThread = freeRunnerThread
56
- freeRunnerThread = nil
54
+ local function acquireRunnerThreadAndCallEventHandler(memoryCategory, fn, ...)
55
+ local acquiredRunnerThread = weakFreeRunnerThreadLookup[memoryCategory]
56
+ weakFreeRunnerThreadLookup[memoryCategory] = nil
57
57
  fn(...)
58
58
  -- The handler finished running, this runner thread is free again.
59
- freeRunnerThread = acquiredRunnerThread
59
+ weakFreeRunnerThreadLookup[memoryCategory] = acquiredRunnerThread
60
60
  end
61
61
 
62
62
  -- Coroutine runner that we create coroutines of. The coroutine can be
63
63
  -- repeatedly resumed with functions to run followed by the argument to run
64
64
  -- them with.
65
- local function runEventHandlerInFreeThread()
65
+ local function runEventHandlerInFreeThread(memoryCategory)
66
+ if #memoryCategory == 0 then
67
+ debug.setmemorycategory("signal_unknown")
68
+ else
69
+ debug.setmemorycategory(memoryCategory)
70
+ end
71
+
66
72
  -- Note: We cannot use the initial set of arguments passed to
67
73
  -- runEventHandlerInFreeThread for a call to the handler, because those
68
74
  -- arguments would stay on the stack for the duration of the thread's
@@ -75,40 +81,56 @@ end
75
81
 
76
82
  -- Connection class
77
83
  local Connection = {}
84
+ Connection.ClassName = "Connection"
78
85
  Connection.__index = Connection
79
86
 
80
87
  function Connection.new(signal, fn)
81
88
  return setmetatable({
82
- _connected = true,
89
+ -- selene: allow(incorrect_standard_library_use)
90
+ _memoryCategory = debug.getmemorycategory(),
83
91
  _signal = signal,
84
92
  _fn = fn,
85
- _next = false,
86
93
  }, Connection)
87
94
  end
88
95
 
96
+ function Connection:IsConnected()
97
+ return rawget(self, "_signal") ~= nil
98
+ end
99
+
89
100
  function Connection:Disconnect()
90
- self._connected = false
91
-
92
- -- Unhook the node, but DON'T clear it. That way any fire calls that are
93
- -- currently sitting on this node will be able to iterate forwards off of
94
- -- it, but any subsequent fire calls will not hit it, and it will be GCed
95
- -- when no more fire calls are sitting on it.
96
- if self._signal._handlerListHead == self then
97
- self._signal._handlerListHead = self._next
101
+ local signal = rawget(self, "_signal")
102
+ if not signal then
103
+ return
104
+ end
105
+
106
+ -- Unhook the node. Originally the good signal would not clear this signal and
107
+ -- rely upon GC. However, this means that connections would keep themselves and other
108
+ -- disconnected nodes in the chain alive, keeping the function closure alive, and in return
109
+ -- keeping the signal alive. This means a `Maid` could keep full object trees alive if a
110
+ -- connection was made to them.
111
+
112
+ local ourNext = rawget(self, "_next")
113
+
114
+ if signal._handlerListHead == self then
115
+ signal._handlerListHead = ourNext or false
98
116
  else
99
- local prev = self._signal._handlerListHead
100
- while prev and prev._next ~= self do
101
- prev = prev._next
117
+ local prev = signal._handlerListHead
118
+ while prev and rawget(prev, "_next") ~= self do
119
+ prev = rawget(prev, "_next")
102
120
  end
103
121
  if prev then
104
- prev._next = self._next
122
+ rawset(prev, "_next", ourNext)
105
123
  end
106
124
  end
125
+
126
+ -- Clear all member variables that aren't _next so keeping a connection
127
+ -- indexed allows for GC of other components
128
+ table.clear(self)
107
129
  end
108
130
 
109
131
  Connection.Destroy = Connection.Disconnect
110
132
 
111
- -- Make Connection strict
133
+ -- Make signal strict
112
134
  setmetatable(Connection, {
113
135
  __index = function(_, key)
114
136
  error(string.format("Attempt to get Connection::%s (not a valid member)", tostring(key)), 2)
@@ -153,7 +175,7 @@ end
153
175
  function Signal:Connect(fn)
154
176
  local connection = Connection.new(self, fn)
155
177
  if self._handlerListHead then
156
- connection._next = self._handlerListHead
178
+ rawset(connection, "_next", self._handlerListHead)
157
179
  self._handlerListHead = connection
158
180
  else
159
181
  self._handlerListHead = connection
@@ -186,17 +208,26 @@ end
186
208
  @param ... T -- Variable arguments to pass to handler
187
209
  ]=]
188
210
  function Signal:Fire(...)
189
- local item = self._handlerListHead
190
- while item do
191
- if item._connected then
192
- if not freeRunnerThread then
193
- freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
194
- -- Get the freeRunnerThread to the first yield
195
- coroutine.resume(freeRunnerThread)
211
+ local connection = self._handlerListHead
212
+ while connection do
213
+ -- capture our next node, which could after this be cleared or disconnected.
214
+ -- any connections occuring during fire will be added to the _handerListHead and not be fired
215
+ -- in this round. Any disconnections in the chain will still work here.
216
+ local nextNode = rawget(connection, "_next")
217
+
218
+ if rawget(connection, "_signal") ~= nil then -- isConnected
219
+ local memoryCategory = connection._memoryCategory
220
+
221
+ -- Get the freeRunnerThread to the first yield
222
+ if not weakFreeRunnerThreadLookup[memoryCategory] then
223
+ weakFreeRunnerThreadLookup[memoryCategory] = coroutine.create(runEventHandlerInFreeThread)
224
+ coroutine.resume(weakFreeRunnerThreadLookup[memoryCategory], memoryCategory)
196
225
  end
197
- task.spawn(freeRunnerThread, item._fn, ...)
226
+
227
+ task.spawn(weakFreeRunnerThreadLookup[memoryCategory], memoryCategory, connection._fn, ...)
198
228
  end
199
- item = item._next
229
+
230
+ connection = nextNode
200
231
  end
201
232
  end
202
233
 
@@ -213,11 +244,13 @@ end
213
244
  ]=]
214
245
  function Signal:Wait()
215
246
  local waitingCoroutine = coroutine.running()
216
- local cn;
217
- cn = self:Connect(function(...)
218
- cn:Disconnect()
247
+
248
+ local connection
249
+ connection = self:Connect(function(...)
250
+ connection:Disconnect()
219
251
  task.spawn(waitingCoroutine, ...)
220
252
  end)
253
+
221
254
  return coroutine.yield()
222
255
  end
223
256
 
@@ -233,14 +266,12 @@ end
233
266
  @return RBXScriptConnection
234
267
  ]=]
235
268
  function Signal:Once(fn)
236
- local cn;
237
- cn = self:Connect(function(...)
238
- if cn._connected then
239
- cn:Disconnect()
240
- end
269
+ local connection
270
+ connection = self:Connect(function(...)
271
+ connection:Disconnect()
241
272
  fn(...)
242
273
  end)
243
- return cn
274
+ return connection
244
275
  end
245
276
 
246
277
  --[=[