@axpecter/lync 2.2.1 → 2.3.2
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/README.md +109 -147
- package/package.json +1 -1
- package/src/Types.luau +34 -22
- package/src/api/Group.luau +40 -33
- package/src/api/Packet.luau +140 -66
- package/src/api/Query.luau +103 -65
- package/src/api/Scope.luau +26 -10
- package/src/api/Signal.luau +43 -52
- package/src/codec/Base.luau +28 -14
- package/src/codec/composite/Array.luau +296 -115
- package/src/codec/composite/Map.luau +377 -57
- package/src/codec/composite/Optional.luau +10 -2
- package/src/codec/composite/Shared.luau +134 -64
- package/src/codec/composite/Struct.luau +294 -43
- package/src/codec/composite/Tagged.luau +21 -16
- package/src/codec/composite/Tuple.luau +32 -15
- package/src/codec/datatype/Buffer.luau +7 -7
- package/src/codec/datatype/CFrame.luau +34 -122
- package/src/codec/datatype/Color.luau +10 -10
- package/src/codec/datatype/Instance.luau +15 -12
- package/src/codec/datatype/IntVector.luau +2 -2
- package/src/codec/datatype/NumberRange.luau +15 -8
- package/src/codec/datatype/Ray.luau +13 -14
- package/src/codec/datatype/Rect.luau +12 -12
- package/src/codec/datatype/Region.luau +13 -14
- package/src/codec/datatype/Sequence.luau +87 -65
- package/src/codec/datatype/String.luau +17 -10
- package/src/codec/datatype/UDim.luau +10 -8
- package/src/codec/datatype/Vector.luau +20 -59
- package/src/codec/meta/Auto.luau +94 -126
- package/src/codec/meta/Bitfield.luau +12 -14
- package/src/codec/meta/Custom.luau +3 -1
- package/src/codec/meta/DeltaScalar.luau +390 -0
- package/src/codec/meta/Enum.luau +9 -9
- package/src/codec/meta/Float.luau +6 -28
- package/src/codec/meta/Nothing.luau +1 -1
- package/src/codec/meta/Unknown.luau +10 -7
- package/src/codec/primitive/Bool.luau +6 -4
- package/src/codec/primitive/Float16.luau +5 -2
- package/src/codec/primitive/Int.luau +5 -6
- package/src/codec/primitive/Number.luau +6 -4
- package/src/codec/primitive/Signed.luau +2 -2
- package/src/codec/primitive/Varint.luau +76 -39
- package/src/codec/primitive/Zint.luau +99 -0
- package/src/index.d.ts +207 -53
- package/src/init.luau +116 -103
- package/src/internal/Baseline.luau +9 -1
- package/src/internal/Channel.luau +191 -157
- package/src/internal/Middleware.luau +22 -6
- package/src/internal/Pool.luau +12 -4
- package/src/internal/Registry.luau +25 -16
- package/src/internal/Transport.luau +1 -1
- package/src/transport/Bridge.luau +47 -33
- package/src/transport/Client.luau +25 -21
- package/src/transport/Gate.luau +227 -172
- package/src/transport/Reader.luau +174 -106
- package/src/transport/Server.luau +46 -57
- package/src/util/Array.luau +18 -0
- package/src/util/Buffer.luau +90 -0
- package/src/util/Constants.luau +30 -0
- package/src/util/Log.luau +68 -0
- package/src/util/Player.luau +14 -0
- package/src/util/Quantize.luau +84 -0
- package/src/util/Quat.luau +124 -0
- package/src/internal/Util.luau +0 -26
package/src/api/Query.luau
CHANGED
|
@@ -6,38 +6,82 @@ local Players = game:GetService("Players")
|
|
|
6
6
|
local RunService = game:GetService("RunService")
|
|
7
7
|
|
|
8
8
|
local Channel = require(script.Parent.Parent.internal.Channel)
|
|
9
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
10
|
+
local Player = require(script.Parent.Parent.util.Player)
|
|
9
11
|
local Registry = require(script.Parent.Parent.internal.Registry)
|
|
10
12
|
local Transport = require(script.Parent.Parent.internal.Transport)
|
|
11
13
|
local Types = require(script.Parent.Parent.Types)
|
|
12
|
-
local Util = require(script.Parent.Parent.internal.Util)
|
|
13
14
|
|
|
14
15
|
-- Constants --------------------------------------------------------------
|
|
15
16
|
|
|
16
17
|
local IS_SERVER = RunService:IsServer()
|
|
17
18
|
local DEFAULT_TIMEOUT = 5
|
|
18
19
|
|
|
20
|
+
local EMPTY_OPTIONS: Types.QueryOptions = table.freeze({})
|
|
21
|
+
|
|
19
22
|
-- State ------------------------------------------------------------------
|
|
20
23
|
|
|
21
24
|
local _nextCorrId = 0
|
|
25
|
+
-- Correlation -> waiting thread / completion closure. Cleared on resolve or timeout.
|
|
22
26
|
local _pending: { [number]: thread } = {}
|
|
23
27
|
local _pendingGather: { [number]: (any) -> () } = {}
|
|
24
28
|
|
|
25
29
|
-- Private ----------------------------------------------------------------
|
|
26
30
|
|
|
27
|
-
local isPlayer =
|
|
31
|
+
local isPlayer = Player.is
|
|
28
32
|
|
|
29
33
|
local function nextCorr(): number
|
|
30
34
|
_nextCorrId += 1
|
|
31
35
|
return _nextCorrId
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
local function writeQueryTracked(
|
|
39
|
+
ch: Types.ChannelState,
|
|
40
|
+
reg: Types.Registration,
|
|
41
|
+
corrId: number,
|
|
42
|
+
codec: Types.InternalCodec<any>?,
|
|
43
|
+
data: any
|
|
44
|
+
): ()
|
|
45
|
+
if Channel.statsEnabled() then
|
|
46
|
+
local before = ch.cursor
|
|
47
|
+
Channel.writeQuery(ch, reg.id, corrId, codec, data)
|
|
48
|
+
reg.bytesSent += ch.cursor - before
|
|
49
|
+
else
|
|
50
|
+
Channel.writeQuery(ch, reg.id, corrId, codec, data)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
local function collectTargets(target: any): { Player }
|
|
55
|
+
local targets: { Player } = {}
|
|
56
|
+
if typeof(target) ~= "table" then
|
|
57
|
+
return targets
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
local kind = target._lyncKind
|
|
61
|
+
if kind == "all" then
|
|
62
|
+
for _, p in Players:GetPlayers() do
|
|
63
|
+
table.insert(targets, p)
|
|
64
|
+
end
|
|
65
|
+
elseif kind == "group" then
|
|
66
|
+
for p in target._members do
|
|
67
|
+
table.insert(targets, p)
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
for _, p in target do
|
|
71
|
+
if isPlayer(p) then
|
|
72
|
+
table.insert(targets, p :: Player)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
return targets
|
|
77
|
+
end
|
|
78
|
+
|
|
34
79
|
-- Public types -----------------------------------------------------------
|
|
35
80
|
|
|
36
81
|
--[[
|
|
37
82
|
Server `request(data, target)` returns `Resp?` for a Player target and
|
|
38
|
-
`{ [Player]: Resp? }` for a multi-target table; Luau
|
|
39
|
-
|
|
40
|
-
single-target case). Multi-target callers must cast the return.
|
|
83
|
+
`{ [Player]: Resp? }` for a multi-target table; Luau lacks overloads on
|
|
84
|
+
table fields, so the type advertises `Resp?` and multi-target callers cast.
|
|
41
85
|
]]
|
|
42
86
|
export type QueryHandle<Req, Resp> = {
|
|
43
87
|
handle: (
|
|
@@ -59,10 +103,11 @@ function Query.define<Req, Resp>(
|
|
|
59
103
|
responseCodec: Types.InternalCodec<Resp>,
|
|
60
104
|
options: Types.QueryOptions?
|
|
61
105
|
): QueryHandle<Req, Resp>
|
|
62
|
-
local opts = options or
|
|
106
|
+
local opts = options or EMPTY_OPTIONS
|
|
63
107
|
local timeout = opts.timeout or DEFAULT_TIMEOUT
|
|
64
108
|
|
|
65
109
|
local reqOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, name)
|
|
110
|
+
-- Embedded "\0resp" suffix stays uniquely paired with the request reg.
|
|
66
111
|
local respName = `{name}\0resp`
|
|
67
112
|
local respOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, respName)
|
|
68
113
|
|
|
@@ -98,6 +143,7 @@ function Query.define<Req, Resp>(
|
|
|
98
143
|
|
|
99
144
|
reqReg.partner = respReg.id
|
|
100
145
|
|
|
146
|
+
-- Single active handler; replacing it disconnects the prior one via handlerToken.
|
|
101
147
|
local activeHandler: ((any, Player?) -> any)? = nil
|
|
102
148
|
local handlerToken = 0
|
|
103
149
|
|
|
@@ -122,18 +168,22 @@ function Query.define<Req, Resp>(
|
|
|
122
168
|
end
|
|
123
169
|
|
|
124
170
|
local ok, response = pcall(handler, value, sender)
|
|
171
|
+
if not ok then
|
|
172
|
+
Log.warn(`query "{name}" handler errored: {response}`)
|
|
173
|
+
end
|
|
174
|
+
-- pcall failure path returns nil response (writeQuery encodes a nil-MSB header).
|
|
125
175
|
local payloadCodec = if ok then responseCodec else nil
|
|
126
176
|
local payloadData = if ok then response else nil
|
|
127
177
|
|
|
128
|
-
local ch = if IS_SERVER
|
|
129
|
-
then Transport.server().getChannel(sender, false)
|
|
130
|
-
|
|
131
|
-
else nil
|
|
178
|
+
local ch: Types.ChannelState? = if IS_SERVER
|
|
179
|
+
then (if sender then Transport.server().getChannel(sender, false) else nil)
|
|
180
|
+
else Transport.client().getChannel(false)
|
|
132
181
|
if ch then
|
|
133
|
-
|
|
182
|
+
writeQueryTracked(ch, respReg, corrId, payloadCodec, payloadData)
|
|
134
183
|
end
|
|
135
184
|
end)
|
|
136
185
|
|
|
186
|
+
-- Resolve the waiting thread with nil after `timeout` seconds.
|
|
137
187
|
local function scheduleSingleTimeout(corrId: number, running: thread): ()
|
|
138
188
|
task.delay(timeout, function()
|
|
139
189
|
if _pending[corrId] == running then
|
|
@@ -143,10 +193,10 @@ function Query.define<Req, Resp>(
|
|
|
143
193
|
end)
|
|
144
194
|
end
|
|
145
195
|
|
|
146
|
-
|
|
196
|
+
-- Issue a single-target query on `ch` and yield until reply or timeout.
|
|
197
|
+
local function requestOnChannel(ch: Types.ChannelState, data: any): any
|
|
147
198
|
local corrId = nextCorr()
|
|
148
|
-
|
|
149
|
-
Channel.writeQuery(ch, reqReg.id, corrId, requestCodec, data)
|
|
199
|
+
writeQueryTracked(ch, reqReg, corrId, requestCodec, data)
|
|
150
200
|
|
|
151
201
|
local running = coroutine.running()
|
|
152
202
|
_pending[corrId] = running
|
|
@@ -155,30 +205,41 @@ function Query.define<Req, Resp>(
|
|
|
155
205
|
end
|
|
156
206
|
|
|
157
207
|
local function requestMulti(targets: { Player }, data: any): { [Player]: any }
|
|
158
|
-
|
|
208
|
+
local targetCount = #targets
|
|
209
|
+
if targetCount == 0 then
|
|
159
210
|
return {}
|
|
160
211
|
end
|
|
161
212
|
|
|
162
213
|
local results: { [Player]: any } = {}
|
|
163
|
-
local remaining =
|
|
214
|
+
local remaining = targetCount
|
|
164
215
|
local running = coroutine.running()
|
|
165
216
|
local resolved = false
|
|
217
|
+
local corrIds = table.create(targetCount)
|
|
166
218
|
|
|
219
|
+
--[[
|
|
220
|
+
On timeout, surface the partial result table AND evict every
|
|
221
|
+
still-pending closure. Without eviction each unanswered closure
|
|
222
|
+
keeps `results`/`remaining`/`resolveOnce` alive: a slow leak.
|
|
223
|
+
]]
|
|
167
224
|
local function resolveOnce(): ()
|
|
168
225
|
if resolved then
|
|
169
226
|
return
|
|
170
227
|
end
|
|
171
228
|
resolved = true
|
|
229
|
+
for i = 1, targetCount do
|
|
230
|
+
_pendingGather[corrIds[i]] = nil
|
|
231
|
+
end
|
|
172
232
|
task.spawn(running, results)
|
|
173
233
|
end
|
|
174
234
|
|
|
175
235
|
local Server = Transport.server()
|
|
176
|
-
for
|
|
236
|
+
for i, p in targets do
|
|
177
237
|
local pCorr = nextCorr()
|
|
178
|
-
|
|
179
|
-
|
|
238
|
+
corrIds[i] = pCorr
|
|
239
|
+
local ch = Server.getChannel(p, false)
|
|
240
|
+
writeQueryTracked(ch, reqReg, pCorr, requestCodec, data)
|
|
180
241
|
|
|
181
|
-
local pRef =
|
|
242
|
+
local pRef = p
|
|
182
243
|
_pendingGather[pCorr] = function(response: any)
|
|
183
244
|
results[pRef] = response
|
|
184
245
|
remaining -= 1
|
|
@@ -192,54 +253,28 @@ function Query.define<Req, Resp>(
|
|
|
192
253
|
return coroutine.yield()
|
|
193
254
|
end
|
|
194
255
|
|
|
195
|
-
local function collectTargets(target: any): { Player }
|
|
196
|
-
local targets: { Player } = {}
|
|
197
|
-
if typeof(target) ~= "table" then
|
|
198
|
-
return targets
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
if target._lyncAll then
|
|
202
|
-
for _, p in Players:GetPlayers() do
|
|
203
|
-
table.insert(targets, p)
|
|
204
|
-
end
|
|
205
|
-
elseif target._lyncGroup then
|
|
206
|
-
for player in target._members do
|
|
207
|
-
table.insert(targets, player)
|
|
208
|
-
end
|
|
209
|
-
else
|
|
210
|
-
for _, p in target do
|
|
211
|
-
if isPlayer(p) then
|
|
212
|
-
table.insert(targets, p :: Player)
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
return targets
|
|
217
|
-
end
|
|
218
|
-
|
|
219
256
|
local function requestFromServer(data: any, target: any): any
|
|
220
257
|
if target == nil then
|
|
221
|
-
error(`
|
|
258
|
+
Log.error(`"{name}" server request requires a target`)
|
|
222
259
|
end
|
|
223
260
|
if isPlayer(target) then
|
|
224
|
-
return
|
|
261
|
+
return requestOnChannel(Transport.server().getChannel(target :: Player, false), data)
|
|
225
262
|
end
|
|
226
263
|
return requestMulti(collectTargets(target), data)
|
|
227
264
|
end
|
|
228
265
|
|
|
229
266
|
local function requestFromClient(data: any): any
|
|
230
|
-
|
|
231
|
-
local ch = Transport.client().getChannel(false)
|
|
232
|
-
Channel.writeQuery(ch, reqReg.id, corrId, requestCodec, data)
|
|
233
|
-
|
|
234
|
-
local running = coroutine.running()
|
|
235
|
-
_pending[corrId] = running
|
|
236
|
-
scheduleSingleTimeout(corrId, running)
|
|
237
|
-
return coroutine.yield()
|
|
267
|
+
return requestOnChannel(Transport.client().getChannel(false), data)
|
|
238
268
|
end
|
|
239
269
|
|
|
240
270
|
type Self = QueryHandle<Req, Resp>
|
|
241
271
|
|
|
242
272
|
local handle = {
|
|
273
|
+
--[[
|
|
274
|
+
Replace the active handler. The returned Connection only clears
|
|
275
|
+
the active slot if no later :handle has overwritten it: token
|
|
276
|
+
comparison guards against a stale disconnect wiping a fresh handler.
|
|
277
|
+
]]
|
|
243
278
|
handle = function(
|
|
244
279
|
_self: Self,
|
|
245
280
|
fn: (request: Req, player: Player?) -> Resp?
|
|
@@ -248,17 +283,20 @@ function Query.define<Req, Resp>(
|
|
|
248
283
|
local myToken = handlerToken
|
|
249
284
|
activeHandler = fn :: any
|
|
250
285
|
|
|
251
|
-
local conn
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
286
|
+
local conn: Types.Connection
|
|
287
|
+
conn = {
|
|
288
|
+
connected = true,
|
|
289
|
+
disconnect = function(self): ()
|
|
290
|
+
if not self.connected then
|
|
291
|
+
return
|
|
292
|
+
end
|
|
293
|
+
self.connected = false
|
|
294
|
+
if handlerToken == myToken then
|
|
295
|
+
activeHandler = nil
|
|
296
|
+
end
|
|
297
|
+
end,
|
|
298
|
+
}
|
|
299
|
+
return conn
|
|
262
300
|
end,
|
|
263
301
|
|
|
264
302
|
request = function(_self: Self, data: Req, target: any?): Resp?
|
package/src/api/Scope.luau
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
-- Batched connection lifecycle
|
|
4
|
-
-- RBXScriptConnection, and any object with :disconnect() / :Disconnect().
|
|
3
|
+
-- Batched connection lifecycle for Lync, RBXScriptConnection, and disconnect()-style objects.
|
|
5
4
|
|
|
6
5
|
local Types = require(script.Parent.Parent.Types)
|
|
7
6
|
|
|
@@ -25,21 +24,29 @@ local function pushConnection(self: Scope, conn: any): ()
|
|
|
25
24
|
self._connections[idx] = conn
|
|
26
25
|
end
|
|
27
26
|
|
|
27
|
+
-- Routes :on / :once through `methodName` so both call sites share insert + size bookkeeping.
|
|
28
|
+
local function attach(
|
|
29
|
+
self: Scope,
|
|
30
|
+
source: any,
|
|
31
|
+
fn: (...any) -> (),
|
|
32
|
+
methodName: string
|
|
33
|
+
): Types.Connection
|
|
34
|
+
local conn = source[methodName](source, fn)
|
|
35
|
+
pushConnection(self, conn)
|
|
36
|
+
return conn
|
|
37
|
+
end
|
|
38
|
+
|
|
28
39
|
-- Public -----------------------------------------------------------------
|
|
29
40
|
|
|
30
41
|
local ScopeMeta = {}
|
|
31
42
|
ScopeMeta.__index = ScopeMeta
|
|
32
43
|
|
|
33
44
|
function ScopeMeta.on(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
|
|
34
|
-
|
|
35
|
-
pushConnection(self, conn)
|
|
36
|
-
return conn
|
|
45
|
+
return attach(self, source, fn, "on")
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
function ScopeMeta.once(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
|
|
40
|
-
|
|
41
|
-
pushConnection(self, conn)
|
|
42
|
-
return conn
|
|
49
|
+
return attach(self, source, fn, "once")
|
|
43
50
|
end
|
|
44
51
|
|
|
45
52
|
ScopeMeta.add = pushConnection
|
|
@@ -48,6 +55,11 @@ function ScopeMeta.size(self: Scope): number
|
|
|
48
55
|
return self._count
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
--[[
|
|
59
|
+
Disconnect every tracked source. The shape sniff handles three kinds:
|
|
60
|
+
Roblox RBXScriptConnection (:Disconnect), Lync Connection (:disconnect),
|
|
61
|
+
and tables exposing either capitalization.
|
|
62
|
+
]]
|
|
51
63
|
function ScopeMeta.destroy(self: Scope): ()
|
|
52
64
|
local conns = self._connections
|
|
53
65
|
local count = self._count
|
|
@@ -56,8 +68,12 @@ function ScopeMeta.destroy(self: Scope): ()
|
|
|
56
68
|
conns[i] = nil
|
|
57
69
|
if typeof(conn) == "RBXScriptConnection" then
|
|
58
70
|
(conn :: RBXScriptConnection):Disconnect()
|
|
59
|
-
elseif typeof(conn) == "table"
|
|
60
|
-
conn
|
|
71
|
+
elseif typeof(conn) == "table" then
|
|
72
|
+
if conn.disconnect then
|
|
73
|
+
conn:disconnect()
|
|
74
|
+
elseif conn.Disconnect then
|
|
75
|
+
conn:Disconnect()
|
|
76
|
+
end
|
|
61
77
|
end
|
|
62
78
|
end
|
|
63
79
|
self._count = 0
|
package/src/api/Signal.luau
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
-- O(1) disconnect signal
|
|
3
|
+
-- O(1) disconnect signal with thread reuse and reentrancy-safe fire.
|
|
4
4
|
|
|
5
5
|
local Types = require(script.Parent.Parent.Types)
|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ local STATE_ONCE = 2
|
|
|
12
12
|
|
|
13
13
|
-- State ------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
+
-- Single recyclable thread for async dispatch; saves coroutine.create per fire.
|
|
15
16
|
local _freeThread: thread? = nil
|
|
16
17
|
|
|
17
18
|
-- Private ----------------------------------------------------------------
|
|
@@ -41,14 +42,19 @@ type Entry = {
|
|
|
41
42
|
fn: (...any) -> (),
|
|
42
43
|
_state: number,
|
|
43
44
|
connected: boolean,
|
|
44
|
-
|
|
45
|
-
_signalCount: { number },
|
|
45
|
+
_signal: SignalFields,
|
|
46
46
|
_index: number,
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
type SignalFields = {
|
|
50
|
+
_entries: { Entry },
|
|
51
|
+
_count: number,
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
local EntryImpl = {}
|
|
50
55
|
EntryImpl.__index = EntryImpl
|
|
51
56
|
|
|
57
|
+
-- Swap-with-last for O(1) disconnect; the moved entry's _index is patched too.
|
|
52
58
|
local function disconnectEntry(entry: Entry): ()
|
|
53
59
|
if entry._state == STATE_DISCONNECTED then
|
|
54
60
|
return
|
|
@@ -57,9 +63,10 @@ local function disconnectEntry(entry: Entry): ()
|
|
|
57
63
|
entry._state = STATE_DISCONNECTED
|
|
58
64
|
entry.connected = false
|
|
59
65
|
|
|
60
|
-
local
|
|
61
|
-
local
|
|
62
|
-
local idx
|
|
66
|
+
local signal = entry._signal
|
|
67
|
+
local entries = signal._entries
|
|
68
|
+
local idx = entry._index
|
|
69
|
+
local last = signal._count
|
|
63
70
|
|
|
64
71
|
if idx < last then
|
|
65
72
|
local moved = entries[last]
|
|
@@ -67,31 +74,40 @@ local function disconnectEntry(entry: Entry): ()
|
|
|
67
74
|
moved._index = idx
|
|
68
75
|
end
|
|
69
76
|
entries[last] = nil
|
|
70
|
-
|
|
77
|
+
signal._count = last - 1
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
EntryImpl.disconnect = disconnectEntry
|
|
74
81
|
table.freeze(EntryImpl)
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
-- Auto-disconnects ONCE entries before invoking; chooses sync vs spawned thread.
|
|
84
|
+
local function invokeEntry(entry: Entry, sync: boolean, ...: any): ()
|
|
85
|
+
local state = entry._state
|
|
86
|
+
if state == STATE_DISCONNECTED then
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
if state == STATE_ONCE then
|
|
90
|
+
disconnectEntry(entry)
|
|
91
|
+
end
|
|
92
|
+
if sync then
|
|
93
|
+
entry.fn(...)
|
|
94
|
+
else
|
|
95
|
+
spawnCallback(entry.fn, ...)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
80
98
|
|
|
81
99
|
local SignalImpl = {}
|
|
82
100
|
SignalImpl.__index = SignalImpl
|
|
83
101
|
|
|
84
102
|
local function newEntry(self: SignalFields, callback: (...any) -> (), state: number): Entry
|
|
85
|
-
local
|
|
86
|
-
|
|
87
|
-
countRef[1] = idx
|
|
103
|
+
local idx = self._count + 1
|
|
104
|
+
self._count = idx
|
|
88
105
|
|
|
89
106
|
local entry: Entry = setmetatable({
|
|
90
107
|
fn = callback,
|
|
91
108
|
_state = state,
|
|
92
109
|
connected = true,
|
|
93
|
-
|
|
94
|
-
_signalCount = countRef,
|
|
110
|
+
_signal = self,
|
|
95
111
|
_index = idx,
|
|
96
112
|
}, EntryImpl) :: any
|
|
97
113
|
|
|
@@ -118,53 +134,28 @@ function SignalImpl.wait(self: SignalFields): ...any
|
|
|
118
134
|
end
|
|
119
135
|
|
|
120
136
|
local function dispatch(self: SignalFields, sync: boolean, ...: any): ()
|
|
121
|
-
local count = self._count
|
|
137
|
+
local count = self._count
|
|
122
138
|
if count == 0 then
|
|
123
139
|
return
|
|
124
140
|
end
|
|
125
141
|
|
|
126
142
|
local entries = self._entries
|
|
127
143
|
|
|
144
|
+
-- Single-listener fast path skips the snapshot allocation.
|
|
128
145
|
if count == 1 then
|
|
129
|
-
|
|
130
|
-
local state = entry._state
|
|
131
|
-
if state == STATE_ONCE then
|
|
132
|
-
disconnectEntry(entry)
|
|
133
|
-
if sync then
|
|
134
|
-
entry.fn(...)
|
|
135
|
-
else
|
|
136
|
-
spawnCallback(entry.fn, ...)
|
|
137
|
-
end
|
|
138
|
-
elseif state == STATE_CONNECTED then
|
|
139
|
-
if sync then
|
|
140
|
-
entry.fn(...)
|
|
141
|
-
else
|
|
142
|
-
spawnCallback(entry.fn, ...)
|
|
143
|
-
end
|
|
144
|
-
end
|
|
146
|
+
invokeEntry(entries[1], sync, ...)
|
|
145
147
|
return
|
|
146
148
|
end
|
|
147
149
|
|
|
150
|
+
--[[
|
|
151
|
+
Re-entrancy: a handler may disconnect or add others mid-fire.
|
|
152
|
+
Snapshot the entry list so the iteration view is stable.
|
|
153
|
+
]]
|
|
148
154
|
local snapshot = table.create(count)
|
|
149
155
|
table.move(entries, 1, count, 1, snapshot)
|
|
150
156
|
|
|
151
157
|
for i = 1, count do
|
|
152
|
-
|
|
153
|
-
local state = entry._state
|
|
154
|
-
if state == STATE_ONCE then
|
|
155
|
-
disconnectEntry(entry)
|
|
156
|
-
if sync then
|
|
157
|
-
entry.fn(...)
|
|
158
|
-
else
|
|
159
|
-
spawnCallback(entry.fn, ...)
|
|
160
|
-
end
|
|
161
|
-
elseif state == STATE_CONNECTED then
|
|
162
|
-
if sync then
|
|
163
|
-
entry.fn(...)
|
|
164
|
-
else
|
|
165
|
-
spawnCallback(entry.fn, ...)
|
|
166
|
-
end
|
|
167
|
-
end
|
|
158
|
+
invokeEntry(snapshot[i], sync, ...)
|
|
168
159
|
end
|
|
169
160
|
end
|
|
170
161
|
|
|
@@ -177,11 +168,11 @@ function SignalImpl.fireSync(self: SignalFields, ...: any): ()
|
|
|
177
168
|
end
|
|
178
169
|
|
|
179
170
|
function SignalImpl.count(self: SignalFields): number
|
|
180
|
-
return self._count
|
|
171
|
+
return self._count
|
|
181
172
|
end
|
|
182
173
|
|
|
183
174
|
function SignalImpl.hasListeners(self: SignalFields): boolean
|
|
184
|
-
return self._count
|
|
175
|
+
return self._count > 0
|
|
185
176
|
end
|
|
186
177
|
|
|
187
178
|
table.freeze(SignalImpl)
|
|
@@ -191,7 +182,7 @@ table.freeze(SignalImpl)
|
|
|
191
182
|
local SignalModule = {}
|
|
192
183
|
|
|
193
184
|
function SignalModule.create(): Types.SignalLike
|
|
194
|
-
return (setmetatable({ _entries = {}, _count =
|
|
185
|
+
return (setmetatable({ _entries = {}, _count = 0 }, SignalImpl) :: any) :: Types.SignalLike
|
|
195
186
|
end
|
|
196
187
|
|
|
197
188
|
return table.freeze(SignalModule)
|
package/src/codec/Base.luau
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
-- Fixed-size codec factory
|
|
3
|
+
-- Fixed-size codec factory + channel buffer-grow primitive.
|
|
4
4
|
|
|
5
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
5
6
|
local Types = require(script.Parent.Parent.Types)
|
|
6
7
|
|
|
7
8
|
-- Constants --------------------------------------------------------------
|
|
@@ -19,9 +20,9 @@ local countlz = bit32.countlz
|
|
|
19
20
|
local lshift = bit32.lshift
|
|
20
21
|
|
|
21
22
|
--[[
|
|
22
|
-
Grow `ch.buff` so
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
Grow `ch.buff` so cursor + bytes fits. New size is the next power of two
|
|
24
|
+
>= needed, capped by _maxSize. Callers MUST re-read `ch.buff` after
|
|
25
|
+
return: the buffer reference is replaced.
|
|
25
26
|
]]
|
|
26
27
|
local function alloc(ch: Types.ChannelState, bytes: number): ()
|
|
27
28
|
if bytes == 0 then
|
|
@@ -36,9 +37,9 @@ local function alloc(ch: Types.ChannelState, bytes: number): ()
|
|
|
36
37
|
if needed > _maxSize then
|
|
37
38
|
local name = ch.currentPacket
|
|
38
39
|
if name then
|
|
39
|
-
error(`
|
|
40
|
+
Log.error(`packet "{name}" overflowed at {needed}B (max {_maxSize}B)`)
|
|
40
41
|
end
|
|
41
|
-
error(`
|
|
42
|
+
Log.error(`overflowed at {needed}B (max {_maxSize}B)`)
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
local newSize = lshift(1, 32 - countlz(needed - 1))
|
|
@@ -53,18 +54,31 @@ end
|
|
|
53
54
|
local Base = {}
|
|
54
55
|
|
|
55
56
|
Base.INITIAL_BUF = INITIAL_BUF
|
|
56
|
-
|
|
57
57
|
Base.alloc = alloc
|
|
58
58
|
|
|
59
|
+
-- Cheap inline guard hoisted into every codec that grows a channel directly.
|
|
60
|
+
function Base.ensure(ch: Types.ChannelState, n: number): ()
|
|
61
|
+
if ch.cursor + n > ch.size then
|
|
62
|
+
alloc(ch, n)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
-- Floor is INITIAL_BUF: anything smaller errors on the very first write.
|
|
59
67
|
function Base.setMaxSize(bytes: number): ()
|
|
68
|
+
if bytes < INITIAL_BUF then
|
|
69
|
+
Log.error(`bytes ({bytes}) must be >= {INITIAL_BUF}`)
|
|
70
|
+
end
|
|
60
71
|
_maxSize = bytes
|
|
61
72
|
end
|
|
62
73
|
|
|
74
|
+
function Base.reset(): ()
|
|
75
|
+
_maxSize = DEFAULT_MAX
|
|
76
|
+
end
|
|
77
|
+
|
|
63
78
|
--[[
|
|
64
|
-
Build a fixed-size codec.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
pre-sized fast paths.
|
|
79
|
+
Build a fixed-size codec. Exposes channel-level write/read (handles
|
|
80
|
+
buffer growth + cursor) plus raw _directWrite/_directRead for composite
|
|
81
|
+
fast paths that pre-size the channel themselves.
|
|
68
82
|
]]
|
|
69
83
|
function Base.define<T>(
|
|
70
84
|
size: number,
|
|
@@ -101,9 +115,9 @@ function Base.define<T>(
|
|
|
101
115
|
end
|
|
102
116
|
|
|
103
117
|
--[[
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
Zero-byte codec: writes nothing, reads always return `value`. Used by
|
|
119
|
+
quantizers when min == max so the wire emits nothing yet decode produces
|
|
120
|
+
a type-faithful value.
|
|
107
121
|
]]
|
|
108
122
|
function Base.constant<T>(value: T): Types.InternalCodec<T>
|
|
109
123
|
return table.freeze({
|