@axpecter/lync 2.2.1 → 2.3.2

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 (65) hide show
  1. package/README.md +109 -147
  2. package/package.json +1 -1
  3. package/src/Types.luau +34 -22
  4. package/src/api/Group.luau +40 -33
  5. package/src/api/Packet.luau +140 -66
  6. package/src/api/Query.luau +103 -65
  7. package/src/api/Scope.luau +26 -10
  8. package/src/api/Signal.luau +43 -52
  9. package/src/codec/Base.luau +28 -14
  10. package/src/codec/composite/Array.luau +296 -115
  11. package/src/codec/composite/Map.luau +377 -57
  12. package/src/codec/composite/Optional.luau +10 -2
  13. package/src/codec/composite/Shared.luau +134 -64
  14. package/src/codec/composite/Struct.luau +294 -43
  15. package/src/codec/composite/Tagged.luau +21 -16
  16. package/src/codec/composite/Tuple.luau +32 -15
  17. package/src/codec/datatype/Buffer.luau +7 -7
  18. package/src/codec/datatype/CFrame.luau +34 -122
  19. package/src/codec/datatype/Color.luau +10 -10
  20. package/src/codec/datatype/Instance.luau +15 -12
  21. package/src/codec/datatype/IntVector.luau +2 -2
  22. package/src/codec/datatype/NumberRange.luau +15 -8
  23. package/src/codec/datatype/Ray.luau +13 -14
  24. package/src/codec/datatype/Rect.luau +12 -12
  25. package/src/codec/datatype/Region.luau +13 -14
  26. package/src/codec/datatype/Sequence.luau +87 -65
  27. package/src/codec/datatype/String.luau +17 -10
  28. package/src/codec/datatype/UDim.luau +10 -8
  29. package/src/codec/datatype/Vector.luau +20 -59
  30. package/src/codec/meta/Auto.luau +94 -126
  31. package/src/codec/meta/Bitfield.luau +12 -14
  32. package/src/codec/meta/Custom.luau +3 -1
  33. package/src/codec/meta/DeltaScalar.luau +390 -0
  34. package/src/codec/meta/Enum.luau +9 -9
  35. package/src/codec/meta/Float.luau +6 -28
  36. package/src/codec/meta/Nothing.luau +1 -1
  37. package/src/codec/meta/Unknown.luau +10 -7
  38. package/src/codec/primitive/Bool.luau +6 -4
  39. package/src/codec/primitive/Float16.luau +5 -2
  40. package/src/codec/primitive/Int.luau +5 -6
  41. package/src/codec/primitive/Number.luau +6 -4
  42. package/src/codec/primitive/Signed.luau +2 -2
  43. package/src/codec/primitive/Varint.luau +76 -39
  44. package/src/codec/primitive/Zint.luau +99 -0
  45. package/src/index.d.ts +207 -53
  46. package/src/init.luau +116 -103
  47. package/src/internal/Baseline.luau +9 -1
  48. package/src/internal/Channel.luau +191 -157
  49. package/src/internal/Middleware.luau +22 -6
  50. package/src/internal/Pool.luau +12 -4
  51. package/src/internal/Registry.luau +25 -16
  52. package/src/internal/Transport.luau +1 -1
  53. package/src/transport/Bridge.luau +47 -33
  54. package/src/transport/Client.luau +25 -21
  55. package/src/transport/Gate.luau +227 -172
  56. package/src/transport/Reader.luau +174 -106
  57. package/src/transport/Server.luau +46 -57
  58. package/src/util/Array.luau +18 -0
  59. package/src/util/Buffer.luau +90 -0
  60. package/src/util/Constants.luau +30 -0
  61. package/src/util/Log.luau +68 -0
  62. package/src/util/Player.luau +14 -0
  63. package/src/util/Quantize.luau +84 -0
  64. package/src/util/Quat.luau +124 -0
  65. package/src/internal/Util.luau +0 -26
@@ -2,6 +2,7 @@
2
2
  --!native
3
3
  -- Schema-aware validation and rate limiting.
4
4
 
5
+ local Log = require(script.Parent.Parent.util.Log)
5
6
  local Types = require(script.Parent.Parent.Types)
6
7
 
7
8
  -- Constants --------------------------------------------------------------
@@ -9,6 +10,9 @@ local Types = require(script.Parent.Parent.Types)
9
10
  local DEFAULT_DEPTH = 16
10
11
  local STRIKE_DECAY_SEC = 5
11
12
 
13
+ local VEC2_AXES = table.freeze({ "X", "Y" })
14
+ local VEC3_AXES = table.freeze({ "X", "Y", "Z" })
15
+
12
16
  -- State ------------------------------------------------------------------
13
17
 
14
18
  local _depth = DEFAULT_DEPTH
@@ -27,17 +31,20 @@ local floor = math.floor
27
31
  local clock = os.clock
28
32
  local minN = math.min
29
33
 
30
- local function checkScalarBounds(value: number, minV: number?, maxV: number?): (boolean, string?)
34
+ -- Validators return string?: nil = accepted, non-nil = rejection reason.
35
+ local validateWithCodec -- forward decl (recursive)
36
+
37
+ local function checkScalarBounds(value: number, minV: number?, maxV: number?): string?
31
38
  if not isfinite(value) then
32
- return false, "non-finite"
39
+ return "non-finite"
33
40
  end
34
41
  if minV and value < minV then
35
- return false, `below min {minV}`
42
+ return `below min {minV}`
36
43
  end
37
44
  if maxV and value > maxV then
38
- return false, `above max {maxV}`
45
+ return `above max {maxV}`
39
46
  end
40
- return true, nil
47
+ return nil
41
48
  end
42
49
 
43
50
  local function checkVectorComponents(
@@ -45,22 +52,24 @@ local function checkVectorComponents(
45
52
  components: { string },
46
53
  minV: number,
47
54
  maxV: number
48
- ): (boolean, string?)
55
+ ): string?
49
56
  for _, axis in components do
50
57
  local v = value[axis]
51
58
  if not isfinite(v) then
52
- return false, `component {axis} non-finite`
59
+ return `component {axis} non-finite`
53
60
  end
54
61
  if v < minV or v > maxV then
55
- return false, `component {axis} ({v}) out of [{minV}, {maxV}]`
62
+ return `component {axis} ({v}) out of [{minV}, {maxV}]`
56
63
  end
57
64
  end
58
- return true, nil
65
+ return nil
59
66
  end
60
67
 
61
- local VEC2_AXES = table.freeze({ "X", "Y" })
62
- local VEC3_AXES = table.freeze({ "X", "Y", "Z" })
63
-
68
+ --[[
69
+ Untyped fallback NaN/inf scan. Used when a codec lacks specific metadata
70
+ (e.g. user custom codec) and as the last step of validateWithCodec.
71
+ Bounded by `depth` so a self-referential table can't recurse forever.
72
+ ]]
64
73
  local function scanValue(value: any, depth: number): boolean
65
74
  if depth <= 0 then
66
75
  return true
@@ -125,168 +134,228 @@ local function scanValue(value: any, depth: number): boolean
125
134
  return true
126
135
  end
127
136
 
128
- local function validateWithCodec(
129
- value: any,
130
- codec: Types.InternalCodec<any>,
131
- depth: number
132
- ): (boolean, string?)
133
- if depth <= 0 then
134
- return true, nil
137
+ local function validateString(value: any, codec: Types.InternalCodec<any>): string?
138
+ local maxLen = codec._maxStringLength
139
+ if maxLen and #(value :: string) > maxLen then
140
+ return `string length {#value} exceeds max {maxLen}`
141
+ end
142
+ return nil
143
+ end
144
+
145
+ local function validateEnum(value: any, codec: Types.InternalCodec<any>): string?
146
+ local valid = codec._enumValues
147
+ if valid and valid[value] == nil then
148
+ return `not a valid enum value: "{value}"`
149
+ end
150
+ return nil
151
+ end
152
+
153
+ local function validateInteger(value: any, codec: Types.InternalCodec<any>): string?
154
+ if not isfinite(value) then
155
+ return "non-finite"
156
+ end
157
+ if floor(value) ~= value then
158
+ return "non-integer"
159
+ end
160
+ return checkScalarBounds(value, codec._min, codec._max)
161
+ end
162
+
163
+ local function validateTagged(value: any, codec: Types.InternalCodec<any>, depth: number): string?
164
+ if typeof(value) ~= "table" then
165
+ return "expected table for tagged variant"
166
+ end
167
+ local tagField = codec._tagField :: string
168
+ local tagName = value[tagField]
169
+ if tagName == nil then
170
+ return `missing tagField "{tagField}"`
171
+ end
172
+ local variants = codec._variants :: { [string]: Types.InternalCodec<any> }
173
+ local variant = variants[tagName]
174
+ if not variant then
175
+ return `unknown variant "{tagName}"`
135
176
  end
177
+ return validateWithCodec(value, variant, depth - 1)
178
+ end
179
+
180
+ local function validateArray(value: any, codec: Types.InternalCodec<any>, depth: number): string?
181
+ if typeof(value) ~= "table" then
182
+ return "expected array"
183
+ end
184
+ local maxCount = codec._maxCount
185
+ local len = #value
186
+ if maxCount and len > maxCount then
187
+ return `array length {len} exceeds max {maxCount}`
188
+ end
189
+ local element = codec._element :: Types.InternalCodec<any>
190
+ local nextDepth = depth - 1
191
+ for i = 1, len do
192
+ local reason = validateWithCodec(value[i], element, nextDepth)
193
+ if reason then
194
+ return `[{i}]: {reason}`
195
+ end
196
+ end
197
+ return nil
198
+ end
136
199
 
137
- -- Optional: branch on nil before any other check.
138
- if codec._isOptional then
200
+ local function validateMap(value: any, codec: Types.InternalCodec<any>, depth: number): string?
201
+ if typeof(value) ~= "table" then
202
+ return "expected map"
203
+ end
204
+ local maxCount = codec._maxCount
205
+ local keyCodec = codec._keyCodec :: Types.InternalCodec<any>
206
+ local valCodec = codec._valueCodec :: Types.InternalCodec<any>
207
+ local nextDepth = depth - 1
208
+ local count = 0
209
+ for k, v in value do
210
+ count += 1
211
+ if maxCount and count > maxCount then
212
+ return `map size exceeds max {maxCount}`
213
+ end
214
+ local reason = validateWithCodec(k, keyCodec, nextDepth)
215
+ if reason then
216
+ return `key: {reason}`
217
+ end
218
+ reason = validateWithCodec(v, valCodec, nextDepth)
219
+ if reason then
220
+ return `value at key "{tostring(k)}": {reason}`
221
+ end
222
+ end
223
+ return nil
224
+ end
225
+
226
+ local function validateTuple(value: any, codec: Types.InternalCodec<any>, depth: number): string?
227
+ if typeof(value) ~= "table" then
228
+ return "expected tuple"
229
+ end
230
+ local elements = codec._elements :: { Types.InternalCodec<any> }
231
+ local nextDepth = depth - 1
232
+ for i = 1, #elements do
233
+ local reason = validateWithCodec(value[i], elements[i], nextDepth)
234
+ if reason then
235
+ return `[{i}]: {reason}`
236
+ end
237
+ end
238
+ return nil
239
+ end
240
+
241
+ local function validateStruct(value: any, codec: Types.InternalCodec<any>, depth: number): string?
242
+ if typeof(value) ~= "table" then
243
+ return "expected table for struct"
244
+ end
245
+ local nextDepth = depth - 1
246
+ for key, fieldCodec in codec._schema :: { [string]: Types.InternalCodec<any> } do
247
+ local fieldVal = value[key]
248
+ -- Bool fields default to false when missing; everything else must be present.
249
+ if fieldVal == nil and not fieldCodec._isBool then
250
+ return `missing field "{key}"`
251
+ end
252
+ local reason = validateWithCodec(fieldVal, fieldCodec, nextDepth)
253
+ if reason then
254
+ return `field "{key}": {reason}`
255
+ end
256
+ end
257
+ return nil
258
+ end
259
+
260
+ function validateWithCodec(value: any, codec: Types.InternalCodec<any>, depth: number): string?
261
+ -- Peel Optional layers in a loop so optional(optional(X)) chains stay flat.
262
+ while codec._isOptional do
263
+ if depth <= 0 then
264
+ return nil
265
+ end
139
266
  if value == nil then
140
- return true, nil
267
+ return nil
141
268
  end
142
- local inner = codec._inner :: Types.InternalCodec<any>
143
- return validateWithCodec(value, inner, depth - 1)
269
+ codec = codec._inner :: Types.InternalCodec<any>
270
+ depth -= 1
271
+ end
272
+ if depth <= 0 then
273
+ return nil
144
274
  end
145
275
 
146
- -- typeCheck (cheap fail-fast for non-composite codecs).
147
276
  local typeCheck = codec._typeCheck
148
277
  if typeCheck and typeof(value) ~= typeCheck then
149
- return false, `expected {typeCheck}, got {typeof(value)}`
278
+ return `expected {typeCheck}, got {typeof(value)}`
150
279
  end
151
280
 
152
- if codec._isBool then
153
- return true, nil
281
+ if typeCheck == "string" then
282
+ local reason = validateString(value, codec)
283
+ if reason then
284
+ return reason
285
+ end
154
286
  end
155
287
 
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.
288
+ if codec._isBool then
289
+ return nil
290
+ end
159
291
  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
164
- return true, nil
292
+ return validateEnum(value, codec)
165
293
  end
166
-
167
294
  if codec._isInteger then
168
- if not isfinite(value) then
169
- return false, "non-finite"
170
- end
171
- if floor(value) ~= value then
172
- return false, "non-integer"
173
- end
174
- return checkScalarBounds(value, codec._min, codec._max)
295
+ return validateInteger(value, codec)
175
296
  end
176
297
 
177
- -- Vector quantized: per-component range check.
178
298
  if (typeCheck == "Vector2" or typeCheck == "Vector3") and codec._min and codec._max then
179
299
  local axes = if typeCheck == "Vector2" then VEC2_AXES else VEC3_AXES
180
300
  return checkVectorComponents(value, axes, codec._min, codec._max)
181
301
  end
182
-
183
- -- Scalar quantized float (number with explicit range).
184
302
  if typeCheck == "number" and codec._min ~= nil and codec._max ~= nil then
185
303
  return checkScalarBounds(value, codec._min, codec._max)
186
304
  end
187
305
 
188
306
  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)
307
+ return validateTagged(value, codec, depth)
203
308
  end
204
-
205
309
  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}`
213
- end
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
221
- end
222
- return true, nil
310
+ return validateArray(value, codec, depth)
223
311
  end
224
-
225
312
  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
247
- end
248
- return true, nil
313
+ return validateMap(value, codec, depth)
249
314
  end
250
-
251
315
  if codec._isTuple then
252
- if typeof(value) ~= "table" then
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
262
- end
263
- return true, nil
316
+ return validateTuple(value, codec, depth)
264
317
  end
265
-
266
318
  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
272
- local fieldVal = value[key]
273
- if fieldVal == nil and not fieldCodec._isBool then
274
- return false, `missing field "{key}"`
275
- end
276
- local ok, reason = validateWithCodec(fieldVal, fieldCodec, nextDepth)
277
- if not ok then
278
- return false, `field "{key}": {reason}`
279
- end
280
- end
281
- return true, nil
319
+ return validateStruct(value, codec, depth)
282
320
  end
283
321
 
284
- -- Fallback: NaN/inf scan for codecs without specific metadata
285
- -- (raw f32/f64, untagged Roblox types, buffer, etc.).
322
+ -- No specific metadata: fall back to a depth-bounded NaN/inf scan.
286
323
  if not scanValue(value, depth) then
287
- return false, "NaN/inf detected"
324
+ return "NaN/inf detected"
325
+ end
326
+ return nil
327
+ end
328
+
329
+ local function getOrCreateBucket(
330
+ map: { [Player]: Types.RateLimitState },
331
+ player: Player,
332
+ burst: number,
333
+ now: number
334
+ ): Types.RateLimitState
335
+ local state = map[player]
336
+ if not state then
337
+ state = { tokens = burst, lastRefill = now, lastAccepted = 0 }
338
+ map[player] = state
288
339
  end
289
- return true, nil
340
+ return state
341
+ end
342
+
343
+ local function consumeToken(
344
+ state: Types.RateLimitState,
345
+ now: number,
346
+ capacity: number,
347
+ rate: number
348
+ ): boolean
349
+ local elapsed = now - state.lastRefill
350
+ state.lastRefill = now
351
+ state.tokens = minN(capacity, state.tokens + elapsed * rate)
352
+
353
+ if state.tokens >= 1 then
354
+ state.tokens -= 1
355
+ state.lastAccepted = now
356
+ return true
357
+ end
358
+ return false
290
359
  end
291
360
 
292
361
  -- Public -----------------------------------------------------------------
@@ -295,12 +364,12 @@ local Gate = {}
295
364
 
296
365
  function Gate.setDepth(depth: number): ()
297
366
  if depth < 1 then
298
- error(`[Lync] Gate.setDepth: depth must be >= 1, got {depth}`)
367
+ Log.error(`depth must be >= 1, got {depth}`)
299
368
  end
300
369
  _depth = depth
301
370
  end
302
371
 
303
- function Gate.validate(value: any, codec: Types.InternalCodec<any>): (boolean, string?)
372
+ function Gate.validate(value: any, codec: Types.InternalCodec<any>): string?
304
373
  return validateWithCodec(value, codec, _depth)
305
374
  end
306
375
 
@@ -322,12 +391,10 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
322
391
  _rateLimits[reg.id] = perPacket
323
392
  end
324
393
 
325
- local state = perPacket[player]
326
- if not state then
327
- state = { tokens = config.burst or 1, lastRefill = now, lastAccepted = 0 }
328
- perPacket[player] = state
329
- end
394
+ local burst = config.burst or 1
395
+ local state = getOrCreateBucket(perPacket, player, burst, now)
330
396
 
397
+ -- Cooldown form: reject anything within `cooldown` seconds of the last accept.
331
398
  if config.cooldown then
332
399
  if now - state.lastAccepted < config.cooldown then
333
400
  return false
@@ -340,17 +407,7 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
340
407
  if not maxPerSecond then
341
408
  return true
342
409
  end
343
-
344
- local elapsed = now - state.lastRefill
345
- state.lastRefill = now
346
- state.tokens = minN(config.burst or 1, state.tokens + elapsed * maxPerSecond)
347
-
348
- if state.tokens >= 1 then
349
- state.tokens -= 1
350
- state.lastAccepted = now
351
- return true
352
- end
353
- return false
410
+ return consumeToken(state, now, burst, maxPerSecond)
354
411
  end
355
412
 
356
413
  function Gate.checkGlobalRateLimit(player: Player): boolean
@@ -360,27 +417,14 @@ function Gate.checkGlobalRateLimit(player: Player): boolean
360
417
  end
361
418
 
362
419
  local now = clock()
363
- local state = _globalLimits[player]
364
- if not state then
365
- state = { tokens = maxPS, lastRefill = now, lastAccepted = 0 }
366
- _globalLimits[player] = state
367
- end
368
-
369
- local elapsed = now - state.lastRefill
370
- state.lastRefill = now
371
- state.tokens = minN(maxPS, state.tokens + elapsed * maxPS)
372
-
373
- if state.tokens >= 1 then
374
- state.tokens -= 1
375
- return true
376
- end
377
- return false
420
+ local state = getOrCreateBucket(_globalLimits, player, maxPS, now)
421
+ return consumeToken(state, now, maxPS, maxPS)
378
422
  end
379
423
 
380
424
  --[[
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.
425
+ Bandwidth check. Frames at or below softLimit refund a strike; frames
426
+ above add one. Strikes decay with idle time so a quiet player isn't
427
+ penalized indefinitely after a noisy burst.
384
428
  ]]
385
429
  function Gate.checkBandwidth(player: Player, bytes: number): boolean
386
430
  if not _bandwidthSoftLimit then
@@ -430,4 +474,15 @@ function Gate.clearPlayer(player: Player): ()
430
474
  end
431
475
  end
432
476
 
477
+ function Gate.reset(): ()
478
+ _depth = DEFAULT_DEPTH
479
+ _globalMaxPerSecond = nil
480
+ _bandwidthSoftLimit = nil
481
+ _bandwidthMaxStrikes = 10
482
+ table.clear(_rateLimits)
483
+ table.clear(_globalLimits)
484
+ table.clear(_bandwidthStrikes)
485
+ table.clear(_bandwidthLastSeen)
486
+ end
487
+
433
488
  return table.freeze(Gate)