@axpecter/lync 2.2.1 → 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 +68 -54
  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 +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 +109 -103
  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,7 +50,7 @@ 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
 
51
- -- Public types (re-exported so users can write `Lync.Packet<MyData>`) ---
53
+ -- Public types -----------------------------------------------------------
52
54
 
53
55
  export type Codec<T> = Types.Codec<T>
54
56
  export type Connection = Types.Connection
@@ -67,8 +69,12 @@ export type PlayerStats = Types.PlayerStats
67
69
 
68
70
  local IS_SERVER = RunService:IsServer()
69
71
 
70
- local DROP = table.freeze({ _lyncDrop = true })
71
- 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" })
72
78
 
73
79
  local CHANNEL_MAX_MIN, CHANNEL_MAX_MAX = 4096, 1048576
74
80
  local DEPTH_MIN, DEPTH_MAX = 4, 32
@@ -77,12 +83,8 @@ local FLUSH_HZ_MIN, FLUSH_HZ_MAX = 1, 60
77
83
 
78
84
  -- State ------------------------------------------------------------------
79
85
 
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 = {
86
+ -- Single table so the Heartbeat closure captures one LCT_VAL, not N LCT_REFs.
87
+ local flushState = {
86
88
  started = false,
87
89
  rate = FLUSH_HZ_MAX,
88
90
  accum = 0,
@@ -92,95 +94,86 @@ local _flushState = {
92
94
 
93
95
  -- Private ----------------------------------------------------------------
94
96
 
95
- local isPlayer = Util.isPlayer
97
+ local isPlayer = Player.is
98
+
99
+ type ExceptValue = { _lyncKind: "except", _excluded: { [Player]: boolean } }
96
100
 
97
- local function except(...: any): any
101
+ local function except(...: any): ExceptValue
98
102
  local excluded: { [Player]: boolean } = {}
99
103
 
100
104
  for i = 1, select("#", ...) do
101
105
  local arg = select(i, ...)
102
106
  if isPlayer(arg) then
103
107
  excluded[arg :: Player] = true
104
- elseif typeof(arg) == "table" and arg._lyncGroup then
105
- 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
106
110
  excluded[player] = true
107
111
  end
108
112
  else
109
- error("[Lync] Lync.except: argument must be a Player or Group")
113
+ Log.error(`argument must be a Player or Group, got {typeof(arg)}`)
110
114
  end
111
115
  end
112
116
 
113
117
  return table.freeze({
114
- _lyncExcept = true,
118
+ _lyncKind = "except" :: "except",
115
119
  _excluded = table.freeze(excluded),
116
120
  })
117
121
  end
118
122
 
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
123
+ local function setupFlush(transport: { flush: () -> () }): ()
124
+ flushState.transport = transport
129
125
 
130
- _flushState.conn = RunService.Heartbeat:Connect(function(dt: number)
131
- local rate = _flushState.rate
126
+ flushState.conn = RunService.Heartbeat:Connect(function(dt: number)
127
+ local rate = flushState.rate
132
128
  if rate >= FLUSH_HZ_MAX then
133
129
  transport.flush()
134
130
  return
135
131
  end
136
132
 
137
- local accum = _flushState.accum + dt
133
+ local accum = flushState.accum + dt
138
134
  local interval = 1 / rate
139
135
  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.
136
+ -- Frame-stall guard: zero the accumulator instead of catching up.
142
137
  accum = if accum >= 2 * interval then 0 else accum - interval
143
138
  transport.flush()
144
139
  end
145
- _flushState.accum = accum
140
+ flushState.accum = accum
146
141
  end)
147
142
  end
148
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
+
149
161
  -- Public -----------------------------------------------------------------
150
162
 
151
163
  local Lync = {}
152
164
 
153
165
  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
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)
184
177
 
185
178
  if options.bandwidthLimit then
186
179
  Gate.configureBandwidth(options.bandwidthLimit.softLimit, options.bandwidthLimit.maxStrikes)
@@ -192,49 +185,64 @@ function Lync.configure(options: Types.ConfigureOptions): ()
192
185
 
193
186
  if options.stats then
194
187
  Channel.enableStats()
195
- if IS_SERVER then
196
- local Server = require(script.transport.Server)
197
- Server.enableStats()
198
- end
199
188
  end
200
189
  end
201
190
 
202
191
  function Lync.start(): ()
203
- if _flushState.started then
204
- error("[Lync] Lync.start: already started")
205
- end
206
- _flushState.started = true
192
+ Log.assert(not flushState.started, "already started")
193
+ flushState.started = true
207
194
 
208
195
  if IS_SERVER then
209
196
  local Server = require(script.transport.Server)
210
197
  Server.start()
198
+ setupFlush(Server)
211
199
  else
212
200
  local Client = require(script.transport.Client)
213
201
  Client.start()
202
+ setupFlush(Client)
214
203
  end
215
-
216
- setupFlush()
217
204
  end
218
205
 
219
206
  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
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
226
212
  end
227
213
 
228
214
  function Lync.flushRate(hz: number): ()
229
215
  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}`)
216
+ Log.error(`hz must be {FLUSH_HZ_MIN}-{FLUSH_HZ_MAX}`)
231
217
  end
232
- _flushState.rate = hz
233
- _flushState.accum = 0
218
+ flushState.rate = hz
219
+ flushState.accum = 0
234
220
  end
235
221
 
236
222
  function Lync.isStarted(): boolean
237
- 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()
238
246
  end
239
247
 
240
248
  Lync.packet = Packet.define
@@ -264,20 +272,21 @@ Lync.stats = table.freeze({
264
272
  local Server = require(script.transport.Server)
265
273
  Server.resetStats()
266
274
  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
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
276
281
  end
277
282
  end,
278
283
  })
279
284
 
280
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
+ ]]
281
290
  capture = function(_label: string?): () end,
282
291
  stop = function(): () end,
283
292
  dump = function(): () end,
@@ -287,20 +296,17 @@ Lync.debug = table.freeze({
287
296
  end,
288
297
 
289
298
  registrations = function(): { any }
299
+ -- Registry IDs are sequential 1..count and never freed, so get(i) is non-nil in range.
290
300
  local count = Registry.count()
291
301
  local result = table.create(count)
292
- local n = 0
293
302
  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
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
+ })
304
310
  end
305
311
  return table.freeze(result)
306
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)