Forest Logo
search
package_2

lync

By @axp3cter

Roblox

Mirrored

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.3"

npm (roblox-ts):

npm install @axpecter/lync
import 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

SharedReplicatedStorage.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

FunctionDescription
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

OptionDefaultRangeDescription
channelMaxSize2621444 KB – 1 MBPer-frame buffer cap.
validationDepth164–32Schema-walk recursion limit.
poolSize162–128Reusable channel-state pool.
bandwidthLimitnone{ softLimit, maxStrikes } per-player throttle.
globalRateLimitnone{ maxPerSecond } across all packets per player.
statsfalseEnables :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
OptionTypeDescription
unreliablebooleanUse UnreliableRemoteEvent. Rejected with delta codecs (a dropped frame would desync the baseline).
rateLimitRateLimitConfigServer-side per-player.
validate(data, player) → (bool, string?)Drop on false. Reason is forwarded to onDrop.
maxPayloadBytesnumberReject 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
OptionDefaultDescription
timeout5Seconds before yielding nil.
rateLimit{ maxPerSecond = 30 }Server-side.
validatenone(data, player) → (bool, string?)

Groups

Lync.group(name) — named player set. Members auto-removed on PlayerRemoving. Iterable: for player in group do.

MethodReturnsDescription
:add(p) / :remove(p)booleantrue 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.

TargetDescription
PlayerOne player.
Lync.allAll connected.
Lync.except(...)Everyone except given Players or Groups.
{ p1, p2 }Array of players.
groupAll 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.connectedboolean
c:disconnect()Idempotent.

Stats

Enable with Lync.configure({ stats = true }).

FunctionDescription
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

FunctionDescription
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

CodecBytesNotes
int(min, max)1 / 2 / 4Picks narrowest u8/u16/u32/i8/i16/i32.
zint(min?, max?)1 – 5Variable-length signed via zigzag varint. 1 byte for [-96, 95].
f16 / f32 / f642 / 4 / 8f16 ≈ ±65504, ~3 digits.
float(min, max, precision)1 / 2 / 3 / 4Quantized; picks u8 / u16 / u24 / u32 wire form.
bool1Auto-bitpacked inside struct and array.

Strings & buffers

CodecNotes
stringVariable length. Binary-safe.
string(maxLength)Bounded. Rejects on read if exceeded.
buffVariable-length raw buffer.

Roblox types

CodecBytes
vec2 / vec38 / 12
cframe24
color33
inst2 (sidecar ref index)
udim / udim28 / 16
numberRange8
rect16
ray24
vec2int16 / vec3int164 / 6
region3 / region3int1624 / 12
numberSequence / colorSequencevariable

Quantized variants

Call as a function for compression.

CodecBytesNotes
vec2(min, max, precision)2 / 4 / 6 / 8Per-component, narrowest fitting width.
vec3(min, max, precision)3 / 6 / 9 / 12Per-component, narrowest fitting width.
cframe()16Smallest-three quaternion. ≤ 0.16° rotation error.

Composites

CodecNotes
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

Tracks the previous frame's value and ships only what changed. Rejected on unreliable = true.

CodecStaticMutation
deltaStruct(schema)1 Bper-field
deltaArray(c, max?)1 Bper-changed-index
deltaMap(k, v, max?)1 Bper-changed-key
deltaInt(min, max)1 B1–5 B
deltaFloat(min, max, precision)1 B1–5 B
deltaVec3(min, max, precision)3 B3–15 B
deltaCFrame(posMin, posMax, precision)1 B4–13 B
  • deltaArray element / deltaMap key+value cannot themselves contain delta state. Use deltaStruct for per-field deltas inside.
  • deltaVec3 and deltaCFrame error on out-of-range components.

Meta

CodecNotes
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.
nothing0 bytes; reads nil. For fire-and-forget signals.
unknownBypasses serialization through the channel sidecar. Must be paired with validate.
autoSelf-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 frame1 MB max
In-flight queries65,535
Enum / tagged variants256
Bitfield total bits32
Sidecar refs per frame65,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.

Toolarray<entity>[100]array<bool>[1000]
roblox16 fps · 559,364 Kbps21 fps · 353,107 Kbps
lync59 fps · 3.37 Kbps61 fps · 2.45 Kbps
blink42 fps · 41.81 Kbps97 fps · 7.91 Kbps
zap39 fps · 41.71 Kbps52 fps · 8.10 Kbps
bytenet32 fps · 41.64 Kbps35 fps · 8.11 Kbps

Network bandwidth — 100 fires/frame, 8 s

WorkloadNaive KbpsOptimizedSavings
array<entity>[100] random3,607deltaArray 3 of 100 mutated154 (–96%)
array<entity>[100] reused3,607XOR baseline (identical frames)2.4 (–99.9%)
array<bool>[1000] random762XOR baseline (1 bit flipped)20.4 (–97%)
struct(state) random201deltaStruct 1 field mutated29.0 (–86%)
map<id, vec3>[200] 5 keys mutated657deltaMap 5 keys mutated393 (–40%)
array<cframe>[50] random4,585
vec3 walking motion (continuous diff)deltaVec319.5
CFrame walking pose (pos + rot)deltaCFrame41.1

License

MIT

Package Details

Install command (Click to copy)


Version

2.3.3

License

MIT

check_circle

Safe for commercial use

infoThe package archive does not include its license text; the license is declared in its manifest metadata.

Automated license review — not legal advice.