@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,224 +1,232 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Deserializes incoming buffers and routes by registration kind.
3
+ -- Deserializes incoming buffers with MSB batch dispatch.
4
4
 
5
5
  local Baseline = require(script.Parent.Parent.internal.Baseline)
6
6
  local Channel = require(script.Parent.Parent.internal.Channel)
7
- local Gate = require(script.Parent.Gate)
7
+ local Gate = require(script.Parent.Parent.transport.Gate)
8
8
  local Middleware = require(script.Parent.Parent.internal.Middleware)
9
9
  local Registry = require(script.Parent.Parent.internal.Registry)
10
+ local Types = require(script.Parent.Parent.Types)
10
11
  local Varint = require(script.Parent.Parent.codec.primitive.Varint)
11
12
 
12
- local registryGet = Registry.get
13
- local gateCheck = Gate.check
14
- local gateReportDrop = Gate.reportDrop
15
-
16
13
  -- Constants -----------------------------------------------------------
17
14
 
18
15
  local KIND_PACKET = Registry.KIND_PACKET
19
16
  local KIND_REQUEST = Registry.KIND_REQUEST
17
+ local KIND_RESPONSE = Registry.KIND_RESPONSE
20
18
 
21
- local TS_FRAME = Channel.TS_FRAME
22
- local TS_OFFSET = Channel.TS_OFFSET
23
- local TS_FULL = Channel.TS_FULL
24
-
25
- local MAX_INCOMING = 65536
19
+ -- Private -------------------------------------------------------------
26
20
 
27
- local floor = math.floor
21
+ local band = bit32.band
22
+ local readu8 = buffer.readu8
28
23
 
29
24
  -- Public --------------------------------------------------------------
30
25
 
31
26
  local Reader = {}
32
27
 
33
- -- Shared incoming-data validation used by both Client and Server receive.
34
28
  function Reader.decodeIncoming(data: any): buffer?
35
29
  if typeof(data) ~= "buffer" then
36
30
  return nil
37
31
  end
38
- if buffer.len(data) > MAX_INCOMING then
32
+ local b = data :: buffer
33
+ local len = buffer.len(b)
34
+ if len == 0 or len > 65536 then
39
35
  return nil
40
36
  end
41
- return data
37
+ return b
42
38
  end
43
39
 
44
- -- Callers wrap this in pcall. Any throw aborts the entire buffer from this source.
45
40
  function Reader.process(
46
41
  incoming: buffer,
42
+ incomingLen: number,
47
43
  refs: { Instance }?,
48
44
  player: Player?,
49
- startOffset: number
45
+ isServer: boolean
50
46
  ): ()
51
- local length = buffer.len(incoming)
52
- local pos = startOffset
53
-
54
- if Baseline.hasDelta then
47
+ -- Set baseline read key for delta codecs
48
+ if Baseline.hasDelta() then
55
49
  Baseline.setReadKey(player or false)
56
50
  end
57
51
 
58
- local hasReceiveMiddleware = Middleware.hasReceive
59
- local isServer = player ~= nil
60
- local statsOn = Channel.statsEnabled()
52
+ local statsEnabled = Channel.statsEnabled()
53
+ local hasRecvHooks = Middleware.hasReceiveHooks()
54
+ local pos = 0
61
55
 
62
- while pos < length do
63
- local id = buffer.readu8(incoming, pos)
56
+ while pos < incomingLen do
57
+ local raw = readu8(incoming, pos)
64
58
  pos += 1
65
59
 
66
- local reg = registryGet(id)
60
+ local msb = band(raw, 0x80) ~= 0
61
+ local id = band(raw, 0x7F)
62
+
63
+ local reg = Registry.get(id)
67
64
  if not reg then
68
- warn(`[Lync] Unknown packet ID {id}`)
65
+ warn(`[Lync] Reader: unknown packet ID {id} at byte {pos - 1}`)
69
66
  break
70
67
  end
71
68
 
72
- local kind = reg.kind
73
-
74
- if kind == KIND_PACKET then
75
- if pos + 2 > length then
76
- break
77
- end
78
-
79
- local count = buffer.readu16(incoming, pos)
80
- pos += 2
69
+ if reg.kind == KIND_PACKET then
70
+ local count: number
81
71
 
82
- if count == 0 then
83
- continue
72
+ if msb then
73
+ count = 1
74
+ else
75
+ count = buffer.readu16(incoming, pos)
76
+ pos += 2
84
77
  end
85
78
 
86
- local codec = reg.codec
87
- local signal = reg.signal
88
- local batchStart = pos
89
- local maxBytes = reg.maxPayloadBytes
90
-
91
- -- Timestamp is per-batch, read once after count
92
- local timestamp: number? = nil
79
+ -- Read timestamp if this packet has one configured
93
80
  local tsMode = reg.timestampMode
94
- if tsMode == TS_FRAME then
95
- timestamp = buffer.readu8(incoming, pos)
81
+ local tsValue: any = nil
82
+
83
+ if tsMode == Channel.TS_FRAME then
84
+ tsValue = readu8(incoming, pos)
96
85
  pos += 1
97
- elseif tsMode == TS_OFFSET then
98
- timestamp = buffer.readu16(incoming, pos)
86
+ elseif tsMode == Channel.TS_OFFSET then
87
+ tsValue = buffer.readu16(incoming, pos)
99
88
  pos += 2
100
- elseif tsMode == TS_FULL then
101
- timestamp = buffer.readf64(incoming, pos)
89
+ elseif tsMode == Channel.TS_FULL then
90
+ tsValue = buffer.readf64(incoming, pos)
102
91
  pos += 8
103
92
  end
104
93
 
105
- -- Cap count for fixed-size codecs to prevent wasted iterations
106
- local fixedSize = (codec :: any)._size
107
- if fixedSize and fixedSize > 0 then
108
- local maxItems = floor((length - pos) / fixedSize)
94
+ -- Prevent a malicious count from reading past the buffer
95
+ if reg.codec._size then
96
+ local maxItems = (incomingLen - pos) // (reg.codec._size :: number)
109
97
  if count > maxItems then
110
98
  count = maxItems
111
99
  end
112
100
  end
113
101
 
114
- -- Hot path: no gate, no middleware, no byte budget
115
- if not (isServer and reg.needsGate) and not hasReceiveMiddleware and not maxBytes then
116
- if statsOn then
117
- local batchBytes = 0
118
- for _ = 1, count do
119
- if pos >= length then
120
- break
121
- end
102
+ for _ = 1, count do
103
+ local value, consumed = reg.codec.read(incoming, pos, refs)
104
+ pos += consumed
122
105
 
123
- local value, consumed = codec.read(incoming, pos, refs)
124
- pos += consumed
125
- batchBytes += consumed
126
- signal:fire(value, player, timestamp)
127
- end
128
- reg.bytesReceived += batchBytes
129
- reg.recvFires += count
130
- else
131
- for _ = 1, count do
132
- if pos >= length then
133
- break
106
+ if isServer and reg.needsGate then
107
+ if player and not Gate.checkGlobalRateLimit(player) then
108
+ if statsEnabled then
109
+ reg.drops += 1
134
110
  end
135
-
136
- local value, consumed = codec.read(incoming, pos, refs)
137
- pos += consumed
138
- signal:fire(value, player, timestamp)
139
- end
140
- end
141
- else
142
- -- Cold path: gate and/or middleware and/or byte budget active
143
- local baseName = reg.baseName
144
- local needsGate = isServer and reg.needsGate
145
- local batchBytes = if statsOn then 0 else nil
146
- local recvCount = if statsOn then 0 else nil
147
-
148
- for _ = 1, count do
149
- if pos >= length then
150
- break
151
- end
152
-
153
- if maxBytes and (pos - batchStart) > maxBytes then
154
- gateReportDrop(player, "size", reg.name, nil)
155
- break
111
+ Middleware.fireDrop(player :: Player, "rate", reg.name, value)
112
+ continue
156
113
  end
157
114
 
158
- local value, consumed = codec.read(incoming, pos, refs)
159
- pos += consumed
160
-
161
- if batchBytes then
162
- batchBytes += consumed
163
- recvCount += 1
115
+ if player and not Gate.checkRateLimit(reg, player) then
116
+ if statsEnabled then
117
+ reg.drops += 1
118
+ end
119
+ Middleware.fireDrop(player :: Player, "rate", reg.name, value)
120
+ continue
164
121
  end
165
122
 
166
- if needsGate and not gateCheck(value, reg, player :: Player) then
123
+ local ok, reason = Gate.validate(value, reg.codec)
124
+ if not ok then
125
+ if statsEnabled then
126
+ reg.drops += 1
127
+ end
128
+ if player then
129
+ Middleware.fireDrop(player, "validation", reg.name, value)
130
+ end
167
131
  continue
168
132
  end
169
133
 
170
- if hasReceiveMiddleware then
171
- local final = Middleware.runReceive(value, baseName, player)
172
- if final == nil then
134
+ if reg.validate then
135
+ local valid, validReason = reg.validate(value, player :: Player)
136
+ if not valid then
137
+ if statsEnabled then
138
+ reg.drops += 1
139
+ end
140
+ Middleware.fireDrop(
141
+ player :: Player,
142
+ validReason or "validate",
143
+ reg.name,
144
+ value
145
+ )
173
146
  continue
174
147
  end
175
- signal:fire(final, player, timestamp)
176
- else
177
- signal:fire(value, player, timestamp)
178
148
  end
179
149
  end
180
- if batchBytes then
181
- reg.bytesReceived += batchBytes
182
- reg.recvFires += recvCount :: number
150
+
151
+ if hasRecvHooks then
152
+ value = Middleware.runReceive(value, reg.name, player)
153
+ end
154
+
155
+ if statsEnabled then
156
+ reg.recvFires += 1
157
+ end
158
+
159
+ if tsValue ~= nil then
160
+ reg.signal:fire(value, player, tsValue)
161
+ else
162
+ reg.signal:fire(value, player)
183
163
  end
184
164
  end
185
- else
186
- -- Query frame: id(u8, already read) + correlationId(varint) + status(u8)
187
- if pos + 2 > length then
188
- break
165
+
166
+ if statsEnabled then
167
+ -- Approximate: counts all bytes consumed in this frame, not just payload
168
+ reg.bytesReceived += pos
189
169
  end
170
+ elseif reg.kind == KIND_REQUEST or reg.kind == KIND_RESPONSE then
171
+ -- MSB distinguishes nil responses (no payload) from data responses
172
+ local corrId, corrBytes = Varint.read(incoming, pos)
173
+ pos += corrBytes
190
174
 
191
- local queryCodec = reg.codec
192
- local querySignal = reg.signal
175
+ if msb then
176
+ if statsEnabled then
177
+ reg.recvFires += 1
178
+ end
179
+ reg.signal:fire(nil, player, corrId)
180
+ else
181
+ local value, consumed = reg.codec.read(incoming, pos, refs)
182
+ pos += consumed
193
183
 
194
- local correlationId, corrBytes = Varint.read(incoming, pos)
195
- pos += corrBytes
196
- local status = buffer.readu8(incoming, pos)
197
- pos += 1
184
+ if isServer and reg.needsGate and reg.kind == KIND_REQUEST then
185
+ if player and not Gate.checkRateLimit(reg, player) then
186
+ if statsEnabled then
187
+ reg.drops += 1
188
+ end
189
+ Middleware.fireDrop(player :: Player, "rate", reg.name, value)
190
+ continue
191
+ end
198
192
 
199
- if status == 1 then
200
- querySignal:fire(nil, player, correlationId)
201
- continue
202
- end
193
+ local ok, reason = Gate.validate(value, reg.codec)
194
+ if not ok then
195
+ if statsEnabled then
196
+ reg.drops += 1
197
+ end
198
+ if player then
199
+ Middleware.fireDrop(player, "validation", reg.name, value)
200
+ end
201
+ continue
202
+ end
203
203
 
204
- local value, consumed = queryCodec.read(incoming, pos, refs)
205
- pos += consumed
204
+ if reg.validate then
205
+ local valid, validReason = reg.validate(value, player :: Player)
206
+ if not valid then
207
+ if statsEnabled then
208
+ reg.drops += 1
209
+ end
210
+ Middleware.fireDrop(
211
+ player :: Player,
212
+ validReason or "validate",
213
+ reg.name,
214
+ value
215
+ )
216
+ continue
217
+ end
218
+ end
219
+ end
206
220
 
207
- if isServer and reg.needsGate and not gateCheck(value, reg, player :: Player) then
208
- continue
209
- end
221
+ if hasRecvHooks then
222
+ value = Middleware.runReceive(value, reg.name, player)
223
+ end
210
224
 
211
- local final: any
212
- if hasReceiveMiddleware then
213
- final = Middleware.runReceive(value, reg.baseName, player)
214
- if final == nil then
215
- continue
225
+ if statsEnabled then
226
+ reg.recvFires += 1
216
227
  end
217
- else
218
- final = value
228
+ reg.signal:fire(value, player, corrId)
219
229
  end
220
-
221
- querySignal:fire(final, player, correlationId)
222
230
  end
223
231
  end
224
232
  end