@axpecter/lync 1.3.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.
Files changed (52) hide show
  1. package/README.md +300 -0
  2. package/package.json +38 -0
  3. package/src/Types.luau +63 -0
  4. package/src/api/Group.luau +126 -0
  5. package/src/api/Namespace.luau +226 -0
  6. package/src/api/Packet.luau +147 -0
  7. package/src/api/Query.luau +295 -0
  8. package/src/api/Signal.luau +224 -0
  9. package/src/codec/Base.luau +49 -0
  10. package/src/codec/composite/Array.luau +275 -0
  11. package/src/codec/composite/Map.luau +395 -0
  12. package/src/codec/composite/Optional.luau +47 -0
  13. package/src/codec/composite/Shared.luau +151 -0
  14. package/src/codec/composite/Struct.luau +440 -0
  15. package/src/codec/composite/Tagged.luau +222 -0
  16. package/src/codec/composite/Tuple.luau +143 -0
  17. package/src/codec/datatype/Buffer.luau +44 -0
  18. package/src/codec/datatype/CFrame.luau +51 -0
  19. package/src/codec/datatype/Color.luau +22 -0
  20. package/src/codec/datatype/Instance.luau +48 -0
  21. package/src/codec/datatype/IntVector.luau +25 -0
  22. package/src/codec/datatype/NumberRange.luau +14 -0
  23. package/src/codec/datatype/Ray.luau +27 -0
  24. package/src/codec/datatype/Rect.luau +21 -0
  25. package/src/codec/datatype/Region.luau +58 -0
  26. package/src/codec/datatype/Sequence.luau +129 -0
  27. package/src/codec/datatype/String.luau +87 -0
  28. package/src/codec/datatype/UDim.luau +27 -0
  29. package/src/codec/datatype/Vector.luau +25 -0
  30. package/src/codec/meta/Auto.luau +353 -0
  31. package/src/codec/meta/Bitfield.luau +191 -0
  32. package/src/codec/meta/Custom.luau +27 -0
  33. package/src/codec/meta/Enum.luau +80 -0
  34. package/src/codec/meta/Nothing.luau +9 -0
  35. package/src/codec/meta/Quantized.luau +170 -0
  36. package/src/codec/meta/Unknown.luau +35 -0
  37. package/src/codec/primitive/Bool.luau +30 -0
  38. package/src/codec/primitive/Float16.luau +111 -0
  39. package/src/codec/primitive/Number.luau +48 -0
  40. package/src/codec/primitive/Varint.luau +76 -0
  41. package/src/index.d.ts +279 -0
  42. package/src/init.luau +161 -0
  43. package/src/internal/Baseline.luau +41 -0
  44. package/src/internal/Channel.luau +235 -0
  45. package/src/internal/Middleware.luau +109 -0
  46. package/src/internal/Pool.luau +68 -0
  47. package/src/internal/Registry.luau +146 -0
  48. package/src/transport/Bridge.luau +66 -0
  49. package/src/transport/Client.luau +151 -0
  50. package/src/transport/Gate.luau +222 -0
  51. package/src/transport/Reader.luau +175 -0
  52. package/src/transport/Server.luau +364 -0
@@ -0,0 +1,235 @@
1
+ --!strict
2
+ --!native
3
+ -- Per-channel buffer state with batching, delta cache, frame XOR.
4
+
5
+ local Types = require (script.Parent.Parent.Types)
6
+
7
+ type ChannelState = Types.ChannelState
8
+
9
+ -- Constants --------------------------------------------------------------
10
+
11
+ local INITIAL_SIZE = 1024
12
+ local MAX_SIZE = 262144
13
+ local MAX_BATCH = 65535
14
+
15
+ local min = math.min
16
+ local bxor = bit32.bxor
17
+ local band32 = bit32.band
18
+ local countlz = bit32.countlz
19
+ local lshift = bit32.lshift
20
+
21
+ -- State ------------------------------------------------------------------
22
+
23
+ local _maxSize = MAX_SIZE
24
+ local _currentPacket = ""
25
+
26
+ -- Public -----------------------------------------------------------------
27
+
28
+ local Channel = {}
29
+
30
+ -- Override the maximum buffer size. Call before start(). Range: 4096–1048576.
31
+ function Channel.setMaxSize (bytes: number): ()
32
+ if bytes < 4096 or bytes > 1048576 then
33
+ error (`[Lync] Channel max size must be 4096–1048576, got {bytes}`)
34
+ end
35
+ _maxSize = bytes
36
+ end
37
+
38
+ function Channel.getMaxSize (): number
39
+ return _maxSize
40
+ end
41
+
42
+ function Channel.create (): ChannelState
43
+ return {
44
+ buff = buffer.create (INITIAL_SIZE),
45
+ cursor = 0,
46
+ size = INITIAL_SIZE,
47
+ refs = {},
48
+ lastId = -1,
49
+ countPos = 0,
50
+ itemCount = 0,
51
+ deltas = {},
52
+ prevDump = nil,
53
+ }
54
+ end
55
+
56
+ -- Grows the channel buffer to fit the requested bytes. Power-of-two sizing.
57
+ function Channel.alloc (ch: ChannelState, bytes: number): ()
58
+ local needed = ch.cursor + bytes
59
+ if needed <= ch.size then
60
+ return
61
+ end
62
+ if needed > _maxSize then
63
+ error (
64
+ `[Lync] Channel overflow writing "{_currentPacket}": {needed} bytes > {_maxSize} max (call Channel.setMaxSize to raise)`
65
+ )
66
+ end
67
+
68
+ local newSize = lshift (1, 32 - countlz (needed - 1))
69
+
70
+ local newBuff = buffer.create (newSize)
71
+ buffer.copy (newBuff, 0, ch.buff, 0, ch.cursor)
72
+ ch.buff = newBuff
73
+ ch.size = newSize
74
+ end
75
+
76
+ local alloc = Channel.alloc
77
+
78
+ -- Seals the open batch header and returns a snapshot of the buffer + refs.
79
+ function Channel.sealAndDump (ch: ChannelState): (buffer, { Instance })
80
+ if ch.lastId >= 0 then
81
+ buffer.writeu16 (ch.buff, ch.countPos, ch.itemCount)
82
+ ch.lastId = -1
83
+ ch.countPos = 0
84
+ ch.itemCount = 0
85
+ end
86
+
87
+ local cur = ch.cursor
88
+ local out = buffer.create (cur)
89
+ buffer.copy (out, 0, ch.buff, 0, cur)
90
+
91
+ local refs = ch.refs
92
+ return out, if #refs > 0 then table.clone (refs) else {}
93
+ end
94
+
95
+ -- Sets the packet name for overflow diagnostics.
96
+ function Channel.setCurrentPacket (name: string): ()
97
+ _currentPacket = name
98
+ end
99
+
100
+ -- Writes a single value into the batch. Opens a new header on ID change or overflow.
101
+ function Channel.writeBatch (ch: ChannelState, id: number, name: string, codec: any, data: any): ()
102
+ if id ~= ch.lastId or ch.itemCount >= MAX_BATCH then
103
+ if ch.lastId >= 0 then
104
+ buffer.writeu16 (ch.buff, ch.countPos, ch.itemCount)
105
+ end
106
+
107
+ _currentPacket = name
108
+ alloc (ch, 3)
109
+ local c = ch.cursor
110
+ buffer.writeu8 (ch.buff, c, id)
111
+ ch.countPos = c + 1
112
+ ch.cursor = c + 3
113
+ ch.lastId = id
114
+ ch.itemCount = 0
115
+ end
116
+
117
+ ch.itemCount += 1
118
+ codec.write (ch, data)
119
+ end
120
+
121
+ -- Appends pre-serialized bytes into a batch. Used by broadcast fan-out.
122
+ function Channel.writeBatchRaw (
123
+ ch: ChannelState,
124
+ id: number,
125
+ src: buffer,
126
+ srcLen: number,
127
+ srcRefs: { Instance }?
128
+ ): ()
129
+ if id ~= ch.lastId or ch.itemCount >= MAX_BATCH then
130
+ if ch.lastId >= 0 then
131
+ buffer.writeu16 (ch.buff, ch.countPos, ch.itemCount)
132
+ end
133
+
134
+ alloc (ch, 3)
135
+ local c = ch.cursor
136
+ buffer.writeu8 (ch.buff, c, id)
137
+ ch.countPos = c + 1
138
+ ch.cursor = c + 3
139
+ ch.lastId = id
140
+ ch.itemCount = 0
141
+ end
142
+
143
+ ch.itemCount += 1
144
+
145
+ if srcLen > 0 then
146
+ alloc (ch, srcLen)
147
+ buffer.copy (ch.buff, ch.cursor, src, 0, srcLen)
148
+ ch.cursor += srcLen
149
+ end
150
+
151
+ if srcRefs then
152
+ local refs = ch.refs
153
+ table.move (srcRefs, 1, #srcRefs, #refs + 1, refs)
154
+ end
155
+ end
156
+
157
+ local function sealOpen (ch: ChannelState): ()
158
+ if ch.lastId >= 0 then
159
+ buffer.writeu16 (ch.buff, ch.countPos, ch.itemCount)
160
+ ch.lastId = -1
161
+ ch.countPos = 0
162
+ ch.itemCount = 0
163
+ end
164
+ end
165
+
166
+ -- Writes a query frame. Seals any open batch, then writes:
167
+ -- id (u8) + correlationId (u16) + status (u8)
168
+ -- Status 0 = payload follows. Status 1 = nil response.
169
+ function Channel.writeQuery (
170
+ ch: ChannelState,
171
+ id: number,
172
+ name: string,
173
+ correlationId: number,
174
+ codec: any?,
175
+ data: any?
176
+ ): ()
177
+ _currentPacket = name
178
+ sealOpen (ch)
179
+
180
+ alloc (ch, 4)
181
+ local c = ch.cursor
182
+ buffer.writeu8 (ch.buff, c, id)
183
+ buffer.writeu16 (ch.buff, c + 1, correlationId)
184
+
185
+ if codec then
186
+ buffer.writeu8 (ch.buff, c + 3, 0)
187
+ ch.cursor = c + 4
188
+ codec.write (ch, data)
189
+ else
190
+ buffer.writeu8 (ch.buff, c + 3, 1)
191
+ ch.cursor = c + 4
192
+ end
193
+ end
194
+
195
+ -- XOR current frame against previous. Produces zero-heavy output for deflate.
196
+ function Channel.xorApply (current: buffer, previous: buffer?): buffer
197
+ local curLen = buffer.len (current)
198
+ if not previous then
199
+ return current
200
+ end
201
+
202
+ local prevLen = buffer.len (previous :: buffer)
203
+ if prevLen == 0 then
204
+ return current
205
+ end
206
+
207
+ local result = buffer.create (curLen)
208
+ local overlap = min (curLen, prevLen)
209
+ local prev = previous :: buffer
210
+
211
+ local aligned = band32 (overlap, 0xFFFFFFFC)
212
+ for i = 0, aligned - 4, 4 do
213
+ buffer.writeu32 (result, i, bxor (buffer.readu32 (current, i), buffer.readu32 (prev, i)))
214
+ end
215
+
216
+ for i = aligned, overlap - 1 do
217
+ buffer.writeu8 (result, i, bxor (buffer.readu8 (current, i), buffer.readu8 (prev, i)))
218
+ end
219
+
220
+ if curLen > overlap then
221
+ buffer.copy (result, overlap, current, overlap, curLen - overlap)
222
+ end
223
+
224
+ return result
225
+ end
226
+
227
+ function Channel.reset (ch: ChannelState): ()
228
+ ch.cursor = 0
229
+ ch.lastId = -1
230
+ ch.countPos = 0
231
+ ch.itemCount = 0
232
+ table.clear (ch.refs)
233
+ end
234
+
235
+ return table.freeze (Channel)
@@ -0,0 +1,109 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Ordered intercept chains for send and receive.
4
+
5
+ -- State ------------------------------------------------------------------
6
+
7
+ type Handler = (data: any, name: string, player: Player?) -> any?
8
+
9
+ type Chain = {
10
+ handlers: { Handler },
11
+ snapshot: { Handler }?,
12
+ }
13
+
14
+ local _send: Chain = { handlers = {}, snapshot = nil }
15
+ local _receive: Chain = { handlers = {}, snapshot = nil }
16
+
17
+ -- Private ----------------------------------------------------------------
18
+
19
+ local function runChain (chain: { Handler }, data: any, name: string, player: Player?): any?
20
+ local current = data
21
+ for i = 1, #chain do
22
+ local result = chain[i] (current, name, player)
23
+ if result == nil then
24
+ return nil
25
+ end
26
+ current = result
27
+ end
28
+ return current
29
+ end
30
+
31
+ local function run (chain: Chain, data: any, name: string, player: Player?): any?
32
+ local handlers = chain.handlers
33
+ local count = #handlers
34
+ if count == 0 then
35
+ return data
36
+ end
37
+
38
+ local snapshot = chain.snapshot
39
+ if not snapshot then
40
+ snapshot = table.clone (handlers)
41
+ chain.snapshot = snapshot
42
+ end
43
+
44
+ local ok: boolean
45
+ local result: any
46
+
47
+ if count == 1 then
48
+ ok, result = pcall (snapshot[1], data, name, player)
49
+ else
50
+ ok, result = pcall (runChain, snapshot, data, name, player)
51
+ end
52
+
53
+ if not ok then
54
+ warn (`[Lync] Middleware error on "{name}": {result}`)
55
+ return nil
56
+ end
57
+
58
+ return result
59
+ end
60
+
61
+ local function addTo (chain: Chain, fn: Handler): () -> ()
62
+ table.insert (chain.handlers, fn)
63
+ chain.snapshot = nil
64
+
65
+ return function (): ()
66
+ local pos = table.find (chain.handlers, fn)
67
+ if pos then
68
+ table.remove (chain.handlers, pos)
69
+ chain.snapshot = nil
70
+ end
71
+ end
72
+ end
73
+
74
+ -- Public -----------------------------------------------------------------
75
+
76
+ local Middleware = {}
77
+
78
+ function Middleware.addSend (fn: Handler): () -> ()
79
+ local remover = addTo (_send, fn)
80
+ Middleware.hasSend = true
81
+
82
+ return function (): ()
83
+ remover ()
84
+ Middleware.hasSend = #_send.handlers > 0
85
+ end
86
+ end
87
+
88
+ function Middleware.addReceive (fn: Handler): () -> ()
89
+ local remover = addTo (_receive, fn)
90
+ Middleware.hasReceive = true
91
+
92
+ return function (): ()
93
+ remover ()
94
+ Middleware.hasReceive = #_receive.handlers > 0
95
+ end
96
+ end
97
+
98
+ function Middleware.runSend (data: any, name: string, player: Player?): any?
99
+ return run (_send, data, name, player)
100
+ end
101
+
102
+ function Middleware.runReceive (data: any, name: string, player: Player?): any?
103
+ return run (_receive, data, name, player)
104
+ end
105
+
106
+ Middleware.hasSend = false
107
+ Middleware.hasReceive = false
108
+
109
+ return Middleware
@@ -0,0 +1,68 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Stack-based ChannelState pool with warmup support.
4
+
5
+ local Channel = require (script.Parent.Channel)
6
+ local Types = require (script.Parent.Parent.Types)
7
+
8
+ type ChannelState = Types.ChannelState
9
+
10
+ -- Constants --------------------------------------------------------------
11
+
12
+ local DEFAULT_MAX = 16
13
+
14
+ -- State ------------------------------------------------------------------
15
+
16
+ local _stack = {} :: { ChannelState }
17
+ local _depth = 0
18
+ local _maxSize = DEFAULT_MAX
19
+
20
+ -- Public -----------------------------------------------------------------
21
+
22
+ local Pool = {}
23
+
24
+ -- Override the max idle ChannelStates in the pool. Default 16. Range: 2–128.
25
+ function Pool.setMaxSize (count: number): ()
26
+ if count < 2 or count > 128 then
27
+ error (`[Lync] Pool max size must be 2–128, got {count}`)
28
+ end
29
+ _maxSize = count
30
+ end
31
+
32
+ function Pool.getMaxSize (): number
33
+ return _maxSize
34
+ end
35
+
36
+ function Pool.acquire (): ChannelState
37
+ if _depth > 0 then
38
+ local ch = _stack[_depth]
39
+ _stack[_depth] = nil :: any
40
+ _depth -= 1
41
+ return ch
42
+ end
43
+
44
+ return Channel.create ()
45
+ end
46
+
47
+ function Pool.release (ch: ChannelState): ()
48
+ ch.cursor = 0
49
+ ch.lastId = -1
50
+ ch.countPos = 0
51
+ ch.itemCount = 0
52
+ table.clear (ch.refs)
53
+ table.clear (ch.deltas)
54
+ ch.prevDump = nil
55
+
56
+ if _depth >= _maxSize then
57
+ return -- discard; GC will reclaim
58
+ end
59
+
60
+ _depth += 1
61
+ _stack[_depth] = ch
62
+ end
63
+
64
+ function Pool.size (): number
65
+ return _depth
66
+ end
67
+
68
+ return table.freeze (Pool)
@@ -0,0 +1,146 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Deterministic ID assignment for packets and queries.
4
+
5
+ local Types = require (script.Parent.Parent.Types)
6
+
7
+ type Codec<T> = Types.Codec<T>
8
+ type Registration = Types.Registration
9
+ type RateLimitConfig = Types.RateLimitConfig
10
+
11
+ -- Constants --------------------------------------------------------------
12
+
13
+ local KIND_PACKET = 0
14
+ local KIND_REQUEST = 1
15
+ local KIND_RESPONSE = 2
16
+ local MAX_PACKETS = 255
17
+
18
+ -- State ------------------------------------------------------------------
19
+
20
+ local _entries = {} :: { Registration }
21
+ local _names = {} :: { [string]: number }
22
+ local _nextId = 1
23
+
24
+ -- Private ----------------------------------------------------------------
25
+
26
+ local function assign (
27
+ name: string,
28
+ baseName: string,
29
+ codec: Codec<any>,
30
+ isUnreliable: boolean,
31
+ kind: number,
32
+ signal: any,
33
+ rateLimit: RateLimitConfig?,
34
+ validate: ((data: any, player: Player) -> (boolean, string?))?,
35
+ maxPayloadBytes: number?
36
+ ): Registration
37
+ if _names[name] then
38
+ error (`[Lync] Duplicate registration: "{name}"`)
39
+ end
40
+ if _nextId > MAX_PACKETS then
41
+ error (`[Lync] Registration limit reached: {MAX_PACKETS} max`)
42
+ end
43
+
44
+ if rateLimit then
45
+ if rateLimit.maxPerSecond <= 0 then
46
+ error (`[Lync] maxPerSecond must be positive: "{name}"`)
47
+ end
48
+ if rateLimit.burstAllowance and rateLimit.burstAllowance <= 0 then
49
+ error (`[Lync] burstAllowance must be positive: "{name}"`)
50
+ end
51
+ end
52
+
53
+ if maxPayloadBytes and maxPayloadBytes <= 0 then
54
+ error (`[Lync] maxPayloadBytes must be positive: "{name}"`)
55
+ end
56
+
57
+ local id = _nextId
58
+ _nextId += 1
59
+
60
+ local reg: Registration = {
61
+ id = id,
62
+ name = name,
63
+ baseName = baseName,
64
+ codec = codec,
65
+ isUnreliable = isUnreliable,
66
+ kind = kind,
67
+ partner = nil,
68
+ signal = signal,
69
+ rateLimit = rateLimit,
70
+ validate = validate,
71
+ needsGate = rateLimit ~= nil or validate ~= nil,
72
+ maxPayloadBytes = maxPayloadBytes,
73
+ }
74
+
75
+ _entries[id] = reg
76
+ _names[name] = id
77
+ return reg
78
+ end
79
+
80
+ -- Public -----------------------------------------------------------------
81
+
82
+ local Registry = {
83
+ KIND_PACKET = KIND_PACKET,
84
+ KIND_REQUEST = KIND_REQUEST,
85
+ KIND_RESPONSE = KIND_RESPONSE,
86
+ }
87
+
88
+ function Registry.register (
89
+ name: string,
90
+ codec: Codec<any>,
91
+ isUnreliable: boolean,
92
+ signal: any,
93
+ rateLimit: RateLimitConfig?,
94
+ validate: ((data: any, player: Player) -> (boolean, string?))?,
95
+ maxPayloadBytes: number?
96
+ ): Registration
97
+ return assign (
98
+ name,
99
+ name,
100
+ codec,
101
+ isUnreliable,
102
+ KIND_PACKET,
103
+ signal,
104
+ rateLimit,
105
+ validate,
106
+ maxPayloadBytes
107
+ )
108
+ end
109
+
110
+ function Registry.registerQueryPair (
111
+ name: string,
112
+ requestCodec: Codec<any>,
113
+ responseCodec: Codec<any>,
114
+ requestSignal: any,
115
+ responseSignal: any,
116
+ rateLimit: RateLimitConfig?,
117
+ validate: ((data: any, player: Player) -> (boolean, string?))?
118
+ ): (Registration, Registration)
119
+ local req = assign (
120
+ `{name}:req`,
121
+ name,
122
+ requestCodec,
123
+ false,
124
+ KIND_REQUEST,
125
+ requestSignal,
126
+ rateLimit,
127
+ validate
128
+ )
129
+ local resp =
130
+ assign (`{name}:resp`, name, responseCodec, false, KIND_RESPONSE, responseSignal, nil, nil)
131
+
132
+ req.partner = resp.id
133
+ resp.partner = req.id
134
+ return req, resp
135
+ end
136
+
137
+ function Registry.get (id: number): Registration?
138
+ return _entries[id]
139
+ end
140
+
141
+ function Registry.getByName (name: string): Registration?
142
+ local id = _names[name]
143
+ return if id then _entries[id] else nil
144
+ end
145
+
146
+ return table.freeze (Registry)
@@ -0,0 +1,66 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Creates and exposes the network remote instances.
4
+
5
+ local ReplicatedStorage = game:GetService ("ReplicatedStorage")
6
+ local RunService = game:GetService ("RunService")
7
+
8
+ -- Constants --------------------------------------------------------------
9
+
10
+ local FOLDER = "_lync"
11
+ local RELIABLE = "R"
12
+ local UNRELIABLE = "U"
13
+ local TIMEOUT = 10
14
+
15
+ -- State ------------------------------------------------------------------
16
+
17
+ local _reliable: RemoteEvent
18
+ local _unreliable: UnreliableRemoteEvent
19
+
20
+ -- Public -----------------------------------------------------------------
21
+
22
+ local Bridge = {}
23
+
24
+ function Bridge.setup (): ()
25
+ if RunService:IsServer () then
26
+ local folder = Instance.new ("Folder")
27
+ folder.Name = FOLDER
28
+
29
+ local reliable = Instance.new ("RemoteEvent")
30
+ reliable.Name = RELIABLE
31
+ reliable.Parent = folder
32
+
33
+ local unreliable = Instance.new ("UnreliableRemoteEvent")
34
+ unreliable.Name = UNRELIABLE
35
+ unreliable.Parent = folder
36
+
37
+ folder.Parent = ReplicatedStorage
38
+
39
+ _reliable = reliable
40
+ _unreliable = unreliable
41
+ else
42
+ local folder = ReplicatedStorage:WaitForChild (FOLDER, TIMEOUT)
43
+ if not folder then
44
+ error ("[Lync] Network folder not found, is the server started?")
45
+ end
46
+
47
+ local reliable = folder:WaitForChild (RELIABLE, TIMEOUT)
48
+ if not reliable then
49
+ error ("[Lync] Reliable remote not found")
50
+ end
51
+
52
+ local unreliable = folder:WaitForChild (UNRELIABLE, TIMEOUT)
53
+ if not unreliable then
54
+ error ("[Lync] Unreliable remote not found")
55
+ end
56
+
57
+ _reliable = reliable :: RemoteEvent
58
+ _unreliable = unreliable :: UnreliableRemoteEvent
59
+ end
60
+
61
+ Bridge.reliable = _reliable :: any
62
+ Bridge.unreliable = _unreliable :: any
63
+ table.freeze (Bridge)
64
+ end
65
+
66
+ return Bridge