@axpecter/lync 2.2.1 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +109 -147
  2. package/package.json +1 -1
  3. package/src/Types.luau +34 -22
  4. package/src/api/Group.luau +40 -33
  5. package/src/api/Packet.luau +140 -66
  6. package/src/api/Query.luau +103 -65
  7. package/src/api/Scope.luau +26 -10
  8. package/src/api/Signal.luau +43 -52
  9. package/src/codec/Base.luau +28 -14
  10. package/src/codec/composite/Array.luau +296 -115
  11. package/src/codec/composite/Map.luau +377 -57
  12. package/src/codec/composite/Optional.luau +10 -2
  13. package/src/codec/composite/Shared.luau +134 -64
  14. package/src/codec/composite/Struct.luau +294 -43
  15. package/src/codec/composite/Tagged.luau +21 -16
  16. package/src/codec/composite/Tuple.luau +32 -15
  17. package/src/codec/datatype/Buffer.luau +7 -7
  18. package/src/codec/datatype/CFrame.luau +34 -122
  19. package/src/codec/datatype/Color.luau +10 -10
  20. package/src/codec/datatype/Instance.luau +15 -12
  21. package/src/codec/datatype/IntVector.luau +2 -2
  22. package/src/codec/datatype/NumberRange.luau +15 -8
  23. package/src/codec/datatype/Ray.luau +13 -14
  24. package/src/codec/datatype/Rect.luau +12 -12
  25. package/src/codec/datatype/Region.luau +13 -14
  26. package/src/codec/datatype/Sequence.luau +87 -65
  27. package/src/codec/datatype/String.luau +17 -10
  28. package/src/codec/datatype/UDim.luau +10 -8
  29. package/src/codec/datatype/Vector.luau +20 -59
  30. package/src/codec/meta/Auto.luau +94 -126
  31. package/src/codec/meta/Bitfield.luau +12 -14
  32. package/src/codec/meta/Custom.luau +3 -1
  33. package/src/codec/meta/DeltaScalar.luau +390 -0
  34. package/src/codec/meta/Enum.luau +9 -9
  35. package/src/codec/meta/Float.luau +6 -28
  36. package/src/codec/meta/Nothing.luau +1 -1
  37. package/src/codec/meta/Unknown.luau +10 -7
  38. package/src/codec/primitive/Bool.luau +6 -4
  39. package/src/codec/primitive/Float16.luau +5 -2
  40. package/src/codec/primitive/Int.luau +5 -6
  41. package/src/codec/primitive/Number.luau +6 -4
  42. package/src/codec/primitive/Signed.luau +2 -2
  43. package/src/codec/primitive/Varint.luau +76 -39
  44. package/src/codec/primitive/Zint.luau +99 -0
  45. package/src/index.d.ts +207 -53
  46. package/src/init.luau +116 -103
  47. package/src/internal/Baseline.luau +9 -1
  48. package/src/internal/Channel.luau +191 -157
  49. package/src/internal/Middleware.luau +22 -6
  50. package/src/internal/Pool.luau +12 -4
  51. package/src/internal/Registry.luau +25 -16
  52. package/src/internal/Transport.luau +1 -1
  53. package/src/transport/Bridge.luau +47 -33
  54. package/src/transport/Client.luau +25 -21
  55. package/src/transport/Gate.luau +227 -172
  56. package/src/transport/Reader.luau +174 -106
  57. package/src/transport/Server.luau +46 -57
  58. package/src/util/Array.luau +18 -0
  59. package/src/util/Buffer.luau +90 -0
  60. package/src/util/Constants.luau +30 -0
  61. package/src/util/Log.luau +68 -0
  62. package/src/util/Player.luau +14 -0
  63. package/src/util/Quantize.luau +84 -0
  64. package/src/util/Quat.luau +124 -0
  65. package/src/internal/Util.luau +0 -26
@@ -6,38 +6,82 @@ local Players = game:GetService("Players")
6
6
  local RunService = game:GetService("RunService")
7
7
 
8
8
  local Channel = require(script.Parent.Parent.internal.Channel)
9
+ local Log = require(script.Parent.Parent.util.Log)
10
+ local Player = require(script.Parent.Parent.util.Player)
9
11
  local Registry = require(script.Parent.Parent.internal.Registry)
10
12
  local Transport = require(script.Parent.Parent.internal.Transport)
11
13
  local Types = require(script.Parent.Parent.Types)
12
- local Util = require(script.Parent.Parent.internal.Util)
13
14
 
14
15
  -- Constants --------------------------------------------------------------
15
16
 
16
17
  local IS_SERVER = RunService:IsServer()
17
18
  local DEFAULT_TIMEOUT = 5
18
19
 
20
+ local EMPTY_OPTIONS: Types.QueryOptions = table.freeze({})
21
+
19
22
  -- State ------------------------------------------------------------------
20
23
 
21
24
  local _nextCorrId = 0
25
+ -- Correlation -> waiting thread / completion closure. Cleared on resolve or timeout.
22
26
  local _pending: { [number]: thread } = {}
23
27
  local _pendingGather: { [number]: (any) -> () } = {}
24
28
 
25
29
  -- Private ----------------------------------------------------------------
26
30
 
27
- local isPlayer = Util.isPlayer
31
+ local isPlayer = Player.is
28
32
 
29
33
  local function nextCorr(): number
30
34
  _nextCorrId += 1
31
35
  return _nextCorrId
32
36
  end
33
37
 
38
+ local function writeQueryTracked(
39
+ ch: Types.ChannelState,
40
+ reg: Types.Registration,
41
+ corrId: number,
42
+ codec: Types.InternalCodec<any>?,
43
+ data: any
44
+ ): ()
45
+ if Channel.statsEnabled() then
46
+ local before = ch.cursor
47
+ Channel.writeQuery(ch, reg.id, corrId, codec, data)
48
+ reg.bytesSent += ch.cursor - before
49
+ else
50
+ Channel.writeQuery(ch, reg.id, corrId, codec, data)
51
+ end
52
+ end
53
+
54
+ local function collectTargets(target: any): { Player }
55
+ local targets: { Player } = {}
56
+ if typeof(target) ~= "table" then
57
+ return targets
58
+ end
59
+
60
+ local kind = target._lyncKind
61
+ if kind == "all" then
62
+ for _, p in Players:GetPlayers() do
63
+ table.insert(targets, p)
64
+ end
65
+ elseif kind == "group" then
66
+ for p in target._members do
67
+ table.insert(targets, p)
68
+ end
69
+ else
70
+ for _, p in target do
71
+ if isPlayer(p) then
72
+ table.insert(targets, p :: Player)
73
+ end
74
+ end
75
+ end
76
+ return targets
77
+ end
78
+
34
79
  -- Public types -----------------------------------------------------------
35
80
 
36
81
  --[[
37
82
  Server `request(data, target)` returns `Resp?` for a Player target and
38
- `{ [Player]: Resp? }` for a multi-target table; Luau doesn't support
39
- overloads on table fields, so the type advertises `Resp?` (the common
40
- single-target case). Multi-target callers must cast the return.
83
+ `{ [Player]: Resp? }` for a multi-target table; Luau lacks overloads on
84
+ table fields, so the type advertises `Resp?` and multi-target callers cast.
41
85
  ]]
42
86
  export type QueryHandle<Req, Resp> = {
43
87
  handle: (
@@ -59,10 +103,11 @@ function Query.define<Req, Resp>(
59
103
  responseCodec: Types.InternalCodec<Resp>,
60
104
  options: Types.QueryOptions?
61
105
  ): QueryHandle<Req, Resp>
62
- local opts = options or ({} :: Types.QueryOptions)
106
+ local opts = options or EMPTY_OPTIONS
63
107
  local timeout = opts.timeout or DEFAULT_TIMEOUT
64
108
 
65
109
  local reqOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, name)
110
+ -- Embedded "\0resp" suffix stays uniquely paired with the request reg.
66
111
  local respName = `{name}\0resp`
67
112
  local respOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, respName)
68
113
 
@@ -98,6 +143,7 @@ function Query.define<Req, Resp>(
98
143
 
99
144
  reqReg.partner = respReg.id
100
145
 
146
+ -- Single active handler; replacing it disconnects the prior one via handlerToken.
101
147
  local activeHandler: ((any, Player?) -> any)? = nil
102
148
  local handlerToken = 0
103
149
 
@@ -122,18 +168,22 @@ function Query.define<Req, Resp>(
122
168
  end
123
169
 
124
170
  local ok, response = pcall(handler, value, sender)
171
+ if not ok then
172
+ Log.warn(`query "{name}" handler errored: {response}`)
173
+ end
174
+ -- pcall failure path returns nil response (writeQuery encodes a nil-MSB header).
125
175
  local payloadCodec = if ok then responseCodec else nil
126
176
  local payloadData = if ok then response else nil
127
177
 
128
- local ch = if IS_SERVER and sender
129
- then Transport.server().getChannel(sender, false)
130
- elseif not IS_SERVER then Transport.client().getChannel(false)
131
- else nil
178
+ local ch: Types.ChannelState? = if IS_SERVER
179
+ then (if sender then Transport.server().getChannel(sender, false) else nil)
180
+ else Transport.client().getChannel(false)
132
181
  if ch then
133
- Channel.writeQuery(ch, respReg.id, corrId, payloadCodec, payloadData)
182
+ writeQueryTracked(ch, respReg, corrId, payloadCodec, payloadData)
134
183
  end
135
184
  end)
136
185
 
186
+ -- Resolve the waiting thread with nil after `timeout` seconds.
137
187
  local function scheduleSingleTimeout(corrId: number, running: thread): ()
138
188
  task.delay(timeout, function()
139
189
  if _pending[corrId] == running then
@@ -143,10 +193,10 @@ function Query.define<Req, Resp>(
143
193
  end)
144
194
  end
145
195
 
146
- local function requestSingle(target: Player, data: any): any
196
+ -- Issue a single-target query on `ch` and yield until reply or timeout.
197
+ local function requestOnChannel(ch: Types.ChannelState, data: any): any
147
198
  local corrId = nextCorr()
148
- local ch = Transport.server().getChannel(target, false)
149
- Channel.writeQuery(ch, reqReg.id, corrId, requestCodec, data)
199
+ writeQueryTracked(ch, reqReg, corrId, requestCodec, data)
150
200
 
151
201
  local running = coroutine.running()
152
202
  _pending[corrId] = running
@@ -155,30 +205,41 @@ function Query.define<Req, Resp>(
155
205
  end
156
206
 
157
207
  local function requestMulti(targets: { Player }, data: any): { [Player]: any }
158
- if #targets == 0 then
208
+ local targetCount = #targets
209
+ if targetCount == 0 then
159
210
  return {}
160
211
  end
161
212
 
162
213
  local results: { [Player]: any } = {}
163
- local remaining = #targets
214
+ local remaining = targetCount
164
215
  local running = coroutine.running()
165
216
  local resolved = false
217
+ local corrIds = table.create(targetCount)
166
218
 
219
+ --[[
220
+ On timeout, surface the partial result table AND evict every
221
+ still-pending closure. Without eviction each unanswered closure
222
+ keeps `results`/`remaining`/`resolveOnce` alive: a slow leak.
223
+ ]]
167
224
  local function resolveOnce(): ()
168
225
  if resolved then
169
226
  return
170
227
  end
171
228
  resolved = true
229
+ for i = 1, targetCount do
230
+ _pendingGather[corrIds[i]] = nil
231
+ end
172
232
  task.spawn(running, results)
173
233
  end
174
234
 
175
235
  local Server = Transport.server()
176
- for _, player in targets do
236
+ for i, p in targets do
177
237
  local pCorr = nextCorr()
178
- local ch = Server.getChannel(player, false)
179
- Channel.writeQuery(ch, reqReg.id, pCorr, requestCodec, data)
238
+ corrIds[i] = pCorr
239
+ local ch = Server.getChannel(p, false)
240
+ writeQueryTracked(ch, reqReg, pCorr, requestCodec, data)
180
241
 
181
- local pRef = player
242
+ local pRef = p
182
243
  _pendingGather[pCorr] = function(response: any)
183
244
  results[pRef] = response
184
245
  remaining -= 1
@@ -192,54 +253,28 @@ function Query.define<Req, Resp>(
192
253
  return coroutine.yield()
193
254
  end
194
255
 
195
- local function collectTargets(target: any): { Player }
196
- local targets: { Player } = {}
197
- if typeof(target) ~= "table" then
198
- return targets
199
- end
200
-
201
- if target._lyncAll then
202
- for _, p in Players:GetPlayers() do
203
- table.insert(targets, p)
204
- end
205
- elseif target._lyncGroup then
206
- for player in target._members do
207
- table.insert(targets, player)
208
- end
209
- else
210
- for _, p in target do
211
- if isPlayer(p) then
212
- table.insert(targets, p :: Player)
213
- end
214
- end
215
- end
216
- return targets
217
- end
218
-
219
256
  local function requestFromServer(data: any, target: any): any
220
257
  if target == nil then
221
- error(`[Lync] Query.request: "{name}" server request requires a target`)
258
+ Log.error(`"{name}" server request requires a target`)
222
259
  end
223
260
  if isPlayer(target) then
224
- return requestSingle(target :: Player, data)
261
+ return requestOnChannel(Transport.server().getChannel(target :: Player, false), data)
225
262
  end
226
263
  return requestMulti(collectTargets(target), data)
227
264
  end
228
265
 
229
266
  local function requestFromClient(data: any): any
230
- local corrId = nextCorr()
231
- local ch = Transport.client().getChannel(false)
232
- Channel.writeQuery(ch, reqReg.id, corrId, requestCodec, data)
233
-
234
- local running = coroutine.running()
235
- _pending[corrId] = running
236
- scheduleSingleTimeout(corrId, running)
237
- return coroutine.yield()
267
+ return requestOnChannel(Transport.client().getChannel(false), data)
238
268
  end
239
269
 
240
270
  type Self = QueryHandle<Req, Resp>
241
271
 
242
272
  local handle = {
273
+ --[[
274
+ Replace the active handler. The returned Connection only clears
275
+ the active slot if no later :handle has overwritten it: token
276
+ comparison guards against a stale disconnect wiping a fresh handler.
277
+ ]]
243
278
  handle = function(
244
279
  _self: Self,
245
280
  fn: (request: Req, player: Player?) -> Resp?
@@ -248,17 +283,20 @@ function Query.define<Req, Resp>(
248
283
  local myToken = handlerToken
249
284
  activeHandler = fn :: any
250
285
 
251
- local conn = { connected = true }
252
- function conn.disconnect(self): ()
253
- if not self.connected then
254
- return
255
- end
256
- self.connected = false
257
- if handlerToken == myToken then
258
- activeHandler = nil
259
- end
260
- end
261
- return conn :: Types.Connection
286
+ local conn: Types.Connection
287
+ conn = {
288
+ connected = true,
289
+ disconnect = function(self): ()
290
+ if not self.connected then
291
+ return
292
+ end
293
+ self.connected = false
294
+ if handlerToken == myToken then
295
+ activeHandler = nil
296
+ end
297
+ end,
298
+ }
299
+ return conn
262
300
  end,
263
301
 
264
302
  request = function(_self: Self, data: Req, target: any?): Resp?
@@ -1,7 +1,6 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Batched connection lifecycle. Accepts Lync Connection,
4
- -- RBXScriptConnection, and any object with :disconnect() / :Disconnect().
3
+ -- Batched connection lifecycle for Lync, RBXScriptConnection, and disconnect()-style objects.
5
4
 
6
5
  local Types = require(script.Parent.Parent.Types)
7
6
 
@@ -25,21 +24,29 @@ local function pushConnection(self: Scope, conn: any): ()
25
24
  self._connections[idx] = conn
26
25
  end
27
26
 
27
+ -- Routes :on / :once through `methodName` so both call sites share insert + size bookkeeping.
28
+ local function attach(
29
+ self: Scope,
30
+ source: any,
31
+ fn: (...any) -> (),
32
+ methodName: string
33
+ ): Types.Connection
34
+ local conn = source[methodName](source, fn)
35
+ pushConnection(self, conn)
36
+ return conn
37
+ end
38
+
28
39
  -- Public -----------------------------------------------------------------
29
40
 
30
41
  local ScopeMeta = {}
31
42
  ScopeMeta.__index = ScopeMeta
32
43
 
33
44
  function ScopeMeta.on(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
34
- local conn = source:on(fn)
35
- pushConnection(self, conn)
36
- return conn
45
+ return attach(self, source, fn, "on")
37
46
  end
38
47
 
39
48
  function ScopeMeta.once(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
40
- local conn = source:once(fn)
41
- pushConnection(self, conn)
42
- return conn
49
+ return attach(self, source, fn, "once")
43
50
  end
44
51
 
45
52
  ScopeMeta.add = pushConnection
@@ -48,6 +55,11 @@ function ScopeMeta.size(self: Scope): number
48
55
  return self._count
49
56
  end
50
57
 
58
+ --[[
59
+ Disconnect every tracked source. The shape sniff handles three kinds:
60
+ Roblox RBXScriptConnection (:Disconnect), Lync Connection (:disconnect),
61
+ and tables exposing either capitalization.
62
+ ]]
51
63
  function ScopeMeta.destroy(self: Scope): ()
52
64
  local conns = self._connections
53
65
  local count = self._count
@@ -56,8 +68,12 @@ function ScopeMeta.destroy(self: Scope): ()
56
68
  conns[i] = nil
57
69
  if typeof(conn) == "RBXScriptConnection" then
58
70
  (conn :: RBXScriptConnection):Disconnect()
59
- elseif typeof(conn) == "table" and conn.disconnect then
60
- conn:disconnect()
71
+ elseif typeof(conn) == "table" then
72
+ if conn.disconnect then
73
+ conn:disconnect()
74
+ elseif conn.Disconnect then
75
+ conn:Disconnect()
76
+ end
61
77
  end
62
78
  end
63
79
  self._count = 0
@@ -1,6 +1,6 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- O(1) disconnect signal system with thread reuse and reentrancy-safe fire.
3
+ -- O(1) disconnect signal with thread reuse and reentrancy-safe fire.
4
4
 
5
5
  local Types = require(script.Parent.Parent.Types)
6
6
 
@@ -12,6 +12,7 @@ local STATE_ONCE = 2
12
12
 
13
13
  -- State ------------------------------------------------------------------
14
14
 
15
+ -- Single recyclable thread for async dispatch; saves coroutine.create per fire.
15
16
  local _freeThread: thread? = nil
16
17
 
17
18
  -- Private ----------------------------------------------------------------
@@ -41,14 +42,19 @@ type Entry = {
41
42
  fn: (...any) -> (),
42
43
  _state: number,
43
44
  connected: boolean,
44
- _signalEntries: { Entry },
45
- _signalCount: { number },
45
+ _signal: SignalFields,
46
46
  _index: number,
47
47
  }
48
48
 
49
+ type SignalFields = {
50
+ _entries: { Entry },
51
+ _count: number,
52
+ }
53
+
49
54
  local EntryImpl = {}
50
55
  EntryImpl.__index = EntryImpl
51
56
 
57
+ -- Swap-with-last for O(1) disconnect; the moved entry's _index is patched too.
52
58
  local function disconnectEntry(entry: Entry): ()
53
59
  if entry._state == STATE_DISCONNECTED then
54
60
  return
@@ -57,9 +63,10 @@ local function disconnectEntry(entry: Entry): ()
57
63
  entry._state = STATE_DISCONNECTED
58
64
  entry.connected = false
59
65
 
60
- local entries = entry._signalEntries
61
- local countRef = entry._signalCount
62
- local idx, last = entry._index, countRef[1]
66
+ local signal = entry._signal
67
+ local entries = signal._entries
68
+ local idx = entry._index
69
+ local last = signal._count
63
70
 
64
71
  if idx < last then
65
72
  local moved = entries[last]
@@ -67,31 +74,40 @@ local function disconnectEntry(entry: Entry): ()
67
74
  moved._index = idx
68
75
  end
69
76
  entries[last] = nil
70
- countRef[1] = last - 1
77
+ signal._count = last - 1
71
78
  end
72
79
 
73
80
  EntryImpl.disconnect = disconnectEntry
74
81
  table.freeze(EntryImpl)
75
82
 
76
- type SignalFields = {
77
- _entries: { Entry },
78
- _count: { number },
79
- }
83
+ -- Auto-disconnects ONCE entries before invoking; chooses sync vs spawned thread.
84
+ local function invokeEntry(entry: Entry, sync: boolean, ...: any): ()
85
+ local state = entry._state
86
+ if state == STATE_DISCONNECTED then
87
+ return
88
+ end
89
+ if state == STATE_ONCE then
90
+ disconnectEntry(entry)
91
+ end
92
+ if sync then
93
+ entry.fn(...)
94
+ else
95
+ spawnCallback(entry.fn, ...)
96
+ end
97
+ end
80
98
 
81
99
  local SignalImpl = {}
82
100
  SignalImpl.__index = SignalImpl
83
101
 
84
102
  local function newEntry(self: SignalFields, callback: (...any) -> (), state: number): Entry
85
- local countRef = self._count
86
- local idx = countRef[1] + 1
87
- countRef[1] = idx
103
+ local idx = self._count + 1
104
+ self._count = idx
88
105
 
89
106
  local entry: Entry = setmetatable({
90
107
  fn = callback,
91
108
  _state = state,
92
109
  connected = true,
93
- _signalEntries = self._entries,
94
- _signalCount = countRef,
110
+ _signal = self,
95
111
  _index = idx,
96
112
  }, EntryImpl) :: any
97
113
 
@@ -118,53 +134,28 @@ function SignalImpl.wait(self: SignalFields): ...any
118
134
  end
119
135
 
120
136
  local function dispatch(self: SignalFields, sync: boolean, ...: any): ()
121
- local count = self._count[1]
137
+ local count = self._count
122
138
  if count == 0 then
123
139
  return
124
140
  end
125
141
 
126
142
  local entries = self._entries
127
143
 
144
+ -- Single-listener fast path skips the snapshot allocation.
128
145
  if count == 1 then
129
- local entry = entries[1]
130
- local state = entry._state
131
- if state == STATE_ONCE then
132
- disconnectEntry(entry)
133
- if sync then
134
- entry.fn(...)
135
- else
136
- spawnCallback(entry.fn, ...)
137
- end
138
- elseif state == STATE_CONNECTED then
139
- if sync then
140
- entry.fn(...)
141
- else
142
- spawnCallback(entry.fn, ...)
143
- end
144
- end
146
+ invokeEntry(entries[1], sync, ...)
145
147
  return
146
148
  end
147
149
 
150
+ --[[
151
+ Re-entrancy: a handler may disconnect or add others mid-fire.
152
+ Snapshot the entry list so the iteration view is stable.
153
+ ]]
148
154
  local snapshot = table.create(count)
149
155
  table.move(entries, 1, count, 1, snapshot)
150
156
 
151
157
  for i = 1, count do
152
- local entry = snapshot[i]
153
- local state = entry._state
154
- if state == STATE_ONCE then
155
- disconnectEntry(entry)
156
- if sync then
157
- entry.fn(...)
158
- else
159
- spawnCallback(entry.fn, ...)
160
- end
161
- elseif state == STATE_CONNECTED then
162
- if sync then
163
- entry.fn(...)
164
- else
165
- spawnCallback(entry.fn, ...)
166
- end
167
- end
158
+ invokeEntry(snapshot[i], sync, ...)
168
159
  end
169
160
  end
170
161
 
@@ -177,11 +168,11 @@ function SignalImpl.fireSync(self: SignalFields, ...: any): ()
177
168
  end
178
169
 
179
170
  function SignalImpl.count(self: SignalFields): number
180
- return self._count[1]
171
+ return self._count
181
172
  end
182
173
 
183
174
  function SignalImpl.hasListeners(self: SignalFields): boolean
184
- return self._count[1] > 0
175
+ return self._count > 0
185
176
  end
186
177
 
187
178
  table.freeze(SignalImpl)
@@ -191,7 +182,7 @@ table.freeze(SignalImpl)
191
182
  local SignalModule = {}
192
183
 
193
184
  function SignalModule.create(): Types.SignalLike
194
- return (setmetatable({ _entries = {}, _count = { 0 } }, SignalImpl) :: any) :: Types.SignalLike
185
+ return (setmetatable({ _entries = {}, _count = 0 }, SignalImpl) :: any) :: Types.SignalLike
195
186
  end
196
187
 
197
188
  return table.freeze(SignalModule)
@@ -1,7 +1,8 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Fixed-size codec factory and channel buffer-grow primitive.
3
+ -- Fixed-size codec factory + channel buffer-grow primitive.
4
4
 
5
+ local Log = require(script.Parent.Parent.util.Log)
5
6
  local Types = require(script.Parent.Parent.Types)
6
7
 
7
8
  -- Constants --------------------------------------------------------------
@@ -19,9 +20,9 @@ local countlz = bit32.countlz
19
20
  local lshift = bit32.lshift
20
21
 
21
22
  --[[
22
- Grow `ch.buff` so `ch.cursor + bytes` fits. New size is the next
23
- power of two >= needed, capped by _maxSize. Caller MUST re-read
24
- `ch.buff` after this returns: the buffer reference is replaced.
23
+ Grow `ch.buff` so cursor + bytes fits. New size is the next power of two
24
+ >= needed, capped by _maxSize. Callers MUST re-read `ch.buff` after
25
+ return: the buffer reference is replaced.
25
26
  ]]
26
27
  local function alloc(ch: Types.ChannelState, bytes: number): ()
27
28
  if bytes == 0 then
@@ -36,9 +37,9 @@ local function alloc(ch: Types.ChannelState, bytes: number): ()
36
37
  if needed > _maxSize then
37
38
  local name = ch.currentPacket
38
39
  if name then
39
- error(`[Lync] Channel.alloc("{name}"): buffer overflow ({needed}B, max {_maxSize}B)`)
40
+ Log.error(`packet "{name}" overflowed at {needed}B (max {_maxSize}B)`)
40
41
  end
41
- error(`[Lync] Channel.alloc: buffer overflow ({needed}B, max {_maxSize}B)`)
42
+ Log.error(`overflowed at {needed}B (max {_maxSize}B)`)
42
43
  end
43
44
 
44
45
  local newSize = lshift(1, 32 - countlz(needed - 1))
@@ -53,18 +54,31 @@ end
53
54
  local Base = {}
54
55
 
55
56
  Base.INITIAL_BUF = INITIAL_BUF
56
-
57
57
  Base.alloc = alloc
58
58
 
59
+ -- Cheap inline guard hoisted into every codec that grows a channel directly.
60
+ function Base.ensure(ch: Types.ChannelState, n: number): ()
61
+ if ch.cursor + n > ch.size then
62
+ alloc(ch, n)
63
+ end
64
+ end
65
+
66
+ -- Floor is INITIAL_BUF: anything smaller errors on the very first write.
59
67
  function Base.setMaxSize(bytes: number): ()
68
+ if bytes < INITIAL_BUF then
69
+ Log.error(`bytes ({bytes}) must be >= {INITIAL_BUF}`)
70
+ end
60
71
  _maxSize = bytes
61
72
  end
62
73
 
74
+ function Base.reset(): ()
75
+ _maxSize = DEFAULT_MAX
76
+ end
77
+
63
78
  --[[
64
- Build a fixed-size codec. The codec exposes channel-level write/read
65
- (which handles buffer growth and cursor bookkeeping) plus the raw
66
- _directWrite / _directRead primitives composite codecs use in their
67
- pre-sized fast paths.
79
+ Build a fixed-size codec. Exposes channel-level write/read (handles
80
+ buffer growth + cursor) plus raw _directWrite/_directRead for composite
81
+ fast paths that pre-size the channel themselves.
68
82
  ]]
69
83
  function Base.define<T>(
70
84
  size: number,
@@ -101,9 +115,9 @@ function Base.define<T>(
101
115
  end
102
116
 
103
117
  --[[
104
- Constant codec: zero bytes wire, decode always returns `value`. Used
105
- by quantizers when min == max so the wire emits nothing yet decode
106
- still produces a type-faithful value.
118
+ Zero-byte codec: writes nothing, reads always return `value`. Used by
119
+ quantizers when min == max so the wire emits nothing yet decode produces
120
+ a type-faithful value.
107
121
  ]]
108
122
  function Base.constant<T>(value: T): Types.InternalCodec<T>
109
123
  return table.freeze({