@axpecter/lync 1.3.1 → 1.4.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/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.0"
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** (ReplicatedStorage, required by both)
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.0"` |
40
167
 
41
168
  ## Packets
42
169
 
@@ -50,23 +177,24 @@ 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
  |:-------|:------------|
@@ -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
 
@@ -121,6 +251,87 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
121
251
  | `connection.connected` | `true` if still connected, `false` after disconnect. |
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)` | Track a raw Connection or RBXScriptConnection. |
274
+ | `scope:destroy()` | Disconnects all tracked connections. 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` | |
295
+ | `group:count()` | `number` | |
296
+ | `group:getSet()` | `{ [Player]: true }` | |
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axpecter/lync",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
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",
@@ -31,43 +31,18 @@ end
31
31
 
32
32
  Players.PlayerRemoving:Connect (onPlayerRemoving)
33
33
 
34
- local function getSetOrError (name: string): { [Player]: true }
35
- local set = _groups[name]
36
- 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 = {}
34
+ -- GroupImpl ---------------------------------------------------------------
45
35
 
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
36
+ local GroupImpl = {}
37
+ GroupImpl.__index = GroupImpl
53
38
 
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
39
+ function GroupImpl.add (self: any, player: Player): boolean
40
+ local name = self._name
41
+ local set = _groups[name]
42
+ if not set then
43
+ error (`[Lync] Group has been destroyed: "{name}"`)
62
44
  end
63
45
 
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
46
  if set[player] then
72
47
  return false
73
48
  end
@@ -85,8 +60,12 @@ function Group.add (name: string, player: Player): boolean
85
60
  return true
86
61
  end
87
62
 
88
- function Group.remove (name: string, player: Player): boolean
89
- local set = getSetOrError (name)
63
+ function GroupImpl.remove (self: any, player: Player): boolean
64
+ local name = self._name
65
+ local set = _groups[name]
66
+ if not set then
67
+ error (`[Lync] Group has been destroyed: "{name}"`)
68
+ end
90
69
 
91
70
  if not set[player] then
92
71
  return false
@@ -103,24 +82,78 @@ function Group.remove (name: string, player: Player): boolean
103
82
  return true
104
83
  end
105
84
 
106
- function Group.has (name: string, player: Player): boolean
107
- local set = getSetOrError (name)
85
+ function GroupImpl.has (self: any, player: Player): boolean
86
+ local name = self._name
87
+ local set = _groups[name]
88
+ if not set then
89
+ error (`[Lync] Group has been destroyed: "{name}"`)
90
+ end
108
91
  return set[player] == true
109
92
  end
110
93
 
111
- function Group.count (name: string): number
112
- getSetOrError (name)
94
+ function GroupImpl.count (self: any): number
95
+ local name = self._name
96
+ if not _groups[name] then
97
+ error (`[Lync] Group has been destroyed: "{name}"`)
98
+ end
113
99
  return _counts[name]
114
100
  end
115
101
 
116
- function Group.getSet (name: string): { [Player]: true }
117
- return getSetOrError (name)
102
+ function GroupImpl.forEach (self: any, fn: (player: Player) -> ()): ()
103
+ local name = self._name
104
+ local set = _groups[name]
105
+ if not set then
106
+ error (`[Lync] Group has been destroyed: "{name}"`)
107
+ end
108
+ for player in set do
109
+ fn (player)
110
+ end
118
111
  end
119
112
 
120
- function Group.forEach (name: string, fn: (player: Player) -> ()): ()
121
- for player in getSetOrError (name) do
122
- fn (player)
113
+ function GroupImpl.getSet (self: any): { [Player]: true }
114
+ local name = self._name
115
+ local set = _groups[name]
116
+ if not set then
117
+ error (`[Lync] Group has been destroyed: "{name}"`)
118
+ end
119
+ return set
120
+ end
121
+
122
+ function GroupImpl.destroy (self: any): ()
123
+ local name = self._name
124
+ local set = _groups[name]
125
+ if not set then
126
+ error (`[Lync] Group has been destroyed: "{name}"`)
123
127
  end
128
+
129
+ for player in set do
130
+ local memberships = _playerGroups[player]
131
+ if memberships then
132
+ memberships[name] = nil
133
+ end
134
+ end
135
+
136
+ _groups[name] = nil
137
+ _counts[name] = nil
138
+ end
139
+
140
+ table.freeze (GroupImpl)
141
+
142
+ -- Public -----------------------------------------------------------------
143
+
144
+ local Group = {}
145
+
146
+ function Group.create (name: string): any
147
+ if _groups[name] then
148
+ error (`[Lync] Group already exists: "{name}"`)
149
+ end
150
+ _groups[name] = {}
151
+ _counts[name] = 0
152
+
153
+ return setmetatable ({
154
+ _tag = "group",
155
+ _name = name,
156
+ }, GroupImpl)
124
157
  end
125
158
 
126
159
  return table.freeze (Group)
@@ -194,6 +194,8 @@ function Namespace.define (name: string, config: NamespaceConfig): any
194
194
 
195
195
  local packets = {} :: { [string]: any }
196
196
  local queries = {} :: { [string]: any }
197
+ local packetsByShort = {} :: { [string]: any }
198
+ local queriesByShort = {} :: { [string]: any }
197
199
  local fields = {} :: { [string]: any }
198
200
 
199
201
  fields._name = name
@@ -207,6 +209,7 @@ function Namespace.define (name: string, config: NamespaceConfig): any
207
209
  local fullName = prefixName (name, shortName)
208
210
  local packet = Packet.define (fullName, packetConfig)
209
211
  packets[fullName] = packet
212
+ packetsByShort[shortName] = packet
210
213
  fields[shortName] = packet
211
214
  end
212
215
  end
@@ -216,10 +219,14 @@ function Namespace.define (name: string, config: NamespaceConfig): any
216
219
  local fullName = prefixName (name, shortName)
217
220
  local query = Query.define (fullName, queryConfig)
218
221
  queries[fullName] = query
222
+ queriesByShort[shortName] = query
219
223
  fields[shortName] = query
220
224
  end
221
225
  end
222
226
 
227
+ fields.packets = table.freeze (packetsByShort)
228
+ fields.queries = table.freeze (queriesByShort)
229
+
223
230
  return setmetatable (fields, NamespaceImpl)
224
231
  end
225
232
 
@@ -21,7 +21,7 @@ local serverWriteTo = Server.writeTo
21
21
  local serverWriteToAll = Server.writeToAll
22
22
  local serverWriteToList = Server.writeToList
23
23
  local serverWriteToAllExcept = Server.writeToAllExcept
24
- local serverWriteToGroup = Server.writeToGroup
24
+ local serverWriteToSet = Server.writeToSet
25
25
  local clientWrite = Client.write
26
26
 
27
27
  -- Private ----------------------------------------------------------------
@@ -45,7 +45,7 @@ function SharedImpl.disconnectAll (self: any): ()
45
45
  self._signal:disconnectAll ()
46
46
  end
47
47
 
48
- -- Server-only metatable . Has sendTo/sendToAll, no send().
48
+ -- Server-only metatable. Single send() dispatches on target type.
49
49
  local ServerImpl = setmetatable ({}, { __index = SharedImpl })
50
50
  ServerImpl.__index = ServerImpl
51
51
 
@@ -64,34 +64,55 @@ local function checkDeltaMode (self: any, mode: number): ()
64
64
  end
65
65
  end
66
66
 
67
- function ServerImpl.sendTo (self: any, data: any, player: Player): ()
68
- checkDeltaMode (self, DELTA_TARGETED)
69
- serverWriteTo (player, self._id, self._name, self._codec, data, self._isUnreliable)
70
- end
67
+ function ServerImpl.send (self: any, data: any, target: any?): ()
68
+ if target == nil then
69
+ error ("[Lync] Server packet:send requires a target")
70
+ end
71
71
 
72
- function ServerImpl.sendToAll (self: any, data: any): ()
73
- checkDeltaMode (self, DELTA_BROADCAST)
74
- serverWriteToAll (self._id, self._name, self._codec, data, self._isUnreliable)
75
- end
72
+ local id = self._id
73
+ local name = self._name
74
+ local codec = self._codec
75
+ local isUnreliable = self._isUnreliable
76
76
 
77
- function ServerImpl.sendToList (self: any, data: any, players: { Player }): ()
78
- checkDeltaMode (self, DELTA_TARGETED)
79
- serverWriteToList (players, self._id, self._name, self._codec, data, self._isUnreliable)
80
- end
77
+ -- Single player
78
+ if typeof (target) == "Instance" then
79
+ checkDeltaMode (self, DELTA_TARGETED)
80
+ serverWriteTo (target :: Player, id, name, codec, data, isUnreliable)
81
+ return
82
+ end
81
83
 
82
- function ServerImpl.sendToAllExcept (self: any, data: any, except: Player): ()
83
- checkDeltaMode (self, DELTA_BROADCAST)
84
- serverWriteToAllExcept (except, self._id, self._name, self._codec, data, self._isUnreliable)
85
- end
84
+ -- Must be a table from here
85
+ local tag = target._tag
86
86
 
87
- function ServerImpl.sendToGroup (self: any, data: any, groupName: string): ()
88
- checkDeltaMode (self, DELTA_BROADCAST)
89
- serverWriteToGroup (groupName, self._id, self._name, self._codec, data, self._isUnreliable)
87
+ -- Lync.all sentinel
88
+ if tag == "all" then
89
+ checkDeltaMode (self, DELTA_BROADCAST)
90
+ serverWriteToAll (id, name, codec, data, isUnreliable)
91
+ return
92
+ end
93
+
94
+ -- Lync.except(player, ...) descriptor
95
+ if tag == "except" then
96
+ checkDeltaMode (self, DELTA_BROADCAST)
97
+ serverWriteToAllExcept (target._set, id, name, codec, data, isUnreliable)
98
+ return
99
+ end
100
+
101
+ -- Group object
102
+ if tag == "group" then
103
+ checkDeltaMode (self, DELTA_BROADCAST)
104
+ serverWriteToSet (target:getSet (), id, name, codec, data, isUnreliable)
105
+ return
106
+ end
107
+
108
+ -- Player list (array)
109
+ checkDeltaMode (self, DELTA_TARGETED)
110
+ serverWriteToList (target :: { Player }, id, name, codec, data, isUnreliable)
90
111
  end
91
112
 
92
113
  table.freeze (ServerImpl)
93
114
 
94
- -- Client-only metatable . Has send(), no sendTo/sendToAll
115
+ -- Client-only metatable. Has send(), no target needed.
95
116
  local ClientImpl = setmetatable ({}, { __index = SharedImpl })
96
117
  ClientImpl.__index = ClientImpl
97
118
 
@@ -6,7 +6,6 @@ local Players = game:GetService ("Players")
6
6
  local RunService = game:GetService ("RunService")
7
7
 
8
8
  local Client = require (script.Parent.Parent.transport.Client)
9
- local Group = require (script.Parent.Group)
10
9
  local Registry = require (script.Parent.Parent.internal.Registry)
11
10
  local Server = require (script.Parent.Parent.transport.Server)
12
11
  local Signal = require (script.Parent.Signal)
@@ -82,90 +81,35 @@ local function onTimeout (correlation: number): ()
82
81
  completeQuery (correlation, nil)
83
82
  end
84
83
 
85
- -- Query ------------------------------------------------------------------
86
-
87
- local QueryImpl = {}
88
- QueryImpl.__index = QueryImpl
89
-
90
- function QueryImpl.listen (self: any, callback: (...any) -> any): Connection
91
- local respId = self._respReg.id
92
- local respName = self._name
93
- local respCodec = self._respReg.codec
94
-
95
- if IS_SERVER then
96
- return self._reqReg.signal:connect (
97
- function (request: any, player: Player, correlation: number): ()
98
- local ok, response = pcall (callback, request, player)
99
- if not ok then
100
- warn (`[Lync] Query handler error on "{respName}": {response}`)
101
- end
102
-
103
- if ok and response ~= nil then
104
- Server.writeQuery (player, respId, respName, correlation, respCodec, response)
105
- else
106
- Server.writeQueryNil (player, respId, respName, correlation)
107
- end
108
- end
109
- )
110
- else
111
- return self._reqReg.signal:connect (
112
- function (request: any, _player: Player?, correlation: number): ()
113
- local ok, response = pcall (callback, request)
114
- if not ok then
115
- warn (`[Lync] Query handler error on "{respName}": {response}`)
116
- end
117
-
118
- if ok and response ~= nil then
119
- Client.writeQuery (respId, respName, correlation, respCodec, response)
120
- else
121
- Client.writeQueryNil (respId, respName, correlation)
122
- end
123
- end
124
- )
84
+ -- Send a query to multiple players and yield. Returns { [Player]: response? }.
85
+ local function requestMulti (self: any, players: { Player }, data: any): { [Player]: any? }
86
+ if not IS_SERVER then
87
+ error ("[Lync] Multi-request is server-only")
125
88
  end
126
- end
127
89
 
128
- function QueryImpl.invoke (self: any, request: any, player: Player?): any
129
- if IS_SERVER and not player then
130
- error ("[Lync] Query:invoke on server requires a player argument")
90
+ local count = #players
91
+ if count == 0 then
92
+ return {}
131
93
  end
132
94
 
133
- local correlation = allocCorrelation ()
134
- local thread = coroutine.running ()
135
-
136
- local entry: PendingQuery = { thread = thread, timeout = nil }
137
- _pending[correlation] = entry
95
+ if count == 1 then
96
+ -- Delegate to single-player path
97
+ local correlation = allocCorrelation ()
98
+ local thread = coroutine.running ()
99
+ local entry: PendingQuery = { thread = thread, timeout = nil }
100
+ _pending[correlation] = entry
138
101
 
139
- if IS_SERVER then
140
102
  Server.writeQuery (
141
- player :: Player,
103
+ players[1],
142
104
  self._reqReg.id,
143
105
  self._name,
144
106
  correlation,
145
107
  self._reqReg.codec,
146
- request
108
+ data
147
109
  )
148
- else
149
- Client.writeQuery (self._reqReg.id, self._name, correlation, self._reqReg.codec, request)
150
- end
151
-
152
- entry.timeout = task.delay (self._timeout, onTimeout, correlation)
153
- return coroutine.yield ()
154
- end
155
-
156
- -- Send a query to multiple players and yield. Returns { [Player]: response? }.
157
- local function invokeMulti (self: any, players: { Player }, request: any): { [Player]: any? }
158
- if not IS_SERVER then
159
- error ("[Lync] Multi-invoke is server-only")
160
- end
161
-
162
- local count = #players
163
- if count == 0 then
164
- return {}
165
- end
166
110
 
167
- if count == 1 then
168
- local result = QueryImpl.invoke (self, request, players[1])
111
+ entry.timeout = task.delay (self._timeout, onTimeout, correlation)
112
+ local result = coroutine.yield ()
169
113
  return { [players[1]] = result }
170
114
  end
171
115
 
@@ -187,8 +131,8 @@ local function invokeMulti (self: any, players: { Player }, request: any): { [Pl
187
131
 
188
132
  local entry: PendingQuery = {
189
133
  thread = nil,
190
- callback = function (data: any): ()
191
- results[player] = data
134
+ callback = function (resp: any): ()
135
+ results[player] = resp
192
136
  remaining -= 1
193
137
  if remaining == 0 and not finished then
194
138
  finished = true
@@ -200,7 +144,7 @@ local function invokeMulti (self: any, players: { Player }, request: any): { [Pl
200
144
 
201
145
  _pending[correlation] = entry
202
146
 
203
- Server.writeQuery (player, reqId, reqName, correlation, reqCodec, request)
147
+ Server.writeQuery (player, reqId, reqName, correlation, reqCodec, data)
204
148
  end
205
149
 
206
150
  local timeoutThread = task.delay (timeout, function (): ()
@@ -232,22 +176,102 @@ local function invokeMulti (self: any, players: { Player }, request: any): { [Pl
232
176
  return coroutine.yield ()
233
177
  end
234
178
 
235
- function QueryImpl.invokeAll (self: any, request: any): { [Player]: any? }
179
+ -- Query ------------------------------------------------------------------
180
+
181
+ local QueryImpl = {}
182
+ QueryImpl.__index = QueryImpl
183
+
184
+ function QueryImpl.listen (self: any, callback: (...any) -> any): Connection
185
+ local respId = self._respReg.id
186
+ local respName = self._name
187
+ local respCodec = self._respReg.codec
188
+
189
+ if IS_SERVER then
190
+ return self._reqReg.signal:connect (
191
+ function (request: any, player: Player, correlation: number): ()
192
+ local ok, response = pcall (callback, request, player)
193
+ if not ok then
194
+ warn (`[Lync] Query handler error on "{respName}": {response}`)
195
+ end
196
+
197
+ if ok and response ~= nil then
198
+ Server.writeQuery (player, respId, respName, correlation, respCodec, response)
199
+ else
200
+ Server.writeQueryNil (player, respId, respName, correlation)
201
+ end
202
+ end
203
+ )
204
+ else
205
+ return self._reqReg.signal:connect (
206
+ function (request: any, _player: Player?, correlation: number): ()
207
+ local ok, response = pcall (callback, request)
208
+ if not ok then
209
+ warn (`[Lync] Query handler error on "{respName}": {response}`)
210
+ end
211
+
212
+ if ok and response ~= nil then
213
+ Client.writeQuery (respId, respName, correlation, respCodec, response)
214
+ else
215
+ Client.writeQueryNil (respId, respName, correlation)
216
+ end
217
+ end
218
+ )
219
+ end
220
+ end
221
+
222
+ -- Client: send request to server, yield until response.
223
+ function QueryImpl.request (self: any, data: any): any
224
+ if IS_SERVER then
225
+ error ("[Lync] query:request is client-only. Use query:requestFrom on server")
226
+ end
227
+
228
+ local correlation = allocCorrelation ()
229
+ local thread = coroutine.running ()
230
+ local entry: PendingQuery = { thread = thread, timeout = nil }
231
+ _pending[correlation] = entry
232
+
233
+ Client.writeQuery (self._reqReg.id, self._name, correlation, self._reqReg.codec, data)
234
+
235
+ entry.timeout = task.delay (self._timeout, onTimeout, correlation)
236
+ return coroutine.yield ()
237
+ end
238
+
239
+ -- Server: send request to one player, yield until response.
240
+ function QueryImpl.requestFrom (self: any, player: Player, data: any): any
241
+ if not IS_SERVER then
242
+ error ("[Lync] query:requestFrom is server-only. Use query:request on client")
243
+ end
244
+
245
+ local correlation = allocCorrelation ()
246
+ local thread = coroutine.running ()
247
+ local entry: PendingQuery = { thread = thread, timeout = nil }
248
+ _pending[correlation] = entry
249
+
250
+ Server.writeQuery (player, self._reqReg.id, self._name, correlation, self._reqReg.codec, data)
251
+
252
+ entry.timeout = task.delay (self._timeout, onTimeout, correlation)
253
+ return coroutine.yield ()
254
+ end
255
+
256
+ -- Server: send request to all players, yield until all respond or timeout.
257
+ function QueryImpl.requestAll (self: any, data: any): { [Player]: any? }
236
258
  local players = Players:GetPlayers ()
237
- return invokeMulti (self, players, request)
259
+ return requestMulti (self, players, data)
238
260
  end
239
261
 
240
- function QueryImpl.invokeList (self: any, request: any, players: { Player }): { [Player]: any? }
241
- return invokeMulti (self, players, request)
262
+ -- Server: send request to a list of players.
263
+ function QueryImpl.requestList (self: any, players: { Player }, data: any): { [Player]: any? }
264
+ return requestMulti (self, players, data)
242
265
  end
243
266
 
244
- function QueryImpl.invokeGroup (self: any, request: any, groupName: string): { [Player]: any? }
245
- local set = Group.getSet (groupName)
267
+ -- Server: send request to all players in a group.
268
+ function QueryImpl.requestGroup (self: any, group: any, data: any): { [Player]: any? }
269
+ local set = group:getSet ()
246
270
  local players = {} :: { Player }
247
271
  for player in set do
248
272
  table.insert (players, player)
249
273
  end
250
- return invokeMulti (self, players, request)
274
+ return requestMulti (self, players, data)
251
275
  end
252
276
 
253
277
  -- Public -----------------------------------------------------------------
@@ -0,0 +1,73 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Batched connection lifecycle management.
4
+
5
+ -- Private ----------------------------------------------------------------
6
+
7
+ local ScopeImpl = {}
8
+ ScopeImpl.__index = ScopeImpl
9
+
10
+ function ScopeImpl.add (self: any, conn: any): ()
11
+ if not conn then
12
+ return
13
+ end
14
+
15
+ local entries = self._entries
16
+ local count = self._count + 1
17
+ self._count = count
18
+ entries[count] = conn
19
+ end
20
+
21
+ function ScopeImpl.listen (self: any, source: any, callback: (...any) -> ()): ()
22
+ local conn = source:listen (callback)
23
+ local entries = self._entries
24
+ local count = self._count + 1
25
+ self._count = count
26
+ entries[count] = conn
27
+ end
28
+
29
+ function ScopeImpl.once (self: any, source: any, callback: (...any) -> ()): ()
30
+ local conn = source:once (callback)
31
+ local entries = self._entries
32
+ local count = self._count + 1
33
+ self._count = count
34
+ entries[count] = conn
35
+ end
36
+
37
+ function ScopeImpl.listenAll (self: any, namespace: any, callback: (...any) -> ()): ()
38
+ local conn = namespace:listenAll (callback)
39
+ local entries = self._entries
40
+ local count = self._count + 1
41
+ self._count = count
42
+ entries[count] = conn
43
+ end
44
+
45
+ function ScopeImpl.destroy (self: any): ()
46
+ local entries = self._entries
47
+ local count = self._count
48
+
49
+ for i = 1, count do
50
+ local conn = entries[i]
51
+ entries[i] = nil
52
+
53
+ if typeof (conn) == "RBXScriptConnection" then
54
+ conn:Disconnect ()
55
+ elseif conn.disconnect then
56
+ conn:disconnect ()
57
+ end
58
+ end
59
+
60
+ self._count = 0
61
+ end
62
+
63
+ table.freeze (ScopeImpl)
64
+
65
+ -- Public -----------------------------------------------------------------
66
+
67
+ local Scope = {}
68
+
69
+ function Scope.create (): any
70
+ return setmetatable ({ _entries = {}, _count = 0 }, ScopeImpl)
71
+ end
72
+
73
+ return table.freeze (Scope)
package/src/index.d.ts CHANGED
@@ -72,6 +72,46 @@ type InferBitfield<S extends Record<string, FieldSpec>> = Prettify<{
72
72
  [K in keyof S]: InferFieldSpec<S[K]>;
73
73
  }>;
74
74
 
75
+ // -- Target descriptors ------------------------------------------------
76
+
77
+ export interface AllTarget {
78
+ readonly _tag: "all";
79
+ }
80
+
81
+ export interface ExceptTarget {
82
+ readonly _tag: "except";
83
+ readonly _set: ReadonlyMap<Player, true>;
84
+ }
85
+
86
+ export interface GroupObject {
87
+ readonly _tag: "group";
88
+ add(this: GroupObject, player: Player): boolean;
89
+ remove(this: GroupObject, player: Player): boolean;
90
+ has(this: GroupObject, player: Player): boolean;
91
+ count(this: GroupObject): number;
92
+ forEach(this: GroupObject, fn: (player: Player) => void): void;
93
+ getSet(this: GroupObject): ReadonlyMap<Player, true>;
94
+ destroy(this: GroupObject): void;
95
+ }
96
+
97
+ export type Target = Player | AllTarget | ExceptTarget | GroupObject | Player[];
98
+
99
+ // -- Scope -------------------------------------------------------------
100
+
101
+ export interface Scope {
102
+ add(this: Scope, conn: Connection | RBXScriptConnection): void;
103
+ listen<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined) => void): void;
104
+ once<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined) => void): void;
105
+ listenAll(this: Scope, namespace: Namespace, callback: (name: string, data: unknown, sender: Player | undefined) => void): void;
106
+ destroy(this: Scope): void;
107
+ }
108
+
109
+ // -- DROP sentinel -----------------------------------------------------
110
+
111
+ export interface DropSentinel {
112
+ readonly _tag: "drop";
113
+ }
114
+
75
115
  // -- Packet ------------------------------------------------------------
76
116
 
77
117
  export interface PacketConfig<T> {
@@ -82,14 +122,8 @@ export interface PacketConfig<T> {
82
122
  maxPayloadBytes?: number;
83
123
  }
84
124
 
85
- // Server methods throw on client and vice versa.
86
125
  export interface Packet<T> {
87
- sendTo(this: Packet<T>, data: T, player: Player): void;
88
- sendToAll(this: Packet<T>, data: T): void;
89
- sendToAllExcept(this: Packet<T>, data: T, except: Player): void;
90
- sendToList(this: Packet<T>, data: T, players: Player[]): void;
91
- sendToGroup(this: Packet<T>, data: T, groupName: string): void;
92
- send(this: Packet<T>, data: T): void;
126
+ send(this: Packet<T>, data: T, target?: Target): void;
93
127
  listen(this: Packet<T>, callback: (data: T, sender: Player | undefined) => void): Connection;
94
128
  once(this: Packet<T>, callback: (data: T, sender: Player | undefined) => void): Connection;
95
129
  wait(this: Packet<T>): LuaTuple<[T, Player | undefined]>;
@@ -111,17 +145,18 @@ export interface Query<Req, Resp> {
111
145
  this: Query<Req, Resp>,
112
146
  callback: (request: Req, player: Player) => Resp | undefined,
113
147
  ): Connection;
114
- invoke(this: Query<Req, Resp>, request: Req, player?: Player): Resp | undefined;
115
- invokeAll(this: Query<Req, Resp>, request: Req): Map<Player, Resp | undefined>;
116
- invokeList(
148
+ request(this: Query<Req, Resp>, data: Req): Resp | undefined;
149
+ requestFrom(this: Query<Req, Resp>, player: Player, data: Req): Resp | undefined;
150
+ requestAll(this: Query<Req, Resp>, data: Req): Map<Player, Resp | undefined>;
151
+ requestList(
117
152
  this: Query<Req, Resp>,
118
- request: Req,
119
153
  players: Player[],
154
+ data: Req,
120
155
  ): Map<Player, Resp | undefined>;
121
- invokeGroup(
156
+ requestGroup(
122
157
  this: Query<Req, Resp>,
123
- request: Req,
124
- groupName: string,
158
+ group: GroupObject,
159
+ data: Req,
125
160
  ): Map<Player, Resp | undefined>;
126
161
  }
127
162
 
@@ -143,6 +178,8 @@ type InferQueries<Q extends Record<string, QueryConfig<unknown, unknown>>> = {
143
178
  };
144
179
 
145
180
  export interface Namespace {
181
+ readonly packets: Record<string, Packet<unknown>>;
182
+ readonly queries: Record<string, Query<unknown, unknown>>;
146
183
  listenAll(
147
184
  this: Namespace,
148
185
  callback: (name: string, data: unknown, sender: Player | undefined) => void,
@@ -166,7 +203,6 @@ export interface Namespace {
166
203
  declare namespace Lync {
167
204
  // Lifecycle
168
205
  export const VERSION: string;
169
- export const version: string;
170
206
  export function start(): void;
171
207
 
172
208
  // Definition
@@ -251,21 +287,24 @@ declare namespace Lync {
251
287
  callback: (player: Player, reason: string, packetName: string, data: unknown) => void,
252
288
  ): () => void;
253
289
  export function onSend(
254
- handler: (data: unknown, name: string, player: Player | undefined) => unknown | undefined,
290
+ handler: (data: unknown, name: string, player: Player | undefined) => unknown | DropSentinel | undefined,
255
291
  ): () => void;
256
292
  export function onReceive(
257
- handler: (data: unknown, name: string, player: Player | undefined) => unknown | undefined,
293
+ handler: (data: unknown, name: string, player: Player | undefined) => unknown | DropSentinel | undefined,
258
294
  ): () => void;
259
295
 
296
+ // Target descriptors
297
+ export const all: AllTarget;
298
+ export function except(...players: Player[]): ExceptTarget;
299
+
300
+ // Middleware sentinel
301
+ export const DROP: DropSentinel;
302
+
260
303
  // Groups
261
- export function createGroup(name: string): void;
262
- export function destroyGroup(name: string): void;
263
- export function addToGroup(name: string, player: Player): boolean;
264
- export function removeFromGroup(name: string, player: Player): boolean;
265
- export function hasInGroup(name: string, player: Player): boolean;
266
- export function getGroupSet(name: string): ReadonlyMap<Player, true>;
267
- export function groupCount(name: string): number;
268
- export function forEachInGroup(name: string, fn: (player: Player) => void): void;
304
+ export function createGroup(name: string): GroupObject;
305
+
306
+ // Scope
307
+ export function scope(): Scope;
269
308
 
270
309
  // Configuration
271
310
  export function setChannelMaxSize(bytes: number): void;
@@ -276,4 +315,4 @@ declare namespace Lync {
276
315
  export function queryPendingCount(): number;
277
316
  }
278
317
 
279
- export default Lync;
318
+ export default Lync;
package/src/init.luau CHANGED
@@ -52,11 +52,25 @@ local Group = require (script.api.Group)
52
52
  local Namespace = require (script.api.Namespace)
53
53
  local Packet = require (script.api.Packet)
54
54
  local Query = require (script.api.Query)
55
+ local Scope = require (script.api.Scope)
55
56
 
56
57
  -- Transport
57
58
  local Client = require (script.transport.Client)
58
59
  local Server = require (script.transport.Server)
59
60
 
61
+ -- Sentinels --------------------------------------------------------------
62
+
63
+ local ALL = table.freeze ({ _tag = "all" })
64
+
65
+ local function except (...: any): any
66
+ local count = select ("#", ...)
67
+ local set = {} :: { [Player]: true }
68
+ for i = 1, count do
69
+ set[select (i, ...)] = true
70
+ end
71
+ return { _tag = "except", _set = set }
72
+ end
73
+
60
74
  -- Public -----------------------------------------------------------------
61
75
 
62
76
  local function start (): ()
@@ -68,8 +82,7 @@ local function start (): ()
68
82
  end
69
83
 
70
84
  local Lync = {
71
- VERSION = "1.3.1",
72
- version = "1.3.1",
85
+ VERSION = "1.4.0",
73
86
 
74
87
  -- Lifecycle
75
88
  start = start,
@@ -139,15 +152,18 @@ local Lync = {
139
152
  onSend = Middleware.addSend,
140
153
  onReceive = Middleware.addReceive,
141
154
 
155
+ -- Target descriptors
156
+ all = ALL,
157
+ except = except,
158
+
159
+ -- Middleware sentinel
160
+ DROP = Middleware.DROP,
161
+
142
162
  -- Groups
143
163
  createGroup = Group.create,
144
- destroyGroup = Group.destroy,
145
- addToGroup = Group.add,
146
- removeFromGroup = Group.remove,
147
- hasInGroup = Group.has,
148
- getGroupSet = Group.getSet,
149
- groupCount = Group.count,
150
- forEachInGroup = Group.forEach,
164
+
165
+ -- Scope
166
+ scope = Scope.create,
151
167
 
152
168
  -- Configuration (call before start)
153
169
  setChannelMaxSize = Channel.setMaxSize,
@@ -14,13 +14,17 @@ type Chain = {
14
14
  local _send: Chain = { handlers = {}, snapshot = nil }
15
15
  local _receive: Chain = { handlers = {}, snapshot = nil }
16
16
 
17
+ -- Constants --------------------------------------------------------------
18
+
19
+ local DROP = table.freeze ({ _tag = "drop" })
20
+
17
21
  -- Private ----------------------------------------------------------------
18
22
 
19
23
  local function runChain (chain: { Handler }, data: any, name: string, player: Player?): any?
20
24
  local current = data
21
25
  for i = 1, #chain do
22
26
  local result = chain[i] (current, name, player)
23
- if result == nil then
27
+ if result == nil or result == DROP then
24
28
  return nil
25
29
  end
26
30
  current = result
@@ -55,6 +59,10 @@ local function run (chain: Chain, data: any, name: string, player: Player?): any
55
59
  return nil
56
60
  end
57
61
 
62
+ if result == DROP then
63
+ return nil
64
+ end
65
+
58
66
  return result
59
67
  end
60
68
 
@@ -105,5 +113,6 @@ end
105
113
 
106
114
  Middleware.hasSend = false
107
115
  Middleware.hasReceive = false
116
+ Middleware.DROP = DROP
108
117
 
109
118
  return Middleware
@@ -14,7 +14,6 @@ local channelWriteBatchRaw = Channel.writeBatchRaw
14
14
  local channelSetPacket = Channel.setCurrentPacket
15
15
 
16
16
  local Gate = require (script.Parent.Gate)
17
- local Group = require (script.Parent.Parent.api.Group)
18
17
  local Middleware = require (script.Parent.Parent.internal.Middleware)
19
18
  local Pool = require (script.Parent.Parent.internal.Pool)
20
19
  local Reader = require (script.Parent.Reader)
@@ -161,7 +160,7 @@ local function broadcastToAll (
161
160
  codec: Codec<any>,
162
161
  data: any,
163
162
  isUnreliable: boolean,
164
- except: Player?
163
+ exceptSet: { [Player]: true }?
165
164
  ): ()
166
165
  local final, snapLen, snapRefs = prepareBroadcast (name, codec, data)
167
166
  if final == nil and data ~= nil then
@@ -170,7 +169,7 @@ local function broadcastToAll (
170
169
 
171
170
  local scratchBuff = _broadScratch.buff
172
171
  for player, state in _players do
173
- if except and player == except then
172
+ if exceptSet and exceptSet[player] then
174
173
  continue
175
174
  end
176
175
  local ch = if isUnreliable then state.unreliable else state.reliable
@@ -304,25 +303,25 @@ function Server.writeToList (
304
303
  end
305
304
 
306
305
  function Server.writeToAllExcept (
307
- except: Player,
306
+ exceptSet: { [Player]: true },
308
307
  id: number,
309
308
  name: string,
310
309
  codec: Codec<any>,
311
310
  data: any,
312
311
  isUnreliable: boolean
313
312
  ): ()
314
- broadcastToAll (id, name, codec, data, isUnreliable, except)
313
+ broadcastToAll (id, name, codec, data, isUnreliable, exceptSet)
315
314
  end
316
315
 
317
- function Server.writeToGroup (
318
- groupName: string,
316
+ function Server.writeToSet (
317
+ set: { [Player]: true },
319
318
  id: number,
320
319
  name: string,
321
320
  codec: Codec<any>,
322
321
  data: any,
323
322
  isUnreliable: boolean
324
323
  ): ()
325
- broadcastToSet (Group.getSet (groupName), id, name, codec, data, isUnreliable)
324
+ broadcastToSet (set, id, name, codec, data, isUnreliable)
326
325
  end
327
326
 
328
327
  -- Shared for both query requests and responses (identical wire format).