@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.
- package/README.md +300 -0
- package/package.json +38 -0
- package/src/Types.luau +63 -0
- package/src/api/Group.luau +126 -0
- package/src/api/Namespace.luau +226 -0
- package/src/api/Packet.luau +147 -0
- package/src/api/Query.luau +295 -0
- package/src/api/Signal.luau +224 -0
- package/src/codec/Base.luau +49 -0
- package/src/codec/composite/Array.luau +275 -0
- package/src/codec/composite/Map.luau +395 -0
- package/src/codec/composite/Optional.luau +47 -0
- package/src/codec/composite/Shared.luau +151 -0
- package/src/codec/composite/Struct.luau +440 -0
- package/src/codec/composite/Tagged.luau +222 -0
- package/src/codec/composite/Tuple.luau +143 -0
- package/src/codec/datatype/Buffer.luau +44 -0
- package/src/codec/datatype/CFrame.luau +51 -0
- package/src/codec/datatype/Color.luau +22 -0
- package/src/codec/datatype/Instance.luau +48 -0
- package/src/codec/datatype/IntVector.luau +25 -0
- package/src/codec/datatype/NumberRange.luau +14 -0
- package/src/codec/datatype/Ray.luau +27 -0
- package/src/codec/datatype/Rect.luau +21 -0
- package/src/codec/datatype/Region.luau +58 -0
- package/src/codec/datatype/Sequence.luau +129 -0
- package/src/codec/datatype/String.luau +87 -0
- package/src/codec/datatype/UDim.luau +27 -0
- package/src/codec/datatype/Vector.luau +25 -0
- package/src/codec/meta/Auto.luau +353 -0
- package/src/codec/meta/Bitfield.luau +191 -0
- package/src/codec/meta/Custom.luau +27 -0
- package/src/codec/meta/Enum.luau +80 -0
- package/src/codec/meta/Nothing.luau +9 -0
- package/src/codec/meta/Quantized.luau +170 -0
- package/src/codec/meta/Unknown.luau +35 -0
- package/src/codec/primitive/Bool.luau +30 -0
- package/src/codec/primitive/Float16.luau +111 -0
- package/src/codec/primitive/Number.luau +48 -0
- package/src/codec/primitive/Varint.luau +76 -0
- package/src/index.d.ts +279 -0
- package/src/init.luau +161 -0
- package/src/internal/Baseline.luau +41 -0
- package/src/internal/Channel.luau +235 -0
- package/src/internal/Middleware.luau +109 -0
- package/src/internal/Pool.luau +68 -0
- package/src/internal/Registry.luau +146 -0
- package/src/transport/Bridge.luau +66 -0
- package/src/transport/Client.luau +151 -0
- package/src/transport/Gate.luau +222 -0
- package/src/transport/Reader.luau +175 -0
- 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)
|