@axpecter/lync 2.2.0 → 2.3.1
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 +95 -143
- package/package.json +1 -1
- package/src/Types.luau +22 -16
- package/src/api/Group.luau +40 -33
- package/src/api/Packet.luau +95 -72
- package/src/api/Query.luau +128 -79
- package/src/api/Scope.luau +26 -10
- package/src/api/Signal.luau +43 -52
- package/src/codec/Base.luau +21 -14
- package/src/codec/composite/Array.luau +56 -134
- package/src/codec/composite/Map.luau +103 -72
- package/src/codec/composite/Optional.luau +6 -2
- package/src/codec/composite/Shared.luau +160 -69
- package/src/codec/composite/Struct.luau +283 -42
- package/src/codec/composite/Tagged.luau +13 -16
- package/src/codec/composite/Tuple.luau +21 -15
- package/src/codec/datatype/Buffer.luau +6 -4
- package/src/codec/datatype/CFrame.luau +56 -44
- 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/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 +69 -38
- package/src/index.d.ts +161 -53
- package/src/init.luau +123 -102
- package/src/internal/Baseline.luau +9 -1
- package/src/internal/Channel.luau +153 -154
- 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 +162 -105
- package/src/transport/Server.luau +46 -57
- package/src/util/Array.luau +18 -0
- package/src/util/Buffer.luau +92 -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 +60 -0
- package/src/internal/Util.luau +0 -26
package/src/api/Packet.luau
CHANGED
|
@@ -7,67 +7,77 @@ local RunService = game:GetService("RunService")
|
|
|
7
7
|
|
|
8
8
|
local Baseline = require(script.Parent.Parent.internal.Baseline)
|
|
9
9
|
local Channel = require(script.Parent.Parent.internal.Channel)
|
|
10
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
10
11
|
local Middleware = require(script.Parent.Parent.internal.Middleware)
|
|
12
|
+
local Player = require(script.Parent.Parent.util.Player)
|
|
11
13
|
local Registry = require(script.Parent.Parent.internal.Registry)
|
|
12
14
|
local Transport = require(script.Parent.Parent.internal.Transport)
|
|
13
15
|
local Types = require(script.Parent.Parent.Types)
|
|
14
|
-
local Util = require(script.Parent.Parent.internal.Util)
|
|
15
16
|
|
|
16
17
|
-- Constants --------------------------------------------------------------
|
|
17
18
|
|
|
18
19
|
local IS_SERVER = RunService:IsServer()
|
|
19
20
|
|
|
21
|
+
-- mode: TS_* tag; headerSize: 1 (frame byte) + timestamp bytes (1, 2, or 8).
|
|
22
|
+
local TS_MODES: { [string]: { mode: number, headerSize: number } } = table.freeze({
|
|
23
|
+
frame = table.freeze({ mode = Channel.TS_FRAME, headerSize = 2 }),
|
|
24
|
+
offset = table.freeze({ mode = Channel.TS_OFFSET, headerSize = 3 }),
|
|
25
|
+
full = table.freeze({ mode = Channel.TS_FULL, headerSize = 9 }),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
local EMPTY_OPTIONS: Types.PacketOptions = table.freeze({})
|
|
29
|
+
|
|
20
30
|
-- Private ----------------------------------------------------------------
|
|
21
31
|
|
|
22
|
-
local isPlayer =
|
|
32
|
+
local isPlayer = Player.is
|
|
23
33
|
|
|
24
34
|
local function resolveTimestampMode(ts: ("frame" | "offset" | "full")?): (number, number)
|
|
25
|
-
if ts ==
|
|
26
|
-
Channel.
|
|
27
|
-
return Channel.TS_FRAME, 2
|
|
28
|
-
end
|
|
29
|
-
if ts == "offset" then
|
|
30
|
-
Channel.enableTimestamps()
|
|
31
|
-
return Channel.TS_OFFSET, 3
|
|
35
|
+
if ts == nil then
|
|
36
|
+
return Channel.TS_NONE, 1
|
|
32
37
|
end
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
37
|
-
return Channel.TS_NONE, 1
|
|
38
|
+
local entry = TS_MODES[ts]
|
|
39
|
+
Channel.enableTimestamps()
|
|
40
|
+
return entry.mode, entry.headerSize
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
-- Public types -----------------------------------------------------------
|
|
41
44
|
|
|
42
|
-
export type PacketHandle = {
|
|
43
|
-
send: (self: PacketHandle
|
|
44
|
-
on: (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
export type PacketHandle<T> = {
|
|
46
|
+
send: (self: PacketHandle<T>, data: T, target: any?) -> (),
|
|
47
|
+
on: (
|
|
48
|
+
self: PacketHandle<T>,
|
|
49
|
+
fn: (data: T, sender: Player?, timestamp: number?) -> ()
|
|
50
|
+
) -> Types.Connection,
|
|
51
|
+
once: (
|
|
52
|
+
self: PacketHandle<T>,
|
|
53
|
+
fn: (data: T, sender: Player?, timestamp: number?) -> ()
|
|
54
|
+
) -> Types.Connection,
|
|
55
|
+
wait: (self: PacketHandle<T>) -> (T, Player?, number?),
|
|
56
|
+
name: (self: PacketHandle<T>) -> string,
|
|
57
|
+
stats: (self: PacketHandle<T>) -> Types.PacketStats,
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
-- Public -----------------------------------------------------------------
|
|
52
61
|
|
|
53
62
|
local Packet = {}
|
|
54
63
|
|
|
55
|
-
function Packet.define(
|
|
64
|
+
function Packet.define<T>(
|
|
56
65
|
name: string,
|
|
57
|
-
codec: Types.InternalCodec<
|
|
66
|
+
codec: Types.InternalCodec<T>,
|
|
58
67
|
options: Types.PacketOptions?
|
|
59
|
-
): PacketHandle
|
|
60
|
-
local opts = options or
|
|
68
|
+
): PacketHandle<T>
|
|
69
|
+
local opts = options or EMPTY_OPTIONS
|
|
61
70
|
local isUnreliable = opts.unreliable or false
|
|
62
71
|
|
|
72
|
+
-- Delta + unreliable is unsafe: a dropped frame permanently desyncs the receiver baseline.
|
|
63
73
|
if isUnreliable and codec._isDelta then
|
|
64
|
-
error(
|
|
74
|
+
Log.error(
|
|
75
|
+
`"{name}" delta codecs need reliable transport (lost packets desync the baseline)`
|
|
76
|
+
)
|
|
65
77
|
end
|
|
66
78
|
|
|
67
79
|
if codec._hasUnknown and not opts.validate then
|
|
68
|
-
warn(
|
|
69
|
-
`[Lync] Lync.packet: "{name}" uses unknown codec without validate; data bypasses schema validation`
|
|
70
|
-
)
|
|
80
|
+
Log.warn(`"{name}" uses unknown codec without validate; data bypasses schema validation`)
|
|
71
81
|
end
|
|
72
82
|
|
|
73
83
|
if codec._isDelta then
|
|
@@ -103,26 +113,34 @@ function Packet.define(
|
|
|
103
113
|
end
|
|
104
114
|
end
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
--[[
|
|
117
|
+
Run the send middleware chain. Returns (dropped, value). A nil value
|
|
118
|
+
is a legitimate payload (Lync.nothing); only the explicit Lync.DROP
|
|
119
|
+
sentinel or a hook returning DROP triggers the drop branch.
|
|
120
|
+
]]
|
|
121
|
+
local function applySend(data: any, player: Player?): (boolean, any)
|
|
107
122
|
if not Middleware.hasSendHooks() then
|
|
108
|
-
return data
|
|
123
|
+
return false, data
|
|
109
124
|
end
|
|
110
|
-
|
|
111
125
|
local sendData = Middleware.runSend(data, name, player)
|
|
112
|
-
if typeof(sendData) == "table" and sendData.
|
|
113
|
-
return nil
|
|
126
|
+
if typeof(sendData) == "table" and sendData._lyncKind == "drop" then
|
|
127
|
+
return true, nil
|
|
114
128
|
end
|
|
115
|
-
return sendData
|
|
129
|
+
return false, sendData
|
|
116
130
|
end
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
-- Caller has already run send middleware once for the broadcast.
|
|
133
|
+
local function broadcast(players: { Player }, data: any): ()
|
|
134
|
+
for _, p in players do
|
|
135
|
+
sendToPlayer(p, data)
|
|
121
136
|
end
|
|
137
|
+
bumpFires()
|
|
138
|
+
end
|
|
122
139
|
|
|
140
|
+
local function sendToTarget(data: any, target: any): ()
|
|
123
141
|
if isPlayer(target) then
|
|
124
|
-
local sendData = applySend(data, target :: Player)
|
|
125
|
-
if
|
|
142
|
+
local dropped, sendData = applySend(data, target :: Player)
|
|
143
|
+
if dropped then
|
|
126
144
|
return
|
|
127
145
|
end
|
|
128
146
|
sendToPlayer(target :: Player, sendData)
|
|
@@ -131,57 +149,59 @@ function Packet.define(
|
|
|
131
149
|
end
|
|
132
150
|
|
|
133
151
|
if typeof(target) == "table" then
|
|
134
|
-
local sendData = applySend(data, nil)
|
|
135
|
-
if
|
|
152
|
+
local dropped, sendData = applySend(data, nil)
|
|
153
|
+
if dropped then
|
|
136
154
|
return
|
|
137
155
|
end
|
|
138
156
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
end
|
|
143
|
-
bumpFires()
|
|
157
|
+
local kind = target._lyncKind
|
|
158
|
+
if kind == "all" then
|
|
159
|
+
broadcast(Players:GetPlayers(), sendData)
|
|
144
160
|
return
|
|
145
161
|
end
|
|
146
162
|
|
|
147
|
-
if
|
|
163
|
+
if kind == "except" then
|
|
148
164
|
local excluded = target._excluded :: { [Player]: boolean }
|
|
149
|
-
for _,
|
|
150
|
-
if not excluded[
|
|
151
|
-
sendToPlayer(
|
|
165
|
+
for _, p in Players:GetPlayers() do
|
|
166
|
+
if not excluded[p] then
|
|
167
|
+
sendToPlayer(p, sendData)
|
|
152
168
|
end
|
|
153
169
|
end
|
|
154
170
|
bumpFires()
|
|
155
171
|
return
|
|
156
172
|
end
|
|
157
173
|
|
|
158
|
-
if
|
|
159
|
-
for
|
|
160
|
-
sendToPlayer(
|
|
174
|
+
if kind == "group" then
|
|
175
|
+
for p in target._members :: { [Player]: boolean } do
|
|
176
|
+
sendToPlayer(p, sendData)
|
|
161
177
|
end
|
|
162
178
|
bumpFires()
|
|
163
179
|
return
|
|
164
180
|
end
|
|
165
181
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
182
|
+
-- Plain { Player } array.
|
|
183
|
+
for _, p in target do
|
|
184
|
+
if isPlayer(p) then
|
|
185
|
+
sendToPlayer(p :: Player, sendData)
|
|
169
186
|
end
|
|
170
187
|
end
|
|
171
188
|
bumpFires()
|
|
172
189
|
return
|
|
173
190
|
end
|
|
174
191
|
|
|
175
|
-
error(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
192
|
+
Log.error(`"{name}" expected Player, table, Group, or Lync.all, got {typeof(target)}`)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
local function sendFromServer(data: any, target: any): ()
|
|
196
|
+
if target == nil then
|
|
197
|
+
Log.error(`"{name}" server send requires a target`)
|
|
198
|
+
end
|
|
199
|
+
sendToTarget(data, target)
|
|
180
200
|
end
|
|
181
201
|
|
|
182
202
|
local function sendFromClient(data: any): ()
|
|
183
|
-
local sendData = applySend(data, nil)
|
|
184
|
-
if
|
|
203
|
+
local dropped, sendData = applySend(data, nil)
|
|
204
|
+
if dropped then
|
|
185
205
|
return
|
|
186
206
|
end
|
|
187
207
|
local ch = Transport.client().getChannel(isUnreliable)
|
|
@@ -189,8 +209,11 @@ function Packet.define(
|
|
|
189
209
|
bumpFires()
|
|
190
210
|
end
|
|
191
211
|
|
|
212
|
+
type Self = PacketHandle<T>
|
|
213
|
+
type Listener = (data: T, sender: Player?, timestamp: number?) -> ()
|
|
214
|
+
|
|
192
215
|
local handle = {
|
|
193
|
-
send = function(_self:
|
|
216
|
+
send = function(_self: Self, data: T, target: any?): ()
|
|
194
217
|
if IS_SERVER then
|
|
195
218
|
sendFromServer(data, target)
|
|
196
219
|
else
|
|
@@ -198,11 +221,11 @@ function Packet.define(
|
|
|
198
221
|
end
|
|
199
222
|
end,
|
|
200
223
|
|
|
201
|
-
on = function(_self:
|
|
202
|
-
return reg.signal:connect(fn)
|
|
224
|
+
on = function(_self: Self, fn: Listener): Types.Connection
|
|
225
|
+
return reg.signal:connect(fn :: any)
|
|
203
226
|
end,
|
|
204
227
|
|
|
205
|
-
once = function(_self:
|
|
228
|
+
once = function(_self: Self, fn: Listener): Types.Connection
|
|
206
229
|
local conn: Types.Connection
|
|
207
230
|
conn = reg.signal:connect(function(...: any)
|
|
208
231
|
conn:disconnect()
|
|
@@ -211,15 +234,15 @@ function Packet.define(
|
|
|
211
234
|
return conn
|
|
212
235
|
end,
|
|
213
236
|
|
|
214
|
-
wait = function(_self:
|
|
237
|
+
wait = function(_self: Self): (T, Player?, number?)
|
|
215
238
|
return reg.signal:wait()
|
|
216
239
|
end,
|
|
217
240
|
|
|
218
|
-
name = function(_self:
|
|
241
|
+
name = function(_self: Self): string
|
|
219
242
|
return reg.name
|
|
220
243
|
end,
|
|
221
244
|
|
|
222
|
-
stats = function(_self:
|
|
245
|
+
stats = function(_self: Self): Types.PacketStats
|
|
223
246
|
return {
|
|
224
247
|
bytesSent = reg.bytesSent,
|
|
225
248
|
bytesReceived = reg.bytesReceived,
|
|
@@ -230,7 +253,7 @@ function Packet.define(
|
|
|
230
253
|
end,
|
|
231
254
|
}
|
|
232
255
|
|
|
233
|
-
return table.freeze(handle) :: PacketHandle
|
|
256
|
+
return table.freeze(handle) :: PacketHandle<T>
|
|
234
257
|
end
|
|
235
258
|
|
|
236
259
|
return table.freeze(Packet)
|
package/src/api/Query.luau
CHANGED
|
@@ -6,54 +6,108 @@ 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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
81
|
+
--[[
|
|
82
|
+
Server `request(data, target)` returns `Resp?` for a Player target and
|
|
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.
|
|
85
|
+
]]
|
|
86
|
+
export type QueryHandle<Req, Resp> = {
|
|
87
|
+
handle: (
|
|
88
|
+
self: QueryHandle<Req, Resp>,
|
|
89
|
+
fn: (request: Req, player: Player?) -> Resp?
|
|
90
|
+
) -> Types.Connection,
|
|
91
|
+
request: (self: QueryHandle<Req, Resp>, data: Req, target: any?) -> Resp?,
|
|
92
|
+
name: (self: QueryHandle<Req, Resp>) -> string,
|
|
93
|
+
stats: (self: QueryHandle<Req, Resp>) -> Types.PacketStats,
|
|
41
94
|
}
|
|
42
95
|
|
|
43
96
|
-- Public -----------------------------------------------------------------
|
|
44
97
|
|
|
45
98
|
local Query = {}
|
|
46
99
|
|
|
47
|
-
function Query.define(
|
|
100
|
+
function Query.define<Req, Resp>(
|
|
48
101
|
name: string,
|
|
49
|
-
requestCodec: Types.InternalCodec<
|
|
50
|
-
responseCodec: Types.InternalCodec<
|
|
102
|
+
requestCodec: Types.InternalCodec<Req>,
|
|
103
|
+
responseCodec: Types.InternalCodec<Resp>,
|
|
51
104
|
options: Types.QueryOptions?
|
|
52
|
-
): QueryHandle
|
|
53
|
-
local opts = options or
|
|
105
|
+
): QueryHandle<Req, Resp>
|
|
106
|
+
local opts = options or EMPTY_OPTIONS
|
|
54
107
|
local timeout = opts.timeout or DEFAULT_TIMEOUT
|
|
55
108
|
|
|
56
109
|
local reqOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, name)
|
|
110
|
+
-- Embedded "\0resp" suffix stays uniquely paired with the request reg.
|
|
57
111
|
local respName = `{name}\0resp`
|
|
58
112
|
local respOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, respName)
|
|
59
113
|
|
|
@@ -89,6 +143,7 @@ function Query.define(
|
|
|
89
143
|
|
|
90
144
|
reqReg.partner = respReg.id
|
|
91
145
|
|
|
146
|
+
-- Single active handler; replacing it disconnects the prior one via handlerToken.
|
|
92
147
|
local activeHandler: ((any, Player?) -> any)? = nil
|
|
93
148
|
local handlerToken = 0
|
|
94
149
|
|
|
@@ -113,18 +168,22 @@ function Query.define(
|
|
|
113
168
|
end
|
|
114
169
|
|
|
115
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).
|
|
116
175
|
local payloadCodec = if ok then responseCodec else nil
|
|
117
176
|
local payloadData = if ok then response else nil
|
|
118
177
|
|
|
119
|
-
local ch = if IS_SERVER
|
|
120
|
-
then Transport.server().getChannel(sender, false)
|
|
121
|
-
|
|
122
|
-
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)
|
|
123
181
|
if ch then
|
|
124
|
-
|
|
182
|
+
writeQueryTracked(ch, respReg, corrId, payloadCodec, payloadData)
|
|
125
183
|
end
|
|
126
184
|
end)
|
|
127
185
|
|
|
186
|
+
-- Resolve the waiting thread with nil after `timeout` seconds.
|
|
128
187
|
local function scheduleSingleTimeout(corrId: number, running: thread): ()
|
|
129
188
|
task.delay(timeout, function()
|
|
130
189
|
if _pending[corrId] == running then
|
|
@@ -134,10 +193,10 @@ function Query.define(
|
|
|
134
193
|
end)
|
|
135
194
|
end
|
|
136
195
|
|
|
137
|
-
|
|
196
|
+
-- Issue a single-target query on `ch` and yield until reply or timeout.
|
|
197
|
+
local function requestOnChannel(ch: Types.ChannelState, data: any): any
|
|
138
198
|
local corrId = nextCorr()
|
|
139
|
-
|
|
140
|
-
Channel.writeQuery(ch, reqReg.id, corrId, requestCodec, data)
|
|
199
|
+
writeQueryTracked(ch, reqReg, corrId, requestCodec, data)
|
|
141
200
|
|
|
142
201
|
local running = coroutine.running()
|
|
143
202
|
_pending[corrId] = running
|
|
@@ -146,30 +205,41 @@ function Query.define(
|
|
|
146
205
|
end
|
|
147
206
|
|
|
148
207
|
local function requestMulti(targets: { Player }, data: any): { [Player]: any }
|
|
149
|
-
|
|
208
|
+
local targetCount = #targets
|
|
209
|
+
if targetCount == 0 then
|
|
150
210
|
return {}
|
|
151
211
|
end
|
|
152
212
|
|
|
153
213
|
local results: { [Player]: any } = {}
|
|
154
|
-
local remaining =
|
|
214
|
+
local remaining = targetCount
|
|
155
215
|
local running = coroutine.running()
|
|
156
216
|
local resolved = false
|
|
217
|
+
local corrIds = table.create(targetCount)
|
|
157
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
|
+
]]
|
|
158
224
|
local function resolveOnce(): ()
|
|
159
225
|
if resolved then
|
|
160
226
|
return
|
|
161
227
|
end
|
|
162
228
|
resolved = true
|
|
229
|
+
for i = 1, targetCount do
|
|
230
|
+
_pendingGather[corrIds[i]] = nil
|
|
231
|
+
end
|
|
163
232
|
task.spawn(running, results)
|
|
164
233
|
end
|
|
165
234
|
|
|
166
235
|
local Server = Transport.server()
|
|
167
|
-
for
|
|
236
|
+
for i, p in targets do
|
|
168
237
|
local pCorr = nextCorr()
|
|
169
|
-
|
|
170
|
-
|
|
238
|
+
corrIds[i] = pCorr
|
|
239
|
+
local ch = Server.getChannel(p, false)
|
|
240
|
+
writeQueryTracked(ch, reqReg, pCorr, requestCodec, data)
|
|
171
241
|
|
|
172
|
-
local pRef =
|
|
242
|
+
local pRef = p
|
|
173
243
|
_pendingGather[pCorr] = function(response: any)
|
|
174
244
|
results[pRef] = response
|
|
175
245
|
remaining -= 1
|
|
@@ -183,85 +253,64 @@ function Query.define(
|
|
|
183
253
|
return coroutine.yield()
|
|
184
254
|
end
|
|
185
255
|
|
|
186
|
-
local function collectTargets(target: any): { Player }
|
|
187
|
-
local targets: { Player } = {}
|
|
188
|
-
if typeof(target) ~= "table" then
|
|
189
|
-
return targets
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
if target._lyncAll then
|
|
193
|
-
for _, p in Players:GetPlayers() do
|
|
194
|
-
table.insert(targets, p)
|
|
195
|
-
end
|
|
196
|
-
elseif target._lyncGroup then
|
|
197
|
-
for player in target._members do
|
|
198
|
-
table.insert(targets, player)
|
|
199
|
-
end
|
|
200
|
-
else
|
|
201
|
-
for _, p in target do
|
|
202
|
-
if isPlayer(p) then
|
|
203
|
-
table.insert(targets, p :: Player)
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
return targets
|
|
208
|
-
end
|
|
209
|
-
|
|
210
256
|
local function requestFromServer(data: any, target: any): any
|
|
211
257
|
if target == nil then
|
|
212
|
-
error(`
|
|
258
|
+
Log.error(`"{name}" server request requires a target`)
|
|
213
259
|
end
|
|
214
260
|
if isPlayer(target) then
|
|
215
|
-
return
|
|
261
|
+
return requestOnChannel(Transport.server().getChannel(target :: Player, false), data)
|
|
216
262
|
end
|
|
217
263
|
return requestMulti(collectTargets(target), data)
|
|
218
264
|
end
|
|
219
265
|
|
|
220
266
|
local function requestFromClient(data: any): any
|
|
221
|
-
|
|
222
|
-
local ch = Transport.client().getChannel(false)
|
|
223
|
-
Channel.writeQuery(ch, reqReg.id, corrId, requestCodec, data)
|
|
224
|
-
|
|
225
|
-
local running = coroutine.running()
|
|
226
|
-
_pending[corrId] = running
|
|
227
|
-
scheduleSingleTimeout(corrId, running)
|
|
228
|
-
return coroutine.yield()
|
|
267
|
+
return requestOnChannel(Transport.client().getChannel(false), data)
|
|
229
268
|
end
|
|
230
269
|
|
|
270
|
+
type Self = QueryHandle<Req, Resp>
|
|
271
|
+
|
|
231
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
|
+
]]
|
|
232
278
|
handle = function(
|
|
233
|
-
_self:
|
|
234
|
-
fn: (request:
|
|
279
|
+
_self: Self,
|
|
280
|
+
fn: (request: Req, player: Player?) -> Resp?
|
|
235
281
|
): Types.Connection
|
|
236
282
|
handlerToken += 1
|
|
237
283
|
local myToken = handlerToken
|
|
238
|
-
activeHandler = fn
|
|
239
|
-
|
|
240
|
-
local conn
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
284
|
+
activeHandler = fn :: any
|
|
285
|
+
|
|
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
|
|
251
300
|
end,
|
|
252
301
|
|
|
253
|
-
request = function(_self:
|
|
302
|
+
request = function(_self: Self, data: Req, target: any?): Resp?
|
|
254
303
|
if IS_SERVER then
|
|
255
304
|
return requestFromServer(data, target)
|
|
256
305
|
end
|
|
257
306
|
return requestFromClient(data)
|
|
258
307
|
end,
|
|
259
308
|
|
|
260
|
-
name = function(_self:
|
|
309
|
+
name = function(_self: Self): string
|
|
261
310
|
return name
|
|
262
311
|
end,
|
|
263
312
|
|
|
264
|
-
stats = function(_self:
|
|
313
|
+
stats = function(_self: Self): Types.PacketStats
|
|
265
314
|
return {
|
|
266
315
|
bytesSent = reqReg.bytesSent + respReg.bytesSent,
|
|
267
316
|
bytesReceived = reqReg.bytesReceived + respReg.bytesReceived,
|
|
@@ -272,7 +321,7 @@ function Query.define(
|
|
|
272
321
|
end,
|
|
273
322
|
}
|
|
274
323
|
|
|
275
|
-
return table.freeze(handle) :: QueryHandle
|
|
324
|
+
return table.freeze(handle) :: QueryHandle<Req, Resp>
|
|
276
325
|
end
|
|
277
326
|
|
|
278
327
|
function Query.pendingCount(): number
|