JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 312
  • Score
    100M100P100Q87171F
  • License MIT

Buffer networking for Roblox. Delta compression, XOR framing, built-in security.

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/lync
import 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