@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/README.md CHANGED
@@ -4,11 +4,12 @@
4
4
  <a href="https://github.com/Axp3cter/Lync/releases/latest">Releases</a> ·
5
5
  <a href="#install">Install</a> ·
6
6
  <a href="#example">Example</a> ·
7
+ <a href="#api">API</a> ·
7
8
  <a href="#codecs">Codecs</a> ·
8
9
  <a href="#benchmarks">Benchmarks</a>
9
10
  </p>
10
11
 
11
- Lync batches all sends into a single buffer per player per frame, applies XOR compression across frames, validates and rate-limits every incoming payload, and does it all without code generation.
12
+ Packets, queries, groups, validation, and rate limiting — all batched into one buffer per player per frame. No code generation.
12
13
 
13
14
  ## Install
14
15
 
@@ -16,7 +17,7 @@ Lync batches all sends into a single buffer per player per frame, applies XOR co
16
17
 
17
18
  ```toml
18
19
  [dependencies]
19
- Lync = "axp3cter/lync@2.1.2"
20
+ Lync = "axp3cter/lync@2.2.1"
20
21
  ```
21
22
 
22
23
  **npm (roblox-ts)**
@@ -36,41 +37,29 @@ Or grab the `.rbxm` from [Releases](https://github.com/Axp3cter/Lync/releases/la
36
37
 
37
38
  ## Example
38
39
 
39
- **Shared** (`ReplicatedStorage.Net`)
40
+ **Shared** `ReplicatedStorage.Net`
40
41
 
41
42
  ```luau
42
43
  local Lync = require(game.ReplicatedStorage.Lync)
43
44
 
44
- local Net = {}
45
-
46
- Net.State = Lync.packet("State", Lync.deltaStruct({
47
- position = Lync.vec3,
48
- health = Lync.float(0, 100, 0.5),
49
- shield = Lync.float(0, 100, 0.5),
50
- status = Lync.enum("idle", "moving", "attacking", "dead"),
51
- alive = Lync.bool,
52
- }))
53
-
54
- Net.Hit = Lync.packet("Hit", Lync.struct({
55
- targetId = Lync.int(0, 65535),
56
- damage = Lync.float(0, 200, 0.1),
57
- headshot = Lync.bool,
58
- }), {
59
- rateLimit = { maxPerSecond = 30, burst = 5 },
60
- validate = function(data, player)
61
- if data.damage > 200 then return false, "damage" end
62
- return true
63
- end,
45
+ return table.freeze({
46
+ State = Lync.packet("State", Lync.deltaStruct({
47
+ position = Lync.vec3,
48
+ health = Lync.float(0, 100, 0.5),
49
+ status = Lync.enum("idle", "moving", "attacking", "dead"),
50
+ alive = Lync.bool,
51
+ })),
52
+
53
+ Hit = Lync.packet("Hit", Lync.struct({
54
+ targetId = Lync.int(0, 65535),
55
+ damage = Lync.float(0, 200, 0.1),
56
+ }), {
57
+ rateLimit = { maxPerSecond = 30, burst = 5 },
58
+ validate = function(data) return data.damage <= 200, "damage" end,
59
+ }),
60
+
61
+ Ping = Lync.query("Ping", Lync.nothing, Lync.f64, { timeout = 3 }),
64
62
  })
65
-
66
- Net.Chat = Lync.packet("Chat", Lync.struct({
67
- msg = Lync.string(200),
68
- channel = Lync.int(0, 255),
69
- }))
70
-
71
- Net.Ping = Lync.query("Ping", Lync.nothing, Lync.f64, { timeout = 3 })
72
-
73
- return table.freeze(Net)
74
63
  ```
75
64
 
76
65
  **Server**
@@ -81,33 +70,16 @@ local Net = require(game.ReplicatedStorage.Net)
81
70
  local Players = game:GetService("Players")
82
71
 
83
72
  local alive = Lync.group("alive")
73
+ Players.PlayerAdded:Connect(function(p) alive:add(p) end)
84
74
 
85
- Lync.onDrop(function(player, reason, name)
86
- warn(player.Name, "dropped", name, reason)
87
- end)
75
+ Net.Hit:on(function(data, sender) -- ... end)
76
+ Net.Ping:handle(function() return os.clock() end)
88
77
 
89
78
  Lync.start()
90
79
 
91
- Players.PlayerAdded:Connect(function(player) alive:add(player) end)
92
-
93
80
  game:GetService("RunService").Heartbeat:Connect(function()
94
- Net.State:send({
95
- position = Vector3.new(0, 5, 0),
96
- health = 100,
97
- shield = 50,
98
- status = "idle",
99
- alive = true,
100
- }, alive)
81
+ Net.State:send(getState(), alive)
101
82
  end)
102
-
103
- Net.Hit:on(function(data, player)
104
- local target = Players:GetPlayerByUserId(data.targetId)
105
- if not target then return end
106
- alive:remove(target)
107
- Net.Chat:send({ msg = player.Name .. " eliminated " .. target.Name, channel = 0 }, Lync.all)
108
- end)
109
-
110
- Net.Ping:handle(function(_, player) return os.clock() end)
111
83
  ```
112
84
 
113
85
  **Client**
@@ -119,36 +91,38 @@ local Net = require(game.ReplicatedStorage.Net)
119
91
  Lync.start()
120
92
 
121
93
  local scope = Lync.scope()
94
+ scope:on(Net.State, function(state) -- ... end)
122
95
 
123
- scope:on(Net.State, function(state)
124
- local character = game.Players.LocalPlayer.Character
125
- if not character then return end
126
- character:PivotTo(CFrame.new(state.position))
127
- end)
128
-
129
- scope:on(Net.Chat, function(data) print("[chat]", data.msg) end)
130
-
131
- Net.Hit:send({ targetId = 123, damage = 45.5, headshot = true })
132
-
96
+ Net.Hit:send({ targetId = 123, damage = 45 })
133
97
  local serverTime = Net.Ping:request(nil)
134
- if serverTime then print("server clock:", serverTime) end
135
98
  ```
136
99
 
137
- ## Packets
100
+ ## API
138
101
 
139
- `Lync.packet(name, codec, options?)`
102
+ ### Lifecycle
103
+
104
+ | Function | Description |
105
+ |:---|:---|
106
+ | `Lync.configure(opts)` | Set options. Must precede `start()`. |
107
+ | `Lync.start()` | Initialize transport. Call once. |
108
+ | `Lync.isStarted()` | `true` after `start()`. |
109
+ | `Lync.flush()` | Force an immediate send. |
110
+ | `Lync.flushRate(hz)` | 1–60. Default 60. |
111
+
112
+ ### Configure options
140
113
 
141
- ### Options
114
+ | Option | Default | Range | Description |
115
+ |:---|---:|:---|:---|
116
+ | `channelMaxSize` | 262144 | 4 KB – 1 MB | Max buffer bytes per frame. |
117
+ | `validationDepth` | 16 | 4–32 | Max recursion depth for input validation. |
118
+ | `poolSize` | 16 | 2–128 | Reusable channel-state pool. |
119
+ | `bandwidthLimit` | none | — | `{ softLimit, maxStrikes }` per-player throttle. |
120
+ | `globalRateLimit` | none | — | `{ maxPerSecond }` across all packets per player. |
121
+ | `stats` | `false` | — | Enables `:stats()` and `Lync.stats.player()`. |
142
122
 
143
- | Field | Type | Default | Description |
144
- |:------|:-----|:--------|:------------|
145
- | `unreliable` | `boolean` | `false` | Send over `UnreliableRemoteEvent`. Cannot use with delta codecs. |
146
- | `rateLimit` | `RateLimitConfig` | none | Server-side rate limiting. |
147
- | `validate` | `(data, player) → (bool, string?)` | none | Server-side validation. Return `false, "reason"` to drop. |
148
- | `maxPayloadBytes` | `number` | none | Max bytes per payload. |
149
- | `timestamp` | `"frame"`, `"offset"`, or `"full"` | none | Appends a timestamp. `"frame"` = 1B counter. `"offset"` = 2B ms. `"full"` = 8B clock. Received as third argument. |
123
+ ### Packets
150
124
 
151
- ### Sending
125
+ `Lync.packet(name, codec, options?)`
152
126
 
153
127
  ```luau
154
128
  -- Server
@@ -160,365 +134,283 @@ packet:send(data, group)
160
134
 
161
135
  -- Client
162
136
  packet:send(data)
163
- ```
164
137
 
165
- ### Receiving
138
+ -- Both
139
+ packet:on(function(data, sender, timestamp?) end) -- returns Connection
140
+ packet:once(fn)
141
+ packet:wait() -- yields, returns data, sender, timestamp?
142
+ packet:name()
143
+ packet:stats() -- requires stats=true
144
+ ```
166
145
 
167
- | Method | Description |
168
- |:-------|:------------|
169
- | `packet:on(fn)` | `fn(data, sender, timestamp?)`. Returns a Connection. |
170
- | `packet:once(fn)` | Fires once, then disconnects. |
171
- | `packet:wait()` | Yields until next fire. Returns `data, sender, timestamp?`. |
172
- | `packet:name()` | Returns the packet name. |
173
- | `packet:stats()` | Returns `{ bytesSent, bytesReceived, fires, recvFires, drops }`. Requires stats enabled. |
146
+ | Option | Type | Description |
147
+ |:---|:---|:---|
148
+ | `unreliable` | boolean | Use `UnreliableRemoteEvent`. Disallowed with delta codecs. |
149
+ | `rateLimit` | `RateLimitConfig` | Server-side per-player limit. |
150
+ | `validate` | `(data, player) (bool, string?)` | Drop on `false`. |
151
+ | `maxPayloadBytes` | number | Reject oversize incoming payloads. |
152
+ | `timestamp` | `"frame"` / `"offset"` / `"full"` | Append 1B / 2B / 8B timestamp. Read as third arg. |
174
153
 
175
- ## Queries
154
+ ### Queries
176
155
 
177
156
  `Lync.query(name, requestCodec, responseCodec, options?)`
178
157
 
179
- Request-response built on packets. Returns `nil` on timeout.
158
+ Request-response on top of two packet IDs.
180
159
 
181
- ### Options
182
-
183
- | Field | Type | Default | Description |
184
- |:------|:-----|:--------|:------------|
185
- | `timeout` | `number` | 5 | Seconds before yielding `nil`. |
186
- | `rateLimit` | `RateLimitConfig` | `{ maxPerSecond = 30 }` | Server-side rate limiting. |
187
- | `validate` | `(data, player) → (bool, string?)` | none | Server-side validation. |
188
-
189
- ### Methods
190
-
191
- | Method | Context | Description |
192
- |:-------|:--------|:------------|
193
- | `query:handle(fn)` | Both | Register handler. Server: `fn(request, player) → response`. Client: `fn(request) → response`. |
194
- | `query:request(data)` | Client | Send to server, yield for response. |
195
- | `query:request(data, player)` | Server | Send to one client. |
196
- | `query:request(data, target)` | Server | Send to multiple. Returns `{ [Player]: response? }`. |
197
- | `query:name()` | Both | Returns the query name. |
198
- | `query:stats()` | Both | Combined stats for request and response channels. |
160
+ ```luau
161
+ -- Server
162
+ query:handle(function(data, player) return response end)
163
+ query:request(data, player) -- → response?
164
+ query:request(data, target) -- { [Player]: response? }
199
165
 
200
- Each query consumes two packet IDs internally.
166
+ -- Client
167
+ query:handle(function(data) return response end)
168
+ query:request(data) -- yields, → response? (nil on timeout)
169
+ ```
201
170
 
202
- ## Groups
171
+ | Option | Default | Description |
172
+ |:---|:---|:---|
173
+ | `timeout` | 5 | Seconds before yielding `nil`. |
174
+ | `rateLimit` | `{ maxPerSecond = 30 }` | Server-side. |
175
+ | `validate` | none | `(data, player) → (bool, string?)` |
203
176
 
204
- `Lync.group(name)`
177
+ ### Groups
205
178
 
206
- Named player sets. Members auto-removed on `PlayerRemoving`. Iterable with `for player in group do`.
179
+ `Lync.group(name)` — named player set. Members auto-removed on `PlayerRemoving`. Iterable: `for player in group do`.
207
180
 
208
181
  | Method | Returns | Description |
209
- |:-------|:--------|:------------|
210
- | `group:add(player)` | `boolean` | `true` if added. |
211
- | `group:remove(player)` | `boolean` | `true` if removed. |
212
- | `group:has(player)` | `boolean` | Membership check. |
213
- | `group:count()` | `number` | Member count. |
214
- | `group:destroy()` | — | Clears members, frees name. |
182
+ |:---|:---|:---|
183
+ | `group:add(p)` / `:remove(p)` | boolean | `true` if changed. |
184
+ | `group:has(p)` | boolean | Membership. |
185
+ | `group:count()` | number | |
186
+ | `group:destroy()` | | Clear and free name. |
215
187
 
216
- ## Scope
188
+ ### Scope
217
189
 
218
- `Lync.scope()`
219
-
220
- Batches connections for cleanup.
190
+ `Lync.scope()` — batches connections for cleanup.
221
191
 
222
192
  ```luau
223
193
  local scope = Lync.scope()
224
- scope:on(packetA, fnA)
225
- scope:on(packetB, fnB)
226
- scope:add(someRBXScriptConnection)
227
- scope:destroy() -- disconnects everything
194
+ scope:on(packet, fn)
195
+ scope:once(packet, fn)
196
+ scope:add(rbxConnection)
197
+ scope:destroy()
228
198
  ```
229
199
 
230
- | Method | Description |
231
- |:-------|:------------|
232
- | `scope:on(source, fn)` | Connect and track. |
233
- | `scope:once(source, fn)` | Connect once and track. |
234
- | `scope:add(connection)` | Track an existing connection. |
235
- | `scope:destroy()` | Disconnect all. Safe to call multiple times. |
236
-
237
- ## Connection
200
+ ### Targets
238
201
 
239
- Returned by `packet:on()`, `packet:once()`, `query:handle()`, and middleware functions.
202
+ Server-side `:send` second arg.
240
203
 
241
- | Field / Method | Description |
242
- |:---------------|:------------|
243
- | `connection.connected` | `boolean` |
244
- | `connection:disconnect()` | Stops the listener. Safe mid-fire, safe to call multiple times. |
204
+ | Target | Description |
205
+ |:---|:---|
206
+ | `Player` | One player. |
207
+ | `Lync.all` | All connected. |
208
+ | `Lync.except(...)` | Everyone except given players or groups. |
209
+ | `{ p1, p2 }` | Array of players. |
210
+ | `group` | All members. |
245
211
 
246
- ## Middleware
212
+ ### Middleware
247
213
 
248
214
  ```luau
249
- Lync.onSend(function(data, name, player)
250
- return data -- or return Lync.DROP to discard
251
- end)
252
-
253
- Lync.onReceive(function(data, name, player)
254
- return data
255
- end)
256
-
257
- Lync.onDrop(function(player, reason, name, data)
258
- warn(player.Name, "dropped", name, reason)
259
- end)
215
+ Lync.onSend(function(data, name, player) return data end) -- return Lync.DROP to discard
216
+ Lync.onReceive(function(data, name, player) return data end)
217
+ Lync.onDrop(function(player, reason, name, data) end)
260
218
  ```
261
219
 
262
- All three return a Connection.
263
-
264
- ## Targets
220
+ All return a `Connection`.
265
221
 
266
- Server-side second argument to `packet:send()`.
222
+ ### Connection
267
223
 
268
- | Target | Description |
269
- |:-------|:------------|
270
- | `player` | Single player. |
271
- | `Lync.all` | All connected players. |
272
- | `Lync.except(...)` | Everyone except specified players or groups. |
273
- | `{ p1, p2, ... }` | Array of players. |
274
- | `group` | All members of a group. |
275
-
276
- ## Codecs
224
+ | | |
225
+ |:---|:---|
226
+ | `c.connected` | boolean |
227
+ | `c:disconnect()` | Idempotent. |
277
228
 
278
- ### Numbers
229
+ ### Stats
279
230
 
280
- `Lync.int(min, max)` picks the smallest wire type for your range.
231
+ `Lync.configure({ stats = true })`.
281
232
 
282
- | Codec | Bytes | Description |
283
- |:------|------:|:------------|
284
- | `Lync.int(0, 255)` | 1 | u8 |
285
- | `Lync.int(0, 65535)` | 2 | u16 |
286
- | `Lync.int(0, 4294967295)` | 4 | u32 |
287
- | `Lync.int(-128, 127)` | 1 | i8 |
288
- | `Lync.int(-32768, 32767)` | 2 | i16 |
289
- | `Lync.int(-2147483648, 2147483647)` | 4 | i32 |
290
- | `Lync.f16` | 2 | Half-precision float. ~3 digits. ±65504. |
291
- | `Lync.f32` | 4 | Single-precision float. |
292
- | `Lync.f64` | 8 | Double-precision float. |
293
- | `Lync.bool` | 1 | Bitpacked inside structs and arrays (8 per byte). |
294
- | `Lync.float(min, max, precision)` | 1–4 | Quantized float. Clamped to range. |
233
+ | Function | Description |
234
+ |:---|:---|
235
+ | `Lync.stats.player(p)` | `{ bytesSent, bytesReceived }`. Server only. |
236
+ | `Lync.stats.reset()` | Zero all counters. |
237
+ | `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }` |
295
238
 
296
- ### Strings & Buffers
239
+ ### Debug
297
240
 
298
- | Codec | Description |
299
- |:------|:------------|
300
- | `Lync.string` | Variable length. Binary-safe. |
301
- | `Lync.string(maxLength)` | Same, but rejects on read if length exceeds `maxLength`. |
302
- | `Lync.buff` | Variable-length buffer. |
241
+ | Function | Description |
242
+ |:---|:---|
243
+ | `Lync.debug.pending()` | In-flight query requests. |
244
+ | `Lync.debug.registrations()` | Frozen array of `{ name, id, kind, isUnreliable }`. |
303
245
 
304
- ### Roblox Types
246
+ ## Codecs
305
247
 
306
- | Codec | Bytes |
307
- |:------|------:|
308
- | `Lync.vec2` | 8 |
309
- | `Lync.vec3` | 12 |
310
- | `Lync.cframe` | 24 |
311
- | `Lync.color3` | 3 |
312
- | `Lync.inst` | 2 |
313
- | `Lync.udim` | 8 |
314
- | `Lync.udim2` | 16 |
315
- | `Lync.numberRange` | 8 |
316
- | `Lync.rect` | 16 |
317
- | `Lync.ray` | 24 |
318
- | `Lync.vec2int16` | 4 |
319
- | `Lync.vec3int16` | 6 |
320
- | `Lync.region3` | 24 |
321
- | `Lync.region3int16` | 12 |
322
- | `Lync.numberSequence` | variable |
323
- | `Lync.colorSequence` | variable |
324
-
325
- ### Quantized Variants
326
-
327
- Call the codec to get a quantized version.
328
-
329
- | Codec | Bytes | Description |
330
- |:------|------:|:------------|
331
- | `Lync.vec2(min, max, precision)` | 2–8 | Per-component quantization. |
332
- | `Lync.vec3(min, max, precision)` | 3–12 | Per-component quantization. |
333
- | `Lync.cframe()` | 16 | Compressed rotation. ≤0.16° angular error. Saves 8B vs lossless. |
248
+ ### Numbers
334
249
 
335
- ### Composites
250
+ | Codec | Bytes | Notes |
251
+ |:---|---:|:---|
252
+ | `int(min, max)` | 1 / 2 / 4 | Picks smallest u8/u16/u32/i8/i16/i32. |
253
+ | `f16` / `f32` / `f64` | 2 / 4 / 8 | f16: ±65504, ~3 digits. |
254
+ | `float(min, max, precision)` | 1–4 | Quantized. Clamped. |
255
+ | `bool` | 1 | Auto-bitpacked inside `struct` and `array`. |
336
256
 
337
- | Codec | Description |
338
- |:------|:------------|
339
- | `Lync.struct({ key = codec })` | Named fields. Bools are automatically bitpacked. |
340
- | `Lync.array(codec, maxCount?)` | Variable-length list. Bool arrays are bitpacked. |
341
- | `Lync.map(keyCodec, valueCodec, maxCount?)` | Key-value pairs. |
342
- | `Lync.optional(codec)` | 1-byte nil flag + value if present. |
343
- | `Lync.tuple(...)` | Ordered positional values. |
344
- | `Lync.tagged(tagField, { name = codec })` | Discriminated union with 1-byte tag. |
257
+ ### Strings & buffers
345
258
 
346
- ### Delta
259
+ | Codec | Notes |
260
+ |:---|:---|
261
+ | `string` | Variable length. Binary-safe. |
262
+ | `string(maxLength)` | Bounded. Rejects on read if exceeded. |
263
+ | `buff` | Variable-length buffer. |
347
264
 
348
- Only works with reliable transport. Sends 1 byte when data hasn't changed.
265
+ ### Roblox types
349
266
 
350
- | Codec | Description |
351
- |:------|:------------|
352
- | `Lync.deltaStruct(schema)` | Delta-compressed struct. |
353
- | `Lync.deltaArray(codec, maxCount?)` | Delta-compressed array. |
354
- | `Lync.deltaMap(keyCodec, valueCodec, maxCount?)` | Delta-compressed map. |
267
+ | Codec | Bytes |
268
+ |:---|---:|
269
+ | `vec2` / `vec3` | 8 / 12 |
270
+ | `cframe` | 24 |
271
+ | `color3` | 3 |
272
+ | `inst` | 2 |
273
+ | `udim` / `udim2` | 8 / 16 |
274
+ | `numberRange` | 8 |
275
+ | `rect` | 16 |
276
+ | `ray` | 24 |
277
+ | `vec2int16` / `vec3int16` | 4 / 6 |
278
+ | `region3` / `region3int16` | 24 / 12 |
279
+ | `numberSequence` / `colorSequence` | variable |
355
280
 
356
- ### Meta
281
+ ### Quantized variants
357
282
 
358
- | Codec | Description |
359
- |:------|:------------|
360
- | `Lync.enum(...)` | String enum. Up to 256 variants. 1 byte. |
361
- | `Lync.bitfield(schema)` | Sub-byte packing. 1–32 bits. |
362
- | `Lync.custom(size, write, read)` | User-defined fixed-size codec. |
363
- | `Lync.nothing` | Zero bytes. Reads `nil`. |
364
- | `Lync.unknown` | Bypasses serialization entirely. Use with `validate`. |
365
- | `Lync.auto` | Self-describing. Supports nil, bool, numbers, strings, buffers, and Roblox types. |
283
+ Call as a function for compression.
366
284
 
367
- ## Rate Limiting
285
+ | Codec | Bytes | Notes |
286
+ |:---|---:|:---|
287
+ | `vec2(min, max, precision)` | 2–8 | Per-component. |
288
+ | `vec3(min, max, precision)` | 3–12 | Per-component. |
289
+ | `cframe()` | 16 | Smallest-three quaternion. ≤0.16° rotation error. |
368
290
 
369
- Two modes (pick one per packet):
291
+ ### Composites
370
292
 
371
- **Token bucket:** `{ maxPerSecond = N, burst = M }`
293
+ | Codec | Notes |
294
+ |:---|:---|
295
+ | `struct({k = c})` | Named fields. Bools auto-bitpacked. |
296
+ | `array(c, max?)` | List. Bool arrays bitpacked. |
297
+ | `map(k, v, max?)` | Key-value pairs. |
298
+ | `optional(c)` | 1B nil flag + value. |
299
+ | `tuple(...)` | Positional. |
300
+ | `tagged(field, {name = c})` | Discriminated union. 1B tag. ≤256 variants. |
372
301
 
373
- **Cooldown:** `{ cooldown = seconds }`
302
+ ### Delta reliable only
374
303
 
375
- Global limit across all packets: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`
304
+ Sends 1 byte when unchanged.
376
305
 
377
- ## Configuration
306
+ | Codec |
307
+ |:---|
308
+ | `deltaStruct(schema)` |
309
+ | `deltaArray(c, max?)` |
310
+ | `deltaMap(k, v, max?)` |
378
311
 
379
- `Lync.configure(options)` — call before `Lync.start()`.
312
+ ### Meta
380
313
 
381
- | Option | Default | Description |
382
- |:-------|--------:|:------------|
383
- | `channelMaxSize` | 262,144 | Max buffer bytes per frame (4,096–1,048,576). |
384
- | `validationDepth` | 16 | Max recursion depth for input validation (4–32). |
385
- | `poolSize` | 16 | Buffer pool size (2–128). |
386
- | `bandwidthLimit` | none | `{ softLimit, maxStrikes }`. Per-player bandwidth throttle. |
387
- | `globalRateLimit` | none | `{ maxPerSecond }`. Global per-player rate limit. |
388
- | `stats` | `false` | Enables `packet:stats()` and `Lync.stats.player()`. |
314
+ | Codec | Notes |
315
+ |:---|:---|
316
+ | `enum(...)` | String enum. ≤256 variants. 1B. |
317
+ | `bitfield(schema)` | 1–32 bits. Sub-byte packing. |
318
+ | `custom(size, write, read, typeCheck?)` | User-defined fixed-size. |
319
+ | `nothing` | 0 bytes. Reads `nil`. |
320
+ | `unknown` | Bypass serialization. Use with `validate`. |
321
+ | `auto` | Self-describing. nil/bool/numbers/strings/buffers/Roblox types. |
389
322
 
390
- ### Lifecycle
323
+ ## Rate limiting
391
324
 
392
- | Function | Description |
393
- |:---------|:------------|
394
- | `Lync.configure(options)` | Set options before start. |
395
- | `Lync.start()` | Initialize transport. Call once after all definitions. |
396
- | `Lync.started` | Read-only boolean. `true` after `start()`. |
397
- | `Lync.flush()` | Force an immediate send. |
398
- | `Lync.flushRate(hz)` | Set flush rate. 1–60. Default 60. |
325
+ Per-packet, pick one mode:
399
326
 
400
- ### Stats
327
+ ```luau
328
+ { maxPerSecond = N, burst = M } -- token bucket
329
+ { cooldown = seconds } -- cooldown
330
+ ```
401
331
 
402
- Enable with `Lync.configure({ stats = true })`.
332
+ Global per-player: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`.
403
333
 
404
- | Function | Description |
405
- |:---------|:------------|
406
- | `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }` |
407
- | `Lync.stats.player(player)` | `{ bytesSent, bytesReceived }` — server only. |
408
- | `Lync.stats.reset()` | Zeros all counters. |
334
+ ## Limits
409
335
 
410
- ### Debug
336
+ | | |
337
+ |:---|---:|
338
+ | Packet + query IDs | 127 |
339
+ | Buffer per frame | 1 MB max |
340
+ | In-flight queries | 65,536 |
341
+ | Enum / tagged variants | 256 |
342
+ | Bitfield total bits | 32 |
411
343
 
412
- | Function | Description |
413
- |:---------|:------------|
414
- | `Lync.debug.pending()` | Number of in-flight query requests. Useful for detecting leaks. |
415
- | `Lync.debug.registrations()` | Frozen array of `{ name, id, kind, isUnreliable }` for all registered packets and queries. |
344
+ ## Benchmarks
416
345
 
417
- ## Limits
346
+ `rojo serve bench.project.json` with one server + one client.
418
347
 
419
- | Constraint | Limit |
420
- |:-----------|------:|
421
- | Packet + query registrations | 127 |
422
- | Buffer per frame | 256 KB default, 1 MB max |
423
- | Concurrent query requests | 65,536 |
424
- | Enum variants | 256 |
425
- | Bitfield bits | 32 |
426
- | Tagged variants | 256 |
348
+ CPU benches run a fixed 1000 iterations per case.
427
349
 
428
- ## Benchmarks
350
+ ### Codec throughput
429
351
 
430
- Run `rojo serve bench.project.json` with one server + one client.
352
+ | Codec | Encode | Decode | RT/s |
353
+ |:---|---:|---:|---:|
354
+ | `bool` | 43 ns | 28 ns | 14.1 M |
355
+ | `int(0, 255)` | 41 ns | 25 ns | 15.2 M |
356
+ | `int(0, 65535)` | 40 ns | 25 ns | 15.3 M |
357
+ | `f16` | 60 ns | 42 ns | 9.7 M |
358
+ | `f32` | 41 ns | 26 ns | 14.9 M |
359
+ | `f64` | 41 ns | 25 ns | 15.3 M |
360
+ | `string` (10 chars) | 45 ns | 73 ns | 8.4 M |
361
+ | `string` (1000 chars) | 74 ns | 250 ns | 3.1 M |
362
+ | `vec3` | 56 ns | 27 ns | 12.1 M |
363
+ | `vec3` quantized | 121 ns | 85 ns | 4.9 M |
364
+ | `cframe` | 88 ns | 186 ns | 3.6 M |
365
+ | `cframe()` | 118 ns | 214 ns | 3.0 M |
366
+ | entity struct (6 fields) | 234 ns | 476 ns | 1.4 M |
367
+ | 100× entities | 15.3 µs | 34.6 µs | 20 K |
368
+ | 1000× bools (bitpacked) | 4.3 µs | 5.3 µs | 104 K |
431
369
 
432
- ### Wire Sizes
370
+ ### Wire sizes
433
371
 
434
372
  | Codec | Bytes |
435
- |:------|------:|
436
- | `bool` | 1 |
437
- | `int(0, 255)` | 1 |
438
- | `int(0, 65535)` | 2 |
439
- | `f16` | 2 |
440
- | `f32` | 4 |
441
- | `f64` | 8 |
442
- | `string` (5 chars) | 6 |
443
- | `string` (1000 chars) | 1002 |
444
- | `vec3` | 12 |
445
- | `vec3(0, 100, 1)` | 3 |
446
- | `cframe` | 24 |
447
- | `cframe()` | 16 |
448
- | `color3` | 3 |
449
- | entity struct (6 fields) | 34 |
373
+ |:---|---:|
374
+ | entity struct (6 fields, lossless) | 34 |
450
375
  | entity compact (quantized) | 13 |
451
- | bitfield | 2 |
452
376
  | 100× entities | 601 |
453
377
  | 1000× bools (bitpacked) | 127 |
378
+ | bitfield flags | 2 |
379
+ | `tuple(u8, vec3, bool)` | 14 |
454
380
 
455
- ### Codec Throughput
456
-
457
- 100k iterations, isolated CPU. No networking.
458
-
459
- | Codec | Encode | Decode | Round-trips/sec |
460
- |:------|-------:|-------:|----------------:|
461
- | `bool` | 44ns | 29ns | 13.9M |
462
- | `int(0, 255)` | 42ns | 28ns | 14.4M |
463
- | `f32` | 41ns | 25ns | 15.0M |
464
- | `f64` | 41ns | 26ns | 14.8M |
465
- | `string` (10 chars) | 46ns | 60ns | 9.4M |
466
- | `string` (1000 chars) | 76ns | 238ns | 3.2M |
467
- | `vec3` | 53ns | 27ns | 12.4M |
468
- | `cframe` | 92ns | 144ns | 4.2M |
469
- | `cframe()` | 123ns | 170ns | 3.4M |
470
- | entity struct | 239ns | 395ns | 1.6M |
471
- | 100× entities | 15.2µs | 34.1µs | 20K |
472
- | 1000× bools | 4.3µs | 5.1µs | 107K |
473
-
474
- ### Delta Savings
475
-
476
- | Codec | Full | Unchanged | Savings |
477
- |:------|-----:|----------:|--------:|
478
- | `deltaStruct` (entity) | 35B | 1B | 97% |
479
- | `deltaStruct` (compact) | 14B | 1B | 93% |
480
- | `deltaArray` (100× entity) | 602B | 1B | 100% |
481
- | `deltaArray` (1000× bool) | 128B | 1B | 99% |
482
- | `deltaMap` (string → u8) | 19B | 1B | 95% |
483
-
484
- ### Network Throughput
485
-
486
- 1000 fires/frame, 8 seconds, one player.
487
-
488
- | Packet | FPS | Kbps |
489
- |:-------|----:|-----:|
490
- | booleans | 60 | 2.5 |
491
- | entity struct | 60 | 2.3 |
492
- | entity compact | 60 | 2.4 |
493
- | bitfield flags | 60 | 2.4 |
494
- | cframe lossless | 60 | 2.5 |
495
- | cframe compressed | 60 | 2.3 |
381
+ ### Delta savings
496
382
 
497
- ### Cross-Library Comparison
383
+ | Codec | Full | Unchanged |
384
+ |:---|---:|---:|
385
+ | `deltaStruct` (entity) | 35 B | 1 B |
386
+ | `deltaStruct` (compact) | 14 B | 1 B |
387
+ | `deltaArray` (100× entity) | 602 B | 1 B |
388
+ | `deltaArray` (1000× bool) | 128 B | 1 B |
389
+ | `deltaMap` (string → u8) | 19 B | 1 B |
498
390
 
499
- Same methodology as [Blink's benchmarks](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md): 1,000 fires/frame, same data every frame, 10 seconds.
391
+ ### Cross-library comparison
500
392
 
501
- Other tool numbers from [Blink v0.17.1](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md) (2025-04-30).
393
+ Same methodology as [Blink](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md): 1000 fires/frame, identical data, 10 s. Other-tool numbers from Blink v0.17.1.
502
394
 
503
395
  > [!NOTE]
504
- > Lync batches all sends into one buffer per frame. Other tools fire one RemoteEvent per send. Lync also includes server-side validation and bool bitpacking (1000 bools = 127B vs ~1002B). Delta compression is not exercised here — see [Delta Savings](#delta-savings).
396
+ > Lync batches all sends into one buffer per frame and bitpacks bools (1000 = 127 B vs ~1002 B). Delta compression isn't exercised here.
505
397
 
506
- #### Entities — 100× struct(6× u8)
398
+ **100× struct(6× u8) entities**
507
399
 
508
400
  | Tool | FPS | Kbps |
509
- |:-----|----:|-----:|
401
+ |:---|---:|---:|
510
402
  | roblox | 16 | 559,364 |
511
- | **lync** | **60** | **3.68** |
403
+ | **lync** | **60** | **3.47** |
512
404
  | blink | 42 | 41.81 |
513
405
  | zap | 39 | 41.71 |
514
406
  | bytenet | 32 | 41.64 |
515
407
 
516
- #### Booleans — 1000× bool
408
+ **1000× bool**
517
409
 
518
410
  | Tool | FPS | Kbps |
519
- |:-----|----:|-----:|
411
+ |:---|---:|---:|
520
412
  | roblox | 21 | 353,107 |
521
- | **lync** | **60** | **2.49** |
413
+ | **lync** | **60** | **2.33** |
522
414
  | blink | 97 | 7.91 |
523
415
  | zap | 52 | 8.10 |
524
416
  | bytenet | 35 | 8.11 |