@axpecter/lync 2.1.0 → 2.1.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 (3) hide show
  1. package/README.md +279 -414
  2. package/package.json +1 -1
  3. package/src/index.d.ts +33 -31
package/README.md CHANGED
@@ -5,13 +5,10 @@
5
5
  <a href="#install">Install</a> ·
6
6
  <a href="#example">Example</a> ·
7
7
  <a href="#codecs">Codecs</a> ·
8
- <a href="#wire-protocol">Wire Protocol</a> ·
9
8
  <a href="#benchmarks">Benchmarks</a>
10
9
  </p>
11
10
 
12
- Lync serializes structured data into flat buffers, batches all sends into a single `RemoteEvent:FireClient` per player per frame, and applies XOR framing so Roblox's internal deflate compressor can eliminate redundancy across frames. On the server, every incoming payload is schema-validated and rate-limited before any listener fires.
13
-
14
- All codecs are defined at runtime. No code generation, no build step, no external CLI. Packets, queries, groups, and middleware are configured in shared modules and resolved at `Lync.start()`.
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.
15
12
 
16
13
  ## Install
17
14
 
@@ -19,7 +16,7 @@ All codecs are defined at runtime. No code generation, no build step, no externa
19
16
 
20
17
  ```toml
21
18
  [dependencies]
22
- Lync = "axp3cter/lync@2.1.0"
19
+ Lync = "axp3cter/lync@2.1.1"
23
20
  ```
24
21
 
25
22
  **npm (roblox-ts)**
@@ -32,10 +29,10 @@ npm install @axpecter/lync
32
29
  import Lync from "@axpecter/lync";
33
30
  ```
34
31
 
35
- Or grab the `.rbxm` from [Releases](https://github.com/Axp3cter/Lync/releases/latest) and drop it into `ReplicatedStorage`.
32
+ Or grab the `.rbxm` from [Releases](https://github.com/Axp3cter/Lync/releases/latest).
36
33
 
37
34
  > [!IMPORTANT]
38
- > All packets, queries, and groups must be defined before calling `Lync.start()`. The registry assigns sequential IDs at define time. Defining packets after `start()` will cause ID mismatches between server and client.
35
+ > Define all packets, queries, and groups before calling `Lync.start()`.
39
36
 
40
37
  ## Example
41
38
 
@@ -137,526 +134,394 @@ local serverTime = Net.Ping:request(nil)
137
134
  if serverTime then print("server clock:", serverTime) end
138
135
  ```
139
136
 
140
- ## Lifecycle
141
-
142
- | Function | Behavior |
143
- |:---------|:---------|
144
- | `Lync.configure(options)` | Sets limits and enables stats. Must be called before `start()`. See [Configuration](#configuration). |
145
- | `Lync.start()` | Server creates remotes under `ReplicatedStorage.LyncRemotes`. Client waits for them. Connects the Heartbeat flush loop. Errors if called twice. |
146
- | `Lync.started` | Read-only boolean. `true` after `start()` returns. |
147
- | `Lync.flush()` | Forces an immediate buffer flush. Resets the accumulator to prevent double-sending on the next Heartbeat. Errors if not started. |
148
- | `Lync.flushRate(hz)` | 1–60. Default 60. At 60, flushes every Heartbeat directly. Below 60, uses an elapsed-time accumulator with drift correction. Callable at runtime. |
149
-
150
137
  ## Packets
151
138
 
152
- `Lync.packet(name, codec, options?)` returns a Packet handle. The second argument is any codec. Options go in the optional third argument.
153
-
154
- ### Packet Options
139
+ `Lync.packet(name, codec, options?)`
155
140
 
156
- | Field | Type | Default | Behavior |
157
- |:------|:-----|:--------|:---------|
158
- | `unreliable` | `boolean` | `false` | Routes through `UnreliableRemoteEvent`. Incompatible with delta codecs (errors at define time). |
159
- | `rateLimit` | `RateLimitConfig` | none | Server-side rate limiting on incoming fires. See [Rate Limiting](#rate-limiting). |
160
- | `validate` | `(data, player) → (bool, string?)` | none | Server-side callback after schema validation. Return `false, "reason"` to drop. Fires `onDrop`. |
161
- | `maxPayloadBytes` | `number` | none | Maximum bytes a single payload can consume. |
162
- | `timestamp` | `"frame"`, `"offset"`, or `"full"` | none | Prepends a timestamp to each item. `"frame"` = u8 wrapping counter (1B). `"offset"` = u16 milliseconds into the current second (2B). `"full"` = f64 `os.clock()` (8B). Listeners receive it as a third argument after `sender`. |
141
+ ### Options
163
142
 
164
- ### Packet Methods
165
-
166
- **Sending (server):**
167
-
168
- ```luau
169
- packet:send(data, player) -- single player
170
- packet:send(data, Lync.all) -- all connected players
171
- packet:send(data, Lync.except(p1, p2)) -- all except specified
172
- packet:send(data, { p1, p2, p3 }) -- array of players
173
- packet:send(data, group) -- group members
174
- ```
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. |
175
150
 
176
- **Sending (client):**
151
+ ### Sending
177
152
 
178
153
  ```luau
179
- packet:send(data) -- to server
154
+ -- Server
155
+ packet:send(data, player)
156
+ packet:send(data, Lync.all)
157
+ packet:send(data, Lync.except(p1, p2))
158
+ packet:send(data, { p1, p2, p3 })
159
+ packet:send(data, group)
160
+
161
+ -- Client
162
+ packet:send(data)
180
163
  ```
181
164
 
182
- **Receiving (both):**
165
+ ### Receiving
183
166
 
184
- | Method | Behavior |
185
- |:-------|:---------|
186
- | `packet:on(fn)` | Connects a listener. `fn(data, sender, timestamp?)`. Server `sender` is `Player`. Client `sender` is `nil`. Returns a [Connection](#connection). |
187
- | `packet:once(fn)` | Same as `on` but auto-disconnects after one fire. |
188
- | `packet:wait()` | Yields until the next fire. Returns `(data, sender, timestamp?)`. |
189
- | `packet:name()` | Returns the registration name string. |
190
- | `packet:stats()` | Returns `{ bytesSent, bytesReceived, fires, recvFires, drops }`. Populated only when stats are enabled. |
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. |
191
174
 
192
175
  ## Queries
193
176
 
194
- `Lync.query(name, requestCodec, responseCodec, options?)` returns a Query handle. Built on RemoteEvents with varint correlation IDs. Returns `nil` on timeout or handler error.
177
+ `Lync.query(name, requestCodec, responseCodec, options?)`
178
+
179
+ Request-response built on packets. Returns `nil` on timeout.
195
180
 
196
- ### Query Options
181
+ ### Options
197
182
 
198
- | Field | Type | Default | Behavior |
199
- |:------|:-----|:--------|:---------|
200
- | `timeout` | `number` | 5 | Seconds before the request yields `nil`. |
201
- | `rateLimit` | `RateLimitConfig` | `{ maxPerSecond = 30 }` | Server-side rate limiting on incoming requests. |
202
- | `validate` | `(data, player) → (bool, string?)` | none | Server-side validation on incoming requests. |
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. |
203
188
 
204
- ### Query Methods
189
+ ### Methods
205
190
 
206
- | Method | Context | Behavior |
207
- |:-------|:--------|:---------|
208
- | `query:handle(fn)` | Both | Registers a handler. Server: `fn(request, player) → response`. Client: `fn(request) → response`. Returns a Connection that clears the handler on disconnect. |
209
- | `query:request(data)` | Client | Sends request to server, yields until response or timeout. Returns the response or `nil`. |
210
- | `query:request(data, player)` | Server | Sends request to one client, yields until response or timeout. |
211
- | `query:request(data, target)` | Server | Sends request to multiple targets. Returns `{ [Player]: response? }`. Accepts `Lync.all`, arrays, and groups. |
212
- | `query:name()` | Both | Returns the registration name. |
213
- | `query:stats()` | Both | Returns combined stats for the request and response channels. |
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. |
214
199
 
215
- Each query consumes two packet IDs internally (one for requests, one for responses).
200
+ Each query consumes two packet IDs internally.
216
201
 
217
202
  ## Groups
218
203
 
219
- `Lync.group(name)` returns a Group. Members are removed automatically on `PlayerRemoving`. Names must be unique (duplicate errors). Destroyed groups free their name for reuse.
204
+ `Lync.group(name)`
220
205
 
221
- Groups implement `__iter`, so `for player in group do` works directly.
206
+ Named player sets. Members auto-removed on `PlayerRemoving`. Iterable with `for player in group do`.
222
207
 
223
- | Method | Returns | Behavior |
224
- |:-------|:--------|:---------|
225
- | `group:add(player)` | `boolean` | `true` if added, `false` if already a member. |
226
- | `group:remove(player)` | `boolean` | `true` if removed, `false` if not a member. |
208
+ | Method | Returns | Description |
209
+ |:-------|:--------|:------------|
210
+ | `group:add(player)` | `boolean` | `true` if added. |
211
+ | `group:remove(player)` | `boolean` | `true` if removed. |
227
212
  | `group:has(player)` | `boolean` | Membership check. |
228
- | `group:count()` | `number` | Current member count. |
229
- | `group:destroy()` | — | Clears all members and frees the name. Safe to call multiple times. |
213
+ | `group:count()` | `number` | Member count. |
214
+ | `group:destroy()` | — | Clears members, frees name. |
230
215
 
231
216
  ## Scope
232
217
 
233
- `Lync.scope()` batches connections for lifecycle-aligned cleanup.
218
+ `Lync.scope()`
219
+
220
+ Batches connections for cleanup.
234
221
 
235
222
  ```luau
236
223
  local scope = Lync.scope()
237
224
  scope:on(packetA, fnA)
238
225
  scope:on(packetB, fnB)
239
226
  scope:add(someRBXScriptConnection)
240
- scope:destroy()
227
+ scope:destroy() -- disconnects everything
241
228
  ```
242
229
 
243
- | Method | Behavior |
244
- |:-------|:---------|
245
- | `scope:on(source, fn)` | Calls `source:on(fn)` and tracks the returned connection. |
246
- | `scope:once(source, fn)` | Calls `source:once(fn)` and tracks the returned connection. |
247
- | `scope:add(connection)` | Accepts both Lync connections and `RBXScriptConnection`. |
248
- | `scope:destroy()` | Disconnects all tracked connections. Safe to call multiple times. |
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. |
249
236
 
250
237
  ## Connection
251
238
 
252
- Returned by `packet:on()`, `packet:once()`, `query:handle()`, `scope:on()`, and middleware functions.
239
+ Returned by `packet:on()`, `packet:once()`, `query:handle()`, and middleware functions.
253
240
 
254
- | Field/Method | Behavior |
255
- |:-------------|:---------|
256
- | `connection.connected` | `boolean`. `true` until disconnected. |
257
- | `connection:disconnect()` | Stops the listener. O(1) via swap-remove. Safe to call multiple times. Safe to call during a fire (snapshot iteration prevents skipped listeners). |
241
+ | Field / Method | Description |
242
+ |:---------------|:------------|
243
+ | `connection.connected` | `boolean` |
244
+ | `connection:disconnect()` | Stops the listener. Safe mid-fire, safe to call multiple times. |
258
245
 
259
246
  ## Middleware
260
247
 
261
- Global intercept chains on all packets. Handlers run in registration order. Return a transformed value to pass it downstream. Return `nil` to pass through unchanged. Return `Lync.DROP` from `onSend` to silently drop the packet.
248
+ ```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)
262
256
 
263
- All three functions return a [Connection](#connection).
257
+ Lync.onDrop(function(player, reason, name, data)
258
+ warn(player.Name, "dropped", name, reason)
259
+ end)
260
+ ```
264
261
 
265
- | Function | Behavior |
266
- |:---------|:---------|
267
- | `Lync.onSend(fn)` | `fn(data, name, player?) → data?`. Runs before serialization. |
268
- | `Lync.onReceive(fn)` | `fn(data, name, player?) → data?`. Runs after deserialization and validation. |
269
- | `Lync.onDrop(fn)` | `fn(player, reason, name, data?)`. Fires when a packet is rejected. Reason is `"rate"`, `"validation"`, or the string returned by the `validate` callback. |
270
- | `Lync.DROP` | Frozen sentinel. Return from `onSend` to silently drop the packet. |
262
+ All three return a Connection.
271
263
 
272
264
  ## Targets
273
265
 
274
- Server-side second argument to `packet:send()` and `query:request()`.
266
+ Server-side second argument to `packet:send()`.
275
267
 
276
- | Target | Behavior |
277
- |:-------|:---------|
278
- | `player` | Single `Player` instance. |
279
- | `Lync.all` | All connected players via `Players:GetPlayers()`. |
280
- | `Lync.except(...)` | All players except specified. Accepts any mix of `Player` and Group arguments. |
281
- | `{ p1, p2, ... }` | Lua array of players. Non-player entries are silently skipped. |
282
- | `group` | All current members of a [Group](#groups). |
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. |
283
275
 
284
276
  ## Codecs
285
277
 
286
278
  ### Numbers
287
279
 
288
- `Lync.int(min, max)` selects the smallest wire type that fits the range:
289
-
290
- | Range | Wire | Bytes |
291
- |:------|:-----|------:|
292
- | `[0, 255]` | u8 | 1 |
293
- | `[0, 65535]` | u16 | 2 |
294
- | `[0, 4294967295]` | u32 | 4 |
295
- | `[-128, 127]` | i8 | 1 |
296
- | `[-32768, 32767]` | i16 | 2 |
297
- | `[-2147483648, 2147483647]` | i32 | 4 |
298
-
299
- Signed integers use unsigned buffer writes (`writeu8`/`writeu16`/`writeu32`) with two's complement conversion because `writei8`/`writei16`/`writei32` are not FASTCALL-optimized in Luau.
300
-
301
- | Codec | Bytes | Behavior |
302
- |:------|------:|:---------|
303
- | `Lync.f16` | 2 | Half-precision IEEE 754. ~3 decimal digits. ±65504 normal range. Overflow clamps to ±inf. NaN preserved. |
304
- | `Lync.f32` | 4 | IEEE 754 single-precision. |
305
- | `Lync.f64` | 8 | IEEE 754 double-precision. |
306
- | `Lync.bool` | 1 | `true`/`false`. Inside structs, bools are separated and bitpacked (8 per byte). Inside arrays, bools are bitpacked. Standalone uses 1 byte. |
307
-
308
- `Lync.float(min, max, precision)` quantizes a float range to an integer range. Wire type is selected by `ceil((max - min) / precision)`: u8 if ≤ 255, u16 if ≤ 65535, u32 otherwise. Values outside `[min, max]` are clamped.
280
+ `Lync.int(min, max)` picks the smallest wire type for your range.
309
281
 
310
- ### Strings and Buffers
311
-
312
- | Codec | Wire format | Behavior |
313
- |:------|:------------|:---------|
314
- | `Lync.string` | varint length + raw bytes | Lengths 0–191 use a 1-byte prefix (dense prefix-varint). 192+ use multi-byte. Binary-safe. |
315
- | `Lync.string(maxLength)` | same | Callable via `__call`. Same write path. Read rejects if decoded length exceeds `maxLength`. |
316
- | `Lync.buff` | varint length + raw bytes | Same wire format as string. Read returns an isolated buffer copy. |
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. |
295
+
296
+ ### Strings & Buffers
297
+
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. |
317
303
 
318
304
  ### Roblox Types
319
305
 
320
- All fixed-size types expose `_directWrite` and `_directRead` for struct fast-path optimization, except `Lync.inst` (requires the channel's ref array).
321
-
322
- | Codec | Bytes | Wire layout |
323
- |:------|------:|:------------|
324
- | `Lync.vec2` | 8 | 2× f32 |
325
- | `Lync.vec3` | 12 | 3× f32 |
326
- | `Lync.cframe` | 24 | 3× f32 position + 3× f32 axis-angle rotation |
327
- | `Lync.color3` | 3 | 3× u8 RGB, clamped to [0, 1] then scaled to [0, 255] |
328
- | `Lync.inst` | 2 | u16 index into sidecar `{ Instance }` array |
329
- | `Lync.udim` | 8 | f32 Scale + i32 Offset |
330
- | `Lync.udim2` | 16 | 2× UDim |
331
- | `Lync.numberRange` | 8 | f32 Min + f32 Max |
332
- | `Lync.rect` | 16 | 4× f32 |
333
- | `Lync.ray` | 24 | 6× f32 (Origin + Direction) |
334
- | `Lync.vec2int16` | 4 | 2× i16 |
335
- | `Lync.vec3int16` | 6 | 3× i16 |
336
- | `Lync.region3` | 24 | 6× f32 (Min + Max) |
337
- | `Lync.region3int16` | 12 | 6× i16 (Min + Max) |
338
- | `Lync.numberSequence` | varint + N×12 | f32 time + f32 value + f32 envelope per keypoint |
339
- | `Lync.colorSequence` | varint + N×7 | f32 time + u8 R + u8 G + u8 B per keypoint |
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 |
340
324
 
341
325
  ### Quantized Variants
342
326
 
343
- These codecs are callable. The bare name gives the lossless version; calling with arguments gives the quantized version.
327
+ Call the codec to get a quantized version.
344
328
 
345
- | Codec | Bytes | Behavior |
346
- |:------|------:|:---------|
347
- | `Lync.vec2(min, max, precision)` | 2–8 | Per-component quantization. 2B at u8, 4B at u16, 8B at u32. |
348
- | `Lync.vec3(min, max, precision)` | 3–12 | Per-component quantization. 3B at u8, 6B at u16, 12B at u32. |
349
- | `Lync.cframe()` | 16 | Smallest-three quaternion compression. 3× f32 position (12B) + 2-bit largest-component index + 3× 10-bit signed quaternion components (4B). Angular precision 0.16° (~0.003 radians). Saves 8 bytes vs lossless. |
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. |
350
334
 
351
335
  ### Composites
352
336
 
353
- | Constructor | Behavior |
354
- |:------------|:---------|
355
- | `Lync.struct(schema)` | `{ [string]: Codec }`. Fields serialized in sorted key order. Bools separated and bitpacked after all non-bool fields. All-fixed-size structs expose `_size`, `_directWrite`, `_directRead`. |
356
- | `Lync.array(element, maxCount?)` | Varint count + elements. Fixed-size elements use a stride loop. Bool elements are bitpacked. Optional `maxCount` rejects on read. |
357
- | `Lync.map(keyCodec, valueCodec, maxCount?)` | Varint count + key-value pairs. |
358
- | `Lync.optional(codec)` | 1-byte flag. `0` = nil. `1` = value follows. |
359
- | `Lync.tuple(...)` | Positional values without keys. All-fixed-size tuples expose `_size`. |
360
- | `Lync.tagged(tagField, variants)` | Discriminated union. u8 variant tag. `variants` is `{ [string]: Codec }`, sorted alphabetically for deterministic tag assignment. Tag field is injected on read. |
361
-
362
- ### Delta Codecs
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. |
363
345
 
364
- Reliable transport only. Errors at define time if combined with `unreliable = true`.
346
+ ### Delta
365
347
 
366
- Delta codecs serialize into a scratch buffer and compare byte-for-byte against a cached baseline. Identical bytes produce a 1-byte `UNCHANGED` flag. Any difference triggers a full re-send prefixed with a `FULL` flag byte.
348
+ Only works with reliable transport. Sends 1 byte when data hasn't changed.
367
349
 
368
- | Constructor | Behavior |
369
- |:------------|:---------|
370
- | `Lync.deltaStruct(schema)` | Same schema as `struct`. First frame is always full. |
371
- | `Lync.deltaArray(element, maxCount?)` | Delta-framed array. |
372
- | `Lync.deltaMap(keyCodec, valueCodec, maxCount?)` | Delta-framed map. |
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. |
373
355
 
374
- ### Meta Codecs
356
+ ### Meta
375
357
 
376
- | Constructor | Behavior |
377
- |:------------|:---------|
378
- | `Lync.enum(...)` | String enum. u8 index, up to 256 variants. Errors on unknown values at write time and duplicate values at define time. |
379
- | `Lync.bitfield(schema)` | Sub-byte packing, 1–32 bits. Spec: `{ type = "bool" }`, `{ type = "uint", width = N }`, or `{ type = "int", width = N }`. Wire: 1B ≤8 bits, 2B ≤16 bits, 4B ≤32 bits. Signed ints use sign extension. Fields sorted alphabetically. |
380
- | `Lync.custom(size, write, read)` | User-defined fixed-size codec. `write(buffer, offset, value)`, `read(buffer, offset) → value`. |
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. |
381
363
  | `Lync.nothing` | Zero bytes. Reads `nil`. |
382
- | `Lync.unknown` | Bypasses buffer serialization. Values go through the remote's sidecar array. Warns at define time if used without `validate`. |
383
- | `Lync.auto` | Self-describing. u8 type tag + value. Integers auto-sized. Floats try f32 then f64. Supports nil, bool, number, string, buffer, and 15 Roblox types. Tables error. |
384
-
385
- ## Wire Protocol
364
+ | `Lync.unknown` | Bypasses serialization entirely. Use with `validate`. |
365
+ | `Lync.auto` | Self-describing. Supports nil, bool, numbers, strings, buffers, and Roblox types. |
386
366
 
387
- ### Dense Prefix-Varint
367
+ ## Rate Limiting
388
368
 
389
- Variable-length unsigned integer encoding. 1-byte range covers 0–191 (LEB128 only covers 0–127).
369
+ Two modes (pick one per packet):
390
370
 
391
- | Range | Bytes | Encoding |
392
- |:------|------:|:---------|
393
- | 0–191 | 1 | Direct value |
394
- | 192–8,383 | 2 | `0xC0 + high5`, `low8` |
395
- | 8,384–1,056,959 | 3 | `0xE0 + high4`, `low16 LE` |
396
- | 1,056,960–4,294,967,295 | 5 | `0xF0`, `u32 LE` |
371
+ **Token bucket:** `{ maxPerSecond = N, burst = M }`
397
372
 
398
- ### MSB Batch Framing
373
+ **Cooldown:** `{ cooldown = seconds }`
399
374
 
400
- All sends within one Heartbeat are batched into a single buffer per player per reliability channel.
375
+ Global limit across all packets: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`
401
376
 
402
- **Single-item:** `[1IIIIIII] [payload]` — MSB set, 7-bit packet ID, no count byte. 1-byte header.
403
-
404
- **Multi-item:** `[0IIIIIII] [u16 count] [payload₁] ...` — MSB clear, u16 item count follows. Used when ≥2 sends to the same packet occur in one frame.
405
-
406
- The single-item path saves 2 bytes per packet per frame vs always writing a count. Maximum 127 packet IDs (7 bits).
407
-
408
- ### XOR Framing
409
-
410
- Reliable channels XOR the current frame against the previous before sending. The receiver XOR's against its previous decoded frame to recover the original. Produces long zero runs that compress well under Roblox's internal deflate.
411
-
412
- XOR operates in u32-aligned chunks with u8 remainder. Mismatched frame sizes are handled: excess bytes in a longer frame are copied directly.
413
-
414
- Unreliable channels skip XOR (no guaranteed frame ordering).
415
-
416
- ## Security
417
-
418
- ### Schema Validation
419
-
420
- Every incoming packet on the server passes through Gate before listeners fire:
421
-
422
- - **`_typeCheck`**: Rejects wrong `typeof`.
423
- - **`_isInteger` + `_min`/`_max`**: Rejects non-integers, NaN, inf, out-of-range.
424
- - **`_schema`** (struct codecs): Recursive per-field validation.
425
- - **Fallback**: NaN/inf scan up to `validationDepth` levels for codecs without metadata.
426
-
427
- Rejected packets fire `onDrop` and are silently discarded. Other packets in the same frame from the same player are unaffected.
428
-
429
- ### Rate Limiting
430
-
431
- Two modes (mutually exclusive):
432
-
433
- **Token bucket:** `{ maxPerSecond = N, burst = M }`. Tokens refill at N/sec. Burst defaults to 1. Each fire costs one token.
377
+ ## Configuration
434
378
 
435
- **Cooldown:** `{ cooldown = seconds }`. Rejects fires within `cooldown` seconds of the last accepted fire.
379
+ `Lync.configure(options)` call before `Lync.start()`.
436
380
 
437
- Global rate limit: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`. Checked before per-packet limits.
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()`. |
438
389
 
439
- ### Bandwidth Throttle
390
+ ### Lifecycle
440
391
 
441
- `Lync.configure({ bandwidthLimit = { softLimit = bytes, maxStrikes = N } })`. Per-player. Oversized frames increment strikes. Small frames decrement (decay). Exceeding `maxStrikes` drops the entire frame.
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. |
442
399
 
443
- ## Stats
400
+ ### Stats
444
401
 
445
- Disabled by default. Zero overhead when off. Enable via `Lync.configure({ stats = true })`.
402
+ Enable with `Lync.configure({ stats = true })`.
446
403
 
447
- | Function | Behavior |
448
- |:---------|:---------|
404
+ | Function | Description |
405
+ |:---------|:------------|
449
406
  | `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }` |
450
- | `Lync.stats.player(player)` | `{ bytesSent, bytesReceived }` or `nil`. Server only. |
407
+ | `Lync.stats.player(player)` | `{ bytesSent, bytesReceived }` server only. |
451
408
  | `Lync.stats.reset()` | Zeros all counters. |
452
409
 
453
- ## Debug
454
-
455
- | Function | Behavior |
456
- |:---------|:---------|
457
- | `Lync.debug.pending()` | In-flight query request count. |
458
- | `Lync.debug.registrations()` | Frozen array of `{ name, id, kind, isUnreliable }`. |
459
-
460
- ## Configuration
461
-
462
- `Lync.configure(options)` — call before `Lync.start()`.
410
+ ### Debug
463
411
 
464
- | Option | Default | Range | Behavior |
465
- |:-------|--------:|:------|:---------|
466
- | `channelMaxSize` | 262,144 | 4,096–1,048,576 | Max bytes per channel buffer per frame. |
467
- | `validationDepth` | 16 | 4–32 | Max recursion for NaN/inf scanning. |
468
- | `poolSize` | 16 | 2–128 | ChannelState reuse pool size. |
469
- | `bandwidthLimit` | none | — | `{ softLimit, maxStrikes }`. Per-player. |
470
- | `globalRateLimit` | none | — | `{ maxPerSecond }`. Per-player across all packets. |
471
- | `stats` | `false` | — | Enables stat counters. |
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. |
472
416
 
473
417
  ## Limits
474
418
 
475
- | Constraint | Value |
419
+ | Constraint | Limit |
476
420
  |:-----------|------:|
477
- | Max packet/query registrations | 127 (7-bit wire ID, queries use 2 each) |
478
- | Max buffer per channel per frame | 256 KB default, 1 MB max |
479
- | Max concurrent query requests | 65,536 (varint correlation IDs) |
480
- | `Lync.enum` variants | 256 |
481
- | `Lync.bitfield` total bits | 32 |
482
- | `Lync.tagged` variants | 256 |
483
- | Bool packing density | 8 per byte |
484
- | String inline varint threshold | 191 bytes (1B prefix), 192+ uses multi-byte |
485
- | Delta + unreliable | Not allowed (define-time error) |
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 |
486
427
 
487
428
  ## Benchmarks
488
429
 
489
- Run `rojo serve bench.project.json`, open in Studio with one local server + one client.
490
-
491
- See `bench/Run.server.luau` for full configuration and methodology.
430
+ Run `rojo serve bench.project.json` with one server + one client.
492
431
 
493
432
  ### Wire Sizes
494
433
 
495
- Exact byte count per codec write. Raw payload only — no batch framing overhead included.
496
-
497
- | Codec | Input | Bytes |
498
- |:------|:------|------:|
499
- | `bool` | `true` | 1 |
500
- | `int(0, 255)` | `42` | 1 |
501
- | `int(0, 65535)` | `1000` | 2 |
502
- | `int(0, 1000000)` | `500000` | 4 |
503
- | `int(-128, 127)` | `-50` | 1 |
504
- | `f16` | `42.5` | 2 |
505
- | `f32` | `3.14` | 4 |
506
- | `f64` | `π` | 8 |
507
- | `nothing` | `nil` | 0 |
508
- | `string` | `""` (empty) | 1 |
509
- | `string` | 5 chars | 6 |
510
- | `string` | 191 chars (max inline prefix) | 192 |
511
- | `string` | 192 chars (varint prefix) | 194 |
512
- | `string` | 1000 chars | 1002 |
513
- | `vec2` | lossless | 8 |
514
- | `vec2(0, 100, 1)` | u8 quantized | 2 |
515
- | `vec3` | lossless | 12 |
516
- | `vec3(0, 100, 1)` | u8 quantized | 3 |
517
- | `vec3(-1000, 1000, 0.1)` | u16 quantized | 6 |
518
- | `cframe` | lossless | 24 |
519
- | `cframe()` | smallest-three | 16 |
520
- | `color3` | RGB | 3 |
521
- | `ray` | origin + direction | 24 |
522
- | entity struct | 6 fields + bool (lossless) | 34 |
523
- | entity struct | quantized fields (compact) | 13 |
524
- | bitfield | bool + uint packed | 2 |
525
- | `array` × 100 entities | 100× struct(6× u8) | 601 |
526
- | `array` × 1000 bools | bitpacked | 127 |
434
+ | 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 |
450
+ | entity compact (quantized) | 13 |
451
+ | bitfield | 2 |
452
+ | 100× entities | 601 |
453
+ | 1000× bools (bitpacked) | 127 |
527
454
 
528
455
  ### Codec Throughput
529
456
 
530
- Isolated CPU cost. No networking. Encode + decode measured independently. 100k iterations with warmup.
531
-
532
- | Codec | Bytes | Encode | Decode | Round-trips/sec |
533
- |:------|------:|-------:|-------:|----------------:|
534
- | `bool` | 1 | 44ns | 29ns | 13,862,127 |
535
- | `int(0, 255)` | 1 | 42ns | 28ns | 14,387,868 |
536
- | `int(0, 65535)` | 2 | 41ns | 28ns | 14,395,324 |
537
- | `f16` | 2 | 61ns | 42ns | 9,686,824 |
538
- | `f32` | 4 | 41ns | 25ns | 15,003,300 |
539
- | `f64` | 8 | 41ns | 26ns | 14,844,063 |
540
- | `string` (empty) | 1 | 30ns | 22ns | 19,240,019 |
541
- | `string` (10 chars) | 11 | 46ns | 60ns | 9,441,889 |
542
- | `string` (100 chars) | 101 | 48ns | 91ns | 7,166,506 |
543
- | `string` (1000 chars) | 1002 | 76ns | 238ns | 3,179,953 |
544
- | `vec2` | 8 | 75ns | 43ns | 8,417,720 |
545
- | `vec3` | 12 | 53ns | 27ns | 12,360,328 |
546
- | `vec3` (quantized) | 3 | 130ns | 85ns | 4,636,348 |
547
- | `cframe` (lossless) | 24 | 92ns | 144ns | 4,232,266 |
548
- | `cframe()` (compressed) | 16 | 123ns | 170ns | 3,413,621 |
549
- | `color3` | 3 | 125ns | 58ns | 5,482,756 |
550
- | `udim2` | 16 | 235ns | 112ns | 2,880,482 |
551
- | entity struct | 34 | 239ns | 395ns | 1,578,183 |
552
- | entity compact | 13 | 377ns | 490ns | 1,153,064 |
553
- | bitfield flags | 2 | 142ns | 332ns | 2,107,486 |
554
- | 100× entity array | 601 | 15.2µs | 34.1µs | 20,306 |
555
- | 1000× bool array | 127 | 4.3µs | 5.1µs | 106,806 |
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 |
556
473
 
557
474
  ### Delta Savings
558
475
 
559
- Byte cost across three consecutive writes: initial (full), identical repeat (unchanged), and single-field mutation (changed).
560
-
561
- | Codec | Full | Unchanged | Changed | Savings |
562
- |:------|-----:|----------:|--------:|--------:|
563
- | `deltaStruct` (entity) | 35B | 1B | 35B | 97% |
564
- | `deltaStruct` (compact) | 14B | 1B | 14B | 93% |
565
- | `deltaArray` (100× entity) | 602B | 1B | 1B | 100% |
566
- | `deltaArray` (1000× bool) | 128B | 1B | 1B | 99% |
567
- | `deltaMap` (string → u8) | 19B | 1B | 19B | 95% |
568
-
569
- ### Batch Framing
570
-
571
- MSB single-item batches use a 1-byte header. Multi-item batches add a u16 count after the header.
572
-
573
- | Scenario | Total bytes | Per-item overhead |
574
- |:---------|----------:|------------------:|
575
- | 1 × u8 (single-item) | 2B | 1B |
576
- | 10 × u8 (multi-item) | 13B | 0.3B |
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% |
577
483
 
578
484
  ### Network Throughput
579
485
 
580
- Live sends to one player. Measured over 8 seconds. FPS and Kbps at median and tail.
581
-
582
- | Packet | Fires/frame | FPS median | FPS p1 | Kbps median | Kbps p95 | Kbps p99 |
583
- |:-------|:---:|----:|----:|-----:|-----:|-----:|
584
- | booleans | 1000 | 60 | 59.9 | 2.5 | 6.2 | 6.2 |
585
- | entity struct | 1000 | 60 | 59.9 | 2.3 | 2.4 | 2.4 |
586
- | entity compact | 1000 | 60 | 59.9 | 2.4 | 2.5 | 2.5 |
587
- | 100× entities | 100 | 60 | 59.9 | 2.3 | 3.1 | 3.1 |
588
- | 1000× bools | 100 | 60 | 59.9 | 2.3 | 2.3 | 2.3 |
589
- | bitfield flags | 1000 | 60 | 59.9 | 2.4 | 2.5 | 2.5 |
590
- | cframe lossless | 1000 | 60 | 59.9 | 2.5 | 2.5 | 2.5 |
591
- | cframe compressed | 1000 | 60 | 59.8 | 2.3 | 2.3 | 2.3 |
486
+ 1000 fires/frame, 8 seconds, one player.
592
487
 
593
- ---
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 |
594
496
 
595
497
  ### Cross-Library Comparison
596
498
 
597
- The tables below use the same data shapes and methodology as [Blink's published benchmarks](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md): 1,000 fires/frame, same data every frame, 10 seconds, Kbps scaled by 60/FPS.
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.
598
500
 
599
- Numbers for `blink`, `zap`, `bytenet`, and `roblox` are copied directly from [Blink v0.17.1 results](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md) (2025-04-30).
501
+ Other tool numbers from [Blink v0.17.1](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md) (2025-04-30).
600
502
 
601
503
  > [!NOTE]
602
- > **Architectural differences that affect these numbers:**
603
- > - Lync batches all sends into one buffer per Heartbeat frame. Other tools fire one RemoteEvent per `send()`, paying ~40 bytes of Roblox overhead per call.
604
- > - Lync includes server-side schema validation and rate limiting. Other tools do not.
605
- > - Lync bitpacks bool arrays (1,000 bools ≈ 127 bytes vs ~1,002 bytes for 1-byte-per-bool).
606
- > - Lync uses runtime codecs. Blink and Zap use code generation with zero runtime schema.
607
- > - Delta compression is not exercised here (same data every frame). See [Delta Savings](#delta-savings) for the real-world impact.
608
- > - FPS is hardware-dependent. Kbps is FPS-scaled, making it comparable across machines.
609
-
610
- **Tool versions:** blink v0.17.1 · zap v0.6.20 · bytenet v0.4.3 · lync v2.1.0
611
-
612
- **Data shapes:** Entities = `100× struct { id u8, x u8, y u8, z u8, orientation u8, animation u8 }`. Booleans = `1000× bool`. [Source](https://github.com/1Axen/blink/blob/main/benchmark/src/shared/benches).
613
-
614
- #### EntitiesFPS
615
-
616
- | Tool | Median | P0 | P80 | P90 | P95 | P100 | Loss |
617
- |:-----|-------:|---:|----:|----:|----:|-----:|-----:|
618
- | roblox | 16.00 | 16.00 | 15.00 | 15.00 | 15.00 | 15.00 | 0% |
619
- | **lync** | **60.00** | **61.00** | **60.00** | **60.00** | **60.00** | **58.00** | **0%** |
620
- | blink | 42.00 | 45.00 | 42.00 | 42.00 | 42.00 | 42.00 | 0% |
621
- | zap | 39.00 | 40.00 | 38.00 | 38.00 | 38.00 | 38.00 | 0% |
622
- | bytenet | 32.00 | 34.00 | 32.00 | 32.00 | 32.00 | 31.00 | 0% |
623
-
624
- #### Entities — Kbps
625
-
626
- | Tool | Median | P0 | P80 | P90 | P95 | P100 | Loss |
627
- |:-----|-------:|---:|----:|----:|----:|-----:|-----:|
628
- | roblox | 559,364 | 559,364 | 676,716 | 676,716 | 676,716 | 784,082 | 0% |
629
- | **lync** | **3.68** | **3.61** | **3.72** | **3.75** | **3.75** | **4.18** | **0%** |
630
- | blink | 41.81 | 26.30 | 42.40 | 42.48 | 42.48 | 42.62 | 0% |
631
- | zap | 41.71 | 25.46 | 42.19 | 42.32 | 42.32 | 42.93 | 0% |
632
- | bytenet | 41.64 | 22.84 | 42.36 | 42.82 | 42.82 | 43.24 | 0% |
633
-
634
- #### Booleans — FPS
635
-
636
- | Tool | Median | P0 | P80 | P90 | P95 | P100 | Loss |
637
- |:-----|-------:|---:|----:|----:|----:|-----:|-----:|
638
- | roblox | 21.00 | 22.00 | 20.00 | 19.00 | 19.00 | 19.00 | 0% |
639
- | **lync** | **60.00** | **61.00** | **60.00** | **60.00** | **60.00** | **59.00** | **0%** |
640
- | blink | 97.00 | 98.00 | 97.00 | 96.00 | 96.00 | 96.00 | 0% |
641
- | zap | 52.00 | 53.00 | 51.00 | 51.00 | 51.00 | 49.00 | 0% |
642
- | bytenet | 35.00 | 37.00 | 35.00 | 35.00 | 35.00 | 34.00 | 0% |
643
-
644
- #### Booleans — Kbps
645
-
646
- | Tool | Median | P0 | P80 | P90 | P95 | P100 | Loss |
647
- |:-----|-------:|---:|----:|----:|----:|-----:|-----:|
648
- | roblox | 353,107 | 196,827 | 690,748 | 842,240 | 842,240 | 1,124,176 | 0% |
649
- | **lync** | **2.49** | **2.44** | **2.50** | **2.52** | **2.52** | **2.54** | **0%** |
650
- | blink | 7.91 | 7.41 | 7.93 | 7.99 | 7.99 | 8.00 | 0% |
651
- | zap | 8.10 | 5.75 | 8.17 | 8.22 | 8.22 | 8.27 | 0% |
652
- | bytenet | 8.11 | 5.07 | 8.35 | 8.46 | 8.46 | 8.47 | 0% |
653
-
654
- #### Wire Size Comparison
655
-
656
- | Data | Lync | Other tools | Difference |
657
- |:-----|-----:|------------:|-----------:|
658
- | 100× entities | 601B | ~602B | -1B |
659
- | 1000× bools | 127B | ~1002B | -875B (87% smaller) |
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).
505
+
506
+ #### Entities 100× struct(6× u8)
507
+
508
+ | Tool | FPS | Kbps |
509
+ |:-----|----:|-----:|
510
+ | roblox | 16 | 559,364 |
511
+ | **lync** | **60** | **3.68** |
512
+ | blink | 42 | 41.81 |
513
+ | zap | 39 | 41.71 |
514
+ | bytenet | 32 | 41.64 |
515
+
516
+ #### Booleans1000× bool
517
+
518
+ | Tool | FPS | Kbps |
519
+ |:-----|----:|-----:|
520
+ | roblox | 21 | 353,107 |
521
+ | **lync** | **60** | **2.49** |
522
+ | blink | 97 | 7.91 |
523
+ | zap | 52 | 8.10 |
524
+ | bytenet | 35 | 8.11 |
660
525
 
661
526
  ## License
662
527
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axpecter/lync",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
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/index.d.ts CHANGED
@@ -139,56 +139,57 @@ declare namespace Lync {
139
139
  interface LyncModule {
140
140
  // ── Lifecycle ────────────────────────────────────────────────────
141
141
 
142
- configure(options: Lync.ConfigureOptions): void;
143
- start(): void;
142
+ configure(this: void, options: Lync.ConfigureOptions): void;
143
+ start(this: void): void;
144
144
  readonly started: boolean;
145
145
 
146
146
  // ── Definitions ─────────────────────────────────────────────────
147
147
 
148
- packet<T>(name: string, codec: Lync.Codec<T>, options?: Lync.PacketOptions<T>): Lync.Packet<T>;
148
+ packet<T>(this: void, name: string, codec: Lync.Codec<T>, options?: Lync.PacketOptions<T>): Lync.Packet<T>;
149
149
 
150
150
  query<Req, Resp>(
151
+ this: void,
151
152
  name: string,
152
153
  requestCodec: Lync.Codec<Req>,
153
154
  responseCodec: Lync.Codec<Resp>,
154
155
  options?: Lync.QueryOptions<Req>,
155
156
  ): Lync.Query<Req, Resp>;
156
157
 
157
- group(name: string): Lync.Group;
158
- scope(): Lync.Scope;
158
+ group(this: void, name: string): Lync.Group;
159
+ scope(this: void): Lync.Scope;
159
160
 
160
161
  // ── Targeting ───────────────────────────────────────────────────
161
162
 
162
163
  readonly all: Lync.AllTarget;
163
- except(...args: Array<Player | Lync.Group>): Lync.ExceptTarget;
164
+ except(this: void, ...args: Array<Player | Lync.Group>): Lync.ExceptTarget;
164
165
  readonly DROP: Lync.DropSentinel;
165
166
 
166
167
  // ── Middleware ───────────────────────────────────────────────────
167
168
 
168
- onSend(fn: (data: unknown, name: string, player?: Player) => unknown): Lync.Connection;
169
- onReceive(fn: (data: unknown, name: string, player?: Player) => unknown): Lync.Connection;
170
- onDrop(fn: (player: Player, reason: string, name: string, data?: unknown) => void): Lync.Connection;
169
+ onSend(this: void, fn: (data: unknown, name: string, player?: Player) => unknown): Lync.Connection;
170
+ onReceive(this: void, fn: (data: unknown, name: string, player?: Player) => unknown): Lync.Connection;
171
+ onDrop(this: void, fn: (player: Player, reason: string, name: string, data?: unknown) => void): Lync.Connection;
171
172
 
172
173
  // ── Runtime Control ─────────────────────────────────────────────
173
174
 
174
- flush(): void;
175
- flushRate(hz: number): void;
175
+ flush(this: void): void;
176
+ flushRate(this: void, hz: number): void;
176
177
 
177
178
  // ── Stats ───────────────────────────────────────────────────────
178
179
 
179
180
  readonly stats: {
180
- player(player: Player): Lync.PlayerStats | undefined;
181
- reset(): void;
181
+ player(this: void, player: Player): Lync.PlayerStats | undefined;
182
+ reset(this: void): void;
182
183
  };
183
184
 
184
185
  // ── Debug ───────────────────────────────────────────────────────
185
186
 
186
187
  readonly debug: {
187
- capture(label?: string): void;
188
- stop(): void;
189
- dump(): void;
190
- pending(): number;
191
- registrations(): ReadonlyArray<{
188
+ capture(this: void, label?: string): void;
189
+ stop(this: void): void;
190
+ dump(this: void): void;
191
+ pending(this: void): number;
192
+ registrations(this: void): ReadonlyArray<{
192
193
  name: string;
193
194
  id: number;
194
195
  kind: number;
@@ -198,8 +199,8 @@ interface LyncModule {
198
199
 
199
200
  // ── Number Codecs ───────────────────────────────────────────────
200
201
 
201
- int(min: number, max: number): Lync.Codec<number>;
202
- float(min: number, max: number, precision: number): Lync.Codec<number>;
202
+ int(this: void, min: number, max: number): Lync.Codec<number>;
203
+ float(this: void, min: number, max: number, precision: number): Lync.Codec<number>;
203
204
  readonly f16: Lync.Codec<number>;
204
205
  readonly f32: Lync.Codec<number>;
205
206
  readonly f64: Lync.Codec<number>;
@@ -234,24 +235,25 @@ interface LyncModule {
234
235
 
235
236
  // ── Composites ──────────────────────────────────────────────────
236
237
 
237
- struct<S extends Record<string, Lync.Codec<unknown>>>(schema: S): Lync.Codec<Lync.InferSchema<S>>;
238
- deltaStruct<S extends Record<string, Lync.Codec<unknown>>>(schema: S): Lync.Codec<Lync.InferSchema<S>>;
239
- array<T>(element: Lync.Codec<T>, maxCount?: number): Lync.Codec<T[]>;
240
- deltaArray<T>(element: Lync.Codec<T>, maxCount?: number): Lync.Codec<T[]>;
241
- map<K, V>(keyCodec: Lync.Codec<K>, valueCodec: Lync.Codec<V>, maxCount?: number): Lync.Codec<Map<K, V>>;
242
- deltaMap<K, V>(keyCodec: Lync.Codec<K>, valueCodec: Lync.Codec<V>, maxCount?: number): Lync.Codec<Map<K, V>>;
243
- optional<T>(codec: Lync.Codec<T>): Lync.Codec<T | undefined>;
244
- tuple<T extends Lync.Codec<unknown>[]>(...codecs: T): Lync.Codec<{ [K in keyof T]: Lync.InferCodec<T[K]> }>;
238
+ struct<S extends Record<string, Lync.Codec<unknown>>>(this: void, schema: S): Lync.Codec<Lync.InferSchema<S>>;
239
+ deltaStruct<S extends Record<string, Lync.Codec<unknown>>>(this: void, schema: S): Lync.Codec<Lync.InferSchema<S>>;
240
+ array<T>(this: void, element: Lync.Codec<T>, maxCount?: number): Lync.Codec<T[]>;
241
+ deltaArray<T>(this: void, element: Lync.Codec<T>, maxCount?: number): Lync.Codec<T[]>;
242
+ map<K, V>(this: void, keyCodec: Lync.Codec<K>, valueCodec: Lync.Codec<V>, maxCount?: number): Lync.Codec<Map<K, V>>;
243
+ deltaMap<K, V>(this: void, keyCodec: Lync.Codec<K>, valueCodec: Lync.Codec<V>, maxCount?: number): Lync.Codec<Map<K, V>>;
244
+ optional<T>(this: void, codec: Lync.Codec<T>): Lync.Codec<T | undefined>;
245
+ tuple<T extends Lync.Codec<unknown>[]>(this: void, ...codecs: T): Lync.Codec<{ [K in keyof T]: Lync.InferCodec<T[K]> }>;
245
246
  tagged<Tag extends string, V extends Record<string, Lync.Codec<unknown>>>(
247
+ this: void,
246
248
  tagField: Tag,
247
249
  variants: V,
248
250
  ): Lync.Codec<{ [K in keyof V]: Lync.InferSchema<{ [F in Tag]: K }> & Lync.InferCodec<V[K]> }[keyof V]>;
249
251
 
250
252
  // ── Meta ────────────────────────────────────────────────────────
251
253
 
252
- enum<T extends string[]>(...values: T): Lync.Codec<T[number]>;
253
- bitfield(schema: Record<string, { type: "bool" } | { type: "uint"; width: number } | { type: "int"; width: number }>): Lync.Codec<Record<string, boolean | number>>;
254
- custom<T>(size: number, write: (b: buffer, offset: number, value: T) => void, read: (b: buffer, offset: number) => T): Lync.Codec<T>;
254
+ enum<T extends string[]>(this: void, ...values: T): Lync.Codec<T[number]>;
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>;
255
257
  readonly nothing: Lync.Codec<undefined>;
256
258
  readonly unknown: Lync.Codec<unknown>;
257
259
  readonly auto: Lync.Codec<unknown>;