{"id":"axp3cter/lync","name":"lync","scope":"axp3cter","platform":"roblox","description":"Buffer networking for Roblox. Delta compression, XOR framing, built-in security","version":"2.3.3","latest":"2.3.3","versions":["1.0.1","1.1.0","1.3.0","1.3.1","1.4.0","1.4.1","1.4.2","1.4.3","1.5.0","1.5.1","1.5.2","2.0.0","2.1.0","2.1.1","2.1.2","2.2.0","2.2.1","2.3.0","2.3.1","2.3.2","2.3.3"],"license":"MIT","licenseRating":"safe","licenseCaveats":["The package archive does not include its license text; the license is declared in its manifest metadata."],"licenseVerified":false,"dependencies":{},"integrity":"6f55eaf1c9fe6fed2de433921d70db6b83944b99ac22fcfa549712adc9b4549a","likes":0,"downloads":0,"install":"forest install axp3cter/lync","url":"https://forest.dev/p/roblox/axp3cter/lync","files":"https://api.forest.dev/ai/package/roblox/axp3cter/lync/files","readme":"<h1 align=\"center\">Lync</h1>\n<p align=\"center\">Buffer networking for Roblox.</p>\n<p align=\"center\">\n  <a href=\"https://github.com/Axp3cter/Lync/releases/latest\">Releases</a> ·\n  <a href=\"#install\">Install</a> ·\n  <a href=\"#example\">Example</a> ·\n  <a href=\"#api\">API</a> ·\n  <a href=\"#codecs\">Codecs</a> ·\n  <a href=\"#benchmarks\">Benchmarks</a>\n</p>\n\nSchemas, 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.\n\n## Install\n\nWally — add to your `wally.toml`:\n\n```toml\nLync = \"axp3cter/lync@2.3.3\"\n```\n\nnpm (roblox-ts):\n\n```bash\nnpm install @axpecter/lync\n```\n\n```typescript\nimport Lync from \"@axpecter/lync\";\n```\n\n**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.\n\n## Example\n\n**Shared** — `ReplicatedStorage.Net`\n\n```luau\nlocal Lync = require(game.ReplicatedStorage.Lync)\n\nreturn table.freeze({\n    State = Lync.packet(\"State\", Lync.deltaStruct({\n        position = Lync.vec3,\n        health   = Lync.float(0, 100, 0.5),\n        status   = Lync.enum(\"idle\", \"moving\", \"attacking\", \"dead\"),\n        alive    = Lync.bool,\n    })),\n\n    Hit = Lync.packet(\"Hit\", Lync.struct({\n        targetId = Lync.int(0, 65535),\n        damage   = Lync.float(0, 200, 0.1),\n    }), {\n        rateLimit = { maxPerSecond = 30, burst = 5 },\n        validate  = function(data) return data.damage <= 200, \"damage\" end,\n    }),\n\n    Ping = Lync.query(\"Ping\", Lync.nothing, Lync.f64, { timeout = 3 }),\n})\n```\n\n**Server**\n\n```luau\nlocal Lync    = require(game.ReplicatedStorage.Lync)\nlocal Net     = require(game.ReplicatedStorage.Net)\nlocal Players = game:GetService(\"Players\")\n\nlocal alive = Lync.group(\"alive\")\nPlayers.PlayerAdded:Connect(function(p) alive:add(p) end)\n\nNet.Hit:on(function(data, sender) end)\nNet.Ping:handle(function() return os.clock() end)\n\nLync.start()\n\ngame:GetService(\"RunService\").Heartbeat:Connect(function()\n    Net.State:send(getState(), alive)\nend)\n```\n\n**Client**\n\n```luau\nlocal Lync = require(game.ReplicatedStorage.Lync)\nlocal Net  = require(game.ReplicatedStorage.Net)\n\nLync.start()\n\nlocal scope = Lync.scope()\nscope:on(Net.State, function(state) end)\n\nNet.Hit:send({ targetId = 123, damage = 45 })\nlocal serverTime = Net.Ping:request(nil)\n```\n\n## API\n\n### Lifecycle\n\n| Function | Description |\n|:---|:---|\n| `Lync.configure(opts)` | Apply options. Must precede `start()`. |\n| `Lync.start()` | Initialize transport. Call once. |\n| `Lync.isStarted()` | `true` after `start()`. |\n| `Lync.flush()` | Force an immediate send. |\n| `Lync.flushRate(hz)` | 1–60 Hz. Default 60. |\n| `Lync.reset()` | Restore module state to post-require defaults. For tests / hot reload. |\n\n### Configure options\n\n| Option | Default | Range | Description |\n|:---|---:|:---|:---|\n| `channelMaxSize` | 262144 | 4 KB – 1 MB | Per-frame buffer cap. |\n| `validationDepth` | 16 | 4–32 | Schema-walk recursion limit. |\n| `poolSize` | 16 | 2–128 | Reusable channel-state pool. |\n| `bandwidthLimit` | none | — | `{ softLimit, maxStrikes }` per-player throttle. |\n| `globalRateLimit` | none | — | `{ maxPerSecond }` across all packets per player. |\n| `stats` | `false` | — | Enables `:stats()` and `Lync.stats.player()`. |\n\n### Packets\n\n`Lync.packet(name, codec, options?)`\n\n```luau\n-- Server\npacket:send(data, player)\npacket:send(data, Lync.all)\npacket:send(data, Lync.except(p1, group1))\npacket:send(data, { p1, p2, p3 })\npacket:send(data, group)\n\n-- Client\npacket:send(data)\n\n-- Both sides\nlocal conn = packet:on(function(data, sender, timestamp) end)\npacket:once(fn)\nlocal data, sender, timestamp = packet:wait()\npacket:name()\npacket:stats() -- requires stats=true\n```\n\n| Option | Type | Description |\n|:---|:---|:---|\n| `unreliable` | boolean | Use `UnreliableRemoteEvent`. Rejected with delta codecs (a dropped frame would desync the baseline). |\n| `rateLimit` | `RateLimitConfig` | Server-side per-player. |\n| `validate` | `(data, player) → (bool, string?)` | Drop on `false`. Reason is forwarded to `onDrop`. |\n| `maxPayloadBytes` | number | Reject oversize incoming payloads early. |\n| `timestamp` | `\"frame\"`, `\"offset\"`, `\"full\"` | Append 1B / 2B / 8B timestamp. Read as the third arg. |\n\n### Queries\n\n`Lync.query(name, requestCodec, responseCodec, options?)`\n\nRequest-response on top of two paired registrations. Single-target requests yield until reply or timeout; multi-target requests gather a partial map.\n\n```luau\n-- Server\nquery:handle(function(data, player) return response end)\nlocal resp = query:request(data, player)        -- response?\nlocal map  = query:request(data, group)         -- { [Player]: response? }\n\n-- Client\nquery:handle(function(data) return response end)\nlocal resp = query:request(data)                -- yields; nil on timeout\n```\n\n| Option | Default | Description |\n|:---|:---|:---|\n| `timeout` | 5 | Seconds before yielding `nil`. |\n| `rateLimit` | `{ maxPerSecond = 30 }` | Server-side. |\n| `validate` | none | `(data, player) → (bool, string?)` |\n\n### Groups\n\n`Lync.group(name)` — named player set. Members auto-removed on `PlayerRemoving`. Iterable: `for player in group do`.\n\n| Method | Returns | Description |\n|:---|:---|:---|\n| `:add(p)` / `:remove(p)` | `boolean` | `true` if membership changed. |\n| `:has(p)` | `boolean` | |\n| `:count()` | `number` | |\n| `:destroy()` | — | Clear members and free the name. |\n\n### Scope\n\n`Lync.scope()` — batches connections for a single `:destroy()`.\n\n```luau\nlocal scope = Lync.scope()\nscope:on(packet, fn)\nscope:once(packet, fn)\nscope:add(rbxConnection)\nscope:destroy()\n```\n\n### Targets\n\nServer-side `:send` second arg.\n\n| Target | Description |\n|:---|:---|\n| `Player` | One player. |\n| `Lync.all` | All connected. |\n| `Lync.except(...)` | Everyone except given Players or Groups. |\n| `{ p1, p2 }` | Array of players. |\n| `group` | All members. |\n\n### Middleware\n\n```luau\n-- Return Lync.DROP from onSend to discard a packet.\nLync.onSend(function(data, name, player) return data end)\nLync.onReceive(function(data, name, player) return data end)\nLync.onDrop(function(player, reason, name, data) end)\n```\n\nAll return a `Connection`. A throwing hook surfaces to the caller and aborts the chain at that point.\n\n### Connection\n\n| | |\n|:---|:---|\n| `c.connected` | `boolean` |\n| `c:disconnect()` | Idempotent. |\n\n### Stats\n\nEnable with `Lync.configure({ stats = true })`.\n\n| Function | Description |\n|:---|:---|\n| `Lync.stats.player(p)` | `{ bytesSent, bytesReceived }`. Server only. |\n| `Lync.stats.reset()` | Zero all counters. |\n| `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }`. Aggregated across the request + response registrations on queries. |\n\n### Debug\n\n| Function | Description |\n|:---|:---|\n| `Lync.debug.pending()` | In-flight query correlation IDs. |\n| `Lync.debug.registrations()` | Frozen `{ name, id, kind, isUnreliable }` per registration. |\n\n`capture` / `stop` / `dump` are reserved no-ops for capture/replay tooling.\n\n## Codecs\n\n### Numbers\n\n| Codec | Bytes | Notes |\n|:---|---:|:---|\n| `int(min, max)` | 1 / 2 / 4 | Picks narrowest u8/u16/u32/i8/i16/i32. |\n| `zint(min?, max?)` | 1 – 5 | Variable-length signed via zigzag varint. 1 byte for [-96, 95]. |\n| `f16` / `f32` / `f64` | 2 / 4 / 8 | `f16` ≈ ±65504, ~3 digits. |\n| `float(min, max, precision)` | 1 / 2 / 3 / 4 | Quantized; picks u8 / u16 / u24 / u32 wire form. |\n| `bool` | 1 | Auto-bitpacked inside `struct` and `array`. |\n\n### Strings & buffers\n\n| Codec | Notes |\n|:---|:---|\n| `string` | Variable length. Binary-safe. |\n| `string(maxLength)` | Bounded. Rejects on read if exceeded. |\n| `buff` | Variable-length raw `buffer`. |\n\n### Roblox types\n\n| Codec | Bytes |\n|:---|---:|\n| `vec2` / `vec3` | 8 / 12 |\n| `cframe` | 24 |\n| `color3` | 3 |\n| `inst` | 2 (sidecar ref index) |\n| `udim` / `udim2` | 8 / 16 |\n| `numberRange` | 8 |\n| `rect` | 16 |\n| `ray` | 24 |\n| `vec2int16` / `vec3int16` | 4 / 6 |\n| `region3` / `region3int16` | 24 / 12 |\n| `numberSequence` / `colorSequence` | variable |\n\n### Quantized variants\n\nCall as a function for compression.\n\n| Codec | Bytes | Notes |\n|:---|---:|:---|\n| `vec2(min, max, precision)` | 2 / 4 / 6 / 8 | Per-component, narrowest fitting width. |\n| `vec3(min, max, precision)` | 3 / 6 / 9 / 12 | Per-component, narrowest fitting width. |\n| `cframe()` | 16 | Smallest-three quaternion. ≤ 0.16° rotation error. |\n\n### Composites\n\n| Codec | Notes |\n|:---|:---|\n| `struct({k = c})` | Named fields. Bools auto-bitpacked into a tail block. |\n| `array(c, max?)` | List. Bool arrays bitpacked. Direct path for fixed-size elements. |\n| `map(k, v, max?)` | Key-value pairs; keys sorted at encode for stable wire bytes. |\n| `optional(c)` | 1B presence flag + value. |\n| `tuple(...)` | Positional. All-direct fast path when every element is fixed-size. |\n| `tagged(field, {name = c})` | Discriminated union. 1B tag. Up to 256 variants. |\n\n### Delta — reliable transport only\n\nTracks the previous frame's value and ships only what changed. Rejected on `unreliable = true`.\n\n| Codec | Static | Mutation |\n|:---|:---:|:---:|\n| `deltaStruct(schema)` | 1 B | per-field |\n| `deltaArray(c, max?)` | 1 B | per-changed-index |\n| `deltaMap(k, v, max?)` | 1 B | per-changed-key |\n| `deltaInt(min, max)` | 1 B | 1–5 B |\n| `deltaFloat(min, max, precision)` | 1 B | 1–5 B |\n| `deltaVec3(min, max, precision)` | 3 B | 3–15 B |\n| `deltaCFrame(posMin, posMax, precision)` | 1 B | 4–13 B |\n\n- `deltaArray` element / `deltaMap` key+value cannot themselves contain delta state. Use `deltaStruct` for per-field deltas inside.\n- `deltaVec3` and `deltaCFrame` error on out-of-range components.\n\n### Meta\n\n| Codec | Notes |\n|:---|:---|\n| `enum(...)` | String enum. ≤ 256 variants. 1B u8 index. |\n| `bitfield(schema)` | 1–32 bits total. `{ type = \"bool\" }`, `{ type = \"uint\", width }`, `{ type = \"int\", width }`. |\n| `custom(size, write, read, typeCheck?)` | User-defined fixed-size codec. |\n| `nothing` | 0 bytes; reads `nil`. For fire-and-forget signals. |\n| `unknown` | Bypasses serialization through the channel sidecar. Must be paired with `validate`. |\n| `auto` | Self-describing: nil / bool / numbers / strings / buffers / Roblox datatypes. 1B type tag + payload. |\n\n## Rate limiting\n\nPer-packet, pick one mode:\n\n- Token bucket: `{ maxPerSecond = N, burst = M }`\n- Cooldown: `{ cooldown = seconds }`\n\nGlobal per-player cap: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`.\n\n## Limits\n\n| | |\n|:---|---:|\n| Packet + query IDs (combined) | 127 |\n| Buffer per frame | 1 MB max |\n| In-flight queries | 65,535 |\n| Enum / tagged variants | 256 |\n| Bitfield total bits | 32 |\n| Sidecar refs per frame | 65,535 |\n\n## Benchmarks\n\n`rojo serve bench.project.json` with one server + one client.\n\n### Cross-library — 1000 fires/frame, 10 s\n\n[Blink's methodology](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md): same payload reused every frame, identical entity / bool shapes. Other tools from Blink v0.17.1.\n\n| Tool | `array<entity>[100]` | `array<bool>[1000]` |\n|:---|:---|:---|\n| roblox | 16 fps · 559,364 Kbps | 21 fps · 353,107 Kbps |\n| **lync** | **59 fps · 3.37 Kbps** | **61 fps · 2.45 Kbps** |\n| blink | 42 fps · 41.81 Kbps | 97 fps · 7.91 Kbps |\n| zap | 39 fps · 41.71 Kbps | 52 fps · 8.10 Kbps |\n| bytenet | 32 fps · 41.64 Kbps | 35 fps · 8.11 Kbps |\n\n### Network bandwidth — 100 fires/frame, 8 s\n\n| Workload | Naive Kbps | Optimized | Savings |\n|:---|---:|:---|---:|\n| `array<entity>[100]` random | 3,607 | `deltaArray` 3 of 100 mutated | **154** (–96%) |\n| `array<entity>[100]` reused | 3,607 | XOR baseline (identical frames) | **2.4** (–99.9%) |\n| `array<bool>[1000]` random | 762 | XOR baseline (1 bit flipped) | **20.4** (–97%) |\n| `struct(state)` random | 201 | `deltaStruct` 1 field mutated | **29.0** (–86%) |\n| `map<id, vec3>[200]` 5 keys mutated | 657 | `deltaMap` 5 keys mutated | **393** (–40%) |\n| `array<cframe>[50]` random | 4,585 | — | — |\n| `vec3` walking motion (continuous diff) | — | `deltaVec3` | **19.5** |\n| `CFrame` walking pose (pos + rot) | — | `deltaCFrame` | **41.1** |\n\n## License\n\nMIT\n","readmeTruncated":false}