@axpecter/lync 2.3.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +302 -299
- package/Types.luau +215 -0
- package/api/Define.luau +154 -0
- package/api/Lifecycle.luau +119 -0
- package/api/Packet.luau +108 -0
- package/api/Query.luau +149 -0
- package/api/Set.luau +189 -0
- package/channel/Blocks.luau +124 -0
- package/channel/Group.luau +118 -0
- package/channel/Link.luau +135 -0
- package/channel/Outbox.luau +261 -0
- package/channel/Recipients.luau +129 -0
- package/channel/Transport.luau +138 -0
- package/codec/Base.luau +236 -0
- package/codec/Composites.luau +128 -0
- package/codec/Datatypes.luau +162 -0
- package/codec/Legality.luau +61 -0
- package/codec/Scalars.luau +72 -0
- package/codec/Text.luau +89 -0
- package/codec/init.luau +39 -0
- package/index.d.ts +285 -0
- package/init.luau +91 -0
- package/kernel/Bits.luau +16 -0
- package/kernel/Catalog.luau +121 -0
- package/kernel/Heap.luau +80 -0
- package/kernel/Listeners.luau +95 -0
- package/kernel/Log.luau +197 -0
- package/kernel/Tally.luau +128 -0
- package/kernel/Threads.luau +44 -0
- package/kernel/Wide.luau +267 -0
- package/package.json +15 -32
- package/runtime/Boundary.luau +180 -0
- package/runtime/Dispatch.luau +91 -0
- package/runtime/Handles.luau +114 -0
- package/runtime/Handshake.luau +90 -0
- package/runtime/Pending.luau +196 -0
- package/runtime/Router.luau +379 -0
- package/runtime/Session.luau +731 -0
- package/shape/Compile.luau +340 -0
- package/shape/Folds.luau +332 -0
- package/shape/Frames.luau +170 -0
- package/shape/Grid.luau +169 -0
- package/shape/Ir.luau +540 -0
- package/shape/Joins.luau +125 -0
- package/shape/Legality.luau +273 -0
- package/shape/Lower.luau +419 -0
- package/shape/Order.luau +64 -0
- package/shape/Regions.luau +113 -0
- package/shape/Rotation.luau +472 -0
- package/shape/Serial.luau +258 -0
- package/shape/Words.luau +89 -0
- package/shape/emit/Block.luau +453 -0
- package/shape/emit/Column.luau +338 -0
- package/shape/emit/Leaf.luau +462 -0
- package/shape/emit/Word.luau +234 -0
- package/shape/emit/init.luau +222 -0
- package/shape/init.luau +78 -0
- package/state/Apply.luau +330 -0
- package/state/Audience.luau +126 -0
- package/state/Catchup.luau +218 -0
- package/state/Coalesce.luau +43 -0
- package/state/Fieldset.luau +177 -0
- package/state/Newest.luau +339 -0
- package/state/Plan.luau +441 -0
- package/state/Sections.luau +468 -0
- package/state/Store.luau +188 -0
- package/state/Sync.luau +762 -0
- package/wire/Bytes.luau +118 -0
- package/wire/Clock.luau +37 -0
- package/wire/Protocol.luau +110 -0
- package/wire/Sequence.luau +40 -0
- package/wire/Varint.luau +124 -0
- package/src/Types.luau +0 -180
- package/src/api/Group.luau +0 -156
- package/src/api/Packet.luau +0 -319
- package/src/api/Query.luau +0 -338
- package/src/api/Scope.luau +0 -90
- package/src/api/Signal.luau +0 -188
- package/src/codec/Base.luau +0 -136
- package/src/codec/composite/Array.luau +0 -456
- package/src/codec/composite/Map.luau +0 -469
- package/src/codec/composite/Optional.luau +0 -54
- package/src/codec/composite/Shared.luau +0 -291
- package/src/codec/composite/Struct.luau +0 -494
- package/src/codec/composite/Tagged.luau +0 -103
- package/src/codec/composite/Tuple.luau +0 -143
- package/src/codec/datatype/Buffer.luau +0 -66
- package/src/codec/datatype/CFrame.luau +0 -109
- package/src/codec/datatype/Color.luau +0 -32
- package/src/codec/datatype/Instance.luau +0 -52
- package/src/codec/datatype/IntVector.luau +0 -39
- package/src/codec/datatype/NumberRange.luau +0 -39
- package/src/codec/datatype/Ray.luau +0 -36
- package/src/codec/datatype/Rect.luau +0 -31
- package/src/codec/datatype/Region.luau +0 -62
- package/src/codec/datatype/Sequence.luau +0 -184
- package/src/codec/datatype/String.luau +0 -94
- package/src/codec/datatype/UDim.luau +0 -46
- package/src/codec/datatype/Vector.luau +0 -130
- package/src/codec/meta/Auto.luau +0 -318
- package/src/codec/meta/Bitfield.luau +0 -147
- package/src/codec/meta/Custom.luau +0 -33
- package/src/codec/meta/DeltaScalar.luau +0 -390
- package/src/codec/meta/Enum.luau +0 -50
- package/src/codec/meta/Float.luau +0 -38
- package/src/codec/meta/Nothing.luau +0 -13
- package/src/codec/meta/Unknown.luau +0 -41
- package/src/codec/primitive/Bool.luau +0 -26
- package/src/codec/primitive/Float16.luau +0 -99
- package/src/codec/primitive/Int.luau +0 -59
- package/src/codec/primitive/Number.luau +0 -24
- package/src/codec/primitive/Signed.luau +0 -32
- package/src/codec/primitive/Varint.luau +0 -165
- package/src/codec/primitive/Zint.luau +0 -99
- package/src/index.d.ts +0 -423
- package/src/init.luau +0 -362
- package/src/internal/Baseline.luau +0 -57
- package/src/internal/Channel.luau +0 -390
- package/src/internal/Middleware.luau +0 -100
- package/src/internal/Pool.luau +0 -65
- package/src/internal/Registry.luau +0 -119
- package/src/internal/Transport.luau +0 -28
- package/src/transport/Bridge.luau +0 -110
- package/src/transport/Client.luau +0 -97
- package/src/transport/Gate.luau +0 -488
- package/src/transport/Reader.luau +0 -331
- package/src/transport/Server.luau +0 -211
- package/src/util/Array.luau +0 -18
- package/src/util/Buffer.luau +0 -90
- package/src/util/Constants.luau +0 -30
- package/src/util/Log.luau +0 -68
- package/src/util/Player.luau +0 -14
- package/src/util/Quantize.luau +0 -84
- package/src/util/Quat.luau +0 -124
package/README.md
CHANGED
|
@@ -1,381 +1,384 @@
|
|
|
1
1
|
<h1 align="center">Lync</h1>
|
|
2
|
-
<p align="center">Buffer networking for Roblox.</p>
|
|
3
|
-
<p align="center">
|
|
4
|
-
<a href="https://github.com/Axp3cter/Lync/releases/latest">Releases</a> ·
|
|
5
|
-
<a href="#install">Install</a> ·
|
|
6
|
-
<a href="#example">Example</a> ·
|
|
7
|
-
<a href="#api">API</a> ·
|
|
8
|
-
<a href="#codecs">Codecs</a> ·
|
|
9
|
-
<a href="#benchmarks">Benchmarks</a>
|
|
10
|
-
</p>
|
|
11
|
-
|
|
12
|
-
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.
|
|
13
2
|
|
|
14
|
-
|
|
3
|
+
<p align="center">Typed buffer networking for Roblox: packets, queries, and replicated sets.</p>
|
|
15
4
|
|
|
16
|
-
|
|
5
|
+
Write the schema once. Both sides require it, the Luau types fall out of it, and every value packs
|
|
6
|
+
into bit level buffers that batch into one frame per client on flush.
|
|
17
7
|
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
```lua
|
|
9
|
+
-- Net.luau, required by both sides
|
|
10
|
+
return Lync.define("arena", {
|
|
11
|
+
Fighters = Lync.replicate(Lync.struct({
|
|
12
|
+
name = Lync.str(1, 20),
|
|
13
|
+
team = Lync.enum({ "red", "blue" }),
|
|
14
|
+
score = Lync.int(0, 1000000):monotonic(),
|
|
15
|
+
pos = Lync.vec3(Lync.quant(-512, 512, 0.1)):newest(10),
|
|
16
|
+
})):keyBy("team"),
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
Strike = Lync.packet(Lync.vec3(Lync.quant(-512, 512, 0.1))):unreliable(),
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
Sell = Lync.query(
|
|
21
|
+
Lync.struct({ item = Lync.enum({ "sword", "shield" }) }),
|
|
22
|
+
Lync.struct({ earned = Lync.int(0, 1000000) })
|
|
23
|
+
),
|
|
24
|
+
})
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
```
|
|
29
|
-
|
|
27
|
+
```lua
|
|
28
|
+
-- server
|
|
29
|
+
Net.Strike:onServer(function(at, player) end)
|
|
30
|
+
Net.Sell:onServer(function(order, player) return { earned = 25 } end)
|
|
31
|
+
Net.Fighters:add(player.UserId, { name = "Ada", team = "red", score = 0, pos = Vector3.zero })
|
|
32
|
+
|
|
33
|
+
Lync.start()
|
|
34
|
+
RunService.PostSimulation:Connect(Lync.flush)
|
|
30
35
|
```
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
```lua
|
|
38
|
+
-- client
|
|
39
|
+
Net.Fighters:onChanged(function(id, record, old) board(id, record.score) end)
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
Lync.start()
|
|
42
|
+
RunService.PostSimulation:Connect(Lync.flush)
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
Net.Strike:fireServer(aim())
|
|
45
|
+
local ok, receipt = Net.Sell:request({ item = "sword" })
|
|
46
|
+
```
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
`record.score` is a `number` and `receipt.earned` is a `number`. Nothing was annotated and nothing
|
|
49
|
+
was generated.
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
State = Lync.packet("State", Lync.deltaStruct({
|
|
43
|
-
position = Lync.vec3,
|
|
44
|
-
health = Lync.float(0, 100, 0.5),
|
|
45
|
-
status = Lync.enum("idle", "moving", "attacking", "dead"),
|
|
46
|
-
alive = Lync.bool,
|
|
47
|
-
})),
|
|
51
|
+
---
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
targetId = Lync.int(0, 65535),
|
|
51
|
-
damage = Lync.float(0, 200, 0.1),
|
|
52
|
-
}), {
|
|
53
|
-
rateLimit = { maxPerSecond = 30, burst = 5 },
|
|
54
|
-
validate = function(data) return data.damage <= 200, "damage" end,
|
|
55
|
-
}),
|
|
53
|
+
## Install
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
```toml
|
|
56
|
+
[dependencies]
|
|
57
|
+
Lync = "axp3cter/lync@3.0.0"
|
|
59
58
|
```
|
|
60
59
|
|
|
61
|
-
|
|
60
|
+
```bash
|
|
61
|
+
npm install @axpecter/lync
|
|
62
|
+
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
local Lync = require(game.ReplicatedStorage.Lync)
|
|
65
|
-
local Net = require(game.ReplicatedStorage.Net)
|
|
66
|
-
local Players = game:GetService("Players")
|
|
64
|
+
Or drop `lync.rbxm` from the latest release into `ReplicatedStorage`.
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
Players.PlayerAdded:Connect(function(p) alive:add(p) end)
|
|
66
|
+
---
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
Net.Ping:handle(function() return os.clock() end)
|
|
68
|
+
## Primitives
|
|
73
69
|
|
|
74
|
-
|
|
70
|
+
| | Carries | Send | Receive |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| `packet` | events | `fireServer` `fireClient` | `onServer` `onClient` |
|
|
73
|
+
| `query` | a request and its reply | `request` | `onServer` `onClient` |
|
|
74
|
+
| `replicate` | server owned records | `add` `update` `remove` `clear` | `onAdded` `onChanged` `onRemoved` |
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Codecs
|
|
79
|
+
|
|
80
|
+
A codec says how one value validates, encodes and decodes. Out of range throws on the way out and
|
|
81
|
+
drops on the way in.
|
|
82
|
+
|
|
83
|
+
```lua
|
|
84
|
+
local Health = Lync.int(0, 100) -- 7 bits on the wire, where an f32 is 32
|
|
79
85
|
```
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
### Numbers
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
| | Type | |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| `int(min, max)` | `number` | A bounded whole number. |
|
|
92
|
+
| `quant(min, max, step)` | `number` | Rounds onto a grid. |
|
|
93
|
+
| `angle(degrees)` | `number` | Cyclic, wrapping at a whole turn. |
|
|
94
|
+
| `f32()` `f64()` | `number` | Roughly 7 and 15 digits. Reach for `quant` to pick the loss yourself. |
|
|
95
|
+
| `vlq()` `vli()` | `number` | Unbounded integers exact to 2^53, unsigned and signed. |
|
|
96
|
+
| `bool()` | `boolean` | One flag. Reach for `bitfield` past one. |
|
|
97
|
+
|
|
98
|
+
### Text
|
|
99
|
+
|
|
100
|
+
| | Type | |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `str(min, max)` | `string` | Byte length bounded. |
|
|
103
|
+
| `str.alphabet(symbols, min, max)` | `string` | Every character from the set. Smaller set, fewer bits. |
|
|
104
|
+
| `str.alphanum` `.base32` `.base64` `.digits` `.hex` | `string` | Presets over `alphabet`, each `(min, max)`. |
|
|
105
|
+
| `buffer(min, max)` | `buffer` | Opaque bytes. Also how you relay bytes you never open. |
|
|
106
|
+
|
|
107
|
+
### Roblox
|
|
108
|
+
|
|
109
|
+
| | Type | |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `vec2(c?)` `vec3(c?)` | `Vector2` `Vector3` | `f32` per component, or hand each one a codec. |
|
|
112
|
+
| `vec3.unit(degrees)` | `Vector3` | A direction. Any nonzero vector normalizes at encode. |
|
|
113
|
+
| `vec3.bounded(max, step)` | `Vector3` | A direction plus a quantized magnitude. |
|
|
114
|
+
| `cframe(position, rotation)` | `CFrame` | A position codec paired with a rotation codec. |
|
|
115
|
+
| `rotation.none()` `.axis()` `.direction()` `.quat()` | `CFrame` | Zero, one, two, or three degrees of freedom. |
|
|
116
|
+
| `color3()` `.rgb565()` `.palette(t)` | `Color3` | Floats, one 16 bit word, or an index into `t`. |
|
|
117
|
+
| `inst(class?)` | `Instance?` | Always optional. A receiver that cannot see it gets nil. |
|
|
118
|
+
|
|
119
|
+
UDim2, Region3, Ray and the rest go over with `:as`.
|
|
86
120
|
|
|
87
|
-
|
|
121
|
+
### Composites
|
|
88
122
|
|
|
89
|
-
|
|
90
|
-
|
|
123
|
+
| | Type | |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| `struct({ k = c })` | `{ k: ... }` | Named fields. An undeclared field throws on encode. |
|
|
126
|
+
| `array(c, min, max)` | `{ T }` | An ordered list, count bounded. |
|
|
127
|
+
| `map(k, v, min, max)` | `{ [K]: V }` | A dictionary, bounded the same way. |
|
|
128
|
+
| `optional(c)` | `T?` | May be absent. |
|
|
129
|
+
| `tagged(field, { k = c })` | union | One struct per variant, the chosen name rides in `field`. |
|
|
130
|
+
| `enum({ "a", "b" })` | `string` | One of a fixed set. |
|
|
131
|
+
| `bitfield({ "a", "b" })` | `{ a: boolean }` | Packed flags, one bit each. |
|
|
91
132
|
|
|
92
|
-
|
|
93
|
-
local serverTime = Net.Ping:request(nil)
|
|
94
|
-
```
|
|
133
|
+
### Modifiers
|
|
95
134
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|:---|---:|:---|:---|
|
|
113
|
-
| `channelMaxSize` | 262144 | 4 KB – 1 MB | Per-frame buffer cap. |
|
|
114
|
-
| `validationDepth` | 16 | 4–32 | Schema-walk recursion limit. |
|
|
115
|
-
| `poolSize` | 16 | 2–128 | Reusable channel-state pool. |
|
|
116
|
-
| `bandwidthLimit` | none | — | `{ softLimit, maxStrikes }` per-player throttle. |
|
|
117
|
-
| `globalRateLimit` | none | — | `{ maxPerSecond }` across all packets per player. |
|
|
118
|
-
| `stats` | `false` | — | Enables `:stats()` and `Lync.stats.player()`. |
|
|
119
|
-
|
|
120
|
-
### Packets
|
|
121
|
-
|
|
122
|
-
`Lync.packet(name, codec, options?)`
|
|
123
|
-
|
|
124
|
-
```luau
|
|
125
|
-
-- Server
|
|
126
|
-
packet:send(data, player)
|
|
127
|
-
packet:send(data, Lync.all)
|
|
128
|
-
packet:send(data, Lync.except(p1, group1))
|
|
129
|
-
packet:send(data, { p1, p2, p3 })
|
|
130
|
-
packet:send(data, group)
|
|
131
|
-
|
|
132
|
-
-- Client
|
|
133
|
-
packet:send(data)
|
|
134
|
-
|
|
135
|
-
-- Both sides
|
|
136
|
-
local conn = packet:on(function(data, sender, timestamp) end)
|
|
137
|
-
packet:once(fn)
|
|
138
|
-
local data, sender, timestamp = packet:wait()
|
|
139
|
-
packet:name()
|
|
140
|
-
packet:stats() -- requires stats=true
|
|
135
|
+
| | Scope | |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `:validate(fn)` | any | `fn(value, ctx)` returns nil to pass or a reason to drop. |
|
|
138
|
+
| `:as(to, from)` | any | Maps the wire type to and from your own. |
|
|
139
|
+
| `:monotonic()` | set fields | Only climbs, so deltas cost less. A decrease throws. |
|
|
140
|
+
| `:newest(hz?)` | set fields | Only the latest matters, and `hz` caps the rate. |
|
|
141
|
+
|
|
142
|
+
A modifier answers a new codec. A set marker inside a packet or query throws at start.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Packets
|
|
147
|
+
|
|
148
|
+
```lua
|
|
149
|
+
Move = Lync.packet(Lync.vec3()):unreliable(),
|
|
150
|
+
Aim = Lync.packet(Lync.rotation.quat(0.2)):newest(20):timestamped(),
|
|
141
151
|
```
|
|
142
152
|
|
|
143
|
-
|
|
|
144
|
-
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
| `
|
|
149
|
-
| `timestamp` | `"frame"`, `"offset"`, `"full"` | Append 1B / 2B / 8B timestamp. Read as the third arg. |
|
|
153
|
+
| | | |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| server | `fireClient(to, payload)` | Encodes once however many receive it. |
|
|
156
|
+
| server | `onServer(fn)` | `fn(payload, player, sent?)`. Any number of listeners. |
|
|
157
|
+
| client | `fireServer(payload)` | |
|
|
158
|
+
| client | `onClient(fn)` | `fn(payload, sent?)`. Any number of listeners. |
|
|
150
159
|
|
|
151
|
-
|
|
160
|
+
Delivery is reliable and ordered by default. The first two below are alternatives, and
|
|
161
|
+
`:timestamped()` stacks with either.
|
|
152
162
|
|
|
153
|
-
|
|
163
|
+
| | |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| `:unreliable()` | Lossy and unordered, and every fire still goes out. Must fit the unreliable cap. |
|
|
166
|
+
| `:newest(hz?)` | Lossy, latest wins. Stale arrivals and unchanged values send nothing. |
|
|
167
|
+
| `:timestamped()` | Adds `sent`, an instant on the shared clock. |
|
|
168
|
+
|
|
169
|
+
`to`, and a set's `audience`, take any of these.
|
|
154
170
|
|
|
155
|
-
|
|
171
|
+
| | |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `Lync.all` | Every client. |
|
|
174
|
+
| `Player` | That client. |
|
|
175
|
+
| `{ Player }` | The listed clients. |
|
|
176
|
+
| `Group` | Its members at send time. |
|
|
177
|
+
| `Lync.except(t)` | Everyone but `t`, a player, list, or group. |
|
|
156
178
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
query:handle(function(data, player) return response end)
|
|
160
|
-
local resp = query:request(data, player) -- response?
|
|
161
|
-
local map = query:request(data, group) -- { [Player]: response? }
|
|
179
|
+
Firing at nobody is reported and throws in Studio. Listeners may yield, and one that throws does
|
|
180
|
+
not stop the others.
|
|
162
181
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Queries
|
|
185
|
+
|
|
186
|
+
```lua
|
|
187
|
+
local ok, res, data = Net.Sell:request({ item = "sword" })
|
|
188
|
+
if ok then print(res.earned) else print(res, data.elapsed) end
|
|
166
189
|
```
|
|
167
190
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
|
172
|
-
|
|
|
191
|
+
No ending raises. A reply, a deadline, and a counterparty that never answered all arrive the same
|
|
192
|
+
way.
|
|
193
|
+
|
|
194
|
+
| | | |
|
|
195
|
+
| --- | --- | --- |
|
|
196
|
+
| client | `request(value, timeout?)` | Yields for the reply. Timeout defaults to 10 s. |
|
|
197
|
+
| server | `request(client, value, fn, timeout?)` | `fn(ok, res, data)` on completion. Never yields. |
|
|
198
|
+
| server | `onServer(fn)` | The lone responder. What it returns is the reply. |
|
|
199
|
+
| client | `onClient(fn)` | The lone responder. |
|
|
173
200
|
|
|
174
|
-
|
|
201
|
+
Four outcomes: `timeout`, `unanswered` when the other side registered none, `leave` when the
|
|
202
|
+
counterparty goes, and `shutdown` when `close` runs first. A domain failure like insufficient funds
|
|
203
|
+
is a value in your response codec, not an outcome.
|
|
175
204
|
|
|
176
|
-
|
|
205
|
+
---
|
|
177
206
|
|
|
178
|
-
|
|
179
|
-
|:---|:---|:---|
|
|
180
|
-
| `:add(p)` / `:remove(p)` | `boolean` | `true` if membership changed. |
|
|
181
|
-
| `:has(p)` | `boolean` | |
|
|
182
|
-
| `:count()` | `number` | |
|
|
183
|
-
| `:destroy()` | — | Clear members and free the name. |
|
|
207
|
+
## Sets
|
|
184
208
|
|
|
185
|
-
|
|
209
|
+
```lua
|
|
210
|
+
Net.Fighters:add(id, { name = "Ada", team = "red", score = 0, pos = Vector3.zero })
|
|
211
|
+
Net.Fighters:update(id, { score = 10 }) -- only score is sent
|
|
212
|
+
```
|
|
186
213
|
|
|
187
|
-
|
|
214
|
+
The server owns the set and each client holds exactly the records its audiences allow. Without
|
|
215
|
+
`keyBy` the whole set goes to everyone. `keyBy(field)` splits records by that field's value, and
|
|
216
|
+
`audience(key, to)` gives each key its viewers.
|
|
188
217
|
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
218
|
+
```lua
|
|
219
|
+
Net.Fighters:audience("red", redTeam)
|
|
220
|
+
Net.Fighters:update(id, { team = "blue" }) -- migrates atomically inside this flush
|
|
221
|
+
-- viewers of "red" alone -> onRemoved(id, "removed")
|
|
222
|
+
-- viewers of "blue" alone -> onAdded(id, record)
|
|
223
|
+
-- viewers of both keys -> onChanged(id, record, old)
|
|
195
224
|
```
|
|
196
225
|
|
|
197
|
-
|
|
226
|
+
| Server | |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| `add(id, record)` | Throws if the id is live. |
|
|
229
|
+
| `update(id, fields)` | Only the named fields. `Lync.none` clears an optional one. |
|
|
230
|
+
| `remove(id)` `clear()` | Throws on an absent id. |
|
|
231
|
+
| `audience(key, to)` | Keyed sets only. |
|
|
198
232
|
|
|
199
|
-
|
|
233
|
+
| Both sides | |
|
|
234
|
+
| --- | --- |
|
|
235
|
+
| `get(id)` | The live record or nil. Borrowed, so copy what you keep. |
|
|
236
|
+
| `#set`, iteration | Count and walk the local view. |
|
|
237
|
+
| `onAdded(fn)` | `fn(id, record)` on first sight: an add, a late join, a visibility gain. |
|
|
238
|
+
| `onChanged(fn)` | `fn(id, record, old)`. The net record after a flush against the one before. |
|
|
239
|
+
| `onRemoved(fn)` | `fn(id, cause)` with `"removed"` or `"cleared"`. |
|
|
200
240
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
| `Lync.all` | All connected. |
|
|
205
|
-
| `Lync.except(...)` | Everyone except given Players or Groups. |
|
|
206
|
-
| `{ p1, p2 }` | Array of players. |
|
|
207
|
-
| `group` | All members. |
|
|
241
|
+
Only changed fields go out, and only to viewers. Two updates in one flush ship once. An add and a
|
|
242
|
+
remove in one flush ship nothing. Ids are exact to 2^53, so UserIds work as they are, Studio's
|
|
243
|
+
negative test ids included.
|
|
208
244
|
|
|
209
|
-
|
|
245
|
+
---
|
|
210
246
|
|
|
211
|
-
|
|
212
|
-
-- Return Lync.DROP from onSend to discard a packet.
|
|
213
|
-
Lync.onSend(function(data, name, player) return data end)
|
|
214
|
-
Lync.onReceive(function(data, name, player) return data end)
|
|
215
|
-
Lync.onDrop(function(player, reason, name, data) end)
|
|
216
|
-
```
|
|
247
|
+
## Lifecycle
|
|
217
248
|
|
|
218
|
-
|
|
249
|
+
| | |
|
|
250
|
+
| --- | --- |
|
|
251
|
+
| `Lync.start()` | Seals definitions and responders. Once per side, after every definition. |
|
|
252
|
+
| `Lync.flush()` | Sends everything buffered. Nothing sends without it, and empty flushes are free. |
|
|
253
|
+
| `Lync.close()` | A final flush, resolves outstanding requests as `shutdown`, releases the transports. |
|
|
254
|
+
| `Lync.stats(name)` | A frozen snapshot of a namespace's counters. Monotonic, so two diff into rates. |
|
|
255
|
+
|
|
256
|
+
```lua
|
|
257
|
+
Lync.flush(8192) -- every namespace, 8 KB of state per client
|
|
258
|
+
Lync.flush("arena", 8192) -- one namespace, on its own cadence
|
|
259
|
+
```
|
|
219
260
|
|
|
220
|
-
|
|
261
|
+
The budget throttles state replication only, so packets, requests and responses always send in
|
|
262
|
+
full. Under 1024 throws. Give latency critical traffic its own namespace.
|
|
221
263
|
|
|
222
264
|
| | |
|
|
223
|
-
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
265
|
+
| --- | --- |
|
|
266
|
+
| `Lync.group()` | A mutable membership set that goes anywhere a recipient does. |
|
|
267
|
+
| `add` `remove` `has` | Manage membership. `#group` and iteration cover the set. |
|
|
268
|
+
| `destroy()` | Empties it and releases it. Any later use throws. |
|
|
226
269
|
|
|
227
|
-
|
|
270
|
+
A group thins out as players leave, and audiences store the group itself.
|
|
228
271
|
|
|
229
|
-
|
|
272
|
+
---
|
|
230
273
|
|
|
231
|
-
|
|
232
|
-
|:---|:---|
|
|
233
|
-
| `Lync.stats.player(p)` | `{ bytesSent, bytesReceived }`. Server only. |
|
|
234
|
-
| `Lync.stats.reset()` | Zero all counters. |
|
|
235
|
-
| `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }`. Aggregated across the request + response registrations on queries. |
|
|
274
|
+
## Validation
|
|
236
275
|
|
|
237
|
-
|
|
276
|
+
Inbound checking has two stages, and a rejection at the first is a drop.
|
|
238
277
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
278
|
+
```lua
|
|
279
|
+
Damage = Lync.int(0, 500):validate(function(amount, ctx)
|
|
280
|
+
-- ctx.player, ctx.now, ctx.last
|
|
281
|
+
if ctx.last ~= nil and ctx.now - ctx.last < 0.1 then return "faster than 10 Hz" end
|
|
282
|
+
return nil
|
|
283
|
+
end)
|
|
284
|
+
```
|
|
243
285
|
|
|
244
|
-
`
|
|
286
|
+
`last` is stamped on every arrival, accepted or not, so a flood of junk cannot reset a sender's
|
|
287
|
+
clock. The table is reused, so copy anything you keep. A rejected payload is dropped before your
|
|
288
|
+
code sees it and logged as one warning. A rejected request is answered by nothing, so the requester
|
|
289
|
+
times out.
|
|
245
290
|
|
|
246
|
-
|
|
291
|
+
---
|
|
247
292
|
|
|
248
|
-
|
|
293
|
+
## Errors and logging
|
|
249
294
|
|
|
250
|
-
|
|
|
251
|
-
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
### Roblox types
|
|
267
|
-
|
|
268
|
-
| Codec | Bytes |
|
|
269
|
-
|:---|---:|
|
|
270
|
-
| `vec2` / `vec3` | 8 / 12 |
|
|
271
|
-
| `cframe` | 24 |
|
|
272
|
-
| `color3` | 3 |
|
|
273
|
-
| `inst` | 2 (sidecar ref index) |
|
|
274
|
-
| `udim` / `udim2` | 8 / 16 |
|
|
275
|
-
| `numberRange` | 8 |
|
|
276
|
-
| `rect` | 16 |
|
|
277
|
-
| `ray` | 24 |
|
|
278
|
-
| `vec2int16` / `vec3int16` | 4 / 6 |
|
|
279
|
-
| `region3` / `region3int16` | 24 / 12 |
|
|
280
|
-
| `numberSequence` / `colorSequence` | variable |
|
|
281
|
-
|
|
282
|
-
### Quantized variants
|
|
283
|
-
|
|
284
|
-
Call as a function for compression.
|
|
285
|
-
|
|
286
|
-
| Codec | Bytes | Notes |
|
|
287
|
-
|:---|---:|:---|
|
|
288
|
-
| `vec2(min, max, precision)` | 2 / 4 / 6 / 8 | Per-component, narrowest fitting width. |
|
|
289
|
-
| `vec3(min, max, precision)` | 3 / 6 / 9 / 12 | Per-component, narrowest fitting width. |
|
|
290
|
-
| `cframe()` | 16 | Smallest-three quaternion. ≤ 0.16° rotation error. |
|
|
295
|
+
| | |
|
|
296
|
+
| --- | --- |
|
|
297
|
+
| Programmer error | A call on the wrong side, a second responder, a second `start`. Throws on the spot. |
|
|
298
|
+
| Dropped input | A validate reason, a payload the schema turns away. A warning, never thrown. |
|
|
299
|
+
| Environmental | A listener or responder that throws. Reported with its trace, and never stalls the rest. |
|
|
300
|
+
| Transport | Timeout, a leaver, shutdown. An outcome code handed back, never thrown. |
|
|
301
|
+
|
|
302
|
+
Every `on*` returns a connection with `:disconnect()`. When a player leaves, each pending request
|
|
303
|
+
touching them resolves as `leave`.
|
|
304
|
+
|
|
305
|
+
```lua
|
|
306
|
+
Lync.onLog(function(kind, message, data)
|
|
307
|
+
if data.player ~= nil then flagSuspicious(data.player, data) end
|
|
308
|
+
end)
|
|
309
|
+
```
|
|
291
310
|
|
|
292
|
-
|
|
311
|
+
`kind` is `warn` for something dropped, `error` for a fault contained, or `debug` in Studio only.
|
|
312
|
+
`data` always carries a file and a line. `Lync.console` is the default printer and is itself a
|
|
313
|
+
connection, so disconnect it to format records yourself. Match on `data`, never on the message.
|
|
293
314
|
|
|
294
|
-
|
|
295
|
-
|:---|:---|
|
|
296
|
-
| `struct({k = c})` | Named fields. Bools auto-bitpacked into a tail block. |
|
|
297
|
-
| `array(c, max?)` | List. Bool arrays bitpacked. Direct path for fixed-size elements. |
|
|
298
|
-
| `map(k, v, max?)` | Key-value pairs; keys sorted at encode for stable wire bytes. |
|
|
299
|
-
| `optional(c)` | 1B presence flag + value. |
|
|
300
|
-
| `tuple(...)` | Positional. All-direct fast path when every element is fixed-size. |
|
|
301
|
-
| `tagged(field, {name = c})` | Discriminated union. 1B tag. Up to 256 variants. |
|
|
315
|
+
---
|
|
302
316
|
|
|
303
|
-
|
|
317
|
+
## Types
|
|
304
318
|
|
|
305
|
-
|
|
319
|
+
Handlers, records and replies come out typed from the schema alone.
|
|
306
320
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
| `deltaArray(c, max?)` | 1 B | per-changed-index |
|
|
311
|
-
| `deltaMap(k, v, max?)` | 1 B | per-changed-key |
|
|
312
|
-
| `deltaInt(min, max)` | 1 B | 1–5 B |
|
|
313
|
-
| `deltaFloat(min, max, precision)` | 1 B | 1–5 B |
|
|
314
|
-
| `deltaVec3(min, max, precision)` | 3 B | 3–15 B |
|
|
315
|
-
| `deltaCFrame(posMin, posMax, precision)` | 1 B | 4–13 B |
|
|
321
|
+
```lua
|
|
322
|
+
type Fighter = Types.Infer<typeof(Codec)> -- { name: string, score: number, tag: string? }
|
|
323
|
+
```
|
|
316
324
|
|
|
317
|
-
|
|
318
|
-
|
|
325
|
+
`Infer` reads a codec, `Schema` reads a table of codecs as the record it describes, and `Update`
|
|
326
|
+
types the argument to a set update. All three live in the `Types` module beside `Lync`. Everything
|
|
327
|
+
else is on `Lync` directly, for signatures at module boundaries.
|
|
328
|
+
|
|
329
|
+
```lua
|
|
330
|
+
local codec: Lync.Codec<number>
|
|
331
|
+
local packet: Lync.Packet<Vector3>
|
|
332
|
+
local query: Lync.Query<Order, Receipt>
|
|
333
|
+
local set: Lync.Set<Fighter>
|
|
334
|
+
local group: Lync.Group
|
|
335
|
+
local conn: Lync.Connection
|
|
336
|
+
local to: Lync.Recipient -- Lync.All | Player | { Player } | Group | Lync.Except
|
|
337
|
+
local kind: Lync.LogKind -- "warn" | "error" | "debug"
|
|
338
|
+
local cause: Lync.Cause -- "removed" | "cleared"
|
|
339
|
+
local code: Lync.OutcomeCode -- "timeout" | "unanswered" | "leave" | "shutdown"
|
|
340
|
+
local ctx: Lync.ValidateContext -- player, now, last
|
|
341
|
+
local log: Lync.LogData -- file, line, player?, definition?
|
|
342
|
+
local why: Lync.OutcomeData -- definition, elapsed?
|
|
343
|
+
local done: Lync.Outcome<Receipt> -- a server request's completion callback
|
|
344
|
+
local snap: Lync.Stats -- flushes, sentBytes, receivedBytes, drops, definitions
|
|
345
|
+
local row: Lync.DefinitionStats -- one definition's line in that snapshot
|
|
346
|
+
local clear: Lync.None -- the type of Lync.none
|
|
347
|
+
```
|
|
319
348
|
|
|
320
|
-
###
|
|
349
|
+
### roblox-ts
|
|
321
350
|
|
|
322
|
-
|
|
323
|
-
|:---|:---|
|
|
324
|
-
| `enum(...)` | String enum. ≤ 256 variants. 1B u8 index. |
|
|
325
|
-
| `bitfield(schema)` | 1–32 bits total. `{ type = "bool" }`, `{ type = "uint", width }`, `{ type = "int", width }`. |
|
|
326
|
-
| `custom(size, write, read, typeCheck?)` | User-defined fixed-size codec. |
|
|
327
|
-
| `nothing` | 0 bytes; reads `nil`. For fire-and-forget signals. |
|
|
328
|
-
| `unknown` | Bypasses serialization through the channel sidecar. Must be paired with `validate`. |
|
|
329
|
-
| `auto` | Self-describing: nil / bool / numbers / strings / buffers / Roblox datatypes. 1B type tag + payload. |
|
|
351
|
+
The same surface, with five differences.
|
|
330
352
|
|
|
331
|
-
|
|
353
|
+
| | Luau | roblox-ts |
|
|
354
|
+
| --- | --- | --- |
|
|
355
|
+
| calls | `set:add(id, r)` | `set.add(id, r)` |
|
|
356
|
+
| count | `#set` | `set.size()` |
|
|
357
|
+
| helpers | `Types.Infer<C>` | `Lync.Infer<C>` |
|
|
358
|
+
| instance class | `Lync.inst("Player")` | `Lync.inst<Player>()` |
|
|
359
|
+
| audience key | untyped | typed, so `audience` before `keyBy` is a compile error |
|
|
332
360
|
|
|
333
|
-
|
|
361
|
+
The package is scoped, so add both roots in `tsconfig.json` and both folders in your project file.
|
|
334
362
|
|
|
335
|
-
|
|
336
|
-
|
|
363
|
+
```json
|
|
364
|
+
"typeRoots": ["node_modules/@rbxts", "node_modules/@axpecter"]
|
|
365
|
+
```
|
|
337
366
|
|
|
338
|
-
|
|
367
|
+
---
|
|
339
368
|
|
|
340
369
|
## Limits
|
|
341
370
|
|
|
342
|
-
| | |
|
|
343
|
-
|
|
344
|
-
|
|
|
345
|
-
|
|
|
346
|
-
|
|
|
347
|
-
|
|
|
348
|
-
|
|
|
349
|
-
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
`rojo serve bench.project.json` with one server + one client.
|
|
354
|
-
|
|
355
|
-
### Cross-library — 1000 fires/frame, 10 s
|
|
356
|
-
|
|
357
|
-
[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.
|
|
358
|
-
|
|
359
|
-
| Tool | `array<entity>[100]` | `array<bool>[1000]` |
|
|
360
|
-
|:---|:---|:---|
|
|
361
|
-
| roblox | 16 fps · 559,364 Kbps | 21 fps · 353,107 Kbps |
|
|
362
|
-
| **lync** | **59 fps · 3.37 Kbps** | **61 fps · 2.45 Kbps** |
|
|
363
|
-
| blink | 42 fps · 41.81 Kbps | 97 fps · 7.91 Kbps |
|
|
364
|
-
| zap | 39 fps · 41.71 Kbps | 52 fps · 8.10 Kbps |
|
|
365
|
-
| bytenet | 32 fps · 41.64 Kbps | 35 fps · 8.11 Kbps |
|
|
366
|
-
|
|
367
|
-
### Network bandwidth — 100 fires/frame, 8 s
|
|
368
|
-
|
|
369
|
-
| Workload | Naive Kbps | Optimized | Savings |
|
|
370
|
-
|:---|---:|:---|---:|
|
|
371
|
-
| `array<entity>[100]` random | 3,607 | `deltaArray` 3 of 100 mutated | **154** (–96%) |
|
|
372
|
-
| `array<entity>[100]` reused | 3,607 | XOR baseline (identical frames) | **2.4** (–99.9%) |
|
|
373
|
-
| `array<bool>[1000]` random | 762 | XOR baseline (1 bit flipped) | **20.4** (–97%) |
|
|
374
|
-
| `struct(state)` random | 201 | `deltaStruct` 1 field mutated | **29.0** (–86%) |
|
|
375
|
-
| `map<id, vec3>[200]` 5 keys mutated | 657 | `deltaMap` 5 keys mutated | **393** (–40%) |
|
|
376
|
-
| `array<cframe>[50]` random | 4,585 | — | — |
|
|
377
|
-
| `vec3` walking motion (continuous diff) | — | `deltaVec3` | **19.5** |
|
|
378
|
-
| `CFrame` walking pose (pos + rot) | — | `deltaCFrame` | **41.1** |
|
|
371
|
+
| | | |
|
|
372
|
+
| --- | --- | --- |
|
|
373
|
+
| set fields | 64 | Nest the extras in a `struct`, which counts as one field. |
|
|
374
|
+
| ids and integers | exact to 2^53 | Past it, carry the value as a `str` or a `buffer`. |
|
|
375
|
+
| request timeout | 10 s | Pass a timeout per call. |
|
|
376
|
+
| in flight requests | 32768 per namespace | You are leaking requests, and the cap is a detector. |
|
|
377
|
+
| state budget | 32 KB/s per client | Raise it with a flush budget, or split the traffic. |
|
|
378
|
+
| unreliable schema | just under 1 KB | Drop `:unreliable()`, or narrow the codec. Checked at start. |
|
|
379
|
+
| one frame | 1 MB | A loop that fires and never flushes. The throw names the size. |
|
|
380
|
+
|
|
381
|
+
---
|
|
379
382
|
|
|
380
383
|
## License
|
|
381
384
|
|