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.
Releases · Install · Example · API · Codecs · Benchmarks
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.
Install
Wally — add to your wally.toml:
Lync = "axp3cter/lync@2.3.1"npm (roblox-ts):
npm install @axpecter/lyncimport Lync from "@axpecter/lync";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.
Example
Shared — ReplicatedStorage.Net
local Lync = require(game.ReplicatedStorage.Lync)
return table.freeze({
State = Lync.packet("State", Lync.deltaStruct({
position = Lync.vec3,
health = Lync.float(0, 100, 0.5),
status = Lync.enum("idle", "moving", "attacking", "dead"),
alive = Lync.bool,
})),
Hit = Lync.packet("Hit", Lync.struct({
targetId = Lync.int(0, 65535),
damage = Lync.float(0, 200, 0.1),
}), {
rateLimit = { maxPerSecond = 30, burst = 5 },
validate = function(data) return data.damage <= 200, "damage" end,
}),
Ping = Lync.query("Ping", Lync.nothing, Lync.f64, { timeout = 3 }),
})Server
local Lync = require(game.ReplicatedStorage.Lync)
local Net = require(game.ReplicatedStorage.Net)
local Players = game:GetService("Players")
local alive = Lync.group("alive")
Players.PlayerAdded:Connect(function(p) alive:add(p) end)
Net.Hit:on(function(data, sender) end)
Net.Ping:handle(function() return os.clock() end)
Lync.start()
game:GetService("RunService").Heartbeat:Connect(function()
Net.State:send(getState(), alive)
end)Client
local Lync = require(game.ReplicatedStorage.Lync)
local Net = require(game.ReplicatedStorage.Net)
Lync.start()
local scope = Lync.scope()
scope:on(Net.State, function(state) end)
Net.Hit:send({ targetId = 123, damage = 45 })
local serverTime = Net.Ping:request(nil)API
Lifecycle
| Function | Description |
|---|---|
Lync.configure(opts) |
Apply options. Must precede start(). |
Lync.start() |
Initialize transport. Call once. |
Lync.isStarted() |
true after start(). |
Lync.flush() |
Force an immediate send. |
Lync.flushRate(hz) |
1–60 Hz. Default 60. |
Lync.reset() |
Restore module state to post-require defaults. For tests / hot reload. |
Configure options
| Option | Default | Range | Description |
|---|---|---|---|
channelMaxSize |
262144 | 4 KB – 1 MB | Per-frame buffer cap. |
validationDepth |
16 | 4–32 | Schema-walk recursion limit. |
poolSize |
16 | 2–128 | Reusable channel-state pool. |
bandwidthLimit |
none | — | { softLimit, maxStrikes } per-player throttle. |
globalRateLimit |
none | — | { maxPerSecond } across all packets per player. |
stats |
false |
— | Enables :stats() and Lync.stats.player(). |
Packets
Lync.packet(name, codec, options?)
-- Server
packet:send(data, player)
packet:send(data, Lync.all)
packet:send(data, Lync.except(p1, group1))
packet:send(data, { p1, p2, p3 })
packet:send(data, group)
-- Client
packet:send(data)
-- Both sides
local conn = packet:on(function(data, sender, timestamp) end)
packet:once(fn)
local data, sender, timestamp = packet:wait()
packet:name()
packet:stats() -- requires stats=true| Option | Type | Description |
|---|---|---|
unreliable |
boolean | Use UnreliableRemoteEvent. Rejected with delta codecs (a dropped frame would desync the baseline). |
rateLimit |
RateLimitConfig |
Server-side per-player. |
validate |
(data, player) → (bool, string?) |
Drop on false. Reason is forwarded to onDrop. |
maxPayloadBytes |
number | Reject oversize incoming payloads early. |
timestamp |
"frame", "offset", "full" |
Append 1B / 2B / 8B timestamp. Read as the third arg. |
Queries
Lync.query(name, requestCodec, responseCodec, options?)
Request-response on top of two paired registrations. Single-target requests yield until reply or timeout; multi-target requests gather a partial map.
-- Server
query:handle(function(data, player) return response end)
local resp = query:request(data, player) -- response?
local map = query:request(data, group) -- { [Player]: response? }
-- Client
query:handle(function(data) return response end)
local resp = query:request(data) -- yields; nil on timeout| Option | Default | Description |
|---|---|---|
timeout |
5 | Seconds before yielding nil. |
rateLimit |
{ maxPerSecond = 30 } |
Server-side. |
validate |
none | (data, player) → (bool, string?) |
Groups
Lync.group(name) — named player set. Members auto-removed on PlayerRemoving. Iterable: for player in group do.
| Method | Returns | Description |
|---|---|---|
:add(p) / :remove(p) |
boolean |
true if membership changed. |
:has(p) |
boolean |
|
:count() |
number |
|
:destroy() |
— | Clear members and free the name. |
Scope
Lync.scope() — batches connections for a single :destroy().
local scope = Lync.scope()
scope:on(packet, fn)
scope:once(packet, fn)
scope:add(rbxConnection)
scope:destroy()Targets
Server-side :send second arg.
| Target | Description |
|---|---|
Player |
One player. |
Lync.all |
All connected. |
Lync.except(...) |
Everyone except given Players or Groups. |
{ p1, p2 } |
Array of players. |
group |
All members. |
Middleware
-- Return Lync.DROP from onSend to discard a packet.
Lync.onSend(function(data, name, player) return data end)
Lync.onReceive(function(data, name, player) return data end)
Lync.onDrop(function(player, reason, name, data) end)All return a Connection. A throwing hook surfaces to the caller and aborts the chain at that point.
Connection
c.connected |
boolean |
c:disconnect() |
Idempotent. |
Stats
Enable with Lync.configure({ stats = true }).
| Function | Description |
|---|---|
Lync.stats.player(p) |
{ bytesSent, bytesReceived }. Server only. |
Lync.stats.reset() |
Zero all counters. |
packet:stats() |
{ bytesSent, bytesReceived, fires, recvFires, drops }. Aggregated across the request + response registrations on queries. |
Debug
| Function | Description |
|---|---|
Lync.debug.pending() |
In-flight query correlation IDs. |
Lync.debug.registrations() |
Frozen { name, id, kind, isUnreliable } per registration. |
capture / stop / dump are reserved no-ops for capture/replay tooling.
Codecs
Numbers
| Codec | Bytes | Notes |
|---|---|---|
int(min, max) |
1 / 2 / 4 | Picks narrowest u8/u16/u32/i8/i16/i32. |
f16 / f32 / f64 |
2 / 4 / 8 | f16 ≈ ±65504, ~3 digits. |
float(min, max, precision) |
1 / 2 / 4 | Quantized; clamped to range. |
bool |
1 | Auto-bitpacked inside struct and array. |
Strings & buffers
| Codec | Notes |
|---|---|
string |
Variable length. Binary-safe. |
string(maxLength) |
Bounded. Rejects on read if exceeded. |
buff |
Variable-length raw buffer. |
Roblox types
| Codec | Bytes |
|---|---|
vec2 / vec3 |
8 / 12 |
cframe |
24 |
color3 |
3 |
inst |
2 (sidecar ref index) |
udim / udim2 |
8 / 16 |
numberRange |
8 |
rect |
16 |
ray |
24 |
vec2int16 / vec3int16 |
4 / 6 |
region3 / region3int16 |
24 / 12 |
numberSequence / colorSequence |
variable |
Quantized variants
Call as a function for compression.
| Codec | Bytes | Notes |
|---|---|---|
vec2(min, max, precision) |
2 / 4 / 8 | Per-component quantization. |
vec3(min, max, precision) |
3 / 6 / 12 | Per-component quantization. |
cframe() |
16 | Smallest-three quaternion. ≤ 0.16° rotation error. |
Composites
| Codec | Notes |
|---|---|
struct({k = c}) |
Named fields. Bools auto-bitpacked into a tail block. |
array(c, max?) |
List. Bool arrays bitpacked. Direct path for fixed-size elements. |
map(k, v, max?) |
Key-value pairs; keys sorted at encode for stable wire bytes. |
optional(c) |
1B presence flag + value. |
tuple(...) |
Positional. All-direct fast path when every element is fixed-size. |
tagged(field, {name = c}) |
Discriminated union. 1B tag. Up to 256 variants. |
Delta — reliable transport only
Sends 1 byte when the value is byte-equal to the cached previous frame.
| Codec | Notes |
|---|---|
deltaStruct(schema) |
Per-segment dirty bitmap; single-segment fast path. |
deltaArray(c, max?) |
Whole-array byte-equality. |
deltaMap(k, v, max?) |
Whole-map byte-equality. |
Meta
| Codec | Notes |
|---|---|
enum(...) |
String enum. ≤ 256 variants. 1B u8 index. |
bitfield(schema) |
1–32 bits total. { type = "bool" }, { type = "uint", width }, { type = "int", width }. |
custom(size, write, read, typeCheck?) |
User-defined fixed-size codec. |
nothing |
0 bytes; reads nil. For fire-and-forget signals. |
unknown |
Bypasses serialization through the channel sidecar. Must be paired with validate. |
auto |
Self-describing: nil / bool / numbers / strings / buffers / Roblox datatypes. 1B type tag + payload. |
Rate limiting
Per-packet, pick one mode:
- Token bucket:
{ maxPerSecond = N, burst = M } - Cooldown:
{ cooldown = seconds }
Global per-player cap: Lync.configure({ globalRateLimit = { maxPerSecond = N } }).
Limits
| Packet + query IDs (combined) | 127 |
| Buffer per frame | 1 MB max |
| In-flight queries | 65,535 |
| Enum / tagged variants | 256 |
| Bitfield total bits | 32 |
| Sidecar refs per frame | 65,535 |
Benchmarks
rojo serve bench.project.json with one server + one client.
Cross-library — 1000 fires/frame, 10 s
Blink's methodology: same payload reused every frame, identical entity / bool shapes. Other tools from Blink v0.17.1.
| Tool | array<entity>[100] |
array<bool>[1000] |
|---|---|---|
| roblox | 16 fps · 559,364 Kbps | 21 fps · 353,107 Kbps |
| lync | 60 fps · 3.44 Kbps | 60 fps · 2.46 Kbps |
| blink | 42 fps · 41.81 Kbps | 97 fps · 7.91 Kbps |
| zap | 39 fps · 41.71 Kbps | 52 fps · 8.10 Kbps |
| bytenet | 32 fps · 41.64 Kbps | 35 fps · 8.11 Kbps |
Network bandwidth — 100 fires/frame, 8 s
| Workload | Kbps |
|---|---|
array<entity>[100] randomised |
3,608 |
array<entity>[100] reused |
2.4 |
array<bool>[1000] randomised |
763 |
array<bool>[1000] 1 bit flipped |
20.5 |
struct(state) randomised |
201 |
deltaStruct(state) 1 field mutated |
29.2 |
License
MIT