@axpecter/lync 2.2.1 → 2.3.2
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/README.md +109 -147
- package/package.json +1 -1
- package/src/Types.luau +34 -22
- package/src/api/Group.luau +40 -33
- package/src/api/Packet.luau +140 -66
- package/src/api/Query.luau +103 -65
- package/src/api/Scope.luau +26 -10
- package/src/api/Signal.luau +43 -52
- package/src/codec/Base.luau +28 -14
- package/src/codec/composite/Array.luau +296 -115
- package/src/codec/composite/Map.luau +377 -57
- package/src/codec/composite/Optional.luau +10 -2
- package/src/codec/composite/Shared.luau +134 -64
- package/src/codec/composite/Struct.luau +294 -43
- package/src/codec/composite/Tagged.luau +21 -16
- package/src/codec/composite/Tuple.luau +32 -15
- package/src/codec/datatype/Buffer.luau +7 -7
- package/src/codec/datatype/CFrame.luau +34 -122
- package/src/codec/datatype/Color.luau +10 -10
- package/src/codec/datatype/Instance.luau +15 -12
- package/src/codec/datatype/IntVector.luau +2 -2
- package/src/codec/datatype/NumberRange.luau +15 -8
- package/src/codec/datatype/Ray.luau +13 -14
- package/src/codec/datatype/Rect.luau +12 -12
- package/src/codec/datatype/Region.luau +13 -14
- package/src/codec/datatype/Sequence.luau +87 -65
- package/src/codec/datatype/String.luau +17 -10
- package/src/codec/datatype/UDim.luau +10 -8
- package/src/codec/datatype/Vector.luau +20 -59
- package/src/codec/meta/Auto.luau +94 -126
- package/src/codec/meta/Bitfield.luau +12 -14
- package/src/codec/meta/Custom.luau +3 -1
- package/src/codec/meta/DeltaScalar.luau +390 -0
- package/src/codec/meta/Enum.luau +9 -9
- package/src/codec/meta/Float.luau +6 -28
- package/src/codec/meta/Nothing.luau +1 -1
- package/src/codec/meta/Unknown.luau +10 -7
- package/src/codec/primitive/Bool.luau +6 -4
- package/src/codec/primitive/Float16.luau +5 -2
- package/src/codec/primitive/Int.luau +5 -6
- package/src/codec/primitive/Number.luau +6 -4
- package/src/codec/primitive/Signed.luau +2 -2
- package/src/codec/primitive/Varint.luau +76 -39
- package/src/codec/primitive/Zint.luau +99 -0
- package/src/index.d.ts +207 -53
- package/src/init.luau +116 -103
- package/src/internal/Baseline.luau +9 -1
- package/src/internal/Channel.luau +191 -157
- package/src/internal/Middleware.luau +22 -6
- package/src/internal/Pool.luau +12 -4
- package/src/internal/Registry.luau +25 -16
- package/src/internal/Transport.luau +1 -1
- package/src/transport/Bridge.luau +47 -33
- package/src/transport/Client.luau +25 -21
- package/src/transport/Gate.luau +227 -172
- package/src/transport/Reader.luau +174 -106
- package/src/transport/Server.luau +46 -57
- package/src/util/Array.luau +18 -0
- package/src/util/Buffer.luau +90 -0
- package/src/util/Constants.luau +30 -0
- package/src/util/Log.luau +68 -0
- package/src/util/Player.luau +14 -0
- package/src/util/Quantize.luau +84 -0
- package/src/util/Quat.luau +124 -0
- package/src/internal/Util.luau +0 -26
package/README.md
CHANGED
|
@@ -9,18 +9,17 @@
|
|
|
9
9
|
<a href="#benchmarks">Benchmarks</a>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
|
|
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
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Wally — add to your `wally.toml`:
|
|
17
17
|
|
|
18
18
|
```toml
|
|
19
|
-
|
|
20
|
-
Lync = "axp3cter/lync@2.2.1"
|
|
19
|
+
Lync = "axp3cter/lync@2.3.2"
|
|
21
20
|
```
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
npm (roblox-ts):
|
|
24
23
|
|
|
25
24
|
```bash
|
|
26
25
|
npm install @axpecter/lync
|
|
@@ -30,10 +29,7 @@ npm install @axpecter/lync
|
|
|
30
29
|
import Lync from "@axpecter/lync";
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
> [!IMPORTANT]
|
|
36
|
-
> Define all packets, queries, and groups before calling `Lync.start()`.
|
|
32
|
+
**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.
|
|
37
33
|
|
|
38
34
|
## Example
|
|
39
35
|
|
|
@@ -72,7 +68,7 @@ local Players = game:GetService("Players")
|
|
|
72
68
|
local alive = Lync.group("alive")
|
|
73
69
|
Players.PlayerAdded:Connect(function(p) alive:add(p) end)
|
|
74
70
|
|
|
75
|
-
Net.Hit:on(function(data, sender)
|
|
71
|
+
Net.Hit:on(function(data, sender) end)
|
|
76
72
|
Net.Ping:handle(function() return os.clock() end)
|
|
77
73
|
|
|
78
74
|
Lync.start()
|
|
@@ -91,7 +87,7 @@ local Net = require(game.ReplicatedStorage.Net)
|
|
|
91
87
|
Lync.start()
|
|
92
88
|
|
|
93
89
|
local scope = Lync.scope()
|
|
94
|
-
scope:on(Net.State, function(state)
|
|
90
|
+
scope:on(Net.State, function(state) end)
|
|
95
91
|
|
|
96
92
|
Net.Hit:send({ targetId = 123, damage = 45 })
|
|
97
93
|
local serverTime = Net.Ping:request(nil)
|
|
@@ -103,18 +99,19 @@ local serverTime = Net.Ping:request(nil)
|
|
|
103
99
|
|
|
104
100
|
| Function | Description |
|
|
105
101
|
|:---|:---|
|
|
106
|
-
| `Lync.configure(opts)` |
|
|
102
|
+
| `Lync.configure(opts)` | Apply options. Must precede `start()`. |
|
|
107
103
|
| `Lync.start()` | Initialize transport. Call once. |
|
|
108
104
|
| `Lync.isStarted()` | `true` after `start()`. |
|
|
109
105
|
| `Lync.flush()` | Force an immediate send. |
|
|
110
|
-
| `Lync.flushRate(hz)` | 1–60. Default 60. |
|
|
106
|
+
| `Lync.flushRate(hz)` | 1–60 Hz. Default 60. |
|
|
107
|
+
| `Lync.reset()` | Restore module state to post-require defaults. For tests / hot reload. |
|
|
111
108
|
|
|
112
109
|
### Configure options
|
|
113
110
|
|
|
114
111
|
| Option | Default | Range | Description |
|
|
115
112
|
|:---|---:|:---|:---|
|
|
116
|
-
| `channelMaxSize` | 262144 | 4 KB – 1 MB |
|
|
117
|
-
| `validationDepth` | 16 | 4–32 |
|
|
113
|
+
| `channelMaxSize` | 262144 | 4 KB – 1 MB | Per-frame buffer cap. |
|
|
114
|
+
| `validationDepth` | 16 | 4–32 | Schema-walk recursion limit. |
|
|
118
115
|
| `poolSize` | 16 | 2–128 | Reusable channel-state pool. |
|
|
119
116
|
| `bandwidthLimit` | none | — | `{ softLimit, maxStrikes }` per-player throttle. |
|
|
120
117
|
| `globalRateLimit` | none | — | `{ maxPerSecond }` across all packets per player. |
|
|
@@ -128,44 +125,44 @@ local serverTime = Net.Ping:request(nil)
|
|
|
128
125
|
-- Server
|
|
129
126
|
packet:send(data, player)
|
|
130
127
|
packet:send(data, Lync.all)
|
|
131
|
-
packet:send(data, Lync.except(p1,
|
|
128
|
+
packet:send(data, Lync.except(p1, group1))
|
|
132
129
|
packet:send(data, { p1, p2, p3 })
|
|
133
130
|
packet:send(data, group)
|
|
134
131
|
|
|
135
132
|
-- Client
|
|
136
133
|
packet:send(data)
|
|
137
134
|
|
|
138
|
-
-- Both
|
|
139
|
-
packet:on(function(data, sender, timestamp
|
|
135
|
+
-- Both sides
|
|
136
|
+
local conn = packet:on(function(data, sender, timestamp) end)
|
|
140
137
|
packet:once(fn)
|
|
141
|
-
|
|
138
|
+
local data, sender, timestamp = packet:wait()
|
|
142
139
|
packet:name()
|
|
143
|
-
packet:stats()
|
|
140
|
+
packet:stats() -- requires stats=true
|
|
144
141
|
```
|
|
145
142
|
|
|
146
143
|
| Option | Type | Description |
|
|
147
144
|
|:---|:---|:---|
|
|
148
|
-
| `unreliable` | boolean | Use `UnreliableRemoteEvent`.
|
|
149
|
-
| `rateLimit` | `RateLimitConfig` | Server-side per-player
|
|
150
|
-
| `validate` | `(data, player) → (bool, string?)` | Drop on `false`. |
|
|
151
|
-
| `maxPayloadBytes` | number | Reject oversize incoming payloads. |
|
|
152
|
-
| `timestamp` | `"frame"
|
|
145
|
+
| `unreliable` | boolean | Use `UnreliableRemoteEvent`. Rejected with delta codecs (a dropped frame would desync the baseline). |
|
|
146
|
+
| `rateLimit` | `RateLimitConfig` | Server-side per-player. |
|
|
147
|
+
| `validate` | `(data, player) → (bool, string?)` | Drop on `false`. Reason is forwarded to `onDrop`. |
|
|
148
|
+
| `maxPayloadBytes` | number | Reject oversize incoming payloads early. |
|
|
149
|
+
| `timestamp` | `"frame"`, `"offset"`, `"full"` | Append 1B / 2B / 8B timestamp. Read as the third arg. |
|
|
153
150
|
|
|
154
151
|
### Queries
|
|
155
152
|
|
|
156
153
|
`Lync.query(name, requestCodec, responseCodec, options?)`
|
|
157
154
|
|
|
158
|
-
Request-response on top of two
|
|
155
|
+
Request-response on top of two paired registrations. Single-target requests yield until reply or timeout; multi-target requests gather a partial map.
|
|
159
156
|
|
|
160
157
|
```luau
|
|
161
158
|
-- Server
|
|
162
159
|
query:handle(function(data, player) return response end)
|
|
163
|
-
query:request(data, player)
|
|
164
|
-
query:request(data,
|
|
160
|
+
local resp = query:request(data, player) -- response?
|
|
161
|
+
local map = query:request(data, group) -- { [Player]: response? }
|
|
165
162
|
|
|
166
163
|
-- Client
|
|
167
164
|
query:handle(function(data) return response end)
|
|
168
|
-
query:request(data)
|
|
165
|
+
local resp = query:request(data) -- yields; nil on timeout
|
|
169
166
|
```
|
|
170
167
|
|
|
171
168
|
| Option | Default | Description |
|
|
@@ -180,14 +177,14 @@ query:request(data) -- yields, → response? (nil on timeout
|
|
|
180
177
|
|
|
181
178
|
| Method | Returns | Description |
|
|
182
179
|
|:---|:---|:---|
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
|
|
|
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. |
|
|
187
184
|
|
|
188
185
|
### Scope
|
|
189
186
|
|
|
190
|
-
`Lync.scope()` — batches connections for
|
|
187
|
+
`Lync.scope()` — batches connections for a single `:destroy()`.
|
|
191
188
|
|
|
192
189
|
```luau
|
|
193
190
|
local scope = Lync.scope()
|
|
@@ -205,43 +202,46 @@ Server-side `:send` second arg.
|
|
|
205
202
|
|:---|:---|
|
|
206
203
|
| `Player` | One player. |
|
|
207
204
|
| `Lync.all` | All connected. |
|
|
208
|
-
| `Lync.except(...)` | Everyone except given
|
|
205
|
+
| `Lync.except(...)` | Everyone except given Players or Groups. |
|
|
209
206
|
| `{ p1, p2 }` | Array of players. |
|
|
210
207
|
| `group` | All members. |
|
|
211
208
|
|
|
212
209
|
### Middleware
|
|
213
210
|
|
|
214
211
|
```luau
|
|
215
|
-
|
|
212
|
+
-- Return Lync.DROP from onSend to discard a packet.
|
|
213
|
+
Lync.onSend(function(data, name, player) return data end)
|
|
216
214
|
Lync.onReceive(function(data, name, player) return data end)
|
|
217
215
|
Lync.onDrop(function(player, reason, name, data) end)
|
|
218
216
|
```
|
|
219
217
|
|
|
220
|
-
All return a `Connection`.
|
|
218
|
+
All return a `Connection`. A throwing hook surfaces to the caller and aborts the chain at that point.
|
|
221
219
|
|
|
222
220
|
### Connection
|
|
223
221
|
|
|
224
222
|
| | |
|
|
225
223
|
|:---|:---|
|
|
226
|
-
| `c.connected` | boolean |
|
|
224
|
+
| `c.connected` | `boolean` |
|
|
227
225
|
| `c:disconnect()` | Idempotent. |
|
|
228
226
|
|
|
229
227
|
### Stats
|
|
230
228
|
|
|
231
|
-
`Lync.configure({ stats = true })`.
|
|
229
|
+
Enable with `Lync.configure({ stats = true })`.
|
|
232
230
|
|
|
233
231
|
| Function | Description |
|
|
234
232
|
|:---|:---|
|
|
235
233
|
| `Lync.stats.player(p)` | `{ bytesSent, bytesReceived }`. Server only. |
|
|
236
234
|
| `Lync.stats.reset()` | Zero all counters. |
|
|
237
|
-
| `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }
|
|
235
|
+
| `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }`. Aggregated across the request + response registrations on queries. |
|
|
238
236
|
|
|
239
237
|
### Debug
|
|
240
238
|
|
|
241
239
|
| Function | Description |
|
|
242
240
|
|:---|:---|
|
|
243
|
-
| `Lync.debug.pending()` | In-flight query
|
|
244
|
-
| `Lync.debug.registrations()` | Frozen
|
|
241
|
+
| `Lync.debug.pending()` | In-flight query correlation IDs. |
|
|
242
|
+
| `Lync.debug.registrations()` | Frozen `{ name, id, kind, isUnreliable }` per registration. |
|
|
243
|
+
|
|
244
|
+
`capture` / `stop` / `dump` are reserved no-ops for capture/replay tooling.
|
|
245
245
|
|
|
246
246
|
## Codecs
|
|
247
247
|
|
|
@@ -249,9 +249,10 @@ All return a `Connection`.
|
|
|
249
249
|
|
|
250
250
|
| Codec | Bytes | Notes |
|
|
251
251
|
|:---|---:|:---|
|
|
252
|
-
| `int(min, max)` | 1 / 2 / 4 | Picks
|
|
253
|
-
| `
|
|
254
|
-
| `
|
|
252
|
+
| `int(min, max)` | 1 / 2 / 4 | Picks narrowest u8/u16/u32/i8/i16/i32. |
|
|
253
|
+
| `zint(min?, max?)` | 1 – 5 | Variable-length signed via zigzag varint. 1 byte for [-96, 95]. |
|
|
254
|
+
| `f16` / `f32` / `f64` | 2 / 4 / 8 | `f16` ≈ ±65504, ~3 digits. |
|
|
255
|
+
| `float(min, max, precision)` | 1 / 2 / 3 / 4 | Quantized; picks u8 / u16 / u24 / u32 wire form. |
|
|
255
256
|
| `bool` | 1 | Auto-bitpacked inside `struct` and `array`. |
|
|
256
257
|
|
|
257
258
|
### Strings & buffers
|
|
@@ -260,7 +261,7 @@ All return a `Connection`.
|
|
|
260
261
|
|:---|:---|
|
|
261
262
|
| `string` | Variable length. Binary-safe. |
|
|
262
263
|
| `string(maxLength)` | Bounded. Rejects on read if exceeded. |
|
|
263
|
-
| `buff` | Variable-length buffer
|
|
264
|
+
| `buff` | Variable-length raw `buffer`. |
|
|
264
265
|
|
|
265
266
|
### Roblox types
|
|
266
267
|
|
|
@@ -269,7 +270,7 @@ All return a `Connection`.
|
|
|
269
270
|
| `vec2` / `vec3` | 8 / 12 |
|
|
270
271
|
| `cframe` | 24 |
|
|
271
272
|
| `color3` | 3 |
|
|
272
|
-
| `inst` | 2 |
|
|
273
|
+
| `inst` | 2 (sidecar ref index) |
|
|
273
274
|
| `udim` / `udim2` | 8 / 16 |
|
|
274
275
|
| `numberRange` | 8 |
|
|
275
276
|
| `rect` | 16 |
|
|
@@ -284,136 +285,97 @@ Call as a function for compression.
|
|
|
284
285
|
|
|
285
286
|
| Codec | Bytes | Notes |
|
|
286
287
|
|:---|---:|:---|
|
|
287
|
-
| `vec2(min, max, precision)` | 2
|
|
288
|
-
| `vec3(min, max, precision)` | 3
|
|
289
|
-
| `cframe()` | 16 | Smallest-three quaternion. ≤0.16° rotation error. |
|
|
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. |
|
|
290
291
|
|
|
291
292
|
### Composites
|
|
292
293
|
|
|
293
294
|
| Codec | Notes |
|
|
294
295
|
|:---|:---|
|
|
295
|
-
| `struct({k = c})` | Named fields. Bools auto-bitpacked. |
|
|
296
|
-
| `array(c, max?)` | List. Bool arrays bitpacked. |
|
|
297
|
-
| `map(k, v, max?)` | Key-value pairs. |
|
|
298
|
-
| `optional(c)` | 1B
|
|
299
|
-
| `tuple(...)` | Positional. |
|
|
300
|
-
| `tagged(field, {name = c})` | Discriminated union. 1B tag.
|
|
301
|
-
|
|
302
|
-
### Delta — reliable only
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
| Codec |
|
|
307
|
-
|
|
308
|
-
| `deltaStruct(schema)` |
|
|
309
|
-
| `deltaArray(c, max?)` |
|
|
310
|
-
| `deltaMap(k, v, max?)` |
|
|
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. |
|
|
302
|
+
|
|
303
|
+
### Delta — reliable transport only
|
|
304
|
+
|
|
305
|
+
Tracks the previous frame's value and ships only what changed. Rejected on `unreliable = true`.
|
|
306
|
+
|
|
307
|
+
| Codec | Static | Mutation |
|
|
308
|
+
|:---|:---:|:---:|
|
|
309
|
+
| `deltaStruct(schema)` | 1 B | per-field |
|
|
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 |
|
|
316
|
+
|
|
317
|
+
- `deltaArray` element / `deltaMap` key+value cannot themselves contain delta state. Use `deltaStruct` for per-field deltas inside.
|
|
318
|
+
- `deltaVec3` and `deltaCFrame` error on out-of-range components.
|
|
311
319
|
|
|
312
320
|
### Meta
|
|
313
321
|
|
|
314
322
|
| Codec | Notes |
|
|
315
323
|
|:---|:---|
|
|
316
|
-
| `enum(...)` | String enum. ≤256 variants. 1B. |
|
|
317
|
-
| `bitfield(schema)` | 1–32 bits.
|
|
318
|
-
| `custom(size, write, read, typeCheck?)` | User-defined fixed-size. |
|
|
319
|
-
| `nothing` | 0 bytes
|
|
320
|
-
| `unknown` |
|
|
321
|
-
| `auto` | Self-describing
|
|
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. |
|
|
322
330
|
|
|
323
331
|
## Rate limiting
|
|
324
332
|
|
|
325
333
|
Per-packet, pick one mode:
|
|
326
334
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
{ cooldown = seconds } -- cooldown
|
|
330
|
-
```
|
|
335
|
+
- Token bucket: `{ maxPerSecond = N, burst = M }`
|
|
336
|
+
- Cooldown: `{ cooldown = seconds }`
|
|
331
337
|
|
|
332
|
-
Global per-player: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`.
|
|
338
|
+
Global per-player cap: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`.
|
|
333
339
|
|
|
334
340
|
## Limits
|
|
335
341
|
|
|
336
342
|
| | |
|
|
337
343
|
|:---|---:|
|
|
338
|
-
| Packet + query IDs | 127 |
|
|
344
|
+
| Packet + query IDs (combined) | 127 |
|
|
339
345
|
| Buffer per frame | 1 MB max |
|
|
340
|
-
| In-flight queries | 65,
|
|
346
|
+
| In-flight queries | 65,535 |
|
|
341
347
|
| Enum / tagged variants | 256 |
|
|
342
348
|
| Bitfield total bits | 32 |
|
|
349
|
+
| Sidecar refs per frame | 65,535 |
|
|
343
350
|
|
|
344
351
|
## Benchmarks
|
|
345
352
|
|
|
346
353
|
`rojo serve bench.project.json` with one server + one client.
|
|
347
354
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
### Codec throughput
|
|
351
|
-
|
|
352
|
-
| Codec | Encode | Decode | RT/s |
|
|
353
|
-
|:---|---:|---:|---:|
|
|
354
|
-
| `bool` | 43 ns | 28 ns | 14.1 M |
|
|
355
|
-
| `int(0, 255)` | 41 ns | 25 ns | 15.2 M |
|
|
356
|
-
| `int(0, 65535)` | 40 ns | 25 ns | 15.3 M |
|
|
357
|
-
| `f16` | 60 ns | 42 ns | 9.7 M |
|
|
358
|
-
| `f32` | 41 ns | 26 ns | 14.9 M |
|
|
359
|
-
| `f64` | 41 ns | 25 ns | 15.3 M |
|
|
360
|
-
| `string` (10 chars) | 45 ns | 73 ns | 8.4 M |
|
|
361
|
-
| `string` (1000 chars) | 74 ns | 250 ns | 3.1 M |
|
|
362
|
-
| `vec3` | 56 ns | 27 ns | 12.1 M |
|
|
363
|
-
| `vec3` quantized | 121 ns | 85 ns | 4.9 M |
|
|
364
|
-
| `cframe` | 88 ns | 186 ns | 3.6 M |
|
|
365
|
-
| `cframe()` | 118 ns | 214 ns | 3.0 M |
|
|
366
|
-
| entity struct (6 fields) | 234 ns | 476 ns | 1.4 M |
|
|
367
|
-
| 100× entities | 15.3 µs | 34.6 µs | 20 K |
|
|
368
|
-
| 1000× bools (bitpacked) | 4.3 µs | 5.3 µs | 104 K |
|
|
369
|
-
|
|
370
|
-
### Wire sizes
|
|
355
|
+
### Cross-library — 1000 fires/frame, 10 s
|
|
371
356
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
|
375
|
-
|
|
376
|
-
|
|
|
377
|
-
|
|
|
378
|
-
|
|
|
379
|
-
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
> [!NOTE]
|
|
396
|
-
> Lync batches all sends into one buffer per frame and bitpacks bools (1000 = 127 B vs ~1002 B). Delta compression isn't exercised here.
|
|
397
|
-
|
|
398
|
-
**100× struct(6× u8) entities**
|
|
399
|
-
|
|
400
|
-
| Tool | FPS | Kbps |
|
|
401
|
-
|:---|---:|---:|
|
|
402
|
-
| roblox | 16 | 559,364 |
|
|
403
|
-
| **lync** | **60** | **3.47** |
|
|
404
|
-
| blink | 42 | 41.81 |
|
|
405
|
-
| zap | 39 | 41.71 |
|
|
406
|
-
| bytenet | 32 | 41.64 |
|
|
407
|
-
|
|
408
|
-
**1000× bool**
|
|
409
|
-
|
|
410
|
-
| Tool | FPS | Kbps |
|
|
411
|
-
|:---|---:|---:|
|
|
412
|
-
| roblox | 21 | 353,107 |
|
|
413
|
-
| **lync** | **60** | **2.33** |
|
|
414
|
-
| blink | 97 | 7.91 |
|
|
415
|
-
| zap | 52 | 8.10 |
|
|
416
|
-
| bytenet | 35 | 8.11 |
|
|
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** |
|
|
417
379
|
|
|
418
380
|
## License
|
|
419
381
|
|
package/package.json
CHANGED
package/src/Types.luau
CHANGED
|
@@ -12,26 +12,26 @@ export type ChannelState = {
|
|
|
12
12
|
itemCount: number,
|
|
13
13
|
singleMode: boolean,
|
|
14
14
|
singlePos: number,
|
|
15
|
-
|
|
15
|
+
--[[
|
|
16
|
+
Per-codec write-side delta state, keyed by deltaId. Shape is opaque
|
|
17
|
+
and codec-internal: deltaStruct stores `{raw, len, segOff, segLen}`,
|
|
18
|
+
deltaArray stores `{perIdx, length}`, deltaMap stores `{perKey}`,
|
|
19
|
+
delta scalars store quantized previous values.
|
|
20
|
+
]]
|
|
21
|
+
deltas: { [number]: any },
|
|
16
22
|
prevDump: buffer?,
|
|
17
23
|
prevDumpLen: number,
|
|
18
24
|
currentPacket: string?,
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
export type DeltaCacheEntry = {
|
|
22
|
-
raw: buffer,
|
|
23
|
-
len: number,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
27
|
export type Codec<T> = {
|
|
27
28
|
write: (ch: ChannelState, value: T) -> (),
|
|
28
29
|
read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
--[[
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
(direct write/read fast path).
|
|
33
|
+
Optional metadata used by Gate (validation), Reader (max-item bound),
|
|
34
|
+
Auto (type dispatch), and composite codecs (direct write/read fast path).
|
|
35
35
|
]]
|
|
36
36
|
export type InternalCodec<T> = {
|
|
37
37
|
write: (ch: ChannelState, value: T) -> (),
|
|
@@ -40,32 +40,38 @@ export type InternalCodec<T> = {
|
|
|
40
40
|
_directWrite: ((b: buffer, offset: number, value: T) -> ())?,
|
|
41
41
|
_directRead: ((b: buffer, offset: number) -> T)?,
|
|
42
42
|
_isDelta: boolean?,
|
|
43
|
+
--[[
|
|
44
|
+
True if any nested codec is delta. Lets the broadcast hot path skip
|
|
45
|
+
encode-once when delta state would diverge per-receiver.
|
|
46
|
+
]]
|
|
47
|
+
_hasDelta: boolean?,
|
|
43
48
|
_isBool: boolean?,
|
|
44
49
|
_hasUnknown: boolean?,
|
|
45
50
|
_min: number?,
|
|
46
51
|
_max: number?,
|
|
47
52
|
_isInteger: boolean?,
|
|
48
|
-
_schema: { [string]: InternalCodec<
|
|
53
|
+
_schema: { [string]: InternalCodec<T> }?,
|
|
49
54
|
_typeCheck: string?,
|
|
50
55
|
--[[
|
|
51
|
-
Composite
|
|
52
|
-
|
|
56
|
+
Composite metadata: lets Gate recurse element/variant validation
|
|
57
|
+
instead of falling through to the generic NaN/inf scan.
|
|
53
58
|
]]
|
|
54
59
|
_isOptional: boolean?,
|
|
55
|
-
_inner: InternalCodec<
|
|
60
|
+
_inner: InternalCodec<T>?,
|
|
56
61
|
_isTagged: boolean?,
|
|
57
|
-
_variants: { [string]: InternalCodec<
|
|
62
|
+
_variants: { [string]: InternalCodec<T> }?,
|
|
58
63
|
_tagField: string?,
|
|
59
64
|
_isArray: boolean?,
|
|
60
|
-
_element: InternalCodec<
|
|
65
|
+
_element: InternalCodec<T>?,
|
|
61
66
|
_maxCount: number?,
|
|
62
67
|
_isMap: boolean?,
|
|
63
|
-
_keyCodec: InternalCodec<
|
|
64
|
-
_valueCodec: InternalCodec<
|
|
68
|
+
_keyCodec: InternalCodec<T>?,
|
|
69
|
+
_valueCodec: InternalCodec<T>?,
|
|
65
70
|
_isTuple: boolean?,
|
|
66
|
-
_elements: { InternalCodec<
|
|
71
|
+
_elements: { InternalCodec<T> }?,
|
|
67
72
|
_isEnum: boolean?,
|
|
68
73
|
_enumValues: { [string]: number }?,
|
|
74
|
+
_maxStringLength: number?,
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
export type RateLimitState = {
|
|
@@ -76,7 +82,7 @@ export type RateLimitState = {
|
|
|
76
82
|
|
|
77
83
|
--[[
|
|
78
84
|
Token-bucket (maxPerSecond + optional burst) or cooldown (cooldown
|
|
79
|
-
seconds between accepts). Mixing both forms is undefined.
|
|
85
|
+
seconds between accepts). Mixing both forms is undefined behavior.
|
|
80
86
|
]]
|
|
81
87
|
export type RateLimitConfig = {
|
|
82
88
|
maxPerSecond: number?,
|
|
@@ -84,6 +90,10 @@ export type RateLimitConfig = {
|
|
|
84
90
|
cooldown: number?,
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
--[[
|
|
94
|
+
User-supplied per-packet validator. Returns (true, nil) on accept;
|
|
95
|
+
(false, reason) on reject — the reason is forwarded to onDrop hooks.
|
|
96
|
+
]]
|
|
87
97
|
export type ValidateFn = (data: any, player: Player) -> (boolean, string?)
|
|
88
98
|
|
|
89
99
|
export type PacketOptions = {
|
|
@@ -106,6 +116,7 @@ export type Registration = {
|
|
|
106
116
|
kind: number,
|
|
107
117
|
codec: InternalCodec<any>,
|
|
108
118
|
isUnreliable: boolean,
|
|
119
|
+
-- Response registration id; set on REQUEST regs only.
|
|
109
120
|
partner: number?,
|
|
110
121
|
signal: SignalLike,
|
|
111
122
|
rateLimit: RateLimitConfig?,
|
|
@@ -160,9 +171,10 @@ export type PlayerStats = {
|
|
|
160
171
|
}
|
|
161
172
|
|
|
162
173
|
--[[
|
|
163
|
-
Server keys baselines per Player; client uses `false` since booleans
|
|
164
|
-
|
|
174
|
+
Server keys baselines per Player; client uses `false` since booleans are
|
|
175
|
+
valid table keys but Players never collide with that.
|
|
165
176
|
]]
|
|
166
177
|
export type BaselineKey = Player | false
|
|
167
178
|
|
|
168
|
-
|
|
179
|
+
-- Type-only module: runtime table is empty so re-requires are cheap.
|
|
180
|
+
return table.freeze({})
|