@axpecter/lync 2.1.2 → 2.2.1

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 +163 -152
  6. package/src/api/Query.luau +224 -211
  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 +133 -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
@@ -2,120 +2,111 @@
2
2
  --!native
3
3
  -- Schema-aware validation and rate limiting.
4
4
 
5
- local Middleware = require(script.Parent.Parent.internal.Middleware)
6
- local Registry = require(script.Parent.Parent.internal.Registry)
7
5
  local Types = require(script.Parent.Parent.Types)
8
6
 
9
- -- Constants -----------------------------------------------------------
7
+ -- Constants --------------------------------------------------------------
10
8
 
11
9
  local DEFAULT_DEPTH = 16
10
+ local STRIKE_DECAY_SEC = 5
12
11
 
13
- -- State ---------------------------------------------------------------
12
+ -- State ------------------------------------------------------------------
14
13
 
15
14
  local _depth = DEFAULT_DEPTH
16
- local _rateLimits: { [number]: { [Player]: Types.RateLimitState } } = {}
17
- local _globalLimits: { [Player]: Types.RateLimitState } = {}
18
15
  local _globalMaxPerSecond: number? = nil
19
16
  local _bandwidthSoftLimit: number? = nil
20
- local _bandwidthMaxStrikes: number? = nil
17
+ local _bandwidthMaxStrikes = 10
18
+ local _rateLimits: { [number]: { [Player]: Types.RateLimitState } } = {}
19
+ local _globalLimits: { [Player]: Types.RateLimitState } = {}
21
20
  local _bandwidthStrikes: { [Player]: number } = {}
21
+ local _bandwidthLastSeen: { [Player]: number } = {}
22
22
 
23
- -- Private -------------------------------------------------------------
23
+ -- Private ----------------------------------------------------------------
24
24
 
25
25
  local isfinite = math.isfinite
26
26
  local floor = math.floor
27
- local abs = math.abs
28
27
  local clock = os.clock
28
+ local minN = math.min
29
29
 
30
- --[[
31
- Checks a number value for NaN/inf and optionally range + integrality.
32
- Returns true if the value passes all checks.
33
- ]]
34
- local function validateNumber(
35
- value: number,
36
- isInteger: boolean?,
37
- minVal: number?,
38
- maxVal: number?
39
- ): boolean
30
+ local function checkScalarBounds(value: number, minV: number?, maxV: number?): (boolean, string?)
40
31
  if not isfinite(value) then
41
- return false
32
+ return false, "non-finite"
42
33
  end
43
-
44
- if isInteger and floor(value) ~= value then
45
- return false
34
+ if minV and value < minV then
35
+ return false, `below min {minV}`
46
36
  end
47
-
48
- if minVal and value < minVal then
49
- return false
50
- end
51
- if maxVal and value > maxVal then
52
- return false
37
+ if maxV and value > maxV then
38
+ return false, `above max {maxV}`
53
39
  end
40
+ return true, nil
41
+ end
54
42
 
55
- return true
43
+ local function checkVectorComponents(
44
+ value: any,
45
+ components: { string },
46
+ minV: number,
47
+ maxV: number
48
+ ): (boolean, string?)
49
+ for _, axis in components do
50
+ local v = value[axis]
51
+ if not isfinite(v) then
52
+ return false, `component {axis} non-finite`
53
+ end
54
+ if v < minV or v > maxV then
55
+ return false, `component {axis} ({v}) out of [{minV}, {maxV}]`
56
+ end
57
+ end
58
+ return true, nil
56
59
  end
57
60
 
58
- --[[
59
- Recursive NaN/inf scan for values without schema metadata.
60
- Used as fallback for f32, f64, auto, and Roblox types.
61
- ]]
61
+ local VEC2_AXES = table.freeze({ "X", "Y" })
62
+ local VEC3_AXES = table.freeze({ "X", "Y", "Z" })
63
+
62
64
  local function scanValue(value: any, depth: number): boolean
63
65
  if depth <= 0 then
64
66
  return true
65
67
  end
66
68
 
67
69
  local t = typeof(value)
68
-
69
70
  if t == "number" then
70
71
  return isfinite(value)
71
72
  end
72
-
73
73
  if t == "Vector2" then
74
74
  return isfinite(value.X) and isfinite(value.Y)
75
75
  end
76
-
77
76
  if t == "Vector3" then
78
77
  return isfinite(value.X) and isfinite(value.Y) and isfinite(value.Z)
79
78
  end
80
-
81
79
  if t == "CFrame" then
82
80
  local pos = value.Position
83
- if not isfinite(pos.X) or not isfinite(pos.Y) or not isfinite(pos.Z) then
81
+ if not (isfinite(pos.X) and isfinite(pos.Y) and isfinite(pos.Z)) then
84
82
  return false
85
83
  end
86
84
  local rx, ry, rz = value:ToEulerAnglesXYZ()
87
85
  return isfinite(rx) and isfinite(ry) and isfinite(rz)
88
86
  end
89
-
90
87
  if t == "UDim" then
91
88
  return isfinite(value.Scale) and isfinite(value.Offset)
92
89
  end
93
-
94
90
  if t == "UDim2" then
95
91
  return isfinite(value.X.Scale)
96
92
  and isfinite(value.X.Offset)
97
93
  and isfinite(value.Y.Scale)
98
94
  and isfinite(value.Y.Offset)
99
95
  end
100
-
101
96
  if t == "Color3" then
102
97
  return isfinite(value.R) and isfinite(value.G) and isfinite(value.B)
103
98
  end
104
-
105
99
  if t == "NumberRange" then
106
100
  return isfinite(value.Min) and isfinite(value.Max)
107
101
  end
108
-
109
102
  if t == "Rect" then
110
103
  return isfinite(value.Min.X)
111
104
  and isfinite(value.Min.Y)
112
105
  and isfinite(value.Max.X)
113
106
  and isfinite(value.Max.Y)
114
107
  end
115
-
116
108
  if t == "Ray" then
117
- local o = value.Origin
118
- local d = value.Direction
109
+ local o, d = value.Origin, value.Direction
119
110
  return isfinite(o.X)
120
111
  and isfinite(o.Y)
121
112
  and isfinite(o.Z)
@@ -123,7 +114,6 @@ local function scanValue(value: any, depth: number): boolean
123
114
  and isfinite(d.Y)
124
115
  and isfinite(d.Z)
125
116
  end
126
-
127
117
  if t == "table" then
128
118
  local nextDepth = depth - 1
129
119
  for _, v in value do
@@ -131,17 +121,10 @@ local function scanValue(value: any, depth: number): boolean
131
121
  return false
132
122
  end
133
123
  end
134
- return true
135
124
  end
136
-
137
- -- string, boolean, nil, buffer, Instance, etc. are always clean
138
125
  return true
139
126
  end
140
127
 
141
- --[[
142
- Schema-aware validation. Dispatches based on codec metadata.
143
- Returns true if valid, false + reason if not.
144
- ]]
145
128
  local function validateWithCodec(
146
129
  value: any,
147
130
  codec: Types.InternalCodec<any>,
@@ -151,61 +134,146 @@ local function validateWithCodec(
151
134
  return true, nil
152
135
  end
153
136
 
154
- local codecAny = codec :: any
155
-
156
- -- Type check
157
- local typeCheck = codecAny._typeCheck
158
- if typeCheck then
159
- if typeof(value) ~= typeCheck then
160
- return false, `expected {typeCheck}, got {typeof(value)}`
137
+ -- Optional: branch on nil before any other check.
138
+ if codec._isOptional then
139
+ if value == nil then
140
+ return true, nil
161
141
  end
142
+ local inner = codec._inner :: Types.InternalCodec<any>
143
+ return validateWithCodec(value, inner, depth - 1)
162
144
  end
163
145
 
164
- -- Boolean: typeof check is sufficient
165
- if codecAny._isBool then
146
+ -- typeCheck (cheap fail-fast for non-composite codecs).
147
+ local typeCheck = codec._typeCheck
148
+ if typeCheck and typeof(value) ~= typeCheck then
149
+ return false, `expected {typeCheck}, got {typeof(value)}`
150
+ end
151
+
152
+ if codec._isBool then
153
+ return true, nil
154
+ end
155
+
156
+ -- Enum: value is a string (typeCheck already validated typeof);
157
+ -- decoded enums are guaranteed to be in the value set, but on
158
+ -- send-side a caller may pass an arbitrary string.
159
+ if codec._isEnum then
160
+ local valid = codec._enumValues
161
+ if valid and valid[value] == nil then
162
+ return false, `not a valid enum value: "{value}"`
163
+ end
166
164
  return true, nil
167
165
  end
168
166
 
169
- -- Integer + range
170
- if codecAny._isInteger then
167
+ if codec._isInteger then
171
168
  if not isfinite(value) then
172
169
  return false, "non-finite"
173
170
  end
174
171
  if floor(value) ~= value then
175
172
  return false, "non-integer"
176
173
  end
177
- local minV = codecAny._min
178
- local maxV = codecAny._max
179
- if minV and value < minV then
180
- return false, `below min {minV}`
174
+ return checkScalarBounds(value, codec._min, codec._max)
175
+ end
176
+
177
+ -- Vector quantized: per-component range check.
178
+ if (typeCheck == "Vector2" or typeCheck == "Vector3") and codec._min and codec._max then
179
+ local axes = if typeCheck == "Vector2" then VEC2_AXES else VEC3_AXES
180
+ return checkVectorComponents(value, axes, codec._min, codec._max)
181
+ end
182
+
183
+ -- Scalar quantized float (number with explicit range).
184
+ if typeCheck == "number" and codec._min ~= nil and codec._max ~= nil then
185
+ return checkScalarBounds(value, codec._min, codec._max)
186
+ end
187
+
188
+ if codec._isTagged then
189
+ if typeof(value) ~= "table" then
190
+ return false, "expected table for tagged variant"
191
+ end
192
+ local tagField = codec._tagField :: string
193
+ local tagName = value[tagField]
194
+ if tagName == nil then
195
+ return false, `missing tagField "{tagField}"`
196
+ end
197
+ local variants = codec._variants :: { [string]: Types.InternalCodec<any> }
198
+ local variant = variants[tagName]
199
+ if not variant then
200
+ return false, `unknown variant "{tagName}"`
201
+ end
202
+ return validateWithCodec(value, variant, depth - 1)
203
+ end
204
+
205
+ if codec._isArray then
206
+ if typeof(value) ~= "table" then
207
+ return false, "expected array"
208
+ end
209
+ local maxCount = codec._maxCount
210
+ local len = #value
211
+ if maxCount and len > maxCount then
212
+ return false, `array length {len} exceeds max {maxCount}`
181
213
  end
182
- if maxV and value > maxV then
183
- return false, `above max {maxV}`
214
+ local element = codec._element :: Types.InternalCodec<any>
215
+ local nextDepth = depth - 1
216
+ for i = 1, len do
217
+ local ok, reason = validateWithCodec(value[i], element, nextDepth)
218
+ if not ok then
219
+ return false, `[{i}]: {reason}`
220
+ end
184
221
  end
185
222
  return true, nil
186
223
  end
187
224
 
188
- -- Float with range (quantized float)
189
- if codecAny._min ~= nil and codecAny._max ~= nil and not codecAny._isInteger then
190
- if not isfinite(value) then
191
- return false, "non-finite"
225
+ if codec._isMap then
226
+ if typeof(value) ~= "table" then
227
+ return false, "expected map"
228
+ end
229
+ local maxCount = codec._maxCount
230
+ local keyCodec = codec._keyCodec :: Types.InternalCodec<any>
231
+ local valCodec = codec._valueCodec :: Types.InternalCodec<any>
232
+ local nextDepth = depth - 1
233
+ local count = 0
234
+ for k, v in value do
235
+ count += 1
236
+ if maxCount and count > maxCount then
237
+ return false, `map size exceeds max {maxCount}`
238
+ end
239
+ local ok, reason = validateWithCodec(k, keyCodec, nextDepth)
240
+ if not ok then
241
+ return false, `key: {reason}`
242
+ end
243
+ ok, reason = validateWithCodec(v, valCodec, nextDepth)
244
+ if not ok then
245
+ return false, `value at key "{tostring(k)}": {reason}`
246
+ end
192
247
  end
193
248
  return true, nil
194
249
  end
195
250
 
196
- -- Struct schema: recursive per-field validation
197
- if codecAny._schema then
251
+ if codec._isTuple then
198
252
  if typeof(value) ~= "table" then
199
- return false, "expected table for struct"
253
+ return false, "expected tuple"
254
+ end
255
+ local elements = codec._elements :: { Types.InternalCodec<any> }
256
+ local nextDepth = depth - 1
257
+ for i = 1, #elements do
258
+ local ok, reason = validateWithCodec(value[i], elements[i], nextDepth)
259
+ if not ok then
260
+ return false, `[{i}]: {reason}`
261
+ end
200
262
  end
263
+ return true, nil
264
+ end
201
265
 
202
- local schema = codecAny._schema
203
- for key, fieldCodec in schema do
266
+ if codec._schema then
267
+ if typeof(value) ~= "table" then
268
+ return false, "expected table for struct"
269
+ end
270
+ local nextDepth = depth - 1
271
+ for key, fieldCodec in codec._schema do
204
272
  local fieldVal = value[key]
205
- if fieldVal == nil and not (fieldCodec :: any)._isBool then
273
+ if fieldVal == nil and not fieldCodec._isBool then
206
274
  return false, `missing field "{key}"`
207
275
  end
208
- local ok, reason = validateWithCodec(fieldVal, fieldCodec, depth - 1)
276
+ local ok, reason = validateWithCodec(fieldVal, fieldCodec, nextDepth)
209
277
  if not ok then
210
278
  return false, `field "{key}": {reason}`
211
279
  end
@@ -213,19 +281,22 @@ local function validateWithCodec(
213
281
  return true, nil
214
282
  end
215
283
 
216
- -- Fallback: generic NaN scan for f32, f64, auto, and Roblox types
284
+ -- Fallback: NaN/inf scan for codecs without specific metadata
285
+ -- (raw f32/f64, untagged Roblox types, buffer, etc.).
217
286
  if not scanValue(value, depth) then
218
287
  return false, "NaN/inf detected"
219
288
  end
220
-
221
289
  return true, nil
222
290
  end
223
291
 
224
- -- Public --------------------------------------------------------------
292
+ -- Public -----------------------------------------------------------------
225
293
 
226
294
  local Gate = {}
227
295
 
228
296
  function Gate.setDepth(depth: number): ()
297
+ if depth < 1 then
298
+ error(`[Lync] Gate.setDepth: depth must be >= 1, got {depth}`)
299
+ end
229
300
  _depth = depth
230
301
  end
231
302
 
@@ -237,8 +308,6 @@ function Gate.scan(value: any): boolean
237
308
  return scanValue(value, _depth)
238
309
  end
239
310
 
240
- -- Rate limiting -------------------------------------------------------
241
-
242
311
  function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
243
312
  local config = reg.rateLimit
244
313
  if not config then
@@ -247,7 +316,6 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
247
316
 
248
317
  local now = clock()
249
318
 
250
- -- Get or create per-player state
251
319
  local perPacket = _rateLimits[reg.id]
252
320
  if not perPacket then
253
321
  perPacket = {}
@@ -256,15 +324,10 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
256
324
 
257
325
  local state = perPacket[player]
258
326
  if not state then
259
- state = {
260
- tokens = config.burst or 1,
261
- lastRefill = now,
262
- lastAccepted = 0,
263
- }
327
+ state = { tokens = config.burst or 1, lastRefill = now, lastAccepted = 0 }
264
328
  perPacket[player] = state
265
329
  end
266
330
 
267
- -- Cooldown mode
268
331
  if config.cooldown then
269
332
  if now - state.lastAccepted < config.cooldown then
270
333
  return false
@@ -273,7 +336,6 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
273
336
  return true
274
337
  end
275
338
 
276
- -- Token bucket mode
277
339
  local maxPerSecond = config.maxPerSecond
278
340
  if not maxPerSecond then
279
341
  return true
@@ -281,16 +343,13 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
281
343
 
282
344
  local elapsed = now - state.lastRefill
283
345
  state.lastRefill = now
284
-
285
- local burst = config.burst or 1
286
- state.tokens = math.min(burst, state.tokens + elapsed * maxPerSecond)
346
+ state.tokens = minN(config.burst or 1, state.tokens + elapsed * maxPerSecond)
287
347
 
288
348
  if state.tokens >= 1 then
289
349
  state.tokens -= 1
290
350
  state.lastAccepted = now
291
351
  return true
292
352
  end
293
-
294
353
  return false
295
354
  end
296
355
 
@@ -309,39 +368,50 @@ function Gate.checkGlobalRateLimit(player: Player): boolean
309
368
 
310
369
  local elapsed = now - state.lastRefill
311
370
  state.lastRefill = now
312
- state.tokens = math.min(maxPS, state.tokens + elapsed * maxPS)
371
+ state.tokens = minN(maxPS, state.tokens + elapsed * maxPS)
313
372
 
314
373
  if state.tokens >= 1 then
315
374
  state.tokens -= 1
316
375
  return true
317
376
  end
318
-
319
377
  return false
320
378
  end
321
379
 
380
+ --[[
381
+ Bandwidth check: frames at or below softLimit refund a strike;
382
+ over-soft frames add one. Strikes also decay with idle time so a
383
+ quiet player isn't penalized indefinitely.
384
+ ]]
322
385
  function Gate.checkBandwidth(player: Player, bytes: number): boolean
323
386
  if not _bandwidthSoftLimit then
324
387
  return true
325
388
  end
326
389
 
390
+ local now = clock()
391
+ local last = _bandwidthLastSeen[player]
392
+ local strikes = _bandwidthStrikes[player] or 0
393
+
394
+ if last then
395
+ local decay = floor((now - last) / STRIKE_DECAY_SEC)
396
+ if decay > 0 then
397
+ strikes = if decay >= strikes then 0 else strikes - decay
398
+ end
399
+ end
400
+ _bandwidthLastSeen[player] = now
401
+
327
402
  if bytes <= _bandwidthSoftLimit then
328
- local strikes = _bandwidthStrikes[player]
329
- if strikes and strikes > 0 then
330
- _bandwidthStrikes[player] = strikes - 1
403
+ if strikes > 0 then
404
+ strikes -= 1
331
405
  end
406
+ _bandwidthStrikes[player] = strikes
332
407
  return true
333
408
  end
334
409
 
335
- local current = _bandwidthStrikes[player]
336
- local strikes = (if current then current else 0) + 1
410
+ strikes += 1
337
411
  _bandwidthStrikes[player] = strikes
338
-
339
- local maxStrikes = if _bandwidthMaxStrikes then _bandwidthMaxStrikes else 10
340
- return strikes <= maxStrikes
412
+ return strikes <= _bandwidthMaxStrikes
341
413
  end
342
414
 
343
- -- Configuration -------------------------------------------------------
344
-
345
415
  function Gate.configureGlobalRateLimit(maxPerSecond: number): ()
346
416
  _globalMaxPerSecond = maxPerSecond
347
417
  end
@@ -354,7 +424,7 @@ end
354
424
  function Gate.clearPlayer(player: Player): ()
355
425
  _globalLimits[player] = nil
356
426
  _bandwidthStrikes[player] = nil
357
-
427
+ _bandwidthLastSeen[player] = nil
358
428
  for _, perPacket in _rateLimits do
359
429
  perPacket[player] = nil
360
430
  end