@axpecter/lync 2.1.2 → 2.2.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 (56) hide show
  1. package/README.md +241 -349
  2. package/package.json +1 -1
  3. package/src/Types.luau +53 -13
  4. package/src/api/Group.luau +41 -51
  5. package/src/api/Packet.luau +163 -152
  6. package/src/api/Query.luau +224 -211
  7. package/src/api/Scope.luau +36 -38
  8. package/src/api/Signal.luau +68 -94
  9. package/src/codec/Base.luau +43 -23
  10. package/src/codec/composite/Array.luau +161 -152
  11. package/src/codec/composite/Map.luau +37 -53
  12. package/src/codec/composite/Optional.luau +15 -21
  13. package/src/codec/composite/Shared.luau +78 -41
  14. package/src/codec/composite/Struct.luau +64 -125
  15. package/src/codec/composite/Tagged.luau +15 -25
  16. package/src/codec/composite/Tuple.luau +15 -20
  17. package/src/codec/datatype/Buffer.luau +44 -51
  18. package/src/codec/datatype/CFrame.luau +72 -104
  19. package/src/codec/datatype/Color.luau +14 -10
  20. package/src/codec/datatype/Instance.luau +32 -42
  21. package/src/codec/datatype/IntVector.luau +24 -22
  22. package/src/codec/datatype/NumberRange.luau +21 -12
  23. package/src/codec/datatype/Ray.luau +13 -7
  24. package/src/codec/datatype/Rect.luau +13 -7
  25. package/src/codec/datatype/Region.luau +25 -22
  26. package/src/codec/datatype/Sequence.luau +144 -118
  27. package/src/codec/datatype/String.luau +29 -59
  28. package/src/codec/datatype/UDim.luau +29 -30
  29. package/src/codec/datatype/Vector.luau +89 -105
  30. package/src/codec/meta/Auto.luau +194 -246
  31. package/src/codec/meta/Bitfield.luau +37 -36
  32. package/src/codec/meta/Custom.luau +10 -10
  33. package/src/codec/meta/Enum.luau +8 -11
  34. package/src/codec/meta/Float.luau +16 -20
  35. package/src/codec/meta/Nothing.luau +2 -2
  36. package/src/codec/meta/Unknown.luau +22 -32
  37. package/src/codec/primitive/Bool.luau +9 -5
  38. package/src/codec/primitive/Float16.luau +13 -23
  39. package/src/codec/primitive/Int.luau +20 -28
  40. package/src/codec/primitive/Number.luau +6 -10
  41. package/src/codec/primitive/Signed.luau +32 -0
  42. package/src/codec/primitive/Varint.luau +59 -28
  43. package/src/index.d.ts +8 -2
  44. package/src/init.luau +133 -143
  45. package/src/internal/Baseline.luau +17 -18
  46. package/src/internal/Channel.luau +143 -212
  47. package/src/internal/Middleware.luau +37 -47
  48. package/src/internal/Pool.luau +10 -10
  49. package/src/internal/Registry.luau +38 -34
  50. package/src/internal/Transport.luau +28 -0
  51. package/src/internal/Util.luau +26 -0
  52. package/src/transport/Bridge.luau +32 -24
  53. package/src/transport/Client.luau +51 -53
  54. package/src/transport/Gate.luau +183 -113
  55. package/src/transport/Reader.luau +95 -66
  56. package/src/transport/Server.luau +130 -125
package/src/index.d.ts CHANGED
@@ -141,7 +141,7 @@ interface LyncModule {
141
141
 
142
142
  configure(this: void, options: Lync.ConfigureOptions): void;
143
143
  start(this: void): void;
144
- readonly started: boolean;
144
+ isStarted(this: void): boolean;
145
145
 
146
146
  // ── Definitions ─────────────────────────────────────────────────
147
147
 
@@ -253,7 +253,13 @@ interface LyncModule {
253
253
 
254
254
  enum<T extends string[]>(this: void, ...values: T): Lync.Codec<T[number]>;
255
255
  bitfield(this: void, schema: Record<string, { type: "bool" } | { type: "uint"; width: number } | { type: "int"; width: number }>): Lync.Codec<Record<string, boolean | number>>;
256
- custom<T>(this: void, size: number, write: (b: buffer, offset: number, value: T) => void, read: (b: buffer, offset: number) => T): Lync.Codec<T>;
256
+ custom<T>(
257
+ this: void,
258
+ size: number,
259
+ write: (b: buffer, offset: number, value: T) => void,
260
+ read: (b: buffer, offset: number) => T,
261
+ typeCheck?: string,
262
+ ): Lync.Codec<T>;
257
263
  readonly nothing: Lync.Codec<undefined>;
258
264
  readonly unknown: Lync.Codec<unknown>;
259
265
  readonly auto: Lync.Codec<unknown>;
package/src/init.luau CHANGED
@@ -15,8 +15,8 @@ local Query = require(script.api.Query)
15
15
  local Registry = require(script.internal.Registry)
16
16
  local Scope = require(script.api.Scope)
17
17
  local Types = require(script.Types)
18
+ local Util = require(script.internal.Util)
18
19
 
19
- -- Codecs
20
20
  local ArrayC = require(script.codec.composite.Array)
21
21
  local AutoC = require(script.codec.meta.Auto)
22
22
  local BitfieldC = require(script.codec.meta.Bitfield)
@@ -48,108 +48,138 @@ local UDimC = require(script.codec.datatype.UDim)
48
48
  local UnknownC = require(script.codec.meta.Unknown)
49
49
  local VectorC = require(script.codec.datatype.Vector)
50
50
 
51
- -- Constants -----------------------------------------------------------
51
+ -- Public types (re-exported so users can write `Lync.Packet<MyData>`) ---
52
52
 
53
- local IS_SERVER = RunService:IsServer()
54
-
55
- -- State ---------------------------------------------------------------
53
+ export type Codec<T> = Types.Codec<T>
54
+ export type Connection = Types.Connection
55
+ export type Packet<T> = Packet.PacketHandle<T>
56
+ export type Query<Req, Resp> = Query.QueryHandle<Req, Resp>
57
+ export type Group = Group.GroupHandle
58
+ export type Scope = Scope.ScopeHandle
59
+ export type PacketOptions = Types.PacketOptions
60
+ export type QueryOptions = Types.QueryOptions
61
+ export type ConfigureOptions = Types.ConfigureOptions
62
+ export type RateLimitConfig = Types.RateLimitConfig
63
+ export type PacketStats = Types.PacketStats
64
+ export type PlayerStats = Types.PlayerStats
56
65
 
57
- local _started = false
58
- local _flushRate = 60
59
- local _flushAccum = 0
60
- local _flushConn: RBXScriptConnection? = nil
66
+ -- Constants --------------------------------------------------------------
61
67
 
62
- -- Private -------------------------------------------------------------
68
+ local IS_SERVER = RunService:IsServer()
63
69
 
64
70
  local DROP = table.freeze({ _lyncDrop = true })
65
71
  local ALL = table.freeze({ _lyncAll = true })
66
72
 
73
+ local CHANNEL_MAX_MIN, CHANNEL_MAX_MAX = 4096, 1048576
74
+ local DEPTH_MIN, DEPTH_MAX = 4, 32
75
+ local POOL_MIN, POOL_MAX = 2, 128
76
+ local FLUSH_HZ_MIN, FLUSH_HZ_MAX = 1, 60
77
+
78
+ -- State ------------------------------------------------------------------
79
+
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
+ started = false,
87
+ rate = FLUSH_HZ_MAX,
88
+ accum = 0,
89
+ conn = nil :: RBXScriptConnection?,
90
+ transport = nil :: { flush: () -> () }?,
91
+ }
92
+
93
+ -- Private ----------------------------------------------------------------
94
+
95
+ local isPlayer = Util.isPlayer
96
+
67
97
  local function except(...: any): any
68
98
  local excluded: { [Player]: boolean } = {}
69
99
 
70
100
  for i = 1, select("#", ...) do
71
101
  local arg = select(i, ...)
72
- if typeof(arg) == "Instance" and (arg :: Instance):IsA("Player") then
102
+ if isPlayer(arg) then
73
103
  excluded[arg :: Player] = true
74
- elseif typeof(arg) == "table" and (arg :: any)._lyncGroup then
75
- for player in (arg :: any)._members do
104
+ elseif typeof(arg) == "table" and arg._lyncGroup then
105
+ for player in arg._members :: { [Player]: boolean } do
76
106
  excluded[player] = true
77
107
  end
78
108
  else
79
- error("[Lync] except: argument must be a Player or Group")
109
+ error("[Lync] Lync.except: argument must be a Player or Group")
80
110
  end
81
111
  end
82
112
 
83
- return table.freeze({ _lyncExcept = true, _excluded = excluded })
113
+ return table.freeze({
114
+ _lyncExcept = true,
115
+ _excluded = table.freeze(excluded),
116
+ })
84
117
  end
85
118
 
86
119
  local function setupFlush(): ()
87
- local flushFn: () -> ()
88
-
120
+ local transport: { flush: () -> () }
89
121
  if IS_SERVER then
90
122
  local Server = require(script.transport.Server)
91
- flushFn = function(): ()
92
- Server.flush()
93
- end
123
+ transport = { flush = Server.flush }
94
124
  else
95
125
  local Client = require(script.transport.Client)
96
- flushFn = function(): ()
97
- Client.flush()
98
- end
126
+ transport = { flush = Client.flush }
99
127
  end
128
+ _flushState.transport = transport
100
129
 
101
- _flushConn = RunService.Heartbeat:Connect(function(dt: number)
102
- if _flushRate >= 60 then
103
- flushFn()
104
- else
105
- _flushAccum += dt
106
- local interval = 1 / _flushRate
107
- if _flushAccum >= interval then
108
- _flushAccum -= interval
109
- -- Prevent drift accumulation
110
- if _flushAccum >= interval then
111
- _flushAccum = 0
112
- end
113
- flushFn()
114
- end
130
+ _flushState.conn = RunService.Heartbeat:Connect(function(dt: number)
131
+ local rate = _flushState.rate
132
+ if rate >= FLUSH_HZ_MAX then
133
+ transport.flush()
134
+ return
135
+ end
136
+
137
+ local accum = _flushState.accum + dt
138
+ local interval = 1 / rate
139
+ 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.
142
+ accum = if accum >= 2 * interval then 0 else accum - interval
143
+ transport.flush()
115
144
  end
145
+ _flushState.accum = accum
116
146
  end)
117
147
  end
118
148
 
119
- -- Public --------------------------------------------------------------
120
-
121
- local Lync: any = {}
149
+ -- Public -----------------------------------------------------------------
122
150
 
123
- -- Lifecycle -----------------------------------------------------------
151
+ local Lync = {}
124
152
 
125
153
  function Lync.configure(options: Types.ConfigureOptions): ()
126
- if _started then
154
+ if _flushState.started then
127
155
  error("[Lync] Lync.configure: must be called before Lync.start()")
128
156
  end
129
157
 
130
- if options.channelMaxSize then
131
- local size = options.channelMaxSize
132
- if size < 4096 or size > 1048576 then
133
- error("[Lync] Lync.configure: channelMaxSize must be 4096-1048576")
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
+ )
134
164
  end
135
165
  Channel.setMaxSize(size)
136
166
  Base.setMaxSize(size)
137
167
  end
138
168
 
139
- if options.validationDepth then
140
- local depth = options.validationDepth
141
- if depth < 4 or depth > 32 then
142
- error("[Lync] Lync.configure: validationDepth must be 4-32")
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}`)
143
173
  end
144
174
  Gate.setDepth(depth)
145
175
  end
146
176
 
147
- if options.poolSize then
148
- local size = options.poolSize
149
- if size < 2 or size > 128 then
150
- error("[Lync] Lync.configure: poolSize must be 2-128")
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}`)
151
181
  end
152
- Pool.setMaxSize(size)
182
+ Pool.setMaxSize(poolSize)
153
183
  end
154
184
 
155
185
  if options.bandwidthLimit then
@@ -170,10 +200,10 @@ function Lync.configure(options: Types.ConfigureOptions): ()
170
200
  end
171
201
 
172
202
  function Lync.start(): ()
173
- if _started then
203
+ if _flushState.started then
174
204
  error("[Lync] Lync.start: already started")
175
205
  end
176
- _started = true
206
+ _flushState.started = true
177
207
 
178
208
  if IS_SERVER then
179
209
  local Server = require(script.transport.Server)
@@ -186,57 +216,40 @@ function Lync.start(): ()
186
216
  setupFlush()
187
217
  end
188
218
 
189
- -- `Lync.started` is a read-only property backed by _started
190
- Lync.started = false
219
+ 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
226
+ end
227
+
228
+ function Lync.flushRate(hz: number): ()
229
+ 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}`)
231
+ end
232
+ _flushState.rate = hz
233
+ _flushState.accum = 0
234
+ end
191
235
 
192
- -- Definitions ---------------------------------------------------------
236
+ function Lync.isStarted(): boolean
237
+ return _flushState.started
238
+ end
193
239
 
194
240
  Lync.packet = Packet.define
195
241
  Lync.query = Query.define
196
242
  Lync.group = Group.create
197
243
  Lync.scope = Scope.create
198
244
 
199
- -- Targeting -----------------------------------------------------------
200
-
201
245
  Lync.all = ALL
202
246
  Lync.except = except
203
247
  Lync.DROP = DROP
204
248
 
205
- -- Middleware ----------------------------------------------------------
206
-
207
249
  Lync.onSend = Middleware.onSend
208
250
  Lync.onReceive = Middleware.onReceive
209
251
  Lync.onDrop = Middleware.onDrop
210
252
 
211
- -- Runtime control -----------------------------------------------------
212
-
213
- function Lync.flush(): ()
214
- if not _started then
215
- error("[Lync] Lync.flush: must call Lync.start() first")
216
- end
217
-
218
- if IS_SERVER then
219
- local Server = require(script.transport.Server)
220
- Server.flush()
221
- else
222
- local Client = require(script.transport.Client)
223
- Client.flush()
224
- end
225
-
226
- -- Reset accumulator to prevent double-flush
227
- _flushAccum = 0
228
- end
229
-
230
- function Lync.flushRate(hz: number): ()
231
- if hz < 1 or hz > 60 then
232
- error("[Lync] Lync.flushRate: hz must be 1-60")
233
- end
234
- _flushRate = hz
235
- _flushAccum = 0
236
- end
237
-
238
- -- Stats ---------------------------------------------------------------
239
-
240
253
  Lync.stats = table.freeze({
241
254
  player = function(player: Player): Types.PlayerStats?
242
255
  if not IS_SERVER then
@@ -251,56 +264,48 @@ Lync.stats = table.freeze({
251
264
  local Server = require(script.transport.Server)
252
265
  Server.resetStats()
253
266
  end
254
-
255
- -- Reset per-registration stats
256
- for _, reg in Registry.all() do
257
- reg.bytesSent = 0
258
- reg.bytesReceived = 0
259
- reg.fires = 0
260
- reg.recvFires = 0
261
- reg.drops = 0
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
262
276
  end
263
277
  end,
264
278
  })
265
279
 
266
- -- Debug ---------------------------------------------------------------
267
-
268
280
  Lync.debug = table.freeze({
269
- capture = function(_label: string?): ()
270
- -- TODO: Implement capture recording
271
- end,
272
-
273
- stop = function(): ()
274
- -- TODO: Implement capture stop
275
- end,
276
-
277
- dump = function(): ()
278
- -- TODO: Implement capture dump
279
- end,
281
+ capture = function(_label: string?): () end,
282
+ stop = function(): () end,
283
+ dump = function(): () end,
280
284
 
281
285
  pending = function(): number
282
286
  return Query.pendingCount()
283
287
  end,
284
288
 
285
289
  registrations = function(): { any }
286
- local regs = Registry.all()
287
- local result = table.create(#regs)
288
-
289
- for i, reg in regs do
290
- result[i] = table.freeze({
291
- name = reg.name,
292
- id = reg.id,
293
- kind = reg.kind,
294
- isUnreliable = reg.isUnreliable,
295
- })
290
+ local count = Registry.count()
291
+ local result = table.create(count)
292
+ local n = 0
293
+ 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
296
304
  end
297
-
298
305
  return table.freeze(result)
299
306
  end,
300
307
  })
301
308
 
302
- -- Codecs --------------------------------------------------------------
303
-
304
309
  Lync.int = IntC.int
305
310
  Lync.float = FloatC.float
306
311
  Lync.f16 = Float16C.f16
@@ -341,19 +346,4 @@ Lync.nothing = NothingC.nothing
341
346
  Lync.unknown = UnknownC.unknown
342
347
  Lync.auto = AutoC.auto
343
348
 
344
- -- Wrap in metatable for `Lync.started` property -----------------------
345
-
346
- local LyncProxy = setmetatable({}, {
347
- __index = function(_self: any, key: string): any
348
- if key == "started" then
349
- return _started
350
- end
351
- return Lync[key]
352
- end,
353
-
354
- __newindex = function(): ()
355
- error("[Lync] Lync: module table is read-only")
356
- end,
357
- })
358
-
359
- return LyncProxy
349
+ return table.freeze(Lync)
@@ -2,40 +2,39 @@
2
2
  --!optimize 2
3
3
  -- Read-side delta cache for delta codecs.
4
4
 
5
- -- State ---------------------------------------------------------------
5
+ local Types = require(script.Parent.Parent.Types)
6
6
 
7
- local _caches: { [number]: { [any]: any } } = {}
8
- local _currentKey: any = false
7
+ -- State ------------------------------------------------------------------
8
+
9
+ local _caches: { [number]: { [Types.BaselineKey]: any } } = {}
10
+ local _currentKey: Types.BaselineKey = false
9
11
  local _hasDelta = false
10
12
 
11
- -- Public --------------------------------------------------------------
13
+ -- Public -----------------------------------------------------------------
12
14
 
13
15
  local Baseline = {}
14
16
 
15
- function Baseline.setReadKey(key: any): ()
17
+ function Baseline.setReadKey(key: Types.BaselineKey): ()
16
18
  _currentKey = key
17
19
  end
18
20
 
19
21
  function Baseline.getCache(deltaId: number): any?
20
- local perPlayer = _caches[deltaId]
21
- if not perPlayer then
22
- return nil
23
- end
24
- return perPlayer[_currentKey]
22
+ local perKey = _caches[deltaId]
23
+ return if perKey then perKey[_currentKey] else nil
25
24
  end
26
25
 
27
26
  function Baseline.setCache(deltaId: number, value: any): ()
28
- local perPlayer = _caches[deltaId]
29
- if not perPlayer then
30
- perPlayer = {}
31
- _caches[deltaId] = perPlayer
27
+ local perKey = _caches[deltaId]
28
+ if not perKey then
29
+ perKey = {}
30
+ _caches[deltaId] = perKey
32
31
  end
33
- perPlayer[_currentKey] = value
32
+ perKey[_currentKey] = value
34
33
  end
35
34
 
36
- function Baseline.clearPlayer(key: any): ()
37
- for _, perPlayer in _caches do
38
- perPlayer[key] = nil
35
+ function Baseline.clearPlayer(key: Types.BaselineKey): ()
36
+ for _, perKey in _caches do
37
+ perKey[key] = nil
39
38
  end
40
39
  end
41
40