@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/README.md CHANGED
@@ -9,18 +9,17 @@
9
9
  <a href="#benchmarks">Benchmarks</a>
10
10
  </p>
11
11
 
12
- Packets, queries, groups, validation, and rate limiting all batched into one buffer per player per frame. No code generation.
12
+ Schemas, packets, queries, groups, validation, rate limiting. Every send batches into one buffer per player per frame; identical frames XOR to ones already in flight; delta codecs collapse unchanged state to a single byte. No code generation.
13
13
 
14
14
  ## Install
15
15
 
16
- **Wally**
16
+ Wally — add to your `wally.toml`:
17
17
 
18
18
  ```toml
19
- [dependencies]
20
- Lync = "axp3cter/lync@2.2.1"
19
+ Lync = "axp3cter/lync@2.3.2"
21
20
  ```
22
21
 
23
- **npm (roblox-ts)**
22
+ npm (roblox-ts):
24
23
 
25
24
  ```bash
26
25
  npm install @axpecter/lync
@@ -30,10 +29,7 @@ npm install @axpecter/lync
30
29
  import Lync from "@axpecter/lync";
31
30
  ```
32
31
 
33
- Or grab the `.rbxm` from [Releases](https://github.com/Axp3cter/Lync/releases/latest).
34
-
35
- > [!IMPORTANT]
36
- > Define all packets, queries, and groups before calling `Lync.start()`.
32
+ **Important.** Define every packet, query, and group before `Lync.start()`. Definitions assign sequential 7-bit IDs that both peers must agree on; defining late on one side desyncs the wire.
37
33
 
38
34
  ## Example
39
35
 
@@ -72,7 +68,7 @@ local Players = game:GetService("Players")
72
68
  local alive = Lync.group("alive")
73
69
  Players.PlayerAdded:Connect(function(p) alive:add(p) end)
74
70
 
75
- Net.Hit:on(function(data, sender) -- ... end)
71
+ Net.Hit:on(function(data, sender) end)
76
72
  Net.Ping:handle(function() return os.clock() end)
77
73
 
78
74
  Lync.start()
@@ -91,7 +87,7 @@ local Net = require(game.ReplicatedStorage.Net)
91
87
  Lync.start()
92
88
 
93
89
  local scope = Lync.scope()
94
- scope:on(Net.State, function(state) -- ... end)
90
+ scope:on(Net.State, function(state) end)
95
91
 
96
92
  Net.Hit:send({ targetId = 123, damage = 45 })
97
93
  local serverTime = Net.Ping:request(nil)
@@ -103,18 +99,19 @@ local serverTime = Net.Ping:request(nil)
103
99
 
104
100
  | Function | Description |
105
101
  |:---|:---|
106
- | `Lync.configure(opts)` | Set options. Must precede `start()`. |
102
+ | `Lync.configure(opts)` | Apply options. Must precede `start()`. |
107
103
  | `Lync.start()` | Initialize transport. Call once. |
108
104
  | `Lync.isStarted()` | `true` after `start()`. |
109
105
  | `Lync.flush()` | Force an immediate send. |
110
- | `Lync.flushRate(hz)` | 1–60. Default 60. |
106
+ | `Lync.flushRate(hz)` | 1–60 Hz. Default 60. |
107
+ | `Lync.reset()` | Restore module state to post-require defaults. For tests / hot reload. |
111
108
 
112
109
  ### Configure options
113
110
 
114
111
  | Option | Default | Range | Description |
115
112
  |:---|---:|:---|:---|
116
- | `channelMaxSize` | 262144 | 4 KB – 1 MB | Max buffer bytes per frame. |
117
- | `validationDepth` | 16 | 4–32 | Max recursion depth for input validation. |
113
+ | `channelMaxSize` | 262144 | 4 KB – 1 MB | Per-frame buffer cap. |
114
+ | `validationDepth` | 16 | 4–32 | Schema-walk recursion limit. |
118
115
  | `poolSize` | 16 | 2–128 | Reusable channel-state pool. |
119
116
  | `bandwidthLimit` | none | — | `{ softLimit, maxStrikes }` per-player throttle. |
120
117
  | `globalRateLimit` | none | — | `{ maxPerSecond }` across all packets per player. |
@@ -128,44 +125,44 @@ local serverTime = Net.Ping:request(nil)
128
125
  -- Server
129
126
  packet:send(data, player)
130
127
  packet:send(data, Lync.all)
131
- packet:send(data, Lync.except(p1, p2))
128
+ packet:send(data, Lync.except(p1, group1))
132
129
  packet:send(data, { p1, p2, p3 })
133
130
  packet:send(data, group)
134
131
 
135
132
  -- Client
136
133
  packet:send(data)
137
134
 
138
- -- Both
139
- packet:on(function(data, sender, timestamp?) end) -- returns Connection
135
+ -- Both sides
136
+ local conn = packet:on(function(data, sender, timestamp) end)
140
137
  packet:once(fn)
141
- packet:wait() -- yields, returns data, sender, timestamp?
138
+ local data, sender, timestamp = packet:wait()
142
139
  packet:name()
143
- packet:stats() -- requires stats=true
140
+ packet:stats() -- requires stats=true
144
141
  ```
145
142
 
146
143
  | Option | Type | Description |
147
144
  |:---|:---|:---|
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. |
145
+ | `unreliable` | boolean | Use `UnreliableRemoteEvent`. Rejected with delta codecs (a dropped frame would desync the baseline). |
146
+ | `rateLimit` | `RateLimitConfig` | Server-side per-player. |
147
+ | `validate` | `(data, player) → (bool, string?)` | Drop on `false`. Reason is forwarded to `onDrop`. |
148
+ | `maxPayloadBytes` | number | Reject oversize incoming payloads early. |
149
+ | `timestamp` | `"frame"`, `"offset"`, `"full"` | Append 1B / 2B / 8B timestamp. Read as the third arg. |
153
150
 
154
151
  ### Queries
155
152
 
156
153
  `Lync.query(name, requestCodec, responseCodec, options?)`
157
154
 
158
- Request-response on top of two packet IDs.
155
+ Request-response on top of two paired registrations. Single-target requests yield until reply or timeout; multi-target requests gather a partial map.
159
156
 
160
157
  ```luau
161
158
  -- Server
162
159
  query:handle(function(data, player) return response end)
163
- query:request(data, player) -- response?
164
- query:request(data, target) -- { [Player]: response? }
160
+ local resp = query:request(data, player) -- response?
161
+ local map = query:request(data, group) -- { [Player]: response? }
165
162
 
166
163
  -- Client
167
164
  query:handle(function(data) return response end)
168
- query:request(data) -- yields, → response? (nil on timeout)
165
+ local resp = query:request(data) -- yields; nil on timeout
169
166
  ```
170
167
 
171
168
  | Option | Default | Description |
@@ -180,14 +177,14 @@ query:request(data) -- yields, → response? (nil on timeout
180
177
 
181
178
  | Method | Returns | Description |
182
179
  |:---|:---|:---|
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. |
180
+ | `:add(p)` / `:remove(p)` | `boolean` | `true` if membership changed. |
181
+ | `:has(p)` | `boolean` | |
182
+ | `:count()` | `number` | |
183
+ | `:destroy()` | — | Clear members and free the name. |
187
184
 
188
185
  ### Scope
189
186
 
190
- `Lync.scope()` — batches connections for cleanup.
187
+ `Lync.scope()` — batches connections for a single `:destroy()`.
191
188
 
192
189
  ```luau
193
190
  local scope = Lync.scope()
@@ -205,43 +202,46 @@ Server-side `:send` second arg.
205
202
  |:---|:---|
206
203
  | `Player` | One player. |
207
204
  | `Lync.all` | All connected. |
208
- | `Lync.except(...)` | Everyone except given players or groups. |
205
+ | `Lync.except(...)` | Everyone except given Players or Groups. |
209
206
  | `{ p1, p2 }` | Array of players. |
210
207
  | `group` | All members. |
211
208
 
212
209
  ### Middleware
213
210
 
214
211
  ```luau
215
- Lync.onSend(function(data, name, player) return data end) -- return Lync.DROP to discard
212
+ -- Return Lync.DROP from onSend to discard a packet.
213
+ Lync.onSend(function(data, name, player) return data end)
216
214
  Lync.onReceive(function(data, name, player) return data end)
217
215
  Lync.onDrop(function(player, reason, name, data) end)
218
216
  ```
219
217
 
220
- All return a `Connection`.
218
+ All return a `Connection`. A throwing hook surfaces to the caller and aborts the chain at that point.
221
219
 
222
220
  ### Connection
223
221
 
224
222
  | | |
225
223
  |:---|:---|
226
- | `c.connected` | boolean |
224
+ | `c.connected` | `boolean` |
227
225
  | `c:disconnect()` | Idempotent. |
228
226
 
229
227
  ### Stats
230
228
 
231
- `Lync.configure({ stats = true })`.
229
+ Enable with `Lync.configure({ stats = true })`.
232
230
 
233
231
  | Function | Description |
234
232
  |:---|:---|
235
233
  | `Lync.stats.player(p)` | `{ bytesSent, bytesReceived }`. Server only. |
236
234
  | `Lync.stats.reset()` | Zero all counters. |
237
- | `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }` |
235
+ | `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }`. Aggregated across the request + response registrations on queries. |
238
236
 
239
237
  ### Debug
240
238
 
241
239
  | Function | Description |
242
240
  |:---|:---|
243
- | `Lync.debug.pending()` | In-flight query requests. |
244
- | `Lync.debug.registrations()` | Frozen array of `{ name, id, kind, isUnreliable }`. |
241
+ | `Lync.debug.pending()` | In-flight query correlation IDs. |
242
+ | `Lync.debug.registrations()` | Frozen `{ name, id, kind, isUnreliable }` per registration. |
243
+
244
+ `capture` / `stop` / `dump` are reserved no-ops for capture/replay tooling.
245
245
 
246
246
  ## Codecs
247
247
 
@@ -249,9 +249,10 @@ All return a `Connection`.
249
249
 
250
250
  | Codec | Bytes | Notes |
251
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. |
252
+ | `int(min, max)` | 1 / 2 / 4 | Picks narrowest u8/u16/u32/i8/i16/i32. |
253
+ | `zint(min?, max?)` | 1 5 | Variable-length signed via zigzag varint. 1 byte for [-96, 95]. |
254
+ | `f16` / `f32` / `f64` | 2 / 4 / 8 | `f16` ≈ ±65504, ~3 digits. |
255
+ | `float(min, max, precision)` | 1 / 2 / 3 / 4 | Quantized; picks u8 / u16 / u24 / u32 wire form. |
255
256
  | `bool` | 1 | Auto-bitpacked inside `struct` and `array`. |
256
257
 
257
258
  ### Strings & buffers
@@ -260,7 +261,7 @@ All return a `Connection`.
260
261
  |:---|:---|
261
262
  | `string` | Variable length. Binary-safe. |
262
263
  | `string(maxLength)` | Bounded. Rejects on read if exceeded. |
263
- | `buff` | Variable-length buffer. |
264
+ | `buff` | Variable-length raw `buffer`. |
264
265
 
265
266
  ### Roblox types
266
267
 
@@ -269,7 +270,7 @@ All return a `Connection`.
269
270
  | `vec2` / `vec3` | 8 / 12 |
270
271
  | `cframe` | 24 |
271
272
  | `color3` | 3 |
272
- | `inst` | 2 |
273
+ | `inst` | 2 (sidecar ref index) |
273
274
  | `udim` / `udim2` | 8 / 16 |
274
275
  | `numberRange` | 8 |
275
276
  | `rect` | 16 |
@@ -284,136 +285,97 @@ Call as a function for compression.
284
285
 
285
286
  | Codec | Bytes | Notes |
286
287
  |:---|---:|:---|
287
- | `vec2(min, max, precision)` | 28 | Per-component. |
288
- | `vec3(min, max, precision)` | 312 | Per-component. |
289
- | `cframe()` | 16 | Smallest-three quaternion. ≤0.16° rotation error. |
288
+ | `vec2(min, max, precision)` | 2 / 4 / 6 / 8 | Per-component, narrowest fitting width. |
289
+ | `vec3(min, max, precision)` | 3 / 6 / 9 / 12 | Per-component, narrowest fitting width. |
290
+ | `cframe()` | 16 | Smallest-three quaternion. ≤ 0.16° rotation error. |
290
291
 
291
292
  ### Composites
292
293
 
293
294
  | Codec | Notes |
294
295
  |:---|:---|
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. |
301
-
302
- ### Delta — reliable only
303
-
304
- Sends 1 byte when unchanged.
305
-
306
- | Codec |
307
- |:---|
308
- | `deltaStruct(schema)` |
309
- | `deltaArray(c, max?)` |
310
- | `deltaMap(k, v, max?)` |
296
+ | `struct({k = c})` | Named fields. Bools auto-bitpacked into a tail block. |
297
+ | `array(c, max?)` | List. Bool arrays bitpacked. Direct path for fixed-size elements. |
298
+ | `map(k, v, max?)` | Key-value pairs; keys sorted at encode for stable wire bytes. |
299
+ | `optional(c)` | 1B presence flag + value. |
300
+ | `tuple(...)` | Positional. All-direct fast path when every element is fixed-size. |
301
+ | `tagged(field, {name = c})` | Discriminated union. 1B tag. Up to 256 variants. |
302
+
303
+ ### Delta — reliable transport only
304
+
305
+ Tracks the previous frame's value and ships only what changed. Rejected on `unreliable = true`.
306
+
307
+ | Codec | Static | Mutation |
308
+ |:---|:---:|:---:|
309
+ | `deltaStruct(schema)` | 1 B | per-field |
310
+ | `deltaArray(c, max?)` | 1 B | per-changed-index |
311
+ | `deltaMap(k, v, max?)` | 1 B | per-changed-key |
312
+ | `deltaInt(min, max)` | 1 B | 1–5 B |
313
+ | `deltaFloat(min, max, precision)` | 1 B | 1–5 B |
314
+ | `deltaVec3(min, max, precision)` | 3 B | 3–15 B |
315
+ | `deltaCFrame(posMin, posMax, precision)` | 1 B | 4–13 B |
316
+
317
+ - `deltaArray` element / `deltaMap` key+value cannot themselves contain delta state. Use `deltaStruct` for per-field deltas inside.
318
+ - `deltaVec3` and `deltaCFrame` error on out-of-range components.
311
319
 
312
320
  ### Meta
313
321
 
314
322
  | Codec | Notes |
315
323
  |:---|:---|
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. |
324
+ | `enum(...)` | String enum. ≤ 256 variants. 1B u8 index. |
325
+ | `bitfield(schema)` | 1–32 bits total. `{ type = "bool" }`, `{ type = "uint", width }`, `{ type = "int", width }`. |
326
+ | `custom(size, write, read, typeCheck?)` | User-defined fixed-size codec. |
327
+ | `nothing` | 0 bytes; reads `nil`. For fire-and-forget signals. |
328
+ | `unknown` | Bypasses serialization through the channel sidecar. Must be paired with `validate`. |
329
+ | `auto` | Self-describing: nil / bool / numbers / strings / buffers / Roblox datatypes. 1B type tag + payload. |
322
330
 
323
331
  ## Rate limiting
324
332
 
325
333
  Per-packet, pick one mode:
326
334
 
327
- ```luau
328
- { maxPerSecond = N, burst = M } -- token bucket
329
- { cooldown = seconds } -- cooldown
330
- ```
335
+ - Token bucket: `{ maxPerSecond = N, burst = M }`
336
+ - Cooldown: `{ cooldown = seconds }`
331
337
 
332
- Global per-player: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`.
338
+ Global per-player cap: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`.
333
339
 
334
340
  ## Limits
335
341
 
336
342
  | | |
337
343
  |:---|---:|
338
- | Packet + query IDs | 127 |
344
+ | Packet + query IDs (combined) | 127 |
339
345
  | Buffer per frame | 1 MB max |
340
- | In-flight queries | 65,536 |
346
+ | In-flight queries | 65,535 |
341
347
  | Enum / tagged variants | 256 |
342
348
  | Bitfield total bits | 32 |
349
+ | Sidecar refs per frame | 65,535 |
343
350
 
344
351
  ## Benchmarks
345
352
 
346
353
  `rojo serve bench.project.json` with one server + one client.
347
354
 
348
- CPU benches run a fixed 1000 iterations per case.
349
-
350
- ### Codec throughput
351
-
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 |
369
-
370
- ### Wire sizes
355
+ ### Cross-library 1000 fires/frame, 10 s
371
356
 
372
- | Codec | Bytes |
373
- |:---|---:|
374
- | entity struct (6 fields, lossless) | 34 |
375
- | entity compact (quantized) | 13 |
376
- | 100× entities | 601 |
377
- | 1000× bools (bitpacked) | 127 |
378
- | bitfield flags | 2 |
379
- | `tuple(u8, vec3, bool)` | 14 |
380
-
381
- ### Delta savings
382
-
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 |
390
-
391
- ### Cross-library comparison
392
-
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.
394
-
395
- > [!NOTE]
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.
397
-
398
- **100× struct(6× u8) entities**
399
-
400
- | Tool | FPS | Kbps |
401
- |:---|---:|---:|
402
- | roblox | 16 | 559,364 |
403
- | **lync** | **60** | **3.47** |
404
- | blink | 42 | 41.81 |
405
- | zap | 39 | 41.71 |
406
- | bytenet | 32 | 41.64 |
407
-
408
- **1000× bool**
409
-
410
- | Tool | FPS | Kbps |
411
- |:---|---:|---:|
412
- | roblox | 21 | 353,107 |
413
- | **lync** | **60** | **2.33** |
414
- | blink | 97 | 7.91 |
415
- | zap | 52 | 8.10 |
416
- | bytenet | 35 | 8.11 |
357
+ [Blink's methodology](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md): same payload reused every frame, identical entity / bool shapes. Other tools from Blink v0.17.1.
358
+
359
+ | Tool | `array<entity>[100]` | `array<bool>[1000]` |
360
+ |:---|:---|:---|
361
+ | roblox | 16 fps · 559,364 Kbps | 21 fps · 353,107 Kbps |
362
+ | **lync** | **59 fps · 3.37 Kbps** | **61 fps · 2.45 Kbps** |
363
+ | blink | 42 fps · 41.81 Kbps | 97 fps · 7.91 Kbps |
364
+ | zap | 39 fps · 41.71 Kbps | 52 fps · 8.10 Kbps |
365
+ | bytenet | 32 fps · 41.64 Kbps | 35 fps · 8.11 Kbps |
366
+
367
+ ### Network bandwidth — 100 fires/frame, 8 s
368
+
369
+ | Workload | Naive Kbps | Optimized | Savings |
370
+ |:---|---:|:---|---:|
371
+ | `array<entity>[100]` random | 3,607 | `deltaArray` 3 of 100 mutated | **154** (–96%) |
372
+ | `array<entity>[100]` reused | 3,607 | XOR baseline (identical frames) | **2.4** (–99.9%) |
373
+ | `array<bool>[1000]` random | 762 | XOR baseline (1 bit flipped) | **20.4** (–97%) |
374
+ | `struct(state)` random | 201 | `deltaStruct` 1 field mutated | **29.0** (–86%) |
375
+ | `map<id, vec3>[200]` 5 keys mutated | 657 | `deltaMap` 5 keys mutated | **393** (–40%) |
376
+ | `array<cframe>[50]` random | 4,585 | — | — |
377
+ | `vec3` walking motion (continuous diff) | — | `deltaVec3` | **19.5** |
378
+ | `CFrame` walking pose (pos + rot) | | `deltaCFrame` | **41.1** |
417
379
 
418
380
  ## License
419
381
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axpecter/lync",
3
- "version": "2.2.1",
3
+ "version": "2.3.2",
4
4
  "description": "Buffer networking for Roblox. Delta compression, XOR framing, built-in security",
5
5
  "main": "src/init.luau",
6
6
  "types": "src/index.d.ts",
package/src/Types.luau CHANGED
@@ -12,26 +12,26 @@ export type ChannelState = {
12
12
  itemCount: number,
13
13
  singleMode: boolean,
14
14
  singlePos: number,
15
- deltas: { [number]: DeltaCacheEntry },
15
+ --[[
16
+ Per-codec write-side delta state, keyed by deltaId. Shape is opaque
17
+ and codec-internal: deltaStruct stores `{raw, len, segOff, segLen}`,
18
+ deltaArray stores `{perIdx, length}`, deltaMap stores `{perKey}`,
19
+ delta scalars store quantized previous values.
20
+ ]]
21
+ deltas: { [number]: any },
16
22
  prevDump: buffer?,
17
23
  prevDumpLen: number,
18
24
  currentPacket: string?,
19
25
  }
20
26
 
21
- export type DeltaCacheEntry = {
22
- raw: buffer,
23
- len: number,
24
- }
25
-
26
27
  export type Codec<T> = {
27
28
  write: (ch: ChannelState, value: T) -> (),
28
29
  read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
29
30
  }
30
31
 
31
32
  --[[
32
- InternalCodec carries optional metadata used by Gate (validation),
33
- Reader (max-item bound), Auto (type dispatch), and composite codecs
34
- (direct write/read fast path).
33
+ Optional metadata used by Gate (validation), Reader (max-item bound),
34
+ Auto (type dispatch), and composite codecs (direct write/read fast path).
35
35
  ]]
36
36
  export type InternalCodec<T> = {
37
37
  write: (ch: ChannelState, value: T) -> (),
@@ -40,32 +40,38 @@ export type InternalCodec<T> = {
40
40
  _directWrite: ((b: buffer, offset: number, value: T) -> ())?,
41
41
  _directRead: ((b: buffer, offset: number) -> T)?,
42
42
  _isDelta: boolean?,
43
+ --[[
44
+ True if any nested codec is delta. Lets the broadcast hot path skip
45
+ encode-once when delta state would diverge per-receiver.
46
+ ]]
47
+ _hasDelta: boolean?,
43
48
  _isBool: boolean?,
44
49
  _hasUnknown: boolean?,
45
50
  _min: number?,
46
51
  _max: number?,
47
52
  _isInteger: boolean?,
48
- _schema: { [string]: InternalCodec<any> }?,
53
+ _schema: { [string]: InternalCodec<T> }?,
49
54
  _typeCheck: string?,
50
55
  --[[
51
- Composite-codec metadata. Gate uses these to recurse element /
52
- variant validation rather than fall through to a generic NaN scan.
56
+ Composite metadata: lets Gate recurse element/variant validation
57
+ instead of falling through to the generic NaN/inf scan.
53
58
  ]]
54
59
  _isOptional: boolean?,
55
- _inner: InternalCodec<any>?,
60
+ _inner: InternalCodec<T>?,
56
61
  _isTagged: boolean?,
57
- _variants: { [string]: InternalCodec<any> }?,
62
+ _variants: { [string]: InternalCodec<T> }?,
58
63
  _tagField: string?,
59
64
  _isArray: boolean?,
60
- _element: InternalCodec<any>?,
65
+ _element: InternalCodec<T>?,
61
66
  _maxCount: number?,
62
67
  _isMap: boolean?,
63
- _keyCodec: InternalCodec<any>?,
64
- _valueCodec: InternalCodec<any>?,
68
+ _keyCodec: InternalCodec<T>?,
69
+ _valueCodec: InternalCodec<T>?,
65
70
  _isTuple: boolean?,
66
- _elements: { InternalCodec<any> }?,
71
+ _elements: { InternalCodec<T> }?,
67
72
  _isEnum: boolean?,
68
73
  _enumValues: { [string]: number }?,
74
+ _maxStringLength: number?,
69
75
  }
70
76
 
71
77
  export type RateLimitState = {
@@ -76,7 +82,7 @@ export type RateLimitState = {
76
82
 
77
83
  --[[
78
84
  Token-bucket (maxPerSecond + optional burst) or cooldown (cooldown
79
- seconds between accepts). Mixing both forms is undefined.
85
+ seconds between accepts). Mixing both forms is undefined behavior.
80
86
  ]]
81
87
  export type RateLimitConfig = {
82
88
  maxPerSecond: number?,
@@ -84,6 +90,10 @@ export type RateLimitConfig = {
84
90
  cooldown: number?,
85
91
  }
86
92
 
93
+ --[[
94
+ User-supplied per-packet validator. Returns (true, nil) on accept;
95
+ (false, reason) on reject — the reason is forwarded to onDrop hooks.
96
+ ]]
87
97
  export type ValidateFn = (data: any, player: Player) -> (boolean, string?)
88
98
 
89
99
  export type PacketOptions = {
@@ -106,6 +116,7 @@ export type Registration = {
106
116
  kind: number,
107
117
  codec: InternalCodec<any>,
108
118
  isUnreliable: boolean,
119
+ -- Response registration id; set on REQUEST regs only.
109
120
  partner: number?,
110
121
  signal: SignalLike,
111
122
  rateLimit: RateLimitConfig?,
@@ -160,9 +171,10 @@ export type PlayerStats = {
160
171
  }
161
172
 
162
173
  --[[
163
- Server keys baselines per Player; client uses `false` since booleans
164
- are valid table keys but Players never collide with that.
174
+ Server keys baselines per Player; client uses `false` since booleans are
175
+ valid table keys but Players never collide with that.
165
176
  ]]
166
177
  export type BaselineKey = Player | false
167
178
 
168
- return nil
179
+ -- Type-only module: runtime table is empty so re-requires are cheap.
180
+ return table.freeze({})