@axpecter/lync 2.2.0 → 2.3.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 (62) hide show
  1. package/README.md +95 -143
  2. package/package.json +1 -1
  3. package/src/Types.luau +22 -16
  4. package/src/api/Group.luau +40 -33
  5. package/src/api/Packet.luau +95 -72
  6. package/src/api/Query.luau +128 -79
  7. package/src/api/Scope.luau +26 -10
  8. package/src/api/Signal.luau +43 -52
  9. package/src/codec/Base.luau +21 -14
  10. package/src/codec/composite/Array.luau +56 -134
  11. package/src/codec/composite/Map.luau +103 -72
  12. package/src/codec/composite/Optional.luau +6 -2
  13. package/src/codec/composite/Shared.luau +160 -69
  14. package/src/codec/composite/Struct.luau +283 -42
  15. package/src/codec/composite/Tagged.luau +13 -16
  16. package/src/codec/composite/Tuple.luau +21 -15
  17. package/src/codec/datatype/Buffer.luau +6 -4
  18. package/src/codec/datatype/CFrame.luau +56 -44
  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/Enum.luau +9 -9
  34. package/src/codec/meta/Float.luau +6 -28
  35. package/src/codec/meta/Nothing.luau +1 -1
  36. package/src/codec/meta/Unknown.luau +10 -7
  37. package/src/codec/primitive/Bool.luau +6 -4
  38. package/src/codec/primitive/Float16.luau +5 -2
  39. package/src/codec/primitive/Int.luau +5 -6
  40. package/src/codec/primitive/Number.luau +6 -4
  41. package/src/codec/primitive/Signed.luau +2 -2
  42. package/src/codec/primitive/Varint.luau +69 -38
  43. package/src/index.d.ts +161 -53
  44. package/src/init.luau +123 -102
  45. package/src/internal/Baseline.luau +9 -1
  46. package/src/internal/Channel.luau +153 -154
  47. package/src/internal/Middleware.luau +22 -6
  48. package/src/internal/Pool.luau +12 -4
  49. package/src/internal/Registry.luau +25 -16
  50. package/src/internal/Transport.luau +1 -1
  51. package/src/transport/Bridge.luau +47 -33
  52. package/src/transport/Client.luau +25 -21
  53. package/src/transport/Gate.luau +227 -172
  54. package/src/transport/Reader.luau +162 -105
  55. package/src/transport/Server.luau +46 -57
  56. package/src/util/Array.luau +18 -0
  57. package/src/util/Buffer.luau +92 -0
  58. package/src/util/Constants.luau +30 -0
  59. package/src/util/Log.luau +68 -0
  60. package/src/util/Player.luau +14 -0
  61. package/src/util/Quantize.luau +60 -0
  62. package/src/internal/Util.luau +0 -26
package/src/init.luau CHANGED
@@ -4,18 +4,20 @@
4
4
 
5
5
  local RunService = game:GetService("RunService")
6
6
 
7
- local Base = require(script.codec.Base)
7
+ local Baseline = require(script.internal.Baseline)
8
+ local Bridge = require(script.transport.Bridge)
8
9
  local Channel = require(script.internal.Channel)
9
10
  local Gate = require(script.transport.Gate)
10
11
  local Group = require(script.api.Group)
12
+ local Log = require(script.util.Log)
11
13
  local Middleware = require(script.internal.Middleware)
12
14
  local Packet = require(script.api.Packet)
15
+ local Player = require(script.util.Player)
13
16
  local Pool = require(script.internal.Pool)
14
17
  local Query = require(script.api.Query)
15
18
  local Registry = require(script.internal.Registry)
16
19
  local Scope = require(script.api.Scope)
17
20
  local Types = require(script.Types)
18
- local Util = require(script.internal.Util)
19
21
 
20
22
  local ArrayC = require(script.codec.composite.Array)
21
23
  local AutoC = require(script.codec.meta.Auto)
@@ -48,12 +50,31 @@ local UDimC = require(script.codec.datatype.UDim)
48
50
  local UnknownC = require(script.codec.meta.Unknown)
49
51
  local VectorC = require(script.codec.datatype.Vector)
50
52
 
53
+ -- Public types -----------------------------------------------------------
54
+
55
+ export type Codec<T> = Types.Codec<T>
56
+ export type Connection = Types.Connection
57
+ export type Packet<T> = Packet.PacketHandle<T>
58
+ export type Query<Req, Resp> = Query.QueryHandle<Req, Resp>
59
+ export type Group = Group.GroupHandle
60
+ export type Scope = Scope.ScopeHandle
61
+ export type PacketOptions = Types.PacketOptions
62
+ export type QueryOptions = Types.QueryOptions
63
+ export type ConfigureOptions = Types.ConfigureOptions
64
+ export type RateLimitConfig = Types.RateLimitConfig
65
+ export type PacketStats = Types.PacketStats
66
+ export type PlayerStats = Types.PlayerStats
67
+
51
68
  -- Constants --------------------------------------------------------------
52
69
 
53
70
  local IS_SERVER = RunService:IsServer()
54
71
 
55
- local DROP = table.freeze({ _lyncDrop = true })
56
- local ALL = table.freeze({ _lyncAll = true })
72
+ --[[
73
+ Singleton-string brand on a single `_lyncKind` field: dispatch is one
74
+ hash lookup at the same predicted slot, and the literals narrow on `==`.
75
+ ]]
76
+ local ALL = table.freeze({ _lyncKind = "all" :: "all" })
77
+ local DROP = table.freeze({ _lyncKind = "drop" :: "drop" })
57
78
 
58
79
  local CHANNEL_MAX_MIN, CHANNEL_MAX_MAX = 4096, 1048576
59
80
  local DEPTH_MIN, DEPTH_MAX = 4, 32
@@ -62,12 +83,8 @@ local FLUSH_HZ_MIN, FLUSH_HZ_MAX = 1, 60
62
83
 
63
84
  -- State ------------------------------------------------------------------
64
85
 
65
- --[[
66
- Wrap mutable flush state in one table so the Heartbeat closure
67
- captures a single LCT_VAL upvalue instead of separate LCT_REF
68
- upvalues for each field.
69
- ]]
70
- local _flushState = {
86
+ -- Single table so the Heartbeat closure captures one LCT_VAL, not N LCT_REFs.
87
+ local flushState = {
71
88
  started = false,
72
89
  rate = FLUSH_HZ_MAX,
73
90
  accum = 0,
@@ -77,95 +94,86 @@ local _flushState = {
77
94
 
78
95
  -- Private ----------------------------------------------------------------
79
96
 
80
- local isPlayer = Util.isPlayer
97
+ local isPlayer = Player.is
81
98
 
82
- local function except(...: any): any
99
+ type ExceptValue = { _lyncKind: "except", _excluded: { [Player]: boolean } }
100
+
101
+ local function except(...: any): ExceptValue
83
102
  local excluded: { [Player]: boolean } = {}
84
103
 
85
104
  for i = 1, select("#", ...) do
86
105
  local arg = select(i, ...)
87
106
  if isPlayer(arg) then
88
107
  excluded[arg :: Player] = true
89
- elseif typeof(arg) == "table" and arg._lyncGroup then
90
- for player in arg._members :: { [Player]: boolean } do
108
+ elseif typeof(arg) == "table" and arg._lyncKind == "group" then
109
+ for player in arg._members do
91
110
  excluded[player] = true
92
111
  end
93
112
  else
94
- error("[Lync] Lync.except: argument must be a Player or Group")
113
+ Log.error(`argument must be a Player or Group, got {typeof(arg)}`)
95
114
  end
96
115
  end
97
116
 
98
117
  return table.freeze({
99
- _lyncExcept = true,
118
+ _lyncKind = "except" :: "except",
100
119
  _excluded = table.freeze(excluded),
101
120
  })
102
121
  end
103
122
 
104
- local function setupFlush(): ()
105
- local transport: { flush: () -> () }
106
- if IS_SERVER then
107
- local Server = require(script.transport.Server)
108
- transport = { flush = Server.flush }
109
- else
110
- local Client = require(script.transport.Client)
111
- transport = { flush = Client.flush }
112
- end
113
- _flushState.transport = transport
123
+ local function setupFlush(transport: { flush: () -> () }): ()
124
+ flushState.transport = transport
114
125
 
115
- _flushState.conn = RunService.Heartbeat:Connect(function(dt: number)
116
- local rate = _flushState.rate
126
+ flushState.conn = RunService.Heartbeat:Connect(function(dt: number)
127
+ local rate = flushState.rate
117
128
  if rate >= FLUSH_HZ_MAX then
118
129
  transport.flush()
119
130
  return
120
131
  end
121
132
 
122
- local accum = _flushState.accum + dt
133
+ local accum = flushState.accum + dt
123
134
  local interval = 1 / rate
124
135
  if accum >= interval then
125
- -- Drift guard: if we're more than one interval behind (frame
126
- -- stall) zero the accumulator instead of catching up.
136
+ -- Frame-stall guard: zero the accumulator instead of catching up.
127
137
  accum = if accum >= 2 * interval then 0 else accum - interval
128
138
  transport.flush()
129
139
  end
130
- _flushState.accum = accum
140
+ flushState.accum = accum
131
141
  end)
132
142
  end
133
143
 
144
+ -- Validates `value` in [min, max] and applies it; errors with a configure-formatted message.
145
+ local function applyOption(
146
+ name: string,
147
+ value: number?,
148
+ min: number,
149
+ max: number,
150
+ apply: (number) -> ()
151
+ ): ()
152
+ if value == nil then
153
+ return
154
+ end
155
+ if value < min or value > max then
156
+ Log.error(`{name} must be {min}-{max}`)
157
+ end
158
+ apply(value)
159
+ end
160
+
134
161
  -- Public -----------------------------------------------------------------
135
162
 
136
163
  local Lync = {}
137
164
 
138
165
  function Lync.configure(options: Types.ConfigureOptions): ()
139
- if _flushState.started then
140
- error("[Lync] Lync.configure: must be called before Lync.start()")
141
- end
142
-
143
- local size = options.channelMaxSize
144
- if size then
145
- if size < CHANNEL_MAX_MIN or size > CHANNEL_MAX_MAX then
146
- error(
147
- `[Lync] Lync.configure: channelMaxSize must be {CHANNEL_MAX_MIN}-{CHANNEL_MAX_MAX}`
148
- )
149
- end
150
- Channel.setMaxSize(size)
151
- Base.setMaxSize(size)
152
- end
153
-
154
- local depth = options.validationDepth
155
- if depth then
156
- if depth < DEPTH_MIN or depth > DEPTH_MAX then
157
- error(`[Lync] Lync.configure: validationDepth must be {DEPTH_MIN}-{DEPTH_MAX}`)
158
- end
159
- Gate.setDepth(depth)
160
- end
161
-
162
- local poolSize = options.poolSize
163
- if poolSize then
164
- if poolSize < POOL_MIN or poolSize > POOL_MAX then
165
- error(`[Lync] Lync.configure: poolSize must be {POOL_MIN}-{POOL_MAX}`)
166
- end
167
- Pool.setMaxSize(poolSize)
168
- end
166
+ Log.assert(not flushState.started, "must be called before Lync.start()")
167
+
168
+ applyOption(
169
+ "channelMaxSize",
170
+ options.channelMaxSize,
171
+ CHANNEL_MAX_MIN,
172
+ CHANNEL_MAX_MAX,
173
+ Channel.setMaxSize
174
+ )
175
+ applyOption("validationDepth", options.validationDepth, DEPTH_MIN, DEPTH_MAX, Gate.setDepth)
176
+ applyOption("poolSize", options.poolSize, POOL_MIN, POOL_MAX, Pool.setMaxSize)
169
177
 
170
178
  if options.bandwidthLimit then
171
179
  Gate.configureBandwidth(options.bandwidthLimit.softLimit, options.bandwidthLimit.maxStrikes)
@@ -177,49 +185,64 @@ function Lync.configure(options: Types.ConfigureOptions): ()
177
185
 
178
186
  if options.stats then
179
187
  Channel.enableStats()
180
- if IS_SERVER then
181
- local Server = require(script.transport.Server)
182
- Server.enableStats()
183
- end
184
188
  end
185
189
  end
186
190
 
187
191
  function Lync.start(): ()
188
- if _flushState.started then
189
- error("[Lync] Lync.start: already started")
190
- end
191
- _flushState.started = true
192
+ Log.assert(not flushState.started, "already started")
193
+ flushState.started = true
192
194
 
193
195
  if IS_SERVER then
194
196
  local Server = require(script.transport.Server)
195
197
  Server.start()
198
+ setupFlush(Server)
196
199
  else
197
200
  local Client = require(script.transport.Client)
198
201
  Client.start()
202
+ setupFlush(Client)
199
203
  end
200
-
201
- setupFlush()
202
204
  end
203
205
 
204
206
  function Lync.flush(): ()
205
- if not _flushState.started then
206
- error("[Lync] Lync.flush: must call Lync.start() first")
207
- end
208
- -- start() is the only path that sets transport, so it's non-nil here.
209
- (_flushState.transport :: { flush: () -> () }).flush()
210
- _flushState.accum = 0
207
+ Log.assert(flushState.started, "must call Lync.start() first")
208
+ -- Bind to a local: bare `(expr).flush()` after a statement parses as a continuation.
209
+ local transport = flushState.transport :: { flush: () -> () }
210
+ transport.flush()
211
+ flushState.accum = 0
211
212
  end
212
213
 
213
214
  function Lync.flushRate(hz: number): ()
214
215
  if hz < FLUSH_HZ_MIN or hz > FLUSH_HZ_MAX then
215
- error(`[Lync] Lync.flushRate: hz must be {FLUSH_HZ_MIN}-{FLUSH_HZ_MAX}`)
216
+ Log.error(`hz must be {FLUSH_HZ_MIN}-{FLUSH_HZ_MAX}`)
216
217
  end
217
- _flushState.rate = hz
218
- _flushState.accum = 0
218
+ flushState.rate = hz
219
+ flushState.accum = 0
219
220
  end
220
221
 
221
222
  function Lync.isStarted(): boolean
222
- return _flushState.started
223
+ return flushState.started
224
+ end
225
+
226
+ -- Restore module state to post-require defaults. For tests / hot reload.
227
+ function Lync.reset(): ()
228
+ if flushState.conn then
229
+ flushState.conn:Disconnect()
230
+ flushState.conn = nil
231
+ end
232
+ flushState.started = false
233
+ flushState.rate = FLUSH_HZ_MAX
234
+ flushState.accum = 0
235
+ flushState.transport = nil
236
+
237
+ Baseline.reset()
238
+ Bridge.reset()
239
+ Channel.resetGlobals()
240
+ Gate.reset()
241
+ Group.reset()
242
+ Log.reset()
243
+ Middleware.reset()
244
+ Pool.reset()
245
+ Registry.reset()
223
246
  end
224
247
 
225
248
  Lync.packet = Packet.define
@@ -249,20 +272,21 @@ Lync.stats = table.freeze({
249
272
  local Server = require(script.transport.Server)
250
273
  Server.resetStats()
251
274
  end
252
- for i = 1, Registry.count() do
253
- local reg = Registry.get(i)
254
- if reg then
255
- reg.bytesSent = 0
256
- reg.bytesReceived = 0
257
- reg.fires = 0
258
- reg.recvFires = 0
259
- reg.drops = 0
260
- end
275
+ for _, reg in Registry.all() do
276
+ reg.bytesSent = 0
277
+ reg.bytesReceived = 0
278
+ reg.fires = 0
279
+ reg.recvFires = 0
280
+ reg.drops = 0
261
281
  end
262
282
  end,
263
283
  })
264
284
 
265
285
  Lync.debug = table.freeze({
286
+ --[[
287
+ Stubs reserved for capture/replay tooling. No-ops keep the API
288
+ surface stable across versions with or without the recorder.
289
+ ]]
266
290
  capture = function(_label: string?): () end,
267
291
  stop = function(): () end,
268
292
  dump = function(): () end,
@@ -272,20 +296,17 @@ Lync.debug = table.freeze({
272
296
  end,
273
297
 
274
298
  registrations = function(): { any }
299
+ -- Registry IDs are sequential 1..count and never freed, so get(i) is non-nil in range.
275
300
  local count = Registry.count()
276
301
  local result = table.create(count)
277
- local n = 0
278
302
  for i = 1, count do
279
- local reg = Registry.get(i)
280
- if reg then
281
- n += 1
282
- result[n] = table.freeze({
283
- name = reg.name,
284
- id = reg.id,
285
- kind = reg.kind,
286
- isUnreliable = reg.isUnreliable,
287
- })
288
- end
303
+ local reg = Registry.get(i) :: Types.Registration
304
+ result[i] = table.freeze({
305
+ name = reg.name,
306
+ id = reg.id,
307
+ kind = reg.kind,
308
+ isUnreliable = reg.isUnreliable,
309
+ })
289
310
  end
290
311
  return table.freeze(result)
291
312
  end,
@@ -1,11 +1,12 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Read-side delta cache for delta codecs.
3
+ -- Read-side delta cache, keyed by (deltaId, peer).
4
4
 
5
5
  local Types = require(script.Parent.Parent.Types)
6
6
 
7
7
  -- State ------------------------------------------------------------------
8
8
 
9
+ -- Per-deltaId map keyed by BaselineKey: server uses Player, client uses `false`.
9
10
  local _caches: { [number]: { [Types.BaselineKey]: any } } = {}
10
11
  local _currentKey: Types.BaselineKey = false
11
12
  local _hasDelta = false
@@ -38,6 +39,7 @@ function Baseline.clearPlayer(key: Types.BaselineKey): ()
38
39
  end
39
40
  end
40
41
 
42
+ -- Reader checks this once to skip baseline-key bookkeeping when no delta codec exists.
41
43
  function Baseline.markHasDelta(): ()
42
44
  _hasDelta = true
43
45
  end
@@ -46,4 +48,10 @@ function Baseline.hasDelta(): boolean
46
48
  return _hasDelta
47
49
  end
48
50
 
51
+ function Baseline.reset(): ()
52
+ table.clear(_caches)
53
+ _currentKey = false
54
+ _hasDelta = false
55
+ end
56
+
49
57
  return table.freeze(Baseline)