@axpecter/lync 1.3.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.
Files changed (52) hide show
  1. package/README.md +300 -0
  2. package/package.json +38 -0
  3. package/src/Types.luau +63 -0
  4. package/src/api/Group.luau +126 -0
  5. package/src/api/Namespace.luau +226 -0
  6. package/src/api/Packet.luau +147 -0
  7. package/src/api/Query.luau +295 -0
  8. package/src/api/Signal.luau +224 -0
  9. package/src/codec/Base.luau +49 -0
  10. package/src/codec/composite/Array.luau +275 -0
  11. package/src/codec/composite/Map.luau +395 -0
  12. package/src/codec/composite/Optional.luau +47 -0
  13. package/src/codec/composite/Shared.luau +151 -0
  14. package/src/codec/composite/Struct.luau +440 -0
  15. package/src/codec/composite/Tagged.luau +222 -0
  16. package/src/codec/composite/Tuple.luau +143 -0
  17. package/src/codec/datatype/Buffer.luau +44 -0
  18. package/src/codec/datatype/CFrame.luau +51 -0
  19. package/src/codec/datatype/Color.luau +22 -0
  20. package/src/codec/datatype/Instance.luau +48 -0
  21. package/src/codec/datatype/IntVector.luau +25 -0
  22. package/src/codec/datatype/NumberRange.luau +14 -0
  23. package/src/codec/datatype/Ray.luau +27 -0
  24. package/src/codec/datatype/Rect.luau +21 -0
  25. package/src/codec/datatype/Region.luau +58 -0
  26. package/src/codec/datatype/Sequence.luau +129 -0
  27. package/src/codec/datatype/String.luau +87 -0
  28. package/src/codec/datatype/UDim.luau +27 -0
  29. package/src/codec/datatype/Vector.luau +25 -0
  30. package/src/codec/meta/Auto.luau +353 -0
  31. package/src/codec/meta/Bitfield.luau +191 -0
  32. package/src/codec/meta/Custom.luau +27 -0
  33. package/src/codec/meta/Enum.luau +80 -0
  34. package/src/codec/meta/Nothing.luau +9 -0
  35. package/src/codec/meta/Quantized.luau +170 -0
  36. package/src/codec/meta/Unknown.luau +35 -0
  37. package/src/codec/primitive/Bool.luau +30 -0
  38. package/src/codec/primitive/Float16.luau +111 -0
  39. package/src/codec/primitive/Number.luau +48 -0
  40. package/src/codec/primitive/Varint.luau +76 -0
  41. package/src/index.d.ts +279 -0
  42. package/src/init.luau +161 -0
  43. package/src/internal/Baseline.luau +41 -0
  44. package/src/internal/Channel.luau +235 -0
  45. package/src/internal/Middleware.luau +109 -0
  46. package/src/internal/Pool.luau +68 -0
  47. package/src/internal/Registry.luau +146 -0
  48. package/src/transport/Bridge.luau +66 -0
  49. package/src/transport/Client.luau +151 -0
  50. package/src/transport/Gate.luau +222 -0
  51. package/src/transport/Reader.luau +175 -0
  52. package/src/transport/Server.luau +364 -0
@@ -0,0 +1,226 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Scoped packet/query groups with batch ops and local middleware.
4
+
5
+ local Middleware = require (script.Parent.Parent.internal.Middleware)
6
+ local Packet = require (script.Parent.Packet)
7
+ local Query = require (script.Parent.Query)
8
+ local Types = require (script.Parent.Parent.Types)
9
+
10
+ type Connection = Types.Connection
11
+ type PacketConfig<T> = Types.PacketConfig<T>
12
+ type QueryConfig<R, S> = Types.QueryConfig<R, S>
13
+
14
+ -- Constants --------------------------------------------------------------
15
+
16
+ local MAX_NAMESPACES = 64
17
+
18
+ -- State ------------------------------------------------------------------
19
+
20
+ local _registered = {} :: { [string]: true }
21
+ local _count = 0
22
+
23
+ -- Private ----------------------------------------------------------------
24
+
25
+ type NamespaceConfig = {
26
+ packets: { [string]: PacketConfig<any> }?,
27
+ queries: { [string]: QueryConfig<any, any> }?,
28
+ }
29
+
30
+ local function prefixName (nsName: string, name: string): string
31
+ return `{nsName}.{name}`
32
+ end
33
+
34
+ -- Wraps a middleware handler so it only fires for packets in this namespace.
35
+ local function scopedHandler (
36
+ nsName: string,
37
+ handler: (data: any, name: string, player: Player?) -> any?
38
+ ): (data: any, name: string, player: Player?) -> any?
39
+ local prefix = nsName .. "."
40
+ local prefixLen = #prefix
41
+
42
+ return function (data: any, name: string, player: Player?): any?
43
+ if string.sub (name, 1, prefixLen) ~= prefix then
44
+ return data
45
+ end
46
+ return handler (data, name, player)
47
+ end
48
+ end
49
+
50
+ -- Namespace --------------------------------------------------------------
51
+
52
+ local NamespaceImpl = {}
53
+ NamespaceImpl.__index = NamespaceImpl
54
+
55
+ -- Listens on every packet in this namespace. Returns a Connection.
56
+ function NamespaceImpl.listenAll (
57
+ self: any,
58
+ callback: (name: string, data: any, sender: Player?) -> ()
59
+ ): Connection
60
+ local connections = {} :: { Connection }
61
+ local prefix = self._name .. "."
62
+ local prefixLen = #prefix
63
+
64
+ for fullName, packet in self._packets do
65
+ local shortName = string.sub (fullName, prefixLen + 1)
66
+ local conn = packet:listen (function (data: any, sender: Player?): ()
67
+ callback (shortName, data, sender)
68
+ end)
69
+ table.insert (connections, conn)
70
+ table.insert (self._tracked, conn)
71
+ end
72
+
73
+ local bundle = {
74
+ connected = true,
75
+ disconnect = function (conn: Connection): ()
76
+ if not conn.connected then
77
+ return
78
+ end
79
+ conn.connected = false
80
+
81
+ for _, inner in connections do
82
+ if inner.connected then
83
+ inner:disconnect ()
84
+ end
85
+ end
86
+ end,
87
+ }
88
+
89
+ return bundle :: any
90
+ end
91
+
92
+ -- Disconnects every listenAll connection. Does not remove scoped middleware.
93
+ function NamespaceImpl.disconnectAll (self: any): ()
94
+ for _, conn in self._tracked do
95
+ if conn.connected then
96
+ conn:disconnect ()
97
+ end
98
+ end
99
+ table.clear (self._tracked)
100
+ end
101
+
102
+ -- Registers send middleware scoped to this namespace. Returns a remover.
103
+ function NamespaceImpl.onSend (
104
+ self: any,
105
+ handler: (data: any, name: string, player: Player?) -> any?
106
+ ): () -> ()
107
+ local wrapped = scopedHandler (self._name, handler)
108
+ local remover = Middleware.addSend (wrapped)
109
+ table.insert (self._removers, remover)
110
+
111
+ return function (): ()
112
+ remover ()
113
+ local idx = table.find (self._removers, remover)
114
+ if idx then
115
+ table.remove (self._removers, idx)
116
+ end
117
+ end
118
+ end
119
+
120
+ -- Registers receive middleware scoped to this namespace. Returns a remover.
121
+ function NamespaceImpl.onReceive (
122
+ self: any,
123
+ handler: (data: any, name: string, player: Player?) -> any?
124
+ ): () -> ()
125
+ local wrapped = scopedHandler (self._name, handler)
126
+ local remover = Middleware.addReceive (wrapped)
127
+ table.insert (self._removers, remover)
128
+
129
+ return function (): ()
130
+ remover ()
131
+ local idx = table.find (self._removers, remover)
132
+ if idx then
133
+ table.remove (self._removers, idx)
134
+ end
135
+ end
136
+ end
137
+
138
+ -- Kills all listeners and removes all scoped middleware. Full cleanup.
139
+ function NamespaceImpl.destroy (self: any): ()
140
+ self:disconnectAll ()
141
+
142
+ for _, remover in self._removers do
143
+ remover ()
144
+ end
145
+ table.clear (self._removers)
146
+ end
147
+
148
+ -- Returns every packet name (without prefix) in this namespace.
149
+ function NamespaceImpl.packetNames (self: any): { string }
150
+ local result = {} :: { string }
151
+ local prefix = self._name .. "."
152
+ local prefixLen = #prefix
153
+
154
+ for fullName in self._packets do
155
+ table.insert (result, string.sub (fullName, prefixLen + 1))
156
+ end
157
+
158
+ table.sort (result)
159
+ return result
160
+ end
161
+
162
+ -- Returns every query name (without prefix) in this namespace.
163
+ function NamespaceImpl.queryNames (self: any): { string }
164
+ local result = {} :: { string }
165
+ local prefix = self._name .. "."
166
+ local prefixLen = #prefix
167
+
168
+ for fullName in self._queries do
169
+ table.insert (result, string.sub (fullName, prefixLen + 1))
170
+ end
171
+
172
+ table.sort (result)
173
+ return result
174
+ end
175
+
176
+ -- Public -----------------------------------------------------------------
177
+
178
+ local Namespace = {}
179
+
180
+ -- Defines a namespace. Names are auto-prefixed to prevent collisions.
181
+ function Namespace.define (name: string, config: NamespaceConfig): any
182
+ if #name == 0 then
183
+ error ("[Lync] Namespace name must not be empty")
184
+ end
185
+ if _registered[name] then
186
+ error (`[Lync] Duplicate namespace: "{name}"`)
187
+ end
188
+ if _count >= MAX_NAMESPACES then
189
+ error (`[Lync] Namespace limit reached: {MAX_NAMESPACES}`)
190
+ end
191
+
192
+ _count += 1
193
+ _registered[name] = true
194
+
195
+ local packets = {} :: { [string]: any }
196
+ local queries = {} :: { [string]: any }
197
+ local fields = {} :: { [string]: any }
198
+
199
+ fields._name = name
200
+ fields._packets = packets
201
+ fields._queries = queries
202
+ fields._tracked = {} :: { Connection }
203
+ fields._removers = {} :: { () -> () }
204
+
205
+ if config.packets then
206
+ for shortName, packetConfig in config.packets do
207
+ local fullName = prefixName (name, shortName)
208
+ local packet = Packet.define (fullName, packetConfig)
209
+ packets[fullName] = packet
210
+ fields[shortName] = packet
211
+ end
212
+ end
213
+
214
+ if config.queries then
215
+ for shortName, queryConfig in config.queries do
216
+ local fullName = prefixName (name, shortName)
217
+ local query = Query.define (fullName, queryConfig)
218
+ queries[fullName] = query
219
+ fields[shortName] = query
220
+ end
221
+ end
222
+
223
+ return setmetatable (fields, NamespaceImpl)
224
+ end
225
+
226
+ return table.freeze (Namespace)
@@ -0,0 +1,147 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Packet definition and send/listen API.
4
+
5
+ local RunService = game:GetService ("RunService")
6
+
7
+ local Client = require (script.Parent.Parent.transport.Client)
8
+ local Registry = require (script.Parent.Parent.internal.Registry)
9
+ local Server = require (script.Parent.Parent.transport.Server)
10
+ local Signal = require (script.Parent.Signal)
11
+ local Types = require (script.Parent.Parent.Types)
12
+
13
+ type Connection = Types.Connection
14
+ type PacketConfig<T> = Types.PacketConfig<T>
15
+
16
+ -- Constants --------------------------------------------------------------
17
+
18
+ local IS_SERVER = RunService:IsServer ()
19
+
20
+ local serverWriteTo = Server.writeTo
21
+ local serverWriteToAll = Server.writeToAll
22
+ local serverWriteToList = Server.writeToList
23
+ local serverWriteToAllExcept = Server.writeToAllExcept
24
+ local serverWriteToGroup = Server.writeToGroup
25
+ local clientWrite = Client.write
26
+
27
+ -- Private ----------------------------------------------------------------
28
+
29
+ -- Shared methods (both contexts)
30
+ local SharedImpl = {}
31
+
32
+ function SharedImpl.listen (self: any, callback: (...any) -> ()): Connection
33
+ return self._signal:connect (callback)
34
+ end
35
+
36
+ function SharedImpl.once (self: any, callback: (...any) -> ()): Connection
37
+ return self._signal:once (callback)
38
+ end
39
+
40
+ function SharedImpl.wait (self: any): ...any
41
+ return self._signal:wait ()
42
+ end
43
+
44
+ function SharedImpl.disconnectAll (self: any): ()
45
+ self._signal:disconnectAll ()
46
+ end
47
+
48
+ -- Server-only metatable . Has sendTo/sendToAll, no send().
49
+ local ServerImpl = setmetatable ({}, { __index = SharedImpl })
50
+ ServerImpl.__index = ServerImpl
51
+
52
+ local DELTA_TARGETED = 1
53
+ local DELTA_BROADCAST = 2
54
+
55
+ local function checkDeltaMode (self: any, mode: number): ()
56
+ if not self._isDelta then
57
+ return
58
+ end
59
+ local current = self._deltaMode
60
+ if current == 0 then
61
+ self._deltaMode = mode
62
+ elseif current ~= mode then
63
+ error (`[Lync] Delta packet cannot mix targeted and broadcast: "{self._name}"`)
64
+ end
65
+ end
66
+
67
+ function ServerImpl.sendTo (self: any, data: any, player: Player): ()
68
+ checkDeltaMode (self, DELTA_TARGETED)
69
+ serverWriteTo (player, self._id, self._name, self._codec, data, self._isUnreliable)
70
+ end
71
+
72
+ function ServerImpl.sendToAll (self: any, data: any): ()
73
+ checkDeltaMode (self, DELTA_BROADCAST)
74
+ serverWriteToAll (self._id, self._name, self._codec, data, self._isUnreliable)
75
+ end
76
+
77
+ function ServerImpl.sendToList (self: any, data: any, players: { Player }): ()
78
+ checkDeltaMode (self, DELTA_TARGETED)
79
+ serverWriteToList (players, self._id, self._name, self._codec, data, self._isUnreliable)
80
+ end
81
+
82
+ function ServerImpl.sendToAllExcept (self: any, data: any, except: Player): ()
83
+ checkDeltaMode (self, DELTA_BROADCAST)
84
+ serverWriteToAllExcept (except, self._id, self._name, self._codec, data, self._isUnreliable)
85
+ end
86
+
87
+ function ServerImpl.sendToGroup (self: any, data: any, groupName: string): ()
88
+ checkDeltaMode (self, DELTA_BROADCAST)
89
+ serverWriteToGroup (groupName, self._id, self._name, self._codec, data, self._isUnreliable)
90
+ end
91
+
92
+ table.freeze (ServerImpl)
93
+
94
+ -- Client-only metatable . Has send(), no sendTo/sendToAll
95
+ local ClientImpl = setmetatable ({}, { __index = SharedImpl })
96
+ ClientImpl.__index = ClientImpl
97
+
98
+ function ClientImpl.send (self: any, data: any): ()
99
+ clientWrite (self._id, self._name, self._codec, data, self._isUnreliable)
100
+ end
101
+
102
+ table.freeze (ClientImpl)
103
+
104
+ -- Public -----------------------------------------------------------------
105
+
106
+ local Packet = {}
107
+
108
+ function Packet.define (name: string, config: PacketConfig<any>): any
109
+ if #name == 0 then
110
+ error ("[Lync] Packet name must not be empty")
111
+ end
112
+ if not config.value then
113
+ error (`[Lync] Packet requires a value codec: "{name}"`)
114
+ end
115
+
116
+ local isUnreliable = config.unreliable or false
117
+
118
+ if (config.value :: any)._isDelta and isUnreliable then
119
+ error (`[Lync] Delta codec requires reliable delivery: "{name}"`)
120
+ end
121
+
122
+ local signal = Signal.create ()
123
+
124
+ local reg = Registry.register (
125
+ name,
126
+ config.value,
127
+ isUnreliable,
128
+ signal,
129
+ config.rateLimit,
130
+ config.validate,
131
+ config.maxPayloadBytes
132
+ )
133
+
134
+ local isDelta = (config.value :: any)._isDelta == true
135
+
136
+ return setmetatable ({
137
+ _id = reg.id,
138
+ _name = reg.name,
139
+ _codec = reg.codec,
140
+ _isUnreliable = isUnreliable,
141
+ _isDelta = isDelta,
142
+ _deltaMode = 0,
143
+ _signal = signal,
144
+ }, if IS_SERVER then ServerImpl else ClientImpl)
145
+ end
146
+
147
+ return table.freeze (Packet)
@@ -0,0 +1,295 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Bidirectional request-reply over RemoteEvents.
4
+
5
+ local Players = game:GetService ("Players")
6
+ local RunService = game:GetService ("RunService")
7
+
8
+ local Client = require (script.Parent.Parent.transport.Client)
9
+ local Group = require (script.Parent.Group)
10
+ local Registry = require (script.Parent.Parent.internal.Registry)
11
+ local Server = require (script.Parent.Parent.transport.Server)
12
+ local Signal = require (script.Parent.Signal)
13
+ local Types = require (script.Parent.Parent.Types)
14
+
15
+ type Connection = Types.Connection
16
+ type QueryConfig<R, S> = Types.QueryConfig<R, S>
17
+
18
+ -- Constants --------------------------------------------------------------
19
+
20
+ local IS_SERVER = RunService:IsServer ()
21
+
22
+ -- Wire ceiling: correlationId is u16 in Channel.writeQuery / Reader.process.
23
+ local POOL_SIZE = 65536
24
+
25
+ -- State ------------------------------------------------------------------
26
+
27
+ type PendingQuery = {
28
+ thread: thread?,
29
+ callback: ((data: any) -> ())?,
30
+ timeout: thread?,
31
+ }
32
+
33
+ local _pending = {} :: { [number]: PendingQuery }
34
+ local _nextId = 0
35
+ local _activeCount = 0
36
+
37
+ -- Private ----------------------------------------------------------------
38
+
39
+ local function allocCorrelation (): number
40
+ if _activeCount >= POOL_SIZE then
41
+ error ("[Lync] All 65536 query correlation slots exhausted")
42
+ end
43
+
44
+ local id = _nextId
45
+
46
+ -- Skip occupied slots. With 65536 capacity and typical single-digit
47
+ -- concurrency, this loop body almost never executes.
48
+ while _pending[id] do
49
+ id = (id + 1) % POOL_SIZE
50
+ end
51
+
52
+ _nextId = (id + 1) % POOL_SIZE
53
+ _activeCount += 1
54
+ return id
55
+ end
56
+
57
+ local function freeCorrelation (): ()
58
+ _activeCount -= 1
59
+ end
60
+
61
+ local function completeQuery (id: number, data: any): ()
62
+ local entry = _pending[id]
63
+ if not entry then
64
+ return
65
+ end
66
+
67
+ _pending[id] = nil
68
+ freeCorrelation ()
69
+
70
+ if entry.timeout then
71
+ task.cancel (entry.timeout)
72
+ end
73
+
74
+ if entry.callback then
75
+ entry.callback (data)
76
+ elseif entry.thread then
77
+ coroutine.resume (entry.thread :: thread, data)
78
+ end
79
+ end
80
+
81
+ local function onTimeout (correlation: number): ()
82
+ completeQuery (correlation, nil)
83
+ end
84
+
85
+ -- Query ------------------------------------------------------------------
86
+
87
+ local QueryImpl = {}
88
+ QueryImpl.__index = QueryImpl
89
+
90
+ function QueryImpl.listen (self: any, callback: (...any) -> any): Connection
91
+ local respId = self._respReg.id
92
+ local respName = self._name
93
+ local respCodec = self._respReg.codec
94
+
95
+ if IS_SERVER then
96
+ return self._reqReg.signal:connect (
97
+ function (request: any, player: Player, correlation: number): ()
98
+ local ok, response = pcall (callback, request, player)
99
+ if not ok then
100
+ warn (`[Lync] Query handler error on "{respName}": {response}`)
101
+ end
102
+
103
+ if ok and response ~= nil then
104
+ Server.writeQuery (player, respId, respName, correlation, respCodec, response)
105
+ else
106
+ Server.writeQueryNil (player, respId, respName, correlation)
107
+ end
108
+ end
109
+ )
110
+ else
111
+ return self._reqReg.signal:connect (
112
+ function (request: any, _player: Player?, correlation: number): ()
113
+ local ok, response = pcall (callback, request)
114
+ if not ok then
115
+ warn (`[Lync] Query handler error on "{respName}": {response}`)
116
+ end
117
+
118
+ if ok and response ~= nil then
119
+ Client.writeQuery (respId, respName, correlation, respCodec, response)
120
+ else
121
+ Client.writeQueryNil (respId, respName, correlation)
122
+ end
123
+ end
124
+ )
125
+ end
126
+ end
127
+
128
+ function QueryImpl.invoke (self: any, request: any, player: Player?): any
129
+ if IS_SERVER and not player then
130
+ error ("[Lync] Query:invoke on server requires a player argument")
131
+ end
132
+
133
+ local correlation = allocCorrelation ()
134
+ local thread = coroutine.running ()
135
+
136
+ local entry: PendingQuery = { thread = thread, timeout = nil }
137
+ _pending[correlation] = entry
138
+
139
+ if IS_SERVER then
140
+ Server.writeQuery (
141
+ player :: Player,
142
+ self._reqReg.id,
143
+ self._name,
144
+ correlation,
145
+ self._reqReg.codec,
146
+ request
147
+ )
148
+ else
149
+ Client.writeQuery (self._reqReg.id, self._name, correlation, self._reqReg.codec, request)
150
+ end
151
+
152
+ entry.timeout = task.delay (self._timeout, onTimeout, correlation)
153
+ return coroutine.yield ()
154
+ end
155
+
156
+ -- Send a query to multiple players and yield. Returns { [Player]: response? }.
157
+ local function invokeMulti (self: any, players: { Player }, request: any): { [Player]: any? }
158
+ if not IS_SERVER then
159
+ error ("[Lync] Multi-invoke is server-only")
160
+ end
161
+
162
+ local count = #players
163
+ if count == 0 then
164
+ return {}
165
+ end
166
+
167
+ if count == 1 then
168
+ local result = QueryImpl.invoke (self, request, players[1])
169
+ return { [players[1]] = result }
170
+ end
171
+
172
+ local thread = coroutine.running ()
173
+ local results = {} :: { [Player]: any? }
174
+ local remaining = count
175
+ local correlations = table.create (count) :: { number }
176
+ local finished = false
177
+
178
+ local reqId = self._reqReg.id
179
+ local reqName = self._name
180
+ local reqCodec = self._reqReg.codec
181
+ local timeout = self._timeout
182
+
183
+ for i = 1, count do
184
+ local player = players[i]
185
+ local correlation = allocCorrelation ()
186
+ correlations[i] = correlation
187
+
188
+ local entry: PendingQuery = {
189
+ thread = nil,
190
+ callback = function (data: any): ()
191
+ results[player] = data
192
+ remaining -= 1
193
+ if remaining == 0 and not finished then
194
+ finished = true
195
+ task.spawn (thread, results)
196
+ end
197
+ end,
198
+ timeout = nil,
199
+ }
200
+
201
+ _pending[correlation] = entry
202
+
203
+ Server.writeQuery (player, reqId, reqName, correlation, reqCodec, request)
204
+ end
205
+
206
+ local timeoutThread = task.delay (timeout, function (): ()
207
+ if finished then
208
+ return
209
+ end
210
+ finished = true
211
+
212
+ for i = 1, count do
213
+ local cid = correlations[i]
214
+ local entry = _pending[cid]
215
+ if entry then
216
+ _pending[cid] = nil
217
+ freeCorrelation ()
218
+ end
219
+ end
220
+
221
+ task.spawn (thread, results)
222
+ end)
223
+
224
+ for i = 1, count do
225
+ local cid = correlations[i]
226
+ local entry = _pending[cid]
227
+ if entry then
228
+ entry.timeout = timeoutThread
229
+ end
230
+ end
231
+
232
+ return coroutine.yield ()
233
+ end
234
+
235
+ function QueryImpl.invokeAll (self: any, request: any): { [Player]: any? }
236
+ local players = Players:GetPlayers ()
237
+ return invokeMulti (self, players, request)
238
+ end
239
+
240
+ function QueryImpl.invokeList (self: any, request: any, players: { Player }): { [Player]: any? }
241
+ return invokeMulti (self, players, request)
242
+ end
243
+
244
+ function QueryImpl.invokeGroup (self: any, request: any, groupName: string): { [Player]: any? }
245
+ local set = Group.getSet (groupName)
246
+ local players = {} :: { Player }
247
+ for player in set do
248
+ table.insert (players, player)
249
+ end
250
+ return invokeMulti (self, players, request)
251
+ end
252
+
253
+ -- Public -----------------------------------------------------------------
254
+
255
+ local Query = {}
256
+
257
+ function Query.define (name: string, config: QueryConfig<any, any>): any
258
+ if not config.request then
259
+ error (`[Lync] Query requires a request codec: "{name}"`)
260
+ end
261
+ if not config.response then
262
+ error (`[Lync] Query requires a response codec: "{name}"`)
263
+ end
264
+
265
+ local reqSignal = Signal.create ()
266
+ local respSignal = Signal.create ()
267
+
268
+ local reqReg, respReg = Registry.registerQueryPair (
269
+ name,
270
+ config.request,
271
+ config.response,
272
+ reqSignal,
273
+ respSignal,
274
+ config.rateLimit,
275
+ config.validate
276
+ )
277
+
278
+ respSignal:connect (function (resp: any, correlation: number): ()
279
+ completeQuery (correlation, resp)
280
+ end)
281
+
282
+ return setmetatable ({
283
+ _name = name,
284
+ _reqReg = reqReg,
285
+ _respReg = respReg,
286
+ _timeout = config.timeout or 5,
287
+ }, QueryImpl)
288
+ end
289
+
290
+ -- Returns the number of queries currently awaiting a response.
291
+ function Query.pendingCount (): number
292
+ return _activeCount
293
+ end
294
+
295
+ return table.freeze (Query)