npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@axpecter/lync

v2.3.3

Published

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

Readme

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/[email protected]"

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

| 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. | | zint(min?, max?) | 1 – 5 | Variable-length signed via zigzag varint. 1 byte for [-96, 95]. | | f16 / f32 / f64 | 2 / 4 / 8 | f16 ≈ ±65504, ~3 digits. | | float(min, max, precision) | 1 / 2 / 3 / 4 | Quantized; picks u8 / u16 / u24 / u32 wire form. | | 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 / 6 / 8 | Per-component, narrowest fitting width. | | vec3(min, max, precision) | 3 / 6 / 9 / 12 | Per-component, narrowest fitting width. | | 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

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

| Codec | Static | Mutation | |:---|:---:|:---:| | deltaStruct(schema) | 1 B | per-field | | deltaArray(c, max?) | 1 B | per-changed-index | | deltaMap(k, v, max?) | 1 B | per-changed-key | | deltaInt(min, max) | 1 B | 1–5 B | | deltaFloat(min, max, precision) | 1 B | 1–5 B | | deltaVec3(min, max, precision) | 3 B | 3–15 B | | deltaCFrame(posMin, posMax, precision) | 1 B | 4–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

| 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 | 59 fps · 3.37 Kbps | 61 fps · 2.45 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 | Naive Kbps | Optimized | Savings | |:---|---:|:---|---:| | array<entity>[100] random | 3,607 | deltaArray 3 of 100 mutated | 154 (–96%) | | array<entity>[100] reused | 3,607 | XOR baseline (identical frames) | 2.4 (–99.9%) | | array<bool>[1000] random | 762 | XOR baseline (1 bit flipped) | 20.4 (–97%) | | struct(state) random | 201 | deltaStruct 1 field mutated | 29.0 (–86%) | | map<id, vec3>[200] 5 keys mutated | 657 | deltaMap 5 keys mutated | 393 (–40%) | | array<cframe>[50] random | 4,585 | — | — | | vec3 walking motion (continuous diff) | — | deltaVec3 | 19.5 | | CFrame walking pose (pos + rot) | — | deltaCFrame | 41.1 |

License

MIT