@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
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)
@@ -25,6 +27,7 @@ local BufferC = require(script.codec.datatype.Buffer)
25
27
  local CFrameC = require(script.codec.datatype.CFrame)
26
28
  local ColorC = require(script.codec.datatype.Color)
27
29
  local CustomC = require(script.codec.meta.Custom)
30
+ local DeltaScalarC = require(script.codec.meta.DeltaScalar)
28
31
  local EnumC = require(script.codec.meta.Enum)
29
32
  local Float16C = require(script.codec.primitive.Float16)
30
33
  local FloatC = require(script.codec.meta.Float)
@@ -47,8 +50,9 @@ local TupleC = require(script.codec.composite.Tuple)
47
50
  local UDimC = require(script.codec.datatype.UDim)
48
51
  local UnknownC = require(script.codec.meta.Unknown)
49
52
  local VectorC = require(script.codec.datatype.Vector)
53
+ local ZintC = require(script.codec.primitive.Zint)
50
54
 
51
- -- Public types (re-exported so users can write `Lync.Packet<MyData>`) ---
55
+ -- Public types -----------------------------------------------------------
52
56
 
53
57
  export type Codec<T> = Types.Codec<T>
54
58
  export type Connection = Types.Connection
@@ -67,8 +71,12 @@ export type PlayerStats = Types.PlayerStats
67
71
 
68
72
  local IS_SERVER = RunService:IsServer()
69
73
 
70
- local DROP = table.freeze({ _lyncDrop = true })
71
- local ALL = table.freeze({ _lyncAll = true })
74
+ --[[
75
+ Singleton-string brand on a single `_lyncKind` field: dispatch is one
76
+ hash lookup at the same predicted slot, and the literals narrow on `==`.
77
+ ]]
78
+ local ALL = table.freeze({ _lyncKind = "all" :: "all" })
79
+ local DROP = table.freeze({ _lyncKind = "drop" :: "drop" })
72
80
 
73
81
  local CHANNEL_MAX_MIN, CHANNEL_MAX_MAX = 4096, 1048576
74
82
  local DEPTH_MIN, DEPTH_MAX = 4, 32
@@ -77,12 +85,8 @@ local FLUSH_HZ_MIN, FLUSH_HZ_MAX = 1, 60
77
85
 
78
86
  -- State ------------------------------------------------------------------
79
87
 
80
- --[[
81
- Wrap mutable flush state in one table so the Heartbeat closure
82
- captures a single LCT_VAL upvalue instead of separate LCT_REF
83
- upvalues for each field.
84
- ]]
85
- local _flushState = {
88
+ -- Single table so the Heartbeat closure captures one LCT_VAL, not N LCT_REFs.
89
+ local flushState = {
86
90
  started = false,
87
91
  rate = FLUSH_HZ_MAX,
88
92
  accum = 0,
@@ -92,95 +96,86 @@ local _flushState = {
92
96
 
93
97
  -- Private ----------------------------------------------------------------
94
98
 
95
- local isPlayer = Util.isPlayer
99
+ local isPlayer = Player.is
100
+
101
+ type ExceptValue = { _lyncKind: "except", _excluded: { [Player]: boolean } }
96
102
 
97
- local function except(...: any): any
103
+ local function except(...: any): ExceptValue
98
104
  local excluded: { [Player]: boolean } = {}
99
105
 
100
106
  for i = 1, select("#", ...) do
101
107
  local arg = select(i, ...)
102
108
  if isPlayer(arg) then
103
109
  excluded[arg :: Player] = true
104
- elseif typeof(arg) == "table" and arg._lyncGroup then
105
- for player in arg._members :: { [Player]: boolean } do
110
+ elseif typeof(arg) == "table" and arg._lyncKind == "group" then
111
+ for player in arg._members do
106
112
  excluded[player] = true
107
113
  end
108
114
  else
109
- error("[Lync] Lync.except: argument must be a Player or Group")
115
+ Log.error(`argument must be a Player or Group, got {typeof(arg)}`)
110
116
  end
111
117
  end
112
118
 
113
119
  return table.freeze({
114
- _lyncExcept = true,
120
+ _lyncKind = "except" :: "except",
115
121
  _excluded = table.freeze(excluded),
116
122
  })
117
123
  end
118
124
 
119
- local function setupFlush(): ()
120
- local transport: { flush: () -> () }
121
- if IS_SERVER then
122
- local Server = require(script.transport.Server)
123
- transport = { flush = Server.flush }
124
- else
125
- local Client = require(script.transport.Client)
126
- transport = { flush = Client.flush }
127
- end
128
- _flushState.transport = transport
125
+ local function setupFlush(transport: { flush: () -> () }): ()
126
+ flushState.transport = transport
129
127
 
130
- _flushState.conn = RunService.Heartbeat:Connect(function(dt: number)
131
- local rate = _flushState.rate
128
+ flushState.conn = RunService.Heartbeat:Connect(function(dt: number)
129
+ local rate = flushState.rate
132
130
  if rate >= FLUSH_HZ_MAX then
133
131
  transport.flush()
134
132
  return
135
133
  end
136
134
 
137
- local accum = _flushState.accum + dt
135
+ local accum = flushState.accum + dt
138
136
  local interval = 1 / rate
139
137
  if accum >= interval then
140
- -- Drift guard: if we're more than one interval behind (frame
141
- -- stall) zero the accumulator instead of catching up.
138
+ -- Frame-stall guard: zero the accumulator instead of catching up.
142
139
  accum = if accum >= 2 * interval then 0 else accum - interval
143
140
  transport.flush()
144
141
  end
145
- _flushState.accum = accum
142
+ flushState.accum = accum
146
143
  end)
147
144
  end
148
145
 
146
+ -- Validates `value` in [min, max] and applies it; errors with a configure-formatted message.
147
+ local function applyOption(
148
+ name: string,
149
+ value: number?,
150
+ min: number,
151
+ max: number,
152
+ apply: (number) -> ()
153
+ ): ()
154
+ if value == nil then
155
+ return
156
+ end
157
+ if value < min or value > max then
158
+ Log.error(`{name} must be {min}-{max}`)
159
+ end
160
+ apply(value)
161
+ end
162
+
149
163
  -- Public -----------------------------------------------------------------
150
164
 
151
165
  local Lync = {}
152
166
 
153
167
  function Lync.configure(options: Types.ConfigureOptions): ()
154
- if _flushState.started then
155
- error("[Lync] Lync.configure: must be called before Lync.start()")
156
- end
157
-
158
- local size = options.channelMaxSize
159
- if size then
160
- if size < CHANNEL_MAX_MIN or size > CHANNEL_MAX_MAX then
161
- error(
162
- `[Lync] Lync.configure: channelMaxSize must be {CHANNEL_MAX_MIN}-{CHANNEL_MAX_MAX}`
163
- )
164
- end
165
- Channel.setMaxSize(size)
166
- Base.setMaxSize(size)
167
- end
168
-
169
- local depth = options.validationDepth
170
- if depth then
171
- if depth < DEPTH_MIN or depth > DEPTH_MAX then
172
- error(`[Lync] Lync.configure: validationDepth must be {DEPTH_MIN}-{DEPTH_MAX}`)
173
- end
174
- Gate.setDepth(depth)
175
- end
176
-
177
- local poolSize = options.poolSize
178
- if poolSize then
179
- if poolSize < POOL_MIN or poolSize > POOL_MAX then
180
- error(`[Lync] Lync.configure: poolSize must be {POOL_MIN}-{POOL_MAX}`)
181
- end
182
- Pool.setMaxSize(poolSize)
183
- end
168
+ Log.assert(not flushState.started, "must be called before Lync.start()")
169
+
170
+ applyOption(
171
+ "channelMaxSize",
172
+ options.channelMaxSize,
173
+ CHANNEL_MAX_MIN,
174
+ CHANNEL_MAX_MAX,
175
+ Channel.setMaxSize
176
+ )
177
+ applyOption("validationDepth", options.validationDepth, DEPTH_MIN, DEPTH_MAX, Gate.setDepth)
178
+ applyOption("poolSize", options.poolSize, POOL_MIN, POOL_MAX, Pool.setMaxSize)
184
179
 
185
180
  if options.bandwidthLimit then
186
181
  Gate.configureBandwidth(options.bandwidthLimit.softLimit, options.bandwidthLimit.maxStrikes)
@@ -192,49 +187,64 @@ function Lync.configure(options: Types.ConfigureOptions): ()
192
187
 
193
188
  if options.stats then
194
189
  Channel.enableStats()
195
- if IS_SERVER then
196
- local Server = require(script.transport.Server)
197
- Server.enableStats()
198
- end
199
190
  end
200
191
  end
201
192
 
202
193
  function Lync.start(): ()
203
- if _flushState.started then
204
- error("[Lync] Lync.start: already started")
205
- end
206
- _flushState.started = true
194
+ Log.assert(not flushState.started, "already started")
195
+ flushState.started = true
207
196
 
208
197
  if IS_SERVER then
209
198
  local Server = require(script.transport.Server)
210
199
  Server.start()
200
+ setupFlush(Server)
211
201
  else
212
202
  local Client = require(script.transport.Client)
213
203
  Client.start()
204
+ setupFlush(Client)
214
205
  end
215
-
216
- setupFlush()
217
206
  end
218
207
 
219
208
  function Lync.flush(): ()
220
- if not _flushState.started then
221
- error("[Lync] Lync.flush: must call Lync.start() first")
222
- end
223
- -- start() is the only path that sets transport, so it's non-nil here.
224
- (_flushState.transport :: { flush: () -> () }).flush()
225
- _flushState.accum = 0
209
+ Log.assert(flushState.started, "must call Lync.start() first")
210
+ -- Bind to a local: bare `(expr).flush()` after a statement parses as a continuation.
211
+ local transport = flushState.transport :: { flush: () -> () }
212
+ transport.flush()
213
+ flushState.accum = 0
226
214
  end
227
215
 
228
216
  function Lync.flushRate(hz: number): ()
229
217
  if hz < FLUSH_HZ_MIN or hz > FLUSH_HZ_MAX then
230
- error(`[Lync] Lync.flushRate: hz must be {FLUSH_HZ_MIN}-{FLUSH_HZ_MAX}`)
218
+ Log.error(`hz must be {FLUSH_HZ_MIN}-{FLUSH_HZ_MAX}`)
231
219
  end
232
- _flushState.rate = hz
233
- _flushState.accum = 0
220
+ flushState.rate = hz
221
+ flushState.accum = 0
234
222
  end
235
223
 
236
224
  function Lync.isStarted(): boolean
237
- return _flushState.started
225
+ return flushState.started
226
+ end
227
+
228
+ -- Restore module state to post-require defaults. For tests / hot reload.
229
+ function Lync.reset(): ()
230
+ if flushState.conn then
231
+ flushState.conn:Disconnect()
232
+ flushState.conn = nil
233
+ end
234
+ flushState.started = false
235
+ flushState.rate = FLUSH_HZ_MAX
236
+ flushState.accum = 0
237
+ flushState.transport = nil
238
+
239
+ Baseline.reset()
240
+ Bridge.reset()
241
+ Channel.resetGlobals()
242
+ Gate.reset()
243
+ Group.reset()
244
+ Log.reset()
245
+ Middleware.reset()
246
+ Pool.reset()
247
+ Registry.reset()
238
248
  end
239
249
 
240
250
  Lync.packet = Packet.define
@@ -264,20 +274,21 @@ Lync.stats = table.freeze({
264
274
  local Server = require(script.transport.Server)
265
275
  Server.resetStats()
266
276
  end
267
- for i = 1, Registry.count() do
268
- local reg = Registry.get(i)
269
- if reg then
270
- reg.bytesSent = 0
271
- reg.bytesReceived = 0
272
- reg.fires = 0
273
- reg.recvFires = 0
274
- reg.drops = 0
275
- end
277
+ for _, reg in Registry.all() do
278
+ reg.bytesSent = 0
279
+ reg.bytesReceived = 0
280
+ reg.fires = 0
281
+ reg.recvFires = 0
282
+ reg.drops = 0
276
283
  end
277
284
  end,
278
285
  })
279
286
 
280
287
  Lync.debug = table.freeze({
288
+ --[[
289
+ Stubs reserved for capture/replay tooling. No-ops keep the API
290
+ surface stable across versions with or without the recorder.
291
+ ]]
281
292
  capture = function(_label: string?): () end,
282
293
  stop = function(): () end,
283
294
  dump = function(): () end,
@@ -287,27 +298,29 @@ Lync.debug = table.freeze({
287
298
  end,
288
299
 
289
300
  registrations = function(): { any }
301
+ -- Registry IDs are sequential 1..count and never freed, so get(i) is non-nil in range.
290
302
  local count = Registry.count()
291
303
  local result = table.create(count)
292
- local n = 0
293
304
  for i = 1, count do
294
- local reg = Registry.get(i)
295
- if reg then
296
- n += 1
297
- result[n] = table.freeze({
298
- name = reg.name,
299
- id = reg.id,
300
- kind = reg.kind,
301
- isUnreliable = reg.isUnreliable,
302
- })
303
- end
305
+ local reg = Registry.get(i) :: Types.Registration
306
+ result[i] = table.freeze({
307
+ name = reg.name,
308
+ id = reg.id,
309
+ kind = reg.kind,
310
+ isUnreliable = reg.isUnreliable,
311
+ })
304
312
  end
305
313
  return table.freeze(result)
306
314
  end,
307
315
  })
308
316
 
309
317
  Lync.int = IntC.int
318
+ Lync.zint = ZintC.zint
310
319
  Lync.float = FloatC.float
320
+ Lync.deltaInt = DeltaScalarC.deltaInt
321
+ Lync.deltaFloat = DeltaScalarC.deltaFloat
322
+ Lync.deltaVec3 = DeltaScalarC.deltaVec3
323
+ Lync.deltaCFrame = DeltaScalarC.deltaCFrame
311
324
  Lync.f16 = Float16C.f16
312
325
  Lync.f32 = NumberC.f32
313
326
  Lync.f64 = NumberC.f64
@@ -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)