@axpecter/lync 2.1.1 → 2.2.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 (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 +145 -143
  6. package/src/api/Query.luau +204 -202
  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 +9 -3
  44. package/src/init.luau +118 -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
 
@@ -247,13 +247,19 @@ interface LyncModule {
247
247
  this: void,
248
248
  tagField: Tag,
249
249
  variants: V,
250
- ): Lync.Codec<{ [K in keyof V]: Lync.InferSchema<{ [F in Tag]: K }> & Lync.InferCodec<V[K]> }[keyof V]>;
250
+ ): Lync.Codec<{ [K in keyof V & string]: { [F in Tag]: K } & Lync.InferCodec<V[K]> }[keyof V & string]>;
251
251
 
252
252
  // ── Meta ────────────────────────────────────────────────────────
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,123 @@ 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
+ -- Constants --------------------------------------------------------------
52
52
 
53
53
  local IS_SERVER = RunService:IsServer()
54
54
 
55
- -- State ---------------------------------------------------------------
55
+ local DROP = table.freeze({ _lyncDrop = true })
56
+ local ALL = table.freeze({ _lyncAll = true })
56
57
 
57
- local _started = false
58
- local _flushRate = 60
59
- local _flushAccum = 0
60
- local _flushConn: RBXScriptConnection? = nil
58
+ local CHANNEL_MAX_MIN, CHANNEL_MAX_MAX = 4096, 1048576
59
+ local DEPTH_MIN, DEPTH_MAX = 4, 32
60
+ local POOL_MIN, POOL_MAX = 2, 128
61
+ local FLUSH_HZ_MIN, FLUSH_HZ_MAX = 1, 60
61
62
 
62
- -- Private -------------------------------------------------------------
63
+ -- State ------------------------------------------------------------------
63
64
 
64
- local DROP = table.freeze({ _lyncDrop = true })
65
- local ALL = table.freeze({ _lyncAll = true })
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 = {
71
+ started = false,
72
+ rate = FLUSH_HZ_MAX,
73
+ accum = 0,
74
+ conn = nil :: RBXScriptConnection?,
75
+ transport = nil :: { flush: () -> () }?,
76
+ }
77
+
78
+ -- Private ----------------------------------------------------------------
79
+
80
+ local isPlayer = Util.isPlayer
66
81
 
67
82
  local function except(...: any): any
68
83
  local excluded: { [Player]: boolean } = {}
69
84
 
70
85
  for i = 1, select("#", ...) do
71
86
  local arg = select(i, ...)
72
- if typeof(arg) == "Instance" and (arg :: Instance):IsA("Player") then
87
+ if isPlayer(arg) then
73
88
  excluded[arg :: Player] = true
74
- elseif typeof(arg) == "table" and (arg :: any)._lyncGroup then
75
- for player in (arg :: any)._members do
89
+ elseif typeof(arg) == "table" and arg._lyncGroup then
90
+ for player in arg._members :: { [Player]: boolean } do
76
91
  excluded[player] = true
77
92
  end
78
93
  else
79
- error("[Lync] except: argument must be a Player or Group")
94
+ error("[Lync] Lync.except: argument must be a Player or Group")
80
95
  end
81
96
  end
82
97
 
83
- return table.freeze({ _lyncExcept = true, _excluded = excluded })
98
+ return table.freeze({
99
+ _lyncExcept = true,
100
+ _excluded = table.freeze(excluded),
101
+ })
84
102
  end
85
103
 
86
104
  local function setupFlush(): ()
87
- local flushFn: () -> ()
88
-
105
+ local transport: { flush: () -> () }
89
106
  if IS_SERVER then
90
107
  local Server = require(script.transport.Server)
91
- flushFn = function(): ()
92
- Server.flush()
93
- end
108
+ transport = { flush = Server.flush }
94
109
  else
95
110
  local Client = require(script.transport.Client)
96
- flushFn = function(): ()
97
- Client.flush()
98
- end
111
+ transport = { flush = Client.flush }
99
112
  end
113
+ _flushState.transport = transport
100
114
 
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
115
+ _flushState.conn = RunService.Heartbeat:Connect(function(dt: number)
116
+ local rate = _flushState.rate
117
+ if rate >= FLUSH_HZ_MAX then
118
+ transport.flush()
119
+ return
120
+ end
121
+
122
+ local accum = _flushState.accum + dt
123
+ local interval = 1 / rate
124
+ 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.
127
+ accum = if accum >= 2 * interval then 0 else accum - interval
128
+ transport.flush()
115
129
  end
130
+ _flushState.accum = accum
116
131
  end)
117
132
  end
118
133
 
119
- -- Public --------------------------------------------------------------
120
-
121
- local Lync: any = {}
134
+ -- Public -----------------------------------------------------------------
122
135
 
123
- -- Lifecycle -----------------------------------------------------------
136
+ local Lync = {}
124
137
 
125
138
  function Lync.configure(options: Types.ConfigureOptions): ()
126
- if _started then
139
+ if _flushState.started then
127
140
  error("[Lync] Lync.configure: must be called before Lync.start()")
128
141
  end
129
142
 
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")
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
+ )
134
149
  end
135
150
  Channel.setMaxSize(size)
136
151
  Base.setMaxSize(size)
137
152
  end
138
153
 
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")
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}`)
143
158
  end
144
159
  Gate.setDepth(depth)
145
160
  end
146
161
 
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")
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}`)
151
166
  end
152
- Pool.setMaxSize(size)
167
+ Pool.setMaxSize(poolSize)
153
168
  end
154
169
 
155
170
  if options.bandwidthLimit then
@@ -170,10 +185,10 @@ function Lync.configure(options: Types.ConfigureOptions): ()
170
185
  end
171
186
 
172
187
  function Lync.start(): ()
173
- if _started then
188
+ if _flushState.started then
174
189
  error("[Lync] Lync.start: already started")
175
190
  end
176
- _started = true
191
+ _flushState.started = true
177
192
 
178
193
  if IS_SERVER then
179
194
  local Server = require(script.transport.Server)
@@ -186,57 +201,40 @@ function Lync.start(): ()
186
201
  setupFlush()
187
202
  end
188
203
 
189
- -- `Lync.started` is a read-only property backed by _started
190
- Lync.started = false
204
+ 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
211
+ end
212
+
213
+ function Lync.flushRate(hz: number): ()
214
+ 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
+ end
217
+ _flushState.rate = hz
218
+ _flushState.accum = 0
219
+ end
191
220
 
192
- -- Definitions ---------------------------------------------------------
221
+ function Lync.isStarted(): boolean
222
+ return _flushState.started
223
+ end
193
224
 
194
225
  Lync.packet = Packet.define
195
226
  Lync.query = Query.define
196
227
  Lync.group = Group.create
197
228
  Lync.scope = Scope.create
198
229
 
199
- -- Targeting -----------------------------------------------------------
200
-
201
230
  Lync.all = ALL
202
231
  Lync.except = except
203
232
  Lync.DROP = DROP
204
233
 
205
- -- Middleware ----------------------------------------------------------
206
-
207
234
  Lync.onSend = Middleware.onSend
208
235
  Lync.onReceive = Middleware.onReceive
209
236
  Lync.onDrop = Middleware.onDrop
210
237
 
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
238
  Lync.stats = table.freeze({
241
239
  player = function(player: Player): Types.PlayerStats?
242
240
  if not IS_SERVER then
@@ -251,56 +249,48 @@ Lync.stats = table.freeze({
251
249
  local Server = require(script.transport.Server)
252
250
  Server.resetStats()
253
251
  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
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
262
261
  end
263
262
  end,
264
263
  })
265
264
 
266
- -- Debug ---------------------------------------------------------------
267
-
268
265
  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,
266
+ capture = function(_label: string?): () end,
267
+ stop = function(): () end,
268
+ dump = function(): () end,
280
269
 
281
270
  pending = function(): number
282
271
  return Query.pendingCount()
283
272
  end,
284
273
 
285
274
  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
- })
275
+ local count = Registry.count()
276
+ local result = table.create(count)
277
+ local n = 0
278
+ 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
296
289
  end
297
-
298
290
  return table.freeze(result)
299
291
  end,
300
292
  })
301
293
 
302
- -- Codecs --------------------------------------------------------------
303
-
304
294
  Lync.int = IntC.int
305
295
  Lync.float = FloatC.float
306
296
  Lync.f16 = Float16C.f16
@@ -341,19 +331,4 @@ Lync.nothing = NothingC.nothing
341
331
  Lync.unknown = UnknownC.unknown
342
332
  Lync.auto = AutoC.auto
343
333
 
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
334
+ 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