@axpecter/lync 1.5.2 → 2.1.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 (55) hide show
  1. package/README.md +499 -357
  2. package/package.json +2 -2
  3. package/src/Types.luau +69 -27
  4. package/src/api/Group.luau +101 -106
  5. package/src/api/Packet.luau +191 -157
  6. package/src/api/Query.luau +223 -282
  7. package/src/api/Scope.luau +46 -57
  8. package/src/api/Signal.luau +104 -137
  9. package/src/codec/Base.luau +69 -20
  10. package/src/codec/composite/Array.luau +189 -201
  11. package/src/codec/composite/Map.luau +97 -341
  12. package/src/codec/composite/Optional.luau +27 -23
  13. package/src/codec/composite/Shared.luau +96 -65
  14. package/src/codec/composite/Struct.luau +201 -339
  15. package/src/codec/composite/Tagged.luau +64 -178
  16. package/src/codec/composite/Tuple.luau +81 -95
  17. package/src/codec/datatype/Buffer.luau +49 -21
  18. package/src/codec/datatype/CFrame.luau +207 -30
  19. package/src/codec/datatype/Color.luau +21 -11
  20. package/src/codec/datatype/Instance.luau +29 -19
  21. package/src/codec/datatype/IntVector.luau +33 -21
  22. package/src/codec/datatype/NumberRange.luau +19 -10
  23. package/src/codec/datatype/Ray.luau +26 -22
  24. package/src/codec/datatype/Rect.luau +20 -16
  25. package/src/codec/datatype/Region.luau +51 -45
  26. package/src/codec/datatype/Sequence.luau +64 -56
  27. package/src/codec/datatype/String.luau +89 -56
  28. package/src/codec/datatype/UDim.luau +40 -22
  29. package/src/codec/datatype/Vector.luau +181 -17
  30. package/src/codec/meta/Auto.luau +295 -253
  31. package/src/codec/meta/Bitfield.luau +104 -148
  32. package/src/codec/meta/Custom.luau +8 -4
  33. package/src/codec/meta/Enum.luau +29 -57
  34. package/src/codec/meta/Float.luau +64 -0
  35. package/src/codec/meta/Nothing.luau +9 -5
  36. package/src/codec/meta/Unknown.luau +44 -35
  37. package/src/codec/primitive/Bool.luau +16 -26
  38. package/src/codec/primitive/Float16.luau +58 -64
  39. package/src/codec/primitive/Int.luau +68 -0
  40. package/src/codec/primitive/Number.luau +21 -43
  41. package/src/codec/primitive/Varint.luau +67 -46
  42. package/src/index.d.ts +249 -306
  43. package/src/init.luau +332 -173
  44. package/src/internal/Baseline.luau +29 -24
  45. package/src/internal/Channel.luau +346 -161
  46. package/src/internal/Middleware.luau +60 -85
  47. package/src/internal/Pool.luau +19 -30
  48. package/src/internal/Registry.luau +61 -101
  49. package/src/transport/Bridge.luau +64 -43
  50. package/src/transport/Client.luau +60 -117
  51. package/src/transport/Gate.luau +282 -133
  52. package/src/transport/Reader.luau +159 -100
  53. package/src/transport/Server.luau +147 -292
  54. package/src/api/Namespace.luau +0 -251
  55. package/src/codec/meta/Quantized.luau +0 -174
@@ -1,119 +1,94 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Ordered intercept chains for send and receive.
3
+ -- Ordered intercept chains returning Connection objects.
4
4
 
5
- type Handler = (data: any, name: string, player: Player?) -> any?
5
+ local Signal = require(script.Parent.Parent.api.Signal)
6
+ local Types = require(script.Parent.Parent.Types)
6
7
 
7
- type Chain = {
8
- handlers: { Handler },
9
- snapshot: { Handler }?,
10
- }
8
+ -- State ---------------------------------------------------------------
11
9
 
12
- -- Constants -----------------------------------------------------------
10
+ local _sendSignal = Signal.create()
11
+ local _receiveSignal = Signal.create()
12
+ local _dropSignal = Signal.create()
13
13
 
14
- local DROP = table.freeze({ _tag = "drop" })
14
+ -- Public --------------------------------------------------------------
15
15
 
16
- -- State ---------------------------------------------------------------
16
+ local Middleware = {}
17
17
 
18
- local _send: Chain = { handlers = {}, snapshot = nil }
19
- local _receive: Chain = { handlers = {}, snapshot = nil }
18
+ function Middleware.onSend(fn: (data: any, name: string, player: Player?) -> any): Types.Connection
19
+ return _sendSignal:connect(fn)
20
+ end
20
21
 
21
- -- Private -------------------------------------------------------------
22
+ function Middleware.onReceive(
23
+ fn: (data: any, name: string, player: Player?) -> any
24
+ ): Types.Connection
25
+ return _receiveSignal:connect(fn)
26
+ end
22
27
 
23
- local function runChain(chain: { Handler }, data: any, name: string, player: Player?): any?
24
- local current = data
25
- for i = 1, #chain do
26
- local result = chain[i](current, name, player)
27
- if result == nil or result == DROP then
28
- return nil
29
- end
30
- current = result
31
- end
32
- return current
28
+ function Middleware.onDrop(
29
+ fn: (player: Player, reason: string, name: string, data: any?) -> ()
30
+ ): Types.Connection
31
+ return _dropSignal:connect(fn)
33
32
  end
34
33
 
35
- local function run(chain: Chain, data: any, name: string, player: Player?): any?
36
- local handlers = chain.handlers
37
- local count = #handlers
34
+ --[[
35
+ Runs send middleware chain. Returns transformed data, or DROP sentinel.
36
+ If no hooks, returns data unchanged (zero overhead).
37
+ ]]
38
+ function Middleware.runSend(data: any, name: string, player: Player?): any
39
+ local count = _sendSignal._count[1]
38
40
  if count == 0 then
39
41
  return data
40
42
  end
41
43
 
42
- local snapshot = chain.snapshot
43
- if not snapshot then
44
- snapshot = table.clone(handlers)
45
- chain.snapshot = snapshot
46
- end
47
-
48
- local ok: boolean
49
- local result: any
50
-
51
- if count == 1 then
52
- ok, result = pcall(snapshot[1], data, name, player)
53
- else
54
- ok, result = pcall(runChain, snapshot, data, name, player)
55
- end
44
+ local entries = _sendSignal._entries
45
+ local result = data
56
46
 
57
- if not ok then
58
- warn(`[Lync] Middleware error on "{name}": {result}`)
59
- return nil
60
- end
61
-
62
- if result == DROP then
63
- return nil
47
+ for i = 1, count do
48
+ local entry = entries[i]
49
+ if entry._state ~= 0 then -- not disconnected
50
+ local returned = entry.fn(result, name, player)
51
+ if returned ~= nil then
52
+ result = returned
53
+ end
54
+ end
64
55
  end
65
56
 
66
57
  return result
67
58
  end
68
59
 
69
- local function addTo(chain: Chain, fn: Handler): () -> ()
70
- table.insert(chain.handlers, fn)
71
- chain.snapshot = nil
72
-
73
- return function(): ()
74
- local pos = table.find(chain.handlers, fn)
75
- if pos then
76
- table.remove(chain.handlers, pos)
77
- chain.snapshot = nil
78
- end
60
+ function Middleware.runReceive(data: any, name: string, player: Player?): any
61
+ local count = _receiveSignal._count[1]
62
+ if count == 0 then
63
+ return data
79
64
  end
80
- end
81
-
82
- -- Public --------------------------------------------------------------
83
65
 
84
- local Middleware = {}
66
+ local entries = _receiveSignal._entries
67
+ local result = data
85
68
 
86
- Middleware.hasSend = false
87
- Middleware.hasReceive = false
88
- Middleware.DROP = DROP
89
-
90
- function Middleware.addSend(fn: Handler): () -> ()
91
- local remover = addTo(_send, fn)
92
- Middleware.hasSend = true
93
-
94
- return function(): ()
95
- remover()
96
- Middleware.hasSend = #_send.handlers > 0
69
+ for i = 1, count do
70
+ local entry = entries[i]
71
+ if entry._state ~= 0 then
72
+ local returned = entry.fn(result, name, player)
73
+ if returned ~= nil then
74
+ result = returned
75
+ end
76
+ end
97
77
  end
98
- end
99
78
 
100
- function Middleware.addReceive(fn: Handler): () -> ()
101
- local remover = addTo(_receive, fn)
102
- Middleware.hasReceive = true
79
+ return result
80
+ end
103
81
 
104
- return function(): ()
105
- remover()
106
- Middleware.hasReceive = #_receive.handlers > 0
107
- end
82
+ function Middleware.fireDrop(player: Player, reason: string, name: string, data: any?): ()
83
+ _dropSignal:fireSync(player, reason, name, data)
108
84
  end
109
85
 
110
- function Middleware.runSend(data: any, name: string, player: Player?): any?
111
- return run(_send, data, name, player)
86
+ function Middleware.hasSendHooks(): boolean
87
+ return _sendSignal._count[1] > 0
112
88
  end
113
89
 
114
- function Middleware.runReceive(data: any, name: string, player: Player?): any?
115
- return run(_receive, data, name, player)
90
+ function Middleware.hasReceiveHooks(): boolean
91
+ return _receiveSignal._count[1] > 0
116
92
  end
117
93
 
118
- -- Not frozen: hasSend and hasReceive are mutated at runtime.
119
- return Middleware
94
+ return table.freeze(Middleware)
@@ -1,67 +1,56 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Stack-based ChannelState pool with warmup support.
3
+ -- Stack-based ChannelState pool for per-player channel reuse.
4
4
 
5
5
  local Channel = require(script.Parent.Channel)
6
6
  local Types = require(script.Parent.Parent.Types)
7
7
 
8
- type ChannelState = Types.ChannelState
9
-
10
8
  -- Constants -----------------------------------------------------------
11
9
 
12
- local DEFAULT_MAX = 16
10
+ local DEFAULT_SIZE = 16
13
11
 
14
12
  -- State ---------------------------------------------------------------
15
13
 
16
- local _stack = {} :: { ChannelState }
14
+ local _stack: { Types.ChannelState } = {}
17
15
  local _depth = 0
18
- local _maxSize = DEFAULT_MAX
16
+ local _maxSize = DEFAULT_SIZE
19
17
 
20
18
  -- Public --------------------------------------------------------------
21
19
 
22
20
  local Pool = {}
23
21
 
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
22
+ function Pool.acquire(): Types.ChannelState
37
23
  if _depth > 0 then
38
24
  local ch = _stack[_depth]
39
25
  _stack[_depth] = nil :: any
40
26
  _depth -= 1
27
+ Channel.reset(ch)
41
28
  return ch
42
29
  end
43
30
 
44
31
  return Channel.create()
45
32
  end
46
33
 
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
-
34
+ function Pool.release(ch: Types.ChannelState): ()
56
35
  if _depth >= _maxSize then
57
- return -- discard; GC will reclaim
36
+ return
58
37
  end
59
38
 
60
39
  _depth += 1
61
40
  _stack[_depth] = ch
62
41
  end
63
42
 
64
- function Pool.size(): number
43
+ function Pool.setMaxSize(size: number): ()
44
+ _maxSize = size
45
+
46
+ -- Trim excess
47
+ while _depth > _maxSize do
48
+ _stack[_depth] = nil :: any
49
+ _depth -= 1
50
+ end
51
+ end
52
+
53
+ function Pool.count(): number
65
54
  return _depth
66
55
  end
67
56
 
@@ -1,146 +1,106 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Deterministic ID assignment for packets and queries.
3
+ -- Deterministic packet/query ID assignment. Sequential 7-bit IDs.
4
4
 
5
+ local Signal = require(script.Parent.Parent.api.Signal)
5
6
  local Types = require(script.Parent.Parent.Types)
6
7
 
7
- type Codec<T> = Types.Codec<T>
8
- type Registration = Types.Registration
9
- type RateLimitConfig = Types.RateLimitConfig
10
-
11
8
  -- Constants -----------------------------------------------------------
12
9
 
13
10
  local KIND_PACKET = 0
14
11
  local KIND_REQUEST = 1
15
12
  local KIND_RESPONSE = 2
16
- local MAX_PACKETS = 255
13
+ local MAX_ID = 127
17
14
 
18
15
  -- State ---------------------------------------------------------------
19
16
 
20
- local _entries = {} :: { Registration }
21
- local _names = {} :: { [string]: number }
22
- local _nextId = 1
17
+ local _nextId = 0
18
+ local _byId: { [number]: Types.Registration } = {}
19
+ local _byName: { [string]: Types.Registration } = {}
20
+
21
+ -- Public --------------------------------------------------------------
22
+
23
+ local Registry = {}
23
24
 
24
- -- Private -------------------------------------------------------------
25
+ Registry.KIND_PACKET = KIND_PACKET
26
+ Registry.KIND_REQUEST = KIND_REQUEST
27
+ Registry.KIND_RESPONSE = KIND_RESPONSE
25
28
 
26
- local function assign(
29
+ function Registry.register(
27
30
  name: string,
28
- baseName: string,
29
- codec: Codec<any>,
30
- isUnreliable: boolean,
31
31
  kind: number,
32
- signal: any,
33
- rateLimit: RateLimitConfig?,
32
+ codec: Types.InternalCodec<any>,
33
+ isUnreliable: boolean,
34
+ partner: number?,
35
+ needsGate: boolean,
36
+ headerSize: number,
37
+ timestampMode: number,
38
+ openFn: (ch: Types.ChannelState, id: number) -> (),
39
+ rateLimit: Types.RateLimitConfig?,
34
40
  validate: ((data: any, player: Player) -> (boolean, string?))?,
35
41
  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
42
+ ): Types.Registration
43
+ _nextId += 1
44
+ local id = _nextId
43
45
 
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
46
+ if id > MAX_ID then
47
+ error(`[Lync] Registry: ID limit exceeded ({MAX_ID}), too many packets/queries`)
51
48
  end
52
49
 
53
- if maxPayloadBytes and maxPayloadBytes <= 0 then
54
- error(`[Lync] maxPayloadBytes must be positive: "{name}"`)
50
+ if _byName[name] and kind ~= KIND_RESPONSE then
51
+ error(`[Lync] Registry: duplicate name "{name}"`)
55
52
  end
56
53
 
57
- local id = _nextId
58
- _nextId += 1
59
-
60
- local reg: Registration = {
54
+ local reg: Types.Registration = {
61
55
  id = id,
62
56
  name = name,
63
- baseName = baseName,
57
+ kind = kind,
64
58
  codec = codec,
65
59
  isUnreliable = isUnreliable,
66
- kind = kind,
67
- partner = nil,
68
- signal = signal,
60
+ partner = partner,
61
+ signal = Signal.create(),
69
62
  rateLimit = rateLimit,
70
63
  validate = validate,
71
- needsGate = rateLimit ~= nil or validate ~= nil,
64
+ needsGate = needsGate,
72
65
  maxPayloadBytes = maxPayloadBytes,
66
+ timestampMode = timestampMode,
67
+ headerSize = headerSize,
68
+ _openFn = openFn,
69
+ bytesSent = 0,
70
+ bytesReceived = 0,
71
+ fires = 0,
72
+ recvFires = 0,
73
+ drops = 0,
73
74
  }
74
75
 
75
- _entries[id] = reg
76
- _names[name] = id
76
+ _byId[id] = reg
77
+ if kind ~= KIND_RESPONSE then
78
+ _byName[name] = reg
79
+ end
80
+
77
81
  return reg
78
82
  end
79
83
 
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
- )
84
+ function Registry.get(id: number): Types.Registration?
85
+ return _byId[id]
108
86
  end
109
87
 
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
88
+ function Registry.getByName(name: string): Types.Registration?
89
+ return _byName[name]
135
90
  end
136
91
 
137
- function Registry.get(id: number): Registration?
138
- return _entries[id]
92
+ function Registry.count(): number
93
+ return _nextId
139
94
  end
140
95
 
141
- function Registry.getByName(name: string): Registration?
142
- local id = _names[name]
143
- return if id then _entries[id] else nil
96
+ function Registry.all(): { Types.Registration }
97
+ local result = table.create(_nextId)
98
+ for i = 1, _nextId do
99
+ if _byId[i] then
100
+ table.insert(result, _byId[i])
101
+ end
102
+ end
103
+ return result
144
104
  end
145
105
 
146
106
  return table.freeze(Registry)
@@ -1,67 +1,88 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Creates and exposes the network remote instances.
3
+ -- Remote instance creation and access.
4
4
 
5
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
6
5
  local RunService = game:GetService("RunService")
7
6
 
8
7
  -- Constants -----------------------------------------------------------
9
8
 
10
- local FOLDER = "_lync"
11
- local RELIABLE = "R"
12
- local UNRELIABLE = "U"
13
- local TIMEOUT = 10
9
+ local RELIABLE_NAME = "LyncReliable"
10
+ local UNRELIABLE_NAME = "LyncUnreliable"
14
11
 
15
12
  -- State ---------------------------------------------------------------
16
13
 
17
- local _reliable: RemoteEvent
18
- local _unreliable: UnreliableRemoteEvent
14
+ local _reliable: RemoteEvent? = nil
15
+ local _unreliable: UnreliableRemoteEvent? = nil
16
+ local _isServer = RunService:IsServer()
19
17
 
20
18
  -- Public --------------------------------------------------------------
21
19
 
22
20
  local Bridge = {}
23
21
 
24
- function Bridge.setup(): ()
25
- if RunService:IsServer() then
26
- local folder = Instance.new("Folder")
27
- folder.Name = FOLDER
22
+ function Bridge.setup(container: Instance): ()
23
+ if _isServer then
24
+ local re = Instance.new("RemoteEvent")
25
+ re.Name = RELIABLE_NAME
26
+ re.Parent = container
28
27
 
29
- local reliable = Instance.new("RemoteEvent")
30
- reliable.Name = RELIABLE
31
- reliable.Parent = folder
28
+ local ure = Instance.new("UnreliableRemoteEvent")
29
+ ure.Name = UNRELIABLE_NAME
30
+ ure.Parent = container
32
31
 
33
- local unreliable = Instance.new("UnreliableRemoteEvent")
34
- unreliable.Name = UNRELIABLE
35
- unreliable.Parent = folder
32
+ _reliable = re
33
+ _unreliable = ure
34
+ else
35
+ _reliable = container:WaitForChild(RELIABLE_NAME) :: RemoteEvent
36
+ _unreliable = container:WaitForChild(UNRELIABLE_NAME) :: UnreliableRemoteEvent
37
+ end
38
+ end
39
+
40
+ function Bridge.reliable(): RemoteEvent
41
+ return _reliable :: RemoteEvent
42
+ end
36
43
 
37
- folder.Parent = ReplicatedStorage
44
+ function Bridge.unreliable(): UnreliableRemoteEvent
45
+ return _unreliable :: UnreliableRemoteEvent
46
+ end
47
+
48
+ function Bridge.isServer(): boolean
49
+ return _isServer
50
+ end
38
51
 
39
- _reliable = reliable
40
- _unreliable = unreliable
52
+ function Bridge.fireServer(data: buffer, refs: { Instance }): ()
53
+ local re = _reliable :: RemoteEvent
54
+ if #refs > 0 then
55
+ re:FireServer(data, refs)
41
56
  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
57
+ re:FireServer(data)
59
58
  end
59
+ end
60
60
 
61
- Bridge.reliable = _reliable :: any
62
- Bridge.unreliable = _unreliable :: any
63
- table.freeze(Bridge)
61
+ function Bridge.fireClient(player: Player, data: buffer, refs: { Instance }): ()
62
+ local re = _reliable :: RemoteEvent
63
+ if #refs > 0 then
64
+ re:FireClient(player, data, refs)
65
+ else
66
+ re:FireClient(player, data)
67
+ end
68
+ end
69
+
70
+ function Bridge.fireServerUnreliable(data: buffer, refs: { Instance }): ()
71
+ local ure = _unreliable :: UnreliableRemoteEvent
72
+ if #refs > 0 then
73
+ ure:FireServer(data, refs)
74
+ else
75
+ ure:FireServer(data)
76
+ end
77
+ end
78
+
79
+ function Bridge.fireClientUnreliable(player: Player, data: buffer, refs: { Instance }): ()
80
+ local ure = _unreliable :: UnreliableRemoteEvent
81
+ if #refs > 0 then
82
+ ure:FireClient(player, data, refs)
83
+ else
84
+ ure:FireClient(player, data)
85
+ end
64
86
  end
65
87
 
66
- -- Not frozen at module level: setup() populates reliable/unreliable then freezes.
67
- return Bridge
88
+ return table.freeze(Bridge)