@axpecter/lync 2.3.2 → 3.0.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +302 -299
  3. package/Types.luau +215 -0
  4. package/api/Define.luau +154 -0
  5. package/api/Lifecycle.luau +119 -0
  6. package/api/Packet.luau +108 -0
  7. package/api/Query.luau +149 -0
  8. package/api/Set.luau +189 -0
  9. package/channel/Blocks.luau +124 -0
  10. package/channel/Group.luau +118 -0
  11. package/channel/Link.luau +135 -0
  12. package/channel/Outbox.luau +261 -0
  13. package/channel/Recipients.luau +129 -0
  14. package/channel/Transport.luau +138 -0
  15. package/codec/Base.luau +236 -0
  16. package/codec/Composites.luau +128 -0
  17. package/codec/Datatypes.luau +162 -0
  18. package/codec/Legality.luau +61 -0
  19. package/codec/Scalars.luau +72 -0
  20. package/codec/Text.luau +89 -0
  21. package/codec/init.luau +39 -0
  22. package/index.d.ts +285 -0
  23. package/init.luau +91 -0
  24. package/kernel/Bits.luau +16 -0
  25. package/kernel/Catalog.luau +121 -0
  26. package/kernel/Heap.luau +80 -0
  27. package/kernel/Listeners.luau +95 -0
  28. package/kernel/Log.luau +197 -0
  29. package/kernel/Tally.luau +128 -0
  30. package/kernel/Threads.luau +44 -0
  31. package/kernel/Wide.luau +267 -0
  32. package/package.json +15 -32
  33. package/runtime/Boundary.luau +180 -0
  34. package/runtime/Dispatch.luau +91 -0
  35. package/runtime/Handles.luau +114 -0
  36. package/runtime/Handshake.luau +90 -0
  37. package/runtime/Pending.luau +196 -0
  38. package/runtime/Router.luau +379 -0
  39. package/runtime/Session.luau +731 -0
  40. package/shape/Compile.luau +340 -0
  41. package/shape/Folds.luau +332 -0
  42. package/shape/Frames.luau +170 -0
  43. package/shape/Grid.luau +169 -0
  44. package/shape/Ir.luau +540 -0
  45. package/shape/Joins.luau +125 -0
  46. package/shape/Legality.luau +273 -0
  47. package/shape/Lower.luau +419 -0
  48. package/shape/Order.luau +64 -0
  49. package/shape/Regions.luau +113 -0
  50. package/shape/Rotation.luau +472 -0
  51. package/shape/Serial.luau +258 -0
  52. package/shape/Words.luau +89 -0
  53. package/shape/emit/Block.luau +453 -0
  54. package/shape/emit/Column.luau +338 -0
  55. package/shape/emit/Leaf.luau +462 -0
  56. package/shape/emit/Word.luau +234 -0
  57. package/shape/emit/init.luau +222 -0
  58. package/shape/init.luau +78 -0
  59. package/state/Apply.luau +330 -0
  60. package/state/Audience.luau +126 -0
  61. package/state/Catchup.luau +218 -0
  62. package/state/Coalesce.luau +43 -0
  63. package/state/Fieldset.luau +177 -0
  64. package/state/Newest.luau +339 -0
  65. package/state/Plan.luau +441 -0
  66. package/state/Sections.luau +468 -0
  67. package/state/Store.luau +188 -0
  68. package/state/Sync.luau +762 -0
  69. package/wire/Bytes.luau +118 -0
  70. package/wire/Clock.luau +37 -0
  71. package/wire/Protocol.luau +110 -0
  72. package/wire/Sequence.luau +40 -0
  73. package/wire/Varint.luau +124 -0
  74. package/src/Types.luau +0 -180
  75. package/src/api/Group.luau +0 -156
  76. package/src/api/Packet.luau +0 -319
  77. package/src/api/Query.luau +0 -338
  78. package/src/api/Scope.luau +0 -90
  79. package/src/api/Signal.luau +0 -188
  80. package/src/codec/Base.luau +0 -136
  81. package/src/codec/composite/Array.luau +0 -456
  82. package/src/codec/composite/Map.luau +0 -469
  83. package/src/codec/composite/Optional.luau +0 -54
  84. package/src/codec/composite/Shared.luau +0 -291
  85. package/src/codec/composite/Struct.luau +0 -494
  86. package/src/codec/composite/Tagged.luau +0 -103
  87. package/src/codec/composite/Tuple.luau +0 -143
  88. package/src/codec/datatype/Buffer.luau +0 -66
  89. package/src/codec/datatype/CFrame.luau +0 -109
  90. package/src/codec/datatype/Color.luau +0 -32
  91. package/src/codec/datatype/Instance.luau +0 -52
  92. package/src/codec/datatype/IntVector.luau +0 -39
  93. package/src/codec/datatype/NumberRange.luau +0 -39
  94. package/src/codec/datatype/Ray.luau +0 -36
  95. package/src/codec/datatype/Rect.luau +0 -31
  96. package/src/codec/datatype/Region.luau +0 -62
  97. package/src/codec/datatype/Sequence.luau +0 -184
  98. package/src/codec/datatype/String.luau +0 -94
  99. package/src/codec/datatype/UDim.luau +0 -46
  100. package/src/codec/datatype/Vector.luau +0 -130
  101. package/src/codec/meta/Auto.luau +0 -318
  102. package/src/codec/meta/Bitfield.luau +0 -147
  103. package/src/codec/meta/Custom.luau +0 -33
  104. package/src/codec/meta/DeltaScalar.luau +0 -390
  105. package/src/codec/meta/Enum.luau +0 -50
  106. package/src/codec/meta/Float.luau +0 -38
  107. package/src/codec/meta/Nothing.luau +0 -13
  108. package/src/codec/meta/Unknown.luau +0 -41
  109. package/src/codec/primitive/Bool.luau +0 -26
  110. package/src/codec/primitive/Float16.luau +0 -99
  111. package/src/codec/primitive/Int.luau +0 -59
  112. package/src/codec/primitive/Number.luau +0 -24
  113. package/src/codec/primitive/Signed.luau +0 -32
  114. package/src/codec/primitive/Varint.luau +0 -165
  115. package/src/codec/primitive/Zint.luau +0 -99
  116. package/src/index.d.ts +0 -423
  117. package/src/init.luau +0 -362
  118. package/src/internal/Baseline.luau +0 -57
  119. package/src/internal/Channel.luau +0 -390
  120. package/src/internal/Middleware.luau +0 -100
  121. package/src/internal/Pool.luau +0 -65
  122. package/src/internal/Registry.luau +0 -119
  123. package/src/internal/Transport.luau +0 -28
  124. package/src/transport/Bridge.luau +0 -110
  125. package/src/transport/Client.luau +0 -97
  126. package/src/transport/Gate.luau +0 -488
  127. package/src/transport/Reader.luau +0 -331
  128. package/src/transport/Server.luau +0 -211
  129. package/src/util/Array.luau +0 -18
  130. package/src/util/Buffer.luau +0 -90
  131. package/src/util/Constants.luau +0 -30
  132. package/src/util/Log.luau +0 -68
  133. package/src/util/Player.luau +0 -14
  134. package/src/util/Quantize.luau +0 -84
  135. package/src/util/Quat.luau +0 -124
@@ -1,110 +0,0 @@
1
- --!strict
2
- --!optimize 2
3
- -- RemoteEvent / UnreliableRemoteEvent wrapper.
4
-
5
- local RunService = game:GetService("RunService")
6
-
7
- local Log = require(script.Parent.Parent.util.Log)
8
-
9
- -- Constants --------------------------------------------------------------
10
-
11
- local RELIABLE_NAME = "LyncReliable"
12
- local UNRELIABLE_NAME = "LyncUnreliable"
13
-
14
- -- State ------------------------------------------------------------------
15
-
16
- local _reliable: RemoteEvent? = nil
17
- local _unreliable: UnreliableRemoteEvent? = nil
18
- local _isServer = RunService:IsServer()
19
-
20
- -- Private ----------------------------------------------------------------
21
-
22
- --[[
23
- Captured to a local first so Log.error's `never` return narrows the
24
- optional. Reading the module global directly wouldn't narrow.
25
- ]]
26
- local function ensureReliable(): RemoteEvent
27
- local r = _reliable
28
- if not r then
29
- Log.error("not set up; call Lync.start() first")
30
- end
31
- return r
32
- end
33
-
34
- local function ensureUnreliable(): UnreliableRemoteEvent
35
- local u = _unreliable
36
- if not u then
37
- Log.error("not set up; call Lync.start() first")
38
- end
39
- return u
40
- end
41
-
42
- -- Drop the trailing arg when refs is empty: Roblox serializes empty tables as several wire bytes.
43
- local function fireToServer(re: any, data: buffer, refs: { Instance }): ()
44
- if #refs > 0 then
45
- re:FireServer(data, refs)
46
- else
47
- re:FireServer(data)
48
- end
49
- end
50
-
51
- local function fireToClient(re: any, player: Player, data: buffer, refs: { Instance }): ()
52
- if #refs > 0 then
53
- re:FireClient(player, data, refs)
54
- else
55
- re:FireClient(player, data)
56
- end
57
- end
58
-
59
- -- Public -----------------------------------------------------------------
60
-
61
- local Bridge = {}
62
-
63
- function Bridge.setup(container: Instance): ()
64
- if _isServer then
65
- local re = Instance.new("RemoteEvent")
66
- re.Name = RELIABLE_NAME
67
- re.Parent = container
68
-
69
- local ure = Instance.new("UnreliableRemoteEvent")
70
- ure.Name = UNRELIABLE_NAME
71
- ure.Parent = container
72
-
73
- _reliable, _unreliable = re, ure
74
- return
75
- end
76
- _reliable = container:WaitForChild(RELIABLE_NAME) :: RemoteEvent
77
- _unreliable = container:WaitForChild(UNRELIABLE_NAME) :: UnreliableRemoteEvent
78
- end
79
-
80
- Bridge.reliable = ensureReliable
81
- Bridge.unreliable = ensureUnreliable
82
-
83
- function Bridge.fireServer(data: buffer, refs: { Instance }): ()
84
- fireToServer(ensureReliable(), data, refs)
85
- end
86
-
87
- function Bridge.fireClient(player: Player, data: buffer, refs: { Instance }): ()
88
- fireToClient(ensureReliable(), player, data, refs)
89
- end
90
-
91
- function Bridge.fireServerUnreliable(data: buffer, refs: { Instance }): ()
92
- fireToServer(ensureUnreliable(), data, refs)
93
- end
94
-
95
- function Bridge.fireClientUnreliable(player: Player, data: buffer, refs: { Instance }): ()
96
- fireToClient(ensureUnreliable(), player, data, refs)
97
- end
98
-
99
- -- Test injection point: harnesses substitute fake remotes.
100
- function Bridge.attach(reliable: any, unreliable: any): ()
101
- _reliable = reliable
102
- _unreliable = unreliable
103
- end
104
-
105
- function Bridge.reset(): ()
106
- _reliable = nil
107
- _unreliable = nil
108
- end
109
-
110
- return table.freeze(Bridge)
@@ -1,97 +0,0 @@
1
- --!strict
2
- --!optimize 2
3
- -- Client transport: single channel pair with outbound XOR delta.
4
-
5
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
6
-
7
- local Bridge = require(script.Parent.Parent.transport.Bridge)
8
- local Channel = require(script.Parent.Parent.internal.Channel)
9
- local Reader = require(script.Parent.Parent.transport.Reader)
10
- local Types = require(script.Parent.Parent.Types)
11
-
12
- -- Constants --------------------------------------------------------------
13
-
14
- local CONTAINER_NAME = "LyncRemotes"
15
-
16
- -- State ------------------------------------------------------------------
17
-
18
- local _reliable: Types.ChannelState = Channel.create()
19
- local _unreliable: Types.ChannelState = Channel.create()
20
- local _prevIncoming: buffer? = nil
21
- local _prevIncomingLen: number = 0
22
-
23
- -- Private ----------------------------------------------------------------
24
-
25
- local xorApply = Channel.xorApply
26
- local sealAndDump = Channel.sealAndDump
27
- local resetCh = Channel.reset
28
-
29
- local function handleIncomingReliable(data: any, refs: any?): ()
30
- local incoming = Reader.decodeIncoming(data)
31
- if not incoming then
32
- return
33
- end
34
-
35
- local incomingLen = buffer.len(incoming)
36
- local decoded = xorApply(incoming, incomingLen, _prevIncoming, _prevIncomingLen)
37
-
38
- --[[
39
- Mirror Server: only adopt `decoded` as the next baseline on success.
40
- A failed parse means the wire is suspect; reusing it would poison
41
- every subsequent packet.
42
- ]]
43
- if Reader.process(decoded, incomingLen, refs, nil, false) then
44
- _prevIncoming = decoded
45
- _prevIncomingLen = incomingLen
46
- else
47
- _prevIncoming = nil
48
- _prevIncomingLen = 0
49
- end
50
- end
51
-
52
- local function handleIncomingUnreliable(data: any, refs: any?): ()
53
- local incoming = Reader.decodeIncoming(data)
54
- if not incoming then
55
- return
56
- end
57
- Reader.process(incoming, buffer.len(incoming), refs, nil, false)
58
- end
59
-
60
- -- Public -----------------------------------------------------------------
61
-
62
- local Client = {}
63
-
64
- function Client.getChannel(isUnreliable: boolean): Types.ChannelState
65
- return if isUnreliable then _unreliable else _reliable
66
- end
67
-
68
- function Client.flush(): ()
69
- if Channel.hasTimestamps() then
70
- Channel.updateTimestamp()
71
- end
72
-
73
- if _reliable.cursor > 0 then
74
- local snapshot, refs, byteLen = sealAndDump(_reliable)
75
- local xored = xorApply(snapshot, byteLen, _reliable.prevDump, _reliable.prevDumpLen)
76
- _reliable.prevDump = snapshot
77
- _reliable.prevDumpLen = byteLen
78
- Bridge.fireServer(xored, refs)
79
- resetCh(_reliable)
80
- end
81
-
82
- if _unreliable.cursor > 0 then
83
- local snapshot, refs = sealAndDump(_unreliable)
84
- Bridge.fireServerUnreliable(snapshot, refs)
85
- resetCh(_unreliable)
86
- end
87
- end
88
-
89
- function Client.start(): ()
90
- local container = ReplicatedStorage:WaitForChild(CONTAINER_NAME)
91
- Bridge.setup(container)
92
-
93
- Bridge.reliable().OnClientEvent:Connect(handleIncomingReliable)
94
- Bridge.unreliable().OnClientEvent:Connect(handleIncomingUnreliable)
95
- end
96
-
97
- return table.freeze(Client)
@@ -1,488 +0,0 @@
1
- --!strict
2
- --!native
3
- -- Schema-aware validation and rate limiting.
4
-
5
- local Log = require(script.Parent.Parent.util.Log)
6
- local Types = require(script.Parent.Parent.Types)
7
-
8
- -- Constants --------------------------------------------------------------
9
-
10
- local DEFAULT_DEPTH = 16
11
- local STRIKE_DECAY_SEC = 5
12
-
13
- local VEC2_AXES = table.freeze({ "X", "Y" })
14
- local VEC3_AXES = table.freeze({ "X", "Y", "Z" })
15
-
16
- -- State ------------------------------------------------------------------
17
-
18
- local _depth = DEFAULT_DEPTH
19
- local _globalMaxPerSecond: number? = nil
20
- local _bandwidthSoftLimit: number? = nil
21
- local _bandwidthMaxStrikes = 10
22
- local _rateLimits: { [number]: { [Player]: Types.RateLimitState } } = {}
23
- local _globalLimits: { [Player]: Types.RateLimitState } = {}
24
- local _bandwidthStrikes: { [Player]: number } = {}
25
- local _bandwidthLastSeen: { [Player]: number } = {}
26
-
27
- -- Private ----------------------------------------------------------------
28
-
29
- local isfinite = math.isfinite
30
- local floor = math.floor
31
- local clock = os.clock
32
- local minN = math.min
33
-
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?
38
- if not isfinite(value) then
39
- return "non-finite"
40
- end
41
- if minV and value < minV then
42
- return `below min {minV}`
43
- end
44
- if maxV and value > maxV then
45
- return `above max {maxV}`
46
- end
47
- return nil
48
- end
49
-
50
- local function checkVectorComponents(
51
- value: any,
52
- components: { string },
53
- minV: number,
54
- maxV: number
55
- ): string?
56
- for _, axis in components do
57
- local v = value[axis]
58
- if not isfinite(v) then
59
- return `component {axis} non-finite`
60
- end
61
- if v < minV or v > maxV then
62
- return `component {axis} ({v}) out of [{minV}, {maxV}]`
63
- end
64
- end
65
- return nil
66
- end
67
-
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
- ]]
73
- local function scanValue(value: any, depth: number): boolean
74
- if depth <= 0 then
75
- return true
76
- end
77
-
78
- local t = typeof(value)
79
- if t == "number" then
80
- return isfinite(value)
81
- end
82
- if t == "Vector2" then
83
- return isfinite(value.X) and isfinite(value.Y)
84
- end
85
- if t == "Vector3" then
86
- return isfinite(value.X) and isfinite(value.Y) and isfinite(value.Z)
87
- end
88
- if t == "CFrame" then
89
- local pos = value.Position
90
- if not (isfinite(pos.X) and isfinite(pos.Y) and isfinite(pos.Z)) then
91
- return false
92
- end
93
- local rx, ry, rz = value:ToEulerAnglesXYZ()
94
- return isfinite(rx) and isfinite(ry) and isfinite(rz)
95
- end
96
- if t == "UDim" then
97
- return isfinite(value.Scale) and isfinite(value.Offset)
98
- end
99
- if t == "UDim2" then
100
- return isfinite(value.X.Scale)
101
- and isfinite(value.X.Offset)
102
- and isfinite(value.Y.Scale)
103
- and isfinite(value.Y.Offset)
104
- end
105
- if t == "Color3" then
106
- return isfinite(value.R) and isfinite(value.G) and isfinite(value.B)
107
- end
108
- if t == "NumberRange" then
109
- return isfinite(value.Min) and isfinite(value.Max)
110
- end
111
- if t == "Rect" then
112
- return isfinite(value.Min.X)
113
- and isfinite(value.Min.Y)
114
- and isfinite(value.Max.X)
115
- and isfinite(value.Max.Y)
116
- end
117
- if t == "Ray" then
118
- local o, d = value.Origin, value.Direction
119
- return isfinite(o.X)
120
- and isfinite(o.Y)
121
- and isfinite(o.Z)
122
- and isfinite(d.X)
123
- and isfinite(d.Y)
124
- and isfinite(d.Z)
125
- end
126
- if t == "table" then
127
- local nextDepth = depth - 1
128
- for _, v in value do
129
- if not scanValue(v, nextDepth) then
130
- return false
131
- end
132
- end
133
- end
134
- return true
135
- end
136
-
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}"`
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
199
-
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
266
- if value == nil then
267
- return nil
268
- end
269
- codec = codec._inner :: Types.InternalCodec<any>
270
- depth -= 1
271
- end
272
- if depth <= 0 then
273
- return nil
274
- end
275
-
276
- local typeCheck = codec._typeCheck
277
- if typeCheck and typeof(value) ~= typeCheck then
278
- return `expected {typeCheck}, got {typeof(value)}`
279
- end
280
-
281
- if typeCheck == "string" then
282
- local reason = validateString(value, codec)
283
- if reason then
284
- return reason
285
- end
286
- end
287
-
288
- if codec._isBool then
289
- return nil
290
- end
291
- if codec._isEnum then
292
- return validateEnum(value, codec)
293
- end
294
- if codec._isInteger then
295
- return validateInteger(value, codec)
296
- end
297
-
298
- if (typeCheck == "Vector2" or typeCheck == "Vector3") and codec._min and codec._max then
299
- local axes = if typeCheck == "Vector2" then VEC2_AXES else VEC3_AXES
300
- return checkVectorComponents(value, axes, codec._min, codec._max)
301
- end
302
- if typeCheck == "number" and codec._min ~= nil and codec._max ~= nil then
303
- return checkScalarBounds(value, codec._min, codec._max)
304
- end
305
-
306
- if codec._isTagged then
307
- return validateTagged(value, codec, depth)
308
- end
309
- if codec._isArray then
310
- return validateArray(value, codec, depth)
311
- end
312
- if codec._isMap then
313
- return validateMap(value, codec, depth)
314
- end
315
- if codec._isTuple then
316
- return validateTuple(value, codec, depth)
317
- end
318
- if codec._schema then
319
- return validateStruct(value, codec, depth)
320
- end
321
-
322
- -- No specific metadata: fall back to a depth-bounded NaN/inf scan.
323
- if not scanValue(value, depth) then
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
339
- end
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
359
- end
360
-
361
- -- Public -----------------------------------------------------------------
362
-
363
- local Gate = {}
364
-
365
- function Gate.setDepth(depth: number): ()
366
- if depth < 1 then
367
- Log.error(`depth must be >= 1, got {depth}`)
368
- end
369
- _depth = depth
370
- end
371
-
372
- function Gate.validate(value: any, codec: Types.InternalCodec<any>): string?
373
- return validateWithCodec(value, codec, _depth)
374
- end
375
-
376
- function Gate.scan(value: any): boolean
377
- return scanValue(value, _depth)
378
- end
379
-
380
- function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
381
- local config = reg.rateLimit
382
- if not config then
383
- return true
384
- end
385
-
386
- local now = clock()
387
-
388
- local perPacket = _rateLimits[reg.id]
389
- if not perPacket then
390
- perPacket = {}
391
- _rateLimits[reg.id] = perPacket
392
- end
393
-
394
- local burst = config.burst or 1
395
- local state = getOrCreateBucket(perPacket, player, burst, now)
396
-
397
- -- Cooldown form: reject anything within `cooldown` seconds of the last accept.
398
- if config.cooldown then
399
- if now - state.lastAccepted < config.cooldown then
400
- return false
401
- end
402
- state.lastAccepted = now
403
- return true
404
- end
405
-
406
- local maxPerSecond = config.maxPerSecond
407
- if not maxPerSecond then
408
- return true
409
- end
410
- return consumeToken(state, now, burst, maxPerSecond)
411
- end
412
-
413
- function Gate.checkGlobalRateLimit(player: Player): boolean
414
- local maxPS = _globalMaxPerSecond
415
- if not maxPS then
416
- return true
417
- end
418
-
419
- local now = clock()
420
- local state = getOrCreateBucket(_globalLimits, player, maxPS, now)
421
- return consumeToken(state, now, maxPS, maxPS)
422
- end
423
-
424
- --[[
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.
428
- ]]
429
- function Gate.checkBandwidth(player: Player, bytes: number): boolean
430
- if not _bandwidthSoftLimit then
431
- return true
432
- end
433
-
434
- local now = clock()
435
- local last = _bandwidthLastSeen[player]
436
- local strikes = _bandwidthStrikes[player] or 0
437
-
438
- if last then
439
- local decay = floor((now - last) / STRIKE_DECAY_SEC)
440
- if decay > 0 then
441
- strikes = if decay >= strikes then 0 else strikes - decay
442
- end
443
- end
444
- _bandwidthLastSeen[player] = now
445
-
446
- if bytes <= _bandwidthSoftLimit then
447
- if strikes > 0 then
448
- strikes -= 1
449
- end
450
- _bandwidthStrikes[player] = strikes
451
- return true
452
- end
453
-
454
- strikes += 1
455
- _bandwidthStrikes[player] = strikes
456
- return strikes <= _bandwidthMaxStrikes
457
- end
458
-
459
- function Gate.configureGlobalRateLimit(maxPerSecond: number): ()
460
- _globalMaxPerSecond = maxPerSecond
461
- end
462
-
463
- function Gate.configureBandwidth(softLimit: number, maxStrikes: number): ()
464
- _bandwidthSoftLimit = softLimit
465
- _bandwidthMaxStrikes = maxStrikes
466
- end
467
-
468
- function Gate.clearPlayer(player: Player): ()
469
- _globalLimits[player] = nil
470
- _bandwidthStrikes[player] = nil
471
- _bandwidthLastSeen[player] = nil
472
- for _, perPacket in _rateLimits do
473
- perPacket[player] = nil
474
- end
475
- end
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
-
488
- return table.freeze(Gate)