@axpecter/lync 2.1.2 → 2.2.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 (56) hide show
  1. package/README.md +241 -349
  2. package/package.json +1 -1
  3. package/src/Types.luau +53 -13
  4. package/src/api/Group.luau +41 -51
  5. package/src/api/Packet.luau +145 -143
  6. package/src/api/Query.luau +204 -202
  7. package/src/api/Scope.luau +36 -38
  8. package/src/api/Signal.luau +68 -94
  9. package/src/codec/Base.luau +43 -23
  10. package/src/codec/composite/Array.luau +161 -152
  11. package/src/codec/composite/Map.luau +37 -53
  12. package/src/codec/composite/Optional.luau +15 -21
  13. package/src/codec/composite/Shared.luau +78 -41
  14. package/src/codec/composite/Struct.luau +64 -125
  15. package/src/codec/composite/Tagged.luau +15 -25
  16. package/src/codec/composite/Tuple.luau +15 -20
  17. package/src/codec/datatype/Buffer.luau +44 -51
  18. package/src/codec/datatype/CFrame.luau +72 -104
  19. package/src/codec/datatype/Color.luau +14 -10
  20. package/src/codec/datatype/Instance.luau +32 -42
  21. package/src/codec/datatype/IntVector.luau +24 -22
  22. package/src/codec/datatype/NumberRange.luau +21 -12
  23. package/src/codec/datatype/Ray.luau +13 -7
  24. package/src/codec/datatype/Rect.luau +13 -7
  25. package/src/codec/datatype/Region.luau +25 -22
  26. package/src/codec/datatype/Sequence.luau +144 -118
  27. package/src/codec/datatype/String.luau +29 -59
  28. package/src/codec/datatype/UDim.luau +29 -30
  29. package/src/codec/datatype/Vector.luau +89 -105
  30. package/src/codec/meta/Auto.luau +194 -246
  31. package/src/codec/meta/Bitfield.luau +37 -36
  32. package/src/codec/meta/Custom.luau +10 -10
  33. package/src/codec/meta/Enum.luau +8 -11
  34. package/src/codec/meta/Float.luau +16 -20
  35. package/src/codec/meta/Nothing.luau +2 -2
  36. package/src/codec/meta/Unknown.luau +22 -32
  37. package/src/codec/primitive/Bool.luau +9 -5
  38. package/src/codec/primitive/Float16.luau +13 -23
  39. package/src/codec/primitive/Int.luau +20 -28
  40. package/src/codec/primitive/Number.luau +6 -10
  41. package/src/codec/primitive/Signed.luau +32 -0
  42. package/src/codec/primitive/Varint.luau +59 -28
  43. package/src/index.d.ts +8 -2
  44. package/src/init.luau +118 -143
  45. package/src/internal/Baseline.luau +17 -18
  46. package/src/internal/Channel.luau +143 -212
  47. package/src/internal/Middleware.luau +37 -47
  48. package/src/internal/Pool.luau +10 -10
  49. package/src/internal/Registry.luau +38 -34
  50. package/src/internal/Transport.luau +28 -0
  51. package/src/internal/Util.luau +26 -0
  52. package/src/transport/Bridge.luau +32 -24
  53. package/src/transport/Client.luau +51 -53
  54. package/src/transport/Gate.luau +183 -113
  55. package/src/transport/Reader.luau +95 -66
  56. package/src/transport/Server.luau +130 -125
@@ -5,24 +5,33 @@
5
5
  local Players = game:GetService("Players")
6
6
  local RunService = game:GetService("RunService")
7
7
 
8
- local Baseline = require(script.Parent.Parent.internal.Baseline)
9
8
  local Channel = require(script.Parent.Parent.internal.Channel)
10
9
  local Registry = require(script.Parent.Parent.internal.Registry)
10
+ local Transport = require(script.Parent.Parent.internal.Transport)
11
11
  local Types = require(script.Parent.Parent.Types)
12
+ local Util = require(script.Parent.Parent.internal.Util)
12
13
 
13
- -- Constants -----------------------------------------------------------
14
+ -- Constants --------------------------------------------------------------
14
15
 
15
16
  local IS_SERVER = RunService:IsServer()
16
17
  local DEFAULT_TIMEOUT = 5
17
18
 
18
- -- State ---------------------------------------------------------------
19
+ -- State ------------------------------------------------------------------
19
20
 
20
21
  local _nextCorrId = 0
21
22
  local _pending: { [number]: thread } = {}
23
+ local _pendingGather: { [number]: (any) -> () } = {}
22
24
 
23
- -- Public --------------------------------------------------------------
25
+ -- Private ----------------------------------------------------------------
24
26
 
25
- local Query = {}
27
+ local isPlayer = Util.isPlayer
28
+
29
+ local function nextCorr(): number
30
+ _nextCorrId += 1
31
+ return _nextCorrId
32
+ end
33
+
34
+ -- Public types -----------------------------------------------------------
26
35
 
27
36
  export type QueryHandle = {
28
37
  handle: (self: QueryHandle, fn: (request: any, player: Player?) -> any) -> Types.Connection,
@@ -31,249 +40,239 @@ export type QueryHandle = {
31
40
  stats: (self: QueryHandle) -> Types.PacketStats,
32
41
  }
33
42
 
43
+ -- Public -----------------------------------------------------------------
44
+
45
+ local Query = {}
46
+
34
47
  function Query.define(
35
48
  name: string,
36
49
  requestCodec: Types.InternalCodec<any>,
37
50
  responseCodec: Types.InternalCodec<any>,
38
51
  options: Types.QueryOptions?
39
52
  ): QueryHandle
40
- local opts = options or {} :: Types.QueryOptions
53
+ local opts = options or ({} :: Types.QueryOptions)
41
54
  local timeout = opts.timeout or DEFAULT_TIMEOUT
42
55
 
43
- -- Register request channel
44
56
  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
- )
59
-
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
- )
57
+ local respName = `{name}\0resp`
58
+ local respOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, respName)
59
+
60
+ local reqReg = Registry.register({
61
+ name = name,
62
+ kind = Registry.KIND_REQUEST,
63
+ codec = requestCodec,
64
+ isUnreliable = false,
65
+ partner = nil,
66
+ needsGate = IS_SERVER,
67
+ headerSize = 1,
68
+ timestampMode = Channel.TS_NONE,
69
+ openFn = reqOpenFn,
70
+ rateLimit = opts.rateLimit or { maxPerSecond = 30 },
71
+ validate = opts.validate,
72
+ maxPayloadBytes = nil,
73
+ })
74
+
75
+ local respReg = Registry.register({
76
+ name = respName,
77
+ kind = Registry.KIND_RESPONSE,
78
+ codec = responseCodec,
79
+ isUnreliable = false,
80
+ partner = reqReg.id,
81
+ needsGate = false,
82
+ headerSize = 1,
83
+ timestampMode = Channel.TS_NONE,
84
+ openFn = respOpenFn,
85
+ rateLimit = nil,
86
+ validate = nil,
87
+ maxPayloadBytes = nil,
88
+ })
76
89
 
77
90
  reqReg.partner = respReg.id
78
91
 
79
- -- Lazy-require to avoid circular deps
80
- local _server: any = nil
81
- local _client: any = nil
92
+ local activeHandler: ((any, Player?) -> any)? = nil
93
+ local handlerToken = 0
82
94
 
83
- local function getServer(): any
84
- if not _server then
85
- _server = require(script.Parent.Parent.transport.Server)
86
- end
87
- return _server
88
- end
89
-
90
- local function getClient(): any
91
- if not _client then
92
- _client = require(script.Parent.Parent.transport.Client)
93
- end
94
- return _client
95
- end
96
-
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
95
+ respReg.signal:connect(function(value: any, _sender: Player?, corrId: number)
96
+ local single = _pending[corrId]
97
+ if single then
101
98
  _pending[corrId] = nil
102
- task.spawn(waiting, value)
99
+ task.spawn(single, value)
100
+ return
101
+ end
102
+ local gather = _pendingGather[corrId]
103
+ if gather then
104
+ _pendingGather[corrId] = nil
105
+ gather(value)
103
106
  end
104
107
  end)
105
108
 
106
- -- Listen for incoming requests and auto-respond
107
- local _handler: ((any, Player?) -> any)? = nil
108
-
109
109
  reqReg.signal:connect(function(value: any, sender: Player?, corrId: number)
110
- if not _handler then
110
+ local handler = activeHandler
111
+ if not handler then
111
112
  return
112
113
  end
113
114
 
114
- -- Run handler, get response
115
- local ok, response = pcall(_handler :: any, value, sender)
116
-
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
115
+ local ok, response = pcall(handler, value, sender)
116
+ local payloadCodec = if ok then responseCodec else nil
117
+ local payloadData = if ok then response else nil
118
+
119
+ local ch = if IS_SERVER and sender
120
+ then Transport.server().getChannel(sender, false)
121
+ elseif not IS_SERVER then Transport.client().getChannel(false)
122
+ else nil
123
+ if ch then
124
+ Channel.writeQuery(ch, respReg.id, corrId, payloadCodec, payloadData)
135
125
  end
136
126
  end)
137
127
 
138
- local handle: any = {}
139
-
140
- function handle.handle(
141
- _self: QueryHandle,
142
- fn: (request: any, player: Player?) -> any
143
- ): Types.Connection
144
- _handler = fn
145
-
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
152
- end
153
- (self :: any).connected = false
154
- _handler = nil
155
- end,
156
- } :: any)
157
- end
158
-
159
- function handle.request(_self: QueryHandle, data: any, target: any?): any
160
- _nextCorrId += 1
161
- local corrId = _nextCorrId
162
-
163
- if IS_SERVER then
164
- if target == nil then
165
- error(`[Lync] Query "{name}": server request requires a target`)
128
+ local function scheduleSingleTimeout(corrId: number, running: thread): ()
129
+ task.delay(timeout, function()
130
+ if _pending[corrId] == running then
131
+ _pending[corrId] = nil
132
+ task.spawn(running, nil)
166
133
  end
134
+ end)
135
+ end
167
136
 
168
- local Server = getServer()
137
+ local function requestSingle(target: Player, data: any): any
138
+ local corrId = nextCorr()
139
+ local ch = Transport.server().getChannel(target, false)
140
+ Channel.writeQuery(ch, reqReg.id, corrId, requestCodec, data)
169
141
 
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)
142
+ local running = coroutine.running()
143
+ _pending[corrId] = running
144
+ scheduleSingleTimeout(corrId, running)
145
+ return coroutine.yield()
146
+ end
174
147
 
175
- -- Wait for response
176
- local running = coroutine.running()
177
- _pending[corrId] = running
148
+ local function requestMulti(targets: { Player }, data: any): { [Player]: any }
149
+ if #targets == 0 then
150
+ return {}
151
+ end
178
152
 
179
- task.delay(timeout, function()
180
- if _pending[corrId] == running then
181
- _pending[corrId] = nil
182
- task.spawn(running, nil)
183
- end
184
- end)
153
+ local results: { [Player]: any } = {}
154
+ local remaining = #targets
155
+ local running = coroutine.running()
156
+ local resolved = false
185
157
 
186
- return coroutine.yield()
158
+ local function resolveOnce(): ()
159
+ if resolved then
160
+ return
187
161
  end
162
+ resolved = true
163
+ task.spawn(running, results)
164
+ end
188
165
 
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
200
- else
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
166
+ local Server = Transport.server()
167
+ for _, player in targets do
168
+ local pCorr = nextCorr()
169
+ local ch = Server.getChannel(player, false)
170
+ Channel.writeQuery(ch, reqReg.id, pCorr, requestCodec, data)
171
+
172
+ local pRef = player
173
+ _pendingGather[pCorr] = function(response: any)
174
+ results[pRef] = response
175
+ remaining -= 1
176
+ if remaining == 0 then
177
+ resolveOnce()
206
178
  end
207
179
  end
180
+ end
208
181
 
209
- if #targets == 0 then
210
- return {}
211
- end
212
-
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
182
+ task.delay(timeout, resolveOnce)
183
+ return coroutine.yield()
184
+ end
232
185
 
233
- -- Timeout
234
- task.delay(timeout, function()
235
- if remaining > 0 then
236
- remaining = 0
237
- task.spawn(running, results)
238
- end
239
- end)
186
+ local function collectTargets(target: any): { Player }
187
+ local targets: { Player } = {}
188
+ if typeof(target) ~= "table" then
189
+ return targets
190
+ end
240
191
 
241
- return coroutine.yield()
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
242
200
  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)
201
+ for _, p in target do
202
+ if isPlayer(p) then
203
+ table.insert(targets, p :: Player)
255
204
  end
256
- end)
257
-
258
- return coroutine.yield()
205
+ end
259
206
  end
207
+ return targets
260
208
  end
261
209
 
262
- function handle.name(_self: QueryHandle): string
263
- return name
210
+ local function requestFromServer(data: any, target: any): any
211
+ if target == nil then
212
+ error(`[Lync] Query.request: "{name}" server request requires a target`)
213
+ end
214
+ if isPlayer(target) then
215
+ return requestSingle(target :: Player, data)
216
+ end
217
+ return requestMulti(collectTargets(target), data)
264
218
  end
265
219
 
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
- }
220
+ local function requestFromClient(data: any): any
221
+ local corrId = nextCorr()
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()
274
229
  end
275
230
 
276
- return table.freeze(handle)
231
+ local handle = {
232
+ handle = function(
233
+ _self: QueryHandle,
234
+ fn: (request: any, player: Player?) -> any
235
+ ): Types.Connection
236
+ handlerToken += 1
237
+ local myToken = handlerToken
238
+ activeHandler = fn
239
+
240
+ local conn = { connected = true }
241
+ function conn.disconnect(self): ()
242
+ if not self.connected then
243
+ return
244
+ end
245
+ self.connected = false
246
+ if handlerToken == myToken then
247
+ activeHandler = nil
248
+ end
249
+ end
250
+ return conn :: Types.Connection
251
+ end,
252
+
253
+ request = function(_self: QueryHandle, data: any, target: any?): any
254
+ if IS_SERVER then
255
+ return requestFromServer(data, target)
256
+ end
257
+ return requestFromClient(data)
258
+ end,
259
+
260
+ name = function(_self: QueryHandle): string
261
+ return name
262
+ end,
263
+
264
+ stats = function(_self: QueryHandle): Types.PacketStats
265
+ return {
266
+ bytesSent = reqReg.bytesSent + respReg.bytesSent,
267
+ bytesReceived = reqReg.bytesReceived + respReg.bytesReceived,
268
+ fires = reqReg.fires + respReg.fires,
269
+ recvFires = reqReg.recvFires + respReg.recvFires,
270
+ drops = reqReg.drops + respReg.drops,
271
+ }
272
+ end,
273
+ }
274
+
275
+ return table.freeze(handle) :: QueryHandle
277
276
  end
278
277
 
279
278
  function Query.pendingCount(): number
@@ -281,6 +280,9 @@ function Query.pendingCount(): number
281
280
  for _ in _pending do
282
281
  count += 1
283
282
  end
283
+ for _ in _pendingGather do
284
+ count += 1
285
+ end
284
286
  return count
285
287
  end
286
288
 
@@ -1,76 +1,74 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Batched connection lifecycle management.
3
+ -- Batched connection lifecycle. Accepts Lync Connection,
4
+ -- RBXScriptConnection, and any object with :disconnect() / :Disconnect().
4
5
 
5
6
  local Types = require(script.Parent.Parent.Types)
6
7
 
7
- -- Public --------------------------------------------------------------
8
-
9
- local Scope = {}
8
+ -- Public types -----------------------------------------------------------
10
9
 
11
10
  export type ScopeHandle = {
12
11
  on: (self: ScopeHandle, source: any, fn: (...any) -> ()) -> Types.Connection,
13
12
  once: (self: ScopeHandle, source: any, fn: (...any) -> ()) -> Types.Connection,
14
13
  add: (self: ScopeHandle, connection: any) -> (),
14
+ size: (self: ScopeHandle) -> number,
15
15
  destroy: (self: ScopeHandle) -> (),
16
16
  }
17
17
 
18
+ type Scope = ScopeHandle & { _connections: { any }, _count: number }
19
+
20
+ -- Private ----------------------------------------------------------------
21
+
22
+ local function pushConnection(self: Scope, conn: any): ()
23
+ local idx = self._count + 1
24
+ self._count = idx
25
+ self._connections[idx] = conn
26
+ end
27
+
28
+ -- Public -----------------------------------------------------------------
29
+
18
30
  local ScopeMeta = {}
19
31
  ScopeMeta.__index = ScopeMeta
20
32
 
21
- function ScopeMeta.on(self: any, source: any, fn: (...any) -> ()): Types.Connection
33
+ function ScopeMeta.on(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
22
34
  local conn = source:on(fn)
23
- local conns = self._connections
24
- local idx = self._count + 1
25
- self._count = idx
26
- conns[idx] = conn
35
+ pushConnection(self, conn)
27
36
  return conn
28
37
  end
29
38
 
30
- function ScopeMeta.once(self: any, source: any, fn: (...any) -> ()): Types.Connection
39
+ function ScopeMeta.once(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
31
40
  local conn = source:once(fn)
32
- local conns = self._connections
33
- local idx = self._count + 1
34
- self._count = idx
35
- conns[idx] = conn
41
+ pushConnection(self, conn)
36
42
  return conn
37
43
  end
38
44
 
39
- function ScopeMeta.add(self: any, connection: any): ()
40
- local idx = self._count + 1
41
- self._count = idx
42
- self._connections[idx] = connection
45
+ ScopeMeta.add = pushConnection
46
+
47
+ function ScopeMeta.size(self: Scope): number
48
+ return self._count
43
49
  end
44
50
 
45
- function ScopeMeta.destroy(self: any): ()
51
+ function ScopeMeta.destroy(self: Scope): ()
46
52
  local conns = self._connections
47
53
  local count = self._count
48
-
49
54
  for i = 1, count do
50
55
  local conn = conns[i]
51
- conns[i] = nil :: any
52
-
53
- --[[
54
- Support both Lync Connection (conn:disconnect())
55
- and RBXScriptConnection (conn:Disconnect()).
56
- ]]
57
- if typeof(conn) == "table" then
58
- if conn.disconnect then
59
- conn:disconnect()
60
- end
61
- elseif typeof(conn) == "RBXScriptConnection" then
56
+ conns[i] = nil
57
+ if typeof(conn) == "RBXScriptConnection" then
62
58
  (conn :: RBXScriptConnection):Disconnect()
59
+ elseif typeof(conn) == "table" and conn.disconnect then
60
+ conn:disconnect()
63
61
  end
64
62
  end
65
-
66
63
  self._count = 0
67
64
  end
68
65
 
69
- function Scope.create(): ScopeHandle
70
- return setmetatable({
71
- _connections = {},
72
- _count = 0,
73
- }, ScopeMeta) :: any
66
+ table.freeze(ScopeMeta)
67
+
68
+ local ScopeModule = {}
69
+
70
+ function ScopeModule.create(): ScopeHandle
71
+ return (setmetatable({ _connections = {}, _count = 0 }, ScopeMeta) :: any) :: ScopeHandle
74
72
  end
75
73
 
76
- return table.freeze(Scope)
74
+ return table.freeze(ScopeModule)