Package Exports
- @axpecter/lync
- @axpecter/lync/src/init.luau
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@axpecter/lync) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Lync
Buffer networking for Roblox. Delta compression, XOR framing, built-in security.
Releases · Benchmarks · Limits
Install
Wally (Luau)
[dependencies]
Lync = "axp3cter/lync@1.3.1"npm (roblox-ts)
npm install @rbxts/lyncimport Lync from "@rbxts/lync";Or grab the .rbxm from releases and drop it in ReplicatedStorage.
[!IMPORTANT] Define everything before calling
Lync.start(). Packets, queries, namespaces, all of it.
Lifecycle
| What it does | |
|---|---|
Lync.start() |
Sets up transport. Server creates remotes, client connects. Call once after all definitions. |
Lync.version |
"1.3.1" |
Lync.VERSION |
"1.3.1" |
Packets
Lync.definePacket(name, config) returns a Packet.
| Config | Type | Required | What it does |
|---|---|---|---|
value |
Codec | Yes | How to serialize the payload. |
unreliable |
boolean | No | Sends over UnreliableRemoteEvent. Default false. Cant use with delta codecs. |
rateLimit |
{ maxPerSecond, burstAllowance? } |
No | Server-side token bucket. Burst defaults to maxPerSecond if you dont set it. |
validate |
(data, player) → (bool, string?) |
No | Server-side. Return false, "reason" to drop. Runs after NaN scan. |
maxPayloadBytes |
number | No | Server-side. Max bytes a single batch of this packet can consume. Fires onDrop with reason "size" if exceeded. |
Server methods:
| Method | What it does |
|---|---|
packet:sendTo(data, player) |
Send to one player. |
packet:sendToAll(data) |
Send to everyone. |
packet:sendToAllExcept(data, except) |
Send to everyone except one. |
packet:sendToList(data, players) |
Send to a list. |
packet:sendToGroup(data, groupName) |
Send to a named group. |
Client methods:
| Method | What it does |
|---|---|
packet:send(data) |
Send to server. |
Shared methods:
| Method | What it does |
|---|---|
packet:listen(fn(data, sender)) |
Listen for incoming. Returns a Connection. Sender is Player on server, nil on client. |
packet:once(fn(data, sender)) |
Same as listen but auto-disconnects after one fire. |
packet:wait() |
Yields until next fire. Returns (data, sender). |
packet:disconnectAll() |
Kills all listeners on this packet. |
Queries
Lync.defineQuery(name, config) returns a Query. Basically RemoteFunctions but built on RemoteEvents. Returns nil if the other side times out or errors.
| Config | Type | Required | What it does |
|---|---|---|---|
request |
Codec | Yes | How to serialize the request. |
response |
Codec | Yes | How to serialize the response. |
timeout |
number | No | Seconds before giving up. Default 5. |
rateLimit |
{ maxPerSecond, burstAllowance? } |
No | Server-side token bucket on incoming requests. |
validate |
(data, player) → (bool, string?) |
No | Server-side validation on incoming requests. |
| Method | Where | What it does |
|---|---|---|
query:listen(fn) |
Both | Register a handler. Server gets fn(request, player) → response. Client gets fn(request) → response. |
query:invoke(request) |
Client | Send request to server, yield until response comes back or timeout. |
query:invoke(request, player) |
Server | Send request to a specific client, yield until response or timeout. |
query:invokeAll(request) |
Server | Send request to all players, yield until all respond or timeout. Returns { [Player]: response? }. |
query:invokeList(request, players) |
Server | Send request to a list of players, yield until all respond or timeout. Returns { [Player]: response? }. |
query:invokeGroup(request, groupName) |
Server | Send request to all players in a named group. Returns { [Player]: response? }. |
Namespaces
Lync.defineNamespace(name, config) returns a Namespace. Takes a packets table and/or a queries table. All names get auto-prefixed with "YourNamespace." so nothing collides.
Access packets and queries by their short name on the returned object: ns.PacketName, ns.QueryName.
| Method | What it does |
|---|---|
ns:listenAll(fn(name, data, sender)) |
Listens to every packet in the namespace. name is the short name without prefix. Returns a Connection. |
ns:onSend(fn(data, name, player) → data?) |
Send middleware that only runs for this namespace. Returns a remover. |
ns:onReceive(fn(data, name, player) → data?) |
Receive middleware that only runs for this namespace. Returns a remover. |
ns:disconnectAll() |
Kills all listeners made through listenAll. |
ns:destroy() |
Kills listeners and removes scoped middleware. Full cleanup. |
ns:packetNames() |
Sorted list of packet short names. |
ns:queryNames() |
Sorted list of query short names. |
Connection
Returned by packet:listen(), packet:once(), query:listen(), and ns:listenAll().
| What it does | |
|---|---|
connection.connected |
true if still connected, false after disconnect. |
connection:disconnect() |
Stops the listener. |
Types
Primitives
| Type | Bytes | Range |
|---|---|---|
Lync.u8 |
1 | 0 to 255 |
Lync.u16 |
2 | 0 to 65,535 |
Lync.u32 |
4 | 0 to 4,294,967,295 |
Lync.i8 |
1 | -128 to 127 |
Lync.i16 |
2 | -32,768 to 32,767 |
Lync.i32 |
4 | -2,147,483,648 to 2,147,483,647 |
Lync.f16 |
2 | ±65,504, roughly 3 digits of precision |
Lync.f32 |
4 | IEEE 754 single |
Lync.f64 |
8 | IEEE 754 double |
Lync.bool |
1 | true/false. Gets packed into bitfields when inside structs. |
Complex
| Type | Bytes | What it is |
|---|---|---|
Lync.string |
varint + N | Varint length prefix then raw bytes. |
Lync.vec2 |
8 | 2x f32. |
Lync.vec3 |
12 | 3x f32. |
Lync.cframe |
24 | Position as 3x f32, rotation as axis-angle 3x f32. |
Lync.color3 |
3 | RGB 0-255 per channel, clamped. |
Lync.inst |
2 | Instance ref through sidecar array. Requires refs on read, throws without them. |
Lync.buff |
varint + N | Varint length prefix then raw bytes. |
Lync.udim |
8 | Scale f32 + Offset i32. |
Lync.udim2 |
16 | 2x UDim (X then Y). |
Lync.numberRange |
8 | Min f32 + Max f32. |
Lync.rect |
16 | Min.X f32 + Min.Y f32 + Max.X f32 + Max.Y f32. |
Lync.vec2int16 |
4 | 2x i16. |
Lync.vec3int16 |
6 | 3x i16. |
Lync.region3 |
24 | Min Vec3 + Max Vec3 as 6x f32. |
Lync.region3int16 |
12 | Min Vec3int16 + Max Vec3int16 as 6x i16. |
Lync.ray |
24 | Origin Vec3 + Direction Vec3 as 6x f32. |
Lync.numberSequence |
varint + N×12 | Varint count then (time f32 + value f32 + envelope f32) per keypoint. |
Lync.colorSequence |
varint + N×7 | Varint count then (time f32 + R u8 + G u8 + B u8) per keypoint. |
Composites
| Constructor | What it does |
|---|---|
Lync.struct({ key = codec }) |
Named fields. Bools get packed into bitfields automatically. |
Lync.array(codec, maxCount?) |
Variable length list with varint count. Optional maxCount rejects on read if exceeded. |
Lync.map(keyCodec, valueCodec, maxCount?) |
Key-value pairs with varint count. Optional maxCount rejects on read if exceeded. |
Lync.optional(codec) |
1 byte flag, value only if present. |
Lync.tuple(codec, codec, ...) |
Ordered positional values, no keys. |
Lync.boundedString(maxLength) |
Same wire format as Lync.string but rejects on read if length exceeds maxLength. |
Delta
Reliable only. Lync will error if you try to use these with unreliable = true.
| Constructor | What it does |
|---|---|
Lync.deltaStruct({ key = codec }) |
First frame sends everything. After that only dirty fields get sent via bitmask. If nothing changed it costs 1 byte. |
Lync.deltaArray(codec, maxCount?) |
Same idea but for arrays. Dirty elements get sent with varint indices. Optional maxCount rejects on read if exceeded. |
Lync.deltaMap(keyCodec, valueCodec, maxCount?) |
Delta compression for key-value maps. Sends only upserted and removed entries after the first frame. Optional maxCount rejects on read if exceeded. |
Specialized
| Constructor | What it does |
|---|---|
Lync.enum(value, value, ...) |
u8 index, up to 256 variants. |
Lync.quantizedFloat(min, max, precision) |
Fixed-point compression. Picks u8/u16/u32 based on your range and precision. |
Lync.quantizedVec3(min, max, precision) |
Same thing but for all 3 components. |
Lync.bitfield({ key = spec }) |
Sub-byte packing, 1 to 32 bits total. Spec is { type = "bool" } or { type = "uint", width = N } or { type = "int", width = N }. |
Lync.tagged(tagField, { name = codec }) |
Discriminated union with a u8 variant tag. Puts tagField into the decoded table so you know which variant it is. |
Lync.custom(size, write, read) |
User-defined fixed-size codec. write is (b, offset, value) → (), read is (b, offset) → value. Plugs into struct/array/delta specialization automatically. |
Lync.nothing |
Zero bytes. Reads nil. Good for fire-and-forget signals. |
Lync.unknown |
Skips serialization entirely, goes through Roblox's sidecar. Requires refs array on read (same as Lync.inst). Use when you dont have a codec for the value. |
Lync.auto |
Self-describing. Writes a u8 type tag then the value. Handles nil, bool, all number types, string, vec2, vec3, color3, cframe, buffer, udim, udim2, numberRange, rect, vec2int16, vec3int16, region3, region3int16, ray, numberSequence, colorSequence. |
Groups
Named player sets. Members get removed automatically on PlayerRemoving.
| Function | Returns | What it does |
|---|---|---|
Lync.createGroup(name) |
Makes a new group. Errors if it already exists. | |
Lync.destroyGroup(name) |
Removes the group and all memberships. | |
Lync.addToGroup(name, player) |
boolean |
true if added, false if already in. |
Lync.removeFromGroup(name, player) |
boolean |
true if removed, false if wasnt in there. |
Lync.hasInGroup(name, player) |
boolean |
|
Lync.groupCount(name) |
number |
|
Lync.getGroupSet(name) |
{ [Player]: true } |
|
Lync.forEachInGroup(name, fn) |
Calls fn(player) for each member. |
Send to a group with packet:sendToGroup(data, groupName).
Middleware
Global intercept on all packets. Handlers run in the order you registered them. Return nil from a handler to drop the packet.
| Function | What it does |
|---|---|
Lync.onSend(fn(data, name, player) → data?) |
Runs before a packet goes out. Returns a remover function. |
Lync.onReceive(fn(data, name, player) → data?) |
Runs when a packet comes in. Returns a remover function. |
Lync.onDrop(fn(player, reason, name, data)) |
Fires when a packet gets rejected. Returns a remover function. Supports multiple handlers. Reason is "nan", "rate", "validate", "size", or whatever string your validate function returned. |
Packets that fail validation are dropped individually. Other packets in the same frame from the same player are unaffected.
Benchmarks
Lync Tests
1,000 packets/frame, 10 seconds, one player.
| Scenario | Without Lync | With Lync | FPS |
|---|---|---|---|
| Static booleans (1B) | 480 Kbps | 2.45 Kbps | 59.98 |
| Static entities (34B) | 16,320 Kbps | 2.52 Kbps | 60.00 |
| Moving entities | 16,320 Kbps | 3.51 Kbps | 59.99 |
| Chaotic entities | 16,320 Kbps | 4.66 Kbps | 59.99 |
Cross-Library Comparison
Same data shapes and methodology as Blink's benchmark suite. 1,000 fires/frame, 10 seconds, same data every frame. Kbps scaled by 60/FPS.
Entities (100x struct of 6x u8, fired 1000 times/frame)
| Tool (FPS) | Median | P0 | P80 | P90 | P95 | P100 |
|---|---|---|---|---|---|---|
| roblox | 16.00 | 16.00 | 15.00 | 15.00 | 15.00 | 15.00 |
| lync | 60.00 | 61.00 | 60.00 | 60.00 | 60.00 | 59.00 |
| blink | 42.00 | 45.00 | 42.00 | 42.00 | 42.00 | 42.00 |
| zap | 39.00 | 40.00 | 38.00 | 38.00 | 38.00 | 38.00 |
| bytenet | 32.00 | 34.00 | 32.00 | 32.00 | 32.00 | 31.00 |
| Tool (Kbps) | Median | P0 | P80 | P90 | P95 | P100 |
|---|---|---|---|---|---|---|
| roblox | 559,364 | 559,364 | 676,715 | 676,715 | 676,715 | 784,081 |
| lync | 3.59 | 3.50 | 3.61 | 3.62 | 3.62 | 4.86 |
| blink | 41.81 | 26.30 | 42.40 | 42.48 | 42.48 | 42.62 |
| zap | 41.71 | 25.46 | 42.19 | 42.32 | 42.32 | 42.93 |
| bytenet | 41.64 | 22.84 | 42.36 | 42.82 | 42.82 | 43.24 |
Booleans (1000x bool, fired 1000 times/frame)
| Tool (FPS) | Median | P0 | P80 | P90 | P95 | P100 |
|---|---|---|---|---|---|---|
| roblox | 21.00 | 22.00 | 20.00 | 19.00 | 19.00 | 19.00 |
| lync | 60.00 | 61.00 | 60.00 | 59.00 | 59.00 | 58.00 |
| blink | 97.00 | 98.00 | 97.00 | 96.00 | 96.00 | 96.00 |
| zap | 52.00 | 53.00 | 51.00 | 51.00 | 51.00 | 49.00 |
| bytenet | 35.00 | 37.00 | 35.00 | 35.00 | 35.00 | 34.00 |
| Tool (Kbps) | Median | P0 | P80 | P90 | P95 | P100 |
|---|---|---|---|---|---|---|
| roblox | 353,107 | 196,826 | 690,747 | 842,240 | 842,240 | 1,124,176 |
| lync | 4.31 | 3.77 | 4.33 | 4.34 | 4.34 | 4.43 |
| blink | 7.91 | 7.41 | 7.93 | 7.99 | 7.99 | 8.00 |
| zap | 8.10 | 5.75 | 8.17 | 8.22 | 8.22 | 8.27 |
| bytenet | 8.11 | 5.07 | 8.35 | 8.46 | 8.46 | 8.47 |
[!NOTE] Lync benchmarks run on Ryzen 7 7800X3D, 32GB DDR5-4800. Other tool numbers are from Blink's published benchmarks (v0.17.1, Ryzen 9 7900X, 34GB DDR5-4800). Different CPUs so FPS numbers arent directly comparable but bandwidth numbers are since Kbps is scaled by 60/FPS. Lync hits the 60 FPS frame cap in both tests.
Limits & Configuration
Call these before Lync.start().
| What | Default | How to change | Notes |
|---|---|---|---|
| Packet types | 255 | Cant change | u8 on the wire. Each query eats 2 IDs. |
| Buffer per channel per frame | 256 KB | Lync.setChannelMaxSize(n) |
4 KB to 1 MB. |
| Concurrent queries | 65,536 | Cant change | u16 correlation IDs. Freed on response or timeout. Lync.queryPendingCount() returns in-flight count. |
| NaN/inf scan depth | 16 | Lync.setValidationDepth(n) |
4 to 32. |
| Channel pool | 16 | Lync.setPoolSize(n) |
2 to 128. Extra gets GCd. |
| Namespaces | 64 | Cant change | |
| Delta + unreliable | Nope | Cant change | Errors at define time. |
License
MIT