@axpecter/lync 2.0.0 → 2.1.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 (55) hide show
  1. package/README.md +475 -426
  2. package/package.json +1 -1
  3. package/src/Types.luau +63 -31
  4. package/src/api/Group.luau +101 -106
  5. package/src/api/Packet.luau +187 -186
  6. package/src/api/Query.luau +223 -284
  7. package/src/api/Scope.luau +46 -57
  8. package/src/api/Signal.luau +104 -137
  9. package/src/codec/Base.luau +69 -20
  10. package/src/codec/composite/Array.luau +179 -270
  11. package/src/codec/composite/Map.luau +97 -347
  12. package/src/codec/composite/Optional.luau +27 -24
  13. package/src/codec/composite/Shared.luau +96 -65
  14. package/src/codec/composite/Struct.luau +200 -360
  15. package/src/codec/composite/Tagged.luau +62 -187
  16. package/src/codec/composite/Tuple.luau +79 -103
  17. package/src/codec/datatype/Buffer.luau +49 -21
  18. package/src/codec/datatype/CFrame.luau +207 -30
  19. package/src/codec/datatype/Color.luau +21 -11
  20. package/src/codec/datatype/Instance.luau +28 -21
  21. package/src/codec/datatype/IntVector.luau +33 -21
  22. package/src/codec/datatype/NumberRange.luau +19 -10
  23. package/src/codec/datatype/Ray.luau +26 -22
  24. package/src/codec/datatype/Rect.luau +20 -16
  25. package/src/codec/datatype/Region.luau +51 -45
  26. package/src/codec/datatype/Sequence.luau +64 -56
  27. package/src/codec/datatype/String.luau +89 -56
  28. package/src/codec/datatype/UDim.luau +40 -22
  29. package/src/codec/datatype/Vector.luau +181 -17
  30. package/src/codec/meta/Auto.luau +295 -253
  31. package/src/codec/meta/Bitfield.luau +104 -148
  32. package/src/codec/meta/Custom.luau +8 -4
  33. package/src/codec/meta/Enum.luau +29 -57
  34. package/src/codec/meta/Float.luau +64 -0
  35. package/src/codec/meta/Nothing.luau +9 -5
  36. package/src/codec/meta/Unknown.luau +44 -36
  37. package/src/codec/primitive/Bool.luau +16 -26
  38. package/src/codec/primitive/Float16.luau +58 -64
  39. package/src/codec/primitive/Int.luau +68 -0
  40. package/src/codec/primitive/Number.luau +21 -43
  41. package/src/codec/primitive/Varint.luau +67 -46
  42. package/src/index.d.ts +249 -335
  43. package/src/init.luau +319 -207
  44. package/src/internal/Baseline.luau +29 -24
  45. package/src/internal/Channel.luau +333 -278
  46. package/src/internal/Middleware.luau +60 -85
  47. package/src/internal/Pool.luau +19 -30
  48. package/src/internal/Registry.luau +56 -113
  49. package/src/transport/Bridge.luau +64 -43
  50. package/src/transport/Client.luau +59 -170
  51. package/src/transport/Gate.luau +282 -142
  52. package/src/transport/Reader.luau +148 -140
  53. package/src/transport/Server.luau +138 -488
  54. package/src/api/Namespace.luau +0 -256
  55. package/src/codec/meta/Quantized.luau +0 -174
@@ -1,348 +1,287 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Bidirectional request-reply over RemoteEvents.
3
+ -- Query definition: handle, request with correlation IDs and timeout.
4
4
 
5
5
  local Players = game:GetService("Players")
6
6
  local RunService = game:GetService("RunService")
7
7
 
8
- local Client = require(script.Parent.Parent.transport.Client)
8
+ local Baseline = require(script.Parent.Parent.internal.Baseline)
9
+ local Channel = require(script.Parent.Parent.internal.Channel)
9
10
  local Registry = require(script.Parent.Parent.internal.Registry)
10
- local Server = require(script.Parent.Parent.transport.Server)
11
- local Signal = require(script.Parent.Signal)
12
11
  local Types = require(script.Parent.Parent.Types)
13
12
 
14
- type Connection = Types.Connection
15
- type QueryConfig<R, S> = Types.QueryConfig<R, S>
16
- type Registration = Types.Registration
17
-
18
13
  -- Constants -----------------------------------------------------------
19
14
 
20
15
  local IS_SERVER = RunService:IsServer()
21
-
22
- -- Wire ceiling: correlationId is u16 in Channel.writeQuery / Reader.process.
23
- local POOL_SIZE = 65536
16
+ local DEFAULT_TIMEOUT = 5
24
17
 
25
18
  -- State ---------------------------------------------------------------
26
19
 
27
- type PendingQuery = {
28
- thread: thread?,
29
- callback: ((data: any) -> ())?,
30
- timeout: thread?,
31
- source: Player?,
32
- }
33
-
34
- local _pending = {} :: { [number]: PendingQuery }
35
- local _nextId = 0
36
- local _activeCount = 0
37
-
38
- -- Private -------------------------------------------------------------
20
+ local _nextCorrId = 0
21
+ local _pending: { [number]: thread } = {}
39
22
 
40
- local function allocCorrelation(): number
41
- if _activeCount >= POOL_SIZE then
42
- error("[Lync] All 65536 query correlation slots exhausted")
43
- end
44
-
45
- local id = _nextId
23
+ -- Public --------------------------------------------------------------
46
24
 
47
- -- Skip occupied slots. With 65536 capacity and typical single-digit
48
- -- concurrency, this loop body almost never executes.
49
- while _pending[id] do
50
- id = (id + 1) % POOL_SIZE
51
- end
25
+ local Query = {}
52
26
 
53
- _nextId = (id + 1) % POOL_SIZE
54
- _activeCount += 1
55
- return id
56
- end
27
+ export type QueryHandle = {
28
+ handle: (self: QueryHandle, fn: (request: any, player: Player?) -> any) -> Types.Connection,
29
+ request: (self: QueryHandle, data: any, target: any?) -> any,
30
+ name: (self: QueryHandle) -> string,
31
+ stats: (self: QueryHandle) -> Types.PacketStats,
32
+ }
57
33
 
58
- -- Decrements the active correlation counter. Does not free a specific slot;
59
- -- that is handled by nil-ing _pending[id] at the call site.
60
- local function releaseCorrelation(): ()
61
- _activeCount -= 1
62
- end
34
+ function Query.define(
35
+ name: string,
36
+ requestCodec: Types.InternalCodec<any>,
37
+ responseCodec: Types.InternalCodec<any>,
38
+ options: Types.QueryOptions?
39
+ ): QueryHandle
40
+ local opts = options or {} :: Types.QueryOptions
41
+ local timeout = opts.timeout or DEFAULT_TIMEOUT
42
+
43
+ -- Register request channel
44
+ local reqOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, name)
45
+ local reqReg = Registry.register(
46
+ name,
47
+ Registry.KIND_REQUEST,
48
+ requestCodec,
49
+ false,
50
+ nil,
51
+ IS_SERVER,
52
+ 1,
53
+ Channel.TS_NONE,
54
+ reqOpenFn,
55
+ opts.rateLimit or { maxPerSecond = 30 },
56
+ opts.validate,
57
+ nil
58
+ )
63
59
 
64
- local function completeQuery(id: number, data: any, source: Player?): ()
65
- local entry = _pending[id]
66
- if not entry then
67
- return
68
- end
60
+ -- Register response channel (paired)
61
+ local respOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, `{name}_resp`)
62
+ local respReg = Registry.register(
63
+ `{name}_resp`,
64
+ Registry.KIND_RESPONSE,
65
+ responseCodec,
66
+ false,
67
+ reqReg.id,
68
+ false,
69
+ 1,
70
+ Channel.TS_NONE,
71
+ respOpenFn,
72
+ nil,
73
+ nil,
74
+ nil
75
+ )
69
76
 
70
- -- Reject responses from wrong player (prevents correlation spoofing)
71
- if source ~= nil and entry.source ~= nil and source ~= entry.source then
72
- return
73
- end
77
+ reqReg.partner = respReg.id
74
78
 
75
- _pending[id] = nil
76
- releaseCorrelation()
79
+ -- Lazy-require to avoid circular deps
80
+ local _server: any = nil
81
+ local _client: any = nil
77
82
 
78
- if entry.timeout then
79
- task.cancel(entry.timeout)
83
+ local function getServer(): any
84
+ if not _server then
85
+ _server = require(script.Parent.Parent.transport.Server)
86
+ end
87
+ return _server
80
88
  end
81
89
 
82
- if entry.callback then
83
- entry.callback(data)
84
- elseif entry.thread then
85
- coroutine.resume(entry.thread :: thread, data)
90
+ local function getClient(): any
91
+ if not _client then
92
+ _client = require(script.Parent.Parent.transport.Client)
93
+ end
94
+ return _client
86
95
  end
87
- end
88
96
 
89
- local function onTimeout(correlation: number): ()
90
- completeQuery(correlation, nil)
91
- end
97
+ -- Listen for incoming responses and resolve pending requests
98
+ respReg.signal:connect(function(value: any, sender: Player?, corrId: number)
99
+ local waiting = _pending[corrId]
100
+ if waiting then
101
+ _pending[corrId] = nil
102
+ task.spawn(waiting, value)
103
+ end
104
+ end)
92
105
 
93
- type QueryFields = {
94
- _name: string,
95
- _reqReg: Registration,
96
- _respReg: Registration,
97
- _timeout: number,
98
- }
106
+ -- Listen for incoming requests and auto-respond
107
+ local _handler: ((any, Player?) -> any)? = nil
99
108
 
100
- local QueryImpl = {}
101
- QueryImpl.__index = QueryImpl
109
+ reqReg.signal:connect(function(value: any, sender: Player?, corrId: number)
110
+ if not _handler then
111
+ return
112
+ end
102
113
 
103
- export type Query = typeof(setmetatable({} :: QueryFields, QueryImpl))
114
+ -- Run handler, get response
115
+ local ok, response = pcall(_handler :: any, value, sender)
104
116
 
105
- -- Send a query to multiple players and yield. Returns { [Player]: response? }.
106
- local function requestMulti(self: Query, players: { Player }, data: any): { [Player]: any? }
107
- if not IS_SERVER then
108
- error("[Lync] Multi-request is server-only")
109
- end
117
+ if IS_SERVER then
118
+ if sender then
119
+ local Server = getServer()
120
+ local ch = Server.getChannel(sender, false)
121
+ if ok then
122
+ Channel.writeQuery(ch, respReg.id, corrId, responseCodec, response)
123
+ else
124
+ Channel.writeQuery(ch, respReg.id, corrId, nil, nil)
125
+ end
126
+ end
127
+ else
128
+ local Client = getClient()
129
+ local ch = Client.getChannel(false)
130
+ if ok then
131
+ Channel.writeQuery(ch, respReg.id, corrId, responseCodec, response)
132
+ else
133
+ Channel.writeQuery(ch, respReg.id, corrId, nil, nil)
134
+ end
135
+ end
136
+ end)
110
137
 
111
- local count = #players
112
- if count == 0 then
113
- return {}
114
- end
138
+ local handle: any = {}
115
139
 
116
- if count == 1 then
117
- local correlation = allocCorrelation()
118
- local thread = coroutine.running()
119
- local entry: PendingQuery = { thread = thread, timeout = nil, source = players[1] }
120
- _pending[correlation] = entry
121
-
122
- Server.writeQuery(
123
- players[1],
124
- self._reqReg.id,
125
- self._name,
126
- correlation,
127
- self._reqReg.codec,
128
- data
129
- )
130
-
131
- entry.timeout = task.delay(self._timeout, onTimeout, correlation)
132
- local result = coroutine.yield()
133
- return { [players[1]] = result }
134
- end
140
+ function handle.handle(
141
+ _self: QueryHandle,
142
+ fn: (request: any, player: Player?) -> any
143
+ ): Types.Connection
144
+ _handler = fn
135
145
 
136
- local thread = coroutine.running()
137
- local results = {} :: { [Player]: any? }
138
- local remaining = count
139
- local correlations = table.create(count) :: { number }
140
- local finished = false
141
-
142
- local reqId = self._reqReg.id
143
- local reqName = self._name
144
- local reqCodec = self._reqReg.codec
145
- local timeout = self._timeout
146
-
147
- for i = 1, count do
148
- local player = players[i]
149
- local correlation = allocCorrelation()
150
- correlations[i] = correlation
151
-
152
- local entry: PendingQuery = {
153
- thread = nil,
154
- callback = function(resp: any): ()
155
- results[player] = resp
156
- remaining -= 1
157
- if remaining == 0 and not finished then
158
- finished = true
159
- task.spawn(thread, results)
146
+ -- Return a connection that clears the handler on disconnect
147
+ return table.freeze({
148
+ connected = true,
149
+ disconnect = function(self: Types.Connection): ()
150
+ if not (self :: any).connected then
151
+ return
160
152
  end
153
+ (self :: any).connected = false
154
+ _handler = nil
161
155
  end,
162
- timeout = nil,
163
- source = player,
164
- }
165
-
166
- _pending[correlation] = entry
167
- Server.writeQuery(player, reqId, reqName, correlation, reqCodec, data)
156
+ } :: any)
168
157
  end
169
158
 
170
- local timeoutThread = task.delay(timeout, function(): ()
171
- if finished then
172
- return
173
- end
174
- finished = true
159
+ function handle.request(_self: QueryHandle, data: any, target: any?): any
160
+ _nextCorrId += 1
161
+ local corrId = _nextCorrId
175
162
 
176
- for i = 1, count do
177
- local cid = correlations[i]
178
- if _pending[cid] then
179
- _pending[cid] = nil
180
- releaseCorrelation()
163
+ if IS_SERVER then
164
+ if target == nil then
165
+ error(`[Lync] Query "{name}": server request requires a target`)
181
166
  end
182
- end
183
-
184
- task.spawn(thread, results)
185
- end)
186
167
 
187
- for i = 1, count do
188
- local cid = correlations[i]
189
- local entry = _pending[cid]
190
- if entry then
191
- entry.timeout = timeoutThread
192
- end
193
- end
168
+ local Server = getServer()
194
169
 
195
- return coroutine.yield()
196
- end
170
+ -- Single player
171
+ if typeof(target) == "Instance" and target:IsA("Player") then
172
+ local ch = Server.getChannel(target :: Player, false)
173
+ Channel.writeQuery(ch, reqReg.id, corrId, requestCodec, data)
197
174
 
198
- function QueryImpl.listen(self: Query, callback: (...any) -> any): Connection
199
- local respId = self._respReg.id
200
- local respName = self._name
201
- local respCodec = self._respReg.codec
202
-
203
- if IS_SERVER then
204
- return self._reqReg.signal:connect(
205
- function(request: any, player: Player, correlation: number): ()
206
- local results = table.pack(pcall(callback, request, player))
207
- if not results[1] then
208
- warn(`[Lync] Query handler error on "{respName}": {results[2]}`)
209
- Server.writeQueryNil(player, respId, respName, correlation)
210
- return
211
- end
175
+ -- Wait for response
176
+ local running = coroutine.running()
177
+ _pending[corrId] = running
212
178
 
213
- local response = if results.n > 2
214
- then table.move(results, 2, results.n, 1, table.create(results.n - 1))
215
- else results[2]
179
+ task.delay(timeout, function()
180
+ if _pending[corrId] == running then
181
+ _pending[corrId] = nil
182
+ task.spawn(running, nil)
183
+ end
184
+ end)
216
185
 
217
- if response ~= nil then
218
- Server.writeQuery(player, respId, respName, correlation, respCodec, response)
219
- else
220
- Server.writeQueryNil(player, respId, respName, correlation)
221
- end
186
+ return coroutine.yield()
222
187
  end
223
- )
224
- else
225
- return self._reqReg.signal:connect(
226
- function(request: any, _player: Player?, correlation: number): ()
227
- local results = table.pack(pcall(callback, request))
228
- if not results[1] then
229
- warn(`[Lync] Query handler error on "{respName}": {results[2]}`)
230
- Client.writeQueryNil(respId, respName, correlation)
231
- return
232
- end
233
188
 
234
- local response = if results.n > 2
235
- then table.move(results, 2, results.n, 1, table.create(results.n - 1))
236
- else results[2]
237
-
238
- if response ~= nil then
239
- Client.writeQuery(respId, respName, correlation, respCodec, response)
189
+ -- Multi-target: collect responses into { [Player]: response? }
190
+ local targets: { Player } = {}
191
+
192
+ if typeof(target) == "table" then
193
+ local tbl = target :: any
194
+ if tbl._lyncAll then
195
+ targets = Players:GetPlayers()
196
+ elseif tbl._lyncGroup then
197
+ for player in tbl._members do
198
+ table.insert(targets, player)
199
+ end
240
200
  else
241
- Client.writeQueryNil(respId, respName, correlation)
201
+ for _, p in tbl do
202
+ if typeof(p) == "Instance" and (p :: Instance):IsA("Player") then
203
+ table.insert(targets, p :: Player)
204
+ end
205
+ end
242
206
  end
243
207
  end
244
- )
245
- end
246
- end
247
-
248
- -- Client: send request to server, yield until response.
249
- function QueryImpl.request(self: Query, data: any): any
250
- if IS_SERVER then
251
- error("[Lync] query:request is client-only. Use query:requestFrom on server")
252
- end
253
208
 
254
- local correlation = allocCorrelation()
255
- local thread = coroutine.running()
256
- local entry: PendingQuery = { thread = thread, timeout = nil }
257
- _pending[correlation] = entry
209
+ if #targets == 0 then
210
+ return {}
211
+ end
258
212
 
259
- Client.writeQuery(self._reqReg.id, self._name, correlation, self._reqReg.codec, data)
213
+ local results: { [Player]: any } = {}
214
+ local remaining = #targets
215
+ local running = coroutine.running()
216
+
217
+ for _, player in targets do
218
+ _nextCorrId += 1
219
+ local playerCorrId = _nextCorrId
220
+ local ch = Server.getChannel(player, false)
221
+ Channel.writeQuery(ch, reqReg.id, playerCorrId, requestCodec, data)
222
+
223
+ local playerRef = player
224
+ _pending[playerCorrId] = coroutine.create(function(response: any)
225
+ results[playerRef] = response
226
+ remaining -= 1
227
+ if remaining == 0 then
228
+ task.spawn(running, results)
229
+ end
230
+ end)
231
+ end
260
232
 
261
- entry.timeout = task.delay(self._timeout, onTimeout, correlation)
262
- return coroutine.yield()
263
- end
233
+ -- Timeout
234
+ task.delay(timeout, function()
235
+ if remaining > 0 then
236
+ remaining = 0
237
+ task.spawn(running, results)
238
+ end
239
+ end)
240
+
241
+ return coroutine.yield()
242
+ else
243
+ -- Client → server
244
+ local Client = getClient()
245
+ local ch = Client.getChannel(false)
246
+ Channel.writeQuery(ch, reqReg.id, corrId, requestCodec, data)
247
+
248
+ local running = coroutine.running()
249
+ _pending[corrId] = running
250
+
251
+ task.delay(timeout, function()
252
+ if _pending[corrId] == running then
253
+ _pending[corrId] = nil
254
+ task.spawn(running, nil)
255
+ end
256
+ end)
264
257
 
265
- -- Server: send request to one player, yield until response.
266
- function QueryImpl.requestFrom(self: Query, player: Player, data: any): any
267
- if not IS_SERVER then
268
- error("[Lync] query:requestFrom is server-only. Use query:request on client")
258
+ return coroutine.yield()
259
+ end
269
260
  end
270
261
 
271
- local correlation = allocCorrelation()
272
- local thread = coroutine.running()
273
- local entry: PendingQuery = { thread = thread, timeout = nil, source = player }
274
- _pending[correlation] = entry
275
-
276
- Server.writeQuery(player, self._reqReg.id, self._name, correlation, self._reqReg.codec, data)
277
-
278
- entry.timeout = task.delay(self._timeout, onTimeout, correlation)
279
- return coroutine.yield()
280
- end
281
-
282
- -- Server: send request to all players, yield until all respond or timeout.
283
- function QueryImpl.requestAll(self: Query, data: any): { [Player]: any? }
284
- return requestMulti(self, Players:GetPlayers(), data)
285
- end
286
-
287
- -- Server: send request to a list of players.
288
- function QueryImpl.requestList(self: Query, players: { Player }, data: any): { [Player]: any? }
289
- return requestMulti(self, players, data)
290
- end
291
-
292
- -- Server: send request to all players in a group.
293
- function QueryImpl.requestGroup(self: Query, group: any, data: any): { [Player]: any? }
294
- local set = group:getSet()
295
- local players = {} :: { Player }
296
- for player in set do
297
- table.insert(players, player)
262
+ function handle.name(_self: QueryHandle): string
263
+ return name
298
264
  end
299
- return requestMulti(self, players, data)
300
- end
301
-
302
- table.freeze(QueryImpl)
303
-
304
- -- Public --------------------------------------------------------------
305
-
306
- local QueryModule = {}
307
265
 
308
- function QueryModule.define(name: string, config: QueryConfig<any, any>): Query
309
- if not config.request then
310
- error(`[Lync] Query requires a request codec: "{name}"`)
311
- end
312
- if not config.response then
313
- error(`[Lync] Query requires a response codec: "{name}"`)
266
+ function handle.stats(_self: QueryHandle): Types.PacketStats
267
+ return {
268
+ bytesSent = reqReg.bytesSent + respReg.bytesSent,
269
+ bytesReceived = reqReg.bytesReceived + respReg.bytesReceived,
270
+ fires = reqReg.fires + respReg.fires,
271
+ recvFires = reqReg.recvFires + respReg.recvFires,
272
+ drops = reqReg.drops + respReg.drops,
273
+ }
314
274
  end
315
275
 
316
- local reqSignal = Signal.create()
317
- local respSignal = Signal.create()
318
-
319
- local rateLimit = config.rateLimit or { maxPerSecond = 30 }
320
-
321
- local reqReg, respReg = Registry.registerQueryPair(
322
- name,
323
- config.request,
324
- config.response,
325
- reqSignal,
326
- respSignal,
327
- rateLimit,
328
- config.validate
329
- )
330
-
331
- respSignal:connect(function(resp: any, source: Player?, correlation: number): ()
332
- completeQuery(correlation, resp, source)
333
- end)
334
-
335
- return setmetatable({
336
- _name = name,
337
- _reqReg = reqReg,
338
- _respReg = respReg,
339
- _timeout = config.timeout or 5,
340
- }, QueryImpl)
276
+ return table.freeze(handle)
341
277
  end
342
278
 
343
- -- Returns the number of queries currently awaiting a response.
344
- function QueryModule.pendingCount(): number
345
- return _activeCount
279
+ function Query.pendingCount(): number
280
+ local count = 0
281
+ for _ in _pending do
282
+ count += 1
283
+ end
284
+ return count
346
285
  end
347
286
 
348
- return table.freeze(QueryModule)
287
+ return table.freeze(Query)