@axpecter/lync 1.3.1 → 1.4.1

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.
Files changed (41) hide show
  1. package/README.md +250 -68
  2. package/package.json +1 -1
  3. package/src/Types.luau +13 -2
  4. package/src/api/Group.luau +77 -46
  5. package/src/api/Namespace.luau +13 -6
  6. package/src/api/Packet.luau +52 -30
  7. package/src/api/Query.luau +116 -90
  8. package/src/api/Scope.luau +73 -0
  9. package/src/api/Signal.luau +10 -4
  10. package/src/codec/Base.luau +7 -3
  11. package/src/codec/composite/Array.luau +18 -15
  12. package/src/codec/composite/Map.luau +45 -31
  13. package/src/codec/composite/Optional.luau +3 -2
  14. package/src/codec/composite/Shared.luau +7 -5
  15. package/src/codec/composite/Struct.luau +6 -4
  16. package/src/codec/composite/Tagged.luau +6 -6
  17. package/src/codec/composite/Tuple.luau +9 -7
  18. package/src/codec/datatype/Buffer.luau +3 -2
  19. package/src/codec/datatype/CFrame.luau +2 -1
  20. package/src/codec/datatype/Instance.luau +3 -2
  21. package/src/codec/datatype/Sequence.luau +3 -4
  22. package/src/codec/datatype/String.luau +3 -6
  23. package/src/codec/meta/Auto.luau +77 -72
  24. package/src/codec/meta/Bitfield.luau +5 -4
  25. package/src/codec/meta/Enum.luau +6 -5
  26. package/src/codec/meta/Quantized.luau +5 -4
  27. package/src/codec/meta/Unknown.luau +5 -1
  28. package/src/codec/primitive/Float16.luau +5 -4
  29. package/src/codec/primitive/Varint.luau +2 -2
  30. package/src/index.d.ts +65 -26
  31. package/src/init.luau +60 -42
  32. package/src/internal/Baseline.luau +6 -2
  33. package/src/internal/Channel.luau +11 -6
  34. package/src/internal/Middleware.luau +18 -8
  35. package/src/internal/Pool.luau +5 -5
  36. package/src/internal/Registry.luau +4 -4
  37. package/src/transport/Bridge.luau +4 -3
  38. package/src/transport/Client.luau +4 -3
  39. package/src/transport/Gate.luau +5 -5
  40. package/src/transport/Reader.luau +3 -3
  41. package/src/transport/Server.luau +14 -16
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
  <p align="center">Buffer networking for Roblox. Delta compression, XOR framing, built-in security.</p>
3
3
  <p align="center">
4
4
  <a href="https://github.com/Axp3cter/Lync/releases/latest">Releases</a> ·
5
+ <a href="#example">Example</a> ·
5
6
  <a href="#benchmarks">Benchmarks</a> ·
6
7
  <a href="#limits--configuration">Limits</a>
7
8
  </p>
@@ -12,17 +13,17 @@
12
13
 
13
14
  ```toml
14
15
  [dependencies]
15
- Lync = "axp3cter/lync@1.3.1"
16
+ Lync = "axp3cter/lync@1.4.1"
16
17
  ```
17
18
 
18
19
  **npm (roblox-ts)**
19
20
 
20
21
  ```bash
21
- npm install @rbxts/lync
22
+ npm install @axpecter/lync
22
23
  ```
23
24
 
24
25
  ```typescript
25
- import Lync from "@rbxts/lync";
26
+ import Lync from "@axpecter/lync";
26
27
  ```
27
28
 
28
29
  Or grab the `.rbxm` from [releases](https://github.com/Axp3cter/Lync/releases/latest) and drop it in `ReplicatedStorage`.
@@ -30,13 +31,139 @@ Or grab the `.rbxm` from [releases](https://github.com/Axp3cter/Lync/releases/la
30
31
  > [!IMPORTANT]
31
32
  > Define everything before calling `Lync.start()`. Packets, queries, namespaces, all of it.
32
33
 
34
+ ## Example
35
+
36
+ **Shared**
37
+
38
+ ```luau
39
+ local Lync = require(game.ReplicatedStorage.Lync)
40
+
41
+ local Net = {}
42
+
43
+ Net.State = Lync.definePacket("State", {
44
+ value = Lync.deltaStruct({
45
+ position = Lync.vec3,
46
+ health = Lync.quantizedFloat(0, 100, 0.5),
47
+ shield = Lync.quantizedFloat(0, 100, 0.5),
48
+ status = Lync.enum("idle", "moving", "attacking", "dead"),
49
+ alive = Lync.bool,
50
+ }),
51
+ })
52
+
53
+ Net.Hit = Lync.definePacket("Hit", {
54
+ value = Lync.struct({
55
+ targetId = Lync.u16,
56
+ damage = Lync.quantizedFloat(0, 200, 0.1),
57
+ headshot = Lync.bool,
58
+ }),
59
+ rateLimit = { maxPerSecond = 30, burstAllowance = 5 },
60
+ validate = function(data, player)
61
+ if data.damage > 200 then return false, "damage" end
62
+ return true
63
+ end,
64
+ })
65
+
66
+ Net.Chat = Lync.definePacket("Chat", {
67
+ value = Lync.struct({ msg = Lync.boundedString(200), channel = Lync.u8 }),
68
+ })
69
+
70
+ Net.Ping = Lync.defineQuery("Ping", {
71
+ request = Lync.nothing,
72
+ response = Lync.f64,
73
+ timeout = 3,
74
+ })
75
+
76
+ return table.freeze(Net)
77
+ ```
78
+
79
+ **Server**
80
+
81
+ ```luau
82
+ local Lync = require(game.ReplicatedStorage.Lync)
83
+ local Net = require(game.ReplicatedStorage.Net)
84
+ local Players = game:GetService("Players")
85
+
86
+ local alive = Lync.createGroup("alive")
87
+
88
+ Lync.onSend(function(data, name)
89
+ print("[out]", name)
90
+ return data
91
+ end)
92
+
93
+ Lync.onDrop(function(player, reason, name)
94
+ warn(player.Name, "dropped", name, reason)
95
+ end)
96
+
97
+ Lync.start()
98
+
99
+ Players.PlayerAdded:Connect(function(player)
100
+ alive:add(player)
101
+ end)
102
+
103
+ game:GetService("RunService").Heartbeat:Connect(function()
104
+ Net.State:send({
105
+ position = Vector3.new(0, 5, 0),
106
+ health = 100,
107
+ shield = 50,
108
+ status = "idle",
109
+ alive = true,
110
+ }, alive)
111
+ end)
112
+
113
+ Net.Hit:listen(function(data, player)
114
+ local target = Players:GetPlayerByUserId(data.targetId)
115
+ if not target then return end
116
+
117
+ alive:remove(target)
118
+ Net.Chat:send({ msg = player.Name .. " eliminated " .. target.Name, channel = 0 }, Lync.all)
119
+ Net.State:send({
120
+ position = Vector3.zero,
121
+ health = 0,
122
+ shield = 0,
123
+ status = "dead",
124
+ alive = false,
125
+ }, Lync.except(target))
126
+ end)
127
+
128
+ Net.Ping:listen(function()
129
+ return os.clock()
130
+ end)
131
+ ```
132
+
133
+ **Client**
134
+
135
+ ```luau
136
+ local Lync = require(game.ReplicatedStorage.Lync)
137
+ local Net = require(game.ReplicatedStorage.Net)
138
+
139
+ Lync.start()
140
+
141
+ local scope = Lync.scope()
142
+
143
+ scope:listen(Net.State, function(state)
144
+ local character = game.Players.LocalPlayer.Character
145
+ if not character then return end
146
+ character:PivotTo(CFrame.new(state.position))
147
+ end)
148
+
149
+ scope:listen(Net.Chat, function(data)
150
+ print("[chat]", data.msg)
151
+ end)
152
+
153
+ Net.Hit:send({ targetId = 123, damage = 45.5, headshot = true })
154
+
155
+ local serverTime = Net.Ping:request(nil)
156
+ if serverTime then
157
+ print("server clock:", serverTime)
158
+ end
159
+ ```
160
+
33
161
  ## Lifecycle
34
162
 
35
163
  | | What it does |
36
164
  |:---------|:------------|
37
165
  | `Lync.start()` | Sets up transport. Server creates remotes, client connects. Call once after all definitions. |
38
- | `Lync.version` | `"1.3.1"` |
39
- | `Lync.VERSION` | `"1.3.1"` |
166
+ | `Lync.VERSION` | `"1.4.1"` |
40
167
 
41
168
  ## Packets
42
169
 
@@ -50,29 +177,30 @@ Or grab the `.rbxm` from [releases](https://github.com/Axp3cter/Lync/releases/la
50
177
  | `validate` | `(data, player) → (bool, string?)` | No | Server-side. Return `false, "reason"` to drop. Runs after NaN scan. |
51
178
  | `maxPayloadBytes` | number | No | Server-side. Max bytes a single batch of this packet can consume. Fires `onDrop` with reason `"size"` if exceeded. |
52
179
 
53
- **Server methods:**
180
+ **Server, single `send` with targets:**
54
181
 
55
- | Method | What it does |
56
- |:-------|:------------|
57
- | `packet:sendTo(data, player)` | Send to one player. |
58
- | `packet:sendToAll(data)` | Send to everyone. |
59
- | `packet:sendToAllExcept(data, except)` | Send to everyone except one. |
60
- | `packet:sendToList(data, players)` | Send to a list. |
61
- | `packet:sendToGroup(data, groupName)` | Send to a named group. |
182
+ ```luau
183
+ packet:send(data, player) -- one player
184
+ packet:send(data, Lync.all) -- everyone
185
+ packet:send(data, Lync.except(player)) -- everyone except one
186
+ packet:send(data, Lync.except(p1, p2)) -- everyone except multiple
187
+ packet:send(data, { p1, p2 }) -- list of players
188
+ packet:send(data, group) -- group object
189
+ ```
62
190
 
63
- **Client methods:**
191
+ **Client:**
64
192
 
65
- | Method | What it does |
66
- |:-------|:------------|
67
- | `packet:send(data)` | Send to server. |
193
+ ```luau
194
+ packet:send(data) -- send to server
195
+ ```
68
196
 
69
- **Shared methods:**
197
+ **Shared (both contexts):**
70
198
 
71
199
  | Method | What it does |
72
200
  |:-------|:------------|
73
- | `packet:listen(fn(data, sender))` | Listen for incoming. Returns a Connection. Sender is `Player` on server, `nil` on client. |
74
- | `packet:once(fn(data, sender))` | Same as listen but auto-disconnects after one fire. |
75
- | `packet:wait()` | Yields until next fire. Returns `(data, sender)`. |
201
+ | `packet:listen(fn(data, sender))` | Sender is `Player` on server, `nil` on client. Returns a Connection. |
202
+ | `packet:once(fn(data, sender))` | Auto-disconnects after one fire. |
203
+ | `packet:wait()` | Returns `(data, sender)`. |
76
204
  | `packet:disconnectAll()` | Kills all listeners on this packet. |
77
205
 
78
206
  ## Queries
@@ -90,17 +218,17 @@ Or grab the `.rbxm` from [releases](https://github.com/Axp3cter/Lync/releases/la
90
218
  | Method | Where | What it does |
91
219
  |:-------|:------|:-------------|
92
220
  | `query:listen(fn)` | Both | Register a handler. Server gets `fn(request, player) → response`. Client gets `fn(request) → response`. |
93
- | `query:invoke(request)` | Client | Send request to server, yield until response comes back or timeout. |
94
- | `query:invoke(request, player)` | Server | Send request to a specific client, yield until response or timeout. |
95
- | `query:invokeAll(request)` | Server | Send request to all players, yield until all respond or timeout. Returns `{ [Player]: response? }`. |
96
- | `query:invokeList(request, players)` | Server | Send request to a list of players, yield until all respond or timeout. Returns `{ [Player]: response? }`. |
97
- | `query:invokeGroup(request, groupName)` | Server | Send request to all players in a named group. Returns `{ [Player]: response? }`. |
221
+ | `query:request(data)` | Client | Send request to server, yield until response or timeout. |
222
+ | `query:requestFrom(player, data)` | Server | Send request to a specific client, yield until response or timeout. |
223
+ | `query:requestAll(data)` | Server | Send request to all players. Returns `{ [Player]: response? }`. |
224
+ | `query:requestList(players, data)` | Server | Send request to a list of players. Returns `{ [Player]: response? }`. |
225
+ | `query:requestGroup(group, data)` | Server | Send request to all players in a group. Returns `{ [Player]: response? }`. |
98
226
 
99
227
  ## Namespaces
100
228
 
101
229
  `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.
102
230
 
103
- Access packets and queries by their short name on the returned object: `ns.PacketName`, `ns.QueryName`.
231
+ Access packets and queries by their short name on the returned object: `ns.PacketName`, `ns.QueryName`. Or use the typed sub-tables: `ns.packets.PacketName`, `ns.queries.QueryName`.
104
232
 
105
233
  | Method | What it does |
106
234
  |:-------|:------------|
@@ -111,6 +239,8 @@ Access packets and queries by their short name on the returned object: `ns.Packe
111
239
  | `ns:destroy()` | Kills listeners and removes scoped middleware. Full cleanup. |
112
240
  | `ns:packetNames()` | Sorted list of packet short names. |
113
241
  | `ns:queryNames()` | Sorted list of query short names. |
242
+ | `ns.packets` | Frozen table mapping short name → Packet object. |
243
+ | `ns.queries` | Frozen table mapping short name → Query object. |
114
244
 
115
245
  ## Connection
116
246
 
@@ -118,9 +248,90 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
118
248
 
119
249
  | | What it does |
120
250
  |:-------|:------------|
121
- | `connection.connected` | `true` if still connected, `false` after disconnect. |
251
+ | `connection.connected` | `boolean` |
122
252
  | `connection:disconnect()` | Stops the listener. |
123
253
 
254
+ ## Scope
255
+
256
+ Batches connections for lifecycle-aligned cleanup.
257
+
258
+ ```luau
259
+ local scope = Lync.scope()
260
+
261
+ scope:listen(packetA, fnA)
262
+ scope:listen(packetB, fnB)
263
+ scope:listenAll(namespace, fnC)
264
+
265
+ scope:destroy() -- disconnects everything
266
+ ```
267
+
268
+ | Method | What it does |
269
+ |:-------|:------------|
270
+ | `scope:listen(source, fn)` | Calls `source:listen(fn)` and tracks the connection. |
271
+ | `scope:once(source, fn)` | Calls `source:once(fn)` and tracks the connection. |
272
+ | `scope:listenAll(namespace, fn)` | Calls `namespace:listenAll(fn)` and tracks the connection. |
273
+ | `scope:add(connection)` | Also accepts RBXScriptConnection. |
274
+ | `scope:destroy()` | Safe to call multiple times. |
275
+
276
+ ## Groups
277
+
278
+ Named player sets. Members get removed automatically on `PlayerRemoving`. `Lync.createGroup(name)` returns a Group object.
279
+
280
+ ```luau
281
+ local vips = Lync.createGroup("vips")
282
+
283
+ vips:add(player)
284
+ vips:remove(player)
285
+ vips:has(player)
286
+
287
+ packet:send(data, vips)
288
+ ```
289
+
290
+ | Method | Returns | What it does |
291
+ |:-------|:--------|:-------------|
292
+ | `group:add(player)` | `boolean` | `true` if added, `false` if already in. |
293
+ | `group:remove(player)` | `boolean` | `true` if removed, `false` if wasnt in there. |
294
+ | `group:has(player)` | `boolean` | Whether the player is in the group. |
295
+ | `group:count()` | `number` | Number of members. |
296
+ | `group:getSet()` | `{ [Player]: true }` | Snapshot of the internal set. |
297
+ | `group:forEach(fn)` | `()` | Calls `fn(player)` for each member. |
298
+ | `group:destroy()` | `()` | Removes the group and all memberships. |
299
+
300
+ ## Middleware
301
+
302
+ Global intercept on all packets. Handlers run in the order you registered them. Return `Lync.DROP` from a handler to drop the packet. Return the data to pass it through.
303
+
304
+ ```luau
305
+ Lync.onSend(function(data, name, player)
306
+ if shouldDrop(data) then
307
+ return Lync.DROP
308
+ end
309
+ data.timestamp = os.clock()
310
+ return data
311
+ end)
312
+ ```
313
+
314
+ | Function | What it does |
315
+ |:---------|:------------|
316
+ | `Lync.onSend(fn(data, name, player) → data \| Lync.DROP)` | Runs before a packet goes out. Returns a remover function. |
317
+ | `Lync.onReceive(fn(data, name, player) → data \| Lync.DROP)` | Runs when a packet comes in. Returns a remover function. |
318
+ | `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. |
319
+ | `Lync.DROP` | Frozen sentinel. Return from middleware to drop the packet. |
320
+
321
+ Packets that fail validation are dropped individually. Other packets in the same frame from the same player are unaffected.
322
+
323
+ ## Target Descriptors
324
+
325
+ Used as the second argument to `packet:send()` on the server.
326
+
327
+ | Target | What it does |
328
+ |:-------|:------------|
329
+ | `player` | Send to one player. |
330
+ | `Lync.all` | Send to all connected players. |
331
+ | `Lync.except(player, ...)` | Send to everyone except the specified players. |
332
+ | `{ p1, p2, ... }` | Send to a list of players. |
333
+ | `group` | Send to all members of a Group object. |
334
+
124
335
  ## Types
125
336
 
126
337
  ### Primitives
@@ -138,7 +349,7 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
138
349
  | `Lync.f64` | 8 | IEEE 754 double |
139
350
  | `Lync.bool` | 1 | true/false. Gets packed into bitfields when inside structs. |
140
351
 
141
- ### Complex
352
+ ### Datatypes
142
353
 
143
354
  | Type | Bytes | What it is |
144
355
  |:-----|------:|:-----------|
@@ -160,6 +371,7 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
160
371
  | `Lync.ray` | 24 | Origin Vec3 + Direction Vec3 as 6x f32. |
161
372
  | `Lync.numberSequence` | varint + N×12 | Varint count then (time f32 + value f32 + envelope f32) per keypoint. |
162
373
  | `Lync.colorSequence` | varint + N×7 | Varint count then (time f32 + R u8 + G u8 + B u8) per keypoint. |
374
+ | `Lync.boundedString(maxLength)` | varint + N | Same wire format as `Lync.string` but rejects on read if length exceeds `maxLength`. |
163
375
 
164
376
  ### Composites
165
377
 
@@ -170,7 +382,7 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
170
382
  | `Lync.map(keyCodec, valueCodec, maxCount?)` | Key-value pairs with varint count. Optional `maxCount` rejects on read if exceeded. |
171
383
  | `Lync.optional(codec)` | 1 byte flag, value only if present. |
172
384
  | `Lync.tuple(codec, codec, ...)` | Ordered positional values, no keys. |
173
- | `Lync.boundedString(maxLength)` | Same wire format as `Lync.string` but rejects on read if length exceeds `maxLength`. |
385
+ | `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. |
174
386
 
175
387
  ### Delta
176
388
 
@@ -182,7 +394,7 @@ Reliable only. Lync will error if you try to use these with `unreliable = true`.
182
394
  | `Lync.deltaArray(codec, maxCount?)` | Same idea but for arrays. Dirty elements get sent with varint indices. Optional `maxCount` rejects on read if exceeded. |
183
395
  | `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. |
184
396
 
185
- ### Specialized
397
+ ### Meta
186
398
 
187
399
  | Constructor | What it does |
188
400
  |:------------|:------------|
@@ -190,41 +402,11 @@ Reliable only. Lync will error if you try to use these with `unreliable = true`.
190
402
  | `Lync.quantizedFloat(min, max, precision)` | Fixed-point compression. Picks u8/u16/u32 based on your range and precision. |
191
403
  | `Lync.quantizedVec3(min, max, precision)` | Same thing but for all 3 components. |
192
404
  | `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 }`. |
193
- | `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. |
194
405
  | `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. |
195
406
  | `Lync.nothing` | Zero bytes. Reads nil. Good for fire-and-forget signals. |
196
407
  | `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. |
197
408
  | `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. |
198
409
 
199
- ## Groups
200
-
201
- Named player sets. Members get removed automatically on `PlayerRemoving`.
202
-
203
- | Function | Returns | What it does |
204
- |:---------|:--------|:-------------|
205
- | `Lync.createGroup(name)` | | Makes a new group. Errors if it already exists. |
206
- | `Lync.destroyGroup(name)` | | Removes the group and all memberships. |
207
- | `Lync.addToGroup(name, player)` | `boolean` | `true` if added, `false` if already in. |
208
- | `Lync.removeFromGroup(name, player)` | `boolean` | `true` if removed, `false` if wasnt in there. |
209
- | `Lync.hasInGroup(name, player)` | `boolean` | |
210
- | `Lync.groupCount(name)` | `number` | |
211
- | `Lync.getGroupSet(name)` | `{ [Player]: true }` | |
212
- | `Lync.forEachInGroup(name, fn)` | | Calls `fn(player)` for each member. |
213
-
214
- Send to a group with `packet:sendToGroup(data, groupName)`.
215
-
216
- ## Middleware
217
-
218
- Global intercept on all packets. Handlers run in the order you registered them. Return `nil` from a handler to drop the packet.
219
-
220
- | Function | What it does |
221
- |:---------|:------------|
222
- | `Lync.onSend(fn(data, name, player) → data?)` | Runs before a packet goes out. Returns a remover function. |
223
- | `Lync.onReceive(fn(data, name, player) → data?)` | Runs when a packet comes in. Returns a remover function. |
224
- | `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. |
225
-
226
- Packets that fail validation are dropped individually. Other packets in the same frame from the same player are unaffected.
227
-
228
410
  ## Benchmarks
229
411
 
230
412
  ### Lync Tests
@@ -233,10 +415,10 @@ Packets that fail validation are dropped individually. Other packets in the same
233
415
 
234
416
  | Scenario | Without Lync | With Lync | FPS |
235
417
  |:---------|------------:|---------:|----:|
236
- | Static booleans (1B) | 480 Kbps | **2.45 Kbps** | 59.98 |
237
- | Static entities (34B) | 16,320 Kbps | **2.52 Kbps** | 60.00 |
238
- | Moving entities | 16,320 Kbps | **3.51 Kbps** | 59.99 |
239
- | Chaotic entities | 16,320 Kbps | **4.66 Kbps** | 59.99 |
418
+ | Static booleans (1B) | 480 Kbps | **2.34 Kbps** | 60.00 |
419
+ | Static entities (34B) | 16,320 Kbps | **2.62 Kbps** | 60.00 |
420
+ | Moving entities | 16,320 Kbps | **3.14 Kbps** | 60.00 |
421
+ | Chaotic entities | 16,320 Kbps | **4.76 Kbps** | 59.99 |
240
422
 
241
423
  ### Cross-Library Comparison
242
424
 
@@ -255,7 +437,7 @@ Same data shapes and methodology as [Blink's benchmark suite](https://github.com
255
437
  | Tool (Kbps) | Median | P0 | P80 | P90 | P95 | P100 |
256
438
  |:------------|-------:|---:|----:|----:|----:|-----:|
257
439
  | roblox | 559,364 | 559,364 | 676,715 | 676,715 | 676,715 | 784,081 |
258
- | **lync** | **3.59** | 3.50 | 3.61 | 3.62 | 3.62 | 4.86 |
440
+ | **lync** | **3.61** | 3.53 | 3.63 | 3.64 | 3.64 | 4.64 |
259
441
  | blink | 41.81 | 26.30 | 42.40 | 42.48 | 42.48 | 42.62 |
260
442
  | zap | 41.71 | 25.46 | 42.19 | 42.32 | 42.32 | 42.93 |
261
443
  | bytenet | 41.64 | 22.84 | 42.36 | 42.82 | 42.82 | 43.24 |
@@ -265,7 +447,7 @@ Same data shapes and methodology as [Blink's benchmark suite](https://github.com
265
447
  | Tool (FPS) | Median | P0 | P80 | P90 | P95 | P100 |
266
448
  |:-----------|-------:|---:|----:|----:|----:|-----:|
267
449
  | roblox | 21.00 | 22.00 | 20.00 | 19.00 | 19.00 | 19.00 |
268
- | **lync** | **60.00** | 61.00 | 60.00 | 59.00 | 59.00 | 58.00 |
450
+ | **lync** | **60.00** | 61.00 | 60.00 | 60.00 | 60.00 | 59.00 |
269
451
  | blink | 97.00 | 98.00 | 97.00 | 96.00 | 96.00 | 96.00 |
270
452
  | zap | 52.00 | 53.00 | 51.00 | 51.00 | 51.00 | 49.00 |
271
453
  | bytenet | 35.00 | 37.00 | 35.00 | 35.00 | 35.00 | 34.00 |
@@ -273,7 +455,7 @@ Same data shapes and methodology as [Blink's benchmark suite](https://github.com
273
455
  | Tool (Kbps) | Median | P0 | P80 | P90 | P95 | P100 |
274
456
  |:------------|-------:|---:|----:|----:|----:|-----:|
275
457
  | roblox | 353,107 | 196,826 | 690,747 | 842,240 | 842,240 | 1,124,176 |
276
- | **lync** | **4.31** | 3.77 | 4.33 | 4.34 | 4.34 | 4.43 |
458
+ | **lync** | **4.31** | 3.85 | 4.36 | 4.38 | 4.38 | 4.44 |
277
459
  | blink | 7.91 | 7.41 | 7.93 | 7.99 | 7.99 | 8.00 |
278
460
  | zap | 8.10 | 5.75 | 8.17 | 8.22 | 8.22 | 8.27 |
279
461
  | bytenet | 8.11 | 5.07 | 8.35 | 8.46 | 8.46 | 8.47 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axpecter/lync",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "Buffer networking for Roblox. Delta compression, XOR framing, built-in security.",
5
5
  "main": "src/init.luau",
6
6
  "types": "src/index.d.ts",
package/src/Types.luau CHANGED
@@ -1,6 +1,6 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Shared type definitions.
3
+ -- Shared type definitions for the Lync networking library.
4
4
 
5
5
  export type ChannelState = {
6
6
  buff: buffer,
@@ -10,7 +10,7 @@ export type ChannelState = {
10
10
  lastId: number,
11
11
  countPos: number,
12
12
  itemCount: number,
13
- deltas: { [number]: any },
13
+ deltas: { [number]: any }, -- per-codec delta caches; shape varies by codec kind
14
14
  prevDump: buffer?,
15
15
  }
16
16
 
@@ -19,6 +19,17 @@ export type Codec<T> = {
19
19
  read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
20
20
  }
21
21
 
22
+ -- Extended codec exposing internal fast-path metadata used by composite codecs.
23
+ export type InternalCodec<T> = {
24
+ write: (ch: ChannelState, value: T) -> (),
25
+ read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
26
+ _size: number?,
27
+ _directWrite: ((b: buffer, offset: number, value: T) -> ())?,
28
+ _directRead: ((b: buffer, offset: number) -> T)?,
29
+ _isDelta: boolean?,
30
+ _isBool: boolean?,
31
+ }
32
+
22
33
  export type RateLimitConfig = {
23
34
  maxPerSecond: number,
24
35
  burstAllowance: number?,
@@ -4,13 +4,13 @@
4
4
 
5
5
  local Players = game:GetService ("Players")
6
6
 
7
- -- State ------------------------------------------------------------------
7
+ -- State --------------------------------------------------------------
8
8
 
9
9
  local _groups = {} :: { [string]: { [Player]: true } }
10
10
  local _counts = {} :: { [string]: number }
11
11
  local _playerGroups = {} :: { [Player]: { [string]: true } }
12
12
 
13
- -- Private ----------------------------------------------------------------
13
+ -- Private ------------------------------------------------------------
14
14
 
15
15
  local function onPlayerRemoving (player: Player): ()
16
16
  local memberships = _playerGroups[player]
@@ -31,43 +31,16 @@ end
31
31
 
32
32
  Players.PlayerRemoving:Connect (onPlayerRemoving)
33
33
 
34
- local function getSetOrError (name: string): { [Player]: true }
34
+ local GroupImpl = {}
35
+ GroupImpl.__index = GroupImpl
36
+
37
+ function GroupImpl.add (self: any, player: Player): boolean
38
+ local name = self._name
35
39
  local set = _groups[name]
36
40
  if not set then
37
- error (`[Lync] Group does not exist: \"{name}\"`)
38
- end
39
- return set
40
- end
41
-
42
- -- Public -----------------------------------------------------------------
43
-
44
- local Group = {}
45
-
46
- function Group.create (name: string): ()
47
- if _groups[name] then
48
- error (`[Lync] Group already exists: \"{name}\"`)
49
- end
50
- _groups[name] = {}
51
- _counts[name] = 0
52
- end
53
-
54
- function Group.destroy (name: string): ()
55
- local set = getSetOrError (name)
56
-
57
- for player in set do
58
- local memberships = _playerGroups[player]
59
- if memberships then
60
- memberships[name] = nil
61
- end
41
+ error (`[Lync] Group has been destroyed: "{name}"`)
62
42
  end
63
43
 
64
- _groups[name] = nil
65
- _counts[name] = nil
66
- end
67
-
68
- function Group.add (name: string, player: Player): boolean
69
- local set = getSetOrError (name)
70
-
71
44
  if set[player] then
72
45
  return false
73
46
  end
@@ -85,8 +58,12 @@ function Group.add (name: string, player: Player): boolean
85
58
  return true
86
59
  end
87
60
 
88
- function Group.remove (name: string, player: Player): boolean
89
- local set = getSetOrError (name)
61
+ function GroupImpl.remove (self: any, player: Player): boolean
62
+ local name = self._name
63
+ local set = _groups[name]
64
+ if not set then
65
+ error (`[Lync] Group has been destroyed: "{name}"`)
66
+ end
90
67
 
91
68
  if not set[player] then
92
69
  return false
@@ -103,24 +80,78 @@ function Group.remove (name: string, player: Player): boolean
103
80
  return true
104
81
  end
105
82
 
106
- function Group.has (name: string, player: Player): boolean
107
- local set = getSetOrError (name)
83
+ function GroupImpl.has (self: any, player: Player): boolean
84
+ local name = self._name
85
+ local set = _groups[name]
86
+ if not set then
87
+ error (`[Lync] Group has been destroyed: "{name}"`)
88
+ end
108
89
  return set[player] == true
109
90
  end
110
91
 
111
- function Group.count (name: string): number
112
- getSetOrError (name)
92
+ function GroupImpl.count (self: any): number
93
+ local name = self._name
94
+ if not _groups[name] then
95
+ error (`[Lync] Group has been destroyed: "{name}"`)
96
+ end
113
97
  return _counts[name]
114
98
  end
115
99
 
116
- function Group.getSet (name: string): { [Player]: true }
117
- return getSetOrError (name)
100
+ function GroupImpl.forEach (self: any, fn: (player: Player) -> ()): ()
101
+ local name = self._name
102
+ local set = _groups[name]
103
+ if not set then
104
+ error (`[Lync] Group has been destroyed: "{name}"`)
105
+ end
106
+ for player in set do
107
+ fn (player)
108
+ end
109
+ end
110
+
111
+ function GroupImpl.getSet (self: any): { [Player]: true }
112
+ local name = self._name
113
+ local set = _groups[name]
114
+ if not set then
115
+ error (`[Lync] Group has been destroyed: "{name}"`)
116
+ end
117
+ return set
118
118
  end
119
119
 
120
- function Group.forEach (name: string, fn: (player: Player) -> ()): ()
121
- for player in getSetOrError (name) do
122
- fn (player)
120
+ function GroupImpl.destroy (self: any): ()
121
+ local name = self._name
122
+ local set = _groups[name]
123
+ if not set then
124
+ error (`[Lync] Group has been destroyed: "{name}"`)
125
+ end
126
+
127
+ for player in set do
128
+ local memberships = _playerGroups[player]
129
+ if memberships then
130
+ memberships[name] = nil
131
+ end
123
132
  end
133
+
134
+ _groups[name] = nil
135
+ _counts[name] = nil
136
+ end
137
+
138
+ table.freeze (GroupImpl)
139
+
140
+ -- Public -------------------------------------------------------------
141
+
142
+ local Group = {}
143
+
144
+ function Group.create (name: string): any
145
+ if _groups[name] then
146
+ error (`[Lync] Group already exists: "{name}"`)
147
+ end
148
+ _groups[name] = {}
149
+ _counts[name] = 0
150
+
151
+ return setmetatable ({
152
+ _tag = "group",
153
+ _name = name,
154
+ }, GroupImpl)
124
155
  end
125
156
 
126
157
  return table.freeze (Group)